tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

prefs.sys.mjs (17410B)


      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/. */
      4 
      5 // Prefs which start with this prefix are our "control" prefs - they indicate
      6 // which preferences should be synced.
      7 const PREF_SYNC_PREFS_PREFIX = "services.sync.prefs.sync.";
      8 
      9 // Prefs which have a default value are usually not synced - however, if the
     10 // preference exists under this prefix and the value is:
     11 // * `true`, then we do sync default values.
     12 // * `false`, then as soon as we ever sync a non-default value out, or sync
     13 //    any value in, then we toggle the value to `true`.
     14 //
     15 // We never explicitly set this pref back to false, so it's one-shot.
     16 // Some preferences which are known to have a different default value on
     17 // different platforms have this preference with a default value of `false`,
     18 // so they don't sync until one device changes to the non-default value, then
     19 // that value forever syncs, even if it gets reset back to the default.
     20 // Note that preferences handled this way *must also* have the "normal"
     21 // control pref set.
     22 // A possible future enhancement would be to sync these prefs so that
     23 // other distributions can flag them if they change the default, but that
     24 // doesn't seem worthwhile until we can be confident they'd actually create
     25 // this special control pref at the same time they flip the default.
     26 const PREF_SYNC_SEEN_PREFIX = "services.sync.prefs.sync-seen.";
     27 
     28 import {
     29  Store,
     30  SyncEngine,
     31  Tracker,
     32 } from "resource://services-sync/engines.sys.mjs";
     33 import { CryptoWrapper } from "resource://services-sync/record.sys.mjs";
     34 import { Svc, Utils } from "resource://services-sync/util.sys.mjs";
     35 import { SCORE_INCREMENT_XLARGE } from "resource://services-sync/constants.sys.mjs";
     36 import { CommonUtils } from "resource://services-common/utils.sys.mjs";
     37 
     38 const lazy = {};
     39 
     40 ChromeUtils.defineLazyGetter(lazy, "PREFS_GUID", () =>
     41  CommonUtils.encodeBase64URL(Services.appinfo.ID)
     42 );
     43 
     44 ChromeUtils.defineESModuleGetters(lazy, {
     45  AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
     46 });
     47 
     48 // In bug 1538015, we decided that it isn't always safe to allow all "incoming"
     49 // preferences to be applied locally. So we introduced another preference to control
     50 // this for backward compatibility. We removed that capability in bug 1854698, but in the
     51 // interests of working well between different versions of Firefox, we still forever
     52 // want to prevent this preference from syncing.
     53 // This was the name of the "control" pref.
     54 const PREF_SYNC_PREFS_ARBITRARY =
     55  "services.sync.prefs.dangerously_allow_arbitrary";
     56 
     57 // Check for a local control pref or PREF_SYNC_PREFS_ARBITRARY
     58 function isAllowedPrefName(prefName) {
     59  if (prefName == PREF_SYNC_PREFS_ARBITRARY) {
     60    return false; // never allow this.
     61  }
     62  // The pref must already have a control pref set, although it doesn't matter
     63  // here whether that value is true or false. We can't use prefHasUserValue
     64  // here because we also want to check prefs still with default values.
     65  try {
     66    Services.prefs.getBoolPref(PREF_SYNC_PREFS_PREFIX + prefName);
     67    // pref exists!
     68    return true;
     69  } catch (_) {
     70    return false;
     71  }
     72 }
     73 
     74 export function PrefRec(collection, id) {
     75  CryptoWrapper.call(this, collection, id);
     76 }
     77 
     78 PrefRec.prototype = {
     79  _logName: "Sync.Record.Pref",
     80 };
     81 Object.setPrototypeOf(PrefRec.prototype, CryptoWrapper.prototype);
     82 
     83 Utils.deferGetSet(PrefRec, "cleartext", ["value"]);
     84 
     85 export function PrefsEngine(service) {
     86  SyncEngine.call(this, "Prefs", service);
     87 }
     88 
     89 PrefsEngine.prototype = {
     90  _storeObj: PrefStore,
     91  _trackerObj: PrefTracker,
     92  _recordObj: PrefRec,
     93  version: 2,
     94 
     95  syncPriority: 1,
     96  allowSkippedRecord: false,
     97 
     98  async getChangedIDs() {
     99    // No need for a proper timestamp (no conflict resolution needed).
    100    let changedIDs = {};
    101    if (this._tracker.modified) {
    102      changedIDs[lazy.PREFS_GUID] = 0;
    103    }
    104    return changedIDs;
    105  },
    106 
    107  async _wipeClient() {
    108    await SyncEngine.prototype._wipeClient.call(this);
    109    this.justWiped = true;
    110  },
    111 
    112  async _reconcile(item) {
    113    // Apply the incoming item if we don't care about the local data
    114    if (this.justWiped) {
    115      this.justWiped = false;
    116      return true;
    117    }
    118    return SyncEngine.prototype._reconcile.call(this, item);
    119  },
    120 
    121  async _uploadOutgoing() {
    122    try {
    123      await SyncEngine.prototype._uploadOutgoing.call(this);
    124    } finally {
    125      this._store._incomingPrefs = null;
    126    }
    127  },
    128 
    129  async trackRemainingChanges() {
    130    if (this._modified.count() > 0) {
    131      this._tracker.modified = true;
    132    }
    133  },
    134 };
    135 Object.setPrototypeOf(PrefsEngine.prototype, SyncEngine.prototype);
    136 
    137 // We don't use services.sync.engine.tabs.filteredSchemes since it includes
    138 // about: pages and the like, which we want to be syncable in preferences.
    139 // Blob, moz-extension, data and file uris are never safe to sync,
    140 // so we limit our check to those.
    141 const UNSYNCABLE_URL_REGEXP = /^(moz-extension|blob|data|file):/i;
    142 function isUnsyncableURLPref(prefName) {
    143  if (Services.prefs.getPrefType(prefName) != Ci.nsIPrefBranch.PREF_STRING) {
    144    return false;
    145  }
    146  const prefValue = Services.prefs.getStringPref(prefName, "");
    147  return UNSYNCABLE_URL_REGEXP.test(prefValue);
    148 }
    149 
    150 function PrefStore(name, engine) {
    151  Store.call(this, name, engine);
    152  Svc.Obs.add(
    153    "profile-before-change",
    154    function () {
    155      this.__prefs = null;
    156    },
    157    this
    158  );
    159 }
    160 PrefStore.prototype = {
    161  __prefs: null,
    162  // used just for logging so we can work out why we chose to re-upload
    163  _incomingPrefs: null,
    164  get _prefs() {
    165    if (!this.__prefs) {
    166      this.__prefs = Services.prefs.getBranch("");
    167    }
    168    return this.__prefs;
    169  },
    170 
    171  _getSyncPrefs() {
    172    let syncPrefs = Services.prefs
    173      .getBranch(PREF_SYNC_PREFS_PREFIX)
    174      .getChildList("")
    175      .filter(pref => isAllowedPrefName(pref) && !isUnsyncableURLPref(pref));
    176    // Also sync preferences that determine which prefs get synced.
    177    let controlPrefs = syncPrefs.map(pref => PREF_SYNC_PREFS_PREFIX + pref);
    178    return controlPrefs.concat(syncPrefs);
    179  },
    180 
    181  _isSynced(pref) {
    182    if (pref.startsWith(PREF_SYNC_PREFS_PREFIX)) {
    183      // this is an incoming control pref, which is ignored if there's not already
    184      // a local control pref for the preference.
    185      let controlledPref = pref.slice(PREF_SYNC_PREFS_PREFIX.length);
    186      return isAllowedPrefName(controlledPref);
    187    }
    188 
    189    // This is the pref itself - it must be both allowed, and have a control
    190    // pref which is true.
    191    if (!this._prefs.getBoolPref(PREF_SYNC_PREFS_PREFIX + pref, false)) {
    192      return false;
    193    }
    194    return isAllowedPrefName(pref);
    195  },
    196 
    197  // Given a preference name, returns either a string, bool, number or null.
    198  _getPrefValue(pref) {
    199    switch (this._prefs.getPrefType(pref)) {
    200      case Ci.nsIPrefBranch.PREF_STRING:
    201        return this._prefs.getStringPref(pref);
    202      case Ci.nsIPrefBranch.PREF_INT:
    203        return this._prefs.getIntPref(pref);
    204      case Ci.nsIPrefBranch.PREF_BOOL:
    205        return this._prefs.getBoolPref(pref);
    206      //  case Ci.nsIPrefBranch.PREF_INVALID: handled by the fallthrough
    207    }
    208    return null;
    209  },
    210 
    211  _getAllPrefs() {
    212    let values = {};
    213    for (let pref of this._getSyncPrefs()) {
    214      // Note: _isSynced doesn't call isUnsyncableURLPref since it would cause
    215      // us not to apply (syncable) changes to preferences that are set locally
    216      // which have unsyncable urls.
    217      if (this._isSynced(pref) && !isUnsyncableURLPref(pref)) {
    218        let isSet = this._prefs.prefHasUserValue(pref);
    219        // Missing and default prefs get the null value, unless that `seen`
    220        // pref is set, in which case it always gets the value.
    221        let forceValue = this._prefs.getBoolPref(
    222          PREF_SYNC_SEEN_PREFIX + pref,
    223          false
    224        );
    225        if (isSet || forceValue) {
    226          values[pref] = this._getPrefValue(pref);
    227        } else {
    228          values[pref] = null;
    229        }
    230        // If incoming and outgoing don't match then either the user toggled a
    231        // pref that doesn't match an incoming non-default value for that pref
    232        // during a sync (unlikely!) or it refused to stick and is behaving oddly.
    233        if (this._incomingPrefs) {
    234          let inValue = this._incomingPrefs[pref];
    235          let outValue = values[pref];
    236          if (inValue != null && outValue != null && inValue != outValue) {
    237            this._log.debug(`Incoming pref '${pref}' refused to stick?`);
    238            this._log.trace(`Incoming: '${inValue}', outgoing: '${outValue}'`);
    239          }
    240        }
    241        // If this is a special "sync-seen" pref, and it's not the default value,
    242        // set the seen pref to true.
    243        if (
    244          isSet &&
    245          this._prefs.getBoolPref(PREF_SYNC_SEEN_PREFIX + pref, false) === false
    246        ) {
    247          this._log.trace(`toggling sync-seen pref for '${pref}' to true`);
    248          this._prefs.setBoolPref(PREF_SYNC_SEEN_PREFIX + pref, true);
    249        }
    250      }
    251    }
    252    return values;
    253  },
    254 
    255  _maybeLogPrefChange(pref, incomingValue, existingValue) {
    256    if (incomingValue != existingValue) {
    257      this._log.debug(`Adjusting preference "${pref}" to the incoming value`);
    258      // values are PII, so must only be logged at trace.
    259      this._log.trace(`Existing: ${existingValue}. Incoming: ${incomingValue}`);
    260    }
    261  },
    262 
    263  async _setAllPrefs(values) {
    264    const selectedThemeIDPref = "extensions.activeThemeID";
    265    const pendingThemePref = "extensions.pendingActiveThemeID";
    266    let selectedThemeIDBefore = this._prefs.getStringPref(
    267      selectedThemeIDPref,
    268      ""
    269    );
    270    let selectedThemeIDAfter = selectedThemeIDBefore;
    271    // Clear the pending theme pref that might've hung around
    272    if (this._prefs.prefHasUserValue(pendingThemePref)) {
    273      this._prefs.clearUserPref(pendingThemePref);
    274    }
    275 
    276    // Update 'services.sync.prefs.sync.foo.pref' before 'foo.pref', otherwise
    277    // _isSynced returns false when 'foo.pref' doesn't exist (e.g., on a new device).
    278    let prefs = Object.keys(values).sort(
    279      a => -a.indexOf(PREF_SYNC_PREFS_PREFIX)
    280    );
    281    for (let pref of prefs) {
    282      let value = values[pref];
    283      if (!this._isSynced(pref)) {
    284        // It's unusual for us to find an incoming preference (ie, a pref some other
    285        // instance thinks is syncable) which we don't think is syncable.
    286        this._log.trace(`Ignoring incoming unsyncable preference "${pref}"`);
    287        continue;
    288      }
    289 
    290      if (typeof value == "string" && UNSYNCABLE_URL_REGEXP.test(value)) {
    291        this._log.trace(`Skipping incoming unsyncable url for pref: ${pref}`);
    292        continue;
    293      }
    294 
    295      switch (pref) {
    296        // Some special prefs we don't want to set directly.
    297        case selectedThemeIDPref:
    298          selectedThemeIDAfter = value;
    299          break;
    300 
    301        // default is to just set the pref
    302        default: {
    303          if (value == null) {
    304            // Pref has gone missing. The best we can do is reset it.
    305            if (this._prefs.prefHasUserValue(pref)) {
    306              this._log.debug(`Clearing existing local preference "${pref}"`);
    307              this._log.trace(
    308                `Existing local value for preference: ${this._getPrefValue(
    309                  pref
    310                )}`
    311              );
    312            }
    313            this._prefs.clearUserPref(pref);
    314          } else {
    315            try {
    316              switch (typeof value) {
    317                case "string":
    318                  this._maybeLogPrefChange(
    319                    pref,
    320                    value,
    321                    this._prefs.getStringPref(pref, undefined)
    322                  );
    323                  this._prefs.setStringPref(pref, value);
    324                  break;
    325                case "number":
    326                  this._maybeLogPrefChange(
    327                    pref,
    328                    value,
    329                    this._prefs.getIntPref(pref, undefined)
    330                  );
    331                  this._prefs.setIntPref(pref, value);
    332                  break;
    333                case "boolean":
    334                  this._maybeLogPrefChange(
    335                    pref,
    336                    value,
    337                    this._prefs.getBoolPref(pref, undefined)
    338                  );
    339                  this._prefs.setBoolPref(pref, value);
    340                  break;
    341              }
    342            } catch (ex) {
    343              this._log.trace(`Failed to set pref: ${pref}`, ex);
    344            }
    345          }
    346          // If there's a "sync-seen" pref for this it gets toggled to true
    347          // regardless of the value.
    348          let seenPref = PREF_SYNC_SEEN_PREFIX + pref;
    349          if (
    350            this._prefs.getPrefType(seenPref) != Ci.nsIPrefBranch.PREF_INVALID
    351          ) {
    352            this._prefs.setBoolPref(PREF_SYNC_SEEN_PREFIX + pref, true);
    353          }
    354        }
    355      }
    356    }
    357    // Themes are a little messy. Themes which have been installed are handled
    358    // by the addons engine - but default themes aren't seen by that engine.
    359    // So if there's a new default theme ID and that ID corresponds to a
    360    // system addon, then we arrange to enable that addon here.
    361    if (selectedThemeIDBefore != selectedThemeIDAfter) {
    362      // We need to await before continuing here because enabling theme-addons
    363      // also sets the extensions.activeThemeId pref, if we don't
    364      // there are scenarios where the prefs thought we didn't
    365      // actually set the pref and could cause unintended theme resets
    366      try {
    367        await this._maybeEnableBuiltinTheme(selectedThemeIDAfter);
    368      } catch (e) {
    369        this._log.error("Failed to maybe update the default theme", e);
    370      }
    371    }
    372  },
    373 
    374  async _maybeEnableBuiltinTheme(themeId) {
    375    let addon = null;
    376    try {
    377      addon = await lazy.AddonManager.getAddonByID(themeId);
    378    } catch (ex) {
    379      this._log.trace(
    380        `There's no addon with ID '${themeId} - it can't be a builtin theme`
    381      );
    382      return;
    383    }
    384    if (addon && addon.isBuiltin && addon.type == "theme") {
    385      this._log.trace(`Enabling builtin theme '${themeId}'`);
    386      await addon.enable();
    387    } else {
    388      // We set this pref if the theme is not built-in and instead need to pass it
    389      // to the addons engine and enable, see addonutils for more info
    390      this._prefs.setStringPref("extensions.pendingActiveThemeID", themeId);
    391      this._log.trace(
    392        `Have incoming theme ID of '${themeId}' but it's not a builtin theme,
    393        setting extensions.pendingActiveThemeID so addons engine can enable it`
    394      );
    395    }
    396  },
    397 
    398  async getAllIDs() {
    399    /* We store all prefs in just one WBO, with just one GUID */
    400    let allprefs = {};
    401    allprefs[lazy.PREFS_GUID] = true;
    402    return allprefs;
    403  },
    404 
    405  async changeItemID() {
    406    this._log.trace("PrefStore GUID is constant!");
    407  },
    408 
    409  async itemExists(id) {
    410    return id === lazy.PREFS_GUID;
    411  },
    412 
    413  async createRecord(id, collection) {
    414    let record = new PrefRec(collection, id);
    415 
    416    if (id == lazy.PREFS_GUID) {
    417      record.value = this._getAllPrefs();
    418    } else {
    419      record.deleted = true;
    420    }
    421 
    422    return record;
    423  },
    424 
    425  async create() {
    426    this._log.trace("Ignoring create request");
    427  },
    428 
    429  async remove() {
    430    this._log.trace("Ignoring remove request");
    431  },
    432 
    433  async update(record) {
    434    // Silently ignore pref updates that are for other apps.
    435    if (record.id != lazy.PREFS_GUID) {
    436      return;
    437    }
    438 
    439    this._log.trace("Received pref updates, applying...");
    440    this._incomingPrefs = record.value;
    441    await this._setAllPrefs(record.value);
    442  },
    443 
    444  async wipe() {
    445    this._log.trace("Ignoring wipe request");
    446  },
    447 };
    448 Object.setPrototypeOf(PrefStore.prototype, Store.prototype);
    449 
    450 function PrefTracker(name, engine) {
    451  Tracker.call(this, name, engine);
    452  this._ignoreAll = false;
    453  Svc.Obs.add("profile-before-change", this.asyncObserver);
    454 }
    455 PrefTracker.prototype = {
    456  get ignoreAll() {
    457    return this._ignoreAll;
    458  },
    459 
    460  set ignoreAll(value) {
    461    this._ignoreAll = value;
    462  },
    463 
    464  get modified() {
    465    return Svc.PrefBranch.getBoolPref("engine.prefs.modified", false);
    466  },
    467  set modified(value) {
    468    Svc.PrefBranch.setBoolPref("engine.prefs.modified", value);
    469  },
    470 
    471  clearChangedIDs: function clearChangedIDs() {
    472    this.modified = false;
    473  },
    474 
    475  __prefs: null,
    476  get _prefs() {
    477    if (!this.__prefs) {
    478      this.__prefs = Services.prefs.getBranch("");
    479    }
    480    return this.__prefs;
    481  },
    482 
    483  onStart() {
    484    Services.prefs.addObserver("", this.asyncObserver);
    485  },
    486 
    487  onStop() {
    488    this.__prefs = null;
    489    Services.prefs.removeObserver("", this.asyncObserver);
    490  },
    491 
    492  async observe(subject, topic, data) {
    493    switch (topic) {
    494      case "profile-before-change":
    495        await this.stop();
    496        break;
    497      case "nsPref:changed":
    498        if (this.ignoreAll) {
    499          break;
    500        }
    501        // Trigger a sync for MULTI-DEVICE for a change that determines
    502        // which prefs are synced or a regular pref change.
    503        if (
    504          data.indexOf(PREF_SYNC_PREFS_PREFIX) == 0 ||
    505          this._prefs.getBoolPref(PREF_SYNC_PREFS_PREFIX + data, false)
    506        ) {
    507          this.score += SCORE_INCREMENT_XLARGE;
    508          this.modified = true;
    509          this._log.trace("Preference " + data + " changed");
    510        }
    511        break;
    512    }
    513  },
    514 };
    515 Object.setPrototypeOf(PrefTracker.prototype, Tracker.prototype);
    516 
    517 export function getPrefsGUIDForTest() {
    518  return lazy.PREFS_GUID;
    519 }