Pārlūkot izejas kodu

Version 1.3.8: ACL-Implementation

trendschau 5 gadi atpakaļ
vecāks
revīzija
74ecf7457e

+ 2 - 1
composer.json

@@ -19,7 +19,8 @@
 		"erusev/parsedown": "~1.4",
 		"erusev/parsedown-extra": "dev-master",
 		"jbroadway/urlify": "1.1.3",
-		"vlucas/valitron": "dev-master"
+		"vlucas/valitron": "dev-master",
+        "laminas/laminas-permissions-acl": "^2.7"
     },
 	"autoload": {
 		"psr-4": {

+ 115 - 6
composer.lock

@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "b2dd4b8a1c943c5e407add9f1b9104ea",
+    "content-hash": "87094a87b3a795ce73c299e4535358fb",
     "packages": [
         {
             "name": "erusev/parsedown",
@@ -159,6 +159,111 @@
             ],
             "time": "2019-06-13T18:30:56+00:00"
         },
+        {
+            "name": "laminas/laminas-permissions-acl",
+            "version": "2.7.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/laminas/laminas-permissions-acl.git",
+                "reference": "624567fe376a70e0bfb5aa8217d5afa13b9d6e61"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/laminas/laminas-permissions-acl/zipball/624567fe376a70e0bfb5aa8217d5afa13b9d6e61",
+                "reference": "624567fe376a70e0bfb5aa8217d5afa13b9d6e61",
+                "shasum": ""
+            },
+            "require": {
+                "laminas/laminas-zendframework-bridge": "^1.0",
+                "php": "^5.6 || ^7.0"
+            },
+            "replace": {
+                "zendframework/zend-permissions-acl": "self.version"
+            },
+            "require-dev": {
+                "laminas/laminas-coding-standard": "~1.0.0",
+                "laminas/laminas-servicemanager": "^2.7.5 || ^3.0.3",
+                "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.5"
+            },
+            "suggest": {
+                "laminas/laminas-servicemanager": "To support Laminas\\Permissions\\Acl\\Assertion\\AssertionManager plugin manager usage"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "2.7.x-dev",
+                    "dev-develop": "2.8.x-dev"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Laminas\\Permissions\\Acl\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "description": "Provides a lightweight and flexible access control list (ACL) implementation for privileges management",
+            "homepage": "https://laminas.dev",
+            "keywords": [
+                "acl",
+                "laminas"
+            ],
+            "time": "2019-12-31T17:37:23+00:00"
+        },
+        {
+            "name": "laminas/laminas-zendframework-bridge",
+            "version": "1.0.4",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/laminas/laminas-zendframework-bridge.git",
+                "reference": "fcd87520e4943d968557803919523772475e8ea3"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/laminas/laminas-zendframework-bridge/zipball/fcd87520e4943d968557803919523772475e8ea3",
+                "reference": "fcd87520e4943d968557803919523772475e8ea3",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^5.6 || ^7.0"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.1",
+                "squizlabs/php_codesniffer": "^3.5"
+            },
+            "type": "library",
+            "extra": {
+                "branch-alias": {
+                    "dev-master": "1.0.x-dev",
+                    "dev-develop": "1.1.x-dev"
+                },
+                "laminas": {
+                    "module": "Laminas\\ZendFrameworkBridge"
+                }
+            },
+            "autoload": {
+                "files": [
+                    "src/autoload.php"
+                ],
+                "psr-4": {
+                    "Laminas\\ZendFrameworkBridge\\": "src//"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "BSD-3-Clause"
+            ],
+            "description": "Alias legacy ZF class names to Laminas Project equivalents.",
+            "keywords": [
+                "ZendFramework",
+                "autoloading",
+                "laminas",
+                "zf"
+            ],
+            "time": "2020-05-20T16:45:56+00:00"
+        },
         {
             "name": "nikic/fast-route",
             "version": "v1.3.0",
@@ -686,16 +791,16 @@
         },
         {
             "name": "symfony/polyfill-ctype",
-            "version": "v1.17.0",
+            "version": "v1.17.1",
             "source": {
                 "type": "git",
                 "url": "https://github.com/symfony/polyfill-ctype.git",
-                "reference": "e94c8b1bbe2bc77507a1056cdb06451c75b427f9"
+                "reference": "2edd75b8b35d62fd3eeabba73b26b8f1f60ce13d"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e94c8b1bbe2bc77507a1056cdb06451c75b427f9",
-                "reference": "e94c8b1bbe2bc77507a1056cdb06451c75b427f9",
+                "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/2edd75b8b35d62fd3eeabba73b26b8f1f60ce13d",
+                "reference": "2edd75b8b35d62fd3eeabba73b26b8f1f60ce13d",
                 "shasum": ""
             },
             "require": {
@@ -708,6 +813,10 @@
             "extra": {
                 "branch-alias": {
                     "dev-master": "1.17-dev"
+                },
+                "thanks": {
+                    "name": "symfony/polyfill",
+                    "url": "https://github.com/symfony/polyfill"
                 }
             },
             "autoload": {
@@ -740,7 +849,7 @@
                 "polyfill",
                 "portable"
             ],
-            "time": "2020-05-12T16:14:59+00:00"
+            "time": "2020-06-06T08:46:27+00:00"
         },
         {
             "name": "symfony/yaml",

BIN
media/live/hostinger-1.png


BIN
media/live/hostinger-2.png


BIN
media/live/hostinger.png


BIN
media/live/logo.png


BIN
media/original/hostinger-1.png


BIN
media/original/hostinger-2.png


BIN
media/original/hostinger.png


BIN
media/original/logo.png


BIN
media/thumbs/hostinger-1.png


BIN
media/thumbs/hostinger-2.png


BIN
media/thumbs/hostinger.png


BIN
media/thumbs/logo.png


+ 181 - 42
system/Controllers/SettingsController.php

@@ -231,11 +231,9 @@ class SettingsController extends Controller
 		}
 		
 		/* add the users for navigation */
-		$user		= new User();
-		$users		= $user->getUsers();
 		$route 		= $request->getAttribute('route');
 
-		return $this->render($response, 'settings/themes.twig', array('settings' => $userSettings, 'themes' => $themedata, 'users' => $users, 'route' => $route->getName() ));
+		return $this->render($response, 'settings/themes.twig', array('settings' => $userSettings, 'themes' => $themedata, 'route' => $route->getName() ));
 	}
 	
 	public function showPlugins($request, $response, $args)
@@ -300,11 +298,9 @@ class SettingsController extends Controller
 			}
 		}
 		
