engines/forms.js

320 lines, 182 LOC, 51 covered (28%)

2 1
/* ***** BEGIN LICENSE BLOCK *****
2
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
3
 *
4
 * The contents of this file are subject to the Mozilla Public License Version
5
 * 1.1 (the "License"); you may not use this file except in compliance with
6
 * the License. You may obtain a copy of the License at
7
 * http://www.mozilla.org/MPL/
8
 *
9
 * Software distributed under the License is distributed on an "AS IS" basis,
10
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
11
 * for the specific language governing rights and limitations under the
12
 * License.
13
 *
14
 * The Original Code is Bookmarks Sync.
15
 *
16
 * The Initial Developer of the Original Code is Mozilla.
17
 * Portions created by the Initial Developer are Copyright (C) 2008
18
 * the Initial Developer. All Rights Reserved.
19
 *
20
 * Contributor(s):
21
 *  Anant Narayanan <anant@kix.in>
22
 *
23
 * Alternatively, the contents of this file may be used under the terms of
24
 * either the GNU General Public License Version 2 or later (the "GPL"), or
25
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
26
 * in which case the provisions of the GPL or the LGPL are applicable instead
27
 * of those above. If you wish to allow use of your version of this file only
28
 * under the terms of either the GPL or the LGPL, and not to allow others to
29
 * use your version of this file under the terms of the MPL, indicate your
30
 * decision by deleting the provisions above and replace them with the notice
31
 * and other provisions required by the GPL or the LGPL. If you do not delete
32
 * the provisions above, a recipient may use your version of this file under
33
 * the terms of any one of the MPL, the GPL or the LGPL.
34
 *
35
 * ***** END LICENSE BLOCK ***** */
