6use InvalidArgumentException;
23use UnexpectedValueException;
40 AuthManager::ACTION_LOGIN, AuthManager::ACTION_LOGIN_CONTINUE,
41 AuthManager::ACTION_CREATE, AuthManager::ACTION_CREATE_CONTINUE,
42 AuthManager::ACTION_LINK, AuthManager::ACTION_LINK_CONTINUE,
43 AuthManager::ACTION_CHANGE, AuthManager::ACTION_REMOVE, AuthManager::ACTION_UNLINK,
77 array $requests, array $fieldInfo, array &$formDescriptor, $action
90 return $this->savedRequest ?: $this->
getContext()->getRequest();
103 protected function setRequest( array $data, $wasPosted =
null ) {
107 $data + $request->getQueryValues(),
108 $wasPosted ?? $request->wasPosted()
143 $key =
'AuthManagerSpecialPage:return:' . $this->
getName();
150 $authData = array_diff_key( $this->
getRequest()->getValues(),
151 $preservedParams, [
'title' => 1 ] );
152 $authManager->setAuthenticationSessionData( $key, $authData );
159 $authData = $authManager->getAuthenticationSessionData( $key );
161 $authManager->removeAuthenticationSessionData( $key );
162 $this->isReturn =
true;
182 $key =
'AuthManagerSpecialPage:reauth:' . $this->
getName();
185 if ( $securityLevel ) {
186 $securityStatus = $authManager->securitySensitiveOperationStatus( $securityLevel );
187 if ( $securityStatus === AuthManager::SEC_REAUTH ) {
188 $queryParams = array_diff_key( $request->getQueryValues(), [
'title' =>
true ] );
190 if ( $request->wasPosted() ) {
192 $uniqueId = MWCryptRand::generateHex( 6 );
193 $key .=
':' . $uniqueId;
195 $queryParams = [
'authUniqueId' => $uniqueId ] + $queryParams;
196 $authData = array_diff_key( $request->getValues(),
197 $this->getPreservedParams(), [
'title' => 1 ] );
198 $authManager->setAuthenticationSessionData( $key, $authData );
202 $url = $title->getFullURL( [
203 'returnto' => $this->
getFullTitle()->getPrefixedDBkey(),
205 'force' => $securityLevel,
212 if ( $securityStatus !== AuthManager::SEC_OK ) {
213 throw new ErrorPageError(
'cannotauth-not-allowed-title',
'cannotauth-not-allowed' );
217 $uniqueId = $request->getVal(
'authUniqueId' );
219 $key .=
':' . $uniqueId;
220 $authData = $authManager->getAuthenticationSessionData( $key );
222 $authManager->removeAuthenticationSessionData( $key );
246 return array_key_exists( $defaultKey, static::$messages )
247 ? static::$messages[$defaultKey] : $defaultKey;
274 !$reset && $this->subPage ===
$subPage && $this->authAction
282 $this->authAction =
$authAction ?: $request->getText(
'authAction' );
283 if ( !in_array( $this->authAction, static::$allowedActions,
true ) ) {
285 if ( $request->wasPosted() ) {
287 if ( in_array( $continueAction, static::$allowedActions,
true ) ) {
288 $this->authAction = $continueAction;
294 $this->authAction, $this->
getUser() );
295 $this->authRequests = array_filter( $allReqs,
function ( $req ) {
305 return in_array( $this->authAction, [
306 AuthManager::ACTION_LOGIN_CONTINUE,
307 AuthManager::ACTION_CREATE_CONTINUE,
308 AuthManager::ACTION_LINK_CONTINUE,
319 case AuthManager::ACTION_LOGIN:
320 $action = AuthManager::ACTION_LOGIN_CONTINUE;
322 case AuthManager::ACTION_CREATE:
323 $action = AuthManager::ACTION_CREATE_CONTINUE;
325 case AuthManager::ACTION_LINK:
326 $action = AuthManager::ACTION_LINK_CONTINUE;
342 if ( !in_array( $action, static::$allowedActions,
true ) ) {
343 throw new InvalidArgumentException(
'invalid action: ' . $action );
348 : $authManager->getAuthenticationRequests( $action );
355 case AuthManager::ACTION_LOGIN:
356 case AuthManager::ACTION_LOGIN_CONTINUE:
357 return $authManager->canAuthenticateNow();
358 case AuthManager::ACTION_CREATE:
359 case AuthManager::ACTION_CREATE_CONTINUE:
360 return $authManager->canCreateAccounts();
361 case AuthManager::ACTION_LINK:
362 case AuthManager::ACTION_LINK_CONTINUE:
363 return $authManager->canLinkAccounts();
364 case AuthManager::ACTION_CHANGE:
365 case AuthManager::ACTION_REMOVE:
366 case AuthManager::ACTION_UNLINK:
370 throw new InvalidArgumentException(
'invalid action: ' . $action );
381 if ( !in_array( $action, static::$allowedActions,
true ) ) {
382 throw new InvalidArgumentException(
'invalid action: ' . $action );
390 case AuthManager::ACTION_LOGIN:
391 return $authManager->beginAuthentication( $requests, $returnToUrl );
392 case AuthManager::ACTION_LOGIN_CONTINUE:
393 return $authManager->continueAuthentication( $requests );
394 case AuthManager::ACTION_CREATE:
395 return $authManager->beginAccountCreation( $this->
getAuthority(), $requests,
397 case AuthManager::ACTION_CREATE_CONTINUE:
398 return $authManager->continueAccountCreation( $requests );
399 case AuthManager::ACTION_LINK:
400 return $authManager->beginAccountLink( $this->
getUser(), $requests, $returnToUrl );
401 case AuthManager::ACTION_LINK_CONTINUE:
402 return $authManager->continueAccountLink( $requests );
403 case AuthManager::ACTION_CHANGE:
404 case AuthManager::ACTION_REMOVE:
405 case AuthManager::ACTION_UNLINK:
406 if ( count( $requests ) > 1 ) {
407 throw new InvalidArgumentException(
'only one auth request can be changed at a time' );
411 throw new InvalidArgumentException(
'no auth request' );
413 $req = reset( $requests );
414 $status = $authManager->allowsAuthenticationDataChange( $req );
415 $this->
getHookRunner()->onChangeAuthenticationDataAudit( $req, $status );
416 if ( !$status->isGood() ) {
417 return AuthenticationResponse::newFail( $status->getMessage() );
419 $authManager->changeAuthenticationData( $req );
420 return AuthenticationResponse::newPass();
423 throw new InvalidArgumentException(
'invalid action: ' . $action );
440 $form = $this->
getAuthForm( $this->authRequests, $this->authAction );
441 $form->setSubmitCallback( [ $this,
'handleFormSubmit' ] );
447 if ( $sessionToken->wasNew() ) {
448 return Status::newFatal( $this->
messageKey(
'authform-newtoken' ) );
449 } elseif ( !$requestTokenValue ) {
450 return Status::newFatal( $this->
messageKey(
'authform-notoken' ) );
451 } elseif ( !$sessionToken->match( $requestTokenValue ) ) {
452 return Status::newFatal( $this->
messageKey(
'authform-wrongtoken' ) );
455 $form->prepareForm();
456 $status = $form->trySubmit();
460 if ( $status ===
true ) {
462 throw new UnexpectedValueException(
'HTMLForm::trySubmit() returned true' );
463 } elseif ( $status ===
false ) {
465 } elseif ( $status instanceof
Status ) {
470 $status = Status::wrap( $status );
471 } elseif ( is_string( $status ) ) {
472 $status = Status::newFatal(
new RawMessage(
'$1', [ $status ] ) );
473 } elseif ( is_array( $status ) ) {
474 if ( is_string( reset( $status ) ) ) {
476 $status = Status::newFatal( ...$status );
477 } elseif ( is_array( reset( $status ) ) ) {
478 $ret = Status::newGood();
479 foreach ( $status as $message ) {
481 $ret->fatal( ...$message );
485 throw new UnexpectedValueException(
'invalid HTMLForm::trySubmit() return value: '
486 .
'first element of array is ' . get_debug_type( reset( $status ) ) );
491 throw new UnexpectedValueException(
'invalid HTMLForm::trySubmit() return type: '
492 . get_debug_type( $status ) );
495 if ( ( !$status || !$status->isOK() ) && $this->isReturn ) {
500 LoggerFactory::getInstance(
'authentication' )
501 ->warning(
'Validation error on return', [
'data' => $form->mFieldData,
502 'status' => $status->getWikiText(
false,
false,
'en' ) ] );
508 AuthManager::ACTION_CHANGE, AuthManager::ACTION_REMOVE, AuthManager::ACTION_UNLINK
510 if ( in_array( $this->authAction, $changeActions,
true ) && $status && !$status->isOK() ) {
511 $this->
getHookRunner()->onChangeAuthenticationDataAudit( reset( $this->authRequests ), $status );
524 $requests = AuthenticationRequest::loadRequestsFromSubmission( $this->authRequests, $data );
528 return Status::newGood( $response );
552 if ( is_bool( $options ) ) {
553 wfDeprecated( __METHOD__ .
' boolean $options',
'1.43' );
554 $options = [
'withToken' => $options ];
558 'withToken' =>
false,
561 '@phan-var array{reset: bool, withToken: bool} $options';
566 'uselang' => $request->getVal(
'uselang' ),
567 'variant' => $request->getVal(
'variant' ),
568 'returnto' => $request->getVal(
'returnto' ),
569 'returntoquery' => $request->getVal(
'returntoquery' ),
570 'returntoanchor' => $request->getVal(
'returntoanchor' ),
573 if ( !$options[
'reset'] && $this->authAction !== $this->
getDefaultAction( $this->subPage ) ) {
577 if ( $options[
'withToken'] ) {
584 $params, [
'reset' => $options[
'reset'] ]
587 return array_filter(
$params, fn ( $val ) => $val !==
null );
598 $fieldInfo = AuthenticationRequest::mergeFieldInfo( $requests );
603 return $formDescriptor;
615 if ( $context->getRequest() !== $this->getRequest() ) {
620 $form = HTMLForm::factory(
'ooui', $formDescriptor, $context );
623 $form->addHiddenField(
'authAction', $this->authAction );
635 $status = Status::wrap( $status );
637 $form = $this->
getAuthForm( $this->authRequests, $this->authAction );
638 $form->prepareForm()->displayForm( $status );
653 $customSubmitButtonPresent =
false;
659 foreach ( $requests as $req ) {
660 if ( $req->required === AuthenticationRequest::PRIMARY_REQUIRED ) {
662 $customSubmitButtonPresent =
true;
668 return !$customSubmitButtonPresent;
678 if ( $info[
'type'] ===
'button' ) {
692 foreach ( $formDescriptor as &$definition ) {
694 if ( array_key_exists(
'class', $definition ) ) {
695 $class = $definition[
'class'];
696 } elseif ( array_key_exists(
'type', $definition ) ) {
697 $class = HTMLForm::$typeMappings[$definition[
'type']];
699 if ( $class !== HTMLInfoField::class ) {
700 $definition[
'tabindex'] = $i;
712 return $this->
getRequest()->getSession()->getToken(
'AuthManagerSpecialPage:'
722 return 'wpAuthToken';
735 $formDescriptor = [];
736 foreach ( $fieldInfo as $fieldName => $singleFieldInfo ) {
740 $requestSnapshot = serialize( $requests );
742 $this->
getHookRunner()->onAuthChangeFormFields( $requests, $fieldInfo,
743 $formDescriptor, $action );
744 if ( $requestSnapshot !== serialize( $requests ) ) {
745 LoggerFactory::getInstance(
'authentication' )->warning(
746 'AuthChangeFormFields hook changed auth requests' );
753 return $formDescriptor;
768 'name' => $fieldName,
771 if ( $type ===
'submit' && isset( $singleFieldInfo[
'label'] ) ) {
772 $descriptor[
'default'] = $singleFieldInfo[
'label']->plain();
773 } elseif ( $type !==
'submit' ) {
774 $descriptor += array_filter( [
776 'label-message' => self::getField( $singleFieldInfo,
'label' ),
779 if ( isset( $singleFieldInfo[
'options'] ) ) {
780 $descriptor[
'options'] = array_flip( array_map(
static function ( $message ) {
782 return $message->parse();
783 }, $singleFieldInfo[
'options'] ) );
786 if ( isset( $singleFieldInfo[
'value'] ) ) {
787 $descriptor[
'default'] = $singleFieldInfo[
'value'];
790 if ( empty( $singleFieldInfo[
'optional'] ) ) {
791 $descriptor[
'required'] =
true;
806 foreach ( $formDescriptor as &$field ) {
807 $field[
'__index'] = $i++;
810 uasort( $formDescriptor,
static function ( $first, $second ) {
812 ?: $first[
'__index'] <=> $second[
'__index'];
814 foreach ( $formDescriptor as &$field ) {
815 unset( $field[
'__index'] );
826 protected static function getField( array $array, $fieldName, $default =
null ) {
827 if ( array_key_exists( $fieldName, $array ) ) {
828 return $array[$fieldName];
844 'password' =>
'password',
845 'select' =>
'select',
846 'checkbox' =>
'check',
847 'multiselect' =>
'multiselect',
848 'button' =>
'submit',
849 'hidden' =>
'hidden',
852 if ( !array_key_exists( $type, $map ) ) {
853 throw new InvalidArgumentException(
'invalid field type: ' . $type );
871 array $fieldInfo, array $formDescriptor, array $defaultFormDescriptor
874 foreach ( $defaultFormDescriptor as $fieldName => $defaultField ) {
878 !isset( $fieldInfo[$fieldName] )
880 !isset( $defaultField[
'baseField'] )
881 || !isset( $fieldInfo[$defaultField[
'baseField']] )
884 !isset( $defaultField[
'type'] )
885 || !in_array( $defaultField[
'type'], [
'submit',
'info' ],
true )
888 $defaultFormDescriptor[$fieldName] =
null;
893 $requestField = $formDescriptor[$fieldName] ?? [];
895 isset( $defaultField[
'label'] )
896 || isset( $defaultField[
'label-message'] )
897 || isset( $defaultField[
'label-raw'] )
899 unset( $requestField[
'label'], $requestField[
'label-message'], $defaultField[
'label-raw'] );
902 $defaultFormDescriptor[$fieldName] += $requestField;
905 return array_filter( $defaultFormDescriptor + $formDescriptor );
910class_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.
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::...
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.
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.
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.