array( 'name' => __( 'SMS rules', '85pl-android-sms-api' ), 'singular_name' => __( 'SMS rule', '85pl-android-sms-api' ), ), 'public' => false, 'show_ui' => false, 'show_in_menu' => false, 'show_in_rest' => false, 'exclude_from_search' => true, 'publicly_queryable' => false, 'rewrite' => false, 'query_var' => false, 'supports' => array( 'title' ), 'capability_type' => array( 'smsportal_rule', 'smsportal_rules' ), 'map_meta_cap' => false, 'capabilities' => array( 'edit_post' => 'manage_options', 'read_post' => 'manage_options', 'delete_post' => 'manage_options', 'edit_posts' => 'manage_options', 'edit_others_posts' => 'manage_options', 'delete_posts' => 'manage_options', 'publish_posts' => 'manage_options', 'read_private_posts' => 'manage_options', 'delete_private_posts' => 'manage_options', 'delete_published_posts' => 'manage_options', 'delete_others_posts' => 'manage_options', 'edit_private_posts' => 'manage_options', 'edit_published_posts' => 'manage_options', 'create_posts' => 'manage_options', ), ) ); } /** * Create or update a rule. * * @param array $data Rule fields (event_id, recipient_type, fixed_phone, phone_field, template, * expires_hours, hours_before, enabled). * @param int $id Existing rule ID, or 0 to create. * @return int|\WP_Error Rule ID, or an error (also when $id is not an existing rule). */ public static function save( array $data, int $id = 0 ) { if ( $id > 0 && ( ! get_post( $id ) || self::POST_TYPE !== get_post_type( $id ) ) ) { return new \WP_Error( 'smsportal_no_rule', __( 'This rule does not exist.', '85pl-android-sms-api' ) ); } $rule = self::sanitize( $data ); $post_id = wp_insert_post( array( 'ID' => $id, 'post_type' => self::POST_TYPE, 'post_status' => 'publish', 'post_title' => $rule['event_id'], ) ); if ( ! is_int( $post_id ) || $post_id <= 0 ) { return new \WP_Error( 'smsportal_rule_not_saved', __( 'The rule could not be saved.', '85pl-android-sms-api' ) ); } foreach ( $rule as $key => $value ) { update_post_meta( $post_id, self::META_PREFIX . $key, wp_slash( is_bool( $value ) ? (int) $value : $value ) ); } return $post_id; } /** * One rule. * * @param int $id Rule ID. * @return array|null */ public static function get( int $id ): ?array { $post = get_post( $id ); if ( ! $post || self::POST_TYPE !== $post->post_type ) { return null; } $meta = array(); foreach ( array( 'event_id', 'recipient_type', 'fixed_phone', 'phone_field', 'template', 'expires_hours', 'hours_before', 'enabled' ) as $key ) { $meta[ $key ] = get_post_meta( $id, self::META_PREFIX . $key, true ); } return array_merge( array( 'id' => $id ), self::sanitize( $meta ) ); } /** * All rules, oldest first. * * @return array> */ public static function all(): array { $ids = get_posts( array( 'post_type' => self::POST_TYPE, 'post_status' => 'publish', 'numberposts' => -1, 'fields' => 'ids', 'orderby' => 'ID', 'order' => 'ASC', ) ); $rules = array(); foreach ( $ids as $id ) { $rule = self::get( (int) $id ); if ( null !== $rule ) { $rules[] = $rule; } } return $rules; } /** * Delete a rule. * * @param int $id Rule ID. * @return bool False when it is not a rule. */ public static function delete( int $id ): bool { if ( $id <= 0 || self::POST_TYPE !== get_post_type( $id ) ) { return false; } return wp_delete_post( $id, true ) instanceof \WP_Post; } /** * Enabled rules of an event. * * @param string $event_id Event ID. * @return array> */ public static function enabled_for_event( string $event_id ): array { $ids = get_posts( array( 'post_type' => self::POST_TYPE, 'post_status' => 'publish', 'numberposts' => -1, 'fields' => 'ids', 'orderby' => 'ID', 'order' => 'ASC', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- few rules, filtered by event. 'meta_query' => array( array( 'key' => self::META_PREFIX . 'event_id', 'value' => $event_id, ), ), ) ); $rules = array(); foreach ( $ids as $id ) { $rule = self::get( (int) $id ); if ( null !== $rule && $rule['enabled'] ) { $rules[] = $rule; } } return $rules; } /** * Clean rule fields and fill the defaults. * * phone_field: form field holding the customer's number (Contact Form 7 rules with recipient "customer"). * hours_before: how long before the appointment a reminder goes out (reminder events). * * @param array $data Raw fields. * @return array */ private static function sanitize( array $data ): array { $type = (string) ( $data['recipient_type'] ?? 'customer' ); $hours = $data['expires_hours'] ?? self::DEFAULT_EXPIRES_HOURS; $before = $data['hours_before'] ?? ''; $enabled = $data['enabled'] ?? true; return array( 'event_id' => sanitize_text_field( (string) ( $data['event_id'] ?? '' ) ), 'recipient_type' => in_array( $type, self::RECIPIENT_TYPES, true ) ? $type : 'customer', 'fixed_phone' => sanitize_text_field( (string) ( $data['fixed_phone'] ?? '' ) ), 'phone_field' => sanitize_text_field( (string) ( $data['phone_field'] ?? '' ) ), 'template' => self::clean_template( is_scalar( $data['template'] ?? '' ) ? (string) ( $data['template'] ?? '' ) : '' ), 'expires_hours' => '' === $hours ? self::DEFAULT_EXPIRES_HOURS : self::hours( $hours ), 'hours_before' => '' === $before || self::hours( $before ) < 1 ? self::DEFAULT_HOURS_BEFORE : self::hours( $before ), 'enabled' => ! in_array( $enabled, array( false, 0, '0', '' ), true ), ); } /** * SMS text as plain text: valid UTF-8, line breaks as "\n", no other control characters, trimmed. * Markup characters stay as typed (an SMS is not HTML); screens escape it when printing. * * @param string $template Text. * @return string */ public static function clean_template( string $template ): string { $text = str_replace( array( "\r\n", "\r" ), "\n", wp_check_invalid_utf8( $template, true ) ); $text = (string) preg_replace( '/[\x00-\x09\x0B-\x1F\x7F]/u', '', $text ); return trim( $text ); } /** * Whole hours between 0 and MAX_HOURS. * * @param mixed $value Hours. * @return int */ private static function hours( $value ): int { return max( 0, min( self::MAX_HOURS, (int) ( is_scalar( $value ) ? $value : 0 ) ) ); } }