EBernhardson (WMF) has uploaded a new change for review. https://gerrit.wikimedia.org/r/80303
Change subject: First stab at content moderation ...................................................................... First stab at content moderation Change-Id: I39241b68fd56d74ffe5a5051777dcb6e09750d9d --- M Flow.i18n.php M Flow.php M flow.sql M includes/Block/Topic.php M includes/Data/ObjectManager.php M includes/Data/RevisionStorage.php M includes/Model/AbstractRevision.php M includes/Model/PostRevision.php M templates/post.html.php 9 files changed, 354 insertions(+), 168 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Flow refs/changes/03/80303/1 diff --git a/Flow.i18n.php b/Flow.i18n.php index 4cd3713..ce378c3 100644 --- a/Flow.i18n.php +++ b/Flow.i18n.php @@ -15,7 +15,12 @@ 'flow-disclaimer' => "By clicking the \"Add message\" button, you agree to the Terms of Use, and you irrevocably agree to release your contribution under the CC-BY-SA 3.0 License and the GFDL. You agree that a hyperlink or URL is sufficient attribution under the Creative Commons license.", + 'flow-post-hidden' => '[post hidden]', + 'flow-post-hidden-by' => 'Hidden by $1 $2', 'flow-post-deleted' => '[post deleted]', + 'flow-post-deleted-by' => 'Deleted by $1 $2', + 'flow-post-oversighted' => '[post oversighted]', + 'flow-post-oversighted-by' => 'Oversighted by $1 $2', 'flow-post-actions' => 'actions', 'flow-topic-actions' => 'actions', 'flow-cancel' => 'Cancel', @@ -33,7 +38,9 @@ 'flow-post-action-view' => 'Permalink', 'flow-post-action-post-history' => 'Post history', + 'flow-post-action-oversight-post' => 'Oversight post', 'flow-post-action-delete-post' => 'Delete post', + 'flow-post-action-hide-post' => 'Hide post', 'flow-post-action-edit-post' => 'Edit post', 'flow-post-action-edit' => 'Edit', 'flow-post-action-restore-post' => 'Restore post', @@ -52,6 +59,7 @@ 'flow-error-missing-replyto' => 'No replyTo parameter was supplied. This parameter is required for the "reply" action.', 'flow-error-invalid-replyto' => 'replyTo parameter was invalid. The specified post could not be found.', 'flow-error-delete-failure' => 'Deletion of this item failed.', + 'flow-error-hide-failure' => 'Hiding this item failed.', 'flow-error-missing-postId' => 'No postId parameter was supplied. This parameter is required to manipulate a post.', 'flow-error-invalid-postId' => 'postId parameter was invalid. The specified post could not be found.', 'flow-error-restore-failure' => 'Restoration of this item failed.', @@ -67,6 +75,7 @@ 'flow-comment-restored' => 'Restored comment', 'flow-comment-deleted' => 'Deleted comment', + 'flow-comment-hidden' => 'Hidden comment', ); /** Message documentation (Message documentation) @@ -86,6 +95,7 @@ See also: * {{msg-mw|Wikimedia-copyrightwarning}}', 'flow-post-deleted' => 'Used as username/content if the post was deleted.', + 'flow-post-hidden' => 'Used as username/content if the post was hidden.', 'flow-post-actions' => 'Used as link text. {{Identical|Action}}', 'flow-topic-actions' => 'Used as link text. @@ -104,6 +114,7 @@ 'flow-post-action-view' => 'Used as text for the link which is used to view. {{Identical|Permalink}}', 'flow-post-action-post-history' => 'Used as text for the link which is used to view post-history of the topic.', + 'flow-post-action-hide-post' => 'Used as label for the Submit button.', 'flow-post-action-delete-post' => 'Used as label for the Submit button. See also: @@ -144,6 +155,9 @@ 'flow-error-delete-failure' => 'Used as error message. "this item" refers either "this topic" or "this post".', + 'flow-error-hide-failure' => 'Used as error message. + +"this item" refers either "this topic" or "this post".', 'flow-error-missing-postId' => 'Used as error message when deleting/restoring a post. "manipulate" refers either "delete" or "restore".', @@ -161,6 +175,7 @@ See also: * {{msg-mw|Flow-comment-deleted}}', + 'flow-comment-hidden' => 'Used as comment when the comment has been hidden.', 'flow-comment-deleted' => 'Used as comment when the comment has been deleted. See also: diff --git a/Flow.php b/Flow.php index 7ff4339..042ecfd 100755 --- a/Flow.php +++ b/Flow.php @@ -165,6 +165,12 @@ ), ); +// User permissions + +$wgGroupPermissions['autoconfirmed']['flow-hide'] = true; +$wgGroupPermissions['sysop']['flow-delete'] = true; +$wgGroupPermissions['oversight']['flow-oversight'] = true; + // Configuration // URL for more information about the Flow notification system diff --git a/flow.sql b/flow.sql index caad08b..b6ef1fd 100644 --- a/flow.sql +++ b/flow.sql @@ -112,6 +112,13 @@ rev_content mediumblob not null, -- comment attached to revision's flag change rev_comment varchar(255) binary null, + -- current moderation state + rev_mod_state varchar(32) binary not null, + -- moderated by who? + rev_mod_user_id bigint unsigned, + rev_mod_user_text varchar(255) binary, + rev_mod_timestamp varchar(14) binary, + PRIMARY KEY (rev_id) ) /*$wgDBTableOptions*/; diff --git a/includes/Block/Topic.php b/includes/Block/Topic.php index e5c9d1b..8433452 100644 --- a/includes/Block/Topic.php +++ b/includes/Block/Topic.php @@ -4,6 +4,7 @@ use Flow\Model\UUID; use Flow\Model\Workflow; +use Flow\Model\AbstractRevision; use Flow\Model\PostRevision; use Flow\Data\ManagerGroup; use Flow\Data\RootPostLoader; @@ -23,8 +24,12 @@ // POST actions, GET do not need to be listed // unrecognized GET actions fallback to 'view' protected $supportedActions = array( - 'edit-post', 'delete-post', 'restore-post', - 'reply', 'delete-topic', 'edit-title', + // Standard editing + 'edit-post', 'reply', + // Moderation + 'hide-post', 'delete-post', 'oversight-post', 'restore-post', + // Other stuff + 'hide-topic', 'edit-title', ); public function __construct( Workflow $workflow, ManagerGroup $storage, $root ) { @@ -50,13 +55,21 @@ $this->validateReply(); break; - case 'delete-topic': + case 'hide-topic': // this should be a workflow level action, not implemented per-block - $this->validateDeleteTopic(); + $this->validateHideTopic(); + break; + + case 'hide-post': + $this->validateModeratePost( AbstractRevision::MODERATED_HIDDEN ); break; case 'delete-post': - $this->validateDeletePost(); + $this->validateModeratePost( AbstractRevision::MODERATED_DELETED ); + break; + + case 'oversight-post': + $this->validateModeratePost( AbstractRevision::MODERATED_OVERSIGHTED ); break; case 'restore-post': @@ -112,33 +125,27 @@ } } - protected function validateDeleteTopic() { + protected function validateHideTopic() { if ( !$this->workflow->lock( $this->user ) ) { - $this->errors['delete-topic'] = wfMessage( 'flow-error-delete-failure' ); + $this->errors['hide-topic'] = wfMessage( 'flow-error-hide-failure' ); } } - protected function validateDeletePost() { + protected function validateModeratePost( $moderationState ) { if ( empty( $this->submitted['postId'] ) ) { - $this->errors['delete-post'] = wfMessage( 'flow-error-missing-postId' ); + $this->errors['moderate-post'] = wfMessage( 'flow-error-missing-postId' ); return; } - $found = $this->storage->find( - 'PostRevision', - array( 'tree_rev_descendant_id' => UUID::create( $this->submitted['postId'] ) ), - array( 'sort' => 'rev_id', 'order' => 'DESC', 'limit' => 1 ) - ); - if ( !$found ) { - $this->errors['delete-post'] = wfMessage( 'flow-error-invalid-postId' ); + $post = $this->loadRequestedPost( $this->submitted['postId'] ); + if ( !$post ) { + $this->errors['moderate-post'] = wfMessage( 'flow-error-invalid-postId' ); return; } - // TODO: validate it has $this->workflow as its topic - $post = reset( $found ); - // returns new revision to save - $this->newRevision = $post->addFlag( $this->user, 'deleted', 'flow-comment-deleted' ); + $this->newRevision = $post->moderate( $this->user, $moderationState ); if ( !$this->newRevision ) { - $this->errors['delete-post'] = wfMessage( 'flow-error-delete-failure' ); + die( 'no allowed' ); + $this->errors['moderate'] = wfMessage( 'flow-error-not-allowed' ); } } @@ -147,20 +154,15 @@ $this->errors['restore-post'] = wfMessage( 'flow-error-missing-postId' ); return; } - $found = $this->storage->find( - 'PostRevision', - array( 'tree_rev_descendant_id' => UUID::create( $this->submitted['postId'] ) ), - array( 'sort' => 'rev_id', 'order' => 'DESC', 'limit' => 1 ) - ); - if ( !$found ) { + $post = $this->loadRequestedPost( $this->submitted['postId'] ); + if ( !$post ) { $this->errors['restore-post'] = wfMessage( 'flow-error-invalid-postId' ); return; } - $post = reset( $found ); - $this->newRevision = $post->removeFlag( $this->user, 'deleted', 'flow-comment-restored' ); + $this->newRevision = $post->restore( $this->user ); if ( !$this->newRevision ) { - $this->errors['restore-post'] = wfMessage( 'flow-error-restore-failure' ); + $this->errors['restore-post'] = wfMessage( 'flow-error-not-allowed' ); } } @@ -189,7 +191,9 @@ public function commit() { switch( $this->action ) { case 'reply': + case 'hide-post': case 'delete-post': + case 'oversight-post': case 'restore-post': case 'edit-title': case 'edit-post': @@ -221,6 +225,7 @@ return $output; break; + case 'delete-topic': $this->storage->put( $this->workflow ); @@ -291,6 +296,10 @@ if ( !isset( $options['postId'] ) ) { throw new \Exception( 'No postId provided' ); } + $post = $this->loadRequestedPost( $options['postId'] ); + if ( $post->isFlaggedAll( 'oversighted', 'deleted', 'hidden' ) ) { + throw new \Exception( 'Cannot edit restricted post. Restore first.' ); + } return $templating->render( "flow:edit-post.html.php", array( 'block' => $this, 'topic' => $this->workflow, @@ -353,7 +362,7 @@ $output['post-id'] = $post->getPostId()->getHex(); - if ( $post->isFlagged( 'deleted' ) ) { + if ( $post->isFlaggedAll( 'deleted' ) ) { $output['post-deleted'] = 'post-deleted'; } else { $output['content'] = array( '*' => $post->getContent() ); diff --git a/includes/Data/ObjectManager.php b/includes/Data/ObjectManager.php index b460160..7d04460 100644 --- a/includes/Data/ObjectManager.php +++ b/includes/Data/ObjectManager.php @@ -413,6 +413,22 @@ } } + static public function calcUpdates( array $old, array $new ) { + $updates = array(); + foreach ( array_keys( $new ) as $key ) { + if ( !array_key_exists( $key, $old ) || $old[$key] !== $new[$key] ) { + $updates[$key] = $new[$key]; + } + unset( $old[$key] ); + } + // These keys dont exist in $new + foreach ( array_keys( $old ) as $key ) { + $updates[$key] = null; + } + return $updates; + } + + /** * Separate a set of keys from an array. Returns null if not * all keys are set. @@ -506,7 +522,7 @@ $missing = array_diff( $this->primaryKey, array_keys( $old ) ); throw new PersistenceException( 'Row has null primary key: ' . implode( $missing ) ); } - $updates = $this->calcUpdates( $old, $new ); + $updates = ObjectManager::calcUpdates( $old, $new ); if ( !$updates ) { return true; // nothing to change, success } @@ -517,22 +533,6 @@ // we also want to check that $pk actually selected a row to update return $res && $dbw->affectedRows(); } - - protected function calcUpdates( array $old, array $new ) { - $updates = array(); - foreach ( array_keys( $new ) as $key ) { - if ( !array_key_exists( $key, $old ) || $old[$key] !== $new[$key] ) { - $updates[$key] = $new[$key]; - } - unset( $old[$key] ); - } - // These keys dont exist in $new - foreach ( array_keys( $old ) as $key ) { - $updates[$key] = null; - } - return $updates; - } - /** * @return boolean success diff --git a/includes/Data/RevisionStorage.php b/includes/Data/RevisionStorage.php index 0233371..543d2ae 100644 --- a/includes/Data/RevisionStorage.php +++ b/includes/Data/RevisionStorage.php @@ -10,7 +10,9 @@ use User; abstract class RevisionStorage implements WritableObjectStorage { - static protected $allowedUpdateColumns = array( 'rev_flags' ); + static protected $allowedUpdateColumns = array( + 'rev_mod_state', 'rev_mod_user_id', 'rev_mod_user_text', 'rev_mod_timestamp', + ); protected $dbFactory; protected $externalStores; @@ -267,7 +269,8 @@ // This is to *UPDATE* a revision. It should hardly ever be used. // For the most part should insert a new revision. This will only be called // for oversighting? - public function update( array $row, array $changeSet ) { + public function update( array $old, array $new ) { + $changeSet = ObjectManager::calcUpdates( $old, $new ); $extra = array_diff( array_keys( $changeSet ), self::$allowedUpdateColumns ); if ( $extra ) { throw new \MWException( 'Update not allowed on: ' . implode( ', ', $extra ) ); @@ -279,14 +282,15 @@ $dbw = $this->dbFactory->getDB( DB_MASTER ); $res = $dbw->update( 'flow_revision', - $updates, - array( 'rev_id' => $row['rev_id'] ), + $rev, + array( 'rev_id' => $old['rev_id'] ), __METHOD__ ); - if ( !( $res && $res->numRows() ) ) { + if ( !( $res && $dbw->affectedRows() ) ) { return false; } } + // TODO: this probably wont work, it needs $row return $this->updateRelated( $rev, $related ); } @@ -456,12 +460,12 @@ } public function onAfterInsert( $object, array $new ) { - $new['topic_root'] = $this->treeRepository->findRoot( UUID::create( $new['tree_rev_descendant_id'] ) ); + $new['topic_root'] = $this->treeRepository->findRoot( UUID::create( $new['tree_rev_descendant_id'] ) )->getBinary(); parent::onAfterInsert( $object, $new ); } public function onAfterUpdate( $object, array $old, array $new ) { - $old['topic_root'] = $new['topic_root'] = $this->treeRepository->findRoot( UUID::create( $old['tree_rev_descendant_id'] ) ); + $old['topic_root'] = $new['topic_root'] = $this->treeRepository->findRoot( UUID::create( $old['tree_rev_descendant_id'] ) )->getBinary(); parent::onAfterUpdate( $object, $old, $new ); } diff --git a/includes/Model/AbstractRevision.php b/includes/Model/AbstractRevision.php index 7b87cfc..f487c90 100644 --- a/includes/Model/AbstractRevision.php +++ b/includes/Model/AbstractRevision.php @@ -2,9 +2,39 @@ namespace Flow\Model; +use MWTimestamp; use User; abstract class AbstractRevision { + const MODERATED_NONE = ''; + const MODERATED_HIDDEN = 'hide'; + const MODERATED_DELETED = 'delete'; + + const MODERATED_OVERSIGHTED = 'oversight'; + + static private $perms = array( + self::MODERATED_NONE => array( + 'perm' => null, + 'usertext' => null, + 'content' => null + ), + self::MODERATED_HIDDEN => array( + 'perm' => 'flow-hide', + 'usertext' => 'flow-post-hidden', + 'content' => 'flow-post-hidden-by', + ), + self::MODERATED_DELETED => array( + 'perm' => 'flow-delete', + 'usertext' => 'flow-post-deleted', + 'content' => 'flow-post-deleted-by', + ), + self::MODERATED_OVERSIGHTED => array( + 'perm' => 'flow-oversight', + 'usertext' => 'flow-post-oversighted', + 'content' => 'flow-post-oversighted-by', + ), + ); + protected $revId; protected $userId; protected $userText; @@ -22,6 +52,15 @@ // This is decompressed on-demand from $this->content in self::getContent() protected $decompressedContent; + // moderation states for the revision. This is technically denormalized data + // since it can be overwritten and does not provide a full history. + // The tricky part is updating moderation is a new revision for hide and + // delete, but adjusts an existing revision for full oversight. + protected $moderationState = self::MODERATED_NONE; + protected $moderationTimestamp; + protected $moderatedByUserId; + protected $moderatedByUserText; + static public function fromStorageRow( array $row, $obj = null ) { if ( $obj === null ) { $obj = new static; @@ -33,11 +72,17 @@ $obj->userText = $row['rev_user_text']; $obj->prevRevision = UUID::create( $row['rev_parent_id'] ); $obj->comment = $row['rev_comment']; - $obj->flags = array_filter( explode( ',', $row['rev_flags'] ) ); + $obj->flags = array_filter( explode( ',', $row['rev_flags'] ) ); $obj->content = $row['rev_content']; // null if external store is not being used $obj->contentUrl = $row['rev_content_url']; $obj->decompressedContent = null; + + $obj->moderationState = $row['rev_mod_state']; + $obj->moderatedByUserId = $row['rev_mod_user_id']; + $obj->moderatedByUserText = $row['rev_mod_user_text']; + $obj->moderationTimestamp = $row['rev_mod_timestamp']; + return $obj; } @@ -53,6 +98,11 @@ 'rev_content' => $obj->content, 'rev_content_url' => $obj->contentUrl, 'rev_flags' => implode( ',', $obj->flags ), + + 'rev_mod_state' => $obj->moderationState, + 'rev_mod_user_id' => $obj->moderatedByUserId, + 'rev_mod_user_text' => $obj->moderatedByUserText, + 'rev_mod_timestamp' => $obj->moderationTimestamp, ); } @@ -77,18 +127,91 @@ return $obj; } + public function moderate( User $user, $state ) { + $mostRestricted = max( $state, $this->moderationState ); + if ( !$this->isAllowed( $user, $mostRestricted ) ) { + return null; + } + // Oversight is special, other moderation types just create + // a new revision but oversighting adjusts the existing revision. + // Yes this mucks with the history just being a revision list. + if ( $state === self::MODERATED_OVERSIGHTED ) { + $obj = $this; + } else { + $obj = $this->newNullRevision( $user ); + } + + $obj->moderationState = $state; + if ( $state === self::MODERATED_NONE ) { + $obj->moderatedByUserId = null; + $obj->moderatedByUserText = null; + $obj->moderationTimestamp = null; + } else { + $obj->moderatedByUserId = $user->getId(); + $obj->moderatedByUserText = $user->getName(); + $obj->moderationTimestamp = wfTimestampNow(); + } + return $obj; + } + + public function restore( User $user ) { + return $this->moderate( $user, self::MODERATED_NONE ); + } + public function getRevisionId() { return $this->revId; } - public function getContent() { + // Is the user allowed to see this revision ? + protected function isAllowed( $user = null, $state = null ) { + if ( $state === null ) { + $state = $this->moderationState; + } + if ( !isset( self::$perms[$state] ) ) { + throw new \Exception( 'Unknown stored moderation state' ); + } + + $perm = self::$perms[$state]['perm']; + return $perm === null || ( $user && $user->isAllowed( $perm ) ); + } + + public function getContent( $user = null ) { + if ( $this->isAllowed( $user ) ) { + return $this->getContentRaw(); + } else { + $moderatedAt = new MWTimestamp( $this->moderationTimestamp ); + + return wfMessage( + self::$perms[$this->moderationState]['content'], + $this->moderatedByUserText, + $moderatedAt->getHumanTimestamp( new MWTimestamp ) + ); + } + } + + public function getContentRaw() { if ( $this->decompressedContent === null ) { $this->decompressedContent = \Revision::decompressRevisionText( $this->content, $this->flags ); } return $this->decompressedContent; } + public function getUserText( $user = null ) { + if ( $this->isAllowed( $user ) ) { + return $this->getUserTextRaw(); + } else { + return wfMessage( self::$perms[$this->moderationState]['usertext'] ); + } + } + + public function getUserTextRaw() { + return $this->userText; + } + protected function setContent( $content ) { + if ( $this->moderationState !== self::MODERATED_NONE ) { + throw new \Exception( 'Cannot change content of restricted revision' ); + } if ( $content !== $this->getContent() ) { $this->content = $this->decompressedContent = $content; $this->contentUrl = null; @@ -105,32 +228,34 @@ return $this->comment; } - public function addFlag( User $user, $flag, $comment ) { - if ( $this->isFlagged( $flag ) ) { - // already flagged - return $this; - } - if ( false !== strpos( ',', $flag ) ) { - throw new \MWException( 'Invalid flag name: contains comma' ); - } - $updated = $this->newNullRevision( $user ); - $updated->flags[] = $flag; - $updated->comment = $comment; - return $updated; + public function getModerationState() { + return $this->moderationState; } - public function removeFlag( User $user, $flag, $comment ) { - if ( !$this->isFlagged( $flag ) ) { - return $this; - } - $updated = $this->newNullRevision( $user ); - unset( $updated->flags[array_search( $flag, $updated->flags )] ); - $updated->comment = $comment; - return $updated; + public function getModerationTimestamp() { + return $this->moderationTimestamp; } - public function isFlagged( $flag ) { - return false !== array_search( $flag, $this->flags ); + /** + * @param string|array $flags + * @return boolean True when at least one flag in $flags is set + */ + public function isFlaggedAny( $flags ) { + foreach ( (array) $flags as $flag ) { + if ( false !== array_search( $flag, $this->flags ) ) { + return true; + } + } + return false; + } + + public function isFlaggedAll( $flags ) { + foreach ( (array) $flags as $flag ) { + if ( false === array_search( $flag, $this->flags ) ) { + return false; + } + } + return true; } } diff --git a/includes/Model/PostRevision.php b/includes/Model/PostRevision.php index e91da62..1d42315 100644 --- a/includes/Model/PostRevision.php +++ b/includes/Model/PostRevision.php @@ -83,10 +83,6 @@ return $this->postId; } - public function getUserText() { - return $this->userText; - } - public function isTopicTitle() { return $this->replyToId === null; } diff --git a/templates/post.html.php b/templates/post.html.php index 332e18e..e110546 100644 --- a/templates/post.html.php +++ b/templates/post.html.php @@ -48,47 +48,7 @@ ); }; -$renderPost = function( $post ) use( $self, $block ) { - echo $self->renderPost( $post, $block ); -}; - -echo Html::openElement( 'div', array( - 'class' => 'flow-post-container', - 'data-post-id' => $post->getRevisionId()->getHex(), -) ); - -$class = 'flow-post'; -$actions = array(); -$replyForm = ''; -if ( $post->isFlagged( 'deleted' ) ) { - $class .= ' flow-post-deleted'; -} - -echo Html::openElement( 'div', array( - 'class' => $class, - 'data-post-id' => $post->getPostId()->getHex(), - 'id' => 'flow-post-' . $post->getPostId()->getHex(), -) ); - -if ( $post->isFlagged( 'deleted' ) ) { - $content = wfMessage( 'flow-post-deleted' ); - $user = wfMessage( 'flow-post-deleted' ); - - // TODO make conditional on rights - $actions['restore'] = $postAction( 'restore-post', array( 'postId' => $post->getPostId()->getHex() ), 'mw-ui-constructive' ); - - $actions['history'] = Html::element( 'a', array( - 'href' => $self->generateUrl( $block->getWorkflowId(), 'post-history', array( - $block->getName() . '[postId]' => $post->getPostId()->getHex(), - ) ), - ), wfMessage( 'flow-post-action-history' )->plain() ); -} else { - $user = Html::element( 'span', null, $post->getUserText() ); - $content = $post->getContent(); - $actions['delete'] = $postAction( 'delete-post', array( 'postId' => $post->getPostId()->getHex() ), 'mw-ui-destructive' ); - $actions['history'] = $getAction( 'post-history' ); - $actions['permalink'] = $getAction( 'view' ); - $actions['edit-post'] = $getAction( 'edit-post' ); +$createReplyForm = function() use( $self, $block, $post, $editToken ) { $replyForm = Html::openElement( 'form', array( 'method' => 'POST', // root post id is same as topic workflow id @@ -102,7 +62,8 @@ $replyForm .= $error->text() . '<br>'; // the pain ... } } - $replyForm .= Html::element( 'input', array( + return $replyForm . + Html::element( 'input', array( 'type' => 'hidden', 'name' => $block->getName() . '[replyTo]', 'value' => $post->getPostId()->getHex(), @@ -125,53 +86,116 @@ ), wfMessage( 'flow-disclaimer' )->parse() ) . Html::closeElement( 'div' ) . '</form>'; +}; + +$class = 'flow-post'; +$content = $post->getContent(); +$userText = $post->getUserText(); +if ( !$userText instanceof \Message ) { + $userText = Html::element( 'span', null, $userText ); +} +$actions = array(); +$replyForm = ''; + +if ( $post->isFlaggedAny( 'oversighted', 'deleted', 'hidden' ) ) { + $class .= ' flow-post-deleted'; } -?> -<div class="flow-post-title"> - <div class="flow-post-authorline"> -<?php -echo $user; -?> - <span class="flow-datestamp"> - <span class="flow-agotime" style="display: inline"><timestamp></span> - <span class="flow-utctime" style="display: none"><timestamp></span> - </span> - </div> -</div> +// Build the actions for the post +switch( $post->getModerationState() ) { +case $post::MODERATED_NONE: + if ( $user->isAllowed( 'flow-hide' ) ) { + $actions['hide'] = $postAction( 'hide-post', array( 'postId' => $post->getPostId()->getHex() ), 'mw-ui-destructive' ); + } + if ( $user->isAllowed( 'flow-delete' ) ) { + $actions['delete'] = $postAction( 'delete-post', array( 'postId' => $post->getPostId()->getHex() ), 'mw-ui-destructive' ); + } + if ( $user->isAllowed( 'flow-oversight' ) ) { + $actions['oversight'] = $postAction( 'oversight-post', array( 'postId' => $post->getPostId()->getHex() ), 'mw-ui-destructive' ); + } + $actions['history'] = $getAction( 'post-history' ); + $actions['edit-post'] = $getAction( 'edit-post' ); + $replyForm = $createReplyForm(); + break; -<div class="flow-post-content"> -<?php -echo $content -?> -</div> -<div class="flow-post-controls"> - <div class="flow-post-actions"> - <a><?php echo wfMessage('flow-post-actions')->escaped(); ?></a> - <div class="flow-actionbox-pokey"> </div> - <div class="flow-post-actionbox"> - <ul> -<?php -foreach( $actions as $key => $action ) { - echo '<li class="flow-action-'.$key.'">' . $action . "</li>\n"; +case $post::MODERATED_HIDDEN: + if ( $user->isAllowed( 'flow-hide' ) ) { + $actions['restore'] = $postAction( 'restore-post', array( 'postId' => $post->getPostId()->getHex() ), 'mw-ui-constructive' ); + } + if ( $user->isAllowed( 'flow-delete' ) ) { + $actions['delete'] = $postAction( 'delete-post', array( 'postId' => $post->getPostId()->getHex() ), 'mw-ui-destructive' ); + } + if ( $user->isAllowed( 'flow-oversight' ) ) { + $actions['oversight'] = $postAction( 'oversight-post', array( 'postId' => $post->getPostId()->getHex() ), 'mw-ui-destructive' ); + } + $actions['history'] = $getAction( 'post-history' ); + break; + +case $post::MODERATED_DELETED: + if ( $user->isAllowedAny( 'flow-delete', 'flow-oversight' ) ) { + $actions['restore'] = $postAction( 'restore-post', array( 'postId' => $post->getPostId()->getHex() ), 'mw-ui-constructive' ); + } + $actions['history'] = $getAction( 'post-history' ); + break; + +case $post::MODERATED_OVERSIGHTED: + if ( !$user->isAllowed( 'flow-oversight' ) ) { + // no children, no nothing + return; + } + $actions['restore'] = $postAction( 'restore-post', array( 'postId' => $post->getPostId()->getHex() ), 'mw-ui-constructive' ); + $actions['history'] = $getAction( 'post-history' ); + break; } -?> - </ul> + +// Default always-available actions +$actions['permalink'] = $getAction( 'view' ); + +// The actual output +echo Html::openElement( 'div', array( + 'class' => 'flow-post-container', + 'data-post-id' => $post->getRevisionId()->getHex(), +) ); + echo Html::openElement( 'div', array( + 'class' => $class, + 'data-post-id' => $post->getPostId()->getHex(), + 'id' => 'flow-post-' . $post->getPostId()->getHex(), + ) ); ?> + <div class="flow-post-title"> + <div class="flow-post-authorline"> + <?php echo $userText; ?> + <span class="flow-datestamp"> + <span class="flow-agotime" style="display: inline"><timestamp></span> + <span class="flow-utctime" style="display: none"><timestamp></span> + </span> + </div> + </div> + + <div class="flow-post-content"> + <?php echo $content ?> + </div> + <div class="flow-post-controls"> + <div class="flow-post-actions"> + <a><?php echo wfMessage( 'flow-post-actions' )->escaped(); ?></a> + <div class="flow-actionbox-pokey"> </div> + <div class="flow-post-actionbox"> + <ul> + <?php + foreach( $actions as $key => $action ) { + echo '<li class="flow-action-'.$key.'">' . $action . "</li>\n"; + } + ?> + </ul> + </div> + </div> </div> </div> + <?php echo $replyForm; ?> + <div class='flow-post-replies'> + <?php + foreach( $post->getChildren() as $child ) { + echo $this->renderPost( $child, $block ); + } + ?> + </div> </div> -<?php - -echo '</div>'; - -echo $replyForm; - -echo Html::openElement( 'div', array( - 'class' => 'flow-post-replies', -) ); -foreach( $post->getChildren() as $child ) { - $renderPost( $child ); -} - -echo '</div>'; -echo '</div>'; \ No newline at end of file -- To view, visit https://gerrit.wikimedia.org/r/80303 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I39241b68fd56d74ffe5a5051777dcb6e09750d9d Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/extensions/Flow Gerrit-Branch: master Gerrit-Owner: EBernhardson (WMF) <ebernhard...@wikimedia.org> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits