forms.sys.mjs (7381B)
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 import { 6 Store, 7 SyncEngine, 8 LegacyTracker, 9 } from "resource://services-sync/engines.sys.mjs"; 10 11 import { CryptoWrapper } from "resource://services-sync/record.sys.mjs"; 12 import { Svc, Utils } from "resource://services-sync/util.sys.mjs"; 13 14 import { SCORE_INCREMENT_MEDIUM } from "resource://services-sync/constants.sys.mjs"; 15 import { 16 CollectionProblemData, 17 CollectionValidator, 18 } from "resource://services-sync/collection_validator.sys.mjs"; 19 20 import { Async } from "resource://services-common/async.sys.mjs"; 21 import { Log } from "resource://gre/modules/Log.sys.mjs"; 22 23 const lazy = {}; 24 ChromeUtils.defineESModuleGetters(lazy, { 25 FormHistory: "resource://gre/modules/FormHistory.sys.mjs", 26 }); 27 28 const FORMS_TTL = 3 * 365 * 24 * 60 * 60; // Three years in seconds. 29 30 export function FormRec(collection, id) { 31 CryptoWrapper.call(this, collection, id); 32 } 33 34 FormRec.prototype = { 35 _logName: "Sync.Record.Form", 36 ttl: FORMS_TTL, 37 }; 38 Object.setPrototypeOf(FormRec.prototype, CryptoWrapper.prototype); 39 40 Utils.deferGetSet(FormRec, "cleartext", ["name", "value"]); 41 42 var FormWrapper = { 43 _log: Log.repository.getLogger("Sync.Engine.Forms"), 44 45 _getEntryCols: ["fieldname", "value"], 46 _guidCols: ["guid"], 47 48 _search(terms, searchData) { 49 return lazy.FormHistory.search(terms, searchData); 50 }, 51 52 async _update(changes) { 53 if (!lazy.FormHistory.enabled) { 54 return; // update isn't going to do anything. 55 } 56 await lazy.FormHistory.update(changes).catch(console.error); 57 }, 58 59 async getEntry(guid) { 60 let results = await this._search(this._getEntryCols, { guid }); 61 if (!results.length) { 62 return null; 63 } 64 return { name: results[0].fieldname, value: results[0].value }; 65 }, 66 67 async getGUID(name, value) { 68 // Query for the provided entry. 69 let query = { fieldname: name, value }; 70 let results = await this._search(this._guidCols, query); 71 return results.length ? results[0].guid : null; 72 }, 73 74 async hasGUID(guid) { 75 // We could probably use a count function here, but search exists... 76 let results = await this._search(this._guidCols, { guid }); 77 return !!results.length; 78 }, 79 80 async replaceGUID(oldGUID, newGUID) { 81 let changes = { 82 op: "update", 83 guid: oldGUID, 84 newGuid: newGUID, 85 }; 86 await this._update(changes); 87 }, 88 }; 89 90 export function FormEngine(service) { 91 SyncEngine.call(this, "Forms", service); 92 } 93 94 FormEngine.prototype = { 95 _storeObj: FormStore, 96 _trackerObj: FormTracker, 97 _recordObj: FormRec, 98 99 syncPriority: 6, 100 101 get prefName() { 102 return "history"; 103 }, 104 105 async _findDupe(item) { 106 return FormWrapper.getGUID(item.name, item.value); 107 }, 108 }; 109 Object.setPrototypeOf(FormEngine.prototype, SyncEngine.prototype); 110 111 function FormStore(name, engine) { 112 Store.call(this, name, engine); 113 } 114 FormStore.prototype = { 115 async _processChange(change) { 116 // If this._changes is defined, then we are applying a batch, so we 117 // can defer it. 118 if (this._changes) { 119 this._changes.push(change); 120 return; 121 } 122 123 // Otherwise we must handle the change right now. 124 await FormWrapper._update(change); 125 }, 126 127 async applyIncomingBatch(records, countTelemetry) { 128 Async.checkAppReady(); 129 // We collect all the changes to be made then apply them all at once. 130 this._changes = []; 131 let failures = await Store.prototype.applyIncomingBatch.call( 132 this, 133 records, 134 countTelemetry 135 ); 136 if (this._changes.length) { 137 await FormWrapper._update(this._changes); 138 } 139 delete this._changes; 140 return failures; 141 }, 142 143 async getAllIDs() { 144 let results = await FormWrapper._search(["guid"], []); 145 let guids = {}; 146 for (let result of results) { 147 guids[result.guid] = true; 148 } 149 return guids; 150 }, 151 152 async changeItemID(oldID, newID) { 153 await FormWrapper.replaceGUID(oldID, newID); 154 }, 155 156 async itemExists(id) { 157 return FormWrapper.hasGUID(id); 158 }, 159 160 async createRecord(id, collection) { 161 let record = new FormRec(collection, id); 162 let entry = await FormWrapper.getEntry(id); 163 if (entry != null) { 164 record.name = entry.name; 165 record.value = entry.value; 166 } else { 167 record.deleted = true; 168 } 169 return record; 170 }, 171 172 async create(record) { 173 this._log.trace("Adding form record for " + record.name); 174 let change = { 175 op: "add", 176 guid: record.id, 177 fieldname: record.name, 178 value: record.value, 179 }; 180 await this._processChange(change); 181 }, 182 183 async remove(record) { 184 this._log.trace("Removing form record: " + record.id); 185 let change = { 186 op: "remove", 187 guid: record.id, 188 }; 189 await this._processChange(change); 190 }, 191 192 async update() { 193 this._log.trace("Ignoring form record update request!"); 194 }, 195 196 async wipe() { 197 let change = { 198 op: "remove", 199 }; 200 await FormWrapper._update(change); 201 }, 202 }; 203 Object.setPrototypeOf(FormStore.prototype, Store.prototype); 204 205 function FormTracker(name, engine) { 206 LegacyTracker.call(this, name, engine); 207 } 208 FormTracker.prototype = { 209 QueryInterface: ChromeUtils.generateQI([ 210 "nsIObserver", 211 "nsISupportsWeakReference", 212 ]), 213 214 onStart() { 215 Svc.Obs.add("satchel-storage-changed", this.asyncObserver); 216 }, 217 218 onStop() { 219 Svc.Obs.remove("satchel-storage-changed", this.asyncObserver); 220 }, 221 222 async observe(subject, topic, data) { 223 if (this.ignoreAll) { 224 return; 225 } 226 switch (topic) { 227 case "satchel-storage-changed": 228 if (data == "formhistory-add" || data == "formhistory-remove") { 229 let guid = subject.QueryInterface(Ci.nsISupportsString).toString(); 230 await this.trackEntry(guid); 231 } 232 break; 233 } 234 }, 235 236 async trackEntry(guid) { 237 const added = await this.addChangedID(guid); 238 if (added) { 239 this.score += SCORE_INCREMENT_MEDIUM; 240 } 241 }, 242 }; 243 Object.setPrototypeOf(FormTracker.prototype, LegacyTracker.prototype); 244 245 class FormsProblemData extends CollectionProblemData { 246 getSummary() { 247 // We don't support syncing deleted form data, so "clientMissing" isn't a problem 248 return super.getSummary().filter(entry => entry.name !== "clientMissing"); 249 } 250 } 251 252 export class FormValidator extends CollectionValidator { 253 constructor() { 254 super("forms", "id", ["name", "value"]); 255 this.ignoresMissingClients = true; 256 } 257 258 emptyProblemData() { 259 return new FormsProblemData(); 260 } 261 262 async getClientItems() { 263 return FormWrapper._search(["guid", "fieldname", "value"], {}); 264 } 265 266 normalizeClientItem(item) { 267 return { 268 id: item.guid, 269 guid: item.guid, 270 name: item.fieldname, 271 fieldname: item.fieldname, 272 value: item.value, 273 original: item, 274 }; 275 } 276 277 async normalizeServerItem(item) { 278 let res = Object.assign( 279 { 280 guid: item.id, 281 fieldname: item.name, 282 original: item, 283 }, 284 item 285 ); 286 // Missing `name` or `value` causes the getGUID call to throw 287 if (item.name !== undefined && item.value !== undefined) { 288 let guid = await FormWrapper.getGUID(item.name, item.value); 289 if (guid) { 290 res.guid = guid; 291 res.id = guid; 292 res.duped = true; 293 } 294 } 295 296 return res; 297 } 298 }