UpdateService.jsm 186 KB
Newer Older
1
2
3
4
5
/* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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/. */
6

7
"use strict";
8

9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const { AppConstants } = ChromeUtils.import(
  "resource://gre/modules/AppConstants.jsm"
);
const { AUSTLMY } = ChromeUtils.import(
  "resource://gre/modules/UpdateTelemetry.jsm"
);
const {
  Bits,
  BitsRequest,
  BitsUnknownError,
  BitsVerificationError,
} = ChromeUtils.import("resource://gre/modules/Bits.jsm");
const { FileUtils } = ChromeUtils.import(
  "resource://gre/modules/FileUtils.jsm"
);
const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = ChromeUtils.import(
  "resource://gre/modules/XPCOMUtils.jsm"
);
29

30
XPCOMUtils.defineLazyGlobalGetters(this, ["DOMParser", "XMLHttpRequest"]);
31

32
XPCOMUtils.defineLazyModuleGetters(this, {
33
  AddonManager: "resource://gre/modules/AddonManager.jsm",
34
35
  AsyncShutdown: "resource://gre/modules/AsyncShutdown.jsm",
  CertUtils: "resource://gre/modules/CertUtils.jsm",
36
#ifdef XP_WIN
37
  ctypes: "resource://gre/modules/ctypes.jsm",
38
#endif
39
40
  DeferredTask: "resource://gre/modules/DeferredTask.jsm",
  OS: "resource://gre/modules/osfile.jsm",
41
  setTimeout: "resource://gre/modules/Timer.jsm",
42
  UpdateUtils: "resource://gre/modules/UpdateUtils.jsm",
43
#if !defined(TOR_BROWSER_UPDATE)
44
  WindowsRegistry: "resource://gre/modules/WindowsRegistry.jsm",
45
#endif
46
47
});

48
49
50
const UPDATESERVICE_CID = Components.ID(
  "{B3C290A6-3943-4B89-8BBE-C01EB7B3B311}"
);
51

52
53
54
55
56
57
const PREF_APP_UPDATE_ALTUPDATEDIRPATH = "app.update.altUpdateDirPath";
const PREF_APP_UPDATE_BACKGROUNDERRORS = "app.update.backgroundErrors";
const PREF_APP_UPDATE_BACKGROUNDMAXERRORS = "app.update.backgroundMaxErrors";
const PREF_APP_UPDATE_BITS_ENABLED = "app.update.BITS.enabled";
const PREF_APP_UPDATE_CANCELATIONS = "app.update.cancelations";
const PREF_APP_UPDATE_CANCELATIONS_OSX = "app.update.cancelations.osx";
58
const PREF_APP_UPDATE_CANCELATIONS_OSX_MAX = "app.update.cancelations.osx.max";
59
60
const PREF_APP_UPDATE_DISABLEDFORTESTING = "app.update.disabledForTesting";
const PREF_APP_UPDATE_DOWNLOAD_ATTEMPTS = "app.update.download.attempts";
61
const PREF_APP_UPDATE_DOWNLOAD_MAXATTEMPTS = "app.update.download.maxAttempts";
62
63
64
65
const PREF_APP_UPDATE_ELEVATE_NEVER = "app.update.elevate.never";
const PREF_APP_UPDATE_ELEVATE_VERSION = "app.update.elevate.version";
const PREF_APP_UPDATE_ELEVATE_ATTEMPTS = "app.update.elevate.attempts";
const PREF_APP_UPDATE_ELEVATE_MAXATTEMPTS = "app.update.elevate.maxAttempts";
66
67
const PREF_APP_UPDATE_LANGPACK_ENABLED = "app.update.langpack.enabled";
const PREF_APP_UPDATE_LANGPACK_TIMEOUT = "app.update.langpack.timeout";
68
69
const PREF_APP_UPDATE_LOG = "app.update.log";
const PREF_APP_UPDATE_LOG_FILE = "app.update.log.file";
70
const PREF_APP_UPDATE_NOTIFYDURINGDOWNLOAD = "app.update.notifyDuringDownload";
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
const PREF_APP_UPDATE_PROMPTWAITTIME = "app.update.promptWaitTime";
const PREF_APP_UPDATE_SERVICE_ENABLED = "app.update.service.enabled";
const PREF_APP_UPDATE_SERVICE_ERRORS = "app.update.service.errors";
const PREF_APP_UPDATE_SERVICE_MAXERRORS = "app.update.service.maxErrors";
const PREF_APP_UPDATE_SOCKET_MAXERRORS = "app.update.socket.maxErrors";
const PREF_APP_UPDATE_SOCKET_RETRYTIMEOUT = "app.update.socket.retryTimeout";
const PREF_APP_UPDATE_STAGING_ENABLED = "app.update.staging.enabled";
const PREF_APP_UPDATE_URL_DETAILS = "app.update.url.details";
const PREF_NETWORK_PROXY_TYPE = "network.proxy.type";

const URI_BRAND_PROPERTIES = "chrome://branding/locale/brand.properties";
const URI_UPDATE_NS = "http://www.mozilla.org/2005/app-update";
const URI_UPDATES_PROPERTIES =
  "chrome://mozapps/locale/update/updates.properties";

