Completed
Pull Request — master (#5143)
by Damian
10:16
created

Versioned::isOnDraft()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 12
rs 9.4285
cc 2
eloc 8
nc 2
nop 0
1
<?php
2
3
// namespace SilverStripe\Framework\Model\Versioning
4
5
/**
6
 * The Versioned extension allows your DataObjects to have several versions,
7
 * allowing you to rollback changes and view history. An example of this is
8
 * the pages used in the CMS.
9
 *
10
 * @property int $Version
11
 * @property DataObject|Versioned $owner
12
 *
13
 * @package framework
14
 * @subpackage model
15
 */
16
class Versioned extends DataExtension implements TemplateGlobalProvider {
17
	/**
18
	 * An array of possible stages.
19
	 * @var array
20
	 */
21
	protected $stages;
22
23
	/**
24
	 * The 'default' stage.
25
	 * @var string
26
	 */
27
	protected $defaultStage;
28
29
	/**
30
	 * The 'live' stage.
31
	 * @var string
32
	 */
33
	protected $liveStage;
34
35
	/**
36
	 * The default reading mode
37
	 */
38
	const DEFAULT_MODE = 'Stage.Live';
39
40
	/**
41
	 * A version that a DataObject should be when it is 'migrating',
42
	 * that is, when it is in the process of moving from one stage to another.
43
	 * @var string
44
	 */
45
	public $migratingVersion;
46
47
	/**
48
	 * A cache used by get_versionnumber_by_stage().
49
	 * Clear through {@link flushCache()}.
50
	 *
51
	 * @var array
52
	 */
53
	protected static $cache_versionnumber;
54
55
	/**
56
	 * @var string
57
	 */
58
	protected static $reading_mode = null;
59
60
	/**
61
	 * @var Boolean Flag which is temporarily changed during the write() process
62
	 * to influence augmentWrite() behaviour. If set to TRUE, no new version will be created
63
	 * for the following write. Needs to be public as other classes introspect this state
64
	 * during the write process in order to adapt to this versioning behaviour.
65
	 */
66
	public $_nextWriteWithoutVersion = false;
67
68
	/**
69
	 * Additional database columns for the new
70
	 * "_versions" table. Used in {@link augmentDatabase()}
71
	 * and all Versioned calls extending or creating
72
	 * SELECT statements.
73
	 *
74
	 * @var array $db_for_versions_table
75
	 */
76
	private static $db_for_versions_table = array(
77
		"RecordID" => "Int",
78
		"Version" => "Int",
79
		"WasPublished" => "Boolean",
80
		"AuthorID" => "Int",
81
		"PublisherID" => "Int"
82
	);
83
84
	/**
85
	 * @var array
86
	 */
87
	private static $db = array(
88
		'Version' => 'Int'
89
	);
90
91
	/**
92
	 * Used to enable or disable the prepopulation of the version number cache.
93
	 * Defaults to true.
94
	 *
95
	 * @var boolean
96
	 */
97
	private static $prepopulate_versionnumber_cache = true;
98
99
	/**
100
	 * Keep track of the archive tables that have been created.
101
	 *
102
	 * @var array
103
	 */
104
	private static $archive_tables = array();
105
106
	/**
107
	 * Additional database indexes for the new
108
	 * "_versions" table. Used in {@link augmentDatabase()}.
109
	 *
110
	 * @var array $indexes_for_versions_table
111
	 */
112
	private static $indexes_for_versions_table = array(
113
		'RecordID_Version' => '("RecordID","Version")',
114
		'RecordID' => true,
115
		'Version' => true,
116
		'AuthorID' => true,
117
		'PublisherID' => true,
118
	);
119
120
121
	/**
122
	 * An array of DataObject extensions that may require versioning for extra tables
123
	 * The array value is a set of suffixes to form these table names, assuming a preceding '_'.
124
	 * E.g. if Extension1 creates a new table 'Class_suffix1'
125
	 * and Extension2 the tables 'Class_suffix2' and 'Class_suffix3':
126
	 *
127
	 * 	$versionableExtensions = array(
128
	 * 		'Extension1' => 'suffix1',
129
	 * 		'Extension2' => array('suffix2', 'suffix3'),
130
	 * 	);
131
	 *
132
	 * This can also be manipulated by updating the current loaded config
133
	 *
134
	 * SiteTree:
135
	 *   versionableExtensions:
136
	 *     - Extension1:
137
	 *       - suffix1
138
	 *       - suffix2
139
	 *     - Extension2:
140
	 *       - suffix1
141
	 *       - suffix2
142
	 *
143
	 * or programatically:
144
	 *
145
	 *  Config::inst()->update($this->owner->class, 'versionableExtensions',
146
	 *  array('Extension1' => 'suffix1', 'Extension2' => array('suffix2', 'suffix3')));
147
	 *
148
	 *
149
	 * Make sure your extension has a static $enabled-property that determines if it is
150
	 * processed by Versioned.
151
	 *
152
	 * @var array
153
	 */
154
	protected static $versionableExtensions = array('Translatable' => 'lang');
155
156
	/**
157
	 * Permissions necessary to view records outside of the live stage (e.g. archive / draft stage).
158
	 *
159
	 * @config
160
	 * @var array
161
	 */
162
	private static $non_live_permissions = array('CMS_ACCESS_LeftAndMain', 'CMS_ACCESS_CMSMain', 'VIEW_DRAFT_CONTENT');
163
164
	/**
165
	 * List of relationships on this object that are "owned" by this object.
166
	 * Owership in the context of versioned objects is a relationship where
167
	 * the publishing of owning objects requires the publishing of owned objects.
168
	 *
169
	 * E.g. A page owns a set of banners, as in order for the page to be published, all
170
	 * banners on this page must also be published for it to be visible.
171
	 *
172
	 * Typically any object and its owned objects should be visible in the same edit view.
173
	 * E.g. a page and {@see GridField} of banners.
174
	 *
175
	 * Page hierarchy is typically not considered an ownership relationship.
176
	 *
177
	 * Ownership is recursive; If A owns B and B owns C then A owns C.
178
	 *
179
	 * @config
180
	 * @var array List of has_many or many_many relationships owned by this object.
181
	 */
182
	private static $owns = array();
183
184
	/**
185
	 * Opposing relationship to owns config; Represents the objects which
186
	 * own the current object.
187
	 *
188
	 * @var array
189
	 */
190
	private static $owned_by = array();
191
192
	/**
193
	 * Reset static configuration variables to their default values.
194
	 */
195
	public static function reset() {
196
		self::$reading_mode = '';
197
198
		Session::clear('readingMode');
199
	}
200
201
	/**
202
	 * Construct a new Versioned object.
203
	 *
204
	 * @var array $stages The different stages the versioned object can be.
205
	 * The first stage is considered the 'default' stage, the last stage is
206
	 * considered the 'live' stage.
207
	 */
208
	public function __construct($stages = array('Stage','Live')) {
209
		parent::__construct();
210
211
		if(!is_array($stages)) {
212
			$stages = func_get_args();
213
		}
214
215
		$this->stages = $stages;
216
		$this->defaultStage = reset($stages);
217
		$this->liveStage = array_pop($stages);
218
	}
219
220
	/**
221
	 * Amend freshly created DataQuery objects with versioned-specific
222
	 * information.
223
	 *
224
	 * @param SQLSelect
225
	 * @param DataQuery
226
	 */
227
	public function augmentDataQueryCreation(SQLSelect &$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...
228
		$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...
229
230
		if($parts[0] == 'Archive') {
231
			$dataQuery->setQueryParam('Versioned.mode', 'archive');
232
			$dataQuery->setQueryParam('Versioned.date', $parts[1]);
233
		} else if($parts[0] == 'Stage' && in_array($parts[1], $this->stages)) {
234
			$dataQuery->setQueryParam('Versioned.mode', 'stage');
235
			$dataQuery->setQueryParam('Versioned.stage', $parts[1]);
236
		}
237
	}
238
239
240
	public function updateInheritableQueryParams(&$params) {
241
		// Versioned.mode === all_versions doesn't inherit very well, so default to stage
242
		if(isset($params['Versioned.mode']) && $params['Versioned.mode'] === 'all_versions') {
243
			$params['Versioned.mode'] = 'stage';
244
			$params['Versioned.stage'] = $this->defaultStage;
245
		}
246
	}
247
248
	/**
249
	 * Augment the the SQLSelect that is created by the DataQuery
250
	 *
251
	 * @param SQLSelect $query
252
	 * @param DataQuery $dataQuery
253
	 * @throws InvalidArgumentException
254
	 */
255
	public function augmentSQL(SQLSelect $query, DataQuery $dataQuery = null) {
256
		if(!$dataQuery || !$dataQuery->getQueryParam('Versioned.mode')) {
257
			return;
258
		}
259
260
		$baseTable = ClassInfo::baseDataClass($dataQuery->dataClass());
261
262
		switch($dataQuery->getQueryParam('Versioned.mode')) {
263
		// Reading a specific data from the archive
264
		case 'archive':
265
			$date = $dataQuery->getQueryParam('Versioned.date');
266
			foreach($query->getFrom() as $table => $dummy) {
267
				if(!$this->isTableVersioned($table)) {
268
					continue;
269
				}
270
271
				$query->renameTable($table, $table . '_versions');
272
				$query->replaceText("\"{$table}_versions\".\"ID\"", "\"{$table}_versions\".\"RecordID\"");
273
				$query->replaceText("`{$table}_versions`.`ID`", "`{$table}_versions`.`RecordID`");
274
275
				// Add all <basetable>_versions columns
276
				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...
277
					$query->selectField(sprintf('"%s_versions"."%s"', $baseTable, $name), $name);
278
				}
279
				$query->selectField(sprintf('"%s_versions"."%s"', $baseTable, 'RecordID'), "ID");
280
281
				if($table != $baseTable) {
282
					$query->addWhere("\"{$table}_versions\".\"Version\" = \"{$baseTable}_versions\".\"Version\"");
283
				}
284
			}
285
			// Link to the version archived on that date
286
			$query->addWhere(array(
287
				"\"{$baseTable}_versions\".\"Version\" IN
288
				(SELECT LatestVersion FROM
289
					(SELECT
290
						\"{$baseTable}_versions\".\"RecordID\",
291
						MAX(\"{$baseTable}_versions\".\"Version\") AS LatestVersion
292
						FROM \"{$baseTable}_versions\"
293
						WHERE \"{$baseTable}_versions\".\"LastEdited\" <= ?
294
						GROUP BY \"{$baseTable}_versions\".\"RecordID\"
295
					) AS \"{$baseTable}_versions_latest\"
296
					WHERE \"{$baseTable}_versions_latest\".\"RecordID\" = \"{$baseTable}_versions\".\"RecordID\"
297
				)" => $date
298
			));
299
			break;
300
301
		// Reading a specific stage (Stage or Live)
302
		case 'stage':
303
			$stage = $dataQuery->getQueryParam('Versioned.stage');
304
			if($stage && ($stage != $this->defaultStage)) {
305
				foreach($query->getFrom() as $table => $dummy) {
306
					if(!$this->isTableVersioned($table)) {
307
						continue;
308
					}
309
					$query->renameTable($table, $table . '_' . $stage);
310
				}
311
			}
312
			break;
313
314
		// Reading a specific stage, but only return items that aren't in any other stage
315
		case 'stage_unique':
316
			$stage = $dataQuery->getQueryParam('Versioned.stage');
317
318
			// Recurse to do the default stage behavior (must be first, we rely on stage renaming happening before
319
			// below)
320
			$dataQuery->setQueryParam('Versioned.mode', 'stage');
321
			$this->augmentSQL($query, $dataQuery);
322
			$dataQuery->setQueryParam('Versioned.mode', 'stage_unique');
323
324
			// Now exclude any ID from any other stage. Note that we double rename to avoid the regular stage rename
325
			// renaming all subquery references to be Versioned.stage
326
			foreach($this->stages as $excluding) {
327
				if ($excluding == $stage) continue;
328
329
				$tempName = 'ExclusionarySource_'.$excluding;
330
				$excludingTable = $baseTable . ($excluding && $excluding != $this->defaultStage ? "_$excluding" : '');
331
332
				$query->addWhere('"'.$baseTable.'"."ID" NOT IN (SELECT "ID" FROM "'.$tempName.'")');
333
				$query->renameTable($tempName, $excludingTable);
334
			}
335
			break;
336
337
		// Return all version instances
338
		case 'all_versions':
339
		case 'latest_versions':
340
			foreach($query->getFrom() as $alias => $join) {
341
				if(!$this->isTableVersioned($alias)) {
342
					continue;
343
				}
344
345
				if($alias != $baseTable) {
346
					// Make sure join includes version as well
347
					$query->setJoinFilter(
348
						$alias,
349
						"\"{$alias}_versions\".\"RecordID\" = \"{$baseTable}_versions\".\"RecordID\""
350
						. " AND \"{$alias}_versions\".\"Version\" = \"{$baseTable}_versions\".\"Version\""
351
					);
352
				}
353
				$query->renameTable($alias, $alias . '_versions');
354
			}
355
356
			// Add all <basetable>_versions columns
357
			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...
358
				$query->selectField(sprintf('"%s_versions"."%s"', $baseTable, $name), $name);
359
			}
360
361
			// Alias the record ID as the row ID, and ensure ID filters are aliased correctly
362
			$query->selectField("\"{$baseTable}_versions\".\"RecordID\"", "ID");
363
			$query->replaceText("\"{$baseTable}_versions\".\"ID\"", "\"{$baseTable}_versions\".\"RecordID\"");
364
365
			// However, if doing count, undo rewrite of "ID" column
366
			$query->replaceText(
367
				"count(DISTINCT \"{$baseTable}_versions\".\"RecordID\")",
368
				"count(DISTINCT \"{$baseTable}_versions\".\"ID\")"
369
			);
370
371
			// latest_version has one more step
372
			// Return latest version instances, regardless of whether they are on a particular stage
373
			// This provides "show all, including deleted" functonality
374
			if($dataQuery->getQueryParam('Versioned.mode') == 'latest_versions') {
375
				$query->addWhere(
376
					"\"{$baseTable}_versions\".\"Version\" IN
377
					(SELECT LatestVersion FROM
378
						(SELECT
379
							\"{$baseTable}_versions\".\"RecordID\",
380
							MAX(\"{$baseTable}_versions\".\"Version\") AS LatestVersion
381
							FROM \"{$baseTable}_versions\"
382
							GROUP BY \"{$baseTable}_versions\".\"RecordID\"
383
						) AS \"{$baseTable}_versions_latest\"
384
						WHERE \"{$baseTable}_versions_latest\".\"RecordID\" = \"{$baseTable}_versions\".\"RecordID\"
385
					)");
386
			} else {
387
				// If all versions are requested, ensure that records are sorted by this field
388
				$query->addOrderBy(sprintf('"%s_versions"."%s"', $baseTable, 'Version'));
389
			}
390
			break;
391
		default:
392
			throw new InvalidArgumentException("Bad value for query parameter Versioned.mode: "
393
				. $dataQuery->getQueryParam('Versioned.mode'));
394
		}
395
	}
396
397
	/**
398
	 * Determine if the given versioned table is a part of the sub-tree of the current dataobject
399
	 * This helps prevent rewriting of other tables that get joined in, in particular, many_many tables
400
	 *
401
	 * @param string $table
402
	 * @return bool True if this table should be versioned
403
	 */
404
	protected function isTableVersioned($table) {
405
		if(!class_exists($table)) {
406
			return false;
407
		}
408
		$baseClass = ClassInfo::baseDataClass($this->owner);
409
		return is_a($table, $baseClass, true);
410
	}
411
412
	/**
413
	 * For lazy loaded fields requiring extra sql manipulation, ie versioning.
414
	 *
415
	 * @param SQLSelect $query
416
	 * @param DataQuery $dataQuery
417
	 * @param DataObject $dataObject
418
	 */
419
	public function augmentLoadLazyFields(SQLSelect &$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...
420
		// The VersionedMode local variable ensures that this decorator only applies to
421
		// queries that have originated from the Versioned object, and have the Versioned
422
		// metadata set on the query object. This prevents regular queries from
423
		// accidentally querying the *_versions tables.
424
		$versionedMode = $dataObject->getSourceQueryParam('Versioned.mode');
425
		$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...
426
		$modesToAllowVersioning = array('all_versions', 'latest_versions', 'archive');
427
		if(
428
			!empty($dataObject->Version) &&
429
			(!empty($versionedMode) && in_array($versionedMode,$modesToAllowVersioning))
430
		) {
431
			$dataQuery->where("\"$dataClass\".\"RecordID\" = " . $dataObject->ID);
432
			$dataQuery->where("\"$dataClass\".\"Version\" = " . $dataObject->Version);
433
			$dataQuery->setQueryParam('Versioned.mode', 'all_versions');
434
		} else {
435
			// Same behaviour as in DataObject->loadLazyFields
436
			$dataQuery->where("\"$dataClass\".\"ID\" = {$dataObject->ID}")->limit(1);
437
		}
438
	}
439
440
441
	/**
442
	 * Called by {@link SapphireTest} when the database is reset.
443
	 *
444
	 * @todo Reduce the coupling between this and SapphireTest, somehow.
445
	 */
446
	public static function on_db_reset() {
447
		// Drop all temporary tables
448
		$db = DB::get_conn();
449
		foreach(self::$archive_tables as $tableName) {
450
			if(method_exists($db, 'dropTable')) $db->dropTable($tableName);
451
			else $db->query("DROP TABLE \"$tableName\"");
452
		}
453
454
		// Remove references to them
455
		self::$archive_tables = array();
456
	}
457
458
	public function augmentDatabase() {
459
		$classTable = $this->owner->class;
460
461
		$isRootClass = ($this->owner->class == ClassInfo::baseDataClass($this->owner->class));
462
463
		// Build a list of suffixes whose tables need versioning
464
		$allSuffixes = array();
465
		$versionableExtensions = $this->owner->config()->versionableExtensions;
466
		if(count($versionableExtensions)){
467
			foreach ($versionableExtensions as $versionableExtension => $suffixes) {
468
			if ($this->owner->hasExtension($versionableExtension)) {
469
				$allSuffixes = array_merge($allSuffixes, (array)$suffixes);
470
				foreach ((array)$suffixes as $suffix) {
471
					$allSuffixes[$suffix] = $versionableExtension;
472
				}
473
			}
474
		}
475
		}
476
477
		// Add the default table with an empty suffix to the list (table name = class name)
478
		array_push($allSuffixes,'');
479
480
		foreach ($allSuffixes as $key => $suffix) {
481
			// check that this is a valid suffix
482
			if (!is_int($key)) continue;
483
484
			if ($suffix) $table = "{$classTable}_$suffix";
485
			else $table = $classTable;
486
487
			$fields = DataObject::database_fields($this->owner->class);
488
			unset($fields['ID']);
489
			if($fields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fields 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...
490
				$options = Config::inst()->get($this->owner->class, 'create_table_options', Config::FIRST_SET);
491
				$indexes = $this->owner->databaseIndexes();
492
				if ($suffix && ($ext = $this->owner->getExtensionInstance($allSuffixes[$suffix]))) {
493
					if (!$ext->isVersionedTable($table)) continue;
494
					$ext->setOwner($this->owner);
495
					$fields = $ext->fieldsInExtraTables($suffix);
496
					$ext->clearOwner();
497
					$indexes = $fields['indexes'];
498
					$fields = $fields['db'];
499
				}
500
501
				// Create tables for other stages
502
				foreach($this->stages as $stage) {
503
					// Extra tables for _Live, etc.
504
					// Change unique indexes to 'index'.  Versioned tables may run into unique indexing difficulties
505
					// otherwise.
506
					$indexes = $this->uniqueToIndex($indexes);
507
					if($stage != $this->defaultStage) {
508
						DB::require_table("{$table}_$stage", $fields, $indexes, false, $options);
509
					}
510
511
					// Version fields on each root table (including Stage)
512
					/*
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...
513
					if($isRootClass) {
514
						$stageTable = ($stage == $this->defaultStage) ? $table : "{$table}_$stage";
515
						$parts=Array('datatype'=>'int', 'precision'=>11, 'null'=>'not null', 'default'=>(int)0);
516
						$values=Array('type'=>'int', 'parts'=>$parts);
517
						DB::requireField($stageTable, 'Version', $values);
518
					}
519
					*/
520
				}
521
522
				if($isRootClass) {
523
					// Create table for all versions
524
					$versionFields = array_merge(
525
						Config::inst()->get('Versioned', 'db_for_versions_table'),
526
						(array)$fields
527
					);
528
529
					$versionIndexes = array_merge(
530
						Config::inst()->get('Versioned', 'indexes_for_versions_table'),
531
						(array)$indexes
532
					);
533
				} else {
534
					// Create fields for any tables of subclasses
535
					$versionFields = array_merge(
536
						array(
537
							"RecordID" => "Int",
538
							"Version" => "Int",
539
						),
540
						(array)$fields
541
					);
542
543
					//Unique indexes will not work on versioned tables, so we'll convert them to standard indexes:
544
					$indexes = $this->uniqueToIndex($indexes);
545
					$versionIndexes = array_merge(
546
						array(
547
							'RecordID_Version' => array('type' => 'unique', 'value' => '"RecordID","Version"'),
548
							'RecordID' => true,
549
							'Version' => true,
550
						),
551
						(array)$indexes
552
					);
553
				}
554
555
				if(DB::get_schema()->hasTable("{$table}_versions")) {
556
					// Fix data that lacks the uniqueness constraint (since this was added later and
557
					// bugs meant that the constraint was validated)
558
					$duplications = DB::query("SELECT MIN(\"ID\") AS \"ID\", \"RecordID\", \"Version\"
559
						FROM \"{$table}_versions\" GROUP BY \"RecordID\", \"Version\"
560
						HAVING COUNT(*) > 1");
561
562
					foreach($duplications as $dup) {
563
						DB::alteration_message("Removing {$table}_versions duplicate data for "
564
							."{$dup['RecordID']}/{$dup['Version']}" ,"deleted");
565
						DB::prepared_query(
566
							"DELETE FROM \"{$table}_versions\" WHERE \"RecordID\" = ?
567
							AND \"Version\" = ? AND \"ID\" != ?",
568
							array($dup['RecordID'], $dup['Version'], $dup['ID'])
569
						);
570
					}
571
572
					// Remove junk which has no data in parent classes. Only needs to run the following
573
					// when versioned data is spread over multiple tables
574
					if(!$isRootClass && ($versionedTables = ClassInfo::dataClassesFor($table))) {
575
576
						foreach($versionedTables as $child) {
577
							if($table === $child) break; // only need subclasses
578
579
							// Select all orphaned version records
580
							$orphanedQuery = SQLSelect::create()
581
								->selectField("\"{$table}_versions\".\"ID\"")
582
								->setFrom("\"{$table}_versions\"");
583
584
							// If we have a parent table limit orphaned records
585
							// to only those that exist in this
586
							if(DB::get_schema()->hasTable("{$child}_versions")) {
587
								$orphanedQuery
588
									->addLeftJoin(
589
										"{$child}_versions",
590
										"\"{$child}_versions\".\"RecordID\" = \"{$table}_versions\".\"RecordID\"
591
										AND \"{$child}_versions\".\"Version\" = \"{$table}_versions\".\"Version\""
592
									)
593
									->addWhere("\"{$child}_versions\".\"ID\" IS NULL");
594
							}
595
596
							$count = $orphanedQuery->count();
597
							if($count > 0) {
598
								DB::alteration_message("Removing {$count} orphaned versioned records", "deleted");
599
								$ids = $orphanedQuery->execute()->column();
600
								foreach($ids as $id) {
601
									DB::prepared_query(
602
										"DELETE FROM \"{$table}_versions\" WHERE \"ID\" = ?",
603
										array($id)
604
									);
605
								}
606
							}
607
						}
608
					}
609
				}
610
611
				DB::require_table("{$table}_versions", $versionFields, $versionIndexes, true, $options);
612
			} else {
613
				DB::dont_require_table("{$table}_versions");
614
				foreach($this->stages as $stage) {
615
					if($stage != $this->defaultStage) DB::dont_require_table("{$table}_$stage");
616
				}
617
			}
618
		}
619
	}
620
621
	/**
622
	 * Helper for augmentDatabase() to find unique indexes and convert them to non-unique
623
	 *
624
	 * @param array $indexes The indexes to convert
625
	 * @return array $indexes
626
	 */
627
	private function uniqueToIndex($indexes) {
628
		$unique_regex = '/unique/i';
629
		$results = array();
630
		foreach ($indexes as $key => $index) {
631
			$results[$key] = $index;
632
633
			// support string descriptors
634
			if (is_string($index)) {
635
				if (preg_match($unique_regex, $index)) {
636
					$results[$key] = preg_replace($unique_regex, 'index', $index);
637
				}
638
			}
639
640
			// canonical, array-based descriptors
641
			elseif (is_array($index)) {
642
				if (strtolower($index['type']) == 'unique') {
643
					$results[$key]['type'] = 'index';
644
				}
645
			}
646
		}
647
		return $results;
648
	}
649
650
	/**
651
	 * Generates a ($table)_version DB manipulation and injects it into the current $manipulation
652
	 *
653
	 * @param array $manipulation Source manipulation data
654
	 * @param string $table Name of table
655
	 * @param int $recordID ID of record to version
656
	 */
657
	protected function augmentWriteVersioned(&$manipulation, $table, $recordID) {
658
		$baseDataClass = ClassInfo::baseDataClass($table);
659
660
		// Set up a new entry in (table)_versions
661
		$newManipulation = array(
662
			"command" => "insert",
663
			"fields" => isset($manipulation[$table]['fields']) ? $manipulation[$table]['fields'] : null
664
		);
665
666
		// Add any extra, unchanged fields to the version record.
667
		$data = DB::prepared_query("SELECT * FROM \"$table\" WHERE \"ID\" = ?", array($recordID))->record();
668
669
		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...
670
			$fields = DataObject::database_fields($table);
671
672
			if (is_array($fields)) {
673
				$data = array_intersect_key($data, $fields);
674
675
				foreach ($data as $k => $v) {
676
					if (!isset($newManipulation['fields'][$k])) {
677
						$newManipulation['fields'][$k] = $v;
678
					}
679
				}
680
			}
681
		}
682
683
		// Ensure that the ID is instead written to the RecordID field
684
		$newManipulation['fields']['RecordID'] = $recordID;
685
		unset($newManipulation['fields']['ID']);
686
687
		// Generate next version ID to use
688
		$nextVersion = 0;
689
		if($recordID) {
690
			$nextVersion = DB::prepared_query("SELECT MAX(\"Version\") + 1
691
				FROM \"{$baseDataClass}_versions\" WHERE \"RecordID\" = ?",
692
				array($recordID)
693
			)->value();
694
		}
695
		$nextVersion = $nextVersion ?: 1;
696
697
		if($table === $baseDataClass) {
698
		// Write AuthorID for baseclass
699
			$userID = (Member::currentUser()) ? Member::currentUser()->ID : 0;
700
			$newManipulation['fields']['AuthorID'] = $userID;
701
702
			// Update main table version if not previously known
703
			$manipulation[$table]['fields']['Version'] = $nextVersion;
704
		}
705
706
		// Update _versions table manipulation
707
		$newManipulation['fields']['Version'] = $nextVersion;
708
		$manipulation["{$table}_versions"] = $newManipulation;
709
	}
710
711
	/**
712
	 * Rewrite the given manipulation to update the selected (non-default) stage
713
	 *
714
	 * @param array $manipulation Source manipulation data
715
	 * @param string $table Name of table
716
	 * @param int $recordID ID of record to version
717
	 */
718
	protected function augmentWriteStaged(&$manipulation, $table, $recordID) {
719
		// If the record has already been inserted in the (table), get rid of it.
720
		if($manipulation[$table]['command'] == 'insert') {
721
			DB::prepared_query(
722
				"DELETE FROM \"{$table}\" WHERE \"ID\" = ?",
723
				array($recordID)
724
			);
725
		}
726
727
		$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...
728
		$manipulation[$newTable] = $manipulation[$table];
729
		unset($manipulation[$table]);
730
	}
731
732
733
	public function augmentWrite(&$manipulation) {
734
		// get Version number from base data table on write
735
		$version = null;
736
		$baseDataClass = ClassInfo::baseDataClass($this->owner->class);
737
		if(isset($manipulation[$baseDataClass]['fields'])) {
738
			if ($this->migratingVersion) {
739
				$manipulation[$baseDataClass]['fields']['Version'] = $this->migratingVersion;
740
			}
741
			if (isset($manipulation[$baseDataClass]['fields']['Version'])) {
742
				$version = $manipulation[$baseDataClass]['fields']['Version'];
743
			}
744
		}
745
746
		// Update all tables
747
		$tables = array_keys($manipulation);
748
		foreach($tables as $table) {
749
750
			// Make sure that the augmented write is being applied to a table that can be versioned
751
			if( !$this->canBeVersioned($table) ) {
752
				unset($manipulation[$table]);
753
				continue;
754
			}
755
756
			// Get ID field
757
			$id = $manipulation[$table]['id']
758
				? $manipulation[$table]['id']
759
				: $manipulation[$table]['fields']['ID'];
760
			if(!$id) {
761
				user_error("Couldn't find ID in " . var_export($manipulation[$table], true), E_USER_ERROR);
762
			}
763
764
			if($version < 0 || $this->_nextWriteWithoutVersion) {
765
				// Putting a Version of -1 is a signal to leave the version table alone, despite their being no version
766
				unset($manipulation[$table]['fields']['Version']);
767
			} elseif(empty($version)) {
768
				// If we haven't got a version #, then we're creating a new version.
769
				// Otherwise, we're just copying a version to another table
770
				$this->augmentWriteVersioned($manipulation, $table, $id);
771
			}
772
773
			// Remove "Version" column from subclasses of baseDataClass
774
			if(!$this->hasVersionField($table)) {
775
				unset($manipulation[$table]['fields']['Version']);
776
			}
777
778
			// Grab a version number - it should be the same across all tables.
779
			if(isset($manipulation[$table]['fields']['Version'])) {
780
				$thisVersion = $manipulation[$table]['fields']['Version'];
781
			}
782
783
			// If we're editing Live, then use (table)_Live instead of (table)
784
			if(
785
				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...
786
				&& 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...
787
				&& 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...
788
			) {
789
				$this->augmentWriteStaged($manipulation, $table, $id);
790
			}
791
		}
792
793
		// Clear the migration flag
794
		if($this->migratingVersion) {
795
			$this->migrateVersion(null);
796
		}
797
798
		// Add the new version # back into the data object, for accessing
799
		// after this write
800
		if(isset($thisVersion)) {
801
			$this->owner->Version = str_replace("'","", $thisVersion);
802
		}
803
	}
804
805
	/**
806
	 * Perform a write without affecting the version table.
807
	 * On objects without versioning.
808
	 *
809
	 * @return int The ID of the record
810
	 */
811
	public function writeWithoutVersion() {
812
		$this->_nextWriteWithoutVersion = true;
813
814
		return $this->owner->write();
815
	}
816
817
	/**
818
	 *
819
	 */
820
	public function onAfterWrite() {
821
		$this->_nextWriteWithoutVersion = false;
822
	}
823
824
	/**
825
	 * If a write was skipped, then we need to ensure that we don't leave a
826
	 * migrateVersion() value lying around for the next write.
827
	 */
828
	public function onAfterSkippedWrite() {
829
		$this->migrateVersion(null);
830
	}
831
832
	/**
833
	 * Find all objects owned by the current object.
834
	 * Note that objects will only be searched in the same stage as the given record.
835
	 *
836
	 * @param bool $recursive True if recursive
837
	 * @param ArrayList $list Optional list to add items to
838
	 * @return ArrayList list of objects
839
	 */
840
	public function findOwned($recursive = true, $list = null)
841
	{
842
		// Find objects in these relationships
843
		return $this->findRelatedObjects('owns', $recursive, $list);
844
	}
845
846
	/**
847
	 * Find objects which own this object.
848
	 * Note that objects will only be searched in the same stage as the given record.
849
	 *
850
	 * @param bool $recursive True if recursive
851
	 * @param ArrayList $list Optional list to add items to
852
	 * @return ArrayList list of objects
853
	 */
854
	public function findOwners($recursive = true, $list = null)
855
	{
856
		// Find objects in these relationships
857
		return $this->findRelatedObjects('owned_by', $recursive, $list);
858
	}
859
860
	/**
861
	 * Find objects in the given relationships, merging them into the given list
862
	 *
863
	 * @param array $source Config property to extract relationships from
864
	 * @param bool $recursive True if recursive
865
	 * @param ArrayList $list Optional list to add items to
866
	 * @return ArrayList The list
867
	 */
868
	public function findRelatedObjects($source, $recursive = true, $list = null)
869
	{
870
		if (!$list) {
871
			$list = new ArrayList();
872
		}
873
874
		// Skip search for unsaved records
875
		if(!$this->owner->isInDB()) {
876
			return $list;
877
		}
878
879
		$relationships = $this->owner->config()->{$source};
880
		foreach($relationships as $relationship) {
881
			// Warn if invalid config
882
			if(!$this->owner->hasMethod($relationship)) {
883
				trigger_error(sprintf(
884
					"Invalid %s config value \"%s\" on object on class \"%s\"",
885
					$source,
886
					$relationship,
887
					$this->owner->class
888
				), E_USER_WARNING);
889
				continue;
890
			}
891
892
			// Inspect value of this relationship
893
			$items = $this->owner->{$relationship}();
894
			if(!$items) {
895
				continue;
896
			}
897
			if($items instanceof DataObject) {
898
				$items = array($items);
899
			}
900
901
			/** @var Versioned|DataObject $item */
902
			foreach($items as $item) {
903
				// Identify item
904
				$itemKey = $item->class . '/' . $item->ID;
905
906
				// Skip unsaved, unversioned, or already checked objects
907
				if(!$item->isInDB() || !$item->has_extension('Versioned') || isset($list[$itemKey])) {
908
					continue;
909
				}
910
911
				// Save record
912
				$list[$itemKey] = $item;
913
				if($recursive) {
914
					$item->findRelatedObjects($source, true, $list);
915
				};
916
			}
917
		}
918
		return $list;
919
	}
920
921
	/**
922
	 * This function should return true if the current user can publish this record.
923
	 * It can be overloaded to customise the security model for an application.
924
	 *
925
	 * Denies permission if any of the following conditions is true:
926
	 * - canPublish() on any extension returns false
927
	 * - canEdit() returns false
928
	 *
929
	 * @param Member $member
930
	 * @return bool True if the current user can publish this record.
931
	 */
932
	public function canPublish($member = null) {
933
		// Skip if invoked by extendedCan()
934
		if(func_num_args() > 4) {
935
			return null;
936
		}
937
938
		if(!$member) {
939
			$member = Member::currentUser();
940
		}
941
942
		if(Permission::checkMember($member, "ADMIN")) {
943
			return true;
944
		}
945
946
		// Standard mechanism for accepting permission changes from extensions
947
		$extended = $this->owner->extendedCan('canPublish', $member);
948
		if($extended !== null) {
949
			return $extended;
950
		}
951
952
		// Default to relying on edit permission
953
		return $this->owner->canEdit($member);
954
	}
955
956
	/**
957
	 * Check if the current user can delete this record from live
958
	 *
959
	 * @param null $member
960
	 * @return mixed
961
	 */
962
	public function canUnpublish($member = null) {
963
		// Skip if invoked by extendedCan()
964
		if(func_num_args() > 4) {
965
			return null;
966
		}
967
968
		if(!$member) {
969
			$member = Member::currentUser();
970
		}
971
972
		if(Permission::checkMember($member, "ADMIN")) {
973
			return true;
974
		}
975
976
		// Standard mechanism for accepting permission changes from extensions
977
		$extended = $this->owner->extendedCan('canUnpublish', $member);
978
		if($extended !== null) {
979
			return $extended;
980
		}
981
982
		// Default to relying on canPublish
983
		return $this->owner->canPublish($member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by \Member::currentUser() on line 969 can also be of type object<DataObject>; however, Versioned::canPublish() does only seem to accept object<Member>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
984
	}
985
986
	/**
987
	 * Check if the current user is allowed to archive this record.
988
	 * If extended, ensure that both canDelete and canUnpublish are extended also
989
	 *
990
	 * @param Member $member
991
	 * @return bool
992
	 */
993
	public function canArchive($member = null) {
994
		// Skip if invoked by extendedCan()
995
		if(func_num_args() > 4) {
996
			return null;
997
		}
998
999
		if(!$member) {
1000
            $member = Member::currentUser();
1001
        }
1002
1003
		if(Permission::checkMember($member, "ADMIN")) {
1004
			return true;
1005
		}
1006
1007
		// Standard mechanism for accepting permission changes from extensions
1008
		$extended = $this->owner->extendedCan('canArchive', $member);
1009
		if($extended !== null) {
1010
            return $extended;
1011
        }
1012
1013
		// Check if this record can be deleted from stage
1014
        if(!$this->owner->canDelete($member)) {
1015
            return false;
1016
        }
1017
1018
        // Check if we can delete from live
1019
        if(!$this->owner->canUnpublish($member)) {
0 ignored issues
show
Bug introduced by
It seems like $member can also be of type object<DataObject>; however, Versioned::canUnpublish() does only seem to accept null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
1020
            return false;
1021
        }
1022
1023
		return true;
1024
	}
1025
1026
	/**
1027
	 * Extend permissions to include additional security for objects that are not published to live.
1028
	 *
1029
	 * @param Member $member
1030
	 * @return bool|null
1031
	 */
1032
	public function canView($member = null) {
1033
		// Invoke default version-gnostic canView
1034
		if ($this->owner->canViewVersioned($member) === false) {
1035
			return false;
1036
		}
1037
	}
1038
1039
	/**
1040
	 * Determine if there are any additional restrictions on this object for the given reading version.
1041
	 *
1042
	 * Override this in a subclass to customise any additional effect that Versioned applies to canView.
1043
	 *
1044
	 * This is expected to be called by canView, and thus is only responsible for denying access if
1045
	 * the default canView would otherwise ALLOW access. Thus it should not be called in isolation
1046
	 * as an authoritative permission check.
1047
	 *
1048
	 * This has the following extension points:
1049
	 *  - canViewDraft is invoked if Mode = stage and Stage = stage
1050
	 *  - canViewArchived is invoked if Mode = archive
1051
	 *
1052
	 * @param Member $member
1053
	 * @return bool False is returned if the current viewing mode denies visibility
1054
	 */
1055
	public function canViewVersioned($member = null) {
1056
		// Bypass when live stage
1057
		$mode = $this->owner->getSourceQueryParam("Versioned.mode");
1058
		$stage = $this->owner->getSourceQueryParam("Versioned.stage");
1059
		if ($mode === 'stage' && $stage === static::get_live_stage()) {
1060
			return true;
1061
		}
1062
1063
		// Bypass if site is unsecured
1064
		if (Session::get('unsecuredDraftSite')) {
1065
			return true;
1066
		}
1067
1068
		// Bypass if record doesn't have a live stage
1069
		if(!in_array(static::get_live_stage(), $this->getVersionedStages())) {
1070
			return true;
1071
		}
1072
1073
		// If we weren't definitely loaded from live, and we can't view non-live content, we need to
1074
		// check to make sure this version is the live version and so can be viewed
1075
		$latestVersion = Versioned::get_versionnumber_by_stage($this->owner->class, 'Live', $this->owner->ID);
0 ignored issues
show
Bug introduced by
The property ID does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
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...
1076
		if ($latestVersion == $this->owner->Version) {
1077
			// Even if this is loaded from a non-live stage, this is the live version
1078
			return true;
1079
		}
1080
1081
		// Extend versioned behaviour
1082
		$extended = $this->owner->extendedCan('canViewNonLive', $member);
1083
		if($extended !== null) {
1084
			return (bool)$extended;
1085
		}
1086
1087
		// Fall back to default permission check
1088
		$permissions = Config::inst()->get($this->owner->class, 'non_live_permissions', Config::FIRST_SET);
1089
		$check = Permission::checkMember($member, $permissions);
1090
		return (bool)$check;
1091
	}
1092
1093
	/**
1094
	 * Determines canView permissions for the latest version of this object on a specific stage.
1095
	 * Usually the stage is read from {@link Versioned::current_stage()}.
1096
	 *
1097
	 * This method should be invoked by user code to check if a record is visible in the given stage.
1098
	 *
1099
	 * This method should not be called via ->extend('canViewStage'), but rather should be
1100
	 * overridden in the extended class.
1101
	 *
1102
	 * @param string $stage
1103
	 * @param Member $member
1104
	 * @return bool
1105
	 */
1106
	public function canViewStage($stage = 'Live', $member = null) {
1107
		$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...
1108
		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...
1109
1110
		$versionFromStage = DataObject::get($this->owner->class)->byID($this->owner->ID);
1111
1112
		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...
1113
		return $versionFromStage ? $versionFromStage->canView($member) : false;
1114
	}
1115
1116
	/**
1117
	 * Determine if a table is supporting the Versioned extensions (e.g.
1118
	 * $table_versions does exists).
1119
	 *
1120
	 * @param string $table Table name
1121
	 * @return boolean
1122
	 */
1123
	public function canBeVersioned($table) {
1124
		return ClassInfo::exists($table)
1125
			&& is_subclass_of($table, 'DataObject')
1126
			&& DataObject::has_own_table($table);
1127
	}
1128
1129
	/**
1130
	 * Check if a certain table has the 'Version' field.
1131
	 *
1132
	 * @param string $table Table name
1133
	 *
1134
	 * @return boolean Returns false if the field isn't in the table, true otherwise
1135
	 */
1136
	public function hasVersionField($table) {
1137
		$rPos = strrpos($table,'_');
1138
1139
		if(($rPos !== false) && in_array(substr($table,$rPos), $this->stages)) {
1140
			$tableWithoutStage = substr($table,0,$rPos);
1141
		} else {
1142
			$tableWithoutStage = $table;
1143
		}
1144
1145
		return ('DataObject' == get_parent_class($tableWithoutStage));
1146
	}
1147
1148
	/**
1149
	 * @param string $table
1150
	 *
1151
	 * @return string
1152
	 */
1153
	public function extendWithSuffix($table) {
1154
		$owner = $this->owner;
1155
		$versionableExtensions = $owner->config()->versionableExtensions;
1156
1157
		if(count($versionableExtensions)){
1158
			foreach ($versionableExtensions as $versionableExtension => $suffixes) {
1159
				if ($owner->hasExtension($versionableExtension)) {
1160
					$ext = $owner->getExtensionInstance($versionableExtension);
1161
					$ext->setOwner($owner);
1162
				$table = $ext->extendWithSuffix($table);
1163
				$ext->clearOwner();
1164
			}
1165
		}
1166
		}
1167
1168
		return $table;
1169
	}
1170
1171
	/**
1172
	 * Get the latest published DataObject.
1173
	 *
1174
	 * @return DataObject
1175
	 */
1176
	public function latestPublished() {
1177
		// Get the root data object class - this will have the version field
1178
		$table1 = $this->owner->class;
1179
		while( ($p = get_parent_class($table1)) != "DataObject") $table1 = $p;
1180
1181
		$table2 = $table1 . "_$this->liveStage";
1182
1183
		return DB::prepared_query("SELECT \"$table1\".\"Version\" = \"$table2\".\"Version\" FROM \"$table1\"
1184
			 INNER JOIN \"$table2\" ON \"$table1\".\"ID\" = \"$table2\".\"ID\"
1185
			 WHERE \"$table1\".\"ID\" = ?",
1186
			array($this->owner->ID)
1187
		)->value();
1188
	}
1189
1190
	/**
1191
	 * Provides a simple doPublish action for Versioned dataobjects
1192
	 *
1193
	 * @return bool True if publish was successful
1194
	 */
1195
	public function doPublish() {
1196
		$owner = $this->owner;
1197
		$owner->invokeWithExtensions('onBeforePublish');
1198
		$owner->write();
1199
		$owner->publish("Stage", "Live");
1200
		$owner->invokeWithExtensions('onAfterPublish');
1201
		return true;
1202
	}
1203
1204
1205
1206
	/**
1207
	 * Removes the record from both live and stage
1208
	 *
1209
	 * @return bool Success
1210
	 */
1211
	public function doArchive() {
1212
		$owner = $this->owner;
1213
		$owner->invokeWithExtensions('onBeforeArchive', $this);
1214
1215
		if($owner->doUnpublish()) {
1216
			$owner->delete();
0 ignored issues
show
Bug introduced by
The method delete() does not exist on Versioned. Did you maybe mean deleteFromStage()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
1217
			$owner->invokeWithExtensions('onAfterArchive', $this);
1218
1219
			return true;
1220
		}
1221
1222
		return false;
1223
	}
1224
1225
	/**
1226
	 * Removes this record from the live site
1227
	 *
1228
	 * @return bool Flag whether the unpublish was successful
1229
	 *
1230
	 * @uses SiteTreeExtension->onBeforeUnpublish()
1231
	 * @uses SiteTreeExtension->onAfterUnpublish()
1232
	 */
1233
	public function doUnpublish() {
1234
		$owner = $this->owner;
1235
		if(!$owner->isInDB()) {
1236
			return false;
1237
		}
1238
1239
		$owner->invokeWithExtensions('onBeforeUnpublish');
1240
1241
		$origStage = self::current_stage();
1242
		self::reading_stage(self::get_live_stage());
1243
1244
		// This way our ID won't be unset
1245
		$clone = clone $owner;
1246
		$clone->delete();
0 ignored issues
show
Bug introduced by
The method delete() does not exist on Versioned. Did you maybe mean deleteFromStage()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
1247
1248
		self::reading_stage($origStage);
1249
1250
		// If we're on the draft site, then we can update the status.
1251
		// Otherwise, these lines will resurrect an inappropriate record
1252
		if(self::current_stage() != self::get_live_stage() && $this->isOnDraft()) {
1253
			$owner->write();
1254
		}
1255
1256
		$owner->invokeWithExtensions('onAfterUnpublish');
1257
1258
		return true;
1259
	}
1260
1261
	/**
1262
	 * Move a database record from one stage to the other.
1263
	 *
1264
	 * @param int|string $fromStage Place to copy from.  Can be either a stage name or a version number.
1265
	 * @param string $toStage Place to copy to.  Must be a stage name.
1266
	 * @param bool $createNewVersion Set this to true to create a new version number.
1267
	 * By default, the existing version number will be copied over.
1268
	 */
1269
	public function publish($fromStage, $toStage, $createNewVersion = false) {
1270
		$owner = $this->owner;
1271
		$owner->invokeWithExtensions('onBeforeVersionedPublish', $fromStage, $toStage, $createNewVersion);
1272
1273
		$baseClass = ClassInfo::baseDataClass($owner->class);
1274
1275
		/** @var Versioned|DataObject $from */
1276
		if(is_numeric($fromStage)) {
1277
			$from = Versioned::get_version($baseClass, $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...
1278
		} else {
1279
			$this->owner->flushCache();
1280
			$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...
1281
				"\"{$baseClass}\".\"ID\" = ?" => $owner->ID
1282
			));
1283
		}
1284
		if(!$from) {
1285
			throw new InvalidArgumentException("Can't find {$baseClass}#{$owner->ID} in stage {$fromStage}");
1286
		}
1287
1288
		$from->forceChange();
1289
		if($createNewVersion) {
1290
			// Clear version to be automatically created on write
1291
			$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...
1292
		} else {
1293
			$from->migrateVersion($from->Version);
1294
1295
		// Mark this version as having been published at some stage
1296
			$publisherID = isset(Member::currentUser()->ID) ? Member::currentUser()->ID : 0;
1297
			$extTable = $this->extendWithSuffix($baseClass);
1298
		DB::prepared_query("UPDATE \"{$extTable}_versions\"
1299
			SET \"WasPublished\" = ?, \"PublisherID\" = ?
1300
			WHERE \"RecordID\" = ? AND \"Version\" = ?",
1301
			array(1, $publisherID, $from->ID, $from->Version)
1302
		);
1303
		}
1304
1305
		// Change to new stage, write, and revert state
1306
		$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...
1307
		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...
1308
1309
		// Migrate stage prior to write
1310
		$from->setSourceQueryParam('Versioned.mode', 'stage');
1311
		$from->setSourceQueryParam('Versioned.stage', $toStage);
1312
1313
		$conn = DB::get_conn();
1314
		if(method_exists($conn, 'allowPrimaryKeyEditing')) {
1315
			$conn->allowPrimaryKeyEditing($baseClass, true);
1316
			$from->write();
1317
			$conn->allowPrimaryKeyEditing($baseClass, false);
1318
		} else {
1319
			$from->write();
1320
		}
1321
1322
		$from->destroy();
1323
1324
		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...
1325
1326
		$owner->invokeWithExtensions('onAfterVersionedPublish', $fromStage, $toStage, $createNewVersion);
1327
	}
1328
1329
	/**
1330
	 * Set the migrating version.
1331
	 *
1332
	 * @param string $version The version.
1333
	 */
1334
	public function migrateVersion($version) {
1335
		$this->migratingVersion = $version;
1336
	}
1337
1338
	/**
1339
	 * Compare two stages to see if they're different.
1340
	 *
1341
	 * Only checks the version numbers, not the actual content.
1342
	 *
1343
	 * @param string $stage1 The first stage to check.
1344
	 * @param string $stage2
1345
	 */
1346
	public function stagesDiffer($stage1, $stage2) {
1347
		$table1 = $this->baseTable($stage1);
1348
		$table2 = $this->baseTable($stage2);
1349
1350
		if(!is_numeric($this->owner->ID)) {
1351
			return true;
1352
		}
1353
1354
		// We test for equality - if one of the versions doesn't exist, this
1355
		// will be false.
1356
1357
		// TODO: DB Abstraction: if statement here:
1358
		$stagesAreEqual = DB::prepared_query(
1359
			"SELECT CASE WHEN \"$table1\".\"Version\"=\"$table2\".\"Version\" THEN 1 ELSE 0 END
1360
			 FROM \"$table1\" INNER JOIN \"$table2\" ON \"$table1\".\"ID\" = \"$table2\".\"ID\"
1361
			 AND \"$table1\".\"ID\" = ?",
1362
			array($this->owner->ID)
1363
		)->value();
1364
1365
		return !$stagesAreEqual;
1366
	}
1367
1368
	/**
1369
	 * @param string $filter
1370
	 * @param string $sort
1371
	 * @param string $limit
1372
	 * @param string $join Deprecated, use leftJoin($table, $joinClause) instead
1373
	 * @param string $having
1374
	 */
1375
	public function Versions($filter = "", $sort = "", $limit = "", $join = "", $having = "") {
1376
		return $this->allVersions($filter, $sort, $limit, $join, $having);
1377
	}
1378
1379
	/**
1380
	 * Return a list of all the versions available.
1381
	 *
1382
	 * @param  string $filter
1383
	 * @param  string $sort
1384
	 * @param  string $limit
1385
	 * @param  string $join   Deprecated, use leftJoin($table, $joinClause) instead
1386
	 * @param  string $having
1387
	 * @return ArrayList
1388
	 */
1389
	public function allVersions($filter = "", $sort = "", $limit = "", $join = "", $having = "") {
1390
		// Make sure the table names are not postfixed (e.g. _Live)
1391
		$oldMode = self::get_reading_mode();
1392
		self::reading_stage('Stage');
1393
1394
		$list = DataObject::get(get_class($this->owner), $filter, $sort, $join, $limit);
1395
		if($having) $having = $list->having($having);
1396
1397
		$query = $list->dataQuery()->query();
1398
1399
		foreach($query->getFrom() as $table => $tableJoin) {
1400
			if(is_string($tableJoin) && $tableJoin[0] == '"') {
1401
				$baseTable = str_replace('"','',$tableJoin);
1402
			} elseif(is_string($tableJoin) && substr($tableJoin,0,5) != 'INNER') {
1403
				$query->setFrom(array(
1404
					$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...
1405
						. " AND \"$table\".\"Version\" = \"{$baseTable}_versions\".\"Version\""
1406
				));
1407
			}
1408
			$query->renameTable($table, $table . '_versions');
1409
		}
1410
1411
		// Add all <basetable>_versions columns
1412
		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...
1413
			$query->selectField(sprintf('"%s_versions"."%s"', $baseTable, $name), $name);
1414
		}
1415
1416
		$query->addWhere(array(
1417
			"\"{$baseTable}_versions\".\"RecordID\" = ?" => $this->owner->ID
1418
		));
1419
		$query->setOrderBy(($sort) ? $sort
1420
			: "\"{$baseTable}_versions\".\"LastEdited\" DESC, \"{$baseTable}_versions\".\"Version\" DESC");
1421
1422
		$records = $query->execute();
1423
		$versions = new ArrayList();
1424
1425
		foreach($records as $record) {
1426
			$versions->push(new Versioned_Version($record));
1427
		}
1428
1429
		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...
1430
		return $versions;
1431
	}
1432
1433
	/**
1434
	 * Compare two version, and return the diff between them.
1435
	 *
1436
	 * @param string $from The version to compare from.
1437
	 * @param string $to The version to compare to.
1438
	 *
1439
	 * @return DataObject
1440
	 */
1441
	public function compareVersions($from, $to) {
1442
		$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...
1443
		$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...
1444
1445
		$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 1443 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...
1446
1447
		return $diff->diffedData();
1448
	}
1449
1450
	/**
1451
	 * Return the base table - the class that directly extends DataObject.
1452
	 *
1453
	 * @param string $stage
1454
	 * @return string
1455
	 */
1456
	public function baseTable($stage = null) {
1457
		$tableClasses = ClassInfo::dataClassesFor($this->owner->class);
1458
		$baseClass = array_shift($tableClasses);
1459
1460
		if(!$stage || $stage == $this->defaultStage) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $stage 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...
1461
			return $baseClass;
1462
		}
1463
1464
		return $baseClass . "_$stage";
1465
	}
1466
1467
	//-----------------------------------------------------------------------------------------------//
1468
1469
1470
	/**
1471
	 * Determine if the current user is able to set the given site stage / archive
1472
	 *
1473
	 * @param SS_HTTPRequest $request
1474
	 * @return bool
1475
	 */
1476
	public static function can_choose_site_stage($request) {
1477
		// Request is allowed if stage isn't being modified
1478
		if((!$request->getVar('stage') || $request->getVar('stage') === static::get_live_stage())
1479
			&& !$request->getVar('archiveDate')
1480
		) {
1481
			return true;
1482
		}
1483
1484
		// Check permissions with member ID in session.
1485
		$member = Member::currentUser();
1486
		$permissions = Config::inst()->get(get_called_class(), 'non_live_permissions');
1487
		return $member && Permission::checkMember($member, $permissions);
1488
	}
1489
1490
	/**
1491
	 * Choose the stage the site is currently on.
1492
	 *
1493
	 * If $_GET['stage'] is set, then it will use that stage, and store it in
1494
	 * the session.
1495
	 *
1496
	 * if $_GET['archiveDate'] is set, it will use that date, and store it in
1497
	 * the session.
1498
	 *
1499
	 * If neither of these are set, it checks the session, otherwise the stage
1500
	 * is set to 'Live'.
1501
	 */
1502
	public static function choose_site_stage() {
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...
1503
		// Check any pre-existing session mode
1504
		$preexistingMode = Session::get('readingMode');
1505
1506
		// Determine the reading mode
1507
		if(isset($_GET['stage'])) {
1508
			$stage = ucfirst(strtolower($_GET['stage']));
1509
			if(!in_array($stage, array('Stage', 'Live'))) $stage = 'Live';
1510
			$mode = 'Stage.' . $stage;
1511
		} elseif (isset($_GET['archiveDate']) && strtotime($_GET['archiveDate'])) {
1512
			$mode = 'Archive.' . $_GET['archiveDate'];
1513
		} elseif($preexistingMode) {
1514
			$mode = $preexistingMode;
1515
		} else {
1516
			$mode = self::DEFAULT_MODE;
1517
		}
1518
1519
		// Save reading mode
1520
		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...
1521
1522
		// Try not to store the mode in the session if not needed
1523
		if(($preexistingMode && $preexistingMode !== $mode)
1524
			|| (!$preexistingMode && $mode !== self::DEFAULT_MODE)
1525
		) {
1526
			Session::set('readingMode', $mode);
1527
		}
1528
1529
		if(!headers_sent() && !Director::is_cli()) {
1530
			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...
1531
				// clear the cookie if it's set
1532
				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...
1533
					Cookie::force_expiry('bypassStaticCache', null, null, false, true /* httponly */);
1534
				}
1535
			} else {
1536
				// set the cookie if it's cleared
1537
				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...
1538
					Cookie::set('bypassStaticCache', '1', 0, null, null, false, true /* httponly */);
1539
				}
1540
			}
