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

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

616 lines 17.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles audit logging functionalities .
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\Model\Notification\Audit_Report;
24 use WP_Defender\Component\Config\Config_Hub_Helper;
25 use WP_Defender\Model\Setting\Audit_Logging as Model_Audit_Logging;
26
27 /**
28 * Handles audit logging functionalities .
29 */
30 class Audit_Logging extends Event {
31
32 use User;
33 use Formats;
34
35 /**
36 * The slug identifier for this controller.
37 *
38 * @var string
39 */
40 public $slug = 'wdf-logging';
41
42 /**
43 * The model for handling the data.
44 *
45 * @var Model_Audit_Logging
46 */
47 public $model;
48
49 /**
50 * Service for handling logic.
51 *
52 * @var Audit|null
53 */
54 public ?Audit $service;
55
56 /**
57 * Initializes the model and service, registers routes, and sets up scheduled events if the model is active.
58 */
59 public function __construct() {
60 $this->register_page(
61 esc_html( Model_Audit_Logging::get_module_name() ),
62 $this->slug,
63 array( $this, 'main_view' ),
64 $this->parent_slug
65 );
66 add_action( 'defender_enqueue_assets', array( $this, 'enqueue_assets' ) );
67 $this->model = wd_di()->get( Model_Audit_Logging::class );
68 $this->service = new Audit();
69 $this->register_routes();
70 if ( $this->model->is_active() ) {
71 $this->service->enqueue_event_listener();
72 add_action( 'shutdown', array( $this, 'cache_audit_logs' ) );
73 /**
74 * We will schedule the time for flush data into cloud.
75 */
76 if ( ! wp_next_scheduled( 'audit_sync_events' ) ) {
77 wp_schedule_event( time() + 15, 'hourly', 'audit_sync_events' );
78 }
79 add_action( 'audit_sync_events', array( $this, 'sync_events' ) );
80
81 /**
82 * We will schedule the time to clean up old logs.
83 */
84 if ( is_multisite() ) {
85 $network_cron_manager = wd_di()->get( \WP_Defender\Component\Network_Cron_Manager::class );
86 $network_cron_manager->register_callback(
87 'audit_clean_up_logs',
88 array( $this->service, 'audit_clean_up_logs' ),
89 HOUR_IN_SECONDS
90 );
91 } else {
92 if ( ! wp_next_scheduled( 'audit_clean_up_logs' ) ) {
93 wp_schedule_event( time(), 'hourly', 'audit_clean_up_logs' );
94 }
95 add_action( 'audit_clean_up_logs', array( $this->service, 'audit_clean_up_logs' ) );
96 }
97 }
98 }
99
100 /**
101 * Sync all the events into cloud, this will happen per hourly basis.
102 *
103 * @return void
104 */
105 public function sync_events(): void {
106 $this->service->flush();
107 }
108
109 /**
110 * Exports audit logs as a CSV file.
111 *
112 * @return void
113 * @throws Exception If there is an error during export.
114 * @defender_route
115 */
116 public function export_as_csv(): void {
117 $date_from = HTTP::get(
118 'date_from',
119 wp_date( 'Y-m-d H:i:s', strtotime( '-7 days', time() ) )
120 );
121 $date_to = HTTP::get( 'date_to', wp_date( 'Y-m-d H:i:s', time() ) );
122 // Convert date using timezone.
123 $timezone = wp_timezone();
124 $date_from = ( new DateTime( $date_from, $timezone ) )->setTime( 0, 0, 0 )->getTimestamp();
125 $date_to = ( new DateTime( $date_to, $timezone ) )->setTime( 23, 59, 59 )->getTimestamp();
126 $username = HTTP::get( 'term', '' );
127 $user_id = '';
128 $user = get_user_by( 'login', $username );
129 $events = HTTP::get( 'event_type', array() );
130 if ( is_object( $user ) ) {
131 $user_id = $user->ID;
132 }
133
134 $handler = new Audit();
135 $ip_address = HTTP::get( 'ip_address', '' );
136 $result = $handler->fetch( $date_from, $date_to, $events, $user_id, $ip_address, false );
137 // WP_Filesystem class doesn’t directly provide a function for opening a stream to php://memory with the 'w' mode.
138 $fp = fopen( 'php://memory', 'w' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
139 $headers = array(
140 esc_html__( 'Summary', 'defender-security' ),
141 esc_html__( 'Date / Time', 'defender-security' ),
142 esc_html__( 'Context', 'defender-security' ),
143 esc_html__( 'Type', 'defender-security' ),
144 esc_html__( 'IP address', 'defender-security' ),
145 esc_html__( 'User', 'defender-security' ),
146 );
147 fputcsv( $fp, $headers, ',', '"', '\\' );
148 foreach ( $result as $log ) {
149 $fields = $log->export();
150 $vars = array(
151 $fields['msg'],
152 is_array( $fields['timestamp'] )
153 ? $this->format_date_time( $fields['timestamp'][0] )
154 : $this->format_date_time( $fields['timestamp'] ),
155 $fields['context'],
156 $fields['action_type'],
157 $fields['ip'],
158 $this->get_user_display( $fields['user_id'] ),
159 );
160 fputcsv( $fp, $vars, ',', '"', '\\' );
161 }
162 $filename = 'wdf-audit-logs-export-' . wp_date( 'ymdHis' ) . '.csv';
163 fseek( $fp, 0 );
164 header( 'Content-Type: text/csv' );
165 header( 'Content-Disposition: attachment; filename="' . $filename . '";' );
166 // Make php send the generated csv lines to the browser.
167 fpassthru( $fp );
168 exit();
169 }
170
171 /**
172 * We'll pass all the event logs into the db handler, so it writes down to db.
173 * Do it in shutdown runtime, so no delay time.
174 *
175 * @return void
176 */
177 public function cache_audit_logs(): void {
178 $audit = new Audit();
179 $audit->log_audit_events();
180 }
181
182 /**
183 * Pull the logs from db cached:
184 * - date_from: the start of the date we will run the query, as mysql time format,
185 * - date_to: similar to the above,
186 * others will refer to Audit.
187 *
188 * @param Request $request The request object containing filter parameters.
189 *
190 * @return Response
191 * @throws Exception If there is an error during log retrieval.
192 * @defender_route
193 */
194 public function pull_logs( Request $request ): Response {
195 $data = $request->get_data(
196 array(
197 'date_from' => array(
198 'type' => 'string',
199 'sanitize' => 'sanitize_text_field',
200 ),
201 'date_to' => array(
202 'type' => 'string',
203 'sanitize' => 'sanitize_text_field',
204 ),
205 'username' => array(
206 'type' => 'string',
207 'sanitize' => 'sanitize_text_field',
208 ),
209 'events' => array(
210 'type' => 'array',
211 'sanitize' => 'sanitize_text_field',
212 ),
213 'ip_address' => array(
214 'type' => 'string',
215 'sanitize' => 'sanitize_text_field',
216 ),
217 'paged' => array(
218 'type' => 'int',
219 'sanitize' => 'sanitize_text_field',
220 ),
221 )
222 );
223 if ( empty( $data['date_from'] ) || empty( $data['date_to'] ) ) {
224 return new Response( false, array( 'message' => esc_html__( 'Invalid data.', 'defender-security' ) ) );
225 }
226 // Convert date using timezone.
227 $timezone = wp_timezone();
228 $date_from = ( new DateTime( $data['date_from'], $timezone ) )
229 ->setTime( 0, 0, 0 )
230 ->getTimestamp();
231 $date_to = ( new DateTime( $data['date_to'], $timezone ) )
232 ->setTime( 23, 59, 59 )
233 ->getTimestamp();
234
235 $events = $data['events'] ?? array();
236 $ip_address = $data['ip_address'] ?? '';
237 $paged = $data['paged'] ?? 1;
238 $username = $data['username'] ?? '';
239 $user_id = '';
240 if ( ! empty( $username ) ) {
241 $user = get_user_by( 'login', $username );
242 if ( is_object( $user ) ) {
243 $user_id = $user->ID;
244 // Fetch result with the specified user.
245 $result = $this->service->fetch( $date_from, $date_to, $events, $user_id, $ip_address, $paged );
246 } else {
247 // A non-existent username.
248 $result = array();
249 }
250 } else {
251 // Fetch result with empty user field.
252 $result = $this->service->fetch( $date_from, $date_to, $events, $user_id, $ip_address, $paged );
253 }
254
255 if ( is_wp_error( $result ) ) {
256 return new Response( false, array( 'message' => $result->get_error_message() ) );
257 }
258 $logs = array();
259 if ( ! empty( $result ) ) {
260 foreach ( $result as $item ) {
261 $logs[] = array_merge(
262 $item->export(),
263 array(
264 'user' => $this->get_user_display( $item->user_id ),
265 'user_url' => (int) $item->user_id > 0 ? get_edit_user_link( $item->user_id ) : '',
266 'log_date' => $this->get_date( $item->timestamp ),
267 'format_date' => $this->format_date_time( $item->timestamp ),
268 )
269 );
270 }
271 }
272 // @since 3.0.0 If no logs then $count = 0.
273 if ( empty( $logs ) ) {
274 $count = 0;
275 } else {
276 $count = Audit_Log::count( $date_from, $date_to, $events, $user_id, $ip_address );
277 }
278 $per_page = 20;
279
280 // Get the count for the submitted data.
281 return new Response(
282 true,
283 array(
284 'logs' => $logs,
285 'total_items' => $count,
286 'total_pages' => ceil( $count / $per_page ),
287 'per_page' => $per_page,
288 )
289 );
290 }
291
292 /**
293 * Generates a human-readable frequency text for audit reports.
294 *
295 * @param Audit_Report $audit_report The audit report object.
296 *
297 * @return string Returns the formatted frequency description.
298 */
299 public function get_frequency_text( Audit_Report $audit_report ): string {
300 $text = '';
301 switch ( $audit_report->frequency ) {
302 case 'daily':
303 $text = ucfirst( $audit_report->day ) . 's at ' . $audit_report->time;
304 break;
305 case 'weekly':
306 case 'monthly':
307 $text = ucfirst( $audit_report->frequency ) . ' on ' . ucfirst( $audit_report->day ) . 's at ' . $audit_report->time;
308 break;
309 default:
310 break;
311 }
312
313 return $text;
314 }
315
316 /**
317 * Enqueues scripts and styles for this page.
318 * Only enqueues assets if the page is active.
319 */
320 public function enqueue_assets() {
321 if ( ! $this->is_page_active() ) {
322 return;
323 }
324 wp_localize_script(
325 'def-audit',
326 'audit',
327 $this->data_frontend()
328 );
329 wp_enqueue_script( 'def-audit' );
330 $this->enqueue_main_assets();
331 }
332
333 /**
334 * Render the root element for frontend.
335 *
336 * @return void
337 */
338 public function main_view(): void {
339 $this->render( 'main' );
340 }
341
342 /**
343 * Provides a summary of audit logs.
344 *
345 * @return void
346 * @throws Exception If there is an error during summary generation.
347 * @defender_route
348 */
349 public function summary(): void {
350 $response = $this->model->is_active() ? $this->summary_data() : array();
351 wp_send_json_success( $response );
352 }
353
354 /**
355 * Returns an array with summary data for audit logging.
356 *
357 * @param bool $for_hub Default 'false' because it's displayed on site summary sections.
358 *
359 * @return array
360 * @throws Exception Emits Exception in case of an error.
361 */
362 public function summary_data( bool $for_hub = false ): array {
363 // Monthly count.
364 $date_from = ( new DateTime( wp_date( 'Y-m-d', strtotime( '-30 days' ) ) ) )
365 ->setTime( 0, 0, 0 )
366 ->getTimestamp();
367 $date_to = ( new DateTime( wp_date( 'Y-m-d' ) ) )->setTime( 23, 59, 59 )->getTimestamp();
368 $month_count = Audit_Log::count( $date_from, $date_to );
369 // Weekly count.
370 $date_from = ( new DateTime( wp_date( 'Y-m-d', strtotime( '-7 days' ) ) ) )
371 ->setTime( 0, 0, 0 )
372 ->getTimestamp();
373 $week_count = Audit_Log::count( $date_from, $date_to );
374 // Daily count. Sync data to the Hub without timezone.
375 $date_from = $for_hub ? new DateTime( 'now' ) : new DateTime( 'now', wp_timezone() );
376 $date_from = $date_from->modify( '-24 hours' )->setTime( 0, 0, 0 )->getTimestamp();
377 $day_count = Audit_Log::count( $date_from, $date_to );
378 // Get the last item.
379 $last = Audit_Log::get_last();
380 if ( is_object( $last ) ) {
381 $last = $for_hub
382 ? $this->persistent_hub_datetime_format( $last->timestamp )
383 : $this->format_date_time( $last->timestamp );
384 } else {
385 $last = 'n/a';
386 }
387
388 return array(
389 'monthCount' => $month_count,
390 'weekCount' => $week_count,
391 'dayCount' => $day_count,
392 'lastEvent' => $last,
393 'report' => wd_di()->get( Audit_Report::class )->to_string(),
394 );
395 }
396
397 /**
398 * Save settings.
399 *
400 * @param Request $request The request object containing new settings data.
401 *
402 * @return Response
403 * @defender_route
404 */
405 public function save_settings( Request $request ): Response {
406 $data = $request->get_data_by_model( $this->model );
407 if ( false === $data['enabled'] && $data['enabled'] !== $this->model->is_active() ) {
408 // Toggle off, so we need to flush everything to cloud.
409 $this->service->flush();
410 }
411
412 $this->model->import( $data );
413 if ( $this->model->validate() ) {
414 $this->model->save();
415 }
416
417 Config_Hub_Helper::set_clear_active_flag();
418
419 return new Response(
420 true,
421 array_merge(
422 $this->data_frontend(),
423 array(
424 'message' => esc_html__( 'Your settings have been updated.', 'defender-security' ),
425 'auto_close' => true,
426 )
427 )
428 );
429 }
430
431 /**
432 * Converts the current state of the object to an array.
433 *
434 * @return array Returns an associative array of object properties.
435 */
436 public function to_array(): array {
437 return array_merge(
438 array(
439 'enabled' => $this->model->is_active(),
440 'report' => true,
441 ),
442 $this->dump_routes_and_nonces()
443 );
444 }
445
446 /**
447 * Removes settings for all submodules.
448 */
449 public function remove_settings(): void {
450 ( new Model_Audit_Logging() )->delete();
451 }
452
453 /**
454 * Delete all the data & the cache.
455 */
456 public function remove_data(): void {
457 Audit_Log::truncate();
458 // Remove cached data.
459 Array_Cache::remove( 'sockets', 'audit' );
460 Array_Cache::remove( 'logs', 'audit' );
461 Array_Cache::remove( 'menu_updated', 'audit' );
462 Array_Cache::remove( 'post_updated', 'audit' );
463 delete_site_option( Audit::CACHE_LAST_CHECKPOINT );
464 }
465
466 /**
467 * Provides data for the frontend.
468 *
469 * @return array An array of data for the frontend.
470 * @throws Exception If there is an error.
471 */
472 public function data_frontend(): array {
473 $logs = array();
474 $count = 0;
475 $per_page = 20;
476 $total_page = 1;
477 if ( $this->model->is_active() ) {
478 $timezone = wp_timezone();
479 $date_from = ( new DateTime() )->setTimezone( $timezone )
480 ->sub( new DateInterval( 'P7D' ) )->setTime( 0, 0, 0 );
481 $date_to = ( new DateTime() )->setTimezone( $timezone )->setTime( 23, 59, 59 );
482 $result = $this->service->fetch(
483 $date_from->getTimestamp(),
484 $date_to->getTimestamp(),
485 array(),
486 '',
487 '',
488 1
489 );
490 if ( ! is_wp_error( $result ) ) {
491 foreach ( $result as $item ) {
492 $logs[] = array_merge(
493 $item->export(),
494 array(
495 'user' => $this->get_user_display( $item->user_id ),
496 'user_url' => (int) $item->user_id > 0 ? get_edit_user_link( $item->user_id ) : '',
497 'log_date' => $this->get_date( $item->timestamp ),
498 'format_date' => $this->format_date_time( $item->timestamp ),
499 )
500 );
501 }
502 $count = Audit_Log::count( $date_from->getTimestamp(), $date_to->getTimestamp() );
503 $total_page = ceil( $count / $per_page );
504 }
505 }
506
507 return array_merge(
508 array(
509 'model' => $this->model->export(),
510 'logs' => $logs,
511 'events_type' => Audit_Log::allowed_events(),
512 'summary' => array(
513 'count_7_days' => $count,
514 'report' => wd_di()->get( Audit_Report::class )->to_string(),
515 ),
516 'paging' => array(
517 'paged' => 1,
518 'total_pages' => $total_page,
519 'count' => $count,
520 ),
521 ),
522 $this->dump_routes_and_nonces()
523 );
524 }
525
526 /**
527 * Imports data into the model.
528 *
529 * @param array $data Data to be imported into the model.
530 *
531 * @throws Exception If table is not defined.
532 */
533 public function import_data( array $data ) {
534 $model = $this->model;
535 if ( empty( $data ) ) {
536 $model->enabled = false;
537 $model->storage_days = '6 months';
538 $model->save();
539 } else {
540 $model->import( $data );
541 if ( $model->validate() ) {
542 $model->save();
543 }
544 }
545 }
546
547 /**
548 * Exports strings.
549 *
550 * @return array An array of strings.
551 */
552 public function export_strings(): array {
553 if ( ! ( new WPMUDEV() )->is_pro() ) {
554 return array(
555 sprintf(
556 /* translators: %s: Html for Pro-tag. */
557 esc_html__( 'Inactive %s', 'defender-security' ),
558 '<span class="sui-tag sui-tag-pro">Pro</span>'
559 ),
560 );
561 }
562
563 if ( $this->model->is_active() ) {
564 $strings = array( esc_html__( 'Active', 'defender-security' ) );
565 $audit_report = new Audit_Report();
566 if ( 'enabled' === $audit_report->status ) {
567 $strings[] = sprintf(
568 /* translators: %s: Frequency value. */
569 esc_html__( 'Email reports sending %s', 'defender-security' ),
570 $audit_report->frequency
571 );
572 }
573 } else {
574 $strings = array( esc_html__( 'Inactive', 'defender-security' ) );
575 }
576
577 return $strings;
578 }
579
580 /**
581 * Generates configuration strings based on the provided configuration and
582 * whether the product is a pro version.
583 *
584 * @param array $config Configuration data.
585 * @param bool $is_pro Indicates if the product is a pro version.
586 *
587 * @return array Returns an array of configuration strings.
588 */
589 public function config_strings( array $config, bool $is_pro ): array {
590 if ( $is_pro ) {
591 if ( $config['enabled'] ) {
592 $strings = array( esc_html__( 'Active', 'defender-security' ) );
593 if ( isset( $config['report'] ) && 'enabled' === $config['report'] ) {
594 $strings[] = sprintf(
595 /* translators: %s: Frequency value. */
596 esc_html__( 'Email reports sending %s', 'defender-security' ),
597 $config['frequency']
598 );
599 }
600 } else {
601 $strings = array( esc_html__( 'Inactive', 'defender-security' ) );
602 }
603 } else {
604 $strings = array(
605 sprintf(
606 /* translators: %s: Html for Pro-tag. */
607 esc_html__( 'Inactive %s', 'defender-security' ),
608 '<span class="sui-tag sui-tag-pro">Pro</span>'
609 ),
610 );
611 }
612
613 return $strings;
614 }
615 }
616