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