toolkit/components/places/FaviconHelpers.cpp
author Marco Bonardo <mbonardo@mozilla.com>
Mon, 11 Feb 2019 17:27:29 +0000
changeset 513044 5da23f314b322edfd1ce11a19416dcb78bbf67fb
parent 505383 6f3709b3878117466168c40affa7bca0b60cf75b
child 513335 70a034af6e4b3b25d341baabdf1e59d265e1bd38
child 516645 74f99033251cd6c36eb6b0b5a4713eb6b5c793fa
permissions -rw-r--r--
Bug 1519058 - Some bookmark favicons disappear after clearing history. r=Standard8 a=lizzard Differential Revision: https://phabricator.services.mozilla.com/D19154

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 * 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 "FaviconHelpers.h"

#include "nsICacheEntry.h"
#include "nsICachingChannel.h"
#include "nsIClassOfService.h"
#include "nsIAsyncVerifyRedirectCallback.h"
#include "nsIPrincipal.h"

#include "nsNavHistory.h"
#include "nsFaviconService.h"
#include "mozilla/storage.h"
#include "mozilla/Telemetry.h"
#include "nsNetUtil.h"
#include "nsPrintfCString.h"
#include "nsStreamUtils.h"
#include "nsStringStream.h"
#include "nsIPrivateBrowsingChannel.h"
#include "nsISupportsPriority.h"
#include "nsContentUtils.h"
#include <algorithm>
#include <deque>
#include "mozilla/gfx/2D.h"
#include "imgIContainer.h"
#include "ImageOps.h"
#include "imgIEncoder.h"

using namespace mozilla::places;
using namespace mozilla::storage;

namespace mozilla {
namespace places {

namespace {

/**
 * Fetches information about a page from the database.
 *
 * @param aDB
 *        Database connection to history tables.
 * @param _page
 *        Page that should be fetched.
 */
nsresult FetchPageInfo(const RefPtr<Database>& aDB, PageData& _page) {
  MOZ_ASSERT(_page.spec.Length(), "Must have a non-empty spec!");
  MOZ_ASSERT(!NS_IsMainThread());

  // The subquery finds the bookmarked uri we want to set the icon for,
  // walking up redirects.
  nsCString query = nsPrintfCString(
      "SELECT h.id, pi.id, h.guid, ( "
      "WITH RECURSIVE "
      "destinations(visit_type, from_visit, place_id, rev_host, bm) AS ( "
      "SELECT v.visit_type, v.from_visit, p.id, p.rev_host, b.id "
      "FROM moz_places p  "
      "LEFT JOIN moz_historyvisits v ON v.place_id = p.id  "
      "LEFT JOIN moz_bookmarks b ON b.fk = p.id "
      "WHERE p.id = h.id "
      "UNION "
      "SELECT src.visit_type, src.from_visit, src.place_id, p.rev_host, b.id "
      "FROM moz_places p "
      "JOIN moz_historyvisits src ON src.place_id = p.id "
      "JOIN destinations dest ON dest.from_visit = src.id AND dest.visit_type "
      "IN (%d, %d) "
      "LEFT JOIN moz_bookmarks b ON b.fk = src.place_id "
      "WHERE instr(p.rev_host, dest.rev_host) = 1 "
      "OR instr(dest.rev_host, p.rev_host) = 1 "
      ") "
      "SELECT url "
      "FROM moz_places p "
      "JOIN destinations r ON r.place_id = p.id "
      "WHERE bm NOTNULL "
      "LIMIT 1 "
      "), fixup_url(get_unreversed_host(h.rev_host)) AS host "
      "FROM moz_places h "
      "LEFT JOIN moz_pages_w_icons pi ON page_url_hash = hash(:page_url) AND "
      "page_url = :page_url "
      "WHERE h.url_hash = hash(:page_url) AND h.url = :page_url",
      nsINavHistoryService::TRANSITION_REDIRECT_PERMANENT,
      nsINavHistoryService::TRANSITION_REDIRECT_TEMPORARY);

  nsCOMPtr<mozIStorageStatement> stmt = aDB->GetStatement(query);
  NS_ENSURE_STATE(stmt);
  mozStorageStatementScoper scoper(stmt);

  nsresult rv =
      URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), _page.spec);
  NS_ENSURE_SUCCESS(rv, rv);

  bool hasResult;
  rv = stmt->ExecuteStep(&hasResult);
  NS_ENSURE_SUCCESS(rv, rv);
  if (!hasResult) {
    // The page does not exist.
    return NS_ERROR_NOT_AVAILABLE;
  }

