Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 69
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
ActionFormatter
0.00% covered (danger)
0.00%
0 / 69
0.00% covered (danger)
0.00%
0 / 4
240
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 getActionMessage
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 1
42
 getActionText
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 getRoot
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3namespace Flow\Log;
4
5use Flow\Collection\PostCollection;
6use Flow\Container;
7use Flow\Conversion\Utils;
8use Flow\Data\ManagerGroup;
9use Flow\Model\UUID;
10use Flow\Repository\TreeRepository;
11use Flow\RevisionActionPermissions;
12use Flow\Templating;
13use Flow\UrlGenerator;
14use MediaWiki\Html\Html;
15use MediaWiki\Logging\LogEntry;
16use MediaWiki\Logging\LogFormatter;
17use MediaWiki\Logging\LogPage;
18use MediaWiki\Message\Message;
19
20class ActionFormatter extends LogFormatter {
21    /**
22     * @var UUID[]
23     */
24    private static $uuids = [];
25
26    /**
27     * @var RevisionActionPermissions
28     */
29    protected $permissions;
30
31    /**
32     * @var Templating
33     */
34    protected $templating;
35
36    public function __construct( LogEntry $entry ) {
37        parent::__construct( $entry );
38
39        $this->permissions = Container::get( 'permissions' );
40        $this->templating = Container::get( 'templating' );
41
42        $params = $this->entry->getParameters();
43        // serialized topicId or postId can be stored
44        foreach ( $params as $value ) {
45            if ( $value instanceof UUID ) {
46                static::$uuids[$value->getAlphadecimal()] = $value;
47            }
48        }
49    }
50
51    /**
52     * Formats an activity log entry.
53     *
54     * @return string The log entry
55     */
56    protected function getActionMessage() {
57        /*
58         * At this point, all log entries will already have been created & we've
59         * gathered all uuids in constructor: we can now batch-load all of them.
60         * We won't directly be using that batch-loaded data (nothing will even
61         * be returned) but it'll ensure that everything we need will be
62         * retrieved from cache/storage efficiently & waiting in memory for when
63         * we request it again.
64         */
65        static $loaded = false;
66        if ( !$loaded ) {
67            /** @var ManagerGroup $storage */
68            $storage = Container::get( 'storage' );
69            /** @var TreeRepository $treeRepository */
70            $treeRepository = Container::get( 'repository.tree' );
71
72            $query = new LogQuery( $storage, $treeRepository );
73            $query->loadMetadataBatch( static::$uuids );
74            $loaded = true;
75        }
76
77        $root = $this->getRoot();
78        if ( !$root ) {
79            // failed to load required data
80            return '';
81        }
82
83        $type = $this->entry->getType();
84        $action = $this->entry->getSubtype();
85        $title = $this->entry->getTarget();
86        $params = $this->entry->getParameters();
87
88        if ( isset( $params['postId'] ) ) {
89            /** @var UrlGenerator $urlGenerator */
90            $urlGenerator = Container::get( 'url_generator' );
91
92            // generate link that highlights the post
93            $anchor = $urlGenerator->postLink(
94                $title,
95                UUID::create( $params['topicId'] ),
96                UUID::create( $params['postId'] )
97            );
98            $title = $anchor->resolveTitle();
99        }
100
101        $rootLastRevision = $root->getLastRevision();
102
103        // Give grep a chance to find the usages:
104        // A few of the -topic-title-not-visible are not reachable with the current
105        // config (since people looking at the suppression log can see suppressed
106        // content), but are included to make it less brittle.
107        // logentry-delete-flow-delete-post, logentry-delete-flow-delete-post-topic-title-not-visible,
108        // logentry-delete-flow-restore-post, logentry-delete-flow-restore-post-topic-title-not-visible,
109        // logentry-suppress-flow-restore-post, logentry-suppress-flow-restore-post-topic-title-not-visible,
110        // logentry-suppress-flow-suppress-post, logentry-suppress-flow-suppress-post-topic-title-not-visible,
111        // logentry-delete-flow-delete-topic, logentry-delete-flow-delete-topic-topic-title-not-visible,
112        // logentry-delete-flow-restore-topic, logentry-delete-flow-restore-topic-topic-title-not-visible,
113        // logentry-suppress-flow-restore-topic, logentry-suppress-flow-restore-topic-topic-title-not-visible,
114        // logentry-suppress-flow-suppress-topic, logentry-suppress-flow-suppress-topic-topic-title-not-visible,
115        // logentry-lock-flow-lock-topic, logentry-lock-flow-lock-topic-topic-title-not-visible
116        // logentry-lock-flow-restore-topic, logentry-lock-flow-restore-topic-topic-title-not-visible,
117        $messageKey = "logentry-$type-$action";
118        $isTopicTitleVisible = $this->permissions->isAllowed( $rootLastRevision, 'view-topic-title' );
119
120        if ( !$isTopicTitleVisible ) {
121            $messageKey .= '-topic-title-not-visible';
122        }
123
124        $message = $this->msg( $messageKey )
125            ->params( [
126                Message::rawParam( $this->getPerformerElement() ),
127                $this->entry->getPerformerIdentity()->getName(),
128            ] );
129
130        if ( $isTopicTitleVisible ) {
131            $message->params( [
132                $title, // Title of topic
133                $title->getFullURL(), // Full URL of topic, with highlighted post if applicable
134            ] );
135
136            $message->plaintextParams( $this->templating->getContent( $rootLastRevision, 'topic-title-plaintext' ) );
137        }
138
139        $message->params( $root->getWorkflow()->getOwnerTitle() ); // board title object
140
141        $message->parse();
142
143        return Html::rawElement(
144            'span',
145            [ 'class' => 'plainlinks' ],
146            $message->parse()
147        );
148    }
149
150    /**
151     * The native LogFormatter::getActionText provides no clean way of handling
152     * the Flow action text in a plain text format (e.g. as used by CheckUser)
153     *
154     * @return string
155     */
156    public function getActionText() {
157        if ( $this->canView( LogPage::DELETED_ACTION ) ) {
158            $text = $this->getActionMessage();
159            return $this->plaintext ? Utils::htmlToPlaintext( $text ) : $text;
160        } else {
161            return parent::getActionText();
162        }
163    }
164
165    /**
166     * @return PostCollection|bool
167     */
168    protected function getRoot() {
169        $params = $this->entry->getParameters();
170
171        try {
172            if ( !isset( $params['topicId'] ) ) {
173                // failed finding the expected data in storage
174                wfWarn( __METHOD__ . ': Failed to locate topicId in log_params for: ' . serialize( $params ) .
175                    ' (forgot to run FlowFixLog.php?)' );
176                return false;
177            }
178
179            $uuid = UUID::create( $params['topicId'] );
180            $collection = PostCollection::newFromId( $uuid );
181
182            // see if this post is valid
183            $collection->getLastRevision();
184            return $collection;
185        } catch ( \Exception ) {
186            // failed finding the expected data in storage
187            wfWarn( __METHOD__ . ': Failed to locate root for: ' . serialize( $params ) .
188                ' (potentially storage issue)' );
189            return false;
190        }
191    }
192}