File: /var/www/html/wptoho/wp-content/plugins/defender-security/src/component/audit/class-menu-audit.php
<?php
/**
* Handles the auditing of navigation menu events.
*
* @package WP_Defender\Component\Audit
*/
namespace WP_Defender\Component\Audit;
use Calotes\Helper\Array_Cache;
use WP_Defender\Model\Audit_Log;
/**
* Handles the auditing of navigation menu events such as menu creation.
*
* @author Shoaib Ali
* @since 2.6.1
*/
class Menu_Audit extends Audit_Event {
public const ACTION_CREATED = 'created';
/**
* Type of audit.
*
* @var string
*/
protected $type = 'Navigation Menu';
/**
* Menu_Audit constructor.
* Hooks into the admin_init action to cache the old menu state.
*/
public function __construct() {
add_action( 'admin_init', array( $this, 'cache_old_menu' ) );
}
/**
* Cache old menu before updating.
*/
public function cache_old_menu() {
$script_name = filter_var( defender_get_data_from_request( 'SCRIPT_NAME', 's' ), FILTER_SANITIZE_URL );
if ( filter_input( INPUT_POST, 'action' ) ) {
$action_type = filter_input( INPUT_POST, 'action' );
} else {
$action_type = filter_input( INPUT_GET, 'action' );
}
if ( filter_input( INPUT_POST, 'menu' ) ) {
$term_id = filter_input( INPUT_POST, 'menu' );
} else {
$term_id = filter_input( INPUT_GET, 'menu' );
}
if (
'nav-menus.php' === basename( $script_name )
&& ! empty( $action_type )
&& ! empty( $term_id )
) {
Array_Cache::append(
'menu_updated',
$this->build_menu( $term_id ),
'audit'
);
}
}
/**
* Constructs an array representing the menu structure based on the provided term ID.
*
* @param mixed $term_id The ID of the menu.
*
* @return array
*/
public function build_menu( $term_id ): array {
$menu = array();
$menu_obj = wp_get_nav_menu_object( $term_id );
if ( ! empty( $menu_obj ) ) {
$menu['term_id'] = $menu_obj->term_id;
$menu['name'] = $menu_obj->name;
$menu['items'] = array();
$items = wp_get_nav_menu_items( $menu_obj->term_id );
if ( ! empty( $items ) ) {
foreach ( $items as $item ) {
array_push(
$menu['items'],
array(
'item_id' => $item->ID,
'title' => $item->title,
'url' => $item->url,
'object' => $item->object,
'menu_order' => $item->menu_order,
'menu_item_parent' => $item->menu_item_parent,
)
);
}
}
}
return $menu;
}
/**
* Returns an array of hooks associated with various menu actions for auditing purposes.
*
* @return array
*/
public function get_hooks(): array {
return array(
'wp_create_nav_menu' => array(
'args' => array( 'term_id', 'menu_data' ),
'callback' => array( self::class, 'menu_created_callback' ),
'event_type' => Audit_Log::EVENT_TYPE_MENU,
'action_type' => self::ACTION_CREATED,
),
'wp_update_nav_menu' => array(
'args' => array( 'menu_id', 'menu_data' ),
'callback' => array( self::class, 'menu_updated_callback' ),
'event_type' => Audit_Log::EVENT_TYPE_MENU,
'action_type' => self::ACTION_UPDATED,
),
'wp_delete_nav_menu' => array(
'args' => array( 'term_id' ),
'callback' => array( self::class, 'menu_deleted_callback' ),
'event_type' => Audit_Log::EVENT_TYPE_MENU,
'action_type' => self::ACTION_DELETED,
),
'wp_update_nav_menu_item' => array(
'args' => array( 'menu_id', 'menu_item_db_id', 'args' ),
'callback' => array( self::class, 'menu_item_updated_callback' ),
'event_type' => Audit_Log::EVENT_TYPE_MENU,
'action_type' => self::ACTION_UPDATED,
),
'delete_post' => array(
'args' => array( 'post_ID' ),
'callback' => array( self::class, 'menu_item_deleted_callback' ),
'event_type' => Audit_Log::EVENT_TYPE_MENU,
'action_type' => self::ACTION_DELETED,
),
);
}
/**
* Callback for when a menu item is updated. It checks if the update is significant for logging.
*
* @return array|bool
*/
public function menu_item_updated_callback() {
if ( $this->doing_autosave() ) {
return false;
}
// Filter $_POST.
$post_array = filter_input_array( INPUT_POST );
if ( ! isset( $post_array['menu-name'] ) ) {
return false;
}
$func_args = func_get_args();
$menu_id = $func_args[1]['menu_id'];
$menu_item_db_id = $func_args[1]['menu_item_db_id'];
$args = $func_args[1]['args'];
$blog_name = is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : '';
$old_menu = $this->get_cached_menu( $menu_id );
if ( ! empty( $old_menu ) ) {
$old_items = array_column( $old_menu['items'], 'item_id' );
$new_items = array_keys( $post_array['menu-item-title'] );
if ( ! empty( $old_menu['items'] ) ) {
foreach ( $old_menu['items'] as $old_item ) {
if ( $menu_item_db_id === $old_item['item_id'] ) {
if (
$old_item['menu_order'] !== $args['menu-item-position']
|| $old_item['menu_item_parent'] !== $args['menu-item-parent-id']
|| ( ! empty( $args['menu-item-title'] ) && $old_item['title'] !== $args['menu-item-title'] )
) {
return array(
sprintf(
/* translators: 1: Blog name, 2: User's display name, 3: Menu item, 4: Menu name */
esc_html__( '%1$s %2$s updated item "%3$s" from menu "%4$s"', 'defender-security' ),
$blog_name,
$this->get_user_display( get_current_user_id() ),
$post_array['menu-item-title'][ $menu_item_db_id ],
$post_array['menu-name']
),
$this->type,
);
}
break;
}
}
}
$added_items = array_diff( $new_items, $old_items );
if ( in_array( $menu_item_db_id, $added_items, true ) ) {
return array(
sprintf(
/* translators: 1: Blog name, 2: User's display name, 3: Menu item, 4: Menu name */
esc_html__( '%1$s %2$s added item "%3$s" to menu "%4$s"', 'defender-security' ),
$blog_name,
$this->get_user_display( get_current_user_id() ),
$post_array['menu-item-title'][ $menu_item_db_id ],
$post_array['menu-name']
),
$this->type,
);
}
}
return false;
}
/**
* Callback for when a menu item is deleted. It logs the deletion if the item existed in the cached menu.
*
* @return array|bool
*/
public function menu_item_deleted_callback() {
$post_array = filter_input_array( INPUT_POST );
if ( ! isset( $post_array['menu-name'] ) ) {
return false;
}
$func_args = func_get_args();
$menu_item_db_id = $func_args[1]['post_ID'];
$menu_id = (int) $post_array['menu'];
$blog_name = is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : '';
$old_menu = $this->get_cached_menu( $menu_id );
if ( ! empty( $old_menu ) && is_array( $old_menu ) ) {
$old_items = array_column( $old_menu['items'], 'item_id' );
if ( in_array( $menu_item_db_id, $old_items, true ) ) {
$key = array_search( $menu_item_db_id, $old_items, true );
return array(
sprintf(
/* translators: 1: Blog name, 2: User's display name, 3: Menu item, 4: Menu name */
esc_html__( '%1$s %2$s removed item "%3$s" from menu "%4$s"', 'defender-security' ),
$blog_name,
$this->get_user_display( get_current_user_id() ),
$old_menu['items'][ $key ]['title'],
$post_array['menu-name']
),
$this->type,
);
}
}
return false;
}
/**
* Callback for when a new menu is created. It logs the creation event.
*
* @return array
*/
public function menu_created_callback(): array {
$func_args = func_get_args();
$menu_data = $func_args[1]['menu_data'];
$blog_name = is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : '';
return array(
sprintf(
/* translators: 1: Blog name, 2: User's display name, 3: Menu name */
esc_html__( '%1$s %2$s created a new menu "%3$s"', 'defender-security' ),
$blog_name,
$this->get_user_display( get_current_user_id() ),
$menu_data['menu-name']
),
$this->type,
);
}
/**
* Callback for when a menu is updated. It logs the update if there are changes to the menu structure.
*
* @return array|bool
*/
public function menu_updated_callback() {
$func_args = func_get_args();
$menu_id = $func_args[1]['menu_id'];
$menu_data = $func_args[1]['menu_data'];
$post_array = filter_input_array( INPUT_POST );
$new_items = isset( $post_array['menu-item-title'] ) ? array_keys( $post_array['menu-item-title'] ) : array();
$old_menu = $this->get_cached_menu( $menu_id );
$old_items = array_column( $old_menu['items'], 'item_id' );
sort( $new_items );
sort( $old_items );
if ( $new_items === $old_items ) {
// Check if item title, position or parent is changed.
$is_any_item_changed = false;
foreach ( $old_menu['items'] as $old_items ) {
if (
$old_items['title'] !== $post_array['menu-item-title'][ $old_items['item_id'] ]
|| $old_items['menu_order'] !== (int) $post_array['menu-item-position'][ $old_items['item_id'] ]
|| $old_items['menu_item_parent'] !== $post_array['menu-item-parent-id'][ $old_items['item_id'] ]
) {
$is_any_item_changed = true;
break;
}
}
if ( false === $is_any_item_changed ) {
$blog_name = is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : '';
return array(
sprintf(
/* translators: 1: Blog name, 2: User's display name, 3: Menu name */
esc_html__( '%1$s %2$s updated menu "%3$s"', 'defender-security' ),
$blog_name,
$this->get_user_display( get_current_user_id() ),
$menu_data['menu-name']
),
$this->type,
);
}
}
return false;
}
/**
* Callback for when a menu is deleted. It logs the deletion event.
*
* @return array
*/
public function menu_deleted_callback(): array {
$func_args = func_get_args();
$menu_id = $func_args[1]['term_id'];
$blog_name = is_multisite() ? '[' . get_bloginfo( 'name' ) . ']' : '';
$old_menu = $this->get_cached_menu( $menu_id );
return array(
sprintf(
/* translators: 1: Blog name, 2: User's display name, 3: Menu name */
esc_html__( '%1$s %2$s deleted menu "%3$s"', 'defender-security' ),
$blog_name,
$this->get_user_display( get_current_user_id() ),
$old_menu['name']
),
$this->type,
);
}
/**
* Retrieves a cached menu from the runtime cache based on the term ID.
*
* @param int $term_id Menu term ID.
*
* @return bool|array
*/
public function get_cached_menu( $term_id ) {
$cached = Array_Cache::get( 'menu_updated', 'audit', array() );
foreach ( $cached as $menu_arr ) {
if ( $term_id === $menu_arr['term_id'] ) {
return $menu_arr;
}
}
return false;
}
/**
* Checks if the current request is doing an auto save.
*
* @return bool
*/
public function doing_autosave(): bool {
return defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE;
}
}