1541
		}
1542
	}
1543
1544
	/**
1545
	 * Set the current reading mode.
1546
	 *
1547
	 * @param string $mode
1548
	 */
1549
	public static function set_reading_mode($mode) {
1550
		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...
1551
	}
1552
1553
	/**
1554
	 * Get the current reading mode.
1555
	 *
1556
	 * @return string
1557
	 */
1558
	public static function get_reading_mode() {
1559
		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...
1560
	}
1561
1562
	/**
1563
	 * Get the name of the 'live' stage.
1564
	 *
1565
	 * @return string
1566
	 */
1567
	public static function get_live_stage() {
1568
		return "Live";
1569
	}
1570
1571
	/**
1572
	 * Get the current reading stage.
1573
	 *
1574
	 * @return string
1575
	 */
1576
	public static function current_stage() {
1577
		$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...
1578
1579
		if($parts[0] == 'Stage') {
1580
			return $parts[1];
1581
		}
1582
	}
1583
1584
	/**
1585
	 * Get the current archive date.
1586
	 *
1587
	 * @return string
1588
	 */
1589
	public static function current_archived_date() {
1590
		$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...
1591
		if($parts[0] == 'Archive') return $parts[1];
1592
	}
1593
1594
	/**
1595
	 * Set the reading stage.
1596
	 *
1597
	 * @param string $stage New reading stage.
1598
	 */
