Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 102 |
|
0.00% |
0 / 8 |
CRAP | |
0.00% |
0 / 1 |
ChangesListFormatter | |
0.00% |
0 / 102 |
|
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 / 18 |
|
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 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 | [] |
98 | ); |
99 | $summary = '<span class="autocomment">' . $msg->text() . '</span>'; |
100 | |
101 | // '(' + '' + '←' + summary + ')' |
102 | $text = MediaWikiServices::getInstance()->getCommentFormatter() |
103 | ->formatBlock( $prefix . $link . $summary ); |
104 | |
105 | // MediaWiki\CommentFormatter\CommentFormatter::formatBlock escaped everything, but what we built was safe |
106 | // and should not be escaped so let's go back to decoded entities |
107 | return htmlspecialchars_decode( $text ); |
108 | } |
109 | |
110 | /** |
111 | * This overrides the default title link to include highlights for the posts |
112 | * that have not yet been seen. |
113 | * |
114 | * @param array $data |
115 | * @param FormatterRow $row |
116 | * @param IContextSource $ctx |
117 | * @return string |
118 | */ |
119 | protected function getTitleLink( array $data, FormatterRow $row, IContextSource $ctx ) { |
120 | if ( !$row instanceof RecentChangesRow ) { |
121 | // actually, this should be typehint, but can't because this needs |
122 | // to match the parent's more generic typehint |
123 | return parent::getTitleLink( $data, $row, $ctx ); |
124 | } |
125 | |
126 | if ( !isset( $data['links']['topic'] ) || !$data['links']['topic'] instanceof Anchor ) { |
127 | // no valid title anchor (probably header entry) |
128 | return parent::getTitleLink( $data, $row, $ctx ); |
129 | } |
130 | |
131 | $watched = $row->recentChange->getAttribute( 'wl_notificationtimestamp' ); |
132 | if ( is_bool( $watched ) ) { |
133 | // RC & watchlist share most code; the latter is unaware of when |
134 | // something was watched though, so we'll ignore that here |
135 | return parent::getTitleLink( $data, $row, $ctx ); |
136 | } |
137 | |
138 | if ( $watched === null ) { |
139 | // there is no data for unread posts - they've all been seen |
140 | return parent::getTitleLink( $data, $row, $ctx ); |
141 | } |
142 | |
143 | // get comparison UUID corresponding to this last watched timestamp |
144 | $uuid = UUID::getComparisonUUID( $watched ); |
145 | |
146 | // add highlight details to anchor |
147 | /** @var Anchor $anchor */ |
148 | $anchor = clone $data['links']['topic']; |
149 | $anchor->query['fromnotif'] = '1'; |
150 | $anchor->fragment = '#flow-post-' . $uuid->getAlphadecimal(); |
151 | $data['links']['topic'] = $anchor; |
152 | |
153 | // now pass it on to parent with the new, updated, link ;) |
154 | return parent::getTitleLink( $data, $row, $ctx ); |
155 | } |
156 | |
157 | /** |
158 | * @param RecentChangesRow $row |
159 | * @param IContextSource $ctx |
160 | * @return string |
161 | * @throws PermissionException |
162 | */ |
163 | public function getTimestampLink( $row, $ctx ) { |
164 | $this->serializer->setIncludeHistoryProperties( true ); |
165 | $this->serializer->setIncludeContent( false ); |
166 | |
167 | $data = $this->serializer->formatApi( $row, $ctx, 'recentchanges' ); |
168 | if ( $data === false ) { |
169 | throw new PermissionException( 'Insufficient permissions for ' . $row->revision->getRevisionId()->getAlphadecimal() ); |
170 | } |
171 | |
172 | return $this->formatTimestamp( $data, 'time' ); |
173 | } |
174 | |
175 | /** |
176 | * @param RecentChangesRow $row |
177 | * @param IContextSource $ctx |
178 | * @param \RCCacheEntry[] $block |
179 | * @param array $links |
180 | * @return array|false Links array, or false on failure |
181 | * @throws FlowException |
182 | * @throws \Flow\Exception\InvalidInputException |
183 | */ |
184 | public function getLogTextLinks( RecentChangesRow $row, IContextSource $ctx, array $block, array $links = [] ) { |
185 | $data = $this->serializer->formatApi( $row, $ctx, 'recentchanges' ); |
186 | if ( !$data ) { |
187 | return false; |
188 | } |
189 | |
190 | // Find the last (oldest) row in $block that is a Flow row. Note that there can be non-Flow |
191 | // things in $block (T228290). |
192 | $flowRows = array_filter( $block, static function ( $blockRow ) { |
193 | $source = $blockRow->getAttribute( 'rc_source' ); |
194 | return $source === RecentChangesListener::SRC_FLOW || |
195 | ( $source === null && $blockRow->getAttribute( 'rc_type' ) === RC_FLOW ); |
196 | } ); |
197 | $oldestRow = end( $flowRows ) ?? $row->recentChange; |
198 | |
199 | $old = unserialize( $oldestRow->getAttribute( 'rc_params' ) ); |
200 | $oldId = $old ? UUID::create( $old['flow-workflow-change']['revision'] ) : $row->revision->getRevisionId(); |
201 | |
202 | if ( isset( $data['links']['topic'] ) ) { |
203 | // add highlight details to anchor |
204 | // FIXME: This doesn't work well if the different rows in $block are for different topics |
205 | /** @var Anchor $anchor */ |
206 | $anchor = clone $data['links']['topic']; |
207 | $anchor->query['fromnotif'] = '1'; |
208 | $anchor->fragment = '#flow-post-' . $oldId->getAlphadecimal(); |
209 | } elseif ( isset( $data['links']['workflow'] ) ) { |
210 | $anchor = $data['links']['workflow']; |
211 | } else { |
212 | // this will be caught and logged by the RC hook, it will not fatal the page. |
213 | throw new FlowException( "No anchor available for revision $oldId" ); |
214 | } |
215 | |
216 | $changes = count( $block ); |
217 | // link text: "n changes" |
218 | $text = $ctx->msg( 'nchanges' )->numParams( $changes )->escaped(); |
219 | |
220 | // override total changes link |
221 | $links['total-changes'] = $anchor->toHtml( $text ); |
222 | |
223 | return $links; |
224 | } |
225 | |
226 | /** |
227 | * @param RecentChangesRow $row |
228 | * @param IContextSource $ctx |
229 | * @return array |
230 | */ |
231 | public function getFlags( RecentChangesRow $row, IContextSource $ctx ) { |
232 | return [ |
233 | 'newpage' => $row->isFirstReply && $row->revision->isFirstRevision(), |
234 | 'minor' => false, |
235 | 'unpatrolled' => ChangesList::isUnpatrolled( $row->recentChange, $ctx->getUser() ), |
236 | 'bot' => false, |
237 | ]; |
238 | } |
239 | |
240 | /** |
241 | * @param array $flags |
242 | * @return string |
243 | */ |
244 | protected function formatFlags( $flags ) { |
245 | $flagKeys = array_keys( array_filter( $flags ) ); |
246 | if ( $flagKeys ) { |
247 | $formattedFlags = array_map( [ ChangesList::class, 'flag' ], $flagKeys ); |
248 | return implode( ' ', $formattedFlags ) . ' '; |
249 | } |
250 | return ''; |
251 | } |
252 | } |