fix(security): apply the Image-security defaults instead of storing them (#1296)
Four controls in Settings → Image security were written, reloaded and rendered as toggles, and read by nothing: default_protection_level → events.protection_level default_image_quality → events.image_quality enable_canvas_rendering → events.use_canvas_rendering default_fragmentation_level → events.fragmentation_level Each maps onto a column migration 038 already created, and each is labelled "… by default". `enable_devtools_protection` was the only one of the five ever wired (#317), and its plumbing is the pattern this follows. Reported for enable_canvas_rendering by @leonlivevocalist-svg while instrumenting #1287 — the setting was globally true on their install and zero canvas elements were created. Checking the neighbours found three more of the same, so fixing one and leaving three would have been worse than leaving all four. CREATION-TIME ONLY, deliberately. Applying these to existing events would silently change live galleries on upgrade: an install with enable_canvas_rendering already on would flip every grid to canvas rendering, which is memory-expensive at scale and is the exact profile under investigation in #1287. New events inherit; existing rows are untouched. A missing or malformed value yields no key, so creation falls through to the column default exactly as before — including the ranges, where an out-of-range quality or fragmentation level is ignored rather than clamped into something the operator did not choose. `false` is carried through rather than dropped as falsy, or "off" would be unreachable. The spread sits after the explicit columns so a value supplied by the request still wins. 12 tests: the mapping, the false case, seven malformed inputs falling through, partial configuration, and that a settings failure cannot block event creation.
This commit is contained in:
@@ -30,7 +30,8 @@ const { clampIntOrUndefined } = require('../../utils/numericHelpers');
|
||||
const { getFrontendBaseUrl, getAbsoluteFrontendUrl } = require('../../utils/frontendUrl');
|
||||
const downloadZipService = require('../../services/downloadZipService');
|
||||
const { resolveEventFeedbackDefaults, applyFeedbackDefaults, KEYBIND_MODES } = require('../../services/feedbackDefaults');
|
||||
const { validateHeroImageAnchor, getEventFieldRequirements, readBooleanSetting, getDownloadProtectionDefaults, getBrandingDefaults, getCustomerNameFromPayload, getCustomerEmailFromPayload, getCustomerPhoneFromPayload, isPhoneFieldEnabled, mapEventForApi, hasCustomerContactColumns, deleteEventCascade, SLIDESHOW_TRANSITIONS, SLIDESHOW_COLORFILTERS } = require('./helpers');
|
||||
const { validateHeroImageAnchor, getEventFieldRequirements, readBooleanSetting, getDownloadProtectionDefaults,
|
||||
getImageSecurityDefaults, getBrandingDefaults, getCustomerNameFromPayload, getCustomerEmailFromPayload, getCustomerPhoneFromPayload, isPhoneFieldEnabled, mapEventForApi, hasCustomerContactColumns, deleteEventCascade, SLIDESHOW_TRANSITIONS, SLIDESHOW_COLORFILTERS } = require('./helpers');
|
||||
|
||||
/**
|
||||
* `events.slug` is UNIQUE, and both routes that mint one do a read-then-insert
|
||||
@@ -544,6 +545,10 @@ module.exports = (router) => {
|
||||
// the request explicitly overrides it (#317 — admin disabled it globally
|
||||
// but new events still got it ON because the column default is true).
|
||||
const protectionDefaults = await getDownloadProtectionDefaults();
|
||||
// #1296 — the other four Image-security settings, which were written,
|
||||
// rendered as controls, and read by nothing. Creation-time only; see
|
||||
// getImageSecurityDefaults for why existing events are left alone.
|
||||
const imageSecurityDefaults = await getImageSecurityDefaults();
|
||||
const effectiveEnableDevtoolsProtection =
|
||||
enableDevtoolsProtectionInput !== undefined
|
||||
? enableDevtoolsProtectionInput
|
||||
@@ -617,6 +622,18 @@ module.exports = (router) => {
|
||||
allow_downloads: formatBoolean(allow_downloads !== undefined ? allow_downloads : true),
|
||||
disable_right_click: formatBoolean(disable_right_click !== undefined ? disable_right_click : false),
|
||||
enable_devtools_protection: formatBoolean(effectiveEnableDevtoolsProtection),
|
||||
// Spread AFTER the explicit columns so a value the request supplied
|
||||
// still wins; each key is present only when the global setting held
|
||||
// a usable value, so anything unset falls through to the column
|
||||
// default exactly as before (#1296).
|
||||
...(imageSecurityDefaults.protection_level !== undefined
|
||||
? { protection_level: imageSecurityDefaults.protection_level } : {}),
|
||||
...(imageSecurityDefaults.image_quality !== undefined
|
||||
? { image_quality: imageSecurityDefaults.image_quality } : {}),
|
||||
...(imageSecurityDefaults.use_canvas_rendering !== undefined
|
||||
? { use_canvas_rendering: formatBoolean(imageSecurityDefaults.use_canvas_rendering) } : {}),
|
||||
...(imageSecurityDefaults.fragmentation_level !== undefined
|
||||
? { fragmentation_level: imageSecurityDefaults.fragmentation_level } : {}),
|
||||
watermark_downloads: formatBoolean(watermark_downloads !== undefined ? watermark_downloads : false),
|
||||
watermark_text,
|
||||
allow_presigned_download: formatBoolean(allow_presigned_download === true || allow_presigned_download === 'true'),
|
||||
|
||||
@@ -95,6 +95,85 @@ const getDownloadProtectionDefaults = async () => {
|
||||
return { enable_devtools_protection: await readBooleanSetting('enable_devtools_protection') };
|
||||
};
|
||||
|
||||
/**
|
||||
* The rest of Settings → Image security, as creation defaults (#1296).
|
||||
*
|
||||
* Four settings in that panel were written, reloaded and rendered as
|
||||
* controls, and read by nothing:
|
||||
*
|
||||
* default_protection_level → events.protection_level
|
||||
* default_image_quality → events.image_quality
|
||||
* enable_canvas_rendering → events.use_canvas_rendering
|
||||
* default_fragmentation_level → events.fragmentation_level
|
||||
*
|
||||
* Each maps onto a column migration 038 already created, and each is
|
||||
* labelled "… by default", so applying them at creation is what the panel
|
||||
* has always claimed to do. `enable_devtools_protection` above is the only
|
||||
* one of the five that was ever wired.
|
||||
*
|
||||
* Creation-time only, deliberately. Applying them to EXISTING events would
|
||||
* silently change live galleries on upgrade — an install with
|
||||
* enable_canvas_rendering already on would switch every grid to canvas
|
||||
* rendering, which is memory-expensive at scale and is the profile under
|
||||
* investigation in #1287. New events only; existing rows untouched.
|
||||
*
|
||||
* Any value that is missing or malformed comes back undefined so the caller
|
||||
* falls through to the column default, exactly as before this existed.
|
||||
*/
|
||||
const PROTECTION_LEVELS = ['basic', 'standard', 'enhanced', 'maximum'];
|
||||
|
||||
const getImageSecurityDefaults = async () => {
|
||||
const defaults = {};
|
||||
try {
|
||||
const rows = await db('app_settings')
|
||||
.whereIn('setting_key', [
|
||||
'default_protection_level',
|
||||
'default_image_quality',
|
||||
'enable_canvas_rendering',
|
||||
'default_fragmentation_level',
|
||||
])
|
||||
.select('setting_key', 'setting_value');
|
||||
|
||||
const read = (key) => {
|
||||
const row = rows.find((r) => r.setting_key === key);
|
||||
if (!row) return undefined;
|
||||
let value = row.setting_value;
|
||||
if (typeof value === 'string') {
|
||||
try { value = JSON.parse(value); } catch { /* keep raw */ }
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
const level = read('default_protection_level');
|
||||
if (typeof level === 'string' && PROTECTION_LEVELS.includes(level)) {
|
||||
defaults.protection_level = level;
|
||||
}
|
||||
|
||||
// The column is an integer percentage; anything outside 1..100 is a
|
||||
// misconfiguration and falls through rather than being clamped into
|
||||
// something the operator did not choose.
|
||||
const quality = parseInt(read('default_image_quality'), 10);
|
||||
if (Number.isInteger(quality) && quality >= 1 && quality <= 100) {
|
||||
defaults.image_quality = quality;
|
||||
}
|
||||
|
||||
const canvas = read('enable_canvas_rendering');
|
||||
if (typeof canvas === 'boolean') {
|
||||
defaults.use_canvas_rendering = canvas;
|
||||
}
|
||||
|
||||
const fragmentation = parseInt(read('default_fragmentation_level'), 10);
|
||||
if (Number.isInteger(fragmentation) && fragmentation >= 1 && fragmentation <= 10) {
|
||||
defaults.fragmentation_level = fragmentation;
|
||||
}
|
||||
} catch (error) {
|
||||
// A settings read must never block event creation; the column defaults
|
||||
// are a correct fallback.
|
||||
logger.error('Failed to read image-security defaults', { error: error.message });
|
||||
}
|
||||
return defaults;
|
||||
};
|
||||
|
||||
// Helper to get branding defaults for new events (Feature 7: Branding Inheritance).
|
||||
//
|
||||
// Note: `branding_logo_position` (header bar — left/center/right) is a
|
||||
@@ -528,6 +607,7 @@ module.exports = {
|
||||
getEventFieldRequirements,
|
||||
readBooleanSetting,
|
||||
getDownloadProtectionDefaults,
|
||||
getImageSecurityDefaults,
|
||||
getBrandingDefaults,
|
||||
getCustomerNameFromPayload,
|
||||
getCustomerEmailFromPayload,
|
||||
|
||||
Reference in New Issue
Block a user