1599
	public static function reading_stage($stage) {
1600
		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...
1601
	}
1602
1603
	/**
1604
	 * Set the reading archive date.
1605
	 *
1606
	 * @param string $date New reading archived date.
1607
	 */
1608
	public static function reading_archived_date($date) {
1609
		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...
1610
	}
1611
1612
1613
	/**
1614
	 * Get a singleton instance of a class in the given stage.
1615
	 *
1616
	 * @param string $class The name of the class.
1617
	 * @param string $stage The name of the stage.
1618
	 * @param string $filter A filter to be inserted into the WHERE clause.
1619
	 * @param boolean $cache Use caching.
1620
	 * @param string $sort A sort expression to be inserted into the ORDER BY clause.
1621
	 *
1622
	 * @return DataObject
1623
	 */
1624
	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...
1625
		// TODO: No identity cache operating
1626
		$items = self::get_by_stage($class, $stage, $filter, $sort, null, 1);
1627
1628
		return $items->First();
1629
	}
1630
1631
	/**
1632
	 * Gets the current version number of a specific record.
1633
	 *
1634
	 * @param string $class
1635
	 * @param string $stage
1636
	 * @param int $id
1637
	 * @param boolean $cache
1638
	 *
1639
	 * @return int
1640
	 */
1641
	public static function get_versionnumber_by_stage($class, $stage, $id, $cache = true) {
1642
		$baseClass = ClassInfo::baseDataClass($class);
1643
		$stageTable = ($stage == 'Stage') ? $baseClass : "{$baseClass}_{$stage}";
1644
1645
		// cached call
1646
		if($cache && isset(self::$cache_versionnumber[$baseClass][$stage][$id])) {
1647
			return self::$cache_versionnumber[$baseClass][$stage][$id];
1648
		}
1649
1650
		// get version as performance-optimized SQL query (gets called for each record in the sitetree)
1651
		$version = DB::prepared_query(
1652
			"SELECT \"Version\" FROM \"$stageTable\" WHERE \"ID\" = ?",
1653
			array($id)
1654
		)->value();
1655
1656
		// cache value (if required)
1657
		if($cache) {
1658
			if(!isset(self::$cache_versionnumber[$baseClass])) {
1659
				self::$cache_versionnumber[$baseClass] = array();
1660
			}
1661
1662
			if(!isset(self::$cache_versionnumber[$baseClass][$stage])) {
1663
				self::$cache_versionnumber[$baseClass][$stage] = array();
1664
			}
1665
1666
			self::$cache_versionnumber[$baseClass][$stage][$id] = $version;
1667
		}
1668
1669
		return $version;
1670
	}
