Services.js (16501B)
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 "use strict"; 6 7 /* globals localStorage, window */ 8 9 // XXX: This file is a copy of the Services shim from devtools-services. 10 // See https://github.com/firefox-devtools/devtools-core/blob/a9263b4c3f88ea42879a36cdc3ca8217b4a528ea/packages/devtools-services/index.js 11 // Many Jest tests in the debugger rely on preferences, but can't use Services. 12 // This fixture is probably doing too much and should be reduced to the minimum 13 // needed to pass the tests. 14 15 /* eslint-disable mozilla/valid-services */ 16 17 // Some constants from nsIPrefBranch.idl. 18 const PREF_INVALID = 0; 19 const PREF_STRING = 32; 20 const PREF_INT = 64; 21 const PREF_BOOL = 128; 22 const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed"; 23 24 // We prefix all our local storage items with this. 25 const PREFIX = "Services.prefs:"; 26 27 /** 28 * Create a new preference branch. This object conforms largely to 29 * nsIPrefBranch and nsIPrefService, though it only implements the 30 * subset needed by devtools. A preference branch can hold child 31 * preferences while also holding a preference value itself. 32 */ 33 class PrefBranch { 34 /** 35 * @param {PrefBranch} parent the parent branch, or null for the root 36 * branch. 37 * @param {string} name the base name of this branch 38 * @param {string} fullName the fully-qualified name of this branch 39 */ 40 constructor(parent, name, fullName) { 41 this._parent = parent; 42 this._name = name; 43 this._fullName = fullName; 44 this._observers = {}; 45 this._children = {}; 46 47 // Properties used when this branch has a value as well. 48 this._defaultValue = null; 49 this._hasUserValue = false; 50 this._userValue = null; 51 this._type = PREF_INVALID; 52 } 53 PREF_INVALID = PREF_INVALID; 54 PREF_STRING = PREF_STRING; 55 PREF_INT = PREF_INT; 56 PREF_BOOL = PREF_BOOL; 57 58 /** @see nsIPrefBranch.root. */ 59 get root() { 60 return this._fullName; 61 } 62 63 /** @see nsIPrefBranch.getPrefType. */ 64 getPrefType(prefName) { 65 return this._findPref(prefName)._type; 66 } 67 68 /** @see nsIPrefBranch.getBoolPref. */ 69 getBoolPref(prefName, defaultValue) { 70 try { 71 const thePref = this._findPref(prefName); 72 if (thePref._type !== PREF_BOOL) { 73 throw new Error(`${prefName} does not have bool type`); 74 } 75 return thePref._get(); 76 } catch (e) { 77 if (typeof defaultValue !== "undefined") { 78 return defaultValue; 79 } 80 throw e; 81 } 82 } 83 84 /** @see nsIPrefBranch.setBoolPref. */ 85 setBoolPref(prefName, value) { 86 if (typeof value !== "boolean") { 87 throw new Error("non-bool passed to setBoolPref"); 88 } 89 const thePref = this._findOrCreatePref(prefName, value, true, value); 90 if (thePref._type !== PREF_BOOL) { 91 throw new Error(`${prefName} does not have bool type`); 92 } 93 thePref._set(value); 94 } 95 96 /** @see nsIPrefBranch.getCharPref. */ 97 getCharPref(prefName, defaultValue) { 98 try { 99 const thePref = this._findPref(prefName); 100 if (thePref._type !== PREF_STRING) { 101 throw new Error(`${prefName} does not have string type`); 102 } 103 return thePref._get(); 104 } catch (e) { 105 if (typeof defaultValue !== "undefined") { 106 return defaultValue; 107 } 108 throw e; 109 } 110 } 111 112 /** @see nsIPrefBranch.getStringPref. */ 113 getStringPref() { 114 return this.getCharPref.apply(this, arguments); 115 } 116 117 /** @see nsIPrefBranch.setCharPref. */ 118 setCharPref(prefName, value) { 119 if (typeof value !== "string") { 120 throw new Error("non-string passed to setCharPref"); 121 } 122 const thePref = this._findOrCreatePref(prefName, value, true, value); 123 if (thePref._type !== PREF_STRING) { 124 throw new Error(`${prefName} does not have string type`); 125 } 126 thePref._set(value); 127 } 128 129 /** @see nsIPrefBranch.setStringPref. */ 130 setStringPref() { 131 return this.setCharPref.apply(this, arguments); 132 } 133 134 /** @see nsIPrefBranch.getIntPref. */ 135 getIntPref(prefName, defaultValue) { 136 try { 137 const thePref = this._findPref(prefName); 138 if (thePref._type !== PREF_INT) { 139 throw new Error(`${prefName} does not have int type`); 140 } 141 return thePref._get(); 142 } catch (e) { 143 if (typeof defaultValue !== "undefined") { 144 return defaultValue; 145 } 146 throw e; 147 } 148 } 149 150 /** @see nsIPrefBranch.setIntPref. */ 151 setIntPref(prefName, value) { 152 if (typeof value !== "number") { 153 throw new Error("non-number passed to setIntPref"); 154 } 155 const thePref = this._findOrCreatePref(prefName, value, true, value); 156 if (thePref._type !== PREF_INT) { 157 throw new Error(`${prefName} does not have int type`); 158 } 159 thePref._set(value); 160 } 161 162 /** @see nsIPrefBranch.clearUserPref */ 163 clearUserPref(prefName) { 164 const thePref = this._findPref(prefName); 165 thePref._clearUserValue(); 166 } 167 168 /** @see nsIPrefBranch.prefHasUserValue */ 169 prefHasUserValue(prefName) { 170 const thePref = this._findPref(prefName); 171 return thePref._hasUserValue; 172 } 173 174 /** @see nsIPrefBranch.addObserver */ 175 addObserver(domain, observer, holdWeak) { 176 if (holdWeak) { 177 throw new Error("shim prefs only supports strong observers"); 178 } 179 180 if (!(domain in this._observers)) { 181 this._observers[domain] = []; 182 } 183 this._observers[domain].push(observer); 184 } 185 186 /** @see nsIPrefBranch.removeObserver */ 187 removeObserver(domain, observer) { 188 if (!(domain in this._observers)) { 189 return; 190 } 191 const index = this._observers[domain].indexOf(observer); 192 if (index >= 0) { 193 this._observers[domain].splice(index, 1); 194 } 195 } 196 197 /** @see nsIPrefService.savePrefFile */ 198 savePrefFile(file) { 199 if (file) { 200 throw new Error("shim prefs only supports null file in savePrefFile"); 201 } 202 // Nothing to do - this implementation always writes back. 203 } 204 205 /** @see nsIPrefService.getBranch */ 206 getBranch(prefRoot) { 207 if (!prefRoot) { 208 return this; 209 } 210 if (prefRoot.endsWith(".")) { 211 prefRoot = prefRoot.slice(0, -1); 212 } 213 // This is a bit weird since it could erroneously return a pref, 214 // not a pref branch. 215 return this._findPref(prefRoot); 216 } 217 218 /** 219 * Return this preference's current value. 220 * 221 * @return {Any} The current value of this preference. This may 222 * return a string, a number, or a boolean depending on the 223 * preference's type. 224 */ 225 _get() { 226 if (this._hasUserValue) { 227 return this._userValue; 228 } 229 return this._defaultValue; 230 } 231 232 /** 233 * Set the preference's value. The new value is assumed to be a 234 * user value. After setting the value, this function emits a 235 * change notification. 236 * 237 * @param {Any} value the new value 238 */ 239 _set(value) { 240 if (!this._hasUserValue || value !== this._userValue) { 241 this._userValue = value; 242 this._hasUserValue = true; 243 this._saveAndNotify(); 244 } 245 } 246 247 /** 248 * Set the default value for this preference, and emit a 249 * notification if this results in a visible change. 250 * 251 * @param {Any} value the new default value 252 */ 253 _setDefault(value) { 254 if (this._defaultValue !== value) { 255 this._defaultValue = value; 256 if (!this._hasUserValue) { 257 this._saveAndNotify(); 258 } 259 } 260 } 261 262 /** 263 * If this preference has a user value, clear it. If a change was 264 * made, emit a change notification. 265 */ 266 _clearUserValue() { 267 if (this._hasUserValue) { 268 this._userValue = null; 269 this._hasUserValue = false; 270 this._saveAndNotify(); 271 } 272 } 273 274 /** 275 * Helper function to write the preference's value to local storage 276 * and then emit a change notification. 277 */ 278 _saveAndNotify() { 279 const store = { 280 type: this._type, 281 defaultValue: this._defaultValue, 282 hasUserValue: this._hasUserValue, 283 userValue: this._userValue, 284 }; 285 286 localStorage.setItem(PREFIX + this._fullName, JSON.stringify(store)); 287 this._parent._notify(this._name); 288 } 289 290 /** 291 * Change this preference's value without writing it back to local 292 * storage. This is used to handle changes to local storage that 293 * were made externally. 294 * 295 * @param {number} type one of the PREF_* values 296 * @param {Any} userValue the user value to use if the pref does not exist 297 * @param {Any} defaultValue the default value to use if the pref 298 * does not exist 299 * @param {boolean} hasUserValue if a new pref is created, whether 300 * the default value is also a user value 301 * @param {object} store the new value of the preference. It should 302 * be of the form {type, defaultValue, hasUserValue, userValue}; 303 * where |type| is one of the PREF_* type constants; |defaultValue| 304 * and |userValue| are the default and user values, respectively; 305 * and |hasUserValue| is a boolean indicating whether the user value 306 * is valid 307 */ 308 _storageUpdated(type, userValue, hasUserValue, defaultValue) { 309 this._type = type; 310 this._defaultValue = defaultValue; 311 this._hasUserValue = hasUserValue; 312 this._userValue = userValue; 313 // There's no need to write this back to local storage, since it 314 // came from there; and this avoids infinite event loops. 315 this._parent._notify(this._name); 316 } 317 318 /** 319 * Helper function to find either a Preference or PrefBranch object 320 * given its name. If the name is not found, throws an exception. 321 * 322 * @param {string} prefName the fully-qualified preference name 323 * @return {object} Either a Preference or PrefBranch object 324 */ 325 _findPref(prefName) { 326 const branchNames = prefName.split("."); 327 let branch = this; 328 329 for (const branchName of branchNames) { 330 branch = branch._children[branchName]; 331 if (!branch) { 332 // throw new Error(`could not find pref branch ${ prefName}`); 333 return false; 334 } 335 } 336 337 return branch; 338 } 339 340 /** 341 * Helper function to notify any observers when a preference has 342 * changed. This will also notify the parent branch for further 343 * reporting. 344 * 345 * @param {string} relativeName the name of the updated pref, 346 * relative to this branch 347 */ 348 _notify(relativeName) { 349 for (const domain in this._observers) { 350 if ( 351 relativeName === domain || 352 domain === "" || 353 (domain.endsWith(".") && relativeName.startsWith(domain)) 354 ) { 355 // Allow mutation while walking. 356 const localList = this._observers[domain].slice(); 357 for (const observer of localList) { 358 try { 359 if ("observe" in observer) { 360 observer.observe( 361 this, 362 NS_PREFBRANCH_PREFCHANGE_TOPIC_ID, 363 relativeName 364 ); 365 } else { 366 // Function-style observer -- these aren't mentioned in 367 // the IDL, but they're accepted and devtools uses them. 368 observer(this, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID, relativeName); 369 } 370 } catch (e) { 371 console.error(e); 372 } 373 } 374 } 375 } 376 377 if (this._parent) { 378 this._parent._notify(`${this._name}.${relativeName}`); 379 } 380 } 381 382 /** 383 * Helper function to create a branch given an array of branch names 384 * representing the path of the new branch. 385 * 386 * @param {Array} branchList an array of strings, one per component 387 * of the branch to be created 388 * @return {PrefBranch} the new branch 389 */ 390 _createBranch(branchList) { 391 let parent = this; 392 for (const branch of branchList) { 393 if (!parent._children[branch]) { 394 const isParentRoot = !parent._parent; 395 const branchName = (isParentRoot ? "" : `${parent.root}.`) + branch; 396 parent._children[branch] = new PrefBranch(parent, branch, branchName); 397 } 398 parent = parent._children[branch]; 399 } 400 return parent; 401 } 402 403 /** 404 * Create a new preference. The new preference is assumed to be in 405 * local storage already, and the new value is taken from there. 406 * 407 * @param {string} keyName the full-qualified name of the preference. 408 * This is also the name of the key in local storage. 409 * @param {Any} userValue the user value to use if the pref does not exist 410 * @param {boolean} hasUserValue if a new pref is created, whether 411 * the default value is also a user value 412 * @param {Any} defaultValue the default value to use if the pref 413 * does not exist 414 * @param {boolean} init if true, then this call is initialization 415 * from local storage and should override the default prefs 416 */ 417 _findOrCreatePref( 418 keyName, 419 userValue, 420 hasUserValue, 421 defaultValue, 422 init = false 423 ) { 424 const branch = this._createBranch(keyName.split(".")); 425 426 if (hasUserValue && typeof userValue !== typeof defaultValue) { 427 throw new Error(`inconsistent values when creating ${keyName}`); 428 } 429 430 let type; 431 switch (typeof defaultValue) { 432 case "boolean": 433 type = PREF_BOOL; 434 break; 435 case "number": 436 type = PREF_INT; 437 break; 438 case "string": 439 type = PREF_STRING; 440 break; 441 default: 442 throw new Error(`unhandled argument type: ${typeof defaultValue}`); 443 } 444 445 if (init || branch._type === PREF_INVALID) { 446 branch._storageUpdated(type, userValue, hasUserValue, defaultValue); 447 } else if (branch._type !== type) { 448 throw new Error(`attempt to change type of pref ${keyName}`); 449 } 450 451 return branch; 452 } 453 454 getKeyName(keyName) { 455 if (keyName.startsWith(PREFIX)) { 456 return keyName.slice(PREFIX.length); 457 } 458 459 return keyName; 460 } 461 462 /** 463 * Helper function that is called when local storage changes. This 464 * updates the preferences and notifies pref observers as needed. 465 * 466 * @param {StorageEvent} event the event representing the local 467 * storage change 468 */ 469 _onStorageChange(event) { 470 if (event.storageArea !== localStorage) { 471 return; 472 } 473 474 const key = this.getKeyName(event.key); 475 476 // Ignore delete events. Not clear what's correct. 477 if (key === null || event.newValue === null) { 478 return; 479 } 480 481 const { type, userValue, hasUserValue, defaultValue } = JSON.parse( 482 event.newValue 483 ); 484 if (event.oldValue === null) { 485 this._findOrCreatePref(key, userValue, hasUserValue, defaultValue); 486 } else { 487 const thePref = this._findPref(key); 488 thePref._storageUpdated(type, userValue, hasUserValue, defaultValue); 489 } 490 } 491 492 /** 493 * Helper function to initialize the root PrefBranch. 494 */ 495 _initializeRoot() { 496 if (Services._defaultPrefsEnabled) { 497 /* eslint-disable no-eval */ 498 // let devtools = require("raw!prefs!devtools/client/preferences/devtools"); 499 // eval(devtools); 500 // let all = require("raw!prefs!modules/libpref/init/all"); 501 // eval(all); 502 /* eslint-enable no-eval */ 503 } 504 505 // Read the prefs from local storage and create the local 506 // representations. 507 for (let i = 0; i < localStorage.length; ++i) { 508 const keyName = localStorage.key(i); 509 if (keyName.startsWith(PREFIX)) { 510 const { userValue, hasUserValue, defaultValue } = JSON.parse( 511 localStorage.getItem(keyName) 512 ); 513 this._findOrCreatePref( 514 keyName.slice(PREFIX.length), 515 userValue, 516 hasUserValue, 517 defaultValue, 518 true 519 ); 520 } 521 } 522 523 this._onStorageChange = this._onStorageChange.bind(this); 524 window.addEventListener("storage", this._onStorageChange); 525 } 526 } 527 528 const Services = { 529 _prefs: null, 530 531 _defaultPrefsEnabled: true, 532 533 get prefs() { 534 if (!this._prefs) { 535 this._prefs = new PrefBranch(null, "", ""); 536 this._prefs._initializeRoot(); 537 } 538 return this._prefs; 539 }, 540 541 appinfo: "", 542 locale: { 543 appLocalesAsLangTags: ["en-US", "en"], 544 }, 545 obs: { addObserver: () => {} }, 546 strings: { 547 createBundle() { 548 return { 549 GetStringFromName() { 550 return "NodeTest"; 551 }, 552 }; 553 }, 554 }, 555 intl: { 556 stringHasRTLChars: () => false, 557 }, 558 }; 559 560 function pref(name, value) { 561 // eslint-disable-next-line mozilla/valid-services-property 562 const thePref = Services.prefs._findOrCreatePref(name, value, true, value); 563 thePref._setDefault(value); 564 } 565 566 module.exports = Services; 567 Services.pref = pref; 568 Services.uuid = { generateUUID: () => {} }; 569 Services.dns = {};