Completed
Push — master ( 9e3f76...51d53f )
by Hamish
10:45
created

Versioned::findOwners()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 2
dl 0
loc 12
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
// namespace SilverStripe\Framework\Model\Versioning
4
5
/**
6
 * The Versioned extension allows your DataObjects to have several versions,
7
 * allowing you to rollback changes and view history. An example of this is
8
 * the pages used in the CMS.
9
 *
10
 * @property int $Version
11
 * @property DataObject|Versioned $owner
12
 *
13
 * @package framework
14
 * @subpackage model
15
 */
16
class Versioned extends DataExtension implements TemplateGlobalProvider {
17
18
	/**
19
	 * Versioning mode for this object.
20
	 * Note: Not related to the current versioning mode in the state / session
21
	 * Will be one of 'StagedVersioned' or 'Versioned';
22
	 *
23
	 * @var string
24
	 */
25
	protected $mode;
26
27
	/**
28
	 * The default reading mode
29
	 */
30
	const DEFAULT_MODE = 'Stage.Live';
31
32
	/**
33
	 * Constructor arg to specify that staging is active on this record.
34
	 * 'Staging' implies that 'Versioning' is also enabled.
35
	 */
36
	const STAGEDVERSIONED = 'StagedVersioned';
37
38
	/**
39
	 * Constructor arg to specify that versioning only is active on this record.
40
	 */
41
	const VERSIONED = 'Versioned';
42
43
	/**
44
	 * The Public stage.
45
	 */
46
	const LIVE = 'Live';
47
48
	/**
49
	 * The draft (default) stage
50
	 */
51
	const DRAFT = 'Stage';
52
53
	/**
54
	 * A version that a DataObject should be when it is 'migrating',
55
	 * that is, when it is in the process of moving from one stage to another.
56
	 * @var string
57
	 */
58
	public $migratingVersion;
59
60
	/**
61
	 * A cache used by get_versionnumber_by_stage().
62
	 * Clear through {@link flushCache()}.
63
	 *
64
	 * @var array
65
	 */
66
	protected static $cache_versionnumber;
67
68
	/**
69
	 * Current reading mode
70
	 *
71
	 * @var string
72
	 */
73
	protected static $reading_mode = null;
74
75
	/**
76
	 * @var Boolean Flag which is temporarily changed during the write() process
77
	 * to influence augmentWrite() behaviour. If set to TRUE, no new version will be created
78
	 * for the following write. Needs to be public as other classes introspect this state
79
	 * during the write process in order to adapt to this versioning behaviour.
80
	 */
81
	public $_nextWriteWithoutVersion = false;
82
83
	/**
84
	 * Additional database columns for the new
85
	 * "_versions" table. Used in {@link augmentDatabase()}
86
	 * and all Versioned calls extending or creating
87
	 * SELECT statements.
88
	 *
89
	 * @var array $db_for_versions_table
90
	 */
91
	private static $db_for_versions_table = array(
92
		"RecordID" => "Int",
93
		"Version" => "Int",
94
		"WasPublished" => "Boolean",
95
		"AuthorID" => "Int",
96
		"PublisherID" => "Int"
97
	);
98
99
	/**
100
	 * @var array
101
	 */
102
	private static $db = array(
103
		'Version' => 'Int'
104
	);
105
106
	/**
107
	 * Used to enable or disable the prepopulation of the version number cache.
108
	 * Defaults to true.
109
	 *
110
	 * @config
111
	 * @var boolean
112
	 */
113
	private static $prepopulate_versionnumber_cache = true;
114
115
	/**
116
	 * Additional database indexes for the new
117
	 * "_versions" table. Used in {@link augmentDatabase()}.
118
	 *
119
	 * @var array $indexes_for_versions_table
120
	 */
121
	private static $indexes_for_versions_table = array(
122
		'RecordID_Version' => '("RecordID","Version")',
123
		'RecordID' => true,
124
		'Version' => true,
125
		'AuthorID' => true,
126
		'PublisherID' => true,
127
	);
128
129
130
	/**
131
	 * An array of DataObject extensions that may require versioning for extra tables
132
	 * The array value is a set of suffixes to form these table names, assuming a preceding '_'.
133
	 * E.g. if Extension1 creates a new table 'Class_suffix1'
134
	 * and Extension2 the tables 'Class_suffix2' and 'Class_suffix3':
135
	 *
136
	 * 	$versionableExtensions = array(
137
	 * 		'Extension1' => 'suffix1',
138
	 * 		'Extension2' => array('suffix2', 'suffix3'),
139
	 * 	);
140
	 *
141
	 * This can also be manipulated by updating the current loaded config
142
	 *
143
	 * SiteTree:
144
	 *   versionableExtensions:
145
	 *     - Extension1:
146
	 *       - suffix1
147
	 *       - suffix2
148
	 *     - Extension2:
149
	 *       - suffix1
150
	 *       - suffix2
151
	 *
152
	 * or programatically:
153
	 *
154
	 *  Config::inst()->update($this->owner->class, 'versionableExtensions',
155
	 *  array('Extension1' => 'suffix1', 'Extension2' => array('suffix2', 'suffix3')));
156
	 *
157
	 *
158
	 * Your extension must implement VersionableExtension interface in order to
159
	 * apply custom tables for versioned.
160
	 *
161
	 * @config
162
	 * @var array
163
	 */
164
	private static $versionableExtensions = [];
165
166
	/**
167
	 * Permissions necessary to view records outside of the live stage (e.g. archive / draft stage).
168
	 *
169
	 * @config
170
	 * @var array
171
	 */
172
	private static $non_live_permissions = array('CMS_ACCESS_LeftAndMain', 'CMS_ACCESS_CMSMain', 'VIEW_DRAFT_CONTENT');
173
174
	/**
175
	 * List of relationships on this object that are "owned" by this object.
176
	 * Owership in the context of versioned objects is a relationship where
177
	 * the publishing of owning objects requires the publishing of owned objects.
178
	 *
179
	 * E.g. A page owns a set of banners, as in order for the page to be published, all
180
	 * banners on this page must also be published for it to be visible.
181
	 *
182
	 * Typically any object and its owned objects should be visible in the same edit view.
183
	 * E.g. a page and {@see GridField} of banners.
184
	 *
185
	 * Page hierarchy is typically not considered an ownership relationship.
186
	 *
187
	 * Ownership is recursive; If A owns B and B owns C then A owns C.
188
	 *
189
	 * @config
190
	 * @var array List of has_many or many_many relationships owned by this object.
191
	 */
192
	private static $owns = array();
193
194
	/**
195
	 * Opposing relationship to owns config; Represents the objects which
196
	 * own the current object.
197
	 *
198
	 * @var array
199
	 */
200
	private static $owned_by = array();
201
202
	/**
203
	 * Reset static configuration variables to their default values.
204
	 */
205
	public static function reset() {
206
		self::$reading_mode = '';
207
		Session::clear('readingMode');
208
	}
209
210
	/**
211
	 * Amend freshly created DataQuery objects with versioned-specific
212
	 * information.
213
	 *
214
	 * @param SQLSelect
215
	 * @param DataQuery
216
	 */
217
	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...
218
		$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...
219
220
		if($parts[0] == 'Archive') {
221
			$dataQuery->setQueryParam('Versioned.mode', 'archive');
222
			$dataQuery->setQueryParam('Versioned.date', $parts[1]);
223
		} else if($parts[0] == 'Stage' && $this->hasStages()) {
224
			$dataQuery->setQueryParam('Versioned.mode', 'stage');
225
			$dataQuery->setQueryParam('Versioned.stage', $parts[1]);
226
		}
227
	}
228
229
	/**
230
	 * Construct a new Versioned object.
231
	 *
232
	 * @var string $mode One of "StagedVersioned" or "Versioned".
233
	 */
234
	public function __construct($mode = self::STAGEDVERSIONED) {
235
		parent::__construct();
236
237
		// Handle deprecated behaviour
238
		if($mode === 'Stage' && func_num_args() === 1) {
239
			Deprecation::notice("5.0", "Versioned now takes a mode as a single parameter");
240
			$mode = static::VERSIONED;
241
		} elseif(is_array($mode) || func_num_args() > 1) {
242
			Deprecation::notice("5.0", "Versioned now takes a mode as a single parameter");
243
			$mode = func_num_args() > 1 || count($mode) > 1
244
				? static::STAGEDVERSIONED
245
				: static::VERSIONED;
246
		}
247
248
		if(!in_array($mode, array(static::STAGEDVERSIONED, static::VERSIONED))) {
249
			throw new InvalidArgumentException("Invalid mode: {$mode}");
250
		}
251
252
		$this->mode = $mode;
253
	}
254
255
	/**
256
	 * Cache of version to modified dates for this objects
257
	 *
258
	 * @var array
259
	 */
260
	protected $versionModifiedCache = array();
261
262
	/**
263
	 * Get modified date for the given version
264
	 *
265
	 * @param int $version
266
	 * @return string
267
	 */
268
	protected function getLastEditedForVersion($version) {
269
		// Cache key
270
		$baseTable = $this->baseTable();
271
		$id = $this->owner->ID;
0 ignored issues
show
Bug introduced by
The property ID does not exist. Did you maybe forget to declare it?

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

class MyClass { }

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

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

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
272
		$key = "{$baseTable}#{$id}/{$version}";
273
274
		// Check cache
275
		if(isset($this->versionModifiedCache[$key])) {
276
			return $this->versionModifiedCache[$key];
277
		}
278
279
		// Build query
280
		$table = "\"{$baseTable}_versions\"";
281
		$query = SQLSelect::create('"LastEdited"', $table)
282
			->addWhere([
283
				"{$table}.\"RecordID\"" => $id,
284
				"{$table}.\"Version\"" => $version
285
			]);
286
		$date = $query->execute()->value();
287
		if($date) {
288
			$this->versionModifiedCache[$key] = $date;
289
		}
290
		return $date;
291
	}
292
293
	/**
294
	 * Updates query parameters of relations attached to versioned dataobjects
295
	 *
296
	 * @param array $params
297
	 */
298
	public function updateInheritableQueryParams(&$params) {
299
		// Skip if versioned isn't set
300
		if(!isset($params['Versioned.mode'])) {
301
			return;
302
		}
303
304
		// Adjust query based on original selection criterea
305
		switch($params['Versioned.mode']) {
306
			case 'all_versions': {
0 ignored issues
show
Coding Style introduced by
CASE statements must be defined using a colon

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
307
				// Versioned.mode === all_versions doesn't inherit very well, so default to stage
308
				$params['Versioned.mode'] = 'stage';
309
				$params['Versioned.stage'] = static::DRAFT;
310
				break;
311
			}
312
			case 'version': {
0 ignored issues
show
Coding Style introduced by
CASE statements must be defined using a colon

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
313
				// If we selected this object from a specific version, we need
314
				// to find the date this version was published, and ensure
315
				// inherited queries select from that date.
316
				$version = $params['Versioned.version'];
317
				$date = $this->getLastEditedForVersion($version);
318
319
				// Filter related objects at the same date as this version
320
				unset($params['Versioned.version']);
321
				if($date) {
322
					$params['Versioned.mode'] = 'archive';
323
					$params['Versioned.date'] = $date;
324
				} else {
325
					// Fallback to default
326
					$params['Versioned.mode'] = 'stage';
327
					$params['Versioned.stage'] = static::DRAFT;
328
				}
329
				break;
330
			}
331
		}
332
	}
333
334
	/**
335
	 * Augment the the SQLSelect that is created by the DataQuery
336
	 *
337
	 * See {@see augmentLazyLoadFields} for lazy-loading applied prior to this.
338
	 *
339
	 * @param SQLSelect $query
340
	 * @param DataQuery $dataQuery
341
	 * @throws InvalidArgumentException
342
	 */
