diff --git a/src/drivers/webextension/_locales/en/messages.json b/src/drivers/webextension/_locales/en/messages.json index 60cba2dd6..9f7b85cc0 100644 --- a/src/drivers/webextension/_locales/en/messages.json +++ b/src/drivers/webextension/_locales/en/messages.json @@ -8,7 +8,7 @@ "optionUpgradeMessage": { "message": "Tell me about upgrades" }, "optionDynamicIcon": { "message": "Use technology icon instead of Wappalyzer logo" }, "optionTracking": { "message": "Anonymously send identified technologies to wappalyzer.com" }, - "optionThemeMode": { "message": "Enable dark mode compatibility." }, + "optionThemeMode": { "message": "Enable dark mode compatibility" }, "nothingToDo": { "message": "Nothing to do here." }, "noAppsDetected": { "message": "No technologies detected." }, "categoryPin": { "message": "Always show icon" }, diff --git a/src/drivers/webextension/css/options.css b/src/drivers/webextension/css/options.css deleted file mode 100644 index b1eaa7df9..000000000 --- a/src/drivers/webextension/css/options.css +++ /dev/null @@ -1,108 +0,0 @@ -body { - color: #303942; - cursor: default; - direction: __MSG_@@bidi_dir__; - font-family: Helvetica, Arial, sans-serif; - font-size: .8rem; - line-height: 1.4rem; - margin: 0; -} - -p { - margin: 0 0 1rem 0; -} - -h1, h2, h3 { - font-weight: normal; - line-height: 1; -} - -h1 { - border-bottom: 1px solid #dbdbdb; - font-size: 1.5rem; - margin: 0 0 1.5rem 0; - padding: 1rem 0 1.5rem 0; -} - -h2 { - font-size: 1.3em; - margin-bottom: 0.4em; -} - -h3 { - color: black; - font-size: 1.2em; - margin-bottom: 0.5em; -} - -a { - color: rgb(17, 85, 204); - text-decoration: underline; -} - -label { - display: block; -} - -button { - background: #4608ad; - border: none; - border-radius: .2rem; - color: white; - font-size: inherit; - padding: 0 .6rem; - line-height: 1.8rem; -} - -a:active { - color: rgb(5, 37, 119); -} - -.hero { - background: linear-gradient(160deg, #32067c, #150233); - padding: 1.5rem 1.5rem 1rem 1.5rem; -} - - .hero img { - height: 3rem; - } - -.container { - margin: 0 auto; - max-width: 800px; -} - -.content { - padding: 1.5rem; -} - -#options-saved { - display: none; - margin-left: .5rem; - -webkit-animation: fadeout 2s; -} - -#about { - border-top: 1px solid #dbdbdb; - margin-top: 1.5rem; - padding: 1.5rem 0 0 0; -} - - #about img { - margin-right: .2rem; - vertical-align: middle; - } - - #about button { - background: white; - border: 1px solid #dbdbdb; - cursor: pointer; - color: #303942; - margin-bottom: .5rem; - margin-inline-end: 1rem; - } - -@-webkit-keyframes fadeout { - from { opacity: 1; } - to { opacity: 0; } -} diff --git a/src/drivers/webextension/css/popup.css b/src/drivers/webextension/css/styles.css similarity index 90% rename from src/drivers/webextension/css/popup.css rename to src/drivers/webextension/css/styles.css index a73551918..9ed0f55a3 100644 --- a/src/drivers/webextension/css/popup.css +++ b/src/drivers/webextension/css/styles.css @@ -10,6 +10,7 @@ body { background: #fff; + color: var(--color-text); direction: __MSG_@@bidi_dir__; font-family: Helvetica, Arial, sans-serif; font-size: .9rem; @@ -36,7 +37,7 @@ a:hover { align-items: center; border-bottom: 1px solid var(--color-secondary); display: flex; - height: 4rem; + height: 4.5rem; } .header__logo { @@ -79,6 +80,12 @@ a:hover { padding: 1.5rem 1.5rem .5rem 1.5rem; } +.empty { + opacity: .3; + padding: 3rem 1.5rem .5rem 1.5rem; + text-align: center; +} + .category { page-break-inside: avoid; break-inside: avoid-column; @@ -189,9 +196,19 @@ a:hover { margin-top: 1rem; } +.options { + padding: 1.5rem 1.5rem 1rem 1.5rem; +} + +.options__label { + display: block; + margin-bottom: .5rem; +} + @media (prefers-color-scheme: dark) { body.theme-mode { background: var(--color-primary-darken); + color: var(--color-text-dark); } .theme-mode a { @@ -222,6 +239,10 @@ a:hover { border-color: var(--color-secondary-dark) } + .theme-mode .footer__settings { + color: var(--color-text-dark); + } + .theme-mode .alerts__icon { color:var(--color-text-dark); } diff --git a/src/drivers/webextension/html/options.html b/src/drivers/webextension/html/options.html index 23b49fb83..7cd30cee3 100644 --- a/src/drivers/webextension/html/options.html +++ b/src/drivers/webextension/html/options.html @@ -1,69 +1,41 @@ - - + - Wappalyzer options - - + - + - - -
-
- -
-
+
+
+
+
@@ -50,6 +54,9 @@ + +   +  
diff --git a/src/drivers/webextension/js/content.js b/src/drivers/webextension/js/content.js index 3be7fa391..e637475c5 100644 --- a/src/drivers/webextension/js/content.js +++ b/src/drivers/webextension/js/content.js @@ -2,10 +2,10 @@ /* eslint-env browser */ /* globals chrome */ -const port = chrome.runtime.connect({ name: 'content.js' }) +const Content = { + port: chrome.runtime.connect({ name: 'content.js' }), -;(async function() { - if (typeof chrome !== 'undefined' && typeof document.body !== 'undefined') { + async init() { await new Promise((resolve) => setTimeout(resolve, 1000)) try { @@ -25,13 +25,13 @@ const port = chrome.runtime.connect({ name: 'content.js' }) html = chunks.join('\n') - // Scripts + // Script tags const scripts = Array.from(document.scripts) .filter(({ src }) => src) .map(({ src }) => src) .filter((script) => script.indexOf('data:text/javascript;') !== 0) - // Meta + // Meta tags const meta = Array.from(document.querySelectorAll('meta')) .map((meta) => ({ key: meta.getAttribute('name') || meta.getAttribute('property'), @@ -39,61 +39,64 @@ const port = chrome.runtime.connect({ name: 'content.js' }) })) .filter(({ value }) => value) - port.postMessage({ + Content.port.postMessage({ func: 'onContentLoad', args: [location.href, { html, scripts, meta }] }) - // JavaScript variables - const script = document.createElement('script') - - script.onload = () => { - const onMessage = (event) => { - if (event.data.id !== 'js') { - return - } - - window.removeEventListener('message', onMessage) + Content.port.postMessage({ func: 'getTechnologies' }) + } catch (error) { + Content.port.postMessage({ func: 'error', args: [error, 'content.js'] }) + } + }, - port.postMessage({ - func: 'analyze', - args: [new URL(location.href), { js: event.data.js }] - }) + onGetTechnologies(technologies) { + const script = document.createElement('script') - script.remove() + script.onload = () => { + const onMessage = ({ data }) => { + if (!data.wappalyzer || !data.wappalyzer.js) { + return } - window.addEventListener('message', onMessage) + window.removeEventListener('message', onMessage) + + Content.port.postMessage({ + func: 'analyzeJs', + args: [location.href, data.wappalyzer.js] + }) - port.postMessage({ id: 'get_js_patterns' }) + script.remove() } - script.setAttribute('src', chrome.extension.getURL('js/inject.js')) + window.addEventListener('message', onMessage) - document.body.appendChild(script) - } catch (error) { - port.postMessage({ func: 'error', args: [error, 'content.js'] }) + window.postMessage({ + wappalyzer: { + technologies: technologies + .filter(({ js }) => Object.keys(js).length) + .filter(({ name }) => name === 'jQuery') + .map(({ name, js }) => ({ name, chains: Object.keys(js) })) + } + }) } + + script.setAttribute('src', chrome.extension.getURL('js/inject.js')) + + document.body.appendChild(script) } -})() - -port.onMessage.addListener((message) => { - switch (message.id) { - case 'get_js_patterns': - postMessage( - { - id: 'patterns', - patterns: message.response.patterns - }, - window.location.href - ) - - break - default: - // Do nothing +} + +Content.port.onMessage.addListener(({ func, args }) => { + const onFunc = `on${func.charAt(0).toUpperCase() + func.slice(1)}` + + if (Content[onFunc]) { + Content[onFunc](args) } }) -// https://stackoverflow.com/a/44774834 -// https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/tabs/executeScript#Return_value -undefined // eslint-disable-line no-unused-expressions +if (/complete|interactive|loaded/.test(document.readyState)) { + Content.init() +} else { + document.addEventListener('DOMContentLoaded', Content.init) +} diff --git a/src/drivers/webextension/js/driver.js b/src/drivers/webextension/js/driver.js index 5671c66ef..0dd587cc3 100644 --- a/src/drivers/webextension/js/driver.js +++ b/src/drivers/webextension/js/driver.js @@ -2,7 +2,14 @@ /* eslint-env browser */ /* globals chrome, Wappalyzer, Utils */ -const { setTechnologies, setCategories, analyze, resolve, unique } = Wappalyzer +const { + setTechnologies, + setCategories, + analyze, + analyzeManyToMany, + resolve, + unique +} = Wappalyzer const { promisify, getOption } = Utils const Driver = { @@ -52,6 +59,26 @@ const Driver = { } }, + async analyzeJs(href, js) { + const url = new URL(href) + + await Driver.onDetect( + url, + Array.prototype.concat.apply( + [], + await Promise.all( + js.map(({ name, chain, value }) => + analyzeManyToMany( + Wappalyzer.technologies.find(({ name: _name }) => name === _name), + 'js', + { [chain]: [value] } + ) + ) + ) + ) + ) + }, + onRuntimeConnect(port) { port.onMessage.addListener(async ({ func, args }) => { if (!func) { @@ -60,6 +87,12 @@ const Driver = { Driver.log({ port: port.name, func, args }) + if (!Driver[func]) { + Driver.error(new Error(`Method does not exist: Driver.${func}`)) + + return + } + port.postMessage({ func, args: await Driver[func].call(port.sender, ...(args || [])) @@ -175,7 +208,10 @@ const Driver = { headers['content-type'] && /\/x?html/.test(headers['content-type'][0]) ) { - await Driver.onDetect(url, await analyze(url, { headers }, { tab })) + await Driver.onDetect( + url, + await analyze(url.href, { headers }, { tab }) + ) } } } catch (error) { @@ -192,12 +228,16 @@ const Driver = { domain: `.${url.hostname}` }) - await Driver.onDetect(url, await analyze(url, items)) + await Driver.onDetect(url, await analyze(href, items)) } catch (error) { Driver.error(error) } }, + getTechnologies() { + return Wappalyzer.technologies + }, + async onDetect(url, detections = []) { Driver.cache.hostnames[url.hostname] = unique([ ...(Driver.cache.hostnames[url.hostname] || []), diff --git a/src/drivers/webextension/js/inject.js b/src/drivers/webextension/js/inject.js index e23cb48b9..b0807124e 100644 --- a/src/drivers/webextension/js/inject.js +++ b/src/drivers/webextension/js/inject.js @@ -1,62 +1,44 @@ /* eslint-env browser */ -/* eslint-disable no-restricted-globals, no-prototype-builtins */ -;(() => { +;(function() { try { - const detectJs = (chain) => { - const properties = chain.split('.') - - let value = properties.length ? window : null - - for (let i = 0; i < properties.length; i += 1) { - const property = properties[i] - - if (value && value.hasOwnProperty(property)) { - value = value[property] - } else { - value = null - - break - } - } - - return typeof value === 'string' || typeof value === 'number' - ? value - : !!value - } - - const onMessage = (event) => { - if (event.data.id !== 'patterns') { + const onMessage = ({ data }) => { + if (!data.wappalyzer) { return } - removeEventListener('message', onMessage) - - const patterns = event.data.patterns || {} - - const js = {} - - for (const appName in patterns) { - if (patterns.hasOwnProperty(appName)) { - js[appName] = {} + const { technologies } = data.wappalyzer || {} - for (const chain in patterns[appName]) { - if (patterns[appName].hasOwnProperty(chain)) { - js[appName][chain] = {} - - for (const index in patterns[appName][chain]) { - const value = detectJs(chain) + removeEventListener('message', onMessage) - if (value && patterns[appName][chain].hasOwnProperty(index)) { - js[appName][chain][index] = value - } - } - } - } + postMessage({ + wappalyzer: { + js: technologies.reduce((results, { name, chains }) => { + chains.forEach((chain) => { + const value = chain + .split('.') + .reduce( + (value, method) => + value && value.hasOwnProperty(method) + ? value[method] + : undefined, + window + ) + + technologies.push({ + name, + chain, + value: + typeof value === 'string' || typeof value === 'number' + ? value + : !!value + }) + }) + + return technologies + }, []) } - } - - postMessage({ id: 'js', js }, window.location.href) + }) } addEventListener('message', onMessage) diff --git a/src/drivers/webextension/js/options.js b/src/drivers/webextension/js/options.js index 29d11d5d4..505117b52 100644 --- a/src/drivers/webextension/js/options.js +++ b/src/drivers/webextension/js/options.js @@ -1,109 +1,44 @@ -/** global: browser */ -/** global: Wappalyzer */ -/* globals browser Wappalyzer */ +'use strict' /* eslint-env browser */ +/* globals Utils */ -const wappalyzer = new Wappalyzer() +const { i18n, getOption, setOption } = Utils -/** - * Get a value from localStorage - */ -function getOption(name, defaultValue = null) { - return new Promise(async (resolve, reject) => { - let value = defaultValue +const Options = { + async init() { + // Theme mode + const themeMode = await getOption('themeMode', false) - try { - const option = await browser.storage.local.get(name) - - if (option[name] !== undefined) { - value = option[name] - } - } catch (error) { - wappalyzer.log(error.message, 'driver', 'error') - - return reject(error.message) + if (themeMode) { + document.querySelector('body').classList.add('theme-mode') } - return resolve(value) - }) + ;[ + ['upgradeMessage', true], + ['dynamicIcon', true], + ['tracking', true], + ['themeMode', false] + ].map(async ([option, defaultValue]) => { + const el = document + .querySelector( + `[data-i18n="option${option.charAt(0).toUpperCase() + + option.slice(1)}"]` + ) + .parentNode.querySelector('input') + + el.checked = !!(await getOption(option, defaultValue)) + + el.addEventListener('click', async () => { + await setOption(option, !!el.checked) + }) + }) + + i18n() + } } -/** - * Set a value in localStorage - */ -function setOption(name, value) { - return new Promise(async (resolve, reject) => { - try { - await browser.storage.local.set({ [name]: value }) - } catch (error) { - wappalyzer.log(error.message, 'driver', 'error') - - return reject(error.message) - } - - return resolve() - }) +if (/complete|interactive|loaded/.test(document.readyState)) { + Options.init() +} else { + document.addEventListener('DOMContentLoaded', Options.init) } - -document.addEventListener('DOMContentLoaded', async () => { - const nodes = document.querySelectorAll('[data-i18n]') - - Array.prototype.forEach.call(nodes, (node) => { - node.childNodes[0].nodeValue = browser.i18n.getMessage(node.dataset.i18n) - }) - - document.querySelector('#github').addEventListener('click', () => { - window.open(wappalyzer.config.githubURL) - }) - - document.querySelector('#twitter').addEventListener('click', () => { - window.open(wappalyzer.config.twitterURL) - }) - - document.querySelector('#wappalyzer').addEventListener('click', () => { - window.open(wappalyzer.config.websiteURL) - }) - - let el - let value - - // Upgrade message - value = await getOption('upgradeMessage', true) - - el = document.querySelector('#option-upgrade-message') - - el.checked = value - - el.addEventListener('change', (e) => - setOption('upgradeMessage', e.target.checked) - ) - - // Dynamic icon - value = await getOption('dynamicIcon', true) - - el = document.querySelector('#option-dynamic-icon') - - el.checked = value - - el.addEventListener('change', (e) => - setOption('dynamicIcon', e.target.checked) - ) - - // Tracking - value = await getOption('tracking', true) - - el = document.querySelector('#option-tracking') - - el.checked = value - - el.addEventListener('change', (e) => setOption('tracking', e.target.checked)) - - // Theme Mode - value = await getOption('themeMode', false) - - el = document.querySelector('#option-theme-mode') - - el.checked = value - - el.addEventListener('change', (e) => setOption('themeMode', e.target.checked)) -}) diff --git a/src/drivers/webextension/js/popup.js b/src/drivers/webextension/js/popup.js index 8170aa98c..a58e1cb79 100644 --- a/src/drivers/webextension/js/popup.js +++ b/src/drivers/webextension/js/popup.js @@ -2,7 +2,7 @@ /* eslint-env browser */ /* globals chrome, Utils */ -const { agent, getOption, setOption, promisify } = Utils +const { agent, i18n, getOption, setOption, promisify } = Utils const Popup = { port: chrome.runtime.connect({ name: 'popup.js' }), @@ -37,7 +37,7 @@ const Popup = { } else { document.querySelector('.detections').style.display = 'none' - Popup.i18n() + i18n() } // Alert @@ -65,12 +65,6 @@ const Popup = { Popup.driver('log', message, 'popup.js') }, - i18n() { - Array.from(document.querySelectorAll('[data-i18n]')).forEach( - (node) => (node.innerHTML = chrome.i18n.getMessage(node.dataset.i18n)) - ) - }, - categorise(technologies) { return Object.values( technologies.reduce((categories, technology) => { @@ -91,6 +85,10 @@ const Popup = { async onGetDetections(detections) { const pinnedCategory = await getOption('pinnedCategory') + if (detections.length) { + document.querySelector('.empty').remove() + } + Popup.categorise(detections).forEach( ({ id, name, slug: categorySlug, technologies }) => { const categoryNode = Popup.templates.category.cloneNode(true) @@ -149,7 +147,7 @@ const Popup = { a.addEventListener('click', () => Popup.driver('open', a.href)) ) - Popup.i18n() + i18n() } } diff --git a/src/drivers/webextension/js/utils.js b/src/drivers/webextension/js/utils.js index b35190ff3..3f83449d1 100644 --- a/src/drivers/webextension/js/utils.js +++ b/src/drivers/webextension/js/utils.js @@ -43,5 +43,11 @@ const Utils = { } catch (error) { throw new Error(error.message || error.toString()) } + }, + + i18n() { + Array.from(document.querySelectorAll('[data-i18n]')).forEach( + (node) => (node.innerHTML = chrome.i18n.getMessage(node.dataset.i18n)) + ) } }