UpdateService.jsm 181 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
const { AppConstants } = ChromeUtils.import(
  "resource://gre/modules/AppConstants.jsm"
);
const { AUSTLMY } = ChromeUtils.import(
  "resource://gre/modules/UpdateTelemetry.jsm"
);
15
16
17
18
19

const { TorProtocolService } = ChromeUtils.import(
  "resource:///modules/TorProtocolService.jsm"
);

20
21
22
23
24
25
26
27
28
29
30
31
32
33
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"
);
34

35
XPCOMUtils.defineLazyGlobalGetters(this, ["DOMParser", "XMLHttpRequest"]);
36

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

51
52
53
const UPDATESERVICE_CID = Components.ID(
  "{B3C290A6-3943-4B89-8BBE-C01EB7B3B311}"
);
54

55
56
57
58
59
60
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";
61
const PREF_APP_UPDATE_CANCELATIONS_OSX_MAX = "app.update.cancelations.osx.max";
62
63
const PREF_APP_UPDATE_DISABLEDFORTESTING = "app.update.disabledForTesting";
const PREF_APP_UPDATE_DOWNLOAD_ATTEMPTS = "app.update.download.attempts";
64
const PREF_APP_UPDATE_DOWNLOAD_MAXATTEMPTS = "app.update.download.maxAttempts";
65
66
67
68
69
70
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";
const PREF_APP_UPDATE_LOG = "app.update.log";
const PREF_APP_UPDATE_LOG_FILE = "app.update.log.file";
71
const PREF_APP_UPDATE_NOTIFYDURINGDOWNLOAD = "app.update.notifyDuringDownload";
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
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";
92

93
94
const FILE_ACTIVE_UPDATE_XML = "active-update.xml";
const FILE_BACKUP_UPDATE_LOG = "backup-update.log";
95
96
97
98
99
100
101
102
103
104
105
106
107
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";
108
109
const STATE_PENDING_SERVICE = "pending-service";
const STATE_PENDING_ELEVATE = "pending-elevate";
110
111
const STATE_APPLYING = "applying";
const STATE_APPLIED = "applied";
112
const STATE_APPLIED_SERVICE = "applied-service";
113
const STATE_SUCCEEDED = "succeeded";
114
const STATE_DOWNLOAD_FAILED = "download-failed";
115
const STATE_FAILED = "failed";
116

117
118
// BITS will keep retrying a download after transient errors, unless this much
// time has passed since there has been download progress.
119
120
121
122
// 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;
123

124
125
126
127
128
129
130
// 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;

131
// The values below used by this code are from common/updatererrors.h
132
133
const WRITE_ERROR = 7;
const ELEVATION_CANCELED = 9;
134
135
const SERVICE_UPDATER_COULD_NOT_BE_STARTED = 24;
const SERVICE_NOT_ENOUGH_COMMAND_LINE_ARGS = 25;
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
161
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;
162
163

// Array of write errors to simplify checks for write errors
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
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,
];
179
180

// Array of write errors to simplify checks for service errors
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
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,
];
197
198

// Error codes 80 through 99 are reserved for nsUpdateService.js and are not
199
// defined in common/updatererrors.h
200
201
202
203
204
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;
205
206

// Custom update error codes
207
const BACKGROUNDCHECK_MULTIPLE_FAILURES = 110;
208
const NETWORK_ERROR_OFFLINE = 111;
209
const PROXY_SERVER_CONNECTION_REFUSED = 2152398920;
210

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

214
215
216
// 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.
217
const HRESULT_E_ACCESSDENIED = -2147024891;
218

219
const DOWNLOAD_CHUNK_SIZE = 300000; // bytes
220

221
222
223
224
// 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;

225
226
227
228
229
// 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.
230
const DEFAULT_SOCKET_RETRYTIMEOUT = 2000;
231

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

236
237
238
239
240
241
242
243
244
245
246
// 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",
};

