Completed
Pull Request — master (#5304)
by Damian
10:55
created

ChangeSetItem::ThumbnailURL()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 7
rs 9.4285
cc 2
eloc 5
nc 2
nop 2
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
13
 * @property int $ObjectID
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 getTitle() {
69
		// Get title of modified object
70
		$object = $this->getObjectLatestVersion();
71
		if($object) {
72
			return $object->getTitle();
73
		}
74
		return $this->i18n_singular_name() . ' #' . $this->ID;
75
	}
76
77
78
79
	/**
80
	 * Get a thumbnail for this object
81
	 *
82
	 * @param int $width Preferred width of the thumbnail
83
	 * @param int $height Preferred height of the thumbnail
84
	 * @return string URL to the thumbnail, if available
85
	 */
86
	public function ThumbnailURL($width, $height) {
87
		$object = $this->getObjectLatestVersion();
88
		if($object instanceof Thumbnail) {
89
			return $object->ThumbnailURL($width, $height);
90
		}
91
		return null;
92
	}
93
94
95
	/**
96
	 * Get the type of change: none, created, deleted, modified, manymany
97
	 *
98
	 * @return string
99
	 */
100
	public function getChangeType() {
101
		// Get change versions
102
		if($this->VersionBefore || $this->VersionAfter) {
103
			$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...
104
			$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...
105
		} else {
106
			$draftVersion = Versioned::get_versionnumber_by_stage(
107
				$this->ObjectClass, Versioned::DRAFT, $this->ObjectID, false
108
			);
109
			$liveVersion = Versioned::get_versionnumber_by_stage(
110
				$this->ObjectClass, Versioned::LIVE, $this->ObjectID, false
111
			);
112
		}
113
114
		// Version comparisons
115
		if ($draftVersion == $liveVersion) {
116
			return self::CHANGE_NONE;
117
		} elseif (!$liveVersion) {
118
			return self::CHANGE_CREATED;
119
		} elseif (!$draftVersion) {
120
			return self::CHANGE_DELETED;
121
		} else {
122
			return self::CHANGE_MODIFIED;
123
		}
124
	}
125
126
	/**
127
	 * Find version of this object in the given stage
128
	 *
129
	 * @param string $stage
130
	 * @return Versioned|DataObject
131
	 */
132
	protected function getObjectInStage($stage) {
133
		return Versioned::get_by_stage($this->ObjectClass, $stage)->byID($this->ObjectID);
134
	}
135
136
	/**
137
	 * Find latest version of this object
138
	 *
139
	 * @return Versioned|DataObject
140
	 */
141
	protected function getObjectLatestVersion() {
142
		return Versioned::get_latest_version($this->ObjectClass, $this->ObjectID);
143
	}
144
145
	/**
146
	 * Get all implicit objects for this change
147
	 *
148
	 * @return SS_List
149
	 */
150
	public function findReferenced() {
151
		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...
152
			// If deleted from stage, need to look at live record
153
			return $this->getObjectInStage(Versioned::LIVE)->findOwners(false);
154
		} else {
155
			// If changed on stage, look at owned objects there
156
			return $this->getObjectInStage(Versioned::DRAFT)->findOwned()->filterByCallback(function ($owned) {
157
				/** @var Versioned|DataObject $owned */
158
				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...
159
			});
160
		}
161
	}
162
163
	/**
164
	 * Publish this item, then close it.
165
	 *
166
	 * Note: Unlike Versioned::doPublish() and Versioned::doUnpublish, this action is not recursive.
167
	 */
168
	public function publish() {
169
		// Logical checks prior to publish
170
		if(!$this->canPublish()) {
171
			throw new Exception("The current member does not have permission to publish this ChangeSetItem.");
172
		}
173
		if($this->VersionBefore || $this->VersionAfter) {
174
			throw new BadMethodCallException("This ChangeSetItem has already been published");
175
		}
176
177
		// Record state changed
178
		$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...
179
			$this->ObjectClass, Versioned::DRAFT, $this->ObjectID, false
180
		);
181
		$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...