-		$user 	= new User();
-		$users 	= $user->getUsers();
 		$route 	= $request->getAttribute('route');
 		
-		return $this->render($response, 'settings/plugins.twig', array('settings' => $userSettings, 'plugins' => $plugins, 'users' => $users, 'route' => $route->getName() ));
+		return $this->render($response, 'settings/plugins.twig', array('settings' => $userSettings, 'plugins' => $plugins, 'route' => $route->getName() ));
 	}
 
 	/*************************************
@@ -482,10 +478,13 @@ class SettingsController extends Controller
 		}
 	}
 
-	private function validateInput($objectType, $objectName, $userInput, $validate)
+	private function validateInput($objectType, $objectName, $userInput, $validate, $originalSettings = NULL)
 	{
-		/* fetch the original settings from the folder (plugin or theme) to get the field definitions */
-		$originalSettings = \Typemill\Settings::getObjectSettings($objectType, $objectName);
+		if(!$originalSettings)
+		{
+			# fetch the original settings from the folder (plugin or theme) to get the field definitions
+			$originalSettings = \Typemill\Settings::getObjectSettings($objectType, $objectName);
+		}
 
 		# images get special treatment
 		$imageFieldDefinitions = array();
@@ -557,7 +556,7 @@ class SettingsController extends Controller
 		# initiate image processor with standard image sizes
 		$processImages = new ProcessImage($userSettings['images']);
 
-		if(!$processImages->checkFolders())
+		if(!$processImages->checkFolders('images'))
 		{
 			$this->c->flash->addMessage('error', 'Please make sure that your media folder exists and is writable.');
 			return false; 
@@ -591,10 +590,57 @@ class SettingsController extends Controller
 	/***********************
 	**   USER MANAGEMENT  **
 	***********************/
+
+	public function showAccount($request, $response, $args)
+	{		
+		$username 	= $_SESSION['user'];
+
+		$validate 	= new Validation();
+		
+		if($validate->username($username))
+		{
+			# get settings
+			$settings 	= $this->c->get('settings');
+
+			# get user with userdata
+			$user 		= new User();
+			$userdata 	= $user->getSecureUser($username);
+			
+			# instantiate field-builder
+			$fieldsModel	= new Fields();
+
+			# get the field-definitions
+			$fieldDefinitions = $this->getUserFields($_SESSION['role']);
+
+			# prepare userdata for field-builder
+			$userSettings['user'][$username] = $userdata;
+
+			# generate the input form
+			$userform = $fieldsModel->getFields($userSettings, 'user', $username, $fieldDefinitions);
+
+			$route = $request->getAttribute('route');
+
+			return $this->render($response, 'settings/user.twig', array(
+				'settings' 		=> $settings, 
+				'usersettings' 	=> $userSettings, 		// needed for image url in form, will overwrite settings for field-template
+				'userform' 		=> $userform, 			// field model, needed to generate frontend-field
+				'userdata' 		=> $userdata, 			// needed to fill form with data
+#				'userrole' 		=> false,				// not needed ? 
+#				'username' 		=> $args['username'], 	// not needed ?
+				'route' 		=> $route->getName()  	// needed to set link active
+			));
+			
+			return $this->render($response, 'settings/user.twig', array('settings' => $settings, 'usersettings' => $userSettings, 'userdata' => $userdata, 'userrole' => false, 'username' => $username, 'userform' => $userform, 'route' => $route->getName() ));
+		}
+		
+		$this->c->flash->addMessage('error', 'User does not exists');
+		return $response->withRedirect($this->c->router->pathFor('home'));
+	}
 	
 	public function showUser($request, $response, $args)
 	{
-		if($_SESSION['role'] == 'editor' && $_SESSION['user'] !== $args['username'])
+		# if user has no rights to watch userlist, then only show his user-entry
+		if(!$this->c->acl->isAllowed($_SESSION['role'], 'userlist', 'view') && $_SESSION['user'] !== $args['username'] )
 		{
 			return $response->withRedirect($this->c->router->pathFor('user.show', ['username' => $_SESSION['user']] ));
 		}
@@ -603,20 +649,77 @@ class SettingsController extends Controller
 		
 		if($validate->username($args['username']))
 		{
-			$user 		= new User();
-			$users		= $user->getUsers();
-			$userrole	= $user->getUserroles();
-			$userdata 	= $user->getUser($args['username']);
+			# get settings
 			$settings 	= $this->c->get('settings');
+
+			# get user with userdata		
+			$user 		= new User();
+			$userdata 	= $user->getSecureUser($args['username']);
+
+			$username	= $userdata['username'];
 			
-			if($userdata)
-			{				
-				return $this->render($response, 'settings/user.twig', array('settings' => $settings, 'users' => $users, 'userdata' => $userdata, 'userrole' => $userrole, 'username' => $args['username'] ));
-			}
+			# instantiate field-builder
+			$fieldsModel	= new Fields();
+
+			# get the field-definitions
+			$fieldDefinitions = $this->getUserFields($userdata['userrole']);
+
+			# prepare userdata for field-builder
+			$userSettings['user'][$username] = $userdata;
+
+			# generate the input form
+			$userform = $fieldsModel->getFields($userSettings, 'user', $username, $fieldDefinitions);
+
+			$route = $request->getAttribute('route');
+			
+			return $this->render($response, 'settings/user.twig', array(
+				'settings' 		=> $settings, 
+				'usersettings' 	=> $userSettings, 		// needed for image url in form, will overwrite settings for field-template
+				'userform' 		=> $userform, 			// field model, needed to generate frontend-field
+				'userdata' 		=> $userdata, 			// needed to fill form with data
+#				'userrole' 		=> false,				// not needed ? 
+#				'username' 		=> $args['username'], 	// not needed ?
+				'route' 		=> $route->getName()  	// needed to set link active
+			));
 		}
 		
 		$this->c->flash->addMessage('error', 'User does not exists');
-		return $response->withRedirect($this->c->router->pathFor('user.list'));
+		return $response->withRedirect($this->c->router->pathFor('user.account'));
+	}
+
+	private function getUserFields($role)
+	{
+		$fields = [];
+		$fields['username'] 	= ['label' => 'Username (read only)', 'type' => 'text', 'readonly' => true];
+		$fields['image'] 		= ['label' => 'Profile-Image', 'type' => 'image'];
+		$fields['description'] 	= ['label' => 'Author-Description (Markdown)', 'type' => 'textarea'];
+		$fields['firstname'] 	= ['label' => 'First Name', 'type' => 'text'];
+		$fields['lastname'] 	= ['label' => 'Last Name', 'type' => 'text'];
+		$fields['email'] 		= ['label' => 'E-Mail', 'type' => 'text', 'required' => true];
+		$fields['userrole'] 	= ['label' => 'Role', 'type' => 'text', 'readonly' => true];
+		$fields['password'] 	= ['label' => 'Actual Password', 'type' => 'password'];
+		$fields['newpassword'] 	= ['label' => 'New Password', 'type' => 'password'];
+
+		# dispatch fields;
+
+		# change admin stuff
+		if($_SESSION['role'] == 'administrator')
+		{
+			$userroles = $this->c->acl->getRoles();
+			$options = [];
+
+			# we need associative array to make select-field with key/value work
+			foreach($userroles as $userrole)
+			{
+				$options[$userrole] = $userrole;
+ 			}
+
+			$fields['userrole'] = ['label' => 'Role', 'type' => 'select', 'options' => $options];
+		}
+
+		$userform = [];
+		$userform['forms']['fields'] = $fields;
+		return $userform; 
 	}
 
 	public function listUser($request, $response)
@@ -632,7 +735,7 @@ class SettingsController extends Controller
 			$userdata[] = $user->getUser($username);
 		}
 		