1671
1672
	/**
1673
	 * Pre-populate the cache for Versioned::get_versionnumber_by_stage() for
1674
	 * a list of record IDs, for more efficient database querying.  If $idList
1675
	 * is null, then every record will be pre-cached.
1676
	 *
1677
	 * @param string $class
1678
	 * @param string $stage
1679
	 * @param array $idList
1680
	 */
1681
	public static function prepopulate_versionnumber_cache($class, $stage, $idList = null) {
1682
		if (!Config::inst()->get('Versioned', 'prepopulate_versionnumber_cache')) {
1683
			return;
1684
		}
1685
		$filter = "";
1686
		$parameters = array();
1687
		if($idList) {
1688
			// Validate the ID list
1689
			foreach($idList as $id) {
1690
				if(!is_numeric($id)) {
1691
					user_error("Bad ID passed to Versioned::prepopulate_versionnumber_cache() in \$idList: " . $id,
1692
					E_USER_ERROR);
1693
				}
1694
			}
1695
			$filter = 'WHERE "ID" IN ('.DB::placeholders($idList).')';
1696
			$parameters = $idList;
1697
		}
1698
1699
		$baseClass = ClassInfo::baseDataClass($class);
1700
		$stageTable = ($stage == 'Stage') ? $baseClass : "{$baseClass}_{$stage}";
1701
1702
		$versions = DB::prepared_query("SELECT \"ID\", \"Version\" FROM \"$stageTable\" $filter", $parameters)->map();
1703
1704
		foreach($versions as $id => $version) {
1705
			self::$cache_versionnumber[$baseClass][$stage][$id] = $version;
1706
		}
1707
	}
