PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.3.0
Defender Security – Malware Scanner, Login Security & Firewall v6.3.0
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
← All changes | src/traits/io.php +270 -51 3.0.1 → 6.3.0 View file →
@@ -1,37 +1,73 @@
1 1 <?php
2 +/**
3 + * Helper functions for Input Output related tasks.
4 + *
5 + * @package WP_Defender\Traits
6 + */
2 7
3 8 namespace WP_Defender\Traits;
4 9
10 +use RecursiveIteratorIterator;
11 +use RecursiveDirectoryIterator;
12 +use WP_Defender\Helper\File as File_Helper;
5 13 use WP_Defender\Component\Logger\Rotation_Logger as Logger;
14 +use WP_Filesystem_Base;
6 15
7 16 trait IO {
17 +
8 18 /**
9 19 * A simple function to create & return the folder that we can use to write tmp files.
10 20 *
21 + * @param bool $main_site_path If true then return main site's upload dir path for a multisite.
22 + *
23 + * @since 4.1.0 The `$main_site_path` parameter was added.
11 24 * @return string
12 25 */
13 - protected function get_tmp_path() {
26 + protected function get_tmp_path( bool $main_site_path = false ): string {
27 + global $wp_filesystem;
28 + // Initialize the WP filesystem, no more using 'file-put-contents' function.
29 + if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
30 + require_once ABSPATH . '/wp-admin/includes/file.php';
31 + WP_Filesystem();
32 + }
33 + $is_switch_to_main_site = $main_site_path && is_multisite() && ! is_main_site();
34 + if ( $is_switch_to_main_site ) {
35 + // Switch to the main site.
36 + switch_to_blog( get_main_site_id() );
37 + }
38 +
14 39 $upload_dir = wp_upload_dir()['basedir'];
15 - $tmp_dir = $upload_dir . DIRECTORY_SEPARATOR . 'wp-defender';
40 +
41 + if ( $is_switch_to_main_site ) {
42 + // Restore the current site if switched.
43 + restore_current_blog();
44 + }
45 +
46 + $tmp_dir = $upload_dir . DIRECTORY_SEPARATOR . 'wp-defender';
16 47 if ( ! is_dir( $tmp_dir ) ) {
17 48 wp_mkdir_p( $tmp_dir );
18 49 }
19 50
20 51 if ( ! is_file( $tmp_dir . DIRECTORY_SEPARATOR . 'index.php' ) ) {
21 - file_put_contents( $tmp_dir . DIRECTORY_SEPARATOR . 'index.php', '' );
52 + $wp_filesystem->put_contents( $tmp_dir . DIRECTORY_SEPARATOR . 'index.php', '' );
22 53 }
23 54
55 + $file_helper = wd_di()->get( File_Helper::class );
56 + $file_helper->maybe_dir_access_deny( $tmp_dir );
57 +
24 58 return $tmp_dir;
25 59 }
26 60
27 61 /**
28 - * @param $category
62 + * Returns the path to the log file for a given category.
29 63 *
30 - * @return string
64 + * @param string $category The category of the log file. Defaults to an empty string.
65 + *
66 + * @return string The path to the log file.
31 67 */
32 - public function get_log_path( $category = '' ) {
33 - $file = empty( $category ) ? 'defender.log' : $category;
68 + public function get_log_path( $category = '' ): string {
69 + $file = '' === $category ? wd_internal_log() : $category;
34 70
35 71 $logger = new Logger();
36 72 $file_name = $logger->generate_file_name( $file );
37 73
@@ -38,94 +74,92 @@
38 74 return $this->get_tmp_path() . DIRECTORY_SEPARATOR . $file_name;
39 75 }
40 76
41 77 /**
42 - * Create a lock, this will be use in scanning.
78 + * Create a lock. This will be used for 2FA.
43 79 *
44 80 * @return string
45 81 */
46 - protected function get_lock_path() {
47 - return $this->get_tmp_path() . DIRECTORY_SEPARATOR . 'scan.lock';
82 + protected function get_2fa_lock_path(): string {
83 + return $this->get_tmp_path() . DIRECTORY_SEPARATOR . 'two-fa.lock';
48 84 }
49 85
50 86 /**
51 87 * Delete a folder with every content inside.
52 88 *
53 - * @param $dir
89 + * @param string $dir The path to the folder.
90 + *
91 + * @return bool
54 92 */
55 - public function delete_dir( $dir ) {
93 + public function delete_dir( $dir ): bool {
94 + global $wp_filesystem;
95 + // Initialize the WP filesystem, no more using 'file-put-contents' function.
96 + if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
97 + require_once ABSPATH . '/wp-admin/includes/file.php';
98 + WP_Filesystem();
99 + }
56 100 if ( ! is_dir( $dir ) ) {
57 - return;
101 + return false;
58 102 }
59 - $it = new \RecursiveDirectoryIterator( $dir, \RecursiveDirectoryIterator::SKIP_DOTS );
60 - $files = new \RecursiveIteratorIterator(
103 + $it = new RecursiveDirectoryIterator( $dir, RecursiveDirectoryIterator::SKIP_DOTS );
104 + $files = new RecursiveIteratorIterator(
61 105 $it,
62 - \RecursiveIteratorIterator::CHILD_FIRST
106 + RecursiveIteratorIterator::CHILD_FIRST
63 107 );
108 + $ret = true;
64 109 foreach ( $files as $file ) {
65 110 if ( $file->isDir() ) {
66 - $ret = rmdir( $file->getPathname() );
111 + $ret = $wp_filesystem->rmdir( $file->getPathname(), true );
67 112 } else {
68 - $ret = unlink( $file->getPathname() );
113 + $wp_filesystem->delete( $file->getPathname() );
69 114 }
70 115 if ( false === $ret ) {
71 116 return false;
72 117 }
73 118 }
74 - rmdir( $dir );
75 119
76 - return true;
120 + return $wp_filesystem->rmdir( $dir, true );
77 121 }
78 122
79 123 /**
80 - * @return string|\WP_Error
81 - */
82 - public function get_tmp_folder() {
83 - $tmp_dir = $this->get_tmp_path() . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR;
84 - if ( ! is_dir( $tmp_dir ) ) {
85 - wp_mkdir_p( $tmp_dir );
86 - if ( ! wp_mkdir_p( $tmp_dir ) ) {
87 - return new \WP_Error( 'defender_dir', sprintf( __( 'Unable to create the tmp directory %s', 'wpdef' ), $tmp_dir ) );
88 - }
89 - }
90 - return $tmp_dir;
91 - }
92 -
93 - /**
94 124 * Not remove double quotes inside str_replace().
95 - * @param string $file_path
96 125 *
97 - * @return string
126 + * @param string $data The string or array being searched and replaced on.
127 + *
128 + * @return array|string
98 129 */
99 - protected function convert_end_lines_dos_to_linux( $file_path ) {
100 - return str_replace( array( "\r\n", "\r" ), "\n", file_get_contents( $file_path ) );
130 + protected function convert_end_lines_dos_to_linux( $data ) {
131 + return str_replace( array( "\r\n", "\r" ), "\n", $data );
101 132 }
102 133
103 134 /**
104 135 * Not remove double quotes inside str_replace().
105 - * @param string $file_path
106 136 *
107 - * @return string
137 + * @param string $data The string or array being searched and replaced on.
138 + *
139 + * @return array|string
108 140 */
109 - protected function convert_end_lines_linux_to_dos( $file_path ) {
110 - return str_replace( "\n", "\r\n", $this->convert_end_lines_dos_to_linux( $file_path ) );
141 + protected function convert_end_lines_linux_to_dos( $data ) {
142 + return str_replace( "\n", "\r\n", $this->convert_end_lines_dos_to_linux( $data ) );
111 143 }
112 144
113 145 /**
114 146 * Compare hashes on different OS.
115 - * @param string $file_path
116 - * @param string|array $file_hash
117 147 *
148 + * @param string $file_path The filename.
149 + * @param string|array $file_hash The user-supplied string to compare against.
150 + *
118 151 * @return bool
119 152 */
120 153 protected function compare_hashes_on_different_os( $file_path, $file_hash ) {
121 - 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 ) ) {
122 156 return true;
123 157 }
124 - if ( hash_equals( md5( $this->convert_end_lines_dos_to_linux( $file_path ) ), $file_hash ) ) {
158 + if ( hash_equals( $this->hash_file( $file_path, 'linux' ), $file_hash ) ) {
125 159 return true;
126 160 }
127 - if ( hash_equals( md5( $this->convert_end_lines_linux_to_dos( $file_path ) ), $file_hash ) ) {
161 + if ( hash_equals( $this->hash_file( $file_path, 'dos' ), $file_hash ) ) {
128 162 return true;
129 163 }
130 164
131 165 return false;
@@ -131,21 +165,22 @@
131 165 return false;
132 166 }
133 167
134 168 /**
135 - * @param string $file_path Path to file.
136 - * @param string|array $file_hash Hash or some hashes of file2, e.g. for readme.txt.
169 + * Compare hashes.
137 170 *
171 + * @param string $file_path Path to file.
172 + * @param string|array $file_hash Hash or some hashes of file2, e.g. for readme.txt.
173 + *
138 174 * @return bool
139 175 */
140 176 public function compare_hashes( $file_path, $file_hash ) {
141 177 if ( is_string( $file_hash ) ) {
142 -
143 178 return $this->compare_hashes_on_different_os( $file_path, $file_hash );
144 179 } elseif ( is_array( $file_hash ) ) {
145 180 // Sometimes file has some hashes.
146 181 foreach ( $file_hash as $hash_value ) {
147 - 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 ) ) {
148 183 return true;
149 184 }
150 185 }
151 186
@@ -150,9 +185,193 @@
150 185 }
151 186
152 187 return false;
153 188 } else {
189 + return false;
190 + }
191 + }
154 192
193 + /**
194 + * Hash a file in chunks.
195 + *
196 + * @param string $file_path Path to a file.
197 + * @param string $convert_to Convert end of lines characters to linux or dos.
198 + *
199 + * @return bool|string
200 + * @since 3.10.0
201 + */
202 + protected function hash_file( string $file_path, string $convert_to = '' ) {
203 + global $wp_filesystem;
204 + // Initialize the WP filesystem, no more using 'file-put-contents' function.
205 + if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
206 + require_once ABSPATH . '/wp-admin/includes/file.php';
207 + WP_Filesystem();
208 + }
209 + if ( ! file_exists( $file_path ) ) {
155 210 return false;
211 + }
212 +
213 + $context = hash_init( 'md5' );
214 + $data = $wp_filesystem->get_contents( $file_path );
215 +
216 + if ( 'linux' === $convert_to ) {
217 + $data = $this->convert_end_lines_dos_to_linux( $data );
218 + } elseif ( 'dos' === $convert_to ) {
219 + $data = $this->convert_end_lines_linux_to_dos( $data );
220 + }
221 +
222 + hash_update( $context, $data );
223 +
224 + return hash_final( $context, false );
225 + }
226 +
227 + /**
228 + * Retrieves the lock file path used in scanning.
229 + *
230 + * @param string $lock_filename The lock file name.
231 + *
232 + * @return string The lock file path.
233 + *
234 + * @throws \RuntimeException If the lock file name is not defined.
235 + */
236 + protected function get_lock_path( string $lock_filename ): string {
237 + if ( '' === $lock_filename ) {
238 + throw new \RuntimeException( 'Lock file name must be defined in the class using IO trait.' );
239 + }
240 +
241 + return $this->get_tmp_path() . DIRECTORY_SEPARATOR . $lock_filename;
242 + }
243 +
244 + /**
245 + * Delete file lock.
246 + *
247 + * @param string $lock_filename The lock file name.
248 + */
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 );
253 + }
254 + }
255 +
256 + /**
257 + * Atomically acquire a file lock.
258 + *
259 + * @param string $lock_filename The lock filename.
260 + *
261 + * @return bool True when the lock is acquired.
262 + */
263 + public function try_create_lock( string $lock_filename ): bool {
264 + global $wp_filesystem;
265 +
266 + if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
267 + require_once ABSPATH . '/wp-admin/includes/file.php';
268 + WP_Filesystem();
269 + }
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 );
283 + }
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.
289 + return false;
290 + }
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 +
295 + return true;
296 + }
297 +
298 + /**
299 + * Acquire a lock for a given cron event.
300 + *
301 + * @param string $event Unique cron event name.
302 + * @param string $ttl Cron schedule name to hold the lock. Default is 'every_minute'.
303 + * @return bool True if lock is acquired, False if another process is running.
304 + */
305 + public function acquire_cron_lock( string $event, string $ttl = 'every_minute' ): bool {
306 + $lock_key = "{$event}_lock";
307 + $last_run_key = "{$event}_last_run";
308 + $now = time();
309 + $lock_duration = 59; // Default lock duration.
310 +
311 + // Set lock duration based on cron schedule.
312 + switch ( $ttl ) {
313 + case 'twicedaily':
314 + $lock_duration = ( DAY_IN_SECONDS / 2 ) - 1;
315 + break;
316 + case 'hourly':
317 + $lock_duration = HOUR_IN_SECONDS - 1;
318 + break;
319 + case 'daily':
320 + $lock_duration = DAY_IN_SECONDS - 1;
321 + break;
322 + default:
323 + break;
324 + }
325 +
326 + // Prevent execution if it has already run within lock duration.
327 + $last_run = get_site_option( $last_run_key, 0 );
328 + if ( $now < $last_run + $lock_duration ) {
329 + return false;
330 + }
331 +
332 + // Check if another process is running.
333 + $lock_time = (int) get_site_option( $lock_key, 0 );
334 + if ( $lock_time > 0 && ( $now < $lock_time + $lock_duration ) ) {
335 + return false;
336 + }
337 +
338 + // Acquire the lock.
339 + if ( ! update_site_option( $lock_key, $now ) ) {
340 + return false;
341 + }
342 + return true;
343 + }
344 +
345 + /**
346 + * Release the lock and update the last run time.
347 + *
348 + * @param string $event Unique cron event name.
349 + */
350 + public function release_cron_lock( string $event ): void {
351 + $lock_key = "{$event}_lock";
352 + $last_run_key = "{$event}_last_run";
353 + update_site_option( $last_run_key, time() );
354 + delete_site_option( $lock_key );
355 + }
356 +
357 + /**
358 + * Detect the line ending style used in a given text.
359 + *
360 + * @param string $text The text to analyze.
361 + *
362 + * @return string The detected line ending style: "\r\n" for Windows, "\n" for Unix, or "\r" for Classic Mac.
363 + */
364 + public function detect_line_ending( string $text ): string {
365 + $count_crlf = substr_count( $text, "\r\n" );
366 + $count_lf = substr_count( $text, "\n" ) - $count_crlf;
367 + $count_cr = substr_count( $text, "\r" ) - $count_crlf;
368 +
369 + if ( $count_crlf >= $count_lf && $count_crlf >= $count_cr ) {
370 + return "\r\n"; // Windows-style.
371 + } elseif ( $count_lf >= $count_cr ) {
372 + return "\n"; // Unix-style.
373 + } else {
374 + return "\r"; // Classic Mac (rare).
156 375 }
157 376 }
158 377 }