Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
22.97% covered (danger)
22.97%
17 / 74
25.00% covered (danger)
25.00%
3 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
RevisionActionPermissions
22.97% covered (danger)
22.97%
17 / 74
25.00% covered (danger)
25.00%
3 / 12
628.29
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getAllowedActions
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 isAllowed
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
90
 isAllowedAny
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 isRootAllowed
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 isBoardAllowed
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 isRevisionAllowed
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
4.03
 getPermission
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 getRoot
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 getUser
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getActions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setUser
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace Flow;
4
5use Closure;
6use Flow\Collection\CollectionCache;
7use Flow\Collection\PostCollection;
8use Flow\Exception\DataModelException;
9use Flow\Model\AbstractRevision;
10use Flow\Model\PostRevision;
11use Flow\Model\PostSummary;
12use Flow\Model\Workflow;
13use MediaWiki\MediaWikiServices;
14use MediaWiki\User\User;
15
16/**
17 * Role based security for revisions based on moderation state
18 */
19class 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}