%s

', esc_html__( 'Accessibility Audio TTS: WordPress version is outdated. Please update for security reasons.', 'accessibility-audio-tts-text-to-speech-for-articles' ) ); } ); } // Check if PHP is up to date. if ( version_compare( PHP_VERSION, '7.4', '<' ) ) { add_action( 'admin_notices', function () { printf( '

%s

', esc_html__( 'Accessibility Audio TTS: PHP version is outdated and unsupported. Please update immediately.', 'accessibility-audio-tts-text-to-speech-for-articles' ) ); } ); } } /** * Sanitize input data * * @param mixed $data Data to sanitize. * @param string $type Type of sanitization. * @return mixed Sanitized data. * @since 1.0.6 */ public static function sanitize_input( $data, $type = 'text' ) { if ( is_array( $data ) ) { return array_map( function ( $item ) use ( $type ) { return self::sanitize_input( $item, $type ); }, $data ); } switch ( $type ) { case 'email': return sanitize_email( $data ); case 'url': return esc_url_raw( $data ); case 'int': return intval( $data ); case 'float': return floatval( $data ); case 'bool': return (bool) $data; case 'key': return sanitize_key( $data ); case 'slug': return sanitize_title( $data ); case 'html': return wp_kses_post( $data ); case 'textarea': return sanitize_textarea_field( $data ); case 'text': default: return sanitize_text_field( $data ); } } /** * Validate input data * * @param mixed $data Data to validate. * @param string $type Type of validation. * @param array $options Validation options. * @return bool True if valid, false otherwise. * @since 1.0.6 */ public static function validate_input( $data, $type = 'text', $options = array() ) { switch ( $type ) { case 'email': return is_email( $data ); case 'url': return filter_var( $data, FILTER_VALIDATE_URL ) !== false; case 'int': $min = isset( $options['min'] ) ? $options['min'] : null; $max = isset( $options['max'] ) ? $options['max'] : null; $int = filter_var( $data, FILTER_VALIDATE_INT ); if ( false === $int ) { return false; } if ( null !== $min && $int < $min ) { return false; } if ( null !== $max && $int > $max ) { return false; } return true; case 'float': return filter_var( $data, FILTER_VALIDATE_FLOAT ) !== false; case 'bool': return filter_var( $data, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE ) !== null; case 'json': json_decode( $data ); return json_last_error() === JSON_ERROR_NONE; case 'length': $min = isset( $options['min'] ) ? $options['min'] : 0; $max = isset( $options['max'] ) ? $options['max'] : PHP_INT_MAX; $length = strlen( $data ); return $length >= $min && $length <= $max; case 'regex': if ( ! isset( $options['pattern'] ) ) { return false; } return preg_match( $options['pattern'], $data ) === 1; case 'text': default: return is_string( $data ) && ! empty( trim( $data ) ); } } /** * Verify nonce with enhanced security * * @param string $nonce Nonce to verify. * @param string $action Action name. * @param string $context Context (ajax, form, etc.). * @return bool True if valid, false otherwise. * @since 1.0.6 */ public static function verify_nonce( $nonce, $action, $context = 'form' ) { // Basic nonce verification. if ( ! wp_verify_nonce( $nonce, $action ) ) { return false; } // Additional security checks for AJAX requests. if ( 'ajax' === $context ) { // Check if request is actually AJAX. if ( ! wp_doing_ajax() ) { return false; } // Check referer for AJAX requests. if ( ! check_ajax_referer( $action, 'nonce', false ) ) { return false; } } return true; } /** * Check user capabilities with context * * @param string $capability Capability to check. * @param int $object_id Optional object ID. * @param string $context Context for the check. * @return bool True if user has capability, false otherwise. * @since 1.0.6 */ public static function check_capability( $capability, $object_id = 0, $context = '' ) { // Basic capability check. if ( ! current_user_can( $capability, $object_id ) ) { return false; } // Additional context-specific checks. switch ( $context ) { case 'admin': // Ensure user is in admin area. if ( ! is_admin() ) { return false; } break; case 'ajax': // Ensure this is an AJAX request. if ( ! wp_doing_ajax() ) { return false; } break; case 'frontend': // Ensure user is not in admin area. if ( is_admin() ) { return false; } break; } return true; } /** * Rate limiting for actions * * @param string $action Action identifier. * @param int $limit Number of allowed actions. * @param int $window Time window in seconds. * @param string $identifier User identifier (IP, user ID, etc.). * @return bool True if action is allowed, false if rate limited. * @since 1.0.6 */ public static function rate_limit( $action, $limit = 10, $window = 60, $identifier = null ) { if ( null === $identifier ) { $identifier = self::get_client_identifier(); } $key = $action . '_' . $identifier; $current_time = time(); // Initialize if not exists. if ( ! isset( self::$rate_limits[ $key ] ) ) { self::$rate_limits[ $key ] = array(); } // Clean old entries. self::$rate_limits[ $key ] = array_filter( self::$rate_limits[ $key ], function ( $timestamp ) use ( $current_time, $window ) { return ( $current_time - $timestamp ) < $window; } ); // Check if limit exceeded. if ( count( self::$rate_limits[ $key ] ) >= $limit ) { return false; } // Add current timestamp. self::$rate_limits[ $key ][] = $current_time; return true; } /** * Get client identifier for rate limiting * * @return string Client identifier. * @since 1.0.6 */ private static function get_client_identifier() { // Use user ID if logged in. if ( is_user_logged_in() ) { return 'user_' . get_current_user_id(); } // Use IP address for anonymous users. $ip = self::get_client_ip(); return 'ip_' . md5( $ip ); } /** * Get client IP address * * @return string Client IP address. * @since 1.0.6 */ public static function get_client_ip() { $ip_keys = array( 'HTTP_CF_CONNECTING_IP', // Cloudflare. 'HTTP_CLIENT_IP', // Proxy. 'HTTP_X_FORWARDED_FOR', // Load balancer/proxy. 'HTTP_X_FORWARDED', // Proxy. 'HTTP_X_CLUSTER_CLIENT_IP', // Cluster. 'HTTP_FORWARDED_FOR', // Proxy. 'HTTP_FORWARDED', // Proxy. 'REMOTE_ADDR', // Standard. ); foreach ( $ip_keys as $key ) { if ( array_key_exists( $key, $_SERVER ) && ! empty( $_SERVER[ $key ] ) ) { $ip = sanitize_text_field( wp_unslash( $_SERVER[ $key ] ) ); // Handle comma-separated IPs. if ( strpos( $ip, ',' ) !== false ) { $ip = trim( explode( ',', $ip )[0] ); } // Validate IP. if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) ) { return $ip; } } } return '0.0.0.0'; } /** * Escape output for different contexts * * @param mixed $data Data to escape. * @param string $context Context for escaping. * @return mixed Escaped data. * @since 1.0.6 */ public static function escape_output( $data, $context = 'html' ) { if ( is_array( $data ) ) { return array_map( function ( $item ) use ( $context ) { return self::escape_output( $item, $context ); }, $data ); } switch ( $context ) { case 'attr': return esc_attr( $data ); case 'url': return esc_url( $data ); case 'js': return esc_js( $data ); case 'textarea': return esc_textarea( $data ); case 'sql': global $wpdb; return $wpdb->prepare( '%s', $data ); case 'html': default: return esc_html( $data ); } } /** * Log security events * * @param string $event Event type. * @param string $message Event message. * @param array $context Additional context. * @since 1.0.6 */ public static function log_security_event( $event, $message, $context = array() ) { if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) { return; } // No visitor IP, account ID, arbitrary request arguments or provider payloads. accessibility_audio_tts_debug_log( 'Security validation failed: ' . sanitize_key( (string) $event ) ); } /** * Custom die handler for security violations * * @param callable $handler Current die handler. * @return callable Modified die handler. * @since 1.0.6 */ public static function custom_die_handler( $handler ) { return function ( $message, $title = '', $args = array() ) use ( $handler ) { // Convert WP_Error to string if needed. $message_text = ''; if ( is_wp_error( $message ) ) { $message_text = $message->get_error_message(); } elseif ( is_string( $message ) ) { $message_text = $message; } elseif ( is_object( $message ) && method_exists( $message, '__toString' ) ) { $message_text = (string) $message; } // Log security-related deaths. if ( ! empty( $message_text ) && ( strpos( $message_text, 'nonce' ) !== false || strpos( $message_text, 'permission' ) !== false || strpos( $message_text, 'capability' ) !== false ) ) { self::log_security_event( 'security_violation', $message, array( 'title' => $title, 'args' => $args, 'request_uri' => isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '', 'user_agent' => isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '', ) ); } return call_user_func( $handler, $message, $title, $args ); }; } /** * Generate secure random token * * @param int $length Token length. * @return string Random token. * @since 1.0.6 */ public static function generate_token( $length = 32 ) { if ( function_exists( 'random_bytes' ) ) { return bin2hex( random_bytes( $length / 2 ) ); } elseif ( function_exists( 'openssl_random_pseudo_bytes' ) ) { return bin2hex( openssl_random_pseudo_bytes( $length / 2 ) ); } else { // Fallback for older PHP versions. return substr( str_shuffle( str_repeat( '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', ceil( $length / 62 ) ) ), 1, $length ); } } /** * Hash sensitive data * * @param string $data Data to hash. * @param string $salt Optional salt. * @return string Hashed data. * @since 1.0.6 */ public static function hash_data( $data, $salt = '' ) { if ( empty( $salt ) ) { $salt = wp_salt( 'auth' ); } return hash_hmac( 'sha256', $data, $salt ); } /** * Verify hashed data * * @param string $data Original data. * @param string $hash Hash to verify against. * @param string $salt Optional salt. * @return bool True if hash matches, false otherwise. * @since 1.0.6 */ public static function verify_hash( $data, $hash, $salt = '' ) { $computed_hash = self::hash_data( $data, $salt ); return hash_equals( $computed_hash, $hash ); } }