const KEY_EXECUTABLE = "XREExeF";
const KEY_PROFILE_DIR = "ProfD";
const KEY_UPDROOT = "UpdRootD";

const DIR_UPDATES = "updates";
91

92
93
const FILE_ACTIVE_UPDATE_XML = "active-update.xml";
const FILE_BACKUP_UPDATE_LOG = "backup-update.log";
94
95
96
97
98
99
100
101
102
103
104
105
106
const FILE_BT_RESULT = "bt.result";
const FILE_LAST_UPDATE_LOG = "last-update.log";
const FILE_UPDATES_XML = "updates.xml";
const FILE_UPDATE_LOG = "update.log";
const FILE_UPDATE_MAR = "update.mar";
const FILE_UPDATE_STATUS = "update.status";
const FILE_UPDATE_TEST = "update.test";
const FILE_UPDATE_VERSION = "update.version";
const FILE_UPDATE_MESSAGES = "update_messages.log";

const STATE_NONE = "null";
const STATE_DOWNLOADING = "downloading";
const STATE_PENDING = "pending";
107
108
const STATE_PENDING_SERVICE = "pending-service";
const STATE_PENDING_ELEVATE = "pending-elevate";
109
110
const STATE_APPLYING = "applying";
const STATE_APPLIED = "applied";
111
const STATE_APPLIED_SERVICE = "applied-service";
112
const STATE_SUCCEEDED = "succeeded";
113
const STATE_DOWNLOAD_FAILED = "download-failed";
114
const STATE_FAILED = "failed";
115

116
117
// BITS will keep retrying a download after transient errors, unless this much
// time has passed since there has been download progress.
118
119
120
121
// Similarly to ...POLL_RATE_MS below, we are much more aggressive when the user
// is watching the download progress.
const BITS_IDLE_NO_PROGRESS_TIMEOUT_SECS = 3600; // 1 hour
const BITS_ACTIVE_NO_PROGRESS_TIMEOUT_SECS = 5;
122

123
124
125
126
127
128
129
// These value control how frequently we get updates from the BITS client on
// the progress made downloading. The difference between the two is that the
// active interval is the one used when the user is watching. The idle interval
// is the one used when no one is watching.
const BITS_IDLE_POLL_RATE_MS = 1000;
const BITS_ACTIVE_POLL_RATE_MS = 200;

130
// The values below used by this code are from common/updatererrors.h
131
132
const WRITE_ERROR = 7;
const ELEVATION_CANCELED = 9;
133
134
const SERVICE_UPDATER_COULD_NOT_BE_STARTED = 24;
const SERVICE_NOT_ENOUGH_COMMAND_LINE_ARGS = 25;
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
const SERVICE_UPDATER_SIGN_ERROR = 26;
const SERVICE_UPDATER_COMPARE_ERROR = 27;
const SERVICE_UPDATER_IDENTITY_ERROR = 28;
const SERVICE_STILL_APPLYING_ON_SUCCESS = 29;
const SERVICE_STILL_APPLYING_ON_FAILURE = 30;
const SERVICE_UPDATER_NOT_FIXED_DRIVE = 31;
const SERVICE_COULD_NOT_LOCK_UPDATER = 32;
const SERVICE_INSTALLDIR_ERROR = 33;
const WRITE_ERROR_ACCESS_DENIED = 35;
const WRITE_ERROR_CALLBACK_APP = 37;
const UNEXPECTED_STAGING_ERROR = 43;
const DELETE_ERROR_STAGING_LOCK_FILE = 44;
const SERVICE_COULD_NOT_COPY_UPDATER = 49;
const SERVICE_STILL_APPLYING_TERMINATED = 50;
const SERVICE_STILL_APPLYING_NO_EXIT_CODE = 51;
const SERVICE_COULD_NOT_IMPERSONATE = 58;
const WRITE_ERROR_FILE_COPY = 61;
const WRITE_ERROR_DELETE_FILE = 62;
const WRITE_ERROR_OPEN_PATCH_FILE = 63;
const WRITE_ERROR_PATCH_FILE = 64;
const WRITE_ERROR_APPLY_DIR_PATH = 65;
const WRITE_ERROR_CALLBACK_PATH = 66;
const WRITE_ERROR_FILE_ACCESS_DENIED = 67;
const WRITE_ERROR_DIR_ACCESS_DENIED = 68;
const WRITE_ERROR_DELETE_BACKUP = 69;
const WRITE_ERROR_EXTRACT = 70;
161
162

// Array of write errors to simplify checks for write errors
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
const WRITE_ERRORS = [
  WRITE_ERROR,
  WRITE_ERROR_ACCESS_DENIED,
  WRITE_ERROR_CALLBACK_APP,
  WRITE_ERROR_FILE_COPY,
  WRITE_ERROR_DELETE_FILE,
  WRITE_ERROR_OPEN_PATCH_FILE,
  WRITE_ERROR_PATCH_FILE,
  WRITE_ERROR_APPLY_DIR_PATH,
  WRITE_ERROR_CALLBACK_PATH,
  WRITE_ERROR_FILE_ACCESS_DENIED,
  WRITE_ERROR_DIR_ACCESS_DENIED,
  WRITE_ERROR_DELETE_BACKUP,
  WRITE_ERROR_EXTRACT,
];
178
179

