$row ) { if ( 'terms' === $row['key'] ) { $rows[ $index ]['show'] = '' !== (string) Settings::get( 'signup_terms_url', '' ); } } return $rows; } /** * Builds one stored row. * * @param string $key Field key. * @param bool $show Whether the field is on the form. * @param bool $required Whether the field must be filled in. * @return array */ private static function row( $key, $show, $required ) { return array( 'key' => $key, 'show' => (bool) $show, 'required' => (bool) $required, ); } /** * Every field that can be on the form, keyed by field key. * * @return array */ public static function catalogue() { $fields = array( 'name' => array( 'label' => __( 'Name', 'spacebay' ), 'type' => 'text', 'maxlength' => 60, 'autocomplete' => 'name', ), 'username' => array( 'label' => __( 'Username', 'spacebay' ), 'type' => 'text', 'maxlength' => 60, 'autocomplete' => 'username', 'help' => __( 'Letters and numbers, no spaces.', 'spacebay' ), ), 'email' => array( 'label' => __( 'Email address', 'spacebay' ), 'type' => 'email', 'maxlength' => 100, 'autocomplete' => 'email', ), 'password' => array( 'label' => __( 'Password', 'spacebay' ), 'type' => 'password', 'maxlength' => 200, 'autocomplete' => 'new-password', 'help' => sprintf( /* translators: %d: Number of characters. */ __( 'At least %d characters.', 'spacebay' ), self::MIN_PASSWORD ), ), 'headline' => array( 'label' => __( 'Headline', 'spacebay' ), 'type' => 'text', 'maxlength' => 120, ), 'location' => array( 'label' => __( 'Location', 'spacebay' ), 'type' => 'text', 'maxlength' => 100, ), 'website' => array( 'label' => __( 'Website', 'spacebay' ), 'type' => 'url', 'maxlength' => 255, 'placeholder' => 'https://', 'autocomplete' => 'url', ), 'bio' => array( 'label' => __( 'About me', 'spacebay' ), 'type' => 'textarea', 'maxlength' => 1000, ), ); /** * Filters the fields that can be on the signup form. Add-ons add fields * with keys that start with "field_" and "custom" => true. * * @since 1.0.0 * * @param array $fields Fields keyed by field key. */ $fields = (array) apply_filters( 'spacebay_signup_catalogue', $fields ); $fields['terms'] = array( 'label' => __( 'Agree to the terms', 'spacebay' ), 'type' => 'checkbox', ); return $fields; } /** * Cleans a list of rows before it is saved. * * Keys are checked when the form is built, because profile fields can be * added and removed after the signup form was saved. * * @param array $input Raw rows. * @return array[] */ public static function sanitize( array $input ) { $rows = array(); $seen = array(); foreach ( array_slice( $input, 0, 60 ) as $raw ) { if ( ! is_array( $raw ) || ! isset( $raw['key'] ) ) { continue; } $key = sanitize_key( (string) $raw['key'] ); if ( '' === $key || isset( $seen[ $key ] ) ) { continue; } $seen[ $key ] = true; $locked = in_array( $key, self::LOCKED, true ); $rows[] = self::row( $key, $locked || rest_sanitize_boolean( $raw['show'] ?? false ), $locked || 'terms' === $key || rest_sanitize_boolean( $raw['required'] ?? false ) ); } return $rows; } /** * The saved rows matched with the catalogue: unknown keys are dropped, * new fields are added as hidden, and each row gets its label and type. * * @return array[] */ public static function resolved() { $catalogue = self::catalogue(); $stored = self::default_rows(); /** * Filters which signup fields are shown, which are required and their * order (the signup form builder of SpaceBay Pro). * * @since 1.0.0 * * @param array[] $rows Rows with key, show and required. */ $stored = apply_filters( 'spacebay_signup_rows', $stored ); $stored = is_array( $stored ) ? self::sanitize( $stored ) : self::default_rows(); $rows = array(); foreach ( $stored as $row ) { if ( isset( $catalogue[ $row['key'] ] ) ) { $rows[ $row['key'] ] = $row; } } foreach ( array_keys( $catalogue ) as $key ) { if ( ! isset( $rows[ $key ] ) ) { $locked = in_array( $key, self::LOCKED, true ); $rows[ $key ] = self::row( $key, $locked, $locked || 'terms' === $key ); } } $out = array(); foreach ( $rows as $key => $row ) { $out[] = array_merge( $row, array( 'label' => $catalogue[ $key ]['label'], 'type' => $catalogue[ $key ]['type'], 'locked' => in_array( $key, self::LOCKED, true ), ) ); } return $out; } /** * The fields a visitor sees on the signup form, in order. * * @return array[] */ public static function form() { $catalogue = self::catalogue(); $form = array(); foreach ( self::resolved() as $row ) { if ( ! $row['show'] ) { continue; } $field = $catalogue[ $row['key'] ]; $form[] = array( 'key' => $row['key'], 'label' => $field['label'], 'type' => $field['type'], 'required' => (bool) $row['required'], 'maxlength' => (int) ( $field['maxlength'] ?? 0 ), 'placeholder' => (string) ( $field['placeholder'] ?? '' ), 'autocomplete' => (string) ( $field['autocomplete'] ?? '' ), 'help' => (string) ( $field['help'] ?? '' ), 'options' => array_values( (array) ( $field['options'] ?? array() ) ), ); } return $form; } /** * Checks a submitted signup form. * * @param array $input Raw values keyed by field key. * @return array|WP_Error { * Clean values. * * @type string $email Email address. * @type string $password Password, unchanged. * @type string $name Display name, may be empty. * @type string $username Username, may be empty. * @type array $profile Profile values for Profiles::update(). * } */ public static function validate( array $input ) { $clean = array( 'email' => '', 'password' => '', 'name' => '', 'username' => '', 'profile' => array(), ); $custom = array(); foreach ( self::form() as $field ) { $key = $field['key']; $value = $input[ $key ] ?? ''; if ( 'terms' === $key ) { if ( ! rest_sanitize_boolean( $value ) ) { return self::error( $key, __( 'Please agree to the terms to create an account.', 'spacebay' ) ); } continue; } $value = is_scalar( $value ) ? (string) $value : ''; $value = 'password' === $key ? $value : trim( $value ); if ( '' === $value ) { if ( $field['required'] ) { return self::error( $key, sprintf( /* translators: %s: Field name, for example "Name". */ __( 'Please fill in "%s".', 'spacebay' ), $field['label'] ) ); } continue; } switch ( $key ) { case 'email': $value = sanitize_email( $value ); if ( ! is_email( $value ) ) { return self::error( $key, __( 'Please enter a valid email address.', 'spacebay' ) ); } if ( email_exists( $value ) ) { return self::error( $key, __( 'An account with this email address already exists. Log in instead.', 'spacebay' ), 'spacebay_email_exists' ); } $clean['email'] = $value; break; case 'password': $check = self::check_password( $value, (string) ( $input['email'] ?? '' ) ); if ( is_wp_error( $check ) ) { return $check; } $clean['password'] = $value; break; case 'name': $clean['name'] = mb_substr( sanitize_text_field( $value ), 0, 60 ); break; case 'username': $username = sanitize_user( $value, true ); if ( '' === $username || $username !== $value || ! validate_username( $username ) || mb_strlen( $username ) > 60 ) { return self::error( $key, __( 'Usernames can only contain letters, numbers and these characters: . - _ @', 'spacebay' ) ); } if ( username_exists( $username ) ) { return self::error( $key, __( 'That username is taken. Please choose another one.', 'spacebay' ) ); } $clean['username'] = $username; break; case 'headline': case 'location': case 'bio': $clean['profile'][ $key ] = $value; break; case 'website': $url = Url::clean( $value ); if ( '' === $url ) { return self::error( $key, __( 'Please enter a full web address that starts with https://', 'spacebay' ) ); } $clean['profile']['website'] = $url; break; default: if ( 0 === strpos( $key, self::CUSTOM_PREFIX ) ) { $custom_key = substr( $key, strlen( self::CUSTOM_PREFIX ) ); /** This filter is documented in includes/Members/Profiles.php */ $values = (array) apply_filters( 'spacebay_profile_field_values', array(), array( $custom_key => $value ), 0 ); if ( $field['required'] && '' === (string) ( $values[ $custom_key ] ?? '' ) ) { return self::error( $key, sprintf( /* translators: %s: Field name, for example "Name". */ __( 'Please fill in "%s".', 'spacebay' ), $field['label'] ) ); } $custom = array_merge( $custom, $values ); } } } if ( $custom ) { $clean['profile']['fields'] = $custom; } return $clean; } /** * Checks that a password is long enough and is not the email address. * * @param string $password Password. * @param string $email Email address of the account. * @return true|WP_Error */ public static function check_password( $password, $email = '' ) { if ( mb_strlen( $password ) < self::MIN_PASSWORD ) { return self::error( 'password', sprintf( /* translators: %d: Number of characters. */ __( 'Please choose a password with at least %d characters.', 'spacebay' ), self::MIN_PASSWORD ) ); } if ( mb_strlen( $password ) > 200 ) { return self::error( 'password', __( 'That password is too long. Please use 200 characters or fewer.', 'spacebay' ) ); } if ( '' !== $email && 0 === strcasecmp( $password, trim( $email ) ) ) { return self::error( 'password', __( 'Your password cannot be your email address.', 'spacebay' ) ); } return true; } /** * Builds a validation error that names the field it belongs to. * * @param string $field Field key. * @param string $message Message for the visitor. * @param string $code Error code. * @return WP_Error */ private static function error( $field, $message, $code = 'spacebay_signup_invalid' ) { return new WP_Error( $code, $message, array( 'status' => 400, 'field' => $field, ) ); } }