HEX
Server: Apache
System: Linux pdx1-shared-a1-38 6.6.104-grsec-jammy+ #3 SMP Tue Sep 16 00:28:11 UTC 2025 x86_64
User: mmickelson (3396398)
PHP: 8.1.31
Disabled: NONE
Upload Files
File: //usr/local/wp/vendor/wp-cli/entity-command/src/Menu_Item_Command.php
<?php

use WP_CLI\Formatter;
use WP_CLI\Utils;

/**
 * List, add, and delete items associated with a menu.
 *
 * ## EXAMPLES
 *
 *     # Add an existing post to an existing menu
 *     $ wp menu item add-post sidebar-menu 33 --title="Custom Test Post"
 *     Success: Menu item added.
 *
 *     # Create a new menu link item
 *     $ wp menu item add-custom sidebar-menu Apple http://apple.com
 *     Success: Menu item added.
 *
 *     # Delete menu item
 *     $ wp menu item delete 45
 *     Success: Deleted 1 of 1 menu items.
 */
class Menu_Item_Command extends WP_CLI_Command {

	protected $obj_fields = [
		'db_id',
		'type',
		'title',
		'link',
		'position',
	];

	/**
	 * Gets a list of items associated with a menu.
	 *
	 * ## OPTIONS
	 *
	 * <menu>
	 * : The name, slug, or term ID for the menu.
	 *
	 * [--fields=<fields>]
	 * : Limit the output to specific object fields.
	 *
	 * [--format=<format>]
	 * : Render output in a particular format.
	 * ---
	 * default: table
	 * options:
	 *   - table
	 *   - csv
	 *   - json
	 *   - count
	 *   - ids
	 *   - yaml
	 * ---
	 *
	 * ## AVAILABLE FIELDS
	 *
	 * These fields will be displayed by default for each menu item:
	 *
	 * * db_id
	 * * type
	 * * title
	 * * link
	 * * position
	 *
	 * These fields are optionally available:
	 *
	 * * menu_item_parent
	 * * object_id
	 * * object
	 * * type
	 * * type_label
	 * * target
	 * * attr_title
	 * * description
	 * * classes
	 * * xfn
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp menu item list main-menu
	 *     +-------+-----------+-------------+---------------------------------+----------+
	 *     | db_id | type      | title       | link                            | position |
	 *     +-------+-----------+-------------+---------------------------------+----------+
	 *     | 5     | custom    | Home        | http://example.com              | 1        |
	 *     | 6     | post_type | Sample Page | http://example.com/sample-page/ | 2        |
	 *     +-------+-----------+-------------+---------------------------------+----------+
	 *
	 * @subcommand list
	 */
	public function list_( $args, $assoc_args ) {

		$items = wp_get_nav_menu_items( $args[0] );
		if ( false === $items || is_wp_error( $items ) ) {
			WP_CLI::error( 'Invalid menu.' );
		}

		// Correct position inconsistency and
		// protected `url` param in WP-CLI
		$items = array_map(
			function ( $item ) {
					$item->position = $item->menu_order;
					$item->link     = $item->url;
					return $item;
			},
			$items
		);

		if ( ! empty( $assoc_args['format'] ) && 'ids' === $assoc_args['format'] ) {
			$items = array_map(
				function ( $item ) {
						return $item->db_id;
				},
				$items
			);
		}

		$formatter = $this->get_formatter( $assoc_args );
		$formatter->display_items( $items );
	}

	/**
	 * Adds a post as a menu item.
	 *
	 * ## OPTIONS
	 *
	 * <menu>
	 * : The name, slug, or term ID for the menu.
	 *
	 * <post-id>
	 * : Post ID to add to the menu.
	 *
	 * [--title=<title>]
	 * : Set a custom title for the menu item.
	 *
	 * [--link=<link>]
	 * : Set a custom url for the menu item.
	 *
	 * [--description=<description>]
	 * : Set a custom description for the menu item.
	 *
	 * [--attr-title=<attr-title>]
	 * : Set a custom title attribute for the menu item.
	 *
	 * [--target=<target>]
	 * : Set a custom link target for the menu item.
	 *
	 * [--classes=<classes>]
	 * : Set a custom link classes for the menu item.
	 *
	 * [--position=<position>]
	 * : Specify the position of this menu item.
	 *
	 * [--parent-id=<parent-id>]
	 * : Make this menu item a child of another menu item.
	 *
	 * [--porcelain]
	 * : Output just the new menu item id.
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp menu item add-post sidebar-menu 33 --title="Custom Test Post"
	 *     Success: Menu item added.
	 *
	 * @subcommand add-post
	 */
	public function add_post( $args, $assoc_args ) {

		$assoc_args['object-id'] = $args[1];
		unset( $args[1] );
		$post = get_post( $assoc_args['object-id'] );
		if ( ! $post ) {
			WP_CLI::error( 'Invalid post.' );
		}
		$assoc_args['object'] = $post->post_type;

		$this->add_or_update_item( 'add', 'post_type', $args, $assoc_args );
	}