// Array of write errors to simplify checks for service errors
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
const SERVICE_ERRORS = [
  SERVICE_UPDATER_COULD_NOT_BE_STARTED,
  SERVICE_NOT_ENOUGH_COMMAND_LINE_ARGS,
  SERVICE_UPDATER_SIGN_ERROR,
  SERVICE_UPDATER_COMPARE_ERROR,
  SERVICE_UPDATER_IDENTITY_ERROR,
  SERVICE_STILL_APPLYING_ON_SUCCESS,
  SERVICE_STILL_APPLYING_ON_FAILURE,
  SERVICE_UPDATER_NOT_FIXED_DRIVE,
  SERVICE_COULD_NOT_LOCK_UPDATER,
  SERVICE_INSTALLDIR_ERROR,
  SERVICE_COULD_NOT_COPY_UPDATER,
  SERVICE_STILL_APPLYING_TERMINATED,
  SERVICE_STILL_APPLYING_NO_EXIT_CODE,
  SERVICE_COULD_NOT_IMPERSONATE,
];
196
197

// Error codes 80 through 99 are reserved for nsUpdateService.js and are not
198
// defined in common/updatererrors.h
199
200
201
202
203
const ERR_OLDER_VERSION_OR_SAME_BUILD = 90;
const ERR_UPDATE_STATE_NONE = 91;
const ERR_CHANNEL_CHANGE = 92;
const INVALID_UPDATER_STATE_CODE = 98;
const INVALID_UPDATER_STATUS_CODE = 99;
204
205

// Custom update error codes
206
const BACKGROUNDCHECK_MULTIPLE_FAILURES = 110;
207
const NETWORK_ERROR_OFFLINE = 111;
208

209
// Error codes should be < 1000. Errors above 1000 represent http status codes
210
const HTTP_ERROR_OFFSET = 1000;
211

212
213
214
// The is an HRESULT error that may be returned from the BITS interface
// indicating that access was denied. Often, this error code is returned when
// attempting to access a job created by a different user.
215
const HRESULT_E_ACCESSDENIED = -2147024891;
216

217
const DOWNLOAD_CHUNK_SIZE = 300000; // bytes
218

219
220
221
222
// The number of consecutive failures when updating using the service before
// setting the app.update.service.enabled preference to false.
const DEFAULT_SERVICE_MAX_ERRORS = 10;

223
224
225
226
227
// The number of consecutive socket errors to allow before falling back to
// downloading a different MAR file or failing if already downloading the full.
const DEFAULT_SOCKET_MAX_ERRORS = 10;

// The number of milliseconds to wait before retrying a connection error.
228
const DEFAULT_SOCKET_RETRYTIMEOUT = 2000;
229

230
231
// Default maximum number of elevation cancelations per update version before
// giving up.
232
const DEFAULT_CANCELATIONS_OSX_MAX = 3;
233

234
235
236
237
238
239
240
241
242
243
244
// This maps app IDs to their respective notification topic which signals when
// the application's user interface has been displayed.
const APPID_TO_TOPIC = {
  // Firefox
  "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}": "sessionstore-windows-restored",
  // SeaMonkey
  "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}": "sessionstore-windows-restored",
  // Thunderbird
  "{3550f703-e582-4d05-9a08-453d09bdfdc6}": "mail-startup-done",
};

245
246
247
// The interval for the update xml write deferred task.
const XML_SAVER_INTERVAL_MS = 200;

248
249
250
251
// How long after a patch has downloaded should we wait for language packs to
// update before proceeding anyway.
const LANGPACK_UPDATE_DEFAULT_TIMEOUT = 300000;

252
253
254
// Object to keep track of the current phase of the update and whether there
// has been a write failure for the phase so only one telemetry ping is made
// for the phase.
255
var gUpdateFileWriteInfo = { phase: null, failure: false };
256
var gUpdateMutexHandle = null;
257
258
259
// The permissions of the update directory should be fixed no more than once per
// session
var gUpdateDirPermissionFixAttempted = false;
260
261
// This is used for serializing writes to the update log file
var gLogfileWritePromise;
262
263
264
265
266
// This value will be set to true if it appears that BITS is being used by
// another user to download updates. We don't really want two users using BITS
// at once. Computers with many users (ex: a school computer), should not end
// up with dozens of BITS jobs.
var gBITSInUseByAnotherUser = false;
267

268
XPCOMUtils.defineLazyGetter(this, "gLogEnabled", function aus_gLogEnabled() {
269
270
271
272
  return (
    Services.prefs.getBoolPref(PREF_APP_UPDATE_LOG, false) ||
    Services.prefs.getBoolPref(PREF_APP_UPDATE_LOG_FILE, false)
  );
273
274
});

275
276
277
278
279
280
281
XPCOMUtils.defineLazyGetter(
  this,
  "gLogfileEnabled",
  function aus_gLogfileEnabled() {
    return Services.prefs.getBoolPref(PREF_APP_UPDATE_LOG_FILE, false);
  }
);
282

