Browse Source

Add PasswordableBehavior

euromark 11 years ago
parent
commit
82d18afb92

+ 461 - 0
src/Model/Behavior/PasswordableBehavior.php

@@ -0,0 +1,461 @@
+<?php
+namespace Tools\Model\Behavior;
+
+use Cake\Event\Event;
+use Cake\ORM\Behavior;
+use Cake\ORM\Entity;
+use Cake\ORM\Query;
+use Cake\ORM\Table;
+use Cake\Utility\Inflector;
+use Cake\Core\Configure;
+use Cake\Auth\PasswordHasherFactory;
+
+if (!defined('PWD_MIN_LENGTH')) {
+	define('PWD_MIN_LENGTH', 6);
+}
+if (!defined('PWD_MAX_LENGTH')) {
+	define('PWD_MAX_LENGTH', 20);
+}
+
+/**
+ * A CakePHP behavior to work with passwords the easy way
+ * - complete validation
+ * - hashing of password
+ * - requires fields (no tempering even without security component)
+ * - usable for edit forms (require=>false for optional password update)
+ *
+ * Usage: Do NOT hard-add it in the model itself.
+ * attach it dynamically in only those actions where you actually change the password like so:
+ * $this->Users->addBehavior('Tools.Passwordable', array(SETTINGSARRAY));
+ * as first line in any action where you want to allow the user to change his password
+ * also add the two form fields in the form (pwd, PWD_confirm)
+ * the rest is cake automagic :)
+ *
+ * Also note that you can apply global settings via Configure key 'Passwordable', as well,
+ * if you don't want to manually pass them along each time you use the behavior. This also
+ * keeps the code clean and lean.
+ *
+ * Now also is capable of:
+ * - Require current password prior to altering it (current=>true)
+ * - Don't allow the same password it was before (allowSame=>false)
+ * - Support different auth types and password hashing algorythms
+ * - PasswordHasher support
+ * - Tools.Modern PasswordHasher and password_hash()/password_verify() support
+ *
+ * @author Mark Scherer
+ * @link http://www.dereuromark.de/2011/08/25/working-with-passwords-in-cakephp
+ * @license MIT
+ */
+class PasswordableBehavior extends Behavior {
+
+	/**
+	 * @var array
+	 */
+	protected $_defaultConfig = array(
+		'field' => 'password',
+		'confirm' => true, // Set to false if in admin view and no confirmation (pwd_repeat) is required
+		'require' => true, // If a password change is required (set to false for edit forms, leave it true for pure password update forms)
+		'current' => false, // Enquire the current password for security purposes
+		'formField' => 'pwd',
+		'formFieldRepeat' => 'pwd_repeat',
+		'formFieldCurrent' => 'pwd_current',
+		'userModel' => null, // Defaults to Users
+		'auth' => null, // Which component (defaults to AuthComponent),
+		'authType' => 'Form', // Which type of authenticate (Form, Blowfish, ...)
+		'passwordHasher' => 'Default', // If a custom pwd hasher is been used
+		'allowSame' => true, // Don't allow the old password on change
+		'minLength' => PWD_MIN_LENGTH,
+		'maxLength' => PWD_MAX_LENGTH,
+		'validator' => 'default'
+	);
+
+	/**
+	 * @var array
+	 */
+	protected $_validationRules = array(
+		'formField' => array(
+			'between' => array(
+				'rule' => array('lengthBetween', PWD_MIN_LENGTH, PWD_MAX_LENGTH),
+				'message' => array('valErrBetweenCharacters %s %s', PWD_MIN_LENGTH, PWD_MAX_LENGTH),
+				'last' => true,
+				//'provider' => 'table'
+			)
+		),
+		'formFieldRepeat' => array(
+			'between' => array(
+				'rule' => array('lengthBetween', PWD_MIN_LENGTH, PWD_MAX_LENGTH),
+				'message' => array('valErrBetweenCharacters %s %s', PWD_MIN_LENGTH, PWD_MAX_LENGTH),
+				'last' => true,
+				//'provider' => 'table'
+			),
+			'validateIdentical' => array(
+				'rule' => array('validateIdentical', 'formField'),
+				'message' => 'valErrPwdNotMatch',
+				'last' => true,
+				'provider' => 'table'
+			),
+		),
+		'formFieldCurrent' => array(
+			'notEmpty' => array(
+				'rule' => array('notEmpty'),
+				'message' => 'valErrProvideCurrentPwd',
+				'last' => true,
+			),
+			'validateCurrentPwd' => array(
+				'rule' => 'validateCurrentPwd',
+				'message' => 'valErrCurrentPwdIncorrect',
+				'last' => true,
+				'provider' => 'table'
+			)
+		),
+	);
+
+	/**
+	 * Password hasher instance.
+	 *
+	 * @var AbstractPasswordHasher
+	 */
+	protected $_passwordHasher;
+
+	/**
+	 * Adding validation rules
+	 * also adds and merges config settings (direct + configure)
+	 *
+	 * @return void
+	 */
+	public function __construct(Table $table, array $config = []) {
+		$defaults = $this->_defaultConfig;
+		if ($configureDefaults = Configure::read('Passwordable')) {
+			$defaults = $configureDefaults + $defaults;
+		}
+		$config + $defaults;
+		parent::__construct($table, $config);
+
+		$formField = $this->_config['formField'];
+		$formFieldRepeat = $this->_config['formFieldRepeat'];
+		$formFieldCurrent = $this->_config['formFieldCurrent'];
+
+		if ($formField === $this->_config['field']) {
+			throw new \Exception('Invalid setup - the form field must to be different from the model field (' . $this->_config['field'] . ').');
+		}
+
+		$rules = $this->_validationRules;
+		foreach ($rules as $field => $fieldRules) {
+			foreach ($fieldRules as $key => $rule) {
+				//$rule['allowEmpty'] = !$this->_config['require'];
+
+				if ($key === 'between') {
+					$rule['rule'][1] = $this->_config['minLength'];
+					$rule['message'][1] = $this->_config['minLength'];
+					$rule['rule'][2] = $this->_config['maxLength'];
+					$rule['message'][2] = $this->_config['maxLength'];
+				}
+
+				$fieldRules[$key] = $rule;
+			}
+			$rules[$field] = $fieldRules;
+		}
+
+		$validator = $table->validator($this->_config['validator']);
+
+		// Add the validation rules if not already attached
+		if (!count($validator->field($formField))) {
+			$validator->add($formField, $rules['formField']);
+			$validator->allowEmpty($formField, !$this->_config['require']);
+		}
+		if (!count($validator->field($formFieldRepeat))) {
+			$ruleSet = $rules['formFieldRepeat'];
+			$ruleSet['validateIdentical']['rule'][1] = $formField;
+			$validator->add($formFieldRepeat, $ruleSet);
+			$validator->allowEmpty($formFieldRepeat, !$this->_config['require']);
+		}
+
+		if ($this->_config['current'] && !count($validator->field($formFieldCurrent))) {
+			$validator->add($formFieldCurrent, $rules['formFieldCurrent']);
+			$validator->allowEmpty($formFieldCurrent, !$this->_config['require']);
+
+			if (!$this->_config['allowSame']) {
+				$validator->add($formField, 'validateNotSame', array(
+					'rule' => array('validateNotSame', $formField, $formFieldCurrent),
+					'message' => 'valErrPwdSameAsBefore',
+					'last' => true,
+					'provider' => 'table'
+				));
+			}
+		} elseif (!count($validator->field($formFieldCurrent))) {
+			// Try to match the password against the hash in the DB
+			if (!$this->_config['allowSame']) {
+				$validator->add($formField, 'validateNotSame', array(
+					'rule' => array('validateNotSameHash', $formField),
+					'message' => 'valErrPwdSameAsBefore',
+					//'allowEmpty' => !$this->_config['require'],
+					'last' => true,
+					'provider' => 'table'
+				));
+				$validator->allowEmpty($formField, !$this->_config['require']);
+			}
+		}
+	}
+
+	/**
+	 * Preparing the data
+	 *
+	 * @return void
+	 */
+	public function beforeValidate(Event $event, Entity $entity) {
+		$formField = $this->_config['formField'];
+		$formFieldRepeat = $this->_config['formFieldRepeat'];
+		$formFieldCurrent = $this->_config['formFieldCurrent'];
+
+		// Make sure fields are set and validation rules are triggered - prevents tempering of form data
+		if ($entity->get($formField) === null) {
+			$entity->set($formField, '');
+		}
+		if ($this->_config['confirm'] && $entity->get($formFieldRepeat) === null) {
+			$entity->set($formFieldRepeat, '');
+		}
+		if ($this->_config['current'] && $entity->get($formFieldCurrent) === null) {
+			$entity->set($formFieldCurrent, '');
+		}
+
+		// Check if we need to trigger any validation rules
+		if (!$this->_config['require']) {
+			$current = $entity->get($formFieldCurrent);
+			$new = $entity->get($formField) || $entity->get($formFieldRepeat);
+			if (!$new && !$current) {
+				//$validator->remove($formField); // tmp only!
+				//unset($Model->validate[$formField]);
+				$entity->unsetProperty($formField);
+				if ($this->_config['confirm']) {
+					//$validator->remove($formFieldRepeat); // tmp only!
+					//unset($Model->validate[$formFieldRepeat]);
+					$entity->unsetProperty($formFieldRepeat);
+				}
+				if ($this->_config['current']) {
+					//$validator->remove($formFieldCurrent); // tmp only!
+					//unset($Model->validate[$formFieldCurrent]);
+					$entity->unsetProperty($formFieldCurrent);
+				}
+				return true;
+			}
+			// Make sure we trigger validation if allowEmpty is set but we have the password field set
+			if ($new) {
+				if ($this->_config['confirm'] && !$entity->get($formFieldRepeat)) {
+					$entity->errors($formFieldRepeat, __d('tools', 'valErrPwdNotMatch'));
+				}
+			}
+		}
+
+		// Update whitelist
+		$this->_modifyWhitelist($entity);
+
+		return true;
+	}
+
+	/**
+	 * Hashing the password and whitelisting
+	 *
+	 * @param Event $event
+	 * @return void
+	 */
+	public function beforeSave(Event $event, Entity $entity) {
+		$formField = $this->_config['formField'];
+		$field = $this->_config['field'];
+
+		if ($entity->get($formField) !== null) {
+			$cost = !empty($this->_config['hashCost']) ? $this->_config['hashCost'] : 10;
+			$options = array('cost' => $cost);
+			$PasswordHasher = $this->_getPasswordHasher($this->_config['passwordHasher']);
+			$entity->set($field, $PasswordHasher->hash($entity->get($formField), $options));
+
+			if (!$entity->get($field)) {
+				throw new \Exception('Empty field');
+			}
+
+			$entity->unsetProperty($formField);
+			//$entity->set($formField, null);
+
+			if ($this->_config['confirm']) {
+				$formFieldRepeat = $this->_config['formFieldRepeat'];
+				$entity->unsetProperty($formFieldRepeat);
+				//unset($Model->data[$table->alias()][$formFieldRepeat]);
+			}
+			if ($this->_config['current']) {
+				$formFieldCurrent = $this->_config['formFieldCurrent'];
+				$entity->unsetProperty($formFieldCurrent);
+				//unset($Model->data[$table->alias()][$formFieldCurrent]);
+			}
+		}
+
+		// Update whitelist
+		$this->_modifyWhitelist($entity, true);
+		return true;
+	}
+
+	/**
+	 * Checks if the PasswordHasher class supports this and if so, whether the
+	 * password needs to be rehashed or not.
+	 * This is mainly supported by Tools.Modern (using Bcrypt) yet.
+	 *
+	 * @param string $hash Currently hashed password.
+	 * @return bool Success
+	 */
+	public function needsPasswordRehash($hash) {
+		$PasswordHasher = $this->_getPasswordHasher($this->_config['passwordHasher']);
+		if (!method_exists($PasswordHasher, 'needsRehash')) {
+			return false;
+		}
+		return $PasswordHasher->needsRehash($hash);
+	}
+
+	/**
+	 * If not implemented in AppModel
+	 *
+	 * Note: requires the used Auth component to be App::uses() loaded.
+	 * It also reqires the same Auth setup as in your AppController's beforeFilter().
+	 * So if you set up any special passwordHasher or auth type, you need to provide those
+	 * with the settings passed to the behavior:
+	 *
+	 * 'authType' => 'Blowfish', 'passwordHasher' => array(
+	 *     'className' => 'Simple',
+	 *     'hashType' => 'sha256'
+	 * )
+	 *
+	 * @throws CakeException
+	 * @param Model $Model
+	 * @param array $data
+	 * @return bool Success
+	 */
+	public function validateCurrentPwd($data) {
+		if (is_array($data)) {
+			$pwd = array_shift($data);
+		} else {
+			$pwd = $data;
+		}
+die(debug($data));
+		$uid = null;
+		if ($Model->id) {
+			$uid = $Model->id;
+		} elseif (!empty($Model->data[$table->alias()]['id'])) {
+			$uid = $Model->data[$table->alias()]['id'];
+		} else {
+			trigger_error('No user id given');
+			return false;
+		}
+
+		return $this->_validateSameHash($pwd);
+	}
+
+	/**
+	 * If not implemented in AppModel
+	 *
+	 * @param Model $Model
+	 * @param array $data
+	 * @param string $compareWith String to compare field value with
+	 * @return bool Success
+	 */
+	public function validateIdentical($value, $field, $config) {
+		$compareValue = $config['providers']['entity']->get($field);
+		return ($compareValue === $value);
+	}
+
+	/**
+	 * If not implemented in AppModel
+	 *
+	 * @return bool Success
+	 */
+	public function validateNotSame($data, $field1, $field2) {
+		die(debug($data));
+		$value1 = $Model->data[$table->alias()][$field1];
+		$value2 = $Model->data[$table->alias()][$field2];
+		return ($value1 !== $value2);
+	}
+
+	/**
+	 * If not implemented in AppModel
+	 *
+	 * @return bool Success
+	 */
+	public function validateNotSameHash($data, $formField) {
+		$field = $this->_config['field'];
+die(debug($formField));
+		if (!isset($Model->data[$table->alias()][$Model->primaryKey])) {
+			return true;
+		}
+
+		$primaryKey = $Model->data[$table->alias()][$Model->primaryKey];
+		$value = $Model->data[$table->alias()][$formField];
+
+		$dbValue = $Model->field($field, array($Model->primaryKey => $primaryKey));
+		if (!$dbValue) {
+			return true;
+		}
+
+		$PasswordHasher = $this->_getPasswordHasher($this->_config['passwordHasher']);
+		return !$PasswordHasher->check($value, $dbValue);
+	}
+
+	/**
+	 * PasswordableBehavior::_validateSameHash()
+	 *
+	 * @param Model $Model
+	 * @param string $pwd
+	 * @return bool Success
+	 */
+	protected function _validateSameHash($pwd) {
+		$field = $this->_config['field'];
+
+		$primaryKey = $Model->data[$table->alias()][$Model->primaryKey];
+		$dbValue = $Model->field($field, array($Model->primaryKey => $primaryKey));
+		if (!$dbValue && $pwd) {
+			return false;
+		}
+
+		$PasswordHasher = $this->_getPasswordHasher($this->_config['passwordHasher']);
+		return $PasswordHasher->check($pwd, $dbValue);
+	}
+
+	/**
+	 * PasswordableBehavior::_getPasswordHasher()
+	 *
+	 * @param mixed $hasher Name or options array.
+	 * @return PasswordHasher
+	 */
+	protected function _getPasswordHasher($hasher) {
+		if ($this->_passwordHasher) {
+			return $this->_passwordHasher;
+		}
+		return $this->_passwordHasher = PasswordHasherFactory::build($hasher);
+	}
+
+	/**
+	 * Modify the model's whitelist.
+	 *
+	 * Since 2.5 behaviors can also modify the whitelist for validate, thus this behavior can now
+	 * (>= CakePHP 2.5) add the form fields automatically, as well (not just the password field itself).
+	 *
+	 * @param Model $Model
+	 * @return void
+	 */
+	protected function _modifyWhitelist(Entity $entity, $onSave = false) {
+		$fields = array();
+		if ($onSave) {
+			$fields[] = $this->_config['field'];
+		} else {
+			$fields[] = $this->_config['formField'];
+			if ($this->_config['confirm']) {
+				$fields[] = $this->_config['formFieldRepeat'];
+			}
+			if ($this->_config['current']) {
+				$fields[] = $this->_config['formFieldCurrent'];
+			}
+		}
+
+		foreach ($fields as $field) {
+			if (!empty($Model->whitelist) && !in_array($field, $Model->whitelist)) {
+				$Model->whitelist = array_merge($Model->whitelist, array($field));
+			}
+		}
+	}
+
+}

