diff --git a/cache/lastCache.txt b/cache/lastCache.txt index 706ab51..e1f0236 100644 --- a/cache/lastCache.txt +++ b/cache/lastCache.txt @@ -1 +1 @@ -1528305569 +1529843776 \ No newline at end of file diff --git a/content/0_typemill/01-use-cases.md b/content/0_typemill/01-use-cases.md index a36d135..15f9c71 100644 --- a/content/0_typemill/01-use-cases.md +++ b/content/0_typemill/01-use-cases.md @@ -16,6 +16,6 @@ Whenever you want to publish a finished text work as a website and if you like w If you want to create a blog, a wiki or a classic corporate website, please use a specialized CMS for that instead of TYPEMILL. -TYPEMILL is under heavy developement and not finished right now. It has an admin panel for settings, but it does not provide an online content editor at the moment. An online editor and different output formats like mobi and ePup are on the roadmap, so stay tuned. +TYPEMILL is under heavy developement and not finished right now. It has an admin panel for settings and a very basic editor for existing content. More options like delete or create new pages and manage your media-files will be added step by step in next weeks. Also different output formats like mobi and ePup are on the roadmap, so stay tuned. -For time being, you can use an offline markdown-editor like Typora and upload your content-files with a FTP software like FileZilla. \ No newline at end of file +You have to use a FTP-software like FileZilla until the basic editing features are ready. Check the roadmap for more informations. \ No newline at end of file diff --git a/content/0_typemill/03-features.md b/content/0_typemill/03-features.md index 378cc1c..e24b697 100644 --- a/content/0_typemill/03-features.md +++ b/content/0_typemill/03-features.md @@ -7,6 +7,7 @@ This is what you can **do with TYPEMILL**: - Create a website with simple files and folders. - Use markdown for your content files. - Use an admin panel to configure your site. +- Use the content editor to edit existing pages. - Choose themes. - Activate plugins. Check the [list of plugins](/writers/plugins) for that. - Create your own theme with HTML, CSS and Twig (a template language for PHP). @@ -21,4 +22,4 @@ This is, what **TYPEMILL does** for you: - It adds hierarchic numbers to your chapters and pages. - It generates a google sitemap, a last modified date and much more. -Right now there is no content editor, so you have to create and edit your content offline with your favourite markdown editor (e.g. Typora) and upload the files with an FTP software. But a content editor will follow soon and we have a great roadmap for TYPEMILL. +Right now there is only a simple content editor that provides basic editing of existing pages. If you want to create new pages or delete existing pages, then you have to use an offline markdown editor like Typora and a FTP software like FileZilla. I will add all basic features for the online-editor step by step within the next weeks. \ No newline at end of file diff --git a/content/0_typemill/09-roadmap.md b/content/0_typemill/09-roadmap.md index 45a980c..b69b2da 100644 --- a/content/0_typemill/09-roadmap.md +++ b/content/0_typemill/09-roadmap.md @@ -4,18 +4,29 @@ There are a lot of plans for future releases of TYPEMILL, but it also follows th Here are some **milestones** of the past: -- Introduction of TYPEMILL version 1.0.0 -- Added a google sitemap with version 1.0.1 -- Added a table of contents tag (TOC) with version 1.0.5 -- Introduced plugins with version 1.1.0 -- Added an author panel for configurations with version 1.1.3 -- Added math support (mathjax/katex) with version 1.1.5 +- Introduction of TYPEMILL (v. 1.0.0) +- Added a google sitemap (v. 1.0.1) +- Added a table of contents tag (TOC) (v. 1.0.5) +- Introduced plugins (v. 1.1.0) +- Added an author panel for configurations (v. 1.1.3) +- Added math support (mathjax/katex) (v. 1.1.5) +- Added a basic content editor to change existing pages (v. 1.2.0) And here is the **roadmap** for this year (2018): -- Add a basic markdown editor to manage your content online. -- Add an Image- and media-management. -- Add an advanced WYSIWYG markdown editor for a great writers experience. +- Editor: Delete content / pages (v. 1.2.1) +- Editor: Create new pages (v. 1.2.2) +- Editor: Move pages (v. 1.2.2) +- Editor: Save as draft or publish live (v. 1.2.3) +- Editor: Edit meta-information (v. 1.2.4) +- Editor: Markdown and HTML-preview (v. 1.2.5) +- Editor: Manage images and assets (v. 1.3.0) +- Editor: Add formatting options (v. 1.3.0) +- Editor: Create a solution for direct preview or WYSIWYG (1.3.0) +- Editor: Create the best author- and writing experience you have ever seen (1.4.0) + +Other features with lower prio: + - Create additional output formats like mobi, epub and pdf. - Create a clean API. - More themes for special publications like documentations, books or lyrics. diff --git a/content/0_typemill/index.md b/content/0_typemill/index.md index 290911d..33a30b0 100644 --- a/content/0_typemill/index.md +++ b/content/0_typemill/index.md @@ -1,3 +1,7 @@ -#About TYPEMILL +# About TYPEMILL -TYPEMILL is a simple flat file CMS to create a website like this. It transforms a bunch of **text files** (Markdown) into a **website** and generates a **navigation**. TYPEMILL is under construction and provides a simple admin area for settings right now. An online editor and different output formats for e-books like mobi and epub are on their way. If you are a developer: TYPEMILL already supports themes and plugins. \ No newline at end of file +TYPEMILL is a simple flat file CMS to create a website like this. It transforms a bunch of **text files** (Markdown) into a **website** and generates a **navigation**. + +TYPEMILL is under construction. Right now it provides only a very basic editor and a simple admin area for settings, plugins and themes. The author-experience will be improved step by step and output formats for e-books like mobi and epub are planned for the future. + +If you are a developer, you can already create your own themes and your own plugins. \ No newline at end of file diff --git a/content/1_getting-started/01-installation.md b/content/1_getting-started/01-installation.md index dcf0bd8..35f5ce2 100644 --- a/content/1_getting-started/01-installation.md +++ b/content/1_getting-started/01-installation.md @@ -13,9 +13,9 @@ Don't forget to make some folders and files writable (set permission to `774`): - `\settings` folder and files - `\content` folder and files -All settings and users are stored in the folder `settings`. You can manually edit these files, but it is not recommended because it can crash the system if done wrong. +All settings and users are stored in the folder `settings`. You can manually edit these files, but it is not recommended because it might crash the system if done wrong. -You can configure your system online, but there is no content editor yet. So for time beeing, you have to edit your content offline with a markdown editor and upload the files with an FTP software. If your changes are not immediately visible, press `F5` to refresh the cache. +You can configure your system online, but there is only a simple editor to change existing content right now. So for time beeing, you have to create new content offline with a markdown editor and upload the files with an FTP software. If your changes are not immediately visible, press `F5` to refresh the cache. If you need more detailed instructions, please read on. @@ -58,4 +58,4 @@ If you run your website with https (recommended) or if you want to redirect www- ## Run Locally -If you are a developer and if you want to run TYPEMILL locally, then simply download TYPEMILL (zip or git) and visit your local folder like `localhost/typemill`. No additional work is required. +If you are a developer and if you want to run TYPEMILL locally, then simply download TYPEMILL (zip or git) and visit your local folder like `localhost/typemill`. No additional work is required. \ No newline at end of file diff --git a/content/1_getting-started/02-settings.md b/content/1_getting-started/02-settings.md index 9f6bc2f..3f0a3c1 100644 --- a/content/1_getting-started/02-settings.md +++ b/content/1_getting-started/02-settings.md @@ -1,13 +1,13 @@ # Settings -As of Version 1.1.3 you can edit all settings in the new authoring panel of TYPEMILL. Just visit the url `yourwebsite.com/tm/login` and go to settings after the login. There you can edit: +As of Version 1.1.3 you can edit all settings in the new author panel of TYPEMILL. Just visit the url `yourwebsite.com/tm/login` and go to settings after the login. There you can edit: * The system (basic settings). * Themes (choose themes and configure it). * Plugins (activate plugins and configure them). * Users (create, update and delete). -All settings are stored in the `\settings` folder of TYPEMILL. It is not recommended to edit the settings manually, because it can crash the system if done wrong. +All settings are stored in the `\settings` folder of TYPEMILL. It is not recommended to edit the settings manually, because it might crash the system if done wrong. ## Advanced Settings @@ -17,5 +17,4 @@ There are some settings that are not available via the author panel. Most of the displayErrorDetails: true ```` -Don't forget to set it back to `false` before you deploy the website live. It is not secure to show the world your internal errors and many hosters will turn off all public error reports by default. - +Don't forget to set it back to `false` before you deploy the website live. It is not secure to show the world your internal errors and many hosters will turn off all public error reports by default. \ No newline at end of file diff --git a/content/2_for-writers/00-quick-start.md b/content/2_for-writers/00-quick-start.md index 8bb714b..5991c5f 100644 --- a/content/2_for-writers/00-quick-start.md +++ b/content/2_for-writers/00-quick-start.md @@ -10,4 +10,4 @@ You are a pro and don't want to read the whole manual? No problem, this is a qui - **F5**: After some changes, use the `F5` key to refresh the navigation manually. - **Lean back** and let TYPEMILL create a nice website for you. -The TYPEMILL system ships with this user manual in the content folder. So you can check folder and look how the files are written and the folders are organized. \ No newline at end of file +The TYPEMILL system ships with this user manual in the content folder. Check how the files are written and how the folders are organized. \ No newline at end of file diff --git a/content/2_for-writers/03-author-panel.md b/content/2_for-writers/03-author-panel.md index 9aab467..7c84e02 100644 --- a/content/2_for-writers/03-author-panel.md +++ b/content/2_for-writers/03-author-panel.md @@ -8,12 +8,26 @@ https://yourwebsite.net/tm/login You can also use the url `https://yourwebsite.net/setup` that redirects to the login screen. +## The Content Editor + +In the **content area** of the author panel you can: + +* Navigate through your existing content. +* Edit all existing content pages with markdown syntax. + +There are several **limitations** right now: + +* You cannot use HTML, JavaScript or any other code in the editor, only markdown-syntax is allowed. +* You cannot delete, create or reorder the pages right now (use FTP for this), but these features are on the way. +* There is no media-management right now. + +The content editor has highest priority in the roadmap, so you can expect al lot of improvements in the next weeks. + +## Settings, Themes and Plugins + In the **settings area** of the author panel you can: * Configure your **system**. * Choose and configure a **theme**. * Activate and configure **plugins**. -* Manage **users**. - -the **content area** of the author-panel is not ready yet, but it is on it's way and will be published in near future. Then you can create and manage all the content of your website online. For time being you have to create your content offline e.g. with a markdown editor and upload your content files with a FTP software. - +* Manage **users**. \ No newline at end of file diff --git a/content/2_for-writers/20-google-sitemap.md b/content/2_for-writers/20-google-sitemap.md index d78adb0..a2e788c 100644 --- a/content/2_for-writers/20-google-sitemap.md +++ b/content/2_for-writers/20-google-sitemap.md @@ -1,3 +1,3 @@ # Google Sitemap -As of version 1.0.1, TYPEMILL creates a google sitemap in the cache folder. You can reach the sitemap with the url `https://yourwebsite.net/cache/sitemap.xml` and add the sitemap to the google search console. The sitemap will update once a day. You can also trigger a manual update with the F5 key (Windows) that refreshs the cache of your browser and the cache of TYPEMILL. \ No newline at end of file +As of version 1.0.1, TYPEMILL creates a google sitemap in the cache folder. You can find the url for the sitemap in the system settings (basically it is something like `https://yourwebsite.net/cache/sitemap.xml`). Simply add the url to the google search console and you are donw. The sitemap will update once a day. You can also trigger a manual update with the F5 key (Windows) that refreshs the cache of your browser and the cache of TYPEMILL. \ No newline at end of file diff --git a/content/5_info/01-release-notes.md b/content/5_info/01-release-notes.md index d68c344..fd0471f 100644 --- a/content/5_info/01-release-notes.md +++ b/content/5_info/01-release-notes.md @@ -1,12 +1,33 @@ -#Release Notes +# Release Notes This is the version history with some release notes. +## Version 1.2.0: Introducing a Basic Content Editor + +_Release date: 25.06.2018_ + +**Please follow the instruction for simple updates** in the [documentation](/gettings-started/update), so simply update the `system` folder. + +Version 1.2.0 introduces a very basic content editor and is a major milestone for the developement of TYPEMILL as a full CMS. With the editor, the author can only edit existing content with markdown syntax right now. It is not possible to delete content or to create new content. These features will be added very soon. + +There are quite a lot of changes in the background: + +* IMPORTANT: HTML and other code is now completely disabled. All code is disallowed in the content editor and all code-syntax will be escaped in the frontend. You can use markdown syntax for fenced code blocks and for inline code to display code-examples on pages. +* Vue.js is added. +* A content navigation is added. +* Save functionality is added with ajax. +* API-routes are added for managing content. +* The content of the editor is validated (might cause problems with lot of code-syntax). +* Errors are displayed in frontend. +* Appropriate server status is send. +* The twig-cache is disabled again. It might become an optional feature in future. +* URL for xml-sitemap is displayed correctly now. + ## Version 1.1.7: Improved Session Management _Release date: 04.06.2018_ -**Please follow the instructions for minor updates** in the [documentation](/gettings-started/update). Please also update the Typemill theme. +**Please follow the instructions for simple updates** in the [documentation](/gettings-started/update). Please also update the Typemill theme. - URL to google sitemap is not displayed in settings. - Session Cookies are only set when authentication is required. diff --git a/content/5_info/20_Übermaß.md b/content/5_info/20_▄berma▀.md similarity index 100% rename from content/5_info/20_Übermaß.md rename to content/5_info/20_▄berma▀.md diff --git a/content/index.md b/content/index.md index 423579c..af930f9 100644 --- a/content/index.md +++ b/content/index.md @@ -1 +1,3 @@ -TYPEMILL is a small flat file cms designed for **writers**. It creates websites based on markdown files and fits perfectly for text-works like studies, manuals or documentations. TYPEMILL is simple, lightweight and open source. Just download and start. \ No newline at end of file +# Typemill + +TYPEMILL is a small flat file cms designed for **writers**. It creates websites based on markdown files and is a perfect solution for text-works like studies, manuals or documentations. TYPEMILL is simple, lightweight and open source. Just download and start. \ No newline at end of file diff --git a/settings/README.md b/settings/README.md new file mode 100644 index 0000000..3b64ca7 --- /dev/null +++ b/settings/README.md @@ -0,0 +1,10 @@ +Settings are generated during the setup of the system. + +If you are a developer, you can manually add the settings + +```` +displayErrorDetails: true +```` + +This will display the php errors during the developement process. Do not forget to disable the settings for the live environment (delete it or set it to false). + diff --git a/system/Controllers/AuthController.php b/system/Controllers/AuthController.php index 777b1ce..7468745 100644 --- a/system/Controllers/AuthController.php +++ b/system/Controllers/AuthController.php @@ -16,7 +16,7 @@ class AuthController extends Controller { if(isset($_SESSION['login'])) { - return $response->withRedirect($this->c->router->pathFor('settings.show')); + return $response->withRedirect($this->c->router->pathFor('content.show')); } else { @@ -125,7 +125,7 @@ class AuthController extends Controller $yaml->updateYaml('settings/users', '.logins', $logins); } - return $response->withRedirect($this->c->router->pathFor('settings.show')); + return $response->withRedirect($this->c->router->pathFor('content.show')); } } diff --git a/system/Controllers/ContentController.php b/system/Controllers/ContentController.php index aeb1d69..187ebdf 100644 --- a/system/Controllers/ContentController.php +++ b/system/Controllers/ContentController.php @@ -5,10 +5,16 @@ namespace Typemill\Controllers; use Slim\Views\Twig; use Slim\Http\Request; use Slim\Http\Response; +use Typemill\Models\Validation; use Typemill\Models\Folder; +use Typemill\Models\Write; use Typemill\Models\WriteYaml; +use Typemill\Models\WriteCache; use \Symfony\Component\Yaml\Yaml; use Typemill\Models\Helpers; +use Typemill\Extensions\ParsedownExtension; +use \Parsedown; + class ContentController extends Controller { @@ -51,6 +57,13 @@ class ContentController extends Controller { /* check, if there is an index-file in the root of the content folder */ $contentMD = file_exists($pathToContent . DIRECTORY_SEPARATOR . 'index.md') ? file_get_contents($pathToContent . DIRECTORY_SEPARATOR . 'index.md') : NULL; + + /* if there is content (index.md), then add a marker for frontend, so ajax calls for homepage-index-urls work */ + if($contentMD) + { + $item = new \stdClass; + $item->urlRel = 'is_homepage_index'; + } } else { @@ -95,7 +108,104 @@ class ContentController extends Controller $title = trim($contentParts[0], "# \t\n\r\0\x0B"); $content = trim($contentParts[1]); } - + return $this->render($response, 'content/content.twig', array('navigation' => $structure, 'title' => $title, 'content' => $content, 'item' => $item, 'settings' => $settings )); - } + } + + + + public function updateArticle(Request $request, Response $response, $args) + { + /* Extract the parameters from get-call */ + $params = $request->getParams(); + + /* validate input */ + $validate = new Validation(); + $vResult = $validate->editorInput($params); + + if(is_array($vResult)) + { + return $response->withJson(['errors' => $vResult], 422); + } + + /* initiate variables and objects that we need */ + $settings = $this->c->get('settings'); + $pathToContent = $settings['rootPath'] . $settings['contentFolder']; + $uri = $request->getUri(); + $base_url = $uri->getBaseUrl(); + $write = new writeCache(); + + /* we will use the cached structure to find the url for the page-update. It acts as whitelist and is more secure than a file-path, for example. */ + $structure = $write->getCache('cache', 'structure.txt'); + + /* if there is no structure, create a fresh structure */ + if(!$structure) + { + $structure = $this->getFreshStructure($pathToContent, $write, $uri); + if(!$structure) + { + return $response->withJson(['errors' => ['content folder is empty']], 404); + } + } + + /* if it is the homepage */ + if($params['url'] == 'is_homepage_index') + { + $item = new \stdClass; + $item->elementType = 'folder'; + $item->path = ''; + } + else + { + /* search for the url in the structure */ + $item = Folder::getItemForUrl($structure, $params['url']); + } + + if(!$item) + { + return $response->withJson(['errors' => ['requested page-url not found']], 404); + } + + if($item->elementType == 'folder') + { + $path = $item->path . DIRECTORY_SEPARATOR . 'index.md'; + } + elseif($item->elementType == 'file') + { + $path = $item->path; + } + + /* get the markdown file */ + $mdFile = $write->getFile($settings['contentFolder'], $path); + if($mdFile) + { + /* merge title with content for complete markdown document */ + $updatedContent = '# ' . $params['title'] . "\r\n\r\n" . $params['content']; + + /* update the file */ + $write->writeFile($settings['contentFolder'], $path, $updatedContent); + return $response->withJson(['success'], 200); + } + return $response->withJson(['errors' => ['requested markdown-file not found']], 404); + } + + protected function getFreshStructure($pathToContent, $cache, $uri) + { + /* scan the content of the folder */ + $structure = Folder::scanFolder($pathToContent); + + /* if there is no content, render an empty page */ + if(count($structure) == 0) + { + return false; + } + + /* create an array of object with the whole content of the folder */ + $structure = Folder::getFolderContentDetails($structure, $uri->getBaseUrl(), $uri->getBasePath()); + + /* cache navigation */ + $cache->updateCache('cache', 'structure.txt', 'lastCache.txt', $structure); + + return $structure; + } } \ No newline at end of file diff --git a/system/Controllers/PageController.php b/system/Controllers/PageController.php index 1b67547..0c72175 100644 --- a/system/Controllers/PageController.php +++ b/system/Controllers/PageController.php @@ -122,6 +122,7 @@ class PageController extends Controller /* initialize parsedown */ $parsedown = new ParsedownExtension(); + $parsedown->setSafeMode(true); /* parse markdown-file to content-array */ $contentArray = $parsedown->text($contentMD); diff --git a/system/Extensions/ParsedownExtension.php b/system/Extensions/ParsedownExtension.php index cacf829..c425143 100644 --- a/system/Extensions/ParsedownExtension.php +++ b/system/Extensions/ParsedownExtension.php @@ -401,7 +401,18 @@ class ParsedownExtension extends \ParsedownExtra return $Block; } } - + /* + protected function blockFencedCodeComplete($Block) + { + $text = $Block['element']['element']['text']; + unset($Block['element']['element']['text']); + + $Block['element']['element']['rawHtml'] = "