1708
1709
	/**
1710
	 * Get a set of class instances by the given stage.
1711
	 *
1712
	 * @param string $class The name of the class.
1713
	 * @param string $stage The name of the stage.
1714
	 * @param string $filter A filter to be inserted into the WHERE clause.
1715
	 * @param string $sort A sort expression to be inserted into the ORDER BY clause.
1716
	 * @param string $join Deprecated, use leftJoin($table, $joinClause) instead
1717
	 * @param int $limit A limit on the number of records returned from the database.
1718
	 * @param string $containerClass The container class for the result set (default is DataList)
1719
	 *
1720
	 * @return DataList A modified DataList designated to the specified stage
1721
	 */
1722
	public static function get_by_stage(
1723
		$class, $stage, $filter = '', $sort = '', $join = '', $limit = null, $containerClass = 'DataList'
1724
	) {
1725
		$result = DataObject::get($class, $filter, $sort, $join, $limit, $containerClass);
1726
		return $result->setDataQueryParam(array(
1727
			'Versioned.mode' => 'stage',
1728
			'Versioned.stage' => $stage
1729
		));
1730
	}
1731
1732
	/**
1733
	 * Delete this record from the given stage
1734
	 *
1735
	 * @param string $stage
1736
	 */
1737
	public function deleteFromStage($stage) {
1738
		$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...
1739
		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...
1740
		$clone = clone $this->owner;
1741
		$clone->delete();
0 ignored issues
show
Bug introduced by
The method delete() does not exist on Versioned. Did you maybe mean deleteFromStage()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
1742
		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...
1743
1744
		// Fix the version number cache (in case you go delete from stage and then check ExistsOnLive)
1745
		$baseClass = ClassInfo::baseDataClass($this->owner->class);
1746
		self::$cache_versionnumber[$baseClass][$stage][$this->owner->ID] = null;
1747
	}
