|
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/. */ |
|
5 |
|
6 const CURRENT_SCHEMA_VERSION = 23; |
|
7 |
|
8 const NS_APP_USER_PROFILE_50_DIR = "ProfD"; |
|
9 const NS_APP_PROFILE_DIR_STARTUP = "ProfDS"; |
|
10 |
|
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; |
|
20 |
|
21 const TITLE_LENGTH_MAX = 4096; |
|
22 |
|
23 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
24 |
|
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"); |
|
45 |
|
46 // This imports various other objects in addition to PlacesUtils. |
|
47 Cu.import("resource://gre/modules/PlacesUtils.jsm"); |
|
48 |
|
49 XPCOMUtils.defineLazyGetter(this, "SMALLPNG_DATA_URI", function() { |
|
50 return NetUtil.newURI( |
|
51 "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAA" + |
|
52 "AAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg=="); |
|
53 }); |
|
54 |
|
55 function LOG(aMsg) { |
|
56 aMsg = ("*** PLACES TESTS: " + aMsg); |
|
57 Services.console.logStringMessage(aMsg); |
|
58 print(aMsg); |
|
59 } |
|
60 |
|
61 let gTestDir = do_get_cwd(); |
|
62 |
|
63 // Initialize profile. |
|
64 let gProfD = do_get_profile(); |
|
65 |
|
66 // Remove any old database. |
|
67 clearDB(); |
|
68 |
|
69 /** |
|
70 * Shortcut to create a nsIURI. |
|
71 * |
|
72 * @param aSpec |
|
73 * URLString of the uri. |
|
74 */ |
|
75 function uri(aSpec) NetUtil.newURI(aSpec); |
|
76 |
|
77 |
|
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 } |
|
97 |
|
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); |
|
103 |
|
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 } |
|
110 |
|
111 return gDBConn.connectionReady ? gDBConn : null; |
|
112 }; |
|
113 |
|
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 } |
|
134 |
|
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); |
|
147 |
|
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 } |
|
156 |
|
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 } |
|
172 |
|
173 |
|
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 } |
|
192 |
|
193 |
|
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 } |
|
207 |
|
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 } |
|
215 |
|
216 return true; |
|
217 } |
|
218 |
|
219 |
|
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 } |
|
231 |
|
232 |
|
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); |
|
242 |
|
243 print("\n*** Printing data from " + aName); |
|
244 let count = 0; |
|
245 while (stmt.executeStep()) { |
|
246 let columns = stmt.numEntries; |
|
247 |
|
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 } |
|
254 |
|
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"); |
|
273 |
|
274 count++; |
|
275 } |
|
276 print("*** There were a total of " + count + " rows of data.\n"); |
|
277 |
|
278 stmt.finalize(); |
|
279 } |
|
280 |
|
281 |
|
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 } |
|
304 |
|
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 } |
|
329 |
|
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 } |
|
342 |
|
343 |
|
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 } |
|
363 |
|
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(); |
|
377 |
|
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); |
|
383 |
|
384 return deferred.promise; |
|
385 } |
|
386 |
|
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 } |
|
399 |
|
400 |
|
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 } |
|
410 |
|
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 } |
|
415 |
|
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 } |
|
438 |
|
439 |
|
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 } |
|
451 |
|
452 |
|
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 } |
|
464 |
|
465 |
|
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 } |
|
498 |
|
499 |
|
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 } |
|
511 |
|
512 |
|
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 } |
|
542 |
|
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 } |
|
566 |
|
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(); |
|
585 |
|
586 return !!hidden; |
|
587 } |
|
588 |
|
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 } |
|
607 |
|
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(); |
|
624 |
|
625 let db = DBConn(); |
|
626 let begin = db.createAsyncStatement("BEGIN EXCLUSIVE"); |
|
627 begin.executeAsync(); |
|
628 begin.finalize(); |
|
629 |
|
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(); |
|
640 |
|
641 return deferred.promise; |
|
642 } |
|
643 |
|
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 } |
|
658 |
|
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 } |
|
675 |
|
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 } |
|
703 |
|
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 } |
|
722 |
|
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 } |
|
750 |
|
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 } |
|
769 |
|
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 } |
|
780 |
|
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; |
|
796 |
|
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 } |
|
805 |
|
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() {} |
|
811 |
|
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 }; |
|
824 |
|
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() {} |
|
830 |
|
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 }; |
|
844 |
|
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() {} |
|
851 |
|
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 }; |
|
873 |
|
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 } |
|
904 |
|
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 } |
|
918 |
|
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 ); |
|
933 |
|
934 return deferred.promise; |
|
935 } |
|
936 |
|
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(); |
|
947 |
|
948 PlacesUtils.asyncHistory.isURIVisited(aURI, function(aURI, aIsVisited) { |
|
949 deferred.resolve(aIsVisited); |
|
950 }); |
|
951 |
|
952 return deferred.promise; |
|
953 } |
|
954 |