  rv = stmt->GetInt64(0, &_page.placeId);
  NS_ENSURE_SUCCESS(rv, rv);
  // May be null, and in such a case this will be 0.
  _page.id = stmt->AsInt64(1);
  rv = stmt->GetUTF8String(2, _page.guid);
  NS_ENSURE_SUCCESS(rv, rv);
  // Bookmarked url can be nullptr.
  bool isNull;
  rv = stmt->GetIsNull(3, &isNull);
  NS_ENSURE_SUCCESS(rv, rv);
  // The page could not be bookmarked.
  if (!isNull) {
    rv = stmt->GetUTF8String(3, _page.bookmarkedSpec);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  if (_page.host.IsEmpty()) {
    rv = stmt->GetUTF8String(4, _page.host);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  if (!_page.canAddToHistory) {
    // Either history is disabled or the scheme is not supported.  In such a
    // case we want to update the icon only if the page is bookmarked.

    if (_page.bookmarkedSpec.IsEmpty()) {
      // The page is not bookmarked.  Since updating the icon with a disabled
      // history would be a privacy leak, bail out as if the page did not exist.
      return NS_ERROR_NOT_AVAILABLE;
    } else {
      // The page, or a redirect to it, is bookmarked.  If the bookmarked spec
      // is different from the requested one, use it.
      if (!_page.bookmarkedSpec.Equals(_page.spec)) {
        _page.spec = _page.bookmarkedSpec;
        rv = FetchPageInfo(aDB, _page);
        NS_ENSURE_SUCCESS(rv, rv);
      }
    }
  }

  return NS_OK;
}

/**
 * Stores information about an icon in the database.
 *
 * @param aDB
 *        Database connection to history tables.
 * @param aIcon
 *        Icon that should be stored.
 * @param aMustReplace
 *        If set to true, the function will bail out with NS_ERROR_NOT_AVAILABLE
 *        if it can't find a previous stored icon to replace.
 * @note Should be wrapped in a transaction.
 */
nsresult SetIconInfo(const RefPtr<Database>& aDB, IconData& aIcon,
                     bool aMustReplace = false) {
  MOZ_ASSERT(!NS_IsMainThread());
  MOZ_ASSERT(aIcon.payloads.Length() > 0);
  MOZ_ASSERT(!aIcon.spec.IsEmpty());
  MOZ_ASSERT(aIcon.expiration > 0);

  // There are multiple cases possible at this point:
  //   1. We must insert some payloads and no payloads exist in the table. This
  //      would be a straight INSERT.
  //   2. The table contains the same number of payloads we are inserting. This
  //      would be a straight UPDATE.
  //   3. The table contains more payloads than we are inserting. This would be
  //      an UPDATE and a DELETE.
  //   4. The table contains less payloads than we are inserting. This would be
  //      an UPDATE and an INSERT.
  // We can't just remove all the old entries and insert the new ones, cause
  // we'd lose the referential integrity with pages.  For the same reason we
  // cannot use INSERT OR REPLACE, since it's implemented as DELETE AND INSERT.
  // Thus, we follow this strategy:
  //   * SELECT all existing icon ids
  //   * For each payload, either UPDATE OR INSERT reusing icon ids.
  //   * If any previous icon ids is leftover, DELETE it.

  nsCOMPtr<mozIStorageStatement> selectStmt = aDB->GetStatement(
      "SELECT id FROM moz_icons "
      "WHERE fixed_icon_url_hash = hash(fixup_url(:url)) "
      "AND icon_url = :url ");
  NS_ENSURE_STATE(selectStmt);
  mozStorageStatementScoper scoper(selectStmt);
  nsresult rv =
      URIBinder::Bind(selectStmt, NS_LITERAL_CSTRING("url"), aIcon.spec);
  NS_ENSURE_SUCCESS(rv, rv);
  std::deque<int64_t> ids;
  bool hasResult = false;
  while (NS_SUCCEEDED(selectStmt->ExecuteStep(&hasResult)) && hasResult) {
    int64_t id = selectStmt->AsInt64(0);
    MOZ_ASSERT(id > 0);
    ids.push_back(id);
  }
  if (aMustReplace && ids.empty()) {
    return NS_ERROR_NOT_AVAILABLE;
  }

  nsCOMPtr<mozIStorageStatement> insertStmt = aDB->GetStatement(
      "INSERT INTO moz_icons "
      "(icon_url, fixed_icon_url_hash, width, root, expire_ms, data) "
      "VALUES (:url, hash(fixup_url(:url)), :width, :root, :expire, :data) ");
  NS_ENSURE_STATE(insertStmt);
  // ReplaceFaviconData may replace data for an already existing icon, and in
  // that case it won't have the page uri at hand, thus it can't tell if the
  // icon is a root icon or not. For that reason, never overwrite a root = 1.
  nsCOMPtr<mozIStorageStatement> updateStmt = aDB->GetStatement(
      "UPDATE moz_icons SET width = :width, "
      "expire_ms = :expire, "
      "data = :data, "
      "root = (root  OR :root) "
      "WHERE id = :id ");
  NS_ENSURE_STATE(updateStmt);

  for (auto& payload : aIcon.payloads) {
    // Sanity checks.
    MOZ_ASSERT(payload.mimeType.EqualsLiteral(PNG_MIME_TYPE) ||
                   payload.mimeType.EqualsLiteral(SVG_MIME_TYPE),
               "Only png and svg payloads are supported");
    MOZ_ASSERT(!payload.mimeType.EqualsLiteral(SVG_MIME_TYPE) ||
                   payload.width == UINT16_MAX,
               "SVG payloads should have max width");
    MOZ_ASSERT(payload.width > 0, "Payload should have a width");
#ifdef DEBUG
    // Done to ensure we fetch the id. See the MOZ_ASSERT below.
    payload.id = 0;
#endif
    if (!ids.empty()) {
      // Pop the first existing id for reuse.
      int64_t id = ids.front();
      ids.pop_front();
      mozStorageStatementScoper scoper(updateStmt);
      rv = updateStmt->BindInt64ByName(NS_LITERAL_CSTRING("id"), id);
      NS_ENSURE_SUCCESS(rv, rv);
      rv = updateStmt->BindInt32ByName(NS_LITERAL_CSTRING("width"),
                                       payload.width);
      NS_ENSURE_SUCCESS(rv, rv);
      rv = updateStmt->BindInt64ByName(NS_LITERAL_CSTRING("expire"),
                                       aIcon.expiration / 1000);
      NS_ENSURE_SUCCESS(rv, rv);
      rv = updateStmt->BindInt32ByName(NS_LITERAL_CSTRING("root"),
                                       aIcon.rootIcon);
      NS_ENSURE_SUCCESS(rv, rv);
      rv = updateStmt->BindBlobByName(NS_LITERAL_CSTRING("data"),
                                      TO_INTBUFFER(payload.data),
                                      payload.data.Length());
      NS_ENSURE_SUCCESS(rv, rv);
      rv = updateStmt->Execute();
      NS_ENSURE_SUCCESS(rv, rv);
      // Set the new payload id.
      payload.id = id;
    } else {
      // Insert a new entry.
      mozStorageStatementScoper scoper(insertStmt);
      rv = URIBinder::Bind(insertStmt, NS_LITERAL_CSTRING("url"), aIcon.spec);
      NS_ENSURE_SUCCESS(rv, rv);
      rv = insertStmt->BindInt32ByName(NS_LITERAL_CSTRING("width"),
                                       payload.width);
      NS_ENSURE_SUCCESS(rv, rv);

      rv = insertStmt->BindInt32ByName(NS_LITERAL_CSTRING("root"),
                                       aIcon.rootIcon);
      NS_ENSURE_SUCCESS(rv, rv);
      rv = insertStmt->BindInt64ByName(NS_LITERAL_CSTRING("expire"),
                                       aIcon.expiration / 1000);
      NS_ENSURE_SUCCESS(rv, rv);
      rv = insertStmt->BindBlobByName(NS_LITERAL_CSTRING("data"),
                                      TO_INTBUFFER(payload.data),
                                      payload.data.Length());
      NS_ENSURE_SUCCESS(rv, rv);
      rv = insertStmt->Execute();
      NS_ENSURE_SUCCESS(rv, rv);
      // Set the new payload id.
      payload.id = nsFaviconService::sLastInsertedIconId;
    }
    MOZ_ASSERT(payload.id > 0, "Payload should have an id");
  }

  if (!ids.empty()) {
    // Remove any old leftover payload.
    nsAutoCString sql("DELETE FROM moz_icons WHERE id IN (");
    for (int64_t id : ids) {
      sql.AppendInt(id);
      sql.AppendLiteral(",");
    }
    sql.AppendLiteral(" 0)");  // Non-existing id to match the trailing comma.
    nsCOMPtr<mozIStorageStatement> stmt = aDB->GetStatement(sql);
    NS_ENSURE_STATE(stmt);
    mozStorageStatementScoper scoper(stmt);
    rv = stmt->Execute();
    NS_ENSURE_SUCCESS(rv, rv);
  }

  return NS_OK;
}

/**
 * Fetches information on a icon url from the database.
 *
 * @param aDBConn
 *        Database connection to history tables.
 * @param aPreferredWidth
 *        The preferred size to fetch.
 * @param _icon
 *        Icon that should be fetched.
 */
nsresult FetchIconInfo(const RefPtr<Database>& aDB, uint16_t aPreferredWidth,
                       IconData& _icon) {
  MOZ_ASSERT(_icon.spec.Length(), "Must have a non-empty spec!");
  MOZ_ASSERT(!NS_IsMainThread());

  if (_icon.status & ICON_STATUS_CACHED) {
    // The icon data has already been set by ReplaceFaviconData.
    return NS_OK;
  }

  nsCOMPtr<mozIStorageStatement> stmt = aDB->GetStatement(
      "/* do not warn (bug no: not worth having a compound index) */ "
      "SELECT id, expire_ms, data, width, root "
      "FROM moz_icons "
      "WHERE fixed_icon_url_hash = hash(fixup_url(:url)) "
      "AND icon_url = :url "
      "ORDER BY width DESC ");
  NS_ENSURE_STATE(stmt);
  mozStorageStatementScoper scoper(stmt);

  DebugOnly<nsresult> rv =
      URIBinder::Bind(stmt, NS_LITERAL_CSTRING("url"), _icon.spec);
  MOZ_ASSERT(NS_SUCCEEDED(rv));

  bool hasResult = false;
  while (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) {
    IconPayload payload;
    rv = stmt->GetInt64(0, &payload.id);
    MOZ_ASSERT(NS_SUCCEEDED(rv));

    // Expiration can be nullptr.
    bool isNull;
    rv = stmt->GetIsNull(1, &isNull);
    MOZ_ASSERT(NS_SUCCEEDED(rv));
    if (!isNull) {
      int64_t expire_ms;
      rv = stmt->GetInt64(1, &expire_ms);
      MOZ_ASSERT(NS_SUCCEEDED(rv));
      _icon.expiration = expire_ms * 1000;
    }

    uint8_t* data;
    uint32_t dataLen = 0;
    rv = stmt->GetBlob(2, &dataLen, &data);
    MOZ_ASSERT(NS_SUCCEEDED(rv));
    payload.data.Adopt(TO_CHARBUFFER(data), dataLen);

    int32_t width;
    rv = stmt->GetInt32(3, &width);
    MOZ_ASSERT(NS_SUCCEEDED(rv));
    payload.width = width;
    if (payload.width == UINT16_MAX) {
      payload.mimeType.AssignLiteral(SVG_MIME_TYPE);
    } else {
      payload.mimeType.AssignLiteral(PNG_MIME_TYPE);
    }

    int32_t rootIcon;
    rv = stmt->GetInt32(4, &rootIcon);
    MOZ_ASSERT(NS_SUCCEEDED(rv));
    _icon.rootIcon = rootIcon;

    if (aPreferredWidth == 0 || _icon.payloads.Length() == 0) {
      _icon.payloads.AppendElement(payload);
    } else if (payload.width >= aPreferredWidth) {
      // Only retain the best matching payload.
      _icon.payloads.ReplaceElementAt(0, payload);
    } else {
      break;
    }
  }

  return NS_OK;
}

nsresult FetchIconPerSpec(const RefPtr<Database>& aDB,
                          const nsACString& aPageSpec,
                          const nsACString& aPageHost, IconData& aIconData,
                          uint16_t aPreferredWidth) {
  MOZ_ASSERT(!aPageSpec.IsEmpty(), "Page spec must not be empty.");
  MOZ_ASSERT(!NS_IsMainThread());

  // This selects both associated and root domain icons, ordered by width,
  // where an associated icon has priority over a root domain icon.
  // Regardless, note that while this way we are far more efficient, we lost
  // associations with root domain icons, so it's possible we'll return one
  // for a specific size when an associated icon for that size doesn't exist.
  nsCOMPtr<mozIStorageStatement> stmt = aDB->GetStatement(
      "/* do not warn (bug no: not worth having a compound index) */ "
      "SELECT width, icon_url, root "
      "FROM moz_icons i "
      "JOIN moz_icons_to_pages ON i.id = icon_id "
      "JOIN moz_pages_w_icons p ON p.id = page_id "
      "WHERE page_url_hash = hash(:url) AND page_url = :url "
      "OR (:hash_idx AND page_url_hash = hash(substr(:url, 0, :hash_idx)) "
      "AND page_url = substr(:url, 0, :hash_idx)) "
      "UNION ALL "
      "SELECT width, icon_url, root "
      "FROM moz_icons i "
      "WHERE fixed_icon_url_hash = hash(fixup_url(:root_icon_url)) "
      "ORDER BY width DESC, root ASC ");
  NS_ENSURE_STATE(stmt);
  mozStorageStatementScoper scoper(stmt);

  nsresult rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("url"), aPageSpec);
  NS_ENSURE_SUCCESS(rv, rv);
  nsAutoCString rootIconFixedUrl(aPageHost);
  if (!rootIconFixedUrl.IsEmpty()) {
    rootIconFixedUrl.AppendLiteral("/favicon.ico");
  }
  rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("root_icon_url"),
                                  rootIconFixedUrl);
  NS_ENSURE_SUCCESS(rv, rv);
  int32_t hashIdx = PromiseFlatCString(aPageSpec).RFind("#");
  rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("hash_idx"), hashIdx + 1);
  NS_ENSURE_SUCCESS(rv, rv);

