Sat, 03 Jan 2015 20:18:00 +0100
Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 const CURRENT_SCHEMA_VERSION = 23;
8 const NS_APP_USER_PROFILE_50_DIR = "ProfD";
9 const NS_APP_PROFILE_DIR_STARTUP = "ProfDS";
11 // Shortcuts to transitions type.
12 const TRANSITION_LINK = Ci.nsINavHistoryService.TRANSITION_LINK;
13 const TRANSITION_TYPED = Ci.nsINavHistoryService.TRANSITION_TYPED;
14 const TRANSITION_BOOKMARK = Ci.nsINavHistoryService.TRANSITION_BOOKMARK;
15 const TRANSITION_EMBED = Ci.nsINavHistoryService.TRANSITION_EMBED;
16 const TRANSITION_FRAMED_LINK = Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK;
17 const TRANSITION_REDIRECT_PERMANENT = Ci.nsINavHistoryService.TRANSITION_REDIRECT_PERMANENT;
18 const TRANSITION_REDIRECT_TEMPORARY = Ci.nsINavHistoryService.TRANSITION_REDIRECT_TEMPORARY;
19 const TRANSITION_DOWNLOAD = Ci.nsINavHistoryService.TRANSITION_DOWNLOAD;
21 const TITLE_LENGTH_MAX = 4096;
23 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
25 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
26 "resource://gre/modules/FileUtils.jsm");
27 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
28 "resource://gre/modules/NetUtil.jsm");
29 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
30 "resource://gre/modules/Promise.jsm");
31 XPCOMUtils.defineLazyModuleGetter(this, "Services",
32 "resource://gre/modules/Services.jsm");
33 XPCOMUtils.defineLazyModuleGetter(this, "Task",
34 "resource://gre/modules/Task.jsm");
35 XPCOMUtils.defineLazyModuleGetter(this, "BookmarkJSONUtils",
36 "resource://gre/modules/BookmarkJSONUtils.jsm");
37 XPCOMUtils.defineLazyModuleGetter(this, "BookmarkHTMLUtils",
38 "resource://gre/modules/BookmarkHTMLUtils.jsm");
39 XPCOMUtils.defineLazyModuleGetter(this, "PlacesBackups",
40 "resource://gre/modules/PlacesBackups.jsm");
41 XPCOMUtils.defineLazyModuleGetter(this, "PlacesTransactions",
42 "resource://gre/modules/PlacesTransactions.jsm");
43 XPCOMUtils.defineLazyModuleGetter(this, "OS",
44 "resource://gre/modules/osfile.jsm");
46 // This imports various other objects in addition to PlacesUtils.
47 Cu.import("resource://gre/modules/PlacesUtils.jsm");
49 XPCOMUtils.defineLazyGetter(this, "SMALLPNG_DATA_URI", function() {
50 return NetUtil.newURI(
51 "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAA" +
52 "AAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==");
53 });
55 function LOG(aMsg) {
56 aMsg = ("*** PLACES TESTS: " + aMsg);
57 Services.console.logStringMessage(aMsg);
58 print(aMsg);
59 }
61 let gTestDir = do_get_cwd();
63 // Initialize profile.
64 let gProfD = do_get_profile();
66 // Remove any old database.
67 clearDB();
69 /**
70 * Shortcut to create a nsIURI.
71 *
72 * @param aSpec
73 * URLString of the uri.
74 */
75 function uri(aSpec) NetUtil.newURI(aSpec);
78 /**
79 * Gets the database connection. If the Places connection is invalid it will
80 * try to create a new connection.
81 *
82 * @param [optional] aForceNewConnection
83 * Forces creation of a new connection to the database. When a
84 * connection is asyncClosed it cannot anymore schedule async statements,
85 * though connectionReady will keep returning true (Bug 726990).
86 *
87 * @return The database connection or null if unable to get one.
88 */
89 let gDBConn;
90 function DBConn(aForceNewConnection) {
91 if (!aForceNewConnection) {
92 let db = PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
93 .DBConnection;
94 if (db.connectionReady)
95 return db;
96 }
98 // If the Places database connection has been closed, create a new connection.
99 if (!gDBConn || aForceNewConnection) {
100 let file = Services.dirsvc.get('ProfD', Ci.nsIFile);
101 file.append("places.sqlite");
102 let dbConn = gDBConn = Services.storage.openDatabase(file);
104 // Be sure to cleanly close this connection.
105 Services.obs.addObserver(function DBCloseCallback(aSubject, aTopic, aData) {
106 Services.obs.removeObserver(DBCloseCallback, aTopic);
107 dbConn.asyncClose();
108 }, "profile-before-change", false);
109 }
111 return gDBConn.connectionReady ? gDBConn : null;
112 };
114 /**
115 * Reads data from the provided inputstream.
116 *
117 * @return an array of bytes.
118 */
119 function readInputStreamData(aStream) {
120 let bistream = Cc["@mozilla.org/binaryinputstream;1"].
121 createInstance(Ci.nsIBinaryInputStream);
122 try {
123 bistream.setInputStream(aStream);
124 let expectedData = [];
125 let avail;
126 while ((avail = bistream.available())) {
127 expectedData = expectedData.concat(bistream.readByteArray(avail));
128 }
129 return expectedData;
130 } finally {
131 bistream.close();
132 }
133 }
135 /**
136 * Reads the data from the specified nsIFile.
137 *
138 * @param aFile
139 * The nsIFile to read from.
140 * @return an array of bytes.
141 */
142 function readFileData(aFile) {
143 let inputStream = Cc["@mozilla.org/network/file-input-stream;1"].
144 createInstance(Ci.nsIFileInputStream);
145 // init the stream as RD_ONLY, -1 == default permissions.
146 inputStream.init(aFile, 0x01, -1, null);
148 // Check the returned size versus the expected size.
149 let size = inputStream.available();
150 let bytes = readInputStreamData(inputStream);
151 if (size != bytes.length) {
152 throw "Didn't read expected number of bytes";
153 }
154 return bytes;
155 }
157 /**
158 * Reads the data from the named file, verifying the expected file length.
159 *
160 * @param aFileName
161 * This file should be located in the same folder as the test.
162 * @param aExpectedLength
163 * Expected length of the file.
164 *
165 * @return The array of bytes read from the file.
166 */
167 function readFileOfLength(aFileName, aExpectedLength) {
168 let data = readFileData(do_get_file(aFileName));
169 do_check_eq(data.length, aExpectedLength);
170 return data;
171 }
174 /**
175 * Returns the base64-encoded version of the given string. This function is
176 * similar to window.btoa, but is available to xpcshell tests also.
177 *
178 * @param aString
179 * Each character in this string corresponds to a byte, and must be a
180 * code point in the range 0-255.
181 *
182 * @return The base64-encoded string.
183 */
184 function base64EncodeString(aString) {
185 var stream = Cc["@mozilla.org/io/string-input-stream;1"]
186 .createInstance(Ci.nsIStringInputStream);
187 stream.setData(aString, aString.length);
188 var encoder = Cc["@mozilla.org/scriptablebase64encoder;1"]
189 .createInstance(Ci.nsIScriptableBase64Encoder);
190 return encoder.encodeToString(stream, aString.length);
191 }
194 /**
195 * Compares two arrays, and returns true if they are equal.
196 *
197 * @param aArray1
198 * First array to compare.
199 * @param aArray2
200 * Second array to compare.
201 */
202 function compareArrays(aArray1, aArray2) {
203 if (aArray1.length != aArray2.length) {
204 print("compareArrays: array lengths differ\n");
205 return false;
206 }
208 for (let i = 0; i < aArray1.length; i++) {
209 if (aArray1[i] != aArray2[i]) {
210 print("compareArrays: arrays differ at index " + i + ": " +
211 "(" + aArray1[i] + ") != (" + aArray2[i] +")\n");
212 return false;
213 }
214 }
216 return true;
217 }
220 /**
221 * Deletes a previously created sqlite file from the profile folder.
222 */
223 function clearDB() {
224 try {
225 let file = Services.dirsvc.get('ProfD', Ci.nsIFile);
226 file.append("places.sqlite");
227 if (file.exists())
228 file.remove(false);
229 } catch(ex) { dump("Exception: " + ex); }
230 }
233 /**
234 * Dumps the rows of a table out to the console.
235 *
236 * @param aName
237 * The name of the table or view to output.
238 */
239 function dump_table(aName)
240 {
241 let stmt = DBConn().createStatement("SELECT * FROM " + aName);
243 print("\n*** Printing data from " + aName);
244 let count = 0;
245 while (stmt.executeStep()) {
246 let columns = stmt.numEntries;
248 if (count == 0) {
249 // Print the column names.
250 for (let i = 0; i < columns; i++)
251 dump(stmt.getColumnName(i) + "\t");
252 dump("\n");
253 }
255 // Print the rows.
256 for (let i = 0; i < columns; i++) {
257 switch (stmt.getTypeOfIndex(i)) {
258 case Ci.mozIStorageValueArray.VALUE_TYPE_NULL:
259 dump("NULL\t");
260 break;
261 case Ci.mozIStorageValueArray.VALUE_TYPE_INTEGER:
262 dump(stmt.getInt64(i) + "\t");
263 break;
264 case Ci.mozIStorageValueArray.VALUE_TYPE_FLOAT:
265 dump(stmt.getDouble(i) + "\t");
266 break;
267 case Ci.mozIStorageValueArray.VALUE_TYPE_TEXT:
268 dump(stmt.getString(i) + "\t");
269 break;
270 }
271 }
272 dump("\n");
274 count++;
275 }
276 print("*** There were a total of " + count + " rows of data.\n");
278 stmt.finalize();
279 }
282 /**
283 * Checks if an address is found in the database.
284 * @param aURI
285 * nsIURI or address to look for.
286 * @return place id of the page or 0 if not found
287 */
288 function page_in_database(aURI)
289 {
290 let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI;
291 let stmt = DBConn().createStatement(
292 "SELECT id FROM moz_places WHERE url = :url"
293 );
294 stmt.params.url = url;
295 try {
296 if (!stmt.executeStep())
297 return 0;
298 return stmt.getInt64(0);
299 }
300 finally {
301 stmt.finalize();
302 }
303 }
305 /**
306 * Checks how many visits exist for a specified page.
307 * @param aURI
308 * nsIURI or address to look for.
309 * @return number of visits found.
310 */
311 function visits_in_database(aURI)
312 {
313 let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI;
314 let stmt = DBConn().createStatement(
315 "SELECT count(*) FROM moz_historyvisits v "
316 + "JOIN moz_places h ON h.id = v.place_id "
317 + "WHERE url = :url"
318 );
319 stmt.params.url = url;
320 try {
321 if (!stmt.executeStep())
322 return 0;
323 return stmt.getInt64(0);
324 }
325 finally {
326 stmt.finalize();
327 }
328 }
330 /**
331 * Removes all bookmarks and checks for correct cleanup
332 */
333 function remove_all_bookmarks() {
334 let PU = PlacesUtils;
335 // Clear all bookmarks
336 PU.bookmarks.removeFolderChildren(PU.bookmarks.bookmarksMenuFolder);
337 PU.bookmarks.removeFolderChildren(PU.bookmarks.toolbarFolder);
338 PU.bookmarks.removeFolderChildren(PU.bookmarks.unfiledBookmarksFolder);
339 // Check for correct cleanup
340 check_no_bookmarks();
341 }
344 /**
345 * Checks that we don't have any bookmark
346 */
347 function check_no_bookmarks() {
348 let query = PlacesUtils.history.getNewQuery();
349 let folders = [
350 PlacesUtils.bookmarks.toolbarFolder,
351 PlacesUtils.bookmarks.bookmarksMenuFolder,
352 PlacesUtils.bookmarks.unfiledBookmarksFolder,
353 ];
354 query.setFolders(folders, 3);
355 let options = PlacesUtils.history.getNewQueryOptions();
356 options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
357 let root = PlacesUtils.history.executeQuery(query, options).root;
358 root.containerOpen = true;
359 if (root.childCount != 0)
360 do_throw("Unable to remove all bookmarks");
361 root.containerOpen = false;
362 }
364 /**
365 * Allows waiting for an observer notification once.
366 *
367 * @param aTopic
368 * Notification topic to observe.
369 *
370 * @return {Promise}
371 * @resolves The array [aSubject, aData] from the observed notification.
372 * @rejects Never.
373 */
374 function promiseTopicObserved(aTopic)
375 {
376 let deferred = Promise.defer();
378 Services.obs.addObserver(
379 function PTO_observe(aSubject, aTopic, aData) {
380 Services.obs.removeObserver(PTO_observe, aTopic);
381 deferred.resolve([aSubject, aData]);
382 }, aTopic, false);
384 return deferred.promise;
385 }
387 /**
388 * Clears history asynchronously.
389 *
390 * @return {Promise}
391 * @resolves When history has been cleared.
392 * @rejects Never.
393 */
394 function promiseClearHistory() {
395 let promise = promiseTopicObserved(PlacesUtils.TOPIC_EXPIRATION_FINISHED);
396 do_execute_soon(function() PlacesUtils.bhistory.removeAllPages());
397 return promise;
398 }
401 /**
402 * Simulates a Places shutdown.
403 */
404 function shutdownPlaces(aKeepAliveConnection)
405 {
406 let hs = PlacesUtils.history.QueryInterface(Ci.nsIObserver);
407 hs.observe(null, "profile-change-teardown", null);
408 hs.observe(null, "profile-before-change", null);
409 }
411 const FILENAME_BOOKMARKS_HTML = "bookmarks.html";
412 let (backup_date = new Date().toLocaleFormat("%Y-%m-%d")) {
413 const FILENAME_BOOKMARKS_JSON = "bookmarks-" + backup_date + ".json";
414 }
416 /**
417 * Creates a bookmarks.html file in the profile folder from a given source file.
418 *
419 * @param aFilename
420 * Name of the file to copy to the profile folder. This file must
421 * exist in the directory that contains the test files.
422 *
423 * @return nsIFile object for the file.
424 */
425 function create_bookmarks_html(aFilename) {
426 if (!aFilename)
427 do_throw("you must pass a filename to create_bookmarks_html function");
428 remove_bookmarks_html();
429 let bookmarksHTMLFile = gTestDir.clone();
430 bookmarksHTMLFile.append(aFilename);
431 do_check_true(bookmarksHTMLFile.exists());
432 bookmarksHTMLFile.copyTo(gProfD, FILENAME_BOOKMARKS_HTML);
433 let profileBookmarksHTMLFile = gProfD.clone();
434 profileBookmarksHTMLFile.append(FILENAME_BOOKMARKS_HTML);
435 do_check_true(profileBookmarksHTMLFile.exists());
436 return profileBookmarksHTMLFile;
437 }
440 /**
441 * Remove bookmarks.html file from the profile folder.
442 */
443 function remove_bookmarks_html() {
444 let profileBookmarksHTMLFile = gProfD.clone();
445 profileBookmarksHTMLFile.append(FILENAME_BOOKMARKS_HTML);
446 if (profileBookmarksHTMLFile.exists()) {
447 profileBookmarksHTMLFile.remove(false);
448 do_check_false(profileBookmarksHTMLFile.exists());
449 }
450 }
453 /**
454 * Check bookmarks.html file exists in the profile folder.
455 *
456 * @return nsIFile object for the file.
457 */
458 function check_bookmarks_html() {
459 let profileBookmarksHTMLFile = gProfD.clone();
460 profileBookmarksHTMLFile.append(FILENAME_BOOKMARKS_HTML);
461 do_check_true(profileBookmarksHTMLFile.exists());
462 return profileBookmarksHTMLFile;
463 }
466 /**
467 * Creates a JSON backup in the profile folder folder from a given source file.
468 *
469 * @param aFilename
470 * Name of the file to copy to the profile folder. This file must
471 * exist in the directory that contains the test files.
472 *
473 * @return nsIFile object for the file.
474 */
475 function create_JSON_backup(aFilename) {
476 if (!aFilename)
477 do_throw("you must pass a filename to create_JSON_backup function");
478 let bookmarksBackupDir = gProfD.clone();
479 bookmarksBackupDir.append("bookmarkbackups");
480 if (!bookmarksBackupDir.exists()) {
481 bookmarksBackupDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8));
482 do_check_true(bookmarksBackupDir.exists());
483 }
484 let profileBookmarksJSONFile = bookmarksBackupDir.clone();
485 profileBookmarksJSONFile.append(FILENAME_BOOKMARKS_JSON);
486 if (profileBookmarksJSONFile.exists()) {
487 profileBookmarksJSONFile.remove();
488 }
489 let bookmarksJSONFile = gTestDir.clone();
490 bookmarksJSONFile.append(aFilename);
491 do_check_true(bookmarksJSONFile.exists());
492 bookmarksJSONFile.copyTo(bookmarksBackupDir, FILENAME_BOOKMARKS_JSON);
493 profileBookmarksJSONFile = bookmarksBackupDir.clone();
494 profileBookmarksJSONFile.append(FILENAME_BOOKMARKS_JSON);
495 do_check_true(profileBookmarksJSONFile.exists());
496 return profileBookmarksJSONFile;
497 }
500 /**
501 * Remove bookmarksbackup dir and all backups from the profile folder.
502 */
503 function remove_all_JSON_backups() {
504 let bookmarksBackupDir = gProfD.clone();
505 bookmarksBackupDir.append("bookmarkbackups");
506 if (bookmarksBackupDir.exists()) {
507 bookmarksBackupDir.remove(true);
508 do_check_false(bookmarksBackupDir.exists());
509 }
510 }
513 /**
514 * Check a JSON backup file for today exists in the profile folder.
515 *
516 * @param aIsAutomaticBackup The boolean indicates whether it's an automatic
517 * backup.
518 * @return nsIFile object for the file.
519 */
520 function check_JSON_backup(aIsAutomaticBackup) {
521 let profileBookmarksJSONFile;
522 if (aIsAutomaticBackup) {
523 let bookmarksBackupDir = gProfD.clone();
524 bookmarksBackupDir.append("bookmarkbackups");
525 let files = bookmarksBackupDir.directoryEntries;
526 let backup_date = new Date().toLocaleFormat("%Y-%m-%d");
527 while (files.hasMoreElements()) {
528 let entry = files.getNext().QueryInterface(Ci.nsIFile);
529 if (PlacesBackups.filenamesRegex.test(entry.leafName)) {
530 profileBookmarksJSONFile = entry;
531 break;
532 }
533 }
534 } else {
535 profileBookmarksJSONFile = gProfD.clone();
536 profileBookmarksJSONFile.append("bookmarkbackups");
537 profileBookmarksJSONFile.append(FILENAME_BOOKMARKS_JSON);
538 }
539 do_check_true(profileBookmarksJSONFile.exists());
540 return profileBookmarksJSONFile;
541 }
543 /**
544 * Returns the frecency of a url.
545 *
546 * @param aURI
547 * The URI or spec to get frecency for.
548 * @return the frecency value.
549 */
550 function frecencyForUrl(aURI)
551 {
552 let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI;
553 let stmt = DBConn().createStatement(
554 "SELECT frecency FROM moz_places WHERE url = ?1"
555 );
556 stmt.bindByIndex(0, url);
557 try {
558 if (!stmt.executeStep()) {
559 throw new Error("No result for frecency.");
560 }
561 return stmt.getInt32(0);
562 } finally {
563 stmt.finalize();
564 }
565 }
567 /**
568 * Returns the hidden status of a url.
569 *
570 * @param aURI
571 * The URI or spec to get hidden for.
572 * @return @return true if the url is hidden, false otherwise.
573 */
574 function isUrlHidden(aURI)
575 {
576 let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI;
577 let stmt = DBConn().createStatement(
578 "SELECT hidden FROM moz_places WHERE url = ?1"
579 );
580 stmt.bindByIndex(0, url);
581 if (!stmt.executeStep())
582 throw new Error("No result for hidden.");
583 let hidden = stmt.getInt32(0);
584 stmt.finalize();
586 return !!hidden;
587 }
589 /**
590 * Compares two times in usecs, considering eventual platform timers skews.
591 *
592 * @param aTimeBefore
593 * The older time in usecs.
594 * @param aTimeAfter
595 * The newer time in usecs.
596 * @return true if times are ordered, false otherwise.
597 */
598 function is_time_ordered(before, after) {
599 // Windows has an estimated 16ms timers precision, since Date.now() and
600 // PR_Now() use different code atm, the results can be unordered by this
601 // amount of time. See bug 558745 and bug 557406.
602 let isWindows = ("@mozilla.org/windows-registry-key;1" in Cc);
603 // Just to be safe we consider 20ms.
604 let skew = isWindows ? 20000000 : 0;
605 return after - before > -skew;
606 }
608 /**
609 * Waits for all pending async statements on the default connection.
610 *
611 * @return {Promise}
612 * @resolves When all pending async statements finished.
613 * @rejects Never.
614 *
615 * @note The result is achieved by asynchronously executing a query requiring
616 * a write lock. Since all statements on the same connection are
617 * serialized, the end of this write operation means that all writes are
618 * complete. Note that WAL makes so that writers don't block readers, but
619 * this is a problem only across different connections.
620 */
621 function promiseAsyncUpdates()
622 {
623 let deferred = Promise.defer();
625 let db = DBConn();
626 let begin = db.createAsyncStatement("BEGIN EXCLUSIVE");
627 begin.executeAsync();
628 begin.finalize();
630 let commit = db.createAsyncStatement("COMMIT");
631 commit.executeAsync({
632 handleResult: function () {},
633 handleError: function () {},
634 handleCompletion: function(aReason)
635 {
636 deferred.resolve();
637 }
638 });
639 commit.finalize();
641 return deferred.promise;
642 }
644 /**
645 * Shutdowns Places, invoking the callback when the connection has been closed.
646 *
647 * @param aCallback
648 * Function to be called when done.
649 */
650 function waitForConnectionClosed(aCallback)
651 {
652 Services.obs.addObserver(function WFCCCallback() {
653 Services.obs.removeObserver(WFCCCallback, "places-connection-closed");
654 aCallback();
655 }, "places-connection-closed", false);
656 shutdownPlaces();
657 }
659 /**
660 * Tests if a given guid is valid for use in Places or not.
661 *
662 * @param aGuid
663 * The guid to test.
664 * @param [optional] aStack
665 * The stack frame used to report the error.
666 */
667 function do_check_valid_places_guid(aGuid,
668 aStack)
669 {
670 if (!aStack) {
671 aStack = Components.stack.caller;
672 }
673 do_check_true(/^[a-zA-Z0-9\-_]{12}$/.test(aGuid), aStack);
674 }
676 /**
677 * Retrieves the guid for a given uri.
678 *
679 * @param aURI
680 * The uri to check.
681 * @param [optional] aStack
682 * The stack frame used to report the error.
683 * @return the associated the guid.
684 */
685 function do_get_guid_for_uri(aURI,
686 aStack)
687 {
688 if (!aStack) {
689 aStack = Components.stack.caller;
690 }
691 let stmt = DBConn().createStatement(
692 "SELECT guid "
693 + "FROM moz_places "
694 + "WHERE url = :url "
695 );
696 stmt.params.url = aURI.spec;
697 do_check_true(stmt.executeStep(), aStack);
698 let guid = stmt.row.guid;
699 stmt.finalize();
700 do_check_valid_places_guid(guid, aStack);
701 return guid;
702 }
704 /**
705 * Tests that a guid was set in moz_places for a given uri.
706 *
707 * @param aURI
708 * The uri to check.
709 * @param [optional] aGUID
710 * The expected guid in the database.
711 */
712 function do_check_guid_for_uri(aURI,
713 aGUID)
714 {
715 let caller = Components.stack.caller;
716 let guid = do_get_guid_for_uri(aURI, caller);
717 if (aGUID) {
718 do_check_valid_places_guid(aGUID, caller);
719 do_check_eq(guid, aGUID, caller);
720 }
721 }
723 /**
724 * Retrieves the guid for a given bookmark.
725 *
726 * @param aId
727 * The bookmark id to check.
728 * @param [optional] aStack
729 * The stack frame used to report the error.
730 * @return the associated the guid.
731 */
732 function do_get_guid_for_bookmark(aId,
733 aStack)
734 {
735 if (!aStack) {
736 aStack = Components.stack.caller;
737 }
738 let stmt = DBConn().createStatement(
739 "SELECT guid "
740 + "FROM moz_bookmarks "
741 + "WHERE id = :item_id "
742 );
743 stmt.params.item_id = aId;
744 do_check_true(stmt.executeStep(), aStack);
745 let guid = stmt.row.guid;
746 stmt.finalize();
747 do_check_valid_places_guid(guid, aStack);
748 return guid;
749 }
751 /**
752 * Tests that a guid was set in moz_places for a given bookmark.
753 *
754 * @param aId
755 * The bookmark id to check.
756 * @param [optional] aGUID
757 * The expected guid in the database.
758 */
759 function do_check_guid_for_bookmark(aId,
760 aGUID)
761 {
762 let caller = Components.stack.caller;
763 let guid = do_get_guid_for_bookmark(aId, caller);
764 if (aGUID) {
765 do_check_valid_places_guid(aGUID, caller);
766 do_check_eq(guid, aGUID, caller);
767 }
768 }
770 /**
771 * Logs info to the console in the standard way (includes the filename).
772 *
773 * @param aMessage
774 * The message to log to the console.
775 */
776 function do_log_info(aMessage)
777 {
778 print("TEST-INFO | " + _TEST_FILE + " | " + aMessage);
779 }
781 /**
782 * Compares 2 arrays returning whether they contains the same elements.
783 *
784 * @param a1
785 * First array to compare.
786 * @param a2
787 * Second array to compare.
788 * @param [optional] sorted
789 * Whether the comparison should take in count position of the elements.
790 * @return true if the arrays contain the same elements, false otherwise.
791 */
792 function do_compare_arrays(a1, a2, sorted)
793 {
794 if (a1.length != a2.length)
795 return false;
797 if (sorted) {
798 return a1.every(function (e, i) e == a2[i]);
799 }
800 else {
801 return a1.filter(function (e) a2.indexOf(e) == -1).length == 0 &&
802 a2.filter(function (e) a1.indexOf(e) == -1).length == 0;
803 }
804 }
806 /**
807 * Generic nsINavBookmarkObserver that doesn't implement anything, but provides
808 * dummy methods to prevent errors about an object not having a certain method.
809 */
810 function NavBookmarkObserver() {}
812 NavBookmarkObserver.prototype = {
813 onBeginUpdateBatch: function () {},
814 onEndUpdateBatch: function () {},
815 onItemAdded: function () {},
816 onItemRemoved: function () {},
817 onItemChanged: function () {},
818 onItemVisited: function () {},
819 onItemMoved: function () {},
820 QueryInterface: XPCOMUtils.generateQI([
821 Ci.nsINavBookmarkObserver,
822 ])
823 };
825 /**
826 * Generic nsINavHistoryObserver that doesn't implement anything, but provides
827 * dummy methods to prevent errors about an object not having a certain method.
828 */
829 function NavHistoryObserver() {}
831 NavHistoryObserver.prototype = {
832 onBeginUpdateBatch: function () {},
833 onEndUpdateBatch: function () {},
834 onVisit: function () {},
835 onTitleChanged: function () {},
836 onDeleteURI: function () {},
837 onClearHistory: function () {},
838 onPageChanged: function () {},
839 onDeleteVisits: function () {},
840 QueryInterface: XPCOMUtils.generateQI([
841 Ci.nsINavHistoryObserver,
842 ])
843 };
845 /**
846 * Generic nsINavHistoryResultObserver that doesn't implement anything, but
847 * provides dummy methods to prevent errors about an object not having a certain
848 * method.
849 */
850 function NavHistoryResultObserver() {}
852 NavHistoryResultObserver.prototype = {
853 batching: function () {},
854 containerStateChanged: function () {},
855 invalidateContainer: function () {},
856 nodeAnnotationChanged: function () {},
857 nodeDateAddedChanged: function () {},
858 nodeHistoryDetailsChanged: function () {},
859 nodeIconChanged: function () {},
860 nodeInserted: function () {},
861 nodeKeywordChanged: function () {},
862 nodeLastModifiedChanged: function () {},
863 nodeMoved: function () {},
864 nodeRemoved: function () {},
865 nodeTagsChanged: function () {},
866 nodeTitleChanged: function () {},
867 nodeURIChanged: function () {},
868 sortingChanged: function () {},
869 QueryInterface: XPCOMUtils.generateQI([
870 Ci.nsINavHistoryResultObserver,
871 ])
872 };
874 /**
875 * Asynchronously adds visits to a page.
876 *
877 * @param aPlaceInfo
878 * Can be an nsIURI, in such a case a single LINK visit will be added.
879 * Otherwise can be an object describing the visit to add, or an array
880 * of these objects:
881 * { uri: nsIURI of the page,
882 * transition: one of the TRANSITION_* from nsINavHistoryService,
883 * [optional] title: title of the page,
884 * [optional] visitDate: visit date in microseconds from the epoch
885 * [optional] referrer: nsIURI of the referrer for this visit
886 * }
887 *
888 * @return {Promise}
889 * @resolves When all visits have been added successfully.
890 * @rejects JavaScript exception.
891 */
892 function promiseAddVisits(aPlaceInfo)
893 {
894 let deferred = Promise.defer();
895 let places = [];
896 if (aPlaceInfo instanceof Ci.nsIURI) {
897 places.push({ uri: aPlaceInfo });
898 }
899 else if (Array.isArray(aPlaceInfo)) {
900 places = places.concat(aPlaceInfo);
901 } else {
902 places.push(aPlaceInfo)
903 }
905 // Create mozIVisitInfo for each entry.
906 let now = Date.now();
907 for (let i = 0; i < places.length; i++) {
908 if (!places[i].title) {
909 places[i].title = "test visit for " + places[i].uri.spec;
910 }
911 places[i].visits = [{
912 transitionType: places[i].transition === undefined ? TRANSITION_LINK
913 : places[i].transition,
914 visitDate: places[i].visitDate || (now++) * 1000,
915 referrerURI: places[i].referrer
916 }];
917 }
919 PlacesUtils.asyncHistory.updatePlaces(
920 places,
921 {
922 handleError: function AAV_handleError(aResultCode, aPlaceInfo) {
923 let ex = new Components.Exception("Unexpected error in adding visits.",
924 aResultCode);
925 deferred.reject(ex);
926 },
927 handleResult: function () {},
928 handleCompletion: function UP_handleCompletion() {
929 deferred.resolve();
930 }
931 }
932 );
934 return deferred.promise;
935 }
937 /**
938 * Asynchronously check a url is visited.
939 *
940 * @param aURI The URI.
941 * @return {Promise}
942 * @resolves When the check has been added successfully.
943 * @rejects JavaScript exception.
944 */
945 function promiseIsURIVisited(aURI) {
946 let deferred = Promise.defer();
948 PlacesUtils.asyncHistory.isURIVisited(aURI, function(aURI, aIsVisited) {
949 deferred.resolve(aIsVisited);
950 });
952 return deferred.promise;
953 }