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 "use strict"; | 18 "use strict"; |
19 | 19 |
20 /** | 20 const {ElemHide} = require("compiled"); |
21 * @fileOverview Element hiding implementation. | |
22 */ | |
23 | 21 |
24 const {ElemHideException} = require("filterClasses"); | 22 exports.ElemHide = ElemHide; |
25 const {FilterNotifier} = require("filterNotifier"); | |
26 | |
27 /** | |
28 * Lookup table, filters by their associated key | |
29 * @type {Object} | |
30 */ | |
31 let filterByKey = []; | |
32 | |
33 /** | |
34 * Lookup table, keys of the filters by filter text | |
35 * @type {Object} | |
36 */ | |
37 let keyByFilter = Object.create(null); | |
38 | |
39 /** | |
40 * Nested lookup table, filter (or false if inactive) by filter key by domain. | |
41 * (Only contains filters that aren't unconditionally matched for all domains.) | |
42 * @type {Object} | |
43 */ | |
44 let filtersByDomain = Object.create(null); | |
45 | |
46 /** | |
47 * Lookup table, filter key by selector. (Only used for selectors that are | |
48 * unconditionally matched for all domains.) | |
49 */ | |
50 let filterKeyBySelector = Object.create(null); | |
51 | |
52 /** | |
53 * This array caches the keys of filterKeyBySelector table (selectors which | |
54 * unconditionally apply on all domains). It will be null if the cache needs to | |
55 * be rebuilt. | |
56 */ | |
57 let unconditionalSelectors = null; | |
58 | |
59 /** | |
60 * This array caches the values of filterKeyBySelector table (filterIds for | |
61 * selectors which unconditionally apply on all domains). It will be null if the | |
62 * cache needs to be rebuilt. | |
63 */ | |
64 let unconditionalFilterKeys = null; | |
65 | |
66 /** | |
67 * Object to be used instead when a filter has a blank domains property. | |
68 */ | |
69 let defaultDomains = Object.create(null); | |
70 defaultDomains[""] = true; | |
71 | |
72 /** | |
73 * Lookup table, keys are known element hiding exceptions | |
74 * @type {Object} | |
75 */ | |
76 let knownExceptions = Object.create(null); | |
77 | |
78 /** | |
79 * Lookup table, lists of element hiding exceptions by selector | |
80 * @type {Object} | |
81 */ | |
82 let exceptions = Object.create(null); | |
83 | |
84 /** | |
85 * Container for element hiding filters | |
86 * @class | |
87 */ | |
88 let ElemHide = exports.ElemHide = { | |
89 /** | |
90 * Removes all known filters | |
91 */ | |
92 clear() | |
93 { | |
94 filterByKey = []; | |
95 keyByFilter = Object.create(null); | |
96 filtersByDomain = Object.create(null); | |
97 filterKeyBySelector = Object.create(null); | |
98 unconditionalSelectors = unconditionalFilterKeys = null; | |
99 knownExceptions = Object.create(null); | |
100 exceptions = Object.create(null); | |
101 FilterNotifier.emit("elemhideupdate"); | |
102 }, | |
103 | |
104 _addToFiltersByDomain(key, filter) | |
105 { | |
106 let domains = filter.domains || defaultDomains; | |
107 for (let domain in domains) | |
108 { | |
109 let filters = filtersByDomain[domain]; | |
110 if (!filters) | |
111 filters = filtersByDomain[domain] = Object.create(null); | |
112 | |
113 if (domains[domain]) | |
114 filters[key] = filter; | |
115 else | |
116 filters[key] = false; | |
117 } | |
118 }, | |
119 | |
120 /** | |
121 * Add a new element hiding filter | |
122 * @param {ElemHideFilter} filter | |
123 */ | |
124 add(filter) | |
125 { | |
126 if (filter instanceof ElemHideException) | |
127 { | |
128 if (filter.text in knownExceptions) | |
129 return; | |
130 | |
131 let {selector} = filter; | |
132 if (!(selector in exceptions)) | |
133 exceptions[selector] = []; | |
134 exceptions[selector].push(filter); | |
135 | |
136 // If this is the first exception for a previously unconditionally | |
137 // applied element hiding selector we need to take care to update the | |
138 // lookups. | |
139 let filterKey = filterKeyBySelector[selector]; | |
140 if (typeof filterKey != "undefined") | |
141 { | |
142 this._addToFiltersByDomain(filterKey, filterByKey[filterKey]); | |
143 delete filterKeyBySelector[selector]; | |
144 unconditionalSelectors = unconditionalFilterKeys = null; | |
145 } | |
146 | |
147 knownExceptions[filter.text] = true; | |
148 } | |
149 else | |
150 { | |
151 if (filter.text in keyByFilter) | |
152 return; | |
153 | |
154 let key = filterByKey.push(filter) - 1; | |
155 keyByFilter[filter.text] = key; | |
156 | |
157 if (!(filter.domains || filter.selector in exceptions)) | |
158 { | |
159 // The new filter's selector is unconditionally applied to all domains | |
160 filterKeyBySelector[filter.selector] = key; | |
161 unconditionalSelectors = unconditionalFilterKeys = null; | |
162 } | |
163 else | |
164 { | |
165 // The new filter's selector only applies to some domains | |
166 this._addToFiltersByDomain(key, filter); | |
167 } | |
168 } | |
169 | |
170 FilterNotifier.emit("elemhideupdate"); | |
171 }, | |
172 | |
173 _removeFilterKey(key, filter) | |
174 { | |
175 if (filterKeyBySelector[filter.selector] == key) | |
176 { | |
177 delete filterKeyBySelector[filter.selector]; | |
178 unconditionalSelectors = unconditionalFilterKeys = null; | |
179 return; | |
180 } | |
181 | |
182 // We haven't found this filter in unconditional filters, look in | |
183 // filtersByDomain. | |
184 let domains = filter.domains || defaultDomains; | |
185 for (let domain in domains) | |
186 { | |
187 let filters = filtersByDomain[domain]; | |
188 if (filters) | |
189 delete filters[key]; | |
190 } | |
191 }, | |
192 | |
193 /** | |
194 * Removes an element hiding filter | |
195 * @param {ElemHideFilter} filter | |
196 */ | |
197 remove(filter) | |
198 { | |
199 if (filter instanceof ElemHideException) | |
200 { | |
201 if (!(filter.text in knownExceptions)) | |
202 return; | |
203 | |
204 let list = exceptions[filter.selector]; | |
205 let index = list.indexOf(filter); | |
206 if (index >= 0) | |
207 list.splice(index, 1); | |
208 delete knownExceptions[filter.text]; | |
209 } | |
210 else | |
211 { | |
212 if (!(filter.text in keyByFilter)) | |
213 return; | |
214 | |
215 let key = keyByFilter[filter.text]; | |
216 delete filterByKey[key]; | |
217 delete keyByFilter[filter.text]; | |
218 this._removeFilterKey(key, filter); | |
219 } | |
220 | |
221 FilterNotifier.emit("elemhideupdate"); | |
222 }, | |
223 | |
224 /** | |
225 * Checks whether an exception rule is registered for a filter on a particular | |
226 * domain. | |
227 * @param {Filter} filter | |
228 * @param {string} docDomain | |
229 * @return {ElemHideException} | |
230 */ | |
231 getException(filter, docDomain) | |
232 { | |
233 if (!(filter.selector in exceptions)) | |
234 return null; | |
235 | |
236 let list = exceptions[filter.selector]; | |
237 for (let i = list.length - 1; i >= 0; i--) | |
238 { | |
239 if (list[i].isActiveOnDomain(docDomain)) | |
240 return list[i]; | |
241 } | |
242 | |
243 return null; | |
244 }, | |
245 | |
246 /** | |
247 * Retrieves an element hiding filter by the corresponding protocol key | |
248 * @param {number} key | |
249 * @return {Filter} | |
250 */ | |
251 getFilterByKey(key) | |
252 { | |
253 return (key in filterByKey ? filterByKey[key] : null); | |
254 }, | |
255 | |
256 /** | |
257 * Returns a list of all selectors as a nested map. On first level, the keys | |
258 * are all values of `ElemHideBase.selectorDomain` (domains on which these | |
259 * selectors should apply, ignoring exceptions). The values are maps again, | |
260 * with the keys being selectors and values the corresponding filter keys. | |
261 * @returns {Map.<String,Map<String,String>>} | |
262 */ | |
263 getSelectors() | |
264 { | |
265 let domains = new Map(); | |
266 for (let key in filterByKey) | |
267 { | |
268 let filter = filterByKey[key]; | |
269 if (!filter.selector) | |
270 continue; | |
271 | |
272 let domain = filter.selectorDomain || ""; | |
273 | |
274 if (!domains.has(domain)) | |
275 domains.set(domain, new Map()); | |
276 domains.get(domain).set(filter.selector, key); | |
277 } | |
278 | |
279 return domains; | |
280 }, | |
281 | |
282 /** | |
283 * Returns a list of selectors that apply on each website unconditionally. | |
284 * @returns {string[]} | |
285 */ | |
286 getUnconditionalSelectors() | |
287 { | |
288 if (!unconditionalSelectors) | |
289 unconditionalSelectors = Object.keys(filterKeyBySelector); | |
290 return unconditionalSelectors.slice(); | |
291 }, | |
292 | |
293 /** | |
294 * Returns a list of filter keys for selectors which apply to all websites | |
295 * without exception. | |
296 * @returns {number[]} | |
297 */ | |
298 getUnconditionalFilterKeys() | |
299 { | |
300 if (!unconditionalFilterKeys) | |
301 { | |
302 let selectors = this.getUnconditionalSelectors(); | |
303 unconditionalFilterKeys = []; | |
304 for (let selector of selectors) | |
305 unconditionalFilterKeys.push(filterKeyBySelector[selector]); | |
306 } | |
307 return unconditionalFilterKeys.slice(); | |
308 }, | |
309 | |
310 | |
311 /** | |
312 * Constant used by getSelectorsForDomain to return all selectors applying to | |
313 * a particular hostname. | |
314 */ | |
315 ALL_MATCHING: 0, | |
316 | |
317 /** | |
318 * Constant used by getSelectorsForDomain to exclude selectors which apply to | |
319 * all websites without exception. | |
320 */ | |
321 NO_UNCONDITIONAL: 1, | |
322 | |
323 /** | |
324 * Constant used by getSelectorsForDomain to return only selectors for filters | |
325 * which specifically match the given host name. | |
326 */ | |
327 SPECIFIC_ONLY: 2, | |
328 | |
329 /** | |
330 * Determines from the current filter list which selectors should be applied | |
331 * on a particular host name. Optionally returns the corresponding filter | |
332 * keys. | |
333 * @param {string} domain | |
334 * @param {number} [criteria] | |
335 * One of the following: ElemHide.ALL_MATCHING, ElemHide.NO_UNCONDITIONAL or | |
336 * ElemHide.SPECIFIC_ONLY. | |
337 * @param {boolean} [provideFilterKeys] | |
338 * If true, the function will return a list of corresponding filter keys in | |
339 * addition to selectors. | |
340 * @returns {string[]|Array.<string[]>} | |
341 * List of selectors or an array with two elements (list of selectors and | |
342 * list of corresponding keys) if provideFilterKeys is true. | |
343 */ | |
344 getSelectorsForDomain(domain, criteria, provideFilterKeys) | |
345 { | |
346 let filterKeys = []; | |
347 let selectors = []; | |
348 | |
349 if (typeof criteria == "undefined") | |
350 criteria = ElemHide.ALL_MATCHING; | |
351 if (criteria < ElemHide.NO_UNCONDITIONAL) | |
352 { | |
353 selectors = this.getUnconditionalSelectors(); | |
354 if (provideFilterKeys) | |
355 filterKeys = this.getUnconditionalFilterKeys(); | |
356 } | |
357 | |
358 let specificOnly = (criteria >= ElemHide.SPECIFIC_ONLY); | |
359 let seenFilters = Object.create(null); | |
360 let currentDomain = domain ? domain.toUpperCase() : ""; | |
361 while (true) | |
362 { | |
363 if (specificOnly && currentDomain == "") | |
364 break; | |
365 | |
366 let filters = filtersByDomain[currentDomain]; | |
367 if (filters) | |
368 { | |
369 for (let filterKey in filters) | |
370 { | |
371 if (filterKey in seenFilters) | |
372 continue; | |
373 seenFilters[filterKey] = true; | |
374 | |
375 let filter = filters[filterKey]; | |
376 if (filter && !this.getException(filter, domain)) | |
377 { | |
378 selectors.push(filter.selector); | |
379 // It is faster to always push the key, even if not required. | |
380 filterKeys.push(filterKey); | |
381 } | |
382 } | |
383 } | |
384 | |
385 if (currentDomain == "") | |
386 break; | |
387 | |
388 let nextDot = currentDomain.indexOf("."); | |
389 currentDomain = nextDot == -1 ? "" : currentDomain.substr(nextDot + 1); | |
390 } | |
391 | |
392 if (provideFilterKeys) | |
393 return [selectors, filterKeys]; | |
394 return selectors; | |
395 } | |
396 }; | |
OLD | NEW |