247
248
249
250
251
252
// The interval for the update xml write deferred task.
const XML_SAVER_INTERVAL_MS = 200;

// 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.
253
var gUpdateFileWriteInfo = { phase: null, failure: false };
254
var gUpdateMutexHandle = null;
255
256
257
// The permissions of the update directory should be fixed no more than once per
// session
var gUpdateDirPermissionFixAttempted = false;
258
259
// This is used for serializing writes to the update log file
var gLogfileWritePromise;
260
261
262
263
264
// 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;
265

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

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

281
282
283
284
285
286
287
XPCOMUtils.defineLazyGetter(
  this,
  "gUpdateBundle",
  function aus_gUpdateBundle() {
    return Services.strings.createBundle(URI_UPDATES_PROPERTIES);
  }
);
288

289
290
291
292
293
294
295
296
/**
 * 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) {
297
298
  const NORMAL_FILE_TYPE = Ci.nsIFile.NORMAL_FILE_TYPE;
  const DIRECTORY_TYPE = Ci.nsIFile.DIRECTORY_TYPE;
299
  if (updateTestFile.exists()) {
300
    updateTestFile.remove(false);
301
302
303
304
305
  }
  updateTestFile.create(
    createDirectory ? DIRECTORY_TYPE : NORMAL_FILE_TYPE,
    createDirectory ? FileUtils.PERMS_DIRECTORY : FileUtils.PERMS_FILE
  );
306
307
308
  updateTestFile.remove(false);
}

309
#ifdef XP_WIN
310
/**
311
 * Windows only function that closes a Win32 handle.
312
313
314
315
 *
 * @param handle The handle to close
 */
function closeHandle(handle) {
316
317
  if (handle) {
    let lib = ctypes.open("kernel32.dll");
318
319
320
321
322
323
    let CloseHandle = lib.declare(
      "CloseHandle",
      ctypes.winapi_abi,
      ctypes.int32_t /* success */,
      ctypes.void_t.ptr
    ); /* handle */
324
325
326
    CloseHandle(handle);
    lib.close();
  }
327
328
329
}

/**
330
 * Windows only function that creates a mutex.
331
 *
332
333
334
335
336
 * @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.
337
 */
338
339
function createMutex(aName, aAllowExisting = true) {
  if (AppConstants.platform != "win") {
340
    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
341
342
343
  }

  const INITIAL_OWN = 1;
344
  const ERROR_ALREADY_EXISTS = 0xb7;
345
  let lib = ctypes.open("kernel32.dll");
346
347
348
349
350
351
352
353
  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 */
354

355
356
  let handle = CreateMutexW(null, INITIAL_OWN, aName);
  let alreadyExists = ctypes.winLastError == ERROR_ALREADY_EXISTS;
357
358
359
360
361
362
  if (handle && !handle.isNull() && !aAllowExisting && alreadyExists) {
    closeHandle(handle);
    handle = null;
  }
  lib.close();

363
  if (handle && handle.isNull()) {
364
    handle = null;
365
  }
366
367
368
369
370

  return handle;
}

/**
371
372
 * Windows only function that determines a unique mutex name for the
 * installation.
373
 *
374
375
376
 * @param aGlobal
 *        true if the function should return a global mutex. A global mutex is
 *        valid across different sessions.
377
378
 * @return Global mutex path
 */
379
380
function getPerInstallationMutexName(aGlobal = true) {
  if (AppConstants.platform != "win") {
381
    throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
382
  }
383

384
385
386
  let hasher = Cc["@mozilla.org/security/hash;1"].createInstance(
    Ci.nsICryptoHash
  );
387
  hasher.init(hasher.SHA1);
388

389
  let exeFile = Services.dirsvc.get(KEY_EXECUTABLE, Ci.nsIFile);
390

391
392
393
  let converter = Cc[
    "@mozilla.org/intl/scriptableunicodeconverter"
  ].createInstance(Ci.nsIScriptableUnicodeConverter);
394
395
396
397
  converter.charset = "UTF-8";
  var data = converter.convertToByteArray(exeFile.path.toLowerCase());

  hasher.update(data, data.length);
398
399
400
  return (
    (aGlobal ? "Global\\" : "") + "MozillaUpdateMutex-" + hasher.finish(true)
  );
401
}
402
#endif
403
404
405

