Webylon 3.1 API Docs
  • Package
  • Class
  • Tree
  • Deprecated
  • Download
Version: current
  • 3.2
  • 3.1

Packages

  • auth
  • Booking
  • cart
    • shipping
    • steppedcheckout
  • Catalog
  • cms
    • assets
    • batchaction
    • batchactions
    • bulkloading
    • comments
    • content
    • core
    • export
    • newsletter
    • publishers
    • reports
    • security
    • tasks
  • Dashboard
  • DataObjectManager
  • event
  • faq
  • forms
    • actions
    • core
    • fields-basic
    • fields-dataless
    • fields-datetime
    • fields-files
    • fields-formatted
    • fields-formattedinput
    • fields-relational
    • fields-structural
    • transformations
    • validators
  • googlesitemaps
  • guestbook
  • installer
  • newsletter
  • None
  • photo
    • gallery
  • PHP
  • polls
  • recaptcha
  • sapphire
    • api
    • bulkloading
    • control
    • core
    • cron
    • dev
    • email
    • fields-formattedinput
    • filesystem
    • formatters
    • forms
    • i18n
    • integration
    • misc
    • model
    • parsers
    • search
    • security
    • tasks
    • testing
    • tools
    • validation
    • view
    • widgets
  • seo
    • open
      • graph
  • sfDateTimePlugin
  • spamprotection
  • stealth
    • captha
  • subsites
  • userform
    • pagetypes
  • userforms
  • webylon
  • widgets

Classes

  • AdditionalMenuWidget_Item
  • AdvancedSliderHomepageWidget_Item
  • AssetManagerFolder
  • BannerWidget_Item
  • BaseObjectDecorator
  • BookingOrder
  • BookingPaymentMethod
  • BookingService
  • Boolean
  • ButtonsBlockHomepageWidget_Item
  • CarouselHomepageWidget_Item
  • CatalogRubricsHomepageWidget_CatalogDecorator
  • ClientEmailOrderNotification
  • ClientVKOrderNotification
  • ComponentSet
  • Currency
  • DatabaseAdmin
  • DataObject
  • DataObjectDecorator
  • DataObjectLog
  • DataObjectSet
  • DataObjectSet_Iterator
  • Date
  • DB
  • DBField
  • Decimal
  • DocumentItem
  • DocumentPage_File
  • Double
  • Enum
  • ErrorPageSubsite
  • FileDataObjectTrackingDecorator
  • FileImportDecorator
  • Float
  • ForeignKey
  • Hierarchy
  • HTMLText
  • HTMLVarchar
  • ImportLog_Item
  • Int
  • ManagerEmailOrderNotification
  • Material3D_File
  • MediawebPage_File
  • MediawebPage_Photo
  • MobileContentDecorator
  • Money
  • MultiEnum
  • MySQLDatabase
  • MySQLQuery
  • OrderDataObject
  • OrderHandlersDecorator
  • OrderItemVariationDecorator
  • OrderService
  • OrderServiceOrder
  • OrdersExportDecorator
  • PageIcon
  • PageWidgets
  • Payment
  • PaymentMethodShippingDecorator
  • PaymentOrderExtension
  • Percentage
  • PhotoAlbumItem
  • PhotoAlbumProductLinkDecorator
  • PhotoAlbumWidgetLinkDecorator
  • PhotoGalleryHomepageWidget_Item
  • PrimaryKey
  • Product3DDecorator
  • ProductCatalogCatalogLinkedDecorator
  • RatePeriod
  • RealtyImportLog
  • RealtyImportLog_Item
  • RedirectEntry
  • RoomOrder
  • RoomOrderPerson
  • RoomRate
  • RoomService
  • RoomServiceOrder
  • SberbankPaymentDecorator
  • SeoOpenGraphPageDecorator
  • ServiceOrder
  • ShippingMethodPaymentDecorator
  • ShopCountry
  • SimpleOrderCatalogDecorator
  • SimpleOrderProductDecorator
  • SiteConfigWidgets
  • SiteTreeDecorator
  • SiteTreeImportDecorator
  • SliderHomepageWidget_Item
  • SMSCOrderNotification
  • SMSOrderNotification
  • SortableDataObject
  • SQLMap
  • SQLMap_Iterator
  • SQLQuery
  • SS_Database
  • SS_Datetime
  • SS_Query
  • StringField
  • SubsiteDomain
  • Text
  • TextAnonsWidget_Item
  • Texture3D_File
  • Time
  • Varchar
  • Versioned
  • Versioned_Version
  • VideoCategory
  • VideoEntry
  • VKNotificationQueue
  • WebylonWidget_Item
  • YaMoneyPaymentDecorator
  • Year

