WordPress.org

Make WordPress Core


Ignore:
Timestamp:
06/16/2015 10:07:08 PM (6 years ago)
Author:
ocean90
Message:

Add menu management to the Customizer.

This brings in the Menu Customizer plugin: https://wordpress.org/plugins/menu-customizer/.

props celloexpressions, westonruter, valendesigns, voldemortensen, ocean90, adamsilverstein, kucrut, jorbin, designsimply, afercia, davidakennedy, obenland.
see #32576.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-customize-setting.php

    r32767 r32806  
    631631    }
    632632}
     633
     634/**
     635 * Customize Setting to represent a nav_menu.
     636 *
     637 * Subclass of WP_Customize_Setting to represent a nav_menu taxonomy term, and
     638 * the IDs for the nav_menu_items associated with the nav menu.
     639 *
     640 * @since 4.3.0
     641 *
     642 * @see wp_get_nav_menu_items()
     643 * @see WP_Customize_Setting
     644 */
     645class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting {
     646
     647    const ID_PATTERN = '/^nav_menu_item\[(?P<id>-?\d+)\]$/';
     648
     649    const POST_TYPE = 'nav_menu_item';
     650
     651    const TYPE = 'nav_menu_item';
     652
     653    /**
     654     * Setting type.
     655     *
     656     * @since 4.3.0
     657     *
     658     * @var string
     659     */
     660    public $type = self::TYPE;
     661
     662    /**
     663     * Default setting value.
     664     *
     665     * @since 4.3.0
     666     *
     667     * @see wp_setup_nav_menu_item()
     668     * @var array
     669     */
     670    public $default = array(
     671        // The $menu_item_data for wp_update_nav_menu_item().
     672        'object_id'        => 0,
     673        'object'           => '', // Taxonomy name.
     674        'menu_item_parent' => 0, // A.K.A. menu-item-parent-id; note that post_parent is different, and not included.
     675        'position'         => 0, // A.K.A. menu_order.
     676        'type'             => 'custom', // Note that type_label is not included here.
     677        'title'            => '',
     678        'url'              => '',
     679        'target'           => '',
     680        'attr_title'       => '',
     681        'description'      => '',
     682        'classes'          => '',
     683        'xfn'              => '',
     684        'status'           => 'publish',
     685        'original_title'   => '',
     686        'nav_menu_term_id' => 0, // This will be supplied as the $menu_id arg for wp_update_nav_menu_item().
     687        // @todo also expose invalid?
     688    );
     689
     690    /**
     691     * Default transport.
     692     *
     693     * @since 4.3.0
     694     *
     695     * @var string
     696     */
     697    public $transport = 'postMessage';
     698
     699    /**
     700     * The post ID represented by this setting instance. This is the db_id.
     701     *
     702     * A negative value represents a placeholder ID for a new menu not yet saved.
     703     *
     704     * @todo Should this be $db_id, and also use this for WP_Customize_Nav_Menu_Setting::$term_id
     705     *
     706     * @since 4.3.0
     707     *
     708     * @var int
     709     */
     710    public $post_id;
     711
     712    /**
     713     * Previous (placeholder) post ID used before creating a new menu item.
     714     *
     715     * This value will be exported to JS via the customize_save_response filter
     716     * so that JavaScript can update the settings to refer to the newly-assigned
     717     * post ID. This value is always negative to indicate it does not refer to
     718     * a real post.
     719     *
     720     * @since 4.3.0
     721     *
     722     * @see WP_Customize_Nav_Menu_Item_Setting::update()
     723     * @see WP_Customize_Nav_Menu_Item_Setting::amend_customize_save_response()
     724     *
     725     * @var int
     726     */
     727    public $previous_post_id;
     728
     729    /**
     730     * When previewing or updating a menu item, this stores the previous nav_menu_term_id
     731     * which ensures that we can apply the proper filters.
     732     *
     733     * @since 4.3.0
     734     *
     735     * @var int
     736     */
     737    public $original_nav_menu_term_id;
     738
     739    /**
     740     * Whether or not preview() was called.
     741     *
     742     * @since 4.3.0
     743     *
     744     * @var bool
     745     */
     746    protected $is_previewed = false;
     747
     748    /**
     749     * Whether or not update() was called.
     750     *
     751     * @since 4.3.0
     752     *
     753     * @var bool
     754     */
     755    protected $is_updated = false;
     756
     757    /**
     758     * Status for calling the update method, used in customize_save_response filter.
     759     *
     760     * When status is inserted, the placeholder post ID is stored in $previous_post_id.
     761     * When status is error, the error is stored in $update_error.
     762     *
     763     * @since 4.3.0
     764     *
     765     * @see WP_Customize_Nav_Menu_Item_Setting::update()
     766     * @see WP_Customize_Nav_Menu_Item_Setting::amend_customize_save_response()
     767     *
     768     * @var string updated|inserted|deleted|error
     769     */
     770    public $update_status;
     771
     772    /**
     773     * Any error object returned by wp_update_nav_menu_item() when setting is updated.
     774     *
     775     * @since 4.3.0
     776     *
     777     * @see WP_Customize_Nav_Menu_Item_Setting::update()
     778     * @see WP_Customize_Nav_Menu_Item_Setting::amend_customize_save_response()
     779     *
     780     * @var WP_Error
     781     */
     782    public $update_error;
     783
     784    /**
     785     * Constructor.
     786     *
     787     * Any supplied $args override class property defaults.
     788     *
     789     * @since 4.3.0
     790     *
     791     * @param WP_Customize_Manager $manager Manager instance.
     792     * @param string               $id      An specific ID of the setting. Can be a
     793     *                                      theme mod or option name.
     794     * @param array                $args    Optional. Setting arguments.
     795     * @throws Exception If $id is not valid for this setting type.
     796     */
     797    public function __construct( WP_Customize_Manager $manager, $id, array $args = array() ) {
     798        if ( empty( $manager->nav_menus ) ) {
     799            throw new Exception( 'Expected WP_Customize_Manager::$nav_menus to be set.' );
     800        }
     801
     802        if ( ! preg_match( self::ID_PATTERN, $id, $matches ) ) {
     803            throw new Exception( "Illegal widget setting ID: $id" );
     804        }
     805
     806        $this->post_id = intval( $matches['id'] );
     807
     808        $menu = $this->value();
     809        $this->original_nav_menu_term_id = $menu['nav_menu_term_id'];
     810
     811        parent::__construct( $manager, $id, $args );
     812    }
     813
     814    /**
     815     * Get the instance data for a given widget setting.
     816     *
     817     * @since 4.3.0
     818     *
     819     * @see wp_setup_nav_menu_item()
     820     *
     821     * @return array
     822     */
     823    public function value() {
     824        if ( $this->is_previewed && $this->_previewed_blog_id === get_current_blog_id() ) {
     825            $undefined  = new stdClass(); // Symbol.
     826            $post_value = $this->post_value( $undefined );
     827
     828            if ( $undefined === $post_value ) {
     829                $value = $this->_original_value;
     830            } else {
     831                $value = $post_value;
     832            }
     833        } else {
     834            $value = false;
     835
     836            // Note that a ID of less than one indicates a nav_menu not yet inserted.
     837            if ( $this->post_id > 0 ) {
     838                $post = get_post( $this->post_id );
     839                if ( $post && self::POST_TYPE === $post->post_type ) {
     840                    $item  = wp_setup_nav_menu_item( $post );
     841                    $value = wp_array_slice_assoc(
     842                        (array) $item,
     843                        array_keys( $this->default )
     844                    );
     845                    $value['position']       = $item->menu_order;
     846                    $value['status']         = $item->post_status;
     847                    $value['original_title'] = '';
     848
     849                    $menus = wp_get_post_terms( $post->ID, WP_Customize_Nav_Menu_Setting::TAXONOMY, array(
     850                        'fields' => 'ids',
     851                    ) );
     852
     853                    if ( ! empty( $menus ) ) {
     854                        $value['nav_menu_term_id'] = array_shift( $menus );
     855                    } else {
     856                        $value['nav_menu_term_id'] = 0;
     857                    }
     858
     859                    if ( 'post_type' === $value['type'] ) {
     860                        $original_title = get_the_title( $value['object_id'] );
     861                    } else if ( 'taxonomy' === $value['type'] ) {
     862                        $original_title = get_term_field( 'name', $value['object_id'], $value['object'], 'raw' );
     863                        if ( is_wp_error( $original_title ) ) {
     864                            $original_title = '';
     865                        }
     866                    }
     867
     868                    if ( ! empty( $original_title ) ) {
     869                        $value['original_title'] = $original_title;
     870                    }
     871                }
     872            }
     873
     874            if ( ! is_array( $value ) ) {
     875                $value = $this->default;
     876            }
     877        }
     878
     879        if ( is_array( $value ) ) {
     880            foreach ( array( 'object_id', 'menu_item_parent', 'nav_menu_term_id' ) as $key ) {
     881                $value[ $key ] = intval( $value[ $key ] );
     882            }
     883        }
     884
     885        return $value;
     886    }
     887
     888    /**
     889     * Handle previewing the setting.
     890     *
     891     * @since 4.3.0
     892     *
     893     * @see WP_Customize_Manager::post_value()
     894     */
     895    public function preview() {
     896        if ( $this->is_previewed ) {
     897            return;
     898        }
     899
     900        $this->is_previewed              = true;
     901        $this->_original_value           = $this->value();
     902        $this->original_nav_menu_term_id = $this->_original_value['nav_menu_term_id'];
     903        $this->_previewed_blog_id        = get_current_blog_id();
     904
     905        add_filter( 'wp_get_nav_menu_items', array( $this, 'filter_wp_get_nav_menu_items' ), 10, 3 );
     906
     907        $sort_callback = array( __CLASS__, 'sort_wp_get_nav_menu_items' );
     908        if ( ! has_filter( 'wp_get_nav_menu_items', $sort_callback ) ) {
     909            add_filter( 'wp_get_nav_menu_items', array( __CLASS__, 'sort_wp_get_nav_menu_items' ), 1000, 3 );
     910        }
     911
     912        // @todo Add get_post_metadata filters for plugins to add their data.
     913    }
     914
     915    /**
     916     * Filter the wp_get_nav_menu_items() result to supply the previewed menu items.
     917     *
     918     * @since 4.3.0
     919     *
     920     * @see wp_get_nav_menu_items()
     921     *
     922     * @param array  $items An array of menu item post objects.
     923     * @param object $menu  The menu object.
     924     * @param array  $args  An array of arguments used to retrieve menu item objects.
     925     * @return array Array of menu items,
     926     */
     927    function filter_wp_get_nav_menu_items( $items, $menu, $args ) {
     928        $this_item = $this->value();
     929        $current_nav_menu_term_id = $this_item['nav_menu_term_id'];
     930        unset( $this_item['nav_menu_term_id'] );
     931
     932        $should_filter = (
     933            $menu->term_id === $this->original_nav_menu_term_id
     934            ||
     935            $menu->term_id === $current_nav_menu_term_id
     936        );
     937        if ( ! $should_filter ) {
     938            return $items;
     939        }
     940
     941        // Handle deleted menu item, or menu item moved to another menu.
     942        $should_remove = (
     943            false === $this_item
     944            ||
     945            (
     946                $this->original_nav_menu_term_id === $menu->term_id
     947                &&
     948                $current_nav_menu_term_id !== $this->original_nav_menu_term_id
     949            )
     950        );
     951        if ( $should_remove ) {
     952            $filtered_items = array();
     953            foreach ( $items as $item ) {
     954                if ( $item->db_id !== $this->post_id ) {
     955                    $filtered_items[] = $item;
     956                }
     957            }
     958            return $filtered_items;
     959        }
     960
     961        $mutated = false;
     962        $should_update = (
     963            is_array( $this_item )
     964            &&
     965            $current_nav_menu_term_id === $menu->term_id
     966        );
     967        if ( $should_update ) {
     968            foreach ( $items as $item ) {
     969                if ( $item->db_id === $this->post_id ) {
     970                    foreach ( get_object_vars( $this->value_as_wp_post_nav_menu_item() ) as $key => $value ) {
     971                        $item->$key = $value;
     972                    }
     973                    $mutated = true;
     974                }
     975            }
     976
     977            // Not found so we have to append it..
     978            if ( ! $mutated ) {
     979                $items[] = $this->value_as_wp_post_nav_menu_item();
     980            }
     981        }
     982
     983        return $items;
     984    }
     985
     986    /**
     987     * Re-apply the tail logic also applied on $items by wp_get_nav_menu_items().
     988     *
     989     * @since 4.3.0
     990     *
     991     * @see wp_get_nav_menu_items()
     992     *
     993     * @param array  $items An array of menu item post objects.
     994     * @param object $menu  The menu object.
     995     * @param array  $args  An array of arguments used to retrieve menu item objects.
     996     * @return array Array of menu items,
     997     */
     998    static function sort_wp_get_nav_menu_items( $items, $menu, $args ) {
     999        // @todo We should probably re-apply some constraints imposed by $args.
     1000        unset( $args['include'] );
     1001
     1002        // Remove invalid items only in frontend.
     1003        if ( ! is_admin() ) {
     1004            $items = array_filter( $items, '_is_valid_nav_menu_item' );
     1005        }
     1006
     1007        if ( ARRAY_A === $args['output'] ) {
     1008            $GLOBALS['_menu_item_sort_prop'] = $args['output_key'];
     1009            usort( $items, '_sort_nav_menu_items' );
     1010            $i = 1;
     1011
     1012            foreach ( $items as $k => $item ) {
     1013                $items[ $k ]->$args['output_key'] = $i++;
     1014            }
     1015        }
     1016
     1017        return $items;
     1018    }
     1019
     1020    /**
     1021     * Get the value emulated into a WP_Post and set up as a nav_menu_item.
     1022     *
     1023     * @since 4.3.0
     1024     *
     1025     * @return WP_Post With {@see wp_setup_nav_menu_item()} applied.
     1026     */
     1027    public function value_as_wp_post_nav_menu_item() {
     1028        $item = (object) $this->value();
     1029        unset( $item->nav_menu_term_id );
     1030
     1031        $item->post_status = $item->status;
     1032        unset( $item->status );
     1033
     1034        $item->post_type = 'nav_menu_item';
     1035        $item->menu_order = $item->position;
     1036        unset( $item->position );
     1037
     1038        $item->post_author = get_current_user_id();
     1039
     1040        if ( $item->title ) {
     1041            $item->post_title = $item->title;
     1042        }
     1043
     1044        $item->ID = $this->post_id;
     1045        $post = new WP_Post( (object) $item );
     1046        $post = wp_setup_nav_menu_item( $post );
     1047
     1048        return $post;
     1049    }
     1050
     1051    /**
     1052     * Sanitize an input.
     1053     *
     1054     * Note that parent::sanitize() erroneously does wp_unslash() on $value, but
     1055     * we remove that in this override.
     1056     *
     1057     * @since 4.3.0
     1058     *
     1059     * @param array $menu_item_value The value to sanitize.
     1060     * @return array|false|null Null if an input isn't valid. False if it is marked for deletion. Otherwise the sanitized value.
     1061     */
     1062    public function sanitize( $menu_item_value ) {
     1063        // Menu is marked for deletion.
     1064        if ( false === $menu_item_value ) {
     1065            return $menu_item_value;
     1066        }
     1067
     1068        // Invalid.
     1069        if ( ! is_array( $menu_item_value ) ) {
     1070            return null;
     1071        }
     1072
     1073        $default = array(
     1074            'object_id'        => 0,
     1075            'object'           => '',
     1076            'menu_item_parent' => 0,
     1077            'position'         => 0,
     1078            'type'             => 'custom',
     1079            'title'            => '',
     1080            'url'              => '',
     1081            'target'           => '',
     1082            'attr_title'       => '',
     1083            'description'      => '',
     1084            'classes'          => '',
     1085            'xfn'              => '',
     1086            'status'           => 'publish',
     1087            'original_title'   => '',
     1088            'nav_menu_term_id' => 0,
     1089        );
     1090        $menu_item_value = array_merge( $default, $menu_item_value );
     1091        $menu_item_value = wp_array_slice_assoc( $menu_item_value, array_keys( $default ) );
     1092        $menu_item_value['position'] = max( 0, intval( $menu_item_value['position'] ) );
     1093
     1094        foreach ( array( 'object_id', 'menu_item_parent', 'nav_menu_term_id' ) as $key ) {
     1095            // Note we need to allow negative-integer IDs for previewed objects not inserted yet.
     1096            $menu_item_value[ $key ] = intval( $menu_item_value[ $key ] );
     1097        }
     1098
     1099        foreach ( array( 'type', 'object', 'target' ) as $key ) {
     1100            $menu_item_value[ $key ] = sanitize_key( $menu_item_value[ $key ] );
     1101        }
     1102
     1103        foreach ( array( 'xfn', 'classes' ) as $key ) {
     1104            $value = $menu_item_value[ $key ];
     1105            if ( ! is_array( $value ) ) {
     1106                $value = explode( ' ', $value );
     1107            }
     1108            $menu_item_value[ $key ] = implode( ' ', array_map( 'sanitize_html_class', $value ) );
     1109        }
     1110
     1111        foreach ( array( 'title', 'attr_title', 'description', 'original_title' ) as $key ) {
     1112            // @todo Should esc_attr() the attr_title as well?
     1113            $menu_item_value[ $key ] = sanitize_text_field( $menu_item_value[ $key ] );
     1114        }
     1115
     1116        $menu_item_value['url'] = esc_url_raw( $menu_item_value['url'] );
     1117        if ( ! get_post_status_object( $menu_item_value['status'] ) ) {
     1118            $menu_item_value['status'] = 'publish';
     1119        }
     1120
     1121        /** This filter is documented in wp-includes/class-wp-customize-setting.php */
     1122        return apply_filters( "customize_sanitize_{$this->id}", $menu_item_value, $this );
     1123    }
     1124
     1125    /**
     1126     * Create/update the nav_menu_item post for this setting.
     1127     *
     1128     * Any created menu items will have their assigned post IDs exported to the client
     1129     * via the customize_save_response filter. Likewise, any errors will be exported
     1130     * to the client via the customize_save_response() filter.
     1131     *
     1132     * To delete a menu, the client can send false as the value.
     1133     *
     1134     * @since 4.3.0
     1135     *
     1136     * @see wp_update_nav_menu_item()
     1137     *
     1138     * @param array|false $value The menu item array to update. If false, then the menu item will be deleted entirely.
     1139     *                           See {@see WP_Customize_Nav_Menu_Item_Setting::$default} for what the value should
     1140     *                           consist of.
     1141     * @return void
     1142     */
     1143    protected function update( $value ) {
     1144        if ( $this->is_updated ) {
     1145            return;
     1146        }
     1147
     1148        $this->is_updated = true;
     1149        $is_placeholder   = ( $this->post_id < 0 );
     1150        $is_delete        = ( false === $value );
     1151
     1152        add_filter( 'customize_save_response', array( $this, 'amend_customize_save_response' ) );
     1153
     1154        if ( $is_delete ) {
     1155            // If the current setting post is a placeholder, a delete request is a no-op.
     1156            if ( $is_placeholder ) {
     1157                $this->update_status = 'deleted';
     1158            } else {
     1159                $r = wp_delete_post( $this->post_id, true );
     1160
     1161                if ( false === $r ) {
     1162                    $this->update_error  = new WP_Error( 'delete_failure' );
     1163                    $this->update_status = 'error';
     1164                } else {
     1165                    $this->update_status = 'deleted';
     1166                }
     1167                // @todo send back the IDs for all associated nav menu items deleted, so these settings (and controls) can be removed from Customizer?
     1168            }
     1169        } else {
     1170
     1171            // Handle saving menu items for menus that are being newly-created.
     1172            if ( $value['nav_menu_term_id'] < 0 ) {
     1173                $nav_menu_setting_id = sprintf( 'nav_menu[%s]', $value['nav_menu_term_id'] );
     1174                $nav_menu_setting    = $this->manager->get_setting( $nav_menu_setting_id );
     1175
     1176                if ( ! $nav_menu_setting || ! ( $nav_menu_setting instanceof WP_Customize_Nav_Menu_Setting ) ) {
     1177                    $this->update_status = 'error';
     1178                    $this->update_error  = new WP_Error( 'unexpected_nav_menu_setting' );
     1179                    return;
     1180                }
     1181
     1182                if ( false === $nav_menu_setting->save() ) {
     1183                    $this->update_status = 'error';
     1184                    $this->update_error  = new WP_Error( 'nav_menu_setting_failure' );
     1185                }
     1186
     1187                if ( $nav_menu_setting->previous_term_id !== intval( $value['nav_menu_term_id'] ) ) {
     1188                    $this->update_status = 'error';
     1189                    $this->update_error  = new WP_Error( 'unexpected_previous_term_id' );
     1190                    return;
     1191                }
     1192
     1193                $value['nav_menu_term_id'] = $nav_menu_setting->term_id;
     1194            }
     1195
     1196            // Handle saving a nav menu item that is a child of a nav menu item being newly-created.
     1197            if ( $value['menu_item_parent'] < 0 ) {
     1198                $parent_nav_menu_item_setting_id = sprintf( 'nav_menu_item[%s]', $value['menu_item_parent'] );
     1199                $parent_nav_menu_item_setting    = $this->manager->get_setting( $parent_nav_menu_item_setting_id );
     1200
     1201                if ( ! $parent_nav_menu_item_setting || ! ( $parent_nav_menu_item_setting instanceof WP_Customize_Nav_Menu_Item_Setting ) ) {
     1202                    $this->update_status = 'error';
     1203                    $this->update_error  = new WP_Error( 'unexpected_nav_menu_item_setting' );
     1204                    return;
     1205                }
     1206
     1207                if ( false === $parent_nav_menu_item_setting->save() ) {
     1208                    $this->update_status = 'error';
     1209                    $this->update_error  = new WP_Error( 'nav_menu_item_setting_failure' );
     1210                }
     1211
     1212                if ( $parent_nav_menu_item_setting->previous_post_id !== intval( $value['menu_item_parent'] ) ) {
     1213                    $this->update_status = 'error';
     1214                    $this->update_error  = new WP_Error( 'unexpected_previous_post_id' );
     1215                    return;
     1216                }
     1217
     1218                $value['menu_item_parent'] = $parent_nav_menu_item_setting->post_id;
     1219            }
     1220
     1221            // Insert or update menu.
     1222            $menu_item_data = array(
     1223                'menu-item-object-id'   => $value['object_id'],
     1224                'menu-item-object'      => $value['object'],
     1225                'menu-item-parent-id'   => $value['menu_item_parent'],
     1226                'menu-item-position'    => $value['position'],
     1227                'menu-item-type'        => $value['type'],
     1228                'menu-item-title'       => $value['title'],
     1229                'menu-item-url'         => $value['url'],
     1230                'menu-item-description' => $value['description'],
     1231                'menu-item-attr-title'  => $value['attr_title'],
     1232                'menu-item-target'      => $value['target'],
     1233                'menu-item-classes'     => $value['classes'],
     1234                'menu-item-xfn'         => $value['xfn'],
     1235                'menu-item-status'      => $value['status'],
     1236            );
     1237
     1238            $r = wp_update_nav_menu_item(
     1239                $value['nav_menu_term_id'],
     1240                $is_placeholder ? 0 : $this->post_id,
     1241                $menu_item_data
     1242            );
     1243
     1244            if ( is_wp_error( $r ) ) {
     1245                $this->update_status = 'error';
     1246                $this->update_error = $r;
     1247            } else {
     1248                if ( $is_placeholder ) {
     1249                    $this->previous_post_id = $this->post_id;
     1250                    $this->post_id = $r;
     1251                    $this->update_status = 'inserted';
     1252                } else {
     1253                    $this->update_status = 'updated';
     1254                }
     1255            }
     1256        }
     1257
     1258    }
     1259
     1260    /**
     1261     * Export data for the JS client.
     1262     *
     1263     * @since 4.3.0
     1264     *
     1265     * @see WP_Customize_Nav_Menu_Item_Setting::update()
     1266     *
     1267     * @param array $data Additional information passed back to the 'saved' event on `wp.customize`.
     1268     * @return array
     1269     */
     1270    function amend_customize_save_response( $data ) {
     1271        if ( ! isset( $data['nav_menu_item_updates'] ) ) {
     1272            $data['nav_menu_item_updates'] = array();
     1273        }
     1274
     1275        $data['nav_menu_item_updates'][] = array(
     1276            'post_id'          => $this->post_id,
     1277            'previous_post_id' => $this->previous_post_id,
     1278            'error'            => $this->update_error ? $this->update_error->get_error_code() : null,
     1279            'status'           => $this->update_status,
     1280        );
     1281
     1282        return $data;
     1283    }
     1284}
     1285
     1286/**
     1287 * Customize Setting to represent a nav_menu.
     1288 *
     1289 * Subclass of WP_Customize_Setting to represent a nav_menu taxonomy term, and
     1290 * the IDs for the nav_menu_items associated with the nav menu.
     1291 *
     1292 * @since 4.3.0
     1293 *
     1294 * @see wp_get_nav_menu_object()
     1295 * @see WP_Customize_Setting
     1296 */
     1297class WP_Customize_Nav_Menu_Setting extends WP_Customize_Setting {
     1298
     1299    const ID_PATTERN = '/^nav_menu\[(?P<id>-?\d+)\]$/';
     1300
     1301    const TAXONOMY = 'nav_menu';
     1302
     1303    const TYPE = 'nav_menu';
     1304
     1305    /**
     1306     * Setting type.
     1307     *
     1308     * @since 4.3.0
     1309     *
     1310     * @var string
     1311     */
     1312    public $type = self::TYPE;
     1313
     1314    /**
     1315     * Default setting value.
     1316     *
     1317     * @since 4.3.0
     1318     *
     1319     * @see wp_get_nav_menu_object()
     1320     *
     1321     * @var array
     1322     */
     1323    public $default = array(
     1324        'name'        => '',
     1325        'description' => '',
     1326        'parent'      => 0,
     1327        'auto_add'    => false,
     1328    );
     1329
     1330    /**
     1331     * Default transport.
     1332     *
     1333     * @since 4.3.0
     1334     *
     1335     * @var string
     1336     */
     1337    public $transport = 'postMessage';
     1338
     1339    /**
     1340     * The term ID represented by this setting instance.
     1341     *
     1342     * A negative value represents a placeholder ID for a new menu not yet saved.
     1343     *
     1344     * @since 4.3.0
     1345     *
     1346     * @var int
     1347     */
     1348    public $term_id;
     1349
     1350    /**
     1351     * Previous (placeholder) term ID used before creating a new menu.
     1352     *
     1353     * This value will be exported to JS via the customize_save_response filter
     1354     * so that JavaScript can update the settings to refer to the newly-assigned
     1355     * term ID. This value is always negative to indicate it does not refer to
     1356     * a real term.
     1357     *
     1358     * @since 4.3.0
     1359     *
     1360     * @see WP_Customize_Nav_Menu_Setting::update()
     1361     * @see WP_Customize_Nav_Menu_Setting::amend_customize_save_response()
     1362     *
     1363     * @var int
     1364     */
     1365    public $previous_term_id;
     1366
     1367    /**
     1368     * Whether or not preview() was called.
     1369     *
     1370     * @since 4.3.0
     1371     *
     1372     * @var bool
     1373     */
     1374    protected $is_previewed = false;
     1375
     1376    /**
     1377     * Whether or not update() was called.
     1378     *
     1379     * @since 4.3.0
     1380     *
     1381     * @var bool
     1382     */
     1383    protected $is_updated = false;
     1384
     1385    /**
     1386     * Status for calling the update method, used in customize_save_response filter.
     1387     *
     1388     * When status is inserted, the placeholder term ID is stored in $previous_term_id.
     1389     * When status is error, the error is stored in $update_error.
     1390     *
     1391     * @since 4.3.0
     1392     *
     1393     * @see WP_Customize_Nav_Menu_Setting::update()
     1394     * @see WP_Customize_Nav_Menu_Setting::amend_customize_save_response()
     1395     *
     1396     * @var string updated|inserted|deleted|error
     1397     */
     1398    public $update_status;
     1399
     1400    /**
     1401     * Any error object returned by wp_update_nav_menu_object() when setting is updated.
     1402     *
     1403     * @since 4.3.0
     1404     *
     1405     * @see WP_Customize_Nav_Menu_Setting::update()
     1406     * @see WP_Customize_Nav_Menu_Setting::amend_customize_save_response()
     1407     *
     1408     * @var WP_Error
     1409     */
     1410    public $update_error;
     1411
     1412    /**
     1413     * Constructor.
     1414     *
     1415     * Any supplied $args override class property defaults.
     1416     *
     1417     * @since 4.3.0
     1418     *
     1419     * @param WP_Customize_Manager $manager Manager instance.
     1420     * @param string               $id      An specific ID of the setting. Can be a
     1421     *                                      theme mod or option name.
     1422     * @param array                $args    Optional. Setting arguments.
     1423     * @throws Exception If $id is not valid for this setting type.
     1424     */
     1425    public function __construct( WP_Customize_Manager $manager, $id, array $args = array() ) {
     1426        if ( empty( $manager->nav_menus ) ) {
     1427            throw new Exception( 'Expected WP_Customize_Manager::$nav_menus to be set.' );
     1428        }
     1429
     1430        if ( ! preg_match( self::ID_PATTERN, $id, $matches ) ) {
     1431            throw new Exception( "Illegal widget setting ID: $id" );
     1432        }
     1433
     1434        $this->term_id = intval( $matches['id'] );
     1435
     1436        parent::__construct( $manager, $id, $args );
     1437    }
     1438
     1439    /**
     1440     * Get the instance data for a given widget setting.
     1441     *
     1442     * @since 4.3.0
     1443     *
     1444     * @see wp_get_nav_menu_object()
     1445     *
     1446     * @return array
     1447     */
     1448    public function value() {
     1449        if ( $this->is_previewed && $this->_previewed_blog_id === get_current_blog_id() ) {
     1450            $undefined  = new stdClass(); // Symbol.
     1451            $post_value = $this->post_value( $undefined );
     1452
     1453            if ( $undefined === $post_value ) {
     1454                $value = $this->_original_value;
     1455            } else {
     1456                $value = $post_value;
     1457            }
     1458        } else {
     1459            $value = false;
     1460
     1461            // Note that a term_id of less than one indicates a nav_menu not yet inserted.
     1462            if ( $this->term_id > 0 ) {
     1463                $term = wp_get_nav_menu_object( $this->term_id );
     1464
     1465                if ( $term ) {
     1466                    $value = wp_array_slice_assoc( (array) $term, array_keys( $this->default ) );
     1467
     1468                    $nav_menu_options  = (array) get_option( 'nav_menu_options', array() );
     1469                    $value['auto_add'] = false;
     1470
     1471                    if ( isset( $nav_menu_options['auto_add'] ) && is_array( $nav_menu_options['auto_add'] ) ) {
     1472                        $value['auto_add'] = in_array( $term->term_id, $nav_menu_options['auto_add'] );
     1473                    }
     1474                }
     1475            }
     1476
     1477            if ( ! is_array( $value ) ) {
     1478                $value = $this->default;
     1479            }
     1480        }
     1481        return $value;
     1482    }
     1483
     1484    /**
     1485     * Handle previewing the setting.
     1486     *
     1487     * @since 4.3.0
     1488     *
     1489     * @see WP_Customize_Manager::post_value()
     1490     */
     1491    public function preview() {
     1492        if ( $this->is_previewed ) {
     1493            return;
     1494        }
     1495
     1496        $this->is_previewed       = true;
     1497        $this->_original_value    = $this->value();
     1498        $this->_previewed_blog_id = get_current_blog_id();
     1499
     1500        add_filter( 'wp_get_nav_menu_object', array( $this, 'filter_wp_get_nav_menu_object' ), 10, 2 );
     1501        add_filter( 'default_option_nav_menu_options', array( $this, 'filter_nav_menu_options' ) );
     1502        add_filter( 'option_nav_menu_options', array( $this, 'filter_nav_menu_options' ) );
     1503    }
     1504
     1505    /**
     1506     * Filter the wp_get_nav_menu_object() result to supply the previewed menu object.
     1507     *
     1508     * Requesting a nav_menu object by anything but ID is not supported.
     1509     *
     1510     * @since 4.3.0
     1511     *
     1512     * @see wp_get_nav_menu_object()
     1513     *
     1514     * @param object|null $menu_obj Object returned by wp_get_nav_menu_object().
     1515     * @param string      $menu_id  ID of the nav_menu term. Requests by slug or name will be ignored.
     1516     * @return object|null
     1517     */
     1518    function filter_wp_get_nav_menu_object( $menu_obj, $menu_id ) {
     1519        $ok = (
     1520            get_current_blog_id() === $this->_previewed_blog_id
     1521            &&
     1522            is_int( $menu_id )
     1523            &&
     1524            $menu_id === $this->term_id
     1525        );
     1526        if ( ! $ok ) {
     1527            return $menu_obj;
     1528        }
     1529
     1530        $setting_value = $this->value();
     1531
     1532        // Handle deleted menus.
     1533        if ( false === $setting_value ) {
     1534            return false;
     1535        }
     1536
     1537        // Handle sanitization failure by preventing short-circuiting.
     1538        if ( null === $setting_value ) {
     1539            return $menu_obj;
     1540        }
     1541
     1542        $menu_obj = (object) array_merge( array(
     1543                'term_id'          => $this->term_id,
     1544                'term_taxonomy_id' => $this->term_id,
     1545                'slug'             => sanitize_title( $setting_value['name'] ),
     1546                'count'            => 0,
     1547                'term_group'       => 0,
     1548                'taxonomy'         => self::TAXONOMY,
     1549                'filter'           => 'raw',
     1550            ), $setting_value );
     1551
     1552        return $menu_obj;
     1553    }
     1554
     1555    /**
     1556     * Filter the nav_menu_options option to include this menu's auto_add preference.
     1557     *
     1558     * @since 4.3.0
     1559     *
     1560     * @param array $nav_menu_options Nav menu options including auto_add.
     1561     * @return array
     1562     */
     1563    function filter_nav_menu_options( $nav_menu_options ) {
     1564        if ( $this->_previewed_blog_id !== get_current_blog_id() ) {
     1565            return $nav_menu_options;
     1566        }
     1567
     1568        $menu = $this->value();
     1569        $nav_menu_options = $this->filter_nav_menu_options_value(
     1570            $nav_menu_options,
     1571            $this->term_id,
     1572            false === $menu ? false : $menu['auto_add']
     1573        );
     1574
     1575        return $nav_menu_options;
     1576    }
     1577
     1578    /**
     1579     * Sanitize an input.
     1580     *
     1581     * Note that parent::sanitize() erroneously does wp_unslash() on $value, but
     1582     * we remove that in this override.
     1583     *
     1584     * @since 4.3.0
     1585     *
     1586     * @param array $value The value to sanitize.
     1587     * @return array|false|null Null if an input isn't valid. False if it is marked for deletion. Otherwise the sanitized value.
     1588     */
     1589    public function sanitize( $value ) {
     1590        // Menu is marked for deletion.
     1591        if ( false === $value ) {
     1592            return $value;
     1593        }
     1594
     1595        // Invalid.
     1596        if ( ! is_array( $value ) ) {
     1597            return null;
     1598        }
     1599
     1600        $default = array(
     1601            'name'        => '',
     1602            'description' => '',
     1603            'parent'      => 0,
     1604            'auto_add'    => false,
     1605        );
     1606        $value = array_merge( $default, $value );
     1607        $value = wp_array_slice_assoc( $value, array_keys( $default ) );
     1608
     1609        $value['name']        = trim( esc_html( $value['name'] ) ); // This sanitization code is used in wp-admin/nav-menus.php.
     1610        $value['description'] = sanitize_text_field( $value['description'] );
     1611        $value['parent']      = max( 0, intval( $value['parent'] ) );
     1612        $value['auto_add']    = ! empty( $value['auto_add'] );
     1613
     1614        /** This filter is documented in wp-includes/class-wp-customize-setting.php */
     1615        return apply_filters( "customize_sanitize_{$this->id}", $value, $this );
     1616    }
     1617
     1618    /**
     1619     * Create/update the nav_menu term for this setting.
     1620     *
     1621     * Any created menus will have their assigned term IDs exported to the client
     1622     * via the customize_save_response filter. Likewise, any errors will be exported
     1623     * to the client via the customize_save_response() filter.
     1624     *
     1625     * To delete a menu, the client can send false as the value.
     1626     *
     1627     * @since 4.3.0
     1628     *
     1629     * @see wp_update_nav_menu_object()
     1630     *
     1631     * @param array|false $value {
     1632     *     The value to update. Note that slug cannot be updated via wp_update_nav_menu_object().
     1633     *     If false, then the menu will be deleted entirely.
     1634     *
     1635     *     @type string $name        The name of the menu to save.
     1636     *     @type string $description The term description. Default empty string.
     1637     *     @type int    $parent      The id of the parent term. Default 0.
     1638     *     @type bool   $auto_add    Whether pages will auto_add to this menu. Default false.
     1639     * }
     1640     * @return void
     1641     */
     1642    protected function update( $value ) {
     1643        if ( $this->is_updated ) {
     1644            return;
     1645        }
     1646
     1647        $this->is_updated = true;
     1648        $is_placeholder   = ( $this->term_id < 0 );
     1649        $is_delete        = ( false === $value );
     1650
     1651        add_filter( 'customize_save_response', array( $this, 'amend_customize_save_response' ) );
     1652
     1653        $auto_add = null;
     1654        if ( $is_delete ) {
     1655            // If the current setting term is a placeholder, a delete request is a no-op.
     1656            if ( $is_placeholder ) {
     1657                $this->update_status = 'deleted';
     1658            } else {
     1659                $r = wp_delete_nav_menu( $this->term_id );
     1660
     1661                if ( is_wp_error( $r ) ) {
     1662                    $this->update_status = 'error';
     1663                    $this->update_error  = $r;
     1664                } else {
     1665                    $this->update_status = 'deleted';
     1666                    $auto_add = false;
     1667                }
     1668            }
     1669        } else {
     1670            // Insert or update menu.
     1671            $menu_data = wp_array_slice_assoc( $value, array( 'description', 'parent' ) );
     1672            if ( isset( $value['name'] ) ) {
     1673                $menu_data['menu-name'] = $value['name'];
     1674            }
     1675
     1676            $r = wp_update_nav_menu_object( $is_placeholder ? 0 : $this->term_id, $menu_data );
     1677            if ( is_wp_error( $r ) ) {
     1678                $this->update_status = 'error';
     1679                $this->update_error  = $r;
     1680            } else {
     1681                if ( $is_placeholder ) {
     1682                    $this->previous_term_id = $this->term_id;
     1683                    $this->term_id          = $r;
     1684                    $this->update_status    = 'inserted';
     1685                } else {
     1686                    $this->update_status = 'updated';
     1687                }
     1688
     1689                $auto_add = $value['auto_add'];
     1690            }
     1691        }
     1692
     1693        if ( null !== $auto_add ) {
     1694            $nav_menu_options = $this->filter_nav_menu_options_value(
     1695                (array) get_option( 'nav_menu_options', array() ),
     1696                $this->term_id,
     1697                $auto_add
     1698            );
     1699            update_option( 'nav_menu_options', $nav_menu_options );
     1700        }
     1701
     1702        // Make sure that new menus assigned to nav menu locations use their new IDs.
     1703        if ( 'inserted' === $this->update_status ) {
     1704            foreach ( $this->manager->settings() as $setting ) {
     1705                if ( ! preg_match( '/^nav_menu_locations\[/', $setting->id ) ) {
     1706                    continue;
     1707                }
     1708
     1709                $post_value = $setting->post_value( null );
     1710                if ( ! is_null( $post_value ) && $this->previous_term_id === intval( $post_value ) ) {
     1711                    $this->manager->set_post_value( $setting->id, $this->term_id );
     1712                    $setting->save();
     1713                }
     1714            }
     1715        }
     1716    }
     1717
     1718    /**
     1719     * Update a nav_menu_options array.
     1720     *
     1721     * @since 4.3.0
     1722     *
     1723     * @see WP_Customize_Nav_Menu_Setting::filter_nav_menu_options()
     1724     * @see WP_Customize_Nav_Menu_Setting::update()
     1725     *
     1726     * @param array $nav_menu_options Array as returned by get_option( 'nav_menu_options' ).
     1727     * @param int   $menu_id          The term ID for the given menu.
     1728     * @param bool  $auto_add         Whether to auto-add or not.
     1729     * @return array
     1730     */
     1731    protected function filter_nav_menu_options_value( $nav_menu_options, $menu_id, $auto_add ) {
     1732        $nav_menu_options = (array) $nav_menu_options;
     1733        if ( ! isset( $nav_menu_options['auto_add'] ) ) {
     1734            $nav_menu_options['auto_add'] = array();
     1735        }
     1736
     1737        $i = array_search( $menu_id, $nav_menu_options['auto_add'] );
     1738        if ( $auto_add && false === $i ) {
     1739            array_push( $nav_menu_options['auto_add'], $this->term_id );
     1740        } else if ( ! $auto_add && false !== $i ) {
     1741            array_splice( $nav_menu_options['auto_add'], $i, 1 );
     1742        }
     1743
     1744        return $nav_menu_options;
     1745    }
     1746
     1747    /**
     1748     * Export data for the JS client.
     1749     *
     1750     * @since 4.3.0
     1751     *
     1752     * @see WP_Customize_Nav_Menu_Setting::update()
     1753     *
     1754     * @param array $data Additional information passed back to the 'saved' event on `wp.customize`.
     1755     * @return array
     1756     */
     1757    function amend_customize_save_response( $data ) {
     1758        if ( ! isset( $data['nav_menu_updates'] ) ) {
     1759            $data['nav_menu_updates'] = array();
     1760        }
     1761
     1762        $data['nav_menu_updates'][] = array(
     1763            'term_id'          => $this->term_id,
     1764            'previous_term_id' => $this->previous_term_id,
     1765            'error'            => $this->update_error ? $this->update_error->get_error_code() : null,
     1766            'status'           => $this->update_status,
     1767        );
     1768
     1769        return $data;
     1770    }
     1771}
Note: See TracChangeset for help on using the changeset viewer.