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 ChangesList; |
6 | use Flow\Conversion\Utils; |
7 | use Flow\Data\Listener\RecentChangesListener; |
8 | use Flow\Exception\FlowException; |
9 | use Flow\Exception\PermissionException; |
10 | use Flow\Model\Anchor; |
11 | use Flow\Model\UUID; |
12 | use MediaWiki\Context\IContextSource; |
13 | use MediaWiki\MediaWikiServices; |
14 | |
15 | class 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 | } |