Completed
Push — master ( 2fdc96...4f1f24 )
by Damian
12:09
created

Versioned::canArchive()   C

Complexity

Conditions 7
Paths 11

Size

Total Lines 32
Code Lines 15

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 32
rs 6.7272
cc 7
eloc 15
nc 11
nop 1
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
 *
12
 * @package framework
13
 * @subpackage model
14
 */
15
class Versioned extends DataExtension implements TemplateGlobalProvider {
16
	/**
17
	 * An array of possible stages.
18
	 * @var array
19
	 */
20
	protected $stages;
21
22
	/**
23
	 * The 'default' stage.
24
	 * @var string
25
	 */
26
	protected $defaultStage;
27
28
	/**
29
	 * The 'live' stage.
30
	 * @var string
31
	 */
32
	protected $liveStage;
33
34
	/**
35
	 * The default reading mode
36
	 */
37
	const DEFAULT_MODE = 'Stage.Live';
38
39
	/**
40
	 * A version that a DataObject should be when it is 'migrating',
41
	 * that is, when it is in the process of moving from one stage to another.
42
	 * @var string
43
	 */
44
	public $migratingVersion;
45
46
	/**
47
	 * A cache used by get_versionnumber_by_stage().
48
	 * Clear through {@link flushCache()}.
49
	 *
50
	 * @var array
51
	 */
52
	protected static $cache_versionnumber;
53
54
	/**
55
	 * @var string
56
	 */
57
	protected static $reading_mode = null;
58
59
	/**
60
	 * @var Boolean Flag which is temporarily changed during the write() process
61
	 * to influence augmentWrite() behaviour. If set to TRUE, no new version will be created
62
	 * for the following write. Needs to be public as other classes introspect this state
63
	 * during the write process in order to adapt to this versioning behaviour.
64
	 */
65
	public $_nextWriteWithoutVersion = false;
66
67
	/**
68
	 * Additional database columns for the new
69
	 * "_versions" table. Used in {@link augmentDatabase()}
70
	 * and all Versioned calls extending or creating
71
	 * SELECT statements.
72
	 *
73
	 * @var array $db_for_versions_table
74
	 */
75
	private static $db_for_versions_table = array(
76
		"RecordID" => "Int",
77
		"Version" => "Int",
78
		"WasPublished" => "Boolean",
79
		"AuthorID" => "Int",
80
		"PublisherID" => "Int"
81
	);
82
83
	/**
84
	 * @var array
85
	 */
86
	private static $db = array(
87
		'Version' => 'Int'
88
	);
89
90
	/**
91
	 * Used to enable or disable the prepopulation of the version number cache.
92
	 * Defaults to true.
93
	 *
94
	 * @var boolean
95
	 */
96
	private static $prepopulate_versionnumber_cache = true;
97
98
	/**
99
	 * Keep track of the archive tables that have been created.
100
	 *
101
	 * @var array
102
	 */
103
	private static $archive_tables = array();
104
105
	/**
106
	 * Additional database indexes for the new
107
	 * "_versions" table. Used in {@link augmentDatabase()}.
108
	 *
109
	 * @var array $indexes_for_versions_table
110
	 */
111
	private static $indexes_for_versions_table = array(
112
		'RecordID_Version' => '("RecordID","Version")',
113
		'RecordID' => true,
114
		'Version' => true,
115
		'AuthorID' => true,
116
		'PublisherID' => true,
117
	);
118
119
120
	/**
121
	 * An array of DataObject extensions that may require versioning for extra tables
122
	 * The array value is a set of suffixes to form these table names, assuming a preceding '_'.
123
	 * E.g. if Extension1 creates a new table 'Class_suffix1'
124
	 * and Extension2 the tables 'Class_suffix2' and 'Class_suffix3':
125
	 *
126
	 * 	$versionableExtensions = array(
127
	 * 		'Extension1' => 'suffix1',
128
	 * 		'Extension2' => array('suffix2', 'suffix3'),
129
	 * 	);
130
	 *
131
	 * This can also be manipulated by updating the current loaded config
132
	 *
133
	 * SiteTree:
134
	 *   versionableExtensions:
135
	 *     - Extension1:
136
	 *       - suffix1
137
	 *       - suffix2
138
	 *     - Extension2:
139
	 *       - suffix1
140
	 *       - suffix2
141
	 *
142
	 * or programatically:
143
	 *
144
	 *  Config::inst()->update($this->owner->class, 'versionableExtensions',
145
	 *  array('Extension1' => 'suffix1', 'Extension2' => array('suffix2', 'suffix3')));
146
	 *
147
	 *
148
	 * Make sure your extension has a static $enabled-property that determines if it is
149
	 * processed by Versioned.
150
	 *
151
	 * @var array
152
	 */
153
	protected static $versionableExtensions = array('Translatable' => 'lang');
154
155
	/**
156
	 * Permissions necessary to view records outside of the live stage (e.g. archive / draft stage).
157
	 *
158
	 * @config
159
	 * @var array
160
	 */
161
	private static $non_live_permissions = array('CMS_ACCESS_LeftAndMain', 'CMS_ACCESS_CMSMain', 'VIEW_DRAFT_CONTENT');
162
163
	/**
164
	 * Reset static configuration variables to their default values.
165
	 */
166
	public static function reset() {
167
		self::$reading_mode = '';
168
169
		Session::clear('readingMode');
170
	}
171
172
	/**
173
	 * Construct a new Versioned object.
174
	 *
175
	 * @var array $stages The different stages the versioned object can be.
176
	 * The first stage is considered the 'default' stage, the last stage is
177
	 * considered the 'live' stage.
178
	 */
179
	public function __construct($stages = array('Stage','Live')) {
180
		parent::__construct();
181
182
		if(!is_array($stages)) {
183
			$stages = func_get_args();
184
		}
185
186
		$this->stages = $stages;
187
		$this->defaultStage = reset($stages);
188
		$this->liveStage = array_pop($stages);
189
	}
190
191
	/**
192
	 * Amend freshly created DataQuery objects with versioned-specific
193
	 * information.
194
	 *
195
	 * @param SQLSelect
196
	 * @param DataQuery
197
	 */
198
	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...
199
		$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...
200
201
		if($parts[0] == 'Archive') {
202
			$dataQuery->setQueryParam('Versioned.mode', 'archive');
203
			$dataQuery->setQueryParam('Versioned.date', $parts[1]);
204
205
		} else if($parts[0] == 'Stage' && $parts[1] != $this->defaultStage
206
				&& array_search($parts[1],$this->stages) !== false) {
207
208
			$dataQuery->setQueryParam('Versioned.mode', 'stage');
209
			$dataQuery->setQueryParam('Versioned.stage', $parts[1]);
210
		}
211
212
	}
213
214
	/**
215
	 * Augment the the SQLSelect that is created by the DataQuery
216
	 *
217
	 * @param SQLSelect $query
218
	 * @param DataQuery $dataQuery
219
	 * @throws InvalidArgumentException
220
	 */