283
284
285
286
287
288
289
XPCOMUtils.defineLazyGetter(
  this,
  "gUpdateBundle",
  function aus_gUpdateBundle() {
    return Services.strings.createBundle(URI_UPDATES_PROPERTIES);
  }
);
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
/**
 * When a plain JS object is passed through xpconnect the other side sees a
 * wrapped version of the object instead of the real object. Since these two
 * objects are different they act as different keys for Map and WeakMap. However
 * xpconnect gives us a way to get the underlying JS object from the wrapper so
 * this function returns the JS object regardless of whether passed the JS
 * object or its wrapper for use in places where it is unclear which one you
 * have.
 */
function unwrap(obj) {
  return obj.wrappedJSObject ?? obj;
}

/**
 * When an update starts to download (and if the feature is enabled) the add-ons
 * manager starts downloading updated language packs for the new application
 * version. A promise is used to track whether those updates are complete so the
 * front-end is only notified that an application update is ready once the
 * language pack updates have been staged.
 *
 * In order to be able to access that promise from various places in the update
 * service they are cached in this map using the nsIUpdate object as a weak
 * owner. Note that the key should always be the result of calling the above
 * unwrap function on the nsIUpdate to ensure a consistent object is used as the
 * key.
 *
 * When the language packs finish staging the nsIUpdate entriy is removed from
 * this map so if the entry is still there then language pack updates are in
 * progress.
 */
const LangPackUpdates = new WeakMap();

323
324
325
326
327
328
329
330
/**
 * Tests to make sure that we can write to a given directory.
 *
 * @param updateTestFile a test file in the directory that needs to be tested.
 * @param createDirectory whether a test directory should be created.
 * @throws if we don't have right access to the directory.
 */
function testWriteAccess(updateTestFile, createDirectory) {
331
332
  const NORMAL_FILE_TYPE = Ci.nsIFile.NORMAL_FILE_TYPE;
  const DIRECTORY_TYPE = Ci.nsIFile.DIRECTORY_TYPE;
333
  if (updateTestFile.exists()) {
334
    updateTestFile.remove(false);
335
336
337
338
339
  }
  updateTestFile.create(
    createDirectory ? DIRECTORY_TYPE : NORMAL_FILE_TYPE,
    createDirectory ? FileUtils.PERMS_DIRECTORY : FileUtils.PERMS_FILE
  );
340
341
342
  updateTestFile.remove(false);
}

343
#ifdef XP_WIN
344
/**
345
 * Windows only function that closes a Win32 handle.
346
347
348
349
 *
 * @param handle The handle to close
 */
function closeHandle(handle) {
350
351
  if (handle) {
    let lib = ctypes.open("kernel32.dll");
352
353
354
355
356
357
    let CloseHandle = lib.declare(
      "CloseHandle",
      ctypes.winapi_abi,
      ctypes.int32_t /* success */,
      ctypes.void_t.ptr
    ); /* handle */
358
359
360
    CloseHandle(handle);
    lib.close();
  }
361
362
363
}

/**
364
 * Windows only function that creates a mutex.
365
 *
366
367
368
369
370
 * @param  aName
 *         The name for the mutex.
 * @param  aAllowExisting
 *         If false the function will close the handle and return null.
 * @return The Win32 handle to the mutex.
371
 */
372
373
function createMutex(aName, aAllowExisting = true) {
  if (AppConstants.platform != "win") {
374
    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
375
376
377
  }

  const INITIAL_OWN = 1;
378
  const ERROR_ALREADY_EXISTS = 0xb7;
379
  let lib = ctypes.open("kernel32.dll");
380
381
382
383
384
385
386
387
  let CreateMutexW = lib.declare(
    "CreateMutexW",
    ctypes.winapi_abi,
    ctypes.void_t.ptr /* return handle */,
    ctypes.void_t.ptr /* security attributes */,
    ctypes.int32_t /* initial owner */,
    ctypes.char16_t.ptr
  ); /* name */
388

389
390
  let handle = CreateMutexW(null, INITIAL_OWN, aName);
  let alreadyExists = ctypes.winLastError == ERROR_ALREADY_EXISTS;
391
392
393
394
395
396
  if (handle && !handle.isNull() && !aAllowExisting && alreadyExists) {
    closeHandle(handle);
    handle = null;
  }
  lib.close();

397
  if (handle && handle.isNull()) {
398
    handle = null;
399
  }
400
401
402
403
404

  return handle;
}

/**
405
406
 * Windows only function that determines a unique mutex name for the
 * installation.
407
 *
408
409
410
 * @param aGlobal
 *        true if the function should return a global mutex. A global mutex is
 *        valid across different sessions.
411
412
 * @return Global mutex path
 */
