Completed
Pull Request — master (#5247)
by Damian
11:17
created

ChangeSetItem::canCreate()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 3
rs 10
cc 1
eloc 2
nc 1
nop 2
1
<?php
2
3
// namespace SilverStripe\Framework\Model\Versioning
4
5
/**
6
 * A single line in a changeset
7
 *
8
 * @property string $ReferencedBy
9
 * @property string $Added
10
 * @property string $ObjectClass
11
 * @property int $ObjectID
12
 * @method ManyManyList ReferencedBy() List of explicit items that require this change
13
 * @method ManyManyList References() List of implicit items required by this change
14
 * @method ChangeSet ChangeSet()
15
 */
16
class ChangeSetItem extends DataObject {
17
18
	const EXPLICITLY = 'explicitly';
19
20
	const IMPLICITLY = 'implicitly';
21
22
	/** Represents an object deleted */
23
	const CHANGE_DELETED = 'deleted';
24
25
	/** Represents an object which was modified */
26
	const CHANGE_MODIFIED = 'modified';
27
28
	/** Represents an object added */
29
	const CHANGE_CREATED = 'created';
30
31
	/** Represents an object which hasn't been changed directly, but owns a modified many_many relationship. */
32
	//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...
33
34
	/**
35
	 * Represents that an object has not yet been changed, but
36
	 * should be included in this changeset as soon as any changes exist
37
	 */
38
	const CHANGE_NONE = 'none';
39
40
	private static $db = array(
41
		'VersionBefore' => 'Int',
42
		'VersionAfter'  => 'Int',
43
		'Added'         => "Enum('explicitly, implicitly', 'implicitly')"
44
	);
45
46
	private static $has_one = array(
47
		'ChangeSet' => 'ChangeSet',
48
		'Object'      => 'DataObject',
49
	);
50
51
	private static $many_many = array(
52
		'ReferencedBy' => 'ChangeSetItem'
53
	);
54
55
	private static $belongs_many_many = array(
56
		'References' => 'ChangeSetItem.ReferencedBy'
57
	);
58
59
	private static $indexes = array(
60
		'ObjectUniquePerChangeSet' => array(
61
			'type' => 'unique',
62
			'value' => '"ObjectID", "ObjectClass", "ChangeSetID"'
63
		)
64
	);
65
66
	/**
67
	 * Get the type of change: none, created, deleted, modified, manymany
68
	 *
69
	 * @return string
70
	 */
71
	public function getChangeType() {
72
		// Get change versions
73
		if($this->VersionBefore || $this->VersionAfter) {
74
			$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...
75
			$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...
76
		} else {
77
			$draftVersion = Versioned::get_versionnumber_by_stage(
78
				$this->ObjectClass, Versioned::DRAFT, $this->ObjectID, false
79
			);
80
			$liveVersion = Versioned::get_versionnumber_by_stage(
81
				$this->ObjectClass, Versioned::LIVE, $this->ObjectID, false
82
			);
83
		}
84
85
		// Version comparisons
86
		if ($draftVersion == $liveVersion) {
87
			return self::CHANGE_NONE;
88
		} elseif (!$liveVersion) {
89
			return self::CHANGE_CREATED;
90
		} elseif (!$draftVersion) {
91
			return self::CHANGE_DELETED;
92
		} else {
93
			return self::CHANGE_MODIFIED;
94
		}
95
	}
96
97
	/**
98
	 * Find version of this object in the given stage
99
	 *
100
	 * @param string $stage
101
	 * @return Versioned|DataObject
102
	 */
103
	private function getObjectInStage($stage) {
104
		return Versioned::get_by_stage($this->ObjectClass, $stage)->byID($this->ObjectID);
105
	}
106
107
	/**
108
	 * Get all implicit objects for this change
109
	 *
110
	 * @return SS_List
111
	 */
112
	public function findReferenced() {
113
		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...
114
			// If deleted from stage, need to look at live record
115
			return $this->getObjectInStage(Versioned::LIVE)->findOwners(false);
116
		} else {
117
			// If changed on stage, look at owned objects there
118
			return $this->getObjectInStage(Versioned::DRAFT)->findOwned()->filterByCallback(function ($owned) {
119
				/** @var Versioned|DataObject $owned */
120
				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...
121
			});
122
		}
123
	}
124
125
	/**
126
	 * Publish this item, then close it.
127
	 *
128
	 * Note: Unlike Versioned::doPublish() and Versioned::doUnpublish, this action is not recursive.
129
	 */
130
	public function publish() {
131
		// Logical checks prior to publish
132
		if(!$this->canPublish()) {
133
			throw new Exception("The current member does not have permission to publish this ChangeSetItem.");
134
		}
135
		if($this->VersionBefore || $this->VersionAfter) {
136
			throw new BadMethodCallException("This ChangeSetItem has already been published");
137
		}
138
139
		// Record state changed
140
		$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...
141
			$this->ObjectClass, Versioned::DRAFT, $this->ObjectID, false
142
		);
143
		$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...
144
			$this->ObjectClass, Versioned::LIVE, $this->ObjectID, false
145
		);