343
	public function augmentSQL(SQLSelect $query, DataQuery $dataQuery = null) {
344
		if(!$dataQuery || !$dataQuery->getQueryParam('Versioned.mode')) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $dataQuery->getQueryParam('Versioned.mode') 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...
345
			return;
346
		}
347
348
		$baseTable = $this->baseTable();
349
		$versionedMode = $dataQuery->getQueryParam('Versioned.mode');
350
		switch($versionedMode) {
351
		// Reading a specific stage (Stage or Live)
352
		case 'stage':
353
			// Check if we need to rewrite this table
354
			$stage = $dataQuery->getQueryParam('Versioned.stage');
355
			if(!$this->hasStages() || $stage === static::DRAFT) {
356
				break;
357
			}
358
			// Rewrite all tables to select from the live version
359
			foreach($query->getFrom() as $table => $dummy) {
360
				if(!$this->isTableVersioned($table)) {
361
					continue;
362
				}
363
				$stageTable = $this->stageTable($table, $stage);
364
				$query->renameTable($table, $stageTable);
365
			}
366
			break;
367
368
		// Reading a specific stage, but only return items that aren't in any other stage
369
		case 'stage_unique':
370
			if(!$this->hasStages()) {
371
				break;
372
			}
373
374
			$stage = $dataQuery->getQueryParam('Versioned.stage');
375
			// Recurse to do the default stage behavior (must be first, we rely on stage renaming happening before
376
			// below)
377
			$dataQuery->setQueryParam('Versioned.mode', 'stage');
378
			$this->augmentSQL($query, $dataQuery);
379
			$dataQuery->setQueryParam('Versioned.mode', 'stage_unique');
380
381
			// Now exclude any ID from any other stage. Note that we double rename to avoid the regular stage rename
382
			// renaming all subquery references to be Versioned.stage
383
			foreach([static::DRAFT, static::LIVE] as $excluding) {
384
				if ($excluding == $stage) {
385
					continue;
386
				}
387
388
				$tempName = 'ExclusionarySource_'.$excluding;
389
				$excludingTable = $this->baseTable($excluding);
390
391
				$query->addWhere('"'.$baseTable.'"."ID" NOT IN (SELECT "ID" FROM "'.$tempName.'")');
392
				$query->renameTable($tempName, $excludingTable);
393
			}
394
			break;
395
396
		// Return all version instances
397
		case 'archive':
398
		case 'all_versions':
399
		case 'latest_versions':
400
		case 'version':
401
			foreach($query->getFrom() as $alias => $join) {
402
				if(!$this->isTableVersioned($alias)) {
403
					continue;
404
				}
405
406
				if($alias != $baseTable) {
407
					// Make sure join includes version as well
408
					$query->setJoinFilter(
409
						$alias,
410
						"\"{$alias}_versions\".\"RecordID\" = \"{$baseTable}_versions\".\"RecordID\""
411
						. " AND \"{$alias}_versions\".\"Version\" = \"{$baseTable}_versions\".\"Version\""
412
					);
413
				}
414
				$query->renameTable($alias, $alias . '_versions');
415
			}
416
417
			// Add all <basetable>_versions columns
418
			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...
419
				$query->selectField(sprintf('"%s_versions"."%s"', $baseTable, $name), $name);
420
			}
421
422
			// Alias the record ID as the row ID, and ensure ID filters are aliased correctly
423
			$query->selectField("\"{$baseTable}_versions\".\"RecordID\"", "ID");
424
			$query->replaceText("\"{$baseTable}_versions\".\"ID\"", "\"{$baseTable}_versions\".\"RecordID\"");
425
426
			// However, if doing count, undo rewrite of "ID" column
427
			$query->replaceText(
428
				"count(DISTINCT \"{$baseTable}_versions\".\"RecordID\")",
429
				"count(DISTINCT \"{$baseTable}_versions\".\"ID\")"
430
			);
431
432
			// Add additional versioning filters
433
			switch($versionedMode) {
434
				case 'archive': {
0 ignored issues
show
Coding Style introduced by
CASE statements must be defined using a colon

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
435
					$date = $dataQuery->getQueryParam('Versioned.date');
436
					if(!$date) {
437
						throw new InvalidArgumentException("Invalid archive date");
438
					}
439
					// Link to the version archived on that date
440
					$query->addWhere([
441
						"\"{$baseTable}_versions\".\"Version\" IN
442
						(SELECT LatestVersion FROM
443
							(SELECT
444
								\"{$baseTable}_versions\".\"RecordID\",
445
								MAX(\"{$baseTable}_versions\".\"Version\") AS LatestVersion
446
								FROM \"{$baseTable}_versions\"
447
								WHERE \"{$baseTable}_versions\".\"LastEdited\" <= ?
448
								GROUP BY \"{$baseTable}_versions\".\"RecordID\"
449
							) AS \"{$baseTable}_versions_latest\"
450
							WHERE \"{$baseTable}_versions_latest\".\"RecordID\" = \"{$baseTable}_versions\".\"RecordID\"
451
						)" => $date
452
					]);
453
					break;
454
				}
455
				case 'latest_versions': {
0 ignored issues
show
Coding Style introduced by
CASE statements must be defined using a colon

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
456
					// Return latest version instances, regardless of whether they are on a particular stage
457
					// This provides "show all, including deleted" functonality
458
					$query->addWhere(
459
						"\"{$baseTable}_versions\".\"Version\" IN
460
						(SELECT LatestVersion FROM
461
							(SELECT
462
								\"{$baseTable}_versions\".\"RecordID\",
463
								MAX(\"{$baseTable}_versions\".\"Version\") AS LatestVersion
464
								FROM \"{$baseTable}_versions\"
465
								GROUP BY \"{$baseTable}_versions\".\"RecordID\"
466
							) AS \"{$baseTable}_versions_latest\"
467
							WHERE \"{$baseTable}_versions_latest\".\"RecordID\" = \"{$baseTable}_versions\".\"RecordID\"
468
						)"
469
					);
470
					break;
471
				}
472
				case 'version': {
0 ignored issues
show
Coding Style introduced by
CASE statements must be defined using a colon

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
473
					// If selecting a specific version, filter it here
474
					$version = $dataQuery->getQueryParam('Versioned.version');
475
					if(!$version) {
476
						throw new InvalidArgumentException("Invalid version");
477
					}
478
					$query->addWhere([
479
						"\"{$baseTable}_versions\".\"Version\"" => $version
480
					]);
481
					break;
482
				}
483
				case 'all_versions':
484
				default: {
0 ignored issues
show
Coding Style introduced by
DEFAULT statements must be defined using a colon

As per the PSR-2 coding standard, default statements should not be wrapped in curly braces.

switch ($expr) {
    default: { //wrong
        doSomething();
        break;
    }
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
485
					// If all versions are requested, ensure that records are sorted by this field
486
					$query->addOrderBy(sprintf('"%s_versions"."%s"', $baseTable, 'Version'));
487
					break;
488
				}
489
			}
490
			break;
491
		default:
492
			throw new InvalidArgumentException("Bad value for query parameter Versioned.mode: "
493
				. $dataQuery->getQueryParam('Versioned.mode'));
494
		}
495
	}
496
497
	/**
498
	 * Determine if the given versioned table is a part of the sub-tree of the current dataobject
499
	 * This helps prevent rewriting of other tables that get joined in, in particular, many_many tables
500
	 *
501
	 * @param string $table
502
	 * @return bool True if this table should be versioned
503
	 */
504
	protected function isTableVersioned($table) {
505
		$schema = DataObject::getSchema();
506
		$tableClass = $schema->tableClass($table);
507
		if(empty($tableClass)) {
508
			return false;
509
		}
510
511
		// Check that this class belongs to the same tree
512
		$baseClass = $schema->baseDataClass($this->owner);
513
		if(!is_a($tableClass, $baseClass, true)) {
514
			return false;
515
		}
516
517
		// Check that this isn't a derived table
518
		// (e.g. _Live, or a many_many table)
519
		$mainTable = $schema->tableName($tableClass);
520
		if($mainTable !== $table) {
521
			return false;
522
		}
523
524
		return true;
525
	}
526
527
	/**
528
	 * For lazy loaded fields requiring extra sql manipulation, ie versioning.
529
	 *
530
	 * @param SQLSelect $query
531
	 * @param DataQuery $dataQuery
532
	 * @param DataObject $dataObject
533
	 */
534
	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...
535
		// The VersionedMode local variable ensures that this decorator only applies to
536
		// queries that have originated from the Versioned object, and have the Versioned
537
		// metadata set on the query object. This prevents regular queries from
538
		// accidentally querying the *_versions tables.
539
		$versionedMode = $dataObject->getSourceQueryParam('Versioned.mode');
540
		$modesToAllowVersioning = array('all_versions', 'latest_versions', 'archive', 'version');
541
		if(
542
			!empty($dataObject->Version) &&
543
			(!empty($versionedMode) && in_array($versionedMode,$modesToAllowVersioning))
544
		) {
545
			// This will ensure that augmentSQL will select only the same version as the owner,
546
			// regardless of how this object was initially selected
547
			$versionColumn = $this->owner->getSchema()->sqlColumnForField($this->owner, 'Version');
548
			$dataQuery->where([
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...
549
				$versionColumn => $dataObject->Version
550
			]);
551
			$dataQuery->setQueryParam('Versioned.mode', 'all_versions');
552
		}
553
	}
554
555
	public function augmentDatabase() {
556
		$owner = $this->owner;
557
		$class = get_class($owner);
558
		$baseTable = $this->baseTable();
559
		$classTable = $owner->getSchema()->tableName($owner);
560
561
		$isRootClass = $class === $owner->baseClass();
562
563
		// Build a list of suffixes whose tables need versioning
564
		$allSuffixes = array();
565
		$versionableExtensions = $owner->config()->versionableExtensions;
566
		if(count($versionableExtensions)){
567
			foreach ($versionableExtensions as $versionableExtension => $suffixes) {
568
				if ($owner->hasExtension($versionableExtension)) {
569
					foreach ((array)$suffixes as $suffix) {
570
						$allSuffixes[$suffix] = $versionableExtension;
571
					}
572
				}
573
			}
574
		}
575
576
		// Add the default table with an empty suffix to the list (table name = class name)
577
		$allSuffixes[''] = null;
578
579
		foreach ($allSuffixes as $suffix => $extension) {
580
			// Check tables for this build
581
			if ($suffix) {
582
				$suffixBaseTable = "{$baseTable}_{$suffix}";
583
				$suffixTable = "{$classTable}_{$suffix}";
584
			}  else {
585
				$suffixBaseTable = $baseTable;
586
				$suffixTable = $classTable;
587
			}
588
589
			$fields = DataObject::database_fields($owner->class);
590
			unset($fields['ID']);
591
			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...
592
				$options = Config::inst()->get($owner->class, 'create_table_options', Config::FIRST_SET);
593
				$indexes = $owner->databaseIndexes();
594
				$extensionClass = $allSuffixes[$suffix];
595
				if ($suffix && ($extension = $owner->getExtensionInstance($extensionClass))) {
596
					if (!$extension instanceof VersionableExtension) {
597
						throw new LogicException(
598
							"Extension {$extensionClass} must implement VersionableExtension"
599
						);
600
					}
601
					// Allow versionable extension to customise table fields and indexes
602
					$extension->setOwner($owner);
603
					if ($extension->isVersionedTable($suffixTable)) {
604
						$extension->updateVersionableFields($suffix, $fields, $indexes);
605
					}
606
					$extension->clearOwner();
607
				}
608
609
				// Build _Live table
610
				if($this->hasStages()) {
611
					$liveTable = $this->stageTable($suffixTable, static::LIVE);
612
					DB::require_table($liveTable, $fields, $indexes, false, $options);
613
				}
614
615
				// Build _versions table
616
				//Unique indexes will not work on versioned tables, so we'll convert them to standard indexes:
617
				$nonUniqueIndexes = $this->uniqueToIndex($indexes);
618
				if($isRootClass) {
619
					// Create table for all versions
620
					$versionFields = array_merge(
621
						Config::inst()->get('Versioned', 'db_for_versions_table'),
622
						(array)$fields
623
					);
624
					$versionIndexes = array_merge(
625
						Config::inst()->get('Versioned', 'indexes_for_versions_table'),
626
						(array)$nonUniqueIndexes
627
					);
628
				} else {
629
					// Create fields for any tables of subclasses
630
					$versionFields = array_merge(
631
						array(
632
							"RecordID" => "Int",
633
							"Version" => "Int",
634
						),
635
						(array)$fields
636
					);
637
					$versionIndexes = array_merge(
638
						array(
639
							'RecordID_Version' => array('type' => 'unique', 'value' => '"RecordID","Version"'),
640
							'RecordID' => true,
641
							'Version' => true,
642
						),
643
						(array)$nonUniqueIndexes
644
					);
645
				}
646
647
				// Cleanup any orphans
648
				$this->cleanupVersionedOrphans("{$suffixBaseTable}_versions", "{$suffixTable}_versions");
649
650
				// Build versions table
651
				DB::require_table("{$suffixTable}_versions", $versionFields, $versionIndexes, true, $options);
652
			} else {
653
				DB::dont_require_table("{$suffixTable}_versions");
654
				if($this->hasStages()) {
655
					$liveTable = $this->stageTable($suffixTable, static::LIVE);
656
					DB::dont_require_table($liveTable);
657
				}
658
			}
659
		}
