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