-		return $this->render($response, 'settings/userlist.twig', array('settings' => $settings, 'users' => $users, 'userdata' => $userdata, 'route' => $route->getName() ));		
+		return $this->render($response, 'settings/userlist.twig', array('settings' => $settings, 'users' => $users, 'userdata' => $userdata, 'route' => $route->getName() ));
 	}
 	
 	public function newUser($request, $response, $args)
@@ -663,12 +766,18 @@ class SettingsController extends Controller
 			
 			$params 		= $request->getParams();
 			$user 			= new User();
-			$userroles		= $user->getUserroles();
 			$validate		= new Validation();
+			$userroles 		= $this->c->acl->getRoles();
 
 			if($validate->newUser($params, $userroles))
 			{
-				$userdata	= array('username' => $params['username'], 'firstname' => $params['firstname'], 'lastname' => $params['lastname'], 'email' => $params['email'], 'userrole' => $params['userrole'], 'password' => $params['password']);
+				$userdata	= array(
+					'username' => $params['username'], 
+					'firstname' => $params['firstname'], 
+					'lastname' => $params['lastname'], 
+					'email' => $params['email'], 
+					'userrole' => $params['userrole'], 
+					'password' => $params['password']);
 				
 				$user->createUser($userdata);
 
@@ -683,6 +792,7 @@ class SettingsController extends Controller
 	
 	public function updateUser($request, $response, $args)
 	{
+
 		if($request->isPost())
 		{
 			$referer		= $request->getHeader('HTTP_REFERER');
@@ -697,44 +807,73 @@ class SettingsController extends Controller
 			}
 			
 			$params 		= $request->getParams();
+			$userdata 		= $params['user'];
 			$user 			= new User();
-			$userroles		= $user->getUserroles();
 			$validate		= new Validation();
-			
-			/* non admins have different update rights */
-			if($_SESSION['role'] !== 'administrator')
+			$userroles 		= $this->c->acl->getRoles();
+
+			print_r($params);
+			die();
+						
+			# check if user is allowed to view (edit) userlist and other users
+			if(!$this->c->acl->isAllowed($_SESSION['role'], 'userlist', 'view'))
 			{
-				/* if an editor tries to update other userdata than its own */
-				if($_SESSION['user'] !== $params['username'])
+				# if an editor tries to update other userdata than its own */
+				if($_SESSION['user'] !== $userdata['username'])
 				{
-					return $response->withRedirect($this->c->router->pathFor('user.show', ['username' => $_SESSION['user']] ));
+					return $response->withRedirect($this->c->router->pathFor('user.account'));
 				}
 				
-				/* non admins cannot change his userrole */
-				$params['userrole'] = $_SESSION['role'];
+				# non admins cannot change his userrole, so set it to session-value
+				$userdata['userrole'] = $_SESSION['role'];
 			}
-	
-			if($validate->existingUser($params, $userroles))
+
+			# validate standard fields for users
+			if($validate->existingUser($userdata, $userroles))
 			{
-				$userdata	= array('username' => $params['username'], 'firstname' => $params['firstname'], 'lastname' => $params['lastname'], 'email' => $params['email'], 'userrole' => $params['userrole']);
-				
-				if(empty($params['password']) AND empty($params['newpassword']))
+				# validate custom input fields and return images
+				$userfields = $this->getUserFields($userdata['userrole']);
+				$imageFields = $this->validateInput('users', 'user', $userdata, $validate, $userfields);
+
+				if(!isset($_SESSION['errors']) && !empty($imageFields))
+				{
+					$images = $request->getUploadedFiles();
+
+					if(isset($images['user']))
+					{
+						# set image size
+						$settings = $this->c->get('settings');
+						$settings['images']['live'] = ['width' => 500, 'height' => 500];
+						$userdata = $this->saveImages($imageFields, $userdata, $settings, $images['user']);
+					}
+				}
+			
+				# check for errors and redirect to path, if errors found */
+				if(isset($_SESSION['errors']))
+				{
+					$this->c->flash->addMessage('error', 'Please correct the errors');
+					return $response->withRedirect($this->c->router->pathFor('user.show', ['username' => $userdata['username']]));
+				}
+
+				if(empty($userdata['password']) AND empty($userdata['newpassword']))
 				{
 					$user->updateUser($userdata);
 					$this->c->flash->addMessage('info', 'Saved all changes');
-					return $response->withRedirect($this->c->router->pathFor('user.show', ['username' => $params['username']]));
+					return $response->withRedirect($this->c->router->pathFor('user.show', ['username' => $userdata['username']]));
 				}
-				elseif($validate->newPassword($params))
+				elseif($validate->newPassword($userdata))
 				{
-					$userdata['password'] = $params['newpassword'];				
+					$userdata['password'] = $userdata['newpassword'];
+					unset($userdata['newpassword']);
+
 					$user->updateUser($userdata);
 					$this->c->flash->addMessage('info', 'Saved all changes');
-					return $response->withRedirect($this->c->router->pathFor('user.show', ['username' => $params['username']]));
+					return $response->withRedirect($this->c->router->pathFor('user.show', ['username' => $userdata['username']]));
 				}
 			}
 			
 			$this->c->flash->addMessage('error', 'Please correct your input');
-			return $response->withRedirect($this->c->router->pathFor('user.show', ['username' => $params['username']]));
+			return $response->withRedirect($this->c->router->pathFor('user.show', ['username' => $userdata['username']]));
 		}
 	}
 	

