Completed
Pull Request — master (#5408)
by Damian
23:40 queued 12:41
created

ChangeSetItem::getPreviewLinks()   C

Complexity

Conditions 7
Paths 4

Size

Total Lines 23
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 23
rs 6.7272
cc 7
eloc 13
nc 4
nop 0
1
<?php
2
3
// namespace SilverStripe\Framework\Model\Versioning
4
5
use SilverStripe\Filesystem\Thumbnail;
6
7
8
/**
9
 * A single line in a changeset
10
 *
11
 * @property string $Added
12
 * @property string $ObjectClass The _base_ data class for the referenced DataObject
13
 * @property int $ObjectID The numeric ID for the referenced object
14
 * @method ManyManyList ReferencedBy() List of explicit items that require this change
15
 * @method ManyManyList References() List of implicit items required by this change
16
 * @method ChangeSet ChangeSet()
17
 */
18
class ChangeSetItem extends DataObject implements Thumbnail {
19
20
	const EXPLICITLY = 'explicitly';
21
22
	const IMPLICITLY = 'implicitly';
23
24
	/** Represents an object deleted */
25
	const CHANGE_DELETED = 'deleted';
26
27
	/** Represents an object which was modified */
28
	const CHANGE_MODIFIED = 'modified';
29
30
	/** Represents an object added */
31
	const CHANGE_CREATED = 'created';
32
33
	/** Represents an object which hasn't been changed directly, but owns a modified many_many relationship. */
34
	//const CHANGE_MANYMANY = 'manymany';
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
35
36
	/**
37
	 * Represents that an object has not yet been changed, but
38
	 * should be included in this changeset as soon as any changes exist
39
	 */
40
	const CHANGE_NONE = 'none';
41
42
	private static $db = array(
43
		'VersionBefore' => 'Int',
44
		'VersionAfter'  => 'Int',
45
		'Added'         => "Enum('explicitly, implicitly', 'implicitly')"
46
	);
47
48
	private static $has_one = array(
49
		'ChangeSet' => 'ChangeSet',
50
		'Object'      => 'DataObject',
51
	);
52
53
	private static $many_many = array(
54
		'ReferencedBy' => 'ChangeSetItem'
55
	);
56
57
	private static $belongs_many_many = array(
58
		'References' => 'ChangeSetItem.ReferencedBy'
59
	);
60
61
	private static $indexes = array(
62
		'ObjectUniquePerChangeSet' => array(
63
			'type' => 'unique',
64
			'value' => '"ObjectID", "ObjectClass", "ChangeSetID"'
65
		)
66
	);
67
68
	public function onBeforeWrite() {
69
		// Make sure ObjectClass refers to the base data class in the case of old or wrong code
70
		$this->ObjectClass = ClassInfo::baseDataClass($this->ObjectClass);
71
		parent::onBeforeWrite();
72
	}
73
74
	public function getTitle() {
75
		// Get title of modified object
76
		$object = $this->getObjectLatestVersion();
77
		if($object) {
78
			return $object->getTitle();
79
		}
80
		return $this->i18n_singular_name() . ' #' . $this->ID;
81
	}
82
83
	/**
84
	 * Get a thumbnail for this object
85
	 *
86
	 * @param int $width Preferred width of the thumbnail
87
	 * @param int $height Preferred height of the thumbnail
88
	 * @return string URL to the thumbnail, if available
89
	 */
90
	public function ThumbnailURL($width, $height) {
91
		$object = $this->getObjectLatestVersion();
92
		if($object instanceof Thumbnail) {
93
			return $object->ThumbnailURL($width, $height);
94
		}
95
		return null;
96
	}
97
98
	/**
99
	 * Get the type of change: none, created, deleted, modified, manymany
100
	 *
101
	 * @return string
102
	 */
103
	public function getChangeType() {
104
		// Get change versions
105
		if($this->VersionBefore || $this->VersionAfter) {
106
			$draftVersion = $this->VersionAfter; // After publishing draft was written to stage
0 ignored issues
show
Documentation introduced by
The property VersionAfter does not exist on object<ChangeSetItem>. 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...
107
			$liveVersion = $this->VersionBefore; // The live version before the publish
0 ignored issues
show
Documentation introduced by
The property VersionBefore does not exist on object<ChangeSetItem>. 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...
108
		} else {
109
			$draftVersion = Versioned::get_versionnumber_by_stage(
110
				$this->ObjectClass, Versioned::DRAFT, $this->ObjectID, false
111
			);
112
			$liveVersion = Versioned::get_versionnumber_by_stage(
113
				$this->ObjectClass, Versioned::LIVE, $this->ObjectID, false
114
			);
115
		}
116
117
		// Version comparisons
118
		if ($draftVersion == $liveVersion) {
119
			return self::CHANGE_NONE;
120
		} elseif (!$liveVersion) {
121
			return self::CHANGE_CREATED;
122
		} elseif (!$draftVersion) {
123
			return self::CHANGE_DELETED;
124
		} else {
125
			return self::CHANGE_MODIFIED;
126
		}
127
	}
128
129
	/**
130
	 * Find version of this object in the given stage
131
	 *
132
	 * @param string $stage
133
	 * @return Versioned|DataObject
134
	 */
135
	protected function getObjectInStage($stage) {
136
		return Versioned::get_by_stage($this->ObjectClass, $stage)->byID($this->ObjectID);
137
	}
138
139
	/**
140
	 * Find latest version of this object
141
	 *
142
	 * @return Versioned|DataObject
143
	 */
144
	protected function getObjectLatestVersion() {
145
		return Versioned::get_latest_version($this->ObjectClass, $this->ObjectID);
146
	}
147
148
	/**
149
	 * Get all implicit objects for this change
150
	 *
151
	 * @return SS_List
152
	 */
153
	public function findReferenced() {
154
		if($this->getChangeType() === ChangeSetItem::CHANGE_DELETED) {
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...
155
			// If deleted from stage, need to look at live record
156
			return $this->getObjectInStage(Versioned::LIVE)->findOwners(false);
157
		} else {
158
			// If changed on stage, look at owned objects there
159
			return $this->getObjectInStage(Versioned::DRAFT)->findOwned()->filterByCallback(function ($owned) {
160
				/** @var Versioned|DataObject $owned */
161
				return $owned->stagesDiffer(Versioned::DRAFT, Versioned::LIVE);
0 ignored issues
show
Bug introduced by
The method stagesDiffer 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...
162
			});
163
		}
164
	}
165
166
	/**
167
	 * Publish this item, then close it.
168
	 *
169
	 * Note: Unlike Versioned::doPublish() and Versioned::doUnpublish, this action is not recursive.
170
	 */
171
	public function publish() {
172
		// Logical checks prior to publish
173
		if(!$this->canPublish()) {
174
			throw new Exception("The current member does not have permission to publish this ChangeSetItem.");
175
		}
176
		if($this->VersionBefore || $this->VersionAfter) {
177
			throw new BadMethodCallException("This ChangeSetItem has already been published");
178
		}
179
180
		// Record state changed
181
		$this->VersionAfter = Versioned::get_versionnumber_by_stage(
0 ignored issues
show
Documentation introduced by
The property VersionAfter does not exist on object<ChangeSetItem>. 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...
182
			$this->ObjectClass, Versioned::DRAFT, $this->ObjectID, false
183
		);
184
		$this->VersionBefore = Versioned::get_versionnumber_by_stage(
0 ignored issues
show
Documentation introduced by
The property VersionBefore does not exist on object<ChangeSetItem>. 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...
185
			$this->ObjectClass, Versioned::LIVE, $this->ObjectID, false
186
		);
187
188
		switch($this->getChangeType()) {
189
			case static::CHANGE_NONE: {
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...
190
				break;
191
			}
192
			case static::CHANGE_DELETED: {
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...
193
				// Non-recursive delete
194
				$object = $this->getObjectInStage(Versioned::LIVE);
195
				$object->deleteFromStage(Versioned::LIVE);
196
				break;
197
			}
198
			case static::CHANGE_MODIFIED:
199
			case static::CHANGE_CREATED: {
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...
200
				// Non-recursive publish
201
				$object = $this->getObjectInStage(Versioned::DRAFT);
202
				$object->publishSingle();
203
				break;
204
			}
205
		}
206
207
		$this->write();
208
	}
209
210
	/** Reverts this item, then close it. **/
211
	public function revert() {
212
		user_error('Not implemented', E_USER_ERROR);
213
	}
214
215
	public function canView($member = null) {
216
		return $this->can(__FUNCTION__, $member);
217
	}
218
219
	public function canEdit($member = null) {
220
		return $this->can(__FUNCTION__, $member);
221
	}
222
223
	public function canCreate($member = null, $context = array()) {
224
		return $this->can(__FUNCTION__, $member, $context);
225
	}
226
227
	public function canDelete($member = null) {
228
		return $this->can(__FUNCTION__, $member);
229
	}
230
231
	/**
232
	 * Check if the BeforeVersion of this changeset can be restored to draft
233
	 *
234
	 * @param Member $member
235
	 * @return bool
236
	 */
237
	public function canRevert($member) {
238
		// Just get the best version as this object may not even exist on either stage anymore.
239
		/** @var Versioned|DataObject $object */
240
		$object = $this->getObjectLatestVersion();
241
		if(!$object) {
242
			return false;
243
		}
244
245
		// Check change type
246
		switch($this->getChangeType()) {
247
			case static::CHANGE_CREATED: {
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...
248
				// Revert creation by deleting from stage
249
				if(!$object->canDelete($member)) {
250
					return false;
251
				}
252
				break;
253
			}
254
			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...
255
				// All other actions are typically editing draft stage
256
				if(!$object->canEdit($member)) {
257
					return false;
258
				}
259
				break;
260
			}
261
		}
262
263
		// If object can be published/unpublished let extensions deny
264
		return $this->can(__FUNCTION__, $member);
265
	}
266
267
	/**
268
	 * Check if this ChangeSetItem can be published
269
	 *
270
	 * @param Member $member
271
	 * @return bool
272
	 */
273
	public function canPublish($member = null) {
274
		// Check canMethod to invoke on object
275
		switch($this->getChangeType()) {
276
			case static::CHANGE_DELETED: {
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...
277
				/** @var Versioned|DataObject $object */
278
				$object = Versioned::get_by_stage($this->ObjectClass, Versioned::LIVE)->byID($this->ObjectID);
279
				if(!$object || !$object->canUnpublish($member)) {
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 273 can also be of type object<Member>; however, Versioned::canUnpublish() does only seem to accept null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
Bug introduced by
The method canUnpublish 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...
280
					return false;
281
				}
282
				break;
283
			}
284
			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...
285
				/** @var Versioned|DataObject $object */
286
				$object = Versioned::get_by_stage($this->ObjectClass, Versioned::DRAFT)->byID($this->ObjectID);
287
				if(!$object || !$object->canPublish($member)) {
0 ignored issues
show
Bug introduced by
The method canPublish 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...
288
					return false;
289
				}
290
				break;
291
			}
292
		}
293
294
		// If object can be published/unpublished let extensions deny
295
		return $this->can(__FUNCTION__, $member);
296
	}
