Completed
Push — 3.2 ( d51264...ff5ed6 )
by Damian
13:29 queued 01:53
created

Versioned::publish()   B

Complexity

Conditions 6
Paths 14

Size

Total Lines 54
Code Lines 32

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 54
rs 8.7449
cc 6
eloc 32
nc 14
nop 3

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * The Versioned extension allows your DataObjects to have several versions,
5
 * allowing you to rollback changes and view history. An example of this is
6
 * the pages used in the CMS.
7
 *
8
 * @property int $Version
9
 *
10
 * @package framework
11
 * @subpackage model
12
 */
13
class Versioned extends DataExtension implements TemplateGlobalProvider {
14
	/**
15
	 * An array of possible stages.
16
	 * @var array
17
	 */
18
	protected $stages;
19
20
	/**
21
	 * The 'default' stage.
22
	 * @var string
23
	 */
24
	protected $defaultStage;
25
26
	/**
27
	 * The 'live' stage.
28
	 * @var string
29
	 */
30
	protected $liveStage;
31
32
	/**
33
	 * The default reading mode
34
	 */
35
	const DEFAULT_MODE = 'Stage.Live';
36
37
	/**
38
	 * A version that a DataObject should be when it is 'migrating',
39
	 * that is, when it is in the process of moving from one stage to another.
40
	 * @var string
41
	 */
42
	public $migratingVersion;
43
44
	/**
45
	 * A cache used by get_versionnumber_by_stage().
46
	 * Clear through {@link flushCache()}.
47
	 *
48
	 * @var array
49
	 */
50
	protected static $cache_versionnumber;
51
52
	/**
53
	 * @var string
54
	 */
55
	protected static $reading_mode = null;
56
57
	/**
58
	 * @var Boolean Flag which is temporarily changed during the write() process
59
	 * to influence augmentWrite() behaviour. If set to TRUE, no new version will be created
60
	 * for the following write. Needs to be public as other classes introspect this state
61
	 * during the write process in order to adapt to this versioning behaviour.
62
	 */
63
	public $_nextWriteWithoutVersion = false;
64
65
	/**
66
	 * Additional database columns for the new
67
	 * "_versions" table. Used in {@link augmentDatabase()}
68
	 * and all Versioned calls extending or creating
69
	 * SELECT statements.
70
	 *
71
	 * @var array $db_for_versions_table
72
	 */
73
	private static $db_for_versions_table = array(
74
		"RecordID" => "Int",
75
		"Version" => "Int",
76
		"WasPublished" => "Boolean",
77
		"AuthorID" => "Int",
78
		"PublisherID" => "Int"
79
	);
80
81
	/**
82
	 * @var array
83
	 */
84
	private static $db = array(
85
		'Version' => 'Int'
86
	);
87
88
	/**
89
	 * Used to enable or disable the prepopulation of the version number cache.
90
	 * Defaults to true.
91
	 *
92
	 * @var boolean
93
	 */
94
	private static $prepopulate_versionnumber_cache = true;
95
96
	/**
97
	 * Keep track of the archive tables that have been created.
98
	 *
99
	 * @var array
100
	 */
101
	private static $archive_tables = array();
102
103
	/**
104
	 * Additional database indexes for the new
105
	 * "_versions" table. Used in {@link augmentDatabase()}.
106
	 *
107
	 * @var array $indexes_for_versions_table
108
	 */
109
	private static $indexes_for_versions_table = array(
110
		'RecordID_Version' => '("RecordID","Version")',
111
		'RecordID' => true,
112
		'Version' => true,
113
		'AuthorID' => true,
114
		'PublisherID' => true,
115
	);
116
117
	/**
118
	 * An array of DataObject extensions that may require versioning for extra tables
119
	 * The array value is a set of suffixes to form these table names, assuming a preceding '_'.
120
	 * E.g. if Extension1 creates a new table 'Class_suffix1'
121
	 * and Extension2 the tables 'Class_suffix2' and 'Class_suffix3':
122
	 *
123
	 * 	$versionableExtensions = array(
124
	 * 		'Extension1' => 'suffix1',
125
	 * 		'Extension2' => array('suffix2', 'suffix3'),
126
	 * 	);
127
	 *
128
	 * Make sure your extension has a static $enabled-property that determines if it is
129
	 * processed by Versioned.
130
	 *
131
	 * @var array
132
	 */
133
	protected static $versionableExtensions = array('Translatable' => 'lang');
134
135
	/**
136
	 * Reset static configuration variables to their default values.
137
	 */
138
	public static function reset() {
139
		self::$reading_mode = '';
140
141
		Session::clear('readingMode');
142
	}
143
144
	/**
145
	 * Construct a new Versioned object.
146
	 *
147
	 * @var array $stages The different stages the versioned object can be.
148
	 * The first stage is considered the 'default' stage, the last stage is
149
	 * considered the 'live' stage.
150
	 */
151
	public function __construct($stages = array('Stage','Live')) {
152
		parent::__construct();
153
154
		if(!is_array($stages)) {
155
			$stages = func_get_args();
156
		}
157
158
		$this->stages = $stages;
159
		$this->defaultStage = reset($stages);
160
		$this->liveStage = array_pop($stages);
161
	}
162
163
	/**
164
	 * Amend freshly created DataQuery objects with versioned-specific
165
	 * information.
166
	 *
167
	 * @param SQLQuery
168
	 * @param DataQuery
169
	 */
170
	public function augmentDataQueryCreation(SQLQuery &$query, DataQuery &$dataQuery) {
0 ignored issues
show
Unused Code introduced by
The parameter $query is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
171
		$parts = explode('.', Versioned::get_reading_mode());
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
172
173
		if($parts[0] == 'Archive') {
174
			$dataQuery->setQueryParam('Versioned.mode', 'archive');
175
			$dataQuery->setQueryParam('Versioned.date', $parts[1]);
176
177
		} else if($parts[0] == 'Stage' && $parts[1] != $this->defaultStage
178
				&& array_search($parts[1],$this->stages) !== false) {
179
180
			$dataQuery->setQueryParam('Versioned.mode', 'stage');
181
			$dataQuery->setQueryParam('Versioned.stage', $parts[1]);
182
		}
183
184
	}
185
186
	/**
187
	 * Augment the the SQLQuery that is created by the DataQuery
188
	 * @todo Should this all go into VersionedDataQuery?
189
	 */
190
	public function augmentSQL(SQLQuery &$query, DataQuery &$dataQuery = null) {
191
		if(!$dataQuery || !$dataQuery->getQueryParam('Versioned.mode')) {
192
			return;
193
		}
194
195
		$baseTable = ClassInfo::baseDataClass($dataQuery->dataClass());
196
197
		switch($dataQuery->getQueryParam('Versioned.mode')) {
198
		// Reading a specific data from the archive
199
		case 'archive':
200
			$date = $dataQuery->getQueryParam('Versioned.date');
201
			foreach($query->getFrom() as $table => $dummy) {
202
				if(!DB::get_schema()->hasTable($table . '_versions')) {
203
					continue;
204
				}
205
206
				$query->renameTable($table, $table . '_versions');
207
				$query->replaceText("\"{$table}_versions\".\"ID\"", "\"{$table}_versions\".\"RecordID\"");
208
				$query->replaceText("`{$table}_versions`.`ID`", "`{$table}_versions`.`RecordID`");
209
210
				// Add all <basetable>_versions columns
211
				foreach(Config::inst()->get('Versioned', 'db_for_versions_table') as $name => $type) {
0 ignored issues
show
Bug introduced by
The expression \Config::inst()->get('Ve...db_for_versions_table') of type array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
212
					$query->selectField(sprintf('"%s_versions"."%s"', $baseTable, $name), $name);
213
				}
214
				$query->selectField(sprintf('"%s_versions"."%s"', $baseTable, 'RecordID'), "ID");
215
216
				if($table != $baseTable) {
217
					$query->addWhere("\"{$table}_versions\".\"Version\" = \"{$baseTable}_versions\".\"Version\"");
218
				}
219
			}
220
			// Link to the version archived on that date
221
			$query->addWhere(array(
222
				"\"{$baseTable}_versions\".\"Version\" IN
223
				(SELECT LatestVersion FROM
224
					(SELECT
225
						\"{$baseTable}_versions\".\"RecordID\",
226
						MAX(\"{$baseTable}_versions\".\"Version\") AS LatestVersion
227
						FROM \"{$baseTable}_versions\"
228
						WHERE \"{$baseTable}_versions\".\"LastEdited\" <= ?
229
						GROUP BY \"{$baseTable}_versions\".\"RecordID\"
230
					) AS \"{$baseTable}_versions_latest\"
231
					WHERE \"{$baseTable}_versions_latest\".\"RecordID\" = \"{$baseTable}_versions\".\"RecordID\"
232
				)" => $date
233
			));
234
			break;
235
236
		// Reading a specific stage (Stage or Live)
237
		case 'stage':
238
			$stage = $dataQuery->getQueryParam('Versioned.stage');
239
			if($stage && ($stage != $this->defaultStage)) {
240
				foreach($query->getFrom() as $table => $dummy) {
241
					// Only rewrite table names that are actually part of the subclass tree
242
					// This helps prevent rewriting of other tables that get joined in, in
243
					// particular, many_many tables
244
					if(class_exists($table) && ($table == $this->owner->class
245
							|| is_subclass_of($table, $this->owner->class)
246
							|| is_subclass_of($this->owner->class, $table))) {
247
						$query->renameTable($table, $table . '_' . $stage);
248
					}
249
				}
250
			}
251
			break;
252
253
		// Reading a specific stage, but only return items that aren't in any other stage
254
		case 'stage_unique':
255
			$stage = $dataQuery->getQueryParam('Versioned.stage');
256
257
			// Recurse to do the default stage behavior (must be first, we rely on stage renaming happening before
258
			// below)
259
			$dataQuery->setQueryParam('Versioned.mode', 'stage');
260
			$this->augmentSQL($query, $dataQuery);
261
			$dataQuery->setQueryParam('Versioned.mode', 'stage_unique');
0 ignored issues
show
Bug introduced by
It seems like $dataQuery is not always an object, but can also be of type null. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
262
263
			// Now exclude any ID from any other stage. Note that we double rename to avoid the regular stage rename
264
			// renaming all subquery references to be Versioned.stage
265
			foreach($this->stages as $excluding) {
266
				if ($excluding == $stage) continue;
267
268
				$tempName = 'ExclusionarySource_'.$excluding;
269
				$excludingTable = $baseTable . ($excluding && $excluding != $this->defaultStage ? "_$excluding" : '');
270
271
				$query->addWhere('"'.$baseTable.'"."ID" NOT IN (SELECT "ID" FROM "'.$tempName.'")');
272
				$query->renameTable($tempName, $excludingTable);
273
			}
274
			break;
275
276
		// Return all version instances
277
		case 'all_versions':
278
		case 'latest_versions':
279
			foreach($query->getFrom() as $alias => $join) {
280
				if($alias != $baseTable) {
281
					$query->setJoinFilter($alias, "\"$alias\".\"RecordID\" = \"{$baseTable}_versions\".\"RecordID\""
282
						. " AND \"$alias\".\"Version\" = \"{$baseTable}_versions\".\"Version\"");
283
				}
284
				$query->renameTable($alias, $alias . '_versions');
285
			}
286
287
			// Add all <basetable>_versions columns
288
			foreach(Config::inst()->get('Versioned', 'db_for_versions_table') as $name => $type) {
0 ignored issues
show
Bug introduced by
The expression \Config::inst()->get('Ve...db_for_versions_table') of type array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
289
				$query->selectField(sprintf('"%s_versions"."%s"', $baseTable, $name), $name);
290
			}
291
292
			// Alias the record ID as the row ID
293
			$query->selectField(sprintf('"%s_versions"."%s"', $baseTable, 'RecordID'), "ID");
294
295
			// Ensure that any sort order referring to this ID is correctly aliased
296
			$orders = $query->getOrderBy();
297
			foreach($orders as $order => $dir) {
298
				if($order === "\"$baseTable\".\"ID\"") {
299
					unset($orders[$order]);
300
					$orders["\"{$baseTable}_versions\".\"RecordID\""] = $dir;
301
				}
302
			}
303
			$query->setOrderBy($orders);
304
305
			// latest_version has one more step
306
			// Return latest version instances, regardless of whether they are on a particular stage
307
			// This provides "show all, including deleted" functonality
308
			if($dataQuery->getQueryParam('Versioned.mode') == 'latest_versions') {
309
				$query->addWhere(
310
					"\"{$alias}_versions\".\"Version\" IN
0 ignored issues
show
Bug introduced by
The variable $alias seems to be defined by a foreach iteration on line 279. Are you sure the iterator is never empty, otherwise this variable is not defined?

It seems like you are relying on a variable being defined by an iteration:

foreach ($a as $b) {
}

// $b is defined here only if $a has elements, for example if $a is array()
// then $b would not be defined here. To avoid that, we recommend to set a
// default value for $b.


// Better
$b = 0; // or whatever default makes sense in your context
foreach ($a as $b) {
}

// $b is now guaranteed to be defined here.
Loading history...
311
					(SELECT LatestVersion FROM
312
						(SELECT
313
							\"{$alias}_versions\".\"RecordID\",
314
							MAX(\"{$alias}_versions\".\"Version\") AS LatestVersion
315
							FROM \"{$alias}_versions\"
316
							GROUP BY \"{$alias}_versions\".\"RecordID\"
317
						) AS \"{$alias}_versions_latest\"
318
						WHERE \"{$alias}_versions_latest\".\"RecordID\" = \"{$alias}_versions\".\"RecordID\"
319
					)");
320
			} else {
321
				// If all versions are requested, ensure that records are sorted by this field
322
				$query->addOrderBy(sprintf('"%s_versions"."%s"', $baseTable, 'Version'));
323
			}
324
			break;
325
		default:
326
			throw new InvalidArgumentException("Bad value for query parameter Versioned.mode: "
327
				. $dataQuery->getQueryParam('Versioned.mode'));
328
		}
329
	}
330
331
	/**
332
	 * For lazy loaded fields requiring extra sql manipulation, ie versioning.
333
	 *
334
	 * @param SQLQuery $query
335
	 * @param DataQuery $dataQuery
336
	 * @param DataObject $dataObject
337
	 */
338
	public function augmentLoadLazyFields(SQLQuery &$query, DataQuery &$dataQuery = null, $dataObject) {
0 ignored issues
show
Unused Code introduced by
The parameter $query is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
339
		// The VersionedMode local variable ensures that this decorator only applies to
340
		// queries that have originated from the Versioned object, and have the Versioned
341
		// metadata set on the query object. This prevents regular queries from
342
		// accidentally querying the *_versions tables.
343
		$versionedMode = $dataObject->getSourceQueryParam('Versioned.mode');
344
		$dataClass = $dataQuery->dataClass();
0 ignored issues
show
Bug introduced by
It seems like $dataQuery is not always an object, but can also be of type null. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
345
		$modesToAllowVersioning = array('all_versions', 'latest_versions', 'archive');
346
		if(
347
			!empty($dataObject->Version) &&
348
			(!empty($versionedMode) && in_array($versionedMode,$modesToAllowVersioning))
349
		) {
350
			$dataQuery->where("\"$dataClass\".\"RecordID\" = " . $dataObject->ID);
351
			$dataQuery->where("\"$dataClass\".\"Version\" = " . $dataObject->Version);
352
			$dataQuery->setQueryParam('Versioned.mode', 'all_versions');
353
		} else {
354
			// Same behaviour as in DataObject->loadLazyFields
355
			$dataQuery->where("\"$dataClass\".\"ID\" = {$dataObject->ID}")->limit(1);
356
		}
357
	}
358
359
360
	/**
361
	 * Called by {@link SapphireTest} when the database is reset.
362
	 *
363
	 * @todo Reduce the coupling between this and SapphireTest, somehow.
364
	 */
365
	public static function on_db_reset() {
366
		// Drop all temporary tables
367
		$db = DB::get_conn();
368
		foreach(self::$archive_tables as $tableName) {
369
			if(method_exists($db, 'dropTable')) $db->dropTable($tableName);
370
			else $db->query("DROP TABLE \"$tableName\"");
371
		}
372
373
		// Remove references to them
374
		self::$archive_tables = array();
375
	}
376
377
	public function augmentDatabase() {
378
		$classTable = $this->owner->class;
379
380
		$isRootClass = ($this->owner->class == ClassInfo::baseDataClass($this->owner->class));
381
382
		// Build a list of suffixes whose tables need versioning
383
		$allSuffixes = array();
384
		foreach (Versioned::$versionableExtensions as $versionableExtension => $suffixes) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
385
			if ($this->owner->hasExtension($versionableExtension)) {
386
				$allSuffixes = array_merge($allSuffixes, (array)$suffixes);
387
				foreach ((array)$suffixes as $suffix) {
388
					$allSuffixes[$suffix] = $versionableExtension;
389
				}
390
			}
391
		}
392
393
		// Add the default table with an empty suffix to the list (table name = class name)
394
		array_push($allSuffixes,'');
395
396
		foreach ($allSuffixes as $key => $suffix) {
397
			// check that this is a valid suffix
398
			if (!is_int($key)) continue;
399
400
			if ($suffix) $table = "{$classTable}_$suffix";
401
			else $table = $classTable;
402
403
			if($fields = DataObject::database_fields($this->owner->class)) {
404
				$options = Config::inst()->get($this->owner->class, 'create_table_options', Config::FIRST_SET);
405
				$indexes = $this->owner->databaseIndexes();
406
				if ($suffix && ($ext = $this->owner->getExtensionInstance($allSuffixes[$suffix]))) {
407
					if (!$ext->isVersionedTable($table)) continue;
408
					$ext->setOwner($this->owner);
409
					$fields = $ext->fieldsInExtraTables($suffix);
410
					$ext->clearOwner();
411
					$indexes = $fields['indexes'];
412
					$fields = $fields['db'];
413
				}
414
415
				// Create tables for other stages
416
				foreach($this->stages as $stage) {
417
					// Extra tables for _Live, etc.
418
					// Change unique indexes to 'index'.  Versioned tables may run into unique indexing difficulties
419
					// otherwise.
420
					$indexes = $this->uniqueToIndex($indexes);
421
					if($stage != $this->defaultStage) {
422
						DB::require_table("{$table}_$stage", $fields, $indexes, false, $options);
423
					}
424
425
					// Version fields on each root table (including Stage)
426
					/*
0 ignored issues
show
Unused Code Comprehensibility introduced by
68% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
427
					if($isRootClass) {
428
						$stageTable = ($stage == $this->defaultStage) ? $table : "{$table}_$stage";
429
						$parts=Array('datatype'=>'int', 'precision'=>11, 'null'=>'not null', 'default'=>(int)0);
430
						$values=Array('type'=>'int', 'parts'=>$parts);
431
						DB::requireField($stageTable, 'Version', $values);
432
					}
433
					*/
434
				}
435
436
				if($isRootClass) {
437
					// Create table for all versions
438
					$versionFields = array_merge(
439
						Config::inst()->get('Versioned', 'db_for_versions_table'),
440
						(array)$fields
441
					);
442
443
					$versionIndexes = array_merge(
444
						Config::inst()->get('Versioned', 'indexes_for_versions_table'),
445
						(array)$indexes
446
					);
447
				} else {
448
					// Create fields for any tables of subclasses
449
					$versionFields = array_merge(
450
						array(
451
							"RecordID" => "Int",
452
							"Version" => "Int",
453
						),
454
						(array)$fields
455
					);
456
457
					//Unique indexes will not work on versioned tables, so we'll convert them to standard indexes:
458
					$indexes = $this->uniqueToIndex($indexes);
459
					$versionIndexes = array_merge(
460
						array(
461
							'RecordID_Version' => array('type' => 'unique', 'value' => '"RecordID","Version"'),
462
							'RecordID' => true,
463
							'Version' => true,
464
						),
465
						(array)$indexes
466
					);
467
				}
468
469
				if(DB::get_schema()->hasTable("{$table}_versions")) {
470
					// Fix data that lacks the uniqueness constraint (since this was added later and
471
					// bugs meant that the constraint was validated)
472
					$duplications = DB::query("SELECT MIN(\"ID\") AS \"ID\", \"RecordID\", \"Version\"
473
						FROM \"{$table}_versions\" GROUP BY \"RecordID\", \"Version\"
474
						HAVING COUNT(*) > 1");
475
476
					foreach($duplications as $dup) {
477
						DB::alteration_message("Removing {$table}_versions duplicate data for "
478
							."{$dup['RecordID']}/{$dup['Version']}" ,"deleted");
479
						DB::prepared_query(
480
							"DELETE FROM \"{$table}_versions\" WHERE \"RecordID\" = ?
481
							AND \"Version\" = ? AND \"ID\" != ?",
482
							array($dup['RecordID'], $dup['Version'], $dup['ID'])
483
						);
484
					}
485
486
					// Remove junk which has no data in parent classes. Only needs to run the following
487
					// when versioned data is spread over multiple tables
488
					if(!$isRootClass && ($versionedTables = ClassInfo::dataClassesFor($table))) {
489
490
						foreach($versionedTables as $child) {
491
							if($table === $child) break; // only need subclasses
492
493
							// Select all orphaned version records
494
							$orphanedQuery = SQLSelect::create()
495
								->selectField("\"{$table}_versions\".\"ID\"")
496
								->setFrom("\"{$table}_versions\"");
497
498
							// If we have a parent table limit orphaned records
499
							// to only those that exist in this
500
							if(DB::get_schema()->hasTable("{$child}_versions")) {
501
								$orphanedQuery
502
									->addLeftJoin(
503
										"{$child}_versions",
504
										"\"{$child}_versions\".\"RecordID\" = \"{$table}_versions\".\"RecordID\"
505
										AND \"{$child}_versions\".\"Version\" = \"{$table}_versions\".\"Version\""
506
									)
507
									->addWhere("\"{$child}_versions\".\"ID\" IS NULL");
508
							}
509
510
							$count = $orphanedQuery->count();
511
							if($count > 0) {
512
								DB::alteration_message("Removing {$count} orphaned versioned records", "deleted");
513
								$ids = $orphanedQuery->execute()->column();
514
								foreach($ids as $id) {
515
									DB::prepared_query(
516
										"DELETE FROM \"{$table}_versions\" WHERE \"ID\" = ?",
517
										array($id)
518
									);
519
								}
520
							}
521
						}
522
					}
523
				}
524
525
				DB::require_table("{$table}_versions", $versionFields, $versionIndexes, true, $options);
526
			} else {
527
				DB::dont_require_table("{$table}_versions");
528
				foreach($this->stages as $stage) {
529
					if($stage != $this->defaultStage) DB::dont_require_table("{$table}_$stage");
530
				}
531
			}
532
		}
533
	}
534
535
	/**
536
	 * Helper for augmentDatabase() to find unique indexes and convert them to non-unique
537
	 *
538
	 * @param array $indexes The indexes to convert
539
	 * @return array $indexes
540
	 */
541
	private function uniqueToIndex($indexes) {
542
		$unique_regex = '/unique/i';
543
		$results = array();
544
		foreach ($indexes as $key => $index) {
545
			$results[$key] = $index;
546
547
			// support string descriptors
548
			if (is_string($index)) {
549
				if (preg_match($unique_regex, $index)) {
550
					$results[$key] = preg_replace($unique_regex, 'index', $index);
551
				}
552
			}
553
554
			// canonical, array-based descriptors
555
			elseif (is_array($index)) {
556
				if (strtolower($index['type']) == 'unique') {
557
					$results[$key]['type'] = 'index';
558
				}
559
			}
560
		}
561
		return $results;
562
	}
563
564
	/**
565
	 * Generates a ($table)_version DB manipulation and injects it into the current $manipulation
566
	 *
567
	 * @param array $manipulation Source manipulation data
568
	 * @param string $table Name of table
569
	 * @param int $recordID ID of record to version
570
	 */
571
	protected function augmentWriteVersioned(&$manipulation, $table, $recordID) {
572
		$baseDataClass = ClassInfo::baseDataClass($table);
573
574
		// Set up a new entry in (table)_versions
575
		$newManipulation = array(
576
			"command" => "insert",
577
			"fields" => isset($manipulation[$table]['fields']) ? $manipulation[$table]['fields'] : null
578
		);
579
580
		// Add any extra, unchanged fields to the version record.
581
		$data = DB::prepared_query("SELECT * FROM \"$table\" WHERE \"ID\" = ?", array($recordID))->record();
582
583
		if ($data) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $data of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
584
			$fields = DataObject::database_fields($table);
585
586
			if (is_array($fields)) {
587
				$data = array_intersect_key($data, $fields);
588
589
				foreach ($data as $k => $v) {
590
					if (!isset($newManipulation['fields'][$k])) {
591
						$newManipulation['fields'][$k] = $v;
592
					}
593
				}
594
			}
595
		}
596
597
		// Ensure that the ID is instead written to the RecordID field
598
		$newManipulation['fields']['RecordID'] = $recordID;
599
		unset($newManipulation['fields']['ID']);
600
601
		// Generate next version ID to use
602
		$nextVersion = 0;
603
		if($recordID) {
604
			$nextVersion = DB::prepared_query("SELECT MAX(\"Version\") + 1
605
				FROM \"{$baseDataClass}_versions\" WHERE \"RecordID\" = ?",
606
				array($recordID)
607
			)->value();
608
		}
609
		$nextVersion = $nextVersion ?: 1;
610
611
		if($table === $baseDataClass) {
612
			// Write AuthorID for baseclass
613
			$userID = (Member::currentUser()) ? Member::currentUser()->ID : 0;
614
			$newManipulation['fields']['AuthorID'] = $userID;
615
616
			// Update main table version if not previously known
617
			$manipulation[$table]['fields']['Version'] = $nextVersion;
618
		}
619
620
		// Update _versions table manipulation
621
		$newManipulation['fields']['Version'] = $nextVersion;
622
		$manipulation["{$table}_versions"] = $newManipulation;
623
	}
624
625
	/**
626
	 * Rewrite the given manipulation to update the selected (non-default) stage
627
	 *
628
	 * @param array $manipulation Source manipulation data
629
	 * @param string $table Name of table
630
	 * @param int $recordID ID of record to version
631
	 */
632
	protected function augmentWriteStaged(&$manipulation, $table, $recordID) {
633
		// If the record has already been inserted in the (table), get rid of it.
634
		if($manipulation[$table]['command'] == 'insert') {
635
			DB::prepared_query(
636
				"DELETE FROM \"{$table}\" WHERE \"ID\" = ?",
637
				array($recordID)
638
			);
639
		}
640
641
		$newTable = $table . '_' . Versioned::current_stage();
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
642
		$manipulation[$newTable] = $manipulation[$table];
643
		unset($manipulation[$table]);
644
	}
645
646
647
	public function augmentWrite(&$manipulation) {
648
		// get Version number from base data table on write
649
		$version = null;
650
		$baseDataClass = ClassInfo::baseDataClass($this->owner->class);
651
		if(isset($manipulation[$baseDataClass]['fields'])) {
652
			if ($this->migratingVersion) {
653
				$manipulation[$baseDataClass]['fields']['Version'] = $this->migratingVersion;
654
			}
655
			if (isset($manipulation[$baseDataClass]['fields']['Version'])) {
656
				$version = $manipulation[$baseDataClass]['fields']['Version'];
657
			}
658
		}
659
660
		// Update all tables
661
		$tables = array_keys($manipulation);
662
		foreach($tables as $table) {
663
664
			// Make sure that the augmented write is being applied to a table that can be versioned
665
			if( !$this->canBeVersioned($table) ) {
666
				unset($manipulation[$table]);
667
				continue;
668
			}
669
670
			// Get ID field
671
			$id = $manipulation[$table]['id']
672
				? $manipulation[$table]['id']
673
				: $manipulation[$table]['fields']['ID'];
674
			if(!$id) {
675
				user_error("Couldn't find ID in " . var_export($manipulation[$table], true), E_USER_ERROR);
676
			}
677
678
			if($version < 0 || $this->_nextWriteWithoutVersion) {
679
				// Putting a Version of -1 is a signal to leave the version table alone, despite their being no version
680
				unset($manipulation[$table]['fields']['Version']);
681
			} elseif(empty($version)) {
682
				// If we haven't got a version #, then we're creating a new version.
683
				// Otherwise, we're just copying a version to another table
684
				$this->augmentWriteVersioned($manipulation, $table, $id);
685
			}
686
687
			// Remove "Version" column from subclasses of baseDataClass
688
			if(!$this->hasVersionField($table)) {
689
				unset($manipulation[$table]['fields']['Version']);
690
			}
691
692
			// Grab a version number - it should be the same across all tables.
693
			if(isset($manipulation[$table]['fields']['Version'])) {
694
				$thisVersion = $manipulation[$table]['fields']['Version'];
695
			}
696
697
			// If we're editing Live, then use (table)_Live instead of (table)
698
			if(
699
				Versioned::current_stage()
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
700
				&& Versioned::current_stage() != $this->defaultStage
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
701
				&& in_array(Versioned::current_stage(), $this->stages)
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
702
			) {
703
				$this->augmentWriteStaged($manipulation, $table, $id);
704
			}
705
		}
706
707
		// Clear the migration flag
708
		if($this->migratingVersion) {
709
			$this->migrateVersion(null);
710
		}
711
712
		// Add the new version # back into the data object, for accessing
713
		// after this write
714
		if(isset($thisVersion)) {
715
			$this->owner->Version = str_replace("'","", $thisVersion);
716
		}
717
	}
718
719
	/**
720
	 * Perform a write without affecting the version table.
721
	 * On objects without versioning.
722
	 *
723
	 * @return int The ID of the record
724
	 */
725
	public function writeWithoutVersion() {
726
		$this->_nextWriteWithoutVersion = true;
727
728
		return $this->owner->write();
729
	}
730
731
	/**
732
	 *
733
	 */
734
	public function onAfterWrite() {
735
		$this->_nextWriteWithoutVersion = false;
736
	}
737
738
	/**
739
	 * If a write was skipped, then we need to ensure that we don't leave a
740
	 * migrateVersion() value lying around for the next write.
741
	 *
742
	 *
743
	 */
744
	public function onAfterSkippedWrite() {
745
		$this->migrateVersion(null);
746
	}
747
748
	/**
749
	 * Determine if a table is supporting the Versioned extensions (e.g.
750
	 * $table_versions does exists).
751
	 *
752
	 * @param string $table Table name
753
	 * @return boolean
754
	 */
755
	public function canBeVersioned($table) {
756
		return ClassInfo::exists($table)
757
			&& is_subclass_of($table, 'DataObject')
758
			&& DataObject::has_own_table($table);
759
	}
760
761
	/**
762
	 * Check if a certain table has the 'Version' field.
763
	 *
764
	 * @param string $table Table name
765
	 *
766
	 * @return boolean Returns false if the field isn't in the table, true otherwise
767
	 */
768
	public function hasVersionField($table) {
769
		$rPos = strrpos($table,'_');
770
771
		if(($rPos !== false) && in_array(substr($table,$rPos), $this->stages)) {
772
			$tableWithoutStage = substr($table,0,$rPos);
773
		} else {
774
			$tableWithoutStage = $table;
775
		}
776
777
		return ('DataObject' == get_parent_class($tableWithoutStage));
778
	}
779
780
	/**
781
	 * @param string $table
782
	 *
783
	 * @return string
784
	 */
785
	public function extendWithSuffix($table) {
786
		foreach (Versioned::$versionableExtensions as $versionableExtension => $suffixes) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
787
			if ($this->owner->hasExtension($versionableExtension)) {
788
				$ext = $this->owner->getExtensionInstance($versionableExtension);
789
				$ext->setOwner($this->owner);
790
				$table = $ext->extendWithSuffix($table);
791
				$ext->clearOwner();
792
			}
793
		}
794
795
		return $table;
796
	}
797
798
	/**
799
	 * Get the latest published DataObject.
800
	 *
801
	 * @return DataObject
802
	 */
803
	public function latestPublished() {
804
		// Get the root data object class - this will have the version field
805
		$table1 = $this->owner->class;
806
		while( ($p = get_parent_class($table1)) != "DataObject") $table1 = $p;
807
808
		$table2 = $table1 . "_$this->liveStage";
809
810
		return DB::prepared_query("SELECT \"$table1\".\"Version\" = \"$table2\".\"Version\" FROM \"$table1\"
811
			 INNER JOIN \"$table2\" ON \"$table1\".\"ID\" = \"$table2\".\"ID\"
812
			 WHERE \"$table1\".\"ID\" = ?",
813
			array($this->owner->ID)
814
		)->value();
815
	}
816
817
	/**
818
	 * Move a database record from one stage to the other.
819
	 *
820
	 * @param int|string $fromStage Place to copy from.  Can be either a stage name or a version number.
821
	 * @param string $toStage Place to copy to.  Must be a stage name.
822
	 * @param bool $createNewVersion Set this to true to create a new version number.
823
	 *  By default, the existing version number will be copied over.
824
	 */
825
	public function publish($fromStage, $toStage, $createNewVersion = false) {
826
		$this->owner->extend('onBeforeVersionedPublish', $fromStage, $toStage, $createNewVersion);
827
828
		$baseClass = ClassInfo::baseDataClass($this->owner->class);
829
		$extTable = $this->extendWithSuffix($baseClass);
830
831
		/** @var Versioned|DataObject $from */
832
		if(is_numeric($fromStage)) {
833
			$from = Versioned::get_version($baseClass, $this->owner->ID, $fromStage);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
834
		} else {
835
			$this->owner->flushCache();
836
			$from = Versioned::get_one_by_stage($baseClass, $fromStage, array(
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
837
				"\"{$baseClass}\".\"ID\" = ?" => $this->owner->ID
838
			));
839
		}
840
		if(!$from) {
841
			user_error("Can't find {$this->owner->class}/{$this->owner->ID} in stage {$fromStage}", E_USER_WARNING);
842
			return;
843
		}
844
845
		// Set version of new record
846
		$from->forceChange();
847
		if($createNewVersion) {
848
			// Clear version to be automatically created on write
849
			$from->Version = null;
0 ignored issues
show
Documentation introduced by
The property Version does not exist on object<DataObject>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
850
		} else {
851
			$from->migrateVersion($from->Version);
852
853
			// Mark this version as having been published at some stage
854
			$publisherID = isset(Member::currentUser()->ID) ? Member::currentUser()->ID : 0;
855
			DB::prepared_query("UPDATE \"{$extTable}_versions\"
856
				SET \"WasPublished\" = ?, \"PublisherID\" = ?
857
				WHERE \"RecordID\" = ? AND \"Version\" = ?",
858
				array(1, $publisherID, $from->ID, $from->Version)
859
			);
860
		}
861
862
		// Change to new stage, write, and revert state
863
		$oldMode = Versioned::get_reading_mode();
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
864
		Versioned::reading_stage($toStage);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
865
866
		$conn = DB::get_conn();
867
		if(method_exists($conn, 'allowPrimaryKeyEditing')) {
868
			$conn->allowPrimaryKeyEditing($baseClass, true);
869
			$from->write();
870
			$conn->allowPrimaryKeyEditing($baseClass, false);
871
		} else {
872
			$from->write();
873
		}
874
875
		$from->destroy();
876
877
		Versioned::set_reading_mode($oldMode);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
878
	}
879
880
	/**
881
	 * Set the migrating version.
882
	 *
883
	 * @param string $version The version.
884
	 */
885
	public function migrateVersion($version) {
886
		$this->migratingVersion = $version;
887
	}
888
889
	/**
890
	 * Compare two stages to see if they're different.
891
	 *
892
	 * Only checks the version numbers, not the actual content.
893
	 *
894
	 * @param string $stage1 The first stage to check.
895
	 * @param string $stage2
896
	 */
897
	public function stagesDiffer($stage1, $stage2) {
898
		$table1 = $this->baseTable($stage1);
899
		$table2 = $this->baseTable($stage2);
900
901
		if(!is_numeric($this->owner->ID)) {
902
			return true;
903
		}
904
905
		// We test for equality - if one of the versions doesn't exist, this
906
		// will be false.
907
908
		// TODO: DB Abstraction: if statement here:
909
		$stagesAreEqual = DB::prepared_query(
910
			"SELECT CASE WHEN \"$table1\".\"Version\"=\"$table2\".\"Version\" THEN 1 ELSE 0 END
911
			 FROM \"$table1\" INNER JOIN \"$table2\" ON \"$table1\".\"ID\" = \"$table2\".\"ID\"
912
			 AND \"$table1\".\"ID\" = ?",
913
			array($this->owner->ID)
914
		)->value();
915
916
		return !$stagesAreEqual;
917
	}
918
919
	/**
920
	 * @param string $filter
921
	 * @param string $sort
922
	 * @param string $limit
923
	 * @param string $join Deprecated, use leftJoin($table, $joinClause) instead
924
	 * @param string $having
925
	 */
926
	public function Versions($filter = "", $sort = "", $limit = "", $join = "", $having = "") {
927
		return $this->allVersions($filter, $sort, $limit, $join, $having);
928
	}
929
930
	/**
931
	 * Return a list of all the versions available.
932
	 *
933
	 * @param  string $filter
934
	 * @param  string $sort
935
	 * @param  string $limit
936
	 * @param  string $join   Deprecated, use leftJoin($table, $joinClause) instead
937
	 * @param  string $having
938
	 */
939
	public function allVersions($filter = "", $sort = "", $limit = "", $join = "", $having = "") {
940
		// Make sure the table names are not postfixed (e.g. _Live)
941
		$oldMode = self::get_reading_mode();
942
		self::reading_stage('Stage');
943
944
		$list = DataObject::get(get_class($this->owner), $filter, $sort, $join, $limit);
945
		if($having) $having = $list->having($having);
946
947
		$query = $list->dataQuery()->query();
948
949
		foreach($query->getFrom() as $table => $tableJoin) {
950
			if(is_string($tableJoin) && $tableJoin[0] == '"') {
951
				$baseTable = str_replace('"','',$tableJoin);
952
			} elseif(is_string($tableJoin) && substr($tableJoin,0,5) != 'INNER') {
953
				$query->setFrom(array(
954
					$table => "LEFT JOIN \"$table\" ON \"$table\".\"RecordID\"=\"{$baseTable}_versions\".\"RecordID\""
0 ignored issues
show
Bug introduced by
The variable $baseTable does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
955
						. " AND \"$table\".\"Version\" = \"{$baseTable}_versions\".\"Version\""
956
				));
957
			}
958
			$query->renameTable($table, $table . '_versions');
959
		}
960
961
		// Add all <basetable>_versions columns
962
		foreach(Config::inst()->get('Versioned', 'db_for_versions_table') as $name => $type) {
0 ignored issues
show
Bug introduced by
The expression \Config::inst()->get('Ve...db_for_versions_table') of type array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
963
			$query->selectField(sprintf('"%s_versions"."%s"', $baseTable, $name), $name);
964
		}
965
966
		$query->addWhere(array(
967
			"\"{$baseTable}_versions\".\"RecordID\" = ?" => $this->owner->ID
968
		));
969
		$query->setOrderBy(($sort) ? $sort
970
			: "\"{$baseTable}_versions\".\"LastEdited\" DESC, \"{$baseTable}_versions\".\"Version\" DESC");
971
972
		$records = $query->execute();
973
		$versions = new ArrayList();
974
975
		foreach($records as $record) {
976
			$versions->push(new Versioned_Version($record));
977
		}
978
979
		Versioned::set_reading_mode($oldMode);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
980
		return $versions;
981
	}
982
983
	/**
984
	 * Compare two version, and return the diff between them.
985
	 *
986
	 * @param string $from The version to compare from.
987
	 * @param string $to The version to compare to.
988
	 *
989
	 * @return DataObject
990
	 */
991
	public function compareVersions($from, $to) {
992
		$fromRecord = Versioned::get_version($this->owner->class, $this->owner->ID, $from);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
993
		$toRecord = Versioned::get_version($this->owner->class, $this->owner->ID, $to);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
994
995
		$diff = new DataDifferencer($fromRecord, $toRecord);
0 ignored issues
show
Bug introduced by
It seems like $toRecord defined by \Versioned::get_version(... $this->owner->ID, $to) on line 993 can be null; however, DataDifferencer::__construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
996
997
		return $diff->diffedData();
998
	}
999
1000
	/**
1001
	 * Return the base table - the class that directly extends DataObject.
1002
	 *
1003
	 * @return string
1004
	 */
1005
	public function baseTable($stage = null) {
1006
		$tableClasses = ClassInfo::dataClassesFor($this->owner->class);
1007
		$baseClass = array_shift($tableClasses);
1008
1009
		if(!$stage || $stage == $this->defaultStage) {
1010
			return $baseClass;
1011
		}
1012
1013
		return $baseClass . "_$stage";
1014
	}
1015
1016
	//-----------------------------------------------------------------------------------------------//
1017
1018
1019
	/**
1020
	 * Choose the stage the site is currently on.
1021
	 *
1022
	 * If $_GET['stage'] is set, then it will use that stage, and store it in
1023
	 * the session.
1024
	 *
1025
	 * if $_GET['archiveDate'] is set, it will use that date, and store it in
1026
	 * the session.
1027
	 *
1028
	 * If neither of these are set, it checks the session, otherwise the stage
1029
	 * is set to 'Live'.
1030
	 *
1031
	 * @param Session $session Optional session within which to store the resulting stage
1032
	 */
1033
	public static function choose_site_stage($session = null) {
0 ignored issues
show
Coding Style introduced by
choose_site_stage uses the super-global variable $_GET which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
1034
		// Check any pre-existing session mode
1035
		$preexistingMode = $session
1036
			? $session->inst_get('readingMode')
1037
			: Session::get('readingMode');
1038
1039
		// Determine the reading mode
1040
		if(isset($_GET['stage'])) {
1041
			$stage = ucfirst(strtolower($_GET['stage']));
1042
			if(!in_array($stage, array('Stage', 'Live'))) $stage = 'Live';
1043
			$mode = 'Stage.' . $stage;
1044
		} elseif (isset($_GET['archiveDate']) && strtotime($_GET['archiveDate'])) {
1045
			$mode = 'Archive.' . $_GET['archiveDate'];
1046
		} elseif($preexistingMode) {
1047
			$mode = $preexistingMode;
1048
		} else {
1049
			$mode = self::DEFAULT_MODE;
1050
		}
1051
1052
		// Save reading mode
1053
		Versioned::set_reading_mode($mode);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1054
1055
		// Try not to store the mode in the session if not needed
1056
		if(($preexistingMode && $preexistingMode !== $mode)
1057
			|| (!$preexistingMode && $mode !== self::DEFAULT_MODE)
1058
		) {
1059
			if($session) {
1060
				$session->inst_set('readingMode', $mode);
1061
			} else {
1062
				Session::set('readingMode', $mode);
1063
			}
1064
		}
1065
1066
		if(!headers_sent() && !Director::is_cli()) {
1067
			if(Versioned::current_stage() == 'Live') {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1068
				// clear the cookie if it's set
1069
				if(Cookie::get('bypassStaticCache')) {
0 ignored issues
show
Bug Best Practice introduced by
The expression \Cookie::get('bypassStaticCache') of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1070
					Cookie::force_expiry('bypassStaticCache', null, null, false, true /* httponly */);
1071
				}
1072
			} else {
1073
				// set the cookie if it's cleared
1074
				if(!Cookie::get('bypassStaticCache')) {
0 ignored issues
show
Bug Best Practice introduced by
The expression \Cookie::get('bypassStaticCache') of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1075
					Cookie::set('bypassStaticCache', '1', 0, null, null, false, true /* httponly */);
1076
				}
1077
			}
1078
		}
1079
	}
1080
1081
	/**
1082
	 * Set the current reading mode.
1083
	 *
1084
	 * @param string $mode
1085
	 */
1086
	public static function set_reading_mode($mode) {
1087
		Versioned::$reading_mode = $mode;
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1088
	}
1089
1090
	/**
1091
	 * Get the current reading mode.
1092
	 *
1093
	 * @return string
1094
	 */
1095
	public static function get_reading_mode() {
1096
		return Versioned::$reading_mode;
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1097
	}
1098
1099
	/**
1100
	 * Get the name of the 'live' stage.
1101
	 *
1102
	 * @return string
1103
	 */
1104
	public static function get_live_stage() {
1105
		return "Live";
1106
	}
1107
1108
	/**
1109
	 * Get the current reading stage.
1110
	 *
1111
	 * @return string
1112
	 */
1113
	public static function current_stage() {
1114
		$parts = explode('.', Versioned::get_reading_mode());
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1115
1116
		if($parts[0] == 'Stage') {
1117
			return $parts[1];
1118
		}
1119
	}
1120
1121
	/**
1122
	 * Get the current archive date.
1123
	 *
1124
	 * @return string
1125
	 */
1126
	public static function current_archived_date() {
1127
		$parts = explode('.', Versioned::get_reading_mode());
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1128
		if($parts[0] == 'Archive') return $parts[1];
1129
	}
1130
1131
	/**
1132
	 * Set the reading stage.
1133
	 *
1134
	 * @param string $stage New reading stage.
1135
	 */
1136
	public static function reading_stage($stage) {
1137
		Versioned::set_reading_mode('Stage.' . $stage);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1138
	}
1139
1140
	/**
1141
	 * Set the reading archive date.
1142
	 *
1143
	 * @param string $date New reading archived date.
1144
	 */
1145
	public static function reading_archived_date($date) {
1146
		Versioned::set_reading_mode('Archive.' . $date);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1147
	}
1148
1149
1150
	/**
1151
	 * Get a singleton instance of a class in the given stage.
1152
	 *
1153
	 * @param string $class The name of the class.
1154
	 * @param string $stage The name of the stage.
1155
	 * @param string $filter A filter to be inserted into the WHERE clause.
1156
	 * @param boolean $cache Use caching.
1157
	 * @param string $orderby A sort expression to be inserted into the ORDER BY clause.
0 ignored issues
show
Bug introduced by
There is no parameter named $orderby. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
1158
	 *
1159
	 * @return DataObject
1160
	 */
1161
	public static function get_one_by_stage($class, $stage, $filter = '', $cache = true, $sort = '') {
0 ignored issues
show
Unused Code introduced by
The parameter $cache is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1162
		// TODO: No identity cache operating
1163
		$items = self::get_by_stage($class, $stage, $filter, $sort, null, 1);
1164
1165
		return $items->First();
1166
	}
1167
1168
	/**
1169
	 * Gets the current version number of a specific record.
1170
	 *
1171
	 * @param string $class
1172
	 * @param string $stage
1173
	 * @param int $id
1174
	 * @param boolean $cache
1175
	 *
1176
	 * @return int
1177
	 */
1178
	public static function get_versionnumber_by_stage($class, $stage, $id, $cache = true) {
1179
		$baseClass = ClassInfo::baseDataClass($class);
1180
		$stageTable = ($stage == 'Stage') ? $baseClass : "{$baseClass}_{$stage}";
1181
1182
		// cached call
1183
		if($cache && isset(self::$cache_versionnumber[$baseClass][$stage][$id])) {
1184
			return self::$cache_versionnumber[$baseClass][$stage][$id];
1185
		}
1186
1187
		// get version as performance-optimized SQL query (gets called for each page in the sitetree)
1188
		$version = DB::prepared_query(
1189
			"SELECT \"Version\" FROM \"$stageTable\" WHERE \"ID\" = ?",
1190
			array($id)
1191
		)->value();
1192
1193
		// cache value (if required)
1194
		if($cache) {
1195
			if(!isset(self::$cache_versionnumber[$baseClass])) {
1196
				self::$cache_versionnumber[$baseClass] = array();
1197
			}
1198
1199
			if(!isset(self::$cache_versionnumber[$baseClass][$stage])) {
1200
				self::$cache_versionnumber[$baseClass][$stage] = array();
1201
			}
1202
1203
			self::$cache_versionnumber[$baseClass][$stage][$id] = $version;
1204
		}
1205
1206
		return $version;
1207
	}
1208
1209
	/**
1210
	 * Pre-populate the cache for Versioned::get_versionnumber_by_stage() for
1211
	 * a list of record IDs, for more efficient database querying.  If $idList
1212
	 * is null, then every page will be pre-cached.
1213
	 *
1214
	 * @param string $class
1215
	 * @param string $stage
1216
	 * @param array $idList
1217
	 */
1218
	public static function prepopulate_versionnumber_cache($class, $stage, $idList = null) {
1219
		if (!Config::inst()->get('Versioned', 'prepopulate_versionnumber_cache')) {
1220
			return;
1221
		}
1222
		$filter = "";
1223
		$parameters = array();
1224
		if($idList) {
1225
			// Validate the ID list
1226
			foreach($idList as $id) {
1227
				if(!is_numeric($id)) {
1228
					user_error("Bad ID passed to Versioned::prepopulate_versionnumber_cache() in \$idList: " . $id,
1229
					E_USER_ERROR);
1230
				}
1231
			}
1232
			$filter = 'WHERE "ID" IN ('.DB::placeholders($idList).')';
1233
			$parameters = $idList;
1234
		}
1235
1236
		$baseClass = ClassInfo::baseDataClass($class);
1237
		$stageTable = ($stage == 'Stage') ? $baseClass : "{$baseClass}_{$stage}";
1238
1239
		$versions = DB::prepared_query("SELECT \"ID\", \"Version\" FROM \"$stageTable\" $filter", $parameters)->map();
1240
1241
		foreach($versions as $id => $version) {
1242
			self::$cache_versionnumber[$baseClass][$stage][$id] = $version;
1243
		}
1244
	}
1245
1246
	/**
1247
	 * Get a set of class instances by the given stage.
1248
	 *
1249
	 * @param string $class The name of the class.
1250
	 * @param string $stage The name of the stage.
1251
	 * @param string $filter A filter to be inserted into the WHERE clause.
1252
	 * @param string $sort A sort expression to be inserted into the ORDER BY clause.
1253
	 * @param string $join Deprecated, use leftJoin($table, $joinClause) instead
1254
	 * @param int $limit A limit on the number of records returned from the database.
1255
	 * @param string $containerClass The container class for the result set (default is DataList)
1256
	 *
1257
	 * @return DataList A modified DataList designated to the specified stage
1258
	 */
1259
	public static function get_by_stage($class, $stage, $filter = '', $sort = '', $join = '', $limit = '',
1260
			$containerClass = 'DataList') {
1261
1262
		$result = DataObject::get($class, $filter, $sort, $join, $limit, $containerClass);
1263
		return $result->setDataQueryParam(array(
1264
			'Versioned.mode' => 'stage',
1265
			'Versioned.stage' => $stage
1266
		));
1267
	}
1268
1269
	/**
1270
	 * @param string $stage
1271
	 *
1272
	 * @return int
1273
	 */
1274
	public function deleteFromStage($stage) {
1275
		$oldMode = Versioned::get_reading_mode();
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1276
		Versioned::reading_stage($stage);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1277
		$clone = clone $this->owner;
1278
		$result = $clone->delete();
1279
		Versioned::set_reading_mode($oldMode);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1280
1281
		// Fix the version number cache (in case you go delete from stage and then check ExistsOnLive)
1282
		$baseClass = ClassInfo::baseDataClass($this->owner->class);
1283
		self::$cache_versionnumber[$baseClass][$stage][$this->owner->ID] = null;
1284
1285
		return $result;
1286
	}
1287
1288
	/**
1289
	 * @param string $stage
1290
	 * @param boolean $forceInsert
1291
	 */
1292
	public function writeToStage($stage, $forceInsert = false) {
1293
		$oldMode = Versioned::get_reading_mode();
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1294
		Versioned::reading_stage($stage);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1295
1296
		$this->owner->forceChange();
1297
		$result = $this->owner->write(false, $forceInsert);
1298
		Versioned::set_reading_mode($oldMode);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1299
1300
		return $result;
1301
	}
1302
1303
	/**
1304
	 * Roll the draft version of this page to match the published page.
1305
	 * Caution: Doesn't overwrite the object properties with the rolled back version.
1306
	 *
1307
	 * @param int $version Either the string 'Live' or a version number
1308
	 */
1309
	public function doRollbackTo($version) {
1310
		$this->owner->extend('onBeforeRollback', $version);
1311
		$this->publish($version, "Stage", true);
1312
1313
		$this->owner->writeWithoutVersion();
1314
1315
		$this->owner->extend('onAfterRollback', $version);
1316
	}
1317
1318
	/**
1319
	 * Return the latest version of the given page.
1320
	 *
1321
	 * @return DataObject
1322
	 */
1323
	public static function get_latest_version($class, $id) {
1324
		$baseClass = ClassInfo::baseDataClass($class);
1325
		$list = DataList::create($baseClass)
1326
			->where("\"$baseClass\".\"RecordID\" = $id")
1327
			->setDataQueryParam("Versioned.mode", "latest_versions");
1328
1329
		return $list->First();
1330
	}
1331
1332
	/**
1333
	 * Returns whether the current record is the latest one.
1334
	 *
1335
	 * @todo Performance - could do this directly via SQL.
1336
	 *
1337
	 * @see get_latest_version()
1338
	 * @see latestPublished
1339
	 *
1340
	 * @return boolean
1341
	 */
1342
	public function isLatestVersion() {
1343
		$version = self::get_latest_version($this->owner->class, $this->owner->ID);
1344
1345
		return ($version->Version == $this->owner->Version);
1346
	}
1347
1348
	/**
1349
	 * Return the equivalent of a DataList::create() call, querying the latest
1350
	 * version of each page stored in the (class)_versions tables.
1351
	 *
1352
	 * In particular, this will query deleted records as well as active ones.
1353
	 *
1354
	 * @param string $class
1355
	 * @param string $filter
1356
	 * @param string $sort
1357
	 */
1358
	public static function get_including_deleted($class, $filter = "", $sort = "") {
1359
		$list = DataList::create($class)
1360
			->where($filter)
1361
			->sort($sort)
1362
			->setDataQueryParam("Versioned.mode", "latest_versions");
1363
1364
		return $list;
1365
	}
1366
1367
	/**
1368
	 * Return the specific version of the given id.
1369
	 *
1370
	 * Caution: The record is retrieved as a DataObject, but saving back
1371
	 * modifications via write() will create a new version, rather than
1372
	 * modifying the existing one.
1373
	 *
1374
	 * @param string $class
1375
	 * @param int $id
1376
	 * @param int $version
1377
	 *
1378
	 * @return DataObject
1379
	 */
1380
	public static function get_version($class, $id, $version) {
1381
		$baseClass = ClassInfo::baseDataClass($class);
1382
		$list = DataList::create($baseClass)
1383
			->where("\"$baseClass\".\"RecordID\" = $id")
1384
			->where("\"$baseClass\".\"Version\" = " . (int)$version)
1385
			->setDataQueryParam("Versioned.mode", 'all_versions');
1386
1387
		return $list->First();
1388
	}
1389
1390
	/**
1391
	 * Return a list of all versions for a given id.
1392
	 *
1393
	 * @param string $class
1394
	 * @param int $id
1395
	 *
1396
	 * @return DataList
1397
	 */
1398
	public static function get_all_versions($class, $id) {
1399
		$baseClass = ClassInfo::baseDataClass($class);
1400
		$list = DataList::create($class)
1401
			->where("\"$baseClass\".\"RecordID\" = $id")
1402
			->setDataQueryParam('Versioned.mode', 'all_versions');
1403
1404
		return $list;
1405
	}
1406
1407
	/**
1408
	 * @param array $labels
1409
	 */
1410
	public function updateFieldLabels(&$labels) {
1411
		$labels['Versions'] = _t('Versioned.has_many_Versions', 'Versions', 'Past Versions of this page');
1412
	}
1413
1414
	/**
1415
	 * @param FieldList
1416
	 */
1417
	public function updateCMSFields(FieldList $fields) {
1418
		// remove the version field from the CMS as this should be left
1419
		// entirely up to the extension (not the cms user).
1420
		$fields->removeByName('Version');
1421
	}
1422
1423
	/**
1424
	 * Ensure version ID is reset to 0 on duplicate
1425
	 *
1426
	 * @param DataObject $source Record this was duplicated from
1427
	 * @param bool $doWrite
1428
	 */
1429
	public function onBeforeDuplicate($source, $doWrite) {
0 ignored issues
show
Unused Code introduced by
The parameter $source is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $doWrite is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1430
		$this->owner->Version = 0;
1431
	}
1432
1433
	public function flushCache() {
1434
		self::$cache_versionnumber = array();
1435
	}
1436
1437
	/**
1438
	 * Return a piece of text to keep DataObject cache keys appropriately specific.
1439
	 *
1440
	 * @return string
1441
	 */
1442
	public function cacheKeyComponent() {
1443
		return 'versionedmode-'.self::get_reading_mode();
1444
	}
1445
1446
	/**
1447
	 * Returns an array of possible stages.
1448
	 *
1449
	 * @return array
1450
	 */
1451
	public function getVersionedStages() {
1452
		return $this->stages;
1453
	}
1454
1455
	/**
1456
	 * @return string
1457
	 */
1458
	public function getDefaultStage() {
1459
		return $this->defaultStage;
1460
	}
1461
1462
	public static function get_template_global_variables() {
1463
		return array(
1464
			'CurrentReadingMode' => 'get_reading_mode'
1465
		);
1466
	}
1467
}
1468
1469
/**
1470
 * Represents a single version of a record.
1471
 *
1472
 * @package framework
1473
 * @subpackage model
1474
 *
1475
 * @see Versioned
1476
 */
1477
class Versioned_Version extends ViewableData {
1478
	/**
1479
	 * @var array
1480
	 */
1481
	protected $record;
1482
1483
	/**
1484
	 * @var DataObject
1485
	 */
1486
	protected $object;
1487
1488
	public function __construct($record) {
1489
		$this->record = $record;
1490
		$record['ID'] = $record['RecordID'];
1491
		$className = $record['ClassName'];
1492
1493
		$this->object = ClassInfo::exists($className) ? new $className($record) : new DataObject($record);
1494
		$this->failover = $this->object;
1495
1496
		parent::__construct();
1497
	}
1498
1499
	/**
1500
	 * @return string
1501
	 */
1502
	public function PublishedClass() {
1503
		return $this->record['WasPublished'] ? 'published' : 'internal';
1504
	}
1505
1506
	/**
1507
	 * @return Member
1508
	 */
1509
	public function Author() {
1510
		return Member::get()->byId($this->record['AuthorID']);
1511
	}
1512
1513
	/**
1514
	 * @return Member
1515
	 */
1516
	public function Publisher() {
1517
		if (!$this->record['WasPublished']) {
1518
			return null;
1519
		}
1520
1521
		return Member::get()->byId($this->record['PublisherID']);
1522
	}
1523
1524
	/**
1525
	 * @return boolean
1526
	 */
1527
	public function Published() {
1528
		return !empty($this->record['WasPublished']);
1529
	}
1530
1531
	/**
1532
	 * Copied from DataObject to allow access via dot notation.
1533
	 */
1534
	public function relField($fieldName) {
1535
		$component = $this;
1536
1537
		if(strpos($fieldName, '.') !== false) {
1538
			$parts = explode('.', $fieldName);
1539
			$fieldName = array_pop($parts);
1540
1541
			// Traverse dot syntax
1542
			foreach($parts as $relation) {
1543
				if($component instanceof SS_List) {
1544
					if(method_exists($component,$relation)) {
1545
						$component = $component->$relation();
1546
					} else {
1547
						$component = $component->relation($relation);
1548
					}
1549
				} else {
1550
					$component = $component->$relation();
1551
				}
1552
			}
1553
		}
1554
1555
		// Unlike has-one's, these "relations" can return false
1556
		if($component) {
1557
			if ($component->hasMethod($fieldName)) {
1558
				return $component->$fieldName();
1559
			}
1560
1561
			return $component->$fieldName;
1562
		}
1563
	}
1564
}
1565