1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 #include <stdio.h>
8 
9 #include "mozilla/Components.h"
10 #include "mozilla/DebugOnly.h"
11 #include "mozilla/IntegerPrintfMacros.h"
12 #include "mozilla/intl/LocaleService.h"
13 
14 #include "nsNavHistory.h"
15 
16 #include "mozIPlacesAutoComplete.h"
17 #include "nsNavBookmarks.h"
18 #include "nsFaviconService.h"
19 #include "nsPlacesMacros.h"
20 #include "nsPlacesTriggers.h"
21 #include "mozilla/intl/AppDateTimeFormat.h"
22 #include "History.h"
23 #include "Helpers.h"
24 #include "NotifyRankingChanged.h"
25 
26 #include "nsTArray.h"
27 #include "nsNetUtil.h"
28 #include "nsPrintfCString.h"
29 #include "nsPromiseFlatString.h"
30 #include "nsString.h"
31 #include "nsUnicharUtils.h"
32 #include "prsystem.h"
33 #include "prtime.h"
34 #include "nsEscape.h"
35 #include "nsIEffectiveTLDService.h"
36 #include "nsIClassInfoImpl.h"
37 #include "nsIIDNService.h"
38 #include "nsQueryObject.h"
39 #include "nsThreadUtils.h"
40 #include "nsAppDirectoryServiceDefs.h"
41 #include "nsMathUtils.h"
42 #include "nsReadableUtils.h"
43 #include "mozilla/storage.h"
44 #include "mozilla/Preferences.h"
45 #include <algorithm>
46 
47 using namespace mozilla;
48 using namespace mozilla::places;
49 
50 // The maximum number of things that we will store in the recent events list
51 // before calling ExpireNonrecentEvents. This number should be big enough so it
52 // is very difficult to get that many unconsumed events (for example, typed but
53 // never visited) in the RECENT_EVENT_THRESHOLD. Otherwise, we'll start
54 // checking each one for every page visit, which will be somewhat slower.
55 #define RECENT_EVENT_QUEUE_MAX_LENGTH 128
56 
57 // preference ID strings
58 #define PREF_HISTORY_ENABLED "places.history.enabled"
59 #define PREF_MATCH_DIACRITICS "places.search.matchDiacritics"
60 
61 #define PREF_FREC_NUM_VISITS "places.frecency.numVisits"
62 #define PREF_FREC_NUM_VISITS_DEF 10
63 #define PREF_FREC_FIRST_BUCKET_CUTOFF "places.frecency.firstBucketCutoff"
64 #define PREF_FREC_FIRST_BUCKET_CUTOFF_DEF 4
65 #define PREF_FREC_SECOND_BUCKET_CUTOFF "places.frecency.secondBucketCutoff"
66 #define PREF_FREC_SECOND_BUCKET_CUTOFF_DEF 14
67 #define PREF_FREC_THIRD_BUCKET_CUTOFF "places.frecency.thirdBucketCutoff"
68 #define PREF_FREC_THIRD_BUCKET_CUTOFF_DEF 31
69 #define PREF_FREC_FOURTH_BUCKET_CUTOFF "places.frecency.fourthBucketCutoff"
70 #define PREF_FREC_FOURTH_BUCKET_CUTOFF_DEF 90
71 #define PREF_FREC_FIRST_BUCKET_WEIGHT "places.frecency.firstBucketWeight"
72 #define PREF_FREC_FIRST_BUCKET_WEIGHT_DEF 100
73 #define PREF_FREC_SECOND_BUCKET_WEIGHT "places.frecency.secondBucketWeight"
74 #define PREF_FREC_SECOND_BUCKET_WEIGHT_DEF 70
75 #define PREF_FREC_THIRD_BUCKET_WEIGHT "places.frecency.thirdBucketWeight"
76 #define PREF_FREC_THIRD_BUCKET_WEIGHT_DEF 50
77 #define PREF_FREC_FOURTH_BUCKET_WEIGHT "places.frecency.fourthBucketWeight"
78 #define PREF_FREC_FOURTH_BUCKET_WEIGHT_DEF 30
79 #define PREF_FREC_DEFAULT_BUCKET_WEIGHT "places.frecency.defaultBucketWeight"
80 #define PREF_FREC_DEFAULT_BUCKET_WEIGHT_DEF 10
81 #define PREF_FREC_EMBED_VISIT_BONUS "places.frecency.embedVisitBonus"
82 #define PREF_FREC_EMBED_VISIT_BONUS_DEF 0
83 #define PREF_FREC_FRAMED_LINK_VISIT_BONUS "places.frecency.framedLinkVisitBonus"
84 #define PREF_FREC_FRAMED_LINK_VISIT_BONUS_DEF 0
85 #define PREF_FREC_LINK_VISIT_BONUS "places.frecency.linkVisitBonus"
86 #define PREF_FREC_LINK_VISIT_BONUS_DEF 100
87 #define PREF_FREC_TYPED_VISIT_BONUS "places.frecency.typedVisitBonus"
88 #define PREF_FREC_TYPED_VISIT_BONUS_DEF 2000
89 #define PREF_FREC_BOOKMARK_VISIT_BONUS "places.frecency.bookmarkVisitBonus"
90 #define PREF_FREC_BOOKMARK_VISIT_BONUS_DEF 75
91 #define PREF_FREC_DOWNLOAD_VISIT_BONUS "places.frecency.downloadVisitBonus"
92 #define PREF_FREC_DOWNLOAD_VISIT_BONUS_DEF 0
93 #define PREF_FREC_PERM_REDIRECT_VISIT_BONUS \
94   "places.frecency.permRedirectVisitBonus"
95 #define PREF_FREC_PERM_REDIRECT_VISIT_BONUS_DEF 0
96 #define PREF_FREC_TEMP_REDIRECT_VISIT_BONUS \
97   "places.frecency.tempRedirectVisitBonus"
98 #define PREF_FREC_TEMP_REDIRECT_VISIT_BONUS_DEF 0
99 #define PREF_FREC_REDIR_SOURCE_VISIT_BONUS \
100   "places.frecency.redirectSourceVisitBonus"
101 #define PREF_FREC_REDIR_SOURCE_VISIT_BONUS_DEF 25
102 #define PREF_FREC_DEFAULT_VISIT_BONUS "places.frecency.defaultVisitBonus"
103 #define PREF_FREC_DEFAULT_VISIT_BONUS_DEF 0
104 #define PREF_FREC_UNVISITED_BOOKMARK_BONUS \
105   "places.frecency.unvisitedBookmarkBonus"
106 #define PREF_FREC_UNVISITED_BOOKMARK_BONUS_DEF 140
107 #define PREF_FREC_UNVISITED_TYPED_BONUS "places.frecency.unvisitedTypedBonus"
108 #define PREF_FREC_UNVISITED_TYPED_BONUS_DEF 200
109 #define PREF_FREC_RELOAD_VISIT_BONUS "places.frecency.reloadVisitBonus"
110 #define PREF_FREC_RELOAD_VISIT_BONUS_DEF 0
111 
112 // This is a 'hidden' pref for the purposes of unit tests.
113 #define PREF_FREC_DECAY_RATE "places.frecency.decayRate"
114 #define PREF_FREC_DECAY_RATE_DEF 0.975f
115 // An adaptive history entry is removed if unused for these many days.
116 #define ADAPTIVE_HISTORY_EXPIRE_DAYS 90
117 
118 // In order to avoid calling PR_now() too often we use a cached "now" value
119 // for repeating stuff.  These are milliseconds between "now" cache refreshes.
120 #define RENEW_CACHED_NOW_TIMEOUT ((int32_t)3 * PR_MSEC_PER_SEC)
121 
122 // These macros are used when splitting history by date.
123 // These are the day containers and catch-all final container.
124 #define HISTORY_ADDITIONAL_DATE_CONT_NUM 3
125 // We use a guess of the number of months considering all of them 30 days
126 // long, but we split only the last 6 months.
127 #define HISTORY_DATE_CONT_NUM(_daysFromOldestVisit) \
128   (HISTORY_ADDITIONAL_DATE_CONT_NUM +               \
129    std::min(6, (int32_t)ceilf((float)_daysFromOldestVisit / 30)))
130 // Max number of containers, used to initialize the params hash.
131 #define HISTORY_DATE_CONT_LENGTH 8
132 
133 // Initial length of the recent events cache.
134 #define RECENT_EVENTS_INITIAL_CACHE_LENGTH 64
135 
136 // Observed topics.
137 #define TOPIC_IDLE_DAILY "idle-daily"
138 #define TOPIC_PREF_CHANGED "nsPref:changed"
139 #define TOPIC_PROFILE_TEARDOWN "profile-change-teardown"
140 #define TOPIC_PROFILE_CHANGE "profile-before-change"
141 
142 static const char* kObservedPrefs[] = {PREF_HISTORY_ENABLED,
143                                        PREF_MATCH_DIACRITICS,
144                                        PREF_FREC_NUM_VISITS,
145                                        PREF_FREC_FIRST_BUCKET_CUTOFF,
146                                        PREF_FREC_SECOND_BUCKET_CUTOFF,
147                                        PREF_FREC_THIRD_BUCKET_CUTOFF,
148                                        PREF_FREC_FOURTH_BUCKET_CUTOFF,
149                                        PREF_FREC_FIRST_BUCKET_WEIGHT,
150                                        PREF_FREC_SECOND_BUCKET_WEIGHT,
151                                        PREF_FREC_THIRD_BUCKET_WEIGHT,
152                                        PREF_FREC_FOURTH_BUCKET_WEIGHT,
153                                        PREF_FREC_DEFAULT_BUCKET_WEIGHT,
154                                        PREF_FREC_EMBED_VISIT_BONUS,
155                                        PREF_FREC_FRAMED_LINK_VISIT_BONUS,
156                                        PREF_FREC_LINK_VISIT_BONUS,
157                                        PREF_FREC_TYPED_VISIT_BONUS,
158                                        PREF_FREC_BOOKMARK_VISIT_BONUS,
159                                        PREF_FREC_DOWNLOAD_VISIT_BONUS,
160                                        PREF_FREC_PERM_REDIRECT_VISIT_BONUS,
161                                        PREF_FREC_TEMP_REDIRECT_VISIT_BONUS,
162                                        PREF_FREC_REDIR_SOURCE_VISIT_BONUS,
163                                        PREF_FREC_DEFAULT_VISIT_BONUS,
164                                        PREF_FREC_UNVISITED_BOOKMARK_BONUS,
165                                        PREF_FREC_UNVISITED_TYPED_BONUS,
166                                        nullptr};
167 
168 NS_IMPL_ADDREF(nsNavHistory)
169 NS_IMPL_RELEASE(nsNavHistory)
170 
171 NS_IMPL_CLASSINFO(nsNavHistory, nullptr, nsIClassInfo::SINGLETON,
172                   NS_NAVHISTORYSERVICE_CID)
173 NS_INTERFACE_MAP_BEGIN(nsNavHistory)
174   NS_INTERFACE_MAP_ENTRY(nsINavHistoryService)
175   NS_INTERFACE_MAP_ENTRY(nsIObserver)
176   NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
177   NS_INTERFACE_MAP_ENTRY(mozIStorageVacuumParticipant)
178   NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsINavHistoryService)
179   NS_IMPL_QUERY_CLASSINFO(nsNavHistory)
180 NS_INTERFACE_MAP_END
181 
182 // We don't care about flattening everything
183 NS_IMPL_CI_INTERFACE_GETTER(nsNavHistory, nsINavHistoryService)
184 
185 namespace {
186 
187 static nsCString GetSimpleBookmarksQueryParent(
188     const RefPtr<nsNavHistoryQuery>& aQuery,
189     const RefPtr<nsNavHistoryQueryOptions>& aOptions);
190 static void ParseSearchTermsFromQuery(const RefPtr<nsNavHistoryQuery>& aQuery,
191                                       nsTArray<nsString>* aTerms);
192 
GetTagsSqlFragment(int64_t aTagsFolder,const nsACString & aRelation,bool aHasSearchTerms,nsACString & _sqlFragment)193 void GetTagsSqlFragment(int64_t aTagsFolder, const nsACString& aRelation,
194                         bool aHasSearchTerms, nsACString& _sqlFragment) {
195   if (!aHasSearchTerms)
196     _sqlFragment.AssignLiteral("null");
197   else {
198     // This subquery DOES NOT order tags for performance reasons.
199     _sqlFragment.Assign(
200         nsLiteralCString("(SELECT GROUP_CONCAT(t_t.title, ',') "
201                          "FROM moz_bookmarks b_t "
202                          "JOIN moz_bookmarks t_t ON t_t.id = +b_t.parent  "
203                          "WHERE b_t.fk = ") +
204         aRelation +
205         nsLiteralCString(" "
206                          "AND t_t.parent = ") +
207         nsPrintfCString("%" PRId64, aTagsFolder) +
208         nsLiteralCString(" "
209                          ")"));
210   }
211 
212   _sqlFragment.AppendLiteral(" AS tags ");
213 }
214 
215 /**
216  * Recalculates invalid frecencies in chunks on the storage thread, optionally
217  * decays frecencies, and notifies history observers on the main thread.
218  */
219 class FixAndDecayFrecencyRunnable final : public Runnable {
220  public:
FixAndDecayFrecencyRunnable(Database * aDB,float aDecayRate)221   explicit FixAndDecayFrecencyRunnable(Database* aDB, float aDecayRate)
222       : Runnable("places::FixAndDecayFrecencyRunnable"),
223         mDB(aDB),
224         mDecayRate(aDecayRate),
225         mDecayReason(mozIStorageStatementCallback::REASON_FINISHED) {}
226 
227   // MOZ_CAN_RUN_SCRIPT_BOUNDARY until Runnable::Run is marked
228   // MOZ_CAN_RUN_SCRIPT.  See bug 1535398.
229   MOZ_CAN_RUN_SCRIPT_BOUNDARY
Run()230   NS_IMETHOD Run() override {
231     if (NS_IsMainThread()) {
232       nsNavHistory* navHistory = nsNavHistory::GetHistoryService();
233       NS_ENSURE_STATE(navHistory);
234 
235       navHistory->DecayFrecencyCompleted();
236 
237       if (mozIStorageStatementCallback::REASON_FINISHED == mDecayReason) {
238         NotifyRankingChanged().Run();
239       }
240 
241       return NS_OK;
242     }
243 
244     MOZ_ASSERT(!NS_IsMainThread(),
245                "Frecencies should be recalculated on async thread");
246 
247     nsCOMPtr<mozIStorageStatement> updateStmt = mDB->GetStatement(
248         "UPDATE moz_places "
249         "SET frecency = CALCULATE_FRECENCY(id) "
250         "WHERE id IN ("
251         "SELECT id FROM moz_places "
252         "WHERE frecency < 0 "
253         "ORDER BY frecency ASC "
254         "LIMIT 400"
255         ")");
256     NS_ENSURE_STATE(updateStmt);
257     nsresult rv = updateStmt->Execute();
258     NS_ENSURE_SUCCESS(rv, rv);
259 
260     nsCOMPtr<mozIStorageStatement> selectStmt = mDB->GetStatement(
261         "SELECT id FROM moz_places WHERE frecency < 0 "
262         "LIMIT 1");
263     NS_ENSURE_STATE(selectStmt);
264     bool hasResult = false;
265     rv = selectStmt->ExecuteStep(&hasResult);
266     NS_ENSURE_SUCCESS(rv, rv);
267     if (hasResult) {
268       // There are more invalid frecencies to fix. Re-dispatch to the async
269       // storage thread for the next chunk.
270       return NS_DispatchToCurrentThread(this);
271     }
272 
273     mozStorageTransaction transaction(
274         mDB->MainConn(), false, mozIStorageConnection::TRANSACTION_IMMEDIATE);
275 
276     // XXX Handle the error, bug 1696133.
277     Unused << NS_WARN_IF(NS_FAILED(transaction.Start()));
278 
279     if (NS_WARN_IF(NS_FAILED(DecayFrecencies()))) {
280       mDecayReason = mozIStorageStatementCallback::REASON_ERROR;
281     }
282 
283     // We've finished fixing and decaying frecencies. Trigger frecency updates
284     // for all affected origins.
285     nsCOMPtr<mozIStorageStatement> updateOriginFrecenciesStmt =
286         mDB->GetStatement("DELETE FROM moz_updateoriginsupdate_temp");
287     NS_ENSURE_STATE(updateOriginFrecenciesStmt);
288     rv = updateOriginFrecenciesStmt->Execute();
289     NS_ENSURE_SUCCESS(rv, rv);
290 
291     rv = transaction.Commit();
292     NS_ENSURE_SUCCESS(rv, rv);
293 
294     // Re-dispatch to the main thread to notify observers.
295     return NS_DispatchToMainThread(this);
296   }
297 
298  private:
DecayFrecencies()299   nsresult DecayFrecencies() {
300     TimeStamp start = TimeStamp::Now();
301 
302     // Globally decay places frecency rankings to estimate reduced frecency
303     // values of pages that haven't been visited for a while, i.e., they do
304     // not get an updated frecency.  A scaling factor of .975 results in .5 the
305     // original value after 28 days.
306     // When changing the scaling factor, ensure that the barrier in
307     // moz_places_afterupdate_frecency_trigger still ignores these changes.
308     nsCOMPtr<mozIStorageStatement> decayFrecency = mDB->GetStatement(
309         "UPDATE moz_places SET frecency = ROUND(frecency * :decay_rate) "
310         "WHERE frecency > 0");
311     NS_ENSURE_STATE(decayFrecency);
312     nsresult rv = decayFrecency->BindDoubleByName(
313         "decay_rate"_ns, static_cast<double>(mDecayRate));
314     NS_ENSURE_SUCCESS(rv, rv);
315     rv = decayFrecency->Execute();
316     NS_ENSURE_SUCCESS(rv, rv);
317 
318     // Decay potentially unused adaptive entries (e.g. those that are at 1)
319     // to allow better chances for new entries that will start at 1.
320     nsCOMPtr<mozIStorageStatement> decayAdaptive = mDB->GetStatement(
321         "UPDATE moz_inputhistory SET use_count = use_count * :decay_rate");
322     NS_ENSURE_STATE(decayAdaptive);
323     rv = decayAdaptive->BindDoubleByName("decay_rate"_ns,
324                                          static_cast<double>(mDecayRate));
325     NS_ENSURE_SUCCESS(rv, rv);
326     rv = decayAdaptive->Execute();
327     NS_ENSURE_SUCCESS(rv, rv);
328 
329     // Delete any adaptive entries that won't help in ordering anymore.
330     nsCOMPtr<mozIStorageStatement> deleteAdaptive = mDB->GetStatement(
331         "DELETE FROM moz_inputhistory WHERE use_count < :use_count");
332     NS_ENSURE_STATE(deleteAdaptive);
333     rv = deleteAdaptive->BindDoubleByName(
334         "use_count"_ns, std::pow(static_cast<double>(mDecayRate),
335                                  ADAPTIVE_HISTORY_EXPIRE_DAYS));
336     NS_ENSURE_SUCCESS(rv, rv);
337     rv = deleteAdaptive->Execute();
338     NS_ENSURE_SUCCESS(rv, rv);
339 
340     Telemetry::AccumulateTimeDelta(
341         Telemetry::PLACES_IDLE_FRECENCY_DECAY_TIME_MS, start);
342 
343     return NS_OK;
344   }
345 
346   RefPtr<Database> mDB;
347   float mDecayRate;
348   uint16_t mDecayReason;
349 };
350 
351 }  // namespace
352 
353 // Queries rows indexes to bind or get values, if adding a new one, be sure to
354 // update nsNavBookmarks statements and its kGetChildrenIndex_* constants
355 const int32_t nsNavHistory::kGetInfoIndex_PageID = 0;
356 const int32_t nsNavHistory::kGetInfoIndex_URL = 1;
357 const int32_t nsNavHistory::kGetInfoIndex_Title = 2;
358 const int32_t nsNavHistory::kGetInfoIndex_RevHost = 3;
359 const int32_t nsNavHistory::kGetInfoIndex_VisitCount = 4;
360 const int32_t nsNavHistory::kGetInfoIndex_VisitDate = 5;
361 const int32_t nsNavHistory::kGetInfoIndex_FaviconURL = 6;
362 const int32_t nsNavHistory::kGetInfoIndex_ItemId = 7;
363 const int32_t nsNavHistory::kGetInfoIndex_ItemDateAdded = 8;
364 const int32_t nsNavHistory::kGetInfoIndex_ItemLastModified = 9;
365 const int32_t nsNavHistory::kGetInfoIndex_ItemParentId = 10;
366 const int32_t nsNavHistory::kGetInfoIndex_ItemTags = 11;
367 const int32_t nsNavHistory::kGetInfoIndex_Frecency = 12;
368 const int32_t nsNavHistory::kGetInfoIndex_Hidden = 13;
369 const int32_t nsNavHistory::kGetInfoIndex_Guid = 14;
370 const int32_t nsNavHistory::kGetInfoIndex_VisitId = 15;
371 const int32_t nsNavHistory::kGetInfoIndex_FromVisitId = 16;
372 const int32_t nsNavHistory::kGetInfoIndex_VisitType = 17;
373 // These columns are followed by corresponding constants in nsNavBookmarks.cpp,
374 // which must be kept in sync:
375 // nsNavBookmarks::kGetChildrenIndex_Guid = 18;
376 // nsNavBookmarks::kGetChildrenIndex_Position = 19;
377 // nsNavBookmarks::kGetChildrenIndex_Type = 20;
378 // nsNavBookmarks::kGetChildrenIndex_PlaceID = 21;
379 
PLACES_FACTORY_SINGLETON_IMPLEMENTATION(nsNavHistory,gHistoryService)380 PLACES_FACTORY_SINGLETON_IMPLEMENTATION(nsNavHistory, gHistoryService)
381 
382 nsNavHistory::nsNavHistory()
383     : mCachedNow(0),
384       mRecentTyped(RECENT_EVENTS_INITIAL_CACHE_LENGTH),
385       mRecentLink(RECENT_EVENTS_INITIAL_CACHE_LENGTH),
386       mRecentBookmark(RECENT_EVENTS_INITIAL_CACHE_LENGTH),
387       mHistoryEnabled(true),
388       mMatchDiacritics(false),
389       mNumVisitsForFrecency(10),
390       mDecayFrecencyPendingCount(0),
391       mTagsFolder(-1),
392       mLastCachedStartOfDay(INT64_MAX),
393       mLastCachedEndOfDay(0) {
394   NS_ASSERTION(!gHistoryService,
395                "Attempting to create two instances of the service!");
396   gHistoryService = this;
397 }
398 
~nsNavHistory()399 nsNavHistory::~nsNavHistory() {
400   MOZ_ASSERT(NS_IsMainThread(), "Must be called on the main thread");
401 
402   // remove the static reference to the service. Check to make sure its us
403   // in case somebody creates an extra instance of the service.
404   NS_ASSERTION(gHistoryService == this,
405                "Deleting a non-singleton instance of the service");
406 
407   if (gHistoryService == this) gHistoryService = nullptr;
408 }
409 
Init()410 nsresult nsNavHistory::Init() {
411   LoadPrefs();
412 
413   mDB = Database::GetDatabase();
414   NS_ENSURE_STATE(mDB);
415 
416   /*****************************************************************************
417    *** IMPORTANT NOTICE!
418    ***
419    *** Nothing after these add observer calls should return anything but NS_OK.
420    *** If a failure code is returned, this nsNavHistory object will be held onto
421    *** by the observer service and the preference service.
422    ****************************************************************************/
423 
424   // Observe preferences changes.
425   Preferences::AddWeakObservers(this, kObservedPrefs);
426 
427   nsCOMPtr<nsIObserverService> obsSvc = services::GetObserverService();
428   if (obsSvc) {
429     (void)obsSvc->AddObserver(this, TOPIC_PLACES_CONNECTION_CLOSED, true);
430     (void)obsSvc->AddObserver(this, TOPIC_IDLE_DAILY, true);
431   }
432 
433   // Don't add code that can fail here! Do it up above, before we add our
434   // observers.
435 
436   return NS_OK;
437 }
438 
439 NS_IMETHODIMP
GetDatabaseStatus(uint16_t * aDatabaseStatus)440 nsNavHistory::GetDatabaseStatus(uint16_t* aDatabaseStatus) {
441   NS_ENSURE_ARG_POINTER(aDatabaseStatus);
442   *aDatabaseStatus = mDB->GetDatabaseStatus();
443   return NS_OK;
444 }
445 
GetRecentFlags(nsIURI * aURI)446 uint32_t nsNavHistory::GetRecentFlags(nsIURI* aURI) {
447   uint32_t result = 0;
448   nsAutoCString spec;
449   nsresult rv = aURI->GetSpec(spec);
450   NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Unable to get aURI's spec");
451 
452   if (NS_SUCCEEDED(rv)) {
453     if (CheckIsRecentEvent(&mRecentTyped, spec)) result |= RECENT_TYPED;
454     if (CheckIsRecentEvent(&mRecentLink, spec)) result |= RECENT_ACTIVATED;
455     if (CheckIsRecentEvent(&mRecentBookmark, spec)) result |= RECENT_BOOKMARKED;
456   }
457 
458   return result;
459 }
460 
GetIdForPage(nsIURI * aURI,int64_t * _pageId,nsCString & _GUID)461 nsresult nsNavHistory::GetIdForPage(nsIURI* aURI, int64_t* _pageId,
462                                     nsCString& _GUID) {
463   *_pageId = 0;
464 
465   nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
466       "SELECT id, url, title, rev_host, visit_count, guid "
467       "FROM moz_places "
468       "WHERE url_hash = hash(:page_url) AND url = :page_url ");
469   NS_ENSURE_STATE(stmt);
470   mozStorageStatementScoper scoper(stmt);
471 
472   nsresult rv = URIBinder::Bind(stmt, "page_url"_ns, aURI);
473   NS_ENSURE_SUCCESS(rv, rv);
474 
475   bool hasEntry = false;
476   rv = stmt->ExecuteStep(&hasEntry);
477   NS_ENSURE_SUCCESS(rv, rv);
478 
479   if (hasEntry) {
480     rv = stmt->GetInt64(0, _pageId);
481     NS_ENSURE_SUCCESS(rv, rv);
482     rv = stmt->GetUTF8String(5, _GUID);
483     NS_ENSURE_SUCCESS(rv, rv);
484   }
485 
486   return NS_OK;
487 }
488 
GetOrCreateIdForPage(nsIURI * aURI,int64_t * _pageId,nsCString & _GUID)489 nsresult nsNavHistory::GetOrCreateIdForPage(nsIURI* aURI, int64_t* _pageId,
490                                             nsCString& _GUID) {
491   nsresult rv = GetIdForPage(aURI, _pageId, _GUID);
492   NS_ENSURE_SUCCESS(rv, rv);
493 
494   if (*_pageId != 0) {
495     return NS_OK;
496   }
497 
498   {
499     // Create a new hidden, untyped and unvisited entry.
500     nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
501         "INSERT INTO moz_places (url, url_hash, rev_host, hidden, frecency, "
502         "guid) "
503         "VALUES (:page_url, hash(:page_url), :rev_host, :hidden, :frecency, "
504         ":guid) ");
505     NS_ENSURE_STATE(stmt);
506     mozStorageStatementScoper scoper(stmt);
507 
508     rv = URIBinder::Bind(stmt, "page_url"_ns, aURI);
509     NS_ENSURE_SUCCESS(rv, rv);
510     // host (reversed with trailing period)
511     nsAutoString revHost;
512     rv = GetReversedHostname(aURI, revHost);
513     // Not all URI types have hostnames, so this is optional.
514     if (NS_SUCCEEDED(rv)) {
515       rv = stmt->BindStringByName("rev_host"_ns, revHost);
516     } else {
517       rv = stmt->BindNullByName("rev_host"_ns);
518     }
519     NS_ENSURE_SUCCESS(rv, rv);
520     rv = stmt->BindInt32ByName("hidden"_ns, 1);
521     NS_ENSURE_SUCCESS(rv, rv);
522     nsAutoCString spec;
523     rv = aURI->GetSpec(spec);
524     NS_ENSURE_SUCCESS(rv, rv);
525     rv = stmt->BindInt32ByName("frecency"_ns, IsQueryURI(spec) ? 0 : -1);
526     NS_ENSURE_SUCCESS(rv, rv);
527     rv = GenerateGUID(_GUID);
528     NS_ENSURE_SUCCESS(rv, rv);
529     rv = stmt->BindUTF8StringByName("guid"_ns, _GUID);
530     NS_ENSURE_SUCCESS(rv, rv);
531 
532     rv = stmt->Execute();
533     NS_ENSURE_SUCCESS(rv, rv);
534 
535     *_pageId = sLastInsertedPlaceId;
536   }
537 
538   {
539     // Trigger the updates to the moz_origins tables
540     nsCOMPtr<mozIStorageStatement> stmt =
541         mDB->GetStatement("DELETE FROM moz_updateoriginsinsert_temp");
542     NS_ENSURE_STATE(stmt);
543     mozStorageStatementScoper scoper(stmt);
544   }
545 
546   return NS_OK;
547 }
548 
LoadPrefs()549 void nsNavHistory::LoadPrefs() {
550   // History preferences.
551   mHistoryEnabled = Preferences::GetBool(PREF_HISTORY_ENABLED, true);
552   mMatchDiacritics = Preferences::GetBool(PREF_MATCH_DIACRITICS, false);
553 
554   // Frecency preferences.
555 #define FRECENCY_PREF(_prop, _pref) \
556   _prop = Preferences::GetInt(_pref, _pref##_DEF)
557 
558   FRECENCY_PREF(mNumVisitsForFrecency, PREF_FREC_NUM_VISITS);
559   FRECENCY_PREF(mFirstBucketCutoffInDays, PREF_FREC_FIRST_BUCKET_CUTOFF);
560   FRECENCY_PREF(mSecondBucketCutoffInDays, PREF_FREC_SECOND_BUCKET_CUTOFF);
561   FRECENCY_PREF(mThirdBucketCutoffInDays, PREF_FREC_THIRD_BUCKET_CUTOFF);
562   FRECENCY_PREF(mFourthBucketCutoffInDays, PREF_FREC_FOURTH_BUCKET_CUTOFF);
563   FRECENCY_PREF(mEmbedVisitBonus, PREF_FREC_EMBED_VISIT_BONUS);
564   FRECENCY_PREF(mFramedLinkVisitBonus, PREF_FREC_FRAMED_LINK_VISIT_BONUS);
565   FRECENCY_PREF(mLinkVisitBonus, PREF_FREC_LINK_VISIT_BONUS);
566   FRECENCY_PREF(mTypedVisitBonus, PREF_FREC_TYPED_VISIT_BONUS);
567   FRECENCY_PREF(mBookmarkVisitBonus, PREF_FREC_BOOKMARK_VISIT_BONUS);
568   FRECENCY_PREF(mDownloadVisitBonus, PREF_FREC_DOWNLOAD_VISIT_BONUS);
569   FRECENCY_PREF(mPermRedirectVisitBonus, PREF_FREC_PERM_REDIRECT_VISIT_BONUS);
570   FRECENCY_PREF(mTempRedirectVisitBonus, PREF_FREC_TEMP_REDIRECT_VISIT_BONUS);
571   FRECENCY_PREF(mRedirectSourceVisitBonus, PREF_FREC_REDIR_SOURCE_VISIT_BONUS);
572   FRECENCY_PREF(mDefaultVisitBonus, PREF_FREC_DEFAULT_VISIT_BONUS);
573   FRECENCY_PREF(mUnvisitedBookmarkBonus, PREF_FREC_UNVISITED_BOOKMARK_BONUS);
574   FRECENCY_PREF(mUnvisitedTypedBonus, PREF_FREC_UNVISITED_TYPED_BONUS);
575   FRECENCY_PREF(mReloadVisitBonus, PREF_FREC_RELOAD_VISIT_BONUS);
576   FRECENCY_PREF(mFirstBucketWeight, PREF_FREC_FIRST_BUCKET_WEIGHT);
577   FRECENCY_PREF(mSecondBucketWeight, PREF_FREC_SECOND_BUCKET_WEIGHT);
578   FRECENCY_PREF(mThirdBucketWeight, PREF_FREC_THIRD_BUCKET_WEIGHT);
579   FRECENCY_PREF(mFourthBucketWeight, PREF_FREC_FOURTH_BUCKET_WEIGHT);
580   FRECENCY_PREF(mDefaultWeight, PREF_FREC_DEFAULT_BUCKET_WEIGHT);
581 
582 #undef FRECENCY_PREF
583 }
584 
UpdateDaysOfHistory(PRTime visitTime)585 void nsNavHistory::UpdateDaysOfHistory(PRTime visitTime) {
586   if (sDaysOfHistory == 0) {
587     sDaysOfHistory = 1;
588   }
589 
590   if (visitTime > mLastCachedEndOfDay || visitTime < mLastCachedStartOfDay) {
591     InvalidateDaysOfHistory();
592   }
593 }
594 
595 NS_IMETHODIMP
RecalculateOriginFrecencyStats(nsIObserver * aCallback)596 nsNavHistory::RecalculateOriginFrecencyStats(nsIObserver* aCallback) {
597   RefPtr<nsNavHistory> self(this);
598   nsMainThreadPtrHandle<nsIObserver> callback(
599       !aCallback ? nullptr
600                  : new nsMainThreadPtrHolder<nsIObserver>(
601                        "nsNavHistory::RecalculateOriginFrecencyStats callback",
602                        aCallback));
603 
604   nsCOMPtr<nsIEventTarget> target(do_GetInterface(mDB->MainConn()));
605   NS_ENSURE_STATE(target);
606   nsresult rv = target->Dispatch(NS_NewRunnableFunction(
607       "nsNavHistory::RecalculateOriginFrecencyStats", [self, callback] {
608         Unused << self->RecalculateOriginFrecencyStatsInternal();
609         Unused << NS_DispatchToMainThread(NS_NewRunnableFunction(
610             "nsNavHistory::RecalculateOriginFrecencyStats callback",
611             [callback] {
612               if (callback) {
613                 Unused << callback->Observe(nullptr, "", nullptr);
614               }
615             }));
616       }));
617   NS_ENSURE_SUCCESS(rv, rv);
618 
619   return NS_OK;
620 }
621 
RecalculateOriginFrecencyStatsInternal()622 nsresult nsNavHistory::RecalculateOriginFrecencyStatsInternal() {
623   nsCOMPtr<mozIStorageConnection> conn(mDB->MainConn());
624   NS_ENSURE_STATE(conn);
625 
626   nsresult rv = conn->ExecuteSimpleSQL(nsLiteralCString(
627       "INSERT OR REPLACE INTO moz_meta(key, value) VALUES "
628       "( "
629       "'" MOZ_META_KEY_ORIGIN_FRECENCY_COUNT
630       "' , "
631       "(SELECT COUNT(*) FROM moz_origins WHERE frecency > 0) "
632       "), "
633       "( "
634       "'" MOZ_META_KEY_ORIGIN_FRECENCY_SUM
635       "', "
636       "(SELECT TOTAL(frecency) FROM moz_origins WHERE frecency > 0) "
637       "), "
638       "( "
639       "'" MOZ_META_KEY_ORIGIN_FRECENCY_SUM_OF_SQUARES
640       "' , "
641       "(SELECT TOTAL(frecency * frecency) FROM moz_origins WHERE frecency > 0) "
642       ") "));
643   NS_ENSURE_SUCCESS(rv, rv);
644 
645   return NS_OK;
646 }
647 
648 Atomic<int64_t> nsNavHistory::sLastInsertedPlaceId(0);
649 Atomic<int64_t> nsNavHistory::sLastInsertedVisitId(0);
650 
651 void  // static
StoreLastInsertedId(const nsACString & aTable,const int64_t aLastInsertedId)652 nsNavHistory::StoreLastInsertedId(const nsACString& aTable,
653                                   const int64_t aLastInsertedId) {
654   if (aTable.EqualsLiteral("moz_places")) {
655     nsNavHistory::sLastInsertedPlaceId = aLastInsertedId;
656   } else if (aTable.EqualsLiteral("moz_historyvisits")) {
657     nsNavHistory::sLastInsertedVisitId = aLastInsertedId;
658   } else {
659     MOZ_ASSERT(false, "Trying to store the insert id for an unknown table?");
660   }
661 }
662 
663 Atomic<int32_t> nsNavHistory::sDaysOfHistory(-1);
664 
665 void  // static
InvalidateDaysOfHistory()666 nsNavHistory::InvalidateDaysOfHistory() {
667   sDaysOfHistory = -1;
668 }
669 
GetDaysOfHistory()670 int32_t nsNavHistory::GetDaysOfHistory() {
671   MOZ_ASSERT(NS_IsMainThread(), "This can only be called on the main thread");
672 
673   if (sDaysOfHistory != -1) return sDaysOfHistory;
674 
675   // SQLite doesn't have a CEIL() function, so we must do that later.
676   // We should also take into account timers resolution, that may be as bad as
677   // 16ms on Windows, so in some cases the difference may be 0, if the
678   // check is done near the visit.  Thus remember to check for NULL separately.
679   nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
680       "SELECT CAST(( "
681       "strftime('%s','now','localtime','utc') - "
682       "(SELECT MIN(visit_date)/1000000 FROM moz_historyvisits) "
683       ") AS DOUBLE) "
684       "/86400, "
685       "strftime('%s','now','localtime','+1 day','start of day','utc') * "
686       "1000000");
687   NS_ENSURE_TRUE(stmt, 0);
688   mozStorageStatementScoper scoper(stmt);
689 
690   bool hasResult;
691   if (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) {
692     // If we get NULL, then there are no visits, otherwise there must always be
693     // at least 1 day of history.
694     bool hasNoVisits;
695     (void)stmt->GetIsNull(0, &hasNoVisits);
696     sDaysOfHistory =
697         hasNoVisits
698             ? 0
699             : std::max(1, static_cast<int32_t>(ceil(stmt->AsDouble(0))));
700     mLastCachedStartOfDay =
701         NormalizeTime(nsINavHistoryQuery::TIME_RELATIVE_TODAY, 0);
702     mLastCachedEndOfDay = stmt->AsInt64(1) - 1;  // Start of tomorrow - 1.
703   }
704 
705   return sDaysOfHistory;
706 }
707 
GetNow()708 PRTime nsNavHistory::GetNow() {
709   if (!mCachedNow) {
710     mCachedNow = PR_Now();
711     if (!mExpireNowTimer) mExpireNowTimer = NS_NewTimer();
712     if (mExpireNowTimer)
713       mExpireNowTimer->InitWithNamedFuncCallback(
714           expireNowTimerCallback, this, RENEW_CACHED_NOW_TIMEOUT,
715           nsITimer::TYPE_ONE_SHOT, "nsNavHistory::GetNow");
716   }
717   return mCachedNow;
718 }
719 
expireNowTimerCallback(nsITimer * aTimer,void * aClosure)720 void nsNavHistory::expireNowTimerCallback(nsITimer* aTimer, void* aClosure) {
721   nsNavHistory* history = static_cast<nsNavHistory*>(aClosure);
722   if (history) {
723     history->mCachedNow = 0;
724     history->mExpireNowTimer = nullptr;
725   }
726 }
727 
728 /**
729  * Code borrowed from mozilla/xpfe/components/history/src/nsGlobalHistory.cpp
730  * Pass in a pre-normalized now and a date, and we'll find the difference since
731  * midnight on each of the days.
732  */
NormalizeTimeRelativeToday(PRTime aTime)733 static PRTime NormalizeTimeRelativeToday(PRTime aTime) {
734   // round to midnight this morning
735   PRExplodedTime explodedTime;
736   PR_ExplodeTime(aTime, PR_LocalTimeParameters, &explodedTime);
737 
738   // set to midnight (0:00)
739   explodedTime.tm_min = explodedTime.tm_hour = explodedTime.tm_sec =
740       explodedTime.tm_usec = 0;
741 
742   return PR_ImplodeTime(&explodedTime);
743 }
744 
745 // nsNavHistory::NormalizeTime
746 //
747 //    Converts a nsINavHistoryQuery reference+offset time into a PRTime
748 //    relative to the epoch.
749 //
750 //    It is important that this function NOT use the current time optimization.
751 //    It is called to update queries, and we really need to know what right
752 //    now is because those incoming values will also have current times that
753 //    we will have to compare against.
754 
755 PRTime  // static
NormalizeTime(uint32_t aRelative,PRTime aOffset)756 nsNavHistory::NormalizeTime(uint32_t aRelative, PRTime aOffset) {
757   PRTime ref;
758   switch (aRelative) {
759     case nsINavHistoryQuery::TIME_RELATIVE_EPOCH:
760       return aOffset;
761     case nsINavHistoryQuery::TIME_RELATIVE_TODAY:
762       ref = NormalizeTimeRelativeToday(PR_Now());
763       break;
764     case nsINavHistoryQuery::TIME_RELATIVE_NOW:
765       ref = PR_Now();
766       break;
767     default:
768       MOZ_ASSERT_UNREACHABLE("Invalid relative time");
769       return 0;
770   }
771   return ref + aOffset;
772 }
773 
774 // nsNavHistory::DomainNameFromURI
775 //
776 //    This does the www.mozilla.org -> mozilla.org and
777 //    foo.theregister.co.uk -> theregister.co.uk conversion
DomainNameFromURI(nsIURI * aURI,nsACString & aDomainName)778 void nsNavHistory::DomainNameFromURI(nsIURI* aURI, nsACString& aDomainName) {
779   // lazily get the effective tld service
780   if (!mTLDService)
781     mTLDService = do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID);
782 
783   if (mTLDService) {
784     // get the base domain for a given hostname.
785     // e.g. for "images.bbc.co.uk", this would be "bbc.co.uk".
786     nsresult rv = mTLDService->GetBaseDomain(aURI, 0, aDomainName);
787     if (NS_SUCCEEDED(rv)) return;
788   }
789 
790   // just return the original hostname
791   // (it's also possible the host is an IP address)
792   aURI->GetAsciiHost(aDomainName);
793 }
794 
hasHistoryEntries()795 bool nsNavHistory::hasHistoryEntries() { return GetDaysOfHistory() > 0; }
796 
797 // Call this method before visiting a URL in order to help determine the
798 // transition type of the visit.
799 //
800 // @see MarkPageAsTyped
801 
802 NS_IMETHODIMP
MarkPageAsFollowedBookmark(nsIURI * aURI)803 nsNavHistory::MarkPageAsFollowedBookmark(nsIURI* aURI) {
804   NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
805   NS_ENSURE_ARG(aURI);
806 
807   // don't add when history is disabled
808   if (IsHistoryDisabled()) return NS_OK;
809 
810   nsAutoCString uriString;
811   nsresult rv = aURI->GetSpec(uriString);
812   NS_ENSURE_SUCCESS(rv, rv);
813 
814   mRecentBookmark.InsertOrUpdate(uriString, GetNow());
815 
816   if (mRecentBookmark.Count() > RECENT_EVENT_QUEUE_MAX_LENGTH)
817     ExpireNonrecentEvents(&mRecentBookmark);
818 
819   return NS_OK;
820 }
821 
822 // nsNavHistory::CanAddURI
823 //
824 //    Filter out unwanted URIs such as "chrome:", "mailbox:", etc.
825 //
826 //    The model is if we don't know differently then add which basically means
827 //    we are suppose to try all the things we know not to allow in and then if
828 //    we don't bail go on and allow it in.
829 
830 NS_IMETHODIMP
CanAddURI(nsIURI * aURI,bool * canAdd)831 nsNavHistory::CanAddURI(nsIURI* aURI, bool* canAdd) {
832   NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
833   NS_ENSURE_ARG(aURI);
834   NS_ENSURE_ARG_POINTER(canAdd);
835 
836   // If history is disabled, don't add any entry.
837   *canAdd = !IsHistoryDisabled() && BaseHistory::CanStore(aURI);
838   return NS_OK;
839 }
840 
841 NS_IMETHODIMP
GetNewQuery(nsINavHistoryQuery ** _retval)842 nsNavHistory::GetNewQuery(nsINavHistoryQuery** _retval) {
843   NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
844   NS_ENSURE_ARG_POINTER(_retval);
845 
846   RefPtr<nsNavHistoryQuery> query = new nsNavHistoryQuery();
847   query.forget(_retval);
848   return NS_OK;
849 }
850 
851 // nsNavHistory::GetNewQueryOptions
852 
853 NS_IMETHODIMP
GetNewQueryOptions(nsINavHistoryQueryOptions ** _retval)854 nsNavHistory::GetNewQueryOptions(nsINavHistoryQueryOptions** _retval) {
855   NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
856   NS_ENSURE_ARG_POINTER(_retval);
857 
858   RefPtr<nsNavHistoryQueryOptions> queryOptions =
859       new nsNavHistoryQueryOptions();
860   queryOptions.forget(_retval);
861   return NS_OK;
862 }
863 
864 NS_IMETHODIMP
ExecuteQuery(nsINavHistoryQuery * aQuery,nsINavHistoryQueryOptions * aOptions,nsINavHistoryResult ** _retval)865 nsNavHistory::ExecuteQuery(nsINavHistoryQuery* aQuery,
866                            nsINavHistoryQueryOptions* aOptions,
867                            nsINavHistoryResult** _retval) {
868   NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
869   NS_ENSURE_ARG(aQuery);
870   NS_ENSURE_ARG(aOptions);
871   NS_ENSURE_ARG_POINTER(_retval);
872 
873   // Clone the input query and options, because the caller might change the
874   // objects, but we always want to reflect the original parameters.
875   nsCOMPtr<nsINavHistoryQuery> queryClone;
876   aQuery->Clone(getter_AddRefs(queryClone));
877   NS_ENSURE_STATE(queryClone);
878   RefPtr<nsNavHistoryQuery> query = do_QueryObject(queryClone);
879   NS_ENSURE_STATE(query);
880   nsCOMPtr<nsINavHistoryQueryOptions> optionsClone;
881   aOptions->Clone(getter_AddRefs(optionsClone));
882   NS_ENSURE_STATE(optionsClone);
883   RefPtr<nsNavHistoryQueryOptions> options = do_QueryObject(optionsClone);
884   NS_ENSURE_STATE(options);
885 
886   // Create the root node.
887   RefPtr<nsNavHistoryContainerResultNode> rootNode;
888 
889   nsCString folderGuid = GetSimpleBookmarksQueryParent(query, options);
890   if (!folderGuid.IsEmpty()) {
891     // In the simple case where we're just querying children of a single
892     // bookmark folder, we can more efficiently generate results.
893     nsNavBookmarks* bookmarks = nsNavBookmarks::GetBookmarksService();
894     NS_ENSURE_TRUE(bookmarks, NS_ERROR_OUT_OF_MEMORY);
895     RefPtr<nsNavHistoryResultNode> tempRootNode;
896     nsresult rv = bookmarks->ResultNodeForContainer(
897         folderGuid, options, getter_AddRefs(tempRootNode));
898     if (NS_SUCCEEDED(rv)) {
899       rootNode = tempRootNode->GetAsContainer();
900     } else {
901       NS_WARNING("Generating a generic empty node for a broken query!");
902       // This is a perf hack to generate an empty query that skips filtering.
903       options->SetExcludeItems(true);
904     }
905   }
906 
907   if (!rootNode) {
908     // Either this is not a folder shortcut, or is a broken one.  In both cases
909     // just generate a query node.
910     nsAutoCString queryUri;
911     nsresult rv = QueryToQueryString(query, options, queryUri);
912     NS_ENSURE_SUCCESS(rv, rv);
913     rootNode =
914         new nsNavHistoryQueryResultNode(""_ns, 0, queryUri, query, options);
915   }
916 
917   // Create the result that will hold nodes.  Inject batching status into it.
918   RefPtr<nsNavHistoryResult> result =
919       new nsNavHistoryResult(rootNode, query, options);
920   result.forget(_retval);
921   return NS_OK;
922 }
923 
924 // determine from our nsNavHistoryQuery array and nsNavHistoryQueryOptions
925 // if this is the place query from the history menu.
926 // from browser-menubar.inc, our history menu query is:
927 // place:sort=4&maxResults=10
928 // note, any maxResult > 0 will still be considered a history menu query
929 // or if this is the place query from the old "Most Visited" item in some
930 // profiles: folder: place:sort=8&maxResults=10 note, any maxResult > 0 will
931 // still be considered a Most Visited menu query
IsOptimizableHistoryQuery(const RefPtr<nsNavHistoryQuery> & aQuery,const RefPtr<nsNavHistoryQueryOptions> & aOptions,uint16_t aSortMode)932 static bool IsOptimizableHistoryQuery(
933     const RefPtr<nsNavHistoryQuery>& aQuery,
934     const RefPtr<nsNavHistoryQueryOptions>& aOptions, uint16_t aSortMode) {
935   if (aOptions->QueryType() != nsINavHistoryQueryOptions::QUERY_TYPE_HISTORY)
936     return false;
937 
938   if (aOptions->ResultType() != nsINavHistoryQueryOptions::RESULTS_AS_URI)
939     return false;
940 
941   if (aOptions->SortingMode() != aSortMode) return false;
942 
943   if (aOptions->MaxResults() <= 0) return false;
944 
945   if (aOptions->ExcludeItems()) return false;
946 
947   if (aOptions->IncludeHidden()) return false;
948 
949   if (aQuery->MinVisits() != -1 || aQuery->MaxVisits() != -1) return false;
950 
951   if (aQuery->BeginTime() || aQuery->BeginTimeReference()) return false;
952 
953   if (aQuery->EndTime() || aQuery->EndTimeReference()) return false;
954 
955   if (!aQuery->SearchTerms().IsEmpty()) return false;
956 
957   if (aQuery->OnlyBookmarked()) return false;
958 
959   if (aQuery->DomainIsHost() || !aQuery->Domain().IsEmpty()) return false;
960 
961   if (aQuery->AnnotationIsNot() || !aQuery->Annotation().IsEmpty())
962     return false;
963 
964   if (aQuery->Parents().Length() > 0) return false;
965 
966   if (aQuery->Tags().Length() > 0) return false;
967 
968   if (aQuery->Transitions().Length() > 0) return false;
969 
970   return true;
971 }
972 
NeedToFilterResultSet(const RefPtr<nsNavHistoryQuery> & aQuery,nsNavHistoryQueryOptions * aOptions)973 static bool NeedToFilterResultSet(const RefPtr<nsNavHistoryQuery>& aQuery,
974                                   nsNavHistoryQueryOptions* aOptions) {
975   return aOptions->ExcludeQueries();
976 }
977 
978 // ** Helper class for ConstructQueryString **/
979 
980 class PlacesSQLQueryBuilder {
981  public:
982   PlacesSQLQueryBuilder(const nsCString& aConditions,
983                         const RefPtr<nsNavHistoryQuery>& aQuery,
984                         const RefPtr<nsNavHistoryQueryOptions>& aOptions,
985                         bool aUseLimit, nsNavHistory::StringHash& aAddParams,
986                         bool aHasSearchTerms);
987 
988   nsresult GetQueryString(nsCString& aQueryString);
989 
990  private:
991   nsresult Select();
992 
993   nsresult SelectAsURI();
994   nsresult SelectAsVisit();
995   nsresult SelectAsDay();
996   nsresult SelectAsSite();
997   nsresult SelectAsTag();
998   nsresult SelectAsRoots();
999   nsresult SelectAsLeftPane();
1000 
1001   nsresult Where();
1002   nsresult GroupBy();
1003   nsresult OrderBy();
1004   nsresult Limit();
1005 
1006   void OrderByColumnIndexAsc(int32_t aIndex);
1007   void OrderByColumnIndexDesc(int32_t aIndex);
1008   // Use these if you want a case insensitive sorting.
1009   void OrderByTextColumnIndexAsc(int32_t aIndex);
1010   void OrderByTextColumnIndexDesc(int32_t aIndex);
1011 
1012   const nsCString& mConditions;
1013   bool mUseLimit;
1014   bool mHasSearchTerms;
1015 
1016   uint16_t mResultType;
1017   uint16_t mQueryType;
1018   bool mIncludeHidden;
1019   uint16_t mSortingMode;
1020   uint32_t mMaxResults;
1021 
1022   nsCString mQueryString;
1023   nsCString mGroupBy;
1024   bool mHasDateColumns;
1025   bool mSkipOrderBy;
1026 
1027   nsNavHistory::StringHash& mAddParams;
1028 };
1029 
PlacesSQLQueryBuilder(const nsCString & aConditions,const RefPtr<nsNavHistoryQuery> & aQuery,const RefPtr<nsNavHistoryQueryOptions> & aOptions,bool aUseLimit,nsNavHistory::StringHash & aAddParams,bool aHasSearchTerms)1030 PlacesSQLQueryBuilder::PlacesSQLQueryBuilder(
1031     const nsCString& aConditions, const RefPtr<nsNavHistoryQuery>& aQuery,
1032     const RefPtr<nsNavHistoryQueryOptions>& aOptions, bool aUseLimit,
1033     nsNavHistory::StringHash& aAddParams, bool aHasSearchTerms)
1034     : mConditions(aConditions),
1035       mUseLimit(aUseLimit),
1036       mHasSearchTerms(aHasSearchTerms),
1037       mResultType(aOptions->ResultType()),
1038       mQueryType(aOptions->QueryType()),
1039       mIncludeHidden(aOptions->IncludeHidden()),
1040       mSortingMode(aOptions->SortingMode()),
1041       mMaxResults(aOptions->MaxResults()),
1042       mSkipOrderBy(false),
1043       mAddParams(aAddParams) {
1044   mHasDateColumns =
1045       (mQueryType == nsINavHistoryQueryOptions::QUERY_TYPE_BOOKMARKS);
1046   // Force the default sorting mode for tag queries.
1047   if (mSortingMode == nsINavHistoryQueryOptions::SORT_BY_NONE &&
1048       aQuery->Tags().Length() > 0) {
1049     mSortingMode = nsINavHistoryQueryOptions::SORT_BY_TITLE_ASCENDING;
1050   }
1051 }
1052 
GetQueryString(nsCString & aQueryString)1053 nsresult PlacesSQLQueryBuilder::GetQueryString(nsCString& aQueryString) {
1054   nsresult rv = Select();
1055   NS_ENSURE_SUCCESS(rv, rv);
1056   rv = Where();
1057   NS_ENSURE_SUCCESS(rv, rv);
1058   rv = GroupBy();
1059   NS_ENSURE_SUCCESS(rv, rv);
1060   rv = OrderBy();
1061   NS_ENSURE_SUCCESS(rv, rv);
1062   rv = Limit();
1063   NS_ENSURE_SUCCESS(rv, rv);
1064 
1065   aQueryString = mQueryString;
1066   return NS_OK;
1067 }
1068 
Select()1069 nsresult PlacesSQLQueryBuilder::Select() {
1070   nsresult rv;
1071 
1072   switch (mResultType) {
1073     case nsINavHistoryQueryOptions::RESULTS_AS_URI:
1074       rv = SelectAsURI();
1075       NS_ENSURE_SUCCESS(rv, rv);
1076       break;
1077 
1078     case nsINavHistoryQueryOptions::RESULTS_AS_VISIT:
1079       rv = SelectAsVisit();
1080       NS_ENSURE_SUCCESS(rv, rv);
1081       break;
1082 
1083     case nsINavHistoryQueryOptions::RESULTS_AS_DATE_QUERY:
1084     case nsINavHistoryQueryOptions::RESULTS_AS_DATE_SITE_QUERY:
1085       rv = SelectAsDay();
1086       NS_ENSURE_SUCCESS(rv, rv);
1087       break;
1088 
1089     case nsINavHistoryQueryOptions::RESULTS_AS_SITE_QUERY:
1090       rv = SelectAsSite();
1091       NS_ENSURE_SUCCESS(rv, rv);
1092       break;
1093 
1094     case nsINavHistoryQueryOptions::RESULTS_AS_TAGS_ROOT:
1095       rv = SelectAsTag();
1096       NS_ENSURE_SUCCESS(rv, rv);
1097       break;
1098 
1099     case nsINavHistoryQueryOptions::RESULTS_AS_ROOTS_QUERY:
1100       rv = SelectAsRoots();
1101       NS_ENSURE_SUCCESS(rv, rv);
1102       break;
1103 
1104     case nsINavHistoryQueryOptions::RESULTS_AS_LEFT_PANE_QUERY:
1105       rv = SelectAsLeftPane();
1106       NS_ENSURE_SUCCESS(rv, rv);
1107       break;
1108 
1109     default:
1110       MOZ_ASSERT_UNREACHABLE("Invalid result type");
1111   }
1112   return NS_OK;
1113 }
1114 
SelectAsURI()1115 nsresult PlacesSQLQueryBuilder::SelectAsURI() {
1116   nsNavHistory* history = nsNavHistory::GetHistoryService();
1117   NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
1118   nsAutoCString tagsSqlFragment;
1119 
1120   switch (mQueryType) {
1121     case nsINavHistoryQueryOptions::QUERY_TYPE_HISTORY:
1122       GetTagsSqlFragment(history->GetTagsFolder(), "h.id"_ns, mHasSearchTerms,
1123                          tagsSqlFragment);
1124 
1125       mQueryString = nsLiteralCString(
1126                          "SELECT h.id, h.url, h.title AS page_title, "
1127                          "h.rev_host, h.visit_count, "
1128                          "h.last_visit_date, null, null, null, null, null, ") +
1129                      tagsSqlFragment +
1130                      nsLiteralCString(
1131                          ", h.frecency, h.hidden, h.guid, "
1132                          "null, null, null "
1133                          "FROM moz_places h "
1134                          // WHERE 1 is a no-op since additonal conditions will
1135                          // start with AND.
1136                          "WHERE 1 "
1137                          "{QUERY_OPTIONS_VISITS} {QUERY_OPTIONS_PLACES} "
1138                          "{ADDITIONAL_CONDITIONS} ");
1139       break;
1140 
1141     case nsINavHistoryQueryOptions::QUERY_TYPE_BOOKMARKS:
1142 
1143       GetTagsSqlFragment(history->GetTagsFolder(), "b.fk"_ns, mHasSearchTerms,
1144                          tagsSqlFragment);
1145       mQueryString =
1146           nsLiteralCString(
1147               "SELECT b.fk, h.url, b.title AS page_title, "
1148               "h.rev_host, h.visit_count, h.last_visit_date, null, b.id, "
1149               "b.dateAdded, b.lastModified, b.parent, ") +
1150           tagsSqlFragment +
1151           nsLiteralCString(
1152               ", h.frecency, h.hidden, h.guid,"
1153               "null, null, null, b.guid, b.position, b.type, b.fk "
1154               "FROM moz_bookmarks b "
1155               "JOIN moz_places h ON b.fk = h.id "
1156               "WHERE NOT EXISTS "
1157               "(SELECT id FROM moz_bookmarks "
1158               "WHERE id = b.parent AND parent = ") +
1159           nsPrintfCString("%" PRId64, history->GetTagsFolder()) +
1160           nsLiteralCString(
1161               ") "
1162               "AND NOT h.url_hash BETWEEN hash('place', 'prefix_lo') AND "
1163               "hash('place', 'prefix_hi') "
1164               "{ADDITIONAL_CONDITIONS}");
1165       break;
1166 
1167     default:
1168       return NS_ERROR_NOT_IMPLEMENTED;
1169   }
1170   return NS_OK;
1171 }
1172 
SelectAsVisit()1173 nsresult PlacesSQLQueryBuilder::SelectAsVisit() {
1174   nsNavHistory* history = nsNavHistory::GetHistoryService();
1175   NS_ENSURE_TRUE(history, NS_ERROR_OUT_OF_MEMORY);
1176   nsAutoCString tagsSqlFragment;
1177   GetTagsSqlFragment(history->GetTagsFolder(), "h.id"_ns, mHasSearchTerms,
1178                      tagsSqlFragment);
1179   mQueryString =
1180       nsLiteralCString(
1181           "SELECT h.id, h.url, h.title AS page_title, h.rev_host, "
1182           "h.visit_count, "
1183           "v.visit_date, null, null, null, null, null, ") +
1184       tagsSqlFragment +
1185       nsLiteralCString(
1186           ", h.frecency, h.hidden, h.guid, "
1187           "v.id, v.from_visit, v.visit_type "
1188           "FROM moz_places h "
1189           "JOIN moz_historyvisits v ON h.id = v.place_id "
1190           // WHERE 1 is a no-op since additonal conditions will start with AND.
1191           "WHERE 1 "
1192           "{QUERY_OPTIONS_VISITS} {QUERY_OPTIONS_PLACES} "
1193           "{ADDITIONAL_CONDITIONS} ");
1194 
1195   return NS_OK;
1196 }
1197 
SelectAsDay()1198 nsresult PlacesSQLQueryBuilder::SelectAsDay() {
1199   mSkipOrderBy = true;
1200 
1201   // Sort child queries based on sorting mode if it's provided, otherwise
1202   // fallback to default sort by title ascending.
1203   uint16_t sortingMode = nsINavHistoryQueryOptions::SORT_BY_TITLE_ASCENDING;
1204   if (mSortingMode != nsINavHistoryQueryOptions::SORT_BY_NONE &&
1205       mResultType == nsINavHistoryQueryOptions::RESULTS_AS_DATE_QUERY)
1206     sortingMode = mSortingMode;
1207 
1208   uint16_t resultType =
1209       mResultType == nsINavHistoryQueryOptions::RESULTS_AS_DATE_QUERY
1210           ? (uint16_t)nsINavHistoryQueryOptions::RESULTS_AS_URI
1211           : (uint16_t)nsINavHistoryQueryOptions::RESULTS_AS_SITE_QUERY;
1212 
1213   // beginTime will become the node's time property, we don't use endTime
1214   // because it could overlap, and we use time to sort containers and find
1215   // insert position in a result.
1216   mQueryString = nsPrintfCString(
1217       "SELECT null, "
1218       "'place:type=%d&sort=%d&beginTime='||beginTime||'&endTime='||endTime, "
1219       "dayTitle, null, null, beginTime, null, null, null, null, null, null, "
1220       "null, null, null "
1221       "FROM (",  // TOUTER BEGIN
1222       resultType, sortingMode);
1223 
1224   nsNavHistory* history = nsNavHistory::GetHistoryService();
1225   NS_ENSURE_STATE(history);
1226 
1227   int32_t daysOfHistory = history->GetDaysOfHistory();
1228   for (int32_t i = 0; i <= HISTORY_DATE_CONT_NUM(daysOfHistory); i++) {
1229     nsAutoCString dateName;
1230     // Timeframes are calculated as BeginTime <= container < EndTime.
1231     // Notice times can't be relative to now, since to recognize a query we
1232     // must ensure it won't change based on the time it is built.
1233     // So, to select till now, we really select till start of tomorrow, that is
1234     // a fixed timestamp.
1235     // These are used as limits for the inside containers.
1236     nsAutoCString sqlFragmentContainerBeginTime, sqlFragmentContainerEndTime;
1237     // These are used to query if the container should be visible.
1238     nsAutoCString sqlFragmentSearchBeginTime, sqlFragmentSearchEndTime;
1239     switch (i) {
1240       case 0:
1241         // Today
1242         history->GetStringFromName("finduri-AgeInDays-is-0", dateName);
1243         // From start of today
1244         sqlFragmentContainerBeginTime = nsLiteralCString(
1245             "(strftime('%s','now','localtime','start of day','utc')*1000000)");
1246         // To now (tomorrow)
1247         sqlFragmentContainerEndTime = nsLiteralCString(
1248             "(strftime('%s','now','localtime','start of day','+1 "
1249             "day','utc')*1000000)");
1250         // Search for the same timeframe.
1251         sqlFragmentSearchBeginTime = sqlFragmentContainerBeginTime;
1252         sqlFragmentSearchEndTime = sqlFragmentContainerEndTime;
1253         break;
1254       case 1:
1255         // Yesterday
1256         history->GetStringFromName("finduri-AgeInDays-is-1", dateName);
1257         // From start of yesterday
1258         sqlFragmentContainerBeginTime = nsLiteralCString(
1259             "(strftime('%s','now','localtime','start of day','-1 "
1260             "day','utc')*1000000)");
1261         // To start of today
1262         sqlFragmentContainerEndTime = nsLiteralCString(
1263             "(strftime('%s','now','localtime','start of day','utc')*1000000)");
1264         // Search for the same timeframe.
1265         sqlFragmentSearchBeginTime = sqlFragmentContainerBeginTime;
1266         sqlFragmentSearchEndTime = sqlFragmentContainerEndTime;
1267         break;
1268       case 2:
1269         // Last 7 days
1270         history->GetAgeInDaysString(7, "finduri-AgeInDays-last-is", dateName);
1271         // From start of 7 days ago
1272         sqlFragmentContainerBeginTime = nsLiteralCString(
1273             "(strftime('%s','now','localtime','start of day','-7 "
1274             "days','utc')*1000000)");
1275         // To now (tomorrow)
1276         sqlFragmentContainerEndTime = nsLiteralCString(
1277             "(strftime('%s','now','localtime','start of day','+1 "
1278             "day','utc')*1000000)");
1279         // This is an overlapped container, but we show it only if there are
1280         // visits older than yesterday.
1281         sqlFragmentSearchBeginTime = sqlFragmentContainerBeginTime;
1282         sqlFragmentSearchEndTime = nsLiteralCString(
1283             "(strftime('%s','now','localtime','start of day','-1 "
1284             "day','utc')*1000000)");
1285         break;
1286       case 3:
1287         // This month
1288         history->GetStringFromName("finduri-AgeInMonths-is-0", dateName);
1289         // From start of this month
1290         sqlFragmentContainerBeginTime = nsLiteralCString(
1291             "(strftime('%s','now','localtime','start of "
1292             "month','utc')*1000000)");
1293         // To now (tomorrow)
1294         sqlFragmentContainerEndTime = nsLiteralCString(
1295             "(strftime('%s','now','localtime','start of day','+1 "
1296             "day','utc')*1000000)");
1297         // This is an overlapped container, but we show it only if there are
1298         // visits older than 7 days ago.
1299         sqlFragmentSearchBeginTime = sqlFragmentContainerBeginTime;
1300         sqlFragmentSearchEndTime = nsLiteralCString(
1301             "(strftime('%s','now','localtime','start of day','-7 "
1302             "days','utc')*1000000)");
1303         break;
1304       default:
1305         if (i == HISTORY_ADDITIONAL_DATE_CONT_NUM + 6) {
1306           // Older than 6 months
1307           history->GetAgeInDaysString(6, "finduri-AgeInMonths-isgreater",
1308                                       dateName);
1309           // From start of epoch
1310           sqlFragmentContainerBeginTime =
1311               "(datetime(0, 'unixepoch')*1000000)"_ns;
1312           // To start of 6 months ago ( 5 months + this month).
1313           sqlFragmentContainerEndTime = nsLiteralCString(
1314               "(strftime('%s','now','localtime','start of month','-5 "
1315               "months','utc')*1000000)");
1316           // Search for the same timeframe.
1317           sqlFragmentSearchBeginTime = sqlFragmentContainerBeginTime;
1318           sqlFragmentSearchEndTime = sqlFragmentContainerEndTime;
1319           break;
1320         }
1321         int32_t MonthIndex = i - HISTORY_ADDITIONAL_DATE_CONT_NUM;
1322         // Previous months' titles are month's name if inside this year,
1323         // month's name and year for previous years.
1324         PRExplodedTime tm;
1325         PR_ExplodeTime(PR_Now(), PR_LocalTimeParameters, &tm);
1326         uint16_t currentYear = tm.tm_year;
1327         // Set day before month, setting month without day could cause issues.
1328         // For example setting month to February when today is 30, since
1329         // February has not 30 days, will return March instead.
1330         // Also, we use day 2 instead of day 1, so that the GMT month is always
1331         // the same as the local month. (Bug 603002)
1332         tm.tm_mday = 2;
1333         tm.tm_month -= MonthIndex;
1334         // Notice we use GMTParameters because we just want to get the first
1335         // day of each month.  Using LocalTimeParameters would instead force us
1336         // to apply a DST correction that we don't really need here.
1337         PR_NormalizeTime(&tm, PR_GMTParameters);
1338         // If the container is for a past year, add the year to its title,
1339         // otherwise just show the month name.
1340         if (tm.tm_year < currentYear) {
1341           nsNavHistory::GetMonthYear(tm, dateName);
1342         } else {
1343           nsNavHistory::GetMonthName(tm, dateName);
1344         }
1345 
1346         // From start of MonthIndex + 1 months ago
1347         sqlFragmentContainerBeginTime = nsLiteralCString(
1348             "(strftime('%s','now','localtime','start of month','-");
1349         sqlFragmentContainerBeginTime.AppendInt(MonthIndex);
1350         sqlFragmentContainerBeginTime.AppendLiteral(" months','utc')*1000000)");
1351         // To start of MonthIndex months ago
1352         sqlFragmentContainerEndTime = nsLiteralCString(
1353             "(strftime('%s','now','localtime','start of month','-");
1354         sqlFragmentContainerEndTime.AppendInt(MonthIndex - 1);
1355         sqlFragmentContainerEndTime.AppendLiteral(" months','utc')*1000000)");
1356         // Search for the same timeframe.
1357         sqlFragmentSearchBeginTime = sqlFragmentContainerBeginTime;
1358         sqlFragmentSearchEndTime = sqlFragmentContainerEndTime;
1359         break;
1360     }
1361 
1362     nsPrintfCString dateParam("dayTitle%d", i);
1363     mAddParams.InsertOrUpdate(dateParam, dateName);
1364 
1365     nsPrintfCString dayRange(
1366         "SELECT :%s AS dayTitle, "
1367         "%s AS beginTime, "
1368         "%s AS endTime "
1369         "WHERE EXISTS ( "
1370         "SELECT id FROM moz_historyvisits "
1371         "WHERE visit_date >= %s "
1372         "AND visit_date < %s "
1373         "AND visit_type NOT IN (0,%d,%d) "
1374         "{QUERY_OPTIONS_VISITS} "
1375         "LIMIT 1 "
1376         ") ",
1377         dateParam.get(), sqlFragmentContainerBeginTime.get(),
1378         sqlFragmentContainerEndTime.get(), sqlFragmentSearchBeginTime.get(),
1379         sqlFragmentSearchEndTime.get(), nsINavHistoryService::TRANSITION_EMBED,
1380         nsINavHistoryService::TRANSITION_FRAMED_LINK);
1381 
1382     mQueryString.Append(dayRange);
1383 
1384     if (i < HISTORY_DATE_CONT_NUM(daysOfHistory))
1385       mQueryString.AppendLiteral(" UNION ALL ");
1386   }
1387 
1388   mQueryString.AppendLiteral(") ");  // TOUTER END
1389 
1390   return NS_OK;
1391 }
1392 
SelectAsSite()1393 nsresult PlacesSQLQueryBuilder::SelectAsSite() {
1394   nsAutoCString localFiles;
1395 
1396   nsNavHistory* history = nsNavHistory::GetHistoryService();
1397   NS_ENSURE_STATE(history);
1398 
1399   history->GetStringFromName("localhost", localFiles);
1400   mAddParams.InsertOrUpdate("localhost"_ns, localFiles);
1401 
1402   // If there are additional conditions the query has to join on visits too.
1403   nsAutoCString visitsJoin;
1404   nsAutoCString additionalConditions;
1405   nsAutoCString timeConstraints;
1406   if (!mConditions.IsEmpty()) {
1407     visitsJoin.AssignLiteral("JOIN moz_historyvisits v ON v.place_id = h.id ");
1408     additionalConditions.AssignLiteral(
1409         "{QUERY_OPTIONS_VISITS} "
1410         "{QUERY_OPTIONS_PLACES} "
1411         "{ADDITIONAL_CONDITIONS} ");
1412     timeConstraints.AssignLiteral(
1413         "||'&beginTime='||:begin_time||"
1414         "'&endTime='||:end_time");
1415   }
1416 
1417   mQueryString = nsPrintfCString(
1418       "SELECT null, 'place:type=%d&sort=%d&domain=&domainIsHost=true'%s, "
1419       ":localhost, :localhost, null, null, null, null, null, null, null, "
1420       "null, null, null "
1421       "WHERE EXISTS ( "
1422       "SELECT h.id FROM moz_places h "
1423       "%s "
1424       "WHERE h.hidden = 0 "
1425       "AND h.visit_count > 0 "
1426       "AND h.url_hash BETWEEN hash('file', 'prefix_lo') AND "
1427       "hash('file', 'prefix_hi') "
1428       "%s "
1429       "LIMIT 1 "
1430       ") "
1431       "UNION ALL "
1432       "SELECT null, "
1433       "'place:type=%d&sort=%d&domain='||host||'&domainIsHost=true'%s, "
1434       "host, host, null, null, null, null, null, null, null, "
1435       "null, null, null "
1436       "FROM ( "
1437       "SELECT get_unreversed_host(h.rev_host) AS host "
1438       "FROM moz_places h "
1439       "%s "
1440       "WHERE h.hidden = 0 "
1441       "AND h.rev_host <> '.' "
1442       "AND h.visit_count > 0 "
1443       "%s "
1444       "GROUP BY h.rev_host "
1445       "ORDER BY host ASC "
1446       ") ",
1447       nsINavHistoryQueryOptions::RESULTS_AS_URI, mSortingMode,
1448       timeConstraints.get(), visitsJoin.get(), additionalConditions.get(),
1449       nsINavHistoryQueryOptions::RESULTS_AS_URI, mSortingMode,
1450       timeConstraints.get(), visitsJoin.get(), additionalConditions.get());
1451 
1452   return NS_OK;
1453 }
1454 
SelectAsTag()1455 nsresult PlacesSQLQueryBuilder::SelectAsTag() {
1456   nsNavHistory* history = nsNavHistory::GetHistoryService();
1457   NS_ENSURE_STATE(history);
1458 
1459   // This allows sorting by date fields what is not possible with
1460   // other history queries.
1461   mHasDateColumns = true;
1462 
1463   // TODO (Bug 1449939): This is likely wrong, since the tag name should
1464   // probably be urlencoded, and we have no util for that in SQL, yet.
1465   // We could encode the tag when the user sets it though.
1466   mQueryString = nsPrintfCString(
1467       "SELECT null, 'place:tag=' || title, "
1468       "title, null, null, null, null, null, dateAdded, "
1469       "lastModified, null, null, null, null, null, null "
1470       "FROM moz_bookmarks "
1471       "WHERE parent = %" PRId64,
1472       history->GetTagsFolder());
1473 
1474   return NS_OK;
1475 }
1476 
SelectAsRoots()1477 nsresult PlacesSQLQueryBuilder::SelectAsRoots() {
1478   nsNavHistory* history = nsNavHistory::GetHistoryService();
1479   NS_ENSURE_STATE(history);
1480 
1481   nsAutoCString toolbarTitle;
1482   nsAutoCString menuTitle;
1483   nsAutoCString unfiledTitle;
1484 
1485   history->GetStringFromName("BookmarksToolbarFolderTitle", toolbarTitle);
1486   mAddParams.InsertOrUpdate("BookmarksToolbarFolderTitle"_ns, toolbarTitle);
1487   history->GetStringFromName("BookmarksMenuFolderTitle", menuTitle);
1488   mAddParams.InsertOrUpdate("BookmarksMenuFolderTitle"_ns, menuTitle);
1489   history->GetStringFromName("OtherBookmarksFolderTitle", unfiledTitle);
1490   mAddParams.InsertOrUpdate("OtherBookmarksFolderTitle"_ns, unfiledTitle);
1491 
1492   nsAutoCString mobileString;
1493 
1494   if (Preferences::GetBool(MOBILE_BOOKMARKS_PREF, false)) {
1495     nsAutoCString mobileTitle;
1496     history->GetStringFromName("MobileBookmarksFolderTitle", mobileTitle);
1497     mAddParams.InsertOrUpdate("MobileBookmarksFolderTitle"_ns, mobileTitle);
1498 
1499     mobileString = nsLiteralCString(
1500         ","
1501         "(null, 'place:parent=" MOBILE_ROOT_GUID
1502         "', :MobileBookmarksFolderTitle, null, null, null, "
1503         "null, null, 0, 0, null, null, null, null, "
1504         "'" MOBILE_BOOKMARKS_VIRTUAL_GUID "', null) ");
1505   }
1506 
1507   mQueryString =
1508       nsLiteralCString(
1509           "SELECT * FROM ("
1510           "VALUES(null, 'place:parent=" TOOLBAR_ROOT_GUID
1511           "', :BookmarksToolbarFolderTitle, null, null, null, "
1512           "null, null, 0, 0, null, null, null, null, 'toolbar____v', null), "
1513           "(null, 'place:parent=" MENU_ROOT_GUID
1514           "', :BookmarksMenuFolderTitle, null, null, null, "
1515           "null, null, 0, 0, null, null, null, null, 'menu_______v', null), "
1516           "(null, 'place:parent=" UNFILED_ROOT_GUID
1517           "', :OtherBookmarksFolderTitle, null, null, null, "
1518           "null, null, 0, 0, null, null, null, null, 'unfiled____v', null) ") +
1519       mobileString + ")"_ns;
1520 
1521   return NS_OK;
1522 }
1523 
SelectAsLeftPane()1524 nsresult PlacesSQLQueryBuilder::SelectAsLeftPane() {
1525   nsNavHistory* history = nsNavHistory::GetHistoryService();
1526   NS_ENSURE_STATE(history);
1527 
1528   nsAutoCString historyTitle;
1529   nsAutoCString downloadsTitle;
1530   nsAutoCString tagsTitle;
1531   nsAutoCString allBookmarksTitle;
1532 
1533   history->GetStringFromName("OrganizerQueryHistory", historyTitle);
1534   mAddParams.InsertOrUpdate("OrganizerQueryHistory"_ns, historyTitle);
1535   history->GetStringFromName("OrganizerQueryDownloads", downloadsTitle);
1536   mAddParams.InsertOrUpdate("OrganizerQueryDownloads"_ns, downloadsTitle);
1537   history->GetStringFromName("TagsFolderTitle", tagsTitle);
1538   mAddParams.InsertOrUpdate("TagsFolderTitle"_ns, tagsTitle);
1539   history->GetStringFromName("OrganizerQueryAllBookmarks", allBookmarksTitle);
1540   mAddParams.InsertOrUpdate("OrganizerQueryAllBookmarks"_ns, allBookmarksTitle);
1541 
1542   mQueryString = nsPrintfCString(
1543       "SELECT * FROM ("
1544       "VALUES"
1545       "(null, 'place:type=%d&sort=%d', :OrganizerQueryHistory, null, null, "
1546       "null, "
1547       "null, null, 0, 0, null, null, null, null, 'history____v', null), "
1548       "(null, 'place:transition=%d&sort=%d', :OrganizerQueryDownloads, null, "
1549       "null, null, "
1550       "null, null, 0, 0, null, null, null, null, 'downloads__v', null), "
1551       "(null, 'place:type=%d&sort=%d', :TagsFolderTitle, null, null, null, "
1552       "null, null, 0, 0, null, null, null, null, 'tags_______v', null), "
1553       "(null, 'place:type=%d', :OrganizerQueryAllBookmarks, null, null, null, "
1554       "null, null, 0, 0, null, null, null, null, 'allbms_____v', null) "
1555       ")",
1556       nsINavHistoryQueryOptions::RESULTS_AS_DATE_QUERY,
1557       nsINavHistoryQueryOptions::SORT_BY_DATE_DESCENDING,
1558       nsINavHistoryService::TRANSITION_DOWNLOAD,
1559       nsINavHistoryQueryOptions::SORT_BY_DATE_DESCENDING,
1560       nsINavHistoryQueryOptions::RESULTS_AS_TAGS_ROOT,
1561       nsINavHistoryQueryOptions::SORT_BY_TITLE_ASCENDING,
1562       nsINavHistoryQueryOptions::RESULTS_AS_ROOTS_QUERY);
1563   return NS_OK;
1564 }
1565 
Where()1566 nsresult PlacesSQLQueryBuilder::Where() {
1567   // Set query options
1568   nsAutoCString additionalVisitsConditions;
1569   nsAutoCString additionalPlacesConditions;
1570 
1571   if (!mIncludeHidden) {
1572     additionalPlacesConditions += "AND hidden = 0 "_ns;
1573   }
1574 
1575   if (mQueryType == nsINavHistoryQueryOptions::QUERY_TYPE_HISTORY) {
1576     // last_visit_date is updated for any kind of visit, so it's a good
1577     // indicator whether the page has visits.
1578     additionalPlacesConditions += "AND last_visit_date NOTNULL "_ns;
1579   }
1580 
1581   if (mResultType == nsINavHistoryQueryOptions::RESULTS_AS_URI &&
1582       !additionalVisitsConditions.IsEmpty()) {
1583     // URI results don't join on visits.
1584     nsAutoCString tmp = additionalVisitsConditions;
1585     additionalVisitsConditions =
1586         "AND EXISTS (SELECT 1 FROM moz_historyvisits WHERE place_id = h.id ";
1587     additionalVisitsConditions.Append(tmp);
1588     additionalVisitsConditions.AppendLiteral("LIMIT 1)");
1589   }
1590 
1591   mQueryString.ReplaceSubstring("{QUERY_OPTIONS_VISITS}",
1592                                 additionalVisitsConditions.get());
1593   mQueryString.ReplaceSubstring("{QUERY_OPTIONS_PLACES}",
1594                                 additionalPlacesConditions.get());
1595 
1596   // If we used WHERE already, we inject the conditions
1597   // in place of {ADDITIONAL_CONDITIONS}
1598   if (mQueryString.Find("{ADDITIONAL_CONDITIONS}") != kNotFound) {
1599     nsAutoCString innerCondition;
1600     // If we have condition AND it
1601     if (!mConditions.IsEmpty()) {
1602       innerCondition = " AND (";
1603       innerCondition += mConditions;
1604       innerCondition += ")";
1605     }
1606     mQueryString.ReplaceSubstring("{ADDITIONAL_CONDITIONS}",
1607                                   innerCondition.get());
1608 
1609   } else if (!mConditions.IsEmpty()) {
1610     mQueryString += "WHERE ";
1611     mQueryString += mConditions;
1612   }
1613   return NS_OK;
1614 }
1615 
GroupBy()1616 nsresult PlacesSQLQueryBuilder::GroupBy() {
1617   mQueryString += mGroupBy;
1618   return NS_OK;
1619 }
1620 
OrderBy()1621 nsresult PlacesSQLQueryBuilder::OrderBy() {
1622   if (mSkipOrderBy) return NS_OK;
1623 
1624   // Sort clause: we will sort later, but if it comes out of the DB sorted,
1625   // our later sort will be basically free. The DB can sort these for free
1626   // most of the time anyway, because it has indices over these items.
1627   switch (mSortingMode) {
1628     case nsINavHistoryQueryOptions::SORT_BY_NONE:
1629       // Ensure sorting does not change based on tables status.
1630       if (mResultType == nsINavHistoryQueryOptions::RESULTS_AS_URI) {
1631         if (mQueryType == nsINavHistoryQueryOptions::QUERY_TYPE_BOOKMARKS)
1632           mQueryString += " ORDER BY b.id ASC "_ns;
1633         else if (mQueryType == nsINavHistoryQueryOptions::QUERY_TYPE_HISTORY)
1634           mQueryString += " ORDER BY h.id ASC "_ns;
1635       }
1636       break;
1637     case nsINavHistoryQueryOptions::SORT_BY_TITLE_ASCENDING:
1638     case nsINavHistoryQueryOptions::SORT_BY_TITLE_DESCENDING:
1639       // If the user wants few results, we limit them by date, necessitating
1640       // a sort by date here (see the IDL definition for maxResults).
1641       // Otherwise we will do actual sorting by title, but since we could need
1642       // to special sort for some locale we will repeat a second sorting at the
1643       // end in nsNavHistoryResult, that should be faster since the list will be
1644       // almost ordered.
1645       if (mMaxResults > 0)
1646         OrderByColumnIndexDesc(nsNavHistory::kGetInfoIndex_VisitDate);
1647       else if (mSortingMode ==
1648                nsINavHistoryQueryOptions::SORT_BY_TITLE_ASCENDING)
1649         OrderByTextColumnIndexAsc(nsNavHistory::kGetInfoIndex_Title);
1650       else
1651         OrderByTextColumnIndexDesc(nsNavHistory::kGetInfoIndex_Title);
1652       break;
1653     case nsINavHistoryQueryOptions::SORT_BY_DATE_ASCENDING:
1654       OrderByColumnIndexAsc(nsNavHistory::kGetInfoIndex_VisitDate);
1655       break;
1656     case nsINavHistoryQueryOptions::SORT_BY_DATE_DESCENDING:
1657       OrderByColumnIndexDesc(nsNavHistory::kGetInfoIndex_VisitDate);
1658       break;
1659     case nsINavHistoryQueryOptions::SORT_BY_URI_ASCENDING:
1660       OrderByColumnIndexAsc(nsNavHistory::kGetInfoIndex_URL);
1661       break;
1662     case nsINavHistoryQueryOptions::SORT_BY_URI_DESCENDING:
1663       OrderByColumnIndexDesc(nsNavHistory::kGetInfoIndex_URL);
1664       break;
1665     case nsINavHistoryQueryOptions::SORT_BY_VISITCOUNT_ASCENDING:
1666       OrderByColumnIndexAsc(nsNavHistory::kGetInfoIndex_VisitCount);
1667       break;
1668     case nsINavHistoryQueryOptions::SORT_BY_VISITCOUNT_DESCENDING:
1669       OrderByColumnIndexDesc(nsNavHistory::kGetInfoIndex_VisitCount);
1670       break;
1671     case nsINavHistoryQueryOptions::SORT_BY_DATEADDED_ASCENDING:
1672       if (mHasDateColumns)
1673         OrderByColumnIndexAsc(nsNavHistory::kGetInfoIndex_ItemDateAdded);
1674       break;
1675     case nsINavHistoryQueryOptions::SORT_BY_DATEADDED_DESCENDING:
1676       if (mHasDateColumns)
1677         OrderByColumnIndexDesc(nsNavHistory::kGetInfoIndex_ItemDateAdded);
1678       break;
1679     case nsINavHistoryQueryOptions::SORT_BY_LASTMODIFIED_ASCENDING:
1680       if (mHasDateColumns)
1681         OrderByColumnIndexAsc(nsNavHistory::kGetInfoIndex_ItemLastModified);
1682       break;
1683     case nsINavHistoryQueryOptions::SORT_BY_LASTMODIFIED_DESCENDING:
1684       if (mHasDateColumns)
1685         OrderByColumnIndexDesc(nsNavHistory::kGetInfoIndex_ItemLastModified);
1686       break;
1687     case nsINavHistoryQueryOptions::SORT_BY_TAGS_ASCENDING:
1688     case nsINavHistoryQueryOptions::SORT_BY_TAGS_DESCENDING:
1689       break;  // Sort later in nsNavHistoryQueryResultNode::FillChildren()
1690     case nsINavHistoryQueryOptions::SORT_BY_FRECENCY_ASCENDING:
1691       OrderByColumnIndexAsc(nsNavHistory::kGetInfoIndex_Frecency);
1692       break;
1693     case nsINavHistoryQueryOptions::SORT_BY_FRECENCY_DESCENDING:
1694       OrderByColumnIndexDesc(nsNavHistory::kGetInfoIndex_Frecency);
1695       break;
1696     default:
1697       MOZ_ASSERT_UNREACHABLE("Invalid sorting mode");
1698   }
1699   return NS_OK;
1700 }
1701 
OrderByColumnIndexAsc(int32_t aIndex)1702 void PlacesSQLQueryBuilder::OrderByColumnIndexAsc(int32_t aIndex) {
1703   mQueryString += nsPrintfCString(" ORDER BY %d ASC", aIndex + 1);
1704 }
1705 
OrderByColumnIndexDesc(int32_t aIndex)1706 void PlacesSQLQueryBuilder::OrderByColumnIndexDesc(int32_t aIndex) {
1707   mQueryString += nsPrintfCString(" ORDER BY %d DESC", aIndex + 1);
1708 }
1709 
OrderByTextColumnIndexAsc(int32_t aIndex)1710 void PlacesSQLQueryBuilder::OrderByTextColumnIndexAsc(int32_t aIndex) {
1711   mQueryString +=
1712       nsPrintfCString(" ORDER BY %d COLLATE NOCASE ASC", aIndex + 1);
1713 }
1714 
OrderByTextColumnIndexDesc(int32_t aIndex)1715 void PlacesSQLQueryBuilder::OrderByTextColumnIndexDesc(int32_t aIndex) {
1716   mQueryString +=
1717       nsPrintfCString(" ORDER BY %d COLLATE NOCASE DESC", aIndex + 1);
1718 }
1719 
Limit()1720 nsresult PlacesSQLQueryBuilder::Limit() {
1721   if (mUseLimit && mMaxResults > 0) {
1722     mQueryString += " LIMIT "_ns;
1723     mQueryString.AppendInt(mMaxResults);
1724     mQueryString.Append(' ');
1725   }
1726   return NS_OK;
1727 }
1728 
ConstructQueryString(const RefPtr<nsNavHistoryQuery> & aQuery,const RefPtr<nsNavHistoryQueryOptions> & aOptions,nsCString & queryString,bool & aParamsPresent,nsNavHistory::StringHash & aAddParams)1729 nsresult nsNavHistory::ConstructQueryString(
1730     const RefPtr<nsNavHistoryQuery>& aQuery,
1731     const RefPtr<nsNavHistoryQueryOptions>& aOptions, nsCString& queryString,
1732     bool& aParamsPresent, nsNavHistory::StringHash& aAddParams) {
1733   // For information about visit_type see nsINavHistoryService.idl.
1734   // visitType == 0 is undefined (see bug #375777 for details).
1735   // Some sites, especially Javascript-heavy ones, load things in frames to
1736   // display them, resulting in a lot of these entries. This is the reason
1737   // why such visits are filtered out.
1738   nsresult rv;
1739   aParamsPresent = false;
1740 
1741   int32_t sortingMode = aOptions->SortingMode();
1742   NS_ASSERTION(
1743       sortingMode >= nsINavHistoryQueryOptions::SORT_BY_NONE &&
1744           sortingMode <= nsINavHistoryQueryOptions::SORT_BY_FRECENCY_DESCENDING,
1745       "Invalid sortingMode found while building query!");
1746 
1747   bool hasSearchTerms = !aQuery->SearchTerms().IsEmpty();
1748 
1749   nsAutoCString tagsSqlFragment;
1750   GetTagsSqlFragment(GetTagsFolder(), "h.id"_ns, hasSearchTerms,
1751                      tagsSqlFragment);
1752 
1753   if (IsOptimizableHistoryQuery(
1754           aQuery, aOptions,
1755           nsINavHistoryQueryOptions::SORT_BY_DATE_DESCENDING) ||
1756       IsOptimizableHistoryQuery(
1757           aQuery, aOptions,
1758           nsINavHistoryQueryOptions::SORT_BY_VISITCOUNT_DESCENDING)) {
1759     // Generate an optimized query for the history menu and the old most visited
1760     // bookmark that was inserted into profiles.
1761     queryString =
1762         nsLiteralCString(
1763             "SELECT h.id, h.url, h.title AS page_title, h.rev_host, "
1764             "h.visit_count, h.last_visit_date, "
1765             "null, null, null, null, null, ") +
1766         tagsSqlFragment +
1767         nsLiteralCString(
1768             ", h.frecency, h.hidden, h.guid, "
1769             "null, null, null "
1770             "FROM moz_places h "
1771             "WHERE h.hidden = 0 "
1772             "AND EXISTS (SELECT id FROM moz_historyvisits WHERE place_id = "
1773             "h.id "
1774             "AND visit_type NOT IN ") +
1775         nsPrintfCString("(0,%d,%d) ", nsINavHistoryService::TRANSITION_EMBED,
1776                         nsINavHistoryService::TRANSITION_FRAMED_LINK) +
1777         nsLiteralCString(
1778             "LIMIT 1) "
1779             "{QUERY_OPTIONS} ");
1780 
1781     queryString.AppendLiteral("ORDER BY ");
1782     if (sortingMode == nsINavHistoryQueryOptions::SORT_BY_DATE_DESCENDING)
1783       queryString.AppendLiteral("last_visit_date DESC ");
1784     else
1785       queryString.AppendLiteral("visit_count DESC ");
1786 
1787     queryString.AppendLiteral("LIMIT ");
1788     queryString.AppendInt(aOptions->MaxResults());
1789 
1790     nsAutoCString additionalQueryOptions;
1791 
1792     queryString.ReplaceSubstring("{QUERY_OPTIONS}",
1793                                  additionalQueryOptions.get());
1794     return NS_OK;
1795   }
1796 
1797   // If the query is a tag query, the type is bookmarks.
1798   if (!aQuery->Tags().IsEmpty()) {
1799     aOptions->SetQueryType(nsNavHistoryQueryOptions::QUERY_TYPE_BOOKMARKS);
1800   }
1801 
1802   nsAutoCString conditions;
1803   nsCString queryClause;
1804   rv = QueryToSelectClause(aQuery, aOptions, &queryClause);
1805   NS_ENSURE_SUCCESS(rv, rv);
1806   if (!queryClause.IsEmpty()) {
1807     // TODO: This should be set on a case basis, not blindly.
1808     aParamsPresent = true;
1809     conditions += queryClause;
1810   }
1811 
1812   // Determine whether we can push maxResults constraints into the query
1813   // as LIMIT, or if we need to do result count clamping later
1814   // using FilterResultSet()
1815   bool useLimitClause = !NeedToFilterResultSet(aQuery, aOptions);
1816 
1817   PlacesSQLQueryBuilder queryStringBuilder(
1818       conditions, aQuery, aOptions, useLimitClause, aAddParams, hasSearchTerms);
1819   rv = queryStringBuilder.GetQueryString(queryString);
1820   NS_ENSURE_SUCCESS(rv, rv);
1821 
1822   return NS_OK;
1823 }
1824 
1825 // nsNavHistory::GetQueryResults
1826 //
1827 //    Call this to get the results from a complex query. This is used by
1828 //    nsNavHistoryQueryResultNode to populate its children. For simple bookmark
1829 //    queries, use nsNavBookmarks::QueryFolderChildren.
1830 //
1831 //    THIS DOES NOT DO SORTING. You will need to sort the container yourself
1832 //    when you get the results. This is because sorting depends on tree
1833 //    statistics that will be built from the perspective of the tree. See
1834 //    nsNavHistoryQueryResultNode::FillChildren
1835 //
1836 //    FIXME: This only does keyword searching for the first query, and does
1837 //    it ANDed with the all the rest of the queries.
1838 
GetQueryResults(nsNavHistoryQueryResultNode * aResultNode,const RefPtr<nsNavHistoryQuery> & aQuery,const RefPtr<nsNavHistoryQueryOptions> & aOptions,nsCOMArray<nsNavHistoryResultNode> * aResults)1839 nsresult nsNavHistory::GetQueryResults(
1840     nsNavHistoryQueryResultNode* aResultNode,
1841     const RefPtr<nsNavHistoryQuery>& aQuery,
1842     const RefPtr<nsNavHistoryQueryOptions>& aOptions,
1843     nsCOMArray<nsNavHistoryResultNode>* aResults) {
1844   NS_ENSURE_ARG_POINTER(aQuery);
1845   NS_ENSURE_ARG_POINTER(aOptions);
1846   NS_ASSERTION(aResults->Count() == 0, "Initial result array must be empty");
1847 
1848   nsCString queryString;
1849   bool paramsPresent = false;
1850   nsNavHistory::StringHash addParams(HISTORY_DATE_CONT_LENGTH);
1851   nsresult rv = ConstructQueryString(aQuery, aOptions, queryString,
1852                                      paramsPresent, addParams);
1853   NS_ENSURE_SUCCESS(rv, rv);
1854 
1855   // create statement
1856   nsCOMPtr<mozIStorageStatement> statement = mDB->GetStatement(queryString);
1857 #ifdef DEBUG
1858   if (!statement) {
1859     nsCOMPtr<mozIStorageConnection> conn = mDB->MainConn();
1860     if (conn) {
1861       nsAutoCString lastErrorString;
1862       (void)conn->GetLastErrorString(lastErrorString);
1863       int32_t lastError = 0;
1864       (void)conn->GetLastError(&lastError);
1865       printf(
1866           "Places failed to create a statement from this query:\n%s\nStorage "
1867           "error (%d): %s\n",
1868           queryString.get(), lastError, lastErrorString.get());
1869     }
1870   }
1871 #endif
1872   NS_ENSURE_STATE(statement);
1873   mozStorageStatementScoper scoper(statement);
1874 
1875   if (paramsPresent) {
1876     rv = BindQueryClauseParameters(statement, aQuery, aOptions);
1877     NS_ENSURE_SUCCESS(rv, rv);
1878   }
1879 
1880   for (const auto& entry : addParams) {
1881     nsresult rv =
1882         statement->BindUTF8StringByName(entry.GetKey(), entry.GetData());
1883     if (NS_FAILED(rv)) {
1884       break;
1885     }
1886   }
1887 
1888   // Optimize the case where there is no need for any post-query filtering.
1889   if (NeedToFilterResultSet(aQuery, aOptions)) {
1890     // Generate the top-level results.
1891     nsCOMArray<nsNavHistoryResultNode> toplevel;
1892     rv = ResultsAsList(statement, aOptions, &toplevel);
1893     NS_ENSURE_SUCCESS(rv, rv);
1894 
1895     FilterResultSet(aResultNode, toplevel, aResults, aQuery, aOptions);
1896   } else {
1897     rv = ResultsAsList(statement, aOptions, aResults);
1898     NS_ENSURE_SUCCESS(rv, rv);
1899   }
1900 
1901   return NS_OK;
1902 }
1903 
1904 NS_IMETHODIMP
GetHistoryDisabled(bool * _retval)1905 nsNavHistory::GetHistoryDisabled(bool* _retval) {
1906   NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
1907   NS_ENSURE_ARG_POINTER(_retval);
1908 
1909   *_retval = IsHistoryDisabled();
1910   return NS_OK;
1911 }
1912 
1913 // Call this method before visiting a URL in order to help determine the
1914 // transition type of the visit.
1915 //
1916 // @see MarkPageAsFollowedBookmark
1917 
1918 NS_IMETHODIMP
MarkPageAsTyped(nsIURI * aURI)1919 nsNavHistory::MarkPageAsTyped(nsIURI* aURI) {
1920   NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
1921   NS_ENSURE_ARG(aURI);
1922 
1923   // don't add when history is disabled
1924   if (IsHistoryDisabled()) return NS_OK;
1925 
1926   nsAutoCString uriString;
1927   nsresult rv = aURI->GetSpec(uriString);
1928   NS_ENSURE_SUCCESS(rv, rv);
1929 
1930   mRecentTyped.InsertOrUpdate(uriString, GetNow());
1931 
1932   if (mRecentTyped.Count() > RECENT_EVENT_QUEUE_MAX_LENGTH)
1933     ExpireNonrecentEvents(&mRecentTyped);
1934 
1935   return NS_OK;
1936 }
1937 
1938 // Call this method before visiting a URL in order to help determine the
1939 // transition type of the visit.
1940 //
1941 // @see MarkPageAsTyped
1942 
1943 NS_IMETHODIMP
MarkPageAsFollowedLink(nsIURI * aURI)1944 nsNavHistory::MarkPageAsFollowedLink(nsIURI* aURI) {
1945   NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
1946   NS_ENSURE_ARG(aURI);
1947 
1948   // don't add when history is disabled
1949   if (IsHistoryDisabled()) return NS_OK;
1950 
1951   nsAutoCString uriString;
1952   nsresult rv = aURI->GetSpec(uriString);
1953   NS_ENSURE_SUCCESS(rv, rv);
1954 
1955   mRecentLink.InsertOrUpdate(uriString, GetNow());
1956 
1957   if (mRecentLink.Count() > RECENT_EVENT_QUEUE_MAX_LENGTH)
1958     ExpireNonrecentEvents(&mRecentLink);
1959 
1960   return NS_OK;
1961 }
1962 
1963 ////////////////////////////////////////////////////////////////////////////////
1964 //// mozIStorageVacuumParticipant
1965 
1966 NS_IMETHODIMP
GetDatabaseConnection(mozIStorageConnection ** _DBConnection)1967 nsNavHistory::GetDatabaseConnection(mozIStorageConnection** _DBConnection) {
1968   return GetDBConnection(_DBConnection);
1969 }
1970 
1971 NS_IMETHODIMP
GetExpectedDatabasePageSize(int32_t * _expectedPageSize)1972 nsNavHistory::GetExpectedDatabasePageSize(int32_t* _expectedPageSize) {
1973   NS_ENSURE_STATE(mDB);
1974   NS_ENSURE_STATE(mDB->MainConn());
1975   return mDB->MainConn()->GetDefaultPageSize(_expectedPageSize);
1976 }
1977 
1978 NS_IMETHODIMP
OnBeginVacuum(bool * _vacuumGranted)1979 nsNavHistory::OnBeginVacuum(bool* _vacuumGranted) {
1980   // TODO: Check if we have to deny the vacuum in some heavy-load case.
1981   // We could maybe want to do that during batches?
1982   *_vacuumGranted = true;
1983   return NS_OK;
1984 }
1985 
1986 NS_IMETHODIMP
OnEndVacuum(bool aSucceeded)1987 nsNavHistory::OnEndVacuum(bool aSucceeded) {
1988   NS_WARNING_ASSERTION(aSucceeded, "Places.sqlite vacuum failed.");
1989   return NS_OK;
1990 }
1991 
1992 NS_IMETHODIMP
GetDBConnection(mozIStorageConnection ** _DBConnection)1993 nsNavHistory::GetDBConnection(mozIStorageConnection** _DBConnection) {
1994   NS_ENSURE_ARG_POINTER(_DBConnection);
1995   nsCOMPtr<mozIStorageConnection> connection = mDB->MainConn();
1996   connection.forget(_DBConnection);
1997 
1998   return NS_OK;
1999 }
2000 
2001 NS_IMETHODIMP
GetShutdownClient(nsIAsyncShutdownClient ** _shutdownClient)2002 nsNavHistory::GetShutdownClient(nsIAsyncShutdownClient** _shutdownClient) {
2003   NS_ENSURE_ARG_POINTER(_shutdownClient);
2004   nsCOMPtr<nsIAsyncShutdownClient> client = mDB->GetClientsShutdown();
2005   if (!client) {
2006     return NS_ERROR_UNEXPECTED;
2007   }
2008   client.forget(_shutdownClient);
2009   return NS_OK;
2010 }
2011 
2012 NS_IMETHODIMP
GetConnectionShutdownClient(nsIAsyncShutdownClient ** _shutdownClient)2013 nsNavHistory::GetConnectionShutdownClient(
2014     nsIAsyncShutdownClient** _shutdownClient) {
2015   NS_ENSURE_ARG_POINTER(_shutdownClient);
2016   nsCOMPtr<nsIAsyncShutdownClient> client = mDB->GetConnectionShutdown();
2017   if (!client) {
2018     return NS_ERROR_UNEXPECTED;
2019   }
2020   client.forget(_shutdownClient);
2021   return NS_OK;
2022 }
2023 
2024 NS_IMETHODIMP
AsyncExecuteLegacyQuery(nsINavHistoryQuery * aQuery,nsINavHistoryQueryOptions * aOptions,mozIStorageStatementCallback * aCallback,mozIStoragePendingStatement ** _stmt)2025 nsNavHistory::AsyncExecuteLegacyQuery(nsINavHistoryQuery* aQuery,
2026                                       nsINavHistoryQueryOptions* aOptions,
2027                                       mozIStorageStatementCallback* aCallback,
2028                                       mozIStoragePendingStatement** _stmt) {
2029   NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
2030   NS_ENSURE_ARG(aQuery);
2031   NS_ENSURE_ARG(aOptions);
2032   NS_ENSURE_ARG(aCallback);
2033   NS_ENSURE_ARG_POINTER(_stmt);
2034 
2035   RefPtr<nsNavHistoryQuery> query = do_QueryObject(aQuery);
2036   NS_ENSURE_STATE(query);
2037   RefPtr<nsNavHistoryQueryOptions> options = do_QueryObject(aOptions);
2038   NS_ENSURE_ARG(options);
2039 
2040   nsCString queryString;
2041   bool paramsPresent = false;
2042   nsNavHistory::StringHash addParams(HISTORY_DATE_CONT_LENGTH);
2043   nsresult rv = ConstructQueryString(query, options, queryString, paramsPresent,
2044                                      addParams);
2045   NS_ENSURE_SUCCESS(rv, rv);
2046 
2047   nsCOMPtr<mozIStorageAsyncStatement> statement =
2048       mDB->GetAsyncStatement(queryString);
2049   NS_ENSURE_STATE(statement);
2050 
2051 #ifdef DEBUG
2052   if (NS_FAILED(rv)) {
2053     nsCOMPtr<mozIStorageConnection> conn = mDB->MainConn();
2054     if (conn) {
2055       nsAutoCString lastErrorString;
2056       (void)mDB->MainConn()->GetLastErrorString(lastErrorString);
2057       int32_t lastError = 0;
2058       (void)mDB->MainConn()->GetLastError(&lastError);
2059       printf(
2060           "Places failed to create a statement from this query:\n%s\nStorage "
2061           "error (%d): %s\n",
2062           queryString.get(), lastError, lastErrorString.get());
2063     }
2064   }
2065 #endif
2066   NS_ENSURE_SUCCESS(rv, rv);
2067 
2068   if (paramsPresent) {
2069     rv = BindQueryClauseParameters(statement, query, options);
2070     NS_ENSURE_SUCCESS(rv, rv);
2071   }
2072 
2073   for (const auto& entry : addParams) {
2074     nsresult rv =
2075         statement->BindUTF8StringByName(entry.GetKey(), entry.GetData());
2076     if (NS_FAILED(rv)) {
2077       break;
2078     }
2079   }
2080 
2081   rv = statement->ExecuteAsync(aCallback, _stmt);
2082   NS_ENSURE_SUCCESS(rv, rv);
2083 
2084   return NS_OK;
2085 }
2086 
2087 ////////////////////////////////////////////////////////////////////////////////
2088 //// nsIObserver
2089 
2090 NS_IMETHODIMP
Observe(nsISupports * aSubject,const char * aTopic,const char16_t * aData)2091 nsNavHistory::Observe(nsISupports* aSubject, const char* aTopic,
2092                       const char16_t* aData) {
2093   NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
2094   if (strcmp(aTopic, TOPIC_PROFILE_TEARDOWN) == 0 ||
2095       strcmp(aTopic, TOPIC_PROFILE_CHANGE) == 0 ||
2096       strcmp(aTopic, TOPIC_SIMULATE_PLACES_SHUTDOWN) == 0) {
2097     // These notifications are used by tests to simulate a Places shutdown.
2098     // They should just be forwarded to the Database handle.
2099     mDB->Observe(aSubject, aTopic, aData);
2100   }
2101 
2102   else if (strcmp(aTopic, TOPIC_PREF_CHANGED) == 0) {
2103     LoadPrefs();
2104   }
2105 
2106   else if (strcmp(aTopic, TOPIC_IDLE_DAILY) == 0) {
2107     (void)DecayFrecency();
2108   }
2109 
2110   return NS_OK;
2111 }
2112 
2113 NS_IMETHODIMP
DecayFrecency()2114 nsNavHistory::DecayFrecency() {
2115   float decayRate =
2116       Preferences::GetFloat(PREF_FREC_DECAY_RATE, PREF_FREC_DECAY_RATE_DEF);
2117   if (decayRate > 1.0f) {
2118     MOZ_ASSERT(false, "The frecency decay rate should not be greater than 1.0");
2119     decayRate = PREF_FREC_DECAY_RATE_DEF;
2120   }
2121 
2122   RefPtr<FixAndDecayFrecencyRunnable> runnable =
2123       new FixAndDecayFrecencyRunnable(mDB, decayRate);
2124   nsCOMPtr<nsIEventTarget> target = do_GetInterface(mDB->MainConn());
2125   NS_ENSURE_STATE(target);
2126 
2127   mDecayFrecencyPendingCount++;
2128   return target->Dispatch(runnable, NS_DISPATCH_NORMAL);
2129 }
2130 
DecayFrecencyCompleted()2131 void nsNavHistory::DecayFrecencyCompleted() {
2132   MOZ_ASSERT(mDecayFrecencyPendingCount > 0);
2133   mDecayFrecencyPendingCount--;
2134 }
2135 
IsFrecencyDecaying() const2136 bool nsNavHistory::IsFrecencyDecaying() const {
2137   return mDecayFrecencyPendingCount > 0;
2138 }
2139 
2140 // Query stuff *****************************************************************
2141 
2142 // Helper class for QueryToSelectClause
2143 //
2144 // This class helps to build part of the WHERE clause.
2145 
2146 class ConditionBuilder {
2147  public:
Condition(const char * aStr)2148   ConditionBuilder& Condition(const char* aStr) {
2149     if (!mClause.IsEmpty()) mClause.AppendLiteral(" AND ");
2150     Str(aStr);
2151     return *this;
2152   }
2153 
Str(const char * aStr)2154   ConditionBuilder& Str(const char* aStr) {
2155     mClause.Append(' ');
2156     mClause.Append(aStr);
2157     mClause.Append(' ');
2158     return *this;
2159   }
2160 
Param(const char * aParam)2161   ConditionBuilder& Param(const char* aParam) {
2162     mClause.Append(' ');
2163     mClause.Append(aParam);
2164     mClause.Append(' ');
2165     return *this;
2166   }
2167 
GetClauseString(nsCString & aResult)2168   void GetClauseString(nsCString& aResult) { aResult = mClause; }
2169 
2170  private:
2171   nsCString mClause;
2172 };
2173 
2174 // nsNavHistory::QueryToSelectClause
2175 //
2176 //    THE BEHAVIOR SHOULD BE IN SYNC WITH BindQueryClauseParameters
2177 //
2178 //    I don't check return values from the query object getters because there's
2179 //    no way for those to fail.
2180 
QueryToSelectClause(const RefPtr<nsNavHistoryQuery> & aQuery,const RefPtr<nsNavHistoryQueryOptions> & aOptions,nsCString * aClause)2181 nsresult nsNavHistory::QueryToSelectClause(
2182     const RefPtr<nsNavHistoryQuery>& aQuery,
2183     const RefPtr<nsNavHistoryQueryOptions>& aOptions, nsCString* aClause) {
2184   bool hasIt;
2185   // We don't use the value from options here - we post filter if that
2186   // is set.
2187   bool excludeQueries = false;
2188 
2189   ConditionBuilder clause;
2190 
2191   if ((NS_SUCCEEDED(aQuery->GetHasBeginTime(&hasIt)) && hasIt) ||
2192       (NS_SUCCEEDED(aQuery->GetHasEndTime(&hasIt)) && hasIt)) {
2193     clause.Condition(
2194         "EXISTS (SELECT 1 FROM moz_historyvisits "
2195         "WHERE place_id = h.id");
2196     // begin time
2197     if (NS_SUCCEEDED(aQuery->GetHasBeginTime(&hasIt)) && hasIt)
2198       clause.Condition("visit_date >=").Param(":begin_time");
2199     // end time
2200     if (NS_SUCCEEDED(aQuery->GetHasEndTime(&hasIt)) && hasIt)
2201       clause.Condition("visit_date <=").Param(":end_time");
2202     clause.Str(" LIMIT 1)");
2203   }
2204 
2205   // search terms
2206   int32_t searchBehavior = mozIPlacesAutoComplete::BEHAVIOR_HISTORY |
2207                            mozIPlacesAutoComplete::BEHAVIOR_BOOKMARK;
2208   if (!aQuery->SearchTerms().IsEmpty()) {
2209     // Re-use the autocomplete_match function.  Setting the behavior to match
2210     // history or typed history or bookmarks or open pages will match almost
2211     // everything.
2212     clause.Condition("AUTOCOMPLETE_MATCH(")
2213         .Param(":search_string")
2214         .Str(", h.url, page_title, tags, ")
2215         .Str(nsPrintfCString("1, 1, 1, 1, %d, %d",
2216                              mozIPlacesAutoComplete::MATCH_ANYWHERE_UNMODIFIED,
2217                              searchBehavior)
2218                  .get())
2219         .Str(", NULL)");
2220     // Serching by terms implicitly exclude queries.
2221     excludeQueries = true;
2222   }
2223 
2224   // min and max visit count
2225   if (aQuery->MinVisits() >= 0)
2226     clause.Condition("h.visit_count >=").Param(":min_visits");
2227 
2228   if (aQuery->MaxVisits() >= 0)
2229     clause.Condition("h.visit_count <=").Param(":max_visits");
2230 
2231   // only bookmarked, has no affect on bookmarks-only queries
2232   if (aOptions->QueryType() !=
2233           nsINavHistoryQueryOptions::QUERY_TYPE_BOOKMARKS &&
2234       aQuery->OnlyBookmarked())
2235     clause.Condition("EXISTS (SELECT b.fk FROM moz_bookmarks b WHERE b.type = ")
2236         .Str(nsPrintfCString("%d", nsNavBookmarks::TYPE_BOOKMARK).get())
2237         .Str("AND b.fk = h.id)");
2238 
2239   // domain
2240   if (!aQuery->Domain().IsVoid()) {
2241     bool domainIsHost = false;
2242     aQuery->GetDomainIsHost(&domainIsHost);
2243     if (domainIsHost)
2244       clause.Condition("h.rev_host =").Param(":domain_lower");
2245     else
2246       // see domain setting in BindQueryClauseParameters for why we do this
2247       clause.Condition("h.rev_host >=")
2248           .Param(":domain_lower")
2249           .Condition("h.rev_host <")
2250           .Param(":domain_upper");
2251   }
2252 
2253   // URI
2254   if (aQuery->Uri()) {
2255     clause.Condition("h.url_hash = hash(")
2256         .Param(":uri")
2257         .Str(")")
2258         .Condition("h.url =")
2259         .Param(":uri");
2260   }
2261 
2262   // annotation
2263   if (!aQuery->Annotation().IsEmpty()) {
2264     clause.Condition("");
2265     if (aQuery->AnnotationIsNot()) clause.Str("NOT");
2266     clause
2267         .Str(
2268             "EXISTS "
2269             "(SELECT h.id "
2270             "FROM moz_annos anno "
2271             "JOIN moz_anno_attributes annoname "
2272             "ON anno.anno_attribute_id = annoname.id "
2273             "WHERE anno.place_id = h.id "
2274             "AND annoname.name = ")
2275         .Param(":anno")
2276         .Str(")");
2277     // annotation-based queries don't get the common conditions, so you get
2278     // all URLs with that annotation
2279   }
2280 
2281   // tags
2282   const nsTArray<nsString>& tags = aQuery->Tags();
2283   if (tags.Length() > 0) {
2284     clause.Condition("h.id");
2285     if (aQuery->TagsAreNot()) clause.Str("NOT");
2286     clause
2287         .Str(
2288             "IN "
2289             "(SELECT bms.fk "
2290             "FROM moz_bookmarks bms "
2291             "JOIN moz_bookmarks tags ON bms.parent = tags.id "
2292             "WHERE tags.parent =")
2293         .Param(":tags_folder")
2294         .Str("AND lower(tags.title) IN (");
2295     for (uint32_t i = 0; i < tags.Length(); ++i) {
2296       nsPrintfCString param(":tag%d_", i);
2297       clause.Param(param.get());
2298       if (i < tags.Length() - 1) clause.Str(",");
2299     }
2300     clause.Str(")");
2301     if (!aQuery->TagsAreNot()) {
2302       clause.Str("GROUP BY bms.fk HAVING count(*) >=").Param(":tag_count");
2303     }
2304     clause.Str(")");
2305   }
2306 
2307   // transitions
2308   const nsTArray<uint32_t>& transitions = aQuery->Transitions();
2309   for (uint32_t i = 0; i < transitions.Length(); ++i) {
2310     nsPrintfCString param(":transition%d_", i);
2311     clause
2312         .Condition(
2313             "h.id IN (SELECT place_id FROM moz_historyvisits "
2314             "WHERE visit_type = ")
2315         .Param(param.get())
2316         .Str(")");
2317   }
2318 
2319   // parents
2320   const nsTArray<nsCString>& parents = aQuery->Parents();
2321   if (parents.Length() > 0) {
2322     aOptions->SetQueryType(nsNavHistoryQueryOptions::QUERY_TYPE_BOOKMARKS);
2323     clause.Condition(
2324         "b.parent IN( "
2325         "WITH RECURSIVE parents(id) AS ( "
2326         "SELECT id FROM moz_bookmarks WHERE GUID IN (");
2327 
2328     for (uint32_t i = 0; i < parents.Length(); ++i) {
2329       nsPrintfCString param(":parentguid%d_", i);
2330       clause.Param(param.get());
2331       if (i < parents.Length() - 1) {
2332         clause.Str(",");
2333       }
2334     }
2335     clause.Str(
2336         ") "
2337         "UNION ALL "
2338         "SELECT b2.id "
2339         "FROM moz_bookmarks b2 "
2340         "JOIN parents p ON b2.parent = p.id "
2341         "WHERE b2.type = 2 "
2342         ") "
2343         "SELECT id FROM parents "
2344         ")");
2345   }
2346 
2347   if (excludeQueries) {
2348     // Serching by terms implicitly exclude queries and folder shortcuts.
2349     clause.Condition(
2350         "NOT h.url_hash BETWEEN hash('place', 'prefix_lo') AND "
2351         "hash('place', 'prefix_hi')");
2352   }
2353 
2354   clause.GetClauseString(*aClause);
2355   return NS_OK;
2356 }
2357 
2358 // nsNavHistory::BindQueryClauseParameters
2359 //
2360 //    THE BEHAVIOR SHOULD BE IN SYNC WITH QueryToSelectClause
2361 
BindQueryClauseParameters(mozIStorageBaseStatement * statement,const RefPtr<nsNavHistoryQuery> & aQuery,const RefPtr<nsNavHistoryQueryOptions> & aOptions)2362 nsresult nsNavHistory::BindQueryClauseParameters(
2363     mozIStorageBaseStatement* statement,
2364     const RefPtr<nsNavHistoryQuery>& aQuery,
2365     const RefPtr<nsNavHistoryQueryOptions>& aOptions) {
2366   nsresult rv;
2367 
2368   bool hasIt;
2369   // begin time
2370   if (NS_SUCCEEDED(aQuery->GetHasBeginTime(&hasIt)) && hasIt) {
2371     PRTime time =
2372         NormalizeTime(aQuery->BeginTimeReference(), aQuery->BeginTime());
2373     rv = statement->BindInt64ByName("begin_time"_ns, time);
2374     NS_ENSURE_SUCCESS(rv, rv);
2375   }
2376 
2377   // end time
2378   if (NS_SUCCEEDED(aQuery->GetHasEndTime(&hasIt)) && hasIt) {
2379     PRTime time = NormalizeTime(aQuery->EndTimeReference(), aQuery->EndTime());
2380     rv = statement->BindInt64ByName("end_time"_ns, time);
2381     NS_ENSURE_SUCCESS(rv, rv);
2382   }
2383 
2384   // search terms
2385   if (!aQuery->SearchTerms().IsEmpty()) {
2386     rv = statement->BindStringByName("search_string"_ns, aQuery->SearchTerms());
2387     NS_ENSURE_SUCCESS(rv, rv);
2388   }
2389 
2390   // min and max visit count
2391   int32_t visits = aQuery->MinVisits();
2392   if (visits >= 0) {
2393     rv = statement->BindInt32ByName("min_visits"_ns, visits);
2394     NS_ENSURE_SUCCESS(rv, rv);
2395   }
2396 
2397   visits = aQuery->MaxVisits();
2398   if (visits >= 0) {
2399     rv = statement->BindInt32ByName("max_visits"_ns, visits);
2400     NS_ENSURE_SUCCESS(rv, rv);
2401   }
2402 
2403   // domain (see GetReversedHostname for more info on reversed host names)
2404   if (!aQuery->Domain().IsVoid()) {
2405     nsString revDomain;
2406     GetReversedHostname(NS_ConvertUTF8toUTF16(aQuery->Domain()), revDomain);
2407 
2408     if (aQuery->DomainIsHost()) {
2409       rv = statement->BindStringByName("domain_lower"_ns, revDomain);
2410       NS_ENSURE_SUCCESS(rv, rv);
2411     } else {
2412       // for "mozilla.org" do query >= "gro.allizom." AND < "gro.allizom/"
2413       // which will get everything starting with "gro.allizom." while using the
2414       // index (using SUBSTRING() causes indexes to be discarded).
2415       NS_ASSERTION(revDomain[revDomain.Length() - 1] == '.',
2416                    "Invalid rev. host");
2417       rv = statement->BindStringByName("domain_lower"_ns, revDomain);
2418       NS_ENSURE_SUCCESS(rv, rv);
2419       revDomain.Truncate(revDomain.Length() - 1);
2420       revDomain.Append(char16_t('/'));
2421       rv = statement->BindStringByName("domain_upper"_ns, revDomain);
2422       NS_ENSURE_SUCCESS(rv, rv);
2423     }
2424   }
2425 
2426   // URI
2427   if (aQuery->Uri()) {
2428     rv = URIBinder::Bind(statement, "uri"_ns, aQuery->Uri());
2429     NS_ENSURE_SUCCESS(rv, rv);
2430   }
2431 
2432   // annotation
2433   if (!aQuery->Annotation().IsEmpty()) {
2434     rv = statement->BindUTF8StringByName("anno"_ns, aQuery->Annotation());
2435     NS_ENSURE_SUCCESS(rv, rv);
2436   }
2437 
2438   // tags
2439   const nsTArray<nsString>& tags = aQuery->Tags();
2440   if (tags.Length() > 0) {
2441     for (uint32_t i = 0; i < tags.Length(); ++i) {
2442       nsPrintfCString paramName("tag%d_", i);
2443       nsString utf16Tag = tags[i];
2444       ToLowerCase(utf16Tag);
2445       NS_ConvertUTF16toUTF8 tag(utf16Tag);
2446       rv = statement->BindUTF8StringByName(paramName, tag);
2447       NS_ENSURE_SUCCESS(rv, rv);
2448     }
2449     int64_t tagsFolder = GetTagsFolder();
2450     rv = statement->BindInt64ByName("tags_folder"_ns, tagsFolder);
2451     NS_ENSURE_SUCCESS(rv, rv);
2452     if (!aQuery->TagsAreNot()) {
2453       rv = statement->BindInt32ByName("tag_count"_ns, tags.Length());
2454       NS_ENSURE_SUCCESS(rv, rv);
2455     }
2456   }
2457 
2458   // transitions
2459   const nsTArray<uint32_t>& transitions = aQuery->Transitions();
2460   for (uint32_t i = 0; i < transitions.Length(); ++i) {
2461     nsPrintfCString paramName("transition%d_", i);
2462     rv = statement->BindInt64ByName(paramName, transitions[i]);
2463     NS_ENSURE_SUCCESS(rv, rv);
2464   }
2465 
2466   // parents
2467   const nsTArray<nsCString>& parents = aQuery->Parents();
2468   for (uint32_t i = 0; i < parents.Length(); ++i) {
2469     nsPrintfCString paramName("parentguid%d_", i);
2470     rv = statement->BindUTF8StringByName(paramName, parents[i]);
2471     NS_ENSURE_SUCCESS(rv, rv);
2472   }
2473 
2474   return NS_OK;
2475 }
2476 
2477 // nsNavHistory::ResultsAsList
2478 //
2479 
ResultsAsList(mozIStorageStatement * statement,nsNavHistoryQueryOptions * aOptions,nsCOMArray<nsNavHistoryResultNode> * aResults)2480 nsresult nsNavHistory::ResultsAsList(
2481     mozIStorageStatement* statement, nsNavHistoryQueryOptions* aOptions,
2482     nsCOMArray<nsNavHistoryResultNode>* aResults) {
2483   nsresult rv;
2484   nsCOMPtr<mozIStorageValueArray> row = do_QueryInterface(statement, &rv);
2485   NS_ENSURE_SUCCESS(rv, rv);
2486 
2487   bool hasMore = false;
2488   while (NS_SUCCEEDED(statement->ExecuteStep(&hasMore)) && hasMore) {
2489     RefPtr<nsNavHistoryResultNode> result;
2490     rv = RowToResult(row, aOptions, getter_AddRefs(result));
2491     NS_ENSURE_SUCCESS(rv, rv);
2492     aResults->AppendElement(result.forget());
2493   }
2494   return NS_OK;
2495 }
2496 
2497 const int64_t UNDEFINED_URN_VALUE = -1;
2498 
2499 // Create a urn (like
2500 // urn:places-persist:place:group=0&group=1&sort=1&type=1,,%28local%20files%29)
2501 // to be used to persist the open state of this container
CreatePlacesPersistURN(nsNavHistoryQueryResultNode * aResultNode,int64_t aValue,const nsCString & aTitle,nsCString & aURN)2502 nsresult CreatePlacesPersistURN(nsNavHistoryQueryResultNode* aResultNode,
2503                                 int64_t aValue, const nsCString& aTitle,
2504                                 nsCString& aURN) {
2505   nsAutoCString uri;
2506   nsresult rv = aResultNode->GetUri(uri);
2507   NS_ENSURE_SUCCESS(rv, rv);
2508 
2509   aURN.AssignLiteral("urn:places-persist:");
2510   aURN.Append(uri);
2511 
2512   aURN.Append(',');
2513   if (aValue != UNDEFINED_URN_VALUE) aURN.AppendInt(aValue);
2514 
2515   aURN.Append(',');
2516   if (!aTitle.IsEmpty()) {
2517     nsAutoCString escapedTitle;
2518     bool success = NS_Escape(aTitle, escapedTitle, url_XAlphas);
2519     NS_ENSURE_TRUE(success, NS_ERROR_OUT_OF_MEMORY);
2520     aURN.Append(escapedTitle);
2521   }
2522 
2523   return NS_OK;
2524 }
2525 
GetTagsFolder()2526 int64_t nsNavHistory::GetTagsFolder() {
2527   // cache our tags folder
2528   // note, we can't do this in nsNavHistory::Init(),
2529   // as getting the bookmarks service would initialize it.
2530   if (mTagsFolder == -1) {
2531     nsNavBookmarks* bookmarks = nsNavBookmarks::GetBookmarksService();
2532     NS_ENSURE_TRUE(bookmarks, -1);
2533 
2534     nsresult rv = bookmarks->GetTagsFolder(&mTagsFolder);
2535     NS_ENSURE_SUCCESS(rv, -1);
2536   }
2537   return mTagsFolder;
2538 }
2539 
2540 // nsNavHistory::FilterResultSet
2541 //
2542 // This does some post-query-execution filtering:
2543 //   - searching on title, url and tags
2544 //   - limit count
2545 //
2546 // Note:  changes to filtering in FilterResultSet()
2547 // may require changes to NeedToFilterResultSet()
2548 
2549 // static
FilterResultSet(nsNavHistoryQueryResultNode * aQueryNode,const nsCOMArray<nsNavHistoryResultNode> & aSet,nsCOMArray<nsNavHistoryResultNode> * aFiltered,const RefPtr<nsNavHistoryQuery> & aQuery,nsNavHistoryQueryOptions * aOptions)2550 nsresult nsNavHistory::FilterResultSet(
2551     nsNavHistoryQueryResultNode* aQueryNode,
2552     const nsCOMArray<nsNavHistoryResultNode>& aSet,
2553     nsCOMArray<nsNavHistoryResultNode>* aFiltered,
2554     const RefPtr<nsNavHistoryQuery>& aQuery,
2555     nsNavHistoryQueryOptions* aOptions) {
2556   // parse the search terms
2557   nsTArray<nsString> terms;
2558   ParseSearchTermsFromQuery(aQuery, &terms);
2559 
2560   bool excludeQueries = aOptions->ExcludeQueries();
2561   for (int32_t nodeIndex = 0; nodeIndex < aSet.Count(); nodeIndex++) {
2562     if (excludeQueries && aSet[nodeIndex]->IsQuery()) {
2563       continue;
2564     }
2565 
2566     if (aSet[nodeIndex]->mItemId != -1 && aQueryNode &&
2567         aQueryNode->mItemId == aSet[nodeIndex]->mItemId) {
2568       continue;
2569     }
2570 
2571     // If there are search terms, we are already getting only uri nodes,
2572     // thus we don't need to filter node types. Though, we must check for
2573     // matching terms.
2574     if (terms.Length()) {
2575       // Filter based on search terms.
2576       // Convert title and url for the current node to UTF16 strings.
2577       NS_ConvertUTF8toUTF16 nodeTitle(aSet[nodeIndex]->mTitle);
2578       // Unescape the URL for search terms matching.
2579       nsAutoCString cNodeURL(aSet[nodeIndex]->mURI);
2580       NS_ConvertUTF8toUTF16 nodeURL(NS_UnescapeURL(cNodeURL));
2581 
2582       // Determine if every search term matches anywhere in the title, url or
2583       // tag.
2584       bool matchAllTerms = true;
2585       for (int32_t termIndex = terms.Length() - 1;
2586            termIndex >= 0 && matchAllTerms; termIndex--) {
2587         nsString& term = terms.ElementAt(termIndex);
2588         // True if any of them match; false makes us quit the loop
2589         matchAllTerms =
2590             CaseInsensitiveFindInReadable(term, nodeTitle) ||
2591             CaseInsensitiveFindInReadable(term, nodeURL) ||
2592             CaseInsensitiveFindInReadable(term, aSet[nodeIndex]->mTags);
2593       }
2594       // Skip the node if we don't match all terms in the title, url or tag
2595       if (!matchAllTerms) {
2596         continue;
2597       }
2598     }
2599 
2600     aFiltered->AppendObject(aSet[nodeIndex]);
2601 
2602     // Stop once we have reached max results.
2603     if (aOptions->MaxResults() > 0 &&
2604         (uint32_t)aFiltered->Count() >= aOptions->MaxResults())
2605       break;
2606   }
2607 
2608   return NS_OK;
2609 }
2610 
2611 NS_IMETHODIMP
MakeGuid(nsACString & aGuid)2612 nsNavHistory::MakeGuid(nsACString& aGuid) {
2613   if (NS_FAILED(GenerateGUID(aGuid))) {
2614     MOZ_ASSERT(false, "Shouldn't fail to create a guid!");
2615     aGuid.SetIsVoid(true);
2616   }
2617   return NS_OK;
2618 }
2619 
2620 NS_IMETHODIMP
HashURL(const nsACString & aSpec,const nsACString & aMode,uint64_t * _hash)2621 nsNavHistory::HashURL(const nsACString& aSpec, const nsACString& aMode,
2622                       uint64_t* _hash) {
2623   return places::HashURL(aSpec, aMode, _hash);
2624 }
2625 
2626 // nsNavHistory::CheckIsRecentEvent
2627 //
2628 //    Sees if this URL happened "recently."
2629 //
2630 //    It is always removed from our recent list no matter what. It only counts
2631 //    as "recent" if the event happened more recently than our event
2632 //    threshold ago.
2633 
CheckIsRecentEvent(RecentEventHash * hashTable,const nsACString & url)2634 bool nsNavHistory::CheckIsRecentEvent(RecentEventHash* hashTable,
2635                                       const nsACString& url) {
2636   PRTime eventTime;
2637   if (hashTable->Get(url, reinterpret_cast<int64_t*>(&eventTime))) {
2638     hashTable->Remove(url);
2639     if (eventTime > GetNow() - RECENT_EVENT_THRESHOLD) return true;
2640     return false;
2641   }
2642   return false;
2643 }
2644 
2645 // nsNavHistory::ExpireNonrecentEvents
2646 //
2647 //    This goes through our
2648 
ExpireNonrecentEvents(RecentEventHash * hashTable)2649 void nsNavHistory::ExpireNonrecentEvents(RecentEventHash* hashTable) {
2650   int64_t threshold = GetNow() - RECENT_EVENT_THRESHOLD;
2651   for (auto iter = hashTable->Iter(); !iter.Done(); iter.Next()) {
2652     if (iter.Data() < threshold) {
2653       iter.Remove();
2654     }
2655   }
2656 }
2657 
2658 // nsNavHistory::RowToResult
2659 //
2660 //    Here, we just have a generic row. It could be a query, URL, visit,
2661 //    or full visit.
2662 
RowToResult(mozIStorageValueArray * aRow,nsNavHistoryQueryOptions * aOptions,nsNavHistoryResultNode ** aResult)2663 nsresult nsNavHistory::RowToResult(mozIStorageValueArray* aRow,
2664                                    nsNavHistoryQueryOptions* aOptions,
2665                                    nsNavHistoryResultNode** aResult) {
2666   NS_ASSERTION(aRow && aOptions && aResult, "Null pointer in RowToResult");
2667 
2668   // URL
2669   nsAutoCString url;
2670   nsresult rv = aRow->GetUTF8String(kGetInfoIndex_URL, url);
2671   NS_ENSURE_SUCCESS(rv, rv);
2672   // In case of data corruption URL may be null, but our UI code prefers an
2673   // empty string.
2674   if (url.IsVoid()) {
2675     MOZ_ASSERT(false, "Found a NULL url in moz_places");
2676     url.SetIsVoid(false);
2677   }
2678 
2679   // title
2680   nsAutoCString title;
2681   bool isNull;
2682   rv = aRow->GetIsNull(kGetInfoIndex_Title, &isNull);
2683   NS_ENSURE_SUCCESS(rv, rv);
2684   if (!isNull) {
2685     rv = aRow->GetUTF8String(kGetInfoIndex_Title, title);
2686     NS_ENSURE_SUCCESS(rv, rv);
2687   }
2688 
2689   uint32_t accessCount = aRow->AsInt32(kGetInfoIndex_VisitCount);
2690   PRTime time = aRow->AsInt64(kGetInfoIndex_VisitDate);
2691 
2692   // itemId
2693   int64_t itemId = aRow->AsInt64(kGetInfoIndex_ItemId);
2694   if (itemId == 0) {
2695     // This is not a bookmark.  For non-bookmarks we use a -1 itemId value.
2696     // Notice ids in sqlite tables start from 1, so itemId cannot ever be 0.
2697     itemId = -1;
2698   }
2699 
2700   if (IsQueryURI(url)) {
2701     // Special case "place:" URIs: turn them into containers.
2702     if (itemId != -1) {
2703       // We should never expose the history title for query nodes if the
2704       // bookmark-item's title is set to null (the history title may be the
2705       // query string without the place: prefix). Thus we call getItemTitle
2706       // explicitly. Doing this in the SQL query would be less performant since
2707       // it should be done for all results rather than only for queries.
2708       nsNavBookmarks* bookmarks = nsNavBookmarks::GetBookmarksService();
2709       NS_ENSURE_TRUE(bookmarks, NS_ERROR_OUT_OF_MEMORY);
2710 
2711       rv = bookmarks->GetItemTitle(itemId, title);
2712       NS_ENSURE_SUCCESS(rv, rv);
2713     }
2714 
2715     nsAutoCString guid;
2716     if (itemId != -1) {
2717       rv = aRow->GetUTF8String(nsNavBookmarks::kGetChildrenIndex_Guid, guid);
2718       NS_ENSURE_SUCCESS(rv, rv);
2719     }
2720 
2721     if (aOptions->ResultType() ==
2722             nsNavHistoryQueryOptions::RESULTS_AS_ROOTS_QUERY ||
2723         aOptions->ResultType() ==
2724             nsNavHistoryQueryOptions::RESULTS_AS_LEFT_PANE_QUERY) {
2725       rv = aRow->GetUTF8String(kGetInfoIndex_Guid, guid);
2726       NS_ENSURE_SUCCESS(rv, rv);
2727     }
2728 
2729     RefPtr<nsNavHistoryResultNode> resultNode;
2730     rv = QueryRowToResult(itemId, guid, url, title, accessCount, time,
2731                           getter_AddRefs(resultNode));
2732     NS_ENSURE_SUCCESS(rv, rv);
2733 
2734     if (itemId != -1 || aOptions->ResultType() ==
2735                             nsNavHistoryQueryOptions::RESULTS_AS_TAGS_ROOT) {
2736       // RESULTS_AS_TAGS_ROOT has date columns
2737       resultNode->mDateAdded = aRow->AsInt64(kGetInfoIndex_ItemDateAdded);
2738       resultNode->mLastModified = aRow->AsInt64(kGetInfoIndex_ItemLastModified);
2739       if (resultNode->IsFolder()) {
2740         // If it's a simple folder node (i.e. a shortcut to another folder),
2741         // apply our options for it. However, if the parent type was tag query,
2742         // we do not apply them, because it would not yield any results.
2743         resultNode->GetAsContainer()->mOptions = aOptions;
2744       }
2745     }
2746 
2747     resultNode.forget(aResult);
2748     return rv;
2749   } else if (aOptions->ResultType() ==
2750              nsNavHistoryQueryOptions::RESULTS_AS_URI) {
2751     RefPtr<nsNavHistoryResultNode> resultNode =
2752         new nsNavHistoryResultNode(url, title, accessCount, time);
2753 
2754     if (itemId != -1) {
2755       resultNode->mItemId = itemId;
2756       resultNode->mDateAdded = aRow->AsInt64(kGetInfoIndex_ItemDateAdded);
2757       resultNode->mLastModified = aRow->AsInt64(kGetInfoIndex_ItemLastModified);
2758 
2759       rv = aRow->GetUTF8String(nsNavBookmarks::kGetChildrenIndex_Guid,
2760                                resultNode->mBookmarkGuid);
2761       NS_ENSURE_SUCCESS(rv, rv);
2762     }
2763 
2764     resultNode->mFrecency = aRow->AsInt32(kGetInfoIndex_Frecency);
2765     resultNode->mHidden = !!aRow->AsInt32(kGetInfoIndex_Hidden);
2766 
2767     nsAutoString tags;
2768     rv = aRow->GetString(kGetInfoIndex_ItemTags, tags);
2769     NS_ENSURE_SUCCESS(rv, rv);
2770     if (!tags.IsVoid()) {
2771       resultNode->mTags.Assign(tags);
2772     }
2773 
2774     rv = aRow->GetUTF8String(kGetInfoIndex_Guid, resultNode->mPageGuid);
2775     NS_ENSURE_SUCCESS(rv, rv);
2776 
2777     resultNode.forget(aResult);
2778     return NS_OK;
2779   }
2780 
2781   if (aOptions->ResultType() == nsNavHistoryQueryOptions::RESULTS_AS_VISIT) {
2782     RefPtr<nsNavHistoryResultNode> resultNode =
2783         new nsNavHistoryResultNode(url, title, accessCount, time);
2784 
2785     nsAutoString tags;
2786     rv = aRow->GetString(kGetInfoIndex_ItemTags, tags);
2787     if (!tags.IsVoid()) resultNode->mTags.Assign(tags);
2788 
2789     rv = aRow->GetUTF8String(kGetInfoIndex_Guid, resultNode->mPageGuid);
2790     NS_ENSURE_SUCCESS(rv, rv);
2791 
2792     rv = aRow->GetInt64(kGetInfoIndex_VisitId, &resultNode->mVisitId);
2793     NS_ENSURE_SUCCESS(rv, rv);
2794 
2795     int64_t fromVisitId;
2796     rv = aRow->GetInt64(kGetInfoIndex_FromVisitId, &fromVisitId);
2797     NS_ENSURE_SUCCESS(rv, rv);
2798 
2799     if (fromVisitId > 0) {
2800       resultNode->mFromVisitId = fromVisitId;
2801     }
2802 
2803     resultNode->mTransitionType = aRow->AsInt32(kGetInfoIndex_VisitType);
2804 
2805     resultNode.forget(aResult);
2806     return NS_OK;
2807   }
2808 
2809   return NS_ERROR_FAILURE;
2810 }
2811 
2812 // nsNavHistory::QueryRowToResult
2813 //
2814 //    Called by RowToResult when the URI is a place: URI to generate the proper
2815 //    folder or query node.
2816 
QueryRowToResult(int64_t itemId,const nsACString & aBookmarkGuid,const nsACString & aURI,const nsACString & aTitle,uint32_t aAccessCount,PRTime aTime,nsNavHistoryResultNode ** aNode)2817 nsresult nsNavHistory::QueryRowToResult(int64_t itemId,
2818                                         const nsACString& aBookmarkGuid,
2819                                         const nsACString& aURI,
2820                                         const nsACString& aTitle,
2821                                         uint32_t aAccessCount, PRTime aTime,
2822                                         nsNavHistoryResultNode** aNode) {
2823   // Only assert if the itemId is set. In some cases (e.g. virtual queries), we
2824   // have a guid, but not an itemId.
2825   if (itemId != -1) {
2826     MOZ_ASSERT(!aBookmarkGuid.IsEmpty());
2827   }
2828 
2829   nsCOMPtr<nsINavHistoryQuery> query;
2830   nsCOMPtr<nsINavHistoryQueryOptions> options;
2831   nsresult rv =
2832       QueryStringToQuery(aURI, getter_AddRefs(query), getter_AddRefs(options));
2833   RefPtr<nsNavHistoryResultNode> resultNode;
2834   RefPtr<nsNavHistoryQuery> queryObj = do_QueryObject(query);
2835   NS_ENSURE_STATE(queryObj);
2836   RefPtr<nsNavHistoryQueryOptions> optionsObj = do_QueryObject(options);
2837   NS_ENSURE_STATE(optionsObj);
2838   // If this failed the query does not parse correctly, let the error pass and
2839   // handle it later.
2840   if (NS_SUCCEEDED(rv)) {
2841     // Check if this is a folder shortcut, so we can take a faster path.
2842     nsCString targetFolderGuid =
2843         GetSimpleBookmarksQueryParent(queryObj, optionsObj);
2844     if (!targetFolderGuid.IsEmpty()) {
2845       nsNavBookmarks* bookmarks = nsNavBookmarks::GetBookmarksService();
2846       NS_ENSURE_TRUE(bookmarks, NS_ERROR_OUT_OF_MEMORY);
2847 
2848       rv = bookmarks->ResultNodeForContainer(targetFolderGuid, optionsObj,
2849                                              getter_AddRefs(resultNode));
2850       // If this failed the shortcut is pointing to nowhere, let the error pass
2851       // and handle it later.
2852       if (NS_SUCCEEDED(rv)) {
2853         // At this point the node is set up like a regular folder node. Here
2854         // we make the necessary change to make it a folder shortcut.
2855         resultNode->mItemId = itemId;
2856         resultNode->mBookmarkGuid = aBookmarkGuid;
2857         resultNode->GetAsFolder()->mTargetFolderGuid = targetFolderGuid;
2858 
2859         // Use the query item title, unless it's empty (in that case use the
2860         // concrete folder title).
2861         if (!aTitle.IsEmpty()) {
2862           resultNode->mTitle = aTitle;
2863         }
2864       }
2865     } else {
2866       // This is a regular query.
2867       resultNode = new nsNavHistoryQueryResultNode(aTitle, aTime, aURI,
2868                                                    queryObj, optionsObj);
2869       resultNode->mItemId = itemId;
2870       resultNode->mBookmarkGuid = aBookmarkGuid;
2871     }
2872   }
2873 
2874   if (NS_FAILED(rv)) {
2875     NS_WARNING("Generating a generic empty node for a broken query!");
2876     // This is a broken query, that either did not parse or points to not
2877     // existing data.  We don't want to return failure since that will kill the
2878     // whole result.  Instead make a generic empty query node.
2879     resultNode =
2880         new nsNavHistoryQueryResultNode(aTitle, 0, aURI, queryObj, optionsObj);
2881     resultNode->mItemId = itemId;
2882     resultNode->mBookmarkGuid = aBookmarkGuid;
2883     // This is a perf hack to generate an empty query that skips filtering.
2884     resultNode->GetAsQuery()->Options()->SetExcludeItems(true);
2885   }
2886 
2887   resultNode.forget(aNode);
2888   return NS_OK;
2889 }
2890 
2891 // nsNavHistory::VisitIdToResultNode
2892 //
2893 //    Used by the query results to create new nodes on the fly when
2894 //    notifications come in. This just creates a node for the given visit ID.
2895 
VisitIdToResultNode(int64_t visitId,nsNavHistoryQueryOptions * aOptions,nsNavHistoryResultNode ** aResult)2896 nsresult nsNavHistory::VisitIdToResultNode(int64_t visitId,
2897                                            nsNavHistoryQueryOptions* aOptions,
2898                                            nsNavHistoryResultNode** aResult) {
2899   MOZ_ASSERT(visitId > 0, "The passed-in visit id must be valid");
2900   nsAutoCString tagsFragment;
2901   GetTagsSqlFragment(GetTagsFolder(), "h.id"_ns, true, tagsFragment);
2902 
2903   nsCOMPtr<mozIStorageStatement> statement;
2904   switch (aOptions->ResultType()) {
2905     case nsNavHistoryQueryOptions::RESULTS_AS_VISIT:
2906       // visit query - want exact visit time
2907       // Should match kGetInfoIndex_* (see GetQueryResults)
2908       statement = mDB->GetStatement(
2909           nsLiteralCString(
2910               "SELECT h.id, h.url, h.title, h.rev_host, h.visit_count, "
2911               "v.visit_date, null, null, null, null, null, ") +
2912           tagsFragment +
2913           nsLiteralCString(", h.frecency, h.hidden, h.guid, "
2914                            "v.id, v.from_visit, v.visit_type "
2915                            "FROM moz_places h "
2916                            "JOIN moz_historyvisits v ON h.id = v.place_id "
2917                            "WHERE v.id = :visit_id "));
2918       break;
2919 
2920     case nsNavHistoryQueryOptions::RESULTS_AS_URI:
2921       // URL results - want last visit time
2922       // Should match kGetInfoIndex_* (see GetQueryResults)
2923       statement = mDB->GetStatement(
2924           nsLiteralCString(
2925               "SELECT h.id, h.url, h.title, h.rev_host, h.visit_count, "
2926               "h.last_visit_date, null, null, null, null, null, ") +
2927           tagsFragment +
2928           nsLiteralCString(", h.frecency, h.hidden, h.guid, "
2929                            "null, null, null "
2930                            "FROM moz_places h "
2931                            "JOIN moz_historyvisits v ON h.id = v.place_id "
2932                            "WHERE v.id = :visit_id "));
2933       break;
2934 
2935     default:
2936       // Query base types like RESULTS_AS_*_QUERY handle additions
2937       // by registering their own observers when they are expanded.
2938       return NS_OK;
2939   }
2940   NS_ENSURE_STATE(statement);
2941   mozStorageStatementScoper scoper(statement);
2942 
2943   nsresult rv = statement->BindInt64ByName("visit_id"_ns, visitId);
2944   NS_ENSURE_SUCCESS(rv, rv);
2945 
2946   bool hasMore = false;
2947   rv = statement->ExecuteStep(&hasMore);
2948   NS_ENSURE_SUCCESS(rv, rv);
2949   if (!hasMore) {
2950     // Oops, we were passed an id that doesn't exist! It is indeed possible
2951     // that between the insertion and the notification time, another enqueued
2952     // task removed it. Since this can happen, we'll just issue a warning.
2953     NS_WARNING(
2954         "Cannot build a result node for a non existing visit id, was it "
2955         "removed?");
2956     return NS_ERROR_NOT_AVAILABLE;
2957   }
2958 
2959   nsCOMPtr<mozIStorageValueArray> row = do_QueryInterface(statement, &rv);
2960   NS_ENSURE_SUCCESS(rv, rv);
2961 
2962   return RowToResult(row, aOptions, aResult);
2963 }
2964 
BookmarkIdToResultNode(int64_t aBookmarkId,nsNavHistoryQueryOptions * aOptions,nsNavHistoryResultNode ** aResult)2965 nsresult nsNavHistory::BookmarkIdToResultNode(
2966     int64_t aBookmarkId, nsNavHistoryQueryOptions* aOptions,
2967     nsNavHistoryResultNode** aResult) {
2968   MOZ_ASSERT(aBookmarkId > 0, "The passed-in bookmark id must be valid");
2969   nsAutoCString tagsFragment;
2970   GetTagsSqlFragment(GetTagsFolder(), "h.id"_ns, true, tagsFragment);
2971   // Should match kGetInfoIndex_*
2972   nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
2973       nsLiteralCString(
2974           "SELECT b.fk, h.url, b.title, "
2975           "h.rev_host, h.visit_count, h.last_visit_date, null, b.id, "
2976           "b.dateAdded, b.lastModified, b.parent, ") +
2977       tagsFragment +
2978       nsLiteralCString(", h.frecency, h.hidden, h.guid, "
2979                        "null, null, null, b.guid, b.position, b.type, b.fk "
2980                        "FROM moz_bookmarks b "
2981                        "JOIN moz_places h ON b.fk = h.id "
2982                        "WHERE b.id = :item_id "));
2983   NS_ENSURE_STATE(stmt);
2984   mozStorageStatementScoper scoper(stmt);
2985 
2986   nsresult rv = stmt->BindInt64ByName("item_id"_ns, aBookmarkId);
2987   NS_ENSURE_SUCCESS(rv, rv);
2988 
2989   bool hasMore = false;
2990   rv = stmt->ExecuteStep(&hasMore);
2991   NS_ENSURE_SUCCESS(rv, rv);
2992   if (!hasMore) {
2993     // Oops, we were passed an id that doesn't exist! It is indeed possible
2994     // that between the insertion and the notification time, another enqueued
2995     // task removed it. Since this can happen, we'll just issue a warning.
2996     NS_WARNING(
2997         "Cannot build a result node for a non existing bookmark id, was it "
2998         "removed?");
2999     return NS_ERROR_NOT_AVAILABLE;
3000   }
3001 
3002   nsCOMPtr<mozIStorageValueArray> row = do_QueryInterface(stmt, &rv);
3003   NS_ENSURE_SUCCESS(rv, rv);
3004 
3005   return RowToResult(row, aOptions, aResult);
3006 }
3007 
URIToResultNode(nsIURI * aURI,nsNavHistoryQueryOptions * aOptions,nsNavHistoryResultNode ** aResult)3008 nsresult nsNavHistory::URIToResultNode(nsIURI* aURI,
3009                                        nsNavHistoryQueryOptions* aOptions,
3010                                        nsNavHistoryResultNode** aResult) {
3011   MOZ_ASSERT(aURI, "The passed-in URI must be not-null");
3012   nsAutoCString tagsFragment;
3013   GetTagsSqlFragment(GetTagsFolder(), "h.id"_ns, true, tagsFragment);
3014   // Should match kGetInfoIndex_*
3015   nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
3016       nsLiteralCString("SELECT h.id, :page_url, COALESCE(b.title, h.title), "
3017                        "h.rev_host, h.visit_count, h.last_visit_date, null, "
3018                        "b.id, b.dateAdded, b.lastModified, b.parent, ") +
3019       tagsFragment +
3020       nsLiteralCString(
3021           ", h.frecency, h.hidden, h.guid, "
3022           "null, null, null, b.guid, b.position, b.type, b.fk "
3023           "FROM moz_places h "
3024           "LEFT JOIN moz_bookmarks b ON b.fk = h.id "
3025           "WHERE h.url_hash = hash(:page_url) AND h.url = :page_url "));
3026   NS_ENSURE_STATE(stmt);
3027   mozStorageStatementScoper scoper(stmt);
3028 
3029   nsresult rv = URIBinder::Bind(stmt, "page_url"_ns, aURI);
3030   NS_ENSURE_SUCCESS(rv, rv);
3031 
3032   bool hasMore = false;
3033   rv = stmt->ExecuteStep(&hasMore);
3034   NS_ENSURE_SUCCESS(rv, rv);
3035   if (!hasMore) {
3036     // Oops, we were passed an URL that doesn't exist! It is indeed possible
3037     // that between the insertion and the notification time, another enqueued
3038     // task removed it. Since this can happen, we'll just issue a warning.
3039     NS_WARNING(
3040         "Cannot build a result node for a non existing page id, was it "
3041         "removed?");
3042     return NS_ERROR_NOT_AVAILABLE;
3043   }
3044 
3045   nsCOMPtr<mozIStorageValueArray> row = do_QueryInterface(stmt, &rv);
3046   NS_ENSURE_SUCCESS(rv, rv);
3047 
3048   return RowToResult(row, aOptions, aResult);
3049 }
3050 
GetAgeInDaysString(int32_t aInt,const char * aName,nsACString & aResult)3051 void nsNavHistory::GetAgeInDaysString(int32_t aInt, const char* aName,
3052                                       nsACString& aResult) {
3053   nsIStringBundle* bundle = GetBundle();
3054   if (bundle) {
3055     AutoTArray<nsString, 1> strings;
3056     strings.AppendElement()->AppendInt(aInt);
3057     nsAutoString value;
3058     nsresult rv = bundle->FormatStringFromName(aName, strings, value);
3059     if (NS_SUCCEEDED(rv)) {
3060       CopyUTF16toUTF8(value, aResult);
3061       return;
3062     }
3063   }
3064   aResult.Assign(aName);
3065 }
3066 
GetStringFromName(const char * aName,nsACString & aResult)3067 void nsNavHistory::GetStringFromName(const char* aName, nsACString& aResult) {
3068   nsIStringBundle* bundle = GetBundle();
3069   if (bundle) {
3070     nsAutoString value;
3071     nsresult rv = bundle->GetStringFromName(aName, value);
3072     if (NS_SUCCEEDED(rv)) {
3073       CopyUTF16toUTF8(value, aResult);
3074       return;
3075     }
3076   }
3077   aResult.Assign(aName);
3078 }
3079 
3080 // static
GetMonthName(const PRExplodedTime & aTime,nsACString & aResult)3081 void nsNavHistory::GetMonthName(const PRExplodedTime& aTime,
3082                                 nsACString& aResult) {
3083   nsAutoString month;
3084 
3085   mozilla::intl::DateTimeFormat::ComponentsBag components;
3086   components.month = Some(mozilla::intl::DateTimeFormat::Month::Long);
3087   nsresult rv =
3088       mozilla::intl::AppDateTimeFormat::Format(components, &aTime, month);
3089   if (NS_FAILED(rv)) {
3090     aResult = nsPrintfCString("[%d]", aTime.tm_month + 1);
3091     return;
3092   }
3093   CopyUTF16toUTF8(month, aResult);
3094 }
3095 
3096 // static
GetMonthYear(const PRExplodedTime & aTime,nsACString & aResult)3097 void nsNavHistory::GetMonthYear(const PRExplodedTime& aTime,
3098                                 nsACString& aResult) {
3099   nsAutoString monthYear;
3100   mozilla::intl::DateTimeFormat::ComponentsBag components;
3101   components.month = Some(mozilla::intl::DateTimeFormat::Month::Long);
3102   components.year = Some(mozilla::intl::DateTimeFormat::Numeric::Numeric);
3103   nsresult rv =
3104       mozilla::intl::AppDateTimeFormat::Format(components, &aTime, monthYear);
3105   if (NS_FAILED(rv)) {
3106     aResult = nsPrintfCString("[%d-%d]", aTime.tm_month + 1, aTime.tm_year);
3107     return;
3108   }
3109   CopyUTF16toUTF8(monthYear, aResult);
3110 }
3111 
3112 namespace {
3113 
3114 // GetSimpleBookmarksQueryParent
3115 //
3116 //    Determines if this is a simple bookmarks query for a
3117 //    folder with no other constraints. In these common cases, we can more
3118 //    efficiently compute the results.
3119 //
3120 //    A simple bookmarks query will result in a hierarchical tree of
3121 //    bookmark items, folders and separators.
3122 //
3123 //    Returns the folder ID if it is a simple folder query, 0 if not.
GetSimpleBookmarksQueryParent(const RefPtr<nsNavHistoryQuery> & aQuery,const RefPtr<nsNavHistoryQueryOptions> & aOptions)3124 static nsCString GetSimpleBookmarksQueryParent(
3125     const RefPtr<nsNavHistoryQuery>& aQuery,
3126     const RefPtr<nsNavHistoryQueryOptions>& aOptions) {
3127   if (aQuery->Parents().Length() != 1) return ""_ns;
3128 
3129   bool hasIt;
3130   if (NS_SUCCEEDED(aQuery->GetHasBeginTime(&hasIt)) && hasIt) return ""_ns;
3131   if (NS_SUCCEEDED(aQuery->GetHasEndTime(&hasIt)) && hasIt) return ""_ns;
3132   if (!aQuery->Domain().IsVoid()) return ""_ns;
3133   if (aQuery->Uri()) return ""_ns;
3134   if (!aQuery->SearchTerms().IsEmpty()) return ""_ns;
3135   if (aQuery->Tags().Length() > 0) return ""_ns;
3136   if (aOptions->MaxResults() > 0) return ""_ns;
3137 
3138   // Don't care about onlyBookmarked flag, since specifying a bookmark
3139   // folder is inferring onlyBookmarked.
3140 
3141   return aQuery->Parents()[0];
3142 }
3143 
3144 // ParseSearchTermsFromQuery
3145 //
3146 //    Construct an array of search terms from the given query.
3147 //    Within a query, all the terms are ANDed together.
3148 //
3149 //    This just breaks the query up into words. We don't do anything fancy,
3150 //    not even quoting. We do, however, strip quotes, because people might
3151 //    try to input quotes expecting them to do something and get no results
3152 //    back.
3153 
isQueryWhitespace(char16_t ch)3154 inline bool isQueryWhitespace(char16_t ch) { return ch == ' '; }
3155 
ParseSearchTermsFromQuery(const RefPtr<nsNavHistoryQuery> & aQuery,nsTArray<nsString> * aTerms)3156 void ParseSearchTermsFromQuery(const RefPtr<nsNavHistoryQuery>& aQuery,
3157                                nsTArray<nsString>* aTerms) {
3158   int32_t lastBegin = -1;
3159   if (!aQuery->SearchTerms().IsEmpty()) {
3160     const nsString& searchTerms = aQuery->SearchTerms();
3161     for (uint32_t j = 0; j < searchTerms.Length(); j++) {
3162       if (isQueryWhitespace(searchTerms[j]) || searchTerms[j] == '"') {
3163         if (lastBegin >= 0) {
3164           // found the end of a word
3165           aTerms->AppendElement(
3166               Substring(searchTerms, lastBegin, j - lastBegin));
3167           lastBegin = -1;
3168         }
3169       } else {
3170         if (lastBegin < 0) {
3171           // found the beginning of a word
3172           lastBegin = j;
3173         }
3174       }
3175     }
3176     // last word
3177     if (lastBegin >= 0)
3178       aTerms->AppendElement(Substring(searchTerms, lastBegin));
3179   }
3180 }
3181 
3182 }  // namespace
3183 
UpdateFrecency(int64_t aPlaceId)3184 nsresult nsNavHistory::UpdateFrecency(int64_t aPlaceId) {
3185   nsCOMPtr<mozIStorageAsyncStatement> updateFrecencyStmt =
3186       mDB->GetAsyncStatement(
3187           "UPDATE moz_places "
3188           "SET frecency = CALCULATE_FRECENCY(:page_id) "
3189           "WHERE id = :page_id");
3190   NS_ENSURE_STATE(updateFrecencyStmt);
3191   NS_DispatchToMainThread(new NotifyRankingChanged());
3192   nsresult rv = updateFrecencyStmt->BindInt64ByName("page_id"_ns, aPlaceId);
3193   NS_ENSURE_SUCCESS(rv, rv);
3194   nsCOMPtr<mozIStorageAsyncStatement> updateHiddenStmt = mDB->GetAsyncStatement(
3195       "UPDATE moz_places "
3196       "SET hidden = 0 "
3197       "WHERE id = :page_id AND frecency <> 0");
3198   NS_ENSURE_STATE(updateHiddenStmt);
3199   rv = updateHiddenStmt->BindInt64ByName("page_id"_ns, aPlaceId);
3200   NS_ENSURE_SUCCESS(rv, rv);
3201 
3202   nsCOMPtr<mozIStorageConnection> conn = mDB->MainConn();
3203   if (!conn) {
3204     return NS_ERROR_UNEXPECTED;
3205   }
3206 
3207   nsTArray<RefPtr<mozIStorageBaseStatement>> stmts = {
3208       ToRefPtr(std::move(updateFrecencyStmt)),
3209       ToRefPtr(std::move(updateHiddenStmt)),
3210   };
3211   nsCOMPtr<mozIStoragePendingStatement> ps;
3212   rv = conn->ExecuteAsync(stmts, nullptr, getter_AddRefs(ps));
3213   NS_ENSURE_SUCCESS(rv, rv);
3214 
3215   // Trigger frecency updates for all affected origins.
3216   nsCOMPtr<mozIStorageAsyncStatement> updateOriginFrecenciesStmt =
3217       mDB->GetAsyncStatement("DELETE FROM moz_updateoriginsupdate_temp");
3218   NS_ENSURE_STATE(updateOriginFrecenciesStmt);
3219   rv = updateOriginFrecenciesStmt->ExecuteAsync(nullptr, getter_AddRefs(ps));
3220   NS_ENSURE_SUCCESS(rv, rv);
3221 
3222   return NS_OK;
3223 }
3224 
GetCollator()3225 const mozilla::intl::Collator* nsNavHistory::GetCollator() {
3226   if (mCollator) {
3227     return mCollator.get();
3228   }
3229 
3230   auto result = mozilla::intl::LocaleService::TryCreateComponent<
3231       mozilla::intl::Collator>();
3232   NS_ENSURE_TRUE(result.isOk(), nullptr);
3233   auto collator = result.unwrap();
3234 
3235   // Sort in a case-insensitive way, where "base" letters are considered
3236   // equal, e.g: a = á, a = A, a ≠ b.
3237   using mozilla::intl::Collator;
3238   Collator::Options options{};
3239   options.sensitivity = Collator::Sensitivity::Base;
3240   auto optResult = collator->SetOptions(options);
3241   NS_ENSURE_TRUE(optResult.isOk(), nullptr);
3242 
3243   mCollator = UniquePtr<const Collator>(collator.release());
3244 
3245   return mCollator.get();
3246 }
3247 
GetBundle()3248 nsIStringBundle* nsNavHistory::GetBundle() {
3249   if (!mBundle) {
3250     nsCOMPtr<nsIStringBundleService> bundleService =
3251         components::StringBundle::Service();
3252     NS_ENSURE_TRUE(bundleService, nullptr);
3253     nsresult rv = bundleService->CreateBundle(
3254         "chrome://places/locale/places.properties", getter_AddRefs(mBundle));
3255     NS_ENSURE_SUCCESS(rv, nullptr);
3256   }
3257   return mBundle;
3258 }
3259