- Delete old Vite+Svelte frontend - Initialize new SvelteKit project with TypeScript - Configure Tailwind CSS v4 + DaisyUI - Implement JWT authentication with auto-refresh - Create login page with form validation (Zod) - Add protected route guards - Update Docker configuration for single-stage build - Add E2E tests with Playwright (6/11 passing) - Fix Svelte 5 reactivity with $state() runes Known issues: - 5 E2E tests failing (timing/async issues) - Token refresh implementation needs debugging - Validation error display timing
209 lines
7.9 KiB
JavaScript
209 lines
7.9 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
const NEW_LINES_REGEXP = /[\n\r]/gm;
|
|
const parseInts = (a) => a.map((v) => parseInt(v, 10));
|
|
/**
|
|
* HTML input element value sanitizer.
|
|
*/
|
|
class HTMLInputElementValueSanitizer {
|
|
/**
|
|
* Sanitizes a value.
|
|
*
|
|
* @param input Input.
|
|
* @param value Value.
|
|
*/
|
|
static sanitize(input, value) {
|
|
switch (input.type) {
|
|
case 'password':
|
|
case 'search':
|
|
case 'tel':
|
|
case 'text':
|
|
return value.replace(NEW_LINES_REGEXP, '');
|
|
case 'color':
|
|
// https://html.spec.whatwg.org/multipage/forms.html#color-state-(type=color):value-sanitization-algorithm
|
|
return /^#[a-fA-F\d]{6}$/.test(value) ? value.toLowerCase() : '#000000';
|
|
case 'email':
|
|
// https://html.spec.whatwg.org/multipage/forms.html#e-mail-state-(type=email):value-sanitization-algorithm
|
|
// https://html.spec.whatwg.org/multipage/forms.html#e-mail-state-(type=email):value-sanitization-algorithm-2
|
|
if (input.multiple) {
|
|
return value
|
|
.split(',')
|
|
.map((token) => token.trim())
|
|
.join(',');
|
|
}
|
|
return value.trim().replace(NEW_LINES_REGEXP, '');
|
|
case 'number':
|
|
// https://html.spec.whatwg.org/multipage/input.html#number-state-(type=number):value-sanitization-algorithm
|
|
return !isNaN(Number.parseFloat(value)) ? value : '';
|
|
case 'range': {
|
|
// https://html.spec.whatwg.org/multipage/input.html#range-state-(type=range):value-sanitization-algorithm
|
|
const number = Number.parseFloat(value);
|
|
const min = parseFloat(input.min) || 0;
|
|
const max = parseFloat(input.max) || 100;
|
|
if (isNaN(number)) {
|
|
return max < min ? String(min) : String((min + max) / 2);
|
|
}
|
|
else if (number < min) {
|
|
return String(min);
|
|
}
|
|
else if (number > max) {
|
|
return String(max);
|
|
}
|
|
return value;
|
|
}
|
|
case 'url':
|
|
// https://html.spec.whatwg.org/multipage/forms.html#url-state-(type=url):value-sanitization-algorithm
|
|
return value.trim().replace(NEW_LINES_REGEXP, '');
|
|
case 'date':
|
|
// https://html.spec.whatwg.org/multipage/input.html#date-state-(type=date):value-sanitization-algorithm
|
|
return this.sanitizeDate(value);
|
|
case 'datetime-local': {
|
|
// https://html.spec.whatwg.org/multipage/input.html#local-date-and-time-state-(type=datetime-local):value-sanitization-algorithm
|
|
const match = value.match(/^(\d\d\d\d)-(\d\d)-(\d\d)[T ](\d\d):(\d\d)(?::(\d\d)(?:\.(\d{1,3}))?)?$/);
|
|
if (!match) {
|
|
return '';
|
|
}
|
|
const dateString = this.sanitizeDate(value.slice(0, 10));
|
|
let timeString = this.sanitizeTime(value.slice(11));
|
|
if (!(dateString && timeString)) {
|
|
return '';
|
|
}
|
|
// Has seconds so needs to remove trailing zeros
|
|
if (match[6] !== undefined) {
|
|
if (timeString.indexOf('.') !== -1) {
|
|
// Remove unecessary zeros milliseconds
|
|
timeString = timeString.replace(/(?:\.0*|(\.\d+?)0+)$/, '$1');
|
|
}
|
|
timeString = timeString.replace(/(\d\d:\d\d)(:00)$/, '$1');
|
|
}
|
|
return dateString + 'T' + timeString;
|
|
}
|
|
case 'month':
|
|
// https://html.spec.whatwg.org/multipage/input.html#month-state-(type=month):value-sanitization-algorithm
|
|
if (!(value.match(/^(\d\d\d\d)-(\d\d)$/) && this.parseMonthComponent(value))) {
|
|
return '';
|
|
}
|
|
return this.checkBoundaries(value, input.min, input.max) ? value : '';
|
|
case 'time': {
|
|
// https://html.spec.whatwg.org/multipage/input.html#time-state-(type=time):value-sanitization-algorithm
|
|
value = this.sanitizeTime(value);
|
|
return value && this.checkBoundaries(value, input.min, input.max) ? value : '';
|
|
}
|
|
case 'week': {
|
|
// https://html.spec.whatwg.org/multipage/input.html#week-state-(type=week):value-sanitization-algorithm
|
|
const match = value.match(/^(\d\d\d\d)-W(\d\d)$/);
|
|
if (!match) {
|
|
return '';
|
|
}
|
|
const [intY, intW] = parseInts(match.slice(1, 3));
|
|
if (intY <= 0 || intW < 1 || intW > 53) {
|
|
return '';
|
|
}
|
|
// Check date is valid
|
|
const lastWeek = this.lastIsoWeekOfYear(intY);
|
|
if (intW < 1 || intW > 52 + lastWeek) {
|
|
return '';
|
|
}
|
|
if (!this.checkBoundaries(value, input.min, input.max)) {
|
|
return '';
|
|
}
|
|
return value;
|
|
}
|
|
}
|
|
return value;
|
|
}
|
|
/**
|
|
* Checks if a value is within the boundaries of min and max.
|
|
*
|
|
* @param value
|
|
* @param min
|
|
* @param max
|
|
*/
|
|
static checkBoundaries(value, min, max) {
|
|
if (min && min > value) {
|
|
return false;
|
|
}
|
|
else if (max && max < value) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
/**
|
|
* Parses the month component of a date string.
|
|
*
|
|
* @param value
|
|
*/
|
|
static parseMonthComponent(value) {
|
|
const [Y, M] = value.split('-');
|
|
const [intY, intM] = parseInts([Y, M]);
|
|
if (isNaN(intY) || isNaN(intM) || intY <= 0 || intM < 1 || intM > 12) {
|
|
return '';
|
|
}
|
|
return value;
|
|
}
|
|
/**
|
|
* Returns the last ISO week of a year.
|
|
*
|
|
* @param year
|
|
*/
|
|
static lastIsoWeekOfYear = (year) => {
|
|
const date = new Date(+year, 11, 31);
|
|
const day = (date.getDay() + 6) % 7;
|
|
date.setDate(date.getDate() - day + 3);
|
|
const firstThursday = date.getTime();
|
|
date.setMonth(0, 1);
|
|
if (date.getDay() !== 4) {
|
|
date.setMonth(0, 1 + ((4 - date.getDay() + 7) % 7));
|
|
}
|
|
return 1 + Math.ceil((firstThursday - date.getTime()) / 604800000);
|
|
};
|
|
/**
|
|
* Sanitizes a date string.
|
|
*
|
|
* @param value
|
|
*/
|
|
static sanitizeDate(value) {
|
|
const match = value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
if (!match) {
|
|
return '';
|
|
}
|
|
const month = this.parseMonthComponent(value.slice(0, 7));
|
|
if (!month) {
|
|
return '';
|
|
}
|
|
const [intY, intM, intD] = parseInts(match.slice(1, 4));
|
|
if (intD < 1 || intD > 31) {
|
|
return '';
|
|
}
|
|
// Check date is valid
|
|
const lastDayOfMonth = new Date(intY, intM, 0).getDate();
|
|
if (intD > lastDayOfMonth) {
|
|
return '';
|
|
}
|
|
return value;
|
|
}
|
|
/**
|
|
* Sanitizes a time string.
|
|
*
|
|
* @param value
|
|
*/
|
|
static sanitizeTime(value) {
|
|
const match = value.match(/^(\d{2}):(\d{2})(?::(\d{2}(?:\.(\d{1,3}))?))?$/);
|
|
if (!match) {
|
|
return '';
|
|
}
|
|
const [intH, intM] = parseInts(match.slice(1, 3));
|
|
const ms = parseFloat(match[3] || '0') * 1000;
|
|
if (intH > 23 || intM > 59 || ms > 59999) {
|
|
return '';
|
|
}
|
|
if (ms === 0) {
|
|
return `${match[1]}:${match[2]}`;
|
|
}
|
|
else {
|
|
return `${match[1]}:${match[2]}${ms >= 10000 ? `:${ms / 1000}` : `:0${ms / 1000}`}`;
|
|
}
|
|
}
|
|
}
|
|
exports.default = HTMLInputElementValueSanitizer;
|
|
//# sourceMappingURL=HTMLInputElementValueSanitizer.cjs.map
|