297
298
	/**
299
	 * Default permissions for this ChangeSetItem
300
	 *
301
	 * @param string $perm
302
	 * @param Member $member
303
	 * @param array $context
304
	 * @return bool
305
	 */
306
	public function can($perm, $member = null, $context = array()) {
307
		if(!$member) {
308
			$member = Member::currentUser();
309
		}
310
311
		// Allow extensions to bypass default permissions, but only if
312
		// each change can be individually published.
313
		$extended = $this->extendedCan($perm, $member, $context);
314
		if($extended !== null) {
315
			return $extended;
316
		}
317
318
		// Default permissions
319
		return (bool)Permission::checkMember($member, ChangeSet::config()->required_permission);
320
	}
321
322
	/**
323
	 * Get the ChangeSetItems that reference a passed DataObject
324
	 *
325
	 * @param DataObject $object
326
	 * @return DataList
327
	 */
328
	public static function get_for_object($object) {
329
		return ChangeSetItem::get()->filter([
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...
330
			'ObjectID' => $object->ID,
331
			'ObjectClass' => ClassInfo::baseDataClass($object)
332
		]);
333
	}
334
335
	/**
336
	 * Get the ChangeSetItems that reference a passed DataObject
337
	 *
338
	 * @param int $objectID The ID of the object
339
	 * @param string $objectClass The class of the object (or any parent class)
340
	 * @return DataList
341
	 */