221
	public function augmentSQL(SQLSelect $query, DataQuery $dataQuery = null) {
222
		if(!$dataQuery || !$dataQuery->getQueryParam('Versioned.mode')) {
223
			return;
224
		}
225
226
		$baseTable = ClassInfo::baseDataClass($dataQuery->dataClass());
227
228
		switch($dataQuery->getQueryParam('Versioned.mode')) {
229
		// Reading a specific data from the archive
230
		case 'archive':
231
			$date = $dataQuery->getQueryParam('Versioned.date');
232
			foreach($query->getFrom() as $table => $dummy) {
233
				if(!DB::get_schema()->hasTable($table . '_versions')) {
234
					continue;
235
				}
236
237
				$query->renameTable($table, $table . '_versions');
238
				$query->replaceText("\"{$table}_versions\".\"ID\"", "\"{$table}_versions\".\"RecordID\"");
239
				$query->replaceText("`{$table}_versions`.`ID`", "`{$table}_versions`.`RecordID`");
240
241
				// Add all <basetable>_versions columns
242
				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...
243
					$query->selectField(sprintf('"%s_versions"."%s"', $baseTable, $name), $name);
244
				}
245
				$query->selectField(sprintf('"%s_versions"."%s"', $baseTable, 'RecordID'), "ID");
246
247
				if($table != $baseTable) {
248
					$query->addWhere("\"{$table}_versions\".\"Version\" = \"{$baseTable}_versions\".\"Version\"");
249
				}
250
			}
251
			// Link to the version archived on that date
252
			$query->addWhere(array(
253
				"\"{$baseTable}_versions\".\"Version\" IN
254
				(SELECT LatestVersion FROM
255
					(SELECT
256
						\"{$baseTable}_versions\".\"RecordID\",
257
						MAX(\"{$baseTable}_versions\".\"Version\") AS LatestVersion
258
						FROM \"{$baseTable}_versions\"
259
						WHERE \"{$baseTable}_versions\".\"LastEdited\" <= ?
260
						GROUP BY \"{$baseTable}_versions\".\"RecordID\"
261
					) AS \"{$baseTable}_versions_latest\"
262
					WHERE \"{$baseTable}_versions_latest\".\"RecordID\" = \"{$baseTable}_versions\".\"RecordID\"
263
				)" => $date
264
			));
265
			break;
266
267
		// Reading a specific stage (Stage or Live)
268
		case 'stage':
269
			$stage = $dataQuery->getQueryParam('Versioned.stage');
270
			if($stage && ($stage != $this->defaultStage)) {
271
				foreach($query->getFrom() as $table => $dummy) {
272
					// Only rewrite table names that are actually part of the subclass tree
273
					// This helps prevent rewriting of other tables that get joined in, in
274
					// particular, many_many tables
275
					if(class_exists($table) && ($table == $this->owner->class
276
							|| is_subclass_of($table, $this->owner->class)
277
							|| is_subclass_of($this->owner->class, $table))) {
278
						$query->renameTable($table, $table . '_' . $stage);
279
					}
280
				}
281
			}
282
			break;
283
284
		// Reading a specific stage, but only return items that aren't in any other stage
285
		case 'stage_unique':
286
			$stage = $dataQuery->getQueryParam('Versioned.stage');
287
288
			// Recurse to do the default stage behavior (must be first, we rely on stage renaming happening before
289
			// below)
290
			$dataQuery->setQueryParam('Versioned.mode', 'stage');
291
			$this->augmentSQL($query, $dataQuery);
292
			$dataQuery->setQueryParam('Versioned.mode', 'stage_unique');
293
294
			// Now exclude any ID from any other stage. Note that we double rename to avoid the regular stage rename
295
			// renaming all subquery references to be Versioned.stage
296
			foreach($this->stages as $excluding) {
297
				if ($excluding == $stage) continue;
298
299
				$tempName = 'ExclusionarySource_'.$excluding;
300
				$excludingTable = $baseTable . ($excluding && $excluding != $this->defaultStage ? "_$excluding" : '');
301
302
				$query->addWhere('"'.$baseTable.'"."ID" NOT IN (SELECT "ID" FROM "'.$tempName.'")');
303
				$query->renameTable($tempName, $excludingTable);
304
			}
305
			break;
306
307
		// Return all version instances
308
		case 'all_versions':
309
		case 'latest_versions':
310
			foreach($query->getFrom() as $alias => $join) {
311
				if($alias != $baseTable) {
312
					$query->setJoinFilter($alias, "\"$alias\".\"RecordID\" = \"{$baseTable}_versions\".\"RecordID\""
313
						. " AND \"$alias\".\"Version\" = \"{$baseTable}_versions\".\"Version\"");
314
				}
315
				$query->renameTable($alias, $alias . '_versions');
316
			}
317
318
			// Add all <basetable>_versions columns
319
			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...
320
				$query->selectField(sprintf('"%s_versions"."%s"', $baseTable, $name), $name);
321
			}
322
323
			// Alias the record ID as the row ID
324
			$query->selectField(sprintf('"%s_versions"."%s"', $baseTable, 'RecordID'), "ID");
325
326
			// Ensure that any sort order referring to this ID is correctly aliased
327
			$orders = $query->getOrderBy();
328
			foreach($orders as $order => $dir) {
329
				if($order === "\"$baseTable\".\"ID\"") {
330
					unset($orders[$order]);
331
					$orders["\"{$baseTable}_versions\".\"RecordID\""] = $dir;
332
				}
333
			}
334
			$query->setOrderBy($orders);
335
336
			// latest_version has one more step
337
			// Return latest version instances, regardless of whether they are on a particular stage
338
			// This provides "show all, including deleted" functonality
339
			if($dataQuery->getQueryParam('Versioned.mode') == 'latest_versions') {
340
				$query->addWhere(
341
					"\"{$alias}_versions\".\"Version\" IN
0 ignored issues
show
Bug introduced by
The variable $alias seems to be defined by a foreach iteration on line 310. Are you sure the iterator is never empty, otherwise this variable is not defined?

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

foreach ($a as $b) {
}

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


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

// $b is now guaranteed to be defined here.
Loading history...
342
					(SELECT LatestVersion FROM
343
						(SELECT
344
							\"{$alias}_versions\".\"RecordID\",
345
							MAX(\"{$alias}_versions\".\"Version\") AS LatestVersion
346
							FROM \"{$alias}_versions\"
347
							GROUP BY \"{$alias}_versions\".\"RecordID\"
348
						) AS \"{$alias}_versions_latest\"
349
						WHERE \"{$alias}_versions_latest\".\"RecordID\" = \"{$alias}_versions\".\"RecordID\"
350
					)");
351
			} else {
352
				// If all versions are requested, ensure that records are sorted by this field
353
				$query->addOrderBy(sprintf('"%s_versions"."%s"', $baseTable, 'Version'));
354
			}
355
			break;
356
		default:
357
			throw new InvalidArgumentException("Bad value for query parameter Versioned.mode: "
358
				. $dataQuery->getQueryParam('Versioned.mode'));
359
		}
360
	}
361
362
	/**
363
	 * For lazy loaded fields requiring extra sql manipulation, ie versioning.
364
	 *
365
	 * @param SQLSelect $query
366
	 * @param DataQuery $dataQuery
367
	 * @param DataObject $dataObject
368
	 */
369
	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...
370
		// The VersionedMode local variable ensures that this decorator only applies to
371
		// queries that have originated from the Versioned object, and have the Versioned
372
		// metadata set on the query object. This prevents regular queries from
373
		// accidentally querying the *_versions tables.
374
		$versionedMode = $dataObject->getSourceQueryParam('Versioned.mode');
375
		$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...
376
		$modesToAllowVersioning = array('all_versions', 'latest_versions', 'archive');
377
		if(
378
			!empty($dataObject->Version) &&
379
			(!empty($versionedMode) && in_array($versionedMode,$modesToAllowVersioning))
380
		) {
381
			$dataQuery->where("\"$dataClass\".\"RecordID\" = " . $dataObject->ID);
382
			$dataQuery->where("\"$dataClass\".\"Version\" = " . $dataObject->Version);
383
			$dataQuery->setQueryParam('Versioned.mode', 'all_versions');
384
		} else {
385
			// Same behaviour as in DataObject->loadLazyFields
386
			$dataQuery->where("\"$dataClass\".\"ID\" = {$dataObject->ID}")->limit(1);
387
		}
388
	}
389
390
391
	/**
392
	 * Called by {@link SapphireTest} when the database is reset.
393
	 *
394
	 * @todo Reduce the coupling between this and SapphireTest, somehow.
395
	 */
396
	public static function on_db_reset() {
397
		// Drop all temporary tables
398
		$db = DB::get_conn();
399
		foreach(self::$archive_tables as $tableName) {
400
			if(method_exists($db, 'dropTable')) $db->dropTable($tableName);
401
			else $db->query("DROP TABLE \"$tableName\"");
402
		}
403
404
		// Remove references to them
405
		self::$archive_tables = array();
406
	}
