Make WordPress Core

Changeset 35383


Ignore:
Timestamp:
10/24/2015 06:10:17 PM (11 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 );
    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-header-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 }
    7889
    78910/**
     
    82041        }
    82142}
    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 }
  • trunk/src/wp-includes/customize/class-wp-customize-nav-menu-item-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 }
    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 }
    8429
    84310/**
     
    1646813        }
    1647814}
    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-nav-menu-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 }
    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 }
    16489
    164910/**
Note: See TracChangeset for help on using the changeset viewer.