+ 14 - 0
system/Events/OnResourcesLoaded.php

@@ -0,0 +1,14 @@
+<?php
+
+namespace Typemill\Events;
+
+use Symfony\Component\EventDispatcher\Event;
+
+/**
+ * Event for acl.
+ */
+
+class OnResourcesLoaded extends BaseEvent
+{
+
+}

+ 14 - 0
system/Events/OnRolesPermissionsLoaded.php

@@ -0,0 +1,14 @@
+<?php
+
+namespace Typemill\Events;
+
+use Symfony\Component\EventDispatcher\Event;
+
+/**
+ * Event for acl.
+ */
+
+class OnRolesPermissionsLoaded extends BaseEvent
+{
+
+}

+ 37 - 0
system/Middleware/accessController.php

@@ -0,0 +1,37 @@
+<?php
+
+namespace Typemill\Middleware;
+
+use Slim\Interfaces\RouterInterface;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+class accessController
+{
+	protected $router;
+	
+	public function __construct(RouterInterface $router, $acl, $resource, $privilege)
+	{
+		$this->router 		= $router;
+		$this->acl 			= $acl;
+		$this->resource 	= $resource;
+		$this->privilege 	= $privilege;
+	}
+
+	public function __invoke(Request $request, Response $response, $next)
+	{
+		if(!isset($_SESSION['login']))
+		{
+			return $response->withRedirect($this->router->pathFor('auth.show'));
+		}
+
+		if(!$this->acl->isAllowed($_SESSION['role'], $this->resource, $this->privilege ))
+		{
+			# redirect to frontend startpage
+			# alternatively return an error and show an error page.
+			return $response->withRedirect($this->router->pathFor('home'));
+		}
+
+		return $next($request, $response);
+	}
+}

+ 1 - 1
system/Models/Fields.php

@@ -78,7 +78,7 @@ class Fields
 					}
 				}
 				elseif($field->getType() == "checkbox")
