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

374 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 * Create a file lock, so we can check if a process already running.
246 *
247 * @param string $lock_filename The lock file name.
248 */
249 public function create_lock( string $lock_filename ) {
250 $this->remove_lock( $lock_filename );
251 file_put_contents( $this->get_lock_path( $lock_filename ), time(), LOCK_EX ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
252 }
253
254 /**
255 * Delete file lock.
256 *
257 * @param string $lock_filename The lock file name.
258 */
259 public function remove_lock( string $lock_filename ) {
260 $lock_path = $this->get_lock_path( $lock_filename );
261 if ( file_exists( $lock_path ) ) {
262 wp_delete_file( $lock_path );
263 }
264 }
265
266 /**
267 * Check if a lock is valid.
268 *
269 * @param string $lock_filename The lock file name.
270 *
271 * @return bool
272 */
273 public function has_lock( string $lock_filename ): bool {
274 global $wp_filesystem;
275 // Initialize the WP filesystem, no more using 'file-put-contents' function.
276 if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) {
277 require_once ABSPATH . '/wp-admin/includes/file.php';
278 WP_Filesystem();
279 }
280
281 $lock_path = $this->get_lock_path( $lock_filename );
282 if ( ! file_exists( $lock_path ) ) {
283 return false;
284 }
285 $time = $wp_filesystem->get_contents( $lock_path );
286 if ( strtotime( '+90 seconds', $time ) < time() ) {
287 // Usually a timeout window is 30 seconds, so we should allow lock at 1.30min for safe.
288 return false;
289 }
290
291 return true;
292 }
293
294 /**
295 * Acquire a lock for a given cron event.
296 *
297 * @param string $event Unique cron event name.
298 * @param string $ttl Cron schedule name to hold the lock. Default is 'every_minute'.
299 * @return bool True if lock is acquired, False if another process is running.
300 */
301 public function acquire_cron_lock( string $event, string $ttl = 'every_minute' ): bool {
302 $lock_key = "{$event}_lock";
303 $last_run_key = "{$event}_last_run";
304 $now = time();
305 $lock_duration = 59; // Default lock duration.
306
307 // Set lock duration based on cron schedule.
308 switch ( $ttl ) {
309 case 'twicedaily':
310 $lock_duration = ( DAY_IN_SECONDS / 2 ) - 1;
311 break;
312 case 'hourly':
313 $lock_duration = HOUR_IN_SECONDS - 1;
314 break;
315 case 'daily':
316 $lock_duration = DAY_IN_SECONDS - 1;
317 break;
318 default:
319 break;
320 }
321
322 // Prevent execution if it has already run within lock duration.
323 $last_run = get_site_option( $last_run_key, 0 );
324 if ( $now < $last_run + $lock_duration ) {
325 return false;
326 }
327
328 // Check if another process is running.
329 $lock_time = (int) get_site_option( $lock_key, 0 );
330 if ( $lock_time > 0 && ( $now < $lock_time + $lock_duration ) ) {
331 return false;
332 }
333
334 // Acquire the lock.
335 if ( ! update_site_option( $lock_key, $now ) ) {
336 return false;
337 }
338 return true;
339 }
340
341 /**
342 * Release the lock and update the last run time.
343 *
344 * @param string $event Unique cron event name.
345 */
346 public function release_cron_lock( string $event ): void {
347 $lock_key = "{$event}_lock";
348 $last_run_key = "{$event}_last_run";
349 update_site_option( $last_run_key, time() );
350 delete_site_option( $lock_key );
351 }
352
353 /**
354 * Detect the line ending style used in a given text.
355 *
356 * @param string $text The text to analyze.
357 *
358 * @return string The detected line ending style: "\r\n" for Windows, "\n" for Unix, or "\r" for Classic Mac.
359 */
360 public function detect_line_ending( string $text ): string {
361 $count_crlf = substr_count( $text, "\r\n" );
362 $count_lf = substr_count( $text, "\n" ) - $count_crlf;
363 $count_cr = substr_count( $text, "\r" ) - $count_crlf;
364
365 if ( $count_crlf >= $count_lf && $count_crlf >= $count_cr ) {
366 return "\r\n"; // Windows-style.
367 } elseif ( $count_lf >= $count_cr ) {
368 return "\n"; // Unix-style.
369 } else {
370 return "\r"; // Classic Mac (rare).
371 }
372 }
373 }
374