MediaWiki master
AuthManagerSpecialPage.php
Go to the documentation of this file.
1<?php
2
4
5use InvalidArgumentException;
6use LogicException;
23use MWCryptRand;
24use Profiler;
25use StatusValue;
26use UnexpectedValueException;
27
38abstract class AuthManagerSpecialPage extends SpecialPage {
42 protected static $allowedActions = [
43 AuthManager::ACTION_LOGIN, AuthManager::ACTION_LOGIN_CONTINUE,
44 AuthManager::ACTION_CREATE, AuthManager::ACTION_CREATE_CONTINUE,
45 AuthManager::ACTION_LINK, AuthManager::ACTION_LINK_CONTINUE,
46 AuthManager::ACTION_CHANGE, AuthManager::ACTION_REMOVE, AuthManager::ACTION_UNLINK,
47 ];
48
50 protected static $messages = [];
51
53 protected $authAction;
54
56 protected $authRequests;
57
59 protected $subPage;
60
62 protected $isReturn;
63
65 protected $savedRequest;
66
79 public function onAuthChangeFormFields(
80 array $requests, array $fieldInfo, array &$formDescriptor, $action
81 ) {
82 }
83
88 protected function getLoginSecurityLevel() {
89 return $this->getName();
90 }
91
92 public function getRequest() {
93 return $this->savedRequest ?: $this->getContext()->getRequest();
94 }
95
106 protected function setRequest( array $data, $wasPosted = null ) {
107 $request = $this->getContext()->getRequest();
108 $this->savedRequest = new DerivativeRequest(
109 $request,
110 $data + $request->getQueryValues(),
111 $wasPosted ?? $request->wasPosted()
112 );
113 }
114
116 protected function beforeExecute( $subPage ) {
117 $this->getOutput()->disallowUserJs();
118
119 return $this->handleReturnBeforeExecute( $subPage )
120 && $this->handleReauthBeforeExecute( $subPage );
121 }
122
139 protected function handleReturnBeforeExecute( $subPage ) {
140 $authManager = $this->getAuthManager();
141 $key = 'AuthManagerSpecialPage:return:' . $this->getName();
142
143 if ( $subPage === 'return' ) {
144 $this->loadAuth( $subPage );
145 $preservedParams = $this->getPreservedParams();
146
147 // FIXME save POST values only from request
148 $authData = array_diff_key( $this->getRequest()->getValues(),
149 $preservedParams, [ 'title' => 1 ] );
150 $uniqueId = MWCryptRand::generateHex( 6 );
151 $preservedParams['authUniqueId'] = $uniqueId;
152 $key .= ':' . $uniqueId;
153 $authManager->setAuthenticationSessionData( $key, $authData );
154
155 $url = $this->getPageTitle()->getFullURL( $preservedParams, false, PROTO_HTTPS );
156 $this->getOutput()->redirect( $url );
157 return false;
158 } elseif ( $this->getRequest()->getCheck( 'authUniqueId' ) ) {
159 $uniqueId = $this->getRequest()->getVal( 'authUniqueId' );
160 $key .= ':' . $uniqueId;
161 $authData = $authManager->getAuthenticationSessionData( $key );
162 if ( $authData ) {
163 $authManager->removeAuthenticationSessionData( $key );
164 $this->isReturn = true;
165 $this->setRequest( $authData, true );
166 $this->setPostTransactionProfilerExpectations( __METHOD__ );
167 }
168 }
169
170 return true;
171 }
172
183 protected function handleReauthBeforeExecute( $subPage ) {
184 $authManager = $this->getAuthManager();
185 $request = $this->getRequest();
186 $key = 'AuthManagerSpecialPage:reauth:' . $this->getName();
187
188 $securityLevel = $this->getLoginSecurityLevel();
189 if ( $securityLevel ) {
190 $securityStatus = $authManager->securitySensitiveOperationStatus( $securityLevel );
191 if ( $securityStatus === AuthManager::SEC_REAUTH ) {
192 $queryParams = array_diff_key( $request->getQueryValues(), [ 'title' => true ] );
193
194 if ( $request->wasPosted() ) {
195 // unique ID in case the same special page is open in multiple browser tabs
196 $uniqueId = MWCryptRand::generateHex( 6 );
197 $key .= ':' . $uniqueId;
198
199 $queryParams = [ 'authUniqueId' => $uniqueId ] + $queryParams;
200 $authData = array_diff_key( $request->getValues(),
201 $this->getPreservedParams(), [ 'title' => 1 ] );
202 $authManager->setAuthenticationSessionData( $key, $authData );
203 }
204
205 // Copied from RedirectSpecialPage::getRedirectQuery()
206 // Would using $this->getPreservedParams() be appropriate here?
207 $keepParams = [ 'uselang', 'useskin', 'useformat', 'variant', 'debug', 'safemode' ];
208
209 $title = SpecialPage::getTitleFor( 'Userlogin' );
210 $url = $title->getFullURL( [
211 'returnto' => $this->getFullTitle()->getPrefixedDBkey(),
212 'returntoquery' => wfArrayToCgi( $queryParams ),
213 'force' => $securityLevel,
214 ] + array_intersect_key( $queryParams, array_fill_keys( $keepParams, true ) ), false, PROTO_HTTPS );
215
216 $this->getOutput()->redirect( $url );
217 return false;
218 }
219
220 if ( $securityStatus !== AuthManager::SEC_OK ) {
221 throw new ErrorPageError( 'cannotauth-not-allowed-title', 'cannotauth-not-allowed' );
222 }
223 }
224
225 $uniqueId = $request->getVal( 'authUniqueId' );
226 if ( $uniqueId ) {
227 $key .= ':' . $uniqueId;
228 $authData = $authManager->getAuthenticationSessionData( $key );
229 if ( $authData ) {
230 $authManager->removeAuthenticationSessionData( $key );
231 $this->setRequest( $authData, true );
232 $this->setPostTransactionProfilerExpectations( __METHOD__ );
233 }
234 }
235
236 return true;
237 }
238
239 private function setPostTransactionProfilerExpectations( string $fname ) {
240 $trxLimits = $this->getConfig()->get( MainConfigNames::TrxProfilerLimits );
241 $trxProfiler = Profiler::instance()->getTransactionProfiler();
242 $trxProfiler->redefineExpectations( $trxLimits['POST'], $fname );
243 DeferredUpdates::addCallableUpdate( static function () use ( $trxProfiler, $trxLimits, $fname ) {
244 $trxProfiler->redefineExpectations( $trxLimits['PostSend-POST'], $fname );
245 } );
246 }
247
255 abstract protected function getDefaultAction( $subPage );
256
263 protected function messageKey( $defaultKey ) {
264 return array_key_exists( $defaultKey, static::$messages )
265 ? static::$messages[$defaultKey] : $defaultKey;
266 }
267
273 protected function getRequestBlacklist() {
274 return [];
275 }
276
287 protected function loadAuth( $subPage, $authAction = null, $reset = false ) {
288 // Do not load if already loaded, to cut down on the number of getAuthenticationRequests
289 // calls. This is important for requests which have hidden information, so any
290 // getAuthenticationRequests call would mean putting data into some cache.
291 if (
292 !$reset && $this->subPage === $subPage && $this->authAction
293 && ( !$authAction || $authAction === $this->authAction )
294 ) {
295 return;
296 }
297
298 $request = $this->getRequest();
299 $this->subPage = $subPage;
300 $this->authAction = $authAction ?: $request->getText( 'authAction' );
301 if ( !in_array( $this->authAction, static::$allowedActions, true ) ) {
302 $this->authAction = $this->getDefaultAction( $subPage );
303 if ( $request->wasPosted() ) {
304 $continueAction = $this->getContinueAction( $this->authAction );
305 if ( in_array( $continueAction, static::$allowedActions, true ) ) {
306 $this->authAction = $continueAction;
307 }
308 }
309 }
310
311 $allReqs = $this->getAuthManager()->getAuthenticationRequests(
312 $this->authAction, $this->getUser() );
313 $this->authRequests = array_filter( $allReqs, function ( $req ) {
314 return !in_array( get_class( $req ), $this->getRequestBlacklist(), true );
315 } );
316 }
317
322 protected function isContinued() {
323 return in_array( $this->authAction, [
324 AuthManager::ACTION_LOGIN_CONTINUE,
325 AuthManager::ACTION_CREATE_CONTINUE,
326 AuthManager::ACTION_LINK_CONTINUE,
327 ], true );
328 }
329
335 protected function getContinueAction( $action ) {
336 switch ( $action ) {
337 case AuthManager::ACTION_LOGIN:
338 $action = AuthManager::ACTION_LOGIN_CONTINUE;
339 break;
340 case AuthManager::ACTION_CREATE:
341 $action = AuthManager::ACTION_CREATE_CONTINUE;
342 break;
343 case AuthManager::ACTION_LINK:
344 $action = AuthManager::ACTION_LINK_CONTINUE;
345 break;
346 }
347 return $action;
348 }
349
357 protected function isActionAllowed( $action ) {
358 $authManager = $this->getAuthManager();
359 if ( !in_array( $action, static::$allowedActions, true ) ) {
360 throw new InvalidArgumentException( 'invalid action: ' . $action );
361 }
362
363 // calling getAuthenticationRequests can be expensive, avoid if possible
364 $requests = ( $action === $this->authAction ) ? $this->authRequests
365 : $authManager->getAuthenticationRequests( $action );
366 if ( !$requests ) {
367 // no provider supports this action in the current state
368 return false;
369 }
370
371 switch ( $action ) {
372 case AuthManager::ACTION_LOGIN:
373 case AuthManager::ACTION_LOGIN_CONTINUE:
374 return $authManager->canAuthenticateNow();
375 case AuthManager::ACTION_CREATE:
376 case AuthManager::ACTION_CREATE_CONTINUE:
377 return $authManager->canCreateAccounts();
378 case AuthManager::ACTION_LINK:
379 case AuthManager::ACTION_LINK_CONTINUE:
380 return $authManager->canLinkAccounts();
381 case AuthManager::ACTION_CHANGE:
382 case AuthManager::ACTION_REMOVE:
383 case AuthManager::ACTION_UNLINK:
384 return true;
385 default:
386 // should never reach here but makes static code analyzers happy
387 throw new InvalidArgumentException( 'invalid action: ' . $action );
388 }
389 }
390
397 protected function performAuthenticationStep( $action, array $requests ) {
398 if ( !in_array( $action, static::$allowedActions, true ) ) {
399 throw new InvalidArgumentException( 'invalid action: ' . $action );
400 }
401
402 $authManager = $this->getAuthManager();
403 $returnToUrl = $this->getPageTitle( 'return' )
404 ->getFullURL( $this->getPreservedParams( [ 'withToken' => true ] ), false, PROTO_HTTPS );
405
406 switch ( $action ) {
407 case AuthManager::ACTION_LOGIN:
408 return $authManager->beginAuthentication( $requests, $returnToUrl );
409 case AuthManager::ACTION_LOGIN_CONTINUE:
410 return $authManager->continueAuthentication( $requests );
411 case AuthManager::ACTION_CREATE:
412 return $authManager->beginAccountCreation( $this->getAuthority(), $requests,
413 $returnToUrl );
414 case AuthManager::ACTION_CREATE_CONTINUE:
415 return $authManager->continueAccountCreation( $requests );
416 case AuthManager::ACTION_LINK:
417 return $authManager->beginAccountLink( $this->getUser(), $requests, $returnToUrl );
418 case AuthManager::ACTION_LINK_CONTINUE:
419 return $authManager->continueAccountLink( $requests );
420 case AuthManager::ACTION_CHANGE:
421 case AuthManager::ACTION_REMOVE:
422 case AuthManager::ACTION_UNLINK:
423 if ( count( $requests ) > 1 ) {
424 throw new InvalidArgumentException( 'only one auth request can be changed at a time' );
425 }
426
427 if ( !$requests ) {
428 throw new InvalidArgumentException( 'no auth request' );
429 }
430 $req = reset( $requests );
431 $status = $authManager->allowsAuthenticationDataChange( $req );
432 $this->getHookRunner()->onChangeAuthenticationDataAudit( $req, $status );
433 if ( !$status->isGood() ) {
434 return AuthenticationResponse::newFail( $status->getMessage() );
435 }
436 $authManager->changeAuthenticationData( $req );
437 return AuthenticationResponse::newPass();
438 default:
439 // should never reach here but makes static code analyzers happy
440 throw new InvalidArgumentException( 'invalid action: ' . $action );
441 }
442 }
443
454 protected function trySubmit() {
455 $status = false;
456
457 $form = $this->getAuthForm( $this->authRequests, $this->authAction );
458 $form->setSubmitCallback( [ $this, 'handleFormSubmit' ] );
459
460 if ( $this->getRequest()->wasPosted() ) {
461 // handle tokens manually; $form->tryAuthorizedSubmit only works for logged-in users
462 $requestTokenValue = $this->getRequest()->getVal( $this->getTokenName() );
463 $sessionToken = $this->getToken();
464 if ( $sessionToken->wasNew() ) {
465 return Status::newFatal( $this->messageKey( 'authform-newtoken' ) );
466 } elseif ( !$requestTokenValue ) {
467 return Status::newFatal( $this->messageKey( 'authform-notoken' ) );
468 } elseif ( !$sessionToken->match( $requestTokenValue ) ) {
469 return Status::newFatal( $this->messageKey( 'authform-wrongtoken' ) );
470 }
471
472 $form->prepareForm();
473 $status = $form->trySubmit();
474
475 // HTMLForm submit return values are a mess; let's ensure it is false or a Status
476 // FIXME this probably should be in HTMLForm
477 if ( $status === true ) {
478 // not supposed to happen since our submit handler should always return a Status
479 throw new UnexpectedValueException( 'HTMLForm::trySubmit() returned true' );
480 } elseif ( $status === false ) {
481 // form was not submitted; nothing to do
482 } elseif ( $status instanceof Status ) {
483 // already handled by the form; nothing to do
484 } elseif ( $status instanceof StatusValue ) {
485 // in theory not an allowed return type but nothing stops the submit handler from
486 // accidentally returning it so best check and fix
487 $status = Status::wrap( $status );
488 } elseif ( is_string( $status ) ) {
489 $status = Status::newFatal( new RawMessage( '$1', [ $status ] ) );
490 } elseif ( is_array( $status ) ) {
491 if ( is_string( reset( $status ) ) ) {
492 // @phan-suppress-next-line PhanParamTooFewUnpack
493 $status = Status::newFatal( ...$status );
494 } elseif ( is_array( reset( $status ) ) ) {
495 $ret = Status::newGood();
496 foreach ( $status as $message ) {
497 // @phan-suppress-next-line PhanParamTooFewUnpack
498 $ret->fatal( ...$message );
499 }
500 $status = $ret;
501 } else {
502 throw new UnexpectedValueException( 'invalid HTMLForm::trySubmit() return value: '
503 . 'first element of array is ' . get_debug_type( reset( $status ) ) );
504 }
505 } else {
506 // not supposed to happen, but HTMLForm does not verify the return type
507 // from the submit callback; better safe then sorry!
508 throw new UnexpectedValueException( 'invalid HTMLForm::trySubmit() return type: '
509 . get_debug_type( $status ) );
510 }
511
512 if ( ( !$status || !$status->isOK() ) && $this->isReturn ) {
513 // This is awkward. There was a form validation error, which means the data was not
514 // passed to AuthManager. Normally we would display the form with an error message,
515 // but for the data we received via the redirect flow that would not be helpful at all.
516 // Let's just submit the data to AuthManager directly instead.
517 LoggerFactory::getInstance( 'authentication' )
518 ->warning( 'Validation error on return', [ 'data' => $form->mFieldData,
519 'status' => $status->getWikiText( false, false, 'en' ) ] );
520 $status = $this->handleFormSubmit( $form->mFieldData );
521 }
522 }
523
524 $changeActions = [
525 AuthManager::ACTION_CHANGE, AuthManager::ACTION_REMOVE, AuthManager::ACTION_UNLINK
526 ];
527 if ( in_array( $this->authAction, $changeActions, true ) && $status && !$status->isOK() ) {
528 $this->getHookRunner()->onChangeAuthenticationDataAudit( reset( $this->authRequests ), $status );
529 }
530
531 return $status;
532 }
533
540 public function handleFormSubmit( $data ) {
541 $requests = AuthenticationRequest::loadRequestsFromSubmission( $this->authRequests, $data );
542 $response = $this->performAuthenticationStep( $this->authAction, $requests );
543
544 // we can't handle FAIL or similar as failure here since it might require changing the form
545 return Status::newGood( $response );
546 }
547
568 protected function getPreservedParams( $options = [] ) {
569 if ( is_bool( $options ) ) {
570 wfDeprecated( __METHOD__ . ' boolean $options', '1.43' );
571 $options = [ 'withToken' => $options ];
572 }
573 $options += [
574 'reset' => false,
575 'withToken' => false,
576 ];
577 // Help Phan figure out that these fields are now definitely set - https://github.com/phan/phan/issues/4864
578 '@phan-var array{reset: bool, withToken: bool} $options';
579 $params = [];
580 $request = $this->getRequest();
581
582 $params += [
583 'uselang' => $request->getVal( 'uselang' ),
584 'variant' => $request->getVal( 'variant' ),
585 'returnto' => $request->getVal( 'returnto' ),
586 'returntoquery' => $request->getVal( 'returntoquery' ),
587 'returntoanchor' => $request->getVal( 'returntoanchor' ),
588 ];
589
590 if ( !$options['reset'] && $this->authAction !== $this->getDefaultAction( $this->subPage ) ) {
591 $params['authAction'] = $this->getContinueAction( $this->authAction );
592 }
593
594 if ( $options['withToken'] ) {
595 $params[$this->getTokenName()] = $this->getToken()->toString();
596 }
597
598 // Allow authentication extensions like CentralAuth to preserve their own
599 // query params during and after the authentication process.
600 $this->getHookRunner()->onAuthPreserveQueryParams(
601 $params, [ 'request' => $request, 'reset' => $options['reset'] ]
602 );
603
604 return array_filter( $params, static fn ( $val ) => $val !== null );
605 }
606
614 protected function getAuthFormDescriptor( $requests, $action ) {
615 $fieldInfo = AuthenticationRequest::mergeFieldInfo( $requests );
616 $formDescriptor = $this->fieldInfoToFormDescriptor( $requests, $fieldInfo, $action );
617
618 $this->addTabIndex( $formDescriptor );
619
620 return $formDescriptor;
621 }
622
629 protected function getAuthForm( array $requests, $action ) {
630 $formDescriptor = $this->getAuthFormDescriptor( $requests, $action );
631 $context = $this->getContext();
632 if ( $context->getRequest() !== $this->getRequest() ) {
633 // We have overridden the request, need to make sure the form uses that too.
634 $context = new DerivativeContext( $this->getContext() );
635 $context->setRequest( $this->getRequest() );
636 }
637 $form = HTMLForm::factory( 'ooui', $formDescriptor, $context );
638 $form->setAction( $this->getFullTitle()->getFullURL( $this->getPreservedParams() ) );
639 $form->addHiddenField( $this->getTokenName(), $this->getToken()->toString() );
640 $form->addHiddenField( 'authAction', $this->authAction );
641 $form->suppressDefaultSubmit( !$this->needsSubmitButton( $requests ) );
642
643 return $form;
644 }
645
650 protected function displayForm( $status ) {
651 if ( $status instanceof StatusValue ) {
652 $status = Status::wrap( $status );
653 }
654 $form = $this->getAuthForm( $this->authRequests, $this->authAction );
655 $form->prepareForm()->displayForm( $status );
656 }
657
669 protected function needsSubmitButton( array $requests ) {
670 $customSubmitButtonPresent = false;
671
672 // Secondary and preauth providers always need their data; they will not care what button
673 // is used, so they can be ignored. So can OPTIONAL buttons createdby primary providers;
674 // that's the point in being optional. Se we need to check whether all primary providers
675 // have their own buttons and whether there is at least one button present.
676 foreach ( $requests as $req ) {
677 if ( $req->required === AuthenticationRequest::PRIMARY_REQUIRED ) {
678 if ( $this->hasOwnSubmitButton( $req ) ) {
679 $customSubmitButtonPresent = true;
680 } else {
681 return true;
682 }
683 }
684 }
685 return !$customSubmitButtonPresent;
686 }
687
693 protected function hasOwnSubmitButton( AuthenticationRequest $req ) {
694 foreach ( $req->getFieldInfo() as $info ) {
695 if ( $info['type'] === 'button' ) {
696 return true;
697 }
698 }
699 return false;
700 }
701
707 protected function addTabIndex( &$formDescriptor ) {
708 $i = 1;
709 foreach ( $formDescriptor as &$definition ) {
710 $class = false;
711 if ( array_key_exists( 'class', $definition ) ) {
712 $class = $definition['class'];
713 } elseif ( array_key_exists( 'type', $definition ) ) {
714 $class = HTMLForm::$typeMappings[$definition['type']];
715 }
716 if ( $class !== HTMLInfoField::class ) {
717 $definition['tabindex'] = $i;
718 $i++;
719 }
720 }
721 }
722
728 protected function getToken() {
729 return $this->getRequest()->getSession()->getToken( 'AuthManagerSpecialPage:'
730 . $this->getName() );
731 }
732
738 protected function getTokenName() {
739 return 'wpAuthToken';
740 }
741
751 protected function fieldInfoToFormDescriptor( array $requests, array $fieldInfo, $action ) {
752 $formDescriptor = [];
753 foreach ( $fieldInfo as $fieldName => $singleFieldInfo ) {
754 $formDescriptor[$fieldName] = self::mapSingleFieldInfo( $singleFieldInfo, $fieldName );
755 }
756
757 $requestSnapshot = serialize( $requests );
758 $this->onAuthChangeFormFields( $requests, $fieldInfo, $formDescriptor, $action );
759 $this->getHookRunner()->onAuthChangeFormFields( $requests, $fieldInfo,
760 $formDescriptor, $action );
761 if ( $requestSnapshot !== serialize( $requests ) ) {
762 LoggerFactory::getInstance( 'authentication' )->warning(
763 'AuthChangeFormFields hook changed auth requests' );
764 }
765
766 // Process the special 'weight' property, which is a way for AuthChangeFormFields hook
767 // subscribers (who only see one field at a time) to influence ordering.
768 self::sortFormDescriptorFields( $formDescriptor );
769
770 return $formDescriptor;
771 }
772
780 protected static function mapSingleFieldInfo( $singleFieldInfo, $fieldName ) {
781 $type = self::mapFieldInfoTypeToFormDescriptorType( $singleFieldInfo['type'] );
782 $descriptor = [
783 'type' => $type,
784 // Do not prefix input name with 'wp'. This is important for the redirect flow.
785 'name' => $fieldName,
786 ];
787
788 if ( $type === 'submit' && isset( $singleFieldInfo['label'] ) ) {
789 $descriptor['default'] = $singleFieldInfo['label']->plain();
790 } elseif ( $type !== 'submit' ) {
791 $descriptor += array_filter( [
792 // help-message is omitted as it is usually not really useful for a web interface
793 'label-message' => self::getField( $singleFieldInfo, 'label' ),
794 ] );
795
796 if ( isset( $singleFieldInfo['options'] ) ) {
797 $descriptor['options'] = array_flip( array_map( static function ( $message ) {
799 return $message->parse();
800 }, $singleFieldInfo['options'] ) );
801 }
802
803 if ( isset( $singleFieldInfo['value'] ) ) {
804 $descriptor['default'] = $singleFieldInfo['value'];
805 }
806
807 if ( empty( $singleFieldInfo['optional'] ) ) {
808 $descriptor['required'] = true;
809 }
810 }
811
812 return $descriptor;
813 }
814
820 protected static function sortFormDescriptorFields( array &$formDescriptor ) {
821 $i = 0;
822 foreach ( $formDescriptor as &$field ) {
823 $field['__index'] = $i++;
824 }
825 unset( $field );
826 uasort( $formDescriptor, static function ( $first, $second ) {
827 return self::getField( $first, 'weight', 0 ) <=> self::getField( $second, 'weight', 0 )
828 ?: $first['__index'] <=> $second['__index'];
829 } );
830 foreach ( $formDescriptor as &$field ) {
831 unset( $field['__index'] );
832 }
833 }
834
842 protected static function getField( array $array, $fieldName, $default = null ) {
843 if ( array_key_exists( $fieldName, $array ) ) {
844 return $array[$fieldName];
845 } else {
846 return $default;
847 }
848 }
849
857 protected static function mapFieldInfoTypeToFormDescriptorType( $type ) {
858 $map = [
859 'string' => 'text',
860 'password' => 'password',
861 'select' => 'select',
862 'checkbox' => 'check',
863 'multiselect' => 'multiselect',
864 'button' => 'submit',
865 'hidden' => 'hidden',
866 'null' => 'info',
867 ];
868 if ( !array_key_exists( $type, $map ) ) {
869 throw new InvalidArgumentException( 'invalid field type: ' . $type );
870 }
871 return $map[$type];
872 }
873
886 protected static function mergeDefaultFormDescriptor(
887 array $fieldInfo, array $formDescriptor, array $defaultFormDescriptor
888 ) {
889 // keep the ordering from $defaultFormDescriptor where there is no explicit weight
890 foreach ( $defaultFormDescriptor as $fieldName => $defaultField ) {
891 // remove everything that is not in the fieldinfo, is not marked as a supplemental field
892 // to something in the fieldinfo, and is not an info field or a submit button
893 if (
894 !isset( $fieldInfo[$fieldName] )
895 && (
896 !isset( $defaultField['baseField'] )
897 || !isset( $fieldInfo[$defaultField['baseField']] )
898 )
899 && (
900 !isset( $defaultField['type'] )
901 || !in_array( $defaultField['type'], [ 'submit', 'info' ], true )
902 )
903 ) {
904 $defaultFormDescriptor[$fieldName] = null;
905 continue;
906 }
907
908 // default message labels should always take priority
909 $requestField = $formDescriptor[$fieldName] ?? [];
910 if (
911 isset( $defaultField['label'] )
912 || isset( $defaultField['label-message'] )
913 || isset( $defaultField['label-raw'] )
914 ) {
915 unset( $requestField['label'], $requestField['label-message'], $defaultField['label-raw'] );
916 }
917
918 $defaultFormDescriptor[$fieldName] += $requestField;
919 }
920
921 return array_filter( $defaultFormDescriptor + $formDescriptor );
922 }
923}
924
926class_alias( AuthManagerSpecialPage::class, 'AuthManagerSpecialPage' );
const PROTO_HTTPS
Definition Defines.php:232
wfArrayToCgi( $array1, $array2=null, $prefix='')
This function takes one or two arrays as input, and returns a CGI-style string, e....
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
This serves as the entry point to the authentication system.
This is a value object for authentication requests.
getFieldInfo()
Fetch input field info.
This is a value object to hold authentication response data.
An IContextSource implementation which will inherit context from another source but allow individual ...
Defer callable updates to run later in the PHP process.
An error page which can definitely be safely rendered using the OutputPage.
An information field (text blob), not a proper input.
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition HTMLForm.php:209
Variant of the Message class.
Create PSR-3 logger objects.
A class containing constants representing the names of configuration variables.
const TrxProfilerLimits
Name constant for the TrxProfilerLimits setting, for use with Config::get()
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:157
Similar to MediaWiki\Request\FauxRequest, but only fakes URL parameters and method (POST or GET) and ...
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form,...
Value object representing a CSRF token.
Definition Token.php:34
A special page subclass for authentication-related special pages.
bool $isReturn
True if the current request is a result of returning from a redirect flow.
handleReturnBeforeExecute( $subPage)
Handle redirection from the /return subpage.
static mapFieldInfoTypeToFormDescriptorType( $type)
Maps AuthenticationRequest::getFieldInfo() types to HTMLForm types.
getAuthFormDescriptor( $requests, $action)
Generates a HTMLForm descriptor array from a set of authentication requests.
setRequest(array $data, $wasPosted=null)
Override the POST data, GET data from the real request is preserved.
static getField(array $array, $fieldName, $default=null)
Get an array value, or a default if it does not exist.
getContinueAction( $action)
Gets the _CONTINUE version of an action.
static sortFormDescriptorFields(array &$formDescriptor)
Sort the fields of a form descriptor by their 'weight' property.
onAuthChangeFormFields(array $requests, array $fieldInfo, array &$formDescriptor, $action)
Change the form descriptor that determines how a field will look in the authentication form.
static mapSingleFieldInfo( $singleFieldInfo, $fieldName)
Maps an authentication field configuration for a single field (as returned by AuthenticationRequest::...
isContinued()
Returns true if this is not the first step of the authentication.
needsSubmitButton(array $requests)
Returns true if the form built from the given AuthenticationRequests needs a submit button.
handleReauthBeforeExecute( $subPage)
Handle redirection when the user needs to (re)authenticate.
string $authAction
one of the AuthManager::ACTION_* constants.
messageKey( $defaultKey)
Return custom message key.
static string[] $allowedActions
The list of actions this special page deals with.
static mergeDefaultFormDescriptor(array $fieldInfo, array $formDescriptor, array $defaultFormDescriptor)
Apply defaults to a form descriptor, without creating non-existent fields.
isActionAllowed( $action)
Checks whether AuthManager is ready to perform the action.
beforeExecute( $subPage)
Gets called before.SpecialPage::execute. Return false to prevent calling execute() (since 1....
getRequestBlacklist()
Allows blacklisting certain request types.
trySubmit()
Attempts to do an authentication step with the submitted data.
loadAuth( $subPage, $authAction=null, $reset=false)
Load or initialize $authAction, $authRequests and $subPage.
getTokenName()
Returns the name of the CSRF token (under which it should be found in the POST or GET data).
addTabIndex(&$formDescriptor)
Adds a sequential tabindex starting from 1 to all form elements.
getPreservedParams( $options=[])
Returns URL query parameters which should be preserved between authentication requests.
fieldInfoToFormDescriptor(array $requests, array $fieldInfo, $action)
Turns a field info array into a form descriptor.
handleFormSubmit( $data)
Submit handler callback for HTMLForm.
getDefaultAction( $subPage)
Get the default action for this special page if none is given via URL/POST data.
hasOwnSubmitButton(AuthenticationRequest $req)
Checks whether the given AuthenticationRequest has its own submit button.
getRequest()
Get the WebRequest being used for this instance.
WebRequest null $savedRequest
If set, will be used instead of the real request.
Parent class for all special pages.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
getUser()
Shortcut to get the User executing this instance.
getPageTitle( $subpage=false)
Get a self-referential title object.
getConfig()
Shortcut to get main config object.
getContext()
Gets the context this SpecialPage is executed in.
getOutput()
Get the OutputPage being used for this instance.
getAuthority()
Shortcut to get the Authority executing this instance.
getName()
Get the canonical, unlocalized name of this special page without namespace.
getFullTitle()
Return the full title, including $par.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:54
Profiler base class that defines the interface and some shared functionality.
Definition Profiler.php:37
static instance()
Definition Profiler.php:105
Generic operation result class Has warning/error list, boolean status and arbitrary value.