1748
1749
	/**
1750
	 * Write the given record to the draft stage
1751
	 *
1752
	 * @param string $stage
1753
	 * @param boolean $forceInsert
1754
	 * @return int The ID of the record
1755
	 */
1756
	public function writeToStage($stage, $forceInsert = false) {
1757
		$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...
1758
		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...
1759
1760
		$this->owner->forceChange();
1761
		$result = $this->owner->write(false, $forceInsert);
1762
		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...
1763
1764
		return $result;
1765
	}
1766
1767
	/**
1768
	 * Roll the draft version of this record to match the published record.
1769
	 * Caution: Doesn't overwrite the object properties with the rolled back version.
1770
	 *
1771
	 * @param int $version Either the string 'Live' or a version number
1772
	 */
1773
	public function doRollbackTo($version) {
1774
		$owner = $this->owner;
1775
		$owner->extend('onBeforeRollback', $version);
0 ignored issues
show
Bug introduced by
The method extend() does not exist on Versioned. Did you maybe mean extendWithSuffix()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
1776
		$this->publish($version, "Stage", true);
1777
		$owner->writeWithoutVersion();
1778
		$owner->extend('onAfterRollback', $version);
0 ignored issues
show
Bug introduced by
The method extend() does not exist on Versioned. Did you maybe mean extendWithSuffix()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
1779
	}
