Linux webm002.cluster126.gra.hosting.ovh.net 5.15.206-ovh-vps-grsec-zfs-classid #1 SMP Fri May 15 02:41:25 UTC 2026 x86_64
/
home
/
a
/
r
/
i
/
ariannadhf
/
www
/
wp-content
/
plugins
/
simple-history
/
loggers
/
/home/a/r/i/ariannadhf/www/wp-content/plugins/simple-history/loggers/class-menu-logger.php
<?php namespace Simple_History\Loggers; use Simple_History\Event_Details\Event_Details_Group; use Simple_History\Event_Details\Event_Details_Item; use Simple_History\Helpers; /** * Logs WordPress menu edits */ class Menu_Logger extends Logger { /** @var string Logger slug */ public $slug = 'SimpleMenuLogger'; /** @var array<int,string> Menu names captured before deletion, keyed by term ID. */ private $pre_delete_menu_names = []; /** * Get array with information about this logger * * @return array */ public function get_info() { return array( 'name' => __( 'Menu Logger', 'simple-history' ), 'description' => __( 'Logs menu edits', 'simple-history' ), 'capability' => 'edit_theme_options', 'messages' => array( 'created_menu' => __( 'Created menu "{menu_name}"', 'simple-history' ), 'edited_menu' => __( 'Edited menu "{menu_name}"', 'simple-history' ), 'deleted_menu' => __( 'Deleted menu "{menu_name}"', 'simple-history' ), 'edited_menu_item' => __( 'Edited a menu item', 'simple-history' ), 'edited_menu_locations' => __( 'Updated menu locations', 'simple-history' ), ), 'labels' => array( 'search' => array( 'label' => _x( 'Menus', 'Menu logger: search', 'simple-history' ), 'label_all' => _x( 'All menu activity', 'Menu updates logger: search', 'simple-history' ), 'options' => array( _x( 'Created menus', 'Menu updates logger: search', 'simple-history' ) => array( 'created_menu', ), _x( 'Edited menus', 'Menu updates logger: search', 'simple-history' ) => array( 'edited_menu', 'edited_menu_item', 'edited_menu_locations', ), _x( 'Deleted menus', 'Menu updates logger: search', 'simple-history' ) => array( 'deleted_menu', ), ), ), ), ); } /** * Called when logger is loaded. */ public function loaded() { /* * Fires after a navigation menu has been successfully deleted. * * @since 3.0.0 * * @param int $term_id ID of the deleted menu. */ add_action( 'load-nav-menus.php', array( $this, 'on_load_nav_menus_page_detect_delete' ) ); /* * Fires after a navigation menu is successfully created. * * @since 3.0.0 * * @param int $term_id ID of the new menu. * @param array $menu_data An array of menu data. */ add_action( 'wp_create_nav_menu', array( $this, 'on_wp_create_nav_menu' ), 10, 2 ); // This is fired when adding nav items in the editor, not at save, so not // good to log because user might not end up saving the changes // add_action("wp_update_nav_menu_item", array($this, "on_wp_update_nav_menu_item"), 10, 3 ); // Fired before "wp_update_nav_menu" below, to remember menu layout before it's updated // so we can't detect changes. add_action( 'load-nav-menus.php', array( $this, 'on_load_nav_menus_page_detect_update' ) ); // Detect menu location change in "manage locations". add_action( 'load-nav-menus.php', array( $this, 'on_load_nav_menus_page_detect_locations_update' ) ); add_filter( 'simple_history/categories_logger/skip_taxonomies', array( $this, 'on_categories_logger_skip_taxonomy' ) ); // WP-CLI / non-admin support. // Capture menu name before term deletion (wp_delete_nav_menu fires after, when name is gone). add_action( 'pre_delete_term', array( $this, 'on_pre_delete_term_capture_menu_name' ), 10, 2 ); add_action( 'wp_delete_nav_menu', array( $this, 'on_wp_delete_nav_menu' ), 10, 1 ); // Log menu item updates outside admin (WP-CLI, REST API). add_action( 'wp_update_nav_menu_item', array( $this, 'on_wp_update_nav_menu_item_non_admin' ), 10, 3 ); // Detect nav_menu_locations changes via theme_mods (set_theme_mod path). add_action( 'updated_option', array( $this, 'on_updated_option_detect_nav_menu_locations' ), 10, 3 ); } /** * Add taxonomy "nav_menu" to list of categories to not log changes to, * because menus are stored in this taxonomy, * and the menu logger will log menu changes, * so don't let categories logger log this * or there will be duplicates. * * @param mixed $taxonomies_to_skip Array with taxonomies to skip. * @return array */ public function on_categories_logger_skip_taxonomy( $taxonomies_to_skip ) { $taxonomies_to_skip[] = 'nav_menu'; return $taxonomies_to_skip; } /** * Capture menu name before the term is deleted (fired for ALL term deletions). * Stores the name so on_wp_delete_nav_menu() can log it after the fact. * * @param int $term Term ID. * @param string $taxonomy Taxonomy name. */ public function on_pre_delete_term_capture_menu_name( $term, $taxonomy ) { if ( $taxonomy !== 'nav_menu' ) { return; } $menu = wp_get_nav_menu_object( $term ); if ( ! $menu || is_wp_error( $menu ) ) { return; } $this->pre_delete_menu_names[ (int) $term ] = $menu->name; } /** * Log a menu deletion from a non-admin context (WP-CLI). * Fires from wp_delete_nav_menu() after the menu is gone; * the name was captured by on_pre_delete_term_capture_menu_name(). * * @param int $term_id ID of the deleted menu term. */ public function on_wp_delete_nav_menu( $term_id ) { $term_id = (int) $term_id; // Admin path handled by on_load_nav_menus_page_detect_delete(). if ( is_admin() ) { unset( $this->pre_delete_menu_names[ $term_id ] ); return; } $menu_name = $this->pre_delete_menu_names[ $term_id ] ?? ''; unset( $this->pre_delete_menu_names[ $term_id ] ); $this->info_message( 'deleted_menu', array( 'menu_term_id' => $term_id, 'menu_name' => $menu_name, ) ); } /** * Log menu item additions/updates outside wp-admin (WP-CLI, REST API). * The admin form disables this hook because changes in the editor are not * committed until "Save menu" is clicked. In WP-CLI/REST, they commit immediately. * * @param int $menu_id ID of the nav menu. * @param int $menu_item_db_id ID of the menu item. * @param array $args Menu item arguments. */ public function on_wp_update_nav_menu_item_non_admin( $menu_id, $menu_item_db_id, $args ) { if ( is_admin() ) { return; } if ( ! Helpers::is_rest_request() && ! Helpers::is_wp_cli() ) { return; } $menu = wp_get_nav_menu_object( $menu_id ); $this->info_message( 'edited_menu_item', array( 'menu_id' => $menu_id, 'menu_name' => $menu ? $menu->name : '', 'menu_item_db_id' => $menu_item_db_id, ) ); } /** * Detect nav_menu_locations changes via theme_mods option (set_theme_mod path). * Used by WP-CLI (`wp menu location assign`) which calls set_theme_mod() directly. * Admin form path is handled by on_load_nav_menus_page_detect_locations_update(). * * @param string $option Option name. * @param mixed $old_value Previous option value. * @param mixed $new_value New option value. */ public function on_updated_option_detect_nav_menu_locations( $option, $old_value, $new_value ) { if ( strpos( $option, 'theme_mods_' ) !== 0 ) { return; } if ( is_admin() ) { return; } $old_locations = isset( $old_value['nav_menu_locations'] ) ? (array) $old_value['nav_menu_locations'] : array(); $new_locations = isset( $new_value['nav_menu_locations'] ) ? (array) $new_value['nav_menu_locations'] : array(); if ( $old_locations === $new_locations ) { return; } $registered_menus = get_registered_nav_menus(); $changes = array(); foreach ( $new_locations as $location_slug => $new_menu_id ) { $old_menu_id = isset( $old_locations[ $location_slug ] ) ? (int) $old_locations[ $location_slug ] : 0; $new_menu_id = (int) $new_menu_id; if ( $old_menu_id === $new_menu_id ) { continue; } $changes[] = array( 'location' => $registered_menus[ $location_slug ] ?? $location_slug, 'from' => $this->get_menu_name_by_id( $old_menu_id ), 'to' => $this->get_menu_name_by_id( $new_menu_id ), ); } if ( empty( $changes ) ) { return; } $this->info_message( 'edited_menu_locations', array( 'menu_locations' => Helpers::json_encode( $new_locations ), 'locations_changed' => Helpers::json_encode( $changes ), ) ); } /** * Can't use action "wp_delete_nav_menu" because * it's fired after menu is deleted, so we don't have the name in this action */ public function on_load_nav_menus_page_detect_delete() { // Check that needed vars are set. // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! isset( $_REQUEST['menu'], $_REQUEST['action'] ) ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( $_REQUEST['action'] !== 'delete' ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended $menu_id = sanitize_text_field( wp_unslash( $_REQUEST['menu'] ) ); if ( ! is_nav_menu( $menu_id ) ) { return; } $menu = wp_get_nav_menu_object( $menu_id ); $this->info_message( 'deleted_menu', array( 'menu_term_id' => $menu_id, 'menu_name' => $menu->name, ) ); } /** * Detect menu being created * * @param int $term_id ID of the new menu. * @param array $menu_data An array of menu data. */ public function on_wp_create_nav_menu( $term_id, $menu_data ) { $menu = wp_get_nav_menu_object( $term_id ); if ( ! $menu ) { return; } $this->info_message( 'created_menu', array( 'term_id' => $term_id, 'menu_name' => $menu->name, ) ); } /** * Detect menu being saved */ public function on_load_nav_menus_page_detect_update() { /* This is the data to be saved $_REQUEST: Array ( [action] => update [menu] => 25 [menu-name] => Main menu edit [menu-item-title] => Array ( [25243] => My new page edited [25244] => My new page [25245] => This is my new page. How does it look in the logs? <h1>Hej!</h1> [25264] => This page have revisions [25265] => Lorem ipsum dolor sit amet ) [menu-locations] => Array ( [primary] => 25 ) ) */ // Check that needed vars are set. // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! isset( $_REQUEST['menu'], $_REQUEST['action'], $_REQUEST['menu-name'] ) ) { return; } // Only go on for update action. // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( $_REQUEST['action'] !== 'update' ) { return; } // Make sure we got the id of a menu. // phpcs:ignore WordPress.Security.NonceVerification.Recommended $menu_id = sanitize_text_field( wp_unslash( $_REQUEST['menu'] ) ); if ( ! is_nav_menu( $menu_id ) ) { return; } // Snapshot the menu's current name before update so we can detect renames. $old_menu = wp_get_nav_menu_object( $menu_id ); $old_menu_name_raw = $old_menu instanceof \WP_Term ? (string) $old_menu->name : ''; // Get saved menu. May be empty if this is the first time we save the menu. $arr_prev_menu_items = wp_get_nav_menu_items( $menu_id ); // Build map of old items keyed by db_id. $old_items_map = array(); if ( is_array( $arr_prev_menu_items ) ) { foreach ( $arr_prev_menu_items as $item ) { $old_items_map[ $item->db_id ] = $item; } } // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $new_ids = array_values( isset( $_POST['menu-item-db-id'] ) ? (array) wp_unslash( $_POST['menu-item-db-id'] ) : array() ); $old_ids = array_keys( $old_items_map ); // Detect added, removed, renamed, moved items. $arr_removed = array_diff( $old_ids, $new_ids ); $arr_added = array_diff( $new_ids, $old_ids ); $menu_changes = array( 'added' => array(), 'removed' => array(), 'renamed' => array(), 'moved' => array(), 'order_changed' => false, ); // phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated $post_titles = isset( $_POST['menu-item-title'] ) ? (array) wp_unslash( $_POST['menu-item-title'] ) : array(); $post_parents = isset( $_POST['menu-item-parent-id'] ) ? (array) wp_unslash( $_POST['menu-item-parent-id'] ) : array(); $post_types = isset( $_POST['menu-item-type'] ) ? (array) wp_unslash( $_POST['menu-item-type'] ) : array(); $post_type_labels = isset( $_POST['menu-item-type-label'] ) ? (array) wp_unslash( $_POST['menu-item-type-label'] ) : array(); $post_objects = isset( $_POST['menu-item-object'] ) ? (array) wp_unslash( $_POST['menu-item-object'] ) : array(); $post_urls = isset( $_POST['menu-item-url'] ) ? (array) wp_unslash( $_POST['menu-item-url'] ) : array(); // phpcs:enable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated // Added items. foreach ( $arr_added as $added_id ) { $type_label = isset( $post_type_labels[ $added_id ] ) ? sanitize_text_field( $post_type_labels[ $added_id ] ) : ''; // Type label is often empty for newly added items, resolve from type/object. if ( $type_label === '' ) { $type_label = $this->get_menu_item_type_label( $post_types[ $added_id ] ?? '', $post_objects[ $added_id ] ?? '' ); } $change = array( 'id' => (int) $added_id, 'title' => isset( $post_titles[ $added_id ] ) ? sanitize_text_field( $post_titles[ $added_id ] ) : '', 'type' => $type_label, ); if ( isset( $post_types[ $added_id ] ) && $post_types[ $added_id ] === 'custom' && ! empty( $post_urls[ $added_id ] ) ) { $change['url'] = esc_url_raw( $post_urls[ $added_id ] ); } $menu_changes['added'][] = $change; } // Removed items. foreach ( $arr_removed as $removed_id ) { if ( ! isset( $old_items_map[ $removed_id ] ) ) { continue; } $old_item = $old_items_map[ $removed_id ]; $menu_changes['removed'][] = array( 'id' => (int) $removed_id, 'title' => $old_item->title, 'type' => $old_item->type_label, ); } // Renamed and moved items (items present in both old and new). $existing_ids = array_intersect( $new_ids, $old_ids ); foreach ( $existing_ids as $item_id ) { if ( ! isset( $old_items_map[ $item_id ] ) ) { continue; } $old_item = $old_items_map[ $item_id ]; // Two raw shapes for the old label: // - post_title: the value stored in the DB (empty when the item // has no custom label and inherits the linked object's title). // - $old_item->title: the resolved label as set by // wp_setup_nav_menu_item() — falls back to the linked object's // title when post_title is empty. // The form re-submits whatever label the user sees in the input, // which matches one of these two. Treat a match against either as // "no rename" so we don't flag items with HTML in the label // (e.g. "Books <span>& authors</span>") or items inheriting their // label from a linked page on every save. $old_post_title = isset( $old_item->post_title ) ? (string) $old_item->post_title : ''; $old_display_title = isset( $old_item->title ) ? (string) $old_item->title : ''; $new_title_raw = isset( $post_titles[ $item_id ] ) ? (string) $post_titles[ $item_id ] : ''; $new_title = sanitize_text_field( $new_title_raw ); $is_rename = $new_title_raw !== '' && $new_title_raw !== $old_post_title && $new_title_raw !== $old_display_title; if ( $is_rename ) { $menu_changes['renamed'][] = array( 'id' => (int) $item_id, 'from' => $old_display_title !== '' ? $old_display_title : $old_post_title, 'to' => $new_title_raw, ); } // Detect move (parent change). $new_parent = isset( $post_parents[ $item_id ] ) ? (int) $post_parents[ $item_id ] : 0; $old_parent = (int) $old_item->menu_item_parent; if ( $new_parent === $old_parent ) { continue; } $title = $new_title !== '' ? $new_title : $old_item->title; // Resolve parent titles for readability. $from_label = $this->get_menu_item_parent_label( $old_parent, $old_items_map, $post_titles ); $to_label = $this->get_menu_item_parent_label( $new_parent, $old_items_map, $post_titles ); $menu_changes['moved'][] = array( 'id' => (int) $item_id, 'title' => $title, 'from' => $from_label, 'to' => $to_label, ); } // Detect order changes by comparing old and new item sequences // for items that existed in both (excluding added/removed). // Cast to int for consistent comparison since $_POST values are strings. $old_order = array_map( 'intval', array_values( array_intersect( $old_ids, $new_ids ) ) ); $new_order = array_map( 'intval', array_values( array_intersect( $new_ids, $old_ids ) ) ); if ( $old_order !== $new_order ) { $menu_changes['order_changed'] = true; } // Detect location changes from "Display location" checkboxes. // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.InputNotValidated $post_locations = isset( $_POST['menu-locations'] ) ? (array) wp_unslash( $_POST['menu-locations'] ) : array(); $locations_changed = $this->detect_location_changes( $post_locations, (int) $menu_id ); // Detect "auto add pages" setting change. // phpcs:ignore WordPress.Security.NonceVerification.Missing $new_auto_add = ! empty( $_POST['auto-add-pages'] ); $auto_add_options = get_option( 'nav_menu_options', array() ); $old_auto_add = isset( $auto_add_options['auto_add'] ) && is_array( $auto_add_options['auto_add'] ) && in_array( (int) $menu_id, $auto_add_options['auto_add'], true ); // Remove empty change types. $menu_changes = array_filter( $menu_changes ); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotValidated $new_menu_name = sanitize_text_field( wp_unslash( $_POST['menu-name'] ) ); $context = array( 'menu_id' => $menu_id, 'menu_name' => $new_menu_name, 'menu_items_added' => count( $arr_added ), 'menu_items_removed' => count( $arr_removed ), 'menu_changes' => Helpers::json_encode( $menu_changes ), ); // Detect rename of the menu itself (the "Menu Name" field). if ( $old_menu_name_raw !== '' && $new_menu_name !== '' && $new_menu_name !== $old_menu_name_raw ) { $context['menu_name_prev'] = $old_menu_name_raw; } if ( ! empty( $locations_changed ) ) { $context['locations_changed'] = Helpers::json_encode( $locations_changed ); } if ( $new_auto_add !== $old_auto_add ) { $context['auto_add_pages_new'] = $new_auto_add ? '1' : '0'; $context['auto_add_pages_prev'] = $old_auto_add ? '1' : '0'; } $this->info_message( 'edited_menu', $context ); } /** * Action links for menu events — jump to the menu editor. * * @param object $row Log row. * @return array */ public function get_action_links( $row ) { if ( ! current_user_can( 'edit_theme_options' ) ) { return array(); } $context = $row->context; $message_key = $context['_message_key'] ?? ''; // "Manage locations" link for location-only events. if ( $message_key === 'edited_menu_locations' ) { return array( array( 'url' => add_query_arg( array( 'action' => 'locations' ), admin_url( 'nav-menus.php' ) ), 'label' => __( 'Manage menu locations', 'simple-history' ), 'action' => 'edit', ), ); } // "Edit menu" link for events that target a specific menu. if ( ! in_array( $message_key, array( 'edited_menu', 'created_menu' ), true ) ) { return array(); } // Resolve menu term id from context. edited_menu uses "menu_id"; // created_menu uses "term_id". $menu_id = 0; if ( ! empty( $context['menu_id'] ) ) { $menu_id = (int) $context['menu_id']; } elseif ( ! empty( $context['term_id'] ) ) { $menu_id = (int) $context['term_id']; } if ( $menu_id <= 0 || ! is_nav_menu( $menu_id ) ) { return array(); } return array( array( 'url' => add_query_arg( array( 'action' => 'edit', 'menu' => $menu_id, ), admin_url( 'nav-menus.php' ) ), 'label' => __( 'Edit menu', 'simple-history' ), 'action' => 'edit', ), ); } /** * Get detailed output. * * @param object $row Log row. * @return Event_Details_Group|string */ public function get_log_row_details_output( $row ) { $context = $row->context; $message_key = $context['_message_key']; if ( $message_key === 'edited_menu' ) { return $this->get_edited_menu_details( $context ); } if ( $message_key === 'edited_menu_locations' ) { return $this->get_edited_locations_details( $context ); } return ''; } /** * Get details output for edited_menu events. * * @param array $context Log row context. * @return Event_Details_Group|string */ private function get_edited_menu_details( $context ) { // New format with detailed changes. if ( ! empty( $context['menu_changes'] ) ) { $changes = json_decode( $context['menu_changes'], true ); if ( ! is_array( $changes ) ) { return ''; } $group = new Event_Details_Group(); // Menu rename (the menu itself, not its items). menu_name_prev is // only set in context when the name actually changed. if ( ! empty( $context['menu_name_prev'] ) ) { $group->add_item( new Event_Details_Item( array( 'menu_name', 'menu_name_prev' ), __( 'Menu name', 'simple-history' ) ) ); } if ( ! empty( $changes['added'] ) ) { $descriptions = array(); foreach ( $changes['added'] as $item ) { $descriptions[] = $this->format_menu_item_description( $item ); } $group->add_item( ( new Event_Details_Item( null, __( 'Added', 'simple-history' ) ) ) ->set_new_value( implode( _x( ', ', 'Menu item list separator', 'simple-history' ), $descriptions ) ) ); } if ( ! empty( $changes['removed'] ) ) { $descriptions = array(); foreach ( $changes['removed'] as $item ) { $descriptions[] = $this->format_menu_item_description( $item ); } $group->add_item( ( new Event_Details_Item( null, __( 'Removed', 'simple-history' ) ) ) ->set_new_value( implode( _x( ', ', 'Menu item list separator', 'simple-history' ), $descriptions ) ) ); } if ( ! empty( $changes['renamed'] ) ) { foreach ( $changes['renamed'] as $item ) { $group->add_item( ( new Event_Details_Item( null, __( 'Renamed', 'simple-history' ) ) ) ->set_new_value( $item['to'] ) ->set_prev_value( $item['from'] ) ); } } if ( ! empty( $changes['moved'] ) ) { foreach ( $changes['moved'] as $item ) { $group->add_item( ( new Event_Details_Item( null, __( 'Moved', 'simple-history' ) ) ) ->set_new_value( sprintf( // translators: 1: menu item title, 2: parent location. __( '%1$s: %2$s', 'simple-history' ), $item['title'], $item['to'] ) ) ->set_prev_value( sprintf( // translators: 1: menu item title, 2: parent location. __( '%1$s: %2$s', 'simple-history' ), $item['title'], $item['from'] ) ) ); } } if ( ! empty( $changes['order_changed'] ) ) { $group->add_item( ( new Event_Details_Item( null, __( 'Order', 'simple-history' ) ) ) ->set_new_value( __( 'Changed menu item order', 'simple-history' ) ) ); } // Location changes from "Display location" checkboxes. $this->add_location_items_to_group( $group, $context ); // Auto add pages setting change. if ( isset( $context['auto_add_pages_new'] ) ) { $enabled_label = __( 'Enabled', 'simple-history' ); $disabled_label = __( 'Disabled', 'simple-history' ); $group->add_item( ( new Event_Details_Item( null, __( 'Auto add pages', 'simple-history' ) ) ) ->set_new_value( $context['auto_add_pages_new'] === '1' ? $enabled_label : $disabled_label ) ->set_prev_value( $context['auto_add_pages_prev'] === '1' ? $enabled_label : $disabled_label ) ); } if ( empty( $group->items ) ) { return ''; } return $group; } // Legacy format: count-based display for old log entries. if ( ! empty( $context['menu_items_added'] ) || ! empty( $context['menu_items_removed'] ) ) { $output = '<p>'; $output .= '<span class="SimpleHistoryLogitem__inlineDivided">'; $output .= sprintf( // translators: Number of menu items added. _nx( '%1$s menu item added', '%1$s menu items added', $context['menu_items_added'], 'menu logger', 'simple-history' ), esc_attr( $context['menu_items_added'] ) ); $output .= '</span> '; $output .= '<span class="SimpleHistoryLogitem__inlineDivided">'; $output .= sprintf( // translators: Number of menu items removed. _nx( '%1$s menu item removed', '%1$s menu items removed', $context['menu_items_removed'], 'menu logger', 'simple-history' ), esc_attr( $context['menu_items_removed'] ) ); $output .= '</span> '; $output .= '</p>'; return $output; } return ''; } /** * Get details output for edited_menu_locations events. * * @param array $context Log row context. * @return Event_Details_Group|string */ private function get_edited_locations_details( $context ) { $group = new Event_Details_Group(); $this->add_location_items_to_group( $group, $context ); if ( empty( $group->items ) ) { return ''; } return $group; } /** * Add location change items to an Event_Details_Group. * * @param Event_Details_Group $group Group to add items to. * @param array $context Log row context. */ private function add_location_items_to_group( $group, $context ) { if ( empty( $context['locations_changed'] ) ) { return; } $changes = json_decode( $context['locations_changed'], true ); if ( ! is_array( $changes ) || empty( $changes ) ) { return; } foreach ( $changes as $change ) { $group->add_item( ( new Event_Details_Item( null, $change['location'] ) ) ->set_new_value( $change['to'] ) ->set_prev_value( $change['from'] ) ); } } /** * Log updates to theme menu locations */ public function on_load_nav_menus_page_detect_locations_update() { // Check that needed vars are set. // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! isset( $_REQUEST['menu'], $_REQUEST['action'] ) ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( $_REQUEST['action'] !== 'locations' ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotValidated, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $new_locations = (array) wp_unslash( $_POST['menu-locations'] ); $locations_changed = $this->detect_location_changes( $new_locations ); // Only log if something actually changed. if ( empty( $locations_changed ) ) { return; } $this->info_message( 'edited_menu_locations', array( 'menu_locations' => Helpers::json_encode( $new_locations ), 'locations_changed' => Helpers::json_encode( $locations_changed ), ) ); } /** * Get human-readable menu name by ID, or "None" if empty/invalid. * * @param int $menu_id Menu term ID. * @return string Menu name or "None". */ private function get_menu_name_by_id( $menu_id ) { if ( empty( $menu_id ) ) { return __( 'None', 'simple-history' ); } $menu = wp_get_nav_menu_object( $menu_id ); if ( $menu && ! is_wp_error( $menu ) ) { return $menu->name; } return __( 'None', 'simple-history' ); } /** * Compare new menu locations with current ones and return changes. * * @param array $new_locations New menu locations from POST data. * @param int $menu_id Optional menu ID being edited, to detect removed locations. * @return array Array of location changes with location/from/to keys. */ private function detect_location_changes( $new_locations, $menu_id = 0 ) { $old_locations = get_nav_menu_locations(); $registered_menus = get_registered_nav_menus(); $changes = array(); // Detect added or changed locations. foreach ( $new_locations as $location_slug => $new_menu_id ) { $old_menu_id = isset( $old_locations[ $location_slug ] ) ? (int) $old_locations[ $location_slug ] : 0; $new_menu_id = (int) $new_menu_id; if ( $old_menu_id === $new_menu_id ) { continue; } $changes[] = array( 'location' => $registered_menus[ $location_slug ] ?? $location_slug, 'from' => $this->get_menu_name_by_id( $old_menu_id ), 'to' => $this->get_menu_name_by_id( $new_menu_id ), ); } // Detect removed locations: old locations assigned to this menu // that are no longer in the POST data (checkbox was unchecked). if ( $menu_id > 0 ) { foreach ( $old_locations as $location_slug => $old_menu_id ) { if ( (int) $old_menu_id !== $menu_id ) { continue; } if ( isset( $new_locations[ $location_slug ] ) ) { continue; } $changes[] = array( 'location' => $registered_menus[ $location_slug ] ?? $location_slug, 'from' => $this->get_menu_name_by_id( $old_menu_id ), 'to' => $this->get_menu_name_by_id( 0 ), ); } } return $changes; } /** * Get label for a menu item's parent, resolving ID to title. * * @param int $parent_id Parent menu item db_id (0 = top level). * @param array $old_items_map Map of old menu items keyed by db_id. * @param array $post_titles Array of new titles from $_POST keyed by db_id. * @return string Human-readable parent label. */ private function get_menu_item_parent_label( $parent_id, $old_items_map, $post_titles ) { if ( empty( $parent_id ) ) { return __( 'top level', 'simple-history' ); } $parent_title = ''; // Try new title from POST first (may have been renamed). if ( isset( $post_titles[ $parent_id ] ) && $post_titles[ $parent_id ] !== '' ) { $parent_title = sanitize_text_field( $post_titles[ $parent_id ] ); } elseif ( isset( $old_items_map[ $parent_id ] ) ) { $parent_title = $old_items_map[ $parent_id ]->title; } if ( $parent_title === '' ) { return __( 'top level', 'simple-history' ); } return sprintf( // translators: %s is the parent menu item title. __( 'under "%s"', 'simple-history' ), $parent_title ); } /** * Get a human-readable type label for a menu item from its type and object slug. * * @param string $type Menu item type (post_type, taxonomy, custom). * @param string $object_slug Menu item object slug (page, post, category, etc.). * @return string Human-readable label. */ private function get_menu_item_type_label( $type, $object_slug ) { if ( $type === 'post_type' && $object_slug !== '' ) { $post_type_obj = get_post_type_object( $object_slug ); if ( $post_type_obj ) { return $post_type_obj->labels->singular_name; } } if ( $type === 'taxonomy' && $object_slug !== '' ) { $taxonomy_obj = get_taxonomy( $object_slug ); if ( $taxonomy_obj ) { return $taxonomy_obj->labels->singular_name; } } if ( $type === 'custom' ) { return __( 'Custom Link', 'simple-history' ); } return ''; } /** * Format a menu item for display in added/removed lists. * * @param array $item Item data with title, type, and optionally url. * @return string Formatted description. */ private function format_menu_item_description( $item ) { if ( ! empty( $item['type'] ) && ! empty( $item['url'] ) ) { return sprintf( // translators: 1: menu item title, 2: item type, 3: URL. __( '"%1$s" (%2$s: %3$s)', 'simple-history' ), $item['title'], $item['type'], $item['url'] ); } if ( ! empty( $item['type'] ) ) { return sprintf( // translators: 1: menu item title, 2: item type (e.g. Page, Post, Category). __( '"%1$s" (%2$s)', 'simple-history' ), $item['title'], $item['type'] ); } return sprintf( // translators: %s: menu item title. __( '"%s"', 'simple-history' ), $item['title'] ); } }