342
	public static function get_for_object_by_id($objectID, $objectClass) {
343
		return ChangeSetItem::get()->filter([
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...
344
			'ObjectID' => $objectID,
345
			'ObjectClass' => ClassInfo::baseDataClass($objectClass)
346
		]);
347
	}
348
349
	/**
350
	 * Gets the list of modes this record can be previewed in.
351
	 *
352
	 * {@link https://tools.ietf.org/html/draft-kelly-json-hal-07#section-5}
353
	 *
354
	 * @return array Map of links in acceptable HAL format
355
	 */
356
	public function getPreviewLinks() {
357
		$links = [];
358
359
		// Preview draft
360
		$stage = $this->getObjectInStage(Versioned::DRAFT);
361
		if($stage instanceof CMSPreviewable && $stage->canView() && ($link = $stage->PreviewLink())) {
362
			$links[Versioned::DRAFT] = [
363
				'href' => Controller::join_links($link, '?stage=' . Versioned::DRAFT),
364
				'type' => $stage->getMimeType(),
365
			];
366
		}
367
368
		// Preview live
369
		$live = $this->getObjectInStage(Versioned::LIVE);
370
		if($live instanceof CMSPreviewable && $live->canView() && ($link = $live->PreviewLink())) {
371
			$links[Versioned::LIVE] = [
372
				'href' => Controller::join_links($link, '?stage=' . Versioned::LIVE),
373
				'type' => $live->getMimeType(),
374
			];
375
		}
376
377
		return $links;
378
	}
379
380
	/**
381
	 * Get edit link for this item
382
	 *
383
	 * @return string
384
	 */
385
	public function CMSEditLink()
386
	{
387
		$link = $this->getObjectInStage(Versioned::DRAFT);
388
		if($link instanceof CMSPreviewable) {
389
			return $link->CMSEditLink();
390
		}
391
		return null;
392
	}
393
}
394