182
			$this->ObjectClass, Versioned::LIVE, $this->ObjectID, false
183
		);
184
185
		switch($this->getChangeType()) {
186
			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...
187
				break;
188
			}
189
			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...
190
				// Non-recursive delete
191
				$object = $this->getObjectInStage(Versioned::LIVE);
192
				$object->deleteFromStage(Versioned::LIVE);
193
				break;
194
			}
195
			case static::CHANGE_MODIFIED:
196
			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...
197
				// Non-recursive publish
198
				$object = $this->getObjectInStage(Versioned::DRAFT);
199
				$object->publishSingle();
200
				break;
201
			}
202
		}
203
204
		$this->write();
205
	}
206
207
	/** Reverts this item, then close it. **/
208
	public function revert() {
209
		user_error('Not implemented', E_USER_ERROR);
210
	}
211
212
	public function canView($member = null) {
213
		return $this->can(__FUNCTION__, $member);
214
	}
215
216
	public function canEdit($member = null) {
217
		return $this->can(__FUNCTION__, $member);
218
	}
219
220
	public function canCreate($member = null, $context = array()) {
221
		return $this->can(__FUNCTION__, $member, $context);
222
	}
223
224
	public function canDelete($member = null) {
225
		return $this->can(__FUNCTION__, $member);
226
	}
227
228
	/**
229
	 * Check if the BeforeVersion of this changeset can be restored to draft
230
	 *
231
	 * @param Member $member
232
	 * @return bool
233
	 */
234
	public function canRevert($member) {
235
		// Just get the best version as this object may not even exist on either stage anymore.
236
		/** @var Versioned|DataObject $object */
237
		$object = $this->getObjectLatestVersion();
238
		if(!$object) {
239
			return false;
240
		}
241
242
		// Check change type
243
		switch($this->getChangeType()) {
244
			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...
245
				// Revert creation by deleting from stage
246
				if(!$object->canDelete($member)) {
247
					return false;
248
				}
249
				break;
250
			}
251
			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...
252
				// All other actions are typically editing draft stage
253
				if(!$object->canEdit($member)) {
254
					return false;
255
				}
256
				break;
257
			}
258
		}
259
260
		// If object can be published/unpublished let extensions deny
261
		return $this->can(__FUNCTION__, $member);
262
	}
263
264
	/**
265
	 * Check if this ChangeSetItem can be published
266
	 *
267
	 * @param Member $member
268
	 * @return bool
269
	 */
270
	public function canPublish($member = null) {
271
		// Check canMethod to invoke on object
272
		switch($this->getChangeType()) {
273
			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...
274
				/** @var Versioned|DataObject $object */
275
				$object = Versioned::get_by_stage($this->ObjectClass, Versioned::LIVE)->byID($this->ObjectID);
276
				if(!$object || !$object->canUnpublish($member)) {
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 270 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...
277
					return false;
278
				}
279
				break;
280
			}
281
			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...
282
				/** @var Versioned|DataObject $object */
283
				$object = Versioned::get_by_stage($this->ObjectClass, Versioned::DRAFT)->byID($this->ObjectID);
284
				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...
285
					return false;
286
				}
287
				break;
288
			}
289
		}
290
291
		// If object can be published/unpublished let extensions deny
292
		return $this->can(__FUNCTION__, $member);
293
	}
294
295
	/**
296
	 * Default permissions for this ChangeSetItem
297
	 *
298
	 * @param string $perm
299
	 * @param Member $member
300
	 * @param array $context
301
	 * @return bool
302
	 */
303
	public function can($perm, $member = null, $context = array()) {
304
		if(!$member) {
305
			$member = Member::currentUser();
306
		}
307
308
		// Allow extensions to bypass default permissions, but only if
309
		// each change can be individually published.
310
		$extended = $this->extendedCan($perm, $member, $context);
311
		if($extended !== null) {
312
			return $extended;
313
		}
314
315
		// Default permissions
316
		return (bool)Permission::checkMember($member, ChangeSet::config()->required_permission);
317
	}
318
}
319