Files

1451 lines
48 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* Sensor: Plugins and Themes
*
* Plugins and Themes sensor class file.
*
* @since 4.5.0
* @package wsal
* @subpackage sensors
*/
declare(strict_types=1);
namespace WSAL\WP_Sensors;
use WSAL\Helpers\Settings_Helper;
use WSAL\Controllers\Alert_Manager;
use WSAL\WP_Sensors\Helpers\WP_Plugins_Themes_Helper;
// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( '\WSAL\WP_Sensors\WP_Plugins_Themes_Sensor' ) ) {
/**
* Plugins & Themes sensor.
*
* 5000 User installed a plugin
* 5001 User activated a WordPress plugin
* 5002 User deactivated a WordPress plugin
* 5003 User uninstalled a plugin
* 5004 User upgraded a plugin
* 5005 User installed a theme
* 5006 User activated a theme
* 5007 User uninstalled a theme
* 5030 Plugin failed to update
* 5031 User updated a theme
* 5034 Updated translations for a plugin
* 5035 Updated translations for a theme
*
* @package wsal
* @subpackage sensors
* @since 4.5.0
*/
class WP_Plugins_Themes_Sensor {
/**
* List of Themes.
*
* @var array
*
* @since 4.5.0
*/
private static $old_themes = null;
/**
* List of Plugins.
*
* @var array
*
* @since 4.5.0
*/
private static $old_plugins = null;
/**
* Litst of all plugins
*
* @var array
*
* @since 5.3.4.1
*/
private static $plugins_available = null;
/**
* Stores the plugins data for the currently processed plugin
*
* @var array
*
* @since 5.3.0
*/
private static $plugins_data = array();
/**
* Stores the plugins data for the currently processed plugin
*
* @var array
*
* @since 5.4.0
*/
private static $reported_plugins = array();
/**
* Inits the main hooks
*
* @return void
*
* @since 4.5.0
*/
public static function init() {
$has_permission = ( current_user_can( 'install_plugins' ) || current_user_can( 'activate_plugins' ) ||
\current_user_can( 'delete_plugins' ) || current_user_can( 'update_plugins' ) || current_user_can( 'install_themes' ) );
self::event_init();
\add_action( 'switch_theme', array( __CLASS__, 'event_theme_activated' ) );
\add_action( 'deleted_theme', array( __CLASS__, 'on_deleted_theme' ), 10, 2 );
\add_action( 'wp_insert_post', array( __CLASS__, 'plugin_created_post' ), 10, 2 );
// Log plugin deletions, i.e. when a user click "Delete" in the plugins listing
// or choose plugin(s) and select Bulk actions -> Delete.
// Since WordPress 4.4 filters exists that are fired before and after plugin deletion.
\add_action( 'delete_plugin', array( __CLASS__, 'on_action_delete_plugin' ), 10, 1 );
\add_action( 'deleted_plugin', array( __CLASS__, 'on_action_deleted_plugin' ), 10, 2 );
/**
* At least the plugin bulk upgrades fires this action before upgrade
* We use it to fetch the current version of all plugins, before they are upgraded
*/
// \add_filter( 'upgrader_pre_install', array( __CLASS__, 'save_versions_before_update' ), 10, 2 );
// Fires after the upgrades has done it's thing.
// Check hook extra for upgrader initiator.
\add_action( 'upgrader_process_complete', array( __CLASS__, 'on_upgrader_process_complete' ), 10, 2 );
\add_action( 'upgrader_overwrote_package', array( __CLASS__, 'on_package_overwrite' ), 10, 3 );
\add_action( 'activated_plugin', array( __CLASS__, 'on_activated_plugin' ), 10, 2 );
\add_action( 'deactivated_plugin', array( __CLASS__, 'on_deactivated_plugin' ), 10, 1 );
\add_action( 'update_option_active_plugins', array( __CLASS__, 'on_active_plugins_update' ), 10, 2 );
\add_action( 'upgrader_process_complete', array( __CLASS__, 'on_plugin_or_theme_update' ), 10, 2 );
\add_filter( 'upgrader_post_install', array( __CLASS__, 'on_upgrader_post_install' ), 10, 3 );
\add_action( 'set_site_transient', array( __CLASS__, 'on_available_plugin_update' ), 10, 3 );
\add_action( 'set_site_transient', array( __CLASS__, 'on_available_theme_update' ), 10, 3 );
}
/**
* Maybe add thickbox class to the links, this is used by WordPress to trigger the view more information modal.
*
* @param string $url - the URL of the link to check.
*
* @return array $extra_css_classes - An array of extra CSS classes to be added to the link.
*
* @since 5.5.0
*/
public static function maybe_add_thickbox_class( $url ) {
$extra_css_classes = array();
$is_wp_repo_url = strpos( $url, 'plugin-install.php' );
if ( $is_wp_repo_url ) {
// Add thickbox class to the links, this is used by WordPress to trigger the view more information modal.
$extra_css_classes[] = 'thickbox';
}
return $extra_css_classes;
}
/**
* Build the WordPress repository URL for a plugin.
*
* @param string $plugin_slug - The plugin slug which is also the directory name of the plugin.
*
* @return string - The URL to the plugin's page in the WordPress repository.
*
* @since 5.5.0
*/
public static function get_plugin_wp_repo_url( $plugin_slug ) {
// Get correct url, network_admin_url also checks if we're in a multisite or not.
return network_admin_url( 'plugin-install.php?tab=plugin-information&plugin=' . $plugin_slug . '&TB_iframe=true&width=640&height=600' );
}
/**
* Plugin is deactivated
* plugin_name is like admin-menu-tree-page-view/index.php
*
* @param string $plugin_name Plugin name.
*
* @since 5.4.0
*/
public static function on_deactivated_plugin( $plugin_name ) {
if ( ! in_array( $plugin_name, self::$reported_plugins, true ) ) {
$plugin_data = \get_plugin_data( \WP_PLUGIN_DIR . '/' . $plugin_name, true, false );
$plugin_slug = dirname( $plugin_name );
Alert_Manager::trigger_event(
5002,
array(
'PluginFile' => \WP_PLUGIN_DIR . '/' . $plugin_name,
'PluginData' => (object) array(
'Name' => $plugin_data['Name'],
'PluginURI' => $plugin_data['PluginURI'],
'PluginRepoUrl' => self::get_plugin_wp_repo_url( $plugin_slug ),
'Version' => $plugin_data['Version'],
'Author' => $plugin_data['Author'],
'Network' => $plugin_data['Network'] ? 'True' : 'False',
),
)
);
self::$reported_plugins[ $plugin_name ] = $plugin_name;
}
}
/**
* Plugin is activated
* plugin_name is like admin-menu-tree-page-view/index.php
*
* @param string $plugin_name Plugin name.
* @param bool $network_wide Network wide.
*/
public static function on_activated_plugin( $plugin_name, $network_wide = null ) {
if ( ! in_array( $plugin_name, self::$reported_plugins, true ) ) {
$plugin_data = \get_plugin_data( WP_PLUGIN_DIR . '/' . $plugin_name, true, false );
$plugin_slug = dirname( $plugin_name );
Alert_Manager::trigger_event(
5001,
array(
'PluginFile' => WP_PLUGIN_DIR . '/' . $plugin_name,
'PluginData' => (object) array(
'Name' => $plugin_data['Name'],
'PluginRepoUrl' => self::get_plugin_wp_repo_url( $plugin_slug ),
'PluginURI' => $plugin_data['PluginURI'],
'Version' => $plugin_data['Version'],
'Author' => $plugin_data['Author'],
'Network' => $plugin_data['Network'] ? 'True' : 'False',
),
)
);
self::$reported_plugins[ $plugin_name ] = $plugin_name;
}
}
/**
* Called when plugins is updated or installed
* Called from class-wp-upgrader.php
*
* @param \Plugin_Upgrader $plugin_upgrader_instance Plugin_Upgrader instance. In other contexts, $this, might
* be a Theme_Upgrader or Core_Upgrade instance.
* @param array $arr_data Array of bulk item update data.
*
* @return void
*
* @since 5.3.4.1
*/
public static function on_upgrader_process_complete( $plugin_upgrader_instance, $arr_data ): void {
if ( empty( $plugin_upgrader_instance ) || empty( $arr_data ) ) {
return;
}
// Prevent events 5000 and 5005 from being triggered multiple times, triggered in this class on on_upgrader_post_install() before this method.
if ( Alert_Manager::has_triggered( 5000 ) || Alert_Manager::has_triggered( 5005 ) ) {
return;
}
// Check that required data is set.
if ( empty( $arr_data['type'] ) || empty( $arr_data['action'] ) ) {
return;
}
if ( isset( $arr_data['type'] ) && 'plugin' === $arr_data['type'] ) {
self::single_plugin_install( $plugin_upgrader_instance, $arr_data );
self::single_plugin_update( $plugin_upgrader_instance, $arr_data );
self::bulk_plugin_update( $plugin_upgrader_instance, $arr_data );
}
if ( isset( $arr_data['type'] ) && 'theme' === $arr_data['type'] ) {
self::theme_install( $plugin_upgrader_instance, $arr_data );
self::theme_upgrade( $plugin_upgrader_instance, $arr_data );
}
}
/**
* Capture event when single plugin is installed
*
* @param \Plugin_Upgrader $upgrader_instance Plugin_Upgrader instance. In other contexts, $this, might
* be a Theme_Upgrader or Core_Upgrade instance.
* @param array $arr_data Array of bulk item update data.
*
* @return void
*
* @since 5.3.4.1
*/
public static function theme_install( $upgrader_instance, $arr_data ): void {
// Must be type 'theme' and action 'install'.
if ( 'theme' !== $arr_data['type'] || 'install' !== $arr_data['action'] ) {
return;
}
if ( empty( $upgrader_instance->new_theme_data ) ) {
return;
}
// Install theme.
$destination_name = $upgrader_instance->result['destination_name'] ?? '';
if ( empty( $destination_name ) ) {
return;
}
$theme = \wp_get_theme( $destination_name );
if ( ! $theme->exists() ) {
return;
}
$new_theme_data = $upgrader_instance->new_theme_data;
Alert_Manager::trigger_event(
5005,
array(
'Theme' => (object) array(
'Name' => $new_theme_data['Name'],
'ThemeURI' => $theme->ThemeURI, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
'Description' => $theme->get( 'Description' ),
'Author' => $new_theme_data['Author'],
'Version' => $new_theme_data['Version'],
'get_template_directory' => $theme->get_template_directory(),
),
)
);
}
/**
* Capture event when single plugin is installed
*
* @param \Plugin_Upgrader $upgrader_instance Plugin_Upgrader instance. In other contexts, $this, might
* be a Theme_Upgrader or Core_Upgrade instance.
* @param array $arr_data Array of bulk item update data.
*
* @return void
*
* @since 5.3.4.1
*/
public static function theme_upgrade( $upgrader_instance, $arr_data ): void {
// Must be type 'theme' and action 'update'.
if ( 'theme' !== $arr_data['type'] || 'update' !== $arr_data['action'] ) {
return;
}
// If single install make an array so it look like bulk and we can use same code.
if ( isset( $arr_data['bulk'] ) && $arr_data['bulk'] && isset( $arr_data['themes'] ) ) {
$arr_themes = (array) $arr_data['themes'];
} else {
$arr_themes = array(
$arr_data['theme'],
);
}
foreach ( $arr_themes as $one_updated_theme ) {
$theme = \wp_get_theme( $one_updated_theme );
if ( ! is_a( $theme, 'WP_Theme' ) ) {
continue;
}
$theme_name = $theme->get( 'Name' );
$theme_version = $theme->get( 'Version' );
if ( ! $theme_name || ! $theme_version ) {
continue;
}
Alert_Manager::trigger_event(
5031,
array(
'Theme' => (object) array(
'Name' => $theme_name,
'ThemeURI' => $theme->ThemeURI, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
'Description' => $theme->Description, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
'Author' => $theme->Author, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
'Version' => $theme_version,
'get_template_directory' => $theme->get_template_directory(),
),
'ThemeStatus' => WP_Plugins_Themes_Helper::get_theme_status_label( $one_updated_theme ),
)
);
}
}
/**
* Capture event when single plugin is installed
*
* @param \Plugin_Upgrader $plugin_upgrader_instance Plugin_Upgrader instance. In other contexts, $this, might
* be a Theme_Upgrader or Core_Upgrade instance.
* @param array $arr_data Array of bulk item update data.
*
* @return void
*
* @since 5.3.4.1
*/
public static function single_plugin_install( $plugin_upgrader_instance, $arr_data ): void {
// Bail if not single plugin install data.
if ( ! isset( $arr_data['action'] ) || 'install' !== $arr_data['action'] || $plugin_upgrader_instance->bulk ) {
return;
}
$upgrader_skin_options = isset( $plugin_upgrader_instance->skin->options ) && is_array( $plugin_upgrader_instance->skin->options ) ? $plugin_upgrader_instance->skin->options : array();
$upgrader_skin_result = isset( $plugin_upgrader_instance->skin->result ) && is_array( $plugin_upgrader_instance->skin->result ) ? $plugin_upgrader_instance->skin->result : array();
$new_plugin_data = $plugin_upgrader_instance->new_plugin_data ?? array();
$plugin_slug = $upgrader_skin_result['destination_name'] ?? '';
$plugin = $plugin_upgrader_instance->plugin_info();
$context = array(
'plugin_slug' => $plugin_slug,
'plugin_name' => $new_plugin_data['Name'] ?? '',
'plugin_title' => $new_plugin_data['Title'] ?? '',
'plugin_url' => $new_plugin_data['PluginURI'] ?? '',
'plugin_version' => $new_plugin_data['Version'] ?? '',
'plugin_author' => $new_plugin_data['Author'] ?? '',
'plugin_requires_wp' => $new_plugin_data['RequiresWP'] ?? '',
'plugin_requires_php' => $new_plugin_data['RequiresPHP'] ?? '',
// Prevent warning Undefined array key "Network" in an edge case when an invalid plugin.zip is uploaded to the site.
'plugin_network' => ( $new_plugin_data['Network'] ?? false ) ? \esc_html__( 'True', 'wp-security-audit-log' ) : \esc_html__( 'False', 'wp-security-audit-log' ),
'plugin_path' => \WP_PLUGIN_DIR . \DIRECTORY_SEPARATOR . $plugin,
);
if ( isset( $new_plugin_data['UpdateURI'] ) ) {
$context['plugin_update_uri'] = $new_plugin_data['UpdateURI'];
}
// Check for premium version being installed / updated.
if ( in_array( $plugin, array( \WpSecurityAuditLog::PREMIUM_VERSION_WHOLE_PLUGIN_NAME, \WpSecurityAuditLog::FREE_VERSION_WHOLE_PLUGIN_NAME, \WpSecurityAuditLog::NOFS_VERSION_WHOLE_PLUGIN_NAME ), true ) ) {
return;
}
$install_source = 'web';
if ( isset( $upgrader_skin_options['type'] ) ) {
$install_source = \strtolower( (string) $upgrader_skin_options['type'] );
}
$context['plugin_install_source'] = $install_source;
// If uploaded plugin store name of ZIP.
// phpcs:ignore WordPress.Security.NonceVerification.Missing
if ( 'upload' === $install_source && isset( $_FILES['pluginzip']['name'] ) ) {
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$plugin_upload_name = sanitize_text_field( $_FILES['pluginzip']['name'] );
$context['plugin_upload_name'] = $plugin_upload_name;
}
if ( ! is_a( $plugin_upgrader_instance->skin->result, 'WP_Error' ) ) {
// Check if the plugin is already installed and we are here because of an update via upload.
$old_plugins = self::get_old_plugins();
$plugin_slug = dirname( $plugin );
if ( isset( $old_plugins[ $plugin ] ) ) {
return;
} else {
// No - it is a real update - no need to fire post activate event.
\remove_action( 'upgrader_overwrote_package', array( __CLASS__, 'on_package_overwrite' ), 10, 3 );
}
Alert_Manager::trigger_event(
5000,
array(
'PluginData' => (object) array(
'Name' => $context['plugin_name'],
'PluginURI' => $context['plugin_url'],
'PluginRepoUrl' => self::get_plugin_wp_repo_url( $plugin_slug ),
'Version' => $context['plugin_version'],
'Author' => $context['plugin_author'],
'Network' => $context['plugin_network'],
'Slug' => $context['plugin_slug'],
'Title' => $context['plugin_title'],
'plugin_dir_path' => $context['plugin_path'],
),
)
);
}
}
/**
* Capture event when single plugin is updated
*
* @param \Plugin_Upgrader $plugin_upgrader_instance Plugin_Upgrader instance. In other contexts, $this, might
* be a Theme_Upgrader or Core_Upgrade instance.
* @param array $arr_data Array of bulk item update data.
*
* @return void
*
* @since 5.3.4.1
*/
public static function single_plugin_update( $plugin_upgrader_instance, $arr_data ): void {
if ( ! isset( $arr_data['action'] ) || 'update' !== $arr_data['action'] || $plugin_upgrader_instance->bulk ) {
return;
}
if ( is_array( $arr_data['plugin'] ) && isset( $arr_data['plugin'] ) && 'wp-security-audit-log.php' === $arr_data['plugin'] ) {
return;
}
// No plugin info in instance, so get it ourself.
$plugin_data = array();
if ( file_exists( \WP_PLUGIN_DIR . '/' . $arr_data['plugin'] ) ) {
$plugin_data = \get_plugin_data( \WP_PLUGIN_DIR . '/' . $arr_data['plugin'], true, false );
}
$plugin_slug = dirname( $arr_data['plugin'] );
$context = array(
'plugin_slug' => $plugin_slug,
'plugin_name' => $plugin_data['Name'] ?? '',
'plugin_title' => $plugin_data['Title'] ?? '',
'plugin_description' => $plugin_data['Description'] ?? '',
'plugin_author' => $plugin_data['Author'] ?? '',
'plugin_version' => $plugin_data['Version'] ?? '',
'plugin_url' => $plugin_data['PluginURI'] ?? '',
'plugin_network' => ( $plugin_data['Network'] ?? false ) ? \esc_html__( 'True', 'wp-security-audit-log' ) : \esc_html__( 'False', 'wp-security-audit-log' ),
'plugin_path' => \WP_PLUGIN_DIR . \DIRECTORY_SEPARATOR . $arr_data['plugin'],
);
// Add Update URI if it is set. Available since WP 5.8.
if ( isset( $plugin_data['UpdateURI'] ) ) {
$context['plugin_update_uri'] = $plugin_data['UpdateURI'];
}
if ( ! \is_wp_error( \validate_plugin( $arr_data['plugin'] ) ) ) {
$old_plugins = self::get_old_plugins();
$old_version = ( isset( $old_plugins[ $arr_data['plugin'] ] ) ) ? $old_plugins[ $arr_data['plugin'] ]['Version'] : false;
if ( $old_version !== $context['plugin_version'] ) {
Alert_Manager::trigger_event(
5004,
array(
'PluginFile' => $arr_data['plugin'],
'PluginData' => (object) array(
'Name' => $context['plugin_name'],
'PluginURI' => $context['plugin_url'],
'PluginRepoUrl' => self::get_plugin_wp_repo_url( $plugin_slug ),
'Version' => $context['plugin_version'],
'Author' => $context['plugin_author'],
'Network' => $context['plugin_network'],
'Slug' => $context['plugin_slug'],
'Title' => $context['plugin_title'],
'plugin_dir_path' => $context['plugin_path'],
),
'OldVersion' => $old_version,
'PluginStatus' => WP_Plugins_Themes_Helper::get_plugin_status_label( $arr_data['plugin'] ),
)
);
}
}
}
/**
* Capture event when single plugin is updated
*
* @param \Plugin_Upgrader $plugin_upgrader_instance Plugin_Upgrader instance. In other contexts, $this, might
* be a Theme_Upgrader or Core_Upgrade instance.
* @param array $arr_data Array of bulk item update data.
*
* @return void
*
* @since 5.3.4.1
*/
public static function bulk_plugin_update( $plugin_upgrader_instance, $arr_data ): void {
// Bail if not bulk plugin update.
if ( ! isset( $arr_data['bulk'] ) || ! $arr_data['bulk'] || ! isset( $arr_data['action'] ) || 'update' !== $arr_data['action'] ) {
return;
}
$plugins_updated = isset( $arr_data['plugins'] ) ? (array) $arr_data['plugins'] : array();
foreach ( $plugins_updated as $plugin ) {
$arr_data['plugin'] = $plugin;
if ( is_array( $arr_data['plugin'] ) && isset( $arr_data['plugin'] ) && 'wp-security-audit-log.php' === $arr_data['plugin'] ) {
return;
}
// No plugin info in instance, so get it ourself.
$plugin_data = array();
if ( file_exists( \WP_PLUGIN_DIR . '/' . $arr_data['plugin'] ) ) {
$plugin_data = \get_plugin_data( \WP_PLUGIN_DIR . '/' . $arr_data['plugin'], true, false );
}
$plugin_slug = dirname( $arr_data['plugin'] );
$context = array(
'plugin_slug' => $plugin_slug,
'plugin_name' => $plugin_data['Name'] ?? '',
'plugin_title' => $plugin_data['Title'] ?? '',
'plugin_description' => $plugin_data['Description'] ?? '',
'plugin_author' => $plugin_data['Author'] ?? '',
'plugin_version' => $plugin_data['Version'] ?? '',
'plugin_url' => $plugin_data['PluginURI'] ?? '',
'plugin_network' => ( $plugin_data['Network'] ?? false ) ? \esc_html__( 'True', 'wp-security-audit-log' ) : \esc_html__( 'False', 'wp-security-audit-log' ),
'plugin_path' => \WP_PLUGIN_DIR . \DIRECTORY_SEPARATOR . $arr_data['plugin'],
);
// Add Update URI if it is set. Available since WP 5.8.
if ( isset( $plugin_data['UpdateURI'] ) ) {
$context['plugin_update_uri'] = $plugin_data['UpdateURI'];
}
if ( ! \is_wp_error( \validate_plugin( $arr_data['plugin'], ) ) ) {
$old_plugins = self::get_old_plugins();
$old_version = ( isset( $old_plugins[ $arr_data['plugin'] ] ) ) ? $old_plugins[ $arr_data['plugin'] ]['Version'] : false;
if ( $old_version !== $context['plugin_version'] ) {
Alert_Manager::trigger_event(
5004,
array(
'PluginFile' => $arr_data['plugin'],
'PluginData' => (object) array(
'Name' => $context['plugin_name'],
'PluginURI' => $context['plugin_url'],
'PluginRepoUrl' => self::get_plugin_wp_repo_url( $plugin_slug ),
'Version' => $context['plugin_version'],
'Author' => $context['plugin_author'],
'Network' => $context['plugin_network'],
'Slug' => $context['plugin_slug'],
'Title' => $context['plugin_title'],
'plugin_dir_path' => $context['plugin_path'],
),
'OldVersion' => $old_version,
'PluginStatus' => WP_Plugins_Themes_Helper::get_plugin_status_label( $arr_data['plugin'] ),
)
);
}
}
}
}
/**
* Fires when the upgrader has successfully overwritten a currently installed
* plugin or theme with an uploaded zip package.
*
* @param string $package The package file.
* @param array $plugin_data The new plugin data.
* @param string $package_type The package type (plugin or theme).
*
* @since 4.5.0
*/
public static function on_package_overwrite( $package, $plugin_data, $package_type ) {
if ( 'plugin' !== $package_type ) {
return;
}
if ( is_array( $plugin_data ) && isset( $plugin_data['TextDomain'] ) && 'wp-security-audit-log' === $plugin_data['TextDomain'] ) {
return;
}
$plugin_slug = self::extract_plugin_filename( $plugin_data['TextDomain'] );
$context = array(
'plugin_slug' => $plugin_slug,
'plugin_name' => $plugin_data['Name'],
'plugin_title' => $plugin_data['Title'],
'plugin_description' => $plugin_data['Description'],
'plugin_author' => $plugin_data['Author'],
'plugin_version' => $plugin_data['Version'],
'plugin_url' => $plugin_data['PluginURI'],
'plugin_network' => ( $plugin_data['Network'] ?? false ) ? \esc_html__( 'True', 'wp-security-audit-log' ) : \esc_html__( 'False', 'wp-security-audit-log' ),
'plugin_path' => \WP_PLUGIN_DIR . \DIRECTORY_SEPARATOR . $plugin_slug,
);
if ( ! \is_wp_error( \validate_plugin( $plugin_slug ) ) ) {
$old_plugins = self::get_old_plugins();
$old_version = ( isset( $old_plugins[ $plugin_slug ] ) ) ? $old_plugins[ $plugin_slug ]['Version'] : false;
if ( $old_version !== $context['plugin_version'] ) {
Alert_Manager::trigger_event(
5004,
array(
'PluginFile' => $plugin_slug,
'PluginData' => (object) array(
'Name' => $context['plugin_name'],
'PluginURI' => $context['plugin_url'],
'PluginRepoUrl' => self::get_plugin_wp_repo_url( $plugin_slug ),
'Version' => $context['plugin_version'],
'Author' => $context['plugin_author'],
'Network' => $context['plugin_network'],
'Slug' => $context['plugin_slug'],
'Title' => $context['plugin_title'],
'plugin_dir_path' => $context['plugin_path'],
),
'OldVersion' => $old_version,
'PluginStatus' => WP_Plugins_Themes_Helper::get_plugin_status_label( $plugin_slug ),
)
);
}
}
}
/**
* Extracts plugin filename from the plugin text domain.
*
* @param string $plugin_textdomain - The text domain of the plugin.
*
* @return string
*
* @since 5.3.4.1
*/
public static function extract_plugin_filename( $plugin_textdomain ): string {
foreach ( self::get_all_plugins() as $plugin_file => $plugin ) {
if ( $plugin['TextDomain'] === $plugin_textdomain ) {
return $plugin_file;
}
}
return '';
}
/**
* Store information about a plugin before it gets deleted.
* Called from action `deleted_plugin` that is fired just before the plugin will be deleted.
*
* @param string $plugin_file Path to the plugin file relative to the plugins directory.
*
* @return void
*
* @since 5.3.0
*/
public static function on_action_delete_plugin( $plugin_file ) {
self::$plugins_data[ $plugin_file ] = \get_plugin_data( \WP_PLUGIN_DIR . '/' . $plugin_file, true, false );
}
/**
* Log plugin deletion.
* Called from action `deleted_plugin` that is fired just after a plugin has been deleted.
*
* @param string $plugin_file Path to the plugin file relative to the plugins directory.
* @param bool $deleted Whether the plugin deletion was successful.
*
* @return void
*
* @since 5.3.0
*/
public static function on_action_deleted_plugin( $plugin_file, $deleted ) {
if ( ! $deleted ) {
return;
}
if ( empty( self::$plugins_data[ $plugin_file ] ) ) {
return;
}
$plugin_file = $plugin_file;
$plugin_name = \strip_tags( self::$plugins_data[ $plugin_file ]['Title'] );
$plugin_data = self::$plugins_data[ $plugin_file ];
$plugin_slug = dirname( $plugin_file );
Alert_Manager::trigger_event(
5003,
array(
'PluginFile' => $plugin_file,
'PluginData' => (object) array(
'Name' => $plugin_name,
'Version' => $plugin_data['Version'],
'PluginRepoUrl' => self::get_plugin_wp_repo_url( $plugin_slug ),
),
)
);
}
/**
* Handles a theme deletion attempt.
*
* @param string $stylesheet Stylesheet of the theme to delete.
* @param bool $deleted Whether the theme deletion was successful.
*
* @since 4.5.0
*/
public static function on_deleted_theme( $stylesheet, $deleted ) {
if ( ! $deleted ) {
return;
}
if ( ! array_key_exists( $stylesheet, self::get_old_themes() ) ) {
return;
}
$theme = self::get_old_themes()[ $stylesheet ];
Alert_Manager::trigger_event(
5007,
array(
'Theme' => (object) array(
'Name' => $theme->Name, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
'ThemeURI' => $theme->ThemeURI, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
'Description' => $theme->Description, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
'Author' => $theme->Author, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
'Version' => $theme->Version, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
'get_template_directory' => $theme->get_template_directory(),
),
)
);
}
/**
* Triggered when a user accesses the admin area.
*
* @since 4.5.0
*/
public static function event_init() {
self::get_old_themes();
self::get_old_plugins();
}
/**
* Activated a theme.
*
* @param string $theme_name - Theme name.
*
* @since 4.5.0
*/
public static function event_theme_activated( $theme_name ) {
$theme = null;
foreach ( wp_get_themes() as $item ) {
if ( $theme_name === $item->Name ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
$theme = $item;
break;
}
}
if ( null === $theme ) {
Alert_Manager::log_error(
'Could not locate theme named "' . $theme . '".',
array(
'ThemeName' => $theme_name,
'Themes' => wp_get_themes(),
)
);
return;
}
Alert_Manager::trigger_event(
5006,
array(
'Theme' => (object) array(
'Name' => $theme->Name, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
'ThemeURI' => $theme->ThemeURI, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
'Description' => $theme->Description, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
'Author' => $theme->Author, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
'Version' => $theme->Version, // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
'get_template_directory' => $theme->get_template_directory(),
),
)
);
}
/**
* Get event code by post type.
*
* @param object $post - Post object.
* @param int $type_post - Code for post.
* @param int $type_page - Code for page.
* @param int $type_custom - Code for custom post type.
*
* @return false|int
*
* @since 4.5.0
*/
protected static function get_event_type_for_post_type( $post, $type_post, $type_page, $type_custom ) {
if ( empty( $post ) || ! isset( $post->post_type ) ) {
return false;
}
switch ( $post->post_type ) {
case 'page':
return $type_page;
case 'post':
return $type_post;
default:
return $type_custom;
}
}
/**
* Method: Remove the PHP file after `/` in the plugin
* directory name.
*
* For example, it will remove `/akismet.php` from
* `akismet/akismet.php`.
*
* @param string $plugin Plugin name.
*
* @return string
*
* @since 4.5.0
*/
public static function get_plugin_dir( $plugin ) {
$position = strpos( $plugin, '/' );
if ( false !== $position ) {
$plugin = substr_replace( $plugin, '', $position );
}
return $plugin;
}
/**
* Method: Return plugin file name.
*
* @param string $plugin_name - Plugin name.
*
* @return string
*
* @since 4.5.0
*/
public static function get_plugin_file_name( $plugin_name ) {
// Verify parameter.
if ( empty( $plugin_name ) ) {
return '';
}
// Get all plugins.
$all_plugins = get_plugins();
$plugin_filename = '';
if ( ! empty( $all_plugins ) && is_array( $all_plugins ) ) {
foreach ( $all_plugins as $plugin_file => $plugin_data ) {
if ( $plugin_name === $plugin_data['Name'] ) {
$plugin_filename = $plugin_file;
}
}
}
return $plugin_filename;
}
/**
* Method: Search and return theme object by name.
*
* @param string $theme_name - Theme name.
*
* @return WP_Theme|null
*
* @since 4.5.0
*/
public static function get_theme_by_name( $theme_name ) {
// Check if $theme_name is empty.
if ( empty( $theme_name ) ) {
return null;
}
// Get all themes.
$all_themes = wp_get_themes();
$theme = null;
if ( ! empty( $all_themes ) ) {
foreach ( $all_themes as $theme_slug => $theme_obj ) {
if ( $theme_name === $theme_slug || $theme_name === $theme_obj->get( 'Name' ) ) {
$theme = $theme_obj;
break;
}
}
}
return $theme;
}
/**
* Detects posts created by plugin actions.
*
* @param int $post_id - The post ID.
* @param \WP_Post $post - The post object.
*
* @return void
*
* @since 4.5.0
* @since 5.6.2 Refactored to better detect posts created by plugin actions.
*/
public static function plugin_created_post( $post_id, $post ) {
if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
return;
}
// Ignore if the request is coming from post editor.
if ( isset( $_POST['_wp_http_referer'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
$referrer = \esc_url_raw( \wp_unslash( $_POST['_wp_http_referer'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
$parsed_url = \wp_parse_url( $referrer );
if ( isset( $parsed_url['path'] ) && 'post' === basename( $parsed_url['path'], '.php' ) ) {
return;
}
}
$get_action = isset( $_GET['action'] ) ? \sanitize_text_field( \wp_unslash( $_GET['action'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$post_action = isset( $_POST['action'] ) ? \sanitize_text_field( \wp_unslash( $_POST['action'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing
$plugin_name = isset( $_GET['plugin'] ) ? \sanitize_text_field( \wp_unslash( $_GET['plugin'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$wp_actions = array( 'editpost', 'heartbeat', 'inline-save', 'trash', 'untrash', 'vc_save' );
// Handle GET-based plugin actions (e.g. plugin activation via admin URL).
if ( $get_action && ! in_array( $get_action, $wp_actions, true ) ) {
if (
! in_array( $post->post_type, Alert_Manager::get_ignored_post_types(), true )
&& ! empty( $post->post_title )
&& $plugin_name
&& ! \is_wp_error( \validate_plugin( $plugin_name ) )
) {
$plugin_data = \get_plugin_data( \trailingslashit( \WP_PLUGIN_DIR ) . $plugin_name );
$plugin_slug = dirname( $plugin_name );
$editor_link = self::get_editor_link( $post );
Alert_Manager::trigger_event(
5019,
array(
'PluginName' => ( $plugin_data && isset( $plugin_data['Name'] ) ) ? $plugin_data['Name'] : false,
'PostID' => $post->ID,
'PostType' => $post->post_type,
'PostTitle' => $post->post_title,
'PostStatus' => $post->post_status,
'Username' => 'Plugins',
$editor_link['name'] => $editor_link['value'],
'PluginData' => (object) array(
'PluginRepoUrl' => self::get_plugin_wp_repo_url( $plugin_slug ),
),
)
);
}
}
// Handle POST-based plugin actions that include a plugin identifier in the URL.
if ( $post_action && ! in_array( $post_action, $wp_actions, true ) ) {
if (
! in_array( $post->post_type, Alert_Manager::get_ignored_post_types(), true )
&& ! empty( $post->post_title )
) {
// Ignore WooCommerce Bulk Stock Management page.
$post_page = isset( $_POST['page'] ) ? \sanitize_text_field( \wp_unslash( $_POST['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing
$is_woo_bulk_stock = 'woocommerce-bulk-stock-management' === $post_page;
// Ignore MainWP plugin requests.
$mainwp_actions = array( 'restore', 'unpublish', 'publish' );
$is_mainwp_request = isset( $_POST['mainwpsignature'] ) && in_array( $post_action, $mainwp_actions, true ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
if ( ! $is_woo_bulk_stock && ! $is_mainwp_request && $plugin_name && ! \is_wp_error( \validate_plugin( $plugin_name ) ) ) {
$plugin_data = \get_plugin_data( \trailingslashit( \WP_PLUGIN_DIR ) . $plugin_name );
$plugin_slug = dirname( $plugin_name );
$editor_link = self::get_editor_link( $post );
Alert_Manager::trigger_event(
5019,
array(
'PluginName' => ( $plugin_data && isset( $plugin_data['Name'] ) ) ? $plugin_data['Name'] : false,
'PostID' => $post->ID,
'PostType' => $post->post_type,
'PostTitle' => $post->post_title,
'PostStatus' => $post->post_status,
'Username' => 'Plugins',
$editor_link['name'] => $editor_link['value'],
'PluginData' => (object) array(
'PluginRepoUrl' => self::get_plugin_wp_repo_url( $plugin_slug ),
),
)
);
}
}
}
}
/**
* Get editor link.
*
* @param object $post - The post object.
* @return array $editor_link name and value link.
*
* @since 4.5.0
*/
private static function get_editor_link( $post ) {
$name = 'EditorLink';
$name .= ( 'page' === $post->post_type ) ? 'Page' : 'Post';
$value = get_edit_post_link( $post->ID );
$editor_link = array(
'name' => $name,
'value' => $value,
);
return $editor_link;
}
/**
* Get old themes - That must be done BEFORE themes changes apply globally. It stores "image" the themes before changes.
*
* @return array
*
* @since 5.3.4.1
*/
private static function get_old_themes(): array {
if ( null === self::$old_themes ) {
self::$old_themes = \wp_get_themes();
}
return self::$old_themes;
}
/**
* Get old plugins. That must apply before plugins changes apply globally. It stores "image" the plugins before changes.
*
* @return array
*
* @since 5.3.4.1
*/
private static function get_old_plugins(): array {
if ( null === self::$old_plugins ) {
self::$old_plugins = \get_plugins();
}
return self::$old_plugins;
}
/**
* To prevent $silent flag set to true in the WP core we are using this method.
*
* @param array $old_value - Old value with active plugins.
* @param array $new_value - New value with active plugins.
*
* @return void
*
* @since 5.4.0
*/
public static function on_active_plugins_update( $old_value, $new_value ) {
if ( \is_array( $new_value ) && \is_array( $old_value ) ) {
foreach ( $new_value as $plugin ) {
if ( ! in_array( $plugin, self::$reported_plugins, true ) ) {
// Plugin is not reported neither as active nor as deactivated.
// Check if that plugin exists in the old value. If it does, it means that the plugin was deactivated.
if ( ! in_array( $plugin, $old_value, true ) ) {
// Plugin was activated.
self::on_activated_plugin( $plugin );
self::$reported_plugins[ $plugin ] = $plugin;
}
}
}
foreach ( $old_value as $plugin ) {
if ( ! in_array( $plugin, self::$reported_plugins, true ) ) {
// Plugin is not reported neither as active nor as deactivated.
// Check if that plugin exists in the old value. If it does, it means that the plugin was deactivated.
if ( ! in_array( $plugin, $new_value, true ) ) {
// Plugin was deactivated.
self::on_deactivated_plugin( $plugin );
self::$reported_plugins[ $plugin ] = $plugin;
}
}
}
}
}
/**
* Get all plugins.
*
* @return array
*
* @since 5.3.4.1
*/
private static function get_all_plugins(): array {
if ( null === self::$plugins_available ) {
self::$plugins_available = \get_plugins();
}
return self::$plugins_available;
}
/**
* Notify when a plugin translation is updated.
*
* @param \WP_Upgrader|\Language_Pack_Upgrader $upgrader \WP_Upgrader instance. In other contexts this might be a Theme_Upgrader, Plugin_Upgrader, Core_Upgrade, or Language_Pack_Upgrader instance.
* @param array $options Array of bulk item update data.
*
* @since 5.5.0
*/
public static function on_plugin_or_theme_update( $upgrader, $options ) {
if ( ! isset( $options['type'] ) || ! isset( $options['action'] ) ) {
return;
}
if ( 'translation' !== $options['type'] || 'update' !== $options['action'] ) {
return;
}
$translations = $options['translations'];
foreach ( $translations as $translation ) {
// Only proceed if the translation is for a plugin or a theme.
if ( 'plugin' !== $translation['type'] && 'theme' !== $translation['type'] ) {
continue;
}
/**
* Example of $translation for a plugin:
* (
* [language] => es_ES
* [type] => plugin
* [slug] => wp-statistics
* [version] => 14.15.3
* )
*/
/**
* Example of $translation for a theme:
* (
* [language] => es_ES
* [type] => theme
* [slug] => generatepress
* [version] => 3.6.0
* )
*/
$name = '';
if ( method_exists( $upgrader, 'get_name_for_update' ) ) {
// Name - e.g. "WordPress".
$name = $upgrader->get_name_for_update( (object) $translation );
}
// If name is empty, let's use the slug as a fallback.
if ( empty( $name ) && ! empty( $translation['slug'] ) ) {
$name = $translation['slug'];
}
if ( 'plugin' === $translation['type'] ) {
Alert_Manager::trigger_event(
5034,
array(
'plugin_name' => $name,
'language' => $translation['language'],
)
);
}
if ( 'theme' === $translation['type'] ) {
Alert_Manager::trigger_event(
5035,
array(
'theme_name' => $name,
'language' => $translation['language'],
)
);
}
}
}
/**
* Fallback method in case events 5000 and 5005 were not triggered.
*
* This method has been added to handle the case when a plugin or theme is installed via ManageWP,
* which is not reported by the on_upgrader_process_complete method.
*
* @param bool $response - Installation response.
* @param array $hook_extra - Extra arguments passed to hooked filters.
* @param array $result - Installation result data.
*
* @return bool $response - The installation response after the installation has finished
*
* @since 5.5.0
*/
public static function on_upgrader_post_install( $response, $hook_extra, $result ) {
/**
* If $hook_extra['action'] is not set or not equal to 'install', return early.
* This works both for themes and plugins.
*/
if ( ! isset( $hook_extra['action'] ) || 'install' !== $hook_extra['action'] ) {
return $response;
}
if ( isset( $result['destination_name'] ) ) {
$folder_name = WP_Plugins_Themes_Helper::trim_folder_name( $result['destination_name'] );
$is_plugin = WP_Plugins_Themes_Helper::does_dir_or_file_exist( $folder_name, 'plugin' );
if ( $is_plugin ) {
$event_plugin_data = WP_Plugins_Themes_Helper::get_plugin_event_info_from_folder( $folder_name );
if ( is_array( $event_plugin_data ) ) {
Alert_Manager::trigger_event(
5000,
array(
'PluginData' => (object) $event_plugin_data,
)
);
}
}
$is_theme = WP_Plugins_Themes_Helper::does_dir_or_file_exist( $folder_name, 'theme' );
if ( $is_theme ) {
$theme_event_data = WP_Plugins_Themes_Helper::get_theme_event_info_from_folder( $folder_name );
if ( is_array( $theme_event_data ) ) {
Alert_Manager::trigger_event(
5005,
array(
'Theme' => (object) $theme_event_data,
)
);
}
}
}
return $response;
}
/**
* Notify when a plugin update is available.
*
* @param string $transient - The name of the site transient.
* @param mixed $value - Site transient value.
* @param int $expiration - Time until expiration in seconds.
*
* @since 5.6.0
*/
public static function on_available_plugin_update( $transient, $value, $expiration ) {
// If this is not the update_plugins transient, return early.
if ( 'update_plugins' !== $transient ) {
return;
}
// If there aren't updates, return early.
if ( empty( $value->response ) || ! is_array( $value->response ) ) {
return;
}
$notified_updates = Settings_Helper::get_option_value( 'notified_plugin_updates', array() );
$all_plugins = \get_plugins();
foreach ( $value->response as $plugin_file => $plugin_data ) {
// Double check if the plugin file exists.
if ( ! isset( $all_plugins[ $plugin_file ] ) ) {
continue;
}
$new_available_version = \esc_html( $plugin_data->new_version );
$last_notified_version = $notified_updates[ $plugin_file ] ?? null;
// Check if this version was already notified, and in that case, skip the event for this plugin.
if ( $last_notified_version && version_compare( $last_notified_version, $new_available_version, '>=' ) ) {
continue;
}
$plugin_folder_name = self::get_plugin_dir( $plugin_file );
$event_plugin_data = WP_Plugins_Themes_Helper::get_plugin_event_info_from_folder( $plugin_folder_name );
if ( is_array( $event_plugin_data ) ) {
$plugin_name = $event_plugin_data['Name'];
$plugin_current_version = $event_plugin_data['Version'];
$update_admin_screen = \esc_url( \network_admin_url( 'update-core.php' ) );
$server_address = isset( $_SERVER['SERVER_ADDR'] ) ? \sanitize_text_field( \wp_unslash( $_SERVER['SERVER_ADDR'] ) ) : '127.0.0.1';
Alert_Manager::trigger_event(
5032,
array(
'CurrentUserID' => 0,
'ClientIP' => $server_address,
'PluginName' => $plugin_name,
'CurrentPluginVersion' => $plugin_current_version,
'NewPluginVersion' => $new_available_version,
'UpdateAdminUrl' => $update_admin_screen,
)
);
// Mark this plugin update as notified, use the current new version as reference.
$notified_updates[ $plugin_file ] = $new_available_version;
}
}
// Update the notified updates option.
Settings_Helper::set_option_value( 'notified_plugin_updates', $notified_updates );
}
/**
* Notify when a theme update is available.
*
* @param string $transient - The name of the site transient.
* @param mixed $value - Site transient value.
* @param int $expiration - Time until expiration in seconds.
*
* @since 5.6.0
*/
public static function on_available_theme_update( $transient, $value, $expiration ) {
// If this is not the update_plugins transient, return early.
if ( 'update_themes' !== $transient ) {
return;
}
// If there aren't updates, return early.
if ( empty( $value->response ) || ! is_array( $value->response ) ) {
return;
}
$notified_updates = Settings_Helper::get_option_value( 'notified_theme_updates', array() );
foreach ( $value->response as $theme_folder => $theme_data ) {
$new_available_version = \esc_html( $theme_data['new_version'] );
$last_notified_version = $notified_updates[ $theme_folder ] ?? null;
// Check if this version was already notified, and in that case, skip the event for this plugin.
if ( $last_notified_version && version_compare( $last_notified_version, $new_available_version, '>=' ) ) {
continue;
}
$theme_event_data = WP_Plugins_Themes_Helper::get_theme_event_info_from_folder( $theme_folder );
if ( is_array( $theme_event_data ) ) {
$theme_name = $theme_event_data['Name'];
$theme_current_version = $theme_event_data['Version'];
$update_admin_screen = \esc_url( \network_admin_url( 'update-core.php' ) );
$server_address = isset( $_SERVER['SERVER_ADDR'] ) ? \sanitize_text_field( \wp_unslash( $_SERVER['SERVER_ADDR'] ) ) : '127.0.0.1';
Alert_Manager::trigger_event(
5033,
array(
'CurrentUserID' => 0,
'ClientIP' => $server_address,
'ThemeName' => $theme_name,
'CurrentThemeVersion' => $theme_current_version,
'NewThemeVersion' => $new_available_version,
'UpdateAdminUrl' => $update_admin_screen,
)
);
// Mark this plugin update as notified, use the current new version as reference.
$notified_updates[ $theme_folder ] = $new_available_version;
}
}
// Update the notified updates option.
Settings_Helper::set_option_value( 'notified_theme_updates', $notified_updates );
}
}
}