| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278 |
- /**
- * LocalStorage Validation and Cleanup Utility
- *
- * Validates and cleans up per-user UI state stored in localStorage
- * for non-logged-in users (swimlane heights, list widths, collapse states)
- */
- // Maximum age for localStorage data (90 days)
- const MAX_AGE_MS = 90 * 24 * 60 * 60 * 1000;
- // Maximum number of boards to keep per storage key
- const MAX_BOARDS_PER_KEY = 50;
- // Maximum number of items per board
- const MAX_ITEMS_PER_BOARD = 100;
- /**
- * Validate that a value is a valid positive number
- */
- function isValidNumber(value, min = 0, max = 10000) {
- if (typeof value !== 'number') return false;
- if (isNaN(value)) return false;
- if (!isFinite(value)) return false;
- if (value < min || value > max) return false;
- return true;
- }
- /**
- * Validate that a value is a valid boolean
- */
- function isValidBoolean(value) {
- return typeof value === 'boolean';
- }
- /**
- * Validate and clean swimlane heights data
- * Structure: { boardId: { swimlaneId: height, ... }, ... }
- */
- function validateSwimlaneHeights(data) {
- if (!data || typeof data !== 'object') return {};
-
- const cleaned = {};
- const boardIds = Object.keys(data).slice(0, MAX_BOARDS_PER_KEY);
-
- for (const boardId of boardIds) {
- if (typeof boardId !== 'string' || boardId.length === 0) continue;
-
- const boardData = data[boardId];
- if (!boardData || typeof boardData !== 'object') continue;
-
- const swimlaneIds = Object.keys(boardData).slice(0, MAX_ITEMS_PER_BOARD);
- const cleanedBoard = {};
-
- for (const swimlaneId of swimlaneIds) {
- if (typeof swimlaneId !== 'string' || swimlaneId.length === 0) continue;
-
- const height = boardData[swimlaneId];
- // Valid swimlane heights: -1 (auto) or 50-2000 pixels
- if (isValidNumber(height, -1, 2000)) {
- cleanedBoard[swimlaneId] = height;
- }
- }
-
- if (Object.keys(cleanedBoard).length > 0) {
- cleaned[boardId] = cleanedBoard;
- }
- }
-
- return cleaned;
- }
- /**
- * Validate and clean list widths data
- * Structure: { boardId: { listId: width, ... }, ... }
- */
- function validateListWidths(data) {
- if (!data || typeof data !== 'object') return {};
-
- const cleaned = {};
- const boardIds = Object.keys(data).slice(0, MAX_BOARDS_PER_KEY);
-
- for (const boardId of boardIds) {
- if (typeof boardId !== 'string' || boardId.length === 0) continue;
-
- const boardData = data[boardId];
- if (!boardData || typeof boardData !== 'object') continue;
-
- const listIds = Object.keys(boardData).slice(0, MAX_ITEMS_PER_BOARD);
- const cleanedBoard = {};
-
- for (const listId of listIds) {
- if (typeof listId !== 'string' || listId.length === 0) continue;
-
- const width = boardData[listId];
- // Valid list widths: 100-1000 pixels
- if (isValidNumber(width, 100, 1000)) {
- cleanedBoard[listId] = width;
- }
- }
-
- if (Object.keys(cleanedBoard).length > 0) {
- cleaned[boardId] = cleanedBoard;
- }
- }
-
- return cleaned;
- }
- /**
- * Validate and clean collapsed states data
- * Structure: { boardId: { itemId: boolean, ... }, ... }
- */
- function validateCollapsedStates(data) {
- if (!data || typeof data !== 'object') return {};
-
- const cleaned = {};
- const boardIds = Object.keys(data).slice(0, MAX_BOARDS_PER_KEY);
-
- for (const boardId of boardIds) {
- if (typeof boardId !== 'string' || boardId.length === 0) continue;
-
- const boardData = data[boardId];
- if (!boardData || typeof boardData !== 'object') continue;
-
- const itemIds = Object.keys(boardData).slice(0, MAX_ITEMS_PER_BOARD);
- const cleanedBoard = {};
-
- for (const itemId of itemIds) {
- if (typeof itemId !== 'string' || itemId.length === 0) continue;
-
- const collapsed = boardData[itemId];
- if (isValidBoolean(collapsed)) {
- cleanedBoard[itemId] = collapsed;
- }
- }
-
- if (Object.keys(cleanedBoard).length > 0) {
- cleaned[boardId] = cleanedBoard;
- }
- }
-
- return cleaned;
- }
- /**
- * Validate and clean a single localStorage key
- */
- function validateAndCleanKey(key, validator) {
- try {
- const stored = localStorage.getItem(key);
- if (!stored) return;
-
- const data = JSON.parse(stored);
- const cleaned = validator(data);
-
- // Only write back if data changed
- const cleanedStr = JSON.stringify(cleaned);
- if (cleanedStr !== stored) {
- if (Object.keys(cleaned).length > 0) {
- localStorage.setItem(key, cleanedStr);
- } else {
- localStorage.removeItem(key);
- }
- }
- } catch (e) {
- console.warn(`Error validating localStorage key ${key}:`, e);
- // Remove corrupted data
- try {
- localStorage.removeItem(key);
- } catch (removeError) {
- console.error(`Failed to remove corrupted localStorage key ${key}:`, removeError);
- }
- }
- }
- /**
- * Validate and clean all Wekan localStorage data
- * Called on app startup and periodically
- */
- export function validateAndCleanLocalStorage() {
- if (typeof localStorage === 'undefined') return;
-
- try {
- // Validate swimlane heights
- validateAndCleanKey('wekan-swimlane-heights', validateSwimlaneHeights);
-
- // Validate list widths
- validateAndCleanKey('wekan-list-widths', validateListWidths);
-
- // Validate list constraints
- validateAndCleanKey('wekan-list-constraints', validateListWidths);
-
- // Validate collapsed lists
- validateAndCleanKey('wekan-collapsed-lists', validateCollapsedStates);
-
- // Validate collapsed swimlanes
- validateAndCleanKey('wekan-collapsed-swimlanes', validateCollapsedStates);
-
- // Record last cleanup time
- localStorage.setItem('wekan-last-cleanup', Date.now().toString());
-
- } catch (e) {
- console.error('Error during localStorage validation:', e);
- }
- }
- /**
- * Check if cleanup is needed (once per day)
- */
- export function shouldRunCleanup() {
- if (typeof localStorage === 'undefined') return false;
-
- try {
- const lastCleanup = localStorage.getItem('wekan-last-cleanup');
- if (!lastCleanup) return true;
-
- const lastCleanupTime = parseInt(lastCleanup, 10);
- if (isNaN(lastCleanupTime)) return true;
-
- const timeSince = Date.now() - lastCleanupTime;
- // Run cleanup once per day
- return timeSince > 24 * 60 * 60 * 1000;
- } catch (e) {
- return true;
- }
- }
- /**
- * Get validated data from localStorage
- */
- export function getValidatedLocalStorageData(key, validator) {
- if (typeof localStorage === 'undefined') return {};
-
- try {
- const stored = localStorage.getItem(key);
- if (!stored) return {};
-
- const data = JSON.parse(stored);
- return validator(data);
- } catch (e) {
- console.warn(`Error reading localStorage key ${key}:`, e);
- return {};
- }
- }
- /**
- * Set validated data to localStorage
- */
- export function setValidatedLocalStorageData(key, data, validator) {
- if (typeof localStorage === 'undefined') return false;
-
- try {
- const validated = validator(data);
- localStorage.setItem(key, JSON.stringify(validated));
- return true;
- } catch (e) {
- console.error(`Error writing localStorage key ${key}:`, e);
- return false;
- }
- }
- // Export validators for use by other modules
- export const validators = {
- swimlaneHeights: validateSwimlaneHeights,
- listWidths: validateListWidths,
- collapsedStates: validateCollapsedStates,
- isValidNumber,
- isValidBoolean,
- };
- // Auto-cleanup on module load if needed
- if (Meteor.isClient) {
- Meteor.startup(() => {
- if (shouldRunCleanup()) {
- validateAndCleanLocalStorage();
- }
- });
- }
|