7use InvalidArgumentException;
23use UnexpectedValueException;
39 AuthManager::ACTION_LOGIN, AuthManager::ACTION_LOGIN_CONTINUE,
40 AuthManager::ACTION_CREATE, AuthManager::ACTION_CREATE_CONTINUE,
41 AuthManager::ACTION_LINK, AuthManager::ACTION_LINK_CONTINUE,
42 AuthManager::ACTION_CHANGE, AuthManager::ACTION_REMOVE, AuthManager::ACTION_UNLINK,
76 array $requests, array $fieldInfo, array &$formDescriptor, $action
89 return $this->savedRequest ?: $this->
getContext()->getRequest();
102 protected function setRequest( array $data, $wasPosted =
null ) {
106 $data + $request->getQueryValues(),
107 $wasPosted ?? $request->wasPosted()
142 $key =
'AuthManagerSpecialPage:return:' . $this->
getName();
149 $authData = array_diff_key( $this->
getRequest()->getValues(),
150 $preservedParams, [
'title' => 1 ] );
151 $authManager->setAuthenticationSessionData( $key, $authData );
158 $authData = $authManager->getAuthenticationSessionData( $key );
160 $authManager->removeAuthenticationSessionData( $key );
161 $this->isReturn =
true;
181 $key =
'AuthManagerSpecialPage:reauth:' . $this->
getName();
184 if ( $securityLevel ) {
185 $securityStatus = $authManager->securitySensitiveOperationStatus( $securityLevel );
186 if ( $securityStatus === AuthManager::SEC_REAUTH ) {
187 $queryParams = array_diff_key( $request->getQueryValues(), [
'title' =>
true ] );
189 if ( $request->wasPosted() ) {
191 $uniqueId = MWCryptRand::generateHex( 6 );
192 $key .=
':' . $uniqueId;
194 $queryParams = [
'authUniqueId' => $uniqueId ] + $queryParams;
195 $authData = array_diff_key( $request->getValues(),
196 $this->getPreservedParams(
false ), [
'title' => 1 ] );
197 $authManager->setAuthenticationSessionData( $key, $authData );
201 $url = $title->getFullURL( [
202 'returnto' => $this->
getFullTitle()->getPrefixedDBkey(),
204 'force' => $securityLevel,
211 if ( $securityStatus !== AuthManager::SEC_OK ) {
212 throw new ErrorPageError(
'cannotauth-not-allowed-title',
'cannotauth-not-allowed' );
216 $uniqueId = $request->getVal(
'authUniqueId' );
218 $key .=
':' . $uniqueId;
219 $authData = $authManager->getAuthenticationSessionData( $key );
221 $authManager->removeAuthenticationSessionData( $key );
245 return array_key_exists( $defaultKey, static::$messages )
246 ? static::$messages[$defaultKey] : $defaultKey;
273 !$reset && $this->subPage ===
$subPage && $this->authAction
281 $this->authAction =
$authAction ?: $request->getText(
'authAction' );
282 if ( !in_array( $this->authAction, static::$allowedActions,
true ) ) {
284 if ( $request->wasPosted() ) {
286 if ( in_array( $continueAction, static::$allowedActions,
true ) ) {
287 $this->authAction = $continueAction;
293 $this->authAction, $this->
getUser() );
294 $this->authRequests = array_filter( $allReqs,
function ( $req ) {
304 return in_array( $this->authAction, [
305 AuthManager::ACTION_LOGIN_CONTINUE,
306 AuthManager::ACTION_CREATE_CONTINUE,
307 AuthManager::ACTION_LINK_CONTINUE,
318 case AuthManager::ACTION_LOGIN:
319 $action = AuthManager::ACTION_LOGIN_CONTINUE;
321 case AuthManager::ACTION_CREATE:
322 $action = AuthManager::ACTION_CREATE_CONTINUE;
324 case AuthManager::ACTION_LINK:
325 $action = AuthManager::ACTION_LINK_CONTINUE;
341 if ( !in_array( $action, static::$allowedActions,
true ) ) {
342 throw new InvalidArgumentException(
'invalid action: ' . $action );
347 : $authManager->getAuthenticationRequests( $action );
354 case AuthManager::ACTION_LOGIN:
355 case AuthManager::ACTION_LOGIN_CONTINUE:
356 return $authManager->canAuthenticateNow();
357 case AuthManager::ACTION_CREATE:
358 case AuthManager::ACTION_CREATE_CONTINUE:
359 return $authManager->canCreateAccounts();
360 case AuthManager::ACTION_LINK:
361 case AuthManager::ACTION_LINK_CONTINUE:
362 return $authManager->canLinkAccounts();
363 case AuthManager::ACTION_CHANGE:
364 case AuthManager::ACTION_REMOVE:
365 case AuthManager::ACTION_UNLINK:
369 throw new InvalidArgumentException(
'invalid action: ' . $action );
380 if ( !in_array( $action, static::$allowedActions,
true ) ) {
381 throw new InvalidArgumentException(
'invalid action: ' . $action );
389 case AuthManager::ACTION_LOGIN:
390 return $authManager->beginAuthentication( $requests, $returnToUrl );
391 case AuthManager::ACTION_LOGIN_CONTINUE:
392 return $authManager->continueAuthentication( $requests );
393 case AuthManager::ACTION_CREATE:
394 return $authManager->beginAccountCreation( $this->
getAuthority(), $requests,
396 case AuthManager::ACTION_CREATE_CONTINUE:
397 return $authManager->continueAccountCreation( $requests );
398 case AuthManager::ACTION_LINK:
399 return $authManager->beginAccountLink( $this->
getUser(), $requests, $returnToUrl );
400 case AuthManager::ACTION_LINK_CONTINUE:
401 return $authManager->continueAccountLink( $requests );
402 case AuthManager::ACTION_CHANGE:
403 case AuthManager::ACTION_REMOVE:
404 case AuthManager::ACTION_UNLINK:
405 if ( count( $requests ) > 1 ) {
406 throw new InvalidArgumentException(
'only one auth request can be changed at a time' );
410 throw new InvalidArgumentException(
'no auth request' );
412 $req = reset( $requests );
413 $status = $authManager->allowsAuthenticationDataChange( $req );
414 $this->
getHookRunner()->onChangeAuthenticationDataAudit( $req, $status );
415 if ( !$status->isGood() ) {
416 return AuthenticationResponse::newFail( $status->getMessage() );
418 $authManager->changeAuthenticationData( $req );
419 return AuthenticationResponse::newPass();
422 throw new InvalidArgumentException(
'invalid action: ' . $action );
439 $form = $this->
getAuthForm( $this->authRequests, $this->authAction );
440 $form->setSubmitCallback( [ $this,
'handleFormSubmit' ] );
446 if ( $sessionToken->wasNew() ) {
447 return Status::newFatal( $this->
messageKey(
'authform-newtoken' ) );
448 } elseif ( !$requestTokenValue ) {
449 return Status::newFatal( $this->
messageKey(
'authform-notoken' ) );
450 } elseif ( !$sessionToken->match( $requestTokenValue ) ) {
451 return Status::newFatal( $this->
messageKey(
'authform-wrongtoken' ) );
454 $form->prepareForm();
455 $status = $form->trySubmit();
459 if ( $status ===
true ) {
461 throw new UnexpectedValueException(
'HTMLForm::trySubmit() returned true' );
462 } elseif ( $status ===
false ) {
464 } elseif ( $status instanceof
Status ) {
469 $status = Status::wrap( $status );
470 } elseif ( is_string( $status ) ) {
471 $status = Status::newFatal(
new RawMessage(
'$1', [ $status ] ) );
472 } elseif ( is_array( $status ) ) {
473 if ( is_string( reset( $status ) ) ) {
475 $status = Status::newFatal( ...$status );
476 } elseif ( is_array( reset( $status ) ) ) {
477 $ret = Status::newGood();
478 foreach ( $status as $message ) {
480 $ret->fatal( ...$message );
484 throw new UnexpectedValueException(
'invalid HTMLForm::trySubmit() return value: '
485 .
'first element of array is ' . gettype( reset( $status ) ) );
490 throw new UnexpectedValueException(
'invalid HTMLForm::trySubmit() return type: '
491 . gettype( $status ) );
494 if ( ( !$status || !$status->isOK() ) && $this->isReturn ) {
499 LoggerFactory::getInstance(
'authentication' )
500 ->warning(
'Validation error on return', [
'data' => $form->mFieldData,
501 'status' => $status->getWikiText(
false,
false,
'en' ) ] );
507 AuthManager::ACTION_CHANGE, AuthManager::ACTION_REMOVE, AuthManager::ACTION_UNLINK
509 if ( in_array( $this->authAction, $changeActions,
true ) && $status && !$status->isOK() ) {
510 $this->
getHookRunner()->onChangeAuthenticationDataAudit( reset( $this->authRequests ), $status );
523 $requests = AuthenticationRequest::loadRequestsFromSubmission( $this->authRequests, $data );
527 return Status::newGood( $response );
557 $fieldInfo = AuthenticationRequest::mergeFieldInfo( $requests );
562 return $formDescriptor;
574 if ( $context->getRequest() !== $this->getRequest() ) {
579 $form = HTMLForm::factory(
'ooui', $formDescriptor, $context );
582 $form->addHiddenField(
'authAction', $this->authAction );
594 $status = Status::wrap( $status );
596 $form = $this->
getAuthForm( $this->authRequests, $this->authAction );
597 $form->prepareForm()->displayForm( $status );
612 $customSubmitButtonPresent =
false;
618 foreach ( $requests as $req ) {
619 if ( $req->required === AuthenticationRequest::PRIMARY_REQUIRED ) {
621 $customSubmitButtonPresent =
true;
627 return !$customSubmitButtonPresent;
637 if ( $info[
'type'] ===
'button' ) {
651 foreach ( $formDescriptor as &$definition ) {
653 if ( array_key_exists(
'class', $definition ) ) {
654 $class = $definition[
'class'];
655 } elseif ( array_key_exists(
'type', $definition ) ) {
656 $class = HTMLForm::$typeMappings[$definition[
'type']];
658 if ( $class !== HTMLInfoField::class ) {
659 $definition[
'tabindex'] = $i;
671 return $this->
getRequest()->getSession()->getToken(
'AuthManagerSpecialPage:'
681 return 'wpAuthToken';
694 $formDescriptor = [];
695 foreach ( $fieldInfo as $fieldName => $singleFieldInfo ) {
699 $requestSnapshot = serialize( $requests );
701 $this->
getHookRunner()->onAuthChangeFormFields( $requests, $fieldInfo,
702 $formDescriptor, $action );
703 if ( $requestSnapshot !== serialize( $requests ) ) {
704 LoggerFactory::getInstance(
'authentication' )->warning(
705 'AuthChangeFormFields hook changed auth requests' );
712 return $formDescriptor;
727 'name' => $fieldName,
730 if ( $type ===
'submit' && isset( $singleFieldInfo[
'label'] ) ) {
731 $descriptor[
'default'] = $singleFieldInfo[
'label']->plain();
732 } elseif ( $type !==
'submit' ) {
733 $descriptor += array_filter( [
735 'label-message' => self::getField( $singleFieldInfo,
'label' ),
738 if ( isset( $singleFieldInfo[
'options'] ) ) {
739 $descriptor[
'options'] = array_flip( array_map(
static function ( $message ) {
741 return $message->parse();
742 }, $singleFieldInfo[
'options'] ) );
745 if ( isset( $singleFieldInfo[
'value'] ) ) {
746 $descriptor[
'default'] = $singleFieldInfo[
'value'];
749 if ( empty( $singleFieldInfo[
'optional'] ) ) {
750 $descriptor[
'required'] =
true;
765 foreach ( $formDescriptor as &$field ) {
766 $field[
'__index'] = $i++;
769 uasort( $formDescriptor,
static function ( $first, $second ) {
771 ?: $first[
'__index'] <=> $second[
'__index'];
773 foreach ( $formDescriptor as &$field ) {
774 unset( $field[
'__index'] );
785 protected static function getField( array $array, $fieldName, $default =
null ) {
786 if ( array_key_exists( $fieldName, $array ) ) {
787 return $array[$fieldName];
803 'password' =>
'password',
804 'select' =>
'select',
805 'checkbox' =>
'check',
806 'multiselect' =>
'multiselect',
807 'button' =>
'submit',
808 'hidden' =>
'hidden',
811 if ( !array_key_exists( $type, $map ) ) {
812 throw new InvalidArgumentException(
'invalid field type: ' . $type );
830 array $fieldInfo, array $formDescriptor, array $defaultFormDescriptor
833 foreach ( $defaultFormDescriptor as $fieldName => $defaultField ) {
837 !isset( $fieldInfo[$fieldName] )
839 !isset( $defaultField[
'baseField'] )
840 || !isset( $fieldInfo[$defaultField[
'baseField']] )
843 !isset( $defaultField[
'type'] )
844 || !in_array( $defaultField[
'type'], [
'submit',
'info' ],
true )
847 $defaultFormDescriptor[$fieldName] =
null;
852 $requestField = $formDescriptor[$fieldName] ?? [];
854 isset( $defaultField[
'label'] )
855 || isset( $defaultField[
'label-message'] )
856 || isset( $defaultField[
'label-raw'] )
858 unset( $requestField[
'label'], $requestField[
'label-message'], $defaultField[
'label-raw'] );
861 $defaultFormDescriptor[$fieldName] += $requestField;
864 return array_filter( $defaultFormDescriptor + $formDescriptor );
869class_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....
array $params
The job parameters.
An error page which can definitely be safely rendered using the OutputPage.
An IContextSource implementation which will inherit context from another source but allow individual ...
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 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.