^ "` plus glob characters) and rely on * `realpath()` + the allowlist below to enforce the actual * "is this a Java binary" check. */ if ( self::contains_shell_metacharacters( $path ) ) { return false; } $real = \realpath( $path ); if ( false === $real || '' === $real ) { return false; } if ( ! \is_file( $real ) || ! \is_executable( $real ) ) { return false; } /** * On Windows the binary must end in `.exe` to be runnable * without a shell-side resolution step. */ if ( '\\' === DIRECTORY_SEPARATOR ) { if ( '.exe' !== \strtolower( \substr( $real, -4 ) ) ) { return false; } } if ( ! self::is_under_allowed_dirs( $real, self::allowed_dirs() ) ) { return false; } return $real; } /** * Validate a candidate value for the Java executable and emit a * debug-log entry if the candidate is rejected. * * Thin wrapper around `validate()` that surfaces the rejection * to operators via the `minify` debug log so admins can diagnose * a "minifier stopped working" symptom directly from the log. * * @since 2.10.0 * * @param string $path Candidate path to a Java binary. * @param string $context Short tag used in the debug-log entry * (for example, the engine slug `yuijs`). * * @return string|false Canonical path on success, false on rejection. */ public static function validate_with_log( $path, $context = '' ) { $w3tc_result = self::validate( $path ); if ( false === $w3tc_result ) { $tag = '' === $context ? 'java' : $context; /** * Include the rejected path verbatim in the log line. The metachar * regex inside `validate()` runs before this code path, so the * string is free of control bytes; debug logs land on the * filesystem (not in any UI), so echoing the path is operator- * facing only. Without this, debugging "minifier silently * stopped working" requires instrumenting PHP to see what value * was being validated. */ $logged_path = \is_string( $path ) ? $path : '(non-string)'; Util_Debug::log( 'minify', \sprintf( 'Util_Java: rejected non-allowlisted Java executable path "%s" for %s (allowed dirs: %s).', $logged_path, $tag, \implode( PATH_SEPARATOR, self::allowed_dirs() ) ) ); } return $w3tc_result; } /** * Validate a candidate JAR path before it is passed to `java -jar`. * * Returns the canonical absolute path on success, or `false` on * any failure. Callers must not fall back to the raw value: the * vendored minifier wrappers interpolate `$jarFile` into `exec()`. * * The candidate must canonicalize via `realpath()` (symlink targets * that leave the allowlist are refused), must be a readable regular * file whose name ends in `.jar`, must start with ZIP magic bytes, * and must live under one of `allowed_jar_dirs()`. Operators with * non-standard layouts may extend the list by defining * `W3TC_JAVA_JAR_ALLOWED_DIRS` in `wp-config.php`. * * @since 2.10.6 * * @param string $path Candidate path to a JAR. * * @return string|false Canonical path on success, false on rejection. */ public static function validate_jar( $path ) { if ( ! \is_string( $path ) || '' === $path ) { return false; } if ( self::contains_shell_metacharacters( $path ) ) { return false; } $real = \realpath( $path ); if ( false === $real || '' === $real ) { return false; } if ( ! \is_file( $real ) || ! \is_readable( $real ) ) { return false; } if ( '.jar' !== \strtolower( \substr( $real, -4 ) ) ) { return false; } if ( ! self::is_under_allowed_dirs( $real, self::allowed_jar_dirs() ) ) { return false; } /** * JAR files are ZIP archives. Read magic only after the * allowlist check so out-of-boundary files are never opened. */ $handle = \fopen( $real, 'rb' ); if ( false === $handle ) { return false; } $magic = \fread( $handle, 4 ); \fclose( $handle ); if ( ! \is_string( $magic ) || 2 > \strlen( $magic ) || 0 !== \strpos( $magic, 'PK' ) ) { return false; } return $real; } /** * Validate a candidate JAR path and emit a minify debug-log entry * if the candidate is rejected. * * @since 2.10.6 * * @param string $path Candidate path to a JAR. * @param string $context Short tag used in the debug-log entry. * * @return string|false Canonical path on success, false on rejection. */ public static function validate_jar_with_log( $path, $context = '' ) { $w3tc_result = self::validate_jar( $path ); if ( false === $w3tc_result ) { $tag = '' === $context ? 'jar' : $context; $logged_path = \is_string( $path ) ? $path : '(non-string)'; Util_Debug::log( 'minify', \sprintf( 'Util_Java: rejected JAR path "%s" for %s (allowed dirs: %s).', $logged_path, $tag, \implode( PATH_SEPARATOR, self::allowed_jar_dirs() ) ) ); } return $w3tc_result; } /** * Validate a Java executable and JAR together. * * Returns canonical paths on success, or `false` if either path is * rejected. Callers should treat `false` as "do not invoke the tool". * * @since 2.10.6 * * @param string $path_java Candidate Java executable. * @param string $path_jar Candidate JAR. * @param string $context Short tag for debug-log entries. * * @return array{java:string,jar:string}|false */ public static function validate_tools( $path_java, $path_jar, $context = '' ) { $java = self::validate_with_log( $path_java, $context ); if ( false === $java ) { return false; } $jar = self::validate_jar_with_log( $path_jar, $context ); if ( false === $jar ) { return false; } return array( 'java' => $java, 'jar' => $jar, ); } /** * Returns the list of directories from which a JAR is accepted. * * Defaults include the plugin directory, `WP_CONTENT_DIR`, and * platform package locations. Operators may override by defining * `W3TC_JAVA_JAR_ALLOWED_DIRS` in `wp-config.php` using * `PATH_SEPARATOR`. Existing directories are canonicalized so a * symlinked `wp-content` still matches `realpath()` of a JAR. * * @since 2.10.6 * * @return string[] */ public static function allowed_jar_dirs() { if ( \defined( 'W3TC_JAVA_JAR_ALLOWED_DIRS' ) ) { $raw = (string) \constant( 'W3TC_JAVA_JAR_ALLOWED_DIRS' ); $dirs = \array_filter( \array_map( 'trim', \explode( PATH_SEPARATOR, $raw ) ) ); if ( ! empty( $dirs ) ) { return self::canonicalize_allowed_dirs( \array_values( $dirs ) ); } } $dirs = array(); if ( \defined( 'W3TC_DIR' ) && \is_string( \constant( 'W3TC_DIR' ) ) && '' !== \constant( 'W3TC_DIR' ) ) { $dirs[] = \constant( 'W3TC_DIR' ); } if ( \defined( 'WP_CONTENT_DIR' ) && \is_string( \WP_CONTENT_DIR ) && '' !== \WP_CONTENT_DIR ) { $dirs[] = \WP_CONTENT_DIR; } if ( '\\' === DIRECTORY_SEPARATOR ) { $dirs[] = 'C:\\Program Files'; $dirs[] = 'C:\\Program Files (x86)'; } else { $dirs[] = '/usr/share/java'; $dirs[] = '/usr/share'; $dirs[] = '/opt'; } return self::canonicalize_allowed_dirs( $dirs ); } /** * Returns the list of directories from which a Java binary is * accepted. Defaults to a conservative set per platform; * operators may override by defining `W3TC_JAVA_BIN_ALLOWED_DIRS` * in `wp-config.php`. The override is parsed using PHP's * `PATH_SEPARATOR` (`;` on Windows, `:` elsewhere) so a Windows * drive letter such as `C:\Program Files\Java\bin` is not * accidentally split on its own colon. * * The constant must be defined server-side; it is never read * from web input. * * **Allowlist semantics — sharp edge.** The check in `validate()` * is a pure string-prefix match against `