9use InvalidArgumentException;
22use UnexpectedValueException;
38 AuthManager::ACTION_LOGIN, AuthManager::ACTION_LOGIN_CONTINUE,
39 AuthManager::ACTION_CREATE, AuthManager::ACTION_CREATE_CONTINUE,
40 AuthManager::ACTION_LINK, AuthManager::ACTION_LINK_CONTINUE,
41 AuthManager::ACTION_CHANGE, AuthManager::ACTION_REMOVE, AuthManager::ACTION_UNLINK,
75 array $requests, array $fieldInfo, array &$formDescriptor, $action
88 return $this->savedRequest ?: $this->
getContext()->getRequest();
101 protected function setRequest( array $data, $wasPosted =
null ) {
105 $data + $request->getQueryValues(),
106 $wasPosted ?? $request->wasPosted()
141 $key =
'AuthManagerSpecialPage:return:' . $this->
getName();
148 $authData = array_diff_key( $this->
getRequest()->getValues(),
149 $preservedParams, [
'title' => 1 ] );
150 $authManager->setAuthenticationSessionData( $key, $authData );
157 $authData = $authManager->getAuthenticationSessionData( $key );
159 $authManager->removeAuthenticationSessionData( $key );
160 $this->isReturn =
true;
180 $key =
'AuthManagerSpecialPage:reauth:' . $this->
getName();
183 if ( $securityLevel ) {
184 $securityStatus = $authManager->securitySensitiveOperationStatus( $securityLevel );
185 if ( $securityStatus === AuthManager::SEC_REAUTH ) {
186 $queryParams = array_diff_key( $request->getQueryValues(), [
'title' =>
true ] );
188 if ( $request->wasPosted() ) {
190 $uniqueId = MWCryptRand::generateHex( 6 );
191 $key .=
':' . $uniqueId;
193 $queryParams = [
'authUniqueId' => $uniqueId ] + $queryParams;
194 $authData = array_diff_key( $request->getValues(),
195 $this->getPreservedParams(
false ), [
'title' => 1 ] );
196 $authManager->setAuthenticationSessionData( $key, $authData );
200 $url = $title->getFullURL( [
201 'returnto' => $this->
getFullTitle()->getPrefixedDBkey(),
203 'force' => $securityLevel,
208 } elseif ( $securityStatus !== AuthManager::SEC_OK ) {
209 throw new ErrorPageError(
'cannotauth-not-allowed-title',
'cannotauth-not-allowed' );
213 $uniqueId = $request->getVal(
'authUniqueId' );
215 $key .=
':' . $uniqueId;
216 $authData = $authManager->getAuthenticationSessionData( $key );
218 $authManager->removeAuthenticationSessionData( $key );
242 return array_key_exists( $defaultKey, static::$messages )
243 ? static::$messages[$defaultKey] : $defaultKey;
270 !$reset && $this->subPage ===
$subPage && $this->authAction
278 $this->authAction =
$authAction ?: $request->getText(
'authAction' );
279 if ( !in_array( $this->authAction, static::$allowedActions,
true ) ) {
281 if ( $request->wasPosted() ) {
283 if ( in_array( $continueAction, static::$allowedActions,
true ) ) {
284 $this->authAction = $continueAction;
290 $this->authAction, $this->
getUser() );
291 $this->authRequests = array_filter( $allReqs,
function ( $req ) {
301 return in_array( $this->authAction, [
302 AuthManager::ACTION_LOGIN_CONTINUE,
303 AuthManager::ACTION_CREATE_CONTINUE,
304 AuthManager::ACTION_LINK_CONTINUE,
315 case AuthManager::ACTION_LOGIN:
316 $action = AuthManager::ACTION_LOGIN_CONTINUE;
318 case AuthManager::ACTION_CREATE:
319 $action = AuthManager::ACTION_CREATE_CONTINUE;
321 case AuthManager::ACTION_LINK:
322 $action = AuthManager::ACTION_LINK_CONTINUE;
338 if ( !in_array( $action, static::$allowedActions,
true ) ) {
339 throw new InvalidArgumentException(
'invalid action: ' . $action );
344 : $authManager->getAuthenticationRequests( $action );
351 case AuthManager::ACTION_LOGIN:
352 case AuthManager::ACTION_LOGIN_CONTINUE:
353 return $authManager->canAuthenticateNow();
354 case AuthManager::ACTION_CREATE:
355 case AuthManager::ACTION_CREATE_CONTINUE:
356 return $authManager->canCreateAccounts();
357 case AuthManager::ACTION_LINK:
358 case AuthManager::ACTION_LINK_CONTINUE:
359 return $authManager->canLinkAccounts();
360 case AuthManager::ACTION_CHANGE:
361 case AuthManager::ACTION_REMOVE:
362 case AuthManager::ACTION_UNLINK:
366 throw new InvalidArgumentException(
'invalid action: ' . $action );
377 if ( !in_array( $action, static::$allowedActions,
true ) ) {
378 throw new InvalidArgumentException(
'invalid action: ' . $action );
386 case AuthManager::ACTION_LOGIN:
387 return $authManager->beginAuthentication( $requests, $returnToUrl );
388 case AuthManager::ACTION_LOGIN_CONTINUE:
389 return $authManager->continueAuthentication( $requests );
390 case AuthManager::ACTION_CREATE:
391 return $authManager->beginAccountCreation( $this->
getAuthority(), $requests,
393 case AuthManager::ACTION_CREATE_CONTINUE:
394 return $authManager->continueAccountCreation( $requests );
395 case AuthManager::ACTION_LINK:
396 return $authManager->beginAccountLink( $this->
getUser(), $requests, $returnToUrl );
397 case AuthManager::ACTION_LINK_CONTINUE:
398 return $authManager->continueAccountLink( $requests );
399 case AuthManager::ACTION_CHANGE:
400 case AuthManager::ACTION_REMOVE:
401 case AuthManager::ACTION_UNLINK:
402 if ( count( $requests ) > 1 ) {
403 throw new InvalidArgumentException(
'only one auth request can be changed at a time' );
404 } elseif ( !$requests ) {
405 throw new InvalidArgumentException(
'no auth request' );
407 $req = reset( $requests );
408 $status = $authManager->allowsAuthenticationDataChange( $req );
409 $this->
getHookRunner()->onChangeAuthenticationDataAudit( $req, $status );
410 if ( !$status->isGood() ) {
411 return AuthenticationResponse::newFail( $status->getMessage() );
413 $authManager->changeAuthenticationData( $req );
414 return AuthenticationResponse::newPass();
417 throw new InvalidArgumentException(
'invalid action: ' . $action );
434 $form = $this->
getAuthForm( $this->authRequests, $this->authAction );
435 $form->setSubmitCallback( [ $this,
'handleFormSubmit' ] );
441 if ( $sessionToken->wasNew() ) {
442 return Status::newFatal( $this->
messageKey(
'authform-newtoken' ) );
443 } elseif ( !$requestTokenValue ) {
444 return Status::newFatal( $this->
messageKey(
'authform-notoken' ) );
445 } elseif ( !$sessionToken->match( $requestTokenValue ) ) {
446 return Status::newFatal( $this->
messageKey(
'authform-wrongtoken' ) );
449 $form->prepareForm();
450 $status = $form->trySubmit();
454 if ( $status ===
true ) {
456 throw new UnexpectedValueException(
'HTMLForm::trySubmit() returned true' );
457 } elseif ( $status ===
false ) {
459 } elseif ( $status instanceof
Status ) {
464 $status = Status::wrap( $status );
465 } elseif ( is_string( $status ) ) {
466 $status = Status::newFatal(
new RawMessage(
'$1', [ $status ] ) );
467 } elseif ( is_array( $status ) ) {
468 if ( is_string( reset( $status ) ) ) {
470 $status = Status::newFatal( ...$status );
471 } elseif ( is_array( reset( $status ) ) ) {
472 $ret = Status::newGood();
473 foreach ( $status as $message ) {
475 $ret->fatal( ...$message );
479 throw new UnexpectedValueException(
'invalid HTMLForm::trySubmit() return value: '
480 .
'first element of array is ' . gettype( reset( $status ) ) );
485 throw new UnexpectedValueException(
'invalid HTMLForm::trySubmit() return type: '
486 . gettype( $status ) );
489 if ( ( !$status || !$status->isOK() ) && $this->isReturn ) {
494 LoggerFactory::getInstance(
'authentication' )
495 ->warning(
'Validation error on return', [
'data' => $form->mFieldData,
496 'status' => $status->getWikiText(
false,
false,
'en' ) ] );
502 AuthManager::ACTION_CHANGE, AuthManager::ACTION_REMOVE, AuthManager::ACTION_UNLINK
504 if ( in_array( $this->authAction, $changeActions,
true ) && $status && !$status->isOK() ) {
505 $this->
getHookRunner()->onChangeAuthenticationDataAudit( reset( $this->authRequests ), $status );
518 $requests = AuthenticationRequest::loadRequestsFromSubmission( $this->authRequests, $data );
522 return Status::newGood( $response );
552 $fieldInfo = AuthenticationRequest::mergeFieldInfo( $requests );
557 return $formDescriptor;
569 if ( $context->getRequest() !== $this->getRequest() ) {
574 $form = HTMLForm::factory(
'ooui', $formDescriptor, $context );
577 $form->addHiddenField(
'authAction', $this->authAction );
589 $status = Status::wrap( $status );
591 $form = $this->
getAuthForm( $this->authRequests, $this->authAction );
592 $form->prepareForm()->displayForm( $status );
607 $customSubmitButtonPresent =
false;
613 foreach ( $requests as $req ) {
614 if ( $req->required === AuthenticationRequest::PRIMARY_REQUIRED ) {
616 $customSubmitButtonPresent =
true;
622 return !$customSubmitButtonPresent;
632 if ( $info[
'type'] ===
'button' ) {
646 foreach ( $formDescriptor as &$definition ) {
648 if ( array_key_exists(
'class', $definition ) ) {
649 $class = $definition[
'class'];
650 } elseif ( array_key_exists(
'type', $definition ) ) {
651 $class = HTMLForm::$typeMappings[$definition[
'type']];
653 if ( $class !== HTMLInfoField::class ) {
654 $definition[
'tabindex'] = $i;
666 return $this->
getRequest()->getSession()->getToken(
'AuthManagerSpecialPage:'
676 return 'wpAuthToken';
689 $formDescriptor = [];
690 foreach ( $fieldInfo as $fieldName => $singleFieldInfo ) {
694 $requestSnapshot = serialize( $requests );
696 $this->
getHookRunner()->onAuthChangeFormFields( $requests, $fieldInfo,
697 $formDescriptor, $action );
698 if ( $requestSnapshot !== serialize( $requests ) ) {
699 LoggerFactory::getInstance(
'authentication' )->warning(
700 'AuthChangeFormFields hook changed auth requests' );
707 return $formDescriptor;
722 'name' => $fieldName,
725 if ( $type ===
'submit' && isset( $singleFieldInfo[
'label'] ) ) {
726 $descriptor[
'default'] = $singleFieldInfo[
'label']->plain();
727 } elseif ( $type !==
'submit' ) {
728 $descriptor += array_filter( [
730 'label-message' => self::getField( $singleFieldInfo,
'label' ),
733 if ( isset( $singleFieldInfo[
'options'] ) ) {
734 $descriptor[
'options'] = array_flip( array_map(
static function ( $message ) {
736 return $message->parse();
737 }, $singleFieldInfo[
'options'] ) );
740 if ( isset( $singleFieldInfo[
'value'] ) ) {
741 $descriptor[
'default'] = $singleFieldInfo[
'value'];
744 if ( empty( $singleFieldInfo[
'optional'] ) ) {
745 $descriptor[
'required'] =
true;
760 foreach ( $formDescriptor as &$field ) {
761 $field[
'__index'] = $i++;
763 uasort( $formDescriptor,
static function ( $first, $second ) {
765 ?: $first[
'__index'] <=> $second[
'__index'];
767 foreach ( $formDescriptor as &$field ) {
768 unset( $field[
'__index'] );
779 protected static function getField( array $array, $fieldName, $default =
null ) {
780 if ( array_key_exists( $fieldName, $array ) ) {
781 return $array[$fieldName];
796 'password' =>
'password',
797 'select' =>
'select',
798 'checkbox' =>
'check',
799 'multiselect' =>
'multiselect',
800 'button' =>
'submit',
801 'hidden' =>
'hidden',
804 if ( !array_key_exists( $type, $map ) ) {
805 throw new \LogicException(
'invalid field type: ' . $type );
823 array $fieldInfo, array $formDescriptor, array $defaultFormDescriptor
826 foreach ( $defaultFormDescriptor as $fieldName => $defaultField ) {
830 !isset( $fieldInfo[$fieldName] )
832 !isset( $defaultField[
'baseField'] )
833 || !isset( $fieldInfo[$defaultField[
'baseField']] )
836 !isset( $defaultField[
'type'] )
837 || !in_array( $defaultField[
'type'], [
'submit',
'info' ],
true )
840 $defaultFormDescriptor[$fieldName] =
null;
845 $requestField = $formDescriptor[$fieldName] ?? [];
847 isset( $defaultField[
'label'] )
848 || isset( $defaultField[
'label-message'] )
849 || isset( $defaultField[
'label-raw'] )
851 unset( $requestField[
'label'], $requestField[
'label-message'], $defaultField[
'label-raw'] );
854 $defaultFormDescriptor[$fieldName] += $requestField;
857 return array_filter( $defaultFormDescriptor + $formDescriptor );
865class_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....
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.
An information field (text blob), not a proper input.
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::...
getPreservedParams( $withToken=false)
Returns URL query parameters which can be used to reload the page (or leave and return) while preserv...
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.
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.
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.
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 name of this Special Page.
getFullTitle()
Return the full title, including $par.
Generic operation result class Has warning/error list, boolean status and arbitrary value.