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

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