| 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 const {RegExpFilter, |  | 
| 21        WhitelistFilter, |  | 
| 22        ElemHideFilter} = require("filterClasses"); |  | 
| 23 const {SpecialSubscription} = require("subscriptionClasses"); |  | 
| 24 const {FilterStorage} = require("filterStorage"); |  | 
| 25 const {defaultMatcher} = require("matcher"); |  | 
| 26 const {FilterNotifier} = require("filterNotifier"); |  | 
| 27 const {extractHostFromFrame} = require("url"); |  | 
| 28 const {port} = require("messaging"); | 20 const {port} = require("messaging"); | 
| 29 | 21 const {DevLogger} = require("devLogger"); | 
| 30 const nonRequestTypes = ["DOCUMENT", "ELEMHIDE", "GENERICBLOCK", "GENERICHIDE"]; |  | 
| 31 |  | 
| 32 // Mapping of inspected tabs to their devpanel page |  | 
| 33 // and recorded items. We can't use a PageMap here, |  | 
| 34 // because data must persist after navigation/reload. |  | 
| 35 let panels = new Map(); |  | 
| 36 |  | 
| 37 function isActivePanel(panel) |  | 
| 38 { |  | 
| 39   return panel && !panel.reload && !panel.reloading; |  | 
| 40 } |  | 
| 41 |  | 
| 42 function getActivePanel(page) |  | 
| 43 { |  | 
| 44   let panel = panels.get(page.id); |  | 
| 45   if (isActivePanel(panel)) |  | 
| 46     return panel; |  | 
| 47   return null; |  | 
| 48 } |  | 
| 49 |  | 
| 50 function getFilterInfo(filter) |  | 
| 51 { |  | 
| 52   if (!filter) |  | 
| 53     return null; |  | 
| 54 |  | 
| 55   let userDefined = false; |  | 
| 56   let subscriptionTitle = null; |  | 
| 57 |  | 
| 58   for (let subscription of filter.subscriptions) |  | 
| 59   { |  | 
| 60     if (!subscription.disabled) |  | 
| 61     { |  | 
| 62       if (subscription instanceof SpecialSubscription) |  | 
| 63         userDefined = true; |  | 
| 64       else |  | 
| 65         subscriptionTitle = subscription.title; |  | 
| 66     } |  | 
| 67   } |  | 
| 68 |  | 
| 69   return { |  | 
| 70     text: filter.text, |  | 
| 71     whitelisted: filter instanceof WhitelistFilter, |  | 
| 72     userDefined, |  | 
| 73     subscription: subscriptionTitle |  | 
| 74   }; |  | 
| 75 } |  | 
| 76 |  | 
| 77 function hasRecord(panel, request, filter) |  | 
| 78 { |  | 
| 79   return panel.records.some(record => |  | 
| 80     record.request.url == request.url && |  | 
| 81     record.request.docDomain == request.docDomain && |  | 
| 82 |  | 
| 83     // Ignore partial (e.g. ELEMHIDE) whitelisting if there is already |  | 
| 84     // a DOCUMENT exception which disables all means of blocking. |  | 
| 85     (record.request.type == "DOCUMENT" ? |  | 
| 86        nonRequestTypes.includes(request.type) : |  | 
| 87        record.request.type == request.type) && |  | 
| 88 |  | 
| 89     // Matched element hiding filters don't relate to a particular request, |  | 
| 90     // so we have to compare the selector in order to avoid duplicates. |  | 
| 91     (record.filter && record.filter.selector) == (filter && filter.selector) |  | 
| 92   ); |  | 
| 93 } |  | 
| 94 |  | 
| 95 function addRecord(panel, request, filter) |  | 
| 96 { |  | 
| 97   if (!hasRecord(panel, request, filter)) |  | 
| 98   { |  | 
| 99     panel.port.postMessage({ |  | 
| 100       type: "add-record", |  | 
| 101       request, |  | 
| 102       filter: getFilterInfo(filter) |  | 
| 103     }); |  | 
| 104 |  | 
| 105     panel.records.push({request, filter}); |  | 
| 106   } |  | 
| 107 } |  | 
| 108 |  | 
| 109 function matchRequest(request) |  | 
| 110 { |  | 
| 111   return defaultMatcher.matchesAny( |  | 
| 112     request.url, |  | 
| 113     RegExpFilter.typeMap[request.type], |  | 
| 114     request.docDomain, |  | 
| 115     request.thirdParty, |  | 
| 116     request.sitekey, |  | 
| 117     request.specificOnly |  | 
| 118   ); |  | 
| 119 } |  | 
| 120 |  | 
| 121 /** |  | 
| 122  * Logs a request to the devtools panel. |  | 
| 123  * |  | 
| 124  * @param {?Page}    page          The page the request occured on or null if |  | 
| 125  *                                 the request isn't associated with a page |  | 
| 126  * @param {string}   url           The URL of the request |  | 
| 127  * @param {string}   type          The request type |  | 
| 128  * @param {string}   docDomain     The IDN-decoded hostname of the document |  | 
| 129  * @param {boolean}  thirdParty    Whether the origin of the request and |  | 
| 130  *                                 document differs |  | 
| 131  * @param {?string}  sitekey       The active sitekey if there is any |  | 
| 132  * @param {?boolean} specificOnly  Whether generic filters should be ignored |  | 
| 133  * @param {?BlockingFilter} filter The matched filter or null if there is no |  | 
| 134  *                                 match |  | 
| 135  */ |  | 
| 136 exports.logRequest = function(page, url, type, docDomain, |  | 
| 137                               thirdParty, sitekey, |  | 
| 138                               specificOnly, filter) |  | 
| 139 { |  | 
| 140   if (panels.size == 0) |  | 
| 141     return; |  | 
| 142 |  | 
| 143   let request = {url, type, docDomain, thirdParty, sitekey, specificOnly}; |  | 
| 144   for (let [tabId, panel] of panels) |  | 
| 145     if ((!page || page.id == tabId) && isActivePanel(panel)) |  | 
| 146       addRecord(panel, request, filter); |  | 
| 147 }; |  | 
| 148 |  | 
| 149 /** |  | 
| 150  * Logs active element hiding filters to the devtools panel. |  | 
| 151  * |  | 
| 152  * @param {Page}     page       The page the elements were hidden on |  | 
| 153  * @param {string[]} selectors  The selectors of applied ElemHideFilters |  | 
| 154  * @param {string[]} filters    The text of applied ElemHideEmulationFilters |  | 
| 155  * @param {string}   docDomain  The IDN-decoded hostname of the document |  | 
| 156  */ |  | 
| 157 function logHiddenElements(page, selectors, filters, docDomain) |  | 
| 158 { |  | 
| 159   let panel = getActivePanel(page); |  | 
| 160   if (panel) |  | 
| 161   { |  | 
| 162     for (let subscription of FilterStorage.subscriptions) |  | 
| 163     { |  | 
| 164       if (subscription.disabled) |  | 
| 165         continue; |  | 
| 166 |  | 
| 167       for (let filter of subscription.filters) |  | 
| 168       { |  | 
| 169         // We only know the exact filter in case of element hiding emulation. |  | 
| 170         // For regular element hiding filters, the content script only knows |  | 
| 171         // the selector, so we have to find a filter that has an identical |  | 
| 172         // selector and is active on the domain the match was reported from. |  | 
| 173         let isActiveElemHideFilter = filter instanceof ElemHideFilter && |  | 
| 174                                      selectors.includes(filter.selector) && |  | 
| 175                                      filter.isActiveOnDomain(docDomain); |  | 
| 176 |  | 
| 177         if (isActiveElemHideFilter || filters.includes(filter.text)) |  | 
| 178           addRecord(panel, {type: "ELEMHIDE", docDomain}, filter); |  | 
| 179       } |  | 
| 180     } |  | 
| 181   } |  | 
| 182 } |  | 
| 183 |  | 
| 184 /** |  | 
| 185  * Logs a whitelisting filter, that disables (some kind of) |  | 
| 186  * blocking for a particular document, to the devtools panel. |  | 
| 187  * |  | 
| 188  * @param {Page}         page      The page the whitelisting is active on |  | 
| 189  * @param {string}       url       The url of the whitelisted document |  | 
| 190  * @param {number}       typeMask  The bit mask of whitelisting types checked |  | 
| 191  *                                 for |  | 
| 192  * @param {string}       docDomain The IDN-decoded hostname of the parent |  | 
| 193  *                                 document |  | 
| 194  * @param {WhitelistFilter} filter The matched whitelisting filter |  | 
| 195  */ |  | 
| 196 exports.logWhitelistedDocument = function(page, url, typeMask, docDomain, |  | 
| 197                                           filter) |  | 
| 198 { |  | 
| 199   let panel = getActivePanel(page); |  | 
| 200   if (panel) |  | 
| 201   { |  | 
| 202     for (let type of nonRequestTypes) |  | 
| 203     { |  | 
| 204       if (typeMask & filter.contentType & RegExpFilter.typeMap[type]) |  | 
| 205         addRecord(panel, {url, type, docDomain}, filter); |  | 
| 206     } |  | 
| 207   } |  | 
| 208 }; |  | 
| 209 |  | 
| 210 /** |  | 
| 211  * Checks whether a page is inspected by the devtools panel. |  | 
| 212  * |  | 
| 213  * @param {Page} page |  | 
| 214  * @return {boolean} |  | 
| 215  */ |  | 
| 216 exports.hasPanel = function(page) |  | 
| 217 { |  | 
| 218   return panels.has(page.id); |  | 
| 219 }; |  | 
| 220 |  | 
| 221 function onBeforeRequest(details) |  | 
| 222 { |  | 
| 223   let panel = panels.get(details.tabId); |  | 
| 224 |  | 
| 225   // Clear the devtools panel and reload the inspected tab without caching |  | 
| 226   // when a new request is issued. However, make sure that we don't end up |  | 
| 227   // in an infinite recursion if we already triggered a reload. |  | 
| 228   if (panel.reloading) |  | 
| 229   { |  | 
| 230     panel.reloading = false; |  | 
| 231   } |  | 
| 232   else |  | 
| 233   { |  | 
| 234     panel.records = []; |  | 
| 235     panel.port.postMessage({type: "reset"}); |  | 
| 236 |  | 
| 237     // We can't repeat the request if it isn't a GET request. Chrome would |  | 
| 238     // prompt the user to confirm reloading the page, and POST requests are |  | 
| 239     // known to cause issues on many websites if repeated. |  | 
| 240     if (details.method == "GET") |  | 
| 241       panel.reload = true; |  | 
| 242   } |  | 
| 243 } |  | 
| 244 |  | 
| 245 function onLoading(page) |  | 
| 246 { |  | 
| 247   let tabId = page.id; |  | 
| 248   let panel = panels.get(tabId); |  | 
| 249 |  | 
| 250   // Reloading the tab is the only way that allows bypassing all caches, in |  | 
| 251   // order to see all requests in the devtools panel. Reloading must not be |  | 
| 252   // performed before the tab changes to "loading", otherwise it will load the |  | 
| 253   // previous URL. |  | 
| 254   if (panel && panel.reload) |  | 
| 255   { |  | 
| 256     browser.tabs.reload(tabId, {bypassCache: true}); |  | 
| 257 |  | 
| 258     panel.reload = false; |  | 
| 259     panel.reloading = true; |  | 
| 260   } |  | 
| 261 } |  | 
| 262 |  | 
| 263 function updateFilters(filters, added) |  | 
| 264 { |  | 
| 265   for (let panel of panels.values()) |  | 
| 266   { |  | 
| 267     for (let i = 0; i < panel.records.length; i++) |  | 
| 268     { |  | 
| 269       let record = panel.records[i]; |  | 
| 270 |  | 
| 271       // If an added filter matches a request shown in the devtools panel, |  | 
| 272       // update that record to show the new filter. Ignore filters that aren't |  | 
| 273       // associated with any sub-resource request. There is no record for these |  | 
| 274       // if they don't already match. In particular, in case of element hiding |  | 
| 275       // filters, we also wouldn't know if any new element matches. |  | 
| 276       if (added) |  | 
| 277       { |  | 
| 278         if (nonRequestTypes.includes(record.request.type)) |  | 
| 279           continue; |  | 
| 280 |  | 
| 281         let filter = matchRequest(record.request); |  | 
| 282         if (!filters.includes(filter)) |  | 
| 283           continue; |  | 
| 284 |  | 
| 285         record.filter = filter; |  | 
| 286       } |  | 
| 287 |  | 
| 288       // If a filter shown in the devtools panel got removed, update that |  | 
| 289       // record to show the filter that matches now, or none, instead. |  | 
| 290       // For filters that aren't associated with any sub-resource request, |  | 
| 291       // just remove the record. We wouldn't know whether another filter |  | 
| 292       // matches instead until the page is reloaded. |  | 
| 293       else |  | 
| 294       { |  | 
| 295         if (!filters.includes(record.filter)) |  | 
| 296           continue; |  | 
| 297 |  | 
| 298         if (nonRequestTypes.includes(record.request.type)) |  | 
| 299         { |  | 
| 300           panel.port.postMessage({ |  | 
| 301             type: "remove-record", |  | 
| 302             index: i |  | 
| 303           }); |  | 
| 304           panel.records.splice(i--, 1); |  | 
| 305           continue; |  | 
| 306         } |  | 
| 307 |  | 
| 308         record.filter = matchRequest(record.request); |  | 
| 309       } |  | 
| 310 |  | 
| 311       panel.port.postMessage({ |  | 
| 312         type: "update-record", |  | 
| 313         index: i, |  | 
| 314         request: record.request, |  | 
| 315         filter: getFilterInfo(record.filter) |  | 
| 316       }); |  | 
| 317     } |  | 
| 318   } |  | 
| 319 } |  | 
| 320 |  | 
| 321 function onFilterAdded(filter) |  | 
| 322 { |  | 
| 323   updateFilters([filter], true); |  | 
| 324 } |  | 
| 325 |  | 
| 326 function onFilterRemoved(filter) |  | 
| 327 { |  | 
| 328   updateFilters([filter], false); |  | 
| 329 } |  | 
| 330 |  | 
| 331 function onSubscriptionAdded(subscription) |  | 
| 332 { |  | 
| 333   if (subscription instanceof SpecialSubscription) |  | 
| 334     updateFilters(subscription.filters, true); |  | 
| 335 } |  | 
| 336 | 22 | 
| 337 browser.runtime.onConnect.addListener(newPort => | 23 browser.runtime.onConnect.addListener(newPort => | 
| 338 { | 24 { | 
| 339   let match = newPort.name.match(/^devtools-(\d+)$/); | 25   let match = newPort.name.match(/^devtools-(\d+)$/); | 
| 340   if (!match) | 26   if (!match) | 
| 341     return; | 27     return; | 
| 342 | 28 | 
| 343   let inspectedTabId = parseInt(match[1], 10); | 29   let inspectedTabId = parseInt(match[1], 10); | 
| 344   let localOnBeforeRequest = onBeforeRequest.bind(); | 30   let onDevLogger = newPort.postMessage.bind(newPort); | 
| 345 | 31 | 
| 346   browser.webRequest.onBeforeRequest.addListener( | 32   DevLogger.on(inspectedTabId, onDevLogger); | 
| 347     localOnBeforeRequest, |  | 
| 348     { |  | 
| 349       urls: ["http://*/*", "https://*/*"], |  | 
| 350       types: ["main_frame"], |  | 
| 351       tabId: inspectedTabId |  | 
| 352     } |  | 
| 353   ); |  | 
| 354 |  | 
| 355   if (panels.size == 0) |  | 
| 356   { |  | 
| 357     ext.pages.onLoading.addListener(onLoading); |  | 
| 358     FilterNotifier.on("filter.added", onFilterAdded); |  | 
| 359     FilterNotifier.on("filter.removed", onFilterRemoved); |  | 
| 360     FilterNotifier.on("subscription.added", onSubscriptionAdded); |  | 
| 361   } |  | 
| 362 |  | 
| 363   newPort.onDisconnect.addListener(() => | 33   newPort.onDisconnect.addListener(() => | 
| 364   { | 34   { | 
| 365     panels.delete(inspectedTabId); | 35     DevLogger.off(inspectedTabId, onDevLogger); | 
| 366     browser.webRequest.onBeforeRequest.removeListener(localOnBeforeRequest); |  | 
| 367 |  | 
| 368     if (panels.size == 0) |  | 
| 369     { |  | 
| 370       ext.pages.onLoading.removeListener(onLoading); |  | 
| 371       FilterNotifier.off("filter.added", onFilterAdded); |  | 
| 372       FilterNotifier.off("filter.removed", onFilterRemoved); |  | 
| 373       FilterNotifier.off("subscription.added", onSubscriptionAdded); |  | 
| 374     } |  | 
| 375   }); | 36   }); | 
| 376 |  | 
| 377   panels.set(inspectedTabId, {port: newPort, records: []}); |  | 
| 378 }); | 37 }); | 
| 379 |  | 
| 380 port.on("devtools.traceElemHide", (message, sender) => |  | 
| 381 { |  | 
| 382   logHiddenElements( |  | 
| 383     sender.page, message.selectors, message.filters, |  | 
| 384     extractHostFromFrame(sender.frame) |  | 
| 385   ); |  | 
| 386 }); |  | 
| OLD | NEW | 
|---|