407
408
	public function augmentDatabase() {
409
		$classTable = $this->owner->class;
410
411
		$isRootClass = ($this->owner->class == ClassInfo::baseDataClass($this->owner->class));
412
413
		// Build a list of suffixes whose tables need versioning
414
		$allSuffixes = array();
415
		$versionableExtensions = $this->owner->config()->versionableExtensions;
0 ignored issues
show
Documentation introduced by
The property versionableExtensions does not exist on object<Config_ForClass>. 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...
416
		if(count($versionableExtensions)){
417
			foreach ($versionableExtensions as $versionableExtension => $suffixes) {
418
				if ($this->owner->hasExtension($versionableExtension)) {
419
					$allSuffixes = array_merge($allSuffixes, (array)$suffixes);
420
					foreach ((array)$suffixes as $suffix) {
421
						$allSuffixes[$suffix] = $versionableExtension;
422
					}
423
				}
424
			}
425
		}
426
427
		// Add the default table with an empty suffix to the list (table name = class name)
428
		array_push($allSuffixes,'');
429
430
		foreach ($allSuffixes as $key => $suffix) {
431
			// check that this is a valid suffix
432
			if (!is_int($key)) continue;
433
434
			if ($suffix) $table = "{$classTable}_$suffix";
435
			else $table = $classTable;
436
437
			$fields = DataObject::database_fields($this->owner->class);
438
			unset($fields['ID']);
439
			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...
440
				$options = Config::inst()->get($this->owner->class, 'create_table_options', Config::FIRST_SET);
441
				$indexes = $this->owner->databaseIndexes();
442
				if ($suffix && ($ext = $this->owner->getExtensionInstance($allSuffixes[$suffix]))) {
443
					if (!$ext->isVersionedTable($table)) continue;
444
					$ext->setOwner($this->owner);
445
					$fields = $ext->fieldsInExtraTables($suffix);
446
					$ext->clearOwner();
447
					$indexes = $fields['indexes'];
448
					$fields = $fields['db'];
449
				}
450
451
				// Create tables for other stages
452
				foreach($this->stages as $stage) {
453
					// Extra tables for _Live, etc.
454
					// Change unique indexes to 'index'.  Versioned tables may run into unique indexing difficulties
455
					// otherwise.
456
					$indexes = $this->uniqueToIndex($indexes);
457
					if($stage != $this->defaultStage) {
458
						DB::require_table("{$table}_$stage", $fields, $indexes, false, $options);
459
					}
460
461
					// Version fields on each root table (including Stage)
462
					/*
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...
463
					if($isRootClass) {
464
						$stageTable = ($stage == $this->defaultStage) ? $table : "{$table}_$stage";
465
						$parts=Array('datatype'=>'int', 'precision'=>11, 'null'=>'not null', 'default'=>(int)0);
466
						$values=Array('type'=>'int', 'parts'=>$parts);
467
						DB::requireField($stageTable, 'Version', $values);
468
					}
469
					*/
470
				}
471
472
				if($isRootClass) {
473
					// Create table for all versions
474
					$versionFields = array_merge(
475
						Config::inst()->get('Versioned', 'db_for_versions_table'),
476
						(array)$fields
477
					);
478
479
					$versionIndexes = array_merge(
480
						Config::inst()->get('Versioned', 'indexes_for_versions_table'),
481
						(array)$indexes
482
					);
483
				} else {
484
					// Create fields for any tables of subclasses
485
					$versionFields = array_merge(
486
						array(
487
							"RecordID" => "Int",
488
							"Version" => "Int",
489
						),
490
						(array)$fields
491
					);
492
493
					//Unique indexes will not work on versioned tables, so we'll convert them to standard indexes:
494
					$indexes = $this->uniqueToIndex($indexes);
495
					$versionIndexes = array_merge(
496
						array(
497
							'RecordID_Version' => array('type' => 'unique', 'value' => '"RecordID","Version"'),
498
							'RecordID' => true,
499
							'Version' => true,
500
						),
501
						(array)$indexes
502
					);
503
				}
504
505
				if(DB::get_schema()->hasTable("{$table}_versions")) {
506
					// Fix data that lacks the uniqueness constraint (since this was added later and
507
					// bugs meant that the constraint was validated)
508
					$duplications = DB::query("SELECT MIN(\"ID\") AS \"ID\", \"RecordID\", \"Version\"
509
						FROM \"{$table}_versions\" GROUP BY \"RecordID\", \"Version\"
510
						HAVING COUNT(*) > 1");
511
512
					foreach($duplications as $dup) {
513
						DB::alteration_message("Removing {$table}_versions duplicate data for "
514
							."{$dup['RecordID']}/{$dup['Version']}" ,"deleted");
515
						DB::prepared_query(
516
							"DELETE FROM \"{$table}_versions\" WHERE \"RecordID\" = ?
517
							AND \"Version\" = ? AND \"ID\" != ?",
518
							array($dup['RecordID'], $dup['Version'], $dup['ID'])
519
						);
520
					}
521
522
					// Remove junk which has no data in parent classes. Only needs to run the following
523
					// when versioned data is spread over multiple tables
524
					if(!$isRootClass && ($versionedTables = ClassInfo::dataClassesFor($table))) {
525
526
						foreach($versionedTables as $child) {
527
							if($table === $child) break; // only need subclasses
528
529
							// Select all orphaned version records
530
							$orphanedQuery = SQLSelect::create()
531
								->selectField("\"{$table}_versions\".\"ID\"")
532
								->setFrom("\"{$table}_versions\"");
533
534
							// If we have a parent table limit orphaned records
535
							// to only those that exist in this
536
							if(DB::get_schema()->hasTable("{$child}_versions")) {
537
								$orphanedQuery
538
									->addLeftJoin(
539
										"{$child}_versions",
540
										"\"{$child}_versions\".\"RecordID\" = \"{$table}_versions\".\"RecordID\"
541
										AND \"{$child}_versions\".\"Version\" = \"{$table}_versions\".\"Version\""
542
									)
543
									->addWhere("\"{$child}_versions\".\"ID\" IS NULL");
544
							}
545
546
							$count = $orphanedQuery->count();
547
							if($count > 0) {
548
								DB::alteration_message("Removing {$count} orphaned versioned records", "deleted");
549
								$ids = $orphanedQuery->execute()->column();
550
								foreach($ids as $id) {
551
									DB::prepared_query(
552
										"DELETE FROM \"{$table}_versions\" WHERE \"ID\" = ?",
553
										array($id)
554
									);
555
								}
556
							}
557
						}
558
					}
559
				}
560
561
				DB::require_table("{$table}_versions", $versionFields, $versionIndexes, true, $options);
562
			} else {
563
				DB::dont_require_table("{$table}_versions");
564
				foreach($this->stages as $stage) {
565
					if($stage != $this->defaultStage) DB::dont_require_table("{$table}_$stage");
566
				}
567
			}
568
		}
569
	}
570
571
	/**
572
	 * Helper for augmentDatabase() to find unique indexes and convert them to non-unique
573
	 *
574
	 * @param array $indexes The indexes to convert
575
	 * @return array $indexes
576
	 */
577
	private function uniqueToIndex($indexes) {
578
		$unique_regex = '/unique/i';
579
		$results = array();
580
		foreach ($indexes as $key => $index) {
581
			$results[$key] = $index;
582
583
			// support string descriptors
584
			if (is_string($index)) {
585
				if (preg_match($unique_regex, $index)) {
586
					$results[$key] = preg_replace($unique_regex, 'index', $index);
587
				}
588
			}
589
590
			// canonical, array-based descriptors
591
			elseif (is_array($index)) {
592
				if (strtolower($index['type']) == 'unique') {
593
					$results[$key]['type'] = 'index';
594
				}
595
			}
596
		}
597
		return $results;
598
	}
599
600
	/**
601
	 * Generates a ($table)_version DB manipulation and injects it into the current $manipulation
602
	 *
603
	 * @param array $manipulation Source manipulation data
604
	 * @param string $table Name of table
605
	 * @param int $recordID ID of record to version
606
	 */
607
	protected function augmentWriteVersioned(&$manipulation, $table, $recordID) {
608
		$baseDataClass = ClassInfo::baseDataClass($table);
609
610
		// Set up a new entry in (table)_versions
611
		$newManipulation = array(
612
			"command" => "insert",
613
			"fields" => isset($manipulation[$table]['fields']) ? $manipulation[$table]['fields'] : null
614
		);
615
616
		// Add any extra, unchanged fields to the version record.
617
		$data = DB::prepared_query("SELECT * FROM \"$table\" WHERE \"ID\" = ?", array($recordID))->record();
618
619
		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...
620
			$fields = DataObject::database_fields($table);
621
622
			if (is_array($fields)) {
623
				$data = array_intersect_key($data, $fields);
624
625
				foreach ($data as $k => $v) {
626
					if (!isset($newManipulation['fields'][$k])) {
627
						$newManipulation['fields'][$k] = $v;
628
					}
629
				}
630
			}
631
		}
632
633
		// Ensure that the ID is instead written to the RecordID field
634
		$newManipulation['fields']['RecordID'] = $recordID;
635
		unset($newManipulation['fields']['ID']);
636
637
		// Generate next version ID to use
638
		$nextVersion = 0;
639
		if($recordID) {
640
			$nextVersion = DB::prepared_query("SELECT MAX(\"Version\") + 1
641
				FROM \"{$baseDataClass}_versions\" WHERE \"RecordID\" = ?",
642
				array($recordID)
643
			)->value();
644
		}
645
		$nextVersion = $nextVersion ?: 1;
646
647
		if($table === $baseDataClass) {
648
		// Write AuthorID for baseclass
649
			$userID = (Member::currentUser()) ? Member::currentUser()->ID : 0;
650
			$newManipulation['fields']['AuthorID'] = $userID;
651
652
			// Update main table version if not previously known
653
			$manipulation[$table]['fields']['Version'] = $nextVersion;
654
		}
655
656
		// Update _versions table manipulation
657
		$newManipulation['fields']['Version'] = $nextVersion;
658
		$manipulation["{$table}_versions"] = $newManipulation;
659
	}
660
661
	/**
662
	 * Rewrite the given manipulation to update the selected (non-default) stage
663
	 *
664
	 * @param array $manipulation Source manipulation data
665
	 * @param string $table Name of table
666
	 * @param int $recordID ID of record to version
667
	 */
