Skip to content
This repository has been archived by the owner on Dec 27, 2022. It is now read-only.

Add initial read-only REST API endpoints for snapshots #63

Merged
merged 9 commits into from Jul 27, 2016
5 changes: 5 additions & 0 deletions .dev-lib
@@ -1,3 +1,8 @@
PATH_INCLUDES='*.* php js css tests'
WPCS_GIT_TREE=develop
ASSETS_DIR=wp-assets

function after_wp_install {
echo "Installing REST API..."
svn export -q https://plugins.svn.wordpress.org/rest-api/trunk/ "$WP_CORE_DIR/src/wp-content/plugins/rest-api"
}
2 changes: 1 addition & 1 deletion dev-lib
39 changes: 35 additions & 4 deletions php/class-post-type.php
Expand Up @@ -94,13 +94,18 @@ public function register() {
'publish_posts' => 'do_not_allow',
),
'rewrite' => false,
'show_in_customizer' => false,
'show_in_customizer' => false, // Prevent inception.
'show_in_rest' => true,
'rest_base' => 'customize_snapshots',
'rest_controller_class' => __NAMESPACE__ . '\\Snapshot_REST_API_Controller',
'customize_snapshot_post_type_obj' => $this,
'menu_icon' => 'dashicons-camera',
'register_meta_box_cb' => array( $this, 'setup_metaboxes' ),
);

register_post_type( static::SLUG, $args );

add_filter( 'post_type_link', array( $this, 'filter_post_type_link' ), 10, 2 );
add_action( 'add_meta_boxes_' . static::SLUG, array( $this, 'remove_publish_metabox' ), 100 );
add_action( 'load-revision.php', array( $this, 'suspend_kses_for_snapshot_revision_restore' ) );
add_filter( 'bulk_actions-edit-' . static::SLUG, array( $this, 'filter_bulk_actions' ) );
Expand All @@ -110,6 +115,23 @@ public function register() {
add_filter( 'user_has_cap', array( $this, 'filter_user_has_cap' ), 10, 2 );
}

/**
* Filter post link.
*
* @param string $url URL.
* @param \WP_Post $post Post.
* @return string URL.
*/
public function filter_post_type_link( $url, $post ) {
if ( self::SLUG === $post->post_type ) {
$url = add_query_arg(
array( 'customize_snapshot_uuid' => $post->post_name ),
home_url( '/' )
);
}
return $url;
}

/**
* Suspend kses which runs on content_save_pre and can corrupt JSON in post_content.
*
Expand Down Expand Up @@ -256,10 +278,13 @@ public function filter_post_row_actions( $actions, $post ) {
$actions
);

$frontend_view_url = add_query_arg( array_map( 'rawurlencode', $args ), home_url() );
$actions = array_merge(
array(
'front-view' => sprintf( '<a href="%s">%s</a>', esc_url( $frontend_view_url ), esc_html__( 'Preview Snapshot', 'customize-snapshots' ) ),
'front-view' => sprintf(
'<a href="%s">%s</a>',
esc_url( get_permalink( $post->ID ) ),
esc_html__( 'Preview Snapshot', 'customize-snapshots' )
),
),
$actions
);
Expand Down Expand Up @@ -329,7 +354,7 @@ public function render_data_metabox( $post ) {
esc_html__( 'Edit in Customizer', 'customize-snapshots' )
);

$frontend_view_url = add_query_arg( array_map( 'rawurlencode', $args ), home_url() );
$frontend_view_url = get_permalink( $post->ID );
echo sprintf(
'<a href="%s" class="button button-secondary">%s</a>',
esc_url( $frontend_view_url ),
Expand Down Expand Up @@ -506,6 +531,12 @@ public function save( array $args ) {
}
$post_arr['post_status'] = $args['status'];
}
if ( ! empty( $args['author'] ) ) {
$post_arr['post_author'] = $args['author'];
}
if ( ! empty( $args['date_gmt'] ) ) {
$post_arr['post_date_gmt'] = $args['date_gmt'];
}

$this->suspend_kses();
if ( $is_update ) {
Expand Down
185 changes: 185 additions & 0 deletions php/class-snapshot-rest-api-controller.php
@@ -0,0 +1,185 @@
<?php
/**
* REST API Controller.
*
* @package CustomizeSnapshots
*/

namespace CustomizeSnapshots;

