File: /var/www/html/wptoho/wp-content/plugins/defender-security/src/component/audit/class-core-audit.php
<?php
/**
* Represents an audit system for WordPress core, themes, and plugins.
*
* @package WP_Defender\Component\Audit
*/
namespace WP_Defender\Component\Audit;
use WP_Upgrader;
use WP_Defender\Model\Audit_Log;
use WP_Defender\Component\Security_Tweaks\WP_Version;
/**
* Handles the auditing of WordPress core, themes, and plugins such as plugin activation, deactivation, and
* installation, and theme activation, deactivation, and installation.
*/
class Core_Audit extends Audit_Event {
public const ACTION_ACTIVATED = 'activated', ACTION_DEACTIVATED = 'deactivated', ACTION_INSTALLED = 'installed', ACTION_UPGRADED = 'upgraded';
public const FILE_ADDED = 'file_added', FILE_MODIFIED = 'file_modified';
public const CONTEXT_THEME = 'ct_theme', CONTEXT_PLUGIN = 'ct_plugin', CONTEXT_CORE = 'ct_core';
/**
* Retrieves a list of hooks that the audit system will respond to.
*
* @return array Associative array of hooks and their corresponding callback information.
*/
public function get_hooks(): array {
$data = array(
'switch_theme' => array(
'args' => array( 'new_name', 'new_theme' ),
'callback' => array( self::class, 'process_activate_theme' ),
'event_type' => Audit_Log::EVENT_TYPE_SYSTEM,
'action_type' => self::ACTION_ACTIVATED,
),
'activated_plugin' => array(
'args' => array( 'plugin' ),
'text' => sprintf(
/* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Plugin name, 4: Plugin version. */
esc_html__( '%1$s %2$s activated plugin: %3$s, version %4$s', 'defender-security' ),
'{{blog_name}}',
'{{wp_user}}',
'{{plugin_name}}',
'{{plugin_version}}'
),
'event_type' => Audit_Log::EVENT_TYPE_SYSTEM,
'action_type' => self::ACTION_ACTIVATED,
'context' => self::CONTEXT_PLUGIN,
'program_args' => array(
'plugin_abs_path' => array(
'callable' => array( self::class, 'get_plugin_abs_path' ),
'params' => array(
'{{plugin}}',
),
),
'plugin_name' => array(
'callable' => 'get_plugin_data',
'params' => array(
'{{plugin_abs_path}}',
),
'result_property' => 'Name',
),
'plugin_version' => array(
'callable' => 'get_plugin_data',
'params' => array(
'{{plugin_abs_path}}',
),
'result_property' => 'Version',
),
),
),
'deleted_plugin' => array(
'args' => array( 'plugin_file', 'deleted' ),
'callback' => array( self::class, 'process_delete_plugin' ),
'action_type' => self::ACTION_DEACTIVATED,
'event_type' => Audit_Log::EVENT_TYPE_SYSTEM,
),
'deactivated_plugin' => array(
'args' => array( 'plugin' ),
'text' => sprintf(
/* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Plugin name, 4: Plugin version. */
esc_html__( '%1$s %2$s deactivated plugin: %3$s, version %4$s', 'defender-security' ),
'{{blog_name}}',
'{{wp_user}}',
'{{plugin_name}}',
'{{plugin_version}}'
),
'action_type' => self::ACTION_DEACTIVATED,
'event_type' => Audit_Log::EVENT_TYPE_SYSTEM,
'context' => self::CONTEXT_PLUGIN,
'program_args' => array(
'plugin_abs_path' => array(
'callable' => array( self::class, 'get_plugin_abs_path' ),
'params' => array(
'{{plugin}}',
),
),
'plugin_name' => array(
'callable' => 'get_plugin_data',
'params' => array(
'{{plugin_abs_path}}',
),
'result_property' => 'Name',
),
'plugin_version' => array(
'callable' => 'get_plugin_data',
'params' => array(
'{{plugin_abs_path}}',
),
'result_property' => 'Version',
),
),
),
'upgrader_process_complete' => array(
'args' => array( 'upgrader', 'options' ),
'callback' => array( self::class, 'process_installer' ),
'action_type' => self::ACTION_UPGRADED,
'event_type' => Audit_Log::EVENT_TYPE_SYSTEM,
),
);
global $wp_version;
// @since 2.7.0 Add hook for deleted theme. Use 'deleted_theme' hook that was added since WP v5.8.0.
if ( version_compare( $wp_version, '5.8.0', '>=' ) ) {
$data['deleted_theme'] = array(
'args' => array( 'stylesheet', 'deleted' ),
'text' => sprintf(
/* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Stylesheet of the theme. */
esc_html__( '%1$s %2$s deleted theme: %3$s', 'defender-security' ),
'{{blog_name}}',
'{{wp_user}}',
'{{stylesheet}}'
),
'action_type' => self::ACTION_DEACTIVATED,
'event_type' => Audit_Log::EVENT_TYPE_SYSTEM,
'context' => self::CONTEXT_THEME,
);
}
return $data;
}
/**
* Prepares the data for recording a core upgrade event.
*
* @param string $wp_version The new WordPress version.
*
* @return array Array containing the formatted message and the context.
*/
private function get_upgraded_core_data_for_record( $wp_version ) {
return array(
sprintf(
/* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Wordpress version. */
esc_html__( '%1$s %2$s updated WordPress to %3$s', 'defender-security' ),
is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : '',
$this->get_source_of_action(),
$wp_version
),
self::CONTEXT_CORE,
);
}
/**
* Handles the upgrade process for WordPress core and prepares the audit log entry.
*
* @return array|bool Returns the log entry data or false if no update is needed.
*/
public function upgrade_core() {
$update_core = get_site_transient( 'update_core' );
if ( is_object( $update_core ) ) {
$updates = $update_core->updates;
$updates = array_shift( $updates );
if ( is_object( $updates ) && property_exists( $updates, 'version' ) ) {
return $this->get_upgraded_core_data_for_record( $updates->version );
}
} elseif ( empty( $update_core ) && $this->is_hub_request() ) {
// Hub has already updated WP core to the latest version.
$wp_version = ( new WP_Version() )->get_latest_version();
return $wp_version ? $this->get_upgraded_core_data_for_record( $wp_version ) : false;
}
return false;
}
/**
* Processes bulk upgrade actions for themes or plugins and generates the audit log entry.
*
* @param WP_Upgrader $upgrader The upgrade instance.
* @param array $options Options containing type and items being upgraded.
*
* @return array|bool Returns the log entry data or false if unable to generate log data.
*/
public function bulk_upgrade( WP_Upgrader $upgrader, array $options ) {
if ( ! is_object( $upgrader->skin ) ) {
return false;
}
$blog_name = is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : '';
if ( isset( $upgrader->skin->result ) && is_wp_error( $upgrader->skin->result ) ) {
// Todo: extend Audit table with a new column for the result of the process and divide it into success and failure.
$failed_result = true;
} else {
$failed_result = false;
}
if ( 'theme' === $options['type'] ) {
$texts = array();
foreach ( $options['themes'] as $slug ) {
$theme = wp_get_theme( $slug );
if ( is_object( $theme ) ) {
$texts[] = sprintf(
/* translators: 1: Theme name, 2: Theme version. */
$failed_result ? esc_html( '%1$s version %2$s' ) : esc_html( '%1$s to %2$s' ),
$theme->Name, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
$theme->get( 'Version' )
);
}
}
if ( count( $texts ) ) {
return array(
sprintf(
$failed_result ?
/* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Translated status for themes. */
esc_html__( '%1$s %2$s was unable to update themes: %3$s', 'defender-security' ) :
/* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Translated status for themes. */
esc_html__( '%1$s %2$s updated themes: %3$s', 'defender-security' ),
$blog_name,
$this->get_source_of_action(),
implode( ', ', $texts )
),
self::CONTEXT_THEME,
);
} else {
return false;
}
} elseif ( 'plugin' === $options['type'] ) {
$texts = array();
foreach ( $options['plugins'] as $slug ) {
$plugin = get_plugin_data( WP_PLUGIN_DIR . DIRECTORY_SEPARATOR . $slug );
if ( is_array( $plugin ) && isset( $plugin['Name'] ) && ! empty( $plugin['Name'] ) ) {
$texts[] = sprintf(
/* translators: 1: Plugin name, 2: Plugin version. */
$failed_result ? esc_html( '%1$s version %2$s' ) : esc_html( '%1$s to %2$s' ),
$plugin['Name'],
$plugin['Version']
);
}
}
if ( count( $texts ) ) {
return array(
sprintf(
$failed_result ?
/* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Translated status for plugins. */
esc_html__( '%1$s %2$s was unable to update plugins: %3$s', 'defender-security' ) :
/* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Translated status for plugins. */
esc_html__( '%1$s %2$s updated plugins: %3$s', 'defender-security' ),
$blog_name,
$this->get_source_of_action(),
implode( ', ', $texts )
),
self::CONTEXT_PLUGIN,
);
} else {
return false;
}
}
}
/**
* Handles single item upgrade for themes or plugins and prepares the audit log entry.
*
* @param WP_Upgrader $upgrader The upgrade instance.
* @param array $options Options containing type and the item being upgraded.
*
* @return array|bool Returns the log entry data or false if unable to generate log data.
*/
public function single_upgrade( WP_Upgrader $upgrader, array $options ) {
$blog_name = is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : '';
if ( 'theme' === $options['type'] ) {
$theme = wp_get_theme( $options['theme'] );
if ( is_object( $theme ) ) {
$name = $theme->Name; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
$version = $theme->get( 'Version' );
return array(
sprintf(
/* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Theme name, 4: Theme version. */
esc_html__( '%1$s %2$s updated theme: %3$s, version %4$s', 'defender-security' ),
$blog_name,
$this->get_source_of_action(),
$name,
$version
),
self::CONTEXT_THEME,
);
} else {
return false;
}
} elseif ( 'plugin' === $options['type'] ) {
$slug = $options['plugin'];
$data = get_plugin_data( WP_PLUGIN_DIR . DIRECTORY_SEPARATOR . $slug );
if ( is_array( $data ) ) {
$name = $data['Name'];
$version = $data['Version'];
return array(
sprintf(
/* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Plugin name, 4: Plugin version. */
esc_html__( '%1$s %2$s updated plugin: %3$s, version %4$s', 'defender-security' ),
$blog_name,
$this->get_source_of_action(),
$name,
$version
),
self::CONTEXT_PLUGIN,
);
} else {
return false;
}
}
}
/**
* Install process.
* Log in the format: {BLOG_NAME} {USERNAME} installed {theme/plugin}: {theme/plugin name}, version {VERSION}.
*
* @param WP_Upgrader $upgrade The upgrade instance.
* @param array $options Options containing type and the item being installed.
*
* @return bool|array
*/
private function single_install( WP_Upgrader $upgrade, array $options ) {
if ( ! is_object( $upgrade->skin ) ) {
return false;
}
// Only for plugins, themes. No for translation, core.
if ( ! in_array( $options['type'], array( 'theme', 'plugin' ), true ) ) {
return false;
}
$name = '';
if ( ! empty( $upgrade->skin->api ) ) {
$name = $upgrade->skin->api->name;
} elseif ( ! empty( $upgrade->skin->upgrader ) ) {
$type_data = ( 'theme' === $options['type'] && isset( $upgrade->skin->upgrader->new_theme_data ) )
? $upgrade->skin->upgrader->new_theme_data
: $upgrade->skin->upgrader->new_plugin_data;
if ( ! empty( $type_data['Name'] ) && ! empty( $type_data['Version'] ) ) {
$name = $type_data['Name'] . ', version ' . $type_data['Version'];
}
} elseif ( ! empty( $upgrade->skin->result ) ) {
if ( is_array( $upgrade->skin->result ) && isset( $upgrade->skin->result['destination_name'] ) ) {
$name = $upgrade->skin->result['destination_name'];
} elseif ( is_object( $upgrade->skin->result ) && property_exists(
$upgrade->skin->result,
'destination_name'
) ) {
$name = $upgrade->skin->result->destination_name;
}
}
if ( empty( $name ) ) {
return false;
}
$blog_name = is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : '';
if ( 'theme' === $options['type'] ) {
return array(
sprintf(
/* translators: %s - blog name, %s - username, %s - theme name */
esc_html__( '%1$s %2$s installed theme: %3$s', 'defender-security' ),
$blog_name,
$this->get_source_of_action(),
$name
),
self::CONTEXT_THEME,
self::ACTION_INSTALLED,
);
} else {
return array(
sprintf(
/* translators: %s - blog name, %s - username, %s - plugin name */
esc_html__( '%1$s %2$s installed plugin: %3$s', 'defender-security' ),
$blog_name,
$this->get_source_of_action(),
$name
),
self::CONTEXT_PLUGIN,
self::ACTION_INSTALLED,
);
}
return false;
}
/**
* Param 'upgrader' is WP_Upgrader instance. It might be a Theme_Upgrader, Plugin_Upgrader, Core_Upgrade, or
* Language_Pack_Upgrader instance. Param 'options' is array of bulk item update data. Keys:
* 'action' (string) Type of action. Default 'update'.
* 'type' (string) Type of update process. Accepts 'plugin', 'theme', 'translation', or 'core'.
* 'bulk' (bool) Whether the update process is a bulk update. Default true.
* 'plugins' (array) Array of the basename paths of the plugins' main files.
* 'themes' (array) The theme slugs.
* 'translations' (array) Array of translations update data: 'language', 'type', 'slug', 'version'.
*
* @return mixed
*/
public function process_installer() {
$args = func_get_args();
$upgrader = $args[1]['upgrader'];
$options = $args[1]['options'];
if ( 'core' === $options['type'] ) {
return $this->upgrade_core();
// If this is core, we just create text and return.
} elseif ( isset( $options['bulk'] ) && true === $options['bulk'] ) {
// Case: local install/update work with 'bulk' => true (for mass action and no).
return $this->bulk_upgrade( $upgrader, $options );
} elseif ( 'install' === $options['action'] ) {
// Case: actions from Hub.
return $this->single_install( $upgrader, $options );
} elseif ( 'update' === $options['action'] ) {
// Case: actions from Hub.
return $this->single_upgrade( $upgrader, $options );
}
}
/**
* Fires after the theme is switched.
*
* @return array|bool
*/
public function process_activate_theme() {
$args = func_get_args();
$new_theme = $args[1]['new_theme'];
if ( ! is_object( $new_theme ) ) {
return false;
}
$new_name = $args[1]['new_name'];
$version = $new_theme->get( 'Version' );
$blog_name = is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : '';
return array(
sprintf(
/* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Theme name, 4: Theme version. */
esc_html__( '%1$s %2$s activated theme: %3$s, version %4$s', 'defender-security' ),
$blog_name,
$this->get_source_of_action(),
$new_name,
$version
),
self::CONTEXT_THEME,
);
}
/**
* Fires immediately after a plugin deletion.
* There is no way to get the plugin data (name, version) because it has been removed. Only slug.
*
* @return array
*/
public function process_delete_plugin(): array {
$args = func_get_args();
// If 'deleted'-arg is false then the plugin deletion wasn't successful.
if ( empty( $args[1]['deleted'] ) ) {
// Todo: extend Audit table with a new column for the result of the process and divide it into success and failure.
$failed_result = true;
} else {
$failed_result = false;
}
$plugin_file = $args[1]['plugin_file'];
$blog_name = is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : '';
return array(
sprintf(
$failed_result ?
/* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Plugin file. */
esc_html__( '%1$s %2$s was unable to delete plugin: %3$s', 'defender-security' ) :
/* translators: 1: Blog name, 2: Source of action. For e.g. Hub or a logged-in user, 3: Plugin file. */
esc_html__( '%1$s %2$s deleted plugin: %3$s', 'defender-security' ),
$blog_name,
$this->get_source_of_action(),
$plugin_file
),
self::CONTEXT_PLUGIN,
);
}
/**
* Provides a dictionary of audit event types and their descriptions.
*
* @return array Associative array of event keys and their descriptions.
*/
public function dictionary(): array {
return array(
self::ACTION_DEACTIVATED => esc_html__( 'deactivated', 'defender-security' ),
self::ACTION_UPGRADED => esc_html__( 'upgraded', 'defender-security' ),
self::ACTION_ACTIVATED => esc_html__( 'activated', 'defender-security' ),
self::ACTION_INSTALLED => esc_html__( 'installed', 'defender-security' ),
self::CONTEXT_THEME => esc_html__( 'theme', 'defender-security' ),
self::CONTEXT_PLUGIN => esc_html__( 'plugin', 'defender-security' ),
self::CONTEXT_CORE => esc_html__( 'WordPress', 'defender-security' ),
self::FILE_ADDED => esc_html__( 'File Added', 'defender-security' ),
self::FILE_MODIFIED => esc_html__( 'File Modified', 'defender-security' ),
);
}
/**
* Retrieves the absolute path of a plugin.
*
* @param string $slug The plugin slug or relative path.
*
* @return string The absolute path of the plugin.
*/
public function get_plugin_abs_path( string $slug ): string {
if ( ! is_file( $slug ) ) {
$slug = WP_PLUGIN_DIR . DIRECTORY_SEPARATOR . $slug;
}
return $slug;
}
}