/**
 * Whether or not the current instance has the update mutex. The update mutex
406
 * gives protection against 2 applications from the same installation updating:
407
 * 1) Running multiple profiles from the same installation path
408
 * 2) Two applications running in 2 different user sessions from the same path
409
410
411
412
 *
 * @return true if this instance holds the update mutex
 */
function hasUpdateMutex() {
413
#ifdef XP_WIN
414
415
416
  if (AppConstants.platform != "win") {
    return true;
  }
417
418
  if (!gUpdateMutexHandle) {
    gUpdateMutexHandle = createMutex(getPerInstallationMutexName(true), false);
419
  }
420
  return !!gUpdateMutexHandle;
421
422
423
#else
  return true;
#endif
424
425
}

426
427
428
429
430
431
432
433
434
/**
 * 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()) {
435
    let item = items.nextFile;
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
    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() {
454
455
456
457
458
#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
459
460
461
462
463
464
465
  if (AppConstants.platform != "macosx") {
    return false;
  }

  try {
    // Recursively check that the application bundle (and its descendants) can
    // be written to.
466
467
468
469
470
471
472
473
474
475
476
477
    LOG(
      "getElevationRequired - recursively testing write access on " +
        getInstallDirRoot().path
    );
    if (
      !getInstallDirRoot().isWritable() ||
      !areDirectoryEntriesWriteable(getInstallDirRoot())
    ) {
      LOG(
        "getElevationRequired - unable to write to application bundle, " +
          "elevation required"
      );
478
479
480
      return true;
    }
  } catch (ex) {
481
482
483
484
485
    LOG(
      "getElevationRequired - unable to write to application bundle, " +
        "elevation required. Exception: " +
        ex
    );
486
487
    return true;
  }
488
489
490
491
  LOG(
    "getElevationRequired - able to write to application bundle, elevation " +
      "not required"
  );
492
  return false;
493
#endif
494
495
496
497
}

/**
 * Determines whether or not an update can be applied. This is always true on
498
499
500
501
 * 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.
502
503
504
 *
 * @return true if an update can be applied, false otherwise
 */
505
function getCanApplyUpdates() {
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
  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;
  }

526
#if !defined(TOR_BROWSER_UPDATE)
527
  if (AppConstants.platform == "macosx") {
528
529
530
531
    LOG(
      "getCanApplyUpdates - bypass the write since elevation can be used " +
        "on Mac OS X"
    );
532
    return true;
533
534
  }

535
  if (shouldUseService()) {
536
537
538
539
    LOG(
      "getCanApplyUpdates - bypass the write checks because the Windows " +
        "Maintenance Service can be used"
    );
540
541
    return true;
  }
542
#endif
543

544
545
546
547
548
549
550
  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.
551
552
      let userCanElevate = Services.appinfo.QueryInterface(Ci.nsIWinAppHelper)
        .userCanElevate;
553
554
555
556
557
558
      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()) {
559
560
          appDirTestFile.remove(false);
        }
561
562
563
564
        appDirTestFile.create(
          Ci.nsIFile.NORMAL_FILE_TYPE,
          FileUtils.PERMS_FILE
        );
565
        appDirTestFile.remove(false);
566
567
      }
    }
568
569
570
571
572
  } catch (e) {
    LOG("getCanApplyUpdates - unable to apply updates. Exception: " + e);
    // No write access to the installation directory
    return false;
  }
573

574
  LOG("getCanApplyUpdates - able to apply updates");
575
  return true;
