* - password: * - select: * - multiselect: More a grid of checkboxes than (uses 'label' as button text) * - hidden: Not visible to the user, but needs to be preserved for the next request * - null: No widget, just display the 'label' message. * - options: (array) Maps option values to Messages for the * 'select' and 'multiselect' types. * - value: (string) Value (for 'null' and 'hidden') or default value (for other types). * - label: (Message) Text suitable for a label in an HTML form * - help: (Message) Text suitable as a description of what the field is * - optional: (bool) If set and truthy, the field may be left empty * - sensitive: (bool) If set and truthy, the field is considered sensitive. Code using the * request should avoid exposing the value of the field. * - skippable: (bool) If set and truthy, the client is free to hide this * field from the user to streamline the workflow. If all fields are * skippable (except possibly a single button), no user interaction is * required at all. * * All AuthenticationRequests are populated from the same data, so most of the time you'll * want to prefix fields names with something unique to the extension/provider (although * in some cases sharing the field with other requests is the right thing to do, e.g. for * a 'password' field). * * @return array As above */ abstract public function getFieldInfo(); /** * Returns metadata about this request. * * This is mainly for the benefit of API clients which need more detailed render hints * than what's available through getFieldInfo(). Semantics are unspecified and left to the * individual subclasses, but the contents of the array should be primitive types so that they * can be transformed into JSON or similar formats. * * @return array A (possibly nested) array with primitive types */ public function getMetadata() { return []; } /** * Initialize form submitted form data. * * The default behavior is to to check for each key of self::getFieldInfo() * in the submitted data, and copy the value - after type-appropriate transformations - * to $this->$key. Most subclasses won't need to override this; if you do override it, * make sure to always return false if self::getFieldInfo() returns an empty array. * * @param array $data Submitted data as an associative array (keys will correspond * to getFieldInfo()) * @return bool Whether the request data was successfully loaded */ public function loadFromSubmission( array $data ) { $fields = array_filter( $this->getFieldInfo(), function ( $info ) { return $info['type'] !== 'null'; } ); if ( !$fields ) { return false; } foreach ( $fields as $field => $info ) { // Checkboxes and buttons are special. Depending on the method used // to populate $data, they might be unset meaning false or they // might be boolean. Further, image buttons might submit the // coordinates of the click rather than the expected value. if ( $info['type'] === 'checkbox' || $info['type'] === 'button' ) { $this->$field = isset( $data[$field] ) && $data[$field] !== false || isset( $data["{$field}_x"] ) && $data["{$field}_x"] !== false; if ( !$this->$field && empty( $info['optional'] ) ) { return false; } continue; } // Multiselect are too, slightly if ( !isset( $data[$field] ) && $info['type'] === 'multiselect' ) { $data[$field] = []; } if ( !isset( $data[$field] ) ) { return false; } if ( $data[$field] === '' || $data[$field] === [] ) { if ( empty( $info['optional'] ) ) { return false; } } else { switch ( $info['type'] ) { case 'select': if ( !isset( $info['options'][$data[$field]] ) ) { return false; } break; case 'multiselect': $data[$field] = (array)$data[$field]; $allowed = array_keys( $info['options'] ); if ( array_diff( $data[$field], $allowed ) !== [] ) { return false; } break; } } $this->$field = $data[$field]; } return true; } /** * Describe the credentials represented by this request * * This is used on requests returned by * AuthenticationProvider::getAuthenticationRequests() for ACTION_LINK * and ACTION_REMOVE and for requests returned in * AuthenticationResponse::$linkRequest to create useful user interfaces. * * @return Message[] with the following keys: * - provider: A Message identifying the service that provides * the credentials, e.g. the name of the third party authentication * service. * - account: A Message identifying the credentials themselves, * e.g. the email address used with the third party authentication * service. */ public function describeCredentials() { return [ 'provider' => new \RawMessage( '$1', [ get_called_class() ] ), 'account' => new \RawMessage( '$1', [ $this->getUniqueId() ] ), ]; } /** * Update a set of requests with form submit data, discarding ones that fail * @param AuthenticationRequest[] $reqs * @param array $data * @return AuthenticationRequest[] */ public static function loadRequestsFromSubmission( array $reqs, array $data ) { return array_values( array_filter( $reqs, function ( $req ) use ( $data ) { return $req->loadFromSubmission( $data ); } ) ); } /** * Select a request by class name. * @param AuthenticationRequest[] $reqs * @param string $class Class name * @param bool $allowSubclasses If true, also returns any request that's a subclass of the given * class. * @return AuthenticationRequest|null Returns null if there is not exactly * one matching request. */ public static function getRequestByClass( array $reqs, $class, $allowSubclasses = false ) { $requests = array_filter( $reqs, function ( $req ) use ( $class, $allowSubclasses ) { if ( $allowSubclasses ) { return is_a( $req, $class, false ); } else { return get_class( $req ) === $class; } } ); return count( $requests ) === 1 ? reset( $requests ) : null; } /** * Get the username from the set of requests * * Only considers requests that have a "username" field. * * @param AuthenticationRequest[] $reqs * @return string|null * @throws \UnexpectedValueException If multiple different usernames are present. */ public static function getUsernameFromRequests( array $reqs ) { $username = null; $otherClass = null; foreach ( $reqs as $req ) { $info = $req->getFieldInfo(); if ( $info && array_key_exists( 'username', $info ) && $req->username !== null ) { if ( $username === null ) { $username = $req->username; $otherClass = get_class( $req ); } elseif ( $username !== $req->username ) { $requestClass = get_class( $req ); throw new \UnexpectedValueException( "Conflicting username fields: \"{$req->username}\" from " . "$requestClass::\$username vs. \"$username\" from $otherClass::\$username" ); } } } return $username; } /** * Merge the output of multiple AuthenticationRequest::getFieldInfo() calls. * @param AuthenticationRequest[] $reqs * @return array * @throws \UnexpectedValueException If fields cannot be merged */ public static function mergeFieldInfo( array $reqs ) { $merged = []; // fields that are required by some primary providers but not others are not actually required $primaryRequests = array_filter( $reqs, function ( $req ) { return $req->required === AuthenticationRequest::PRIMARY_REQUIRED; } ); $sharedRequiredPrimaryFields = array_reduce( $primaryRequests, function ( $shared, $req ) { $required = array_keys( array_filter( $req->getFieldInfo(), function ( $options ) { return empty( $options['optional'] ); } ) ); if ( $shared === null ) { return $required; } else { return array_intersect( $shared, $required ); } }, null ); foreach ( $reqs as $req ) { $info = $req->getFieldInfo(); if ( !$info ) { continue; } foreach ( $info as $name => $options ) { if ( // If the request isn't required, its fields aren't required either. $req->required === self::OPTIONAL // If there is a primary not requiring this field, no matter how many others do, // authentication can proceed without it. || $req->required === self::PRIMARY_REQUIRED && !in_array( $name, $sharedRequiredPrimaryFields, true ) ) { $options['optional'] = true; } else { $options['optional'] = !empty( $options['optional'] ); } $options['sensitive'] = !empty( $options['sensitive'] ); if ( !array_key_exists( $name, $merged ) ) { $merged[$name] = $options; } elseif ( $merged[$name]['type'] !== $options['type'] ) { throw new \UnexpectedValueException( "Field type conflict for \"$name\", " . "\"{$merged[$name]['type']}\" vs \"{$options['type']}\"" ); } else { if ( isset( $options['options'] ) ) { if ( isset( $merged[$name]['options'] ) ) { $merged[$name]['options'] += $options['options']; } else { // @codeCoverageIgnoreStart $merged[$name]['options'] = $options['options']; // @codeCoverageIgnoreEnd } } $merged[$name]['optional'] = $merged[$name]['optional'] && $options['optional']; $merged[$name]['sensitive'] = $merged[$name]['sensitive'] || $options['sensitive']; // No way to merge 'value', 'image', 'help', or 'label', so just use // the value from the first request. } } } return $merged; } /** * Implementing this mainly for use from the unit tests. * @param array $data * @return AuthenticationRequest */ public static function __set_state( $data ) { $ret = new static(); foreach ( $data as $k => $v ) { $ret->$k = $v; } return $ret; } }