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,
83 array $requests, array $fieldInfo, array &$formDescriptor, $action
97 return $this->savedRequest ?: $this->
getContext()->getRequest();
110 protected function setRequest( array $data, $wasPosted =
null ) {
112 $this->isFakePostRequest = $wasPosted ===
true && $request->wasPosted() ===
false;
115 $data + $request->getQueryValues(),
116 $wasPosted ?? $request->wasPosted()
146 $key =
'AuthManagerSpecialPage:return:' . $this->
getName();
148 if ( $subPage ===
'return' ) {
153 $authData = array_diff_key( $this->
getRequest()->getValues(),
154 $preservedParams, [
'title' => 1 ] );
155 $uniqueId = MWCryptRand::generateHex( 6 );
156 $preservedParams[
'authUniqueId'] = $uniqueId;
157 $key .=
':' . $uniqueId;
158 $authManager->setAuthenticationSessionData( $key, $authData );
163 } elseif ( $this->
getRequest()->getCheck(
'authUniqueId' ) ) {
164 $uniqueId = $this->
getRequest()->getVal(
'authUniqueId' );
165 $key .=
':' . $uniqueId;
166 $authData = $authManager->getAuthenticationSessionData( $key );
168 $authManager->removeAuthenticationSessionData( $key );
169 $this->isReturn =
true;
171 $this->setPostTransactionProfilerExpectations( __METHOD__ );
191 $key =
'AuthManagerSpecialPage:reauth:' . $this->
getName();
194 if ( $securityLevel ) {
195 $securityStatus = $authManager->securitySensitiveOperationStatus( $securityLevel );
196 if ( $securityStatus === AuthManager::SEC_REAUTH ) {
197 $queryParams = array_diff_key( $request->getQueryValues(), [
'title' =>
true ] );
199 if ( $request->wasPosted() ) {
201 $uniqueId = MWCryptRand::generateHex( 6 );
202 $key .=
':' . $uniqueId;
204 $queryParams = [
'authUniqueId' => $uniqueId ] + $queryParams;
205 $authData = array_diff_key( $request->getValues(),
206 $this->getPreservedParams(), [
'title' => 1 ] );
207 $authManager->setAuthenticationSessionData( $key, $authData );
212 $keepParams = [
'uselang',
'useskin',
'useformat',
'variant',
'debug',
'safemode' ];
215 $url = $title->getFullURL( [
216 'returnto' => $this->
getFullTitle()->getPrefixedDBkey(),
218 'force' => $securityLevel,
219 ] + array_intersect_key( $queryParams, array_fill_keys( $keepParams,
true ) ),
false,
PROTO_HTTPS );
225 if ( $securityStatus !== AuthManager::SEC_OK ) {
226 throw new ErrorPageError(
'cannotauth-not-allowed-title',
'cannotauth-not-allowed' );
230 $uniqueId = $request->getVal(
'authUniqueId' );
232 $key .=
':' . $uniqueId;
233 $authData = $authManager->getAuthenticationSessionData( $key );
235 $authManager->removeAuthenticationSessionData( $key );
237 $this->setPostTransactionProfilerExpectations( __METHOD__ );
244 private function setPostTransactionProfilerExpectations(
string $fname ) {
247 $trxProfiler->redefineExpectations( $trxLimits[
'POST'], $fname );
248 DeferredUpdates::addCallableUpdate(
static function () use ( $trxProfiler, $trxLimits, $fname ) {
249 $trxProfiler->redefineExpectations( $trxLimits[
'PostSend-POST'], $fname );
269 return array_key_exists( $defaultKey, static::$messages )
270 ? static::$messages[$defaultKey] : $defaultKey;
297 !$reset && $this->subPage ===
$subPage && $this->authAction
305 $this->authAction =
$authAction ?: $request->getText(
'authAction' );
306 if ( !in_array( $this->authAction, static::$allowedActions,
true ) ) {
308 if ( $request->wasPosted() ) {
310 if ( in_array( $continueAction, static::$allowedActions,
true ) ) {
311 $this->authAction = $continueAction;
317 $this->authAction, $this->
getUser() );
318 $this->authRequests = array_filter( $allReqs,
function ( $req ) {
328 return in_array( $this->authAction, [
329 AuthManager::ACTION_LOGIN_CONTINUE,
330 AuthManager::ACTION_CREATE_CONTINUE,
331 AuthManager::ACTION_LINK_CONTINUE,
342 case AuthManager::ACTION_LOGIN:
343 $action = AuthManager::ACTION_LOGIN_CONTINUE;
345 case AuthManager::ACTION_CREATE:
346 $action = AuthManager::ACTION_CREATE_CONTINUE;
348 case AuthManager::ACTION_LINK:
349 $action = AuthManager::ACTION_LINK_CONTINUE;
364 if ( !in_array( $action, static::$allowedActions,
true ) ) {
365 throw new InvalidArgumentException(
'invalid action: ' . $action );
370 : $authManager->getAuthenticationRequests( $action );
377 case AuthManager::ACTION_LOGIN:
378 case AuthManager::ACTION_LOGIN_CONTINUE:
379 return $authManager->canAuthenticateNow();
380 case AuthManager::ACTION_CREATE:
381 case AuthManager::ACTION_CREATE_CONTINUE:
382 return $authManager->canCreateAccounts();
383 case AuthManager::ACTION_LINK:
384 case AuthManager::ACTION_LINK_CONTINUE:
385 return $authManager->canLinkAccounts();
386 case AuthManager::ACTION_CHANGE:
387 case AuthManager::ACTION_REMOVE:
388 case AuthManager::ACTION_UNLINK:
392 throw new InvalidArgumentException(
'invalid action: ' . $action );
403 if ( !in_array( $action, static::$allowedActions,
true ) ) {
404 throw new InvalidArgumentException(
'invalid action: ' . $action );
412 case AuthManager::ACTION_LOGIN:
413 return $authManager->beginAuthentication( $requests, $returnToUrl );
414 case AuthManager::ACTION_LOGIN_CONTINUE:
415 return $authManager->continueAuthentication( $requests );
416 case AuthManager::ACTION_CREATE:
417 return $authManager->beginAccountCreation( $this->
getAuthority(), $requests,
419 case AuthManager::ACTION_CREATE_CONTINUE:
420 return $authManager->continueAccountCreation( $requests );
421 case AuthManager::ACTION_LINK:
422 return $authManager->beginAccountLink( $this->
getUser(), $requests, $returnToUrl );
423 case AuthManager::ACTION_LINK_CONTINUE:
424 return $authManager->continueAccountLink( $requests );
425 case AuthManager::ACTION_CHANGE:
426 case AuthManager::ACTION_REMOVE:
427 case AuthManager::ACTION_UNLINK:
428 if ( count( $requests ) > 1 ) {
429 throw new InvalidArgumentException(
'only one auth request can be changed at a time' );
433 throw new InvalidArgumentException(
'no auth request' );
435 $req = reset( $requests );
436 $status = $authManager->allowsAuthenticationDataChange( $req );
437 $this->
getHookRunner()->onChangeAuthenticationDataAudit( $req, $status );
438 if ( !$status->isGood() ) {
439 return AuthenticationResponse::newFail( $status->getMessage() );
441 $authManager->changeAuthenticationData( $req );
442 return AuthenticationResponse::newPass();
445 throw new InvalidArgumentException(
'invalid action: ' . $action );
462 $form = $this->
getAuthForm( $this->authRequests, $this->authAction );
469 if ( $sessionToken->wasNew() && ( !$this->isFakePostRequest || $this->isReturn ) ) {
471 return Status::newFatal( $this->
messageKey(
'authform-newtoken' ) );
472 } elseif ( !$requestTokenValue ) {
473 return Status::newFatal( $this->
messageKey(
'authform-notoken' ) );
474 } elseif ( !$sessionToken->match( $requestTokenValue ) ) {
475 return Status::newFatal( $this->
messageKey(
'authform-wrongtoken' ) );
478 $form->prepareForm();
479 $status = $form->trySubmit();
483 if ( $status ===
true ) {
485 throw new UnexpectedValueException(
'HTMLForm::trySubmit() returned true' );
486 } elseif ( $status ===
false ) {
488 } elseif ( $status instanceof
Status ) {
493 $status = Status::wrap( $status );
494 } elseif ( is_string( $status ) ) {
495 $status = Status::newFatal(
new RawMessage(
'$1', [ $status ] ) );
496 } elseif ( is_array( $status ) ) {
497 if ( is_string( reset( $status ) ) ) {
499 $status = Status::newFatal( ...$status );
500 } elseif ( is_array( reset( $status ) ) ) {
501 $ret = Status::newGood();
502 foreach ( $status as $message ) {
504 $ret->fatal( ...$message );
508 throw new UnexpectedValueException(
'invalid HTMLForm::trySubmit() return value: '
509 .
'first element of array is ' . get_debug_type( reset( $status ) ) );
514 throw new UnexpectedValueException(
'invalid HTMLForm::trySubmit() return type: '
515 . get_debug_type( $status ) );
518 if ( ( !$status || !$status->isOK() ) && $this->isReturn ) {
523 LoggerFactory::getInstance(
'authentication' )
524 ->warning(
'Validation error on return', [
'data' => $form->mFieldData,
525 'status' => $status->getWikiText(
false,
false,
'en' ) ] );
531 AuthManager::ACTION_CHANGE, AuthManager::ACTION_REMOVE, AuthManager::ACTION_UNLINK
533 if ( in_array( $this->authAction, $changeActions,
true ) && $status && !$status->isOK() ) {
534 $this->
getHookRunner()->onChangeAuthenticationDataAudit( reset( $this->authRequests ), $status );
547 $requests = AuthenticationRequest::loadRequestsFromSubmission( $this->authRequests, $data );
551 return Status::newGood( $response );
575 if ( is_bool( $options ) ) {
576 wfDeprecated( __METHOD__ .
' boolean $options',
'1.43' );
577 $options = [
'withToken' => $options ];
581 'withToken' =>
false,
587 'uselang' => $request->getVal(
'uselang' ),
588 'variant' => $request->getVal(
'variant' ),
589 'returnto' => $request->getVal(
'returnto' ),
590 'returntoquery' => $request->getVal(
'returntoquery' ),
591 'returntoanchor' => $request->getVal(
'returntoanchor' ),
594 if ( !$options[
'reset'] && $this->authAction !== $this->
getDefaultAction( $this->subPage ) ) {
598 if ( $options[
'withToken'] ) {
605 $params, [
'request' => $request,
'reset' => $options[
'reset'] ]
608 return array_filter( $params,
static fn ( $val ) => $val !==
null );
619 $fieldInfo = AuthenticationRequest::mergeFieldInfo( $requests );
624 return $formDescriptor;
636 if ( $context->getRequest() !== $this->getRequest() ) {
641 $form = HTMLForm::factory(
'ooui', $formDescriptor, $context );
644 $form->addHiddenField(
'authAction', $this->authAction );
656 $status = Status::wrap( $status );
658 $form = $this->
getAuthForm( $this->authRequests, $this->authAction );
659 $form->prepareForm()->displayForm( $status );
674 $customSubmitButtonPresent =
false;
680 foreach ( $requests as $req ) {
681 if ( $req->required === AuthenticationRequest::PRIMARY_REQUIRED ) {
683 $customSubmitButtonPresent =
true;
689 return !$customSubmitButtonPresent;
699 if ( $info[
'type'] ===
'button' ) {
713 foreach ( $formDescriptor as &$definition ) {
715 if ( array_key_exists(
'class', $definition ) ) {
716 $class = $definition[
'class'];
717 } elseif ( array_key_exists(
'type', $definition ) ) {
718 $class = HTMLForm::$typeMappings[$definition[
'type']];
720 if ( $class !== HTMLInfoField::class ) {
721 $definition[
'tabindex'] = $i;
733 return $this->
getRequest()->getSession()->getToken(
'AuthManagerSpecialPage:'
743 return 'wpAuthToken';
756 $formDescriptor = [];
757 foreach ( $fieldInfo as $fieldName => $singleFieldInfo ) {
761 $requestSnapshot = serialize( $requests );
763 $this->
getHookRunner()->onAuthChangeFormFields( $requests, $fieldInfo,
764 $formDescriptor, $action );
765 if ( $requestSnapshot !== serialize( $requests ) ) {
766 LoggerFactory::getInstance(
'authentication' )->warning(
767 'AuthChangeFormFields hook changed auth requests' );
774 return $formDescriptor;
789 'name' => $fieldName,
792 if ( $type ===
'submit' && isset( $singleFieldInfo[
'label'] ) ) {
793 $descriptor[
'default'] = $singleFieldInfo[
'label']->plain();
794 } elseif ( $type !==
'submit' ) {
795 $descriptor += array_filter( [
797 'label-message' => self::getField( $singleFieldInfo,
'label' ),
800 if ( isset( $singleFieldInfo[
'options'] ) ) {
801 $descriptor[
'options'] = array_flip( array_map(
static function ( $message ) {
803 return $message->parse();
804 }, $singleFieldInfo[
'options'] ) );
807 if ( isset( $singleFieldInfo[
'value'] ) ) {
808 $descriptor[
'default'] = $singleFieldInfo[
'value'];
811 if ( empty( $singleFieldInfo[
'optional'] ) ) {
812 $descriptor[
'required'] =
true;
826 foreach ( $formDescriptor as &$field ) {
827 $field[
'__index'] = $i++;
830 uasort( $formDescriptor,
static function ( $first, $second ) {
832 ?: $first[
'__index'] <=> $second[
'__index'];
834 foreach ( $formDescriptor as &$field ) {
835 unset( $field[
'__index'] );
846 protected static function getField( array $array, $fieldName, $default =
null ) {
847 if ( array_key_exists( $fieldName, $array ) ) {
848 return $array[$fieldName];
864 'password' =>
'password',
865 'select' =>
'select',
866 'checkbox' =>
'check',
867 'multiselect' =>
'multiselect',
868 'button' =>
'submit',
869 'hidden' =>
'hidden',
872 if ( !array_key_exists( $type, $map ) ) {
873 throw new InvalidArgumentException(
'invalid field type: ' . $type );
891 array $fieldInfo, array $formDescriptor, array $defaultFormDescriptor
894 foreach ( $defaultFormDescriptor as $fieldName => $defaultField ) {
898 !isset( $fieldInfo[$fieldName] )
900 !isset( $defaultField[
'baseField'] )
901 || !isset( $fieldInfo[$defaultField[
'baseField']] )
904 !isset( $defaultField[
'type'] )
905 || !in_array( $defaultField[
'type'], [
'submit',
'info' ],
true )
908 $defaultFormDescriptor[$fieldName] =
null;
913 $requestField = $formDescriptor[$fieldName] ?? [];
915 isset( $defaultField[
'label'] )
916 || isset( $defaultField[
'label-message'] )
917 || isset( $defaultField[
'label-raw'] )
919 unset( $requestField[
'label'], $requestField[
'label-message'], $defaultField[
'label-raw'] );
922 $defaultFormDescriptor[$fieldName] += $requestField;
925 return array_filter( $defaultFormDescriptor + $formDescriptor );
930class_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.
A cryptographic random generator class used for generating secret keys.
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 execute.Return false to prevent calling execute() (since 1.27+)....
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.
bool $isFakePostRequest
Set when we're pretending that we got a POST request during redirect flows.
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 1.18
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.