| @@ -10,8 +10,9 @@ | ||
| 10 | 10 | use RecursiveIteratorIterator; |
| 11 | 11 | use RecursiveDirectoryIterator; |
| 12 | 12 | use WP_Defender\Helper\File as File_Helper; |
| 13 | 13 | use WP_Defender\Component\Logger\Rotation_Logger as Logger; |
| 14 | +use WP_Filesystem_Base; | |
| 14 | 15 | |
| 15 | 16 | trait IO { |
| 16 | 17 | |
| 17 | 18 | /** |
| @@ -24,9 +25,9 @@ | ||
| 24 | 25 | */ |
| 25 | 26 | protected function get_tmp_path( bool $main_site_path = false ): string { |
| 26 | 27 | global $wp_filesystem; |
| 27 | 28 | // Initialize the WP filesystem, no more using 'file-put-contents' function. |
| 28 | - if ( empty( $wp_filesystem ) ) { | |
| 29 | + if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) { | |
| 29 | 30 | require_once ABSPATH . '/wp-admin/includes/file.php'; |
| 30 | 31 | WP_Filesystem(); |
| 31 | 32 | } |
| 32 | 33 | $is_switch_to_main_site = $main_site_path && is_multisite() && ! is_main_site(); |
| @@ -64,9 +65,9 @@ | ||
| 64 | 65 | * |
| 65 | 66 | * @return string The path to the log file. |
| 66 | 67 | */ |
| 67 | 68 | public function get_log_path( $category = '' ): string { |
| 68 | - $file = empty( $category ) ? wd_internal_log() : $category; | |
| 69 | + $file = '' === $category ? wd_internal_log() : $category; | |
| 69 | 70 | |
| 70 | 71 | $logger = new Logger(); |
| 71 | 72 | $file_name = $logger->generate_file_name( $file ); |
| 72 | 73 | |
| @@ -91,9 +92,9 @@ | ||
| 91 | 92 | */ |
| 92 | 93 | public function delete_dir( $dir ): bool { |
| 93 | 94 | global $wp_filesystem; |
| 94 | 95 | // Initialize the WP filesystem, no more using 'file-put-contents' function. |
| 95 | - if ( empty( $wp_filesystem ) ) { | |
| 96 | + if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) { | |
| 96 | 97 | require_once ABSPATH . '/wp-admin/includes/file.php'; |
| 97 | 98 | WP_Filesystem(); |
| 98 | 99 | } |
| 99 | 100 | if ( ! is_dir( $dir ) ) { |
| @@ -149,9 +150,10 @@ | ||
| 149 | 150 | * |
| 150 | 151 | * @return bool |
| 151 | 152 | */ |
| 152 | 153 | protected function compare_hashes_on_different_os( $file_path, $file_hash ) { |
| 153 | - if ( hash_equals( md5_file( $file_path ), $file_hash ) ) { | |
| 154 | + $hashed_file = md5_file( $file_path ); | |
| 155 | + if ( false !== $hashed_file && hash_equals( $hashed_file, $file_hash ) ) { | |
| 154 | 156 | return true; |
| 155 | 157 | } |
| 156 | 158 | if ( hash_equals( $this->hash_file( $file_path, 'linux' ), $file_hash ) ) { |
| 157 | 159 | return true; |
| @@ -176,9 +178,9 @@ | ||
| 176 | 178 | return $this->compare_hashes_on_different_os( $file_path, $file_hash ); |
| 177 | 179 | } elseif ( is_array( $file_hash ) ) { |
| 178 | 180 | // Sometimes file has some hashes. |
| 179 | 181 | foreach ( $file_hash as $hash_value ) { |
| 180 | - if ( $this->compare_hashes_on_different_os( $file_path, $hash_value ) ) { | |
| 182 | + if ( is_string( $hash_value ) && $this->compare_hashes_on_different_os( $file_path, $hash_value ) ) { | |
| 181 | 183 | return true; |
| 182 | 184 | } |
| 183 | 185 | } |
| 184 | 186 | |
| @@ -199,9 +201,9 @@ | ||
| 199 | 201 | */ |
| 200 | 202 | protected function hash_file( string $file_path, string $convert_to = '' ) { |
| 201 | 203 | global $wp_filesystem; |
| 202 | 204 | // Initialize the WP filesystem, no more using 'file-put-contents' function. |
| 203 | - if ( empty( $wp_filesystem ) ) { | |
| 205 | + if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) { | |
| 204 | 206 | require_once ABSPATH . '/wp-admin/includes/file.php'; |
| 205 | 207 | WP_Filesystem(); |
| 206 | 208 | } |
| 207 | 209 | if ( ! file_exists( $file_path ) ) { |
| @@ -224,58 +226,73 @@ | ||
| 224 | 226 | |
| 225 | 227 | /** |
| 226 | 228 | * Retrieves the lock file path used in scanning. |
| 227 | 229 | * |
| 230 | + * @param string $lock_filename The lock file name. | |
| 231 | + * | |
| 228 | 232 | * @return string The lock file path. |
| 229 | 233 | * |
| 230 | 234 | * @throws \RuntimeException If the lock file name is not defined. |
| 231 | 235 | */ |
| 232 | - protected function get_lock_path(): string { | |
| 233 | - if ( empty( $this->lock_filename ) ) { | |
| 236 | + protected function get_lock_path( string $lock_filename ): string { | |
| 237 | + if ( '' === $lock_filename ) { | |
| 234 | 238 | throw new \RuntimeException( 'Lock file name must be defined in the class using IO trait.' ); |
| 235 | 239 | } |
| 236 | 240 | |
| 237 | - return $this->get_tmp_path() . DIRECTORY_SEPARATOR . $this->lock_filename; | |
| 241 | + return $this->get_tmp_path() . DIRECTORY_SEPARATOR . $lock_filename; | |
| 238 | 242 | } |
| 239 | 243 | |
| 240 | 244 | /** |
| 241 | - * Create a file lock, so we can check if a process already running. | |
| 242 | - */ | |
| 243 | - public function create_lock() { | |
| 244 | - $this->remove_lock(); | |
| 245 | - file_put_contents( $this->get_lock_path(), time(), LOCK_EX ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents | |
| 246 | - } | |
| 247 | - | |
| 248 | - /** | |
| 249 | 245 | * Delete file lock. |
| 246 | + * | |
| 247 | + * @param string $lock_filename The lock file name. | |
| 250 | 248 | */ |
| 251 | - public function remove_lock() { | |
| 252 | - if ( file_exists( $this->get_lock_path() ) ) { | |
| 253 | - wp_delete_file( $this->get_lock_path() ); | |
| 249 | + public function remove_lock( string $lock_filename ) { | |
| 250 | + $lock_path = $this->get_lock_path( $lock_filename ); | |
| 251 | + if ( file_exists( $lock_path ) ) { | |
| 252 | + wp_delete_file( $lock_path ); | |
| 254 | 253 | } |
| 255 | 254 | } |
| 256 | 255 | |
| 257 | 256 | /** |
| 258 | - * Check if a lock is valid. | |
| 257 | + * Atomically acquire a file lock. | |
| 259 | 258 | * |
| 260 | - * @return bool | |
| 259 | + * @param string $lock_filename The lock filename. | |
| 260 | + * | |
| 261 | + * @return bool True when the lock is acquired. | |
| 261 | 262 | */ |
| 262 | - public function has_lock(): bool { | |
| 263 | + public function try_create_lock( string $lock_filename ): bool { | |
| 263 | 264 | global $wp_filesystem; |
| 264 | - // Initialize the WP filesystem, no more using 'file-put-contents' function. | |
| 265 | - if ( empty( $wp_filesystem ) ) { | |
| 265 | + | |
| 266 | + if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) { | |
| 266 | 267 | require_once ABSPATH . '/wp-admin/includes/file.php'; |
| 267 | 268 | WP_Filesystem(); |
| 268 | 269 | } |
| 269 | - if ( ! file_exists( $this->get_lock_path() ) ) { | |
| 270 | - return false; | |
| 270 | + | |
| 271 | + $lock_path = $this->get_lock_path( $lock_filename ); | |
| 272 | + | |
| 273 | + if ( is_file( $lock_path ) ) { | |
| 274 | + $time = (int) $wp_filesystem->get_contents( $lock_path ); | |
| 275 | + | |
| 276 | + // Existing lock is still valid. | |
| 277 | + if ( ( $time + 90 ) >= time() ) { | |
| 278 | + return false; | |
| 279 | + } | |
| 280 | + | |
| 281 | + // Remove stale lock. | |
| 282 | + wp_delete_file( $lock_path ); | |
| 271 | 283 | } |
| 272 | - $time = $wp_filesystem->get_contents( $this->get_lock_path() ); | |
| 273 | - if ( strtotime( '+90 seconds', $time ) < time() ) { | |
| 274 | - // Usually a timeout window is 30 seconds, so we should allow lock at 1.30min for safe. | |
| 284 | + | |
| 285 | + $handle = @fopen( $lock_path, 'x' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen, WordPress.PHP.NoSilencedErrors.Discouraged | |
| 286 | + | |
| 287 | + if ( false === $handle ) { | |
| 288 | + // Another process acquired the lock. | |
| 275 | 289 | return false; |
| 276 | 290 | } |
| 277 | 291 | |
| 292 | + fwrite( $handle, (string) time() ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite | |
| 293 | + fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose | |
| 294 | + | |
| 278 | 295 | return true; |
| 279 | 296 | } |
| 280 | 297 | |
| 281 | 298 | /** |
| @@ -312,10 +329,10 @@ | ||
| 312 | 329 | return false; |
| 313 | 330 | } |
| 314 | 331 | |
| 315 | 332 | // Check if another process is running. |
| 316 | - $lock_time = get_site_option( $lock_key, 0 ); | |
| 317 | - if ( $lock_time && ( $now < $lock_time + $lock_duration ) ) { | |
| 333 | + $lock_time = (int) get_site_option( $lock_key, 0 ); | |
| 334 | + if ( $lock_time > 0 && ( $now < $lock_time + $lock_duration ) ) { | |
| 318 | 335 | return false; |
| 319 | 336 | } |
| 320 | 337 | |
| 321 | 338 | // Acquire the lock. |