JezK
Edit File: file.cls.php
<?php // phpcs:ignoreFile /** * LiteSpeed File Operator Library Class * Append/Replace content to a file * * @since 1.1.0 */ namespace LiteSpeed; defined('WPINC') || exit(); class File { const MARKER = 'LiteSpeed Operator'; const REMOTE_MAX_BYTES = 33554432; const E_NET = 'net'; const E_HTTP = 'http'; const E_DATA = 'data'; const E_FILE = 'file'; /** * Marker name embedded in the seeded .htaccess. Bump the suffix when the rule template changes so ensure_static_protection() re-seeds outdated copies on existing sites. * * @since 7.9 */ const STATIC_DIR_HTACCESS_MARKER = 'LSCACHE_STATIC_PROTECT_V2'; /** * Build the whitelist .htaccess content for LITESPEED_STATIC_DIR * Anything not explicitly allowed returns 403. * Lazy-gen folders (avatar/localres) hand missing requests off to WP's index.php directly * * @since 7.9 * * @return string Full .htaccess body. */ public static function build_static_dir_htaccess() { $home_path = parse_url(home_url('/'), PHP_URL_PATH); if (!is_string($home_path) || '' === $home_path) { $home_path = '/'; } $wp_index = rtrim($home_path, '/') . '/index.php'; $marker = self::STATIC_DIR_HTACCESS_MARKER; $content = <<<HTACCESS # BEGIN {$marker} # DO NOT EDIT — auto-generated by LiteSpeed Cache. # Whitelist: only listed root files and folders are publicly readable. Options -Indexes <IfModule mod_rewrite.c> RewriteEngine On # Block dotfiles (.litespeed_conf.dat, .htaccess, etc.) anywhere in the tree RewriteRule (^|/)\.[^/]+$ - [F,L] # Whitelisted root-level public files RewriteRule ^robots\.txt$ - [L] # Pre-generated public asset folders: serve as-is, 404 if missing RewriteRule ^(css|js|ucss|ccss|lqip|optimax|crawler)(/|$) - [L] # Lazy-gen folders: serve existing files directly RewriteCond %{REQUEST_FILENAME} -f RewriteRule ^(avatar|localres)(/|$) - [L] # Lazy-gen folders: missing file → hand off to WP index.php for on-the-fly generation RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^(avatar|localres)/.+ {$wp_index} [L] # Default deny RewriteRule .* - [F,L] </IfModule> # END {$marker} HTACCESS; /** * Filters the generated static-directory .htaccess content. * * @since 7.9.1 * * @param string $content Full .htaccess body. */ return apply_filters('litespeed_static_dir_htaccess', $content); } /** * Seed .htaccess + robots.txt into LITESPEED_STATIC_DIR if missing or outdated. * * Re-seeds whenever the on-disk body differs from the freshly built template — covers template * bumps, hand edits that drop critical rules, partial writes, and home_url() changes that would * otherwise leave a stale rewrite target in the file. * * @since 7.9 */ public static function ensure_static_protection() { if (!is_dir(LITESPEED_STATIC_DIR)) { return; } $htaccess_path = LITESPEED_STATIC_DIR . '/.htaccess'; $content = self::build_static_dir_htaccess(); if (is_file($htaccess_path)) { $existing = @file_get_contents($htaccess_path); if (false !== $existing && $existing === $content) { return; } } $bytes = @file_put_contents($htaccess_path, $content, LOCK_EX); $robots_path = LITESPEED_STATIC_DIR . '/robots.txt'; if (!is_file($robots_path)) { @file_put_contents($robots_path, "User-agent: *\nDisallow: /\n", LOCK_EX); } $banner = sprintf( /* translators: %s: full path to the .htaccess file LiteSpeed Cache failed to create. */ __('LiteSpeed Cache could not create %s. Sensitive files (configuration backups, debug logs) under this folder may be publicly accessible. Fix the directory permissions to restore protection.', 'litespeed-cache'), '<code>' . esc_html($htaccess_path) . '</code>' ); if ($bytes === strlen($content)) { Admin_Display::dismiss_pin_by_content($banner, Admin_Display::NOTICE_RED, true); } else { Admin_Display::error($banner, false, true); } } /** * Download a bounded remote file through the WordPress safe HTTP API. * * @since 7.9.1 * * @param string $url Remote URL. * @param string $filename Final destination path. * @param int $timeout Request timeout in seconds. * @param int $redirects Maximum redirects. * @param string|array $root Bound root for the destination. * @return string|\WP_Error Temporary file path or a classified failure. */ public static function download( $url, $filename, $timeout = 60, $redirects = 5, $root = '' ) { if (!is_string($url) || '' === $url) { return new \WP_Error(self::E_DATA); } if (!is_string($filename) || '' === $filename || is_link($filename)) { return new \WP_Error(self::E_FILE); } $folder = dirname($filename); if ((!is_dir($folder) && ($root || !wp_mkdir_p($folder))) || !($temp = self::temp_file($folder, '.lscwp-', $root))) { return new \WP_Error(self::E_FILE); } if (defined('LITESPEED_STATIC_DIR') && 0 === strpos($filename, LITESPEED_STATIC_DIR . '/')) { self::ensure_static_protection(); } $response = wp_safe_remote_get($url, [ 'timeout' => $timeout, 'redirection' => $redirects, 'limit_response_size' => self::REMOTE_MAX_BYTES + 1, 'stream' => true, 'filename' => $temp, ]); if (is_wp_error($response)) { wp_delete_file($temp); return new \WP_Error(self::E_NET, $response->get_error_message(), $response->get_error_code()); } $code = (int) wp_remote_retrieve_response_code($response); if (200 !== $code) { wp_delete_file($temp); return new \WP_Error(self::E_HTTP, '', $code); } clearstatcache(true, $temp); $size = is_file($temp) ? filesize($temp) : false; if (!$size || self::REMOTE_MAX_BYTES < $size) { wp_delete_file($temp); return new \WP_Error(self::E_DATA); } return $temp; } /** * Detect if an URL is 404 * * @since 3.3 */ public static function is_404( $url ) { $response = wp_safe_remote_get($url); $code = wp_remote_retrieve_response_code($response); if ($code == 404) { return true; } return false; } /** * Delete folder * * @since 2.1 */ public static function rrmdir( $dir ) { $files = array_diff(scandir($dir), array( '.', '..' )); foreach ($files as $file) { is_dir("$dir/$file") ? self::rrmdir("$dir/$file") : unlink("$dir/$file"); } return rmdir($dir); } public static function count_lines( $filename ) { if (!file_exists($filename)) { return 0; } $file = new \SplFileObject($filename); $file->seek(PHP_INT_MAX); return $file->key() + 1; } /** * Read data from file * * @since 1.1.0 * @param string $filename * @param int $start_line * @param int $lines */ public static function read( $filename, $start_line = null, $lines = null ) { if (!file_exists($filename)) { return ''; } if (!is_readable($filename)) { return false; } if ($start_line !== null) { $res = array(); $file = new \SplFileObject($filename); $file->seek($start_line); if ($lines === null) { while (!$file->eof()) { $res[] = rtrim($file->current(), "\n"); $file->next(); } } else { for ($i = 0; $i < $lines; $i++) { if ($file->eof()) { break; } $res[] = rtrim($file->current(), "\n"); $file->next(); } } unset($file); return $res; } $content = file_get_contents($filename); $content = self::remove_zero_space($content); return $content; } /** * Append data to file * * @since 1.1.5 * @access public * @param string $filename * @param string $data * @param boolean $mkdir * @param boolean $silence Used to avoid WP's functions are used */ public static function append( $filename, $data, $mkdir = false, $silence = true ) { return self::save($filename, $data, $mkdir, true, $silence); } /** * Save data to file * * @since 1.1.0 * @param string $filename * @param string $data * @param boolean $mkdir * @param boolean $append If the content needs to be appended * @param boolean $silence Used to avoid WP's functions are used */ public static function save( $filename, $data, $mkdir = false, $append = false, $silence = true ) { if (is_null($filename)) { return $silence ? false : __('Filename is empty!', 'litespeed-cache'); } $error = false; $folder = dirname($filename); // mkdir if folder does not exist if (!file_exists($folder)) { if (!$mkdir) { return $silence ? false : sprintf(__('Folder does not exist: %s', 'litespeed-cache'), $folder); } set_error_handler('litespeed_exception_handler'); try { mkdir($folder, 0755, true); } catch (\ErrorException $ex) { return $silence ? false : sprintf(__('Can not create folder: %1$s. Error: %2$s', 'litespeed-cache'), $folder, $ex->getMessage()); } restore_error_handler(); } if (strpos($filename, LITESPEED_STATIC_DIR . '/') === 0) { self::ensure_static_protection(); } if (!file_exists($filename)) { if (!is_writable($folder)) { return $silence ? false : sprintf(__('Folder is not writable: %s.', 'litespeed-cache'), $folder); } set_error_handler('litespeed_exception_handler'); try { touch($filename); } catch (\ErrorException $ex) { return $silence ? false : sprintf(__('File %s is not writable.', 'litespeed-cache'), $filename); } restore_error_handler(); } elseif (!is_writable($filename)) { return $silence ? false : sprintf(__('File %s is not writable.', 'litespeed-cache'), $filename); } $data = self::remove_zero_space($data); $ret = file_put_contents($filename, $data, $append ? FILE_APPEND : LOCK_EX); if ($ret === false) { return $silence ? false : sprintf(__('Failed to write to %s.', 'litespeed-cache'), $filename); } return true; } /** * Atomically save content through a same-directory temporary file. * * @since 7.9.1 * * @param string $filename Destination file. * @param string $data File content. * @param bool $binary Whether content must remain byte-exact. * @return bool */ public static function save_atomic( $filename, $data, $binary = false ) { if (!is_string($filename) || '' === $filename || !is_string($data) || is_link($filename)) { return false; } $folder = dirname($filename); if (!is_dir($folder) && !wp_mkdir_p($folder)) { return false; } if (defined('LITESPEED_STATIC_DIR') && 0 === strpos($filename, LITESPEED_STATIC_DIR . '/')) { self::ensure_static_protection(); } if (!$binary) { $data = self::remove_zero_space($data); } $temp = self::temp_file($folder); if (!$temp) { return false; } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents $written = file_put_contents($temp, $data, LOCK_EX); if (false === $written || strlen($data) !== $written) { wp_delete_file($temp); return false; } return self::publish_temp_file($temp, $filename); } /** * Bind a root directory to its current device and inode. * * @since 7.9.1 * * @param string $root Root directory. * @return array|false `[ path, identity ]`, or false. */ public static function bind_root( $root ) { $resolved = realpath($root); $stat = $resolved ? @lstat($resolved) : false; return $stat ? [ wp_normalize_path($resolved), $stat['dev'] . ':' . $stat['ino'] ] : false; } /** * Confirm that a path still resolves inside a root directory. * * @since 7.9.1 * * @param string $file Path to test. * @param string|array $root Root path, or the result of `bind_root()`; empty skips the check. * @return bool */ public static function within( $file, $root ) { $identity = ''; if (is_array($root)) { list($root, $identity) = $root; } if ('' === $root) { return true; } $resolved = realpath($file); $base = realpath($root); if (!$resolved || !$base) { return false; } if ('' !== $identity) { $stat = @lstat($base); if (!$stat || $identity !== $stat['dev'] . ':' . $stat['ino']) { return false; } } $resolved = wp_normalize_path($resolved); $base = wp_normalize_path($base); return $resolved === $base || 0 === strpos($resolved, trailingslashit($base)); } /** * Create a plugin temporary file. * * @since 7.9.1 * * @param string $dir Destination directory. * @param string $prefix Temporary filename prefix. * @param string $root Directory `$dir` must resolve inside; empty skips the check. * @return string|false */ public static function temp_file( $dir, $prefix = '.lscwp-', $root = '' ) { $dir = realpath($dir); if (!$dir || !is_dir($dir) || !self::within($dir, $root)) { return false; } $file = @tempnam($dir, $prefix); if (!$file || dirname($file) !== $dir || is_link($file)) { if ($file) { wp_delete_file($file); } return false; } return $file; } /** * Publish a same-directory temporary file with WordPress's file mode. * * @since 7.9.1 * * @param string $temp Temporary file. * @param string $target Destination file. * @param string $root Directory the destination must resolve inside; empty skips the check. * @return bool */ public static function publish_temp_file( $temp, $target, $root = '' ) { $mode = defined('FS_CHMOD_FILE') ? FS_CHMOD_FILE : 0644; $temp_dir = realpath(dirname($temp)); $target_dir = realpath(dirname($target)); // Same-directory is not in-scope: both sides move together when their shared parent is swapped. if (!$temp_dir || $temp_dir !== $target_dir || !is_file($temp) || is_link($temp) || !self::within($target_dir, $root)) { wp_delete_file($temp); return false; } if (!@chmod($temp, $mode)) { Root::debugErr('Failed to set published file permissions: ' . $target); } if (!@rename($temp, $target)) { wp_delete_file($temp); return false; } return true; } /** * Remove Unicode zero-width space <200b><200c> * * @since 2.1.2 * @since 2.9 changed to public */ public static function remove_zero_space( $content ) { if (is_array($content)) { $content = array_map(__CLASS__ . '::remove_zero_space', $content); return $content; } // Remove UTF-8 BOM if present if (substr($content, 0, 3) === "\xEF\xBB\xBF") { $content = substr($content, 3); } $content = str_replace("\xe2\x80\x8b", '', $content); $content = str_replace("\xe2\x80\x8c", '', $content); $content = str_replace("\xe2\x80\x8d", '', $content); return $content; } /** * Appends an array of strings into a file (.htaccess ), placing it between * BEGIN and END markers. * * Replaces existing marked info. Retains surrounding * data. Creates file if none exists. * * @param string $filename Filename to alter. * @param string $marker The marker to alter. * @param array|string|false $insertion The new content to insert. * @param bool $prepend Prepend insertion if not exist. * @return bool True on write success, false on failure. */ public static function insert_with_markers( $filename, $insertion = false, $marker = false, $prepend = false ) { if (!$marker) { $marker = self::MARKER; } if (!$insertion) { $insertion = array(); } return self::_insert_with_markers($filename, $marker, $insertion, $prepend); // todo: capture exceptions } /** * Return wrapped block data with marker * * @param string $insertion * @param string $marker * @return string The block data */ public static function wrap_marker_data( $insertion, $marker = false ) { if (!$marker) { $marker = self::MARKER; } $start_marker = "# BEGIN {$marker}"; $end_marker = "# END {$marker}"; $new_data = implode("\n", array_merge(array( $start_marker ), $insertion, array( $end_marker ))); return $new_data; } /** * Touch block data from file, return with marker * * @param string $filename * @param string $marker * @return string The current block data */ public static function touch_marker_data( $filename, $marker = false ) { if (!$marker) { $marker = self::MARKER; } $result = self::_extract_from_markers($filename, $marker); if (!$result) { return false; } $start_marker = "# BEGIN {$marker}"; $end_marker = "# END {$marker}"; $new_data = implode("\n", array_merge(array( $start_marker ), $result, array( $end_marker ))); return $new_data; } /** * Extracts strings from between the BEGIN and END markers in the .htaccess file. * * @param string $filename * @param string $marker * @return array An array of strings from a file (.htaccess ) from between BEGIN and END markers. */ public static function extract_from_markers( $filename, $marker = false ) { if (!$marker) { $marker = self::MARKER; } return self::_extract_from_markers($filename, $marker); } /** * Extracts strings from between the BEGIN and END markers in the .htaccess file. * * @param string $filename * @param string $marker * @return array An array of strings from a file (.htaccess ) from between BEGIN and END markers. */ private static function _extract_from_markers( $filename, $marker ) { $result = array(); if (!file_exists($filename)) { return $result; } if ($markerdata = explode("\n", implode('', file($filename)))) { $state = false; foreach ($markerdata as $markerline) { if (strpos($markerline, '# END ' . $marker) !== false) { $state = false; } if ($state) { $result[] = $markerline; } if (strpos($markerline, '# BEGIN ' . $marker) !== false) { $state = true; } } } return array_map('trim', $result); } /** * Inserts an array of strings into a file (.htaccess ), placing it between BEGIN and END markers. * * Replaces existing marked info. Retains surrounding data. Creates file if none exists. * * NOTE: will throw error if failed * * @since 3.0- * @since 3.0 Throw errors if failed * @access private */ private static function _insert_with_markers( $filename, $marker, $insertion, $prepend = false ) { if (!file_exists($filename)) { if (!is_writable(dirname($filename))) { Error::t('W', dirname($filename)); } set_error_handler('litespeed_exception_handler'); try { touch($filename); } catch (\ErrorException $ex) { Error::t('W', $filename); } restore_error_handler(); } elseif (!is_writable($filename)) { Error::t('W', $filename); } if (!is_array($insertion)) { $insertion = explode("\n", $insertion); } $start_marker = "# BEGIN {$marker}"; $end_marker = "# END {$marker}"; $fp = fopen($filename, 'r+'); if (!$fp) { Error::t('W', $filename); } // Attempt to get a lock. If the filesystem supports locking, this will block until the lock is acquired. flock($fp, LOCK_EX); $lines = array(); while (!feof($fp)) { $lines[] = rtrim(fgets($fp), "\r\n"); } // Split out the existing file into the preceding lines, and those that appear after the marker $pre_lines = $post_lines = $existing_lines = array(); $found_marker = $found_end_marker = false; foreach ($lines as $line) { if (!$found_marker && false !== strpos($line, $start_marker)) { $found_marker = true; continue; } elseif (!$found_end_marker && false !== strpos($line, $end_marker)) { $found_end_marker = true; continue; } if (!$found_marker) { $pre_lines[] = $line; } elseif ($found_marker && $found_end_marker) { $post_lines[] = $line; } else { $existing_lines[] = $line; } } // Check to see if there was a change if ($existing_lines === $insertion) { flock($fp, LOCK_UN); fclose($fp); return true; } // Check if need to prepend data if not exist if ($prepend && !$post_lines) { // Generate the new file data $new_file_data = implode("\n", array_merge(array( $start_marker ), $insertion, array( $end_marker ), $pre_lines)); } else { // Generate the new file data $new_file_data = implode("\n", array_merge($pre_lines, array( $start_marker ), $insertion, array( $end_marker ), $post_lines)); } // Write to the start of the file, and truncate it to that length fseek($fp, 0); $bytes = fwrite($fp, $new_file_data); if ($bytes) { ftruncate($fp, ftell($fp)); } fflush($fp); flock($fp, LOCK_UN); fclose($fp); return (bool) $bytes; } }