Przeglądaj źródła

Version 1.2.1 Editor Improvement

Sebastian 7 lat temu
rodzic
commit
1aa20023d0

+ 1 - 1
cache/lastCache.txt

@@ -1 +1 @@
-1530040430
+1530865190

+ 1 - 1
content/0_typemill/03-features.md

@@ -1,4 +1,4 @@
-# Features
+# Features qwer
 
 
 TYPEMILL has a limited set of features right now. It transforms a bunch of **markdown files** into a **website** and generates a list of contents for **navigation**. 
 TYPEMILL has a limited set of features right now. It transforms a bunch of **markdown files** into a **website** and generates a list of contents for **navigation**. 
 
 

+ 2 - 2
content/0_typemill/index.md

@@ -1,6 +1,6 @@
-# About TYPEMILL
+# 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 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. 
 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. 
 
 

+ 51 - 3
content/5_info/15_markdown-test.md

@@ -51,6 +51,20 @@ A paragraph is a simple text-block separated with a new line above and below.
 
 
 A paragraph is a simple text-block separated with a new line above and below.
 A paragraph is a simple text-block separated with a new line above and below.
 
 
+## Soft Linebreak
+
+````
+For a soft linebreak (eg. for dialoges in literature), add two spaces at the end of a line and use a simple return.
+
+She said: "Hello"  
+He said: "again"
+````
+
+For a soft linebreak (eg. for dialoges in literature), add two spaces at the end of a line and use a simple return.
+
+She said: "Hello"  
+He said: "again"
+
 ##Emphasis
 ##Emphasis
 
 
 ````
 ````
@@ -137,11 +151,11 @@ Or you can use a shortcut like http://typemill.net.
 ```
 ```
 The same rules as with links, but with a !
 The same rules as with links, but with a !
 
 