36
14 37
const EXPORTED_SYMBOLS = ['FormEngine'];
38
8 39
const Cc = Components.classes;
8 40
const Ci = Components.interfaces;
8 41
const Cu = Components.utils;
42
10 43
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
10 44
Cu.import("resource://weave/util.js");
10 45
Cu.import("resource://weave/engines.js");
10 46
Cu.import("resource://weave/stores.js");
10 47
Cu.import("resource://weave/trackers.js");
10 48
Cu.import("resource://weave/type_records/forms.js");
49
4 50
let FormWrapper = {
4 51
  getAllEntries: function getAllEntries() {
52
    let entries = [];
53
    // Sort by (lastUsed - minLast) / (maxLast - minLast) * timesUsed / maxTimes
54
    let query = this.createStatement(
55
      "SELECT fieldname, value FROM moz_formhistory " +
56
      "ORDER BY 1.0 * (lastUsed - (SELECT lastUsed FROM moz_formhistory ORDER BY lastUsed ASC LIMIT 1)) / " +
57
        "((SELECT lastUsed FROM moz_formhistory ORDER BY lastUsed DESC LIMIT 1) - (SELECT lastUsed FROM moz_formhistory ORDER BY lastUsed ASC LIMIT 1)) * " +
58
        "timesUsed / (SELECT timesUsed FROM moz_formhistory ORDER BY timesUsed DESC LIMIT 1) DESC " +
59
      "LIMIT 500");
60
    while (query.executeStep()) {
61
      entries.push({
62
        name: query.row.fieldname,
63
        value: query.row.value
64
      });
65
    }
66
    return entries;
67
  },
68
4 69
  getEntry: function getEntry(guid) {
70
    let query = this.createStatement(
71
      "SELECT fieldname, value FROM moz_formhistory WHERE guid = :guid");
72
    query.params.guid = guid;
73
    if (!query.executeStep())
74
      return;
75
76
    return {
77
      name: query.row.fieldname,
78
      value: query.row.value
79
    };
80
  },
81
4 82
  getGUID: function getGUID(name, value) {
83
    // Query for the provided entry
84
    let getQuery = this.createStatement(
85
      "SELECT guid FROM moz_formhistory " +
86
      "WHERE fieldname = :name AND value = :value");
87
    getQuery.params.name = name;
88
    getQuery.params.value = value;
89
    getQuery.executeStep();
90
91
    // Give the guid if we found one
92
    if (getQuery.row.guid != null)
93
      return getQuery.row.guid;
94
95
    // We need to create a guid for this entry
96
    let setQuery = this.createStatement(
97
      "UPDATE moz_formhistory SET guid = :guid " +
98
      "WHERE fieldname = :name AND value = :value");
99
    let guid = Utils.makeGUID();
100
    setQuery.params.guid = guid;
101
    setQuery.params.name = name;
102
    setQuery.params.value = value;
103
    setQuery.execute();
104
105
    return guid;
106
  },
107
4 108
  hasGUID: function hasGUID(guid) {
109
    let query = this.createStatement(
110
      "SELECT 1 FROM moz_formhistory WHERE guid = :guid");
111
    query.params.guid = guid;
112
    return query.executeStep();
113
  },
114
4 115
  replaceGUID: function replaceGUID(oldGUID, newGUID) {
116
    let query = this.createStatement(
117
      "UPDATE moz_formhistory SET guid = :newGUID WHERE guid = :oldGUID");
118
    query.params.oldGUID = oldGUID;
119
    query.params.newGUID = newGUID;
120
    query.execute();
121
  },
122
8 123
  createStatement: function createStatement(query) {
124
    try {
125
      // Just return the statement right away if it's okay
126
      return Svc.Form.DBConnection.createStatement(query);
127
    }
128
    catch(ex) {
129
      // Assume guid column must not exist yet, so add it with an index
130
      Svc.Form.DBConnection.executeSimpleSQL(
131
        "ALTER TABLE moz_formhistory ADD COLUMN guid TEXT");
132
      Svc.Form.DBConnection.executeSimpleSQL(
133
        "CREATE INDEX IF NOT EXISTS moz_formhistory_guid_index " +
134
        "ON moz_formhistory (guid)");
135
136
      // Try creating the query now that the column exists
137
      return Svc.Form.DBConnection.createStatement(query);
138
    }
139
  }
140
};
141
6 142
function FormEngine() {
14 143
  SyncEngine.call(this, "Forms");
144
}
4 145
FormEngine.prototype = {
6 146
  __proto__: SyncEngine.prototype,
4 147
  _storeObj: FormStore,
4 148
  _trackerObj: FormTracker,
4 149
  _recordObj: FormRec,
7 150
  get prefName() "history",
151
10 152
  _findDupe: function _findDupe(item) {
153
    if (Svc.Form.entryExists(item.name, item.value))
154
      return FormWrapper.getGUID(item.name, item.value);
155
  }
156
};
157
4 158
function FormStore(name) {
159
  Store.call(this, name);
160
}
4 161
FormStore.prototype = {
6 162
  __proto__: Store.prototype,
163
4 164
  getAllIDs: function FormStore_getAllIDs() {
165
    let guids = {};
166
    for each (let {name, value} in FormWrapper.getAllEntries())
167
      guids[FormWrapper.getGUID(name, value)] = true;
168
    return guids;
169
  },
170
4 171
  changeItemID: function FormStore_changeItemID(oldID, newID) {
172
    FormWrapper.replaceGUID(oldID, newID);
173
  },
174
4 175
  itemExists: function FormStore_itemExists(id) {
176
    return FormWrapper.hasGUID(id);
177
  },
178
4 179
  createRecord: function createRecord(guid) {
180
    let record = new FormRec();
181
    let entry = FormWrapper.getEntry(guid);
182
    if (entry != null) {
183
      record.name = entry.name;
184
      record.value = entry.value
185
    }
186
    else
187
      record.deleted = true;
188
    return record;
189
  },
190
4 191
  create: function FormStore_create(record) {
192
    this._log.trace("Adding form record for " + record.name);
193
    Svc.Form.addEntry(record.name, record.value);
194
  },
195
4 196
  remove: function FormStore_remove(record) {
197
    this._log.trace("Removing form record: " + record.id);
198
199
    // Just skip remove requests for things already gone
200
    let entry = FormWrapper.getEntry(record.id);
201
    if (entry == null)
202
      return;
203
204
    Svc.Form.removeEntry(entry.name, entry.value);
205
  },
206
4 207
  update: function FormStore_update(record) {
208
    this._log.warn("Ignoring form record update request!");
209
  },
210
10 211
  wipe: function FormStore_wipe() {
212
    Svc.Form.removeAllEntries();
213
  }
214
};
215
6 216
function FormTracker(name) {
12 217
  Tracker.call(this, name);
14 218
  Svc.Obs.add("form-notifier", this);
16 219
  Svc.Obs.add("earlyformsubmit", this);
220
}
4 221
FormTracker.prototype = {
6 222
  __proto__: Tracker.prototype,
223
8 224
  QueryInterface: XPCOMUtils.generateQI([
8 225
    Ci.nsIFormSubmitObserver,
12 226
    Ci.nsIObserver]),
227
4 228
  trackEntry: function trackEntry(name, value) {
229
    this.addChangedID(FormWrapper.getGUID(name, value));
230
    this.score += 10;
231
  },
232
4 233
  observe: function observe(subject, topic, data) {
234
    let name, value;
235
236
    // Figure out if it's a function that we care about tracking
237
    let formCall = JSON.parse(data);
238
    let func = formCall.func;
239
    if ((func == "addEntry" && formCall.type == "after") ||
240
        (func == "removeEntry" && formCall.type == "before"))
241
      [name, value] = formCall.args;
242
243
    // Skip if there's nothing of interest
244
    if (name == null || value == null)
245
      return;
246
247
    this._log.trace("Logging form action: " + [func, name, value]);
248
    this.trackEntry(name, value);
249
  },
250
10 251
  notify: function FormTracker_notify(formElement, aWindow, actionURI) {
252
    if (this.ignoreAll)
253
      return;
254
255
    this._log.trace("Form submission notification for " + actionURI.spec);
256
257
    // XXX Bug 487541 Copy the logic from nsFormHistory::Notify to avoid
258
    // divergent logic, which can lead to security issues, until there's a
259
    // better way to get satchel's results like with a notification.
260
261
    // Determine if a dom node has the autocomplete attribute set to "off"
262
    let completeOff = function(domNode) {
263
      let autocomplete = domNode.getAttribute("autocomplete");
264
      return autocomplete && autocomplete.search(/^off$/i) == 0;
265
    }
266
267
    if (completeOff(formElement)) {
268
      this._log.trace("Form autocomplete set to off");
269
      return;
270
    }
271
272
    /* Get number of elements in form, add points and changedIDs */
273
    let len = formElement.length;
274
    let elements = formElement.elements;
275
    for (let i = 0; i < len; i++) {
276
      let el = elements.item(i);
277
278
      // Grab the name for debugging, but check if empty when satchel would
279
      let name = el.name;
280
      if (name === "")
281
        name = el.id;
282
283
      if (!(el instanceof Ci.nsIDOMHTMLInputElement)) {
284
        this._log.trace(name + " is not a DOMHTMLInputElement: " + el);
285
        continue;
286
      }
287
288
      if (el.type.search(/^text$/i) != 0) {
289
        this._log.trace(name + "'s type is not 'text': " + el.type);
290
        continue;
291
      }
292
293
      if (completeOff(el)) {
294
        this._log.trace(name + "'s autocomplete set to off");
295
        continue;
296
      }
297
298
      if (el.value === "") {
299
        this._log.trace(name + "'s value is empty");
300
        continue;
301
      }
302
303
      if (el.value == el.defaultValue) {
304
        this._log.trace(name + "'s value is the default");
305
        continue;
306
      }
307
308
      if (name === "") {
309
        this._log.trace("Text input element has no name or id");
310
        continue;
311
      }
312
313
      // Get the GUID on a delay so that it can be added to the DB first...
314
      Utils.delay(function() {
315
        this._log.trace("Logging form element: " + [name, el.value]);
316
        this.trackEntry(name, el.value);
317
      }, 0, this);
318
    }
319
  }
2 320
};