660
	}
661
662
	/**
663
	 * Cleanup orphaned records in the _versions table
664
	 *
665
	 * @param string $baseTable base table to use as authoritative source of records
666
	 * @param string $childTable Sub-table to clean orphans from
667
	 */
668
	protected function cleanupVersionedOrphans($baseTable, $childTable) {
669
		// Skip if child table doesn't exist
670
		if(!DB::get_schema()->hasTable($childTable)) {
671
			return;
672
		}
673
		// Skip if tables are the same
674
		if($childTable === $baseTable) {
675
			return;
676
		}
677
678
		// Select all orphaned version records
679
		$orphanedQuery = SQLSelect::create()
680
			->selectField("\"{$childTable}\".\"ID\"")
681
			->setFrom("\"{$childTable}\"");
682
683
		// If we have a parent table limit orphaned records
684
		// to only those that exist in this
685
		if(DB::get_schema()->hasTable($baseTable)) {
686
			$orphanedQuery
687
				->addLeftJoin(
688
					$baseTable,
689
					"\"{$childTable}\".\"RecordID\" = \"{$baseTable}\".\"RecordID\"
690
					AND \"{$childTable}\".\"Version\" = \"{$baseTable}\".\"Version\""
691
				)
692
				->addWhere("\"{$baseTable}\".\"ID\" IS NULL");
693
		}
694
695
		$count = $orphanedQuery->count();
696
		if($count > 0) {
697
			DB::alteration_message("Removing {$count} orphaned versioned records", "deleted");
698
			$ids = $orphanedQuery->execute()->column();
699
			foreach($ids as $id) {
700
				DB::prepared_query("DELETE FROM \"{$childTable}\" WHERE \"ID\" = ?", array($id));
701
			}
702
		}
703
	}
704
705
	/**
706
	 * Helper for augmentDatabase() to find unique indexes and convert them to non-unique
707
	 *
708
	 * @param array $indexes The indexes to convert
709
	 * @return array $indexes
710
	 */
711
	private function uniqueToIndex($indexes) {
712
		$unique_regex = '/unique/i';
713
		$results = array();
714
		foreach ($indexes as $key => $index) {
715
			$results[$key] = $index;
716
717
			// support string descriptors
718
			if (is_string($index)) {
719
				if (preg_match($unique_regex, $index)) {
720
					$results[$key] = preg_replace($unique_regex, 'index', $index);
721
				}
722
			}
723
724
			// canonical, array-based descriptors
725
			elseif (is_array($index)) {
726
				if (strtolower($index['type']) == 'unique') {
727
					$results[$key]['type'] = 'index';
728
				}
729
			}
730
		}
731
		return $results;
732
	}
733
734
	/**
735
	 * Generates a ($table)_version DB manipulation and injects it into the current $manipulation
736
	 *
737
	 * @param array $manipulation Source manipulation data
738
	 * @param string $class Class
739
	 * @param string $table Table Table for this class
740
	 * @param int $recordID ID of record to version
741
	 */
742
	protected function augmentWriteVersioned(&$manipulation, $class, $table, $recordID) {
743
		$baseDataClass = DataObject::getSchema()->baseDataClass($class);
744
		$baseDataTable = DataObject::getSchema()->tableName($baseDataClass);
745
746
		// Set up a new entry in (table)_versions
747
		$newManipulation = array(
748
			"command" => "insert",
749
			"fields" => isset($manipulation[$table]['fields']) ? $manipulation[$table]['fields'] : null,
750
			"class" => $class,
751
		);
752
753
		// Add any extra, unchanged fields to the version record.
754
		$data = DB::prepared_query("SELECT * FROM \"{$table}\" WHERE \"ID\" = ?", array($recordID))->record();
755
756
		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...
757
			$fields = DataObject::database_fields($class);
758
759
			if (is_array($fields)) {
760
				$data = array_intersect_key($data, $fields);
761
762
				foreach ($data as $k => $v) {
763
					if (!isset($newManipulation['fields'][$k])) {
764
						$newManipulation['fields'][$k] = $v;
765
					}
766
				}
767
			}
768
		}
769
770
		// Ensure that the ID is instead written to the RecordID field
771
		$newManipulation['fields']['RecordID'] = $recordID;
772
		unset($newManipulation['fields']['ID']);
773
774
		// Generate next version ID to use
775
		$nextVersion = 0;
776
		if($recordID) {
777
			$nextVersion = DB::prepared_query("SELECT MAX(\"Version\") + 1
778
				FROM \"{$baseDataTable}_versions\" WHERE \"RecordID\" = ?",
779
				array($recordID)
780
			)->value();
781
		}
782
		$nextVersion = $nextVersion ?: 1;
783
784
		if($class === $baseDataClass) {
785
			// Write AuthorID for baseclass
786
			$userID = (Member::currentUser()) ? Member::currentUser()->ID : 0;
787
			$newManipulation['fields']['AuthorID'] = $userID;
788
789
			// Update main table version if not previously known
790
			$manipulation[$table]['fields']['Version'] = $nextVersion;
791
		}
792
793
		// Update _versions table manipulation
794
		$newManipulation['fields']['Version'] = $nextVersion;
795
		$manipulation["{$table}_versions"] = $newManipulation;
796
	}
797
798
	/**
799
	 * Rewrite the given manipulation to update the selected (non-default) stage
800
	 *
801
	 * @param array $manipulation Source manipulation data
802
	 * @param string $table Name of table
803
	 * @param int $recordID ID of record to version
804
	 */