576
}
577

578
579
580
581
582
583
/**
 * 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.
 */
584
585
586
587
588
589
590
591
592
593
594
XPCOMUtils.defineLazyGetter(
  this,
  "gCanStageUpdatesSession",
  function aus_gCSUS() {
    if (getElevationRequired()) {
      LOG(
        "gCanStageUpdatesSession - unable to stage updates because elevation " +
          "is required."
      );
      return false;
    }
595

596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
    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;
628
629
    }

630
631
632
633
    LOG("gCanStageUpdatesSession - able to stage updates");
    return true;
  }
);
634

635
636
637
638
639
640
/**
 * Whether or not the application can stage an update.
 *
 * @return true if updates can be staged.
 */
function getCanStageUpdates() {
641
  // If staging updates are disabled, then just bail out!
642
  if (!Services.prefs.getBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, false)) {
643
644
645
646
    LOG(
      "getCanStageUpdates - staging updates is disabled by preference " +
        PREF_APP_UPDATE_STAGING_ENABLED
    );
647
648
649
    return false;
  }

650
  if (AppConstants.platform == "win" && shouldUseService()) {
651
652
    // No need to perform directory write checks, the maintenance service will
    // be able to write to all directories.
653
    LOG("getCanStageUpdates - able to stage updates using the service");
654
655
656
    return true;
  }

657
  if (!hasUpdateMutex()) {
658
659
    LOG(
      "getCanStageUpdates - unable to apply updates because another " +
660
        "instance of the application is already handling updates for this " +
661
662
        "installation."
    );
663
664
665
    return false;
  }

666
  return gCanStageUpdatesSession;
667
}
668

669
670
671
672
673
674
675
676
677
678
/*
 * 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
679
680
681
682
 *         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
683
684
685
686
687
688
689
690
691
692
 */
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";
  }
693

694
695
696
697
698
699
700
701
702
703
704
705
  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;
706
707
708
709
710
  if (
    Services.prefs.getIntPref(PREF_NETWORK_PROXY_TYPE, defaultProxy) !=
      defaultProxy &&
    !Cu.isInAutomation
  ) {
711
712
713
714
715
716
717
    LOG("getCanUseBits - Not using BITS because of proxy usage");
    return "NoBits_Proxy";
  }
  LOG("getCanUseBits - BITS can be used to download updates");
  return "CanUseBits";
}

718
/**
719
720
 * Logs a string to the error console. If enabled, also logs to the update
 * messages file.
721
722
723
724
725
726
 * @param   string
 *          The string to write to the error console.
 */
function LOG(string) {
  if (gLogEnabled) {
    dump("*** AUS:SVC " + string + "\n");
727
728
729
    if (!Cu.isInAutomation) {
      Services.console.logStringMessage("AUS:SVC " + string);
    }
730
731
732
733
734

    if (gLogfileEnabled) {
      if (!gLogfileWritePromise) {
        let logfile = Services.dirsvc.get(KEY_PROFILE_DIR, Ci.nsIFile);
        logfile.append(FILE_UPDATE_MESSAGES);
735
736
737
738
        gLogfileWritePromise = OS.File.open(logfile.path, {
          write: true,
          append: true,
        }).catch(error => {
739
          dump("*** AUS:SVC Unable to open messages file: " + error + "\n");
740
          Services.console.logStringMessage(
741
            "AUS:SVC Unable to open messages file: " + error
742
          );
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
          // 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");
759
          Services.console.logStringMessage(
760
            "AUS:SVC Unable to write to messages file: " + e
761
          );
762
763
764
765
        }
        return logfile;
      });
    }
766
767
768
  }
}

769
770
771
772
773
774
775
776
777
778
779
780
781
782
/**
 * 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;
}

783
/**
784
785
786
787
788
789
 * 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.
790
 */
