*/ private static function setting_keys() { return array( 'name' => 'forms.register_name', 'first_name' => 'forms.field_first_name', 'last_name' => 'forms.field_last_name', 'email' => 'forms.field_email', 'username' => 'forms.field_username', 'password' => 'forms.register_password', 'password_confirm' => 'forms.field_password_confirm', 'national_id' => 'forms.field_national_id', 'postal_code' => 'forms.field_postal_code', 'birth_date' => 'forms.field_birth_date', ); } /** * Returns the fields to show, with their mode, for a type of identifier. * * The email field is only asked when the visitor registers with a phone * number. The confirmation field only appears together with the password. * * @param string $type Identifier type: "phone" or "email". * @return array Field name to "optional" or "required". */ public static function for_type( $type ) { $fields = array(); foreach ( self::setting_keys() as $name => $key ) { $mode = (string) Options::get( $key ); if ( 'optional' !== $mode && 'required' !== $mode ) { continue; } if ( 'email' === $name && 'phone' !== $type ) { continue; } $fields[ $name ] = $mode; } if ( ! isset( $fields['password'] ) ) { unset( $fields['password_confirm'] ); } if ( Options::get( 'forms.terms_required' ) ) { $fields['terms'] = 'required'; } return $fields; } /** * Validates submitted details and returns the clean values. * * @param string $type Identifier type. * @param array $data Submitted values. * @return array|WP_Error */ public static function validate( $type, array $data ) { $fields = self::for_type( $type ); $clean = array(); foreach ( array( 'name', 'first_name', 'last_name', 'email', 'username', 'national_id', 'postal_code', 'birth_date' ) as $name ) { $value = isset( $fields[ $name ], $data[ $name ] ) ? trim( (string) $data[ $name ] ) : ''; if ( '' === $value ) { if ( isset( $fields[ $name ] ) && 'required' === $fields[ $name ] ) { return self::required_error( $name ); } continue; } $checked = self::check( $name, $value ); if ( is_wp_error( $checked ) ) { return $checked; } $clean[ $name ] = $checked; } $password = isset( $fields['password'], $data['password'] ) ? (string) $data['password'] : ''; if ( '' === $password && isset( $fields['password'] ) && 'required' === $fields['password'] ) { return new WP_Error( 'aslansheed_password_weak', __( 'Please choose a password.', 'aslansheed-login' ) ); } if ( '' !== $password ) { if ( strlen( $password ) < (int) Options::get( 'forms.password_min_length' ) ) { return new WP_Error( 'aslansheed_password_weak', sprintf( /* translators: %d: minimum number of characters. */ __( 'Please choose a password with at least %d characters.', 'aslansheed-login' ), (int) Options::get( 'forms.password_min_length' ) ) ); } if ( isset( $fields['password_confirm'] ) && ( ! isset( $data['password_confirm'] ) || (string) $data['password_confirm'] !== $password ) ) { return new WP_Error( 'aslansheed_password_mismatch', __( 'The two passwords do not match.', 'aslansheed-login' ) ); } $clean['password'] = $password; } $custom = Custom_Fields::validate( $type, isset( $data['custom'] ) ? $data['custom'] : array() ); if ( is_wp_error( $custom ) ) { return $custom; } if ( array() !== $custom ) { $clean['custom'] = $custom; } if ( isset( $fields['terms'] ) && empty( $data['terms'] ) ) { return new WP_Error( 'aslansheed_terms_required', __( 'Please accept the terms to continue.', 'aslansheed-login' ) ); } return $clean; } /** * Stores the details that live in user meta. * * @param int $user_id User id. * @param array $clean Values from validate(). * @return void */ public static function save_meta( $user_id, array $clean ) { if ( isset( $clean['national_id'] ) ) { update_user_meta( $user_id, self::META_NATIONAL_ID, Crypto::encrypt( $clean['national_id'] ) ); update_user_meta( $user_id, self::META_NATIONAL_ID_INDEX, Crypto::hmac( 'national_id|' . $clean['national_id'] ) ); } if ( isset( $clean['postal_code'] ) ) { update_user_meta( $user_id, self::META_POSTAL_CODE, $clean['postal_code'] ); } if ( isset( $clean['birth_date'] ) ) { update_user_meta( $user_id, self::META_BIRTH_DATE, $clean['birth_date'] ); } if ( isset( $clean['custom'] ) && is_array( $clean['custom'] ) ) { Custom_Fields::save( $user_id, $clean['custom'] ); } } /** * Returns the national ID of a user, or an empty string. * * @param int $user_id User id. * @return string */ public static function national_id( $user_id ) { $plain = Crypto::decrypt( (string) get_user_meta( $user_id, self::META_NATIONAL_ID, true ) ); return is_string( $plain ) ? $plain : ''; } /** * Checks the Iranian national code: 10 digits, not one repeated digit, and a valid check digit. * * @param string $code Digits only. * @return bool */ public static function is_valid_national_id( $code ) { if ( ! preg_match( '/^\d{10}$/', $code ) || preg_match( '/^(\d)\1{9}$/', $code ) ) { return false; } $sum = 0; for ( $i = 0; $i < 9; $i++ ) { $sum += (int) $code[ $i ] * ( 10 - $i ); } $remainder = $sum % 11; $check = $remainder < 2 ? $remainder : 11 - $remainder; return (int) $code[9] === $check; } /** * Validates one field and returns its clean value. * * @param string $name Field name. * @param string $value Trimmed raw value. * @return string|WP_Error */ private static function check( $name, $value ) { switch ( $name ) { case 'name': case 'first_name': case 'last_name': return substr( sanitize_text_field( $value ), 0, 100 ); case 'email': $email = strtolower( sanitize_email( $value ) ); if ( '' === $email || ! is_email( $email ) ) { return new WP_Error( 'aslansheed_email_invalid', __( 'Please enter a valid email address.', 'aslansheed-login' ) ); } if ( email_exists( $email ) ) { return new WP_Error( 'aslansheed_email_taken', __( 'This email address is already used by another account.', 'aslansheed-login' ) ); } return $email; case 'username': $login = sanitize_user( $value, true ); if ( '' === $login || strlen( $login ) < 3 || strlen( $login ) > 60 || ! validate_username( $login ) ) { return new WP_Error( 'aslansheed_username_invalid', __( 'Please choose a username of 3 to 60 letters, digits, dots or dashes.', 'aslansheed-login' ) ); } if ( username_exists( $login ) ) { return new WP_Error( 'aslansheed_username_taken', __( 'This username is already taken.', 'aslansheed-login' ) ); } return $login; case 'national_id': $code = Phone_Normalizer::ascii_digits( $value ); if ( ! self::is_valid_national_id( $code ) ) { return new WP_Error( 'aslansheed_national_invalid', __( 'The national ID is not valid.', 'aslansheed-login' ) ); } if ( Options::get( 'forms.national_id_unique' ) && self::national_id_taken( $code ) ) { return new WP_Error( 'aslansheed_national_taken', __( 'This national ID is already registered.', 'aslansheed-login' ) ); } return $code; case 'postal_code': $code = Phone_Normalizer::ascii_digits( $value ); if ( ! preg_match( '/^\d{10}$/', $code ) || preg_match( '/^(\d)\1{9}$/', $code ) ) { return new WP_Error( 'aslansheed_postal_invalid', __( 'The postal code must be 10 digits.', 'aslansheed-login' ) ); } return $code; default: return self::check_date( Phone_Normalizer::ascii_digits( $value ) ); } } /** * Accepts a Gregorian date written as YYYY-MM-DD that exists and is not in the future. * * The form converts Jalali dates to Gregorian before it submits them. * * @param string $value Date string. * @return string|WP_Error */ private static function check_date( $value ) { $invalid = new WP_Error( 'aslansheed_date_invalid', __( 'Please enter a valid date.', 'aslansheed-login' ) ); if ( ! preg_match( '/^(\d{4})-(\d{2})-(\d{2})$/', $value, $m ) || ! checkdate( (int) $m[2], (int) $m[3], (int) $m[1] ) || (int) $m[1] < 1900 ) { return $invalid; } if ( $value > gmdate( 'Y-m-d' ) ) { return $invalid; } return $value; } /** * Tells whether a national ID is already used by a user. * * @param string $code Digits. * @return bool */ private static function national_id_taken( $code ) { $users = get_users( array( 'meta_key' => self::META_NATIONAL_ID_INDEX, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Indexed lookup of one hash. 'meta_value' => Crypto::hmac( 'national_id|' . $code ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value -- Indexed lookup of one hash. 'number' => 1, 'fields' => 'ID', ) ); return array() !== $users; } /** * Builds the error for an empty required field. * * @param string $name Field name. * @return WP_Error */ private static function required_error( $name ) { $labels = array( 'name' => __( 'a display name', 'aslansheed-login' ), 'first_name' => __( 'your first name', 'aslansheed-login' ), 'last_name' => __( 'your last name', 'aslansheed-login' ), 'email' => __( 'your email address', 'aslansheed-login' ), 'username' => __( 'a username', 'aslansheed-login' ), 'national_id' => __( 'your national ID', 'aslansheed-login' ), 'postal_code' => __( 'your postal code', 'aslansheed-login' ), 'birth_date' => __( 'your birth date', 'aslansheed-login' ), ); return new WP_Error( 'name' === $name ? 'aslansheed_name_required' : 'aslansheed_field_required', sprintf( /* translators: %s: what the visitor must enter, for example "your first name". */ __( 'Please enter %s.', 'aslansheed-login' ), isset( $labels[ $name ] ) ? $labels[ $name ] : $name ) ); } }