You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
412 lines
11 KiB
412 lines
11 KiB
'use strict'
|
|
/* eslint-env browser */
|
|
/* globals chrome */
|
|
|
|
function inject(src, id, message) {
|
|
return new Promise((resolve) => {
|
|
// Inject a script tag into the page to access methods of the window object
|
|
const script = document.createElement('script')
|
|
|
|
script.onload = () => {
|
|
const onMessage = ({ data }) => {
|
|
if (!data.wappalyzer || !data.wappalyzer[id]) {
|
|
return
|
|
}
|
|
|
|
window.removeEventListener('message', onMessage)
|
|
|
|
resolve(data.wappalyzer[id])
|
|
|
|
script.remove()
|
|
}
|
|
|
|
window.addEventListener('message', onMessage)
|
|
|
|
window.postMessage({
|
|
wappalyzer: message,
|
|
})
|
|
}
|
|
|
|
script.setAttribute('src', chrome.runtime.getURL(src))
|
|
|
|
document.body.appendChild(script)
|
|
})
|
|
}
|
|
|
|
function getJs(technologies) {
|
|
return inject('js/js.js', 'js', {
|
|
technologies: technologies
|
|
.filter(({ js }) => Object.keys(js).length)
|
|
.map(({ name, js }) => ({ name, chains: Object.keys(js) })),
|
|
})
|
|
}
|
|
|
|
async function getDom(technologies) {
|
|
const _technologies = technologies
|
|
.filter(({ dom }) => dom && dom.constructor === Object)
|
|
.map(({ name, dom }) => ({ name, dom }))
|
|
|
|
return [
|
|
...(await inject('js/dom.js', 'dom', {
|
|
technologies: _technologies.filter(({ dom }) =>
|
|
Object.values(dom)
|
|
.flat()
|
|
.some(({ properties }) => properties)
|
|
),
|
|
})),
|
|
..._technologies.reduce((technologies, { name, dom }) => {
|
|
const toScalar = (value) =>
|
|
typeof value === 'string' || typeof value === 'number' ? value : !!value
|
|
|
|
Object.keys(dom).forEach((selector) => {
|
|
let nodes = []
|
|
|
|
try {
|
|
nodes = document.querySelectorAll(selector)
|
|
} catch (error) {
|
|
Content.driver('error', error)
|
|
}
|
|
|
|
if (!nodes.length) {
|
|
return
|
|
}
|
|
|
|
dom[selector].forEach(({ exists, text, attributes }) => {
|
|
nodes.forEach((node) => {
|
|
if (exists) {
|
|
technologies.push({
|
|
name,
|
|
selector,
|
|
exists: '',
|
|
})
|
|
}
|
|
|
|
if (text) {
|
|
// eslint-disable-next-line unicorn/prefer-text-content
|
|
const value = node.innerText.trim()
|
|
|
|
if (value) {
|
|
technologies.push({
|
|
name,
|
|
selector,
|
|
text: value,
|
|
})
|
|
}
|
|
}
|
|
|
|
if (attributes) {
|
|
Object.keys(attributes).forEach((attribute) => {
|
|
if (node.hasAttribute(attribute)) {
|
|
const value = node.getAttribute(attribute)
|
|
|
|
technologies.push({
|
|
name,
|
|
selector,
|
|
attribute,
|
|
value: toScalar(value),
|
|
})
|
|
}
|
|
})
|
|
}
|
|
})
|
|
})
|
|
})
|
|
|
|
return technologies
|
|
}, []),
|
|
]
|
|
}
|
|
|
|
const Content = {
|
|
cache: {},
|
|
language: '',
|
|
|
|
analyzedRequires: [],
|
|
|
|
/**
|
|
* Initialise content script
|
|
*/
|
|
async init() {
|
|
const url = location.href
|
|
|
|
if (await Content.driver('isDisabledDomain', url)) {
|
|
return
|
|
}
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
|
|
try {
|
|
// HTML
|
|
let html = new XMLSerializer().serializeToString(document)
|
|
|
|
// Discard the middle portion of HTML to avoid performance degradation on large pages
|
|
const chunks = []
|
|
const maxCols = 2000
|
|
const maxRows = 3000
|
|
const rows = html.length / maxCols
|
|
|
|
for (let i = 0; i < rows; i += 1) {
|
|
if (i < maxRows / 2 || i > rows - maxRows / 2) {
|
|
chunks.push(html.slice(i * maxCols, (i + 1) * maxCols))
|
|
}
|
|
}
|
|
|
|
html = chunks.join('\n')
|
|
|
|
// Determine language based on the HTML lang attribute or content
|
|
Content.language =
|
|
document.documentElement.getAttribute('lang') ||
|
|
document.documentElement.getAttribute('xml:lang') ||
|
|
(await new Promise((resolve) =>
|
|
chrome.i18n.detectLanguage
|
|
? chrome.i18n.detectLanguage(html, ({ languages }) =>
|
|
resolve(
|
|
languages
|
|
.filter(({ percentage }) => percentage >= 75)
|
|
.map(({ language: lang }) => lang)[0]
|
|
)
|
|
)
|
|
: resolve()
|
|
))
|
|
|
|
const cookies = document.cookie.split('; ').reduce(
|
|
(cookies, cookie) => ({
|
|
...cookies,
|
|
[cookie.split('=').shift()]: [cookie.split('=').pop()],
|
|
}),
|
|
{}
|
|
)
|
|
|
|
// Text
|
|
// eslint-disable-next-line unicorn/prefer-text-content
|
|
const text = document.body.innerText.replace(/\s+/g, ' ').slice(0, 25000)
|
|
|
|
// CSS rules
|
|
let css = []
|
|
|
|
try {
|
|
for (const sheet of Array.from(document.styleSheets)) {
|
|
for (const rules of Array.from(sheet.cssRules)) {
|
|
css.push(rules.cssText)
|
|
|
|
if (css.length >= 3000) {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
// Continue
|
|
}
|
|
|
|
css = css.join('\n')
|
|
|
|
// Script tags
|
|
const scriptNodes = Array.from(document.scripts)
|
|
|
|
const scriptSrc = scriptNodes
|
|
.filter(({ src }) => src && !src.startsWith('data:text/javascript;'))
|
|
.map(({ src }) => src)
|
|
|
|
const scripts = scriptNodes
|
|
.map((node) => node.textContent)
|
|
.filter((script) => script)
|
|
|
|
// Meta tags
|
|
const meta = Array.from(document.querySelectorAll('meta')).reduce(
|
|
(metas, meta) => {
|
|
const key = meta.getAttribute('name') || meta.getAttribute('property')
|
|
|
|
if (key) {
|
|
metas[key.toLowerCase()] = metas[key.toLowerCase()] || []
|
|
|
|
metas[key.toLowerCase()].push(meta.getAttribute('content'))
|
|
}
|
|
|
|
return metas
|
|
},
|
|
{}
|
|
)
|
|
|
|
// Detect Google Ads
|
|
if (/^(www\.)?google(\.[a-z]{2,3}){1,2}$/.test(location.hostname)) {
|
|
const ads = document.querySelectorAll(
|
|
'#tads [data-text-ad] a[data-pcu]'
|
|
)
|
|
|
|
for (const ad of ads) {
|
|
Content.driver('detectTechnology', [ad.href, 'Google Ads'])
|
|
}
|
|
}
|
|
|
|
// Detect Microsoft Ads
|
|
if (/^(www\.)?bing\.com$/.test(location.hostname)) {
|
|
const ads = document.querySelectorAll('.b_ad .b_adurl cite')
|
|
|
|
for (const ad of ads) {
|
|
const url = ad.textContent.split(' ')[0].trim()
|
|
|
|
Content.driver('detectTechnology', [
|
|
url.startsWith('http') ? url : `http://${url}`,
|
|
'Microsoft Advertising',
|
|
])
|
|
}
|
|
}
|
|
|
|
// Detect Facebook Ads
|
|
if (/^(www\.)?facebook\.com$/.test(location.hostname)) {
|
|
const ads = document.querySelectorAll('a[aria-label="Advertiser"]')
|
|
|
|
for (const ad of ads) {
|
|
const urls = [
|
|
...new Set([
|
|
`https://${decodeURIComponent(
|
|
ad.href.split(/^.+\?u=https%3A%2F%2F/).pop()
|
|
)
|
|
.split('/')
|
|
.shift()}`,
|
|
|
|
// eslint-disable-next-line unicorn/prefer-text-content
|
|
`https://${ad.innerText.split('\n').pop()}`,
|
|
]),
|
|
]
|
|
|
|
urls.forEach((url) =>
|
|
Content.driver('detectTechnology', [url, 'Facebook Ads'])
|
|
)
|
|
}
|
|
}
|
|
|
|
Content.cache = { html, text, css, scriptSrc, scripts, meta, cookies }
|
|
|
|
await Content.driver('onContentLoad', [
|
|
url,
|
|
Content.cache,
|
|
Content.language,
|
|
])
|
|
|
|
const technologies = await Content.driver('getTechnologies')
|
|
|
|
await Content.onGetTechnologies(technologies)
|
|
|
|
// Delayed second pass to capture async JS
|
|
await new Promise((resolve) => setTimeout(resolve, 5000))
|
|
|
|
const js = await getJs(technologies)
|
|
|
|
await Content.driver('analyzeJs', [url, js])
|
|
} catch (error) {
|
|
Content.driver('error', error)
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Enable scripts to call Driver functions through messaging
|
|
* @param {Object} message
|
|
* @param {Object} sender
|
|
* @param {Function} callback
|
|
*/
|
|
onMessage({ source, func, args }, sender, callback) {
|
|
if (!func) {
|
|
return
|
|
}
|
|
|
|
Content.driver('log', { source, func, args })
|
|
|
|
if (!Content[func]) {
|
|
Content.error(new Error(`Method does not exist: Content.${func}`))
|
|
|
|
return
|
|
}
|
|
|
|
Promise.resolve(Content[func].call(Content[func], ...(args || [])))
|
|
.then(callback)
|
|
.catch(Content.error)
|
|
|
|
return !!callback
|
|
},
|
|
|
|
driver(func, args) {
|
|
return new Promise((resolve) => {
|
|
chrome.runtime.sendMessage(
|
|
{
|
|
source: 'content.js',
|
|
func,
|
|
args:
|
|
args instanceof Error
|
|
? [args.toString()]
|
|
: args
|
|
? Array.isArray(args)
|
|
? args
|
|
: [args]
|
|
: [],
|
|
},
|
|
(response) => {
|
|
chrome.runtime.lastError
|
|
? func === 'error'
|
|
? resolve()
|
|
: Content.driver(
|
|
'error',
|
|
new Error(
|
|
`${
|
|
chrome.runtime.lastError.message
|
|
}: Driver.${func}(${JSON.stringify(args)})`
|
|
)
|
|
)
|
|
: resolve(response)
|
|
}
|
|
)
|
|
})
|
|
},
|
|
|
|
async analyzeRequires(url, requires) {
|
|
await Promise.all(
|
|
requires.map(async ({ name, categoryId, technologies }) => {
|
|
const id = categoryId ? `category:${categoryId}` : `technology:${name}`
|
|
|
|
if (
|
|
!Content.analyzedRequires.includes(id) &&
|
|
Object.keys(Content.cache).length
|
|
) {
|
|
Content.analyzedRequires.push(id)
|
|
|
|
await Promise.all([
|
|
Content.onGetTechnologies(technologies, name, categoryId),
|
|
Content.driver('onContentLoad', [
|
|
url,
|
|
Content.cache,
|
|
Content.language,
|
|
name,
|
|
categoryId,
|
|
]),
|
|
])
|
|
}
|
|
})
|
|
)
|
|
},
|
|
|
|
/**
|
|
* Callback for getTechnologies
|
|
* @param {Array} technologies
|
|
*/
|
|
async onGetTechnologies(technologies = [], requires, categoryRequires) {
|
|
const url = location.href
|
|
|
|
const js = await getJs(technologies)
|
|
const dom = await getDom(technologies)
|
|
|
|
await Promise.all([
|
|
Content.driver('analyzeJs', [url, js, requires, categoryRequires]),
|
|
Content.driver('analyzeDom', [url, dom, requires, categoryRequires]),
|
|
])
|
|
},
|
|
}
|
|
|
|
// Enable messaging between scripts
|
|
chrome.runtime.onMessage.addListener(Content.onMessage)
|
|
|
|
if (/complete|interactive|loaded/.test(document.readyState)) {
|
|
Content.init()
|
|
} else {
|
|
document.addEventListener('DOMContentLoaded', Content.init)
|
|
}
|