/** * @file ext-storage.js * * This extension allows automatic saving of the SVG canvas contents upon * page unload (which can later be automatically retrieved upon future * editor loads). * * The functionality was originally part of the SVG Editor, but moved to a * separate extension to make the setting behavior optional, and adapted * to inform the user of its setting of local data. * * @license MIT * * @copyright 2010 Brett Zamir * @todo Revisit on whether to use `svgEditor.pref` over directly setting * `curConfig` in all extensions for a more public API (not only for `extPath` * and `imagePath`, but other currently used config in the extensions) * @todo We might provide control of storage settings through the UI besides the * initial (or URL-forced) dialog. * */ export default { name: 'storage', init ({$}) { const svgEditor = this; const svgCanvas = svgEditor.canvas; // We could empty any already-set data for users when they decline storage, // but it would be a risk for users who wanted to store but accidentally // said "no"; instead, we'll let those who already set it, delete it themselves; // to change, set the "emptyStorageOnDecline" config setting to true // in svgedit-config-iife.js/svgedit-config-es.js. const { emptyStorageOnDecline, // When the code in svg-editor.js prevents local storage on load per // user request, we also prevent storing on unload here so as to // avoid third-party sites making XSRF requests or providing links // which would cause the user's local storage not to load and then // upon page unload (such as the user closing the window), the storage // would thereby be set with an empty value, erasing any of the // user's prior work. To change this behavior so that no use of storage // or adding of new storage takes place regardless of settings, set // the "noStorageOnLoad" config setting to true in svgedit-config-*.js. noStorageOnLoad, forceStorage } = svgEditor.curConfig; const {storage, updateCanvas} = svgEditor; /** * Replace `storagePrompt` parameter within URL. * @param {string} val * @returns {void} * @todo Replace the string manipulation with `searchParams.set` */ function replaceStoragePrompt (val) { val = val ? 'storagePrompt=' + val : ''; const loc = top.location; // Allow this to work with the embedded editor as well if (loc.href.includes('storagePrompt=')) { /* loc.href = loc.href.replace(/(?[&?])storagePrompt=[^&]*(?&?)/, function (n0, sep, amp) { return (val ? sep : '') + val + (!val && amp ? sep : (amp || '')); }); */ loc.href = loc.href.replace(/([&?])storagePrompt=[^&]*(&?)/, function (n0, n1, amp) { return (val ? n1 : '') + val + (!val && amp ? n1 : (amp || '')); }); } else { loc.href += (loc.href.includes('?') ? '&' : '?') + val; } } /** * Sets SVG content as a string with "svgedit-" and the current * canvas name as namespace. * @param {string} val * @returns {void} */ function setSVGContentStorage (val) { if (storage) { const name = 'svgedit-' + svgEditor.curConfig.canvasName; if (!val) { storage.removeItem(name); } else { storage.setItem(name, val); } } } /** * Set the cookie to expire. * @param {string} cookie * @returns {void} */ function expireCookie (cookie) { document.cookie = encodeURIComponent(cookie) + '=; expires=Thu, 01 Jan 1970 00:00:00 GMT'; } /** * Expire the storage cookie. * @returns {void} */ function removeStoragePrefCookie () { expireCookie('svgeditstore'); } /** * Empties storage for each of the current preferences. * @returns {void} */ function emptyStorage () { setSVGContentStorage(''); Object.keys(svgEditor.curPrefs).forEach((name) => { name = 'svg-edit-' + name; if (storage) { storage.removeItem(name); } expireCookie(name); }); } // emptyStorage(); /** * Listen for unloading: If and only if opted in by the user, set the content * document and preferences into storage: * 1. Prevent save warnings (since we're automatically saving unsaved * content into storage) * 2. Use localStorage to set SVG contents (potentially too large to allow in cookies) * 3. Use localStorage (where available) or cookies to set preferences. * @returns {void} */ function setupBeforeUnloadListener () { window.addEventListener('beforeunload', function (e) { // Don't save anything unless the user opted in to storage if (!document.cookie.match(/(?:^|;\s*)svgeditstore=(?:prefsAndContent|prefsOnly)/)) { return; } if (document.cookie.match(/(?:^|;\s*)svgeditstore=prefsAndContent/)) { setSVGContentStorage(svgCanvas.getSvgString()); } svgEditor.setConfig({no_save_warning: true}); // No need for explicit saving at all once storage is on // svgEditor.showSaveWarning = false; const {curPrefs} = svgEditor; Object.entries(curPrefs).forEach(([key, val]) => { const store = (val !== undefined); key = 'svg-edit-' + key; if (!store) { return; } if (storage) { storage.setItem(key, val); } else if (window.widget) { window.widget.setPreferenceForKey(val, key); } else { val = encodeURIComponent(val); document.cookie = encodeURIComponent(key) + '=' + val + '; expires=Fri, 31 Dec 9999 23:59:59 GMT'; } }); }); } let loaded = false; return { name: 'storage', async langReady ({importLocale}) { const storagePrompt = new URL(top.location).searchParams.get('storagePrompt'); const confirmSetStorage = await importLocale(); const { message, storagePrefsAndContent, storagePrefsOnly, storagePrefs, storageNoPrefsOrContent, storageNoPrefs, rememberLabel, rememberTooltip } = confirmSetStorage; // No need to run this one-time dialog again just because the user // changes the language if (loaded) { return; } loaded = true; // Note that the following can load even if "noStorageOnLoad" is // set to false; to avoid any chance of storage, avoid this // extension! (and to avoid using any prior storage, set the // config option "noStorageOnLoad" to true). if (!forceStorage && ( // If the URL has been explicitly set to always prompt the // user (e.g., so one can be pointed to a URL where one // can alter one's settings, say to prevent future storage)... storagePrompt === 'true' || ( // ...or...if the URL at least doesn't explicitly prevent a // storage prompt (as we use for users who // don't want to set cookies at all but who don't want // continual prompts about it)... storagePrompt !== 'false' && // ...and this user hasn't previously indicated a desire for storage !document.cookie.match(/(?:^|;\s*)svgeditstore=(?:prefsAndContent|prefsOnly)/) ) // ...then show the storage prompt. )) { const options = []; if (storage) { options.unshift( {value: 'prefsAndContent', text: storagePrefsAndContent}, {value: 'prefsOnly', text: storagePrefsOnly}, {value: 'noPrefsOrContent', text: storageNoPrefsOrContent} ); } else { options.unshift( {value: 'prefsOnly', text: storagePrefs}, {value: 'noPrefsOrContent', text: storageNoPrefs} ); } // Hack to temporarily provide a wide and high enough dialog const oldContainerWidth = $('#dialog_container')[0].style.width, oldContainerMarginLeft = $('#dialog_container')[0].style.marginLeft, oldContentHeight = $('#dialog_content')[0].style.height, oldContainerHeight = $('#dialog_container')[0].style.height; $('#dialog_content')[0].style.height = '120px'; $('#dialog_container')[0].style.height = '170px'; $('#dialog_container')[0].style.width = '800px'; $('#dialog_container')[0].style.marginLeft = '-400px'; // Open select-with-checkbox dialog // From svg-editor.js svgEditor.storagePromptState = 'waiting'; const {response: pref, checked} = await $.select( message, options, null, null, { label: rememberLabel, checked: true, tooltip: rememberTooltip } ); if (pref && pref !== 'noPrefsOrContent') { // Regardless of whether the user opted // to remember the choice (and move to a URL which won't // ask them again), we have to assume the user // doesn't even want to remember their not wanting // storage, so we don't set the cookie or continue on with // setting storage on beforeunload document.cookie = 'svgeditstore=' + encodeURIComponent(pref) + '; expires=Fri, 31 Dec 9999 23:59:59 GMT'; // 'prefsAndContent' | 'prefsOnly' // If the URL was configured to always insist on a prompt, if // the user does indicate a wish to store their info, we // don't want ask them again upon page refresh so move // them instead to a URL which does not always prompt if (storagePrompt === 'true' && checked) { replaceStoragePrompt(); return; } } else { // The user does not wish storage (or cancelled, which we treat equivalently) removeStoragePrefCookie(); if (pref && // If the user explicitly expresses wish for no storage emptyStorageOnDecline ) { emptyStorage(); } if (pref && checked) { // Open a URL which won't set storage and won't prompt user about storage replaceStoragePrompt('false'); return; } } // Reset width/height of dialog (e.g., for use by Export) $('#dialog_container')[0].style.width = oldContainerWidth; $('#dialog_container')[0].style.marginLeft = oldContainerMarginLeft; $('#dialog_content')[0].style.height = oldContentHeight; $('#dialog_container')[0].style.height = oldContainerHeight; // It should be enough to (conditionally) add to storage on // beforeunload, but if we wished to update immediately, // we might wish to try setting: // svgEditor.setConfig({noStorageOnLoad: true}); // and then call: // svgEditor.loadContentAndPrefs(); // We don't check for noStorageOnLoad here because // the prompt gives the user the option to store data setupBeforeUnloadListener(); svgEditor.storagePromptState = 'closed'; updateCanvas(true); } else if (!noStorageOnLoad || forceStorage) { setupBeforeUnloadListener(); } } }; } };