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

604 lines 16.8 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 ( empty( $data['date_from'] ) || empty( $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 ( ! empty( $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 ( ! empty( $result ) ) {
246 foreach ( $result as $item ) {
247 $logs[] = array_merge(
248 $item->export(),
249 array(
250 'user' => $this->get_user_display( $item->user_id ),
251 'user_url' => (int) $item->user_id > 0 ? get_edit_user_link( $item->user_id ) : '',
252 'log_date' => $this->get_date( $item->timestamp ),
253 'format_date' => $this->format_date_time( $item->timestamp ),
254 )
255 );
256 }
257 }
258 // @since 3.0.0 If no logs then $count = 0.
259 if ( empty( $logs ) ) {
260 $count = 0;
261 } else {
262 $count = Audit_Log::count( $date_from, $date_to, $events, $user_id, $ip_address );
263 }
264 $per_page = 20;
265
266 // Get the count for the submitted data.
267 return new Response(
268 true,
269 array(
270 'logs' => $logs,
271 'total_items' => $count,
272 'total_pages' => ceil( $count / $per_page ),
273 'per_page' => $per_page,
274 )
275 );
276 }
277
278 /**
279 * Generates a human-readable frequency text for audit reports.
280 *
281 * @param Audit_Report $audit_report The audit report object.
282 *
283 * @return string Returns the formatted frequency description.
284 */
285 public function get_frequency_text( Audit_Report $audit_report ): string {
286 $text = '';
287 switch ( $audit_report->frequency ) {
288 case 'daily':
289 $text = ucfirst( $audit_report->day ) . 's at ' . $audit_report->time;
290 break;
291 case 'weekly':
292 case 'monthly':
293 $text = ucfirst( $audit_report->frequency ) . ' on ' . ucfirst( $audit_report->day ) . 's at ' . $audit_report->time;
294 break;
295 default:
296 break;
297 }
298
299 return $text;
300 }
301
302 /**
303 * Enqueues scripts and styles for this page.
304 * Only enqueues assets if the page is active.
305 */
306 public function enqueue_assets() {
307 if ( ! $this->is_page_active() ) {
308 return;
309 }
310 wp_localize_script(
311 'def-audit',
312 'audit',
313 $this->data_frontend()
314 );
315 wp_enqueue_script( 'def-audit' );
316 $this->enqueue_main_assets();
317 }
318
319 /**
320 * Render the root element for frontend.
321 *
322 * @return void
323 */
324 public function main_view(): void {
325 $this->render( 'main' );
326 }
327
328 /**
329 * Provides a summary of audit logs.
330 *
331 * @return void
332 * @throws Exception If there is an error during summary generation.
333 * @defender_route
334 */
335 public function summary(): void {
336 $response = $this->model->is_active() ? $this->summary_data() : array();
337 wp_send_json_success( $response );
338 }
339
340 /**
341 * Returns an array with summary data for audit logging.
342 *
343 * @param bool $for_hub Default 'false' because it's displayed on site summary sections.
344 *
345 * @return array
346 * @throws Exception Emits Exception in case of an error.
347 */
348 public function summary_data( bool $for_hub = false ): array {
349 // Monthly count.
350 $date_from = ( new DateTime( wp_date( 'Y-m-d', strtotime( '-30 days' ) ) ) )
351 ->setTime( 0, 0, 0 )
352 ->getTimestamp();
353 $date_to = ( new DateTime( wp_date( 'Y-m-d' ) ) )->setTime( 23, 59, 59 )->getTimestamp();
354 $month_count = Audit_Log::count( $date_from, $date_to );
355 // Weekly count.
356 $date_from = ( new DateTime( wp_date( 'Y-m-d', strtotime( '-7 days' ) ) ) )
357 ->setTime( 0, 0, 0 )
358 ->getTimestamp();
359 $week_count = Audit_Log::count( $date_from, $date_to );
360 // Daily count. Sync data to the Hub without timezone.
361 $date_from = $for_hub ? new DateTime( 'now' ) : new DateTime( 'now', wp_timezone() );
362 $date_from = $date_from->modify( '-24 hours' )->setTime( 0, 0, 0 )->getTimestamp();
363 $day_count = Audit_Log::count( $date_from, $date_to );
364 // Get the last item.
365 $last = Audit_Log::get_last();
366 if ( is_object( $last ) ) {
367 $last = $for_hub
368 ? $this->persistent_hub_datetime_format( $last->timestamp )
369 : $this->format_date_time( $last->timestamp );
370 } else {
371 $last = 'n/a';
372 }
373
374 return array(
375 'monthCount' => $month_count,
376 'weekCount' => $week_count,
377 'dayCount' => $day_count,
378 'lastEvent' => $last,
379 'report' => wd_di()->get( Audit_Report::class )->to_string(),
380 );
381 }
382
383 /**
384 * Save settings.
385 *
386 * @param Request $request The request object containing new settings data.
387 *
388 * @return Response
389 * @defender_route
390 */
391 public function save_settings( Request $request ): Response {
392 $data = $request->get_data_by_model( $this->model );
393 if ( false === $data['enabled'] && $data['enabled'] !== $this->model->is_active() ) {
394 // Toggle off, so we need to flush everything to cloud.
395 $this->service->flush();
396 }
397
398 $this->model->import( $data );
399 if ( $this->model->validate() ) {
400 $this->model->save();
401 }
402
403 Config_Hub_Helper::set_clear_active_flag();
404
405 return new Response(
406 true,
407 array_merge(
408 $this->data_frontend(),
409 array(
410 'message' => esc_html__( 'Your settings have been updated.', 'defender-security' ),
411 'auto_close' => true,
412 )
413 )
414 );
415 }
416
417 /**
418 * Converts the current state of the object to an array.
419 *
420 * @return array Returns an associative array of object properties.
421 */
422 public function to_array(): array {
423 return array_merge(
424 array(
425 'enabled' => $this->model->is_active(),
426 'report' => true,
427 ),
428 $this->dump_routes_and_nonces()
429 );
430 }
431
432 /**
433 * Removes settings for all submodules.
434 */
435 public function remove_settings(): void {
436 ( new Model_Audit_Logging() )->delete();
437 }
438
439 /**
440 * Delete all the data & the cache.
441 */
442 public function remove_data(): void {
443 Audit_Log::truncate();
444 // Remove cached data.
445 Array_Cache::remove( 'sockets', 'audit' );
446 Array_Cache::remove( 'logs', 'audit' );
447 Array_Cache::remove( 'menu_updated', 'audit' );
448 Array_Cache::remove( 'post_updated', 'audit' );
449 delete_site_option( Audit::CACHE_LAST_CHECKPOINT );
450 }
451
452 /**
453 * Provides data for the frontend.
454 *
455 * @return array An array of data for the frontend.
456 * @throws Exception If there is an error.
457 */
458 public function data_frontend(): array {
459 $logs = array();
460 $count = 0;
461 $per_page = 20;
462 $total_page = 1;
463 if ( $this->model->is_active() ) {
464 $timezone = wp_timezone();
465 $date_from = ( new DateTime() )->setTimezone( $timezone )
466 ->sub( new DateInterval( 'P7D' ) )->setTime( 0, 0, 0 );
467 $date_to = ( new DateTime() )->setTimezone( $timezone )->setTime( 23, 59, 59 );
468 $result = $this->service->fetch(
469 $date_from->getTimestamp(),
470 $date_to->getTimestamp(),
471 array(),
472 '',
473 '',
474 1
475 );
476 if ( ! is_wp_error( $result ) ) {
477 foreach ( $result as $item ) {
478 $logs[] = array_merge(
479 $item->export(),
480 array(
481 'user' => $this->get_user_display( $item->user_id ),
482 'user_url' => (int) $item->user_id > 0 ? get_edit_user_link( $item->user_id ) : '',
483 'log_date' => $this->get_date( $item->timestamp ),
484 'format_date' => $this->format_date_time( $item->timestamp ),
485 )
486 );
487 }
488 $count = Audit_Log::count( $date_from->getTimestamp(), $date_to->getTimestamp() );
489 $total_page = ceil( $count / $per_page );
490 }
491 }
492
493 return array_merge(
494 array(
495 'model' => $this->model->export(),
496 'logs' => $logs,
497 'events_type' => Audit_Log::allowed_events(),
498 'summary' => array(
499 'count_7_days' => $count,
500 'report' => wd_di()->get( Audit_Report::class )->to_string(),
501 ),
502 'paging' => array(
503 'paged' => 1,
504 'total_pages' => $total_page,
505 'count' => $count,
506 ),
507 'hub_connector' => wd_di()->get( Hub_Connector::class )->data_frontend(),
508 'antibot' => wd_di()->get( Antibot_Global_Firewall::class )->data_frontend(),
509 ),
510 $this->dump_routes_and_nonces()
511 );
512 }
513
514 /**
515 * Imports data into the model.
516 *
517 * @param array $data Data to be imported into the model.
518 *
519 * @throws Exception If table is not defined.
520 */
521 public function import_data( array $data ) {
522 $model = $this->model;
523 if ( empty( $data ) ) {
524 $model->enabled = false;
525 $model->storage_days = '6 months';
526 $model->save();
527 } else {
528 $model->import( $data );
529 if ( $model->validate() ) {
530 $model->save();
531 }
532 }
533 }
534
535 /**
536 * Exports strings.
537 *
538 * @return array An array of strings.
539 */
540 public function export_strings(): array {
541 if ( ! ( new WPMUDEV() )->is_pro() ) {
542 return array(
543 sprintf(
544 /* translators: %s: Html for Pro-tag. */
545 esc_html__( 'Inactive %s', 'defender-security' ),
546 '<span class="sui-tag sui-tag-pro">Pro</span>'
547 ),
548 );
549 }
550
551 if ( $this->model->is_active() ) {
552 $strings = array( esc_html__( 'Active', 'defender-security' ) );
553 $audit_report = new Audit_Report();
554 if ( 'enabled' === $audit_report->status ) {
555 $strings[] = sprintf(
556 /* translators: %s: Frequency value. */
557 esc_html__( 'Email reports sending %s', 'defender-security' ),
558 $audit_report->frequency
559 );
560 }
561 } else {
562 $strings = array( esc_html__( 'Inactive', 'defender-security' ) );
563 }
564
565 return $strings;
566 }
567
568 /**
569 * Generates configuration strings based on the provided configuration and
570 * whether the product is a pro version.
571 *
572 * @param array $config Configuration data.
573 * @param bool $is_pro Indicates if the product is a pro version.
574 *
575 * @return array Returns an array of configuration strings.
576 */
577 public function config_strings( array $config, bool $is_pro ): array {
578 if ( $is_pro ) {
579 if ( $config['enabled'] ) {
580 $strings = array( esc_html__( 'Active', 'defender-security' ) );
581 if ( isset( $config['report'] ) && 'enabled' === $config['report'] ) {
582 $strings[] = sprintf(
583 /* translators: %s: Frequency value. */
584 esc_html__( 'Email reports sending %s', 'defender-security' ),
585 $config['frequency']
586 );
587 }
588 } else {
589 $strings = array( esc_html__( 'Inactive', 'defender-security' ) );
590 }
591 } else {
592 $strings = array(
593 sprintf(
594 /* translators: %s: Html for Pro-tag. */
595 esc_html__( 'Inactive %s', 'defender-security' ),
596 '<span class="sui-tag sui-tag-pro">Pro</span>'
597 ),
598 );
599 }
600
601 return $strings;
602 }
603 }
604