413
414
function getPerInstallationMutexName(aGlobal = true) {
  if (AppConstants.platform != "win") {
415
    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
416
  }
417

418
419
420
  let hasher = Cc["@mozilla.org/security/hash;1"].createInstance(
    Ci.nsICryptoHash
  );
421
  hasher.init(hasher.SHA1);
422

423
  let exeFile = Services.dirsvc.get(KEY_EXECUTABLE, Ci.nsIFile);
424

425
426
427
  let converter = Cc[
    "@mozilla.org/intl/scriptableunicodeconverter"
  ].createInstance(Ci.nsIScriptableUnicodeConverter);
428
429
430
431
  converter.charset = "UTF-8";
  var data = converter.convertToByteArray(exeFile.path.toLowerCase());

  hasher.update(data, data.length);
432
433
434
  return (
    (aGlobal ? "Global\\" : "") + "MozillaUpdateMutex-" + hasher.finish(true)
  );
435
}
436
#endif
437
438
439

/**
 * Whether or not the current instance has the update mutex. The update mutex
440
 * gives protection against 2 applications from the same installation updating:
441
 * 1) Running multiple profiles from the same installation path
442
 * 2) Two applications running in 2 different user sessions from the same path
443
444
445
446
 *
 * @return true if this instance holds the update mutex
 */
function hasUpdateMutex() {
447
#ifdef XP_WIN
448
449
450
  if (AppConstants.platform != "win") {
    return true;
  }
451
452
  if (!gUpdateMutexHandle) {
    gUpdateMutexHandle = createMutex(getPerInstallationMutexName(true), false);
453
  }
454
  return !!gUpdateMutexHandle;
455
456
457
#else
  return true;
#endif
458
459
}

460
461
462
463
464
465
466
467
468
/**
 * Determines whether or not all descendants of a directory are writeable.
 * Note: Does not check the root directory itself for writeability.
 *
 * @return true if all descendants are writeable, false otherwise
 */
function areDirectoryEntriesWriteable(aDir) {
  let items = aDir.directoryEntries;
  while (items.hasMoreElements()) {
469
    let item = items.nextFile;
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
    if (!item.isWritable()) {
      LOG("areDirectoryEntriesWriteable - unable to write to " + item.path);
      return false;
    }
    if (item.isDirectory() && !areDirectoryEntriesWriteable(item)) {
      return false;
    }
  }
  return true;
}

/**
 * OSX only function to determine if the user requires elevation to be able to
 * write to the application bundle.
 *
 * @return true if elevation is required, false otherwise
 */
function getElevationRequired() {
488
489
490
491
492
#if defined(TOR_BROWSER_UPDATE)
  // To avoid potential security holes associated with running the updater
  // process with elevated privileges, Tor Browser does not support elevation.
  return false;
#else
493
494
495
496
497
498
499
  if (AppConstants.platform != "macosx") {
    return false;
  }

  try {
    // Recursively check that the application bundle (and its descendants) can
    // be written to.
500
501
502
503
504
505
506
507
508
509
510
511
    LOG(
      "getElevationRequired - recursively testing write access on " +
        getInstallDirRoot().path
    );
    if (
      !getInstallDirRoot().isWritable() ||
      !areDirectoryEntriesWriteable(getInstallDirRoot())
    ) {
      LOG(
        "getElevationRequired - unable to write to application bundle, " +
          "elevation required"
      );
512
513
514
      return true;
    }
  } catch (ex) {
515
516
517
518
519
    LOG(
      "getElevationRequired - unable to write to application bundle, " +
        "elevation required. Exception: " +
        ex
    );
520
521
    return true;
  }
522
523
524
525
  LOG(
    "getElevationRequired - able to write to application bundle, elevation " +
      "not required"
  );
526
  return false;
527
#endif
528
529
}

530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
/**
 * A promise that resolves when language packs are downloading or if no language
 * packs are being downloaded.
 */
function promiseLangPacksUpdated(update) {
  let promise = LangPackUpdates.get(unwrap(update));
  if (promise) {
    LOG(
      "promiseLangPacksUpdated - waiting for language pack updates to stage."
    );
    return promise;
  }

  // In case callers rely on a promise just return an already resolved promise.
  return Promise.resolve();
}

547
548
/**
 * Determines whether or not an update can be applied. This is always true on
549
550
551
552
 * Windows when the service is used. On Mac OS X and Linux, if the user has
 * write access to the update directory this will return true because on OSX we
 * offer users the option to perform an elevated update when necessary and on
 * Linux the update directory is located in the application directory.
553
554
555
 *
 * @return true if an update can be applied, false otherwise
 */