-				{					
+				{
 					# checkboxes need a special treatment, because field does not exist in settings if unchecked by user
 					if(isset($userSettings[$objectType][$objectName][$fieldName]))
 					{

+ 10 - 1
system/Models/User.php

@@ -29,6 +29,13 @@ class User extends WriteYaml
 		$user = $this->getYaml('settings/users', $username . '.yaml');
 		return $user;
 	}
+
+	public function getSecureUser($username)
+	{
+		$user = $this->getYaml('settings/users', $username . '.yaml');
+		unset($user['password']);
+		return $user;
+	}
 	
 	public function createUser($params)
 	{		
@@ -91,11 +98,13 @@ class User extends WriteYaml
 		}
 	}
 	
+	/* replaced by ACL
 	public function getUserroles()
 	{
 		return array('administrator', 'editor');
 	}	
-	
+	*/
+
 	public function login($username)
 	{
 		$user = $this->getUser($username);

+ 24 - 20
system/Routes/Web.php

@@ -9,6 +9,7 @@ use Typemill\Controllers\ContentBackendController;
 use Typemill\Middleware\RedirectIfUnauthenticated;
 use Typemill\Middleware\RedirectIfAuthenticated;
 use Typemill\Middleware\RedirectIfNoAdmin;
+use Typemill\Middleware\accessController;
 
 if($settings['settings']['setup'])
 {
@@ -35,37 +36,40 @@ $app->get('/tm/login', AuthController::class . ':show')->setName('auth.show')->a
 $app->post('/tm/login', AuthController::class . ':login')->setName('auth.login')->add(new RedirectIfAuthenticated($container['router'], $container['settings']));
 $app->get('/tm/logout', AuthController::class . ':logout')->setName('auth.logout')->add(new RedirectIfUnauthenticated($container['router'], $container['flash']));
 
-$app->get('/tm/settings', SettingsController::class . ':showSettings')->setName('settings.show')->add(new RedirectIfNoAdmin($container['router'], $container['flash']));
-$app->post('/tm/settings', SettingsController::class . ':saveSettings')->setName('settings.save')->add(new RedirectIfNoAdmin($container['router'], $container['flash']));
-$app->get('/tm/themes', SettingsController::class . ':showThemes')->setName('themes.show')->add(new RedirectIfNoAdmin($container['router'], $container['flash']));
-$app->post('/tm/themes', SettingsController::class . ':saveThemes')->setName('themes.save')->add(new RedirectIfNoAdmin($container['router'], $container['flash']));
-$app->get('/tm/plugins', SettingsController::class . ':showPlugins')->setName('plugins.show')->add(new RedirectIfNoAdmin($container['router'], $container['flash']));
-$app->post('/tm/plugins', SettingsController::class . ':savePlugins')->setName('plugins.save')->add(new RedirectIfNoAdmin($container['router'], $container['flash']));
-$app->get('/tm/user/new', SettingsController::class . ':newUser')->setName('user.new')->add(new RedirectIfNoAdmin($container['router'], $container['flash']));
-$app->post('/tm/user/create', SettingsController::class . ':createUser')->setName('user.create')->add(new RedirectIfNoAdmin($container['router'], $container['flash']));
+$app->get('/tm/settings', SettingsController::class . ':showSettings')->setName('settings.show')->add(new accessController($container['router'], $container['acl'], 'settings', 'view'));
+$app->post('/tm/settings', SettingsController::class . ':saveSettings')->setName('settings.save')->add(new accessController($container['router'], $container['acl'], 'settings', 'update'));
+$app->get('/tm/themes', SettingsController::class . ':showThemes')->setName('themes.show')->add(new accessController($container['router'], $container['acl'], 'themes', 'view'));
+$app->post('/tm/themes', SettingsController::class . ':saveThemes')->setName('themes.save')->add(new accessController($container['router'], $container['acl'], 'themes', 'update'));
 
-$app->post('/tm/user/update', SettingsController::class . ':updateUser')->setName('user.update')->add(new RedirectIfUnauthenticated($container['router'], $container['flash']));
-$app->post('/tm/user/delete', SettingsController::class . ':deleteUser')->setName('user.delete')->add(new RedirectIfUnauthenticated($container['router'], $container['flash']));
-$app->get('/tm/user/{username}', SettingsController::class . ':showUser')->setName('user.show')->add(new RedirectIfUnauthenticated($container['router'], $container['flash']));
-$app->get('/tm/user', SettingsController::class . ':listUser')->setName('user.list')->add(new RedirectIfNoAdmin($container['router'], $container['flash']));
+$app->get('/tm/plugins', SettingsController::class . ':showPlugins')->setName('plugins.show')->add(new accessController($container['router'], $container['acl'], 'plugins', 'view'));
+$app->post('/tm/plugins', SettingsController::class . ':savePlugins')->setName('plugins.save')->add(new accessController($container['router'], $container['acl'], 'plugins', 'update'));
+$app->get('/tm/user/new', SettingsController::class . ':newUser')->setName('user.new')->add(new accessController($container['router'], $container['acl'], 'users', 'create'));
+$app->post('/tm/user/create', SettingsController::class . ':createUser')->setName('user.create')->add(new accessController($container['router'], $container['acl'], 'user', 'create'));
+$app->post('/tm/user/update', SettingsController::class . ':updateUser')->setName('user.update')->add(new accessController($container['router'], $container['acl'], 'user', 'update'));
+$app->post('/tm/user/delete', SettingsController::class . ':deleteUser')->setName('user.delete')->add(new accessController($container['router'], $container['acl'], 'user', 'delete'));
+$app->get('/tm/user/account', SettingsController::class . ':showAccount')->setName('user.account')->add(new accessController($container['router'], $container['acl'], 'user', 'view'));
+$app->get('/tm/user/{username}', SettingsController::class . ':showUser')->setName('user.show')->add(new accessController($container['router'], $container['acl'], 'user', 'view'));
+$app->get('/tm/user', SettingsController::class . ':listUser')->setName('user.list')->add(new accessController($container['router'], $container['acl'], 'userlist', 'view'));
 
-$app->get('/tm/content/raw[/{params:.*}]', ContentBackendController::class . ':showContent')->setName('content.raw')->add(new RedirectIfUnauthenticated($container['router'], $container['flash']));
-$app->get('/tm/content/visual[/{params:.*}]', ContentBackendController::class . ':showBlox')->setName('content.visual')->add(new RedirectIfUnauthenticated($container['router'], $container['flash']));
-$app->get('/tm/content[/{params:.*}]', ContentBackendController::class . ':showEmpty')->setName('content.empty')->add(new RedirectIfUnauthenticated($container['router'], $container['flash']));
+$app->get('/tm/content/raw[/{params:.*}]', ContentBackendController::class . ':showContent')->setName('content.raw')->add(new accessController($container['router'], $container['acl'], 'content', 'view'));
+$app->get('/tm/content/visual[/{params:.*}]', ContentBackendController::class . ':showBlox')->setName('content.visual')->add(new accessController($container['router'], $container['acl'], 'content', 'view'));
+$app->get('/tm/content[/{params:.*}]', ContentBackendController::class . ':showEmpty')->setName('content.empty')->add(new accessController($container['router'], $container['acl'], 'content', 'view'));
 
 foreach($routes as $pluginRoute)
 {
-	$method = $pluginRoute['httpMethod'];
-	$route	= $pluginRoute['route'];
-	$class	= $pluginRoute['class'];
+	$method 	= $pluginRoute['httpMethod'];
+	$route		= $pluginRoute['route'];
+	$class		= $pluginRoute['class'];
+	$resource 	= isset($pluginRoute['resource']) ? $pluginRoute['resource'] : NULL;
+	$privilege 	= isset($pluginRoute['privilege']) ? $pluginRoute['privilege'] : NULL;
 
 	if(isset($pluginRoute['name']))
 	{
-		$app->{$method}($route, $class)->setName($pluginRoute['name']);
+		$app->{$method}($route, $class)->setName($pluginRoute['name'])->add(new accessController($container['router'], $container['acl'], $resource, $privilege));
 	}
 	else
 	{
-		$app->{$method}($route, $class);
+		$app->{$method}($route, $class)->add(new accessController($container['router'], $container['acl'], $resource, $privilege));
 	}
 }
 

+ 62 - 1
system/Settings.php

@@ -2,6 +2,10 @@
 
 namespace Typemill;
 
+use Laminas\Permissions\Acl\Acl;
+use Laminas\Permissions\Acl\Role\GenericRole as Role;
+use Laminas\Permissions\Acl\Resource\GenericResource as Resource;
+
 class Settings
 {	
 	public static function loadSettings()
@@ -182,4 +186,61 @@ class Settings
 			$yaml->updateYaml('settings', 'settings.yaml', $settings);					
 		}
 	}
-}
+
+	public static function loadResources()
+	{
+		return ['content',
+				'user',
+				'userlist',
+				'settings',
+				'themes',
+				'plugins'];
+	}
+
+	public static function loadRolesAndPermissions()
+	{
+		$guest['name']			= 'guest';
+		$guest['inherits'] 		= NULL;
+		$guest['permissions']	= [];
+
+		$member['name']			= 'member';
+		$member['inherits'] 	= 'guest';
+		$member['permissions']	= ['user' => ['view','update','delete']];
+
+		$author['name']			= 'author';
+		$author['inherits']		= 'member';
+		$author['permissions']	= ['content' => ['view','create', 'update', 'delete']];
+
+		$editor['name']			= 'editor';
+		$editor['inherits']		= 'author';
+		$editor['permissions']	= ['content' => ['publish', 'depublish']];
+
+		return [$guest, $member, $author, $editor];
+	}
+
+	public static function createAcl($roles, $resources)
+	{
+		$acl = new Acl();
+
+		foreach($resources as $resource)
+		{
+			$acl->addResource(new Resource($resource));
+		}
+
+		# add administrator role
+		$acl->addRole(new Role('administrator'));
+		$acl->allow('administrator');
+
+		# add all other roles dynamically
+		foreach($roles as $role)
+		{
+			$acl->addRole(new Role($role['name']), $role['inherits']);
+			foreach($role['permissions'] as $resource =>  $permissions)
+			{
+				$acl->allow($role['name'], $resource, $permissions);
+			}
+		}
+
+		return $acl;
+	}
+}

