LEFT | RIGHT |
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-2016 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 /** @module filterComposer */ | 18 /** @module filterComposer */ |
19 | 19 |
20 "use strict"; | 20 "use strict"; |
21 | 21 |
22 let {defaultMatcher} = require("matcher"); | 22 const {defaultMatcher} = require("matcher"); |
23 let {RegExpFilter} = require("filterClasses"); | 23 const {RegExpFilter} = require("filterClasses"); |
24 let {FilterNotifier} = require("filterNotifier"); | 24 const {FilterNotifier} = require("filterNotifier"); |
25 let {Prefs} = require("prefs"); | 25 const {Prefs} = require("prefs"); |
26 let {extractHostFromFrame, stringifyURL, isThirdParty} = require("url"); | 26 const {extractHostFromFrame, stringifyURL, isThirdParty} = require("url"); |
27 let {getKey, checkWhitelisted} = require("whitelisting"); | 27 const {getKey, checkWhitelisted} = require("whitelisting"); |
28 let {port} = require("messaging"); | 28 const {port} = require("messaging"); |
| 29 const info = require("info"); |
29 | 30 |
30 let readyPages = new ext.PageMap(); | 31 let readyPages = new ext.PageMap(); |
31 | 32 |
32 /** | 33 /** |
33 * Checks whether the given page is ready to use the filter composer | 34 * Checks whether the given page is ready to use the filter composer |
34 * | 35 * |
35 * @param {Page} page | 36 * @param {Page} page |
36 * @return {boolean} | 37 * @return {boolean} |
37 */ | 38 */ |
38 exports.isPageReady = function(page) | 39 exports.isPageReady = page => |
39 { | 40 { |
40 return readyPages.has(page); | 41 return readyPages.has(page); |
41 }; | 42 }; |
42 | 43 |
43 function isValidString(s) { | 44 function isValidString(s) |
| 45 { |
44 return s && s.indexOf("\0") == -1; | 46 return s && s.indexOf("\0") == -1; |
45 } | 47 } |
46 | 48 |
47 function escapeChar(chr) | 49 function escapeChar(chr) |
48 { | 50 { |
49 let code = chr.charCodeAt(0); | 51 let code = chr.charCodeAt(0); |
50 | 52 |
51 // Control characters and leading digits must be escaped based on | 53 // Control characters and leading digits must be escaped based on |
52 // their char code in CSS. Moreover, curly brackets aren't allowed | 54 // their char code in CSS. Moreover, curly brackets aren't allowed |
53 // in elemhide filters, and therefore must be escaped based on their | 55 // in elemhide filters, and therefore must be escaped based on their |
54 // char code as well. | 56 // char code as well. |
55 if (code <= 0x1F || code == 0x7F || /[\d\{\}]/.test(chr)) | 57 if (code <= 0x1F || code == 0x7F || /[\d{}]/.test(chr)) |
56 return "\\" + code.toString(16) + " "; | 58 return "\\" + code.toString(16) + " "; |
57 | 59 |
58 return "\\" + chr; | 60 return "\\" + chr; |
59 } | 61 } |
60 | 62 |
61 let escapeCSS = | 63 let escapeCSS = |
62 /** | 64 /** |
63 * Escapes a token (e.g. tag, id, class or attribute) to be used in CSS selector
s. | 65 * Escapes a token (e.g. tag, id, class or attribute) to be used in |
| 66 * CSS selectors. |
64 * | 67 * |
65 * @param {string} s | 68 * @param {string} s |
66 * @return {string} | 69 * @return {string} |
67 * @static | 70 * @static |
68 */ | 71 */ |
69 exports.escapeCSS = function(s) | 72 exports.escapeCSS = s => |
70 { | 73 { |
71 return s.replace(/^[\d\-]|[^\w\-\u0080-\uFFFF]/g, escapeChar); | 74 return s.replace(/^[\d-]|[^\w\-\u0080-\uFFFF]/g, escapeChar); |
72 }; | 75 }; |
73 | 76 |
74 let quoteCSS = | 77 let quoteCSS = |
75 /** | 78 /** |
76 * Quotes a string to be used as attribute value in CSS selectors. | 79 * Quotes a string to be used as attribute value in CSS selectors. |
77 * | 80 * |
78 * @param {string} value | 81 * @param {string} value |
79 * @return {string} | 82 * @return {string} |
80 * @static | 83 * @static |
81 */ | 84 */ |
82 exports.quoteCSS = function(value) | 85 exports.quoteCSS = value => |
83 { | 86 { |
84 return '"' + value.replace(/["\\\{\}\x00-\x1F\x7F]/g, escapeChar) + '"'; | 87 return '"' + value.replace(/["\\{}\x00-\x1F\x7F]/g, escapeChar) + '"'; |
85 }; | 88 }; |
86 | 89 |
87 function composeFilters(details) | 90 function composeFilters(details) |
88 { | 91 { |
| 92 let {page, frame} = details; |
89 let filters = []; | 93 let filters = []; |
90 let selectors = []; | 94 let selectors = []; |
91 | 95 |
92 let page = details.page; | |
93 let frame = details.frame; | |
94 | |
95 if (!checkWhitelisted(page, frame)) | 96 if (!checkWhitelisted(page, frame)) |
96 { | 97 { |
97 let typeMask = RegExpFilter.typeMap[details.type]; | 98 let typeMask = RegExpFilter.typeMap[details.type]; |
98 let docDomain = extractHostFromFrame(frame); | 99 let docDomain = extractHostFromFrame(frame); |
99 let specificOnly = checkWhitelisted(page, frame, RegExpFilter.typeMap.GENERI
CBLOCK); | 100 let specificOnly = checkWhitelisted(page, frame, |
| 101 RegExpFilter.typeMap.GENERICBLOCK); |
100 | 102 |
101 // Add a blocking filter for each URL of the element that can be blocked | 103 // Add a blocking filter for each URL of the element that can be blocked |
102 for (let url of details.urls) | 104 for (let url of details.urls) |
103 { | 105 { |
104 let urlObj = new URL(url, details.baseURL); | 106 let urlObj = new URL(url, details.baseURL); |
105 url = stringifyURL(urlObj); | 107 url = stringifyURL(urlObj); |
106 | 108 |
107 let filter = defaultMatcher.whitelist.matchesAny( | 109 let filter = defaultMatcher.whitelist.matchesAny( |
108 url, typeMask, docDomain, | 110 url, typeMask, docDomain, |
109 isThirdParty(urlObj, docDomain), | 111 isThirdParty(urlObj, docDomain), |
110 getKey(page, frame), specificOnly | 112 getKey(page, frame), specificOnly |
111 ); | 113 ); |
112 | 114 |
113 if (!filter) | 115 if (!filter) |
114 { | 116 { |
115 let filterText = url.replace(/^[\w\-]+:\/+(?:www\.)?/, "||"); | 117 let filterText = url.replace(/^[\w-]+:\/+(?:www\.)?/, "||"); |
116 | 118 |
117 if (specificOnly) | 119 if (specificOnly) |
118 filterText += "$domain=" + docDomain; | 120 filterText += "$domain=" + docDomain; |
119 | 121 |
120 if (filters.indexOf(filterText) == -1) | 122 if (!filters.includes(filterText)) |
121 filters.push(filterText); | 123 filters.push(filterText); |
122 } | 124 } |
123 } | 125 } |
124 | 126 |
125 // If we couldn't generate any blocking filters, fallback to element hiding | 127 // If we couldn't generate any blocking filters, fallback to element hiding |
126 let selectors = []; | 128 if (filters.length == 0 && !checkWhitelisted(page, frame, |
127 if (filters.length == 0 && !checkWhitelisted(page, frame, RegExpFilter.typeM
ap.ELEMHIDE)) | 129 RegExpFilter.typeMap.ELEMHIDE)) |
128 { | 130 { |
129 // Generate CSS selectors based on the element's "id" and "class" attribut
e | 131 // Generate CSS selectors based on the element's "id" and |
| 132 // "class" attribute. |
130 if (isValidString(details.id)) | 133 if (isValidString(details.id)) |
131 selectors.push("#" + escapeCSS(details.id)); | 134 selectors.push("#" + escapeCSS(details.id)); |
132 | 135 |
133 let classes = details.classes.filter(isValidString); | 136 let classes = details.classes.filter(isValidString); |
134 if (classes.length > 0) | 137 if (classes.length > 0) |
135 selectors.push(classes.map(c => "." + escapeCSS(c)).join("")); | 138 selectors.push(classes.map(c => "." + escapeCSS(c)).join("")); |
136 | 139 |
137 // If there is a "src" attribute, specifiying a URL that we can't block, | 140 // If there is a "src" attribute, specifiying a URL that we can't block, |
138 // generate a CSS selector matching the "src" attribute | 141 // generate a CSS selector matching the "src" attribute |
139 if (isValidString(details.src)) | 142 if (isValidString(details.src)) |
140 selectors.push(escapeCSS(details.tagName) + "[src=" + quoteCSS(details.s
rc) + "]"); | 143 { |
141 | 144 selectors.push( |
142 // As last resort, if there is a "style" attribute, and we couldn't genera
te | 145 escapeCSS(details.tagName) + "[src=" + quoteCSS(details.src) + "]" |
143 // any filters so far, generate a CSS selector matching the "style" attrib
ute | 146 ); |
144 if (isValidString(details.style) && selectors.length == 0 && filters.lengt
h == 0) | 147 } |
145 selectors.push(escapeCSS(details.tagName) + "[style=" + quoteCSS(details
.style) + "]"); | 148 |
| 149 // As last resort, if there is a "style" attribute, and we |
| 150 // couldn't generate any filters so far, generate a CSS selector |
| 151 // matching the "style" attribute |
| 152 if (isValidString(details.style) && selectors.length == 0 && |
| 153 filters.length == 0) |
| 154 { |
| 155 selectors.push( |
| 156 escapeCSS(details.tagName) + "[style=" + quoteCSS(details.style) + "]" |
| 157 ); |
| 158 } |
146 | 159 |
147 // Add an element hiding filter for each generated CSS selector | 160 // Add an element hiding filter for each generated CSS selector |
148 for (let selector of selectors) | 161 for (let selector of selectors) |
149 filters.push(docDomain.replace(/^www\./, "") + "##" + selector); | 162 filters.push(docDomain.replace(/^www\./, "") + "##" + selector); |
150 } | 163 } |
151 } | 164 } |
152 | 165 |
153 return {filters: filters, selectors: selectors}; | 166 return {filters, selectors}; |
154 } | 167 } |
155 | 168 |
156 let contextMenuItem = { | 169 let contextMenuItem = { |
157 title: ext.i18n.getMessage("block_element"), | 170 title: browser.i18n.getMessage("block_element"), |
158 contexts: ["image", "video", "audio"], | 171 contexts: ["image", "video", "audio"], |
159 onclick: (page, info) => | 172 onclick(page, info) |
160 { | 173 { |
161 page.sendMessage( | 174 page.sendMessage( |
162 {type: "composer.content.contextMenuClicked"}, undefined, info.frameId | 175 {type: "composer.content.contextMenuClicked"}, undefined, info.frameId |
163 ); | 176 ); |
164 } | 177 } |
165 }; | 178 }; |
166 | 179 |
167 function updateContextMenu(page, filter) | 180 function updateContextMenu(page, filter) |
168 { | 181 { |
169 page.contextMenus.remove(contextMenuItem); | 182 page.contextMenus.remove(contextMenuItem); |
170 | 183 |
171 if (typeof filter == "undefined") | 184 if (typeof filter == "undefined") |
172 filter = checkWhitelisted(page); | 185 filter = checkWhitelisted(page); |
173 if (!filter && Prefs.shouldShowBlockElementMenu && readyPages.has(page)) | 186 |
| 187 // We don't support the filter composer on Firefox for Android, because the |
| 188 // user experience on mobile is quite different. |
| 189 if (info.application != "fennec" && |
| 190 !filter && Prefs.shouldShowBlockElementMenu && readyPages.has(page)) |
| 191 { |
174 page.contextMenus.create(contextMenuItem); | 192 page.contextMenus.create(contextMenuItem); |
| 193 } |
175 } | 194 } |
176 | 195 |
177 FilterNotifier.on("page.WhitelistingStateRevalidate", updateContextMenu); | 196 FilterNotifier.on("page.WhitelistingStateRevalidate", updateContextMenu); |
178 | 197 |
179 Prefs.on("shouldShowBlockElementMenu", () => | 198 Prefs.on("shouldShowBlockElementMenu", () => |
180 { | 199 { |
181 ext.pages.query({}, pages => | 200 browser.tabs.query({}, tabs => |
182 { | 201 { |
183 for (let page of pages) | 202 for (let tab of tabs) |
184 updateContextMenu(page); | 203 updateContextMenu(new ext.Page(tab)); |
185 }); | 204 }); |
| 205 }); |
| 206 |
| 207 port.on("composer.isPageReady", (message, sender) => |
| 208 { |
| 209 return readyPages.has(new ext.Page({id: message.pageId})); |
186 }); | 210 }); |
187 | 211 |
188 port.on("composer.ready", (message, sender) => | 212 port.on("composer.ready", (message, sender) => |
189 { | 213 { |
190 readyPages.set(sender.page, null); | 214 readyPages.set(sender.page, null); |
191 updateContextMenu(sender.page); | 215 updateContextMenu(sender.page); |
192 }); | 216 }); |
193 | 217 |
194 port.on("composer.openDialog", (message, sender) => | 218 port.on("composer.openDialog", (message, sender) => |
195 { | 219 { |
196 return new Promise(resolve => | 220 return new Promise(resolve => |
197 { | 221 { |
198 ext.windows.create({ | 222 ext.windows.create({ |
199 url: ext.getURL("composer.html"), | 223 url: browser.extension.getURL("composer.html"), |
200 left: 50, | 224 left: 50, |
201 top: 50, | 225 top: 50, |
202 width: 420, | 226 width: 420, |
203 height: 200, | 227 height: 200, |
204 type: "popup" | 228 type: "popup" |
205 }, | 229 }, |
206 popupPage => | 230 popupPage => |
207 { | 231 { |
208 let popupPageId = popupPage.id; | 232 let popupPageId = popupPage.id; |
209 function onRemoved(removedPageId) | 233 function onRemoved(removedPageId) |
(...skipping 10 matching lines...) Expand all Loading... |
220 ext.pages.onRemoved.addListener(onRemoved); | 244 ext.pages.onRemoved.addListener(onRemoved); |
221 resolve(popupPageId); | 245 resolve(popupPageId); |
222 }); | 246 }); |
223 }); | 247 }); |
224 }); | 248 }); |
225 | 249 |
226 port.on("composer.getFilters", (message, sender) => | 250 port.on("composer.getFilters", (message, sender) => |
227 { | 251 { |
228 return composeFilters({ | 252 return composeFilters({ |
229 tagName: message.tagName, | 253 tagName: message.tagName, |
230 id: message.id, | 254 id: message.id, |
231 src: message.src, | 255 src: message.src, |
232 style: message.style, | 256 style: message.style, |
233 classes: message.classes, | 257 classes: message.classes, |
234 urls: message.urls, | 258 urls: message.urls, |
235 type: message.mediatype, | 259 type: message.mediatype, |
236 baseURL: message.baseURL, | 260 baseURL: message.baseURL, |
237 page: sender.page, | 261 page: sender.page, |
238 frame: sender.frame | 262 frame: sender.frame |
239 }); | 263 }); |
240 }); | 264 }); |
241 | 265 |
| 266 port.on("composer.quoteCSS", (message, sender) => |
| 267 { |
| 268 return quoteCSS(message.CSS); |
| 269 }); |
| 270 |
242 ext.pages.onLoading.addListener(page => | 271 ext.pages.onLoading.addListener(page => |
243 { | 272 { |
244 page.sendMessage({type: "composer.content.finished"}); | 273 page.sendMessage({type: "composer.content.finished"}); |
245 }); | 274 }); |
LEFT | RIGHT |