  // Return the biggest icon close to the preferred width. It may be bigger
  // or smaller if the preferred width isn't found.
  bool hasResult;
  int32_t lastWidth = 0;
  while (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) {
    int32_t width;
    rv = stmt->GetInt32(0, &width);
    if (lastWidth == width) {
      // We already found an icon for this width. We always prefer the first
      // icon found, because it's a non-root icon, per the root ASC ordering.
      continue;
    }
    if (!aIconData.spec.IsEmpty() && width < aPreferredWidth) {
      // We found the best match, or we already found a match so we don't need
      // to fallback to the root domain icon.
      break;
    }
    lastWidth = width;
    rv = stmt->GetUTF8String(1, aIconData.spec);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  return NS_OK;
}

/**
 * Tries to compute the expiration time for a icon from the channel.
 *
 * @param aChannel
 *        The network channel used to fetch the icon.
 * @return a valid expiration value for the fetched icon.
 */
PRTime GetExpirationTimeFromChannel(nsIChannel* aChannel) {
  MOZ_ASSERT(NS_IsMainThread());

  // Attempt to get an expiration time from the cache.  If this fails, we'll
  // make one up.
  PRTime expiration = -1;
  nsCOMPtr<nsICachingChannel> cachingChannel = do_QueryInterface(aChannel);
  if (cachingChannel) {
    nsCOMPtr<nsISupports> cacheToken;
    nsresult rv = cachingChannel->GetCacheToken(getter_AddRefs(cacheToken));
    if (NS_SUCCEEDED(rv)) {
      nsCOMPtr<nsICacheEntry> cacheEntry = do_QueryInterface(cacheToken);
      uint32_t seconds;
      rv = cacheEntry->GetExpirationTime(&seconds);
      if (NS_SUCCEEDED(rv)) {
        // Set the expiration, but make sure we honor our cap.
        expiration = PR_Now() + std::min((PRTime)seconds * PR_USEC_PER_SEC,
                                         MAX_FAVICON_EXPIRATION);
      }
    }
  }
  // If we did not obtain a time from the cache, use the cap value.
  return expiration < 0 ? PR_Now() + MAX_FAVICON_EXPIRATION : expiration;
}

}  // namespace

////////////////////////////////////////////////////////////////////////////////
//// AsyncFetchAndSetIconForPage

NS_IMPL_ISUPPORTS_INHERITED(AsyncFetchAndSetIconForPage, Runnable,
                            nsIStreamListener, nsIInterfaceRequestor,
                            nsIChannelEventSink, mozIPlacesPendingOperation)

AsyncFetchAndSetIconForPage::AsyncFetchAndSetIconForPage(
    IconData& aIcon, PageData& aPage, bool aFaviconLoadPrivate,
    nsIFaviconDataCallback* aCallback, nsIPrincipal* aLoadingPrincipal,
    uint64_t aRequestContextID)
    : Runnable("places::AsyncFetchAndSetIconForPage"),
      mCallback(new nsMainThreadPtrHolder<nsIFaviconDataCallback>(
          "AsyncFetchAndSetIconForPage::mCallback", aCallback)),
      mIcon(aIcon),
      mPage(aPage),
      mFaviconLoadPrivate(aFaviconLoadPrivate),
      mLoadingPrincipal(new nsMainThreadPtrHolder<nsIPrincipal>(
          "AsyncFetchAndSetIconForPage::mLoadingPrincipal", aLoadingPrincipal)),
      mCanceled(false),
      mRequestContextID(aRequestContextID) {
  MOZ_ASSERT(NS_IsMainThread());
}