$text

"; + $Block['element']['element']['allowRawHtmlInSafeMode'] = true; + + return $Block; + } + */ # # Fenced MathJax protected function blockFencedMathJaxLaTeX($Line) @@ -449,5 +460,5 @@ class ParsedownExtension extends \ParsedownExtra $text = $Block['element']['text']; $Block['element']['text'] = "\$\$\n" . $text . "\n\$\$"; return $Block; - } + } } \ No newline at end of file diff --git a/system/Models/Validation.php b/system/Models/Validation.php index b473ca0..96b9449 100644 --- a/system/Models/Validation.php +++ b/system/Models/Validation.php @@ -63,6 +63,22 @@ class Validation } return false; }, 'contains html'); + + Validator::addRule('markdownSecure', function($field, $value, array $params, array $fields) + { + /* strip out code blocks and blockquotes */ + $value = preg_replace('/[````][\s\S]+?[````]/', '', $value); + $value = preg_replace('/[```][\s\S]+?[```]/', '', $value); + $value = preg_replace('/[``][\s\S]+?[``]/', '', $value); + $value = preg_replace('/`[\s\S]+?`/', '', $value); + $value = preg_replace('/>[\s\S]+?[\n\r]/', '', $value); + + if ( $value == strip_tags($value) ) + { + return true; + } + return false; + }, 'not secure. For code please use markdown `inline-code` or ````fenced code blocks````.'); } /** @@ -172,9 +188,35 @@ class Validation return $this->validationResult($v, $name); } + + /** + * validation for content editor + * + * @param array $params with form data. + * @return true or $v->errors with array of errors to use in json-response + */ + + public function editorInput(array $params) + { + $v = new Validator($params); + + $v->rule('required', ['title', 'content', 'url']); + $v->rule('lengthBetween', 'title', 2, 40); + $v->rule('noHTML', 'title'); + $v->rule('markdownSecure', 'content'); + + if($v->validate()) + { + return true; + } + else + { + return $v->errors(); + } + } /** - * validation for dynamic settings (themes and plugins) + * validation for dynamic fields ( settings for themes and plugins) * * @param string $fieldName with the name of the field. * @param array or string $fieldValue with the values of the field. diff --git a/system/Models/Write.php b/system/Models/Write.php index 2f5549f..e9f65ca 100644 --- a/system/Models/Write.php +++ b/system/Models/Write.php @@ -50,7 +50,7 @@ class Write return true; } - protected function writeFile($folder, $file, $data) + public function writeFile($folder, $file, $data) { if($this->checkPath($folder)) { diff --git a/system/Routes/Api.php b/system/Routes/Api.php index 6727af6..0912789 100644 --- a/system/Routes/Api.php +++ b/system/Routes/Api.php @@ -1,5 +1,10 @@ get('/api/v1/themes', SettingsController::class . ':getThemeSettings')->setName('api.themes'); \ No newline at end of file +$app->get('/api/v1/themes', SettingsController::class . ':getThemeSettings')->setName('api.themes'); +$app->put('/api/v1/article', ContentController::class . ':updateArticle')->setName('api.article.update')->add(new RedirectIfUnauthenticated($container['router'], $container['flash'])); \ No newline at end of file diff --git a/system/author/content/content.twig b/system/author/content/content.twig index 2b294fe..e6bae84 100644 --- a/system/author/content/content.twig +++ b/system/author/content/content.twig @@ -8,18 +8,26 @@
- -
- - -
-
- - +
+
+ + +
+ +
+ + + + +
+
+ +
+
- +
diff --git a/system/author/css/style.css b/system/author/css/style.css index d5b086c..5313373 100644 --- a/system/author/css/style.css +++ b/system/author/css/style.css @@ -2,7 +2,7 @@ * TRANSITION * **********************/ -a, a:link, a:visited, a:focus, a:hover, a:active, button, .button, input, .control-group, .sidebar-menu, .menu-action, .button-arrow{ +a, a:link, a:visited, a:focus, a:hover, a:active, button, .button, input, .control-group, .sidebar-menu, .sidebar-menu--content, .menu-action, .button-arrow{ -webkit-transition: all 0.2s ease; -moz-transition: all 0.2s ease; -o-transition: all 0.2s ease; @@ -22,6 +22,8 @@ body{ background: #f9f8f6; color: #444; font-size: 16px; +} +body,input,select,textarea{ font-family: Helvetica, Calibri, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; @@ -125,7 +127,7 @@ aside.sidebar{ * MENU * ************************/ -.sidebar-menu{ +.sidebar-menu,.sidebar-menu--content{ max-height: 40px; padding: 0px 20px; overflow: hidden; @@ -136,6 +138,9 @@ aside.sidebar{ .sidebar-menu.expand{ max-height: 500px; } +.sidebar-menu--content.expand{ + max-height: 3000px; +} .menu-action{ display: block; width: 100%; @@ -186,6 +191,11 @@ header input[type="submit"]{ box-sizing:border-box; border: 0px; } +h1 .version-number{ + text-transform: uppercase; + font-size: 0.5em; + font-weight: 300; +} /******************** * SETUP FORM * @@ -210,7 +220,6 @@ header input[type="submit"]{ margin-bottom: 0px; } .setupWrapper input[type="submit"]:disabled{ - pointer: cursor; cursor: default; background: #f9f8f6; border: 2px solid #f9f8f6; @@ -1047,6 +1056,92 @@ label .help, .label .help{ } + +/******************** +* EDITOR * +********************/ + +.editor .large{ + position: relative; +} +.editor input[name="title"]{ + font-size: 1.8em; + padding: 10px 20px; +} +.editor textarea{ + font-size: 1em; + padding: 20px; + line-height: 1.4em; +} +.editor span.error{ + position: absolute; + left:20px; + bottom: 0px; +} +.editor .message{ + display: inline-block; + width: 70%; +} +.editor .message span.error{ + position: relative; + left: 0; + width: 100%; + padding: 15px 20px; + color: #e0474c; +} +.editor button{ + position: relative; + border-radius: 3px; + color: #f9f8f6; + border: 2px solid #e0474c; + background: #e0474c; + padding:10px; + min-width: 200px; +} +.editor button:hover{ + border: 2px solid #cc4146; + background: #cc4146; +} +.editor button:disabled, .editor button[disabled]{ + border: 2px solid #cc4146; + background: #cc4146; + color: #eee; + cursor: default; +} +.editor button:disabled:after, +.editor button[disabled]:after, +.editor button.success:after, +.editor button.fail:after{ + position: absolute; + right: 8px; + top: 8px; + width: 8px; + height: 8px; + border-radius: 50%; + content: ''; +} +.editor button:disabled:after, +.editor button[disabled]:after{ + border: 8px solid #eee; + border-top: 8px solid #ccc; + background: #ccc; + animation: spin 2s linear infinite; +} +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} +.editor button.success:after, +.editor button.fail:after{ + border: 8px solid #eee; +} +.editor button.success:after{ + background: #00cc00; +} +.editor button.fail:after{ + background: #e0474c; +} + @media only screen and (min-width: 600px) { header.headline{ padding: 0px 20px; @@ -1102,7 +1197,7 @@ label .help, .label .help{ vertical-align: top; background: transparent; } - .sidebar-menu{ + .sidebar-menu, .sidebar-menu--content{ max-height: 2000px; padding: 0px 20px 0 0; overflow: hidden; @@ -1110,6 +1205,12 @@ label .help, .label .help{ font-size: 1em; text-align: left; } + .sidebar-menu{ + max-height: 2000px; + } + .sidebar-menu--content{ + max-height: 3000px; + } .menu-action{ display: none; width: 0px; @@ -1127,28 +1228,12 @@ label .help, .label .help{ .sidebar-menu, .sidebar-menu--content{ font-size: 0.9em; } - .sidebar-menu--content li.level-1{ + .sidebar-menu--content li.level-1, .sidebar-menu--content li.level-0 { font-weight: 700; } - - /* - .menu-item a, .menu-item a:link, .menu-item a:visited{ - background: transparent; - width: auto; - height: 0; - padding: 0px 10px; - line-height: 2px; - border-top: 8px solid transparent; - border-bottom: 8px solid transparent; - border-left: 10px solid white; - } - .menu-item a:hover, .menu-item a:focus, .menu-item a:active, .menu-item a.active{ - color: #fff; - color: #e0474c; - background: transparent; - border-left: 10px solid #e0474c; - } - */ + .menu-list.margin-bottom{ + margin-bottom: 40px; + } .menu-item a, .menu-item a:link, .menu-item a:visited{ position: relative; width: auto; diff --git a/system/author/js/auth.js b/system/author/js/auth.js index 8ebb716..b2f0469 100644 --- a/system/author/js/auth.js +++ b/system/author/js/auth.js @@ -21,10 +21,8 @@ if(wait) loginbtn.disabled = false; loginbtn.value = 'Login'; var countdown = document.getElementById("counter"); - // var flash = document.getElementById("flash-message"); countdown.parentNode.removeChild(countdown); - // flash.parentNode.removeChild(flash); clearInterval(counter); } diff --git a/system/author/js/author.js b/system/author/js/author.js index c8ee7b3..093a257 100644 --- a/system/author/js/author.js +++ b/system/author/js/author.js @@ -1,5 +1,3 @@ -(function () -{ /********************************** ** Global HttpRequest-Function ** ** for AJAX-Requests ** @@ -56,26 +54,20 @@ else { var httpRequest = prepareHttpRequest(); - httpRequest.open(getPost, url, true); + httpRequest.open(getPost, url, true); } + httpRequest.onreadystatechange = function(e) { if (this.readyState == 4) - { - if(this.status == 200) + { + if(httpRequest.response && callback) { - if(httpRequest.responseText && callback) - { - callback(httpRequest.response); - } - } - else - { - console.log('connection error, status '+this.status); - } + callback(httpRequest.response, this.status); + } } }; - + // httpRequest.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); // httpRequest.setRequestHeader('Content-Type', 'text/plain'); httpRequest.setRequestHeader('Content-Type', 'application/json'); @@ -309,5 +301,4 @@ })(thisTarget); - } -})(); \ No newline at end of file + } \ No newline at end of file diff --git a/system/author/js/vue-editor.js b/system/author/js/vue-editor.js new file mode 100644 index 0000000..24467be --- /dev/null +++ b/system/author/js/vue-editor.js @@ -0,0 +1,136 @@ +const root = document.getElementById("main").dataset.url; + +Vue.component('resizable-textarea', { + methods: { + resizeTextarea (event) { + event.target.style.height = 'auto' + event.target.style.height = (event.target.scrollHeight) + 'px' + }, + }, + mounted () { + this.$nextTick(() => { + this.$el.setAttribute('style', 'height:' + (this.$el.scrollHeight) + 'px;overflow-y:hidden;') + }) + + this.$el.addEventListener('input', this.resizeTextarea) + }, + beforeDestroy () { + this.$el.removeEventListener('input', this.resizeTextarea) + }, + render () { + return this.$slots.default[0] + }, +}); + +new Vue({ + el: '#editor', + data: { + markdown: document.getElementById("origContent").value + }, + methods: { + saveMarkdown: function(e){ + e.preventDefault(); + + e.target.disabled = true; + e.target.classList.remove("success", "fail"); + + deleteErrors(); + + var getPost = 'PUT', + url = root + '/api/v1/article', + contentData = {'url': document.getElementById("url").value, 'title': document.getElementById("title").value, 'content': document.getElementById("content").value }; + + sendJson(function(response, httpStatus) + { + if(response) + { + e.target.disabled = false; + var result = JSON.parse(response); + + if(result.errors) + { + e.target.classList.add('fail'); + processErrors(result.errors, httpStatus); + } + else + { + e.target.classList.add('success'); + } + } + else + { + e.target.disabled = false; + e.target.classList.add('fail'); + console.info('no response'); + } + }, getPost, url, contentData ); + } + } +}) + +function processErrors(errors, httpStatus) +{ + if(errors.length == 0) return; + + var message = ''; + + if(httpStatus == "404") + { + message = errors[0]; + } + + if(httpStatus == "422") + { + var fields = ''; + + for (var key in errors) + { + fields = fields + ' "' + key + '"'; + + if(key == 'url' || !errors.hasOwnProperty(key)) continue; + + + var errorMessages = errors[key], + fieldElement = document.getElementById(key), + fieldMessage = document.createElement("span"), + fieldWrapper = fieldElement.parentElement; + + fieldWrapper.classList.add("error"); + + fieldMessage.className = "error"; + fieldMessage.innerHTML = errorMessages[0]; + fieldWrapper.classList.add("error"); + fieldWrapper.appendChild(fieldMessage); + } + + message = 'Please correct the errors in these Fields: ' + fields.toUpperCase() + '. '; + } + + var messageWrapper = document.getElementById("message"), + messageSpan = document.createElement("span"); + + messageSpan.className = "error"; + messageSpan.innerHTML = message; + messageWrapper.appendChild(messageSpan); +} + +function deleteErrors() +{ + var errors = document.querySelectorAll('.error'); + + if(errors.length == 0) return; + + for(var key in errors) + { + if(!errors.hasOwnProperty(key)) continue; + + if(errors[key].tagName == "SPAN") + { + errors[key].parentElement.removeChild(errors[key]); + } + else + { + errors[key].classList.remove("error"); + } + } +} \ No newline at end of file diff --git a/system/author/js/vue.min.js b/system/author/js/vue.min.js new file mode 100644 index 0000000..37febff --- /dev/null +++ b/system/author/js/vue.min.js @@ -0,0 +1,6 @@ +/*! + * Vue.js v2.5.16 + * (c) 2014-2018 Evan You + * Released under the MIT License. + */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.Vue=t()}(this,function(){"use strict";var y=Object.freeze({});function M(e){return null==e}function D(e){return null!=e}function S(e){return!0===e}function T(e){return"string"==typeof e||"number"==typeof e||"symbol"==typeof e||"boolean"==typeof e}function P(e){return null!==e&&"object"==typeof e}var r=Object.prototype.toString;function l(e){return"[object Object]"===r.call(e)}function i(e){var t=parseFloat(String(e));return 0<=t&&Math.floor(t)===t&&isFinite(e)}function t(e){return null==e?"":"object"==typeof e?JSON.stringify(e,null,2):String(e)}function F(e){var t=parseFloat(e);return isNaN(t)?e:t}function s(e,t){for(var n=Object.create(null),r=e.split(","),i=0;ie.id;)n--;bt.splice(n+1,0,e)}else bt.push(e);Ct||(Ct=!0,Ze(At))}}(this)},St.prototype.run=function(){if(this.active){var e=this.get();if(e!==this.value||P(e)||this.deep){var t=this.value;if(this.value=e,this.user)try{this.cb.call(this.vm,e,t)}catch(e){Fe(e,this.vm,'callback for watcher "'+this.expression+'"')}else this.cb.call(this.vm,e,t)}}},St.prototype.evaluate=function(){this.value=this.get(),this.dirty=!1},St.prototype.depend=function(){for(var e=this.deps.length;e--;)this.deps[e].depend()},St.prototype.teardown=function(){if(this.active){this.vm._isBeingDestroyed||f(this.vm._watchers,this);for(var e=this.deps.length;e--;)this.deps[e].removeSub(this);this.active=!1}};var Tt={enumerable:!0,configurable:!0,get:$,set:$};function Et(e,t,n){Tt.get=function(){return this[t][n]},Tt.set=function(e){this[t][n]=e},Object.defineProperty(e,n,Tt)}function jt(e){e._watchers=[];var t=e.$options;t.props&&function(n,r){var i=n.$options.propsData||{},o=n._props={},a=n.$options._propKeys=[];n.$parent&&ge(!1);var e=function(e){a.push(e);var t=Ie(e,r,i,n);Ce(o,e,t),e in n||Et(n,"_props",e)};for(var t in r)e(t);ge(!0)}(e,t.props),t.methods&&function(e,t){e.$options.props;for(var n in t)e[n]=null==t[n]?$:v(t[n],e)}(e,t.methods),t.data?function(e){var t=e.$options.data;l(t=e._data="function"==typeof t?function(e,t){se();try{return e.call(t,t)}catch(e){return Fe(e,t,"data()"),{}}finally{ce()}}(t,e):t||{})||(t={});var n=Object.keys(t),r=e.$options.props,i=(e.$options.methods,n.length);for(;i--;){var o=n[i];r&&p(r,o)||(void 0,36!==(a=(o+"").charCodeAt(0))&&95!==a&&Et(e,"_data",o))}var a;we(t,!0)}(e):we(e._data={},!0),t.computed&&function(e,t){var n=e._computedWatchers=Object.create(null),r=Y();for(var i in t){var o=t[i],a="function"==typeof o?o:o.get;r||(n[i]=new St(e,a||$,$,Nt)),i in e||Lt(e,i,o)}}(e,t.computed),t.watch&&t.watch!==G&&function(e,t){for(var n in t){var r=t[n];if(Array.isArray(r))for(var i=0;iparseInt(this.max)&&bn(a,s[0],s,this._vnode)),t.data.keepAlive=!0}return t||e&&e[0]}}};$n=hn,Cn={get:function(){return j}},Object.defineProperty($n,"config",Cn),$n.util={warn:re,extend:m,mergeOptions:Ne,defineReactive:Ce},$n.set=xe,$n.delete=ke,$n.nextTick=Ze,$n.options=Object.create(null),k.forEach(function(e){$n.options[e+"s"]=Object.create(null)}),m(($n.options._base=$n).options.components,kn),$n.use=function(e){var t=this._installedPlugins||(this._installedPlugins=[]);if(-1=a&&l()};setTimeout(function(){c\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/,oo="[a-zA-Z_][\\w\\-\\.]*",ao="((?:"+oo+"\\:)?"+oo+")",so=new RegExp("^<"+ao),co=/^\s*(\/?)>/,lo=new RegExp("^<\\/"+ao+"[^>]*>"),uo=/^]+>/i,fo=/^",""":'"',"&":"&"," ":"\n"," ":"\t"},go=/&(?:lt|gt|quot|amp);/g,_o=/&(?:lt|gt|quot|amp|#10|#9);/g,bo=s("pre,textarea",!0),$o=function(e,t){return e&&bo(e)&&"\n"===t[0]};var wo,Co,xo,ko,Ao,Oo,So,To,Eo=/^@|^v-on:/,jo=/^v-|^@|^:/,No=/([^]*?)\s+(?:in|of)\s+([^]*)/,Lo=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/,Io=/^\(|\)$/g,Mo=/:(.*)$/,Do=/^:|^v-bind:/,Po=/\.[^.]+/g,Fo=e(eo);function Ro(e,t,n){return{type:1,tag:e,attrsList:t,attrsMap:function(e){for(var t={},n=0,r=e.length;n]*>)","i")),n=i.replace(t,function(e,t,n){return r=n.length,ho(o)||"noscript"===o||(t=t.replace(//g,"$1").replace(//g,"$1")),$o(o,t)&&(t=t.slice(1)),d.chars&&d.chars(t),""});a+=i.length-n.length,i=n,A(o,a-r,a)}else{var s=i.indexOf("<");if(0===s){if(fo.test(i)){var c=i.indexOf("--\x3e");if(0<=c){d.shouldKeepComment&&d.comment(i.substring(4,c)),C(c+3);continue}}if(po.test(i)){var l=i.indexOf("]>");if(0<=l){C(l+2);continue}}var u=i.match(uo);if(u){C(u[0].length);continue}var f=i.match(lo);if(f){var p=a;C(f[0].length),A(f[1],p,a);continue}var _=x();if(_){k(_),$o(v,i)&&C(1);continue}}var b=void 0,$=void 0,w=void 0;if(0<=s){for($=i.slice(s);!(lo.test($)||so.test($)||fo.test($)||po.test($)||(w=$.indexOf("<",1))<0);)s+=w,$=i.slice(s);b=i.substring(0,s),C(s)}s<0&&(b=i,i=""),d.chars&&b&&d.chars(b)}if(i===e){d.chars&&d.chars(i);break}}function C(e){a+=e,i=i.substring(e)}function x(){var e=i.match(so);if(e){var t,n,r={tagName:e[1],attrs:[],start:a};for(C(e[0].length);!(t=i.match(co))&&(n=i.match(io));)C(n[0].length),r.attrs.push(n);if(t)return r.unarySlash=t[1],C(t[0].length),r.end=a,r}}function k(e){var t=e.tagName,n=e.unarySlash;m&&("p"===v&&ro(t)&&A(v),g(t)&&v===t&&A(t));for(var r,i,o,a=y(t)||!!n,s=e.attrs.length,c=new Array(s),l=0;l-1"+("true"===d?":("+l+")":":_q("+l+","+d+")")),Ar(c,"change","var $$a="+l+",$$el=$event.target,$$c=$$el.checked?("+d+"):("+v+");if(Array.isArray($$a)){var $$v="+(f?"_n("+p+")":p)+",$$i=_i($$a,$$v);if($$el.checked){$$i<0&&("+Er(l,"$$a.concat([$$v])")+")}else{$$i>-1&&("+Er(l,"$$a.slice(0,$$i).concat($$a.slice($$i+1))")+")}}else{"+Er(l,"$$c")+"}",null,!0);else if("input"===$&&"radio"===w)r=e,i=_,a=(o=b)&&o.number,s=Or(r,"value")||"null",Cr(r,"checked","_q("+i+","+(s=a?"_n("+s+")":s)+")"),Ar(r,"change",Er(i,s),null,!0);else if("input"===$||"textarea"===$)!function(e,t,n){var r=e.attrsMap.type,i=n||{},o=i.lazy,a=i.number,s=i.trim,c=!o&&"range"!==r,l=o?"change":"range"===r?Pr:"input",u="$event.target.value";s&&(u="$event.target.value.trim()"),a&&(u="_n("+u+")");var f=Er(t,u);c&&(f="if($event.target.composing)return;"+f),Cr(e,"value","("+t+")"),Ar(e,l,f,null,!0),(s||a)&&Ar(e,"blur","$forceUpdate()")}(e,_,b);else if(!j.isReservedTag($))return Tr(e,_,b),!1;return!0},text:function(e,t){t.value&&Cr(e,"textContent","_s("+t.value+")")},html:function(e,t){t.value&&Cr(e,"innerHTML","_s("+t.value+")")}},isPreTag:function(e){return"pre"===e},isUnaryTag:to,mustUseProp:Sn,canBeLeftOpenTag:no,isReservedTag:Un,getTagNamespace:Vn,staticKeys:(Go=Wo,Go.reduce(function(e,t){return e.concat(t.staticKeys||[])},[]).join(","))},Qo=e(function(e){return s("type,tag,attrsList,attrsMap,plain,parent,children,attrs"+(e?","+e:""))});function ea(e,t){e&&(Zo=Qo(t.staticKeys||""),Xo=t.isReservedTag||O,function e(t){t.static=function(e){if(2===e.type)return!1;if(3===e.type)return!0;return!(!e.pre&&(e.hasBindings||e.if||e.for||c(e.tag)||!Xo(e.tag)||function(e){for(;e.parent;){if("template"!==(e=e.parent).tag)return!1;if(e.for)return!0}return!1}(e)||!Object.keys(e).every(Zo)))}(t);if(1===t.type){if(!Xo(t.tag)&&"slot"!==t.tag&&null==t.attrsMap["inline-template"])return;for(var n=0,r=t.children.length;n|^function\s*\(/,na=/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*$/,ra={esc:27,tab:9,enter:13,space:32,up:38,left:37,right:39,down:40,delete:[8,46]},ia={esc:"Escape",tab:"Tab",enter:"Enter",space:" ",up:["Up","ArrowUp"],left:["Left","ArrowLeft"],right:["Right","ArrowRight"],down:["Down","ArrowDown"],delete:["Backspace","Delete"]},oa=function(e){return"if("+e+")return null;"},aa={stop:"$event.stopPropagation();",prevent:"$event.preventDefault();",self:oa("$event.target !== $event.currentTarget"),ctrl:oa("!$event.ctrlKey"),shift:oa("!$event.shiftKey"),alt:oa("!$event.altKey"),meta:oa("!$event.metaKey"),left:oa("'button' in $event && $event.button !== 0"),middle:oa("'button' in $event && $event.button !== 1"),right:oa("'button' in $event && $event.button !== 2")};function sa(e,t,n){var r=t?"nativeOn:{":"on:{";for(var i in e)r+='"'+i+'":'+ca(i,e[i])+",";return r.slice(0,-1)+"}"}function ca(t,e){if(!e)return"function(){}";if(Array.isArray(e))return"["+e.map(function(e){return ca(t,e)}).join(",")+"]";var n=na.test(e.value),r=ta.test(e.value);if(e.modifiers){var i="",o="",a=[];for(var s in e.modifiers)if(aa[s])o+=aa[s],ra[s]&&a.push(s);else if("exact"===s){var c=e.modifiers;o+=oa(["ctrl","shift","alt","meta"].filter(function(e){return!c[e]}).map(function(e){return"$event."+e+"Key"}).join("||"))}else a.push(s);return a.length&&(i+="if(!('button' in $event)&&"+a.map(la).join("&&")+")return null;"),o&&(i+=o),"function($event){"+i+(n?"return "+e.value+"($event)":r?"return ("+e.value+")($event)":e.value)+"}"}return n||r?e.value:"function($event){"+e.value+"}"}function la(e){var t=parseInt(e,10);if(t)return"$event.keyCode!=="+t;var n=ra[e],r=ia[e];return"_k($event.keyCode,"+JSON.stringify(e)+","+JSON.stringify(n)+",$event.key,"+JSON.stringify(r)+")"}var ua={on:function(e,t){e.wrapListeners=function(e){return"_g("+e+","+t.value+")"}},bind:function(t,n){t.wrapData=function(e){return"_b("+e+",'"+t.tag+"',"+n.value+","+(n.modifiers&&n.modifiers.prop?"true":"false")+(n.modifiers&&n.modifiers.sync?",true":"")+")"}},cloak:$},fa=function(e){this.options=e,this.warn=e.warn||$r,this.transforms=wr(e.modules,"transformCode"),this.dataGenFns=wr(e.modules,"genData"),this.directives=m(m({},ua),e.directives);var t=e.isReservedTag||O;this.maybeComponent=function(e){return!t(e.tag)},this.onceId=0,this.staticRenderFns=[]};function pa(e,t){var n=new fa(t);return{render:"with(this){return "+(e?da(e,n):'_c("div")')+"}",staticRenderFns:n.staticRenderFns}}function da(e,t){if(e.staticRoot&&!e.staticProcessed)return va(e,t);if(e.once&&!e.onceProcessed)return ha(e,t);if(e.for&&!e.forProcessed)return f=t,v=(u=e).for,h=u.alias,m=u.iterator1?","+u.iterator1:"",y=u.iterator2?","+u.iterator2:"",u.forProcessed=!0,(d||"_l")+"(("+v+"),function("+h+m+y+"){return "+(p||da)(u,f)+"})";if(e.if&&!e.ifProcessed)return ma(e,t);if("template"!==e.tag||e.slotTarget){if("slot"===e.tag)return function(e,t){var n=e.slotName||'"default"',r=_a(e,t),i="_t("+n+(r?","+r:""),o=e.attrs&&"{"+e.attrs.map(function(e){return g(e.name)+":"+e.value}).join(",")+"}",a=e.attrsMap["v-bind"];!o&&!a||r||(i+=",null");o&&(i+=","+o);a&&(i+=(o?"":",null")+","+a);return i+")"}(e,t);var n;if(e.component)a=e.component,c=t,l=(s=e).inlineTemplate?null:_a(s,c,!0),n="_c("+a+","+ya(s,c)+(l?","+l:"")+")";else{var r=e.plain?void 0:ya(e,t),i=e.inlineTemplate?null:_a(e,t,!0);n="_c('"+e.tag+"'"+(r?","+r:"")+(i?","+i:"")+")"}for(var o=0;o':'
',0 {% include 'partials/flash.twig' %} -
+
diff --git a/system/author/layouts/layoutContent.twig b/system/author/layouts/layoutContent.twig index f5a471c..65e2a0b 100644 --- a/system/author/layouts/layoutContent.twig +++ b/system/author/layouts/layoutContent.twig @@ -25,7 +25,7 @@ {% include 'partials/navi.twig' %} {% include 'partials/flash.twig' %} -
+
@@ -34,6 +34,8 @@
+ + \ No newline at end of file diff --git a/system/author/partials/aside.twig b/system/author/partials/aside.twig index 787af2d..c4ee2bf 100644 --- a/system/author/partials/aside.twig +++ b/system/author/partials/aside.twig @@ -2,7 +2,7 @@ {% if is_role('administrator') %}

Settings

-
- +
Startpage