* * @see http://wcpos.com * @package WCPOS\WooCommercePOS */ namespace WCPOS\WooCommercePOS\Templates; use WP_Error; /** * Validator class. */ class Validator { /** * List of dangerous PHP functions that should be flagged. * * @var array */ private static $dangerous_functions = array( 'eval', 'exec', 'system', 'shell_exec', 'passthru', 'proc_open', 'popen', 'curl_exec', 'curl_multi_exec', 'parse_ini_file', 'show_source', 'file_put_contents', 'fopen', 'fwrite', 'unlink', 'rmdir', 'mkdir', 'chmod', 'chown', 'chgrp', 'touch', 'symlink', 'link', 'tempnam', 'tmpfile', 'move_uploaded_file', 'phpinfo', 'assert', 'create_function', 'call_user_func', 'call_user_func_array', ); /** * Validate template code. * * @param string $content Template content. * @param string $language Template language (php, javascript). * * @return true|WP_Error True on success, WP_Error on failure. */ public static function validate( string $content, string $language ) { if ( 'php' === $language ) { return self::validate_php( $content ); } if ( 'javascript' === $language ) { return self::validate_javascript( $content ); } return new WP_Error( 'invalid_language', /* translators: Validation error shown when an unsupported template language is provided. */ __( 'Invalid template language.', 'woocommerce-pos' ) ); } /** * Sanitize template content. * * @param string $content Template content. * @param string $language Template language. * * @return string Sanitized content. */ public static function sanitize( string $content, string $language ): string { if ( 'php' === $language ) { // For PHP, we don't want to sanitize too much as it would break the code // Just ensure proper slashing for storage. return $content; } if ( 'javascript' === $language ) { // For JavaScript, similarly we want to preserve the code structure. return $content; } return $content; } /** * Check if user is allowed to edit templates. * * @return bool True if user can edit templates, false otherwise. */ public static function can_edit_templates(): bool { return current_user_can( 'manage_woocommerce_pos' ); } /** * Log template validation attempt. * * @param int $template_id Template ID. * @param string $content Template content. * @param mixed $result Validation result. * * @return void */ public static function log_validation( int $template_id, string $content, $result ): void { if ( ! \defined( 'WCPOS_TEMPLATE_VALIDATION_LOG' ) || ! WCPOS_TEMPLATE_VALIDATION_LOG ) { return; } $user = wp_get_current_user(); $log_entry = array( 'timestamp' => current_time( 'mysql' ), 'user_id' => $user->ID, 'user_login' => $user->user_login, 'template_id' => $template_id, 'result' => is_wp_error( $result ) ? $result->get_error_message() : 'success', 'content_hash' => md5( $content ), ); // Store in option (you might want to use a custom table for better performance). $logs = get_option( 'wcpos_template_validation_logs', array() ); $logs[] = $log_entry; // Keep only last 100 entries. if ( \count( $logs ) > 100 ) { $logs = \array_slice( $logs, -100 ); } update_option( 'wcpos_template_validation_logs', $logs ); } /** * Validate PHP template code. * * @param string $content PHP template content. * * @return true|WP_Error True on success, WP_Error on failure. */ private static function validate_php( string $content ) { // Check for syntax errors. $syntax_check = self::check_php_syntax( $content ); if ( is_wp_error( $syntax_check ) ) { return $syntax_check; } // Check for dangerous functions. $dangerous_check = self::check_dangerous_functions( $content ); if ( is_wp_error( $dangerous_check ) ) { return $dangerous_check; } /* * Filters the PHP template validation result. * * @param true|WP_Error $result Validation result. * @param string $content Template content. * * @since 1.8.0 * * @hook woocommerce_pos_validate_php_template */ return apply_filters( 'woocommerce_pos_validate_php_template', true, $content ); } /** * Validate JavaScript template code. * * @param string $content JavaScript template content. * * @return true|WP_Error True on success, WP_Error on failure. */ private static function validate_javascript( string $content ) { // Basic validation for JavaScript // Check for obviously dangerous patterns. $dangerous_patterns = array( '/eval\s*\(/i', '/Function\s*\(/i', '/setTimeout\s*\(\s*["\']/', '/setInterval\s*\(\s*["\']/', ); foreach ( $dangerous_patterns as $pattern ) { if ( preg_match( $pattern, $content ) ) { return new WP_Error( 'dangerous_javascript', \sprintf( // translators: %s: pattern that was found. __( 'Template contains potentially dangerous JavaScript code: %s', 'woocommerce-pos' ), $pattern ) ); } } /* * Filters the JavaScript template validation result. * * @param true|WP_Error $result Validation result. * @param string $content Template content. * * @since 1.8.0 * * @hook woocommerce_pos_validate_javascript_template */ return apply_filters( 'woocommerce_pos_validate_javascript_template', true, $content ); } /** * Check PHP syntax. * * @param string $content PHP code to check. * * @return true|WP_Error True if syntax is valid, WP_Error otherwise. */ private static function check_php_syntax( string $content ) { // Use php -l to check syntax if available. if ( \function_exists( 'exec' ) && ! \defined( 'DISABLE_TEMPLATE_SYNTAX_CHECK' ) ) { $temp_file = tempnam( sys_get_temp_dir(), 'wcpos_template_' ); file_put_contents( $temp_file, $content ); $output = array(); $return_var = 0; exec( 'php -l ' . escapeshellarg( $temp_file ) . ' 2>&1', $output, $return_var ); unlink( $temp_file ); if ( 0 !== $return_var ) { return new WP_Error( 'php_syntax_error', \sprintf( // translators: %s: error message. __( 'PHP syntax error: %s', 'woocommerce-pos' ), implode( "\n", $output ) ) ); } } // Fallback: Basic check for unclosed PHP tags. $open_tags = substr_count( $content, '' ); // Note: It's valid to have more open tags than close tags (files can end without closing tag) // But if we have more close tags, that's definitely an error. if ( $close_tags > $open_tags ) { return new WP_Error( 'php_syntax_error', __( 'PHP syntax error: Mismatched PHP tags.', 'woocommerce-pos' ) ); } return true; } /** * Check for dangerous PHP functions. * * @param string $content PHP code to check. * * @return true|WP_Error True if no dangerous functions found, WP_Error otherwise. */ private static function check_dangerous_functions( string $content ) { /** * Filters the list of dangerous PHP functions. * * @param array $functions List of dangerous function names. * * @since 1.8.0 * * @hook woocommerce_pos_template_dangerous_functions */ $dangerous = apply_filters( 'woocommerce_pos_template_dangerous_functions', self::$dangerous_functions ); // Remove comments from content to avoid false positives. $content_without_comments = preg_replace( '/\/\*[\s\S]*?\*\/|\/\/.*$/m', '', $content ); foreach ( $dangerous as $function ) { // Check for function calls. if ( preg_match( '/\b' . preg_quote( $function, '/' ) . '\s*\(/i', $content_without_comments ) ) { /** * Filters whether to allow dangerous functions in templates. * * @param bool $allow Whether to allow the dangerous function. * @param string $function Function name. * @param string $content Template content. * * @since 1.8.0 * * @hook woocommerce_pos_template_allow_dangerous_function */ $allow = apply_filters( 'woocommerce_pos_template_allow_dangerous_function', false, $function, $content ); if ( ! $allow ) { return new WP_Error( 'dangerous_function', \sprintf( // translators: %s: function name. __( 'Template contains dangerous function: %s(). This function is not allowed for security reasons.', 'woocommerce-pos' ), $function ) ); } } } return true; } }