+ 4 - 1
system/author/css/style.css

@@ -67,9 +67,12 @@ a, a:link, a:visited, a:focus, a:hover, a:active, .link, button, .button, .tab-b
 .hover-bg-tm-green:hover{
 	background:#66b0a3;
 }
-.hover-b--tm-green:hover{
+.hover-b--tm-green:hover,.hover-b--tm-green.active{
 	border-color:#66b0a3;
 }
+.hover-bg-white.active{
+	background-color: #fff;
+}
 .w-100{
 	width:100%!important;
 }

+ 16 - 0
system/author/layouts/layout.twig

@@ -41,6 +41,22 @@
 					<title>power-off</title>
 					<path d="M24 14c0 6.609-5.391 12-12 12s-12-5.391-12-12c0-3.797 1.75-7.297 4.797-9.578 0.891-0.672 2.141-0.5 2.797 0.391 0.672 0.875 0.484 2.141-0.391 2.797-2.031 1.531-3.203 3.859-3.203 6.391 0 4.406 3.594 8 8 8s8-3.594 8-8c0-2.531-1.172-4.859-3.203-6.391-0.875-0.656-1.062-1.922-0.391-2.797 0.656-0.891 1.922-1.062 2.797-0.391 3.047 2.281 4.797 5.781 4.797 9.578zM14 2v10c0 1.094-0.906 2-2 2s-2-0.906-2-2v-10c0-1.094 0.906-2 2-2s2 0.906 2 2z"></path>
 				</symbol>
+				<symbol id="icon-user" viewBox="0 0 20 28">
+					<path d="M20 21.859c0 2.281-1.5 4.141-3.328 4.141h-13.344c-1.828 0-3.328-1.859-3.328-4.141 0-4.109 1.016-8.859 5.109-8.859 1.266 1.234 2.984 2 4.891 2s3.625-0.766 4.891-2c4.094 0 5.109 4.75 5.109 8.859zM16 8c0 3.313-2.688 6-6 6s-6-2.688-6-6 2.688-6 6-6 6 2.688 6 6z"></path>
+				</symbol>
+				<symbol id="icon-group" viewBox="0 0 30 28">
+					<path d="M9.266 14c-1.625 0.047-3.094 0.75-4.141 2h-2.094c-1.563 0-3.031-0.75-3.031-2.484 0-1.266-0.047-5.516 1.937-5.516 0.328 0 1.953 1.328 4.062 1.328 0.719 0 1.406-0.125 2.078-0.359-0.047 0.344-0.078 0.688-0.078 1.031 0 1.422 0.453 2.828 1.266 4zM26 23.953c0 2.531-1.672 4.047-4.172 4.047h-13.656c-2.5 0-4.172-1.516-4.172-4.047 0-3.531 0.828-8.953 5.406-8.953 0.531 0 2.469 2.172 5.594 2.172s5.063-2.172 5.594-2.172c4.578 0 5.406 5.422 5.406 8.953zM10 4c0 2.203-1.797 4-4 4s-4-1.797-4-4 1.797-4 4-4 4 1.797 4 4zM21 10c0 3.313-2.688 6-6 6s-6-2.688-6-6 2.688-6 6-6 6 2.688 6 6zM30 13.516c0 1.734-1.469 2.484-3.031 2.484h-2.094c-1.047-1.25-2.516-1.953-4.141-2 0.812-1.172 1.266-2.578 1.266-4 0-0.344-0.031-0.688-0.078-1.031 0.672 0.234 1.359 0.359 2.078 0.359 2.109 0 3.734-1.328 4.062-1.328 1.984 0 1.937 4.25 1.937 5.516zM28 4c0 2.203-1.797 4-4 4s-4-1.797-4-4 1.797-4 4-4 4 1.797 4 4z"></path>
+				</symbol>
+				<symbol id="icon-wrench" viewBox="0 0 26 28">
+					<path d="M6 23c0-0.547-0.453-1-1-1s-1 0.453-1 1 0.453 1 1 1 1-0.453 1-1zM16.063 16.438l-10.656 10.656c-0.359 0.359-0.875 0.578-1.406 0.578s-1.047-0.219-1.422-0.578l-1.656-1.687c-0.375-0.359-0.594-0.875-0.594-1.406s0.219-1.047 0.594-1.422l10.641-10.641c0.812 2.047 2.453 3.687 4.5 4.5zM25.969 9.641c0 0.516-0.187 1.156-0.359 1.656-0.984 2.781-3.656 4.703-6.609 4.703-3.859 0-7-3.141-7-7s3.141-7 7-7c1.141 0 2.625 0.344 3.578 0.984 0.156 0.109 0.25 0.25 0.25 0.438 0 0.172-0.109 0.344-0.25 0.438l-4.578 2.641v3.5l3.016 1.672c0.516-0.297 4.141-2.578 4.453-2.578s0.5 0.234 0.5 0.547z"></path>
+				</symbol>
+				<symbol id="icon-plug" viewBox="0 0 28 28">
+					<path d="M27.422 7.078c0.766 0.781 0.766 2.047 0 2.828l-6.266 6.25 2.344 2.344-2.5 2.5c-3.422 3.422-8.641 3.906-12.516 1.344l-5.656 5.656h-2.828v-2.828l5.656-5.656c-2.562-3.875-2.078-9.094 1.344-12.516l2.5-2.5 2.344 2.344 6.25-6.266c0.781-0.766 2.047-0.766 2.828 0 0.781 0.781 0.781 2.063 0 2.828l-6.25 6.266 3.656 3.656 6.266-6.25c0.781-0.781 2.047-0.781 2.828 0z"></path>
+				</symbol>
+				<symbol id="icon-paint-brush" viewBox="0 0 28 28">
+					<path d="M25.234 0c1.422 0 2.734 1.062 2.734 2.547 0 0.828-0.328 1.625-0.703 2.359-1.219 2.312-5.313 9.953-7.266 11.75-0.953 0.891-2.078 1.422-3.406 1.422-2.641 0-4.797-2.25-4.797-4.875 0-1.25 0.516-2.469 1.437-3.313l9.969-9.047c0.547-0.5 1.266-0.844 2.031-0.844zM11.031 16.156c0.812 1.578 2.297 2.766 4.016 3.219l0.016 1.109c0.094 4.453-3 7.516-7.469 7.516-5.297 0-7.594-4.219-7.594-9.016 0.578 0.391 2.594 2 3.25 2 0.391 0 0.719-0.219 0.859-0.578 1.328-3.469 3.406-4.094 6.922-4.25z"></path>
+				</symbol>
+				{{ assets.renderSvg() }}
 			</defs>
 		</svg>
 		

+ 6 - 13
system/author/partials/aside.twig

@@ -1,19 +1,12 @@
 <nav id="sidebar-menu" class="sidebar-menu">
 	{% if is_role('administrator') %}
 		<div id="mobile-menu" class="menu-action">{{ __('Menu') }} <span class="button-arrow"></span></div>
-		<h3>{{ __('Settings') }}</h3>
-		<ul class="menu-list margin-bottom">
-			<li class="menu-item"><a href="{{ path_for('settings.show') }}"{{ (route == 'settings.show') ? ' class="active"' : '' }}>{{ __('System') }}</a></li>
-			<li class="menu-item"><a href="{{ path_for('themes.show') }}"{{ (route == 'themes.show') ? ' class="active"' : '' }}>{{ __('Themes') }}</a></li>
-			<li class="menu-item"><a href="{{ path_for('plugins.show') }}"{{ (route == 'plugins.show') ? ' class="active"' : '' }}>{{ __('Plugins') }}</a></li>
-		</ul>
-		<h3>{{ __('Users') }}</h3>
-		<ul class="menu-list">
-			<li class="menu-item"><a href="{{ path_for('user.list') }}"{{ (route == 'user.list') ? ' class="active"' : '' }}>{{ __('All users') }}</a></li>
-			<li class="menu-item"><a href="{{ path_for('user.new') }}"{{  (route == 'user.new') ? ' class="active"' : '' }}>{{ __('Create user') }}</a></li>
-			{% for user in users %}
-				<li class="menu-item"><a href="{{ path_for('user.show', {'username' : user }) }}"{{ (username == user) ? ' class="active"' : '' }}>{{ user }}</a></li>
-			{% endfor %}
+		<ul class="list pa0 ma0">
+			<li class="pb1"><a class="link dark-gray hover-bg-white bl bw2 b--near-white hover-b--tm-green pa2 dib w-100{{ (route == 'settings.show') ? ' active' : '' }}" href="{{ path_for('settings.show') }}"><svg class="icon icon-wrench mr2"><use xlink:href="#icon-wrench"></use></svg> {{ __('System') }}</a></li>
+			<li class="pb1"><a class="link dark-gray hover-bg-white bl bw2 b--near-white hover-b--tm-green pa2 dib w-100{{ (route == 'themes.show') ? ' active' : '' }}" href="{{ path_for('themes.show') }}"><svg class="icon icon-paint-brush mr2"><use xlink:href="#icon-paint-brush"></use></svg> {{ __('Themes') }}</a></li>
+			<li class="pb1"><a class="link dark-gray hover-bg-white bl bw2 b--near-white hover-b--tm-green pa2 dib w-100{{ (route == 'plugins.show') ? ' active' : '' }}" href="{{ path_for('plugins.show') }}"><svg class="icon icon-plug mr2"><use xlink:href="#icon-plug"></use></svg> {{ __('Plugins') }}</a></li>
+			<li class="pb1"><a class="link dark-gray hover-bg-white bl bw2 b--near-white hover-b--tm-green pa2 dib w-100{{ (route == 'user.account') ? ' active' : '' }}" href="{{ path_for('user.account') }}"><svg class="icon icon-user mr2"><use xlink:href="#icon-user"></use></svg> {{ __('Account') }}</a></li>
+			<li class="pb1"><a class="link dark-gray hover-bg-white bl bw2 b--near-white hover-b--tm-green pa2 dib w-100{{ (route == 'user.list') ? ' active' : '' }}" href="{{ path_for('user.list') }}"><svg class="icon icon-group mr2"><use xlink:href="#icon-group"></use></svg> {{ __('Users') }}</a></li>
 		</ul>
 	{% endif %}
 </nav>

+ 2 - 2
system/author/partials/fields.twig

@@ -1,5 +1,5 @@
-<div class="cardField{{ errors[itemName][field.name] ? ' error' : '' }}{{field.fieldsize ? ' ' ~ field.fieldsize : ''}}">
-
+<div class="{{ class ? class : 'cardField' }}{{ errors[itemName][field.name] ? ' error' : '' }}{{field.fieldsize ? ' ' ~ field.fieldsize : ''}}">
+	
 	<label for="{{ itemName }}[{{ field.name }}]">{{ __( field.getLabel() ) }}
 		{% if field.getAttribute('required') %}<strong><abbr title="{{ __('required') }}">*</abbr></strong>{% endif %}
 		{% if field.help %}<div class="help">?<span class="tooltip">{{__(field.help|slice(0,100))}}</span></div>{% endif %}

+ 18 - 63
system/author/settings/user.twig

@@ -5,79 +5,35 @@
 	
 	<div class="formWrapper">
 
-		<form id="userform" method="POST" action="{{ path_for('user.update') }}">
+		<form id="userform" method="POST" action="{{ path_for('user.update') }}" enctype="multipart/form-data">
 		
 			<section id="user" class="settings">
 
 				<header class="headline">
-					<h1>{{ __('Edit User') }}</h1>
+					<h1>{{ userdata.username }}</h1>
 				</header>
 				
 				<fieldset class="auth">
-				
-					<div class="large{{ errors.username ? ' errors' : '' }}">
-						<label for="username">{{ __('Username') }} <small>({{ __('not editable') }})</small></label>
-						<input type="text" name="showusername" value="{{ old.username ? old.username : userdata.username }}" required disabled>
-						<input type="hidden" name="username" value="{{ userdata.username }}">
-						{% if errors.username %}
-							<span class="error">{{ errors.username | first }}</span>
-						{% endif %}
-					</div>
-
-					<div class="large{{ errors.firstname ? ' errors' : '' }}">
-						<label for="firstname">{{ __('First Name') }}</label>
-						<input type="text" name="firstname" value="{{ old.firstname ? old.firstname : userdata.firstname }}">
-						{% if errors.firstname %}
-							<span class="error">{{ errors.firstname | first }}</span>
-						{% endif %}
-					</div>
 
-					<div class="large{{ errors.lastname ? ' errors' : '' }}">
-						<label for="lastname">{{ __('Last Name') }}</label>
-						<input type="text" name="lastname" value="{{ old.lastname ? old.lastname : userdata.lastname }}">
-						{% if errors.lastname %}
-							<span class="error">{{ errors.lastname | first }}</span>
-						{% endif %}
-					</div>
-					
-					<div class="large{{ errors.email ? ' errors' : '' }}">
-						<label for="email">{{ __('E-Mail') }} <abbr title="{{ __('required') }}">*</abbr></label>
-						<input type="text" name="email" value="{{ old.email ? old.email : userdata.email }}" required>
-						{% if errors.email %}
-							<span class="error">{{ errors.email | first }}</span>
-						{% endif %}
-					</div>
+					{% for field in userform %}
 
-					{% if is_role('administrator') %}
-						<div class="large{{ errors.userrole ? ' errors' : '' }}">
-							<label for="userrole">{{ __('Role') }} <abbr title="{{ __('required') }}">*</abbr></label>
-							<select name="userrole" required>
-								{% for role in userrole %}
-									<option value="{{ role }}"{% if (role == old.userrole or role == userdata.userrole) %} selected{% endif %}>{{ role }}</option>
+						{% if field.type == 'fieldset' %}
+											
+							<fieldset class="subfield">
+								<legend>{{ field.legend }}</legend>
+								{% for field in field.fields %}
+									{% include '/partials/fields.twig' with { 'settings': usersettings, 'object' : 'user', 'itemName' : userdata.username, 'class' : 'large' } %}
 								{% endfor %}
-							</select>
-							{% if errors.userrole %}
-								<span class="error">{{ errors.userrole | first }}</span>
+							</fieldset>
+						
+							{% else %}
+								
+								{% include '/partials/fields.twig' with { 'settings': usersettings, 'object' : 'user', 'itemName' : userdata.username, 'class' : 'large' } %}
+
 							{% endif %}
-						</div>
-					{% endif %}
-					
-					<div class="large{{ errors.password ? ' errors' : '' }}">
-						<label for="password">{{ __('Actual Password') }}</label>
-						<input type="password" name="password">
-						{% if errors.password %}
-							<span class="error">{{ errors.password | first }}</span>
-						{% endif %}
-					</div>
+								
+						{% endfor %}
 
-					<div class="large{{ errors.newpassword ? ' errors' : '' }}">
-						<label for="newpassword">{{ __('New Password') }}</label>
-						<input type="password" name="newpassword">
-						{% if errors.newpassword %}
-							<span class="error">{{ errors.newpassword | first }}</span>
-						{% endif %}
-					</div>
-					
 				</fieldset>
 
 			</section>
@@ -87,7 +43,7 @@
 			<div class="actionLink">
 				<a href="#" id="openModal" class="openModal">{{ __('Delete User') }}</a>
 			</div>
-		</form>		
+		</form>
 	</div>
 	
 	<div id="modalWindow" class="modal">
@@ -103,5 +59,4 @@
 		</div>
 	</div>
 	
-	
 {% endblock %}

+ 28 - 0
system/system.php

@@ -3,6 +3,8 @@
 use Typemill\Events\OnSettingsLoaded;
 use Typemill\Events\OnPluginsLoaded;
 use Typemill\Events\OnSessionSegmentsLoaded;
+use Typemill\Events\OnRolesPermissionsLoaded;
+use Typemill\Events\OnResourcesLoaded;
 
 /****************************
 * HIDE ERRORS BY DEFAULT	  *
@@ -109,6 +111,32 @@ $dispatcher->dispatch('onPluginsLoaded', new OnPluginsLoaded($pluginNames));
 # dispatch settings event and get all setting-updates from plugins
 $dispatcher->dispatch('onSettingsLoaded', new OnSettingsLoaded($settings))->getData();
 
+
+/**********************************
+* 	LOAD ROLES AND PERMISSIONS 	  *
+**********************************/
+
+# load roles and permissions
+$rolesAndPermissions = Typemill\Settings::loadRolesAndPermissions();
+
+# dispatch roles so plugins can enhance them
+$rolesAndPermissions = $dispatcher->dispatch('onRolesPermissionsLoaded', new OnRolesPermissionsLoaded($rolesAndPermissions))->getData();
+
+# load resources
+$resources = Typemill\Settings::loadResources();
+
+# dispatch roles so plugins can enhance them
+$resources = $dispatcher->dispatch('onResourcesLoaded', new OnResourcesLoaded($resources))->getData();
+
+# create acl-object
+$acl = Typemill\Settings::createAcl($rolesAndPermissions, $resources);
+
+# add acl to container
+$container['acl'] = function($c) use ($acl)
+{
+	return $acl;
+};
+
 /******************************
 * ADD DISPATCHER TO CONTAINER *
 ******************************/