668
	protected function augmentWriteStaged(&$manipulation, $table, $recordID) {
669
		// If the record has already been inserted in the (table), get rid of it.
670
		if($manipulation[$table]['command'] == 'insert') {
671
			DB::prepared_query(
672
				"DELETE FROM \"{$table}\" WHERE \"ID\" = ?",
673
				array($recordID)
674
			);
675
		}
676
677
		$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...
678
		$manipulation[$newTable] = $manipulation[$table];
679
		unset($manipulation[$table]);
680
	}
681
682
683
	public function augmentWrite(&$manipulation) {
684
		// get Version number from base data table on write
685
		$version = null;
686
		$baseDataClass = ClassInfo::baseDataClass($this->owner->class);
687
		if(isset($manipulation[$baseDataClass]['fields'])) {
688
			if ($this->migratingVersion) {
689
				$manipulation[$baseDataClass]['fields']['Version'] = $this->migratingVersion;
690
			}
691
			if (isset($manipulation[$baseDataClass]['fields']['Version'])) {
692
				$version = $manipulation[$baseDataClass]['fields']['Version'];
693
			}
694
		}
695
696
		// Update all tables
697
		$tables = array_keys($manipulation);
698
		foreach($tables as $table) {
699
700
			// Make sure that the augmented write is being applied to a table that can be versioned
701
			if( !$this->canBeVersioned($table) ) {
702
				unset($manipulation[$table]);
703
				continue;
704
			}
705
706
			// Get ID field
707
			$id = $manipulation[$table]['id']
708
				? $manipulation[$table]['id']
709
				: $manipulation[$table]['fields']['ID'];
710
			if(!$id) {
711
				user_error("Couldn't find ID in " . var_export($manipulation[$table], true), E_USER_ERROR);
712
			}
713
714
			if($version < 0 || $this->_nextWriteWithoutVersion) {
715
				// Putting a Version of -1 is a signal to leave the version table alone, despite their being no version
716
				unset($manipulation[$table]['fields']['Version']);
717
			} elseif(empty($version)) {
718
				// If we haven't got a version #, then we're creating a new version.
719
				// Otherwise, we're just copying a version to another table
720
				$this->augmentWriteVersioned($manipulation, $table, $id);
721
			}
722
723
			// Remove "Version" column from subclasses of baseDataClass
724
			if(!$this->hasVersionField($table)) {
725
				unset($manipulation[$table]['fields']['Version']);
726
			}
727
728
			// Grab a version number - it should be the same across all tables.
729
			if(isset($manipulation[$table]['fields']['Version'])) {
730
				$thisVersion = $manipulation[$table]['fields']['Version'];
731
			}
732
733
			// If we're editing Live, then use (table)_Live instead of (table)
734
			if(
735
				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...
736
				&& 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...
737
				&& 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...
738
			) {
739
				$this->augmentWriteStaged($manipulation, $table, $id);
740
			}
741
		}
742
743
		// Clear the migration flag
744
		if($this->migratingVersion) {
745
			$this->migrateVersion(null);
746
		}
747
748
		// Add the new version # back into the data object, for accessing
749
		// after this write
750
		if(isset($thisVersion)) {
751
			$this->owner->Version = str_replace("'","", $thisVersion);
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...
752
		}
753
	}
754
755
	/**
756
	 * Perform a write without affecting the version table.
757
	 * On objects without versioning.
758
	 *
759
	 * @return int The ID of the record
760
	 */
761
	public function writeWithoutVersion() {
762
		$this->_nextWriteWithoutVersion = true;
763
764
		return $this->owner->write();
765
	}
766
767
	/**
768
	 *
769
	 */
770
	public function onAfterWrite() {
771
		$this->_nextWriteWithoutVersion = false;
772
	}
773
774
	/**
775
	 * If a write was skipped, then we need to ensure that we don't leave a
776
	 * migrateVersion() value lying around for the next write.
777
	 */
778
	public function onAfterSkippedWrite() {
779
		$this->migrateVersion(null);
780
	}
781
782
	/**
783
	 * This function should return true if the current user can publish this record.
784
	 * It can be overloaded to customise the security model for an application.
785
	 *
786
	 * Denies permission if any of the following conditions is true:
787
	 * - canPublish() on any extension returns false
788
	 * - canEdit() returns false
789
	 *
790
	 * @param Member $member
791
	 * @return bool True if the current user can publish this record.
792
	 */
