first commit

This commit is contained in:
Rachit Bhargava
2023-07-21 17:12:10 -04:00
parent d0fe47dde4
commit 5d0f0734d8
14003 changed files with 2829464 additions and 0 deletions

View File

@@ -0,0 +1,366 @@
<?php
namespace WPMailSMTP\Admin\DebugEvents;
use WPMailSMTP\Admin\Area;
use WPMailSMTP\Options;
use WPMailSMTP\Tasks\DebugEventsCleanupTask;
use WPMailSMTP\WP;
/**
* Debug Events class.
*
* @since 3.0.0
*/
class DebugEvents {
/**
* Register hooks.
*
* @since 3.0.0
*/
public function hooks() {
// Process AJAX requests.
add_action( 'wp_ajax_wp_mail_smtp_debug_event_preview', [ $this, 'process_ajax_debug_event_preview' ] );
add_action( 'wp_ajax_wp_mail_smtp_delete_all_debug_events', [ $this, 'process_ajax_delete_all_debug_events' ] );
// Initialize screen options for the Debug Events page.
add_action( 'load-wp-mail-smtp_page_wp-mail-smtp-tools', [ $this, 'screen_options' ] );
add_filter( 'set-screen-option', [ $this, 'set_screen_options' ], 10, 3 );
add_filter( 'set_screen_option_wp_mail_smtp_debug_events_per_page', [ $this, 'set_screen_options' ], 10, 3 );
// Cancel previous debug events cleanup task if retention period option was changed.
add_filter( 'wp_mail_smtp_options_set', [ $this, 'maybe_cancel_debug_events_cleanup_task' ] );
// Detect debug events log retention period constant change.
if ( Options::init()->is_const_defined( 'debug_events', 'retention_period' ) ) {
add_action( 'admin_init', [ $this, 'detect_debug_events_retention_period_constant_change' ] );
}
}
/**
* Detect debug events retention period constant change.
*
* @since 3.6.0
*/
public function detect_debug_events_retention_period_constant_change() {
if ( ! WP::in_wp_admin() ) {
return;
}
if ( Options::init()->is_const_changed( 'debug_events', 'retention_period' ) ) {
( new DebugEventsCleanupTask() )->cancel();
}
}
/**
* Cancel previous debug events cleanup task if retention period option was changed.
*
* @since 3.6.0
*
* @param array $options Currently processed options passed to a filter hook.
*
* @return array
*/
public function maybe_cancel_debug_events_cleanup_task( $options ) {
if ( isset( $options['debug_events']['retention_period'] ) ) {
// If this option has changed, cancel the recurring cleanup task and init again.
if ( Options::init()->is_option_changed( $options['debug_events']['retention_period'], 'debug_events', 'retention_period' ) ) {
( new DebugEventsCleanupTask() )->cancel();
}
}
return $options;
}
/**
* Process AJAX request for deleting all debug event entries.
*
* @since 3.0.0
*/
public function process_ajax_delete_all_debug_events() {
if (
empty( $_POST['nonce'] ) ||
! wp_verify_nonce( sanitize_key( $_POST['nonce'] ), 'wp_mail_smtp_debug_events' )
) {
wp_send_json_error( esc_html__( 'Access rejected.', 'wp-mail-smtp' ) );
}
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( esc_html__( 'You don\'t have the capability to perform this action.', 'wp-mail-smtp' ) );
}
global $wpdb;
$table = self::get_table_name();
$sql = "TRUNCATE TABLE `$table`;";
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$result = $wpdb->query( $sql );
if ( $result !== false ) {
wp_send_json_success( esc_html__( 'All debug event entries were deleted successfully.', 'wp-mail-smtp' ) );
}
wp_send_json_error(
sprintf( /* translators: %s - WPDB error message. */
esc_html__( 'There was an issue while trying to delete all debug event entries. Error message: %s', 'wp-mail-smtp' ),
$wpdb->last_error
)
);
}
/**
* Process AJAX request for debug event preview.
*
* @since 3.0.0
*/
public function process_ajax_debug_event_preview() {
if (
empty( $_POST['nonce'] ) ||
! wp_verify_nonce( sanitize_key( $_POST['nonce'] ), 'wp_mail_smtp_debug_events' )
) {
wp_send_json_error( esc_html__( 'Access rejected.', 'wp-mail-smtp' ) );
}
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( esc_html__( 'You don\'t have the capability to perform this action.', 'wp-mail-smtp' ) );
}
$event_id = isset( $_POST['id'] ) ? intval( $_POST['id'] ) : false;
if ( empty( $event_id ) ) {
wp_send_json_error( esc_html__( 'No Debug Event ID provided!', 'wp-mail-smtp' ) );
}
$event = new Event( $event_id );
wp_send_json_success(
[
'title' => $event->get_title(),
'content' => $event->get_details_html(),
]
);
}
/**
* Add the debug event to the DB.
*
* @since 3.0.0
*
* @param string $message The event's message.
* @param int $type The event's type.
*
* @return bool|int
*/
public static function add( $message = '', $type = 0 ) {
if ( ! in_array( $type, array_keys( Event::get_types() ), true ) ) {
return false;
}
if ( $type === Event::TYPE_DEBUG && ! self::is_debug_enabled() ) {
return false;
}
try {
$event = new Event();
$event->set_type( $type );
$event->set_content( $message );
$event->set_initiator();
return $event->save()->get_id();
} catch ( \Exception $exception ) {
return false;
}
}
/**
* Save the debug message.
*
* @since 3.0.0
* @since 3.5.0 Returns Event ID.
*
* @param string $message The debug message.
*
* @return bool|int
*/
public static function add_debug( $message = '' ) {
return self::add( $message, Event::TYPE_DEBUG );
}
/**
* Get the debug message from the provided debug event IDs.
*
* @since 3.0.0
*
* @param array|string|int $ids A single or a list of debug event IDs.
*
* @return array
*/
public static function get_debug_messages( $ids ) {
global $wpdb;
if ( empty( $ids ) ) {
return [];
}
if ( ! self::is_valid_db() ) {
return [];
}
// Convert to a string.
if ( is_array( $ids ) ) {
$ids = implode( ',', $ids );
}
$ids = explode( ',', (string) $ids );
$ids = array_map( 'intval', $ids );
$placeholders = implode( ', ', array_fill( 0, count( $ids ), '%d' ) );
$table = self::get_table_name();
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
$events_data = $wpdb->get_results(
$wpdb->prepare( "SELECT id, content, initiator, event_type, created_at FROM {$table} WHERE id IN ( {$placeholders} )", $ids )
);
// phpcs:enable
if ( empty( $events_data ) ) {
return [];
}
return array_map(
function ( $event_item ) {
$event = new Event( $event_item );
return $event->get_short_details();
},
$events_data
);
}
/**
* Register the screen options for the debug events page.
*
* @since 3.0.0
*/
public function screen_options() {
$screen = get_current_screen();
if (
! is_object( $screen ) ||
strpos( $screen->id, 'wp-mail-smtp_page_wp-mail-smtp-tools' ) === false ||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
! isset( $_GET['tab'] ) || $_GET['tab'] !== 'debug-events'
) {
return;
}
add_screen_option(
'per_page',
[
'label' => esc_html__( 'Number of events per page:', 'wp-mail-smtp' ),
'option' => 'wp_mail_smtp_debug_events_per_page',
'default' => EventsCollection::PER_PAGE,
]
);
}
/**
* Set the screen options for the debug events page.
*
* @since 3.0.0
*
* @param bool $keep Whether to save or skip saving the screen option value.
* @param string $option The option name.
* @param int $value The number of items to use.
*
* @return bool|int
*/
public function set_screen_options( $keep, $option, $value ) {
if ( 'wp_mail_smtp_debug_events_per_page' === $option ) {
return (int) $value;
}
return $keep;
}
/**
* Whether the email debug for debug events is enabled or not.
*
* @since 3.0.0
*
* @return bool
*/
public static function is_debug_enabled() {
return (bool) Options::init()->get( 'debug_events', 'email_debug' );
}
/**
* Get the debug events page URL.
*
* @since 3.0.0
*
* @return string
*/
public static function get_page_url() {
return add_query_arg(
[
'tab' => 'debug-events',
],
wp_mail_smtp()->get_admin()->get_admin_page_url( Area::SLUG . '-tools' )
);
}
/**
* Get the DB table name.
*
* @since 3.0.0
*
* @return string Table name, prefixed.
*/
public static function get_table_name() {
global $wpdb;
return $wpdb->prefix . 'wpmailsmtp_debug_events';
}
/**
* Whether the DB table exists.
*
* @since 3.0.0
*
* @return bool
*/
public static function is_valid_db() {
global $wpdb;
static $is_valid = null;
// Return cached value only if table already exists.
if ( $is_valid === true ) {
return true;
}
$table = self::get_table_name();
$is_valid = (bool) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s;', $table ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching
return $is_valid;
}
}

