Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 92
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
CampaignBenefitsBlock
0.00% covered (danger)
0.00%
0 / 92
0.00% covered (danger)
0.00%
0 / 6
506
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getLegalFooter
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getHtml
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 getCampaignTemplateHtml
0.00% covered (danger)
0.00%
0 / 54
0.00% covered (danger)
0.00%
0 / 1
156
 getCampaignValue
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getVideo
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2
3namespace GrowthExperiments;
4
5use ExtensionRegistry;
6use GrowthExperiments\NewcomerTasks\CampaignConfig;
7use HTMLForm;
8use IContextSource;
9use MediaWiki\Html\Html;
10use MediaWiki\Linker\Linker;
11use MediaWiki\MediaWikiServices;
12use MediaWiki\Minerva\Skins\SkinMinerva;
13use MediaWiki\Output\OutputPage;
14use MediaWiki\Title\MalformedTitleException;
15use MediaWiki\Title\Title;
16use MessageLocalizer;
17use OOUI\IconWidget;
18use Wikimedia\Assert\Assert;
19
20/**
21 * Customized version of the SpecialCreateAccount hero message, for campaigns.
22 * Somewhat customizable via community configuration: the 'signupPageTemplate' and
23 * 'signupPageTemplateParameters' parameters of the campaign configuration (see the
24 * CampaignConfig class) will be passed to CampaignBenefitsBlock::getCampaignTemplateHtml.
25 */
26class CampaignBenefitsBlock {
27
28    private IContextSource $context;
29    private HTMLForm $authForm;
30    private CampaignConfig $campaignConfig;
31
32    /**
33     * @param IContextSource $context
34     * @param HTMLForm $authForm
35     * @param CampaignConfig $campaignConfig
36     */
37    public function __construct(
38        IContextSource $context,
39        HTMLForm $authForm,
40        CampaignConfig $campaignConfig
41    ) {
42        $this->context = $context;
43        $this->authForm = $authForm;
44        $this->campaignConfig = $campaignConfig;
45    }
46
47    /**
48     * Get footer content for the special page. Displayed via SkinAddFooterLinks hook.
49     * @param MessageLocalizer $ctx
50     * @return string|void
51     */
52    public static function getLegalFooter( MessageLocalizer $ctx ) {
53        return $ctx->msg( 'growthexperiments-campaigns-footer' )->parse();
54    }
55
56    /**
57     * @return string HTML to render on Special:CreateAccount.
58     */
59    public function getHtml(): string {
60        $campaignName = $this->campaignConfig->getCampaignIndexFromCampaignTerm( $this->getCampaignValue() );
61        // If we got here, VariantHooks::shouldShowNewLandingPageHtml() is true
62        // so there is a campaign with a template. Make phan happy.
63        Assert::invariant( $campaignName !== null, '$campaignName is not null' );
64        $template = $this->campaignConfig->getSignupPageTemplate( $campaignName );
65        Assert::invariant( $template !== null, '$template is not null' );
66        $parameters = $this->campaignConfig->getSignupPageTemplateParameters( $campaignName );
67
68        // We only really have one template at this pont, with small variations.
69        return $this->getCampaignTemplateHtml( $template, $parameters );
70    }
71
72    /**
73     * Known templates/parameters:
74     * - hero: welcome text with hero image
75     *   FIXME: the image should be parametrized (currently it's CSS+SVG)
76     *   - messageKey: used in th name of various messages:
77     *     - growthexperiments-{messageKey}-title: title text (h2)
78     *     - growthexperiments-{messageKey}-body: main welcome text
79     *     - growthexperiments-{messageKey}-title-mobile and
80     *       growthexperiments-{messageKey}-body-mobile: alternative text for mobile (to allow for
81     *       shorter text and avoid pushing the registration form below the fold). Disable (set to
82     *       '-') to not show anything; omit or blank to show the same text as on desktop.
83     *     - growthexperiments-{messageKey}-bullet1/2/3: three bullet items after the main text,
84     *       with the lightbulb, mentor and difficulty-easy-bw icons, meant to highlight Growth
85     *       features. Only used if showBenefitsList is true (but then all three are required).
86     *     Also used as a CSS class (.mw-ge-{messageKey}-block) for selecting a specific campaign.
87     *   - showBenefitsList: whether to show the benefit list (three bullet items highlighting
88     *     various Growth features), default false
89     * - video: welcome text with video on top
90     *   - messageKey, showBenefitsList: as above
91     *   - file: video filename from Commons (without namespace)
92     *   - thumbtime: timestamp to use for still image for the video (default: leave it to MediaWiki)
93     * @param string $template
94     * @param array $parameters
95     * @return string
96     */
97    private function getCampaignTemplateHtml( $template, $parameters ) {
98        $this->context->getOutput()->enableOOUI();
99        $this->context->getOutput()->addModuleStyles( [
100            'oojs-ui.styles.icons-interactions',
101            'ext.growthExperiments.icons',
102            'ext.growthExperiments.Account.styles',
103        ] );
104
105        $this->context->getOutput()->addBodyClasses( 'mw-ge-customlandingpage' );
106
107        $isMobile = $this->context->getSkin() instanceof SkinMinerva;
108        $messageKey = $parameters['messageKey'];
109        $shouldShowBenefitsList = $parameters['showBenefitsList'] ?? false;
110        $shouldShowBenefitListInPlatform = $shouldShowBenefitsList === true ||
111            ( $shouldShowBenefitsList === 'desktop' && !$isMobile );
112        $benefitsList = '';
113        $videoHtml = '';
114        if ( $shouldShowBenefitListInPlatform ) {
115            foreach ( [ 'lightbulb', 'mentor', 'difficulty-easy-bw' ] as $i => $icon ) {
116                $index = $i + 1;
117                $benefitMessage = $this->context->msg( "growthexperiments-$messageKey-bullet$index" );
118                if ( !$benefitMessage->exists() ) {
119                    $benefitMessage = $this->context->msg( "growthexperiments-signupcampaign-bullet$index" );
120                }
121                $benefitsList .= Html::rawElement( 'li', [],
122                    new IconWidget( [ 'icon' => $icon ] )
123                    . Html::element( 'span', [],
124                        // The following message keys are used here:
125                        // * growthexperiments-signupcampaign-bullet1
126                        // * growthexperiments-signupcampaign-bullet2
127                        // * growthexperiments-signupcampaign-bullet3
128                        $benefitMessage->text()
129                    )
130                );
131            }
132            $benefitsList = Html::rawElement( 'ul', [ 'class' => 'mw-ge-donorsignup-list' ], $benefitsList );
133        }
134        if ( $template === 'video' ) {
135            $filename = $parameters['file'];
136            $thumbtime = $parameters['thumbtime'] ?? null;
137            $videoHtml = $this->getVideo( $this->context->getOutput(), $filename, $thumbtime );
138        }
139
140        // The following message keys are used here:
141        // * growthexperiments-recurringcampaign-title
142        // * growthexperiments-signupcampaign-title
143        // * growthexperiments-josacampaign-title
144        // * growthexperiments-glamcampaign-title
145        // * growthexperiments-marketingvideocampaign-title
146        $titleMessage = $this->context->msg( "growthexperiments-$messageKey-title" );
147        // The following message keys are used here:
148        // * growthexperiments-recurringcampaign-body
149        // * growthexperiments-signupcampaign-body
150        // * growthexperiments-josacampaign-body
151        // * growthexperiments-glamcampaign-body
152        // * growthexperiments-marketingvideocampaign-body
153        $bodyMessage = $this->context->msg( "growthexperiments-$messageKey-body" );
154        if ( $isMobile ) {
155            // use mobile-specific title/body if they exist and aren't empty
156            if ( !$this->context->msg( "growthexperiments-$messageKey-title-mobile" )->isBlank() ) {
157                // The following message keys are used here:
158                // none as of now
159                $titleMessage = $this->context->msg( "growthexperiments-$messageKey-title-mobile" );
160            }
161            if ( !$this->context->msg( "growthexperiments-$messageKey-body-mobile" )->isBlank() ) {
162                // The following message keys are used here:
163                // * growthexperiments-marketingvideocampaign-body-mobile
164                $bodyMessage = $this->context->msg( "growthexperiments-$messageKey-body-mobile" );
165            }
166        }
167
168        $campaignTitle = '';
169        $campaignBody = '';
170        // note that a message consisting of a single dash is disabled but not blank
171        if ( !$titleMessage->isDisabled() ) {
172            $campaignTitle = Html::rawElement( 'h2', [ 'class' => 'mw-ge-donorsignup-title' ],
173                $titleMessage->parse() );
174        }
175        if ( !$bodyMessage->isDisabled() ) {
176            $campaignBody = Html::rawElement( 'p', [ 'class' => 'mw-ge-donorsignup-body' ],
177                $bodyMessage->parse() );
178        }
179        return Html::rawElement( 'div', [ 'class' => 'mw-createacct-benefits-container' ],
180            Html::rawElement( 'div', [ 'class' => "mw-ge-donorsignup-block mw-ge-donorsignup-block-$messageKey" ],
181                $campaignTitle
182                . $campaignBody
183                . $benefitsList
184            )
185            . $videoHtml
186        );
187    }
188
189    /**
190     * Get the campaign from the account creation form
191     *
192     * @return string
193     */
194    private function getCampaignValue(): string {
195        return $this->authForm->getField( 'campaign' )->getDefault();
196    }
197
198    /**
199     * Add a video player to the output.
200     *
201     * @param OutputPage $output Used te register required assets.
202     * @param string $filename Video file name (without the 'File:' prefix).
203     * @param int|null $thumbtime Optional time position for thumbnail generation, in seconds.
204     *   Theoretically a float, but non-integer support is broken: T228467
205     * @return string Video player HTML
206     */
207    private function getVideo( OutputPage $output, string $filename, int $thumbtime = null ) {
208        if ( !ExtensionRegistry::getInstance()->isLoaded( 'TimedMediaHandler' ) ) {
209            Util::logText( 'TimedMediaHandler not loaded' );
210            return '';
211        }
212        try {
213            $title = Title::newFromTextThrow( 'File:' . $filename );
214        } catch ( MalformedTitleException $e ) {
215            Util::logText( $e->getMessage(), [ 'filename' => $filename ] );
216            return '';
217        }
218        $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $title );
219        if ( !$file ) {
220            Util::logText( "File not found: $filename" );
221            return '';
222        }
223
224        $output->addModules( [ 'ext.tmh.player' ] );
225        $output->addModuleStyles( [ 'ext.tmh.player.styles' ] );
226
227        $params = [];
228        if ( Util::isMobile( $this->context->getSkin() ) ) {
229            // For mobile, we don't know the width, so we pick a somewhat arbitrary height
230            // to keep the controls for the video close to the thumbnail.
231            $params['height'] = 200;
232        } else {
233            // Set same width as benefits container on desktop.
234            $params['width'] = 400;
235        }
236        if ( $thumbtime !== null ) {
237            $params['thumbtime'] = $thumbtime;
238        }
239        $html = Linker::makeImageLink(
240            MediaWikiServices::getInstance()->getParser(),
241            $title,
242            $file,
243            [ 'align' => 'center' ],
244            $params
245        );
246        return Html::rawElement( 'div', [ 'class' => 'mw-ge-video' ], $html );
247    }
248
249}