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 / controller / class-audit-logging.php

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

616 lines 17.0 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 ( ! wp_next_scheduled( 'audit_clean_up_logs' ) ) {
85 wp_schedule_event( time(), 'hourly', 'audit_clean_up_logs' );
86 }
87 add_action( 'audit_clean_up_logs', array( $this, 'clean_up_audit_logs' ) );
88 }
89 }
90
91 /**
92 * Sync all the events into cloud, this will happen per hourly basis.
93 *
94 * @return void
95 */
96 public function sync_events(): void {
97 $this->service->flush();
98 }
99
100 /**
101 * Clean up all the old logs from the local storage, this will happen per hourly basis.
102 *
103 * @return void
104 * @throws Exception When the $duration cannot be parsed as an interval.
105 */
106 public function clean_up_audit_logs(): void {
107 $this->service->audit_clean_up_logs();
108 }
109
110 /**
111 * Exports audit logs as a CSV file.
112 *
113 * @return void
114 * @throws Exception If there is an error during export.
115 * @defender_route
116 */
117 public function export_as_csv(): void {
118 $date_from = HTTP::get(
119 'date_from',
120 wp_date( 'Y-m-d H:i:s', strtotime( '-7 days', time() ) )
121 );
122 $date_to = HTTP::get( 'date_to', wp_date( 'Y-m-d H:i:s', time() ) );
123 // Convert date using timezone.
124 $timezone = wp_timezone();
125 $date_from = ( new DateTime( $date_from, $timezone ) )->setTime( 0, 0, 0 )->getTimestamp();
126 $date_to = ( new DateTime( $date_to, $timezone ) )->setTime( 23, 59, 59 )->getTimestamp();
127 $username = HTTP::get( 'term', '' );
128 $user_id = '';
129 $user = get_user_by( 'login', $username );
130 $events = HTTP::get( 'event_type', array() );
131 if ( is_object( $user ) ) {
132 $user_id = $user->ID;
133 }
134
135 $handler = new Audit();
136 $ip_address = HTTP::get( 'ip_address', '' );
137 $result = $handler->fetch( $date_from, $date_to, $events, $user_id, $ip_address, false );
138 // WP_Filesystem class doesn’t directly provide a function for opening a stream to php://memory with the 'w' mode.
139 $fp = fopen( 'php://memory', 'w' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
140 $headers = array(
141 esc_html__( 'Summary', 'defender-security' ),
142 esc_html__( 'Date / Time', 'defender-security' ),
143 esc_html__( 'Context', 'defender-security' ),
144 esc_html__( 'Type', 'defender-security' ),
145 esc_html__( 'IP address', 'defender-security' ),
146 esc_html__( 'User', 'defender-security' ),
147 );
148 fputcsv( $fp, $headers, ',', '"', '\\' );
149 foreach ( $result as $log ) {
150 $fields = $log->export();
151 $vars = array(
152 $fields['msg'],
153 is_array( $fields['timestamp'] )
154 ? $this->format_date_time( $fields['timestamp'][0] )
155 : $this->format_date_time( $fields['timestamp'] ),
156 $fields['context'],
157 $fields['action_type'],
158 $fields['ip'],
159 $this->get_user_display( $fields['user_id'] ),
160 );
161 fputcsv( $fp, $vars, ',', '"', '\\' );
162 }
163 $filename = 'wdf-audit-logs-export-' . wp_date( 'ymdHis' ) . '.csv';
164 fseek( $fp, 0 );
165 header( 'Content-Type: text/csv' );
166 header( 'Content-Disposition: attachment; filename="' . $filename . '";' );
167 // Make php send the generated csv lines to the browser.
168 fpassthru( $fp );
169 exit();
170 }
171
172 /**
173 * We'll pass all the event logs into the db handler, so it writes down to db.
174 * Do it in shutdown runtime, so no delay time.
175 *
176 * @return void
177 */
178 public function cache_audit_logs(): void {
179 $audit = new Audit();
180 $audit->log_audit_events();
181 }
182
183 /**
184 * Pull the logs from db cached:
185 * - date_from: the start of the date we will run the query, as mysql time format,
186 * - date_to: similar to the above,
187 * others will refer to Audit.
188 *
189 * @param Request $request The request object containing filter parameters.
190 *
191 * @return Response
192 * @throws Exception If there is an error during log retrieval.
193 * @defender_route
194 */
195 public function pull_logs( Request $request ): Response {
196 $data = $request->get_data(
197 array(
198 'date_from' => array(
199 'type' => 'string',
200 'sanitize' => 'sanitize_text_field',
201 ),
202 'date_to' => array(
203 'type' => 'string',
204 'sanitize' => 'sanitize_text_field',
205 ),
206 'username' => array(
207 'type' => 'string',
208 'sanitize' => 'sanitize_text_field',
209 ),
210 'events' => array(
211 'type' => 'array',
212 'sanitize' => 'sanitize_text_field',
213 ),
214 'ip_address' => array(
215 'type' => 'string',
216 'sanitize' => 'sanitize_text_field',
217 ),
218 'paged' => array(
219 'type' => 'int',
220 'sanitize' => 'sanitize_text_field',
221 ),
222 )
223 );
224 if ( empty( $data['date_from'] ) || empty( $data['date_to'] ) ) {
225 return new Response( false, array( 'message' => esc_html__( 'Invalid data.', 'defender-security' ) ) );
226 }
227 // Convert date using timezone.
228 $timezone = wp_timezone();
229 $date_from = ( new DateTime( $data['date_from'], $timezone ) )
230 ->setTime( 0, 0, 0 )
231 ->getTimestamp();
232 $date_to = ( new DateTime( $data['date_to'], $timezone ) )
233 ->setTime( 23, 59, 59 )
234 ->getTimestamp();
235
236 $events = $data['events'] ?? array();
237 $ip_address = $data['ip_address'] ?? '';
238 $paged = $data['paged'] ?? 1;
239 $username = $data['username'] ?? '';
240 $user_id = '';
241 if ( ! empty( $username ) ) {
242 $user = get_user_by( 'login', $username );
243 if ( is_object( $user ) ) {
244 $user_id = $user->ID;
245 // Fetch result with the specified user.
246 $result = $this->service->fetch( $date_from, $date_to, $events, $user_id, $ip_address, $paged );
247 } else {
248 // A non-existent username.
249 $result = array();
250 }
251 } else {
252 // Fetch result with empty user field.
253 $result = $this->service->fetch( $date_from, $date_to, $events, $user_id, $ip_address, $paged );
254 }
255
256 if ( is_wp_error( $result ) ) {
257 return new Response( false, array( 'message' => $result->get_error_message() ) );
258 }
259 $logs = array();
260 if ( ! empty( $result ) ) {
261 foreach ( $result as $item ) {
262 $logs[] = array_merge(
263 $item->export(),
264 array(
265 'user' => $this->get_user_display( $item->user_id ),
266 'user_url' => (int) $item->user_id > 0 ? get_edit_user_link( $item->user_id ) : '',
267 'log_date' => $this->get_date( $item->timestamp ),
268 'format_date' => $this->format_date_time( $item->timestamp ),
269 )
270 );
271 }
272 }
273 // @since 3.0.0 If no logs then $count = 0.
274 if ( empty( $logs ) ) {
275 $count = 0;
276 } else {
277 $count = Audit_Log::count( $date_from, $date_to, $events, $user_id, $ip_address );
278 }
279 $per_page = 20;
280
281 // Get the count for the submitted data.
282 return new Response(
283 true,
284 array(
285 'logs' => $logs,
286 'total_items' => $count,
287 'total_pages' => ceil( $count / $per_page ),
288 'per_page' => $per_page,
289 )
290 );
291 }
292
293 /**
294 * Generates a human-readable frequency text for audit reports.
295 *
296 * @param Audit_Report $audit_report The audit report object.
297 *
298 * @return string Returns the formatted frequency description.
299 */
300 public function get_frequency_text( Audit_Report $audit_report ): string {
301 $text = '';
302 switch ( $audit_report->frequency ) {
303 case 'daily':
304 $text = ucfirst( $audit_report->day ) . 's at ' . $audit_report->time;
305 break;
306 case 'weekly':
307 case 'monthly':
308 $text = ucfirst( $audit_report->frequency ) . ' on ' . ucfirst( $audit_report->day ) . 's at ' . $audit_report->time;
309 break;
310 default:
311 break;
312 }
313
314 return $text;
315 }
316
317 /**
318 * Enqueues scripts and styles for this page.
319 * Only enqueues assets if the page is active.
320 */
321 public function enqueue_assets() {
322 if ( ! $this->is_page_active() ) {
323 return;
324 }
325 wp_localize_script(
326 'def-audit',
327 'audit',
328 $this->data_frontend()
329 );
330 wp_enqueue_script( 'def-audit' );
331 $this->enqueue_main_assets();
332 }
333
334 /**
335 * Render the root element for frontend.
336 *
337 * @return void
338 */
339 public function main_view(): void {
340 $this->render( 'main' );
341 }
342
343 /**
344 * Provides a summary of audit logs.
345 *
346 * @return void
347 * @throws Exception If there is an error during summary generation.
348 * @defender_route
349 */
350 public function summary(): void {
351 $response = $this->model->is_active() ? $this->summary_data() : array();
352 wp_send_json_success( $response );
353 }
354
355 /**
356 * Returns an array with summary data for audit logging.
357 *
358 * @param bool $for_hub Default 'false' because it's displayed on site summary sections.
359 *
360 * @return array
361 * @throws Exception Emits Exception in case of an error.
362 */
363 public function summary_data( bool $for_hub = false ): array {
364 // Monthly count.
365 $date_from = ( new DateTime( wp_date( 'Y-m-d', strtotime( '-30 days' ) ) ) )
366 ->setTime( 0, 0, 0 )
367 ->getTimestamp();
368 $date_to = ( new DateTime( wp_date( 'Y-m-d' ) ) )->setTime( 23, 59, 59 )->getTimestamp();
369 $month_count = Audit_Log::count( $date_from, $date_to );
370 // Weekly count.
371 $date_from = ( new DateTime( wp_date( 'Y-m-d', strtotime( '-7 days' ) ) ) )
372 ->setTime( 0, 0, 0 )
373 ->getTimestamp();
374 $week_count = Audit_Log::count( $date_from, $date_to );
375 // Daily count. Sync data to the Hub without timezone.
376 $date_from = $for_hub ? new DateTime( 'now' ) : new DateTime( 'now', wp_timezone() );
377 $date_from = $date_from->modify( '-24 hours' )->setTime( 0, 0, 0 )->getTimestamp();
378 $day_count = Audit_Log::count( $date_from, $date_to );
379 // Get the last item.
380 $last = Audit_Log::get_last();
381 if ( is_object( $last ) ) {
382 $last = $for_hub
383 ? $this->persistent_hub_datetime_format( $last->timestamp )
384 : $this->format_date_time( $last->timestamp );
385 } else {
386 $last = 'n/a';
387 }
388
389 return array(
390 'monthCount' => $month_count,
391 'weekCount' => $week_count,
392 'dayCount' => $day_count,
393 'lastEvent' => $last,
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