summaryrefslogtreecommitdiffstats
path: root/toolkit/components/places/SQLFunctions.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/places/SQLFunctions.cpp')
-rw-r--r--toolkit/components/places/SQLFunctions.cpp1520
1 files changed, 1520 insertions, 0 deletions
diff --git a/toolkit/components/places/SQLFunctions.cpp b/toolkit/components/places/SQLFunctions.cpp
new file mode 100644
index 0000000000..e625f3fa09
--- /dev/null
+++ b/toolkit/components/places/SQLFunctions.cpp
@@ -0,0 +1,1520 @@
+/* vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/storage.h"
+#include "mozilla/StaticPrefs_places.h"
+#include "nsString.h"
+#include "nsFaviconService.h"
+#include "nsNavBookmarks.h"
+#include "nsUnicharUtils.h"
+#include "nsWhitespaceTokenizer.h"
+#include "nsEscape.h"
+#include "mozIPlacesAutoComplete.h"
+#include "SQLFunctions.h"
+#include "nsMathUtils.h"
+#include "nsUnicodeProperties.h"
+#include "nsUTF8Utils.h"
+#include "nsINavHistoryService.h"
+#include "nsPrintfCString.h"
+#include "nsNavHistory.h"
+#include "mozilla/Likely.h"
+#include "mozilla/Services.h"
+#include "mozilla/Utf8.h"
+#include "nsURLHelper.h"
+#include "nsVariant.h"
+#include "nsICryptoHash.h"
+
+// Maximum number of chars to search through.
+// MatchAutoCompleteFunction won't look for matches over this threshold.
+#define MAX_CHARS_TO_SEARCH_THROUGH 255
+
+#define SECONDS_PER_DAY 86400
+
+using namespace mozilla::storage;
+
+////////////////////////////////////////////////////////////////////////////////
+//// Anonymous Helpers
+
+namespace {
+
+using const_char_iterator = nsACString::const_char_iterator;
+using size_type = nsACString::size_type;
+using char_type = nsACString::char_type;
+
+/**
+ * Scan forward through UTF-8 text until the next potential character that
+ * could match a given codepoint when lower-cased (false positives are okay).
+ * This avoids having to actually parse the UTF-8 text, which is slow.
+ *
+ * @param aStart
+ * An iterator pointing to the first character position considered.
+ * It will be updated by this function.
+ * @param aEnd
+ * An interator pointing to past-the-end of the string.
+ */
+static MOZ_ALWAYS_INLINE void goToNextSearchCandidate(
+ const_char_iterator& aStart, const const_char_iterator& aEnd,
+ uint32_t aSearchFor) {
+ // If the character we search for is ASCII, then we can scan until we find
+ // it or its ASCII uppercase character, modulo the special cases
+ // U+0130 LATIN CAPITAL LETTER I WITH DOT ABOVE and U+212A KELVIN SIGN
+ // (which are the only non-ASCII characters that lower-case to ASCII ones).
+ // Since false positives are okay, we approximate ASCII lower-casing by
+ // bit-ORing with 0x20, for increased performance.
+ //
+ // If the character we search for is *not* ASCII, we can ignore everything
+ // that is, since all ASCII characters lower-case to ASCII.
+ //
+ // Because of how UTF-8 uses high-order bits, this will never land us
+ // in the middle of a codepoint.
+ //
+ // The assumptions about Unicode made here are verified in the test_casing
+ // gtest.
+ if (aSearchFor < 128) {
+ // When searching for I or K, we pick out the first byte of the UTF-8
+ // encoding of the corresponding special case character, and look for it
+ // in the loop below. For other characters we fall back to 0xff, which
+ // is not a valid UTF-8 byte.
+ unsigned char target = (unsigned char)(aSearchFor | 0x20);
+ unsigned char special = 0xff;
+ if (target == 'i' || target == 'k') {
+ special = (target == 'i' ? 0xc4 : 0xe2);
+ }
+
+ while (aStart < aEnd && (unsigned char)(*aStart | 0x20) != target &&
+ (unsigned char)*aStart != special) {
+ aStart++;
+ }
+ } else {
+ while (aStart < aEnd && (unsigned char)(*aStart) < 128) {
+ aStart++;
+ }
+ }
+}
+
+/**
+ * Check whether a character position is on a word boundary of a UTF-8 string
+ * (rather than within a word). We define "within word" to be any position
+ * between [a-zA-Z] and [a-z] -- this lets us match CamelCase words.
+ * TODO: support non-latin alphabets.
+ *
+ * @param aPos
+ * An iterator pointing to the character position considered. It must
+ * *not* be the first byte of a string.
+ *
+ * @return true if boundary, false otherwise.
+ */
+static MOZ_ALWAYS_INLINE bool isOnBoundary(const_char_iterator aPos) {
+ if ('a' <= *aPos && *aPos <= 'z') {
+ char prev = static_cast<char>(*(aPos - 1) | 0x20);
+ return !('a' <= prev && prev <= 'z');
+ }
+ return true;
+}
+
+/**
+ * Check whether a token string matches a particular position of a source
+ * string, case insensitively (or optionally, case and diacritic insensitively).
+ *
+ * @param aTokenStart
+ * An iterator pointing to the start of the token string.
+ * @param aTokenEnd
+ * An iterator pointing past-the-end of the token string.
+ * @param aSourceStart
+ * An iterator pointing to the position of source string to start
+ * matching at.
+ * @param aSourceEnd
+ * An iterator pointing past-the-end of the source string.
+ * @param aMatchDiacritics
+ * Whether or not the match is diacritic-sensitive.
+ *
+ * @return true if the string [aTokenStart, aTokenEnd) matches the start of
+ * the string [aSourceStart, aSourceEnd, false otherwise.
+ */
+static MOZ_ALWAYS_INLINE bool stringMatch(const_char_iterator aTokenStart,
+ const_char_iterator aTokenEnd,
+ const_char_iterator aSourceStart,
+ const_char_iterator aSourceEnd,
+ bool aMatchDiacritics) {
+ const_char_iterator tokenCur = aTokenStart, sourceCur = aSourceStart;
+
+ while (tokenCur < aTokenEnd) {
+ if (sourceCur >= aSourceEnd) {
+ return false;
+ }
+
+ bool error;
+ if (!CaseInsensitiveUTF8CharsEqual(sourceCur, tokenCur, aSourceEnd,
+ aTokenEnd, &sourceCur, &tokenCur, &error,
+ aMatchDiacritics)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+enum FindInStringBehavior { eFindOnBoundary, eFindAnywhere };
+
+/**
+ * Common implementation for findAnywhere and findOnBoundary.
+ *
+ * @param aToken
+ * The token we're searching for
+ * @param aSourceString
+ * The string in which we're searching
+ * @param aBehavior
+ * eFindOnBoundary if we should only consider matchines which occur on
+ * word boundaries, or eFindAnywhere if we should consider matches
+ * which appear anywhere.
+ *
+ * @return true if aToken was found in aSourceString, false otherwise.
+ */
+static bool findInString(const nsDependentCSubstring& aToken,
+ const nsACString& aSourceString,
+ FindInStringBehavior aBehavior) {
+ // GetLowerUTF8Codepoint assumes that there's at least one byte in
+ // the string, so don't pass an empty token here.
+ MOZ_ASSERT(!aToken.IsEmpty(), "Don't search for an empty token!");
+
+ // We cannot match anything if there is nothing to search.
+ if (aSourceString.IsEmpty()) {
+ return false;
+ }
+
+ const nsNavHistory* history = nsNavHistory::GetConstHistoryService();
+ bool matchDiacritics = history && history->MatchDiacritics();
+
+ const_char_iterator tokenStart(aToken.BeginReading()),
+ tokenEnd(aToken.EndReading()), tokenNext,
+ sourceStart(aSourceString.BeginReading()),
+ sourceEnd(aSourceString.EndReading()), sourceCur(sourceStart), sourceNext;
+
+ uint32_t tokenFirstChar =
+ GetLowerUTF8Codepoint(tokenStart, tokenEnd, &tokenNext);
+ if (tokenFirstChar == uint32_t(-1)) {
+ return false;
+ }
+ if (!matchDiacritics) {
+ tokenFirstChar = ToNaked(tokenFirstChar);
+ }
+
+ for (;;) {
+ if (matchDiacritics) {
+ // Scan forward to the next viable candidate (if any).
+ goToNextSearchCandidate(sourceCur, sourceEnd, tokenFirstChar);
+ }
+ if (sourceCur == sourceEnd) {
+ break;
+ }
+
+ // Check whether the first character in the token matches the character
+ // at sourceCur. At the same time, get a pointer to the next character
+ // in the source.
+ uint32_t sourceFirstChar =
+ GetLowerUTF8Codepoint(sourceCur, sourceEnd, &sourceNext);
+ if (sourceFirstChar == uint32_t(-1)) {
+ return false;
+ }
+ if (!matchDiacritics) {
+ sourceFirstChar = ToNaked(sourceFirstChar);
+ }
+
+ if (sourceFirstChar == tokenFirstChar &&
+ (aBehavior != eFindOnBoundary || sourceCur == sourceStart ||
+ isOnBoundary(sourceCur)) &&
+ stringMatch(tokenNext, tokenEnd, sourceNext, sourceEnd,
+ matchDiacritics)) {
+ return true;
+ }
+
+ sourceCur = sourceNext;
+ }
+
+ return false;
+}
+
+static MOZ_ALWAYS_INLINE nsDependentCString
+getSharedUTF8String(mozIStorageValueArray* aValues, uint32_t aIndex) {
+ uint32_t len;
+ const char* str = aValues->AsSharedUTF8String(aIndex, &len);
+ if (!str) {
+ return nsDependentCString("", (size_t)0);
+ }
+ return nsDependentCString(str, len);
+}
+
+/**
+ * Gets the length of the prefix in a URI spec. "Prefix" is defined to be the
+ * scheme, colon, and, if present, two slashes.
+ *
+ * Examples:
+ *
+ * http://example.com
+ * ~~~~~~~
+ * => length == 7
+ *
+ * foo:example
+ * ~~~~
+ * => length == 4
+ *
+ * not a spec
+ * => length == 0
+ *
+ * @param aSpec
+ * A URI spec, or a string that may be a URI spec.
+ * @return The length of the prefix in the spec. If there isn't a prefix,
+ * returns 0.
+ */
+static MOZ_ALWAYS_INLINE size_type getPrefixLength(const nsACString& aSpec) {
+ // To keep the search bounded, look at 64 characters at most. The longest
+ // IANA schemes are ~30, so double that and round up to a nice number.
+ size_type length = std::min(static_cast<size_type>(64), aSpec.Length());
+ for (size_type i = 0; i < length; ++i) {
+ if (aSpec[i] == static_cast<char_type>(':')) {
+ // Found the ':'. Now skip past "//", if present.
+ if (i + 2 < aSpec.Length() &&
+ aSpec[i + 1] == static_cast<char_type>('/') &&
+ aSpec[i + 2] == static_cast<char_type>('/')) {
+ i += 2;
+ }
+ return i + 1;
+ }
+ }
+ return 0;
+}
+
+/**
+ * Gets the index in a URI spec of the host and port substring and optionally
+ * its length.
+ *
+ * Examples:
+ *
+ * http://example.com/
+ * ~~~~~~~~~~~
+ * => index == 7, length == 11
+ *
+ * http://example.com:8888/
+ * ~~~~~~~~~~~~~~~~
+ * => index == 7, length == 16
+ *
+ * http://user:pass@example.com/
+ * ~~~~~~~~~~~
+ * => index == 17, length == 11
+ *
+ * foo:example
+ * ~~~~~~~
+ * => index == 4, length == 7
+ *
+ * not a spec
+ * ~~~~~~~~~~
+ * => index == 0, length == 10
+ *
+ * @param aSpec
+ * A URI spec, or a string that may be a URI spec.
+ * @param _hostAndPortLength
+ * The length of the host and port substring is returned through this
+ * param. Pass null if you don't care.
+ * @return The length of the host and port substring in the spec. If aSpec
+ * doesn't look like a URI, then the entire aSpec is assumed to be a
+ * "host and port", and this returns 0, and _hostAndPortLength will be
+ * the length of aSpec.
+ */
+static MOZ_ALWAYS_INLINE size_type
+indexOfHostAndPort(const nsACString& aSpec, size_type* _hostAndPortLength) {
+ size_type index = getPrefixLength(aSpec);
+ size_type i = index;
+ for (; i < aSpec.Length(); ++i) {
+ // RFC 3986 (URIs): The origin ("authority") is terminated by '/', '?', or
+ // '#' (or the end of the URI).
+ if (aSpec[i] == static_cast<char_type>('/') ||
+ aSpec[i] == static_cast<char_type>('?') ||
+ aSpec[i] == static_cast<char_type>('#')) {
+ break;
+ }
+ // RFC 3986: '@' marks the end of the userinfo component.
+ if (aSpec[i] == static_cast<char_type>('@')) {
+ index = i + 1;
+ }
+ }
+ if (_hostAndPortLength) {
+ *_hostAndPortLength = i - index;
+ }
+ return index;
+}
+
+} // End anonymous namespace
+
+namespace mozilla::places {
+
+////////////////////////////////////////////////////////////////////////////////
+//// AutoComplete Matching Function
+
+/* static */
+nsresult MatchAutoCompleteFunction::create(mozIStorageConnection* aDBConn) {
+ RefPtr<MatchAutoCompleteFunction> function = new MatchAutoCompleteFunction();
+
+ nsresult rv = aDBConn->CreateFunction("autocomplete_match"_ns,
+ kArgIndexLength, function);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+/* static */
+nsDependentCSubstring MatchAutoCompleteFunction::fixupURISpec(
+ const nsACString& aURISpec, int32_t aMatchBehavior, nsACString& aSpecBuf) {
+ nsDependentCSubstring fixedSpec;
+
+ // Try to unescape the string. If that succeeds and yields a different
+ // string which is also valid UTF-8, we'll use it.
+ // Otherwise, we will simply use our original string.
+ bool unescaped =
+ NS_UnescapeURL(aURISpec.BeginReading(), (int32_t)aURISpec.Length(),
+ esc_SkipControl, aSpecBuf);
+ if (unescaped && IsUtf8(aSpecBuf)) {
+ fixedSpec.Rebind(aSpecBuf, 0);
+ } else {
+ fixedSpec.Rebind(aURISpec, 0);
+ }
+
+ if (aMatchBehavior == mozIPlacesAutoComplete::MATCH_ANYWHERE_UNMODIFIED) {
+ return fixedSpec;
+ }
+
+ if (StringBeginsWith(fixedSpec, "http://"_ns)) {
+ fixedSpec.Rebind(fixedSpec, 7);
+ } else if (StringBeginsWith(fixedSpec, "https://"_ns)) {
+ fixedSpec.Rebind(fixedSpec, 8);
+ } else if (StringBeginsWith(fixedSpec, "ftp://"_ns)) {
+ fixedSpec.Rebind(fixedSpec, 6);
+ }
+
+ return fixedSpec;
+}
+
+/* static */
+bool MatchAutoCompleteFunction::findAnywhere(
+ const nsDependentCSubstring& aToken, const nsACString& aSourceString) {
+ // We can't use FindInReadable here; it works only for ASCII.
+
+ return findInString(aToken, aSourceString, eFindAnywhere);
+}
+
+/* static */
+bool MatchAutoCompleteFunction::findOnBoundary(
+ const nsDependentCSubstring& aToken, const nsACString& aSourceString) {
+ return findInString(aToken, aSourceString, eFindOnBoundary);
+}
+
+/* static */
+MatchAutoCompleteFunction::searchFunctionPtr
+MatchAutoCompleteFunction::getSearchFunction(int32_t aBehavior) {
+ switch (aBehavior) {
+ case mozIPlacesAutoComplete::MATCH_ANYWHERE:
+ case mozIPlacesAutoComplete::MATCH_ANYWHERE_UNMODIFIED:
+ return findAnywhere;
+ case mozIPlacesAutoComplete::MATCH_BOUNDARY:
+ default:
+ return findOnBoundary;
+ };
+}
+
+NS_IMPL_ISUPPORTS(MatchAutoCompleteFunction, mozIStorageFunction)
+
+MatchAutoCompleteFunction::MatchAutoCompleteFunction()
+ : mCachedZero(new IntegerVariant(0)), mCachedOne(new IntegerVariant(1)) {
+ static_assert(IntegerVariant::HasThreadSafeRefCnt::value,
+ "Caching assumes that variants have thread-safe refcounting");
+}
+
+NS_IMETHODIMP
+MatchAutoCompleteFunction::OnFunctionCall(mozIStorageValueArray* aArguments,
+ nsIVariant** _result) {
+ // Macro to make the code a bit cleaner and easier to read. Operates on
+ // searchBehavior.
+ int32_t searchBehavior = aArguments->AsInt32(kArgIndexSearchBehavior);
+#define HAS_BEHAVIOR(aBitName) \
+ (searchBehavior & mozIPlacesAutoComplete::BEHAVIOR_##aBitName)
+
+ nsDependentCString searchString =
+ getSharedUTF8String(aArguments, kArgSearchString);
+ nsDependentCString url = getSharedUTF8String(aArguments, kArgIndexURL);
+
+ int32_t matchBehavior = aArguments->AsInt32(kArgIndexMatchBehavior);
+
+ // We only want to filter javascript: URLs if we are not supposed to search
+ // for them, and the search does not start with "javascript:".
+ if (matchBehavior != mozIPlacesAutoComplete::MATCH_ANYWHERE_UNMODIFIED &&
+ StringBeginsWith(url, "javascript:"_ns) && !HAS_BEHAVIOR(JAVASCRIPT) &&
+ !StringBeginsWith(searchString, "javascript:"_ns)) {
+ *_result = do_AddRef(mCachedZero).take();
+ return NS_OK;
+ }
+
+ int32_t visitCount = aArguments->AsInt32(kArgIndexVisitCount);
+ // Filtering on typed is no more used by Firefox, it is still being used by
+ // comm-central clients.
+ bool typed = aArguments->AsInt32(kArgIndexTyped) != 0;
+ bool bookmark = aArguments->AsInt32(kArgIndexBookmark) != 0;
+ nsDependentCString tags = getSharedUTF8String(aArguments, kArgIndexTags);
+ int32_t openPageCount = aArguments->AsInt32(kArgIndexOpenPageCount);
+ bool matches = false;
+ if (HAS_BEHAVIOR(RESTRICT)) {
+ // Make sure we match all the filter requirements. If a given restriction
+ // is active, make sure the corresponding condition is not true.
+ matches = (!HAS_BEHAVIOR(HISTORY) || visitCount > 0) &&
+ (!HAS_BEHAVIOR(TYPED) || typed) &&
+ (!HAS_BEHAVIOR(BOOKMARK) || bookmark) &&
+ (!HAS_BEHAVIOR(TAG) || !tags.IsVoid()) &&
+ (!HAS_BEHAVIOR(OPENPAGE) || openPageCount > 0);
+ } else {
+ // Make sure that we match all the filter requirements and that the
+ // corresponding condition is true if at least a given restriction is
+ // active.
+ matches = (HAS_BEHAVIOR(HISTORY) && visitCount > 0) ||
+ (HAS_BEHAVIOR(TYPED) && typed) ||
+ (HAS_BEHAVIOR(BOOKMARK) && bookmark) ||
+ (HAS_BEHAVIOR(TAG) && !tags.IsVoid()) ||
+ (HAS_BEHAVIOR(OPENPAGE) && openPageCount > 0);
+ }
+
+ if (!matches) {
+ *_result = do_AddRef(mCachedZero).take();
+ return NS_OK;
+ }
+
+ // Obtain our search function.
+ searchFunctionPtr searchFunction = getSearchFunction(matchBehavior);
+
+ // Clean up our URI spec and prepare it for searching.
+ nsCString fixedUrlBuf;
+ nsDependentCSubstring fixedUrl =
+ fixupURISpec(url, matchBehavior, fixedUrlBuf);
+ // Limit the number of chars we search through.
+ const nsDependentCSubstring& trimmedUrl =
+ Substring(fixedUrl, 0, MAX_CHARS_TO_SEARCH_THROUGH);
+
+ nsDependentCString title = getSharedUTF8String(aArguments, kArgIndexTitle);
+ // Limit the number of chars we search through.
+ const nsDependentCSubstring& trimmedTitle =
+ Substring(title, 0, MAX_CHARS_TO_SEARCH_THROUGH);
+
+ // Caller may pass a fallback title, for example in case of bookmarks or
+ // snapshots, one may want to search both the user provided title and the
+ // history one.
+ nsDependentCString fallbackTitle =
+ getSharedUTF8String(aArguments, kArgIndexFallbackTitle);
+ // Limit the number of chars we search through.
+ const nsDependentCSubstring& trimmedFallbackTitle =
+ Substring(fallbackTitle, 0, MAX_CHARS_TO_SEARCH_THROUGH);
+
+ // Determine if every token matches either the bookmark title, tags, page
+ // title, or page URL.
+ nsCWhitespaceTokenizer tokenizer(searchString);
+ while (matches && tokenizer.hasMoreTokens()) {
+ const nsDependentCSubstring& token = tokenizer.nextToken();
+
+ if (HAS_BEHAVIOR(TITLE) && HAS_BEHAVIOR(URL)) {
+ matches = (searchFunction(token, trimmedTitle) ||
+ searchFunction(token, trimmedFallbackTitle) ||
+ searchFunction(token, tags)) &&
+ searchFunction(token, trimmedUrl);
+ } else if (HAS_BEHAVIOR(TITLE)) {
+ matches = searchFunction(token, trimmedTitle) ||
+ searchFunction(token, trimmedFallbackTitle) ||
+ searchFunction(token, tags);
+ } else if (HAS_BEHAVIOR(URL)) {
+ matches = searchFunction(token, trimmedUrl);
+ } else {
+ matches = searchFunction(token, trimmedTitle) ||
+ searchFunction(token, trimmedFallbackTitle) ||
+ searchFunction(token, tags) ||
+ searchFunction(token, trimmedUrl);
+ }
+ }
+
+ *_result = do_AddRef(matches ? mCachedOne : mCachedZero).take();
+ return NS_OK;
+#undef HAS_BEHAVIOR
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// Frecency Calculation Function
+
+/* static */
+nsresult CalculateFrecencyFunction::create(mozIStorageConnection* aDBConn) {
+ RefPtr<CalculateFrecencyFunction> function = new CalculateFrecencyFunction();
+
+ nsresult rv = aDBConn->CreateFunction("calculate_frecency"_ns, -1, function);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(CalculateFrecencyFunction, mozIStorageFunction)
+
+NS_IMETHODIMP
+CalculateFrecencyFunction::OnFunctionCall(mozIStorageValueArray* aArguments,
+ nsIVariant** _result) {
+ // Fetch arguments. Use default values if they were omitted.
+ uint32_t numEntries;
+ nsresult rv = aArguments->GetNumEntries(&numEntries);
+ NS_ENSURE_SUCCESS(rv, rv);
+ MOZ_ASSERT(numEntries <= 2, "unexpected number of arguments");
+
+ int64_t pageId = aArguments->AsInt64(0);
+ MOZ_ASSERT(pageId > 0, "Should always pass a valid page id");
+ if (pageId <= 0) {
+ *_result = MakeAndAddRef<IntegerVariant>(0).take();
+ return NS_OK;
+ }
+
+ enum RedirectBonus { eUnknown, eRedirect, eNormal };
+
+ RedirectBonus mostRecentVisitBonus = eUnknown;
+
+ if (numEntries > 1) {
+ mostRecentVisitBonus = aArguments->AsInt32(1) ? eRedirect : eNormal;
+ }
+
+ int32_t typed = 0;
+ int32_t visitCount = 0;
+ PRTime mostRecentBookmarkTime = 0;
+ int32_t isQuery = 0;
+ float pointsForSampledVisits = 0.0f;
+ int32_t numSampledVisits = 0;
+ int32_t bonus = 0;
+
+ // This is a const version of the history object for thread-safety.
+ const nsNavHistory* history = nsNavHistory::GetConstHistoryService();
+ NS_ENSURE_STATE(history);
+ RefPtr<Database> DB = Database::GetDatabase();
+ NS_ENSURE_STATE(DB);
+
+ // Fetch the page stats from the database.
+ {
+ nsCOMPtr<mozIStorageStatement> getPageInfo = DB->GetStatement(
+ "SELECT typed, visit_count, MAX(dateAdded), "
+ "(substr(url, 0, 7) = 'place:') "
+ "FROM moz_places h "
+ "LEFT JOIN moz_bookmarks ON fk = h.id "
+ "WHERE h.id = :page_id");
+ NS_ENSURE_STATE(getPageInfo);
+ mozStorageStatementScoper infoScoper(getPageInfo);
+
+ rv = getPageInfo->BindInt64ByName("page_id"_ns, pageId);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasResult = false;
+ rv = getPageInfo->ExecuteStep(&hasResult);
+ NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && hasResult, NS_ERROR_UNEXPECTED);
+
+ rv = getPageInfo->GetInt32(0, &typed);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = getPageInfo->GetInt32(1, &visitCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = getPageInfo->GetInt64(2, &mostRecentBookmarkTime);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = getPageInfo->GetInt32(3, &isQuery);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (visitCount > 0) {
+ // Get a sample of the last visits to the page, to calculate its weight.
+ // In case the visit is a redirect target, calculate the frecency
+ // as if the original page was visited.
+ // If it's a redirect source, we may want to use a lower bonus.
+ nsCString redirectsTransitionFragment = nsPrintfCString(
+ "%d AND %d ", nsINavHistoryService::TRANSITION_REDIRECT_PERMANENT,
+ nsINavHistoryService::TRANSITION_REDIRECT_TEMPORARY);
+ nsCOMPtr<mozIStorageStatement> getVisits = DB->GetStatement(
+ nsLiteralCString(
+ "/* do not warn (bug 659740 - SQLite may ignore index if few "
+ "visits exist) */"
+ "SELECT "
+ "IFNULL(origin.visit_type, v.visit_type) AS visit_type, "
+ "target.visit_type AS target_visit_type, "
+ "ROUND((strftime('%s','now','localtime','utc') - "
+ "v.visit_date/1000000)/86400) AS age_in_days, "
+ "v.source AS visit_source "
+ "FROM moz_historyvisits v "
+ "LEFT JOIN moz_historyvisits origin ON origin.id = v.from_visit "
+ "AND v.visit_type BETWEEN ") +
+ redirectsTransitionFragment +
+ nsLiteralCString(
+ "LEFT JOIN moz_historyvisits target ON v.id = target.from_visit "
+ "AND target.visit_type BETWEEN ") +
+ redirectsTransitionFragment +
+ nsLiteralCString("WHERE v.place_id = :page_id "
+ "ORDER BY v.visit_date DESC "
+ "LIMIT :max_visits "));
+ NS_ENSURE_STATE(getVisits);
+ mozStorageStatementScoper visitsScoper(getVisits);
+ rv = getVisits->BindInt64ByName("page_id"_ns, pageId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = getVisits->BindInt32ByName("max_visits"_ns,
+ history->GetNumVisitsForFrecency());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Fetch only a limited number of recent visits.
+ bool hasResult = false;
+ while (NS_SUCCEEDED(getVisits->ExecuteStep(&hasResult)) && hasResult) {
+ // If this is a redirect target, we'll use the visitType of the source,
+ // otherwise the actual visitType.
+ int32_t visitType = getVisits->AsInt32(0);
+
+ // When adding a new visit, we should haved passed-in whether we should
+ // use the redirect bonus. We can't fetch this information from the
+ // database, because we only store redirect targets.
+ // For older visits we extract the value from the database.
+ bool useRedirectBonus = mostRecentVisitBonus == eRedirect;
+ if (mostRecentVisitBonus == eUnknown || numSampledVisits > 0) {
+ int32_t targetVisitType = getVisits->AsInt32(1);
+ useRedirectBonus =
+ targetVisitType ==
+ nsINavHistoryService::TRANSITION_REDIRECT_PERMANENT ||
+ (targetVisitType ==
+ nsINavHistoryService::TRANSITION_REDIRECT_TEMPORARY &&
+ visitType != nsINavHistoryService::TRANSITION_TYPED);
+ }
+
+ uint32_t visitSource = getVisits->AsInt32(3);
+ if (mostRecentBookmarkTime) {
+ // For bookmarked visit, add full bonus.
+ bonus = history->GetFrecencyTransitionBonus(visitType, true,
+ useRedirectBonus);
+ bonus += history->GetFrecencyTransitionBonus(
+ nsINavHistoryService::TRANSITION_BOOKMARK, true);
+ } else if (visitSource == nsINavHistoryService::VISIT_SOURCE_ORGANIC) {
+ bonus = history->GetFrecencyTransitionBonus(visitType, true,
+ useRedirectBonus);
+ } else if (visitSource == nsINavHistoryService::VISIT_SOURCE_SEARCHED) {
+ bonus = history->GetFrecencyTransitionBonus(
+ nsINavHistoryService::TRANSITION_LINK, true, useRedirectBonus);
+ }
+
+ // If bonus was zero, we can skip the work to determine the weight.
+ if (bonus) {
+ int32_t ageInDays = getVisits->AsInt32(2);
+ int32_t weight = history->GetFrecencyAgedWeight(ageInDays);
+ pointsForSampledVisits += ((float)weight * ((float)bonus / 100.0f));
+ }
+
+ numSampledVisits++;
+ }
+ }
+
+ // If we sampled some visits for this page, use the calculated weight.
+ if (numSampledVisits) {
+ // We were unable to calculate points, maybe cause all the visits in the
+ // sample had a zero bonus. Though, we know the page has some past valid
+ // visit, or visit_count would be zero. Thus we set the frecency to
+ // -1, so they are still shown in autocomplete.
+ if (pointsForSampledVisits == 0.0f) {
+ *_result = MakeAndAddRef<IntegerVariant>(-1).take();
+ } else {
+ // Estimate frecency using the sampled visits.
+ // Use ceilf() so that we don't round down to 0, which
+ // would cause us to completely ignore the place during autocomplete.
+ *_result =
+ MakeAndAddRef<IntegerVariant>(
+ (int32_t)ceilf((float)visitCount * ceilf(pointsForSampledVisits) /
+ (float)numSampledVisits))
+ .take();
+ }
+ return NS_OK;
+ }
+
+ // Otherwise this page has no visits, it may be bookmarked.
+ if (!mostRecentBookmarkTime || isQuery) {
+ *_result = MakeAndAddRef<IntegerVariant>(0).take();
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(bonus == 0, "Pages should arrive here with 0 bonus");
+ MOZ_ASSERT(mostRecentBookmarkTime > 0, "This should be a bookmarked page");
+
+ // For unvisited bookmarks, produce a non-zero frecency, so that they show
+ // up in URL bar autocomplete.
+ // Make it so something bookmarked and typed will have a higher frecency
+ // than something just typed or just bookmarked.
+ bonus += history->GetFrecencyTransitionBonus(
+ nsINavHistoryService::TRANSITION_BOOKMARK, false);
+ if (typed) {
+ bonus += history->GetFrecencyTransitionBonus(
+ nsINavHistoryService::TRANSITION_TYPED, false);
+ }
+
+ // Use an appropriate bucket depending on the bookmark creation date.
+ int32_t bookmarkAgeInDays =
+ static_cast<int32_t>((PR_Now() - mostRecentBookmarkTime) /
+ ((PRTime)SECONDS_PER_DAY * (PRTime)PR_USEC_PER_SEC));
+
+ pointsForSampledVisits =
+ (float)history->GetFrecencyAgedWeight(bookmarkAgeInDays) *
+ ((float)bonus / 100.0f);
+
+ // use ceilf() so that we don't round down to 0, which
+ // would cause us to completely ignore the place during autocomplete
+ *_result =
+ MakeAndAddRef<IntegerVariant>((int32_t)ceilf(pointsForSampledVisits))
+ .take();
+
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// Frecency Calculation Function
+
+/* static */
+nsresult CalculateAltFrecencyFunction::create(mozIStorageConnection* aDBConn) {
+ RefPtr<CalculateAltFrecencyFunction> function =
+ new CalculateAltFrecencyFunction();
+
+ nsresult rv =
+ aDBConn->CreateFunction("calculate_alt_frecency"_ns, -1, function);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(CalculateAltFrecencyFunction, mozIStorageFunction)
+
+NS_IMETHODIMP
+CalculateAltFrecencyFunction::OnFunctionCall(mozIStorageValueArray* aArguments,
+ nsIVariant** _result) {
+ // Fetch arguments. Use default values if they were omitted.
+ uint32_t numEntries;
+ nsresult rv = aArguments->GetNumEntries(&numEntries);
+ NS_ENSURE_SUCCESS(rv, rv);
+ MOZ_ASSERT(numEntries <= 2, "unexpected number of arguments");
+
+ int64_t pageId = aArguments->AsInt64(0);
+ MOZ_ASSERT(pageId > 0, "Should always pass a valid page id");
+ if (pageId <= 0) {
+ *_result = MakeAndAddRef<IntegerVariant>(0).take();
+ return NS_OK;
+ }
+
+ int32_t isRedirect = 0;
+ if (numEntries > 1) {
+ isRedirect = aArguments->AsInt32(1);
+ }
+ // This is a const version of the history object for thread-safety.
+ const nsNavHistory* history = nsNavHistory::GetConstHistoryService();
+ NS_ENSURE_STATE(history);
+ RefPtr<Database> DB = Database::GetDatabase();
+ NS_ENSURE_STATE(DB);
+
+ /*
+ Exponentially decay each visit with an half-life of halfLifeDays.
+ Score per each visit is a weight exponentially decayed depending on how
+ far away is from a reference date, that is the most recent visit date.
+ The weight for each visit is assigned depending on the visit type and other
+ information (bookmarked, a redirect, a typed entry).
+ If a page has no visits, consider a single visit with an high weight and
+ decay its score using the bookmark date as reference time.
+ Frecency is the sum of all the scores / number of samples.
+ The final score is further decayed using the same half-life.
+ To avoid having to decay the score manually, the stored value is the number
+ of days after which the score would become 1.
+
+ TODO: Add reference link to source docs here.
+ */
+ nsCOMPtr<mozIStorageStatement> stmt = DB->GetStatement(
+ "WITH "
+ "lambda (lambda) AS ( "
+ " SELECT ln(2) / :halfLifeDays "
+ "), "
+ "visits (days, weight) AS ( "
+ " SELECT "
+ " v.visit_date / 86400000000, "
+ " (SELECT CASE "
+ " WHEN IFNULL(s.visit_type, v.visit_type) = 3 " // from a bookmark
+ " OR v.source = 2 " // is a bookmark
+ " OR ( IFNULL(s.visit_type, v.visit_type) = 2 " // is typed
+ " AND v.source <> 3 " // not a search
+ " AND t.id IS NULL AND NOT :isRedirect " // not a redirect
+ " ) "
+ " THEN :highWeight "
+ " WHEN t.id IS NULL AND NOT :isRedirect " // not a redirect
+ " AND IFNULL(s.visit_type, v.visit_type) NOT IN (4, 8, 9) "
+ " THEN :mediumWeight "
+ " ELSE :lowWeight "
+ " END) "
+ " FROM moz_historyvisits v "
+ // If it's a redirect target, use the visit_type of the source.
+ " LEFT JOIN moz_historyvisits s ON s.id = v.from_visit "
+ " AND v.visit_type IN (5,6) "
+ // If it's a redirect, use a low weight.
+ " LEFT JOIN moz_historyvisits t ON t.from_visit = v.id "
+ " AND t.visit_type IN (5,6) "
+ " WHERE v.place_id = :pageId "
+ " ORDER BY v.visit_date DESC "
+ " LIMIT :numSampledVisits "
+ "), "
+ "bookmark (days, weight) AS ( "
+ " SELECT dateAdded / 86400000000, 100 "
+ " FROM moz_bookmarks "
+ " WHERE fk = :pageId "
+ " ORDER BY dateAdded DESC "
+ " LIMIT 1 "
+ "), "
+ "samples (days, weight) AS ( "
+ " SELECT * FROM bookmark WHERE (SELECT count(*) FROM visits) = 0 "
+ " UNION ALL "
+ " SELECT * FROM visits "
+ "), "
+ "reference (days, samples_count) AS ( "
+ " SELECT max(samples.days), count(*) FROM samples "
+ "), "
+ "scores (score) AS ( "
+ " SELECT (weight * exp(-lambda * (samples.days - reference.days))) "
+ " FROM samples, reference, lambda "
+ ") "
+ "SELECT CASE "
+ "WHEN (substr(url, 0, 7) = 'place:') THEN 0 "
+ "ELSE "
+ " reference.days + CAST (( "
+ " ln( "
+ " sum(score) / samples_count * MAX(visit_count, samples_count) "
+ " ) / lambda "
+ " ) AS INTEGER) "
+ "END "
+ "FROM moz_places h, reference, lambda, scores "
+ "WHERE h.id = :pageId");
+ NS_ENSURE_STATE(stmt);
+ mozStorageStatementScoper infoScoper(stmt);
+
+ rv = stmt->BindInt64ByName("pageId"_ns, pageId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByName("isRedirect"_ns, isRedirect);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByName(
+ "halfLifeDays"_ns,
+ StaticPrefs::places_frecency_pages_alternative_halfLifeDays_AtStartup());
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByName(
+ "numSampledVisits"_ns,
+ StaticPrefs::
+ places_frecency_pages_alternative_numSampledVisits_AtStartup());
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByName(
+ "lowWeight"_ns,
+ StaticPrefs::places_frecency_pages_alternative_lowWeight_AtStartup());
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByName(
+ "mediumWeight"_ns,
+ StaticPrefs::places_frecency_pages_alternative_mediumWeight_AtStartup());
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = stmt->BindInt64ByName(
+ "highWeight"_ns,
+ StaticPrefs::places_frecency_pages_alternative_highWeight_AtStartup());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasResult = false;
+ rv = stmt->ExecuteStep(&hasResult);
+ NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && hasResult, NS_ERROR_UNEXPECTED);
+
+ bool isNull;
+ if (NS_SUCCEEDED(stmt->GetIsNull(0, &isNull)) && isNull) {
+ *_result = MakeAndAddRef<NullVariant>().take();
+ } else {
+ int32_t score;
+ rv = stmt->GetInt32(0, &score);
+ NS_ENSURE_SUCCESS(rv, rv);
+ *_result = MakeAndAddRef<IntegerVariant>(score).take();
+ }
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// GUID Creation Function
+
+/* static */
+nsresult GenerateGUIDFunction::create(mozIStorageConnection* aDBConn) {
+ RefPtr<GenerateGUIDFunction> function = new GenerateGUIDFunction();
+ nsresult rv = aDBConn->CreateFunction("generate_guid"_ns, 0, function);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(GenerateGUIDFunction, mozIStorageFunction)
+
+NS_IMETHODIMP
+GenerateGUIDFunction::OnFunctionCall(mozIStorageValueArray* aArguments,
+ nsIVariant** _result) {
+ nsAutoCString guid;
+ nsresult rv = GenerateGUID(guid);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *_result = MakeAndAddRef<UTF8TextVariant>(guid).take();
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// GUID Validation Function
+
+/* static */
+nsresult IsValidGUIDFunction::create(mozIStorageConnection* aDBConn) {
+ RefPtr<IsValidGUIDFunction> function = new IsValidGUIDFunction();
+ return aDBConn->CreateFunction("is_valid_guid"_ns, 1, function);
+}
+
+NS_IMPL_ISUPPORTS(IsValidGUIDFunction, mozIStorageFunction)
+
+NS_IMETHODIMP
+IsValidGUIDFunction::OnFunctionCall(mozIStorageValueArray* aArguments,
+ nsIVariant** _result) {
+ // Must have non-null function arguments.
+ MOZ_ASSERT(aArguments);
+
+ nsAutoCString guid;
+ aArguments->GetUTF8String(0, guid);
+
+ RefPtr<nsVariant> result = new nsVariant();
+ result->SetAsBool(IsValidGUID(guid));
+ result.forget(_result);
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// Get Unreversed Host Function
+
+/* static */
+nsresult GetUnreversedHostFunction::create(mozIStorageConnection* aDBConn) {
+ RefPtr<GetUnreversedHostFunction> function = new GetUnreversedHostFunction();
+ nsresult rv = aDBConn->CreateFunction("get_unreversed_host"_ns, 1, function);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(GetUnreversedHostFunction, mozIStorageFunction)
+
+NS_IMETHODIMP
+GetUnreversedHostFunction::OnFunctionCall(mozIStorageValueArray* aArguments,
+ nsIVariant** _result) {
+ // Must have non-null function arguments.
+ MOZ_ASSERT(aArguments);
+
+ nsAutoString src;
+ aArguments->GetString(0, src);
+
+ RefPtr<nsVariant> result = new nsVariant();
+
+ if (src.Length() > 1) {
+ src.Truncate(src.Length() - 1);
+ nsAutoString dest;
+ ReverseString(src, dest);
+ result->SetAsAString(dest);
+ } else {
+ result->SetAsAString(u""_ns);
+ }
+ result.forget(_result);
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// Fixup URL Function
+
+/* static */
+nsresult FixupURLFunction::create(mozIStorageConnection* aDBConn) {
+ RefPtr<FixupURLFunction> function = new FixupURLFunction();
+ nsresult rv = aDBConn->CreateFunction("fixup_url"_ns, 1, function);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(FixupURLFunction, mozIStorageFunction)
+
+NS_IMETHODIMP
+FixupURLFunction::OnFunctionCall(mozIStorageValueArray* aArguments,
+ nsIVariant** _result) {
+ // Must have non-null function arguments.
+ MOZ_ASSERT(aArguments);
+
+ nsAutoString src;
+ aArguments->GetString(0, src);
+
+ RefPtr<nsVariant> result = new nsVariant();
+
+ if (StringBeginsWith(src, u"http://"_ns)) {
+ src.Cut(0, 7);
+ } else if (StringBeginsWith(src, u"https://"_ns)) {
+ src.Cut(0, 8);
+ } else if (StringBeginsWith(src, u"ftp://"_ns)) {
+ src.Cut(0, 6);
+ }
+
+ // Remove common URL hostname prefixes
+ if (StringBeginsWith(src, u"www."_ns)) {
+ src.Cut(0, 4);
+ }
+
+ result->SetAsAString(src);
+ result.forget(_result);
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// Store Last Inserted Id Function
+
+/* static */
+nsresult StoreLastInsertedIdFunction::create(mozIStorageConnection* aDBConn) {
+ RefPtr<StoreLastInsertedIdFunction> function =
+ new StoreLastInsertedIdFunction();
+ nsresult rv =
+ aDBConn->CreateFunction("store_last_inserted_id"_ns, 2, function);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(StoreLastInsertedIdFunction, mozIStorageFunction)
+
+NS_IMETHODIMP
+StoreLastInsertedIdFunction::OnFunctionCall(mozIStorageValueArray* aArgs,
+ nsIVariant** _result) {
+ uint32_t numArgs;
+ nsresult rv = aArgs->GetNumEntries(&numArgs);
+ NS_ENSURE_SUCCESS(rv, rv);
+ MOZ_ASSERT(numArgs == 2);
+
+ nsAutoCString table;
+ rv = aArgs->GetUTF8String(0, table);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int64_t lastInsertedId = aArgs->AsInt64(1);
+
+ MOZ_ASSERT(table.EqualsLiteral("moz_places") ||
+ table.EqualsLiteral("moz_historyvisits") ||
+ table.EqualsLiteral("moz_bookmarks") ||
+ table.EqualsLiteral("moz_icons"));
+
+ if (table.EqualsLiteral("moz_bookmarks")) {
+ nsNavBookmarks::StoreLastInsertedId(table, lastInsertedId);
+ } else if (table.EqualsLiteral("moz_icons")) {
+ nsFaviconService::StoreLastInsertedId(table, lastInsertedId);
+ } else {
+ nsNavHistory::StoreLastInsertedId(table, lastInsertedId);
+ }
+
+ RefPtr<nsVariant> result = new nsVariant();
+ rv = result->SetAsInt64(lastInsertedId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ result.forget(_result);
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// Get Query Param Function
+
+/* static */
+nsresult GetQueryParamFunction::create(mozIStorageConnection* aDBConn) {
+ RefPtr<GetQueryParamFunction> function = new GetQueryParamFunction();
+ return aDBConn->CreateFunction("get_query_param"_ns, 2, function);
+}
+
+NS_IMPL_ISUPPORTS(GetQueryParamFunction, mozIStorageFunction)
+
+NS_IMETHODIMP
+GetQueryParamFunction::OnFunctionCall(mozIStorageValueArray* aArguments,
+ nsIVariant** _result) {
+ // Must have non-null function arguments.
+ MOZ_ASSERT(aArguments);
+
+ nsDependentCString queryString = getSharedUTF8String(aArguments, 0);
+ nsDependentCString paramName = getSharedUTF8String(aArguments, 1);
+
+ RefPtr<nsVariant> result = new nsVariant();
+ if (!queryString.IsEmpty() && !paramName.IsEmpty()) {
+ URLParams::Parse(
+ queryString,
+ [&paramName, &result](const nsAString& aName, const nsAString& aValue) {
+ NS_ConvertUTF16toUTF8 name(aName);
+ if (!paramName.Equals(name)) {
+ return true;
+ }
+ result->SetAsAString(aValue);
+ return false;
+ });
+ }
+
+ result.forget(_result);
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// Hash Function
+
+/* static */
+nsresult HashFunction::create(mozIStorageConnection* aDBConn) {
+ RefPtr<HashFunction> function = new HashFunction();
+ return aDBConn->CreateFunction("hash"_ns, -1, function);
+}
+
+NS_IMPL_ISUPPORTS(HashFunction, mozIStorageFunction)
+
+NS_IMETHODIMP
+HashFunction::OnFunctionCall(mozIStorageValueArray* aArguments,
+ nsIVariant** _result) {
+ // Must have non-null function arguments.
+ MOZ_ASSERT(aArguments);
+
+ // Fetch arguments. Use default values if they were omitted.
+ uint32_t numEntries;
+ nsresult rv = aArguments->GetNumEntries(&numEntries);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(numEntries >= 1 && numEntries <= 2, NS_ERROR_FAILURE);
+
+ nsDependentCString str = getSharedUTF8String(aArguments, 0);
+ nsAutoCString mode;
+ if (numEntries > 1) {
+ aArguments->GetUTF8String(1, mode);
+ }
+
+ RefPtr<nsVariant> result = new nsVariant();
+ uint64_t hash;
+ rv = mozilla::places::HashURL(str, mode, &hash);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = result->SetAsInt64((int64_t)hash);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ result.forget(_result);
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// MD5 Function
+
+/* static */
+nsresult MD5HexFunction::create(mozIStorageConnection* aDBConn) {
+ RefPtr<MD5HexFunction> function = new MD5HexFunction();
+ return aDBConn->CreateFunction("md5hex"_ns, -1, function);
+}
+
+NS_IMPL_ISUPPORTS(MD5HexFunction, mozIStorageFunction)
+
+NS_IMETHODIMP
+MD5HexFunction::OnFunctionCall(mozIStorageValueArray* aArguments,
+ nsIVariant** _result) {
+ // Must have non-null function arguments.
+ MOZ_ASSERT(aArguments);
+
+ // Fetch arguments.
+ uint32_t numEntries;
+ nsresult rv = aArguments->GetNumEntries(&numEntries);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(numEntries == 1, NS_ERROR_FAILURE);
+ nsDependentCString str = getSharedUTF8String(aArguments, 0);
+
+ nsCOMPtr<nsICryptoHash> hasher =
+ do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // MD5 is not a secure hash function, but it's ok for this use.
+ rv = hasher->Init(nsICryptoHash::MD5);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = hasher->Update(reinterpret_cast<const uint8_t*>(str.BeginReading()),
+ str.Length());
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsAutoCString binaryHash, hashString;
+ rv = hasher->Finish(false, binaryHash);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Convert to HEX.
+ static const char* const hex = "0123456789abcdef";
+ hashString.SetCapacity(2 * binaryHash.Length());
+ for (size_t i = 0; i < binaryHash.Length(); ++i) {
+ auto c = static_cast<unsigned char>(binaryHash[i]);
+ hashString.Append(hex[(c >> 4) & 0x0F]);
+ hashString.Append(hex[c & 0x0F]);
+ }
+
+ RefPtr<nsVariant> result = new nsVariant();
+ result->SetAsACString(hashString);
+ result.forget(_result);
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// Get prefix function
+
+/* static */
+nsresult GetPrefixFunction::create(mozIStorageConnection* aDBConn) {
+ RefPtr<GetPrefixFunction> function = new GetPrefixFunction();
+ nsresult rv = aDBConn->CreateFunction("get_prefix"_ns, 1, function);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(GetPrefixFunction, mozIStorageFunction)
+
+NS_IMETHODIMP
+GetPrefixFunction::OnFunctionCall(mozIStorageValueArray* aArgs,
+ nsIVariant** _result) {
+ MOZ_ASSERT(aArgs);
+
+ uint32_t numArgs;
+ nsresult rv = aArgs->GetNumEntries(&numArgs);
+ NS_ENSURE_SUCCESS(rv, rv);
+ MOZ_ASSERT(numArgs == 1);
+
+ nsDependentCString spec(getSharedUTF8String(aArgs, 0));
+
+ RefPtr<nsVariant> result = new nsVariant();
+ result->SetAsACString(Substring(spec, 0, getPrefixLength(spec)));
+ result.forget(_result);
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// Get host and port function
+
+/* static */
+nsresult GetHostAndPortFunction::create(mozIStorageConnection* aDBConn) {
+ RefPtr<GetHostAndPortFunction> function = new GetHostAndPortFunction();
+ nsresult rv = aDBConn->CreateFunction("get_host_and_port"_ns, 1, function);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(GetHostAndPortFunction, mozIStorageFunction)
+
+NS_IMETHODIMP
+GetHostAndPortFunction::OnFunctionCall(mozIStorageValueArray* aArgs,
+ nsIVariant** _result) {
+ MOZ_ASSERT(aArgs);
+
+ uint32_t numArgs;
+ nsresult rv = aArgs->GetNumEntries(&numArgs);
+ NS_ENSURE_SUCCESS(rv, rv);
+ MOZ_ASSERT(numArgs == 1);
+
+ nsDependentCString spec(getSharedUTF8String(aArgs, 0));
+
+ RefPtr<nsVariant> result = new nsVariant();
+
+ size_type length;
+ size_type index = indexOfHostAndPort(spec, &length);
+ result->SetAsACString(Substring(spec, index, length));
+ result.forget(_result);
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// Strip prefix and userinfo function
+
+/* static */
+nsresult StripPrefixAndUserinfoFunction::create(
+ mozIStorageConnection* aDBConn) {
+ RefPtr<StripPrefixAndUserinfoFunction> function =
+ new StripPrefixAndUserinfoFunction();
+ nsresult rv =
+ aDBConn->CreateFunction("strip_prefix_and_userinfo"_ns, 1, function);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(StripPrefixAndUserinfoFunction, mozIStorageFunction)
+
+NS_IMETHODIMP
+StripPrefixAndUserinfoFunction::OnFunctionCall(mozIStorageValueArray* aArgs,
+ nsIVariant** _result) {
+ MOZ_ASSERT(aArgs);
+
+ uint32_t numArgs;
+ nsresult rv = aArgs->GetNumEntries(&numArgs);
+ NS_ENSURE_SUCCESS(rv, rv);
+ MOZ_ASSERT(numArgs == 1);
+
+ nsDependentCString spec(getSharedUTF8String(aArgs, 0));
+
+ RefPtr<nsVariant> result = new nsVariant();
+
+ size_type index = indexOfHostAndPort(spec, nullptr);
+ result->SetAsACString(Substring(spec, index, spec.Length() - index));
+ result.forget(_result);
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// Is frecency decaying function
+
+/* static */
+nsresult IsFrecencyDecayingFunction::create(mozIStorageConnection* aDBConn) {
+ RefPtr<IsFrecencyDecayingFunction> function =
+ new IsFrecencyDecayingFunction();
+ nsresult rv = aDBConn->CreateFunction("is_frecency_decaying"_ns, 0, function);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(IsFrecencyDecayingFunction, mozIStorageFunction)
+
+NS_IMETHODIMP
+IsFrecencyDecayingFunction::OnFunctionCall(mozIStorageValueArray* aArgs,
+ nsIVariant** _result) {
+ MOZ_ASSERT(aArgs);
+
+#ifdef DEBUG
+ uint32_t numArgs;
+ MOZ_ASSERT(NS_SUCCEEDED(aArgs->GetNumEntries(&numArgs)) && numArgs == 0);
+#endif
+
+ RefPtr<nsVariant> result = new nsVariant();
+ nsresult rv = result->SetAsBool(nsNavHistory::sIsFrecencyDecaying);
+ NS_ENSURE_SUCCESS(rv, rv);
+ result.forget(_result);
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// Should start frecency recalculation function
+
+/* static */
+nsresult SetShouldStartFrecencyRecalculationFunction::create(
+ mozIStorageConnection* aDBConn) {
+ RefPtr<SetShouldStartFrecencyRecalculationFunction> function =
+ new SetShouldStartFrecencyRecalculationFunction();
+ nsresult rv = aDBConn->CreateFunction(
+ "set_should_start_frecency_recalculation"_ns, 0, function);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(SetShouldStartFrecencyRecalculationFunction,
+ mozIStorageFunction)
+
+NS_IMETHODIMP
+SetShouldStartFrecencyRecalculationFunction::OnFunctionCall(
+ mozIStorageValueArray* aArgs, nsIVariant** _result) {
+ MOZ_ASSERT(aArgs);
+
+#ifdef DEBUG
+ uint32_t numArgs;
+ MOZ_ASSERT(NS_SUCCEEDED(aArgs->GetNumEntries(&numArgs)) && numArgs == 0);
+#endif
+
+ // When changing from false to true, dispatch a runnable to the main-thread
+ // to start a recalculation. Once there's nothing left to recalculathe this
+ // boolean will be set back to false. Note this means there will be a short
+ // interval between completing a recalculation and setting this back to false
+ // where we could potentially lose a recalculation request. That should not be
+ // a big deal, since the recalculation will just happen at the next operation
+ // changing frecency or, in the worst case, at the next session.
+ if (!nsNavHistory::sShouldStartFrecencyRecalculation.exchange(true)) {
+ mozilla::Unused << NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "SetShouldStartFrecencyRecalculationFunction::Notify", [] {
+ nsCOMPtr<nsIObserverService> os = services::GetObserverService();
+ if (os) {
+ mozilla::Unused << os->NotifyObservers(
+ nullptr, "frecency-recalculation-needed", nullptr);
+ }
+ }));
+ }
+
+ RefPtr<nsVariant> result = new nsVariant();
+ nsresult rv = result->SetAsBool(true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ result.forget(_result);
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// Note Sync Change Function
+
+/* static */
+nsresult NoteSyncChangeFunction::create(mozIStorageConnection* aDBConn) {
+ RefPtr<NoteSyncChangeFunction> function = new NoteSyncChangeFunction();
+ nsresult rv = aDBConn->CreateFunction("note_sync_change"_ns, 0, function);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(NoteSyncChangeFunction, mozIStorageFunction)
+
+NS_IMETHODIMP
+NoteSyncChangeFunction::OnFunctionCall(mozIStorageValueArray* aArgs,
+ nsIVariant** _result) {
+ nsNavBookmarks::NoteSyncChange();
+ *_result = nullptr;
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// Invalidate days of history Function
+
+/* static */
+nsresult InvalidateDaysOfHistoryFunction::create(
+ mozIStorageConnection* aDBConn) {
+ RefPtr<InvalidateDaysOfHistoryFunction> function =
+ new InvalidateDaysOfHistoryFunction();
+ nsresult rv =
+ aDBConn->CreateFunction("invalidate_days_of_history"_ns, 0, function);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(InvalidateDaysOfHistoryFunction, mozIStorageFunction)
+
+NS_IMETHODIMP
+InvalidateDaysOfHistoryFunction::OnFunctionCall(mozIStorageValueArray* aArgs,
+ nsIVariant** _result) {
+ nsNavHistory::InvalidateDaysOfHistory();
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+//// Target folder guid from places query Function
+
+/* static */
+nsresult TargetFolderGuidFunction::create(mozIStorageConnection* aDBConn) {
+ RefPtr<TargetFolderGuidFunction> function = new TargetFolderGuidFunction();
+ nsresult rv = aDBConn->CreateFunction("target_folder_guid"_ns, 1, function);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(TargetFolderGuidFunction, mozIStorageFunction)
+
+NS_IMETHODIMP
+TargetFolderGuidFunction::OnFunctionCall(mozIStorageValueArray* aArguments,
+ nsIVariant** _result) {
+ // Must have non-null function arguments.
+ MOZ_ASSERT(aArguments);
+ // Must have one argument.
+ DebugOnly<uint32_t> numArgs = 0;
+ MOZ_ASSERT(NS_SUCCEEDED(aArguments->GetNumEntries(&numArgs)) && numArgs == 1,
+ "unexpected number of arguments");
+
+ nsDependentCString queryURI = getSharedUTF8String(aArguments, 0);
+ Maybe<nsCString> targetFolderGuid =
+ nsNavHistory::GetTargetFolderGuid(queryURI);
+
+ if (targetFolderGuid.isSome()) {
+ RefPtr<nsVariant> result = new nsVariant();
+ result->SetAsACString(*targetFolderGuid);
+ result.forget(_result);
+ } else {
+ *_result = MakeAndAddRef<NullVariant>().take();
+ }
+
+ return NS_OK;
+}
+
+} // namespace mozilla::places