Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
14.78% covered (danger)
14.78%
34 / 230
0.00% covered (danger)
0.00%
0 / 22
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialWelcomeSurvey
14.78% covered (danger)
14.78%
34 / 230
0.00% covered (danger)
0.00%
0 / 22
1298.17
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDisplayFormat
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
75.00% covered (warning)
75.00%
9 / 12
0.00% covered (danger)
0.00%
0 / 1
6.56
 getDescription
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 doesWrites
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 processSkip
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 getFormFields
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
42
 alterForm
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
2
 onSubmit
80.65% covered (warning)
80.65%
25 / 31
0.00% covered (danger)
0.00%
0 / 1
3.07
 showConfirmationPage
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 showHomepageAwareConfirmationPage
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 showDefaultConfirmationPage
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
6
 getCloseButtonHtml
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 getConfirmationButtonsWrapper
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getHomepageAwareActionButtons
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 getHomepageButton
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 redirect
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 buildGettingStartedLinks
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
20
 buildSidebar
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
2
 loadDependencies
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 initializeWelcomeSurveyLogger
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace GrowthExperiments\Specials;
4
5use GrowthExperiments\EventLogging\WelcomeSurveyLogger;
6use GrowthExperiments\HomepageHooks;
7use GrowthExperiments\Util;
8use GrowthExperiments\WelcomeSurveyFactory;
9use MediaWiki\Config\ConfigException;
10use MediaWiki\Html\Html;
11use MediaWiki\HTMLForm\HTMLForm;
12use MediaWiki\SpecialPage\FormSpecialPage;
13use MediaWiki\SpecialPage\SpecialPageFactory;
14use MediaWiki\Status\Status;
15use MediaWiki\Title\Title;
16use MediaWiki\Utils\MWTimestamp;
17
18class SpecialWelcomeSurvey extends FormSpecialPage {
19
20    public const ACTION_VIEW = 'view';
21    public const ACTION_SUBMIT_ATTEMPT = 'submit_attempt';
22    public const ACTION_SAVE = 'save';
23    public const ACTION_SKIP = 'skip';
24    public const ACTION_SUBMIT_SUCCESS = 'submit_success';
25    public const ACTION_SHOW_CONFIRMATION_PAGE = 'show_confirmation_page';
26
27    private string $groupName;
28    private SpecialPageFactory $specialPageFactory;
29    private WelcomeSurveyFactory $welcomeSurveyFactory;
30    private WelcomeSurveyLogger $welcomeSurveyLogger;
31
32    /**
33     * @param SpecialPageFactory $specialPageFactory
34     * @param WelcomeSurveyFactory $welcomeSurveyFactory
35     * @param WelcomeSurveyLogger $welcomeSurveyLogger
36     */
37    public function __construct(
38        SpecialPageFactory $specialPageFactory,
39        WelcomeSurveyFactory $welcomeSurveyFactory,
40        WelcomeSurveyLogger $welcomeSurveyLogger
41    ) {
42        parent::__construct( 'WelcomeSurvey', '', false );
43        $this->specialPageFactory = $specialPageFactory;
44        $this->welcomeSurveyFactory = $welcomeSurveyFactory;
45        $this->welcomeSurveyLogger = $welcomeSurveyLogger;
46    }
47
48    /** @inheritDoc */
49    protected function getGroupName() {
50        return 'growth-tools';
51    }
52
53    /**
54     * @inheritDoc
55     */
56    protected function getDisplayFormat() {
57        return 'ooui';
58    }
59
60    /**
61     * @inheritDoc
62     */
63    public function execute( $par ) {
64        $this->initializeWelcomeSurveyLogger();
65        if ( !$par && !$this->getRequest()->wasPosted() ) {
66            $this->welcomeSurveyLogger->logInteraction( self::ACTION_VIEW );
67        }
68        if ( !$par && $this->getRequest()->wasPosted() ) {
69            $this->welcomeSurveyLogger->logInteraction( self::ACTION_SUBMIT_ATTEMPT );
70        }
71        $this->requireNamedUser();
72        if ( $par === 'skip' ) {
73            $this->processSkip();
74            return;
75        }
76        $this->getOutput()->addModuleStyles( 'ext.growthExperiments.Account.styles' );
77        $this->getOutput()->addJsConfigVars( 'welcomesurvey', true );
78        parent::execute( $par );
79    }
80
81    /**
82     * Overridden in order to inject the current user's name as message parameter
83     *
84     * @inheritDoc
85     */
86    public function getDescription() {
87        return $this->msg( strtolower( $this->mName ) )
88            ->params( $this->getUser()->getName() );
89    }
90
91    /**
92     * @inheritDoc
93     */
94    public function doesWrites() {
95        return true;
96    }
97
98    /**
99     * Handle the /skip endpoint, used to dismiss reminder notices about the survey
100     * as a no-JS fallback to the /growthexperiments/v0/welcomesurvey/skip API.
101     */
102    protected function processSkip() {
103        $output = $this->getOutput();
104
105        // Don't do writes on GET. There is no legitimate way to get here with a GET query
106        // so we don't bother with error processing.
107        if ( $this->getRequest()->wasPosted() ) {
108            $csrfToken = $this->getRequest()->getRawVal( 'token' );
109            if ( !$this->getContext()->getCsrfTokenSet()->matchToken( $csrfToken, 'welcomesurvey' ) ) {
110                $output->showErrorPage( 'sessionfailure-title', 'sessionfailure', [],
111                    $this->specialPageFactory->getTitleForAlias( 'Homepage' ) );
112                return;
113            }
114
115            $welcomeSurvey = $this->welcomeSurveyFactory->newWelcomeSurvey( $this->getContext() );
116            $welcomeSurvey->dismiss();
117        }
118
119        $output->redirect(
120            $this->specialPageFactory->getTitleForAlias( 'Homepage' )->getFullURL()
121        );
122    }
123
124    /**
125     * Get an HTMLForm descriptor array
126     * @return array
127     */
128    protected function getFormFields() {
129        $welcomeSurvey = $this->welcomeSurveyFactory->newWelcomeSurvey( $this->getContext() );
130        $this->groupName = $welcomeSurvey->getGroup( true );
131        $questions = $welcomeSurvey->getQuestions( $this->groupName );
132
133        if ( !$questions ) {
134            // redirect away
135            $request = $this->getRequest();
136            $this->redirect(
137                $request->getVal( 'returnto' ),
138                $request->getVal( 'returntoquery' )
139            );
140            return [];
141        }
142
143        // Transform questions
144        foreach ( $questions as &$question ) {
145            // Add select options for 'placeholder' and 'other'
146            if ( $question[ 'type' ] === 'select' ) {
147                if ( isset( $question[ 'placeholder-message' ] ) ) {
148                    // Add 'placeholder' as the first options
149                    $question['options-messages'] = [ $question['placeholder-message'] => 'placeholder' ] +
150                        $question['options-messages'];
151                }
152                if ( isset( $question[ 'other-message' ] ) ) {
153                    // Add 'other' as the last options
154                    $question['options-messages'] += [ $question[ 'other-message' ] => 'other' ];
155                }
156            }
157        }
158        $this->loadDependencies( $questions );
159        return $questions;
160    }
161
162    /**
163     * @inheritDoc
164     */
165    protected function alterForm( HTMLForm $form ) {
166        $form->setId( 'welcome-survey-form' );
167
168        // subtitle
169        $form->addHeaderHtml(
170            Html::rawElement(
171                'div',
172                [ 'class' => 'welcomesurvey-subtitle' ],
173                $this->msg( 'welcomesurvey-subtitle' )->parse()
174            )
175        );
176
177        $form->addHiddenField( '_render_date', MWTimestamp::now() );
178        $form->addHiddenField( '_group', $this->groupName );
179        $form->addHiddenField( '_returnto', $this->getRequest()->getVal( 'returnto' ) );
180        $form->addHiddenField( '_welcomesurveytoken', $this->getRequest()->getVal( '_welcomesurveytoken' ) );
181
182        // save button
183        $form->setSubmitTextMsg( 'welcomesurvey-save-btn' );
184        $form->setSubmitName( 'save' );
185
186        // skip button
187        $form->addButton( [
188            'name' => 'skip',
189            'value' => 'skip',
190            'framed' => false,
191            'flags' => 'destructive',
192            'attribs' => [ 'class' => 'welcomesurvey-skip-btn' ],
193            'label-message' => [ 'welcomesurvey-skip-btn', $this->getUser()->getName() ],
194        ] );
195
196        // sidebar
197        $form->setPostHtml( $this->buildSidebar() );
198    }
199
200    /**
201     * Process the form on POST submission.
202     * @param array $data
203     * @return bool|string|array|Status As documented for HTMLForm::trySubmit.
204     */
205    public function onSubmit( array $data ) {
206        $this->initializeWelcomeSurveyLogger();
207
208        $request = $this->getRequest();
209        $save = $request->getBool( 'save' );
210        $group = $request->getVal( '_group' );
211        $token = $request->getVal( '_welcomesurveytoken' );
212        $renderDate = $request->getVal( '_render_date' );
213        $redirectParams = wfCgiToArray( $request->getVal( 'redirectparams', '' ) );
214        $returnTo = $redirectParams[ 'returnto' ] ?? $request->getVal( '_returnto', '' );
215        $returnToQuery = $redirectParams[ 'returntoquery' ] ?? '';
216
217        $welcomeSurvey = $this->welcomeSurveyFactory->newWelcomeSurvey( $this->getContext() );
218        $welcomeSurvey->handleResponses(
219            $data,
220            $save,
221            $group,
222            $renderDate
223        );
224
225        $this->welcomeSurveyLogger->logInteraction( self::ACTION_SUBMIT_SUCCESS );
226
227        if ( $save ) {
228            // show confirmation page
229            $returnToQueryArray = wfCgiToArray( $returnToQuery );
230            $returnToQueryArray['_welcomesurveytoken'] = $token;
231            $returnToQuery = wfArrayToCgi( $returnToQueryArray );
232            $this->welcomeSurveyLogger->logInteraction( self::ACTION_SAVE );
233            $this->showConfirmationPage( $returnTo, $returnToQuery );
234        } else {
235            // redirect to pre-createaccount page with query
236            $returnToQueryArray = wfCgiToArray( $returnToQuery );
237            $returnToQueryArray['_welcomesurveytoken'] = $token;
238            if ( HomepageHooks::isHomepageEnabled( $this->getUser() ) ) {
239                $returnToQueryArray['source'] = 'welcomesurvey-originalcontext';
240            }
241            $returnToQuery = wfArrayToCgi( $returnToQueryArray );
242            $this->welcomeSurveyLogger->logInteraction( self::ACTION_SKIP );
243            $this->redirect( $returnTo, $returnToQuery );
244        }
245
246        return true;
247    }
248
249    private function showConfirmationPage( $to, $query ) {
250        $this->getOutput()->setPageTitleMsg( $this->msg( 'welcomesurvey-save-confirmation-title' ) );
251        HomepageHooks::isHomepageEnabled( $this->getUser() ) ?
252            $this->showHomepageAwareConfirmationPage( $to, $query ) :
253            $this->showDefaultConfirmationPage( $to, $query );
254        $this->welcomeSurveyLogger->logInteraction( self::ACTION_SHOW_CONFIRMATION_PAGE );
255    }
256
257    private function showHomepageAwareConfirmationPage( $to, $query ) {
258        $title = Title::newFromText( $to ) ?: $this->specialPageFactory->getTitleForAlias( 'Homepage' );
259        if ( $title->isMainPage() ) {
260            $title = $this->specialPageFactory->getTitleForAlias( 'Homepage' );
261        }
262
263        $this->getOutput()->addHTML(
264            Html::rawElement(
265                'div',
266                [ 'class' => 'welcomesurvey-confirmation' ],
267                $this->msg( 'welcomesurvey-save-confirmation-text' )
268                    ->parseAsBlock() .
269                $this->getHomepageAwareActionButtons( $title, $query )
270            )
271        );
272    }
273
274    private function showDefaultConfirmationPage( $to, $query ) {
275        $this->getOutput()->addHTML(
276            Html::rawElement(
277                'div',
278                [ 'class' => 'welcomesurvey-confirmation' ],
279                $this->msg( 'welcomesurvey-save-confirmation-text' )
280                    ->parseAsBlock() .
281                Html::element(
282                    'div',
283                    [ 'class' => 'welcomesurvey-confirmation-editing-title' ],
284                    $this->msg( 'welcomesurvey-sidebar-editing-title' )->text()
285                ) .
286                $this->msg( 'welcomesurvey-sidebar-editing-text' )
287                    ->params( $this->getUser()->getName() )
288                    ->parseAsBlock() .
289                $this->buildGettingStartedLinks( 'confirmation' ) .
290                $this->getCloseButtonHtml( Title::newFromText( $to ) ?: Title::newMainPage(), $query )
291            )
292        );
293    }
294
295    /**
296     * @param Title $title
297     * @param string $query
298     * @return string
299     * @throws ConfigException
300     */
301    private function getCloseButtonHtml( Title $title, $query ) {
302        return $this->getConfirmationButtonsWrapper(
303            Html::linkButton(
304                $this->msg( 'welcomesurvey-close-btn', $title->getPrefixedText() )->text(),
305                [
306                    'href' => $title->getLinkURL( $query ),
307                    'class' => 'mw-ui-button mw-ui-progressive'
308                ]
309            )
310        );
311    }
312
313    private function getConfirmationButtonsWrapper( $rawHtml ) {
314        return Html::rawElement(
315            'div',
316            [ 'class' => 'welcomesurvey-confirmation-buttons' ],
317            $rawHtml
318        );
319    }
320
321    private function getHomepageAwareActionButtons( Title $title, $query ) {
322        if ( $title->isSpecial( 'Homepage' ) ) {
323            return $this->getConfirmationButtonsWrapper( $this->getHomepageButton() );
324        }
325        $queryArray = wfCgiToArray( $query );
326        $queryArray['source'] = 'welcomesurvey-originalcontext';
327        return $this->getConfirmationButtonsWrapper(
328            Html::linkButton( $this->msg( 'welcomesurvey-close-btn', $title )->text(), [
329                'href' => $title->getLinkURL( wfArrayToCgi( $queryArray ) ),
330                'class' => 'mw-ui-button mw-ui-safe'
331            ] ) .
332            $this->getHomepageButton()
333        );
334    }
335
336    private function getHomepageButton() {
337        return Html::linkButton(
338            $this->msg( 'growthexperiments-homepage-welcomesurvey-default-close',
339                $this->getUser()->getName()
340            )->text(),
341            [
342                'href' => $this->specialPageFactory->getTitleForAlias( 'Homepage' )->getLinkURL(
343                    [ 'source' => 'specialwelcomesurvey' ]
344                ),
345                'class' => 'mw-ui-button mw-ui-progressive mw-ge-welcomesurvey-homepage-button'
346            ]
347        );
348    }
349
350    private function redirect( $to, $query ) {
351        $title = Title::newFromText( $to ) ?: Title::newMainPage();
352        $this->getOutput()->redirect( $title->getFullUrlForRedirect( $query ) );
353    }
354
355    private function buildGettingStartedLinks( $source ) {
356        $html = '<ul class="welcomesurvey-gettingstarted-links">';
357        for ( $i = 1; $i <= 4; $i++ ) {
358            $text = $this->msg( "welcomesurvey-sidebar-editing-link$i-text" );
359            $title = $this->msg( "welcomesurvey-sidebar-editing-link$i-title" );
360            if ( $text->isDisabled() || $title->isDisabled() ) {
361                continue;
362            }
363
364            $url = Title::newFromText( $title->text() )->getLinkURL( [ 'source' => $source ] );
365            $html .= Html::rawElement(
366                'li',
367                [ 'class' => 'mw-parser-output' ],
368                Html::element(
369                    'a',
370                    [
371                        'href' => $url,
372                        'target' => '_blank',
373                        'class' => 'external',
374                    ],
375                    $text->text()
376                )
377            );
378        }
379        $html .= '</ul>';
380        return $html;
381    }
382
383    private function buildSidebar() {
384        return Html::rawElement(
385            'div',
386            [ 'class' => 'welcomesurvey-sidebar' ],
387            Html::rawElement(
388                'div',
389                [ 'class' => 'welcomesurvey-sidebar-section' ],
390                Html::element(
391                    'div',
392                    [ 'class' => 'welcomesurvey-sidebar-section-title' ],
393                    $this->msg( 'welcomesurvey-sidebar-editing-title' )->text()
394                ) .
395                Html::rawElement(
396                    'div',
397                    [ 'class' => 'welcomesurvey-sidebar-section-text' ],
398                    $this->msg( 'welcomesurvey-sidebar-editing-text' )
399                        ->params( $this->getUser()->getName() )
400                        ->parseAsBlock()
401                ) .
402                $this->buildGettingStartedLinks( 'survey' )
403            )
404        );
405    }
406
407    /**
408     * Load ResourceLoader module dependencies defined by questions.
409     * @param array $questions
410     */
411    private function loadDependencies( array $questions ) {
412        array_walk( $questions, function ( $question ) {
413            $this->getOutput()->addModules( $question['dependencies']['modules'] ?? '' );
414        } );
415    }
416
417    private function initializeWelcomeSurveyLogger(): void {
418        $this->welcomeSurveyLogger->initialize(
419            $this->getRequest(),
420            $this->getUser(),
421            Util::isMobile( $this->getSkin() )
422        );
423    }
424
425}