├── data ├── icons │ ├── 128.png │ ├── 16.png │ ├── 256.png │ ├── 32.png │ ├── 48.png │ ├── 512.png │ ├── 64.png │ ├── success │ │ ├── 16.png │ │ ├── 32.png │ │ └── 48.png │ └── warn │ │ ├── 16.png │ │ ├── 32.png │ │ └── 48.png ├── options │ ├── index.css │ ├── index.html │ ├── index.js │ ├── matched.js │ └── matched.json └── window │ ├── index.css │ ├── index.html │ ├── index.js │ └── warn.svg ├── manifest.json └── worker.js /data/icons/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/james-fray/download-virus-check/e7206bc3f25c465df86505e5aa4451de1b25cd57/data/icons/128.png -------------------------------------------------------------------------------- /data/icons/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/james-fray/download-virus-check/e7206bc3f25c465df86505e5aa4451de1b25cd57/data/icons/16.png -------------------------------------------------------------------------------- /data/icons/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/james-fray/download-virus-check/e7206bc3f25c465df86505e5aa4451de1b25cd57/data/icons/256.png -------------------------------------------------------------------------------- /data/icons/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/james-fray/download-virus-check/e7206bc3f25c465df86505e5aa4451de1b25cd57/data/icons/32.png -------------------------------------------------------------------------------- /data/icons/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/james-fray/download-virus-check/e7206bc3f25c465df86505e5aa4451de1b25cd57/data/icons/48.png -------------------------------------------------------------------------------- /data/icons/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/james-fray/download-virus-check/e7206bc3f25c465df86505e5aa4451de1b25cd57/data/icons/512.png -------------------------------------------------------------------------------- /data/icons/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/james-fray/download-virus-check/e7206bc3f25c465df86505e5aa4451de1b25cd57/data/icons/64.png -------------------------------------------------------------------------------- /data/icons/success/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/james-fray/download-virus-check/e7206bc3f25c465df86505e5aa4451de1b25cd57/data/icons/success/16.png -------------------------------------------------------------------------------- /data/icons/success/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/james-fray/download-virus-check/e7206bc3f25c465df86505e5aa4451de1b25cd57/data/icons/success/32.png -------------------------------------------------------------------------------- /data/icons/success/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/james-fray/download-virus-check/e7206bc3f25c465df86505e5aa4451de1b25cd57/data/icons/success/48.png -------------------------------------------------------------------------------- /data/icons/warn/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/james-fray/download-virus-check/e7206bc3f25c465df86505e5aa4451de1b25cd57/data/icons/warn/16.png -------------------------------------------------------------------------------- /data/icons/warn/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/james-fray/download-virus-check/e7206bc3f25c465df86505e5aa4451de1b25cd57/data/icons/warn/32.png -------------------------------------------------------------------------------- /data/icons/warn/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/james-fray/download-virus-check/e7206bc3f25c465df86505e5aa4451de1b25cd57/data/icons/warn/48.png -------------------------------------------------------------------------------- /data/options/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-size: 13px; 3 | font-family: Arial, "Helvetica Neue", Helvetica, sans-serif; 4 | background-color: #fff; 5 | color: #4d5156; 6 | margin: 10px; 7 | } 8 | select, 9 | button, 10 | input[type=submit], 11 | input[type=button] { 12 | height: 24px; 13 | color: #444; 14 | background-image: linear-gradient(rgb(237, 237, 237), rgb(237, 237, 237) 38%, rgb(222, 222, 222)); 15 | box-shadow: rgba(0, 0, 0, 0.08) 0 1px 0, rgba(255, 255, 255, 0.75) 0 1px 2px inset; 16 | text-shadow: rgb(240, 240, 240) 0 1px 0; 17 | } 18 | select, 19 | button, 20 | textarea, 21 | input[type=text], 22 | input[type=number], 23 | input[type=submit], 24 | input[type=button] { 25 | border: solid 1px rgba(0, 0, 0, 0.25); 26 | } 27 | input[type=button]:disabled { 28 | opacity: 0.5; 29 | } 30 | textarea { 31 | width: 100%; 32 | box-sizing: border-box; 33 | display: block; 34 | padding: 5px; 35 | margin-top: 5px; 36 | } 37 | a, 38 | a:visited { 39 | color: #07c; 40 | } 41 | 42 | input[type=number], 43 | input[type=text], 44 | textarea { 45 | min-width: 200px; 46 | padding: 5px; 47 | } 48 | td ~ td { 49 | text-align: right; 50 | } 51 | .grid { 52 | display: grid; 53 | grid-template-columns: 1fr min-content; 54 | grid-gap: 10px; 55 | align-items: center; 56 | justify-items: left; 57 | } 58 | .note { 59 | margin: 0; 60 | padding: 2px 10px; 61 | color: #7d7d7d; 62 | grid-column: 1/3; 63 | background-color: #dbf5ff; 64 | } 65 | -------------------------------------------------------------------------------- /data/options/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Options :: Download Virus Checker 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | Minimum number of positive reports to display the warning window. The minimum acceptable value is 1. Normally each file is checked against 68 anti-virus solutions. 14 | 15 | 16 | By default, any VirusTotal Community registered user is entitled to a free API key that allows them to interact with a basic set of endpoints. First Register and then get your API key to insert into this box. Without the API key, this extension is not able to check your downloads. 17 | 18 | 19 | Displays logs. Use only for debugging 20 |
21 | 22 |
23 | 24 | 25 |
26 | Comma-separated list of mime-types to ignore scanning 27 | 28 |
29 |