NS_IMETHODIMP
AsyncFetchAndSetIconForPage::Run() {
  MOZ_ASSERT(!NS_IsMainThread());

  // Try to fetch the icon from the database.
  RefPtr<Database> DB = Database::GetDatabase();
  NS_ENSURE_STATE(DB);
  nsresult rv = FetchIconInfo(DB, 0, mIcon);
  NS_ENSURE_SUCCESS(rv, rv);

  bool isInvalidIcon = !mIcon.payloads.Length() || PR_Now() > mIcon.expiration;
  bool fetchIconFromNetwork =
      mIcon.fetchMode == FETCH_ALWAYS ||
      (mIcon.fetchMode == FETCH_IF_MISSING && isInvalidIcon);

  // Check if we can associate the icon to this page.
  rv = FetchPageInfo(DB, mPage);
  if (NS_FAILED(rv)) {
    if (rv == NS_ERROR_NOT_AVAILABLE) {
      // We have never seen this page.  If we can add the page to history,
      // we will try to do it later, otherwise just bail out.
      if (!mPage.canAddToHistory) {
        return NS_OK;
      }
    }
    return rv;
  }

  if (!fetchIconFromNetwork) {
    // There is already a valid icon or we don't want to fetch a new one,
    // directly proceed with association.
    RefPtr<AsyncAssociateIconToPage> event =
        new AsyncAssociateIconToPage(mIcon, mPage, mCallback);
    // We're already on the async thread.
    return event->Run();
  }

  // Fetch the icon from the network, the request starts from the main-thread.
  // When done this will associate the icon to the page and notify.
  nsCOMPtr<nsIRunnable> event =
      NewRunnableMethod("places::AsyncFetchAndSetIconForPage::FetchFromNetwork",
                        this, &AsyncFetchAndSetIconForPage::FetchFromNetwork);
  return NS_DispatchToMainThread(event);
}

nsresult AsyncFetchAndSetIconForPage::FetchFromNetwork() {
  MOZ_ASSERT(NS_IsMainThread());

  if (mCanceled) {
    return NS_OK;
  }

  // Ensure data is cleared, since it's going to be overwritten.
  mIcon.payloads.Clear();

  IconPayload payload;
  mIcon.payloads.AppendElement(payload);

  nsCOMPtr<nsIURI> iconURI;
  nsresult rv = NS_NewURI(getter_AddRefs(iconURI), mIcon.spec);
  NS_ENSURE_SUCCESS(rv, rv);
  nsCOMPtr<nsIChannel> channel;
  rv = NS_NewChannel(getter_AddRefs(channel), iconURI, mLoadingPrincipal,
                     nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS |
                         nsILoadInfo::SEC_ALLOW_CHROME |
                         nsILoadInfo::SEC_DISALLOW_SCRIPT,
                     nsIContentPolicy::TYPE_INTERNAL_IMAGE_FAVICON);

  NS_ENSURE_SUCCESS(rv, rv);
  nsCOMPtr<nsIInterfaceRequestor> listenerRequestor =
      do_QueryInterface(reinterpret_cast<nsISupports*>(this));
  NS_ENSURE_STATE(listenerRequestor);
  rv = channel->SetNotificationCallbacks(listenerRequestor);
  NS_ENSURE_SUCCESS(rv, rv);
  nsCOMPtr<nsIPrivateBrowsingChannel> pbChannel = do_QueryInterface(channel);
  if (pbChannel) {
    rv = pbChannel->SetPrivate(mFaviconLoadPrivate);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  nsCOMPtr<nsISupportsPriority> priorityChannel = do_QueryInterface(channel);
  if (priorityChannel) {
    priorityChannel->AdjustPriority(nsISupportsPriority::PRIORITY_LOWEST);
  }

  if (nsContentUtils::IsTailingEnabled()) {
    nsCOMPtr<nsIClassOfService> cos = do_QueryInterface(channel);
    if (cos) {
      cos->AddClassFlags(nsIClassOfService::Tail |
                         nsIClassOfService::Throttleable);
    }

    nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(channel));
    if (httpChannel) {
      Unused << httpChannel->SetRequestContextID(mRequestContextID);
    }
  }

  rv = channel->AsyncOpen2(this);
  if (NS_SUCCEEDED(rv)) {
    mRequest = channel;
  }
  return rv;
}

NS_IMETHODIMP
AsyncFetchAndSetIconForPage::Cancel() {
  MOZ_ASSERT(NS_IsMainThread());
  if (mCanceled) {
    return NS_ERROR_UNEXPECTED;
  }
  mCanceled = true;
  if (mRequest) {
    mRequest->Cancel(NS_BINDING_ABORTED);
  }
  return NS_OK;
}

NS_IMETHODIMP
AsyncFetchAndSetIconForPage::OnStartRequest(nsIRequest* aRequest,
                                            nsISupports* aContext) {
  // mRequest should already be set from ::FetchFromNetwork, but in the case of
  // a redirect we might get a new request, and we should make sure we keep a
  // reference to the most current request.
  mRequest = aRequest;
  if (mCanceled) {
    mRequest->Cancel(NS_BINDING_ABORTED);
  }
  return NS_OK;
}

NS_IMETHODIMP
AsyncFetchAndSetIconForPage::OnDataAvailable(nsIRequest* aRequest,
                                             nsISupports* aContext,
                                             nsIInputStream* aInputStream,
                                             uint64_t aOffset,
                                             uint32_t aCount) {
  MOZ_ASSERT(mIcon.payloads.Length() == 1);
  // Limit downloads to 500KB.
  const size_t kMaxDownloadSize = 500 * 1024;
  if (mIcon.payloads[0].data.Length() + aCount > kMaxDownloadSize) {
    mIcon.payloads.Clear();
    return NS_ERROR_FILE_TOO_BIG;
  }

  nsAutoCString buffer;
  nsresult rv = NS_ConsumeStream(aInputStream, aCount, buffer);
  if (rv != NS_BASE_STREAM_WOULD_BLOCK && NS_FAILED(rv)) {
    return rv;
  }

  if (!mIcon.payloads[0].data.Append(buffer, fallible)) {
    mIcon.payloads.Clear();
    return NS_ERROR_OUT_OF_MEMORY;
  }

  return NS_OK;
}

NS_IMETHODIMP
AsyncFetchAndSetIconForPage::GetInterface(const nsIID& uuid, void** aResult) {
  return QueryInterface(uuid, aResult);
}

NS_IMETHODIMP
AsyncFetchAndSetIconForPage::AsyncOnChannelRedirect(
    nsIChannel* oldChannel, nsIChannel* newChannel, uint32_t flags,
    nsIAsyncVerifyRedirectCallback* cb) {
  // If we've been canceled, stop the redirect with NS_BINDING_ABORTED, and
  // handle the cancel on the original channel.
  (void)cb->OnRedirectVerifyCallback(mCanceled ? NS_BINDING_ABORTED : NS_OK);
  return NS_OK;
}

