netwerk/protocol/http/UserAgentUpdates.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/netwerk/protocol/http/UserAgentUpdates.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,242 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +"use strict";
     1.9 +
    1.10 +this.EXPORTED_SYMBOLS = ["UserAgentUpdates"];
    1.11 +
    1.12 +const Ci = Components.interfaces;
    1.13 +const Cc = Components.classes;
    1.14 +const Cu = Components.utils;
    1.15 +
    1.16 +Cu.import("resource://gre/modules/Services.jsm");
    1.17 +Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    1.18 +
    1.19 +XPCOMUtils.defineLazyModuleGetter(
    1.20 +  this, "FileUtils", "resource://gre/modules/FileUtils.jsm");
    1.21 +
    1.22 +XPCOMUtils.defineLazyModuleGetter(
    1.23 +  this, "OS", "resource://gre/modules/osfile.jsm");
    1.24 +
    1.25 +XPCOMUtils.defineLazyModuleGetter(
    1.26 +  this, "UpdateChannel", "resource://gre/modules/UpdateChannel.jsm");
    1.27 +
    1.28 +XPCOMUtils.defineLazyServiceGetter(
    1.29 +  this, "gUpdateTimer", "@mozilla.org/updates/timer-manager;1", "nsIUpdateTimerManager");
    1.30 +
    1.31 +XPCOMUtils.defineLazyGetter(this, "gApp",
    1.32 +  function() Cc["@mozilla.org/xre/app-info;1"]
    1.33 +    .getService(Ci.nsIXULAppInfo).QueryInterface(Ci.nsIXULRuntime)
    1.34 +);
    1.35 +
    1.36 +XPCOMUtils.defineLazyGetter(this, "gDecoder",
    1.37 +  function() new TextDecoder()
    1.38 +);
    1.39 +
    1.40 +XPCOMUtils.defineLazyGetter(this, "gEncoder",
    1.41 +  function() new TextEncoder()
    1.42 +);
    1.43 +
    1.44 +const TIMER_ID = "user-agent-updates-timer";
    1.45 +
    1.46 +const PREF_UPDATES = "general.useragent.updates.";
    1.47 +const PREF_UPDATES_ENABLED = PREF_UPDATES + "enabled";
    1.48 +const PREF_UPDATES_URL = PREF_UPDATES + "url";
    1.49 +const PREF_UPDATES_INTERVAL = PREF_UPDATES + "interval";
    1.50 +const PREF_UPDATES_RETRY = PREF_UPDATES + "retry";
    1.51 +const PREF_UPDATES_TIMEOUT = PREF_UPDATES + "timeout";
    1.52 +const PREF_UPDATES_LASTUPDATED = PREF_UPDATES + "lastupdated";
    1.53 +
    1.54 +const KEY_PREFDIR = "PrefD";
    1.55 +const KEY_APPDIR = "XCurProcD";
    1.56 +const FILE_UPDATES = "ua-update.json";
    1.57 +
    1.58 +const PREF_APP_DISTRIBUTION = "distribution.id";
    1.59 +const PREF_APP_DISTRIBUTION_VERSION = "distribution.version";
    1.60 +
    1.61 +var gInitialized = false;
    1.62 +
    1.63 +this.UserAgentUpdates = {
    1.64 +  init: function(callback) {
    1.65 +    if (gInitialized) {
    1.66 +      return;
    1.67 +    }
    1.68 +    gInitialized = true;
    1.69 +
    1.70 +    this._callback = callback;
    1.71 +    this._lastUpdated = 0;
    1.72 +    this._applySavedUpdate();
    1.73 +
    1.74 +    Services.prefs.addObserver(PREF_UPDATES, this, false);
    1.75 +  },
    1.76 +
    1.77 +  uninit: function() {
    1.78 +    if (!gInitialized) {
    1.79 +      return;
    1.80 +    }
    1.81 +    gInitialized = false;
    1.82 +    Services.prefs.removeObserver(PREF_UPDATES, this);
    1.83 +  },
    1.84 +
    1.85 +  _applyUpdate: function(update) {
    1.86 +    // Check pref again in case it has changed
    1.87 +    if (update && this._getPref(PREF_UPDATES_ENABLED, false)) {
    1.88 +      this._callback(update);
    1.89 +    } else {
    1.90 +      this._callback(null);
    1.91 +    }
    1.92 +  },
    1.93 +
    1.94 +  _applySavedUpdate: function() {
    1.95 +    if (!this._getPref(PREF_UPDATES_ENABLED, false)) {
    1.96 +      // remove previous overrides
    1.97 +      this._applyUpdate(null);
    1.98 +      return;
    1.99 +    }
   1.100 +    // try loading from profile dir, then from app dir
   1.101 +    let dirs = [KEY_PREFDIR, KEY_APPDIR];
   1.102 +    dirs.reduce((prevLoad, dir) => {
   1.103 +      let file = FileUtils.getFile(dir, [FILE_UPDATES], true).path;
   1.104 +      // tryNext returns promise to read file under dir and parse it
   1.105 +      let tryNext = () => OS.File.read(file).then(
   1.106 +        (bytes) => {
   1.107 +          let update = JSON.parse(gDecoder.decode(bytes));
   1.108 +          if (!update) {
   1.109 +            throw new Error("invalid update");
   1.110 +          }
   1.111 +          return update;
   1.112 +        }
   1.113 +      );
   1.114 +      // try to load next one if the previous load failed
   1.115 +      return prevLoad ? prevLoad.then(null, tryNext) : tryNext();
   1.116 +    }, null).then(
   1.117 +      // apply update if loading was successful
   1.118 +      (update) => this._applyUpdate(update)
   1.119 +    );
   1.120 +    this._scheduleUpdate();
   1.121 +  },
   1.122 +
   1.123 +  _saveToFile: function(update) {
   1.124 +    let file = FileUtils.getFile(KEY_PREFDIR, [FILE_UPDATES], true);
   1.125 +    let path = file.path;
   1.126 +    let bytes = gEncoder.encode(JSON.stringify(update));
   1.127 +    OS.File.writeAtomic(path, bytes, {tmpPath: path + ".tmp"}).then(
   1.128 +      () => {
   1.129 +        this._lastUpdated = Date.now();
   1.130 +        Services.prefs.setCharPref(
   1.131 +          PREF_UPDATES_LASTUPDATED, this._lastUpdated.toString());
   1.132 +      },
   1.133 +      Cu.reportError
   1.134 +    );
   1.135 +  },
   1.136 +
   1.137 +  _getPref: function(name, def) {
   1.138 +    try {
   1.139 +      switch (typeof def) {
   1.140 +        case "number": return Services.prefs.getIntPref(name);
   1.141 +        case "boolean": return Services.prefs.getBoolPref(name);
   1.142 +      }
   1.143 +      return Services.prefs.getCharPref(name);
   1.144 +    } catch (e) {
   1.145 +      return def;
   1.146 +    }
   1.147 +  },
   1.148 +
   1.149 +  _getParameters: function() ({
   1.150 +    "%DATE%": function() Date.now().toString(),
   1.151 +    "%PRODUCT%": function() gApp.name,
   1.152 +    "%APP_ID%": function() gApp.ID,
   1.153 +    "%APP_VERSION%": function() gApp.version,
   1.154 +    "%BUILD_ID%": function() gApp.appBuildID,
   1.155 +    "%OS%": function() gApp.OS,
   1.156 +    "%CHANNEL%": function() UpdateChannel.get(),
   1.157 +    "%DISTRIBUTION%": function() this._getPref(PREF_APP_DISTRIBUTION, ""),
   1.158 +    "%DISTRIBUTION_VERSION%": function() this._getPref(PREF_APP_DISTRIBUTION_VERSION, ""),
   1.159 +  }),
   1.160 +
   1.161 +  _getUpdateURL: function() {
   1.162 +    let url = this._getPref(PREF_UPDATES_URL, "");
   1.163 +    let params = this._getParameters();
   1.164 +    return url.replace(/%[A-Z_]+%/g, function(match) {
   1.165 +      let param = params[match];
   1.166 +      // preserve the %FOO% string (e.g. as an encoding) if it's not a valid parameter
   1.167 +      return param ? encodeURIComponent(param()) : match;
   1.168 +    });
   1.169 +  },
   1.170 +
   1.171 +  _fetchUpdate: function(url, success, error) {
   1.172 +    let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
   1.173 +                    .createInstance(Ci.nsIXMLHttpRequest);
   1.174 +    request.mozBackgroundRequest = true;
   1.175 +    request.timeout = this._getPref(PREF_UPDATES_TIMEOUT, 60000);
   1.176 +    request.open("GET", url, true);
   1.177 +    request.overrideMimeType("application/json");
   1.178 +    request.responseType = "json";
   1.179 +
   1.180 +    request.addEventListener("load", function() {
   1.181 +      let response = request.response;
   1.182 +      response ? success(response) : error();
   1.183 +    });
   1.184 +    request.addEventListener("error", error);
   1.185 +    request.send();
   1.186 +  },
   1.187 +
   1.188 +  _update: function() {
   1.189 +    let url = this._getUpdateURL();
   1.190 +    url && this._fetchUpdate(url,
   1.191 +      (function(response) { // success
   1.192 +        // apply update and save overrides to profile
   1.193 +        this._applyUpdate(response);
   1.194 +        this._saveToFile(response);
   1.195 +        this._scheduleUpdate(); // cancel any retries
   1.196 +      }).bind(this),
   1.197 +      (function(response) { // error
   1.198 +        this._scheduleUpdate(true /* retry */);
   1.199 +      }).bind(this));
   1.200 +  },
   1.201 +
   1.202 +  _scheduleUpdate: function(retry) {
   1.203 +    // only schedule updates in the main process
   1.204 +    if (gApp.processType !== Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
   1.205 +      return;
   1.206 +    }
   1.207 +    let interval = this._getPref(PREF_UPDATES_INTERVAL, 604800 /* 1 week */);
   1.208 +    if (retry) {
   1.209 +      interval = this._getPref(PREF_UPDATES_RETRY, interval);
   1.210 +    }
   1.211 +    gUpdateTimer.registerTimer(TIMER_ID, this, Math.max(1, interval));
   1.212 +  },
   1.213 +
   1.214 +  notify: function(timer) {
   1.215 +    // timer notification
   1.216 +    if (this._getPref(PREF_UPDATES_ENABLED, false)) {
   1.217 +      this._update();
   1.218 +    }
   1.219 +  },
   1.220 +
   1.221 +  observe: function(subject, topic, data) {
   1.222 +    switch (topic) {
   1.223 +      case "nsPref:changed":
   1.224 +        if (data === PREF_UPDATES_ENABLED) {
   1.225 +          this._applySavedUpdate();
   1.226 +        } else if (data === PREF_UPDATES_INTERVAL) {
   1.227 +          this._scheduleUpdate();
   1.228 +        } else if (data === PREF_UPDATES_LASTUPDATED) {
   1.229 +          // reload from file if there has been an update
   1.230 +          let lastUpdated = parseInt(
   1.231 +            this._getPref(PREF_UPDATES_LASTUPDATED, "0"), 0);
   1.232 +          if (lastUpdated > this._lastUpdated) {
   1.233 +            this._applySavedUpdate();
   1.234 +            this._lastUpdated = lastUpdated;
   1.235 +          }
   1.236 +        }
   1.237 +        break;
   1.238 +    }
   1.239 +  },
   1.240 +
   1.241 +  QueryInterface: XPCOMUtils.generateQI([
   1.242 +    Ci.nsIObserver,
   1.243 +    Ci.nsITimerCallback,
   1.244 +  ]),
   1.245 +};

mercurial