From befcc65225dfd33b53b0f3ece8b72d6df6e6ec1a Mon Sep 17 00:00:00 2001 From: trendschau Date: Wed, 23 Aug 2023 13:31:45 +0200 Subject: [PATCH] Finished svg-sanitizer and error messages in blox editor --- composer.json | 7 +- content/00-welcome/05-todos.txt | 2 +- data/navigation/navi-live.txt | 1 + media/tmp/1528492471.svg | 57 +++++++++ .../Controllers/ControllerApiFile.php | 61 ++++----- .../Controllers/ControllerApiImage.php | 2 +- .../Controllers/ControllerWebAuthor.php | 4 + system/typemill/Models/Media.php | 63 ++++++++-- system/typemill/Models/SvgSanitizer.php | 116 ++++++++++++++++++ .../typemill/author/js/vue-blox-components.js | 93 +++++++------- 10 files changed, 322 insertions(+), 84 deletions(-) create mode 100644 data/navigation/navi-live.txt create mode 100644 media/tmp/1528492471.svg create mode 100644 system/typemill/Models/SvgSanitizer.php diff --git a/composer.json b/composer.json index 44db4f4..a4d74d5 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,10 @@ "homepage": "https://typemill.net", "license": "MIT", "config": { - "vendor-dir": "system/vendor" + "vendor-dir": "system/vendor", + "allow-plugins": { + "composer/installers": true + } }, "require": { "php": "^8.0", @@ -26,7 +29,7 @@ "laminas/laminas-permissions-acl": "^2.10", "akrabat/proxy-detection-middleware": "^1.0.0", "gregwar/captcha": "1.*" - }, + }, "autoload": { "psr-4": { "Typemill\\": "system/typemill/", diff --git a/content/00-welcome/05-todos.txt b/content/00-welcome/05-todos.txt index 971f52d..8ae8731 100644 --- a/content/00-welcome/05-todos.txt +++ b/content/00-welcome/05-todos.txt @@ -1 +1 @@ -["# ToDos Version 2","[TOC]","## System settings","* DONE: Migrate from backend to frontend with vue and api\n* DONE: Redesign\n* DONE: License feature\n* DONE: Enhance with plugins","----","## Visual Editor","* DONE: Refactor and redesign\n* DONE: Fix toc component in new block\n* DONE: Fix hr component in new block\n* DONE: finish shortcode component\n* DONE: Fix inline formats\n* DONE: fix lenght of page\n* DONE: Fix design of new block at the end (background color)\n* DONE: Move Block\n* DONE: Fix headline design\n* DONE: Fix save on two enter\n* DONE: fix quote design\n* DONE: Fix toc preview\n* DONE: disable enable \n* DONE: Add load sign (from navigation)\n* DONE: File is not published from tmp to media\/files if you save the block.\n* ToDo: Customfields not styled yet.\n* ToDo: Warn if open another block\n* ToDo: finish youtube component","## Raw Editor","* DONE: Refactor and redesign\n* DONE: Integrate highlighting","## Navigation","* DONE: Refactor and redesign\n* DONE: fix status in navigation\n* DONE: refresh navigation after changes\n* ToDo: fix error messages\n* ToDo: Wrong frontend navigation if unpublished pages","## Publish Controller","* DONE: Refactor and redesign\n* DONE: Create \n* DONE: publish\n* DONE: unpublish\n* DONE: discard\n* DONE: delete\n* DONE: save draft\n* DONE: switch to raw","## Meta Tabs","* DONE: Refactor and redesign\n* DONE: Enhance with plugins","## Medialib","* DONE: Refactor and redesign","## Posts","* DONE: Refactor and redesign","## Plugins","* Asset Class in progress","## Frontend","* DONE: Refactor\n* DONE: Test restrictions","## Other big tasks","* DONE: System setup\n* DONE: Recover Password","## Medium tasks","* DONE: Merge processAssets modell\n* DONE: Table of content duplicated for published pages\n* DONE: Session handling: csrf fail and session start error if restrictions are active\n* DONE: Image and files for meta","## Open tasks","* DONE: Sitemap and ping\n* DONE: Version check\n* SVG checker: https:\/\/github.com\/TribalSystems\/SVG-Sanitizer\n* Markdown secure rendering\n* Responsive design\n* Backend form builder\n* Proxy support\n* Image generation on the fly\n* Captcha integration\n* Fix error api systemnavi\n* Reference feature\n* Typemill Utilities\n* Clear cache\n* Show security Log\n* User search only for +10 users","## Cleanups:","* DONE: Events\n* Error messages\n* Translations","## Info: Select userroles","* Userroles for file restriction: in vue-blox-components loaded via api\n* Userroles for userfields: in php model user getUserFields()\n* Userroles for meta: in php controller apiAuthorMeta getMeta()\n* Plugins and themes: in php model extension getThemeDefinitions()","## Info: License Check","* On activation in apiControllerExtension. It checks the license in yaml.\n* In plugin php code with setPremiumLicense\n* In static plugins, it checks manual premium list and method setPremiumLicense and more "] \ No newline at end of file +["# ToDos Version 2","[TOC]","## System settings","* DONE: Migrate from backend to frontend with vue and api\n* DONE: Redesign\n* DONE: License feature\n* DONE: Enhance with plugins","----","## Visual Editor","* DONE: Refactor and redesign\n* DONE: Fix toc component in new block\n* DONE: Fix hr component in new block\n* DONE: finish shortcode component\n* DONE: Fix inline formats\n* DONE: fix lenght of page\n* DONE: Fix design of new block at the end (background color)\n* DONE: Move Block\n* DONE: Fix headline design\n* DONE: Fix save on two enter\n* DONE: fix quote design\n* DONE: Fix toc preview\n* DONE: disable enable \n* DONE: Add load sign (from navigation)\n* DONE: File is not published from tmp to media\/files if you save the block.\n* ToDo: Customfields not styled yet.\n* ToDo: Warn if open another block\n* ToDo: finish youtube component","## Raw Editor","* DONE: Refactor and redesign\n* DONE: Integrate highlighting","## Navigation","* DONE: Refactor and redesign\n* DONE: fix status in navigation\n* DONE: refresh navigation after changes\n* ToDo: fix error messages\n* ToDo: Wrong frontend navigation if unpublished pages","## Publish Controller","* DONE: Refactor and redesign\n* DONE: Create \n* DONE: publish\n* DONE: unpublish\n* DONE: discard\n* DONE: delete\n* DONE: save draft\n* DONE: switch to raw","## Meta Tabs","* DONE: Refactor and redesign\n* DONE: Enhance with plugins","## Medialib","* DONE: Refactor and redesign","## Posts","* DONE: Refactor and redesign","## Plugins","* Asset Class in progress","## Frontend","* DONE: Refactor\n* DONE: Test restrictions","## Other big tasks","* DONE: System setup\n* DONE: Recover Password","## Medium tasks","* DONE: Merge processAssets modell\n* DONE: Table of content duplicated for published pages\n* DONE: Session handling: csrf fail and session start error if restrictions are active\n* DONE: Image and files for meta","## Open tasks","* DONE: Sitemap and ping\n* DONE: Version check\n* DONE: Proxy support\n* DONE: SVG checker: https:\/\/github.com\/TribalSystems\/SVG-Sanitizer\n* Markdown secure rendering\n* Responsive design\n* Backend form builder\n* Image generation on the fly\n* Captcha integration\n* Fix error api systemnavi\n* Reference feature\n* Typemill Utilities\n* Clear cache\n* Show security Log\n* User search only for +10 users","## Cleanups:","* DONE: Events\n* Error messages\n* Translations","## Info: Select userroles","* Userroles for file restriction: in vue-blox-components loaded via api\n* Userroles for userfields: in php model user getUserFields()\n* Userroles for meta: in php controller apiAuthorMeta getMeta()\n* Plugins and themes: in php model extension getThemeDefinitions()","## Info: License Check","* On activation in apiControllerExtension. It checks the license in yaml.\n* In plugin php code with setPremiumLicense\n* In static plugins, it checks manual premium list and method setPremiumLicense and more "] \ No newline at end of file diff --git a/data/navigation/navi-live.txt b/data/navigation/navi-live.txt new file mode 100644 index 0000000..fa2b493 --- /dev/null +++ b/data/navigation/navi-live.txt @@ -0,0 +1 @@ +a:2:{i:0;O:8:"stdClass":22:{s:12:"originalName";s:10:"00-welcome";s:11:"elementType";s:6:"folder";s:8:"contains";s:5:"pages";s:6:"status";s:9:"published";s:8:"fileType";s:2:"md";s:5:"order";s:2:"00";s:4:"name";s:7:"welcome";s:4:"slug";s:7:"welcome";s:4:"path";s:11:"/00-welcome";s:15:"pathWithoutType";s:17:"/00-welcome/index";s:9:"urlRelWoF";s:8:"/welcome";s:6:"urlRel";s:17:"/typemill/welcome";s:6:"urlAbs";s:33:"http://localhost/typemill/welcome";s:3:"key";i:0;s:7:"keyPath";i:0;s:12:"keyPathArray";a:1:{i:0;s:1:"0";}s:7:"chapter";i:1;s:6:"active";b:0;s:12:"activeParent";b:0;s:4:"hide";b:0;s:13:"folderContent";a:6:{i:0;O:8:"stdClass":20:{s:12:"originalName";s:24:"00-setup-your-website.md";s:11:"elementType";s:4:"file";s:6:"status";s:9:"published";s:8:"fileType";s:2:"md";s:5:"order";s:2:"00";s:4:"name";s:18:"setup your website";s:4:"slug";s:18:"setup-your-website";s:4:"path";s:36:"/00-welcome/00-setup-your-website.md";s:15:"pathWithoutType";s:33:"/00-welcome/00-setup-your-website";s:3:"key";i:0;s:7:"keyPath";s:3:"0.0";s:12:"keyPathArray";a:2:{i:0;s:1:"0";i:1;s:1:"0";}s:7:"chapter";s:3:"1.1";s:9:"urlRelWoF";s:27:"/welcome/setup-your-website";s:6:"urlRel";s:36:"/typemill/welcome/setup-your-website";s:6:"urlAbs";s:52:"http://localhost/typemill/welcome/setup-your-website";s:6:"active";b:0;s:12:"activeParent";b:0;s:4:"hide";b:0;s:7:"noindex";b:0;}i:1;O:8:"stdClass":20:{s:12:"originalName";s:19:"01-manage-access.md";s:11:"elementType";s:4:"file";s:6:"status";s:9:"published";s:8:"fileType";s:2:"md";s:5:"order";s:2:"01";s:4:"name";s:13:"manage access";s:4:"slug";s:13:"manage-access";s:4:"path";s:31:"/00-welcome/01-manage-access.md";s:15:"pathWithoutType";s:28:"/00-welcome/01-manage-access";s:3:"key";i:1;s:7:"keyPath";s:3:"0.1";s:12:"keyPathArray";a:2:{i:0;s:1:"0";i:1;s:1:"1";}s:7:"chapter";s:3:"1.2";s:9:"urlRelWoF";s:22:"/welcome/manage-access";s:6:"urlRel";s:31:"/typemill/welcome/manage-access";s:6:"urlAbs";s:47:"http://localhost/typemill/welcome/manage-access";s:6:"active";b:0;s:12:"activeParent";b:0;s:4:"hide";b:0;s:7:"noindex";b:0;}i:2;O:8:"stdClass":20:{s:12:"originalName";s:19:"02-write-content.md";s:11:"elementType";s:4:"file";s:6:"status";s:9:"published";s:8:"fileType";s:2:"md";s:5:"order";s:2:"02";s:4:"name";s:13:"write content";s:4:"slug";s:13:"write-content";s:4:"path";s:31:"/00-welcome/02-write-content.md";s:15:"pathWithoutType";s:28:"/00-welcome/02-write-content";s:3:"key";i:2;s:7:"keyPath";s:3:"0.2";s:12:"keyPathArray";a:2:{i:0;s:1:"0";i:1;s:1:"2";}s:7:"chapter";s:3:"1.3";s:9:"urlRelWoF";s:22:"/welcome/write-content";s:6:"urlRel";s:31:"/typemill/welcome/write-content";s:6:"urlAbs";s:47:"http://localhost/typemill/welcome/write-content";s:6:"active";b:0;s:12:"activeParent";b:0;s:4:"hide";b:0;s:7:"noindex";b:0;}i:3;O:8:"stdClass":20:{s:12:"originalName";s:14:"03-get-help.md";s:11:"elementType";s:4:"file";s:6:"status";s:9:"published";s:8:"fileType";s:2:"md";s:5:"order";s:2:"03";s:4:"name";s:8:"get help";s:4:"slug";s:8:"get-help";s:4:"path";s:26:"/00-welcome/03-get-help.md";s:15:"pathWithoutType";s:23:"/00-welcome/03-get-help";s:3:"key";i:3;s:7:"keyPath";s:3:"0.3";s:12:"keyPathArray";a:2:{i:0;s:1:"0";i:1;s:1:"3";}s:7:"chapter";s:3:"1.4";s:9:"urlRelWoF";s:17:"/welcome/get-help";s:6:"urlRel";s:26:"/typemill/welcome/get-help";s:6:"urlAbs";s:42:"http://localhost/typemill/welcome/get-help";s:6:"active";b:0;s:12:"activeParent";b:0;s:4:"hide";b:0;s:7:"noindex";b:0;}i:4;O:8:"stdClass":20:{s:12:"originalName";s:19:"04-markdown-test.md";s:11:"elementType";s:4:"file";s:6:"status";s:9:"published";s:8:"fileType";s:2:"md";s:5:"order";s:2:"04";s:4:"name";s:13:"markdown test";s:4:"slug";s:13:"markdown-test";s:4:"path";s:31:"/00-welcome/04-markdown-test.md";s:15:"pathWithoutType";s:28:"/00-welcome/04-markdown-test";s:3:"key";i:4;s:7:"keyPath";s:3:"0.4";s:12:"keyPathArray";a:2:{i:0;s:1:"0";i:1;s:1:"4";}s:7:"chapter";s:3:"1.5";s:9:"urlRelWoF";s:22:"/welcome/markdown-test";s:6:"urlRel";s:31:"/typemill/welcome/markdown-test";s:6:"urlAbs";s:47:"http://localhost/typemill/welcome/markdown-test";s:6:"active";b:0;s:12:"activeParent";b:0;s:4:"hide";b:0;s:7:"noindex";b:0;}i:5;O:8:"stdClass":20:{s:12:"originalName";s:11:"05-todos.md";s:11:"elementType";s:4:"file";s:6:"status";s:9:"published";s:8:"fileType";s:2:"md";s:5:"order";s:2:"05";s:4:"name";s:6:"To Dos";s:4:"slug";s:5:"todos";s:4:"path";s:23:"/00-welcome/05-todos.md";s:15:"pathWithoutType";s:20:"/00-welcome/05-todos";s:3:"key";i:5;s:7:"keyPath";s:3:"0.5";s:12:"keyPathArray";a:2:{i:0;s:1:"0";i:1;s:1:"5";}s:7:"chapter";s:3:"1.6";s:9:"urlRelWoF";s:14:"/welcome/todos";s:6:"urlRel";s:23:"/typemill/welcome/todos";s:6:"urlAbs";s:39:"http://localhost/typemill/welcome/todos";s:6:"active";b:0;s:12:"activeParent";b:0;s:4:"hide";b:0;s:7:"noindex";b:0;}}s:7:"noindex";b:0;}i:1;O:8:"stdClass":22:{s:12:"originalName";s:16:"01-cyanine-theme";s:11:"elementType";s:6:"folder";s:8:"contains";s:5:"pages";s:6:"status";s:9:"published";s:8:"fileType";s:2:"md";s:5:"order";s:2:"01";s:4:"name";s:13:"cyanine theme";s:4:"slug";s:13:"cyanine-theme";s:4:"path";s:17:"/01-cyanine-theme";s:15:"pathWithoutType";s:23:"/01-cyanine-theme/index";s:9:"urlRelWoF";s:14:"/cyanine-theme";s:6:"urlRel";s:23:"/typemill/cyanine-theme";s:6:"urlAbs";s:39:"http://localhost/typemill/cyanine-theme";s:3:"key";i:1;s:7:"keyPath";i:1;s:12:"keyPathArray";a:1:{i:0;s:1:"1";}s:7:"chapter";i:2;s:6:"active";b:0;s:12:"activeParent";b:0;s:4:"hide";b:0;s:13:"folderContent";a:4:{i:0;O:8:"stdClass":20:{s:12:"originalName";s:17:"00-landingpage.md";s:11:"elementType";s:4:"file";s:6:"status";s:9:"published";s:8:"fileType";s:2:"md";s:5:"order";s:2:"00";s:4:"name";s:11:"landingpage";s:4:"slug";s:11:"landingpage";s:4:"path";s:35:"/01-cyanine-theme/00-landingpage.md";s:15:"pathWithoutType";s:32:"/01-cyanine-theme/00-landingpage";s:3:"key";i:0;s:7:"keyPath";s:3:"1.0";s:12:"keyPathArray";a:2:{i:0;s:1:"1";i:1;s:1:"0";}s:7:"chapter";s:3:"2.1";s:9:"urlRelWoF";s:26:"/cyanine-theme/landingpage";s:6:"urlRel";s:35:"/typemill/cyanine-theme/landingpage";s:6:"urlAbs";s:51:"http://localhost/typemill/cyanine-theme/landingpage";s:6:"active";b:0;s:12:"activeParent";b:0;s:4:"hide";b:0;s:7:"noindex";b:1;}i:1;O:8:"stdClass":20:{s:12:"originalName";s:22:"01-colors-and-fonts.md";s:11:"elementType";s:4:"file";s:6:"status";s:9:"published";s:8:"fileType";s:2:"md";s:5:"order";s:2:"01";s:4:"name";s:16:"colors and fonts";s:4:"slug";s:16:"colors-and-fonts";s:4:"path";s:40:"/01-cyanine-theme/01-colors-and-fonts.md";s:15:"pathWithoutType";s:37:"/01-cyanine-theme/01-colors-and-fonts";s:3:"key";i:1;s:7:"keyPath";s:3:"1.1";s:12:"keyPathArray";a:2:{i:0;s:1:"1";i:1;s:1:"1";}s:7:"chapter";s:3:"2.2";s:9:"urlRelWoF";s:31:"/cyanine-theme/colors-and-fonts";s:6:"urlRel";s:40:"/typemill/cyanine-theme/colors-and-fonts";s:6:"urlAbs";s:56:"http://localhost/typemill/cyanine-theme/colors-and-fonts";s:6:"active";b:0;s:12:"activeParent";b:0;s:4:"hide";b:0;s:7:"noindex";b:0;}i:2;O:8:"stdClass":20:{s:12:"originalName";s:12:"02-footer.md";s:11:"elementType";s:4:"file";s:6:"status";s:9:"published";s:8:"fileType";s:2:"md";s:5:"order";s:2:"02";s:4:"name";s:6:"footer";s:4:"slug";s:6:"footer";s:4:"path";s:30:"/01-cyanine-theme/02-footer.md";s:15:"pathWithoutType";s:27:"/01-cyanine-theme/02-footer";s:3:"key";i:2;s:7:"keyPath";s:3:"1.2";s:12:"keyPathArray";a:2:{i:0;s:1:"1";i:1;s:1:"2";}s:7:"chapter";s:3:"2.3";s:9:"urlRelWoF";s:21:"/cyanine-theme/footer";s:6:"urlRel";s:30:"/typemill/cyanine-theme/footer";s:6:"urlAbs";s:46:"http://localhost/typemill/cyanine-theme/footer";s:6:"active";b:0;s:12:"activeParent";b:0;s:4:"hide";b:0;s:7:"noindex";b:0;}i:3;O:8:"stdClass":20:{s:12:"originalName";s:22:"03-content-elements.md";s:11:"elementType";s:4:"file";s:6:"status";s:9:"published";s:8:"fileType";s:2:"md";s:5:"order";s:2:"03";s:4:"name";s:16:"content elements";s:4:"slug";s:16:"content-elements";s:4:"path";s:40:"/01-cyanine-theme/03-content-elements.md";s:15:"pathWithoutType";s:37:"/01-cyanine-theme/03-content-elements";s:3:"key";i:3;s:7:"keyPath";s:3:"1.3";s:12:"keyPathArray";a:2:{i:0;s:1:"1";i:1;s:1:"3";}s:7:"chapter";s:3:"2.4";s:9:"urlRelWoF";s:31:"/cyanine-theme/content-elements";s:6:"urlRel";s:40:"/typemill/cyanine-theme/content-elements";s:6:"urlAbs";s:56:"http://localhost/typemill/cyanine-theme/content-elements";s:6:"active";b:0;s:12:"activeParent";b:0;s:4:"hide";b:0;s:7:"noindex";b:0;}}s:7:"noindex";b:0;}} \ No newline at end of file diff --git a/media/tmp/1528492471.svg b/media/tmp/1528492471.svg new file mode 100644 index 0000000..c9f8796 --- /dev/null +++ b/media/tmp/1528492471.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/system/typemill/Controllers/ControllerApiFile.php b/system/typemill/Controllers/ControllerApiFile.php index ee4e09e..0f30d7b 100644 --- a/system/typemill/Controllers/ControllerApiFile.php +++ b/system/typemill/Controllers/ControllerApiFile.php @@ -206,45 +206,46 @@ class ControllerApiFile extends Controller $media = new Media(); - $fileinfo = $fileProcessor->storeFile($params['file'], $params['name']); - $filePath = str_replace('media/files', 'media/tmp', $fileinfo['url']); - - if($fileinfo) + $fileinfo = $media->storeFile($params['file'], $params['name']); + if(!$fileinfo OR !isset($fileinfo['url'])) { - # if the previous check of the mtype with the base64 string failed, then do it now again with the temporary file - if(!$mtype) - { - $fullPath = $this->settings['rootPath'] . $filePath; - $finfo = finfo_open( FILEINFO_MIME_TYPE ); - $mtype = @finfo_file( $finfo, $fullPath ); - finfo_close($finfo); - - if(!$mtype OR !$this->checkAllowedMimeTypes($mtype, $extension)) - { - $media->clearTempFolder(); - - $response->getBody()->write(json_encode([ - 'message' => 'The mime-type is missing, not allowed or does not fit to the file extension.' - ])); - - return $response->withHeader('Content-Type', 'application/json')->withStatus(422); - } - } - $response->getBody()->write(json_encode([ - 'message' => 'File has been stored', - 'fileinfo' => $fileinfo, - 'filepath' => $filePath + 'message' => 'We Could not store file to temporary folder.', + 'fullerrors' => $media->errors ])); - return $response->withHeader('Content-Type', 'application/json'); + return $response->withHeader('Content-Type', 'application/json')->withStatus(422); } + # if the previous check of the mtype with the base64 string failed, then do it now again with the temporary file + if(!$mtype) + { + $fullPath = $this->settings['rootPath'] . $filePath; + $finfo = finfo_open( FILEINFO_MIME_TYPE ); + $mtype = @finfo_file( $finfo, $fullPath ); + finfo_close($finfo); + + if(!$mtype OR !$this->checkAllowedMimeTypes($mtype, $extension)) + { + $media->clearTempFolder(); + + $response->getBody()->write(json_encode([ + 'message' => 'The mime-type is missing, not allowed or does not fit to the file extension.' + ])); + + return $response->withHeader('Content-Type', 'application/json')->withStatus(422); + } + } + + $filePath = str_replace('media/files', 'media/tmp', $fileinfo['url']); + $response->getBody()->write(json_encode([ - 'message' => 'Could not store file to temporary folder.' + 'message' => 'File has been stored', + 'fileinfo' => $fileinfo, + 'filepath' => $filePath ])); - return $response->withHeader('Content-Type', 'application/json')->withStatus(422); + return $response->withHeader('Content-Type', 'application/json'); } public function publishFile(Request $request, Response $response, $args) diff --git a/system/typemill/Controllers/ControllerApiImage.php b/system/typemill/Controllers/ControllerApiImage.php index 18a799e..5001873 100644 --- a/system/typemill/Controllers/ControllerApiImage.php +++ b/system/typemill/Controllers/ControllerApiImage.php @@ -155,7 +155,7 @@ class ControllerApiImage extends Controller # check if image name already exisits in live folder and create an unique name (do not overwrite existing files) $storage = new StorageWrapper('\Typemill\Models\Storage'); - $uniqueImageName = $storage->createUniqueImageName($img->getFilename(), $img->getExtension()); + $uniqueImageName = $storage->createUniqueImageName($media->getFilename(), $media->getExtension()); $media->setFilename($uniqueImageName); # store the original image diff --git a/system/typemill/Controllers/ControllerWebAuthor.php b/system/typemill/Controllers/ControllerWebAuthor.php index 4199724..0d53ffc 100644 --- a/system/typemill/Controllers/ControllerWebAuthor.php +++ b/system/typemill/Controllers/ControllerWebAuthor.php @@ -7,6 +7,7 @@ use Psr\Http\Message\ResponseInterface as Response; use Slim\Routing\RouteContext; use Typemill\Models\Navigation; use Typemill\Models\Content; +use Typemill\Models\SvgSanitizer; use Typemill\Events\OnPagetreeLoaded; use Typemill\Events\OnItemLoaded; use Typemill\Events\OnMarkdownLoaded; @@ -17,6 +18,9 @@ class ControllerWebAuthor extends Controller { public function showBlox(Request $request, Response $response, $args) { + + $svg = new SvgSanitizer(); + # get url for requested page $url = isset($args['route']) ? '/' . $args['route'] : '/'; $urlinfo = $this->c->get('urlinfo'); diff --git a/system/typemill/Models/Media.php b/system/typemill/Models/Media.php index 9b08e67..84e3721 100644 --- a/system/typemill/Models/Media.php +++ b/system/typemill/Models/Media.php @@ -3,6 +3,7 @@ namespace Typemill\Models; use Typemill\Models\Folder; +use Typemill\Models\SvgSanitizer; use Typemill\Static\Slug; class Media @@ -92,10 +93,6 @@ class Media public function decode(string $file) { - $fileParts = explode(";base64,", $file); - $fileType = explode("/", $fileParts[0]); - $fileData = base64_decode($fileParts[1]); - $fileParts = explode(";base64,", $file); if(!isset($fileParts[0]) OR !isset($fileParts[1])) @@ -106,7 +103,7 @@ class Media } $type = explode("/", $fileParts[0]); - $this->filetype = strtolower($fileType[0]); + $this->filetype = strtolower($type[1]); $this->filedata = base64_decode($fileParts[1]); return true; @@ -195,6 +192,29 @@ class Media $this->decode($file); + if($this->extension == "svg") + { + $svg = new SvgSanitizer(); + + $loaded = $svg->loadSVG($this->filedata); + if($loaded === false) + { + $this->errors[] = 'We could not load the svg file, it is probably corrupted.'; + return false; + } + + $svg->sanitize(); + $sanitized = $svg->saveSVG(); + if($sanitized === false) + { + $this->errors[] = 'We could not create a sanitized version of the svg, it probably has invalid content.'; + return false; + } + + $this->filedata = $sanitized; + } + + $fullpath = $this->getFullPath(); if($this->filedata !== false && file_put_contents($fullpath, $this->filedata)) @@ -301,6 +321,28 @@ class Media return false; } + if($this->extension == "svg") + { + $svg = new SvgSanitizer(); + + $loaded = $svg->loadSVG($this->filedata); + if($loaded === false) + { + $this->errors[] = 'We could not load the svg file, it is probably corrupted.'; + return false; + } + + $svg->sanitize(); + $sanitized = $svg->saveSVG(); + if($sanitized === false) + { + $this->errors[] = 'We could not create a sanitized version of the svg, it probably has invalid content.'; + return false; + } + + $this->filedata = $sanitized; + } + return true; } @@ -335,8 +377,9 @@ class Media public function saveOriginal($destinationfolder = 'ORIGINAL') { $path = $this->tmpFolder . $destinationfolder . '+' . $this->filename . '.' . $this->extension; - - if(!file_put_contents($path, $this->filedata)) + + $result = file_put_contents($path, $this->filedata); + if($result === false) { $this->errors[] = 'could not store the image in the temporary folder'; } @@ -347,6 +390,12 @@ class Media { $this->saveOriginal('LIVE'); $this->saveOriginal('THUMBS'); + + if(empty($this->errors)) + { + return true; + } + return false; } public function createImage() diff --git a/system/typemill/Models/SvgSanitizer.php b/system/typemill/Models/SvgSanitizer.php new file mode 100644 index 0000000..261f686 --- /dev/null +++ b/system/typemill/Models/SvgSanitizer.php @@ -0,0 +1,116 @@ + ['class' => true, 'clip-path' => true, 'clip-rule' => true, 'fill' => true, 'fill-opacity' => true, 'fill-rule' => true, 'filter' => true, 'id' => true, 'mask' => true, 'opacity' => true, 'stroke' => true, 'stroke-dasharray' => true, 'stroke-dashoffset' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, 'stroke-miterlimit' => true, 'stroke-opacity' => true, 'stroke-width' => true, 'style' => true, 'systemLanguage' => true, 'transform' => true, 'xlink:title' => true], + 'circle' => ['class' => true, 'clip-path' => true, 'clip-rule' => true, 'cx' => true, 'cy' => true, 'fill' => true, 'fill-opacity' => true, 'fill-rule' => true, 'filter' => true, 'id' => true, 'mask' => true, 'opacity' => true, 'r' => true, 'requiredFeatures' => true, 'stroke' => true, 'stroke-dasharray' => true, 'stroke-dashoffset' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, 'stroke-miterlimit' => true, 'stroke-opacity' => true, 'stroke-width' => true, 'style' => true, 'systemLanguage' => true, 'transform' => true], + 'clipPath' => ['class' => true, 'clipPathUnits' => true, 'id' => true], + 'defs' => [], + 'style' => ['type' => true], + 'desc' => [], + 'ellipse' => ['class' => true, 'clip-path' => true, 'clip-rule' => true, 'cx' => true, 'cy' => true, 'fill' => true, 'fill-opacity' => true, 'fill-rule' => true, 'filter' => true, 'id' => true, 'mask' => true, 'opacity' => true, 'requiredFeatures' => true, 'rx' => true, 'ry' => true, 'stroke' => true, 'stroke-dasharray' => true, 'stroke-dashoffset' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, 'stroke-miterlimit' => true, 'stroke-opacity' => true, 'stroke-width' => true, 'style' => true, 'systemLanguage' => true, 'transform' => true], + 'feGaussianBlur' => ['class' => true, 'color-interpolation-filters' => true, 'id' => true, 'requiredFeatures' => true, 'stdDeviation' => true], + 'filter' => ['class' => true, 'color-interpolation-filters' => true, 'filterRes' => true, 'filterUnits' => true, 'height' => true, 'id' => true, 'primitiveUnits' => true, 'requiredFeatures' => true, 'width' => true, 'x' => true, 'y' => true], + 'foreignObject' => ['class' => true, 'font-size' => true, 'height' => true, 'id' => true, 'opacity' => true, 'requiredFeatures' => true, 'style' => true, 'transform' => true, 'width' => true, 'x' => true, 'y' => true], + 'g' => ['class' => true, 'clip-path' => true, 'clip-rule' => true, 'id' => true, 'display' => true, 'fill' => true, 'fill-opacity' => true, 'fill-rule' => true, 'filter' => true, 'mask' => true, 'opacity' => true, 'requiredFeatures' => true, 'stroke' => true, 'stroke-dasharray' => true, 'stroke-dashoffset' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, 'stroke-miterlimit' => true, 'stroke-opacity' => true, 'stroke-width' => true, 'style' => true, 'systemLanguage' => true, 'transform' => true, 'font-family' => true, 'font-size' => true, 'font-style' => true, 'font-weight' => true, 'text-anchor' => true], + 'image' => ['class' => true, 'clip-path' => true, 'clip-rule' => true, 'filter' => true, 'height' => true, 'id' => true, 'mask' => true, 'opacity' => true, 'requiredFeatures' => true, 'style' => true, 'systemLanguage' => true, 'transform' => true, 'width' => true, 'x' => true, 'xlink:title' => true, 'y' => true], + 'line' => ['class' => true, 'clip-path' => true, 'clip-rule' => true, 'fill' => true, 'fill-opacity' => true, 'fill-rule' => true, 'filter' => true, 'id' => true, 'marker-end' => true, 'marker-mid' => true, 'marker-start' => true, 'mask' => true, 'opacity' => true, 'requiredFeatures' => true, 'stroke' => true, 'stroke-dasharray' => true, 'stroke-dashoffset' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, 'stroke-miterlimit' => true, 'stroke-opacity' => true, 'stroke-width' => true, 'style' => true, 'systemLanguage' => true, 'transform' => true, 'x1' => true, 'x2' => true, 'y1' => true, 'y2' => true], + 'linearGradient' => ['class' => true, 'id' => true, 'gradientTransform' => true, 'gradientUnits' => true, 'requiredFeatures' => true, 'spreadMethod' => true, 'systemLanguage' => true, 'x1' => true, 'x2' => true, 'y1' => true, 'y2' => true], + 'marker' => ['id' => true, 'class' => true, 'markerHeight' => true, 'markerUnits' => true, 'markerWidth' => true, 'orient' => true, 'preserveAspectRatio' => true, 'refX' => true, 'refY' => true, 'systemLanguage' => true, 'viewBox' => true], + 'mask' => ['class' => true, 'height' => true, 'id' => true, 'maskContentUnits' => true, 'maskUnits' => true, 'width' => true, 'x' => true, 'y' => true], + 'metadata' => ['class' => true, 'id' => true], + 'path' => ['class' => true, 'clip-path' => true, 'clip-rule' => true, 'd' => true, 'fill' => true, 'fill-opacity' => true, 'fill-rule' => true, 'filter' => true, 'id' => true, 'marker-end' => true, 'marker-mid' => true, 'marker-start' => true, 'mask' => true, 'opacity' => true, 'requiredFeatures' => true, 'stroke' => true, 'stroke-dasharray' => true, 'stroke-dashoffset' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, 'stroke-miterlimit' => true, 'stroke-opacity' => true, 'stroke-width' => true, 'style' => true, 'systemLanguage' => true, 'transform' => true], + 'pattern' => ['class' => true, 'height' => true, 'id' => true, 'patternContentUnits' => true, 'patternTransform' => true, 'patternUnits' => true, 'requiredFeatures' => true, 'style' => true, 'systemLanguage' => true, 'viewBox' => true, 'width' => true, 'x' => true, 'y' => true], + 'polygon' => ['class' => true, 'clip-path' => true, 'clip-rule' => true, 'id' => true, 'fill' => true, 'fill-opacity' => true, 'fill-rule' => true, 'filter' => true, 'id' => true, 'class' => true, 'marker-end' => true, 'marker-mid' => true, 'marker-start' => true, 'mask' => true, 'opacity' => true, 'points' => true, 'requiredFeatures' => true, 'stroke' => true, 'stroke-dasharray' => true, 'stroke-dashoffset' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, 'stroke-miterlimit' => true, 'stroke-opacity' => true, 'stroke-width' => true, 'style' => true, 'systemLanguage' => true, 'transform' => true], + 'polyline' => ['class' => true, 'clip-path' => true, 'clip-rule' => true, 'id' => true, 'fill' => true, 'fill-opacity' => true, 'fill-rule' => true, 'filter' => true, 'marker-end' => true, 'marker-mid' => true, 'marker-start' => true, 'mask' => true, 'opacity' => true, 'points' => true, 'requiredFeatures' => true, 'stroke' => true, 'stroke-dasharray' => true, 'stroke-dashoffset' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, 'stroke-miterlimit' => true, 'stroke-opacity' => true, 'stroke-width' => true, 'style' => true, 'systemLanguage' => true, 'transform' => true], + 'radialGradient' => ['class' => true, 'cx' => true, 'cy' => true, 'fx' => true, 'fy' => true, 'gradientTransform' => true, 'gradientUnits' => true, 'id' => true, 'r' => true, 'requiredFeatures' => true, 'spreadMethod' => true, 'systemLanguage' => true], + 'rect' => ['class' => true, 'clip-path' => true, 'clip-rule' => true, 'fill' => true, 'fill-opacity' => true, 'fill-rule' => true, 'filter' => true, 'height' => true, 'id' => true, 'mask' => true, 'opacity' => true, 'requiredFeatures' => true, 'rx' => true, 'ry' => true, 'stroke' => true, 'stroke-dasharray' => true, 'stroke-dashoffset' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, 'stroke-miterlimit' => true, 'stroke-opacity' => true, 'stroke-width' => true, 'style' => true, 'systemLanguage' => true, 'transform' => true, 'width' => true, 'x' => true, 'y' => true], + 'stop' => ['class' => true, 'id' => true, 'offset' => true, 'requiredFeatures' => true, 'stop-color' => true, 'stop-opacity' => true, 'style' => true, 'systemLanguage' => true], + 'svg' => ['class' => true, 'clip-path' => true, 'clip-rule' => true, 'filter' => true, 'id' => true, 'height' => true, 'mask' => true, 'preserveAspectRatio' => true, 'requiredFeatures' => true, 'style' => true, 'systemLanguage' => true, 'viewBox' => true, 'width' => true, 'x' => true, 'xmlns' => true, 'xmlns:se' => true, 'xmlns:xlink' => true, 'y' => true], + 'switch' => ['class' => true, 'id' => true, 'requiredFeatures' => true, 'systemLanguage' => true], + 'symbol' => ['class' => true, 'fill' => true, 'fill-opacity' => true, 'fill-rule' => true, 'filter' => true, 'font-family' => true, 'font-size' => true, 'font-style' => true, 'font-weight' => true, 'id' => true, 'opacity' => true, 'preserveAspectRatio' => true, 'requiredFeatures' => true, 'stroke' => true, 'stroke-dasharray' => true, 'stroke-dashoffset' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, 'stroke-miterlimit' => true, 'stroke-opacity' => true, 'stroke-width' => true, 'style' => true, 'systemLanguage' => true, 'transform' => true, 'viewBox' => true], + 'text' => ['class' => true, 'clip-path' => true, 'clip-rule' => true, 'fill' => true, 'fill-opacity' => true, 'fill-rule' => true, 'filter' => true, 'font-family' => true, 'font-size' => true, 'font-style' => true, 'font-weight' => true, 'id' => true, 'mask' => true, 'opacity' => true, 'requiredFeatures' => true, 'stroke' => true, 'stroke-dasharray' => true, 'stroke-dashoffset' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, 'stroke-miterlimit' => true, 'stroke-opacity' => true, 'stroke-width' => true, 'style' => true, 'systemLanguage' => true, 'text-anchor' => true, 'transform' => true, 'x' => true, 'xml:space' => true, 'y' => true], + 'textPath' => ['class' => true, 'id' => true, 'method' => true, 'requiredFeatures' => true, 'spacing' => true, 'startOffset' => true, 'style' => true, 'systemLanguage' => true, 'transform' => true], + 'title' => [], + 'tspan' => ['class' => true, 'clip-path' => true, 'clip-rule' => true, 'dx' => true, 'dy' => true, 'fill' => true, 'fill-opacity' => true, 'fill-rule' => true, 'filter' => true, 'font-family' => true, 'font-size' => true, 'font-style' => true, 'font-weight' => true, 'id' => true, 'mask' => true, 'opacity' => true, 'requiredFeatures' => true, 'rotate' => true, 'stroke' => true, 'stroke-dasharray' => true, 'stroke-dashoffset' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, 'stroke-miterlimit' => true, 'stroke-opacity' => true, 'stroke-width' => true, 'style' => true, 'systemLanguage' => true, 'text-anchor' => true, 'textLength' => true, 'transform' => true, 'x' => true, 'xml:space' => true, 'y' => true], + 'use' => ['class' => true, 'clip-path' => true, 'clip-rule' => true, 'fill' => true, 'fill-opacity' => true, 'fill-rule' => true, 'filter' => true, 'height' => true, 'id' => true, 'mask' => true, 'stroke' => true, 'stroke-dasharray' => true, 'stroke-dashoffset' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, 'stroke-miterlimit' => true, 'stroke-opacity' => true, 'stroke-width' => true, 'style' => true, 'transform' => true, 'width' => true, 'x' => true, 'y' => true], + ]; + + function __construct() { + $this->xmlDoc = new \DOMDocument(); + $this->xmlDoc->preserveWhiteSpace = false; + } + + //Load the SVG data from a file + function load($file) { + $this->xmlDoc->load($file); + } + + function loadSVG(string $string) { + $result = $this->xmlDoc->loadXML($string); + } + + //Remove any elements from the XML that are unrelated to SVGs + function sanitize() { + + //Get every element in the document, and loop through them all + $allElements = $this->xmlDoc->getElementsByTagName("*"); + + for ($i = 0; $i < $allElements->length; $i++) { + $currentNode = $allElements->item($i); + + //Remove any elements not on the whitelist + if (!isset(self::$whitelist[$currentNode->tagName])) { + $currentNode->parentNode->removeChild($currentNode); + $i--; + + } else { + $attributesWhitelist = self::$whitelist[$currentNode->tagName]; + $attributesToRemove = []; + + //If the element is allowed, loop through checking its attributes v.s. the attributes allowed for that element + for ($j = 0; $j < $currentNode->attributes->length; $j++) { + $attrName = $currentNode->attributes->item($j)->name; + + if (!isset($attributesWhitelist[$attrName])) { + $attributesToRemove[] = $attrName; + } + } + + //Remove any blocked attributes + if (!empty($attributesToRemove)) { + foreach ($attributesToRemove as $attrName) { + $currentNode->removeAttribute($attrName); + } + } + } + } + } + + function saveSVG() { + $this->xmlDoc->formatOutput = true; + return($this->xmlDoc->saveXML()); + } + + function save($file) { + $this->xmlDoc->formatOutput = true; + return($this->xmlDoc->save($file)); + } +} diff --git a/system/typemill/author/js/vue-blox-components.js b/system/typemill/author/js/vue-blox-components.js index 8791ecb..23d0a13 100644 --- a/system/typemill/author/js/vue-blox-components.js +++ b/system/typemill/author/js/vue-blox-components.js @@ -1639,13 +1639,11 @@ bloxeditor.component('image-component', { if(errors) { - console.info(errors); + eventBus.$emit('publishermessage', errors); } else { this.compmarkdown = imgmarkdown; -// publishController.errors.message = false; -// this.$parent.activatePage(); this.$emit('updateMarkdownEvent', imgmarkdown); } @@ -1718,13 +1716,14 @@ bloxeditor.component('image-component', { if (!imageFile.type.match('image.*')) { - alert("wrong format"); -// publishController.errors.message = "Only images are allowed."; + let message = this.$filters.translate('Only images are allowed'); + eventBus.$emit('publishermessage', message); } else if (size > this.maxsize) { - alert("wrong size"); -// publishController.errors.message = "The maximal size of images is " + this.maxsize + " MB"; + let message = "The maximal size of images is " + this.maxsize + " MB"; + message = this.$filters.translate(message); + eventBus.$emit('publishermessage', message); } else { @@ -1761,9 +1760,10 @@ bloxeditor.component('image-component', { { if(error.response) { - alert("errror in response"); + let message = error.response.data.message; + message = self.$filters.translate(message); + eventBus.$emit('publishermessage', message); } - }); } } @@ -1775,7 +1775,8 @@ bloxeditor.component('image-component', { if(!this.imgfile) { - alert("no file"); + let message = this.$filters.translate("Imagefile is missing."); + eventBus.$emit('publishermessage', message); return; } if(!this.saveimage) @@ -1804,7 +1805,8 @@ bloxeditor.component('image-component', { { if(error.response) { - console.info(error.response); + let message = self.$filters.translate(error.response.data.message); + eventBus.$emit('publishermessage', message); } }); } @@ -1976,14 +1978,10 @@ bloxeditor.component('file-component', { } if(errors) { - alert("errors"); -// this.$parent.freezePage(); -// publishController.errors.message = errors; + eventBus.$emit('publishermessage', this.$filters.translate(errors)); } else { -// publishController.errors.message = false; -// this.$parent.activatePage(); this.$emit('updateMarkdownEvent', filemarkdown); this.compmarkdown = filemarkdown; } @@ -2011,7 +2009,11 @@ bloxeditor.component('file-component', { }) .catch(function (error) { - alert("error response"); + if(error.response.data.message) + { + let message = myself.$filters.translate(error.response.data.message); + eventBus.$emit('publishermessage', message); + } }); }, updaterestriction: function() @@ -2033,15 +2035,13 @@ bloxeditor.component('file-component', { if (size > this.maxsize) { - alert("error size"); - // publishController.errors.message = "The maximal size of a file is " + this.maxsize + " MB"; + let message = "The maximal size of a file is " + this.maxsize + " MB"; + eventBus.$emit('publishermessage', message); } else { self = this; -// self.$parent.freezePage(); -// self.$root.$data.file = true; self.load = true; let reader = new FileReader(); @@ -2056,7 +2056,6 @@ bloxeditor.component('file-component', { .then(function (response) { self.load = false; -// self.$parent.activatePage(); self.filemeta = true; self.savefile = true; @@ -2070,11 +2069,10 @@ bloxeditor.component('file-component', { .catch(function (error) { self.load = false; -// self.$parent.activatePage(); if(error.response) { - alert("error response") -// publishController.errors.message = error.response.data.errors; + let message = self.$filters.translate(error.response.data.message); + eventBus.$emit('publishermessage', message); } }); } @@ -2087,7 +2085,9 @@ bloxeditor.component('file-component', { if(!this.fileurl) { - alert("no file"); + let message = this.$filters.translate('file is missing.'); + eventBus.$emit('publishermessage', message); + return; } @@ -2116,7 +2116,8 @@ bloxeditor.component('file-component', { { if(error.response) { - console.info(error.response); + let message = self.$filters.translate(error.response.data.message); + eventBus.$emit('publishermessage', message); } }); } @@ -2206,7 +2207,8 @@ bloxeditor.component('video-component', { if(this.provider != "youtube") { this.updatemarkdown(""); - alert("we only support youtube right now"); + let message = this.$filters.translate("We only support youtube right now."); + eventBus.$emit('publishermessage', message); } }, updatemarkdown(url) @@ -2242,7 +2244,8 @@ bloxeditor.component('video-component', { { if(error.response) { - console.info(error.response); + let message = self.$filters.translate(error.response.data.message); + eventBus.$emit('publishermessage', message); } }); }, @@ -2286,23 +2289,27 @@ bloxeditor.component('shortcode-component', { var myself = this; - tmaxios.get('/api/v1/shortcodedata',{ - params: { + tmaxios.get('/api/v1/shortcodedata', + { + params: + { 'url': data.urlinfo.route, - } - }) - .then(function (response) { - if(response.data.shortcodedata !== false) - { - myself.shortcodedata = response.data.shortcodedata; - myself.parseshortcode(); - } - }) - .catch(function (error) + } + }) + .then(function (response) + { + if(response.data.shortcodedata !== false) { - if(error.response) + myself.shortcodedata = response.data.shortcodedata; + myself.parseshortcode(); + } + }) + .catch(function (error) + { + if(error.response) { - + let message = self.$filters.translate(error.response.data.message); + eventBus.$emit('publishermessage', message); } }); },