NS_IMETHODIMP
AsyncFetchAndSetIconForPage::OnStopRequest(nsIRequest* aRequest,
                                           nsISupports* aContext,
                                           nsresult aStatusCode) {
  MOZ_ASSERT(NS_IsMainThread());

  // Don't need to track this anymore.
  mRequest = nullptr;
  if (mCanceled) {
    return NS_OK;
  }

  nsFaviconService* favicons = nsFaviconService::GetFaviconService();
  NS_ENSURE_STATE(favicons);

  nsresult rv;

  // If fetching the icon failed, bail out.
  if (NS_FAILED(aStatusCode) || mIcon.payloads.Length() == 0) {
    return NS_OK;
  }

  nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
  // aRequest should always QI to nsIChannel.
  MOZ_ASSERT(channel);

  MOZ_ASSERT(mIcon.payloads.Length() == 1);
  IconPayload& payload = mIcon.payloads[0];

  nsAutoCString contentType;
  channel->GetContentType(contentType);
  // Bug 366324 - We don't want to sniff for SVG, so rely on server-specified
  // type.
  if (contentType.EqualsLiteral(SVG_MIME_TYPE)) {
    payload.mimeType.AssignLiteral(SVG_MIME_TYPE);
    payload.width = UINT16_MAX;
  } else {
    NS_SniffContent(NS_DATA_SNIFFER_CATEGORY, aRequest,
                    TO_INTBUFFER(payload.data), payload.data.Length(),
                    payload.mimeType);
  }

  // If the icon does not have a valid MIME type, bail out.
  if (payload.mimeType.IsEmpty()) {
    return NS_OK;
  }

  mIcon.expiration = GetExpirationTimeFromChannel(channel);

  // Telemetry probes to measure the favicon file sizes for each different file
  // type. This allow us to measure common file sizes while also observing each
  // type popularity.
  if (payload.mimeType.EqualsLiteral(PNG_MIME_TYPE)) {
    mozilla::Telemetry::Accumulate(mozilla::Telemetry::PLACES_FAVICON_PNG_SIZES,
                                   payload.data.Length());
  } else if (payload.mimeType.EqualsLiteral("image/x-icon") ||
             payload.mimeType.EqualsLiteral("image/vnd.microsoft.icon")) {
    mozilla::Telemetry::Accumulate(mozilla::Telemetry::PLACES_FAVICON_ICO_SIZES,
                                   payload.data.Length());
  } else if (payload.mimeType.EqualsLiteral("image/jpeg") ||
             payload.mimeType.EqualsLiteral("image/pjpeg")) {
    mozilla::Telemetry::Accumulate(
        mozilla::Telemetry::PLACES_FAVICON_JPEG_SIZES, payload.data.Length());
  } else if (payload.mimeType.EqualsLiteral("image/gif")) {
    mozilla::Telemetry::Accumulate(mozilla::Telemetry::PLACES_FAVICON_GIF_SIZES,
                                   payload.data.Length());
  } else if (payload.mimeType.EqualsLiteral("image/bmp") ||
             payload.mimeType.EqualsLiteral("image/x-windows-bmp")) {
    mozilla::Telemetry::Accumulate(mozilla::Telemetry::PLACES_FAVICON_BMP_SIZES,
                                   payload.data.Length());
  } else if (payload.mimeType.EqualsLiteral(SVG_MIME_TYPE)) {
    mozilla::Telemetry::Accumulate(mozilla::Telemetry::PLACES_FAVICON_SVG_SIZES,
                                   payload.data.Length());
  } else {
    mozilla::Telemetry::Accumulate(
        mozilla::Telemetry::PLACES_FAVICON_OTHER_SIZES, payload.data.Length());
  }

  rv = favicons->OptimizeIconSizes(mIcon);
  NS_ENSURE_SUCCESS(rv, rv);

  // If there's not valid payload, don't store the icon into to the database.
  if (mIcon.payloads.Length() == 0) {
    return NS_OK;
  }

  mIcon.status = ICON_STATUS_CHANGED;

  RefPtr<Database> DB = Database::GetDatabase();
  NS_ENSURE_STATE(DB);
  RefPtr<AsyncAssociateIconToPage> event =
      new AsyncAssociateIconToPage(mIcon, mPage, mCallback);
  DB->DispatchToAsyncThread(event);

  return NS_OK;
}

////////////////////////////////////////////////////////////////////////////////
//// AsyncAssociateIconToPage

AsyncAssociateIconToPage::AsyncAssociateIconToPage(
    const IconData& aIcon, const PageData& aPage,
    const nsMainThreadPtrHandle<nsIFaviconDataCallback>& aCallback)
    : Runnable("places::AsyncAssociateIconToPage"),
      mCallback(aCallback),
      mIcon(aIcon),
      mPage(aPage) {
  // May be created in both threads.
}

NS_IMETHODIMP
AsyncAssociateIconToPage::Run() {
  MOZ_ASSERT(!NS_IsMainThread());
  MOZ_ASSERT(!mPage.guid.IsEmpty(),
             "Page info should have been fetched already");
  MOZ_ASSERT(mPage.canAddToHistory || !mPage.bookmarkedSpec.IsEmpty(),
             "The page should be addable to history or a bookmark");

  bool shouldUpdateIcon = mIcon.status & ICON_STATUS_CHANGED;
  if (!shouldUpdateIcon) {
    for (const auto& payload : mIcon.payloads) {
      // If the entry is missing from the database, we should add it.
      if (payload.id == 0) {
        shouldUpdateIcon = true;
        break;
      }
    }
  }

  RefPtr<Database> DB = Database::GetDatabase();
  NS_ENSURE_STATE(DB);
  mozStorageTransaction transaction(
      DB->MainConn(), false, mozIStorageConnection::TRANSACTION_IMMEDIATE);
  nsresult rv;
  if (shouldUpdateIcon) {
    rv = SetIconInfo(DB, mIcon);
    if (NS_FAILED(rv)) {
      (void)transaction.Commit();
      return rv;
    }

    mIcon.status = (mIcon.status & ~(ICON_STATUS_CACHED)) | ICON_STATUS_SAVED;
  }

  // If the page does not have an id, don't try to insert a new one, cause we
  // don't know where the page comes from.  Not doing so we may end adding
  // a page that otherwise we'd explicitly ignore, like a POST or an error page.
  if (mPage.placeId == 0) {
    rv = transaction.Commit();
    NS_ENSURE_SUCCESS(rv, rv);
    return NS_OK;
  }

  // Don't associate pages to root domain icons, since those will be returned
  // regardless.  This saves a lot of work and database space since we don't
  // need to store urls and relations.
  // Though, this is possible only if both the page and the icon have the same
  // host, otherwise we couldn't relate them.
  if (!mIcon.rootIcon || !mIcon.host.Equals(mPage.host)) {
    // The page may have associated payloads already, and those could have to be
    // expired. For example at a certain point a page could decide to stop
    // serving its usual 16px and 32px pngs, and use an svg instead. On the
    // other side, we could also be in the process of adding more payloads to
    // this page, and we should not expire the payloads we just added. For this,
    // we use the expiration field as an indicator and remove relations based on
    // it being elapsed. We don't remove orphan icons at this time since it
    // would have a cost. The privacy hit is limited since history removal
    // methods already expire orphan icons.
    if (mPage.id != 0) {
      nsCOMPtr<mozIStorageStatement> stmt;
      stmt = DB->GetStatement(
          "DELETE FROM moz_icons_to_pages "
          "WHERE icon_id IN ( "
          "SELECT icon_id FROM moz_icons_to_pages "
          "JOIN moz_icons i ON icon_id = i.id "
          "WHERE page_id = :page_id "
          "AND expire_ms < strftime('%s','now','localtime','start of day','-7 "
          "days','utc') * 1000 "
          ") AND page_id = :page_id ");
      NS_ENSURE_STATE(stmt);
      mozStorageStatementScoper scoper(stmt);
      rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), mPage.id);
      NS_ENSURE_SUCCESS(rv, rv);
      rv = stmt->Execute();
      NS_ENSURE_SUCCESS(rv, rv);
    } else {
      // We need to create the page entry.
      nsCOMPtr<mozIStorageStatement> stmt;
      stmt = DB->GetStatement(
          "INSERT OR IGNORE INTO moz_pages_w_icons (page_url, page_url_hash) "
          "VALUES (:page_url, hash(:page_url)) ");
      NS_ENSURE_STATE(stmt);
      mozStorageStatementScoper scoper(stmt);
      rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), mPage.spec);
      NS_ENSURE_SUCCESS(rv, rv);
      rv = stmt->Execute();
      NS_ENSURE_SUCCESS(rv, rv);
    }

    // Then we can create the relations.
    nsCOMPtr<mozIStorageStatement> stmt;
    stmt = DB->GetStatement(
        "INSERT OR IGNORE INTO moz_icons_to_pages (page_id, icon_id) "
        "VALUES ((SELECT id from moz_pages_w_icons WHERE page_url_hash = "
        "hash(:page_url) AND page_url = :page_url), "
        ":icon_id) ");
    NS_ENSURE_STATE(stmt);

    // For some reason using BindingParamsArray here fails execution, so we must
    // execute the statements one by one.
    // In the future we may want to investigate the reasons, sounds like related
    // to contraints.
    for (const auto& payload : mIcon.payloads) {
      mozStorageStatementScoper scoper(stmt);
      nsCOMPtr<mozIStorageBindingParams> params;
      rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), mPage.spec);
      NS_ENSURE_SUCCESS(rv, rv);
      rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("icon_id"), payload.id);
      NS_ENSURE_SUCCESS(rv, rv);
      rv = stmt->Execute();
      NS_ENSURE_SUCCESS(rv, rv);
    }
  }

  mIcon.status |= ICON_STATUS_ASSOCIATED;

  rv = transaction.Commit();
  NS_ENSURE_SUCCESS(rv, rv);

  // Finally, dispatch an event to the main thread to notify observers.
  nsCOMPtr<nsIRunnable> event =
      new NotifyIconObservers(mIcon, mPage, mCallback);
  rv = NS_DispatchToMainThread(event);
  NS_ENSURE_SUCCESS(rv, rv);

  // If there is a bookmarked page that redirects to this one, try to update its
  // icon as well.
  if (!mPage.bookmarkedSpec.IsEmpty() &&
      !mPage.bookmarkedSpec.Equals(mPage.spec)) {
    // Create a new page struct to avoid polluting it with old data.
    PageData bookmarkedPage;
    bookmarkedPage.spec = mPage.bookmarkedSpec;
    RefPtr<Database> DB = Database::GetDatabase();
    if (DB && NS_SUCCEEDED(FetchPageInfo(DB, bookmarkedPage))) {
      // This will be silent, so be sure to not pass in the current callback.
      nsMainThreadPtrHandle<nsIFaviconDataCallback> nullCallback;
      RefPtr<AsyncAssociateIconToPage> event =
          new AsyncAssociateIconToPage(mIcon, bookmarkedPage, nullCallback);
      Unused << event->Run();
    }
  }

  return NS_OK;
}