791
function getUpdateDirCreate(pathArray) {
792
793
794
795
796
  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";
797
798
799
800
    let alternatePath = Services.prefs.getCharPref(
      PREF_APP_UPDATE_ALTUPDATEDIRPATH,
      null
    );
801
802
803
804
805
806
807
    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;
808
809
810
      let updateDir = Cc["@mozilla.org/file/local;1"].createInstance(
        Ci.nsIFile
      );
811
812
813
814
815
816
817
818
      updateDir.initWithPath(alternatePath);
      for (let i = 0; i < pathArray.length; ++i) {
        updateDir.append(pathArray[i]);
      }
      return updateDir;
    }
  }

819
820
821
822
  return FileUtils.getDir(KEY_UPDROOT, pathArray, true);
}

/**
823
824
825
 * Gets the application base directory.
 *
 * @return  nsIFile object for the application base directory.
826
827
828
 */
function getAppBaseDir() {
  return Services.dirsvc.get(KEY_EXECUTABLE, Ci.nsIFile).parent;
829
}
830

831
832
833
834
835
836
837
838
/**
 * 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() {
839
840
841
842
843
  let dir = getAppBaseDir();
  if (AppConstants.platform == "macosx") {
    // On Mac, we store the Updated.app directory inside the bundle directory.
    dir = dir.parent.parent;
  }
844
845
846
  return dir;
}

847
/**
848
 * Gets the file at the specified hierarchy under the update root directory.
849
 * @param   pathArray
850
 *          An array of path components to locate beneath the directory
851
852
853
 *          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
854
 *          if it does not exist, however all required directories along
855
856
857
 *          the way are.
 */
function getUpdateFile(pathArray) {
858
  let file = getUpdateDirCreate(pathArray.slice(0, -1));
859
860
861
862
  file.append(pathArray[pathArray.length - 1]);
  return file;
}

863
/**
864
 * Returns human readable status text from the updates.properties bundle
865
866
867
868
869
870
 * 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|
871
 * @return  A human readable status text string
872
873
 */
function getStatusTextFromCode(code, defaultCode) {
874
  let reason;
875
  try {
876
    reason = gUpdateBundle.GetStringFromName("check_error-" + code);
877
878
879
    LOG(
      "getStatusTextFromCode - transfer error: " + reason + ", code: " + code
    );
880
  } catch (e) {
881
    // Use the default reason
882
    reason = gUpdateBundle.GetStringFromName("check_error-" + defaultCode);
883
884
885
886
887
888
    LOG(
      "getStatusTextFromCode - transfer error: " +
        reason +
        ", default code: " +
        defaultCode
    );
889
890
891
892
893
894
  }
  return reason;
}

/**
 * Get the Active Updates directory
895
 * @return The active updates directory, as a nsIFile object
896
 */
897
function getUpdatesDir() {
898
899
  // Right now, we only support downloading one patch at a time, so we always
  // use the same target directory.
900
  return getUpdateDirCreate([DIR_UPDATES, "0"]);
901
902
903
904
905
906
907
}

/**
 * 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
908
 * @return  The status value of the update.
909
910
 */
function readStatusFile(dir) {
911
  let statusFile = dir.clone();
912
  statusFile.append(FILE_UPDATE_STATUS);
913
  let status = readStringFromFile(statusFile) || STATE_NONE;
914
  LOG("readStatusFile - status: " + status + ", path: " + statusFile.path);
915
  return status;
916
917
}

918
919
920
921
922
923
924
925
926
927
928
929
930
/**
 * 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);
931
932
933
934
935
936
  LOG(
    "readBinaryTransparencyResult - result: " +
      result +
      ", path: " +
      binaryTransparencyResultFile.path
  );
937
938
939
940
941
942
943
944
  // 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;
}

945
/**
946
 * Writes the current update operation/state to a file in the patch
947
948
949
 * directory, indicating to the patching system that operations need
 * to be performed.
 * @param   dir
950
 *          The patch directory where the update.status file should be
951
952
953
954
955
 *          written.
 * @param   state
 *          The state value to write.
 */