View File

@@ -0,0 +1,597 @@
<?php
namespace WPMailSMTP\Admin\DebugEvents;
use WPMailSMTP\WP;
/**
* Debug Event class.
*
* @since 3.0.0
*/
class Event {
/**
* This is an error event.
*
* @since 3.0.0
*/
const TYPE_ERROR = 0;
/**
* This is a debug event.
*
* @since 3.0.0
*/
const TYPE_DEBUG = 1;
/**
* The event's ID.
*
* @since 3.0.0
*
* @var int
*/
protected $id = 0;
/**
* The event's content.
*
* @since 3.0.0
*
* @var string
*/
protected $content = '';
/**
* The event's initiator - who called the `wp_mail` function?
* JSON encoded string.
*
* @since 3.0.0
*
* @var string
*/
protected $initiator = '';
/**
* The event's type.
*
* @since 3.0.0
*
* @var int
*/
protected $event_type = 0;
/**
* The date and time when this event was created.
*
* @since 3.0.0
*
* @var \DateTime
*/
protected $created_at;
/**
* Retrieve a particular event when constructing the object.
*
* @since 3.0.0
*
* @param int|object $id_or_row The event ID or object with event attributes.
*/
public function __construct( $id_or_row = null ) {
$this->populate_event( $id_or_row );
}
/**
* Get and prepare the event data.
*
* @since 3.0.0
*
* @param int|object $id_or_row The event ID or object with event attributes.
*/
private function populate_event( $id_or_row ) {
$event = null;
if ( is_numeric( $id_or_row ) ) {
// Get by ID.
$collection = new EventsCollection( [ 'id' => (int) $id_or_row ] );
$events = $collection->get();
if ( $events->valid() ) {
$event = $events->current();
}
} elseif (
is_object( $id_or_row ) &&
isset(
$id_or_row->id,
$id_or_row->content,
$id_or_row->initiator,
$id_or_row->event_type,
$id_or_row->created_at
)
) {
$event = $id_or_row;
}
if ( $event !== null ) {
foreach ( get_object_vars( $event ) as $key => $value ) {
$this->{$key} = $value;
}
}
}
/**
* Event ID as per our DB table.
*
* @since 3.0.0
*
* @return int
*/
public function get_id() {
return (int) $this->id;
}
/**
* Get the event title.
*
* @since 3.0.0
*
* @return string
*/
public function get_title() {
/* translators: %d the event ID. */
return sprintf( esc_html__( 'Event #%d', 'wp-mail-smtp' ), $this->get_id() );
}
/**
* Get the content of the event.
*
* @since 3.0.0
*
* @return string
*/
public function get_content() {
return $this->content;
}
/**
* Get the event's type.
*
* @since 3.0.0
*
* @return int
*/
public function get_type() {
return (int) $this->event_type;
}
/**
* Get the list of all event types.
*
* @since 3.0.0
*
* @return array
*/
public static function get_types() {
return [
self::TYPE_ERROR => esc_html__( 'Error', 'wp-mail-smtp' ),
self::TYPE_DEBUG => esc_html__( 'Debug', 'wp-mail-smtp' ),
];
}
/**
* Get human readable type name.
*
* @since 3.0.0
*
* @return string
*/
public function get_type_name() {
$types = self::get_types();
return isset( $types[ $this->get_type() ] ) ? $types[ $this->get_type() ] : '';
}
/**
* Get the date/time when this event was created.
*
* @since 3.0.0
*
* @throws \Exception Emits exception on incorrect date.
*
* @return \DateTime
*/
public function get_created_at() {
$timezone = new \DateTimeZone( 'UTC' );
$date = false;
if ( ! empty( $this->created_at ) ) {
$date = \DateTime::createFromFormat( WP::datetime_mysql_format(), $this->created_at, $timezone );
}
if ( $date === false ) {
$date = new \DateTime( 'now', $timezone );
}
return $date;
}
/**
* Get the date/time when this event was created in a nicely formatted string.
*
* @since 3.0.0
*
* @return string
*/
public function get_created_at_formatted() {
try {
$date = $this->get_created_at();
} catch ( \Exception $e ) {
$date = null;
}
if ( empty( $date ) ) {
return esc_html__( 'N/A', 'wp-mail-smtp' );
}
return esc_html(
date_i18n(
WP::datetime_format(),
strtotime( get_date_from_gmt( $date->format( WP::datetime_mysql_format() ) ) )
)
);
}
/**
* Get the event's initiator raw data.
* Who called the `wp_mail` function?
*
* @since 3.0.0
*
* @return array
*/
public function get_initiator_raw() {
return json_decode( $this->initiator, true );
}
/**
* Get the event's initiator name.
* Which plugin/theme (or WP core) called the `wp_mail` function?
*
* @since 3.0.0
*
* @return string
*/
public function get_initiator() {
$initiator = (array) $this->get_initiator_raw();
if ( empty( $initiator['file'] ) ) {
return '';
}
return WP::get_initiator_name( $initiator['file'] );
}
/**
* Get the event's initiator file path.
*
* @since 3.0.0
*
* @return string
*/
public function get_initiator_file_path() {
$initiator = (array) $this->get_initiator_raw();
if ( empty( $initiator['file'] ) ) {
return '';
}
return $initiator['file'];
}
/**
* Get the event's initiator file line.
*
* @since 3.0.0
*
* @return string
*/
public function get_initiator_file_line() {
$initiator = (array) $this->get_initiator_raw();
if ( empty( $initiator['line'] ) ) {
return '';
}
return $initiator['line'];
}
/**
* Get the event's initiator backtrace.
*
* @since 3.6.0
*
* @return array
*/
private function get_initiator_backtrace() {
$initiator = (array) $this->get_initiator_raw();
if ( empty( $initiator['backtrace'] ) ) {
return [];
}
return $initiator['backtrace'];
}
/**
* Get the event preview HTML.
*
* @since 3.0.0
*
* @return string
*/
public function get_details_html() {
$initiator = $this->get_initiator();
$initiator_backtrace = $this->get_initiator_backtrace();
ob_start();
?>
<div class="wp-mail-smtp-debug-event-preview">
<div class="wp-mail-smtp-debug-event-preview-subtitle">
<span><?php esc_html_e( 'Debug Event Details', 'wp-mail-smtp' ); ?></span>
</div>
<div class="wp-mail-smtp-debug-event-row wp-mail-smtp-debug-event-preview-type">
<span class="debug-event-label"><?php esc_html_e( 'Type', 'wp-mail-smtp' ); ?></span>
<span class="debug-event-value"><?php echo esc_html( $this->get_type_name() ); ?></span>
</div>
<div class="wp-mail-smtp-debug-event-row wp-mail-smtp-debug-event-preview-date">
<span class="debug-event-label"><?php esc_html_e( 'Date', 'wp-mail-smtp' ); ?></span>
<span class="debug-event-value"><?php echo esc_html( $this->get_created_at_formatted() ); ?></span>
</div>
<div class="wp-mail-smtp-debug-event-row wp-mail-smtp-debug-event-preview-content">
<span class="debug-event-label"><?php esc_html_e( 'Content', 'wp-mail-smtp' ); ?></span>
<div class="debug-event-value">
<?php echo wp_kses( str_replace( [ "\r\n", "\r", "\n" ], '<br>', $this->get_content() ), [ 'br' => [] ] ); ?>
</div>
</div>
<?php if ( ! empty( $initiator ) ) : ?>
<div class="wp-mail-smtp-debug-event-row wp-mail-smtp-debug-event-preview-caller">
<span class="debug-event-label"><?php esc_html_e( 'Source', 'wp-mail-smtp' ); ?></span>
<div class="debug-event-value">
<span class="debug-event-initiator"><?php echo esc_html( $initiator ); ?></span>
<p class="debug-event-code">
<?php
printf( /* Translators: %1$s the path of a file, %2$s the line number in the file. */
esc_html__( '%1$s (line: %2$s)', 'wp-mail-smtp' ),
esc_html( $this->get_initiator_file_path() ),
esc_html( $this->get_initiator_file_line() )
);
?>
<?php if ( ! empty( $initiator_backtrace ) ) : ?>
<br><br>
<b><?php esc_html_e( 'Backtrace:', 'wp-mail-smtp' ); ?></b>
<br>
<?php
foreach ( $initiator_backtrace as $i => $item ) {
printf(
/* translators: %1$d - index number; %2$s - function name; %3$s - file path; %4$s - line number. */
esc_html__( '[%1$d] %2$s called at [%3$s:%4$s]', 'wp-mail-smtp' ),
$i,
isset( $item['class'] ) ? esc_html( $item['class'] . $item['type'] . $item['function'] ) : esc_html( $item['function'] ),
isset( $item['file'] ) ? esc_html( $item['file'] ) : '', // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
isset( $item['line'] ) ? esc_html( $item['line'] ) : '' // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
);
echo '<br>';
}
?>
<?php endif; ?>
</p>
</div>
</div>
<?php endif; ?>
</div>
<?php
return ob_get_clean();
}
/**
* Get the short details about this event (event content and the initiator's name).
*
* @since 3.0.0
*
* @return string
*/
public function get_short_details() {
$result = [];
if ( ! empty( $this->get_initiator() ) ) {
$result[] = sprintf(
/* Translators: %s - Email initiator/source name. */
esc_html__( 'Email Source: %s', 'wp-mail-smtp' ),
esc_html( $this->get_initiator() )
);
}
$result[] = esc_html( $this->get_content() );
return implode( WP::EOL, $result );
}
/**
* Save a new or modified event in DB.
*
* @since 3.0.0
*
* @throws \Exception When event init fails.
*
* @return Event New or updated event class instance.
*/
public function save() {
global $wpdb;
$table = DebugEvents::get_table_name();
if ( (bool) $this->get_id() ) {
// Update the existing DB table record.
$wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching
$table,
[
'content' => $this->content,
'initiator' => $this->initiator,
'event_type' => $this->event_type,
'created_at' => $this->get_created_at()->format( WP::datetime_mysql_format() ),
],
[
'id' => $this->get_id(),
],
[
'%s', // content.
'%s', // initiator.
'%s', // type.
'%s', // created_at.
],
[
'%d',
]
);
$event_id = $this->get_id();
} else {
// Create a new DB table record.
$wpdb->insert(
$table,
[
'content' => $this->content,
'initiator' => $this->initiator,
'event_type' => $this->event_type,
'created_at' => $this->get_created_at()->format( WP::datetime_mysql_format() ),
],
[
'%s', // content.
'%s', // initiator.
'%s', // type.
'%s', // created_at.
]
);
$event_id = $wpdb->insert_id;
}
try {
$event = new Event( $event_id );
} catch ( \Exception $e ) {
$event = new Event();
}
return $event;
}
/**
* Set the content of this event.
*
* @since 3.0.0
*
* @param string|array $content The event's content.
*/
public function set_content( $content ) {
if ( ! is_string( $content ) ) {
$this->content = wp_json_encode( $content );
} else {
$this->content = wp_strip_all_tags( str_replace( '<br>', "\r\n", $content ), false );
}
}
/**
* Set the initiator by checking the backtrace for the wp_mail function call.
*
* @since 3.0.0
*/
public function set_initiator() {
$initiator = wp_mail_smtp()->get_wp_mail_initiator();
if ( empty( $initiator->get_file() ) ) {
return;
}
$data['file'] = $initiator->get_file();
if ( ! empty( $initiator->get_line() ) ) {
$data['line'] = $initiator->get_line();
}
if ( DebugEvents::is_debug_enabled() ) {
$data['backtrace'] = $initiator->get_backtrace();
}
$this->initiator = wp_json_encode( $data );
}
/**
* Set the type of this event.
*
* @since 3.0.0
*
* @param int $type The event's type.
*/
public function set_type( $type ) {
$this->event_type = (int) $type;
}
/**
* Whether the event instance is a valid entity to work with.
*
* @since 3.0.0
*/
public function is_valid() {
return ! ( empty( $this->id ) || empty( $this->created_at ) );
}
/**
* Whether this is an error event.
*
* @since 3.0.0
*
* @return bool
*/
public function is_error() {
return self::TYPE_ERROR === $this->get_type();
}
/**
* Whether this is a debug event.
*
* @since 3.0.0
*
* @return bool
*/
public function is_debug() {
return self::TYPE_DEBUG === $this->get_type();
}
}

