feat(security,ui): self-host React deps, Tailwind, fonts; strict CSP; local QR; better selection state
Replace CDN React/ReactDOM/Babel with local libs; remove Babel and inline scripts Build Tailwind locally, add safelist; switch to assets/tailwind.css Self-host Font Awesome and Inter (CSS + woff2); remove external font CDNs Implement strict CSP (no unsafe-inline/eval; scripts/styles/fonts from self) Extract inline handlers; move PWA scripts to external files Add local QR code generation (qrcode lib) and remove api.qrserver.com Improve SessionTypeSelector visual selection (highlighted background and ring) Keep PWA working with service worker and offline assets Refs: CSP hardening, offline-first, no external dependencies
This commit is contained in:
63
node_modules/qrcode/lib/renderer/canvas.js
generated
vendored
Normal file
63
node_modules/qrcode/lib/renderer/canvas.js
generated
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
const Utils = require('./utils')
|
||||
|
||||
function clearCanvas (ctx, canvas, size) {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
if (!canvas.style) canvas.style = {}
|
||||
canvas.height = size
|
||||
canvas.width = size
|
||||
canvas.style.height = size + 'px'
|
||||
canvas.style.width = size + 'px'
|
||||
}
|
||||
|
||||
function getCanvasElement () {
|
||||
try {
|
||||
return document.createElement('canvas')
|
||||
} catch (e) {
|
||||
throw new Error('You need to specify a canvas element')
|
||||
}
|
||||
}
|
||||
|
||||
exports.render = function render (qrData, canvas, options) {
|
||||
let opts = options
|
||||
let canvasEl = canvas
|
||||
|
||||
if (typeof opts === 'undefined' && (!canvas || !canvas.getContext)) {
|
||||
opts = canvas
|
||||
canvas = undefined
|
||||
}
|
||||
|
||||
if (!canvas) {
|
||||
canvasEl = getCanvasElement()
|
||||
}
|
||||
|
||||
opts = Utils.getOptions(opts)
|
||||
const size = Utils.getImageWidth(qrData.modules.size, opts)
|
||||
|
||||
const ctx = canvasEl.getContext('2d')
|
||||
const image = ctx.createImageData(size, size)
|
||||
Utils.qrToImageData(image.data, qrData, opts)
|
||||
|
||||
clearCanvas(ctx, canvasEl, size)
|
||||
ctx.putImageData(image, 0, 0)
|
||||
|
||||
return canvasEl
|
||||
}
|
||||
|
||||
exports.renderToDataURL = function renderToDataURL (qrData, canvas, options) {
|
||||
let opts = options
|
||||
|
||||
if (typeof opts === 'undefined' && (!canvas || !canvas.getContext)) {
|
||||
opts = canvas
|
||||
canvas = undefined
|
||||
}
|
||||
|
||||
if (!opts) opts = {}
|
||||
|
||||
const canvasEl = exports.render(qrData, canvas, opts)
|
||||
|
||||
const type = opts.type || 'image/png'
|
||||
const rendererOpts = opts.rendererOpts || {}
|
||||
|
||||
return canvasEl.toDataURL(type, rendererOpts.quality)
|
||||
}
|
||||
78
node_modules/qrcode/lib/renderer/png.js
generated
vendored
Normal file
78
node_modules/qrcode/lib/renderer/png.js
generated
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
const fs = require('fs')
|
||||
const PNG = require('pngjs').PNG
|
||||
const Utils = require('./utils')
|
||||
|
||||
exports.render = function render (qrData, options) {
|
||||
const opts = Utils.getOptions(options)
|
||||
const pngOpts = opts.rendererOpts
|
||||
const size = Utils.getImageWidth(qrData.modules.size, opts)
|
||||
|
||||
pngOpts.width = size
|
||||
pngOpts.height = size
|
||||
|
||||
const pngImage = new PNG(pngOpts)
|
||||
Utils.qrToImageData(pngImage.data, qrData, opts)
|
||||
|
||||
return pngImage
|
||||
}
|
||||
|
||||
exports.renderToDataURL = function renderToDataURL (qrData, options, cb) {
|
||||
if (typeof cb === 'undefined') {
|
||||
cb = options
|
||||
options = undefined
|
||||
}
|
||||
|
||||
exports.renderToBuffer(qrData, options, function (err, output) {
|
||||
if (err) cb(err)
|
||||
let url = 'data:image/png;base64,'
|
||||
url += output.toString('base64')
|
||||
cb(null, url)
|
||||
})
|
||||
}
|
||||
|
||||
exports.renderToBuffer = function renderToBuffer (qrData, options, cb) {
|
||||
if (typeof cb === 'undefined') {
|
||||
cb = options
|
||||
options = undefined
|
||||
}
|
||||
|
||||
const png = exports.render(qrData, options)
|
||||
const buffer = []
|
||||
|
||||
png.on('error', cb)
|
||||
|
||||
png.on('data', function (data) {
|
||||
buffer.push(data)
|
||||
})
|
||||
|
||||
png.on('end', function () {
|
||||
cb(null, Buffer.concat(buffer))
|
||||
})
|
||||
|
||||
png.pack()
|
||||
}
|
||||
|
||||
exports.renderToFile = function renderToFile (path, qrData, options, cb) {
|
||||
if (typeof cb === 'undefined') {
|
||||
cb = options
|
||||
options = undefined
|
||||
}
|
||||
|
||||
let called = false
|
||||
const done = (...args) => {
|
||||
if (called) return
|
||||
called = true
|
||||
cb.apply(null, args)
|
||||
}
|
||||
const stream = fs.createWriteStream(path)
|
||||
|
||||
stream.on('error', done)
|
||||
stream.on('close', done)
|
||||
|
||||
exports.renderToFileStream(stream, qrData, options)
|
||||
}
|
||||
|
||||
exports.renderToFileStream = function renderToFileStream (stream, qrData, options) {
|
||||
const png = exports.render(qrData, options)
|
||||
png.pack().pipe(stream)
|
||||
}
|
||||
81
node_modules/qrcode/lib/renderer/svg-tag.js
generated
vendored
Normal file
81
node_modules/qrcode/lib/renderer/svg-tag.js
generated
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
const Utils = require('./utils')
|
||||
|
||||
function getColorAttrib (color, attrib) {
|
||||
const alpha = color.a / 255
|
||||
const str = attrib + '="' + color.hex + '"'
|
||||
|
||||
return alpha < 1
|
||||
? str + ' ' + attrib + '-opacity="' + alpha.toFixed(2).slice(1) + '"'
|
||||
: str
|
||||
}
|
||||
|
||||
function svgCmd (cmd, x, y) {
|
||||
let str = cmd + x
|
||||
if (typeof y !== 'undefined') str += ' ' + y
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
function qrToPath (data, size, margin) {
|
||||
let path = ''
|
||||
let moveBy = 0
|
||||
let newRow = false
|
||||
let lineLength = 0
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const col = Math.floor(i % size)
|
||||
const row = Math.floor(i / size)
|
||||
|
||||
if (!col && !newRow) newRow = true
|
||||
|
||||
if (data[i]) {
|
||||
lineLength++
|
||||
|
||||
if (!(i > 0 && col > 0 && data[i - 1])) {
|
||||
path += newRow
|
||||
? svgCmd('M', col + margin, 0.5 + row + margin)
|
||||
: svgCmd('m', moveBy, 0)
|
||||
|
||||
moveBy = 0
|
||||
newRow = false
|
||||
}
|
||||
|
||||
if (!(col + 1 < size && data[i + 1])) {
|
||||
path += svgCmd('h', lineLength)
|
||||
lineLength = 0
|
||||
}
|
||||
} else {
|
||||
moveBy++
|
||||
}
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
exports.render = function render (qrData, options, cb) {
|
||||
const opts = Utils.getOptions(options)
|
||||
const size = qrData.modules.size
|
||||
const data = qrData.modules.data
|
||||
const qrcodesize = size + opts.margin * 2
|
||||
|
||||
const bg = !opts.color.light.a
|
||||
? ''
|
||||
: '<path ' + getColorAttrib(opts.color.light, 'fill') +
|
||||
' d="M0 0h' + qrcodesize + 'v' + qrcodesize + 'H0z"/>'
|
||||
|
||||
const path =
|
||||
'<path ' + getColorAttrib(opts.color.dark, 'stroke') +
|
||||
' d="' + qrToPath(data, size, opts.margin) + '"/>'
|
||||
|
||||
const viewBox = 'viewBox="' + '0 0 ' + qrcodesize + ' ' + qrcodesize + '"'
|
||||
|
||||
const width = !opts.width ? '' : 'width="' + opts.width + '" height="' + opts.width + '" '
|
||||
|
||||
const svgTag = '<svg xmlns="http://www.w3.org/2000/svg" ' + width + viewBox + ' shape-rendering="crispEdges">' + bg + path + '</svg>\n'
|
||||
|
||||
if (typeof cb === 'function') {
|
||||
cb(null, svgTag)
|
||||
}
|
||||
|
||||
return svgTag
|
||||
}
|
||||
19
node_modules/qrcode/lib/renderer/svg.js
generated
vendored
Normal file
19
node_modules/qrcode/lib/renderer/svg.js
generated
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
const svgTagRenderer = require('./svg-tag')
|
||||
|
||||
exports.render = svgTagRenderer.render
|
||||
|
||||
exports.renderToFile = function renderToFile (path, qrData, options, cb) {
|
||||
if (typeof cb === 'undefined') {
|
||||
cb = options
|
||||
options = undefined
|
||||
}
|
||||
|
||||
const fs = require('fs')
|
||||
const svgTag = exports.render(qrData, options)
|
||||
|
||||
const xmlStr = '<?xml version="1.0" encoding="utf-8"?>' +
|
||||
'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">' +
|
||||
svgTag
|
||||
|
||||
fs.writeFile(path, xmlStr, cb)
|
||||
}
|
||||
9
node_modules/qrcode/lib/renderer/terminal.js
generated
vendored
Normal file
9
node_modules/qrcode/lib/renderer/terminal.js
generated
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
const big = require('./terminal/terminal')
|
||||
const small = require('./terminal/terminal-small')
|
||||
|
||||
exports.render = function (qrData, options, cb) {
|
||||
if (options && options.small) {
|
||||
return small.render(qrData, options, cb)
|
||||
}
|
||||
return big.render(qrData, options, cb)
|
||||
}
|
||||
85
node_modules/qrcode/lib/renderer/terminal/terminal-small.js
generated
vendored
Normal file
85
node_modules/qrcode/lib/renderer/terminal/terminal-small.js
generated
vendored
Normal file
@@ -0,0 +1,85 @@
|
||||
const backgroundWhite = '\x1b[47m'
|
||||
const backgroundBlack = '\x1b[40m'
|
||||
const foregroundWhite = '\x1b[37m'
|
||||
const foregroundBlack = '\x1b[30m'
|
||||
const reset = '\x1b[0m'
|
||||
const lineSetupNormal = backgroundWhite + foregroundBlack // setup colors
|
||||
const lineSetupInverse = backgroundBlack + foregroundWhite // setup colors
|
||||
|
||||
const createPalette = function (lineSetup, foregroundWhite, foregroundBlack) {
|
||||
return {
|
||||
// 1 ... white, 2 ... black, 0 ... transparent (default)
|
||||
|
||||
'00': reset + ' ' + lineSetup,
|
||||
'01': reset + foregroundWhite + '▄' + lineSetup,
|
||||
'02': reset + foregroundBlack + '▄' + lineSetup,
|
||||
10: reset + foregroundWhite + '▀' + lineSetup,
|
||||
11: ' ',
|
||||
12: '▄',
|
||||
20: reset + foregroundBlack + '▀' + lineSetup,
|
||||
21: '▀',
|
||||
22: '█'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns code for QR pixel
|
||||
* @param {boolean[][]} modules
|
||||
* @param {number} size
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @return {'0' | '1' | '2'}
|
||||
*/
|
||||
const mkCodePixel = function (modules, size, x, y) {
|
||||
const sizePlus = size + 1
|
||||
if ((x >= sizePlus) || (y >= sizePlus) || (y < -1) || (x < -1)) return '0'
|
||||
if ((x >= size) || (y >= size) || (y < 0) || (x < 0)) return '1'
|
||||
const idx = (y * size) + x
|
||||
return modules[idx] ? '2' : '1'
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns code for four QR pixels. Suitable as key in palette.
|
||||
* @param {boolean[][]} modules
|
||||
* @param {number} size
|
||||
* @param {number} x
|
||||
* @param {number} y
|
||||
* @return {keyof palette}
|
||||
*/
|
||||
const mkCode = function (modules, size, x, y) {
|
||||
return (
|
||||
mkCodePixel(modules, size, x, y) +
|
||||
mkCodePixel(modules, size, x, y + 1)
|
||||
)
|
||||
}
|
||||
|
||||
exports.render = function (qrData, options, cb) {
|
||||
const size = qrData.modules.size
|
||||
const data = qrData.modules.data
|
||||
|
||||
const inverse = !!(options && options.inverse)
|
||||
const lineSetup = options && options.inverse ? lineSetupInverse : lineSetupNormal
|
||||
const white = inverse ? foregroundBlack : foregroundWhite
|
||||
const black = inverse ? foregroundWhite : foregroundBlack
|
||||
|
||||
const palette = createPalette(lineSetup, white, black)
|
||||
const newLine = reset + '\n' + lineSetup
|
||||
|
||||
let output = lineSetup // setup colors
|
||||
|
||||
for (let y = -1; y < size + 1; y += 2) {
|
||||
for (let x = -1; x < size; x++) {
|
||||
output += palette[mkCode(data, size, x, y)]
|
||||
}
|
||||
|
||||
output += palette[mkCode(data, size, size, y)] + newLine
|
||||
}
|
||||
|
||||
output += reset
|
||||
|
||||
if (typeof cb === 'function') {
|
||||
cb(null, output)
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
49
node_modules/qrcode/lib/renderer/terminal/terminal.js
generated
vendored
Normal file
49
node_modules/qrcode/lib/renderer/terminal/terminal.js
generated
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
// let Utils = require('./utils')
|
||||
|
||||
exports.render = function (qrData, options, cb) {
|
||||
const size = qrData.modules.size
|
||||
const data = qrData.modules.data
|
||||
|
||||
// let opts = Utils.getOptions(options)
|
||||
|
||||
// use same scheme as https://github.com/gtanner/qrcode-terminal because it actually works! =)
|
||||
const black = '\x1b[40m \x1b[0m'
|
||||
const white = '\x1b[47m \x1b[0m'
|
||||
|
||||
let output = ''
|
||||
const hMargin = Array(size + 3).join(white)
|
||||
const vMargin = Array(2).join(white)
|
||||
|
||||
output += hMargin + '\n'
|
||||
for (let i = 0; i < size; ++i) {
|
||||
output += white
|
||||
for (let j = 0; j < size; j++) {
|
||||
// let topModule = data[i * size + j]
|
||||
// let bottomModule = data[(i + 1) * size + j]
|
||||
|
||||
output += data[i * size + j] ? black : white// getBlockChar(topModule, bottomModule)
|
||||
}
|
||||
// output += white+'\n'
|
||||
output += vMargin + '\n'
|
||||
}
|
||||
|
||||
output += hMargin + '\n'
|
||||
|
||||
if (typeof cb === 'function') {
|
||||
cb(null, output)
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
/*
|
||||
exports.renderToFile = function renderToFile (path, qrData, options, cb) {
|
||||
if (typeof cb === 'undefined') {
|
||||
cb = options
|
||||
options = undefined
|
||||
}
|
||||
|
||||
let fs = require('fs')
|
||||
let utf8 = exports.render(qrData, options)
|
||||
fs.writeFile(path, utf8, cb)
|
||||
}
|
||||
*/
|
||||
71
node_modules/qrcode/lib/renderer/utf8.js
generated
vendored
Normal file
71
node_modules/qrcode/lib/renderer/utf8.js
generated
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
const Utils = require('./utils')
|
||||
|
||||
const BLOCK_CHAR = {
|
||||
WW: ' ',
|
||||
WB: '▄',
|
||||
BB: '█',
|
||||
BW: '▀'
|
||||
}
|
||||
|
||||
const INVERTED_BLOCK_CHAR = {
|
||||
BB: ' ',
|
||||
BW: '▄',
|
||||
WW: '█',
|
||||
WB: '▀'
|
||||
}
|
||||
|
||||
function getBlockChar (top, bottom, blocks) {
|
||||
if (top && bottom) return blocks.BB
|
||||
if (top && !bottom) return blocks.BW
|
||||
if (!top && bottom) return blocks.WB
|
||||
return blocks.WW
|
||||
}
|
||||
|
||||
exports.render = function (qrData, options, cb) {
|
||||
const opts = Utils.getOptions(options)
|
||||
let blocks = BLOCK_CHAR
|
||||
if (opts.color.dark.hex === '#ffffff' || opts.color.light.hex === '#000000') {
|
||||
blocks = INVERTED_BLOCK_CHAR
|
||||
}
|
||||
|
||||
const size = qrData.modules.size
|
||||
const data = qrData.modules.data
|
||||
|
||||
let output = ''
|
||||
let hMargin = Array(size + (opts.margin * 2) + 1).join(blocks.WW)
|
||||
hMargin = Array((opts.margin / 2) + 1).join(hMargin + '\n')
|
||||
|
||||
const vMargin = Array(opts.margin + 1).join(blocks.WW)
|
||||
|
||||
output += hMargin
|
||||
for (let i = 0; i < size; i += 2) {
|
||||
output += vMargin
|
||||
for (let j = 0; j < size; j++) {
|
||||
const topModule = data[i * size + j]
|
||||
const bottomModule = data[(i + 1) * size + j]
|
||||
|
||||
output += getBlockChar(topModule, bottomModule, blocks)
|
||||
}
|
||||
|
||||
output += vMargin + '\n'
|
||||
}
|
||||
|
||||
output += hMargin.slice(0, -1)
|
||||
|
||||
if (typeof cb === 'function') {
|
||||
cb(null, output)
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
exports.renderToFile = function renderToFile (path, qrData, options, cb) {
|
||||
if (typeof cb === 'undefined') {
|
||||
cb = options
|
||||
options = undefined
|
||||
}
|
||||
|
||||
const fs = require('fs')
|
||||
const utf8 = exports.render(qrData, options)
|
||||
fs.writeFile(path, utf8, cb)
|
||||
}
|
||||
99
node_modules/qrcode/lib/renderer/utils.js
generated
vendored
Normal file
99
node_modules/qrcode/lib/renderer/utils.js
generated
vendored
Normal file
@@ -0,0 +1,99 @@
|
||||
function hex2rgba (hex) {
|
||||
if (typeof hex === 'number') {
|
||||
hex = hex.toString()
|
||||
}
|
||||
|
||||
if (typeof hex !== 'string') {
|
||||
throw new Error('Color should be defined as hex string')
|
||||
}
|
||||
|
||||
let hexCode = hex.slice().replace('#', '').split('')
|
||||
if (hexCode.length < 3 || hexCode.length === 5 || hexCode.length > 8) {
|
||||
throw new Error('Invalid hex color: ' + hex)
|
||||
}
|
||||
|
||||
// Convert from short to long form (fff -> ffffff)
|
||||
if (hexCode.length === 3 || hexCode.length === 4) {
|
||||
hexCode = Array.prototype.concat.apply([], hexCode.map(function (c) {
|
||||
return [c, c]
|
||||
}))
|
||||
}
|
||||
|
||||
// Add default alpha value
|
||||
if (hexCode.length === 6) hexCode.push('F', 'F')
|
||||
|
||||
const hexValue = parseInt(hexCode.join(''), 16)
|
||||
|
||||
return {
|
||||
r: (hexValue >> 24) & 255,
|
||||
g: (hexValue >> 16) & 255,
|
||||
b: (hexValue >> 8) & 255,
|
||||
a: hexValue & 255,
|
||||
hex: '#' + hexCode.slice(0, 6).join('')
|
||||
}
|
||||
}
|
||||
|
||||
exports.getOptions = function getOptions (options) {
|
||||
if (!options) options = {}
|
||||
if (!options.color) options.color = {}
|
||||
|
||||
const margin = typeof options.margin === 'undefined' ||
|
||||
options.margin === null ||
|
||||
options.margin < 0
|
||||
? 4
|
||||
: options.margin
|
||||
|
||||
const width = options.width && options.width >= 21 ? options.width : undefined
|
||||
const scale = options.scale || 4
|
||||
|
||||
return {
|
||||
width: width,
|
||||
scale: width ? 4 : scale,
|
||||
margin: margin,
|
||||
color: {
|
||||
dark: hex2rgba(options.color.dark || '#000000ff'),
|
||||
light: hex2rgba(options.color.light || '#ffffffff')
|
||||
},
|
||||
type: options.type,
|
||||
rendererOpts: options.rendererOpts || {}
|
||||
}
|
||||
}
|
||||
|
||||
exports.getScale = function getScale (qrSize, opts) {
|
||||
return opts.width && opts.width >= qrSize + opts.margin * 2
|
||||
? opts.width / (qrSize + opts.margin * 2)
|
||||
: opts.scale
|
||||
}
|
||||
|
||||
exports.getImageWidth = function getImageWidth (qrSize, opts) {
|
||||
const scale = exports.getScale(qrSize, opts)
|
||||
return Math.floor((qrSize + opts.margin * 2) * scale)
|
||||
}
|
||||
|
||||
exports.qrToImageData = function qrToImageData (imgData, qr, opts) {
|
||||
const size = qr.modules.size
|
||||
const data = qr.modules.data
|
||||
const scale = exports.getScale(size, opts)
|
||||
const symbolSize = Math.floor((size + opts.margin * 2) * scale)
|
||||
const scaledMargin = opts.margin * scale
|
||||
const palette = [opts.color.light, opts.color.dark]
|
||||
|
||||
for (let i = 0; i < symbolSize; i++) {
|
||||
for (let j = 0; j < symbolSize; j++) {
|
||||
let posDst = (i * symbolSize + j) * 4
|
||||
let pxColor = opts.color.light
|
||||
|
||||
if (i >= scaledMargin && j >= scaledMargin &&
|
||||
i < symbolSize - scaledMargin && j < symbolSize - scaledMargin) {
|
||||
const iSrc = Math.floor((i - scaledMargin) / scale)
|
||||
const jSrc = Math.floor((j - scaledMargin) / scale)
|
||||
pxColor = palette[data[iSrc * size + jSrc] ? 1 : 0]
|
||||
}
|
||||
|
||||
imgData[posDst++] = pxColor.r
|
||||
imgData[posDst++] = pxColor.g
|
||||
imgData[posDst++] = pxColor.b
|
||||
imgData[posDst] = pxColor.a
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user