Sending somebody a label meant sending a screenshot and a list of settings to retype, and they would get one of them wrong. Copy share link hands them a URL that reopens the generator with the content, the label, the copy count and every layout value that was actually changed. The whole payload rides in the fragment. A fragment is never transmitted, so it does not reach Flask, it is not in the IIS log, and the proxy between them never sees it - which matters because the content is usually an asset number or a serial, and those have no business scattered through logs. It is NOT secrecy: the link sits in a chat message and in browser history, and anyone holding it can read the content. Secure Send exists for the case where that matters, and the note under the box says so. Only values that differ from a first-time visitor's defaults are carried, so the link stays short enough to paste into a chat box; a shared link then wins over the recipient's own saved settings, because somebody following one is asking to see THAT code rather than their own last one. An unknown key is ignored rather than refused, so a link made by a newer version still opens on an older one. The picture is never carried - a data URL runs to megabytes. Values are coerced back to the type the field holds. Everything arrives from a URL as a string, and a nudge of 0 plus '2' is '02'. Proven end to end: a link made in one browser, opened in a clean context with no saved settings, rendering the same code.
117 lines
4.0 KiB
JavaScript
117 lines
4.0 KiB
JavaScript
/**
|
|
* A link that opens the generator with somebody's work already in it.
|
|
*
|
|
* WHY THE FRAGMENT
|
|
*
|
|
* Everything rides after the `#`. A fragment is never transmitted: it does not
|
|
* reach Flask, it is not in the IIS log, and the proxy between the two never
|
|
* sees it. A query string would put the encoded content in all three, and the
|
|
* whole point of this feature is to send somebody a code they can print - which
|
|
* means the content is often an asset number, a serial, or a URL somebody would
|
|
* rather not scatter through logs.
|
|
*
|
|
* It is NOT secrecy. The link still sits in a chat message, in browser history
|
|
* and in whatever archives that chat. Anyone holding the link has the content.
|
|
* Secure Send exists for the case where that matters.
|
|
*
|
|
* WHAT TRAVELS
|
|
*
|
|
* What was typed (content, label, copies, the CSV) plus the settings that
|
|
* change how the label looks. Not the picture: a data URL runs to megabytes and
|
|
* would produce a link no chat client will carry.
|
|
*/
|
|
|
|
// Short keys because the whole thing has to survive being pasted into a chat
|
|
// box. `c` is the content, `t` the code type, `p` the label preset.
|
|
const SHORT = {
|
|
c: 'content',
|
|
l: 'label',
|
|
n: 'copies',
|
|
b: 'back',
|
|
v: 'csv',
|
|
s: 'source',
|
|
t: 'codetype',
|
|
e: 'errorcorrection',
|
|
p: 'preset',
|
|
}
|
|
|
|
// Layout values travel under their own names - there are enough of them that
|
|
// abbreviating would cost more in confusion than it saves in characters.
|
|
const PASSTHROUGH = [
|
|
'labelwidth', 'labelheight', 'padding', 'quiet', 'gap', 'codesize',
|
|
'barheight', 'labelfont', 'layout', 'align', 'valign',
|
|
'contentx', 'contenty', 'nudgex', 'nudgey', 'dpi',
|
|
'backenabled', 'backsource', 'backcodetype', 'backtext', 'backlayout',
|
|
'backorder',
|
|
]
|
|
|
|
const LONG = Object.fromEntries(Object.entries(SHORT).map(([k, v]) => [v, k]))
|
|
|
|
/**
|
|
* Build the fragment for a state object. Returns '' when there is nothing to
|
|
* share, so a caller can disable its button rather than emit a bare '#'.
|
|
*/
|
|
export function buildFragment (state, defaults = {}) {
|
|
const params = new URLSearchParams()
|
|
|
|
for (const [key, name] of Object.entries(SHORT)) {
|
|
const value = state[name]
|
|
if (value === undefined || value === null || value === '') continue
|
|
// Copies of 1 is what everybody has; it does not need saying.
|
|
if (name === 'copies' && Number(value) === 1) continue
|
|
params.set(key, String(value))
|
|
}
|
|
|
|
// Only the layout values that DIFFER from this browser's current settings.
|
|
// Sending all twenty would make a link four times longer for no gain, and the
|
|
// recipient's own saved settings are a reasonable starting point.
|
|
for (const name of PASSTHROUGH) {
|
|
const value = state[name]
|
|
if (value === undefined || value === null || value === '') continue
|
|
if (defaults[name] !== undefined && String(defaults[name]) === String(value)) continue
|
|
params.set(name, String(value))
|
|
}
|
|
|
|
const encoded = params.toString()
|
|
return encoded ? '#' + encoded : ''
|
|
}
|
|
|
|
/**
|
|
* Read a fragment back into a partial state object.
|
|
*
|
|
* Unknown keys are ignored rather than refused: a link made by a newer version
|
|
* must still open here, minus whatever this version does not understand.
|
|
*/
|
|
export function readFragment (hash) {
|
|
const raw = String(hash || '').replace(/^#/, '')
|
|
if (!raw) return {}
|
|
|
|
const params = new URLSearchParams(raw)
|
|
const state = {}
|
|
|
|
for (const [key, value] of params.entries()) {
|
|
const name = SHORT[key] || (PASSTHROUGH.includes(key) ? key : null)
|
|
if (!name) continue
|
|
state[name] = value
|
|
}
|
|
return state
|
|
}
|
|
|
|
/**
|
|
* Coerce a fragment value to the type the ref already holds.
|
|
*
|
|
* Everything arrives as a string. Assigning '203' to a numeric input renders,
|
|
* but the moment it is used in arithmetic it concatenates - so a nudge of 0
|
|
* plus '2' becomes '02'. Match the current value's type instead.
|
|
*/
|
|
export function coerce (value, current) {
|
|
if (typeof current === 'number') {
|
|
const number = Number(value)
|
|
return Number.isFinite(number) ? number : current
|
|
}
|
|
if (typeof current === 'boolean') {
|
|
return value === 'true' || value === '1'
|
|
}
|
|
return value
|
|
}
|