PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.8.1
Defender Security – Malware Scanner, Login Security & Firewall v5.8.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.8.1, at src/controller/class-audit-logging.php

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