Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
17.21% |
42 / 244 |
|
5.88% |
1 / 17 |
CRAP | |
0.00% |
0 / 1 |
RecentChangesPropagationHooks | |
17.21% |
42 / 244 |
|
5.88% |
1 / 17 |
576.27 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
makeRecentChangesEntry | |
100.00% |
42 / 42 |
|
100.00% |
1 / 1 |
2 | |||
getWordSep | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
isWikiStoriesRelatedChange | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
makeStoryLink | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
makeArticleLink | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
getStoryId | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
makeDiffLink | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
2 | |||
makeHistLink | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
2 | |||
makeDiffHistLinks | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
makeTimestampLink | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
2 | |||
makeUserLinks | |
0.00% |
0 / 41 |
|
0.00% |
0 / 1 |
12 | |||
makeComment | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
onEnhancedChangesListModifyBlockLineData | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
6 | |||
onEnhancedChangesListModifyLineData | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
6 | |||
onOldChangesListRecentChangesLine | |
0.00% |
0 / 37 |
|
0.00% |
0 / 1 |
30 | |||
onChangesListSpecialPageStructuredFilters | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\Wikistories\Hooks; |
4 | |
5 | use ChangesListBooleanFilter; |
6 | use HtmlArmor; |
7 | use MediaWiki\CommentStore\CommentStoreComment; |
8 | use MediaWiki\Config\Config; |
9 | use MediaWiki\Context\IContextSource; |
10 | use MediaWiki\Hook\EnhancedChangesListModifyBlockLineDataHook; |
11 | use MediaWiki\Hook\EnhancedChangesListModifyLineDataHook; |
12 | use MediaWiki\Hook\OldChangesListRecentChangesLineHook; |
13 | use MediaWiki\Html\Html; |
14 | use MediaWiki\Language\Language; |
15 | use MediaWiki\Linker\LinkRenderer; |
16 | use MediaWiki\MediaWikiServices; |
17 | use MediaWiki\Page\PageIdentity; |
18 | use MediaWiki\Page\PageReference; |
19 | use MediaWiki\Page\PageReferenceValue; |
20 | use MediaWiki\Revision\RevisionRecord; |
21 | use MediaWiki\Revision\RevisionStore; |
22 | use MediaWiki\SpecialPage\Hook\ChangesListSpecialPageStructuredFiltersHook; |
23 | use MediaWiki\SpecialPage\SpecialPage; |
24 | use MediaWiki\Storage\EditResult; |
25 | use MediaWiki\User\User; |
26 | use MediaWiki\User\UserFactory; |
27 | use MediaWiki\User\UserIdentity; |
28 | use RecentChange; |
29 | use Wikimedia\Rdbms\ILoadBalancer; |
30 | |
31 | class RecentChangesPropagationHooks implements |
32 | EnhancedChangesListModifyBlockLineDataHook, |
33 | EnhancedChangesListModifyLineDataHook, |
34 | OldChangesListRecentChangesLineHook, |
35 | ChangesListSpecialPageStructuredFiltersHook |
36 | { |
37 | public const SRC_WIKISTORIES = 'src_wikistories'; |
38 | |
39 | /** @var RevisionStore */ |
40 | private $revisionStore; |
41 | |
42 | /** @var Config */ |
43 | private $config; |
44 | |
45 | /** @var LinkRenderer */ |
46 | private $linkRenderer; |
47 | |
48 | /** @var ILoadBalancer */ |
49 | private $loadBalancer; |
50 | |
51 | /** @var string */ |
52 | private $sep; |
53 | |
54 | /** @var string */ |
55 | private $wordSep = null; |
56 | |
57 | /** @var UserFactory */ |
58 | private $userFactory; |
59 | |
60 | /** |
61 | * @param RevisionStore $revisionStore |
62 | * @param Config $config |
63 | * @param LinkRenderer $linkRenderer |
64 | * @param ILoadBalancer $loadBalancer |
65 | * @param UserFactory $userFactory |
66 | */ |
67 | public function __construct( |
68 | RevisionStore $revisionStore, |
69 | Config $config, |
70 | LinkRenderer $linkRenderer, |
71 | ILoadBalancer $loadBalancer, |
72 | UserFactory $userFactory |
73 | ) { |
74 | $this->revisionStore = $revisionStore; |
75 | $this->config = $config; |
76 | $this->linkRenderer = $linkRenderer; |
77 | $this->loadBalancer = $loadBalancer; |
78 | $this->userFactory = $userFactory; |
79 | |
80 | $this->sep = ' ' . Html::element( 'span', [ 'class' => 'mw-changeslist-separator' ], '' ) . ' '; |
81 | } |
82 | |
83 | /** |
84 | * When a story is saved (created or edited), we create a recent changes |
85 | * entry for the related article so that watchers of that article can |
86 | * be aware of the story change. |
87 | * |
88 | * @note The logic for creating the fake RecentChanges entry is in this class |
89 | * because this is where we define how that entry is later visualized. |
90 | * The actual insertion of the fake RC entry is left to EventIngress, which |
91 | * handles core events triggered by page changes. |
92 | */ |
93 | public static function makeRecentChangesEntry( |
94 | PageIdentity $article, |
95 | RevisionRecord $revisionRecord, |
96 | UserIdentity $user, |
97 | string $summary, |
98 | string $requestIP, |
99 | bool $minor, |
100 | bool $bot, |
101 | int $patrolled, |
102 | ?EditResult $editResult |
103 | ): RecentChange { |
104 | // NOTE: $revisionRecord does not belong to $article! |
105 | |
106 | $rc = new RecentChange; |
107 | $rc->mAttribs = [ |
108 | 'rc_timestamp' => $revisionRecord->getTimestamp(), |
109 | 'rc_namespace' => $article->getNamespace(), |
110 | 'rc_title' => $article->getDBkey(), |
111 | 'rc_type' => RC_EXTERNAL, |
112 | 'rc_source' => self::SRC_WIKISTORIES, |
113 | 'rc_minor' => $minor, |
114 | 'rc_cur_id' => $article->getId(), |
115 | 'rc_user' => $user->getId(), |
116 | 'rc_user_text' => $user->getName(), |
117 | 'rc_comment' => $summary, |
118 | 'rc_comment_text' => $summary, |
119 | 'rc_comment_data' => null, |
120 | 'rc_this_oldid' => (int)$revisionRecord->getId(), |
121 | 'rc_last_oldid' => (int)$revisionRecord->getParentId(), |
122 | 'rc_bot' => $bot, |
123 | 'rc_ip' => $requestIP, |
124 | 'rc_patrolled' => $patrolled, |
125 | 'rc_new' => 0, |
126 | 'rc_old_len' => 0, |
127 | 'rc_new_len' => 0, |
128 | 'rc_deleted' => 0, |
129 | 'rc_logid' => 0, |
130 | 'rc_log_type' => null, |
131 | 'rc_log_action' => '', |
132 | 'rc_params' => serialize( [ |
133 | 'story_title' => $revisionRecord->getPage()->getDBkey(), |
134 | 'story_id' => $revisionRecord->getPage()->getId(), |
135 | ] ) |
136 | ]; |
137 | |
138 | // TODO: deprecate the 'prefixedDBkey' entry, let callers do the formatting. |
139 | $formatter = MediaWikiServices::getInstance()->getTitleFormatter(); |
140 | |
141 | $rc->mExtra = [ |
142 | 'prefixedDBkey' => $formatter->getPrefixedDBkey( $article ), |
143 | 'lastTimestamp' => 0, |
144 | 'oldSize' => 0, |
145 | 'newSize' => 0, |
146 | 'pageStatus' => 'changed' |
147 | ]; |
148 | |
149 | if ( $editResult ) { |
150 | $rc->setEditResult( $editResult ); |
151 | } |
152 | |
153 | return $rc; |
154 | } |
155 | |
156 | /** |
157 | * @param IContextSource $context |
158 | * @return string Word separator |
159 | */ |
160 | private function getWordSep( IContextSource $context ): string { |
161 | if ( $this->wordSep === null ) { |
162 | $this->wordSep = $context->msg( 'word-separator' )->plain(); |
163 | } |
164 | return $this->wordSep; |
165 | } |
166 | |
167 | /** |
168 | * @param RecentChange $rc |
169 | * @return bool |
170 | */ |
171 | private function isWikiStoriesRelatedChange( RecentChange $rc ): bool { |
172 | return $rc->getAttribute( 'rc_source' ) === self::SRC_WIKISTORIES; |
173 | } |
174 | |
175 | /** |
176 | * @param IContextSource $context |
177 | * @param PageReference $story |
178 | * @param bool $parens |
179 | * @return string |
180 | */ |
181 | private function makeStoryLink( IContextSource $context, PageReference $story, $parens = false ): string { |
182 | $storyLink = $this->linkRenderer->makeKnownLink( $story ); |
183 | $formattedLink = $parens ? |
184 | $context->msg( 'parentheses' )->rawParams( $storyLink )->text() : |
185 | $storyLink; |
186 | return Html::rawElement( |
187 | 'span', |
188 | [], |
189 | $formattedLink |
190 | ); |
191 | } |
192 | |
193 | /** |
194 | * @param PageReference $article |
195 | * @return string |
196 | */ |
197 | private function makeArticleLink( PageReference $article ): string { |
198 | return Html::rawElement( |
199 | 'span', |
200 | [ 'class' => 'mw-title' ], |
201 | $this->linkRenderer->makeKnownLink( $article ) |
202 | ); |
203 | } |
204 | |
205 | /** |
206 | * @param RecentChange $rc |
207 | * @return int |
208 | */ |
209 | private function getStoryId( RecentChange $rc ): int { |
210 | $params = $rc->parseParams(); |
211 | return $params[ 'story_id' ]; |
212 | } |
213 | |
214 | /** |
215 | * @param IContextSource $context |
216 | * @param PageReference $story |
217 | * @param RecentChange $rc |
218 | * @return string |
219 | */ |
220 | private function makeDiffLink( IContextSource $context, PageReference $story, RecentChange $rc ): string { |
221 | return Html::rawElement( |
222 | 'span', |
223 | [], |
224 | $this->linkRenderer->makeKnownLink( |
225 | $story, |
226 | new HtmlArmor( $context->msg( 'diff' )->escaped() ), |
227 | [ 'class' => 'mw-changeslist-diff' ], |
228 | [ |
229 | 'curid' => $this->getStoryId( $rc ), |
230 | 'diff' => $rc->getAttribute( 'rc_this_oldid' ), |
231 | 'oldid' => $rc->getAttribute( 'rc_last_oldid' ), |
232 | ] |
233 | ) |
234 | ); |
235 | } |
236 | |
237 | /** |
238 | * @param IContextSource $context |
239 | * @param PageReference $story |
240 | * @param RecentChange $rc |
241 | * @return string |
242 | */ |
243 | private function makeHistLink( IContextSource $context, PageReference $story, RecentChange $rc ): string { |
244 | return Html::rawElement( |
245 | 'span', |
246 | [], |
247 | $this->linkRenderer->makeKnownLink( |
248 | $story, |
249 | new HtmlArmor( $context->msg( 'hist' )->escaped() ), |
250 | [ 'class' => 'mw-changeslist-history' ], |
251 | [ |
252 | 'curid' => $this->getStoryId( $rc ), |
253 | 'action' => 'history', |
254 | ] |
255 | ) |
256 | ); |
257 | } |
258 | |
259 | /** |
260 | * @param IContextSource $context |
261 | * @param PageReference $story |
262 | * @param RecentChange $rc |
263 | * @return string |
264 | */ |
265 | private function makeDiffHistLinks( |
266 | IContextSource $context, |
267 | PageReference $story, |
268 | RecentChange $rc |
269 | ): string { |
270 | $diffLink = $this->makeDiffLink( $context, $story, $rc ); |
271 | $histLink = $this->makeHistLink( $context, $story, $rc ); |
272 | return Html::rawElement( |
273 | 'span', |
274 | [ 'class' => 'mw-changeslist-links' ], |
275 | $diffLink . $histLink |
276 | ); |
277 | } |
278 | |
279 | /** |
280 | * @param PageReference $story |
281 | * @param RecentChange $rc |
282 | * @param Language $lang |
283 | * @return string |
284 | */ |
285 | private function makeTimestampLink( PageReference $story, RecentChange $rc, Language $lang ): string { |
286 | $user = $rc->getPerformerIdentity(); |
287 | return $this->linkRenderer->makeKnownLink( |
288 | $story, |
289 | $lang->userTime( $rc->getAttribute( 'rc_timestamp' ), $user ), |
290 | [ 'class' => 'mw-changeslist-date' ], |
291 | [ |
292 | 'title' => $story->getDBkey(), |
293 | 'curid' => $this->getStoryId( $rc ), |
294 | 'oldid' => $rc->getAttribute( 'rc_this_oldid' ), |
295 | ] |
296 | ); |
297 | } |
298 | |
299 | /** |
300 | * @param IContextSource $context |
301 | * @param int $visibility |
302 | * @param User $user |
303 | * @return string |
304 | */ |
305 | private function makeUserLinks( IContextSource $context, int $visibility, User $user ) { |
306 | if ( !RevisionRecord::userCanBitfield( |
307 | $visibility, |
308 | RevisionRecord::DELETED_USER, |
309 | $user ) |
310 | ) { |
311 | // The username has been moderated and cannot be seen by the current user |
312 | return Html::rawElement( |
313 | 'span', |
314 | [ 'class' => 'history-deleted' ], |
315 | $context->msg( 'rev-deleted-user' )->escaped() |
316 | ); |
317 | } |
318 | |
319 | $userLink = $this->linkRenderer->makeLink( |
320 | $user->getUserPage(), |
321 | $user->getName(), |
322 | [ 'class' => 'mw-userlink' ] |
323 | ); |
324 | |
325 | $links = []; |
326 | |
327 | $links[] = Html::rawElement( |
328 | 'span', |
329 | [], |
330 | $this->linkRenderer->makeLink( |
331 | $user->getTalkPage(), |
332 | $context->msg( 'talkpagelinktext' )->text(), |
333 | [ 'class' => 'mw-usertoollinks-talk' ] |
334 | ) |
335 | ); |
336 | |
337 | if ( $user->isRegistered() ) { |
338 | $links[] = Html::rawElement( |
339 | 'span', |
340 | [], |
341 | $this->linkRenderer->makeLink( |
342 | SpecialPage::getTitleValueFor( 'Contributions', $user->getName() ), |
343 | $context->msg( 'contribslink' )->text(), |
344 | [ 'class' => 'mw-usertoollinks-contribs' ] |
345 | ) |
346 | ); |
347 | } |
348 | return $userLink . |
349 | $this->getWordSep( $context ) . |
350 | Html::rawElement( |
351 | 'span', |
352 | [ 'class' => 'mw-usertoollinks mw-changeslist-links' ], |
353 | implode( '', $links ) |
354 | ); |
355 | } |
356 | |
357 | /** |
358 | * @param IContextSource $context |
359 | * @param CommentStoreComment|null $comment |
360 | * @return string |
361 | */ |
362 | private function makeComment( IContextSource $context, $comment ): string { |
363 | $text = $comment ? $comment->text : null; |
364 | if ( $text !== null && $text !== '' ) { |
365 | return Html::rawElement( |
366 | 'span', |
367 | [ 'class' => 'comment' ], |
368 | $context->msg( 'parentheses', $text )->parse() |
369 | ); |
370 | |
371 | } |
372 | return ''; |
373 | } |
374 | |
375 | /** |
376 | * Use this hook to alter data used to build a non-grouped recent change line in |
377 | * EnhancedChangesList. |
378 | * |
379 | * @inheritDoc |
380 | */ |
381 | public function onEnhancedChangesListModifyBlockLineData( $changesList, &$data, $rc ) { |
382 | if ( !$this->isWikiStoriesRelatedChange( $rc ) ) { |
383 | return; |
384 | } |
385 | |
386 | $params = $rc->parseParams(); |
387 | $story = PageReferenceValue::localReference( NS_STORY, $params[ 'story_title' ] ); |
388 | $lang = $changesList->getLanguage(); |
389 | $context = $changesList->getContext(); |
390 | |
391 | $data[ 'recentChangesFlags' ][ 'wikistories-edit' ] = true; |
392 | |
393 | // Make timestamp link to specific revision |
394 | $data[ 'timestampLink' ] = $this->makeTimestampLink( $story, $rc, $lang ); |
395 | |
396 | // Append story link to article link |
397 | $data[ 'articleLink' ] .= $this->sep . $this->makeStoryLink( $context, $story, true ); |
398 | |
399 | // Remove character diff section |
400 | unset( $data['characterDiff'] ); |
401 | unset( $data['separatorAftercharacterDiff'] ); |
402 | |
403 | // Make DIFF and HIST links for story instead of article |
404 | $data[ 'historyLink' ] = $this->getWordSep( $context ) . |
405 | $this->makeDiffHistLinks( $context, $story, $rc ); |
406 | } |
407 | |
408 | /** |
409 | * Use this hook to alter data used to build a grouped recent change inner line in |
410 | * EnhancedChangesList. |
411 | * |
412 | * @inheritDoc |
413 | */ |
414 | public function onEnhancedChangesListModifyLineData( $changesList, &$data, $block, $rc, &$classes, &$attribs ) { |
415 | if ( !$this->isWikiStoriesRelatedChange( $rc ) ) { |
416 | return; |
417 | } |
418 | |
419 | $params = $rc->parseParams(); |
420 | $story = PageReferenceValue::localReference( NS_STORY, $params[ 'story_title' ] ); |
421 | $lang = $changesList->getLanguage(); |
422 | $context = $changesList->getContext(); |
423 | |
424 | $data[ 'recentChangesFlags' ][ 'wikistories-edit' ] = true; |
425 | |
426 | // Make timestamp link to specific revision |
427 | $data[ 'timestampLink' ] = $this->makeTimestampLink( $story, $rc, $lang ); |
428 | |
429 | // Replace "(cur last)" links with "story (diff hist)" links |
430 | $data[ 'currentAndLastLinks' ] = $this->getWordSep( $context ) . |
431 | $this->makeStoryLink( $context, $story ) . |
432 | $this->getWordSep( $context ) . |
433 | $this->makeDiffHistLinks( $context, $story, $rc ); |
434 | |
435 | // Remove character diff section |
436 | unset( $data['characterDiff'] ); |
437 | unset( $data['separatorAfterCharacterDiff'] ); |
438 | } |
439 | |
440 | /** |
441 | * Use this hook to customize a recent changes line. |
442 | * |
443 | * @inheritDoc |
444 | */ |
445 | public function onOldChangesListRecentChangesLine( $changeslist, &$s, $rc, &$classes, &$attribs ) { |
446 | if ( !$this->isWikiStoriesRelatedChange( $rc ) ) { |
447 | return; |
448 | } |
449 | |
450 | $params = $rc->parseParams(); |
451 | $story = PageReferenceValue::localReference( NS_STORY, $params['story_title'] ); |
452 | $rev = $this->revisionStore->getRevisionById( $rc->getAttribute( 'rc_this_oldid' ) ); |
453 | $user = $this->userFactory->newFromUserIdentity( $rc->getPerformerIdentity() ); |
454 | $lang = $changeslist->getLanguage(); |
455 | $context = $changeslist->getContext(); |
456 | $comment = $rev !== null ? $rev->getComment( RevisionRecord::FOR_PUBLIC, $user ) : null; |
457 | |
458 | $flag = $changeslist->recentChangesFlags( |
459 | [ |
460 | 'wikistories-edit' => true, |
461 | 'minor' => $rc->getAttribute( 'rc_minor' ), |
462 | 'bot' => $rc->getAttribute( 'rc_bot' ), |
463 | ], |
464 | '' |
465 | ); |
466 | |
467 | $article = $rc->getPage(); |
468 | if ( $article === null ) { |
469 | return; |
470 | } |
471 | |
472 | $s = Html::rawElement( |
473 | 'span', |
474 | [ 'class' => 'mw-changeslist-line-inner' ], |
475 | $this->makeDiffHistLinks( $context, $story, $rc ) . |
476 | $this->sep . |
477 | $flag . |
478 | $this->getWordSep( $context ) . |
479 | $this->makeArticleLink( $article ) . |
480 | $this->getWordSep( $context ) . |
481 | $this->makeStoryLink( $context, $story, true ) . |
482 | $this->getWordSep( $context ) . |
483 | $this->makeTimestampLink( $story, $rc, $lang ) . |
484 | $this->sep . |
485 | $this->makeUserLinks( $context, $rev !== null ? $rev->getVisibility() : 0, $user ) . |
486 | $this->getWordSep( $context ) . |
487 | $this->makeComment( $context, $comment ) |
488 | ); |
489 | } |
490 | |
491 | /** |
492 | * @inheritDoc |
493 | */ |
494 | public function onChangesListSpecialPageStructuredFilters( $special ) { |
495 | // @phan-suppress-next-line PhanNoopNew |
496 | new ChangesListBooleanFilter( [ |
497 | 'name' => 'hidewikistories', |
498 | 'group' => $special->getFilterGroup( 'changeType' ), |
499 | 'priority' => -4, |
500 | 'label' => 'wikistories-rcfilters-hidewikistories-label', |
501 | 'description' => 'wikistories-rcfilters-hidewikistories-description', |
502 | 'showHide' => 'rcshowhidewikistories', |
503 | 'default' => false, |
504 | 'queryCallable' => static function ( |
505 | $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds |
506 | ) { |
507 | $conds[] = $dbr->expr( 'rc_source', '!=', self::SRC_WIKISTORIES ); |
508 | }, |
509 | 'cssClassSuffix' => 'src-mw-wikistories', |
510 | 'isRowApplicableCallable' => function ( $ctx, $rc ) { |
511 | return $this->isWikiStoriesRelatedChange( $rc ); |
512 | } |
513 | ] ); |
514 | } |
515 | } |