793
	public function canPublish($member = null) {
794
		// Skip if invoked by extendedCan()
795
		if(func_num_args() > 4) {
796
			return null;
797
		}
798
799
		if(!$member) {
800
			$member = Member::currentUser();
801
		}
802
803
		if(Permission::checkMember($member, "ADMIN")) {
804
			return true;
805
		}
806
807
		// Standard mechanism for accepting permission changes from extensions
808
		$extended = $this->owner->extendedCan('canPublish', $member);
809
		if($extended !== null) {
810
			return $extended;
811
		}
812
813
		// Default to relying on edit permission
814
		return $this->owner->canEdit($member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by \Member::currentUser() on line 800 can also be of type object<DataObject>; however, DataObject::canEdit() 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...
815
	}
816
817
	/**
818
	 * Check if the current user can delete this record from live
819
	 *
820
	 * @param null $member
821
	 * @return mixed
822
	 */
823
	public function canUnpublish($member = null) {
824
		// Skip if invoked by extendedCan()
825
		if(func_num_args() > 4) {
826
			return null;
827
		}
828
829
		if(!$member) {
830
			$member = Member::currentUser();
831
		}
832
833
		if(Permission::checkMember($member, "ADMIN")) {
834
			return true;
835
		}
836
837
		// Standard mechanism for accepting permission changes from extensions
838
		$extended = $this->owner->extendedCan('canUnpublish', $member);
839
		if($extended !== null) {
840
			return $extended;
841
		}
842
843
		// Default to relying on canPublish
844
		return $this->owner->canPublish($member);
845
	}
846
847
	/**
848
	 * Check if the current user is allowed to archive this record.
849
	 * If extended, ensure that both canDelete and canUnpublish are extended also
850
	 *
851
	 * @param Member $member
852
	 * @return bool
853
	 */
854
	public function canArchive($member = null) {
855
		// Skip if invoked by extendedCan()
856
		if(func_num_args() > 4) {
857
			return null;
858
		}
859
860
		if(!$member) {
861
            $member = Member::currentUser();
862
        }
863
864
		if(Permission::checkMember($member, "ADMIN")) {
865
			return true;
866
		}
867
868
		// Standard mechanism for accepting permission changes from extensions
869
		$extended = $this->owner->extendedCan('canArchive', $member);
870
		if($extended !== null) {
871
            return $extended;
872
        }
873
874
		// Check if this record can be deleted from stage
875
        if(!$this->owner->canDelete($member)) {
0 ignored issues
show
Bug introduced by
It seems like $member defined by \Member::currentUser() on line 861 can also be of type object<DataObject>; however, DataObject::canDelete() 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...
876
            return false;
877
        }
878
879
        // Check if we can delete from live
880
        if(!$this->owner->canUnpublish($member)) {
881
            return false;
882
        }
883
884
		return true;
885
	}
886
887
	/**
888
	 * Extend permissions to include additional security for objects that are not published to live.
889
	 *
890
	 * @param Member $member
891
	 * @return bool|null
892
	 */
893
	public function canView($member = null) {
894
		// Invoke default version-gnostic canView
895
		if ($this->owner->canViewVersioned($member) === false) {
896
			return false;
897
		}
898
	}
899
900
	/**
901
	 * Determine if there are any additional restrictions on this object for the given reading version.
902
	 *
903
	 * Override this in a subclass to customise any additional effect that Versioned applies to canView.
904
	 *
905
	 * This is expected to be called by canView, and thus is only responsible for denying access if
906
	 * the default canView would otherwise ALLOW access. Thus it should not be called in isolation
907
	 * as an authoritative permission check.
908
	 *
909
	 * This has the following extension points:
910
	 *  - canViewDraft is invoked if Mode = stage and Stage = stage
911
	 *  - canViewArchived is invoked if Mode = archive
912
	 *
913
	 * @param Member $member
914
	 * @return bool False is returned if the current viewing mode denies visibility
915
	 */
916
	public function canViewVersioned($member = null) {
917
		// Bypass when live stage
918
		$mode = $this->owner->getSourceQueryParam("Versioned.mode");
919
		$stage = $this->owner->getSourceQueryParam("Versioned.stage");
920
		if ($mode === 'stage' && $stage === static::get_live_stage()) {
921
			return true;
922
		}
923
924
		// Bypass if site is unsecured
925
		if (Session::get('unsecuredDraftSite')) {
926
			return true;
927
		}
928
929
		// Bypass if record doesn't have a live stage
930
		if(!in_array(static::get_live_stage(), $this->getVersionedStages())) {
931
			return true;
932
		}
933
934
		// If we weren't definitely loaded from live, and we can't view non-live content, we need to
935
		// check to make sure this version is the live version and so can be viewed
936
		$latestVersion = Versioned::get_versionnumber_by_stage($this->owner->class, 'Live', $this->owner->ID);
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...
937
		if ($latestVersion == $this->owner->Version) {
938
			// Even if this is loaded from a non-live stage, this is the live version
939
			return true;
940
		}
941
942
		// Extend versioned behaviour
943
		$extended = $this->owner->extendedCan('canViewNonLive', $member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 916 can be null; however, DataObject::extendedCan() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
944
		if($extended !== null) {
945
			return (bool)$extended;
946
		}
947
948
		// Fall back to default permission check
949
		$permissions = Config::inst()->get($this->owner->class, 'non_live_permissions', Config::FIRST_SET);
950
		$check = Permission::checkMember($member, $permissions);
951
		return (bool)$check;
952
	}
953
954
	/**
955
	 * Determines canView permissions for the latest version of this object on a specific stage.
956
	 * Usually the stage is read from {@link Versioned::current_stage()}.
957
	 *
958
	 * This method should be invoked by user code to check if a record is visible in the given stage.
959
	 *
960
	 * This method should not be called via ->extend('canViewStage'), but rather should be
961
	 * overridden in the extended class.
962
	 *
963
	 * @param string $stage
964
	 * @param Member $member
965
	 * @return bool
966
	 */
967
	public function canViewStage($stage = 'Live', $member = null) {
968
		$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...
969
		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...
970
971
		$versionFromStage = DataObject::get($this->owner->class)->byID($this->owner->ID);
972
973
		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...
974
		return $versionFromStage ? $versionFromStage->canView($member) : false;
975
	}
976
977
	/**
978
	 * Determine if a table is supporting the Versioned extensions (e.g.
979
	 * $table_versions does exists).
980
	 *
981
	 * @param string $table Table name
982
	 * @return boolean
983
	 */
984
	public function canBeVersioned($table) {
985
		return ClassInfo::exists($table)
986
			&& is_subclass_of($table, 'DataObject')
987
			&& DataObject::has_own_table($table);
988
	}
989
990
	/**
991
	 * Check if a certain table has the 'Version' field.
992
	 *
993
	 * @param string $table Table name
994
	 *
995
	 * @return boolean Returns false if the field isn't in the table, true otherwise
996
	 */
997
	public function hasVersionField($table) {
998
		$rPos = strrpos($table,'_');
999
1000
		if(($rPos !== false) && in_array(substr($table,$rPos), $this->stages)) {
1001
			$tableWithoutStage = substr($table,0,$rPos);
1002
		} else {
1003
			$tableWithoutStage = $table;
1004
		}
1005
1006
		return ('DataObject' == get_parent_class($tableWithoutStage));
1007
	}
1008
1009
	/**
1010
	 * @param string $table
1011
	 *
1012
	 * @return string
1013
	 */
1014
	public function extendWithSuffix($table) {
1015
		$owner = $this->owner;
1016
		$versionableExtensions = $owner->config()->versionableExtensions;
0 ignored issues
show
Documentation introduced by
The property versionableExtensions does not exist on object<Config_ForClass>. 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...
1017
1018
		if(count($versionableExtensions)){
1019
			foreach ($versionableExtensions as $versionableExtension => $suffixes) {
1020
				if ($owner->hasExtension($versionableExtension)) {
1021
					$ext = $owner->getExtensionInstance($versionableExtension);
1022
					$ext->setOwner($owner);
1023
					$table = $ext->extendWithSuffix($table);
1024
					$ext->clearOwner();
1025
				}
1026
			}
1027
		}
1028
1029
		return $table;
1030
	}
1031
1032
	/**
1033
	 * Get the latest published DataObject.
1034
	 *
1035
	 * @return DataObject
1036
	 */
1037
	public function latestPublished() {
1038
		// Get the root data object class - this will have the version field
1039
		$table1 = $this->owner->class;
1040
		while( ($p = get_parent_class($table1)) != "DataObject") $table1 = $p;
1041
1042
		$table2 = $table1 . "_$this->liveStage";
1043
1044
		return DB::prepared_query("SELECT \"$table1\".\"Version\" = \"$table2\".\"Version\" FROM \"$table1\"
1045
			 INNER JOIN \"$table2\" ON \"$table1\".\"ID\" = \"$table2\".\"ID\"
1046
			 WHERE \"$table1\".\"ID\" = ?",
1047
			array($this->owner->ID)
1048
		)->value();
1049
	}
1050
1051
	/**
1052
	 * Provides a simple doPublish action for Versioned dataobjects
1053
	 *
1054
	 * @return bool True if publish was successful
1055
	 */
1056
	public function doPublish() {
1057
		$owner = $this->owner;
1058
		$owner->invokeWithExtensions('onBeforePublish');
1059
		$owner->write();
1060
		$owner->publish("Stage", "Live");
1061
		$owner->invokeWithExtensions('onAfterPublish');
1062
		return true;
1063
	}
1064
1065
1066
1067
	/**
1068
	 * Removes the record from both live and stage
1069
	 *
1070
	 * @return bool Success
1071
	 */
1072
	public function doArchive() {
1073
		$owner = $this->owner;
1074
		$owner->invokeWithExtensions('onBeforeArchive', $this);
1075
1076
		if($owner->doUnpublish()) {
1077
			$owner->delete();
1078
			$owner->invokeWithExtensions('onAfterArchive', $this);
1079
1080
			return true;
1081
		}
1082
1083
		return false;
1084
	}
1085
1086
	/**
1087
	 * Removes this record from the live site
1088
	 *
1089
	 * @return bool Flag whether the unpublish was successful
1090
	 *
1091
	 * @uses SiteTreeExtension->onBeforeUnpublish()
1092
	 * @uses SiteTreeExtension->onAfterUnpublish()
1093
	 */
1094
	public function doUnpublish() {
1095
		$owner = $this->owner;
1096
		if(!$owner->isInDB()) {
1097
			return false;
1098
		}
1099
1100
		$owner->invokeWithExtensions('onBeforeUnpublish');
1101
1102
		$origStage = self::current_stage();
1103
		self::reading_stage(self::get_live_stage());
1104
1105
		// This way our ID won't be unset
1106
		$clone = clone $owner;
1107
		$clone->delete();
1108
1109
		self::reading_stage($origStage);
1110
1111
		// If we're on the draft site, then we can update the status.
1112
		// Otherwise, these lines will resurrect an inappropriate record
1113
		if(self::current_stage() != self::get_live_stage() && $this->isOnDraft()) {
1114
			$owner->write();
1115
		}
1116
1117
		$owner->invokeWithExtensions('onAfterUnpublish');
1118
1119
		return true;
1120
	}
1121
1122
	/**
1123
	 * Move a database record from one stage to the other.
1124
	 *
1125
	 * @param int|string $fromStage Place to copy from.  Can be either a stage name or a version number.
1126
	 * @param string $toStage Place to copy to.  Must be a stage name.
1127
	 * @param bool $createNewVersion Set this to true to create a new version number.
1128
	 * By default, the existing version number will be copied over.
1129
	 */
1130
	public function publish($fromStage, $toStage, $createNewVersion = false) {
1131
		$owner = $this->owner;
1132
		$owner->extend('onBeforeVersionedPublish', $fromStage, $toStage, $createNewVersion);
1133
1134
		$baseClass = ClassInfo::baseDataClass($owner->class);
1135
1136
		/** @var Versioned|DataObject $from */
1137
		if(is_numeric($fromStage)) {
1138
			$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...
1139
		} else {
1140
			$this->owner->flushCache();
1141
			$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...
1142
				"\"{$baseClass}\".\"ID\" = ?" => $owner->ID
1143
			));
1144
		}
1145
		if(!$from) {
1146
			throw new InvalidArgumentException("Can't find {$baseClass}#{$owner->ID} in stage {$fromStage}");
1147
		}
1148
1149
		$from->forceChange();
1150
		if($createNewVersion) {
1151
			// Clear version to be automatically created on write
1152
			$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...
1153
		} else {
1154
			$from->migrateVersion($from->Version);
1155
1156
			// Mark this version as having been published at some stage
1157
			$publisherID = isset(Member::currentUser()->ID) ? Member::currentUser()->ID : 0;
1158
			$extTable = $this->extendWithSuffix($baseClass);
1159
			DB::prepared_query("UPDATE \"{$extTable}_versions\"
1160
				SET \"WasPublished\" = ?, \"PublisherID\" = ?
1161
				WHERE \"RecordID\" = ? AND \"Version\" = ?",
1162
				array(1, $publisherID, $from->ID, $from->Version)
1163
			);
1164
		}
1165
1166
		// Change to new stage, write, and revert state
1167
		$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...
1168
		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...
1169
1170
		// Migrate stage prior to write
1171
		$from->setSourceQueryParam('Versioned.mode', 'stage');
1172
		$from->setSourceQueryParam('Versioned.stage', $toStage);
1173
1174
		$conn = DB::get_conn();
1175
		if(method_exists($conn, 'allowPrimaryKeyEditing')) {
1176
			$conn->allowPrimaryKeyEditing($baseClass, true);
1177
			$from->write();
1178
			$conn->allowPrimaryKeyEditing($baseClass, false);
1179
		} else {
1180
			$from->write();
1181
		}
1182
1183
		$from->destroy();
1184
1185
		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...
1186
1187
		$owner->extend('onAfterVersionedPublish', $fromStage, $toStage, $createNewVersion);
1188
	}
