Changeset 35383
- Timestamp:
- 10/24/2015 06:10:17 PM (9 years ago)
- 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 770 770 } 771 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 } 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 */ 773 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-filter-setting.php' ); 774 775 /** WP_Customize_Header_Image_Setting class */ 776 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-header-image-setting.php' ); 777 778 /** WP_Customize_Background_Image_Setting class */ 779 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-background-image-setting.php' ); 780 781 /** WP_Customize_Nav_Menu_Item_Setting class */ 782 require_once( ABSPATH . WPINC . '/customize/class-wp-customize-nav-menu-item-setting.php' ); 783 784 /** WP_Customize_Nav_Menu_Setting class */ 785 require_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 5 5 * @package WordPress 6 6 * @subpackage Customize 7 * @since 3.4.07 * @since 4.4.0 8 8 */ 9 10 /**11 * Customize Setting class.12 *13 * Handles saving and sanitizing of settings.14 *15 * @since 3.4.016 *17 * @see WP_Customize_Manager18 */19 class WP_Customize_Setting {20 /**21 * @access public22 * @var WP_Customize_Manager23 */24 public $manager;25 26 /**27 * Unique string identifier for the setting.28 *29 * @access public30 * @var string31 */32 public $id;33 34 /**35 * @access public36 * @var string37 */38 public $type = 'theme_mod';39 40 /**41 * Capability required to edit this setting.42 *43 * @var string44 */45 public $capability = 'edit_theme_options';46 47 /**48 * Feature a theme is required to support to enable this setting.49 *50 * @access public51 * @var string52 */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 callback61 */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 the69 * preview when loading the Customizer. Normally a setting only is synced to70 * the preview if it has been changed. This allows the setting to be sent71 * from the start.72 *73 * @since 4.2.074 * @access public75 * @var bool76 */77 public $dirty = false;78 79 /**80 * @var array81 */82 protected $id_data = array();83 84 /**85 * Cache of multidimensional values to improve performance.86 *87 * @since 4.4.088 * @access protected89 * @var array90 * @static91 */92 protected static $aggregated_multidimensionals = array();93 94 /**95 * Whether the multidimensional setting is aggregated.96 *97 * @since 4.4.098 * @access protected99 * @var bool100 */101 protected $is_multidimensional_aggregated = false;102 103 /**104 * Constructor.105 *106 * Any supplied $args override class property defaults.107 *108 * @since 3.4.0109 *110 * @param WP_Customize_Manager $manager111 * @param string $id An specific ID of the setting. Can be a112 * 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.0158 * @access public159 *160 * @return array {161 * ID data for multidimensional setting.162 *163 * @type string $base ID base164 * @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 update175 * calls get combined into one call, greatly improving performance.176 *177 * @since 4.4.0178 * @access protected179 */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.0202 * @access protected203 * @var int204 */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.0211 * @access public212 *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.1227 * @var mixed228 */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 incoming235 * post value for the setting, then this method will short-circuit since236 * there is no change to preview.237 *238 * @since 3.4.0239 * @since 4.4.0 Added boolean return value.240 * @access public241 *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 needs256 * 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 settings308 * 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.0313 *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 settings320 * 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.0325 *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 current337 * blog is now not the same blog, then this method does a no-op and returns338 * the original value.339 *340 * @since 3.4.0341 *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() will357 * not add the filter in the first place if it has an initial value358 * 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 for369 * the first setting previewed will be used to apply the values for the others.370 *371 * @since 4.4.0372 * @access public373 *374 * @see WP_Customize_Setting::$aggregated_multidimensionals375 * @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 save411 * the value of the setting.412 *413 * @since 3.4.0414 *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 to427 * the base slug of the setting name.428 *429 * @since 3.4.0430 *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.0442 *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.0454 *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.0465 *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.0476 * @access protected477 *478 * @param mixed $default Value to return if root does not exist.479 * @return mixed480 */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 multidimensional490 * will need to override this method to obtain the data from the appropriate491 * 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.0501 * @access protected502 *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 multidimensional520 * will need to override this method to obtain the data from the appropriate521 * 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.0531 *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 settings549 * 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.0554 *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.0568 * @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.0578 * @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.0588 *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 to602 * the base slug of the setting name.603 *604 * For settings handled as theme_mods or options, see those corresponding605 * functions for available hooks.606 *607 * @since 3.4.0608 *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.0625 *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.0636 *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.0652 *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.0669 *670 * @param $root671 * @param $keys672 * @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 value698 $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.0719 *720 * @param $root721 * @param $keys722 * @param mixed $value The value to update.723 * @return mixed724 */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.0743 *744 * @param $root745 * @param $keys746 * @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.0761 *762 * @param $root763 * @param $keys764 * @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.0778 *779 * @see WP_Customize_Setting780 */781 class WP_Customize_Filter_Setting extends WP_Customize_Setting {782 783 /**784 * @since 3.4.0785 */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.0795 *796 * @see WP_Customize_Setting797 */798 final class WP_Customize_Header_Image_Setting extends WP_Customize_Setting {799 public $id = 'header_image_data';800 801 /**802 * @since 3.4.0803 *804 * @global Custom_Image_Header $custom_image_header805 *806 * @param $value807 */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 else819 $custom_image_header->set_header_image( $value );820 }821 }822 9 823 10 /** … … 840 27 } 841 28 } 842 843 /**844 * Customize Setting to represent a nav_menu.845 *846 * Subclass of WP_Customize_Setting to represent a nav_menu taxonomy term, and847 * the IDs for the nav_menu_items associated with the nav menu.848 *849 * @since 4.3.0850 *851 * @see WP_Customize_Setting852 */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.0865 * @access public866 * @var string867 */868 public $type = self::TYPE;869 870 /**871 * Default setting value.872 *873 * @since 4.3.0874 * @access public875 * @var array876 *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.0903 * @access public904 * @var string905 */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.0914 * @access public915 * @var int916 */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.0923 * @access protected924 * @var array925 */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 filter932 * so that JavaScript can update the settings to refer to the newly-assigned933 * post ID. This value is always negative to indicate it does not refer to934 * a real post.935 *936 * @since 4.3.0937 * @access public938 * @var int939 *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_id947 * which ensures that we can apply the proper filters.948 *949 * @since 4.3.0950 * @access public951 * @var int952 */953 public $original_nav_menu_term_id;954 955 /**956 * Whether or not preview() was called.957 *958 * @since 4.3.0959 * @access protected960 * @var bool961 */962 protected $is_previewed = false;963 964 /**965 * Whether or not update() was called.966 *967 * @since 4.3.0968 * @access protected969 * @var bool970 */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.0980 * @access public981 * @var string updated|inserted|deleted|error982 *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.0992 * @access public993 * @var WP_Error994 *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.01006 * @access public1007 *1008 * @param WP_Customize_Manager $manager Bootstrap Customizer instance.1009 * @param string $id An specific ID of the setting. Can be a1010 * 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.01043 * @access public1044 *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.01059 * @access public1060 *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.01107 * @access protected1108 *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.01203 * @since 4.4.0 Added boolean return value.1204 * @access public1205 *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.01243 * @access public1244 *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_id1259 ||1260 $menu->term_id === $current_nav_menu_term_id1261 );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_item1269 ||1270 true === $this_item['_invalid']1271 ||1272 (1273 $this->original_nav_menu_term_id === $menu->term_id1274 &&1275 $current_nav_menu_term_id !== $this->original_nav_menu_term_id1276 )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_id1293 );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.01317 * @access public1318 * @static1319 *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.01353 * @access public1354 *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, but1407 * we remove that in this override.1408 *1409 * @since 4.3.01410 * @access public1411 *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 client1486 * via the customize_save_response filter. Likewise, any errors will be exported1487 * 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.01492 * @access protected1493 *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 deleted1497 * entirely. See WP_Customize_Nav_Menu_Item_Setting::$default for what the value1498 * should consist of.1499 * @return null|void1500 */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_data1605 );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.01627 * @access public1628 *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, and1653 * the IDs for the nav_menu_items associated with the nav menu.1654 *1655 * @since 4.3.01656 *1657 * @see wp_get_nav_menu_object()1658 * @see WP_Customize_Setting1659 */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.01672 * @access public1673 * @var string1674 */1675 public $type = self::TYPE;1676 1677 /**1678 * Default setting value.1679 *1680 * @since 4.3.01681 * @access public1682 * @var array1683 *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.01697 * @access public1698 * @var string1699 */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.01708 * @access public1709 * @var int1710 */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 filter1717 * so that JavaScript can update the settings to refer to the newly-assigned1718 * term ID. This value is always negative to indicate it does not refer to1719 * a real term.1720 *1721 * @since 4.3.01722 * @access public1723 * @var int1724 *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.01734 * @access protected1735 * @var bool1736 */1737 protected $is_previewed = false;1738 1739 /**1740 * Whether or not update() was called.1741 *1742 * @since 4.3.01743 * @access protected1744 * @var bool1745 */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.01755 * @access public1756 * @var string updated|inserted|deleted|error1757 *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.01767 * @access public1768 * @var WP_Error1769 *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.01781 * @access public1782 *1783 * @param WP_Customize_Manager $manager Bootstrap Customizer instance.1784 * @param string $id An specific ID of the setting. Can be a1785 * 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.01808 * @access public1809 *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.01854 * @since 4.4.0 Added boolean return value1855 * @access public1856 *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.01889 * @access public1890 *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 array1896 */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'] === true1940 1941 return $menus;1942 }1943 1944 /**1945 * Temporary non-closure passing of orderby value to function.1946 *1947 * @since 4.3.01948 * @access protected1949 * @var string1950 *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.01962 * @access protected1963 * @param object $menu11964 * @param object $menu21965 * @return int1966 *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.01980 * @access public1981 *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|null1987 */1988 public function filter_wp_get_nav_menu_object( $menu_obj, $menu_id ) {1989 $ok = (1990 get_current_blog_id() === $this->_previewed_blog_id1991 &&1992 is_int( $menu_id )1993 &&1994 $menu_id === $this->term_id1995 );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.02029 * @access public2030 *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, but2053 * we remove that in this override.2054 *2055 * @since 4.3.02056 * @access public2057 *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 protected2099 * @since 4.3.02100 * @var array2101 *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 client2110 * via the customize_save_response filter. Likewise, any errors will be exported2111 * 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.02116 * @access protected2117 *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|void2130 */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_add2195 );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.02239 * @access protected2240 *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.02269 * @access public2270 *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_updates2295 );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 5 5 * @package WordPress 6 6 * @subpackage Customize 7 * @since 3.4.07 * @since 4.4.0 8 8 */ 9 10 /**11 * Customize Setting class.12 *13 * Handles saving and sanitizing of settings.14 *15 * @since 3.4.016 *17 * @see WP_Customize_Manager18 */19 class WP_Customize_Setting {20 /**21 * @access public22 * @var WP_Customize_Manager23 */24 public $manager;25 26 /**27 * Unique string identifier for the setting.28 *29 * @access public30 * @var string31 */32 public $id;33 34 /**35 * @access public36 * @var string37 */38 public $type = 'theme_mod';39 40 /**41 * Capability required to edit this setting.42 *43 * @var string44 */45 public $capability = 'edit_theme_options';46 47 /**48 * Feature a theme is required to support to enable this setting.49 *50 * @access public51 * @var string52 */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 callback61 */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 the69 * preview when loading the Customizer. Normally a setting only is synced to70 * the preview if it has been changed. This allows the setting to be sent71 * from the start.72 *73 * @since 4.2.074 * @access public75 * @var bool76 */77 public $dirty = false;78 79 /**80 * @var array81 */82 protected $id_data = array();83 84 /**85 * Cache of multidimensional values to improve performance.86 *87 * @since 4.4.088 * @access protected89 * @var array90 * @static91 */92 protected static $aggregated_multidimensionals = array();93 94 /**95 * Whether the multidimensional setting is aggregated.96 *97 * @since 4.4.098 * @access protected99 * @var bool100 */101 protected $is_multidimensional_aggregated = false;102 103 /**104 * Constructor.105 *106 * Any supplied $args override class property defaults.107 *108 * @since 3.4.0109 *110 * @param WP_Customize_Manager $manager111 * @param string $id An specific ID of the setting. Can be a112 * 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.0158 * @access public159 *160 * @return array {161 * ID data for multidimensional setting.162 *163 * @type string $base ID base164 * @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 update175 * calls get combined into one call, greatly improving performance.176 *177 * @since 4.4.0178 * @access protected179 */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.0202 * @access protected203 * @var int204 */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.0211 * @access public212 *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.1227 * @var mixed228 */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 incoming235 * post value for the setting, then this method will short-circuit since236 * there is no change to preview.237 *238 * @since 3.4.0239 * @since 4.4.0 Added boolean return value.240 * @access public241 *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 needs256 * 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 settings308 * 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.0313 *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 settings320 * 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.0325 *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 current337 * blog is now not the same blog, then this method does a no-op and returns338 * the original value.339 *340 * @since 3.4.0341 *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() will357 * not add the filter in the first place if it has an initial value358 * 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 for369 * the first setting previewed will be used to apply the values for the others.370 *371 * @since 4.4.0372 * @access public373 *374 * @see WP_Customize_Setting::$aggregated_multidimensionals375 * @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 save411 * the value of the setting.412 *413 * @since 3.4.0414 *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 to427 * the base slug of the setting name.428 *429 * @since 3.4.0430 *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.0442 *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.0454 *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.0465 *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.0476 * @access protected477 *478 * @param mixed $default Value to return if root does not exist.479 * @return mixed480 */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 multidimensional490 * will need to override this method to obtain the data from the appropriate491 * 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.0501 * @access protected502 *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 multidimensional520 * will need to override this method to obtain the data from the appropriate521 * 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.0531 *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 settings549 * 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.0554 *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.0568 * @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.0578 * @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.0588 *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 to602 * the base slug of the setting name.603 *604 * For settings handled as theme_mods or options, see those corresponding605 * functions for available hooks.606 *607 * @since 3.4.0608 *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.0625 *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.0636 *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.0652 *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.0669 *670 * @param $root671 * @param $keys672 * @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 value698 $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.0719 *720 * @param $root721 * @param $keys722 * @param mixed $value The value to update.723 * @return mixed724 */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.0743 *744 * @param $root745 * @param $keys746 * @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.0761 *762 * @param $root763 * @param $keys764 * @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 9 772 10 /** … … 786 24 public function update( $value ) {} 787 25 } 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.0795 *796 * @see WP_Customize_Setting797 */798 final class WP_Customize_Header_Image_Setting extends WP_Customize_Setting {799 public $id = 'header_image_data';800 801 /**802 * @since 3.4.0803 *804 * @global Custom_Image_Header $custom_image_header805 *806 * @param $value807 */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 else819 $custom_image_header->set_header_image( $value );820 }821 }822 823 /**824 * Customizer Background Image Setting class.825 *826 * @since 3.4.0827 *828 * @see WP_Customize_Setting829 */830 final class WP_Customize_Background_Image_Setting extends WP_Customize_Setting {831 public $id = 'background_image_thumb';832 833 /**834 * @since 3.4.0835 *836 * @param $value837 */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, and847 * the IDs for the nav_menu_items associated with the nav menu.848 *849 * @since 4.3.0850 *851 * @see WP_Customize_Setting852 */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.0865 * @access public866 * @var string867 */868 public $type = self::TYPE;869 870 /**871 * Default setting value.872 *873 * @since 4.3.0874 * @access public875 * @var array876 *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.0903 * @access public904 * @var string905 */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.0914 * @access public915 * @var int916 */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.0923 * @access protected924 * @var array925 */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 filter932 * so that JavaScript can update the settings to refer to the newly-assigned933 * post ID. This value is always negative to indicate it does not refer to934 * a real post.935 *936 * @since 4.3.0937 * @access public938 * @var int939 *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_id947 * which ensures that we can apply the proper filters.948 *949 * @since 4.3.0950 * @access public951 * @var int952 */953 public $original_nav_menu_term_id;954 955 /**956 * Whether or not preview() was called.957 *958 * @since 4.3.0959 * @access protected960 * @var bool961 */962 protected $is_previewed = false;963 964 /**965 * Whether or not update() was called.966 *967 * @since 4.3.0968 * @access protected969 * @var bool970 */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.0980 * @access public981 * @var string updated|inserted|deleted|error982 *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.0992 * @access public993 * @var WP_Error994 *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.01006 * @access public1007 *1008 * @param WP_Customize_Manager $manager Bootstrap Customizer instance.1009 * @param string $id An specific ID of the setting. Can be a1010 * 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.01043 * @access public1044 *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.01059 * @access public1060 *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.01107 * @access protected1108 *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.01203 * @since 4.4.0 Added boolean return value.1204 * @access public1205 *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.01243 * @access public1244 *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_id1259 ||1260 $menu->term_id === $current_nav_menu_term_id1261 );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_item1269 ||1270 true === $this_item['_invalid']1271 ||1272 (1273 $this->original_nav_menu_term_id === $menu->term_id1274 &&1275 $current_nav_menu_term_id !== $this->original_nav_menu_term_id1276 )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_id1293 );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.01317 * @access public1318 * @static1319 *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.01353 * @access public1354 *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, but1407 * we remove that in this override.1408 *1409 * @since 4.3.01410 * @access public1411 *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 client1486 * via the customize_save_response filter. Likewise, any errors will be exported1487 * 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.01492 * @access protected1493 *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 deleted1497 * entirely. See WP_Customize_Nav_Menu_Item_Setting::$default for what the value1498 * should consist of.1499 * @return null|void1500 */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_data1605 );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.01627 * @access public1628 *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, and1653 * the IDs for the nav_menu_items associated with the nav menu.1654 *1655 * @since 4.3.01656 *1657 * @see wp_get_nav_menu_object()1658 * @see WP_Customize_Setting1659 */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.01672 * @access public1673 * @var string1674 */1675 public $type = self::TYPE;1676 1677 /**1678 * Default setting value.1679 *1680 * @since 4.3.01681 * @access public1682 * @var array1683 *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.01697 * @access public1698 * @var string1699 */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.01708 * @access public1709 * @var int1710 */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 filter1717 * so that JavaScript can update the settings to refer to the newly-assigned1718 * term ID. This value is always negative to indicate it does not refer to1719 * a real term.1720 *1721 * @since 4.3.01722 * @access public1723 * @var int1724 *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.01734 * @access protected1735 * @var bool1736 */1737 protected $is_previewed = false;1738 1739 /**1740 * Whether or not update() was called.1741 *1742 * @since 4.3.01743 * @access protected1744 * @var bool1745 */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.01755 * @access public1756 * @var string updated|inserted|deleted|error1757 *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.01767 * @access public1768 * @var WP_Error1769 *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.01781 * @access public1782 *1783 * @param WP_Customize_Manager $manager Bootstrap Customizer instance.1784 * @param string $id An specific ID of the setting. Can be a1785 * 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.01808 * @access public1809 *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.01854 * @since 4.4.0 Added boolean return value1855 * @access public1856 *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.01889 * @access public1890 *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 array1896 */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 );