/**
* REST API Controller Class
*
* @todo Add support for editing. Make sure Post_Type::save() is used.
* @todo Add support for PATCH requests.
* @todo Allow use of UUID instead of ID in routes.
* @todo Disallow edits to slug.
*
* @package CustomizeSnapshots
*/
class Snapshot_REST_API_Controller extends \WP_REST_Posts_Controller {

/**
* Post type instance.
*
* @var Post_Type
*/
public $snapshot_post_type;

/**
* Snapshot_REST_API_Controller constructor.
*
* @throws Exception If the post type was not registered properly.
* @param string $post_type Post type.
*/
public function __construct( $post_type ) {
$post_type_obj = get_post_type_object( $post_type );
if ( empty( $post_type_obj ) || empty( $post_type_obj->customize_snapshot_post_type_obj ) ) {
throw new Exception( 'Missing customize_snapshot post type obj or arg for customize_snapshot_post_type_obj' );
}
$this->snapshot_post_type = $post_type_obj->customize_snapshot_post_type_obj;
parent::__construct( $post_type );
}

/**
* Get the Post's schema, conforming to JSON Schema.
*
* @return array
*/
public function get_item_schema() {
$schema = parent::get_item_schema();
$schema['properties']['content'] = array(
'description' => __( 'Object mapping setting ID to an object of setting params, including value.', 'customize-snapshots' ),
'type' => 'object',
'context' => array( 'view', 'edit' ),
);
return $schema;
}

/**
* Get the query params for collections of attachments.
*
* @return array
*/
public function get_collection_params() {
$params = parent::get_collection_params();
$params['author']['sanitize_callback'] = array( $this, 'parse_author_list' );
$params['author_exclude']['sanitize_callback'] = array( $this, 'parse_author_list' );
return $params;
}

/**
* Parse comma-separated list of authors represented as IDs or usernames.
*
* @param string $author_list Authors.
* @return array User IDs.
*/
public function parse_author_list( $author_list ) {
if ( empty( $author_list ) ) {
return array();
}
$authors = array();
foreach ( preg_split( '/\s*,\s*/', trim( $author_list ) ) as $author ) {
if ( is_numeric( $author ) ) {
$authors[] = intval( $author );
} else {
$user = get_user_by( 'slug', sanitize_user( $author ) );
if ( $user ) {
$authors[] = $user->ID;
} else {
$authors[] = -1;
}
}
}
return $authors;
}

/**
* Check for fundamental customize capability to do anything with snapshots.
*
* @return bool|\WP_Error
*/
protected function check_initial_access_permission() {
if ( ! current_user_can( 'customize' ) ) {
return new \WP_Error( 'rest_customize_unauthorized', __( 'Sorry, Customizer snapshots require proper authentication (the customize capability).', 'customize-snapshots' ), array( 'status' => rest_authorization_required_code() ) );
}
return true;
}

/**
* Check if a given request has basic access to read a snapshot.
*
* @param \WP_REST_Request $request Full details about the request.
* @return \WP_Error|boolean
*/
public function get_item_permissions_check( $request ) {
$error = $this->check_initial_access_permission();
if ( is_wp_error( $error ) ) {
return $error;
}
return parent::get_item_permissions_check( $request );
}

/**
* Check if a given request has basic access to read snapshots.
*
* @param \WP_REST_Request $request Full details about the request.
* @return \WP_Error|boolean
*/
public function get_items_permissions_check( $request ) {
$error = $this->check_initial_access_permission();
if ( is_wp_error( $error ) ) {
return $error;
}
return parent::get_items_permissions_check( $request );
}

/**
* Restrict read permission to whether the user can edit.
*
* @param \WP_Post $post Post object.
* @return boolean Can we read it?
*/
public function check_read_permission( $post ) {
$post_type_obj = get_post_type_object( 'customize_snapshot' );
if ( ! current_user_can( $post_type_obj->cap->edit_post, $post->ID ) ) {
return false;
}
return current_user_can( 'customize' ) && parent::check_read_permission( $post );
}

/**
* Prepare a single post output for response.
*
* @param \WP_Post $post Post object.
* @param \WP_REST_Request $request Request object.
* @return \WP_REST_Response $response Response.
*/
public function prepare_item_for_response( $post, $request ) {
$response = parent::prepare_item_for_response( $post, $request );
$response->data['content'] = $this->snapshot_post_type->get_post_content( $post );
return $response;
}

/**
* Update one item from the collection.
*
* @param \WP_REST_Request $request Full data about the request.
* @return \WP_Error|\WP_REST_Response
*/
public function update_item( $request ) {
unset( $request );
return new \WP_Error( 'invalid-method', sprintf( __( "Method '%s' not yet implemented.", 'customize-snapshots' ), __METHOD__ ), array( 'status' => 405 ) );
}

/**
* Delete one item from the collection.
*
* @param \WP_REST_Request $request Full data about the request.
* @return \WP_Error|\WP_REST_Response
*/
public function delete_item( $request ) {
unset( $request );
return new \WP_Error( 'invalid-method', sprintf( __( "Method '%s' not yet implemented.", 'customize-snapshots' ), __METHOD__ ), array( 'status' => 405 ) );
}
}
1 change: 0 additions & 1 deletion phpunit.xml.dist

