home *** CD-ROM | disk | FTP | other *** search
/ HTML Examples / WP.iso / wordpress / wp-includes / customize / class-wp-customize-nav-menu-item-setting.php < prev    next >
Encoding:
PHP Script  |  2017-10-17  |  26.3 KB  |  890 lines

  1. <?php
  2. /**
  3.  * Customize API: WP_Customize_Nav_Menu_Item_Setting class
  4.  *
  5.  * @package WordPress
  6.  * @subpackage Customize
  7.  * @since 4.4.0
  8.  */
  9.  
  10. /**
  11.  * Customize Setting to represent a nav_menu.
  12.  *
  13.  * Subclass of WP_Customize_Setting to represent a nav_menu taxonomy term, and
  14.  * the IDs for the nav_menu_items associated with the nav menu.
  15.  *
  16.  * @since 4.3.0
  17.  *
  18.  * @see WP_Customize_Setting
  19.  */
  20. class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting {
  21.  
  22.     const ID_PATTERN = '/^nav_menu_item\[(?P<id>-?\d+)\]$/';
  23.  
  24.     const POST_TYPE = 'nav_menu_item';
  25.  
  26.     const TYPE = 'nav_menu_item';
  27.  
  28.     /**
  29.      * Setting type.
  30.      *
  31.      * @since 4.3.0
  32.      * @var string
  33.      */
  34.     public $type = self::TYPE;
  35.  
  36.     /**
  37.      * Default setting value.
  38.      *
  39.      * @since 4.3.0
  40.      * @var array
  41.      *
  42.      * @see wp_setup_nav_menu_item()
  43.      */
  44.     public $default = array(
  45.         // The $menu_item_data for wp_update_nav_menu_item().
  46.         'object_id'        => 0,
  47.         'object'           => '', // Taxonomy name.
  48.         'menu_item_parent' => 0, // A.K.A. menu-item-parent-id; note that post_parent is different, and not included.
  49.         'position'         => 0, // A.K.A. menu_order.
  50.         'type'             => 'custom', // Note that type_label is not included here.
  51.         'title'            => '',
  52.         'url'              => '',
  53.         'target'           => '',
  54.         'attr_title'       => '',
  55.         'description'      => '',
  56.         'classes'          => '',
  57.         'xfn'              => '',
  58.         'status'           => 'publish',
  59.         'original_title'   => '',
  60.         'nav_menu_term_id' => 0, // This will be supplied as the $menu_id arg for wp_update_nav_menu_item().
  61.         '_invalid'         => false,
  62.     );
  63.  
  64.     /**
  65.      * Default transport.
  66.      *
  67.      * @since 4.3.0
  68.      * @since 4.5.0 Default changed to 'refresh'
  69.      * @var string
  70.      */
  71.     public $transport = 'refresh';
  72.  
  73.     /**
  74.      * The post ID represented by this setting instance. This is the db_id.
  75.      *
  76.      * A negative value represents a placeholder ID for a new menu not yet saved.
  77.      *
  78.      * @since 4.3.0
  79.      * @var int
  80.      */
  81.     public $post_id;
  82.  
  83.     /**
  84.      * Storage of pre-setup menu item to prevent wasted calls to wp_setup_nav_menu_item().
  85.      *
  86.      * @since 4.3.0
  87.      * @var array
  88.      */
  89.     protected $value;
  90.  
  91.     /**
  92.      * Previous (placeholder) post ID used before creating a new menu item.
  93.      *
  94.      * This value will be exported to JS via the customize_save_response filter
  95.      * so that JavaScript can update the settings to refer to the newly-assigned
  96.      * post ID. This value is always negative to indicate it does not refer to
  97.      * a real post.
  98.      *
  99.      * @since 4.3.0
  100.      * @var int
  101.      *
  102.      * @see WP_Customize_Nav_Menu_Item_Setting::update()
  103.      * @see WP_Customize_Nav_Menu_Item_Setting::amend_customize_save_response()
  104.      */
  105.     public $previous_post_id;
  106.  
  107.     /**
  108.      * When previewing or updating a menu item, this stores the previous nav_menu_term_id
  109.      * which ensures that we can apply the proper filters.
  110.      *
  111.      * @since 4.3.0
  112.      * @var int
  113.      */
  114.     public $original_nav_menu_term_id;
  115.  
  116.     /**
  117.      * Whether or not update() was called.
  118.      *
  119.      * @since 4.3.0
  120.      * @var bool
  121.      */
  122.     protected $is_updated = false;
  123.  
  124.     /**
  125.      * Status for calling the update method, used in customize_save_response filter.
  126.      *
  127.      * See {@see 'customize_save_response'}.
  128.      *
  129.      * When status is inserted, the placeholder post ID is stored in $previous_post_id.
  130.      * When status is error, the error is stored in $update_error.
  131.      *
  132.      * @since 4.3.0
  133.      * @var string updated|inserted|deleted|error
  134.      *
  135.      * @see WP_Customize_Nav_Menu_Item_Setting::update()
  136.      * @see WP_Customize_Nav_Menu_Item_Setting::amend_customize_save_response()
  137.      */
  138.     public $update_status;
  139.  
  140.     /**
  141.      * Any error object returned by wp_update_nav_menu_item() when setting is updated.
  142.      *
  143.      * @since 4.3.0
  144.      * @var WP_Error
  145.      *
  146.      * @see WP_Customize_Nav_Menu_Item_Setting::update()
  147.      * @see WP_Customize_Nav_Menu_Item_Setting::amend_customize_save_response()
  148.      */
  149.     public $update_error;
  150.  
  151.     /**
  152.      * Constructor.
  153.      *
  154.      * Any supplied $args override class property defaults.
  155.      *
  156.      * @since 4.3.0
  157.      *
  158.      * @param WP_Customize_Manager $manager Bootstrap Customizer instance.
  159.      * @param string               $id      An specific ID of the setting. Can be a
  160.      *                                      theme mod or option name.
  161.      * @param array                $args    Optional. Setting arguments.
  162.      *
  163.      * @throws Exception If $id is not valid for this setting type.
  164.      */
  165.     public function __construct( WP_Customize_Manager $manager, $id, array $args = array() ) {
  166.         if ( empty( $manager->nav_menus ) ) {
  167.             throw new Exception( 'Expected WP_Customize_Manager::$nav_menus to be set.' );
  168.         }
  169.  
  170.         if ( ! preg_match( self::ID_PATTERN, $id, $matches ) ) {
  171.             throw new Exception( "Illegal widget setting ID: $id" );
  172.         }
  173.  
  174.         $this->post_id = intval( $matches['id'] );
  175.         add_action( 'wp_update_nav_menu_item', array( $this, 'flush_cached_value' ), 10, 2 );
  176.  
  177.         parent::__construct( $manager, $id, $args );
  178.  
  179.         // Ensure that an initially-supplied value is valid.
  180.         if ( isset( $this->value ) ) {
  181.             $this->populate_value();
  182.             foreach ( array_diff( array_keys( $this->default ), array_keys( $this->value ) ) as $missing ) {
  183.                 throw new Exception( "Supplied nav_menu_item value missing property: $missing" );
  184.             }
  185.         }
  186.  
  187.     }
  188.  
  189.     /**
  190.      * Clear the cached value when this nav menu item is updated.
  191.      *
  192.      * @since 4.3.0
  193.      *
  194.      * @param int $menu_id       The term ID for the menu.
  195.      * @param int $menu_item_id  The post ID for the menu item.
  196.      */
  197.     public function flush_cached_value( $menu_id, $menu_item_id ) {
  198.         unset( $menu_id );
  199.         if ( $menu_item_id === $this->post_id ) {
  200.             $this->value = null;
  201.         }
  202.     }
  203.  
  204.     /**
  205.      * Get the instance data for a given nav_menu_item setting.
  206.      *
  207.      * @since 4.3.0
  208.      *
  209.      * @see wp_setup_nav_menu_item()
  210.      *
  211.      * @return array|false Instance data array, or false if the item is marked for deletion.
  212.      */
  213.     public function value() {
  214.         if ( $this->is_previewed && $this->_previewed_blog_id === get_current_blog_id() ) {
  215.             $undefined  = new stdClass(); // Symbol.
  216.             $post_value = $this->post_value( $undefined );
  217.  
  218.             if ( $undefined === $post_value ) {
  219.                 $value = $this->_original_value;
  220.             } else {
  221.                 $value = $post_value;
  222.             }
  223.             if ( ! empty( $value ) && empty( $value['original_title'] ) ) {
  224.                 $value['original_title'] = $this->get_original_title( (object) $value );
  225.             }
  226.         } elseif ( isset( $this->value ) ) {
  227.             $value = $this->value;
  228.         } else {
  229.             $value = false;
  230.  
  231.             // Note that a ID of less than one indicates a nav_menu not yet inserted.
  232.             if ( $this->post_id > 0 ) {
  233.                 $post = get_post( $this->post_id );
  234.                 if ( $post && self::POST_TYPE === $post->post_type ) {
  235.                     $is_title_empty = empty( $post->post_title );
  236.                     $value = (array) wp_setup_nav_menu_item( $post );
  237.                     if ( $is_title_empty ) {
  238.                         $value['title'] = '';
  239.                     }
  240.                 }
  241.             }
  242.  
  243.             if ( ! is_array( $value ) ) {
  244.                 $value = $this->default;
  245.             }
  246.  
  247.             // Cache the value for future calls to avoid having to re-call wp_setup_nav_menu_item().
  248.             $this->value = $value;
  249.             $this->populate_value();
  250.             $value = $this->value;
  251.         }
  252.  
  253.         if ( ! empty( $value ) && empty( $value['type_label'] ) ) {
  254.             $value['type_label'] = $this->get_type_label( (object) $value );
  255.         }
  256.  
  257.         return $value;
  258.     }
  259.  
  260.     /**
  261.      * Get original title.
  262.      *
  263.      * @since 4.7.0
  264.      *
  265.      * @param object $item Nav menu item.
  266.      * @return string The original title.
  267.      */
  268.     protected function get_original_title( $item ) {
  269.         $original_title = '';
  270.         if ( 'post_type' === $item->type && ! empty( $item->object_id ) ) {
  271.             $original_object = get_post( $item->object_id );
  272.             if ( $original_object ) {
  273.                 /** This filter is documented in wp-includes/post-template.php */
  274.                 $original_title = apply_filters( 'the_title', $original_object->post_title, $original_object->ID );
  275.  
  276.                 if ( '' === $original_title ) {
  277.                     /* translators: %d: ID of a post */
  278.                     $original_title = sprintf( __( '#%d (no title)' ), $original_object->ID );
  279.                 }
  280.             }
  281.         } elseif ( 'taxonomy' === $item->type && ! empty( $item->object_id ) ) {
  282.             $original_term_title = get_term_field( 'name', $item->object_id, $item->object, 'raw' );
  283.             if ( ! is_wp_error( $original_term_title ) ) {
  284.                 $original_title = $original_term_title;
  285.             }
  286.         } elseif ( 'post_type_archive' === $item->type ) {
  287.             $original_object = get_post_type_object( $item->object );
  288.             if ( $original_object ) {
  289.                 $original_title = $original_object->labels->archives;
  290.             }
  291.         }
  292.         $original_title = html_entity_decode( $original_title, ENT_QUOTES, get_bloginfo( 'charset' ) );
  293.         return $original_title;
  294.     }
  295.  
  296.     /**
  297.      * Get type label.
  298.      *
  299.      * @since 4.7.0
  300.      *
  301.      * @param object $item Nav menu item.
  302.      * @returns string The type label.
  303.      */
  304.     protected function get_type_label( $item ) {
  305.         if ( 'post_type' === $item->type ) {
  306.             $object = get_post_type_object( $item->object );
  307.             if ( $object ) {
  308.                 $type_label = $object->labels->singular_name;
  309.             } else {
  310.                 $type_label = $item->object;
  311.             }
  312.         } elseif ( 'taxonomy' === $item->type ) {
  313.             $object = get_taxonomy( $item->object );
  314.             if ( $object ) {
  315.                 $type_label = $object->labels->singular_name;
  316.             } else {
  317.                 $type_label = $item->object;
  318.             }
  319.         } elseif ( 'post_type_archive' === $item->type ) {
  320.             $type_label = __( 'Post Type Archive' );
  321.         } else {
  322.             $type_label = __( 'Custom Link' );
  323.         }
  324.         return $type_label;
  325.     }
  326.  
  327.     /**
  328.      * Ensure that the value is fully populated with the necessary properties.
  329.      *
  330.      * Translates some properties added by wp_setup_nav_menu_item() and removes others.
  331.      *
  332.      * @since 4.3.0
  333.      *
  334.      * @see WP_Customize_Nav_Menu_Item_Setting::value()
  335.      */
  336.     protected function populate_value() {
  337.         if ( ! is_array( $this->value ) ) {
  338.             return;
  339.         }
  340.  
  341.         if ( isset( $this->value['menu_order'] ) ) {
  342.             $this->value['position'] = $this->value['menu_order'];
  343.             unset( $this->value['menu_order'] );
  344.         }
  345.         if ( isset( $this->value['post_status'] ) ) {
  346.             $this->value['status'] = $this->value['post_status'];
  347.             unset( $this->value['post_status'] );
  348.         }
  349.  
  350.         if ( ! isset( $this->value['original_title'] ) ) {
  351.             $this->value['original_title'] = $this->get_original_title( (object) $this->value );
  352.         }
  353.  
  354.         if ( ! isset( $this->value['nav_menu_term_id'] ) && $this->post_id > 0 ) {
  355.             $menus = wp_get_post_terms( $this->post_id, WP_Customize_Nav_Menu_Setting::TAXONOMY, array(
  356.                 'fields' => 'ids',
  357.             ) );
  358.             if ( ! empty( $menus ) ) {
  359.                 $this->value['nav_menu_term_id'] = array_shift( $menus );
  360.             } else {
  361.                 $this->value['nav_menu_term_id'] = 0;
  362.             }
  363.         }
  364.  
  365.         foreach ( array( 'object_id', 'menu_item_parent', 'nav_menu_term_id' ) as $key ) {
  366.             if ( ! is_int( $this->value[ $key ] ) ) {
  367.                 $this->value[ $key ] = intval( $this->value[ $key ] );
  368.             }
  369.         }
  370.         foreach ( array( 'classes', 'xfn' ) as $key ) {
  371.             if ( is_array( $this->value[ $key ] ) ) {
  372.                 $this->value[ $key ] = implode( ' ', $this->value[ $key ] );
  373.             }
  374.         }
  375.  
  376.         if ( ! isset( $this->value['title'] ) ) {
  377.             $this->value['title'] = '';
  378.         }
  379.  
  380.         if ( ! isset( $this->value['_invalid'] ) ) {
  381.             $this->value['_invalid'] = false;
  382.             $is_known_invalid = (
  383.                 ( ( 'post_type' === $this->value['type'] || 'post_type_archive' === $this->value['type'] ) && ! post_type_exists( $this->value['object'] ) )
  384.                 ||
  385.                 ( 'taxonomy' === $this->value['type'] && ! taxonomy_exists( $this->value['object'] ) )
  386.             );
  387.             if ( $is_known_invalid ) {
  388.                 $this->value['_invalid'] = true;
  389.             }
  390.         }
  391.  
  392.         // Remove remaining properties available on a setup nav_menu_item post object which aren't relevant to the setting value.
  393.         $irrelevant_properties = array(
  394.             'ID',
  395.             'comment_count',
  396.             'comment_status',
  397.             'db_id',
  398.             'filter',
  399.             'guid',
  400.             'ping_status',
  401.             'pinged',
  402.             'post_author',
  403.             'post_content',
  404.             'post_content_filtered',
  405.             'post_date',
  406.             'post_date_gmt',
  407.             'post_excerpt',
  408.             'post_mime_type',
  409.             'post_modified',
  410.             'post_modified_gmt',
  411.             'post_name',
  412.             'post_parent',
  413.             'post_password',
  414.             'post_title',
  415.             'post_type',
  416.             'to_ping',
  417.         );
  418.         foreach ( $irrelevant_properties as $property ) {
  419.             unset( $this->value[ $property ] );
  420.         }
  421.     }
  422.  
  423.     /**
  424.      * Handle previewing the setting.
  425.      *
  426.      * @since 4.3.0
  427.      * @since 4.4.0 Added boolean return value.
  428.      *
  429.      * @see WP_Customize_Manager::post_value()
  430.      *
  431.      * @return bool False if method short-circuited due to no-op.
  432.      */
  433.     public function preview() {
  434.         if ( $this->is_previewed ) {
  435.             return false;
  436.         }
  437.  
  438.         $undefined = new stdClass();
  439.         $is_placeholder = ( $this->post_id < 0 );
  440.         $is_dirty = ( $undefined !== $this->post_value( $undefined ) );
  441.         if ( ! $is_placeholder && ! $is_dirty ) {
  442.             return false;
  443.         }
  444.  
  445.         $this->is_previewed              = true;
  446.         $this->_original_value           = $this->value();
  447.         $this->original_nav_menu_term_id = $this->_original_value['nav_menu_term_id'];
  448.         $this->_previewed_blog_id        = get_current_blog_id();
  449.  
  450.         add_filter( 'wp_get_nav_menu_items', array( $this, 'filter_wp_get_nav_menu_items' ), 10, 3 );
  451.  
  452.         $sort_callback = array( __CLASS__, 'sort_wp_get_nav_menu_items' );
  453.         if ( ! has_filter( 'wp_get_nav_menu_items', $sort_callback ) ) {
  454.             add_filter( 'wp_get_nav_menu_items', array( __CLASS__, 'sort_wp_get_nav_menu_items' ), 1000, 3 );
  455.         }
  456.  
  457.         // @todo Add get_post_metadata filters for plugins to add their data.
  458.  
  459.         return true;
  460.     }
  461.  
  462.     /**
  463.      * Filters the wp_get_nav_menu_items() result to supply the previewed menu items.
  464.      *
  465.      * @since 4.3.0
  466.      *
  467.      * @see wp_get_nav_menu_items()
  468.      *
  469.      * @param array  $items An array of menu item post objects.
  470.      * @param object $menu  The menu object.
  471.      * @param array  $args  An array of arguments used to retrieve menu item objects.
  472.      * @return array Array of menu items,
  473.      */
  474.     public function filter_wp_get_nav_menu_items( $items, $menu, $args ) {
  475.         $this_item = $this->value();
  476.         $current_nav_menu_term_id = $this_item['nav_menu_term_id'];
  477.         unset( $this_item['nav_menu_term_id'] );
  478.  
  479.         $should_filter = (
  480.             $menu->term_id === $this->original_nav_menu_term_id
  481.             ||
  482.             $menu->term_id === $current_nav_menu_term_id
  483.         );
  484.         if ( ! $should_filter ) {
  485.             return $items;
  486.         }
  487.  
  488.         // Handle deleted menu item, or menu item moved to another menu.
  489.         $should_remove = (
  490.             false === $this_item
  491.             ||
  492.             true === $this_item['_invalid']
  493.             ||
  494.             (
  495.                 $this->original_nav_menu_term_id === $menu->term_id
  496.                 &&
  497.                 $current_nav_menu_term_id !== $this->original_nav_menu_term_id
  498.             )
  499.         );
  500.         if ( $should_remove ) {
  501.             $filtered_items = array();
  502.             foreach ( $items as $item ) {
  503.                 if ( $item->db_id !== $this->post_id ) {
  504.                     $filtered_items[] = $item;
  505.                 }
  506.             }
  507.             return $filtered_items;
  508.         }
  509.  
  510.         $mutated = false;
  511.         $should_update = (
  512.             is_array( $this_item )
  513.             &&
  514.             $current_nav_menu_term_id === $menu->term_id
  515.         );
  516.         if ( $should_update ) {
  517.             foreach ( $items as $item ) {
  518.                 if ( $item->db_id === $this->post_id ) {
  519.                     foreach ( get_object_vars( $this->value_as_wp_post_nav_menu_item() ) as $key => $value ) {
  520.                         $item->$key = $value;
  521.                     }
  522.                     $mutated = true;
  523.                 }
  524.             }
  525.  
  526.             // Not found so we have to append it..
  527.             if ( ! $mutated ) {
  528.                 $items[] = $this->value_as_wp_post_nav_menu_item();
  529.             }
  530.         }
  531.  
  532.         return $items;
  533.     }
  534.  
  535.     /**
  536.      * Re-apply the tail logic also applied on $items by wp_get_nav_menu_items().
  537.      *
  538.      * @since 4.3.0
  539.      * @static
  540.      *
  541.      * @see wp_get_nav_menu_items()
  542.      *
  543.      * @param array  $items An array of menu item post objects.
  544.      * @param object $menu  The menu object.
  545.      * @param array  $args  An array of arguments used to retrieve menu item objects.
  546.      * @return array Array of menu items,
  547.      */
  548.     public static function sort_wp_get_nav_menu_items( $items, $menu, $args ) {
  549.         // @todo We should probably re-apply some constraints imposed by $args.
  550.         unset( $args['include'] );
  551.  
  552.         // Remove invalid items only in front end.
  553.         if ( ! is_admin() ) {
  554.             $items = array_filter( $items, '_is_valid_nav_menu_item' );
  555.         }
  556.  
  557.         if ( ARRAY_A === $args['output'] ) {
  558.             $items = wp_list_sort( $items, array(
  559.                 $args['output_key'] => 'ASC',
  560.             ) );
  561.             $i = 1;
  562.  
  563.             foreach ( $items as $k => $item ) {
  564.                 $items[ $k ]->{$args['output_key']} = $i++;
  565.             }
  566.         }
  567.  
  568.         return $items;
  569.     }
  570.  
  571.     /**
  572.      * Get the value emulated into a WP_Post and set up as a nav_menu_item.
  573.      *
  574.      * @since 4.3.0
  575.      *
  576.      * @return WP_Post With wp_setup_nav_menu_item() applied.
  577.      */
  578.     public function value_as_wp_post_nav_menu_item() {
  579.         $item = (object) $this->value();
  580.         unset( $item->nav_menu_term_id );
  581.  
  582.         $item->post_status = $item->status;
  583.         unset( $item->status );
  584.  
  585.         $item->post_type = 'nav_menu_item';
  586.         $item->menu_order = $item->position;
  587.         unset( $item->position );
  588.  
  589.         if ( empty( $item->original_title ) ) {
  590.             $item->original_title = $this->get_original_title( $item );
  591.         }
  592.         if ( empty( $item->title ) && ! empty( $item->original_title ) ) {
  593.             $item->title = $item->original_title;
  594.         }
  595.         if ( $item->title ) {
  596.             $item->post_title = $item->title;
  597.         }
  598.  
  599.         $item->ID = $this->post_id;
  600.         $item->db_id = $this->post_id;
  601.         $post = new WP_Post( (object) $item );
  602.  
  603.         if ( empty( $post->post_author ) ) {
  604.             $post->post_author = get_current_user_id();
  605.         }
  606.  
  607.         if ( ! isset( $post->type_label ) ) {
  608.             $post->type_label = $this->get_type_label( $post );
  609.         }
  610.  
  611.         // Ensure nav menu item URL is set according to linked object.
  612.         if ( 'post_type' === $post->type && ! empty( $post->object_id ) ) {
  613.             $post->url = get_permalink( $post->object_id );
  614.         } elseif ( 'taxonomy' === $post->type && ! empty( $post->object ) && ! empty( $post->object_id ) ) {
  615.             $post->url = get_term_link( (int) $post->object_id, $post->object );
  616.         } elseif ( 'post_type_archive' === $post->type && ! empty( $post->object ) ) {
  617.             $post->url = get_post_type_archive_link( $post->object );
  618.         }
  619.         if ( is_wp_error( $post->url ) ) {
  620.             $post->url = '';
  621.         }
  622.  
  623.         /** This filter is documented in wp-includes/nav-menu.php */
  624.         $post->attr_title = apply_filters( 'nav_menu_attr_title', $post->attr_title );
  625.  
  626.         /** This filter is documented in wp-includes/nav-menu.php */
  627.         $post->description = apply_filters( 'nav_menu_description', wp_trim_words( $post->description, 200 ) );
  628.  
  629.         /** This filter is documented in wp-includes/nav-menu.php */
  630.         $post = apply_filters( 'wp_setup_nav_menu_item', $post );
  631.  
  632.         return $post;
  633.     }
  634.  
  635.     /**
  636.      * Sanitize an input.
  637.      *
  638.      * Note that parent::sanitize() erroneously does wp_unslash() on $value, but
  639.      * we remove that in this override.
  640.      *
  641.      * @since 4.3.0
  642.      *
  643.      * @param array $menu_item_value The value to sanitize.
  644.      * @return array|false|null|WP_Error Null or WP_Error if an input isn't valid. False if it is marked for deletion.
  645.      *                                   Otherwise the sanitized value.
  646.      */
  647.     public function sanitize( $menu_item_value ) {
  648.         // Menu is marked for deletion.
  649.         if ( false === $menu_item_value ) {
  650.             return $menu_item_value;
  651.         }
  652.  
  653.         // Invalid.
  654.         if ( ! is_array( $menu_item_value ) ) {
  655.             return null;
  656.         }
  657.  
  658.         $default = array(
  659.             'object_id'        => 0,
  660.             'object'           => '',
  661.             'menu_item_parent' => 0,
  662.             'position'         => 0,
  663.             'type'             => 'custom',
  664.             'title'            => '',
  665.             'url'              => '',
  666.             'target'           => '',
  667.             'attr_title'       => '',
  668.             'description'      => '',
  669.             'classes'          => '',
  670.             'xfn'              => '',
  671.             'status'           => 'publish',
  672.             'original_title'   => '',
  673.             'nav_menu_term_id' => 0,
  674.             '_invalid'         => false,
  675.         );
  676.         $menu_item_value = array_merge( $default, $menu_item_value );
  677.         $menu_item_value = wp_array_slice_assoc( $menu_item_value, array_keys( $default ) );
  678.         $menu_item_value['position'] = intval( $menu_item_value['position'] );
  679.  
  680.         foreach ( array( 'object_id', 'menu_item_parent', 'nav_menu_term_id' ) as $key ) {
  681.             // Note we need to allow negative-integer IDs for previewed objects not inserted yet.
  682.             $menu_item_value[ $key ] = intval( $menu_item_value[ $key ] );
  683.         }
  684.  
  685.         foreach ( array( 'type', 'object', 'target' ) as $key ) {
  686.             $menu_item_value[ $key ] = sanitize_key( $menu_item_value[ $key ] );
  687.         }
  688.  
  689.         foreach ( array( 'xfn', 'classes' ) as $key ) {
  690.             $value = $menu_item_value[ $key ];
  691.             if ( ! is_array( $value ) ) {
  692.                 $value = explode( ' ', $value );
  693.             }
  694.             $menu_item_value[ $key ] = implode( ' ', array_map( 'sanitize_html_class', $value ) );
  695.         }
  696.  
  697.         $menu_item_value['original_title'] = sanitize_text_field( $menu_item_value['original_title'] );
  698.  
  699.         // Apply the same filters as when calling wp_insert_post().
  700.  
  701.         /** This filter is documented in wp-includes/post.php */
  702.         $menu_item_value['title'] = wp_unslash( apply_filters( 'title_save_pre', wp_slash( $menu_item_value['title'] ) ) );
  703.  
  704.         /** This filter is documented in wp-includes/post.php */
  705.         $menu_item_value['attr_title'] = wp_unslash( apply_filters( 'excerpt_save_pre', wp_slash( $menu_item_value['attr_title'] ) ) );
  706.  
  707.         /** This filter is documented in wp-includes/post.php */
  708.         $menu_item_value['description'] = wp_unslash( apply_filters( 'content_save_pre', wp_slash( $menu_item_value['description'] ) ) );
  709.  
  710.         if ( '' !== $menu_item_value['url'] ) {
  711.             $menu_item_value['url'] = esc_url_raw( $menu_item_value['url'] );
  712.             if ( '' === $menu_item_value['url'] ) {
  713.                 return new WP_Error( 'invalid_url', __( 'Invalid URL.' ) ); // Fail sanitization if URL is invalid.
  714.             }
  715.         }
  716.         if ( 'publish' !== $menu_item_value['status'] ) {
  717.             $menu_item_value['status'] = 'draft';
  718.         }
  719.  
  720.         $menu_item_value['_invalid'] = (bool) $menu_item_value['_invalid'];
  721.  
  722.         /** This filter is documented in wp-includes/class-wp-customize-setting.php */
  723.         return apply_filters( "customize_sanitize_{$this->id}", $menu_item_value, $this );
  724.     }
  725.  
  726.     /**
  727.      * Creates/updates the nav_menu_item post for this setting.
  728.      *
  729.      * Any created menu items will have their assigned post IDs exported to the client
  730.      * via the {@see 'customize_save_response'} filter. Likewise, any errors will be
  731.      * exported to the client via the customize_save_response() filter.
  732.      *
  733.      * To delete a menu, the client can send false as the value.
  734.      *
  735.      * @since 4.3.0
  736.      *
  737.      * @see wp_update_nav_menu_item()
  738.      *
  739.      * @param array|false $value The menu item array to update. If false, then the menu item will be deleted
  740.      *                           entirely. See WP_Customize_Nav_Menu_Item_Setting::$default for what the value
  741.      *                           should consist of.
  742.      * @return null|void
  743.      */
  744.     protected function update( $value ) {
  745.         if ( $this->is_updated ) {
  746.             return;
  747.         }
  748.  
  749.         $this->is_updated = true;
  750.         $is_placeholder   = ( $this->post_id < 0 );
  751.         $is_delete        = ( false === $value );
  752.  
  753.         // Update the cached value.
  754.         $this->value = $value;
  755.  
  756.         add_filter( 'customize_save_response', array( $this, 'amend_customize_save_response' ) );
  757.  
  758.         if ( $is_delete ) {
  759.             // If the current setting post is a placeholder, a delete request is a no-op.
  760.             if ( $is_placeholder ) {
  761.                 $this->update_status = 'deleted';
  762.             } else {
  763.                 $r = wp_delete_post( $this->post_id, true );
  764.  
  765.                 if ( false === $r ) {
  766.                     $this->update_error  = new WP_Error( 'delete_failure' );
  767.                     $this->update_status = 'error';
  768.                 } else {
  769.                     $this->update_status = 'deleted';
  770.                 }
  771.                 // @todo send back the IDs for all associated nav menu items deleted, so these settings (and controls) can be removed from Customizer?
  772.             }
  773.         } else {
  774.  
  775.             // Handle saving menu items for menus that are being newly-created.
  776.             if ( $value['nav_menu_term_id'] < 0 ) {
  777.                 $nav_menu_setting_id = sprintf( 'nav_menu[%s]', $value['nav_menu_term_id'] );
  778.                 $nav_menu_setting    = $this->manager->get_setting( $nav_menu_setting_id );
  779.  
  780.                 if ( ! $nav_menu_setting || ! ( $nav_menu_setting instanceof WP_Customize_Nav_Menu_Setting ) ) {
  781.                     $this->update_status = 'error';
  782.                     $this->update_error  = new WP_Error( 'unexpected_nav_menu_setting' );
  783.                     return;
  784.                 }
  785.  
  786.                 if ( false === $nav_menu_setting->save() ) {
  787.                     $this->update_status = 'error';
  788.                     $this->update_error  = new WP_Error( 'nav_menu_setting_failure' );
  789.                     return;
  790.                 }
  791.  
  792.                 if ( $nav_menu_setting->previous_term_id !== intval( $value['nav_menu_term_id'] ) ) {
  793.                     $this->update_status = 'error';
  794.                     $this->update_error  = new WP_Error( 'unexpected_previous_term_id' );
  795.                     return;
  796.                 }
  797.  
  798.                 $value['nav_menu_term_id'] = $nav_menu_setting->term_id;
  799.             }
  800.  
  801.             // Handle saving a nav menu item that is a child of a nav menu item being newly-created.
  802.             if ( $value['menu_item_parent'] < 0 ) {
  803.                 $parent_nav_menu_item_setting_id = sprintf( 'nav_menu_item[%s]', $value['menu_item_parent'] );
  804.                 $parent_nav_menu_item_setting    = $this->manager->get_setting( $parent_nav_menu_item_setting_id );
  805.  
  806.                 if ( ! $parent_nav_menu_item_setting || ! ( $parent_nav_menu_item_setting instanceof WP_Customize_Nav_Menu_Item_Setting ) ) {
  807.                     $this->update_status = 'error';
  808.                     $this->update_error  = new WP_Error( 'unexpected_nav_menu_item_setting' );
  809.                     return;
  810.                 }
  811.  
  812.                 if ( false === $parent_nav_menu_item_setting->save() ) {
  813.                     $this->update_status = 'error';
  814.                     $this->update_error  = new WP_Error( 'nav_menu_item_setting_failure' );
  815.                     return;
  816.                 }
  817.  
  818.                 if ( $parent_nav_menu_item_setting->previous_post_id !== intval( $value['menu_item_parent'] ) ) {
  819.                     $this->update_status = 'error';
  820.                     $this->update_error  = new WP_Error( 'unexpected_previous_post_id' );
  821.                     return;
  822.                 }
  823.  
  824.                 $value['menu_item_parent'] = $parent_nav_menu_item_setting->post_id;
  825.             }
  826.  
  827.             // Insert or update menu.
  828.             $menu_item_data = array(
  829.                 'menu-item-object-id'   => $value['object_id'],
  830.                 'menu-item-object'      => $value['object'],
  831.                 'menu-item-parent-id'   => $value['menu_item_parent'],
  832.                 'menu-item-position'    => $value['position'],
  833.                 'menu-item-type'        => $value['type'],
  834.                 'menu-item-title'       => $value['title'],
  835.                 'menu-item-url'         => $value['url'],
  836.                 'menu-item-description' => $value['description'],
  837.                 'menu-item-attr-title'  => $value['attr_title'],
  838.                 'menu-item-target'      => $value['target'],
  839.                 'menu-item-classes'     => $value['classes'],
  840.                 'menu-item-xfn'         => $value['xfn'],
  841.                 'menu-item-status'      => $value['status'],
  842.             );
  843.  
  844.             $r = wp_update_nav_menu_item(
  845.                 $value['nav_menu_term_id'],
  846.                 $is_placeholder ? 0 : $this->post_id,
  847.                 wp_slash( $menu_item_data )
  848.             );
  849.  
  850.             if ( is_wp_error( $r ) ) {
  851.                 $this->update_status = 'error';
  852.                 $this->update_error = $r;
  853.             } else {
  854.                 if ( $is_placeholder ) {
  855.                     $this->previous_post_id = $this->post_id;
  856.                     $this->post_id = $r;
  857.                     $this->update_status = 'inserted';
  858.                 } else {
  859.                     $this->update_status = 'updated';
  860.                 }
  861.             }
  862.         }
  863.  
  864.     }
  865.  
  866.     /**
  867.      * Export data for the JS client.
  868.      *
  869.      * @since 4.3.0
  870.      *
  871.      * @see WP_Customize_Nav_Menu_Item_Setting::update()
  872.      *
  873.      * @param array $data Additional information passed back to the 'saved' event on `wp.customize`.
  874.      * @return array Save response data.
  875.      */
  876.     public function amend_customize_save_response( $data ) {
  877.         if ( ! isset( $data['nav_menu_item_updates'] ) ) {
  878.             $data['nav_menu_item_updates'] = array();
  879.         }
  880.  
  881.         $data['nav_menu_item_updates'][] = array(
  882.             'post_id'          => $this->post_id,
  883.             'previous_post_id' => $this->previous_post_id,
  884.             'error'            => $this->update_error ? $this->update_error->get_error_code() : null,
  885.             'status'           => $this->update_status,
  886.         );
  887.         return $data;
  888.     }
  889. }
  890.