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 ( $menu_obj instanceof \WP_Term ) { $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 ( is_array( $items ) && array() !== $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 ( isset( $old_menu['items'] ) && is_array( $old_menu['items'] ) ) { $old_items = array_column( $old_menu['items'], 'item_id' ); $new_items = array_keys( $post_array['menu-item-title'] ); if ( array() !== $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'] || ( isset( $args['menu-item-title'] ) && '' !== $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 ( isset( $old_menu['items'] ) && is_array( $old_menu['items'] ) && array() !== $old_menu['items'] ) { $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 $items ) { if ( $items['title'] !== $post_array['menu-item-title'][ $items['item_id'] ] || $items['menu_order'] !== (int) $post_array['menu-item-position'][ $items['item_id'] ] || $items['menu_item_parent'] !== $post_array['menu-item-parent-id'][ $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; } }