This file was deleted.

31 changes: 31 additions & 0 deletions phpunit.xml.dist
@@ -0,0 +1,31 @@
<phpunit
bootstrap="dev-lib/phpunit-plugin-bootstrap.php"
backupGlobals="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
>
<php>
<const name="WP_TEST_VIP_QUICKSTART_ACTIVATED_PLUGINS" value="jetpack/jetpack.php,media-explorer/media-explorer.php,writing-helper/writing-helper.php,mrss/mrss.php,wordpress-importer/wordpress-importer.php,keyring/keyring.php,polldaddy/polldaddy.php" />
<const name="WPCOM_VIP_DISABLE_REMOTE_REQUEST_ERROR_REPORTING" value="1" />
<const name="WP_TEST_ACTIVATED_PLUGINS" value="rest-api/plugin.php" />
</php>
<testsuites>
<testsuite>
<directory prefix="test-" suffix=".php">./tests/</directory>
</testsuite>
</testsuites>

<filter>
<whitelist processUncoveredFilesFromWhitelist="false">
<directory suffix=".php">./</directory>
<exclude>
<directory suffix=".php">./dev-lib</directory>
<directory suffix=".php">./node_modules</directory>
<directory suffix=".php">./tests</directory>
<directory suffix=".php">./vendor</directory>
</exclude>
</whitelist>
</filter>
</phpunit>
43 changes: 43 additions & 0 deletions tests/php/test-class-post-type.php
Expand Up @@ -46,6 +46,7 @@ public function test_register() {
$post_type->register();
$this->assertTrue( post_type_exists( Post_Type::SLUG ) );

$this->assertEquals( 10, has_filter( 'post_type_link', array( $post_type, 'filter_post_type_link' ) ) );
$this->assertEquals( 100, has_action( 'add_meta_boxes_' . Post_Type::SLUG, array( $post_type, 'remove_publish_metabox' ) ) );
$this->assertEquals( 10, has_action( 'load-revision.php', array( $post_type, 'suspend_kses_for_snapshot_revision_restore' ) ) );
$this->assertEquals( 10, has_filter( 'bulk_actions-edit-' . Post_Type::SLUG, array( $post_type, 'filter_bulk_actions' ) ) );
Expand All @@ -55,7 +56,30 @@ public function test_register() {
$this->assertEquals( 10, has_filter( 'user_has_cap', array( $post_type, 'filter_user_has_cap' ) ) );
}

/**
* Test filter_post_type_link.
*
* @covers Post_Type::filter_post_type_link()
*/
function test_filter_post_type_link() {
$post_type = new Post_Type( $this->plugin->customize_snapshot_manager );

$post_id = $post_type->save( array(
'uuid' => self::UUID,
'data' => array(
'blogname' => array( 'value' => 'Hello' ),
),
) );

$this->assertContains(
'customize_snapshot_uuid=' . self::UUID,
$post_type->filter_post_type_link( '', get_post( $post_id ) )
);

remove_all_filters( 'post_type_link' );
$post_type->register();
$this->assertContains( 'customize_snapshot_uuid=' . self::UUID, get_permalink( $post_id ) );
}

/**
* Suspend kses which runs on content_save_pre and can corrupt JSON in post_content.
Expand Down Expand Up @@ -465,6 +489,25 @@ public function test_save() {

$this->assertEquals( get_stylesheet(), get_post_meta( $r, '_snapshot_theme', true ) );
$this->assertEquals( $this->plugin->version, get_post_meta( $r, '_snapshot_version', true ) );

// Success with author supplied.
$user_id = $this->factory()->user->create( array( 'role' => 'administrator' ) );
$post_id = $post_type->save( array(
'uuid' => self::UUID,
'data' => $data,
'status' => 'publish',
'author' => $user_id,
) );
$this->assertEquals( $user_id, get_post( $post_id )->post_author );

// Success with future date.
$post_id = $post_type->save( array(
'uuid' => self::UUID,
'data' => $data,
'status' => 'publish',
'date_gmt' => gmdate( 'Y-m-d H:i:s', time() + 24 * 3600 ),
) );
$this->assertEquals( 'future', get_post_status( $post_id ) );
}

/**
Expand Down