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