556
function getCanApplyUpdates() {
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
  try {
    // Check if it is possible to write to the update directory so clients won't
    // repeatedly try to apply an update without the ability to complete the
    // update process which requires write access to the update directory.
    let updateTestFile = getUpdateFile([FILE_UPDATE_TEST]);
    LOG("getCanApplyUpdates - testing write access " + updateTestFile.path);
    testWriteAccess(updateTestFile, false);
  } catch (e) {
    LOG(
      "getCanApplyUpdates - unable to apply updates without write " +
        "access to the update directory. Exception: " +
        e
    );
    // Attempt to fix the update directory permissions. If successful the next
    // time this function is called the write access check to the update
    // directory will succeed.
    fixUpdateDirectoryPermissions();
    return false;
  }

577
#if !defined(TOR_BROWSER_UPDATE)
578
  if (AppConstants.platform == "macosx") {
579
580
581
582
    LOG(
      "getCanApplyUpdates - bypass the write since elevation can be used " +
        "on Mac OS X"
    );
583
    return true;
584
585
  }

586
  if (shouldUseService()) {
587
588
589
590
    LOG(
      "getCanApplyUpdates - bypass the write checks because the Windows " +
        "Maintenance Service can be used"
    );
591
592
    return true;
  }
593
#endif
594

595
596
597
598
599
600
601
  try {
    if (AppConstants.platform == "win") {
      // On Windows when the maintenance service isn't used updates can still be
      // performed in a location requiring admin privileges by the client
      // accepting a UAC prompt from an elevation request made by the updater.
      // Whether the client can elevate (e.g. has a split token) is determined
      // in nsXULAppInfo::GetUserCanElevate which is located in nsAppRunner.cpp.
602
603
      let userCanElevate = Services.appinfo.QueryInterface(Ci.nsIWinAppHelper)
        .userCanElevate;
604
605
606
607
608
609
      if (!userCanElevate) {
        // if we're unable to create the test file this will throw an exception.
        let appDirTestFile = getAppBaseDir();
        appDirTestFile.append(FILE_UPDATE_TEST);
        LOG("getCanApplyUpdates - testing write access " + appDirTestFile.path);
        if (appDirTestFile.exists()) {
610
611
          appDirTestFile.remove(false);
        }
612
613
614
615
        appDirTestFile.create(
          Ci.nsIFile.NORMAL_FILE_TYPE,
          FileUtils.PERMS_FILE
        );
616
        appDirTestFile.remove(false);
617
618
      }
    }
619
620
621
622
623
  } catch (e) {
    LOG("getCanApplyUpdates - unable to apply updates. Exception: " + e);
    // No write access to the installation directory
    return false;
  }
624

625
  LOG("getCanApplyUpdates - able to apply updates");
626
  return true;
627
}
628

629
630
631
632
633
634
/**
 * Whether or not the application can stage an update for the current session.
 * These checks are only performed once per session due to using a lazy getter.
 *
 * @return true if updates can be staged for this session.
 */
635
636
637
638
639
640
641
642
643
644
645
XPCOMUtils.defineLazyGetter(
  this,
  "gCanStageUpdatesSession",
  function aus_gCSUS() {
    if (getElevationRequired()) {
      LOG(
        "gCanStageUpdatesSession - unable to stage updates because elevation " +
          "is required."
      );
      return false;
    }
646

647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
    try {
      let updateTestFile;
      if (AppConstants.platform == "macosx") {
        updateTestFile = getUpdateFile([FILE_UPDATE_TEST]);
      } else {
        updateTestFile = getInstallDirRoot();
        updateTestFile.append(FILE_UPDATE_TEST);
      }
      LOG(
        "gCanStageUpdatesSession - testing write access " + updateTestFile.path
      );
      testWriteAccess(updateTestFile, true);
      if (AppConstants.platform != "macosx") {
        // On all platforms except Mac, we need to test the parent directory as
        // well, as we need to be able to move files in that directory during the
        // replacing step.
        updateTestFile = getInstallDirRoot().parent;
        updateTestFile.append(FILE_UPDATE_TEST);
        LOG(
          "gCanStageUpdatesSession - testing write access " +
            updateTestFile.path
        );
        updateTestFile.createUnique(
          Ci.nsIFile.DIRECTORY_TYPE,
          FileUtils.PERMS_DIRECTORY
        );
        updateTestFile.remove(false);
      }
    } catch (e) {
      LOG("gCanStageUpdatesSession - unable to stage updates. Exception: " + e);
      // No write privileges
      return false;
679
680
    }

681
682
683
684
    LOG("gCanStageUpdatesSession - able to stage updates");
    return true;
  }
);
685

686
687
688
689
690
691
/**
 * Whether or not the application can stage an update.
 *
 * @return true if updates can be staged.
 */
function getCanStageUpdates() {
692
  // If staging updates are disabled, then just bail out!
693
  if (!Services.prefs.getBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, false)) {
694
695
696
697
    LOG(
      "getCanStageUpdates - staging updates is disabled by preference " +
        PREF_APP_UPDATE_STAGING_ENABLED
    );
698
699
700
    return false;
  }

701
  if (AppConstants.platform == "win" && shouldUseService()) {
702
703
    // No need to perform directory write checks, the maintenance service will
    // be able to write to all directories.
704
    LOG("getCanStageUpdates - able to stage updates using the service");
705
706
707
    return true;
  }

708
  if (!hasUpdateMutex()) {
709
710
    LOG(
      "getCanStageUpdates - unable to apply updates because another " +
711
        "instance of the application is already handling updates for this " +
712
713
        "installation."
    );
714
715
716
    return false;
  }

717
  return gCanStageUpdatesSession;
718
}
719

720
721
722
723
724
725
726
727
728
729
/*
 * Whether or not the application can use BITS to download updates.
 *
 * @return A string with one of these values:
 *           CanUseBits
 *           NoBits_NotWindows
 *           NoBits_FeatureOff
 *           NoBits_Pref
 *           NoBits_Proxy
 *           NoBits_OtherUser
730
731
732
733
 *         These strings are directly compatible with the categories for
 *         UPDATE_CAN_USE_BITS_EXTERNAL and UPDATE_CAN_USE_BITS_NOTIFY telemetry
 *         probes. If this function is made to return other values, they should
 *         also be added to the labels lists for those probes in Histograms.json
734
735
736
737
738
739
740
741
742
743
 */