	/**
	 * Adds a taxonomy term as a menu item.
	 *
	 * ## OPTIONS
	 *
	 * <menu>
	 * : The name, slug, or term ID for the menu.
	 *
	 * <taxonomy>
	 * : Taxonomy of the term to be added.
	 *
	 * <term-id>
	 * : Term ID of the term to be added.
	 *
	 * [--title=<title>]
	 * : Set a custom title for the menu item.
	 *
	 * [--link=<link>]
	 * : Set a custom url for the menu item.
	 *
	 * [--description=<description>]
	 * : Set a custom description for the menu item.
	 *
	 * [--attr-title=<attr-title>]
	 * : Set a custom title attribute for the menu item.
	 *
	 * [--target=<target>]
	 * : Set a custom link target for the menu item.
	 *
	 * [--classes=<classes>]
	 * : Set a custom link classes for the menu item.
	 *
	 * [--position=<position>]
	 * : Specify the position of this menu item.
	 *
	 * [--parent-id=<parent-id>]
	 * : Make this menu item a child of another menu item.
	 *
	 * [--porcelain]
	 * : Output just the new menu item id.
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp menu item add-term sidebar-menu post_tag 24
	 *     Success: Menu item added.
	 *
	 * @subcommand add-term
	 */
	public function add_term( $args, $assoc_args ) {

		$assoc_args['object'] = $args[1];
		unset( $args[1] );
		$assoc_args['object-id'] = $args[2];
		unset( $args[2] );

		if ( ! get_term_by( 'id', $assoc_args['object-id'], $assoc_args['object'] ) ) {
			WP_CLI::error( 'Invalid term.' );
		}

		$this->add_or_update_item( 'add', 'taxonomy', $args, $assoc_args );
	}

	/**
	 * Adds a custom menu item.
	 *
	 * ## OPTIONS
	 *
	 * <menu>
	 * : The name, slug, or term ID for the menu.
	 *
	 * <title>
	 * : Title for the link.
	 *
	 * <link>
	 * : Target URL for the link.
	 *
	 * [--description=<description>]
	 * : Set a custom description for the menu item.
	 *
	 * [--attr-title=<attr-title>]
	 * : Set a custom title attribute for the menu item.
	 *
	 * [--target=<target>]
	 * : Set a custom link target for the menu item.
	 *
	 * [--classes=<classes>]
	 * : Set a custom link classes for the menu item.
	 *
	 * [--position=<position>]
	 * : Specify the position of this menu item.
	 *
	 * [--parent-id=<parent-id>]
	 * : Make this menu item a child of another menu item.
	 *
	 * [--porcelain]
	 * : Output just the new menu item id.
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp menu item add-custom sidebar-menu Apple http://apple.com
	 *     Success: Menu item added.
	 *
	 * @subcommand add-custom
	 */
	public function add_custom( $args, $assoc_args ) {

		$assoc_args['title'] = $args[1];
		unset( $args[1] );
		$assoc_args['link'] = $args[2];
		unset( $args[2] );
		$this->add_or_update_item( 'add', 'custom', $args, $assoc_args );
	}

