Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 110
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialInvitationList
0.00% covered (danger)
0.00%
0 / 110
0.00% covered (danger)
0.00%
0 / 7
462
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 maybeDisplayList
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
30
 displayList
0.00% covered (danger)
0.00%
0 / 48
0.00% covered (danger)
0.00%
0 / 1
20
 getWorklistLinks
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 splitUsersByScore
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 getUserLinks
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3declare( strict_types=1 );
4
5namespace MediaWiki\Extension\CampaignEvents\Special;
6
7use LogicException;
8use MediaWiki\Extension\CampaignEvents\Invitation\InvitationList;
9use MediaWiki\Extension\CampaignEvents\Invitation\InvitationListNotFoundException;
10use MediaWiki\Extension\CampaignEvents\Invitation\InvitationListStore;
11use MediaWiki\Extension\CampaignEvents\MWEntity\CampaignsCentralUserLookup;
12use MediaWiki\Extension\CampaignEvents\MWEntity\CentralUser;
13use MediaWiki\Extension\CampaignEvents\MWEntity\CentralUserNotFoundException;
14use MediaWiki\Extension\CampaignEvents\MWEntity\HiddenCentralUserException;
15use MediaWiki\Extension\CampaignEvents\MWEntity\MWAuthorityProxy;
16use MediaWiki\Extension\CampaignEvents\MWEntity\UserLinker;
17use MediaWiki\Extension\CampaignEvents\Permissions\PermissionChecker;
18use MediaWiki\Html\Html;
19use MediaWiki\Html\TemplateParser;
20use MediaWiki\Message\Message;
21use MediaWiki\Page\PageIdentity;
22use MediaWiki\SpecialPage\SpecialPage;
23use OOUI\HtmlSnippet;
24use OOUI\MessageWidget;
25
26class SpecialInvitationList extends SpecialPage {
27    use InvitationFeatureAccessTrait;
28
29    public const PAGE_NAME = 'InvitationList';
30
31    private PermissionChecker $permissionChecker;
32    private InvitationListStore $invitationListStore;
33    private CampaignsCentralUserLookup $centralUserLookup;
34    private UserLinker $userLinker;
35    private TemplateParser $templateParser;
36
37    private const HIGHLY_RECOMMENDED_MIN_SCORE = 70;
38    public const RECOMMENDED_MIN_SCORE = 25;
39
40    public function __construct(
41        PermissionChecker $permissionChecker,
42        InvitationListStore $invitationListStore,
43        CampaignsCentralUserLookup $centralUserLookup,
44        UserLinker $userLinker
45    ) {
46        parent::__construct( self::PAGE_NAME );
47        $this->permissionChecker = $permissionChecker;
48        $this->invitationListStore = $invitationListStore;
49        $this->centralUserLookup = $centralUserLookup;
50        $this->userLinker = $userLinker;
51        $this->templateParser = new TemplateParser( __DIR__ . '/../../templates' );
52    }
53
54    /**
55     * @inheritDoc
56     */
57    public function execute( $par ): void {
58        $this->setHeaders();
59        $this->outputHeader();
60        $mwAuthority = new MWAuthorityProxy( $this->getAuthority() );
61        $out = $this->getOutput();
62        $out->enableOOUI();
63        $isEnabledAndPermitted = $this->checkInvitationFeatureAccess(
64            $this->getOutput(),
65            $mwAuthority
66        );
67        if ( $isEnabledAndPermitted ) {
68            $this->maybeDisplayList( $par );
69        }
70    }
71
72    private function maybeDisplayList( ?string $par ): void {
73        if ( $par === null ) {
74            $this->getOutput()->redirect(
75                SpecialPage::getTitleFor( SpecialMyInvitationLists::PAGE_NAME )->getLocalURL()
76            );
77            return;
78        }
79
80        $listID = (int)$par;
81        if ( (string)$listID !== $par ) {
82            $this->getOutput()->addHTML( Html::errorBox(
83                $this->msg( 'campaignevents-invitation-list-invalid-id' )->parseAsBlock()
84            ) );
85            return;
86        }
87        try {
88            $invitationList = $this->invitationListStore->getInvitationList( $listID );
89        } catch ( InvitationListNotFoundException $_ ) {
90            $this->getOutput()->addHTML( Html::errorBox(
91                $this->msg( 'campaignevents-invitation-list-does-not-exist' )->parseAsBlock()
92            ) );
93            return;
94        }
95
96        $user = $this->centralUserLookup->newFromAuthority( new MWAuthorityProxy( $this->getAuthority() ) );
97        if ( !$invitationList->getCreator()->equals( $user ) ) {
98            $this->getOutput()->addHTML( Html::errorBox(
99                $this->msg( 'campaignevents-invitation-list-not-creator' )->parseAsBlock()
100            ) );
101            return;
102        }
103
104        $this->displayList( $invitationList );
105    }
106
107    private function displayList( InvitationList $list ): void {
108        $out = $this->getOutput();
109        $out->setPageTitleMsg(
110            $this->msg( 'campaignevents-invitationlist-event' )->params( $list->getName() )
111        );
112
113        if ( $list->getStatus() === InvitationList::STATUS_PENDING ) {
114            $messageWidget = new MessageWidget( [
115                'type' => 'notice',
116                'label' => new HtmlSnippet( $this->msg( 'campaignevents-invitation-list-processing' )->parse() )
117            ] );
118            $out->addHTML( $messageWidget );
119            return;
120        }
121
122        // TODO: Load only the styles for accordions. We need a RL module for that.
123        $out->addModuleStyles( [ 'codex-styles' ] );
124
125        $invitationListUsers = $this->invitationListStore->getInvitationListUsers( $list->getListID() );
126        [ $highlyRecommended, $recommended ] = self::splitUsersByScore( $invitationListUsers );
127        $numEditors = count( $highlyRecommended ) + count( $recommended );
128
129        $out->addWikiMsg(
130            'campaignevents-invitation-list-intro',
131            Message::numParam( $numEditors )
132        );
133
134        $noUsersWarning = '';
135        if ( $numEditors > 0 ) {
136            $allUsersByID = array_fill_keys( array_merge( $highlyRecommended, $recommended ), null );
137            // Warm up the cache for all users, even those that don't exist or are deleted.
138            $allUsernames = $this->centralUserLookup->getNamesIncludingDeletedAndSuppressed( $allUsersByID );
139            // But preload links only for those who actually exist.
140            $usernamesToPreload = array_filter(
141                $allUsernames,
142                static function ( $name ) {
143                    return $name !== CampaignsCentralUserLookup::USER_HIDDEN &&
144                        $name !== CampaignsCentralUserLookup::USER_NOT_FOUND;
145                }
146            );
147            $this->userLinker->preloadUserLinks( $usernamesToPreload );
148        } else {
149            $noUsersWarning = new MessageWidget( [
150                'type' => 'warning',
151                'label' => new HtmlSnippet( $this->msg( 'campaignevents-invitationlist-no-editors' )->parse() ),
152            ] );
153        }
154
155        $data = [
156            'noUsersWarning' => $noUsersWarning,
157            'highlyRecommendedLabel' => $this->msg( 'campaignevents-invitationlist-highly-recommended' )->text(),
158            'highlyRecommendedDesc' => $this->msg( 'campaignevents-invitationlist-highly-recommended-info' )->text(),
159            'highlyRecommended' => $this->getUserLinks( $highlyRecommended ),
160            'recommendedLabel' => $this->msg( 'campaignevents-invitationlist-recommended' )->text(),
161            'recommendedDesc' => $this->msg( 'campaignevents-invitationlist-recommended-info' )->text(),
162            'recommended' => $this->getUserLinks( $recommended ),
163            'worklistLabel' => $this->msg( 'campaignevents-invitationlist-worklist-label' )->text(),
164            'worklist' => $this->getWorklistLinks( $list->getListID() )
165        ];
166
167        $template = $this->templateParser->processTemplate( 'InvitationList', $data );
168        $out->addHTML( $template );
169    }
170
171    private function getWorklistLinks( int $invitationListID ): array {
172        $worklist = $this->invitationListStore->getWorklist( $invitationListID );
173        $pagesByWiki = $worklist->getPagesByWiki();
174        if ( count( $pagesByWiki ) !== 1 ) {
175            throw new LogicException( 'Expected a single wiki' );
176        }
177        $localPages = reset( $pagesByWiki );
178        $linkRenderer = $this->getLinkRenderer();
179        return array_map( static fn ( PageIdentity $page ) => $linkRenderer->makeLink( $page ), $localPages );
180    }
181
182    /**
183     * Given a list of potential invitees, group them by score range.
184     *
185     * @param array<int,int> $users
186     * @return int[][] First element is a list of highly recommended user IDs. Second element is a list of recommended
187     * user IDs.
188     * @phan-return array{0:list<int>,1:list<int>}
189     */
190    private static function splitUsersByScore( array $users ): array {
191        $highlyRecommended = [];
192        $recommended = [];
193        foreach ( $users as $userID => $score ) {
194            if ( $score >= self::HIGHLY_RECOMMENDED_MIN_SCORE ) {
195                $highlyRecommended[] = $userID;
196            } elseif ( $score >= self::RECOMMENDED_MIN_SCORE ) {
197                $recommended[] = $userID;
198            }
199        }
200
201        return [ $highlyRecommended, $recommended ];
202    }
203
204    /**
205     * @param int[] $userIDs
206     * @return string[]
207     */
208    private function getUserLinks( array $userIDs ): array {
209        $links = [];
210        foreach ( $userIDs as $userID ) {
211            try {
212                $links[] = $this->userLinker->generateUserLink( new CentralUser( $userID ) );
213            } catch ( CentralUserNotFoundException | HiddenCentralUserException $_ ) {
214                continue;
215            }
216        }
217        return $links;
218    }
219}