EBernhardson has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/91562


Change subject: Consolidate action access to a single location
......................................................................

Consolidate action access to a single location

Views and blocks were using different ways to decide what a user can and cannot
do.  This pulls the related code out of the view helper and reuses in both the
view helpers and the blocks.

Change-Id: I03956a78c493fc56e5aad11aab9ffce17e56bba5
---
M Flow.php
M includes/Block/Topic.php
A includes/PostActions.php
M includes/Templating.php
M includes/View/PostActionMenu.php
5 files changed, 226 insertions(+), 98 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Flow 
refs/changes/62/91562/1

diff --git a/Flow.php b/Flow.php
index 451b463..291df6c 100755
--- a/Flow.php
+++ b/Flow.php
@@ -72,6 +72,7 @@
 $wgAutoloadClasses['Flow\TalkpageManager'] = $dir . 
'includes/TalkpageManager.php';
 $wgAutoloadClasses['Flow\NotificationFormatter'] = $dir . 
'includes/Notifications/Formatter.php';
 $wgAutoloadClasses['Flow\NotificationController'] = $dir . 
'includes/Notifications/Controller.php';
+$wgAutoloadClasses['Flow\PostActions'] = $dir . 'includes/PostActions.php';
 
 // Classes that model our data
 $wgAutoloadClasses['Flow\Model\Definition'] = $dir . 
'includes/Model/Definition.php';
diff --git a/includes/Block/Topic.php b/includes/Block/Topic.php
index 6cec6dc..2652d20 100644
--- a/includes/Block/Topic.php
+++ b/includes/Block/Topic.php
@@ -10,6 +10,7 @@
 use Flow\Model\AbstractRevision;
 use Flow\Model\PostRevision;
 use Flow\NotificationController;
+use Flow\PostActions;
 use Flow\Templating;
 use EchoEvent;
 use User;
@@ -34,6 +35,12 @@
                'hide-topic', 'edit-title',
        );
 
+       /**
+        * @var PostActions $security Allows or denys actions to be performed
+        */
+       protected $security;
+
+
        public function __construct( Workflow $workflow, ManagerGroup $storage, 
NotificationController $notificationController, $root ) {
                parent::__construct( $workflow, $storage, 
$notificationController );
                if ( $root instanceof PostRevision ) {
@@ -45,6 +52,11 @@
                                'Expected PostRevision or RootPostLoader, 
received: ' . is_object( $root ) ? get_class( $root ) : gettype( $root )
                        );
                }