1189
1190
	/**
1191
	 * Set the migrating version.
1192
	 *
1193
	 * @param string $version The version.
1194
	 */
1195
	public function migrateVersion($version) {
1196
		$this->migratingVersion = $version;
1197
	}
1198
1199
	/**
1200
	 * Compare two stages to see if they're different.
1201
	 *
1202
	 * Only checks the version numbers, not the actual content.
1203
	 *
1204
	 * @param string $stage1 The first stage to check.
1205
	 * @param string $stage2
1206
	 */
1207
	public function stagesDiffer($stage1, $stage2) {
1208
		$table1 = $this->baseTable($stage1);
1209
		$table2 = $this->baseTable($stage2);
1210
1211
		if(!is_numeric($this->owner->ID)) {
1212
			return true;
1213
		}
1214
1215
		// We test for equality - if one of the versions doesn't exist, this
1216
		// will be false.
1217
1218
		// TODO: DB Abstraction: if statement here:
1219
		$stagesAreEqual = DB::prepared_query(
1220
			"SELECT CASE WHEN \"$table1\".\"Version\"=\"$table2\".\"Version\" THEN 1 ELSE 0 END
1221
			 FROM \"$table1\" INNER JOIN \"$table2\" ON \"$table1\".\"ID\" = \"$table2\".\"ID\"
1222
			 AND \"$table1\".\"ID\" = ?",
1223
			array($this->owner->ID)
1224
		)->value();
1225
1226
		return !$stagesAreEqual;
1227
	}
1228
1229
	/**
1230
	 * @param string $filter
1231
	 * @param string $sort
1232
	 * @param string $limit
1233
	 * @param string $join Deprecated, use leftJoin($table, $joinClause) instead
1234
	 * @param string $having
1235
	 */
1236
	public function Versions($filter = "", $sort = "", $limit = "", $join = "", $having = "") {
1237
		return $this->allVersions($filter, $sort, $limit, $join, $having);
1238
	}
1239
1240
	/**
1241
	 * Return a list of all the versions available.
1242
	 *
1243
	 * @param  string $filter
1244
	 * @param  string $sort
1245
	 * @param  string $limit
1246
	 * @param  string $join   Deprecated, use leftJoin($table, $joinClause) instead
1247
	 * @param  string $having
1248
	 */
1249
	public function allVersions($filter = "", $sort = "", $limit = "", $join = "", $having = "") {
1250
		// Make sure the table names are not postfixed (e.g. _Live)
1251
		$oldMode = self::get_reading_mode();
1252
		self::reading_stage('Stage');
1253
1254
		$list = DataObject::get(get_class($this->owner), $filter, $sort, $join, $limit);
1255
		if($having) $having = $list->having($having);
1256
1257
		$query = $list->dataQuery()->query();
1258
1259
		foreach($query->getFrom() as $table => $tableJoin) {
1260
			if(is_string($tableJoin) && $tableJoin[0] == '"') {
1261
				$baseTable = str_replace('"','',$tableJoin);
1262
			} elseif(is_string($tableJoin) && substr($tableJoin,0,5) != 'INNER') {
1263
				$query->setFrom(array(
1264
					$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...
1265
						. " AND \"$table\".\"Version\" = \"{$baseTable}_versions\".\"Version\""
1266
				));
1267
			}
1268
			$query->renameTable($table, $table . '_versions');
1269
		}
1270
1271
		// Add all <basetable>_versions columns
1272
		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...
1273
			$query->selectField(sprintf('"%s_versions"."%s"', $baseTable, $name), $name);
1274
		}
1275
1276
		$query->addWhere(array(
1277
			"\"{$baseTable}_versions\".\"RecordID\" = ?" => $this->owner->ID
1278
		));
1279
		$query->setOrderBy(($sort) ? $sort
1280
			: "\"{$baseTable}_versions\".\"LastEdited\" DESC, \"{$baseTable}_versions\".\"Version\" DESC");
1281
1282
		$records = $query->execute();
1283
		$versions = new ArrayList();
1284
1285
		foreach($records as $record) {
1286
			$versions->push(new Versioned_Version($record));
1287
		}
1288
1289
		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...
1290
		return $versions;
1291
	}
1292
1293
	/**
1294
	 * Compare two version, and return the diff between them.
1295
	 *
1296
	 * @param string $from The version to compare from.
1297
	 * @param string $to The version to compare to.
1298
	 *
1299
	 * @return DataObject
1300
	 */
1301
	public function compareVersions($from, $to) {
1302
		$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...
1303
		$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...
1304
1305
		$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 1303 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...
1306
1307
		return $diff->diffedData();
1308
	}
1309
1310
	/**
1311
	 * Return the base table - the class that directly extends DataObject.
1312
	 *
1313
	 * @return string
1314
	 */
1315
	public function baseTable($stage = null) {
1316
		$tableClasses = ClassInfo::dataClassesFor($this->owner->class);
1317
		$baseClass = array_shift($tableClasses);
1318
1319
		if(!$stage || $stage == $this->defaultStage) {
1320
			return $baseClass;
1321
		}
1322
1323
		return $baseClass . "_$stage";
1324
	}
1325
1326
	//-----------------------------------------------------------------------------------------------//
1327
1328
1329
	/**
1330
	 * Determine if the current user is able to set the given site stage / archive
1331
	 *
1332
	 * @param SS_HTTPRequest $request
1333
	 * @return bool
1334
	 */
1335
	public static function can_choose_site_stage($request) {
1336
		// Request is allowed if stage isn't being modified
1337
		if((!$request->getVar('stage') || $request->getVar('stage') === static::get_live_stage())
1338
			&& !$request->getVar('archiveDate')
1339
		) {
1340
			return true;
1341
		}
1342
1343
		// Check permissions with member ID in session.
1344
		$member = Member::currentUser();
1345
		$permissions = Config::inst()->get(get_called_class(), 'non_live_permissions');
1346
		return $member && Permission::checkMember($member, $permissions);
1347
	}
1348
1349
	/**
1350
	 * Choose the stage the site is currently on.
1351
	 *
1352
	 * If $_GET['stage'] is set, then it will use that stage, and store it in
1353
	 * the session.
1354
	 *
1355
	 * if $_GET['archiveDate'] is set, it will use that date, and store it in
1356
	 * the session.
1357
	 *
1358
	 * If neither of these are set, it checks the session, otherwise the stage
1359
	 * is set to 'Live'.
1360
	 */
1361
	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...
1362
		// Check any pre-existing session mode
1363
		$preexistingMode = Session::get('readingMode');
1364
1365
		// Determine the reading mode
1366
		if(isset($_GET['stage'])) {
1367
			$stage = ucfirst(strtolower($_GET['stage']));
1368
			if(!in_array($stage, array('Stage', 'Live'))) $stage = 'Live';
1369
			$mode = 'Stage.' . $stage;
1370
		} elseif (isset($_GET['archiveDate']) && strtotime($_GET['archiveDate'])) {
1371
			$mode = 'Archive.' . $_GET['archiveDate'];
1372
		} elseif($preexistingMode) {
1373
			$mode = $preexistingMode;
1374
		} else {
1375
			$mode = self::DEFAULT_MODE;
1376
		}
1377
1378
		// Save reading mode
1379
		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...
1380
1381
		// Try not to store the mode in the session if not needed
1382
		if(($preexistingMode && $preexistingMode !== $mode)
1383
			|| (!$preexistingMode && $mode !== self::DEFAULT_MODE)
1384
		) {
1385
			Session::set('readingMode', $mode);
1386
		}
1387
1388
		if(!headers_sent() && !Director::is_cli()) {
1389
			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...
1390
				// clear the cookie if it's set
1391
				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...
1392
					Cookie::force_expiry('bypassStaticCache', null, null, false, true /* httponly */);
1393
				}
