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 | }; |