function writeStatusFile(dir, state) {
956
  let statusFile = dir.clone();
957
  statusFile.append(FILE_UPDATE_STATUS);
958
959
960
961
  let success = writeStringToFile(statusFile, state);
  if (!success) {
    handleCriticalWriteFailure(statusFile.path);
  }
962
963
}

964
/**
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
 * Writes the update's application version to a file in the patch directory. If
 * the update doesn't provide application version information via the
 * appVersion attribute the string "null" will be written to the file.
 * This value is compared during startup (in nsUpdateDriver.cpp) to determine if
 * the update should be applied. Note that this won't provide protection from
 * downgrade of the application for the nightly user case where the application
 * version doesn't change.
 * @param   dir
 *          The patch directory where the update.version file should be
 *          written.
 * @param   version
 *          The version value to write. Will be the string "null" when the
 *          update doesn't provide the appVersion attribute in the update xml.
 */
function writeVersionFile(dir, version) {
  let versionFile = dir.clone();
  versionFile.append(FILE_UPDATE_VERSION);
982
983
984
985
  let success = writeStringToFile(versionFile, version);
  if (!success) {
    handleCriticalWriteFailure(versionFile.path);
  }
986
987
}

988
989
/**
 * Determines if the service should be used to attempt an update
990
 * or not.
991
992
993
994
 *
 * @return  true if the service should be used for updates.
 */
function shouldUseService() {
995
996
997
998
999
  // This function will return true if the mantenance service should be used if
  // all of the following conditions are met:
  // 1) This build was done with the maintenance service enabled
  // 2) The maintenance service is installed
  // 3) The pref for using the service is enabled
1000
1001
1002
1003
1004
  if (
    !AppConstants.MOZ_MAINTENANCE_SERVICE ||
    !isServiceInstalled() ||
    !Services.prefs.getBoolPref(PREF_APP_UPDATE_SERVICE_ENABLED, false)
  ) {
1005
    LOG("shouldUseService - returning false");
1006
1007
1008
    return false;
  }

1009
1010
  LOG("shouldUseService - returning true");
  return true;
1011
1012
}

1013
/**
1014
 * Determines if the service is is installed.
1015
 *
1016
 * @return  true if the service is installed.
1017
1018
 */
function isServiceInstalled() {
1019
1020
1021
  if (!AppConstants.MOZ_MAINTENANCE_SERVICE || AppConstants.platform != "win") {
    LOG("isServiceInstalled - returning false");
    return false;
1022
  }
1023
1024
1025

  let installed = 0;
  try {
1026
1027
1028
1029
1030
1031
1032
1033
    let wrk = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
      Ci.nsIWindowsRegKey
    );
    wrk.open(
      wrk.ROOT_KEY_LOCAL_MACHINE,
      "SOFTWARE\\Mozilla\\MaintenanceService",
      wrk.ACCESS_READ | wrk.WOW64_64
    );
1034
1035
    installed = wrk.readIntValue("Installed");
    wrk.close();
1036
  } catch (e) {}
1037
  installed = installed == 1; // convert to bool
1038
1039
  LOG("isServiceInstalled - returning " + installed);
  return installed;
1040
1041
}

1042
/**
1043
1044
1045
1046
1047
 * Removes the contents of the updates patch directory and rotates the update
 * logs when present. If the update.log exists in the patch directory this will
 * move the last-update.log if it exists to backup-update.log in the parent
 * directory of the patch directory and then move the update.log in the patch
 * directory to last-update.log in the parent directory of the patch directory.
1048
 *
1049
1050
 * @param aRemovePatchFiles (optional, defaults to true)
 *        When true the update's patch directory contents are removed.
1051
 */
