summaryrefslogtreecommitdiffstats
path: root/browser/components/newtab/lib/FaviconFeed.sys.mjs
blob: a76566d3e823d34e59e808c34a9b61fd5a3365f7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
/* 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/. */

import { actionTypes as at } from "resource://activity-stream/common/Actions.sys.mjs";
import { getDomain } from "resource://activity-stream/lib/TippyTopProvider.sys.mjs";

// We use importESModule here instead of static import so that
// the Karma test environment won't choke on this module. This
// is because the Karma test environment already stubs out
// RemoteSettings, and overrides importESModule to be a no-op (which
// can't be done for a static import statement).

// eslint-disable-next-line mozilla/use-static-import
const { RemoteSettings } = ChromeUtils.importESModule(
  "resource://services-settings/remote-settings.sys.mjs"
);

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs",
  PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
});

const MIN_FAVICON_SIZE = 96;

/**
 * Get favicon info (uri and size) for a uri from Places.
 *
 * @param uri {nsIURI} Page to check for favicon data
 * @returns A promise of an object (possibly null) containing the data
 */
function getFaviconInfo(uri) {
  return new Promise(resolve =>
    lazy.PlacesUtils.favicons.getFaviconDataForPage(
      uri,
      // Package up the icon data in an object if we have it; otherwise null
      (iconUri, faviconLength, favicon, mimeType, faviconSize) =>
        resolve(iconUri ? { iconUri, faviconSize } : null),
      lazy.NewTabUtils.activityStreamProvider.THUMB_FAVICON_SIZE
    )
  );
}

/**
 * Fetches visit paths for a given URL from its most recent visit in Places.
 *
 * Note that this includes the URL itself as well as all the following
 * permenent&temporary redirected URLs if any.
 *
 * @param {String} a URL string
 *
 * @returns {Array} Returns an array containing objects as
 *   {int}    visit_id: ID of the visit in moz_historyvisits.
 *   {String} url: URL of the redirected URL.
 */
async function fetchVisitPaths(url) {
  const query = `
    WITH RECURSIVE path(visit_id)
    AS (
      SELECT v.id
      FROM moz_places h
      JOIN moz_historyvisits v
        ON v.place_id = h.id
      WHERE h.url_hash = hash(:url) AND h.url = :url
        AND v.visit_date = h.last_visit_date

      UNION

      SELECT id
      FROM moz_historyvisits
      JOIN path
        ON visit_id = from_visit
      WHERE visit_type IN
        (${lazy.PlacesUtils.history.TRANSITIONS.REDIRECT_PERMANENT},
         ${lazy.PlacesUtils.history.TRANSITIONS.REDIRECT_TEMPORARY})
    )
    SELECT visit_id, (
      SELECT (
        SELECT url
        FROM moz_places
        WHERE id = place_id)
      FROM moz_historyvisits
      WHERE id = visit_id) AS url
    FROM path
  `;

  const visits =
    await lazy.NewTabUtils.activityStreamProvider.executePlacesQuery(query, {
      columns: ["visit_id", "url"],
      params: { url },
    });
  return visits;
}

/**
 * Fetch favicon for a url by following its redirects in Places.
 *
 * This can improve the rich icon coverage for Top Sites since Places only
 * associates the favicon to the final url if the original one gets redirected.
 * Note this is not an urgent request, hence it is dispatched to the main
 * thread idle handler to avoid any possible performance impact.
 */
export async function fetchIconFromRedirects(url) {
  const visitPaths = await fetchVisitPaths(url);
  if (visitPaths.length > 1) {
    const lastVisit = visitPaths.pop();
    const redirectedUri = Services.io.newURI(lastVisit.url);
    const iconInfo = await getFaviconInfo(redirectedUri);
    if (iconInfo && iconInfo.faviconSize >= MIN_FAVICON_SIZE) {
      lazy.PlacesUtils.favicons.setAndFetchFaviconForPage(
        Services.io.newURI(url),
        iconInfo.iconUri,
        false,
        lazy.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
        null,
        Services.scriptSecurityManager.getSystemPrincipal()
      );
    }
  }
}

export class FaviconFeed {
  constructor() {
    this._queryForRedirects = new Set();
  }

  /**
   * fetchIcon attempts to fetch a rich icon for the given url from two sources.
   * First, it looks up the tippy top feed, if it's still missing, then it queries
   * the places for rich icon with its most recent visit in order to deal with
   * the redirected visit. See Bug 1421428 for more details.
   */
  async fetchIcon(url) {
    // Avoid initializing and fetching icons if prefs are turned off
    if (!this.shouldFetchIcons) {
      return;
    }

    const site = await this.getSite(getDomain(url));
    if (!site) {
      if (!this._queryForRedirects.has(url)) {
        this._queryForRedirects.add(url);
        Services.tm.idleDispatchToMainThread(() => fetchIconFromRedirects(url));
      }
      return;
    }

    let iconUri = Services.io.newURI(site.image_url);
    // The #tippytop is to be able to identify them for telemetry.
    iconUri = iconUri.mutate().setRef("tippytop").finalize();
    lazy.PlacesUtils.favicons.setAndFetchFaviconForPage(
      Services.io.newURI(url),
      iconUri,
      false,
      lazy.PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
      null,
      Services.scriptSecurityManager.getSystemPrincipal()
    );
  }

  /**
   * Get the site tippy top data from Remote Settings.
   */
  async getSite(domain) {
    const sites = await this.tippyTop.get({
      filters: { domain },
      syncIfEmpty: false,
    });
    return sites.length ? sites[0] : null;
  }

  /**
   * Get the tippy top collection from Remote Settings.
   */
  get tippyTop() {
    if (!this._tippyTop) {
      this._tippyTop = RemoteSettings("tippytop");
    }
    return this._tippyTop;
  }

  /**
   * Determine if we should be fetching and saving icons.
   */
  get shouldFetchIcons() {
    return Services.prefs.getBoolPref("browser.chrome.site_icons");
  }

  onAction(action) {
    switch (action.type) {
      case at.RICH_ICON_MISSING:
        this.fetchIcon(action.data.url);
        break;
    }
  }
}