Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 98
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 / 98
0.00% covered (danger)
0.00%
0 / 8
600
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 / 21
0.00% covered (danger)
0.00%
0 / 1
30
 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            return $blockRow->getAttribute( 'rc_source' ) === RecentChangesListener::SRC_FLOW;
193        } );
194        $oldestRow = end( $flowRows ) ?? $row->recentChange;
195
196        $old = unserialize( $oldestRow->getAttribute( 'rc_params' ) );
197        $oldId = $old ? UUID::create( $old['flow-workflow-change']['revision'] ) : $row->revision->getRevisionId();
198
199        if ( isset( $data['links']['topic'] ) ) {
200            // add highlight details to anchor
201            // FIXME: This doesn't work well if the different rows in $block are for different topics
202            /** @var Anchor $anchor */
203            $anchor = clone $data['links']['topic'];
204            $anchor->query['fromnotif'] = '1';
205            $anchor->fragment = '#flow-post-' . $oldId->getAlphadecimal();
206        } elseif ( isset( $data['links']['workflow'] ) ) {
207            $anchor = $data['links']['workflow'];
208        } else {
209            // this will be caught and logged by the RC hook, it will not fatal the page.
210            throw new FlowException( "No anchor available for revision $oldId" );
211        }
212
213        $changes = count( $block );
214        // link text: "n changes"
215        $text = $ctx->msg( 'nchanges' )->numParams( $changes )->escaped();
216
217        // override total changes link
218        $links['total-changes'] = $anchor->toHtml( $text );
219
220        return $links;
221    }
222
223    /**
224     * @param RecentChangesRow $row
225     * @param IContextSource $ctx
226     * @return array
227     */
228    public function getFlags( RecentChangesRow $row, IContextSource $ctx ) {
229        return [
230            'newpage' => $row->isFirstReply && $row->revision->isFirstRevision(),
231            'minor' => false,
232            'unpatrolled' => ChangesList::isUnpatrolled( $row->recentChange, $ctx->getUser() ),
233            'bot' => false,
234        ];
235    }
236
237    /**
238     * @param array $flags
239     * @return string
240     */
241    protected function formatFlags( $flags ) {
242        $flagKeys = array_keys( array_filter( $flags ) );
243        if ( $flagKeys ) {
244            $formattedFlags = array_map( [ ChangesList::class, 'flag' ], $flagKeys );
245            return implode( ' ', $formattedFlags ) . ' ';
246        }
247        return '';
248    }
249}