From 3f050f87c3867ffb6b55ba9213d63eea3cc7647c Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Sun, 19 Jun 2016 12:24:23 +0000 Subject: [PATCH] Menus: Support nested array variables in POST data when saving menus. [36510] allowed larger menus to be created in the Edit Menu screen by JSON-encoding the entire form into a single input field. However, it did not correctly handle nested arrays. This introduces a new `_wp_expand_nav_menu_post_data()` helper function to handle this POST data which uses `array_replace_recursive()` internally. Since the latter is only available on PHP 5.3+, we add a compatibility function to ensure PHP 5.2 support. Props ericlewis, neverything, swissspidy. Fixes #36590 for trunk. See #14134. git-svn-id: https://develop.svn.wordpress.org/trunk@37748 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/nav-menu.php | 43 +++++++ src/wp-admin/nav-menus.php | 22 +--- src/wp-includes/compat.php | 50 +++++++++ .../tests/menu/wpExpandNavMenuPostData.php | 105 ++++++++++++++++++ 4 files changed, 200 insertions(+), 20 deletions(-) create mode 100644 tests/phpunit/tests/menu/wpExpandNavMenuPostData.php diff --git a/src/wp-admin/includes/nav-menu.php b/src/wp-admin/includes/nav-menu.php index 349b6dbfc8..f07b25e36c 100644 --- a/src/wp-admin/includes/nav-menu.php +++ b/src/wp-admin/includes/nav-menu.php @@ -1059,3 +1059,46 @@ function wp_nav_menu_update_menu_items ( $nav_menu_selected_id, $nav_menu_select return $messages; } + +/** + * If a JSON blob of navigation menu data is in POST data, expand it and inject + * it into `$_POST` to avoid PHP `max_input_vars` limitations. See #14134. + * + * @ignore + * @since 4.5.3 + * @access private + */ +function _wp_expand_nav_menu_post_data() { + if ( ! isset( $_POST['nav-menu-data'] ) ) { + return; + } + + $data = json_decode( stripslashes( $_POST['nav-menu-data'] ) ); + + if ( ! is_null( $data ) && $data ) { + foreach ( $data as $post_input_data ) { + // For input names that are arrays (e.g. `menu-item-db-id[3][4][5]`), + // derive the array path keys via regex and set the value in $_POST. + preg_match( '#([^\[]*)(\[(.+)\])?#', $post_input_data->name, $matches ); + + $array_bits = array( $matches[1] ); + + if ( isset( $matches[3] ) ) { + $array_bits = array_merge( $array_bits, explode( '][', $matches[3] ) ); + } + + $new_post_data = array(); + + // Build the new array value from leaf to trunk. + for ( $i = count( $array_bits ) - 1; $i >= 0; $i -- ) { + if ( $i == count( $array_bits ) - 1 ) { + $new_post_data[ $array_bits[ $i ] ] = wp_slash( $post_input_data->value ); + } else { + $new_post_data = array( $array_bits[ $i ] => $new_post_data ); + } + } + + $_POST = array_replace_recursive( $_POST, $new_post_data ); + } + } +} diff --git a/src/wp-admin/nav-menus.php b/src/wp-admin/nav-menus.php index 8e6209c83f..ed42790ca8 100644 --- a/src/wp-admin/nav-menus.php +++ b/src/wp-admin/nav-menus.php @@ -53,26 +53,8 @@ $action = isset( $_REQUEST['action'] ) ? $_REQUEST['action'] : 'edit'; * If a JSON blob of navigation menu data is found, expand it and inject it * into `$_POST` to avoid PHP `max_input_vars` limitations. See #14134. */ -if ( isset( $_POST['nav-menu-data'] ) ) { - $data = json_decode( stripslashes( $_POST['nav-menu-data'] ) ); - if ( ! is_null( $data ) && $data ) { - foreach ( $data as $post_input_data ) { - // For input names that are arrays (e.g. `menu-item-db-id[3]`), derive the array path keys via regex. - if ( preg_match( '#(.*)\[(\w+)\]#', $post_input_data->name, $matches ) ) { - if ( empty( $_POST[ $matches[1] ] ) ) { - $_POST[ $matches[1] ] = array(); - } - // Cast input elements with a numeric array index to integers. - if ( is_numeric( $matches[2] ) ) { - $matches[2] = (int) $matches[2]; - } - $_POST[ $matches[1] ][ $matches[2] ] = wp_slash( $post_input_data->value ); - } else { - $_POST[ $post_input_data->name ] = wp_slash( $post_input_data->value ); - } - } - } -} +_wp_expand_nav_menu_post_data(); + switch ( $action ) { case 'add-menu-item': check_admin_referer( 'add-menu_item', 'menu-settings-column-nonce' ); diff --git a/src/wp-includes/compat.php b/src/wp-includes/compat.php index 783debfa5e..ad49ba3a0b 100644 --- a/src/wp-includes/compat.php +++ b/src/wp-includes/compat.php @@ -435,6 +435,56 @@ if ( ! function_exists( 'random_int' ) ) { require ABSPATH . WPINC . '/random_compat/random.php'; } +if ( ! function_exists( 'array_replace_recursive' ) ) : + /** + * PHP-agnostic version of {@link array_replace_recursive()}. + * + * The array_replace_recursive() function is a PHP 5.3 function. WordPress + * currently supports down to PHP 5.2, so this method is a workaround + * for PHP 5.2. + * + * Note: array_replace_recursive() supports infinite arguments, but for our use- + * case, we only need to support two arguments. + * + * Subject to removal once WordPress makes PHP 5.3.0 the minimum requirement. + * + * @since 4.5.3 + * + * @see http://php.net/manual/en/function.array-replace-recursive.php#109390 + * + * @param array $base Array with keys needing to be replaced. + * @param array $replacements Array with the replaced keys. + * + * @return array + */ + function array_replace_recursive( $base = array(), $replacements = array() ) { + foreach ( array_slice( func_get_args(), 1 ) as $replacements ) { + $bref_stack = array( &$base ); + $head_stack = array( $replacements ); + + do { + end( $bref_stack ); + + $bref = &$bref_stack[ key( $bref_stack ) ]; + $head = array_pop( $head_stack ); + + unset( $bref_stack[ key( $bref_stack ) ] ); + + foreach ( array_keys( $head ) as $key ) { + if ( isset( $key, $bref ) && is_array( $bref[ $key ] ) && is_array( $head[ $key ] ) ) { + $bref_stack[] = &$bref[ $key ]; + $head_stack[] = $head[ $key ]; + } else { + $bref[ $key ] = $head[ $key ]; + } + } + } while ( count( $head_stack ) ); + } + + return $base; + } +endif; + // SPL can be disabled on PHP 5.2 if ( ! function_exists( 'spl_autoload_register' ) ): $_wp_spl_autoloaders = array(); diff --git a/tests/phpunit/tests/menu/wpExpandNavMenuPostData.php b/tests/phpunit/tests/menu/wpExpandNavMenuPostData.php new file mode 100644 index 0000000000..361c4f9dcd --- /dev/null +++ b/tests/phpunit/tests/menu/wpExpandNavMenuPostData.php @@ -0,0 +1,105 @@ +name = 'yesorno'; + $data[0]->value = 'yes'; + $_POST['nav-menu-data'] = addslashes( json_encode( $data ) ); + + _wp_expand_nav_menu_post_data(); + + $expected = array( + 'nav-menu-data' => $_POST['nav-menu-data'], + 'yesorno' => 'yes' + ); + + $this->assertEquals( $expected, $_POST ); + } + + public function test_multidimensional_nested_array_should_expand() { + include_once( ABSPATH . 'wp-admin/includes/nav-menu.php' ); + + if ( empty( $_POST ) ) { + $_POST = array(); + } + + $data = array(); + $data[0] = new StdClass; + $data[0]->name = 'would[1][do][the][trick]'; + $data[0]->value = 'yes'; + $_POST['nav-menu-data'] = addslashes( json_encode( $data ) ); + + _wp_expand_nav_menu_post_data(); + + $expected = array( + 'nav-menu-data' => $_POST['nav-menu-data'], + 'would' => array( + 1 => array( + 'do' => array( + 'the' => array( + 'trick' => 'yes', + ), + ), + ), + ), + ); + $this->assertEquals( $expected, $_POST ); + } + + public function test_multidimensional_nested_array_should_expand_and_merge() { + include_once( ABSPATH . 'wp-admin/includes/nav-menu.php' ); + + if ( empty( $_POST ) ) { + $_POST = array(); + } + + $data = array(); + $data[0] = new StdClass; + $data[0]->name = 'would[1][do][the][trick]'; + $data[0]->value = 'yes'; + $data[1] = new StdClass; + $data[1]->name = 'would[2][do][the][trick]'; + $data[1]->value = 'yes'; + $data[2] = new StdClass; + $data[2]->name = 'would[2][do][the][job]'; + $data[2]->value = 'yes'; + $_POST['nav-menu-data'] = addslashes( json_encode( $data ) ); + + _wp_expand_nav_menu_post_data(); + + $expected = array( + 'nav-menu-data' => $_POST['nav-menu-data'], + 'would' => array( + 1 => array( + 'do' => array( + 'the' => array( + 'trick' => 'yes', + ), + ), + ), + 2 => array( + 'do' => array( + 'the' => array( + 'trick' => 'yes', + 'job' => 'yes', + ), + ), + ), + ), + ); + + $this->assertEquals( $expected, $_POST ); + } +}