146
147
		switch($this->getChangeType()) {
148
			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...
149
				break;
150
			}
151
			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...
152
				// Non-recursive delete
153
				$object = $this->getObjectInStage(Versioned::LIVE);
154
				$object->deleteFromStage(Versioned::LIVE);
155
				break;
156
			}
157
			case static::CHANGE_MODIFIED:
158
			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...
159
				// Non-recursive publish
160
				$object = $this->getObjectInStage(Versioned::DRAFT);
161
				$object->publishSingle();
162
				break;
163
			}
164
		}
165
166
		$this->write();
167
	}
168
169
	/** Reverts this item, then close it. **/
170
	public function revert() {
171
		user_error('Not implemented', E_USER_ERROR);
172
	}
173
174
	public function canView($member = null) {
175
		return $this->can(__FUNCTION__, $member);
176
	}
177
178
	public function canEdit($member = null) {
179
		return $this->can(__FUNCTION__, $member);
180
	}
181
182
	public function canCreate($member = null, $context = array()) {
183
		return $this->can(__FUNCTION__, $member, $context);
184
	}
185
186
	public function canDelete($member = null) {
187
		return $this->can(__FUNCTION__, $member);
188
	}
189
190
	/**
191
	 * Check if the BeforeVersion of this changeset can be restored to draft
192
	 *
193
	 * @param Member $member
194
	 * @return bool
195
	 */
196
	public function canRevert($member) {
197
		// Just get the best version as this object may not even exist on either stage anymore.
198
		/** @var Versioned|DataObject $object */
199
		$object = Versioned::get_latest_version($this->ObjectClass, $this->ObjectID);
200
		if(!$object) {
201
			return false;
202
		}
203
204
		// Check change type
205
		switch($this->getChangeType()) {
206
			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...
207
				// Revert creation by deleting from stage
208
				if(!$object->canDelete($member)) {
209
					return false;
210
				}
211
				break;
212
			}
213
			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...
214
				// All other actions are typically editing draft stage
215
				if(!$object->canEdit($member)) {
216
					return false;
217
				}
218
				break;
219
			}
220
		}
221
222
		// If object can be published/unpublished let extensions deny
223
		return $this->can(__FUNCTION__, $member);
224
	}
225
226
	/**
227
	 * Check if this ChangeSetItem can be published
228
	 *
229
	 * @param Member $member
230
	 * @return bool
231
	 */
232
	public function canPublish($member = null) {
233
		// Check canMethod to invoke on object
234
		switch($this->getChangeType()) {
235
			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...
236
				/** @var Versioned|DataObject $object */
237
				$object = Versioned::get_by_stage($this->ObjectClass, Versioned::LIVE)->byID($this->ObjectID);
238
				if(!$object || !$object->canUnpublish($member)) {
0 ignored issues
show
Bug introduced by
It seems like $member defined by parameter $member on line 232 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...
239
					return false;
240
				}
241
				break;
242
			}
243
			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...
244
				/** @var Versioned|DataObject $object */
245
				$object = Versioned::get_by_stage($this->ObjectClass, Versioned::DRAFT)->byID($this->ObjectID);
246
				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...
247
					return false;
248
				}
249
				break;
250
			}
251
		}
252
253
		// If object can be published/unpublished let extensions deny
254
		return $this->can(__FUNCTION__, $member);
255
	}
256
257
	/**
258
	 * Default permissions for this ChangeSetItem
259
	 *
260
	 * @param string $perm
261
	 * @param Member $member
262
	 * @param array $context
263
	 * @return bool
264
	 */
265
	public function can($perm, $member = null, $context = array()) {
266
		if(!$member) {
267
			$member = Member::currentUser();
268
		}
269
270
		// Allow extensions to bypass default permissions, but only if
271
		// each change can be individually published.
272
		$extended = $this->extendedCan($perm, $member, $context);
273
		if($extended !== null) {
274
			return $extended;
275
		}
276
277
		// Default permissions
278
		return (bool)Permission::checkMember($member, ChangeSet::config()->required_permission);
279
	}
280
281
}
282