diff --git a/.travis.yml b/.travis.yml index 5a82b06..91a3f7a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,7 +23,7 @@ php: env: - WP_VERSION=trunk WP_MULTISITE=0 - WP_VERSION=latest WP_MULTISITE=0 - - WP_VERSION=4.6.1 WP_MULTISITE=0 + - WP_VERSION=4.7.5 WP_MULTISITE=0 - WP_VERSION=latest WP_MULTISITE=1 install: diff --git a/js/customize-post-section.js b/js/customize-post-section.js index 3674b49..e82834c 100644 --- a/js/customize-post-section.js +++ b/js/customize-post-section.js @@ -282,6 +282,9 @@ section.addStatusControl(); section.addDateControl(); } + if ( api.Posts.data.themeSupports['post-formats'] && postTypeObj.supports['post-formats'] ) { + section.addPostFormatControl(); + } if ( postTypeObj.supports['page-attributes'] || postTypeObj.supports.parent ) { section.addParentControl(); } @@ -633,6 +636,69 @@ return control; }, + /** + * Add post format control. + * + * @todo Move this into a separate file/component like Page Template and Featured Image. + * + * @see post_format_meta_box() in PHP. + * + * @returns {wp.customize.Control} Added control. + */ + addPostFormatControl: function addPostFormatControl() { + var section = this, control, settingId, setting, controlId, postTypeObj, postFormats; + + postTypeObj = api.Posts.data.postTypes[ section.params.post_type ]; + if ( ! api.Posts.data.themeSupports['post-formats'] || ! postTypeObj.supports['post-formats'] ) { + return null; + } + + settingId = 'post_terms[' + section.params.post_type + '][' + String( section.params.post_id ) + '][post_format]'; + setting = api( settingId ); + if ( ! setting ) { + return null; + } + + // Add in the current one if it isn't there yet, in case the current theme doesn't support it + postFormats = _.uniq( + [ 'standard' ].concat( api.Posts.data.themeSupports['post-formats'] ).concat( [ setting.get() ] ) + ); + + controlId = section.id + '[post_format]'; + control = new api.controlConstructor.dynamic( controlId, { + params: { + section: section.id, + priority: 65, + label: postTypeObj.labels.post_format_field || api.Posts.data.l10n.fieldPostFormatLabel, + active: true, + settings: { + 'default': setting.id + }, + field_type: 'select', + input_attrs: { + 'data-customize-setting-link': settingId // @todo The need for this needs to be eliminated in core. + }, + choices: _.map( postFormats, function( postFormat ) { + return { + text: api.Posts.data.l10n.postFormatStrings[ postFormat ] || postFormat, + value: postFormat + }; + } ) + } + } ); + + // Override preview trying to de-activate control not present in preview context. + control.active.validate = function() { + return true; + }; + + // Register. + section.postFieldControls.post_format = control; + api.control.add( control.id, control ); + + return control; + }, + /** * Add parent control. * diff --git a/js/customize-posts.js b/js/customize-posts.js index 4ee5edd..cb66d97 100644 --- a/js/customize-posts.js +++ b/js/customize-posts.js @@ -73,30 +73,36 @@ component.parseSettingId = function parseSettingId( settingId ) { var parsed = {}, idParts; idParts = settingId.replace( /]/g, '' ).split( '[' ); - if ( 'post' !== idParts[0] && 'postmeta' !== idParts[0] ) { - return null; - } - parsed.settingType = idParts[0]; - if ( 'post' === parsed.settingType && 3 !== idParts.length || 'postmeta' === parsed.settingType && 4 !== idParts.length ) { - return null; - } + parsed.settingType = idParts.shift(); - parsed.postType = idParts[1]; + parsed.postType = idParts.shift(); if ( ! parsed.postType ) { return null; } - parsed.postId = parseInt( idParts[2], 10 ); - if ( isNaN( parsed.postId ) || parsed.postId <= 0 ) { + parsed.postId = parseInt( idParts.shift(), 10 ); + if ( ! parsed.postId || isNaN( parsed.postId ) || parsed.postId <= 0 ) { return null; } if ( 'postmeta' === parsed.settingType ) { - parsed.metaKey = idParts[3]; + parsed.metaKey = idParts.shift(); if ( ! parsed.metaKey ) { return null; } + } else if ( 'post_terms' === parsed.settingType ) { + parsed.taxonomy = idParts.shift(); + if ( ! parsed.taxonomy ) { + return null; + } + } else if ( 'post' !== parsed.settingType ) { + return null; } + + if ( 0 !== idParts.length ) { + return null; // Too many ID parts. + } + return parsed; }; diff --git a/js/edit-post-preview-admin.js b/js/edit-post-preview-admin.js index da0ebbe..89ddb25 100644 --- a/js/edit-post-preview-admin.js +++ b/js/edit-post-preview-admin.js @@ -38,7 +38,8 @@ var EditPostPreviewAdmin = (function( $ ) { wasMobile, parentId, menuOrder, - request; + request, + postFormat; event.preventDefault(); @@ -75,9 +76,17 @@ var EditPostPreviewAdmin = (function( $ ) { ping_status: $( '#ping_status' ).prop( 'checked' ) ? 'open' : 'closed', post_author: parseInt( $( '#post_author_override' ).val(), 10 ) }; - postSettingId = 'post[' + postType + '][' + postId + ']'; + postSettingId = 'post[' + postType + '][' + String( postId ) + ']'; settings[ postSettingId ] = postSettingValue; + postFormat = $( '#post-formats-select input[name=post_format]:checked' ).val(); + if ( '0' === postFormat ) { + postFormat = 'standard'; + } + if ( postFormat ) { + settings[ 'post_terms[' + postType + '][' + String( postId ) + '][post_format]' ] = postFormat; + } + // Allow plugins to inject additional settings to preview. wp.customize.trigger( 'settings-from-edit-post-screen', settings ); @@ -86,7 +95,7 @@ var EditPostPreviewAdmin = (function( $ ) { sessionStorage.setItem( 'previewedCustomizePostSettings[' + postId + ']', JSON.stringify( settings ) ); wp.customize.Loader.open( component.data.customize_url ); wp.customize.Loader.settings.browser.mobile = wasMobile; - component.bindChangesFromCustomizer( postSettingId, editor ); + component.bindChangesFromCustomizer( postId, postType, editor ); } else { $btn.addClass( 'disabled' ); component.previewButtonSpinner.addClass( 'is-active is-active-preview' ); @@ -94,7 +103,7 @@ var EditPostPreviewAdmin = (function( $ ) { customize_posts_update_changeset_nonce: component.data.customize_posts_update_changeset_nonce, previewed_post: component.data.previewed_post, customize_url: component.data.customize_url, - input_data: postSettingValue + settings: JSON.stringify( settings ) } ); request.fail( function( resp ) { @@ -108,7 +117,7 @@ var EditPostPreviewAdmin = (function( $ ) { request.done( function( resp ) { wp.customize.Loader.open( resp.customize_url ); wp.customize.Loader.settings.browser.mobile = wasMobile; - component.bindChangesFromCustomizer( postSettingId, editor ); + component.bindChangesFromCustomizer( postId, postType, editor ); } ); request.always( function() { @@ -121,13 +130,17 @@ var EditPostPreviewAdmin = (function( $ ) { /** * Sync changes from the Customizer to the post input fields. * - * @param {string} postSettingId post setting id. - * @param {object} editor Tinymce object. + * @param {int} postId - Post ID. + * @param {string} postType - Post type. + * @param {object} editor - Tinymce object. * @return {void} */ - component.bindChangesFromCustomizer = function( postSettingId, editor ) { + component.bindChangesFromCustomizer = function( postId, postType, editor ) { + var postSettingId, postFormatSettingId; + postSettingId = 'post[' + postType + '][' + String( postId ) + ']'; + postFormatSettingId = 'post_terms[' + postType + '][' + String( postId ) + '][post_format]'; wp.customize.Loader.messenger.bind( 'customize-post-settings-data', function( data ) { - var settingParentId; + var settingParentId, postFormatRadioSelector; if ( data[ postSettingId ] ) { $( '#title' ).val( data[ postSettingId ].post_title ).trigger( 'change' ); if ( editor ) { @@ -147,6 +160,14 @@ var EditPostPreviewAdmin = (function( $ ) { $( '#new-post-slug' ).val( data[ postSettingId ].post_name ); $( '#editable-post-name, #editable-post-name-full' ).text( data[ postSettingId ].post_name ); } + if ( data[ postFormatSettingId ] ) { + if ( 'standard' === data[ postFormatSettingId ] ) { + postFormatRadioSelector = '#post-format-0'; + } else { + postFormatRadioSelector = '#post-format-' + data[ postFormatSettingId ]; + } + $( postFormatRadioSelector ).prop( 'checked', true ).trigger( 'change' ); + } // Let plugins handle updates. wp.customize.trigger( 'settings-from-customizer', data ); diff --git a/js/edit-post-preview-customize.js b/js/edit-post-preview-customize.js index 5ae370f..71b11fb 100644 --- a/js/edit-post-preview-customize.js +++ b/js/edit-post-preview-customize.js @@ -101,13 +101,11 @@ var EditPostPreviewCustomize = (function( $, api ) { } ); // Start listening to changes to the post and postmeta. - api( 'post[' + component.data.previewed_post.post_type + '][' + component.data.previewed_post.ID + ']', function( setting ) { - setting.bind( function( data ) { - var settings = {}; - settings[ setting.id ] = data; - component.sendSettingsToEditPostScreen( settings ); - } ); - } ); + api.bind( 'change', function( setting ) { + var settings = {}; + settings[ setting.id ] = setting.get(); + component.sendSettingsToEditPostScreen( settings ); + }); component.parentFrame.send( 'customize-post-preview-ready' ); }; diff --git a/php/class-edit-post-preview.php b/php/class-edit-post-preview.php index 3cdf47d..93ae863 100644 --- a/php/class-edit-post-preview.php +++ b/php/class-edit-post-preview.php @@ -227,9 +227,14 @@ public function update_post_changeset() { } elseif ( empty( $_POST['customize_url'] ) ) { status_header( 400 ); wp_send_json_error( 'missing_customize_url' ); - } elseif ( empty( $_POST['input_data'] ) || ! is_array( $_POST['input_data'] ) ) { + } elseif ( empty( $_POST['settings'] ) ) { status_header( 400 ); - wp_send_json_error( 'missing_input_data' ); + wp_send_json_error( 'missing_settings' ); + } + $setting_values = json_decode( wp_unslash( $_POST['settings'] ), true ); + if ( ! is_array( $setting_values ) ) { + status_header( 400 ); + wp_send_json_error( 'invalid_settings' ); } $previewed_post_id = intval( wp_unslash( $_POST['previewed_post'] ) ); @@ -293,25 +298,28 @@ public function update_post_changeset() { */ $wp_customize_posts = $wp_customize->posts; - $settings = $wp_customize_posts->get_settings( array( $previewed_post_id ) ); - $setting = array_shift( $settings ); + if ( ! did_action( 'customize_register' ) ) { + remove_action( 'customize_register', array( $wp_customize, 'register_controls' ) ); + $wp_customize->register_controls(); // Due to race condition with themes that set customize_register priority to 10. + do_action( 'customize_register', $wp_customize ); + } + if ( ! did_action( 'customize_posts_register_meta' ) ) { + $wp_customize_posts->register_meta(); + } - if ( ! $setting ) { - status_header( 404 ); - wp_send_json_error( 'setting_not_found' ); - return; - } elseif ( ! $setting->check_capabilities() ) { - status_header( 403 ); - wp_send_json_error( 'changeset_already_published' ); - return; + $wp_customize->add_dynamic_settings( array_keys( $setting_values ) ); + + foreach ( $setting_values as $setting_id => $setting_value ) { + $setting = $wp_customize->get_setting( $setting_id ); + if ( $setting instanceof WP_Customize_Post_Setting ) { + $setting_value = wp_array_slice_assoc( + array_merge( $setting->value(), $setting_value ), + array_keys( $setting->default ) + ); + } + $wp_customize->set_post_value( $setting_id, $setting_value ); } - $setting->preview(); - // Note that save_changeset_post() will handle validation and sanitization. - $wp_customize->set_post_value( $setting->id, wp_array_slice_assoc( - array_merge( $setting->value(), wp_unslash( $_POST['input_data'] ) ), - array_keys( $setting->default ) - ) ); $response = $wp_customize->save_changeset_post(); if ( is_wp_error( $response ) ) { wp_send_json_error( $response->get_error_code() ); diff --git a/php/class-wp-customize-post-format-setting.php b/php/class-wp-customize-post-format-setting.php new file mode 100644 index 0000000..ee8a531 --- /dev/null +++ b/php/class-wp-customize-post-format-setting.php @@ -0,0 +1,81 @@ +taxonomy ) { + throw new Exception( 'Expected taxonomy to be post_format' ); + } + } + + /** + * Sanitize the setting's value for use in JavaScript. + * + * @return string Post Format. + */ + public function js_value() { + $term_ids = $this->value(); + $term_id = array_shift( $term_ids ); + if ( empty( $term_id ) ) { + return 'standard'; + } + $term = get_term( $term_id, $this->taxonomy ); + if ( ! ( $term instanceof WP_Term ) ) { + return 'standard'; + } + return str_replace( 'post-format-', '', $term->slug ); + } + + /** + * Sanitize (and validate) an input. + * + * @param string $format The value to sanitize. + * @return array|WP_Error|null Sanitized term IDs array or WP_Error if invalid (or null if not WP 4.6-alpha). + */ + public function sanitize( $format ) { + $has_setting_validation = method_exists( 'WP_Customize_Setting', 'validate' ); + + if ( ! in_array( $format, get_post_format_slugs() ) ) { + return $has_setting_validation ? new WP_Error( 'illegal_slug', __( 'Unrecognized post format slug.', 'customize-posts' ) ) : null; + } + + $value = array(); + if ( 'standard' !== $format ) { + $slug = 'post-format-' . $format; + $term = get_term_by( 'slug', $slug, $this->taxonomy ); + + // Make sure the post format term exists. + if ( $term instanceof WP_Term ) { + $value[] = $term->term_id; + } else { + $term = wp_insert_term( $slug, $this->taxonomy ); + if ( is_wp_error( $term ) ) { + return $has_setting_validation ? $term : null; + } + $value[] = $term['term_id']; + } + } + + $value = parent::sanitize( $value ); + + return $value; + } +} diff --git a/php/class-wp-customize-post-terms-setting.php b/php/class-wp-customize-post-terms-setting.php new file mode 100644 index 0000000..164d9c6 --- /dev/null +++ b/php/class-wp-customize-post-terms-setting.php @@ -0,0 +1,223 @@ +[^\]]+)\]\[(?P\d+)\]\[(?P.+)\]$/'; + + const TYPE = 'post_terms'; + + /** + * Type of setting. + * + * @var string + */ + public $type = self::TYPE; + + /** + * Post type. + * + * @var string + */ + public $post_type; + + /** + * Post ID. + * + * @var string + */ + public $post_id; + + /** + * Taxonomy name. + * + * @var string + */ + public $taxonomy; + + /** + * Posts component. + * + * @var WP_Customize_Posts + */ + public $posts_component; + + /** + * Default value, empty list of taxonomy terms. + * + * @var array + */ + public $default = array(); + + /** + * Capability. + * + * @var string + */ + public $capability = 'assign_terms'; + + /** + * WP_Customize_Post_Terms_Setting constructor. + * + * @param WP_Customize_Manager $manager Manager. + * @param string $id Setting ID. + * @param array $args Setting args. + * @throws Exception If the ID is in an invalid format. + */ + public function __construct( WP_Customize_Manager $manager, $id, $args = array() ) { + if ( ! preg_match( self::SETTING_ID_PATTERN, $id, $matches ) ) { + throw new Exception( 'Illegal setting id: ' . $id ); + } + $args['post_id'] = intval( $matches['post_id'] ); + $args['post_type'] = $matches['post_type']; + $args['taxonomy'] = $matches['taxonomy']; + $post_type_obj = get_post_type_object( $args['post_type'] ); + if ( ! $post_type_obj ) { + throw new Exception( 'Unrecognized post type: ' . $args['post_type'] ); + } + $taxonomy_obj = get_taxonomy( $args['taxonomy'] ); + if ( ! $taxonomy_obj ) { + throw new Exception( 'Unrecognized taxonomy: ' . $args['taxonomy'] ); + } + if ( empty( $manager->posts ) || ! ( $manager->posts instanceof WP_Customize_Posts ) ) { + throw new Exception( 'Posts component not instantiated.' ); + } + $this->posts_component = $manager->posts; + + if ( empty( $args['capability'] ) && ! empty( $taxonomy_obj->cap->assign_terms ) ) { + $args['capability'] = $taxonomy_obj->cap->assign_terms; + } + parent::__construct( $manager, $id, $args ); + } + + /** + * Get setting ID for a given post taxonomy terms setting. + * + * @param WP_Post $post Post. + * @param string $taxonomy Taxonomy name. + * @return string Setting ID. + */ + static function get_post_terms_setting_id( WP_Post $post, $taxonomy ) { + return sprintf( 'post_terms[%s][%d][%s]', $post->post_type, $post->ID, $taxonomy ); + } + + /** + * Return setting value. + * + * @return array Term IDs. + */ + public function value() { + $terms = get_the_terms( $this->post_id, $this->taxonomy ); + if ( ! is_array( $terms ) ) { + return array(); + } + return wp_list_pluck( $terms, 'term_id' ); + } + + /** + * Sanitize (and validate) an input. + * + * @param array $term_ids The term IDs to sanitize and validate. + * @return array|WP_Error|null Sanitized term IDs array or WP_Error if invalid (or null if not WP 4.6-alpha). + */ + public function sanitize( $term_ids ) { + $has_setting_validation = method_exists( 'WP_Customize_Setting', 'validate' ); + + if ( ! is_array( $term_ids ) ) { + return $has_setting_validation ? new WP_Error( 'expected_array', __( 'Expected array value for post terms setting.', 'customize-posts' ) ) : null; + } + + /** This filter is documented in wp-includes/class-wp-customize-setting.php */ + $term_ids = apply_filters( "customize_sanitize_{$this->id}", $term_ids, $this ); + + if ( is_wp_error( $term_ids ) ) { + return $has_setting_validation ? $term_ids : null; + } + + foreach ( $term_ids as $term_id ) { + if ( ! is_numeric( $term_id ) || $term_id <= 0 ) { + return $has_setting_validation ? new WP_Error( 'invalid_term_id', __( 'Invalid ID supplied for post terms.', 'customize-posts' ) ) : null; + } + $term = get_term( $term_id, $this->taxonomy ); + if ( is_wp_error( $term ) ) { + return $has_setting_validation ? $term : null; + } + if ( ! ( $term instanceof WP_Term ) ) { + return $has_setting_validation ? new WP_Error( 'missing_term', __( 'Missing term', 'customize-posts' ) ) : null; + } + if ( $term->taxonomy !== $this->taxonomy ) { + return $has_setting_validation ? new WP_Error( 'term_taxonomy_mismatch', __( 'Supplied term is not for the expected taxonomy', 'customize-posts' ) ) : null; + } + } + + $term_ids = array_map( 'intval', $term_ids ); + return $term_ids; + } + + /** + * Flag this setting as one to be previewed. + * + * Note that the previewing logic is handled by WP_Customize_Posts_Preview. + * + * @see get_the_terms() + * @see wp_get_post_terms() + * @see wp_get_object_terms() + * @see WP_Customize_Posts_Preview::filter_get_the_terms_to_preview() + * + * @return bool + */ + public function preview() { + if ( $this->is_previewed ) { + return true; + } + $this->is_previewed = true; + + if ( array_key_exists( $this->id, $this->manager->unsanitized_post_values() ) ) { + $this->prime_term_relationships_cache(); + } else { + add_action( "customize_post_value_set_{$this->id}", array( $this, 'prime_term_relationships_cache' ) ); + } + + if ( ! isset( $this->posts_component->preview->previewed_post_terms_settings[ $this->post_id ] ) ) { + $this->posts_component->preview->previewed_post_terms_settings[ $this->post_id ] = array(); + } + $this->posts_component->preview->previewed_post_terms_settings[ $this->post_id ][ $this->taxonomy ] = $this; + $this->posts_component->preview->add_preview_filters(); + return true; + } + + /** + * Prime the cache to prevent customized state from being cached. + * + * @see get_the_terms() + * @return void + */ + public function prime_term_relationships_cache() { + $this->posts_component->preview->post_terms_preview_suspended = true; + get_the_terms( $this->post_id, $this->taxonomy ); + $this->posts_component->preview->post_terms_preview_suspended = false; + } + + /** + * Update the post terms. + * + * Please note that the capability check will have already been done. + * + * @see WP_Customize_Setting::save() + * + * @param string $term_ids The value to update. + * @return bool The result of saving the value. + */ + protected function update( $term_ids ) { + $r = wp_set_post_terms( $this->post_id, $term_ids, $this->taxonomy, false ); + return ! is_wp_error( $r ); + } +} diff --git a/php/class-wp-customize-postmeta-setting.php b/php/class-wp-customize-postmeta-setting.php index b19c8f4..6ab673e 100644 --- a/php/class-wp-customize-postmeta-setting.php +++ b/php/class-wp-customize-postmeta-setting.php @@ -67,7 +67,7 @@ class WP_Customize_Postmeta_Setting extends WP_Customize_Setting { public $posts_component; /** - * WP_Customize_Post_Setting constructor. + * WP_Customize_Postmeta_Setting constructor. * * @access public * diff --git a/php/class-wp-customize-posts-preview.php b/php/class-wp-customize-posts-preview.php index 6a1bdbe..8228fa2 100644 --- a/php/class-wp-customize-posts-preview.php +++ b/php/class-wp-customize-posts-preview.php @@ -42,6 +42,13 @@ final class WP_Customize_Posts_Preview { */ public $previewed_postmeta_settings = array(); + /** + * Previewed post terms settings by post ID and taxonomy key. + * + * @var WP_Customize_Post_Terms_Setting[] + */ + public $previewed_post_terms_settings = array(); + /** * List of the orderby keys used in queries in the response. * @@ -118,6 +125,8 @@ public function add_preview_filters() { add_action( 'the_post', array( $this, 'preview_setup_postdata' ) ); add_filter( 'the_title', array( $this, 'filter_the_title' ), 1, 2 ); add_filter( 'get_post_metadata', array( $this, 'filter_get_post_meta_to_preview' ), 1000, 4 ); + add_filter( 'get_the_terms', array( $this, 'filter_get_the_terms_to_preview' ), 1, 3 ); + add_filter( 'get_object_terms', array( $this, 'filter_get_object_terms_to_preview' ), 1, 4 ); add_filter( 'wp_setup_nav_menu_item', array( $this, 'filter_nav_menu_item_to_set_post_dependent_props' ), 100 ); add_filter( 'comments_open', array( $this, 'filter_preview_comments_open' ), 10, 2 ); add_filter( 'pings_open', array( $this, 'filter_preview_pings_open' ), 10, 2 ); @@ -1184,6 +1193,251 @@ public function filter_get_post_meta_to_preview( $value, $object_id, $meta_key, } } + /** + * Filter post terms to inject customized value. + * + * @see WP_Customize_Post_Terms_Setting::preview() + * @see get_the_terms() + * + * @param array|WP_Error $terms List of attached terms, or WP_Error on failure. + * @param int $post_id Post ID. + * @param string $taxonomy Name of the taxonomy. + * @return WP_Term[] Terms. + */ + public function filter_get_the_terms_to_preview( $terms, $post_id, $taxonomy ) { + + // Short-circuit if no setting has been previewed. + $previewed_settings = $this->get_previewed_post_terms_settings( array( $post_id ), array( $taxonomy ) ); + $previewed_setting = array_shift( $previewed_settings ); + if ( ! $previewed_setting ) { + return $terms; + } + + $post_value = $previewed_setting->post_value( null ); + if ( ! is_array( $post_value ) ) { + return $terms; + } + + $customized_terms = array(); + foreach ( $post_value as $term_id ) { + $term = get_term( $term_id ); + if ( $term instanceof WP_Term ) { + $customized_terms[] = $term; + } + } + return $customized_terms; + } + + /** + * Get post terms settings that are currently previewed. + * + * @param array $object_ids Object IDs. + * @param array $taxonomies Taxonomies. + * @return WP_Customize_Post_Terms_Setting[] Settings. + */ + public function get_previewed_post_terms_settings( $object_ids, $taxonomies ) { + $previewed_settings = array(); + foreach ( $object_ids as $object_id ) { + foreach ( $taxonomies as $taxonomy ) { + if ( isset( $this->previewed_post_terms_settings[ $object_id ][ $taxonomy ] ) ) { + $setting = $this->previewed_post_terms_settings[ $object_id ][ $taxonomy ]; + $previewed_settings[ $setting->id ] = $setting; + } + } + } + + // Remove previewed settings which don't have any customized values anyway. + $previewed_settings = wp_array_slice_assoc( + $previewed_settings, + array_keys( $this->component->manager->unsanitized_post_values() ) + ); + + return $previewed_settings; + } + + /** + * Whether post terms previewing is suspended. + * + * @var bool + */ + public $post_terms_preview_suspended = false; + + /** + * Filter post terms to inject customized value. + * + * @see WP_Customize_Post_Terms_Setting::preview() + * @see wp_get_post_terms() + * @see wp_get_object_terms() + * + * @param array $terms An array of terms for the given object or objects. + * @param array $object_ids Array of object IDs for which `$terms` were retrieved. + * @param array $taxonomies Array of taxonomies from which `$terms` were retrieved. + * @param array $args An array of arguments for retrieving terms for the given + * object(s). See wp_get_object_terms() for details. + * @return array|int Terms. + */ + public function filter_get_object_terms_to_preview( $terms, $object_ids, $taxonomies, $args ) { + if ( $this->post_terms_preview_suspended ) { + return $terms; + } + + if ( empty( $object_ids ) ) { + trigger_error( 'Customize Posts: filter_get_object_terms_to_preview requires one or more object_ids to operate.', E_USER_NOTICE ); + return $terms; + } + + $previewed_settings = $this->get_previewed_post_terms_settings( $object_ids, $taxonomies ); + + // Short-circuit if none of the settings have been previewed. + if ( empty( $previewed_settings ) ) { + return $terms; + } + + $args = array_merge( + array( + 'fields' => 'all', + 'order' => 'ASC', + 'orderby' => 'name', + ), + $args + ); + + // Unsupported to get counts for each term. Will require lower-level WP_Term_Query integration. + if ( ! in_array( $args['fields'], array( 'all', 'ids', 'all_with_object_id' ), true ) ) { + trigger_error( sprintf( 'Customize Posts: post_terms setting unsupported fields arg "%s".', $args['fields'] ), E_USER_NOTICE ); + return $terms; + } + + // Unable to orderby count since previewing post/term assignments naturally changes the counts. + if ( 'count' === $args['orderby'] ) { + trigger_error( sprintf( 'Customize Posts: post_terms setting unsupported orderby arg "%s".', $args['orderby'] ), E_USER_NOTICE ); + return $terms; + } + + // Make sure 0 is set to global $post. + $object_ids = wp_list_pluck( array_map( 'get_post', $object_ids ), 'ID' ); + + /* + * Args that can possibly be added via the `wp_get_object_terms_args` filter which aren't supported yet, + * or which may not be supportable without lower-level integration with WP_Term_Query. + */ + $unsupported_args = array( + 'count', + 'hide_empty', + 'include', + 'exclude', + 'exclude_tree', + 'number', + 'offset', + 'name', + 'slug', + 'term_taxonomy_id', + 'hierarchical', + 'search', + 'name__like', + 'description__like', + 'pad_counts', + 'get', + 'child_of', + 'parent', + 'childless', + 'meta_query', + 'meta_key', + 'meta_value', + 'meta_type', + 'meta_compare', + ); + $unsupported_count = 0; + foreach ( $unsupported_args as $arg_name ) { + if ( array_key_exists( $arg_name, $args ) ) { + trigger_error( sprintf( 'Customize Posts: post_terms setting unsupported arg: %s', $arg_name ), E_USER_NOTICE ); + $unsupported_count += 1; + } + } + if ( $unsupported_count > 0 ) { + return $terms; + } + + // Link each term with its associated object. + $object_linked_terms = array(); + foreach ( $terms as $term ) { + if ( ! ( $term instanceof WP_Term) ) { + $term = get_term( $term ); + } + if ( ! ( $term instanceof WP_Term) ) { + continue; + } + if ( ! empty( $term->object_id ) ) { + $object_linked_terms[] = $term; + } else { + foreach ( $object_ids as $object_id ) { + $this->post_terms_preview_suspended = true; // Since has_term() can end up calling wp_get_object_terms(). + if ( has_term( $term->term_id, $term->taxonomy, $object_id ) ) { + $object_term = clone $term; + $object_term->object_id = $object_id; + $object_linked_terms[] = $object_term; + } + $this->post_terms_preview_suspended = false; + } + } + } + + // Group terms by taxonomy and post to facilitate overriding. + $grouped_terms = array(); + foreach ( $object_linked_terms as $term ) { + $post = get_post( $term->object_id ); + if ( ! $post ) { + continue; + } + $setting_id = WP_Customize_Post_Terms_Setting::get_post_terms_setting_id( $post, $term->taxonomy ); + if ( ! isset( $grouped_terms[ $setting_id ] ) ) { + $grouped_terms[ $setting_id ] = array(); + } + $grouped_terms[ $setting_id ][] = $term; + } + + // Obtain previewed terms for each group. + foreach ( $previewed_settings as $previewed_setting ) { + $post_value = $previewed_setting->post_value( null ); + if ( is_array( $post_value ) ) { + $customized_terms = array(); + foreach ( $post_value as $term_id ) { + $term = get_term( $term_id ); + if ( $term instanceof WP_Term ) { + if ( 'all_with_object_id' === $args['fields'] ) { + $term->object_id = $previewed_setting->post_id; + } + + $customized_terms[] = $term; + } + } + $grouped_terms[ $previewed_setting->id ] = $customized_terms; + } + } + + $terms = call_user_func_array( 'array_merge', $grouped_terms ); + + $terms = wp_list_sort( $terms, $args['orderby'], $args['order'] ); + + $returned_terms = array(); + if ( 'ids' === $args['fields'] ) { + $returned_terms = wp_list_pluck( $terms, 'term_id' ); + } elseif ( 'tt_ids' === $args['fields'] ) { + $returned_terms = wp_list_pluck( $terms, 'term_taxonomy_id' ); + } elseif ( 'id=>' === substr( $args['fields'], 0, 4 ) ) { + $field = strpos( $args['fields'], 4 ); + foreach ( $terms as $term ) { + $returned_terms[ $term->term_id ] = $term->$field; + } + } elseif ( 'names' === $args['fields'] ) { + $returned_terms = wp_list_pluck( $terms, 'name' ); + } else { + $returned_terms = $terms; + } + + return $returned_terms; + } + /** * Recognize partials for posts appearing in preview. * diff --git a/php/class-wp-customize-posts.php b/php/class-wp-customize-posts.php index dbea11d..99413aa 100644 --- a/php/class-wp-customize-posts.php +++ b/php/class-wp-customize-posts.php @@ -80,6 +80,8 @@ public function __construct( WP_Customize_Manager $manager ) { require_once dirname( __FILE__ ) . '/class-wp-customize-post-discussion-fields-control.php'; require_once dirname( __FILE__ ) . '/class-wp-customize-post-setting.php'; require_once dirname( __FILE__ ) . '/class-wp-customize-postmeta-setting.php'; + require_once dirname( __FILE__ ) . '/class-wp-customize-post-terms-setting.php'; + require_once dirname( __FILE__ ) . '/class-wp-customize-post-format-setting.php'; require_once dirname( __FILE__ ) . '/class-wp-customize-post-date-control.php'; require_once dirname( __FILE__ ) . '/class-wp-customize-post-status-control.php'; require_once dirname( __FILE__ ) . '/class-wp-customize-post-editor-control.php'; @@ -93,6 +95,8 @@ public function __construct( WP_Customize_Manager $manager ) { add_action( 'customize_register', array( $this, 'ensure_static_front_page_constructs_registered' ), 11 ); add_action( 'customize_register', array( $this, 'register_constructs' ), 20 ); add_filter( 'map_meta_cap', array( $this, 'filter_map_meta_cap' ), 10, 4 ); + $this->configure_builtins(); + add_action( 'init', array( $this, 'configure_builtins' ), 1 ); // Because built-ins get registered twice. 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 ); @@ -212,10 +216,12 @@ public function get_post_types() { } /** + * Configure built-in core post types and taxonomies. + * * Set missing post type descriptions for built-in post types and explicitly disallow attachments in customizer UI. */ - public function configure_builtin_post_types() { - global $wp_post_types; + public function configure_builtins() { + global $wp_post_types, $wp_taxonomies; if ( post_type_exists( 'post' ) && empty( $wp_post_types['post']->description ) ) { $wp_post_types['post']->description = __( 'Posts are entries listed in reverse chronological order, usually on the site homepage or on a dedicated posts page. Posts can be organized by tags or categories.', 'customize-posts' ); } @@ -225,6 +231,13 @@ public function configure_builtin_post_types() { if ( post_type_exists( 'attachment' ) && ! isset( $wp_post_types['attachment']->show_in_customizer ) ) { $wp_post_types['attachment']->show_in_customizer = false; } + + if ( taxonomy_exists( 'post_format' ) ) { + if ( ! isset( $wp_taxonomies['post_format']->show_in_customizer ) ) { + $wp_taxonomies['post_format']->show_in_customizer = true; + } + $wp_taxonomies['post_format']->customize_setting_class = 'WP_Customize_Post_Format_Setting'; + } } /** @@ -251,7 +264,7 @@ public function register_post_type_meta( $post_type, $meta_key, $setting_args = 'sanitize_callback' => null, 'sanitize_js_callback' => null, 'validate_callback' => null, - 'setting_class' => 'WP_Customize_Postmeta_Setting', + 'setting_class' => 'WP_Customize_Postmeta_Setting', // @todo Rename to customize_setting_class? ), $setting_args ); @@ -438,7 +451,6 @@ public function register_constructs() { $panel_priority = 900; // Before widgets. // Note that this does not include nav_menu_item. - $this->configure_builtin_post_types(); foreach ( $this->get_post_types() as $post_type_object ) { if ( empty( $post_type_object->show_in_customizer ) ) { continue; @@ -525,6 +537,28 @@ public function filter_customize_dynamic_setting_args( $args, $setting_id ) { $registered ); $args['type'] = 'postmeta'; + } elseif ( preg_match( WP_Customize_Post_Terms_Setting::SETTING_ID_PATTERN, $setting_id, $matches ) ) { + if ( ! post_type_exists( $matches['post_type'] ) || ! taxonomy_exists( $matches['taxonomy'] ) ) { + return $args; + } + $taxonomy = get_taxonomy( $matches['taxonomy'] ); + if ( empty( $taxonomy->show_in_customizer ) ) { + return $args; + } + if ( 'post_format' === $taxonomy->name && ! post_type_supports( $matches['post_type'], 'post-formats' ) ) { // @todo Remove from being hard-coded? + return $args; + } + if ( false === $args ) { + $args = array(); + } + if ( isset( $taxonomy->customize_setting_type ) ) { + $args['type'] = $taxonomy->customize_setting_type; + } else { + $args['type'] = 'post_terms'; + } + if ( isset( $taxonomy->customize_setting_class ) ) { + $args['setting_class'] = $taxonomy->customize_setting_class; + } } return $args; @@ -541,15 +575,15 @@ public function filter_customize_dynamic_setting_args( $args, $setting_id ) { */ public function filter_customize_dynamic_setting_class( $class, $setting_id, $args ) { unset( $setting_id ); - if ( isset( $args['type'] ) ) { + if ( isset( $args['setting_class'] ) ) { + $class = $args['setting_class']; + } elseif ( isset( $args['type'] ) ) { if ( 'post' === $args['type'] ) { $class = 'WP_Customize_Post_Setting'; } elseif ( 'postmeta' === $args['type'] ) { - if ( isset( $args['setting_class'] ) ) { - $class = $args['setting_class']; - } else { - $class = 'WP_Customize_Postmeta_Setting'; - } + $class = 'WP_Customize_Postmeta_Setting'; + } elseif ( 'post_terms' === $args['type'] ) { + $class = 'WP_Customize_Post_Terms_Setting'; } } return $class; @@ -601,6 +635,24 @@ public function register_post_type_meta_settings( $post ) { return $setting_ids; } + /** + * Add all post terms settings for all taxonomies that support showing in Customizer. + * + * @param WP_Post $post Post. + * @return array + */ + public function register_post_terms_settings( $post ) { + $setting_ids = array(); + foreach ( get_taxonomies( array(), 'objects' ) as $taxonomy ) { + if ( empty( $taxonomy->show_in_customizer ) ) { + continue; + } + $setting_ids[] = WP_Customize_Post_Terms_Setting::get_post_terms_setting_id( $post, $taxonomy->name ); + } + $this->manager->add_dynamic_settings( $setting_ids ); + return $setting_ids; + } + /** * Get the post status choices array. * @@ -757,7 +809,7 @@ public function filter_customize_save_response_to_export_saved_values( $response $response['saved_post_setting_values'] = array(); foreach ( array_keys( $this->manager->unsanitized_post_values() ) as $setting_id ) { $setting = $this->manager->get_setting( $setting_id ); - if ( ( $setting instanceof WP_Customize_Post_Setting || $setting instanceof WP_Customize_Postmeta_Setting ) && get_post( $setting->post_id ) ) { + if ( ( $setting instanceof WP_Customize_Post_Setting || $setting instanceof WP_Customize_Postmeta_Setting || $setting instanceof WP_Customize_Post_Terms_Setting ) && get_post( $setting->post_id ) ) { $response['saved_post_setting_values'][ $setting->id ] = $setting->js_value(); } } @@ -806,7 +858,16 @@ public function enqueue_scripts() { ); } + $theme_supports = array(); + if ( current_theme_supports( 'post-formats' ) ) { + $theme_support = get_theme_support( 'post-formats' ); + if ( isset( $theme_support[0] ) && is_array( $theme_support[0] ) ) { + $theme_supports['post-formats'] = $theme_support[0]; + } + } + $exports = array( + 'themeSupports' => $theme_supports, 'postTypes' => $post_types, 'postStatusChoices' => $this->get_post_status_choices(), 'authorChoices' => $this->get_author_choices(), // @todo Use Ajax to fetch this data or Customize Object Selector (once it supports users). @@ -824,9 +885,11 @@ public function enqueue_scripts() { 'fieldExcerptLabel' => __( 'Excerpt', 'customize-posts' ), 'fieldDiscussionLabel' => __( 'Discussion', 'customize-posts' ), 'fieldAuthorLabel' => __( 'Author', 'customize-posts' ), + 'fieldPostFormatLabel' => __( 'Post Format', 'customize-posts' ), 'fieldParentLabel' => __( 'Parent', 'customize-posts' ), 'fieldOrderLabel' => __( 'Order', 'customize-posts' ), 'noTitle' => __( '(no title)', 'customize-posts' ), + /* translators: placeholder is conflicted value */ 'theirChange' => __( 'Their change: %s', 'customize-posts' ), 'openEditor' => __( 'Open Editor', 'customize-posts' ), // @todo Move this into editor control? 'closeEditor' => __( 'Close Editor', 'customize-posts' ), @@ -839,6 +902,7 @@ public function enqueue_scripts() { 'editPostFailure' => __( 'Failed to open for editing.', 'customize-posts' ), 'createPostFailure' => __( 'Failed to create for editing.', 'customize-posts' ), 'installCustomizeObjectSelector' => sprintf( + /* translators: placeholder is link to Customize Object Selector plugin */ __( 'This control depends on having the %s plugin installed and activated.', 'customize-posts' ), sprintf( '%s', @@ -850,6 +914,7 @@ public function enqueue_scripts() { /* translators: %s post type */ 'jumpToPostPlaceholder' => __( 'Jump to %s', 'customize-posts' ), + 'postFormatStrings' => get_post_format_strings(), ), ); @@ -1315,10 +1380,10 @@ public function force_empty_post_dates( $data ) { } /** - * Get post/postmeta settings for the given post IDs. + * Get post/postmeta/post_terms settings for the given post IDs. * * @param int[] $post_ids Post IDs. - * @return WP_Customize_Post_Setting[]|WP_Customize_Postmeta_Setting[] Settings. + * @return WP_Customize_Post_Setting[]|WP_Customize_Postmeta_Setting[]|WP_Customize_Post_Terms_Setting[]|WP_Customize_Nav_Menu_Item_Setting[] Settings. */ public function get_settings( array $post_ids ) { $query = new WP_Query( array( @@ -1339,6 +1404,7 @@ public function get_settings( array $post_ids ) { $this->manager->add_dynamic_settings( $post_setting_ids ); foreach ( $query->posts as $post ) { $this->register_post_type_meta_settings( $post ); + $this->register_post_terms_settings( $post ); } $settings = array(); foreach ( $this->manager->settings() as $setting ) { @@ -1347,6 +1413,8 @@ public function get_settings( array $post_ids ) { || $setting instanceof WP_Customize_Postmeta_Setting || + $setting instanceof WP_Customize_Post_Terms_Setting + || $setting instanceof WP_Customize_Nav_Menu_Item_Setting ); if ( $is_requested_setting_type && in_array( $setting->post_id, $post_ids, true ) ) { @@ -1471,7 +1539,7 @@ public function ajax_insert_auto_draft_post() { } /** - * Handle ajax request for lazy-loaded post/postmeta settings. + * Handle ajax request for lazy-loaded post/postmeta/post_terms settings. * * @action wp_ajax_customize-posts-fetch-settings * @access public diff --git a/readme.md b/readme.md index c347c20..34c56ad 100644 --- a/readme.md +++ b/readme.md @@ -6,7 +6,7 @@ Edit posts and postmeta in the Customizer. Stop editing your posts/postmeta blin **Contributors:** [xwp](https://profiles.wordpress.org/xwp), [westonruter](https://profiles.wordpress.org/westonruter), [valendesigns](https://profiles.wordpress.org/valendesigns), [sayedwp](https://profiles.wordpress.org/sayedwp), [utkarshpatel.](https://profiles.wordpress.org/utkarshpatel.) **Tags:** [customizer](https://wordpress.org/plugins/tags/customizer), [customize](https://wordpress.org/plugins/tags/customize), [posts](https://wordpress.org/plugins/tags/posts), [postmeta](https://wordpress.org/plugins/tags/postmeta), [editor](https://wordpress.org/plugins/tags/editor), [preview](https://wordpress.org/plugins/tags/preview), [featured-image](https://wordpress.org/plugins/tags/featured-image), [page-template](https://wordpress.org/plugins/tags/page-template) -**Requires at least:** 4.5.0 +**Requires at least:** 4.7 **Tested up to:** 4.8.1 **Stable tag:** 0.8.7 **License:** [GPLv2 or later](http://www.gnu.org/licenses/gpl-2.0.html) diff --git a/readme.txt b/readme.txt index 0718883..9ecd21d 100644 --- a/readme.txt +++ b/readme.txt @@ -1,7 +1,7 @@ === Customize Posts === Contributors: xwp, westonruter, valendesigns, sayedwp, utkarshpatel. Tags: customizer, customize, posts, postmeta, editor, preview, featured-image, page-template -Requires at least: 4.5.0 +Requires at least: 4.7 Tested up to: 4.8.1 Stable tag: 0.8.7 License: GPLv2 or later diff --git a/tests/php/test-ajax-class-wp-customize-posts.php b/tests/php/test-ajax-class-wp-customize-posts.php index 46bef7a..5662b86 100644 --- a/tests/php/test-ajax-class-wp-customize-posts.php +++ b/tests/php/test-ajax-class-wp-customize-posts.php @@ -486,12 +486,15 @@ public function test_update_post_changeset_successes() { 'ping_status' => 'closed', 'post_author' => $user_id, ); + $settings = array(); + $setting_id = "post[post][$post_id]"; + $settings[ $setting_id ] = $input_data; $_POST = wp_slash( array( 'customize_posts_update_changeset_nonce' => wp_create_nonce( 'customize_posts_update_changeset' ), 'previewed_post' => $post_id, 'customize_url' => wp_customize_url(), - 'input_data' => $input_data, + 'settings' => wp_json_encode( $settings ), ) ); $this->make_ajax_call( 'customize_posts_update_changeset' ); $response = json_decode( $this->_last_response, true ); @@ -501,8 +504,7 @@ public function test_update_post_changeset_successes() { $this->assertArrayHasKey( 'response', $response['data'] ); $this->assertArrayHasKey( 'setting_validities', $response['data']['response'] ); - $setting_key = "post[post][$post_id]"; - $this->assertTrue( $response['data']['response']['setting_validities'][ $setting_key ] ); + $this->assertTrue( $response['data']['response']['setting_validities'][ $setting_id ] ); $customize_url_parts = parse_url( $response['data']['customize_url'] ); parse_str( $customize_url_parts['query'], $url_query ); @@ -524,7 +526,7 @@ public function test_update_post_changeset_successes() { $settings_data = json_decode( $post->post_content, true ); foreach( $settings_data as $key => $data ) { - $this->assertEquals( $setting_key, $key ); + $this->assertEquals( $setting_id, $key ); $this->assertArraySubset( $input_data, $data['value'] ); } } diff --git a/tests/php/test-class-wp-customize-post-terms-setting.php b/tests/php/test-class-wp-customize-post-terms-setting.php new file mode 100644 index 0000000..ba19d55 --- /dev/null +++ b/tests/php/test-class-wp-customize-post-terms-setting.php @@ -0,0 +1,372 @@ +post->create_many( 3, array( 'post_type' => 'post' ) ); + + foreach ( array( 'a', 'b', 'c' ) as $slug ) { + self::$post_tag_term_ids[ $slug ] = self::factory()->term->create( array( + 'taxonomy' => 'post_tag', + 'name' => $slug, + ) ); + }; + } + + /** + * Set up. + */ + function setUp() { + parent::setUp(); + remove_all_filters( 'customize_loaded_components' ); + wp_set_current_user( $this->factory()->user->create( array( 'role' => 'administrator' ) ) ); + require_once( ABSPATH . WPINC . '/class-wp-customize-manager.php' ); + $this->plugin = new Customize_Posts_Plugin(); + // @codingStandardsIgnoreStart + $GLOBALS['wp_customize'] = new WP_Customize_Manager(); + // @codingStandardsIgnoreStop + $this->manager = $GLOBALS['wp_customize']; + + remove_action( 'after_setup_theme', 'twentyfifteen_setup' ); + remove_action( 'after_setup_theme', 'twentysixteen_setup' ); + } + + /** + * Tear down. + */ + function tearDown() { + unset( $GLOBALS['wp_customize'] ); + parent::tearDown(); + } + + /** + * Test get_post_terms_setting_id() method. + * + * @covers WP_Customize_Post_Terms_Setting::get_post_terms_setting_id() + */ + function test_get_post_terms_setting_id() { + $post = get_post( $this->factory()->post->create( array( 'post_type' => 'page' ) ) ); + $setting_id = WP_Customize_Post_Terms_Setting::get_post_terms_setting_id( $post, 'category' ); + $this->assertEquals( "post_terms[page][$post->ID][category]", $setting_id ); + } + + /** + * Test constructor exceptions. + * + * @covers WP_Customize_Post_Terms_Setting::__construct() + */ + function test_construct_exceptions() { + // Test illegal setting id. + $exception = null; + try { + new WP_Customize_Post_Terms_Setting( $this->manager, 'bad' ); + } catch ( Exception $e ) { + $exception = $e; + } + $this->assertInstanceOf( 'Exception', $exception ); + $this->assertContains( 'Illegal setting id', $exception->getMessage() ); + + // Test illegal setting id. + $exception = null; + try { + new WP_Customize_Post_Terms_Setting( $this->manager, sprintf( 'post_terms[post][%d][food]', -123 ) ); + } catch ( Exception $e ) { + $exception = $e; + } + $this->assertInstanceOf( 'Exception', $exception ); + $this->assertContains( 'Illegal setting id', $exception->getMessage() ); + + // Test unrecognized post type. + $bad_post_id = $this->factory()->post->create( array( 'post_type' => 'unknown' ) ); + $setting_id = WP_Customize_Post_Terms_Setting::get_post_terms_setting_id( get_post( $bad_post_id ), 'bad' ); + try { + new WP_Customize_Post_Terms_Setting( $this->manager, $setting_id ); + } catch ( Exception $e ) { + $exception = $e; + } + $this->assertInstanceOf( 'Exception', $exception ); + $this->assertContains( 'Unrecognized post type', $exception->getMessage() ); + + // Test unrecognized taxonomy. + $post_id = $this->factory()->post->create( array( 'post_type' => 'post' ) ); + $setting_id = WP_Customize_Post_Terms_Setting::get_post_terms_setting_id( get_post( $post_id ), 'bad' ); + try { + new WP_Customize_Post_Terms_Setting( $this->manager, $setting_id ); + } catch ( Exception $e ) { + $exception = $e; + } + $this->assertInstanceOf( 'Exception', $exception ); + $this->assertContains( 'Unrecognized taxonomy', $exception->getMessage() ); + + // Test posts component is not created. + unset( $this->manager->posts ); + $setting_id = WP_Customize_Post_Terms_Setting::get_post_terms_setting_id( get_post( $post_id ), 'category' ); + try { + new WP_Customize_Post_Terms_Setting( $this->manager, $setting_id ); + } catch ( Exception $e ) { + $exception = $e; + } + $this->manager->posts = $this->posts; + $this->assertInstanceOf( 'Exception', $exception ); + $this->assertContains( 'Posts component not instantiated', $exception->getMessage() ); + } + + /** + * Test constructor properties. + * + * @covers WP_Customize_Post_Terms_Setting::__construct() + */ + function test_construct_properties() { + $admin_user_id = get_current_user_id(); + $this->assertTrue( current_user_can( 'manage_categories' ) ); + $post_id = $this->factory()->post->create( array( 'post_type' => 'post', 'post_author' => $admin_user_id ) ); + $post = get_post( $post_id ); + $taxonomy = 'category'; + $setting_id = sprintf( 'post_terms[post][%d][%s]', $post_id, $taxonomy ); + + $setting = new WP_Customize_Post_Terms_Setting( $this->manager, $setting_id ); + $this->assertEquals( $post_id, $setting->post_id ); + $this->assertEquals( $post->post_type, $setting->post_type ); + $this->assertEquals( $taxonomy, $setting->taxonomy ); + $this->assertEquals( array(), $setting->default ); + $this->assertEquals( version_compare( strtok( get_bloginfo( 'version' ), '-' ), '4.7', '>=' ) ? 'assign_categories' : 'edit_posts', $setting->capability ); + $this->assertInstanceOf( 'WP_Customize_Posts', $setting->posts_component ); + + $setting = new WP_Customize_Post_Terms_Setting( $this->manager, $setting_id, array( + 'capability' => 'create_awesome', + ) ); + $this->assertEquals( 'create_awesome', $setting->capability ); + $this->assertEquals( array(), $setting->default ); + + add_filter( 'user_has_cap', '__return_empty_array' ); + $setting = new WP_Customize_Post_Terms_Setting( $this->manager, $setting_id, array( + 'capability' => 'delete_awesome', + ) ); + $this->assertEquals( 'delete_awesome', $setting->capability ); + remove_filter( 'user_has_cap', '__return_empty_array' ); + } + + /** + * Test sanitize. + * + * @covers WP_Customize_Post_Terms_Setting::sanitize() + */ + function test_sanitize() { + $post_id = $this->factory()->post->create( array( 'post_type' => 'post') ); + $taxonomy = 'category'; + $setting_id = WP_Customize_Post_Terms_Setting::get_post_terms_setting_id( get_post( $post_id ), $taxonomy ); + $setting = new WP_Customize_Post_Terms_Setting( $this->manager, $setting_id ); + $term_id = $this->factory()->term->create( array( + 'taxonomy' => $taxonomy, + 'name' => 'Foo', + ) ); + $this->assertSame( $setting->sanitize( array( (string) $term_id ) ), array( $term_id ) ); + + $this->assertSame( $setting->sanitize( array() ), array() ); + + $validity = $setting->sanitize( 'bad' ); + $this->assertInstanceOf( 'WP_Error', $validity ); + $this->assertEquals( 'expected_array', $validity->get_error_code() ); + + $validity = $setting->sanitize( array( 'bad' ) ); + $this->assertInstanceOf( 'WP_Error', $validity ); + $this->assertEquals( 'invalid_term_id', $validity->get_error_code() ); + + $validity = $setting->sanitize( array( -1 ) ); + $this->assertInstanceOf( 'WP_Error', $validity ); + $this->assertEquals( 'invalid_term_id', $validity->get_error_code() ); + + $tag_term_id = $this->factory()->term->create( array( + 'taxonomy' => 'post_tag', + 'name' => 'Foo', + ) ); + $validity = $setting->sanitize( array( $tag_term_id ) ); + $this->assertInstanceOf( 'WP_Error', $validity ); + $this->assertEquals( 'missing_term', $validity->get_error_code() ); + + add_filter( "customize_sanitize_{$setting_id}", '__return_empty_array' ); + $this->assertSame( $setting->sanitize( array( 1, 2, 3 ) ), array() ); + } + + /** + * Test getting value that is not previewed. + * + * @covers WP_Customize_Post_Terms_Setting::value() + */ + function test_non_previewed_value() { + $post_id = $this->factory()->post->create( array( 'post_type' => 'post') ); + + $taxonomy = 'post_tag'; + $term_id_1 = $this->factory()->term->create( array( + 'taxonomy' => $taxonomy, + 'name' => 'Foo', + ) ); + $term_id_2 = $this->factory()->term->create( array( + 'taxonomy' => $taxonomy, + 'name' => 'Bar', + ) ); + wp_set_post_terms( $post_id, array( $term_id_1 ), $taxonomy ); + + $setting_id = WP_Customize_Post_Terms_Setting::get_post_terms_setting_id( get_post( $post_id ), $taxonomy ); + $exception = null; + $setting = new WP_Customize_Post_Terms_Setting( $this->manager, $setting_id ); + $this->assertSame( array( $term_id_1 ), $setting->value() ); + wp_set_post_terms( $post_id, array( $term_id_2 ), $taxonomy ); + $this->assertSame( array( $term_id_2 ), $setting->value() ); + } + + /** + * Test previewing. + * + * @covers WP_Customize_Post_Terms_Setting::preview() + * @covers WP_Customize_Posts_Preview::filter_get_the_terms_to_preview() + * @covers WP_Customize_Posts_Preview::filter_get_object_terms_to_preview() + */ + function test_preview() { + $taxonomy = 'post_tag'; + $initial_post_terms = array( self::$post_tag_term_ids['a'], self::$post_tag_term_ids['b'] ); + wp_set_post_terms( self::$post_ids[0], $initial_post_terms, $taxonomy ); + wp_set_post_terms( self::$post_ids[1], array( self::$post_tag_term_ids['b'], self::$post_tag_term_ids['c'] ), $taxonomy ); + wp_set_post_terms( self::$post_ids[2], array( self::$post_tag_term_ids['c'] ), $taxonomy ); + + $terms = wp_get_object_terms( array( self::$post_ids[0], self::$post_ids[1] ), 'post_tag', array( 'fields' => 'all_with_object_id' ) ); + $expected = array( + self::$post_tag_term_ids['a'], + self::$post_tag_term_ids['b'], + self::$post_tag_term_ids['b'], + self::$post_tag_term_ids['c'], + ); + $this->assertEqualSets( $expected, wp_list_pluck( $terms, 'term_id' ) ); + + $setting_id = WP_Customize_Post_Terms_Setting::get_post_terms_setting_id( get_post( self::$post_ids[0] ), $taxonomy ); + $other_setting_id = WP_Customize_Post_Terms_Setting::get_post_terms_setting_id( get_post( self::$post_ids[2] ), $taxonomy ); + $previewed_post_terms = array( self::$post_tag_term_ids['b'], self::$post_tag_term_ids['c'] ); + $this->manager->set_post_value( $setting_id, $previewed_post_terms ); + + $setting = new WP_Customize_Post_Terms_Setting( $this->manager, $setting_id ); + $other_setting = new WP_Customize_Post_Terms_Setting( $this->manager, $other_setting_id ); + $this->assertEquals( $initial_post_terms, $setting->value() ); + $this->assertEquals( $initial_post_terms, wp_list_pluck( get_the_terms( self::$post_ids[0], $taxonomy ), 'term_id' ) ); + $this->assertEquals( $initial_post_terms, wp_get_post_terms( self::$post_ids[0], $taxonomy, array( 'fields' => 'ids' ) ) ); + $terms = wp_get_post_terms( self::$post_ids[0], $taxonomy, array( 'fields' => 'all_with_object_id' ) ); + $this->assertEquals( $initial_post_terms, wp_list_pluck( $terms, 'term_id' ) ); + + $this->assertEquals( array( self::$post_tag_term_ids['b'], self::$post_tag_term_ids['a'] ), wp_get_post_terms( self::$post_ids[0], $taxonomy, array( + 'fields' => 'ids', + 'orderby' => 'name', + 'order' => 'DESC', + ) ) ); + + wp_cache_delete( $setting->post_id, $setting->taxonomy . '_relationships' ); + wp_cache_delete( $other_setting->post_id, $setting->taxonomy . '_relationships' ); + $this->assertTrue( $setting->preview() ); + + $this->assertEquals( $initial_post_terms, wp_cache_get( $setting->post_id, $setting->taxonomy . '_relationships' ) ); + $this->assertEquals( $previewed_post_terms, $setting->value() ); + $this->assertEquals( $previewed_post_terms, wp_list_pluck( get_the_terms( self::$post_ids[0], $taxonomy ), 'term_id' ) ); + $this->assertEquals( $previewed_post_terms, wp_get_post_terms( self::$post_ids[0], $taxonomy, array( 'fields' => 'ids' ) ) ); + $terms = wp_get_post_terms( self::$post_ids[0], $taxonomy, array( 'fields' => 'all' ) ); + $this->assertEquals( $previewed_post_terms, wp_list_pluck( $terms, 'term_id' ) ); + $this->assertObjectNotHasAttribute( 'object_id', $terms[0] ); + $terms = wp_get_post_terms( self::$post_ids[0], $taxonomy, array( 'fields' => 'all_with_object_id' ) ); + $this->assertEquals( $previewed_post_terms, wp_list_pluck( $terms, 'term_id' ) ); + $this->assertObjectHasAttribute( 'object_id', $terms[0] ); + $this->assertSame( self::$post_ids[0], $terms[0]->object_id ); + + $this->assertTrue( $other_setting->preview() ); + $this->manager->set_post_value( $other_setting_id, array( self::$post_tag_term_ids['a'] ) ); + $this->assertEquals( array( self::$post_tag_term_ids['c'] ), wp_cache_get( $other_setting->post_id, $other_setting->taxonomy . '_relationships' ) ); + $this->assertEquals( array( self::$post_tag_term_ids['a'] ), $other_setting->value() ); + $this->assertEquals( array( self::$post_tag_term_ids['a'] ), wp_get_post_terms( $other_setting->post_id, $taxonomy, array( 'fields' => 'ids' ) ) ); + + $this->assertEquals( array( self::$post_tag_term_ids['c'], self::$post_tag_term_ids['b'] ), wp_get_post_terms( self::$post_ids[0], $taxonomy, array( + 'fields' => 'ids', + 'orderby' => 'name', + 'order' => 'DESC', + ) ) ); + + $terms = wp_get_object_terms( array( self::$post_ids[0], self::$post_ids[1] ), 'post_tag', array( 'fields' => 'all_with_object_id' ) ); + $expected = array( + self::$post_tag_term_ids['b'], + self::$post_tag_term_ids['b'], + self::$post_tag_term_ids['c'], + self::$post_tag_term_ids['c'], + ); + $this->assertEqualSets( $expected, wp_list_pluck( $terms, 'term_id' ) ); + + // Make sure the object cache did not get polluted when previewing is removed. + remove_all_filters( 'get_object_terms' ); + remove_all_filters( 'get_the_terms' ); + $this->assertEquals( $initial_post_terms, wp_list_pluck( get_the_terms( self::$post_ids[0], $taxonomy ), 'term_id' ) ); + } + + /** + * Test single save. + * + * @covers WP_Customize_Post_Terms_Setting::update() + */ + function test_update() { + $post_id = self::$post_ids[0]; + $taxonomy = 'post_tag'; + $term_id_1 = self::$post_tag_term_ids['a']; + $term_id_2 = self::$post_tag_term_ids['b']; + $initial_post_terms = array( $term_id_1 ); + wp_set_post_terms( $post_id, $initial_post_terms, $taxonomy ); + + $setting_id = WP_Customize_Post_Terms_Setting::get_post_terms_setting_id( get_post( $post_id ), $taxonomy ); + $modified_post_terms = array( $term_id_2 ); + $this->manager->set_post_value( $setting_id, $modified_post_terms ); + + $setting = new WP_Customize_Post_Terms_Setting( $this->manager, $setting_id ); + $this->assertEquals( $initial_post_terms, $setting->value() ); + $action_count = did_action( 'set_object_terms' ); + $this->assertTrue( false !== $setting->save() ); + $this->assertEquals( $action_count + 1, did_action( 'set_object_terms' ) ); + $this->assertEquals( $modified_post_terms, $setting->value() ); + $this->assertEquals( $modified_post_terms, wp_list_pluck( get_the_terms( $post_id, $taxonomy ), 'term_id' ) ); + } +} diff --git a/tests/php/test-class-wp-customize-posts.php b/tests/php/test-class-wp-customize-posts.php index 29baadf..68409c3 100644 --- a/tests/php/test-class-wp-customize-posts.php +++ b/tests/php/test-class-wp-customize-posts.php @@ -170,7 +170,7 @@ public function test_get_post_types() { /** * Test post type descriptions for built-in post types gets set. * - * @see WP_Customize_Posts::configure_builtin_post_types() + * @see WP_Customize_Posts::configure_builtins() */ public function test_configure_builtin_post_types() { global $wp_post_types; @@ -183,14 +183,14 @@ public function test_configure_builtin_post_types() { $this->assertEmpty( $wp_post_types['page']->description ); $this->assertObjectNotHasAttribute( 'show_in_customizer', $wp_post_types['attachment'] ); - $this->posts->configure_builtin_post_types(); + $this->posts->configure_builtins(); $this->assertNotEmpty( $wp_post_types['post']->description ); $this->assertNotEmpty( $wp_post_types['page']->description ); $this->assertFalse( $wp_post_types['attachment']->show_in_customizer ); $wp_post_types['attachment']->show_in_customizer = true; - $this->posts->configure_builtin_post_types(); + $this->posts->configure_builtins(); $this->assertTrue( $wp_post_types['attachment']->show_in_customizer ); } @@ -748,11 +748,13 @@ public function test_get_settings() { $this->assertEqualSets( array( WP_Customize_Post_Setting::get_post_setting_id( get_post( $published_post_id ) ), + WP_Customize_Post_Terms_Setting::get_post_terms_setting_id( get_post( $published_post_id ), 'post_format' ), WP_Customize_Post_Setting::get_post_setting_id( get_post( $trashed_post_id ) ), + WP_Customize_Post_Terms_Setting::get_post_terms_setting_id( get_post( $trashed_post_id ), 'post_format' ), WP_Customize_Post_Setting::get_post_setting_id( get_post( $draft_page_id ) ), sprintf( 'nav_menu_item[%s]', $nav_menu_item_id ), WP_Customize_Postmeta_Setting::get_post_meta_setting_id( get_post( $published_post_id ), 'baz' ), - WP_Customize_Postmeta_Setting::get_post_meta_setting_id( get_post( $trashed_post_id ), 'baz' ) + WP_Customize_Postmeta_Setting::get_post_meta_setting_id( get_post( $trashed_post_id ), 'baz' ), ), array_keys( $settings_params ) );