Browse Source

Fix tests

euromark 11 years ago
parent
commit
f9b2cf054e

+ 428 - 14
src/Model/Table/Table.php

@@ -4,6 +4,7 @@ namespace Tools\Model\Table;
 
 use Cake\ORM\Table as CakeTable;
 use Cake\Validation\Validator;
+use Cake\Validation\Validation;
 use Cake\Utility\Inflector;
 use Cake\Core\Configure;
 
@@ -90,28 +91,150 @@ class Table extends CakeTable {
 	 */
 	public function validationDefault(Validator $validator) {
 		if (!empty($this->validate)) {
-			foreach ($this->validate as $k => $v) {
-				if (is_int($k)) {
-					$k = $v;
-					$v = array();
+			foreach ($this->validate as $field => $rules) {
+				if (is_int($field)) {
+					$field = $rules;
+					$rules = array();
 				}
-				if (isset($v['required'])) {
-					$validator->validatePresence($k, $v['required']);
-					unset($v['required']);
+				foreach ((array)$rules as $rule) {
+					if (isset($rule['required'])) {
+						$validator->requirePresence($field, $rule['required']);
+						unset($rule['required']);
+					}
+					if (isset($rule['allowEmpty'])) {
+						$validator->allowEmpty($field, $rule['allowEmpty']);
+						unset($rule['allowEmpty']);
+					}
 				}
-				if (isset($v['allowEmpty'])) {
-					$validator->allowEmpty($k, $v['allowEmpty']);
-					unset($v['allowEmpty']);
-				}
-				$validator->add($k, $v);
+				$validator->add($field, $rules);
 			}
 		}
 
 		return $validator;
 	}
 
+/**
+ * Validator method used to check the uniqueness of a value for a column.
+ * This is meant to be used with the validation API and not to be called
+ * directly.
+ *
+ * ### Example:
+ *
+ * {{{
+ * $validator->add('email', [
+ *	'unique' => ['rule' => 'validateUnique', 'provider' => 'table']
+ * ])
+ * }}}
+ *
+ * Unique validation can be scoped to the value of another column:
+ *
+ * {{{
+ * $validator->add('email', [
+ *	'unique' => [
+ *		'rule' => ['validateUnique', ['scope' => 'site_id']],
+ *		'provider' => 'table'
+ *	]
+ * ]);
+ * }}}
+ *
+ * In the above example, the email uniqueness will be scoped to only rows having
+ * the same site_id. Scoping will only be used if the scoping field is present in
+ * the data to be validated.
+ *
+ * @override To allow multiple scoped values
+ *
+ * @param mixed $value The value of column to be checked for uniqueness
+ * @param array $options The options array, optionally containing the 'scope' key
+ * @param array $context The validation context as provided by the validation routine
+ * @return bool true if the value is unique
+ */
+	public function validateUnique($value, array $options, array $context = []) {
+		if (empty($context)) {
+			$context = $options;
+		}
+
+		$conditions = [$context['field'] => $value];
+		if (!empty($options['scope'])) {
+			foreach ((array)$options['scope'] as $scope) {
+				if (!isset($context['data'][$scope])) {
+					continue;
+				}
+				$scopedValue = $context['data'][$scope];
+				$conditions[$scope] = $scopedValue;
+			}
+		}
+
+		if (!$context['newRecord']) {
+			$keys = (array)$this->primaryKey();
+			$not = [];
+			foreach ($keys as $key) {
+				if (isset($context['data'][$key])) {
+					$not[$key] = $context['data'][$key];
+				}
+			}
+			$conditions['NOT'] = $not;
+		}
+
+		return !$this->exists($conditions);
+	}
+
 	/**
-	 * Shim to provide 2.x way of find('first').
+	 * Checks a record, if it is unique - depending on other fields in this table (transfered as array)
+	 * example in model: 'rule' => array ('validateUnique', array('belongs_to_table_id','some_id','user_id')),
+	 * if all keys (of the array transferred) match a record, return false, otherwise true
+	 *
+	 * @param array $fields Other fields to depend on
+	 * TODO: add possibity of deep nested validation (User -> Comment -> CommentCategory: UNIQUE comment_id, Comment.user_id)
+	 * @param array $options
+	 * - requireDependentFields Require all dependent fields for the validation rule to return true
+	 * @return bool Success
+	 */
+	public function validateUniqueExt($fieldValue, $fields = array(), $options = array()) {
+		$id = (!empty($this->data[$this->alias][$this->primaryKey]) ? $this->data[$this->alias][$this->primaryKey] : 0);
+		if (!$id && $this->id) {
+			$id = $this->id;
+		}
+
+		$conditions = array(
+			$this->alias . '.' . $fieldName => $fieldValue,
+			$this->alias . '.id !=' => $id);
+
+		$fields = (array)$fields;
+		if (!array_key_exists('allowEmpty', $fields)) {
+			foreach ($fields as $dependingField) {
+				if (isset($this->data[$this->alias][$dependingField])) { // add ONLY if some content is transfered (check on that first!)
+					$conditions[$this->alias . '.' . $dependingField] = $this->data[$this->alias][$dependingField];
+
+				} elseif (isset($this->data['Validation'][$dependingField])) { // add ONLY if some content is transfered (check on that first!
+					$conditions[$this->alias . '.' . $dependingField] = $this->data['Validation'][$dependingField];
+
+				} elseif (!empty($id)) {
+					// manual query! (only possible on edit)
+					$res = $this->find('first', array('fields' => array($this->alias . '.' . $dependingField), 'conditions' => array($this->alias . '.id' => $id)));
+					if (!empty($res)) {
+						$conditions[$this->alias . '.' . $dependingField] = $res[$this->alias][$dependingField];
+					}
+				} else {
+					if (!empty($options['requireDependentFields'])) {
+						trigger_error('Required field ' . $dependingField . ' for validateUnique validation not present');
+						return false;
+					}
+					return true;
+				}
+			}
+		}
+
+		$this->recursive = -1;
+		if (count($conditions) > 2) {
+			$this->recursive = 0;
+		}
+		$options = array('fields' => array($this->alias . '.' . $this->primaryKey), 'conditions' => $conditions);
+		$res = $this->find('first', $options);
+		return empty($res);
+	}
+
+	/**
+	 * Shim to provide 2.x way of find('first') for easier upgrade.
 	 *
 	 * @param string $type
 	 * @param array $options
@@ -147,7 +270,9 @@ class Table extends CakeTable {
 	 * @return \Cake\Datasource\EntityInterface
 	 */
 	public function newEntity(array $data = [], array $options = []) {
-		$options += ['markNew' => Configure::read('Entity.autoMarkNew') ? 'auto' : null];
+		if (Configure::read('Entity.autoMarkNew')) {
+			$options += ['markNew' => 'auto'];
+		}
 		if (isset($options['markNew']) && $options['markNew'] === 'auto') {
 			$this->_primaryKey = (array)$this->primaryKey();
 			$this->_primaryKey = $this->_primaryKey[0];
@@ -168,4 +293,293 @@ class Table extends CakeTable {
 		}
 	}
 
+	/**
+	 * Get all related entries that have been used so far
+	 *
+	 * @param string $modelName The related model
+	 * @param string $groupField Field to group by
+	 * @param string $type Find type
+	 * @param array $options
+	 * @return array
+	 */
+	public function getRelatedInUse($modelName, $groupField = null, $type = 'all', $options = array()) {
+		if ($groupField === null) {
+			$groupField = $this->belongsTo[$modelName]['foreignKey'];
+		}
+		$defaults = array(
+			'contain' => array($modelName),
+			'group' => $groupField,
+			'order' => $this->$modelName->order ? $this->$modelName->order : array($modelName . '.' . $this->$modelName->displayField => 'ASC'),
+		);
+		if ($type === 'list') {
+			$defaults['fields'] = array($modelName . '.' . $this->$modelName->primaryKey, $modelName . '.' . $this->$modelName->displayField);
+		}
+		$options += $defaults;
+		return $this->find($type, $options);
+	}
+
+	/**
+	 * Get all fields that have been used so far
+	 *
+	 * @param string $groupField Field to group by
+	 * @param string $type Find type
+	 * @param array $options
+	 * @return array
+	 */
+	public function getFieldInUse($groupField, $type = 'all', $options = array()) {
+		$defaults = array(
+			'group' => $groupField,
+			'order' => array($this->alias . '.' . $this->displayField => 'ASC'),
+		);
+		if ($type === 'list') {
+			$defaults['fields'] = array($this->alias . '.' . $this->primaryKey, $this->alias . '.' . $this->displayField);
+		}
+		$options += $defaults;
+		return $this->find($type, $options);
+	}
+
+	/**
+	 * Checks if the content of 2 fields are equal
+	 * Does not check on empty fields! Return TRUE even if both are empty (secure against empty in another rule)!
+	 *
+	 * Options:
+	 * - compare: field to compare to
+	 * - cast: if casting should be applied to both values
+	 *
+	 * @param mixed $value
+	 * @param array $options
+	 * @return bool Success
+	 */
+	public function validateIdentical($value, array $options, array $context = []) {
+		if (!is_array($options)) {
+			$options = array('compare' => $options);
+		}
+		if (!isset($context['data'][$options['compare']])) {
+			return false;
+		}
+		$compareValue = $context['data'][$options['compare']];
+
+		$matching = array('string' => 'string', 'int' => 'integer', 'float' => 'float', 'bool' => 'boolean');
+		if (!empty($options['cast']) && array_key_exists($options['cast'], $matching)) {
+			// cast values to string/int/float/bool if desired
+			settype($compareValue, $matching[$options['cast']]);
+			settype($value, $matching[$options['cast']]);
+		}
+		return ($compareValue === $value);
+	}
+
+	/**
+	 * Checks if a url is valid AND accessable (returns false otherwise)
+	 *
+	 * @param array/string $data: full url(!) starting with http://...
+	 * @options array
+	 * - allowEmpty TRUE/FALSE (TRUE: if empty => return TRUE)
+	 * - required TRUE/FALSE (TRUE: overrides allowEmpty)
+	 * - autoComplete (default: TRUE)
+	 * - deep (default: TRUE)
+	 * @return bool Success
+	 */
+	public function validateUrl($url, $options = array()) {
+		if (empty($url)) {
+			if (!empty($options['allowEmpty']) && empty($options['required'])) {
+				return true;
+			}
+			return false;
+		}
+		if (!isset($options['autoComplete']) || $options['autoComplete'] !== false) {
+			$url = $this->_autoCompleteUrl($url);
+			if (isset($key)) {
+				$this->data[$this->alias][$key] = $url;
+			}
+		}
+
+		if (!isset($options['strict']) || $options['strict'] !== false) {
+			$options['strict'] = true;
+		}
+
+		// validation
+		if (!Validation::url($url, $options['strict']) && env('REMOTE_ADDR') && env('REMOTE_ADDR') !== '127.0.0.1') {
+			return false;
+		}
+		// same domain?
+		if (!empty($options['sameDomain']) && env('HTTP_HOST')) {
+			$is = parse_url($url, PHP_URL_HOST);
+			$expected = env('HTTP_HOST');
+			if (mb_strtolower($is) !== mb_strtolower($expected)) {
+				return false;
+			}
+		}
+
+		if (isset($options['deep']) && $options['deep'] === false) {
+			return true;
+		}
+		return $this->_validUrl($url);
+	}
+
+	/**
+	 * Prepend protocol if missing
+	 *
+	 * @param string $url
+	 * @return string Url
+	 */
+	protected function _autoCompleteUrl($url) {
+		if (mb_strpos($url, '/') === 0) {
+			$url = Router::url($url, true);
+		} elseif (mb_strpos($url, '://') === false && mb_strpos($url, 'www.') === 0) {
+			$url = 'http://' . $url;
+		}
+		return $url;
+	}
+
+	/**
+	 * Checks if a url is valid
+	 *
+	 * @param string url
+	 * @return bool Success
+	 */
+	protected function _validUrl($url) {
+		$headers = Utility::getHeaderFromUrl($url);
+		if ($headers === false) {
+			return false;
+		}
+		$headers = implode("\n", $headers);
+		$protocol = mb_strpos($url, 'https://') === 0 ? 'HTTP' : 'HTTP';
+		if (!preg_match('#^' . $protocol . '/.*?\s+[(200|301|302)]+\s#i', $headers)) {
+			return false;
+		}
+		if (preg_match('#^' . $protocol . '/.*?\s+[(404|999)]+\s#i', $headers)) {
+			return false;
+		}
+		return true;
+	}
+
+	/**
+	 * Validation of DateTime Fields (both Date and Time together)
+	 *
+	 * @param options
+	 * - dateFormat (defaults to 'ymd')
+	 * - allowEmpty
+	 * - after/before (fieldName to validate against)
+	 * - min/max (defaults to >= 1 - at least 1 minute apart)
+	 * @return bool Success
+	 */
+	public function validateDateTime($value, $options = array(), $config = array()) {
+		$format = !empty($options['dateFormat']) ? $options['dateFormat'] : 'ymd';
+
+		$pieces = $value->format(FORMAT_DB_DATETIME);
+		$dateTime = explode(' ', $pieces, 2);
+		$date = $dateTime[0];
+		$time = (!empty($dateTime[1]) ? $dateTime[1] : '');
+
+		if (!empty($options['allowEmpty']) && (empty($date) && empty($time) || $date == DEFAULT_DATE && $time == DEFAULT_TIME || $date == DEFAULT_DATE && empty($time))) {
+			return true;
+		}
+
+		//TODO: cleanup
+		if (Validation::date($date, $format) && Validation::time($time)) {
+			// after/before?
+			$minutes = isset($options['min']) ? $options['min'] : 1;
+			if (!empty($options['after']) && isset($config['data'][$options['after']])) {
+				$compare = $value->subMinutes($minutes);
+				if ($config['data'][$options['after']]->gt($compare)) {
+					return false;
+				}
+			}
+			if (!empty($options['before']) && isset($config['data'][$options['before']])) {
+				$compare = $value->addMinutes($minutes);
+				if ($config['data'][$options['before']]->lt($compare)) {
+					return false;
+				}
+			}
+			return true;
+		}
+		return false;
+	}
+
+	/**
+	 * Validation of Date fields (as the core one is buggy!!!)
+	 *
+	 * @param options
+	 * - dateFormat (defaults to 'ymd')
+	 * - allowEmpty
+	 * - after/before (fieldName to validate against)
+	 * - min (defaults to 0 - equal is OK too)
+	 * @return bool Success
+	 */
+	public function validateDate($value, $options = array()) {
+		$format = !empty($options['format']) ? $options['format'] : 'ymd';
+
+		$dateTime = explode(' ', $value, 2);
+		$date = $dateTime[0];
+
+		if (!empty($options['allowEmpty']) && (empty($date) || $date == DEFAULT_DATE)) {
+			return true;
+		}
+		if (Validation::date($date, $format)) {
+			// after/before?
+			$days = !empty($options['min']) ? $options['min'] : 0;
+			if (!empty($options['after']) && isset($this->data[$this->alias][$options['after']])) {
+				if ($this->data[$this->alias][$options['after']] > date(FORMAT_DB_DATE, strtotime($date) - $days * DAY)) {
+					return false;
+				}
+			}
+			if (!empty($options['before']) && isset($this->data[$this->alias][$options['before']])) {
+				if ($this->data[$this->alias][$options['before']] < date(FORMAT_DB_DATE, strtotime($date) + $days * DAY)) {
+					return false;
+				}
+			}
+			return true;
+		}
+		return false;
+	}
+
+	/**
+	 * Validation of Time fields
+	 *
+	 * @param array $options
+	 * - timeFormat (defaults to 'hms')
+	 * - allowEmpty
+	 * - after/before (fieldName to validate against)
+	 * - min/max (defaults to >= 1 - at least 1 minute apart)
+	 * @return bool Success
+	 */
+	public function validateTime($value, $options = array()) {
+		$dateTime = explode(' ', $value, 2);
+		$value = array_pop($dateTime);
+
+		if (Validation::time($value)) {
+			// after/before?
+			if (!empty($options['after']) && isset($this->data[$this->alias][$options['after']])) {
+				if ($this->data[$this->alias][$options['after']] >= $value) {
+					return false;
+				}
+			}
+			if (!empty($options['before']) && isset($this->data[$this->alias][$options['before']])) {
+				if ($this->data[$this->alias][$options['before']] <= $value) {
+					return false;
+				}
+			}
+			return true;
+		}
+		return false;
+	}
+
+	/**
+	 * Validation of Date Fields (>= minDate && <= maxDate)
+	 *
+	 * @param options
+	 * - min/max (TODO!!)
+	 */
+	public function validateDateRange($value, $options = array()) {
+	}
+
+	/**
+	 * Validation of Time Fields (>= minTime && <= maxTime)
+	 *
+	 * @param options
+	 * - min/max (TODO!!)
+	 */
+	public function validateTimeRange($value, $options = array()) {
+	}
+
 }

+ 1 - 1
src/Network/Email/Email.php

@@ -16,7 +16,7 @@ class Email extends CakeEmail {
 			$config = 'default';
 		}
 		parent::__construct($config);
-
+die(debug($this));
 		//$this->resetAndSet();
 	}
 

+ 10 - 0
src/Utility/Time.php

@@ -11,6 +11,16 @@ use Cake\Core\Configure;
  */
 class Time extends CakeTime {
 
+/**
+ * {@inheritDoc}
+ */
+	public function __construct($time = null, $tz = null) {
+		if (is_array($time)) {
+			return;
+		}
+		parent::__construct($time, $tz);
+	}
+
 	/**
 	 * Detect if a timezone has a DST
 	 *

+ 12 - 9
src/View/Helper/CommonHelper.php

@@ -3,6 +3,9 @@ namespace Tools\View\Helper;
 
 use Cake\Core\Configure;
 use Cake\View\Helper;
+use Cake\Utility\Hash;
+use Cake\Utility\Inflector;
+use Tools\Controller\Component\CommonComponent;
 
 /**
  * Common helper
@@ -11,7 +14,7 @@ use Cake\View\Helper;
  */
 class CommonHelper extends Helper {
 
-	public $helpers = array('Session', 'Html');
+	public $helpers = array('Session', 'Html', 'Url');
 
 	/**
 	 * Display all flash messages.
@@ -241,9 +244,9 @@ class CommonHelper extends Helper {
 	 * @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);
+		$canonical = $this->Url->build($url, $full);
+		$options = array('rel' => 'canonical', 'link' => $canonical);
+		return $this->Html->meta($options);
 	}
 
 	/**
@@ -258,8 +261,8 @@ class CommonHelper extends Helper {
 	 * @return string HTML Markup
 	 */
 	public function metaAlternate($url, $lang, $full = false) {
-		//$canonical = $this->Html->url($url, $full);
-		$url = $this->Html->url($url, $full);
+		//$canonical = $this->Url->build($url, $full);
+		$url = $this->Url->build($url, $full);
 		//return $this->Html->meta('canonical', $canonical, array('rel'=>'canonical', 'type'=>null, 'title'=>null));
 		$lang = (array)$lang;
 		$res = array();
@@ -272,8 +275,8 @@ class CommonHelper extends Helper {
 			$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;
+				$options = array('rel' => 'alternate', 'hreflang' => $l, 'link' => $url);
+				$res[] = $this->Html->meta($options) . PHP_EOL;
 			}
 		}
 		return implode('', $res);
@@ -296,7 +299,7 @@ class CommonHelper extends Helper {
 			$title = h($title);
 		}
 
-		return sprintf($tags['meta'], $title, $this->url($url));
+		return sprintf($tags['meta'], $title, $this->Url->build($url));
 	}
 
 	/**

+ 1 - 0
src/View/Helper/GoogleMapV3Helper.php

@@ -4,6 +4,7 @@ namespace Tools\View\Helper;
 use Cake\View\Helper;
 use Cake\Utility\Hash;
 use Cake\Core\Configure;
+use Cake\Router\Router;
 
 /**
  * This is a CakePHP helper that helps users to integrate GoogleMap v3

+ 81 - 38
src/View/Helper/TimeHelper.php

@@ -3,6 +3,8 @@
 namespace Tools\View\Helper;
 
 use Cake\View\Helper\TimeHelper as CakeTimeHelper;
+use Cake\View\View;
+use Cake\Core\App;
 
 /**
  * Wrapper for TimeHelper and TimeLib
@@ -11,10 +13,56 @@ class TimeHelper extends CakeTimeHelper {
 
 	public $helpers = array('Html');
 
-	public function __construct($View = null, $config = array()) {
-		$defaults = array('engine' => 'Tools.Time');
-		$config += $defaults;
+/**
+ * Default config for this class
+ *
+ * @var mixed
+ */
+	protected $_defaultConfig = [
+		'engine' => 'Tools\Utility\Time'
+	];
+
+/**
+ * Cake\I18n\LocalizedNumber instance
+ *
+ * @var \Cake\I18n\Number
+ */
+	protected $_engine = null;
+
+/**
+ * Default Constructor
+ *
+ * ### Settings:
+ *
+ * - `engine` Class name to use to replace Cake\I18n\Time functionality
+ *            The class needs to be placed in the `Utility` directory.
+ *
+ * @param \Cake\View\View $View The View this helper is being attached to.
+ * @param array $config Configuration settings for the helper
+ * @throws \Cake\Core\Exception\Exception When the engine class could not be found.
+ */
+	public function __construct(View $View, array $config = array()) {
 		parent::__construct($View, $config);
+
+		$config = $this->_config;
+
+		$engineClass = App::className($config['engine'], 'Utility');
+		if ($engineClass) {
+			$this->_engine = new $engineClass($config);
+		} else {
+			throw new Exception(sprintf('Class for %s could not be found', $config['engine']));
+		}
+	}
+
+/**
+ * Call methods from Cake\I18n\Number utility class
+ *
+ * @param string $method Method to invoke
+ * @param array $params Array of params for the method.
+ * @return mixed Whatever is returned by called method, or false on failure
+ */
+	public function __call($method, $params) {
+		return call_user_func_array(array($this->_engine, $method), $params);
 	}
 
 	/**
@@ -67,8 +115,7 @@ class TimeHelper extends CakeTimeHelper {
 	 * @param array $attr: html attributes
 	 * @return nicely formatted date
 	 */
-	public function published($dateString = null, $userOffset = null, $options = array(), $attr = array()) {
-		$date = $dateString ? $this->fromString($dateString, $userOffset) : null; // time() ?
+	public function published(\DateTime $date, $options = array(), $attr = array()) {
 		$niceDate = '';
 		$when = null;
 		$span = '';
@@ -76,47 +123,43 @@ class TimeHelper extends CakeTimeHelper {
 		$whenArray = array('-1' => 'already', '0' => 'today', '1' => 'notyet');
 		$titles = array('-1' => __d('tools', 'publishedAlready'), '0' => __d('tools', 'publishedToday'), '1' => __d('tools', 'publishedNotYet'));
 
-		if (!empty($date)) {
+		$y = $this->isThisYear($date) ? '' : ' Y';
 
-			$y = $this->isThisYear($date) ? '' : ' Y';
+		$format = (!empty($options['format']) ? $options['format'] : FORMAT_NICE_YMD);
 
-			$format = (!empty($options['format']) ? $options['format'] : FORMAT_NICE_YMD);
-
-			// Hack
-			// //TODO: get this to work with datetime - somehow cleaner
-			$timeAttachment = '';
-			if (isset($options['niceDateTime'])) {
-				$timeAttachment = ', ' . $this->niceDate($date, $options['niceDateTime']);
-				$whenOverride = true;
-			}
+		// Hack
+		// //TODO: get this to work with datetime - somehow cleaner
+		$timeAttachment = '';
+		if (isset($options['niceDateTime'])) {
+			$timeAttachment = ', ' . $this->nice($date, $options['niceDateTime']);
+			$whenOverride = true;
+		}
 
-			if ($this->isToday($date)) {
-				$when = 0;
-				$niceDate = __d('tools', 'Today') . $timeAttachment;
-			} elseif ($this->isTomorrow($date)) {
+		if ($this->isToday($date)) {
+			$when = 0;
+			$niceDate = __d('tools', 'Today') . $timeAttachment;
+		} elseif ($this->isTomorrow($date)) {
+			$when = 1;
+			$niceDate = __d('tools', 'Tomorrow') . $timeAttachment;
+		} elseif ($this->wasYesterday($date)) {
+			$when = -1;
+			$niceDate = __d('tools', 'Yesterday') . $timeAttachment;
+		} else {
+			// before or after?
+			if ($this->isFuture($date)) {
 				$when = 1;
-				$niceDate = __d('tools', 'Tomorrow') . $timeAttachment;
-			} elseif ($this->wasYesterday($date)) {
-				$when = -1;
-				$niceDate = __d('tools', 'Yesterday') . $timeAttachment;
 			} else {
-				// before or after?
-				if ($this->isNotTodayAndInTheFuture($date)) {
-					$when = 1;
-				} else {
-					$when = -1;
-				}
-				$niceDate = $this->niceDate($date, $format) . $timeAttachment; //date("M jS{$y}", $date);
+				$when = -1;
 			}
+			$niceDate = $this->format($date, $format) . $timeAttachment; //date("M jS{$y}", $date);
+		}
 
-			if (!empty($whenOverride) && $when == 0) {
-				if ($this->isInTheFuture($date)) {
-					$when = 1;
-				} else {
-					$when = -1;
-				}
+		if (!empty($whenOverride) && $when == 0) {
+			if ($this->isInTheFuture($date)) {
+				$when = 1;
+			} else {
+				$when = -1;
 			}
-
 		}
 
 		if (empty($niceDate) || $when === null) {

+ 4 - 1
src/View/Helper/TimelineHelper.php

@@ -209,7 +209,10 @@ JS;
 	 * @param \DateTime $date
 	 * @return string
 	 */
-	protected function _date(\DateTime $date) {
+	protected function _date($date = null) {
+		if ($date === null || !$date instanceof \DateTime) {
+			return '';
+		}
 		$datePieces = array();
 		$datePieces[] = $date->format('Y');
 		// JavaScript uses 0-indexed months, so we need to subtract 1 month from PHP's output

+ 83 - 0
tests/Fixture/RolesFixture.php

@@ -0,0 +1,83 @@
+<?php
+namespace Tools\Test\Fixture;
+
+use Cake\TestSuite\Fixture\TestFixture;
+
+/**
+ * RoleFixture
+ *
+ */
+class RolesFixture extends TestFixture {
+
+	/**
+	 * Fields
+	 *
+	 * @var array
+	 */
+	public $fields = array(
+		'id' => ['type' => 'integer', 'null' => false, 'default' => null, 'length' => 10, 'collate' => null, 'comment' => ''],
+		'name' => ['type' => 'string', 'null' => false, 'length' => 64, 'collate' => 'utf8_unicode_ci', 'comment' => '', 'charset' => 'utf8'],
+		'description' => ['type' => 'string', 'null' => false, 'default' => null, 'collate' => 'utf8_unicode_ci', 'comment' => '', 'charset' => 'utf8'],
+		'alias' => ['type' => 'string', 'null' => false, 'default' => null, 'length' => 20, 'collate' => 'utf8_unicode_ci', 'comment' => '', 'charset' => 'utf8'],
+		'default_role' => ['type' => 'boolean', 'null' => false, 'default' => '0', 'collate' => null, 'comment' => 'set at register'],
+		'created' => ['type' => 'datetime', 'null' => true, 'default' => null, 'collate' => null, 'comment' => ''],
+		'modified' => ['type' => 'datetime', 'null' => true, 'default' => null, 'collate' => null, 'comment' => ''],
+		'sort' => ['type' => 'integer', 'null' => false, 'default' => '0', 'length' => 10, 'collate' => null, 'comment' => ''],
+		'active' => ['type' => 'boolean', 'null' => false, 'default' => '0', 'collate' => null, 'comment' => ''],
+		'_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']], 'PRIMARY' => ['type' => 'unique', 'columns' => 'id']],
+		'_options' => []
+	);
+
+	/**
+	 * Records
+	 *
+	 * @var array
+	 */
+	public $records = array(
+		array(
+			'id' => '2',
+			'name' => 'Admin',
+			'description' => 'Zuständig für die Verwaltung der Seite und Mitglieder, Ahndung von Missbrauch und CO',
+			'alias' => 'admin',
+			'default_role' => 0,
+			'created' => '2010-01-07 03:36:33',
+			'modified' => '2010-01-07 03:36:33',
+			'sort' => '6',
+			'active' => 1
+		),
+		array(
+			'id' => '4',
+			'name' => 'User',
+			'description' => 'Standardrolle jedes Mitglieds (ausreichend für die meisten Aktionen)',
+			'alias' => 'user',
+			'default_role' => 1,
+			'created' => '2010-01-07 03:36:33',
+			'modified' => '2010-01-07 03:36:33',
+			'sort' => '1',
+			'active' => 1
+		),
+		array(
+			'id' => '6',
+			'name' => 'Partner',
+			'description' => 'Partner',
+			'alias' => 'partner',
+			'default_role' => 0,
+			'created' => '2010-01-07 03:36:33',
+			'modified' => '2010-01-07 03:36:33',
+			'sort' => '0',
+			'active' => 1
+		),
+		array(
+			'id' => '1',
+			'name' => 'Super-Admin',
+			'description' => 'Zuständig für Programmierung, Sicherheit, Bugfixes, Hosting und CO',
+			'alias' => 'superadmin',
+			'default_role' => 0,
+			'created' => '2010-01-07 03:36:33',
+			'modified' => '2010-01-07 03:36:33',
+			'sort' => '7',
+			'active' => 1
+		),
+	);
+
+}

+ 36 - 0
tests/Fixture/ToolsUsersFixture.php

@@ -0,0 +1,36 @@
+<?php
+namespace Tools\Test\Fixture;
+
+use Cake\TestSuite\Fixture\TestFixture;
+
+/**
+ * ToolsUser Fixture
+ */
+class ToolsUsersFixture extends TestFixture {
+
+	/**
+	 * Fields
+	 *
+	 * @var array
+	 */
+	public $fields = array(
+		'id' => ['type' => 'integer'],
+		'name' => ['type' => 'string', 'null' => false],
+		'password' => ['type' => 'string', 'null' => false],
+		'role_id' => ['type' => 'integer', 'null' => true],
+		'_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]]
+	);
+
+	/**
+	 * Records property
+	 *
+	 * @var array
+	 */
+	public $records = array(
+		array('id' => 1, 'role_id' => 1, 'password' => '123456', 'name' => 'User 1'),
+		array('id' => 2, 'role_id' => 2, 'password' => '123456', 'name' => 'User 2'),
+		array('id' => 3, 'role_id' => 1, 'password' => '123456', 'name' => 'User 3'),
+		array('id' => 4, 'role_id' => 3, 'password' => '123456', 'name' => 'User 4')
+	);
+
+}

+ 31 - 0
tests/TestApp/Controller/Component/AppleComponent.php

@@ -0,0 +1,31 @@
+<?php
+namespace TestApp\Controller\Component;
+
+use Cake\Controller\Component;
+use Cake\Controller\Controller;
+use Cake\Event\Event;
+
+/**
+ * AppleComponent class
+ *
+ */
+class AppleComponent extends Component {
+
+/**
+ * components property
+ *
+ * @var array
+ */
+	public $components = array('Banana');
+
+/**
+ * startup method
+ *
+ * @param Event $event
+ * @param mixed $controller
+ * @return void
+ */
+	public function startup(Event $event) {
+	}
+
+}

+ 32 - 0
tests/TestApp/Controller/Component/BananaComponent.php

@@ -0,0 +1,32 @@
+<?php
+namespace TestApp\Controller\Component;
+
+use Cake\Controller\Component;
+use Cake\Controller\Controller;
+use Cake\Event\Event;
+
+/**
+ * BananaComponent class
+ *
+ */
+class BananaComponent extends Component {
+
+/**
+ * testField property
+ *
+ * @var string
+ */
+	public $testField = 'BananaField';
+
+/**
+ * startup method
+ *
+ * @param Event $event
+ * @param Controller $controller
+ * @return string
+ */
+	public function startup(Event $event) {
+		$this->_registry->getController()->bar = 'fail';
+	}
+
+}

+ 23 - 0
tests/TestApp/Controller/Component/TestComponent.php

@@ -0,0 +1,23 @@
+<?php
+namespace TestApp\Controller\Component;
+
+use Cake\Controller\Component;
+use Cake\Event\Event;
+
+class TestComponent extends Component {
+
+	public $Controller;
+
+	public $isInit = false;
+
+	public $isStartup = false;
+
+	public function beforeFilter(Event $event) {
+		$this->isInit = true;
+	}
+
+	public function startup(Event $event) {
+		$this->isStartup = true;
+	}
+
+}

+ 197 - 0
tests/TestCase/Controller/Component/CommonComponentTest.php

@@ -0,0 +1,197 @@
+<?php
+namespace Tools\Test\TestCase\Controller\Component;
+
+use Cake\Controller\ComponentRegistry;
+use Cake\Controller\Component;
+use Cake\Controller\Component\CommonComponent;
+use Cake\Controller\Controller;
+use Cake\Core\Configure;
+use Cake\Network\Request;
+use Cake\Network\Session;
+use Cake\Routing\DispatcherFactory;
+use Cake\TestSuite\TestCase;
+
+/**
+ */
+class CommonComponentTest extends TestCase {
+
+	//public $fixtures = array('core.sessions', 'plugin.tools.tools_users', 'plugin.tools.roles');
+
+	public function setUp() {
+		parent::setUp();
+
+		Configure::write('App.namespace', 'TestApp');
+
+		$this->Controller = new CommonComponentTestController();
+		$this->Controller->startupProcess();
+	}
+
+	public function tearDown() {
+		parent::tearDown();
+
+		unset($this->Controller->Common);
+		unset($this->Controller);
+	}
+
+	/**
+	 * CommonComponentTest::testLoadComponent()
+	 *
+	 * @return void
+	 */
+	public function testLoadComponent() {
+		$this->assertTrue(!isset($this->Controller->Apple));
+		$this->Controller->Common->loadComponent('Apple');
+		$this->assertTrue(isset($this->Controller->Apple));
+
+		// with plugin
+		$this->Controller->Session = null;
+		$this->assertTrue(!isset($this->Controller->Session));
+		$this->Controller->Common->loadComponent('Tools.Session', ['foo' => 'bar']);
+		$this->Controller->components()->unload('Session');
+		$this->Controller->Common->loadComponent('Tools.Session',['foo' => 'baz']);
+		$this->assertTrue(isset($this->Controller->Session));
+
+		// with options
+		$this->Controller->Test = null;
+		$this->assertTrue(!isset($this->Controller->Test));
+		$this->Controller->Common->loadComponent('Test', array('x' => 'z'), false);
+		$this->assertTrue(isset($this->Controller->Test));
+		$this->assertFalse($this->Controller->Test->isInit);
+		$this->assertFalse($this->Controller->Test->isStartup);
+
+		// with options
+		$this->Controller->components()->unload('Test');
+		$this->Controller->Test = null;
+		$this->assertTrue(!isset($this->Controller->Test));
+		$this->Controller->Common->loadComponent('Test', array('x' => 'y'));
+		$this->assertTrue(isset($this->Controller->Test));
+		$this->assertTrue($this->Controller->Test->isInit);
+		$this->assertTrue($this->Controller->Test->isStartup);
+
+		$config = $this->Controller->Test->config();
+		$this->assertEquals(['x' => 'y'], $config);
+	}
+
+	/**
+	 * CommonComponentTest::testGetParams()
+	 *
+	 * @return void
+	 */
+	public function testGetParams() {
+		$is = $this->Controller->Common->getPassedParam('x');
+		$this->assertNull($is);
+
+		$is = $this->Controller->Common->getPassedParam('x', 'y');
+		$this->assertSame('y', $is);
+	}
+
+	/**
+	 * CommonComponentTest::testGetDefaultUrlParams()
+	 *
+	 * @return void
+	 */
+	public function testGetDefaultUrlParams() {
+		$is = $this->Controller->Common->defaultUrlParams();
+		$this->assertNotEmpty($is);
+	}
+
+	/**
+	 * CommonComponentTest::testcurrentUrl()
+	 *
+	 * @return void
+	 */
+	public function testCurrentUrl() {
+		$is = $this->Controller->Common->currentUrl();
+		$this->assertTrue(is_array($is) && !empty($is));
+
+		$is = $this->Controller->Common->currentUrl(true);
+		$this->assertTrue(!is_array($is) && !empty($is));
+	}
+
+	/**
+	 * CommonComponentTest::testIsForeignReferer()
+	 *
+	 * @return void
+	 */
+	public function testIsForeignReferer() {
+		$ref = 'http://www.spiegel.de';
+		$is = $this->Controller->Common->isForeignReferer($ref);
+		$this->assertTrue($is);
+
+		$ref = Configure::read('App.fullBaseUrl') . '/some/controller/action';
+		$is = $this->Controller->Common->isForeignReferer($ref);
+		$this->assertFalse($is);
+
+		$ref = '';
+		$is = $this->Controller->Common->isForeignReferer($ref);
+		$this->assertFalse($is);
+	}
+
+	/**
+	 * CommonComponentTest::testTransientFlashMessage()
+	 *
+	 * @return void
+	 */
+	public function testTransientFlashMessage() {
+		$is = $this->Controller->Common->transientFlashMessage('xyz', 'success');
+		//$this->assertTrue($is);
+
+		$res = Configure::read('messages');
+		//debug($res);
+		$this->assertTrue(!empty($res));
+		$this->assertTrue(isset($res['success'][0]) && $res['success'][0] === 'xyz');
+	}
+
+	/**
+	 * CommonComponentTest::testFlashMessage()
+	 *
+	 * @return void
+	 */
+	public function testFlashMessage() {
+		$this->Controller->request->session()->delete('messages');
+		$is = $this->Controller->Common->flashMessage('efg');
+
+		$res = $this->Controller->request->session()->read('messages');
+		$this->assertTrue(!empty($res));
+		$this->assertTrue(isset($res['info'][0]) && $res['info'][0] === 'efg');
+	}
+
+}
+
+/*** additional helper classes ***/
+
+class MyToolsUser extends \Cake\Orm\Entity {
+
+	public $useTable = 'tools_users';
+
+	public $name = 'MyToolsUser';
+
+	public $alias = 'User';
+
+	public $belongsTo = array(
+		'Role',
+	);
+
+}
+
+// Use Controller instead of AppController to avoid conflicts
+class CommonComponentTestController extends Controller {
+
+	public $components = array('Tools.Common');
+
+	public $failed = false;
+
+	public $testHeaders = array();
+
+	public function fail() {
+		$this->failed = true;
+	}
+
+	public function redirect($url, $status = null, $exit = true) {
+		return $status;
+	}
+
+	public function header($status) {
+		$this->testHeaders[] = $status;
+	}
+}

+ 172 - 0
tests/TestCase/Controller/Component/SessionComponentTest.php

@@ -0,0 +1,172 @@
+<?php
+namespace Tools\Test\TestCase\Controller\Component;
+
+use Cake\Controller\ComponentRegistry;
+use Cake\Controller\Component\SessionComponent;
+use Cake\Controller\Controller;
+use Cake\Core\Configure;
+use Cake\Network\Request;
+use Cake\Network\Session;
+use Cake\Routing\DispatcherFactory;
+use Cake\TestSuite\TestCase;
+
+/**
+ * SessionComponentTest class
+ *
+ */
+class SessionComponentTest extends TestCase {
+
+	protected static $_sessionBackup;
+
+/**
+ * fixtures
+ *
+ * @var string
+ */
+	public $fixtures = array('core.sessions');
+
+/**
+ * test case startup
+ *
+ * @return void
+ */
+	public static function setupBeforeClass() {
+		DispatcherFactory::add('Routing');
+		DispatcherFactory::add('ControllerFactory');
+	}
+
+/**
+ * cleanup after test case.
+ *
+ * @return void
+ */
+	public static function teardownAfterClass() {
+		DispatcherFactory::clear();
+	}
+
+/**
+ * setUp method
+ *
+ * @return void
+ */
+	public function setUp() {
+		parent::setUp();
+		$_SESSION = [];
+		Configure::write('App.namespace', 'TestApp');
+		$controller = new Controller(new Request(['session' => new Session()]));
+		$this->ComponentRegistry = new ComponentRegistry($controller);
+	}
+
+/**
+ * tearDown method
+ *
+ * @return void
+ */
+	public function tearDown() {
+		parent::tearDown();
+	}
+
+/**
+ * testSessionReadWrite method
+ *
+ * @return void
+ */
+	public function testSessionReadWrite() {
+		$Session = new SessionComponent($this->ComponentRegistry);
+
+		$this->assertNull($Session->read('Test'));
+
+		$Session->write('Test', 'some value');
+		$this->assertEquals('some value', $Session->read('Test'));
+		$Session->delete('Test');
+
+		$Session->write('Test.key.path', 'some value');
+		$this->assertEquals('some value', $Session->read('Test.key.path'));
+		$this->assertEquals(array('path' => 'some value'), $Session->read('Test.key'));
+		$Session->write('Test.key.path2', 'another value');
+		$this->assertEquals(array('path' => 'some value', 'path2' => 'another value'), $Session->read('Test.key'));
+		$Session->delete('Test');
+
+		$array = array('key1' => 'val1', 'key2' => 'val2', 'key3' => 'val3');
+		$Session->write('Test', $array);
+		$this->assertEquals($Session->read('Test'), $array);
+		$Session->delete('Test');
+
+		$Session->write(array('Test'), 'some value');
+		$Session->write(array('Test' => 'some value'));
+		$this->assertEquals('some value', $Session->read('Test'));
+		$Session->delete('Test');
+	}
+
+/**
+ * testSessionDelete method
+ *
+ * @return void
+ */
+	public function testSessionDelete() {
+		$Session = new SessionComponent($this->ComponentRegistry);
+
+		$Session->write('Test', 'some value');
+		$Session->delete('Test');
+		$this->assertNull($Session->read('Test'));
+	}
+
+/**
+ * testSessionCheck method
+ *
+ * @return void
+ */
+	public function testSessionCheck() {
+		$Session = new SessionComponent($this->ComponentRegistry);
+
+		$this->assertFalse($Session->check('Test'));
+
+		$Session->write('Test', 'some value');
+		$this->assertTrue($Session->check('Test'));
+		$Session->delete('Test');
+	}
+
+/**
+ * testSessionFlash method
+ *
+ * @return void
+ */
+	public function testSessionFlash() {
+		$Session = new SessionComponent($this->ComponentRegistry);
+
+		$this->assertNull($Session->read('Flash.flash'));
+
+		$Session->setFlash('This is a test message');
+		$this->assertEquals(array(
+				'message' => 'This is a test message',
+				'element' => null,
+				'params' => array(),
+				'key' => 'flash'
+			), $Session->read('Flash.flash'));
+	}
+
+/**
+ * testSessionId method
+ *
+ * @return void
+ */
+	public function testSessionId() {
+		$Session = new SessionComponent($this->ComponentRegistry);
+		$this->assertEquals(session_id(), $Session->id());
+	}
+
+/**
+ * testSessionDestroy method
+ *
+ * @return void
+ */
+	public function testSessionDestroy() {
+		$Session = new SessionComponent($this->ComponentRegistry);
+
+		$Session->write('Test', 'some value');
+		$this->assertEquals('some value', $Session->read('Test'));
+		$Session->destroy('Test');
+		$this->assertNull($Session->read('Test'));
+	}
+
+}

+ 236 - 0
tests/TestCase/View/Helper/CommonHelperTest.php

@@ -0,0 +1,236 @@
+<?php
+namespace Tools\TestCase\View\Helper;
+
+use Tools\View\Helper\CommonHelper;
+use Tools\TestSuite\TestCase;
+use Cake\View\View;
+use Cake\Core\Configure;
+use Cake\Routing\Router;
+
+/**
+ * CommonHelper tests
+ */
+class CommonHelperTest extends TestCase {
+
+	public $fixtures = array('core.sessions');
+
+	public $Common;
+
+	public function setUp() {
+		parent::setUp();
+
+		Router::reload();
+		$View = new View(null);
+		$this->Common = new CommonHelper($View);
+	}
+
+	/**
+	 * CommonHelperTest::testFlashMessage()
+	 *
+	 * @return void
+	 */
+	public function testFlashMessage() {
+		$result = $this->Common->flashMessage(h('Foo & bar'), 'success');
+		$expected = '<div class="flash-messages flashMessages"><div class="message success">Foo &amp;amp; bar</div></div>';
+		$this->assertEquals($expected, $result);
+	}
+
+	/**
+	 * CommonHelperTest::testMetaRobots()
+	 *
+	 * @return void
+	 */
+	public function testMetaRobots() {
+		$result = $this->Common->metaRobots();
+		$this->assertContains('<meta name="robots" content="', $result);
+	}
+
+	/**
+	 * CommonHelperTest::testMetaName()
+	 *
+	 * @return void
+	 */
+	public function testMetaName() {
+		$result = $this->Common->metaName('foo', array(1, 2, 3));
+		$expected = '<meta name="foo" content="1, 2, 3" />';
+		$this->assertEquals($expected, $result);
+	}
+
+	/**
+	 * CommonHelperTest::testMetaDescription()
+	 *
+	 * @return void
+	 */
+	public function testMetaDescription() {
+		$result = $this->Common->metaDescription('foo', 'deu');
+		$expected = '<meta lang="deu" name="description" content="foo"/>';
+		$this->assertEquals($expected, $result);
+	}
+
+	/**
+	 * CommonHelperTest::testMetaKeywords()
+	 *
+	 * @return void
+	 */
+	public function testMetaKeywords() {
+		$result = $this->Common->metaKeywords('foo bar', 'deu');
+		$expected = '<meta lang="deu" name="keywords" content="foo bar"/>';
+		$this->assertEquals($expected, $result);
+	}
+
+	/**
+	 * CommonHelperTest::testMetaRss()
+	 *
+	 * @return void
+	 */
+	public function testMetaRss() {
+		$result = $this->Common->metaRss('/some/url', 'some title');
+		$expected = '<link rel="alternate" type="application/rss+xml" title="some title" href="/some/url" />';
+		$this->assertEquals($expected, $result);
+	}
+
+	/**
+	 * CommonHelperTest::testMetaEquiv()
+	 *
+	 * @return void
+	 */
+	public function testMetaEquiv() {
+		$result = $this->Common->metaEquiv('type', 'value');
+		$expected = '<meta http-equiv="type" content="value" />';
+		$this->assertEquals($expected, $result);
+	}
+
+	/**
+	 * CommonHelperTest::testFlash()
+	 *
+	 * @return void
+	 */
+	public function testFlash() {
+		$this->Common->addFlashMessage(h('Foo & bar'), 'success');
+
+		$result = $this->Common->flash();
+		$expected = '<div class="flash-messages flashMessages"><div class="message success">Foo &amp; bar</div></div>';
+		$this->assertEquals($expected, $result);
+
+		$this->Common->addFlashMessage('I am an error', 'error');
+		$this->Common->addFlashMessage('I am a warning', 'warning');
+		$this->Common->addFlashMessage('I am some info', 'info');
+		$this->Common->addFlashMessage('I am also some info');
+		$this->Common->addFlashMessage('I am sth custom', 'custom');
+
+		$result = $this->Common->flash();
+		$this->assertTextContains('message error', $result);
+		$this->assertTextContains('message warning', $result);
+		$this->assertTextContains('message info', $result);
+		$this->assertTextContains('message custom', $result);
+
+		$result = substr_count($result, 'message info');
+		$this->assertSame(2, $result);
+	}
+
+	/**
+	 * Test that you can define your own order or just output a subpart of
+	 * the types.
+	 *
+	 * @return void
+	 */
+	public function testFlashWithTypes() {
+		$this->Common->addFlashMessage('I am an error', 'error');
+		$this->Common->addFlashMessage('I am a warning', 'warning');
+		$this->Common->addFlashMessage('I am some info', 'info');
+		$this->Common->addFlashMessage('I am also some info');
+		$this->Common->addFlashMessage('I am sth custom', 'custom');
+
+		$result = $this->Common->flash(array('warning', 'error'));
+		$expected = '<div class="flash-messages flashMessages"><div class="message warning">I am a warning</div><div class="message error">I am an error</div></div>';
+		$this->assertEquals($expected, $result);
+
+		$result = $this->Common->flash(array('info'));
+		$expected = '<div class="flash-messages flashMessages"><div class="message info">I am some info</div><div class="message info">I am also some info</div></div>';
+		$this->assertEquals($expected, $result);
+
+		$result = $this->Common->flash();
+		$expected = '<div class="flash-messages flashMessages"><div class="message custom">I am sth custom</div></div>';
+		$this->assertEquals($expected, $result);
+	}
+
+	/**
+	 * @return void
+	 */
+	public function testMetaCanonical() {
+		$is = $this->Common->metaCanonical('/some/url/param1');
+		$this->assertEquals('<link href="' . $this->Common->Url->build('/some/url/param1') . '" rel="canonical"/>', trim($is));
+
+		$is = $this->Common->metaCanonical('/some/url/param1', true);
+		$this->assertEquals('<link href="' . $this->Common->Url->build('/some/url/param1', true) . '" rel="canonical"/>', trim($is));
+	}
+
+	/**
+	 * @return void
+	 */
+	public function testMetaAlternate() {
+		$is = $this->Common->metaAlternate('/some/url/param1', 'de-de', true);
+		$this->assertEquals('<link href="' . $this->Common->Url->build('/some/url/param1', true) . '" rel="alternate" hreflang="de-de"/>', trim($is));
+
+		$is = $this->Common->metaAlternate(array('controller' => 'some', 'action' => 'url'), 'de', true);
+		$this->assertEquals('<link href="' . $this->Common->Url->build('/some/url', true) . '" rel="alternate" hreflang="de"/>', trim($is));
+
+		$is = $this->Common->metaAlternate(array('controller' => 'some', 'action' => 'url'), array('de', 'de-ch'), true);
+		$this->assertEquals('<link href="' . $this->Common->Url->build('/some/url', true) . '" rel="alternate" hreflang="de"/>' . PHP_EOL . '<link href="' . $this->Common->Url->build('/some/url', true) . '" rel="alternate" hreflang="de-ch"/>', trim($is));
+
+		$is = $this->Common->metaAlternate(array('controller' => 'some', 'action' => 'url'), array('de' => array('ch', 'at'), 'en' => array('gb', 'us')), true);
+		$this->assertEquals('<link href="' . $this->Common->Url->build('/some/url', true) . '" rel="alternate" hreflang="de-ch"/>' . PHP_EOL .
+			'<link href="' . $this->Common->Url->build('/some/url', true) . '" rel="alternate" hreflang="de-at"/>' . PHP_EOL .
+			'<link href="' . $this->Common->Url->build('/some/url', true) . '" rel="alternate" hreflang="en-gb"/>' . PHP_EOL .
+			'<link href="' . $this->Common->Url->build('/some/url', true) . '" rel="alternate" hreflang="en-us"/>', trim($is));
+	}
+
+	/**
+	 * CommonHelperTest::testAsp()
+	 *
+	 * @return void
+	 */
+	public function testAsp() {
+		$res = $this->Common->asp('House', 2, true);
+		$expected = __d('tools', 'Houses');
+		$this->assertEquals($expected, $res);
+
+		$res = $this->Common->asp('House', 1, true);
+		$expected = __d('tools', 'House');
+		$this->assertEquals($expected, $res);
+	}
+
+	/**
+	 * CommonHelperTest::testSp()
+	 *
+	 * @return void
+	 */
+	public function testSp() {
+		$res = $this->Common->sp('House', 'Houses', 0, true);
+		$expected = __d('tools', 'Houses');
+		$this->assertEquals($expected, $res);
+
+		$res = $this->Common->sp('House', 'Houses', 2, true);
+		$this->assertEquals($expected, $res);
+
+		$res = $this->Common->sp('House', 'Houses', 1, true);
+		$expected = __d('tools', 'House');
+		$this->assertEquals($expected, $res);
+
+		$res = $this->Common->sp('House', 'Houses', 1);
+		$expected = 'House';
+		$this->assertEquals($expected, $res);
+	}
+
+	/**
+	 * TearDown method
+	 *
+	 * @return void
+	 */
+	public function tearDown() {
+		parent::tearDown();
+
+		unset($this->Common);
+	}
+
+}

+ 339 - 0
tests/TestCase/View/Helper/FormatHelperTest.php

@@ -0,0 +1,339 @@
+<?php
+namespace Tools\TestCase\View\Helper;
+
+use Tools\View\Helper\FormatHelper;
+use Tools\TestSuite\TestCase;
+use Cake\View\View;
+use Cake\Core\Configure;
+
+/**
+ * Datetime Test Case
+ */
+class FormatHelperTest extends TestCase {
+
+	public $fixtures = array('core.sessions');
+
+	public $Format;
+
+	public function setUp() {
+		parent::setUp();
+
+		Configure::write('App.imageBaseUrl', 'img/');
+
+		$this->Format = new FormatHelper(new View(null));
+	}
+
+	/**
+	 * @return void
+	 */
+	public function testDisabledLink() {
+		$content = 'xyz';
+		$data = array(
+			array(),
+			array('class' => 'disabledLink', 'title' => false),
+			array('class' => 'helloClass', 'title' => 'helloTitle')
+		);
+		foreach ($data as $key => $value) {
+			$res = $this->Format->disabledLink($content, $value);
+			//echo ''.$res.' (\''.h($res).'\')';
+			$this->assertTrue(!empty($res));
+		}
+	}
+
+	/**
+	 * @return void
+	 */
+	public function testWarning() {
+		$content = 'xyz';
+		$data = array(
+			true,
+			false
+		);
+		foreach ($data as $key => $value) {
+			$res = $this->Format->warning($content . ' ' . (int)$value, $value);
+			//echo ''.$res.'';
+			$this->assertTrue(!empty($res));
+		}
+	}
+
+	/**
+	 * FormatHelperTest::testIcon()
+	 *
+	 * @return void
+	 */
+	public function testIcon() {
+		$result = $this->Format->icon('edit');
+		$expected = '<img src="/img/icons/edit.gif" title="' . __d('tools', 'Edit') . '" alt="[' . __d('tools', 'Edit') . ']" class="icon"/>';
+		$this->assertEquals($expected, $result);
+	}
+
+	/**
+	 * FormatHelperTest::testCIcon()
+	 *
+	 * @return void
+	 */
+	public function testCIcon() {
+		$result = $this->Format->cIcon('edit.png');
+		$expected = '<img src="/img/icons/edit.png" title="' . __d('tools', 'Edit') . '" alt="[' . __d('tools', 'Edit') . ']" class="icon"/>';
+		$this->assertEquals($expected, $result);
+	}
+
+	/**
+	 * FormatHelperTest::testIconWithFontIcon()
+	 *
+	 * @return void
+	 */
+	public function testIconWithFontIcon() {
+		$this->Format->config('fontIcons', array('edit' => 'fa fa-pencil'));
+		$result = $this->Format->icon('edit');
+		$expected = '<i class="fa fa-pencil edit" title="' . __d('tools', 'Edit') . '" data-placement="bottom" data-toggle="tooltip"></i>';
+		$this->assertEquals($expected, $result);
+	}
+
+	/**
+	 * FormatHelperTest::testCIconWithFontIcon()
+	 *
+	 * @return void
+	 */
+	public function testCIconWithFontIcon() {
+		$this->Format->config('fontIcons', array('edit' => 'fa fa-pencil'));
+		$result = $this->Format->cIcon('edit.png');
+		$expected = '<i class="fa fa-pencil edit" title="' . __d('tools', 'Edit') . '" data-placement="bottom" data-toggle="tooltip"></i>';
+		$this->assertEquals($expected, $result);
+	}
+
+	/**
+	 * FormatHelperTest::testSpeedOfIcons()
+	 *
+	 * @return void
+	 */
+	public function testSpeedOfIcons() {
+		$count = 1000;
+
+		$time1 = microtime(true);
+		for ($i = 0; $i < $count; $i++) {
+			$result = $this->Format->icon('edit');
+		}
+		$time2 = microtime(true);
+
+		$this->Format->config('fontIcons', array('edit' => 'fa fa-pencil'));
+
+		$time3 = microtime(true);
+		for ($i = 0; $i < $count; $i++) {
+			$result = $this->Format->icon('edit');
+		}
+		$time4 = microtime(true);
+
+		$normalIconSpeed = number_format($time2 - $time1, 2);
+		$this->debug('Normal Icons: ' . $normalIconSpeed);
+		$fontIconViaStringTemplateSpeed = number_format($time4 - $time3, 2);
+		$this->debug('StringTemplate and Font Icons: ' . $fontIconViaStringTemplateSpeed);
+		$this->assertTrue($fontIconViaStringTemplateSpeed < $normalIconSpeed);
+	}
+
+	/**
+	 * @return void
+	 */
+	public function testFontIcon() {
+		$result = $this->Format->fontIcon('signin');
+		$expected = '<i class="fa-signin"></i>';
+		$this->assertEquals($expected, $result);
+
+		$result = $this->Format->fontIcon('signin', array('rotate' => 90));
+		$expected = '<i class="fa-signin fa-rotate-90"></i>';
+		$this->assertEquals($expected, $result);
+
+		$result = $this->Format->fontIcon('signin', array('size' => 5, 'extra' => array('muted')));
+		$expected = '<i class="fa-signin fa-muted fa-5x"></i>';
+		$this->assertEquals($expected, $result);
+
+		$result = $this->Format->fontIcon('signin', array('size' => 5, 'extra' => array('muted'), 'namespace' => 'icon'));
+		$expected = '<i class="icon-signin icon-muted icon-5x"></i>';
+		$this->assertEquals($expected, $result);
+	}
+
+	/**
+	 * @return void
+	 */
+	public function testYesNo() {
+		$result = $this->Format->yesNo(true);
+		$expected = '<img src="/img/icons/yes.gif" title="' . __d('tools', 'Yes') . '" alt=';
+		$this->assertTextContains($expected, $result);
+
+		$result = $this->Format->yesNo(false);
+		$expected = '<img src="/img/icons/no.gif" title="' . __d('tools', 'No') . '" alt=';
+		$this->assertTextContains($expected, $result);
+
+		$this->Format->config('fontIcons', array(
+			'yes' => 'fa fa-check',
+			'no' => 'fa fa-times'));
+
+		$result = $this->Format->yesNo(true);
+		$expected = '<i class="fa fa-check yes" title="' . __d('tools', 'Yes') . '" data-placement="bottom" data-toggle="tooltip"></i>';
+		$this->assertEquals($expected, $result);
+
+		$result = $this->Format->yesNo(false);
+		$expected = '<i class="fa fa-times no" title="' . __d('tools', 'No') . '" data-placement="bottom" data-toggle="tooltip"></i>';
+		$this->assertEquals($expected, $result);
+	}
+
+
+	/**
+	 * @return void
+	 */
+	public function testOk() {
+		$content = 'xyz';
+		$data = array(
+			true,
+			false
+		);
+		foreach ($data as $key => $value) {
+			$res = $this->Format->ok($content . ' ' . (int)$value, $value);
+			//echo ''.$res.'';
+			$this->assertTrue(!empty($res));
+		}
+	}
+
+	/**
+	 * FormatHelperTest::testThumbs()
+	 *
+	 * @return void
+	 */
+	public function testThumbs() {
+		$result = $this->Format->thumbs(1);
+		$this->assertNotEmpty($result);
+	}
+
+	/**
+	 * FormatHelperTest::testGenderIcon()
+	 *
+	 * @return void
+	 */
+	public function testGenderIcon() {
+		$result = $this->Format->genderIcon();
+		$this->assertNotEmpty($result);
+	}
+
+	/**
+	 * FormatHelperTest::testPad()
+	 *
+	 * @return void
+	 */
+	public function testPad() {
+		$result = $this->Format->pad('foo bar', 20, '-');
+		$expected = 'foo bar-------------';
+		$this->assertEquals($expected, $result);
+
+		$result = $this->Format->pad('foo bar', 20, '-', STR_PAD_LEFT);
+		$expected = '-------------foo bar';
+		$this->assertEquals($expected, $result);
+	}
+
+	/**
+	 * FormatHelperTest::testAbsolutePaginateCount()
+	 *
+	 * @return void
+	 */
+	public function testAbsolutePaginateCount() {
+		$paginator = array(
+			'page' => 1,
+			'pageCount' => 3,
+			'count' => 25,
+			'limit' => 10
+		);
+		$result = $this->Format->absolutePaginateCount($paginator, 2);
+		$this->debug($result);
+		$this->assertEquals(2, $result);
+	}
+
+	/**
+	 * FormatHelperTest::testSiteIcon()
+	 *
+	 * @return void
+	 */
+	public function testSiteIcon() {
+		$result = $this->Format->siteIcon('http://www.example.org');
+		$this->debug($result);
+		$expected = '<img src="http://www.google.com/s2/favicons?domain=www.example.org';
+		$this->assertContains($expected, $result);
+	}
+
+	/**
+	 * FormatHelperTest::testConfigure()
+	 *
+	 * @return void
+	 */
+	public function testNeighbors() {
+		if (!defined('ICON_PREV')) {
+			define('ICON_PREV', 'prev');
+		}
+		if (!defined('ICON_NEXT')) {
+			define('ICON_NEXT', 'next');
+		}
+
+		$neighbors = array(
+			'prev' => array('ModelName' => array('id' => 1, 'foo' => 'bar')),
+			'next' => array('ModelName' => array('id' => 2, 'foo' => 'y')),
+		);
+		$result = $this->Format->neighbors($neighbors, 'foo');
+		$expected = '<div class="next-prev-navi nextPrevNavi"><a href="/index/1" title="bar"><img src="/img/icons/prev" alt="" class="icon"/>&nbsp;prevRecord</a>&nbsp;&nbsp;<a href="/index/2" title="y"><img src="/img/icons/next" alt="" class="icon"/>&nbsp;nextRecord</a></div>';
+
+		$this->assertEquals($expected, $result);
+
+		$this->Format->config('fontIcons', array(
+			'prev' => 'fa fa-prev',
+			'next' => 'fa fa-next'));
+		$result = $this->Format->neighbors($neighbors, 'foo');
+		$expected = '<div class="next-prev-navi nextPrevNavi"><a href="/index/1" title="bar"><i class="fa fa-prev prev" title="" data-placement="bottom" data-toggle="tooltip"></i>&nbsp;prevRecord</a>&nbsp;&nbsp;<a href="/index/2" title="y"><i class="fa fa-next next" title="" data-placement="bottom" data-toggle="tooltip"></i>&nbsp;nextRecord</a></div>';
+		$this->assertEquals($expected, $result);
+	}
+
+	/**
+	 * FormatHelperTest::testTab2space()
+	 *
+	 * @return void
+	 */
+	public function testTab2space() {
+		$text = "foo\t\tfoobar\tbla\n";
+		$text .= "fooo\t\tbar\t\tbla\n";
+		$text .= "foooo\t\tbar\t\tbla\n";
+		$result = $this->Format->tab2space($text);
+		//echo "<pre>" . $text . "</pre>";
+		//echo'becomes';
+		//echo "<pre>" . $result . "</pre>";
+	}
+
+	/**
+	 * FormatHelperTest::testArray2table()
+	 *
+	 * @return void
+	 */
+	public function testArray2table() {
+		$array = array(
+			array('x' => '0', 'y' => '0.5', 'z' => '0.9'),
+			array('1', '2', '3'),
+			array('4', '5', '6'),
+		);
+
+		$is = $this->Format->array2table($array);
+		//echo $is;
+		//$this->assertEquals($expected, $is);
+
+		// recursive?
+		$array = array(
+			array('a' => array('2'), 'b' => array('2'), 'c' => array('2')),
+			array(array('2'), array('2'), array('2')),
+			array(array('2'), array('2'), array(array('s' => '3', 't' => '4'))),
+		);
+
+		$is = $this->Format->array2table($array, array('recursive' => true));
+		//echo $is;
+	}
+
+	public function tearDown() {
+		parent::tearDown();
+
+		unset($this->Format);
+	}
+
+}

+ 0 - 1
tests/TestCase/View/Helper/GoogleMapV3HelperTest.php

@@ -31,7 +31,6 @@ class GoogleMapV3HelperTest extends TestCase {
 		$this->GoogleMapV3 = new GoogleMapV3Helper(new View(null), $config);
 
 		$result = $this->GoogleMapV3->config();
-		debug($result);
 		$this->assertEquals('foo', $result['map']['type']);
 		$this->assertEquals(8, $result['map']['zoom']);
 	}

+ 91 - 0
tests/TestCase/View/Helper/TimeHelperTest.php

@@ -0,0 +1,91 @@
+<?php
+namespace Tools\TestCase\View\Helper;
+
+use Tools\View\Helper\TimeHelper;
+use Tools\TestSuite\TestCase;
+use Cake\View\View;
+use Cake\Core\Configure;
+use Tools\Utility\Time;
+
+/**
+ * Datetime Test Case
+ *
+ */
+class TimeHelperTest extends TestCase {
+
+	public function setUp() {
+		parent::setUp();
+
+		$this->Time = new TimeHelper(new View(null));
+	}
+
+	/**
+	 * Test user age
+	 *
+	 * @return void
+	 */
+	public function testUserAge() {
+		$res = $this->Time->userAge((date('Y') - 4) . '-01-01');
+		$this->assertTrue($res >= 3 && $res <= 5);
+
+		$res = $this->Time->userAge('2023-01-01');
+		$this->assertSame('---', $res);
+
+		$res = $this->Time->userAge('1903-01-01');
+		$this->assertSame('---', $res);
+
+		$res = $this->Time->userAge('1901-01-01');
+		$this->assertSame('---', $res);
+	}
+
+	/**
+	 * Tests that calling a CakeTime method works.
+	 *
+	 * @return void
+	 */
+	public function testTimeAgoInWords() {
+		$res = $this->Time->timeAgoInWords(date(FORMAT_DB_DATETIME, time() - 4 * DAY - 5 * HOUR));
+		$this->debug($res);
+	}
+
+	/**
+	 * DatetimeHelperTest::testPublished()
+	 *
+	 * @return void
+	 */
+	public function testPublished() {
+		$result = $this->Time->published((new Time(date(FORMAT_DB_DATETIME)))->addSeconds(1));
+		$expected = 'class="published today';
+		$this->assertContains($expected, $result);
+
+		$result = $this->Time->published((new Time(date(FORMAT_DB_DATETIME)))->addDays(1));
+		$expected = 'class="published notyet';
+		$this->assertContains($expected, $result);
+
+		$result = $this->Time->published((new Time(date(FORMAT_DB_DATETIME)))->subDays(2));
+		$expected = 'class="published already';
+		$this->assertContains($expected, $result);
+	}
+
+	/**
+	 * DatetimeHelperTest::testTimezones()
+	 *
+	 * @return void
+	 */
+	public function testTimezones() {
+		$result = $this->Time->timezones();
+		$this->assertTrue(!empty($result));
+	}
+
+	/**
+	 * TearDown method
+	 *
+	 * @return void
+	 */
+	public function tearDown() {
+		parent::tearDown();
+
+		unset($this->Time);
+	}
+
+}

+ 91 - 0
tests/TestCase/View/Helper/TimelineHelperTest.php

@@ -0,0 +1,91 @@
+<?php
+namespace Tools\TestCase\View\Helper;
+
+use Tools\View\Helper\TimelineHelper;
+use Cake\TestSuite\TestCase;
+use Cake\View\View;
+use Cake\Core\Configure;
+
+/**
+ * Timeline Helper Test Case
+ */
+class TimelineHelperTest extends TestCase {
+
+	public $Timeline;
+
+	/**
+	 * TimelineHelperTest::setUp()
+	 *
+	 * @return void
+	 */
+	public function setUp() {
+		parent::setUp();
+
+		$this->Timeline = new TimelineTestHelper(new View(null));
+	}
+
+	/**
+	 * @return void
+	 */
+	public function testAddItem() {
+		$data = array(
+			'start' => null,
+			'content' => '',
+		);
+		$this->Timeline->addItem($data);
+		$items = $this->Timeline->items();
+		$this->assertSame(1, count($items));
+
+		$data = array(
+			array(
+				'start' => null,
+				'content' => '',
+			),
+			array(
+				'start' => null,
+				'content' => '',
+			)
+		);
+		$this->Timeline->addItems($data);
+		$items = $this->Timeline->items();
+		$this->assertSame(3, count($items));
+	}
+
+	/**
+	 * @return void
+	 */
+	public function testFinalize() {
+		$this->testAddItem();
+		$data = array(
+			'start' => new \DateTime(),
+			'content' => '',
+		);
+		$this->Timeline->addItem($data);
+		$data = array(
+			'start' => new \DateTime(date(FORMAT_DB_DATE)),
+			'content' => '',
+		);
+		$this->Timeline->addItem($data);
+
+		$result = $this->Timeline->finalize(true);
+		$this->assertContains('\'start\': new Date(', $result);
+	}
+
+	public function tearDown() {
+		parent::tearDown();
+
+		unset($this->Timeline);
+	}
+
+}
+
+class TimelineTestHelper extends TimelineHelper {
+
+	/**
+	 * @return array
+	 */
+	public function items() {
+		return $this->_items;
+	}
+
+}