////////////////////////////////////////////////////////////////////////////////
//// AsyncGetFaviconURLForPage

AsyncGetFaviconURLForPage::AsyncGetFaviconURLForPage(
    const nsACString& aPageSpec, const nsACString& aPageHost,
    uint16_t aPreferredWidth, nsIFaviconDataCallback* aCallback)
    : Runnable("places::AsyncGetFaviconURLForPage"),
      mPreferredWidth(aPreferredWidth == 0 ? UINT16_MAX : aPreferredWidth),
      mCallback(new nsMainThreadPtrHolder<nsIFaviconDataCallback>(
          "AsyncGetFaviconURLForPage::mCallback", aCallback)) {
  MOZ_ASSERT(NS_IsMainThread());
  mPageSpec.Assign(aPageSpec);
  mPageHost.Assign(aPageHost);
}

NS_IMETHODIMP
AsyncGetFaviconURLForPage::Run() {
  MOZ_ASSERT(!NS_IsMainThread());

  RefPtr<Database> DB = Database::GetDatabase();
  NS_ENSURE_STATE(DB);
  IconData iconData;
  nsresult rv =
      FetchIconPerSpec(DB, mPageSpec, mPageHost, iconData, mPreferredWidth);
  NS_ENSURE_SUCCESS(rv, rv);

  // Now notify our callback of the icon spec we retrieved, even if empty.
  PageData pageData;
  pageData.spec.Assign(mPageSpec);

  nsCOMPtr<nsIRunnable> event =
      new NotifyIconObservers(iconData, pageData, mCallback);
  rv = NS_DispatchToMainThread(event);
  NS_ENSURE_SUCCESS(rv, rv);

  return NS_OK;
}

////////////////////////////////////////////////////////////////////////////////
//// AsyncGetFaviconDataForPage

AsyncGetFaviconDataForPage::AsyncGetFaviconDataForPage(
    const nsACString& aPageSpec, const nsACString& aPageHost,
    uint16_t aPreferredWidth, nsIFaviconDataCallback* aCallback)
    : Runnable("places::AsyncGetFaviconDataForPage"),
      mPreferredWidth(aPreferredWidth == 0 ? UINT16_MAX : aPreferredWidth),
      mCallback(new nsMainThreadPtrHolder<nsIFaviconDataCallback>(
          "AsyncGetFaviconDataForPage::mCallback", aCallback)) {
  MOZ_ASSERT(NS_IsMainThread());
  mPageSpec.Assign(aPageSpec);
  mPageHost.Assign(aPageHost);
}

NS_IMETHODIMP
AsyncGetFaviconDataForPage::Run() {
  MOZ_ASSERT(!NS_IsMainThread());

  RefPtr<Database> DB = Database::GetDatabase();
  NS_ENSURE_STATE(DB);
  IconData iconData;
  nsresult rv =
      FetchIconPerSpec(DB, mPageSpec, mPageHost, iconData, mPreferredWidth);
  NS_ENSURE_SUCCESS(rv, rv);

  if (!iconData.spec.IsEmpty()) {
    rv = FetchIconInfo(DB, mPreferredWidth, iconData);
    if (NS_FAILED(rv)) {
      iconData.spec.Truncate();
    }
  }

  PageData pageData;
  pageData.spec.Assign(mPageSpec);

  nsCOMPtr<nsIRunnable> event =
      new NotifyIconObservers(iconData, pageData, mCallback);
  rv = NS_DispatchToMainThread(event);
  NS_ENSURE_SUCCESS(rv, rv);
  return NS_OK;
}

////////////////////////////////////////////////////////////////////////////////
//// AsyncReplaceFaviconData

AsyncReplaceFaviconData::AsyncReplaceFaviconData(const IconData& aIcon)
    : Runnable("places::AsyncReplaceFaviconData"), mIcon(aIcon) {
  MOZ_ASSERT(NS_IsMainThread());
}

NS_IMETHODIMP
AsyncReplaceFaviconData::Run() {
  MOZ_ASSERT(!NS_IsMainThread());

  RefPtr<Database> DB = Database::GetDatabase();
  NS_ENSURE_STATE(DB);

  mozStorageTransaction transaction(
      DB->MainConn(), false, mozIStorageConnection::TRANSACTION_IMMEDIATE);
  nsresult rv = SetIconInfo(DB, mIcon, true);
  if (rv == NS_ERROR_NOT_AVAILABLE) {
    // There's no previous icon to replace, we don't need to do anything.
    (void)transaction.Commit();
    return NS_OK;
  }
  NS_ENSURE_SUCCESS(rv, rv);
  rv = transaction.Commit();
  NS_ENSURE_SUCCESS(rv, rv);

  // We can invalidate the cache version since we now persist the icon.
  nsCOMPtr<nsIRunnable> event = NewRunnableMethod(
      "places::AsyncReplaceFaviconData::RemoveIconDataCacheEntry", this,
      &AsyncReplaceFaviconData::RemoveIconDataCacheEntry);
  rv = NS_DispatchToMainThread(event);
  NS_ENSURE_SUCCESS(rv, rv);

  return NS_OK;
}