1780
1781
	/**
1782
	 * Return the latest version of the given record.
1783
	 *
1784
	 * @param string $class
1785
	 * @param int $id
1786
	 * @return DataObject
1787
	 */
1788
	public static function get_latest_version($class, $id) {
1789
		$baseClass = ClassInfo::baseDataClass($class);
1790
		$list = DataList::create($baseClass)
1791
			->where(array("\"$baseClass\".\"RecordID\"" => $id))
1792
			->setDataQueryParam("Versioned.mode", "latest_versions");
1793
1794
		return $list->First();
1795
	}
1796
1797
	/**
1798
	 * Returns whether the current record is the latest one.
1799
	 *
1800
	 * @todo Performance - could do this directly via SQL.
1801
	 *
1802
	 * @see get_latest_version()
1803
	 * @see latestPublished
1804
	 *
1805
	 * @return boolean
1806
	 */
1807
	public function isLatestVersion() {
1808
		if(!$this->owner->isInDB()) {
1809
			return false;
1810
		}
1811
1812
		$version = self::get_latest_version($this->owner->class, $this->owner->ID);
1813
		return ($version->Version == $this->owner->Version);
1814
	}
1815
1816
	/**
1817
	 * Check if this record exists on live
1818
	 *
1819
	 * @return bool
1820
	 */