function getCanUseBits() {
  if (AppConstants.platform != "win") {
    LOG("getCanUseBits - Not using BITS because this is not Windows");
    return "NoBits_NotWindows";
  }
  if (!AppConstants.MOZ_BITS_DOWNLOAD) {
    LOG("getCanUseBits - Not using BITS because the feature is disabled");
    return "NoBits_FeatureOff";
  }
744

745
746
747
748
749
750
751
752
753
754
755
756
  if (!Services.prefs.getBoolPref(PREF_APP_UPDATE_BITS_ENABLED, true)) {
    LOG("getCanUseBits - Not using BITS. Disabled by pref.");
    return "NoBits_Pref";
  }
  if (gBITSInUseByAnotherUser) {
    LOG("getCanUseBits - Not using BITS. Already in use by another user");
    return "NoBits_OtherUser";
  }
  // Firefox support for passing proxies to BITS is still rudimentary.
  // For now, disable BITS support on configurations that are not using the
  // standard system proxy.
  let defaultProxy = Ci.nsIProtocolProxyService.PROXYCONFIG_SYSTEM;
757
758
759
760
761
  if (
    Services.prefs.getIntPref(PREF_NETWORK_PROXY_TYPE, defaultProxy) !=
      defaultProxy &&
    !Cu.isInAutomation
  ) {
762
763
764
765
766
767
768
    LOG("getCanUseBits - Not using BITS because of proxy usage");
    return "NoBits_Proxy";
  }
  LOG("getCanUseBits - BITS can be used to download updates");
  return "CanUseBits";
}

769
/**
770
771
 * Logs a string to the error console. If enabled, also logs to the update
 * messages file.
772
773
774
775
776
777
 * @param   string
 *          The string to write to the error console.
 */
function LOG(string) {
  if (gLogEnabled) {
    dump("*** AUS:SVC " + string + "\n");
778
779
780
    if (!Cu.isInAutomation) {
      Services.console.logStringMessage("AUS:SVC " + string);
    }
781
782
783
784
785

    if (gLogfileEnabled) {
      if (!gLogfileWritePromise) {
        let logfile = Services.dirsvc.get(KEY_PROFILE_DIR, Ci.nsIFile);
        logfile.append(FILE_UPDATE_MESSAGES);
786
787
788
789
        gLogfileWritePromise = OS.File.open(logfile.path, {
          write: true,
          append: true,
        }).catch(error => {
790
          dump("*** AUS:SVC Unable to open messages file: " + error + "\n");
791
          Services.console.logStringMessage(
792
            "AUS:SVC Unable to open messages file: " + error
793
          );
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
          // Reject on failure so that writes are not attempted without a file
          // handle.
          return Promise.reject(error);
        });
      }
      gLogfileWritePromise = gLogfileWritePromise.then(async logfile => {
        // Catch failures from write promises and always return the logfile.
        // This allows subsequent write attempts and ensures that the file
        // handle keeps getting passed down the promise chain so that it will
        // be properly closed on shutdown.
        try {
          let encoded = new TextEncoder().encode(string + "\n");
          await logfile.write(encoded);
          await logfile.flush();
        } catch (e) {
          dump("*** AUS:SVC Unable to write to messages file: " + e + "\n");
810
          Services.console.logStringMessage(
811
            "AUS:SVC Unable to write to messages file: " + e
812
          );
813
814
815
816
        }
        return logfile;
      });
    }
817
818
819
  }
}

820
821
822
823
824
825
826
827
828
829
830
831
832
833
/**
 * Convert a string containing binary values to hex.
 */
function binaryToHex(input) {
  var result = "";
  for (var i = 0; i < input.length; ++i) {
    var hex = input.charCodeAt(i).toString(16);
    if (hex.length == 1)
      hex = "0" + hex;
    result += hex;
  }
  return result;
}

834
/**
835
836
837
838
839
840
 * Gets the specified directory at the specified hierarchy under the
 * update root directory and creates it if it doesn't exist.
 * @param   pathArray
 *          An array of path components to locate beneath the directory
 *          specified by |key|
 * @return  nsIFile object for the location specified.
841
 */
