OLD | NEW |
1 /* | 1 /* |
2 * This file is part of Adblock Plus <https://adblockplus.org/>, | 2 * This file is part of Adblock Plus <https://adblockplus.org/>, |
3 * Copyright (C) 2006-present eyeo GmbH | 3 * Copyright (C) 2006-present eyeo GmbH |
4 * | 4 * |
5 * Adblock Plus is free software: you can redistribute it and/or modify | 5 * Adblock Plus is free software: you can redistribute it and/or modify |
6 * it under the terms of the GNU General Public License version 3 as | 6 * it under the terms of the GNU General Public License version 3 as |
7 * published by the Free Software Foundation. | 7 * published by the Free Software Foundation. |
8 * | 8 * |
9 * Adblock Plus is distributed in the hope that it will be useful, | 9 * Adblock Plus is distributed in the hope that it will be useful, |
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of | 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 * GNU General Public License for more details. | 12 * GNU General Public License for more details. |
13 * | 13 * |
14 * You should have received a copy of the GNU General Public License | 14 * You should have received a copy of the GNU General Public License |
15 * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. | 15 * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. |
16 */ | 16 */ |
17 | 17 |
18 var window = this; | 18 "use strict"; |
| 19 |
| 20 let window = this; |
19 | 21 |
20 // | 22 // |
21 // Module framework stuff | 23 // Module framework stuff |
22 // | 24 // |
23 | 25 |
24 function require(module) | 26 function require(module) |
25 { | 27 { |
26 return require.scopes[module]; | 28 return require.scopes[module]; |
27 } | 29 } |
28 require.scopes = {__proto__: null}; | 30 require.scopes = {__proto__: null}; |
29 | 31 |
30 function importAll(module, globalObj) | |
31 { | |
32 var exports = require(module); | |
33 for (var key in exports) | |
34 globalObj[key] = exports[key]; | |
35 } | |
36 | |
37 const onShutdown = { | 32 const onShutdown = { |
38 done: false, | 33 done: false, |
39 add: function() {}, | 34 add() {}, |
40 remove: function() {} | 35 remove() {} |
41 }; | 36 }; |
42 | 37 |
43 // | 38 // |
44 // XPCOM emulation | 39 // XPCOM emulation |
45 // | 40 // |
46 | 41 |
| 42 // nsIHttpChannel is checked against instanceof. |
| 43 class nsIHttpChannel |
| 44 { |
| 45 } |
| 46 |
47 const Components = | 47 const Components = |
48 { | 48 { |
49 interfaces: | 49 interfaces: |
50 { | 50 { |
51 nsIHttpChannel: function() {}, | 51 nsIHttpChannel, |
52 nsITimer: {TYPE_REPEATING_SLACK: 0}, | 52 nsITimer: {TYPE_REPEATING_SLACK: 0} |
53 }, | |
54 classes: | |
55 { | |
56 "@mozilla.org/timer;1": | |
57 { | |
58 createInstance: function() | |
59 { | |
60 return new FakeTimer(); | |
61 } | |
62 }, | 53 }, |
63 }, | 54 classes: |
64 utils: { | 55 { |
65 reportError: function(e) | 56 "@mozilla.org/timer;1": |
66 { | 57 { |
67 console.error(e); | 58 createInstance() |
68 console.trace(); | 59 { |
| 60 return new FakeTimer(); |
| 61 } |
| 62 } |
69 }, | 63 }, |
70 import: function(resource) | 64 utils: |
71 { | 65 { |
72 let match = /^resource:\/\/gre\/modules\/(.+)\.jsm$/.exec(resource); | 66 reportError(e) |
73 let resourceName = match && match[1]; | 67 { |
74 if (resourceName && Cu.import.resources.has(resourceName)) | 68 console.error(e); |
75 return {[resourceName]: Cu.import.resources.get(resourceName)}; | 69 console.trace(); |
76 throw new Error("Attempt to import unknown JavaScript module " + resource)
; | 70 }, |
| 71 import(resource) |
| 72 { |
| 73 let match = /^resource:\/\/gre\/modules\/(.+)\.jsm$/.exec(resource); |
| 74 let resourceName = match && match[1]; |
| 75 if (resourceName && Cu.import.resources.has(resourceName)) |
| 76 return {[resourceName]: Cu.import.resources.get(resourceName)}; |
| 77 throw new Error( |
| 78 "Attempt to import unknown JavaScript module " + resource); |
| 79 } |
77 } | 80 } |
78 }, | 81 }; |
79 }; | |
80 | 82 |
81 const Cc = Components.classes; | 83 const Cc = Components.classes; |
82 const Ci = Components.interfaces; | 84 const Ci = Components.interfaces; |
83 const Cu = Components.utils; | 85 const Cu = Components.utils; |
84 | 86 |
85 Cu.import.resources = new Map(); | 87 Cu.import.resources = new Map(); |
86 | 88 |
87 Cu.import.resources.set("XPCOMUtils", | 89 Cu.import.resources.set("XPCOMUtils", |
88 { | 90 { |
89 generateQI: function() {} | 91 generateQI() {} |
90 }); | 92 }); |
91 | 93 |
92 // | 94 // |
93 // Services.jsm module emulation | 95 // Services.jsm module emulation |
94 // | 96 // |
95 | 97 |
96 Cu.import.resources.set("Services", | 98 Cu.import.resources.set("Services", |
97 { | 99 { |
98 obs: { | 100 obs: { |
99 addObserver: function() {}, | 101 addObserver() {}, |
100 removeObserver: function() {} | 102 removeObserver() {} |
101 }, | 103 }, |
102 vc: { | 104 vc: { |
103 compare: function(v1, v2) | 105 compare(v1, v2) |
104 { | 106 { |
105 function parsePart(s) | 107 function parsePart(s) |
106 { | 108 { |
107 if (!s) | 109 if (!s) |
108 return parsePart("0"); | 110 return parsePart("0"); |
109 | 111 |
110 var part = { | 112 let part = { |
111 numA: 0, | 113 numA: 0, |
112 strB: "", | 114 strB: "", |
113 numC: 0, | 115 numC: 0, |
114 extraD: "" | 116 extraD: "" |
115 }; | 117 }; |
116 | 118 |
117 if (s === "*") | 119 if (s === "*") |
118 { | 120 { |
119 part.numA = Number.MAX_VALUE; | 121 part.numA = Number.MAX_VALUE; |
| 122 return part; |
| 123 } |
| 124 |
| 125 let matches = s.match(/(\d*)(\D*)(\d*)(.*)/); |
| 126 part.numA = parseInt(matches[1], 10) || part.numA; |
| 127 part.strB = matches[2] || part.strB; |
| 128 part.numC = parseInt(matches[3], 10) || part.numC; |
| 129 part.extraD = matches[4] || part.extraD; |
| 130 |
| 131 if (part.strB == "+") |
| 132 { |
| 133 part.numA++; |
| 134 part.strB = "pre"; |
| 135 } |
| 136 |
120 return part; | 137 return part; |
121 } | 138 } |
122 | 139 |
123 var matches = s.match(/(\d*)(\D*)(\d*)(.*)/); | 140 function comparePartElement(s1, s2) |
124 part.numA = parseInt(matches[1], 10) || part.numA; | 141 { |
125 part.strB = matches[2] || part.strB; | 142 if (s1 === "" && s2 !== "") |
126 part.numC = parseInt(matches[3], 10) || part.numC; | 143 return 1; |
127 part.extraD = matches[4] || part.extraD; | 144 if (s1 !== "" && s2 === "") |
128 | 145 return -1; |
129 if (part.strB == "+") | 146 return s1 === s2 ? 0 : (s1 > s2 ? 1 : -1); |
130 { | 147 } |
131 part.numA++; | 148 |
132 part.strB = "pre"; | 149 function compareParts(p1, p2) |
133 } | 150 { |
134 | 151 let result = 0; |
135 return part; | 152 let elements = ["numA", "strB", "numC", "extraD"]; |
136 } | 153 elements.some(element => |
137 | 154 { |
138 function comparePartElement(s1, s2) | 155 result = comparePartElement(p1[element], p2[element]); |
139 { | 156 return result; |
140 if (s1 === "" && s2 !== "") | 157 }); |
141 return 1; | |
142 if (s1 !== "" && s2 === "") | |
143 return -1; | |
144 return s1 === s2 ? 0 : (s1 > s2 ? 1 : -1); | |
145 } | |
146 | |
147 function compareParts(p1, p2) | |
148 { | |
149 var result = 0; | |
150 var elements = ["numA", "strB", "numC", "extraD"]; | |
151 elements.some(function(element) | |
152 { | |
153 result = comparePartElement(p1[element], p2[element]); | |
154 return result; | 158 return result; |
155 }); | 159 } |
156 return result; | 160 |
157 } | 161 let parts1 = v1.split("."); |
158 | 162 let parts2 = v2.split("."); |
159 var parts1 = v1.split("."); | 163 for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) |
160 var parts2 = v2.split("."); | 164 { |
161 for (var i = 0; i < Math.max(parts1.length, parts2.length); i++) | 165 let result = compareParts(parsePart(parts1[i]), parsePart(parts2[i])); |
162 { | 166 if (result) |
163 var result = compareParts(parsePart(parts1[i]), parsePart(parts2[i])); | 167 return result; |
164 if (result) | 168 } |
165 return result; | 169 return 0; |
166 } | 170 } |
167 return 0; | |
168 } | 171 } |
169 } | 172 }); |
170 }); | |
171 | 173 |
172 function FakeTimer() | 174 function FakeTimer() |
173 { | 175 { |
174 } | 176 } |
175 FakeTimer.prototype = | 177 FakeTimer.prototype = |
176 { | 178 { |
177 delay: 0, | 179 delay: 0, |
178 callback: null, | 180 callback: null, |
179 initWithCallback: function(callback, delay) | 181 initWithCallback(callback, delay) |
180 { | 182 { |
181 this.callback = callback; | 183 this.callback = callback; |
182 this.delay = delay; | 184 this.delay = delay; |
183 this.scheduleTimeout(); | 185 this.scheduleTimeout(); |
184 }, | 186 }, |
185 scheduleTimeout: function() | 187 scheduleTimeout() |
186 { | 188 { |
187 var me = this; | 189 setTimeout(() => |
188 setTimeout(function() | |
189 { | 190 { |
190 try | 191 try |
191 { | 192 { |
192 me.callback(); | 193 this.callback(); |
193 } | 194 } |
194 catch(e) | 195 catch (e) |
195 { | 196 { |
196 Cu.reportError(e); | 197 Cu.reportError(e); |
197 } | 198 } |
198 me.scheduleTimeout(); | 199 this.scheduleTimeout(); |
199 }, this.delay); | 200 }, this.delay); |
200 } | 201 } |
201 }; | 202 }; |
202 | 203 |
203 // | 204 // |
204 // Fake XMLHttpRequest implementation | 205 // Fake XMLHttpRequest implementation |
205 // | 206 // |
206 | 207 |
207 function XMLHttpRequest() | 208 function XMLHttpRequest() |
208 { | 209 { |
209 this._requestHeaders = {}; | 210 this._requestHeaders = {}; |
210 this._loadHandlers = []; | 211 this._loadHandlers = []; |
211 this._errorHandlers = []; | 212 this._errorHandlers = []; |
212 }; | 213 } |
213 XMLHttpRequest.prototype = | 214 XMLHttpRequest.prototype = |
214 { | 215 { |
215 _url: null, | 216 _url: null, |
216 _requestHeaders: null, | 217 _requestHeaders: null, |
217 _responseHeaders: null, | 218 _responseHeaders: null, |
218 _loadHandlers: null, | 219 _loadHandlers: null, |
219 _errorHandlers: null, | 220 _errorHandlers: null, |
220 onload: null, | 221 onload: null, |
221 onerror: null, | 222 onerror: null, |
222 status: 0, | 223 status: 0, |
(...skipping 14 matching lines...) Expand all Loading... |
237 "dnt": true, | 238 "dnt": true, |
238 "expect": true, | 239 "expect": true, |
239 "host": true, | 240 "host": true, |
240 "keep-alive": true, | 241 "keep-alive": true, |
241 "origin": true, | 242 "origin": true, |
242 "referer": true, | 243 "referer": true, |
243 "te": true, | 244 "te": true, |
244 "trailer": true, | 245 "trailer": true, |
245 "transfer-encoding": true, | 246 "transfer-encoding": true, |
246 "upgrade": true, | 247 "upgrade": true, |
247 "via": true, | 248 "via": true |
248 }, | 249 }, |
249 _forbiddenRequestHeadersRe: new RegExp("^(Proxy|Sec)-", "i"), | 250 _forbiddenRequestHeadersRe: new RegExp("^(Proxy|Sec)-", "i"), |
250 | 251 |
251 _isRequestHeaderAllowed: function(header) | 252 _isRequestHeaderAllowed(header) |
252 { | 253 { |
253 if (this._forbiddenRequestHeaders.hasOwnProperty(header.toLowerCase())) | 254 if (this._forbiddenRequestHeaders.hasOwnProperty(header.toLowerCase())) |
254 return false; | 255 return false; |
255 if (header.match(this._forbiddenRequestHeadersRe)) | 256 if (header.match(this._forbiddenRequestHeadersRe)) |
256 return false; | 257 return false; |
257 | 258 |
258 return true; | 259 return true; |
259 }, | 260 }, |
260 | 261 |
261 addEventListener: function(eventName, handler, capture) | 262 addEventListener(eventName, handler, capture) |
262 { | 263 { |
263 var list; | 264 let list; |
264 if (eventName == "load") | 265 if (eventName == "load") |
265 list = this._loadHandlers; | 266 list = this._loadHandlers; |
266 else if (eventName == "error") | 267 else if (eventName == "error") |
267 list = this._errorHandlers; | 268 list = this._errorHandlers; |
268 else | 269 else |
269 throw new Error("Event type " + eventName + " not supported"); | 270 throw new Error("Event type " + eventName + " not supported"); |
270 | 271 |
271 if (list.indexOf(handler) < 0) | 272 if (list.indexOf(handler) < 0) |
272 list.push(handler); | 273 list.push(handler); |
273 }, | 274 }, |
274 | 275 |
275 removeEventListener: function(eventName, handler, capture) | 276 removeEventListener(eventName, handler, capture) |
276 { | 277 { |
277 var list; | 278 let list; |
278 if (eventName == "load") | 279 if (eventName == "load") |
279 list = this._loadHandlers; | 280 list = this._loadHandlers; |
280 else if (eventName == "error") | 281 else if (eventName == "error") |
281 list = this._errorHandlers; | 282 list = this._errorHandlers; |
282 else | 283 else |
283 throw new Error("Event type " + eventName + " not supported"); | 284 throw new Error("Event type " + eventName + " not supported"); |
284 | 285 |
285 var index = list.indexOf(handler); | 286 let index = list.indexOf(handler); |
286 if (index >= 0) | 287 if (index >= 0) |
287 list.splice(index, 1); | 288 list.splice(index, 1); |
288 }, | 289 }, |
289 | 290 |
290 open: function(method, url, async, user, password) | 291 open(method, url, async, user, password) |
291 { | 292 { |
292 if (method != "GET") | 293 if (method != "GET") |
293 throw new Error("Only GET requests are currently supported"); | 294 throw new Error("Only GET requests are currently supported"); |
294 if (typeof async != "undefined" && !async) | 295 if (typeof async != "undefined" && !async) |
295 throw new Error("Sync requests are not supported"); | 296 throw new Error("Sync requests are not supported"); |
296 if (typeof user != "undefined" || typeof password != "undefined") | 297 if (typeof user != "undefined" || typeof password != "undefined") |
297 throw new Error("User authentification is not supported"); | 298 throw new Error("User authentification is not supported"); |
298 if (this.readyState != 0) | 299 if (this.readyState != 0) |
299 throw new Error("Already opened"); | 300 throw new Error("Already opened"); |
300 | 301 |
301 this.readyState = 1; | 302 this.readyState = 1; |
302 this._url = url; | 303 this._url = url; |
303 }, | 304 }, |
304 | 305 |
305 send: function(data) | 306 send(data) |
306 { | 307 { |
307 if (this.readyState != 1) | 308 if (this.readyState != 1) |
308 throw new Error("XMLHttpRequest.send() is being called before XMLHttpReque
st.open()"); | 309 throw new Error( |
| 310 "XMLHttpRequest.send() is being called before XMLHttpRequest.open()"); |
309 if (typeof data != "undefined" && data) | 311 if (typeof data != "undefined" && data) |
310 throw new Error("Sending data to server is not supported"); | 312 throw new Error("Sending data to server is not supported"); |
311 | 313 |
312 this.readyState = 3; | 314 this.readyState = 3; |
313 | 315 |
314 var onGetDone = function(result) | 316 let onGetDone = result => |
315 { | 317 { |
316 this.channel.status = result.status; | 318 this.channel.status = result.status; |
317 this.status = result.responseStatus; | 319 this.status = result.responseStatus; |
318 this.responseText = result.responseText; | 320 this.responseText = result.responseText; |
319 this._responseHeaders = result.responseHeaders; | 321 this._responseHeaders = result.responseHeaders; |
320 this.readyState = 4; | 322 this.readyState = 4; |
321 | 323 |
322 // Notify event listeners | 324 // Notify event listeners |
323 const NS_OK = 0; | 325 const NS_OK = 0; |
324 var eventName = (this.channel.status == NS_OK ? "load" : "error"); | 326 let eventName = (this.channel.status == NS_OK ? "load" : "error"); |
325 var event = {type: eventName}; | 327 let event = {type: eventName}; |
326 | 328 |
327 if (this["on" + eventName]) | 329 if (this["on" + eventName]) |
328 this["on" + eventName].call(this, event); | 330 this["on" + eventName].call(this, event); |
329 | 331 |
330 var list = this["_" + eventName + "Handlers"]; | 332 let list = this["_" + eventName + "Handlers"]; |
331 for (var i = 0; i < list.length; i++) | 333 for (let i = 0; i < list.length; i++) |
332 list[i].call(this, event); | 334 list[i].call(this, event); |
333 }.bind(this); | 335 }; |
334 // HACK (#5066): the code checking whether the connection is allowed is temp
orary, | 336 // HACK (#5066): the code checking whether the connection is |
335 // the actual check should be in the core when we make a decision whether | 337 // allowed is temporary, the actual check should be in the core |
336 // to update a subscription with current connection or not, thus whether to | 338 // when we make a decision whether to update a subscription with |
337 // even construct XMLHttpRequest object or not. | 339 // current connection or not, thus whether to even construct |
338 _isSubscriptionDownloadAllowed(function(isAllowed) | 340 // XMLHttpRequest object or not. |
| 341 _isSubscriptionDownloadAllowed(isAllowed => |
339 { | 342 { |
340 if (!isAllowed) | 343 if (!isAllowed) |
341 { | 344 { |
342 onGetDone({ | 345 onGetDone({ |
343 status: 0x804b000d, //NS_ERROR_CONNECTION_REFUSED; | 346 status: 0x804b000d, // NS_ERROR_CONNECTION_REFUSED; |
344 responseStatus: 0 | 347 responseStatus: 0 |
345 }); | 348 }); |
346 return; | 349 return; |
347 } | 350 } |
348 window._webRequest.GET(this._url, this._requestHeaders, onGetDone); | 351 window._webRequest.GET(this._url, this._requestHeaders, onGetDone); |
349 }.bind(this)); | 352 }); |
350 }, | 353 }, |
351 | 354 |
352 overrideMimeType: function(mime) | 355 overrideMimeType(mime) |
353 { | 356 { |
354 }, | 357 }, |
355 | 358 |
356 setRequestHeader: function(name, value) | 359 setRequestHeader(name, value) |
357 { | 360 { |
358 if (this.readyState > 1) | 361 if (this.readyState > 1) |
359 throw new Error("Cannot set request header after sending"); | 362 throw new Error("Cannot set request header after sending"); |
360 | 363 |
361 if (this._isRequestHeaderAllowed(name)) | 364 if (this._isRequestHeaderAllowed(name)) |
362 this._requestHeaders[name] = value; | 365 this._requestHeaders[name] = value; |
363 else | 366 else |
364 console.warn("Attempt to set a forbidden header was denied: " + name); | 367 console.warn("Attempt to set a forbidden header was denied: " + name); |
365 }, | 368 }, |
366 | 369 |
367 getResponseHeader: function(name) | 370 getResponseHeader(name) |
368 { | 371 { |
369 name = name.toLowerCase(); | 372 name = name.toLowerCase(); |
370 if (!this._responseHeaders || !this._responseHeaders.hasOwnProperty(name)) | 373 if (!this._responseHeaders || !this._responseHeaders.hasOwnProperty(name)) |
371 return null; | 374 return null; |
372 else | 375 return this._responseHeaders[name]; |
373 return this._responseHeaders[name]; | |
374 }, | 376 }, |
375 | 377 |
376 channel: | 378 channel: |
377 { | 379 { |
378 status: -1, | 380 status: -1, |
379 notificationCallbacks: {}, | 381 notificationCallbacks: {}, |
380 loadFlags: 0, | 382 loadFlags: 0, |
381 INHIBIT_CACHING: 0, | 383 INHIBIT_CACHING: 0, |
382 VALIDATE_ALWAYS: 0, | 384 VALIDATE_ALWAYS: 0, |
383 QueryInterface: function() | 385 QueryInterface() |
384 { | 386 { |
385 return this; | 387 return this; |
386 } | 388 } |
387 } | 389 } |
388 }; | 390 }; |
389 | 391 |
390 function _isSubscriptionDownloadAllowed(callback) { | 392 function _isSubscriptionDownloadAllowed(callback) |
| 393 { |
391 // It's a bit hacky, JsEngine interface which is used by FilterEngine does | 394 // It's a bit hacky, JsEngine interface which is used by FilterEngine does |
392 // not allow to inject an arbitrary callback, so we use triggerEvent | 395 // not allow to inject an arbitrary callback, so we use triggerEvent |
393 // mechanism. | 396 // mechanism. |
394 // Yet one hack (#5039). | 397 // Yet one hack (#5039). |
395 var allowed_connection_type = require("prefs").Prefs.allowed_connection_type; | 398 let allowedConnectionType = require("prefs").Prefs.allowed_connection_type; |
396 if (allowed_connection_type == "") | 399 if (allowedConnectionType == "") |
397 allowed_connection_type = null; | 400 allowedConnectionType = null; |
398 _triggerEvent("_isSubscriptionDownloadAllowed", allowed_connection_type, callb
ack); | 401 _triggerEvent("_isSubscriptionDownloadAllowed", allowedConnectionType, |
| 402 callback); |
399 } | 403 } |
400 | |
401 // Polyfill Array.prototype.find | |
402 // from https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Ob
jects/Array/find | |
403 // https://tc39.github.io/ecma262/#sec-array.prototype.find | |
404 if (!Array.prototype.find) { | |
405 Object.defineProperty(Array.prototype, 'find', { | |
406 value: function (predicate) { | |
407 // 1. Let O be ? ToObject(this value). | |
408 if (this == null) { | |
409 throw new TypeError('"this" is null or not defined'); | |
410 } | |
411 | |
412 var o = Object(this); | |
413 | |
414 // 2. Let len be ? ToLength(? Get(O, "length")). | |
415 var len = o.length >>> 0; | |
416 | |
417 // 3. If IsCallable(predicate) is false, throw a TypeError exception. | |
418 if (typeof predicate !== 'function') { | |
419 throw new TypeError('predicate must be a function'); | |
420 } | |
421 | |
422 // 4. If thisArg was supplied, let T be thisArg; else let T be undefined. | |
423 var thisArg = arguments[1]; | |
424 | |
425 // 5. Let k be 0. | |
426 var k = 0; | |
427 | |
428 // 6. Repeat, while k < len | |
429 while (k < len) { | |
430 // a. Let Pk be ! ToString(k). | |
431 // b. Let kValue be ? Get(O, Pk). | |
432 // c. Let testResult be ToBoolean(? Call(predicate, T, "kValue, k, O")). | |
433 // d. If testResult is true, return kValue. | |
434 var kValue = o[k]; | |
435 if (predicate.call(thisArg, kValue, k, o)) { | |
436 return kValue; | |
437 } | |
438 // e. Increase k by 1. | |
439 k++; | |
440 } | |
441 } | |
442 }); | |
443 } | |
OLD | NEW |