PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.2.0
Defender Security – Malware Scanner, Login Security & Firewall v6.2.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
defender-security / src / traits / io.php

io.php in Defender Security – Malware Scanner, Login Security & Firewall 6.2.0, at src/traits/io.php

378 lines 10.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Helper functions for Input Output related tasks.
4 *
5 * @package WP_Defender\Traits
6 */
7
8 namespace WP_Defender\Traits;
9
10 use RecursiveIteratorIterator;
11 use RecursiveDirectoryIterator;
12 use WP_Defender\Helper\File as File_Helper;
13 use WP_Defender\Component\Logger\Rotation_Logger as Logger;
14 use WP_Filesystem_Base;
15
16 trait IO {
17
18 /**
19 * A simple function to create & return the folder that we can use to write tmp files.
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.
24 * @return string
25 */
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
39 $upload_dir = wp_upload_dir()['basedir'];
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';
47 if ( ! is_dir( $tmp_dir ) ) {
48 wp_mkdir_p( $tmp_dir );
49 }
50
51 if ( ! is_file( $tmp_dir . DIRECTORY_SEPARATOR . 'index.php' ) ) {
52 $wp_filesystem->put_contents( $tmp_dir . DIRECTORY_SEPARATOR . 'index.php', '' );
53 }
54
55 $file_helper = wd_di()->get( File_Helper::class );
56 $file_helper->maybe_dir_access_deny( $tmp_dir );
57
58 return $tmp_dir;
59 }
60
61 /**
62 * Returns the path to the log file for a given category.
63 *
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.
67 */
68 public function get_log_path( $category = '' ): string {
69 $file = '' === $category ? wd_internal_log() : $category;
70
71 $logger = new Logger();
72 $file_name = $logger->generate_file_name( $file );
73
74 return $this->get_tmp_path() . DIRECTORY_SEPARATOR . $file_name;
75 }
76
77 /**
78 * Create a lock. This will be used for 2FA.
79 *
80 * @return string
81 */
82 protected function get_2fa_lock_path(): string {
83 return $this->get_tmp_path() . DIRECTORY_SEPARATOR . 'two-fa.lock';
84 }
85
86 /**
87 * Delete a folder with every content inside.
88 *
89 * @param string $dir The path to the folder.
90 *
91 * @return bool
92 */
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 }
100 if ( ! is_dir( $dir ) ) {
101 return false;
102 }
103 $it = new RecursiveDirectoryIterator( $dir, RecursiveDirectoryIterator::SKIP_DOTS );
104 $files = new RecursiveIteratorIterator(
105 $it,
106 RecursiveIteratorIterator::CHILD_FIRST
107 );
108 $ret = true;
109 foreach ( $files as $file ) {
110 if ( $file->isDir() ) {
111 $ret = $wp_filesystem->rmdir( $file->getPathname(), true );
112 } else {
113 $wp_filesystem->delete( $file->getPathname() );
114 }
115 if ( false === $ret ) {
116 return false;
117 }
118 }
119
120 return $wp_filesystem->rmdir( $dir, true );
121 }
122
123 /**
124 * Not remove double quotes inside str_replace().
125 *
126 * @param string $data The string or array being searched and replaced on.
127 *
128 * @return array|string
129 */
130 protected function convert_end_lines_dos_to_linux( $data ) {
131 return str_replace( array( "\r\n", "\r" ), "\n", $data );
132 }
133
134 /**
135 * Not remove double quotes inside str_replace().
136 *
137 * @param string $data The string or array being searched and replaced on.
138 *
139 * @return array|string
140 */
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 ) );
143 }
144
145 /**
146 * Compare hashes on different OS.
147 *
148 * @param string $file_path The filename.
149 * @param string|array $file_hash The user-supplied string to compare against.
150 *
151 * @return bool
152 */
153 protected function compare_hashes_on_different_os( $file_path, $file_hash ) {
154 $hashed_file = md5_file( $file_path );
155 if ( false !== $hashed_file && hash_equals( $hashed_file, $file_hash ) ) {
156 return true;
157 }
158 if ( hash_equals( $this->hash_file( $file_path, 'linux' ), $file_hash ) ) {
159 return true;
160 }
161 if ( hash_equals( $this->hash_file( $file_path, 'dos' ), $file_hash ) ) {
162 return true;
163 }
164
165 return false;
166 }
167
168 /**
169 * Compare hashes.
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 *
174 * @return bool
175 */
176 public function compare_hashes( $file_path, $file_hash ) {
177 if ( is_string( $file_hash ) ) {
178 return $this->compare_hashes_on_different_os( $file_path, $file_hash );
179 } elseif ( is_array( $file_hash ) ) {
180 // Sometimes file has some hashes.
181 foreach ( $file_hash as $hash_value ) {
182 if ( is_string( $hash_value ) && $this->compare_hashes_on_different_os( $file_path, $hash_value ) ) {
183 return true;
184 }
185 }
186
187 return false;
188 } else {
189 return false;
190 }
191 }
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 ) ) {
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).
375 }
376 }
377 }
378