PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.8.1
Defender Security – Malware Scanner, Login Security & Firewall v5.8.1
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 5.8.1, at src/traits/io.php

373 lines 10.2 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 if ( hash_equals( md5_file( $file_path ), $file_hash ) ) {
155 return true;
156 }
157 if ( hash_equals( $this->hash_file( $file_path, 'linux' ), $file_hash ) ) {
158 return true;
159 }
160 if ( hash_equals( $this->hash_file( $file_path, 'dos' ), $file_hash ) ) {
161 return true;
162 }
163
164 return false;
165 }
166
167 /**
168 * Compare hashes.
169 *
170 * @param string $file_path Path to file.
171 * @param string|array $file_hash Hash or some hashes of file2, e.g. for readme.txt.
172 *
173 * @return bool
174 */
175 public function compare_hashes( $file_path, $file_hash ) {
176 if ( is_string( $file_hash ) ) {
177 return $this->compare_hashes_on_different_os( $file_path, $file_hash );
178 } elseif ( is_array( $file_hash ) ) {
179 // Sometimes file has some hashes.
180 foreach ( $file_hash as $hash_value ) {
181 if ( $this->compare_hashes_on_different_os( $file_path, $hash_value ) ) {
182 return true;
183 }
184 }
185
186 return false;
187 } else {
188 return false;
189 }
190 }
191
192 /**
193 * Hash a file in chunks.
194 *
195 * @param string $file_path Path to a file.
196 * @param string $convert_to Convert end of lines characters to linux or dos.
197 *
198 * @return bool|string
199 * @since 3.10.0
200 */
201 protected function hash_file( string $file_path, string $convert_to = '' ) {
202 global $wp_filesystem;
203 // Initialize the WP filesystem, no more using 'file-put-contents' function.
204 if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
205 require_once ABSPATH . '/wp-admin/includes/file.php';
206 WP_Filesystem();
207 }
208 if ( ! file_exists( $file_path ) ) {
209 return false;
210 }
211
212 $context = hash_init( 'md5' );
213 $data = $wp_filesystem->get_contents( $file_path );
214
215 if ( 'linux' === $convert_to ) {
216 $data = $this->convert_end_lines_dos_to_linux( $data );
217 } elseif ( 'dos' === $convert_to ) {
218 $data = $this->convert_end_lines_linux_to_dos( $data );
219 }
220
221 hash_update( $context, $data );
222
223 return hash_final( $context, false );
224 }
225
226 /**
227 * Retrieves the lock file path used in scanning.
228 *
229 * @param string $lock_filename The lock file name.
230 *
231 * @return string The lock file path.
232 *
233 * @throws \RuntimeException If the lock file name is not defined.
234 */
235 protected function get_lock_path( string $lock_filename ): string {
236 if ( '' === $lock_filename ) {
237 throw new \RuntimeException( 'Lock file name must be defined in the class using IO trait.' );
238 }
239
240 return $this->get_tmp_path() . DIRECTORY_SEPARATOR . $lock_filename;
241 }
242
243 /**
244 * Create a file lock, so we can check if a process already running.
245 *
246 * @param string $lock_filename The lock file name.
247 */
248 public function create_lock( string $lock_filename ) {
249 $this->remove_lock( $lock_filename );
250 file_put_contents( $this->get_lock_path( $lock_filename ), time(), LOCK_EX ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
251 }
252
253 /**
254 * Delete file lock.
255 *
256 * @param string $lock_filename The lock file name.
257 */
258 public function remove_lock( string $lock_filename ) {
259 $lock_path = $this->get_lock_path( $lock_filename );
260 if ( file_exists( $lock_path ) ) {
261 wp_delete_file( $lock_path );
262 }
263 }
264
265 /**
266 * Check if a lock is valid.
267 *
268 * @param string $lock_filename The lock file name.
269 *
270 * @return bool
271 */
272 public function has_lock( string $lock_filename ): bool {
273 global $wp_filesystem;
274 // Initialize the WP filesystem, no more using 'file-put-contents' function.
275 if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
276 require_once ABSPATH . '/wp-admin/includes/file.php';
277 WP_Filesystem();
278 }
279
280 $lock_path = $this->get_lock_path( $lock_filename );
281 if ( ! file_exists( $lock_path ) ) {
282 return false;
283 }
284 $time = $wp_filesystem->get_contents( $lock_path );
285 if ( strtotime( '+90 seconds', $time ) < time() ) {
286 // Usually a timeout window is 30 seconds, so we should allow lock at 1.30min for safe.
287 return false;
288 }
289
290 return true;
291 }
292
293 /**
294 * Acquire a lock for a given cron event.
295 *
296 * @param string $event Unique cron event name.
297 * @param string $ttl Cron schedule name to hold the lock. Default is 'every_minute'.
298 * @return bool True if lock is acquired, False if another process is running.
299 */
300 public function acquire_cron_lock( string $event, string $ttl = 'every_minute' ): bool {
301 $lock_key = "{$event}_lock";
302 $last_run_key = "{$event}_last_run";
303 $now = time();
304 $lock_duration = 59; // Default lock duration.
305
306 // Set lock duration based on cron schedule.
307 switch ( $ttl ) {
308 case 'twicedaily':
309 $lock_duration = ( DAY_IN_SECONDS / 2 ) - 1;
310 break;
311 case 'hourly':
312 $lock_duration = HOUR_IN_SECONDS - 1;
313 break;
314 case 'daily':
315 $lock_duration = DAY_IN_SECONDS - 1;
316 break;
317 default:
318 break;
319 }
320
321 // Prevent execution if it has already run within lock duration.
322 $last_run = get_site_option( $last_run_key, 0 );
323 if ( $now < $last_run + $lock_duration ) {
324 return false;
325 }
326
327 // Check if another process is running.
328 $lock_time = (int) get_site_option( $lock_key, 0 );
329 if ( $lock_time > 0 && ( $now < $lock_time + $lock_duration ) ) {
330 return false;
331 }
332
333 // Acquire the lock.
334 if ( ! update_site_option( $lock_key, $now ) ) {
335 return false;
336 }
337 return true;
338 }
339
340 /**
341 * Release the lock and update the last run time.
342 *
343 * @param string $event Unique cron event name.
344 */
345 public function release_cron_lock( string $event ): void {
346 $lock_key = "{$event}_lock";
347 $last_run_key = "{$event}_last_run";
348 update_site_option( $last_run_key, time() );
349 delete_site_option( $lock_key );
350 }
351
352 /**
353 * Detect the line ending style used in a given text.
354 *
355 * @param string $text The text to analyze.
356 *
357 * @return string The detected line ending style: "\r\n" for Windows, "\n" for Unix, or "\r" for Classic Mac.
358 */
359 public function detect_line_ending( string $text ): string {
360 $count_crlf = substr_count( $text, "\r\n" );
361 $count_lf = substr_count( $text, "\n" ) - $count_crlf;
362 $count_cr = substr_count( $text, "\r" ) - $count_crlf;
363
364 if ( $count_crlf >= $count_lf && $count_crlf >= $count_cr ) {
365 return "\r\n"; // Windows-style.
366 } elseif ( $count_lf >= $count_cr ) {
367 return "\n"; // Unix-style.
368 } else {
369 return "\r"; // Classic Mac (rare).
370 }
371 }
372 }
373