%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(
'',
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 );
}
}