+       }
+
+       public function init( $action, $user ) {
+               parent::init( $action, $user );
+               $this->security = new PostActions( $user );
        }
 
        protected function validate() {
@@ -97,6 +109,10 @@
                        if ( !$topicTitle ) {
                                throw new \Exception( 'No revision associated 
with workflow?' );
                        }
+                       if ( !$this->security->isAllowed( $topicTitle, 
'edit-title' ) ) {
+                               $this->errors['security'] = wfMessage( 
'flow-error-not-allowed' );
+                               return;
+                       }
 
                        $this->newRevision = $topicTitle->newNextRevision( 
$this->user, $this->submitted['content'], 'flow-rev-message-edit-title' );
 
@@ -122,6 +138,9 @@
                        $post = $this->storage->get( 'PostRevision', 
$this->submitted['replyTo'] );
                        if ( !$post ) {
                                $this->errors['replyTo'] = wfMessage( 
'flow-error-invalid-replyto' );
+                       } elseif ( !$this->security->isAllowed( $post, 'reply' 
) ) {
+                               // Or should the check be rolled into the 
!$post condition?
+                               $this->errors['security'] = wfMessage( 
'flow-error-not-allowed' );
                        } else {
                                // TODO: assert post belongs to this tree?  
Does it really matter?
                                // answer: might not belong, and probably does 
matter due to inter-wiki interaction
@@ -154,6 +173,9 @@
                if ( !$post ) {
                        $this->errors['moderate-post'] = wfMessage( 
'flow-error-invalid-postId' );
                        return;
+               } elseif ( !$this->security->isAllowed( $post, 
"{$moderationState}-post" ) ) {
+                       $this->errors['security'] = wfMessage( 
'flow-error-not-allowed' );
+                       return;
                }
 
                $this->newRevision = $post->moderate( $this->user, 
$moderationState );
@@ -170,6 +192,9 @@
                $post = $this->loadRequestedPost( $this->submitted['postId'] );
                if ( !$post ) {
                        $this->errors['restore-post'] = wfMessage( 
'flow-error-invalid-postId' );
+                       return;
+               } elseif ( !$this->security->isAllowed( $post, "restore-post" ) 
) {
+                       $this->errors['restore-post'] = wfMessage( 
'flow-error-not-allowed' );
                        return;
                }
 
@@ -193,8 +218,8 @@
                        $this->errors['edit-post'] = wfMessage( 
'flow-post-not-found' );
                        return;
                }
-               if ( !$post->isAllowedToEdit( $this->user ) ) {
-                       $this->errors['edit-post'] = wfMessage( 
'flow-error-edit-restricted' );
+               if ( !$this->security->isAllowed( $post, 'edit-post' ) ) {
+                       $this->errors['security'] = wfMessage( 
'flow-error-not-allowed' );
                        return;
                }
 
@@ -269,6 +294,10 @@
                        return $this->renderPostHistory( $templating, $options, 
$return );
 
                case 'topic-history':
+                       $history = $this->loadTopicHistory();
+                       if ( !$this->security->isAllowed( reset( $history ), 
'post-history' ) ) {
+                               throw new \Exception( 'Not Allowed' );
+                       }
                        return $templating->render( 
"flow:topic-history.html.php", array(
                                'block' => $this,
                                'topic' => $this->workflow,
@@ -279,6 +308,10 @@
                        return $this->renderEditPost( $templating, $options, 
$return );
 
                case 'edit-title':
+                       $topicTitle = $this->loadTopicTitle();
+                       if ( !$this->security->isAllowed( $topicTitle, 
'edit-post' ) ) {
+                               throw new \Exception( 'Not Allowed' );
+                       }
                        return $templating->render( "flow:edit-title.html.php", 
array(
                                'block' => $this,
                                'topic' => $this->workflow,
@@ -288,10 +321,14 @@
                default:
                        $root = $this->loadRootPost();
 
+                       if ( !$this->security->isAllowed( $root, 'view' ) ) {
+                               throw new \Exception( 'Not Allowed' );
+                       }
+
                        if ( isset( $options['postId'] ) ) {
                                $indexDescendant = $root->registerDescendant( 
$options['postId'] );
                                $post = $root->getRecursiveResult( 
$indexDescendant );
-                               if ( $post === false ) {
+                               if ( $post === null ) {
                                        throw new \MWException( 'Requested 
postId is not available within post tree' );
                                }
 
@@ -314,6 +351,10 @@
                if ( !isset( $options['postId'] ) ) {
                        throw new \Exception( 'No postId provided' );
                }
+               $history = $this->getHistory( $options['postId'] );
+               if ( !$this->security->isAllowed( reset( $history ), 
'post-history' ) ) {
+                       throw new \MWException( 'Not Allowed' );
+               }
                return $templating->render( "flow:post-history.html.php", array(
                        'block' => $this,
                        'topic' => $this->workflow,
@@ -328,6 +369,9 @@
                $post = $this->loadRequestedPost( $options['postId'] );
                if ( $post->isModerated() ) {
                        throw new \Exception( 'Cannot edit restricted post.  
Restore first.' );
+               }
+               if ( !$this->security->isAllowed( $post, 'edit-post' ) ) {
+                       throw new \MWException( 'Not Allowed' );
                }
                return $templating->render( "flow:edit-post.html.php", array(
                        'block' => $this,
@@ -347,12 +391,19 @@
                        }
 
                        if ( ! $post ) {
-                               throw new MWException( "Requested post could 
not be found" );
+                               throw new \MWException( "Requested post could 
not be found" );
                        }
 
-                       return array( $this->renderPostAPI( $templating, $post, 
$options ) );
+                       $res = $this->renderPostAPI( $templating, $post, 
$options );
+                       if ( $res === null ) {
+                               throw new \MWException( 'Not Allowed' );
+                       }
+                       return array( $res );
                } else {
-                       return $this->renderTopicAPI( $templating, $options );
+                       $output = $this->renderTopicAPI( $templating, $options 
);
+                       if ( $output === null ) {
+                       }
+                       return $output;
                }
        }
 
@@ -361,6 +412,9 @@
                $rootPost = $this->loadRootPost();
                $topic = $this->workflow;
 
+               if ( !$this->security->isAllowed( $rootPost, 'view' ) ) {
+                       throw new \MWException( 'Not Allowed' );
+               }
                $output = array(
                        '_element' => 'post',
                        'title' => $rootPost->getContent( null, 'wikitext' ),
@@ -389,15 +443,23 @@
                }
 
                foreach( $rootPost->getChildren() as $child ) {
-                       $output[] = $this->renderPostAPI( $templating, $child, 
$options );
+                       $res = $this->renderPostAPI( $templating, $child, 
$options );
+                       if ( $res !== null ) {
+                               $output[] = $res;
+                       }
                }
 
                return $output;
        }
 
        protected function renderPostAPI( Templating $templating, PostRevision 
$post, array $options ) {
-               $output = array();
+               if ( !$this->security->isAllowed( $post, 'view' ) ) {
+                       // we have to return null, or we would have to 
duplicate this call when rendering children.
+                       // callers must check for null and do as appropriate
+                       return null;
+               }
 
+               $output = array();
                $output['post-id'] = $post->getPostId()->getHex();
                $contentFormat = 'wikitext';
 
@@ -412,14 +474,17 @@
                                '*' => $post->getContent( null, $contentFormat 
),
                                'format' => $contentFormat
                        );
-                       $output['user'] = $post->getUserText();
+                       $output['user'] = $post->getCreatorText();
                }
 
                if ( ! isset( $options['no-children'] ) ) {
                        $children = array( '_element' => 'post' );
 
                        foreach( $post->getChildren() as $child ) {
-                               $children[] = $this->renderPostAPI( 
$templating, $child, $options );
+                               $res = $this->renderPostAPI( $templating, 
$child, $options );
+                               if ( $res !== null ) {
+                                       $children[] = $res;
+                               }
                        }
 
                        if ( count( $children ) > 1 ) {
@@ -442,11 +507,13 @@
                $output['post-id'] = $postId;
 
                foreach( $history as $revision ) {
-                       $output[] = array(
-                               'revision-id' => 
$revision->getRevisionId()->getHex(),
-                               'revision-author' => $revision->getUserText(),
-                               'revision-change-type' => 
$revision->getChangeType(),
-                       );
+                       if ( $this->security->isAllowed( $revision, 'view' ) ) {
+                               $output[] = array(
+                                       'revision-id' => 
$revision->getRevisionId()->getHex(),
+                                       'revision-author' => 
$revision->getUserText(),
+                                       'revision-change-type' => 
$revision->getChangeType(),
+                               );
+                       }
                }
 
                return $output;
@@ -532,7 +599,7 @@
                        array( 'topic_root' => $this->workflow->getId() ),
                        array( 'sort' => 'rev_id', 'order' => 'DESC', 'limit' 
=> 100 )
                );
-               if ( $found ) {
+               if ( !$found ) {
                        return $found;
                } else {
                        throw new \MWException( "Unable to load topic history 
for topic " . $this->workflow->getId()->getHex() );
diff --git a/includes/PostActions.php b/includes/PostActions.php
new file mode 100644
index 0000000..b67b6ab
--- /dev/null
+++ b/includes/PostActions.php
@@ -0,0 +1,131 @@
+<?php
+
+namespace Flow;
+
+use Flow\Model\PostRevision;
+use Closure;
+
+/**
+ * role based security for posts based on moderation state
+ * Perhaps better name?
+ */
+class PostActions {
+
+       public function __construct( $user ) {
+               $this->user = $user;
+
+               $this->actions = array(
+                       'hide-post' => array(
+                               // Permissions required to perform action. The 
key is the moderation state
+                               // of the post to perform the action against. 
The value is a string or array
+                               // of user rights that can allow this action.
+                               PostRevision::MODERATED_NONE => 'flow-hide',
+                       ),
+
+                       'delete-post' => array(
+                               PostRevision::MODERATED_NONE => 'flow-delete',
+                               PostRevision::MODERATED_HIDDEN => 'flow-delete',
+                       ),
+
+                       'censor-post' => array(
+                               PostRevision::MODERATED_NONE => 'flow-censor',
+                               PostRevision::MODERATED_HIDDEN => 'flow-censor',
+                               PostRevision::MODERATED_DELETED => 
'flow-censor',
+                       ),
+
+                       'restore-post' => array(
+                               PostRevision::MODERATED_HIDDEN => array( 
'flow-hide', 'flow-delete', 'flow-censor' ),
+                               PostRevision::MODERATED_DELETED => array( 
'flow-delete', 'flow-censor' ),
+                               PostRevision::MODERATED_CENSORED => 
'flow-censor',
+                       ),
+
+                       'post-history' => array(
+                               PostRevision::MODERATED_NONE => '',
+                               PostRevision::MODERATED_HIDDEN => '',
+                               PostRevision::MODERATED_DELETED => '',
+                               PostRevision::MODERATED_CENSORED => 
'flow-censor',
+                       ),
+
+                       'edit-post' => function( PostRevision $post ) use ( 
$user ) {
+                               // no permissions needed for own posts
+                               return array(
+                                       PostRevision::MODERATED_NONE => 
$post->isAllowedToEdit( $user ) ? '' : 'flow-edit-post',
+                                       PostRevision::MODERATED_HIDDEN => 
$post->isAllowedToEdit( $user ) ? '' : 'flow-edit-post',
+                                       PostRevision::MODERATED_DELETED => 
$post->isAllowedToEdit( $user ) ? '' : 'flow-edit-post',
+                                       PostRevision::MODERATED_CENSORED => 
$post->isAllowedToEdit( $user ) ? '' : 'flow-edit-post',
+                               );
+                       },
+
+                       'view' => array(
+                               PostRevision::MODERATED_NONE => '',
+                               PostRevision::MODERATED_HIDDEN => '',
+                               PostRevision::MODERATED_DELETED => '',
+                               PostRevision::MODERATED_CENSORED => '',
+                       ),
+               );
+       }
+
+       public function getAllowedActions( PostRevision $post ) {
+               $allowed = array();
+               foreach( array_keys( $this->actions ) as $action ) {
+                       if ( $this->isAllowedAny( $post, $action ) ) {
+                               $allowed[] = $action;
+                       }
+               }
+               return $allowed;
+       }
+
+       /**
+        * Check if a user is allowed to perform (a) certain action(s).
+        *
+        * @param string $action
+        * @return bool
+        */
+       public function isAllowed( PostRevision $post, $action ) {
+               if ( !isset( $this->actions[$action] ) ) {
+                       return false;
+               }
+               $permissions = $this->actions[$action];
+               if ( $permissions instanceof Closure ) {
+                       $permissions = $permissions( $post );
+               }
+               $state = $post->getModerationState();
+               // If no permission is defined for this state, then the action 
is not allowed
+               // check if permission is set for this action
+               if ( !isset( $permissions[$state] ) ) {
+                       return false;
+               }
+
+               // check if user is allowed to perform action
+               $res = call_user_func_array(
+                       array( $this->user, 'isAllowedAny' ),
+                       (array) $permissions[$state]
+               );
+               return $res;
+       }
+
+       /**
+        * Check if a user is allowed to perform certain actions.
+        *
+        * @param string $action
+        * @param string[optional] $action2 Overloadable to check if either of 
the provided actions are allowed
+        * @return bool
+        */
+       public function isAllowedAny( PostRevision $post, $action /* [, 
$action2 [, ... ]] */ ) {
+               // slice to remove $post from actions
+               $actions = array_slice( func_get_args(), 1, -1 );
+               $allowed = false;
+
+               foreach ( $actions as $action ) {
+                       $allowed |= $this->isAllowed( $post, $action );
+
+                       // as soon as we've found one that is allowed, break
+                       if ( $allowed ) {
+                               break;
+                       }
+               }
+
+               return $allowed;
+       }
+}
+
diff --git a/includes/Templating.php b/includes/Templating.php
index 250404c..739b50e 100644
--- a/includes/Templating.php
+++ b/includes/Templating.php
@@ -98,7 +98,7 @@
                                // class has too many responsibilities to keep 
receiving all required objects in the constructor.
                                'postActionMenu' => new PostActionMenu(
                                        $this->urlGenerator,
-                                       $wgUser,
+                                       new PostActions( $wgUser ),
                                        $block,
                                        $post,
                                        $wgUser->getEditToken( $wgFlowTokenSalt 
)
@@ -220,7 +220,7 @@
         * Gets a Flow-formatted plaintext human-readable identifier for a user.
         * Usually the user's name, but it can also return "an anonymous user",
         * or information about an item's moderation state.
-        * 
+        *
         * @param  User             $user    The User object to get a 
description for.
         * @param  AbstractRevision $rev     An AbstractRevision object to 
retrieve moderation state from.
         * @param  bool             $showIPs Whether or not to show IP 
addresses for anonymous users
diff --git a/includes/View/PostActionMenu.php b/includes/View/PostActionMenu.php
index 547862d..1ca1540 100644
--- a/includes/View/PostActionMenu.php
+++ b/includes/View/PostActionMenu.php
@@ -4,9 +4,9 @@
 
 use Flow\Block\Block;
 use Flow\Model\PostRevision;
+use Flow\PostActions;
 use Flow\UrlGenerator;
 use Html;
-use User;
 
 class PostActionMenu {
        // Received via constructor
@@ -16,9 +16,9 @@
        protected $post;
        protected $user;
 
-       public function __construct( UrlGenerator $urlGenerator, User $user, 
Block $block, PostRevision $post, $editToken ) {
+       public function __construct( UrlGenerator $urlGenerator, PostActions 
$security, Block $block, PostRevision $post, $editToken ) {
                $this->urlGenerator = $urlGenerator;
-               $this->user = $user;
+               $this->security = $security;
                $this->block = $block;
                $this->post = $post;
                $this->editToken = $editToken;
@@ -34,60 +34,24 @@
                $actions = array(
                        'hide-post' => array(
                                'method' => 'POST',
-                               'permissions' => array(
-                                       PostRevision::MODERATED_NONE => 
'flow-hide',
-                               ),
                        ),
                        'delete-post' => array(
                                'method' => 'POST',
-                               'permissions' => array(
-                                       PostRevision::MODERATED_NONE => 
'flow-delete',
-                                       PostRevision::MODERATED_HIDDEN => 
'flow-delete',
-                               ),
                        ),
                        'censor-post' => array(
                                'method' => 'POST',
-                               'permissions' => array(
-                                       PostRevision::MODERATED_NONE => 
'flow-censor',
-                                       PostRevision::MODERATED_HIDDEN => 
'flow-censor',
-                                       PostRevision::MODERATED_DELETED => 
'flow-censor',
-                               ),
                        ),
                        'restore-post' => array(
                                'method' => 'POST',
-                               'permissions' => array(
-                                       PostRevision::MODERATED_HIDDEN => 
array( 'flow-hide', 'flow-delete', 'flow-censor' ),
-                                       PostRevision::MODERATED_DELETED => 
array( 'flow-delete', 'flow-censor' ),
-                                       PostRevision::MODERATED_CENSORED => 
'flow-censor',
-                               ),
                        ),
                        'post-history' => array(
                                'method' => 'GET',
-                               'permissions' => array(
-                                       PostRevision::MODERATED_NONE => '',
-                                       PostRevision::MODERATED_HIDDEN => '',
-                                       PostRevision::MODERATED_DELETED => '',
-                                       PostRevision::MODERATED_CENSORED => 
'flow-censor',
-                               ),
                        ),
                        'edit-post' => array(
                                'method' => 'GET',
-                               'permissions' => array(
-                                       // no permissions needed for own posts
-                                       PostRevision::MODERATED_NONE => 
$this->post->isAllowedToEdit( $this->user ) ? '' : 'flow-edit-post',
-                                       PostRevision::MODERATED_HIDDEN => 
$this->post->isAllowedToEdit( $this->user ) ? '' : 'flow-edit-post',
-                                       PostRevision::MODERATED_DELETED => 
$this->post->isAllowedToEdit( $this->user ) ? '' : 'flow-edit-post',
-                                       PostRevision::MODERATED_CENSORED => 
$this->post->isAllowedToEdit( $this->user ) ? '' : 'flow-edit-post',
-                               ),
                        ),
                        'view' => array(
                                'method' => 'GET',
-                               'permissions' => array(
-                                       PostRevision::MODERATED_NONE => '',
-                                       PostRevision::MODERATED_HIDDEN => '',
-                                       PostRevision::MODERATED_DELETED => '',
-                                       PostRevision::MODERATED_CENSORED => '',
-                               ),
                        ),
                );
 
@@ -103,13 +67,11 @@
         * @return string|bool Button HTML or false on failure
         */
        public function getButton( $action, $content, $class ) {
-               $details = $this->getActionDetails( $action );
-
-               if ( !$this->isAllowed( $action ) ) {
+               if ( !$this->security->isAllowed( $this->post, $action ) ) {
                        return false;
                }
-
                $data = array( $this->block->getName() . '[postId]' => 
$this->post->getPostId()->getHex() );
+               $details = $this->getActionDetails( $action );
                if ( $details['method'] === 'POST' ) {
                        return $this->postAction( $action, $data, $content, 
$class );
                } else {
@@ -117,48 +79,15 @@
                }
        }
 
-       /**
-        * Check if a user is allowed to perform (a) certain action(s).
-        *
-        * @param string $action
-        * @return bool
-        */
        public function isAllowed( $action ) {
-               $details = $this->getActionDetails( $action );
-
-               // check if permission is set for this action
-               if ( !isset( 
$details['permissions'][$this->post->getModerationState()] ) ) {
-                       return false;
-               }
-
-               // check if user is allowed to perform action
-               return call_user_func_array(
-                       array( $this->user, 'isAllowedAny' ),
-                       (array) 
$details['permissions'][$this->post->getModerationState()]
-               );
+               return $this->security->isAllowed( $this->post, $action );
        }
 
-       /**
-        * Check if a user is allowed to perform certain actions.
-        *
-        * @param string $action
-        * @param string[optional] $action2 Overloadable to check if either of 
the provided actions are allowed
-        * @return bool
-        */
        public function isAllowedAny( $action /* [, $action2 [, ... ]] */ ) {
-               $actions = func_get_args();
-               $allowed = false;
+               $arguments = func_get_args();
+               array_unshift( $arguments, $this->post );
 
-               foreach ( $actions as $action ) {
-                       $allowed |= $this->isAllowed( $action );
-
-                       // as soon as we've found one that is allowed, break
-                       if ( $allowed ) {
-                               break;
-                       }
-               }
-
-               return $allowed;
+               return call_user_func_array( array( $this->security, 
'isAllowedAny' ), $arguments );
        }
 
        /**

-- 
To view, visit https://gerrit.wikimedia.org/r/91562
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I03956a78c493fc56e5aad11aab9ffce17e56bba5
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/Flow
Gerrit-Branch: master
Gerrit-Owner: EBernhardson <[email protected]>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to