| Current Path : /home/flapst5/ekmtoronto.com/wp-content/plugins/wp-defender/src/controller/ |
| Current File : /home/flapst5/ekmtoronto.com/wp-content/plugins/wp-defender/src/controller/class-audit-logging.php |
<?php
/**
* Handle Audit Logging module.
*
* @package WP_Defender\Controller
*/
namespace WP_Defender\Controller;
use DateTime;
use Exception;
use DateInterval;
use WP_Defender\Event;
use Calotes\Helper\HTTP;
use WP_Defender\Traits\User;
use Calotes\Component\Request;
use Calotes\Component\Response;
use Calotes\Helper\Array_Cache;
use WP_Defender\Traits\Formats;
use WP_Defender\Component\Audit;
use WP_Defender\Model\Audit_Log;
use WP_Defender\Behavior\WPMUDEV;
use WP_Defender\Component\Network_Cron_Manager;
use WP_Defender\Model\Notification\Audit_Report;
use WP_Defender\Component\Config\Config_Hub_Helper;
use WP_Defender\Model\Setting\Audit_Logging as Model_Audit_Logging;
/**
* Handle Audit Logging module.
*/
class Audit_Logging extends Event {
public const DEFAULT_PER_PAGE = 10;
use User;
use Formats;
/**
* The slug identifier for this controller.
*
* @var string
*/
public $slug = 'wdf-logging';
/**
* The model for handling the data.
*
* @var Model_Audit_Logging
*/
public $model;
/**
* Service for handling logic.
*
* @var Audit|null
*/
public ?Audit $service;
/**
* Indicates whether the current installation is a pro version.
*
* @var bool
*/
private $is_pro;
/**
* Initializes the model and service, registers routes, and sets up scheduled events if the model is active.
*/
public function __construct() {
$this->register_page(
esc_html__( 'Audit Log', 'wpdef' ),
$this->slug,
array( $this, 'main_view' ),
$this->parent_slug
);
add_action( 'defender_enqueue_assets', array( $this, 'enqueue_assets' ) );
$this->model = wd_di()->get( Model_Audit_Logging::class );
$this->service = new Audit();
$this->register_routes();
$this->is_pro = wd_di()->get( WPMUDEV::class )->is_pro();
if ( $this->is_active() ) {
$this->service->enqueue_event_listener();
add_action( 'shutdown', array( $this, 'cache_audit_logs' ) );
/**
* Network Cron Manager
*
* @var Network_Cron_Manager $network_cron_manager
*/
$network_cron_manager = wd_di()->get( Network_Cron_Manager::class );
$network_cron_manager->register_callback(
'audit_clean_up_logs',
array( $this->service, 'audit_clean_up_logs' ),
HOUR_IN_SECONDS
);
if ( $this->is_pro ) {
$network_cron_manager->register_callback(
'audit_sync_events',
array( $this->service, 'flush' ),
HOUR_IN_SECONDS,
time() + 15
);
}
}
}
/**
* Is there permission to Pro feature?
*
* @return bool
*/
private function is_active(): bool {
return $this->model->is_active();
}
/**
* Parses a raw request params array, resolves filters, and fetches audit logs.
*
* Accepted keys (all optional except date range):
* date_from — date string or empty (defaults to -7 days midnight).
* date_to — date string or empty (defaults to today 23:59:59).
* events — array of event type strings (also accepts key 'event_type' for CSV route).
* username — login name (also accepts key 'term' for CSV route).
* ip_address — IP filter string.
* paged — page number; pass false for unbounded export.
*
* Returns the resolved user_id via the $user_id out-param so callers can use it for COUNT queries.
*
* @param array $params Raw request data.
* @param mixed $paged Page number or false for all results.
* @param int|string $user_id Populated with the resolved user ID (out-param).
* @param int $per_page Number of logs per page.
*
* @return array|\WP_Error
*/
private function fetch_logs( array $params, $paged, &$user_id = '', $per_page = self::DEFAULT_PER_PAGE ) {
$date_from_str = $params['date_from'] ?? '';
$date_to_str = $params['date_to'] ?? '';
$date_from = $date_from_str ? $this->date_string_to_timestamp( $date_from_str ) : strtotime( '-7 days midnight' );
$date_to = $date_to_str ? $this->date_string_to_timestamp( $date_to_str, true ) : strtotime( 'today 23:59:59' );
$events = $params['events'] ?? ( isset( $params['event_type'] ) && is_array( $params['event_type'] ) ? $params['event_type'] : array() );
$username = $params['username'] ?? ( $params['term'] ?? '' );
$ip_address = $params['ip_address'] ?? '';
$user_id = '';
if ( '' !== $username ) {
$user = get_user_by( 'login', $username );
if ( ! is_object( $user ) ) {
// Try matching by display_name or email before giving up.
$users = get_users(
array(
'search' => '*' . $username . '*',
'search_columns' => array( 'display_name' ),
'number' => 1,
)
);
if ( array() === $users ) {
return array();
}
$user_id = $users[0]->ID;
} else {
$user_id = $user->ID;
}
}
return $this->service->fetch( $date_from, $date_to, $events, $user_id, $ip_address, $paged, $per_page );
}
/**
* Prefetches users from a result set then formats each log into an array.
*
* @param array $result Array of Audit_Log objects.
*
* @return array
*/
private function format_log_rows( array $result ): array {
$user_ids = array_filter(
array_unique( array_column( $result, 'user_id' ) ),
static fn ( $user_id ): bool => (int) $user_id > 0
);
if ( array() !== $user_ids ) {
get_users( array( 'include' => $user_ids ) );
}
$logs = array();
foreach ( $result as $item ) {
$item_user_id = isset( $item->user_id ) ? $item->user_id : 0;
$item_user_id = ! is_int( $item_user_id ) ? (int) $item_user_id : $item_user_id;
$logs[] = array_merge(
$item->export(),
array(
'user' => $this->get_user_display( $item->user_id ),
'user_url' => $item_user_id > 0 ? get_edit_user_link( $item_user_id ) : '',
'log_date' => $this->get_date( $item->timestamp ),
'format_date' => $this->format_date_time( $item->timestamp ),
)
);
}
return $logs;
}
/**
* Exports audit logs as a CSV file.
*
* @return void
* @throws Exception If there is an error during export.
* @defender_route
*/
public function export_as_csv(): void {
$result = $this->fetch_logs( defender_get_data_from_request( null, 'g' ), false );
if ( is_wp_error( $result ) ) {
wp_send_json_error(
array(
'message' => $result->get_error_message(),
)
);
}
// WP_Filesystem class doesn't directly provide a function for opening a stream to php://memory with the 'w' mode.
$fp = fopen( 'php://memory', 'w' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
$headers = array(
esc_html__( 'Summary', 'wpdef' ),
esc_html__( 'Date / Time', 'wpdef' ),
esc_html__( 'Context', 'wpdef' ),
esc_html__( 'Type', 'wpdef' ),
esc_html__( 'IP address', 'wpdef' ),
esc_html__( 'User', 'wpdef' ),
);
fputcsv( $fp, $headers, ',', '"', '\\' );
foreach ( $this->format_log_rows( $result ) as $row ) {
$vars = array(
$row['msg'],
is_array( $row['timestamp'] )
? $this->format_date_time( $row['timestamp'][0] )
: $this->format_date_time( $row['timestamp'] ),
$row['context'],
$row['action_type'],
$row['ip'],
$row['user'],
);
fputcsv( $fp, $vars, ',', '"', '\\' );
}
$filename = 'wdf-audit-logs-export-' . wp_date( 'ymdHis' ) . '.csv';
fseek( $fp, 0 );
header( 'Content-Type: text/csv' );
header( 'Content-Disposition: attachment; filename="' . $filename . '";' );
// Make php send the generated csv lines to the browser.
fpassthru( $fp );
exit();
}
/**
* We'll pass all the event logs into the db handler, so it writes down to db.
* Do it in shutdown runtime, so no delay time.
*
* @return void
*/
public function cache_audit_logs(): void {
$audit = new Audit();
$audit->log_audit_events();
}
/**
* Pull the logs from db cached:
* - date_from: the start of the date we will run the query, as mysql time format,
* - date_to: similar to the above,
* others will refer to Audit.
*
* @param Request $request The request object containing filter parameters.
*
* @return Response
* @throws Exception If there is an error during log retrieval.
* @defender_route
*/
public function pull_logs( Request $request ): Response {
$data = $request->get_data(
array(
'date_from' => array(
'type' => 'string',
'sanitize' => 'sanitize_text_field',
),
'date_to' => array(
'type' => 'string',
'sanitize' => 'sanitize_text_field',
),
'username' => array(
'type' => 'string',
'sanitize' => 'sanitize_text_field',
),
'events' => array(
'type' => 'array',
'sanitize' => 'sanitize_text_field',
),
'ip_address' => array(
'type' => 'string',
'sanitize' => 'sanitize_text_field',
),
'paged' => array(
'type' => 'int',
'sanitize' => 'sanitize_text_field',
),
'per_page' => array(
'type' => 'int',
'sanitize' => 'sanitize_text_field',
),
)
);
if ( ! isset( $data['date_from'] ) || ! isset( $data['date_to'] ) ) {
return new Response( false, array( 'message' => esc_html__( 'Invalid data.', 'wpdef' ) ) );
}
// Validate date strings before passing to fetch_logs.
$date_from_check = $this->date_string_to_timestamp( $data['date_from'] );
$date_to_check = $this->date_string_to_timestamp( $data['date_to'], true );
if ( $date_from_check <= 0 || $date_to_check <= 0 ) {
return new Response( false, array( 'message' => esc_html__( 'Invalid data.', 'wpdef' ) ) );
}
$paged = $data['paged'] ?? 1;
$per_page = min( 100, max( 1, (int) ( $data['per_page'] ?? self::DEFAULT_PER_PAGE ) ) );
$result = $this->fetch_logs( $data, $paged, $user_id, $per_page );
if ( is_wp_error( $result ) ) {
return new Response( false, array( 'message' => $result->get_error_message() ) );
}
$logs = is_array( $result ) && array() !== $result ? $this->format_log_rows( $result ) : array();
// @since 3.0.0 If no logs then $count = 0.
// Skip the COUNT query when the result is a partial page — the fetch already told us the total is count($logs).
$events = $data['events'] ?? array();
$ip_address = $data['ip_address'] ?? '';
if ( array() === $logs ) {
$count = 0;
} elseif ( count( $logs ) < $per_page ) {
$count = ( $paged - 1 ) * $per_page + count( $logs );
} else {
$count = Audit_Log::count( $date_from_check, $date_to_check, $events, $user_id, $ip_address );
}
// Get the count for the submitted data.
return new Response(
true,
array(
'logs' => $logs,
'total_items' => $count,
'total_pages' => ceil( $count / $per_page ),
'per_page' => $per_page,
)
);
}
/**
* Generates a human-readable frequency text for audit reports.
*
* @param Audit_Report $audit_report The audit report object.
*
* @return string Returns the formatted frequency description.
*/
public function get_frequency_text( Audit_Report $audit_report ): string {
$text = '';
switch ( $audit_report->frequency ) {
case 'daily':
$text = ucfirst( $audit_report->day ) . 's at ' . $audit_report->time;
break;
case 'weekly':
case 'monthly':
$text = ucfirst( $audit_report->frequency ) . ' on ' . ucfirst( $audit_report->day ) . 's at ' . $audit_report->time;
break;
default:
break;
}
return $text;
}
/**
* Enqueues scripts and styles for this page.
*/
public function enqueue_assets(): void {
if ( ! $this->is_page_active() ) {
return;
}
$handle = 'defender-ui-audit-logging';
wp_enqueue_script(
$handle,
WP_DEFENDER_BASE_URL . 'assets/js/audit-logging-ui.js',
array( 'def-vue', 'def-manifest', 'def-core-ui', 'defender', 'wp-i18n' ),
DEFENDER_VERSION,
true
);
wp_set_script_translations( $handle, 'wpdef' );
wp_localize_script(
$handle,
'defenderUIData',
array_merge(
$this->get_shared_data(),
$this->data_frontend()
)
);
wp_enqueue_style(
$handle,
WP_DEFENDER_BASE_URL . 'assets/css/showcase.css',
array(),
DEFENDER_VERSION
);
$this->enqueue_main_assets();
}
/**
* Render the root element for frontend.
*
* @return void
*/
public function main_view(): void {
$this->render( 'main' );
}
/**
* Provides a summary of audit logs.
*
* @return void
* @throws Exception If there is an error during summary generation.
* @defender_route
*/
public function summary(): void {
$response = $this->is_active() ? $this->summary_data() : array();
wp_send_json_success( $response );
}
/**
* Returns an array with summary data for audit logging.
*
* @param bool $for_hub Default 'false' because it's displayed on site summary sections.
*
* @return array
* @throws Exception Emits Exception in case of an error.
*/
public function summary_data( bool $for_hub = false ): array {
$timezone = wp_timezone();
// Monthly count.
$date_from = ( new DateTime( '-30 days', $timezone ) )
->setTime( 0, 0, 0 )
->getTimestamp();
$date_to = ( new DateTime( 'now', $timezone ) )
->setTime( 23, 59, 59 )
->getTimestamp();
$month_count = Audit_Log::count( $date_from, $date_to );
// Weekly count sets in the returned array.
// Daily count. Sync data to the Hub without timezone.
$date_from = $for_hub ? new DateTime( 'now' ) : new DateTime( 'now', wp_timezone() );
$date_from = $date_from->modify( '-24 hours' )->setTime( 0, 0, 0 )->getTimestamp();
$day_count = Audit_Log::count( $date_from, $date_to );
// Get the last item.
$last = Audit_Log::get_last();
if ( is_object( $last ) ) {
$last = $for_hub
? $this->persistent_hub_datetime_format( $last->timestamp )
: $this->format_date_time( $last->timestamp );
} else {
$last = 'n/a';
}
return array(
'monthCount' => $month_count,
'weekCount' => Audit::get_audit_events_per_week(),
'dayCount' => $day_count,
'lastEvent' => $last,
'report' => wd_di()->get( Audit_Report::class )->to_string(),
);
}
/**
* Save settings.
*
* @param Request $request The request object containing new settings data.
*
* @return Response
* @defender_route
*/
public function save_settings( Request $request ): Response {
$data = $request->get_data_by_model( $this->model );
if ( false === $data['enabled'] && $data['enabled'] !== $this->model->is_active() ) {
// Toggle off, so we need to flush everything to cloud.
$this->service->flush();
}
$this->model->import( $data );
if ( $this->model->validate() ) {
$this->model->save();
}
Config_Hub_Helper::set_clear_active_flag();
return new Response(
true,
array_merge(
$this->data_frontend(),
array(
'message' => esc_html__( 'Your settings have been updated.', 'wpdef' ),
'auto_close' => true,
'model' => $this->model->export(),
)
)
);
}
/**
* Provides the audit settings data used outside the Audit Logging page.
*
* @return array
*/
public function settings_data_frontend(): array {
return array_merge(
array(
'model' => $this->model->export(),
),
$this->dump_routes_and_nonces()
);
}
/**
* Converts the current state of the object to an array.
*
* @return array Returns an associative array of object properties.
*/
public function to_array(): array {
return array_merge(
array(
'enabled' => $this->is_active(),
'report' => true,
),
$this->dump_routes_and_nonces()
);
}
/**
* Removes settings for all submodules.
*/
public function remove_settings(): void {
( new Model_Audit_Logging() )->delete();
}
/**
* Delete all the data & the cache.
*/
public function remove_data(): void {
Audit_Log::truncate();
// Remove cached data.
Array_Cache::remove( 'sockets', 'audit' );
Array_Cache::remove( 'logs', 'audit' );
Array_Cache::remove( 'menu_updated', 'audit' );
Array_Cache::remove( 'post_updated', 'audit' );
delete_site_option( 'wd_audit_fetch_checkpoint' );
}
/**
* Provides data for the frontend.
*
* @return array An array of data for the frontend.
* @throws Exception If there is an error.
*/
public function data_frontend(): array {
$logs = array();
$count = 0;
$per_page = self::DEFAULT_PER_PAGE;
$total_page = 1;
if ( $this->is_active() ) {
$timezone = wp_timezone();
$date_from = ( new DateTime( '-7 days', $timezone ) )->setTime( 0, 0, 0 );
$date_to = ( new DateTime( 'now', $timezone ) )->setTime( 23, 59, 59 );
$ip_address = sanitize_text_field( defender_get_data_from_request( 'ip', 'g' ) );
$result = $this->service->fetch(
$date_from->getTimestamp(),
$date_to->getTimestamp(),
array(),
'',
$ip_address,
1,
$per_page
);
if ( ! is_wp_error( $result ) ) {
$logs = $this->format_log_rows( $result );
// Skip COUNT query when the first page is a partial result — it's cheaper than a second DB round-trip.
if ( count( $logs ) < $per_page ) {
$count = count( $logs );
} else {
$count = Audit_Log::count( $date_from->getTimestamp(), $date_to->getTimestamp(), array(), '', $ip_address );
}
$total_page = ceil( $count / $per_page );
}
}
return array(
'auditLogging' => array_merge(
array(
'model' => $this->model->export(),
'logs' => $logs,
'events_type' => Audit_Log::allowed_events(),
'summary' => array(
'count_7_days' => $count,
'report' => wd_di()->get( Audit_Report::class )->to_string(),
),
'paging' => array(
'paged' => 1,
'total_pages' => $total_page,
'count' => $count,
),
),
$this->dump_routes_and_nonces()
),
'antibot' => wd_di()->get( Antibot_Global_Firewall::class )->data_frontend(),
);
}
/**
* Imports data into the model.
*
* @param array $data Data to be imported into the model.
*
* @throws Exception If table is not defined.
*/
public function import_data( array $data ) {
$model = $this->model;
if ( array() === $data ) {
$model->enabled = false;
$model->storage_days = '6 months';
$model->save();
} else {
$model->import( $data );
if ( $model->validate() ) {
$model->save();
}
}
}
/**
* Exports strings.
*
* @return array An array of strings.
*/
public function export_strings(): array {
$strings = $this->is_active()
? array( esc_html__( 'Active', 'wpdef' ) )
: array( esc_html__( 'Inactive', 'wpdef' ) );
if ( $this->is_pro ) {
if ( $this->is_active() ) {
$audit_report = new Audit_Report();
if ( 'enabled' === $audit_report->status ) {
$strings[] = sprintf(
/* translators: %s: Frequency value. */
esc_html__( 'Email reports sending %s', 'wpdef' ),
$audit_report->frequency
);
}
}
} else {
$strings[] = sprintf(
/* translators: %s: Html for Pro-tag. */
esc_html__( 'Email report inactive %s', 'wpdef' ),
'<span class="sui-tag sui-tag-pro">Pro</span>'
);
}
return $strings;
}
/**
* Generates configuration strings based on the provided configuration.
*
* @param array $config Configuration data.
*
* @return array Returns an array of configuration strings.
*/
public function config_strings( array $config ): array {
$strings = $config['enabled']
? array( esc_html__( 'Active', 'wpdef' ) )
: array( esc_html__( 'Inactive', 'wpdef' ) );
if ( $this->is_pro ) {
if ( $config['enabled'] && isset( $config['report'] ) && 'enabled' === $config['report'] ) {
$strings[] = sprintf(
/* translators: %s: Frequency value. */
esc_html__( 'Email reports sending %s', 'wpdef' ),
$config['frequency']
);
}
} else {
$strings[] = sprintf(
/* translators: %s: Html for Pro-tag. */
esc_html__( 'Email report inactive %s', 'wpdef' ),
'<span class="sui-tag sui-tag-pro">Pro</span>'
);
}
return $strings;
}
}