PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.2.3
Defender Security – Malware Scanner, Login Security & Firewall v6.2.3
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 / component / audit / class-audit-event.php

class-audit-event.php in Defender Security – Malware Scanner, Login Security & Firewall 6.2.3, at src/component/audit/class-audit-event.php

471 lines 14.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Abstract class used for handling audit events in the WP_Defender plugin.
4 *
5 * @package WP_Defender\Component\Audit
6 */
7
8 namespace WP_Defender\Component\Audit;
9
10 use Countable;
11 use ReflectionMethod;
12 use ReflectionException;
13 use WP_Defender\Component;
14 use WP_Defender\Traits\IP;
15 use WP_Defender\Traits\User;
16 use Calotes\Helper\Array_Cache;
17 use WP_Defender\Model\Setting\Audit_Logging;
18
19 /**
20 * Base class for all audit events.
21 */
22 abstract class Audit_Event extends Component {
23
24 use User;
25 use IP;
26
27 public const ACTION_CREATED = 'created', ACTION_DELETED = 'deleted', ACTION_TRASHED = 'trashed', ACTION_RESTORED = 'restored', ACTION_UPDATED = 'updated';
28
29 /**
30 * Return an array of hooks.
31 *
32 * @return mixed
33 */
34 abstract public function get_hooks();
35
36 /**
37 * Loop through the arguments array and process each argument based on its type.
38 *
39 * @param array $args The array of arguments to process.
40 * @param array $params The parameters array to update with processed arguments.
41 * @param mixed $leftover Additional leftover parameter.
42 */
43 private static function get_custom_args( $args, &$params, &$leftover ) {
44 foreach ( $args as $key => $arg ) {
45 if ( is_string( $arg ) && preg_match( '/{{.*}}/', $arg ) ) {
46 $search = str_replace( array( '{{', '}}' ), '', $arg );
47 $found = self::recursive_look_array( $params, $search );
48 if ( false !== $found ) {
49 $params[ $key ] = $found;
50 }
51 } else {
52 $params[ $key ] = $arg;
53 }
54 }
55 }
56
57 /**
58 * Loop through the arguments array and process each argument based on its type.
59 *
60 * @param mixed $args The array of arguments to process.
61 * @param array $params The parameters array to update with processed arguments.
62 * @param mixed $leftover Additional leftover parameter.
63 *
64 * @throws ReflectionException If the class or method does not exist.
65 */
66 private static function get_program_args( $args, &$params, &$leftover ) {
67 foreach ( $args as $key => $arg ) {
68 // Loop through each.
69 $func_args = array();
70 if ( isset( $arg['params'] ) && is_array( $arg['params'] ) ) {
71 foreach ( $arg['params'] as $value ) {
72 if ( is_string( $value ) && preg_match( '/{{.*}}/', $value ) ) {
73 $search = str_replace( array( '{{', '}}' ), '', $value );
74 $found = self::recursive_look_array( $params, $search );
75 if ( false !== $found ) {
76 $func_args[] = $found;
77 }
78 } else {
79 $func_args[] = $value;
80 }
81 }
82 }
83 // Call the function.
84 if ( ! is_array( $arg['callable'] ) ) {
85 $ret = call_user_func_array( $arg['callable'], $func_args );
86 } else {
87 $reflection_method = new ReflectionMethod( $arg['callable'][0], $arg['callable'][1] );
88 $ret = $reflection_method->invokeArgs( new $arg['callable'][0](), $func_args );
89 }
90 if ( isset( $arg['result_property'] ) ) {
91 $ret = self::recursive_look( $ret, explode( '->', $arg['result_property'] ) );
92 }
93
94 $params[ $key ] = $ret;
95 }
96 }
97
98 /**
99 * Recursively looks up a value within a nested array or object based on provided links.
100 *
101 * This method takes an object or an array and an array of links. It iterates over the links
102 * and uses each link to access a nested property or array element of the $obj. For example,
103 * if the $obj is an array ['a' => ['b' => 'c']], and the links are ['a', 'b'], the method will
104 * return 'c'. If the link does not exist in the object or array, the method will return false.
105 *
106 * @param mixed $obj The object or array to access nested properties or array elements.
107 * @param array $links The array of links to access nested properties or array elements of the $obj.
108 *
109 * @return mixed The final value of $obj if the iteration is successful, false otherwise.
110 */
111 private static function recursive_look( $obj, array $links ) {
112 // We will iterate over the links and use each link to access a nested property or array element of the $obj.
113 foreach ( $links as $link ) {
114 if ( is_array( $obj ) && array_key_exists( $link, $obj ) ) {
115 $obj = $obj[ $link ];
116 } elseif ( is_object( $obj ) ) {
117 $vars = get_object_vars( $obj );
118 if ( ! array_key_exists( $link, $vars ) ) {
119 return false;
120 }
121 $obj = $vars[ $link ];
122 } else {
123 return false;
124 }
125 }
126 // If the iteration is successful, we return the final value of $obj.
127 return $obj;
128 }
129
130 /**
131 * Performs a recursive lookup in an array based on a given set of links.
132 *
133 * @param array $data The array to perform the lookup on.
134 * @param string $links The links to follow in the array, separated by '->'.
135 *
136 * @return mixed The value found at the end of the links, or null if not found.
137 */
138 private static function recursive_look_array( $data, $links ) {
139 $links_array = explode( '->', $links );
140
141 // Check if the first link exists and is valid.
142 if ( ! isset( $data[ $links_array[0] ] ) ) {
143 return false;
144 }
145
146 // If the first link exists and is valid, retrieve the value of the first link from the data array.
147 $obj = $data[ $links_array[0] ];
148
149 // If there are more links to process, pass them to recursive_look.
150 if ( count( $links_array ) > 1 ) {
151 // Pass the remaining links starting from index 1 to the recursive_look method.
152 $remaining_links = array_slice( $links_array, 1 );
153 $obj = self::recursive_look( $obj, $remaining_links );
154 }
155
156 // Finally, return the final value found at the end of the links.
157 return $obj;
158 }
159
160 /**
161 * This is a private method that is used to get the text based on the provided parameters.
162 * In many case, text can be condition, example an array contain text 1, text 2, text 3,
163 * if 2 matched, we get the first.
164 *
165 * @param mixed $text The text to be processed. It can be either a string or an array.
166 * @param array $params The parameters used to replace placeholders in the text.
167 *
168 * @return mixed The processed text or false if no match is found.
169 */
170 private function get_text( $text, $params ) {
171 if ( ! is_array( $text ) ) {
172 return $text;
173 }
174
175 $matched = array();
176 foreach ( $text as $row ) {
177 $t = $row[0];
178 // This will be inside parameters.
179 $value_placeholder = str_replace( array( '{{', '}}' ), '', $row[1] );
180 // No need to validate here, all should be passed by hardcode.
181 $value = self::recursive_look_array( $params, $value_placeholder );
182
183 $compare = $row[2];
184 if ( is_string( $compare ) && preg_match( '/{{.*}}/', $compare ) ) {
185 $search = str_replace( array( '{{', '}}' ), '', $compare );
186 $compare = self::recursive_look_array( $params, $search );
187 }
188
189 $comparison = $row[3] ?? '==';
190
191 if ( is_array( $value ) && is_array( $compare ) ) {
192 // Compare 2 arrays.
193 $map = array(
194 '==' => count( self::array_recursive_diff( $value, $compare ) ) === 0,
195 '!=' => count( self::array_recursive_diff( $value, $compare ) ) !== 0,
196 );
197 } elseif ( is_array( $compare ) ) {
198 // In or not in array.
199 $map = array(
200 'in' => in_array( $value, $compare, true ),
201 'not_in' => in_array( $value, $compare, true ) === false,
202 );
203 } else {
204 $map = array(
205 '===' => $value === $compare,
206 '!==' => $value !== $compare,
207 '>' => $value > $compare,
208 '>=' => $value >= $compare,
209 '<' => $value < $compare,
210 '<=' => $value <= $compare,
211 '==' => $value === $compare,
212 '!=' => $value !== $compare,
213 );
214 }
215
216 if ( $map[ $comparison ] ) {
217 $matched[] = $t;
218 }
219 }
220
221 return array_shift( $matched );
222 }
223
224 /**
225 * Recursively finds the difference between two arrays.
226 *
227 * @param array $array1 The first array to compare.
228 * @param array $array2 The second array to compare against.
229 *
230 * @return array Difference between the two arrays.
231 */
232 protected static function array_recursive_diff( $array1, $array2 ): array {
233 $result = array();
234
235 foreach ( $array1 as $key => $value ) {
236 if ( array_key_exists( $key, $array2 ) ) {
237 if ( is_array( $value ) ) {
238 $recursive_diff = self::array_recursive_diff( $value, $array2[ $key ] );
239 if ( count( $recursive_diff ) ) {
240 $result[ $key ] = $recursive_diff;
241 }
242 } elseif ( $value !== $array2[ $key ] ) {
243 $result[ $key ] = $value;
244 }
245 } else {
246 $result[ $key ] = $value;
247 }
248 }
249
250 return $result;
251 }
252
253 /**
254 * Check conditions against parameters and return a boolean result.
255 *
256 * @param mixed $conditions The conditions to check against.
257 * @param array $params The parameters to use for comparison.
258 *
259 * @return bool The boolean result of the condition check.
260 */
261 private static function check_condition( $conditions, $params ): bool {
262 $good = true;
263 foreach ( $conditions as $condition ) {
264 $links = $condition[0];
265 $value = self::recursive_look_array( $params, str_replace( array( '{{', '}}' ), '', $links ) );
266 $compare = $condition[1];
267 if ( is_string( $compare ) && preg_match( '/{{.*}}/', $compare ) ) {
268 $search = str_replace( array( '{{', '}}' ), '', $compare );
269 $compare = self::recursive_look_array( $params, $search );
270 }
271
272 if ( is_array( $compare ) && in_array( $value, $compare, true ) ) {
273 $good = false;
274 } elseif ( $value === $compare ) {
275 $good = false;
276 }
277 }
278
279 return $good;
280 }
281
282 /**
283 * Build log data based on the provided hook data and parameters.
284 *
285 * @return bool Returns false if the log data cannot be built, otherwise true.
286 * @throws ReflectionException If the class or method does not exist.
287 */
288 public function build_log_data() {
289 $args = func_get_args();
290 $hook_name = $args[0];
291 $params = $args[1];
292 $user_id = get_current_user_id();
293 $hook_data = $args[2];
294 // Have to build iup params first.
295 if ( ( is_array( $hook_data['args'] ) || $hook_data['args'] instanceof Countable ? count( $hook_data['args'] ) : 0 ) !== ( is_array( $params ) || $params instanceof Countable ? count( $params ) : 0 ) ) {
296 return false;
297 } elseif (
298 ( ! isset( $hook_data['args'] ) || ! is_array( $hook_data['args'] ) || array() === $hook_data['args'] )
299 && ( ! is_array( $params ) || array() === $params )
300 ) {
301 $params = array();
302 } else {
303 $params = array_combine( $hook_data['args'], $params );
304 }
305
306 if ( isset( $hook_data['callback'] ) && is_array( $hook_data['callback'] ) && count( $hook_data['callback'] ) > 1 ) {
307 // Custom callback provided, call it.
308 $reflection_method = new ReflectionMethod( $hook_data['callback'][0], $hook_data['callback'][1] );
309 $new_hooks = array(
310 'add_user_to_blog',
311 'user_profile_update_errors',
312 'profile_update',
313 'password_reset',
314 'user_register',
315 'wpmu_new_user',
316 );
317 if ( in_array( $hook_name, $new_hooks, true ) ) {
318 $ret = $reflection_method->invokeArgs(
319 new $hook_data['callback'][0](),
320 $params
321 );
322 } else {
323 $ret = $reflection_method->invokeArgs(
324 new $hook_data['callback'][0](),
325 array(
326 $hook_name,
327 $params,
328 )
329 );
330 }
331
332 if ( is_array( $ret ) && 2 === count( $ret ) ) {
333 [ $text, $context ] = $ret;
334 } elseif ( is_array( $ret ) && 3 === count( $ret ) ) {
335 [ $text, $context, $action ] = $ret;
336 } else {
337 $text = false;
338 }
339 } else {
340 /**
341 * First we need to query all parameters,
342 * we have to loop around custom args, program args to queries and maintain the params list,
343 * then we will check the left over, till all done,
344 * then we will build up the text.
345 */
346 $leftover = array();
347 if ( isset( $hook_data['custom_args'] ) ) {
348 self::get_custom_args( $hook_data['custom_args'], $params, $leftover );
349 }
350
351 if ( isset( $hook_data['program_args'] ) ) {
352 self::get_program_args( $hook_data['program_args'], $params, $leftover );
353 }
354 $params = array_merge( $this->get_default_params(), $params );
355 // Still need to check if this condition okay.
356 if ( isset( $hook_data['false_when'] ) && self::check_condition(
357 $hook_data['false_when'],
358 $params
359 ) === false ) {
360 return false;
361 }
362 // Now we got all params as key=>value, just build the text.
363 $text = self::get_text( $hook_data['text'], $params );
364
365 if ( ! is_string( $text ) || '' === $text ) {
366 return false;
367 }
368
369 foreach ( $params as $key => $val ) {
370 $replacer = $val;
371 if ( is_array( $replacer ) || is_object( $replacer ) ) {
372 continue;
373 }
374 $text = str_replace( '{{' . $key . '}}', $replacer, $text );
375 }
376
377 $context = '';
378 if ( isset( $hook_data['context'] ) ) {
379 if ( is_array( $hook_data['context'] ) && is_callable( $hook_data['context'] ) ) {
380 $reflection_method = new ReflectionMethod( $hook_data['context'][0], $hook_data['context'][1] );
381 $context = $reflection_method->invokeArgs(
382 new $hook_data['context'][0](),
383 array(
384 $hook_name,
385 $params,
386 )
387 );
388 } elseif ( preg_match( '/{{.*}}/', $hook_data['context'] ) ) {
389 $context = self::recursive_look_array(
390 $params,
391 str_replace(
392 array(
393 '{{',
394 '}}',
395 ),
396 '',
397 $hook_data['context']
398 )
399 );
400 } else {
401 $context = $hook_data['context'];
402 }
403 }
404 }
405
406 $anonymous_override_list = array(
407 'wp_login',
408 'wp_logout',
409 'retrieve_password',
410 'after_password_reset',
411 );
412 if ( 0 === $user_id && in_array( $hook_name, $anonymous_override_list, true ) ) {
413 // In this state, user id still 0, we have to get the id via hooks param.
414 if ( isset( $params['user'] ) ) {
415 $user = $params['user'];
416 $user_id = $user->ID;
417 } elseif ( isset( $params['user_id'] ) ) {
418 $user_id = $params['user_id'];
419 } else {
420 // Incorrect data. Return.
421 return false;
422 }
423 }
424
425 // We don't get text. Return.
426 if ( ! is_string( $text ) || '' === trim( $text ) ) {
427 return false;
428 }
429
430 // Build data.
431 $settings = new Audit_Logging();
432 $time = time();
433 $event_type = $hook_data['event_type'];
434 $action_type = $action ?? $hook_data['action_type'];
435 $site_url = network_site_url();
436 $msg = wp_strip_all_tags( $text );
437 $blog_id = get_current_blog_id();
438 $ttl = strtotime( '+ ' . $settings->storage_days );
439 foreach ( $this->get_user_ip() as $ip ) {
440 $post = array(
441 'timestamp' => $time,
442 'event_type' => $event_type,
443 'action_type' => $action_type,
444 'site_url' => $site_url,
445 'user_id' => $user_id,
446 'context' => $context,
447 'ip' => $ip,
448 'msg' => $msg,
449 'blog_id' => $blog_id,
450 'ttl' => $ttl,
451 );
452 Array_Cache::append( 'logs', $post, 'audit' );
453 }
454
455 return true;
456 }
457
458 /**
459 * Retrieves default parameters for building log data.
460 *
461 * @return array default parameters.
462 */
463 private function get_default_params(): array {
464 return array(
465 'wp_user' => $this->get_source_of_action(),
466 'wp_user_id' => get_current_user_id(),
467 'blog_name' => is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : '',
468 );
469 }
470 }
471