DownloadsViewableInternally.sys.mjs (10721B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 /* 6 * TODO: This is based on what PdfJs was already doing, it would be 7 * best to use this over there as well to reduce duplication and 8 * inconsistency. 9 */ 10 11 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 12 13 const lazy = {}; 14 15 XPCOMUtils.defineLazyServiceGetter( 16 lazy, 17 "HandlerService", 18 "@mozilla.org/uriloader/handler-service;1", 19 Ci.nsIHandlerService 20 ); 21 XPCOMUtils.defineLazyServiceGetter( 22 lazy, 23 "MIMEService", 24 "@mozilla.org/mime;1", 25 Ci.nsIMIMEService 26 ); 27 28 ChromeUtils.defineESModuleGetters(lazy, { 29 Integration: "resource://gre/modules/Integration.sys.mjs", 30 }); 31 32 const PREF_BRANCH = "browser.download.viewableInternally."; 33 export const PREF_ENABLED_TYPES = PREF_BRANCH + "enabledTypes"; 34 export const PREF_BRANCH_WAS_REGISTERED = PREF_BRANCH + "typeWasRegistered."; 35 36 export const PREF_BRANCH_PREVIOUS_ACTION = 37 PREF_BRANCH + "previousHandler.preferredAction."; 38 39 export const PREF_BRANCH_PREVIOUS_ASK = 40 PREF_BRANCH + "previousHandler.alwaysAskBeforeHandling."; 41 42 export let DownloadsViewableInternally = { 43 /** 44 * Initially add/remove handlers, watch pref, register with Integration.downloads. 45 */ 46 register() { 47 // Watch the pref 48 XPCOMUtils.defineLazyPreferenceGetter( 49 this, 50 "_enabledTypes", 51 PREF_ENABLED_TYPES, 52 "", 53 () => this._updateAllHandlers(), 54 pref => { 55 let itemStr = pref.trim(); 56 return itemStr ? itemStr.split(",").map(s => s.trim()) : []; 57 } 58 ); 59 60 for (let handlerType of this._downloadTypesViewableInternally) { 61 if (handlerType.initAvailable) { 62 handlerType.initAvailable(); 63 } 64 } 65 66 // Initially update handlers 67 this._updateAllHandlers(); 68 69 // Register the check for use in DownloadIntegration 70 lazy.Integration.downloads.register(() => ({ 71 shouldViewDownloadInternally: 72 this._shouldViewDownloadInternally.bind(this), 73 })); 74 }, 75 76 /** 77 * MIME types to handle with an internal viewer, for downloaded files. 78 * 79 * |extension| is an extenson that will be viewable, as an alternative for 80 * the MIME type itself. It is also used more generally to identify this 81 * type: It is part of a pref name to indicate the handler was set up once, 82 * and it is the string present in |PREF_ENABLED_TYPES| to enable the type. 83 * 84 * |mimeTypes| are the types that will be viewable. A handler is set up for 85 * the first element in the array. 86 * 87 * If |managedElsewhere| is falsy, |_updateAllHandlers()| will set 88 * up or remove handlers for the type, and |_shouldViewDownloadInternally()| 89 * will check for it in |PREF_ENABLED_TYPES|. 90 * 91 * |available| is used to check whether this type should have 92 * handleInternally handlers set up, and if false then 93 * |_shouldViewDownloadInternally()| will also return false for this 94 * type. If |available| would change, |DownloadsViewableInternally._updateHandler()| 95 * should be called for the type. 96 * 97 * |initAvailable()| is an opportunity to initially set |available|, set up 98 * observers to change it when prefs change, etc. 99 * 100 */ 101 _downloadTypesViewableInternally: [ 102 { 103 extension: "xml", 104 mimeTypes: ["text/xml", "application/xml"], 105 available: true, 106 managedElsewhere: true, 107 }, 108 { 109 extension: "svg", 110 mimeTypes: ["image/svg+xml"], 111 112 initAvailable() { 113 XPCOMUtils.defineLazyPreferenceGetter( 114 this, 115 "available", 116 "svg.disabled", 117 true, 118 () => DownloadsViewableInternally._updateHandler(this), 119 // transform disabled to enabled/available 120 disabledPref => !disabledPref 121 ); 122 }, 123 // available getter is set by initAvailable() 124 managedElsewhere: true, 125 }, 126 { 127 extension: "webp", 128 mimeTypes: ["image/webp"], 129 available: true, 130 managedElsewhere: false, 131 }, 132 { 133 extension: "avif", 134 mimeTypes: ["image/avif"], 135 available: true, 136 managedElsewhere: false, 137 }, 138 { 139 extension: "jxl", 140 mimeTypes: ["image/jxl"], 141 initAvailable() { 142 XPCOMUtils.defineLazyPreferenceGetter( 143 this, 144 "available", 145 "image.jxl.enabled", 146 false, 147 () => DownloadsViewableInternally._updateHandler(this) 148 ); 149 }, 150 // available getter is set by initAvailable() 151 }, 152 { 153 extension: "pdf", 154 mimeTypes: ["application/pdf"], 155 // PDF uses pdfjs.disabled rather than PREF_ENABLED_TYPES. 156 // pdfjs.disabled isn't checked here because PdfJs's own _becomeHandler 157 // and _unbecomeHandler manage the handler if the pref is set, and there 158 // is an explicit check in nsUnknownContentTypeDialog.shouldShowInternalHandlerOption 159 available: true, 160 managedElsewhere: true, 161 }, 162 ], 163 164 /* 165 * Implementation for DownloadIntegration.shouldViewDownloadInternally 166 */ 167 _shouldViewDownloadInternally(aMimeType, aExtension) { 168 if (!aMimeType) { 169 return false; 170 } 171 172 return this._downloadTypesViewableInternally.some(handlerType => { 173 if ( 174 !handlerType.managedElsewhere && 175 !this._enabledTypes.includes(handlerType.extension) 176 ) { 177 return false; 178 } 179 180 return ( 181 (handlerType.mimeTypes.includes(aMimeType) || 182 handlerType.extension == aExtension?.toLowerCase()) && 183 handlerType.available 184 ); 185 }); 186 }, 187 188 _makeFakeHandler(aMimeType, aExtension) { 189 // Based on PdfJs gPdfFakeHandlerInfo. 190 return { 191 QueryInterface: ChromeUtils.generateQI(["nsIMIMEInfo"]), 192 getFileExtensions() { 193 return [aExtension]; 194 }, 195 possibleApplicationHandlers: Cc["@mozilla.org/array;1"].createInstance( 196 Ci.nsIMutableArray 197 ), 198 extensionExists(ext) { 199 return ext == aExtension; 200 }, 201 alwaysAskBeforeHandling: false, 202 preferredAction: Ci.nsIHandlerInfo.handleInternally, 203 type: aMimeType, 204 }; 205 }, 206 207 _saveSettings(handlerInfo, handlerType) { 208 Services.prefs.setIntPref( 209 PREF_BRANCH_PREVIOUS_ACTION + handlerType.extension, 210 handlerInfo.preferredAction 211 ); 212 Services.prefs.setBoolPref( 213 PREF_BRANCH_PREVIOUS_ASK + handlerType.extension, 214 handlerInfo.alwaysAskBeforeHandling 215 ); 216 }, 217 218 _restoreSettings(handlerInfo, handlerType) { 219 const prevActionPref = PREF_BRANCH_PREVIOUS_ACTION + handlerType.extension; 220 if (Services.prefs.prefHasUserValue(prevActionPref)) { 221 handlerInfo.alwaysAskBeforeHandling = Services.prefs.getBoolPref( 222 PREF_BRANCH_PREVIOUS_ASK + handlerType.extension 223 ); 224 handlerInfo.preferredAction = Services.prefs.getIntPref(prevActionPref); 225 lazy.HandlerService.store(handlerInfo); 226 } else { 227 // Nothing to restore, just remove the handler. 228 lazy.HandlerService.remove(handlerInfo); 229 } 230 }, 231 232 _clearSavedSettings(extension) { 233 Services.prefs.clearUserPref(PREF_BRANCH_PREVIOUS_ACTION + extension); 234 Services.prefs.clearUserPref(PREF_BRANCH_PREVIOUS_ASK + extension); 235 }, 236 237 _updateAllHandlers() { 238 // Set up or remove handlers for each type, if not done already 239 for (const handlerType of this._downloadTypesViewableInternally) { 240 if (!handlerType.managedElsewhere) { 241 this._updateHandler(handlerType); 242 } 243 } 244 }, 245 246 _updateHandler(handlerType) { 247 const wasRegistered = Services.prefs.getBoolPref( 248 PREF_BRANCH_WAS_REGISTERED + handlerType.extension, 249 false 250 ); 251 252 const toBeRegistered = 253 this._enabledTypes.includes(handlerType.extension) && 254 handlerType.available; 255 256 if (toBeRegistered && !wasRegistered) { 257 this._becomeHandler(handlerType); 258 } else if (!toBeRegistered && wasRegistered) { 259 this._unbecomeHandler(handlerType); 260 } 261 }, 262 263 _becomeHandler(handlerType) { 264 // Set up an empty handler with only a preferred action, to avoid 265 // having to ask the OS about handlers on startup. 266 let fakeHandlerInfo = this._makeFakeHandler( 267 handlerType.mimeTypes[0], 268 handlerType.extension 269 ); 270 if (!lazy.HandlerService.exists(fakeHandlerInfo)) { 271 lazy.HandlerService.store(fakeHandlerInfo); 272 } else { 273 const handlerInfo = lazy.MIMEService.getFromTypeAndExtension( 274 handlerType.mimeTypes[0], 275 handlerType.extension 276 ); 277 278 if (handlerInfo.preferredAction != Ci.nsIHandlerInfo.handleInternally) { 279 // Save the previous settings of preferredAction and 280 // alwaysAskBeforeHandling in case we need to revert them. 281 // Even if we don't force preferredAction here, the user could 282 // set handleInternally manually. 283 this._saveSettings(handlerInfo, handlerType); 284 } else { 285 // handleInternally shouldn't already have been set, the best we 286 // can do to restore is to remove the handler, so make sure 287 // the settings are clear. 288 this._clearSavedSettings(handlerType.extension); 289 } 290 291 // Replace the preferred action if it didn't indicate an external viewer. 292 // Note: This is a point of departure from PdfJs, which always replaces 293 // the preferred action. 294 if ( 295 handlerInfo.preferredAction != Ci.nsIHandlerInfo.useHelperApp && 296 handlerInfo.preferredAction != Ci.nsIHandlerInfo.useSystemDefault 297 ) { 298 handlerInfo.preferredAction = Ci.nsIHandlerInfo.handleInternally; 299 handlerInfo.alwaysAskBeforeHandling = false; 300 301 lazy.HandlerService.store(handlerInfo); 302 } 303 } 304 305 // Note that we set up for this type so a) we don't keep replacing the 306 // handler and b) so it can be cleared later. 307 Services.prefs.setBoolPref( 308 PREF_BRANCH_WAS_REGISTERED + handlerType.extension, 309 true 310 ); 311 }, 312 313 _unbecomeHandler(handlerType) { 314 let handlerInfo; 315 try { 316 handlerInfo = lazy.MIMEService.getFromTypeAndExtension( 317 handlerType.mimeTypes[0], 318 handlerType.extension 319 ); 320 } catch (ex) { 321 // Allow the handler lookup to fail. 322 } 323 // Restore preferred action if it is still handleInternally 324 // (possibly just removing the handler if nothing was saved for it). 325 if (handlerInfo?.preferredAction == Ci.nsIHandlerInfo.handleInternally) { 326 this._restoreSettings(handlerInfo, handlerType); 327 } 328 329 // In any case we do not control this handler now. 330 this._clearSavedSettings(handlerType.extension); 331 Services.prefs.clearUserPref( 332 PREF_BRANCH_WAS_REGISTERED + handlerType.extension 333 ); 334 }, 335 };