mirror of
https://github.com/phishingclub/phishingclub.git
synced 2026-10-08 08:21:12 +02:00
573 lines
14 KiB
JavaScript
573 lines
14 KiB
JavaScript
// The reason to use an extra js file to hold the functions is the lint and IDE support.
|
|
// To debug just add "debugger" keyword to the line you want to pause, then run something like:
|
|
//
|
|
// go run ./lib/js/generate/main.go
|
|
// go test -run ^TestClick$ -- -rod=show,devtools
|
|
|
|
const functions = {
|
|
element(selector) {
|
|
const s = functions.selectable(this)
|
|
return s.querySelector(selector)
|
|
},
|
|
|
|
triggerFavicon() {
|
|
return new Promise((resolve, reject) => {
|
|
const faviconElement = document.querySelector('link[rel~=icon]')
|
|
const href = (faviconElement && faviconElement.href) || '/favicon.ico'
|
|
const faviconUrl = new URL(href, window.location).toString()
|
|
const xhr = new XMLHttpRequest()
|
|
xhr.open('GET', faviconUrl)
|
|
|
|
xhr.ontimeout = function () {
|
|
reject({
|
|
errorType: 'timeout_error',
|
|
xhr: xhr
|
|
})
|
|
}
|
|
|
|
xhr.onreadystatechange = function () {
|
|
if (xhr.readyState === 4) {
|
|
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
|
|
resolve({
|
|
status: xhr.status,
|
|
statusText: xhr.statusText,
|
|
responseText: xhr.responseText
|
|
})
|
|
} else {
|
|
reject({
|
|
errorType: 'status_error',
|
|
xhr: xhr,
|
|
status: xhr.status,
|
|
statusText: xhr.statusText,
|
|
responseText: xhr.responseText
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
xhr.onerror = function () {
|
|
reject({
|
|
errorType: 'onerror',
|
|
xhr: xhr,
|
|
status: xhr.status,
|
|
statusText: xhr.statusText,
|
|
responseText: xhr.responseText
|
|
})
|
|
}
|
|
xhr.send()
|
|
})
|
|
},
|
|
|
|
elements(selector) {
|
|
return functions.selectable(this).querySelectorAll(selector)
|
|
},
|
|
|
|
elementX(xPath) {
|
|
const s = functions.selectable(this)
|
|
return document.evaluate(
|
|
xPath,
|
|
s,
|
|
null,
|
|
XPathResult.FIRST_ORDERED_NODE_TYPE
|
|
).singleNodeValue
|
|
},
|
|
|
|
elementsX(xpath) {
|
|
const s = functions.selectable(this)
|
|
const iter = document.evaluate(
|
|
xpath,
|
|
s,
|
|
null,
|
|
XPathResult.ORDERED_NODE_ITERATOR_TYPE
|
|
)
|
|
const list = []
|
|
let el
|
|
while ((el = iter.iterateNext())) list.push(el)
|
|
return list
|
|
},
|
|
|
|
elementR(selector, regex) {
|
|
var reg
|
|
var m = regex.match(/(\/?)(.+)\1([a-z]*)/i)
|
|
// cSpell:ignore gmix
|
|
if (m[3] && !/^(?!.*?(.).*?\1)[gmixXsuUAJ]+$/.test(m[3]))
|
|
reg = new RegExp(regex)
|
|
else reg = new RegExp(m[2], m[3])
|
|
|
|
const s = functions.selectable(this)
|
|
const el = Array.from(s.querySelectorAll(selector)).find((e) =>
|
|
reg.test(functions.text.call(e))
|
|
)
|
|
return el ? el : null
|
|
},
|
|
|
|
parents(selector) {
|
|
let p = this.parentElement
|
|
const list = []
|
|
while (p) {
|
|
if (p.matches(selector)) {
|
|
list.push(p)
|
|
}
|
|
p = p.parentElement
|
|
}
|
|
return list
|
|
},
|
|
|
|
containsElement(target) {
|
|
var node = target
|
|
while (node != null) {
|
|
if (node === this) {
|
|
return true
|
|
}
|
|
node = node.parentElement
|
|
}
|
|
return false
|
|
},
|
|
|
|
async initMouseTracer(iconId, icon) {
|
|
await functions.waitLoad()
|
|
|
|
if (document.getElementById(iconId)) {
|
|
return
|
|
}
|
|
|
|
const tmp = document.createElement('div')
|
|
tmp.innerHTML = icon
|
|
const svg = tmp.lastChild
|
|
svg.id = iconId
|
|
svg.style =
|
|
'position: absolute; z-index: 2147483647; width: 17px; pointer-events: none;'
|
|
svg.removeAttribute('width')
|
|
svg.removeAttribute('height')
|
|
document.body.parentElement.appendChild(svg)
|
|
},
|
|
|
|
updateMouseTracer(iconId, x, y) {
|
|
const svg = document.getElementById(iconId)
|
|
if (!svg) {
|
|
return false
|
|
}
|
|
svg.style.left = x - 2 + 'px'
|
|
svg.style.top = y - 3 + 'px'
|
|
return true
|
|
},
|
|
|
|
rect() {
|
|
const b = functions.tag(this).getBoundingClientRect()
|
|
return { x: b.x, y: b.y, width: b.width, height: b.height }
|
|
},
|
|
|
|
async overlay(id, left, top, width, height, msg) {
|
|
await functions.waitLoad()
|
|
|
|
const div = document.createElement('div')
|
|
div.id = id
|
|
div.style = `position: fixed; z-index:2147483647; border: 2px dashed red;
|
|
border-radius: 3px; box-shadow: #5f3232 0 0 3px; pointer-events: none;
|
|
box-sizing: border-box;
|
|
left: ${left}px;
|
|
top: ${top}px;
|
|
height: ${height}px;
|
|
width: ${width}px;`
|
|
|
|
if (width * height === 0) {
|
|
div.style.border = 'none'
|
|
}
|
|
|
|
if (!msg) {
|
|
document.body.parentElement.appendChild(div)
|
|
return
|
|
}
|
|
|
|
const msgDiv = document.createElement('div')
|
|
msgDiv.style = `position: absolute; color: #cc26d6; font-size: 12px; background: #ffffffeb;
|
|
box-shadow: #333 0 0 3px; padding: 2px 5px; border-radius: 3px; white-space: nowrap;
|
|
top: ${height}px;`
|
|
|
|
msgDiv.innerHTML = msg
|
|
div.appendChild(msgDiv)
|
|
document.body.parentElement.appendChild(div)
|
|
|
|
if (window.innerHeight < msgDiv.offsetHeight + top + height) {
|
|
msgDiv.style.top = -msgDiv.offsetHeight - 2 + 'px'
|
|
}
|
|
|
|
if (window.innerWidth < msgDiv.offsetWidth + left) {
|
|
msgDiv.style.left = window.innerWidth - msgDiv.offsetWidth - left + 'px'
|
|
}
|
|
},
|
|
|
|
async elementOverlay(id, msg) {
|
|
const interval = 100
|
|
const el = functions.tag(this)
|
|
|
|
let pre = el.getBoundingClientRect()
|
|
await functions.overlay(id, pre.left, pre.top, pre.width, pre.height, msg)
|
|
|
|
const update = () => {
|
|
const overlay = document.getElementById(id)
|
|
if (overlay === null) return
|
|
|
|
const box = el.getBoundingClientRect()
|
|
if (
|
|
pre.left === box.left &&
|
|
pre.top === box.top &&
|
|
pre.width === box.width &&
|
|
pre.height === box.height
|
|
) {
|
|
setTimeout(update, interval)
|
|
return
|
|
}
|
|
|
|
overlay.style.left = box.left + 'px'
|
|
overlay.style.top = box.top + 'px'
|
|
overlay.style.width = box.width + 'px'
|
|
overlay.style.height = box.height + 'px'
|
|
pre = box
|
|
|
|
setTimeout(update, interval)
|
|
}
|
|
|
|
setTimeout(update, interval)
|
|
},
|
|
|
|
removeOverlay(id) {
|
|
const el = document.getElementById(id)
|
|
// prevent override like prototype.js
|
|
el && Element.prototype.remove.call(el)
|
|
},
|
|
|
|
waitIdle(timeout) {
|
|
return new Promise((resolve) => {
|
|
window.requestIdleCallback(resolve, { timeout })
|
|
})
|
|
},
|
|
|
|
waitLoad() {
|
|
const isWin = this === window
|
|
return new Promise((resolve, reject) => {
|
|
if (isWin) {
|
|
if (document.readyState === 'complete') return resolve()
|
|
window.addEventListener('load', resolve)
|
|
} else {
|
|
if (this.complete === undefined || this.complete) {
|
|
resolve()
|
|
} else {
|
|
this.addEventListener('load', resolve)
|
|
this.addEventListener('error', reject)
|
|
}
|
|
}
|
|
})
|
|
},
|
|
|
|
inputEvent() {
|
|
this.dispatchEvent(new Event('input', { bubbles: true }))
|
|
this.dispatchEvent(new Event('change', { bubbles: true }))
|
|
},
|
|
|
|
inputTime(stamp) {
|
|
const time = new Date(stamp)
|
|
|
|
const pad = (n) => n.toString().padStart(2, '0')
|
|
|
|
const y = time.getFullYear()
|
|
const mon = pad(time.getMonth() + 1)
|
|
const d = pad(time.getDate())
|
|
const h = pad(time.getHours())
|
|
const min = pad(time.getMinutes())
|
|
|
|
switch (this.type) {
|
|
case 'date':
|
|
this.value = `${y}-${mon}-${d}`
|
|
break
|
|
case 'datetime-local':
|
|
this.value = `${y}-${mon}-${d}T${h}:${min}`
|
|
break
|
|
case 'month':
|
|
this.value = `${y}-${mon}`
|
|
break
|
|
case 'time':
|
|
this.value = `${h}:${min}`
|
|
break
|
|
}
|
|
|
|
functions.inputEvent.call(this)
|
|
},
|
|
|
|
inputColor(color) {
|
|
this.value = `${color}`
|
|
|
|
functions.inputEvent.call(this)
|
|
},
|
|
selectText(pattern) {
|
|
const m = this.value.match(new RegExp(pattern))
|
|
if (m) {
|
|
this.setSelectionRange(m.index, m.index + m[0].length)
|
|
}
|
|
},
|
|
|
|
selectAllText() {
|
|
this.select()
|
|
},
|
|
|
|
select(selectors, selected, type) {
|
|
let matchers
|
|
switch (type) {
|
|
case 'regex':
|
|
matchers = selectors.map((s) => {
|
|
const reg = new RegExp(s)
|
|
return (el) => reg.test(el.innerText)
|
|
})
|
|
break
|
|
case 'css-selector':
|
|
matchers = selectors.map((s) => (el) => el.matches(s))
|
|
break
|
|
default:
|
|
matchers = selectors.map((s) => (el) => el.innerText.includes(s))
|
|
break
|
|
}
|
|
|
|
const opts = Array.from(this.options)
|
|
let has = false
|
|
matchers.forEach((s) => {
|
|
const el = opts.find(s)
|
|
if (el) {
|
|
el.selected = selected
|
|
has = true
|
|
return
|
|
}
|
|
})
|
|
|
|
this.dispatchEvent(new Event('input', { bubbles: true }))
|
|
this.dispatchEvent(new Event('change', { bubbles: true }))
|
|
|
|
return has
|
|
},
|
|
|
|
visible() {
|
|
const el = functions.tag(this)
|
|
const box = el.getBoundingClientRect()
|
|
const style = window.getComputedStyle(el)
|
|
return (
|
|
style.display !== 'none' &&
|
|
style.visibility !== 'hidden' &&
|
|
!!(box.top || box.bottom || box.width || box.height)
|
|
)
|
|
},
|
|
|
|
invisible() {
|
|
return !functions.visible.apply(this)
|
|
},
|
|
|
|
text() {
|
|
switch (this.tagName) {
|
|
case 'INPUT':
|
|
case 'TEXTAREA':
|
|
return this.value || this.placeholder
|
|
case 'SELECT':
|
|
return Array.from(this.selectedOptions)
|
|
.map((el) => el.innerText)
|
|
.join()
|
|
case undefined:
|
|
return this.textContent
|
|
default:
|
|
return this.innerText
|
|
}
|
|
},
|
|
|
|
resource() {
|
|
return new Promise((resolve, reject) => {
|
|
if (this.complete) {
|
|
return resolve(this.currentSrc)
|
|
}
|
|
this.addEventListener('load', () => resolve(this.currentSrc))
|
|
this.addEventListener('error', (e) => reject(e))
|
|
})
|
|
},
|
|
|
|
addScriptTag(id, url, content) {
|
|
if (document.getElementById(id)) return
|
|
|
|
return new Promise((resolve, reject) => {
|
|
var s = document.createElement('script')
|
|
|
|
if (url) {
|
|
s.src = url
|
|
s.onload = resolve
|
|
} else {
|
|
s.type = 'text/javascript'
|
|
s.text = content
|
|
resolve()
|
|
}
|
|
|
|
s.id = id
|
|
s.onerror = reject
|
|
document.head.appendChild(s)
|
|
})
|
|
},
|
|
|
|
addStyleTag(id, url, content) {
|
|
if (document.getElementById(id)) return
|
|
|
|
return new Promise((resolve, reject) => {
|
|
var el
|
|
|
|
if (url) {
|
|
el = document.createElement('link')
|
|
el.rel = 'stylesheet'
|
|
el.href = url
|
|
} else {
|
|
el = document.createElement('style')
|
|
el.type = 'text/css'
|
|
el.appendChild(document.createTextNode(content))
|
|
resolve()
|
|
}
|
|
|
|
el.id = id
|
|
el.onload = resolve
|
|
el.onerror = reject
|
|
document.head.appendChild(el)
|
|
})
|
|
},
|
|
|
|
selectable(s) {
|
|
return s.querySelector ? s : document
|
|
},
|
|
|
|
tag(el) {
|
|
return el.tagName ? el : el.parentElement
|
|
},
|
|
|
|
exposeFunc(name, bind) {
|
|
let callbackCount = 0
|
|
window[name] = (req) =>
|
|
new Promise((resolve, reject) => {
|
|
const cb = bind + '_cb' + callbackCount++
|
|
window[cb] = (res, err) => {
|
|
delete window[cb]
|
|
err ? reject(err) : resolve(res)
|
|
}
|
|
window[bind](JSON.stringify({ req, cb }))
|
|
})
|
|
},
|
|
|
|
getXPath(optimized) {
|
|
class Step {
|
|
constructor(value, optimized) {
|
|
this.value = value
|
|
this.optimized = optimized || false
|
|
}
|
|
toString() {
|
|
return this.value
|
|
}
|
|
}
|
|
const xPathValue = function xPathValue(node, optimized) {
|
|
let ownValue
|
|
const ownIndex = xPathIndex(node)
|
|
if (ownIndex === -1) {
|
|
return null
|
|
}
|
|
switch (node.nodeType) {
|
|
case Node.ELEMENT_NODE:
|
|
if (optimized && node.id) {
|
|
return new Step(`//*[@id='${node.id}']`, true)
|
|
}
|
|
ownValue = node.localName
|
|
break
|
|
case Node.ATTRIBUTE_NODE:
|
|
ownValue = `@${node.nodeName}`
|
|
break
|
|
case Node.TEXT_NODE:
|
|
case Node.CDATA_SECTION_NODE:
|
|
ownValue = 'text()'
|
|
break
|
|
case Node.PROCESSING_INSTRUCTION_NODE:
|
|
ownValue = 'processing-instruction()'
|
|
break
|
|
case Node.COMMENT_NODE:
|
|
ownValue = 'comment()'
|
|
break
|
|
case Node.DOCUMENT_NODE:
|
|
ownValue = ''
|
|
break
|
|
default:
|
|
ownValue = ''
|
|
break
|
|
}
|
|
if (ownIndex > 0) {
|
|
ownValue += `[${ownIndex}]`
|
|
}
|
|
return new Step(ownValue, node.nodeType === Node.DOCUMENT_NODE)
|
|
}
|
|
const xPathIndex = function xPathIndex(node) {
|
|
function areNodesSimilar(left, right) {
|
|
if (left === right) {
|
|
return true
|
|
}
|
|
if (
|
|
left.nodeType === Node.ELEMENT_NODE &&
|
|
right.nodeType === Node.ELEMENT_NODE
|
|
) {
|
|
return left.localName === right.localName
|
|
}
|
|
if (left.nodeType === right.nodeType) {
|
|
return true
|
|
}
|
|
const leftType =
|
|
left.nodeType === Node.CDATA_SECTION_NODE
|
|
? Node.TEXT_NODE
|
|
: left.nodeType
|
|
const rightType =
|
|
right.nodeType === Node.CDATA_SECTION_NODE
|
|
? Node.TEXT_NODE
|
|
: right.nodeType
|
|
return leftType === rightType
|
|
}
|
|
const parentNode = node.parentNode
|
|
const siblings = parentNode ? parentNode.children : null
|
|
if (!siblings) {
|
|
return 0
|
|
}
|
|
let hasSameNamedElements
|
|
for (let i = 0; i < siblings.length; ++i) {
|
|
if (areNodesSimilar(node, siblings[i]) && !(siblings[i] === node)) {
|
|
hasSameNamedElements = true
|
|
break
|
|
}
|
|
}
|
|
if (!hasSameNamedElements) {
|
|
return 0
|
|
}
|
|
let ownIndex = 1
|
|
for (let i = 0; i < siblings.length; ++i) {
|
|
if (areNodesSimilar(node, siblings[i])) {
|
|
if (siblings[i] === node) {
|
|
return ownIndex
|
|
}
|
|
++ownIndex
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
const node = this
|
|
if (node.nodeType === Node.DOCUMENT_NODE) {
|
|
return '/'
|
|
}
|
|
const steps = []
|
|
let contextNode = node
|
|
while (contextNode) {
|
|
const step = xPathValue(contextNode, optimized)
|
|
if (!step) {
|
|
break
|
|
}
|
|
steps.push(step)
|
|
if (step.optimized) {
|
|
break
|
|
}
|
|
contextNode = contextNode.parentNode
|
|
}
|
|
steps.reverse()
|
|
return (steps.length && steps[0].optimized ? '' : '/') + steps.join('/')
|
|
}
|
|
}
|