842
function getUpdateDirCreate(pathArray) {
843
844
845
846
847
  if (Cu.isInAutomation) {
    // This allows tests to use an alternate updates directory so they can test
    // startup behavior.
    const MAGIC_TEST_ROOT_PREFIX = "<test-root>";
    const PREF_TEST_ROOT = "mochitest.testRoot";
848
849
850
851
    let alternatePath = Services.prefs.getCharPref(
      PREF_APP_UPDATE_ALTUPDATEDIRPATH,
      null
    );
852
853
854
855
856
857
858
    if (alternatePath && alternatePath.startsWith(MAGIC_TEST_ROOT_PREFIX)) {
      let testRoot = Services.prefs.getCharPref(PREF_TEST_ROOT);
      let relativePath = alternatePath.substring(MAGIC_TEST_ROOT_PREFIX.length);
      if (AppConstants.platform == "win") {
        relativePath = relativePath.replace(/\//g, "\\");
      }
      alternatePath = testRoot + relativePath;
859
860
861
      let updateDir = Cc["@mozilla.org/file/local;1"].createInstance(
        Ci.nsIFile
      );
862
863
864
865
866
867
868
869
      updateDir.initWithPath(alternatePath);
      for (let i = 0; i < pathArray.length; ++i) {
        updateDir.append(pathArray[i]);
      }
      return updateDir;
    }
  }

870
871
872
873
  return FileUtils.getDir(KEY_UPDROOT, pathArray, true);
}

/**
874
875
876
 * Gets the application base directory.
 *
 * @return  nsIFile object for the application base directory.
877
878
879
 */
function getAppBaseDir() {
  return Services.dirsvc.get(KEY_EXECUTABLE, Ci.nsIFile).parent;
880
}
881

882
883
884
885
886
887
888
889
/**
 * Gets the root of the installation directory which is the application
 * bundle directory on Mac OS X and the location of the application binary
 * on all other platforms.
 *
 * @return nsIFile object for the directory
 */
function getInstallDirRoot() {
890
891
892
893
894
  let dir = getAppBaseDir();
  if (AppConstants.platform == "macosx") {
    // On Mac, we store the Updated.app directory inside the bundle directory.
    dir = dir.parent.parent;
  }
895
896
897
  return dir;
}

898
/**
899
 * Gets the file at the specified hierarchy under the update root directory.
900
 * @param   pathArray
901
 *          An array of path components to locate beneath the directory
902
903
904
 *          specified by |key|. The last item in this array must be the
 *          leaf name of a file.
 * @return  nsIFile object for the file specified. The file is NOT created
905
 *          if it does not exist, however all required directories along
906
907
908
 *          the way are.
 */
function getUpdateFile(pathArray) {
909
  let file = getUpdateDirCreate(pathArray.slice(0, -1));
910
911
912
913
  file.append(pathArray[pathArray.length - 1]);
  return file;
}

914
/**
915
 * Returns human readable status text from the updates.properties bundle
916
917
918
919
920
921
 * based on an error code
 * @param   code
 *          The error code to look up human readable status text for
 * @param   defaultCode
 *          The default code to look up should human readable status text
 *          not exist for |code|
922
 * @return  A human readable status text string
923
924
 */
function getStatusTextFromCode(code, defaultCode) {
925
  let reason;
926
  try {
927
    reason = gUpdateBundle.GetStringFromName("check_error-" + code);
928
929
930
    LOG(
      "getStatusTextFromCode - transfer error: " + reason + ", code: " + code
    );
931
  } catch (e) {
932
    // Use the default reason
933
    reason = gUpdateBundle.GetStringFromName("check_error-" + defaultCode);
934
935
936
937
938
939
    LOG(
      "getStatusTextFromCode - transfer error: " +
        reason +
        ", default code: " +
        defaultCode
    );
940
941
942
943
944
945
  }
  return reason;
}

/**
 * Get the Active Updates directory
946
 * @return The active updates directory, as a nsIFile object
947
 */
948
function getUpdatesDir() {
949
950
  // Right now, we only support downloading one patch at a time, so we always
  // use the same target directory.
951
  return getUpdateDirCreate([DIR_UPDATES, "0"]);
952
953
954
955
956
957
958
}

/**
 * Reads the update state from the update.status file in the specified
 * directory.
 * @param   dir
 *          The dir to look for an update.status file in
959
 * @return  The status value of the update.
960
961
 */
function readStatusFile(dir) {
962
  let statusFile = dir.clone();
963
  statusFile.append(FILE_UPDATE_STATUS);
964
  let status = readStringFromFile(statusFile) || STATE_NONE;
965
  LOG("readStatusFile - status: " + status + ", path: " + statusFile.path);
966
  return status;
967
968
}

969
970
971
972
973
974
975
976
977
978
979
980
981
/**
 * Reads the binary transparency result file from the given directory.
 * Removes the file if it is present (so don't call this twice and expect a
 * result the second time).
 * @param   dir
 *          The dir to look for an update.bt file in
 * @return  A error code from verifying binary transparency information or null
 *          if the file was not present (indicating there was no error).
 */
function readBinaryTransparencyResult(dir) {
  let binaryTransparencyResultFile = dir.clone();
  binaryTransparencyResultFile.append(FILE_BT_RESULT);
  let result = readStringFromFile(binaryTransparencyResultFile);
982
983
984
985
986
987
  LOG(
    "readBinaryTransparencyResult - result: " +
      result +
      ", path: " +
      binaryTransparencyResultFile.path
  );
988
989
990
991
992
993
994
995
  // If result is non-null, the file exists. We should remove it to avoid
  // double-reporting this result.
  if (result) {
    binaryTransparencyResultFile.remove(false);
  }
  return result;
}

996
/**
997
 * Writes the current update operation/state to a file in the patch
998
999
1000
 * directory, indicating to the patching system that operations need
 * to be performed.
 * @param   dir