Matthias Mullie has uploaded a new change for review.
https://gerrit.wikimedia.org/r/92346
Change subject: Flow moderation actions in logs
......................................................................
Flow moderation actions in logs
Meanwhile also:
* Centralized actions config into FlowActions.php (from PostActionPermissions &
PostActionMenu)
* Slightly changed closure code for PostActionMenu (change prompted by afore-
mentioned change)
* Fixed a moderation bug due to '' (moderation status) and 'moderate'
(moderation action)
* Removed validateRestoredPost (can just use validateModeratePost instead)
Change-Id: Ifb5a36d6915454b17ef42085fd7a3d4c37bbf36c
---
M Flow.i18n.php
M Flow.php
A FlowActions.php
M container.php
M includes/Block/Topic.php
A includes/Log/Formatter.php
A includes/Log/Logger.php
M includes/PostActionPermissions.php
M includes/View/PostActionMenu.php
9 files changed, 366 insertions(+), 116 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/mediawiki/extensions/Flow
refs/changes/46/92346/1
diff --git a/Flow.i18n.php b/Flow.i18n.php
index 9f52f6e..45cf8aa 100644
--- a/Flow.i18n.php
+++ b/Flow.i18n.php
@@ -11,6 +11,12 @@
'flow-desc' => 'Workflow management system',
'flow-specialpage' => '$1 – Flow',
+ 'log-name-flow' => 'Flow activity log',
+ 'logentry-delete-flow-delete-post' => '$1 {{GENDER:$2|deleted}} a <span
class="plainlinks">[$4 comment]</span> on [[$3]]',
+ 'logentry-delete-flow-restore-post' => '$1 {{GENDER:$2|restored}} a
<span class="plainlinks">[$4 comment]</span> on [[$3]]',
+ 'logentry-suppress-flow-censor-post' => '$1 {{GENDER:$2|suppressed}} a
<span class="plainlinks">[$4 comment]</span> on [[$3]]',
+ 'logentry-suppress-flow-restore-post' => '$1 {{GENDER:$2|deleted}} a
<span class="plainlinks">[$4 comment]</span> on [[$3]]',
+
'flow-user-anonymous' => 'Anonymous',
'flow-user-moderated' => 'Moderated user',
@@ -77,6 +83,7 @@
'flow-error-invalid-postId' => '"postId" parameter was invalid. The
specified post ($1) could not be found.',
'flow-error-restore-failure' => 'Restoration of this item failed.',
'flow-error-invalid-moderation-state' => 'An invalid value was provided
for moderationState',
+ 'flow-error-invalid-moderation-reason' => 'Please provide a reason for
the moderation',
'flow-error-not-allowed' => 'Insufficient permissions to execute this
action',
'flow-edit-header-submit' => 'Save header',
@@ -175,6 +182,36 @@
'flow-desc' =>
'{{desc|name=Flow|url=http://www.mediawiki.org/wiki/Extension:Flow}}',
'flow-specialpage' => 'Used as page title in [[Special:Flow]].
Parameters:
* $1 - page title',
+ 'log-name-flow' => '{{doc-logpage}}
+Name of the Flow log filter on the [[Special:Log]] page.',
+ 'logentry-delete-flow-delete-post' => 'Text for a deletion log entry
when a post was deleted.
+
+Parameters:
+* $1: The user: link to the user page.
+* $2: The username. Can be used for GENDER.
+* $3: The page where the post was moderated.
+* $4: Permalink url to the moderated post.',
+ 'logentry-delete-flow-restore-post' => 'Text for a deletion log entry
when a deleted post was restored.
+
+Parameters:
+* $1: The user: link to the user page.
+* $2: The username. Can be used for GENDER.
+* $3: The page where the post was moderated.
+* $4: Permalink url to the moderated post.',
+ 'logentry-suppress-flow-censor-post' => 'Text for a deletion log entry
when a post was suppressed.
+
+Parameters:
+* $1: The user: link to the user page.
+* $2: The username. Can be used for GENDER.
+* $3: The page where the post was moderated.
+* $4: Permalink url to the moderated post.',
+ 'logentry-suppress-flow-restore-post' => 'Text for a deletion log entry
when a suppressed post was restored.
+
+Parameters:
+* $1: The user: link to the user page.
+* $2: The username. Can be used for GENDER.
+* $3: The page where the post was moderated.
+* $4: Permalink url to the moderated post.',
'flow-user-anonymous' => 'Name to display for anonymous users.
{{Identical|Anonymous}}',
'flow-user-moderated' => 'Name to display instead of a moderated user
name',
@@ -320,7 +357,8 @@
Usually indicates a code bug, so technical terminology is okay.
-Valid values for moderationState are: none, hidden, deleted, censored',
+Valid values for moderationState are: (none), hidden, deleted, censored',
+ 'flow-error-invalid-moderation-reason' => 'Used as error message when
no reason is given for the moderation of a post.',
'flow-error-not-allowed' => 'Insufficient permissions to execute this
action',
'flow-edit-header-submit' => 'Used as label for the Submit button.',
'flow-edit-title-submit' => 'Used as label for the Submit button.',
diff --git a/Flow.php b/Flow.php
index eab4dbe..87a8ec4 100755
--- a/Flow.php
+++ b/Flow.php
@@ -73,6 +73,8 @@
$wgAutoloadClasses['Flow\NotificationFormatter'] = $dir .
'includes/Notifications/Formatter.php';
$wgAutoloadClasses['Flow\NotificationController'] = $dir .
'includes/Notifications/Controller.php';
$wgAutoloadClasses['Flow\PostActionPermissions'] = $dir .
'includes/PostActionPermissions.php';
+$wgAutoloadClasses['Flow\Log\Logger'] = $dir . 'includes/Log/Logger.php';
+$wgAutoloadClasses['Flow\Log\Formatter'] = $dir . 'includes/Log/Formatter.php';
// Classes that model our data
$wgAutoloadClasses['Flow\Model\Definition'] = $dir .
'includes/Model/Definition.php';
@@ -220,3 +222,21 @@
// Namespaces to occupy is an array of NS_* constants, e.g. array(
NS_USER_TALK ).
$wgFlowOccupyNamespaces = array();
+
+// Action details config file
+require $dir . 'FlowActions.php';
+
+// Register activity log formatter hooks
+foreach( $wgFlowActions as $action => $options ) {
+ if ( isset( $options['log_type'] ) ) {
+ $log = $options['log_type'];
+
+ // Some actions are more complex closures - to be added
manually.
+ if ( is_string( $log ) ) {
+ $wgLogActionsHandlers["$log/flow-$action"] =
'Flow\Log\Formatter';
+ }
+ }
+}
+// Manually add that more complex actions
+$wgLogActionsHandlers['delete/flow-restore-post'] = 'Flow\Log\Formatter';
+$wgLogActionsHandlers['suppress/flow-restore-post'] = 'Flow\Log\Formatter';
diff --git a/FlowActions.php b/FlowActions.php
new file mode 100644
index 0000000..5b57801
--- /dev/null
+++ b/FlowActions.php
@@ -0,0 +1,103 @@
+<?php
+
+use Flow\Model\PostRevision;
+use Flow\PostActionPermissions;
+use Flow\Log\Logger;
+
+/**
+ * Flow actions: key => value map with key being the action name.
+ * The value consists of an array of these below keys (and appropriate values):
+ * * log_type: the Special:Log filter to save actions to.
+ * * permissions: array of permissions, where each key is the existing post
+ * state and value is the action required to execute the action.
+ * * button-method: used in PostActionMenu, to generate GET (a) or POST (form)
+ * links for the action.
+ */
+$wgFlowActions = array(
+ 'hide-post' => array(
+ 'log_type' => false,
+ 'permissions' => 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',
+ ),
+ 'button-method' => 'POST',
+ ),
+ 'delete-post' => array(
+ 'log_type' => 'delete',
+ 'permissions' => array(
+ PostRevision::MODERATED_NONE => 'flow-delete',
+ PostRevision::MODERATED_HIDDEN => 'flow-delete',
+ ),
+ 'button-method' => 'POST',
+ ),
+ 'censor-post' => array(
+ 'log_type' => 'suppress',
+ 'permissions' => array(
+ PostRevision::MODERATED_NONE => 'flow-censor',
+ PostRevision::MODERATED_HIDDEN => 'flow-censor',
+ PostRevision::MODERATED_DELETED => 'flow-censor',
+ ),
+ 'button-method' => 'POST',
+ ),
+ 'restore-post' => array(
+ 'log_type' => function( PostRevision $post, Logger $logger ) {
+ // Kind of log depends on the previous change type:
+ // * if post was deleted, restore should go to deletion
log
+ // * if post was suppressed, restore should go to
suppression log
+ global $wgFlowActions;
+ return $wgFlowActions[$post->getModerationState() .
'-post']['log_type'];
+ },
+ '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',
+ ),
+ 'button-method' => 'POST',
+ ),
+ 'post-history' => array(
+ 'log_type' => false,
+ 'permissions' => array(
+ PostRevision::MODERATED_NONE => '',
+ PostRevision::MODERATED_HIDDEN => '',
+ PostRevision::MODERATED_DELETED => '',
+ PostRevision::MODERATED_CENSORED => 'flow-censor',
+ ),
+ 'button-method' => 'GET',
+ ),
+ 'edit-post' => array(
+ 'log_type' => false,
+ 'permissions' => array(
+ // no permissions needed for own posts
+ PostRevision::MODERATED_NONE => function( PostRevision
$post, PostActionPermissions $permissions ) {
+ return $post->isCreator( $permissions->user ) ?
'' : 'flow-edit-post';
+ }
+ ),
+ 'button-method' => 'GET',
+ ),
+ 'view' => array(
+ 'log_type' => false, // don't log views
+ 'permissions' => array(
+ PostRevision::MODERATED_NONE => '',
+ PostRevision::MODERATED_HIDDEN => array( 'flow-hide',
'flow-delete', 'flow-censor' ),
+ PostRevision::MODERATED_DELETED => array(
'flow-delete', 'flow-censor' ),
+ PostRevision::MODERATED_CENSORED => 'flow-censor',
+ ),
+ 'button-method' => 'GET',
+ ),
+ 'reply' => array(
+ 'log_type' => false,
+ 'permissions' => array(
+ PostRevision::MODERATED_NONE => '',
+ ),
+ 'button-method' => 'GET',
+ ),
+ 'edit-title' => array(
+ 'log_type' => false,
+ 'permissions' => array(
+ PostRevision::MODERATED_NONE => '',
+ ),
+ 'button-method' => 'GET',
+ ),
+);
diff --git a/container.php b/container.php
index 1681fc3..b430170 100644
--- a/container.php
+++ b/container.php
@@ -328,4 +328,11 @@
);
} );
+$c['logger'] = $c->share( function( $c ) {
+ return new Flow\Log\Logger(
+ $c['url_generator'],
+ $c['user']
+ );
+} );
+
return $c;
diff --git a/includes/Block/Topic.php b/includes/Block/Topic.php
index f4ea2db..3e9ed85 100644
--- a/includes/Block/Topic.php
+++ b/includes/Block/Topic.php
@@ -12,6 +12,7 @@
use Flow\NotificationController;
use Flow\PostActionPermissions;
use Flow\Templating;
+use Flow\Container;
use EchoEvent;
use User;
@@ -91,7 +92,7 @@
break;
case 'restore-post':
- $this->validateRestorePost();
+ $this->validateModeratePost( 'restore' );
break;
case 'edit-post':
@@ -185,44 +186,43 @@
return;
}
- if ( ! $moderationState ) {
+ $newState = $moderationState;
+ if ( !$moderationState ) {
$this->errors['moderate-post'] = wfMessage(
'flow-error-invalid-moderation-state' );
return;
} elseif ( $moderationState === 'restore' ) {
- $moderationState = '';
+ $newState = '';
}
- if ( ! $post->isValidModerationState( $moderationState ) ) {
+ if ( !$post->isValidModerationState( $newState ) ) {
$this->errors['moderate-post'] = wfMessage(
'flow-error-invalid-moderation-state' );
return;
- } elseif ( !$this->permissions->isAllowed( $post,
"{$moderationState}-post" ) ) {
+ } elseif ( !$this->permissions->isAllowed( $post,
"$moderationState-post" ) ) {
$this->errors['permissions'] = wfMessage(
'flow-error-not-allowed' );
return;
}
- $this->newRevision = $post->moderate( $this->user,
$moderationState );
+ if ( empty( $this->submitted['postId'] ) ) {
+ $this->errors['moderate-post'] = wfMessage(
'flow-error-invalid-moderation-reason' );
+ return;
+ }
+
+ $this->newRevision = $post->moderate( $this->user, $newState );
if ( !$this->newRevision ) {
$this->errors['moderate'] = wfMessage(
'flow-error-not-allowed' );
- }
- }
-
- protected function validateRestorePost() {
- if ( empty( $this->submitted['postId'] ) ) {
- $this->errors['restore-post'] = wfMessage(
'flow-error-missing-postId' );
- return;
- }
- $post = $this->loadRequestedPost( $this->submitted['postId'] );
- if ( !$post ) {
- $this->errors['restore-post'] = wfMessage(
'flow-error-invalid-postId' );
- return;
- } elseif ( !$this->permissions->isAllowed( $post,
"restore-post" ) ) {
- $this->errors['permissions'] = wfMessage(
'flow-error-not-allowed' );
- return;
- }
-
- $this->newRevision = $post->restore( $this->user );
- if ( !$this->newRevision ) {
- $this->errors['restore-post'] = wfMessage(
'flow-error-not-allowed' );
+ } else {
+ $logger = Container::get( 'logger' );
+ if ( $logger->canLog( $post, "$moderationState-post" )
) {
+ $logger->log(
+ $post,
+ "$moderationState-post",
+ $this->submitted['reason'],
+ $this->getWorkflow(),
+ array(
+ $this->getName() . '[postId]'
=> $post->getPostId()->getHex(),
+ )
+ );
+ }
}
}
diff --git a/includes/Log/Formatter.php b/includes/Log/Formatter.php
new file mode 100644
index 0000000..c24cb00
--- /dev/null
+++ b/includes/Log/Formatter.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace Flow\Log;
+
+use Message;
+
+class Formatter extends \LogFormatter {
+ /**
+ * Formats an activity log entry.
+ *
+ * @return string The log entry
+ */
+ protected function getActionMessage() {
+ global $wgLang, $wgContLang;
+
+ $type = $this->entry->getType();
+ $action = $this->entry->getSubtype();
+ $title = $this->entry->getTarget();
+ $skin = $this->plaintext ? null : $this->context->getSkin();
+ $params = $this->entry->getParameters();
+
+ // Give grep a chance to find the usages:
+ // logentry-delete-flow-delete-post,
logentry-delete-flow-restore-post,
+ // logentry-suppress-flow-restore-post,
logentry-suppress-flow-censor-post,
+ $language = $skin === null ? $wgContLang : $wgLang;
+ return wfMessage( "logentry-$type-$action" )
+ ->params( array(
+ Message::rawParam( $this->getPerformerElement()
),
+ $this->entry->getPerformer()->getId(),
+ $title,
+ $title->getFullUrl( $params ),
+ ) )
+ ->inLanguage( $language )
+ ->parse();
+ }
+
+ /**
+ * The native LogFormatter::getActionText provides no clean way of
handling
+ * the Flow action text in a plain text format (e.g. as used by
CheckUser)
+ *
+ * @return string
+ */
+ public function getActionText() {
+ $text = $this->getActionMessage();
+ return $this->plaintext ? strip_tags( $text ) : $text;
+ }
+}
diff --git a/includes/Log/Logger.php b/includes/Log/Logger.php
new file mode 100644
index 0000000..bd52bd6
--- /dev/null
+++ b/includes/Log/Logger.php
@@ -0,0 +1,96 @@
+<?php
+
+namespace Flow\Log;
+
+use Flow\Model\PostRevision;
+use Flow\Model\Workflow;
+use Flow\UrlGenerator;
+use ManualLogEntry;
+use User;
+use Closure;
+
+class Logger {
+ /**
+ * @var UrlGenerator
+ */
+ public $urlGenerator;
+
+ /**
+ * @var User
+ */
+ public $user;
+
+ /**
+ * @param User $user
+ */
+ public function __construct( UrlGenerator $urlGenerator, User $user ) {
+ $this->urlGenerator = $urlGenerator;
+ $this->user = $user;
+ }
+
+ /**
+ * Check if an action should be logged (= if a log_type is set)
+ *
+ * @param PostRevision $post
+ * @param string $action
+ * @return bool
+ */
+ public function canLog( PostRevision $post, $action ) {
+ return (bool) $this->getLogType( $post, $action );
+ }
+
+ /**
+ * Adds an activity item to the log under the flow|suppress.
+ *
+ * @param PostRevision $post
+ * @param string $action The action we'll be logging
+ * @param string $reason Comment, reason for the moderation
+ * @param Workflow $workflow Workflow being worked on
+ * @param array $params Additional parameters to be saved
+ * @return int The id of the newly inserted log entry
+ */
+ public function log( PostRevision $post, $action, $reason, Workflow
$workflow, $params = array() ) {
+ wfProfileIn( __METHOD__ );
+
+ if ( !$this->canLog( $post, $action ) ) {
+ wfProfileOut( __METHOD__ );
+ return null;
+ }
+
+ $logType = $this->getLogType( $post, $action );
+
+ list( $title, $query ) = $this->urlGenerator->generateUrlData(
+ $workflow,
+ 'view',
+ $params
+ );
+
+ // insert logging entry
+ $logEntry = new ManualLogEntry( $logType, "flow-$action" );
+ $logEntry->setTarget( $title );
+ $logEntry->setPerformer( $this->user );
+ $logEntry->setParameters( $params );
+ $logEntry->setComment( $reason );
+ $logId = $logEntry->insert();
+ $logEntry->publish( $logId );
+
+ wfProfileOut( __METHOD__ );
+ return $logId;
+ }
+
+ /**
+ * @param PostRevision $post
+ * @param string $action
+ * @return string
+ */
+ public function getLogType( PostRevision $post, $action ) {
+ global $wgFlowActions;
+
+ $logType = $wgFlowActions[$action]['log_type'];
+ if ( $logType instanceof Closure) {
+ $logType = $logType( $post, $this );
+ }
+
+ return $logType;
+ }
+}
diff --git a/includes/PostActionPermissions.php
b/includes/PostActionPermissions.php
index 8eaee9a..74f435d 100644
--- a/includes/PostActionPermissions.php
+++ b/includes/PostActionPermissions.php
@@ -4,71 +4,29 @@
use Flow\Model\PostRevision;
use Closure;
+use User;
/**
* role based security for posts based on moderation state
*/
class PostActionPermissions {
+ /**
+ * @var array
+ */
+ protected $actions = array();
+
+ /**
+ * @var User
+ */
+ public $user;
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->isCreator( $user ) ? '' : 'flow-edit-post',
- );
- },
-
- 'view' => array(
- PostRevision::MODERATED_NONE => '',
- PostRevision::MODERATED_HIDDEN => array(
'flow-hide', 'flow-delete', 'flow-censor' ),
- PostRevision::MODERATED_DELETED => array(
'flow-delete', 'flow-censor' ),
- PostRevision::MODERATED_CENSORED =>
'flow-censor',
- ),
-
- 'reply' => array(
- PostRevision::MODERATED_NONE => '',
- ),
-
- 'edit-title' => array(
- PostRevision::MODERATED_NONE => '',
- ),
-
-
- );
+ global $wgFlowActions;
+ $this->actions = array_map( function( $action ) {
+ return $action['permissions'];
+ }, $wgFlowActions );
}
/**
@@ -98,10 +56,8 @@
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
@@ -109,10 +65,18 @@
return false;
}
+ // Some permissions may be more complex to be defined as simple
array
+ // values, in which case they're a Closure (which will accept
+ // PostRevision & PostActionPermissions as arguments)
+ $permission = $permissions[$state];
+ if ( $permission instanceof Closure ) {
+ $permission = $permission( $post, $this );
+ }
+
// check if user is allowed to perform action
$res = call_user_func_array(
array( $this->user, 'isAllowedAny' ),
- (array) $permissions[$state]
+ (array) $permission
);
return $res;
}
@@ -143,4 +107,3 @@
return $allowed;
}
}
-
diff --git a/includes/View/PostActionMenu.php b/includes/View/PostActionMenu.php
index 745c0ba..bfd9913 100644
--- a/includes/View/PostActionMenu.php
+++ b/includes/View/PostActionMenu.php
@@ -30,32 +30,9 @@
* @param string $action
* @return array|bool Array of action details or false if invalid
*/
- protected function getActionDetails( $action ) {
- $actions = array(
- 'hide-post' => array(
- 'method' => 'POST',
- ),
- 'delete-post' => array(
- 'method' => 'POST',
- ),
- 'censor-post' => array(
- 'method' => 'POST',
- ),
- 'restore-post' => array(
- 'method' => 'POST',
- ),
- 'post-history' => array(
- 'method' => 'GET',
- ),
- 'edit-post' => array(
- 'method' => 'GET',
- ),
- 'view' => array(
- 'method' => 'GET',
- ),
- );
-
- return isset( $actions[$action] ) ? $actions[$action] : false;
+ protected function getMethod( $action ) {
+ global $wgFlowActions;
+ return isset( $wgFlowActions[$action]['method'] ) ?
$wgFlowActions[$action]['method'] : false;
}
/**
@@ -71,8 +48,7 @@
return false;
}
$data = array( $this->block->getName() . '[postId]' =>
$this->post->getPostId()->getHex() );
- $details = $this->getActionDetails( $action );
- if ( $details['method'] === 'POST' ) {
+ if ( $this->getMethod( $action ) === 'POST' ) {
return $this->postAction( $action, $data, $content,
$class );
} else {
return $this->getAction( $action, $data, $content,
$class );
--
To view, visit https://gerrit.wikimedia.org/r/92346
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: Ifb5a36d6915454b17ef42085fd7a3d4c37bbf36c
Gerrit-PatchSet: 1
Gerrit-Project: mediawiki/extensions/Flow
Gerrit-Branch: master
Gerrit-Owner: Matthias Mullie <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits