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