definition with type, label, title). UM's registration handling * (um_submit_form_register) creates the user and fires um_registration_complete ( $user_id, $args, $form_data ) * with the posted values in $args (by meta key) and the form's post data in $form_data (form_id, ...). The * status is set afterwards, by a callback of the same hook at priority 100 that may redirect and exit: ours * runs just before it. $form_data is null for a user added in wp-admin: that is not a form registration. * - The account status is the user meta account_status: approved, awaiting_admin_review, * awaiting_email_confirmation, inactive, rejected. Every change goes through * UM()->common()->users()->set_status(), which fires um_after_user_status_is_changed ( $status, $user_id, * $old_status ) once, and only when the value really changed. The per-status hooks (um_after_user_is_approved, * um_after_user_is_inactive, ...) fire in the same call, so listening to both would send twice: only the * status hook is used. A registration sets the first status from an empty old status: an auto-approved account * is the registration itself (the registration event covers it, no "approved" SMS), an account that awaits * review is reported (an administrator's rule with a fixed number is the "notify admin" case). Awaiting e-mail * confirmation has no event (the person has just registered and received the e-mail). Reactivating an inactive * account is "approved" again. * - The profile fields phone_number and mobile_number are plain user meta (the Telephone fields of the profile * and registration forms). * * @package Smsportal */ namespace Smsportal\Integrations; use Smsportal\Events; use Smsportal\Phone; use Smsportal\Rules; use Smsportal\Variables; defined( 'ABSPATH' ) || exit; /** * Ultimate Member users as SMS events. The customer's number is the user's profile field (mobile_number, then * phone_number); for a registration the rule may choose the form field instead. */ class UltimateMember { const GROUP = 'Ultimate Member'; const EVENT_REGISTERED_PREFIX = 'um.registered.'; const EVENT_APPROVED = 'um.approved'; const EVENT_AWAITING_REVIEW = 'um.awaiting_review'; const EVENT_DEACTIVATED = 'um.deactivated'; const EVENT_REJECTED = 'um.rejected'; const OBJECT_TYPE = 'um_user'; /** * Ultimate Member account status => event. */ const STATUS_EVENTS = array( 'approved' => self::EVENT_APPROVED, 'awaiting_admin_review' => self::EVENT_AWAITING_REVIEW, 'inactive' => self::EVENT_DEACTIVATED, 'rejected' => self::EVENT_REJECTED, ); /** * Profile fields (user meta) that may hold the number, in the order they are tried. */ const PHONE_FIELDS = array( 'mobile_number', 'phone_number' ); /** * Registration form field types that carry text worth putting into an SMS (no passwords, files, images, * maps, videos, content blocks or dividers). */ const FIELD_TYPES = array( 'text', 'tel', 'number', 'textarea', 'select', 'multiselect', 'radio', 'checkbox', 'url', 'date', 'time', 'rating' ); /** * Fields the built-in variables already cover, and consent fields (not for a message). */ const SKIPPED_FIELDS = '/^(first_name|last_name|user_login|username|user_email|role)$|consent|terms|gdpr|agree|privacy/i'; /** * Context keys of our own (prefixed so they cannot clash with form field names). */ const KEY_USER = '_smsportal_user_id'; const KEY_STATUS = '_smsportal_status'; const KEY_FORM = '_smsportal_form_id'; /** * Whether Ultimate Member is active. * * @return bool */ public static function available(): bool { return function_exists( 'UM' ) && function_exists( 'um_user_profile_url' ) && function_exists( 'um_get_core_page' ); } /** * Register hooks. * * @return void */ public static function register(): void { add_filter( 'smsportal_register_events', array( self::class, 'events' ) ); add_filter( 'smsportal_privacy_phones', array( self::class, 'privacy_phones' ), 10, 2 ); // Before Ultimate Member sets the status and redirects (priority 100). add_action( 'um_registration_complete', array( self::class, 'on_registered' ), 99, 3 ); add_action( 'um_after_user_status_is_changed', array( self::class, 'on_status_changed' ), 20, 3 ); } /** * Add the events: one per registration form, then one per status. * * @param mixed $events Events so far. * @return array> */ public static function events( $events ): array { $events = is_array( $events ) ? $events : array(); foreach ( self::forms() as $form ) { $events[] = self::registration_event( $form ); } $variables = self::status_variables(); $events[] = self::status_event( self::EVENT_APPROVED, __( 'Ultimate Member: account approved', '85pl-android-sms-api' ), 'approved', $variables ); $events[] = self::status_event( self::EVENT_AWAITING_REVIEW, __( 'Ultimate Member: account awaits review (notify the administrator)', '85pl-android-sms-api' ), 'awaiting_admin_review', $variables ); $events[] = self::status_event( self::EVENT_DEACTIVATED, __( 'Ultimate Member: account deactivated', '85pl-android-sms-api' ), 'inactive', $variables ); $events[] = self::status_event( self::EVENT_REJECTED, __( 'Ultimate Member: account rejected', '85pl-android-sms-api' ), 'rejected', $variables ); return $events; } /** * Hook um_registration_complete: a person registered with a form. * * @param mixed $user_id User ID. * @param mixed $args Posted values (by meta key). * @param mixed $form_data Form's data; null when the user was added in wp-admin. * @return void */ public static function on_registered( $user_id, $args = array(), $form_data = null ): void { $user_id = is_scalar( $user_id ) ? absint( $user_id ) : 0; $form_id = is_array( $form_data ) && isset( $form_data['form_id'] ) && is_scalar( $form_data['form_id'] ) ? absint( $form_data['form_id'] ) : 0; if ( $user_id < 1 || $form_id < 1 || ! is_array( $args ) ) { return; } $event_id = self::EVENT_REGISTERED_PREFIX . $form_id; try { $user = get_userdata( $user_id ); if ( $user instanceof \WP_User ) { Events::fire( $event_id, self::registration_context( $user, $form_id, $args ) ); } } catch ( \Throwable $e ) { Events::report( $e, $event_id ); } } /** * Hook um_after_user_status_is_changed: the account has a new status. * * @param mixed $status New status. * @param mixed $user_id User ID. * @param mixed $old_status Previous status ('' = the registration sets the first one). * @return void */ public static function on_status_changed( $status, $user_id, $old_status = '' ): void { $status = is_string( $status ) ? $status : ''; $user_id = is_scalar( $user_id ) ? absint( $user_id ) : 0; $event_id = self::STATUS_EVENTS[ $status ] ?? ''; if ( '' === $event_id || $user_id < 1 ) { return; } // An account approved on registration is the registration (its own event), not an approval. if ( 'approved' === $status && ( ! is_string( $old_status ) || '' === $old_status ) ) { return; } try { $user = get_userdata( $user_id ); if ( $user instanceof \WP_User ) { Events::fire( $event_id, self::status_context( $user, $status ) ); } } catch ( \Throwable $e ) { Events::report( $e, $event_id ); } } /** * Numbers of a person for the privacy tools: the profile fields and the registration form fields chosen in * the rules, as stored in the user meta. * * @param mixed $phones Numbers so far. * @param string $email E-mail address. * @return array */ public static function privacy_phones( $phones, $email ): array { $phones = is_array( $phones ) ? $phones : array(); $user = '' === trim( (string) $email ) ? false : get_user_by( 'email', trim( (string) $email ) ); if ( ! $user instanceof \WP_User ) { return $phones; } $fields = self::PHONE_FIELDS; foreach ( Rules::all() as $rule ) { if ( 0 === strpos( (string) $rule['event_id'], self::EVENT_REGISTERED_PREFIX ) && '' !== $rule['phone_field'] ) { $fields[] = (string) $rule['phone_field']; } } foreach ( array_unique( $fields ) as $field ) { $value = get_user_meta( $user->ID, $field, true ); $phone = is_string( $value ) && '' !== trim( $value ) ? Phone::normalize( $value, Phone::default_country_code() ) : null; if ( null !== $phone ) { $phones[] = $phone; } } return array_values( array_unique( array_filter( $phones, 'is_string' ) ) ); } /** * Context of a registration: the form's posted values and the person's data. * * @param \WP_User $user New user. * @param int $form_id Registration form (post ID). * @param array $values Posted values by meta key. * @return array */ public static function registration_context( \WP_User $user, int $form_id, array $values ): array { $context = array(); foreach ( array_keys( self::form_fields( $form_id ) ) as $key ) { if ( isset( $values[ $key ] ) ) { $text = self::text( $values[ $key ] ); if ( null !== $text ) { $context[ $key ] = $text; } } } return array_merge( $context, self::person( $user ), array( self::KEY_USER => $user->ID, self::KEY_FORM => $form_id, ) ); } /** * Context of a status change. * * @param \WP_User $user User. * @param string $status Ultimate Member's status key. * @return array */ public static function status_context( \WP_User $user, string $status ): array { $statuses = UM()->common()->users()->statuses_list(); $label = $statuses[ $status ] ?? $status; return array_merge( self::person( $user ), array( 'status' => wp_specialchars_decode( (string) $label, ENT_QUOTES ), 'link_logowania' => (string) um_get_core_page( 'login' ), 'nazwa_firmy' => wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ), self::KEY_USER => $user->ID, self::KEY_STATUS => $status, ) ); } /** * The person's own variables. * * @param \WP_User $user User. * @return array */ private static function person( \WP_User $user ): array { return array( 'imie' => (string) get_user_meta( $user->ID, 'first_name', true ), 'nazwisko' => (string) get_user_meta( $user->ID, 'last_name', true ), 'login' => $user->user_login, 'email' => $user->user_email, 'link_profilu' => (string) um_user_profile_url( $user->ID ), ); } /** * A posted value as text (lists joined with commas, HTML entities decoded). * * @param mixed $value Value. * @return string|null Null for what is not text. */ private static function text( $value ): ?string { if ( is_array( $value ) ) { $value = implode( ', ', array_filter( array_map( 'strval', array_filter( $value, 'is_scalar' ) ), 'strlen' ) ); } if ( ! is_scalar( $value ) ) { return null; } return trim( html_entity_decode( (string) $value, ENT_QUOTES | ENT_HTML5, 'UTF-8' ) ); } /** * Published registration forms. * * @return array */ private static function forms(): array { return get_posts( array( 'post_type' => 'um_form', 'post_status' => 'publish', 'posts_per_page' => 200, 'meta_key' => '_um_mode', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- a handful of forms. 'meta_value' => 'register', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value 'orderby' => 'ID', 'order' => 'ASC', ) ); } /** * Fields of a registration form that can be message variables. * * @param int $form_id Form (post ID). * @return array Meta key => label. */ private static function form_fields( int $form_id ): array { $definitions = maybe_unserialize( get_post_meta( $form_id, '_um_custom_fields', true ) ); $fields = array(); foreach ( is_array( $definitions ) ? $definitions : array() as $key => $definition ) { $key = is_string( $key ) ? $key : ''; if ( 1 !== preg_match( '/^[A-Za-z0-9_\-]+$/', $key ) || 1 === preg_match( self::SKIPPED_FIELDS, $key ) || ! is_array( $definition ) || ! in_array( $definition['type'] ?? '', self::FIELD_TYPES, true ) ) { continue; } $label = (string) ( $definition['label'] ?? '' ); $fields[ $key ] = '' === $label ? (string) ( $definition['title'] ?? $key ) : $label; } return $fields; } /** * Event of one registration form. * * @param \WP_Post $form Form. * @return array */ private static function registration_event( \WP_Post $form ): array { $form_id = (int) $form->ID; $variables = array_merge( self::form_fields( $form_id ), Variables::for_editor( array( 'imie' => __( 'First name', '85pl-android-sms-api' ), 'nazwisko' => __( 'Last name', '85pl-android-sms-api' ), 'login' => __( 'Username', '85pl-android-sms-api' ), 'email' => __( 'E-mail address', '85pl-android-sms-api' ), 'link_profilu' => __( 'Profile link', '85pl-android-sms-api' ), ) ) ); return array( 'id' => self::EVENT_REGISTERED_PREFIX . $form_id, /* translators: %s: form title. */ 'label' => sprintf( __( 'Ultimate Member: registration with the form "%s"', '85pl-android-sms-api' ), $form->post_title ), 'group' => self::GROUP, 'icon' => SMSPORTAL_URL_BASE . 'assets/integrations/ultimate-member.png', 'variables' => $variables, 'customer_phone' => static function ( array $context, array $rule = array() ) { $field = trim( (string) ( $rule['phone_field'] ?? '' ) ); $fields = '' === $field ? self::PHONE_FIELDS : array( $field ); foreach ( $fields as $name ) { $phone = $context[ $name ] ?? null; if ( is_string( $phone ) && '' !== trim( $phone ) ) { return $phone; } } return null; }, 'phone_field_default' => self::PHONE_FIELDS[0], 'object' => static function ( array $context ): array { return self::object_of( $context, (string) ( $context[ self::KEY_USER ] ?? '' ) ); }, 'sample' => static function () use ( $form_id ) { $user = self::latest_user( '' ); return null === $user ? null : self::sample_registration( $user, $form_id ); }, 'rule_fields' => array( 'phone_field' ), ); } /** * Event of one status. * * @param string $id Event ID. * @param string $label Label. * @param string $status Ultimate Member's status. * @param array $variables Variables. * @return array */ private static function status_event( string $id, string $label, string $status, array $variables ): array { return array( 'id' => $id, 'label' => $label, 'group' => self::GROUP, 'icon' => SMSPORTAL_URL_BASE . 'assets/integrations/ultimate-member.png', 'variables' => $variables, 'customer_phone' => static function ( array $context ) { $user_id = absint( $context[ self::KEY_USER ] ?? 0 ); foreach ( self::PHONE_FIELDS as $name ) { $phone = $user_id > 0 ? get_user_meta( $user_id, $name, true ) : ''; if ( is_string( $phone ) && '' !== trim( $phone ) ) { return $phone; } } return null; }, 'object' => static function ( array $context ): array { // One SMS per user and status, however often the account returns to it. return self::object_of( $context, ( $context[ self::KEY_USER ] ?? '' ) . ':' . ( $context[ self::KEY_STATUS ] ?? '' ) ); }, 'sample' => static function () use ( $status ) { $user = self::latest_user( $status ); return null === $user ? null : self::status_context( $user, $status ); }, 'rule_fields' => array(), ); } /** * Object of an event. * * @param array $context Event data. * @param string $id Object ID. * @return array{type:string,id:string,url:string} */ private static function object_of( array $context, string $id ): array { $user = absint( $context[ self::KEY_USER ] ?? 0 ); return array( 'type' => self::OBJECT_TYPE, 'id' => $user > 0 ? $id : '', 'url' => $user > 0 ? admin_url( 'user-edit.php?user_id=' . $user ) : '', ); } /** * Variables of the status events. * * @return array */ private static function status_variables(): array { return Variables::for_editor( array( 'imie' => __( 'First name', '85pl-android-sms-api' ), 'nazwisko' => __( 'Last name', '85pl-android-sms-api' ), 'login' => __( 'Username', '85pl-android-sms-api' ), 'email' => __( 'E-mail address', '85pl-android-sms-api' ), 'status' => __( 'Account status', '85pl-android-sms-api' ), 'link_profilu' => __( 'Profile link', '85pl-android-sms-api' ), 'link_logowania' => __( 'Login page link', '85pl-android-sms-api' ), 'nazwa_firmy' => __( 'Site name', '85pl-android-sms-api' ), ) ); } /** * Latest user (with the given status; '' = any), for the rule preview. * * @param string $status Ultimate Member's status. * @return \WP_User|null */ private static function latest_user( string $status ): ?\WP_User { $args = array( 'number' => 1, 'orderby' => 'ID', 'order' => 'DESC', ); if ( '' !== $status ) { $args['meta_key'] = 'account_status'; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- one preview query. $args['meta_value'] = $status; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value } $users = get_users( $args ); return $users[0] ?? null; } /** * Preview data of a registration form from an existing user's meta. * * @param \WP_User $user User. * @param int $form_id Form. * @return array */ private static function sample_registration( \WP_User $user, int $form_id ): array { $values = array(); foreach ( array_keys( self::form_fields( $form_id ) ) as $key ) { $values[ $key ] = get_user_meta( $user->ID, $key, true ); } return self::registration_context( $user, $form_id, $values ); } }