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