nsresult AsyncReplaceFaviconData::RemoveIconDataCacheEntry() {
  MOZ_ASSERT(NS_IsMainThread());

  nsCOMPtr<nsIURI> iconURI;
  nsresult rv = NS_NewURI(getter_AddRefs(iconURI), mIcon.spec);
  NS_ENSURE_SUCCESS(rv, rv);

  nsFaviconService* favicons = nsFaviconService::GetFaviconService();
  NS_ENSURE_STATE(favicons);
  favicons->mUnassociatedIcons.RemoveEntry(iconURI);

  return NS_OK;
}

////////////////////////////////////////////////////////////////////////////////
//// NotifyIconObservers

NotifyIconObservers::NotifyIconObservers(
    const IconData& aIcon, const PageData& aPage,
    const nsMainThreadPtrHandle<nsIFaviconDataCallback>& aCallback)
    : Runnable("places::NotifyIconObservers"),
      mCallback(aCallback),
      mIcon(aIcon),
      mPage(aPage) {}

NS_IMETHODIMP
NotifyIconObservers::Run() {
  MOZ_ASSERT(NS_IsMainThread());

  nsCOMPtr<nsIURI> iconURI;
  if (!mIcon.spec.IsEmpty()) {
    MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(iconURI), mIcon.spec));
    if (iconURI) {
      // Notify observers only if something changed.
      if (mIcon.status & ICON_STATUS_SAVED ||
          mIcon.status & ICON_STATUS_ASSOCIATED) {
        nsCOMPtr<nsIURI> pageURI;
        MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(pageURI), mPage.spec));
        if (pageURI) {
          nsFaviconService* favicons = nsFaviconService::GetFaviconService();
          MOZ_ASSERT(favicons);
          if (favicons) {
            (void)favicons->SendFaviconNotifications(pageURI, iconURI,
                                                     mPage.guid);
          }
        }
      }
    }
  }

  if (!mCallback) {
    return NS_OK;
  }

  if (mIcon.payloads.Length() > 0) {
    IconPayload& payload = mIcon.payloads[0];
    return mCallback->OnComplete(iconURI, payload.data.Length(),
                                 TO_INTBUFFER(payload.data), payload.mimeType,
                                 payload.width);
  }
  return mCallback->OnComplete(iconURI, 0, TO_INTBUFFER(EmptyCString()),
                               EmptyCString(), 0);
}

////////////////////////////////////////////////////////////////////////////////
//// FetchAndConvertUnsupportedPayloads

FetchAndConvertUnsupportedPayloads::FetchAndConvertUnsupportedPayloads(
    mozIStorageConnection* aDBConn)
    : Runnable("places::FetchAndConvertUnsupportedPayloads"), mDB(aDBConn) {}

NS_IMETHODIMP
FetchAndConvertUnsupportedPayloads::Run() {
  if (NS_IsMainThread()) {
    Preferences::ClearUser(PREF_CONVERT_PAYLOADS);
    return NS_OK;
  }

  MOZ_ASSERT(!NS_IsMainThread());
  NS_ENSURE_STATE(mDB);

  nsCOMPtr<mozIStorageStatement> stmt;
  nsresult rv = mDB->CreateStatement(
      NS_LITERAL_CSTRING(
          "SELECT id, width, data FROM moz_icons WHERE typeof(width) = 'text' "
          "ORDER BY id ASC "
          "LIMIT 200 "),
      getter_AddRefs(stmt));
  NS_ENSURE_SUCCESS(rv, rv);

  mozStorageTransaction transaction(
      mDB, false, mozIStorageConnection::TRANSACTION_IMMEDIATE);

  // We should do the work in chunks, or the wal journal may grow too much.
  uint8_t count = 0;
  bool hasResult;
  while (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) {
    ++count;
    int64_t id = stmt->AsInt64(0);
    MOZ_ASSERT(id > 0);
    nsAutoCString mimeType;
    rv = stmt->GetUTF8String(1, mimeType);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      continue;
    }
    uint8_t* data;
    uint32_t dataLen = 0;
    rv = stmt->GetBlob(2, &dataLen, &data);
    if (NS_WARN_IF(NS_FAILED(rv))) {
      continue;
    }
    nsCString buf;
    buf.Adopt(TO_CHARBUFFER(data), dataLen);

    int32_t width = 0;
    rv = ConvertPayload(id, mimeType, buf, &width);
    Unused << NS_WARN_IF(NS_FAILED(rv));
    if (NS_SUCCEEDED(rv)) {
      rv = StorePayload(id, width, buf);
      if (NS_WARN_IF(NS_FAILED(rv))) {
        continue;
      }
    }
  }

  rv = transaction.Commit();
  NS_ENSURE_SUCCESS(rv, rv);

  if (count == 200) {
    // There are more results to handle. Re-dispatch to the same thread for the
    // next chunk.
    return NS_DispatchToCurrentThread(this);
  }

  // We're done. Remove any leftovers.
  rv = mDB->ExecuteSimpleSQL(
      NS_LITERAL_CSTRING("DELETE FROM moz_icons WHERE typeof(width) = 'text'"));
  NS_ENSURE_SUCCESS(rv, rv);
  // Run a one-time VACUUM of places.sqlite, since we removed a lot from it.
  // It may cause jank, but not doing it could cause dataloss due to expiration.
  rv = mDB->ExecuteSimpleSQL(NS_LITERAL_CSTRING("VACUUM"));
  NS_ENSURE_SUCCESS(rv, rv);

  // Re-dispatch to the main-thread to flip the conversion pref.
  return NS_DispatchToMainThread(this);
}