-![alt-text](/info/markdown.png)
+![alt-text](/media/markdown.png)
 
 
-![alt-text](/info/markdown.png "my title")
+![alt-text](/media/markdown.png "my title")
 
 
-![alt-text](/info/markdown.png "my title"){#myid .myclass}
+![alt-text](/media/markdown.png "my title"){#myid .myclass}
 ```
 ```
 
 
 The same rules as with links, but with a !
 The same rules as with links, but with a !
@@ -152,6 +166,40 @@ The same rules as with links, but with a !
 
 
 ![alt-text](/media/markdown.png "my title"){#myid .imgClass .myClass}
 ![alt-text](/media/markdown.png "my title"){#myid .imgClass .myClass}
 
 
+## Linked Images
+
+````
+You can link an image with a nested syntax like this:
+
+[![alt-text](/media/markdown.png)](https://typemill.net)
+````
+
+You can link an image with a nested syntax like this:
+
+[![alt-text](/media/markdown.png)](https://typemill.net)
+
+## Image Position
+
+````
+You can controll the image position with the classes .left, .right and .middle like this:
+
+![alt-text](/media/markdown.png){.left}
+![alt-text](/media/markdown.png){.right}
+![alt-text](/media/markdown.png){.middle}
+````
+
+![image float left](/media/markdown.png){.left}
+
+The first image should float on the left side of this paragraph. This might not work with all themes. If you are a theme developer, please ensure that you support the image classes "left", "right" and "middle".
+
+![image float right](/media/markdown.png){.right}
+
+The second image should float on the right side of this paragraph. This might not work with all themes. If you are a theme developer, please ensure that you support the image classes "left", "right" and "middle".
+
+![image middle](/media/markdown.png){.middle}
+
+The thirds image should be placed above this paragraph and centered to the middle of the content area. This might not work with all themes. If you are a theme developer, please ensure that you support the image classes "left", "right" and "middle".
+
 ## Blockquote
 ## Blockquote
 
 
 ```
 ```

+ 1 - 1
content/index.md

@@ -1,3 +1,3 @@
 # Typemill
 # 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. 
+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.

+ 23 - 6
readme.md

@@ -15,7 +15,7 @@ TYPEMILL is a small flat file cms designed for writers. It creates websites base
 * Supports configurable themes and plugins.
 * Supports configurable themes and plugins.
 * Provides an author panel to configure the system, the themes and the plugins.
 * Provides an author panel to configure the system, the themes and the plugins.
 * Creates and manages users.
 * Creates and manages users.
-* Online editing is on its way (for time beeing upload markdown files).
+* Provides a basic online editing (only for existing files so far, in development).
 * Markdown supports table of contents (TOC), tables, footnotes, abbreviations and definition lists.
 * Markdown supports table of contents (TOC), tables, footnotes, abbreviations and definition lists.
 * Supports MathJax and KaTeX (plugin).
 * Supports MathJax and KaTeX (plugin).
 * Supports code highlighting (plugin).
 * Supports code highlighting (plugin).
@@ -24,7 +24,9 @@ TYPEMILL is a small flat file cms designed for writers. It creates websites base
 
 
 ## Installation
 ## Installation
 
 
-Download TYPEMILL from the [TYPEMILL website](http://typemill.net) or clone this repository with git. Open your git command line (e.g. gitbash), go to your project folder (e.g. htdocs) and type:
+Download TYPEMILL from the [TYPEMILL website](http://typemill.net), unzip the files and you are done.
+
+If you are a developer, you can also clone this repository. To do so, open your git command line (e.g. gitbash), go to your project folder (e.g. htdocs) and type:
 
 
     git clone git://github.com/trendschau/typemill.git
     git clone git://github.com/trendschau/typemill.git
 
 
@@ -33,7 +35,7 @@ The GitHub-version has no vendor-folder, so you have to update and include all l
     composer update
     composer update
 If you did not use composer before, please go to the [composer website](http://getcomposer.org) and start to learn.
 If you did not use composer before, please go to the [composer website](http://getcomposer.org) and start to learn.
 
 
-To run TYPEMILL **live**, simply upload the files to your server.
+To run TYPEMILL on a **live** system, simply upload the files to your server.
 
 
 ## Setup
 ## Setup
 
 
@@ -41,7 +43,7 @@ Please go to `your-typemill-website.com/setup`, create an initial user and then
 
 
 ## Login
 ## Login
 
 
-You can find your login screen under `/tm-author/login` or simply go to `/setup` and you will be redirected to the login-page. 
+You can find your login screen under `/tm/login` or simply go to `/setup` and you will be redirected to the login-page. 
 
 
 ## Requirements
 ## Requirements
 
 
@@ -53,10 +55,25 @@ You can read the full documentation for writers, for theme developers and for pl
 
 
 ## Contribute
 ## Contribute
 
 
-If you want to contribute to TYPEMILL, please fork this GitHub repository first. Then make your changes and create a pull request. I will review all request as soon as possible.
+Typemill is still in an early stage and contributions are highly welcome. Here are some ideas for non-coder:
+
+* Find bugs and errors (open a new issue on github for it).
+* Improve the documentation.
+* Describe some missing features and explain, why they are important for other users.
+
+Some ideas for devs (please fork this repository make your changes and create a pull request):
+
+* Fix a bug.
+* Create a nice theme.
+* Create a new plugin.
+* Improve the CSS-code with BEM and make it modular.
+* Rebuild the theme with css-grid.
+* Improve accessibility of html and css.
+* Help to establish autotests with selenium or cypress.
+* Write unit-tests.
 
 
 For hints, questions, problems and support, please open up a new issue on GitHub.
 For hints, questions, problems and support, please open up a new issue on GitHub.
 
 
 ## Licence
 ## Licence
 
 
-TYPEMILL is published under MIT licence.
+TYPEMILL is published under MIT licence. Please check the licence of the included libraries, too.

+ 13 - 10
system/Controllers/ContentController.php

@@ -29,7 +29,6 @@ class ContentController extends Controller
 	
 	
 	public function showContent(Request $request, Response $response, $args)
 	public function showContent(Request $request, Response $response, $args)
 	{
 	{
-
 		$settings		= $this->c->get('settings');
 		$settings		= $this->c->get('settings');
 		$pathToContent	= $settings['rootPath'] . $settings['contentFolder'];
 		$pathToContent	= $settings['rootPath'] . $settings['contentFolder'];
 		$uri 			= $request->getUri();
 		$uri 			= $request->getUri();
@@ -115,8 +114,6 @@ class ContentController extends Controller
 		return $this->render($response, 'content/content.twig', array('navigation' => $structure, 'title' => $title, 'content' => $content, 'item' => $item, 'settings' => $settings ));
 		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)
 	public function updateArticle(Request $request, Response $response, $args)
 	{
 	{
 		/* Extract the parameters from get-call */
 		/* Extract the parameters from get-call */
@@ -147,7 +144,7 @@ class ContentController extends Controller
 			$structure 	= $this->getFreshStructure($pathToContent, $write, $uri);
 			$structure 	= $this->getFreshStructure($pathToContent, $write, $uri);
 			if(!$structure)
 			if(!$structure)
 			{
 			{
-				return $response->withJson(['errors' => ['content folder is empty']], 404);
+				return $response->withJson(['errors' => ['message' => 'content folder is empty']], 404);
 			}
 			}
 		}
 		}
 		
 		
@@ -163,10 +160,10 @@ class ContentController extends Controller
 			/* search for the url in the structure */
 			/* search for the url in the structure */
 			$item = Folder::getItemForUrl($structure, $params['url']);			
 			$item = Folder::getItemForUrl($structure, $params['url']);			
 		}
 		}
-				
+
 		if(!$item)
 		if(!$item)
 		{
 		{
-			return $response->withJson(['errors' => ['requested page-url not found']], 404);
+			return $response->withJson(['errors' => ['message' => 'requested page-url not found']], 404);
 		}
 		}
 				
 				
 		if($item->elementType == 'folder')
 		if($item->elementType == 'folder')
@@ -182,14 +179,20 @@ class ContentController extends Controller
 		$mdFile	= $write->getFile($settings['contentFolder'], $path);		
 		$mdFile	= $write->getFile($settings['contentFolder'], $path);		
 		if($mdFile)
 		if($mdFile)
 		{
 		{
-			/* merge title with content for complete markdown document */
+			/* merge title with content forcomplete markdown document */
 			$updatedContent = '# ' . $params['title'] . "\r\n\r\n" . $params['content'];
 			$updatedContent = '# ' . $params['title'] . "\r\n\r\n" . $params['content'];
 			
 			
 			/* update the file */
 			/* update the file */
-			$write->writeFile($settings['contentFolder'], $path, $updatedContent);
-			return $response->withJson(['success'], 200);
+			if($write->writeFile($settings['contentFolder'], $path, $updatedContent))
+			{
+				return $response->withJson(['success'], 200);
+			}
+			else
+			{
+				return $response->withJson(['errors' => ['message' => 'Could not write to file. Please check if file is writable']], 404);
+			}
 		}
 		}
-		return $response->withJson(['errors' => ['requested markdown-file not found']], 404);
+		return $response->withJson(['errors' => ['message' => 'requested markdown-file not found']], 404);
 	}
 	}
 	
 	
 	protected function getFreshStructure($pathToContent, $cache, $uri)
 	protected function getFreshStructure($pathToContent, $cache, $uri)

+ 4 - 2
system/Controllers/PageController.php

@@ -119,15 +119,17 @@ class PageController extends Controller
 		}
 		}
 		
 		
 		$contentMD = $this->c->dispatcher->dispatch('onMarkdownLoaded', new OnMarkdownLoaded($contentMD))->getData();
 		$contentMD = $this->c->dispatcher->dispatch('onMarkdownLoaded', new OnMarkdownLoaded($contentMD))->getData();
-				
+		
 		/* initialize parsedown */
 		/* initialize parsedown */
 		$parsedown 		= new ParsedownExtension();
 		$parsedown 		= new ParsedownExtension();
+		
+		/* set safe mode to escape javascript and html in markdown */
 		$parsedown->setSafeMode(true);
 		$parsedown->setSafeMode(true);
 
 
 		/* parse markdown-file to content-array */
 		/* parse markdown-file to content-array */
 		$contentArray 	= $parsedown->text($contentMD);
 		$contentArray 	= $parsedown->text($contentMD);
 		$contentArray 	= $this->c->dispatcher->dispatch('onContentArrayLoaded', new OnContentArrayLoaded($contentArray))->getData();
 		$contentArray 	= $this->c->dispatcher->dispatch('onContentArrayLoaded', new OnContentArrayLoaded($contentArray))->getData();
-	
+
 		/* get the first image from content array */
 		/* get the first image from content array */
 		$firstImage		= $this->getFirstImage($contentArray);
 		$firstImage		= $this->getFirstImage($contentArray);
 		
 		

+ 5 - 5
system/Controllers/SettingsController.php

@@ -40,11 +40,11 @@ class SettingsController extends Controller
 			{
 			{
 				/* make sure only allowed fields are stored */
 				/* make sure only allowed fields are stored */
 				$newSettings = array(
 				$newSettings = array(
-					'title' 	=> $newSettings['title'],
-					'author' 	=> $newSettings['author'],
-					'copyright' => $newSettings['copyright'],
-					'year'		=> $newSettings['year'],
-					'statpage' 	=> isset($newSettings['startpage']) ? true : false
+					'title' 		=> $newSettings['title'],
+					'author' 		=> $newSettings['author'],
+					'copyright' 	=> $newSettings['copyright'],
+					'year'			=> $newSettings['year'],
+					'startpage' 	=> isset($newSettings['startpage']) ? true : false
 				);
 				);
 				
 				
 				$copyright 					= $this->getCopyright();
 				$copyright 					= $this->getCopyright();

+ 18 - 213
system/Extensions/ParsedownExtension.php

@@ -18,34 +18,25 @@ class ParsedownExtension extends \ParsedownExtra
         array_unshift($this->BlockTypes['['], 'TableOfContents');
         array_unshift($this->BlockTypes['['], 'TableOfContents');
     }
     }
 	
 	
-    function text($text)
-    {
-        # make sure no definitions are set
-        $this->DefinitionData = array();
-
-        # standardize line breaks
-        $text = str_replace(array("\r\n", "\r"), "\n", $text);
-
-        # remove surrounding line breaks
-        $text = trim($text, "\n");
-
-        # split text into lines
-        $lines = explode("\n", $text);
-
-        # iterate through lines to identify blocks and return array of content
-        $blocks = $this->getContentArray($lines);
+	function text($text)
+	{
+        $Elements = $this->textElements($text);
 		
 		
-		return $blocks;
+		return $Elements;
 	}
 	}
 	
 	
-	function markup($blocks)
-	{
-		# iterate through array of content and get markup
-		$markup = $this->getMarkup($blocks);
-		
+	function markup($Elements)
+	{	
+        # convert to markup
+        $markup = $this->elements($Elements);
+				
         # trim line breaks
         # trim line breaks
         $markup = trim($markup, "\n");
         $markup = trim($markup, "\n");
 
 
+        # merge consecutive dl elements
+        $markup = preg_replace('/<\/dl>\s+<dl>\s+/', '', $markup);
+
+		# create table of contents
         if(isset($this->DefinitionData['TableOfContents']))
         if(isset($this->DefinitionData['TableOfContents']))
         {
         {
 			$TOC = $this->buildTOC($this->headlines);
 			$TOC = $this->buildTOC($this->headlines);
@@ -53,9 +44,6 @@ class ParsedownExtension extends \ParsedownExtra
 			$markup = preg_replace('%(<p[^>]*>\[TOC\]</p>)%i', $TOC, $markup);
 			$markup = preg_replace('%(<p[^>]*>\[TOC\]</p>)%i', $TOC, $markup);
         }
         }
 		
 		
-        # merge consecutive dl elements
-        $markup = preg_replace('/<\/dl>\s+<dl>\s+/', '', $markup);
-
         # add footnotes
         # add footnotes
         if (isset($this->DefinitionData['Footnote']))
         if (isset($this->DefinitionData['Footnote']))
         {
         {
@@ -63,10 +51,10 @@ class ParsedownExtension extends \ParsedownExtra
 
 
             $markup .= "\n" . $this->element($Element);
             $markup .= "\n" . $this->element($Element);
         }
         }
-			
-        return $markup;
-    }
 		
 		
+		return $markup;
+	}
+			
     # TableOfContents
     # TableOfContents
 
 
     protected function blockTableOfContents($line, $block)
     protected function blockTableOfContents($line, $block)
@@ -157,181 +145,8 @@ class ParsedownExtension extends \ParsedownExtra
 		return $markup;
 		return $markup;
 	}
 	}
 	
 	
-	
-    #
-    # Blocks
-    #
-
-    protected function getContentArray(array $lines)
-    {
-        $CurrentBlock = null;
-
-        foreach ($lines as $line)
-        {
-            if (chop($line) === '')
-            {
-                if (isset($CurrentBlock))
-                {
-                    $CurrentBlock['interrupted'] = true;
-                }
-
-                continue;
-            }
-
-            if (strpos($line, "\t") !== false)
-            {
-                $parts = explode("\t", $line);
-
-                $line = $parts[0];
-
-                unset($parts[0]);
-
-                foreach ($parts as $part)
-                {
-                    $shortage = 4 - mb_strlen($line, 'utf-8') % 4;
-
-                    $line .= str_repeat(' ', $shortage);
-                    $line .= $part;
-                }
-            }
-
-            $indent = 0;
-
-            while (isset($line[$indent]) and $line[$indent] === ' ')
-            {
-                $indent ++;
-            }
-
-            $text = $indent > 0 ? substr($line, $indent) : $line;
-
-            # ~
-
-            $Line = array('body' => $line, 'indent' => $indent, 'text' => $text);
-
-            # ~
-
-            if (isset($CurrentBlock['continuable']))
-            {
-                $Block = $this->{'block'.$CurrentBlock['type'].'Continue'}($Line, $CurrentBlock);
-
-                if (isset($Block))
-                {
-                    $CurrentBlock = $Block;
-
-                    continue;
-                }
-                else
-                {
-                    if ($this->isBlockCompletable($CurrentBlock['type']))
-                    {
-                        $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
-                    }
-                }
-            }
-
-            # ~
-
-            $marker = $text[0];
-
-            # ~
-
-            $blockTypes = $this->unmarkedBlockTypes;
-
-            if (isset($this->BlockTypes[$marker]))
-            {
-                foreach ($this->BlockTypes[$marker] as $blockType)
-                {
-                    $blockTypes []= $blockType;
-                }
-            }
-
-            #
-            # ~
-
-            foreach ($blockTypes as $blockType)
-            {
-                $Block = $this->{'block'.$blockType}($Line, $CurrentBlock);
-
-                if (isset($Block))
-                {
-                    $Block['type'] = $blockType;
-
-                    if ( ! isset($Block['identified']))
-                    {
-                        $Blocks []= $CurrentBlock;
-
-                        $Block['identified'] = true;
-                    }
-
-                    if ($this->isBlockContinuable($blockType))
-                    {
-                        $Block['continuable'] = true;
-                    }
-
-                    $CurrentBlock = $Block;
-
-                    continue 2;
-                }
-            }
-
-            # ~
-
-            if (isset($CurrentBlock) and ! isset($CurrentBlock['type']) and ! isset($CurrentBlock['interrupted']))
-            {
-                $CurrentBlock['element']['text'] .= "\n".$text;
-            }
-            else
-            {
-                $Blocks []= $CurrentBlock;
-
-                $CurrentBlock = $this->paragraph($Line);
-
-                $CurrentBlock['identified'] = true;
-            }
-        }
-
-        # ~
-
-        if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type']))
-        {
-            $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
-        }
-
-        # ~
-
-        $Blocks []= $CurrentBlock;
-
-        unset($Blocks[0]);
-
-        # ~
-		return $Blocks;
-	}
-	
-	public function getMarkup($Blocks)
-	{
-        $markup = '';
-
-        foreach ($Blocks as $Block)
-        {
-            if (isset($Block['hidden']))
-            {
-                continue;
-            }
-
-            $markup .= "\n";
-            $markup .= isset($Block['markup']) ? $Block['markup'] : $this->element($Block['element']);
-        }
-
-        $markup .= "\n";
-
-        # ~
-
-        return $markup;
-    }
-	
 	# math support. Check https://github.com/aidantwoods/parsedown/blob/mathjaxlatex/ParsedownExtensionMathJaxLaTeX.php
 	# math support. Check https://github.com/aidantwoods/parsedown/blob/mathjaxlatex/ParsedownExtensionMathJaxLaTeX.php
 
 
-
     protected function inlineCode($Excerpt)
     protected function inlineCode($Excerpt)
     {
     {
         $marker = $Excerpt['text'][0];
         $marker = $Excerpt['text'][0];
@@ -401,18 +216,7 @@ class ParsedownExtension extends \ParsedownExtra
             return $Block;
             return $Block;
         }
         }
     }
     }
-	/*
-	protected function blockFencedCodeComplete($Block)
-	{	
-		$text = $Block['element']['element']['text'];
-		unset($Block['element']['element']['text']);
-
-		$Block['element']['element']['rawHtml'] = "<p>$text</p>";
-		$Block['element']['element']['allowRawHtmlInSafeMode'] = true;
 
 
-		return $Block;
-	}	
-	*/
     #
     #
     # Fenced MathJax
     # Fenced MathJax
     protected function blockFencedMathJaxLaTeX($Line)
     protected function blockFencedMathJaxLaTeX($Line)
@@ -436,7 +240,8 @@ class ParsedownExtension extends \ParsedownExtra
 	
 	
     protected function blockFencedMathJaxLaTeXContinue($Line, $Block)
     protected function blockFencedMathJaxLaTeXContinue($Line, $Block)
     {
     {
-        if (isset($Block['complete']))
+
+		if (isset($Block['complete']))
         {
         {
             return;
             return;
         }
         }

+ 1 - 1
system/Middleware/RedirectIfAuthenticated.php

@@ -19,7 +19,7 @@ class RedirectIfAuthenticated
 	{
 	{
 		if(isset($_SESSION['login']))
 		if(isset($_SESSION['login']))
 		{
 		{
-			$response = $response->withRedirect($this->router->pathFor('settings.show'));
+			$response = $response->withRedirect($this->router->pathFor('content.show'));
 		}
 		}
 		
 		
 		return $next($request, $response);
 		return $next($request, $response);

+ 2 - 2
system/Middleware/RedirectIfUnauthenticated.php

@@ -16,10 +16,10 @@ class RedirectIfUnauthenticated
 	}
 	}
 
 
 	public function __invoke(Request $request, Response $response, $next)
 	public function __invoke(Request $request, Response $response, $next)
-	{
+	{		
 		if(!isset($_SESSION['login']))
 		if(!isset($_SESSION['login']))
 		{
 		{
-			$response = $response->withRedirect($this->router->pathFor('auth.show'));
+			return $response->withRedirect($this->router->pathFor('auth.show'));
 		}
 		}
 				
 				
 		return $next($request, $response);
 		return $next($request, $response);

+ 26 - 0
system/Middleware/RestrictApiAccess.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace Typemill\Middleware;
+
+use Slim\Interfaces\RouterInterface;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class RestrictApiAccess
+{
+	protected $router;
+	
+	public function __construct(RouterInterface $router)
+	{
+		$this->router = $router;
+	}
+
+	public function __invoke(Request $request, Response $response, $next)
+	{
+		if(!isset($_SESSION['login']) || !isset($_SESSION['role']))
+		{
+			return $response->withJson(['errors' => ['access denied']], 403);
+		}
+		return $next($request, $response);
+	}
+}

+ 2 - 2
system/Models/Folder.php

@@ -67,7 +67,7 @@ class Folder
 				$item->urlRel			= $fullSlugWithFolder . '/' . $item->slug;
 				$item->urlRel			= $fullSlugWithFolder . '/' . $item->slug;
 				$item->urlAbs			= $baseUrl . $fullSlugWithoutFolder . '/' . $item->slug;
 				$item->urlAbs			= $baseUrl . $fullSlugWithoutFolder . '/' . $item->slug;
 				$item->key				= $iteration;
 				$item->key				= $iteration;
-				$item->keyPath			= $keyPath ? $keyPath . '.' . $iteration : $iteration;
+				$item->keyPath			= isset($keyPath) ? $keyPath . '.' . $iteration : $iteration;
 				$item->keyPathArray		= explode('.', $item->keyPath);
 				$item->keyPathArray		= explode('.', $item->keyPath);
 				$item->chapter			= $chapter ? $chapter . '.' . $chapternr : $chapternr;
 				$item->chapter			= $chapter ? $chapter . '.' . $chapternr : $chapternr;
 				
 				
@@ -150,7 +150,7 @@ class Folder
 		if($item->elementType == 'folder')
 		if($item->elementType == 'folder')
 		{
 		{
 			/* get the first element in the folder */
 			/* get the first element in the folder */
-			$item->nextItem = isset($item->folderContent[0]) ? $item->folderContent[0] : false;
+			$item->nextItem = isset($item->folderContent[0]) ? clone($item->folderContent[0]) : false;
 		}
 		}
 		
 		
 		if(!$item->nextItem)
 		if(!$item->nextItem)

+ 1 - 1
system/Models/Validation.php

@@ -201,7 +201,7 @@ class Validation
 		$v = new Validator($params);
 		$v = new Validator($params);
 		
 		
 		$v->rule('required', ['title', 'content', 'url']);
 		$v->rule('required', ['title', 'content', 'url']);
-		$v->rule('lengthBetween', 'title', 2, 40);
+		$v->rule('lengthBetween', 'title', 2, 100);
 		$v->rule('noHTML', 'title');
 		$v->rule('noHTML', 'title');
 		$v->rule('markdownSecure', 'content');
 		$v->rule('markdownSecure', 'content');
 		
 		

+ 6 - 1
system/Models/Write.php

@@ -55,7 +55,12 @@ class Write
 		if($this->checkPath($folder))
 		if($this->checkPath($folder))
 		{
 		{
 			$filePath 	= $this->basePath . $folder . DIRECTORY_SEPARATOR . $file;
 			$filePath 	= $this->basePath . $folder . DIRECTORY_SEPARATOR . $file;
-			$openFile 	= fopen($filePath, "w");
+			$openFile 	= @fopen($filePath, "w");
+			
+			if(!$openFile)
+			{
+				return false;
+			}
 			
 			
 			fwrite($openFile, $data);
 			fwrite($openFile, $data);
 			fclose($openFile);
 			fclose($openFile);

+ 3 - 5
system/Routes/Api.php

@@ -2,9 +2,7 @@
 
 
 use Typemill\Controllers\SettingsController;
 use Typemill\Controllers\SettingsController;
 use Typemill\Controllers\ContentController;
 use Typemill\Controllers\ContentController;
-use Typemill\Middleware\RedirectIfUnauthenticated;
-use Typemill\Middleware\RedirectIfAuthenticated;
-use Typemill\Middleware\RedirectIfNoAdmin;
+use Typemill\Middleware\RestrictApiAccess;
 
 
-$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']));
+$app->get('/api/v1/themes', SettingsController::class . ':getThemeSettings')->setName('api.themes')->add(new RestrictApiAccess($container['router']));
+$app->put('/api/v1/article', ContentController::class . ':updateArticle')->setName('api.article.update')->add(new RestrictApiAccess($container['router']));

+ 18 - 13
system/author/content/content.twig

@@ -6,28 +6,33 @@
 	<div class="formWrapper">
 	<div class="formWrapper">
 
 
 		<section>
 		<section>
-			<form>
-				<fieldset>
-					<div id="editor" class="editor">
-						<div class="large">
+			<div id="editor" class="editor">
+				<form action="#" @submit.prevent="saveMarkdown">
+					<fieldset>
+						<div class="large" :class="{'error' : errors.title}">
 							<label for="title">Title*</label>
 							<label for="title">Title*</label>
-							<input name="title" id="title" type="text" value="{{ title }}" required />
+							<input id="title" name="title" type="text" v-model="form.title" required />
+							<span class="error" v-if="errors.title">${ errors.title }</span>
 						</div>
 						</div>
-						<input name="url" id="url" type="hidden" value="{{ item.urlRel }}" required />
-						<div class="large">
+						<div class="large" :class="{'error' : errors.content}">
 							<label for="content">Content*</label>
 							<label for="content">Content*</label>
 							<resizable-textarea>
 							<resizable-textarea>
-								<textarea id="content" v-model="markdown" name=="content" required></textarea>
+								<textarea id="content" v-model="form.content" required></textarea>
 							</resizable-textarea>
 							</resizable-textarea>
+							<span class="error" v-if="errors.content">${ errors.content }</span>
 						</div>
 						</div>
+						<input id="path" type="hidden" value="{{ item.urlRel }}" required readonly />
 						<div class="large">
 						<div class="large">
-							<button v-on:click="saveMarkdown">Save</button>
-							<div id="message" class="message"></div>
+							<button :class="bresult" :disabled="bdisabled">Save</button>
+							<div v-if="errors.message" class="message error">${ errors.message }</div>
 						</div>
 						</div>
-					</div>
-				</fieldset>
-			</form>
+					</fieldset>
+				</form>
+			</div>
+			<input id="origTitle" style="display:none" value="{{title}}">
 			<textarea id="origContent" style="display:none">{{ content }}</textarea>
 			<textarea id="origContent" style="display:none">{{ content }}</textarea>
+			{{ csrf_field() | raw }}
+
 		</section>
 		</section>
 		
 		
 	</div>
 	</div>

+ 34 - 93
system/author/js/vue-editor.js

@@ -1,5 +1,3 @@
-const root = document.getElementById("main").dataset.url;
-
 Vue.component('resizable-textarea', {
 Vue.component('resizable-textarea', {
   methods: {
   methods: {
     resizeTextarea (event) {
     resizeTextarea (event) {
@@ -22,115 +20,58 @@ Vue.component('resizable-textarea', {
   },
   },
 });
 });
 
 
-new Vue({
+let app = new Vue({
+    delimiters: ['${', '}'],
 	el: '#editor',
 	el: '#editor',
 	data: {
 	data: {
-		markdown: document.getElementById("origContent").value
+		form: {
+			title: 		document.getElementById("origTitle").value,
+			content: 	document.getElementById("origContent").value,
+			url: 		document.getElementById("path").value,
+			csrf_name: 	document.getElementById("csrf_name").value,
+			csrf_value:	document.getElementById("csrf_value").value,			
+		},
+		root: 		document.getElementById("main").dataset.url,
+		errors:{
+			title: false,
+			content: false,
+			message: false,
+		},
+		bdisabled: false,
+		bresult: false,
 	},
 	},
 	methods: {
 	methods: {
 		saveMarkdown: function(e){
 		saveMarkdown: function(e){
-			e.preventDefault();
-			
-			e.target.disabled = true;
-			e.target.classList.remove("success", "fail");
-			
-			deleteErrors();
+		
+			var self = this;
+			self.errors = {title: false, content: false, message: false},
+			self.bresult = '';
+			self.bdisabled = "disabled";
+		
+			var url = this.root + '/api/v1/article';
+			var method 	= 'PUT';
 			
 			
-			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)
 			sendJson(function(response, httpStatus)
 			{
 			{
 				if(response)
 				if(response)
 				{
 				{
-					e.target.disabled = false;
+					self.bdisabled = false;
+					
 					var result = JSON.parse(response);
 					var result = JSON.parse(response);
 					
 					
 					if(result.errors)
 					if(result.errors)
 					{
 					{
-						e.target.classList.add('fail');
-						processErrors(result.errors, httpStatus);
+						self.bresult = 'fail';						
+						if(result.errors.title){ self.errors.title = result.errors.title[0] };
+						if(result.errors.content){ self.errors.content = result.errors.content[0] };
+						if(result.errors.message){ self.errors.message = result.errors.message };
 					}
 					}
 					else
 					else
 					{
 					{
-						e.target.classList.add('success');
+						self.bresult = '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");
+			}, method, url, this.form );
 		}
 		}
 	}
 	}
-}
+})

+ 2 - 2
system/author/settings/system.twig

@@ -1,6 +1,7 @@
 {% extends 'layouts/layout.twig' %}
 {% extends 'layouts/layout.twig' %}
 {% block title %}Setup{% endblock %}
 {% block title %}Setup{% endblock %}
 {% set startpage = old.settings.startpage ? old.settings.startpage : settings.startpage %}
 {% set startpage = old.settings.startpage ? old.settings.startpage : settings.startpage %}
+{% set linebreaks = old.settings.linebreaks ? old.settings.linebreaks : settings.linebreaks %}
 {% set year = settings.year ? settings.year : "now"|date("Y") %}
 {% set year = settings.year ? settings.year : "now"|date("Y") %}
 
 
 {% block content %}
 {% block content %}
@@ -66,9 +67,8 @@
 							<input name="settings[startpage]" type="checkbox" id="startpage"{{ startpage ? ' checked' : '' }}>
 							<input name="settings[startpage]" type="checkbox" id="startpage"{{ startpage ? ' checked' : '' }}>
 							<span class="checkmark"></span>
 							<span class="checkmark"></span>
 						</label>
 						</label>
-					</div>
+					</div>					
 				</fieldset>
 				</fieldset>
-
 			</section>
 			</section>
 			<input type="submit" value="Save All Settings" />
 			<input type="submit" value="Save All Settings" />
 
 

+ 7 - 5
system/system.php

@@ -103,7 +103,7 @@ $container['assets'] = function($c)
 * 	DECIDE FOR SESSION	*
 * 	DECIDE FOR SESSION	*
 ************************/
 ************************/
 
 
-$session_segments = array('setup', 'tm/', '/setup', '/tm/');
+$session_segments = array('setup', 'tm/', 'api/', '/setup', '/tm/', '/api/');
 $path = $container['request']->getUri()->getPath();
 $path = $container['request']->getUri()->getPath();
 $container['flash'] = false;
 $container['flash'] = false;
 $container['csrf'] = false;
 $container['csrf'] = false;
@@ -112,7 +112,7 @@ foreach($session_segments as $segment)
 {	
 {	
 	if(substr( $path, 0, strlen($segment) ) === $segment)
 	if(substr( $path, 0, strlen($segment) ) === $segment)
 	{		
 	{		
-		/* start a session */
+		// configure session
 		ini_set( 'session.cookie_httponly', 1 );
 		ini_set( 'session.cookie_httponly', 1 );
 		ini_set('session.use_strict_mode', 1);
 		ini_set('session.use_strict_mode', 1);
 		if($container['request']->getUri()->getScheme() == 'https')
 		if($container['request']->getUri()->getScheme() == 'https')
@@ -124,9 +124,8 @@ foreach($session_segments as $segment)
 		{
 		{
 			session_name('typemill-session');
 			session_name('typemill-session');
 		}
 		}
-		session_start();
 		
 		
-		/* add csrf-protection */
+		// add csrf-protection
 		$container['csrf'] = function ($c)
 		$container['csrf'] = function ($c)
 		{
 		{
 			$guard = new \Slim\Csrf\Guard();
 			$guard = new \Slim\Csrf\Guard();
@@ -135,11 +134,14 @@ foreach($session_segments as $segment)
 			return $guard;
 			return $guard;
 		};
 		};
 		
 		
-		/* add flash to container */
+		// add flash to container
 		$container['flash'] = function () 
 		$container['flash'] = function () 
 		{
 		{
 			return new \Slim\Flash\Messages();
 			return new \Slim\Flash\Messages();
 		};
 		};
+		
+		// start session
+		session_start();
 	}
 	}
 }
 }
 
 

+ 7 - 6
themes/typemill/cover.twig

@@ -7,13 +7,14 @@
 	<div class="lead">
 	<div class="lead">
 
 
 		{{ content }}
 		{{ content }}
-
-		<a href="{{ navigation[0].urlRel }}">{{ settings.themes.typemill.start ? settings.themes.typemill.start : 'Start'}}</a>
-		
-		{% if settings.setup  %}
-			<a href="{{ base_url }}/setup">Setup</a>
-		{% endif %}
 		
 		
+		<div class="actionLink">
+			<a href="{{ navigation[0].urlRel }}">{{ settings.themes.typemill.start ? settings.themes.typemill.start : 'Start'}}</a>
+			
+			{% if settings.setup  %}
+				<a href="{{ base_url }}/setup">Setup</a>
+			{% endif %}
+		</div>
 	</div>
 	</div>
 	
 	
 {% endblock %}
 {% endblock %}

+ 26 - 5
themes/typemill/css/style.css

@@ -21,9 +21,11 @@ aside{	background: #f9f8f6; border-left: 30px solid #FFF; border-right: 30px sol
 .main-menu li a:focus, .main-menu li a:hover, .main-menu li a:active, .main-menu li.active.file a{ color: #e0474c; }
 .main-menu li a:focus, .main-menu li a:hover, .main-menu li a:active, .main-menu li.active.file a{ color: #e0474c; }
 article {background: #FFF; }
 article {background: #FFF; }
 article a, article a:link, article a:visited,
 article a, article a:link, article a:visited,
-footer a, footer a:link, footer a:visited{ text-decoration: none; color: #e0474c; }
+footer a, footer a:link, footer a:visited
+.lead a, .lead a:link, .lead a:visited{ text-decoration: none; color: #e0474c; }
 article a:focus, article a:hover, article a:active,
 article a:focus, article a:hover, article a:active,
-footer a:focus, footer a:hover, footer a:active{ text-decoration: underline }
+footer a:focus, footer a:hover, footer a:active
+.lead a:focus, .lead a:hover, .lead a:active{ text-decoration: underline }
 article .breadcrumb,article .paging a{ background: #f9f8f6; }
 article .breadcrumb,article .paging a{ background: #f9f8f6; }
 article .breadcrumb span a{ background: #e0474c; color: #f9f8f6; border: 1px solid #e0474c; }
 article .breadcrumb span a{ background: #e0474c; color: #f9f8f6; border: 1px solid #e0474c; }
 article .breadcrumb a:focus,article .breadcrumb a:hover,article .breadcrumb a:active { background: #f9f8f6; color: #e0474c; }
 article .breadcrumb a:focus,article .breadcrumb a:hover,article .breadcrumb a:active { background: #f9f8f6; color: #e0474c; }
@@ -36,9 +38,9 @@ header a, .cover{ color: #444; }
 footer{ background: #FFF; }
 footer{ background: #FFF; }
 .chapterNumber{ color: #bbb; }
 .chapterNumber{ color: #bbb; }
 .chapter h1{ border-bottom: 2px solid #f9f8f6; }
 .chapter h1{ border-bottom: 2px solid #f9f8f6; }
-.cover .lead a, .cover .lead a:link, .cover .lead a:visited,
+.cover .actionLink a, .cover .actionLink a:link, .cover .actionLink a:visited,
 a.readMore, a.readMore:link, a.readMore:visited{ border: 2px solid #e0474c; background: #e0474c; color: #f9f8f6; }
 a.readMore, a.readMore:link, a.readMore:visited{ border: 2px solid #e0474c; background: #e0474c; color: #f9f8f6; }
-.cover .lead a:focus, .cover .lead a:hover, .cover .lead a:active,
+.cover .lead a:focus, .cover .actionLink a:hover, .cover .actionLink a:active,
 a.readMore:focus, a.readMore:hover, a.readMore:active{
 a.readMore:focus, a.readMore:hover, a.readMore:active{
 	border: 2px solid #e0474c;
 	border: 2px solid #e0474c;
 	color: #444;
 	color: #444;
@@ -204,7 +206,7 @@ header p{
 	font-size: 2.5em; 
 	font-size: 2.5em; 
 	font-weight: 700;
 	font-weight: 700;
 }
 }
-.cover .lead a, a.readMore{
+.cover .actionLink a, a.readMore{
 	display: inline-block;
 	display: inline-block;
 	min-width: 100px;
 	min-width: 100px;
 	padding: 5px 10px;
 	padding: 5px 10px;
@@ -319,6 +321,25 @@ article{
 article img{
 article img{
 	width: 100%;
 	width: 100%;
 }
 }
+article img.left{
+	width: auto;
+	max-width: 100%;
+	float: left;
+	margin: 10px 10px 10px 0;
+}
+article img.right{
+	width: auto;
+	max-width: 100%;
+	float: right;
+	margin: 10px 0px 10px 10px;	
+}
+article img.middle{
+	display:block;
+	width: auto;
+	max-width: 100%;
+	margin: auto;
+}
+
 
 
 /************************
 /************************
 *  	PAGING / BREADCRUMB *
 *  	PAGING / BREADCRUMB *