1821
	public function isPublished() {
1822
		if(!$this->owner->isInDB()) {
1823
			return false;
1824
		}
1825
1826
		$table = ClassInfo::baseDataClass($this->owner->class) . '_' . self::get_live_stage();
1827
		$result = DB::prepared_query(
1828
			"SELECT COUNT(*) FROM \"{$table}\" WHERE \"{$table}\".\"ID\" = ?",
1829
			array($this->owner->ID)
1830
		);
1831
		return (bool)$result->value();
1832
	}
1833
1834
	/**
1835
	 * Check if this record exists on the draft stage
1836
	 *
1837
	 * @return bool
1838
	 */
1839
	public function isOnDraft() {
1840
		if(!$this->owner->isInDB()) {
1841
			return false;
1842
		}
1843
1844
		$table = ClassInfo::baseDataClass($this->owner->class);
1845
		$result = DB::prepared_query(
1846
			"SELECT COUNT(*) FROM \"{$table}\" WHERE \"{$table}\".\"ID\" = ?",
1847
			array($this->owner->ID)
1848
		);
1849
		return (bool)$result->value();
1850
	}
1851
1852
1853
1854
	/**
1855
	 * Return the equivalent of a DataList::create() call, querying the latest
1856
	 * version of each record stored in the (class)_versions tables.
1857
	 *
1858
	 * In particular, this will query deleted records as well as active ones.
1859
	 *
1860
	 * @param string $class
1861
	 * @param string $filter
1862
	 * @param string $sort
1863
	 * @return DataList
1864
	 */
1865
	public static function get_including_deleted($class, $filter = "", $sort = "") {
1866
		$list = DataList::create($class)
1867
			->where($filter)
1868
			->sort($sort)
1869
			->setDataQueryParam("Versioned.mode", "latest_versions");
1870
1871
		return $list;
1872
	}
1873
1874
	/**
1875
	 * Return the specific version of the given id.
1876
	 *
1877
	 * Caution: The record is retrieved as a DataObject, but saving back
1878
	 * modifications via write() will create a new version, rather than
1879
	 * modifying the existing one.
1880
	 *
1881
	 * @param string $class
1882
	 * @param int $id
1883
	 * @param int $version
1884
	 *
1885
	 * @return DataObject
1886
	 */
1887
	public static function get_version($class, $id, $version) {
1888
		$baseClass = ClassInfo::baseDataClass($class);
1889
		$list = DataList::create($baseClass)
1890
			->where(array(
1891
				"\"{$baseClass}\".\"RecordID\"" => $id,
1892
				"\"{$baseClass}\".\"Version\"" => $version
1893
			))
1894
			->setDataQueryParam("Versioned.mode", 'all_versions');
1895
1896
		return $list->First();
1897
	}
1898
1899
	/**
1900
	 * Return a list of all versions for a given id.
1901
	 *
1902
	 * @param string $class
1903
	 * @param int $id
1904
	 *
1905
	 * @return DataList
1906
	 */
1907
	public static function get_all_versions($class, $id) {
1908
		$list = DataList::create($class)
1909
			->filter('ID', $id)
1910
			->setDataQueryParam('Versioned.mode', 'all_versions');
1911
1912
		return $list;
1913
	}
1914
1915
	/**
1916
	 * @param array $labels
1917
	 */
1918
	public function updateFieldLabels(&$labels) {
1919
		$labels['Versions'] = _t('Versioned.has_many_Versions', 'Versions', 'Past Versions of this record');
1920
	}
1921
1922
	/**
1923
	 * @param FieldList
1924
	 */
1925
	public function updateCMSFields(FieldList $fields) {
1926
		// remove the version field from the CMS as this should be left
1927
		// entirely up to the extension (not the cms user).
1928
		$fields->removeByName('Version');
1929
	}
1930
1931
	/**
1932
	 * Ensure version ID is reset to 0 on duplicate
1933
	 *
1934
	 * @param DataObject $source Record this was duplicated from
1935
	 * @param bool $doWrite
1936
	 */
1937
	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...
1938
		$this->owner->Version = 0;
1939
	}
1940
1941
	public function flushCache() {
1942
		self::$cache_versionnumber = array();
1943
	}
1944
1945
	/**
1946
	 * Return a piece of text to keep DataObject cache keys appropriately specific.
1947
	 *
1948
	 * @return string
1949
	 */
1950
	public function cacheKeyComponent() {
1951
		return 'versionedmode-'.self::get_reading_mode();
1952
	}
1953
1954
	/**
1955
	 * Returns an array of possible stages.
1956
	 *
1957
	 * @return array
1958
	 */
1959
	public function getVersionedStages() {
1960
		return $this->stages;
1961
	}
1962
1963
	/**
1964
	 * @return string
1965
	 */
1966
	public function getDefaultStage() {
1967
		return $this->defaultStage;
1968
	}
1969
1970
	public static function get_template_global_variables() {
1971
		return array(
1972
			'CurrentReadingMode' => 'get_reading_mode'
1973
		);
1974
	}
1975
}
1976
1977
/**
1978
 * Represents a single version of a record.
1979
 *
1980
 * @package framework
1981
 * @subpackage model
1982
 *
1983
 * @see Versioned
1984
 */
1985
class Versioned_Version extends ViewableData {
1986
	/**
1987
	 * @var array
1988
	 */
1989
	protected $record;
1990
1991
	/**
1992
	 * @var DataObject
1993
	 */
1994
	protected $object;
1995
1996
	/**
1997
	 * Create a new version from a database row
1998
	 *
1999
	 * @param array $record
2000
	 */
2001
	public function __construct($record) {
2002
		$this->record = $record;
2003
		$record['ID'] = $record['RecordID'];
2004
		$className = $record['ClassName'];
2005
2006
		$this->object = ClassInfo::exists($className) ? new $className($record) : new DataObject($record);
2007
		$this->failover = $this->object;
2008
2009
		parent::__construct();
2010
	}
2011
2012
	/**
2013
	 * Either 'published' if published, or 'internal' if not.
2014
	 *
2015
	 * @return string
2016
	 */
2017
	public function PublishedClass() {
2018
		return $this->record['WasPublished'] ? 'published' : 'internal';
2019
	}
2020
2021
	/**
2022
	 * Author of this DataObject
2023
	 *
2024
	 * @return Member
2025
	 */
2026
	public function Author() {
2027
		return Member::get()->byId($this->record['AuthorID']);
2028
	}
2029
2030
	/**
2031
	 * Member object of the person who last published this record
2032
	 *
2033
	 * @return Member
2034
	 */
2035
	public function Publisher() {
2036
		if (!$this->record['WasPublished']) {
2037
			return null;
2038
		}
2039
2040
		return Member::get()->byId($this->record['PublisherID']);
2041
	}
2042
2043
	/**
2044
	 * True if this record is published via publish() method
2045
	 *
2046
	 * @return boolean
2047
	 */
2048
	public function Published() {
2049
		return !empty($this->record['WasPublished']);
2050
	}
2051
2052
	/**
2053
	 * Traverses to a field referenced by relationships between data objects, returning the value
2054
	 * The path to the related field is specified with dot separated syntax (eg: Parent.Child.Child.FieldName)
2055
	 *
2056
	 * @param $fieldName string
2057
	 * @return string | null - will return null on a missing value
2058
	 */
2059
	public function relField($fieldName) {
2060
		$component = $this;
2061
2062
		// We're dealing with relations here so we traverse the dot syntax
2063
		if(strpos($fieldName, '.') !== false) {
2064
			$relations = explode('.', $fieldName);
2065
			$fieldName = array_pop($relations);
2066
			foreach($relations as $relation) {
2067
				// Inspect $component for element $relation
2068
				if($component->hasMethod($relation)) {
2069
					// Check nested method
2070
						$component = $component->$relation();
2071
				} elseif($component instanceof SS_List) {
2072
					// Select adjacent relation from DataList
2073
						$component = $component->relation($relation);
2074
				} elseif($component instanceof DataObject
2075
					&& ($dbObject = $component->dbObject($relation))
2076
				) {
2077
					// Select db object
2078
					$component = $dbObject;
2079
				} else {
2080
					user_error("$relation is not a relation/field on ".get_class($component), E_USER_ERROR);
2081
				}
2082
			}
2083
		}
2084
2085
		// Bail if the component is null
2086
		if(!$component) {
2087
			return null;
2088
		}
2089
			if ($component->hasMethod($fieldName)) {
2090
				return $component->$fieldName();
2091
			}
2092
			return $component->$fieldName;
2093
		}
2094
	}
2095