euromark 11 years ago
parent
commit
fda13552cf

+ 3 - 0
README.md

@@ -9,7 +9,10 @@ A CakePHP 3.x Plugin containing several useful tools that can be used in many pr
 ## Version notice
 
 This cake3 branch only works for **CakePHP3.x** - please use the master branch for CakePHP 2.x!
+**It is still dev** (not even alpha), please be careful with using it.
 
+### Planned Release Cycle:
+Dev (currently), Alpha, Beta, RC, 1.0 stable (incl. tagged release then).
 
 ## How to include
 Installing the Plugin is pretty much as with every other CakePHP Plugin.

+ 2 - 0
config/bootstrap.php

@@ -0,0 +1,2 @@
+<?php
+namespace Tools\config;

+ 178 - 0
src/Auth/AuthUserTrait.php

@@ -0,0 +1,178 @@
+<?php
+
+namespace Tools\Auth;
+
+use Cake\Utility\Hash;
+
+if (!defined('USER_ROLE_KEY')) {
+	define('USER_ROLE_KEY', 'Role');
+}
+if (!defined('USER_RIGHT_KEY')) {
+	define('USER_RIGHT_KEY', 'Right');
+}
+
+/**
+ * Convenience wrapper to access Auth data and check on rights/roles.
+ *
+ * It can be used anywhere in the application due to static access.
+ * So in the view we can use this shortcut to check if a user is logged in:
+ *
+ *   if (Auth::id()) {
+ *     // Display element
+ *   }
+ *
+ * Simply add it at the class file:
+ *
+ *   trait AuthUserTrait;
+ *
+ * But needs
+ *
+ *   protected function _getUser() {}
+ *
+ * to be implemented in the using class.
+ *
+ * Expects the Role session infos to be either
+ * 	- `Auth.User.role_id` (single) or
+ * 	- `Auth.User.Role` (multi - flat array of roles, or array role data)
+ * and can be adjusted via constants and defined().
+ * Same goes for Right data.
+ *
+ * Note: This uses AuthComponent internally to work with both stateful and stateless auth.
+ *
+ * @author Mark Scherer
+ * @license MIT
+ */
+trait AuthUserTrait {
+
+	/**
+	 * Get the user id of the current session.
+	 *
+	 * This can be used anywhere to check if a user is logged in.
+	 *
+	 * @return mixed User id if existent, null otherwise.
+	 */
+	public function id() {
+		return $this->user('id');
+	}
+
+	/**
+	 * Get the role(s) of the current session.
+	 *
+	 * It will return the single role for single role setup, and a flat
+	 * list of roles for multi role setup.
+	 *
+	 * @return mixed String or array of roles or null if inexistent.
+	 */
+	public function roles() {
+		$roles = $this->user(USER_ROLE_KEY);
+		if (!is_array($roles)) {
+			return $roles;
+		}
+		if (isset($roles[0]['id'])) {
+			$roles = Hash::extract($roles, '{n}.id');
+		}
+		return $roles;
+	}
+
+	/**
+	 * Get the user data of the current session.
+	 *
+	 * @param string $key Key in dot syntax.
+	 * @return mixed Data
+	 */
+	public function user($key = null) {
+		return Hash::get($this->_getUser(), $key);
+	}
+
+	/**
+	 * Check if the current session has this role.
+	 *
+	 * @param mixed $role
+	 * @param mixed $providedRoles
+	 * @return bool Success
+	 */
+	public function hasRole($ownRole, $providedRoles = null) {
+		if ($providedRoles !== null) {
+			$roles = $providedRoles;
+		} else {
+			$roles = $this->roles();
+		}
+		if (is_array($roles)) {
+			if (in_array($ownRole, $roles)) {
+				return true;
+			}
+		} elseif (!empty($roles)) {
+			if ($ownRole == $roles) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	/**
+	 * Check if the current session has one of these roles.
+	 *
+	 * You can either require one of the roles (default), or you can require all
+	 * roles to match.
+	 *
+	 * @param mixed $roles
+	 * @param bool $oneRoleIsEnough (if all $roles have to match instead of just one)
+	 * @param mixed $providedRoles
+	 * @return bool Success
+	 */
+	public function hasRoles($ownRoles, $oneRoleIsEnough = true, $providedRoles = null) {
+		if ($providedRoles !== null) {
+			$roles = $providedRoles;
+		} else {
+			$roles = $this->roles();
+		}
+		$ownRoles = (array)$ownRoles;
+		if (empty($ownRoles)) {
+			return false;
+		}
+		$count = 0;
+		foreach ($ownRoles as $role) {
+			if ($this->hasRole($role, $roles)) {
+				if ($oneRoleIsEnough) {
+					return true;
+				}
+				$count++;
+			} else {
+				if (!$oneRoleIsEnough) {
+					return false;
+				}
+			}
+		}
+
+		if ($count === count($ownRoles)) {
+			return true;
+		}
+		return false;
+	}
+
+	/**
+	 * Check if the current session has this right.
+	 *
+	 * Rights can be an additional element to give permissions, e.g.
+	 * the right to send messages/emails, to friend request other users,...
+	 * This can be set via Right model and stored in the Auth array upon login
+	 * the same way the roles are.
+	 *
+	 * @param mixed $role
+	 * @param mixed $providedRights
+	 * @return bool Success
+	 */
+	public function hasRight($ownRight, $providedRights = null) {
+		if ($providedRights !== null) {
+			$rights = $providedRights;
+		} else {
+			$rights = $this->user(USER_RIGHT_KEY);
+		}
+		$rights = (array)$rights;
+		if (array_key_exists($ownRight, $rights) && !empty($rights[$ownRight])) {
+			return true;
+		}
+		return false;
+	}
+
+}

+ 34 - 0
src/Controller/Component/AuthUserComponent.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace Tools\Controller\Component;
+
+use Cake\Controller\Component;
+use Cake\Event\Event;
+use Tools\Auth\AuthUserTrait;
+
+/**
+ * Authentication User component class
+ */
+class AuthUserComponent extends Component {
+
+	use AuthUserTrait;
+
+	public $components = array('Session', 'Auth');
+
+	/**
+	 * AuthUserComponent::beforeRender()
+	 *
+	 * @param Event $event
+	 * @return void
+	 */
+	public function beforeRender(Event $event) {
+		$controller = $event->subject();
+		$authUser = $this->_getUser();
+		$controller->set(compact('authUser'));
+	}
+
+	protected function _getUser() {
+		return (array)$this->Auth->user();
+	}
+
+}

+ 390 - 0
src/Controller/Component/CommonComponent.php

@@ -0,0 +1,390 @@
+<?php
+namespace Tools\Controller\Component;
+
+use Cake\Controller\Component;
+use Cake\Core\Configure;
+use Cake\Event\Event;
+use Tools\Utility\Utility;
+
+use Tools\Lib\UserAgentLib;
+
+if (!defined('CLASS_USER')) {
+	define('CLASS_USER', 'User');
+}
+
+/**
+ * A component included in every app to take care of common stuff.
+ *
+ * @author Mark Scherer
+ * @copyright 2012 Mark Scherer
+ * @license MIT
+ */
+class CommonComponent extends Component {
+
+	public $components = array('Session', 'RequestHandler');
+
+	public $userModel = CLASS_USER;
+
+	/**
+	 * For this helper the controller has to be passed as reference
+	 * for manual startup with $disableStartup = true (requires this to be called prior to any other method)
+	 *
+	 * @return void
+	 */
+	public function startup(Event $event) {
+		$this->Controller = $event->subject();
+
+		// Data preparation
+		if (!empty($this->Controller->request->data) && !Configure::read('DataPreparation.notrim')) {
+			$this->Controller->request->data = $this->trimDeep($this->Controller->request->data);
+		}
+		if (!empty($this->Controller->request->query) && !Configure::read('DataPreparation.notrim')) {
+			$this->Controller->request->query = $this->trimDeep($this->Controller->request->query);
+		}
+		if (!empty($this->Controller->request->params['pass']) && !Configure::read('DataPreparation.notrim')) {
+			$this->Controller->request->params['pass'] = $this->trimDeep($this->Controller->request->params['pass']);
+		}
+		/*
+		// Auto layout switch
+		if ($this->Controller->request->is('ajax')) {
+			$this->Controller->layout = 'ajax';
+		}
+		*/
+	}
+
+	/**
+	 * Called after the Controller::beforeRender(), after the view class is loaded, and before the
+	 * Controller::render()
+	 *
+	 * @param object $Controller Controller with components to beforeRender
+	 * @return void
+	 */
+	public function beforeRender(Event $event) {
+		if ($messages = $this->Session->read('Message')) {
+			foreach ($messages as $message) {
+				$this->flashMessage($message['message'], 'error');
+			}
+			$this->Session->delete('Message');
+		}
+
+		if ($this->Controller->request->is('ajax')) {
+			$ajaxMessages = array_merge(
+				(array)$this->Session->read('messages'),
+				(array)Configure::read('messages')
+			);
+			// The header can be read with JavaScript and a custom Message can be displayed
+			$this->Controller->response->header('X-Ajax-Flashmessage', json_encode($ajaxMessages));
+
+			$this->Session->delete('messages');
+		}
+
+		// Custom options
+		if (isset($Controller->options)) {
+			$Controller->set('options', $Controller->options);
+		}
+	}
+
+	/**
+	 * List all direct actions of a controller
+	 *
+	 * @return array Actions
+	 */
+	public function listActions() {
+		$class = Inflector::camelize($this->Controller->name) . 'Controller';
+		$parentClassMethods = get_class_methods(get_parent_class($class));
+		$subClassMethods = get_class_methods($class);
+		$classMethods = array_diff($subClassMethods, $parentClassMethods);
+		foreach ($classMethods as $key => $value) {
+			if (substr($value, 0, 1) === '_') {
+				unset($classMethods[$key]);
+			}
+		}
+		return $classMethods;
+	}
+
+	/**
+	 * Convenience method to check on POSTED data.
+	 * Doesn't matter if it's POST, PUT or PATCH.
+	 *
+	 * Note that you can also use request->is(array('post', 'put', 'patch') directly.
+	 *
+	 * @return bool If it is of type POST/PUT/PATCH
+	 */
+	public function isPosted() {
+		return $this->Controller->request->is(array('post', 'put', 'patch'));
+	}
+
+	/**
+	 * Adds a flash message.
+	 * Updates "messages" session content (to enable multiple messages of one type).
+	 *
+	 * @param string $message Message to output.
+	 * @param string $type Type ('error', 'warning', 'success', 'info' or custom class).
+	 * @return void
+	 */
+	public function flashMessage($message, $type = null) {
+		if (!$type) {
+			$type = 'info';
+		}
+
+		$old = (array)$this->Session->read('messages');
+		if (isset($old[$type]) && count($old[$type]) > 99) {
+			array_shift($old[$type]);
+		}
+		$old[$type][] = $message;
+		$this->Session->write('messages', $old);
+	}
+
+	/**
+	 * Adds a transient flash message.
+	 * These flash messages that are not saved (only available for current view),
+	 * will be merged into the session flash ones prior to output.
+	 *
+	 * @param string $message Message to output.
+	 * @param string $type Type ('error', 'warning', 'success', 'info' or custom class).
+	 * @return void
+	 */
+	public static function transientFlashMessage($message, $type = null) {
+		if (!$type) {
+			$type = 'info';
+		}
+
+		$old = (array)Configure::read('messages');
+		if (isset($old[$type]) && count($old[$type]) > 99) {
+			array_shift($old[$type]);
+		}
+		$old[$type][] = $message;
+		Configure::write('messages', $old);
+	}
+
+	/**
+	 * Add component just in time (inside actions - only when needed)
+	 * aware of plugins and config array (if passed)
+	 * @param mixed $components (single string or multiple array)
+	 * @poaram bool $callbacks (defaults to true)
+	 */
+	public function loadComponent($components = array(), $callbacks = true) {
+		foreach ((array)$components as $component => $config) {
+			if (is_int($component)) {
+				$component = $config;
+				$config = array();
+			}
+			list($plugin, $componentName) = pluginSplit($component);
+			if (isset($this->Controller->{$componentName})) {
+				continue;
+			}
+
+			$this->Controller->{$componentName} = $this->Controller->Components->load($component, $config);
+			if (!$callbacks) {
+				continue;
+			}
+			if (method_exists($this->Controller->{$componentName}, 'initialize')) {
+				$this->Controller->{$componentName}->initialize($this->Controller);
+			}
+			if (method_exists($this->Controller->{$componentName}, 'startup')) {
+				$this->Controller->{$componentName}->startup($this->Controller);
+			}
+		}
+	}
+
+	/**
+	 * Used to get the value of a passed param.
+	 *
+	 * @param mixed $var
+	 * @param mixed $default
+	 * @return mixed
+	 */
+	public function getPassedParam($var, $default = null) {
+		return (isset($this->Controller->request->params['pass'][$var])) ? $this->Controller->request->params['pass'][$var] : $default;
+	}
+
+	/**
+	 * Returns defaultUrlParams including configured prefixes.
+	 *
+	 * @return array Url params
+	 */
+	public static function defaultUrlParams() {
+		$defaults = array('plugin' => false);
+		$prefixes = (array)Configure::read('Routing.prefixes');
+		foreach ($prefixes as $prefix) {
+			$defaults[$prefix] = false;
+		}
+		return $defaults;
+	}
+
+	/**
+	 * Returns current url (with all missing params automatically added).
+	 * Necessary for Router::url() and comparison of urls to work.
+	 *
+	 * @param bool $asString: defaults to false = array
+	 * @return mixed Url
+	 */
+	public function currentUrl($asString = false) {
+		if (isset($this->Controller->request->params['prefix']) && mb_strpos($this->Controller->request->params['action'], $this->Controller->request->params['prefix']) === 0) {
+			$action = mb_substr($this->Controller->request->params['action'], mb_strlen($this->Controller->request->params['prefix']) + 1);
+		} else {
+			$action = $this->Controller->request->params['action'];
+		}
+
+		$url = array_merge($this->Controller->request->params['named'], $this->Controller->request->params['pass'], array('prefix' => isset($this->Controller->request->params['prefix']) ? $this->Controller->request->params['prefix'] : null,
+			'plugin' => $this->Controller->request->params['plugin'], 'action' => $action, 'controller' => $this->Controller->request->params['controller']));
+
+		if ($asString === true) {
+			return Router::url($url);
+		}
+		return $url;
+	}
+
+	/**
+	 * Smart Referer Redirect - will try to use an existing referer first
+	 * otherwise it will use the default url
+	 *
+	 * @param mixed $url
+	 * @param bool $allowSelf if redirect to the same controller/action (url) is allowed
+	 * @param int $status
+	 * @return void
+	 */
+	public function autoRedirect($whereTo, $allowSelf = true, $status = null) {
+		if ($allowSelf || $this->Controller->referer(null, true) !== '/' . $this->Controller->request->url) {
+			$this->Controller->redirect($this->Controller->referer($whereTo, true), $status);
+		}
+		$this->Controller->redirect($whereTo, $status);
+	}
+
+	/**
+	 * Should be a 303, but:
+	 * Note: Many pre-HTTP/1.1 user agents do not understand the 303 status. When interoperability with such clients is a concern, the 302 status code may be used instead, since most user agents react to a 302 response as described here for 303.
+	 *
+	 * TODO: change to 303 with backwardscompatability for older browsers...
+	 *
+	 * @see http://en.wikipedia.org/wiki/Post/Redirect/Get
+	 * @param mixed $url
+	 * @param int $status
+	 * @return void
+	 */
+	public function postRedirect($whereTo, $status = 302) {
+		$this->Controller->redirect($whereTo, $status);
+	}
+
+	/**
+	 * Combine auto with post
+	 * also allows whitelisting certain actions for autoRedirect (use Controller::$autoRedirectActions)
+	 * @param mixed $url
+	 * @param bool $conditionalAutoRedirect false to skip whitelisting
+	 * @param int $status
+	 * @return void
+	 */
+	public function autoPostRedirect($whereTo, $conditionalAutoRedirect = true, $status = 302) {
+		$referer = $this->Controller->referer($whereTo, true);
+		if (!$conditionalAutoRedirect && !empty($referer)) {
+			$this->postRedirect($referer, $status);
+		}
+
+		if (!empty($referer)) {
+			$referer = Router::parse($referer);
+		}
+
+		if (!$conditionalAutoRedirect || empty($this->Controller->autoRedirectActions) || is_array($referer) && !empty($referer['action'])) {
+			// Be sure that controller offset exists, otherwise you
+			// will run into problems, if you use url rewriting.
+			$refererController = null;
+			if (isset($referer['controller'])) {
+				$refererController = Inflector::camelize($referer['controller']);
+			}
+			// fixme
+			if (!isset($this->Controller->autoRedirectActions)) {
+				$this->Controller->autoRedirectActions = array();
+			}
+			foreach ($this->Controller->autoRedirectActions as $action) {
+				list($controller, $action) = pluginSplit($action);
+				if (!empty($controller) && $refererController !== '*' && $refererController != $controller) {
+					continue;
+				}
+				if (empty($controller) && $refererController != Inflector::camelize($this->Controller->request->params['controller'])) {
+					continue;
+				}
+				if (!in_array($referer['action'], $this->Controller->autoRedirectActions)) {
+					continue;
+				}
+				$this->autoRedirect($whereTo, true, $status);
+			}
+		}
+		$this->postRedirect($whereTo, $status);
+	}
+
+	/**
+	 * Automatically add missing url parts of the current url including
+	 * - querystring (especially for 3.x then)
+	 * - named params (until 3.x when they will become deprecated)
+	 * - passed params
+	 *
+	 * @param mixed $url
+	 * @param int $status
+	 * @param bool $exit
+	 * @return void
+	 */
+	public function completeRedirect($url = null, $status = null, $exit = true) {
+		if ($url === null) {
+			$url = $this->Controller->request->params;
+			unset($url['named']);
+			unset($url['pass']);
+			unset($url['isAjax']);
+		}
+		if (is_array($url)) {
+			$url += $this->Controller->request->params['named'];
+			$url += $this->Controller->request->params['pass'];
+		}
+		return $this->Controller->redirect($url, $status, $exit);
+	}
+
+	/**
+	 * Only redirect to itself if cookies are on
+	 * Prevents problems with lost data
+	 * Note: Many pre-HTTP/1.1 user agents do not understand the 303 status. When interoperability with such clients is a concern, the 302 status code may be used instead, since most user agents react to a 302 response as described here for 303.
+	 *
+	 * @see http://en.wikipedia.org/wiki/Post/Redirect/Get
+	 * TODO: change to 303 with backwardscompatability for older browsers...
+	 * @param int $status
+	 * @return void
+	 */
+	public function prgRedirect($status = 302) {
+		if (!empty($_COOKIE[Configure::read('Session.cookie')])) {
+			$this->Controller->redirect('/' . $this->Controller->request->url, $status);
+		}
+	}
+
+	/**
+	 * Set headers to cache this request.
+	 * Opposite of Controller::disableCache()
+	 * TODO: set response class header instead
+	 *
+	 * @param int $seconds
+	 * @return void
+	 */
+	public function forceCache($seconds = HOUR) {
+		$this->Controller->response->header('Cache-Control', 'public, max-age=' . $seconds);
+		$this->Controller->response->header('Last-modified', gmdate("D, j M Y H:i:s", time()) . " GMT");
+		$this->Controller->response->header('Expires', gmdate("D, j M Y H:i:s", time() + $seconds) . " GMT");
+	}
+
+	/**
+	 * Referrer checking (where does the user come from)
+	 * Only returns true for a valid external referrer.
+	 *
+	 * @return bool Success
+	 */
+	public function isForeignReferer($ref = null) {
+		if ($ref === null) {
+			$ref = env('HTTP_REFERER');
+		}
+		if (!$ref) {
+			return false;
+		}
+		$base = Configure::read('App.fullBaseUrl') . '/';
+		if (strpos($ref, $base) === 0) {
+			return false;
+		}
+		return true;
+	}
+
+}

+ 38 - 0
src/Controller/Controller.php

@@ -0,0 +1,38 @@
+<?php
+namespace Tools\Controller;
+
+use Cake\Controller\Controller as CakeController;
+
+/**
+ * DRY Controller stuff
+ */
+class Controller extends CakeController {
+
+	/**
+	 * Add headers for IE8 etc to fix caching issues in those stupid browsers
+	 *
+	 * @return void
+	 */
+	public function disableCache() {
+		$this->response->header(array(
+			'Pragma' => 'no-cache',
+		));
+		$this->response->disableCache();
+	}
+
+	/**
+	 * Handles automatic pagination of model records.
+	 *
+	 * @overwrite to support defaults like limit, querystring settings
+	 * @param \Cake\ORM\Table|string|\Cake\ORM\Query $object Table to paginate
+	 *   (e.g: Table instance, 'TableName' or a Query object)
+	 * @return \Cake\ORM\ResultSet Query results
+	 */
+	public function paginate($object = null) {
+		if ($defaultSettings = (array)Configure::read('Paginator')) {
+			$this->paginate += $defaultSettings;
+		}
+		return parent::paginate($object);
+	}
+
+}

+ 7 - 0
src/Model/Entity/Entity.php

@@ -0,0 +1,7 @@
+<?php
+namespace Tools\Model\Entity;
+
+use Cake\ORM\Entity as CakeEntity;
+
+class Entity extends CakeEntity {
+}

+ 138 - 0
src/Model/Table/Table.php

@@ -0,0 +1,138 @@
+<?php
+
+namespace Tools\Model\Table;
+
+use Cake\ORM\Table as CakeTable;
+use Cake\Validation\Validator;
+use Cake\Utility\Inflector;
+
+class Table extends CakeTable {
+
+	/**
+	 * initialize()
+	 *
+	 * @param mixed $config
+	 * @return void
+	 */
+	public function initialize(array $config) {
+		// Shims
+		$this->_shimRelations();
+
+		$this->addBehavior('Timestamp');
+	}
+
+	/**
+	 * Shim the 2.x way of class properties for relations.
+	 *
+	 * @return void
+	 */
+	protected function _shimRelations() {
+		if (!empty($this->belongsTo)) {
+			foreach ($this->belongsTo as $k => $v) {
+				if (is_int($k)) {
+					$k = $v;
+					$v = array();
+				}
+				if (!empty($v['className'])) {
+					$v['className'] = Inflector::pluralize($v['className']);
+				}
+				$v = array_filter($v);
+				$this->belongsTo(Inflector::pluralize($k), $v);
+			}
+		}
+		if (!empty($this->hasOne)) {
+			foreach ($this->hasOne as $k => $v) {
+				if (is_int($k)) {
+					$k = $v;
+					$v = array();
+				}
+				if (!empty($v['className'])) {
+					$v['className'] = Inflector::pluralize($v['className']);
+				}
+				$v = array_filter($v);
+				$this->hasOne(Inflector::pluralize($k), $v);
+			}
+		}
+		if (!empty($this->hasMany)) {
+			foreach ($this->hasMany as $k => $v) {
+				if (is_int($k)) {
+					$k = $v;
+					$v = array();
+				}
+				if (!empty($v['className'])) {
+					$v['className'] = Inflector::pluralize($v['className']);
+				}
+				$v = array_filter($v);
+				$this->hasMany(Inflector::pluralize($k), $v);
+			}
+		}
+		if (!empty($this->hasAndBelongsToMany)) {
+			foreach ($this->hasAndBelongsToMany as $k => $v) {
+				if (is_int($k)) {
+					$k = $v;
+					$v = array();
+				}
+				if (!empty($v['className'])) {
+					$v['className'] = Inflector::pluralize($v['className']);
+				}
+				$v = array_filter($v);
+				$this->belongsToMany(Inflector::pluralize($k), $v);
+			}
+		}
+	}
+
+	/**
+	 * Shim the 2.x way of validate class properties.
+	 *
+	 * @param Validator $validator
+	 * @return Validator
+	 */
+	public function validationDefault(Validator $validator) {
+		if (!empty($this->validate)) {
+			foreach ($this->validate as $k => $v) {
+				if (is_int($k)) {
+					$k = $v;
+					$v = array();
+				}
+				if (isset($v['required'])) {
+					$validator->validatePresence($k, $v['required']);
+					unset($v['required']);
+				}
+				if (isset($v['allowEmpty'])) {
+					$validator->allowEmpty($k, $v['allowEmpty']);
+					unset($v['allowEmpty']);
+				}
+				$validator->add($k, $v);
+			}
+		}
+
+		return $validator;
+	}
+
+	/**
+	 * Shim to provide 2.x way of find('first').
+	 *
+	 * @param string $type
+	 * @param array $options
+	 * @return Query
+	 */
+	public function find($type = 'all', $options = []) {
+		if ($type === 'first') {
+			return parent::find('all', $options)->first();
+		}
+		return parent::find($type, $options);
+	}
+
+	/**
+	 * truncate()
+	 *
+	 * @return void
+	 */
+	public function truncate() {
+		$sql = $this->schema()->truncateSql($this->_connection);
+		foreach ($sql as $snippet) {
+			$this->_connection->execute($snippet);
+		}
+	}
+
+}

+ 11 - 0
src/TestSuite/TestCase.php

@@ -0,0 +1,11 @@
+<?php
+namespace Tools\TestSuite;
+
+use Cake\TestSuite\TestCase as CakeTestCase;
+
+/**
+ * Tools TestCase class
+ *
+ */
+abstract class TestCase extends CakeTestCase {
+}

+ 31 - 0
src/View/Helper/AuthUserHelper.php

@@ -0,0 +1,31 @@
+<?php
+namespace Tools\View\Helper;
+
+use Cake\Core\Configure;
+use Cake\View\Helper;
+use Tools\Auth\AuthUserTrait;
+
+/**
+ * Helper to access auth user data.
+ *
+ * @author Mark Scherer
+ */
+class AuthUserHelper extends Helper {
+
+	use AuthUserTrait;
+
+	public $helpers = array('Session');
+
+	/**
+	 * AuthUserHelper::_getUser()
+	 *
+	 * @return array
+	 */
+	protected function _getUser() {
+		if (!isset($this->_View->viewVars['authUser'])) {
+			throw new \RuntimeException('AuthUser helper needs AuthUser component to function');
+		}
+		return $this->_View->viewVars['authUser'];
+	}
+
+}

+ 334 - 0
src/View/Helper/CommonHelper.php

@@ -0,0 +1,334 @@
+<?php
+namespace Tools\View\Helper;
+
+use Cake\Core\Configure;
+use Cake\View\Helper;
+
+/**
+ * Common helper
+ *
+ * @author Mark Scherer
+ */
+class CommonHelper extends Helper {
+
+	public $helpers = array('Session', 'Html');
+
+	/**
+	 * Display all flash messages.
+	 *
+	 * TODO: export div wrapping method (for static messaging on a page)
+	 *
+	 * @param array $types Types to output. Defaults to all if none are specified.
+	 * @return string HTML
+	 */
+	public function flash(array $types = array()) {
+		// Get the messages from the session
+		$messages = (array)$this->Session->read('messages');
+		$cMessages = (array)Configure::read('messages');
+		if (!empty($cMessages)) {
+			$messages = (array)Hash::merge($messages, $cMessages);
+		}
+		$html = '';
+		if (!empty($messages)) {
+			$html = '<div class="flash-messages flashMessages">';
+
+			if ($types) {
+				foreach ($types as $type) {
+					// Add a div for each message using the type as the class.
+					foreach ($messages as $messageType => $msgs) {
+						if ($messageType !== $type) {
+							continue;
+						}
+						foreach ((array)$msgs as $msg) {
+							$html .= $this->_message($msg, $messageType);
+						}
+					}
+				}
+			} else {
+				foreach ($messages as $messageType => $msgs) {
+					foreach ((array)$msgs as $msg) {
+						$html .= $this->_message($msg, $messageType);
+					}
+				}
+			}
+			$html .= '</div>';
+			if ($types) {
+				foreach ($types as $type) {
+					CakeSession::delete('messages.' . $type);
+					Configure::delete('messages.' . $type);
+				}
+			} else {
+				CakeSession::delete('messages');
+				Configure::delete('messages');
+			}
+		}
+
+		return $html;
+	}
+
+	/**
+	 * Outputs a single flashMessage directly.
+	 * Note that this does not use the Session.
+	 *
+	 * @param string $message String to output.
+	 * @param string $type Type (success, warning, error, info)
+	 * @param bool $escape Set to false to disable escaping.
+	 * @return string HTML
+	 */
+	public function flashMessage($msg, $type = 'info', $escape = true) {
+		$html = '<div class="flash-messages flashMessages">';
+		if ($escape) {
+			$msg = h($msg);
+		}
+		$html .= $this->_message($msg, $type);
+		$html .= '</div>';
+		return $html;
+	}
+
+	/**
+	 * Formats a message
+	 *
+	 * @param string $msg Message to output.
+	 * @param string $type Type that will be formatted to a class tag.
+	 * @return string
+	 */
+	protected function _message($msg, $type) {
+		if (!empty($msg)) {
+			return '<div class="message' . (!empty($type) ? ' ' . $type : '') . '">' . $msg . '</div>';
+		}
+		return '';
+	}
+
+	/**
+	 * Add a message on the fly
+	 *
+	 * @param string $msg
+	 * @param string $class
+	 * @return void
+	 */
+	public function addFlashMessage($msg, $class = null) {
+		CommonComponent::transientFlashMessage($msg, $class);
+	}
+
+	/**
+	 * CommonHelper::transientFlashMessage()
+	 *
+	 * @param mixed $msg
+	 * @param mixed $class
+	 * @return void
+	 * @deprecated Use addFlashMessage() instead
+	 */
+	public function transientFlashMessage($msg, $class = null) {
+		$this->addFlashMessage($msg, $class);
+	}
+
+	/**
+	 * Auto-pluralizing a word using the Inflection class
+	 * //TODO: move to lib or bootstrap
+	 *
+	 * @param string $singular The string to be pl.
+	 * @param int $count
+	 * @return string "member" or "members" OR "Mitglied"/"Mitglieder" if autoTranslate TRUE
+	 */
+	public function asp($singular, $count, $autoTranslate = false) {
+		if ((int)$count !== 1) {
+			$pural = Inflector::pluralize($singular);
+		} else {
+			$pural = null; // No pluralization necessary
+		}
+		return $this->sp($singular, $pural, $count, $autoTranslate);
+	}
+
+	/**
+	 * Manual pluralizing a word using the Inflection class
+	 * //TODO: move to lib or bootstrap
+	 *
+	 * @param string $singular
+	 * @param string $plural
+	 * @param int $count
+	 * @return string result
+	 */
+	public function sp($singular, $plural, $count, $autoTranslate = false) {
+		if ((int)$count !== 1) {
+			$result = $plural;
+		} else {
+			$result = $singular;
+		}
+
+		if ($autoTranslate) {
+			$result = __($result);
+		}
+		return $result;
+	}
+
+	/**
+	 * Convenience method for clean ROBOTS allowance
+	 *
+	 * @param string $type - private/public
+	 * @return string HTML
+	 */
+	public function metaRobots($type = null) {
+		if ($type === null && ($meta = Configure::read('Config.robots')) !== null) {
+			$type = $meta;
+		}
+		$content = array();
+		if ($type === 'public') {
+			$this->privatePage = false;
+			$content['robots'] = array('index', 'follow', 'noarchive');
+
+		} else {
+			$this->privatePage = true;
+			$content['robots'] = array('noindex', 'nofollow', 'noarchive');
+		}
+
+		$return = '<meta name="robots" content="' . implode(',', $content['robots']) . '" />';
+		return $return;
+	}
+
+	/**
+	 * Convenience method for clean meta name tags
+	 *
+	 * @param string $name: author, date, generator, revisit-after, language
+	 * @param mixed $content: if array, it will be seperated by commas
+	 * @return string HTML Markup
+	 */
+	public function metaName($name = null, $content = null) {
+		if (empty($name) || empty($content)) {
+			return '';
+		}
+
+		$content = (array)$content;
+		$return = '<meta name="' . $name . '" content="' . implode(', ', $content) . '" />';
+		return $return;
+	}
+
+	/**
+	 * Convenience method for meta description
+	 *
+	 * @param string $content
+	 * @param string $language (iso2: de, en-us, ...)
+	 * @param array $additionalOptions
+	 * @return string HTML Markup
+	 */
+	public function metaDescription($content, $language = null, $options = array()) {
+		if (!empty($language)) {
+			$options['lang'] = mb_strtolower($language);
+		} elseif ($language !== false) {
+			$options['lang'] = Configure::read('Config.locale');
+		}
+		return $this->Html->meta('description', $content, $options);
+	}
+
+	/**
+	 * Convenience method to output meta keywords
+	 *
+	 * @param string|array $keywords
+	 * @param string $language (iso2: de, en-us, ...)
+	 * @param bool $escape
+	 * @return string HTML Markup
+	 */
+	public function metaKeywords($keywords = null, $language = null, $escape = true) {
+		if ($keywords === null) {
+			$keywords = Configure::read('Config.keywords');
+		}
+		if (is_array($keywords)) {
+			$keywords = implode(', ', $keywords);
+		}
+		if ($escape) {
+			$keywords = h($keywords);
+		}
+		if (!empty($language)) {
+			$options['lang'] = mb_strtolower($language);
+		} elseif ($language !== false) {
+			$options['lang'] = Configure::read('Config.locale');
+		}
+		return $this->Html->meta('keywords', $keywords, $options);
+	}
+
+	/**
+	 * Convenience function for "canonical" SEO links
+	 *
+	 * @param mixed $url
+	 * @param bool $full
+	 * @return string HTML Markup
+	 */
+	public function metaCanonical($url = null, $full = false) {
+		$canonical = $this->Html->url($url, $full);
+		$options = array('rel' => 'canonical', 'type' => null, 'title' => null);
+		return $this->Html->meta('canonical', $canonical, $options);
+	}
+
+	/**
+	 * Convenience method for "alternate" SEO links
+	 *
+	 * @param mixed $url
+	 * @param mixed $lang (lang(iso2) or array of langs)
+	 * lang: language (in ISO 6391-1 format) + optionally the region (in ISO 3166-1 Alpha 2 format)
+	 * - de
+	 * - de-ch
+	 * etc
+	 * @return string HTML Markup
+	 */
+	public function metaAlternate($url, $lang, $full = false) {
+		//$canonical = $this->Html->url($url, $full);
+		$url = $this->Html->url($url, $full);
+		//return $this->Html->meta('canonical', $canonical, array('rel'=>'canonical', 'type'=>null, 'title'=>null));
+		$lang = (array)$lang;
+		$res = array();
+		foreach ($lang as $language => $countries) {
+			if (is_numeric($language)) {
+				$language = '';
+			} else {
+				$language .= '-';
+			}
+			$countries = (array)$countries;
+			foreach ($countries as $country) {
+				$l = $language . $country;
+				$options = array('rel' => 'alternate', 'hreflang' => $l, 'type' => null, 'title' => null);
+				$res[] = $this->Html->meta('alternate', $url, $options) . PHP_EOL;
+			}
+		}
+		return implode('', $res);
+	}
+
+	/**
+	 * Convenience method for META Tags
+	 *
+	 * @param mixed $url
+	 * @param string $title
+	 * @return string HTML Markup
+	 */
+	public function metaRss($url, $title = null) {
+		$tags = array(
+			'meta' => '<link rel="alternate" type="application/rss+xml" title="%s" href="%s" />',
+		);
+		if (empty($title)) {
+			$title = __('Subscribe to this feed');
+		} else {
+			$title = h($title);
+		}
+
+		return sprintf($tags['meta'], $title, $this->url($url));
+	}
+
+	/**
+	 * Convenience method for META Tags
+	 *
+	 * @param string $type
+	 * @param string $content
+	 * @return string HTML Markup
+	 */
+	public function metaEquiv($type, $value, $escape = true) {
+		$tags = array(
+			'meta' => '<meta http-equiv="%s"%s />',
+		);
+		if ($value === null) {
+			return '';
+		}
+		if ($escape) {
+			$value = h($value);
+		}
+		return sprintf($tags['meta'], $type, ' content="' . $value . '"');
+	}
+
+}

+ 833 - 0
src/View/Helper/FormatHelper.php

@@ -0,0 +1,833 @@
+<?php
+namespace Tools\View\Helper;
+
+use Cake\Core\Configure;
+use Cake\View\View;
+use Cake\View\Helper\TextHelper;
+use Cake\View\StringTemplate;
+
+/**
+ * Format helper with basic html snippets
+ *
+ * TODO: make snippets more "css and background image" (instead of inline img links)
+ *
+ * @author Mark Scherer
+ * @license MIT
+ */
+class FormatHelper extends TextHelper {
+
+	/**
+	 * Other helpers used by FormHelper
+	 *
+	 * @var array
+	 */
+	public $helpers = array('Html', 'Tools.Numeric');
+
+	public $template;
+
+	protected $_defaults = array(
+		'fontIcons' => false, // Defaults to false for BC
+		'iconNamespace' => 'fa',  // Used to be icon
+	);
+
+	public function __construct(View $View, array $config = array()) {
+  $config += $this->_defaults;
+
+		if ($config['fontIcons'] === true) {
+			$config['fontIcons'] = (array)Configure::read('Format.fontIcons');
+			if ($namespace = Configure::read('Format.iconNamespace')) {
+				$config['iconNamespace'] = $namespace;
+			}
+		}
+
+		$templates = array(
+			'icon' => '<i class="{{class}}" title="{{title}}" data-placement="bottom" data-toggle="tooltip"></i>',
+		) + (array)Configure::read('Format.templates');
+		if (!isset($this->template)) {
+			$this->template = new StringTemplate($templates);
+		}
+
+		parent::__construct($View, $config);
+	}
+
+	/**
+	 * jqueryAccess: {id}Pro, {id}Contra
+	 *
+	 * @return string
+	 */
+	public function thumbs($id, $inactive = false, $inactiveTitle = null) {
+		$status = 'Active';
+		$upTitle = __('consentThis');
+		$downTitle = __('dissentThis');
+		if ($inactive === true) {
+			$status = 'Inactive';
+			$upTitle = $downTitle = !empty($inactiveTitle) ? $inactiveTitle : __('alreadyVoted');
+		}
+
+		if ($this->settings['fontIcons']) {
+			// TODO: Return proper font icons
+			// fa-thumbs-down
+			// fa-thumbs-up
+		}
+
+		$ret = '<div class="thumbsUpDown">';
+		$ret .= '<div id="' . $id . 'Pro' . $status . '" rel="' . $id . '" class="thumbUp up' . $status . '" title="' . $upTitle . '"></div>';
+		$ret .= '<div id="' . $id . 'Contra' . $status . '" rel="' . $id . '" class="thumbDown down' . $status . '" title="' . $downTitle . '"></div>';
+		$ret .= '<br class="clear"/>';
+		$ret .=	'</div>';
+		return $ret;
+	}
+
+	/**
+	 * Display neighbor quicklinks
+	 *
+	 * @param array $neighbors (containing prev and next)
+	 * @param string $field: just field or Model.field syntax
+	 * @param array $options:
+	 * - name: title name: next{Record} (if none is provided, "record" is used - not translated!)
+	 * - slug: true/false (defaults to false)
+	 * - titleField: field or Model.field
+	 * @return string
+	 */
+	public function neighbors($neighbors, $field, $options = array()) {
+		if (mb_strpos($field, '.') !== false) {
+			$fieldArray = explode('.', $field, 2);
+			$alias = $fieldArray[0];
+			$field = $fieldArray[1];
+		}
+
+		if (empty($alias)) {
+			if (!empty($neighbors['prev'])) {
+				$modelNames = array_keys($neighbors['prev']);
+				$alias = $modelNames[0];
+			} elseif (!empty($neighbors['next'])) {
+				$modelNames = array_keys($neighbors['next']);
+				$alias = $modelNames[0];
+			}
+		}
+		if (empty($field)) {
+
+		}
+
+		$name = 'Record'; // Translation further down!
+		if (!empty($options['name'])) {
+			$name = ucfirst($options['name']);
+		}
+
+		$prevSlug = $nextSlug = null;
+		if (!empty($options['slug'])) {
+			if (!empty($neighbors['prev'])) {
+				$prevSlug = Inflector::slug($neighbors['prev'][$alias][$field], '-');
+			}
+			if (!empty($neighbors['next'])) {
+				$nextSlug = Inflector::slug($neighbors['next'][$alias][$field], '-');
+			}
+		}
+		$titleAlias = $alias;
+		$titleField = $field;
+		if (!empty($options['titleField'])) {
+			if (mb_strpos($options['titleField'], '.') !== false) {
+				$fieldArray = explode('.', $options['titleField'], 2);
+				$titleAlias = $fieldArray[0];
+				$titleField = $fieldArray[1];
+			} else {
+				$titleField = $options['titleField'];
+			}
+		}
+		if (!isset($options['escape']) || $options['escape'] === false) {
+			$titleField = h($titleField);
+		}
+
+		$ret = '<div class="next-prev-navi nextPrevNavi">';
+		if (!empty($neighbors['prev'])) {
+			$url = array($neighbors['prev'][$alias]['id'], $prevSlug);
+			if (!empty($options['url'])) {
+				$url += $options['url'];
+			}
+
+			$ret .= $this->Html->link($this->cIcon(ICON_PREV, false) . '&nbsp;' . __('prev' . $name), $url, array('escape' => false, 'title' => $neighbors['prev'][$titleAlias][$titleField]));
+		} else {
+			$ret .= $this->cIcon(ICON_PREV_DISABLED, __('noPrev' . $name)) . '&nbsp;' . __('prev' . $name);
+		}
+		$ret .= '&nbsp;&nbsp;';
+		if (!empty($neighbors['next'])) {
+			$url = array($neighbors['next'][$alias]['id'], $prevSlug);
+			if (!empty($options['url'])) {
+				$url += $options['url'];
+			}
+
+			$ret .= $this->Html->link($this->cIcon(ICON_NEXT, false) . '&nbsp;' . __('next' . $name), $url, array('escape' => false, 'title' => $neighbors['next'][$titleAlias][$titleField]));
+		} else {
+			$ret .= $this->cIcon(ICON_NEXT_DISABLED, __('noNext' . $name)) . '&nbsp;' . __('next' . $name);
+		}
+		$ret .= '</div>';
+		return $ret;
+	}
+
+	const GENDER_FEMALE = 2;
+	const GENDER_MALE = 1;
+
+	/**
+	 * Displays gender icon
+	 *
+	 * @return string
+	 */
+	public function genderIcon($value = null) {
+		$value = (int)$value;
+		if ($value == static::GENDER_FEMALE) {
+			$icon =	$this->icon('genderFemale', null, null, null, array('class' => 'gender'));
+		} elseif ($value == static::GENDER_MALE) {
+			$icon =	$this->icon('genderMale', null, null, null, array('class' => 'gender'));
+		} else {
+			$icon =	$this->icon('genderUnknown', null, null, null, array('class' => 'gender'));
+		}
+		return $icon;
+	}
+
+	/**
+	 * Display a font icon (fast and resource-efficient).
+	 * Uses http://fontawesome.io/icons/
+	 *
+	 * Options:
+	 * - size (int|string: 1...5 or large)
+	 * - rotate (integer: 90, 270, ...)
+	 * - spin (booelan: true/false)
+	 * - extra (array: muted, light, dark, border)
+	 * - pull (string: left, right)
+	 *
+	 * @param string|array $icon
+	 * @param array $options
+	 * @return string
+	 */
+	public function fontIcon($icon, array $options = array(), array $attributes = array()) {
+		$defaults = array(
+			'namespace' => $this->settings['iconNamespace']
+		);
+		$options += $defaults;
+		$icon = (array)$icon;
+		$class = array();
+		foreach ($icon as $i) {
+			$class[] = $options['namespace'] . '-' . $i;
+		}
+		if (!empty($options['extra'])) {
+			foreach ($options['extra'] as $i) {
+				$class[] = $options['namespace'] . '-' . $i;
+			}
+		}
+		if (!empty($options['size'])) {
+			$class[] = $options['namespace'] . '-' . ($options['size'] === 'large' ? 'large' : $options['size'] . 'x');
+		}
+		if (!empty($options['pull'])) {
+			$class[] = 'pull-' . $options['pull'];
+		}
+		if (!empty($options['rotate'])) {
+			$class[] = $options['namespace'] . '-rotate-' . (int)$options['rotate'];
+		}
+		if (!empty($options['spin'])) {
+			$class[] = $options['namespace'] . '-spin';
+		}
+		return '<i class="' . implode(' ', $class) . '"></i>';
+	}
+
+	/**
+	 * Quick way of printing default icons
+	 *
+	 * @todo refactor to $type, $options, $attributes
+	 *
+	 * @param type
+	 * @param title
+	 * @param alt (set to FALSE if no alt is supposed to be shown)
+	 * @param bool automagic i18n translate [default true = __('xyz')]
+	 * @param options array ('class'=>'','width/height'=>'','onclick=>'') etc
+	 * @return string
+	 */
+	public function icon($type, $t = null, $a = null, $translate = null, $options = array()) {
+		if (isset($t) && $t === false) {
+			$title = '';
+		} else {
+			$title = $t;
+		}
+
+		if (isset($a) && $a === false) {
+			$alt = '';
+		} else {
+			$alt = $a;
+		}
+
+		if (!$this->settings['fontIcons'] || !isset($this->settings['fontIcons'][$type])) {
+			if (array_key_exists($type, $this->icons)) {
+				$pic = $this->icons[$type]['pic'];
+				$title = (isset($title) ? $title : $this->icons[$type]['title']);
+				$alt = (isset($alt) ? $alt : preg_replace('/[^a-zA-Z0-9]/', '', $this->icons[$type]['title']));
+				if ($translate !== false) {
+					$title = __($title);
+					$alt = __($alt);
+				}
+				$alt = '[' . $alt . ']';
+			} else {
+				$pic = 'pixelspace.gif';
+			}
+			$defaults = array('title' => $title, 'alt' => $alt, 'class' => 'icon');
+			$newOptions = $options + $defaults;
+
+			return $this->Html->image('icons/' . $pic, $newOptions);
+		}
+
+		$options['title'] = $title;
+		$options['translate'] = $translate;
+		return $this->_fontIcon($type, $options);
+	}
+
+	/**
+	 * Custom Icons
+	 *
+	 * @param string $icon (constant or filename)
+	 * @param array $options:
+	 * - translate, ...
+	 * @param array $attributes:
+	 * - title, alt, ...
+	 * THE REST IS DEPRECATED
+	 * @return string
+	 */
+	public function cIcon($icon, $t = null, $a = null, $translate = true, $options = array()) {
+		if (is_array($t)) {
+			$translate = isset($t['translate']) ? $t['translate'] : true;
+			$options = (array)$a;
+			$a = isset($t['alt']) ? $t['alt'] : null; // deprecated
+			$t = isset($t['title']) ? $t['title'] : null; // deprecated
+		}
+
+		$type = extractPathInfo('filename', $icon);
+
+		if (!$this->settings['fontIcons'] || !isset($this->settings['fontIcons'][$type])) {
+			$title = isset($t) ? $t : ucfirst($type);
+			$alt = (isset($a) ? $a : Inflector::slug($title, '-'));
+			if ($translate !== false) {
+				$title = __($title);
+				$alt = __($alt);
+			}
+			$alt = '[' . $alt . ']';
+
+			$defaults = array('title' => $title, 'alt' => $alt, 'class' => 'icon');
+			$options += $defaults;
+			if (substr($icon, 0, 1) !== '/') {
+				$icon = 'icons/' . $icon;
+			}
+			return $this->Html->image($icon, $options);
+		}
+
+		$options['title'] = $t;
+		$options['translate'] = $translate;
+		return $this->_fontIcon($type, $options);
+	}
+
+	/**
+	 * FormatHelper::_fontIcon()
+	 *
+	 * @param string $type
+	 * @param array $options
+	 * @return string
+	 */
+	protected function _fontIcon($type, $options) {
+		$iconType = $this->settings['fontIcons'][$type];
+
+		$defaults = array(
+			'class' => $iconType . ' ' . $type
+		);
+		$options += $defaults;
+
+		if (!isset($options['title'])) {
+			$options['title'] = ucfirst($type);
+			if ($options['translate'] !== false) {
+				$options['title'] = __($options['title']);
+			}
+		}
+
+		return $this->template->format('icon', $options);
+	}
+
+	/**
+	 * Display yes/no symbol.
+	 *
+	 * @todo $on=1, $text=false, $ontitle=false,... => in array(OPTIONS)
+	 *
+	 * @param text: default FALSE; if TRUE, text instead of the image
+	 * @param ontitle: default FALSE; if it is embadded in a link, set to TRUE
+	 * @return image:Yes/No or text:Yes/No
+	 */
+	public function yesNo($v, $ontitle = null, $offtitle = null, $on = 1, $text = false, $notitle = false) {
+		$ontitle = (!empty($ontitle) ? $ontitle : __('Yes'));
+		$offtitle = (!empty($offtitle) ? $offtitle : __('No'));
+		$sbez = array('0' => @substr($offtitle, 0, 1), '1' => @substr($ontitle, 0, 1));
+		$bez = array('0' => $offtitle, '1' => $ontitle);
+
+		if ($v == $on) {
+			$icon = ICON_YES;
+			$value = 1;
+		} else {
+			$icon = ICON_NO;
+			$value = 0;
+		}
+
+		if ($text !== false) {
+			return $bez[$value];
+		}
+
+		$options = array('title' => ($ontitle === false ? '' : $bez[$value]), 'alt' => $sbez[$value], 'class' => 'icon');
+
+		if ($this->settings['fontIcons']) {
+			return $this->cIcon($icon, $options['title']);
+		}
+		return $this->Html->image('icons/' . $icon, $options);
+	}
+
+	/**
+	 * Get URL of a png img of a website (16x16 pixel).
+	 *
+	 * @param string domain
+	 * @return string
+	 */
+	public function siteIconUrl($domain) {
+		if (strpos($domain, 'http') === 0) {
+			// Strip protocol
+			$pieces = parse_url($domain);
+			$domain = $pieces['host'];
+		}
+		return 'http://www.google.com/s2/favicons?domain=' . $domain;
+	}
+
+	/**
+	 * Display a png img of a website (16x16 pixel)
+	 * if not available, will return a fallback image (a globe)
+	 *
+	 * @param domain (preferably without protocol, e.g. "www.site.com")
+	 * @return string
+	 */
+	public function siteIcon($domain, $options = array()) {
+		$url = $this->siteIconUrl($domain);
+		$options['width'] = 16;
+		$options['height'] = 16;
+		if (!isset($options['alt'])) {
+			$options['alt'] = $domain;
+		}
+		if (!isset($options['title'])) {
+			$options['title'] = $domain;
+		}
+		return $this->Html->image($url, $options);
+	}
+
+	/**
+	 * Display a disabled link tag
+	 *
+	 * @param string $text
+	 * @param array $options
+	 * @return string
+	 */
+	public function disabledLink($text, $options = array()) {
+		$defaults = array('class' => 'disabledLink', 'title' => __('notAvailable'));
+		$options += $defaults;
+
+		return $this->Html->tag('span', $text, $options);
+	}
+
+	/**
+	 * Generates a pagination count: #1 etc for each pagination record
+	 * respects order (ASC/DESC)
+	 *
+	 * @param array $paginator
+	 * @param int $count (current post count on this page)
+	 * @param string $dir (ASC/DESC)
+	 * @return int
+	 * @deprecated
+	 */
+	public function absolutePaginateCount(array $paginator, $count, $dir = null) {
+		if ($dir === null) {
+			$dir = 'ASC';
+		}
+
+		$currentPage = $paginator['page'];
+		$pageCount = $paginator['pageCount'];
+		$totalCount = $paginator['count'];
+
+		$limit = $paginator['limit'];
+		$step = 1; //$paginator['step'];
+		//pr($paginator);
+
+		if ($dir === 'DESC') {
+			$currentCount = $count + ($pageCount - $currentPage) * $limit * $step;
+			if ($currentPage != $pageCount && $pageCount > 1) {
+				$currentCount -= $pageCount * $limit * $step - $totalCount;
+			}
+		} else {
+			$currentCount = $count + ($currentPage - 1) * $limit * $step;
+		}
+
+		return $currentCount;
+	}
+
+	/**
+	 * Fixes utf8 problems of native php str_pad function
+	 * //TODO: move to textext helper?
+	 *
+	 * @param string $input
+	 * @param int $padLength
+	 * @param string $padString
+	 * @param mixed $padType
+	 * @return string input
+	 */
+	public function pad($input, $padLength, $padString, $padType = STR_PAD_RIGHT) {
+		$length = mb_strlen($input);
+		if ($padLength - $length > 0) {
+			switch ($padType) {
+				case STR_PAD_LEFT:
+					$input = str_repeat($padString, $padLength - $length) . $input;
+					break;
+				case STR_PAD_RIGHT:
+					$input .= str_repeat($padString, $padLength - $length);
+					break;
+			}
+		}
+		return $input;
+	}
+
+	/**
+	 * Returns red colored if not ok
+	 *
+	 * @param string $value
+	 * @param $okValue
+	 * @return string Value in HTML tags
+	 */
+	public function warning($value, $ok = false) {
+		if (!$ok) {
+			return $this->ok($value, false);
+		}
+		return $value;
+	}
+
+	/**
+	 * Returns green on ok, red otherwise
+	 *
+	 * @todo Remove inline css and make classes better: green=>ok red=>not-ok
+	 *   Maybe use templating
+	 *
+	 * @param mixed $currentValue
+	 * @param bool $ok: true/false (defaults to false)
+	 * //@param string $comparizonType
+	 * //@param mixed $okValue
+	 * @return string newValue nicely formatted/colored
+	 */
+	public function ok($value, $ok = false) {
+		if ($ok) {
+			$value = '<span class="green" style="color:green">' . $value . '</span>';
+		} else {
+			$value = '<span class="red" style="color:red">' . $value . '</span>';
+		}
+		return $value;
+	}
+
+	/**
+	 * Useful for displaying tabbed (code) content when the default of 8 spaces
+	 * inside <pre> is too much. This converts it to spaces for better output.
+	 *
+	 * Inspired by the tab2space function found at:
+	 * @see http://aidan.dotgeek.org/lib/?file=function.tab2space.php
+	 * @param string $text
+	 * @param int $spaces
+	 * @return string
+	 */
+	public function tab2space($text, $spaces = 4) {
+		$spaces = str_repeat(" ", $spaces);
+		$text = preg_split("/\r\n|\r|\n/", trim($text));
+		$wordLengths = array();
+		$wArray = array();
+
+		// Store word lengths
+		foreach ($text as $line) {
+			$words = preg_split("/(\t+)/", $line, -1, PREG_SPLIT_DELIM_CAPTURE);
+			foreach (array_keys($words) as $i) {
+				$strlen = strlen($words[$i]);
+				$add = isset($wordLengths[$i]) && ($wordLengths[$i] < $strlen);
+				if ($add || !isset($wordLengths[$i])) {
+					$wordLengths[$i] = $strlen;
+				}
+			}
+			$wArray[] = $words;
+		}
+
+		$text = '';
+
+		// Apply padding when appropriate and rebuild the string
+		foreach (array_keys($wArray) as $i) {
+			foreach (array_keys($wArray[$i]) as $ii) {
+				if (preg_match("/^\t+$/", $wArray[$i][$ii])) {
+					$wArray[$i][$ii] = str_pad($wArray[$i][$ii], $wordLengths[$ii], "\t");
+				} else {
+					$wArray[$i][$ii] = str_pad($wArray[$i][$ii], $wordLengths[$ii]);
+				}
+			}
+			$text .= str_replace("\t", $spaces, implode("", $wArray[$i])) . "\n";
+		}
+
+		return $text;
+	}
+
+	/**
+	 * Translate a result array into a HTML table
+	 *
+	 * @todo Move to Text Helper etc.
+	 *
+	 * @author Aidan Lister <aidan@php.net>
+	 * @version 1.3.2
+	 * @link http://aidanlister.com/2004/04/converting-arrays-to-human-readable-tables/
+	 * @param array $array The result (numericaly keyed, associative inner) array.
+	 * @param bool $recursive Recursively generate tables for multi-dimensional arrays
+	 * @param string $null String to output for blank cells
+	 */
+	public function array2table($array, $options = array()) {
+		$defaults = array(
+			'null' => '&nbsp;',
+			'recursive' => false,
+			'heading' => true,
+			'escape' => true
+		);
+		$options += $defaults;
+
+		// Sanity check
+		if (empty($array) || !is_array($array)) {
+			return false;
+		}
+
+		if (!isset($array[0]) || !is_array($array[0])) {
+			$array = array($array);
+		}
+
+		// Start the table
+		$table = "<table>\n";
+
+		if ($options['heading']) {
+			// The header
+			$table .= "\t<tr>";
+			// Take the keys from the first row as the headings
+			foreach (array_keys($array[0]) as $heading) {
+				$table .= '<th>' . ($options['escape'] ? h($heading) : $heading) . '</th>';
+			}
+			$table .= "</tr>\n";
+		}
+
+		// The body
+		foreach ($array as $row) {
+			$table .= "\t<tr>";
+			foreach ($row as $cell) {
+				$table .= '<td>';
+
+				// Cast objects
+				if (is_object($cell)) {
+					$cell = (array)$cell;
+				}
+
+				if ($options['recursive'] && is_array($cell) && !empty($cell)) {
+					// Recursive mode
+					$table .= "\n" . static::array2table($cell, $options) . "\n";
+				} else {
+					$table .= (!is_array($cell) && strlen($cell) > 0) ? ($options['escape'] ? h($cell) : $cell) : $options['null'];
+				}
+
+				$table .= '</td>';
+			}
+
+			$table .= "</tr>\n";
+		}
+
+		$table .= '</table>';
+		return $table;
+	}
+
+	public $icons = array(
+		'up' => array(
+			'pic' => ICON_UP,
+			'title' => 'Up',
+		),
+		'down' => array(
+			'pic' => ICON_DOWN,
+			'title' => 'Down',
+		),
+		'edit' => array(
+			'pic' => ICON_EDIT,
+			'title' => 'Edit',
+		),
+		'view' => array(
+			'pic' => ICON_VIEW,
+			'title' => 'View',
+		),
+		'delete' => array(
+			'pic' => ICON_DELETE,
+			'title' => 'Delete',
+		),
+		'reset' => array(
+			'pic' => ICON_RESET,
+			'title' => 'Reset',
+		),
+		'help' => array(
+			'pic' => ICON_HELP,
+			'title' => 'Help',
+		),
+		'loader' => array(
+			'pic' => 'loader.white.gif',
+			'title' => 'Loading...',
+		),
+		'loader-alt' => array(
+			'pic' => 'loader.black.gif',
+			'title' => 'Loading...',
+		),
+		'details' => array(
+			'pic' => ICON_DETAILS,
+			'title' => 'Details',
+		),
+		'use' => array(
+			'pic' => ICON_USE,
+			'title' => 'Use',
+		),
+		'yes' => array(
+			'pic' => ICON_YES,
+			'title' => 'Yes',
+		),
+		'no' => array(
+			'pic' => ICON_NO,
+			'title' => 'No',
+		),
+		// deprecated from here down
+		'close' => array(
+			'pic' => ICON_CLOCK,
+			'title' => 'Close',
+		),
+		'reply' => array(
+			'pic' => ICON_REPLY,
+			'title' => 'Reply',
+		),
+		'time' => array(
+			'pic' => ICON_CLOCK,
+			'title' => 'Time',
+		),
+		'check' => array(
+			'pic' => ICON_CHECK,
+			'title' => 'Check',
+		),
+		'role' => array(
+			'pic' => ICON_ROLE,
+			'title' => 'Role',
+		),
+		'add' => array(
+			'pic' => ICON_ADD,
+			'title' => 'Add',
+		),
+		'remove' => array(
+			'pic' => ICON_REMOVE,
+			'title' => 'Remove',
+		),
+		'email' => array(
+			'pic' => ICON_EMAIL,
+			'title' => 'Email',
+		),
+		'options' => array(
+			'pic' => ICON_SETTINGS,
+			'title' => 'Options',
+		),
+		'lock' => array(
+			'pic' => ICON_LOCK,
+			'title' => 'Locked',
+		),
+		'warning' => array(
+			'pic' => ICON_WARNING,
+			'title' => 'Warning',
+		),
+		'genderUnknown' => array(
+			'pic' => 'gender_icon.gif',
+			'title' => 'genderUnknown',
+		),
+		'genderMale' => array(
+			'pic' => 'gender_icon_m.gif',
+			'title' => 'genderMale',
+		),
+		'genderFemale' => array(
+			'pic' => 'gender_icon_f.gif',
+			'title' => 'genderFemale',
+		),
+	);
+
+}
+
+// Default icons
+
+if (!defined('ICON_UP')) {
+	define('ICON_UP', 'up.gif');
+}
+if (!defined('ICON_DOWN')) {
+	define('ICON_DOWN', 'down.gif');
+}
+if (!defined('ICON_EDIT')) {
+	define('ICON_EDIT', 'edit.gif');
+}
+if (!defined('ICON_VIEW')) {
+	define('ICON_VIEW', 'see.gif');
+}
+if (!defined('ICON_DELETE')) {
+	define('ICON_DELETE', 'delete.gif');
+}
+if (!defined('ICON_DETAILS')) {
+	define('ICON_DETAILS', 'loupe.gif');
+}
+if (!defined('ICON_OPTIONS')) {
+	define('ICON_OPTIONS', 'options.gif');
+}
+if (!defined('ICON_SETTINGS')) {
+	define('ICON_SETTINGS', 'options.gif');
+}
+if (!defined('ICON_USE')) {
+	define('ICON_USE', 'use.gif');
+}
+if (!defined('ICON_CLOSE')) {
+	define('ICON_CLOSE', 'close.gif');
+}
+if (!defined('ICON_REPLY')) {
+	define('ICON_REPLY', 'reply.gif');
+}
+
+if (!defined('ICON_RESET')) {
+	define('ICON_RESET', 'reset.gif');
+}
+if (!defined('ICON_HELP')) {
+	define('ICON_HELP', 'help.gif');
+}
+if (!defined('ICON_YES')) {
+	define('ICON_YES', 'yes.gif');
+}
+if (!defined('ICON_NO')) {
+	define('ICON_NO', 'no.gif');
+}
+if (!defined('ICON_CLOCK')) {
+	define('ICON_CLOCK', 'clock.gif');
+}
+if (!defined('ICON_CHECK')) {
+	define('ICON_CHECK', 'check.gif');
+}
+if (!defined('ICON_ROLE')) {
+	define('ICON_ROLE', 'role.gif');
+}
+if (!defined('ICON_ADD')) {
+	define('ICON_ADD', 'add.gif');
+}
+if (!defined('ICON_REMOVE')) {
+	define('ICON_REMOVE', 'remove.gif');
+}
+if (!defined('ICON_EMAIL')) {
+	define('ICON_EMAIL', 'email.gif');
+}
+if (!defined('ICON_LOCK')) {
+	define('ICON_LOCK', 'lock.gif');
+}
+if (!defined('ICON_WARNING')) {
+	define('ICON_WARNING', 'warning.png');
+}
+if (!defined('ICON_MAP')) {
+	define('ICON_MAP', 'map.gif');
+}

+ 2 - 2
tests/Fixture/SluggedArticlesFixture.php

@@ -17,8 +17,8 @@ class SluggedArticlesFixture extends TestFixture {
 		'id' => ['type' => 'integer'],
 		'title' => ['type' => 'string', 'length' => 255, 'null' => false, 'default' => ''],
 		'slug' => ['type' => 'string', 'length' => 245, 'null' => false, 'default' => ''],
-		'long_title' => array('type' => 'string', 'null' => false, 'default' => ''),
-		'long_slug' => array('type' => 'string', 'null' => false, 'default' => ''),
+		'long_title' => ['type' => 'string', 'null' => false, 'default' => ''],
+		'long_slug' => ['type' => 'string', 'null' => false, 'default' => ''],
 		'section' => ['type' => 'integer', 'null' => true],
 		'_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]]
 	);

+ 1 - 1
tests/TestCase/Model/Behavior/SluggedBehaviorTest.php

@@ -22,7 +22,7 @@ class SluggedBehaviorTest extends TestCase {
  * @var array
  */
 	public $fixtures = [
-		'plugin.tools.slugged_articles',
+		'plugin.tools.slugged_articles'
 	];
 
 /**

+ 3 - 1
tests/TestCase/View/Helper/TreeHelperTest.php

@@ -13,7 +13,9 @@ use Cake\Core\Configure;
 
 class TreeHelperTest extends TestCase {
 
-	public $fixtures = array('plugin.tools.after_trees');
+	public $fixtures = array(
+		'plugin.tools.after_trees'
+	);
 
 	public $Table;