1394
			} else {
1395
				// set the cookie if it's cleared
1396
				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...
1397
					Cookie::set('bypassStaticCache', '1', 0, null, null, false, true /* httponly */);
1398
				}
1399
			}
1400
		}
1401
	}
1402
1403
	/**
1404
	 * Set the current reading mode.
1405
	 *
1406
	 * @param string $mode
1407
	 */
1408
	public static function set_reading_mode($mode) {
1409
		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...
1410
	}
1411
1412
	/**
1413
	 * Get the current reading mode.
1414
	 *
1415
	 * @return string
1416
	 */
1417
	public static function get_reading_mode() {
1418
		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...
1419
	}
1420
1421
	/**
1422
	 * Get the name of the 'live' stage.
1423
	 *
1424
	 * @return string
1425
	 */
1426
	public static function get_live_stage() {
1427
		return "Live";
1428
	}
1429
1430
	/**
1431
	 * Get the current reading stage.
1432
	 *
1433
	 * @return string
1434
	 */
1435
	public static function current_stage() {
1436
		$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...
1437
1438
		if($parts[0] == 'Stage') {
1439
			return $parts[1];
1440
		}
1441
	}
1442
1443
	/**
1444
	 * Get the current archive date.
1445
	 *
1446
	 * @return string
1447
	 */
1448
	public static function current_archived_date() {
1449
		$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...
1450
		if($parts[0] == 'Archive') return $parts[1];
1451
	}
1452
1453
	/**
1454
	 * Set the reading stage.
1455
	 *
1456
	 * @param string $stage New reading stage.
1457
	 */
1458
	public static function reading_stage($stage) {
1459
		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...
1460
	}
1461
1462
	/**
1463
	 * Set the reading archive date.
1464
	 *
1465
	 * @param string $date New reading archived date.
1466
	 */
1467
	public static function reading_archived_date($date) {
1468
		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...
1469
	}
1470
1471
1472
	/**
1473
	 * Get a singleton instance of a class in the given stage.
1474
	 *
1475
	 * @param string $class The name of the class.
1476
	 * @param string $stage The name of the stage.
1477
	 * @param string $filter A filter to be inserted into the WHERE clause.
1478
	 * @param boolean $cache Use caching.
1479
	 * @param string $orderby A sort expression to be inserted into the ORDER BY clause.
0 ignored issues
show
Bug introduced by
There is no parameter named $orderby. Was it maybe removed?

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

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

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

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

Loading history...
1480
	 *
1481
	 * @return DataObject
1482
	 */
1483
	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...
1484
		// TODO: No identity cache operating
1485
		$items = self::get_by_stage($class, $stage, $filter, $sort, null, 1);
1486
1487
		return $items->First();
1488
	}
1489
1490
	/**
1491
	 * Gets the current version number of a specific record.
1492
	 *
1493
	 * @param string $class
1494
	 * @param string $stage
1495
	 * @param int $id
1496
	 * @param boolean $cache
1497
	 *
1498
	 * @return int
1499
	 */
1500
	public static function get_versionnumber_by_stage($class, $stage, $id, $cache = true) {
1501
		$baseClass = ClassInfo::baseDataClass($class);
1502
		$stageTable = ($stage == 'Stage') ? $baseClass : "{$baseClass}_{$stage}";
1503
1504
		// cached call
1505
		if($cache && isset(self::$cache_versionnumber[$baseClass][$stage][$id])) {
1506
			return self::$cache_versionnumber[$baseClass][$stage][$id];
1507
		}
1508
1509
		// get version as performance-optimized SQL query (gets called for each record in the sitetree)
1510
		$version = DB::prepared_query(
1511
			"SELECT \"Version\" FROM \"$stageTable\" WHERE \"ID\" = ?",
1512
			array($id)
1513
		)->value();
1514
1515
		// cache value (if required)
1516
		if($cache) {
1517
			if(!isset(self::$cache_versionnumber[$baseClass])) {
1518
				self::$cache_versionnumber[$baseClass] = array();
1519
			}
1520
1521
			if(!isset(self::$cache_versionnumber[$baseClass][$stage])) {
1522
				self::$cache_versionnumber[$baseClass][$stage] = array();
1523
			}
1524
1525
			self::$cache_versionnumber[$baseClass][$stage][$id] = $version;
1526
		}
1527
1528
		return $version;
1529
	}
1530
1531
	/**
1532
	 * Pre-populate the cache for Versioned::get_versionnumber_by_stage() for
1533
	 * a list of record IDs, for more efficient database querying.  If $idList
1534
	 * is null, then every record will be pre-cached.
1535
	 *
1536
	 * @param string $class
1537
	 * @param string $stage
1538
	 * @param array $idList
1539
	 */
1540
	public static function prepopulate_versionnumber_cache($class, $stage, $idList = null) {
1541
		if (!Config::inst()->get('Versioned', 'prepopulate_versionnumber_cache')) {
1542
			return;
1543
		}
1544
		$filter = "";
1545
		$parameters = array();
1546
		if($idList) {
1547
			// Validate the ID list
1548
			foreach($idList as $id) {
1549
				if(!is_numeric($id)) {
1550
					user_error("Bad ID passed to Versioned::prepopulate_versionnumber_cache() in \$idList: " . $id,
1551
					E_USER_ERROR);
1552
				}
1553
			}
1554
			$filter = 'WHERE "ID" IN ('.DB::placeholders($idList).')';
1555
			$parameters = $idList;
1556
		}
1557
1558
		$baseClass = ClassInfo::baseDataClass($class);
1559
		$stageTable = ($stage == 'Stage') ? $baseClass : "{$baseClass}_{$stage}";
1560
1561
		$versions = DB::prepared_query("SELECT \"ID\", \"Version\" FROM \"$stageTable\" $filter", $parameters)->map();
1562
1563
		foreach($versions as $id => $version) {
1564
			self::$cache_versionnumber[$baseClass][$stage][$id] = $version;
1565
		}
1566
	}
1567
1568
	/**
1569
	 * Get a set of class instances by the given stage.
1570
	 *
1571
	 * @param string $class The name of the class.
1572
	 * @param string $stage The name of the stage.
1573
	 * @param string $filter A filter to be inserted into the WHERE clause.
1574
	 * @param string $sort A sort expression to be inserted into the ORDER BY clause.
1575
	 * @param string $join Deprecated, use leftJoin($table, $joinClause) instead
1576
	 * @param int $limit A limit on the number of records returned from the database.
1577
	 * @param string $containerClass The container class for the result set (default is DataList)
1578
	 *
1579
	 * @return DataList A modified DataList designated to the specified stage
1580
	 */
1581
	public static function get_by_stage($class, $stage, $filter = '', $sort = '', $join = '', $limit = '',
1582
			$containerClass = 'DataList') {
1583
1584
		$result = DataObject::get($class, $filter, $sort, $join, $limit, $containerClass);
1585
		return $result->setDataQueryParam(array(
1586
			'Versioned.mode' => 'stage',
1587
			'Versioned.stage' => $stage
1588
		));
1589
	}
1590
1591
	/**
1592
	 * @param string $stage
1593
	 *
1594
	 * @return int
1595
	 */
