1 <?php
2 3 4 5 6 7
8 class Versioned extends DataObjectDecorator {
9 10 11 12
13 protected $stages;
14
15 16 17 18
19 protected $defaultStage;
20
21 22 23 24
25 protected $liveStage;
26
27 28 29 30 31
32 public $migratingVersion;
33
34 35 36 37 38 39
40 protected static $cache_versionnumber;
41
42 43 44 45 46 47 48 49
50 static $db_for_versions_table = array(
51 "RecordID" => "Int",
52 "Version" => "Int",
53 "WasPublished" => "Boolean",
54 "AuthorID" => "Int",
55 "PublisherID" => "Int"
56 );
57
58 59 60 61 62 63
64 static $indexes_for_versions_table = array(
65 'RecordID_Version' => '(RecordID,Version)',
66 'RecordID' => true,
67 'Version' => true,
68 'AuthorID' => true,
69 'PublisherID' => true,
70 );
71
72 static $versions_ttl = 20;
73
74 75 76
77 static function reset() {
78 self::$reading_mode = '';
79
80 Session::clear('readingMode');
81 }
82
83 84 85 86 87 88
89 function __construct($stages) {
90 parent::__construct();
91
92 if(!is_array($stages)) {
93 $stages = func_get_args();
94 }
95 $this->stages = $stages;
96 $this->defaultStage = reset($stages);
97 $this->liveStage = array_pop($stages);
98 }
99
100 function () {
101 return array(
102 'db' => array(
103 'Version' => 'Int',
104 ),
105 'has_many' => array(
106 'Versions' => 'SiteTree',
107 )
108 );
109 }
110
111 function augmentSQL(SQLQuery &$query) {
112
113 if($date = Versioned::current_archived_date()) {
114 foreach($query->from as $table => $dummy) {
115 if(!isset($baseTable)) {
116 $baseTable = $table;
117 }
118 $query->renameTable($table, $table . '_versions');
119 $query->replaceText("\"$table\".\"ID\"", "\"$table\".\"RecordID\"");
120
121
122 foreach(self::$db_for_versions_table as $name => $type) {
123 $query->select[] = sprintf('"%s_versions"."%s"', $baseTable, $name);
124 }
125 $query->select[] = sprintf('"%s_versions"."%s" AS "ID"', $baseTable, 'RecordID');
126
127 if($table != $baseTable) {
128 $query->from[$table] .= " AND \"{$table}_versions\".\"Version\" = \"{$baseTable}_versions\".\"Version\"";
129 }
130 }
131
132
133 $archiveTable = $this->requireArchiveTempTable($baseTable, $date);
134 $query->from[$archiveTable] = "INNER JOIN \"$archiveTable\"
135 ON \"$archiveTable\".\"ID\" = \"{$baseTable}_versions\".\"RecordID\"
136 AND \"$archiveTable\".\"Version\" = \"{$baseTable}_versions\".\"Version\"";
137
138
139 } else if(Versioned::current_stage() && Versioned::current_stage() != $this->defaultStage
140 && array_search(Versioned::current_stage(), $this->stages) !== false) {
141 foreach($query->from as $table => $dummy) {
142 $query->renameTable($table, $table . '_' . Versioned::current_stage());
143 }
144 }
145 }
146
147 148 149
150 private static $archive_tables = array();
151
152 153 154 155
156 public static function on_db_reset() {
157
158 $db = DB::getConn();
159 foreach(self::$archive_tables as $tableName) {
160 if(method_exists($db, 'dropTable')) $db->dropTable($tableName);
161 else $db->query("DROP TABLE \"$tableName\"");
162 }
163
164
165 self::$archive_tables = array();
166 }
167
168 169 170 171 172 173 174
175 protected static function requireArchiveTempTable($baseTable, $date = null) {
176 if(!isset(self::$archive_tables[$baseTable])) {
177 self::$archive_tables[$baseTable] = DB::createTable("_Archive$baseTable", array(
178 "ID" => "INT NOT NULL",
179 "Version" => "INT NOT NULL",
180 ), null, array('temporary' => true));
181 }
182
183 if(!DB::query("SELECT COUNT(*) FROM \"" . self::$archive_tables[$baseTable] . "\"")->value()) {
184 if($date) $dateClause = "WHERE \"LastEdited\" <= '$date'";
185 else $dateClause = "";
186
187 DB::query("INSERT INTO \"" . self::$archive_tables[$baseTable] . "\"
188 SELECT \"RecordID\", max(\"Version\") FROM \"{$baseTable}_versions\"
189 $dateClause
190 GROUP BY \"RecordID\"");
191 }
192
193 return self::$archive_tables[$baseTable];
194 }
195
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211
212 protected static $versionableExtensions = array('Translatable' => 'lang');
213
214 function augmentDatabase() {
215 $classTable = $this->owner->class;
216
217 $isRootClass = ($this->owner->class == ClassInfo::baseDataClass($this->owner->class));
218
219
220 $allSuffixes = array();
221 foreach (Versioned::$versionableExtensions as $versionableExtension => $suffixes) {
222 if ($this->owner->hasExtension($versionableExtension)) {
223 $allSuffixes = array_merge($allSuffixes, (array)$suffixes);
224 foreach ((array)$suffixes as $suffix) {
225 $allSuffixes[$suffix] = $versionableExtension;
226 }
227 }
228 }
229
230
231 array_push($allSuffixes,'');
232
233 foreach ($allSuffixes as $key => $suffix) {
234
235 if (!is_int($key)) continue;
236
237 if ($suffix) $table = "{$classTable}_$suffix";
238 else $table = $classTable;
239
240 if($fields = DataObject::database_fields($this->owner->class)) {
241 $indexes = $this->owner->databaseIndexes();
242 if ($suffix && ($ext = $this->owner->getExtensionInstance($allSuffixes[$suffix]))) {
243 if (!$ext->isVersionedTable($table)) continue;
244 $ext->setOwner($this->owner);
245 $fields = $ext->fieldsInExtraTables($suffix);
246 $ext->clearOwner();
247 $indexes = $fields['indexes'];
248 $fields = $fields['db'];
249 }
250
251
252 foreach($this->stages as $stage) {
253
254 if($stage != $this->defaultStage) {
255 DB::requireTable("{$table}_$stage", $fields, $indexes, false);
256 }
257
258
259 260 261 262 263 264 265 266
267 }
268
269 if($isRootClass) {
270
271 $versionFields = array_merge(
272 self::$db_for_versions_table,
273 (array)$fields
274 );
275
276 $versionIndexes = array_merge(
277 self::$indexes_for_versions_table,
278 (array)$indexes
279 );
280 } else {
281
282 $versionFields = array_merge(
283 array(
284 "RecordID" => "Int",
285 "Version" => "Int",
286 ),
287 (array)$fields
288 );
289
290 $versionIndexes = array_merge(
291 array(
292 'RecordID_Version' => array('type' => 'unique', 'value' => 'RecordID,Version'),
293 'RecordID' => true,
294 'Version' => true,
295 ),
296 (array)$indexes
297 );
298 }
299
300 if(DB::getConn()->hasTable("{$table}_versions")) {
301
302
303 $duplications = DB::query("SELECT MIN(\"ID\") AS \"ID\", \"RecordID\", \"Version\"
304 FROM \"{$table}_versions\" GROUP BY \"RecordID\", \"Version\"
305 HAVING COUNT(*) > 1");
306
307 foreach($duplications as $dup) {
308 DB::alteration_message("Removing {$table}_versions duplicate data for "
309 ."{$dup['RecordID']}/{$dup['Version']}" ,"deleted");
310 DB::query("DELETE FROM \"{$table}_versions\" WHERE \"RecordID\" = {$dup['RecordID']}
311 AND \"Version\" = {$dup['Version']} AND \"ID\" != {$dup['ID']}");
312 }
313
314
315
316 if(!$isRootClass && ($versionedTables = ClassInfo::dataClassesFor($table))) {
317
318 foreach($versionedTables as $child) {
319 if($table == $child) break;
320 $count = DB::query("
321 SELECT COUNT(*) FROM \"{$table}_versions\"
322 LEFT JOIN \"{$child}_versions\"
323 ON \"{$child}_versions\".\"RecordID\" = \"{$table}_versions\".\"RecordID\"
324 AND \"{$child}_versions\".\"Version\" = \"{$table}_versions\".\"Version\"
325 WHERE \"{$child}_versions\".\"ID\" IS NULL
326 ")->value();
327
328 if($count > 0) {
329 DB::alteration_message("Removing orphaned versioned records", "deleted");
330
331 $effectedIDs = DB::query("
332 SELECT \"{$table}_versions\".\"ID\" FROM \"{$table}_versions\"
333 LEFT JOIN \"{$child}_versions\"
334 ON \"{$child}_versions\".\"RecordID\" = \"{$table}_versions\".\"RecordID\"
335 AND \"{$child}_versions\".\"Version\" = \"{$table}_versions\".\"Version\"
336 WHERE \"{$child}_versions\".\"ID\" IS NULL
337 ")->column();
338
339 if(is_array($effectedIDs)) {
340 foreach($effectedIDs as $key => $value) {
341 DB::query("DELETE FROM \"{$table}_versions\" WHERE \"{$table}_versions\".\"ID\" = '$value'");
342 }
343 }
344 }
345 }
346 }
347 }
348
349 DB::requireTable("{$table}_versions", $versionFields, $versionIndexes);
350 } else {
351 DB::dontRequireTable("{$table}_versions");
352 foreach($this->stages as $stage) {
353 if($stage != $this->defaultStage) DB::dontrequireTable("{$table}_$stage");
354 }
355 }
356 }
357 }
358
359 360 361 362
363 function augmentWrite(&$manipulation) {
364 $tables = array_keys($manipulation);
365 $version_table = array();
366 foreach($tables as $table) {
367 $baseDataClass = ClassInfo::baseDataClass($table);
368
369 $isRootClass = ($table == $baseDataClass);
370
371
372 if( !$this->canBeVersioned($table) ) {
373 unset($manipulation[$table]);
374 continue;
375 }
376 $id = $manipulation[$table]['id'] ? $manipulation[$table]['id'] : $manipulation[$table]['fields']['ID'];;
377 if(!$id) user_error("Couldn't find ID in " . var_export($manipulation[$table], true), E_USER_ERROR);
378
379 $rid = isset($manipulation[$table]['RecordID']) ? $manipulation[$table]['RecordID'] : $id;
380
381 $newManipulation = array(
382 "command" => "insert",
383 "fields" => isset($manipulation[$table]['fields']) ? $manipulation[$table]['fields'] : null
384 );
385
386 if($this->migratingVersion) {
387 $manipulation[$table]['fields']['Version'] = $this->migratingVersion;
388 }
389
390
391
392 if(!isset($manipulation[$table]['fields']['Version'])) {
393
394 $data = DB::query("SELECT * FROM \"$table\" WHERE \"ID\" = $id")->record();
395 if($data) foreach($data as $k => $v) {
396 if (!isset($newManipulation['fields'][$k])) $newManipulation['fields'][$k] = "'" . DB::getConn()->addslashes($v) . "'";
397 }
398
399
400 $newManipulation['fields']['RecordID'] = $rid;
401 unset($newManipulation['fields']['ID']);
402
403
404 if (isset($version_table[$table])) $nextVersion = $version_table[$table];
405 else unset($nextVersion);
406
407 if($rid && !isset($nextVersion)) $nextVersion = DB::query("SELECT MAX(\"Version\") + 1 FROM \"{$baseDataClass}_versions\" WHERE \"RecordID\" = $rid")->value();
408
409 $newManipulation['fields']['Version'] = $nextVersion ? $nextVersion : 1;
410
411 if($isRootClass) {
412 $userID = (Member::currentUser()) ? Member::currentUser()->ID : 0;
413 $newManipulation['fields']['AuthorID'] = $userID;
414 }
415
416
417
418 $manipulation["{$table}_versions"] = $newManipulation;
419
420
421 $manipulation[$table]['fields']['Version'] = $newManipulation['fields']['Version'];
422 $version_table[$table] = $nextVersion;
423 }
424
425
426 if($manipulation[$table]['fields']['Version'] < 0) unset($manipulation[$table]['fields']['Version']);
427
428 if(!$this->hasVersionField($table)) unset($manipulation[$table]['fields']['Version']);
429
430
431 if(isset($manipulation[$table]['fields']['Version'])) $thisVersion = $manipulation[$table]['fields']['Version'];
432
433
434 if(Versioned::current_stage() && Versioned::current_stage() != $this->defaultStage) {
435
436 if($manipulation[$table]['command']=='insert') {
437 DB::query("DELETE FROM \"{$table}\" WHERE \"ID\"='$id'");
438 }
439
440 $newTable = $table . '_' . Versioned::current_stage();
441 $manipulation[$newTable] = $manipulation[$table];
442 unset($manipulation[$table]);
443 }
444 }
445
446
447 if($this->migratingVersion) $this->migrateVersion(null);
448
449
450 if(isset($thisVersion)) $this->owner->Version = str_replace("'","",$thisVersion);
451 }
452
453 454 455 456
457 function onAfterSkippedWrite() {
458 $this->migrateVersion(null);
459 }
460
461 462 463 464 465 466
467 function canBeVersioned($table) {
468 return ClassInfo::exists($table)
469 && ClassInfo::is_subclass_of($table, 'DataObject')
470 && DataObject::has_own_table($table);
471 }
472
473 474 475 476 477 478
479 function hasVersionField($table) {
480 $rPos = strrpos($table,'_');
481 if(($rPos !== false) && in_array(substr($table,$rPos), $this->stages)) {
482 $tableWithoutStage = substr($table,0,$rPos);
483 } else {
484 $tableWithoutStage = $table;
485 }
486 return ('DataObject' == get_parent_class($tableWithoutStage));
487 }
488 function extendWithSuffix($table) {
489 foreach (Versioned::$versionableExtensions as $versionableExtension => $suffixes) {
490 if ($this->owner->hasExtension($versionableExtension)) {
491 $ext = $this->owner->getExtensionInstance($versionableExtension);
492 $ext->setOwner($this->owner);
493 $table = $ext->extendWithSuffix($table);
494 $ext->clearOwner();
495 }
496 }
497 return $table;
498 }
499
500
501
502 503 504 505
506 function latestPublished() {
507
508 $table1 = $this->owner->class;
509 while( ($p = get_parent_class($table1)) != "DataObject") $table1 = $p;
510
511 $table2 = $table1 . "_$this->liveStage";
512
513 return DB::query("SELECT \"$table1\".\"Version\" = \"$table2\".\"Version\" FROM \"$table1\" INNER JOIN \"$table2\" ON \"$table1\".\"ID\" = \"$table2\".\"ID\" WHERE \"$table1\".\"ID\" = ". $this->owner->ID)->value();
514 }
515
516 517 518 519 520 521
522 function publish($fromStage, $toStage, $createNewVersion = false) {
523 $baseClass = $this->owner->class;
524 while( ($p = get_parent_class($baseClass)) != "DataObject") $baseClass = $p;
525 $extTable = $this->extendWithSuffix($baseClass);
526
527 if(is_numeric($fromStage)) {
528 $from = Versioned::get_version($baseClass, $this->owner->ID, $fromStage);
529 } else {
530 $this->owner->flushCache();
531 $from = Versioned::get_one_by_stage($baseClass, $fromStage, "\"{$baseClass}\".\"ID\" = {$this->owner->ID}");
532 }
533
534 $publisherID = isset(Member::currentUser()->ID) ? Member::currentUser()->ID : 0;
535 if($from) {
536 $from->forceChange();
537 if($createNewVersion) {
538 $latest = self::get_latest_version($baseClass, $this->owner->ID);
539 $this->owner->Version = $latest->Version + 1;
540 } else {
541 $from->migrateVersion($from->Version);
542 }
543
544
545 DB::query("UPDATE \"{$extTable}_versions\" SET \"WasPublished\" = '1', \"PublisherID\" = $publisherID WHERE \"RecordID\" = $from->ID AND \"Version\" = $from->Version");
546 $this->clearOldVersions();
547
548 $oldMode = Versioned::get_reading_mode();
549 Versioned::reading_stage($toStage);
550
551 $conn = DB::getConn();
552 if(method_exists($conn, 'allowPrimaryKeyEditing')) $conn->allowPrimaryKeyEditing($baseClass, true);
553 $from->write();
554 if(method_exists($conn, 'allowPrimaryKeyEditing')) $conn->allowPrimaryKeyEditing($baseClass, false);
555
556 $from->destroy();
557
558 Versioned::set_reading_mode($oldMode);
559 } else {
560 user_error("Can't find {$this->owner->URLSegment}/{$this->owner->ID} in stage $fromStage", E_USER_WARNING);
561 }
562 }
563
564 function clearOldVersions($ttl = 0) {
565 $baseClass = $this->owner->class;
566 $tables = array($baseClass);
567 while( ($p = get_parent_class($baseClass)) != "DataObject") {
568 $baseClass = $p;
569 $tables[] = $baseClass;
570 }
571
572 if ($ttl <= 0)
573 $ttl = self::$versions_ttl;
574
575 $id = $this->owner->ID;
576 $extTable = $this->extendWithSuffix($baseClass);
577 $versionsToSave = DB::query("SELECT Version FROM \"{$extTable}_versions\" WHERE WasPublished=1 AND RecordID={$id} ORDER BY ID DESC LIMIT {$ttl}")->column();
578 $versionsToSave = array_merge($versionsToSave, DB::query("SELECT Version FROM \"{$extTable}_versions\" WHERE WasPublished=0 AND RecordID={$id} ORDER BY ID DESC LIMIT {$ttl}")->column());
579 $deleted = 0;
580 if (count($versionsToSave)) {
581 $versionsToSave = join(',', $versionsToSave);
582 foreach ($tables as $table) {
583 $extTable = $this->extendWithSuffix($table);
584 if (DB::getConn()->hasTable("{$extTable}_versions")) {
585 DB::query("DELETE FROM \"{$extTable}_versions\" WHERE RecordID={$id} AND Version not in ({$versionsToSave})");
586 $deleted += DB::affectedRows();
587
588 $duplications = DB::query("SELECT MIN(\"ID\") AS \"ID\", \"RecordID\", \"Version\"
589 FROM \"{$table}_versions\" WHERE \"RecordID\"={$id} GROUP BY \"RecordID\", \"Version\"
590 HAVING COUNT(*) > 1");
591
592 foreach($duplications as $dup) {
593 DB::query("DELETE FROM \"{$table}_versions\" WHERE \"RecordID\" = {$dup['RecordID']} AND \"Version\" = {$dup['Version']} AND \"ID\" != {$dup['ID']}");
594 }
595 }
596 }
597 }
598 return $deleted;
599 }
600
601 602 603 604
605 function migrateVersion($version) {
606 $this->migratingVersion = $version;
607 }
608
609 610 611 612 613 614
615 function stagesDiffer($stage1, $stage2) {
616 $table1 = $this->baseTable($stage1);
617 $table2 = $this->baseTable($stage2);
618
619 if(!is_numeric($this->owner->ID)) {
620 return true;
621 }
622
623
624
625 $stagesAreEqual = DB::query("SELECT CASE WHEN \"$table1\".\"Version\"=\"$table2\".\"Version\" THEN 1 ELSE 0 END FROM \"$table1\" INNER JOIN \"$table2\" ON \"$table1\".\"ID\" = \"$table2\".\"ID\" AND \"$table1\".\"ID\" = {$this->owner->ID}")->value();
626 return !$stagesAreEqual;
627 }
628
629 function Versions($filter = "", $sort = "", $limit = "", $join = "", $having = "") {
630 return $this->allVersions($filter, $sort, $limit, $join, $having);
631 }
632
633 634 635 636
637 function allVersions($filter = "", $sort = "", $limit = "", $join = "", $having = "") {
638
639 $oldMode = self::get_reading_mode();
640 self::reading_stage('Stage');
641
642 $query = $this->owner->extendedSQL($filter, $sort, $limit, $join, $having);
643
644 foreach($query->from as $table => $tableJoin) {
645 if($tableJoin[0] == '"') $baseTable = str_replace('"','',$tableJoin);
646 else if (substr($tableJoin,0,5) != 'INNER') $query->from[$table] = "LEFT JOIN \"$table\" ON \"$table\".\"RecordID\" = \"{$baseTable}_versions\".\"RecordID\" AND \"$table\".\"Version\" = \"{$baseTable}_versions\".\"Version\"";
647 $query->renameTable($table, $table . '_versions');
648 }
649
650
651 foreach(self::$db_for_versions_table as $name => $type) {
652 $query->select[] = sprintf('"%s_versions"."%s"', $baseTable, $name);
653 }
654
655 $query->where[] = "\"{$baseTable}_versions\".\"RecordID\" = '{$this->owner->ID}'";
656 $query->orderby = ($sort) ? $sort : "\"{$baseTable}_versions\".\"LastEdited\" DESC, \"{$baseTable}_versions\".\"Version\" DESC";
657 $query->distinct = true;
658
659 $records = $query->execute();
660 $versions = new DataObjectSet();
661
662 foreach($records as $record) {
663 $versions->push(new Versioned_Version($record));
664 }
665
666 Versioned::set_reading_mode($oldMode);
667 return $versions;
668 }
669
670 671 672 673 674 675
676 function compareVersions($from, $to) {
677 $fromRecord = Versioned::get_version($this->owner->class, $this->owner->ID, $from);
678 $toRecord = Versioned::get_version($this->owner->class, $this->owner->ID, $to);
679
680 $diff = new DataDifferencer($fromRecord, $toRecord);
681 return $diff->diffedData();
682 }
683
684 685 686 687
688 function baseTable($stage = null) {
689 $tableClasses = ClassInfo::dataClassesFor($this->owner->class);
690 $baseClass = array_shift($tableClasses);
691 return (!$stage || $stage == $this->defaultStage) ? $baseClass : $baseClass . "_$stage";
692 }
693
694
695
696 697 698 699 700 701
702 static function choose_site_stage() {
703 if(isset($_GET['stage'])) {
704 $stage = ucfirst(strtolower($_GET['stage']));
705
706 if(!in_array($stage, array('Stage', 'Live'))) $stage = 'Live';
707
708 Session::set('readingMode', 'Stage.' . $stage);
709 }
710 if(isset($_GET['archiveDate'])) {
711 Session::set('readingMode', 'Archive.' . $_GET['archiveDate']);
712 }
713
714 if($mode = Session::get('readingMode')) {
715 Versioned::set_reading_mode($mode);
716 } else {
717 Versioned::reading_stage("Live");
718 }
719
720 if(!headers_sent()) {
721 if(Versioned::current_stage() == 'Live') {
722 Cookie::set('bypassStaticCache', null, 0, null, null, false, true );
723 } else {
724 Cookie::set('bypassStaticCache', '1', 0, null, null, false, true );
725 }
726 }
727 }
728
729 730 731
732 static function set_reading_mode($mode) {
733 Versioned::$reading_mode = $mode;
734 }
735
736 737 738 739
740 static function get_reading_mode() {
741 return Versioned::$reading_mode;
742 }
743
744 745 746 747
748 static function get_live_stage() {
749 return "Live";
750 }
751
752 753 754 755
756 static function current_stage() {
757 $parts = explode('.', Versioned::get_reading_mode());
758 if($parts[0] == 'Stage') return $parts[1];
759 }
760
761 762 763 764
765 static function current_archived_date() {
766 $parts = explode('.', Versioned::get_reading_mode());
767 if($parts[0] == 'Archive') return $parts[1];
768 }
769
770 771 772 773
774 static function reading_stage($stage) {
775 Versioned::set_reading_mode('Stage.' . $stage);
776 }
777
778 779 780 781
782 static function reading_archived_date($date) {
783 Versioned::set_reading_mode('Archive.' . $date);
784 }
785
786
787 788 789 790 791 792 793 794 795 796
797 static function get_one_by_stage($class, $stage, $filter = '', $cache = true, $orderby = '') {
798 $oldMode = Versioned::get_reading_mode();
799 Versioned::reading_stage($stage);
800
801 singleton($class)->flushCache();
802 $result = DataObject::get_one($class, $filter, $cache, $orderby);
803 singleton($class)->flushCache();
804
805 Versioned::set_reading_mode($oldMode);
806 return $result;
807 }
808
809 810 811 812 813 814 815 816 817
818 static function get_versionnumber_by_stage($class, $stage, $id, $cache = true) {
819 $baseClass = ClassInfo::baseDataClass($class);
820 $stageTable = ($stage == 'Stage') ? $baseClass : "{$baseClass}_{$stage}";
821
822
823 if($cache && isset(self::$cache_versionnumber[$baseClass][$stage][$id])) {
824 return self::$cache_versionnumber[$baseClass][$stage][$id];
825 }
826
827
828 $version = DB::query("SELECT \"Version\" FROM \"$stageTable\" WHERE \"ID\" = $id")->value();
829
830
831 if($cache) {
832 if(!isset(self::$cache_versionnumber[$baseClass])) self::$cache_versionnumber[$baseClass] = array();
833 if(!isset(self::$cache_versionnumber[$baseClass][$stage])) self::$cache_versionnumber[$baseClass][$stage] = array();
834 self::$cache_versionnumber[$baseClass][$stage][$id] = $version;
835 }
836
837 return $version;
838 }
839
840 841 842 843
844 static function prepopulate_versionnumber_cache($class, $stage, $idList = null) {
845 $filter = "";
846 if($idList) {
847
848 foreach($idList as $id) if(!is_numeric($id)) user_error("Bad ID passed to Versioned::prepopulate_versionnumber_cache() in \$idList: " . $id, E_USER_ERROR);
849 $filter = "WHERE \"ID\" IN(" .implode(", ", $idList) . ")";
850 }
851
852 $baseClass = ClassInfo::baseDataClass($class);
853 $stageTable = ($stage == 'Stage') ? $baseClass : "{$baseClass}_{$stage}";
854
855 $versions = DB::query("SELECT \"ID\", \"Version\" FROM \"$stageTable\" $filter")->map();
856 foreach($versions as $id => $version) {
857 self::$cache_versionnumber[$baseClass][$stage][$id] = $version;
858 }
859 }
860
861 862 863 864 865 866 867 868 869 870 871 872
873 static function get_by_stage($class, $stage, $filter = '', $sort = '', $join = '', $limit = '', $containerClass = 'DataObjectSet') {
874 $oldMode = Versioned::get_reading_mode();
875 Versioned::reading_stage($stage);
876 $result = DataObject::get($class, $filter, $sort, $join, $limit, $containerClass);
877 Versioned::set_reading_mode($oldMode);
878 return $result;
879 }
880
881 function deleteFromStage($stage) {
882 $oldMode = Versioned::get_reading_mode();
883 Versioned::reading_stage($stage);
884 $clone = clone $this->owner;
885 $result = $clone->delete();
886 Versioned::set_reading_mode($oldMode);
887
888
889 $baseClass = ClassInfo::baseDataClass($this->owner->class);
890 self::$cache_versionnumber[$baseClass][$stage][$this->owner->ID] = null;
891
892 return $result;
893 }
894
895 function writeToStage($stage, $forceInsert = false) {
896 $oldMode = Versioned::get_reading_mode();
897 Versioned::reading_stage($stage);
898 $result = $this->owner->write(false, $forceInsert);
899 Versioned::set_reading_mode($oldMode);
900 return $result;
901 }
902
903 904 905 906
907 function buildVersionSQL($filter = "", $sort = "") {
908 $query = $this->owner->extendedSQL($filter,$sort);
909 foreach($query->from as $table => $join) {
910 if($join[0] == '"') $baseTable = str_replace('"','',$join);
911 else $query->from[$table] = "LEFT JOIN \"$table\" ON \"$table\".\"RecordID\" = \"{$baseTable}_versions\".\"RecordID\" AND \"$table\".\"Version\" = \"{$baseTable}_versions\".\"Version\"";
912 $query->renameTable($table, $table . '_versions');
913 }
914
915
916 foreach(self::$db_for_versions_table as $name => $type) {
917 $query->select[] = sprintf('"%s_versions"."%s"', $baseTable, $name);
918 }
919 $query->select[] = sprintf('"%s_versions"."%s" AS "ID"', $baseTable, 'RecordID');
920
921 return $query;
922 }
923
924 static function build_version_sql($className, $filter = "", $sort = "") {
925 $query = singleton($className)->extendedSQL($filter,$sort);
926 foreach($query->from as $table => $join) {
927 if($join[0] == '"') $baseTable = str_replace('"','',$join);
928 else $query->from[$table] = "LEFT JOIN \"$table\" ON \"$table\".\"RecordID\" = \"{$baseTable}_versions\".\"RecordID\" AND \"$table\".\"Version\" = \"{$baseTable}_versions\".\"Version\"";
929 $query->renameTable($table, $table . '_versions');
930 }
931
932
933 foreach(self::$db_for_versions_table as $name => $type) {
934 $query->select[] = sprintf('"%s_versions"."%s"', $baseTable, $name);
935 }
936 $query->select[] = sprintf('"%s_versions"."%s" AS "ID"', $baseTable, 'RecordID');
937
938 return $query;
939 }
940
941 942 943 944 945
946 static function get_latest_version($class, $id) {
947 $oldMode = Versioned::get_reading_mode();
948 Versioned::set_reading_mode('');
949
950 $baseTable = ClassInfo::baseDataClass($class);
951 $query = singleton($class)->buildVersionSQL("\"{$baseTable}\".\"RecordID\" = $id", "\"{$baseTable}\".\"Version\" DESC");
952 $query->limit = 1;
953 $record = $query->execute()->record();
954 Versioned::set_reading_mode($oldMode);
955
956 if(!$record) return;
957
958 $className = $record['ClassName'];
959 if(!$className) {
960 Debug::show($query->sql());
961 Debug::show($record);
962 user_error("Versioned::get_version: Couldn't get $class.$id", E_USER_ERROR);
963 }
964
965 return new $className($record);
966 }
967
968 969 970 971 972 973
974 static function get_including_deleted($class, $filter = "", $sort = "") {
975 $query = self::get_including_deleted_query($class, $filter, $sort);
976 $query->distinct = true;
977
978 $SNG = singleton($class);
979 return $SNG->buildDataObjectSet($query->execute(), 'DataObjectSet', null, $class);
980 }
981
982 983 984 985 986 987
988 static function get_including_deleted_query($class, $filter = "", $sort = "") {
989 $oldMode = Versioned::get_reading_mode();
990 Versioned::set_reading_mode('');
991
992 $SNG = singleton($class);
993
994
995 $query = $SNG->buildVersionSQL($filter, $sort);
996 $baseTable = ClassInfo::baseDataClass($class);
997 $archiveTable = self::requireArchiveTempTable($baseTable);
998 $query->from[$archiveTable] = "INNER JOIN \"$archiveTable\"
999 ON \"$archiveTable\".\"ID\" = \"{$baseTable}_versions\".\"RecordID\"
1000 AND \"$archiveTable\".\"Version\" = \"{$baseTable}_versions\".\"Version\"";
1001
1002 Versioned::set_reading_mode($oldMode);
1003 return $query;
1004 }
1005
1006 1007 1008
1009 static function get_version($class, $id, $version) {
1010 $oldMode = Versioned::get_reading_mode();
1011 Versioned::set_reading_mode('');
1012
1013 $baseTable = ClassInfo::baseDataClass($class);
1014 $query = singleton($class)->buildVersionSQL("\"{$baseTable}\".\"RecordID\" = $id AND \"{$baseTable}\".\"Version\" = $version");
1015 $record = $query->execute()->record();
1016 $className = $record['ClassName'];
1017 if(!$className) {
1018 Debug::show($query->sql());
1019 Debug::show($record);
1020 user_error("Versioned::get_version: Couldn't get $class.$id, version $version", E_USER_ERROR);
1021 }
1022
1023 Versioned::set_reading_mode($oldMode);
1024 return new $className($record);
1025 }
1026
1027 1028 1029
1030 static function get_all_versions($class, $id, $version) {
1031 $baseTable = ClassInfo::baseDataClass($class);
1032 $query = singleton($class)->buildVersionSQL("\"{$baseTable}\".\"RecordID\" = $id AND \"{$baseTable}\".\"Version\" = $version");
1033 $record = $query->execute()->record();
1034 $className = $record['ClassName'];
1035 if(!$className) {
1036 Debug::show($query->sql());
1037 Debug::show($record);
1038 user_error("Versioned::get_version: Couldn't get $class.$id, version $version", E_USER_ERROR);
1039 }
1040 return new $className($record);
1041 }
1042
1043 function contentcontrollerInit($controller) {
1044 self::choose_site_stage();
1045 }
1046 function modelascontrollerInit($controller) {
1047 self::choose_site_stage();
1048 }
1049
1050 protected static $reading_mode = null;
1051
1052 function updateFieldLabels(&$labels) {
1053 $labels['Versions'] = _t('Versioned.has_many_Versions', 'Versions', PR_MEDIUM, 'Past Versions of this page');
1054 }
1055
1056 function flushCache() {
1057 self::$cache_versionnumber = array();
1058 }
1059
1060 1061 1062
1063 function cacheKeyComponent() {
1064 return 'stage-'.self::current_stage();
1065 }
1066 }
1067
1068 1069 1070 1071 1072 1073
1074 class Versioned_Version extends ViewableData {
1075 protected $record;
1076 protected $object;
1077
1078 function __construct($record) {
1079 $this->record = $record;
1080 $record['ID'] = $record['RecordID'];
1081 $className = $record['ClassName'];
1082
1083 $this->object = new $className($record);
1084 $this->failover = $this->object;
1085
1086 parent::__construct();
1087 }
1088
1089 function PublishedClass() {
1090 return $this->record['WasPublished'] ? 'published' : 'internal';
1091 }
1092
1093 function Author() {
1094 return DataObject::get_by_id("Member", $this->record['AuthorID']);
1095 }
1096
1097 function Publisher() {
1098 if( !$this->record['WasPublished'] )
1099 return null;
1100
1101 return DataObject::get_by_id("Member", $this->record['PublisherID']);
1102 }
1103
1104 function Published() {
1105 return !empty( $this->record['WasPublished'] );
1106 }
1107 }
1108
1109 ?>
1110
[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the
Silverstripe Forums to ask development related questions.
-