Make WordPress Core

Changeset 35383


Ignore:
Timestamp:
10/24/2015 06:10:17 PM (9 years ago)
Author:
wonderboymusic
Message:

Customize: move WP_Customize_Setting subclasses to wp-includes/customize, they load in the exact same place.

See #34432.

Location:
trunk/src/wp-includes
Files:
1 added
1 edited
5 copied

Legend:

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

    r35308 r35383  
    770770}
    771771
    772 /**
    773  * A setting that is used to filter a value, but will not save the results.
    774  *
    775  * Results should be properly handled using another setting or callback.
    776  *
    777  * @since 3.4.0
    778  *
    779  * @see WP_Customize_Setting
    780  */
    781 class WP_Customize_Filter_Setting extends WP_Customize_Setting {
    782 
    783     /**
    784      * @since 3.4.0
    785      */
    786     public function update( $value ) {}
    787 }
    788 
    789 /**
    790  * A setting that is used to filter a value, but will not save the results.
    791  *
    792  * Results should be properly handled using another setting or callback.
    793  *
    794  * @since 3.4.0
    795  *
    796  * @see WP_Customize_Setting
    797  */
    798 final class WP_Customize_Header_Image_Setting extends WP_Customize_Setting {
    799     public $id = 'header_image_data';
    800 
    801     /**
    802      * @since 3.4.0
    803      *
    804      * @global Custom_Image_Header $custom_image_header
    805      *
    806      * @param $value
    807      */
    808     public function update( $value ) {
    809         global $custom_image_header;
    810 
    811         // If the value doesn't exist (removed or random),
    812         // use the header_image value.
    813         if ( ! $value )
    814             $value = $this->manager->get_setting('header_image')->post_value();
    815 
    816         if ( is_array( $value ) && isset( $value['choice'] ) )
    817             $custom_image_header->set_header_image( $value['choice'] );
    818         else
    819             $custom_image_header->set_header_image( $value );
    820     }
    821 }
    822 
    823 /**
    824  * Customizer Background Image Setting class.
    825  *
    826  * @since 3.4.0
    827  *
    828  * @see WP_Customize_Setting
    829  */
    830 final class WP_Customize_Background_Image_Setting extends WP_Customize_Setting {
    831     public $id = 'background_image_thumb';
    832 
    833     /**
    834      * @since 3.4.0
    835      *
    836      * @param $value
    837      */
    838     public function update( $value ) {
    839         remove_theme_mod( 'background_image_thumb' );
    840     }
    841 }
    842 
    843 /**
    844  * Customize Setting to represent a nav_menu.
    845  *
    846  * Subclass of WP_Customize_Setting to represent a nav_menu taxonomy term, and
    847  * the IDs for the nav_menu_items associated with the nav menu.
    848  *
    849  * @since 4.3.0
    850  *
    851  * @see WP_Customize_Setting
    852  */
    853 class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting {
    854 
    855     const ID_PATTERN = '/^nav_menu_item\[(?P<id>-?\d+)\]$/';
    856 
    857     const POST_TYPE = 'nav_menu_item';
    858 
    859     const TYPE = 'nav_menu_item';
    860 
    861     /**
    862      * Setting type.
    863      *
    864      * @since 4.3.0
    865      * @access public
    866      * @var string
    867      */
    868     public $type = self::TYPE;
    869 
    870     /**
    871      * Default setting value.
    872      *
    873      * @since 4.3.0
    874      * @access public
    875      * @var array
    876      *
    877      * @see wp_setup_nav_menu_item()
    878      */
    879     public $default = array(
    880         // The $menu_item_data for wp_update_nav_menu_item().
    881         'object_id'        => 0,
    882         'object'           => '', // Taxonomy name.
    883         'menu_item_parent' => 0, // A.K.A. menu-item-parent-id; note that post_parent is different, and not included.
    884         'position'         => 0, // A.K.A. menu_order.
    885         'type'             => 'custom', // Note that type_label is not included here.
    886         'title'            => '',
    887         'url'              => '',
    888         'target'           => '',
    889         'attr_title'       => '',
    890         'description'      => '',
    891         'classes'          => '',
    892         'xfn'              => '',
    893         'status'           => 'publish',
    894         'original_title'   => '',
    895         'nav_menu_term_id' => 0, // This will be supplied as the $menu_id arg for wp_update_nav_menu_item().
    896         '_invalid'         => false,
    897     );
    898 
    899     /**
    900      * Default transport.
    901      *
    902      * @since 4.3.0
    903      * @access public
    904      * @var string
    905      */
    906     public $transport = 'postMessage';
    907 
    908     /**
    909      * The post ID represented by this setting instance. This is the db_id.
    910      *
    911      * A negative value represents a placeholder ID for a new menu not yet saved.
    912      *
    913      * @since 4.3.0
    914      * @access public
    915      * @var int
    916      */
    917     public $post_id;
    918 
    919     /**
    920      * Storage of pre-setup menu item to prevent wasted calls to wp_setup_nav_menu_item().
    921      *
    922      * @since 4.3.0
    923      * @access protected
    924      * @var array
    925      */
    926     protected $value;
    927 
    928     /**
    929      * Previous (placeholder) post ID used before creating a new menu item.
    930      *
    931      * This value will be exported to JS via the customize_save_response filter
    932      * so that JavaScript can update the settings to refer to the newly-assigned
    933      * post ID. This value is always negative to indicate it does not refer to
    934      * a real post.
    935      *
    936      * @since 4.3.0
    937      * @access public
    938      * @var int
    939      *
    940      * @see WP_Customize_Nav_Menu_Item_Setting::update()
    941      * @see WP_Customize_Nav_Menu_Item_Setting::amend_customize_save_response()
    942      */
    943     public $previous_post_id;
    944 
    945     /**
    946      * When previewing or updating a menu item, this stores the previous nav_menu_term_id
    947      * which ensures that we can apply the proper filters.
    948      *
    949      * @since 4.3.0
    950      * @access public
    951      * @var int
    952      */
    953     public $original_nav_menu_term_id;
    954 
    955     /**
    956      * Whether or not preview() was called.
    957      *
    958      * @since 4.3.0
    959      * @access protected
    960      * @var bool
    961      */
    962     protected $is_previewed = false;
    963 
    964     /**
    965      * Whether or not update() was called.
    966      *
    967      * @since 4.3.0
    968      * @access protected
    969      * @var bool
    970      */
    971     protected $is_updated = false;
    972 
    973     /**
    974      * Status for calling the update method, used in customize_save_response filter.
    975      *
    976      * When status is inserted, the placeholder post ID is stored in $previous_post_id.
    977      * When status is error, the error is stored in $update_error.
    978      *
    979      * @since 4.3.0
    980      * @access public
    981      * @var string updated|inserted|deleted|error
    982      *
    983      * @see WP_Customize_Nav_Menu_Item_Setting::update()
    984      * @see WP_Customize_Nav_Menu_Item_Setting::amend_customize_save_response()
    985      */
    986     public $update_status;
    987 
    988     /**
    989      * Any error object returned by wp_update_nav_menu_item() when setting is updated.
    990      *
    991      * @since 4.3.0
    992      * @access public
    993      * @var WP_Error
    994      *
    995      * @see WP_Customize_Nav_Menu_Item_Setting::update()
    996      * @see WP_Customize_Nav_Menu_Item_Setting::amend_customize_save_response()
    997      */
    998     public $update_error;
    999 
    1000     /**
    1001      * Constructor.
    1002      *
    1003      * Any supplied $args override class property defaults.
    1004      *
    1005      * @since 4.3.0
    1006      * @access public
    1007      *
    1008      * @param WP_Customize_Manager $manager Bootstrap Customizer instance.
    1009      * @param string               $id      An specific ID of the setting. Can be a
    1010      *                                      theme mod or option name.
    1011      * @param array                $args    Optional. Setting arguments.
    1012      *
    1013      * @throws Exception If $id is not valid for this setting type.
    1014      */
    1015     public function __construct( WP_Customize_Manager $manager, $id, array $args = array() ) {
    1016         if ( empty( $manager->nav_menus ) ) {
    1017             throw new Exception( 'Expected WP_Customize_Manager::$nav_menus to be set.' );
    1018         }
    1019 
    1020         if ( ! preg_match( self::ID_PATTERN, $id, $matches ) ) {
    1021             throw new Exception( "Illegal widget setting ID: $id" );
    1022         }
    1023 
    1024         $this->post_id = intval( $matches['id'] );
    1025         add_action( 'wp_update_nav_menu_item', array( $this, 'flush_cached_value' ), 10, 2 );
    1026 
    1027         parent::__construct( $manager, $id, $args );
    1028 
    1029         // Ensure that an initially-supplied value is valid.
    1030         if ( isset( $this->value ) ) {
    1031             $this->populate_value();
    1032             foreach ( array_diff( array_keys( $this->default ), array_keys( $this->value ) ) as $missing ) {
    1033                 throw new Exception( "Supplied nav_menu_item value missing property: $missing" );
    1034             }
    1035         }
    1036 
    1037     }
    1038 
    1039     /**
    1040      * Clear the cached value when this nav menu item is updated.
    1041      *
    1042      * @since 4.3.0
    1043      * @access public
    1044      *
    1045      * @param int $menu_id       The term ID for the menu.
    1046      * @param int $menu_item_id  The post ID for the menu item.
    1047      */
    1048     public function flush_cached_value( $menu_id, $menu_item_id ) {
    1049         unset( $menu_id );
    1050         if ( $menu_item_id === $this->post_id ) {
    1051             $this->value = null;
    1052         }
    1053     }
    1054 
    1055     /**
    1056      * Get the instance data for a given nav_menu_item setting.
    1057      *
    1058      * @since 4.3.0
    1059      * @access public
    1060      *
    1061      * @see wp_setup_nav_menu_item()
    1062      *
    1063      * @return array|false Instance data array, or false if the item is marked for deletion.
    1064      */
    1065     public function value() {
    1066         if ( $this->is_previewed && $this->_previewed_blog_id === get_current_blog_id() ) {
    1067             $undefined  = new stdClass(); // Symbol.
    1068             $post_value = $this->post_value( $undefined );
    1069 
    1070             if ( $undefined === $post_value ) {
    1071                 $value = $this->_original_value;
    1072             } else {
    1073                 $value = $post_value;
    1074             }
    1075         } else if ( isset( $this->value ) ) {
    1076             $value = $this->value;
    1077         } else {
    1078             $value = false;
    1079 
    1080             // Note that a ID of less than one indicates a nav_menu not yet inserted.
    1081             if ( $this->post_id > 0 ) {
    1082                 $post = get_post( $this->post_id );
    1083                 if ( $post && self::POST_TYPE === $post->post_type ) {
    1084                     $value = (array) wp_setup_nav_menu_item( $post );
    1085                 }
    1086             }
    1087 
    1088             if ( ! is_array( $value ) ) {
    1089                 $value = $this->default;
    1090             }
    1091 
    1092             // Cache the value for future calls to avoid having to re-call wp_setup_nav_menu_item().
    1093             $this->value = $value;
    1094             $this->populate_value();
    1095             $value = $this->value;
    1096         }
    1097 
    1098         return $value;
    1099     }
    1100 
    1101     /**
    1102      * Ensure that the value is fully populated with the necessary properties.
    1103      *
    1104      * Translates some properties added by wp_setup_nav_menu_item() and removes others.
    1105      *
    1106      * @since 4.3.0
    1107      * @access protected
    1108      *
    1109      * @see WP_Customize_Nav_Menu_Item_Setting::value()
    1110      */
    1111     protected function populate_value() {
    1112         if ( ! is_array( $this->value ) ) {
    1113             return;
    1114         }
    1115 
    1116         if ( isset( $this->value['menu_order'] ) ) {
    1117             $this->value['position'] = $this->value['menu_order'];
    1118             unset( $this->value['menu_order'] );
    1119         }
    1120         if ( isset( $this->value['post_status'] ) ) {
    1121             $this->value['status'] = $this->value['post_status'];
    1122             unset( $this->value['post_status'] );
    1123         }
    1124 
    1125         if ( ! isset( $this->value['original_title'] ) ) {
    1126             $original_title = '';
    1127             if ( 'post_type' === $this->value['type'] ) {
    1128                 $original_title = get_the_title( $this->value['object_id'] );
    1129             } elseif ( 'taxonomy' === $this->value['type'] ) {
    1130                 $original_title = get_term_field( 'name', $this->value['object_id'], $this->value['object'], 'raw' );
    1131                 if ( is_wp_error( $original_title ) ) {
    1132                     $original_title = '';
    1133                 }
    1134             }
    1135             $this->value['original_title'] = html_entity_decode( $original_title, ENT_QUOTES, get_bloginfo( 'charset' ) );
    1136         }
    1137 
    1138         if ( ! isset( $this->value['nav_menu_term_id'] ) && $this->post_id > 0 ) {
    1139             $menus = wp_get_post_terms( $this->post_id, WP_Customize_Nav_Menu_Setting::TAXONOMY, array(
    1140                 'fields' => 'ids',
    1141             ) );
    1142             if ( ! empty( $menus ) ) {
    1143                 $this->value['nav_menu_term_id'] = array_shift( $menus );
    1144             } else {
    1145                 $this->value['nav_menu_term_id'] = 0;
    1146             }
    1147         }
    1148 
    1149         foreach ( array( 'object_id', 'menu_item_parent', 'nav_menu_term_id' ) as $key ) {
    1150             if ( ! is_int( $this->value[ $key ] ) ) {
    1151                 $this->value[ $key ] = intval( $this->value[ $key ] );
    1152             }
    1153         }
    1154         foreach ( array( 'classes', 'xfn' ) as $key ) {
    1155             if ( is_array( $this->value[ $key ] ) ) {
    1156                 $this->value[ $key ] = implode( ' ', $this->value[ $key ] );
    1157             }
    1158         }
    1159 
    1160         if ( ! isset( $this->value['_invalid'] ) ) {
    1161             $this->value['_invalid'] = (
    1162                 ( 'post_type' === $this->value['type'] && ! post_type_exists( $this->value['object'] ) )
    1163                 ||
    1164                 ( 'taxonomy' === $this->value['type'] && ! taxonomy_exists( $this->value['object'] ) )
    1165             );
    1166         }
    1167 
    1168         // Remove remaining properties available on a setup nav_menu_item post object which aren't relevant to the setting value.
    1169         $irrelevant_properties = array(
    1170             'ID',
    1171             'comment_count',
    1172             'comment_status',
    1173             'db_id',
    1174             'filter',
    1175             'guid',
    1176             'ping_status',
    1177             'pinged',
    1178             'post_author',
    1179             'post_content',
    1180             'post_content_filtered',
    1181             'post_date',
    1182             'post_date_gmt',
    1183             'post_excerpt',
    1184             'post_mime_type',
    1185             'post_modified',
    1186             'post_modified_gmt',
    1187             'post_name',
    1188             'post_parent',
    1189             'post_password',
    1190             'post_title',
    1191             'post_type',
    1192             'to_ping',
    1193         );
    1194         foreach ( $irrelevant_properties as $property ) {
    1195             unset( $this->value[ $property ] );
    1196         }
    1197     }
    1198 
    1199     /**
    1200      * Handle previewing the setting.
    1201      *
    1202      * @since 4.3.0
    1203      * @since 4.4.0 Added boolean return value.
    1204      * @access public
    1205      *
    1206      * @see WP_Customize_Manager::post_value()
    1207      *
    1208      * @return bool False if method short-circuited due to no-op.
    1209      */
    1210     public function preview() {
    1211         if ( $this->is_previewed ) {
    1212             return false;
    1213         }
    1214 
    1215         $undefined = new stdClass();
    1216         $is_placeholder = ( $this->post_id < 0 );
    1217         $is_dirty = ( $undefined !== $this->post_value( $undefined ) );
    1218         if ( ! $is_placeholder && ! $is_dirty ) {
    1219             return false;
    1220         }
    1221 
    1222         $this->is_previewed              = true;
    1223         $this->_original_value           = $this->value();
    1224         $this->original_nav_menu_term_id = $this->_original_value['nav_menu_term_id'];
    1225         $this->_previewed_blog_id        = get_current_blog_id();
    1226 
    1227         add_filter( 'wp_get_nav_menu_items', array( $this, 'filter_wp_get_nav_menu_items' ), 10, 3 );
    1228 
    1229         $sort_callback = array( __CLASS__, 'sort_wp_get_nav_menu_items' );
    1230         if ( ! has_filter( 'wp_get_nav_menu_items', $sort_callback ) ) {
    1231             add_filter( 'wp_get_nav_menu_items', array( __CLASS__, 'sort_wp_get_nav_menu_items' ), 1000, 3 );
    1232         }
    1233 
    1234         // @todo Add get_post_metadata filters for plugins to add their data.
    1235 
    1236         return true;
    1237     }
    1238 
    1239     /**
    1240      * Filter the wp_get_nav_menu_items() result to supply the previewed menu items.
    1241      *
    1242      * @since 4.3.0
    1243      * @access public
    1244      *
    1245      * @see wp_get_nav_menu_items()
    1246      *
    1247      * @param array  $items An array of menu item post objects.
    1248      * @param object $menu  The menu object.
    1249      * @param array  $args  An array of arguments used to retrieve menu item objects.
    1250      * @return array Array of menu items,
    1251      */
    1252     public function filter_wp_get_nav_menu_items( $items, $menu, $args ) {
    1253         $this_item = $this->value();
    1254         $current_nav_menu_term_id = $this_item['nav_menu_term_id'];
    1255         unset( $this_item['nav_menu_term_id'] );
    1256 
    1257         $should_filter = (
    1258             $menu->term_id === $this->original_nav_menu_term_id
    1259             ||
    1260             $menu->term_id === $current_nav_menu_term_id
    1261         );
    1262         if ( ! $should_filter ) {
    1263             return $items;
    1264         }
    1265 
    1266         // Handle deleted menu item, or menu item moved to another menu.
    1267         $should_remove = (
    1268             false === $this_item
    1269             ||
    1270             true === $this_item['_invalid']
    1271             ||
    1272             (
    1273                 $this->original_nav_menu_term_id === $menu->term_id
    1274                 &&
    1275                 $current_nav_menu_term_id !== $this->original_nav_menu_term_id
    1276             )
    1277         );
    1278         if ( $should_remove ) {
    1279             $filtered_items = array();
    1280             foreach ( $items as $item ) {
    1281                 if ( $item->db_id !== $this->post_id ) {
    1282                     $filtered_items[] = $item;
    1283                 }
    1284             }
    1285             return $filtered_items;
    1286         }
    1287 
    1288         $mutated = false;
    1289         $should_update = (
    1290             is_array( $this_item )
    1291             &&
    1292             $current_nav_menu_term_id === $menu->term_id
    1293         );
    1294         if ( $should_update ) {
    1295             foreach ( $items as $item ) {
    1296                 if ( $item->db_id === $this->post_id ) {
    1297                     foreach ( get_object_vars( $this->value_as_wp_post_nav_menu_item() ) as $key => $value ) {
    1298                         $item->$key = $value;
    1299                     }
    1300                     $mutated = true;
    1301                 }
    1302             }
    1303 
    1304             // Not found so we have to append it..
    1305             if ( ! $mutated ) {
    1306                 $items[] = $this->value_as_wp_post_nav_menu_item();
    1307             }
    1308         }
    1309 
    1310         return $items;
    1311     }
    1312 
    1313     /**
    1314      * Re-apply the tail logic also applied on $items by wp_get_nav_menu_items().
    1315      *
    1316      * @since 4.3.0
    1317      * @access public
    1318      * @static
    1319      *
    1320      * @see wp_get_nav_menu_items()
    1321      *
    1322      * @param array  $items An array of menu item post objects.
    1323      * @param object $menu  The menu object.
    1324      * @param array  $args  An array of arguments used to retrieve menu item objects.
    1325      * @return array Array of menu items,
    1326      */
    1327     public static function sort_wp_get_nav_menu_items( $items, $menu, $args ) {
    1328         // @todo We should probably re-apply some constraints imposed by $args.
    1329         unset( $args['include'] );
    1330 
    1331         // Remove invalid items only in frontend.
    1332         if ( ! is_admin() ) {
    1333             $items = array_filter( $items, '_is_valid_nav_menu_item' );
    1334         }
    1335 
    1336         if ( ARRAY_A === $args['output'] ) {
    1337             $GLOBALS['_menu_item_sort_prop'] = $args['output_key'];
    1338             usort( $items, '_sort_nav_menu_items' );
    1339             $i = 1;
    1340 
    1341             foreach ( $items as $k => $item ) {
    1342                 $items[ $k ]->{$args['output_key']} = $i++;
    1343             }
    1344         }
    1345 
    1346         return $items;
    1347     }
    1348 
    1349     /**
    1350      * Get the value emulated into a WP_Post and set up as a nav_menu_item.
    1351      *
    1352      * @since 4.3.0
    1353      * @access public
    1354      *
    1355      * @return WP_Post With wp_setup_nav_menu_item() applied.
    1356      */
    1357     public function value_as_wp_post_nav_menu_item() {
    1358         $item = (object) $this->value();
    1359         unset( $item->nav_menu_term_id );
    1360 
    1361         $item->post_status = $item->status;
    1362         unset( $item->status );
    1363 
    1364         $item->post_type = 'nav_menu_item';
    1365         $item->menu_order = $item->position;
    1366         unset( $item->position );
    1367 
    1368         if ( $item->title ) {
    1369             $item->post_title = $item->title;
    1370         }
    1371 
    1372         $item->ID = $this->post_id;
    1373         $item->db_id = $this->post_id;
    1374         $post = new WP_Post( (object) $item );
    1375 
    1376         if ( empty( $post->post_author ) ) {
    1377             $post->post_author = get_current_user_id();
    1378         }
    1379 
    1380         if ( ! isset( $post->type_label ) ) {
    1381             if ( 'post_type' === $post->type ) {
    1382                 $object = get_post_type_object( $post->object );
    1383                 if ( $object ) {
    1384                     $post->type_label = $object->labels->singular_name;
    1385                 } else {
    1386                     $post->type_label = $post->object;
    1387                 }
    1388             } elseif ( 'taxonomy' == $post->type ) {
    1389                 $object = get_taxonomy( $post->object );
    1390                 if ( $object ) {
    1391                     $post->type_label = $object->labels->singular_name;
    1392                 } else {
    1393                     $post->type_label = $post->object;
    1394                 }
    1395             } else {
    1396                 $post->type_label = __( 'Custom Link' );
    1397             }
    1398         }
    1399 
    1400         return $post;
    1401     }
    1402 
    1403     /**
    1404      * Sanitize an input.
    1405      *
    1406      * Note that parent::sanitize() erroneously does wp_unslash() on $value, but
    1407      * we remove that in this override.
    1408      *
    1409      * @since 4.3.0
    1410      * @access public
    1411      *
    1412      * @param array $menu_item_value The value to sanitize.
    1413      * @return array|false|null Null if an input isn't valid. False if it is marked for deletion.
    1414      *                          Otherwise the sanitized value.
    1415      */
    1416     public function sanitize( $menu_item_value ) {
    1417         // Menu is marked for deletion.
    1418         if ( false === $menu_item_value ) {
    1419             return $menu_item_value;
    1420         }
    1421 
    1422         // Invalid.
    1423         if ( ! is_array( $menu_item_value ) ) {
    1424             return null;
    1425         }
    1426 
    1427         $default = array(
    1428             'object_id'        => 0,
    1429             'object'           => '',
    1430             'menu_item_parent' => 0,
    1431             'position'         => 0,
    1432             'type'             => 'custom',
    1433             'title'            => '',
    1434             'url'              => '',
    1435             'target'           => '',
    1436             'attr_title'       => '',
    1437             'description'      => '',
    1438             'classes'          => '',
    1439             'xfn'              => '',
    1440             'status'           => 'publish',
    1441             'original_title'   => '',
    1442             'nav_menu_term_id' => 0,
    1443             '_invalid'         => false,
    1444         );
    1445         $menu_item_value = array_merge( $default, $menu_item_value );
    1446         $menu_item_value = wp_array_slice_assoc( $menu_item_value, array_keys( $default ) );
    1447         $menu_item_value['position'] = max( 0, intval( $menu_item_value['position'] ) );
    1448 
    1449         foreach ( array( 'object_id', 'menu_item_parent', 'nav_menu_term_id' ) as $key ) {
    1450             // Note we need to allow negative-integer IDs for previewed objects not inserted yet.
    1451             $menu_item_value[ $key ] = intval( $menu_item_value[ $key ] );
    1452         }
    1453 
    1454         foreach ( array( 'type', 'object', 'target' ) as $key ) {
    1455             $menu_item_value[ $key ] = sanitize_key( $menu_item_value[ $key ] );
    1456         }
    1457 
    1458         foreach ( array( 'xfn', 'classes' ) as $key ) {
    1459             $value = $menu_item_value[ $key ];
    1460             if ( ! is_array( $value ) ) {
    1461                 $value = explode( ' ', $value );
    1462             }
    1463             $menu_item_value[ $key ] = implode( ' ', array_map( 'sanitize_html_class', $value ) );
    1464         }
    1465 
    1466         foreach ( array( 'title', 'attr_title', 'description', 'original_title' ) as $key ) {
    1467             // @todo Should esc_attr() the attr_title as well?
    1468             $menu_item_value[ $key ] = sanitize_text_field( $menu_item_value[ $key ] );
    1469         }
    1470 
    1471         $menu_item_value['url'] = esc_url_raw( $menu_item_value['url'] );
    1472         if ( ! get_post_status_object( $menu_item_value['status'] ) ) {
    1473             $menu_item_value['status'] = 'publish';
    1474         }
    1475 
    1476         $menu_item_value['_invalid'] = (bool) $menu_item_value['_invalid'];
    1477 
    1478         /** This filter is documented in wp-includes/class-wp-customize-setting.php */
    1479         return apply_filters( "customize_sanitize_{$this->id}", $menu_item_value, $this );
    1480     }
    1481 
    1482     /**
    1483      * Create/update the nav_menu_item post for this setting.
    1484      *
    1485      * Any created menu items will have their assigned post IDs exported to the client
    1486      * via the customize_save_response filter. Likewise, any errors will be exported
    1487      * to the client via the customize_save_response() filter.
    1488      *
    1489      * To delete a menu, the client can send false as the value.
    1490      *
    1491      * @since 4.3.0
    1492      * @access protected
    1493      *
    1494      * @see wp_update_nav_menu_item()
    1495      *
    1496      * @param array|false $value The menu item array to update. If false, then the menu item will be deleted
    1497      *                           entirely. See WP_Customize_Nav_Menu_Item_Setting::$default for what the value
    1498      *                           should consist of.
    1499      * @return null|void
    1500      */
    1501     protected function update( $value ) {
    1502         if ( $this->is_updated ) {
    1503             return;
    1504         }
    1505 
    1506         $this->is_updated = true;
    1507         $is_placeholder   = ( $this->post_id < 0 );
    1508         $is_delete        = ( false === $value );
    1509 
    1510         // Update the cached value.
    1511         $this->value = $value;
    1512 
    1513         add_filter( 'customize_save_response', array( $this, 'amend_customize_save_response' ) );
    1514 
    1515         if ( $is_delete ) {
    1516             // If the current setting post is a placeholder, a delete request is a no-op.
    1517             if ( $is_placeholder ) {
    1518                 $this->update_status = 'deleted';
    1519             } else {
    1520                 $r = wp_delete_post( $this->post_id, true );
    1521 
    1522                 if ( false === $r ) {
    1523                     $this->update_error  = new WP_Error( 'delete_failure' );
    1524                     $this->update_status = 'error';
    1525                 } else {
    1526                     $this->update_status = 'deleted';
    1527                 }
    1528                 // @todo send back the IDs for all associated nav menu items deleted, so these settings (and controls) can be removed from Customizer?
    1529             }
    1530         } else {
    1531 
    1532             // Handle saving menu items for menus that are being newly-created.
    1533             if ( $value['nav_menu_term_id'] < 0 ) {
    1534                 $nav_menu_setting_id = sprintf( 'nav_menu[%s]', $value['nav_menu_term_id'] );
    1535                 $nav_menu_setting    = $this->manager->get_setting( $nav_menu_setting_id );
    1536 
    1537                 if ( ! $nav_menu_setting || ! ( $nav_menu_setting instanceof WP_Customize_Nav_Menu_Setting ) ) {
    1538                     $this->update_status = 'error';
    1539                     $this->update_error  = new WP_Error( 'unexpected_nav_menu_setting' );
    1540                     return;
    1541                 }
    1542 
    1543                 if ( false === $nav_menu_setting->save() ) {
    1544                     $this->update_status = 'error';
    1545                     $this->update_error  = new WP_Error( 'nav_menu_setting_failure' );
    1546                     return;
    1547                 }
    1548 
    1549                 if ( $nav_menu_setting->previous_term_id !== intval( $value['nav_menu_term_id'] ) ) {
    1550                     $this->update_status = 'error';
    1551                     $this->update_error  = new WP_Error( 'unexpected_previous_term_id' );
    1552                     return;
    1553                 }
    1554 
    1555                 $value['nav_menu_term_id'] = $nav_menu_setting->term_id;
    1556             }
    1557 
    1558             // Handle saving a nav menu item that is a child of a nav menu item being newly-created.
    1559             if ( $value['menu_item_parent'] < 0 ) {
    1560                 $parent_nav_menu_item_setting_id = sprintf( 'nav_menu_item[%s]', $value['menu_item_parent'] );
    1561                 $parent_nav_menu_item_setting    = $this->manager->get_setting( $parent_nav_menu_item_setting_id );
    1562 
    1563                 if ( ! $parent_nav_menu_item_setting || ! ( $parent_nav_menu_item_setting instanceof WP_Customize_Nav_Menu_Item_Setting ) ) {
    1564                     $this->update_status = 'error';
    1565                     $this->update_error  = new WP_Error( 'unexpected_nav_menu_item_setting' );
    1566                     return;
    1567                 }
    1568 
    1569                 if ( false === $parent_nav_menu_item_setting->save() ) {
    1570                     $this->update_status = 'error';
    1571                     $this->update_error  = new WP_Error( 'nav_menu_item_setting_failure' );
    1572                     return;
    1573                 }
    1574 
    1575                 if ( $parent_nav_menu_item_setting->previous_post_id !== intval( $value['menu_item_parent'] ) ) {
    1576                     $this->update_status = 'error';
    1577                     $this->update_error  = new WP_Error( 'unexpected_previous_post_id' );
    1578                     return;
    1579                 }
    1580 
    1581                 $value['menu_item_parent'] = $parent_nav_menu_item_setting->post_id;
    1582             }
    1583 
    1584             // Insert or update menu.
    1585             $menu_item_data = array(
    1586                 'menu-item-object-id'   => $value['object_id'],
    1587                 'menu-item-object'      => $value['object'],
    1588                 'menu-item-parent-id'   => $value['menu_item_parent'],
    1589                 'menu-item-position'    => $value['position'],
    1590                 'menu-item-type'        => $value['type'],
    1591                 'menu-item-title'       => $value['title'],
    1592                 'menu-item-url'         => $value['url'],
    1593                 'menu-item-description' => $value['description'],
    1594                 'menu-item-attr-title'  => $value['attr_title'],
    1595                 'menu-item-target'      => $value['target'],
    1596                 'menu-item-classes'     => $value['classes'],
    1597                 'menu-item-xfn'         => $value['xfn'],
    1598                 'menu-item-status'      => $value['status'],
    1599             );
    1600 
    1601             $r = wp_update_nav_menu_item(
    1602                 $value['nav_menu_term_id'],
    1603                 $is_placeholder ? 0 : $this->post_id,
    1604                 $menu_item_data
    1605             );
    1606 
    1607             if ( is_wp_error( $r ) ) {
    1608                 $this->update_status = 'error';
    1609                 $this->update_error = $r;
    1610             } else {
    1611                 if ( $is_placeholder ) {
    1612                     $this->previous_post_id = $this->post_id;
    1613                     $this->post_id = $r;
    1614                     $this->update_status = 'inserted';
    1615                 } else {
    1616                     $this->update_status = 'updated';
    1617                 }
    1618             }
    1619         }
    1620 
    1621     }
    1622 
    1623     /**
    1624      * Export data for the JS client.
    1625      *
    1626      * @since 4.3.0
    1627      * @access public
    1628      *
    1629      * @see WP_Customize_Nav_Menu_Item_Setting::update()
    1630      *
    1631      * @param array $data Additional information passed back to the 'saved' event on `wp.customize`.
    1632      * @return array Save response data.
    1633      */
    1634     public function amend_customize_save_response( $data ) {
    1635         if ( ! isset( $data['nav_menu_item_updates'] ) ) {
    1636             $data['nav_menu_item_updates'] = array();
    1637         }
    1638 
    1639         $data['nav_menu_item_updates'][] = array(
    1640             'post_id'          => $this->post_id,
    1641             'previous_post_id' => $this->previous_post_id,
    1642             'error'            => $this->update_error ? $this->update_error->get_error_code() : null,
    1643             'status'           => $this->update_status,
    1644         );
    1645         return $data;
    1646     }
    1647 }
    1648 
    1649 /**
    1650  * Customize Setting to represent a nav_menu.
    1651  *
    1652  * Subclass of WP_Customize_Setting to represent a nav_menu taxonomy term, and
    1653  * the IDs for the nav_menu_items associated with the nav menu.
    1654  *
    1655  * @since 4.3.0
    1656  *
    1657  * @see wp_get_nav_menu_object()
    1658  * @see WP_Customize_Setting
    1659  */
    1660 class WP_Customize_Nav_Menu_Setting extends WP_Customize_Setting {
    1661 
    1662     const ID_PATTERN = '/^nav_menu\[(?P<id>-?\d+)\]$/';
    1663 
    1664     const TAXONOMY = 'nav_menu';
    1665 
    1666     const TYPE = 'nav_menu';
    1667 
    1668     /**
    1669      * Setting type.
    1670      *
    1671      * @since 4.3.0
    1672      * @access public
    1673      * @var string
    1674      */
    1675     public $type = self::TYPE;
    1676 
    1677     /**
    1678      * Default setting value.
    1679      *
    1680      * @since 4.3.0
    1681      * @access public
    1682      * @var array
    1683      *
    1684      * @see wp_get_nav_menu_object()
    1685      */
    1686     public $default = array(
    1687         'name'        => '',
    1688         'description' => '',
    1689         'parent'      => 0,
    1690         'auto_add'    => false,
    1691     );
    1692 
    1693     /**
    1694      * Default transport.
    1695      *
    1696      * @since 4.3.0
    1697      * @access public
    1698      * @var string
    1699      */
    1700     public $transport = 'postMessage';
    1701 
    1702     /**
    1703      * The term ID represented by this setting instance.
    1704      *
    1705      * A negative value represents a placeholder ID for a new menu not yet saved.
    1706      *
    1707      * @since 4.3.0
    1708      * @access public
    1709      * @var int
    1710      */
    1711     public $term_id;
    1712 
    1713     /**
    1714      * Previous (placeholder) term ID used before creating a new menu.
    1715      *
    1716      * This value will be exported to JS via the customize_save_response filter
    1717      * so that JavaScript can update the settings to refer to the newly-assigned
    1718      * term ID. This value is always negative to indicate it does not refer to
    1719      * a real term.
    1720      *
    1721      * @since 4.3.0
    1722      * @access public
    1723      * @var int
    1724      *
    1725      * @see WP_Customize_Nav_Menu_Setting::update()
    1726      * @see WP_Customize_Nav_Menu_Setting::amend_customize_save_response()
    1727      */
    1728     public $previous_term_id;
    1729 
    1730     /**
    1731      * Whether or not preview() was called.
    1732      *
    1733      * @since 4.3.0
    1734      * @access protected
    1735      * @var bool
    1736      */
    1737     protected $is_previewed = false;
    1738 
    1739     /**
    1740      * Whether or not update() was called.
    1741      *
    1742      * @since 4.3.0
    1743      * @access protected
    1744      * @var bool
    1745      */
    1746     protected $is_updated = false;
    1747 
    1748     /**
    1749      * Status for calling the update method, used in customize_save_response filter.
    1750      *
    1751      * When status is inserted, the placeholder term ID is stored in $previous_term_id.
    1752      * When status is error, the error is stored in $update_error.
    1753      *
    1754      * @since 4.3.0
    1755      * @access public
    1756      * @var string updated|inserted|deleted|error
    1757      *
    1758      * @see WP_Customize_Nav_Menu_Setting::update()
    1759      * @see WP_Customize_Nav_Menu_Setting::amend_customize_save_response()
    1760      */
    1761     public $update_status;
    1762 
    1763     /**
    1764      * Any error object returned by wp_update_nav_menu_object() when setting is updated.
    1765      *
    1766      * @since 4.3.0
    1767      * @access public
    1768      * @var WP_Error
    1769      *
    1770      * @see WP_Customize_Nav_Menu_Setting::update()
    1771      * @see WP_Customize_Nav_Menu_Setting::amend_customize_save_response()
    1772      */
    1773     public $update_error;
    1774 
    1775     /**
    1776      * Constructor.
    1777      *
    1778      * Any supplied $args override class property defaults.
    1779      *
    1780      * @since 4.3.0
    1781      * @access public
    1782      *
    1783      * @param WP_Customize_Manager $manager Bootstrap Customizer instance.
    1784      * @param string               $id      An specific ID of the setting. Can be a
    1785      *                                      theme mod or option name.
    1786      * @param array                $args    Optional. Setting arguments.
    1787      *
    1788      * @throws Exception If $id is not valid for this setting type.
    1789      */
    1790     public function __construct( WP_Customize_Manager $manager, $id, array $args = array() ) {
    1791         if ( empty( $manager->nav_menus ) ) {
    1792             throw new Exception( 'Expected WP_Customize_Manager::$nav_menus to be set.' );
    1793         }
    1794 
    1795         if ( ! preg_match( self::ID_PATTERN, $id, $matches ) ) {
    1796             throw new Exception( "Illegal widget setting ID: $id" );
    1797         }
    1798 
    1799         $this->term_id = intval( $matches['id'] );
    1800 
    1801         parent::__construct( $manager, $id, $args );
    1802     }
    1803 
    1804     /**
    1805      * Get the instance data for a given widget setting.
    1806      *
    1807      * @since 4.3.0
    1808      * @access public
    1809      *
    1810      * @see wp_get_nav_menu_object()
    1811      *
    1812      * @return array Instance data.
    1813      */
    1814     public function value() {
    1815         if ( $this->is_previewed && $this->_previewed_blog_id === get_current_blog_id() ) {
    1816             $undefined  = new stdClass(); // Symbol.
    1817             $post_value = $this->post_value( $undefined );
    1818 
    1819             if ( $undefined === $post_value ) {
    1820                 $value = $this->_original_value;
    1821             } else {
    1822                 $value = $post_value;
    1823             }
    1824         } else {
    1825             $value = false;
    1826 
    1827             // Note that a term_id of less than one indicates a nav_menu not yet inserted.
    1828             if ( $this->term_id > 0 ) {
    1829                 $term = wp_get_nav_menu_object( $this->term_id );
    1830 
    1831                 if ( $term ) {
    1832                     $value = wp_array_slice_assoc( (array) $term, array_keys( $this->default ) );
    1833 
    1834                     $nav_menu_options  = (array) get_option( 'nav_menu_options', array() );
    1835                     $value['auto_add'] = false;
    1836 
    1837                     if ( isset( $nav_menu_options['auto_add'] ) && is_array( $nav_menu_options['auto_add'] ) ) {
    1838                         $value['auto_add'] = in_array( $term->term_id, $nav_menu_options['auto_add'] );
    1839                     }
    1840                 }
    1841             }
    1842 
    1843             if ( ! is_array( $value ) ) {
    1844                 $value = $this->default;
    1845             }
    1846         }
    1847         return $value;
    1848     }
    1849 
    1850     /**
    1851      * Handle previewing the setting.
    1852      *
    1853      * @since 4.3.0
    1854      * @since 4.4.0 Added boolean return value
    1855      * @access public
    1856      *
    1857      * @see WP_Customize_Manager::post_value()
    1858      *
    1859      * @return bool False if method short-circuited due to no-op.
    1860      */
    1861     public function preview() {
    1862         if ( $this->is_previewed ) {
    1863             return false;
    1864         }
    1865 
    1866         $undefined = new stdClass();
    1867         $is_placeholder = ( $this->term_id < 0 );
    1868         $is_dirty = ( $undefined !== $this->post_value( $undefined ) );
    1869         if ( ! $is_placeholder && ! $is_dirty ) {
    1870             return false;
    1871         }
    1872 
    1873         $this->is_previewed       = true;
    1874         $this->_original_value    = $this->value();
    1875         $this->_previewed_blog_id = get_current_blog_id();
    1876 
    1877         add_filter( 'wp_get_nav_menus', array( $this, 'filter_wp_get_nav_menus' ), 10, 2 );
    1878         add_filter( 'wp_get_nav_menu_object', array( $this, 'filter_wp_get_nav_menu_object' ), 10, 2 );
    1879         add_filter( 'default_option_nav_menu_options', array( $this, 'filter_nav_menu_options' ) );
    1880         add_filter( 'option_nav_menu_options', array( $this, 'filter_nav_menu_options' ) );
    1881 
    1882         return true;
    1883     }
    1884 
    1885     /**
    1886      * Filter the wp_get_nav_menus() result to ensure the inserted menu object is included, and the deleted one is removed.
    1887      *
    1888      * @since 4.3.0
    1889      * @access public
    1890      *
    1891      * @see wp_get_nav_menus()
    1892      *
    1893      * @param array $menus An array of menu objects.
    1894      * @param array $args  An array of arguments used to retrieve menu objects.
    1895      * @return array
    1896      */
    1897     public function filter_wp_get_nav_menus( $menus, $args ) {
    1898         if ( get_current_blog_id() !== $this->_previewed_blog_id ) {
    1899             return $menus;
    1900         }
    1901 
    1902         $setting_value = $this->value();
    1903         $is_delete = ( false === $setting_value );
    1904         $index = -1;
    1905 
    1906         // Find the existing menu item's position in the list.
    1907         foreach ( $menus as $i => $menu ) {
    1908             if ( (int) $this->term_id === (int) $menu->term_id || (int) $this->previous_term_id === (int) $menu->term_id ) {
    1909                 $index = $i;
    1910                 break;
    1911             }
    1912         }
    1913 
    1914         if ( $is_delete ) {
    1915             // Handle deleted menu by removing it from the list.
    1916             if ( -1 !== $index ) {
    1917                 array_splice( $menus, $index, 1 );
    1918             }
    1919         } else {
    1920             // Handle menus being updated or inserted.
    1921             $menu_obj = (object) array_merge( array(
    1922                 'term_id'          => $this->term_id,
    1923                 'term_taxonomy_id' => $this->term_id,
    1924                 'slug'             => sanitize_title( $setting_value['name'] ),
    1925                 'count'            => 0,
    1926                 'term_group'       => 0,
    1927                 'taxonomy'         => self::TAXONOMY,
    1928                 'filter'           => 'raw',
    1929             ), $setting_value );
    1930 
    1931             array_splice( $menus, $index, ( -1 === $index ? 0 : 1 ), array( $menu_obj ) );
    1932         }
    1933 
    1934         // Make sure the menu objects get re-sorted after an update/insert.
    1935         if ( ! $is_delete && ! empty( $args['orderby'] ) ) {
    1936             $this->_current_menus_sort_orderby = $args['orderby'];
    1937             usort( $menus, array( $this, '_sort_menus_by_orderby' ) );
    1938         }
    1939         // @todo add support for $args['hide_empty'] === true
    1940 
    1941         return $menus;
    1942     }
    1943 
    1944     /**
    1945      * Temporary non-closure passing of orderby value to function.
    1946      *
    1947      * @since 4.3.0
    1948      * @access protected
    1949      * @var string
    1950      *
    1951      * @see WP_Customize_Nav_Menu_Setting::filter_wp_get_nav_menus()
    1952      * @see WP_Customize_Nav_Menu_Setting::_sort_menus_by_orderby()
    1953      */
    1954     protected $_current_menus_sort_orderby;
    1955 
    1956     /**
    1957      * Sort menu objects by the class-supplied orderby property.
    1958      *
    1959      * This is a workaround for a lack of closures.
    1960      *
    1961      * @since 4.3.0
    1962      * @access protected
    1963      * @param object $menu1
    1964      * @param object $menu2
    1965      * @return int
    1966      *
    1967      * @see WP_Customize_Nav_Menu_Setting::filter_wp_get_nav_menus()
    1968      */
    1969     protected function _sort_menus_by_orderby( $menu1, $menu2 ) {
    1970         $key = $this->_current_menus_sort_orderby;
    1971         return strcmp( $menu1->$key, $menu2->$key );
    1972     }
    1973 
    1974     /**
    1975      * Filter the wp_get_nav_menu_object() result to supply the previewed menu object.
    1976      *
    1977      * Requesting a nav_menu object by anything but ID is not supported.
    1978      *
    1979      * @since 4.3.0
    1980      * @access public
    1981      *
    1982      * @see wp_get_nav_menu_object()
    1983      *
    1984      * @param object|null $menu_obj Object returned by wp_get_nav_menu_object().
    1985      * @param string      $menu_id  ID of the nav_menu term. Requests by slug or name will be ignored.
    1986      * @return object|null
    1987      */
    1988     public function filter_wp_get_nav_menu_object( $menu_obj, $menu_id ) {
    1989         $ok = (
    1990             get_current_blog_id() === $this->_previewed_blog_id
    1991             &&
    1992             is_int( $menu_id )
    1993             &&
    1994             $menu_id === $this->term_id
    1995         );
    1996         if ( ! $ok ) {
    1997             return $menu_obj;
    1998         }
    1999 
    2000         $setting_value = $this->value();
    2001 
    2002         // Handle deleted menus.
    2003         if ( false === $setting_value ) {
    2004             return false;
    2005         }
    2006 
    2007         // Handle sanitization failure by preventing short-circuiting.
    2008         if ( null === $setting_value ) {
    2009             return $menu_obj;
    2010         }
    2011 
    2012         $menu_obj = (object) array_merge( array(
    2013                 'term_id'          => $this->term_id,
    2014                 'term_taxonomy_id' => $this->term_id,
    2015                 'slug'             => sanitize_title( $setting_value['name'] ),
    2016                 'count'            => 0,
    2017                 'term_group'       => 0,
    2018                 'taxonomy'         => self::TAXONOMY,
    2019                 'filter'           => 'raw',
    2020             ), $setting_value );
    2021 
    2022         return $menu_obj;
    2023     }
    2024 
    2025     /**
    2026      * Filter the nav_menu_options option to include this menu's auto_add preference.
    2027      *
    2028      * @since 4.3.0
    2029      * @access public
    2030      *
    2031      * @param array $nav_menu_options Nav menu options including auto_add.
    2032      * @return array (Kaybe) modified nav menu options.
    2033      */
    2034     public function filter_nav_menu_options( $nav_menu_options ) {
    2035         if ( $this->_previewed_blog_id !== get_current_blog_id() ) {
    2036             return $nav_menu_options;
    2037         }
    2038 
    2039         $menu = $this->value();
    2040         $nav_menu_options = $this->filter_nav_menu_options_value(
    2041             $nav_menu_options,
    2042             $this->term_id,
    2043             false === $menu ? false : $menu['auto_add']
    2044         );
    2045 
    2046         return $nav_menu_options;
    2047     }
    2048 
    2049     /**
    2050      * Sanitize an input.
    2051      *
    2052      * Note that parent::sanitize() erroneously does wp_unslash() on $value, but
    2053      * we remove that in this override.
    2054      *
    2055      * @since 4.3.0
    2056      * @access public
    2057      *
    2058      * @param array $value The value to sanitize.
    2059      * @return array|false|null Null if an input isn't valid. False if it is marked for deletion.
    2060      *                          Otherwise the sanitized value.
    2061      */
    2062     public function sanitize( $value ) {
    2063         // Menu is marked for deletion.
    2064         if ( false === $value ) {
    2065             return $value;
    2066         }
    2067 
    2068         // Invalid.
    2069         if ( ! is_array( $value ) ) {
    2070             return null;
    2071         }
    2072 
    2073         $default = array(
    2074             'name'        => '',
    2075             'description' => '',
    2076             'parent'      => 0,
    2077             'auto_add'    => false,
    2078         );
    2079         $value = array_merge( $default, $value );
    2080         $value = wp_array_slice_assoc( $value, array_keys( $default ) );
    2081 
    2082         $value['name']        = trim( esc_html( $value['name'] ) ); // This sanitization code is used in wp-admin/nav-menus.php.
    2083         $value['description'] = sanitize_text_field( $value['description'] );
    2084         $value['parent']      = max( 0, intval( $value['parent'] ) );
    2085         $value['auto_add']    = ! empty( $value['auto_add'] );
    2086 
    2087         if ( '' === $value['name'] ) {
    2088             $value['name'] = _x( '(unnamed)', 'Missing menu name.' );
    2089         }
    2090 
    2091         /** This filter is documented in wp-includes/class-wp-customize-setting.php */
    2092         return apply_filters( "customize_sanitize_{$this->id}", $value, $this );
    2093     }
    2094 
    2095     /**
    2096      * Storage for data to be sent back to client in customize_save_response filter.
    2097      *
    2098      * @access protected
    2099      * @since 4.3.0
    2100      * @var array
    2101      *
    2102      * @see WP_Customize_Nav_Menu_Setting::amend_customize_save_response()
    2103      */
    2104     protected $_widget_nav_menu_updates = array();
    2105 
    2106     /**
    2107      * Create/update the nav_menu term for this setting.
    2108      *
    2109      * Any created menus will have their assigned term IDs exported to the client
    2110      * via the customize_save_response filter. Likewise, any errors will be exported
    2111      * to the client via the customize_save_response() filter.
    2112      *
    2113      * To delete a menu, the client can send false as the value.
    2114      *
    2115      * @since 4.3.0
    2116      * @access protected
    2117      *
    2118      * @see wp_update_nav_menu_object()
    2119      *
    2120      * @param array|false $value {
    2121      *     The value to update. Note that slug cannot be updated via wp_update_nav_menu_object().
    2122      *     If false, then the menu will be deleted entirely.
    2123      *
    2124      *     @type string $name        The name of the menu to save.
    2125      *     @type string $description The term description. Default empty string.
    2126      *     @type int    $parent      The id of the parent term. Default 0.
    2127      *     @type bool   $auto_add    Whether pages will auto_add to this menu. Default false.
    2128      * }
    2129      * @return null|void
    2130      */
    2131     protected function update( $value ) {
    2132         if ( $this->is_updated ) {
    2133             return;
    2134         }
    2135 
    2136         $this->is_updated = true;
    2137         $is_placeholder   = ( $this->term_id < 0 );
    2138         $is_delete        = ( false === $value );
    2139 
    2140         add_filter( 'customize_save_response', array( $this, 'amend_customize_save_response' ) );
    2141 
    2142         $auto_add = null;
    2143         if ( $is_delete ) {
    2144             // If the current setting term is a placeholder, a delete request is a no-op.
    2145             if ( $is_placeholder ) {
    2146                 $this->update_status = 'deleted';
    2147             } else {
    2148                 $r = wp_delete_nav_menu( $this->term_id );
    2149 
    2150                 if ( is_wp_error( $r ) ) {
    2151                     $this->update_status = 'error';
    2152                     $this->update_error  = $r;
    2153                 } else {
    2154                     $this->update_status = 'deleted';
    2155                     $auto_add = false;
    2156                 }
    2157             }
    2158         } else {
    2159             // Insert or update menu.
    2160             $menu_data = wp_array_slice_assoc( $value, array( 'description', 'parent' ) );
    2161             $menu_data['menu-name'] = $value['name'];
    2162 
    2163             $menu_id = $is_placeholder ? 0 : $this->term_id;
    2164             $r = wp_update_nav_menu_object( $menu_id, $menu_data );
    2165             $original_name = $menu_data['menu-name'];
    2166             $name_conflict_suffix = 1;
    2167             while ( is_wp_error( $r ) && 'menu_exists' === $r->get_error_code() ) {
    2168                 $name_conflict_suffix += 1;
    2169                 /* translators: 1: original menu name, 2: duplicate count */
    2170                 $menu_data['menu-name'] = sprintf( __( '%1$s (%2$d)' ), $original_name, $name_conflict_suffix );
    2171                 $r = wp_update_nav_menu_object( $menu_id, $menu_data );
    2172             }
    2173 
    2174             if ( is_wp_error( $r ) ) {
    2175                 $this->update_status = 'error';
    2176                 $this->update_error  = $r;
    2177             } else {
    2178                 if ( $is_placeholder ) {
    2179                     $this->previous_term_id = $this->term_id;
    2180                     $this->term_id          = $r;
    2181                     $this->update_status    = 'inserted';
    2182                 } else {
    2183                     $this->update_status = 'updated';
    2184                 }
    2185 
    2186                 $auto_add = $value['auto_add'];
    2187             }
    2188         }
    2189 
    2190         if ( null !== $auto_add ) {
    2191             $nav_menu_options = $this->filter_nav_menu_options_value(
    2192                 (array) get_option( 'nav_menu_options', array() ),
    2193                 $this->term_id,
    2194                 $auto_add
    2195             );
    2196             update_option( 'nav_menu_options', $nav_menu_options );
    2197         }
    2198 
    2199         if ( 'inserted' === $this->update_status ) {
    2200             // Make sure that new menus assigned to nav menu locations use their new IDs.
    2201             foreach ( $this->manager->settings() as $setting ) {
    2202                 if ( ! preg_match( '/^nav_menu_locations\[/', $setting->id ) ) {
    2203                     continue;
    2204                 }
    2205 
    2206                 $post_value = $setting->post_value( null );
    2207                 if ( ! is_null( $post_value ) && $this->previous_term_id === intval( $post_value ) ) {
    2208                     $this->manager->set_post_value( $setting->id, $this->term_id );
    2209                     $setting->save();
    2210                 }
    2211             }
    2212 
    2213             // Make sure that any nav_menu widgets referencing the placeholder nav menu get updated and sent back to client.
    2214             foreach ( array_keys( $this->manager->unsanitized_post_values() ) as $setting_id ) {
    2215                 $nav_menu_widget_setting = $this->manager->get_setting( $setting_id );
    2216                 if ( ! $nav_menu_widget_setting || ! preg_match( '/^widget_nav_menu\[/', $nav_menu_widget_setting->id ) ) {
    2217                     continue;
    2218                 }
    2219 
    2220                 $widget_instance = $nav_menu_widget_setting->post_value(); // Note that this calls WP_Customize_Widgets::sanitize_widget_instance().
    2221                 if ( empty( $widget_instance['nav_menu'] ) || intval( $widget_instance['nav_menu'] ) !== $this->previous_term_id ) {
    2222                     continue;
    2223                 }
    2224 
    2225                 $widget_instance['nav_menu'] = $this->term_id;
    2226                 $updated_widget_instance = $this->manager->widgets->sanitize_widget_js_instance( $widget_instance );
    2227                 $this->manager->set_post_value( $nav_menu_widget_setting->id, $updated_widget_instance );
    2228                 $nav_menu_widget_setting->save();
    2229 
    2230                 $this->_widget_nav_menu_updates[ $nav_menu_widget_setting->id ] = $updated_widget_instance;
    2231             }
    2232         }
    2233     }
    2234 
    2235     /**
    2236      * Updates a nav_menu_options array.
    2237      *
    2238      * @since 4.3.0
    2239      * @access protected
    2240      *
    2241      * @see WP_Customize_Nav_Menu_Setting::filter_nav_menu_options()
    2242      * @see WP_Customize_Nav_Menu_Setting::update()
    2243      *
    2244      * @param array $nav_menu_options Array as returned by get_option( 'nav_menu_options' ).
    2245      * @param int   $menu_id          The term ID for the given menu.
    2246      * @param bool  $auto_add         Whether to auto-add or not.
    2247      * @return array (Maybe) modified nav_menu_otions array.
    2248      */
    2249     protected function filter_nav_menu_options_value( $nav_menu_options, $menu_id, $auto_add ) {
    2250         $nav_menu_options = (array) $nav_menu_options;
    2251         if ( ! isset( $nav_menu_options['auto_add'] ) ) {
    2252             $nav_menu_options['auto_add'] = array();
    2253         }
    2254 
    2255         $i = array_search( $menu_id, $nav_menu_options['auto_add'] );
    2256         if ( $auto_add && false === $i ) {
    2257             array_push( $nav_menu_options['auto_add'], $this->term_id );
    2258         } elseif ( ! $auto_add && false !== $i ) {
    2259             array_splice( $nav_menu_options['auto_add'], $i, 1 );
    2260         }
    2261 
    2262         return $nav_menu_options;
    2263     }
    2264 
    2265     /**
    2266      * Export data for the JS client.
    2267      *
    2268      * @since 4.3.0
    2269      * @access public
    2270      *
    2271      * @see WP_Customize_Nav_Menu_Setting::update()
    2272      *
    2273      * @param array $data Additional information passed back to the 'saved' event on `wp.customize`.
    2274      * @return array Export data.
    2275      */
    2276     public function amend_customize_save_response( $data ) {
    2277         if ( ! isset( $data['nav_menu_updates'] ) ) {
    2278             $data['nav_menu_updates'] = array();
    2279         }
    2280         if ( ! isset( $data['widget_nav_menu_updates'] ) ) {
    2281             $data['widget_nav_menu_updates'] = array();
    2282         }
    2283 
    2284         $data['nav_menu_updates'][] = array(
    2285             'term_id'          => $this->term_id,
    2286             'previous_term_id' => $this->previous_term_id,
    2287             'error'            => $this->update_error ? $this->update_error->get_error_code() : null,
    2288             'status'           => $this->update_status,
    2289             'saved_value'      => 'deleted' === $this->update_status ? null : $this->value(),
    2290         );
    2291 
    2292         $data['widget_nav_menu_updates'] = array_merge(
    2293             $data['widget_nav_menu_updates'],
    2294             $this->_widget_nav_menu_updates
    2295         );
    2296         $this->_widget_nav_menu_updates = array();
    2297 
    2298         return $data;
    2299     }
    2300 }
     772/** WP_Customize_Filter_Setting class */
     773require_once( ABSPATH . WPINC . '/customize/class-wp-customize-filter-setting.php' );
     774
     775/** WP_Customize_Header_Image_Setting class */
     776require_once( ABSPATH . WPINC . '/customize/class-wp-customize-header-image-setting.php' );
     777
     778/** WP_Customize_Background_Image_Setting class */
     779require_once( ABSPATH . WPINC . '/customize/class-wp-customize-background-image-setting.php' );
     780
     781/** WP_Customize_Nav_Menu_Item_Setting class */
     782require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-item-setting.php' );
     783
     784/** WP_Customize_Nav_Menu_Setting class */
     785require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-setting.php' );
  • trunk/src/wp-includes/customize/class-wp-customize-background-image-setting.php

    r35381 r35383  
    55 * @package WordPress
    66 * @subpackage Customize
    7  * @since 3.4.0
     7 * @since 4.4.0
    88 */
    9 
    10 /**
    11  * Customize Setting class.
    12  *
    13  * Handles saving and sanitizing of settings.
    14  *
    15  * @since 3.4.0
    16  *
    17  * @see WP_Customize_Manager
    18  */
    19 class WP_Customize_Setting {
    20     /**
    21      * @access public
    22      * @var WP_Customize_Manager
    23      */
    24     public $manager;
    25 
    26     /**
    27      * Unique string identifier for the setting.
    28      *
    29      * @access public
    30      * @var string
    31      */
    32     public $id;
    33 
    34     /**
    35      * @access public
    36      * @var string
    37      */
    38     public $type = 'theme_mod';
    39 
    40     /**
    41      * Capability required to edit this setting.
    42      *
    43      * @var string
    44      */
    45     public $capability = 'edit_theme_options';
    46 
    47     /**
    48      * Feature a theme is required to support to enable this setting.
    49      *
    50      * @access public
    51      * @var string
    52      */
    53     public $theme_supports  = '';
    54     public $default         = '';
    55     public $transport       = 'refresh';
    56 
    57     /**
    58      * Server-side sanitization callback for the setting's value.
    59      *
    60      * @var callback
    61      */
    62     public $sanitize_callback    = '';
    63     public $sanitize_js_callback = '';
    64 
    65     /**
    66      * Whether or not the setting is initially dirty when created.
    67      *
    68      * This is used to ensure that a setting will be sent from the pane to the
    69      * preview when loading the Customizer. Normally a setting only is synced to
    70      * the preview if it has been changed. This allows the setting to be sent
    71      * from the start.
    72      *
    73      * @since 4.2.0
    74      * @access public
    75      * @var bool
    76      */
    77     public $dirty = false;
    78 
    79     /**
    80      * @var array
    81      */
    82     protected $id_data = array();
    83 
    84     /**
    85      * Cache of multidimensional values to improve performance.
    86      *
    87      * @since 4.4.0
    88      * @access protected
    89      * @var array
    90      * @static
    91      */
    92     protected static $aggregated_multidimensionals = array();
    93 
    94     /**
    95      * Whether the multidimensional setting is aggregated.
    96      *
    97      * @since 4.4.0
    98      * @access protected
    99      * @var bool
    100      */
    101     protected $is_multidimensional_aggregated = false;
    102 
    103     /**
    104      * Constructor.
    105      *
    106      * Any supplied $args override class property defaults.
    107      *
    108      * @since 3.4.0
    109      *
    110      * @param WP_Customize_Manager $manager
    111      * @param string               $id      An specific ID of the setting. Can be a
    112      *                                      theme mod or option name.
    113      * @param array                $args    Setting arguments.
    114      */
    115     public function __construct( $manager, $id, $args = array() ) {
    116         $keys = array_keys( get_object_vars( $this ) );
    117         foreach ( $keys as $key ) {
    118             if ( isset( $args[ $key ] ) ) {
    119                 $this->$key = $args[ $key ];
    120             }
    121         }
    122 
    123         $this->manager = $manager;
    124         $this->id = $id;
    125 
    126         // Parse the ID for array keys.
    127         $this->id_data['keys'] = preg_split( '/\[/', str_replace( ']', '', $this->id ) );
    128         $this->id_data['base'] = array_shift( $this->id_data['keys'] );
    129 
    130         // Rebuild the ID.
    131         $this->id = $this->id_data[ 'base' ];
    132         if ( ! empty( $this->id_data[ 'keys' ] ) ) {
    133             $this->id .= '[' . implode( '][', $this->id_data['keys'] ) . ']';
    134         }
    135 
    136         if ( $this->sanitize_callback ) {
    137             add_filter( "customize_sanitize_{$this->id}", $this->sanitize_callback, 10, 2 );
    138         }
    139         if ( $this->sanitize_js_callback ) {
    140             add_filter( "customize_sanitize_js_{$this->id}", $this->sanitize_js_callback, 10, 2 );
    141         }
    142 
    143         if ( 'option' === $this->type || 'theme_mod' === $this->type ) {
    144             // Other setting types can opt-in to aggregate multidimensional explicitly.
    145             $this->aggregate_multidimensional();
    146 
    147             // Allow option settings to indicate whether they should be autoloaded.
    148             if ( 'option' === $this->type && isset( $args['autoload'] ) ) {
    149                 self::$aggregated_multidimensionals[ $this->type ][ $this->id_data['base'] ]['autoload'] = $args['autoload'];
    150             }
    151         }
    152     }
    153 
    154     /**
    155      * Get parsed ID data for multidimensional setting.
    156      *
    157      * @since 4.4.0
    158      * @access public
    159      *
    160      * @return array {
    161      *     ID data for multidimensional setting.
    162      *
    163      *     @type string $base ID base
    164      *     @type array  $keys Keys for multidimensional array.
    165      * }
    166      */
    167     final public function id_data() {
    168         return $this->id_data;
    169     }
    170 
    171     /**
    172      * Set up the setting for aggregated multidimensional values.
    173      *
    174      * When a multidimensional setting gets aggregated, all of its preview and update
    175      * calls get combined into one call, greatly improving performance.
    176      *
    177      * @since 4.4.0
    178      * @access protected
    179      */
    180     protected function aggregate_multidimensional() {
    181         $id_base = $this->id_data['base'];
    182         if ( ! isset( self::$aggregated_multidimensionals[ $this->type ] ) ) {
    183             self::$aggregated_multidimensionals[ $this->type ] = array();
    184         }
    185         if ( ! isset( self::$aggregated_multidimensionals[ $this->type ][ $id_base ] ) ) {
    186             self::$aggregated_multidimensionals[ $this->type ][ $id_base ] = array(
    187                 'previewed_instances'       => array(), // Calling preview() will add the $setting to the array.
    188                 'preview_applied_instances' => array(), // Flags for which settings have had their values applied.
    189                 'root_value'                => $this->get_root_value( array() ), // Root value for initial state, manipulated by preview and update calls.
    190             );
    191         }
    192 
    193         if ( ! empty( $this->id_data['keys'] ) ) {
    194             $this->is_multidimensional_aggregated = true;
    195         }
    196     }
    197 
    198     /**
    199      * The ID for the current blog when the preview() method was called.
    200      *
    201      * @since 4.2.0
    202      * @access protected
    203      * @var int
    204      */
    205     protected $_previewed_blog_id;
    206 
    207     /**
    208      * Return true if the current blog is not the same as the previewed blog.
    209      *
    210      * @since 4.2.0
    211      * @access public
    212      *
    213      * @return bool If preview() has been called.
    214      */
    215     public function is_current_blog_previewed() {
    216         if ( ! isset( $this->_previewed_blog_id ) ) {
    217             return false;
    218         }
    219         return ( get_current_blog_id() === $this->_previewed_blog_id );
    220     }
    221 
    222     /**
    223      * Original non-previewed value stored by the preview method.
    224      *
    225      * @see WP_Customize_Setting::preview()
    226      * @since 4.1.1
    227      * @var mixed
    228      */
    229     protected $_original_value;
    230 
    231     /**
    232      * Add filters to supply the setting's value when accessed.
    233      *
    234      * If the setting already has a pre-existing value and there is no incoming
    235      * post value for the setting, then this method will short-circuit since
    236      * there is no change to preview.
    237      *
    238      * @since 3.4.0
    239      * @since 4.4.0 Added boolean return value.
    240      * @access public
    241      *
    242      * @return bool False when preview short-circuits due no change needing to be previewed.
    243      */
    244     public function preview() {
    245         if ( ! isset( $this->_previewed_blog_id ) ) {
    246             $this->_previewed_blog_id = get_current_blog_id();
    247         }
    248         $id_base = $this->id_data['base'];
    249         $is_multidimensional = ! empty( $this->id_data['keys'] );
    250         $multidimensional_filter = array( $this, '_multidimensional_preview_filter' );
    251 
    252         /*
    253          * Check if the setting has a pre-existing value (an isset check),
    254          * and if doesn't have any incoming post value. If both checks are true,
    255          * then the preview short-circuits because there is nothing that needs
    256          * to be previewed.
    257          */
    258         $undefined = new stdClass();
    259         $needs_preview = ( $undefined !== $this->post_value( $undefined ) );
    260         $value = null;
    261 
    262         // Since no post value was defined, check if we have an initial value set.
    263         if ( ! $needs_preview ) {
    264             if ( $this->is_multidimensional_aggregated ) {
    265                 $root = self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['root_value'];
    266                 $value = $this->multidimensional_get( $root, $this->id_data['keys'], $undefined );
    267             } else {
    268                 $default = $this->default;
    269                 $this->default = $undefined; // Temporarily set default to undefined so we can detect if existing value is set.
    270                 $value = $this->value();
    271                 $this->default = $default;
    272             }
    273             $needs_preview = ( $undefined === $value ); // Because the default needs to be supplied.
    274         }
    275 
    276         if ( ! $needs_preview ) {
    277             return false;
    278         }
    279 
    280         switch ( $this->type ) {
    281             case 'theme_mod' :
    282                 if ( ! $is_multidimensional ) {
    283                     add_filter( "theme_mod_{$id_base}", array( $this, '_preview_filter' ) );
    284                 } else {
    285                     if ( empty( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['previewed_instances'] ) ) {
    286                         // Only add this filter once for this ID base.
    287                         add_filter( "theme_mod_{$id_base}", $multidimensional_filter );
    288                     }
    289                     self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['previewed_instances'][ $this->id ] = $this;
    290                 }
    291                 break;
    292             case 'option' :
    293                 if ( ! $is_multidimensional ) {
    294                     add_filter( "pre_option_{$id_base}", array( $this, '_preview_filter' ) );
    295                 } else {
    296                     if ( empty( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['previewed_instances'] ) ) {
    297                         // Only add these filters once for this ID base.
    298                         add_filter( "option_{$id_base}", $multidimensional_filter );
    299                         add_filter( "default_option_{$id_base}", $multidimensional_filter );
    300                     }
    301                     self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['previewed_instances'][ $this->id ] = $this;
    302                 }
    303                 break;
    304             default :
    305 
    306                 /**
    307                  * Fires when the {@see WP_Customize_Setting::preview()} method is called for settings
    308                  * not handled as theme_mods or options.
    309                  *
    310                  * The dynamic portion of the hook name, `$this->id`, refers to the setting ID.
    311                  *
    312                  * @since 3.4.0
    313                  *
    314                  * @param WP_Customize_Setting $this {@see WP_Customize_Setting} instance.
    315                  */
    316                 do_action( "customize_preview_{$this->id}", $this );
    317 
    318                 /**
    319                  * Fires when the {@see WP_Customize_Setting::preview()} method is called for settings
    320                  * not handled as theme_mods or options.
    321                  *
    322                  * The dynamic portion of the hook name, `$this->type`, refers to the setting type.
    323                  *
    324                  * @since 4.1.0
    325                  *
    326                  * @param WP_Customize_Setting $this {@see WP_Customize_Setting} instance.
    327                  */
    328                 do_action( "customize_preview_{$this->type}", $this );
    329         }
    330         return true;
    331     }
    332 
    333     /**
    334      * Callback function to filter non-multidimensional theme mods and options.
    335      *
    336      * If switch_to_blog() was called after the preview() method, and the current
    337      * blog is now not the same blog, then this method does a no-op and returns
    338      * the original value.
    339      *
    340      * @since 3.4.0
    341      *
    342      * @param mixed $original Old value.
    343      * @return mixed New or old value.
    344      */
    345     public function _preview_filter( $original ) {
    346         if ( ! $this->is_current_blog_previewed() ) {
    347             return $original;
    348         }
    349 
    350         $undefined = new stdClass(); // Symbol hack.
    351         $post_value = $this->post_value( $undefined );
    352         if ( $undefined !== $post_value ) {
    353             $value = $post_value;
    354         } else {
    355             /*
    356              * Note that we don't use $original here because preview() will
    357              * not add the filter in the first place if it has an initial value
    358              * and there is no post value.
    359              */
    360             $value = $this->default;
    361         }
    362         return $value;
    363     }
    364 
    365     /**
    366      * Callback function to filter multidimensional theme mods and options.
    367      *
    368      * For all multidimensional settings of a given type, the preview filter for
    369      * the first setting previewed will be used to apply the values for the others.
    370      *
    371      * @since 4.4.0
    372      * @access public
    373      *
    374      * @see WP_Customize_Setting::$aggregated_multidimensionals
    375      * @param mixed $original Original root value.
    376      * @return mixed New or old value.
    377      */
    378     public function _multidimensional_preview_filter( $original ) {
    379         if ( ! $this->is_current_blog_previewed() ) {
    380             return $original;
    381         }
    382 
    383         $id_base = $this->id_data['base'];
    384 
    385         // If no settings have been previewed yet (which should not be the case, since $this is), just pass through the original value.
    386         if ( empty( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['previewed_instances'] ) ) {
    387             return $original;
    388         }
    389 
    390         foreach ( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['previewed_instances'] as $previewed_setting ) {
    391             // Skip applying previewed value for any settings that have already been applied.
    392             if ( ! empty( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['preview_applied_instances'][ $previewed_setting->id ] ) ) {
    393                 continue;
    394             }
    395 
    396             // Do the replacements of the posted/default sub value into the root value.
    397             $value = $previewed_setting->post_value( $previewed_setting->default );
    398             $root = self::$aggregated_multidimensionals[ $previewed_setting->type ][ $id_base ]['root_value'];
    399             $root = $previewed_setting->multidimensional_replace( $root, $previewed_setting->id_data['keys'], $value );
    400             self::$aggregated_multidimensionals[ $previewed_setting->type ][ $id_base ]['root_value'] = $root;
    401 
    402             // Mark this setting having been applied so that it will be skipped when the filter is called again.
    403             self::$aggregated_multidimensionals[ $previewed_setting->type ][ $id_base ]['preview_applied_instances'][ $previewed_setting->id ] = true;
    404         }
    405 
    406         return self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['root_value'];
    407     }
    408 
    409     /**
    410      * Check user capabilities and theme supports, and then save
    411      * the value of the setting.
    412      *
    413      * @since 3.4.0
    414      *
    415      * @return false|void False if cap check fails or value isn't set.
    416      */
    417     final public function save() {
    418         $value = $this->post_value();
    419 
    420         if ( ! $this->check_capabilities() || ! isset( $value ) )
    421             return false;
    422 
    423         /**
    424          * Fires when the WP_Customize_Setting::save() method is called.
    425          *
    426          * The dynamic portion of the hook name, `$this->id_data['base']` refers to
    427          * the base slug of the setting name.
    428          *
    429          * @since 3.4.0
    430          *
    431          * @param WP_Customize_Setting $this {@see WP_Customize_Setting} instance.
    432          */
    433         do_action( 'customize_save_' . $this->id_data[ 'base' ], $this );
    434 
    435         $this->update( $value );
    436     }
    437 
    438     /**
    439      * Fetch and sanitize the $_POST value for the setting.
    440      *
    441      * @since 3.4.0
    442      *
    443      * @param mixed $default A default value which is used as a fallback. Default is null.
    444      * @return mixed The default value on failure, otherwise the sanitized value.
    445      */
    446     final public function post_value( $default = null ) {
    447         return $this->manager->post_value( $this, $default );
    448     }
    449 
    450     /**
    451      * Sanitize an input.
    452      *
    453      * @since 3.4.0
    454      *
    455      * @param string|array $value The value to sanitize.
    456      * @return string|array|null Null if an input isn't valid, otherwise the sanitized value.
    457      */
    458     public function sanitize( $value ) {
    459         $value = wp_unslash( $value );
    460 
    461         /**
    462          * Filter a Customize setting value in un-slashed form.
    463          *
    464          * @since 3.4.0
    465          *
    466          * @param mixed                $value Value of the setting.
    467          * @param WP_Customize_Setting $this  WP_Customize_Setting instance.
    468          */
    469         return apply_filters( "customize_sanitize_{$this->id}", $value, $this );
    470     }
    471 
    472     /**
    473      * Get the root value for a setting, especially for multidimensional ones.
    474      *
    475      * @since 4.4.0
    476      * @access protected
    477      *
    478      * @param mixed $default Value to return if root does not exist.
    479      * @return mixed
    480      */
    481     protected function get_root_value( $default = null ) {
    482         $id_base = $this->id_data['base'];
    483         if ( 'option' === $this->type ) {
    484             return get_option( $id_base, $default );
    485         } else if ( 'theme_mod' ) {
    486             return get_theme_mod( $id_base, $default );
    487         } else {
    488             /*
    489              * Any WP_Customize_Setting subclass implementing aggregate multidimensional
    490              * will need to override this method to obtain the data from the appropriate
    491              * location.
    492              */
    493             return $default;
    494         }
    495     }
    496 
    497     /**
    498      * Set the root value for a setting, especially for multidimensional ones.
    499      *
    500      * @since 4.4.0
    501      * @access protected
    502      *
    503      * @param mixed $value Value to set as root of multidimensional setting.
    504      * @return bool Whether the multidimensional root was updated successfully.
    505      */
    506     protected function set_root_value( $value ) {
    507         $id_base = $this->id_data['base'];
    508         if ( 'option' === $this->type ) {
    509             $autoload = true;
    510             if ( isset( self::$aggregated_multidimensionals[ $this->type ][ $this->id_data['base'] ]['autoload'] ) ) {
    511                 $autoload = self::$aggregated_multidimensionals[ $this->type ][ $this->id_data['base'] ]['autoload'];
    512             }
    513             return update_option( $id_base, $value, $autoload );
    514         } else if ( 'theme_mod' ) {
    515             set_theme_mod( $id_base, $value );
    516             return true;
    517         } else {
    518             /*
    519              * Any WP_Customize_Setting subclass implementing aggregate multidimensional
    520              * will need to override this method to obtain the data from the appropriate
    521              * location.
    522              */
    523             return false;
    524         }
    525     }
    526 
    527     /**
    528      * Save the value of the setting, using the related API.
    529      *
    530      * @since 3.4.0
    531      *
    532      * @param mixed $value The value to update.
    533      * @return bool The result of saving the value.
    534      */
    535     protected function update( $value ) {
    536         $id_base = $this->id_data['base'];
    537         if ( 'option' === $this->type || 'theme_mod' === $this->type ) {
    538             if ( ! $this->is_multidimensional_aggregated ) {
    539                 return $this->set_root_value( $value );
    540             } else {
    541                 $root = self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['root_value'];
    542                 $root = $this->multidimensional_replace( $root, $this->id_data['keys'], $value );
    543                 self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['root_value'] = $root;
    544                 return $this->set_root_value( $root );
    545             }
    546         } else {
    547             /**
    548              * Fires when the {@see WP_Customize_Setting::update()} method is called for settings
    549              * not handled as theme_mods or options.
    550              *
    551              * The dynamic portion of the hook name, `$this->type`, refers to the type of setting.
    552              *
    553              * @since 3.4.0
    554              *
    555              * @param mixed                $value Value of the setting.
    556              * @param WP_Customize_Setting $this  WP_Customize_Setting instance.
    557              */
    558             do_action( "customize_update_{$this->type}", $value, $this );
    559 
    560             return has_action( "customize_update_{$this->type}" );
    561         }
    562     }
    563 
    564     /**
    565      * Deprecated method.
    566      *
    567      * @since 3.4.0
    568      * @deprecated 4.4.0 Deprecated in favor of update() method.
    569      */
    570     protected function _update_theme_mod() {
    571         _deprecated_function( __METHOD__, '4.4.0', __CLASS__ . '::update()' );
    572     }
    573 
    574     /**
    575      * Deprecated method.
    576      *
    577      * @since 3.4.0
    578      * @deprecated 4.4.0 Deprecated in favor of update() method.
    579      */
    580     protected function _update_option() {
    581         _deprecated_function( __METHOD__, '4.4.0', __CLASS__ . '::update()' );
    582     }
    583 
    584     /**
    585      * Fetch the value of the setting.
    586      *
    587      * @since 3.4.0
    588      *
    589      * @return mixed The value.
    590      */
    591     public function value() {
    592         $id_base = $this->id_data['base'];
    593         $is_core_type = ( 'option' === $this->type || 'theme_mod' === $this->type );
    594 
    595         if ( ! $is_core_type && ! $this->is_multidimensional_aggregated ) {
    596             $value = $this->get_root_value( $this->default );
    597 
    598             /**
    599              * Filter a Customize setting value not handled as a theme_mod or option.
    600              *
    601              * The dynamic portion of the hook name, `$this->id_date['base']`, refers to
    602              * the base slug of the setting name.
    603              *
    604              * For settings handled as theme_mods or options, see those corresponding
    605              * functions for available hooks.
    606              *
    607              * @since 3.4.0
    608              *
    609              * @param mixed $default The setting default value. Default empty.
    610              */
    611             $value = apply_filters( "customize_value_{$id_base}", $value );
    612         } else if ( $this->is_multidimensional_aggregated ) {
    613             $root_value = self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['root_value'];
    614             $value = $this->multidimensional_get( $root_value, $this->id_data['keys'], $this->default );
    615         } else {
    616             $value = $this->get_root_value( $this->default );
    617         }
    618         return $value;
    619     }
    620 
    621     /**
    622      * Sanitize the setting's value for use in JavaScript.
    623      *
    624      * @since 3.4.0
    625      *
    626      * @return mixed The requested escaped value.
    627      */
    628     public function js_value() {
    629 
    630         /**
    631          * Filter a Customize setting value for use in JavaScript.
    632          *
    633          * The dynamic portion of the hook name, `$this->id`, refers to the setting ID.
    634          *
    635          * @since 3.4.0
    636          *
    637          * @param mixed                $value The setting value.
    638          * @param WP_Customize_Setting $this  {@see WP_Customize_Setting} instance.
    639          */
    640         $value = apply_filters( "customize_sanitize_js_{$this->id}", $this->value(), $this );
    641 
    642         if ( is_string( $value ) )
    643             return html_entity_decode( $value, ENT_QUOTES, 'UTF-8');
    644 
    645         return $value;
    646     }
    647 
    648     /**
    649      * Validate user capabilities whether the theme supports the setting.
    650      *
    651      * @since 3.4.0
    652      *
    653      * @return bool False if theme doesn't support the setting or user can't change setting, otherwise true.
    654      */
    655     final public function check_capabilities() {
    656         if ( $this->capability && ! call_user_func_array( 'current_user_can', (array) $this->capability ) )
    657             return false;
    658 
    659         if ( $this->theme_supports && ! call_user_func_array( 'current_theme_supports', (array) $this->theme_supports ) )
    660             return false;
    661 
    662         return true;
    663     }
    664 
    665     /**
    666      * Multidimensional helper function.
    667      *
    668      * @since 3.4.0
    669      *
    670      * @param $root
    671      * @param $keys
    672      * @param bool $create Default is false.
    673      * @return array|void Keys are 'root', 'node', and 'key'.
    674      */
    675     final protected function multidimensional( &$root, $keys, $create = false ) {
    676         if ( $create && empty( $root ) )
    677             $root = array();
    678 
    679         if ( ! isset( $root ) || empty( $keys ) )
    680             return;
    681 
    682         $last = array_pop( $keys );
    683         $node = &$root;
    684 
    685         foreach ( $keys as $key ) {
    686             if ( $create && ! isset( $node[ $key ] ) )
    687                 $node[ $key ] = array();
    688 
    689             if ( ! is_array( $node ) || ! isset( $node[ $key ] ) )
    690                 return;
    691 
    692             $node = &$node[ $key ];
    693         }
    694 
    695         if ( $create ) {
    696             if ( ! is_array( $node ) ) {
    697                 // account for an array overriding a string or object value
    698                 $node = array();
    699             }
    700             if ( ! isset( $node[ $last ] ) ) {
    701                 $node[ $last ] = array();
    702             }
    703         }
    704 
    705         if ( ! isset( $node[ $last ] ) )
    706             return;
    707 
    708         return array(
    709             'root' => &$root,
    710             'node' => &$node,
    711             'key'  => $last,
    712         );
    713     }
    714 
    715     /**
    716      * Will attempt to replace a specific value in a multidimensional array.
    717      *
    718      * @since 3.4.0
    719      *
    720      * @param $root
    721      * @param $keys
    722      * @param mixed $value The value to update.
    723      * @return mixed
    724      */
    725     final protected function multidimensional_replace( $root, $keys, $value ) {
    726         if ( ! isset( $value ) )
    727             return $root;
    728         elseif ( empty( $keys ) ) // If there are no keys, we're replacing the root.
    729             return $value;
    730 
    731         $result = $this->multidimensional( $root, $keys, true );
    732 
    733         if ( isset( $result ) )
    734             $result['node'][ $result['key'] ] = $value;
    735 
    736         return $root;
    737     }
    738 
    739     /**
    740      * Will attempt to fetch a specific value from a multidimensional array.
    741      *
    742      * @since 3.4.0
    743      *
    744      * @param $root
    745      * @param $keys
    746      * @param mixed $default A default value which is used as a fallback. Default is null.
    747      * @return mixed The requested value or the default value.
    748      */
    749     final protected function multidimensional_get( $root, $keys, $default = null ) {
    750         if ( empty( $keys ) ) // If there are no keys, test the root.
    751             return isset( $root ) ? $root : $default;
    752 
    753         $result = $this->multidimensional( $root, $keys );
    754         return isset( $result ) ? $result['node'][ $result['key'] ] : $default;
    755     }
    756 
    757     /**
    758      * Will attempt to check if a specific value in a multidimensional array is set.
    759      *
    760      * @since 3.4.0
    761      *
    762      * @param $root
    763      * @param $keys
    764      * @return bool True if value is set, false if not.
    765      */
    766     final protected function multidimensional_isset( $root, $keys ) {
    767         $result = $this->multidimensional_get( $root, $keys );
    768         return isset( $result );
    769     }
    770 }
    771 
    772 /**
    773  * A setting that is used to filter a value, but will not save the results.
    774  *
    775  * Results should be properly handled using another setting or callback.
    776  *
    777  * @since 3.4.0
    778  *
    779  * @see WP_Customize_Setting
    780  */
    781 class WP_Customize_Filter_Setting extends WP_Customize_Setting {
    782 
    783     /**
    784      * @since 3.4.0
    785      */
    786     public function update( $value ) {}
    787 }
    788 
    789 /**
    790  * A setting that is used to filter a value, but will not save the results.
    791  *
    792  * Results should be properly handled using another setting or callback.
    793  *
    794  * @since 3.4.0
    795  *
    796  * @see WP_Customize_Setting
    797  */
    798 final class WP_Customize_Header_Image_Setting extends WP_Customize_Setting {
    799     public $id = 'header_image_data';
    800 
    801     /**
    802      * @since 3.4.0
    803      *
    804      * @global Custom_Image_Header $custom_image_header
    805      *
    806      * @param $value
    807      */
    808     public function update( $value ) {
    809         global $custom_image_header;
    810 
    811         // If the value doesn't exist (removed or random),
    812         // use the header_image value.
    813         if ( ! $value )
    814             $value = $this->manager->get_setting('header_image')->post_value();
    815 
    816         if ( is_array( $value ) && isset( $value['choice'] ) )
    817             $custom_image_header->set_header_image( $value['choice'] );
    818         else
    819             $custom_image_header->set_header_image( $value );
    820     }
    821 }
    8229
    82310/**
     
    84027    }
    84128}
    842 
    843 /**
    844  * Customize Setting to represent a nav_menu.
    845  *
    846  * Subclass of WP_Customize_Setting to represent a nav_menu taxonomy term, and
    847  * the IDs for the nav_menu_items associated with the nav menu.
    848  *
    849  * @since 4.3.0
    850  *
    851  * @see WP_Customize_Setting
    852  */
    853 class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting {
    854 
    855     const ID_PATTERN = '/^nav_menu_item\[(?P<id>-?\d+)\]$/';
    856 
    857     const POST_TYPE = 'nav_menu_item';
    858 
    859     const TYPE = 'nav_menu_item';
    860 
    861     /**
    862      * Setting type.
    863      *
    864      * @since 4.3.0
    865      * @access public
    866      * @var string
    867      */
    868     public $type = self::TYPE;
    869 
    870     /**
    871      * Default setting value.
    872      *
    873      * @since 4.3.0
    874      * @access public
    875      * @var array
    876      *
    877      * @see wp_setup_nav_menu_item()
    878      */
    879     public $default = array(
    880         // The $menu_item_data for wp_update_nav_menu_item().
    881         'object_id'        => 0,
    882         'object'           => '', // Taxonomy name.
    883         'menu_item_parent' => 0, // A.K.A. menu-item-parent-id; note that post_parent is different, and not included.
    884         'position'         => 0, // A.K.A. menu_order.
    885         'type'             => 'custom', // Note that type_label is not included here.
    886         'title'            => '',
    887         'url'              => '',
    888         'target'           => '',
    889         'attr_title'       => '',
    890         'description'      => '',
    891         'classes'          => '',
    892         'xfn'              => '',
    893         'status'           => 'publish',
    894         'original_title'   => '',
    895         'nav_menu_term_id' => 0, // This will be supplied as the $menu_id arg for wp_update_nav_menu_item().
    896         '_invalid'         => false,
    897     );
    898 
    899     /**
    900      * Default transport.
    901      *
    902      * @since 4.3.0
    903      * @access public
    904      * @var string
    905      */
    906     public $transport = 'postMessage';
    907 
    908     /**
    909      * The post ID represented by this setting instance. This is the db_id.
    910      *
    911      * A negative value represents a placeholder ID for a new menu not yet saved.
    912      *
    913      * @since 4.3.0
    914      * @access public
    915      * @var int
    916      */
    917     public $post_id;
    918 
    919     /**
    920      * Storage of pre-setup menu item to prevent wasted calls to wp_setup_nav_menu_item().
    921      *
    922      * @since 4.3.0
    923      * @access protected
    924      * @var array
    925      */
    926     protected $value;
    927 
    928     /**
    929      * Previous (placeholder) post ID used before creating a new menu item.
    930      *
    931      * This value will be exported to JS via the customize_save_response filter
    932      * so that JavaScript can update the settings to refer to the newly-assigned
    933      * post ID. This value is always negative to indicate it does not refer to
    934      * a real post.
    935      *
    936      * @since 4.3.0
    937      * @access public
    938      * @var int
    939      *
    940      * @see WP_Customize_Nav_Menu_Item_Setting::update()
    941      * @see WP_Customize_Nav_Menu_Item_Setting::amend_customize_save_response()
    942      */
    943     public $previous_post_id;
    944 
    945     /**
    946      * When previewing or updating a menu item, this stores the previous nav_menu_term_id
    947      * which ensures that we can apply the proper filters.
    948      *
    949      * @since 4.3.0
    950      * @access public
    951      * @var int
    952      */
    953     public $original_nav_menu_term_id;
    954 
    955     /**
    956      * Whether or not preview() was called.
    957      *
    958      * @since 4.3.0
    959      * @access protected
    960      * @var bool
    961      */
    962     protected $is_previewed = false;
    963 
    964     /**
    965      * Whether or not update() was called.
    966      *
    967      * @since 4.3.0
    968      * @access protected
    969      * @var bool
    970      */
    971     protected $is_updated = false;
    972 
    973     /**
    974      * Status for calling the update method, used in customize_save_response filter.
    975      *
    976      * When status is inserted, the placeholder post ID is stored in $previous_post_id.
    977      * When status is error, the error is stored in $update_error.
    978      *
    979      * @since 4.3.0
    980      * @access public
    981      * @var string updated|inserted|deleted|error
    982      *
    983      * @see WP_Customize_Nav_Menu_Item_Setting::update()
    984      * @see WP_Customize_Nav_Menu_Item_Setting::amend_customize_save_response()
    985      */
    986     public $update_status;
    987 
    988     /**
    989      * Any error object returned by wp_update_nav_menu_item() when setting is updated.
    990      *
    991      * @since 4.3.0
    992      * @access public
    993      * @var WP_Error
    994      *
    995      * @see WP_Customize_Nav_Menu_Item_Setting::update()
    996      * @see WP_Customize_Nav_Menu_Item_Setting::amend_customize_save_response()
    997      */
    998     public $update_error;
    999 
    1000     /**
    1001      * Constructor.
    1002      *
    1003      * Any supplied $args override class property defaults.
    1004      *
    1005      * @since 4.3.0
    1006      * @access public
    1007      *
    1008      * @param WP_Customize_Manager $manager Bootstrap Customizer instance.
    1009      * @param string               $id      An specific ID of the setting. Can be a
    1010      *                                      theme mod or option name.
    1011      * @param array                $args    Optional. Setting arguments.
    1012      *
    1013      * @throws Exception If $id is not valid for this setting type.
    1014      */
    1015     public function __construct( WP_Customize_Manager $manager, $id, array $args = array() ) {
    1016         if ( empty( $manager->nav_menus ) ) {
    1017             throw new Exception( 'Expected WP_Customize_Manager::$nav_menus to be set.' );
    1018         }
    1019 
    1020         if ( ! preg_match( self::ID_PATTERN, $id, $matches ) ) {
    1021             throw new Exception( "Illegal widget setting ID: $id" );
    1022         }
    1023 
    1024         $this->post_id = intval( $matches['id'] );
    1025         add_action( 'wp_update_nav_menu_item', array( $this, 'flush_cached_value' ), 10, 2 );
    1026 
    1027         parent::__construct( $manager, $id, $args );
    1028 
    1029         // Ensure that an initially-supplied value is valid.
    1030         if ( isset( $this->value ) ) {
    1031             $this->populate_value();
    1032             foreach ( array_diff( array_keys( $this->default ), array_keys( $this->value ) ) as $missing ) {
    1033                 throw new Exception( "Supplied nav_menu_item value missing property: $missing" );
    1034             }
    1035         }
    1036 
    1037     }
    1038 
    1039     /**
    1040      * Clear the cached value when this nav menu item is updated.
    1041      *
    1042      * @since 4.3.0
    1043      * @access public
    1044      *
    1045      * @param int $menu_id       The term ID for the menu.
    1046      * @param int $menu_item_id  The post ID for the menu item.
    1047      */
    1048     public function flush_cached_value( $menu_id, $menu_item_id ) {
    1049         unset( $menu_id );
    1050         if ( $menu_item_id === $this->post_id ) {
    1051             $this->value = null;
    1052         }
    1053     }
    1054 
    1055     /**
    1056      * Get the instance data for a given nav_menu_item setting.
    1057      *
    1058      * @since 4.3.0
    1059      * @access public
    1060      *
    1061      * @see wp_setup_nav_menu_item()
    1062      *
    1063      * @return array|false Instance data array, or false if the item is marked for deletion.
    1064      */
    1065     public function value() {
    1066         if ( $this->is_previewed && $this->_previewed_blog_id === get_current_blog_id() ) {
    1067             $undefined  = new stdClass(); // Symbol.
    1068             $post_value = $this->post_value( $undefined );
    1069 
    1070             if ( $undefined === $post_value ) {
    1071                 $value = $this->_original_value;
    1072             } else {
    1073                 $value = $post_value;
    1074             }
    1075         } else if ( isset( $this->value ) ) {
    1076             $value = $this->value;
    1077         } else {
    1078             $value = false;
    1079 
    1080             // Note that a ID of less than one indicates a nav_menu not yet inserted.
    1081             if ( $this->post_id > 0 ) {
    1082                 $post = get_post( $this->post_id );
    1083                 if ( $post && self::POST_TYPE === $post->post_type ) {
    1084                     $value = (array) wp_setup_nav_menu_item( $post );
    1085                 }
    1086             }
    1087 
    1088             if ( ! is_array( $value ) ) {
    1089                 $value = $this->default;
    1090             }
    1091 
    1092             // Cache the value for future calls to avoid having to re-call wp_setup_nav_menu_item().
    1093             $this->value = $value;
    1094             $this->populate_value();
    1095             $value = $this->value;
    1096         }
    1097 
    1098         return $value;
    1099     }
    1100 
    1101     /**
    1102      * Ensure that the value is fully populated with the necessary properties.
    1103      *
    1104      * Translates some properties added by wp_setup_nav_menu_item() and removes others.
    1105      *
    1106      * @since 4.3.0
    1107      * @access protected
    1108      *
    1109      * @see WP_Customize_Nav_Menu_Item_Setting::value()
    1110      */
    1111     protected function populate_value() {
    1112         if ( ! is_array( $this->value ) ) {
    1113             return;
    1114         }
    1115 
    1116         if ( isset( $this->value['menu_order'] ) ) {
    1117             $this->value['position'] = $this->value['menu_order'];
    1118             unset( $this->value['menu_order'] );
    1119         }
    1120         if ( isset( $this->value['post_status'] ) ) {
    1121             $this->value['status'] = $this->value['post_status'];
    1122             unset( $this->value['post_status'] );
    1123         }
    1124 
    1125         if ( ! isset( $this->value['original_title'] ) ) {
    1126             $original_title = '';
    1127             if ( 'post_type' === $this->value['type'] ) {
    1128                 $original_title = get_the_title( $this->value['object_id'] );
    1129             } elseif ( 'taxonomy' === $this->value['type'] ) {
    1130                 $original_title = get_term_field( 'name', $this->value['object_id'], $this->value['object'], 'raw' );
    1131                 if ( is_wp_error( $original_title ) ) {
    1132                     $original_title = '';
    1133                 }
    1134             }
    1135             $this->value['original_title'] = html_entity_decode( $original_title, ENT_QUOTES, get_bloginfo( 'charset' ) );
    1136         }
    1137 
    1138         if ( ! isset( $this->value['nav_menu_term_id'] ) && $this->post_id > 0 ) {
    1139             $menus = wp_get_post_terms( $this->post_id, WP_Customize_Nav_Menu_Setting::TAXONOMY, array(
    1140                 'fields' => 'ids',
    1141             ) );
    1142             if ( ! empty( $menus ) ) {
    1143                 $this->value['nav_menu_term_id'] = array_shift( $menus );
    1144             } else {
    1145                 $this->value['nav_menu_term_id'] = 0;
    1146             }
    1147         }
    1148 
    1149         foreach ( array( 'object_id', 'menu_item_parent', 'nav_menu_term_id' ) as $key ) {
    1150             if ( ! is_int( $this->value[ $key ] ) ) {
    1151                 $this->value[ $key ] = intval( $this->value[ $key ] );
    1152             }
    1153         }
    1154         foreach ( array( 'classes', 'xfn' ) as $key ) {
    1155             if ( is_array( $this->value[ $key ] ) ) {
    1156                 $this->value[ $key ] = implode( ' ', $this->value[ $key ] );
    1157             }
    1158         }
    1159 
    1160         if ( ! isset( $this->value['_invalid'] ) ) {
    1161             $this->value['_invalid'] = (
    1162                 ( 'post_type' === $this->value['type'] && ! post_type_exists( $this->value['object'] ) )
    1163                 ||
    1164                 ( 'taxonomy' === $this->value['type'] && ! taxonomy_exists( $this->value['object'] ) )
    1165             );
    1166         }
    1167 
    1168         // Remove remaining properties available on a setup nav_menu_item post object which aren't relevant to the setting value.
    1169         $irrelevant_properties = array(
    1170             'ID',
    1171             'comment_count',
    1172             'comment_status',
    1173             'db_id',
    1174             'filter',
    1175             'guid',
    1176             'ping_status',
    1177             'pinged',
    1178             'post_author',
    1179             'post_content',
    1180             'post_content_filtered',
    1181             'post_date',
    1182             'post_date_gmt',
    1183             'post_excerpt',
    1184             'post_mime_type',
    1185             'post_modified',
    1186             'post_modified_gmt',
    1187             'post_name',
    1188             'post_parent',
    1189             'post_password',
    1190             'post_title',
    1191             'post_type',
    1192             'to_ping',
    1193         );
    1194         foreach ( $irrelevant_properties as $property ) {
    1195             unset( $this->value[ $property ] );
    1196         }
    1197     }
    1198 
    1199     /**
    1200      * Handle previewing the setting.
    1201      *
    1202      * @since 4.3.0
    1203      * @since 4.4.0 Added boolean return value.
    1204      * @access public
    1205      *
    1206      * @see WP_Customize_Manager::post_value()
    1207      *
    1208      * @return bool False if method short-circuited due to no-op.
    1209      */
    1210     public function preview() {
    1211         if ( $this->is_previewed ) {
    1212             return false;
    1213         }
    1214 
    1215         $undefined = new stdClass();
    1216         $is_placeholder = ( $this->post_id < 0 );
    1217         $is_dirty = ( $undefined !== $this->post_value( $undefined ) );
    1218         if ( ! $is_placeholder && ! $is_dirty ) {
    1219             return false;
    1220         }
    1221 
    1222         $this->is_previewed              = true;
    1223         $this->_original_value           = $this->value();
    1224         $this->original_nav_menu_term_id = $this->_original_value['nav_menu_term_id'];
    1225         $this->_previewed_blog_id        = get_current_blog_id();
    1226 
    1227         add_filter( 'wp_get_nav_menu_items', array( $this, 'filter_wp_get_nav_menu_items' ), 10, 3 );
    1228 
    1229         $sort_callback = array( __CLASS__, 'sort_wp_get_nav_menu_items' );
    1230         if ( ! has_filter( 'wp_get_nav_menu_items', $sort_callback ) ) {
    1231             add_filter( 'wp_get_nav_menu_items', array( __CLASS__, 'sort_wp_get_nav_menu_items' ), 1000, 3 );
    1232         }
    1233 
    1234         // @todo Add get_post_metadata filters for plugins to add their data.
    1235 
    1236         return true;
    1237     }
    1238 
    1239     /**
    1240      * Filter the wp_get_nav_menu_items() result to supply the previewed menu items.
    1241      *
    1242      * @since 4.3.0
    1243      * @access public
    1244      *
    1245      * @see wp_get_nav_menu_items()
    1246      *
    1247      * @param array  $items An array of menu item post objects.
    1248      * @param object $menu  The menu object.
    1249      * @param array  $args  An array of arguments used to retrieve menu item objects.
    1250      * @return array Array of menu items,
    1251      */
    1252     public function filter_wp_get_nav_menu_items( $items, $menu, $args ) {
    1253         $this_item = $this->value();
    1254         $current_nav_menu_term_id = $this_item['nav_menu_term_id'];
    1255         unset( $this_item['nav_menu_term_id'] );
    1256 
    1257         $should_filter = (
    1258             $menu->term_id === $this->original_nav_menu_term_id
    1259             ||
    1260             $menu->term_id === $current_nav_menu_term_id
    1261         );
    1262         if ( ! $should_filter ) {
    1263             return $items;
    1264         }
    1265 
    1266         // Handle deleted menu item, or menu item moved to another menu.
    1267         $should_remove = (
    1268             false === $this_item
    1269             ||
    1270             true === $this_item['_invalid']
    1271             ||
    1272             (
    1273                 $this->original_nav_menu_term_id === $menu->term_id
    1274                 &&
    1275                 $current_nav_menu_term_id !== $this->original_nav_menu_term_id
    1276             )
    1277         );
    1278         if ( $should_remove ) {
    1279             $filtered_items = array();
    1280             foreach ( $items as $item ) {
    1281                 if ( $item->db_id !== $this->post_id ) {
    1282                     $filtered_items[] = $item;
    1283                 }
    1284             }
    1285             return $filtered_items;
    1286         }
    1287 
    1288         $mutated = false;
    1289         $should_update = (
    1290             is_array( $this_item )
    1291             &&
    1292             $current_nav_menu_term_id === $menu->term_id
    1293         );
    1294         if ( $should_update ) {
    1295             foreach ( $items as $item ) {
    1296                 if ( $item->db_id === $this->post_id ) {
    1297                     foreach ( get_object_vars( $this->value_as_wp_post_nav_menu_item() ) as $key => $value ) {
    1298                         $item->$key = $value;
    1299                     }
    1300                     $mutated = true;
    1301                 }
    1302             }
    1303 
    1304             // Not found so we have to append it..
    1305             if ( ! $mutated ) {
    1306                 $items[] = $this->value_as_wp_post_nav_menu_item();
    1307             }
    1308         }
    1309 
    1310         return $items;
    1311     }
    1312 
    1313     /**
    1314      * Re-apply the tail logic also applied on $items by wp_get_nav_menu_items().
    1315      *
    1316      * @since 4.3.0
    1317      * @access public
    1318      * @static
    1319      *
    1320      * @see wp_get_nav_menu_items()
    1321      *
    1322      * @param array  $items An array of menu item post objects.
    1323      * @param object $menu  The menu object.
    1324      * @param array  $args  An array of arguments used to retrieve menu item objects.
    1325      * @return array Array of menu items,
    1326      */
    1327     public static function sort_wp_get_nav_menu_items( $items, $menu, $args ) {
    1328         // @todo We should probably re-apply some constraints imposed by $args.
    1329         unset( $args['include'] );
    1330 
    1331         // Remove invalid items only in frontend.
    1332         if ( ! is_admin() ) {
    1333             $items = array_filter( $items, '_is_valid_nav_menu_item' );
    1334         }
    1335 
    1336         if ( ARRAY_A === $args['output'] ) {
    1337             $GLOBALS['_menu_item_sort_prop'] = $args['output_key'];
    1338             usort( $items, '_sort_nav_menu_items' );
    1339             $i = 1;
    1340 
    1341             foreach ( $items as $k => $item ) {
    1342                 $items[ $k ]->{$args['output_key']} = $i++;
    1343             }
    1344         }
    1345 
    1346         return $items;
    1347     }
    1348 
    1349     /**
    1350      * Get the value emulated into a WP_Post and set up as a nav_menu_item.
    1351      *
    1352      * @since 4.3.0
    1353      * @access public
    1354      *
    1355      * @return WP_Post With wp_setup_nav_menu_item() applied.
    1356      */
    1357     public function value_as_wp_post_nav_menu_item() {
    1358         $item = (object) $this->value();
    1359         unset( $item->nav_menu_term_id );
    1360 
    1361         $item->post_status = $item->status;
    1362         unset( $item->status );
    1363 
    1364         $item->post_type = 'nav_menu_item';
    1365         $item->menu_order = $item->position;
    1366         unset( $item->position );
    1367 
    1368         if ( $item->title ) {
    1369             $item->post_title = $item->title;
    1370         }
    1371 
    1372         $item->ID = $this->post_id;
    1373         $item->db_id = $this->post_id;
    1374         $post = new WP_Post( (object) $item );
    1375 
    1376         if ( empty( $post->post_author ) ) {
    1377             $post->post_author = get_current_user_id();
    1378         }
    1379 
    1380         if ( ! isset( $post->type_label ) ) {
    1381             if ( 'post_type' === $post->type ) {
    1382                 $object = get_post_type_object( $post->object );
    1383                 if ( $object ) {
    1384                     $post->type_label = $object->labels->singular_name;
    1385                 } else {
    1386                     $post->type_label = $post->object;
    1387                 }
    1388             } elseif ( 'taxonomy' == $post->type ) {
    1389                 $object = get_taxonomy( $post->object );
    1390                 if ( $object ) {
    1391                     $post->type_label = $object->labels->singular_name;
    1392                 } else {
    1393                     $post->type_label = $post->object;
    1394                 }
    1395             } else {
    1396                 $post->type_label = __( 'Custom Link' );
    1397             }
    1398         }
    1399 
    1400         return $post;
    1401     }
    1402 
    1403     /**
    1404      * Sanitize an input.
    1405      *
    1406      * Note that parent::sanitize() erroneously does wp_unslash() on $value, but
    1407      * we remove that in this override.
    1408      *
    1409      * @since 4.3.0
    1410      * @access public
    1411      *
    1412      * @param array $menu_item_value The value to sanitize.
    1413      * @return array|false|null Null if an input isn't valid. False if it is marked for deletion.
    1414      *                          Otherwise the sanitized value.
    1415      */
    1416     public function sanitize( $menu_item_value ) {
    1417         // Menu is marked for deletion.
    1418         if ( false === $menu_item_value ) {
    1419             return $menu_item_value;
    1420         }
    1421 
    1422         // Invalid.
    1423         if ( ! is_array( $menu_item_value ) ) {
    1424             return null;
    1425         }
    1426 
    1427         $default = array(
    1428             'object_id'        => 0,
    1429             'object'           => '',
    1430             'menu_item_parent' => 0,
    1431             'position'         => 0,
    1432             'type'             => 'custom',
    1433             'title'            => '',
    1434             'url'              => '',
    1435             'target'           => '',
    1436             'attr_title'       => '',
    1437             'description'      => '',
    1438             'classes'          => '',
    1439             'xfn'              => '',
    1440             'status'           => 'publish',
    1441             'original_title'   => '',
    1442             'nav_menu_term_id' => 0,
    1443             '_invalid'         => false,
    1444         );
    1445         $menu_item_value = array_merge( $default, $menu_item_value );
    1446         $menu_item_value = wp_array_slice_assoc( $menu_item_value, array_keys( $default ) );
    1447         $menu_item_value['position'] = max( 0, intval( $menu_item_value['position'] ) );
    1448 
    1449         foreach ( array( 'object_id', 'menu_item_parent', 'nav_menu_term_id' ) as $key ) {
    1450             // Note we need to allow negative-integer IDs for previewed objects not inserted yet.
    1451             $menu_item_value[ $key ] = intval( $menu_item_value[ $key ] );
    1452         }
    1453 
    1454         foreach ( array( 'type', 'object', 'target' ) as $key ) {
    1455             $menu_item_value[ $key ] = sanitize_key( $menu_item_value[ $key ] );
    1456         }
    1457 
    1458         foreach ( array( 'xfn', 'classes' ) as $key ) {
    1459             $value = $menu_item_value[ $key ];
    1460             if ( ! is_array( $value ) ) {
    1461                 $value = explode( ' ', $value );
    1462             }
    1463             $menu_item_value[ $key ] = implode( ' ', array_map( 'sanitize_html_class', $value ) );
    1464         }
    1465 
    1466         foreach ( array( 'title', 'attr_title', 'description', 'original_title' ) as $key ) {
    1467             // @todo Should esc_attr() the attr_title as well?
    1468             $menu_item_value[ $key ] = sanitize_text_field( $menu_item_value[ $key ] );
    1469         }
    1470 
    1471         $menu_item_value['url'] = esc_url_raw( $menu_item_value['url'] );
    1472         if ( ! get_post_status_object( $menu_item_value['status'] ) ) {
    1473             $menu_item_value['status'] = 'publish';
    1474         }
    1475 
    1476         $menu_item_value['_invalid'] = (bool) $menu_item_value['_invalid'];
    1477 
    1478         /** This filter is documented in wp-includes/class-wp-customize-setting.php */
    1479         return apply_filters( "customize_sanitize_{$this->id}", $menu_item_value, $this );
    1480     }
    1481 
    1482     /**
    1483      * Create/update the nav_menu_item post for this setting.
    1484      *
    1485      * Any created menu items will have their assigned post IDs exported to the client
    1486      * via the customize_save_response filter. Likewise, any errors will be exported
    1487      * to the client via the customize_save_response() filter.
    1488      *
    1489      * To delete a menu, the client can send false as the value.
    1490      *
    1491      * @since 4.3.0
    1492      * @access protected
    1493      *
    1494      * @see wp_update_nav_menu_item()
    1495      *
    1496      * @param array|false $value The menu item array to update. If false, then the menu item will be deleted
    1497      *                           entirely. See WP_Customize_Nav_Menu_Item_Setting::$default for what the value
    1498      *                           should consist of.
    1499      * @return null|void
    1500      */
    1501     protected function update( $value ) {
    1502         if ( $this->is_updated ) {
    1503             return;
    1504         }
    1505 
    1506         $this->is_updated = true;
    1507         $is_placeholder   = ( $this->post_id < 0 );
    1508         $is_delete        = ( false === $value );
    1509 
    1510         // Update the cached value.
    1511         $this->value = $value;
    1512 
    1513         add_filter( 'customize_save_response', array( $this, 'amend_customize_save_response' ) );
    1514 
    1515         if ( $is_delete ) {
    1516             // If the current setting post is a placeholder, a delete request is a no-op.
    1517             if ( $is_placeholder ) {
    1518                 $this->update_status = 'deleted';
    1519             } else {
    1520                 $r = wp_delete_post( $this->post_id, true );
    1521 
    1522                 if ( false === $r ) {
    1523                     $this->update_error  = new WP_Error( 'delete_failure' );
    1524                     $this->update_status = 'error';
    1525                 } else {
    1526                     $this->update_status = 'deleted';
    1527                 }
    1528                 // @todo send back the IDs for all associated nav menu items deleted, so these settings (and controls) can be removed from Customizer?
    1529             }
    1530         } else {
    1531 
    1532             // Handle saving menu items for menus that are being newly-created.
    1533             if ( $value['nav_menu_term_id'] < 0 ) {
    1534                 $nav_menu_setting_id = sprintf( 'nav_menu[%s]', $value['nav_menu_term_id'] );
    1535                 $nav_menu_setting    = $this->manager->get_setting( $nav_menu_setting_id );
    1536 
    1537                 if ( ! $nav_menu_setting || ! ( $nav_menu_setting instanceof WP_Customize_Nav_Menu_Setting ) ) {
    1538                     $this->update_status = 'error';
    1539                     $this->update_error  = new WP_Error( 'unexpected_nav_menu_setting' );
    1540                     return;
    1541                 }
    1542 
    1543                 if ( false === $nav_menu_setting->save() ) {
    1544                     $this->update_status = 'error';
    1545                     $this->update_error  = new WP_Error( 'nav_menu_setting_failure' );
    1546                     return;
    1547                 }
    1548 
    1549                 if ( $nav_menu_setting->previous_term_id !== intval( $value['nav_menu_term_id'] ) ) {
    1550                     $this->update_status = 'error';
    1551                     $this->update_error  = new WP_Error( 'unexpected_previous_term_id' );
    1552                     return;
    1553                 }
    1554 
    1555                 $value['nav_menu_term_id'] = $nav_menu_setting->term_id;
    1556             }
    1557 
    1558             // Handle saving a nav menu item that is a child of a nav menu item being newly-created.
    1559             if ( $value['menu_item_parent'] < 0 ) {
    1560                 $parent_nav_menu_item_setting_id = sprintf( 'nav_menu_item[%s]', $value['menu_item_parent'] );
    1561                 $parent_nav_menu_item_setting    = $this->manager->get_setting( $parent_nav_menu_item_setting_id );
    1562 
    1563                 if ( ! $parent_nav_menu_item_setting || ! ( $parent_nav_menu_item_setting instanceof WP_Customize_Nav_Menu_Item_Setting ) ) {
    1564                     $this->update_status = 'error';
    1565                     $this->update_error  = new WP_Error( 'unexpected_nav_menu_item_setting' );
    1566                     return;
    1567                 }
    1568 
    1569                 if ( false === $parent_nav_menu_item_setting->save() ) {
    1570                     $this->update_status = 'error';
    1571                     $this->update_error  = new WP_Error( 'nav_menu_item_setting_failure' );
    1572                     return;
    1573                 }
    1574 
    1575                 if ( $parent_nav_menu_item_setting->previous_post_id !== intval( $value['menu_item_parent'] ) ) {
    1576                     $this->update_status = 'error';
    1577                     $this->update_error  = new WP_Error( 'unexpected_previous_post_id' );
    1578                     return;
    1579                 }
    1580 
    1581                 $value['menu_item_parent'] = $parent_nav_menu_item_setting->post_id;
    1582             }
    1583 
    1584             // Insert or update menu.
    1585             $menu_item_data = array(
    1586                 'menu-item-object-id'   => $value['object_id'],
    1587                 'menu-item-object'      => $value['object'],
    1588                 'menu-item-parent-id'   => $value['menu_item_parent'],
    1589                 'menu-item-position'    => $value['position'],
    1590                 'menu-item-type'        => $value['type'],
    1591                 'menu-item-title'       => $value['title'],
    1592                 'menu-item-url'         => $value['url'],
    1593                 'menu-item-description' => $value['description'],
    1594                 'menu-item-attr-title'  => $value['attr_title'],
    1595                 'menu-item-target'      => $value['target'],
    1596                 'menu-item-classes'     => $value['classes'],
    1597                 'menu-item-xfn'         => $value['xfn'],
    1598                 'menu-item-status'      => $value['status'],
    1599             );
    1600 
    1601             $r = wp_update_nav_menu_item(
    1602                 $value['nav_menu_term_id'],
    1603                 $is_placeholder ? 0 : $this->post_id,
    1604                 $menu_item_data
    1605             );
    1606 
    1607             if ( is_wp_error( $r ) ) {
    1608                 $this->update_status = 'error';
    1609                 $this->update_error = $r;
    1610             } else {
    1611                 if ( $is_placeholder ) {
    1612                     $this->previous_post_id = $this->post_id;
    1613                     $this->post_id = $r;
    1614                     $this->update_status = 'inserted';
    1615                 } else {
    1616                     $this->update_status = 'updated';
    1617                 }
    1618             }
    1619         }
    1620 
    1621     }
    1622 
    1623     /**
    1624      * Export data for the JS client.
    1625      *
    1626      * @since 4.3.0
    1627      * @access public
    1628      *
    1629      * @see WP_Customize_Nav_Menu_Item_Setting::update()
    1630      *
    1631      * @param array $data Additional information passed back to the 'saved' event on `wp.customize`.
    1632      * @return array Save response data.
    1633      */
    1634     public function amend_customize_save_response( $data ) {
    1635         if ( ! isset( $data['nav_menu_item_updates'] ) ) {
    1636             $data['nav_menu_item_updates'] = array();
    1637         }
    1638 
    1639         $data['nav_menu_item_updates'][] = array(
    1640             'post_id'          => $this->post_id,
    1641             'previous_post_id' => $this->previous_post_id,
    1642             'error'            => $this->update_error ? $this->update_error->get_error_code() : null,
    1643             'status'           => $this->update_status,
    1644         );
    1645         return $data;
    1646     }
    1647 }
    1648 
    1649 /**
    1650  * Customize Setting to represent a nav_menu.
    1651  *
    1652  * Subclass of WP_Customize_Setting to represent a nav_menu taxonomy term, and
    1653  * the IDs for the nav_menu_items associated with the nav menu.
    1654  *
    1655  * @since 4.3.0
    1656  *
    1657  * @see wp_get_nav_menu_object()
    1658  * @see WP_Customize_Setting
    1659  */
    1660 class WP_Customize_Nav_Menu_Setting extends WP_Customize_Setting {
    1661 
    1662     const ID_PATTERN = '/^nav_menu\[(?P<id>-?\d+)\]$/';
    1663 
    1664     const TAXONOMY = 'nav_menu';
    1665 
    1666     const TYPE = 'nav_menu';
    1667 
    1668     /**
    1669      * Setting type.
    1670      *
    1671      * @since 4.3.0
    1672      * @access public
    1673      * @var string
    1674      */
    1675     public $type = self::TYPE;
    1676 
    1677     /**
    1678      * Default setting value.
    1679      *
    1680      * @since 4.3.0
    1681      * @access public
    1682      * @var array
    1683      *
    1684      * @see wp_get_nav_menu_object()
    1685      */
    1686     public $default = array(
    1687         'name'        => '',
    1688         'description' => '',
    1689         'parent'      => 0,
    1690         'auto_add'    => false,
    1691     );
    1692 
    1693     /**
    1694      * Default transport.
    1695      *
    1696      * @since 4.3.0
    1697      * @access public
    1698      * @var string
    1699      */
    1700     public $transport = 'postMessage';
    1701 
    1702     /**
    1703      * The term ID represented by this setting instance.
    1704      *
    1705      * A negative value represents a placeholder ID for a new menu not yet saved.
    1706      *
    1707      * @since 4.3.0
    1708      * @access public
    1709      * @var int
    1710      */
    1711     public $term_id;
    1712 
    1713     /**
    1714      * Previous (placeholder) term ID used before creating a new menu.
    1715      *
    1716      * This value will be exported to JS via the customize_save_response filter
    1717      * so that JavaScript can update the settings to refer to the newly-assigned
    1718      * term ID. This value is always negative to indicate it does not refer to
    1719      * a real term.
    1720      *
    1721      * @since 4.3.0
    1722      * @access public
    1723      * @var int
    1724      *
    1725      * @see WP_Customize_Nav_Menu_Setting::update()
    1726      * @see WP_Customize_Nav_Menu_Setting::amend_customize_save_response()
    1727      */
    1728     public $previous_term_id;
    1729 
    1730     /**
    1731      * Whether or not preview() was called.
    1732      *
    1733      * @since 4.3.0
    1734      * @access protected
    1735      * @var bool
    1736      */
    1737     protected $is_previewed = false;
    1738 
    1739     /**
    1740      * Whether or not update() was called.
    1741      *
    1742      * @since 4.3.0
    1743      * @access protected
    1744      * @var bool
    1745      */
    1746     protected $is_updated = false;
    1747 
    1748     /**
    1749      * Status for calling the update method, used in customize_save_response filter.
    1750      *
    1751      * When status is inserted, the placeholder term ID is stored in $previous_term_id.
    1752      * When status is error, the error is stored in $update_error.
    1753      *
    1754      * @since 4.3.0
    1755      * @access public
    1756      * @var string updated|inserted|deleted|error
    1757      *
    1758      * @see WP_Customize_Nav_Menu_Setting::update()
    1759      * @see WP_Customize_Nav_Menu_Setting::amend_customize_save_response()
    1760      */
    1761     public $update_status;
    1762 
    1763     /**
    1764      * Any error object returned by wp_update_nav_menu_object() when setting is updated.
    1765      *
    1766      * @since 4.3.0
    1767      * @access public
    1768      * @var WP_Error
    1769      *
    1770      * @see WP_Customize_Nav_Menu_Setting::update()
    1771      * @see WP_Customize_Nav_Menu_Setting::amend_customize_save_response()
    1772      */
    1773     public $update_error;
    1774 
    1775     /**
    1776      * Constructor.
    1777      *
    1778      * Any supplied $args override class property defaults.
    1779      *
    1780      * @since 4.3.0
    1781      * @access public
    1782      *
    1783      * @param WP_Customize_Manager $manager Bootstrap Customizer instance.
    1784      * @param string               $id      An specific ID of the setting. Can be a
    1785      *                                      theme mod or option name.
    1786      * @param array                $args    Optional. Setting arguments.
    1787      *
    1788      * @throws Exception If $id is not valid for this setting type.
    1789      */
    1790     public function __construct( WP_Customize_Manager $manager, $id, array $args = array() ) {
    1791         if ( empty( $manager->nav_menus ) ) {
    1792             throw new Exception( 'Expected WP_Customize_Manager::$nav_menus to be set.' );
    1793         }
    1794 
    1795         if ( ! preg_match( self::ID_PATTERN, $id, $matches ) ) {
    1796             throw new Exception( "Illegal widget setting ID: $id" );
    1797         }
    1798 
    1799         $this->term_id = intval( $matches['id'] );
    1800 
    1801         parent::__construct( $manager, $id, $args );
    1802     }
    1803 
    1804     /**
    1805      * Get the instance data for a given widget setting.
    1806      *
    1807      * @since 4.3.0
    1808      * @access public
    1809      *
    1810      * @see wp_get_nav_menu_object()
    1811      *
    1812      * @return array Instance data.
    1813      */
    1814     public function value() {
    1815         if ( $this->is_previewed && $this->_previewed_blog_id === get_current_blog_id() ) {
    1816             $undefined  = new stdClass(); // Symbol.
    1817             $post_value = $this->post_value( $undefined );
    1818 
    1819             if ( $undefined === $post_value ) {
    1820                 $value = $this->_original_value;
    1821             } else {
    1822                 $value = $post_value;
    1823             }
    1824         } else {
    1825             $value = false;
    1826 
    1827             // Note that a term_id of less than one indicates a nav_menu not yet inserted.
    1828             if ( $this->term_id > 0 ) {
    1829                 $term = wp_get_nav_menu_object( $this->term_id );
    1830 
    1831                 if ( $term ) {
    1832                     $value = wp_array_slice_assoc( (array) $term, array_keys( $this->default ) );
    1833 
    1834                     $nav_menu_options  = (array) get_option( 'nav_menu_options', array() );
    1835                     $value['auto_add'] = false;
    1836 
    1837                     if ( isset( $nav_menu_options['auto_add'] ) && is_array( $nav_menu_options['auto_add'] ) ) {
    1838                         $value['auto_add'] = in_array( $term->term_id, $nav_menu_options['auto_add'] );
    1839                     }
    1840                 }
    1841             }
    1842 
    1843             if ( ! is_array( $value ) ) {
    1844                 $value = $this->default;
    1845             }
    1846         }
    1847         return $value;
    1848     }
    1849 
    1850     /**
    1851      * Handle previewing the setting.
    1852      *
    1853      * @since 4.3.0
    1854      * @since 4.4.0 Added boolean return value
    1855      * @access public
    1856      *
    1857      * @see WP_Customize_Manager::post_value()
    1858      *
    1859      * @return bool False if method short-circuited due to no-op.
    1860      */
    1861     public function preview() {
    1862         if ( $this->is_previewed ) {
    1863             return false;
    1864         }
    1865 
    1866         $undefined = new stdClass();
    1867         $is_placeholder = ( $this->term_id < 0 );
    1868         $is_dirty = ( $undefined !== $this->post_value( $undefined ) );
    1869         if ( ! $is_placeholder && ! $is_dirty ) {
    1870             return false;
    1871         }
    1872 
    1873         $this->is_previewed       = true;
    1874         $this->_original_value    = $this->value();
    1875         $this->_previewed_blog_id = get_current_blog_id();
    1876 
    1877         add_filter( 'wp_get_nav_menus', array( $this, 'filter_wp_get_nav_menus' ), 10, 2 );
    1878         add_filter( 'wp_get_nav_menu_object', array( $this, 'filter_wp_get_nav_menu_object' ), 10, 2 );
    1879         add_filter( 'default_option_nav_menu_options', array( $this, 'filter_nav_menu_options' ) );
    1880         add_filter( 'option_nav_menu_options', array( $this, 'filter_nav_menu_options' ) );
    1881 
    1882         return true;
    1883     }
    1884 
    1885     /**
    1886      * Filter the wp_get_nav_menus() result to ensure the inserted menu object is included, and the deleted one is removed.
    1887      *
    1888      * @since 4.3.0
    1889      * @access public
    1890      *
    1891      * @see wp_get_nav_menus()
    1892      *
    1893      * @param array $menus An array of menu objects.
    1894      * @param array $args  An array of arguments used to retrieve menu objects.
    1895      * @return array
    1896      */
    1897     public function filter_wp_get_nav_menus( $menus, $args ) {
    1898         if ( get_current_blog_id() !== $this->_previewed_blog_id ) {
    1899             return $menus;
    1900         }
    1901 
    1902         $setting_value = $this->value();
    1903         $is_delete = ( false === $setting_value );
    1904         $index = -1;
    1905 
    1906         // Find the existing menu item's position in the list.
    1907         foreach ( $menus as $i => $menu ) {
    1908             if ( (int) $this->term_id === (int) $menu->term_id || (int) $this->previous_term_id === (int) $menu->term_id ) {
    1909                 $index = $i;
    1910                 break;
    1911             }
    1912         }
    1913 
    1914         if ( $is_delete ) {
    1915             // Handle deleted menu by removing it from the list.
    1916             if ( -1 !== $index ) {
    1917                 array_splice( $menus, $index, 1 );
    1918             }
    1919         } else {
    1920             // Handle menus being updated or inserted.
    1921             $menu_obj = (object) array_merge( array(
    1922                 'term_id'          => $this->term_id,
    1923                 'term_taxonomy_id' => $this->term_id,
    1924                 'slug'             => sanitize_title( $setting_value['name'] ),
    1925                 'count'            => 0,
    1926                 'term_group'       => 0,
    1927                 'taxonomy'         => self::TAXONOMY,
    1928                 'filter'           => 'raw',
    1929             ), $setting_value );
    1930 
    1931             array_splice( $menus, $index, ( -1 === $index ? 0 : 1 ), array( $menu_obj ) );
    1932         }
    1933 
    1934         // Make sure the menu objects get re-sorted after an update/insert.
    1935         if ( ! $is_delete && ! empty( $args['orderby'] ) ) {
    1936             $this->_current_menus_sort_orderby = $args['orderby'];
    1937             usort( $menus, array( $this, '_sort_menus_by_orderby' ) );
    1938         }
    1939         // @todo add support for $args['hide_empty'] === true
    1940 
    1941         return $menus;
    1942     }
    1943 
    1944     /**
    1945      * Temporary non-closure passing of orderby value to function.
    1946      *
    1947      * @since 4.3.0
    1948      * @access protected
    1949      * @var string
    1950      *
    1951      * @see WP_Customize_Nav_Menu_Setting::filter_wp_get_nav_menus()
    1952      * @see WP_Customize_Nav_Menu_Setting::_sort_menus_by_orderby()
    1953      */
    1954     protected $_current_menus_sort_orderby;
    1955 
    1956     /**
    1957      * Sort menu objects by the class-supplied orderby property.
    1958      *
    1959      * This is a workaround for a lack of closures.
    1960      *
    1961      * @since 4.3.0
    1962      * @access protected
    1963      * @param object $menu1
    1964      * @param object $menu2
    1965      * @return int
    1966      *
    1967      * @see WP_Customize_Nav_Menu_Setting::filter_wp_get_nav_menus()
    1968      */
    1969     protected function _sort_menus_by_orderby( $menu1, $menu2 ) {
    1970         $key = $this->_current_menus_sort_orderby;
    1971         return strcmp( $menu1->$key, $menu2->$key );
    1972     }
    1973 
    1974     /**
    1975      * Filter the wp_get_nav_menu_object() result to supply the previewed menu object.
    1976      *
    1977      * Requesting a nav_menu object by anything but ID is not supported.
    1978      *
    1979      * @since 4.3.0
    1980      * @access public
    1981      *
    1982      * @see wp_get_nav_menu_object()
    1983      *
    1984      * @param object|null $menu_obj Object returned by wp_get_nav_menu_object().
    1985      * @param string      $menu_id  ID of the nav_menu term. Requests by slug or name will be ignored.
    1986      * @return object|null
    1987      */
    1988     public function filter_wp_get_nav_menu_object( $menu_obj, $menu_id ) {
    1989         $ok = (
    1990             get_current_blog_id() === $this->_previewed_blog_id
    1991             &&
    1992             is_int( $menu_id )
    1993             &&
    1994             $menu_id === $this->term_id
    1995         );
    1996         if ( ! $ok ) {
    1997             return $menu_obj;
    1998         }
    1999 
    2000         $setting_value = $this->value();
    2001 
    2002         // Handle deleted menus.
    2003         if ( false === $setting_value ) {
    2004             return false;
    2005         }
    2006 
    2007         // Handle sanitization failure by preventing short-circuiting.
    2008         if ( null === $setting_value ) {
    2009             return $menu_obj;
    2010         }
    2011 
    2012         $menu_obj = (object) array_merge( array(
    2013                 'term_id'          => $this->term_id,
    2014                 'term_taxonomy_id' => $this->term_id,
    2015                 'slug'             => sanitize_title( $setting_value['name'] ),
    2016                 'count'            => 0,
    2017                 'term_group'       => 0,
    2018                 'taxonomy'         => self::TAXONOMY,
    2019                 'filter'           => 'raw',
    2020             ), $setting_value );
    2021 
    2022         return $menu_obj;
    2023     }
    2024 
    2025     /**
    2026      * Filter the nav_menu_options option to include this menu's auto_add preference.
    2027      *
    2028      * @since 4.3.0
    2029      * @access public
    2030      *
    2031      * @param array $nav_menu_options Nav menu options including auto_add.
    2032      * @return array (Kaybe) modified nav menu options.
    2033      */
    2034     public function filter_nav_menu_options( $nav_menu_options ) {
    2035         if ( $this->_previewed_blog_id !== get_current_blog_id() ) {
    2036             return $nav_menu_options;
    2037         }
    2038 
    2039         $menu = $this->value();
    2040         $nav_menu_options = $this->filter_nav_menu_options_value(
    2041             $nav_menu_options,
    2042             $this->term_id,
    2043             false === $menu ? false : $menu['auto_add']
    2044         );
    2045 
    2046         return $nav_menu_options;
    2047     }
    2048 
    2049     /**
    2050      * Sanitize an input.
    2051      *
    2052      * Note that parent::sanitize() erroneously does wp_unslash() on $value, but
    2053      * we remove that in this override.
    2054      *
    2055      * @since 4.3.0
    2056      * @access public
    2057      *
    2058      * @param array $value The value to sanitize.
    2059      * @return array|false|null Null if an input isn't valid. False if it is marked for deletion.
    2060      *                          Otherwise the sanitized value.
    2061      */
    2062     public function sanitize( $value ) {
    2063         // Menu is marked for deletion.
    2064         if ( false === $value ) {
    2065             return $value;
    2066         }
    2067 
    2068         // Invalid.
    2069         if ( ! is_array( $value ) ) {
    2070             return null;
    2071         }
    2072 
    2073         $default = array(
    2074             'name'        => '',
    2075             'description' => '',
    2076             'parent'      => 0,
    2077             'auto_add'    => false,
    2078         );
    2079         $value = array_merge( $default, $value );
    2080         $value = wp_array_slice_assoc( $value, array_keys( $default ) );
    2081 
    2082         $value['name']        = trim( esc_html( $value['name'] ) ); // This sanitization code is used in wp-admin/nav-menus.php.
    2083         $value['description'] = sanitize_text_field( $value['description'] );
    2084         $value['parent']      = max( 0, intval( $value['parent'] ) );
    2085         $value['auto_add']    = ! empty( $value['auto_add'] );
    2086 
    2087         if ( '' === $value['name'] ) {
    2088             $value['name'] = _x( '(unnamed)', 'Missing menu name.' );
    2089         }
    2090 
    2091         /** This filter is documented in wp-includes/class-wp-customize-setting.php */
    2092         return apply_filters( "customize_sanitize_{$this->id}", $value, $this );
    2093     }
    2094 
    2095     /**
    2096      * Storage for data to be sent back to client in customize_save_response filter.
    2097      *
    2098      * @access protected
    2099      * @since 4.3.0
    2100      * @var array
    2101      *
    2102      * @see WP_Customize_Nav_Menu_Setting::amend_customize_save_response()
    2103      */
    2104     protected $_widget_nav_menu_updates = array();
    2105 
    2106     /**
    2107      * Create/update the nav_menu term for this setting.
    2108      *
    2109      * Any created menus will have their assigned term IDs exported to the client
    2110      * via the customize_save_response filter. Likewise, any errors will be exported
    2111      * to the client via the customize_save_response() filter.
    2112      *
    2113      * To delete a menu, the client can send false as the value.
    2114      *
    2115      * @since 4.3.0
    2116      * @access protected
    2117      *
    2118      * @see wp_update_nav_menu_object()
    2119      *
    2120      * @param array|false $value {
    2121      *     The value to update. Note that slug cannot be updated via wp_update_nav_menu_object().
    2122      *     If false, then the menu will be deleted entirely.
    2123      *
    2124      *     @type string $name        The name of the menu to save.
    2125      *     @type string $description The term description. Default empty string.
    2126      *     @type int    $parent      The id of the parent term. Default 0.
    2127      *     @type bool   $auto_add    Whether pages will auto_add to this menu. Default false.
    2128      * }
    2129      * @return null|void
    2130      */
    2131     protected function update( $value ) {
    2132         if ( $this->is_updated ) {
    2133             return;
    2134         }
    2135 
    2136         $this->is_updated = true;
    2137         $is_placeholder   = ( $this->term_id < 0 );
    2138         $is_delete        = ( false === $value );
    2139 
    2140         add_filter( 'customize_save_response', array( $this, 'amend_customize_save_response' ) );
    2141 
    2142         $auto_add = null;
    2143         if ( $is_delete ) {
    2144             // If the current setting term is a placeholder, a delete request is a no-op.
    2145             if ( $is_placeholder ) {
    2146                 $this->update_status = 'deleted';
    2147             } else {
    2148                 $r = wp_delete_nav_menu( $this->term_id );
    2149 
    2150                 if ( is_wp_error( $r ) ) {
    2151                     $this->update_status = 'error';
    2152                     $this->update_error  = $r;
    2153                 } else {
    2154                     $this->update_status = 'deleted';
    2155                     $auto_add = false;
    2156                 }
    2157             }
    2158         } else {
    2159             // Insert or update menu.
    2160             $menu_data = wp_array_slice_assoc( $value, array( 'description', 'parent' ) );
    2161             $menu_data['menu-name'] = $value['name'];
    2162 
    2163             $menu_id = $is_placeholder ? 0 : $this->term_id;
    2164             $r = wp_update_nav_menu_object( $menu_id, $menu_data );
    2165             $original_name = $menu_data['menu-name'];
    2166             $name_conflict_suffix = 1;
    2167             while ( is_wp_error( $r ) && 'menu_exists' === $r->get_error_code() ) {
    2168                 $name_conflict_suffix += 1;
    2169                 /* translators: 1: original menu name, 2: duplicate count */
    2170                 $menu_data['menu-name'] = sprintf( __( '%1$s (%2$d)' ), $original_name, $name_conflict_suffix );
    2171                 $r = wp_update_nav_menu_object( $menu_id, $menu_data );
    2172             }
    2173 
    2174             if ( is_wp_error( $r ) ) {
    2175                 $this->update_status = 'error';
    2176                 $this->update_error  = $r;
    2177             } else {
    2178                 if ( $is_placeholder ) {
    2179                     $this->previous_term_id = $this->term_id;
    2180                     $this->term_id          = $r;
    2181                     $this->update_status    = 'inserted';
    2182                 } else {
    2183                     $this->update_status = 'updated';
    2184                 }
    2185 
    2186                 $auto_add = $value['auto_add'];
    2187             }
    2188         }
    2189 
    2190         if ( null !== $auto_add ) {
    2191             $nav_menu_options = $this->filter_nav_menu_options_value(
    2192                 (array) get_option( 'nav_menu_options', array() ),
    2193                 $this->term_id,
    2194                 $auto_add
    2195             );
    2196             update_option( 'nav_menu_options', $nav_menu_options );
    2197         }
    2198 
    2199         if ( 'inserted' === $this->update_status ) {
    2200             // Make sure that new menus assigned to nav menu locations use their new IDs.
    2201             foreach ( $this->manager->settings() as $setting ) {
    2202                 if ( ! preg_match( '/^nav_menu_locations\[/', $setting->id ) ) {
    2203                     continue;
    2204                 }
    2205 
    2206                 $post_value = $setting->post_value( null );
    2207                 if ( ! is_null( $post_value ) && $this->previous_term_id === intval( $post_value ) ) {
    2208                     $this->manager->set_post_value( $setting->id, $this->term_id );
    2209                     $setting->save();
    2210                 }
    2211             }
    2212 
    2213             // Make sure that any nav_menu widgets referencing the placeholder nav menu get updated and sent back to client.
    2214             foreach ( array_keys( $this->manager->unsanitized_post_values() ) as $setting_id ) {
    2215                 $nav_menu_widget_setting = $this->manager->get_setting( $setting_id );
    2216                 if ( ! $nav_menu_widget_setting || ! preg_match( '/^widget_nav_menu\[/', $nav_menu_widget_setting->id ) ) {
    2217                     continue;
    2218                 }
    2219 
    2220                 $widget_instance = $nav_menu_widget_setting->post_value(); // Note that this calls WP_Customize_Widgets::sanitize_widget_instance().
    2221                 if ( empty( $widget_instance['nav_menu'] ) || intval( $widget_instance['nav_menu'] ) !== $this->previous_term_id ) {
    2222                     continue;
    2223                 }
    2224 
    2225                 $widget_instance['nav_menu'] = $this->term_id;
    2226                 $updated_widget_instance = $this->manager->widgets->sanitize_widget_js_instance( $widget_instance );
    2227                 $this->manager->set_post_value( $nav_menu_widget_setting->id, $updated_widget_instance );
    2228                 $nav_menu_widget_setting->save();
    2229 
    2230                 $this->_widget_nav_menu_updates[ $nav_menu_widget_setting->id ] = $updated_widget_instance;
    2231             }
    2232         }
    2233     }
    2234 
    2235     /**
    2236      * Updates a nav_menu_options array.
    2237      *
    2238      * @since 4.3.0
    2239      * @access protected
    2240      *
    2241      * @see WP_Customize_Nav_Menu_Setting::filter_nav_menu_options()
    2242      * @see WP_Customize_Nav_Menu_Setting::update()
    2243      *
    2244      * @param array $nav_menu_options Array as returned by get_option( 'nav_menu_options' ).
    2245      * @param int   $menu_id          The term ID for the given menu.
    2246      * @param bool  $auto_add         Whether to auto-add or not.
    2247      * @return array (Maybe) modified nav_menu_otions array.
    2248      */
    2249     protected function filter_nav_menu_options_value( $nav_menu_options, $menu_id, $auto_add ) {
    2250         $nav_menu_options = (array) $nav_menu_options;
    2251         if ( ! isset( $nav_menu_options['auto_add'] ) ) {
    2252             $nav_menu_options['auto_add'] = array();
    2253         }
    2254 
    2255         $i = array_search( $menu_id, $nav_menu_options['auto_add'] );
    2256         if ( $auto_add && false === $i ) {
    2257             array_push( $nav_menu_options['auto_add'], $this->term_id );
    2258         } elseif ( ! $auto_add && false !== $i ) {
    2259             array_splice( $nav_menu_options['auto_add'], $i, 1 );
    2260         }
    2261 
    2262         return $nav_menu_options;
    2263     }
    2264 
    2265     /**
    2266      * Export data for the JS client.
    2267      *
    2268      * @since 4.3.0
    2269      * @access public
    2270      *
    2271      * @see WP_Customize_Nav_Menu_Setting::update()
    2272      *
    2273      * @param array $data Additional information passed back to the 'saved' event on `wp.customize`.
    2274      * @return array Export data.
    2275      */
    2276     public function amend_customize_save_response( $data ) {
    2277         if ( ! isset( $data['nav_menu_updates'] ) ) {
    2278             $data['nav_menu_updates'] = array();
    2279         }
    2280         if ( ! isset( $data['widget_nav_menu_updates'] ) ) {
    2281             $data['widget_nav_menu_updates'] = array();
    2282         }
    2283 
    2284         $data['nav_menu_updates'][] = array(
    2285             'term_id'          => $this->term_id,
    2286             'previous_term_id' => $this->previous_term_id,
    2287             'error'            => $this->update_error ? $this->update_error->get_error_code() : null,
    2288             'status'           => $this->update_status,
    2289             'saved_value'      => 'deleted' === $this->update_status ? null : $this->value(),
    2290         );
    2291 
    2292         $data['widget_nav_menu_updates'] = array_merge(
    2293             $data['widget_nav_menu_updates'],
    2294             $this->_widget_nav_menu_updates
    2295         );
    2296         $this->_widget_nav_menu_updates = array();
    2297 
    2298         return $data;
    2299     }
    2300 }
  • trunk/src/wp-includes/customize/class-wp-customize-filter-setting.php

    r35381 r35383  
    55 * @package WordPress
    66 * @subpackage Customize
    7  * @since 3.4.0
     7 * @since 4.4.0
    88 */
    9 
    10 /**
    11  * Customize Setting class.
    12  *
    13  * Handles saving and sanitizing of settings.
    14  *
    15  * @since 3.4.0
    16  *
    17  * @see WP_Customize_Manager
    18  */
    19 class WP_Customize_Setting {
    20     /**
    21      * @access public
    22      * @var WP_Customize_Manager
    23      */
    24     public $manager;
    25 
    26     /**
    27      * Unique string identifier for the setting.
    28      *
    29      * @access public
    30      * @var string
    31      */
    32     public $id;
    33 
    34     /**
    35      * @access public
    36      * @var string
    37      */
    38     public $type = 'theme_mod';
    39 
    40     /**
    41      * Capability required to edit this setting.
    42      *
    43      * @var string
    44      */
    45     public $capability = 'edit_theme_options';
    46 
    47     /**
    48      * Feature a theme is required to support to enable this setting.
    49      *
    50      * @access public
    51      * @var string
    52      */
    53     public $theme_supports  = '';
    54     public $default         = '';
    55     public $transport       = 'refresh';
    56 
    57     /**
    58      * Server-side sanitization callback for the setting's value.
    59      *
    60      * @var callback
    61      */
    62     public $sanitize_callback    = '';
    63     public $sanitize_js_callback = '';
    64 
    65     /**
    66      * Whether or not the setting is initially dirty when created.
    67      *
    68      * This is used to ensure that a setting will be sent from the pane to the
    69      * preview when loading the Customizer. Normally a setting only is synced to
    70      * the preview if it has been changed. This allows the setting to be sent
    71      * from the start.
    72      *
    73      * @since 4.2.0
    74      * @access public
    75      * @var bool
    76      */
    77     public $dirty = false;
    78 
    79     /**
    80      * @var array
    81      */
    82     protected $id_data = array();
    83 
    84     /**
    85      * Cache of multidimensional values to improve performance.
    86      *
    87      * @since 4.4.0
    88      * @access protected
    89      * @var array
    90      * @static
    91      */
    92     protected static $aggregated_multidimensionals = array();
    93 
    94     /**
    95      * Whether the multidimensional setting is aggregated.
    96      *
    97      * @since 4.4.0
    98      * @access protected
    99      * @var bool
    100      */
    101     protected $is_multidimensional_aggregated = false;
    102 
    103     /**
    104      * Constructor.
    105      *
    106      * Any supplied $args override class property defaults.
    107      *
    108      * @since 3.4.0
    109      *
    110      * @param WP_Customize_Manager $manager
    111      * @param string               $id      An specific ID of the setting. Can be a
    112      *                                      theme mod or option name.
    113      * @param array                $args    Setting arguments.
    114      */
    115     public function __construct( $manager, $id, $args = array() ) {
    116         $keys = array_keys( get_object_vars( $this ) );
    117         foreach ( $keys as $key ) {
    118             if ( isset( $args[ $key ] ) ) {
    119                 $this->$key = $args[ $key ];
    120             }
    121         }
    122 
    123         $this->manager = $manager;
    124         $this->id = $id;
    125 
    126         // Parse the ID for array keys.
    127         $this->id_data['keys'] = preg_split( '/\[/', str_replace( ']', '', $this->id ) );
    128         $this->id_data['base'] = array_shift( $this->id_data['keys'] );
    129 
    130         // Rebuild the ID.
    131         $this->id = $this->id_data[ 'base' ];
    132         if ( ! empty( $this->id_data[ 'keys' ] ) ) {
    133             $this->id .= '[' . implode( '][', $this->id_data['keys'] ) . ']';
    134         }
    135 
    136         if ( $this->sanitize_callback ) {
    137             add_filter( "customize_sanitize_{$this->id}", $this->sanitize_callback, 10, 2 );
    138         }
    139         if ( $this->sanitize_js_callback ) {
    140             add_filter( "customize_sanitize_js_{$this->id}", $this->sanitize_js_callback, 10, 2 );
    141         }
    142 
    143         if ( 'option' === $this->type || 'theme_mod' === $this->type ) {
    144             // Other setting types can opt-in to aggregate multidimensional explicitly.
    145             $this->aggregate_multidimensional();
    146 
    147             // Allow option settings to indicate whether they should be autoloaded.
    148             if ( 'option' === $this->type && isset( $args['autoload'] ) ) {
    149                 self::$aggregated_multidimensionals[ $this->type ][ $this->id_data['base'] ]['autoload'] = $args['autoload'];
    150             }
    151         }
    152     }
    153 
    154     /**
    155      * Get parsed ID data for multidimensional setting.
    156      *
    157      * @since 4.4.0
    158      * @access public
    159      *
    160      * @return array {
    161      *     ID data for multidimensional setting.
    162      *
    163      *     @type string $base ID base
    164      *     @type array  $keys Keys for multidimensional array.
    165      * }
    166      */
    167     final public function id_data() {
    168         return $this->id_data;
    169     }
    170 
    171     /**
    172      * Set up the setting for aggregated multidimensional values.
    173      *
    174      * When a multidimensional setting gets aggregated, all of its preview and update
    175      * calls get combined into one call, greatly improving performance.
    176      *
    177      * @since 4.4.0
    178      * @access protected
    179      */
    180     protected function aggregate_multidimensional() {
    181         $id_base = $this->id_data['base'];
    182         if ( ! isset( self::$aggregated_multidimensionals[ $this->type ] ) ) {
    183             self::$aggregated_multidimensionals[ $this->type ] = array();
    184         }
    185         if ( ! isset( self::$aggregated_multidimensionals[ $this->type ][ $id_base ] ) ) {
    186             self::$aggregated_multidimensionals[ $this->type ][ $id_base ] = array(
    187                 'previewed_instances'       => array(), // Calling preview() will add the $setting to the array.
    188                 'preview_applied_instances' => array(), // Flags for which settings have had their values applied.
    189                 'root_value'                => $this->get_root_value( array() ), // Root value for initial state, manipulated by preview and update calls.
    190             );
    191         }
    192 
    193         if ( ! empty( $this->id_data['keys'] ) ) {
    194             $this->is_multidimensional_aggregated = true;
    195         }
    196     }
    197 
    198     /**
    199      * The ID for the current blog when the preview() method was called.
    200      *
    201      * @since 4.2.0
    202      * @access protected
    203      * @var int
    204      */
    205     protected $_previewed_blog_id;
    206 
    207     /**
    208      * Return true if the current blog is not the same as the previewed blog.
    209      *
    210      * @since 4.2.0
    211      * @access public
    212      *
    213      * @return bool If preview() has been called.
    214      */
    215     public function is_current_blog_previewed() {
    216         if ( ! isset( $this->_previewed_blog_id ) ) {
    217             return false;
    218         }
    219         return ( get_current_blog_id() === $this->_previewed_blog_id );
    220     }
    221 
    222     /**
    223      * Original non-previewed value stored by the preview method.
    224      *
    225      * @see WP_Customize_Setting::preview()
    226      * @since 4.1.1
    227      * @var mixed
    228      */
    229     protected $_original_value;
    230 
    231     /**
    232      * Add filters to supply the setting's value when accessed.
    233      *
    234      * If the setting already has a pre-existing value and there is no incoming
    235      * post value for the setting, then this method will short-circuit since
    236      * there is no change to preview.
    237      *
    238      * @since 3.4.0
    239      * @since 4.4.0 Added boolean return value.
    240      * @access public
    241      *
    242      * @return bool False when preview short-circuits due no change needing to be previewed.
    243      */
    244     public function preview() {
    245         if ( ! isset( $this->_previewed_blog_id ) ) {
    246             $this->_previewed_blog_id = get_current_blog_id();
    247         }
    248         $id_base = $this->id_data['base'];
    249         $is_multidimensional = ! empty( $this->id_data['keys'] );
    250         $multidimensional_filter = array( $this, '_multidimensional_preview_filter' );
    251 
    252         /*
    253          * Check if the setting has a pre-existing value (an isset check),
    254          * and if doesn't have any incoming post value. If both checks are true,
    255          * then the preview short-circuits because there is nothing that needs
    256          * to be previewed.
    257          */
    258         $undefined = new stdClass();
    259         $needs_preview = ( $undefined !== $this->post_value( $undefined ) );
    260         $value = null;
    261 
    262         // Since no post value was defined, check if we have an initial value set.
    263         if ( ! $needs_preview ) {
    264             if ( $this->is_multidimensional_aggregated ) {
    265                 $root = self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['root_value'];
    266                 $value = $this->multidimensional_get( $root, $this->id_data['keys'], $undefined );
    267             } else {
    268                 $default = $this->default;
    269                 $this->default = $undefined; // Temporarily set default to undefined so we can detect if existing value is set.
    270                 $value = $this->value();
    271                 $this->default = $default;
    272             }
    273             $needs_preview = ( $undefined === $value ); // Because the default needs to be supplied.
    274         }
    275 
    276         if ( ! $needs_preview ) {
    277             return false;
    278         }
    279 
    280         switch ( $this->type ) {
    281             case 'theme_mod' :
    282                 if ( ! $is_multidimensional ) {
    283                     add_filter( "theme_mod_{$id_base}", array( $this, '_preview_filter' ) );
    284                 } else {
    285                     if ( empty( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['previewed_instances'] ) ) {
    286                         // Only add this filter once for this ID base.
    287                         add_filter( "theme_mod_{$id_base}", $multidimensional_filter );
    288                     }
    289                     self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['previewed_instances'][ $this->id ] = $this;
    290                 }
    291                 break;
    292             case 'option' :
    293                 if ( ! $is_multidimensional ) {
    294                     add_filter( "pre_option_{$id_base}", array( $this, '_preview_filter' ) );
    295                 } else {
    296                     if ( empty( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['previewed_instances'] ) ) {
    297                         // Only add these filters once for this ID base.
    298                         add_filter( "option_{$id_base}", $multidimensional_filter );
    299                         add_filter( "default_option_{$id_base}", $multidimensional_filter );
    300                     }
    301                     self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['previewed_instances'][ $this->id ] = $this;
    302                 }
    303                 break;
    304             default :
    305 
    306                 /**
    307                  * Fires when the {@see WP_Customize_Setting::preview()} method is called for settings
    308                  * not handled as theme_mods or options.
    309                  *
    310                  * The dynamic portion of the hook name, `$this->id`, refers to the setting ID.
    311                  *
    312                  * @since 3.4.0
    313                  *
    314                  * @param WP_Customize_Setting $this {@see WP_Customize_Setting} instance.
    315                  */
    316                 do_action( "customize_preview_{$this->id}", $this );
    317 
    318                 /**
    319                  * Fires when the {@see WP_Customize_Setting::preview()} method is called for settings
    320                  * not handled as theme_mods or options.
    321                  *
    322                  * The dynamic portion of the hook name, `$this->type`, refers to the setting type.
    323                  *
    324                  * @since 4.1.0
    325                  *
    326                  * @param WP_Customize_Setting $this {@see WP_Customize_Setting} instance.
    327                  */
    328                 do_action( "customize_preview_{$this->type}", $this );
    329         }
    330         return true;
    331     }
    332 
    333     /**
    334      * Callback function to filter non-multidimensional theme mods and options.
    335      *
    336      * If switch_to_blog() was called after the preview() method, and the current
    337      * blog is now not the same blog, then this method does a no-op and returns
    338      * the original value.
    339      *
    340      * @since 3.4.0
    341      *
    342      * @param mixed $original Old value.
    343      * @return mixed New or old value.
    344      */
    345     public function _preview_filter( $original ) {
    346         if ( ! $this->is_current_blog_previewed() ) {
    347             return $original;
    348         }
    349 
    350         $undefined = new stdClass(); // Symbol hack.
    351         $post_value = $this->post_value( $undefined );
    352         if ( $undefined !== $post_value ) {
    353             $value = $post_value;
    354         } else {
    355             /*
    356              * Note that we don't use $original here because preview() will
    357              * not add the filter in the first place if it has an initial value
    358              * and there is no post value.
    359              */
    360             $value = $this->default;
    361         }
    362         return $value;
    363     }
    364 
    365     /**
    366      * Callback function to filter multidimensional theme mods and options.
    367      *
    368      * For all multidimensional settings of a given type, the preview filter for
    369      * the first setting previewed will be used to apply the values for the others.
    370      *
    371      * @since 4.4.0
    372      * @access public
    373      *
    374      * @see WP_Customize_Setting::$aggregated_multidimensionals
    375      * @param mixed $original Original root value.
    376      * @return mixed New or old value.
    377      */
    378     public function _multidimensional_preview_filter( $original ) {
    379         if ( ! $this->is_current_blog_previewed() ) {
    380             return $original;
    381         }
    382 
    383         $id_base = $this->id_data['base'];
    384 
    385         // If no settings have been previewed yet (which should not be the case, since $this is), just pass through the original value.
    386         if ( empty( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['previewed_instances'] ) ) {
    387             return $original;
    388         }
    389 
    390         foreach ( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['previewed_instances'] as $previewed_setting ) {
    391             // Skip applying previewed value for any settings that have already been applied.
    392             if ( ! empty( self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['preview_applied_instances'][ $previewed_setting->id ] ) ) {
    393                 continue;
    394             }
    395 
    396             // Do the replacements of the posted/default sub value into the root value.
    397             $value = $previewed_setting->post_value( $previewed_setting->default );
    398             $root = self::$aggregated_multidimensionals[ $previewed_setting->type ][ $id_base ]['root_value'];
    399             $root = $previewed_setting->multidimensional_replace( $root, $previewed_setting->id_data['keys'], $value );
    400             self::$aggregated_multidimensionals[ $previewed_setting->type ][ $id_base ]['root_value'] = $root;
    401 
    402             // Mark this setting having been applied so that it will be skipped when the filter is called again.
    403             self::$aggregated_multidimensionals[ $previewed_setting->type ][ $id_base ]['preview_applied_instances'][ $previewed_setting->id ] = true;
    404         }
    405 
    406         return self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['root_value'];
    407     }
    408 
    409     /**
    410      * Check user capabilities and theme supports, and then save
    411      * the value of the setting.
    412      *
    413      * @since 3.4.0
    414      *
    415      * @return false|void False if cap check fails or value isn't set.
    416      */
    417     final public function save() {
    418         $value = $this->post_value();
    419 
    420         if ( ! $this->check_capabilities() || ! isset( $value ) )
    421             return false;
    422 
    423         /**
    424          * Fires when the WP_Customize_Setting::save() method is called.
    425          *
    426          * The dynamic portion of the hook name, `$this->id_data['base']` refers to
    427          * the base slug of the setting name.
    428          *
    429          * @since 3.4.0
    430          *
    431          * @param WP_Customize_Setting $this {@see WP_Customize_Setting} instance.
    432          */
    433         do_action( 'customize_save_' . $this->id_data[ 'base' ], $this );
    434 
    435         $this->update( $value );
    436     }
    437 
    438     /**
    439      * Fetch and sanitize the $_POST value for the setting.
    440      *
    441      * @since 3.4.0
    442      *
    443      * @param mixed $default A default value which is used as a fallback. Default is null.
    444      * @return mixed The default value on failure, otherwise the sanitized value.
    445      */
    446     final public function post_value( $default = null ) {
    447         return $this->manager->post_value( $this, $default );
    448     }
    449 
    450     /**
    451      * Sanitize an input.
    452      *
    453      * @since 3.4.0
    454      *
    455      * @param string|array $value The value to sanitize.
    456      * @return string|array|null Null if an input isn't valid, otherwise the sanitized value.
    457      */
    458     public function sanitize( $value ) {
    459         $value = wp_unslash( $value );
    460 
    461         /**
    462          * Filter a Customize setting value in un-slashed form.
    463          *
    464          * @since 3.4.0
    465          *
    466          * @param mixed                $value Value of the setting.
    467          * @param WP_Customize_Setting $this  WP_Customize_Setting instance.
    468          */
    469         return apply_filters( "customize_sanitize_{$this->id}", $value, $this );
    470     }
    471 
    472     /**
    473      * Get the root value for a setting, especially for multidimensional ones.
    474      *
    475      * @since 4.4.0
    476      * @access protected
    477      *
    478      * @param mixed $default Value to return if root does not exist.
    479      * @return mixed
    480      */
    481     protected function get_root_value( $default = null ) {
    482         $id_base = $this->id_data['base'];
    483         if ( 'option' === $this->type ) {
    484             return get_option( $id_base, $default );
    485         } else if ( 'theme_mod' ) {
    486             return get_theme_mod( $id_base, $default );
    487         } else {
    488             /*
    489              * Any WP_Customize_Setting subclass implementing aggregate multidimensional
    490              * will need to override this method to obtain the data from the appropriate
    491              * location.
    492              */
    493             return $default;
    494         }
    495     }
    496 
    497     /**
    498      * Set the root value for a setting, especially for multidimensional ones.
    499      *
    500      * @since 4.4.0
    501      * @access protected
    502      *
    503      * @param mixed $value Value to set as root of multidimensional setting.
    504      * @return bool Whether the multidimensional root was updated successfully.
    505      */
    506     protected function set_root_value( $value ) {
    507         $id_base = $this->id_data['base'];
    508         if ( 'option' === $this->type ) {
    509             $autoload = true;
    510             if ( isset( self::$aggregated_multidimensionals[ $this->type ][ $this->id_data['base'] ]['autoload'] ) ) {
    511                 $autoload = self::$aggregated_multidimensionals[ $this->type ][ $this->id_data['base'] ]['autoload'];
    512             }
    513             return update_option( $id_base, $value, $autoload );
    514         } else if ( 'theme_mod' ) {
    515             set_theme_mod( $id_base, $value );
    516             return true;
    517         } else {
    518             /*
    519              * Any WP_Customize_Setting subclass implementing aggregate multidimensional
    520              * will need to override this method to obtain the data from the appropriate
    521              * location.
    522              */
    523             return false;
    524         }
    525     }
    526 
    527     /**
    528      * Save the value of the setting, using the related API.
    529      *
    530      * @since 3.4.0
    531      *
    532      * @param mixed $value The value to update.
    533      * @return bool The result of saving the value.
    534      */
    535     protected function update( $value ) {
    536         $id_base = $this->id_data['base'];
    537         if ( 'option' === $this->type || 'theme_mod' === $this->type ) {
    538             if ( ! $this->is_multidimensional_aggregated ) {
    539                 return $this->set_root_value( $value );
    540             } else {
    541                 $root = self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['root_value'];
    542                 $root = $this->multidimensional_replace( $root, $this->id_data['keys'], $value );
    543                 self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['root_value'] = $root;
    544                 return $this->set_root_value( $root );
    545             }
    546         } else {
    547             /**
    548              * Fires when the {@see WP_Customize_Setting::update()} method is called for settings
    549              * not handled as theme_mods or options.
    550              *
    551              * The dynamic portion of the hook name, `$this->type`, refers to the type of setting.
    552              *
    553              * @since 3.4.0
    554              *
    555              * @param mixed                $value Value of the setting.
    556              * @param WP_Customize_Setting $this  WP_Customize_Setting instance.
    557              */
    558             do_action( "customize_update_{$this->type}", $value, $this );
    559 
    560             return has_action( "customize_update_{$this->type}" );
    561         }
    562     }
    563 
    564     /**
    565      * Deprecated method.
    566      *
    567      * @since 3.4.0
    568      * @deprecated 4.4.0 Deprecated in favor of update() method.
    569      */
    570     protected function _update_theme_mod() {
    571         _deprecated_function( __METHOD__, '4.4.0', __CLASS__ . '::update()' );
    572     }
    573 
    574     /**
    575      * Deprecated method.
    576      *
    577      * @since 3.4.0
    578      * @deprecated 4.4.0 Deprecated in favor of update() method.
    579      */
    580     protected function _update_option() {
    581         _deprecated_function( __METHOD__, '4.4.0', __CLASS__ . '::update()' );
    582     }
    583 
    584     /**
    585      * Fetch the value of the setting.
    586      *
    587      * @since 3.4.0
    588      *
    589      * @return mixed The value.
    590      */
    591     public function value() {
    592         $id_base = $this->id_data['base'];
    593         $is_core_type = ( 'option' === $this->type || 'theme_mod' === $this->type );
    594 
    595         if ( ! $is_core_type && ! $this->is_multidimensional_aggregated ) {
    596             $value = $this->get_root_value( $this->default );
    597 
    598             /**
    599              * Filter a Customize setting value not handled as a theme_mod or option.
    600              *
    601              * The dynamic portion of the hook name, `$this->id_date['base']`, refers to
    602              * the base slug of the setting name.
    603              *
    604              * For settings handled as theme_mods or options, see those corresponding
    605              * functions for available hooks.
    606              *
    607              * @since 3.4.0
    608              *
    609              * @param mixed $default The setting default value. Default empty.
    610              */
    611             $value = apply_filters( "customize_value_{$id_base}", $value );
    612         } else if ( $this->is_multidimensional_aggregated ) {
    613             $root_value = self::$aggregated_multidimensionals[ $this->type ][ $id_base ]['root_value'];
    614             $value = $this->multidimensional_get( $root_value, $this->id_data['keys'], $this->default );
    615         } else {
    616             $value = $this->get_root_value( $this->default );
    617         }
    618         return $value;
    619     }
    620 
    621     /**
    622      * Sanitize the setting's value for use in JavaScript.
    623      *
    624      * @since 3.4.0
    625      *
    626      * @return mixed The requested escaped value.
    627      */
    628     public function js_value() {
    629 
    630         /**
    631          * Filter a Customize setting value for use in JavaScript.
    632          *
    633          * The dynamic portion of the hook name, `$this->id`, refers to the setting ID.
    634          *
    635          * @since 3.4.0
    636          *
    637          * @param mixed                $value The setting value.
    638          * @param WP_Customize_Setting $this  {@see WP_Customize_Setting} instance.
    639          */
    640         $value = apply_filters( "customize_sanitize_js_{$this->id}", $this->value(), $this );
    641 
    642         if ( is_string( $value ) )
    643             return html_entity_decode( $value, ENT_QUOTES, 'UTF-8');
    644 
    645         return $value;
    646     }
    647 
    648     /**
    649      * Validate user capabilities whether the theme supports the setting.
    650      *
    651      * @since 3.4.0
    652      *
    653      * @return bool False if theme doesn't support the setting or user can't change setting, otherwise true.
    654      */
    655     final public function check_capabilities() {
    656         if ( $this->capability && ! call_user_func_array( 'current_user_can', (array) $this->capability ) )
    657             return false;
    658 
    659         if ( $this->theme_supports && ! call_user_func_array( 'current_theme_supports', (array) $this->theme_supports ) )
    660             return false;
    661 
    662         return true;
    663     }
    664 
    665     /**
    666      * Multidimensional helper function.
    667      *
    668      * @since 3.4.0
    669      *
    670      * @param $root
    671      * @param $keys
    672      * @param bool $create Default is false.
    673      * @return array|void Keys are 'root', 'node', and 'key'.
    674      */
    675     final protected function multidimensional( &$root, $keys, $create = false ) {
    676         if ( $create && empty( $root ) )
    677             $root = array();
    678 
    679         if ( ! isset( $root ) || empty( $keys ) )
    680             return;
    681 
    682         $last = array_pop( $keys );
    683         $node = &$root;
    684 
    685         foreach ( $keys as $key ) {
    686             if ( $create && ! isset( $node[ $key ] ) )
    687                 $node[ $key ] = array();
    688 
    689             if ( ! is_array( $node ) || ! isset( $node[ $key ] ) )
    690                 return;
    691 
    692             $node = &$node[ $key ];
    693         }
    694 
    695         if ( $create ) {
    696             if ( ! is_array( $node ) ) {
    697                 // account for an array overriding a string or object value
    698                 $node = array();
    699             }
    700             if ( ! isset( $node[ $last ] ) ) {
    701                 $node[ $last ] = array();
    702             }
    703         }
    704 
    705         if ( ! isset( $node[ $last ] ) )
    706             return;
    707 
    708         return array(
    709             'root' => &$root,
    710             'node' => &$node,
    711             'key'  => $last,
    712         );
    713     }
    714 
    715     /**
    716      * Will attempt to replace a specific value in a multidimensional array.
    717      *
    718      * @since 3.4.0
    719      *
    720      * @param $root
    721      * @param $keys
    722      * @param mixed $value The value to update.
    723      * @return mixed
    724      */
    725     final protected function multidimensional_replace( $root, $keys, $value ) {
    726         if ( ! isset( $value ) )
    727             return $root;
    728         elseif ( empty( $keys ) ) // If there are no keys, we're replacing the root.
    729             return $value;
    730 
    731         $result = $this->multidimensional( $root, $keys, true );
    732 
    733         if ( isset( $result ) )
    734             $result['node'][ $result['key'] ] = $value;
    735 
    736         return $root;
    737     }
    738 
    739     /**
    740      * Will attempt to fetch a specific value from a multidimensional array.
    741      *
    742      * @since 3.4.0
    743      *
    744      * @param $root
    745      * @param $keys
    746      * @param mixed $default A default value which is used as a fallback. Default is null.
    747      * @return mixed The requested value or the default value.
    748      */
    749     final protected function multidimensional_get( $root, $keys, $default = null ) {
    750         if ( empty( $keys ) ) // If there are no keys, test the root.
    751             return isset( $root ) ? $root : $default;
    752 
    753         $result = $this->multidimensional( $root, $keys );
    754         return isset( $result ) ? $result['node'][ $result['key'] ] : $default;
    755     }
    756 
    757     /**
    758      * Will attempt to check if a specific value in a multidimensional array is set.
    759      *
    760      * @since 3.4.0
    761      *
    762      * @param $root
    763      * @param $keys
    764      * @return bool True if value is set, false if not.
    765      */
    766     final protected function multidimensional_isset( $root, $keys ) {
    767         $result = $this->multidimensional_get( $root, $keys );
    768         return isset( $result );
    769     }
    770 }
    7719
    77210/**
     
    78624    public function update( $value ) {}
    78725}
    788 
    789 /**
    790  * A setting that is used to filter a value, but will not save the results.
    791  *
    792  * Results should be properly handled using another setting or callback.
    793  *
    794  * @since 3.4.0
    795  *
    796  * @see WP_Customize_Setting
    797  */
    798 final class WP_Customize_Header_Image_Setting extends WP_Customize_Setting {
    799     public $id = 'header_image_data';
    800 
    801     /**
    802      * @since 3.4.0
    803      *
    804      * @global Custom_Image_Header $custom_image_header
    805      *
    806      * @param $value
    807      */
    808     public function update( $value ) {
    809         global $custom_image_header;
    810 
    811         // If the value doesn't exist (removed or random),
    812         // use the header_image value.
    813         if ( ! $value )
    814             $value = $this->manager->get_setting('header_image')->post_value();
    815 
    816         if ( is_array( $value ) && isset( $value['choice'] ) )
    817             $custom_image_header->set_header_image( $value['choice'] );
    818         else
    819             $custom_image_header->set_header_image( $value );
    820     }
    821 }
    822 
    823 /**
    824  * Customizer Background Image Setting class.
    825  *
    826  * @since 3.4.0
    827  *
    828  * @see WP_Customize_Setting
    829  */
    830 final class WP_Customize_Background_Image_Setting extends WP_Customize_Setting {
    831     public $id = 'background_image_thumb';
    832 
    833     /**
    834      * @since 3.4.0
    835      *
    836      * @param $value
    837      */
    838     public function update( $value ) {
    839         remove_theme_mod( 'background_image_thumb' );
    840     }
    841 }
    842 
    843 /**
    844  * Customize Setting to represent a nav_menu.
    845  *
    846  * Subclass of WP_Customize_Setting to represent a nav_menu taxonomy term, and
    847  * the IDs for the nav_menu_items associated with the nav menu.
    848  *
    849  * @since 4.3.0
    850  *
    851  * @see WP_Customize_Setting
    852  */
    853 class WP_Customize_Nav_Menu_Item_Setting extends WP_Customize_Setting {
    854 
    855     const ID_PATTERN = '/^nav_menu_item\[(?P<id>-?\d+)\]$/';
    856 
    857     const POST_TYPE = 'nav_menu_item';
    858 
    859     const TYPE = 'nav_menu_item';
    860 
    861     /**
    862      * Setting type.
    863      *
    864      * @since 4.3.0
    865      * @access public
    866      * @var string
    867      */
    868     public $type = self::TYPE;
    869 
    870     /**
    871      * Default setting value.
    872      *
    873      * @since 4.3.0
    874      * @access public
    875      * @var array
    876      *
    877      * @see wp_setup_nav_menu_item()
    878      */
    879     public $default = array(
    880         // The $menu_item_data for wp_update_nav_menu_item().
    881         'object_id'        => 0,
    882         'object'           => '', // Taxonomy name.
    883         'menu_item_parent' => 0, // A.K.A. menu-item-parent-id; note that post_parent is different, and not included.
    884         'position'         => 0, // A.K.A. menu_order.
    885         'type'             => 'custom', // Note that type_label is not included here.
    886         'title'            => '',
    887         'url'              => '',
    888         'target'           => '',
    889         'attr_title'       => '',
    890         'description'      => '',
    891         'classes'          => '',
    892         'xfn'              => '',
    893         'status'           => 'publish',
    894         'original_title'   => '',
    895         'nav_menu_term_id' => 0, // This will be supplied as the $menu_id arg for wp_update_nav_menu_item().
    896         '_invalid'         => false,
    897     );
    898 
    899     /**
    900      * Default transport.
    901      *
    902      * @since 4.3.0
    903      * @access public
    904      * @var string
    905      */
    906     public $transport = 'postMessage';
    907 
    908     /**
    909      * The post ID represented by this setting instance. This is the db_id.
    910      *
    911      * A negative value represents a placeholder ID for a new menu not yet saved.
    912      *
    913      * @since 4.3.0
    914      * @access public
    915      * @var int
    916      */
    917     public $post_id;
    918 
    919     /**
    920      * Storage of pre-setup menu item to prevent wasted calls to wp_setup_nav_menu_item().
    921      *
    922      * @since 4.3.0
    923      * @access protected
    924      * @var array
    925      */
    926     protected $value;
    927 
    928     /**
    929      * Previous (placeholder) post ID used before creating a new menu item.
    930      *
    931      * This value will be exported to JS via the customize_save_response filter
    932      * so that JavaScript can update the settings to refer to the newly-assigned
    933      * post ID. This value is always negative to indicate it does not refer to
    934      * a real post.
    935      *
    936      * @since 4.3.0
    937      * @access public
    938      * @var int
    939      *
    940      * @see WP_Customize_Nav_Menu_Item_Setting::update()
    941      * @see WP_Customize_Nav_Menu_Item_Setting::amend_customize_save_response()
    942      */
    943     public $previous_post_id;
    944 
    945     /**
    946      * When previewing or updating a menu item, this stores the previous nav_menu_term_id
    947      * which ensures that we can apply the proper filters.
    948      *
    949      * @since 4.3.0
    950      * @access public
    951      * @var int
    952      */
    953     public $original_nav_menu_term_id;
    954 
    955     /**
    956      * Whether or not preview() was called.
    957      *
    958      * @since 4.3.0
    959      * @access protected
    960      * @var bool
    961      */
    962     protected $is_previewed = false;
    963 
    964     /**
    965      * Whether or not update() was called.
    966      *
    967      * @since 4.3.0
    968      * @access protected
    969      * @var bool
    970      */
    971     protected $is_updated = false;
    972 
    973     /**
    974      * Status for calling the update method, used in customize_save_response filter.
    975      *
    976      * When status is inserted, the placeholder post ID is stored in $previous_post_id.
    977      * When status is error, the error is stored in $update_error.
    978      *
    979      * @since 4.3.0
    980      * @access public
    981      * @var string updated|inserted|deleted|error
    982      *
    983      * @see WP_Customize_Nav_Menu_Item_Setting::update()
    984      * @see WP_Customize_Nav_Menu_Item_Setting::amend_customize_save_response()
    985      */
    986     public $update_status;
    987 
    988     /**
    989      * Any error object returned by wp_update_nav_menu_item() when setting is updated.
    990      *
    991      * @since 4.3.0
    992      * @access public
    993      * @var WP_Error
    994      *
    995      * @see WP_Customize_Nav_Menu_Item_Setting::update()
    996      * @see WP_Customize_Nav_Menu_Item_Setting::amend_customize_save_response()
    997      */
    998     public $update_error;
    999 
    1000     /**
    1001      * Constructor.
    1002      *
    1003      * Any supplied $args override class property defaults.
    1004      *
    1005      * @since 4.3.0
    1006      * @access public
    1007      *
    1008      * @param WP_Customize_Manager $manager Bootstrap Customizer instance.
    1009      * @param string               $id      An specific ID of the setting. Can be a
    1010      *                                      theme mod or option name.
    1011      * @param array                $args    Optional. Setting arguments.
    1012      *
    1013      * @throws Exception If $id is not valid for this setting type.
    1014      */
    1015     public function __construct( WP_Customize_Manager $manager, $id, array $args = array() ) {
    1016         if ( empty( $manager->nav_menus ) ) {
    1017             throw new Exception( 'Expected WP_Customize_Manager::$nav_menus to be set.' );
    1018         }
    1019 
    1020         if ( ! preg_match( self::ID_PATTERN, $id, $matches ) ) {
    1021             throw new Exception( "Illegal widget setting ID: $id" );
    1022         }
    1023 
    1024         $this->post_id = intval( $matches['id'] );
    1025         add_action( 'wp_update_nav_menu_item', array( $this, 'flush_cached_value' ), 10, 2 );
    1026 
    1027         parent::__construct( $manager, $id, $args );
    1028 
    1029         // Ensure that an initially-supplied value is valid.
    1030         if ( isset( $this->value ) ) {
    1031             $this->populate_value();
    1032             foreach ( array_diff( array_keys( $this->default ), array_keys( $this->value ) ) as $missing ) {
    1033                 throw new Exception( "Supplied nav_menu_item value missing property: $missing" );
    1034             }
    1035         }
    1036 
    1037     }
    1038 
    1039     /**
    1040      * Clear the cached value when this nav menu item is updated.
    1041      *
    1042      * @since 4.3.0
    1043      * @access public
    1044      *
    1045      * @param int $menu_id       The term ID for the menu.
    1046      * @param int $menu_item_id  The post ID for the menu item.
    1047      */
    1048     public function flush_cached_value( $menu_id, $menu_item_id ) {
    1049         unset( $menu_id );
    1050         if ( $menu_item_id === $this->post_id ) {
    1051             $this->value = null;
    1052         }
    1053     }
    1054 
    1055     /**
    1056      * Get the instance data for a given nav_menu_item setting.
    1057      *
    1058      * @since 4.3.0
    1059      * @access public
    1060      *
    1061      * @see wp_setup_nav_menu_item()
    1062      *
    1063      * @return array|false Instance data array, or false if the item is marked for deletion.
    1064      */
    1065     public function value() {
    1066         if ( $this->is_previewed && $this->_previewed_blog_id === get_current_blog_id() ) {
    1067             $undefined  = new stdClass(); // Symbol.
    1068             $post_value = $this->post_value( $undefined );
    1069 
    1070             if ( $undefined === $post_value ) {
    1071                 $value = $this->_original_value;
    1072             } else {
    1073                 $value = $post_value;
    1074             }
    1075         } else if ( isset( $this->value ) ) {
    1076             $value = $this->value;
    1077         } else {
    1078             $value = false;
    1079 
    1080             // Note that a ID of less than one indicates a nav_menu not yet inserted.
    1081             if ( $this->post_id > 0 ) {
    1082                 $post = get_post( $this->post_id );
    1083                 if ( $post && self::POST_TYPE === $post->post_type ) {
    1084                     $value = (array) wp_setup_nav_menu_item( $post );
    1085                 }
    1086             }
    1087 
    1088             if ( ! is_array( $value ) ) {
    1089                 $value = $this->default;
    1090             }
    1091 
    1092             // Cache the value for future calls to avoid having to re-call wp_setup_nav_menu_item().
    1093             $this->value = $value;
    1094             $this->populate_value();
    1095             $value = $this->value;
    1096         }
    1097 
    1098         return $value;
    1099     }
    1100 
    1101     /**
    1102      * Ensure that the value is fully populated with the necessary properties.
    1103      *
    1104      * Translates some properties added by wp_setup_nav_menu_item() and removes others.
    1105      *
    1106      * @since 4.3.0
    1107      * @access protected
    1108      *
    1109      * @see WP_Customize_Nav_Menu_Item_Setting::value()
    1110      */
    1111     protected function populate_value() {
    1112         if ( ! is_array( $this->value ) ) {
    1113             return;
    1114         }
    1115 
    1116         if ( isset( $this->value['menu_order'] ) ) {
    1117             $this->value['position'] = $this->value['menu_order'];
    1118             unset( $this->value['menu_order'] );
    1119         }
    1120         if ( isset( $this->value['post_status'] ) ) {
    1121             $this->value['status'] = $this->value['post_status'];
    1122             unset( $this->value['post_status'] );
    1123         }
    1124 
    1125         if ( ! isset( $this->value['original_title'] ) ) {
    1126             $original_title = '';
    1127             if ( 'post_type' === $this->value['type'] ) {
    1128                 $original_title = get_the_title( $this->value['object_id'] );
    1129             } elseif ( 'taxonomy' === $this->value['type'] ) {
    1130                 $original_title = get_term_field( 'name', $this->value['object_id'], $this->value['object'], 'raw' );
    1131                 if ( is_wp_error( $original_title ) ) {
    1132                     $original_title = '';
    1133                 }
    1134             }
    1135             $this->value['original_title'] = html_entity_decode( $original_title, ENT_QUOTES, get_bloginfo( 'charset' ) );
    1136         }
    1137 
    1138         if ( ! isset( $this->value['nav_menu_term_id'] ) && $this->post_id > 0 ) {
    1139             $menus = wp_get_post_terms( $this->post_id, WP_Customize_Nav_Menu_Setting::TAXONOMY, array(
    1140                 'fields' => 'ids',
    1141             ) );
    1142             if ( ! empty( $menus ) ) {
    1143                 $this->value['nav_menu_term_id'] = array_shift( $menus );
    1144             } else {
    1145                 $this->value['nav_menu_term_id'] = 0;
    1146             }
    1147         }
    1148 
    1149         foreach ( array( 'object_id', 'menu_item_parent', 'nav_menu_term_id' ) as $key ) {
    1150             if ( ! is_int( $this->value[ $key ] ) ) {
    1151                 $this->value[ $key ] = intval( $this->value[ $key ] );
    1152             }
    1153         }
    1154         foreach ( array( 'classes', 'xfn' ) as $key ) {
    1155             if ( is_array( $this->value[ $key ] ) ) {
    1156                 $this->value[ $key ] = implode( ' ', $this->value[ $key ] );
    1157             }
    1158         }
    1159 
    1160         if ( ! isset( $this->value['_invalid'] ) ) {
    1161             $this->value['_invalid'] = (
    1162                 ( 'post_type' === $this->value['type'] && ! post_type_exists( $this->value['object'] ) )
    1163                 ||
    1164                 ( 'taxonomy' === $this->value['type'] && ! taxonomy_exists( $this->value['object'] ) )
    1165             );
    1166         }
    1167 
    1168         // Remove remaining properties available on a setup nav_menu_item post object which aren't relevant to the setting value.
    1169         $irrelevant_properties = array(
    1170             'ID',
    1171             'comment_count',
    1172             'comment_status',
    1173             'db_id',
    1174             'filter',
    1175             'guid',
    1176             'ping_status',
    1177             'pinged',
    1178             'post_author',
    1179             'post_content',
    1180             'post_content_filtered',
    1181             'post_date',
    1182             'post_date_gmt',
    1183             'post_excerpt',
    1184             'post_mime_type',
    1185             'post_modified',
    1186             'post_modified_gmt',
    1187             'post_name',
    1188             'post_parent',
    1189             'post_password',
    1190             'post_title',
    1191             'post_type',
    1192             'to_ping',
    1193         );
    1194         foreach ( $irrelevant_properties as $property ) {
    1195             unset( $this->value[ $property ] );
    1196         }
    1197     }
    1198 
    1199     /**
    1200      * Handle previewing the setting.
    1201      *
    1202      * @since 4.3.0
    1203      * @since 4.4.0 Added boolean return value.
    1204      * @access public
    1205      *
    1206      * @see WP_Customize_Manager::post_value()
    1207      *
    1208      * @return bool False if method short-circuited due to no-op.
    1209      */
    1210     public function preview() {
    1211         if ( $this->is_previewed ) {
    1212             return false;
    1213         }
    1214 
    1215         $undefined = new stdClass();
    1216         $is_placeholder = ( $this->post_id < 0 );
    1217         $is_dirty = ( $undefined !== $this->post_value( $undefined ) );
    1218         if ( ! $is_placeholder && ! $is_dirty ) {
    1219             return false;
    1220         }
    1221 
    1222         $this->is_previewed              = true;
    1223         $this->_original_value           = $this->value();
    1224         $this->original_nav_menu_term_id = $this->_original_value['nav_menu_term_id'];
    1225         $this->_previewed_blog_id        = get_current_blog_id();
    1226 
    1227         add_filter( 'wp_get_nav_menu_items', array( $this, 'filter_wp_get_nav_menu_items' ), 10, 3 );
    1228 
    1229         $sort_callback = array( __CLASS__, 'sort_wp_get_nav_menu_items' );
    1230         if ( ! has_filter( 'wp_get_nav_menu_items', $sort_callback ) ) {
    1231             add_filter( 'wp_get_nav_menu_items', array( __CLASS__, 'sort_wp_get_nav_menu_items' ), 1000, 3 );
    1232         }
    1233 
    1234         // @todo Add get_post_metadata filters for plugins to add their data.
    1235 
    1236         return true;
    1237     }
    1238 
    1239     /**
    1240      * Filter the wp_get_nav_menu_items() result to supply the previewed menu items.
    1241      *
    1242      * @since 4.3.0
    1243      * @access public
    1244      *
    1245      * @see wp_get_nav_menu_items()
    1246      *
    1247      * @param array  $items An array of menu item post objects.
    1248      * @param object $menu  The menu object.
    1249      * @param array  $args  An array of arguments used to retrieve menu item objects.
    1250      * @return array Array of menu items,
    1251      */
    1252     public function filter_wp_get_nav_menu_items( $items, $menu, $args ) {
    1253         $this_item = $this->value();
    1254         $current_nav_menu_term_id = $this_item['nav_menu_term_id'];
    1255         unset( $this_item['nav_menu_term_id'] );
    1256 
    1257         $should_filter = (
    1258             $menu->term_id === $this->original_nav_menu_term_id
    1259             ||
    1260             $menu->term_id === $current_nav_menu_term_id
    1261         );
    1262         if ( ! $should_filter ) {
    1263             return $items;
    1264         }
    1265 
    1266         // Handle deleted menu item, or menu item moved to another menu.
    1267         $should_remove = (
    1268             false === $this_item
    1269             ||
    1270             true === $this_item['_invalid']
    1271             ||
    1272             (
    1273                 $this->original_nav_menu_term_id === $menu->term_id
    1274                 &&
    1275                 $current_nav_menu_term_id !== $this->original_nav_menu_term_id
    1276             )
    1277         );
    1278         if ( $should_remove ) {
    1279             $filtered_items = array();
    1280             foreach ( $items as $item ) {
    1281                 if ( $item->db_id !== $this->post_id ) {
    1282                     $filtered_items[] = $item;
    1283                 }
    1284             }
    1285             return $filtered_items;
    1286         }
    1287 
    1288         $mutated = false;
    1289         $should_update = (
    1290             is_array( $this_item )
    1291             &&
    1292             $current_nav_menu_term_id === $menu->term_id
    1293         );
    1294         if ( $should_update ) {
    1295             foreach ( $items as $item ) {
    1296                 if ( $item->db_id === $this->post_id ) {
    1297                     foreach ( get_object_vars( $this->value_as_wp_post_nav_menu_item() ) as $key => $value ) {
    1298                         $item->$key = $value;
    1299                     }
    1300                     $mutated = true;
    1301                 }
    1302             }
    1303 
    1304             // Not found so we have to append it..
    1305             if ( ! $mutated ) {
    1306                 $items[] = $this->value_as_wp_post_nav_menu_item();
    1307             }
    1308         }
    1309 
    1310         return $items;
    1311     }
    1312 
    1313     /**
    1314      * Re-apply the tail logic also applied on $items by wp_get_nav_menu_items().
    1315      *
    1316      * @since 4.3.0
    1317      * @access public
    1318      * @static
    1319      *
    1320      * @see wp_get_nav_menu_items()
    1321      *
    1322      * @param array  $items An array of menu item post objects.
    1323      * @param object $menu  The menu object.
    1324      * @param array  $args  An array of arguments used to retrieve menu item objects.
    1325      * @return array Array of menu items,
    1326      */
    1327     public static function sort_wp_get_nav_menu_items( $items, $menu, $args ) {
    1328         // @todo We should probably re-apply some constraints imposed by $args.
    1329         unset( $args['include'] );
    1330 
    1331         // Remove invalid items only in frontend.
    1332         if ( ! is_admin() ) {
    1333             $items = array_filter( $items, '_is_valid_nav_menu_item' );
    1334         }
    1335 
    1336         if ( ARRAY_A === $args['output'] ) {
    1337             $GLOBALS['_menu_item_sort_prop'] = $args['output_key'];
    1338             usort( $items, '_sort_nav_menu_items' );
    1339             $i = 1;
    1340 
    1341             foreach ( $items as $k => $item ) {
    1342                 $items[ $k ]->{$args['output_key']} = $i++;
    1343             }
    1344         }
    1345 
    1346         return $items;
    1347     }
    1348 
    1349     /**
    1350      * Get the value emulated into a WP_Post and set up as a nav_menu_item.
    1351      *
    1352      * @since 4.3.0
    1353      * @access public
    1354      *
    1355      * @return WP_Post With wp_setup_nav_menu_item() applied.
    1356      */
    1357     public function value_as_wp_post_nav_menu_item() {
    1358         $item = (object) $this->value();
    1359         unset( $item->nav_menu_term_id );
    1360 
    1361         $item->post_status = $item->status;
    1362         unset( $item->status );
    1363 
    1364         $item->post_type = 'nav_menu_item';
    1365         $item->menu_order = $item->position;
    1366         unset( $item->position );
    1367 
    1368         if ( $item->title ) {
    1369             $item->post_title = $item->title;
    1370         }
    1371 
    1372         $item->ID = $this->post_id;
    1373         $item->db_id = $this->post_id;
    1374         $post = new WP_Post( (object) $item );
    1375 
    1376         if ( empty( $post->post_author ) ) {
    1377             $post->post_author = get_current_user_id();
    1378         }
    1379 
    1380         if ( ! isset( $post->type_label ) ) {
    1381             if ( 'post_type' === $post->type ) {
    1382                 $object = get_post_type_object( $post->object );
    1383                 if ( $object ) {
    1384                     $post->type_label = $object->labels->singular_name;
    1385                 } else {
    1386                     $post->type_label = $post->object;
    1387                 }
    1388             } elseif ( 'taxonomy' == $post->type ) {
    1389                 $object = get_taxonomy( $post->object );
    1390                 if ( $object ) {
    1391                     $post->type_label = $object->labels->singular_name;
    1392                 } else {
    1393                     $post->type_label = $post->object;
    1394                 }
    1395             } else {
    1396                 $post->type_label = __( 'Custom Link' );
    1397             }
    1398         }
    1399 
    1400         return $post;
    1401     }
    1402 
    1403     /**
    1404      * Sanitize an input.
    1405      *
    1406      * Note that parent::sanitize() erroneously does wp_unslash() on $value, but
    1407      * we remove that in this override.
    1408      *
    1409      * @since 4.3.0
    1410      * @access public
    1411      *
    1412      * @param array $menu_item_value The value to sanitize.
    1413      * @return array|false|null Null if an input isn't valid. False if it is marked for deletion.
    1414      *                          Otherwise the sanitized value.
    1415      */
    1416     public function sanitize( $menu_item_value ) {
    1417         // Menu is marked for deletion.
    1418         if ( false === $menu_item_value ) {
    1419             return $menu_item_value;
    1420         }
    1421 
    1422         // Invalid.
    1423         if ( ! is_array( $menu_item_value ) ) {
    1424             return null;
    1425         }
    1426 
    1427         $default = array(
    1428             'object_id'        => 0,
    1429             'object'           => '',
    1430             'menu_item_parent' => 0,
    1431             'position'         => 0,
    1432             'type'             => 'custom',
    1433             'title'            => '',
    1434             'url'              => '',
    1435             'target'           => '',
    1436             'attr_title'       => '',
    1437             'description'      => '',
    1438             'classes'          => '',
    1439             'xfn'              => '',
    1440             'status'           => 'publish',
    1441             'original_title'   => '',
    1442             'nav_menu_term_id' => 0,
    1443             '_invalid'         => false,
    1444         );
    1445         $menu_item_value = array_merge( $default, $menu_item_value );
    1446         $menu_item_value = wp_array_slice_assoc( $menu_item_value, array_keys( $default ) );
    1447         $menu_item_value['position'] = max( 0, intval( $menu_item_value['position'] ) );
    1448 
    1449         foreach ( array( 'object_id', 'menu_item_parent', 'nav_menu_term_id' ) as $key ) {
    1450             // Note we need to allow negative-integer IDs for previewed objects not inserted yet.
    1451             $menu_item_value[ $key ] = intval( $menu_item_value[ $key ] );
    1452         }
    1453 
    1454         foreach ( array( 'type', 'object', 'target' ) as $key ) {
    1455             $menu_item_value[ $key ] = sanitize_key( $menu_item_value[ $key ] );
    1456         }
    1457 
    1458         foreach ( array( 'xfn', 'classes' ) as $key ) {
    1459             $value = $menu_item_value[ $key ];
    1460             if ( ! is_array( $value ) ) {
    1461                 $value = explode( ' ', $value );
    1462             }
    1463             $menu_item_value[ $key ] = implode( ' ', array_map( 'sanitize_html_class', $value ) );
    1464         }
    1465 
    1466         foreach ( array( 'title', 'attr_title', 'description', 'original_title' ) as $key ) {
    1467             // @todo Should esc_attr() the attr_title as well?
    1468             $menu_item_value[ $key ] = sanitize_text_field( $menu_item_value[ $key ] );
    1469         }
    1470 
    1471         $menu_item_value['url'] = esc_url_raw( $menu_item_value['url'] );
    1472         if ( ! get_post_status_object( $menu_item_value['status'] ) ) {
    1473             $menu_item_value['status'] = 'publish';
    1474         }
    1475 
    1476         $menu_item_value['_invalid'] = (bool) $menu_item_value['_invalid'];
    1477 
    1478         /** This filter is documented in wp-includes/class-wp-customize-setting.php */
    1479         return apply_filters( "customize_sanitize_{$this->id}", $menu_item_value, $this );
    1480     }
    1481 
    1482     /**
    1483      * Create/update the nav_menu_item post for this setting.
    1484      *
    1485      * Any created menu items will have their assigned post IDs exported to the client
    1486      * via the customize_save_response filter. Likewise, any errors will be exported
    1487      * to the client via the customize_save_response() filter.
    1488      *
    1489      * To delete a menu, the client can send false as the value.
    1490      *
    1491      * @since 4.3.0
    1492      * @access protected
    1493      *
    1494      * @see wp_update_nav_menu_item()
    1495      *
    1496      * @param array|false $value The menu item array to update. If false, then the menu item will be deleted
    1497      *                           entirely. See WP_Customize_Nav_Menu_Item_Setting::$default for what the value
    1498      *                           should consist of.
    1499      * @return null|void
    1500      */
    1501     protected function update( $value ) {
    1502         if ( $this->is_updated ) {
    1503             return;
    1504         }
    1505 
    1506         $this->is_updated = true;
    1507         $is_placeholder   = ( $this->post_id < 0 );
    1508         $is_delete        = ( false === $value );
    1509 
    1510         // Update the cached value.
    1511         $this->value = $value;
    1512 
    1513         add_filter( 'customize_save_response', array( $this, 'amend_customize_save_response' ) );
    1514 
    1515         if ( $is_delete ) {
    1516             // If the current setting post is a placeholder, a delete request is a no-op.
    1517             if ( $is_placeholder ) {
    1518                 $this->update_status = 'deleted';
    1519             } else {
    1520                 $r = wp_delete_post( $this->post_id, true );
    1521 
    1522                 if ( false === $r ) {
    1523                     $this->update_error  = new WP_Error( 'delete_failure' );
    1524                     $this->update_status = 'error';
    1525                 } else {
    1526                     $this->update_status = 'deleted';
    1527                 }
    1528                 // @todo send back the IDs for all associated nav menu items deleted, so these settings (and controls) can be removed from Customizer?
    1529             }
    1530         } else {
    1531 
    1532             // Handle saving menu items for menus that are being newly-created.
    1533             if ( $value['nav_menu_term_id'] < 0 ) {
    1534                 $nav_menu_setting_id = sprintf( 'nav_menu[%s]', $value['nav_menu_term_id'] );
    1535                 $nav_menu_setting    = $this->manager->get_setting( $nav_menu_setting_id );
    1536 
    1537                 if ( ! $nav_menu_setting || ! ( $nav_menu_setting instanceof WP_Customize_Nav_Menu_Setting ) ) {
    1538                     $this->update_status = 'error';
    1539                     $this->update_error  = new WP_Error( 'unexpected_nav_menu_setting' );
    1540                     return;
    1541                 }
    1542 
    1543                 if ( false === $nav_menu_setting->save() ) {
    1544                     $this->update_status = 'error';
    1545                     $this->update_error  = new WP_Error( 'nav_menu_setting_failure' );
    1546                     return;
    1547                 }
    1548 
    1549                 if ( $nav_menu_setting->previous_term_id !== intval( $value['nav_menu_term_id'] ) ) {
    1550                     $this->update_status = 'error';
    1551                     $this->update_error  = new WP_Error( 'unexpected_previous_term_id' );
    1552                     return;
    1553                 }
    1554 
    1555                 $value['nav_menu_term_id'] = $nav_menu_setting->term_id;
    1556             }
    1557 
    1558             // Handle saving a nav menu item that is a child of a nav menu item being newly-created.
    1559             if ( $value['menu_item_parent'] < 0 ) {
    1560                 $parent_nav_menu_item_setting_id = sprintf( 'nav_menu_item[%s]', $value['menu_item_parent'] );
    1561                 $parent_nav_menu_item_setting    = $this->manager->get_setting( $parent_nav_menu_item_setting_id );
    1562 
    1563                 if ( ! $parent_nav_menu_item_setting || ! ( $parent_nav_menu_item_setting instanceof WP_Customize_Nav_Menu_Item_Setting ) ) {
    1564                     $this->update_status = 'error';
    1565                     $this->update_error  = new WP_Error( 'unexpected_nav_menu_item_setting' );
    1566                     return;
    1567                 }
    1568 
    1569                 if ( false === $parent_nav_menu_item_setting->save() ) {
    1570                     $this->update_status = 'error';
    1571                     $this->update_error  = new WP_Error( 'nav_menu_item_setting_failure' );
    1572                     return;
    1573                 }
    1574 
    1575                 if ( $parent_nav_menu_item_setting->previous_post_id !== intval( $value['menu_item_parent'] ) ) {
    1576                     $this->update_status = 'error';
    1577                     $this->update_error  = new WP_Error( 'unexpected_previous_post_id' );
    1578                     return;
    1579                 }
    1580 
    1581                 $value['menu_item_parent'] = $parent_nav_menu_item_setting->post_id;
    1582             }
    1583 
    1584             // Insert or update menu.
    1585             $menu_item_data = array(
    1586                 'menu-item-object-id'   => $value['object_id'],
    1587                 'menu-item-object'      => $value['object'],
    1588                 'menu-item-parent-id'   => $value['menu_item_parent'],
    1589                 'menu-item-position'    => $value['position'],
    1590                 'menu-item-type'        => $value['type'],
    1591                 'menu-item-title'       => $value['title'],
    1592                 'menu-item-url'         => $value['url'],
    1593                 'menu-item-description' => $value['description'],
    1594                 'menu-item-attr-title'  => $value['attr_title'],
    1595                 'menu-item-target'      => $value['target'],
    1596                 'menu-item-classes'     => $value['classes'],
    1597                 'menu-item-xfn'         => $value['xfn'],
    1598                 'menu-item-status'      => $value['status'],
    1599             );
    1600 
    1601             $r = wp_update_nav_menu_item(
    1602                 $value['nav_menu_term_id'],
    1603                 $is_placeholder ? 0 : $this->post_id,
    1604                 $menu_item_data
    1605             );
    1606 
    1607             if ( is_wp_error( $r ) ) {
    1608                 $this->update_status = 'error';
    1609                 $this->update_error = $r;
    1610             } else {
    1611                 if ( $is_placeholder ) {
    1612                     $this->previous_post_id = $this->post_id;
    1613                     $this->post_id = $r;
    1614                     $this->update_status = 'inserted';
    1615                 } else {
    1616                     $this->update_status = 'updated';
    1617                 }
    1618             }
    1619         }
    1620 
    1621     }
    1622 
    1623     /**
    1624      * Export data for the JS client.
    1625      *
    1626      * @since 4.3.0
    1627      * @access public
    1628      *
    1629      * @see WP_Customize_Nav_Menu_Item_Setting::update()
    1630      *
    1631      * @param array $data Additional information passed back to the 'saved' event on `wp.customize`.
    1632      * @return array Save response data.
    1633      */
    1634     public function amend_customize_save_response( $data ) {
    1635         if ( ! isset( $data['nav_menu_item_updates'] ) ) {
    1636             $data['nav_menu_item_updates'] = array();
    1637         }
    1638 
    1639         $data['nav_menu_item_updates'][] = array(
    1640             'post_id'          => $this->post_id,
    1641             'previous_post_id' => $this->previous_post_id,
    1642             'error'            => $this->update_error ? $this->update_error->get_error_code() : null,
    1643             'status'           => $this->update_status,
    1644         );
    1645         return $data;
    1646     }
    1647 }
    1648 
    1649 /**
    1650  * Customize Setting to represent a nav_menu.
    1651  *
    1652  * Subclass of WP_Customize_Setting to represent a nav_menu taxonomy term, and
    1653  * the IDs for the nav_menu_items associated with the nav menu.
    1654  *
    1655  * @since 4.3.0
    1656  *
    1657  * @see wp_get_nav_menu_object()
    1658  * @see WP_Customize_Setting
    1659  */
    1660 class WP_Customize_Nav_Menu_Setting extends WP_Customize_Setting {
    1661 
    1662     const ID_PATTERN = '/^nav_menu\[(?P<id>-?\d+)\]$/';
    1663 
    1664     const TAXONOMY = 'nav_menu';
    1665 
    1666     const TYPE = 'nav_menu';
    1667 
    1668     /**
    1669      * Setting type.
    1670      *
    1671      * @since 4.3.0
    1672      * @access public
    1673      * @var string
    1674      */
    1675     public $type = self::TYPE;
    1676 
    1677     /**
    1678      * Default setting value.
    1679      *
    1680      * @since 4.3.0
    1681      * @access public
    1682      * @var array
    1683      *
    1684      * @see wp_get_nav_menu_object()
    1685      */
    1686     public $default = array(
    1687         'name'        => '',
    1688         'description' => '',
    1689         'parent'      => 0,
    1690         'auto_add'    => false,
    1691     );
    1692 
    1693     /**
    1694      * Default transport.
    1695      *
    1696      * @since 4.3.0
    1697      * @access public
    1698      * @var string
    1699      */
    1700     public $transport = 'postMessage';
    1701 
    1702     /**
    1703      * The term ID represented by this setting instance.
    1704      *
    1705      * A negative value represents a placeholder ID for a new menu not yet saved.
    1706      *
    1707      * @since 4.3.0
    1708      * @access public
    1709      * @var int
    1710      */
    1711     public $term_id;
    1712 
    1713     /**
    1714      * Previous (placeholder) term ID used before creating a new menu.
    1715      *
    1716      * This value will be exported to JS via the customize_save_response filter
    1717      * so that JavaScript can update the settings to refer to the newly-assigned
    1718      * term ID. This value is always negative to indicate it does not refer to
    1719      * a real term.
    1720      *
    1721      * @since 4.3.0
    1722      * @access public
    1723      * @var int
    1724      *
    1725      * @see WP_Customize_Nav_Menu_Setting::update()
    1726      * @see WP_Customize_Nav_Menu_Setting::amend_customize_save_response()
    1727      */
    1728     public $previous_term_id;
    1729 
    1730     /**
    1731      * Whether or not preview() was called.
    1732      *
    1733      * @since 4.3.0
    1734      * @access protected
    1735      * @var bool
    1736      */
    1737     protected $is_previewed = false;
    1738 
    1739     /**
    1740      * Whether or not update() was called.
    1741      *
    1742      * @since 4.3.0
    1743      * @access protected
    1744      * @var bool
    1745      */
    1746     protected $is_updated = false;
    1747 
    1748     /**
    1749      * Status for calling the update method, used in customize_save_response filter.
    1750      *
    1751      * When status is inserted, the placeholder term ID is stored in $previous_term_id.
    1752      * When status is error, the error is stored in $update_error.
    1753      *
    1754      * @since 4.3.0
    1755      * @access public
    1756      * @var string updated|inserted|deleted|error
    1757      *
    1758      * @see WP_Customize_Nav_Menu_Setting::update()
    1759      * @see WP_Customize_Nav_Menu_Setting::amend_customize_save_response()
    1760      */
    1761     public $update_status;
    1762 
    1763     /**
    1764      * Any error object returned by wp_update_nav_menu_object() when setting is updated.
    1765      *
    1766      * @since 4.3.0
    1767      * @access public
    1768      * @var WP_Error
    1769      *
    1770      * @see WP_Customize_Nav_Menu_Setting::update()
    1771      * @see WP_Customize_Nav_Menu_Setting::amend_customize_save_response()
    1772      */
    1773     public $update_error;
    1774 
    1775     /**
    1776      * Constructor.
    1777      *
    1778      * Any supplied $args override class property defaults.
    1779      *
    1780      * @since 4.3.0
    1781      * @access public
    1782      *
    1783      * @param WP_Customize_Manager $manager Bootstrap Customizer instance.
    1784      * @param string               $id      An specific ID of the setting. Can be a
    1785      *                                      theme mod or option name.
    1786      * @param array                $args    Optional. Setting arguments.
    1787      *
    1788      * @throws Exception If $id is not valid for this setting type.
    1789      */
    1790     public function __construct( WP_Customize_Manager $manager, $id, array $args = array() ) {
    1791         if ( empty( $manager->nav_menus ) ) {
    1792             throw new Exception( 'Expected WP_Customize_Manager::$nav_menus to be set.' );
    1793         }
    1794 
    1795         if ( ! preg_match( self::ID_PATTERN, $id, $matches ) ) {
    1796             throw new Exception( "Illegal widget setting ID: $id" );
    1797         }
    1798 
    1799         $this->term_id = intval( $matches['id'] );
    1800 
    1801         parent::__construct( $manager, $id, $args );
    1802     }
    1803 
    1804     /**
    1805      * Get the instance data for a given widget setting.
    1806      *
    1807      * @since 4.3.0
    1808      * @access public
    1809      *
    1810      * @see wp_get_nav_menu_object()
    1811      *
    1812      * @return array Instance data.
    1813      */
    1814     public function value() {
    1815         if ( $this->is_previewed && $this->_previewed_blog_id === get_current_blog_id() ) {
    1816             $undefined  = new stdClass(); // Symbol.
    1817             $post_value = $this->post_value( $undefined );
    1818 
    1819             if ( $undefined === $post_value ) {
    1820                 $value = $this->_original_value;
    1821             } else {
    1822                 $value = $post_value;
    1823             }
    1824         } else {
    1825             $value = false;
    1826 
    1827             // Note that a term_id of less than one indicates a nav_menu not yet inserted.
    1828             if ( $this->term_id > 0 ) {
    1829                 $term = wp_get_nav_menu_object( $this->term_id );
    1830 
    1831                 if ( $term ) {
    1832                     $value = wp_array_slice_assoc( (array) $term, array_keys( $this->default ) );
    1833 
    1834                     $nav_menu_options  = (array) get_option( 'nav_menu_options', array() );
    1835                     $value['auto_add'] = false;
    1836 
    1837                     if ( isset( $nav_menu_options['auto_add'] ) && is_array( $nav_menu_options['auto_add'] ) ) {
    1838                         $value['auto_add'] = in_array( $term->term_id, $nav_menu_options['auto_add'] );
    1839                     }
    1840                 }
    1841             }
    1842 
    1843             if ( ! is_array( $value ) ) {
    1844                 $value = $this->default;
    1845             }
    1846         }
    1847         return $value;
    1848     }
    1849 
    1850     /**
    1851      * Handle previewing the setting.
    1852      *
    1853      * @since 4.3.0
    1854      * @since 4.4.0 Added boolean return value
    1855      * @access public
    1856      *
    1857      * @see WP_Customize_Manager::post_value()
    1858      *
    1859      * @return bool False if method short-circuited due to no-op.
    1860      */
    1861     public function preview() {
    1862         if ( $this->is_previewed ) {
    1863             return false;
    1864         }
    1865 
    1866         $undefined = new stdClass();
    1867         $is_placeholder = ( $this->term_id < 0 );
    1868         $is_dirty = ( $undefined !== $this->post_value( $undefined ) );
    1869         if ( ! $is_placeholder && ! $is_dirty ) {
    1870             return false;
    1871         }
    1872 
    1873         $this->is_previewed       = true;
    1874         $this->_original_value    = $this->value();
    1875         $this->_previewed_blog_id = get_current_blog_id();
    1876 
    1877         add_filter( 'wp_get_nav_menus', array( $this, 'filter_wp_get_nav_menus' ), 10, 2 );
    1878         add_filter( 'wp_get_nav_menu_object', array( $this, 'filter_wp_get_nav_menu_object' ), 10, 2 );
    1879         add_filter( 'default_option_nav_menu_options', array( $this, 'filter_nav_menu_options' ) );
    1880         add_filter( 'option_nav_menu_options', array( $this, 'filter_nav_menu_options' ) );
    1881 
    1882         return true;
    1883     }
    1884 
    1885     /**
    1886      * Filter the wp_get_nav_menus() result to ensure the inserted menu object is included, and the deleted one is removed.
    1887      *
    1888      * @since 4.3.0
    1889      * @access public
    1890      *
    1891      * @see wp_get_nav_menus()
    1892      *
    1893      * @param array $menus An array of menu objects.
    1894      * @param array $args  An array of arguments used to retrieve menu objects.
    1895      * @return array
    1896      */
    1897     public function filter_wp_get_nav_menus( $menus, $args ) {
    1898         if ( get_current_blog_id() !== $this->_previewed_blog_id ) {
    1899             return $menus;
    1900         }
    1901 
    1902         $setting_value = $this->value();
    1903         $is_delete = ( false === $setting_value );
    1904         $index = -1;
    1905 
    1906         // Find the existing menu item's position in the list.
    1907         foreach ( $menus as $i => $menu ) {
    1908             if ( (int) $this->term_id === (int) $menu->term_id || (int) $this->previous_term_id === (int) $menu->term_id ) {
    1909                 $index = $i;
    1910                 break;
    1911             }
    1912         }
    1913 
    1914         if ( $is_delete ) {
    1915             // Handle deleted menu by removing it from the list.
    1916             if ( -1 !== $index ) {
    1917                 array_splice( $menus, $index, 1 );