1052
function cleanUpUpdatesDir(aRemovePatchFiles = true) {
1053
  let updateDir;
1054
  try {
1055
    updateDir = getUpdatesDir();
1056
  } catch (e) {
1057
1058
1059
1060
1061
    LOG(
      "cleanUpUpdatesDir - unable to get the updates patch directory. " +
        "Exception: " +
        e
    );
1062
    return;
1063
  }
1064

1065
  // Preserve the last update log file for debugging purposes.
1066
1067
1068
  let updateLogFile = updateDir.clone();
  updateLogFile.append(FILE_UPDATE_LOG);
  if (updateLogFile.exists()) {
1069
    let dir = updateDir.parent;
1070
    let logFile = dir.clone();
1071
    logFile.append(FILE_LAST_UPDATE_LOG);
1072
    if (logFile.exists()) {
1073
      try {
1074
        logFile.moveTo(dir, FILE_BACKUP_UPDATE_LOG);
1075
      } catch (e) {
1076
1077
1078
1079
1080
1081
        LOG(
          "cleanUpUpdatesDir - failed to rename file " +
            logFile.path +
            " to " +
            FILE_BACKUP_UPDATE_LOG
        );
1082
1083
      }
    }
1084
1085

    try {
1086
      updateLogFile.moveTo(dir, FILE_LAST_UPDATE_LOG);
1087
    } catch (e) {
1088
1089
1090
1091
1092
1093
      LOG(
        "cleanUpUpdatesDir - failed to rename file " +
          updateLogFile.path +
          " to " +
          FILE_LAST_UPDATE_LOG
      );
1094
1095
1096
    }
  }

1097
  if (aRemovePatchFiles) {
1098
1099
    let dirEntries = updateDir.directoryEntries;
    while (dirEntries.hasMoreElements()) {
1100
      let file = dirEntries.nextFile;
1101
1102
      // Now, recursively remove this file.  The recursive removal is needed for
      // Mac OSX because this directory will contain a copy of updater.app,
1103
1104
      // which is itself a directory and the MozUpdater directory on platforms
      // other than Windows.
1105
      try {
1106
        file.remove(true);
1107
      } catch (e) {
1108
        LOG("cleanUpUpdatesDir - failed to remove file " + file.path);
1109
      }
1110
1111
1112
    }
  }
}
1113
1114
1115
1116

/**
 * Clean up updates list and the updates directory.
 */
1117
function cleanupActiveUpdate() {
1118
  // Move the update from the Active Update list into the Past Updates list.
1119
1120
1121
  var um = Cc["@mozilla.org/updates/update-manager;1"].getService(
    Ci.nsIUpdateManager
  );
1122
1123
  // Setting |activeUpdate| to null will move the active update to the update
  // history.
1124
1125
1126
1127
  um.activeUpdate = null;
  um.saveUpdates();

  // Now trash the updates directory, since we're done with it
1128
  cleanUpUpdatesDir();
1129
1130
}

1131
1132
1133
/**
 * Writes a string of text to a file.  A newline will be appended to the data
 * written to the file.  This function only works with ASCII text.
1134
1135
1136
 * @param file An nsIFile indicating what file to write to.
 * @param text A string containing the text to write to the file.
 * @return true on success, false on failure.
1137
1138
 */
function writeStringToFile(file, text) {
1139
1140
1141
1142
1143
1144
1145
1146
1147
  try {
    let fos = FileUtils.openSafeFileOutputStream(file);
    text += "\n";
    fos.write(text, text.length);
    FileUtils.closeSafeFileOutputStream(fos);
  } catch (e) {
    return false;
  }
  return true;
1148
1149
}

1150
function readStringFromInputStream(inputStream) {
1151
1152
1153
  var sis = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
    Ci.nsIScriptableInputStream
  );
1154
1155
1156
  sis.init(inputStream);
  var text = sis.read(sis.available());
  sis.close();
1157
  if (text && text[text.length - 1] == "\n") {
1158
    text = text.slice(0, -1);
1159
  }