Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 197
0.00% covered (danger)
0.00%
0 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
RecentChangesPropagationHooks
0.00% covered (danger)
0.00%
0 / 197
0.00% covered (danger)
0.00%
0 / 16
870
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getWordSep
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 isWikiStoriesRelatedChange
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 makeStoryLink
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 makeArticleLink
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getStoryId
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 makeDiffLink
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 makeHistLink
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
2
 makeDiffHistLinks
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 makeTimestampLink
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 makeUserLinks
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
12
 makeComment
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 onEnhancedChangesListModifyBlockLineData
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 onEnhancedChangesListModifyLineData
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
6
 onOldChangesListRecentChangesLine
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
30
 onChangesListSpecialPageStructuredFilters
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\Wikistories\Hooks;
4
5use HtmlArmor;
6use MediaWiki\CommentStore\CommentStoreComment;
7use MediaWiki\Config\Config;
8use MediaWiki\Context\IContextSource;
9use MediaWiki\Hook\EnhancedChangesListModifyBlockLineDataHook;
10use MediaWiki\Hook\EnhancedChangesListModifyLineDataHook;
11use MediaWiki\Hook\OldChangesListRecentChangesLineHook;
12use MediaWiki\Html\Html;
13use MediaWiki\Language\Language;
14use MediaWiki\Linker\LinkRenderer;
15use MediaWiki\Page\PageReference;
16use MediaWiki\Page\PageReferenceValue;
17use MediaWiki\RecentChanges\ChangesListBooleanFilter;
18use MediaWiki\RecentChanges\RecentChange;
19use MediaWiki\Revision\RevisionRecord;
20use MediaWiki\Revision\RevisionStore;
21use MediaWiki\SpecialPage\Hook\ChangesListSpecialPageStructuredFiltersHook;
22use MediaWiki\SpecialPage\SpecialPage;
23use MediaWiki\User\User;
24use MediaWiki\User\UserFactory;
25use Wikimedia\Rdbms\ILoadBalancer;
26
27class RecentChangesPropagationHooks implements
28    EnhancedChangesListModifyBlockLineDataHook,
29    EnhancedChangesListModifyLineDataHook,
30    OldChangesListRecentChangesLineHook,
31    ChangesListSpecialPageStructuredFiltersHook
32{
33    public const SRC_WIKISTORIES = 'src_wikistories';
34
35    private readonly string $sep;
36    private ?string $wordSep = null;
37
38    public function __construct(
39        private readonly RevisionStore $revisionStore,
40        private readonly Config $config,
41        private readonly LinkRenderer $linkRenderer,
42        private readonly ILoadBalancer $loadBalancer,
43        private readonly UserFactory $userFactory,
44    ) {
45        $this->sep = ' ' . Html::element( 'span', [ 'class' => 'mw-changeslist-separator' ], '' ) . ' ';
46    }
47
48    private function getWordSep( IContextSource $context ): string {
49        if ( $this->wordSep === null ) {
50            $this->wordSep = $context->msg( 'word-separator' )->plain();
51        }
52        return $this->wordSep;
53    }
54
55    private function isWikiStoriesRelatedChange( RecentChange $rc ): bool {
56        return $rc->getAttribute( 'rc_source' ) === self::SRC_WIKISTORIES;
57    }
58
59    private function makeStoryLink(
60        IContextSource $context,
61        PageReference $story,
62        bool $parens = false,
63    ): string {
64        $storyLink = $this->linkRenderer->makeKnownLink( $story );
65        $formattedLink = $parens ?
66            $context->msg( 'parentheses' )->rawParams( $storyLink )->text() :
67            $storyLink;
68        return Html::rawElement(
69            'span',
70            [],
71            $formattedLink
72        );
73    }
74
75    private function makeArticleLink( PageReference $article ): string {
76        return Html::rawElement(
77            'span',
78            [ 'class' => 'mw-title' ],
79            $this->linkRenderer->makeKnownLink( $article )
80        );
81    }
82
83    private function getStoryId( RecentChange $rc ): int {
84        $params = $rc->parseParams();
85        return $params[ 'story_id' ];
86    }
87
88    private function makeDiffLink( IContextSource $context, PageReference $story, RecentChange $rc ): string {
89        return Html::rawElement(
90            'span',
91            [],
92            $this->linkRenderer->makeKnownLink(
93                $story,
94                new HtmlArmor( $context->msg( 'diff' )->escaped() ),
95                [ 'class' => 'mw-changeslist-diff' ],
96                [
97                    'curid' => $this->getStoryId( $rc ),
98                    'diff' => $rc->getAttribute( 'rc_this_oldid' ),
99                    'oldid' => $rc->getAttribute( 'rc_last_oldid' ),
100                ]
101            )
102        );
103    }
104
105    private function makeHistLink( IContextSource $context, PageReference $story, RecentChange $rc ): string {
106        return Html::rawElement(
107            'span',
108            [],
109            $this->linkRenderer->makeKnownLink(
110                $story,
111                new HtmlArmor( $context->msg( 'hist' )->escaped() ),
112                [ 'class' => 'mw-changeslist-history' ],
113                [
114                    'curid' => $this->getStoryId( $rc ),
115                    'action' => 'history',
116                ]
117            )
118        );
119    }
120
121    private function makeDiffHistLinks(
122        IContextSource $context,
123        PageReference $story,
124        RecentChange $rc
125    ): string {
126        $diffLink = $this->makeDiffLink( $context, $story, $rc );
127        $histLink = $this->makeHistLink( $context, $story, $rc );
128        return Html::rawElement(
129            'span',
130            [ 'class' => 'mw-changeslist-links' ],
131            $diffLink . $histLink
132        );
133    }
134
135    private function makeTimestampLink( PageReference $story, RecentChange $rc, Language $lang ): string {
136        $user = $rc->getPerformerIdentity();
137        return $this->linkRenderer->makeKnownLink(
138            $story,
139            $lang->userTime( $rc->getAttribute( 'rc_timestamp' ), $user ),
140            [ 'class' => 'mw-changeslist-date' ],
141            [
142                'title' => $story->getDBkey(),
143                'curid' => $this->getStoryId( $rc ),
144                'oldid' => $rc->getAttribute( 'rc_this_oldid' ),
145            ]
146        );
147    }
148
149    private function makeUserLinks( IContextSource $context, int $visibility, User $user ): string {
150        if ( !RevisionRecord::userCanBitfield(
151            $visibility,
152            RevisionRecord::DELETED_USER,
153            $user )
154        ) {
155            // The username has been moderated and cannot be seen by the current user
156            return Html::rawElement(
157                'span',
158                [ 'class' => 'history-deleted' ],
159                $context->msg( 'rev-deleted-user' )->escaped()
160            );
161        }
162
163        $userLink = $this->linkRenderer->makeLink(
164            $user->getUserPage(),
165            $user->getName(),
166            [ 'class' => 'mw-userlink' ]
167        );
168
169        $links = [];
170
171        $links[] = Html::rawElement(
172            'span',
173            [],
174            $this->linkRenderer->makeLink(
175                $user->getTalkPage(),
176                $context->msg( 'talkpagelinktext' )->text(),
177                [ 'class' => 'mw-usertoollinks-talk' ]
178            )
179        );
180
181        if ( $user->isRegistered() ) {
182            $links[] = Html::rawElement(
183                'span',
184                [],
185                $this->linkRenderer->makeLink(
186                    SpecialPage::getTitleValueFor( 'Contributions', $user->getName() ),
187                    $context->msg( 'contribslink' )->text(),
188                    [ 'class' => 'mw-usertoollinks-contribs' ]
189                )
190            );
191        }
192        return $userLink .
193            $this->getWordSep( $context ) .
194            Html::rawElement(
195                'span',
196                [ 'class' => 'mw-usertoollinks mw-changeslist-links' ],
197                implode( '', $links )
198            );
199    }
200
201    private function makeComment( IContextSource $context, ?CommentStoreComment $comment ): string {
202        $text = $comment ? $comment->text : null;
203        if ( $text !== null && $text !== '' ) {
204            return Html::rawElement(
205                'span',
206                [ 'class' => 'comment' ],
207                $context->msg( 'parentheses', $text )->parse()
208            );
209
210        }
211        return '';
212    }
213
214    /**
215     * Use this hook to alter data used to build a non-grouped recent change line in
216     * EnhancedChangesList.
217     *
218     * @inheritDoc
219     */
220    public function onEnhancedChangesListModifyBlockLineData( $changesList, &$data, $rc ) {
221        if ( !$this->isWikiStoriesRelatedChange( $rc ) ) {
222            return;
223        }
224
225        $params = $rc->parseParams();
226        $story = PageReferenceValue::localReference( NS_STORY, $params[ 'story_title' ] );
227        $lang = $changesList->getLanguage();
228        $context = $changesList->getContext();
229
230        $data[ 'recentChangesFlags' ][ 'wikistories-edit' ] = true;
231
232        // Make timestamp link to specific revision
233        $data[ 'timestampLink' ] = $this->makeTimestampLink( $story, $rc, $lang );
234
235        // Append story link to article link
236        $data[ 'articleLink' ] .= $this->sep . $this->makeStoryLink( $context, $story, true );
237
238        // Remove character diff section
239        unset( $data['characterDiff'] );
240        unset( $data['separatorAftercharacterDiff'] );
241
242        // Make DIFF and HIST links for story instead of article
243        $data[ 'historyLink' ] = $this->getWordSep( $context ) .
244            $this->makeDiffHistLinks( $context, $story, $rc );
245    }
246
247    /**
248     * Use this hook to alter data used to build a grouped recent change inner line in
249     * EnhancedChangesList.
250     *
251     * @inheritDoc
252     */
253    public function onEnhancedChangesListModifyLineData( $changesList, &$data, $block, $rc, &$classes, &$attribs ) {
254        if ( !$this->isWikiStoriesRelatedChange( $rc ) ) {
255            return;
256        }
257
258        $params = $rc->parseParams();
259        $story = PageReferenceValue::localReference( NS_STORY, $params[ 'story_title' ] );
260        $lang = $changesList->getLanguage();
261        $context = $changesList->getContext();
262
263        $data[ 'recentChangesFlags' ][ 'wikistories-edit' ] = true;
264
265        // Make timestamp link to specific revision
266        $data[ 'timestampLink' ] = $this->makeTimestampLink( $story, $rc, $lang );
267
268        // Replace "(cur last)" links with "story (diff hist)" links
269        $data[ 'currentAndLastLinks' ] = $this->getWordSep( $context ) .
270            $this->makeStoryLink( $context, $story ) .
271            $this->getWordSep( $context ) .
272            $this->makeDiffHistLinks( $context, $story, $rc );
273
274        // Remove character diff section
275        unset( $data['characterDiff'] );
276        unset( $data['separatorAfterCharacterDiff'] );
277    }
278
279    /**
280     * Use this hook to customize a recent changes line.
281     *
282     * @inheritDoc
283     */
284    public function onOldChangesListRecentChangesLine( $changeslist, &$s, $rc, &$classes, &$attribs ) {
285        if ( !$this->isWikiStoriesRelatedChange( $rc ) ) {
286            return;
287        }
288
289        $params = $rc->parseParams();
290        $story = PageReferenceValue::localReference( NS_STORY, $params['story_title'] );
291        $rev = $this->revisionStore->getRevisionById( $rc->getAttribute( 'rc_this_oldid' ) );
292        $user = $this->userFactory->newFromUserIdentity( $rc->getPerformerIdentity() );
293        $lang = $changeslist->getLanguage();
294        $context = $changeslist->getContext();
295        $comment = $rev !== null ? $rev->getComment( RevisionRecord::FOR_PUBLIC, $user ) : null;
296
297        $flag = $changeslist->recentChangesFlags(
298            [
299                'wikistories-edit' => true,
300                'minor' => $rc->getAttribute( 'rc_minor' ),
301                'bot' => $rc->getAttribute( 'rc_bot' ),
302            ],
303            ''
304        );
305
306        $article = $rc->getPage();
307        if ( $article === null ) {
308            return;
309        }
310
311        $s = Html::rawElement(
312            'span',
313            [ 'class' => 'mw-changeslist-line-inner' ],
314            $this->makeDiffHistLinks( $context, $story, $rc ) .
315            $this->sep .
316            $flag .
317            $this->getWordSep( $context ) .
318            $this->makeArticleLink( $article ) .
319            $this->getWordSep( $context ) .
320            $this->makeStoryLink( $context, $story, true ) .
321            $this->getWordSep( $context ) .
322            $this->makeTimestampLink( $story, $rc, $lang ) .
323            $this->sep .
324            $this->makeUserLinks( $context, $rev !== null ? $rev->getVisibility() : 0, $user ) .
325            $this->getWordSep( $context ) .
326            $this->makeComment( $context, $comment )
327        );
328    }
329
330    /**
331     * @inheritDoc
332     */
333    public function onChangesListSpecialPageStructuredFilters( $special ) {
334        // @phan-suppress-next-line PhanNoopNew
335        new ChangesListBooleanFilter( [
336            'name' => 'hidewikistories',
337            'group' => $special->getFilterGroup( 'changeType' ),
338            'priority' => -4,
339            'label' => 'wikistories-rcfilters-hidewikistories-label',
340            'description' => 'wikistories-rcfilters-hidewikistories-description',
341            'showHide' => 'rcshowhidewikistories',
342            'default' => false,
343            'queryCallable' => static function (
344                $specialClassName, $ctx, $dbr, &$tables, &$fields, &$conds, &$query_options, &$join_conds
345            ) {
346                $conds[] = $dbr->expr( 'rc_source', '!=', self::SRC_WIKISTORIES );
347            },
348            'cssClassSuffix' => 'src-mw-wikistories',
349            'isRowApplicableCallable' => function ( $ctx, $rc ) {
350                return $this->isWikiStoriesRelatedChange( $rc );
351            }
352        ] );
353    }
354}