Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
22.97% |
17 / 74 |
|
25.00% |
3 / 12 |
CRAP | |
0.00% |
0 / 1 |
RevisionActionPermissions | |
22.97% |
17 / 74 |
|
25.00% |
3 / 12 |
628.29 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getAllowedActions | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
isAllowed | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
90 | |||
isAllowedAny | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
isRootAllowed | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
isBoardAllowed | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
isRevisionAllowed | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
4.03 | |||
getPermission | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
getRoot | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
30 | |||
getUser | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getActions | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setUser | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace Flow; |
4 | |
5 | use Closure; |
6 | use Flow\Collection\CollectionCache; |
7 | use Flow\Collection\PostCollection; |
8 | use Flow\Exception\DataModelException; |
9 | use Flow\Model\AbstractRevision; |
10 | use Flow\Model\PostRevision; |
11 | use Flow\Model\PostSummary; |
12 | use Flow\Model\Workflow; |
13 | use MediaWiki\MediaWikiServices; |
14 | use MediaWiki\User\User; |
15 | |
16 | /** |
17 | * Role based security for revisions based on moderation state |
18 | */ |
19 | class RevisionActionPermissions { |
20 | /** |
21 | * @var FlowActions |
22 | */ |
23 | protected $actions; |
24 | |
25 | /** |
26 | * @var User |
27 | */ |
28 | protected $user; |
29 | |
30 | /** |
31 | * @param FlowActions $actions |
32 | * @param User $user |
33 | */ |
34 | public function __construct( FlowActions $actions, User $user ) { |
35 | $this->user = $user; |
36 | $this->actions = $actions; |
37 | } |
38 | |
39 | /** |
40 | * Get the name of all the actions the user is allowed to perform. |
41 | * |
42 | * @param AbstractRevision|null $revision The revision to check permissions against |
43 | * @return array Array of action names that are allowed |
44 | */ |
45 | public function getAllowedActions( ?AbstractRevision $revision = null ) { |
46 | $allowed = []; |
47 | foreach ( array_keys( $this->actions->getActions() ) as $action ) { |
48 | if ( $this->isAllowedAny( $revision, $action ) ) { |
49 | $allowed[] = $action; |
50 | } |
51 | } |
52 | return $allowed; |
53 | } |
54 | |
55 | /** |
56 | * Check if a user is allowed to perform a certain action. |
57 | * |
58 | * @param AbstractRevision $revision |
59 | * @param string $action |
60 | * @return bool |
61 | */ |
62 | public function isAllowed( AbstractRevision $revision, $action ) { |
63 | // check if we're allowed to $action on this revision |
64 | if ( !$this->isRevisionAllowed( $revision, $action ) ) { |
65 | return false; |
66 | } |
67 | |
68 | /** @var AbstractRevision[] $roots */ |
69 | static $roots = []; |
70 | /** @var Workflow[] $workflows */ |
71 | static $workflows = []; |
72 | |
73 | $revisionId = $revision->getRevisionId()->getAlphadecimal(); |
74 | |
75 | if ( !isset( $roots[$revisionId] ) ) { |
76 | $roots[$revisionId] = $this->getRoot( $revision ); |
77 | } |
78 | // see if we're allowed to perform $action on anything inside this root |
79 | if ( !$revision->getRevisionId()->equals( $roots[$revisionId]->getRevisionId() ) && |
80 | !$this->isRootAllowed( $roots[$revisionId], $action ) |
81 | ) { |
82 | return false; |
83 | } |
84 | |
85 | if ( !isset( $workflows[$revisionId] ) ) { |
86 | $collection = $revision->getCollection(); |
87 | $workflows[$revisionId] = $collection->getBoardWorkflow(); |
88 | } |
89 | // see if we're allowed to perform $action on anything inside this board |
90 | if ( !$this->isBoardAllowed( $workflows[$revisionId], $action ) ) { |
91 | return false; |
92 | } |
93 | |
94 | /** @var CollectionCache $cache */ |
95 | $cache = Container::get( 'collection.cache' ); |
96 | $last = $cache->getLastRevisionFor( $revision ); |
97 | // Also check if the user would be allowed to perform this |
98 | // against the most recent revision - the last revision is the |
99 | // current state of an object, so checking against a revision at |
100 | // one point in time alone isn't enough. |
101 | $isLastRevision = $last->getRevisionId()->equals( $revision->getRevisionId() ); |
102 | if ( !$isLastRevision && !$this->isRevisionAllowed( $last, $action ) ) { |
103 | return false; |
104 | } |
105 | |
106 | return true; |
107 | } |
108 | |
109 | /** |
110 | * Check if a user is allowed to perform certain actions. |
111 | * |
112 | * @param AbstractRevision|null $revision |
113 | * @param string $action Multiple parameters to check if either of the provided actions are allowed |
114 | * @return bool |
115 | */ |
116 | public function isAllowedAny( ?AbstractRevision $revision, $action /* [, $action2 [, ... ]] */ ) { |
117 | $actions = func_get_args(); |
118 | // Pull $revision out of the actions list |
119 | array_shift( $actions ); |
120 | |
121 | foreach ( $actions as $action ) { |
122 | if ( $this->isAllowed( $revision, $action ) ) { |
123 | return true; |
124 | } |
125 | } |
126 | |
127 | return false; |
128 | } |
129 | |
130 | /** |
131 | * Check if a user is allowed to perform a certain action, against the latest |
132 | * root(topic) post related to the provided revision. This is required for |
133 | * things like preventing replies to locked topics. |
134 | * |
135 | * @param AbstractRevision $root |
136 | * @param string $action |
137 | * @return bool |
138 | */ |
139 | public function isRootAllowed( AbstractRevision $root, $action ) { |
140 | // If the `root-permissions` key is not set then it is allowed |
141 | if ( !$this->actions->hasValue( $action, 'root-permissions' ) ) { |
142 | return true; |
143 | } |
144 | |
145 | $permission = $this->getPermission( $root, $action, 'root-permissions' ); |
146 | |
147 | // If `root-permissions` is defined but not for the current state |
148 | // then action is denied |
149 | if ( $permission === null ) { |
150 | return false; |
151 | } |
152 | |
153 | return MediaWikiServices::getInstance()->getPermissionManager() |
154 | ->userHasAnyRight( $this->user, ...(array)$permission ); |
155 | } |
156 | |
157 | /** |
158 | * Check if a user is allowed to perform a certain action, depending on the |
159 | * status (deleted?) of the board. |
160 | * |
161 | * @param Workflow $workflow |
162 | * @param string $action |
163 | * @return bool |
164 | */ |
165 | public function isBoardAllowed( Workflow $workflow, $action ) { |
166 | $permissions = $this->actions->getValue( $action, 'core-delete-permissions' ); |
167 | |
168 | // If user is allowed to see deleted page content, there's no need to |
169 | // even check if it's been deleted (additional storage lookup) |
170 | $allowed = MediaWikiServices::getInstance()->getPermissionManager() |
171 | ->userHasAnyRight( $this->user, ...(array)$permissions ); |
172 | if ( $allowed ) { |
173 | return true; |
174 | } |
175 | |
176 | return !$workflow->isDeleted(); |
177 | } |
178 | |
179 | /** |
180 | * Check if a user is allowed to perform a certain action, only against 1 |
181 | * specific revision (whereas the default isAllowed() will check if the |
182 | * given $action is allowed for both given and the most current revision) |
183 | * |
184 | * @param AbstractRevision|null $revision |
185 | * @param string $action |
186 | * @return bool |
187 | */ |
188 | public function isRevisionAllowed( ?AbstractRevision $revision, $action ) { |
189 | // Users must have the core 'edit' permission to perform any write action in flow |
190 | $performsWrites = $this->actions->getValue( $action, 'performs-writes' ); |
191 | $pm = MediaWikiServices::getInstance()->getPermissionManager(); |
192 | if ( $performsWrites && !$pm->userHasRight( $this->user, 'edit' ) ) { |
193 | return false; |
194 | } |
195 | |
196 | $permission = $this->getPermission( $revision, $action ); |
197 | |
198 | // If no permission is defined for this state, then the action is not allowed |
199 | // check if permission is set for this action |
200 | if ( $permission === null ) { |
201 | return false; |
202 | } |
203 | |
204 | // Check if user is allowed to perform action against this revision |
205 | return $pm->userHasAnyRight( $this->user, ...(array)$permission ); |
206 | } |
207 | |
208 | /** |
209 | * Returns the permission specified in FlowActions for the given action |
210 | * against the given revision's moderation state. |
211 | * |
212 | * @param AbstractRevision|null $revision |
213 | * @param string $action |
214 | * @param string $type |
215 | * @return Closure|string |
216 | */ |
217 | public function getPermission( ?AbstractRevision $revision, $action, $type = 'permissions' ) { |
218 | // $revision may be null if the revision has yet to be created |
219 | $moderationState = AbstractRevision::MODERATED_NONE; |
220 | if ( $revision !== null ) { |
221 | $moderationState = $revision->getModerationState(); |
222 | } |
223 | $permission = $this->actions->getValue( $action, $type, $moderationState ); |
224 | |
225 | // Some permissions may be more complex to be defined as simple array |
226 | // values, in which case they're a Closure (which will accept |
227 | // AbstractRevision & FlowActionPermissions as arguments) |
228 | if ( $permission instanceof Closure ) { |
229 | $permission = $permission( $revision, $this ); |
230 | } |
231 | |
232 | return $permission; |
233 | } |
234 | |
235 | /** |
236 | * @param AbstractRevision $revision |
237 | * @return AbstractRevision |
238 | */ |
239 | protected function getRoot( AbstractRevision $revision ) { |
240 | if ( $revision instanceof PostSummary ) { |
241 | $topicId = $revision->getSummaryTargetId(); |
242 | } elseif ( $revision instanceof PostRevision && !$revision->isTopicTitle() ) { |
243 | try { |
244 | $topicId = $revision->getCollection()->getWorkflowId(); |
245 | } catch ( DataModelException $e ) { |
246 | // failed to locate root post (most likely in unit tests, where |
247 | // we didn't store the tree) |
248 | return $revision; |
249 | } |
250 | } else { |
251 | // if we can't the revision it back to a root, this revision is root |
252 | return $revision; |
253 | } |
254 | |
255 | $collection = PostCollection::newFromId( $topicId ); |
256 | return $collection->getLastRevision(); |
257 | } |
258 | |
259 | /** |
260 | * @return User |
261 | */ |
262 | public function getUser() { |
263 | return $this->user; |
264 | } |
265 | |
266 | /** |
267 | * @return FlowActions |
268 | */ |
269 | public function getActions() { |
270 | return $this->actions; |
271 | } |
272 | |
273 | /** |
274 | * @param User $user |
275 | */ |
276 | public function setUser( User $user ) { |
277 | $this->user = $user; |
278 | } |
279 | } |