m 14 years ago
parent
commit
dc16a1ecdb

+ 228 - 0
Model/Behavior/CaptchaBehavior.php

@@ -0,0 +1,228 @@
+<?php
+
+define('CAPTCHA_MIN_TIME', 3); # seconds the form will need to be filled in by a human
+define('CAPTCHA_MAX_TIME', HOUR);	# seconds the form will need to be submitted in
+
+/**
+ * CaptchaBehavior
+ * NOTES: needs captcha helper
+ *
+ * validate passive or active captchas
+ * active: session-based, db-based or hash-based
+ * 2009-12-12 ms
+ */
+class CaptchaBehavior extends ModelBehavior {
+
+	protected $defaults = array(
+		'minTime' => CAPTCHA_MIN_TIME,
+		'maxTime' => CAPTCHA_MAX_TIME,
+		'log' => false, # log errors
+		'hashType' => null,
+	);
+
+	protected $error = '';
+	protected $internalError = '';
+	//
+	//protected $useSession = false;
+
+	public function setup(Model $Model, $settings = array()) {
+		App::import('Lib', 'Tools.CaptchaLib');
+		$defaults = array_merge(CaptchaLib::$defaults, $this->defaults);
+		$this->Model = $Model;
+		
+		# bootstrap configs
+		$this->settings[$Model->alias] = $defaults;
+		$settings = (array)Configure::read('Captcha');
+		if (!empty($settings)) {
+			$this->settings[$Model->alias] = array_merge($this->settings[$Model->alias], $settings);
+		}
+
+		# local configs in specific action
+		if (!empty($settings['minTime'])) {
+			$this->settings[$Model->alias]['minTime'] = (int)$settings['minTime'];
+		}
+		if (!empty($settings['maxTime'])) {
+			$this->settings[$Model->alias]['maxTime'] = (int)$settings['maxTime'];
+		}
+		if (isset($settings['log'])) {
+			$this->settings[$Model->alias]['log'] = (bool)$settings['log'];
+		}
+		
+		//parent::setup($Model, $settings);
+	}
+
+
+	public function beforeValidate(Model $Model) {
+		parent::beforeValidate($Model);
+		
+		if (!empty($this->Model->whitelist)) {
+			$this->Model->whitelist = array_merge($Model->whitelist, $this->fields());
+		}
+		if (empty($Model->data[$Model->alias])) {
+			$this->Model->invalidate('captcha', 'captchaContentMissing', true);
+			
+		} elseif (!$this->_validateDummyField($Model->data[$Model->alias])) {
+			$this->Model->invalidate('captcha', 'captchaIllegalContent', true);
+
+		} elseif (!$this->_validateCaptchaMinTime($Model->data[$Model->alias])) {
+			$this->Model->invalidate('captcha', 'captchaResultTooFast', true);
+
+		} elseif (!$this->_validateCaptchaMaxTime($Model->data[$Model->alias])) {
+			$this->Model->invalidate('captcha', 'captchaResultTooLate', true);
+			
+		} elseif (in_array($this->settings[$Model->alias]['type'], array('active', 'both')) && !$this->_validateCaptcha($Model->data[$Model->alias])) {
+			$this->Model->invalidate('captcha', 'captchaResultIncorrect', true);
+
+		}
+		
+		unset($Model->data[$Model->alias]['captcha']);
+		unset($Model->data[$Model->alias]['captcha_hash']);
+		unset($Model->data[$Model->alias]['captcha_time']);
+		return true;
+	}
+
+	/**
+	 * return the current used field names to be passed in whitelist etc
+	 * 2010-01-22 ms
+	 */
+	public function fields() {
+		$list = array('captcha', 'captcha_hash', 'captcha_time');
+		$list[] = $this->settings[$this->Model->alias]['dummyField'];
+		return $list;
+	}
+
+
+	protected function _validateDummyField($data) {
+		$dummyField = $this->settings[$this->Model->alias]['dummyField'];
+		if (!isset($data[$dummyField])) {
+			return $this->_setError('Illegal call');
+		}
+		if (!empty($data[$dummyField])) {
+			# dummy field not empty - SPAM!
+			return $this->_setError('Illegal content', 'DummyField = \''.$data[$dummyField].'\'');
+		}
+		return true;
+	}
+
+
+	/**
+	 * flood protection by time
+	 * TODO: SESSION based one as alternative
+	 */
+	protected function _validateCaptchaMinTime($data) {
+		if ($this->settings[$this->Model->alias]['minTime'] <= 0) {
+			return true;
+		}
+		if (isset($data['captcha_hash']) && isset($data['captcha_time'])) {
+			if ($data['captcha_time'] < time() - $this->settings[$this->Model->alias]['minTime']) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	/**
+	 * validates maximum time
+	 * 
+	 * @param array $data
+	 * @return bool 
+	 */
+	protected function _validateCaptchaMaxTime($data) {
+		if ($this->settings[$this->Model->alias]['maxTime'] <= 0) {
+			return true;
+		}
+		if (isset($data['captcha_hash']) && isset($data['captcha_time'])) {
+			if ($data['captcha_time'] + $this->settings[$this->Model->alias]['maxTime'] > time()) {
+				return true;
+			}
+		}
+
+		return false;
+	}	
+
+	/**
+	 * flood protection by false fields and math code
+	 * TODO: build in floodProtection (max Trials etc)
+	 * TODO: SESSION based one as alternative
+	 */
+	protected function _validateCaptcha($data) {
+		if (!isset($data['captcha'])) {
+			# form inputs missing? SPAM!
+			return $this->_setError(__('captchaContentMissing'));
+		}
+
+		$hash = $this->_buildHash($data);
+
+		if ($data['captcha_hash'] == $hash) {
+			return true;
+		}
+		# wrong captcha content or session expired
+		return $this->_setError(__('Captcha incorrect'), 'SubmittedResult = \''.$data['captcha'].'\'');
+	}
+
+	/**
+	 * return error message (or empty string if none)
+	 * @return string
+	 */
+	public function errors() {
+		return $this->error;
+	}
+
+	/**
+	 * only neccessary if there is more than one request per model
+	 * 2009-12-18 ms
+	 */
+	public function reset() {
+		$this->error = '';
+	}
+
+	/**
+	 * build and log error message
+	 * 2009-12-18 ms
+	 */
+	protected function _setError($msg = null, $internalMsg = null) {
+		if (!empty($msg)) {
+			$this->error = $msg;
+		}
+		if (!empty($internalMsg)) {
+			$this->internalError = $internalMsg;
+		}
+
+		
+		$this->_logAttempt();
+		return false;
+	}
+
+	protected function _buildHash($data) {
+		return CaptchaLib::buildHash($data, $this->settings[$this->Model->alias]);
+	}
+
+	/**
+	 * logs attempts
+	 * @param bool errorsOnly (only if error occured, otherwise always)
+	 * @returns null if not logged, true otherwise
+	 * 2009-12-18 ms
+	 */
+	protected function _logAttempt($errorsOnly = true) {
+		if ($errorsOnly === true && empty($this->error) && empty($this->internalError)) {
+			return null;
+		}
+		if (!$this->settings[$this->Model->alias]['log']) {
+			return null;
+		}
+
+		//App::import('Component', 'RequestHandler');
+		$msg = 'IP \''.CakeRequest::clientIP().'\', Agent \''.env('HTTP_USER_AGENT').'\', Referer \''.env('HTTP_REFERER').'\', Host-Referer \''.CommonComponent::getReferer().'\'';
+		if (!empty($this->error)) {
+			$msg .= ', '.$this->error;
+		}
+		if (!empty($this->internalError)) {
+			$msg .= ' ('.$this->internalError.')';
+		}
+		$this->log($msg, 'captcha');
+		return true;
+	}
+
+}
+
+

+ 263 - 0
Model/Behavior/ChangePasswordBehavior.php

@@ -0,0 +1,263 @@
+<?php
+
+/**
+ * Copyright 2011, Mark Scherer 
+ * 
+ * Licensed under The MIT License 
+ * Redistributions of files must retain the above copyright notice. 
+ * 
+ * @version    1.2
+ * @license    http://www.opensource.org/licenses/mit-license.php The MIT License 
+ */
+
+if (!defined('PWD_MIN_LENGTH')) {
+	define('PWD_MIN_LENGTH', 3);
+}
+if (!defined('PWD_MAX_LENGTH')) {
+	define('PWD_MAX_LENGTH', 20);
+}
+
+/**
+ * A cakephp1.3 behavior to change passwords the easy way
+ * - complete validation
+ * - hashing of password
+ * - requires fields (no tempering even without security component)
+ * 
+ * usage: do NOT add it via $actAs = array()
+ * attach it dynamically in only those actions where you actually change the password like so:
+ * $this->User->Behaviors->attach('Tools.ChangePassword', 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 :) 
+ * 
+ * TODO: allowEmpty and nonEmptyToEmpty - maybe with checkbox "set_new_pwd"
+ * feel free to help me out
+ * 
+ * 2011-08-24 ms
+ */
+
+App::uses('Security', 'Utility'); 
+ 
+class ChangePasswordBehavior extends ModelBehavior {
+
+	public $settings = array();
+
+	/**
+	 * @access protected
+	 */
+	public $_defaultSettings = array(
+		'field' => 'password',
+		'confirm' => true, # set to false if in admin view and no confirmation (pwd_repeat) is required
+		'allowEmpty' => false,
+		'current' => false, # expect the current password for security purposes
+		'formField' => 'pwd',
+		'formFieldRepeat' => 'pwd_repeat',
+		'formFieldCurrent' => 'pwd_current',
+		'hashType' => null,
+		'hashSalt' => true,
+		'auth' => 'Auth', # which component,
+		'allowSame' => true, # dont allow the old password on change //TODO: implement
+		'nonEmptyToEmpty' => false, # allow resetting nonempty pwds to empty once set (prevents problems with default edit actions)
+	);
+	
+	public $_validationRules = array(
+		'formField' => array(
+			'between' => array(
+				'rule' => array('between', PWD_MIN_LENGTH, PWD_MAX_LENGTH),
+				'message' => array('valErrBetweenCharacters %s %s', PWD_MIN_LENGTH, PWD_MAX_LENGTH),
+				'last' => true,
+			)
+		),
+		'formFieldRepeat' => array(
+			'between' => array(
+				'rule' => array('between', PWD_MIN_LENGTH, PWD_MAX_LENGTH),
+				'message' => array('valErrBetweenCharacters %s %s', PWD_MIN_LENGTH, PWD_MAX_LENGTH),
+				'last' => true,
+			),
+			'validateIdentical' => array(
+				'rule' => array('validateIdentical', 'formField'),
+				'message' => 'valErrPwdNotMatch',
+				'last' => true,
+			),
+		),
+		'formFieldCurrent' => array(
+			'notEmpty' => array(
+				'rule' => array('notEmpty'),
+				'message' => 'valErrProvideCurrentPwd',
+				'last' => true,
+			),
+			'validateCurrentPwd' => array(
+				'rule' => 'validateCurrentPwd',
+				'message' => 'valErrCurrentPwdIncorrect',
+			)
+		),
+	);
+
+	/**
+	 * if not implemented in app_model
+	 * 2011-07-22 ms
+	 */	
+	public function validateCurrentPwd(Model $Model, $data) {
+		if (is_array($data)) {
+			$pwd = array_shift($data);
+		} else {
+			$pwd = $data;
+		}
+
+		$uid = null;
+		if ($Model->id) {
+			$uid = $Model->id;
+		} elseif (!empty($Model->data[$Model->alias]['id'])) {
+			$uid = $Model->data[$Model->alias]['id'];
+		} else {
+			trigger_error('No user id given');
+			return false;
+		}
+		if (class_exists('AuthExtComponent')) {
+			$this->Auth = new AuthExtComponent(new ComponentCollection());
+		} elseif (class_exists($this->settings[$Model->alias]['auth'].'Component')) {
+			$auth = $this->settings[$Model->alias]['auth'].'Component';
+			$this->Auth = new $auth(new ComponentCollection());
+		} else {
+			trigger_error('No validation class found');
+			return true;
+		}
+		$this->Auth->constructAuthenticate();
+		//debug($this->Auth); die();
+		return $this->Auth->verifyUser($uid, $pwd);
+	}
+	
+	/**
+	 * if not implemented in app_model
+	 * 2011-07-22 ms
+	 */
+	public function validateIdentical(Model $Model, $data, $compareWith = null) {
+		if (is_array($data)) {
+			$value = array_shift($data);
+		} else {
+			$value = $data;
+		}
+		$compareValue = $Model->data[$Model->alias][$compareWith];
+		return ($compareValue == $value);
+	}
+
+	/**
+	 * if not implemented in app_model
+	 * 2011-11-10 ms
+	 */
+	public function validateNotSame(Model $Model, $data, $field1, $field2) {
+		$value1 = $Model->data[$Model->alias][$field1];
+		$value2 = $Model->data[$Model->alias][$field2];
+		return ($value1 != $value2);
+	}	
+	
+	/**
+	 * adding validation rules
+	 * also adds and merges config settings (direct + configure)
+	 * 2011-08-24 ms
+	 */
+	public function setup(Model $Model, $config = array()) {
+		$defaults = $this->_defaultSettings;
+		if ($configureDefaults = Configure::read('ChangePassword')) {
+			$defaults = Set::merge($defaults, $configureDefaults);
+		}
+		$this->settings[$Model->alias] = Set::merge($defaults, $config);
+		
+		$formField = $this->settings[$Model->alias]['formField'];
+		$formFieldRepeat = $this->settings[$Model->alias]['formFieldRepeat'];
+		$formFieldCurrent = $this->settings[$Model->alias]['formFieldCurrent'];
+		
+		# add the validation rules if not already attached
+		if (!isset($Model->validate[$formField])) {
+			$Model->validate[$formField] = $this->_validationRules['formField'];
+		}
+		if (!isset($Model->validate[$formFieldRepeat])) {
+			$Model->validate[$formFieldRepeat] = $this->_validationRules['formFieldRepeat'];
+			$Model->validate[$formFieldRepeat]['validateIdentical']['rule'][1] = $formField;			
+		}
+		
+		if ($this->settings[$Model->alias]['current'] && !isset($Model->validate[$formFieldCurrent])) {
+			$Model->validate[$formFieldCurrent] = $this->_validationRules['formFieldCurrent'];
+			
+			if (!$this->settings[$Model->alias]['allowSame']) {
+				$Model->validate[$formField]['validateNotSame'] = array(
+					'rule' => array('validateNotSame', $formField, $formFieldCurrent),
+					'message' => 'valErrPwdSameAsBefore',
+					'last' => true,
+				);
+			}			
+		}
+
+		# allowEmpty?
+		if (!empty($this->settings[$Model->alias]['allowEmpty'])) {
+			$Model->validate[$formField]['between']['rule'][1] = 0;			
+		}
+	}
+
+	/**
+	 * whitelisting
+	 * 2011-07-22 ms 
+	 */
+	public function beforeValidate(Model $Model) {
+		# add fields to whitelist!
+		$whitelist = array($this->settings[$Model->alias]['formField'], $this->settings[$Model->alias]['formFieldRepeat']);
+		if ($this->settings[$Model->alias]['current']) {
+			$whitelist[] = $this->settings[$Model->alias]['formFieldCurrent'];
+		}
+		if (!empty($Model->whitelist)) {
+			$Model->whitelist = am($Model->whitelist, $whitelist);
+		}
+		
+		# make sure fields are set and validation rules are triggered - prevents tempering of form data
+		$formField = $this->settings[$Model->alias]['formField'];
+		$formFieldRepeat = $this->settings[$Model->alias]['formFieldRepeat'];
+		$formFieldCurrent = $this->settings[$Model->alias]['formFieldCurrent'];
+		if (!isset($Model->data[$Model->alias][$formField])) {
+			$Model->data[$Model->alias][$formField] = '';
+		}
+		if ($this->settings[$Model->alias]['confirm'] && !isset($Model->data[$Model->alias][$formFieldRepeat])) {
+			$Model->data[$Model->alias][$formFieldRepeat] = '';
+		}
+		if ($this->settings[$Model->alias]['current'] && !isset($Model->data[$Model->alias][$formFieldCurrent])) {
+			$Model->data[$Model->alias][$formFieldCurrent] = '';
+		}
+
+		return true;
+	}
+
+
+	/**
+	 * hashing the password now
+	 * 2011-07-22 ms 
+	 */
+	public function beforeSave(Model $Model) {
+		$formField = $this->settings[$Model->alias]['formField'];
+		$formFieldRepeat = $this->settings[$Model->alias]['formFieldRepeat'];
+		$field = $this->settings[$Model->alias]['field']; 
+		$type = $this->settings[$Model->alias]['hashType'];
+		$salt = $this->settings[$Model->alias]['hashSalt'];
+			
+		if (empty($Model->data[$Model->alias][$formField]) && !$this->settings[$Model->alias]['nonEmptyToEmpty']) {
+			# is edit? previous password was "notEmpty"?
+			if (!empty($Model->data[$Model->alias][$Model->primaryKey]) && ($oldPwd = $Model->field($field, array($Model->alias.'.id'=>$Model->data[$Model->alias][$Model->primaryKey]))) && $oldPwd != Security::hash('', $type, $salt)) {
+				unset($Model->data[$Model->alias][$formField]);
+			}
+		}
+	
+		if (isset($Model->data[$Model->alias][$formField])) {
+			$Model->data[$Model->alias][$field] = Security::hash($Model->data[$Model->alias][$formField], $type, $salt);
+			unset($Model->data[$Model->alias][$formField]);
+			if ($this->settings[$Model->alias]['confirm']) {
+				unset($Model->data[$Model->alias][$formFieldRepeat]);
+			}
+			# update whitelist
+			if (!empty($Model->whitelist)) {
+				$Model->whitelist = am($Model->whitelist, array($field));
+			}
+		}
+		
+		return true;
+	}
+
+	
+}

+ 245 - 0
Model/Behavior/JsonableBehavior.php

@@ -0,0 +1,245 @@
+<?php
+/**
+ * Copyright 2011, PJ Hile (http://www.pjhile.com) 
+ * 
+ * Licensed under The MIT License 
+ * Redistributions of files must retain the above copyright notice. 
+ * 
+ * @version    0.1 
+ * @license    http://www.opensource.org/licenses/mit-license.php The MIT License 
+ */
+
+/**
+ * A behavior that will json_encode (and json_decode) fields if they contain an array or specific pattern. 
+ * 
+ * Requres: PHP 5 >= 5.2.0 or PECL json >= 1.2.0 
+ * 
+ * This is a port of the Serializeable behavior by Matsimitsu (http://www.matsimitsu.nl) 
+ */
+ 
+/** 
+ * Modified by Mark Scherer (http://www.dereuromark.de)
+ * 
+ * Now supports different input/output formats:
+ * - "list" is useful as some kind of pseudo enums or simple lists
+ * - "params" is useful for multiple key/value pairs
+ * - can be used to create dynamic forms (and tables)
+ * 
+ * Also automatically cleans lists and works with custom separators etc
+ * 
+ * 2011-07-04 ms
+ */
+class JsonableBehavior extends ModelBehavior {
+
+	public $decoded = null;
+	public $settings = array();
+
+	/**
+	 * //TODO: json input/ouput directly, clean
+	 * @access protected
+	 */
+	public $_defaultSettings = array(
+		'fields' => array(), # empty => only works with array!!!
+		'input' => 'array', # json, array, param, list (param/list only works with specific fields)
+		'output' => 'array', # json, array, param, list (param/list only works with specific fields)
+		'separator' => '|', # only for param or list
+		'keyValueSeparator' => ':', # only for param
+		'leftBound' =>  '{', # only for list
+		'rightBound' => '}', # only for list
+		'clean' => true, # only for param or list (autoclean values on insert)
+		'sort' => false, # only for list
+		'unique' => true, # only for list (autoclean values on insert),
+		'map' => array(), # map on a different DB field
+	);	
+
+	public function setup(Model $Model, $config = array()) {
+		$this->settings[$Model->alias] = Set::merge($this->_defaultSettings, $config);
+		//extract ($this->settings[$Model->alias]);
+		if (!is_array($this->settings[$Model->alias]['fields'])) {
+			$this->settings[$Model->alias]['fields'] = (array)$this->settings[$Model->alias]['fields'];
+		}
+		if (!is_array($this->settings[$Model->alias]['map'])) {
+			$this->settings[$Model->alias]['map'] = (array)$this->settings[$Model->alias]['map'];
+		}
+	}
+
+	/**
+	 * Decodes the fields 
+	 * 
+	 * @param object $Model 
+	 * @param array $results 
+	 * @return array 
+	 * @access public 
+	 */
+	public function afterFind(Model $Model, $results) {
+		$results = $this->decodeItems($Model, $results);
+		return $results;
+	}
+
+	/**
+	 * Decodes the fields of an array (if the value itself was encoded) 
+	 * 
+	 * @param array $arr 
+	 * @return array 
+	 * @access public 
+	 */
+	public function decodeItems(Model $Model, $arr) {
+		foreach ($arr as $akey => $val) {
+			if (!isset($val[$Model->alias])) {
+				return $arr;
+			}
+			$fields = $this->settings[$Model->alias]['fields'];
+		
+			foreach ($val[$Model->alias] as $key => $v) {
+				if (empty($fields) && !is_array($v) || !in_array($key, $fields)) {
+					continue;
+				}
+				if ($this->isEncoded($Model, $v)) {
+					if (!empty($this->settings[$Model->alias]['map'])) {
+						$keys = array_keys($this->settings[$Model->alias]['fields'], $key);
+						if (!empty($keys)) { 
+							$key = $this->settings[$Model->alias]['map'][array_shift($keys)];
+						}
+					}
+					
+					$arr[$akey][$Model->alias][$key] = $this->decoded;
+				}
+			}
+		}
+		return $arr;
+	}
+
+	/**
+	 * Saves all fields that do not belong to the current Model into 'with' helper model. 
+	 * 
+	 * @param object $Model 
+	 * @access public 
+	 */
+	public function beforeSave(Model $Model) {
+		$data = $Model->data[$Model->alias];
+		$usedFields = $this->settings[$Model->alias]['fields'];
+		$mappedFields = $this->settings[$Model->alias]['map'];
+		if (empty($mappedFields)) {
+			$mappedFields = $usedFields;
+		}
+		
+		$fields = array();
+		
+		foreach ($mappedFields as $index => $map) {
+			if (empty($map) || $map == $usedFields[$index]) {
+				$fields[$usedFields[$index]] = $usedFields[$index];
+				continue;
+			}
+			$fields[$map] = $usedFields[$index];
+		}
+
+		foreach ($data as $key => $val) {
+			if (!empty($fields) && !array_key_exists($key, $fields)) {
+				continue;
+			}
+			if (!empty($fields)) {
+				$key = $fields[$key];
+			}
+			if (!empty($this->settings[$Model->alias]['fields']) || is_array($val)) {
+				$Model->data[$Model->alias][$key] = $this->_encode($Model, $val);
+			}
+		}
+		
+		return true;
+	}
+	
+	public function _encode(Model $Model, $val) {
+		if (!empty($this->settings[$Model->alias]['fields'])) {
+			if ($this->settings[$Model->alias]['input'] == 'param') {
+				$val = $this->_fromParam($Model, $val);
+			} elseif ($this->settings[$Model->alias]['input'] == 'list') {
+				$val = $this->_fromList($Model, $val);
+				if ($this->settings[$Model->alias]['unique']) {
+					$val = array_unique($val);
+				}
+				if ($this->settings[$Model->alias]['sort']) {
+					sort($val);
+				}
+			}
+		}
+		if (is_array($val)) {
+			$val = json_encode($val);
+		}
+		return $val;
+	}
+	
+	/**
+	 * fields are absolutely necessary to function properly!
+	 * 2011-06-18 ms
+	 */
+	public function _decode(Model $Model, $val) {
+		$decoded = json_decode($val);
+		if ($decoded === false) {
+			return false;
+		}
+		$decoded = (array)$decoded;
+		if ($this->settings[$Model->alias]['output'] == 'param') {
+			$decoded = $this->_toParam($Model, $decoded);
+		} elseif ($this->settings[$Model->alias]['output'] == 'list') {
+			$decoded = $this->_toList($Model, $decoded);
+		}
+		return $decoded;
+	}
+	
+	/**
+	 * array() => param1:value1|param2:value2|...
+	 */
+	public function _toParam(Model $Model, $val) {
+		$res = array();
+		foreach ($val as $key => $v) {
+			$res[] = $key.$this->settings[$Model->alias]['keyValueSeparator'].$v;
+		}
+		return implode($this->settings[$Model->alias]['separator'], $res);
+	}
+	
+	public function _fromParam(Model $Model, $val) {
+		$leftBound = $this->settings[$Model->alias]['leftBound'];
+		$rightBound = $this->settings[$Model->alias]['rightBound'];
+		$separator = $this->settings[$Model->alias]['separator'];
+		
+		$res = array();
+		$pieces = String::tokenize($val, $separator, $leftBound, $rightBound);
+		foreach ($pieces as $piece) {
+			$subpieces = String::tokenize($piece, $this->settings[$Model->alias]['keyValueSeparator'], $leftBound, $rightBound);
+			if (count($subpieces) < 2) {
+				continue;
+			}
+			$res[$subpieces[0]] = $subpieces[1]; 
+		}
+		return $res;
+	}
+	
+	/**
+	 * array() => value1|value2|value3|...
+	 */
+	public function _toList(Model $Model, $val) {
+		return implode($this->settings[$Model->alias]['separator'], $val);
+	}
+	
+	public function _fromList(Model $Model, $val) {
+		extract($this->settings[$Model->alias]);
+		
+		return String::tokenize($val, $separator, $leftBound, $rightBound);
+	}
+
+	/**
+	 * Checks if string is encoded array/object 
+	 * 
+	 * @param string string to check 
+	 * @access public 
+	 * @return boolean 
+	 */
+	public function isEncoded(Model $Model, $str) {
+		$this->decoded = $this->_decode($Model, $str);
+
+		if ($this->decoded !== false) {
+			return true;
+		}
+		return false;
+	}
+}