Bug 1867925 - Mark some storage-access-api tests as intermittent after wpt-sync....
[gecko.git] / toolkit / modules / GMPInstallManager.sys.mjs
blob9e27900fbc9089ba89b4b96ccf71b9713229b90e
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/. */
5 // 1 day default
6 const DEFAULT_SECONDS_BETWEEN_CHECKS = 60 * 60 * 24;
8 import { PromiseUtils } from "resource://gre/modules/PromiseUtils.sys.mjs";
10 import { Log } from "resource://gre/modules/Log.sys.mjs";
11 import {
12   GMPPrefs,
13   GMPUtils,
14   GMP_PLUGIN_IDS,
15   WIDEVINE_L1_ID,
16   WIDEVINE_L3_ID,
17 } from "resource://gre/modules/GMPUtils.sys.mjs";
19 import { ProductAddonChecker } from "resource://gre/modules/addons/ProductAddonChecker.sys.mjs";
21 const lazy = {};
23 ChromeUtils.defineESModuleGetters(lazy, {
24   CertUtils: "resource://gre/modules/CertUtils.sys.mjs",
25   FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
26   ServiceRequest: "resource://gre/modules/ServiceRequest.sys.mjs",
27   UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs",
28 });
30 function getScopedLogger(prefix) {
31   // `PARENT_LOGGER_ID.` being passed here effectively links this logger
32   // to the parentLogger.
33   return Log.repository.getLoggerWithMessagePrefix("Toolkit.GMP", prefix + " ");
36 const LOCAL_GMP_SOURCES = [
37   {
38     id: "gmp-gmpopenh264",
39     src: "chrome://global/content/gmp-sources/openh264.json",
40     installByDefault: true,
41   },
42   {
43     id: "gmp-widevinecdm",
44     src: "chrome://global/content/gmp-sources/widevinecdm.json",
45     installByDefault: true,
46   },
47   {
48     id: "gmp-widevinecdm-l1",
49     src: "chrome://global/content/gmp-sources/widevinecdm_l1.json",
50     installByDefault: false,
51   },
54 function downloadJSON(uri) {
55   let log = getScopedLogger("GMPInstallManager.checkForAddons");
56   log.info("fetching config from: " + uri);
57   return new Promise((resolve, reject) => {
58     let xmlHttp = new lazy.ServiceRequest({ mozAnon: true });
60     xmlHttp.onload = function (aResponse) {
61       resolve(JSON.parse(this.responseText));
62     };
64     xmlHttp.onerror = function (e) {
65       reject("Fetching " + uri + " results in error code: " + e.target.status);
66     };
68     xmlHttp.open("GET", uri);
69     xmlHttp.overrideMimeType("application/json");
70     xmlHttp.send();
71   });
74 /**
75  * If downloading from the network fails (AUS server is down),
76  * load the sources from local build configuration.
77  */
78 function downloadLocalConfig(sources) {
79   if (!sources.length) {
80     return Promise.resolve({ addons: [] });
81   }
83   let log = getScopedLogger("GMPInstallManager.downloadLocalConfig");
84   return Promise.all(
85     sources.map(conf => {
86       return downloadJSON(conf.src).then(addons => {
87         let platforms = addons.vendors[conf.id].platforms;
88         let target = Services.appinfo.OS + "_" + lazy.UpdateUtils.ABI;
89         let details = null;
91         while (!details) {
92           if (!(target in platforms)) {
93             // There was no matching platform so return false, this addon
94             // will be filtered from the results below
95             log.info("no details found for: " + target);
96             return false;
97           }
98           // Field either has the details of the binary or is an alias
99           // to another build target key that does
100           if (platforms[target].alias) {
101             target = platforms[target].alias;
102           } else {
103             details = platforms[target];
104           }
105         }
107         log.info("found plugin: " + conf.id);
108         return {
109           id: conf.id,
110           URL: details.fileUrl,
111           hashFunction: addons.hashFunction,
112           hashValue: details.hashValue,
113           version: addons.vendors[conf.id].version,
114           size: details.filesize,
115           usedFallback: true,
116         };
117       });
118     })
119   ).then(addons => {
120     // Some filters may not match this platform so
121     // filter those out
122     return { addons: addons.filter(x => x !== false) };
123   });
127  * Provides an easy API for downloading and installing GMP Addons
128  */
129 export function GMPInstallManager() {}
132  * Temp file name used for downloading
133  */
134 GMPInstallManager.prototype = {
135   /**
136    * Obtains a URL with replacement of vars
137    */
138   async _getURL() {
139     let log = getScopedLogger("GMPInstallManager._getURL");
140     // Use the override URL if it is specified.  The override URL is just like
141     // the normal URL but it does not check the cert.
142     let url = GMPPrefs.getString(GMPPrefs.KEY_URL_OVERRIDE, "");
143     if (url) {
144       log.info("Using override url: " + url);
145     } else {
146       url = GMPPrefs.getString(GMPPrefs.KEY_URL);
147       log.info("Using url: " + url);
148     }
150     url = await lazy.UpdateUtils.formatUpdateURL(url);
152     log.info("Using url (with replacement): " + url);
153     return url;
154   },
156   /**
157    * Records telemetry results on if fetching update.xml from Balrog succeeded
158    * when content signature was used to verify the response from Balrog.
159    * @param didGetAddonList
160    *        A boolean indicating if an update.xml containing the addon list was
161    *        successfully fetched (true) or not (false).
162    * @param err
163    *        The error that was thrown (if it exists) for the failure case. This
164    *        is expected to have a addonCheckerErr member which provides further
165    *        information on why the addon checker failed.
166    */
167   recordUpdateXmlTelemetryForContentSignature(didGetAddonList, err = null) {
168     let log = getScopedLogger(
169       "GMPInstallManager.recordUpdateXmlTelemetryForContentSignature"
170     );
171     try {
172       let updateResultHistogram = Services.telemetry.getHistogramById(
173         "MEDIA_GMP_UPDATE_XML_FETCH_RESULT"
174       );
176       // The non-glean telemetry used here will be removed in future and just
177       // the glean data will be gathered.
178       if (didGetAddonList) {
179         updateResultHistogram.add("content_sig_ok");
180         Glean.gmp.updateXmlFetchResult.content_sig_success.add(1);
181         return;
182       }
183       // All remaining cases are failure cases.
184       updateResultHistogram.add("content_sig_fail");
185       if (!err?.addonCheckerErr) {
186         // Unknown error case. If this is happening we should audit error paths
187         // to identify why we're not getting an error, or not getting it
188         // labelled.
189         Glean.gmp.updateXmlFetchResult.content_sig_unknown_error.add(1);
190         return;
191       }
192       const errorToHistogramMap = {
193         [ProductAddonChecker.NETWORK_REQUEST_ERR]:
194           "content_sig_net_request_error",
195         [ProductAddonChecker.NETWORK_TIMEOUT_ERR]: "content_sig_net_timeout",
196         [ProductAddonChecker.ABORT_ERR]: "content_sig_abort",
197         [ProductAddonChecker.VERIFICATION_MISSING_DATA_ERR]:
198           "content_sig_missing_data",
199         [ProductAddonChecker.VERIFICATION_FAILED_ERR]: "content_sig_failed",
200         [ProductAddonChecker.VERIFICATION_INVALID_ERR]: "content_sig_invalid",
201         [ProductAddonChecker.XML_PARSE_ERR]: "content_sig_xml_parse_error",
202       };
203       let metricID =
204         errorToHistogramMap[err.addonCheckerErr] ?? "content_sig_unknown_error";
205       let metric = Glean.gmp.updateXmlFetchResult[metricID];
206       metric.add(1);
207     } catch (e) {
208       // We don't expect this path to be hit, but we don't want telemetry
209       // failures to break GMP updates, so catch any issues here and let the
210       // update machinery continue.
211       log.error(
212         `Failed to record telemetry result of getProductAddonList, got error: ${e}`
213       );
214     }
215   },
217   /**
218    * Records telemetry results on if fetching update.xml from Balrog succeeded
219    * when cert pinning was used to verify the response from Balrog. This
220    * should be removed once we're no longer using cert pinning.
221    * @param didGetAddonList
222    *        A boolean indicating if an update.xml containing the addon list was
223    *        successfully fetched (true) or not (false).
224    * @param err
225    *        The error that was thrown (if it exists) for the failure case. This
226    *        is expected to have a addonCheckerErr member which provides further
227    *        information on why the addon checker failed.
228    */
229   recordUpdateXmlTelemetryForCertPinning(didGetAddonList, err = null) {
230     let log = getScopedLogger(
231       "GMPInstallManager.recordUpdateXmlTelemetryForCertPinning"
232     );
233     try {
234       let updateResultHistogram = Services.telemetry.getHistogramById(
235         "MEDIA_GMP_UPDATE_XML_FETCH_RESULT"
236       );
238       // The non-glean telemetry used here will be removed in future and just
239       // the glean data will be gathered.
240       if (didGetAddonList) {
241         updateResultHistogram.add("cert_pinning_ok");
242         Glean.gmp.updateXmlFetchResult.cert_pin_success.add(1);
243         return;
244       }
245       // All remaining cases are failure cases.
246       updateResultHistogram.add("cert_pinning_fail");
247       if (!err?.addonCheckerErr) {
248         // Unknown error case. If this is happening we should audit error paths
249         // to identify why we're not getting an error, or not getting it
250         // labelled.
251         Glean.gmp.updateXmlFetchResult.cert_pin_unknown_error.add(1);
252         return;
253       }
254       const errorToHistogramMap = {
255         [ProductAddonChecker.NETWORK_REQUEST_ERR]: "cert_pin_net_request_error",
256         [ProductAddonChecker.NETWORK_TIMEOUT_ERR]: "cert_pin_net_timeout",
257         [ProductAddonChecker.ABORT_ERR]: "cert_pin_abort",
258         [ProductAddonChecker.VERIFICATION_MISSING_DATA_ERR]:
259           "cert_pin_missing_data",
260         [ProductAddonChecker.VERIFICATION_FAILED_ERR]: "cert_pin_failed",
261         [ProductAddonChecker.VERIFICATION_INVALID_ERR]: "cert_pin_invalid",
262         [ProductAddonChecker.XML_PARSE_ERR]: "cert_pin_xml_parse_error",
263       };
264       let metricID =
265         errorToHistogramMap[err.addonCheckerErr] ?? "cert_pin_unknown_error";
266       let metric = Glean.gmp.updateXmlFetchResult[metricID];
267       metric.add(1);
268     } catch (e) {
269       // We don't expect this path to be hit, but we don't want telemetry
270       // failures to break GMP updates, so catch any issues here and let the
271       // update machinery continue.
272       log.error(
273         `Failed to record telemetry result of getProductAddonList, got error: ${e}`
274       );
275     }
276   },
278   /**
279    * Performs an addon check.
280    * @return a promise which will be resolved or rejected.
281    *         The promise is resolved with an object with properties:
282    *           addons: array of addons
283    *           usedFallback: whether the data was collected from online or
284    *                         from fallback data within the build
285    *         The promise is rejected with an object with properties:
286    *           target: The XHR request object
287    *           status: The HTTP status code
288    *           type: Sometimes specifies type of rejection
289    */
290   async checkForAddons() {
291     let log = getScopedLogger("GMPInstallManager.checkForAddons");
292     if (this._deferred) {
293       log.error("checkForAddons already called");
294       return Promise.reject({ type: "alreadycalled" });
295     }
297     if (!GMPPrefs.getBool(GMPPrefs.KEY_UPDATE_ENABLED, true)) {
298       log.info("Updates are disabled via media.gmp-manager.updateEnabled");
299       return { usedFallback: true, addons: [] };
300     }
302     this._deferred = PromiseUtils.defer();
303     let deferredPromise = this._deferred.promise;
305     // Should content signature checking of Balrog replies be used? If so this
306     // will be done instead of the older cert pinning method.
307     let checkContentSignature = GMPPrefs.getBool(
308       GMPPrefs.KEY_CHECK_CONTENT_SIGNATURE,
309       true
310     );
312     let allowNonBuiltIn = true;
313     let certs = null;
314     // Only check certificates if we're not using a custom URL, and only if
315     // we're not checking a content signature.
316     if (
317       !Services.prefs.prefHasUserValue(GMPPrefs.KEY_URL_OVERRIDE) &&
318       !checkContentSignature
319     ) {
320       allowNonBuiltIn = !GMPPrefs.getString(
321         GMPPrefs.KEY_CERT_REQUIREBUILTIN,
322         true
323       );
324       if (GMPPrefs.getBool(GMPPrefs.KEY_CERT_CHECKATTRS, true)) {
325         certs = lazy.CertUtils.readCertPrefs(GMPPrefs.KEY_CERTS_BRANCH);
326       }
327     }
329     let url = await this._getURL();
331     log.info(
332       `Fetching product addon list url=${url}, allowNonBuiltIn=${allowNonBuiltIn}, certs=${certs}, checkContentSignature=${checkContentSignature}`
333     );
335     let success = true;
336     let res;
337     try {
338       res = await ProductAddonChecker.getProductAddonList(
339         url,
340         allowNonBuiltIn,
341         certs,
342         checkContentSignature
343       );
345       if (checkContentSignature) {
346         this.recordUpdateXmlTelemetryForContentSignature(true);
347       } else {
348         this.recordUpdateXmlTelemetryForCertPinning(true);
349       }
350     } catch (err) {
351       success = false;
352       if (checkContentSignature) {
353         this.recordUpdateXmlTelemetryForContentSignature(false, err);
354       } else {
355         this.recordUpdateXmlTelemetryForCertPinning(false, err);
356       }
357     }
359     try {
360       if (!success) {
361         log.info("Falling back to local config");
362         let fallbackSources = LOCAL_GMP_SOURCES.filter(function (gmpSource) {
363           return gmpSource.installByDefault;
364         });
365         res = await downloadLocalConfig(fallbackSources);
366       }
367     } catch (err) {
368       this._deferred.reject(err);
369       delete this._deferred;
370       return deferredPromise;
371     }
373     let addons;
374     if (res && res.addons) {
375       addons = res.addons.map(a => new GMPAddon(a));
376     } else {
377       addons = [];
378     }
380     // We need to merge in the addons that are only available via fallback that
381     // the user has requested be forced installed regardless of our update
382     // server configuration.
383     try {
384       let forcedSources = LOCAL_GMP_SOURCES.filter(function (gmpSource) {
385         return GMPPrefs.getBool(
386           GMPPrefs.KEY_PLUGIN_FORCE_INSTALL,
387           false,
388           gmpSource.id
389         );
390       });
392       let forcedConfigs = await downloadLocalConfig(
393         forcedSources.filter(function (gmpSource) {
394           return !addons.find(gmpAddon => gmpAddon.id == gmpSource.id);
395         })
396       );
398       let forcedAddons = forcedConfigs.addons.map(
399         config => new GMPAddon(config)
400       );
402       log.info("Forced " + forcedAddons.length + " addons.");
403       addons = addons.concat(forcedAddons);
404     } catch (err) {
405       log.info("Failed to force addons: " + err);
406     }
408     this._deferred.resolve({ addons });
409     delete this._deferred;
410     return deferredPromise;
411   },
412   /**
413    * Installs the specified addon and calls a callback when done.
414    * @param gmpAddon The GMPAddon object to install
415    * @return a promise which will be resolved or rejected
416    *         The promise will resolve with an array of paths that were extracted
417    *         The promise will reject with an error object:
418    *           target: The XHR request object
419    *           status: The HTTP status code
420    *           type: A string to represent the type of error
421    *                 downloaderr, verifyerr or previouserrorencountered
422    */
423   installAddon(gmpAddon) {
424     if (this._deferred) {
425       let log = getScopedLogger("GMPInstallManager.installAddon");
426       log.error("previous error encountered");
427       return Promise.reject({ type: "previouserrorencountered" });
428     }
429     this.gmpDownloader = new GMPDownloader(gmpAddon);
430     return this.gmpDownloader.start();
431   },
432   _getTimeSinceLastCheck() {
433     let now = Math.round(Date.now() / 1000);
434     // Default to 0 here because `now - 0` will be returned later if that case
435     // is hit. We want a large value so a check will occur.
436     let lastCheck = GMPPrefs.getInt(GMPPrefs.KEY_UPDATE_LAST_CHECK, 0);
437     // Handle clock jumps, return now since we want it to represent
438     // a lot of time has passed since the last check.
439     if (now < lastCheck) {
440       return now;
441     }
442     return now - lastCheck;
443   },
444   get _isEMEEnabled() {
445     return GMPPrefs.getBool(GMPPrefs.KEY_EME_ENABLED, true);
446   },
447   _isAddonEnabled(aAddon) {
448     return GMPPrefs.getBool(GMPPrefs.KEY_PLUGIN_ENABLED, true, aAddon);
449   },
450   _isAddonUpdateEnabled(aAddon) {
451     return (
452       this._isAddonEnabled(aAddon) &&
453       GMPPrefs.getBool(GMPPrefs.KEY_PLUGIN_AUTOUPDATE, true, aAddon)
454     );
455   },
456   _updateLastCheck() {
457     let now = Math.round(Date.now() / 1000);
458     GMPPrefs.setInt(GMPPrefs.KEY_UPDATE_LAST_CHECK, now);
459   },
460   _versionchangeOccurred() {
461     let savedBuildID = GMPPrefs.getString(GMPPrefs.KEY_BUILDID, "");
462     let buildID = Services.appinfo.platformBuildID || "";
463     if (savedBuildID == buildID) {
464       return false;
465     }
466     GMPPrefs.setString(GMPPrefs.KEY_BUILDID, buildID);
467     return true;
468   },
469   /**
470    * Wrapper for checkForAddons and installAddon.
471    * Will only install if not already installed and will log the results.
472    * This will only install/update the OpenH264 and EME plugins
473    * @return a promise which will be resolved if all addons could be installed
474    *         successfully, rejected otherwise.
475    */
476   async simpleCheckAndInstall() {
477     let log = getScopedLogger("GMPInstallManager.simpleCheckAndInstall");
479     if (this._versionchangeOccurred()) {
480       log.info(
481         "A version change occurred. Ignoring " +
482           "media.gmp-manager.lastCheck to check immediately for " +
483           "new or updated GMPs."
484       );
485     } else {
486       let secondsBetweenChecks = GMPPrefs.getInt(
487         GMPPrefs.KEY_SECONDS_BETWEEN_CHECKS,
488         DEFAULT_SECONDS_BETWEEN_CHECKS
489       );
490       let secondsSinceLast = this._getTimeSinceLastCheck();
491       log.info(
492         "Last check was: " +
493           secondsSinceLast +
494           " seconds ago, minimum seconds: " +
495           secondsBetweenChecks
496       );
497       if (secondsBetweenChecks > secondsSinceLast) {
498         log.info("Will not check for updates.");
499         return { status: "too-frequent-no-check" };
500       }
501     }
503     try {
504       let { addons } = await this.checkForAddons();
505       this._updateLastCheck();
506       log.info("Found " + addons.length + " addons advertised.");
507       let addonsToInstall = addons.filter(function (gmpAddon) {
508         log.info("Found addon: " + gmpAddon.toString());
510         if (!gmpAddon.isValid) {
511           log.info("Addon |" + gmpAddon.id + "| is invalid.");
512           return false;
513         }
515         if (GMPUtils.isPluginHidden(gmpAddon)) {
516           log.info("Addon |" + gmpAddon.id + "| has been hidden.");
517           return false;
518         }
520         if (gmpAddon.isInstalled) {
521           log.info("Addon |" + gmpAddon.id + "| already installed.");
522           return false;
523         }
525         // Do not install from fallback if already installed as it
526         // may be a downgrade
527         if (gmpAddon.usedFallback && gmpAddon.isUpdate) {
528           log.info(
529             "Addon |" +
530               gmpAddon.id +
531               "| not installing updates based " +
532               "on fallback."
533           );
534           return false;
535         }
537         let addonUpdateEnabled = false;
538         if (GMP_PLUGIN_IDS.includes(gmpAddon.id)) {
539           if (!this._isAddonEnabled(gmpAddon.id)) {
540             log.info(
541               "GMP |" + gmpAddon.id + "| has been disabled; skipping check."
542             );
543           } else if (!this._isAddonUpdateEnabled(gmpAddon.id)) {
544             log.info(
545               "Auto-update is off for " + gmpAddon.id + ", skipping check."
546             );
547           } else {
548             addonUpdateEnabled = true;
549           }
550         } else {
551           // Currently, we only support installs of OpenH264 and EME plugins.
552           log.info(
553             "Auto-update is off for unknown plugin '" +
554               gmpAddon.id +
555               "', skipping check."
556           );
557         }
559         return addonUpdateEnabled;
560       }, this);
562       if (!addonsToInstall.length) {
563         let now = Math.round(Date.now() / 1000);
564         GMPPrefs.setInt(GMPPrefs.KEY_UPDATE_LAST_EMPTY_CHECK, now);
565         log.info("No new addons to install, returning");
566         return { status: "nothing-new-to-install" };
567       }
569       let installResults = [];
570       let failureEncountered = false;
571       for (let addon of addonsToInstall) {
572         try {
573           await this.installAddon(addon);
574           installResults.push({
575             id: addon.id,
576             result: "succeeded",
577           });
578         } catch (e) {
579           failureEncountered = true;
580           installResults.push({
581             id: addon.id,
582             result: "failed",
583           });
584         }
585       }
586       if (failureEncountered) {
587         // eslint-disable-next-line no-throw-literal
588         throw { status: "failed", results: installResults };
589       }
590       return { status: "succeeded", results: installResults };
591     } catch (e) {
592       log.error("Could not check for addons", e);
593       throw e;
594     }
595   },
597   /**
598    * Makes sure everything is cleaned up
599    */
600   uninit() {
601     let log = getScopedLogger("GMPInstallManager.uninit");
602     if (this._request) {
603       log.info("Aborting request");
604       this._request.abort();
605     }
606     if (this._deferred) {
607       log.info("Rejecting deferred");
608       this._deferred.reject({ type: "uninitialized" });
609     }
610     log.info("Done cleanup");
611   },
613   /**
614    * If set to true, specifies to leave the temporary downloaded zip file.
615    * This is useful for tests.
616    */
617   overrideLeaveDownloadedZip: false,
621  * Used to construct a single GMP addon
622  * GMPAddon objects are returns from GMPInstallManager.checkForAddons
623  * GMPAddon objects can also be used in calls to GMPInstallManager.installAddon
625  * @param addon The ProductAddonChecker `addon` object
626  */
627 export function GMPAddon(addon) {
628   let log = getScopedLogger("GMPAddon.constructor");
629   this.usedFallback = false;
630   for (let name of Object.keys(addon)) {
631     this[name] = addon[name];
632   }
633   log.info("Created new addon: " + this.toString());
636 GMPAddon.prototype = {
637   /**
638    * Returns a string representation of the addon
639    */
640   toString() {
641     return (
642       this.id +
643       " (" +
644       "isValid: " +
645       this.isValid +
646       ", isInstalled: " +
647       this.isInstalled +
648       ", hashFunction: " +
649       this.hashFunction +
650       ", hashValue: " +
651       this.hashValue +
652       (this.size !== undefined ? ", size: " + this.size : "") +
653       ")"
654     );
655   },
656   /**
657    * If all the fields aren't specified don't consider this addon valid
658    * @return true if the addon is parsed and valid
659    */
660   get isValid() {
661     return (
662       this.id &&
663       this.URL &&
664       this.version &&
665       this.hashFunction &&
666       !!this.hashValue
667     );
668   },
669   get isInstalled() {
670     return (
671       this.version &&
672       !!this.hashValue &&
673       GMPPrefs.getString(GMPPrefs.KEY_PLUGIN_VERSION, "", this.id) ===
674         this.version &&
675       GMPPrefs.getString(GMPPrefs.KEY_PLUGIN_HASHVALUE, "", this.id) ===
676         this.hashValue
677     );
678   },
679   get isEME() {
680     return this.id == WIDEVINE_L1_ID || this.id == WIDEVINE_L3_ID;
681   },
682   get isOpenH264() {
683     return this.id == "gmp-gmpopenh264";
684   },
685   /**
686    * @return true if the addon has been previously installed and this is
687    * a new version, if this is a fresh install return false
688    */
689   get isUpdate() {
690     return (
691       this.version &&
692       GMPPrefs.getBool(GMPPrefs.KEY_PLUGIN_VERSION, false, this.id)
693     );
694   },
698  * Constructs a GMPExtractor object which is used to extract a GMP zip
699  * into the specified location.
700  * @param zipPath The path on disk of the zip file to extract
701  * @param relativePath The relative path components inside the profile directory
702  *                     to extract the zip to.
703  */
704 export function GMPExtractor(zipPath, relativeInstallPath) {
705   this.zipPath = zipPath;
706   this.relativeInstallPath = relativeInstallPath;
709 GMPExtractor.prototype = {
710   /**
711    * Installs the this.zipPath contents into the directory used to store GMP
712    * addons for the current platform.
713    *
714    * @return a promise which will be resolved or rejected
715    *         See GMPInstallManager.installAddon for resolve/rejected info
716    */
717   install() {
718     this._deferred = PromiseUtils.defer();
719     let deferredPromise = this._deferred;
720     let { zipPath, relativeInstallPath } = this;
721     // Escape the zip path since the worker will use it as a URI
722     let zipFile = new lazy.FileUtils.File(zipPath);
723     let zipURI = Services.io.newFileURI(zipFile).spec;
724     let worker = new ChromeWorker(
725       "resource://gre/modules/GMPExtractor.worker.js"
726     );
727     worker.onmessage = function (msg) {
728       let log = getScopedLogger("GMPExtractor");
729       worker.terminate();
730       if (msg.data.result != "success") {
731         log.error("Failed to extract zip file: " + zipURI);
732         log.error("Exception: " + msg.data.exception);
733         return deferredPromise.reject({
734           target: this,
735           status: msg.data.exception,
736           type: "exception",
737         });
738       }
739       log.info("Successfully extracted zip file: " + zipURI);
740       return deferredPromise.resolve(msg.data.extractedPaths);
741     };
742     worker.postMessage({ zipURI, relativeInstallPath });
743     return this._deferred.promise;
744   },
748  * Constructs an object which downloads and initiates an install of
749  * the specified GMPAddon object.
750  * @param gmpAddon The addon to install.
751  */
752 export function GMPDownloader(gmpAddon) {
753   this._gmpAddon = gmpAddon;
756 GMPDownloader.prototype = {
757   /**
758    * Starts the download process for an addon.
759    * @return a promise which will be resolved or rejected
760    *         See GMPInstallManager.installAddon for resolve/rejected info
761    */
762   start() {
763     let log = getScopedLogger("GMPDownloader");
764     let gmpAddon = this._gmpAddon;
765     let now = Math.round(Date.now() / 1000);
766     GMPPrefs.setInt(GMPPrefs.KEY_PLUGIN_LAST_INSTALL_START, now, gmpAddon.id);
768     if (!gmpAddon.isValid) {
769       log.info("gmpAddon is not valid, will not continue");
770       return Promise.reject({
771         target: this,
772         type: "downloaderr",
773       });
774     }
775     // If the HTTPS-Only Mode is enabled, every insecure request gets upgraded
776     // by default. This upgrade has to be prevented for openh264 downloads since
777     // the server doesn't support https://
778     const downloadOptions = {
779       httpsOnlyNoUpgrade: gmpAddon.isOpenH264,
780     };
781     return ProductAddonChecker.downloadAddon(gmpAddon, downloadOptions).then(
782       zipPath => {
783         let now = Math.round(Date.now() / 1000);
784         GMPPrefs.setInt(GMPPrefs.KEY_PLUGIN_LAST_DOWNLOAD, now, gmpAddon.id);
785         log.info(
786           `install to directory path: ${gmpAddon.id}/${gmpAddon.version}`
787         );
788         let gmpInstaller = new GMPExtractor(zipPath, [
789           gmpAddon.id,
790           gmpAddon.version,
791         ]);
792         let installPromise = gmpInstaller.install();
793         return installPromise.then(
794           extractedPaths => {
795             // Success, set the prefs
796             let now = Math.round(Date.now() / 1000);
797             GMPPrefs.setInt(GMPPrefs.KEY_PLUGIN_LAST_UPDATE, now, gmpAddon.id);
798             // Remember our ABI, so that if the profile is migrated to another
799             // platform or from 32 -> 64 bit, we notice and don't try to load the
800             // unexecutable plugin library.
801             let abi = GMPUtils._expectedABI(gmpAddon);
802             log.info("Setting ABI to '" + abi + "' for " + gmpAddon.id);
803             GMPPrefs.setString(GMPPrefs.KEY_PLUGIN_ABI, abi, gmpAddon.id);
804             // We use the combination of the hash and version to ensure we are
805             // up to date.
806             GMPPrefs.setString(
807               GMPPrefs.KEY_PLUGIN_HASHVALUE,
808               gmpAddon.hashValue,
809               gmpAddon.id
810             );
811             // Setting the version pref signals installation completion to consumers,
812             // if you need to set other prefs etc. do it before this.
813             GMPPrefs.setString(
814               GMPPrefs.KEY_PLUGIN_VERSION,
815               gmpAddon.version,
816               gmpAddon.id
817             );
818             return extractedPaths;
819           },
820           reason => {
821             GMPPrefs.setString(
822               GMPPrefs.KEY_PLUGIN_LAST_INSTALL_FAIL_REASON,
823               reason,
824               gmpAddon.id
825             );
826             let now = Math.round(Date.now() / 1000);
827             GMPPrefs.setInt(
828               GMPPrefs.KEY_PLUGIN_LAST_INSTALL_FAILED,
829               now,
830               gmpAddon.id
831             );
832             throw reason;
833           }
834         );
835       },
836       reason => {
837         GMPPrefs.setString(
838           GMPPrefs.KEY_PLUGIN_LAST_DOWNLOAD_FAIL_REASON,
839           reason,
840           gmpAddon.id
841         );
842         let now = Math.round(Date.now() / 1000);
843         GMPPrefs.setInt(
844           GMPPrefs.KEY_PLUGIN_LAST_DOWNLOAD_FAILED,
845           now,
846           gmpAddon.id
847         );
848         throw reason;
849       }
850     );
851   },