prefix . self::TABLE; } /** * CREATE TABLE statement for dbDelta. * * @return string */ public static function schema(): string { global $wpdb; $table = self::table(); $charset = $wpdb->get_charset_collate(); return "CREATE TABLE {$table} ( phone varchar(16) NOT NULL, created_at datetime NOT NULL, source varchar(16) NOT NULL, PRIMARY KEY (phone) ) {$charset};"; } /** * Whether the number is blocked. * * @param string $phone Number in any form. * @return bool */ public static function is_blocked( string $phone ): bool { global $wpdb; $normalized = Phone::normalize( $phone, Phone::default_country_code() ); if ( null === $normalized ) { return false; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom table, must be fresh before each send. $found = $wpdb->get_var( $wpdb->prepare( "SELECT phone FROM {$wpdb->prefix}smsportal_blocklist WHERE phone = %s", $normalized ) ); return null !== $found; } /** * Block a number. Blocking it again keeps the first entry. * * @param string $phone Number in any form. * @param string $source "stop" (customer replied STOP) or "manual". * @return bool False when the number or source is invalid. */ public static function add( string $phone, string $source ): bool { global $wpdb; $normalized = Phone::normalize( $phone, Phone::default_country_code() ); if ( null === $normalized || ! in_array( $source, self::SOURCES, true ) ) { return false; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom table. $result = $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->prefix}smsportal_blocklist (phone, created_at, source) VALUES (%s, %s, %s)", $normalized, current_time( 'mysql', true ), $source ) ); return false !== $result; } /** * All blocked numbers, newest first. * * @return array */ public static function all(): array { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom table, the list must be current. $rows = $wpdb->get_results( "SELECT phone, created_at, source FROM {$wpdb->prefix}smsportal_blocklist ORDER BY created_at DESC, phone ASC", ARRAY_A ); return is_array( $rows ) ? $rows : array(); } /** * Opaque reference to a blocked number, so forms never carry the number itself. * * @param string $phone Number in E.164. * @return string */ public static function token( string $phone ): string { return wp_hash( 'smsportal-block|' . $phone ); } /** * Unblock the number behind a token(). * * @param string $token Token. * @return bool False when no blocked number has that token. */ public static function remove_by_token( string $token ): bool { foreach ( self::all() as $row ) { if ( hash_equals( self::token( $row['phone'] ), $token ) ) { return self::remove( $row['phone'] ); } } return false; } /** * Unblock a number. * * @param string $phone Number in any form. * @return bool False when the number is invalid or the query failed. */ public static function remove( string $phone ): bool { global $wpdb; $normalized = Phone::normalize( $phone, Phone::default_country_code() ); if ( null === $normalized ) { return false; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom table. return false !== $wpdb->delete( self::table(), array( 'phone' => $normalized ), array( '%s' ) ); } }