RevisionBehavior.php 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994
  1. <?php
  2. App::uses('ModelBehavior', 'Model');
  3. /**
  4. * Revision Behavior 2.0.4
  5. *
  6. * Revision is a solution for adding undo and other versioning functionality
  7. * to your database models. It is set up to be easy to apply to your project,
  8. * to be easy to use and not get in the way of your other model activity.
  9. * It is also intended to work well with it's sibling, LogableBehavior.
  10. *
  11. * Feature list :
  12. *
  13. * - Easy to install
  14. * - Automagically save revision on model save
  15. * - Able to ignore model saves which only contain certain fields
  16. * - Limit number of revisions to keep, will delete oldest
  17. * - Undo functionality (or update to any revision directly)
  18. * - Revert to a datetime (and even do so cascading)
  19. * - Get a diff model array to compare two or more revisions
  20. * - Inspect any or all revisions of a model
  21. * - Work with Tree Behavior
  22. * - Includes beforeUndelete and afterUndelete callbacks
  23. * - NEW As of 1.2 behavior will revision HABTM relationships (from one way)
  24. *
  25. * Install instructions :
  26. *
  27. * - Place the newest version of RevisionBehavior in your app/models/behaviors folder
  28. * - Add the behavior to AppModel (or single models if you prefer)
  29. * - Create a shadow table for each model that you want revision for.
  30. * - Behavior will gracefully do nothing for models that has behavior, without table
  31. * - If adding to an existing project, run the initializeRevisions() method once for each model.
  32. *
  33. * About shadow tables :
  34. *
  35. * You should make these AFTER you have baked your ordinary tables as they may interfer. By default
  36. * the tables should be named "[prefix][model_table_name]_revs" If you wish to change the suffix you may
  37. * do so in the property called $revision_suffix found bellow. Also by default the behavior expects
  38. * the revision tables to be in the same dbconfig as the model, but you may change this on a per
  39. * model basis with the useDbConfig config option.
  40. *
  41. * Add the same fields as in the live table, with 3 important differences.
  42. * - The 'id' field should NOT be the primary key, nor auto increment
  43. * - Add the fields 'version_id' (int, primary key, autoincrement) and
  44. * 'version_created' (datetime)
  45. * - Skipp fields that should not be saved in shadowtable (lft,right,weight for instance)
  46. *
  47. * Configuration :
  48. *
  49. * - 'limit' : number of revisions to keep, must be at least 2
  50. * - 'ignore' : array containing the name of fields to ignore
  51. * - 'auto' : boolean when false the behavior will NOT generate revisions in afterSave
  52. * - 'useDbConfig' : string/null Name of dbConfig to use. Null to use Model's
  53. *
  54. * Limit functionality :
  55. * The shadow table will save a revision copy when it saves live data, so the newest
  56. * row in the shadow table will (in most cases) be the same as the current live data.
  57. * The exception is when the ignore field functionality is used and the live data is
  58. * updated only in those fields.
  59. *
  60. * Ignore field(s) functionality :
  61. * If you wish to be able to update certain fields without generating new revisions,
  62. * you can add those fields to the configuration ignore array. Any time the behavior's
  63. * afterSave is called with just primary key and these fields, it will NOT generate
  64. * a new revision. It WILL however save these fields together with other fields when it
  65. * does save a revision. You will probably want to set up cron or otherwise call
  66. * createRevision() to update these fields at some points.
  67. *
  68. * Auto functionality :
  69. * By default the behavior will insert itself into the Model's save process by implementing
  70. * beforeSave and afterSave. In afterSave, the behavior will save a new revision of the dataset
  71. * that is now the live data. If you do NOT want this automatic behavior, you may set the config
  72. * option 'auto' to false. Then the shadow table will remain empty unless you call createRevisions
  73. * manually.
  74. *
  75. * HABTM revision feature :
  76. * In order to do revision on HABTM relationship, add a text field to the main model's shadow table
  77. * with the same name as the association, ie if Article habtm ArticleTag as Tag, add a field 'Tag'
  78. * to articles_revs.
  79. * NB! In version 1.2 and up to current, Using HABTM revision requires that both models uses this
  80. * behavior (even if secondary model does not have a shadow table).
  81. *
  82. * 1.1.1 => 1.1.2 changelog
  83. * - revisions() got new paramter: $include_current
  84. * This now defaults to false, resulting in a change from 1.1.1. See tests
  85. *
  86. * 1.1.6 => 1.2
  87. * - includes HABTM revision control (one way)
  88. *
  89. * 1.2 => 1.2.1
  90. * - api change in revertToDate, added paramter for force delete if reverting to before earliest
  91. *
  92. * 1.2.6 => 1.2.7
  93. * - api change: removed shadow(), changed revertToDate() to only recurse into related models that
  94. * are dependent when cascade is true
  95. *
  96. * @author Ronny Vindenes
  97. * @author Alexander 'alkemann' Morland
  98. * @license MIT
  99. * @modifed 27. march 2009
  100. * @version 2.0.4
  101. */
  102. class RevisionBehavior extends ModelBehavior {
  103. /**
  104. * Behavior settings
  105. *
  106. * @access public
  107. * @var array
  108. */
  109. public $settings = array();
  110. /**
  111. * Shadow table prefix
  112. * Only change this value if it causes table name crashes
  113. *
  114. * @access private
  115. * @var string
  116. */
  117. protected $revision_suffix = '_revs';
  118. /**
  119. * Defaul setting values
  120. *
  121. * @access private
  122. * @var array
  123. */
  124. protected $defaults = array(
  125. 'limit' => false,
  126. 'auto' => true,
  127. 'ignore' => array(),
  128. 'useDbConfig' => null,
  129. 'model' => null
  130. );
  131. /**
  132. * Old data, used to detect changes
  133. *
  134. * @var array
  135. */
  136. protected $oldData = array();
  137. /**
  138. * Configure the behavior through the Model::actsAs property
  139. *
  140. * @param object $Model
  141. * @param array $config
  142. */
  143. public function setup(Model $Model, $config = null) {
  144. if (is_array($config)) {
  145. $this->settings[$Model->alias] = array_merge($this->defaults, $config);
  146. } else {
  147. $this->settings[$Model->alias] = $this->defaults;
  148. }
  149. $this->createShadowModel($Model);
  150. $Model->Behaviors->attach('Containable');
  151. }
  152. /**
  153. * Manually create a revision of the current record of Model->id
  154. *
  155. * @example $this->Post->id = 5; $this->Post->createRevision();
  156. * @param object $Model
  157. * @return boolean success
  158. */
  159. public function createRevision(Model $Model) {
  160. if (! $Model->id) {
  161. trigger_error('RevisionBehavior: Model::id must be set', E_USER_WARNING); return null;
  162. }
  163. if (!$Model->ShadowModel) {
  164. trigger_error('RevisionBehavior: ShadowModel doesnt exist.', E_USER_WARNING);
  165. return false;
  166. }
  167. $habtm = array();
  168. $all_habtm = $Model->getAssociated('hasAndBelongsToMany');
  169. foreach ($all_habtm as $assocAlias) {
  170. if (isset($Model->ShadowModel->_schema[$assocAlias])) {
  171. $habtm[] = $assocAlias;
  172. }
  173. }
  174. $data = $Model->find('first', array(
  175. 'conditions'=>array($Model->alias.'.'.$Model->primaryKey => $Model->id),
  176. 'contain' => $habtm
  177. ));
  178. $Model->ShadowModel->create($data);
  179. $Model->ShadowModel->set('version_created', date('Y-m-d H:i:s'));
  180. foreach ($habtm as $assocAlias) {
  181. $foreign_keys = Set::extract($data,'/'.$assocAlias.'/'.$Model->{$assocAlias}->primaryKey);
  182. $Model->ShadowModel->set($assocAlias, implode(',',$foreign_keys));
  183. }
  184. return $Model->ShadowModel->save();
  185. }
  186. /**
  187. * Returns an array that maps to the Model, only with multiple values for fields that has been changed
  188. *
  189. * @example $this->Post->id = 4; $changes = $this->Post->diff();
  190. * @example $this->Post->id = 4; $my_changes = $this->Post->diff(null,nul,array('conditions'=>array('user_id'=>4)));
  191. * @example $this->Post->id = 4; $difference = $this->Post->diff(45,192);
  192. * @param Object $Model
  193. * @param int $from_version_id
  194. * @param int $to_version_id
  195. * @param array $options
  196. * @return array
  197. */
  198. public function diff(Model $Model, $from_version_id = null, $to_version_id = null, $options = array()) {
  199. if (! $Model->id) {
  200. trigger_error('RevisionBehavior: Model::id must be set', E_USER_WARNING); return null;
  201. }
  202. if (!$Model->ShadowModel) {
  203. trigger_error('RevisionBehavior: ShadowModel doesnt exist.', E_USER_WARNING);
  204. return null;
  205. }
  206. if (isset($options['conditions'])) {
  207. $conditions = am($options['conditions'],array($Model->primaryKey => $Model->id));
  208. } else {
  209. $conditions = array( $Model->primaryKey => $Model->id);
  210. }
  211. if (is_numeric($from_version_id) || is_numeric($to_version_id)) {
  212. if (is_numeric($from_version_id) && is_numeric($to_version_id)) {
  213. $conditions['version_id'] = array($from_version_id,$to_version_id);
  214. if ($Model->ShadowModel->find('count',array('conditions'=>$conditions)) < 2) {
  215. return false;
  216. }
  217. } else {
  218. if (is_numeric($from_version_id)) {
  219. $conditions['version_id'] = $from_version_id;
  220. } else {
  221. $conditions['version_id'] = $to_version_id;
  222. }
  223. if ($Model->ShadowModel->find('count',array('conditions'=>$conditions)) < 1) {
  224. return false;
  225. }
  226. }
  227. }
  228. $conditions = array($Model->primaryKey => $Model->id);
  229. if (is_numeric($from_version_id)) {
  230. $conditions['version_id >='] = $from_version_id;
  231. }
  232. if (is_numeric($to_version_id)) {
  233. $conditions['version_id <='] = $to_version_id;
  234. }
  235. $options['conditions'] = $conditions;
  236. $all = $this->revisions($Model,$options,true);
  237. if (sizeof($all) == 0) {
  238. return null;
  239. }
  240. $unified = array();
  241. $keys = array_keys($all[0][$Model->alias]);
  242. foreach ($keys as $field) {
  243. $all_values = Set::extract($all,'/'.$Model->alias.'/'.$field);
  244. $all_values = array_reverse(array_unique(array_reverse($all_values,true)),true);
  245. if (sizeof($all_values) == 1) {
  246. $unified[$field] = reset($all_values);
  247. } else {
  248. $unified[$field] = $all_values;
  249. }
  250. }
  251. return array($Model->alias => $unified);
  252. }
  253. /**
  254. * Will create a current revision of all rows in Model, if none exist.
  255. * Use this if you add the revision to a model that allready has data in
  256. * the DB.
  257. * If you have large tables or big/many fields, use $limit to reduce the
  258. * number of rows that is run at once.
  259. *
  260. * @example $this->Post->initializeRevisions();
  261. * @param object $Model
  262. * @param int $limit number of rows to initialize in one go
  263. * @return boolean
  264. */
  265. public function initializeRevisions(Model $Model, $limit = 100) {
  266. if (!$Model->ShadowModel) {
  267. trigger_error('RevisionBehavior: ShadowModel doesnt exist.', E_USER_WARNING);
  268. return false;
  269. }
  270. if ($Model->ShadowModel->useTable == false) {
  271. trigger_error('RevisionBehavior: Missing shadowtable : '.$Model->table.$this->suffix, E_USER_WARNING);
  272. return null;
  273. }
  274. if ($Model->ShadowModel->find('count') != 0) {
  275. return false;
  276. }
  277. $count = $Model->find('count');
  278. if ($limit < $count) {
  279. $remaining = $count;
  280. for ($p = 1; true; $p++ ) {
  281. $this->init($Model, $p, $limit);
  282. $remaining = $remaining - $limit;
  283. if ($remaining <= 0) {
  284. break;
  285. }
  286. }
  287. } else {
  288. $this->init($Model, 1, $count);
  289. }
  290. return true;
  291. }
  292. /**
  293. * saves revisions for rows matching page and limit given
  294. *
  295. * @param object $Model
  296. * @param int $page
  297. * @param int $limit
  298. */
  299. protected function init(Model $Model, $page, $limit) {
  300. $habtm = array();
  301. $all_habtm = $Model->getAssociated('hasAndBelongsToMany');
  302. foreach ($all_habtm as $assocAlias) {
  303. if (isset($Model->ShadowModel->_schema[$assocAlias])) {
  304. $habtm[] = $assocAlias;
  305. }
  306. }
  307. $all = $Model->find('all', array(
  308. 'limit' => $limit,
  309. 'page' => $page,
  310. 'contain' => $habtm
  311. ));
  312. $version_created = date('Y-m-d H:i:s');
  313. foreach ($all as $data) {
  314. $Model->ShadowModel->create($data);
  315. $Model->ShadowModel->set('version_created', $version_created);
  316. $Model->ShadowModel->save();
  317. }
  318. }
  319. /**
  320. * Finds the newest revision, including the current one.
  321. * Use with caution, the live model may be different depending on the usage
  322. * of ignore fields.
  323. *
  324. * @example $this->Post->id = 6; $newest_revision = $this->Post->newest();
  325. * @param object $Model
  326. * @param array $options
  327. * @return array
  328. */
  329. public function newest(Model $Model, $options = array()) {
  330. if (! $Model->id) {
  331. trigger_error('RevisionBehavior: Model::id must be set', E_USER_WARNING); return null;
  332. }
  333. if (!$Model->ShadowModel) {
  334. trigger_error('RevisionBehavior: ShadowModel doesnt exist.', E_USER_WARNING);
  335. return null;
  336. }
  337. if (isset($options['conditions'])) {
  338. $options['conditions'] = am($options['conditions'],array($Model->alias.'.'.$Model->primaryKey => $Model->id));
  339. } else {
  340. $options['conditions'] = array( $Model->alias.'.'.$Model->primaryKey => $Model->id);
  341. }
  342. return $Model->ShadowModel->find('first',$options);
  343. }
  344. /**
  345. * Find the oldest revision for the current Model->id
  346. * If no limit is used on revision and revision has been enabled for the model
  347. * since start, this call will return the original first record.
  348. *
  349. * @example $this->Post->id = 2; $original = $this->Post->oldest();
  350. * @param object $Model
  351. * @param array $options
  352. * @return array
  353. */
  354. public function oldest(Model $Model, $options = array()) {
  355. if (! $Model->id) {
  356. trigger_error('RevisionBehavior: Model::id must be set', E_USER_WARNING); return null;
  357. }
  358. if (!$Model->ShadowModel) {
  359. trigger_error('RevisionBehavior: ShadowModel doesnt exist.', E_USER_WARNING);
  360. return null;
  361. }
  362. if (isset($options['conditions'])) {
  363. $options['conditions'] = am($options['conditions'],array($Model->primaryKey => $Model->id));
  364. } else {
  365. $options['conditions'] = array( $Model->primaryKey => $Model->id);
  366. }
  367. $options['order'] = 'version_created ASC, version_id ASC';
  368. return $Model->ShadowModel->find('first',$options);
  369. }
  370. /**
  371. * Find the second newest revisions, including the current one.
  372. *
  373. * @example $this->Post->id = 6; $undo_revision = $this->Post->previous();
  374. * @param object $Model
  375. * @param array $options
  376. * @return array
  377. */
  378. public function previous(Model $Model, $options = array()) {
  379. if (! $Model->id) {
  380. trigger_error('RevisionBehavior: Model::id must be set', E_USER_WARNING); return null;
  381. }
  382. if (!$Model->ShadowModel) {
  383. trigger_error('RevisionBehavior: ShadowModel doesnt exist.', E_USER_WARNING);
  384. return false;
  385. }
  386. $options['limit'] = 1;
  387. $options['page'] = 2;
  388. if (isset($options['conditions'])) {
  389. $options['conditions'] = am($options['conditions'],array($Model->primaryKey => $Model->id));
  390. } else {
  391. $options['conditions'] = array( $Model->primaryKey => $Model->id);
  392. }
  393. $revisions = $Model->ShadowModel->find('all',$options);
  394. if (!$revisions) {
  395. return null;
  396. }
  397. return $revisions[0];
  398. }
  399. /**
  400. * Revert all rows matching conditions to given date.
  401. * Model rows outside condition or not edited will not be affected. Edits since date
  402. * will be reverted and rows created since date deleted.
  403. *
  404. * @param object $Model
  405. * @param array $options 'conditions','date'
  406. * @return boolean success
  407. */
  408. public function revertAll(Model $Model, $options = array()) {
  409. if (!$Model->ShadowModel) {
  410. trigger_error('RevisionBehavior: ShadowModel doesnt exist.', E_USER_WARNING);
  411. return false;
  412. }
  413. if (empty($options) || !isset($options['date'])) {
  414. return FALSE;
  415. }
  416. if (!isset($options['conditions'])) {
  417. $options['conditions'] = array();
  418. }
  419. // leave model rows out side of condtions alone
  420. // leave model rows not edited since date alone
  421. $all = $Model->find('all',array('conditions'=>$options['conditions'],'fields'=>$Model->primaryKey));
  422. $allIds = Set::extract($all,'/'.$Model->alias.'/'.$Model->primaryKey);
  423. $cond = $options['conditions'];
  424. $cond['version_created <'] = $options['date'];
  425. $created_before_date = $Model->ShadowModel->find('all',array(
  426. 'order' => $Model->primaryKey,
  427. 'conditions' => $cond,
  428. 'fields' => array('version_id',$Model->primaryKey)
  429. ));
  430. $created_before_dateIds = Set::extract($created_before_date,'/'.$Model->alias.'/'.$Model->primaryKey);
  431. $deleteIds = array_diff($allIds,$created_before_dateIds);
  432. // delete all Model rows where there are only version_created later than date
  433. $Model->deleteAll(array($Model->alias.'.'.$Model->primaryKey => $deleteIds),false,true);
  434. unset($cond['version_created <']);
  435. $cond['version_created >='] = $options['date'];
  436. $created_after_date = $Model->ShadowModel->find('all',array(
  437. 'order' => $Model->primaryKey,
  438. 'conditions' => $cond,
  439. 'fields' => array('version_id',$Model->primaryKey)
  440. ));
  441. $created_after_dateIds = Set::extract($created_after_date,'/'.$Model->alias.'/'.$Model->primaryKey);
  442. $updateIds = array_diff($created_after_dateIds,$deleteIds);
  443. $revertSuccess = true;
  444. // update model rows that have version_created earlier than date to latest before date
  445. foreach ($updateIds as $mid) {
  446. $Model->id = $mid;
  447. if ( ! $Model->revertToDate($options['date']) ) {
  448. $revertSuccess = false;
  449. }
  450. }
  451. return $revertSuccess;
  452. }
  453. /**
  454. * Revert current Model->id to the given revision id
  455. * Will return false if version id is invalid or save fails
  456. *
  457. * @example $this->Post->id = 3; $this->Post->revertTo(12);
  458. * @param object $Model
  459. * @param int $version_id
  460. * @return boolean
  461. */
  462. public function revertTo(Model $Model, $version_id) {
  463. if (! $Model->id) {
  464. trigger_error('RevisionBehavior: Model::id must be set', E_USER_WARNING); return null;
  465. }
  466. if (!$Model->ShadowModel) {
  467. trigger_error('RevisionBehavior: ShadowModel doesnt exist.', E_USER_WARNING);
  468. return false;
  469. }
  470. $data = $Model->ShadowModel->find('first',array('conditions'=>array('version_id'=>$version_id)));
  471. if ($data == false) {
  472. return false;
  473. }
  474. foreach ($Model->getAssociated('hasAndBelongsToMany') as $assocAlias) {
  475. if (isset($Model->ShadowModel->_schema[$assocAlias])) {
  476. $data[$assocAlias][$assocAlias] = explode(',',$data[$Model->alias][$assocAlias]);
  477. }
  478. }
  479. return $Model->save($data);
  480. }
  481. /**
  482. * Revert to the oldest revision after the given datedate.
  483. * Will cascade to hasOne and hasMany associeted models if $cascade is true.
  484. * Will return false if no change is made on the main model
  485. *
  486. * @example $this->Post->id = 3; $this->Post->revertToDate(date('Y-m-d H:i:s',strtotime('Yesterday')));
  487. * @example $this->Post->id = 4; $this->Post->revertToDate('2008-09-01',true);
  488. * @param object $Model
  489. * @param string $datetime
  490. * @param boolean $cascade
  491. * @param boolean $force_delete
  492. * @return boolean
  493. */
  494. public function revertToDate(Model $Model, $datetime, $cascade = false, $force_delete = false) {
  495. if (! $Model->id) {
  496. trigger_error('RevisionBehavior: Model::id must be set', E_USER_WARNING);
  497. return null;
  498. }
  499. if ($cascade) {
  500. $associated = array_merge($Model->hasMany, $Model->hasOne);
  501. foreach ($associated as $assoc => $data) {
  502. // Continue with next association if no shadow model
  503. if (empty($Model->$assoc->ShadowModel)) {
  504. continue;
  505. }
  506. $ids = array();
  507. $cascade = false;
  508. /* Check if association has dependent children */
  509. $depassoc = array_merge($Model->$assoc->hasMany, $Model->$assoc->hasOne);
  510. foreach ($depassoc as $dep) {
  511. if ($dep['dependent']) {
  512. $cascade = true;
  513. }
  514. }
  515. /* Query live data for children */
  516. $children = $Model->$assoc->find('list', array('conditions'=>array($data['foreignKey']=>$Model->id),'recursive'=>-1));
  517. if (!empty($children)) {
  518. $ids = array_keys($children);
  519. }
  520. /* Query shadow table for deleted children */
  521. $revision_children = $Model->$assoc->ShadowModel->find('all', array(
  522. 'fields'=>array('DISTINCT '.$Model->primaryKey),
  523. 'conditions'=>array(
  524. $data['foreignKey']=>$Model->id,
  525. 'NOT' => array( $Model->primaryKey => $ids )
  526. ),
  527. ));
  528. if (!empty($revision_children)) {
  529. $ids = am($ids,Set::extract($revision_children,'/'.$assoc.'/'.$Model->$assoc->primaryKey));
  530. }
  531. /* Revert all children */
  532. foreach ($ids as $id) {
  533. $Model->$assoc->id = $id;
  534. $Model->$assoc->revertToDate($datetime, $cascade, $force_delete);
  535. }
  536. }
  537. }
  538. if (empty($Model->ShadowModel)) {
  539. return true;
  540. }
  541. $data = $Model->ShadowModel->find('first',array(
  542. 'conditions'=>array(
  543. $Model->primaryKey => $Model->id,
  544. 'version_created <='=>$datetime
  545. ),
  546. 'order'=>'version_created ASC, version_id ASC'
  547. ));
  548. /* If no previous version was found and revertToDate() was called with force_delete, then delete the live data, else leave it alone */
  549. if ($data == false) {
  550. if ($force_delete) {
  551. $Model->logableAction['Revision'] = 'revertToDate('.$datetime.') delete';
  552. return $Model->delete($Model->id);
  553. }
  554. return true;
  555. }
  556. $habtm = array();
  557. foreach ($Model->getAssociated('hasAndBelongsToMany') as $assocAlias) {
  558. if (isset($Model->ShadowModel->_schema[$assocAlias])) {
  559. $habtm[] = $assocAlias;
  560. }
  561. }
  562. $liveData = $Model->find('first', array(
  563. 'contain'=> $habtm,
  564. 'conditions'=>array($Model->alias.'.'.$Model->primaryKey => $Model->id)));
  565. $Model->logableAction['Revision'] = 'revertToDate('.$datetime.') add';
  566. if ($liveData) {
  567. $Model->logableAction['Revision'] = 'revertToDate('.$datetime.') edit';
  568. foreach ($Model->getAssociated('hasAndBelongsToMany') as $assocAlias) {
  569. if (isset($Model->ShadowModel->_schema[$assocAlias])) {
  570. $ids = Set::extract($liveData,'/'.$assocAlias.'/'.$Model->$assocAlias->primaryKey);
  571. if (empty($ids) || is_string($ids)) {
  572. $liveData[$Model->alias][$assocAlias] = '';
  573. } else {
  574. $liveData[$Model->alias][$assocAlias] = implode(',',$ids);
  575. }
  576. $data[$assocAlias][$assocAlias] = explode(',',$data[$Model->alias][$assocAlias]);
  577. }
  578. unset($liveData[$assocAlias]);
  579. }
  580. $changeDetected = false;
  581. foreach ($liveData[$Model->alias] as $key => $value) {
  582. if ( isset($data[$Model->alias][$key])) {
  583. $old_value = $data[$Model->alias][$key];
  584. } else {
  585. $old_value = '';
  586. }
  587. if ($value != $old_value ) {
  588. $changeDetected = true;
  589. }
  590. }
  591. if (!$changeDetected) {
  592. return true;
  593. }
  594. }
  595. $auto = $this->settings[$Model->alias]['auto'];
  596. $this->settings[$Model->alias]['auto'] = false;
  597. $Model->ShadowModel->create($data,true);
  598. $Model->ShadowModel->set('version_created', date('Y-m-d H:i:s'));
  599. $Model->ShadowModel->save();
  600. $Model->version_id = $Model->ShadowModel->id;
  601. $success = $Model->save($data);
  602. $this->settings[$Model->alias]['auto'] = $auto;
  603. return $success;
  604. }
  605. /**
  606. * Returns a comeplete list of revisions for the current Model->id.
  607. * The options array may include Model::find parameters to narrow down result
  608. * Alias for shadow('all',array('conditions'=>array($Model->primaryKey => $Model->id)));
  609. *
  610. * @example $this->Post->id = 4; $history = $this->Post->revisions();
  611. * @example $this->Post->id = 4; $today = $this->Post->revisions(array('conditions'=>array('version_create >'=>'2008-12-10')));
  612. * @param object $Model
  613. * @param array $options
  614. * @param boolean $include_current If true will include last saved (live) data
  615. * @return array
  616. */
  617. public function revisions(Model $Model, $options = array(), $include_current = false) {
  618. if (! $Model->id) {
  619. trigger_error('RevisionBehavior: Model::id must be set', E_USER_WARNING); return null;
  620. }
  621. if (!$Model->ShadowModel) {
  622. trigger_error('RevisionBehavior: ShadowModel doesnt exist.', E_USER_WARNING);
  623. return null;
  624. }
  625. if (isset($options['conditions'])) {
  626. $options['conditions'] = am($options['conditions'],array($Model->alias.'.'.$Model->primaryKey => $Model->id));
  627. } else {
  628. $options['conditions'] = array($Model->alias.'.'.$Model->primaryKey => $Model->id);
  629. }
  630. if ( $include_current == false ) {
  631. $current = $this->newest($Model, array('fields'=>array($Model->alias.'.version_id',$Model->primaryKey)));
  632. $options['conditions'][$Model->alias.'.version_id !='] = $current[$Model->alias]['version_id'];
  633. }
  634. return $Model->ShadowModel->find('all',$options);
  635. }
  636. /**
  637. * Undoes an delete by saving the last revision to the Model
  638. * Will return false if this Model->id exist in the live table.
  639. * Calls Model::beforeUndelete and Model::afterUndelete
  640. *
  641. * @example $this->Post->id = 7; $this->Post->undelete();
  642. * @param object $Model
  643. * @return boolean
  644. */
  645. public function undelete(Model $Model) {
  646. if (! $Model->id) {
  647. trigger_error('RevisionBehavior: Model::id must be set', E_USER_WARNING); return null;
  648. }
  649. if (!$Model->ShadowModel) {
  650. trigger_error('RevisionBehavior: ShadowModel doesnt exist.', E_USER_WARNING);
  651. return false;
  652. }
  653. if ($Model->find('count',array(
  654. 'conditions'=>array($Model->primaryKey=>$Model->id),
  655. 'recursive'=>-1)) > 0) {
  656. return false;
  657. }
  658. $data = $this->newest($Model);
  659. if (!$data) {
  660. return false;
  661. }
  662. $beforeUndeleteSuccess = true;
  663. if (method_exists($Model,'beforeUndelete')) {
  664. $beforeUndeleteSuccess = $Model->beforeUndelete();
  665. }
  666. if (!$beforeUndeleteSuccess) {
  667. return false;
  668. }
  669. $model_id = $data[$Model->alias][$Model->primaryKey];
  670. unset($data[$Model->alias][$Model->ShadowModel->primaryKey]);
  671. $Model->create($data,true);
  672. $auto_setting = $this->settings[$Model->alias]['auto'];
  673. $this->settings[$Model->alias]['auto'] = false;
  674. $save_success = $Model->save();
  675. $this->settings[$Model->alias]['auto'] = $auto_setting;
  676. if (!$save_success) {
  677. return false;
  678. }
  679. $Model->updateAll(
  680. array($Model->primaryKey => $model_id),
  681. array($Model->primaryKey => $Model->id)
  682. );
  683. $Model->id = $model_id;
  684. $Model->createRevision();
  685. $afterUndeleteSuccess = true;
  686. if (method_exists($Model,'afterUndelete')) {
  687. $afterUndeleteSuccess = $Model->afterUndelete();
  688. }
  689. return $afterUndeleteSuccess;
  690. }
  691. /**
  692. * Update to previous revision
  693. *
  694. * @example $this->Post->id = 2; $this->Post->undo();
  695. * @param object $Model
  696. * @return boolean
  697. */
  698. public function undo(Model $Model) {
  699. if (! $Model->id) {
  700. trigger_error('RevisionBehavior: Model::id must be set', E_USER_WARNING); return null;
  701. }
  702. if (!$Model->ShadowModel) {
  703. trigger_error('RevisionBehavior: ShadowModel doesnt exist.', E_USER_WARNING);
  704. return false;
  705. }
  706. $data = $this->previous($Model);
  707. if ($data == false) {
  708. $Model->logableAction['Revision'] = 'undo add';
  709. $Model->delete($Model->id);
  710. return false;
  711. }
  712. foreach ($Model->getAssociated('hasAndBelongsToMany') as $assocAlias) {
  713. if (isset($Model->ShadowModel->_schema[$assocAlias])) {
  714. $data[$assocAlias][$assocAlias] = explode(',',$data[$Model->alias][$assocAlias]);
  715. }
  716. }
  717. $Model->logableAction['Revision'] = 'undo changes';
  718. return $Model->save($data);
  719. }
  720. /**
  721. * Calls create revision for all rows matching primary key list of $idlist
  722. *
  723. * @example $this->Model->updateRevisions(array(1,2,3));
  724. * @param object $Model
  725. * @param array $idlist
  726. */
  727. public function updateRevisions(Model $Model, $idlist = array()) {
  728. if (!$Model->ShadowModel) {
  729. trigger_error('RevisionBehavior: ShadowModel doesnt exist.', E_USER_WARNING);
  730. return null;
  731. }
  732. foreach ($idlist as $id ) {
  733. $Model->id = $id;
  734. $Model->createRevision();
  735. }
  736. }
  737. /**
  738. * Causes revision for habtm associated models if that model does version control
  739. * on their relationship. BeforeDelete identifies the related models that will need
  740. * to do the revision update in afterDelete. Uses
  741. *
  742. * @param unknown_type $Model
  743. */
  744. public function afterDelete(Model $Model) {
  745. if ($this->settings[$Model->alias]['auto'] === false) {
  746. return true;
  747. }
  748. if (!$Model->ShadowModel) {
  749. return true;
  750. }
  751. if (isset($this->deleteUpdates[$Model->alias]) && !empty($this->deleteUpdates[$Model->alias])) {
  752. foreach ($this->deleteUpdates[$Model->alias] as $assocAlias => $assocIds) {
  753. $Model->{$assocAlias}->updateRevisions($assocIds);
  754. }
  755. unset($this->deleteUpdates[$Model->alias]);
  756. }
  757. }
  758. /**
  759. * Will create a new revision if changes have been made in the models non-ignore fields.
  760. * Also deletes oldest revision if limit is (active and) reached.
  761. *
  762. * @param object $Model
  763. * @param boolean $created
  764. * @return boolean
  765. */
  766. public function afterSave(Model $Model, $created) {
  767. if ($this->settings[$Model->alias]['auto'] === false) {
  768. return true;
  769. }
  770. if (!$Model->ShadowModel) {
  771. return true;
  772. }
  773. if ($created) {
  774. $Model->ShadowModel->create($Model->data,true);
  775. $Model->ShadowModel->set($Model->primaryKey,$Model->id);
  776. $Model->ShadowModel->set('version_created',date('Y-m-d H:i:s'));
  777. foreach ($Model->data as $alias => $alias_data) {
  778. if (isset($Model->ShadowModel->_schema[$alias])) {
  779. if (isset($alias_data[$alias]) && !empty($alias_data[$alias])) {
  780. $Model->ShadowModel->set($alias,implode(',',$alias_data[$alias]));
  781. }
  782. }
  783. }
  784. $success = $Model->ShadowModel->save();
  785. $Model->version_id = $Model->ShadowModel->id;
  786. return $success;
  787. }
  788. $habtm = array();
  789. foreach ($Model->getAssociated('hasAndBelongsToMany') as $assocAlias) {
  790. if (isset($Model->ShadowModel->_schema[$assocAlias])) {
  791. $habtm[] = $assocAlias;
  792. }
  793. }
  794. $data = $Model->find('first', array(
  795. 'contain'=> $habtm,
  796. 'conditions'=>array($Model->alias.'.'.$Model->primaryKey => $Model->id)));
  797. $changeDetected = false;
  798. foreach ($data[$Model->alias] as $key => $value) {
  799. if ( isset($data[$Model->alias][$Model->primaryKey])
  800. && !empty($this->oldData[$Model->alias])
  801. && isset($this->oldData[$Model->alias][$Model->alias][$key])) {
  802. $old_value = $this->oldData[$Model->alias][$Model->alias][$key];
  803. } else {
  804. $old_value = '';
  805. }
  806. if ($value != $old_value && !in_array($key,$this->settings[$Model->alias]['ignore'])) {
  807. $changeDetected = true;
  808. }
  809. }
  810. $Model->ShadowModel->create($data);
  811. if (!empty($habtm)) {
  812. foreach ($habtm as $assocAlias) {
  813. if (in_array($assocAlias,$this->settings[$Model->alias]['ignore'])) {
  814. continue;
  815. }
  816. $oldIds = Set::extract($this->oldData[$Model->alias],$assocAlias.'.{n}.id');
  817. if (!isset($Model->data[$assocAlias])) {
  818. $Model->ShadowModel->set($assocAlias, implode(',',$oldIds));
  819. continue;
  820. }
  821. $currentIds = Set::extract($data,$assocAlias.'.{n}.id');
  822. $id_changes = array_diff($currentIds,$oldIds);
  823. if (!empty($id_changes)) {
  824. $Model->ShadowModel->set($assocAlias, implode(',',$currentIds));
  825. $changeDetected = true;
  826. } else {
  827. $Model->ShadowModel->set($assocAlias, implode(',',$oldIds));
  828. }
  829. }
  830. }
  831. unset($this->oldData[$Model->alias]);
  832. if (!$changeDetected) {
  833. return true;
  834. }
  835. $Model->ShadowModel->set('version_created', date('Y-m-d H:i:s'));
  836. $Model->ShadowModel->save();
  837. $Model->version_id = $Model->ShadowModel->id;
  838. if (is_numeric($this->settings[$Model->alias]['limit'])) {
  839. $conditions = array('conditions'=>array($Model->alias.'.'.$Model->primaryKey => $Model->id));
  840. $count = $Model->ShadowModel->find('count', $conditions);
  841. if ($count > $this->settings[$Model->alias]['limit']) {
  842. $conditions['order'] = $Model->alias.'.version_created ASC, '.$Model->alias.'.version_id ASC';
  843. $oldest = $Model->ShadowModel->find('first',$conditions);
  844. $Model->ShadowModel->id = null;
  845. $Model->ShadowModel->delete($oldest[$Model->alias][$Model->ShadowModel->primaryKey]);
  846. }
  847. }
  848. return true;
  849. }
  850. /**
  851. * Causes revision for habtm associated models if that model does version control
  852. * on their relationship. BeforeDelete identifies the related models that will need
  853. * to do the revision update in afterDelete.
  854. *
  855. * @param object $Model
  856. * @return boolean
  857. */
  858. public function beforeDelete(Model $Model) {
  859. if ($this->settings[$Model->alias]['auto'] === false) {
  860. return true;
  861. }
  862. if (!$Model->ShadowModel) {
  863. return true;
  864. }
  865. foreach ($Model->hasAndBelongsToMany as $assocAlias => $a) {
  866. if (isset($Model->{$assocAlias}->ShadowModel->_schema[$Model->alias])) {
  867. $joins = $Model->{$a['with']}->find('all',array(
  868. 'recursive' => -1,
  869. 'conditions' => array(
  870. $a['foreignKey'] => $Model->id
  871. )
  872. ));
  873. $this->deleteUpdates[$Model->alias][$assocAlias] = Set::extract($joins,'/'.$a['with'].'/'.$a['associationForeignKey']);
  874. }
  875. }
  876. return true;
  877. }
  878. /**
  879. * Revision uses the beforeSave callback to remember the old data for comparison in afterSave
  880. *
  881. * @param object $Model
  882. * @return boolean
  883. */
  884. public function beforeSave(Model $Model) {
  885. if ($this->settings[$Model->alias]['auto'] === false) {
  886. return true;
  887. }
  888. if (!$Model->ShadowModel) {
  889. return true;
  890. }
  891. $Model->ShadowModel->create();
  892. if (!isset($Model->data[$Model->alias][$Model->primaryKey]) && !$Model->id) {
  893. return true;
  894. }
  895. $habtm = array();
  896. foreach ($Model->getAssociated('hasAndBelongsToMany') as $assocAlias) {
  897. if (isset($Model->ShadowModel->_schema[$assocAlias])) {
  898. $habtm[] = $assocAlias;
  899. }
  900. }
  901. $this->oldData[$Model->alias] = $Model->find('first', array(
  902. 'contain'=> $habtm,
  903. 'conditions'=>array($Model->alias.'.'.$Model->primaryKey => $Model->id)));
  904. return true;
  905. }
  906. /**
  907. * Returns a generic model that maps to the current $Model's shadow table.
  908. *
  909. * @param object $Model
  910. * @return boolean
  911. */
  912. protected function createShadowModel(Model $Model) {
  913. if (is_null($this->settings[$Model->alias]['useDbConfig'])) {
  914. $dbConfig = $Model->useDbConfig;
  915. } else {
  916. $dbConfig = $this->settings[$Model->alias]['useDbConfig'];
  917. }
  918. $db = ConnectionManager::getDataSource($dbConfig);
  919. if ($Model->useTable) {
  920. $shadow_table = $Model->useTable;
  921. } else {
  922. $shadow_table = Inflector::tableize($Model->name);
  923. }
  924. $shadow_table = $shadow_table . $this->revision_suffix;
  925. $prefix = $Model->tablePrefix ? $Model->tablePrefix : $db->config['prefix'];
  926. $full_table_name = $prefix . $shadow_table;
  927. $existing_tables = $db->listSources();
  928. if (!in_array($full_table_name, $existing_tables)) {
  929. $Model->ShadowModel = false;
  930. return false;
  931. }
  932. $useShadowModel = $this->settings[$Model->alias]['model'];
  933. if (is_string($useShadowModel) && App::import('model',$useShadowModel)) {
  934. $Model->ShadowModel = new $useShadowModel(false, $shadow_table, $dbConfig);
  935. } else {
  936. $Model->ShadowModel = new Model(false, $shadow_table, $dbConfig);
  937. }
  938. if ($Model->tablePrefix) {
  939. $Model->ShadowModel->tablePrefix = $Model->tablePrefix;
  940. }
  941. $Model->ShadowModel->alias = $Model->alias;
  942. $Model->ShadowModel->primaryKey = 'version_id';
  943. $Model->ShadowModel->order = 'version_created DESC, version_id DESC';
  944. return true;
  945. }
  946. }