	/**
	 * Updates a menu item.
	 *
	 * ## OPTIONS
	 *
	 * <db-id>
	 * : Database ID for the menu item.
	 *
	 * [--title=<title>]
	 * : Set a custom title for the menu item.
	 *
	 * [--link=<link>]
	 * : Set a custom url for the menu item.
	 *
	 * [--description=<description>]
	 * : Set a custom description for the menu item.
	 *
	 * [--attr-title=<attr-title>]
	 * : Set a custom title attribute for the menu item.
	 *
	 * [--target=<target>]
	 * : Set a custom link target for the menu item.
	 *
	 * [--classes=<classes>]
	 * : Set a custom link classes for the menu item.
	 *
	 * [--position=<position>]
	 * : Specify the position of this menu item.
	 *
	 * [--parent-id=<parent-id>]
	 * : Make this menu item a child of another menu item.
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp menu item update 45 --title=WordPress --link='http://wordpress.org' --target=_blank --position=2
	 *     Success: Menu item updated.
	 *
	 * @subcommand update
	 */
	public function update( $args, $assoc_args ) {

		// Shuffle the position of these.
		$args[1] = $args[0];
		$terms   = get_the_terms( $args[1], 'nav_menu' );
		if ( $terms && ! is_wp_error( $terms ) ) {
			$args[0] = (int) $terms[0]->term_id;
		} else {
			$args[0] = 0;
		}
		$type = get_post_meta( $args[1], '_menu_item_type', true );
		$this->add_or_update_item( 'update', $type, $args, $assoc_args );
	}

