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 | 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 | } |