MediaWiki fundraising/REL1_35
Mustache.php
Go to the documentation of this file.
1<?php
2
3use LightnCandy\LightnCandy;
5use SmashPig\Core\PaymentError;
6use SmashPig\Core\ValidationError;
7
12
13 const EXTENSION = '.html.mustache';
14
21 public static $country;
22
24 public static $fieldErrors;
25
27 public static $baseDir;
28
30 protected static $partials = [
31 'first_name',
32 'issuers',
33 'last_name',
34 'more_info_links',
35 'name_fields',
36 'no_script',
37 'opt_in',
38 'payment_amount',
39 'payment_method',
40 'personal_info',
41 'monthly_convert',
42 'state_dropdown'
43 ];
44
49 public static $messageReplacements = [];
50
51 public function setGateway( GatewayType $gateway ) {
52 parent::setGateway( $gateway );
53
54 // FIXME: late binding fail?
55 self::$baseDir = dirname( $this->getTopLevelTemplate() );
56 $replacements = $gateway->getConfig( 'message_replacements' );
57 if ( $replacements ) {
58 self::$messageReplacements = $replacements;
59 }
60 }
61
68 public function getForm() {
69 $data = $this->getData();
70 self::$country = $data['country'];
71
72 $data = $data + $this->getErrors();
73 $data = $data + $this->getUrlsAndEmails();
74
75 self::$fieldErrors = $data['errors']['field'];
76
77 $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
78 $hookContainer->run(
79 'AlterPaymentFormData',
80 [ &$data ],
81 [ 'abortable' => false ]
82 );
83
84 $options = [
85 'helpers' => [
86 'l10n' => 'Gateway_Form_Mustache::l10n',
87 'fieldError' => 'Gateway_Form_Mustache::fieldError',
88 ],
89 'basedir' => [ self::$baseDir ],
90 'fileext' => self::EXTENSION,
91 'partials' => $this->getPartials( $data ),
92 'options' => LightnCandy::FLAG_RUNTIMEPARTIAL,
93 ];
94 return MustacheHelper::render( $this->getTopLevelTemplate(), $data, $options );
95 }
96
97 protected function getData() {
98 $data = $this->gateway->getData_Unstaged_Escaped();
99 $output = $this->gatewayPage->getContext()->getOutput();
100
101 $data['script_path'] = $this->scriptPath;
102 $relativePath = $this->sanitizePath( $this->getTopLevelTemplate() );
103 $data['template_trail'] = "<!-- Generated from: $relativePath -->";
104 $data['action'] = $this->getNoCacheAction();
105
106 $redirect = $this->gateway->getGlobal( 'NoScriptRedirect' );
107 $data['no_script_redirect'] = $redirect;
108
109 // FIXME: Appeal rendering should be broken out into its own thing.
110 $appealWikiTemplate = $this->gateway->getGlobal( 'AppealWikiTemplate' );
111 $appealWikiTemplate = str_replace( '$appeal', $data['appeal'], $appealWikiTemplate );
112 $appealWikiTemplate = str_replace( '$language', $data['language'], $appealWikiTemplate );
113 $data['appeal_text'] = $output->parseAsContent( '{{' . $appealWikiTemplate . '}}' );
114 $data['is_cc'] = ( $this->gateway->getPaymentMethod() === 'cc' );
115
116 // 'is_tax_ded' is a boolean variable to check if a country falls under tax-exempt countries
117 $tax_ded_countries = $this->gateway->getGlobal( 'TaxDedCountries' );
118 $data['is_tax_ded'] = in_array( $data['country'], $tax_ded_countries );
119 if ( $data['is_tax_ded'] ) {
120 $countries = CountryNames::getNames( $data['language'] );
121 $data['country_full'] = $countries[$data['country']];
122 }
123
124 // Only render monthly convert when we come back from a qualified processor
125 if (
126 // Or when we force display with a querystring flag
127 RequestContext::getMain()->getRequest()->getBool( 'debugMonthlyConvert' ) ||
128 ( $this->gateway->showMonthlyConvert() && $this->gatewayPage->supportsMonthlyConvert )
129 ) {
130 $data['monthly_convert'] = true;
131 }
132
133 $this->addSubmethods( $data );
134 $this->addFormFields( $data );
135 $this->handleOptIn( $data );
136 $this->addCurrencyData( $data );
137 $data['show_continue'] = $this->gatewayPage->showContinueButton();
138 $data['recurring'] = (bool)$data['recurring'];
139 return $data;
140 }
141
142 protected function handleOptIn( &$data ) {
143 // Since this value can be 1, 0, or unset, we need to make
144 // special conditionals for the mustache logic
145 if ( !isset( $data['opt_in'] ) || $data['opt_in'] === '' ) {
146 return;
147 }
148 $hasValidValue = false;
149 switch ( (string)$data['opt_in'] ) {
150 case '1':
151 $data['opted_in'] = true;
152 $hasValidValue = true;
153 break;
154 case '0':
155 $data['opted_out'] = true;
156 $hasValidValue = true;
157 break;
158 default:
160 $this->gateway,
161 '',
162 $this->gateway
163 );
164 $logger->warning( "Invalid opt_in value {$data['opt_in']}" );
165 break;
166 }
167 // If we have a valid value passed in on the query string, don't
168 // show the radio buttons to the user (they've already seen them
169 // in the banner or on donatewiki)
170 // If the value came from 'post' we may be re-rendering a form
171 // with some kind of validation error and should keep showing
172 // the opt_in radio buttons.
173 $dataSources = $this->gateway->getDataSources();
174 if ( $hasValidValue && $dataSources['opt_in'] === 'get' ) {
175 // assuming it's always going to be '_visible' isn't safe, see comment on L234
176 $data['opt_in_visible'] = false;
177 }
178 }
179
180 protected function addSubmethods( &$data ) {
181 if ( !$this->gatewayPage->showSubmethodButtons() ) {
182 $data['show_submethods'] = false;
183 return;
184 }
185
186 $availableSubmethods = $this->gateway->getAvailableSubmethods();
187 $showPresetSubmethod = !empty( $data['payment_submethod'] ) &&
188 array_key_exists( $data['payment_submethod'], $availableSubmethods );
189
190 // if the payment_submethod is not sent explicitly via the query string let's
191 // assume the user will benefit from seeing all available options
192 if ( $this->gateway->getDataSources()['payment_submethod'] != 'get'
193 && $showPresetSubmethod ) {
194 $showPresetSubmethod = false;
195 }
196
197 $showMultipleSubmethods = ( !$showPresetSubmethod && count( $availableSubmethods ) > 1 );
198 $showSingleSubmethod = count( $availableSubmethods ) == 1;
199
200 if ( $showMultipleSubmethods ) {
201 $data['show_submethods'] = true;
202 // Need to add submethod key to its array 'cause mustache doesn't get keys
203 $data['submethods'] = [];
204 foreach ( $availableSubmethods as $key => $submethod ) {
205 $submethod['key'] = $key;
206 if ( isset( $submethod['logo'] ) ) {
207 $submethod['logo'] = $this->getImagePath( $submethod['logo'] );
208 }
209 $submethod['srcset'] = $this->getSrcSet( $submethod );
210 $data['submethods'][] = $submethod;
211 }
212
213 $data['button_class'] = count( $data['submethods'] ) % 4 === 0
214 ? 'four-per-line'
215 : 'three-per-line';
216 } elseif ( $showSingleSubmethod || $showPresetSubmethod ) {
217
218 $submethodName = ( $showPresetSubmethod ) ? $data['payment_submethod'] :
219 array_keys( $availableSubmethods )[0];
220 $submethod = $availableSubmethods[$submethodName];
221 $data['submethod'] = $submethodName;
222
223 if ( isset( $submethod['logo'] ) &&
224 ( $showPresetSubmethod || !empty( $submethod['show_single_logo'] ) ) ) {
225 $data['show_single_submethod'] = true;
226 $data['submethod_label_key'] = $submethod['label_key'] ?? false;
227 $data['submethod_label'] = $submethod['label'] ?? false;
228 $data['submethod_logo'] = $this->getImagePath( $submethod['logo'] );
229 $data['submethod_srcset'] = $this->getSrcSet( $submethod );
230 }
231
232 if ( isset( $submethod['issuerids'] ) ) {
233 $data['show_issuers'] = true;
234 $data['issuers'] = [];
235 foreach ( $submethod['issuerids'] as $code => $label ) {
236 $data['issuers'][] = [
237 'code' => $code,
238 'label' => $label,
239 ];
240 }
241 }
242 }
243 }
244
245 protected function getSrcSet( $submethod ) {
246 if ( empty( $submethod['logo_hd'] ) ) {
247 return '';
248 }
249 $srcSet = [];
250 foreach ( $submethod['logo_hd'] as $scale => $filename ) {
251 $path = $this->getImagePath( $filename );
252 $srcSet[] = "$path $scale";
253 }
254 return 'srcset="' . implode( ',', $srcSet ) . '" ';
255 }
256
257 protected function addFormFields( &$data ) {
258 // If any of these are required, show the address block
259 $address_fields = [
260 'city',
261 'state_province',
262 'postal_code',
263 'street_address',
264 ];
265 // These are shown outside of the 'Billing information' block
266 $outside_personal_block = [
267 'opt_in',
268 'country'
269 ];
270 $show_personal_block = false;
271 $address_field_count = 0;
272 $fields = $this->gateway->getFormFields();
273 foreach ( $fields as $field => $type ) {
274 if ( $type === false ) {
275 continue;
276 }
277
278 // if field type is true(required) or optional it should be visible
279 if ( in_array( $type, [ true, 'optional' ], true ) ) {
280 $data["{$field}_visible"] = true;
281 if ( in_array( $field, $address_fields ) ) {
282 $data["address_visible"] = true;
283 if ( $field !== 'street_address' ) {
284 // street gets its own line
285 $address_field_count++;
286 }
287 }
288
289 // if field type is true(required), we also inject a *_required var to inform the view
290 if ( $type === true ) {
291 $data["{$field}_required"] = true;
292 if ( in_array( $field, $address_fields ) ) {
293 $data["address_required"] = true;
294 }
295 }
296 }
297
298 if ( !in_array( $field, $outside_personal_block ) ) {
299 $show_personal_block = true;
300 }
301 }
302
303 $data['show_personal_fields'] = $show_personal_block;
304
305 // In some countries, the surname (last_name) field should appear before the
306 // given name (first_name) field.
307 $surnameFirstCountries = $this->gateway->getGlobal( 'SurnameFirstCountries' );
308 if ( in_array( $data['country'], $surnameFirstCountries ) ) {
309 $data['show_surname_first'] = true;
310 }
311
312 // this is not great, we're assuming 'visible' (previously 'required') will always be a thing.
313 // the decision for the current _visible suffix is made on line 217
314 if ( !empty( $data["address_visible"] ) ) {
315 $classes = [
316 0 => 'fullwidth',
317 1 => 'fullwidth',
318 2 => 'halfwidth',
319 3 => 'thirdwidth'
320 ];
321 $data['address_css_class'] = $classes[$address_field_count];
322 // not great for the ranty reasons mentioned on line 234
323 if ( !empty( $data["state_province_visible"] ) ) {
324 $this->setStateOptions( $data );
325 }
326 }
327 }
328
329 protected function setStateOptions( &$data ) {
330 $state_list = Subdivisions::getByCountry( $data['country'] );
331 $data['state_province_options'] = [];
332
333 foreach ( $state_list as $abbr => $name ) {
334 $selected = isset( $data['state_province'] )
335 && $data['state_province'] === $abbr;
336
337 $data['state_province_options'][] = [
338 'abbr' => $abbr,
339 'name' => $name,
340 'selected' => $selected,
341 ];
342 }
343 }
344
345 protected function addCurrencyData( &$data ) {
346 $supportedCurrencies = $this->gateway->getCurrencies();
347 if ( count( $supportedCurrencies ) === 1 ) {
348 $data['show_currency_selector'] = false;
349 // The select input will be hidden, but posting the form will use its only value
350 // Display the same currency code
351 $data['currency'] = $supportedCurrencies[0];
352 } else {
353 $data['show_currency_selector'] = true;
354 }
355 foreach ( $supportedCurrencies as $currency ) {
356 $data['currencies'][] = [
357 'code' => $currency,
358 'selected' => ( $currency === $data['currency'] ),
359 ];
360 }
361
362 $data['display_amount'] = Amount::format(
363 $data['amount'],
364 $data['currency'],
365 $data['language'] . '_' . $data['country']
366 );
367 if ( floatval( $data['amount'] ) === 0.0 ) {
368 $data['amount'] = '';
369 }
370 }
371
378 protected function getErrors() {
379 $errors = $this->gateway->getErrorState()->getErrors();
380 $return = [ 'errors' => [
381 'general' => [],
382 'field' => [],
383 ] ];
384 $fieldNames = DonationData::getFieldNames();
385 foreach ( $errors as $error ) {
386 if ( $error instanceof ValidationError ) {
387 $key = $error->getField();
388
390 $error->getMessageKey(),
391 self::$country,
392 RequestContext::getMain()->getLanguage()->getCode(),
393 $error->getMessageParams()
394 );
395 } elseif ( $error instanceof PaymentError ) {
396 $key = $error->getErrorCode();
397 $message = $this->gateway->getErrorMapByCodeAndTranslate( $error->getErrorCode() );
398 } else {
399 throw new RuntimeException( "Unknown error type: " . var_export( $error, true ) );
400 }
401
402 $errorContext = [
403 'key' => $key,
404 'message' => $message,
405 ];
406
407 if ( in_array( $key, $fieldNames ) ) {
408 $return['errors']['field'][$key] = $errorContext;
409 } else {
410 $return['errors']['general'][] = $errorContext;
411 }
412 $return["{$key}_error"] = true;
413
414 // FIXME: Belongs in a separate phase?
415 if ( $key === 'currency' || $key === 'amount' ) {
416 $return['show_amount_input'] = true;
417 }
418 if ( !empty( $return['errors']['general'] ) ) {
419 $return['show_error_reference'] = true;
420 }
421 }
422 return $return;
423 }
424
425 protected function getUrlsAndEmails() {
426 $map = [
427 'problems' => 'Problems',
428 'otherways' => 'OtherWays',
429 'faq' => 'Faq',
430 'tax' => 'Tax',
431 'policy' => 'Policy'
432 ];
433 $urlsAndEmails = [];
434 foreach ( $map as $contextName => $globalName ) {
435 $urlsAndEmails[$contextName . '_url'] = htmlspecialchars(
436 $this->gateway->localizeGlobal( $globalName . 'URL' )
437 );
438 }
439 $urlsAndEmails['problems_email'] = $this->gateway->getGlobal( 'ProblemsEmail' );
440 return $urlsAndEmails;
441 }
442
443 // For the following helper functions, we can't use self:: to refer to
444 // static variables since rendering happens in another class, so we use
445 // Gateway_Form_Mustache::
446
447 // phpcs:disable Squiz.Classes.SelfMemberReference.NotUsed
448
456 public static function l10n( $key, ...$params ) {
457 $language = RequestContext::getMain()->getLanguage()->getCode();
458 // If there are any form variant messages configured swap them out here
459 if ( isset( Gateway_Form_Mustache::$messageReplacements[$key] ) ) {
461 }
462 $filteredParams = MustacheHelper::filterMessageParams( $params );
464 $key,
466 $language,
467 $filteredParams
468 );
469 }
470
477 public static function fieldError( $fieldName ) {
478 if ( isset( Gateway_Form_Mustache::$fieldErrors[$fieldName] ) ) {
479 $context = Gateway_Form_Mustache::$fieldErrors[$fieldName];
480 $context['cssClass'] = 'errorMsg';
481 } else {
482 $context = [
483 'cssClass' => 'errorMsgHide',
484 'key' => $fieldName,
485 ];
486 }
487
488 $path = Gateway_Form_Mustache::$baseDir . DIRECTORY_SEPARATOR
489 . 'error_message' . Gateway_Form_Mustache::EXTENSION;
490
491 return MustacheHelper::render( $path, $context );
492 }
493
494 // phpcs:enable
495
496 public function getResources() {
497 $resources = parent::getResources();
498 $gatewayModules = $this->gateway->getConfig( 'ui_modules' );
499 $this->addModules( 'scripts', $resources, $gatewayModules );
500 if ( $this->gateway->getGlobal( 'LogClientErrors' ) ) {
501 $resources[] = 'ext.donationInterface.errorLog';
502 }
503 if ( $this->gateway->showMonthlyConvert() ) {
504 // Search for any monthlyConvert modules that may have already
505 // been added by the variant=XXX mechanism.
506 $mcModules = preg_grep( '/monthlyConvert/', $resources );
507 if ( empty( $mcModules ) ) {
508 // Only add the default module if no variant-specified
509 // module is already in the list.
510 $resources[] = $this->gateway->getGlobal(
511 'MonthlyConvertDefaultModule'
512 );
513 }
514 }
515 return $resources;
516 }
517
518 public function getStyleModules() {
519 $modules = [ 'ext.donationInterface.mustache.styles' ];
520 $gatewayModules = $this->gateway->getConfig( 'ui_modules' );
521 $this->addModules( 'styles', $modules, $gatewayModules );
522 return $modules;
523 }
524
525 protected function addModules( $key, &$modules, $newModules ) {
526 if ( !empty( $newModules[$key] ) ) {
527 $modules = array_merge(
528 $modules,
529 (array)$newModules[$key]
530 );
531 }
532 }
533
534 protected function getTopLevelTemplate() {
535 return $this->gateway->getGlobal( 'Template' );
536 }
537
538 protected function getImagePath( $name ) {
539 return "{$this->scriptPath}/extensions/DonationInterface/gateway_forms/includes/{$name}";
540 }
541
542 protected function getPartials( array $data ) {
543 $partials = [];
544 if ( empty( $data['variant'] ) ) {
545 $variantDir = false;
546 } else {
547 $variantDir = $this->gateway->getGlobal( 'VariantConfigurationDirectory' ) .
548 DIRECTORY_SEPARATOR . $data['variant'] . DIRECTORY_SEPARATOR;
549 }
550 foreach ( self::$partials as $partial ) {
551 $filename = $partial . self::EXTENSION;
552 if (
553 $variantDir &&
554 file_exists( $variantDir . $filename )
555 ) {
556 $partials[$partial] = rtrim( file_get_contents(
557 $variantDir . $filename
558 ), "\r\n" );
559 } else {
560 $partials[$partial] = rtrim( file_get_contents(
561 self::$baseDir . DIRECTORY_SEPARATOR . $filename
562 ), "\r\n" );
563 }
564 }
565 return $partials;
566 }
567}
static format( $amount, $currencyCode, $locale)
Format an amount and currency for display to users.
Definition Amount.php:189
static getNames( $code)
Get localized country names for a particular language, using fallback languages for missing items.
static getFieldNames()
static getLogger(GatewayType $adapter, $suffix='', LogPrefixProvider $prefixer=null)
getConfig( $key=null)
Get settings loaded from adapter's config directory.
Gateway form rendering using Mustache.
Definition Mustache.php:11
static string $country
We set the following public static variables for use in mustache helper functions l10n and fieldError...
Definition Mustache.php:21
getStyleModules()
All the things that need to be loaded in the head with link tags for styling server-rendered html.
Definition Mustache.php:518
addModules( $key, &$modules, $newModules)
Definition Mustache.php:525
getPartials(array $data)
Definition Mustache.php:542
getErrors()
Get errors, sorted into two buckets - 'general' errors to display at the top of the form,...
Definition Mustache.php:378
setGateway(GatewayType $gateway)
Definition Mustache.php:51
static l10n( $key,... $params)
Get a message value specific to the donor's country and language.
Definition Mustache.php:456
static string[] $partials
Definition Mustache.php:30
static string $baseDir
Definition Mustache.php:27
getSrcSet( $submethod)
Definition Mustache.php:245
static fieldError( $fieldName)
Render a validation error message or blank error placeholder.
Definition Mustache.php:477
static array[] $fieldErrors
Definition Mustache.php:24
getForm()
Return the rendered HTML form, using template parameters from the gateway object.
Definition Mustache.php:68
static string[] $messageReplacements
Keys are message keys used in templates, values are message keys to replace them with.
Definition Mustache.php:49
GatewayAdapter $gateway
Definition Form.php:8
string $scriptPath
Definition Form.php:20
getNoCacheAction()
Determine the 'no cache' form action.
Definition Form.php:56
sanitizePath( $absolutePath)
Given an absolute file path, returns path relative to extension base dir.
Definition Form.php:111
MediaWikiServices is the service locator for the application scope of MediaWiki.
static getCountrySpecificMessage( $key, $country, $language, $params=[])
Retrieves and translates a country-specific message, or the default if no country-specific version ex...
static getByCountry( $country)
GatewayType Interface.