805
	protected function augmentWriteStaged(&$manipulation, $table, $recordID) {
806
		// If the record has already been inserted in the (table), get rid of it.
807
		if($manipulation[$table]['command'] == 'insert') {
808
			DB::prepared_query(
809
				"DELETE FROM \"{$table}\" WHERE \"ID\" = ?",
810
				array($recordID)
811
			);
812
		}
813
814
		$newTable = $this->stageTable($table, Versioned::get_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...
815
		$manipulation[$newTable] = $manipulation[$table];
816
		unset($manipulation[$table]);
817
	}
818
819
820
	public function augmentWrite(&$manipulation) {
821
		// get Version number from base data table on write
822
		$version = null;
823
		$owner = $this->owner;
824
		$baseDataTable = DataObject::getSchema()->baseDataTable($owner);
825
		if(isset($manipulation[$baseDataTable]['fields'])) {
826
			if ($this->migratingVersion) {
827
				$manipulation[$baseDataTable]['fields']['Version'] = $this->migratingVersion;
828
			}
829
			if (isset($manipulation[$baseDataTable]['fields']['Version'])) {
830
				$version = $manipulation[$baseDataTable]['fields']['Version'];
831
			}
832
		}
833
834
		// Update all tables
835
		$tables = array_keys($manipulation);
836
		foreach($tables as $table) {
837
838
			// Make sure that the augmented write is being applied to a table that can be versioned
839
			$class = isset($manipulation[$table]['class']) ? $manipulation[$table]['class'] : null;
840
			if(!$class || !$this->canBeVersioned($class) ) {
841
				unset($manipulation[$table]);
842
				continue;
843
			}
844
845
			// Get ID field
846
			$id = $manipulation[$table]['id']
847
				? $manipulation[$table]['id']
848
				: $manipulation[$table]['fields']['ID'];
849
			if(!$id) {
850
				user_error("Couldn't find ID in " . var_export($manipulation[$table], true), E_USER_ERROR);
851
			}
852
853
			if($version < 0 || $this->_nextWriteWithoutVersion) {
854
				// Putting a Version of -1 is a signal to leave the version table alone, despite their being no version
855
				unset($manipulation[$table]['fields']['Version']);
856
			} elseif(empty($version)) {
857
				// If we haven't got a version #, then we're creating a new version.
858
				// Otherwise, we're just copying a version to another table
859
				$this->augmentWriteVersioned($manipulation, $class, $table, $id);
860
			}
861
862
			// Remove "Version" column from subclasses of baseDataClass
863
			if(!$this->hasVersionField($table)) {
864
				unset($manipulation[$table]['fields']['Version']);
865
			}
866
867
			// Grab a version number - it should be the same across all tables.
868
			if(isset($manipulation[$table]['fields']['Version'])) {
869
				$thisVersion = $manipulation[$table]['fields']['Version'];
870
			}
871
872
			// If we're editing Live, then use (table)_Live instead of (table)
873
			if($this->hasStages() && static::get_stage() === static::LIVE) {
874
				$this->augmentWriteStaged($manipulation, $class, $id);
875
			}
876
		}
877
878
		// Clear the migration flag
879
		if($this->migratingVersion) {
880
			$this->migrateVersion(null);
881
		}
882
883
		// Add the new version # back into the data object, for accessing
884
		// after this write
885
		if(isset($thisVersion)) {
886
			$owner->Version = str_replace("'","", $thisVersion);
887
		}
888
	}
889
890
	/**
891
	 * Perform a write without affecting the version table.
892
	 * On objects without versioning.
893
	 *
894
	 * @return int The ID of the record
895
	 */
896
	public function writeWithoutVersion() {
897
		$this->_nextWriteWithoutVersion = true;
898
899
		return $this->owner->write();
900
	}
901
902
	/**
903
	 *
904
	 */
905
	public function onAfterWrite() {
906
		$this->_nextWriteWithoutVersion = false;
907
	}
908
909
	/**
910
	 * If a write was skipped, then we need to ensure that we don't leave a
911
	 * migrateVersion() value lying around for the next write.
912
	 */
913
	public function onAfterSkippedWrite() {
914
		$this->migrateVersion(null);
915
	}
916
917
	/**
918
	 * Find all objects owned by the current object.
919
	 * Note that objects will only be searched in the same stage as the given record.
920
	 *
921
	 * @param bool $recursive True if recursive
922
	 * @param ArrayList $list Optional list to add items to
923
	 * @return ArrayList list of objects
924
	 */
925
	public function findOwned($recursive = true, $list = null)
926
	{
927
		// Find objects in these relationships
928
		return $this->findRelatedObjects('owns', $recursive, $list);
929
	}
930
931
	/**
932
	 * Find objects which own this object.
933
	 * Note that objects will only be searched in the same stage as the given record.
934
	 *
935
	 * @param bool $recursive True if recursive
936
	 * @param ArrayList $list Optional list to add items to
937
	 * @return ArrayList list of objects
938
	 */
939
	public function findOwners($recursive = true, $list = null) {
940
		if (!$list) {
941
			$list = new ArrayList();
942
		}
943
944
		// Build reverse lookup for ownership
945
		// @todo - Cache this more intelligently
946
		$rules = $this->lookupReverseOwners();
947
948
		// Hand off to recursive method
949
		return $this->findOwnersRecursive($recursive, $list, $rules);
950
	}
951
952
	/**
953
	 * Find objects which own this object.
954
	 * Note that objects will only be searched in the same stage as the given record.
955
	 *
956
	 * @param bool $recursive True if recursive
957
	 * @param ArrayList $list List to add items to
958
	 * @param array $lookup List of reverse lookup rules for owned objects
959
	 * @return ArrayList list of objects
960
	 */
961
	public function findOwnersRecursive($recursive, $list, $lookup) {
962
		// First pass: find objects that are explicitly owned_by (e.g. custom relationships)
963
		$owners = $this->findRelatedObjects('owned_by', false);
964
965
		// Second pass: Find owners via reverse lookup list
966
		foreach($lookup as $ownedClass => $classLookups) {
967
			// Skip owners of other objects
968
			if(!is_a($this->owner, $ownedClass)) {
969
				continue;
970
			}
971
			foreach($classLookups as $classLookup) {
972
				// Merge new owners into this object's owners
973
				$ownerClass = $classLookup['class'];
974
				$ownerRelation = $classLookup['relation'];
975
				$result = $this->owner->inferReciprocalComponent($ownerClass, $ownerRelation);
976
				$this->mergeRelatedObjects($owners, $result);
977
			}
978
		}
979
980
		// Merge all objects into the main list
981
		$newItems = $this->mergeRelatedObjects($list, $owners);
982
983
		// If recursing, iterate over all newly added items
984
		if($recursive) {
985
			foreach($newItems as $item) {
986
				/** @var Versioned|DataObject $item */
987
				$item->findOwnersRecursive(true, $list, $lookup);
0 ignored issues
show
Bug introduced by
The method findOwnersRecursive does only exist in Versioned, but not in DataObject.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
988
			}
989
		}
990
991
		return $list;
992
	}
993
994
	/**
995
	 * Find a list of classes, each of which with a list of methods to invoke
996
	 * to lookup owners.
997
	 *
998
	 * @return array
999
	 */
1000
	protected function lookupReverseOwners() {
1001
		// Find all classes with 'owns' config
1002
		$lookup = array();
1003
		foreach(ClassInfo::subclassesFor('DataObject') as $class) {
1004
			// Ensure this class is versioned
1005
			if(!Object::has_extension($class, 'Versioned')) {
1006
				continue;
1007
			}
1008
1009
			// Check owned objects for this class
1010
			$owns = Config::inst()->get($class, 'owns', Config::UNINHERITED);
1011
			if(empty($owns)) {
1012
				continue;
1013
			}
1014
1015
			/** @var DataObject $instance */
1016
			$instance = $class::singleton();
1017
			foreach($owns as $owned) {
0 ignored issues
show
Bug introduced by
The expression $owns 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...
1018
				// Find owned class
1019
				$ownedClass = $instance->getRelationClass($owned);
1020
				// Skip custom methods that don't have db relationsm
1021
				if(!$ownedClass) {
1022
					continue;
1023
				}
1024
				if($ownedClass === 'DataObject') {
1025
					throw new LogicException(sprintf(
1026
						"Relation %s on class %s cannot be owned as it is polymorphic",
1027
						$owned, $class
1028
					));
1029
				}
1030
1031
				// Add lookup for owned class
1032
				if(!isset($lookup[$ownedClass])) {
1033
					$lookup[$ownedClass] = array();
1034
				}
1035
				$lookup[$ownedClass][] = [
1036
					'class' => $class,
1037
					'relation' => $owned
1038
				];
1039
			}
1040
		}
1041
		return $lookup;
1042
	}
1043
1044
1045
	/**
1046
	 * Find objects in the given relationships, merging them into the given list
1047
	 *
1048
	 * @param array $source Config property to extract relationships from
1049
	 * @param bool $recursive True if recursive
1050
	 * @param ArrayList $list Optional list to add items to
1051
	 * @return ArrayList The list
1052
	 */
1053
	public function findRelatedObjects($source, $recursive = true, $list = null)
1054
	{
1055
		if (!$list) {
1056
			$list = new ArrayList();
1057
		}
1058
1059
		// Skip search for unsaved records
1060
		$owner = $this->owner;
1061
		if(!$owner->isInDB()) {
1062
			return $list;
1063
		}
1064
1065
		$relationships = $owner->config()->{$source};
1066
		foreach($relationships as $relationship) {
1067
			// Warn if invalid config
1068
			if(!$owner->hasMethod($relationship)) {
1069
				trigger_error(sprintf(
1070
					"Invalid %s config value \"%s\" on object on class \"%s\"",
1071
					$source,
1072
					$relationship,
1073
					$owner->class
1074
				), E_USER_WARNING);
1075
				continue;
1076
			}
1077
1078
			// Inspect value of this relationship
1079
			$items = $owner->{$relationship}();
1080
1081
			// Merge any new item
1082
			$newItems = $this->mergeRelatedObjects($list, $items);
1083
1084
			// Recurse if necessary
1085
			if($recursive) {
1086
				foreach($newItems as $item) {
1087
					/** @var Versioned|DataObject $item */
1088
					$item->findRelatedObjects($source, true, $list);
0 ignored issues
show
Bug introduced by
The method findRelatedObjects does only exist in Versioned, but not in DataObject.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1089
				}
1090
			}
1091
		}
1092
		return $list;
1093
	}
1094
1095
	/**
1096
	 * Helper method to merge owned/owning items into a list.
1097
	 * Items already present in the list will be skipped.
1098
	 *
1099
	 * @param ArrayList $list Items to merge into
1100
	 * @param mixed $items List of new items to merge
1101
	 * @return ArrayList List of all newly added items that did not already exist in $list
1102
	 */
1103
	protected function mergeRelatedObjects($list, $items) {
1104
		$added = new ArrayList();
1105
		if(!$items) {
1106
			return $added;
1107
		}
1108
		if($items instanceof DataObject) {
1109
			$items = array($items);
1110
		}
1111
1112
		/** @var Versioned|DataObject $item */
1113
		foreach($items as $item) {
1114
			// Identify item
1115
			$itemKey = $item->class . '/' . $item->ID;
1116
1117
			// Skip unsaved, unversioned, or already checked objects
1118
			if(!$item->isInDB() || !$item->has_extension('Versioned') || isset($list[$itemKey])) {
1119
				continue;
1120
			}
1121
1122
			// Save record
1123
			$list[$itemKey] = $item;
1124
			$added[$itemKey] = $item;
1125
		}
1126
		return $added;
1127
	}
1128
1129
	/**
1130
	 * This function should return true if the current user can publish this record.
1131
	 * It can be overloaded to customise the security model for an application.
1132
	 *
1133
	 * Denies permission if any of the following conditions is true:
1134
	 * - canPublish() on any extension returns false
1135
	 * - canEdit() returns false
1136
	 *
1137
	 * @param Member $member
1138
	 * @return bool True if the current user can publish this record.
1139
	 */
1140
	public function canPublish($member = null) {
1141
		// Skip if invoked by extendedCan()
1142
		if(func_num_args() > 4) {
1143
			return null;
1144
		}
1145
1146
		if(!$member) {
1147
			$member = Member::currentUser();
1148
		}
1149
1150
		if(Permission::checkMember($member, "ADMIN")) {
1151
			return true;
1152
		}
1153
1154
		// Standard mechanism for accepting permission changes from extensions
1155
		$owner = $this->owner;
1156
		$extended = $owner->extendedCan('canPublish', $member);
1157
		if($extended !== null) {
1158
			return $extended;
1159
		}
1160
1161
		// Default to relying on edit permission
1162
		return $owner->canEdit($member);
1163
	}
1164
1165
	/**
1166
	 * Check if the current user can delete this record from live
1167
	 *
1168
	 * @param null $member
1169
	 * @return mixed
1170
	 */
1171
	public function canUnpublish($member = null) {
1172
		// Skip if invoked by extendedCan()
1173
		if(func_num_args() > 4) {
1174
			return null;
1175
		}
1176
1177
		if(!$member) {
1178
			$member = Member::currentUser();
1179
		}
1180
1181
		if(Permission::checkMember($member, "ADMIN")) {
1182
			return true;
1183
		}
1184
1185
		// Standard mechanism for accepting permission changes from extensions
1186
		$owner = $this->owner;
1187
		$extended = $owner->extendedCan('canUnpublish', $member);
1188
		if($extended !== null) {
1189
			return $extended;
1190
		}
1191
1192
		// Default to relying on canPublish
1193
		return $owner->canPublish($member);
0 ignored issues
show
Bug introduced by
It seems like $member defined by \Member::currentUser() on line 1178 can also be of type object<DataObject>; however, Versioned::canPublish() does only seem to accept object<Member>|null, maybe add an additional type check?

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

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

    return array();
}

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

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

Loading history...
1194
	}
1195
1196
	/**
1197
	 * Check if the current user is allowed to archive this record.
1198
	 * If extended, ensure that both canDelete and canUnpublish are extended also
1199
	 *
1200
	 * @param Member $member
1201
	 * @return bool
1202
	 */
1203
	public function canArchive($member = null) {
1204
		// Skip if invoked by extendedCan()
1205
		if(func_num_args() > 4) {
1206
			return null;
1207
		}
1208
1209
		if(!$member) {
1210
            $member = Member::currentUser();
1211
        }
1212
1213
		if(Permission::checkMember($member, "ADMIN")) {
1214
			return true;
1215
		}
1216
1217
		// Standard mechanism for accepting permission changes from extensions
1218
		$owner = $this->owner;
1219
		$extended = $owner->extendedCan('canArchive', $member);
1220
		if($extended !== null) {
1221
            return $extended;
1222
        }
1223
1224
		// Check if this record can be deleted from stage
1225
        if(!$owner->canDelete($member)) {
1226
            return false;
1227
        }
1228
1229
        // Check if we can delete from live
1230
        if(!$owner->canUnpublish($member)) {
0 ignored issues
show
Bug introduced by
It seems like $member can also be of type object<DataObject>; however, Versioned::canUnpublish() does only seem to accept null, maybe add an additional type check?

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

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

    return array();
}

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

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

Loading history...
1231
            return false;
1232
        }
1233
1234
		return true;
1235
	}
1236
1237
	/**
1238
	 * Check if the user can revert this record to live
1239
	 *
1240
	 * @param Member $member
1241
	 * @return bool
1242
	 */
1243
	public function canRevertToLive($member = null) {
1244
		$owner = $this->owner;
1245
1246
		// Skip if invoked by extendedCan()
1247
		if(func_num_args() > 4) {
1248
			return null;
1249
		}
1250
1251
		// Can't revert if not on live
1252
		if(!$owner->isPublished()) {
1253
			return false;
1254
		}
1255
1256
		if(!$member) {
1257
            $member = Member::currentUser();
1258
        }
1259
1260
		if(Permission::checkMember($member, "ADMIN")) {
1261
			return true;
1262
		}
1263
1264
		// Standard mechanism for accepting permission changes from extensions
1265
		$extended = $owner->extendedCan('canRevertToLive', $member);
1266
		if($extended !== null) {
1267
            return $extended;
1268
        }
1269
1270
		// Default to canEdit
1271
		return $owner->canEdit($member);
1272
	}
1273
1274
	/**
1275
	 * Extend permissions to include additional security for objects that are not published to live.
1276
	 *
1277
	 * @param Member $member
1278
	 * @return bool|null
1279
	 */
1280
	public function canView($member = null) {
1281
		// Invoke default version-gnostic canView
1282
		if ($this->owner->canViewVersioned($member) === false) {
1283
			return false;
1284
		}
1285
	}
