2007-07-19 12:40:28 +02:00
< ? php
/**
* The Versioned decorator allows your DataObjects to have several versions , allowing
* you to rollback changes and view history . An example of this is the pages used in the CMS .
2008-02-25 03:10:37 +01:00
* @ package sapphire
* @ subpackage model
2007-07-19 12:40:28 +02:00
*/
class Versioned extends DataObjectDecorator {
/**
* An array of possible stages .
* @ var array
*/
protected $stages ;
/**
* The 'default' stage .
* @ var string
*/
protected $defaultStage ;
/**
* The 'live' stage .
* @ var string
*/
protected $liveStage ;
/**
* A version that a DataObject should be when it is 'migrating' ,
* that is , when it is in the process of moving from one stage to another .
* @ var string
*/
public $migratingVersion ;
2008-12-17 23:38:47 +01:00
/**
* A cache used by get_versionnumber_by_stage () .
* Clear through { @ link flushCache ()} .
*
* @ var array
*/
protected static $cache_versionnumber ;
2009-05-12 06:47:56 +02:00
/**
* Additional database columns for the new
* " _versions " table . Used in { @ link augmentDatabase ()}
* and all Versioned calls decorating or creating
* SELECT statements .
*
* @ var array $db_for_versions_table
*/
static $db_for_versions_table = array (
" RecordID " => " Int " ,
" Version " => " Int " ,
" WasPublished " => " Boolean " ,
" AuthorID " => " Int " ,
" PublisherID " => " Int "
);
/**
* Additional database indexes for the new
* " _versions " table . Used in { @ link augmentDatabase ()} .
*
* @ var array $indexes_for_versions_table
*/
static $indexes_for_versions_table = array (
'RecordID_Version' => '(RecordID, Version)' ,
'RecordID' => true ,
'Version' => true ,
'AuthorID' => true ,
'PublisherID' => true ,
);
2009-08-11 06:45:54 +02:00
/**
* Reset static configuration variables to their default values
*/
static function reset () {
2010-02-04 06:19:02 +01:00
self :: $reading_mode = '' ;
2009-08-11 06:45:54 +02:00
2010-02-04 06:19:02 +01:00
Session :: clear ( 'readingMode' );
2009-08-11 06:45:54 +02:00
}
2009-05-12 06:47:56 +02:00
2007-07-19 12:40:28 +02:00
/**
* Construct a new Versioned object .
* @ var array $stages The different stages the versioned object can be .
* The first stage is consiedered the 'default' stage , the last stage is
* considered the 'live' stage .
*/
function __construct ( $stages ) {
parent :: __construct ();
if ( ! is_array ( $stages )) {
$stages = func_get_args ();
}
$this -> stages = $stages ;
$this -> defaultStage = reset ( $stages );
$this -> liveStage = array_pop ( $stages );
}
2008-11-02 01:36:57 +01:00
function extraStatics () {
2008-08-11 01:35:11 +02:00
return array (
2009-05-07 08:00:50 +02:00
'db' => array (
'Version' => 'Int' ,
),
2008-08-11 01:35:11 +02:00
'has_many' => array (
'Versions' => 'SiteTree' ,
)
);
}
2007-07-19 12:40:28 +02:00
function augmentSQL ( SQLQuery & $query ) {
// Get the content at a specific date
2010-02-04 06:19:02 +01:00
if ( $date = Versioned :: current_archived_date ()) {
2007-07-19 12:40:28 +02:00
foreach ( $query -> from as $table => $dummy ) {
if ( ! isset ( $baseTable )) {
$baseTable = $table ;
}
$query -> renameTable ( $table , $table . '_versions' );
2009-08-10 06:35:50 +02:00
$query -> replaceText ( " \" $table\ " . \ " ID \" " , " \" $table\ " . \ " RecordID \" " );
2009-05-12 06:47:56 +02:00
// Add all <basetable>_versions columns
foreach ( self :: $db_for_versions_table as $name => $type ) {
$query -> select [] = sprintf ( '"%s_versions"."%s"' , $baseTable , $name );
}
$query -> select [] = sprintf ( '"%s_versions"."%s" AS "ID"' , $baseTable , 'RecordID' );
2007-07-19 12:40:28 +02:00
if ( $table != $baseTable ) {
2009-03-09 06:14:45 +01:00
$query -> from [ $table ] .= " AND \" { $table } _versions \" . \" Version \" = \" { $baseTable } _versions \" . \" Version \" " ;
2007-07-19 12:40:28 +02:00
}
}
// Link to the version archived on that date
2009-05-21 07:08:11 +02:00
$archiveTable = $this -> requireArchiveTempTable ( $baseTable , $date );
$query -> from [ $archiveTable ] = " INNER JOIN \" $archiveTable\ "
ON \ " $archiveTable\ " . \ " ID \" = \" { $baseTable } _versions \" . \" RecordID \"
AND \ " $archiveTable\ " . \ " Version \" = \" { $baseTable } _versions \" . \" Version \" " ;
2007-07-19 12:40:28 +02:00
// Get a specific stage
2010-02-04 06:19:02 +01:00
} else if ( Versioned :: current_stage () && Versioned :: current_stage () != $this -> defaultStage
&& array_search ( Versioned :: current_stage (), $this -> stages ) !== false ) {
2007-07-19 12:40:28 +02:00
foreach ( $query -> from as $table => $dummy ) {
2010-02-04 06:19:02 +01:00
$query -> renameTable ( $table , $table . '_' . Versioned :: current_stage ());
2007-07-19 12:40:28 +02:00
}
}
}
2009-05-21 07:08:11 +02:00
/**
* Keep track of the archive tables that have been created
*/
private static $archive_tables = array ();
2009-05-21 10:08:01 +02:00
/**
* Called by { @ link SapphireTest } when the database is reset .
* @ todo Reduce the coupling between this and SapphireTest , somehow .
*/
public static function on_db_reset () {
2010-01-13 00:44:37 +01:00
// Drop all temporary tables
$db = DB :: getConn ();
foreach ( self :: $archive_tables as $tableName ) {
if ( method_exists ( $db , 'dropTable' )) $db -> dropTable ( $tableName );
else $db -> query ( " DROP TABLE \" $tableName\ " " );
}
// Remove references to them
2009-05-21 10:08:01 +02:00
self :: $archive_tables = array ();
}
2007-07-19 12:40:28 +02:00
/**
* Create a temporary table mapping each database record to its version on the given date .
* This is used by the versioning system to return database content on that date .
* @ param string $baseTable The base table .
2009-05-01 05:49:34 +02:00
* @ param string $date The date . If omitted , then the latest version of each page will be returned .
2008-11-24 10:31:14 +01:00
* @ todo Ensure that this is DB abstracted
2007-07-19 12:40:28 +02:00
*/
2009-05-01 05:49:34 +02:00
protected static function requireArchiveTempTable ( $baseTable , $date = null ) {
2009-05-21 07:08:11 +02:00
if ( ! isset ( self :: $archive_tables [ $baseTable ])) {
self :: $archive_tables [ $baseTable ] = DB :: createTable ( " _Archive $baseTable " , array (
" ID " => " INT NOT NULL " ,
" Version " => " INT NOT NULL " ,
), null , array ( 'temporary' => true ));
}
if ( ! DB :: query ( " SELECT COUNT(*) FROM \" " . self :: $archive_tables [ $baseTable ] . " \" " ) -> value ()) {
2009-05-05 09:24:33 +02:00
if ( $date ) $dateClause = " WHERE \" LastEdited \" <= ' $date ' " ;
else $dateClause = " " ;
2009-05-04 03:20:12 +02:00
2009-05-21 07:08:11 +02:00
DB :: query ( " INSERT INTO \" " . self :: $archive_tables [ $baseTable ] . " \"
2009-05-05 09:24:33 +02:00
SELECT \ " RecordID \" , max( \" Version \" ) FROM \" { $baseTable } _versions \"
$dateClause
GROUP BY \ " RecordID \" " );
2007-07-19 12:40:28 +02:00
}
2009-05-21 07:08:11 +02:00
return self :: $archive_tables [ $baseTable ];
2007-07-19 12:40:28 +02:00
}
2009-05-01 05:49:34 +02:00
2007-09-16 18:15:47 +02:00
/**
* An array of DataObject extensions that may require versioning for extra tables
2007-09-16 19:24:51 +02:00
* The array value is a set of suffixes to form these table names , assuming a preceding '_' .
* E . g . if Extension1 creates a new table 'Class_suffix1'
2007-09-16 18:15:47 +02:00
* and Extension2 the tables 'Class_suffix2' and 'Class_suffix3' :
*
* $versionableExtensions = array (
* 'Extension1' => 'suffix1' ,
* 'Extension2' => array ( 'suffix2' , 'suffix3' ),
* );
2007-09-16 19:24:51 +02:00
*
* Make sure your extension has a static $enabled - property that determines if it is
* processed by Versioned .
2007-09-16 18:15:47 +02:00
*
* @ var array
*/
protected static $versionableExtensions = array ( 'Translatable' => 'lang' );
2007-07-19 12:40:28 +02:00
function augmentDatabase () {
2007-09-16 18:15:47 +02:00
$classTable = $this -> owner -> class ;
2009-07-09 08:00:07 +02:00
$isRootClass = ( $this -> owner -> class == ClassInfo :: baseDataClass ( $this -> owner -> class ));
2007-09-16 18:15:47 +02:00
// Build a list of suffixes whose tables need versioning
$allSuffixes = array ();
foreach ( Versioned :: $versionableExtensions as $versionableExtension => $suffixes ) {
2009-08-11 10:49:52 +02:00
if ( $this -> owner -> hasExtension ( $versionableExtension )) {
2007-09-16 18:15:47 +02:00
$allSuffixes = array_merge ( $allSuffixes , ( array ) $suffixes );
foreach (( array ) $suffixes as $suffix ) {
$allSuffixes [ $suffix ] = $versionableExtension ;
}
}
}
2007-07-19 12:40:28 +02:00
2007-09-16 19:24:51 +02:00
// Add the default table with an empty suffix to the list (table name = class name)
2007-09-16 18:15:47 +02:00
array_push ( $allSuffixes , '' );
foreach ( $allSuffixes as $key => $suffix ) {
// check that this is a valid suffix
if ( ! is_int ( $key )) continue ;
if ( $suffix ) $table = " { $classTable } _ $suffix " ;
else $table = $classTable ;
2009-08-11 10:49:52 +02:00
if ( $fields = DataObject :: database_fields ( $this -> owner -> class )) {
2007-09-16 19:24:51 +02:00
$indexes = $this -> owner -> databaseIndexes ();
2010-03-04 05:15:42 +01:00
if ( $suffix && ( $ext = $this -> owner -> getExtensionInstance ( $allSuffixes [ $suffix ]))) {
2007-09-16 18:58:19 +02:00
if ( ! $ext -> isVersionedTable ( $table )) continue ;
2009-06-04 08:48:44 +02:00
$ext -> setOwner ( $this -> owner );
2007-09-16 18:58:19 +02:00
$fields = $ext -> fieldsInExtraTables ( $suffix );
2009-06-04 08:48:44 +02:00
$ext -> clearOwner ();
2007-09-16 18:15:47 +02:00
$indexes = $fields [ 'indexes' ];
$fields = $fields [ 'db' ];
2007-09-16 19:24:51 +02:00
}
2007-07-19 12:40:28 +02:00
2007-09-16 19:24:51 +02:00
// Create tables for other stages
foreach ( $this -> stages as $stage ) {
// Extra tables for _Live, etc.
if ( $stage != $this -> defaultStage ) {
2009-03-11 22:44:58 +01:00
DB :: requireTable ( " { $table } _ $stage " , $fields , $indexes , false );
2007-09-16 19:24:51 +02:00
}
// Version fields on each root table (including Stage)
2009-05-07 08:00:50 +02:00
/*
2009-07-09 08:00:07 +02:00
if ( $isRootClass ) {
2007-09-16 19:24:51 +02:00
$stageTable = ( $stage == $this -> defaultStage ) ? $table : " { $table } _ $stage " ;
2008-11-23 02:01:03 +01:00
$parts = Array ( 'datatype' => 'int' , 'precision' => 11 , 'null' => 'not null' , 'default' => ( int ) 0 );
$values = Array ( 'type' => 'int' , 'parts' => $parts );
DB :: requireField ( $stageTable , 'Version' , $values );
2007-07-19 12:40:28 +02:00
}
2009-05-07 08:00:50 +02:00
*/
2007-07-19 12:40:28 +02:00
}
2007-09-16 19:24:51 +02:00
2009-07-09 08:00:07 +02:00
if ( $isRootClass ) {
// Create table for all versions
$versionFields = array_merge (
self :: $db_for_versions_table ,
( array ) $fields
);
2007-09-16 19:24:51 +02:00
2009-07-09 08:00:07 +02:00
$versionIndexes = array_merge (
self :: $indexes_for_versions_table ,
( array ) $indexes
);
} else {
// Create fields for any tables of subclasses
$versionFields = array_merge (
array (
" RecordID " => " Int " ,
" Version " => " Int " ,
),
( array ) $fields
);
2007-09-16 19:24:51 +02:00
2009-07-09 08:00:07 +02:00
$versionIndexes = array_merge (
array (
'RecordID_Version' => '(RecordID, Version)' ,
'RecordID' => true ,
'Version' => true ,
),
( array ) $indexes
);
2007-09-16 19:24:51 +02:00
}
2009-07-09 08:00:07 +02:00
DB :: requireTable ( " { $table } _versions " , $versionFields , $versionIndexes );
2007-09-16 19:24:51 +02:00
} else {
DB :: dontRequireTable ( " { $table } _versions " );
foreach ( $this -> stages as $stage ) {
if ( $stage != $this -> defaultStage ) DB :: dontrequireTable ( " { $table } _ $stage " );
2007-07-19 12:40:28 +02:00
}
2007-09-16 18:15:47 +02:00
}
2007-07-19 12:40:28 +02:00
}
}
/**
* Augment a write - record request .
* @ param SQLQuery $manipulation Query to augment .
*/
function augmentWrite ( & $manipulation ) {
$tables = array_keys ( $manipulation );
2007-09-16 18:15:47 +02:00
$version_table = array ();
2007-07-19 12:40:28 +02:00
foreach ( $tables as $table ) {
2009-07-09 08:00:07 +02:00
$baseDataClass = ClassInfo :: baseDataClass ( $table );
2009-08-24 09:35:05 +02:00
$isRootClass = ( $table == $baseDataClass );
2007-07-19 12:40:28 +02:00
// Make sure that the augmented write is being applied to a table that can be versioned
2007-09-16 18:15:47 +02:00
if ( ! $this -> canBeVersioned ( $table ) ) {
2007-07-19 12:40:28 +02:00
unset ( $manipulation [ $table ]);
continue ;
}
2009-07-09 08:00:07 +02:00
$id = $manipulation [ $table ][ 'id' ] ? $manipulation [ $table ][ 'id' ] : $manipulation [ $table ][ 'fields' ][ 'ID' ];;
2007-09-16 18:15:47 +02:00
if ( ! $id ) user_error ( " Couldn't find ID in " . var_export ( $manipulation [ $table ], true ), E_USER_ERROR );
2007-07-19 12:40:28 +02:00
2007-09-16 18:15:47 +02:00
$rid = isset ( $manipulation [ $table ][ 'RecordID' ]) ? $manipulation [ $table ][ 'RecordID' ] : $id ;
2007-07-19 12:40:28 +02:00
$newManipulation = array (
" command " => " insert " ,
" fields " => isset ( $manipulation [ $table ][ 'fields' ]) ? $manipulation [ $table ][ 'fields' ] : null
);
if ( $this -> migratingVersion ) {
$manipulation [ $table ][ 'fields' ][ 'Version' ] = $this -> migratingVersion ;
2010-03-17 00:42:23 +01:00
$this -> migrateVersion ( null );
2007-07-19 12:40:28 +02:00
}
2010-03-16 23:43:51 +01:00
2009-07-09 08:00:07 +02:00
// If we haven't got a version #, then we're creating a new version.
// Otherwise, we're just copying a version to another table
2007-07-19 12:40:28 +02:00
if ( ! isset ( $manipulation [ $table ][ 'fields' ][ 'Version' ])) {
// Add any extra, unchanged fields to the version record.
2008-11-24 00:28:16 +01:00
$data = DB :: query ( " SELECT * FROM \" $table\ " WHERE \ " ID \" = $id " ) -> record ();
2007-07-19 12:40:28 +02:00
if ( $data ) foreach ( $data as $k => $v ) {
2009-04-06 01:24:56 +02:00
if ( ! isset ( $newManipulation [ 'fields' ][ $k ])) $newManipulation [ 'fields' ][ $k ] = " ' " . DB :: getConn () -> addslashes ( $v ) . " ' " ;
2007-07-19 12:40:28 +02:00
}
// Set up a new entry in (table)_versions
2007-09-16 18:15:47 +02:00
$newManipulation [ 'fields' ][ 'RecordID' ] = $rid ;
2007-07-19 12:40:28 +02:00
unset ( $newManipulation [ 'fields' ][ 'ID' ]);
// Create a new version #
2007-09-16 18:15:47 +02:00
if ( isset ( $version_table [ $table ])) $nextVersion = $version_table [ $table ];
else unset ( $nextVersion );
2009-07-09 08:00:07 +02:00
if ( $rid && ! isset ( $nextVersion )) $nextVersion = DB :: query ( " SELECT MAX( \" Version \" ) + 1 FROM \" { $baseDataClass } _versions \" WHERE \" RecordID \" = $rid " ) -> value ();
2007-07-19 12:40:28 +02:00
$newManipulation [ 'fields' ][ 'Version' ] = $nextVersion ? $nextVersion : 1 ;
2009-07-09 08:00:07 +02:00
if ( $isRootClass ) {
$userID = ( Member :: currentUser ()) ? Member :: currentUser () -> ID : 0 ;
$newManipulation [ 'fields' ][ 'AuthorID' ] = $userID ;
}
2007-07-19 12:40:28 +02:00
$manipulation [ " { $table } _versions " ] = $newManipulation ;
// Add the version number to this data
$manipulation [ $table ][ 'fields' ][ 'Version' ] = $newManipulation [ 'fields' ][ 'Version' ];
2007-09-16 18:15:47 +02:00
$version_table [ $table ] = $nextVersion ;
2007-07-19 12:40:28 +02:00
}
// Putting a Version of -1 is a signal to leave the version table alone, despite their being no version
if ( $manipulation [ $table ][ 'fields' ][ 'Version' ] < 0 ) unset ( $manipulation [ $table ][ 'fields' ][ 'Version' ]);
2007-09-16 18:58:19 +02:00
if ( ! $this -> hasVersionField ( $table )) unset ( $manipulation [ $table ][ 'fields' ][ 'Version' ]);
2007-07-19 12:40:28 +02:00
// Grab a version number - it should be the same across all tables.
if ( isset ( $manipulation [ $table ][ 'fields' ][ 'Version' ])) $thisVersion = $manipulation [ $table ][ 'fields' ][ 'Version' ];
// If we're editing Live, then use (table)_Live instead of (table)
2010-02-04 06:19:02 +01:00
if ( Versioned :: current_stage () && Versioned :: current_stage () != $this -> defaultStage ) {
$newTable = $table . '_' . Versioned :: current_stage ();
2007-07-19 12:40:28 +02:00
$manipulation [ $newTable ] = $manipulation [ $table ];
unset ( $manipulation [ $table ]);
}
}
// Add the new version # back into the data object, for accessing after this write
2007-09-16 18:15:47 +02:00
if ( isset ( $thisVersion )) $this -> owner -> Version = str_replace ( " ' " , " " , $thisVersion );
}
2010-03-16 23:43:51 +01:00
/**
* If a write was skipped , then we need to ensure that we don ' t leave a migrateVersion ()
* value lying around for the next write .
*/
function onAfterSkippedWrite () {
$this -> migrateVersion ( null );
}
2007-09-16 18:15:47 +02:00
2007-09-16 18:58:19 +02:00
/**
* Determine if a table is supporting the Versioned extensions ( e . g . $table_versions does exists )
*
* @ param string $table Table name
* @ return boolean
*/
2007-09-16 18:15:47 +02:00
function canBeVersioned ( $table ) {
2009-08-11 10:49:52 +02:00
return ClassInfo :: exists ( $table )
&& ClassInfo :: is_subclass_of ( $table , 'DataObject' )
&& DataObject :: has_own_table ( $table );
2007-09-16 18:15:47 +02:00
}
2007-09-16 18:58:19 +02:00
/**
* Check if a certain table has the 'Version' field
*
* @ param string $table Table name
* @ return boolean Returns false if the field isn ' t in the table , true otherwise
*/
function hasVersionField ( $table ) {
2010-03-16 23:08:33 +01:00
$rPos = strrpos ( $table , '_' );
if (( $rPos !== false ) && in_array ( substr ( $table , $rPos ), $this -> stages )) {
$tableWithoutStage = substr ( $table , 0 , $rPos );
} else {
$tableWithoutStage = $table ;
}
return ( 'DataObject' == get_parent_class ( $tableWithoutStage ));
2007-09-16 18:58:19 +02:00
}
2007-09-16 18:15:47 +02:00
function extendWithSuffix ( $table ) {
foreach ( Versioned :: $versionableExtensions as $versionableExtension => $suffixes ) {
if ( $this -> owner -> hasExtension ( $versionableExtension )) {
2010-03-04 05:15:42 +01:00
$ext = $this -> owner -> getExtensionInstance ( $versionableExtension );
2009-06-04 08:48:44 +02:00
$ext -> setOwner ( $this -> owner );
$table = $ext -> extendWithSuffix ( $table );
$ext -> clearOwner ();
2007-09-16 18:15:47 +02:00
}
}
return $table ;
2007-07-19 12:40:28 +02:00
}
//-----------------------------------------------------------------------------------------------//
/**
* Get the latest published DataObject .
* @ return DataObject
*/
function latestPublished () {
// Get the root data object class - this will have the version field
$table1 = $this -> owner -> class ;
while ( ( $p = get_parent_class ( $table1 )) != " DataObject " ) $table1 = $p ;
$table2 = $table1 . " _ $this->liveStage " ;
2008-11-24 00:28:16 +01:00
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 ();
2007-07-19 12:40:28 +02:00
}
/**
* Move a database record from one stage to the other .
* @ param fromStage Place to copy from . Can be either a stage name or a version number .
* @ param toStage Place to copy to . Must be a stage name .
* @ param createNewVersion Set this to true to create a new version number . By default , the existing version number will be copied over .
*/
function publish ( $fromStage , $toStage , $createNewVersion = false ) {
$baseClass = $this -> owner -> class ;
while ( ( $p = get_parent_class ( $baseClass )) != " DataObject " ) $baseClass = $p ;
2009-08-27 06:43:40 +02:00
$extTable = $this -> extendWithSuffix ( $baseClass );
2007-07-19 12:40:28 +02:00
if ( is_numeric ( $fromStage )) {
2009-07-03 03:21:13 +02:00
$from = Versioned :: get_version ( $baseClass , $this -> owner -> ID , $fromStage );
2007-07-19 12:40:28 +02:00
} else {
$this -> owner -> flushCache ();
2009-07-03 03:21:13 +02:00
$from = Versioned :: get_one_by_stage ( $baseClass , $fromStage , " \" { $baseClass } \" . \" ID \" = { $this -> owner -> ID } " );
2007-07-19 12:40:28 +02:00
}
2007-07-20 01:15:05 +02:00
$publisherID = isset ( Member :: currentUser () -> ID ) ? Member :: currentUser () -> ID : 0 ;
2007-07-19 12:40:28 +02:00
if ( $from ) {
$from -> forceChange ();
if ( ! $createNewVersion ) $from -> migrateVersion ( $from -> Version );
// Mark this version as having been published at some stage
2008-11-24 10:31:14 +01:00
DB :: query ( " UPDATE \" { $extTable } _versions \" SET \" WasPublished \" = '1', \" PublisherID \" = $publisherID WHERE \" RecordID \" = $from->ID AND \" Version \" = $from->Version " );
2007-07-19 12:40:28 +02:00
2010-02-04 06:19:02 +01:00
$oldMode = Versioned :: get_reading_mode ();
Versioned :: reading_stage ( $toStage );
2009-07-02 00:27:18 +02:00
$conn = DB :: getConn ();
2009-08-08 06:23:05 +02:00
if ( method_exists ( $conn , 'allowPrimaryKeyEditing' )) $conn -> allowPrimaryKeyEditing ( $baseClass , true );
2007-07-19 12:40:28 +02:00
$from -> write ();
2009-08-08 06:23:05 +02:00
if ( method_exists ( $conn , 'allowPrimaryKeyEditing' )) $conn -> allowPrimaryKeyEditing ( $baseClass , false );
2009-07-02 00:27:18 +02:00
2007-07-19 12:40:28 +02:00
$from -> destroy ();
2010-02-04 06:19:02 +01:00
Versioned :: set_reading_mode ( $oldMode );
2007-07-19 12:40:28 +02:00
} else {
user_error ( " Can't find { $this -> owner -> URLSegment } / { $this -> owner -> ID } in stage $fromStage " , E_USER_WARNING );
}
}
/**
* Set the migrating version .
* @ param string $version The version .
*/
function migrateVersion ( $version ) {
$this -> migratingVersion = $version ;
}
/**
* Compare two stages to see if they ' re different .
* Only checks the version numbers , not the actual content .
* @ param string $stage1 The first stage to check .
* @ param string $stage2
*/
function stagesDiffer ( $stage1 , $stage2 ) {
$table1 = $this -> baseTable ( $stage1 );
$table2 = $this -> baseTable ( $stage2 );
if ( ! is_numeric ( $this -> owner -> ID )) {
return true ;
}
// We test for equality - if one of the versions doesn't exist, this will be false
2008-11-23 02:01:03 +01:00
//TODO: DB Abstraction: if statement here:
2008-11-24 00:28:16 +01:00
$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 ();
2007-07-19 12:40:28 +02:00
return ! $stagesAreEqual ;
}
2009-09-14 07:15:49 +02:00
function Versions ( $filter = " " , $sort = " " , $limit = " " , $join = " " , $having = " " ) {
return $this -> allVersions ( $filter , $sort , $limit , $join , $having );
2008-08-11 01:35:11 +02:00
}
2007-07-19 12:40:28 +02:00
/**
* Return a list of all the versions available .
* @ param string $filter
*/
2009-09-14 07:15:49 +02:00
function allVersions ( $filter = " " , $sort = " " , $limit = " " , $join = " " , $having = " " ) {
$query = $this -> owner -> extendedSQL ( $filter , $sort , $limit , $join , $having );
2007-07-19 12:40:28 +02:00
2009-09-14 07:15:49 +02:00
foreach ( $query -> from as $table => $tableJoin ) {
if ( $tableJoin [ 0 ] == '"' ) $baseTable = str_replace ( '"' , '' , $tableJoin );
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 \" " ;
2007-07-19 12:40:28 +02:00
$query -> renameTable ( $table , $table . '_versions' );
}
2009-05-12 06:47:56 +02:00
// Add all <basetable>_versions columns
foreach ( self :: $db_for_versions_table as $name => $type ) {
$query -> select [] = sprintf ( '"%s_versions"."%s"' , $baseTable , $name );
}
2007-07-19 12:40:28 +02:00
2009-03-03 22:44:21 +01:00
$query -> where [] = " \" { $baseTable } _versions \" . \" RecordID \" = ' { $this -> owner -> ID } ' " ;
2009-09-14 07:15:49 +02:00
$query -> orderby = ( $sort ) ? $sort : " \" { $baseTable } _versions \" . \" LastEdited \" DESC, \" { $baseTable } _versions \" . \" Version \" DESC " ;
2007-07-19 12:40:28 +02:00
$records = $query -> execute ();
$versions = new DataObjectSet ();
foreach ( $records as $record ) {
$versions -> push ( new Versioned_Version ( $record ));
}
return $versions ;
}
/**
* Compare two version , and return the diff between them .
* @ param string $from The version to compare from .
* @ param string $to The version to compare to .
* @ return DataObject
*/
function compareVersions ( $from , $to ) {
$fromRecord = Versioned :: get_version ( $this -> owner -> class , $this -> owner -> ID , $from );
$toRecord = Versioned :: get_version ( $this -> owner -> class , $this -> owner -> ID , $to );
2009-05-23 05:29:33 +02:00
$diff = new DataDifferencer ( $fromRecord , $toRecord );
return $diff -> diffedData ();
2007-07-19 12:40:28 +02:00
}
/**
* Return the base table - the class that directly extends DataObject .
* @ return string
*/
function baseTable ( $stage = null ) {
$tableClasses = ClassInfo :: dataClassesFor ( $this -> owner -> class );
$baseClass = array_shift ( $tableClasses );
return ( ! $stage || $stage == $this -> defaultStage ) ? $baseClass : $baseClass . " _ $stage " ;
}
//-----------------------------------------------------------------------------------------------//
/**
* Choose the stage the site is currently on .
* If $_GET [ 'stage' ] is set , then it will use that stage , and store it in the session .
* if $_GET [ 'archiveDate' ] is set , it will use that date , and store it in the session .
* If neither of these are set , it checks the session , otherwise the stage is set to 'Live' .
*/
static function choose_site_stage () {
if ( isset ( $_GET [ 'stage' ])) {
$_GET [ 'stage' ] = ucfirst ( strtolower ( $_GET [ 'stage' ]));
2010-02-04 06:19:02 +01:00
Session :: set ( 'readingMode' , 'Stage.' . $_GET [ 'stage' ]);
2007-07-19 12:40:28 +02:00
}
2007-11-09 04:40:05 +01:00
if ( isset ( $_GET [ 'archiveDate' ])) {
2010-02-04 06:19:02 +01:00
Session :: set ( 'readingMode' , 'Archive.' . $_GET [ 'archiveDate' ]);
2007-11-09 04:40:05 +01:00
}
2010-02-04 06:19:02 +01:00
if ( Session :: get ( 'readingMode' )) {
Versioned :: set_reading_mode ( Session :: get ( 'readingMode' ));
2007-07-19 12:40:28 +02:00
} else {
Versioned :: reading_stage ( " Live " );
}
2010-01-13 00:21:52 +01:00
if ( Versioned :: current_stage () == 'Live' ) {
Cookie :: set ( 'bypassStaticCache' , null , 0 );
} else {
Cookie :: set ( 'bypassStaticCache' , '1' , 0 );
}
2007-07-19 12:40:28 +02:00
}
2010-02-04 06:19:02 +01:00
/**
* Set the current reading mode .
*/
static function set_reading_mode ( $mode ) {
Versioned :: $reading_mode = $mode ;
}
/**
* Get the current reading mode .
* @ return string
*/
static function get_reading_mode () {
return Versioned :: $reading_mode ;
}
2007-07-19 12:40:28 +02:00
/**
* Get the name of the 'live' stage .
* @ return string
*/
static function get_live_stage () {
return " Live " ;
}
/**
* Get the current reading stage .
* @ return string
*/
static function current_stage () {
2010-02-04 06:19:02 +01:00
$parts = explode ( '.' , Versioned :: get_reading_mode ());
if ( $parts [ 0 ] == 'Stage' ) return $parts [ 1 ];
2007-07-19 12:40:28 +02:00
}
/**
* Get the current archive date .
* @ return string
*/
static function current_archived_date () {
2010-02-04 06:19:02 +01:00
$parts = explode ( '.' , Versioned :: get_reading_mode ());
if ( $parts [ 0 ] == 'Archive' ) return $parts [ 1 ];
2007-07-19 12:40:28 +02:00
}
/**
* Set the reading stage .
* @ param string $stage New reading stage .
*/
static function reading_stage ( $stage ) {
2010-02-04 06:19:02 +01:00
Versioned :: set_reading_mode ( 'Stage.' . $stage );
2007-07-19 12:40:28 +02:00
}
/**
* Set the reading archive date .
* @ param string $date New reading archived date .
*/
static function reading_archived_date ( $date ) {
2010-02-04 06:19:02 +01:00
Versioned :: set_reading_mode ( 'Archive.' . $date );
2007-07-19 12:40:28 +02:00
}
2010-02-04 06:19:02 +01:00
2007-07-19 12:40:28 +02:00
/**
* Get a singleton instance of a class in the given stage .
2008-10-16 12:14:47 +02:00
*
2007-07-19 12:40:28 +02:00
* @ param string $class The name of the class .
* @ param string $stage The name of the stage .
* @ param string $filter A filter to be inserted into the WHERE clause .
2008-10-16 12:14:47 +02:00
* @ param boolean $cache Use caching .
* @ param string $orderby A sort expression to be inserted into the ORDER BY clause .
2007-07-19 12:40:28 +02:00
* @ return DataObject
*/
2008-10-16 12:14:47 +02:00
static function get_one_by_stage ( $class , $stage , $filter = '' , $cache = true , $orderby = '' ) {
2010-02-04 06:19:02 +01:00
$oldMode = Versioned :: get_reading_mode ();
Versioned :: reading_stage ( $stage );
2007-07-19 12:40:28 +02:00
singleton ( $class ) -> flushCache ();
2008-10-16 12:14:47 +02:00
$result = DataObject :: get_one ( $class , $filter , $cache , $orderby );
2007-07-19 12:40:28 +02:00
singleton ( $class ) -> flushCache ();
2010-02-04 06:19:02 +01:00
Versioned :: set_reading_mode ( $oldMode );
2007-07-19 12:40:28 +02:00
return $result ;
}
2008-12-17 23:38:47 +01:00
/**
* Gets the current version number of a specific record .
*
* @ param string $class
* @ param string $stage
* @ param int $id
* @ param boolean $cache
* @ return int
*/
static function get_versionnumber_by_stage ( $class , $stage , $id , $cache = true ) {
$baseClass = ClassInfo :: baseDataClass ( $class );
$stageTable = ( $stage == 'Stage' ) ? $baseClass : " { $baseClass } _ { $stage } " ;
// cached call
if ( $cache && isset ( self :: $cache_versionnumber [ $baseClass ][ $stage ][ $id ])) {
return self :: $cache_versionnumber [ $baseClass ][ $stage ][ $id ];
}
// get version as performance-optimized SQL query (gets called for each page in the sitetree)
2009-02-18 04:12:39 +01:00
$version = DB :: query ( " SELECT \" Version \" FROM \" $stageTable\ " WHERE \ " ID \" = $id " ) -> value ();
2008-12-17 23:38:47 +01:00
// cache value (if required)
if ( $cache ) {
if ( ! isset ( self :: $cache_versionnumber [ $baseClass ])) self :: $cache_versionnumber [ $baseClass ] = array ();
if ( ! isset ( self :: $cache_versionnumber [ $baseClass ][ $stage ])) self :: $cache_versionnumber [ $baseClass ][ $stage ] = array ();
self :: $cache_versionnumber [ $baseClass ][ $stage ][ $id ] = $version ;
}
return $version ;
}
2009-02-02 00:49:53 +01:00
/**
* Pre - populate the cache for Versioned :: get_versionnumber_by_stage () for a list of record IDs ,
* for more efficient database querying . If $idList is null , then every page will be pre - cached .
*/
static function prepopulate_versionnumber_cache ( $class , $stage , $idList = null ) {
$filter = " " ;
if ( $idList ) {
// Validate the ID list
foreach ( $idList as $id ) if ( ! is_numeric ( $id )) user_error ( " Bad ID passed to Versioned::prepopulate_versionnumber_cache() in \$ idList: " . $id , E_USER_ERROR );
2009-03-09 06:14:45 +01:00
$filter = " WHERE \" ID \" IN( " . implode ( " , " , $idList ) . " ) " ;
2009-02-02 00:49:53 +01:00
}
$baseClass = ClassInfo :: baseDataClass ( $class );
$stageTable = ( $stage == 'Stage' ) ? $baseClass : " { $baseClass } _ { $stage } " ;
2009-02-18 04:12:39 +01:00
$versions = DB :: query ( " SELECT \" ID \" , \" Version \" FROM \" $stageTable\ " $filter " )->map();
2009-02-02 00:49:53 +01:00
foreach ( $versions as $id => $version ) {
self :: $cache_versionnumber [ $baseClass ][ $stage ][ $id ] = $version ;
}
}
2008-10-16 12:14:47 +02:00
/**
* Get a set of class instances by the given stage .
*
* @ param string $class The name of the class .
* @ param string $stage The name of the stage .
* @ param string $filter A filter to be inserted into the WHERE clause .
* @ param string $sort A sort expression to be inserted into the ORDER BY clause .
* @ param string $join A join expression , such as LEFT JOIN or INNER JOIN
* @ param int $limit A limit on the number of records returned from the database .
* @ param string $containerClass The container class for the result set ( default is DataObjectSet )
* @ return DataObjectSet
*/
static function get_by_stage ( $class , $stage , $filter = '' , $sort = '' , $join = '' , $limit = '' , $containerClass = 'DataObjectSet' ) {
2010-02-04 06:19:02 +01:00
$oldMode = Versioned :: get_reading_mode ();
Versioned :: reading_stage ( $stage );
2008-10-16 12:14:47 +02:00
$result = DataObject :: get ( $class , $filter , $sort , $join , $limit , $containerClass );
2010-02-04 06:19:02 +01:00
Versioned :: set_reading_mode ( $oldMode );
2007-07-19 12:40:28 +02:00
return $result ;
}
function deleteFromStage ( $stage ) {
2010-02-04 06:19:02 +01:00
$oldMode = Versioned :: get_reading_mode ();
Versioned :: reading_stage ( $stage );
2007-07-19 12:40:28 +02:00
$result = $this -> owner -> delete ();
2010-02-04 06:19:02 +01:00
Versioned :: set_reading_mode ( $oldMode );
2007-07-19 12:40:28 +02:00
return $result ;
}
function writeToStage ( $stage , $forceInsert = false ) {
2010-02-04 06:19:02 +01:00
$oldMode = Versioned :: get_reading_mode ();
Versioned :: reading_stage ( $stage );
2007-07-19 12:40:28 +02:00
$result = $this -> owner -> write ( false , $forceInsert );
2010-02-04 06:19:02 +01:00
Versioned :: set_reading_mode ( $oldMode );
2007-07-19 12:40:28 +02:00
return $result ;
}
/**
* Build a SQL query to get data from the _version table .
* This function is similar in style to { @ link DataObject :: buildSQL }
*/
function buildVersionSQL ( $filter = " " , $sort = " " ) {
2007-09-16 18:15:47 +02:00
$query = $this -> owner -> extendedSQL ( $filter , $sort );
2007-07-19 12:40:28 +02:00
foreach ( $query -> from as $table => $join ) {
2008-11-24 20:28:46 +01:00
if ( $join [ 0 ] == '"' ) $baseTable = str_replace ( '"' , '' , $join );
2009-03-03 22:44:21 +01:00
else $query -> from [ $table ] = " LEFT JOIN \" $table\ " ON \ " $table\ " . \ " RecordID \" = \" { $baseTable } _versions \" . \" RecordID \" AND \" $table\ " . \ " Version \" = \" { $baseTable } _versions \" . \" Version \" " ;
2007-07-19 12:40:28 +02:00
$query -> renameTable ( $table , $table . '_versions' );
}
2009-05-12 06:47:56 +02:00
// Add all <basetable>_versions columns
foreach ( self :: $db_for_versions_table as $name => $type ) {
$query -> select [] = sprintf ( '"%s_versions"."%s"' , $baseTable , $name );
}
$query -> select [] = sprintf ( '"%s_versions"."%s" AS "ID"' , $baseTable , 'RecordID' );
2007-07-19 12:40:28 +02:00
return $query ;
}
static function build_version_sql ( $className , $filter = " " , $sort = " " ) {
2007-09-16 18:15:47 +02:00
$query = singleton ( $className ) -> extendedSQL ( $filter , $sort );
2007-07-19 12:40:28 +02:00
foreach ( $query -> from as $table => $join ) {
2008-11-24 20:28:46 +01:00
if ( $join [ 0 ] == '"' ) $baseTable = str_replace ( '"' , '' , $join );
2009-03-03 22:44:21 +01:00
else $query -> from [ $table ] = " LEFT JOIN \" $table\ " ON \ " $table\ " . \ " RecordID \" = \" { $baseTable } _versions \" . \" RecordID \" AND \" $table\ " . \ " Version \" = \" { $baseTable } _versions \" . \" Version \" " ;
2007-07-19 12:40:28 +02:00
$query -> renameTable ( $table , $table . '_versions' );
}
2009-05-12 06:47:56 +02:00
// Add all <basetable>_versions columns
foreach ( self :: $db_for_versions_table as $name => $type ) {
$query -> select [] = sprintf ( '"%s_versions"."%s"' , $baseTable , $name );
}
$query -> select [] = sprintf ( '"%s_versions"."%s" AS "ID"' , $baseTable , 'RecordID' );
2007-07-19 12:40:28 +02:00
return $query ;
}
/**
2008-12-17 23:38:47 +01:00
* Return the latest version of the given page .
*
* @ return DataObject
2007-07-19 12:40:28 +02:00
*/
static function get_latest_version ( $class , $id ) {
2010-02-04 06:19:02 +01:00
$oldMode = Versioned :: get_reading_mode ();
Versioned :: set_reading_mode ( '' );
2008-02-25 03:10:37 +01:00
2007-07-19 12:40:28 +02:00
$baseTable = ClassInfo :: baseDataClass ( $class );
2009-03-03 22:44:21 +01:00
$query = singleton ( $class ) -> buildVersionSQL ( " \" { $baseTable } \" . \" RecordID \" = $id " , " \" { $baseTable } \" . \" Version \" DESC " );
2007-07-19 12:40:28 +02:00
$query -> limit = 1 ;
$record = $query -> execute () -> record ();
2009-05-12 03:55:43 +02:00
if ( ! $record ) return ;
2007-07-19 12:40:28 +02:00
$className = $record [ 'ClassName' ];
if ( ! $className ) {
Debug :: show ( $query -> sql ());
Debug :: show ( $record );
2009-05-06 06:42:58 +02:00
user_error ( " Versioned::get_version: Couldn't get $class . $id " , E_USER_ERROR );
2007-07-19 12:40:28 +02:00
}
2008-02-25 03:10:37 +01:00
2010-02-04 06:19:02 +01:00
Versioned :: set_reading_mode ( $oldMode );
2008-02-25 03:10:37 +01:00
2007-07-19 12:40:28 +02:00
return new $className ( $record );
}
2009-05-01 05:49:34 +02:00
/**
* Return the equivalent of a DataObject :: get () call , querying the latest
* version of each page stored in the ( class ) _versions tables .
*
* In particular , this will query deleted records as well as active ones .
*/
static function get_including_deleted ( $class , $filter = " " , $sort = " " ) {
2010-01-13 00:21:52 +01:00
$query = self :: get_including_deleted_query ( $class , $filter , $sort );
// Process into a DataObjectSet
$SNG = singleton ( $class );
return $SNG -> buildDataObjectSet ( $query -> execute (), 'DataObjectSet' , null , $class );
}
/**
* Return the query for the equivalent of a DataObject :: get () call , querying the latest
* version of each page stored in the ( class ) _versions tables .
*
* In particular , this will query deleted records as well as active ones .
*/
static function get_including_deleted_query ( $class , $filter = " " , $sort = " " ) {
2010-02-04 06:19:02 +01:00
$oldMode = Versioned :: get_reading_mode ();
Versioned :: set_reading_mode ( '' );
2009-05-01 05:49:34 +02:00
$SNG = singleton ( $class );
// Build query
$query = $SNG -> buildVersionSQL ( $filter , $sort );
$baseTable = ClassInfo :: baseDataClass ( $class );
2009-05-21 07:08:11 +02:00
$archiveTable = self :: requireArchiveTempTable ( $baseTable );
$query -> from [ $archiveTable ] = " INNER JOIN \" $archiveTable\ "
ON \ " $archiveTable\ " . \ " ID \" = \" { $baseTable } _versions \" . \" RecordID \"
AND \ " $archiveTable\ " . \ " Version \" = \" { $baseTable } _versions \" . \" Version \" " ;
2009-05-01 05:49:34 +02:00
2010-02-04 06:19:02 +01:00
Versioned :: set_reading_mode ( $oldMode );
2010-01-13 00:21:52 +01:00
return $query ;
2009-05-01 05:49:34 +02:00
}
2007-07-19 12:40:28 +02:00
2008-12-17 23:38:47 +01:00
/**
* @ return DataObject
*/
2007-07-19 12:40:28 +02:00
static function get_version ( $class , $id , $version ) {
2010-02-04 06:19:02 +01:00
$oldMode = Versioned :: get_reading_mode ();
Versioned :: set_reading_mode ( '' );
2008-02-25 03:10:37 +01:00
2007-07-19 12:40:28 +02:00
$baseTable = ClassInfo :: baseDataClass ( $class );
2009-03-03 22:44:21 +01:00
$query = singleton ( $class ) -> buildVersionSQL ( " \" { $baseTable } \" . \" RecordID \" = $id AND \" { $baseTable } \" . \" Version \" = $version " );
2007-07-19 12:40:28 +02:00
$record = $query -> execute () -> record ();
$className = $record [ 'ClassName' ];
if ( ! $className ) {
Debug :: show ( $query -> sql ());
Debug :: show ( $record );
user_error ( " Versioned::get_version: Couldn't get $class . $id , version $version " , E_USER_ERROR );
}
2008-02-25 03:10:37 +01:00
2010-02-04 06:19:02 +01:00
Versioned :: set_reading_mode ( $oldMode );
2007-07-19 12:40:28 +02:00
return new $className ( $record );
}
2008-12-17 23:38:47 +01:00
/**
* @ return DataObject
*/
2007-07-19 12:40:28 +02:00
static function get_all_versions ( $class , $id , $version ) {
$baseTable = ClassInfo :: baseDataClass ( $class );
2009-03-03 22:44:21 +01:00
$query = singleton ( $class ) -> buildVersionSQL ( " \" { $baseTable } \" . \" RecordID \" = $id AND \" { $baseTable } \" . \" Version \" = $version " );
2007-07-19 12:40:28 +02:00
$record = $query -> execute () -> record ();
2009-05-06 06:42:58 +02:00
$className = $record [ 'ClassName' ];
2007-07-19 12:40:28 +02:00
if ( ! $className ) {
Debug :: show ( $query -> sql ());
Debug :: show ( $record );
user_error ( " Versioned::get_version: Couldn't get $class . $id , version $version " , E_USER_ERROR );
}
return new $className ( $record );
}
2007-08-15 12:13:27 +02:00
function contentcontrollerInit ( $controller ) {
self :: choose_site_stage ();
}
function modelascontrollerInit ( $controller ) {
self :: choose_site_stage ();
}
2010-02-04 06:19:02 +01:00
protected static $reading_mode = null ;
2008-11-02 21:04:10 +01:00
function updateFieldLabels ( & $labels ) {
$labels [ 'Versions' ] = _t ( 'Versioned.has_many_Versions' , 'Versions' , PR_MEDIUM , 'Past Versions of this page' );
}
2008-12-17 23:38:47 +01:00
function flushCache () {
self :: $cache_versionnumber = array ();
}
2010-01-13 00:31:26 +01:00
/**
* Return a piece of text to keep DataObject cache keys appropriately specific
*/
function cacheKeyComponent () {
return 'stage-' . self :: current_stage ();
}
2007-07-19 12:40:28 +02:00
}
/**
* Represents a single version of a record .
2008-02-25 03:10:37 +01:00
* @ package sapphire
* @ subpackage model
* @ see Versioned
2007-07-19 12:40:28 +02:00
*/
class Versioned_Version extends ViewableData {
protected $record ;
protected $object ;
function __construct ( $record ) {
$this -> record = $record ;
$record [ 'ID' ] = $record [ 'RecordID' ];
$className = $record [ 'ClassName' ];
$this -> object = new $className ( $record );
$this -> failover = $this -> object ;
parent :: __construct ();
}
function PublishedClass () {
return $this -> record [ 'WasPublished' ] ? 'published' : 'internal' ;
}
function Author () {
return DataObject :: get_by_id ( " Member " , $this -> record [ 'AuthorID' ]);
}
function Publisher () {
if ( ! $this -> record [ 'WasPublished' ] )
return null ;
return DataObject :: get_by_id ( " Member " , $this -> record [ 'PublisherID' ]);
}
function Published () {
return ! empty ( $this -> record [ 'WasPublished' ] );
}
}
2009-02-02 00:49:53 +01:00
?>