OLD | NEW |
1 /* | 1 /* |
2 * This Source Code is subject to the terms of the Mozilla Public License | 2 * This Source Code is subject to the terms of the Mozilla Public License |
3 * version 2.0 (the "License"). You can obtain a copy of the License at | 3 * version 2.0 (the "License"). You can obtain a copy of the License at |
4 * http://mozilla.org/MPL/2.0/. | 4 * http://mozilla.org/MPL/2.0/. |
5 */ | 5 */ |
6 | 6 |
7 const Cc = Components.classes; | 7 const Cc = Components.classes; |
8 const Ci = Components.interfaces; | 8 const Ci = Components.interfaces; |
9 const Cr = Components.results; | 9 const Cr = Components.results; |
10 const Cu = Components.utils; | 10 const Cu = Components.utils; |
11 | 11 |
12 Cu.import("resource://gre/modules/Services.jsm"); | 12 Cu.import("resource://gre/modules/Services.jsm"); |
13 | 13 |
14 function abprequire(module) | 14 function abprequire(module) |
15 { | 15 { |
16 let result = {}; | 16 let result = {}; |
17 result.wrappedJSObject = result; | 17 result.wrappedJSObject = result; |
18 Services.obs.notifyObservers(result, "adblockplus-require", module); | 18 Services.obs.notifyObservers(result, "adblockplus-require", module); |
19 return result.exports; | 19 return result.exports; |
20 } | 20 } |
21 | 21 |
22 let {Policy} = abprequire("contentPolicy"); | 22 let {Policy} = abprequire("contentPolicy"); |
23 let {RequestNotifier} = abprequire("requestNotifier"); | |
24 let {Filter} = abprequire("filterClasses"); | 23 let {Filter} = abprequire("filterClasses"); |
25 | 24 |
26 let policyGlobal = Cu.getGlobalForObject(Policy); | 25 let origShouldAllow = Policy.shouldAllow; |
27 let PolicyPrivate = null; | 26 if (!origShouldAllow) |
28 | |
29 if ("PolicyImplementation" in policyGlobal) // ABP 2.1+ with scope separation | |
30 PolicyPrivate = policyGlobal.PolicyImplementation; | |
31 else if ("require" in policyGlobal) // ABP 2.1+ without scope sepa
ration | |
32 PolicyPrivate = policyGlobal.require.scopes.contentPolicy.PolicyImplementation
; | |
33 else | |
34 window.close(); | 27 window.close(); |
35 | 28 |
36 let origShouldLoad = PolicyPrivate.shouldLoad; | |
37 let origProcessNode = Policy.processNode; | |
38 | |
39 let currentData = null; | |
40 let processingQueue = []; | 29 let processingQueue = []; |
41 let notifier = null; | 30 let notifier = null; |
42 | 31 |
43 // Randomize URI to work around bug 719376 | 32 // Randomize URI to work around bug 719376 |
44 let stringBundle = Services.strings.createBundle("chrome://abpwatcher/locale/glo
bal.properties?" + Math.random()); | 33 let stringBundle = Services.strings.createBundle("chrome://abpwatcher/locale/glo
bal.properties?" + Math.random()); |
45 | 34 |
46 let clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.
nsIClipboardHelper); | 35 let clipboardHelper = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.
nsIClipboardHelper); |
47 | 36 |
48 function init() | 37 function init() |
49 { | 38 { |
50 let list = document.getElementById("list"); | 39 let list = document.getElementById("list"); |
51 list.view = treeView; | 40 list.view = treeView; |
52 list.focus(); | 41 list.focus(); |
53 | 42 |
54 treeView.addObserver(updateProcessingTime); | 43 treeView.addObserver(updateProcessingTime); |
55 updateProcessingTime(treeView, "refresh"); | 44 updateProcessingTime(treeView, "refresh"); |
56 | 45 |
57 // Make sure the tree view has correct filters | 46 // Make sure the tree view has correct filters |
58 document.getElementById("ignore-early").doCommand(); | |
59 document.getElementById("filterText").doCommand(); | 47 document.getElementById("filterText").doCommand(); |
60 | 48 |
61 notifier = new RequestNotifier(null, handleFilterHit); | 49 Policy.shouldAllow = replacementShouldAllow; |
62 | |
63 PolicyPrivate.shouldLoad = replacementShouldLoad; | |
64 Policy.processNode = replacementProcessNode; | |
65 setInterval(processQueue, 200); | 50 setInterval(processQueue, 200); |
66 } | 51 } |
67 | 52 |
68 function E(id) | 53 function E(id) |
69 { | 54 { |
70 return document.getElementById(id); | 55 return document.getElementById(id); |
71 } | 56 } |
72 | 57 |
73 function replacementShouldLoad(contentType, contentLocation, requestOrigin, node
, mimeTypeGuess, extra) | 58 function replacementShouldAllow({contentType, location, frames, isPrivate}) |
74 { | 59 { |
75 let startTime = null; | 60 let startTime = Date.now(); |
| 61 let currentData = { |
| 62 type: contentType, |
| 63 location: location, |
| 64 frames: frames, |
| 65 isPrivate: isPrivate |
| 66 }; |
| 67 let ret; |
| 68 |
76 try | 69 try |
77 { | 70 { |
78 currentData = {internal: false, earlyReturn: true, filters: []}; | 71 ret = origShouldAllow.apply(this, arguments); |
79 startTime = Date.now(); | |
80 | |
81 if (contentLocation) | |
82 currentData.location = contentLocation.spec; | |
83 if (requestOrigin) | |
84 currentData.origin = requestOrigin.spec; | |
85 | |
86 currentData.type = contentType; | |
87 } catch(e) {} | |
88 | |
89 let ret; | |
90 try | |
91 { | |
92 ret = origShouldLoad.apply(this, arguments); | |
93 return ret; | |
94 } | |
95 finally | |
96 { | |
97 if (startTime !== null) | |
98 currentData.processingTime = (Date.now() - startTime); | |
99 currentData.result = (ret == Ci.nsIContentPolicy.ACCEPT); | |
100 | |
101 processingQueue.push(currentData); | |
102 currentData = null; | |
103 } | |
104 } | |
105 | |
106 function replacementProcessNode(wnd, node, contentType, location, collapse) | |
107 { | |
108 let startTime = null; | |
109 try | |
110 { | |
111 if (currentData && !("context" in currentData)) | |
112 { | |
113 currentData.earlyReturn = false; | |
114 currentData.context = node; | |
115 currentData.window = wnd; | |
116 currentData.internalType = contentType; | |
117 if (location) | |
118 currentData.internalLocation = location.spec; | |
119 } | |
120 else | |
121 { | |
122 // shouldLoad wasn't called - this isn't being called by content policy | |
123 let locationString = (location instanceof Filter ? location.text : locatio
n.spec); | |
124 | |
125 currentData = { | |
126 internal: true, | |
127 earlyReturn: false, | |
128 filters: [], | |
129 location: locationString, | |
130 internalLocation: locationString, | |
131 context: node, | |
132 window: wnd, | |
133 type: contentType, | |
134 internalType: contentType | |
135 }; | |
136 startTime = Date.now(); | |
137 } | |
138 } | |
139 catch(e) | |
140 { | |
141 Cu.reportError(e); | |
142 } | |
143 | |
144 let ret; | |
145 try | |
146 { | |
147 ret = origProcessNode.apply(this, arguments); | |
148 return ret; | 72 return ret; |
149 } | 73 } |
150 finally | 74 finally |
151 { | 75 { |
152 if (startTime !== null) | 76 if (startTime !== null) |
153 { | 77 { |
154 currentData.processingTime = (Date.now() - startTime); | 78 currentData.processingTime = (Date.now() - startTime); |
155 currentData.result = (ret == true); | 79 currentData.result = ret; |
| 80 currentData.filters = ret.hits.filter(h => h.filter).map(h => h.filter); |
156 | 81 |
157 processingQueue.push(currentData); | 82 processingQueue.push(currentData); |
158 currentData = null; | 83 currentData = null; |
159 } | 84 } |
160 } | 85 } |
161 } | 86 } |
162 | 87 |
163 function destroy() | 88 function destroy() |
164 { | 89 { |
165 if (notifier) | 90 if (origShouldAllow) |
166 notifier.shutdown(); | 91 Policy.shouldAllow = origShouldAllow; |
167 if (origShouldLoad) | |
168 PolicyPrivate.shouldLoad = origShouldLoad; | |
169 if (origProcessNode) | |
170 Policy.processNode = origProcessNode; | |
171 } | |
172 | |
173 function handleFilterHit(wnd, node, data) | |
174 { | |
175 if (data.filter && currentData) | |
176 currentData.filters.push(data.filter.text); | |
177 } | 92 } |
178 | 93 |
179 function processQueue() | 94 function processQueue() |
180 { | 95 { |
181 if (!processingQueue.length) | 96 if (!processingQueue.length) |
182 return; | 97 return; |
183 | 98 |
| 99 function stringify(value) |
| 100 { |
| 101 if (typeof value == "undefined" || value == null) |
| 102 return ""; |
| 103 else |
| 104 return String(value); |
| 105 } |
| 106 |
184 for each (let entry in processingQueue) | 107 for each (let entry in processingQueue) |
185 { | 108 { |
186 entry.cols = {}; | 109 entry.cols = { |
187 if (typeof entry.location != "undefined") | 110 address: stringify(entry.location), |
188 entry.cols.address = String(entry.location); | 111 type: stringify(entry.type), |
189 if (typeof entry.type != "undefined") | 112 result: stringBundle.GetStringFromName(entry.result && entry.result.allow
? "decision.allow" : "decision.block"), |
190 { | 113 origin: stringify(entry.frames && entry.frames[0] && entry.frames[0].locat
ion), |
191 entry.cols.type = String(entry.type); | 114 filter: stringify(entry.filters && entry.filters.join(", ")), |
192 try { | 115 time: stringify(entry.processingTime) |
193 // Nasty hack: try to get type name from ABP | 116 }; |
194 if (entry.type in Policy.localizedDescr) | |
195 entry.cols.type = String(Policy.localizedDescr[entry.type]); | |
196 } catch(e) {} | |
197 } | |
198 entry.cols.result = stringBundle.GetStringFromName(entry.result ? "decision.
allow" : "decision.block"); | |
199 if (typeof entry.context != "undefined") | |
200 entry.cols.context = (entry.context ? getNodeLabel(entry.context) : String
(entry.context)); | |
201 if (typeof entry.window != "undefined") | |
202 entry.cols.document = (entry.window ? getNodeLabel(entry.window) : String(
entry.window)); | |
203 if (typeof entry.origin != "undefined") | |
204 entry.cols.origin = String(entry.origin); | |
205 if (entry.filters.length) | |
206 entry.cols.filter = entry.filters.join(", "); | |
207 if (typeof entry.processingTime != "undefined") | |
208 entry.cols.time = String(entry.processingTime); | |
209 | |
210 let additional = []; | |
211 if (entry.internal) | |
212 additional.push(stringBundle.GetStringFromName("additional.internalInvocat
ion")); | |
213 if (typeof entry.internalType != "undefined" && entry.type != entry.internal
Type) | |
214 { | |
215 let internalType = String(entry.internalType); | |
216 try { | |
217 // Nasty hack: try to get type name from ABP | |
218 if (entry.internalType in Policy.localizedDescr) | |
219 internalType = String(Policy.localizedDescr[entry.internalType]); | |
220 } catch(e) {} | |
221 additional.push(stringBundle.formatStringFromName("additional.typeChanged"
, [internalType], 1)); | |
222 } | |
223 if (typeof entry.internalLocation != "undefined" && entry.location != entry.
internalLocation) | |
224 additional.push(stringBundle.formatStringFromName("additional.locationChan
ged", [String(entry.internalLocation)], 1)); | |
225 | |
226 if (additional.length > 0) | |
227 entry.cols.additional = additional.join(", "); | |
228 | |
229 treeView.add(entry); | 117 treeView.add(entry); |
230 } | 118 } |
231 | 119 |
232 processingQueue = []; | 120 processingQueue = []; |
233 } | 121 } |
234 | 122 |
235 function getNodeLabel(node) | |
236 { | |
237 try | |
238 { | |
239 if (node instanceof Ci.nsIDOMWindow) | |
240 return stringBundle.formatStringFromName("NodeLabel.window", [node.locatio
n.href], 1); | |
241 if (node instanceof Ci.nsIDOMDocument) | |
242 return stringBundle.formatStringFromName("NodeLabel.document", [node.URL],
1); | |
243 else if (node instanceof Ci.nsIDOMXULElement) | |
244 return stringBundle.formatStringFromName("NodeLabel.xulElement", [node.tag
Name], 1); | |
245 else if (node instanceof Ci.nsIDOMHTMLElement) | |
246 return stringBundle.formatStringFromName("NodeLabel.htmlElement", [node.ta
gName], 1); | |
247 else if (node instanceof Ci.nsIDOMSVGElement) | |
248 return stringBundle.formatStringFromName("NodeLabel.svgElement", [node.tag
Name], 1); | |
249 else if (node instanceof Ci.nsIDOMElement) | |
250 return stringBundle.formatStringFromName("NodeLabel.element", [node.tagNam
e], 1); | |
251 else | |
252 return stringBundle.formatStringFromName("NodeLabel.unknown", [String(node
)], 1); | |
253 } | |
254 catch (e) | |
255 { | |
256 Cu.reportError(e); | |
257 return stringBundle.formatStringFromName("NodeLabel.unknown", [""], 1); | |
258 } | |
259 } | |
260 | |
261 function fillInTooltip(event) | 123 function fillInTooltip(event) |
262 { | 124 { |
263 let entry = treeView.getEntryAt(event.clientX, event.clientY); | 125 let entry = treeView.getEntryAt(event.clientX, event.clientY); |
264 if (!entry) | 126 if (!entry) |
265 return false; | 127 return false; |
266 | 128 |
267 let rows = document.getElementById("tooltip-rows"); | 129 let rows = document.getElementById("tooltip-rows"); |
268 while (rows.firstChild) | 130 while (rows.firstChild) |
269 rows.removeChild(rows.firstChild); | 131 rows.removeChild(rows.firstChild); |
270 | 132 |
271 let cols = document.getElementById("list").getElementsByTagName("treecol"); | 133 let cols = document.getElementById("list").getElementsByTagName("treecol"); |
272 for (let i = 0; i < cols.length; i++) | 134 for (let i = 0; i < cols.length; i++) |
273 { | 135 { |
274 let col = cols[i].id; | 136 let col = cols[i].id; |
275 if (col && col in entry.cols) | 137 if (col && col in entry.cols) |
276 { | 138 { |
277 let row = document.createElement("row"); | 139 let row = document.createElement("row"); |
278 | 140 |
279 let label = document.createElement("description"); | 141 let label = document.createElement("description"); |
280 label.setAttribute("class", "tooltip-label"); | 142 label.setAttribute("class", "tooltip-label"); |
281 label.setAttribute("value", cols[i].getAttribute("label")); | 143 label.setAttribute("value", cols[i].getAttribute("label")); |
282 row.appendChild(label); | 144 row.appendChild(label); |
283 | 145 |
284 let value = document.createElement("vbox"); | 146 let value = document.createElement("vbox"); |
285 setMultilineContent(value, entry.cols[col]); | 147 let data = entry.cols[col]; |
| 148 if (col == "origin") |
| 149 data = entry.frames.map(f => f.location).join("\n"); |
| 150 setMultilineContent(value, data); |
286 row.appendChild(value); | 151 row.appendChild(value); |
287 | 152 |
288 rows.appendChild(row); | 153 rows.appendChild(row); |
289 } | 154 } |
290 } | 155 } |
291 | 156 |
292 return true; | 157 return true; |
293 } | 158 } |
294 | 159 |
295 function updateContextMenu(event) | 160 function updateContextMenu(event) |
(...skipping 14 matching lines...) Expand all Loading... |
310 clipboardHelper.copyString(String(entry.location)); | 175 clipboardHelper.copyString(String(entry.location)); |
311 } | 176 } |
312 | 177 |
313 function copyFilters() | 178 function copyFilters() |
314 { | 179 { |
315 let entry = treeView.getCurrentEntry(); | 180 let entry = treeView.getCurrentEntry(); |
316 if (entry && entry.filters.length) | 181 if (entry && entry.filters.length) |
317 clipboardHelper.copyString(entry.filters.join("\n")); | 182 clipboardHelper.copyString(entry.filters.join("\n")); |
318 } | 183 } |
319 | 184 |
320 function setMultilineContent(box, text) { | 185 function setMultilineContent(box, text) |
321 // The following is sufficient in Gecko 1.9 but Gecko 1.8 fails on multiline | 186 { |
322 // text fields in tooltips | 187 let lines = text.split(/\n+/); |
323 // box.textContent = text.replace(/\S{80}(?=\S)/g, "$& "); | 188 for (let line of lines) |
324 | 189 { |
325 for (let i = 0; i < text.length; i += 80) { | |
326 let description = document.createElement("description"); | 190 let description = document.createElement("description"); |
327 description.setAttribute("value", text.substr(i, 80)); | 191 description.textContent = line.replace(/\S{80}(?=\S)/g, "$& "); |
328 box.appendChild(description); | 192 box.appendChild(description); |
329 } | 193 } |
330 } | 194 } |
331 | 195 |
332 var totalProcessingTime = 0; | 196 var totalProcessingTime = 0; |
333 function updateProcessingTime(view, operation, entry) | 197 function updateProcessingTime(view, operation, entry) |
334 { | 198 { |
335 if (operation == "add") | 199 if (operation == "add") |
336 totalProcessingTime += entry.processingTime; | 200 totalProcessingTime += entry.processingTime; |
337 else { | 201 else { |
338 totalProcessingTime = 0; | 202 totalProcessingTime = 0; |
339 for each (let entry in view.displayedItems) | 203 for each (let entry in view.displayedItems) |
340 totalProcessingTime += entry.processingTime; | 204 totalProcessingTime += entry.processingTime; |
341 } | 205 } |
342 | 206 |
343 let numItems = view.displayedItems.length; | 207 let numItems = view.displayedItems.length; |
344 | 208 |
345 let summary = document.getElementById("summary"); | 209 let summary = document.getElementById("summary"); |
346 let template = summary.getAttribute("_template"); | 210 let template = summary.getAttribute("_template"); |
347 summary.textContent = template.replace(/\*NUMITEMS\*/g, numItems).replace(/\*T
IME\*/, (totalProcessingTime / 1000).toFixed(3)); | 211 summary.textContent = template.replace(/\*NUMITEMS\*/g, numItems).replace(/\*T
IME\*/, (totalProcessingTime / 1000).toFixed(3)); |
348 } | 212 } |
349 | 213 |
350 var treeView = { | 214 var treeView = { |
351 currentItems: [], | 215 currentItems: [], |
352 displayedItems: [], | 216 displayedItems: [], |
353 _ignoreEarlyReturns: false, | |
354 _filterString: "", | 217 _filterString: "", |
355 _sortColumn: null, | 218 _sortColumn: null, |
356 _sortDirection: null, | 219 _sortDirection: null, |
357 boxObject: null, | 220 boxObject: null, |
358 atoms: {}, | 221 atoms: {}, |
359 observers: [], | 222 observers: [], |
360 | 223 |
361 // | 224 // |
362 // nsISupports implementation | 225 // nsISupports implementation |
363 // | 226 // |
(...skipping 15 matching lines...) Expand all Loading... |
379 selection: null, | 242 selection: null, |
380 | 243 |
381 setTree: function(boxObject) | 244 setTree: function(boxObject) |
382 { | 245 { |
383 if (!boxObject) | 246 if (!boxObject) |
384 return; | 247 return; |
385 | 248 |
386 this.boxObject = boxObject; | 249 this.boxObject = boxObject; |
387 | 250 |
388 let atomService = Cc["@mozilla.org/atom-service;1"].getService(Ci.nsIAtomSer
vice); | 251 let atomService = Cc["@mozilla.org/atom-service;1"].getService(Ci.nsIAtomSer
vice); |
389 for each (let col in ["address", "type", "result", "context", "document", "o
rigin", "additional", "filter", "time"]) | 252 for each (let col in ["address", "type", "result", "origin", "filter", "time
"]) |
390 { | 253 { |
391 let atomStr = "col-" + col; | 254 let atomStr = "col-" + col; |
392 this.atoms[atomStr] = atomService.getAtom(atomStr); | 255 this.atoms[atomStr] = atomService.getAtom(atomStr); |
393 } | 256 } |
394 for each (let flag in ["selected", "blocked"]) | 257 for each (let flag in ["selected", "blocked"]) |
395 { | 258 { |
396 let atomStr = flag + "-true"; | 259 let atomStr = flag + "-true"; |
397 this.atoms[atomStr] = atomService.getAtom(atomStr); | 260 this.atoms[atomStr] = atomService.getAtom(atomStr); |
398 | 261 |
399 atomStr = flag + "-false"; | 262 atomStr = flag + "-false"; |
(...skipping 54 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
454 }, | 317 }, |
455 | 318 |
456 getRowProperties: function(row, properties) | 319 getRowProperties: function(row, properties) |
457 { | 320 { |
458 if (row < 0 || row >= this.displayedItems.length) | 321 if (row < 0 || row >= this.displayedItems.length) |
459 return ""; | 322 return ""; |
460 | 323 |
461 let entry = this.displayedItems[row]; | 324 let entry = this.displayedItems[row]; |
462 return this.generateProperties([ | 325 return this.generateProperties([ |
463 "selected-" + this.selection.isSelected(row), | 326 "selected-" + this.selection.isSelected(row), |
464 "blocked-" + !entry.result | 327 "blocked-" + !(entry.result && entry.result.allow) |
465 ], properties); | 328 ], properties); |
466 }, | 329 }, |
467 | 330 |
468 getCellProperties: function(row, col, properties) | 331 getCellProperties: function(row, col, properties) |
469 { | 332 { |
470 return this.getRowProperties(row, properties) + " " + this.getColumnProperti
es(col, properties); | 333 return this.getRowProperties(row, properties) + " " + this.getColumnProperti
es(col, properties); |
471 }, | 334 }, |
472 | 335 |
473 cycleHeader: function(col) | 336 cycleHeader: function(col) |
474 { | 337 { |
(...skipping 49 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
524 cycleCell: function() {}, | 387 cycleCell: function() {}, |
525 performAction: function() {}, | 388 performAction: function() {}, |
526 performActionOnRow: function() {}, | 389 performActionOnRow: function() {}, |
527 performActionOnCell: function() {}, | 390 performActionOnCell: function() {}, |
528 selectionChanged: function() {}, | 391 selectionChanged: function() {}, |
529 | 392 |
530 // | 393 // |
531 // Custom methods | 394 // Custom methods |
532 // | 395 // |
533 | 396 |
534 get ignoreEarlyReturns() | |
535 { | |
536 return this._ignoreEarlyReturns; | |
537 }, | |
538 set ignoreEarlyReturns(value) | |
539 { | |
540 this._ignoreEarlyReturns = value; | |
541 this.refilter(); | |
542 }, | |
543 | |
544 get filterString() | 397 get filterString() |
545 { | 398 { |
546 return this._filterString; | 399 return this._filterString; |
547 }, | 400 }, |
548 set filterString(value) | 401 set filterString(value) |
549 { | 402 { |
550 this._filterString = value.toLowerCase(); | 403 this._filterString = value.toLowerCase(); |
551 this.refilter(); | 404 this.refilter(); |
552 }, | 405 }, |
553 | 406 |
554 filter: function(entry) | 407 filter: function(entry) |
555 { | 408 { |
556 if (this._ignoreEarlyReturns && entry.earlyReturn) | |
557 return false; | |
558 | |
559 if (this._filterString) | 409 if (this._filterString) |
560 { | 410 { |
561 let foundMatch = false; | 411 let foundMatch = false; |
562 for each (let label in entry.cols) | 412 for each (let label in entry.cols) |
563 if (label.toLowerCase().indexOf(this._filterString) >= 0) | 413 if (label.toLowerCase().indexOf(this._filterString) >= 0) |
564 foundMatch = true; | 414 foundMatch = true; |
565 | 415 |
566 if (!foundMatch) | 416 if (!foundMatch) |
567 return false; | 417 return false; |
568 } | 418 } |
(...skipping 109 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
678 for (let i = 0; i < this.observers.length; i++) | 528 for (let i = 0; i < this.observers.length; i++) |
679 if (this.observers[i] == observer) | 529 if (this.observers[i] == observer) |
680 this.observers.splice(i--, 1); | 530 this.observers.splice(i--, 1); |
681 }, | 531 }, |
682 notifyObservers: function(operation, entry) | 532 notifyObservers: function(operation, entry) |
683 { | 533 { |
684 for each (let observer in this.observers) | 534 for each (let observer in this.observers) |
685 observer(this, operation, entry); | 535 observer(this, operation, entry); |
686 } | 536 } |
687 }; | 537 }; |
OLD | NEW |