From 03224dc51735516bdaa1e14a5051961e70ae4e4c Mon Sep 17 00:00:00 2001 From: Kyle Benk Date: Tue, 16 Jun 2020 11:15:27 -0600 Subject: [PATCH 01/10] Add new menu item context --- fieldmanager.php | 4 + php/class-fieldmanager-field.php | 8 + .../class-fieldmanager-context-menuitem.php | 230 ++++++++++++++++++ 3 files changed, 242 insertions(+) create mode 100644 php/context/class-fieldmanager-context-menuitem.php diff --git a/fieldmanager.php b/fieldmanager.php index 64b166b6a9..ff93a4f789 100644 --- a/fieldmanager.php +++ b/fieldmanager.php @@ -358,6 +358,10 @@ function fm_calculate_context() { $calculated_context = array( 'term', sanitize_text_field( wp_unslash( $_GET['taxonomy'] ) ) ); // WPCS: input var okay. } break; + // Context = "nav-menu". + case 'nav-menus.php': + $calculated_context = array( 'nav_menu', null ); + break; } } } diff --git a/php/class-fieldmanager-field.php b/php/class-fieldmanager-field.php index ca6c7f32c8..6c8bf55dcc 100644 --- a/php/class-fieldmanager-field.php +++ b/php/class-fieldmanager-field.php @@ -1293,6 +1293,14 @@ public function add_quickedit_box( $title, $post_types, $column_display_callback return new Fieldmanager_Context_QuickEdit( $title, $post_types, $column_display_callback, $column_title, $this ); } + /** + * Add this group to an nav menu. + */ + public function add_nav_menu_fields() { + $this->require_base(); + return new Fieldmanager_Context_MenuItem( $this ); + } + /** * Add this group to an options page. * diff --git a/php/context/class-fieldmanager-context-menuitem.php b/php/context/class-fieldmanager-context-menuitem.php new file mode 100644 index 0000000000..10701257c3 --- /dev/null +++ b/php/context/class-fieldmanager-context-menuitem.php @@ -0,0 +1,230 @@ +fm = $fm; + + $this->fm->data_type = 'post'; + + // Save the original form name. + $this->original_form_name = $this->fm->name; + + add_filter( 'wp_nav_menu_item_custom_fields', array( $this, 'add_fields' ), 10, 5 ); + add_action( 'wp_update_nav_menu_item', array( $this, 'save_fields' ), 10, 3 ); + } + + /** + * Get the menu item form name given the menu ID. This allows FM to save meta + * data to each menu item within the same POST request. + * + * @param int $menu_id The menu ID. + * @return string The form name. + */ + public function get_menu_item_form_name( $menu_id ) { + return $this->original_form_name . '_fm-menu-item-id-' . absint( $menu_id ); + } + + /** + * Parse the form name, assuming it already contains the menu ID, into its + * original form name. + * + * @param string $form_name The form name. + * @return mixed False if form name does not exist or an array of menu ID and name. + */ + public function parse_form_name( $form_name ) { + // Not a menu item form name. + if ( false === strpos( $form_name, '_fm-menu-item-id-', true ) ) { + return false; + } + + // Break out the original name from the menu item ID. + $parts = explode( '_fm-menu-item-id-', $form_name ); + + if ( ! empty( $parts[0] ) && ! empty( $parts[1] ) ) { + return [ + 'name' => $parts[0], + 'id' => absint( $parts[1] ), + ]; + } + + return false; + } + + + /** + * Add fields to the editor of a nav menu item. + * + * @param int $item_id Menu item ID. + */ + public function add_fields( $item_id ) { + // Set the ID. + $this->fm->data_id = $item_id; + + // Ensure the ID is part of the name. + $this->fm->name = $this->get_menu_item_form_name( $item_id ); + + // Render the field. + $this->render_field(); + } + + /** + * Save post meta for nav menu items. + * + * @param int $menu_id The ID of the menu. + * @param int $menu_item_db_id The ID of the menu item. + * @param array $menu_item_args Menu item args. + */ + public function save_fields( $menu_id, $menu_item_db_id, $menu_item_args ) { + // Ensure the ID is part of the name. + $this->fm->name = $this->get_menu_item_form_name( $menu_item_db_id ); + + // Ensure that the nonce is set and valid. + if ( ! $this->is_valid_nonce() ) { + return; + } + + // Make sure the current user can save this post. + if ( ! current_user_can( 'edit_posts' ) ) { + $this->fm->_unauthorized_access( __( 'User cannot edit this menu item', 'newsnet' ) ); + return; + } + + $this->save_to_post_meta( $menu_item_db_id ); + } + + /** + * Helper to save an array of data to post meta. + * + * @param int $post_id The post ID. + * @param array $data The post data. + */ + public function save_to_post_meta( $post_id, $data = null ) { + $this->fm->data_id = $post_id; + $this->fm->data_type = 'post'; + + $this->save( $data ); + } + + /** + * Get post meta. + * + * @see get_post_meta(). + * + * @param int $post_id Post ID. + * @param string $meta_key Optional. The meta key to retrieve. By default, returns + * data for all keys. Default empty. + * @param bool $single Optional. Whether to return a single value. Default false. + */ + protected function get_data( $post_id, $meta_key, $single = false ) { + $parts = $this->parse_form_name( $meta_key ); + + if ( ! empty( $parts['name'] ) && ! empty( $parts['id'] ) ) { + $post_id = $parts['id']; + $meta_key = $parts['name']; + } + + return get_post_meta( $post_id, $meta_key, $single ); + } + + /** + * Add post meta. + * + * @see add_post_meta(). + * + * @param int $post_id Post ID. + * @param string $meta_key Metadata name. + * @param mixed $meta_value Metadata value. Must be serializable if non-scalar. + * @param bool $unique Optional. Whether the same key should not be added. + * Default false. + */ + protected function add_data( $post_id, $meta_key, $meta_value, $unique = false ) { + $parts = $this->parse_form_name( $meta_key ); + + if ( ! empty( $parts['name'] ) && ! empty( $parts['id'] ) ) { + $post_id = $parts['id']; + $meta_key = $parts['name']; + } + + return add_post_meta( $post_id, $meta_key, $meta_value, $unique ); + } + + /** + * Update post meta. + * + * @see update_post_meta(). + * + * @param int $post_id Post ID. + * @param string $meta_key Metadata key. + * @param mixed $meta_value Metadata value. Must be serializable if non-scalar. + * @param mixed $data_prev_value Optional. Previous value to check before removing. + * Default empty. + */ + protected function update_data( $post_id, $meta_key, $meta_value, $data_prev_value = '' ) { + $parts = $this->parse_form_name( $meta_key ); + + if ( ! empty( $parts['name'] ) && ! empty( $parts['id'] ) ) { + $post_id = $parts['id']; + $meta_key = $parts['name']; + } + + $meta_value = $this->sanitize_scalar_value( $meta_value ); + return update_post_meta( $post_id, $meta_key, $meta_value, $data_prev_value ); + } + + /** + * Delete post meta. + * + * @see delete_post_meta(). + * + * @param int $post_id Post ID. + * @param string $meta_key Metadata name. + * @param mixed $meta_value Optional. Metadata value. Must be serializable if + * non-scalar. Default empty. + */ + protected function delete_data( $post_id, $meta_key, $meta_value = '' ) { + $parts = $this->parse_form_name( $meta_key ); + + if ( ! empty( $parts['name'] ) && ! empty( $parts['id'] ) ) { + $post_id = $parts['id']; + $meta_key = $parts['name']; + } + + return delete_post_meta( $post_id, $meta_key, $meta_value ); + } +} From 4123e177d89adb1d989f07a54d3d72557553f0ff Mon Sep 17 00:00:00 2001 From: Kyle Benk Date: Tue, 16 Jun 2020 11:33:38 -0600 Subject: [PATCH 02/10] Add unit tests for context --- .../test-fieldmanager-context-menuitem.php | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 tests/php/test-fieldmanager-context-menuitem.php diff --git a/tests/php/test-fieldmanager-context-menuitem.php b/tests/php/test-fieldmanager-context-menuitem.php new file mode 100644 index 0000000000..e9029cc3ac --- /dev/null +++ b/tests/php/test-fieldmanager-context-menuitem.php @@ -0,0 +1,155 @@ +post = array( + 'post_type' => 'nav_menu_item', + 'post_status' => 'publish', + 'post_content' => rand_str(), + 'post_title' => rand_str(), + ); + + // insert a post + $this->post_id = wp_insert_post( $this->post ); + + // reload as proper object + $this->post = get_post( $this->post_id ); + } + + /** + * Get valid test data. + * Several tests transform this data to somehow be invalid. + * + * @return array valid test data + */ + private function _get_valid_test_data() { + return array( + 'base_group' => array( + 'test_basic' => 'lorem ipsum', + 'test_textfield' => 'alley interactive', + 'test_htmlfield' => 'Hello world', + 'test_extended' => array( + array( + 'extext' => array( 'first' ), + ), + array( + 'extext' => array( 'second1', 'second2', 'second3' ), + ), + array( + 'extext' => array( 'third' ), + ), + array( + 'extext' => array( 'fourth' ), + ), + ), + ), + ); + } + + /** + * Get a set of elements + * + * @return Fieldmanager_Group + */ + private function _get_elements() { + return new Fieldmanager_Group( + array( + 'name' => 'base_group', + 'children' => array( + 'test_basic' => new Fieldmanager_TextField(), + 'test_textfield' => new Fieldmanager_TextField( + array( + 'index' => '_test_index', + ) + ), + 'test_htmlfield' => new Fieldmanager_Textarea( + array( + 'sanitize' => 'wp_kses_post', + ) + ), + 'test_extended' => new Fieldmanager_Group( + array( + 'limit' => 4, + 'children' => array( + 'extext' => new Fieldmanager_TextField( + array( + 'limit' => 0, + 'name' => 'extext', + 'one_label_per_item' => false, + 'sortable' => true, + 'index' => '_extext_index', + ) + ), + ), + ) + ), + ), + ) + ); + } + + public function test_context_render() { + $base = $this->_get_elements(); + ob_start(); + $base->add_nav_menu_fields()->add_fields( $this->post->ID ); + $str = ob_get_clean(); + // we can't really care about the structure of the HTML, but we can make sure that all fields are here + $this->assertRegExp( '/]+type="hidden"[^>]+name="fieldmanager-base_group_fm-menu-item-id-' . $this->post->ID . '-nonce"/', $str ); + $this->assertRegExp( '/]+type="text"[^>]+name="base_group_fm-menu-item-id-' . $this->post->ID . '\[test_basic\]"/', $str ); + $this->assertRegExp( '/]+type="text"[^>]+name="base_group_fm-menu-item-id-' . $this->post->ID . '\[test_textfield\]"/', $str ); + $this->assertRegExp( '/]+name="base_group_fm-menu-item-id-' . $this->post->ID . '\[test_htmlfield\]"/', $str ); + $this->assertContains( 'name="base_group_fm-menu-item-id-' . $this->post->ID . '[test_extended][0][extext][proto]"', $str ); + $this->assertContains( 'name="base_group_fm-menu-item-id-' . $this->post->ID . '[test_extended][0][extext][0]"', $str ); + } + + public function test_context_save() { + $base = $this->_get_elements(); + $test_data = $this->_get_valid_test_data(); + + $base->add_nav_menu_fields()->save_to_post_meta( $this->post_id, $test_data['base_group'] ); + + $saved_value = get_post_meta( $this->post_id, 'base_group', true ); + $saved_index = get_post_meta( $this->post_id, '_test_index', true ); + + $this->assertEquals( $saved_value['test_basic'], 'lorem ipsum' ); + $this->assertEquals( $saved_index, $saved_value['test_textfield'] ); + $this->assertEquals( $saved_value['test_textfield'], 'alley interactive' ); + $this->assertEquals( $saved_value['test_htmlfield'], 'Hello world' ); + $this->assertEquals( count( $saved_value['test_extended'] ), 4 ); + $this->assertEquals( count( $saved_value['test_extended'][0]['extext'] ), 1 ); + $this->assertEquals( count( $saved_value['test_extended'][1]['extext'] ), 3 ); + $this->assertEquals( count( $saved_value['test_extended'][2]['extext'] ), 1 ); + $this->assertEquals( count( $saved_value['test_extended'][3]['extext'] ), 1 ); + $this->assertEquals( $saved_value['test_extended'][1]['extext'], array( 'second1', 'second2', 'second3' ) ); + $this->assertEquals( $saved_value['test_extended'][3]['extext'][0], 'fourth' ); + } + + public function test_programmatic_save_posts() { + $base = $this->_get_elements(); + $base->add_nav_menu_fields(); + + $post_id = wp_insert_post( + array( + 'post_type' => 'nav_menu_item', + 'post_name' => 'test-post', + 'post_title' => 'Test Post', + 'post_date' => '2012-10-25 12:34:56', + ) + ); + wp_update_post( + array( + 'ID' => $post_id, + 'post_content' => 'Lorem ipsum dolor sit amet.', + ) + ); + } +} From a481e4e5ad6ae95a3fd5ca6aef43035575e62c3f Mon Sep 17 00:00:00 2001 From: Kyle Benk Date: Tue, 16 Jun 2020 11:34:51 -0600 Subject: [PATCH 03/10] Update text domain --- php/context/class-fieldmanager-context-menuitem.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/php/context/class-fieldmanager-context-menuitem.php b/php/context/class-fieldmanager-context-menuitem.php index 10701257c3..a9b3628446 100644 --- a/php/context/class-fieldmanager-context-menuitem.php +++ b/php/context/class-fieldmanager-context-menuitem.php @@ -121,7 +121,7 @@ public function save_fields( $menu_id, $menu_item_db_id, $menu_item_args ) { // Make sure the current user can save this post. if ( ! current_user_can( 'edit_posts' ) ) { - $this->fm->_unauthorized_access( __( 'User cannot edit this menu item', 'newsnet' ) ); + $this->fm->_unauthorized_access( __( 'User cannot edit this menu item', 'fieldmanager' ) ); return; } From 2b47f8e13f7a3c42c5f97b336efe5da6c5cb8232 Mon Sep 17 00:00:00 2001 From: Kyle Benk Date: Tue, 16 Jun 2020 11:49:04 -0600 Subject: [PATCH 04/10] Add check for wp version --- php/context/class-fieldmanager-context-menuitem.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/php/context/class-fieldmanager-context-menuitem.php b/php/context/class-fieldmanager-context-menuitem.php index a9b3628446..a1c6985893 100644 --- a/php/context/class-fieldmanager-context-menuitem.php +++ b/php/context/class-fieldmanager-context-menuitem.php @@ -38,6 +38,13 @@ class Fieldmanager_Context_MenuItem extends Fieldmanager_Context_Storable { * @param Fieldmanager_Field $fm The base field. */ public function __construct( $fm = null ) { + global $wp_version; + + // Needs WP version 5.4.0 or greater. + if ( version_compare( $wp_version, '5.4.0', '<' ) ) { + return; + } + $this->fm = $fm; $this->fm->data_type = 'post'; From c91f552d1256696af443a36043dc9d16db4e238b Mon Sep 17 00:00:00 2001 From: Kyle Benk Date: Tue, 16 Jun 2020 11:51:46 -0600 Subject: [PATCH 05/10] Update tests --- tests/php/test-fieldmanager-context-menuitem.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/php/test-fieldmanager-context-menuitem.php b/tests/php/test-fieldmanager-context-menuitem.php index e9029cc3ac..7c9e08d69d 100644 --- a/tests/php/test-fieldmanager-context-menuitem.php +++ b/tests/php/test-fieldmanager-context-menuitem.php @@ -1,5 +1,12 @@ Date: Tue, 16 Jun 2020 11:53:47 -0600 Subject: [PATCH 06/10] Do not use array short syntax --- php/context/class-fieldmanager-context-menuitem.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/php/context/class-fieldmanager-context-menuitem.php b/php/context/class-fieldmanager-context-menuitem.php index a1c6985893..9a4f7d693a 100644 --- a/php/context/class-fieldmanager-context-menuitem.php +++ b/php/context/class-fieldmanager-context-menuitem.php @@ -84,10 +84,10 @@ public function parse_form_name( $form_name ) { $parts = explode( '_fm-menu-item-id-', $form_name ); if ( ! empty( $parts[0] ) && ! empty( $parts[1] ) ) { - return [ + return array( 'name' => $parts[0], 'id' => absint( $parts[1] ), - ]; + ); } return false; From c268460e580007ba6a634098feebc1546aaf93a9 Mon Sep 17 00:00:00 2001 From: Kyle Benk Date: Tue, 16 Jun 2020 12:07:37 -0600 Subject: [PATCH 07/10] Check for WP verison 5.4.0 --- .../test-fieldmanager-context-menuitem.php | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/tests/php/test-fieldmanager-context-menuitem.php b/tests/php/test-fieldmanager-context-menuitem.php index 7c9e08d69d..d994575d1a 100644 --- a/tests/php/test-fieldmanager-context-menuitem.php +++ b/tests/php/test-fieldmanager-context-menuitem.php @@ -1,12 +1,5 @@ _get_elements(); ob_start(); $base->add_nav_menu_fields()->add_fields( $this->post->ID ); @@ -119,6 +119,13 @@ public function test_context_render() { } public function test_context_save() { + global $wp_version; + + // Only run these tests for WP versions above 5.4.0. + if ( version_compare( $wp_version, '5.4.0', '<' ) ) { + return; + } + $base = $this->_get_elements(); $test_data = $this->_get_valid_test_data(); @@ -141,6 +148,13 @@ public function test_context_save() { } public function test_programmatic_save_posts() { + global $wp_version; + + // Only run these tests for WP versions above 5.4.0. + if ( version_compare( $wp_version, '5.4.0', '<' ) ) { + return; + } + $base = $this->_get_elements(); $base->add_nav_menu_fields(); From 62a545f9e59269f4ff48654f4737fae59d4b5c14 Mon Sep 17 00:00:00 2001 From: Kyle Benk Date: Wed, 17 Jun 2020 13:39:49 -0600 Subject: [PATCH 08/10] Update unit tests --- .../test-fieldmanager-context-menuitem.php | 45 +++---------------- 1 file changed, 5 insertions(+), 40 deletions(-) diff --git a/tests/php/test-fieldmanager-context-menuitem.php b/tests/php/test-fieldmanager-context-menuitem.php index d994575d1a..3857790b8e 100644 --- a/tests/php/test-fieldmanager-context-menuitem.php +++ b/tests/php/test-fieldmanager-context-menuitem.php @@ -11,18 +11,10 @@ public function setUp() { parent::setUp(); Fieldmanager_Field::$debug = true; - $this->post = array( + $this->post = self::factory()->post->create_and_get( array( 'post_type' => 'nav_menu_item', 'post_status' => 'publish', - 'post_content' => rand_str(), - 'post_title' => rand_str(), - ); - - // insert a post - $this->post_id = wp_insert_post( $this->post ); - - // reload as proper object - $this->post = get_post( $this->post_id ); + ) ); } /** @@ -129,10 +121,10 @@ public function test_context_save() { $base = $this->_get_elements(); $test_data = $this->_get_valid_test_data(); - $base->add_nav_menu_fields()->save_to_post_meta( $this->post_id, $test_data['base_group'] ); + $base->add_nav_menu_fields()->save_to_post_meta( $this->post->ID, $test_data['base_group'] ); - $saved_value = get_post_meta( $this->post_id, 'base_group', true ); - $saved_index = get_post_meta( $this->post_id, '_test_index', true ); + $saved_value = get_post_meta( $this->post->ID, 'base_group', true ); + $saved_index = get_post_meta( $this->post->ID, '_test_index', true ); $this->assertEquals( $saved_value['test_basic'], 'lorem ipsum' ); $this->assertEquals( $saved_index, $saved_value['test_textfield'] ); @@ -146,31 +138,4 @@ public function test_context_save() { $this->assertEquals( $saved_value['test_extended'][1]['extext'], array( 'second1', 'second2', 'second3' ) ); $this->assertEquals( $saved_value['test_extended'][3]['extext'][0], 'fourth' ); } - - public function test_programmatic_save_posts() { - global $wp_version; - - // Only run these tests for WP versions above 5.4.0. - if ( version_compare( $wp_version, '5.4.0', '<' ) ) { - return; - } - - $base = $this->_get_elements(); - $base->add_nav_menu_fields(); - - $post_id = wp_insert_post( - array( - 'post_type' => 'nav_menu_item', - 'post_name' => 'test-post', - 'post_title' => 'Test Post', - 'post_date' => '2012-10-25 12:34:56', - ) - ); - wp_update_post( - array( - 'ID' => $post_id, - 'post_content' => 'Lorem ipsum dolor sit amet.', - ) - ); - } } From 76b15a492b1b28873aff87ccd803136f921366ae Mon Sep 17 00:00:00 2001 From: Kyle Benk Date: Thu, 18 Jun 2020 12:31:04 -0600 Subject: [PATCH 09/10] Update cap --- php/context/class-fieldmanager-context-menuitem.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/php/context/class-fieldmanager-context-menuitem.php b/php/context/class-fieldmanager-context-menuitem.php index 9a4f7d693a..9ddaf07f3e 100644 --- a/php/context/class-fieldmanager-context-menuitem.php +++ b/php/context/class-fieldmanager-context-menuitem.php @@ -127,7 +127,7 @@ public function save_fields( $menu_id, $menu_item_db_id, $menu_item_args ) { } // Make sure the current user can save this post. - if ( ! current_user_can( 'edit_posts' ) ) { + if ( ! current_user_can( 'edit_theme_options' ) ) { $this->fm->_unauthorized_access( __( 'User cannot edit this menu item', 'fieldmanager' ) ); return; } From 01bcf854b192835cedfb2b972b98941031317524 Mon Sep 17 00:00:00 2001 From: Kyle Benk Date: Mon, 22 Jun 2020 10:53:03 -0600 Subject: [PATCH 10/10] Update to 1.3.0 --- fieldmanager.php | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/fieldmanager.php b/fieldmanager.php index ff93a4f789..20fa2faa79 100644 --- a/fieldmanager.php +++ b/fieldmanager.php @@ -3,7 +3,7 @@ * Fieldmanager Base Plugin File. * * @package Fieldmanager - * @version 1.2.6 + * @version 1.3.0 */ /* @@ -11,14 +11,14 @@ Plugin URI: https://github.com/alleyinteractive/wordpress-fieldmanager Description: Add fields to WordPress programatically. Author: Alley -Version: 1.2.6 +Version: 1.3.0 Author URI: https://www.alley.co/ */ /** * Current version of Fieldmanager. */ -define( 'FM_VERSION', '1.2.6' ); +define( 'FM_VERSION', '1.3.0' ); /** * Filesystem path to Fieldmanager. diff --git a/package.json b/package.json index 6a035fe220..04a998dec1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "Fieldmanager", "description": "Fieldmanager is a comprehensive toolkit for building forms, metaboxes, and custom admin screens for WordPress.", - "version": "1.2.6", + "version": "1.3.0", "repository": { "type": "git", "url": "https://github.com/alleyinteractive/wordpress-fieldmanager"