summaryrefslogtreecommitdiffstats
path: root/browser/components/downloads/DownloadsViewableInternally.sys.mjs
blob: 9684e28537f2a3462b3880989486ed8a6cda3625 (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
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
/* 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/. */

/*
 * TODO: This is based on what PdfJs was already doing, it would be
 * best to use this over there as well to reduce duplication and
 * inconsistency.
 */

import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

const lazy = {};

XPCOMUtils.defineLazyServiceGetter(
  lazy,
  "HandlerService",
  "@mozilla.org/uriloader/handler-service;1",
  "nsIHandlerService"
);
XPCOMUtils.defineLazyServiceGetter(
  lazy,
  "MIMEService",
  "@mozilla.org/mime;1",
  "nsIMIMEService"
);

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

const PREF_BRANCH = "browser.download.viewableInternally.";
export const PREF_ENABLED_TYPES = PREF_BRANCH + "enabledTypes";
export const PREF_BRANCH_WAS_REGISTERED = PREF_BRANCH + "typeWasRegistered.";

export const PREF_BRANCH_PREVIOUS_ACTION =
  PREF_BRANCH + "previousHandler.preferredAction.";

export const PREF_BRANCH_PREVIOUS_ASK =
  PREF_BRANCH + "previousHandler.alwaysAskBeforeHandling.";

export let DownloadsViewableInternally = {
  /**
   * Initially add/remove handlers, watch pref, register with Integration.downloads.
   */
  register() {
    // Watch the pref
    XPCOMUtils.defineLazyPreferenceGetter(
      this,
      "_enabledTypes",
      PREF_ENABLED_TYPES,
      "",
      () => this._updateAllHandlers(),
      pref => {
        let itemStr = pref.trim();
        return itemStr ? itemStr.split(",").map(s => s.trim()) : [];
      }
    );

    for (let handlerType of this._downloadTypesViewableInternally) {
      if (handlerType.initAvailable) {
        handlerType.initAvailable();
      }
    }

    // Initially update handlers
    this._updateAllHandlers();

    // Register the check for use in DownloadIntegration
    lazy.Integration.downloads.register(base => ({
      shouldViewDownloadInternally:
        this._shouldViewDownloadInternally.bind(this),
    }));
  },

  /**
   * MIME types to handle with an internal viewer, for downloaded files.
   *
   * |extension| is an extenson that will be viewable, as an alternative for
   *   the MIME type itself. It is also used more generally to identify this
   *   type: It is part of a pref name to indicate the handler was set up once,
   *   and it is the string present in |PREF_ENABLED_TYPES| to enable the type.
   *
   * |mimeTypes| are the types that will be viewable. A handler is set up for
   *   the first element in the array.
   *
   * If |managedElsewhere| is falsy, |_updateAllHandlers()| will set
   *   up or remove handlers for the type, and |_shouldViewDownloadInternally()|
   *   will check for it in |PREF_ENABLED_TYPES|.
   *
   * |available| is used to check whether this type should have
   *   handleInternally handlers set up, and if false then
   *   |_shouldViewDownloadInternally()| will also return false for this
   *   type. If |available| would change, |DownloadsViewableInternally._updateHandler()|
   *   should be called for the type.
   *
   * |initAvailable()| is an opportunity to initially set |available|, set up
   *   observers to change it when prefs change, etc.
   *
   */
  _downloadTypesViewableInternally: [
    {
      extension: "xml",
      mimeTypes: ["text/xml", "application/xml"],
      available: true,
      managedElsewhere: true,
    },
    {
      extension: "svg",
      mimeTypes: ["image/svg+xml"],

      initAvailable() {
        XPCOMUtils.defineLazyPreferenceGetter(
          this,
          "available",
          "svg.disabled",
          true,
          () => DownloadsViewableInternally._updateHandler(this),
          // transform disabled to enabled/available
          disabledPref => !disabledPref
        );
      },
      // available getter is set by initAvailable()
      managedElsewhere: true,
    },
    {
      extension: "webp",
      mimeTypes: ["image/webp"],
      initAvailable() {
        XPCOMUtils.defineLazyPreferenceGetter(
          this,
          "available",
          "image.webp.enabled",
          false,
          () => DownloadsViewableInternally._updateHandler(this)
        );
      },
      // available getter is set by initAvailable()
    },
    {
      extension: "avif",
      mimeTypes: ["image/avif"],
      initAvailable() {
        XPCOMUtils.defineLazyPreferenceGetter(
          this,
          "available",
          "image.avif.enabled",
          false,
          () => DownloadsViewableInternally._updateHandler(this)
        );
      },
      // available getter is set by initAvailable()
    },
    {
      extension: "jxl",
      mimeTypes: ["image/jxl"],
      initAvailable() {
        XPCOMUtils.defineLazyPreferenceGetter(
          this,
          "available",
          "image.jxl.enabled",
          false,
          () => DownloadsViewableInternally._updateHandler(this)
        );
      },
      // available getter is set by initAvailable()
    },
    {
      extension: "pdf",
      mimeTypes: ["application/pdf"],
      // PDF uses pdfjs.disabled rather than PREF_ENABLED_TYPES.
      // pdfjs.disabled isn't checked here because PdfJs's own _becomeHandler
      // and _unbecomeHandler manage the handler if the pref is set, and there
      // is an explicit check in nsUnknownContentTypeDialog.shouldShowInternalHandlerOption
      available: true,
      managedElsewhere: true,
    },
  ],

  /*
   * Implementation for DownloadIntegration.shouldViewDownloadInternally
   */
  _shouldViewDownloadInternally(aMimeType, aExtension) {
    if (!aMimeType) {
      return false;
    }

    return this._downloadTypesViewableInternally.some(handlerType => {
      if (
        !handlerType.managedElsewhere &&
        !this._enabledTypes.includes(handlerType.extension)
      ) {
        return false;
      }

      return (
        (handlerType.mimeTypes.includes(aMimeType) ||
          handlerType.extension == aExtension?.toLowerCase()) &&
        handlerType.available
      );
    });
  },

  _makeFakeHandler(aMimeType, aExtension) {
    // Based on PdfJs gPdfFakeHandlerInfo.
    return {
      QueryInterface: ChromeUtils.generateQI(["nsIMIMEInfo"]),
      getFileExtensions() {
        return [aExtension];
      },
      possibleApplicationHandlers: Cc["@mozilla.org/array;1"].createInstance(
        Ci.nsIMutableArray
      ),
      extensionExists(ext) {
        return ext == aExtension;
      },
      alwaysAskBeforeHandling: false,
      preferredAction: Ci.nsIHandlerInfo.handleInternally,
      type: aMimeType,
    };
  },

  _saveSettings(handlerInfo, handlerType) {
    Services.prefs.setIntPref(
      PREF_BRANCH_PREVIOUS_ACTION + handlerType.extension,
      handlerInfo.preferredAction
    );
    Services.prefs.setBoolPref(
      PREF_BRANCH_PREVIOUS_ASK + handlerType.extension,
      handlerInfo.alwaysAskBeforeHandling
    );
  },

  _restoreSettings(handlerInfo, handlerType) {
    const prevActionPref = PREF_BRANCH_PREVIOUS_ACTION + handlerType.extension;
    if (Services.prefs.prefHasUserValue(prevActionPref)) {
      handlerInfo.alwaysAskBeforeHandling = Services.prefs.getBoolPref(
        PREF_BRANCH_PREVIOUS_ASK + handlerType.extension
      );
      handlerInfo.preferredAction = Services.prefs.getIntPref(prevActionPref);
      lazy.HandlerService.store(handlerInfo);
    } else {
      // Nothing to restore, just remove the handler.
      lazy.HandlerService.remove(handlerInfo);
    }
  },

  _clearSavedSettings(extension) {
    Services.prefs.clearUserPref(PREF_BRANCH_PREVIOUS_ACTION + extension);
    Services.prefs.clearUserPref(PREF_BRANCH_PREVIOUS_ASK + extension);
  },

  _updateAllHandlers() {
    // Set up or remove handlers for each type, if not done already
    for (const handlerType of this._downloadTypesViewableInternally) {
      if (!handlerType.managedElsewhere) {
        this._updateHandler(handlerType);
      }
    }
  },

  _updateHandler(handlerType) {
    const wasRegistered = Services.prefs.getBoolPref(
      PREF_BRANCH_WAS_REGISTERED + handlerType.extension,
      false
    );

    const toBeRegistered =
      this._enabledTypes.includes(handlerType.extension) &&
      handlerType.available;

    if (toBeRegistered && !wasRegistered) {
      this._becomeHandler(handlerType);
    } else if (!toBeRegistered && wasRegistered) {
      this._unbecomeHandler(handlerType);
    }
  },

  _becomeHandler(handlerType) {
    // Set up an empty handler with only a preferred action, to avoid
    // having to ask the OS about handlers on startup.
    let fakeHandlerInfo = this._makeFakeHandler(
      handlerType.mimeTypes[0],
      handlerType.extension
    );
    if (!lazy.HandlerService.exists(fakeHandlerInfo)) {
      lazy.HandlerService.store(fakeHandlerInfo);
    } else {
      const handlerInfo = lazy.MIMEService.getFromTypeAndExtension(
        handlerType.mimeTypes[0],
        handlerType.extension
      );

      if (handlerInfo.preferredAction != Ci.nsIHandlerInfo.handleInternally) {
        // Save the previous settings of preferredAction and
        // alwaysAskBeforeHandling in case we need to revert them.
        // Even if we don't force preferredAction here, the user could
        // set handleInternally manually.
        this._saveSettings(handlerInfo, handlerType);
      } else {
        // handleInternally shouldn't already have been set, the best we
        // can do to restore is to remove the handler, so make sure
        // the settings are clear.
        this._clearSavedSettings(handlerType.extension);
      }

      // Replace the preferred action if it didn't indicate an external viewer.
      // Note: This is a point of departure from PdfJs, which always replaces
      // the preferred action.
      if (
        handlerInfo.preferredAction != Ci.nsIHandlerInfo.useHelperApp &&
        handlerInfo.preferredAction != Ci.nsIHandlerInfo.useSystemDefault
      ) {
        handlerInfo.preferredAction = Ci.nsIHandlerInfo.handleInternally;
        handlerInfo.alwaysAskBeforeHandling = false;

        lazy.HandlerService.store(handlerInfo);
      }
    }

    // Note that we set up for this type so a) we don't keep replacing the
    // handler and b) so it can be cleared later.
    Services.prefs.setBoolPref(
      PREF_BRANCH_WAS_REGISTERED + handlerType.extension,
      true
    );
  },

  _unbecomeHandler(handlerType) {
    let handlerInfo;
    try {
      handlerInfo = lazy.MIMEService.getFromTypeAndExtension(
        handlerType.mimeTypes[0],
        handlerType.extension
      );
    } catch (ex) {
      // Allow the handler lookup to fail.
    }
    // Restore preferred action if it is still handleInternally
    // (possibly just removing the handler if nothing was saved for it).
    if (handlerInfo?.preferredAction == Ci.nsIHandlerInfo.handleInternally) {
      this._restoreSettings(handlerInfo, handlerType);
    }

    // In any case we do not control this handler now.
    this._clearSavedSettings(handlerType.extension);
    Services.prefs.clearUserPref(
      PREF_BRANCH_WAS_REGISTERED + handlerType.extension
    );
  },
};