Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
77.22% |
61 / 79 |
|
35.71% |
5 / 14 |
CRAP | |
0.00% |
0 / 1 |
| Filesystem_Utils | |
77.22% |
61 / 79 |
|
35.71% |
5 / 14 |
44.11 | |
0.00% |
0 / 1 |
| iterate_directory | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
3 | |||
| iterate_files | |
36.36% |
4 / 11 |
|
0.00% |
0 / 1 |
5.32 | |||
| validate_path | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
4.03 | |||
| is_boost_cache_directory | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
| get_request_filename | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
| is_rebuild_file | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| create_directory | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
3.04 | |||
| create_empty_index_files | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
| rebuild_file | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
3.04 | |||
| restore_file | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
| delete_file | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
| delete_empty_dir | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
2.03 | |||
| is_dir_empty | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
| write_to_file | |
66.67% |
4 / 6 |
|
0.00% |
0 / 1 |
3.33 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress; |
| 4 | |
| 5 | use Automattic\Jetpack_Boost\Modules\Optimizations\Page_Cache\Pre_WordPress\Path_Actions\Path_Action; |
| 6 | use SplFileInfo; |
| 7 | |
| 8 | class Filesystem_Utils { |
| 9 | |
| 10 | const DELETE_ALL = 'delete-all'; // delete all files and directories in a given directory, recursively. |
| 11 | const DELETE_FILE = 'delete-single'; // delete a single file or recursively delete a single directory in a given directory. |
| 12 | const DELETE_FILES = 'delete-files'; // delete all files in a given directory. |
| 13 | const REBUILD_ALL = 'rebuild-all'; // rebuild all files and directories in a given directory, recursively. |
| 14 | const REBUILD_FILE = 'rebuild-single'; // rebuild a single file or recursively rebuild a single directory in a given directory. |
| 15 | const REBUILD_FILES = 'rebuild-files'; // rebuild all files in a given directory. |
| 16 | const REBUILD = 'rebuild'; // rebuild mode for managing expired files |
| 17 | const DELETE = 'delete'; // delete mode for managing expired files |
| 18 | const REBUILD_FILE_EXTENSION = '.rebuild.html'; // The extension used for rebuilt files. |
| 19 | |
| 20 | /** |
| 21 | * Iterate over a directory and apply an action to each file. |
| 22 | * |
| 23 | * This applies the action to all files and subdirectories in the given directory. |
| 24 | * |
| 25 | * @param string $path - The directory to iterate over. |
| 26 | * @param Path_Action $action - The action to apply to each file. |
| 27 | * @return int|Boost_Cache_Error - The number of files processed, or Boost_Cache_Error on failure. |
| 28 | */ |
| 29 | public static function iterate_directory( $path, Path_Action $action ) { |
| 30 | clearstatcache(); |
| 31 | $validation_error = self::validate_path( $path ); |
| 32 | if ( $validation_error instanceof Boost_Cache_Error ) { |
| 33 | return $validation_error; |
| 34 | } |
| 35 | |
| 36 | $iterator = new \RecursiveIteratorIterator( |
| 37 | new \RecursiveDirectoryIterator( $path, \RecursiveDirectoryIterator::SKIP_DOTS ), |
| 38 | \RecursiveIteratorIterator::CHILD_FIRST |
| 39 | ); |
| 40 | |
| 41 | $count = 0; |
| 42 | foreach ( $iterator as $file ) { |
| 43 | $count += $action->apply_to_path( new SplFileInfo( $file ) ); |
| 44 | } |
| 45 | |
| 46 | $count += $action->apply_to_path( new SplFileInfo( $path ) ); |
| 47 | |
| 48 | return $count; |
| 49 | } |
| 50 | |
| 51 | /** |
| 52 | * Iterate over a directory and apply an action to each file. |
| 53 | * |
| 54 | * This applies the action to all files in the directory, except index.html. And doesn't go into subdirectories. |
| 55 | * |
| 56 | * @param string $path - The directory to iterate over. |
| 57 | * @param Path_Action $action - The action to apply to each file. |
| 58 | * @return int|Boost_Cache_Error - The number of files processed, or Boost_Cache_Error on failure. |
| 59 | */ |
| 60 | public static function iterate_files( $path, Path_Action $action ) { |
| 61 | clearstatcache(); |
| 62 | $validation_error = self::validate_path( $path ); |
| 63 | if ( $validation_error instanceof Boost_Cache_Error ) { |
| 64 | return $validation_error; |
| 65 | } |
| 66 | |
| 67 | $path = Boost_Cache_Utils::trailingslashit( $path ); |
| 68 | // Files to delete are all files in the given directory, except index.html. index.html is used to prevent directory listing. |
| 69 | $files = array_diff( scandir( $path ), array( '.', '..', 'index.html' ) ); |
| 70 | $count = 0; |
| 71 | foreach ( $files as $file ) { |
| 72 | $fileinfo = new SplFileInfo( $path . $file ); |
| 73 | $count += (int) $action->apply_to_path( $fileinfo ); |
| 74 | } |
| 75 | |
| 76 | return $count; |
| 77 | } |
| 78 | |
| 79 | private static function validate_path( $path ) { |
| 80 | $path = realpath( $path ); |
| 81 | if ( ! $path ) { |
| 82 | // translators: %s is the directory that does not exist. |
| 83 | return new Boost_Cache_Error( 'directory-missing', 'Directory does not exist: ' . $path ); // realpath returns false if a file does not exist. |
| 84 | } |
| 85 | |
| 86 | // make sure that $dir is a directory inside WP_CONTENT . '/boost-cache/'; |
| 87 | if ( self::is_boost_cache_directory( $path ) === false ) { |
| 88 | // translators: %s is the directory that is invalid. |
| 89 | return new Boost_Cache_Error( 'invalid-directory', 'Invalid directory %s' . $path ); |
| 90 | } |
| 91 | |
| 92 | if ( ! is_dir( $path ) ) { |
| 93 | return new Boost_Cache_Error( 'not-a-directory', 'Not a directory' ); |
| 94 | } |
| 95 | |
| 96 | return true; |
| 97 | } |
| 98 | |
| 99 | /** |
| 100 | * Returns true if the given directory is inside the boost-cache directory. |
| 101 | * |
| 102 | * @param string $dir - The directory to check. |
| 103 | * @return bool |
| 104 | */ |
| 105 | public static function is_boost_cache_directory( $dir ) { |
| 106 | $dir = Boost_Cache_Utils::sanitize_file_path( $dir ); |
| 107 | return strpos( $dir, WP_CONTENT_DIR . '/boost-cache' ) !== false; |
| 108 | } |
| 109 | |
| 110 | /** |
| 111 | * Given a request_uri and its parameters, return the filename to use for this cached data. Does not include the file path. |
| 112 | * |
| 113 | * @param array $parameters - An associative array of all the things that make this request special/different. Includes GET parameters and COOKIEs normally. |
| 114 | */ |
| 115 | public static function get_request_filename( $parameters ) { |
| 116 | |
| 117 | /** |
| 118 | * Filters the components used to generate the cache key. |
| 119 | * |
| 120 | * @param array $parameters The array of components, url, cookies, get parameters, etc. |
| 121 | * |
| 122 | * @since 1.0.0 |
| 123 | * @deprecated 3.8.0 |
| 124 | */ |
| 125 | $key_components = apply_filters_deprecated( 'boost_cache_key_components', array( $parameters ), '3.8.0', 'jetpack_boost_cache_parameters' ); |
| 126 | |
| 127 | return md5( |
| 128 | json_encode( // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode |
| 129 | $key_components, |
| 130 | 0 // phpcs:ignore Jetpack.Functions.JsonEncodeFlags.ZeroFound -- No `json_encode()` flags because this needs to match whatever is calculating the hash on the other end. |
| 131 | ) |
| 132 | ) . '.html'; |
| 133 | } |
| 134 | |
| 135 | /** |
| 136 | * Check if a file is a rebuild file. |
| 137 | * |
| 138 | * @param string $file - The file to check. |
| 139 | * @return bool - True if the file is a rebuild file, false otherwise. |
| 140 | */ |
| 141 | public static function is_rebuild_file( $file ) { |
| 142 | return substr( $file, -strlen( self::REBUILD_FILE_EXTENSION ) ) === self::REBUILD_FILE_EXTENSION; |
| 143 | } |
| 144 | |
| 145 | /** |
| 146 | * Creates the directory if it doesn't exist. |
| 147 | * |
| 148 | * @param string $path - The path to the directory to create. |
| 149 | */ |
| 150 | public static function create_directory( $path ) { |
| 151 | if ( ! is_dir( $path ) ) { |
| 152 | // phpcs:ignore WordPress.WP.AlternativeFunctions.dir_mkdir_dirname, WordPress.WP.AlternativeFunctions.file_system_operations_mkdir, WordPress.PHP.NoSilencedErrors.Discouraged |
| 153 | $dir_created = @mkdir( $path, 0755, true ); |
| 154 | |
| 155 | if ( $dir_created ) { |
| 156 | self::create_empty_index_files( $path ); |
| 157 | } |
| 158 | |
| 159 | return $dir_created; |
| 160 | } |
| 161 | |
| 162 | return true; |
| 163 | } |
| 164 | |
| 165 | /** |
| 166 | * Create an empty index.html file in the given directory. |
| 167 | * This is done to prevent directory listing. |
| 168 | */ |
| 169 | private static function create_empty_index_files( $path ) { |
| 170 | if ( self::is_boost_cache_directory( $path ) ) { |
| 171 | self::write_to_file( $path . '/index.html', '' ); |
| 172 | |
| 173 | // Create an empty index.html file in the parent directory as well. |
| 174 | self::create_empty_index_files( dirname( $path ) ); |
| 175 | } |
| 176 | } |
| 177 | |
| 178 | /** |
| 179 | * Rebuild a file. Make a copy of the file with a different extension instead of deleting it. |
| 180 | * |
| 181 | * @param string $file_path - The file to rebuild. |
| 182 | * @return bool - True if the file was rebuilt, false otherwise. |
| 183 | */ |
| 184 | public static function rebuild_file( $file_path ) { |
| 185 | // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable |
| 186 | if ( is_writable( $file_path ) ) { |
| 187 | // only rename the file if it is not already a rebuild file. |
| 188 | if ( ! self::is_rebuild_file( $file_path ) ) { |
| 189 | // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename, WordPress.PHP.NoSilencedErrors.Discouraged |
| 190 | @rename( $file_path, $file_path . self::REBUILD_FILE_EXTENSION ); |
| 191 | @touch( $file_path . self::REBUILD_FILE_EXTENSION ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_touch, WordPress.PHP.NoSilencedErrors.Discouraged |
| 192 | return true; |
| 193 | } |
| 194 | } |
| 195 | |
| 196 | return false; |
| 197 | } |
| 198 | |
| 199 | /** |
| 200 | * Restore a file that was rebuilt so the cache file can be used for other visitors. |
| 201 | * |
| 202 | * @param string $file_path - The rebuilt file |
| 203 | * @return bool - True if the file was restored, false otherwise. |
| 204 | */ |
| 205 | public static function restore_file( $file_path ) { |
| 206 | // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable |
| 207 | if ( is_writable( $file_path ) ) { |
| 208 | // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename, WordPress.PHP.NoSilencedErrors.Discouraged |
| 209 | return @rename( $file_path, str_replace( self::REBUILD_FILE_EXTENSION, '', $file_path ) ); |
| 210 | } |
| 211 | |
| 212 | return false; |
| 213 | } |
| 214 | |
| 215 | /** |
| 216 | * Delete a file. |
| 217 | * |
| 218 | * @param string $file_path - The file to delete. |
| 219 | * @return bool - True if the file was deleted, false otherwise. |
| 220 | */ |
| 221 | public static function delete_file( $file_path ) { |
| 222 | // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_is_writable |
| 223 | $deletable = is_writable( $file_path ); |
| 224 | |
| 225 | if ( $deletable ) { |
| 226 | // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink, WordPress.PHP.NoSilencedErrors.Discouraged |
| 227 | return @unlink( $file_path ); |
| 228 | } |
| 229 | |
| 230 | return false; |
| 231 | } |
| 232 | |
| 233 | /** |
| 234 | * Delete an empty cache directory. |
| 235 | * |
| 236 | * @param string $dir - The directory to delete. |
| 237 | * @return int - 1 if the directory was deleted, 0 otherwise. |
| 238 | * |
| 239 | * This function will delete the index.html file and the directory itself. |
| 240 | */ |
| 241 | public static function delete_empty_dir( $dir ) { |
| 242 | if ( self::is_dir_empty( $dir ) ) { |
| 243 | @unlink( $dir . '/index.html' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink, WordPress.PHP.NoSilencedErrors.Discouraged |
| 244 | @rmdir( $dir ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir, WordPress.PHP.NoSilencedErrors.Discouraged |
| 245 | return 1; |
| 246 | } |
| 247 | return 0; |
| 248 | } |
| 249 | |
| 250 | /** |
| 251 | * Check if a directory is empty. |
| 252 | * |
| 253 | * @param string $dir - The directory to check. |
| 254 | */ |
| 255 | public static function is_dir_empty( $dir ) { |
| 256 | if ( ! is_readable( $dir ) ) { |
| 257 | return new Boost_Cache_Error( 'directory_not_readable', 'Directory is not readable' ); |
| 258 | } |
| 259 | |
| 260 | $files = array_diff( scandir( $dir ), array( '.', '..', 'index.html' ) ); |
| 261 | return empty( $files ); |
| 262 | } |
| 263 | |
| 264 | /** |
| 265 | * Writes data to a file. |
| 266 | * This creates a temporary file first, then renames the file to the final filename. |
| 267 | * This is done to prevent the file from being read while it is being written to. |
| 268 | * |
| 269 | * @param string $filename - The filename to write to. |
| 270 | * @param string $data - The data to write to the file. |
| 271 | * @return bool|Boost_Cache_Error - true on sucess or Boost_Cache_Error on failure. |
| 272 | */ |
| 273 | public static function write_to_file( $filename, $data ) { |
| 274 | $tmp_filename = $filename . uniqid( uniqid(), true ) . '.tmp'; |
| 275 | // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents, WordPress.PHP.NoSilencedErrors.Discouraged |
| 276 | if ( false === @file_put_contents( $tmp_filename, $data ) ) { |
| 277 | return new Boost_Cache_Error( 'could-not-write', 'Could not write to tmp file: ' . $tmp_filename ); |
| 278 | } |
| 279 | |
| 280 | // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename |
| 281 | if ( ! rename( $tmp_filename, $filename ) ) { |
| 282 | return new Boost_Cache_Error( 'could-not-rename', 'Could not rename tmp file to final file: ' . $filename ); |
| 283 | } |
| 284 | return true; |
| 285 | } |
| 286 | } |