summaryrefslogtreecommitdiffstats
path: root/browser/components/newtab/lib/Screenshots.sys.mjs
blob: e5423bd52ffc72c092a33fd00d2f1d9b91cbc3fa (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
/* 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/. */

// 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
// XPCOMUtils, 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 { XPCOMUtils } = ChromeUtils.importESModule(
  "resource://gre/modules/XPCOMUtils.sys.mjs"
);

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  BackgroundPageThumbs: "resource://gre/modules/BackgroundPageThumbs.sys.mjs",
  PageThumbs: "resource://gre/modules/PageThumbs.sys.mjs",
  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
});

const GREY_10 = "#F9F9FA";

XPCOMUtils.defineLazyPreferenceGetter(
  lazy,
  "gPrivilegedAboutProcessEnabled",
  "browser.tabs.remote.separatePrivilegedContentProcess",
  false
);

export const Screenshots = {
  /**
   * Get a screenshot / thumbnail for a url. Either returns the disk cached
   * image or initiates a background request for the url.
   *
   * @param url {string} The url to get a thumbnail
   * @return {Promise} Resolves a custom object or null if failed
   */
  async getScreenshotForURL(url) {
    try {
      await lazy.BackgroundPageThumbs.captureIfMissing(url, {
        backgroundColor: GREY_10,
      });

      // The privileged about content process is able to use the moz-page-thumb
      // protocol, so if it's enabled, send that down.
      if (lazy.gPrivilegedAboutProcessEnabled) {
        return lazy.PageThumbs.getThumbnailURL(url);
      }

      // Otherwise, for normal content processes, we fallback to using
      // Blob URIs for the screenshots.
      const imgPath = lazy.PageThumbs.getThumbnailPath(url);

      const filePathResponse = await fetch(`file://${imgPath}`);
      const fileContents = await filePathResponse.blob();

      // Check if the file is empty, which indicates there isn't actually a
      // thumbnail, so callers can show a failure state.
      if (fileContents.size === 0) {
        return null;
      }

      return { path: imgPath, data: fileContents };
    } catch (err) {
      console.error(`getScreenshot(${url}) failed:`, err);
    }

    // We must have failed to get the screenshot, so persist the failure by
    // storing an empty file. Future calls will then skip requesting and return
    // failure, so do the same thing here. The empty file should not expire with
    // the usual filtering process to avoid repeated background requests, which
    // can cause unwanted high CPU, network and memory usage - Bug 1384094
    try {
      await lazy.PageThumbs._store(url, url, null, true);
    } catch (err) {
      // Probably failed to create the empty file, but not much more we can do.
    }
    return null;
  },

  /**
   * Checks if all the open windows are private browsing windows. If so, we do not
   * want to collect screenshots. If there exists at least 1 non-private window,
   * we are ok to collect screenshots.
   */
  _shouldGetScreenshots() {
    for (let win of Services.wm.getEnumerator("navigator:browser")) {
      if (!lazy.PrivateBrowsingUtils.isWindowPrivate(win)) {
        // As soon as we encounter 1 non-private window, screenshots are fair game.
        return true;
      }
    }
    return false;
  },

  /**
   * Conditionally get a screenshot for a link if there's no existing pending
   * screenshot. Updates the cached link's desired property with the result.
   *
   * @param link {object} Link object to update
   * @param url {string} Url to get a screenshot of
   * @param property {string} Name of property on object to set
   @ @param onScreenshot {function} Callback for when the screenshot loads
   */
  async maybeCacheScreenshot(link, url, property, onScreenshot) {
    // If there are only private windows open, do not collect screenshots
    if (!this._shouldGetScreenshots()) {
      return;
    }
    // __sharedCache may not exist yet for links from default top sites that
    // don't have a default tippy top icon.
    if (!link.__sharedCache) {
      link.__sharedCache = {
        updateLink(prop, val) {
          link[prop] = val;
        },
      };
    }
    const cache = link.__sharedCache;
    // Nothing to do if we already have a pending screenshot or
    // if a previous request failed and returned null.
    if (cache.fetchingScreenshot || link[property] !== undefined) {
      return;
    }

    // Save the promise to the cache so other links get it immediately
    cache.fetchingScreenshot = this.getScreenshotForURL(url);

    // Clean up now that we got the screenshot
    const screenshot = await cache.fetchingScreenshot;
    delete cache.fetchingScreenshot;

    // Update the cache for future links and call back for existing content
    cache.updateLink(property, screenshot);
    onScreenshot(screenshot);
  },
};