euromark 13 年 前
コミット
1065d4f894
1 ファイル変更994 行追加0 行削除
  1. 994 0
      Model/Behavior/RevisionBehavior.php

+ 994 - 0
Model/Behavior/RevisionBehavior.php

@@ -0,0 +1,994 @@
+<?php
+App::uses('ModelBehavior', 'Model');
+
+/**
+ * Revision Behavior 2.0.4
+ *
+ * Revision is a solution for adding undo and other versioning functionality
+ * to your database models. It is set up to be easy to apply to your project,
+ * to be easy to use and not get in the way of your other model activity.
+ * It is also intended to work well with it's sibling, LogableBehavior.
+ *
+ * Feature list :
+ *
+ *  - Easy to install
+ *  - Automagically save revision on model save
+ *  - Able to ignore model saves which only contain certain fields
+ *  - Limit number of revisions to keep, will delete oldest
+ *  - Undo functionality (or update to any revision directly)
+ *  - Revert to a datetime (and even do so cascading)
+ *  - Get a diff model array to compare two or more revisions
+ *  - Inspect any or all revisions of a model
+ *  - Work with Tree Behavior
+ *  - Includes beforeUndelete and afterUndelete callbacks
+ *  - NEW As of 1.2 behavior will revision HABTM relationships (from one way)
+ *
+ * Install instructions :
+ *
+ *  - Place the newest version of RevisionBehavior in your app/models/behaviors folder
+ *  - Add the behavior to AppModel (or single models if you prefer)
+ *  - Create a shadow table for each model that you want revision for.
+ *  - Behavior will gracefully do nothing for models that has behavior, without table
+ *  - If adding to an existing project, run the initializeRevisions() method once for each model.
+ *
+ * About shadow tables :
+ *
+ * You should make these AFTER you have baked your ordinary tables as they may interfer. By default
+ * the tables should be named "[prefix][model_table_name]_revs" If you wish to change the suffix you may
+ * do so in the property called $revision_suffix found bellow. Also by default the behavior expects
+ * the revision tables to be in the same dbconfig as the model, but you may change this on a per
+ * model basis with the useDbConfig config option.
+ *
+ * Add the same fields as in the live table, with 3 important differences.
+ *  - The 'id' field should NOT be the primary key, nor auto increment
+ *  - Add the fields 'version_id' (int, primary key, autoincrement) and
+ *    'version_created' (datetime)
+ *  - Skipp fields that should not be saved in shadowtable (lft,right,weight for instance)
+ *
+ * Configuration :
+ *
+ *  - 'limit' : number of revisions to keep, must be at least 2
+ *  - 'ignore' : array containing the name of fields to ignore
+ *  - 'auto' : boolean when false the behavior will NOT generate revisions in afterSave
+ *  - 'useDbConfig' : string/null Name of dbConfig to use. Null to use Model's
+ *
+ * Limit functionality :
+ * The shadow table will save a revision copy when it saves live data, so the newest
+ * row in the shadow table will (in most cases) be the same as the current live data.
+ * The exception is when the ignore field functionality is used and the live data is
+ * updated only in those fields.
+ *
+ * Ignore field(s) functionality :
+ * If you wish to be able to update certain fields without generating new revisions,
+ * you can add those fields to the configuration ignore array. Any time the behavior's
+ * afterSave is called with just primary key and these fields, it will NOT generate
+ * a new revision. It WILL however save these fields together with other fields when it
+ * does save a revision. You will probably want to set up cron or otherwise call
+ * createRevision() to update these fields at some points.
+ *
+ * Auto functionality :
+ * By default the behavior will insert itself into the Model's save process by implementing
+ * beforeSave and afterSave. In afterSave, the behavior will save a new revision of the dataset
+ * that is now the live data. If you do NOT want this automatic behavior, you may set the config
+ * option 'auto' to false. Then the shadow table will remain empty unless you call createRevisions
+ * manually.
+ *
+ * HABTM revision feature :
+ * In order to do revision on HABTM relationship, add a text field to the main model's shadow table
+ * with the same name as the association, ie if Article habtm ArticleTag as Tag, add a field 'Tag'
+ * to articles_revs.
+ * NB! In version 1.2 and up to current, Using HABTM revision requires that both models uses this
+ * behavior (even if secondary model does not have a shadow table).
+ *
+ * 1.1.1 => 1.1.2 changelog
+ *   - revisions() got new paramter: $include_current
+ *     This now defaults to false, resulting in a change from 1.1.1. See tests
+ *
+ * 1.1.6 => 1.2
+ *   - includes HABTM revision control (one way)
+ *
+ * 1.2 => 1.2.1
+ *   - api change in revertToDate, added paramter for force delete if reverting to before earliest
+ *
+ * 1.2.6 => 1.2.7
+ * 	 - api change: removed shadow(), changed revertToDate() to only recurse into related models that
+ *     are dependent when cascade is true
+ *
+ * @author Ronny Vindenes
+ * @author Alexander 'alkemann' Morland
+ * @license MIT
+ * @modifed 27. march 2009
+ * @version 2.0.4
+ */
+class RevisionBehavior extends ModelBehavior {
+
+	/**
+	 * Behavior settings
+	 *
+	 * @access public
+	 * @var array
+	 */
+	public $settings = array();
+	/**
+	 * Shadow table prefix
+	 * Only change this value if it causes table name crashes
+	 *
+	 * @access private
+	 * @var string
+	 */
+	protected $revision_suffix = '_revs';
+	/**
+	 * Defaul setting values
+	 *
+	 * @access private
+	 * @var array
+	 */
+	protected $defaults = array(
+		'limit' => false,
+		'auto' => true,
+		'ignore' => array(),
+		'useDbConfig' => null,
+		'model' => null
+	);
+	/**
+	 * Old data, used to detect changes
+	 *
+	 * @var array
+	 */
+	protected $oldData = array();
+
+	/**
+	 * Configure the behavior through the Model::actsAs property
+	 *
+	 * @param object $Model
+	 * @param array $config
+	 */
+	public function setup(Model $Model, $config = null) {
+		if (is_array($config)) {
+			$this->settings[$Model->alias] = array_merge($this->defaults, $config);
+		} else {
+			$this->settings[$Model->alias] = $this->defaults;
+		}
+		$this->createShadowModel($Model);
+		$Model->Behaviors->attach('Containable');
+	}
+
+	/**
+	 * Manually create a revision of the current record of Model->id
+	 *
+	 * @example $this->Post->id = 5; $this->Post->createRevision();
+	 * @param object $Model
+	 * @return boolean success
+	 */
+	public function createRevision(Model $Model) {
+		if (! $Model->id) {
+			trigger_error('RevisionBehavior: Model::id must be set', E_USER_WARNING); return null;
+		}
+		if (!$Model->ShadowModel) {
+			trigger_error('RevisionBehavior: ShadowModel doesnt exist.', E_USER_WARNING);
+			return false;
+		}
+		$habtm = array();
+		$all_habtm = $Model->getAssociated('hasAndBelongsToMany');
+		foreach ($all_habtm as $assocAlias) {
+			if (isset($Model->ShadowModel->_schema[$assocAlias])) {
+				$habtm[] = $assocAlias;
+			}
+		}
+		$data = $Model->find('first', array(
+			'conditions'=>array($Model->alias.'.'.$Model->primaryKey => $Model->id),
+			'contain' => $habtm
+		));
+		$Model->ShadowModel->create($data);
+		$Model->ShadowModel->set('version_created', date('Y-m-d H:i:s'));
+		foreach ($habtm as $assocAlias) {
+			$foreign_keys = Set::extract($data,'/'.$assocAlias.'/'.$Model->{$assocAlias}->primaryKey);
+			$Model->ShadowModel->set($assocAlias, implode(',',$foreign_keys));
+		}
+		return $Model->ShadowModel->save();
+	}
+
+	/**
+	 * Returns an array that maps to the Model, only with multiple values for fields that has been changed
+	 *
+	 * @example $this->Post->id = 4; $changes = $this->Post->diff();
+	 * @example $this->Post->id = 4; $my_changes = $this->Post->diff(null,nul,array('conditions'=>array('user_id'=>4)));
+	 * @example $this->Post->id = 4; $difference = $this->Post->diff(45,192);
+	 * @param Object $Model
+	 * @param int $from_version_id
+	 * @param int $to_version_id
+	 * @param array $options
+	 * @return array
+	 */
+	public function diff(Model $Model, $from_version_id = null, $to_version_id = null, $options = array()) {
+		if (! $Model->id) {
+			trigger_error('RevisionBehavior: Model::id must be set', E_USER_WARNING); return null;
+		}
+		if (!$Model->ShadowModel) {
+			trigger_error('RevisionBehavior: ShadowModel doesnt exist.', E_USER_WARNING);
+			return null;
+		}
+		if (isset($options['conditions'])) {
+			$conditions = am($options['conditions'],array($Model->primaryKey => $Model->id));
+		} else {
+			$conditions = array( $Model->primaryKey => $Model->id);
+		}
+		if (is_numeric($from_version_id) || is_numeric($to_version_id)) {
+			if (is_numeric($from_version_id) && is_numeric($to_version_id)) {
+				$conditions['version_id'] = array($from_version_id,$to_version_id);
+				if ($Model->ShadowModel->find('count',array('conditions'=>$conditions)) < 2) {
+					return false;
+				}
+			} else {
+				if (is_numeric($from_version_id)) {
+					$conditions['version_id'] = $from_version_id;
+				} else {
+					$conditions['version_id'] = $to_version_id;
+				}
+				if ($Model->ShadowModel->find('count',array('conditions'=>$conditions)) < 1) {
+					return false;
+				}
+			}
+		}
+		$conditions = array($Model->primaryKey 	=> $Model->id);
+		if (is_numeric($from_version_id)) {
+			$conditions['version_id >='] = $from_version_id;
+		}
+		if (is_numeric($to_version_id)) {
+			$conditions['version_id <='] = $to_version_id;
+		}
+		$options['conditions'] = $conditions;
+		$all = $this->revisions($Model,$options,true);
+		if (sizeof($all) == 0) {
+			return null;
+		}
+		$unified = array();
+		$keys = array_keys($all[0][$Model->alias]);
+		foreach ($keys as $field) {
+			$all_values = Set::extract($all,'/'.$Model->alias.'/'.$field);
+			$all_values = array_reverse(array_unique(array_reverse($all_values,true)),true);
+			if (sizeof($all_values) == 1) {
+				$unified[$field] = reset($all_values);
+			} else {
+				$unified[$field] = $all_values;
+			}
+		}
+		return array($Model->alias => $unified);
+	}
+
+	/**
+	 * Will create a current revision of all rows in Model, if none exist.
+	 * Use this if you add the revision to a model that allready has data in
+	 * the DB.
+	 * If you have large tables or big/many fields, use $limit to reduce the
+	 * number of rows that is run at once.
+	 *
+	 * @example $this->Post->initializeRevisions();
+	 * @param object $Model
+	 * @param int $limit number of rows to initialize in one go
+	 * @return boolean
+	 */
+	public function initializeRevisions(Model $Model, $limit = 100) {
+		if (!$Model->ShadowModel) {
+			trigger_error('RevisionBehavior: ShadowModel doesnt exist.', E_USER_WARNING);
+			return false;
+		}
+		if ($Model->ShadowModel->useTable == false) {
+			trigger_error('RevisionBehavior: Missing shadowtable : '.$Model->table.$this->suffix, E_USER_WARNING);
+			return null;
+		}
+		if ($Model->ShadowModel->find('count') != 0) {
+			return false;
+		}
+		$count = $Model->find('count');
+		if ($limit < $count) {
+			$remaining = $count;
+			for ($p = 1; true; $p++ ) {
+
+				$this->init($Model, $p, $limit);
+
+				$remaining = $remaining - $limit;
+				if ($remaining <= 0) {
+					break;
+				}
+			}
+		} else {
+			$this->init($Model, 1, $count);
+		}
+		return true;
+	}
+
+	/**
+	 * saves revisions for rows matching page and limit given
+	 *
+	 * @param object $Model
+	 * @param int $page
+	 * @param int $limit
+	 */
+	protected function init(Model $Model, $page, $limit) {
+		$habtm = array();
+		$all_habtm = $Model->getAssociated('hasAndBelongsToMany');
+		foreach ($all_habtm as $assocAlias) {
+			if (isset($Model->ShadowModel->_schema[$assocAlias])) {
+				$habtm[] = $assocAlias;
+			}
+		}
+		$all = $Model->find('all', array(
+			'limit' => $limit,
+			'page' => $page,
+			'contain' => $habtm
+		));
+		$version_created = date('Y-m-d H:i:s');
+		foreach ($all as $data) {
+			$Model->ShadowModel->create($data);
+			$Model->ShadowModel->set('version_created', $version_created);
+			$Model->ShadowModel->save();
+		}
+	}
+
+	/**
+	 * Finds the newest revision, including the current one.
+	 * Use with caution, the live model may be different depending on the usage
+	 * of ignore fields.
+	 *
+	 * @example $this->Post->id = 6; $newest_revision = $this->Post->newest();
+	 * @param object $Model
+	 * @param array $options
+	 * @return array
+	 */
+	public function newest(Model $Model, $options = array()) {
+		if (! $Model->id) {
+			trigger_error('RevisionBehavior: Model::id must be set', E_USER_WARNING); return null;
+		}
+		if (!$Model->ShadowModel) {
+			trigger_error('RevisionBehavior: ShadowModel doesnt exist.', E_USER_WARNING);
+			return null;
+		}
+		if (isset($options['conditions'])) {
+			$options['conditions'] = am($options['conditions'],array($Model->alias.'.'.$Model->primaryKey => $Model->id));
+		} else {
+			$options['conditions'] = array( $Model->alias.'.'.$Model->primaryKey => $Model->id);
+		}
+
+		return $Model->ShadowModel->find('first',$options);
+	}
+
+	/**
+	 * Find the oldest revision for the current Model->id
+	 * If no limit is used on revision and revision has been enabled for the model
+	 * since start, this call will return the original first record.
+	 *
+	 * @example $this->Post->id = 2; $original = $this->Post->oldest();
+	 * @param object $Model
+	 * @param array $options
+	 * @return array
+	 */
+	public function oldest(Model $Model, $options = array()) {
+		if (! $Model->id) {
+			trigger_error('RevisionBehavior: Model::id must be set', E_USER_WARNING); return null;
+		}
+		if (!$Model->ShadowModel) {
+			trigger_error('RevisionBehavior: ShadowModel doesnt exist.', E_USER_WARNING);
+			return null;
+		}
+		if (isset($options['conditions'])) {
+			$options['conditions'] = am($options['conditions'],array($Model->primaryKey => $Model->id));
+		} else {
+			$options['conditions'] = array( $Model->primaryKey => $Model->id);
+		}
+		$options['order'] = 'version_created ASC, version_id ASC';
+		return $Model->ShadowModel->find('first',$options);
+	}
+
+	/**
+	 * Find the second newest revisions, including the current one.
+	 *
+	 * @example $this->Post->id = 6; $undo_revision = $this->Post->previous();
+	 * @param object $Model
+	 * @param array $options
+	 * @return array
+	 */
+	public function previous(Model $Model, $options = array()) {
+		if (! $Model->id) {
+			trigger_error('RevisionBehavior: Model::id must be set', E_USER_WARNING); return null;
+		}
+		if (!$Model->ShadowModel) {
+			trigger_error('RevisionBehavior: ShadowModel doesnt exist.', E_USER_WARNING);
+			return false;
+		}
+		$options['limit'] = 1;
+		$options['page'] = 2;
+		if (isset($options['conditions'])) {
+			$options['conditions'] = am($options['conditions'],array($Model->primaryKey => $Model->id));
+		} else {
+			$options['conditions'] = array( $Model->primaryKey => $Model->id);
+		}
+		$revisions = $Model->ShadowModel->find('all',$options);
+		if (!$revisions) {
+			return null;
+		}
+		return $revisions[0];
+	}
+
+	/**
+	 * Revert all rows matching conditions to given date.
+	 * Model rows outside condition or not edited will not be affected. Edits since date
+	 * will be reverted and rows created since date deleted.
+	 *
+	 * @param object $Model
+	 * @param array $options 'conditions','date'
+	 * @return boolean success
+	 */
+	public function revertAll(Model $Model, $options = array()) {
+		if (!$Model->ShadowModel) {
+			trigger_error('RevisionBehavior: ShadowModel doesnt exist.', E_USER_WARNING);
+			return false;
+		}
+		if (empty($options) || !isset($options['date'])) {
+			return FALSE;
+		}
+		if (!isset($options['conditions'])) {
+			$options['conditions'] = array();
+		}
+		// leave model rows out side of condtions alone
+		// leave model rows not edited since date alone
+
+		$all = $Model->find('all',array('conditions'=>$options['conditions'],'fields'=>$Model->primaryKey));
+		$allIds = Set::extract($all,'/'.$Model->alias.'/'.$Model->primaryKey);
+
+		$cond = $options['conditions'];
+		$cond['version_created <'] = $options['date'];
+		$created_before_date = $Model->ShadowModel->find('all',array(
+			'order' => $Model->primaryKey,
+			'conditions' => $cond,
+			'fields' => array('version_id',$Model->primaryKey)
+		));
+		$created_before_dateIds = Set::extract($created_before_date,'/'.$Model->alias.'/'.$Model->primaryKey);
+
+		$deleteIds = array_diff($allIds,$created_before_dateIds);
+
+		// delete all Model rows where there are only version_created later than date
+		$Model->deleteAll(array($Model->alias.'.'.$Model->primaryKey => $deleteIds),false,true);
+
+		unset($cond['version_created <']);
+		$cond['version_created >='] = $options['date'];
+		$created_after_date = $Model->ShadowModel->find('all',array(
+			'order' => $Model->primaryKey,
+			'conditions' => $cond,
+			'fields' => array('version_id',$Model->primaryKey)
+		));
+		$created_after_dateIds = Set::extract($created_after_date,'/'.$Model->alias.'/'.$Model->primaryKey);
+		$updateIds = array_diff($created_after_dateIds,$deleteIds);
+
+		$revertSuccess = true;
+		// update model rows that have version_created earlier than date to latest before date
+		foreach ($updateIds as $mid) {
+			$Model->id = $mid;
+			if ( ! $Model->revertToDate($options['date']) ) {
+				$revertSuccess = false;
+			}
+		}
+		return $revertSuccess;
+	}
+
+	/**
+	 * Revert current Model->id to the given revision id
+	 * Will return false if version id is invalid or save fails
+	 *
+	 * @example $this->Post->id = 3; $this->Post->revertTo(12);
+	 * @param object $Model
+	 * @param int $version_id
+	 * @return boolean
+	 */
+	public function revertTo(Model $Model, $version_id) {
+		if (! $Model->id) {
+			trigger_error('RevisionBehavior: Model::id must be set', E_USER_WARNING); return null;
+		}
+		if (!$Model->ShadowModel) {
+			trigger_error('RevisionBehavior: ShadowModel doesnt exist.', E_USER_WARNING);
+			return false;
+		}
+		$data = $Model->ShadowModel->find('first',array('conditions'=>array('version_id'=>$version_id)));
+		if ($data == false) {
+			return false;
+		}
+		foreach ($Model->getAssociated('hasAndBelongsToMany') as $assocAlias) {
+			if (isset($Model->ShadowModel->_schema[$assocAlias])) {
+				$data[$assocAlias][$assocAlias] = explode(',',$data[$Model->alias][$assocAlias]);
+			}
+		}
+		return $Model->save($data);
+	}
+
+	/**
+	 * Revert to the oldest revision after the given datedate.
+	 * Will cascade to hasOne and hasMany associeted models if $cascade is true.
+	 * Will return false if no change is made on the main model
+	 *
+	 * @example $this->Post->id = 3; $this->Post->revertToDate(date('Y-m-d H:i:s',strtotime('Yesterday')));
+	 * @example $this->Post->id = 4; $this->Post->revertToDate('2008-09-01',true);
+	 * @param object $Model
+	 * @param string $datetime
+	 * @param boolean $cascade
+	 * @param boolean $force_delete
+	 * @return boolean
+	 */
+	public function revertToDate(Model $Model, $datetime, $cascade = false, $force_delete = false) {
+		if (! $Model->id) {
+			trigger_error('RevisionBehavior: Model::id must be set', E_USER_WARNING);
+			return null;
+		}
+		if ($cascade) {
+			$associated = array_merge($Model->hasMany, $Model->hasOne);
+			foreach ($associated as $assoc => $data) {
+
+				// Continue with next association if no shadow model
+				if (empty($Model->$assoc->ShadowModel)) {
+					continue;
+				}
+
+				$ids = array();
+
+				$cascade = false;
+				/* Check if association has dependent children */
+				$depassoc = array_merge($Model->$assoc->hasMany, $Model->$assoc->hasOne);
+				foreach ($depassoc as $dep) {
+					if ($dep['dependent']) {
+						$cascade = true;
+					}
+				}
+
+				/* Query live data for children */
+				$children = $Model->$assoc->find('list', array('conditions'=>array($data['foreignKey']=>$Model->id),'recursive'=>-1));
+				if (!empty($children)) {
+					$ids = array_keys($children);
+				}
+
+				/* Query shadow table for deleted children */
+				$revision_children = $Model->$assoc->ShadowModel->find('all', array(
+					'fields'=>array('DISTINCT '.$Model->primaryKey),
+					'conditions'=>array(
+						$data['foreignKey']=>$Model->id,
+						'NOT' => array( $Model->primaryKey => $ids )
+					),
+				));
+				if (!empty($revision_children)) {
+					$ids = am($ids,Set::extract($revision_children,'/'.$assoc.'/'.$Model->$assoc->primaryKey));
+				}
+
+				/* Revert all children */
+				foreach ($ids as $id) {
+					$Model->$assoc->id = $id;
+					$Model->$assoc->revertToDate($datetime, $cascade, $force_delete);
+				}
+			}
+		}
+		if (empty($Model->ShadowModel)) {
+			return true;
+		}
+		$data = $Model->ShadowModel->find('first',array(
+			'conditions'=>array(
+				$Model->primaryKey => $Model->id,
+				'version_created <='=>$datetime
+			),
+			'order'=>'version_created ASC, version_id ASC'
+		));
+		/* If no previous version was found and revertToDate() was called with force_delete, then delete the live data, else leave it alone */
+		if ($data == false) {
+			if ($force_delete) {
+				$Model->logableAction['Revision'] = 'revertToDate('.$datetime.') delete';
+				return $Model->delete($Model->id);
+			}
+			return true;
+		}
+		$habtm = array();
+		foreach ($Model->getAssociated('hasAndBelongsToMany') as $assocAlias) {
+			if (isset($Model->ShadowModel->_schema[$assocAlias])) {
+				$habtm[] = $assocAlias;
+			}
+		}
+		$liveData = $Model->find('first', array(
+			 		'contain'=> $habtm,
+			 		'conditions'=>array($Model->alias.'.'.$Model->primaryKey => $Model->id)));
+
+		$Model->logableAction['Revision'] = 'revertToDate('.$datetime.') add';
+		if ($liveData) {
+			$Model->logableAction['Revision'] = 'revertToDate('.$datetime.') edit';
+			foreach ($Model->getAssociated('hasAndBelongsToMany') as $assocAlias) {
+				if (isset($Model->ShadowModel->_schema[$assocAlias])) {
+					$ids = Set::extract($liveData,'/'.$assocAlias.'/'.$Model->$assocAlias->primaryKey);
+					if (empty($ids) || is_string($ids)) {
+						$liveData[$Model->alias][$assocAlias] = '';
+					} else {
+						$liveData[$Model->alias][$assocAlias] = implode(',',$ids);
+					}
+					$data[$assocAlias][$assocAlias] = explode(',',$data[$Model->alias][$assocAlias]);
+				}
+				unset($liveData[$assocAlias]);
+			}
+
+			$changeDetected = false;
+			foreach ($liveData[$Model->alias] as $key => $value) {
+				if ( isset($data[$Model->alias][$key])) {
+					$old_value = $data[$Model->alias][$key];
+				} else {
+					$old_value = '';
+				}
+				if ($value != $old_value ) {
+					$changeDetected = true;
+				}
+			}
+
+			if (!$changeDetected) {
+				return true;
+			}
+		}
+
+		$auto = $this->settings[$Model->alias]['auto'];
+		$this->settings[$Model->alias]['auto'] = false;
+		$Model->ShadowModel->create($data,true);
+		$Model->ShadowModel->set('version_created', date('Y-m-d H:i:s'));
+		$Model->ShadowModel->save();
+		$Model->version_id = $Model->ShadowModel->id;
+		$success =  $Model->save($data);
+		$this->settings[$Model->alias]['auto'] = $auto;
+		return $success;
+	}
+
+	/**
+	 * Returns a comeplete list of revisions for the current Model->id.
+	 * The options array may include Model::find parameters to narrow down result
+	 * Alias for shadow('all',array('conditions'=>array($Model->primaryKey => $Model->id)));
+	 *
+	 * @example $this->Post->id = 4; $history = $this->Post->revisions();
+	 * @example $this->Post->id = 4; $today = $this->Post->revisions(array('conditions'=>array('version_create >'=>'2008-12-10')));
+	 * @param object $Model
+	 * @param array $options
+	 * @param boolean $include_current If true will include last saved (live) data
+	 * @return array
+	 */
+	public function revisions(Model $Model, $options = array(), $include_current = false) {
+		if (! $Model->id) {
+			trigger_error('RevisionBehavior: Model::id must be set', E_USER_WARNING); return null;
+		}
+		if (!$Model->ShadowModel) {
+			trigger_error('RevisionBehavior: ShadowModel doesnt exist.', E_USER_WARNING);
+			return null;
+		}
+		if (isset($options['conditions'])) {
+			$options['conditions'] = am($options['conditions'],array($Model->alias.'.'.$Model->primaryKey => $Model->id));
+		} else {
+			$options['conditions'] = array($Model->alias.'.'.$Model->primaryKey => $Model->id);
+		}
+		if ( $include_current == false ) {
+			$current = $this->newest($Model, array('fields'=>array($Model->alias.'.version_id',$Model->primaryKey)));
+			$options['conditions'][$Model->alias.'.version_id !='] = $current[$Model->alias]['version_id'];
+		}
+		return $Model->ShadowModel->find('all',$options);
+	}
+
+	/**
+	 * Undoes an delete by saving the last revision to the Model
+	 * Will return false if this Model->id exist in the live table.
+	 * Calls Model::beforeUndelete and Model::afterUndelete
+	 *
+	 * @example $this->Post->id = 7; $this->Post->undelete();
+	 * @param object $Model
+	 * @return boolean
+	 */
+	public function undelete(Model $Model) {
+		if (! $Model->id) {
+			trigger_error('RevisionBehavior: Model::id must be set', E_USER_WARNING); return null;
+		}
+		if (!$Model->ShadowModel) {
+			trigger_error('RevisionBehavior: ShadowModel doesnt exist.', E_USER_WARNING);
+			return false;
+		}
+		if  ($Model->find('count',array(
+			'conditions'=>array($Model->primaryKey=>$Model->id),
+			'recursive'=>-1)) > 0) {
+			return false;
+		}
+		$data = $this->newest($Model);
+		if (!$data) {
+			return false;
+		}
+		$beforeUndeleteSuccess = true;
+		if (method_exists($Model,'beforeUndelete')) {
+			$beforeUndeleteSuccess = $Model->beforeUndelete();
+		}
+		if (!$beforeUndeleteSuccess) {
+			return false;
+		}
+		$model_id = $data[$Model->alias][$Model->primaryKey];
+		unset($data[$Model->alias][$Model->ShadowModel->primaryKey]);
+		$Model->create($data,true);
+		$auto_setting = $this->settings[$Model->alias]['auto'];
+		$this->settings[$Model->alias]['auto'] = false;
+		$save_success =  $Model->save();
+		$this->settings[$Model->alias]['auto'] = $auto_setting;
+		if (!$save_success) {
+			return false;
+		}
+		$Model->updateAll(
+			array($Model->primaryKey => $model_id),
+			array($Model->primaryKey => $Model->id)
+		);
+		$Model->id = $model_id;
+		$Model->createRevision();
+		$afterUndeleteSuccess = true;
+		if (method_exists($Model,'afterUndelete')) {
+			$afterUndeleteSuccess = $Model->afterUndelete();
+		}
+		return $afterUndeleteSuccess;
+	}
+
+	/**
+	 * Update to previous revision
+	 *
+	 * @example $this->Post->id = 2; $this->Post->undo();
+	 * @param object $Model
+	 * @return boolean
+	 */
+	public function undo(Model $Model) {
+		if (! $Model->id) {
+			trigger_error('RevisionBehavior: Model::id must be set', E_USER_WARNING); return null;
+		}
+		if (!$Model->ShadowModel) {
+			trigger_error('RevisionBehavior: ShadowModel doesnt exist.', E_USER_WARNING);
+			return false;
+		}
+		$data = $this->previous($Model);
+		if ($data == false) {
+			$Model->logableAction['Revision'] = 'undo add';
+			$Model->delete($Model->id);
+			return false;
+		}
+		foreach ($Model->getAssociated('hasAndBelongsToMany') as $assocAlias) {
+			if (isset($Model->ShadowModel->_schema[$assocAlias])) {
+				$data[$assocAlias][$assocAlias] = explode(',',$data[$Model->alias][$assocAlias]);
+			}
+		}
+		$Model->logableAction['Revision'] = 'undo changes';
+		return $Model->save($data);
+	}
+
+	/**
+	 * Calls create revision for all rows matching primary key list of $idlist
+	 *
+	 * @example $this->Model->updateRevisions(array(1,2,3));
+	 * @param object $Model
+	 * @param array $idlist
+	 */
+	public function updateRevisions(Model $Model, $idlist = array()) {
+		if (!$Model->ShadowModel) {
+			trigger_error('RevisionBehavior: ShadowModel doesnt exist.', E_USER_WARNING);
+			return null;
+		}
+		foreach ($idlist as $id ) {
+			$Model->id = $id;
+			$Model->createRevision();
+		}
+	}
+
+	/**
+	 * Causes revision for habtm associated models if that model does version control
+	 * on their relationship.  BeforeDelete identifies the related models that will need
+	 * to do the revision update in afterDelete. Uses
+	 *
+	 * @param unknown_type $Model
+	 */
+	public function afterDelete(Model $Model) {
+		if ($this->settings[$Model->alias]['auto'] === false) {
+			return true;
+		}
+		if (!$Model->ShadowModel) {
+			return true;
+		}
+		if (isset($this->deleteUpdates[$Model->alias]) && !empty($this->deleteUpdates[$Model->alias])) {
+			foreach ($this->deleteUpdates[$Model->alias] as $assocAlias => $assocIds) {
+				$Model->{$assocAlias}->updateRevisions($assocIds);
+			}
+			unset($this->deleteUpdates[$Model->alias]);
+		}
+	}
+
+	/**
+	 * Will create a new revision if changes have been made in the models non-ignore fields.
+	 * Also deletes oldest revision if limit is (active and) reached.
+	 *
+	 * @param object $Model
+	 * @param boolean $created
+	 * @return boolean
+	 */
+	public function afterSave(Model $Model, $created) {
+		if ($this->settings[$Model->alias]['auto'] === false) {
+			return true;
+		}
+		if (!$Model->ShadowModel) {
+			return true;
+		}
+		if ($created) {
+			$Model->ShadowModel->create($Model->data,true);
+			$Model->ShadowModel->set($Model->primaryKey,$Model->id);
+			$Model->ShadowModel->set('version_created',date('Y-m-d H:i:s'));
+			foreach ($Model->data as $alias => $alias_data) {
+				if (isset($Model->ShadowModel->_schema[$alias])) {
+					if (isset($alias_data[$alias]) && !empty($alias_data[$alias])) {
+						$Model->ShadowModel->set($alias,implode(',',$alias_data[$alias]));
+					}
+				}
+			}
+			$success = $Model->ShadowModel->save();
+			$Model->version_id = $Model->ShadowModel->id;
+			return $success;
+		}
+
+		$habtm = array();
+		foreach ($Model->getAssociated('hasAndBelongsToMany') as $assocAlias) {
+			if (isset($Model->ShadowModel->_schema[$assocAlias])) {
+				$habtm[] = $assocAlias;
+			}
+		}
+		$data = $Model->find('first', array(
+			 		'contain'=> $habtm,
+			 		'conditions'=>array($Model->alias.'.'.$Model->primaryKey => $Model->id)));
+
+		$changeDetected = false;
+		foreach ($data[$Model->alias] as $key => $value) {
+	 			if ( isset($data[$Model->alias][$Model->primaryKey])
+	 					&& !empty($this->oldData[$Model->alias])
+	 					&& isset($this->oldData[$Model->alias][$Model->alias][$key])) {
+
+	 				$old_value = $this->oldData[$Model->alias][$Model->alias][$key];
+	 			} else {
+	 				$old_value = '';
+	 			}
+	 			if ($value != $old_value && !in_array($key,$this->settings[$Model->alias]['ignore'])) {
+	 				$changeDetected = true;
+	 			}
+	 		}
+	 		$Model->ShadowModel->create($data);
+	 		if (!empty($habtm)) {
+		 		foreach ($habtm as $assocAlias) {
+		 			if (in_array($assocAlias,$this->settings[$Model->alias]['ignore'])) {
+		 				continue;
+		 			}
+		 			$oldIds = Set::extract($this->oldData[$Model->alias],$assocAlias.'.{n}.id');
+			if (!isset($Model->data[$assocAlias])) {
+			$Model->ShadowModel->set($assocAlias, implode(',',$oldIds));
+			continue;
+			}
+			$currentIds = Set::extract($data,$assocAlias.'.{n}.id');
+			$id_changes = array_diff($currentIds,$oldIds);
+			if (!empty($id_changes)) {
+			$Model->ShadowModel->set($assocAlias, implode(',',$currentIds));
+			$changeDetected = true;
+			} else {
+			$Model->ShadowModel->set($assocAlias, implode(',',$oldIds));
+			}
+		 		}
+	 		}
+	 		unset($this->oldData[$Model->alias]);
+	 		if (!$changeDetected) {
+	 			return true;
+	 		}
+		$Model->ShadowModel->set('version_created', date('Y-m-d H:i:s'));
+		$Model->ShadowModel->save();
+		$Model->version_id = $Model->ShadowModel->id;
+		if (is_numeric($this->settings[$Model->alias]['limit'])) {
+			$conditions = array('conditions'=>array($Model->alias.'.'.$Model->primaryKey => $Model->id));
+			$count = $Model->ShadowModel->find('count', $conditions);
+			if ($count > $this->settings[$Model->alias]['limit']) {
+				$conditions['order'] = $Model->alias.'.version_created ASC, '.$Model->alias.'.version_id ASC';
+				$oldest = $Model->ShadowModel->find('first',$conditions);
+				$Model->ShadowModel->id = null;
+				$Model->ShadowModel->delete($oldest[$Model->alias][$Model->ShadowModel->primaryKey]);
+			}
+		}
+		return true;
+	}
+
+	/**
+	 * Causes revision for habtm associated models if that model does version control
+	 * on their relationship. BeforeDelete identifies the related models that will need
+	 * to do the revision update in afterDelete.
+	 *
+	 * @param object $Model
+	 * @return boolean
+	 */
+	public function beforeDelete(Model $Model) {
+		if ($this->settings[$Model->alias]['auto'] === false) {
+			return true;
+		}
+		if (!$Model->ShadowModel) {
+			return true;
+		}
+		foreach ($Model->hasAndBelongsToMany as $assocAlias => $a) {
+			if (isset($Model->{$assocAlias}->ShadowModel->_schema[$Model->alias])) {
+				$joins =  $Model->{$a['with']}->find('all',array(
+					'recursive' => -1,
+					'conditions' => array(
+						$a['foreignKey'] => $Model->id
+					)
+				));
+				$this->deleteUpdates[$Model->alias][$assocAlias] = Set::extract($joins,'/'.$a['with'].'/'.$a['associationForeignKey']);
+			}
+		}
+		return true;
+	}
+
+	/**
+	 * Revision uses the beforeSave callback to remember the old data for comparison in afterSave
+	 *
+	 * @param object $Model
+	 * @return boolean
+	 */
+	public function beforeSave(Model $Model) {
+		if ($this->settings[$Model->alias]['auto'] === false) {
+			return true;
+		}
+		if (!$Model->ShadowModel) {
+			return true;
+		}
+		$Model->ShadowModel->create();
+		if (!isset($Model->data[$Model->alias][$Model->primaryKey]) && !$Model->id) {
+			return true;
+		}
+
+		$habtm = array();
+		foreach ($Model->getAssociated('hasAndBelongsToMany') as $assocAlias) {
+			if (isset($Model->ShadowModel->_schema[$assocAlias])) {
+				$habtm[] = $assocAlias;
+			}
+		}
+		$this->oldData[$Model->alias] = $Model->find('first', array(
+			 		'contain'=> $habtm,
+			 		'conditions'=>array($Model->alias.'.'.$Model->primaryKey => $Model->id)));
+
+		return true;
+	}
+
+	/**
+	 * Returns a generic model that maps to the current $Model's shadow table.
+	 *
+	 * @param object $Model
+	 * @return boolean
+	 */
+	protected function createShadowModel(Model $Model) {
+		if (is_null($this->settings[$Model->alias]['useDbConfig'])) {
+			$dbConfig = $Model->useDbConfig;
+		} else {
+			$dbConfig = $this->settings[$Model->alias]['useDbConfig'];
+		}
+		$db = ConnectionManager::getDataSource($dbConfig);
+		if ($Model->useTable) {
+			$shadow_table = $Model->useTable;
+		} else {
+			$shadow_table = Inflector::tableize($Model->name);
+		}
+		$shadow_table = $shadow_table . $this->revision_suffix;
+		$prefix = $Model->tablePrefix ? $Model->tablePrefix : $db->config['prefix'];
+		$full_table_name = $prefix . $shadow_table;
+
+		$existing_tables = $db->listSources();
+		if (!in_array($full_table_name, $existing_tables)) {
+			$Model->ShadowModel = false;
+			return false;
+		}
+	$useShadowModel = $this->settings[$Model->alias]['model'];
+		if (is_string($useShadowModel) && App::import('model',$useShadowModel)) {
+		$Model->ShadowModel = new $useShadowModel(false, $shadow_table, $dbConfig);
+	} else {
+		$Model->ShadowModel = new Model(false, $shadow_table, $dbConfig);
+	}
+	if ($Model->tablePrefix) {
+		$Model->ShadowModel->tablePrefix = $Model->tablePrefix;
+	}
+		$Model->ShadowModel->alias = $Model->alias;
+		$Model->ShadowModel->primaryKey = 'version_id';
+		$Model->ShadowModel->order = 'version_created DESC, version_id DESC';
+		return true;
+	}
+
+}