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 file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); 6const { 7 ContentPref, 8 cbHandleCompletion, 9 cbHandleError, 10 cbHandleResult, 11} = ChromeUtils.import("resource://gre/modules/ContentPrefUtils.jsm"); 12const { ContentPrefStore } = ChromeUtils.import( 13 "resource://gre/modules/ContentPrefStore.jsm" 14); 15ChromeUtils.defineModuleGetter( 16 this, 17 "Sqlite", 18 "resource://gre/modules/Sqlite.jsm" 19); 20 21const CACHE_MAX_GROUP_ENTRIES = 100; 22 23const GROUP_CLAUSE = ` 24 SELECT id 25 FROM groups 26 WHERE name = :group OR 27 (:includeSubdomains AND name LIKE :pattern ESCAPE '/') 28`; 29 30function ContentPrefService2() { 31 if (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_CONTENT) { 32 return ChromeUtils.import( 33 "resource://gre/modules/ContentPrefServiceChild.jsm" 34 ).ContentPrefServiceChild; 35 } 36 37 Services.obs.addObserver(this, "last-pb-context-exited"); 38 39 // Observe shutdown so we can shut down the database connection. 40 Services.obs.addObserver(this, "profile-before-change"); 41} 42 43const cache = new ContentPrefStore(); 44cache.set = function CPS_cache_set(group, name, val) { 45 Object.getPrototypeOf(this).set.apply(this, arguments); 46 let groupCount = this._groups.size; 47 if (groupCount >= CACHE_MAX_GROUP_ENTRIES) { 48 // Clean half of the entries 49 for (let [group, name] of this) { 50 this.remove(group, name); 51 groupCount--; 52 if (groupCount < CACHE_MAX_GROUP_ENTRIES / 2) { 53 break; 54 } 55 } 56 } 57}; 58 59const privModeStorage = new ContentPrefStore(); 60 61function executeStatementsInTransaction(conn, stmts) { 62 return conn.executeTransaction(async () => { 63 let rows = []; 64 for (let { sql, params, cachable } of stmts) { 65 let execute = cachable ? conn.executeCached : conn.execute; 66 let stmtRows = await execute.call(conn, sql, params); 67 rows = rows.concat(stmtRows); 68 } 69 return rows; 70 }); 71} 72 73function HostnameGrouper_group(aURI) { 74 var group; 75 76 try { 77 // Accessing the host property of the URI will throw an exception 78 // if the URI is of a type that doesn't have a host property. 79 // Otherwise, we manually throw an exception if the host is empty, 80 // since the effect is the same (we can't derive a group from it). 81 82 group = aURI.host; 83 if (!group) { 84 throw new Error("can't derive group from host; no host in URI"); 85 } 86 } catch (ex) { 87 // If we don't have a host, then use the entire URI (minus the query, 88 // reference, and hash, if possible) as the group. This means that URIs 89 // like about:mozilla and about:blank will be considered separate groups, 90 // but at least they'll be grouped somehow. 91 92 // This also means that each individual file: URL will be considered 93 // its own group. This seems suboptimal, but so does treating the entire 94 // file: URL space as a single group (especially if folks start setting 95 // group-specific capabilities prefs). 96 97 // XXX Is there something better we can do here? 98 99 try { 100 var url = aURI.QueryInterface(Ci.nsIURL); 101 group = aURI.prePath + url.filePath; 102 } catch (ex) { 103 group = aURI.spec; 104 } 105 } 106 107 return group; 108} 109 110ContentPrefService2.prototype = { 111 // XPCOM Plumbing 112 113 classID: Components.ID("{e3f772f3-023f-4b32-b074-36cf0fd5d414}"), 114 115 // Destruction 116 117 _destroy: function CPS2__destroy() { 118 Services.obs.removeObserver(this, "profile-before-change"); 119 Services.obs.removeObserver(this, "last-pb-context-exited"); 120 121 // Delete references to XPCOM components to make sure we don't leak them 122 // (although we haven't observed leakage in tests). Also delete references 123 // in _observers and _genericObservers to avoid cycles with those that 124 // refer to us and don't remove themselves from those observer pools. 125 delete this._observers; 126 delete this._genericObservers; 127 }, 128 129 // in-memory cache and private-browsing stores 130 131 _cache: cache, 132 _pbStore: privModeStorage, 133 134 _connPromise: null, 135 136 get conn() { 137 if (this._connPromise) { 138 return this._connPromise; 139 } 140 141 return (this._connPromise = (async () => { 142 let conn; 143 try { 144 conn = await this._getConnection(); 145 } catch (e) { 146 this.log("Failed to establish database connection: " + e); 147 throw e; 148 } 149 return conn; 150 })()); 151 }, 152 153 // nsIContentPrefService 154 155 getByName: function CPS2_getByName(name, context, callback) { 156 checkNameArg(name); 157 checkCallbackArg(callback, true); 158 159 // Some prefs may be in both the database and the private browsing store. 160 // Notify the caller of such prefs only once, using the values from private 161 // browsing. 162 let pbPrefs = new ContentPrefStore(); 163 if (context && context.usePrivateBrowsing) { 164 for (let [sgroup, sname, val] of this._pbStore) { 165 if (sname == name) { 166 pbPrefs.set(sgroup, sname, val); 167 } 168 } 169 } 170 171 let stmt1 = this._stmt(` 172 SELECT groups.name AS grp, prefs.value AS value 173 FROM prefs 174 JOIN settings ON settings.id = prefs.settingID 175 JOIN groups ON groups.id = prefs.groupID 176 WHERE settings.name = :name 177 `); 178 stmt1.params.name = name; 179 180 let stmt2 = this._stmt(` 181 SELECT NULL AS grp, prefs.value AS value 182 FROM prefs 183 JOIN settings ON settings.id = prefs.settingID 184 WHERE settings.name = :name AND prefs.groupID ISNULL 185 `); 186 stmt2.params.name = name; 187 188 this._execStmts([stmt1, stmt2], { 189 onRow: row => { 190 let grp = row.getResultByName("grp"); 191 let val = row.getResultByName("value"); 192 this._cache.set(grp, name, val); 193 if (!pbPrefs.has(grp, name)) { 194 cbHandleResult(callback, new ContentPref(grp, name, val)); 195 } 196 }, 197 onDone: (reason, ok, gotRow) => { 198 if (ok) { 199 for (let [pbGroup, pbName, pbVal] of pbPrefs) { 200 cbHandleResult(callback, new ContentPref(pbGroup, pbName, pbVal)); 201 } 202 } 203 cbHandleCompletion(callback, reason); 204 }, 205 onError: nsresult => { 206 cbHandleError(callback, nsresult); 207 }, 208 }); 209 }, 210 211 getByDomainAndName: function CPS2_getByDomainAndName( 212 group, 213 name, 214 context, 215 callback 216 ) { 217 checkGroupArg(group); 218 this._get(group, name, false, context, callback); 219 }, 220 221 getBySubdomainAndName: function CPS2_getBySubdomainAndName( 222 group, 223 name, 224 context, 225 callback 226 ) { 227 checkGroupArg(group); 228 this._get(group, name, true, context, callback); 229 }, 230 231 getGlobal: function CPS2_getGlobal(name, context, callback) { 232 this._get(null, name, false, context, callback); 233 }, 234 235 _get: function CPS2__get(group, name, includeSubdomains, context, callback) { 236 group = this._parseGroup(group); 237 checkNameArg(name); 238 checkCallbackArg(callback, true); 239 240 // Some prefs may be in both the database and the private browsing store. 241 // Notify the caller of such prefs only once, using the values from private 242 // browsing. 243 let pbPrefs = new ContentPrefStore(); 244 if (context && context.usePrivateBrowsing) { 245 for (let [sgroup, val] of this._pbStore.match( 246 group, 247 name, 248 includeSubdomains 249 )) { 250 pbPrefs.set(sgroup, name, val); 251 } 252 } 253 254 this._execStmts([this._commonGetStmt(group, name, includeSubdomains)], { 255 onRow: row => { 256 let grp = row.getResultByName("grp"); 257 let val = row.getResultByName("value"); 258 this._cache.set(grp, name, val); 259 if (!pbPrefs.has(group, name)) { 260 cbHandleResult(callback, new ContentPref(grp, name, val)); 261 } 262 }, 263 onDone: (reason, ok, gotRow) => { 264 if (ok) { 265 if (!gotRow) { 266 this._cache.set(group, name, undefined); 267 } 268 for (let [pbGroup, pbName, pbVal] of pbPrefs) { 269 cbHandleResult(callback, new ContentPref(pbGroup, pbName, pbVal)); 270 } 271 } 272 cbHandleCompletion(callback, reason); 273 }, 274 onError: nsresult => { 275 cbHandleError(callback, nsresult); 276 }, 277 }); 278 }, 279 280 _commonGetStmt: function CPS2__commonGetStmt(group, name, includeSubdomains) { 281 let stmt = group 282 ? this._stmtWithGroupClause( 283 group, 284 includeSubdomains, 285 ` 286 SELECT groups.name AS grp, prefs.value AS value 287 FROM prefs 288 JOIN settings ON settings.id = prefs.settingID 289 JOIN groups ON groups.id = prefs.groupID 290 WHERE settings.name = :name AND prefs.groupID IN (${GROUP_CLAUSE}) 291 ` 292 ) 293 : this._stmt(` 294 SELECT NULL AS grp, prefs.value AS value 295 FROM prefs 296 JOIN settings ON settings.id = prefs.settingID 297 WHERE settings.name = :name AND prefs.groupID ISNULL 298 `); 299 stmt.params.name = name; 300 return stmt; 301 }, 302 303 _stmtWithGroupClause: function CPS2__stmtWithGroupClause( 304 group, 305 includeSubdomains, 306 sql 307 ) { 308 let stmt = this._stmt(sql, false); 309 stmt.params.group = group; 310 stmt.params.includeSubdomains = includeSubdomains || false; 311 stmt.params.pattern = 312 "%." + (group == null ? null : group.replace(/\/|%|_/g, "/$&")); 313 return stmt; 314 }, 315 316 getCachedByDomainAndName: function CPS2_getCachedByDomainAndName( 317 group, 318 name, 319 context 320 ) { 321 checkGroupArg(group); 322 let prefs = this._getCached(group, name, false, context); 323 return prefs[0] || null; 324 }, 325 326 getCachedBySubdomainAndName: function CPS2_getCachedBySubdomainAndName( 327 group, 328 name, 329 context 330 ) { 331 checkGroupArg(group); 332 return this._getCached(group, name, true, context); 333 }, 334 335 getCachedGlobal: function CPS2_getCachedGlobal(name, context) { 336 let prefs = this._getCached(null, name, false, context); 337 return prefs[0] || null; 338 }, 339 340 _getCached: function CPS2__getCached( 341 group, 342 name, 343 includeSubdomains, 344 context 345 ) { 346 group = this._parseGroup(group); 347 checkNameArg(name); 348 349 let storesToCheck = [this._cache]; 350 if (context && context.usePrivateBrowsing) { 351 storesToCheck.push(this._pbStore); 352 } 353 354 let outStore = new ContentPrefStore(); 355 storesToCheck.forEach(function(store) { 356 for (let [sgroup, val] of store.match(group, name, includeSubdomains)) { 357 outStore.set(sgroup, name, val); 358 } 359 }); 360 361 let prefs = []; 362 for (let [sgroup, sname, val] of outStore) { 363 prefs.push(new ContentPref(sgroup, sname, val)); 364 } 365 return prefs; 366 }, 367 368 set: function CPS2_set(group, name, value, context, callback) { 369 checkGroupArg(group); 370 this._set(group, name, value, context, callback); 371 }, 372 373 setGlobal: function CPS2_setGlobal(name, value, context, callback) { 374 this._set(null, name, value, context, callback); 375 }, 376 377 _set: function CPS2__set(group, name, value, context, callback) { 378 group = this._parseGroup(group); 379 checkNameArg(name); 380 checkValueArg(value); 381 checkCallbackArg(callback, false); 382 383 if (context && context.usePrivateBrowsing) { 384 this._pbStore.set(group, name, value); 385 this._schedule(function() { 386 cbHandleCompletion(callback, Ci.nsIContentPrefCallback2.COMPLETE_OK); 387 this._notifyPrefSet(group, name, value, context.usePrivateBrowsing); 388 }); 389 return; 390 } 391 392 // Invalidate the cached value so consumers accessing the cache between now 393 // and when the operation finishes don't get old data. 394 this._cache.remove(group, name); 395 396 let stmts = []; 397 398 // Create the setting if it doesn't exist. 399 let stmt = this._stmt(` 400 INSERT OR IGNORE INTO settings (id, name) 401 VALUES((SELECT id FROM settings WHERE name = :name), :name) 402 `); 403 stmt.params.name = name; 404 stmts.push(stmt); 405 406 // Create the group if it doesn't exist. 407 if (group) { 408 stmt = this._stmt(` 409 INSERT OR IGNORE INTO groups (id, name) 410 VALUES((SELECT id FROM groups WHERE name = :group), :group) 411 `); 412 stmt.params.group = group; 413 stmts.push(stmt); 414 } 415 416 // Finally create or update the pref. 417 if (group) { 418 stmt = this._stmt(` 419 INSERT OR REPLACE INTO prefs (id, groupID, settingID, value, timestamp) 420 VALUES( 421 (SELECT prefs.id 422 FROM prefs 423 JOIN groups ON groups.id = prefs.groupID 424 JOIN settings ON settings.id = prefs.settingID 425 WHERE groups.name = :group AND settings.name = :name), 426 (SELECT id FROM groups WHERE name = :group), 427 (SELECT id FROM settings WHERE name = :name), 428 :value, 429 :now 430 ) 431 `); 432 stmt.params.group = group; 433 } else { 434 stmt = this._stmt(` 435 INSERT OR REPLACE INTO prefs (id, groupID, settingID, value, timestamp) 436 VALUES( 437 (SELECT prefs.id 438 FROM prefs 439 JOIN settings ON settings.id = prefs.settingID 440 WHERE prefs.groupID IS NULL AND settings.name = :name), 441 NULL, 442 (SELECT id FROM settings WHERE name = :name), 443 :value, 444 :now 445 ) 446 `); 447 } 448 stmt.params.name = name; 449 stmt.params.value = value; 450 stmt.params.now = Date.now() / 1000; 451 stmts.push(stmt); 452 453 this._execStmts(stmts, { 454 onDone: (reason, ok) => { 455 if (ok) { 456 this._cache.setWithCast(group, name, value); 457 } 458 cbHandleCompletion(callback, reason); 459 if (ok) { 460 this._notifyPrefSet( 461 group, 462 name, 463 value, 464 context && context.usePrivateBrowsing 465 ); 466 } 467 }, 468 onError: nsresult => { 469 cbHandleError(callback, nsresult); 470 }, 471 }); 472 }, 473 474 removeByDomainAndName: function CPS2_removeByDomainAndName( 475 group, 476 name, 477 context, 478 callback 479 ) { 480 checkGroupArg(group); 481 this._remove(group, name, false, context, callback); 482 }, 483 484 removeBySubdomainAndName: function CPS2_removeBySubdomainAndName( 485 group, 486 name, 487 context, 488 callback 489 ) { 490 checkGroupArg(group); 491 this._remove(group, name, true, context, callback); 492 }, 493 494 removeGlobal: function CPS2_removeGlobal(name, context, callback) { 495 this._remove(null, name, false, context, callback); 496 }, 497 498 _remove: function CPS2__remove( 499 group, 500 name, 501 includeSubdomains, 502 context, 503 callback 504 ) { 505 group = this._parseGroup(group); 506 checkNameArg(name); 507 checkCallbackArg(callback, false); 508 509 // Invalidate the cached values so consumers accessing the cache between now 510 // and when the operation finishes don't get old data. 511 for (let sgroup of this._cache.matchGroups(group, includeSubdomains)) { 512 this._cache.remove(sgroup, name); 513 } 514 515 let stmts = []; 516 517 // First get the matching prefs. 518 stmts.push(this._commonGetStmt(group, name, includeSubdomains)); 519 520 // Delete the matching prefs. 521 let stmt = this._stmtWithGroupClause( 522 group, 523 includeSubdomains, 524 ` 525 DELETE FROM prefs 526 WHERE settingID = (SELECT id FROM settings WHERE name = :name) AND 527 CASE typeof(:group) 528 WHEN 'null' THEN prefs.groupID IS NULL 529 ELSE prefs.groupID IN (${GROUP_CLAUSE}) 530 END 531 ` 532 ); 533 stmt.params.name = name; 534 stmts.push(stmt); 535 536 stmts = stmts.concat(this._settingsAndGroupsCleanupStmts()); 537 538 let prefs = new ContentPrefStore(); 539 540 let isPrivate = context && context.usePrivateBrowsing; 541 this._execStmts(stmts, { 542 onRow: row => { 543 let grp = row.getResultByName("grp"); 544 prefs.set(grp, name, undefined); 545 this._cache.set(grp, name, undefined); 546 }, 547 onDone: (reason, ok) => { 548 if (ok) { 549 this._cache.set(group, name, undefined); 550 if (isPrivate) { 551 for (let [sgroup] of this._pbStore.match( 552 group, 553 name, 554 includeSubdomains 555 )) { 556 prefs.set(sgroup, name, undefined); 557 this._pbStore.remove(sgroup, name); 558 } 559 } 560 } 561 cbHandleCompletion(callback, reason); 562 if (ok) { 563 for (let [sgroup, ,] of prefs) { 564 this._notifyPrefRemoved(sgroup, name, isPrivate); 565 } 566 } 567 }, 568 onError: nsresult => { 569 cbHandleError(callback, nsresult); 570 }, 571 }); 572 }, 573 574 // Deletes settings and groups that are no longer used. 575 _settingsAndGroupsCleanupStmts() { 576 // The NOTNULL term in the subquery of the second statment is needed because of 577 // SQLite's weird IN behavior vis-a-vis NULLs. See http://sqlite.org/lang_expr.html. 578 return [ 579 this._stmt(` 580 DELETE FROM settings 581 WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs) 582 `), 583 this._stmt(` 584 DELETE FROM groups WHERE id NOT IN ( 585 SELECT DISTINCT groupID FROM prefs WHERE groupID NOTNULL 586 ) 587 `), 588 ]; 589 }, 590 591 removeByDomain: function CPS2_removeByDomain(group, context, callback) { 592 checkGroupArg(group); 593 this._removeByDomain(group, false, context, callback); 594 }, 595 596 removeBySubdomain: function CPS2_removeBySubdomain(group, context, callback) { 597 checkGroupArg(group); 598 this._removeByDomain(group, true, context, callback); 599 }, 600 601 removeAllGlobals: function CPS2_removeAllGlobals(context, callback) { 602 this._removeByDomain(null, false, context, callback); 603 }, 604 605 _removeByDomain: function CPS2__removeByDomain( 606 group, 607 includeSubdomains, 608 context, 609 callback 610 ) { 611 group = this._parseGroup(group); 612 checkCallbackArg(callback, false); 613 614 // Invalidate the cached values so consumers accessing the cache between now 615 // and when the operation finishes don't get old data. 616 for (let sgroup of this._cache.matchGroups(group, includeSubdomains)) { 617 this._cache.removeGroup(sgroup); 618 } 619 620 let stmts = []; 621 622 // First get the matching prefs, then delete groups and prefs that reference 623 // deleted groups. 624 if (group) { 625 stmts.push( 626 this._stmtWithGroupClause( 627 group, 628 includeSubdomains, 629 ` 630 SELECT groups.name AS grp, settings.name AS name 631 FROM prefs 632 JOIN settings ON settings.id = prefs.settingID 633 JOIN groups ON groups.id = prefs.groupID 634 WHERE prefs.groupID IN (${GROUP_CLAUSE}) 635 ` 636 ) 637 ); 638 stmts.push( 639 this._stmtWithGroupClause( 640 group, 641 includeSubdomains, 642 `DELETE FROM groups WHERE id IN (${GROUP_CLAUSE})` 643 ) 644 ); 645 stmts.push( 646 this._stmt(` 647 DELETE FROM prefs 648 WHERE groupID NOTNULL AND groupID NOT IN (SELECT id FROM groups) 649 `) 650 ); 651 } else { 652 stmts.push( 653 this._stmt(` 654 SELECT NULL AS grp, settings.name AS name 655 FROM prefs 656 JOIN settings ON settings.id = prefs.settingID 657 WHERE prefs.groupID IS NULL 658 `) 659 ); 660 stmts.push(this._stmt("DELETE FROM prefs WHERE groupID IS NULL")); 661 } 662 663 // Finally delete settings that are no longer referenced. 664 stmts.push( 665 this._stmt(` 666 DELETE FROM settings 667 WHERE id NOT IN (SELECT DISTINCT settingID FROM prefs) 668 `) 669 ); 670 671 let prefs = new ContentPrefStore(); 672 673 let isPrivate = context && context.usePrivateBrowsing; 674 this._execStmts(stmts, { 675 onRow: row => { 676 let grp = row.getResultByName("grp"); 677 let name = row.getResultByName("name"); 678 prefs.set(grp, name, undefined); 679 this._cache.set(grp, name, undefined); 680 }, 681 onDone: (reason, ok) => { 682 if (ok && isPrivate) { 683 for (let [sgroup, sname] of this._pbStore) { 684 if ( 685 !group || 686 (!includeSubdomains && group == sgroup) || 687 (includeSubdomains && 688 sgroup && 689 this._pbStore.groupsMatchIncludingSubdomains(group, sgroup)) 690 ) { 691 prefs.set(sgroup, sname, undefined); 692 this._pbStore.remove(sgroup, sname); 693 } 694 } 695 } 696 cbHandleCompletion(callback, reason); 697 if (ok) { 698 for (let [sgroup, sname] of prefs) { 699 this._notifyPrefRemoved(sgroup, sname, isPrivate); 700 } 701 } 702 }, 703 onError: nsresult => { 704 cbHandleError(callback, nsresult); 705 }, 706 }); 707 }, 708 709 _removeAllDomainsSince: function CPS2__removeAllDomainsSince( 710 since, 711 context, 712 callback 713 ) { 714 checkCallbackArg(callback, false); 715 716 since /= 1000; 717 718 // Invalidate the cached values so consumers accessing the cache between now 719 // and when the operation finishes don't get old data. 720 // Invalidate all the group cache because we don't know which groups will be removed. 721 this._cache.removeAllGroups(); 722 723 let stmts = []; 724 725 // Get prefs that are about to be removed to notify about their removal. 726 let stmt = this._stmt(` 727 SELECT groups.name AS grp, settings.name AS name 728 FROM prefs 729 JOIN settings ON settings.id = prefs.settingID 730 JOIN groups ON groups.id = prefs.groupID 731 WHERE timestamp >= :since 732 `); 733 stmt.params.since = since; 734 stmts.push(stmt); 735 736 // Do the actual remove. 737 stmt = this._stmt(` 738 DELETE FROM prefs WHERE groupID NOTNULL AND timestamp >= :since 739 `); 740 stmt.params.since = since; 741 stmts.push(stmt); 742 743 // Cleanup no longer used values. 744 stmts = stmts.concat(this._settingsAndGroupsCleanupStmts()); 745 746 let prefs = new ContentPrefStore(); 747 let isPrivate = context && context.usePrivateBrowsing; 748 this._execStmts(stmts, { 749 onRow: row => { 750 let grp = row.getResultByName("grp"); 751 let name = row.getResultByName("name"); 752 prefs.set(grp, name, undefined); 753 this._cache.set(grp, name, undefined); 754 }, 755 onDone: (reason, ok) => { 756 // This nukes all the groups in _pbStore since we don't have their timestamp 757 // information. 758 if (ok && isPrivate) { 759 for (let [sgroup, sname] of this._pbStore) { 760 if (sgroup) { 761 prefs.set(sgroup, sname, undefined); 762 } 763 } 764 this._pbStore.removeAllGroups(); 765 } 766 cbHandleCompletion(callback, reason); 767 if (ok) { 768 for (let [sgroup, sname] of prefs) { 769 this._notifyPrefRemoved(sgroup, sname, isPrivate); 770 } 771 } 772 }, 773 onError: nsresult => { 774 cbHandleError(callback, nsresult); 775 }, 776 }); 777 }, 778 779 removeAllDomainsSince: function CPS2_removeAllDomainsSince( 780 since, 781 context, 782 callback 783 ) { 784 this._removeAllDomainsSince(since, context, callback); 785 }, 786 787 removeAllDomains: function CPS2_removeAllDomains(context, callback) { 788 this._removeAllDomainsSince(0, context, callback); 789 }, 790 791 removeByName: function CPS2_removeByName(name, context, callback) { 792 checkNameArg(name); 793 checkCallbackArg(callback, false); 794 795 // Invalidate the cached values so consumers accessing the cache between now 796 // and when the operation finishes don't get old data. 797 for (let [group, sname] of this._cache) { 798 if (sname == name) { 799 this._cache.remove(group, name); 800 } 801 } 802 803 let stmts = []; 804 805 // First get the matching prefs. Include null if any of those prefs are 806 // global. 807 let stmt = this._stmt(` 808 SELECT groups.name AS grp 809 FROM prefs 810 JOIN settings ON settings.id = prefs.settingID 811 JOIN groups ON groups.id = prefs.groupID 812 WHERE settings.name = :name 813 UNION 814 SELECT NULL AS grp 815 WHERE EXISTS ( 816 SELECT prefs.id 817 FROM prefs 818 JOIN settings ON settings.id = prefs.settingID 819 WHERE settings.name = :name AND prefs.groupID IS NULL 820 ) 821 `); 822 stmt.params.name = name; 823 stmts.push(stmt); 824 825 // Delete the target settings. 826 stmt = this._stmt("DELETE FROM settings WHERE name = :name"); 827 stmt.params.name = name; 828 stmts.push(stmt); 829 830 // Delete prefs and groups that are no longer used. 831 stmts.push( 832 this._stmt( 833 "DELETE FROM prefs WHERE settingID NOT IN (SELECT id FROM settings)" 834 ) 835 ); 836 stmts.push( 837 this._stmt(` 838 DELETE FROM groups WHERE id NOT IN ( 839 SELECT DISTINCT groupID FROM prefs WHERE groupID NOTNULL 840 ) 841 `) 842 ); 843 844 let prefs = new ContentPrefStore(); 845 let isPrivate = context && context.usePrivateBrowsing; 846 847 this._execStmts(stmts, { 848 onRow: row => { 849 let grp = row.getResultByName("grp"); 850 prefs.set(grp, name, undefined); 851 this._cache.set(grp, name, undefined); 852 }, 853 onDone: (reason, ok) => { 854 if (ok && isPrivate) { 855 for (let [sgroup, sname] of this._pbStore) { 856 if (sname === name) { 857 prefs.set(sgroup, name, undefined); 858 this._pbStore.remove(sgroup, name); 859 } 860 } 861 } 862 cbHandleCompletion(callback, reason); 863 if (ok) { 864 for (let [sgroup, ,] of prefs) { 865 this._notifyPrefRemoved(sgroup, name, isPrivate); 866 } 867 } 868 }, 869 onError: nsresult => { 870 cbHandleError(callback, nsresult); 871 }, 872 }); 873 }, 874 875 /** 876 * Returns the cached mozIStorageAsyncStatement for the given SQL. If no such 877 * statement is cached, one is created and cached. 878 * 879 * @param sql The SQL query string. 880 * @return The cached, possibly new, statement. 881 */ 882 _stmt: function CPS2__stmt(sql, cachable = true) { 883 return { 884 sql, 885 cachable, 886 params: {}, 887 }; 888 }, 889 890 /** 891 * Executes some async statements. 892 * 893 * @param stmts An array of mozIStorageAsyncStatements. 894 * @param callbacks An object with the following methods: 895 * onRow(row) (optional) 896 * Called once for each result row. 897 * row: A mozIStorageRow. 898 * onDone(reason, reasonOK, didGetRow) (required) 899 * Called when done. 900 * reason: A nsIContentPrefService2.COMPLETE_* value. 901 * reasonOK: reason == nsIContentPrefService2.COMPLETE_OK. 902 * didGetRow: True if onRow was ever called. 903 * onError(nsresult) (optional) 904 * Called on error. 905 * nsresult: The error code. 906 */ 907 _execStmts: async function CPS2__execStmts(stmts, callbacks) { 908 let conn = await this.conn; 909 let rows; 910 let ok = true; 911 try { 912 rows = await executeStatementsInTransaction(conn, stmts); 913 } catch (e) { 914 ok = false; 915 if (callbacks.onError) { 916 try { 917 callbacks.onError(e); 918 } catch (e) { 919 Cu.reportError(e); 920 } 921 } else { 922 Cu.reportError(e); 923 } 924 } 925 926 if (rows && callbacks.onRow) { 927 for (let row of rows) { 928 try { 929 callbacks.onRow(row); 930 } catch (e) { 931 Cu.reportError(e); 932 } 933 } 934 } 935 936 try { 937 callbacks.onDone( 938 ok 939 ? Ci.nsIContentPrefCallback2.COMPLETE_OK 940 : Ci.nsIContentPrefCallback2.COMPLETE_ERROR, 941 ok, 942 rows && !!rows.length 943 ); 944 } catch (e) { 945 Cu.reportError(e); 946 } 947 }, 948 949 /** 950 * Parses the domain (the "group", to use the database's term) from the given 951 * string. 952 * 953 * @param groupStr Assumed to be either a string or falsey. 954 * @return If groupStr is a valid URL string, returns the domain of 955 * that URL. If groupStr is some other nonempty string, 956 * returns groupStr itself. Otherwise returns null. 957 */ 958 _parseGroup: function CPS2__parseGroup(groupStr) { 959 if (!groupStr) { 960 return null; 961 } 962 try { 963 var groupURI = Services.io.newURI(groupStr); 964 } catch (err) { 965 return groupStr; 966 } 967 return HostnameGrouper_group(groupURI); 968 }, 969 970 _schedule: function CPS2__schedule(fn) { 971 Services.tm.dispatchToMainThread(fn.bind(this)); 972 }, 973 974 // A hash of arrays of observers, indexed by setting name. 975 _observers: {}, 976 977 // An array of generic observers, which observe all settings. 978 _genericObservers: [], 979 980 addObserverForName: function CPS2_addObserverForName(aName, aObserver) { 981 var observers; 982 if (aName) { 983 if (!this._observers[aName]) { 984 this._observers[aName] = []; 985 } 986 observers = this._observers[aName]; 987 } else { 988 observers = this._genericObservers; 989 } 990 991 if (!observers.includes(aObserver)) { 992 observers.push(aObserver); 993 } 994 }, 995 996 removeObserverForName: function CPS2_removeObserverForName(aName, aObserver) { 997 var observers; 998 if (aName) { 999 if (!this._observers[aName]) { 1000 return; 1001 } 1002 observers = this._observers[aName]; 1003 } else { 1004 observers = this._genericObservers; 1005 } 1006 1007 if (observers.includes(aObserver)) { 1008 observers.splice(observers.indexOf(aObserver), 1); 1009 } 1010 }, 1011 1012 /** 1013 * Construct a list of observers to notify about a change to some setting, 1014 * putting setting-specific observers before before generic ones, so observers 1015 * that initialize individual settings (like the page style controller) 1016 * execute before observers that display multiple settings and depend on them 1017 * being initialized first (like the content prefs sidebar). 1018 */ 1019 _getObservers: function ContentPrefService__getObservers(aName) { 1020 var observers = []; 1021 1022 if (aName && this._observers[aName]) { 1023 observers = observers.concat(this._observers[aName]); 1024 } 1025 observers = observers.concat(this._genericObservers); 1026 1027 return observers; 1028 }, 1029 1030 /** 1031 * Notify all observers about the removal of a preference. 1032 */ 1033 _notifyPrefRemoved: function ContentPrefService__notifyPrefRemoved( 1034 aGroup, 1035 aName, 1036 aIsPrivate 1037 ) { 1038 for (var observer of this._getObservers(aName)) { 1039 try { 1040 observer.onContentPrefRemoved(aGroup, aName, aIsPrivate); 1041 } catch (ex) { 1042 Cu.reportError(ex); 1043 } 1044 } 1045 }, 1046 1047 /** 1048 * Notify all observers about a preference change. 1049 */ 1050 _notifyPrefSet: function ContentPrefService__notifyPrefSet( 1051 aGroup, 1052 aName, 1053 aValue, 1054 aIsPrivate 1055 ) { 1056 for (var observer of this._getObservers(aName)) { 1057 try { 1058 observer.onContentPrefSet(aGroup, aName, aValue, aIsPrivate); 1059 } catch (ex) { 1060 Cu.reportError(ex); 1061 } 1062 } 1063 }, 1064 1065 extractDomain: function CPS2_extractDomain(str) { 1066 return this._parseGroup(str); 1067 }, 1068 1069 /** 1070 * Tests use this as a backchannel by calling it directly. 1071 * 1072 * @param subj This value depends on topic. 1073 * @param topic The backchannel "method" name. 1074 * @param data This value depends on topic. 1075 */ 1076 observe: function CPS2_observe(subj, topic, data) { 1077 switch (topic) { 1078 case "profile-before-change": 1079 this._destroy(); 1080 break; 1081 case "last-pb-context-exited": 1082 this._pbStore.removeAll(); 1083 break; 1084 case "test:reset": 1085 let fn = subj.QueryInterface(Ci.xpcIJSWeakReference).get(); 1086 this._reset(fn); 1087 break; 1088 case "test:db": 1089 let obj = subj.QueryInterface(Ci.xpcIJSWeakReference).get(); 1090 obj.value = this.conn; 1091 break; 1092 } 1093 }, 1094 1095 /** 1096 * Removes all state from the service. Used by tests. 1097 * 1098 * @param callback A function that will be called when done. 1099 */ 1100 async _reset(callback) { 1101 this._pbStore.removeAll(); 1102 this._cache.removeAll(); 1103 1104 this._observers = {}; 1105 this._genericObservers = []; 1106 1107 let tables = ["prefs", "groups", "settings"]; 1108 let stmts = tables.map(t => this._stmt(`DELETE FROM ${t}`)); 1109 this._execStmts(stmts, { 1110 onDone: () => { 1111 callback(); 1112 }, 1113 }); 1114 }, 1115 1116 QueryInterface: ChromeUtils.generateQI([ 1117 "nsIContentPrefService2", 1118 "nsIObserver", 1119 ]), 1120 1121 // Database Creation & Access 1122 1123 _dbVersion: 4, 1124 1125 _dbSchema: { 1126 tables: { 1127 groups: 1128 "id INTEGER PRIMARY KEY, \ 1129 name TEXT NOT NULL", 1130 1131 settings: 1132 "id INTEGER PRIMARY KEY, \ 1133 name TEXT NOT NULL", 1134 1135 prefs: 1136 "id INTEGER PRIMARY KEY, \ 1137 groupID INTEGER REFERENCES groups(id), \ 1138 settingID INTEGER NOT NULL REFERENCES settings(id), \ 1139 value BLOB, \ 1140 timestamp INTEGER NOT NULL DEFAULT 0", // Storage in seconds, API in ms. 0 for migrated values. 1141 }, 1142 indices: { 1143 groups_idx: { 1144 table: "groups", 1145 columns: ["name"], 1146 }, 1147 settings_idx: { 1148 table: "settings", 1149 columns: ["name"], 1150 }, 1151 prefs_idx: { 1152 table: "prefs", 1153 columns: ["timestamp", "groupID", "settingID"], 1154 }, 1155 }, 1156 }, 1157 1158 _debugLog: false, 1159 1160 log: function CPS2_log(aMessage) { 1161 if (this._debugLog) { 1162 Services.console.logStringMessage("ContentPrefService2: " + aMessage); 1163 } 1164 }, 1165 1166 async _getConnection(aAttemptNum = 0) { 1167 let path = PathUtils.join( 1168 await PathUtils.getProfileDir(), 1169 "content-prefs.sqlite" 1170 ); 1171 let conn; 1172 let resetAndRetry = async e => { 1173 if (e.result != Cr.NS_ERROR_FILE_CORRUPTED) { 1174 throw e; 1175 } 1176 1177 if (aAttemptNum >= this.MAX_ATTEMPTS) { 1178 if (conn) { 1179 await conn.close(); 1180 } 1181 this.log("Establishing connection failed too many times. Giving up."); 1182 throw e; 1183 } 1184 1185 try { 1186 await this._failover(conn, path); 1187 } catch (e) { 1188 Cu.reportError(e); 1189 throw e; 1190 } 1191 return this._getConnection(++aAttemptNum); 1192 }; 1193 try { 1194 conn = await Sqlite.openConnection({ path }); 1195 Sqlite.shutdown.addBlocker( 1196 "Closing ContentPrefService2 connection.", 1197 () => conn.close() 1198 ); 1199 } catch (e) { 1200 Cu.reportError(e); 1201 return resetAndRetry(e); 1202 } 1203 1204 try { 1205 await this._dbMaybeInit(conn); 1206 } catch (e) { 1207 Cu.reportError(e); 1208 return resetAndRetry(e); 1209 } 1210 1211 // Turn off disk synchronization checking to reduce disk churn and speed up 1212 // operations when prefs are changed rapidly (such as when a user repeatedly 1213 // changes the value of the browser zoom setting for a site). 1214 // 1215 // Note: this could cause database corruption if the OS crashes or machine 1216 // loses power before the data gets written to disk, but this is considered 1217 // a reasonable risk for the not-so-critical data stored in this database. 1218 await conn.execute("PRAGMA synchronous = OFF"); 1219 1220 return conn; 1221 }, 1222 1223 async _failover(aConn, aPath) { 1224 this.log("Cleaning up DB file - close & remove & backup."); 1225 if (aConn) { 1226 await aConn.close(); 1227 } 1228 let backupFile = aPath + ".corrupt"; 1229 let uniquePath = PathUtils.createUniquePath(backupFile); 1230 await IOUtils.copy(aPath, uniquePath); 1231 await IOUtils.remove(aPath); 1232 this.log("Completed DB cleanup."); 1233 }, 1234 1235 _dbMaybeInit: async function CPS2__dbMaybeInit(aConn) { 1236 let version = parseInt(await aConn.getSchemaVersion(), 10); 1237 this.log("Schema version: " + version); 1238 1239 if (version == 0) { 1240 await this._dbCreateSchema(aConn); 1241 } else if (version != this._dbVersion) { 1242 await this._dbMigrate(aConn, version, this._dbVersion); 1243 } 1244 }, 1245 1246 _createTable: async function CPS2__createTable(aConn, aName) { 1247 let tSQL = this._dbSchema.tables[aName]; 1248 this.log("Creating table " + aName + " with " + tSQL); 1249 await aConn.execute(`CREATE TABLE ${aName} (${tSQL})`); 1250 }, 1251 1252 _createIndex: async function CPS2__createTable(aConn, aName) { 1253 let index = this._dbSchema.indices[aName]; 1254 let statement = 1255 "CREATE INDEX IF NOT EXISTS " + 1256 aName + 1257 " ON " + 1258 index.table + 1259 "(" + 1260 index.columns.join(", ") + 1261 ")"; 1262 await aConn.execute(statement); 1263 }, 1264 1265 _dbCreateSchema: async function CPS2__dbCreateSchema(aConn) { 1266 await aConn.executeTransaction(async () => { 1267 this.log("Creating DB -- tables"); 1268 for (let name in this._dbSchema.tables) { 1269 await this._createTable(aConn, name); 1270 } 1271 1272 this.log("Creating DB -- indices"); 1273 for (let name in this._dbSchema.indices) { 1274 await this._createIndex(aConn, name); 1275 } 1276 1277 await aConn.setSchemaVersion(this._dbVersion); 1278 }); 1279 }, 1280 1281 _dbMigrate: async function CPS2__dbMigrate(aConn, aOldVersion, aNewVersion) { 1282 /** 1283 * Migrations should follow the template rules in bug 1074817 comment 3 which are: 1284 * 1. Migration should be incremental and non-breaking. 1285 * 2. It should be idempotent because one can downgrade an upgrade again. 1286 * On downgrade: 1287 * 1. Decrement schema version so that upgrade runs the migrations again. 1288 */ 1289 await aConn.executeTransaction(async () => { 1290 for (let i = aOldVersion; i < aNewVersion; i++) { 1291 let migrationName = "_dbMigrate" + i + "To" + (i + 1); 1292 if (typeof this[migrationName] != "function") { 1293 throw new Error( 1294 "no migrator function from version " + 1295 aOldVersion + 1296 " to version " + 1297 aNewVersion 1298 ); 1299 } 1300 await this[migrationName](aConn); 1301 } 1302 await aConn.setSchemaVersion(aNewVersion); 1303 }); 1304 }, 1305 1306 _dbMigrate1To2: async function CPS2___dbMigrate1To2(aConn) { 1307 await aConn.execute("ALTER TABLE groups RENAME TO groupsOld"); 1308 await this._createTable(aConn, "groups"); 1309 await aConn.execute(` 1310 INSERT INTO groups (id, name) 1311 SELECT id, name FROM groupsOld 1312 `); 1313 1314 await aConn.execute("DROP TABLE groupers"); 1315 await aConn.execute("DROP TABLE groupsOld"); 1316 }, 1317 1318 _dbMigrate2To3: async function CPS2__dbMigrate2To3(aConn) { 1319 for (let name in this._dbSchema.indices) { 1320 await this._createIndex(aConn, name); 1321 } 1322 }, 1323 1324 _dbMigrate3To4: async function CPS2__dbMigrate3To4(aConn) { 1325 // Add timestamp column if it does not exist yet. This operation is idempotent. 1326 try { 1327 await aConn.execute("SELECT timestamp FROM prefs"); 1328 } catch (e) { 1329 await aConn.execute( 1330 "ALTER TABLE prefs ADD COLUMN timestamp INTEGER NOT NULL DEFAULT 0" 1331 ); 1332 } 1333 1334 // To modify prefs_idx drop it and create again. 1335 await aConn.execute("DROP INDEX IF EXISTS prefs_idx"); 1336 for (let name in this._dbSchema.indices) { 1337 await this._createIndex(aConn, name); 1338 } 1339 }, 1340}; 1341 1342function checkGroupArg(group) { 1343 if (!group || typeof group != "string") { 1344 throw invalidArg("domain must be nonempty string."); 1345 } 1346} 1347 1348function checkNameArg(name) { 1349 if (!name || typeof name != "string") { 1350 throw invalidArg("name must be nonempty string."); 1351 } 1352} 1353 1354function checkValueArg(value) { 1355 if (value === undefined) { 1356 throw invalidArg("value must not be undefined."); 1357 } 1358} 1359 1360function checkCallbackArg(callback, required) { 1361 if (callback && !(callback instanceof Ci.nsIContentPrefCallback2)) { 1362 throw invalidArg("callback must be an nsIContentPrefCallback2."); 1363 } 1364 if (!callback && required) { 1365 throw invalidArg("callback must be given."); 1366 } 1367} 1368 1369function invalidArg(msg) { 1370 return Components.Exception(msg, Cr.NS_ERROR_INVALID_ARG); 1371} 1372 1373// XPCOM Plumbing 1374 1375var EXPORTED_SYMBOLS = ["ContentPrefService2"]; 1376