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

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

625 lines 17.3 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
326 wp_enqueue_script( 'def-moment', defender_asset_url( '/assets/js/vendor/moment/moment.min.js' ), array(), DEFENDER_VERSION, true );
327 wp_enqueue_script(
328 'def-daterangepicker',
329 defender_asset_url( '/assets/js/vendor/daterangepicker/daterangepicker.js' ),
330 array(),
331 DEFENDER_VERSION,
332 true
333 );
334 wp_localize_script(
335 'def-audit',
336 'audit',
337 $this->data_frontend()
338 );
339 wp_enqueue_script( 'def-audit' );
340 $this->enqueue_main_assets();
341 }
342
343 /**
344 * Render the root element for frontend.
345 *
346 * @return void
347 */
348 public function main_view(): void {
349 $this->render( 'main' );
350 }
351
352 /**
353 * Provides a summary of audit logs.
354 *
355 * @return void
356 * @throws Exception If there is an error during summary generation.
357 * @defender_route
358 */
359 public function summary(): void {
360 $response = $this->model->is_active() ? $this->summary_data() : array();
361 wp_send_json_success( $response );
362 }
363
364 /**
365 * Returns an array with summary data for audit logging.
366 *
367 * @param bool $for_hub Default 'false' because it's displayed on site summary sections.
368 *
369 * @return array
370 * @throws Exception Emits Exception in case of an error.
371 */
372 public function summary_data( bool $for_hub = false ): array {
373 // Monthly count.
374 $date_from = ( new DateTime( wp_date( 'Y-m-d', strtotime( '-30 days' ) ) ) )
375 ->setTime( 0, 0, 0 )
376 ->getTimestamp();
377 $date_to = ( new DateTime( wp_date( 'Y-m-d' ) ) )->setTime( 23, 59, 59 )->getTimestamp();
378 $month_count = Audit_Log::count( $date_from, $date_to );
379 // Weekly count.
380 $date_from = ( new DateTime( wp_date( 'Y-m-d', strtotime( '-7 days' ) ) ) )
381 ->setTime( 0, 0, 0 )
382 ->getTimestamp();
383 $week_count = Audit_Log::count( $date_from, $date_to );
384 // Daily count. Sync data to the Hub without timezone.
385 $date_from = $for_hub ? new DateTime( 'now' ) : new DateTime( 'now', wp_timezone() );
386 $date_from = $date_from->modify( '-24 hours' )->setTime( 0, 0, 0 )->getTimestamp();
387 $day_count = Audit_Log::count( $date_from, $date_to );
388 // Get the last item.
389 $last = Audit_Log::get_last();
390 if ( is_object( $last ) ) {
391 $last = $for_hub
392 ? $this->persistent_hub_datetime_format( $last->timestamp )
393 : $this->format_date_time( $last->timestamp );
394 } else {
395 $last = 'n/a';
396 }
397
398 return array(
399 'monthCount' => $month_count,
400 'weekCount' => $week_count,
401 'dayCount' => $day_count,
402 'lastEvent' => $last,
403 );
404 }
405
406 /**
407 * Save settings.
408 *
409 * @param Request $request The request object containing new settings data.
410 *
411 * @return Response
412 * @defender_route
413 */
414 public function save_settings( Request $request ): Response {
415 $data = $request->get_data_by_model( $this->model );
416 if ( false === $data['enabled'] && $data['enabled'] !== $this->model->is_active() ) {
417 // Toggle off, so we need to flush everything to cloud.
418 $this->service->flush();
419 }
420
421 $this->model->import( $data );
422 if ( $this->model->validate() ) {
423 $this->model->save();
424 }
425
426 Config_Hub_Helper::set_clear_active_flag();
427
428 return new Response(
429 true,
430 array_merge(
431 $this->data_frontend(),
432 array(
433 'message' => esc_html__( 'Your settings have been updated.', 'defender-security' ),
434 'auto_close' => true,
435 )
436 )
437 );
438 }
439
440 /**
441 * Converts the current state of the object to an array.
442 *
443 * @return array Returns an associative array of object properties.
444 */
445 public function to_array(): array {
446 return array_merge(
447 array(
448 'enabled' => $this->model->is_active(),
449 'report' => true,
450 ),
451 $this->dump_routes_and_nonces()
452 );
453 }
454
455 /**
456 * Removes settings for all submodules.
457 */
458 public function remove_settings(): void {
459 ( new Model_Audit_Logging() )->delete();
460 }
461
462 /**
463 * Delete all the data & the cache.
464 */
465 public function remove_data(): void {
466 Audit_Log::truncate();
467 // Remove cached data.
468 Array_Cache::remove( 'sockets', 'audit' );
469 Array_Cache::remove( 'logs', 'audit' );
470 Array_Cache::remove( 'menu_updated', 'audit' );
471 Array_Cache::remove( 'post_updated', 'audit' );
472 delete_site_option( Audit::CACHE_LAST_CHECKPOINT );
473 }
474
475 /**
476 * Provides data for the frontend.
477 *
478 * @return array An array of data for the frontend.
479 * @throws Exception If there is an error.
480 */
481 public function data_frontend(): array {
482 $logs = array();
483 $count = 0;
484 $per_page = 20;
485 $total_page = 1;
486 if ( $this->model->is_active() ) {
487 $timezone = wp_timezone();
488 $date_from = ( new DateTime() )->setTimezone( $timezone )
489 ->sub( new DateInterval( 'P7D' ) )->setTime( 0, 0, 0 );
490 $date_to = ( new DateTime() )->setTimezone( $timezone )->setTime( 23, 59, 59 );
491 $result = $this->service->fetch(
492 $date_from->getTimestamp(),
493 $date_to->getTimestamp(),
494 array(),
495 '',
496 '',
497 1
498 );
499 if ( ! is_wp_error( $result ) ) {
500 foreach ( $result as $item ) {
501 $logs[] = array_merge(
502 $item->export(),
503 array(
504 'user' => $this->get_user_display( $item->user_id ),
505 'user_url' => (int) $item->user_id > 0 ? get_edit_user_link( $item->user_id ) : '',
506 'log_date' => $this->get_date( $item->timestamp ),
507 'format_date' => $this->format_date_time( $item->timestamp ),
508 )
509 );
510 }
511 $count = Audit_Log::count( $date_from->getTimestamp(), $date_to->getTimestamp() );
512 $total_page = ceil( $count / $per_page );
513 }
514 }
515
516 return array_merge(
517 array(
518 'model' => $this->model->export(),
519 'logs' => $logs,
520 'events_type' => Audit_Log::allowed_events(),
521 'summary' => array(
522 'count_7_days' => $count,
523 'report' => wd_di()->get( Audit_Report::class )->to_string(),
524 ),
525 'paging' => array(
526 'paged' => 1,
527 'total_pages' => $total_page,
528 'count' => $count,
529 ),
530 ),
531 $this->dump_routes_and_nonces()
532 );
533 }
534
535 /**
536 * Imports data into the model.
537 *
538 * @param array $data Data to be imported into the model.
539 *
540 * @throws Exception If table is not defined.
541 */
542 public function import_data( array $data ) {
543 $model = $this->model;
544 if ( empty( $data ) ) {
545 $model->enabled = false;
546 $model->storage_days = '6 months';
547 $model->save();
548 } else {
549 $model->import( $data );
550 if ( $model->validate() ) {
551 $model->save();
552 }
553 }
554 }
555
556 /**
557 * Exports strings.
558 *
559 * @return array An array of strings.
560 */
561 public function export_strings(): array {
562 if ( ! ( new WPMUDEV() )->is_pro() ) {
563 return array(
564 sprintf(
565 /* translators: %s: Html for Pro-tag. */
566 esc_html__( 'Inactive %s', 'defender-security' ),
567 '<span class="sui-tag sui-tag-pro">Pro</span>'
568 ),
569 );
570 }
571
572 if ( $this->model->is_active() ) {
573 $strings = array( esc_html__( 'Active', 'defender-security' ) );
574 $audit_report = new Audit_Report();
575 if ( 'enabled' === $audit_report->status ) {
576 $strings[] = sprintf(
577 /* translators: %s: Frequency value. */
578 esc_html__( 'Email reports sending %s', 'defender-security' ),
579 $audit_report->frequency
580 );
581 }
582 } else {
583 $strings = array( esc_html__( 'Inactive', 'defender-security' ) );
584 }
585
586 return $strings;
587 }
588
589 /**
590 * Generates configuration strings based on the provided configuration and
591 * whether the product is a pro version.
592 *
593 * @param array $config Configuration data.
594 * @param bool $is_pro Indicates if the product is a pro version.
595 *
596 * @return array Returns an array of configuration strings.
597 */
598 public function config_strings( array $config, bool $is_pro ): array {
599 if ( $is_pro ) {
600 if ( $config['enabled'] ) {
601 $strings = array( esc_html__( 'Active', 'defender-security' ) );
602 if ( isset( $config['report'] ) && 'enabled' === $config['report'] ) {
603 $strings[] = sprintf(
604 /* translators: %s: Frequency value. */
605 esc_html__( 'Email reports sending %s', 'defender-security' ),
606 $config['frequency']
607 );
608 }
609 } else {
610 $strings = array( esc_html__( 'Inactive', 'defender-security' ) );
611 }
612 } else {
613 $strings = array(
614 sprintf(
615 /* translators: %s: Html for Pro-tag. */
616 esc_html__( 'Inactive %s', 'defender-security' ),
617 '<span class="sui-tag sui-tag-pro">Pro</span>'
618 ),
619 );
620 }
621
622 return $strings;
623 }
624 }
625