1596
	public function deleteFromStage($stage) {
1597
		$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...
1598
		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...
1599
		$clone = clone $this->owner;
1600
		$result = $clone->delete();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $result is correct as $clone->delete() (which targets DataObject::delete()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
1601
		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...
1602
1603
		// Fix the version number cache (in case you go delete from stage and then check ExistsOnLive)
1604
		$baseClass = ClassInfo::baseDataClass($this->owner->class);
1605
		self::$cache_versionnumber[$baseClass][$stage][$this->owner->ID] = null;
1606
1607
		return $result;
1608
	}
1609
1610
	/**
1611
	 * @param string $stage
1612
	 * @param boolean $forceInsert
1613
	 */
1614
	public function writeToStage($stage, $forceInsert = false) {
1615
		$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...
1616
		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...
1617
1618
		$this->owner->forceChange();
1619
		$result = $this->owner->write(false, $forceInsert);
1620
		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...
1621
1622
		return $result;
1623
	}
1624
1625
	/**
1626
	 * Roll the draft version of this record to match the published record.
1627
	 * Caution: Doesn't overwrite the object properties with the rolled back version.
1628
	 *
1629
	 * @param int $version Either the string 'Live' or a version number
1630
	 */
1631
	public function doRollbackTo($version) {
1632
		$owner = $this->owner;
1633
		$owner->extend('onBeforeRollback', $version);
1634
		$this->publish($version, "Stage", true);
1635
		$owner->writeWithoutVersion();
1636
		$owner->extend('onAfterRollback', $version);
1637
	}
1638
1639
	/**
1640
	 * Return the latest version of the given record.
1641
	 *
1642
	 * @return DataObject
1643
	 */
1644
	public static function get_latest_version($class, $id) {
1645
		$baseClass = ClassInfo::baseDataClass($class);
1646
		$list = DataList::create($baseClass)
1647
			->where("\"$baseClass\".\"RecordID\" = $id")
1648
			->setDataQueryParam("Versioned.mode", "latest_versions");
1649
1650
		return $list->First();
1651
	}
1652
1653
	/**
1654
	 * Returns whether the current record is the latest one.
1655
	 *
1656
	 * @todo Performance - could do this directly via SQL.
1657
	 *
1658
	 * @see get_latest_version()
1659
	 * @see latestPublished
1660
	 *
1661
	 * @return boolean
1662
	 */
1663
	public function isLatestVersion() {
1664
		if(!$this->owner->isInDB()) {
1665
			return false;
1666
		}
1667
1668
		$version = self::get_latest_version($this->owner->class, $this->owner->ID);
1669
		return ($version->Version == $this->owner->Version);
1670
	}
1671
1672
	/**
1673
	 * Check if this record exists on live
1674
	 *
1675
	 * @return bool
1676
	 */
1677
	public function isPublished() {
1678
		if(!$this->owner->isInDB()) {
1679
			return false;
1680
		}
1681
1682
		$table = ClassInfo::baseDataClass($this->owner->class) . '_' . self::get_live_stage();
1683
		$result = DB::prepared_query(
1684
			"SELECT COUNT(*) FROM \"{$table}\" WHERE \"{$table}\".\"ID\" = ?",
1685
			array($this->owner->ID)
1686
		);
1687
		return (bool)$result->value();
1688
	}
1689
1690
	/**
1691
	 * Check if this record exists on the draft stage
1692
	 *
1693
	 * @return bool
1694
	 */
1695
	public function isOnDraft() {
1696
		if(!$this->owner->isInDB()) {
1697
			return false;
1698
		}
1699
1700
		$table = ClassInfo::baseDataClass($this->owner->class);
1701
		$result = DB::prepared_query(
1702
			"SELECT COUNT(*) FROM \"{$table}\" WHERE \"{$table}\".\"ID\" = ?",
1703
			array($this->owner->ID)
1704
		);
1705
		return (bool)$result->value();
1706
	}
1707
1708
1709
1710
	/**
1711
	 * Return the equivalent of a DataList::create() call, querying the latest
1712
	 * version of each record stored in the (class)_versions tables.
1713
	 *
1714
	 * In particular, this will query deleted records as well as active ones.
1715
	 *
1716
	 * @param string $class
1717
	 * @param string $filter
1718
	 * @param string $sort
1719
	 */
1720
	public static function get_including_deleted($class, $filter = "", $sort = "") {
1721
		$list = DataList::create($class)
1722
			->where($filter)
1723
			->sort($sort)
1724
			->setDataQueryParam("Versioned.mode", "latest_versions");
1725
1726
		return $list;
1727
	}
1728
1729
	/**
1730
	 * Return the specific version of the given id.
1731
	 *
1732
	 * Caution: The record is retrieved as a DataObject, but saving back
1733
	 * modifications via write() will create a new version, rather than
1734
	 * modifying the existing one.
1735
	 *
1736
	 * @param string $class
1737
	 * @param int $id
1738
	 * @param int $version
1739
	 *
1740
	 * @return DataObject
1741
	 */
1742
	public static function get_version($class, $id, $version) {
1743
		$baseClass = ClassInfo::baseDataClass($class);
1744
		$list = DataList::create($baseClass)
1745
			->where("\"$baseClass\".\"RecordID\" = $id")
1746
			->where("\"$baseClass\".\"Version\" = " . (int)$version)
1747
			->setDataQueryParam("Versioned.mode", 'all_versions');
1748
1749
		return $list->First();
1750
	}
1751
1752
	/**
1753
	 * Return a list of all versions for a given id.
1754
	 *
1755
	 * @param string $class
1756
	 * @param int $id
1757
	 *
1758
	 * @return DataList
1759
	 */
1760
	public static function get_all_versions($class, $id) {
1761
		$baseClass = ClassInfo::baseDataClass($class);
1762
		$list = DataList::create($class)
1763
			->where("\"$baseClass\".\"RecordID\" = $id")
1764
			->setDataQueryParam('Versioned.mode', 'all_versions');
1765
1766
		return $list;
1767
	}
1768
1769
	/**
1770
	 * @param array $labels
1771
	 */
1772
	public function updateFieldLabels(&$labels) {
1773
		$labels['Versions'] = _t('Versioned.has_many_Versions', 'Versions', 'Past Versions of this record');
1774
	}
1775
1776
	/**
1777
	 * @param FieldList
1778
	 */
1779
	public function updateCMSFields(FieldList $fields) {
1780
		// remove the version field from the CMS as this should be left
1781
		// entirely up to the extension (not the cms user).
1782
		$fields->removeByName('Version');
1783
	}
1784
1785
	/**
1786
	 * Ensure version ID is reset to 0 on duplicate
1787
	 *
1788
	 * @param DataObject $source Record this was duplicated from
1789
	 * @param bool $doWrite
1790
	 */
1791
	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...
1792
		$this->owner->Version = 0;
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...
1793
	}
1794
1795
	public function flushCache() {
1796
		self::$cache_versionnumber = array();
1797
	}
1798
1799
	/**
1800
	 * Return a piece of text to keep DataObject cache keys appropriately specific.
1801
	 *
1802
	 * @return string
1803
	 */
1804
	public function cacheKeyComponent() {
1805
		return 'versionedmode-'.self::get_reading_mode();
1806
	}
1807
1808
	/**
1809
	 * Returns an array of possible stages.
1810
	 *
1811
	 * @return array
1812
	 */
1813
	public function getVersionedStages() {
1814
		return $this->stages;
1815
	}
1816
1817
	/**
1818
	 * @return string
1819
	 */
1820
	public function getDefaultStage() {
1821
		return $this->defaultStage;
1822
	}
1823
1824
	public static function get_template_global_variables() {
1825
		return array(
1826
			'CurrentReadingMode' => 'get_reading_mode'
1827
		);
1828
	}
1829
}
1830
1831
/**
1832
 * Represents a single version of a record.
1833
 *
1834
 * @package framework
1835
 * @subpackage model
1836
 *
1837
 * @see Versioned
1838
 */
1839
class Versioned_Version extends ViewableData {
1840
	/**
1841
	 * @var array
1842
	 */
1843
	protected $record;
1844
1845
	/**
1846
	 * @var DataObject
1847
	 */
1848
	protected $object;
1849
1850
	public function __construct($record) {
1851
		$this->record = $record;
1852
		$record['ID'] = $record['RecordID'];
1853
		$className = $record['ClassName'];
1854
1855
		$this->object = ClassInfo::exists($className) ? new $className($record) : new DataObject($record);
1856
		$this->failover = $this->object;
1857
1858
		parent::__construct();
1859
	}
1860
1861
	/**
1862
	 * @return string
1863
	 */
1864
	public function PublishedClass() {
1865
		return $this->record['WasPublished'] ? 'published' : 'internal';
1866
	}
1867
1868
	/**
1869
	 * @return Member
1870
	 */
1871
	public function Author() {
1872
		return Member::get()->byId($this->record['AuthorID']);
1873
	}
1874
1875
	/**
1876
	 * @return Member
1877
	 */
1878
	public function Publisher() {
1879
		if (!$this->record['WasPublished']) {
1880
			return null;
1881
		}
1882
1883
		return Member::get()->byId($this->record['PublisherID']);
1884
	}
1885
1886
	/**
1887
	 * @return boolean
1888
	 */
1889
	public function Published() {
1890
		return !empty($this->record['WasPublished']);
1891
	}
1892
1893
	/**
1894
	 * Copied from DataObject to allow access via dot notation.
1895
	 */
1896
	public function relField($fieldName) {
1897
		$component = $this;
1898
1899
		if(strpos($fieldName, '.') !== false) {
1900
			$parts = explode('.', $fieldName);
1901
			$fieldName = array_pop($parts);
1902
1903
			// Traverse dot syntax
1904
			foreach($parts as $relation) {
1905
				if($component instanceof SS_List) {
1906
					if(method_exists($component,$relation)) {
1907
						$component = $component->$relation();
1908
					} else {
1909
						$component = $component->relation($relation);
1910
					}
1911
				} else {
1912
					$component = $component->$relation();
1913
				}
1914
			}
1915
		}
1916
1917
		// Unlike has-one's, these "relations" can return false
1918
		if($component) {
1919
			if ($component->hasMethod($fieldName)) {
1920
				return $component->$fieldName();
1921
			}
1922
1923
			return $component->$fieldName;
1924
		}
1925
	}
1926
}
1927