30 | 31 | 32 |

33 |

34 | 35 |

36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /data/options/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const toast = document.getElementById('toast'); 4 | 5 | const restore = () => chrome.storage.local.get({ 6 | positives: 3, 7 | whitelist: 'image/, audio/, video/, text/', 8 | key: '', 9 | log: false 10 | }, prefs => { 11 | document.getElementById('key').value = prefs.key; 12 | document.getElementById('whitelist').value = prefs.whitelist; 13 | document.getElementById('positives').value = prefs.positives; 14 | document.getElementById('log').checked = prefs.log; 15 | }); 16 | 17 | document.addEventListener('DOMContentLoaded', restore); 18 | document.getElementById('save').addEventListener('click', () => { 19 | const prefs = { 20 | whitelist: document.getElementById('whitelist').value 21 | .split(/\s*,\s*/) 22 | .filter((s, i, l) => s && l.indexOf(s) === i) 23 | .join(', '), 24 | positives: Math.max(1, document.getElementById('positives').value), 25 | key: document.getElementById('key').value, 26 | log: document.getElementById('log').checked 27 | }; 28 | 29 | chrome.storage.local.set(prefs, () => { 30 | toast.textContent = 'Options saved.'; 31 | setTimeout(() => toast.textContent = '', 750); 32 | restore(); 33 | }); 34 | }); 35 | 36 | // reset 37 | document.getElementById('reset').addEventListener('click', e => { 38 | if (e.detail === 1) { 39 | toast.textContent = 'Double-click to reset!'; 40 | window.setTimeout(() => toast.textContent = '', 750); 41 | } 42 | else { 43 | localStorage.clear(); 44 | chrome.storage.local.clear(() => { 45 | chrome.runtime.reload(); 46 | window.close(); 47 | }); 48 | } 49 | }); 50 | // support 51 | document.getElementById('support').addEventListener('click', () => chrome.tabs.create({ 52 | url: chrome.runtime.getManifest().homepage_url + '?rd=donate' 53 | })); 54 | -------------------------------------------------------------------------------- /data/options/matched.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | { 4 | const shuffle = array => { 5 | for (let i = array.length - 1; i > 0; i -= 1) { 6 | const j = Math.floor(Math.random() * (i + 1)); 7 | [array[i], array[j]] = [array[j], array[i]]; 8 | } 9 | 10 | return array; 11 | }; 12 | 13 | const root = document.getElementById('explore'); 14 | 15 | const INC = Number(root.dataset.inc || 100); 16 | const count = Number(localStorage.getItem('explore-count') || INC - 5); 17 | const cols = Number(root.dataset.cols || 3); 18 | 19 | const style = document.createElement('style'); 20 | style.textContent = ` 21 | #explore { 22 | background-color: #fff; 23 | position: relative; 24 | color: #969696; 25 | user-select: none; 26 | } 27 | #explore[data-loaded=true] { 28 | margin: 4px; 29 | padding: 5px; 30 | box-shadow: 0 0 4px #ccc; 31 | border: solid 1px #ccc; 32 | } 33 | #explore .close { 34 | position: absolute; 35 | right: 6px; 36 | top: 4px; 37 | cursor: pointer; 38 | } 39 | #explore>table { 40 | margin-top: 10px; 41 | table-layout: fixed; 42 | width: 100%; 43 | border-collapse: collapse; 44 | } 45 | #explore a { 46 | text-decoration: none; 47 | color: #000; 48 | display: flex; 49 | align-items: center; 50 | justify-content: center; 51 | } 52 | #explore td:first-child a { 53 | justify-content: flex-start; 54 | } 55 | #explore td:last-child a { 56 | justify-content: flex-end; 57 | } 58 | #explore .title { 59 | border-left: solid 1px #ccc; 60 | display: inline-block; 61 | align-items: center; 62 | overflow: hidden; 63 | text-overflow: ellipsis; 64 | white-space: nowrap; 65 | padding-left: 5px; 66 | } 67 | #explore .icon { 68 | min-width: 28px; 69 | height: 28px; 70 | display: inline-flex; 71 | align-items: center; 72 | justify-content: center; 73 | border-radius: 50%; 74 | color: #fff; 75 | margin-right: 5px; 76 | font-size: 10px; 77 | font-weight: 100; 78 | } 79 | #explore .explore { 80 | position: absolute; 81 | right: 10px; 82 | z-index: 1000000; 83 | cursor: pointer; 84 | font-size: 15px; 85 | }`; 86 | document.documentElement.appendChild(style); 87 | 88 | const cload = () => fetch('matched.json').then(r => r.json()).then(build); 89 | const explore = () => { 90 | const span = document.createElement('span'); 91 | span.textContent = '↯'; 92 | span.title = 'Explore more'; 93 | span.classList.add('explore'); 94 | root.appendChild(span); 95 | span.onclick = () => { 96 | root.textContent = ''; 97 | localStorage.setItem('explore-count', INC); 98 | cload(); 99 | }; 100 | }; 101 | const build = json => { 102 | if (json.length === 0) { 103 | return; 104 | } 105 | root.dataset.loaded = true; 106 | root.textContent = 'Explore more'; 107 | const table = document.createElement('table'); 108 | const tr = document.createElement('tr'); 109 | const span = document.createElement('span'); 110 | span.classList.add('close'); 111 | span.textContent = '✕'; 112 | span.onclick = () => { 113 | root.textContent = ''; 114 | root.dataset.loaded = false; 115 | localStorage.setItem('explore-count', 0); 116 | explore(); 117 | }; 118 | root.appendChild(span); 119 | 120 | const {homepage_url} = chrome.runtime.getManifest(); 121 | const origin = homepage_url.split('/').slice(0, -1).join('/'); 122 | const colors = shuffle( 123 | ['524c84', '606470', '755da3', 'c06c84', '393e46', '446e5c', '693e52', '1d566e', '693e52', 'd95858', 'f27370'] 124 | ); 125 | shuffle(Object.entries(json)).slice(0, cols).forEach(([id, {name}], i) => { 126 | const td = document.createElement('td'); 127 | const a = Object.assign(document.createElement('a'), { 128 | target: '_blank', 129 | title: 'Click to browse', 130 | href: origin + '/' + id + '.html?context=explore' 131 | }); 132 | 133 | const icon = document.createElement('span'); 134 | icon.textContent = name.split(' ').slice(0, 2).map(s => s[0]).join('').toUpperCase(); 135 | icon.classList.add('icon'); 136 | icon.style['background-color'] = '#' + colors[i]; 137 | a.appendChild(icon); 138 | 139 | const span = document.createElement('span'); 140 | span.classList.add('title'); 141 | span.textContent = name; 142 | a.appendChild(span); 143 | td.appendChild(a); 144 | tr.appendChild(td); 145 | }); 146 | table.appendChild(tr); 147 | root.appendChild(table); 148 | }; 149 | const init = () => { 150 | if (count >= INC) { 151 | if (count < INC + 3) { 152 | cload(); 153 | } 154 | else { 155 | explore(); 156 | } 157 | if (count > INC + 5) { 158 | localStorage.setItem('explore-count', INC - 6); 159 | } 160 | else { 161 | localStorage.setItem('explore-count', count + 1); 162 | } 163 | } 164 | else { 165 | explore(); 166 | localStorage.setItem('explore-count', count + 1); 167 | } 168 | }; 169 | if (/Edg/.test(navigator.userAgent) === false) { 170 | init(); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /data/options/matched.json: -------------------------------------------------------------------------------- 1 | { 2 | "country-flags": { 3 | "name": "Country Flags & IP WHOIS" 4 | }, 5 | "save-images": { 6 | "name": "Download All Images" 7 | }, 8 | "media-player": { 9 | "name": "YouTube Media Player" 10 | }, 11 | "tab-discard": { 12 | "name": "Auto Tab Discard" 13 | }, 14 | "useragent-switcher": { 15 | "name": "User-Agent Switcher and Manager" 16 | }, 17 | "block-site": { 18 | "name": "Block Site" 19 | }, 20 | "chrome-reader-view": { 21 | "name": "Reader View" 22 | }, 23 | "dark-theme": { 24 | "name": "Dark Theme" 25 | }, 26 | "mute-tab": { 27 | "name": "Mute Tab" 28 | }, 29 | "proxy-switcher": { 30 | "name": "Proxy Switcher" 31 | }, 32 | "popup-blocker": { 33 | "name": "Popup Blocker" 34 | }, 35 | "audio-equalizer": { 36 | "name": "Audio Equalizer" 37 | }, 38 | "clipboard-manager": { 39 | "name": "Clipboard Manager" 40 | }, 41 | "sqlite-viewer": { 42 | "name": "SQLite Viewer" 43 | }, 44 | "audio-joiner": { 45 | "name": "Audio Joiner" 46 | }, 47 | "search-all-tabs": { 48 | "name": "Search all Tabs" 49 | }, 50 | "bookmarks-commander": { 51 | "name": "Bookmarks Commander" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /data/window/index.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | height: 100%; 4 | } 5 | body { 6 | margin: 0; 7 | padding: 10px; 8 | box-sizing: border-box; 9 | font-size: 13px; 10 | font-family: Arial,"Helvetica Neue",Helvetica,sans-serif; 11 | display: flex; 12 | flex-direction: column; 13 | align-items: center; 14 | } 15 | iframe { 16 | flex: 1; 17 | width: 100%; 18 | border: solid 1px #ccc; 19 | margin-top: 10px; 20 | } 21 | h1 { 22 | text-align: center; 23 | width: 100%; 24 | max-width: 480px; 25 | color: red; 26 | font-weight: 300; 27 | } 28 | table { 29 | width: 100%; 30 | table-layout: fixed; 31 | } 32 | 33 | td { 34 | overflow: hidden; 35 | text-overflow: ellipsis; 36 | white-space: nowrap; 37 | } 38 | 39 | p { 40 | background-color: #fff8c4; 41 | border: solid 2px #f7deae; 42 | padding: 10px; 43 | border-radius: 2px; 44 | } 45 | -------------------------------------------------------------------------------- /data/window/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | WARNING :: Download Virus Checker 6 | 7 | 8 | 9 | 10 |