1286
1287
	/**
1288
	 * Determine if there are any additional restrictions on this object for the given reading version.
1289
	 *
1290
	 * Override this in a subclass to customise any additional effect that Versioned applies to canView.
1291
	 *
1292
	 * This is expected to be called by canView, and thus is only responsible for denying access if
1293
	 * the default canView would otherwise ALLOW access. Thus it should not be called in isolation
1294
	 * as an authoritative permission check.
1295
	 *
1296
	 * This has the following extension points:
1297
	 *  - canViewDraft is invoked if Mode = stage and Stage = stage
1298
	 *  - canViewArchived is invoked if Mode = archive
1299
	 *
1300
	 * @param Member $member
1301
	 * @return bool False is returned if the current viewing mode denies visibility
1302
	 */
1303
	public function canViewVersioned($member = null) {
1304
		// Bypass when live stage
1305
		$owner = $this->owner;
1306
		$mode = $owner->getSourceQueryParam("Versioned.mode");
1307
		$stage = $owner->getSourceQueryParam("Versioned.stage");
1308
		if ($mode === 'stage' && $stage === static::LIVE) {
1309
			return true;
1310
		}
1311
1312
		// Bypass if site is unsecured
1313
		if (Session::get('unsecuredDraftSite')) {
1314
			return true;
1315
		}
1316
1317
		// Bypass if record doesn't have a live stage
1318
		if(!$this->hasStages()) {
1319
			return true;
1320
		}
1321
1322
		// If we weren't definitely loaded from live, and we can't view non-live content, we need to
1323
		// check to make sure this version is the live version and so can be viewed
1324
		$latestVersion = Versioned::get_versionnumber_by_stage($owner->class, static::LIVE, $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...
1325
		if ($latestVersion == $owner->Version) {
1326
			// Even if this is loaded from a non-live stage, this is the live version
1327
			return true;
1328
		}
1329
1330
		// Extend versioned behaviour
1331
		$extended = $owner->extendedCan('canViewNonLive', $member);
1332
		if($extended !== null) {
1333
			return (bool)$extended;
1334
		}
1335
1336
		// Fall back to default permission check
1337
		$permissions = Config::inst()->get($owner->class, 'non_live_permissions', Config::FIRST_SET);
1338
		$check = Permission::checkMember($member, $permissions);
1339
		return (bool)$check;
1340
	}
1341
1342
	/**
1343
	 * Determines canView permissions for the latest version of this object on a specific stage.
1344
	 * Usually the stage is read from {@link Versioned::current_stage()}.
1345
	 *
1346
	 * This method should be invoked by user code to check if a record is visible in the given stage.
1347
	 *
1348
	 * This method should not be called via ->extend('canViewStage'), but rather should be
1349
	 * overridden in the extended class.
1350
	 *
1351
	 * @param string $stage
1352
	 * @param Member $member
1353
	 * @return bool
1354
	 */
1355
	public function canViewStage($stage = 'Live', $member = null) {
1356
		$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...
1357
		Versioned::set_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...
1358
1359
		$owner = $this->owner;
1360
		$versionFromStage = DataObject::get($owner->class)->byID($owner->ID);
1361
1362
		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...
1363
		return $versionFromStage ? $versionFromStage->canView($member) : false;
1364
	}
1365
1366
	/**
1367
	 * Determine if a class is supporting the Versioned extensions (e.g.
1368
	 * $table_versions does exists).
1369
	 *
1370
	 * @param string $class Class name
1371
	 * @return boolean
1372
	 */
1373
	public function canBeVersioned($class) {
1374
		return ClassInfo::exists($class)
1375
			&& is_subclass_of($class, 'DataObject')
1376
			&& DataObject::has_own_table($class);
1377
	}
1378
1379
	/**
1380
	 * Check if a certain table has the 'Version' field.
1381
	 *
1382
	 * @param string $table Table name
1383
	 *
1384
	 * @return boolean Returns false if the field isn't in the table, true otherwise
1385
	 */
1386
	public function hasVersionField($table) {
1387
		// Base table has version field
1388
		$class = DataObject::getSchema()->tableClass($table);
1389
		return $class === DataObject::getSchema()->baseDataClass($class);
1390
	}
1391
1392
	/**
1393
	 * @param string $table
1394
	 *
1395
	 * @return string
1396
	 */
1397
	public function extendWithSuffix($table) {
1398
		$owner = $this->owner;
1399
		$versionableExtensions = $owner->config()->versionableExtensions;
1400
1401
		if(count($versionableExtensions)){
1402
			foreach ($versionableExtensions as $versionableExtension => $suffixes) {
1403
				if ($owner->hasExtension($versionableExtension)) {
1404
					$ext = $owner->getExtensionInstance($versionableExtension);
1405
					$ext->setOwner($owner);
1406
					$table = $ext->extendWithSuffix($table);
1407
					$ext->clearOwner();
1408
				}
1409
			}
1410
		}
1411
1412
		return $table;
1413
	}
1414
1415
	/**
1416
	 * Get the latest published DataObject.
1417
	 *
1418
	 * @return DataObject
1419
	 */
1420
	public function latestPublished() {
1421
		// Get the root data object class - this will have the version field
1422
		$owner = $this->owner;
1423
		$draftTable = $this->baseTable();
1424
		$liveTable = $this->stageTable($draftTable, static::LIVE);
1425
1426
		return DB::prepared_query("SELECT \"$draftTable\".\"Version\" = \"$liveTable\".\"Version\" FROM \"$draftTable\"
1427
			 INNER JOIN \"$liveTable\" ON \"$draftTable\".\"ID\" = \"$liveTable\".\"ID\"
1428
			 WHERE \"$draftTable\".\"ID\" = ?",
1429
			array($owner->ID)
1430
		)->value();
1431
	}
1432
1433
	/**
1434
	 * @deprecated 4.0..5.0
1435
	 */
1436
	public function doPublish() {
1437
		Deprecation::notice('5.0', 'Use publishRecursive instead');
1438
		return $this->owner->publishRecursive();
1439
	}
1440
1441
	/**
1442
	 * Publish this object and all owned objects to Live
1443
	 *
1444
	 * @return bool
1445
	 */
1446
	public function publishRecursive() {
1447
		$owner = $this->owner;
1448
		if(!$owner->publishSingle()) {
1449
			return false;
1450
		}
1451
1452
		// Publish owned objects
1453
		foreach ($owner->findOwned(false) as $object) {
1454
			/** @var Versioned|DataObject $object */
1455
			$object->publishRecursive();
0 ignored issues
show
Bug introduced by
The method publishRecursive does only exist in Versioned, but not in DataObject.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1456
		}
1457
1458
		// Unlink any objects disowned as a result of this action
1459
		// I.e. objects which aren't owned anymore by this record, but are by the old live record
1460
		$owner->unlinkDisownedObjects(Versioned::DRAFT, Versioned::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...
1461
1462
		return true;
1463
	}
1464
1465
	/**
1466
	 * Publishes this object to Live, but doesn't publish owned objects.
1467
	 *
1468
	 * @return bool True if publish was successful
1469
	 */
1470
	public function publishSingle() {
1471
		$owner = $this->owner;
1472
		if(!$owner->canPublish()) {
1473
			return false;
1474
		}
1475
1476
		$owner->invokeWithExtensions('onBeforePublish');
1477
		$owner->write();
1478
		$owner->copyVersionToStage(static::DRAFT, static::LIVE);
1479
		$owner->invokeWithExtensions('onAfterPublish');
1480
		return true;
1481
	}
1482
1483
	/**
1484
	 * Set foreign keys of has_many objects to 0 where those objects were
1485
	 * disowned as a result of a partial publish / unpublish.
1486
	 * I.e. this object and its owned objects were recently written to $targetStage,
1487
	 * but deleted objects were not.
1488
	 *
1489
	 * Note that this operation does not create any new Versions
1490
	 *
1491
	 * @param string $sourceStage Objects in this stage will not be unlinked.
1492
	 * @param string $targetStage Objects which exist in this stage but not $sourceStage
1493
	 * will be unlinked.
1494
	 */
1495
	public function unlinkDisownedObjects($sourceStage, $targetStage) {
1496
		$owner = $this->owner;
1497
1498
		// after publishing, objects which used to be owned need to be
1499
		// dis-connected from this object (set ForeignKeyID = 0)
1500
		$owns = $owner->config()->owns;
1501
		$hasMany = $owner->config()->has_many;
1502
		if(empty($owns) || empty($hasMany)) {
1503
			return;
1504
		}
1505
1506
		$ownedHasMany = array_intersect($owns, array_keys($hasMany));
1507
		foreach($ownedHasMany as $relationship) {
1508
			// Find metadata on relationship
1509
			$joinClass = $owner->hasManyComponent($relationship);
1510
			$joinField = $owner->getRemoteJoinField($relationship, 'has_many', $polymorphic);
0 ignored issues
show
Bug introduced by
The variable $polymorphic does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
1511
			$idField = $polymorphic ? "{$joinField}ID" : $joinField;
1512
			$joinTable = DataObject::getSchema()->tableForField($joinClass, $idField);
1513
1514
			// Generate update query which will unlink disowned objects
1515
			$targetTable = $this->stageTable($joinTable, $targetStage);
1516
			$disowned = new SQLUpdate("\"{$targetTable}\"");
1517
			$disowned->assign("\"{$idField}\"", 0);
1518
			$disowned->addWhere(array(
1519
				"\"{$targetTable}\".\"{$idField}\"" => $owner->ID
1520
			));
1521
1522
			// Build exclusion list (items to owned objects we need to keep)
1523
			$sourceTable = $this->stageTable($joinTable, $sourceStage);
1524
			$owned = new SQLSelect("\"{$sourceTable}\".\"ID\"", "\"{$sourceTable}\"");
1525
			$owned->addWhere(array(
1526
				"\"{$sourceTable}\".\"{$idField}\"" => $owner->ID
1527
			));
1528
1529
			// Apply class condition if querying on polymorphic has_one
1530
			if($polymorphic) {
1531
				$disowned->assign("\"{$joinField}Class\"", null);
1532
				$disowned->addWhere(array(
1533
					"\"{$targetTable}\".\"{$joinField}Class\"" => get_class($owner)
1534
				));
1535
				$owned->addWhere(array(
1536
					"\"{$sourceTable}\".\"{$joinField}Class\"" => get_class($owner)
1537
				));
1538
			}
1539
1540
			// Merge queries and perform unlink
1541
			$ownedSQL = $owned->sql($ownedParams);
1542
			$disowned->addWhere(array(
1543
				"\"{$targetTable}\".\"ID\" NOT IN ({$ownedSQL})" => $ownedParams
1544
			));
1545
1546
			$owner->extend('updateDisownershipQuery', $disowned, $sourceStage, $targetStage, $relationship);
0 ignored issues
show
Bug introduced by
The method extend() does not exist on Versioned. Did you maybe mean extendWithSuffix()?

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

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

Loading history...
1547
1548
			$disowned->execute();
1549
		}
1550
	}
1551
1552
	/**
1553
	 * Removes the record from both live and stage
1554
	 *
1555
	 * @return bool Success
1556
	 */
1557
	public function doArchive() {
1558
		$owner = $this->owner;
1559
		if(!$owner->canArchive()) {
1560
			return false;
1561
		}
1562
1563
		$owner->invokeWithExtensions('onBeforeArchive', $this);
1564
		$owner->doUnpublish();
1565
		$owner->delete();
0 ignored issues
show
Bug introduced by
The method delete() does not exist on Versioned. Did you maybe mean deleteFromStage()?

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

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

Loading history...
1566
		$owner->invokeWithExtensions('onAfterArchive', $this);
1567
1568
		return true;
1569
	}
1570
1571
	/**
1572
	 * Removes this record from the live site
1573
	 *
1574
	 * @return bool Flag whether the unpublish was successful
1575
	 */
1576
	public function doUnpublish() {
1577
		$owner = $this->owner;
1578
		if(!$owner->canUnpublish()) {
1579
			return false;
1580
		}
1581
1582
		// Skip if this record isn't saved
1583
		if(!$owner->isInDB()) {
1584
			return false;
1585
		}
1586
1587
		// Skip if this record isn't on live
1588
		if(!$owner->isPublished()) {
1589
			return false;
1590
		}
1591
1592
		$owner->invokeWithExtensions('onBeforeUnpublish');
1593
1594
		$origReadingMode = static::get_reading_mode();
1595
		static::set_stage(static::LIVE);
1596
1597
		// This way our ID won't be unset
1598
		$clone = clone $owner;
1599
		$clone->delete();
0 ignored issues
show
Bug introduced by
The method delete() does not exist on Versioned. Did you maybe mean deleteFromStage()?

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

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

Loading history...
1600
1601
		static::set_reading_mode($origReadingMode);
1602
1603
		$owner->invokeWithExtensions('onAfterUnpublish');
1604
		return true;
1605
	}
1606
1607
	/**
1608
	 * Trigger unpublish of owning objects
1609
	 */
1610
	public function onAfterUnpublish() {
1611
		$owner = $this->owner;
1612
1613
		// Any objects which owned (and thus relied on the unpublished object) are now unpublished automatically.
1614
		foreach ($owner->findOwners(false) as $object) {
1615
			/** @var Versioned|DataObject $object */
1616
			$object->doUnpublish();
0 ignored issues
show
Bug introduced by
The method doUnpublish does only exist in Versioned, but not in DataObject.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1617
		}
1618
	}
1619
1620
1621
	/**
1622
	 * Revert the draft changes: replace the draft content with the content on live
1623
	 *
1624
	 * @return bool True if the revert was successful
1625
	 */
1626
	public function doRevertToLive() {
1627
		$owner = $this->owner;
1628
		if(!$owner->canRevertToLive()) {
1629
			return false;
1630
		}
1631
1632
		$owner->invokeWithExtensions('onBeforeRevertToLive');
1633
		$owner->copyVersionToStage(static::LIVE, static::DRAFT, false);
1634
		$owner->invokeWithExtensions('onAfterRevertToLive');
1635
		return true;
1636
	}
1637
1638
	/**
1639
	 * Trigger revert of all owned objects to stage
1640
	 */
1641
	public function onAfterRevertToLive() {
1642
		$owner = $this->owner;
1643
		/** @var Versioned|DataObject $liveOwner */
1644
		$liveOwner = static::get_by_stage(get_class($owner), static::LIVE)
1645
			->byID($owner->ID);
1646
1647
		// Revert any owned objects from the live stage only
1648
		foreach ($liveOwner->findOwned(false) as $object) {
0 ignored issues
show
Bug introduced by
The method findOwned does only exist in Versioned, but not in DataObject.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1649
			/** @var Versioned|DataObject $object */
1650
			$object->doRevertToLive();
0 ignored issues
show
Bug introduced by
The method doRevertToLive does only exist in Versioned, but not in DataObject.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1651
		}
1652
1653
		// Unlink any objects disowned as a result of this action
1654
		// I.e. objects which aren't owned anymore by this record, but are by the old draft record
1655
		$owner->unlinkDisownedObjects(Versioned::LIVE, Versioned::DRAFT);
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...
1656
	}
1657
1658
	/**
1659
	 * @deprecated 4.0..5.0
1660
	 */
1661
	public function publish($fromStage, $toStage, $createNewVersion = false) {
1662
		Deprecation::notice('5.0', 'Use copyVersionToStage instead');
1663
		$this->owner->copyVersionToStage($fromStage, $toStage, $createNewVersion);
1664
	}
1665
1666
	/**
1667
	 * Move a database record from one stage to the other.
1668
	 *
1669
	 * @param int|string $fromStage Place to copy from.  Can be either a stage name or a version number.
1670
	 * @param string $toStage Place to copy to.  Must be a stage name.
1671
	 * @param bool $createNewVersion Set this to true to create a new version number.
1672
	 * By default, the existing version number will be copied over.
1673
	 */
1674
	public function copyVersionToStage($fromStage, $toStage, $createNewVersion = false) {
1675
		$owner = $this->owner;
1676
		$owner->invokeWithExtensions('onBeforeVersionedPublish', $fromStage, $toStage, $createNewVersion);
1677
1678
		$baseClass = $owner->baseClass();
1679
1680
		/** @var Versioned|DataObject $from */
1681
		if(is_numeric($fromStage)) {
1682
			$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...
1683
		} else {
1684
			$owner->flushCache();
1685
			$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...
1686
				"\"{$baseClass}\".\"ID\" = ?" => $owner->ID
1687
			));
1688
		}
1689
		if(!$from) {
1690
			throw new InvalidArgumentException("Can't find {$baseClass}#{$owner->ID} in stage {$fromStage}");
1691
		}
1692
1693
		$from->forceChange();
1694
		if($createNewVersion) {
1695
			// Clear version to be automatically created on write
1696
			$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...
1697
		} else {
1698
			$from->migrateVersion($from->Version);
1699
1700
			// Mark this version as having been published at some stage
1701
			$publisherID = isset(Member::currentUser()->ID) ? Member::currentUser()->ID : 0;
1702
			$extTable = $this->extendWithSuffix($baseClass);
1703
			DB::prepared_query("UPDATE \"{$extTable}_versions\"
1704
				SET \"WasPublished\" = ?, \"PublisherID\" = ?
1705
				WHERE \"RecordID\" = ? AND \"Version\" = ?",
1706
				array(1, $publisherID, $from->ID, $from->Version)
1707
			);
1708
		}
1709
1710
		// Change to new stage, write, and revert state
1711
		$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...
1712
		Versioned::set_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...
1713
1714
		// Migrate stage prior to write
1715
		$from->setSourceQueryParam('Versioned.mode', 'stage');
1716
		$from->setSourceQueryParam('Versioned.stage', $toStage);
1717
1718
		$conn = DB::get_conn();
1719
		if(method_exists($conn, 'allowPrimaryKeyEditing')) {
1720
			$conn->allowPrimaryKeyEditing($baseClass, true);
1721
			$from->write();
1722
			$conn->allowPrimaryKeyEditing($baseClass, false);
1723
		} else {
1724
			$from->write();
1725
		}
1726
1727
		$from->destroy();
1728
1729
		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...
1730
1731
		$owner->invokeWithExtensions('onAfterVersionedPublish', $fromStage, $toStage, $createNewVersion);
1732
	}