+ 10 - 3
src/Model/Behavior/SluggedBehavior.php

@@ -75,6 +75,7 @@ class SluggedBehavior extends Behavior {
 		'scope' => array(),
 		'tidy' => true,
 		//'implementedFinders' => ['slugged' => 'findSlugged'],
+		//'implementedMethods' => ['slug' => 'slug']
 	);
 
 /**
@@ -148,6 +149,12 @@ class SluggedBehavior extends Behavior {
 		$this->slug($entity);
 	}
 
+	/**
+	 * SluggedBehavior::slug()
+	 *
+	 * @param Entity $entity
+	 * @return void
+	 */
 	public function slug(Entity $entity) {
 		foreach ((array)$this->_config['label'] as $k => $v) {
 			break;
@@ -318,7 +325,7 @@ class SluggedBehavior extends Behavior {
 	 * @param mixed $id
 	 * @return mixed string (the display name) or false
 	 */
-	public function display(Model $Model, $id = null) {
+	public function display($id = null) {
 		if (!$id) {
 			if (!$Model->id) {
 				return false;
@@ -342,7 +349,7 @@ class SluggedBehavior extends Behavior {
 	 * @param integer $recursive
 	 * @return boolean Success
 	 */
-	public function resetSlugs(Model $Model, $params = array()) {
+	public function resetSlugs($params = array()) {
 		$recursive = -1;
 		extract($this->_config);
 		if (!$Model->hasField($slugField)) {
@@ -392,7 +399,7 @@ class SluggedBehavior extends Behavior {
 	 * @param Model $Model
 	 * @return void
 	 */
-	protected function _multiSlug(Model $Model) {
+	protected function _multiSlug(Entity $entity) {
 		extract($this->_config);
 		$data = $Model->data;
 		$field = current($label);

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

@@ -18,6 +18,12 @@ class Table extends CakeTable {
 	 */
 	public function initialize(array $config) {
 		// Shims
+		if (isset($this->primaryKey)) {
+			$this->primaryKey($this->primaryKey);
+		}
+		if (isset($this->displayField)) {
+			$this->displayField($this->displayField);
+		}
 		$this->_shimRelations();
 
 		$this->addBehavior('Timestamp');

+ 10 - 11
tests/Fixture/RolesFixture.php

@@ -15,17 +15,16 @@ class RolesFixture extends TestFixture {
 	 * @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' => []
+		'id' => ['type' => 'integer'],
+		'name' => array('type' => 'string', 'null' => false, 'length' => 64, 'collate' => 'utf8_unicode_ci', 'comment' => '', 'charset' => 'utf8'),
+		'description' => array('type' => 'string', 'null' => false, 'default' => null, 'collate' => 'utf8_unicode_ci', 'comment' => '', 'charset' => 'utf8'),
+		'alias' => array('type' => 'string', 'null' => false, 'default' => null, 'length' => 20, 'collate' => 'utf8_unicode_ci', 'comment' => '', 'charset' => 'utf8'),
+		'default_role' => array('type' => 'boolean', 'null' => false, 'default' => '0', 'collate' => null, 'comment' => 'set at register'),
+		'created' => array('type' => 'datetime', 'null' => true, 'default' => null, 'collate' => null, 'comment' => ''),
+		'modified' => array('type' => 'datetime', 'null' => true, 'default' => null, 'collate' => null, 'comment' => ''),
+		'sort' => array('type' => 'integer', 'null' => false, 'default' => '0', 'length' => 10, 'collate' => null, 'comment' => ''),
+		'active' => array('type' => 'boolean', 'null' => false, 'default' => '0', 'collate' => null, 'comment' => ''),
+		'_constraints' => ['primary' => ['type' => 'primary', 'columns' => ['id']]]
 	);
 
 	/**

+ 817 - 0
tests/TestCase/Model/Behavior/PasswordableBehaviorTest.php

@@ -0,0 +1,817 @@
+<?php
+
+namespace Tools\Model\Behavior;
+
+use Cake\TestSuite\TestCase;
+use Cake\ORM\Behavior;
+use Cake\ORM\Entity;
+use Cake\ORM\Query;
+use Cake\ORM\Table;
+use Cake\Core\Configure;
+use Cake\Auth\DefaultPasswordHasher;
+use Cake\ORM\TableRegistry;
+use Cake\Utility\Security;
+use Cake\Routing\Router;
+use Cake\Network\Request;
+
+class PasswordableBehaviorTest extends TestCase {
+
+	public $fixtures = array(
+		'plugin.tools.tools_users', 'plugin.tools.roles',
+	);
+
+	/**
+	 * SetUp method
+	 *
+	 * @return void
+	 */
+	public function setUp() {
+		parent::setUp();
+
+		Configure::write('App.namespace', 'TestApp');
+
+		Configure::delete('Passwordable');
+		Configure::write('Passwordable.auth', 'AuthTest');
+
+		$this->Users = TableRegistry::get('ToolsUsers');
+		/*
+		if (isset($this->Users->validate['pwd'])) {
+			unset($this->Users->validate['pwd']);
+		}
+		if (isset($this->Users->validate['pwd_repeat'])) {
+			unset($this->Users->validate['pwd_repeat']);
+		}
+		if (isset($this->Users->validate['pwd_current'])) {
+			unset($this->Users->validate['pwd_current']);
+		}
+		if (isset($this->Users->order)) {
+			unset($this->Users->order);
+		}
+		*/
+
+		$user = $this->Users->newEntity();
+		$data = array(
+			'id' => '5',
+			'name' => 'admin',
+			'password' => Security::hash('somepwd', null, true),
+			'role_id' => '1'
+		);
+		$this->Users->patchEntity($user, $data);
+		$result = $this->Users->save($user);
+		$this->assertTrue((bool)$result);
+
+		Router::setRequestInfo(new Request());
+	}
+
+	/**
+	 * PasswordableBehaviorTest::testObject()
+	 *
+	 * @return void
+	 */
+	public function testObject() {
+		$this->Users->Behaviors->load('Tools.Passwordable', array());
+		$this->assertInstanceOf('PasswordableBehavior', $this->Users->Behaviors->Passwordable);
+		$result = $this->Users->Behaviors->loaded('Passwordable');
+		$this->assertTrue($result);
+	}
+
+	/**
+	 * Make sure validation is triggered correctly
+	 *
+	 * @return void
+	 */
+	public function testValidate() {
+		$this->Users->Behaviors->load('Tools.Passwordable', array());
+
+		$this->Users->create();
+		$data = array(
+			'pwd' => '123456',
+		);
+		$this->Users->set($data);
+		$is = $this->Users->save();
+		//debug($this->Users->validationErrors);
+		$this->assertFalse($is);
+		$this->assertEquals(array('pwd_repeat'), array_keys($this->Users->validationErrors));
+
+		$this->Users->create();
+		$data = array(
+			'pwd' => '1234ab',
+			'pwd_repeat' => '123456'
+		);
+		$this->Users->set($data);
+		$is = $this->Users->save();
+		//debug($this->Users->validationErrors);
+		$this->assertFalse($is);
+		$this->assertEquals(array(__d('tools', 'valErrPwdNotMatch')), $this->Users->validationErrors['pwd_repeat']);
+
+		$this->Users->create();
+		$data = array(
+			'pwd' => '123456',
+			'pwd_repeat' => '123456'
+		);
+		$this->Users->set($data);
+		//debug($this->Users->validate);
+		$is = $this->Users->validates();
+		$this->assertTrue(!empty($is));
+	}
+
+	/**
+	 * Test that confirm false does not require confirmation
+	 *
+	 * @return void
+	 */
+	public function testValidateNoConfirm() {
+		$this->Users->Behaviors->load('Tools.Passwordable', array('confirm' => false));
+		$this->Users->create();
+		$data = array(
+			'pwd' => '123456',
+		);
+		$this->Users->set($data);
+		$is = $this->Users->save();
+		//debug($is);
+		$this->assertTrue(!empty($is));
+	}
+
+	/**
+	 * Trigger validation and update process if no values are entered but are required
+	 *
+	 * @return void
+	 */
+	public function testValidateRequired() {
+		$this->Users->Behaviors->load('Tools.Passwordable');
+		$this->Users->create();
+		$data = array(
+			'pwd' => '',
+			'pwd_repeat' => ''
+		);
+		$this->Users->set($data);
+		$is = $this->Users->save();
+		$this->assertFalse($is);
+		$this->assertEquals(array('pwd', 'pwd_repeat'), array_keys($this->Users->validationErrors));
+	}
+
+	/**
+	 * Validation and update process gets skipped if no values are entered
+	 *
+	 * @return void
+	 */
+	public function testValidateNotRequired() {
+		$this->Users->Behaviors->load('Tools.Passwordable', array('require' => false));
+		$this->Users->create();
+		$data = array(
+			'name' => 'foo', // we need at least one field besides the password on CREATE
+			'pwd' => '',
+			'pwd_repeat' => ''
+		);
+		$this->Users->set($data);
+		$is = $this->Users->save();
+		$this->assertTrue((bool)$is);
+		$this->assertEquals(array('name', 'id'), array_keys($is[$this->Users->alias]));
+
+		$id = $this->Users->id;
+		$data = array(
+			'id' => $id,
+			'pwd' => '',
+			'pwd_repeat' => ''
+		);
+		$this->Users->set($data);
+		$is = $this->Users->save();
+		$this->assertTrue((bool)$is);
+		$this->assertEquals(array('id'), array_keys($is[$this->Users->alias]));
+	}
+
+	/**
+	 * PasswordableBehaviorTest::testValidateEmptyWithCurrentPassword()
+	 *
+	 * @return void
+	 */
+	public function testValidateEmptyWithCurrentPassword() {
+		$this->Users->Behaviors->load('Tools.Passwordable', array('current' => true));
+		$this->Users->create();
+		$data = array(
+			'id' => '123',
+			'pwd' => '',
+			'pwd_repeat' => '',
+			'pwd_current' => '123456',
+		);
+		$this->Users->set($data);
+		$is = $this->Users->save();
+		//debug($this->Users->validationErrors);
+		$this->assertFalse($is);
+		$this->assertEquals(array('pwd', 'pwd_repeat', 'pwd_current'), array_keys($this->Users->validationErrors));
+
+		$this->tearDown();
+		$this->setUp();
+
+		$this->Users->Behaviors->load('Tools.Passwordable', array('require' => false, 'current' => true));
+		$this->Users->create();
+		$data = array(
+			'name' => 'foo',
+			'pwd' => '',
+			'pwd_repeat' => '',
+			'pwd_current' => '',
+		);
+		$is = $this->Users->save($data);
+		$this->assertTrue(!empty($is));
+	}
+
+	/**
+	 * Test aliases for field names
+	 */
+	public function testDifferentFieldNames() {
+		$this->Users->Behaviors->load('Tools.Passwordable', array(
+			'formField' => 'passw',
+			'formFieldRepeat' => 'passw_repeat',
+			'formFieldCurrent' => 'passw_current',
+		));
+		$this->Users->create();
+		$data = array(
+			'passw' => '123456',
+			'passw_repeat' => '123456'
+		);
+		$this->Users->set($data);
+		//debug($this->Users->data);
+		$is = $this->Users->save();
+		$this->assertTrue(!empty($is));
+	}
+
+	/**
+	 * Assert that allowSame false does not allow storing the same password as previously entered
+	 */
+	public function testNotSame() {
+		$this->Users->Behaviors->load('Tools.Passwordable', array(
+			'formField' => 'passw',
+			'formFieldRepeat' => 'passw_repeat',
+			'formFieldCurrent' => 'passw_current',
+			'allowSame' => false,
+			'current' => true,
+		));
+		$this->Users->create();
+		$data = array(
+			'id' => '5',
+			'passw_current' => 'something',
+			'passw' => 'somepwd',
+			'passw_repeat' => 'somepwd'
+		);
+		$this->Users->set($data);
+		$is = $this->Users->save();
+		//debug($this->Users->validationErrors);
+		$this->assertFalse($is);
+
+		$this->Users->create();
+		$data = array(
+			'id' => '5',
+			'passw_current' => 'somepwd',
+			'passw' => 'newpwd',
+			'passw_repeat' => 'newpwd'
+		);
+		$this->Users->set($data);
+		$is = $this->Users->save();
+		$this->assertTrue(!empty($is));
+	}
+
+	/**
+	 * Assert that allowSame false does not allow storing the same password as previously entered
+	 */
+	public function testNotSameWithoutCurrentField() {
+		$this->Users->Behaviors->load('Tools.Passwordable', array(
+			'formField' => 'passw',
+			'formFieldRepeat' => 'passw_repeat',
+			'allowSame' => false,
+			'current' => false
+		));
+		$this->Users->create();
+		$data = array(
+			'passw' => 'somepwd',
+			'passw_repeat' => 'somepwd'
+		);
+		$this->Users->set($data);
+		$is = $this->Users->save();
+		$this->assertTrue((bool)$is);
+		$id = $is[$this->Users->alias]['id'];
+
+		$this->Users->create();
+		$data = array(
+			'id' => $id,
+			'passw' => 'somepwd',
+			'passw_repeat' => 'somepwd'
+		);
+		$this->Users->set($data);
+		$is = $this->Users->save();
+		$this->assertFalse((bool)$is);
+
+		$this->Users->create();
+		$data = array(
+			'id' => $id,
+			'passw' => 'newpwd',
+			'passw_repeat' => 'newpwd'
+		);
+		$this->Users->set($data);
+		$is = $this->Users->save();
+		$this->assertTrue((bool)$is);
+	}
+
+	/**
+	 * Assert that on edit it does not wrongly pass validation (require => false)
+	 */
+	public function testRequireFalse() {
+		$this->Users->Behaviors->load('Tools.Passwordable', array(
+			'formField' => 'passw',
+			'formFieldRepeat' => 'passw_repeat',
+			'require' => false
+		));
+		$this->Users->create();
+		$data = array(
+			'passw' => 'somepwd',
+			'passw_repeat' => 'somepwd'
+		);
+		$this->Users->set($data);
+		$is = $this->Users->save();
+		$this->assertTrue((bool)$is);
+		$id = $is[$this->Users->alias]['id'];
+
+		$this->Users->create();
+		$data = array(
+			'id' => $id,
+			'passw' => 'somepwd2',
+			'passw_repeat' => ''
+		);
+		$this->Users->set($data);
+		$is = $this->Users->save();
+		$this->assertFalse((bool)$is);
+		//debug($this->Users->validationErrors);
+
+		$this->Users->create();
+		$data = array(
+			'id' => $id,
+			'passw' => 'somepwd2',
+			'passw_repeat' => 'somepwd2'
+		);
+		$this->Users->set($data);
+		$is = $this->Users->save();
+		$this->assertTrue((bool)$is);
+	}
+
+	/**
+	 * Needs faking of pwd check...
+	 */
+	public function testValidateCurrent() {
+		$this->assertFalse($this->Users->Behaviors->loaded('Passwordable'));
+		$this->Users->create();
+		$data = array(
+			'name' => 'xyz',
+			'password' => Security::hash('somepwd', null, true));
+		$result = $this->Users->save($data);
+		$this->assertTrue(!empty($result));
+		$uid = (string)$this->Users->id;
+
+		$this->Users->Behaviors->load('Tools.Passwordable', array('current' => true));
+		$this->Users->create();
+		$data = array(
+			'id' => $uid,
+			'pwd' => '123456',
+			'pwd_repeat' => '12345678',
+			//'pwd_current' => '',
+		);
+		$this->Users->set($data);
+		$this->assertTrue($this->Users->Behaviors->loaded('Passwordable'));
+		$is = $this->Users->save();
+		$this->assertFalse($is);
+
+		$this->Users->create();
+		$data = array(
+			'id' => $uid,
+			'pwd_current' => 'somepwdx',
+			'pwd' => '123456',
+			'pwd_repeat' => '123456'
+		);
+		$this->Users->set($data);
+		$is = $this->Users->save();
+		$this->assertFalse($is);
+
+		$this->Users->create();
+		$data = array(
+			'id' => $uid,
+			'name' => 'Yeah',
+			'pwd_current' => 'somepwd',
+			'pwd' => '123456',
+			'pwd_repeat' => '123456'
+		);
+		$this->Users->set($data);
+		// Test whitelist setting - only "password" needs to gets auto-added
+		$options = array('validate' => true, 'fieldList' => array('id', 'pwd', 'pwd_repeat', 'pwd_current'));
+		$is = $this->Users->save(null, $options);
+		$this->assertTrue(!empty($is));
+
+		$user = $this->Users->get($uid);
+		// The password is updated, the name not
+		$this->assertSame($is['ToolsUser']['password'], $user['ToolsUser']['password']);
+		$this->assertSame('xyz', $user['ToolsUser']['name']);
+
+		// Proof that we manually need to add pwd, pwd_repeat etc due to a bug in CakePHP<=2.4 allowing behaviors to only modify saving,
+		// not validating of additional whitelist fields. Validation for those will be just skipped, no matter what the behavior tries
+		// to set.
+		$this->Users->create();
+		$data = array(
+			'id' => $uid,
+			'name' => 'Yeah',
+			'pwd_current' => '123', // Obviously wrong
+			'pwd' => 'some', // Too short
+			'pwd_repeat' => 'somex' // Don't match
+		);
+		$this->Users->set($data);
+		// Test whitelist setting - only "password" gets auto-added, pwd, pwd_repeat etc need to be added manually
+		// NOTE that I had to remove the code for adding those fields from the behavior (as it was not functional)
+		// So of course, this won't work now as expected. But feel free to try to add them in the behavior. Results will be the same.
+		$options = array('validate' => true, 'fieldList' => array('id', 'name'));
+		$is = $this->Users->save(null, $options);
+
+		if ((float)Configure::version() >= 2.5) {
+			// Validation errors triggered - as expected
+			$this->assertFalse($is);
+			$this->assertSame(array('pwd', 'pwd_repeat', 'pwd_current'), array_keys($this->Users->validationErrors));
+			return;
+		}
+
+		// Save is successful
+		$this->assertTrue(!empty($is));
+		$user = $this->Users->get($uid);
+
+		$this->assertSame('Yeah', $user['ToolsUser']['name']);
+
+		// The password is not updated, the name is
+		$this->assertSame($is['ToolsUser']['password'], $user['ToolsUser']['password']);
+		$this->assertSame('Yeah', $user['ToolsUser']['name']);
+	}
+
+	/**
+	 * Test cake2.4 passwordHasher feature
+	 *
+	 * @return void
+	 */
+	public function testPasswordHasher() {
+		$this->skipIf((float)Configure::version() < 2.4, 'Needs 2.4 and above');
+
+		$this->Users->Behaviors->load('Tools.Passwordable', array(
+			'formField' => 'pwd',
+			'formFieldRepeat' => 'pwd_repeat',
+			'allowSame' => false,
+			'current' => false,
+			'passwordHasher' => 'Complex',
+		));
+		$this->Users->create();
+		$data = array(
+			'pwd' => 'somepwd',
+			'pwd_repeat' => 'somepwd'
+		);
+		$this->Users->set($data);
+		$result = $this->Users->save();
+		$this->assertTrue((bool)$result);
+		$uid = (string)$this->Users->id;
+
+		$this->Users->Behaviors->load('Tools.Passwordable', array('current' => true));
+		$this->Users->create();
+		$data = array(
+			'id' => $uid,
+			'pwd' => '123456',
+			'pwd_repeat' => '12345678',
+			//'pwd_current' => '',
+		);
+		$this->Users->set($data);
+		$this->assertTrue($this->Users->Behaviors->loaded('Passwordable'));
+		$is = $this->Users->save();
+		$this->assertFalse($is);
+
+		$this->Users->create();
+		$data = array(
+			'id' => $uid,
+			'pwd_current' => 'somepwdx',
+			'pwd' => '123456',
+			'pwd_repeat' => '123456'
+		);
+		$this->Users->set($data);
+		$is = $this->Users->save();
+		$this->assertFalse($is);
+
+		$this->Users->create();
+		$data = array(
+			'id' => $uid,
+			'pwd_current' => 'somepwd',
+			'pwd' => '123456',
+			'pwd_repeat' => '123456'
+		);
+		$this->Users->set($data);
+		$is = $this->Users->save();
+		$this->assertTrue(!empty($is));
+	}
+
+	/**
+	 * PasswordableBehaviorTest::testBlowfish()
+	 *
+	 * @return void
+	 */
+	public function testBlowfish() {
+		$this->Users->Behaviors->load('Tools.Passwordable', array(
+			'allowSame' => false,
+			'current' => false,
+			'authType' => 'Blowfish',
+		));
+
+		$this->Users->create();
+		$data = array(
+			'pwd' => 'somepwd',
+			'pwd_repeat' => 'somepwd'
+		);
+		$this->Users->set($data);
+		$result = $this->Users->save();
+		$this->assertTrue((bool)$result);
+		$uid = (string)$this->Users->id;
+
+		$this->Users->Behaviors->load('Tools.Passwordable', array('current' => true));
+
+		// Without the current password it will not continue
+		$this->Users->create();
+		$data = array(
+			'id' => $uid,
+			'pwd' => '123456',
+			'pwd_repeat' => '12345678',
+		);
+		$this->Users->set($data);
+		$this->assertTrue($this->Users->Behaviors->loaded('Passwordable'));
+		$result = $this->Users->save();
+		$this->assertFalse($result);
+
+		// Without the correct current password it will not continue
+		$this->Users->create();
+		$data = array(
+			'id' => $uid,
+			'pwd_current' => 'somepwdx',
+			'pwd' => '123456',
+			'pwd_repeat' => '123456'
+		);
+		$this->Users->set($data);
+		$result = $this->Users->save();
+		$this->assertFalse($result);
+
+		// Now it will
+		$this->Users->create();
+		$data = array(
+			'id' => $uid,
+			'pwd_current' => 'somepwd',
+			'pwd' => '123456',
+			'pwd_repeat' => '123456'
+		);
+		$this->Users->set($data);
+		$result = $this->Users->save();
+		$this->assertTrue((bool)$result);
+	}
+
+	/**
+	 * Tests that passwords prior to PHP5.5 and/or password_hash() are still working
+	 * if Tools.Modern is being used.
+	 *
+	 * @return void
+	 */
+	public function testBlowfishWithBC() {
+		$this->skipIf(!function_exists('password_hash'), 'password_hash() is not available.');
+
+		$oldHash = Security::hash('foobar', 'blowfish', false);
+		$newHash = password_hash('foobar', PASSWORD_BCRYPT);
+
+		$this->Users->Behaviors->load('Tools.Passwordable', array(
+			'allowSame' => false,
+			'current' => false,
+			'authType' => 'Blowfish',
+			'passwordHasher' => 'Tools.Modern'
+		));
+		$this->Users->create();
+		$data = array(
+			'pwd' => 'somepwd',
+			'pwd_repeat' => 'somepwd'
+		);
+		$this->Users->set($data);
+		$result = $this->Users->save();
+		$this->assertTrue((bool)$result);
+		$uid = (string)$this->Users->id;
+
+		// Same pwd is not allowed
+		$this->Users->create();
+		$data = array(
+			'id' => $uid,
+			'pwd' => 'somepwd',
+			'pwd_repeat' => 'somepwd'
+		);
+		$this->Users->set($data);
+		$result = $this->Users->save();
+		$this->assertFalse($result);
+
+		$this->Users->Behaviors->load('Tools.Passwordable', array('current' => true));
+
+		// Without the correct current password it will not continue
+		$this->Users->create();
+		$data = array(
+			'id' => $uid,
+			'pwd_current' => 'somepwdxyz',
+			'pwd' => '123456',
+			'pwd_repeat' => '123456'
+		);
+		$this->Users->set($data);
+		$result = $this->Users->save();
+		$this->assertFalse($result);
+
+		// Now it will
+		$this->Users->create();
+		$data = array(
+			'id' => $uid,
+			'pwd_current' => 'somepwd',
+			'pwd' => '123456',
+			'pwd_repeat' => '123456'
+		);
+		$this->Users->set($data);
+		$result = $this->Users->save();
+		$this->assertTrue((bool)$result);
+
+		// Lets set a BC password (without password_hash() method but Security class)
+		$data = array(
+			'id' => $uid,
+			'password' => $oldHash,
+		);
+		$result = $this->Users->save($data, array('validate' => false));
+		$this->assertTrue((bool)$result);
+
+		// Now it will still work
+		$this->Users->create();
+		$data = array(
+			'id' => $uid,
+			'pwd_current' => 'foobar',
+			'pwd' => '123456',
+			'pwd_repeat' => '123456'
+		);
+		$this->Users->set($data);
+		$result = $this->Users->save();
+		$this->assertTrue((bool)$result);
+
+		// Lets set an invalid BC password (without password_hash() method but Security class)
+		$data = array(
+			'id' => $uid,
+			'password' => $oldHash . 'x',
+		);
+		$result = $this->Users->save($data, array('validate' => false));
+		$this->assertTrue((bool)$result);
+
+		// Now it will still work
+		$this->Users->create();
+		$data = array(
+			'id' => $uid,
+			'pwd_current' => 'foobar',
+			'pwd' => '123456',
+			'pwd_repeat' => '123456'
+		);
+		$this->Users->set($data);
+		$result = $this->Users->save();
+		$this->assertFalse($result);
+
+		// Lets set a valid BC password (without password_hash() method but Security class)
+		// But the provided pwd is incorrect
+		$data = array(
+			'id' => $uid,
+			'password' => $oldHash,
+		);
+		$result = $this->Users->save($data, array('validate' => false));
+		$this->assertTrue((bool)$result);
+
+		// Now it will still work
+		$this->Users->create();
+		$data = array(
+			'id' => $uid,
+			'pwd_current' => 'foobarx',
+			'pwd' => '123456',
+			'pwd_repeat' => '123456'
+		);
+		$this->Users->set($data);
+		$result = $this->Users->save();
+		$this->assertFalse($result);
+	}
+
+	/**
+	 * Tests needsPasswordRehash()
+	 *
+	 * @return void
+	 */
+	public function testNeedsPasswordRehash() {
+		$this->skipIf(!function_exists('password_hash'), 'password_hash() is not available.');
+
+		$this->Users->Behaviors->load('Tools.Passwordable', array(
+			'allowSame' => false,
+			'current' => false,
+			'authType' => 'Blowfish',
+			'passwordHasher' => 'Tools.Modern'
+		));
+
+		$hash =  password_hash('foobar', PASSWORD_BCRYPT);
+		$result = $this->Users->needsPasswordRehash($hash);
+		$this->assertFalse($result);
+
+		$hash =  sha1('foobar');
+		$result = $this->Users->needsPasswordRehash($hash);
+		$this->assertTrue($result);
+	}
+
+	/**
+	 * Tests needsPasswordRehash()
+	 *
+	 * @return void
+	 */
+	public function testNeedsPasswordRehashWithNotSupportedHasher() {
+		$this->Users->Behaviors->load('Tools.Passwordable', array(
+			'allowSame' => false,
+			'current' => false,
+			'authType' => 'Blowfish',
+		));
+
+		$hash =  password_hash('foobar', PASSWORD_BCRYPT);
+		$result = $this->Users->needsPasswordRehash($hash);
+		$this->assertFalse($result);
+
+		$this->Users->Behaviors->load('Tools.Passwordable', array(
+			'allowSame' => false,
+			'current' => false,
+			'authType' => 'Blowfish',
+			'passwordHasher' => 'Simple'
+		));
+
+		$hash =  password_hash('foobar', PASSWORD_BCRYPT);
+		$result = $this->Users->needsPasswordRehash($hash);
+		$this->assertFalse($result);
+	}
+
+	/**
+	 * PasswordableBehaviorTest::testSettings()
+	 *
+	 * @return void
+	 */
+	public function testSettings() {
+		// Pwd min and max length
+		$this->Users->Behaviors->load('Tools.Passwordable', array(
+			'allowSame' => false,
+			'current' => false,
+			'minLength' => 3,
+			'maxLength' => 6,
+		));
+		$this->Users->create();
+		$data = array(
+			'pwd' => '123',
+			'pwd_repeat' => '123'
+		);
+		$this->Users->set($data);
+		$result = $this->Users->save();
+		$this->assertTrue((bool)$result);
+
+		$this->Users->create();
+		$data = array(
+			'pwd' => '12345678',
+			'pwd_repeat' => '12345678'
+		);
+		$this->Users->set($data);
+		$result = $this->Users->save();
+		$this->assertFalse($result);
+		$expected = array(
+			'pwd' => array(__d('tools', 'valErrBetweenCharacters %s %s', 3, 6)),
+			'pwd_repeat' => array(__d('tools', 'valErrBetweenCharacters %s %s', 3, 6))
+		);
+		$this->assertEquals($expected, $this->Users->validationErrors);
+	}
+
+	/**
+	 * Test that validate false also works.
+	 *
+	 * @return void
+	 */
+	public function testSaveWithValidateFalse() {
+		$this->Users->Behaviors->load('Tools.Passwordable');
+		$this->Users->create();
+		$data = array(
+			'pwd' => '123',
+		);
+		$this->Users->set($data);
+		$result = $this->Users->save(null, array('validate' => false));
+		$this->assertTrue((bool)$result);
+
+		$uid = (string)$this->Users->id;
+
+		$data = array(
+			'id' => $uid,
+			'pwd' => '1234'
+		);
+		$this->Users->set($data);
+		$result2 = $this->Users->save(null, array('validate' => false));
+		$this->assertTrue((bool)$result2);
+
+		$this->assertTrue($result['ToolsUser']['password'] !== $result2['ToolsUser']['password']);
+	}
+
+}
+
+class ComplexPasswordHasher extends DefaultPasswordHasher {
+
+}

+ 87 - 0
tests/TestCase/View/Helper/NumberHelperTest.php

@@ -0,0 +1,87 @@
+<?php
+namespace Tools\TestCase\View\Helper;
+
+use Tools\View\Helper\NumberHelper;
+use Cake\TestSuite\TestCase;
+use Cake\View\View;
+use Cake\Core\Configure;
+use Tools\Utility\Number;
+
+/**
+ * Numeric Test Case
+ */
+class NumberHelperTest extends TestCase {
+
+	public function setUp() {
+		parent::setUp();
+
+		Configure::write('Localization', array(
+			'decimals' => ',',
+			'thousands' => '.'
+		));
+		Number::config();
+		$this->Numeric = new NumberHelper(new View(null));
+	}
+
+	/**
+	 * Test format
+	 *
+	 * TODO: move to NumberLib test?
+	 *
+	 * @return void
+	 */
+	public function testFormat() {
+		$this->skipIf(true, 'FIXME');
+
+		$is = $this->Numeric->format('22');
+		$expected = '22,00';
+		$this->assertEquals($expected, $is);
+
+		$is = $this->Numeric->format('22.30', array('places' => 1));
+		$expected = '22,3';
+		$this->assertEquals($expected, $is);
+
+		$is = $this->Numeric->format('22.30', array('places' => -1));
+		$expected = '20';
+		$this->assertEquals($expected, $is);
+
+		$is = $this->Numeric->format('22.30', array('places' => -2));
+		$expected = '0';
+		$this->assertEquals($expected, $is);
+
+		$is = $this->Numeric->format('22.30', array('places' => 3));
+		$expected = '22,300';
+		$this->assertEquals($expected, $is);
+
+		$is = $this->Numeric->format('abc', array('places' => 2));
+		$expected = '---';
+		$this->assertEquals($expected, $is);
+
+		/*
+		$is = $this->Numeric->format('12.2', array('places'=>'a'));
+		$expected = '12,20';
+		$this->assertEquals($expected, $is);
+		*/
+
+		$is = $this->Numeric->format('22.3', array('places' => 2, 'before' => 'EUR '));
+		$expected = 'EUR 22,30';
+		$this->assertEquals($expected, $is);
+
+		$is = $this->Numeric->format('22.3', array('places' => 2, 'after' => ' EUR'));
+		$expected = '22,30 EUR';
+		$this->assertEquals($expected, $is);
+
+		$is = $this->Numeric->format('22.3', array('places' => 2, 'after' => 'x', 'before' => 'v'));
+		$expected = 'v22,30x';
+		$this->assertEquals($expected, $is);
+
+		#TODO: more
+
+	}
+
+	public function tearDown() {
+		parent::tearDown();
+
+		unset($this->Numeric);
+	}
+}