View File

@@ -0,0 +1,421 @@
<?php
namespace WPMailSMTP\Admin\DebugEvents;
use WPMailSMTP\WP;
/**
* Debug Events Collection.
*
* @since 3.0.0
*/
class EventsCollection implements \Countable, \Iterator {
/**
* Default number of log entries per page.
*
* @since 3.0.0
*
* @var int
*/
const PER_PAGE = 10;
/**
* Number of log entries per page.
*
* @since 3.0.0
*
* @var int
*/
public static $per_page;
/**
* List of all Event instances.
*
* @since 3.0.0
*
* @var array
*/
private $list = [];
/**
* List of current collection instance parameters.
*
* @since 3.0.0
*
* @var array
*/
private $params;
/**
* Used for \Iterator when iterating through Queue in loops.
*
* @since 3.0.0
*
* @var int
*/
private $iterator_position = 0;
/**
* Collection constructor.
* $events = new EventsCollection( [ 'type' => 0 ] );
*
* @since 3.0.0
*
* @param array $params The events collection parameters.
*/
public function __construct( array $params = [] ) {
$this->set_per_page();
$this->params = $this->process_params( $params );
}
/**
* Set the per page attribute to the screen options value.
*
* @since 3.0.0
*/
protected function set_per_page() {
$per_page = (int) get_user_meta(
get_current_user_id(),
'wp_mail_smtp_debug_events_per_page',
true
);
if ( $per_page < 1 ) {
$per_page = self::PER_PAGE;
}
self::$per_page = $per_page;
}
/**
* Verify, sanitize, and populate with default values
* all the passed parameters, which participate in DB queries.
*
* @since 3.0.0
*
* @param array $params The events collection parameters.
*
* @return array
*/
public function process_params( $params ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.MaxExceeded
$params = (array) $params;
$processed = [];
/*
* WHERE.
*/
// Single ID.
if ( ! empty( $params['id'] ) ) {
$processed['id'] = (int) $params['id'];
}
// Multiple IDs.
if (
! empty( $params['ids'] ) &&
is_array( $params['ids'] )
) {
$processed['ids'] = array_unique( array_filter( array_map( 'intval', array_values( $params['ids'] ) ) ) );
}
// Type.
if (
isset( $params['type'] ) &&
in_array( $params['type'], array_keys( Event::get_types() ), true )
) {
$processed['type'] = (int) $params['type'];
}
// Search.
if ( ! empty( $params['search'] ) ) {
$processed['search'] = sanitize_text_field( $params['search'] );
}
/*
* LIMIT.
*/
if ( ! empty( $params['offset'] ) ) {
$processed['offset'] = (int) $params['offset'];
}
if ( ! empty( $params['per_page'] ) ) {
$processed['per_page'] = (int) $params['per_page'];
}
/*
* Sent date.
*/
if ( ! empty( $params['date'] ) ) {
if ( is_string( $params['date'] ) ) {
$params['date'] = array_fill( 0, 2, $params['date'] );
} elseif ( is_array( $params['date'] ) && count( $params['date'] ) === 1 ) {
$params['date'] = array_fill( 0, 2, $params['date'][0] );
}
// We pass array and treat it as a range from:to.
if ( is_array( $params['date'] ) && count( $params['date'] ) === 2 ) {
$date_start = WP::get_day_period_date( 'start_of_day', strtotime( $params['date'][0] ), 'Y-m-d H:i:s', true );
$date_end = WP::get_day_period_date( 'end_of_day', strtotime( $params['date'][1] ), 'Y-m-d H:i:s', true );
if ( ! empty( $date_start ) && ! empty( $date_end ) ) {
$processed['date'] = [ $date_start, $date_end ];
}
}
}
// Merge missing values with defaults.
return wp_parse_args(
$processed,
$this->get_default_params()
);
}
/**
* Get the list of default params for a usual query.
*
* @since 3.0.0
*
* @return array
*/
protected function get_default_params() {
return [
'offset' => 0,
'per_page' => self::$per_page,
'order' => 'DESC',
'orderby' => 'id',
'search' => '',
];
}
/**
* Get the SQL-ready string of WHERE part for a query.
*
* @since 3.0.0
*
* @return string
*/
private function build_where() { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh
global $wpdb;
$where = [ '1=1' ];
// Shortcut single ID or multiple IDs.
if ( ! empty( $this->params['id'] ) || ! empty( $this->params['ids'] ) ) {
if ( ! empty( $this->params['id'] ) ) {
$where[] = $wpdb->prepare( 'id = %d', $this->params['id'] );
} elseif ( ! empty( $this->params['ids'] ) ) {
$where[] = 'id IN (' . implode( ',', $this->params['ids'] ) . ')';
}
// When some ID(s) defined - we should ignore all other possible filtering options.
return implode( ' AND ', $where );
}
// Type.
if ( isset( $this->params['type'] ) ) {
$where[] = $wpdb->prepare( 'event_type = %d', $this->params['type'] );
}
// Search.
if ( ! empty( $this->params['search'] ) ) {
$where[] = '(' .
$wpdb->prepare(
'content LIKE %s',
'%' . $wpdb->esc_like( $this->params['search'] ) . '%'
)
. ' OR ' .
$wpdb->prepare(
'initiator LIKE %s',
'%' . $wpdb->esc_like( $this->params['search'] ) . '%'
)
. ')';
}
// Sent date.
if (
! empty( $this->params['date'] ) &&
is_array( $this->params['date'] ) &&
count( $this->params['date'] ) === 2
) {
$where[] = $wpdb->prepare(
'( created_at >= %s AND created_at <= %s )',
$this->params['date'][0],
$this->params['date'][1]
);
}
return implode( ' AND ', $where );
}
/**
* Get the SQL-ready string of ORDER part for a query.
* Order is always in the params, as per our defaults.
*
* @since 3.0.0
*
* @return string
*/
private function build_order() {
return 'ORDER BY ' . $this->params['orderby'] . ' ' . $this->params['order'];
}
/**
* Get the SQL-ready string of LIMIT part for a query.
* Limit is always in the params, as per our defaults.
*
* @since 3.0.0
*
* @return string
*/
private function build_limit() {
return 'LIMIT ' . $this->params['offset'] . ', ' . $this->params['per_page'];
}
/**
* Count the number of DB records according to filters.
* Do not retrieve actual records.
*
* @since 3.0.0
*
* @return int
*/
public function get_count() {
$table = DebugEvents::get_table_name();
$where = $this->build_where();
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
return (int) WP::wpdb()->get_var(
"SELECT COUNT(id) FROM $table
WHERE {$where}"
);
// phpcs:enable
}
/**
* Get the list of DB records.
* You can either use array returned there OR iterate over the whole object,
* as it implements Iterator interface.
*
* @since 3.0.0
*
* @return EventsCollection
*/
public function get() {
$table = DebugEvents::get_table_name();
$where = $this->build_where();
$limit = $this->build_limit();
$order = $this->build_order();
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
$data = WP::wpdb()->get_results(
"SELECT * FROM $table
WHERE {$where}
{$order}
{$limit}"
);
// phpcs:enable
if ( ! empty( $data ) ) {
// As we got raw data we need to convert each row to Event.
foreach ( $data as $row ) {
$this->list[] = new Event( $row );
}
}
return $this;
}
/*********************************************************************************************
* ****************************** \Counter interface method. *********************************
*********************************************************************************************/
/**
* Count number of Record in a Queue.
*
* @since 3.0.0
*
* @return int
*/
#[\ReturnTypeWillChange]
public function count() {
return count( $this->list );
}
/*********************************************************************************************
* ****************************** \Iterator interface methods. *******************************
*********************************************************************************************/
/**
* Rewind the Iterator to the first element.
*
* @since 3.0.0
*/
#[\ReturnTypeWillChange]
public function rewind() {
$this->iterator_position = 0;
}
/**
* Return the current element.
*
* @since 3.0.0
*
* @return Event|null Return null when no items in collection.
*/
#[\ReturnTypeWillChange]
public function current() {
return $this->valid() ? $this->list[ $this->iterator_position ] : null;
}
/**
* Return the key of the current element.
*
* @since 3.0.0
*
* @return int
*/
#[\ReturnTypeWillChange]
public function key() {
return $this->iterator_position;
}
/**
* Move forward to next element.
*
* @since 3.0.0
*/
#[\ReturnTypeWillChange]
public function next() {
++ $this->iterator_position;
}
/**
* Checks if current position is valid.
*
* @since 3.0.0
*
* @return bool
*/
#[\ReturnTypeWillChange]
public function valid() {
return isset( $this->list[ $this->iterator_position ] );
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace WPMailSMTP\Admin\DebugEvents;
use WPMailSMTP\MigrationAbstract;
/**
* Debug Events Migration Class
*
* @since 3.0.0
*/
class Migration extends MigrationAbstract {
/**
* Version of the debug events database table.
*
* @since 3.0.0
*/
const DB_VERSION = 1;
/**
* Option key where we save the current debug events DB version.
*
* @since 3.0.0
*/
const OPTION_NAME = 'wp_mail_smtp_debug_events_db_version';
/**
* Option key where we save any errors while creating the debug events DB table.
*
* @since 3.0.0
*/
const ERROR_OPTION_NAME = 'wp_mail_smtp_debug_events_db_error';
/**
* Create the debug events DB table structure.
*
* @since 3.0.0
*/
protected function migrate_to_1() {
global $wpdb;
$table = DebugEvents::get_table_name();
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE IF NOT EXISTS `$table` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`content` TEXT DEFAULT NULL,
`initiator` TEXT DEFAULT NULL,
`event_type` TINYINT UNSIGNED NOT NULL DEFAULT '0',
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id)
)
ENGINE='InnoDB'
{$charset_collate};";
$result = $wpdb->query( $sql ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared
if ( ! empty( $wpdb->last_error ) ) {
update_option( self::ERROR_OPTION_NAME, $wpdb->last_error, false );
}
// Save the current version to DB.
if ( $result !== false ) {
$this->update_db_ver( 1 );
}
}
}

View File

@@ -0,0 +1,582 @@
<?php
namespace WPMailSMTP\Admin\DebugEvents;
use WPMailSMTP\Helpers\Helpers;
if ( ! class_exists( 'WP_List_Table', false ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}
/**
* Class Table that displays the list of debug events.
*
* @since 3.0.0
*/
class Table extends \WP_List_Table {
/**
* Number of debug events by different types.
*
* @since 3.0.0
*
* @var array
*/
public $counts;
/**
* Set up a constructor that references the parent constructor.
* Using the parent reference to set some default configs.
*
* @since 3.0.0
*/
public function __construct() {
// Set parent defaults.
parent::__construct(
[
'singular' => 'event',
'plural' => 'events',
'ajax' => false,
]
);
// Include polyfill if mbstring PHP extension is not enabled.
if ( ! function_exists( 'mb_substr' ) || ! function_exists( 'mb_strlen' ) ) {
Helpers::include_mbstring_polyfill();
}
}
/**
* Get the debug event types for filtering purpose.
*
* @since 3.0.0
*
* @return array Associative array of debug event types StatusCode=>Name.
*/
public function get_types() {
return Event::get_types();
}
/**
* Get the items counts for various types of debug logs.
*
* @since 3.0.0
*/
public function get_counts() {
$this->counts = [];
// Base params with applied filters.
$base_params = $this->get_filters_query_params();
$total_params = $base_params;
unset( $total_params['type'] );
$this->counts['total'] = ( new EventsCollection( $total_params ) )->get_count();
foreach ( $this->get_types() as $type => $name ) {
$collection = new EventsCollection( array_merge( $base_params, [ 'type' => $type ] ) );
$this->counts[ 'type_' . $type ] = $collection->get_count();
}
/**
* Filters items counts by various types of debug events.
*
* @since 3.0.0
*
* @param array $counts {
* Items counts by types.
*
* @type integer $total Total items count.
* @type integer $status_{$type_key} Items count by type.
* }
*/
$this->counts = apply_filters( 'wp_mail_smtp_admin_debug_events_table_get_counts', $this->counts );
}
/**
* Retrieve the view types.
*
* @since 3.0.0
*/
public function get_views() {
$base_url = $this->get_filters_base_url();
$current_type = $this->get_filtered_types();
$views = [];
$views['all'] = sprintf(
'<a href="%1$s" %2$s>%3$s&nbsp;<span class="count">(%4$d)</span></a>',
esc_url( remove_query_arg( 'type', $base_url ) ),
$current_type === false ? 'class="current"' : '',
esc_html__( 'All', 'wp-mail-smtp' ),
intval( $this->counts['total'] )
);
foreach ( $this->get_types() as $type => $type_label ) {
$count = intval( $this->counts[ 'type_' . $type ] );
// Skipping types with no events.
if ( $count === 0 && $current_type !== $type ) {
continue;
}
$views[ $type ] = sprintf(
'<a href="%1$s" %2$s>%3$s&nbsp;<span class="count">(%4$d)</span></a>',
esc_url( add_query_arg( 'type', $type, $base_url ) ),
$current_type === $type ? 'class="current"' : '',
esc_html( $type_label ),
$count
);
}
/**
* Filters debug event item views.
*
* @since 3.0.0
*
* @param array $views {
* Debug event items views by types.
*
* @type string $all Total items view.
* @type integer $status_key Items views by type.
* }
* @param array $counts {
* Items counts by types.
*
* @type integer $total Total items count.
* @type integer $status_{$status_key} Items count by types.
* }
*/
return apply_filters( 'wp_mail_smtp_admin_debug_events_table_get_views', $views, $this->counts );
}
/**
* Define the table columns.
*
* @since 3.0.0
*
* @return array Associative array of slug=>Name columns data.
*/
public function get_columns() {
return [
'event' => esc_html__( 'Event', 'wp-mail-smtp' ),
'type' => esc_html__( 'Type', 'wp-mail-smtp' ),
'content' => esc_html__( 'Content', 'wp-mail-smtp' ),
'initiator' => esc_html__( 'Source', 'wp-mail-smtp' ),
'created_at' => esc_html__( 'Date', 'wp-mail-smtp' ),
];
}
/**
* Display the main event title with a link to open event details.
*
* @since 3.0.0
*
* @param Event $item Event object.
*
* @return string
*/
public function column_event( $item ) {
return '<strong>' .
'<a href="#" data-event-id="' . esc_attr( $item->get_id() ) . '"' .
' class="js-wp-mail-smtp-debug-event-preview row-title event-preview" title="' . esc_attr( $item->get_title() ) . '">' .
esc_html( $item->get_title() ) .
'</a>' .
'</strong>';
}
/**
* Display event's type.
*
* @since 3.0.0
*
* @param Event $item Event object.
*
* @return string
*/
public function column_type( $item ) {
return esc_html( $item->get_type_name() );
}
/**
* Display event's content.
*
* @since 3.0.0
*
* @param Event $item Event object.
*
* @return string
*/
public function column_content( $item ) {
$content = $item->get_content();
if ( mb_strlen( $content ) > 100 ) {
$content = mb_substr( $content, 0, 100 ) . '...';
}
return wp_kses_post( $content );
}
/**
* Display event's wp_mail initiator.
*
* @since 3.0.0
*
* @param Event $item Event object.
*
* @return string
*/
public function column_initiator( $item ) {
return esc_html( $item->get_initiator() );
}
/**
* Display event's created date.
*
* @since 3.0.0
*
* @param Event $item Event object.
*
* @return string
*/
public function column_created_at( $item ) {
return $item->get_created_at_formatted();
}
/**
* Return type filter value or FALSE.
*
* @since 3.0.0
*
* @return bool|integer
*/
public function get_filtered_types() {
if ( ! isset( $_REQUEST['type'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return false;
}
return intval( $_REQUEST['type'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
}
/**
* Return date filter value or FALSE.
*
* @since 3.0.0
*
* @return bool|array
*/
public function get_filtered_dates() {
if ( empty( $_REQUEST['date'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return false;
}
$dates = (array) explode( ' - ', sanitize_text_field( wp_unslash( $_REQUEST['date'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return array_map( 'sanitize_text_field', $dates );
}
/**
* Return search filter values or FALSE.
*
* @since 3.0.0
*
* @return bool|array
*/
public function get_filtered_search() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( empty( $_REQUEST['search'] ) ) {
return false;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
return sanitize_text_field( wp_unslash( $_REQUEST['search'] ) );
}
/**
* Whether the event log is filtered or not.
*
* @since 3.0.0
*
* @return bool
*/
public function is_filtered() {
$is_filtered = false;
if (
$this->get_filtered_search() !== false ||
$this->get_filtered_dates() !== false ||
$this->get_filtered_types() !== false
) {
$is_filtered = true;
}
return $is_filtered;
}
/**
* Get current filters query parameters.
*
* @since 3.0.0
*
* @return array
*/
public function get_filters_query_params() {
$params = [
'search' => $this->get_filtered_search(),
'type' => $this->get_filtered_types(),
'date' => $this->get_filtered_dates(),
];
return array_filter(
$params,
function ( $v ) {
return $v !== false;
}
);
}
/**
* Get current filters base url.
*
* @since 3.0.0
*
* @return string
*/
public function get_filters_base_url() {
$base_url = DebugEvents::get_page_url();
$filters_params = $this->get_filters_query_params();
if ( isset( $filters_params['search'] ) ) {
$base_url = add_query_arg( 'search', $filters_params['search'], $base_url );
}
if ( isset( $filters_params['type'] ) ) {
$base_url = add_query_arg( 'type', $filters_params['type'], $base_url );
}
if ( isset( $filters_params['date'] ) ) {
$base_url = add_query_arg( 'date', implode( ' - ', $filters_params['date'] ), $base_url );
}
return $base_url;
}
/**
* Get the data, prepare pagination, process bulk actions.
* Prepare columns for display.
*
* @since 3.0.0
*/
public function prepare_items() {
// Retrieve count.
$this->get_counts();
// Prepare all the params to pass to our Collection. All sanitization is done in that class.
$params = $this->get_filters_query_params();
// Total amount for pagination with WHERE clause - super quick count DB request.
$total_items = ( new EventsCollection( $params ) )->get_count();
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( ! empty( $_REQUEST['orderby'] ) && in_array( $_REQUEST['orderby'], [ 'event', 'type', 'content', 'initiator', 'created_at' ], true ) ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$params['orderby'] = sanitize_key( $_REQUEST['orderby'] );
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( ! empty( $_REQUEST['order'] ) ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$params['order'] = strtoupper( sanitize_text_field( wp_unslash( $_REQUEST['order'] ) ) ) === 'DESC' ? 'DESC' : 'ASC';
}
$params['offset'] = ( $this->get_pagenum() - 1 ) * EventsCollection::$per_page;
// Get the data from the DB using parameters defined above.
$collection = new EventsCollection( $params );
$this->items = $collection->get();
/*
* Register our pagination options & calculations.
*/
$this->set_pagination_args(
[
'total_items' => $total_items,
'per_page' => EventsCollection::$per_page,
]
);
}
/**
* Display the search box.
*
* @since 1.7.0
*
* @param string $text The 'submit' button label.
* @param string $input_id ID attribute value for the search input field.
*/
public function search_box( $text, $input_id ) {
if ( ! $this->is_filtered() && ! $this->has_items() ) {
return;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$search = ! empty( $_REQUEST['search'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['search'] ) ) : '';
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( ! empty( $_REQUEST['orderby'] ) && in_array( $_REQUEST['orderby'], [ 'event', 'type', 'content', 'initiator', 'created_at' ], true ) ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$order_by = sanitize_text_field( wp_unslash( $_REQUEST['orderby'] ) );
echo '<input type="hidden" name="orderby" value="' . esc_attr( $order_by ) . '" />';
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( ! empty( $_REQUEST['order'] ) ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$order = strtoupper( sanitize_text_field( wp_unslash( $_REQUEST['order'] ) ) ) === 'DESC' ? 'DESC' : 'ASC';
echo '<input type="hidden" name="order" value="' . esc_attr( $order ) . '" />';
}
?>
<p class="search-box">
<label class="screen-reader-text" for="<?php echo esc_attr( $input_id ); ?>"><?php echo esc_html( $text ); ?>:</label>
<input type="search" id="<?php echo esc_attr( $input_id ); ?>" name="search" value="<?php echo esc_attr( $search ); ?>" />
<?php submit_button( $text, '', '', false, [ 'id' => 'search-submit' ] ); ?>
</p>
<?php
}
/**
* Whether the table has items to display or not.
*
* @since 3.0.0
*
* @return bool
*/
public function has_items() {
return count( $this->items ) > 0;
}
/**
* Message to be displayed when there are no items.
*
* @since 3.0.0
*/
public function no_items() {
if ( $this->is_filtered() ) {
esc_html_e( 'No events found.', 'wp-mail-smtp' );
} else {
esc_html_e( 'No events have been logged for now.', 'wp-mail-smtp' );
}
}
/**
* Displays the table.
*
* @since 3.0.0
*/
public function display() {
$this->_column_headers = [ $this->get_columns(), [], [] ];
parent::display();
}
/**
* Hide the tablenav if there are no items in the table.
* And remove the bulk action nonce and code.
*
* @since 3.0.0
*
* @param string $which Which tablenav: top or bottom.
*/
protected function display_tablenav( $which ) {
if ( ! $this->has_items() ) {
return;
}
?>
<div class="tablenav <?php echo esc_attr( $which ); ?>">
<?php
$this->extra_tablenav( $which );
$this->pagination( $which );
?>
<br class="clear" />
</div>
<?php
}
/**
* Extra controls to be displayed between bulk actions and pagination.
*
* @since 3.0.0
*
* @param string $which Which tablenav: top or bottom.
*/
protected function extra_tablenav( $which ) {
if ( $which !== 'top' || ! $this->has_items() ) {
return;
}
$date = $this->get_filtered_dates() !== false ? implode( ' - ', $this->get_filtered_dates() ) : '';
?>
<div class="alignleft actions wp-mail-smtp-filter-date">
<input type="text" name="date" class="regular-text wp-mail-smtp-filter-date-selector wp-mail-smtp-filter-date__control"
placeholder="<?php esc_attr_e( 'Select a date range', 'wp-mail-smtp' ); ?>"
value="<?php echo esc_attr( $date ); ?>">
<button type="submit" name="action" value="filter_date" class="button wp-mail-smtp-filter-date__btn">
<?php esc_html_e( 'Filter', 'wp-mail-smtp' ); ?>
</button>
</div>
<?php
if ( current_user_can( 'manage_options' ) ) {
wp_nonce_field( 'wp_mail_smtp_debug_events', 'wp-mail-smtp-debug-events-nonce', false );
printf(
'<button id="wp-mail-smtp-delete-all-debug-events-button" type="button" class="button">%s</button>',
esc_html__( 'Delete All Events', 'wp-mail-smtp' )
);
}
}
/**
* Get the name of the primary column.
* Important for the mobile view.
*
* @since 3.0.0
*
* @return string The name of the primary column.
*/
protected function get_primary_column_name() {
return 'event';
}
}