Interfaces

  • CompositeDBField
  • CurrentPageIdentifier
  • DataObjectInterface
   1 <?php
   2 /**
   3  * The Versioned decorator allows your DataObjects to have several versions, allowing
   4  * you to rollback changes and view history. An example of this is the pages used in the CMS.
   5  * @package sapphire
   6  * @subpackage model
   7  */
   8 class Versioned extends DataObjectDecorator {
   9     /**
  10      * An array of possible stages.
  11      * @var array
  12      */
  13     protected $stages;
  14 
  15     /**
  16      * The 'default' stage.
  17      * @var string
  18      */
  19     protected $defaultStage;
  20 
  21     /**
  22      * The 'live' stage.
  23      * @var string
  24      */
  25     protected $liveStage;
  26 
  27     /**
  28      * A version that a DataObject should be when it is 'migrating',
  29      * that is, when it is in the process of moving from one stage to another.
  30      * @var string
  31      */
  32     public $migratingVersion;
  33 
  34     /**
  35      * A cache used by get_versionnumber_by_stage().
  36      * Clear through {@link flushCache()}.
  37      *
  38      * @var array
  39      */
  40     protected static $cache_versionnumber;
  41 
  42     /**
  43      * Additional database columns for the new
  44      * "_versions" table. Used in {@link augmentDatabase()}
  45      * and all Versioned calls decorating or creating
  46      * SELECT statements.
  47      *
  48      * @var array $db_for_versions_table
  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      * Additional database indexes for the new
  60      * "_versions" table. Used in {@link augmentDatabase()}.
  61      *
  62      * @var array $indexes_for_versions_table
  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      * Reset static configuration variables to their default values
  76      */
  77     static function reset() {
  78         self::$reading_mode = '';
  79 
  80         Session::clear('readingMode');
  81     }
  82 
  83     /**
  84      * Construct a new Versioned object.
  85      * @var array $stages The different stages the versioned object can be.
  86      * The first stage is consiedered the 'default' stage, the last stage is
  87      * considered the 'live' stage.
  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 extraStatics() {
 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         // Get the content at a specific date
 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                 // Add all <basetable>_versions columns
 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             // Link to the version archived on that date
 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         // Get a specific stage
 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      * Keep track of the archive tables that have been created
 149      */
 150     private static $archive_tables = array();
 151 
 152     /**
 153      * Called by {@link SapphireTest} when the database is reset.
 154      * @todo Reduce the coupling between this and SapphireTest, somehow.
 155      */
 156     public static function on_db_reset() {
 157         // Drop all temporary tables
 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         // Remove references to them
 165         self::$archive_tables = array();
 166     }
 167 
 168     /**
 169      * Create a temporary table mapping each database record to its version on the given date.
 170      * This is used by the versioning system to return database content on that date.
 171      * @param string $baseTable The base table.
 172      * @param string $date The date.  If omitted, then the latest version of each page will be returned.
 173      * @todo Ensure that this is DB abstracted
 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      * An array of DataObject extensions that may require versioning for extra tables
 198      * The array value is a set of suffixes to form these table names, assuming a preceding '_'.
 199      * E.g. if Extension1 creates a new table 'Class_suffix1'
 200      * and Extension2 the tables 'Class_suffix2' and 'Class_suffix3':
 201      *
 202      *  $versionableExtensions = array(
 203      *      'Extension1' => 'suffix1',
 204      *      'Extension2' => array('suffix2', 'suffix3'),
 205      *  );
 206      *
 207      * Make sure your extension has a static $enabled-property that determines if it is
 208      * processed by Versioned.
 209      *
 210      * @var array
 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         // Build a list of suffixes whose tables need versioning
 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         // Add the default table with an empty suffix to the list (table name = class name)
 231         array_push($allSuffixes,'');
 232 
 233         foreach ($allSuffixes as $key => $suffix) {
 234             // check that this is a valid suffix
 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                 // Create tables for other stages
 252                 foreach($this->stages as $stage) {
 253                     // Extra tables for _Live, etc.
 254                     if($stage != $this->defaultStage) {
 255                         DB::requireTable("{$table}_$stage", $fields, $indexes, false);
 256                     }
 257 
 258                     // Version fields on each root table (including Stage)
 259                     /*
 260                     if($isRootClass) {
 261                         $stageTable = ($stage == $this->defaultStage) ? $table : "{$table}_$stage";
 262                         $parts=Array('datatype'=>'int', 'precision'=>11, 'null'=>'not null', 'default'=>(int)0);
 263                         $values=Array('type'=>'int', 'parts'=>$parts);
 264                         DB::requireField($stageTable, 'Version', $values);
 265                     }
 266                     */
 267                 }
 268 
 269                 if($isRootClass) {
 270                     // Create table for all versions
 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                     // Create fields for any tables of subclasses
 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                     // Fix data that lacks the uniqueness constraint (since this was added later and
 302                     // bugs meant that the constraint was validated)
 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                     // Remove junk which has no data in parent classes. Only needs to run the following
 315                     // when versioned data is spread over multiple tables
 316                     if(!$isRootClass && ($versionedTables = ClassInfo::dataClassesFor($table))) {
 317 
 318                         foreach($versionedTables as $child) {
 319                             if($table == $child) break; // only need subclasses
 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      * Augment a write-record request.
 361      * @param SQLQuery $manipulation Query to augment.
 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             // Make sure that the augmented write is being applied to a table that can be versioned
 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             // If we haven't got a version #, then we're creating a new version.
 391             // Otherwise, we're just copying a version to another table
 392             if(!isset($manipulation[$table]['fields']['Version'])) {
 393                 // Add any extra, unchanged fields to the version record.
 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                 // Set up a new entry in (table)_versions
 400                 $newManipulation['fields']['RecordID'] = $rid;
 401                 unset($newManipulation['fields']['ID']);
 402 
 403                 // Create a new version #
 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                 // Add the version number to this data
 421                 $manipulation[$table]['fields']['Version'] = $newManipulation['fields']['Version'];
 422                 $version_table[$table] = $nextVersion;
 423             }
 424 
 425             // Putting a Version of -1 is a signal to leave the version table alone, despite their being no version
 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             // Grab a version number - it should be the same across all tables.
 431             if(isset($manipulation[$table]['fields']['Version'])) $thisVersion = $manipulation[$table]['fields']['Version'];
 432 
 433             // If we're editing Live, then use (table)_Live instead of (table)
 434             if(Versioned::current_stage() && Versioned::current_stage() != $this->defaultStage) {
 435                 // If the record has already been inserted in the (table), get rid of it.
 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         // Clear the migration flag
 447         if($this->migratingVersion) $this->migrateVersion(null);
 448 
 449         // Add the new version # back into the data object, for accessing after this write
 450         if(isset($thisVersion)) $this->owner->Version = str_replace("'","",$thisVersion);
 451     }
 452 
 453     /**
 454      * If a write was skipped, then we need to ensure that we don't leave a migrateVersion()
 455      * value lying around for the next write.
 456      */
 457     function onAfterSkippedWrite() {
 458         $this->migrateVersion(null);
 459     }
 460 
 461     /**
 462      * Determine if a table is supporting the Versioned extensions (e.g. $table_versions does exists)
 463      *
 464      * @param string $table Table name
 465      * @return boolean
 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      * Check if a certain table has the 'Version' field
 475      *
 476      * @param string $table Table name
 477      * @return boolean Returns false if the field isn't in the table, true otherwise
 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      * Get the latest published DataObject.
 504      * @return DataObject
 505      */
 506     function latestPublished() {
 507         // Get the root data object class - this will have the version field
 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      * Move a database record from one stage to the other.
 518      * @param fromStage Place to copy from.  Can be either a stage name or a version number.
 519      * @param toStage Place to copy to.  Must be a stage name.
 520      * @param createNewVersion Set this to true to create a new version number.  By default, the existing version number will be copied over.
 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             // Mark this version as having been published at some stage
 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      * Set the migrating version.
 603      * @param string $version The version.
 604      */
 605     function migrateVersion($version) {
 606         $this->migratingVersion = $version;
 607     }
 608 
 609     /**
 610      * Compare two stages to see if they're different.
 611      * Only checks the version numbers, not the actual content.
 612      * @param string $stage1 The first stage to check.
 613      * @param string $stage2
 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         // We test for equality - if one of the versions doesn't exist, this will be false
 624         //TODO: DB Abstraction: if statement here:
 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      * Return a list of all the versions available.
 635      * @param string $filter
 636      */
 637     function allVersions($filter = "", $sort = "", $limit = "", $join = "", $having = "") {
 638         // Make sure the table names are not postfixed (e.g. _Live)
 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         // Add all <basetable>_versions columns
 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      * Compare two version, and return the diff between them.
 672      * @param string $from The version to compare from.
 673      * @param string $to The version to compare to.
 674      * @return DataObject
 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      * Return the base table - the class that directly extends DataObject.
 686      * @return string
 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      * Choose the stage the site is currently on.
 698      * If $_GET['stage'] is set, then it will use that stage, and store it in the session.
 699      * if $_GET['archiveDate'] is set, it will use that date, and store it in the session.
 700      * If neither of these are set, it checks the session, otherwise the stage is set to 'Live'.
 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 /* httponly */);
 723             } else {
 724                 Cookie::set('bypassStaticCache', '1', 0, null, null, false, true /* httponly */);
 725             }
 726         }
 727     }
 728 
 729     /**
 730      * Set the current reading mode.
 731      */
 732     static function set_reading_mode($mode) {
 733         Versioned::$reading_mode = $mode;
 734     }
 735 
 736     /**
 737      * Get the current reading mode.
 738      * @return string
 739      */
 740     static function get_reading_mode() {
 741         return Versioned::$reading_mode;
 742     }
 743 
 744     /**
 745      * Get the name of the 'live' stage.
 746      * @return string
 747      */
 748     static function get_live_stage() {
 749         return "Live";
 750     }
 751 
 752     /**
 753      * Get the current reading stage.
 754      * @return string
 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      * Get the current archive date.
 763      * @return string
 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      * Set the reading stage.
 772      * @param string $stage New reading stage.
 773      */
 774     static function reading_stage($stage) {
 775         Versioned::set_reading_mode('Stage.' . $stage);
 776     }
 777 
 778     /**
 779      * Set the reading archive date.
 780      * @param string $date New reading archived date.
 781      */
 782     static function reading_archived_date($date) {
 783         Versioned::set_reading_mode('Archive.' . $date);
 784     }
 785 
 786 
 787     /**
 788      * Get a singleton instance of a class in the given stage.
 789      *
 790      * @param string $class The name of the class.
 791      * @param string $stage The name of the stage.
 792      * @param string $filter A filter to be inserted into the WHERE clause.
 793      * @param boolean $cache Use caching.
 794      * @param string $orderby A sort expression to be inserted into the ORDER BY clause.
 795      * @return DataObject
 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      * Gets the current version number of a specific record.
 811      *
 812      * @param string $class
 813      * @param string $stage
 814      * @param int $id
 815      * @param boolean $cache
 816      * @return int
 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         // cached call
 823         if($cache && isset(self::$cache_versionnumber[$baseClass][$stage][$id])) {
 824             return self::$cache_versionnumber[$baseClass][$stage][$id];
 825         }
 826 
 827         // get version as performance-optimized SQL query (gets called for each page in the sitetree)
 828         $version = DB::query("SELECT \"Version\" FROM \"$stageTable\" WHERE \"ID\" = $id")->value();
 829 
 830         // cache value (if required)
 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      * Pre-populate the cache for Versioned::get_versionnumber_by_stage() for a list of record IDs,
 842      * for more efficient database querying.  If $idList is null, then every page will be pre-cached.
 843      */
 844     static function prepopulate_versionnumber_cache($class, $stage, $idList = null) {
 845         $filter = "";
 846         if($idList) {
 847             // Validate the ID list
 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      * Get a set of class instances by the given stage.
 863      *
 864      * @param string $class The name of the class.
 865      * @param string $stage The name of the stage.
 866      * @param string $filter A filter to be inserted into the WHERE clause.
 867      * @param string $sort A sort expression to be inserted into the ORDER BY clause.
 868      * @param string $join A join expression, such as LEFT JOIN or INNER JOIN
 869      * @param int $limit A limit on the number of records returned from the database.
 870      * @param string $containerClass The container class for the result set (default is DataObjectSet)
 871      * @return DataObjectSet
 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         // Fix the version number cache (in case you go delete from stage and then check ExistsOnLive)
 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      * Build a SQL query to get data from the _version table.
 905      * This function is similar in style to {@link DataObject::buildSQL}
 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         // Add all <basetable>_versions columns
 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         // Add all <basetable>_versions columns
 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      * Return the latest version of the given page.
 943      *
 944      * @return DataObject
 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      * Return the equivalent of a DataObject::get() call, querying the latest
 970      * version of each page stored in the (class)_versions tables.
 971      *
 972      * In particular, this will query deleted records as well as active ones.
 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         // Process into a DataObjectSet
 978         $SNG = singleton($class);
 979         return $SNG->buildDataObjectSet($query->execute(), 'DataObjectSet', null, $class);
 980     }
 981 
 982     /**
 983      * Return the query for the equivalent of a DataObject::get() call, querying the latest
 984      * version of each page stored in the (class)_versions tables.
 985      *
 986      * In particular, this will query deleted records as well as active ones.
 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         // Build query
 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      * @return DataObject
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      * @return DataObject
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      * Return a piece of text to keep DataObject cache keys appropriately specific
1062      */
1063     function cacheKeyComponent() {
1064         return 'stage-'.self::current_stage();
1065     }
1066 }
1067 
1068 /**
1069  * Represents a single version of a record.
1070  * @package sapphire
1071  * @subpackage model
1072  * @see Versioned
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. -
Webylon 3.1 API Docs API documentation generated by ApiGen 2.8.0