From 82750cd38169de9c17446dea3953b0708c9dad68 Mon Sep 17 00:00:00 2001
From: Thomas Rientjes <synzvato@protonmail.com>
Date: Mon, 13 Mar 2017 19:29:18 +0100
Subject: [PATCH] Implement injection overview and icon badge

---
 core/interceptor.js              |  59 +++++------
 core/main.js                     |   6 +-
 core/request-analyzer.js         |  12 ++-
 core/state-manager.js            | 104 ++++++++++++++++++
 manifest.json                    |   3 +-
 pages/background/background.html |   1 +
 pages/popup/popup.css            |  75 ++++++++++++-
 pages/popup/popup.html           |  43 ++++----
 pages/popup/popup.js             | 174 +++++++++++++++++++++++++++++++
 9 files changed, 412 insertions(+), 65 deletions(-)
 create mode 100644 core/state-manager.js

diff --git a/core/interceptor.js b/core/interceptor.js
index f4e2d41..2fc65ea 100644
--- a/core/interceptor.js
+++ b/core/interceptor.js
@@ -29,28 +29,11 @@ const HTTP_EXPRESSION = /^http?:\/\//;
  * Public Methods
  */
 
-interceptor.register = function () {
+interceptor.handleRequest = function (requestDetails, tabIdentifier, tabUrl) {
 
-    chrome.tabs.onUpdated.addListener(function (tabIdentifier, changeInformation, tabDetails) {
+    let validCandidate, targetDetails, targetPath, amountInjected;
 
-        if (changeInformation.status === 'loading') {
-
-            chrome.webRequest.onBeforeRequest.addListener(function (requestDetails) {
-                return interceptor._handleRequest(requestDetails, tabIdentifier, tabDetails);
-            }, { 'urls': ['*://*/*'], 'tabId': tabIdentifier }, ['blocking']);
-        }
-    });
-};
-
-/**
- * Private Methods
- */
-
-interceptor._handleRequest = function (requestDetails, tabIdentifier, tabDetails) {
-
-    let validCandidate, targetPath;
-
-    validCandidate = requestAnalyzer.isValidCandidate(requestDetails, tabDetails);
+    validCandidate = requestAnalyzer.isValidCandidate(requestDetails, tabUrl);
 
     if (!validCandidate) {
 
@@ -59,7 +42,8 @@ interceptor._handleRequest = function (requestDetails, tabIdentifier, tabDetails
         };
     }
 
-    targetPath = requestAnalyzer.getLocalTarget(requestDetails);
+    targetDetails = requestAnalyzer.getLocalTarget(requestDetails);
+    targetPath = targetDetails.path;
 
     if (!targetPath) {
         return interceptor._handleMissingCandidate(requestDetails.url);
@@ -69,15 +53,10 @@ interceptor._handleRequest = function (requestDetails, tabIdentifier, tabDetails
         return interceptor._handleMissingCandidate(requestDetails.url);
     }
 
-    chrome.storage.local.get('amountInjected', function (items) {
+    stateManager.registerInjection(tabIdentifier, targetDetails);
 
-        let amountInjected;
-
-        amountInjected = items.amountInjected || 0;
-
-        chrome.storage.local.set({
-            'amountInjected': (parseInt(amountInjected) + 1)
-        });
+    chrome.storage.local.set({
+        'amountInjected': ++interceptor.amountInjected
     });
 
     return {
@@ -85,6 +64,10 @@ interceptor._handleRequest = function (requestDetails, tabIdentifier, tabDetails
     };
 };
 
+/**
+ * Private Methods
+ */
+
 interceptor._handleMissingCandidate = function (requestUrl) {
 
     if (interceptor.blockMissing === true) {
@@ -110,22 +93,28 @@ interceptor._handleMissingCandidate = function (requestUrl) {
     }
 };
 
-interceptor._applyBlockMissingPreference = function () {
+interceptor._handleStorageChanged = function (changes) {
 
-    chrome.storage.local.get('blockMissing', function (items) {
-        interceptor.blockMissing = items.blockMissing || false;
-    });
+    if ('blockMissing' in changes) {
+        interceptor.blockMissing = changes.blockMissing.newValue;
+    }
 };
 
 /**
  * Initializations
  */
 
+interceptor.amountInjected = 0;
 interceptor.blockMissing = false;
-interceptor._applyBlockMissingPreference();
+
+chrome.storage.local.get(['amountInjected', 'blockMissing'], function (items) {
+
+    interceptor.amountInjected = items.amountInjected || 0;
+    interceptor.blockMissing = items.blockMissing || false;
+});
 
 /**
  * Event Handlers
  */
 
-chrome.storage.onChanged.addListener(interceptor._applyBlockMissingPreference);
+chrome.storage.onChanged.addListener(interceptor._handleStorageChanged);
diff --git a/core/main.js b/core/main.js
index 1effe60..863d03e 100644
--- a/core/main.js
+++ b/core/main.js
@@ -17,4 +17,8 @@
  * Initializations
  */
 
-interceptor.register();
+chrome.privacy.network.networkPredictionEnabled.set({'value': false});
+
+chrome.browserAction.setBadgeBackgroundColor({
+    'color': [74, 130, 108, 255]
+});
diff --git a/core/request-analyzer.js b/core/request-analyzer.js
index 182472d..a00d37a 100644
--- a/core/request-analyzer.js
+++ b/core/request-analyzer.js
@@ -79,7 +79,7 @@ requestAnalyzer.getLocalTarget = function (requestDetails) {
     }
 
     // Return either the local target's path or false.
-    return requestAnalyzer._findLocalTarget(resourceMappings, basePath, destinationPath);
+    return requestAnalyzer._findLocalTarget(resourceMappings, basePath, destinationHost, destinationPath);
 };
 
 /**
@@ -98,7 +98,7 @@ requestAnalyzer._matchBasePath = function (hostMappings, channelPath) {
     return false;
 };
 
-requestAnalyzer._findLocalTarget = function (resourceMappings, basePath, channelPath) {
+requestAnalyzer._findLocalTarget = function (resourceMappings, basePath, channelHost, channelPath) {
 
     var resourcePath, versionNumber, resourcePattern;
 
@@ -114,10 +114,14 @@ requestAnalyzer._findLocalTarget = function (resourceMappings, basePath, channel
             let targetPath, localPath;
 
             targetPath = resourceMappings[resourceMold].path;
-            return targetPath.replace(VERSION_PLACEHOLDER, versionNumber);
+            targetPath = targetPath.replace(VERSION_PLACEHOLDER, versionNumber);
 
             // Prepare and return a local target.
-            return localPath;
+            return {
+                source: channelHost,
+                version: versionNumber[0],
+                path: targetPath
+            };
         }
     }
 
diff --git a/core/state-manager.js b/core/state-manager.js
new file mode 100644
index 0000000..5d9db03
--- /dev/null
+++ b/core/state-manager.js
@@ -0,0 +1,104 @@
+/**
+ * State Manager
+ * Belongs to Decentraleyes.
+ *
+ * @author      Thomas Rientjes
+ * @since       2017-03-10
+ * @license     MPL 2.0
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+'use strict';
+
+/**
+ * State Manager
+ */
+
+var stateManager = {};
+
+/**
+ * Public Methods
+ */
+
+stateManager.registerInjection = function (tabIdentifier, injection) {
+
+    let injectionIdentifier, registeredTab, injectionCount;
+
+    injectionIdentifier = injection.source + injection.path + injection.version;
+    registeredTab = stateManager.tabs[tabIdentifier];
+
+    registeredTab.injections[injectionIdentifier] = injection;
+    injectionCount = Object.keys(registeredTab.injections).length || 0;
+
+    if (injectionCount > 0) {
+
+        chrome.browserAction.setBadgeText({
+            tabId: tabIdentifier,
+            text: injectionCount.toString()
+        });
+
+    } else {
+
+        chrome.browserAction.setBadgeText({
+            tabId: tabIdentifier,
+            text: ''
+        });
+    }
+};
+
+/**
+ * Private Methods
+ */
+
+stateManager._createTab = function (tab) {
+
+    let tabIdentifier = tab.id;
+
+    stateManager.tabs[tabIdentifier] = {
+        'injections': {}
+    };
+
+    chrome.webRequest.onBeforeRequest.addListener(function (requestDetails) {
+        return interceptor.handleRequest(requestDetails, tabIdentifier, tab);
+    }, { 'urls': ['*://*/*'], 'tabId': tabIdentifier }, ['blocking']);
+};
+
+stateManager._removeTab = function (tabIdentifier) {
+    delete stateManager.tabs[tabIdentifier];
+};
+
+stateManager._updateTab = function (details) {
+
+    let tabIdentifier = details.tabId;
+
+    if (tabIdentifier !== -1) {
+
+        if (stateManager.tabs[tabIdentifier]) {
+            stateManager.tabs[tabIdentifier].injections = {};
+        }
+    }
+};
+
+/**
+ * Initializations
+ */
+
+stateManager.tabs = {};
+
+chrome.tabs.query({}, function (tabs) {
+    tabs.forEach(stateManager._createTab);
+});
+
+/**
+ * Event Handlers
+ */
+
+chrome.tabs.onCreated.addListener(stateManager._createTab);
+chrome.tabs.onRemoved.addListener(stateManager._removeTab);
+
+chrome.webRequest.onBeforeRequest.addListener(stateManager._updateTab, {
+    urls: ['<all_urls>'], types: ['main_frame']
+});
diff --git a/manifest.json b/manifest.json
index fb69001..c4185f4 100644
--- a/manifest.json
+++ b/manifest.json
@@ -17,9 +17,10 @@
 
   "permissions": [
     "*://*/*",
+    "privacy",
     "storage",
-    "webRequest",
     "tabs",
+    "webRequest",
     "webRequestBlocking"
   ],
 
diff --git a/pages/background/background.html b/pages/background/background.html
index b30e9f7..c87530d 100644
--- a/pages/background/background.html
+++ b/pages/background/background.html
@@ -12,6 +12,7 @@
         <script src="../../core/files.js"></script>
         <script src="../../core/resources.js"></script>
         <script src="../../core/mappings.js"></script>
+        <script src="../../core/state-manager.js"></script>
         <script src="../../core/request-analyzer.js"></script>
         <script src="../../core/interceptor.js"></script>
         <script src="../../core/main.js"></script>
diff --git a/pages/popup/popup.css b/pages/popup/popup.css
index 67740c8..207b1bc 100644
--- a/pages/popup/popup.css
+++ b/pages/popup/popup.css
@@ -1,16 +1,18 @@
 body {
     background-color: #f0f0f0;
     color: #555;
+    cursor: default;
     font-family: Noto Sans, Arial, sans-serif !important;
     font-size: 75%;
     margin: 0;
     padding: 0;
+    user-select: none;
     width: 350px;
 }
 
 h1 {
     font-size: 36px;
-    margin: 12px 0 0 0;
+    margin: 0;
     text-align: center;
 }
 
@@ -23,14 +25,74 @@ h1 {
 .description {
     color: #777;
     font-style: italic;
-    margin-bottom: 16px;
+    margin-bottom: 18px;
     text-align: center;
 }
 
 .popup-content {
+    padding: 0;
+}
+
+.list {
+    border-bottom: 1px solid #d8d8d8;
+    margin: 0;
+    padding: 10px 8px;
+}
+
+.list-item {
+    background-color: #f7f7f7;
+    border-bottom: none !important;
+    border: 1px solid #e4e4e4;
+    color: #737373;
+    font-weight: 600;
+    list-style: none;
+    margin: 0;
     padding: 10px;
 }
 
+.sub-list {
+    align-items: center;
+    background-color: #ececec;
+    border-bottom: none !important;
+    border: 1px solid #e0e0e0;
+    box-shadow: inset 0px 2px 10px #e2e2e2;
+    list-style: none;
+    padding-left: 8px;
+    padding: 0;
+}
+
+.sub-list:last-child {
+    border-bottom: 1px solid #e0e0e0 !important;
+}
+
+.sub-list-item {
+    border-bottom: 1px solid #e0e0e0;
+    color: #737373;
+    font-weight: bold;
+    padding: 10px;
+}
+
+.sub-list-item:last-child {
+    border-bottom: none;
+}
+
+.side-note {
+    color: #a5a5a5;
+    font-style: italic;
+    font-weight: normal;
+}
+
+.badge {
+    background-color: #6bb798;
+    border-radius: 10px;
+    color: #fff;
+    font-family: monospace;
+    font-size: 13px;
+    font-weight: bold;
+    margin-right: 8px;
+    padding: 3px 15px;
+}
+
 .button-panel {
     padding: 6px;
     text-align: right;
@@ -41,11 +103,11 @@ h1 {
 }
 
 .text-link {
-    color: #adadad;
+    color: #bdbdbd;
     float: left;
     font-size: 13px;
     padding-left: 4px;
-    padding-top: 5px;
+    padding-top: 2px;
     text-decoration: none;
 }
 
@@ -54,10 +116,15 @@ h1 {
     text-decoration: underline;
 }
 
+#injection-counter {
+    padding-top: 14px;
+}
+
 #extension-options-overlay-header {
     align-items: center;
     border-bottom: solid lightgray 1px;
     display: flex;
+    padding: 4px 0;
     position: relative;
 }
 
diff --git a/pages/popup/popup.html b/pages/popup/popup.html
index f552c49..97b84b5 100644
--- a/pages/popup/popup.html
+++ b/pages/popup/popup.html
@@ -2,34 +2,37 @@
 
 <html>
 
-    <head>
+<head>
 
-        <title>Decentraleyes Popup</title>
+    <title>Decentraleyes Popup</title>
 
-        <link rel="stylesheet" type="text/css" href="popup.css">
+    <link rel="stylesheet" type="text/css" href="popup.css">
 
-    </head>
+</head>
 
-    <body>
+<body>
 
-        <script src="popup.js"></script>
+    <script src="popup.js"></script>
 
-        <div id="extension-options-overlay-header">
-            <img id="extension-options-overlay-icon" src="icon.png">
-            <div id="extension-options-overlay-title">Decentraleyes</div>
-        </div>
+    <div id="extension-options-overlay-header">
+        <img id="extension-options-overlay-icon" src="icon.png">
+        <div id="extension-options-overlay-title">Decentraleyes</div>
+    </div>
 
-        <section class="popup-content">
-            <h1 id="injection-counter"></h1>
-            <div class="title" data-i18n-content="amountInjectedTitle"></div>
-            <div class="description" data-i18n-content="amountInjectedDescription"></div>
-        </section>
+    <section id="popup-content" class="popup-content">
 
-        <section class="button-panel">
-            <a href="https://decentraleyes.org/test" target="_blank" class="text-link">decentraleyes.org/test</a>
-            <button id="options-button" class="btn-sm">Options</button>
-        </section>
+        <h1 id="injection-counter">3</h1>
 
-    </body>
+        <div class="title" data-i18n-content="amountInjectedTitle"></div>
+        <div class="description" data-i18n-content="amountInjectedDescription"></div>
+
+    </section>
+
+    <section class="button-panel">
+        <a href="https://decentraleyes.org/test" target="_blank" class="text-link">decentraleyes.org/test</a>
+        <button id="options-button" class="btn-sm">Options</button>
+    </section>
+
+</body>
 
 </html>
diff --git a/pages/popup/popup.js b/pages/popup/popup.js
index 2d03a62..649999c 100644
--- a/pages/popup/popup.js
+++ b/pages/popup/popup.js
@@ -33,6 +33,180 @@ document.addEventListener('DOMContentLoaded', function () {
 
         let amountInjected = items.amountInjected || 0;
         document.getElementById('injection-counter').innerHTML = amountInjected;
+
+        chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) {
+
+            chrome.runtime.getBackgroundPage(function (backgroundPage) {
+
+                let injections, injectionOverview;
+
+                injections = backgroundPage.stateManager.tabs[tabs[0].id].injections;
+                injectionOverview = {};
+
+                for (let injection in injections) {
+
+                    let injectionSource, libraryName;
+
+                    injection = injections[injection];
+                    injectionSource = injection.source;
+
+                    injectionOverview[injectionSource] = injectionOverview[injectionSource] || [];
+
+                    injectionOverview[injectionSource].push({
+                        'path': injection.path,
+                        'version': injection.version,
+                        'source': injection.source
+                    });
+                }
+
+                let listElement = document.createElement('ul');
+                listElement.setAttribute('class', 'list');
+
+                for (let injectionSource in injectionOverview) {
+
+                    let cdn, listItemElement, badgeElement, badgeTextNode, cdnName, cdnNameTextNode, subListElement;
+
+                    cdn = injectionOverview[injectionSource];
+
+                    listItemElement = document.createElement('li');
+                    listItemElement.setAttribute('class', 'list-item');
+
+                    badgeElement = document.createElement('span');
+                    badgeElement.setAttribute('class', 'badge');
+
+                    badgeTextNode = document.createTextNode(cdn.length);
+                    badgeElement.appendChild(badgeTextNode);
+
+                    switch (injectionSource) {
+
+                    case 'ajax.googleapis.com':
+                        cdnName = 'Google Hosted Libraries';
+                        break;
+                    case 'ajax.aspnetcdn.com':
+                        cdnName = 'Microsoft Ajax CDN';
+                        break;
+                    case 'ajax.microsoft.com':
+                        cdnName = 'Microsoft Ajax CDN [Deprecated]';
+                        break;
+                        case 'cdnjs.cloudflare.com':
+                        cdnName = 'CDNJS (Cloudflare)';
+                        break;
+                    case 'code.jquery.com':
+                        cdnName = 'jQuery CDN (MaxCDN)';
+                        break;
+                    case 'cdn.jsdelivr.net':
+                        cdnName = 'jsDelivr (MaxCDN)';
+                        break;
+                    case 'yastatic.net':
+                        cdnName = 'Yandex CDN';
+                        break;
+                    case 'yandex.st':
+                        cdnName = 'Yandex CDN [Deprecated]';
+                        break;
+                    case 'libs.baidu.com':
+                        cdnName = 'Baidu CDN';
+                        break;
+                    case 'lib.sinaapp.com':
+                        cdnName = 'Sina Public Resources';
+                        break;
+                    case 'upcdn.b0.upaiyun.com':
+                        cdnName = 'UpYun Library';
+                        break;
+                    }
+
+                    cdnNameTextNode = document.createTextNode(cdnName);
+
+                    listItemElement.appendChild(badgeElement);
+                    listItemElement.appendChild(cdnNameTextNode);
+
+                    listElement.appendChild(listItemElement);
+
+                    subListElement = document.createElement('ul');
+                    subListElement.setAttribute('class', 'sub-list');
+
+                    listElement.appendChild(subListElement);
+
+                    cdn.forEach(function (injection) {
+
+                        let subListItemElement, resourcePathDetails, resourceFilename, resourceName,
+                            resourceNameTextNode, sideNoteElement, sideNoteTextNode;
+
+                        subListItemElement = document.createElement('li');
+                        subListItemElement.setAttribute('class', 'sub-list-item');
+
+                        resourcePathDetails = injection.path.split('/');
+                        resourceFilename = resourcePathDetails[resourcePathDetails.length - 1];
+
+                        switch (resourceFilename) {
+
+                        case 'angular.min.js.dec':
+                            resourceName = 'AngularJS';
+                            break;
+                        case 'backbone-min.js.dec':
+                            resourceName = 'Backbone.js';
+                            break;
+                        case 'dojo.js.dec':
+                            resourceName = 'Dojo';
+                            break;
+                        case 'ember.min.js.dec':
+                            resourceName = 'Ember.js';
+                            break;
+                        case 'ext-core.js.dec':
+                            resourceName = 'Ext Core';
+                            break;
+                        case 'jquery.min.js.dec':
+                            resourceName = 'jQuery';
+                            break;
+                        case 'jquery-ui.min.js.dec':
+                            resourceName = 'jQuery UI';
+                            break;
+                        case 'modernizr.min.js.dec':
+                            resourceName = 'Modernizr';
+                            break;
+                        case 'mootools-yui-compressed.js.dec':
+                            resourceName = 'MooTools';
+                            break;
+                        case 'prototype.js.dec':
+                            resourceName = 'Prototype';
+                            break;
+                        case 'scriptaculous.js.dec':
+                            resourceName = 'Scriptaculous';
+                            break;
+                        case 'swfobject.js.dec':
+                            resourceName = 'SWFObject';
+                            break;
+                        case 'underscore-min.js.dec':
+                            resourceName = 'Underscore.js';
+                            break;
+                        case 'webfont.js.dec':
+                            resourceName = 'Web Font Loader';
+                            break;
+                        }
+
+                        resourceNameTextNode = document.createTextNode('- ' + resourceName);
+                        subListItemElement.appendChild(resourceNameTextNode);
+
+                        sideNoteElement = document.createElement('span');
+                        sideNoteElement.setAttribute('class', 'side-note');
+
+                        sideNoteTextNode = document.createTextNode(' v' + injection.version);
+
+                        sideNoteElement.appendChild(sideNoteTextNode);
+                        subListItemElement.appendChild(sideNoteElement);
+
+                        subListElement.appendChild(subListItemElement);
+                    });
+                }
+
+                if (Object.keys(injectionOverview).length > 0) {
+
+                    let popupContentElement = document.getElementById('popup-content');
+                    let injectionCounterElement = document.getElementById('injection-counter');
+
+                    popupContentElement.insertBefore(listElement, injectionCounterElement);
+                }
+            });
+        });
     });
 
     document.getElementById('options-button').addEventListener('click', function () {
-- 
GitLab