true, 'message' => '', ]; } $redeclaration = self::find_function_redeclaration( $snippet_code, $snippet_type ); if ( null !== $redeclaration ) { return [ 'valid' => false, // translators: %1$d is the line number, %2$s is the fully qualified function name. 'message' => sprintf( __( 'Line %1$d: Cannot redeclare function %2$s(). Rename the function or guard its declaration with function_exists().', 'insert-php' ), $redeclaration['line'], $redeclaration['name'] ), ]; } $validation_errors = []; // Set custom error handler to catch warnings and notices. set_error_handler( // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_set_error_handler function ( $errno, $errstr, $errfile, $errline ) use ( &$validation_errors ) { // Extract line number from eval'd code if present. if ( strpos( $errfile, "eval()'d code" ) !== false ) { // translators: %1$d is the line number, %2$s is the error message. $validation_errors[] = sprintf( __( 'Line %1$d: %2$s', 'insert-php' ), $errline, $errstr ); } else { $validation_errors[] = $errstr; } return true; // Don't execute PHP internal error handler. } ); ob_start(); try { $result = WINP_SNIPPET_TYPE_UNIVERSAL === $snippet_type ? eval( '?> ' . $snippet_code . ' false, // translators: %1$d is the line number, %2$s is the error message. 'message' => sprintf( __( 'Syntax error on line %1$d: %2$s', 'insert-php' ), $e->getLine(), $e->getMessage() ), ]; } catch ( Throwable $e ) { ob_end_clean(); restore_error_handler(); // For fatal errors in eval'd code, report the actual line number. if ( strpos( $e->getFile(), "eval()'d code" ) !== false ) { return [ 'valid' => false, // translators: %1$d is the line number, %2$s is the error message. 'message' => sprintf( __( 'Error on line %1$d: %2$s', 'insert-php' ), $e->getLine(), $e->getMessage() ), ]; } return [ 'valid' => false, // translators: %s is the error message. 'message' => sprintf( __( 'Error: %s', 'insert-php' ), $e->getMessage() ), ]; } // Discard any output (echo/print statements are normal for snippets). ob_end_clean(); restore_error_handler(); if ( ! empty( $validation_errors ) ) { return [ 'valid' => false, 'message' => implode( '
', $validation_errors ), ]; } if ( false === $result ) { return [ 'valid' => false, 'message' => __( 'The code contains syntax errors. Please review and fix them before saving.', 'insert-php' ), ]; } return [ 'valid' => true, 'message' => '', ]; } /** * Find a top-level function declaration that would be redeclared by eval(). * * PHP terminates the request when eval() declares an already-loaded function, * so this conflict has to be detected before evaluating the snippet. * * @param string $snippet_code Snippet code. * @param string $snippet_type Snippet type. * @return array{name:string,line:int}|null Conflict details, or null when none is found. */ public static function find_function_redeclaration( $snippet_code, $snippet_type ) { $source = WINP_SNIPPET_TYPE_UNIVERSAL === $snippet_type ? $snippet_code : ' $token ) { if ( is_array( $token ) ) { if ( in_array( $token[0], [ T_CURLY_OPEN, T_DOLLAR_OPEN_CURLY_BRACES ], true ) ) { ++$brace_depth; continue; } if ( T_NAMESPACE === $token[0] ) { $reading_namespace = true; $namespace_name = ''; continue; } if ( $reading_namespace ) { if ( self::is_namespace_name_token( $token[0] ) ) { $namespace_name .= $token[1]; } elseif ( ! in_array( $token[0], [ T_WHITESPACE, T_COMMENT, T_DOC_COMMENT ], true ) ) { $reading_namespace = false; $namespace_name = ''; } continue; } if ( T_FUNCTION !== $token[0] || $brace_depth !== $namespace_depth ) { continue; } $function_name = self::get_function_name( $tokens, $index ); if ( null === $function_name ) { continue; } $qualified_name = $namespace ? $namespace . '\\' . $function_name : $function_name; $normalized_name = strtolower( $qualified_name ); if ( isset( $declared[ $normalized_name ] ) || function_exists( $qualified_name ) ) { return [ 'name' => $qualified_name, 'line' => (int) $token[2], ]; } $declared[ $normalized_name ] = true; continue; } if ( $reading_namespace ) { if ( ';' === $token ) { $namespace = trim( $namespace_name, '\\' ); $namespace_depth = 0; $reading_namespace = false; } elseif ( '{' === $token ) { ++$brace_depth; $namespace = trim( $namespace_name, '\\' ); $namespace_depth = $brace_depth; $reading_namespace = false; } else { $reading_namespace = false; $namespace_name = ''; } continue; } if ( '{' === $token ) { ++$brace_depth; } elseif ( '}' === $token ) { $brace_depth = max( 0, $brace_depth - 1 ); if ( $namespace_depth > $brace_depth ) { $namespace = ''; $namespace_depth = 0; } } } return null; } /** * Check whether a token can be part of a namespace name. * * @param int $token_id Token identifier. * @return bool */ private static function is_namespace_name_token( $token_id ) { $name_tokens = [ T_STRING, T_NS_SEPARATOR ]; foreach ( [ 'T_NAME_QUALIFIED', 'T_NAME_FULLY_QUALIFIED' ] as $constant_name ) { if ( defined( $constant_name ) ) { $name_tokens[] = constant( $constant_name ); } } return in_array( $token_id, $name_tokens, true ); } /** * Get the name following a function token, ignoring whitespace and references. * * @param array|string> $tokens Token stream. * @param int $function_index Function token index. * @return string|null */ private static function get_function_name( $tokens, $function_index ) { $token_count = count( $tokens ); for ( $index = $function_index + 1; $index < $token_count; ++$index ) { $token = $tokens[ $index ]; if ( is_array( $token ) ) { if ( in_array( $token[0], [ T_WHITESPACE, T_COMMENT, T_DOC_COMMENT ], true ) || '&' === $token[1] ) { continue; } return T_STRING === $token[0] ? (string) $token[1] : null; } if ( '&' === $token ) { continue; } return null; } return null; } }