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