Stop! This link might be infected!

11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
File name:
URL:
Start Time:
Reported:
View online:
37 |

38 | Based on this report, decide whether you want to cancel the download and delete the local file or keep it. This is just a warning report! 39 |

40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /data/window/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const args = document.location.search.substr(1).split('&').map(s => s.split('=')).reduce((p, c) => { 4 | p[c[0]] = decodeURIComponent(c[1]); 5 | return p; 6 | }, {}); 7 | 8 | function summary(obj) { 9 | const styles = 10 | 'table {' + 11 | ' white-space: nowrap;' + 12 | ' font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;' + 13 | ' border-collapse: collapse;' + 14 | ' border-spacing: 0px;' + 15 | ' font-size: 13px;' + 16 | ' line-height: 20px;' + 17 | ' color: #333;' + 18 | ' table-layout: fixed;' + 19 | '}' + 20 | 'th {' + 21 | ' border-bottom: 1px solid #DDD;' + 22 | ' padding: 8px;' + 23 | ' text-align: left;' + 24 | '}' + 25 | 'td {' + 26 | ' padding: 8px;' + 27 | ' overflow: hidden;' + 28 | ' text-overflow: ellipsis;' + 29 | '}' + 30 | 'tr:nth-child(even) {' + 31 | ' background: #F9F9F9;' + 32 | '}' + 33 | '.clean {' + 34 | ' color: #33AF99;' + 35 | '}' + 36 | '.defected {' + 37 | ' color: #ED3237;' + 38 | '}'; 39 | const html = 40 | '' + 41 | ' ' + 42 | ' ' + 43 | ' ' + 44 | ' ' + 45 | ' Scan Plus Report' + 46 | ' ' + 47 | ' ' + 48 | ' ' + 49 | ' ' + 50 | ' ' + 51 | ' ' + 52 | ' ' + 53 | ' ' + 54 | ' ' + 55 | ' ' + 56 | ' ' + 57 | ' ' + 58 | '
URL ScannerResultDetail
' + 59 | ''; 60 | 61 | 62 | const body = Object.entries(obj.scans).map(([name, o]) => { 63 | o.name = name; 64 | return o; 65 | }).sort((a, b) => { 66 | if (a.detected && b.detected === false) { 67 | return -1; 68 | } 69 | if (a.detected === false && b.detected) { 70 | return 1; 71 | } 72 | }).map(scan => { 73 | return '' + scan.name + '' + scan.result + '' + (scan.detail || '-') + ''; 74 | }).join(''); 75 | 76 | return 'data:text/html,' + encodeURIComponent(html.replace('', body).replace('', styles)); 77 | } 78 | 79 | 80 | chrome.runtime.onMessage.addListener(request => { 81 | if (request.cmd === 'report') { 82 | if (request.download) { 83 | document.querySelector('[data-id=filename]').textContent = request.download.filename || 'Unknown'; 84 | document.querySelector('[data-id=time]').textContent = (new Date(request.download.startTime)).toLocaleString(); 85 | } 86 | else { 87 | document.querySelector('[data-id=filename]').textContent = request.result.download?.filename || 'Unknown'; 88 | } 89 | document.querySelector('[data-id=url]').textContent = request.url; 90 | document.querySelector('[data-id=report]').textContent = request.result.positives + '/' + request.result.total; 91 | document.querySelector('[data-id=permalink]').href = document.querySelector('[data-id=permalink]').textContent = request.result.permalink; 92 | 93 | document.querySelector('iframe').src = summary(request.result); 94 | } 95 | }); 96 | 97 | if (args.url) { 98 | chrome.runtime.sendMessage({ 99 | cmd: 'get-report', 100 | url: args.url 101 | }); 102 | } 103 | 104 | window.onblur = () => chrome.runtime.sendMessage({ 105 | cmd: 'bring-to-front' 106 | }); 107 | -------------------------------------------------------------------------------- /data/window/warn.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Virus Checker for Downloads", 3 | "description": "Automatically check all the download files against well-known anti-viruses using VirusTotal API", 4 | "version": "0.2.0", 5 | "manifest_version": 3, 6 | "permissions": [ 7 | "storage", 8 | "downloads", 9 | "notifications", 10 | "alarms" 11 | ], 12 | "host_permissions": [ 13 | "https://www.virustotal.com/*" 14 | ], 15 | "background": { 16 | "service_worker": "worker.js" 17 | }, 18 | "action": { 19 | "default_title": "Badge number displays the number of active scanning jobs. Click to abort these jobs." 20 | }, 21 | "homepage_url": "https://add0n.com/virus-checker.html", 22 | "icons": { 23 | "16": "data/icons/16.png", 24 | "32": "data/icons/32.png", 25 | "48": "data/icons/48.png", 26 | "64": "data/icons/64.png", 27 | "128": "data/icons/128.png", 28 | "256": "data/icons/256.png", 29 | "512": "data/icons/512.png" 30 | }, 31 | "options_ui": { 32 | "page": "data/options/index.html" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /worker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const DELAY = 20 * 1000; 4 | const cache = {}; 5 | 6 | const logging = (...args) => logging.print && console.log(...args); 7 | chrome.storage.local.get({ 8 | log: false 9 | }, prefs => logging.print = prefs.log); 10 | 11 | const notify = (e, c = () => {}) => chrome.notifications.create({ 12 | type: 'basic', 13 | iconUrl: '/data/icons/48.png', 14 | title: chrome.runtime.getManifest().name, 15 | message: e.message || e 16 | }, c); 17 | 18 | chrome.action.onClicked.addListener(() => { 19 | chrome.alarms.clearAll(); 20 | chrome.action.setBadgeText({ 21 | text: '' 22 | }); 23 | chrome.storage.local.set({ 24 | queue: [], 25 | scanning: false 26 | }); 27 | notify('Aborting active jobs'); 28 | }); 29 | 30 | chrome.action.setBadgeBackgroundColor({ 31 | color: '#4790f5' 32 | }); 33 | 34 | // badge 35 | const badge = () => chrome.storage.local.get({ 36 | queue: [], 37 | scanning: false 38 | }, prefs => { 39 | const count = prefs.queue.length + (prefs.scanning ? 1 : 0); 40 | chrome.action.setBadgeText({ 41 | text: count ? count.toString() : '' 42 | }); 43 | }); 44 | 45 | // check 46 | const check = () => chrome.storage.local.get({ 47 | queue: [], 48 | scanning: false, 49 | key: '' 50 | }, prefs => { 51 | badge(); 52 | if (prefs.scanning) { 53 | chrome.alarms.getAll(as => { 54 | if (as.length === 0) { 55 | chrome.alarms.create('scanning', { 56 | when: Date.now() 57 | }); 58 | } 59 | }); 60 | return; 61 | } 62 | 63 | if (prefs.queue.length === 0) { 64 | return; 65 | } 66 | logging('check', prefs); 67 | // let's scan a new one 68 | const href = prefs.queue.shift(); 69 | const body = new URLSearchParams(); 70 | body.set('apikey', prefs.key); 71 | body.set('url', href); 72 | 73 | fetch('https://www.virustotal.com/vtapi/v2/url/scan', { 74 | method: 'POST', 75 | body 76 | }).then(r => { 77 | if (r.ok) { 78 | if (r.status === 204) { 79 | throw Error('exceeded the request rate limit'); 80 | } 81 | 82 | r.json().then(json => { 83 | if (json.error) { 84 | throw Error(json.error); 85 | } 86 | if (json.response_code === 0) { 87 | throw Error('The requested resource is not among the finished, queued or pending scans'); 88 | } 89 | else if (json.response_code !== 1) { 90 | throw Error('response code is not equal to one'); 91 | } 92 | 93 | chrome.storage.local.set({ 94 | scanning: json['scan_id'] 95 | }); 96 | chrome.alarms.create('scanning', { 97 | when: Date.now() + DELAY 98 | }); 99 | }); 100 | } 101 | else { 102 | throw Error('response is not ok'); 103 | } 104 | }).catch(e => logging('check aborted', e)).finally(() => chrome.storage.local.set(prefs)); 105 | }); 106 | 107 | chrome.runtime.onStartup.addListener(check); 108 | chrome.runtime.onInstalled.addListener(check); 109 | chrome.storage.onChanged.addListener(ps => { 110 | if (ps.queue) { 111 | check(); 112 | } 113 | else if (ps.scanning && ps.scanning.newValue === false) { 114 | badge(); 115 | chrome.alarms.create('check', { 116 | when: Date.now() + DELAY 117 | }); 118 | } 119 | }); 120 | 121 | // scanning 122 | chrome.alarms.onAlarm.addListener(o => { 123 | if (o.name === 'scanning') { 124 | logging('scanning start', o); 125 | chrome.storage.local.get({ 126 | scanning: false, 127 | key: '' 128 | }, prefs => { 129 | const next = reason => { 130 | logging('scanning end', reason); 131 | chrome.storage.local.set({ 132 | scanning: false 133 | }); 134 | }; 135 | 136 | const body = new URLSearchParams(); 137 | body.set('apikey', prefs.key); 138 | body.set('resource', prefs.scanning); 139 | fetch('https://www.virustotal.com/vtapi/v2/url/report', { 140 | method: 'POST', 141 | body 142 | }).then(r => { 143 | if (r.status === 204) { // exceeded the request rate limit 144 | return chrome.alarms.create('scanning', { 145 | when: Date.now() + DELAY 146 | }); 147 | } 148 | if (r.ok) { 149 | r.json().then(json => { 150 | if (json.response_code !== 1) { 151 | next('response_code is ' + json.response_code); // 152 | } 153 | else { 154 | chrome.storage.local.get({ 155 | positives: 3 156 | }, prefs => { 157 | if (json.positives >= prefs.positives) { 158 | cache[json.url] = json; 159 | 160 | chrome.windows.getCurrent(win => { 161 | const width = 500; 162 | const height = 600; 163 | 164 | chrome.windows.create({ 165 | url: '/data/window/index.html?url=' + encodeURIComponent(json.url), 166 | focused: true, 167 | type: 'panel', 168 | width, 169 | height, 170 | left: Math.round((win.width - width) / 2), 171 | top: Math.round((win.height - height) / 2) 172 | }); 173 | }); 174 | } 175 | else { 176 | logging('Link passed anti-virus check'); 177 | } 178 | next('done'); 179 | }); 180 | } 181 | }); 182 | } 183 | else { 184 | next('response is not ok'); 185 | } 186 | }).catch(e => next(e.message)); 187 | }); 188 | } 189 | else if (o.name === 'check') { 190 | check(); 191 | } 192 | }); 193 | 194 | function scan(download) { 195 | chrome.storage.local.get({ 196 | queue: [] 197 | }, prefs => { 198 | prefs.queue.push(download.url); 199 | 200 | chrome.storage.local.set({ 201 | queue: prefs.queue.filter((s, i, l) => s && l.indexOf(s) === i) 202 | }); 203 | }); 204 | } 205 | 206 | // 207 | chrome.runtime.onMessage.addListener((request, sender) => { 208 | if (request.cmd === 'get-report') { 209 | chrome.downloads.search({ 210 | url: request.url 211 | }, downloads => { 212 | chrome.tabs.sendMessage(sender.tab.id, { 213 | cmd: 'report', 214 | result: cache[request.url], 215 | download: downloads.length ? downloads[0] : false, 216 | url: request.url 217 | }); 218 | setTimeout(() => { 219 | delete cache[request.url]; 220 | }, 60 * 1000); 221 | }); 222 | } 223 | else if (request.cmd === 'bring-to-front') { 224 | // chrome.windows.update(sender.tab.windowId, { 225 | // focused: true 226 | // }); 227 | } 228 | }); 229 | 230 | // adding newly added download to the key list 231 | chrome.downloads.onCreated.addListener(download => { 232 | if (download.state !== 'in_progress') { 233 | return logging('download is not in progress', 'aborting'); 234 | } 235 | 236 | chrome.storage.local.get({ 237 | whitelist: 'image/, audio/, video/, text/', 238 | key: '', 239 | prompt: false, 240 | log: false 241 | }, prefs => { 242 | if (prefs.key === '') { 243 | if (prefs.prompt === false) { 244 | notify('Please set your free API key on the options page', chrome.storage.local.set({ 245 | prompt: true 246 | }, () => chrome.runtime.openOptionsPage())); 247 | } 248 | return logging('API key is not detected', 'aborting'); 249 | } 250 | const ignored = prefs.whitelist.split(', ').reduce((p, c) => p || c && download.mime.startsWith(c), false); 251 | if (ignored) { 252 | logging('Check is ignored', download.mime); 253 | } 254 | else { 255 | scan(download); 256 | } 257 | }); 258 | }); 259 | 260 | /* FAQs & Feedback */ 261 | { 262 | const {management, runtime: {onInstalled, setUninstallURL, getManifest}, storage, tabs} = chrome; 263 | if (navigator.webdriver !== true) { 264 | const page = getManifest().homepage_url; 265 | const {name, version} = getManifest(); 266 | onInstalled.addListener(({reason, previousVersion}) => { 267 | management.getSelf(({installType}) => installType === 'normal' && storage.local.get({ 268 | 'faqs': true, 269 | 'last-update': 0 270 | }, prefs => { 271 | if (reason === 'install' || (prefs.faqs && reason === 'update')) { 272 | const doUpdate = (Date.now() - prefs['last-update']) / 1000 / 60 / 60 / 24 > 45; 273 | if (doUpdate && previousVersion !== version) { 274 | tabs.query({active: true, currentWindow: true}, tbs => tabs.create({ 275 | url: page + '?version=' + version + (previousVersion ? '&p=' + previousVersion : '') + '&type=' + reason, 276 | active: reason === 'install', 277 | ...(tbs && tbs.length && {index: tbs[0].index + 1}) 278 | })); 279 | storage.local.set({'last-update': Date.now()}); 280 | } 281 | } 282 | })); 283 | }); 284 | setUninstallURL(page + '?rd=feedback&name=' + encodeURIComponent(name) + '&version=' + version); 285 | } 286 | } 287 | --------------------------------------------------------------------------------