MediaWiki fundraising/REL1_35
GatewayPage.php
Go to the documentation of this file.
1<?php
19use Psr\Log\LogLevel;
20use SmashPig\Core\Logging\Logger;
21use SmashPig\Core\PaymentError;
22use SmashPig\PaymentData\FinalStatus;
23
34abstract class GatewayPage extends UnlistedSpecialPage {
40
47
52 public $adapter;
53
58 protected $logger;
59
64 protected $showError = false;
65
69 public function __construct() {
70 $me = get_called_class();
71 parent::__construct( $me );
72 }
73
79 public function execute( $par ) {
80 // FIXME: Deprecate "language" param.
81 $language = $this->getRequest()->getVal( 'language' );
82 $this->showError = $this->getRequest()->getBool( 'showError' );
83
84 if ( !$language ) {
85 // For some result pages, language does not come in on a standard URL param
86 // (language or uselang). For those cases, it's pretty safe to assume the
87 // correct language is in session.
88 // FIXME Restrict the places where we access session data
89 $donorData = WmfFramework::getSessionValue( 'Donor' );
90 if ( ( $donorData !== null ) && isset( $donorData[ 'language' ] ) ) {
91 $language = $donorData[ 'language' ];
92 }
93 }
94
95 if ( $language ) {
96 $this->getContext()->setLanguage( $language );
97 }
98
99 if ( $this->getConfig()->get( 'DonationInterfaceFundraiserMaintenance' ) ) {
100 $this->getOutput()->redirect( Title::newFromText( 'Special:FundraiserMaintenance' )->getFullURL(), '302'
101 );
102 return;
103 }
104
105 $gatewayName = $this->getGatewayIdentifier();
106 $className = DonationInterface::getAdapterClassForGateway( $gatewayName );
108
109 try {
110 $variant = $this->getVariant();
111 $this->adapter = new $className( [ 'variant' => $variant ] );
112 $this->logger = DonationLoggerFactory::getLogger( $this->adapter );
113
114 // FIXME: SmashPig should just use Monolog.
115 Logger::getContext()->enterContext( $this->adapter->getLogMessagePrefix() );
116
117 $out = $this->getOutput();
118 $out->preventClickjacking();
119 // Use addModuleStyles to load these CSS rules in early and avoid
120 // a flash of MediaWiki elements.
121 $out->addModuleStyles( 'donationInterface.styles' );
122 $out->addModuleStyles( 'donationInterface.skinOverrideStyles' );
123
124 $out->addModules( 'donationInterface.skinOverride' );
125 // Stolen from Minerva skin
126 $out->addHeadItem( 'viewport',
127 Html::element(
128 'meta', [
129 'name' => 'viewport',
130 'content' => 'initial-scale=1.0, user-scalable=yes, minimum-scale=0.25, maximum-scale=5.0, width=device-width',
131 ]
132 )
133 );
134
135 } catch ( Exception $ex ) {
136 if ( !$this->logger ) {
138 $className,
139 $this->getLogPrefix()
140 );
141 }
142 $this->logger->error(
143 "Exception setting up GatewayPage with adapter class $className: " .
144 "{$ex->getMessage()}\n{$ex->getTraceAsString()}"
145 );
146 // Setup scrambled, no point in continuing
147 $this->displayFailPage();
148 return;
149 }
150
151 // FIXME: Should have checked this before creating the adapter.
152 if ( $this->adapter->getGlobal( 'Enabled' ) !== true ) {
153 $this->logger->info( 'Displaying fail page for disabled gateway' );
154 $this->displayFailPage();
155 return;
156 }
157
158 if ( $this->adapter->getFinalStatus() === FinalStatus::FAILED ) {
159 $this->logger->info( 'Displaying fail page for failed GatewayReady checks' );
160 $this->displayFailPage();
161 return;
162 }
163
164 try {
165 $this->handleRequest();
166 } catch ( Exception $ex ) {
167 $this->logger->error( "Displaying fail page for exception: " . $ex->getMessage() );
168 $this->displayFailPage();
169 return;
170 }
171 }
172
180 protected function handleRequest() {
181 $this->handleDonationRequest();
182 }
183
187 public function displayForm() {
188 if ( $this->showError ) {
189 $form = new MustacheErrorForm();
190 } else {
191 $form = new Gateway_Form_Mustache();
192 }
193 $form->setGateway( $this->adapter );
194 $form->setGatewayPage( $this );
195
196 $formHtml = $form->getForm();
197 $output = $this->getOutput();
198
199 if ( !$this->showError ) {
200 // Only register the setClientVariables callback if we're loading a payments form.
201 // Error forms don't load any gateway-specific scripts so don't need these variables.
202 // And if we're already in an error condition (like an invalid payment method),
203 // calling setClientVariables could throw an exception. We set this hook after
204 // $form->getForm has succeeded so we avoid registering it when that function
205 // throws an error
206 $this->getHookContainer()->register(
207 'MakeGlobalVariablesScript', [ $this, 'setClientVariablesWithErrorHandling' ]
208 );
209 // Also only load any external gateway scripts for payments forms.
210 $this->addGatewaySpecificResources( $output );
211 }
212
213 $output->addModules( $form->getResources() );
214 $output->addModuleStyles( $form->getStyleModules() );
215 $output->addHTML( $formHtml );
216 }
217
221 public function displayFailPage() {
222 if ( $this->adapter ) {
223 $this->showError = true;
224 $this->displayForm();
225 } else {
226 $output = $this->getOutput();
227 $output->prepareErrorPage( $this->msg( 'donate_interface-error-msg-general' ) );
228 $output->addHTML( $this->msg(
229 'donate_interface-otherways',
230 [ $this->getConfig()->get( 'DonationInterfaceOtherWaysURL' ) ]
231 )->plain() );
232 }
233 }
234
242 protected function getGatewayIdentifier() {
244 }
245
254 protected function displayResultsForDebug( PaymentTransactionResponse $results = null ) {
255 $results = empty( $results ) ? $this->adapter->getTransactionResponse() : $results;
256
257 if ( $this->adapter->getGlobal( 'DisplayDebug' ) !== true ) {
258 return;
259 }
260
261 $output = $this->getOutput();
262
263 $output->addHTML( Html::element( 'span', null, $results->getMessage() ) );
264
265 $errors = $results->getErrors();
266 if ( !empty( $errors ) ) {
267 $output->addHTML( Html::openElement( 'ul' ) );
268 foreach ( $errors as $code => $value ) {
269 $output->addHTML( Html::element( 'li', null, "Error $code: " . print_r( $value, true ) ) );
270 }
271 $output->addHTML( Html::closeElement( 'ul' ) );
272 }
273
274 $data = $results->getData();
275 if ( !empty( $data ) ) {
276 $output->addHTML( Html::openElement( 'ul' ) );
277 foreach ( $data as $key => $value ) {
278 if ( is_array( $value ) ) {
279 $output->addHTML( Html::openElement( 'li', null ) . Html::openElement( 'ul' ) );
280 foreach ( $value as $key2 => $val2 ) {
281 $output->addHTML( Html::element( 'li', null, "$key2: $val2" ) );
282 }
283 $output->addHTML( Html::closeElement( 'ul' ) . Html::closeElement( 'li' ) );
284 } else {
285 $output->addHTML( Html::element( 'li', null, "$key: $value" ) );
286 }
287 }
288 $output->addHTML( Html::closeElement( 'ul' ) );
289 } else {
290 $output->addHTML( "Empty Results" );
291 }
292 $donorData = $this->getRequest()->getSessionData( 'Donor' );
293 if ( is_array( $donorData ) ) {
294 $output->addHTML( "Session Donor Vars:" . Html::openElement( 'ul' ) );
295 foreach ( $donorData as $key => $val ) {
296 $output->addHTML( Html::element( 'li', null, "$key: $val" ) );
297 }
298 $output->addHTML( Html::closeElement( 'ul' ) );
299 } else {
300 $output->addHTML( "No Session Donor Vars:" );
301 }
302
303 if ( is_array( $this->adapter->debugarray ) ) {
304 $output->addHTML( "Debug Array:" . Html::openElement( 'ul' ) );
305 foreach ( $this->adapter->debugarray as $val ) {
306 $output->addHTML( Html::element( 'li', null, $val ) );
307 }
308 $output->addHTML( Html::closeElement( 'ul' ) );
309 } else {
310 $output->addHTML( "No Debug Array" );
311 }
312 }
313
317 protected function handleDonationRequest() {
318 $this->setHeaders();
319
320 // TODO: This is where we should feed GPCS parameters into the gateway
321 // and DonationData, rather than harvest params in the adapter itself.
322
323 // dispatch forms/handling
324 if ( $this->adapter->checkTokens() ) {
325 if ( $this->isProcessImmediate() ) {
326 // Check form for errors
327 $validated_ok = $this->adapter->validatedOK();
328
329 // Proceed to the next step, unless there were errors.
330 if ( $validated_ok ) {
331 // Attempt to process the payment, then render the response.
332 $this->processPayment();
333 } else {
334 // Redisplay form to give the donor notification and a
335 // chance correct their errors.
336 $this->displayForm();
337 }
338 } else {
339 $this->adapter->session_addDonorData();
340 $this->displayForm();
341 }
342 } else { // token mismatch
343 $this->adapter->getErrorState()->addError( new PaymentError(
344 'internal-0001',
345 'Failed CSRF token validation',
346 LogLevel::INFO
347 ) );
348 $this->displayForm();
349 }
350 }
351
357 protected function isProcessImmediate() {
358 // If the user posted to this form, process immediately.
359 if ( $this->adapter->posted ) {
360 return true;
361 }
362
363 // Otherwise, respect the "redirect" parameter. If it is "1", try to
364 // skip the interstitial page. If it's "0", do not process immediately.
365 $redirect = $this->adapter->getData_Unstaged_Escaped( 'redirect' );
366 if ( $redirect !== null ) {
367 return ( $redirect === '1' || $redirect === 'true' );
368 }
369
370 return false;
371 }
372
378 protected function processPayment() {
379 $this->renderResponse( $this->adapter->doPayment() );
380 }
381
387 protected function renderResponse( PaymentResult $result ) {
388 if ( $result->isFailed() ) {
389 $this->logger->info( 'Displaying fail page for failed PaymentResult' );
390 $this->displayFailPage();
391 } elseif ( $url = $result->getRedirect() ) {
392 $this->adapter->logPending();
393 $this->getOutput()->redirect( $url );
394 } elseif ( $url = $result->getIframe() ) {
395 // Show a form containing an iframe.
396
397 // Well, that's sketchy. See TODO in renderIframe: we should
398 // accomplish this entirely by passing an iframeSrcUrl parameter
399 // to the template.
400 $this->displayForm();
401
402 $this->renderIframe( $url );
403 } elseif (
404 count( $result->getErrors() )
405 ) {
406 $this->displayForm();
407 } elseif ( $this->adapter->showMonthlyConvert() ) {
408 $this->logger->info( "Displaying monthly convert modal after successful one-time donation PaymentResult" );
409 $this->displayForm();
410 } else {
411 // Success.
412 $thankYouPage = ResultPages::getThankYouPage( $this->adapter );
413 $this->logger->info( "Displaying thank you page $thankYouPage for successful PaymentResult." );
414 $this->getOutput()->redirect( $thankYouPage );
415 }
416 }
417
425 protected function renderIframe( $url ) {
426 $attrs = [
427 'id' => 'paymentiframe',
428 'name' => 'paymentiframe',
429 'width' => '680',
430 'height' => '300'
431 ];
432
433 $attrs['frameborder'] = '0';
434 $attrs['style'] = 'display:block;';
435 $attrs['src'] = $url;
436 $paymentFrame = Xml::openElement( 'iframe', $attrs );
437 $paymentFrame .= Xml::closeElement( 'iframe' );
438
439 $this->getOutput()->addHTML( $paymentFrame );
440 }
441
447 protected function getLogPrefix() {
448 $info = [];
449 $donorData = $this->getRequest()->getSessionData( 'Donor' );
450 if ( is_array( $donorData ) ) {
451 if ( isset( $donorData['contribution_tracking_id'] ) ) {
452 $info[] = $donorData['contribution_tracking_id'];
453 }
454 if ( isset( $donorData['order_id'] ) ) {
455 $info[] = $donorData['order_id'];
456 }
457 }
458 return implode( ':', $info ) . ' ';
459 }
460
461 public function setHeaders() {
462 parent::setHeaders();
463
464 // TODO: Switch title according to failiness.
465 // Maybe ask $form_obj for a title so different errors can show different titles
466 $this->getOutput()->setPageTitle( $this->msg( 'donate_interface-make-your-donation' ) );
467 }
468
469 public function setClientVariablesWithErrorHandling( &$vars ) {
470 try{
471 $this->setClientVariables( $vars );
472 } catch ( Exception $ex ) {
473 $this->logger->error(
474 "Redirecting to fail page for exception in setClientVariables: " . $ex->getMessage()
475 );
476 // At this point in the special page lifecycle the payments form has already been
477 // sent to the outputPage so we can't use displayFailPage. It seems like we can't
478 // even redirect at this point ($this->getOutput()->redirect fails here, I guess
479 // because the headers have been finalized). So we just manipulate the $vars to
480 // tell the javascript to show the error.
481 $vars['DonationInterfaceSetClientVariablesError'] = true;
482 $request = $this->getRequest();
483 $hasParams = count( $request->getQueryValuesOnly() ) > 0;
484 $vars['DonationInterfaceFailUrl'] = $request->getFullRequestURL() .
485 ( $hasParams ? '&' : '?' ) . 'showError=true';
486 }
487 }
488
493 public function setClientVariables( &$vars ) {
494 $language = $this->adapter->getData_Unstaged_Escaped( 'language' );
495 $country = $this->adapter->getData_Unstaged_Escaped( 'country' );
496 $vars['wgDonationInterfaceAmountRules'] = $this->adapter->getDonationRules();
497 $vars['wgDonationInterfaceLogDebug'] = $this->adapter->getGlobal( 'LogDebug' );
498 if ( $this->adapter->showMonthlyConvert() ) {
499 $thankYouUrl = ResultPages::getThankYouPage( $this->adapter );
500 $vars['wgDonationInterfaceThankYouUrl'] = $thankYouUrl;
501 $vars['showMConStartup'] = $this->getRequest()->getBool( 'debugMonthlyConvert' );
502 $vars['wgDonationInterfaceMonthlyConvertAmounts'] = $this->adapter->getMonthlyConvertAmounts();
503 }
504
505 try {
506 $clientRules = $this->adapter->getClientSideValidationRules();
507 if ( !empty( $clientRules ) ) {
508 // Translate all the messages
509 // FIXME: figure out country fallback add the i18n strings
510 // for use with client-side mw.msg()
511 foreach ( $clientRules as &$fieldRules ) {
512 foreach ( $fieldRules as &$rule ) {
513 if ( !empty( $rule['messageKey'] ) ) {
515 $rule['messageKey'],
516 $country,
517 $language
518 );
519 }
520 }
521 }
522 $vars['wgDonationInterfaceValidationRules'] = $clientRules;
523 }
524 } catch ( Exception $ex ) {
525 $this->logger->warning(
526 'Caught exception setting client-side validation rules: ' .
527 $ex->getMessage()
528 );
529 }
530 }
531
532 protected function getVariant() {
533 // FIXME: This is the sort of thing DonationData is supposed to do,
534 // but we construct it too late to use variant in the configuration
535 // reader. We should be pulling all the get / post / session variables
536 // up here in the page class before creating the adapter.
537 $variant = $this->getRequest()->getVal( 'variant' );
538 if ( !$variant ) {
539 $donorData = $this->getRequest()->getSessionData( 'Donor' );
540 if ( $donorData && !empty( $donorData['variant'] ) ) {
541 $variant = $donorData['variant'];
542 }
543 }
544 return $variant;
545 }
546
552 public function showSubmethodButtons() {
553 return true;
554 }
555
561 public function showContinueButton() {
562 return true;
563 }
564
572 public static function getGatewayPageName( string $gatewayId, Config $mwConfig ): string {
573 $gatewayClasses = $mwConfig->get( 'DonationInterfaceGatewayAdapters' );
574
575 // T302939: in order to pass the SpecialPageFatalTest::testSpecialPageDoesNotFatal unit test
576 // since no aliases are defined for those TestingAdapters
577 // will remove below if condition once those TestingAdapter gone from the test cases
578 if ( str_starts_with( $gatewayClasses[ $gatewayId ], 'Testing' ) ) {
579 $specialPage = 'GatewayChooser';
580 } else {
581 // The special page name is the gateway adapter class name with 'Adapter'
582 // replaced with 'Gateway'.
583 $specialPage = str_replace(
584 'Adapter',
585 'Gateway',
586 $gatewayClasses[ $gatewayId ]
587 );
588 }
589
590 return $specialPage;
591 }
592
600 protected function addGatewaySpecificResources( OutputPage $out ): void {
601 }
602}
static setSmashPigProvider( $provider)
Initialize SmashPig context and return configuration object.
static getAdapterClassForGateway( $gateway)
static getLogger(GatewayType $adapter, $suffix='', LogPrefixProvider $prefixer=null)
static getLoggerForType( $adapterType, $prefix='')
Get a logger without an adapter instance.
GatewayAdapter.
GatewayPage This class is the generic unlisted special page in charge of actually displaying the form...
processPayment()
Ask the adapter to perform a payment.
handleDonationRequest()
Respond to a donation request.
addGatewaySpecificResources(OutputPage $out)
Override this to add any gateway-specific scripts or stylesheets that can't be loaded via ResourceLoa...
GatewayAdapter $adapter
The gateway adapter object.
showSubmethodButtons()
Integrations that do not show submethod buttons should override to return false.
static getGatewayPageName(string $gatewayId, Config $mwConfig)
Get the name of the special page for a gateway.
displayForm()
Build and display form to user.
renderIframe( $url)
Append iframe.
setClientVariablesWithErrorHandling(&$vars)
getLogPrefix()
Try to get donor information to tag log entries in case we don't have an adapter instance.
bool $supportsMonthlyConvert
flag for setting Monthly Convert modal on template
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!...
bool $showError
When true, display an error form rather than the standard payment form.
execute( $par)
Show the special page.
getGatewayIdentifier()
Get the current adapter class.
string $gatewayIdentifier
Derived classes must override this with the identifier of the gateway as set in GatewayAdapter::IDENT...
showContinueButton()
Integrations that never need a continue button should override to return false.
setClientVariables(&$vars)
MakeGlobalVariablesScript handler, sends settings to Javascript.
displayResultsForDebug(PaymentTransactionResponse $results=null)
displayResultsForDebug
__construct()
Constructor.
renderResponse(PaymentResult $result)
Take UI action suggested by the payment result.
handleRequest()
Handle the donation request.
displayFailPage()
Display a failure page.
isProcessImmediate()
Determine if we should attempt to process the payment now.
Psr Log LoggerInterface $logger
Gateway-specific logger.
Gateway form rendering using Mustache.
Definition Mustache.php:11
static getCountrySpecificMessage( $key, $country, $language, $params=[])
Retrieves and translates a country-specific message, or the default if no country-specific version ex...
Renders error forms from Mustache templates.
This is one of the Core classes and should be read at least once by any new developers.
preventClickjacking( $enable=true)
Set a flag which will cause an X-Frame-Options header appropriate for edit pages to be sent.
addModuleStyles( $modules)
Load the styles of one or more style-only ResourceLoader modules on this page.
addHeadItem( $name, $value)
Add or replace a head item to the output.
addModules( $modules)
Load one or more ResourceLoader modules on this page.
Contains donation workflow UI hints.
Contains information parsed out of a payment processor's response to a transaction.
static getThankYouPage(GatewayType $adapter, $extraParams=[])
Get the URL for a page to show donors after a successful donation, with the country code appended as ...
getOutput()
Get the OutputPage being used for this instance.
getContext()
Gets the context this SpecialPage is executed in.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getConfig()
Shortcut to get main config object.
getRequest()
Get the WebRequest being used for this instance.
Shortcut to construct a special page which is unlisted by default.
Interface for configuration instances.
Definition Config.php:29