////////////////////////////////////////////////// // Silex, live web creation // http://projects.silexlabs.org/?/silex/ // // Copyright (c) 2012 Silex Labs // http://www.silexlabs.org/ // // Silex is available under the GPL license // http://www.silexlabs.org/silex/silex-licensing/ ////////////////////////////////////////////////// import CloudExplorer from 'cloud-explorer'; import * as express from 'express'; import * as fs from 'fs'; import { JSDOM } from 'jsdom'; import * as nodeModules from 'node_modules-path'; import * as Path from 'path'; import { URL } from 'url'; import { Constants } from '../../Constants'; import BackwardCompat from '../utils/BackwardCompat'; import DomTools from '../utils/DomTools'; export default function({ port, rootUrl }, unifile) { const backwardCompat = new BackwardCompat(rootUrl); function isInTemplateFolder(path) { if (path.startsWith(Path.join(nodeModules('silex-templates'), 'silex-templates')) || path.startsWith(Path.join(nodeModules('silex-blank-templates'), 'silex-blank-templates'))) { return true; } return false; } /** * list all the templates of the given folder */ function getTemplatesList(req, res, next) { const templateFolder = Path.join(nodeModules(req.params.folder), req.params.folder); if (!isInTemplateFolder(templateFolder)) { console.error('Error while trying to get the json representation of the folder ', templateFolder); res.send({success: false, error: 'Error while trying to get the json representation of the folder ' + req.params.folder + ' - folder does not exist'}); return; } fs.readdir(templateFolder, (err, result) => { if (err) { console.error('Error while trying to get the json representation of the folder ', templateFolder, err); res.send({success: false, error: 'Error while trying to get the json representation of the folder ' + req.params.folder + ' - ' + err}); } else { const templateList = result.filter((entry) => { return fs.statSync(Path.join(templateFolder, entry)).isDirectory(); }); res.send(templateList); } }); } /** * load a website from the cloud storage of the user */ function readWebsite(req, res, next) { const connector = req.params[0]; const path = req.params[1]; const url = new URL(`${ rootUrl }/ce/${ connector }/get/${ Path.dirname(path) }/`); unifile.readFile(req.session.unifile, connector, path) .then((buffer) => { return sendWebsiteData(res, buffer, url); }) .catch((err) => { console.error('unifile error catched:', err); CloudExplorer.handleError(res, err); }); } /** * load a website from a template folder on local disk */ function readTemplate(req, res, next) { const path = req.params[0]; const localPath = Path.resolve(nodeModules('silex-templates'), path); const url = new URL(`${ rootUrl }/libs/templates/${ Path.dirname(path) }/`); if (isInTemplateFolder(localPath)) { fs.readFile(localPath, (err, buffer) => { if (err) { CloudExplorer.handleError(res, err); } else { sendWebsiteData(res, buffer, url); } }); } else { CloudExplorer.handleError(res, {message: 'Not authorized.', code: 'EACCES'}); } } function sendWebsiteData(res, buffer, url): Promise { // remove user head tag to avoid bad markup messing with the website const { html, userHead } = DomTools.extractUserHeadTag(buffer.toString('utf-8')); // from now on use a parsed DOM const dom = new JSDOM(html, { url: url.href }); return backwardCompat.update(dom.window.document) .then((wanrningMsg) => { prepareWebsite(dom, url); // done, back to a string const str = dom.serialize(); dom.window.close(); res.send({ message: wanrningMsg, html: str, userHead, }); }) .catch((err) => { console.error('Could not send website data: ', err); res.status(400).send({ message: err.message, }); }); } /** * save a website to the cloud storage of the user */ function writeWebsite(req, res, next) { const connector = req.params[0]; const path = req.params[1]; const { userHead, html } = JSON.parse(req.body); const url = new URL(`${ rootUrl }/ce/${ connector }/get/${ Path.dirname(path) }/`); const dom = new JSDOM(html, { url: url.href }); unprepareWebsite(dom, url); const str = dom.serialize(); const fullHtml = DomTools.insertUserHeadTag(str, userHead); dom.window.close(); unifile.writeFile(req.session.unifile, connector, req.params[1], fullHtml) .then((result) => { res.send(result); }) .catch((err) => { console.error('unifile error catched:', err); CloudExplorer.handleError(res, err); }); } /** * prepare website for edit mode * make all URLs absolute (so that images are still found when I "save as" my website to another folder) */ function prepareWebsite(dom, baseUrl) { // URLs DomTools.transformPaths(dom, (path, el) => { const url = new URL(path, baseUrl); return url.href; }); // update context classes dom.window.document.body.classList.remove(Constants.WEBSITE_CONTEXT_RUNTIME_CLASS_NAME); dom.window.document.body.classList.add(Constants.WEBSITE_CONTEXT_EDITOR_CLASS_NAME); deactivateScripts(dom); // add /css/editable.css const tag = dom.window.document.createElement('link'); tag.rel = 'stylesheet'; tag.href = rootUrl + '/css/editable.css'; tag.classList.add(Constants.SILEX_TEMP_TAGS_CSS_CLASS); dom.window.document.head.appendChild(tag); } /** * prepare website for being saved * * make all URLs relative to current path * * remove useless markup and css classes */ function unprepareWebsite(dom, baseUrl) { // markup dom.window.document.body.classList.add(Constants.WEBSITE_CONTEXT_RUNTIME_CLASS_NAME); dom.window.document.body.classList.remove(Constants.WEBSITE_CONTEXT_EDITOR_CLASS_NAME); reactivateScripts(dom); restoreIFrames(dom); cleanupNoscripts(dom); // URLs DomTools.transformPaths(dom, (path, el) => { const url = new URL(path, baseUrl); if (url.href.startsWith(rootUrl)) { // make it relative return Path.relative(baseUrl.pathname, url.pathname); } return path; }); // remove temp tags Array.from(dom.window.document.querySelectorAll(` .${Constants.SILEX_TEMP_TAGS_CSS_CLASS}, #${Constants.SILEX_CURRENT_PAGE_ID}, .${ Constants.RISZE_HANDLE_CSS_CLASS }`)) .forEach((el: HTMLElement) => { el.remove(); }); // remove useless css classes Constants.SILEX_TEMP_CLASS_NAMES.forEach((className) => { Array.from(dom.window.document.getElementsByClassName(className)) .forEach((el: HTMLElement) => el.classList.remove(className)); }); // cleanup inline styles dom.window.document.body.style.minWidth = ''; // not needed? dom.window.document.body.style.minHeight = ''; // not needed? dom.window.document.body.style.overflow = ''; // set by stage } function deactivateScripts(dom) { Array.from(dom.window.document.getElementsByTagName('script')) .forEach((el: HTMLElement) => { // do not execute scripts, unless they are silex's static scripts // and leave it alone if it has a type different from 'text/javascript' if (!el.hasAttribute('data-silex-static') && (!el.hasAttribute('type') || el.getAttribute('type') === 'text/javascript')) { el.setAttribute('type', 'text/notjavascript'); } }); } function restoreIFrames(dom) { Array.from(dom.window.document.querySelectorAll('[data-silex-iframe-src]')) .forEach((el: HTMLElement) => { el.setAttribute('src', el.getAttribute('data-silex-iframe-src')); el.removeAttribute('data-silex-iframe-src'); }); } function cleanupNoscripts(dom) { Array.from(dom.window.document.querySelectorAll('noscript')) .forEach((el: HTMLElement) => { el.innerHTML = decodeHTMLEntities(el.innerHTML); }); } function decodeHTMLEntities(text) { const entities = [['amp', '&'], ['apos', '\''], ['#x27', '\''], ['#x2F', '/'], ['#39', '\''], ['#47', '/'], ['lt', '<'], ['gt', '>'], ['nbsp', ' '], ['quot', '"']]; entities.forEach((entity) => text = text.replace(new RegExp('&' + entity[0] + ';', 'g'), entity[1])); return text; } function reactivateScripts(dom) { Array.from(dom.window.document.querySelectorAll('script[type="text/notjavascript"]')) .forEach((el: HTMLElement) => el.setAttribute('type', 'text/javascript')); } const router = express.Router(); // website specials router.get(/\/website\/ce\/(.*)\/get\/(.*)/, readWebsite); router.get(/\/website\/libs\/templates\/(.*)/, readTemplate); router.put(/\/website\/ce\/(.*)\/put\/(.*)/, writeWebsite); // ** // list templates router.use('/get/:folder', getTemplatesList); return router; }