	/**
	 * Deletes one or more items from a menu.
	 *
	 * ## OPTIONS
	 *
	 * <db-id>...
	 * : Database ID for the menu item(s).
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp menu item delete 45
	 *     Success: Deleted 1 of 1 menu items.
	 *
	 * @subcommand delete
	 */
	public function delete( $args, $assoc_args ) {
		global $wpdb;

		$count  = 0;
		$errors = 0;

		foreach ( $args as $arg ) {

			$post           = get_post( $arg );
			$menu_term      = get_the_terms( $arg, 'nav_menu' );
			$parent_menu_id = (int) get_post_meta( $arg, '_menu_item_menu_item_parent', true );
			$result         = wp_delete_post( $arg, true );
			if ( ! $result ) {
				WP_CLI::warning( "Couldn't delete menu item {$arg}." );
				++$errors;
			} else {

				if ( is_array( $menu_term ) && ! empty( $menu_term ) && $post ) {
					$this->reorder_menu_items( $menu_term[0]->term_id, $post->menu_order, -1, 0 );
				}

				if ( $parent_menu_id ) {
					$children = $wpdb->get_results( $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key='_menu_item_menu_item_parent' AND meta_value=%s", (int) $arg ) );
					if ( $children ) {
						$children_query = $wpdb->prepare( "UPDATE $wpdb->postmeta SET meta_value = %d WHERE meta_key = '_menu_item_menu_item_parent' AND meta_value=%s", $parent_menu_id, (int) $arg );
						// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $children_query is already prepared above.
						$wpdb->query( $children_query );
						foreach ( $children as $child ) {
							clean_post_cache( $child );
						}
					}
				}
			}

			// phpcs:ignore Universal.Operators.StrictComparisons.LooseNotEqual -- Will increase count for non existent menu.
			if ( false != $result ) {
				++$count;
			}
		}

		Utils\report_batch_operation_results( 'menu item', 'delete', count( $args ), $count, $errors );
	}

	/**
	 * Worker method to create new items or update existing ones.
	 */
	private function add_or_update_item( $method, $type, $args, $assoc_args ) {

		$menu            = $args[0];
		$menu_item_db_id = Utils\get_flag_value( $args, 1, 0 );

		$menu = wp_get_nav_menu_object( $menu );
		if ( ! $menu || is_wp_error( $menu ) ) {
			WP_CLI::error( 'Invalid menu.' );
		}

		// `url` is protected in WP-CLI, so we use `link` instead
		$assoc_args['url'] = Utils\get_flag_value( $assoc_args, 'link' );

		// Need to persist the menu item data. See https://core.trac.wordpress.org/ticket/28138
		if ( 'update' === $method ) {

			$menu_item_obj = get_post( $menu_item_db_id );
			$menu_item_obj = wp_setup_nav_menu_item( $menu_item_obj );

			// Correct the menu position if this was the first item. See https://core.trac.wordpress.org/ticket/28140
			$position = ( 0 === $menu_item_obj->menu_order ) ? 1 : $menu_item_obj->menu_order;

			$default_args = [
				'position'    => $position,
				'title'       => $menu_item_obj->title,
				'url'         => $menu_item_obj->url,
				'description' => $menu_item_obj->description,
				'object'      => $menu_item_obj->object,
				'object-id'   => $menu_item_obj->object_id,
				'parent-id'   => $menu_item_obj->menu_item_parent,
				'attr-title'  => $menu_item_obj->attr_title,
				'target'      => $menu_item_obj->target,
				'classes'     => implode( ' ', $menu_item_obj->classes ), // stored in the database as array
				'xfn'         => $menu_item_obj->xfn,
				'status'      => $menu_item_obj->post_status,
			];

		} else {

			$default_args = [
				'position'    => 0,
				'title'       => '',
				'url'         => '',
				'description' => '',
				'object'      => '',
				'object-id'   => 0,
				'parent-id'   => 0,
				'attr-title'  => '',
				'target'      => '',
				'classes'     => '',
				'xfn'         => '',
				// Core oddly defaults to 'draft' for create,
				// and 'publish' for update
				// Easiest to always work with publish
				'status'      => 'publish',
			];

		}

		$menu_item_args = [];
		foreach ( $default_args as $key => $default_value ) {
			// wp_update_nav_menu_item() has a weird argument prefix
			$new_key                    = 'menu-item-' . $key;
			$menu_item_args[ $new_key ] = Utils\get_flag_value( $assoc_args, $key, $default_value );
		}

		$menu_item_args['menu-item-type'] = $type;
		$result                           = wp_update_nav_menu_item( $menu->term_id, $menu_item_db_id, $menu_item_args );

		if ( is_wp_error( $result ) ) {
			WP_CLI::error( $result->get_error_message() );
		} elseif ( ! $result ) {
			if ( 'add' === $method ) {
				WP_CLI::error( "Couldn't add menu item." );
			} elseif ( 'update' === $method ) {
				WP_CLI::error( "Couldn't update menu item." );
			}
		} else {

			if ( ( 'add' === $method ) && $menu_item_args['menu-item-position'] ) {
				$this->reorder_menu_items( $menu->term_id, $menu_item_args['menu-item-position'], +1, $result );
			}

			/**
			 * Set the menu
			 *
			 * wp_update_nav_menu_item() *should* take care of this, but
			 * depends on wp_insert_post()'s "tax_input" argument, which
			 * is ignored if the user can't edit the taxonomy
			 *
			 * @see https://core.trac.wordpress.org/ticket/27113
			 */
			if ( ! is_object_in_term( $result, 'nav_menu', (int) $menu->term_id ) ) {
				wp_set_object_terms( $result, [ (int) $menu->term_id ], 'nav_menu' );
			}

			if ( 'add' === $method && ! empty( $assoc_args['porcelain'] ) ) {
				WP_CLI::line( $result );
			} elseif ( 'add' === $method ) {
					WP_CLI::success( 'Menu item added.' );
			} elseif ( 'update' === $method ) {
				WP_CLI::success( 'Menu item updated.' );
			}
		}
	}

	/**
	 * Move block of items in one nav_menu up or down by incrementing/decrementing their menu_order field.
	 * Expects the menu items to have proper menu_orders (i.e. doesn't fix errors from previous incorrect operations).
	 *
	 * @param int $menu_id ID of the nav_menu
	 * @param int $min_position minimal menu_order to touch
	 * @param int $increment how much to change menu_order: +1 to move down, -1 to move up
	 * @param int $ignore_item_id menu item that should be ignored by the change (e.g. newly created menu item)
	 * @return int number of rows affected
	 */
	private function reorder_menu_items( $menu_id, $min_position, $increment, $ignore_item_id = 0 ) {
		global $wpdb;
		return $wpdb->query( $wpdb->prepare( "UPDATE $wpdb->posts SET `menu_order`=`menu_order`+(%d) WHERE `menu_order`>=%d AND ID IN (SELECT object_id FROM $wpdb->term_relationships WHERE term_taxonomy_id=%d) AND ID<>%d", (int) $increment, (int) $min_position, (int) $menu_id, (int) $ignore_item_id ) );
	}

	protected function get_formatter( &$assoc_args ) {
		return new Formatter( $assoc_args, $this->obj_fields );
	}
}