Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 100 |
|
0.00% |
0 / 8 |
CRAP | |
0.00% |
0 / 1 |
ChangesListFormatter | |
0.00% |
0 / 100 |
|
0.00% |
0 / 8 |
702 | |
0.00% |
0 / 1 |
getHistoryType | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
format | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
20 | |||
getEditSummary | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
6 | |||
getTitleLink | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
42 | |||
getTimestampLink | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
getLogTextLinks | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
56 | |||
getFlags | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
formatFlags | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | namespace Flow\Formatter; |
4 | |
5 | use Flow\Conversion\Utils; |
6 | use Flow\Data\Listener\RecentChangesListener; |
7 | use Flow\Exception\FlowException; |
8 | use Flow\Exception\PermissionException; |
9 | use Flow\Model\Anchor; |
10 | use Flow\Model\UUID; |
11 | use MediaWiki\Context\IContextSource; |
12 | use MediaWiki\MediaWikiServices; |
13 | use MediaWiki\RecentChanges\ChangesList; |
14 | use MediaWiki\RecentChanges\RCCacheEntry; |
15 | |
16 | class 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 | } |