diff --git a/.dev-lib b/.dev-lib index c9c3ca8c..7d040b93 100644 --- a/.dev-lib +++ b/.dev-lib @@ -1,2 +1,3 @@ PATH_INCLUDES='*.* php js css tests' +WPCS_GIT_TREE=develop ASSETS_DIR=wp-assets \ No newline at end of file diff --git a/php/class-customize-snapshot-manager.php b/php/class-customize-snapshot-manager.php index 508ed1f5..05dfe666 100644 --- a/php/class-customize-snapshot-manager.php +++ b/php/class-customize-snapshot-manager.php @@ -75,6 +75,8 @@ class Customize_Snapshot_Manager { * @param Plugin $plugin Plugin instance. */ public function __construct( Plugin $plugin ) { + add_action( 'init', array( $this, 'create_post_type' ), 0 ); + // Bail if our conditions are not met. if ( ! ( ( isset( $_REQUEST['wp_customize'] ) && 'on' === $_REQUEST['wp_customize'] ) // WPCS: input var ok. || ( is_admin() && isset( $_SERVER['PHP_SELF'] ) && 'customize.php' === basename( $_SERVER['PHP_SELF'] ) ) // WPCS: input var ok; sanitization ok. @@ -109,7 +111,6 @@ public function __construct( Plugin $plugin ) { add_action( 'customize_controls_init', array( $this, 'set_return_url' ) ); add_action( 'init', array( $this, 'maybe_force_redirect' ), 0 ); - add_action( 'init', array( $this, 'create_post_type' ), 0 ); add_action( 'customize_controls_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); add_action( 'wp_ajax_customize_save', array( $this, 'set_snapshot_uuid' ), 0 ); add_action( 'wp_ajax_' . self::AJAX_ACTION, array( $this, 'update_snapshot' ) ); @@ -236,23 +237,129 @@ public function capture_unsanitized_snapshot_post_data() { * @access public */ public function create_post_type() { + $labels = array( + 'name' => _x( 'Snapshots', 'post type general name', 'customize-snapshots' ), + 'singular_name' => _x( 'Snapshot', 'post type singular name', 'customize-snapshots' ), + 'menu_name' => _x( 'Snapshots', 'admin menu', 'customize-snapshots' ), + 'name_admin_bar' => _x( 'Snapshot', 'add new on admin bar', 'customize-snapshots' ), + 'add_new' => _x( 'Add New', 'Customize Snapshot', 'customize-snapshots' ), + 'add_new_item' => __( 'Add New Snapshot', 'customize-snapshots' ), + 'new_item' => __( 'New Snapshot', 'customize-snapshots' ), + 'edit_item' => __( 'Inspect Snapshot', 'customize-snapshots' ), + 'view_item' => __( 'View Snapshot', 'customize-snapshots' ), + 'all_items' => __( 'All Snapshots', 'customize-snapshots' ), + 'search_items' => __( 'Search Snapshots', 'customize-snapshots' ), + 'not_found' => __( 'No snapshots found.', 'customize-snapshots' ), + 'not_found_in_trash' => __( 'No snapshots found in Trash.', 'customize-snapshots' ), + ); + $args = array( - 'labels' => array( - 'name' => __( 'Customize Snapshots', 'customize-snapshots' ), - 'singular_name' => __( 'Customize Snapshot', 'customize-snapshots' ), - ), - 'public' => false, + 'labels' => $labels, + 'description' => __( 'Customize Snapshots.', 'customize-snapshots' ), + 'public' => true, 'capability_type' => 'post', + 'publicly_queryable' => false, + 'query_var' => false, + 'exclude_from_search' => true, + 'show_ui' => true, + 'show_in_nav_menus' => false, + 'show_in_menu' => true, + 'show_in_admin_bar' => false, 'map_meta_cap' => true, 'hierarchical' => false, - 'rewrite' => false, 'delete_with_user' => false, - 'supports' => array( 'title', 'author', 'revisions' ), + 'menu_position' => null, + 'supports' => array( 'revisions' ), + 'rewrite' => false, + 'show_in_customizer' => false, + 'menu_icon' => 'dashicons-camera', + 'register_meta_box_cb' => array( $this, 'setup_metaboxes' ), ); register_post_type( self::POST_TYPE, $args ); } + /** + * Add the metabox. + */ + function setup_metaboxes() { + $id = self::POST_TYPE; + $title = __( 'Data', 'customize-snapshots' ); + $callback = array( $this, 'render_data_metabox' ); + $screen = self::POST_TYPE; + $context = 'normal'; + $priority = 'high'; + add_meta_box( $id, $title, $callback, $screen, $context, $priority ); + remove_meta_box( 'slugdiv', $screen, 'normal' ); + } + + /** + * Render the metabox. + * + * @param \WP_Post $post Post object. + */ + function render_data_metabox( $post ) { + $snapshot_content = static::get_post_content( $post ); + + echo '

' . esc_html( $post->post_name ) . '

'; + + $allowed_tags = array( + 'details' => array( 'class' => true ), + 'pre' => array( 'class' => true ), + 'summary' => array(), + ); + $rendered_content = sprintf( '
%s
', esc_html( static::encode_json( $snapshot_content ) ) ); + echo wp_kses( + apply_filters( 'rendered_customize_snapshot_data', $rendered_content, $snapshot_content, $post ), + $allowed_tags + ); + } + + /** + * Get the snapshot array out of the post_content. + * + * A post revision for a customize_snapshot may also be supplied. + * + * @param \WP_Post $post A customize_snapshot post or a revision post. + * @return array + */ + static function get_post_content( \WP_Post $post ) { + if ( self::POST_TYPE !== $post->post_type ) { + $parent_post = null; + if ( 'revision' === $post->post_type ) { + $parent_post = get_post( $post->post_parent ); + } + if ( ! $parent_post || self::POST_TYPE !== $parent_post->post_type ) { + return array(); + } + } + + // Snapshot is stored as JSON in post_content. + $snapshot = json_decode( $post->post_content, true ); + if ( is_array( $snapshot ) ) { + return $snapshot; + } + + return array(); + } + + /** + * Encode JSON with pretty formatting. + * + * @param array $value The snapshot value. + * @return string + */ + static function encode_json( $value ) { + $flags = 0; + if ( defined( '\JSON_PRETTY_PRINT' ) ) { + $flags |= \JSON_PRETTY_PRINT; + } + if ( defined( '\JSON_UNESCAPED_SLASHES' ) ) { + $flags |= \JSON_UNESCAPED_SLASHES; + } + return wp_json_encode( $value, $flags ); + } + /** * Enqueue styles & scripts for the Customizer. * diff --git a/php/class-customize-snapshot.php b/php/class-customize-snapshot.php index 035da42a..54bf921f 100644 --- a/php/class-customize-snapshot.php +++ b/php/class-customize-snapshot.php @@ -64,6 +64,13 @@ class Customize_Snapshot { */ public $apply_dirty; + /** + * Whether kses filters on content_save_pre are added. + * + * @var bool + */ + protected $kses_suspended = false; + /** * Initial loader. * @@ -101,7 +108,7 @@ public function __construct( Customize_Snapshot_Manager $snapshot_manager, $uuid if ( $post ) { // For reason why base64 encoding is used, see Customize_Snapshot::save(). - $this->data = json_decode( $post->post_content_filtered, true ); + $this->data = json_decode( $post->post_content, true ); if ( json_last_error() ) { $this->snapshot_manager->plugin->trigger_warning( 'JSON parse error: ' . ( function_exists( 'json_last_error_msg' ) ? json_last_error_msg() : json_last_error() ) ); } @@ -390,7 +397,7 @@ public function save( $status = 'draft' ) { } /** - * Filter the snapshot's data before it's saved to 'post_content_filtered'. + * Filter the snapshot's data before it's saved to 'post_content'. * * @param array $data Customizer settings and values. * @return array @@ -400,6 +407,7 @@ public function save( $status = 'draft' ) { // JSON encoded snapshot data. $post_content = wp_json_encode( $this->data, $options ); + $this->suspend_kses(); if ( ! $this->post ) { $postarr = array( 'post_type' => Customize_Snapshot_Manager::POST_TYPE, @@ -407,7 +415,7 @@ public function save( $status = 'draft' ) { 'post_title' => $this->uuid, 'post_status' => $status, 'post_author' => get_current_user_id(), - 'post_content_filtered' => $post_content, + 'post_content' => $post_content, ); $r = wp_insert_post( wp_slash( $postarr ), true ); if ( is_wp_error( $r ) ) { @@ -419,7 +427,7 @@ public function save( $status = 'draft' ) { $postarr = array( 'ID' => $this->post->ID, 'post_status' => $status, - 'post_content_filtered' => wp_slash( $post_content ), + 'post_content' => wp_slash( $post_content ), ); $r = wp_update_post( $postarr, true ); if ( is_wp_error( $r ) ) { @@ -427,7 +435,32 @@ public function save( $status = 'draft' ) { } $this->post = get_post( $r ); } + $this->restore_kses(); return null; } + + /** + * Suspend kses which runs on content_save_pre and can corrupt JSON in post_content. + * + * @see \sanitize_post() + */ + function suspend_kses() { + if ( false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' ) ) { + $this->kses_suspended = true; + kses_remove_filters(); + } + } + + /** + * Restore kses which runs on content_save_pre and can corrupt JSON in post_content. + * + * @see \sanitize_post() + */ + function restore_kses() { + if ( $this->kses_suspended ) { + kses_init_filters(); + $this->kses_suspended = false; + } + } } diff --git a/tests/php/test-class-customize-snapshot.php b/tests/php/test-class-customize-snapshot.php index a3c23e5c..b92c825c 100644 --- a/tests/php/test-class-customize-snapshot.php +++ b/tests/php/test-class-customize-snapshot.php @@ -250,7 +250,7 @@ function test_save() { $this->assertTrue( $snapshot->saved() ); $this->assertEquals( 'draft', $snapshot->status() ); - $decoded = json_decode( $snapshot->post()->post_content_filtered, true ); + $decoded = json_decode( $snapshot->post()->post_content, true ); $this->assertEquals( $decoded['foo'], $snapshot->get( $this->foo ) ); $this->assertEquals( $decoded['bar'], $snapshot->get( $this->bar ) ); @@ -259,7 +259,7 @@ function test_save() { $snapshot->set( $this->bar, 'bar_custom', true ); $snapshot->save( 'publish' ); - $decoded = json_decode( $snapshot->post()->post_content_filtered, true ); + $decoded = json_decode( $snapshot->post()->post_content, true ); $this->assertEquals( $decoded['bar'], $snapshot->get( $this->bar ) ); $this->assertEquals( 'publish', $snapshot->status() ); }