Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Delta Between Two Patch Sets: lib/filterComposer.js

Issue 29370947: Issue 3138 - Improve how context menu "block element" handles iframes (Closed)
Left Patch Set: Addressed feedback Created Jan. 12, 2017, 7:14 a.m.
Right Patch Set: Use messaging instead of requiring the info module Created Oct. 19, 2017, 10:52 a.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
Left: Side by side diff | Download
Right: Side by side diff | Download
« no previous file with change/comment | « ext/background.js ('k') | no next file » | no next file with change/comment »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
LEFTRIGHT
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
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 });
LEFTRIGHT

Powered by Google App Engine
This is Rietveld