From f0c2837586aa1226426af7da1d5049f5403b7389 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 29 Aug 2016 13:56:24 -0700 Subject: [PATCH] Add support for registering customize post meta via register_meta() --- ...wp-customize-featured-image-controller.php | 9 +- ...-wp-customize-page-template-controller.php | 8 ++ ...class-wp-customize-postmeta-controller.php | 4 + php/class-wp-customize-posts.php | 85 ++++++++++++++----- .../test-class-wp-customize-posts-preview.php | 18 ++++ tests/php/test-class-wp-customize-posts.php | 84 +++++++++++++++++- 6 files changed, 185 insertions(+), 23 deletions(-) diff --git a/php/class-wp-customize-featured-image-controller.php b/php/class-wp-customize-featured-image-controller.php index bf47d2b..78327e2 100644 --- a/php/class-wp-customize-featured-image-controller.php +++ b/php/class-wp-customize-featured-image-controller.php @@ -376,13 +376,16 @@ public function sanitize_value( $attachment_id ) { } /** - * Sanitize (and validate) an input for a specific setting instance. + * Validate the value that has been sanitized by the `sanitize_value` method. + * + * Since the `sanitize_callback` used by `sanitize_meta()` cannot return + * any `WP_Error` to represent invalidity, a secondary * * @see update_metadata() * * @param string $attachment_id The value to sanitize. * @param WP_Customize_Postmeta_Setting $setting Setting. - * @return mixed|WP_Error Sanitized value or `WP_Error` if invalid. + * @return mixed|WP_Error|null Sanitized value or `WP_Error` if invalid (or `null` if before WP 4.6). */ public function sanitize_setting( $attachment_id, WP_Customize_Postmeta_Setting $setting ) { unset( $setting ); @@ -398,7 +401,7 @@ public function sanitize_setting( $attachment_id, WP_Customize_Postmeta_Setting /* * Note that at this point, sanitize_meta() has already been called in WP_Customize_Postmeta_Setting::sanitize(), - * and the meta is registered wit WP_Customize_Featured_Image_Controller::sanitize_value() as the sanitize_callback(). + * and the meta is registered with WP_Customize_Featured_Image_Controller::sanitize_value() as the sanitize_callback(). * So $attachment_id is either a valid attachment ID, -1, or false. */ if ( ! $is_valid ) { diff --git a/php/class-wp-customize-page-template-controller.php b/php/class-wp-customize-page-template-controller.php index 45ebb42..819408a 100644 --- a/php/class-wp-customize-page-template-controller.php +++ b/php/class-wp-customize-page-template-controller.php @@ -84,7 +84,15 @@ public function get_page_template_choices() { /** * Apply rudimentary sanitization of a file path for a generic setting instance. * + * The sanitization is rudimentary because `sanitize_meta()` fails to pass the + * associated post ID, so we cannot get the list of page templates to check + * against. Additionally, the callback used in `sanitize_meta()` cannot return + * `WP_Error` to indicate invalidity, so for these reasons we also have a + * `sanitize_setting` callback which is used when saving the customizer + * setting. + * * @see sanitize_meta() + * @see WP_Customize_Page_Template_Controller::sanitize_setting() * * @param string $raw_path Path. * @return string Path. diff --git a/php/class-wp-customize-postmeta-controller.php b/php/class-wp-customize-postmeta-controller.php index 6485140..e5ffb02 100644 --- a/php/class-wp-customize-postmeta-controller.php +++ b/php/class-wp-customize-postmeta-controller.php @@ -135,6 +135,7 @@ public function register_meta( WP_Customize_Posts $posts_component ) { $post_types = $this->post_types; } + // Note that if post_type_supports is defined, it support is missing, the post types will have already been excluded at this point. foreach ( $post_types as $post_type ) { $setting_args = array( 'sanitize_callback' => $this->sanitize_callback, @@ -205,8 +206,11 @@ public function sanitize_value( $meta_value ) { * Sanitize an input. * * Callback for `customize_sanitize_post_meta_{$meta_key}` filter. + * Note that this is redundant and unnecessary due to the `sanitize_value` + * method is used in the underlying `register_meta()` call. * * @see update_metadata() + * @see WP_Customize_Postmeta_Controller::sanitize_value() * * @param string $meta_value The value to sanitize. * @param WP_Customize_Postmeta_Setting $setting Setting. diff --git a/php/class-wp-customize-posts.php b/php/class-wp-customize-posts.php index df2612d..af05a4b 100644 --- a/php/class-wp-customize-posts.php +++ b/php/class-wp-customize-posts.php @@ -92,6 +92,7 @@ public function __construct( WP_Customize_Manager $manager ) { add_filter( 'customize_refresh_nonces', array( $this, 'add_customize_nonce' ) ); add_action( 'customize_register', array( $this, 'register_constructs' ), 20 ); + remove_filter( 'register_meta_args', '_wp_register_meta_args_whitelist' ); // Break warranty seal so additional args can be used in register_meta(). add_action( 'init', array( $this, 'register_meta' ), 100 ); add_filter( 'customize_dynamic_setting_args', array( $this, 'filter_customize_dynamic_setting_args' ), 10, 2 ); add_filter( 'customize_dynamic_setting_class', array( $this, 'filter_customize_dynamic_setting_class' ), 5, 3 ); @@ -191,9 +192,12 @@ public function set_builtin_post_type_descriptions() { /** * Register post meta for a given post type. * - * Please note that a sanitize_callback is intentionally excluded because the - * meta sanitization logic should be re-used with the global register_meta() - * function, which includes a `$sanitize_callback` param. + * Note that the `sanitize_callback` here is for the customizer setting and + * it is normally redundant to supply because `register_meta()` should have + * been already called with its own `sanitize_callback` supplied. A warning + * will be raised if this was not done. Similarly the `capability` parameter + * is not required here because when `register_meta()` was called, an + * `auth_callback` could (and should) be supplied at that point. * * @see register_meta() * @@ -202,23 +206,32 @@ public function set_builtin_post_type_descriptions() { * @param array $setting_args Args. */ public function register_post_type_meta( $post_type, $meta_key, $setting_args = array() ) { - $setting_args = array_merge( - array( - 'capability' => null, - 'theme_supports' => null, - 'default' => null, - 'transport' => null, - 'sanitize_callback' => null, - 'sanitize_js_callback' => null, - 'validate_callback' => null, - 'setting_class' => 'WP_Customize_Postmeta_Setting', - ), - $setting_args + $defaults = array( + 'theme_supports' => null, + 'post_type_supports' => null, + + // Setting args. + 'capability' => null, + 'default' => null, + 'transport' => null, + 'sanitize_callback' => null, + 'sanitize_js_callback' => null, + 'validate_callback' => null, + + 'setting_class' => 'WP_Customize_Postmeta_Setting', ); + $setting_args = array_merge( $defaults, $setting_args ); + if ( isset( $setting_args['auth_callback'] ) ) { + _doing_it_wrong( __METHOD__, esc_html__( 'Only pass auth_callback to register_meta() function. Consider the capability param instead.', 'customize-posts' ), '0.7.0' ); + } + $setting_args = wp_array_slice_assoc( $setting_args, array_keys( $defaults ) ); if ( ! has_filter( "auth_post_meta_{$meta_key}", array( $this, 'auth_post_meta_callback' ) ) ) { add_filter( "auth_post_meta_{$meta_key}", array( $this, 'auth_post_meta_callback' ), 10, 4 ); } + if ( ! has_filter( "sanitize_post_meta_{$meta_key}" ) ) { + _doing_it_wrong( __METHOD__, sprintf( __( 'Expected previous call to register_meta( "post", "%s" ) with a sanitize_callback.', 'customize-posts' ), $meta_key ), '0.7.0' ); // WPCS: xss ok. + } // Filter out null values, aka array_filter with ! is_null. foreach ( array_keys( $setting_args ) as $key => $value ) { @@ -234,7 +247,9 @@ public function register_post_type_meta( $post_type, $meta_key, $setting_args = } /** - * Allow editing post meta in Customizer if user can edit_post for registered post meta. + * Filter auth_post_meta_{$meta_key} according to the capability for the registered meta. + * + * Note that this filter will only apply when the customizer is bootstrapped. * * @param bool $allowed Whether the user can add the post meta. Default false. * @param string $meta_key The meta key. @@ -243,8 +258,7 @@ public function register_post_type_meta( $post_type, $meta_key, $setting_args = * @return bool Allowed. */ public function auth_post_meta_callback( $allowed, $meta_key, $post_id, $user_id ) { - global $wp_customize; - if ( $allowed || empty( $wp_customize ) ) { + if ( $allowed ) { return $allowed; } $post = get_post( $post_id ); @@ -274,6 +288,37 @@ public function auth_post_meta_callback( $allowed, $meta_key, $post_id, $user_id */ public function register_meta() { + // Recognize meta registered for customizer via register_meta(). + if ( function_exists( 'get_registered_meta_keys' ) ) { + foreach ( get_registered_meta_keys( 'post' ) as $meta => $args ) { + if ( empty( $args['show_in_customizer'] ) ) { + continue; + } + + if ( ! empty( $args['post_types'] ) && ! empty( $args['post_type_supports'] ) ) { + $post_types = array_intersect( $args['post_types'], get_post_types_by_support( $args['post_type_supports'] ) ); + } elseif ( ! empty( $args['post_type_supports'] ) ) { + $post_types = get_post_types_by_support( $args['post_type_supports'] ); + } elseif ( ! empty( $args['post_types'] ) ) { + $post_types = $args['post_types']; + } else { + $post_types = array(); + } + + foreach ( $post_types as $post_type ) { + $register_args = array(); + if ( isset( $args['customize_setting_args'] ) ) { + $register_args = array_merge( $register_args, $args['customize_setting_args'] ); + } + if ( isset( $args['customize_setting_class'] ) ) { + $register_args['setting_class'] = $args['customize_setting_class']; + } + $register_args = array_merge( $register_args, wp_array_slice_assoc( $args, array( 'theme_supports', 'post_type_supports' ) ) ); + $this->register_post_type_meta( $post_type, $meta, $register_args ); + } + } + } + /** * Allow plugins to register meta. * @@ -350,7 +395,9 @@ public function filter_customize_dynamic_setting_args( $args, $setting_id ) { } $registered = $this->registered_post_meta[ $matches['post_type'] ][ $matches['meta_key'] ]; if ( isset( $registered['theme_supports'] ) && ! current_theme_supports( $registered['theme_supports'] ) ) { - // We don't really need this because theme_supports will already filter it out of being exported. + return $args; + } + if ( isset( $registered['post_type_supports'] ) && ! post_type_supports( $matches['post_type'], $registered['post_type_supports'] ) ) { return $args; } if ( false === $args ) { diff --git a/tests/php/test-class-wp-customize-posts-preview.php b/tests/php/test-class-wp-customize-posts-preview.php index e5306b0..4689764 100644 --- a/tests/php/test-class-wp-customize-posts-preview.php +++ b/tests/php/test-class-wp-customize-posts-preview.php @@ -386,6 +386,16 @@ public function test_get_previewed_posts_for_query() { $this->assertEquals( array( $post->ID, $page->ID ), $this->posts_component->preview->get_previewed_posts_for_query( $query ) ); } + /** + * Pass through a value with out modification. + * + * @param mixed $x Value + * @return mixed Value. + */ + public function pass_through( $x ) { + return $x; + } + /** * Test querying posts based on meta queries. * @@ -395,6 +405,7 @@ public function test_get_previewed_posts_for_query() { public function test_get_previewed_post_for_meta_query() { $meta_key = 'index'; $post_type = 'post'; + register_meta( 'post', $meta_key, array( $this, 'pass_through' ) ); $this->posts_component->register_post_type_meta( $post_type, $meta_key ); $post_data = array(); @@ -578,12 +589,14 @@ public function test_filter_preview_pings_open() { public function test_register_post_type_meta_settings() { $post = get_post( $this->post_id ); + register_meta( 'post', 'foo', array( $this, 'pass_through' ) ); $this->posts_component->register_post_type_meta( 'post', 'foo' ); $foo_setting_id = WP_Customize_Postmeta_Setting::get_post_meta_setting_id( $post, 'foo' ); $this->assertEmpty( $this->posts_component->manager->get_setting( $foo_setting_id ) ); $this->posts_component->register_post_type_meta_settings( $post ); $this->assertNotEmpty( $this->posts_component->manager->get_setting( $foo_setting_id ) ); + register_meta( 'post', 'bar', array( $this, 'pass_through' ) ); $this->posts_component->register_post_type_meta( 'post', 'bar' ); $bar_setting_id = WP_Customize_Postmeta_Setting::get_post_meta_setting_id( $post, 'bar' ); $this->assertEmpty( $this->posts_component->manager->get_setting( $bar_setting_id ) ); @@ -599,6 +612,8 @@ public function test_register_post_type_meta_settings() { public function test_filter_get_post_meta_to_preview() { $preview = $this->posts_component->preview; $meta_key = 'foo_key'; + register_meta( 'post', $meta_key, array( $this, 'pass_through' ) ); + register_meta( 'post', 'other', array( $this, 'pass_through' ) ); $this->posts_component->register_post_type_meta( 'post', $meta_key ); $this->posts_component->register_post_type_meta( 'post', 'other' ); $this->posts_component->register_post_type_meta_settings( get_post( $this->post_id ) ); @@ -673,6 +688,7 @@ public function test_previewing_empty_array() { $meta_key = 'foo_ids'; $initial_value = array( 1, 2, 3 ); update_post_meta( $this->post_id, $meta_key, $initial_value ); + register_meta( 'post', $meta_key, array( $this, 'pass_through' ) ); $this->posts_component->register_post_type_meta( 'post', $meta_key ); $this->posts_component->register_post_type_meta_settings( get_post( $this->post_id ) ); @@ -856,6 +872,7 @@ public function test_export_preview_data() { $this->assertEquals( $this->post_id, $data['queriedPostId'] ); update_post_meta( $this->post_id, 'foo', 'bar' ); + register_meta( 'post', 'foo', array( $this, 'pass_through' ) ); $this->posts_component->register_post_type_meta( 'post', 'foo' ); $this->do_customize_boot_actions(); query_posts( array( 'p' => $this->post_id, 'preview' => true ) ); @@ -878,6 +895,7 @@ public function test_export_preview_data() { public function test_amend_with_queried_post_ids() { $preview = $this->posts_component->preview; $preview->customize_preview_init(); + register_meta( 'post', 'foo', array( $this, 'pass_through' ) ); $this->posts_component->register_post_type_meta( 'post', 'foo' ); query_posts( 'p=' . $this->post_id ); update_post_meta( $this->post_id, 'foo', 'bar' ); diff --git a/tests/php/test-class-wp-customize-posts.php b/tests/php/test-class-wp-customize-posts.php index e22d8f9..5b2bc40 100644 --- a/tests/php/test-class-wp-customize-posts.php +++ b/tests/php/test-class-wp-customize-posts.php @@ -65,6 +65,15 @@ public function setUp() { } } + /** + * Reset $wp_meta_keys. + */ + function clean_up_global_scope() { + global $wp_meta_keys; + parent::clean_up_global_scope(); + $wp_meta_keys = array(); + } + /** * Teardown. * @@ -189,12 +198,72 @@ public function test_set_builtin_post_type_descriptions() { /** * Test register_post_type_meta(). * - * @see WP_Customize_Posts::register_meta() + * @covers WP_Customize_Posts::register_meta() */ public function test_register_meta() { + + $foo_args = array( + 'sanitize_callback' => 'wp_kses_post', + 'auth_callback' => '__return_true', + 'show_in_customizer' => true, + 'post_type_supports' => 'foo', + 'theme_supports' => 'metavars', + 'post_types' => array( 'post' ), + 'customize_setting_args' => array( + 'capability' => 'edit_posts', + 'transport' => 'postMessage', + 'sanitize_callback' => 'foo_customize_sanitize_callback', + 'validate_callback' => 'foo_customize_validate_callback', + 'sanitize_js_callback' => 'foo_customize_sanitize_js_callback', + 'default' => 'FOO!', + ), + 'customize_setting_class' => 'Foo_Customize_Postmeta_Setting' + ); + + if ( function_exists( 'get_registered_meta_keys' ) ) { + add_post_type_support( 'post', 'foo' ); + add_post_type_support( 'post', 'bar' ); + add_post_type_support( 'post', 'baz' ); + + register_meta( 'post', 'foo', $foo_args ); + register_meta( 'post', 'bar', array( + 'sanitize_callback' => 'wp_kses_post', + 'show_in_customizer' => true, + 'post_type_supports' => 'bar', + ) ); + register_meta( 'post', 'baz', array( + 'sanitize_callback' => 'wp_kses_post', + 'show_in_customizer' => true, + 'post_types' => array( 'post' ), + ) ); + register_meta( 'post', 'qux', array( + 'sanitize_callback' => 'wp_kses_post', + 'show_in_customizer' => true, + ) ); + register_meta( 'post', 'xyzzy', array( + 'sanitize_callback' => 'wp_kses_post', + 'show_in_customizer' => false, + ) ); + } + $count = did_action( 'customize_posts_register_meta' ); do_action( 'init' ); $this->assertEquals( $count + 1, did_action( 'customize_posts_register_meta' ) ); + + if ( function_exists( 'get_registered_meta_keys' ) ) { + $this->assertArrayHasKey( 'foo', $this->posts->registered_post_meta['post'] ); + foreach ( array_keys( $foo_args['customize_setting_args'] ) as $key ) { + $this->assertEquals( $foo_args['customize_setting_args'][ $key ], $this->posts->registered_post_meta['post']['foo'][ $key ] ); + } + $this->assertEquals( $foo_args['customize_setting_class'], $this->posts->registered_post_meta['post']['foo']['setting_class'] ); + $this->assertEquals( $foo_args['theme_supports'], $this->posts->registered_post_meta['post']['foo']['theme_supports'] ); + $this->assertEquals( $foo_args['post_type_supports'], $this->posts->registered_post_meta['post']['foo']['post_type_supports'] ); + + $this->assertArrayHasKey( 'bar', $this->posts->registered_post_meta['post'] ); + $this->assertArrayHasKey( 'baz', $this->posts->registered_post_meta['post'] ); + $this->assertArrayNotHasKey( 'qux', $this->posts->registered_post_meta['post'] ); + $this->assertArrayNotHasKey( 'xyzzy', $this->posts->registered_post_meta['post'] ); + } } /** @@ -215,6 +284,7 @@ public function test_auth_post_meta_callback() { $this->assertFalse( $posts_component->auth_post_meta_callback( false, 'foo', $this->post_id, $this->user_id ) ); + register_meta( 'post', 'foo', array( $this, 'pass_through' ) ); $posts_component->register_post_type_meta( 'post', 'foo' ); $this->assertTrue( $posts_component->auth_post_meta_callback( false, 'foo', $this->post_id, $this->user_id ) ); } @@ -237,6 +307,7 @@ public function test_register_post_type_meta() { 'validate_callback' => array( $this, 'validate_setting' ), 'setting_class' => 'WP_Customize_Postmeta_Setting', ); + register_meta( 'post', 'timezone', array( $this, 'pass_through' ) ); $this->posts->register_post_type_meta( 'post', 'timezone', $args ); $setting_id = WP_Customize_Postmeta_Setting::get_post_meta_setting_id( get_post( $this->post_id ), 'timezone' ); @@ -691,6 +762,7 @@ public function test_get_settings() { $published_post_id = $this->factory()->post->create( array( 'post_status' => 'publish', 'post_name' => 'foo' ) ); $trashed_post_id = $this->factory()->post->create( array( 'post_status' => 'private', 'post_name' => 'bar' ) ); $draft_page_id = $this->factory()->post->create( array( 'post_status' => 'draft', 'post_name' => 'quux', 'post_type' => 'page' ) ); + register_meta( 'post', 'baz', array( $this, 'pass_through' ) ); $this->posts->register_post_type_meta( 'post', 'baz' ); wp_trash_post( $trashed_post_id ); @@ -806,4 +878,14 @@ public function test_get_select2_item_result() { $result = $this->posts->get_select2_item_result( $page ); $this->assertArrayNotHasKey( 'featured_image', $result ); } + + /** + * Pass through a value with out modification. + * + * @param mixed $x Value + * @return mixed Value. + */ + public function pass_through( $x ) { + return $x; + } }