1733
1734
	/**
1735
	 * Set the migrating version.
1736
	 *
1737
	 * @param string $version The version.
1738
	 */
1739
	public function migrateVersion($version) {
1740
		$this->migratingVersion = $version;
1741
	}
1742
1743
	/**
1744
	 * Compare two stages to see if they're different.
1745
	 *
1746
	 * Only checks the version numbers, not the actual content.
1747
	 *
1748
	 * @param string $stage1 The first stage to check.
1749
	 * @param string $stage2
1750
	 * @return bool
1751
	 */
1752
	public function stagesDiffer($stage1, $stage2) {
1753
		$table1 = $this->baseTable($stage1);
1754
		$table2 = $this->baseTable($stage2);
1755
1756
		$owner = $this->owner;
1757
		if(!is_numeric($owner->ID)) {
1758
			return true;
1759
		}
1760
1761
		// We test for equality - if one of the versions doesn't exist, this
1762
		// will be false.
1763
1764
		// TODO: DB Abstraction: if statement here:
1765
		$stagesAreEqual = DB::prepared_query(
1766
			"SELECT CASE WHEN \"$table1\".\"Version\"=\"$table2\".\"Version\" THEN 1 ELSE 0 END
1767
			 FROM \"$table1\" INNER JOIN \"$table2\" ON \"$table1\".\"ID\" = \"$table2\".\"ID\"
1768
			 AND \"$table1\".\"ID\" = ?",
1769
			array($owner->ID)
1770
		)->value();
1771
1772
		return !$stagesAreEqual;
1773
	}
1774
1775
	/**
1776
	 * @param string $filter
1777
	 * @param string $sort
1778
	 * @param string $limit
1779
	 * @param string $join Deprecated, use leftJoin($table, $joinClause) instead
1780
	 * @param string $having
1781
	 * @return ArrayList
1782
	 */
1783
	public function Versions($filter = "", $sort = "", $limit = "", $join = "", $having = "") {
1784
		return $this->allVersions($filter, $sort, $limit, $join, $having);
1785
	}
1786
1787
	/**
1788
	 * Return a list of all the versions available.
1789
	 *
1790
	 * @param  string $filter
1791
	 * @param  string $sort
1792
	 * @param  string $limit
1793
	 * @param  string $join   Deprecated, use leftJoin($table, $joinClause) instead
1794
	 * @param  string $having
1795
	 * @return ArrayList
1796
	 */
1797
	public function allVersions($filter = "", $sort = "", $limit = "", $join = "", $having = "") {
1798
		// Make sure the table names are not postfixed (e.g. _Live)
1799
		$oldMode = static::get_reading_mode();
1800
		static::set_stage(static::DRAFT);
1801
1802
		$owner = $this->owner;
1803
		$list = DataObject::get(get_class($owner), $filter, $sort, $join, $limit);
1804
		if($having) {
1805
			$list->having($having);
1806
		}
1807
1808
		$query = $list->dataQuery()->query();
1809
1810
		foreach($query->getFrom() as $table => $tableJoin) {
1811
			if(is_string($tableJoin) && $tableJoin[0] == '"') {
1812
				$baseTable = str_replace('"','',$tableJoin);
1813
			} elseif(is_string($tableJoin) && substr($tableJoin,0,5) != 'INNER') {
1814
				$query->setFrom(array(
1815
					$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...
1816
						. " AND \"$table\".\"Version\" = \"{$baseTable}_versions\".\"Version\""
1817
				));
1818
			}
1819
			$query->renameTable($table, $table . '_versions');
1820
		}
1821
1822
		// Add all <basetable>_versions columns
1823
		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...
1824
			$query->selectField(sprintf('"%s_versions"."%s"', $baseTable, $name), $name);
1825
		}
1826
1827
		$query->addWhere(array(
1828
			"\"{$baseTable}_versions\".\"RecordID\" = ?" => $owner->ID
1829
		));
1830
		$query->setOrderBy(($sort) ? $sort
1831
			: "\"{$baseTable}_versions\".\"LastEdited\" DESC, \"{$baseTable}_versions\".\"Version\" DESC");
1832
1833
		$records = $query->execute();
1834
		$versions = new ArrayList();
1835
1836
		foreach($records as $record) {
1837
			$versions->push(new Versioned_Version($record));
1838
		}
1839
1840
		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...
1841
		return $versions;
1842
	}
1843
1844
	/**
1845
	 * Compare two version, and return the diff between them.
1846
	 *
1847
	 * @param string $from The version to compare from.
1848
	 * @param string $to The version to compare to.
1849
	 *
1850
	 * @return DataObject
1851
	 */
