5use InvalidArgumentException;
26use UnexpectedValueException;
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,
80 array $requests, array $fieldInfo, array &$formDescriptor, $action
93 return $this->savedRequest ?: $this->
getContext()->getRequest();
106 protected function setRequest( array $data, $wasPosted =
null ) {
110 $data + $request->getQueryValues(),
111 $wasPosted ?? $request->wasPosted()
141 $key =
'AuthManagerSpecialPage:return:' . $this->
getName();
143 if ( $subPage ===
'return' ) {
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 );
158 } elseif ( $this->
getRequest()->getCheck(
'authUniqueId' ) ) {
159 $uniqueId = $this->
getRequest()->getVal(
'authUniqueId' );
160 $key .=
':' . $uniqueId;
161 $authData = $authManager->getAuthenticationSessionData( $key );
163 $authManager->removeAuthenticationSessionData( $key );
164 $this->isReturn =
true;
166 $this->setPostTransactionProfilerExpectations( __METHOD__ );
186 $key =
'AuthManagerSpecialPage:reauth:' . $this->
getName();
189 if ( $securityLevel ) {
190 $securityStatus = $authManager->securitySensitiveOperationStatus( $securityLevel );
191 if ( $securityStatus === AuthManager::SEC_REAUTH ) {
192 $queryParams = array_diff_key( $request->getQueryValues(), [
'title' =>
true ] );
194 if ( $request->wasPosted() ) {
196 $uniqueId = MWCryptRand::generateHex( 6 );
197 $key .=
':' . $uniqueId;
199 $queryParams = [
'authUniqueId' => $uniqueId ] + $queryParams;
200 $authData = array_diff_key( $request->getValues(),
201 $this->getPreservedParams(), [
'title' => 1 ] );
202 $authManager->setAuthenticationSessionData( $key, $authData );
207 $keepParams = [
'uselang',
'useskin',
'useformat',
'variant',
'debug',
'safemode' ];
210 $url = $title->getFullURL( [
211 'returnto' => $this->
getFullTitle()->getPrefixedDBkey(),
213 'force' => $securityLevel,
214 ] + array_intersect_key( $queryParams, array_fill_keys( $keepParams,
true ) ),
false,
PROTO_HTTPS );
220 if ( $securityStatus !== AuthManager::SEC_OK ) {
221 throw new ErrorPageError(
'cannotauth-not-allowed-title',
'cannotauth-not-allowed' );
225 $uniqueId = $request->getVal(
'authUniqueId' );
227 $key .=
':' . $uniqueId;
228 $authData = $authManager->getAuthenticationSessionData( $key );
230 $authManager->removeAuthenticationSessionData( $key );
232 $this->setPostTransactionProfilerExpectations( __METHOD__ );
239 private function setPostTransactionProfilerExpectations(
string $fname ) {
242 $trxProfiler->redefineExpectations( $trxLimits[
'POST'], $fname );
243 DeferredUpdates::addCallableUpdate(
static function () use ( $trxProfiler, $trxLimits, $fname ) {
244 $trxProfiler->redefineExpectations( $trxLimits[
'PostSend-POST'], $fname );
264 return array_key_exists( $defaultKey, static::$messages )
265 ? static::$messages[$defaultKey] : $defaultKey;
292 !$reset && $this->subPage ===
$subPage && $this->authAction
300 $this->authAction =
$authAction ?: $request->getText(
'authAction' );
301 if ( !in_array( $this->authAction, static::$allowedActions,
true ) ) {
303 if ( $request->wasPosted() ) {
305 if ( in_array( $continueAction, static::$allowedActions,
true ) ) {
306 $this->authAction = $continueAction;
312 $this->authAction, $this->
getUser() );
313 $this->authRequests = array_filter( $allReqs,
function ( $req ) {
323 return in_array( $this->authAction, [
324 AuthManager::ACTION_LOGIN_CONTINUE,
325 AuthManager::ACTION_CREATE_CONTINUE,
326 AuthManager::ACTION_LINK_CONTINUE,
337 case AuthManager::ACTION_LOGIN:
338 $action = AuthManager::ACTION_LOGIN_CONTINUE;
340 case AuthManager::ACTION_CREATE:
341 $action = AuthManager::ACTION_CREATE_CONTINUE;
343 case AuthManager::ACTION_LINK:
344 $action = AuthManager::ACTION_LINK_CONTINUE;
359 if ( !in_array( $action, static::$allowedActions,
true ) ) {
360 throw new InvalidArgumentException(
'invalid action: ' . $action );
365 : $authManager->getAuthenticationRequests( $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:
387 throw new InvalidArgumentException(
'invalid action: ' . $action );
398 if ( !in_array( $action, static::$allowedActions,
true ) ) {
399 throw new InvalidArgumentException(
'invalid action: ' . $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,
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' );
428 throw new InvalidArgumentException(
'no auth request' );
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() );
436 $authManager->changeAuthenticationData( $req );
437 return AuthenticationResponse::newPass();
440 throw new InvalidArgumentException(
'invalid action: ' . $action );
457 $form = $this->
getAuthForm( $this->authRequests, $this->authAction );
458 $form->setSubmitCallback( [ $this,
'handleFormSubmit' ] );
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' ) );
472 $form->prepareForm();
473 $status = $form->trySubmit();
477 if ( $status ===
true ) {
479 throw new UnexpectedValueException(
'HTMLForm::trySubmit() returned true' );
480 } elseif ( $status ===
false ) {
482 } elseif ( $status instanceof
Status ) {
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 ) ) ) {
493 $status = Status::newFatal( ...$status );
494 } elseif ( is_array( reset( $status ) ) ) {
495 $ret = Status::newGood();
496 foreach ( $status as $message ) {
498 $ret->fatal( ...$message );
502 throw new UnexpectedValueException(
'invalid HTMLForm::trySubmit() return value: '
503 .
'first element of array is ' . get_debug_type( reset( $status ) ) );
508 throw new UnexpectedValueException(
'invalid HTMLForm::trySubmit() return type: '
509 . get_debug_type( $status ) );
512 if ( ( !$status || !$status->isOK() ) && $this->isReturn ) {
517 LoggerFactory::getInstance(
'authentication' )
518 ->warning(
'Validation error on return', [
'data' => $form->mFieldData,
519 'status' => $status->getWikiText(
false,
false,
'en' ) ] );
525 AuthManager::ACTION_CHANGE, AuthManager::ACTION_REMOVE, AuthManager::ACTION_UNLINK
527 if ( in_array( $this->authAction, $changeActions,
true ) && $status && !$status->isOK() ) {
528 $this->
getHookRunner()->onChangeAuthenticationDataAudit( reset( $this->authRequests ), $status );
541 $requests = AuthenticationRequest::loadRequestsFromSubmission( $this->authRequests, $data );
545 return Status::newGood( $response );
569 if ( is_bool( $options ) ) {
570 wfDeprecated( __METHOD__ .
' boolean $options',
'1.43' );
571 $options = [
'withToken' => $options ];
575 'withToken' =>
false,
578 '@phan-var array{reset: bool, withToken: bool} $options';
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' ),
590 if ( !$options[
'reset'] && $this->authAction !== $this->
getDefaultAction( $this->subPage ) ) {
594 if ( $options[
'withToken'] ) {
601 $params, [
'request' => $request,
'reset' => $options[
'reset'] ]
604 return array_filter( $params,
static fn ( $val ) => $val !==
null );
615 $fieldInfo = AuthenticationRequest::mergeFieldInfo( $requests );
620 return $formDescriptor;
632 if ( $context->getRequest() !== $this->getRequest() ) {
637 $form = HTMLForm::factory(
'ooui', $formDescriptor, $context );
640 $form->addHiddenField(
'authAction', $this->authAction );
652 $status = Status::wrap( $status );
654 $form = $this->
getAuthForm( $this->authRequests, $this->authAction );
655 $form->prepareForm()->displayForm( $status );
670 $customSubmitButtonPresent =
false;
676 foreach ( $requests as $req ) {
677 if ( $req->required === AuthenticationRequest::PRIMARY_REQUIRED ) {
679 $customSubmitButtonPresent =
true;
685 return !$customSubmitButtonPresent;
695 if ( $info[
'type'] ===
'button' ) {
709 foreach ( $formDescriptor as &$definition ) {
711 if ( array_key_exists(
'class', $definition ) ) {
712 $class = $definition[
'class'];
713 } elseif ( array_key_exists(
'type', $definition ) ) {
714 $class = HTMLForm::$typeMappings[$definition[
'type']];
716 if ( $class !== HTMLInfoField::class ) {
717 $definition[
'tabindex'] = $i;
729 return $this->
getRequest()->getSession()->getToken(
'AuthManagerSpecialPage:'
739 return 'wpAuthToken';
752 $formDescriptor = [];
753 foreach ( $fieldInfo as $fieldName => $singleFieldInfo ) {
757 $requestSnapshot = serialize( $requests );
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' );
770 return $formDescriptor;
785 'name' => $fieldName,
788 if ( $type ===
'submit' && isset( $singleFieldInfo[
'label'] ) ) {
789 $descriptor[
'default'] = $singleFieldInfo[
'label']->plain();
790 } elseif ( $type !==
'submit' ) {
791 $descriptor += array_filter( [
793 'label-message' => self::getField( $singleFieldInfo,
'label' ),
796 if ( isset( $singleFieldInfo[
'options'] ) ) {
797 $descriptor[
'options'] = array_flip( array_map(
static function ( $message ) {
799 return $message->parse();
800 }, $singleFieldInfo[
'options'] ) );
803 if ( isset( $singleFieldInfo[
'value'] ) ) {
804 $descriptor[
'default'] = $singleFieldInfo[
'value'];
807 if ( empty( $singleFieldInfo[
'optional'] ) ) {
808 $descriptor[
'required'] =
true;
822 foreach ( $formDescriptor as &$field ) {
823 $field[
'__index'] = $i++;
826 uasort( $formDescriptor,
static function ( $first, $second ) {
828 ?: $first[
'__index'] <=> $second[
'__index'];
830 foreach ( $formDescriptor as &$field ) {
831 unset( $field[
'__index'] );
842 protected static function getField( array $array, $fieldName, $default =
null ) {
843 if ( array_key_exists( $fieldName, $array ) ) {
844 return $array[$fieldName];
860 'password' =>
'password',
861 'select' =>
'select',
862 'checkbox' =>
'check',
863 'multiselect' =>
'multiselect',
864 'button' =>
'submit',
865 'hidden' =>
'hidden',
868 if ( !array_key_exists( $type, $map ) ) {
869 throw new InvalidArgumentException(
'invalid field type: ' . $type );
887 array $fieldInfo, array $formDescriptor, array $defaultFormDescriptor
890 foreach ( $defaultFormDescriptor as $fieldName => $defaultField ) {
894 !isset( $fieldInfo[$fieldName] )
896 !isset( $defaultField[
'baseField'] )
897 || !isset( $fieldInfo[$defaultField[
'baseField']] )
900 !isset( $defaultField[
'type'] )
901 || !in_array( $defaultField[
'type'], [
'submit',
'info' ],
true )
904 $defaultFormDescriptor[$fieldName] =
null;
909 $requestField = $formDescriptor[$fieldName] ?? [];
911 isset( $defaultField[
'label'] )
912 || isset( $defaultField[
'label-message'] )
913 || isset( $defaultField[
'label-raw'] )
915 unset( $requestField[
'label'], $requestField[
'label-message'], $defaultField[
'label-raw'] );
918 $defaultFormDescriptor[$fieldName] += $requestField;
921 return array_filter( $defaultFormDescriptor + $formDescriptor );
926class_alias( AuthManagerSpecialPage::class,
'AuthManagerSpecialPage' );
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.
An IContextSource implementation which will inherit context from another source but allow individual ...
An error page which can definitely be safely rendered using the OutputPage.
A class containing constants representing the names of configuration variables.
const TrxProfilerLimits
Name constant for the TrxProfilerLimits setting, for use with Config::get()
A special page subclass for authentication-related special pages.
AuthenticationRequest[] $authRequests
string $subPage
Subpage of the special page.
performAuthenticationStep( $action, array $requests)
getToken()
Returns the CSRF token.
bool $isReturn
True if the current request is a result of returning from a redirect flow.
static array $messages
Customized messages.
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.
getAuthForm(array $requests, $action)
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.
displayForm( $status)
Display the form.
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.
Profiler base class that defines the interface and some shared functionality.
Generic operation result class Has warning/error list, boolean status and arbitrary value.