Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 698
0.00% covered (danger)
0.00%
0 / 23
CRAP
0.00% covered (danger)
0.00%
0 / 1
EventPageDecorator
0.00% covered (danger)
0.00%
0 / 698
0.00% covered (danger)
0.00%
0 / 23
9312
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
2
 decoratePage
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 maybeAddEnableRegistrationHeader
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 getEnableRegistrationHeader
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
2
 addRegistrationHeader
0.00% covered (danger)
0.00%
0 / 64
0.00% covered (danger)
0.00%
0 / 1
20
 getEventQuestionsData
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
56
 getHeaderElement
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
6
 getParticipantNoticeRow
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 getEventInfoHeaderRow
0.00% covered (danger)
0.00%
0 / 89
0.00% covered (danger)
0.00%
0 / 1
30
 getDetailsDialogContent
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
2
 getDetailsDialogOrganizers
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
12
 getDetailsDialogEventInfo
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 getDetailsDialogDates
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
2
 getDetailsDialogLocation
0.00% covered (danger)
0.00%
0 / 66
0.00% covered (danger)
0.00%
0 / 1
182
 getDetailsDialogChat
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
90
 getDetailsDialogParticipants
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
20
 getParticipantRows
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
56
 getActionElement
0.00% covered (danger)
0.00%
0 / 66
0.00% covered (danger)
0.00%
0 / 1
72
 getUserStatus
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
182
 getParticipantFooter
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 getParticipantRow
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
12
 makeDetailsDialogSection
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 getDetailsDialogWikis
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3declare( strict_types=1 );
4
5namespace MediaWiki\Extension\CampaignEvents\EventPage;
6
7use LogicException;
8use MediaWiki\Extension\CampaignEvents\Event\EventRegistration;
9use MediaWiki\Extension\CampaignEvents\Event\ExistingEventRegistration;
10use MediaWiki\Extension\CampaignEvents\Event\PageEventLookup;
11use MediaWiki\Extension\CampaignEvents\Formatters\EventFormatter;
12use MediaWiki\Extension\CampaignEvents\MWEntity\CampaignsCentralUserLookup;
13use MediaWiki\Extension\CampaignEvents\MWEntity\CampaignsPageFactory;
14use MediaWiki\Extension\CampaignEvents\MWEntity\CentralUser;
15use MediaWiki\Extension\CampaignEvents\MWEntity\CentralUserNotFoundException;
16use MediaWiki\Extension\CampaignEvents\MWEntity\HiddenCentralUserException;
17use MediaWiki\Extension\CampaignEvents\MWEntity\ICampaignsAuthority;
18use MediaWiki\Extension\CampaignEvents\MWEntity\ICampaignsPage;
19use MediaWiki\Extension\CampaignEvents\MWEntity\MWAuthorityProxy;
20use MediaWiki\Extension\CampaignEvents\MWEntity\UserLinker;
21use MediaWiki\Extension\CampaignEvents\MWEntity\UserNotGlobalException;
22use MediaWiki\Extension\CampaignEvents\MWEntity\WikiLookup;
23use MediaWiki\Extension\CampaignEvents\Organizers\OrganizersStore;
24use MediaWiki\Extension\CampaignEvents\Participants\Participant;
25use MediaWiki\Extension\CampaignEvents\Participants\ParticipantsStore;
26use MediaWiki\Extension\CampaignEvents\Participants\RegisterParticipantCommand;
27use MediaWiki\Extension\CampaignEvents\Participants\UnregisterParticipantCommand;
28use MediaWiki\Extension\CampaignEvents\Permissions\PermissionChecker;
29use MediaWiki\Extension\CampaignEvents\Questions\EventQuestionsRegistry;
30use MediaWiki\Extension\CampaignEvents\Special\AbstractEventRegistrationSpecialPage;
31use MediaWiki\Extension\CampaignEvents\Special\SpecialCancelEventRegistration;
32use MediaWiki\Extension\CampaignEvents\Special\SpecialEnableEventRegistration;
33use MediaWiki\Extension\CampaignEvents\Special\SpecialEventDetails;
34use MediaWiki\Extension\CampaignEvents\Special\SpecialRegisterForEvent;
35use MediaWiki\Extension\CampaignEvents\Time\EventTimeFormatter;
36use MediaWiki\Extension\CampaignEvents\Utils;
37use MediaWiki\Extension\CampaignEvents\Widget\TextWithIconWidget;
38use MediaWiki\Html\Html;
39use MediaWiki\Language\Language;
40use MediaWiki\Linker\LinkRenderer;
41use MediaWiki\Output\OutputPage;
42use MediaWiki\Page\ProperPageIdentity;
43use MediaWiki\Permissions\Authority;
44use MediaWiki\SpecialPage\SpecialPage;
45use MediaWiki\User\UserIdentity;
46use OOUI\ButtonWidget;
47use OOUI\Element;
48use OOUI\HorizontalLayout;
49use OOUI\HtmlSnippet;
50use OOUI\IconWidget;
51use OOUI\MessageWidget;
52use OOUI\PanelLayout;
53use OOUI\Tag;
54use UnexpectedValueException;
55use Wikimedia\Message\IMessageFormatterFactory;
56use Wikimedia\Message\ITextFormatter;
57use Wikimedia\Message\MessageValue;
58
59/**
60 * This service is used to add some widgets to the event page, like the registration header.
61 */
62class EventPageDecorator {
63    public const SERVICE_NAME = 'CampaignEventsEventPageDecorator';
64
65    private const ADDRESS_MAX_LENGTH = 30;
66    // See T304719#7909758 for how these numbers were chosen
67    private const ORGANIZERS_LIMIT = 4;
68    private const PARTICIPANTS_LIMIT = 10;
69
70    // Constants for the different statuses of a user wrt a given event registration
71    private const USER_STATUS_BLOCKED = 1;
72    private const USER_STATUS_ORGANIZER = 2;
73    private const USER_STATUS_PARTICIPANT_CAN_UNREGISTER = 3;
74    private const USER_STATUS_CAN_REGISTER = 4;
75    private const USER_STATUS_CANNOT_REGISTER_ENDED = 5;
76    private const USER_STATUS_CANNOT_REGISTER_CLOSED = 6;
77
78    private PageEventLookup $pageEventLookup;
79    private ParticipantsStore $participantsStore;
80    private OrganizersStore $organizersStore;
81    private PermissionChecker $permissionChecker;
82    private LinkRenderer $linkRenderer;
83    private CampaignsPageFactory $campaignsPageFactory;
84    private CampaignsCentralUserLookup $centralUserLookup;
85    private UserLinker $userLinker;
86    private EventTimeFormatter $eventTimeFormatter;
87    private EventPageCacheUpdater $eventPageCacheUpdater;
88    private EventQuestionsRegistry $eventQuestionsRegistry;
89    private WikiLookup $wikiLookup;
90
91    private Language $language;
92    private ICampaignsAuthority $authority;
93    private UserIdentity $viewingUser;
94    private OutputPage $out;
95    private ITextFormatter $msgFormatter;
96
97    /**
98     * @var bool|null Whether the user is registered publicly or privately. This value is lazy-loaded iff the user
99     * status is USER_STATUS_PARTICIPANT_CAN_UNREGISTER.
100     */
101    private ?bool $participantIsPublic = null;
102
103    /**
104     * @param PageEventLookup $pageEventLookup
105     * @param ParticipantsStore $participantsStore
106     * @param OrganizersStore $organizersStore
107     * @param PermissionChecker $permissionChecker
108     * @param IMessageFormatterFactory $messageFormatterFactory
109     * @param LinkRenderer $linkRenderer
110     * @param CampaignsPageFactory $campaignsPageFactory
111     * @param CampaignsCentralUserLookup $centralUserLookup
112     * @param UserLinker $userLinker
113     * @param EventTimeFormatter $eventTimeFormatter
114     * @param EventPageCacheUpdater $eventPageCacheUpdater
115     * @param EventQuestionsRegistry $eventQuestionsRegistry
116     * @param WikiLookup $wikiLookup
117     * @param Language $language
118     * @param Authority $viewingAuthority
119     * @param OutputPage $out
120     */
121    public function __construct(
122        PageEventLookup $pageEventLookup,
123        ParticipantsStore $participantsStore,
124        OrganizersStore $organizersStore,
125        PermissionChecker $permissionChecker,
126        IMessageFormatterFactory $messageFormatterFactory,
127        LinkRenderer $linkRenderer,
128        CampaignsPageFactory $campaignsPageFactory,
129        CampaignsCentralUserLookup $centralUserLookup,
130        UserLinker $userLinker,
131        EventTimeFormatter $eventTimeFormatter,
132        EventPageCacheUpdater $eventPageCacheUpdater,
133        EventQuestionsRegistry $eventQuestionsRegistry,
134        WikiLookup $wikiLookup,
135        Language $language,
136        Authority $viewingAuthority,
137        OutputPage $out
138    ) {
139        $this->pageEventLookup = $pageEventLookup;
140        $this->participantsStore = $participantsStore;
141        $this->organizersStore = $organizersStore;
142        $this->permissionChecker = $permissionChecker;
143        $this->linkRenderer = $linkRenderer;
144        $this->campaignsPageFactory = $campaignsPageFactory;
145        $this->centralUserLookup = $centralUserLookup;
146        $this->userLinker = $userLinker;
147        $this->eventTimeFormatter = $eventTimeFormatter;
148        $this->eventPageCacheUpdater = $eventPageCacheUpdater;
149        $this->eventQuestionsRegistry = $eventQuestionsRegistry;
150        $this->wikiLookup = $wikiLookup;
151
152        $this->language = $language;
153        $this->authority = new MWAuthorityProxy( $viewingAuthority );
154        $this->viewingUser = $viewingAuthority->getUser();
155        $this->out = $out;
156        $this->msgFormatter = $messageFormatterFactory->getTextFormatter( $language->getCode() );
157    }
158
159    /**
160     * This is the main entry point for this class. It adds all the necessary HTML (registration header, popup etc.)
161     * to the given OutputPage, as well as loading some JS/CSS resources.
162     *
163     * @param ProperPageIdentity $page
164     */
165    public function decoratePage( ProperPageIdentity $page ): void {
166        $registration = $this->pageEventLookup->getRegistrationForLocalPage( $page );
167
168        if ( $registration && $registration->getDeletionTimestamp() !== null ) {
169            return;
170        }
171
172        if ( $registration ) {
173            $this->addRegistrationHeader( $page, $registration );
174            $this->eventPageCacheUpdater->adjustCacheForPageWithRegistration( $this->out, $registration );
175        } else {
176            $campaignsPage = $this->campaignsPageFactory->newFromLocalMediaWikiPage( $page );
177            $this->maybeAddEnableRegistrationHeader( $campaignsPage );
178        }
179    }
180
181    /**
182     * @param ICampaignsPage $eventPage
183     */
184    private function maybeAddEnableRegistrationHeader( ICampaignsPage $eventPage ): void {
185        if ( !$this->permissionChecker->userCanEnableRegistration( $this->authority, $eventPage ) ) {
186            return;
187        }
188
189        $this->out->enableOOUI();
190        $this->out->addModuleStyles( [
191            'ext.campaignEvents.eventpage.styles',
192            'oojs-ui.styles.icons-editing-advanced',
193        ] );
194        $this->out->addModules( [ 'ext.campaignEvents.eventpage' ] );
195        // We pass this to the client to avoid hardcoding the name of the page field in JS. Apparently we can't use
196        // a RL callback for this because it doesn't provide the current page.
197        $enableRegistrationURL = SpecialPage::getTitleFor( SpecialEnableEventRegistration::PAGE_NAME )->getLocalURL( [
198            SpecialEnableEventRegistration::PAGE_FIELD_NAME => $eventPage->getPrefixedText()
199        ] );
200        $this->out->addJsConfigVars( [ 'wgCampaignEventsEnableRegistrationURL' => $enableRegistrationURL ] );
201        $this->out->addHTML( $this->getEnableRegistrationHeader( $enableRegistrationURL ) );
202    }
203
204    /**
205     * @param string $enableRegistrationURL
206     * @return Tag
207     */
208    private function getEnableRegistrationHeader( string $enableRegistrationURL ): Tag {
209        $organizerText = ( new Tag( 'div' ) )->appendContent(
210            $this->msgFormatter->format( MessageValue::new( 'campaignevents-eventpage-enableheader-organizer' ) )
211        )->setAttributes( [ 'class' => 'ext-campaignevents-eventpage-organizer-label' ] );
212
213        // Wrap it in a span for use inside a flex container, since the message contains HTML.
214        // XXX Can't use ITextFormatter here because the message contains HTML, see T260689
215        $infoMsg = ( new Tag( 'span' ) )->appendContent(
216            new HtmlSnippet( $this->out->msg( 'campaignevents-eventpage-enableheader-eventpage-desc' )->parse() )
217        );
218        $infoText = ( new Tag( 'div' ) )->appendContent(
219            new IconWidget( [ 'icon' => 'calendar', 'classes' => [ 'ext-campaignevents-eventpage-icon' ] ] ),
220            $infoMsg
221        )->setAttributes( [ 'class' => 'ext-campaignevents-eventpage-enableheader-message' ] );
222        $infoElement = ( new Tag( 'div' ) )->appendContent( $organizerText, $infoText );
223
224        $enableRegistrationBtn = new ButtonWidget( [
225            'flags' => [ 'primary', 'progressive' ],
226            'label' => $this->msgFormatter->format(
227                MessageValue::new( 'campaignevents-eventpage-enableheader-button-label' )
228            ),
229            'classes' => [ 'ext-campaignevents-eventpage-enable-registration-btn' ],
230            'href' => $enableRegistrationURL,
231        ] );
232
233        $layout = new PanelLayout( [
234            'content' => [ $infoElement, $enableRegistrationBtn ],
235            'padded' => true,
236            'framed' => true,
237            'expanded' => false,
238            'classes' => [ 'ext-campaignevents-eventpage-enableheader' ],
239        ] );
240
241        $layout->setAttributes( [
242            // Set the lang/dir explicitly, otherwise it will use that of the site/page language,
243            // not that of the interface.
244            'dir' => $this->language->getDir(),
245            'lang' => $this->language->getHtmlCode()
246        ] );
247        return $layout;
248    }
249
250    private function addRegistrationHeader( ProperPageIdentity $page, ExistingEventRegistration $registration ): void {
251        $this->out->setPreventClickjacking( true );
252        $this->out->enableOOUI();
253        $this->out->addModuleStyles( array_merge(
254            [
255                'ext.campaignEvents.eventpage.styles',
256                'oojs-ui.styles.icons-location',
257                'oojs-ui.styles.icons-interactions',
258                'oojs-ui.styles.icons-moderation',
259                'oojs-ui.styles.icons-user',
260                'oojs-ui.styles.icons-alerts',
261                'oojs-ui.styles.icons-wikimedia'
262            ],
263            UserLinker::MODULE_STYLES
264        ) );
265
266        $this->out->addModules( [ 'ext.campaignEvents.eventpage' ] );
267
268        try {
269            $centralUser = $this->centralUserLookup->newFromAuthority( $this->authority );
270            $curParticipant = $this->participantsStore->getEventParticipant(
271                $registration->getID(),
272                $centralUser,
273                true
274            );
275            $hasAggregatedAnswers = $this->participantsStore->userHasAggregatedAnswers(
276                $registration->getID(),
277                $centralUser
278            );
279        } catch ( UserNotGlobalException $_ ) {
280            $centralUser = null;
281            $curParticipant = null;
282            $hasAggregatedAnswers = false;
283        }
284
285        $userStatus = $this->getUserStatus( $registration, $centralUser, $curParticipant );
286
287        $this->out->addHTML( $this->getHeaderElement( $registration, $userStatus ) );
288        $this->out->addHTML(
289            $this->getDetailsDialogContent(
290                $page,
291                $registration,
292                $userStatus,
293                $curParticipant
294            )
295        );
296
297        $aggregationTimestamp = $curParticipant
298            ? Utils::getAnswerAggregationTimestamp( $curParticipant, $registration )
299            : null;
300
301        $session = $this->out->getRequest()->getSession();
302        $registrationUpdatedVal = $session
303            ->get( AbstractEventRegistrationSpecialPage::REGISTRATION_UPDATED_SESSION_KEY );
304        $registrationUpdatedWarnings = [];
305        $isNewRegistration = false;
306        if ( $registrationUpdatedVal ) {
307            // User just updated registration, show a success notification, plus any warnings.
308            $registrationUpdatedWarnings = $session
309                ->get( AbstractEventRegistrationSpecialPage::REGISTRATION_UPDATED_WARNINGS_SESSION_KEY, [] );
310            $isNewRegistration = $registrationUpdatedVal ===
311                AbstractEventRegistrationSpecialPage::REGISTRATION_UPDATED_SESSION_ENABLED;
312            $session->remove( AbstractEventRegistrationSpecialPage::REGISTRATION_UPDATED_SESSION_KEY );
313            $session->remove( AbstractEventRegistrationSpecialPage::REGISTRATION_UPDATED_WARNINGS_SESSION_KEY );
314        }
315
316        $this->out->addJsConfigVars( [
317            'wgCampaignEventsEventID' => $registration->getID(),
318            'wgCampaignEventsParticipantIsPublic' => $this->participantIsPublic,
319            'wgCampaignEventsEventQuestions' => $this->getEventQuestionsData( $registration, $curParticipant ),
320            'wgCampaignEventsAnswersAlreadyAggregated' => $hasAggregatedAnswers,
321            'wgCampaignEventsAggregationTimestamp' => $aggregationTimestamp,
322            'wgCampaignEventsRegistrationUpdated' => (bool)$registrationUpdatedVal,
323            'wgCampaignEventsIsNewRegistration' => $isNewRegistration,
324            'wgCampaignEventsRegistrationUpdatedWarnings' => $registrationUpdatedWarnings,
325        ] );
326    }
327
328    /**
329     * @param ExistingEventRegistration $registration
330     * @param Participant|null $participant
331     * @return array[]
332     */
333    private function getEventQuestionsData(
334        ExistingEventRegistration $registration,
335        ?Participant $participant
336    ): array {
337        $enabledQuestions = $registration->getParticipantQuestions();
338        $curAnswers = $participant ? $participant->getAnswers() : [];
339        $questionsToShow = EventQuestionsRegistry::getParticipantQuestionsToShow( $enabledQuestions, $curAnswers );
340
341        $questionsData = [];
342        $questionsAPI = $this->eventQuestionsRegistry->getQuestionsForAPI( $questionsToShow );
343        // Localise all messages to avoid having to do that in the client side.
344        foreach ( $questionsAPI as $questionAPIData ) {
345            $curQuestionData = [
346                'type' => $questionAPIData['type'],
347                'label' => $this->msgFormatter->format( MessageValue::new( $questionAPIData['label-message'] ) ),
348            ];
349            if ( isset( $questionAPIData['options-messages'] ) ) {
350                $curQuestionData['options'] = [];
351                foreach ( $questionAPIData['options-messages'] as $messageKey => $value ) {
352                    $message = $this->msgFormatter->format( MessageValue::new( $messageKey ) );
353                    $curQuestionData['options'][$messageKey] = [
354                        'value' => $value,
355                        'message' => $message
356                    ];
357                }
358            }
359            if ( isset( $questionAPIData['other-options'] ) ) {
360                $curQuestionData['other-options'] = [];
361                foreach ( $questionAPIData['other-options'] as $showIfVal => $otherOptData ) {
362                    $curQuestionData['other-options'][$showIfVal] = [
363                        'type' => $otherOptData['type'],
364                        'placeholder' => $this->msgFormatter->format(
365                            MessageValue::new( $otherOptData['label-message'] )
366                        ),
367                    ];
368                }
369            }
370            $questionsData[$questionAPIData['name']] = $curQuestionData;
371        }
372
373        return [
374            'questions' => $questionsData,
375            'answers' => $this->eventQuestionsRegistry->formatAnswersForAPI( $curAnswers, $enabledQuestions )
376        ];
377    }
378
379    /**
380     * Returns the header element.
381     *
382     * @param ExistingEventRegistration $registration
383     * @param int $userStatus One of the self::USER_STATUS_* constants
384     * @return Tag
385     */
386    private function getHeaderElement(
387        ExistingEventRegistration $registration,
388        int $userStatus
389    ): Tag {
390        $content = [];
391
392        $participantNoticeRow = $this->getParticipantNoticeRow( $userStatus );
393        if ( $participantNoticeRow ) {
394            $content[] = $participantNoticeRow;
395        }
396
397        $content[] = $this->getEventInfoHeaderRow( $registration, $userStatus );
398
399        $layout = new PanelLayout( [
400            'content' => $content,
401            'padded' => true,
402            'framed' => true,
403            'expanded' => false,
404            'classes' => [ 'ext-campaignevents-eventpage-header' ],
405        ] );
406
407        $layout->setAttributes( [
408            // Set the lang/dir explicitly, otherwise it will use that of the site/page language,
409            // not that of the interface.
410            'dir' => $this->language->getDir(),
411            'lang' => $this->language->getHtmlCode()
412        ] );
413
414        return $layout;
415    }
416
417    /**
418     * @param int $userStatus
419     * @return Tag|null
420     */
421    private function getParticipantNoticeRow( int $userStatus ): ?Tag {
422        if ( $userStatus !== self::USER_STATUS_PARTICIPANT_CAN_UNREGISTER ) {
423            return null;
424        }
425        $msg = $this->participantIsPublic
426            ? 'campaignevents-eventpage-header-registered-publicly'
427            : 'campaignevents-eventpage-header-registered-privately';
428        return new MessageWidget( [
429            'type' => 'success',
430            'label' => $this->msgFormatter->format( MessageValue::new( $msg ) ),
431            'inline' => true,
432            'classes' => [ 'ext-campaignevents-eventpage-participant-notice' ]
433        ] );
434    }
435
436    /**
437     * @param ExistingEventRegistration $registration
438     * @param int $userStatus
439     * @return Tag
440     */
441    private function getEventInfoHeaderRow(
442        ExistingEventRegistration $registration,
443        int $userStatus
444    ): Tag {
445        $eventID = $registration->getID();
446        $items = [];
447
448        $meetingType = $registration->getMeetingType();
449        if ( $meetingType === EventRegistration::MEETING_TYPE_ONLINE_AND_IN_PERSON ) {
450            $locationContent = $this->msgFormatter->format(
451                MessageValue::new( 'campaignevents-eventpage-header-type-online-and-in-person' )
452            );
453        } elseif ( $meetingType & EventRegistration::MEETING_TYPE_ONLINE ) {
454            $locationContent = $this->msgFormatter->format(
455                MessageValue::new( 'campaignevents-eventpage-header-type-online' )
456            );
457        } else {
458            // In-person event
459            $address = $registration->getMeetingAddress();
460            if ( $address !== null ) {
461                $locationContent = new Tag( 'div' );
462                $locationContent->setAttributes( [
463                    'dir' => Utils::guessStringDirection( $address )
464                ] );
465                $locationContent->addClasses( [ 'ext-campaignevents-eventpage-header-address' ] );
466                $locationContent->appendContent(
467                    $this->language->truncateForVisual( $address, self::ADDRESS_MAX_LENGTH )
468                );
469            } else {
470                $locationContent = $this->msgFormatter->format(
471                    MessageValue::new( 'campaignevents-eventpage-header-type-in-person' )
472                );
473            }
474        }
475        $items[] = new TextWithIconWidget( [
476            'icon' => 'mapPin',
477            'content' => $locationContent,
478            'label' => $this->msgFormatter->format(
479                MessageValue::new( 'campaignevents-eventpage-header-location-label' )
480            ),
481            'icon_classes' => [ 'ext-campaignevents-eventpage-icon' ],
482        ] );
483
484        $formattedStart = $this->eventTimeFormatter->formatStart( $registration, $this->language, $this->viewingUser );
485        $formattedEnd = $this->eventTimeFormatter->formatEnd( $registration, $this->language, $this->viewingUser );
486        $datesMsg = $this->msgFormatter->format(
487            MessageValue::new( 'campaignevents-eventpage-header-dates' )->params(
488                $formattedStart->getTimeAndDate(),
489                $formattedStart->getDate(),
490                $formattedStart->getTime(),
491                $formattedEnd->getTimeAndDate(),
492                $formattedEnd->getDate(),
493                $formattedEnd->getTime()
494            )
495        );
496        $formattedTimezone = EventTimeFormatter::wrapTimeZoneForConversion(
497            $this->eventTimeFormatter->formatTimezone( $registration, $this->viewingUser )
498        );
499        // XXX Can't use ITextFormatter due to parse()
500        $timezoneMsg = $this->out->msg( 'campaignevents-eventpage-header-timezone' )
501            ->params( $formattedTimezone )
502            ->parse();
503        $items[] = new TextWithIconWidget( [
504            'icon' => 'clock',
505            'content' => [
506                EventTimeFormatter::wrapRangeForConversion( $registration, $datesMsg ),
507                ( new Tag( 'div' ) )->appendContent( new HtmlSnippet( $timezoneMsg ) )
508            ],
509            'label' => $this->msgFormatter->format(
510                MessageValue::new( 'campaignevents-eventpage-header-dates-label' )
511            ),
512            'icon_classes' => [ 'ext-campaignevents-eventpage-icon' ],
513            'classes' => [ 'ext-campaignevents-eventpage-header-time' ],
514        ] );
515
516        $items[] = new TextWithIconWidget( [
517            'icon' => 'userGroup',
518            'content' => $this->msgFormatter->format(
519                MessageValue::new( 'campaignevents-eventpage-header-participants' )
520                    ->numParams( $this->participantsStore->getFullParticipantCountForEvent( $eventID ) )
521            ),
522            'label' => $this->msgFormatter->format(
523                MessageValue::new( 'campaignevents-eventpage-header-participants-label' )
524            ),
525            'icon_classes' => [ 'ext-campaignevents-eventpage-icon' ],
526        ] );
527
528        $btnContainer = ( new Tag( 'div' ) )
529            ->addClasses( [ 'ext-campaignevents-eventpage-header-buttons' ] );
530        $btnContainer->appendContent( new ButtonWidget( [
531            'framed' => false,
532            'flags' => [ 'progressive' ],
533            'label' => $this->msgFormatter->format( MessageValue::new( 'campaignevents-eventpage-header-details' ) ),
534            'classes' => [ 'ext-campaignevents-eventpage-details-btn' ],
535            'href' => SpecialPage::getTitleFor( SpecialEventDetails::PAGE_NAME, (string)$eventID )->getLocalURL(),
536        ] ) );
537
538        $actionElement = $this->getActionElement( $eventID, $userStatus );
539        if ( $actionElement ) {
540            $btnContainer->appendContent( $actionElement );
541        }
542
543        $items[] = $btnContainer;
544
545        return ( new Tag( 'div' ) )
546            ->addClasses( [ 'ext-campaignevents-eventpage-header-eventinfo' ] )
547            ->appendContent( ...$items );
548    }
549
550    /**
551     * Returns the content of the "more details" dialog. Unfortunately, we have to build it here rather then on the
552     * client side, for the following reasons:
553     * - There's no way to format dates according to the user preferences (T21992)
554     * - There's no easy way to get the directionality of a language (T181684)
555     * - Other utilities are missing (e.g., generating user links)
556     * - Secondarily, no need to make 3 API requests and worry about them failing.
557     *
558     * @param ProperPageIdentity $page
559     * @param ExistingEventRegistration $registration
560     * @param int $userStatus One of the self::USER_STATUS_* constants
561     * @param Participant|null $participant
562     * @return string
563     */
564    private function getDetailsDialogContent(
565        ProperPageIdentity $page,
566        ExistingEventRegistration $registration,
567        int $userStatus,
568        ?Participant $participant
569    ): string {
570        $eventID = $registration->getID();
571        $organizersCount = $this->organizersStore->getOrganizerCountForEvent( $eventID );
572
573        $eventInfoContainer = $this->getDetailsDialogEventInfo(
574            $page,
575            $registration,
576            $organizersCount,
577            $userStatus
578        );
579        $participantsContainer = $this->getDetailsDialogParticipants(
580            $eventID,
581            $participant,
582            $registration
583        );
584
585        $dialogContent = Html::element(
586            'h2',
587            [ 'class' => 'ext-campaignevents-detailsdialog-header' ],
588            $registration->getName()
589        );
590        $dialogContent .= $this->getDetailsDialogOrganizers(
591            $eventID,
592            $organizersCount
593        );
594        $dialogContent .= Html::rawElement(
595            'div',
596            [ 'class' => 'ext-campaignevents-detailsdialog-body-container' ],
597            $eventInfoContainer . $participantsContainer
598        );
599
600        return Html::rawElement(
601            'div',
602            [ 'id' => 'ext-campaignevents-eventpage-details-dialog-content' ],
603            $dialogContent
604        );
605    }
606
607    /**
608     * @param int $eventID
609     * @param int $organizersCount
610     * @return string
611     */
612    private function getDetailsDialogOrganizers(
613        int $eventID,
614        int $organizersCount
615    ): string {
616        $partialOrganizers = $this->organizersStore->getEventOrganizers( $eventID, self::ORGANIZERS_LIMIT );
617
618        $organizerElements = [];
619        foreach ( $partialOrganizers as $organizer ) {
620            $organizerElements[] = $this->userLinker->generateUserLinkWithFallback(
621                $organizer->getUser(),
622                $this->language->getCode()
623            );
624        }
625        // XXX We need to use OutputPage here because there's no supported way to change the format of
626        // MessageFormatterFactory...
627        $organizersStr = $this->out->msg( 'campaignevents-eventpage-dialog-organizers' )
628            ->rawParams( $this->language->commaList( $organizerElements ) )
629            ->numParams( count( $organizerElements ) )
630            ->escaped();
631        if ( count( $partialOrganizers ) < $organizersCount ) {
632            $organizersStr .= Html::rawElement(
633                'p',
634                [],
635                $this->linkRenderer->makeKnownLink(
636                    SpecialPage::getTitleFor( SpecialEventDetails::PAGE_NAME, (string)$eventID ),
637                    $this->msgFormatter->format(
638                        MessageValue::new( 'campaignevents-eventpage-dialog-organizers-view-all' )
639                    )
640                )
641            );
642        }
643
644        return Html::rawElement(
645            'div',
646            [ 'class' => 'ext-campaignevents-detailsdialog-organizers' ],
647            $organizersStr
648        );
649    }
650
651    private function getDetailsDialogEventInfo(
652        ProperPageIdentity $page,
653        ExistingEventRegistration $registration,
654        int $organizersCount,
655        int $userStatus
656    ): string {
657        $eventInfo = $this->getDetailsDialogDates( $registration );
658        $eventInfo .= $this->getDetailsDialogLocation(
659            $page,
660            $registration,
661            $organizersCount,
662            $userStatus
663        );
664        if ( $registration->getWikis() ) {
665            $eventInfo .= $this->getDetailsDialogWikis( $registration );
666        }
667        $eventInfo .= $this->getDetailsDialogChat( $page, $registration, $userStatus );
668
669        return Html::rawElement(
670            'div',
671            [ 'class' => 'ext-campaignevents-detailsdialog-eventinfo-container' ],
672            $eventInfo
673        );
674    }
675
676    /**
677     * @param ExistingEventRegistration $registration
678     * @return string
679     */
680    private function getDetailsDialogDates( ExistingEventRegistration $registration ): string {
681        $formattedStart = $this->eventTimeFormatter->formatStart( $registration, $this->language, $this->viewingUser );
682        $formattedEnd = $this->eventTimeFormatter->formatEnd( $registration, $this->language, $this->viewingUser );
683        $datesMsg = $this->msgFormatter->format(
684            MessageValue::new( 'campaignevents-eventpage-dialog-dates' )->params(
685                $formattedStart->getTimeAndDate(),
686                $formattedStart->getDate(),
687                $formattedStart->getTime(),
688                $formattedEnd->getTimeAndDate(),
689                $formattedEnd->getDate(),
690                $formattedEnd->getTime()
691            )
692        );
693        $formattedTimezone = EventTimeFormatter::wrapTimeZoneForConversion(
694            $this->eventTimeFormatter->formatTimezone( $registration, $this->viewingUser )
695        );
696        // XXX Can't use $msgFormatter due to parse()
697        $timezoneMsg = $this->out->msg( 'campaignevents-eventpage-dialog-timezone' )
698            ->params( $formattedTimezone )
699            ->parse();
700        return $this->makeDetailsDialogSection(
701            'clock',
702            [
703                EventTimeFormatter::wrapRangeForConversion( $registration, $datesMsg ),
704                ( new Tag( 'div' ) )->appendContent( new HtmlSnippet( $timezoneMsg ) )
705            ],
706            $this->msgFormatter->format(
707                MessageValue::new( 'campaignevents-eventpage-dialog-dates-label' )
708            ),
709            '',
710            [ 'ext-campaignevents-eventpage-detailsdialog-time' ]
711        );
712    }
713
714    private function getDetailsDialogLocation(
715        ProperPageIdentity $page,
716        ExistingEventRegistration $registration,
717        int $organizersCount,
718        int $userStatus
719    ): string {
720        $locationElements = [];
721        $onlineLocationElements = [];
722        if ( $registration->getMeetingType() & EventRegistration::MEETING_TYPE_ONLINE ) {
723            $onlineLocationElements[] = ( new Tag( 'h4' ) )
724                ->addClasses( [ 'ext-campaignevents-eventpage-detailsdialog-location-header' ] )
725                ->appendContent(
726                    $this->msgFormatter->format(
727                        MessageValue::new( 'campaignevents-eventpage-dialog-online-label' )
728                    ) );
729            $meetingURL = $registration->getMeetingURL();
730            if ( $meetingURL === null ) {
731                $linkContent = $this->msgFormatter->format(
732                    MessageValue::new( 'campaignevents-eventpage-dialog-link-not-available' )
733                        ->numParams( $organizersCount )
734                );
735            } elseif (
736                $userStatus === self::USER_STATUS_ORGANIZER ||
737                $userStatus === self::USER_STATUS_PARTICIPANT_CAN_UNREGISTER
738            ) {
739                $linkContent = new HtmlSnippet(
740                    $this->linkRenderer->makeExternalLink( $meetingURL, $meetingURL, $page )
741                );
742            } elseif ( $userStatus === self::USER_STATUS_CAN_REGISTER ) {
743                $linkContent = $this->msgFormatter->format(
744                    MessageValue::new( 'campaignevents-eventpage-dialog-link-register' )
745                );
746            } elseif ( $userStatus === self::USER_STATUS_BLOCKED ) {
747                $linkContent = $this->msgFormatter->format(
748                    MessageValue::new( 'campaignevents-event-details-sensitive-data-message-blocked-user' )
749                );
750            } elseif (
751                $userStatus === self::USER_STATUS_CANNOT_REGISTER_CLOSED ||
752                $userStatus === self::USER_STATUS_CANNOT_REGISTER_ENDED
753            ) {
754                $linkContent = '';
755            } else {
756                throw new LogicException( "Unexpected user status $userStatus" );
757            }
758            $onlineLocationElements[] = ( new Tag( 'p' ) )->appendContent( $linkContent );
759        }
760        if ( $registration->getMeetingType() & EventRegistration::MEETING_TYPE_IN_PERSON ) {
761            $rawAddress = $registration->getMeetingAddress();
762            $rawCountry = $registration->getMeetingCountry();
763            $addressElement = new Tag( 'p' );
764            $addressElement->addClasses( [ 'ext-campaignevents-eventpage-details-address' ] );
765            if ( $rawAddress || $rawCountry ) {
766                $address = $rawAddress . "\n" . $rawCountry;
767                $addressElement->setAttributes( [
768                    'dir' => Utils::guessStringDirection( $address )
769                ] );
770                $addressElement->appendContent( $address );
771            } else {
772                $addressElement->appendContent( $this->msgFormatter->format(
773                    MessageValue::new( 'campaignevents-eventpage-dialog-venue-not-available' )
774                        ->numParams( $organizersCount )
775                ) );
776            }
777            if ( $onlineLocationElements ) {
778                $inPersonLabel = ( new Tag( 'h4' ) )
779                    ->addClasses( [ 'ext-campaignevents-eventpage-detailsdialog-location-header' ] )
780                    ->appendContent( $this->msgFormatter->format(
781                        MessageValue::new( 'campaignevents-eventpage-dialog-in-person-label' )
782                    ) );
783                $locationElements[] = $inPersonLabel;
784                $locationElements[] = $addressElement;
785                $locationElements = array_merge( $locationElements, $onlineLocationElements );
786            } else {
787                $locationElements[] = $addressElement;
788            }
789        } else {
790            $locationElements = array_merge( $locationElements, $onlineLocationElements );
791        }
792        return $this->makeDetailsDialogSection(
793            'mapPin',
794            $locationElements,
795            $this->msgFormatter->format(
796                MessageValue::new( 'campaignevents-eventpage-dialog-location-label' )
797            )
798        );
799    }
800
801    private function getDetailsDialogChat(
802        ProperPageIdentity $page,
803        ExistingEventRegistration $registration,
804        int $userStatus
805    ): string {
806        $chatURL = $registration->getChatURL();
807        if ( $chatURL === null ) {
808            $chatURLContent = $this->msgFormatter->format(
809                MessageValue::new( 'campaignevents-eventpage-dialog-chat-not-available' )
810            );
811        } elseif (
812            $userStatus === self::USER_STATUS_ORGANIZER ||
813            $userStatus === self::USER_STATUS_PARTICIPANT_CAN_UNREGISTER
814        ) {
815            $chatURLContent = new HtmlSnippet(
816                $this->linkRenderer->makeExternalLink( $chatURL, $chatURL, $page )
817            );
818        } elseif ( $userStatus === self::USER_STATUS_CAN_REGISTER ) {
819            $chatURLContent = $this->msgFormatter->format(
820                MessageValue::new( 'campaignevents-eventpage-dialog-chat-register' )
821            );
822        } elseif (
823            $userStatus === self::USER_STATUS_BLOCKED
824        ) {
825            $chatURLContent = $this->msgFormatter->format(
826                MessageValue::new( 'campaignevents-event-details-sensitive-data-message-blocked-user' )
827            );
828        } elseif (
829            $userStatus === self::USER_STATUS_CANNOT_REGISTER_CLOSED ||
830            $userStatus === self::USER_STATUS_CANNOT_REGISTER_ENDED
831        ) {
832            $chatURLContent = '';
833        } else {
834            throw new LogicException( "Unexpected user status $userStatus" );
835        }
836
837        if ( $chatURLContent ) {
838            return $this->makeDetailsDialogSection(
839                'speechBubbles',
840                $chatURLContent,
841                $this->msgFormatter->format(
842                    MessageValue::new( 'campaignevents-eventpage-dialog-chat-label' )
843                )
844            );
845        }
846        return '';
847    }
848
849    /**
850     * @param int $eventID
851     * @param Participant|null $participant
852     * @param ExistingEventRegistration $registration
853     * @return string
854     */
855    private function getDetailsDialogParticipants(
856        int $eventID,
857        ?Participant $participant,
858        ExistingEventRegistration $registration
859    ): string {
860        $showPrivateParticipants = $this->permissionChecker->userCanViewPrivateParticipants(
861            $this->authority,
862            $registration
863        );
864        $participantsCount = $this->participantsStore->getFullParticipantCountForEvent( $eventID );
865        $privateCount = $this->participantsStore->getPrivateParticipantCountForEvent( $eventID );
866        $participantsList = $this->getParticipantRows(
867            $eventID,
868            $participant,
869            $showPrivateParticipants
870        );
871        $participantsFooter = '';
872        if ( self::PARTICIPANTS_LIMIT < $participantsCount ) {
873            $participantsFooter = $this->getParticipantFooter( $eventID );
874        }
875
876        $privateCountFooter = '';
877        if ( $privateCount > 0 ) {
878            $privateCountFooter = new Tag();
879            $privateCountFooter->addClasses( [
880                'ext-campaignevents-detailsdialog-private-participants-footer'
881            ] );
882            $privateCountIcon = new IconWidget( [
883                'icon' => 'lock'
884            ] );
885            $privateCountText = ( new Tag( 'span' ) )
886                ->addClasses( [ 'ext-campaignevents-detailsdialog-private-participants-footer-text' ] );
887            $privateCountText->appendContent(
888                $this->msgFormatter->format(
889                    MessageValue::new( 'campaignevents-eventpage-dialog-participants-private' )
890                        ->numParams( $privateCount )
891                )
892            );
893
894            $privateCountFooter->appendContent( [ $privateCountIcon, $privateCountText ] );
895        }
896
897        return $this->makeDetailsDialogSection(
898            'userGroup',
899            [ $participantsList ?: '', $participantsFooter ],
900            $this->msgFormatter->format(
901                MessageValue::new( 'campaignevents-eventpage-dialog-participants' )
902                    ->numParams( $participantsCount )
903            ),
904            $privateCountFooter
905        );
906    }
907
908    /**
909     * @param int $eventID
910     * @param Participant|null $curUserParticipant
911     * @param bool $showPrivateParticipants
912     *
913     * @return Tag|null
914     */
915    private function getParticipantRows(
916        int $eventID,
917        ?Participant $curUserParticipant,
918        bool $showPrivateParticipants
919    ): ?Tag {
920        $participantsList = ( new Tag( 'ul' ) )
921            ->addClasses( [ 'ext-campaignevents-detailsdialog-participants-list' ] );
922        $partialParticipants = $this->participantsStore->getEventParticipants(
923            $eventID,
924            $curUserParticipant ?
925                self::PARTICIPANTS_LIMIT - 1 :
926                self::PARTICIPANTS_LIMIT,
927            null,
928            null,
929            null,
930            $showPrivateParticipants,
931            $curUserParticipant ? [ $curUserParticipant->getUser()->getCentralID() ] : null
932        );
933
934        if ( !$curUserParticipant && !$partialParticipants ) {
935            return null;
936        }
937
938        if ( $curUserParticipant ) {
939            $participantsList->appendContent(
940                $this->getParticipantRow( $curUserParticipant )
941            );
942        }
943        foreach ( $partialParticipants as $participant ) {
944            $participantsList->appendContent(
945                $this->getParticipantRow( $participant )
946            );
947        }
948        return $participantsList;
949    }
950
951    /**
952     * Returns the "action" element for the header (that are also cloned into the popup). This can be a button for
953     * managing the event, or one to register for it. Or it can be a widget informing the user that they are already
954     * registered, with a button to unregister. There can also be no element if the user is not allowed to register.
955     *
956     * @param int $eventID
957     * @param int $userStatus
958     * @return Element|null
959     */
960    private function getActionElement( int $eventID, int $userStatus ): ?Element {
961        if ( $userStatus === self::USER_STATUS_BLOCKED ) {
962            return null;
963        }
964
965        if (
966            $userStatus === self::USER_STATUS_CANNOT_REGISTER_CLOSED ||
967            $userStatus === self::USER_STATUS_CANNOT_REGISTER_ENDED
968        ) {
969            $msgKey = $userStatus === self::USER_STATUS_CANNOT_REGISTER_CLOSED
970                ? 'campaignevents-eventpage-btn-registration-closed'
971                : 'campaignevents-eventpage-btn-event-ended';
972            return new ButtonWidget( [
973                'disabled' => true,
974                'label' => $this->msgFormatter->format( MessageValue::new( $msgKey ) ),
975                'classes' => [
976                    'ext-campaignevents-eventpage-action-element'
977                ],
978            ] );
979        }
980
981        if ( $userStatus === self::USER_STATUS_ORGANIZER ) {
982            return new ButtonWidget( [
983                'flags' => [ 'progressive' ],
984                'label' => $this->msgFormatter->format( MessageValue::new( 'campaignevents-eventpage-btn-manage' ) ),
985                'classes' => [
986                    'ext-campaignevents-eventpage-action-element',
987                ],
988                'href' => SpecialPage::getTitleFor(
989                    SpecialEventDetails::PAGE_NAME,
990                    (string)$eventID
991                )->getLocalURL(),
992            ] );
993        }
994
995        if ( $userStatus === self::USER_STATUS_PARTICIPANT_CAN_UNREGISTER ) {
996            $unregisterURL = SpecialPage::getTitleFor(
997                SpecialCancelEventRegistration::PAGE_NAME,
998                (string)$eventID
999            )->getLocalURL();
1000
1001            // Note that this will be replaced with a ButtonMenuSelectWidget in JS.
1002            return new HorizontalLayout( [
1003                'items' => [
1004                    new ButtonWidget( [
1005                        'flags' => [ 'progressive' ],
1006                        'label' => $this->msgFormatter->format(
1007                            MessageValue::new( 'campaignevents-eventpage-btn-edit' )
1008                        ),
1009                        'href' => SpecialPage::getTitleFor( SpecialRegisterForEvent::PAGE_NAME, (string)$eventID )
1010                            ->getLocalURL(),
1011                    ] ),
1012                    new ButtonWidget( [
1013                        'flags' => [ 'destructive' ],
1014                        'label' => $this->msgFormatter->format(
1015                            MessageValue::new( 'campaignevents-eventpage-btn-cancel' )
1016                        ),
1017                        'href' => $unregisterURL,
1018                    ] )
1019                ],
1020                'classes' => [
1021                    'ext-campaignevents-eventpage-manage-registration-layout',
1022                    'ext-campaignevents-eventpage-action-element'
1023                ]
1024            ] );
1025        }
1026
1027        if ( $userStatus === self::USER_STATUS_CAN_REGISTER ) {
1028            return new ButtonWidget( [
1029                'flags' => [ 'primary', 'progressive' ],
1030                'label' => $this->msgFormatter->format( MessageValue::new( 'campaignevents-eventpage-btn-register' ) ),
1031                'classes' => [
1032                    'ext-campaignevents-eventpage-register-btn',
1033                    'ext-campaignevents-eventpage-action-element'
1034                ],
1035                'href' => SpecialPage::getTitleFor( SpecialRegisterForEvent::PAGE_NAME, (string)$eventID )
1036                    ->getLocalURL(),
1037            ] );
1038        }
1039        throw new LogicException( "Unexpected user status $userStatus" );
1040    }
1041
1042    /**
1043     * @param ExistingEventRegistration $event
1044     * @param CentralUser|null $centralUser Corresponding to $this->authority, if it exists
1045     * @param Participant|null $participant For $centralUser, if they're a participant
1046     * @return int One of the SELF::USER_STATUS_* constants
1047     */
1048    private function getUserStatus(
1049        ExistingEventRegistration $event,
1050        ?CentralUser $centralUser,
1051        ?Participant $participant
1052    ): int {
1053        // Do not check user blocks or other user-dependent conditions for logged-out users, so that we can serve the
1054        // same (cached) version of the page to everyone. Also, even if the IP is blocked, the user might have an
1055        // account that they can log into, so showing the button is fine.
1056        if ( $centralUser ) {
1057            if ( $this->authority->isSitewideBlocked() ) {
1058                return self::USER_STATUS_BLOCKED;
1059            }
1060
1061            if ( $this->organizersStore->isEventOrganizer( $event->getID(), $centralUser ) ) {
1062                return self::USER_STATUS_ORGANIZER;
1063            }
1064
1065            if ( $participant ) {
1066                $checkUnregistrationAllowedVal = UnregisterParticipantCommand::checkIsUnregistrationAllowed( $event );
1067                switch ( $checkUnregistrationAllowedVal->getValue() ) {
1068                    case UnregisterParticipantCommand::CANNOT_UNREGISTER_DELETED:
1069                        throw new UnexpectedValueException( "Registration should not be deleted at this point." );
1070                    case UnregisterParticipantCommand::CAN_UNREGISTER:
1071                        $this->participantIsPublic = !$participant->isPrivateRegistration();
1072                        return self::USER_STATUS_PARTICIPANT_CAN_UNREGISTER;
1073                    default:
1074                        throw new UnexpectedValueException( "Unexpected value $checkUnregistrationAllowedVal" );
1075                }
1076            }
1077        }
1078
1079        // User is logged-in and not already participating, or logged-out, in which case we'll know better
1080        // once they log in.
1081        $checkRegistrationAllowedVal = RegisterParticipantCommand::checkIsRegistrationAllowed(
1082            $event,
1083            RegisterParticipantCommand::REGISTRATION_NEW
1084        );
1085        switch ( $checkRegistrationAllowedVal->value ) {
1086            case RegisterParticipantCommand::CANNOT_REGISTER_DELETED:
1087                throw new UnexpectedValueException( "Registration should not be deleted at this point." );
1088            case RegisterParticipantCommand::CANNOT_REGISTER_ENDED:
1089                return self::USER_STATUS_CANNOT_REGISTER_ENDED;
1090            case RegisterParticipantCommand::CANNOT_REGISTER_CLOSED:
1091                return self::USER_STATUS_CANNOT_REGISTER_CLOSED;
1092            case RegisterParticipantCommand::CAN_REGISTER:
1093                return self::USER_STATUS_CAN_REGISTER;
1094            default:
1095                throw new UnexpectedValueException( "Unexpected value $checkRegistrationAllowedVal" );
1096        }
1097    }
1098
1099    /**
1100     * @param int $eventID
1101     * @return Tag
1102     */
1103    private function getParticipantFooter( int $eventID ): Tag {
1104        $viewParticipantsURL = SpecialPage::getTitleFor( SpecialEventDetails::PAGE_NAME, (string)$eventID )
1105            ->getLocalURL( [ 'tab' => SpecialEventDetails::PARTICIPANTS_PANEL ] );
1106        return new ButtonWidget( [
1107            'framed' => false,
1108            'flags' => [ 'progressive' ],
1109            'label' => $this->msgFormatter->format(
1110                MessageValue::new( 'campaignevents-eventpage-dialog-participants-view-list' )
1111            ),
1112            'href' => $viewParticipantsURL,
1113        ] );
1114    }
1115
1116    /**
1117     * @param Participant $participant
1118     * @return Tag
1119     */
1120    private function getParticipantRow( Participant $participant ): Tag {
1121        $usernameElement = new HtmlSnippet(
1122            $this->userLinker->generateUserLinkWithFallback(
1123                $participant->getUser(),
1124                $this->language->getCode()
1125            )
1126        );
1127
1128        $tag = ( new Tag( 'li' ) )
1129            ->appendContent( $usernameElement );
1130
1131        if ( $participant->isPrivateRegistration() ) {
1132            try {
1133                $userName = $this->centralUserLookup->getUserName( $participant->getUser() );
1134            } catch ( CentralUserNotFoundException | HiddenCentralUserException $_ ) {
1135                // Hack: use an invalid username to force unspecified gender
1136                $userName = '@';
1137            }
1138            $labelText = $this->msgFormatter->format(
1139                MessageValue::new( 'campaignevents-eventpage-dialog-private-registration-label' )
1140                    ->params( $userName )
1141            );
1142            $tag->appendContent( new IconWidget( [
1143                    'icon' => 'lock',
1144                    'title' => $labelText,
1145                    'label' => $labelText,
1146                    'classes' => [ 'ext-campaignevents-event-details-participants-private-icon' ]
1147                ] )
1148            );
1149        }
1150
1151        return $tag;
1152    }
1153
1154    /**
1155     * @param string $icon
1156     * @param string|Tag|array|HtmlSnippet $content
1157     * @param string $label
1158     * @param string|Tag|array $footer
1159     * @param string[] $classes
1160     * @return string
1161     */
1162    private function makeDetailsDialogSection(
1163        string $icon,
1164        $content,
1165        string $label,
1166        $footer = '',
1167        array $classes = []
1168    ): string {
1169        $iconWidget = new IconWidget( [
1170            'icon' => $icon,
1171            'classes' => [ 'ext-campaignevents-eventpage-detailsdialog-section-icon' ]
1172        ] );
1173        $header = ( new Tag( 'h3' ) )
1174            ->appendContent( $iconWidget, ( new Tag( 'span' ) )->appendContent( $label ) )
1175            ->addClasses( [ 'ext-campaignevents-eventpage-detailsdialog-section-header' ] );
1176
1177        $contentTag = ( new Tag( 'div' ) )
1178            ->appendContent( $content )
1179            ->addClasses( [ 'ext-campaignevents-eventpage-detailsdialog-section-content', ...$classes ] );
1180
1181        return (string)( new Tag( 'div' ) )
1182            ->appendContent( $header, $contentTag, $footer );
1183    }
1184
1185    /**
1186     * @param ExistingEventRegistration $registration
1187     * @return string
1188     */
1189    private function getDetailsDialogWikis( ExistingEventRegistration $registration ): string {
1190        $content = EventFormatter::formatWikis(
1191            $registration,
1192            $this->msgFormatter,
1193            $this->wikiLookup,
1194            $this->language,
1195            $this->linkRenderer,
1196            'campaignevents-eventpage-all-wikis',
1197            'campaignevents-eventpage-wikis-more',
1198        );
1199        return $this->makeDetailsDialogSection(
1200            $this->wikiLookup->getWikiIcon( $registration->getWikis() ),
1201            $content,
1202            $this->msgFormatter->format(
1203                MessageValue::new( 'campaignevents-eventpage-dialog-wikis-label' )
1204            )
1205        );
1206    }
1207}