1852
	public function compareVersions($from, $to) {
1853
		$owner = $this->owner;
1854
		$fromRecord = Versioned::get_version($owner->class, $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...
1855
		$toRecord = Versioned::get_version($owner->class, $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...
1856
1857
		$diff = new DataDifferencer($fromRecord, $toRecord);
0 ignored issues
show
Bug introduced by
It seems like $toRecord defined by \Versioned::get_version(...class, $owner->ID, $to) on line 1855 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...
1858
1859
		return $diff->diffedData();
1860
	}
1861
1862
	/**
1863
	 * Return the base table - the class that directly extends DataObject.
1864
	 *
1865
	 * Protected so it doesn't conflict with DataObject::baseTable()
1866
	 *
1867
	 * @param string $stage
1868
	 * @return string
1869
	 */
1870
	protected function baseTable($stage = null) {
1871
		$baseTable = $this->owner->baseTable();
1872
		return $this->stageTable($baseTable, $stage);
1873
	}
1874
1875
	/**
1876
	 * Given a table and stage determine the table name.
1877
	 *
1878
	 * Note: Stages this asset does not exist in will default to the draft table.
1879
	 *
1880
	 * @param string $table Main table
1881
	 * @param string $stage
1882
	 * @return string Staged table name
1883
	 */
1884
	public function stageTable($table, $stage) {
1885
		if($this->hasStages() && $stage === static::LIVE) {
1886
			return "{$table}_{$stage}";
1887
		}
1888
		return $table;
1889
	}
1890
1891
	//-----------------------------------------------------------------------------------------------//
1892
1893
1894
	/**
1895
	 * Determine if the current user is able to set the given site stage / archive
1896
	 *
1897
	 * @param SS_HTTPRequest $request
1898
	 * @return bool
1899
	 */
1900
	public static function can_choose_site_stage($request) {
1901
		// Request is allowed if stage isn't being modified
1902
		if((!$request->getVar('stage') || $request->getVar('stage') === static::LIVE)
1903
			&& !$request->getVar('archiveDate')
1904
		) {
1905
			return true;
1906
		}
1907
1908
		// Check permissions with member ID in session.
1909
		$member = Member::currentUser();
1910
		$permissions = Config::inst()->get(get_called_class(), 'non_live_permissions');
1911
		return $member && Permission::checkMember($member, $permissions);
1912
	}
1913
1914
	/**
1915
	 * Choose the stage the site is currently on.
1916
	 *
1917
	 * If $_GET['stage'] is set, then it will use that stage, and store it in
1918
	 * the session.
1919
	 *
1920
	 * if $_GET['archiveDate'] is set, it will use that date, and store it in
1921
	 * the session.
1922
	 *
1923
	 * If neither of these are set, it checks the session, otherwise the stage
1924
	 * is set to 'Live'.
1925
	 */
1926
	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...
1927
		// Check any pre-existing session mode
1928
		$preexistingMode = Session::get('readingMode');
1929
1930
		// Determine the reading mode
1931
		if(isset($_GET['stage'])) {
1932
			$stage = ucfirst(strtolower($_GET['stage']));
1933
			if(!in_array($stage, array(static::DRAFT, static::LIVE))) {
1934
				$stage = static::LIVE;
1935
			}
1936
			$mode = 'Stage.' . $stage;
1937
		} elseif (isset($_GET['archiveDate']) && strtotime($_GET['archiveDate'])) {
1938
			$mode = 'Archive.' . $_GET['archiveDate'];
1939
		} elseif($preexistingMode) {
1940
			$mode = $preexistingMode;
1941
		} else {
1942
			$mode = static::DEFAULT_MODE;
1943
		}
1944
1945
		// Save reading mode
1946
		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...
1947
1948
		// Try not to store the mode in the session if not needed
1949
		if(($preexistingMode && $preexistingMode !== $mode)
1950
			|| (!$preexistingMode && $mode !== static::DEFAULT_MODE)
1951
		) {
1952
			Session::set('readingMode', $mode);
1953
		}
1954
1955
		if(!headers_sent() && !Director::is_cli()) {
1956
			if(Versioned::get_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...
1957
				// clear the cookie if it's set
1958
				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...
1959
					Cookie::force_expiry('bypassStaticCache', null, null, false, true /* httponly */);
1960
				}
1961
			} else {
1962
				// set the cookie if it's cleared
1963
				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...
1964
					Cookie::set('bypassStaticCache', '1', 0, null, null, false, true /* httponly */);
1965
				}
1966
			}
1967
		}
1968
	}
1969
1970
	/**
1971
	 * Set the current reading mode.
1972
	 *
1973
	 * @param string $mode
1974
	 */
1975
	public static function set_reading_mode($mode) {
1976
		self::$reading_mode = $mode;
1977
	}
1978
1979
	/**
1980
	 * Get the current reading mode.
1981
	 *
1982
	 * @return string
1983
	 */
1984
	public static function get_reading_mode() {
1985
		return self::$reading_mode;
1986
	}
1987
1988
	/**
1989
	 * Get the current reading stage.
1990
	 *
1991
	 * @return string
1992
	 */
1993
	public static function get_stage() {
1994
		$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...
1995
1996
		if($parts[0] == 'Stage') {
1997
			return $parts[1];
1998
		}
1999
	}
2000
2001
	/**
2002
	 * Get the current archive date.
2003
	 *
2004
	 * @return string
2005
	 */
2006
	public static function current_archived_date() {
2007
		$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...
2008
		if($parts[0] == 'Archive') {
2009
			return $parts[1];
2010
		}
2011
	}
2012
2013
	/**
2014
	 * Set the reading stage.
2015
	 *
2016
	 * @param string $stage New reading stage.
2017
	 * @throws InvalidArgumentException
2018
	 */
2019
	public static function set_stage($stage) {
2020
		if(!in_array($stage, [static::LIVE, static::DRAFT])) {
2021
			throw new \InvalidArgumentException("Invalid stage name \"{$stage}\"");
2022
		}
2023
		static::set_reading_mode('Stage.' . $stage);
2024
	}
2025
2026
	/**
2027
	 * Set the reading archive date.
2028
	 *
2029
	 * @param string $date New reading archived date.
2030
	 */
2031
	public static function reading_archived_date($date) {
2032
		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...
2033
	}
2034
2035
2036
	/**
2037
	 * Get a singleton instance of a class in the given stage.
2038
	 *
2039
	 * @param string $class The name of the class.
2040
	 * @param string $stage The name of the stage.
2041
	 * @param string $filter A filter to be inserted into the WHERE clause.
2042
	 * @param boolean $cache Use caching.
2043
	 * @param string $sort A sort expression to be inserted into the ORDER BY clause.
2044
	 *
2045
	 * @return DataObject
2046
	 */
2047
	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...
2048
		// TODO: No identity cache operating
2049
		$items = static::get_by_stage($class, $stage, $filter, $sort, null, 1);
2050
2051
		return $items->First();
2052
	}
2053
2054
	/**
2055
	 * Gets the current version number of a specific record.
2056
	 *
2057
	 * @param string $class
2058
	 * @param string $stage
2059
	 * @param int $id
2060
	 * @param boolean $cache
2061
	 *
2062
	 * @return int
2063
	 */
2064
	public static function get_versionnumber_by_stage($class, $stage, $id, $cache = true) {
2065
		$baseClass = DataObject::getSchema()->baseDataClass($class);
2066
		$stageTable = DataObject::getSchema()->tableName($baseClass);
2067
		if($stage === static::LIVE) {
2068
			$stageTable .= "_{$stage}";
2069
		}
2070
2071
		// cached call
2072
		if($cache && isset(self::$cache_versionnumber[$baseClass][$stage][$id])) {
2073
			return self::$cache_versionnumber[$baseClass][$stage][$id];
2074
		}
2075
2076
		// get version as performance-optimized SQL query (gets called for each record in the sitetree)
2077
		$version = DB::prepared_query(
2078
			"SELECT \"Version\" FROM \"$stageTable\" WHERE \"ID\" = ?",
2079
			array($id)
2080
		)->value();
2081
2082
		// cache value (if required)
2083
		if($cache) {
2084
			if(!isset(self::$cache_versionnumber[$baseClass])) {
2085
				self::$cache_versionnumber[$baseClass] = array();
2086
			}
2087
2088
			if(!isset(self::$cache_versionnumber[$baseClass][$stage])) {
2089
				self::$cache_versionnumber[$baseClass][$stage] = array();
2090
			}
2091
2092
			self::$cache_versionnumber[$baseClass][$stage][$id] = $version;
2093
		}
2094
2095
		return $version;
2096
	}
2097
2098
	/**
2099
	 * Pre-populate the cache for Versioned::get_versionnumber_by_stage() for
2100
	 * a list of record IDs, for more efficient database querying.  If $idList
2101
	 * is null, then every record will be pre-cached.
2102
	 *
2103
	 * @param string $class
2104
	 * @param string $stage
2105
	 * @param array $idList
2106
	 */
2107
	public static function prepopulate_versionnumber_cache($class, $stage, $idList = null) {
2108
		if (!Config::inst()->get('Versioned', 'prepopulate_versionnumber_cache')) {
2109
			return;
2110
		}
2111
		$filter = "";
2112
		$parameters = array();
2113
		if($idList) {
2114
			// Validate the ID list
2115
			foreach($idList as $id) {
2116
				if(!is_numeric($id)) {
2117
					user_error("Bad ID passed to Versioned::prepopulate_versionnumber_cache() in \$idList: " . $id,
2118
					E_USER_ERROR);
2119
				}
2120
			}
2121
			$filter = 'WHERE "ID" IN ('.DB::placeholders($idList).')';
2122
			$parameters = $idList;
2123
		}
2124
2125
		/** @var Versioned|DataObject $singleton */
2126
		$singleton = DataObject::singleton($class);
2127
		$baseClass = $singleton->baseClass();
0 ignored issues
show
Bug introduced by
The method baseClass does only exist in DataObject, but not in Versioned.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
2128
		$baseTable = $singleton->baseTable();
2129
		$stageTable = $singleton->stageTable($baseTable, $stage);
0 ignored issues
show
Bug introduced by
The method stageTable does only exist in Versioned, but not in DataObject.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
2130
2131
		$versions = DB::prepared_query("SELECT \"ID\", \"Version\" FROM \"$stageTable\" $filter", $parameters)->map();
2132
2133
		foreach($versions as $id => $version) {
2134
			self::$cache_versionnumber[$baseClass][$stage][$id] = $version;
2135
		}
2136
	}
2137
2138
	/**
2139
	 * Get a set of class instances by the given stage.
2140
	 *
2141
	 * @param string $class The name of the class.
2142
	 * @param string $stage The name of the stage.
2143
	 * @param string $filter A filter to be inserted into the WHERE clause.
2144
	 * @param string $sort A sort expression to be inserted into the ORDER BY clause.
2145
	 * @param string $join Deprecated, use leftJoin($table, $joinClause) instead
2146
	 * @param int $limit A limit on the number of records returned from the database.
2147
	 * @param string $containerClass The container class for the result set (default is DataList)
2148
	 *
2149
	 * @return DataList A modified DataList designated to the specified stage
2150
	 */
2151
	public static function get_by_stage(
2152
		$class, $stage, $filter = '', $sort = '', $join = '', $limit = null, $containerClass = 'DataList'
2153
	) {
2154
		$result = DataObject::get($class, $filter, $sort, $join, $limit, $containerClass);
2155
		return $result->setDataQueryParam(array(
2156
			'Versioned.mode' => 'stage',
2157
			'Versioned.stage' => $stage
2158
		));
2159
	}
2160
2161
	/**
2162
	 * Delete this record from the given stage
2163
	 *
2164
	 * @param string $stage
2165
	 */
2166
	public function deleteFromStage($stage) {
2167
		$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...
2168
		Versioned::set_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...
2169
		$owner = $this->owner;
2170
		$clone = clone $owner;
2171
		$clone->delete();
0 ignored issues
show
Bug introduced by
The method delete() does not exist on Versioned. Did you maybe mean deleteFromStage()?

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

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

Loading history...
2172
		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...
2173
2174
		// Fix the version number cache (in case you go delete from stage and then check ExistsOnLive)
2175
		$baseClass = $owner->baseClass();
2176
		self::$cache_versionnumber[$baseClass][$stage][$owner->ID] = null;
2177
	}
2178
2179
	/**
2180
	 * Write the given record to the draft stage
2181
	 *
2182
	 * @param string $stage
2183
	 * @param boolean $forceInsert
2184
	 * @return int The ID of the record
2185
	 */
2186
	public function writeToStage($stage, $forceInsert = false) {
2187
		$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...
2188
		Versioned::set_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...
2189
2190
		$owner = $this->owner;
2191
		$owner->forceChange();
2192
		$result = $owner->write(false, $forceInsert);
2193
		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...
2194
2195
		return $result;
2196
	}