nsresult FetchAndConvertUnsupportedPayloads::ConvertPayload(
    int64_t aId, const nsACString& aMimeType, nsCString& aPayload,
    int32_t* aWidth) {
  // TODO (bug 1346139): this should probably be unified with the function that
  // will handle additions optimization off the main thread.
  MOZ_ASSERT(!NS_IsMainThread());
  *aWidth = 0;

  // Exclude invalid mime types.
  if (aPayload.Length() == 0 || !imgLoader::SupportImageWithMimeType(
                                    PromiseFlatCString(aMimeType).get(),
                                    AcceptedMimeTypes::IMAGES_AND_DOCUMENTS)) {
    return NS_ERROR_FAILURE;
  }

  // If it's an SVG, there's nothing to optimize or convert.
  if (aMimeType.EqualsLiteral(SVG_MIME_TYPE)) {
    *aWidth = UINT16_MAX;
    return NS_OK;
  }

  // Convert the payload to an input stream.
  nsCOMPtr<nsIInputStream> stream;
  nsresult rv = NS_NewByteInputStream(getter_AddRefs(stream), aPayload.get(),
                                      aPayload.Length(), NS_ASSIGNMENT_DEPEND);
  NS_ENSURE_SUCCESS(rv, rv);

  // Decode the input stream to a surface.
  RefPtr<gfx::SourceSurface> surface = image::ImageOps::DecodeToSurface(
      stream.forget(), aMimeType, imgIContainer::DECODE_FLAGS_DEFAULT);
  NS_ENSURE_STATE(surface);
  RefPtr<gfx::DataSourceSurface> dataSurface = surface->GetDataSurface();
  NS_ENSURE_STATE(dataSurface);

  // Read the current size and set an appropriate final width.
  int32_t width = dataSurface->GetSize().width;
  int32_t height = dataSurface->GetSize().height;
  // For non-square images, pick the largest side.
  int32_t originalSize = std::max(width, height);
  int32_t size = originalSize;
  for (uint16_t supportedSize : sFaviconSizes) {
    if (supportedSize <= originalSize) {
      size = supportedSize;
      break;
    }
  }
  *aWidth = size;

  // If the original payload is png and the size is the same, no reason to
  // rescale the image.
  if (aMimeType.EqualsLiteral(PNG_MIME_TYPE) && size == originalSize) {
    return NS_OK;
  }

  // Rescale when needed.
  RefPtr<gfx::DataSourceSurface> targetDataSurface =
      gfx::Factory::CreateDataSourceSurface(gfx::IntSize(size, size),
                                            gfx::SurfaceFormat::B8G8R8A8, true);
  NS_ENSURE_STATE(targetDataSurface);

  {  // Block scope for map.
    gfx::DataSourceSurface::MappedSurface map;
    if (!targetDataSurface->Map(gfx::DataSourceSurface::MapType::WRITE, &map)) {
      return NS_ERROR_FAILURE;
    }

    RefPtr<gfx::DrawTarget> dt = gfx::Factory::CreateDrawTargetForData(
        gfx::BackendType::CAIRO, map.mData, targetDataSurface->GetSize(),
        map.mStride, gfx::SurfaceFormat::B8G8R8A8);
    NS_ENSURE_STATE(dt);

    gfx::IntSize frameSize = dataSurface->GetSize();
    dt->DrawSurface(dataSurface, gfx::Rect(0, 0, size, size),
                    gfx::Rect(0, 0, frameSize.width, frameSize.height),
                    gfx::DrawSurfaceOptions(),
                    gfx::DrawOptions(1.0f, gfx::CompositionOp::OP_SOURCE));
    targetDataSurface->Unmap();
  }

  // Finally Encode.
  nsCOMPtr<imgIEncoder> encoder =
      do_CreateInstance("@mozilla.org/image/encoder;2?type=image/png");
  NS_ENSURE_STATE(encoder);

  gfx::DataSourceSurface::MappedSurface map;
  if (!targetDataSurface->Map(gfx::DataSourceSurface::MapType::READ, &map)) {
    return NS_ERROR_FAILURE;
  }
  rv = encoder->InitFromData(map.mData, map.mStride * size, size, size,
                             map.mStride, imgIEncoder::INPUT_FORMAT_HOSTARGB,
                             EmptyString());
  targetDataSurface->Unmap();
  NS_ENSURE_SUCCESS(rv, rv);

  // Read the stream into a new buffer.
  nsCOMPtr<nsIInputStream> iconStream = encoder;
  NS_ENSURE_STATE(iconStream);
  rv = NS_ConsumeStream(iconStream, UINT32_MAX, aPayload);
  NS_ENSURE_SUCCESS(rv, rv);

  return NS_OK;
}

nsresult FetchAndConvertUnsupportedPayloads::StorePayload(
    int64_t aId, int32_t aWidth, const nsCString& aPayload) {
  MOZ_ASSERT(!NS_IsMainThread());

  NS_ENSURE_STATE(mDB);
  nsCOMPtr<mozIStorageStatement> stmt;
  nsresult rv = mDB->CreateStatement(
      NS_LITERAL_CSTRING(
          "UPDATE moz_icons SET data = :data, width = :width WHERE id = :id"),
      getter_AddRefs(stmt));
  NS_ENSURE_SUCCESS(rv, rv);

  rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("id"), aId);
  NS_ENSURE_SUCCESS(rv, rv);
  rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("width"), aWidth);
  NS_ENSURE_SUCCESS(rv, rv);
  rv = stmt->BindBlobByName(NS_LITERAL_CSTRING("data"), TO_INTBUFFER(aPayload),
                            aPayload.Length());
  NS_ENSURE_SUCCESS(rv, rv);

  rv = stmt->Execute();
  NS_ENSURE_SUCCESS(rv, rv);

  return NS_OK;
}

////////////////////////////////////////////////////////////////////////////////
//// AsyncCopyFavicons

AsyncCopyFavicons::AsyncCopyFavicons(PageData& aFromPage, PageData& aToPage,
                                     nsIFaviconDataCallback* aCallback)
    : Runnable("places::AsyncCopyFavicons"),
      mFromPage(aFromPage),
      mToPage(aToPage),
      mCallback(new nsMainThreadPtrHolder<nsIFaviconDataCallback>(
          "AsyncCopyFavicons::mCallback", aCallback)) {
  MOZ_ASSERT(NS_IsMainThread());
}

NS_IMETHODIMP
AsyncCopyFavicons::Run() {
  MOZ_ASSERT(!NS_IsMainThread());

  IconData icon;

  // Ensure we'll callback and dispatch notifications to the main-thread.
  auto cleanup = MakeScopeExit([&]() {
    // If we bailed out early, just return a null icon uri, since we didn't
    // copy anything.
    if (!(icon.status & ICON_STATUS_ASSOCIATED)) {
      icon.spec.Truncate();
    }
    nsCOMPtr<nsIRunnable> event =
        new NotifyIconObservers(icon, mToPage, mCallback);
    NS_DispatchToMainThread(event);
  });

  RefPtr<Database> DB = Database::GetDatabase();
  NS_ENSURE_STATE(DB);

  nsresult rv = FetchPageInfo(DB, mToPage);
  if (rv == NS_ERROR_NOT_AVAILABLE || !mToPage.placeId) {
    // We have never seen this page, or we can't add this page to history and
    // and it's not a bookmark. We won't add the page.
    return NS_OK;
  }
  NS_ENSURE_SUCCESS(rv, rv);

  // Get just one icon, to check whether the page has any, and to notify later.
  rv = FetchIconPerSpec(DB, mFromPage.spec, EmptyCString(), icon, UINT16_MAX);
  NS_ENSURE_SUCCESS(rv, rv);

  if (icon.spec.IsEmpty()) {
    // There's nothing to copy.
    return NS_OK;
  }

  // Insert an entry in moz_pages_w_icons if needed.
  if (!mToPage.id) {
    // We need to create the page entry.
    nsCOMPtr<mozIStorageStatement> stmt;
    stmt = DB->GetStatement(
        "INSERT OR IGNORE INTO moz_pages_w_icons (page_url, page_url_hash) "
        "VALUES (:page_url, hash(:page_url)) ");
    NS_ENSURE_STATE(stmt);
    mozStorageStatementScoper scoper(stmt);
    rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), mToPage.spec);
    NS_ENSURE_SUCCESS(rv, rv);
    rv = stmt->Execute();
    NS_ENSURE_SUCCESS(rv, rv);
    // Required to to fetch the id and the guid.
    rv = FetchPageInfo(DB, mToPage);
    NS_ENSURE_SUCCESS(rv, rv);
  }

  // Create the relations.
  nsCOMPtr<mozIStorageStatement> stmt = DB->GetStatement(
      "INSERT OR IGNORE INTO moz_icons_to_pages (page_id, icon_id) "
      "SELECT :id, icon_id "
      "FROM moz_icons_to_pages "
      "WHERE page_id = (SELECT id FROM moz_pages_w_icons WHERE page_url_hash = "
      "hash(:url) AND page_url = :url) ");
  NS_ENSURE_STATE(stmt);
  mozStorageStatementScoper scoper(stmt);
  rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("id"), mToPage.id);
  NS_ENSURE_SUCCESS(rv, rv);
  rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("url"), mFromPage.spec);
  NS_ENSURE_SUCCESS(rv, rv);
  rv = stmt->Execute();
  NS_ENSURE_SUCCESS(rv, rv);

  // Setting this will make us send pageChanged notifications.
  // The scope exit will take care of the callback and notifications.
  icon.status |= ICON_STATUS_ASSOCIATED;

  return NS_OK;
}

}  // namespace places
}  // namespace mozilla