Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 100
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
ChangesListFormatter
0.00% covered (danger)
0.00%
0 / 100
0.00% covered (danger)
0.00%
0 / 8
702
0.00% covered (danger)
0.00%
0 / 1
 getHistoryType
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 format
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
20
 getEditSummary
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
6
 getTitleLink
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
42
 getTimestampLink
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getLogTextLinks
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
56
 getFlags
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 formatFlags
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace Flow\Formatter;
4
5use Flow\Conversion\Utils;
6use Flow\Data\Listener\RecentChangesListener;
7use Flow\Exception\FlowException;
8use Flow\Exception\PermissionException;
9use Flow\Model\Anchor;
10use Flow\Model\UUID;
11use MediaWiki\Context\IContextSource;
12use MediaWiki\MediaWikiServices;
13use MediaWiki\RecentChanges\ChangesList;
14use MediaWiki\RecentChanges\RCCacheEntry;
15
16class ChangesListFormatter extends AbstractFormatter {
17    protected function getHistoryType() {
18        return 'recentchanges';
19    }
20
21    /**
22     * @param RecentChangesRow $row
23     * @param IContextSource $ctx
24     * @param bool $linkOnly
25     * @return string|false Output line, or false on failure
26     * @throws FlowException
27     */
28    public function format( RecentChangesRow $row, IContextSource $ctx, $linkOnly = false ) {
29        $this->serializer->setIncludeHistoryProperties( true );
30        $this->serializer->setIncludeContent( false );
31
32        $data = $this->serializer->formatApi( $row, $ctx, 'recentchanges' );
33        if ( !$data ) {
34            return false;
35        }
36
37        if ( $linkOnly ) {
38            return $this->getTitleLink( $data, $row, $ctx );
39        }
40
41        // The ' . . ' text between elements
42        $separator = $this->changeSeparator();
43
44        $links = [];
45        $links[] = $this->getDiffAnchor( $data['links'], $ctx );
46        $links[] = $this->getHistAnchor( $data['links'], $ctx );
47
48        $description = $this->formatDescription( $data, $ctx );
49
50        $flags = $this->getFlags( $row, $ctx );
51
52        return $this->formatAnchorsAsPipeList( $links, $ctx ) .
53            $separator .
54            $this->formatFlags( $flags ) .
55            $this->getTitleLink( $data, $row, $ctx ) .
56            $ctx->msg( 'semicolon-separator' )->escaped() .
57            ' ' .
58            $this->formatTimestamp( $data, 'time' ) .
59            $separator .
60            ChangesList::showCharacterDifference(
61                $data['size']['old'],
62                $data['size']['new'],
63                $ctx
64            ) .
65            ( Utils::htmlToPlaintext( $description ) ? $separator . $description : '' ) .
66            $this->getEditSummary( $row, $ctx, $data );
67    }
68
69    /**
70     * @param RecentChangesRow $row
71     * @param IContextSource $ctx
72     * @param array $data
73     * @return string
74     */
75    public function getEditSummary( RecentChangesRow $row, IContextSource $ctx, array $data ) {
76        // Build description message, piggybacking on history i18n
77        $changeType = $data['changeType'];
78        $actions = $this->permissions->getActions();
79
80        $key = $actions->getValue( $changeType, 'history', 'i18n-message' );
81        // Find specialized message for summary
82        // i18n messages: flow-rev-message-new-post-recentchanges-summary,
83        // flow-rev-message-edit-post-recentchanges-summary
84        $msg = $ctx->msg( $key . '-' . $this->getHistoryType() . '-summary' );
85        if ( !$msg->exists() ) {
86            // No summary for this action
87            return '';
88        }
89
90        $msg = $msg->params( $this->getDescriptionParams( $data, $actions, $changeType ) );
91
92        // Below code is inspired by MediaWiki\CommentFormatter\CommentParser::doSectionLinks
93        $prefix = $ctx->msg( 'autocomment-prefix' )->inContentLanguage()->escaped();
94        $link = MediaWikiServices::getInstance()->getLinkRenderer()->makeLink(
95            $row->workflow->getOwnerTitle(),
96            $ctx->getLanguage()->getArrow( 'backwards' )
97        );
98        $summary = '<span class="autocomment">' . $msg->text() . '</span>';
99
100        // '(' + '' + '←' + summary + ')'
101        $text = MediaWikiServices::getInstance()->getCommentFormatter()
102            ->formatBlock( $prefix . $link . $summary );
103
104        // MediaWiki\CommentFormatter\CommentFormatter::formatBlock escaped everything, but what we built was safe
105        // and should not be escaped so let's go back to decoded entities
106        return htmlspecialchars_decode( $text );
107    }
108
109    /**
110     * This overrides the default title link to include highlights for the posts
111     * that have not yet been seen.
112     *
113     * @param array $data
114     * @param FormatterRow $row
115     * @param IContextSource $ctx
116     * @return string
117     */
118    protected function getTitleLink( array $data, FormatterRow $row, IContextSource $ctx ) {
119        if ( !$row instanceof RecentChangesRow ) {
120            // actually, this should be typehint, but can't because this needs
121            // to match the parent's more generic typehint
122            return parent::getTitleLink( $data, $row, $ctx );
123        }
124
125        if ( !isset( $data['links']['topic'] ) || !$data['links']['topic'] instanceof Anchor ) {
126            // no valid title anchor (probably header entry)
127            return parent::getTitleLink( $data, $row, $ctx );
128        }
129
130        $watched = $row->recentChange->getAttribute( 'wl_notificationtimestamp' );
131        if ( is_bool( $watched ) ) {
132            // RC & watchlist share most code; the latter is unaware of when
133            // something was watched though, so we'll ignore that here
134            return parent::getTitleLink( $data, $row, $ctx );
135        }
136
137        if ( $watched === null ) {
138            // there is no data for unread posts - they've all been seen
139            return parent::getTitleLink( $data, $row, $ctx );
140        }
141
142        // get comparison UUID corresponding to this last watched timestamp
143        $uuid = UUID::getComparisonUUID( $watched );
144
145        // add highlight details to anchor
146        /** @var Anchor $anchor */
147        $anchor = clone $data['links']['topic'];
148        $anchor->query['fromnotif'] = '1';
149        $anchor->fragment = '#flow-post-' . $uuid->getAlphadecimal();
150        $data['links']['topic'] = $anchor;
151
152        // now pass it on to parent with the new, updated, link ;)
153        return parent::getTitleLink( $data, $row, $ctx );
154    }
155
156    /**
157     * @param RecentChangesRow $row
158     * @param IContextSource $ctx
159     * @return string
160     * @throws PermissionException
161     */
162    public function getTimestampLink( $row, $ctx ) {
163        $this->serializer->setIncludeHistoryProperties( true );
164        $this->serializer->setIncludeContent( false );
165
166        $data = $this->serializer->formatApi( $row, $ctx, 'recentchanges' );
167        if ( $data === false ) {
168            throw new PermissionException( 'Insufficient permissions for ' . $row->revision->getRevisionId()->getAlphadecimal() );
169        }
170
171        return $this->formatTimestamp( $data, 'time' );
172    }
173
174    /**
175     * @param RecentChangesRow $row
176     * @param IContextSource $ctx
177     * @param RCCacheEntry[] $block
178     * @param array $links
179     * @return array|false Links array, or false on failure
180     * @throws FlowException
181     * @throws \Flow\Exception\InvalidInputException
182     */
183    public function getLogTextLinks( RecentChangesRow $row, IContextSource $ctx, array $block, array $links = [] ) {
184        $data = $this->serializer->formatApi( $row, $ctx, 'recentchanges' );
185        if ( !$data ) {
186            return false;
187        }
188
189        // Find the last (oldest) row in $block that is a Flow row. Note that there can be non-Flow
190        // things in $block (T228290).
191        $flowRows = array_filter( $block, static function ( $blockRow ) {
192            $source = $blockRow->getAttribute( 'rc_source' );
193            return $source === RecentChangesListener::SRC_FLOW ||
194                ( $source === null && $blockRow->getAttribute( 'rc_type' ) === RC_FLOW );
195        } );
196        $oldestRow = end( $flowRows ) ?? $row->recentChange;
197
198        $old = unserialize( $oldestRow->getAttribute( 'rc_params' ) );
199        $oldId = $old ? UUID::create( $old['flow-workflow-change']['revision'] ) : $row->revision->getRevisionId();
200
201        if ( isset( $data['links']['topic'] ) ) {
202            // add highlight details to anchor
203            // FIXME: This doesn't work well if the different rows in $block are for different topics
204            /** @var Anchor $anchor */
205            $anchor = clone $data['links']['topic'];
206            $anchor->query['fromnotif'] = '1';
207            $anchor->fragment = '#flow-post-' . $oldId->getAlphadecimal();
208        } elseif ( isset( $data['links']['workflow'] ) ) {
209            $anchor = $data['links']['workflow'];
210        } else {
211            // this will be caught and logged by the RC hook, it will not fatal the page.
212            throw new FlowException( "No anchor available for revision $oldId" );
213        }
214
215        $changes = count( $block );
216        // link text: "n changes"
217        $text = $ctx->msg( 'nchanges' )->numParams( $changes )->escaped();
218
219        // override total changes link
220        $links['total-changes'] = $anchor->toHtml( $text );
221
222        return $links;
223    }
224
225    /**
226     * @param RecentChangesRow $row
227     * @param IContextSource $ctx
228     * @return array
229     */
230    public function getFlags( RecentChangesRow $row, IContextSource $ctx ) {
231        return [
232            'newpage' => $row->isFirstReply && $row->revision->isFirstRevision(),
233            'minor' => false,
234            'unpatrolled' => ChangesList::isUnpatrolled( $row->recentChange, $ctx->getUser() ),
235            'bot' => false,
236        ];
237    }
238
239    /**
240     * @param array $flags
241     * @return string
242     */
243    protected function formatFlags( $flags ) {
244        $flagKeys = array_keys( array_filter( $flags ) );
245        if ( $flagKeys ) {
246            $formattedFlags = array_map( [ ChangesList::class, 'flag' ], $flagKeys );
247            return implode( ' ', $formattedFlags ) . ' ';
248        }
249        return '';
250    }
251}