2197
2198
	/**
2199
	 * Roll the draft version of this record to match the published record.
2200
	 * Caution: Doesn't overwrite the object properties with the rolled back version.
2201
	 *
2202
	 * {@see doRevertToLive()} to reollback to live
2203
	 *
2204
	 * @param int $version Version number
2205
	 */
2206
	public function doRollbackTo($version) {
2207
		$owner = $this->owner;
2208
		$owner->extend('onBeforeRollback', $version);
0 ignored issues
show
Bug introduced by
The method extend() does not exist on Versioned. Did you maybe mean extendWithSuffix()?

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

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

Loading history...
2209
		$owner->copyVersionToStage($version, static::DRAFT, true);
2210
		$owner->writeWithoutVersion();
2211
		$owner->extend('onAfterRollback', $version);
0 ignored issues
show
Bug introduced by
The method extend() does not exist on Versioned. Did you maybe mean extendWithSuffix()?

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

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

Loading history...
2212
	}
2213
2214
	public function onAfterRollback($version) {
2215
		// Find record at this version
2216
		$baseClass = DataObject::getSchema()->baseDataClass($this->owner);
2217
		/** @var Versioned|DataObject $recordVersion */
2218
		$recordVersion = static::get_version($baseClass, $this->owner->ID, $version);
2219
2220
		// Note that unlike other publishing actions, rollback is NOT recursive;
2221
		// The owner collects all objects and writes them back using writeToStage();
2222
		foreach ($recordVersion->findOwned() as $object) {
0 ignored issues
show
Bug introduced by
The method findOwned does only exist in Versioned, but not in DataObject.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
2223
			/** @var Versioned|DataObject $object */
2224
			$object->writeToStage(static::DRAFT);
0 ignored issues
show
Bug introduced by
The method writeToStage does only exist in Versioned, but not in DataObject.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
2225
		}
2226
	}
2227
2228
	/**
2229
	 * Return the latest version of the given record.
2230
	 *
2231
	 * @param string $class
2232
	 * @param int $id
2233
	 * @return DataObject
2234
	 */
2235
	public static function get_latest_version($class, $id) {
2236
		$baseClass = DataObject::getSchema()->baseDataClass($class);
2237
		$list = DataList::create($baseClass)
2238
			->setDataQueryParam("Versioned.mode", "latest_versions");
2239
2240
		return $list->byID($id);
2241
	}
2242
2243
	/**
2244
	 * Returns whether the current record is the latest one.
2245
	 *
2246
	 * @todo Performance - could do this directly via SQL.
2247
	 *
2248
	 * @see get_latest_version()
2249
	 * @see latestPublished
2250
	 *
2251
	 * @return boolean
2252
	 */
2253
	public function isLatestVersion() {
2254
		$owner = $this->owner;
2255
		if(!$owner->isInDB()) {
2256
			return false;
2257
		}
2258
2259
		$version = static::get_latest_version($owner->class, $owner->ID);
2260
		return ($version->Version == $owner->Version);
2261
	}
2262
2263
	/**
2264
	 * Check if this record exists on live
2265
	 *
2266
	 * @return bool
2267
	 */
2268
	public function isPublished() {
2269
		$owner = $this->owner;
2270
		if(!$owner->isInDB()) {
2271
			return false;
2272
		}
2273
2274
		// Non-staged objects are considered "published" if saved
2275
		if(!$this->hasStages()) {
2276
			return true;
2277
		}
2278
2279
		$table = $this->baseTable(static::LIVE);
2280
		$result = DB::prepared_query(
2281
			"SELECT COUNT(*) FROM \"{$table}\" WHERE \"{$table}\".\"ID\" = ?",
2282
			array($owner->ID)
2283
		);
2284
		return (bool)$result->value();
2285
	}
2286
2287
	/**
2288
	 * Check if this record exists on the draft stage
2289
	 *
2290
	 * @return bool
2291
	 */
2292
	public function isOnDraft() {
2293
		$owner = $this->owner;
2294
		if(!$owner->isInDB()) {
2295
			return false;
2296
		}
2297
2298
		$table = $this->baseTable();
2299
		$result = DB::prepared_query(
2300
			"SELECT COUNT(*) FROM \"{$table}\" WHERE \"{$table}\".\"ID\" = ?",
2301
			array($owner->ID)
2302
		);
2303
		return (bool)$result->value();
2304
	}
2305
2306
2307
2308
	/**
2309
	 * Return the equivalent of a DataList::create() call, querying the latest
2310
	 * version of each record stored in the (class)_versions tables.
2311
	 *
2312
	 * In particular, this will query deleted records as well as active ones.
2313
	 *
2314
	 * @param string $class
2315
	 * @param string $filter
2316
	 * @param string $sort
2317
	 * @return DataList
2318
	 */
2319
	public static function get_including_deleted($class, $filter = "", $sort = "") {
2320
		$list = DataList::create($class)
2321
			->where($filter)
2322
			->sort($sort)
2323
			->setDataQueryParam("Versioned.mode", "latest_versions");
2324
2325
		return $list;
2326
	}
2327
2328
	/**
2329
	 * Return the specific version of the given id.
2330
	 *
2331
	 * Caution: The record is retrieved as a DataObject, but saving back
2332
	 * modifications via write() will create a new version, rather than
2333
	 * modifying the existing one.
2334
	 *
2335
	 * @param string $class
2336
	 * @param int $id
2337
	 * @param int $version
2338
	 *
2339
	 * @return DataObject
2340
	 */
2341
	public static function get_version($class, $id, $version) {
2342
		$baseClass = DataObject::getSchema()->baseDataClass($class);
2343
		$list = DataList::create($baseClass)
2344
			->setDataQueryParam([
2345
				"Versioned.mode" => 'version',
2346
				"Versioned.version" => $version
2347
			]);
2348
2349
		return $list->byID($id);
2350
	}
2351
2352
	/**
2353
	 * Return a list of all versions for a given id.
2354
	 *
2355
	 * @param string $class
2356
	 * @param int $id
2357
	 *
2358
	 * @return DataList
2359
	 */
2360
	public static function get_all_versions($class, $id) {
2361
		$list = DataList::create($class)
2362
			->filter('ID', $id)
2363
			->setDataQueryParam('Versioned.mode', 'all_versions');
2364
2365
		return $list;
2366
	}
2367
2368
	/**
2369
	 * @param array $labels
2370
	 */
2371
	public function updateFieldLabels(&$labels) {
2372
		$labels['Versions'] = _t('Versioned.has_many_Versions', 'Versions', 'Past Versions of this record');
2373
	}
2374
2375
	/**
2376
	 * @param FieldList
2377
	 */
2378
	public function updateCMSFields(FieldList $fields) {
2379
		// remove the version field from the CMS as this should be left
2380
		// entirely up to the extension (not the cms user).
2381
		$fields->removeByName('Version');
2382
	}
2383
2384
	/**
2385
	 * Ensure version ID is reset to 0 on duplicate
2386
	 *
2387
	 * @param DataObject $source Record this was duplicated from
2388
	 * @param bool $doWrite
2389
	 */
2390
	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...
2391
		$this->owner->Version = 0;
2392
	}
2393
2394
	public function flushCache() {
2395
		self::$cache_versionnumber = array();
2396
	}
2397
2398
	/**
2399
	 * Return a piece of text to keep DataObject cache keys appropriately specific.
2400
	 *
2401
	 * @return string
2402
	 */
2403
	public function cacheKeyComponent() {
2404
		return 'versionedmode-'.static::get_reading_mode();
2405
	}
2406
2407
	/**
2408
	 * Returns an array of possible stages.
2409
	 *
2410
	 * @return array
2411
	 */
2412
	public function getVersionedStages() {
2413
		if($this->hasStages()) {
2414
			return [static::DRAFT, static::LIVE];
2415
		} else {
2416
			return [static::DRAFT];
2417
		}
2418
	}
2419
2420
	public static function get_template_global_variables() {
2421
		return array(
2422
			'CurrentReadingMode' => 'get_reading_mode'
2423
		);
2424
	}
2425
2426
	/**
2427
	 * Check if this object has stages
2428
	 *
2429
	 * @return bool True if this object is staged
2430
	 */
2431
	public function hasStages() {
2432
		return $this->mode === static::STAGEDVERSIONED;
2433
	}
2434
}
2435
2436
/**
2437
 * Represents a single version of a record.
2438
 *
2439
 * @package framework
2440
 * @subpackage model
2441
 *
2442
 * @see Versioned
2443
 */
2444
class Versioned_Version extends ViewableData {
2445
	/**
2446
	 * @var array
2447
	 */
2448
	protected $record;
2449
2450
	/**
2451
	 * @var DataObject
2452
	 */
2453
	protected $object;
2454
2455
	/**
2456
	 * Create a new version from a database row
2457
	 *
2458
	 * @param array $record
2459
	 */
2460
	public function __construct($record) {
2461
		$this->record = $record;
2462
		$record['ID'] = $record['RecordID'];
2463
		$className = $record['ClassName'];
2464
2465
		$this->object = ClassInfo::exists($className) ? new $className($record) : new DataObject($record);
2466
		$this->failover = $this->object;
2467
2468
		parent::__construct();
2469
	}
2470
2471
	/**
2472
	 * Either 'published' if published, or 'internal' if not.
2473
	 *
2474
	 * @return string
2475
	 */
2476
	public function PublishedClass() {
2477
		return $this->record['WasPublished'] ? 'published' : 'internal';
2478
	}
2479
2480
	/**
2481
	 * Author of this DataObject
2482
	 *
2483
	 * @return Member
2484
	 */
2485
	public function Author() {
2486
		return Member::get()->byId($this->record['AuthorID']);
2487
	}
2488
2489
	/**
2490
	 * Member object of the person who last published this record
2491
	 *
2492
	 * @return Member
2493
	 */
2494
	public function Publisher() {
2495
		if (!$this->record['WasPublished']) {
2496
			return null;
2497
		}
2498
2499
		return Member::get()->byId($this->record['PublisherID']);
2500
	}
2501
2502
	/**
2503
	 * True if this record is published via publish() method
2504
	 *
2505
	 * @return boolean
2506
	 */
2507
	public function Published() {
2508
		return !empty($this->record['WasPublished']);
2509
	}
2510
2511
	/**
2512
	 * Traverses to a field referenced by relationships between data objects, returning the value
2513
	 * The path to the related field is specified with dot separated syntax (eg: Parent.Child.Child.FieldName)
2514
	 *
2515
	 * @param $fieldName string
2516
	 * @return string | null - will return null on a missing value
2517
	 */
2518
	public function relField($fieldName) {
2519
		$component = $this;
2520
2521
		// We're dealing with relations here so we traverse the dot syntax
2522
		if(strpos($fieldName, '.') !== false) {
2523
			$relations = explode('.', $fieldName);
2524
			$fieldName = array_pop($relations);
2525
			foreach($relations as $relation) {
2526
				// Inspect $component for element $relation
2527
				if($component->hasMethod($relation)) {
2528
					// Check nested method
2529
						$component = $component->$relation();
2530
				} elseif($component instanceof SS_List) {
2531
					// Select adjacent relation from DataList
2532
						$component = $component->relation($relation);
2533
				} elseif($component instanceof DataObject
2534
					&& ($dbObject = $component->dbObject($relation))
2535
				) {
2536
					// Select db object
2537
					$component = $dbObject;
2538
				} else {
2539
					user_error("$relation is not a relation/field on ".get_class($component), E_USER_ERROR);
2540
				}
2541
			}
2542
		}
2543
2544
		// Bail if the component is null
2545
		if(!$component) {
2546
			return null;
2547
		}
2548
		if ($component->hasMethod($fieldName)) {
2549
			return $component->$fieldName();
2550
		}
2551
		return $component->$fieldName;
2552
	}
2553
}
2554