PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.2.4
Defender Security – Malware Scanner, Login Security & Firewall v6.2.4
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 / controller / class-audit-logging.php

class-audit-logging.php in Defender Security – Malware Scanner, Login Security & Firewall 6.2.4, at src/controller/class-audit-logging.php

672 lines 18.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handle Audit Logging module.
4 *
5 * @package WP_Defender\Controller
6 */
7
8 namespace WP_Defender\Controller;
9
10 use DateTime;
11 use Exception;
12 use DateInterval;
13 use WP_Defender\Event;
14 use Calotes\Helper\HTTP;
15 use WP_Defender\Traits\User;
16 use Calotes\Component\Request;
17 use Calotes\Component\Response;
18 use Calotes\Helper\Array_Cache;
19 use WP_Defender\Traits\Formats;
20 use WP_Defender\Component\Audit;
21 use WP_Defender\Model\Audit_Log;
22 use WP_Defender\Behavior\WPMUDEV;
23 use WP_Defender\Component\Network_Cron_Manager;
24 use WP_Defender\Model\Notification\Audit_Report;
25 use WP_Defender\Component\Config\Config_Hub_Helper;
26 use WP_Defender\Model\Setting\Audit_Logging as Model_Audit_Logging;
27
28 /**
29 * Handle Audit Logging module.
30 */
31 class Audit_Logging extends Event {
32
33 public const DEFAULT_PER_PAGE = 10;
34
35 use User;
36 use Formats;
37
38 /**
39 * The slug identifier for this controller.
40 *
41 * @var string
42 */
43 public $slug = 'wdf-logging';
44
45 /**
46 * The model for handling the data.
47 *
48 * @var Model_Audit_Logging
49 */
50 public $model;
51
52 /**
53 * Service for handling logic.
54 *
55 * @var Audit|null
56 */
57 public ?Audit $service;
58
59 /**
60 * Indicates whether the current installation is a pro version.
61 *
62 * @var bool
63 */
64 private $is_pro;
65
66 /**
67 * Initializes the model and service, registers routes, and sets up scheduled events if the model is active.
68 */
69 public function __construct() {
70 $this->register_page(
71 esc_html__( 'Audit Log', 'defender-security' ),
72 $this->slug,
73 array( $this, 'main_view' ),
74 $this->parent_slug
75 );
76 add_action( 'defender_enqueue_assets', array( $this, 'enqueue_assets' ) );
77 $this->model = wd_di()->get( Model_Audit_Logging::class );
78 $this->service = new Audit();
79 $this->register_routes();
80 $this->is_pro = wd_di()->get( WPMUDEV::class )->is_pro();
81 if ( $this->is_active() ) {
82 $this->service->enqueue_event_listener();
83 add_action( 'shutdown', array( $this, 'cache_audit_logs' ) );
84
85 /**
86 * Network Cron Manager
87 *
88 * @var Network_Cron_Manager $network_cron_manager
89 */
90 $network_cron_manager = wd_di()->get( Network_Cron_Manager::class );
91 $network_cron_manager->register_callback(
92 'audit_clean_up_logs',
93 array( $this->service, 'audit_clean_up_logs' ),
94 HOUR_IN_SECONDS
95 );
96 }
97 }
98
99 /**
100 * Is there permission to Pro feature?
101 *
102 * @return bool
103 */
104 private function is_active(): bool {
105 return $this->model->is_active();
106 }
107
108 /**
109 * Parses a raw request params array, resolves filters, and fetches audit logs.
110 *
111 * Accepted keys (all optional except date range):
112 * date_from — date string or empty (defaults to -7 days midnight).
113 * date_to — date string or empty (defaults to today 23:59:59).
114 * events — array of event type strings (also accepts key 'event_type' for CSV route).
115 * username — login name (also accepts key 'term' for CSV route).
116 * ip_address — IP filter string.
117 * paged — page number; pass false for unbounded export.
118 *
119 * Returns the resolved user_id via the $user_id out-param so callers can use it for COUNT queries.
120 *
121 * @param array $params Raw request data.
122 * @param mixed $paged Page number or false for all results.
123 * @param int|string $user_id Populated with the resolved user ID (out-param).
124 * @param int $per_page Number of logs per page.
125 *
126 * @return array|\WP_Error
127 */
128 private function fetch_logs( array $params, $paged, &$user_id = '', $per_page = self::DEFAULT_PER_PAGE ) {
129 $date_from_str = $params['date_from'] ?? '';
130 $date_to_str = $params['date_to'] ?? '';
131 $date_from = $date_from_str ? $this->date_string_to_timestamp( $date_from_str ) : strtotime( '-7 days midnight' );
132 $date_to = $date_to_str ? $this->date_string_to_timestamp( $date_to_str, true ) : strtotime( 'today 23:59:59' );
133 $events = $params['events'] ?? ( isset( $params['event_type'] ) && is_array( $params['event_type'] ) ? $params['event_type'] : array() );
134 $username = $params['username'] ?? ( $params['term'] ?? '' );
135 $ip_address = $params['ip_address'] ?? '';
136 $user_id = '';
137
138 if ( '' !== $username ) {
139 $user = get_user_by( 'login', $username );
140 if ( ! is_object( $user ) ) {
141 // Try matching by display_name or email before giving up.
142 $users = get_users(
143 array(
144 'search' => '*' . $username . '*',
145 'search_columns' => array( 'display_name' ),
146 'number' => 1,
147 )
148 );
149 if ( array() === $users ) {
150 return array();
151 }
152 $user_id = $users[0]->ID;
153 } else {
154 $user_id = $user->ID;
155 }
156 }
157
158 return $this->service->fetch( $date_from, $date_to, $events, $user_id, $ip_address, $paged, $per_page );
159 }
160
161 /**
162 * Prefetches users from a result set then formats each log into an array.
163 *
164 * @param array $result Array of Audit_Log objects.
165 *
166 * @return array
167 */
168 private function format_log_rows( array $result ): array {
169 $user_ids = array_filter(
170 array_unique( array_column( $result, 'user_id' ) ),
171 static fn ( $user_id ): bool => (int) $user_id > 0
172 );
173 if ( array() !== $user_ids ) {
174 get_users( array( 'include' => $user_ids ) );
175 }
176
177 $logs = array();
178 foreach ( $result as $item ) {
179 $item_user_id = isset( $item->user_id ) ? $item->user_id : 0;
180 $item_user_id = ! is_int( $item_user_id ) ? (int) $item_user_id : $item_user_id;
181 $logs[] = array_merge(
182 $item->export(),
183 array(
184 'user' => $this->get_user_display( $item->user_id ),
185 'user_url' => $item_user_id > 0 ? get_edit_user_link( $item_user_id ) : '',
186 'log_date' => $this->get_date( $item->timestamp ),
187 'format_date' => $this->format_date_time( $item->timestamp ),
188 )
189 );
190 }
191
192 return $logs;
193 }
194
195 /**
196 * Exports audit logs as a CSV file.
197 *
198 * @return void
199 * @throws Exception If there is an error during export.
200 * @defender_route
201 */
202 public function export_as_csv(): void {
203 $result = $this->fetch_logs( defender_get_data_from_request( null, 'g' ), false );
204
205 if ( is_wp_error( $result ) ) {
206 wp_send_json_error(
207 array(
208 'message' => $result->get_error_message(),
209 )
210 );
211 }
212
213 // WP_Filesystem class doesn't directly provide a function for opening a stream to php://memory with the 'w' mode.
214 $fp = fopen( 'php://memory', 'w' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
215 $headers = array(
216 esc_html__( 'Summary', 'defender-security' ),
217 esc_html__( 'Date / Time', 'defender-security' ),
218 esc_html__( 'Context', 'defender-security' ),
219 esc_html__( 'Type', 'defender-security' ),
220 esc_html__( 'IP address', 'defender-security' ),
221 esc_html__( 'User', 'defender-security' ),
222 );
223 fputcsv( $fp, $headers, ',', '"', '\\' );
224 foreach ( $this->format_log_rows( $result ) as $row ) {
225 $vars = array(
226 $row['msg'],
227 is_array( $row['timestamp'] )
228 ? $this->format_date_time( $row['timestamp'][0] )
229 : $this->format_date_time( $row['timestamp'] ),
230 $row['context'],
231 $row['action_type'],
232 $row['ip'],
233 $row['user'],
234 );
235 fputcsv( $fp, $vars, ',', '"', '\\' );
236 }
237 $filename = 'wdf-audit-logs-export-' . wp_date( 'ymdHis' ) . '.csv';
238 fseek( $fp, 0 );
239 header( 'Content-Type: text/csv' );
240 header( 'Content-Disposition: attachment; filename="' . $filename . '";' );
241 // Make php send the generated csv lines to the browser.
242 fpassthru( $fp );
243 exit();
244 }
245
246 /**
247 * We'll pass all the event logs into the db handler, so it writes down to db.
248 * Do it in shutdown runtime, so no delay time.
249 *
250 * @return void
251 */
252 public function cache_audit_logs(): void {
253 $audit = new Audit();
254 $audit->log_audit_events();
255 }
256
257 /**
258 * Pull the logs from db cached:
259 * - date_from: the start of the date we will run the query, as mysql time format,
260 * - date_to: similar to the above,
261 * others will refer to Audit.
262 *
263 * @param Request $request The request object containing filter parameters.
264 *
265 * @return Response
266 * @throws Exception If there is an error during log retrieval.
267 * @defender_route
268 */
269 public function pull_logs( Request $request ): Response {
270 $data = $request->get_data(
271 array(
272 'date_from' => array(
273 'type' => 'string',
274 'sanitize' => 'sanitize_text_field',
275 ),
276 'date_to' => array(
277 'type' => 'string',
278 'sanitize' => 'sanitize_text_field',
279 ),
280 'username' => array(
281 'type' => 'string',
282 'sanitize' => 'sanitize_text_field',
283 ),
284 'events' => array(
285 'type' => 'array',
286 'sanitize' => 'sanitize_text_field',
287 ),
288 'ip_address' => array(
289 'type' => 'string',
290 'sanitize' => 'sanitize_text_field',
291 ),
292 'paged' => array(
293 'type' => 'int',
294 'sanitize' => 'sanitize_text_field',
295 ),
296 'per_page' => array(
297 'type' => 'int',
298 'sanitize' => 'sanitize_text_field',
299 ),
300 )
301 );
302 if ( ! isset( $data['date_from'] ) || ! isset( $data['date_to'] ) ) {
303 return new Response( false, array( 'message' => esc_html__( 'Invalid data.', 'defender-security' ) ) );
304 }
305 // Validate date strings before passing to fetch_logs.
306 $date_from_check = $this->date_string_to_timestamp( $data['date_from'] );
307 $date_to_check = $this->date_string_to_timestamp( $data['date_to'], true );
308 if ( $date_from_check <= 0 || $date_to_check <= 0 ) {
309 return new Response( false, array( 'message' => esc_html__( 'Invalid data.', 'defender-security' ) ) );
310 }
311
312 $paged = $data['paged'] ?? 1;
313 $per_page = min( 100, max( 1, (int) ( $data['per_page'] ?? self::DEFAULT_PER_PAGE ) ) );
314 $result = $this->fetch_logs( $data, $paged, $user_id, $per_page );
315
316 if ( is_wp_error( $result ) ) {
317 return new Response( false, array( 'message' => $result->get_error_message() ) );
318 }
319 $logs = is_array( $result ) && array() !== $result ? $this->format_log_rows( $result ) : array();
320
321 // @since 3.0.0 If no logs then $count = 0.
322 // Skip the COUNT query when the result is a partial page — the fetch already told us the total is count($logs).
323 $events = $data['events'] ?? array();
324 $ip_address = $data['ip_address'] ?? '';
325 if ( array() === $logs ) {
326 $count = 0;
327 } elseif ( count( $logs ) < $per_page ) {
328 $count = ( $paged - 1 ) * $per_page + count( $logs );
329 } else {
330 $count = Audit_Log::count( $date_from_check, $date_to_check, $events, $user_id, $ip_address );
331 }
332
333 // Get the count for the submitted data.
334 return new Response(
335 true,
336 array(
337 'logs' => $logs,
338 'total_items' => $count,
339 'total_pages' => ceil( $count / $per_page ),
340 'per_page' => $per_page,
341 )
342 );
343 }
344
345 /**
346 * Generates a human-readable frequency text for audit reports.
347 *
348 * @param Audit_Report $audit_report The audit report object.
349 *
350 * @return string Returns the formatted frequency description.
351 */
352 public function get_frequency_text( Audit_Report $audit_report ): string {
353 $text = '';
354 switch ( $audit_report->frequency ) {
355 case 'daily':
356 $text = ucfirst( $audit_report->day ) . 's at ' . $audit_report->time;
357 break;
358 case 'weekly':
359 case 'monthly':
360 $text = ucfirst( $audit_report->frequency ) . ' on ' . ucfirst( $audit_report->day ) . 's at ' . $audit_report->time;
361 break;
362 default:
363 break;
364 }
365
366 return $text;
367 }
368
369 /**
370 * Enqueues scripts and styles for this page.
371 */
372 public function enqueue_assets(): void {
373 if ( ! $this->is_page_active() ) {
374 return;
375 }
376
377 $handle = 'defender-ui-audit-logging';
378 wp_enqueue_script(
379 $handle,
380 WP_DEFENDER_BASE_URL . 'assets/js/audit-logging-ui.js',
381 array( 'def-vue', 'def-manifest', 'def-core-ui', 'defender', 'wp-i18n' ),
382 DEFENDER_VERSION,
383 true
384 );
385 wp_set_script_translations( $handle, 'wpdef' );
386
387 wp_localize_script(
388 $handle,
389 'defenderUIData',
390 array_merge(
391 $this->get_shared_data(),
392 $this->data_frontend()
393 )
394 );
395
396 wp_enqueue_style(
397 $handle,
398 WP_DEFENDER_BASE_URL . 'assets/css/showcase.css',
399 array(),
400 DEFENDER_VERSION
401 );
402
403 $this->enqueue_main_assets();
404 }
405
406 /**
407 * Render the root element for frontend.
408 *
409 * @return void
410 */
411 public function main_view(): void {
412 $this->render( 'main' );
413 }
414
415 /**
416 * Provides a summary of audit logs.
417 *
418 * @return void
419 * @throws Exception If there is an error during summary generation.
420 * @defender_route
421 */
422 public function summary(): void {
423 $response = $this->is_active() ? $this->summary_data() : array();
424 wp_send_json_success( $response );
425 }
426
427 /**
428 * Returns an array with summary data for audit logging.
429 *
430 * @param bool $for_hub Default 'false' because it's displayed on site summary sections.
431 *
432 * @return array
433 * @throws Exception Emits Exception in case of an error.
434 */
435 public function summary_data( bool $for_hub = false ): array {
436 $timezone = wp_timezone();
437
438 // Monthly count.
439 $date_from = ( new DateTime( '-30 days', $timezone ) )
440 ->setTime( 0, 0, 0 )
441 ->getTimestamp();
442 $date_to = ( new DateTime( 'now', $timezone ) )
443 ->setTime( 23, 59, 59 )
444 ->getTimestamp();
445 $month_count = Audit_Log::count( $date_from, $date_to );
446
447 // Weekly count sets in the returned array.
448 // Daily count. Sync data to the Hub without timezone.
449 $date_from = $for_hub ? new DateTime( 'now' ) : new DateTime( 'now', wp_timezone() );
450 $date_from = $date_from->modify( '-24 hours' )->setTime( 0, 0, 0 )->getTimestamp();
451 $day_count = Audit_Log::count( $date_from, $date_to );
452
453 // Get the last item.
454 $last = Audit_Log::get_last();
455 if ( is_object( $last ) ) {
456 $last = $for_hub
457 ? $this->persistent_hub_datetime_format( $last->timestamp )
458 : $this->format_date_time( $last->timestamp );
459 } else {
460 $last = 'n/a';
461 }
462
463 return array(
464 'monthCount' => $month_count,
465 'weekCount' => Audit::get_audit_events_per_week(),
466 'dayCount' => $day_count,
467 'lastEvent' => $last,
468 'report' => wd_di()->get( Audit_Report::class )->to_string(),
469 );
470 }
471
472 /**
473 * Save settings.
474 *
475 * @param Request $request The request object containing new settings data.
476 *
477 * @return Response
478 * @defender_route
479 */
480 public function save_settings( Request $request ): Response {
481 $data = $request->get_data_by_model( $this->model );
482
483 $this->model->import( $data );
484 if ( $this->model->validate() ) {
485 $this->model->save();
486 }
487
488 Config_Hub_Helper::set_clear_active_flag();
489
490 return new Response(
491 true,
492 array_merge(
493 $this->data_frontend(),
494 array(
495 'message' => esc_html__( 'Your settings have been updated.', 'defender-security' ),
496 'auto_close' => true,
497 'model' => $this->model->export(),
498 )
499 )
500 );
501 }
502
503 /**
504 * Provides the audit settings data used outside the Audit Logging page.
505 *
506 * @return array
507 */
508 public function settings_data_frontend(): array {
509 return array_merge(
510 array(
511 'model' => $this->model->export(),
512 ),
513 $this->dump_routes_and_nonces()
514 );
515 }
516
517 /**
518 * Converts the current state of the object to an array.
519 *
520 * @return array Returns an associative array of object properties.
521 */
522 public function to_array(): array {
523 return array_merge(
524 array(
525 'enabled' => $this->is_active(),
526 'report' => true,
527 ),
528 $this->dump_routes_and_nonces()
529 );
530 }
531
532 /**
533 * Removes settings for all submodules.
534 */
535 public function remove_settings(): void {
536 ( new Model_Audit_Logging() )->delete();
537 }
538
539 /**
540 * Delete all the data & the cache.
541 */
542 public function remove_data(): void {
543 Audit_Log::truncate();
544 // Remove cached data.
545 Array_Cache::remove( 'sockets', 'audit' );
546 Array_Cache::remove( 'logs', 'audit' );
547 Array_Cache::remove( 'menu_updated', 'audit' );
548 Array_Cache::remove( 'post_updated', 'audit' );
549 delete_site_option( 'wd_audit_fetch_checkpoint' );
550 }
551
552 /**
553 * Provides data for the frontend.
554 *
555 * @return array An array of data for the frontend.
556 * @throws Exception If there is an error.
557 */
558 public function data_frontend(): array {
559 $logs = array();
560 $count = 0;
561 $per_page = self::DEFAULT_PER_PAGE;
562 $total_page = 1;
563 if ( $this->is_active() ) {
564 $timezone = wp_timezone();
565 $date_from = ( new DateTime( '-7 days', $timezone ) )->setTime( 0, 0, 0 );
566 $date_to = ( new DateTime( 'now', $timezone ) )->setTime( 23, 59, 59 );
567 $ip_address = sanitize_text_field( defender_get_data_from_request( 'ip', 'g' ) );
568 $result = $this->service->fetch(
569 $date_from->getTimestamp(),
570 $date_to->getTimestamp(),
571 array(),
572 '',
573 $ip_address,
574 1,
575 $per_page
576 );
577 if ( ! is_wp_error( $result ) ) {
578 $logs = $this->format_log_rows( $result );
579 // Skip COUNT query when the first page is a partial result — it's cheaper than a second DB round-trip.
580 if ( count( $logs ) < $per_page ) {
581 $count = count( $logs );
582 } else {
583 $count = Audit_Log::count( $date_from->getTimestamp(), $date_to->getTimestamp(), array(), '', $ip_address );
584 }
585 $total_page = ceil( $count / $per_page );
586 }
587 }
588
589 return array(
590 'auditLogging' => array_merge(
591 array(
592 'model' => $this->model->export(),
593 'logs' => $logs,
594 'events_type' => Audit_Log::allowed_events(),
595 'summary' => array(
596 'count_7_days' => $count,
597 'report' => wd_di()->get( Audit_Report::class )->to_string(),
598 ),
599 'paging' => array(
600 'paged' => 1,
601 'total_pages' => $total_page,
602 'count' => $count,
603 ),
604 ),
605 $this->dump_routes_and_nonces()
606 ),
607 'antibot' => wd_di()->get( Antibot_Global_Firewall::class )->data_frontend(),
608 );
609 }
610
611 /**
612 * Imports data into the model.
613 *
614 * @param array $data Data to be imported into the model.
615 *
616 * @throws Exception If table is not defined.
617 */
618 public function import_data( array $data ) {
619 $model = $this->model;
620 if ( array() === $data ) {
621 $model->enabled = false;
622 $model->storage_days = '6 months';
623 $model->save();
624 } else {
625 $model->import( $data );
626 if ( $model->validate() ) {
627 $model->save();
628 }
629 }
630 }
631
632 /**
633 * Exports strings.
634 *
635 * @return array An array of strings.
636 */
637 public function export_strings(): array {
638 $strings = $this->is_active()
639 ? array( esc_html__( 'Active', 'defender-security' ) )
640 : array( esc_html__( 'Inactive', 'defender-security' ) );
641
642 $strings[] = sprintf(
643 /* translators: %s: Html for Pro-tag. */
644 esc_html__( 'Email report inactive %s', 'defender-security' ),
645 '<span class="sui-tag sui-tag-pro">Pro</span>'
646 );
647
648 return $strings;
649 }
650
651 /**
652 * Generates configuration strings based on the provided configuration.
653 *
654 * @param array $config Configuration data.
655 *
656 * @return array Returns an array of configuration strings.
657 */
658 public function config_strings( array $config ): array {
659 $strings = $config['enabled']
660 ? array( esc_html__( 'Active', 'defender-security' ) )
661 : array( esc_html__( 'Inactive', 'defender-security' ) );
662
663 $strings[] = sprintf(
664 /* translators: %s: Html for Pro-tag. */
665 esc_html__( 'Email report inactive %s', 'defender-security' ),
666 '<span class="sui-tag sui-tag-pro">Pro</span>'
667 );
668
669 return $strings;
670 }
671 }
672