Skip to content

Commit

Permalink
Merge pull request #811 from 10up/feature/pull-screen-performance
Browse files Browse the repository at this point in the history
Change how the `New` tab is generated on the Pull screen for external connections
  • Loading branch information
jeffpaul authored Nov 8, 2021
2 parents 12c63f0 + def546f commit e113b76
Show file tree
Hide file tree
Showing 3 changed files with 307 additions and 10 deletions.
117 changes: 117 additions & 0 deletions includes/classes/ExternalConnections/WordPressExternalConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,19 @@ public function remote_get( $args = array() ) {
}
}

// When running a query for the Pull screen with excluded items, make a POST request instead
if ( empty( $id ) && isset( $args['post__not_in'] ) && isset( $args['dt_pull_list'] ) ) {
$query_args['post_type'] = isset( $post_type ) ? $post_type : 'post';
$query_args['per_page'] = isset( $posts_per_page ) ? $posts_per_page : 20;

$posts_response = $this->remote_post(
untrailingslashit( $this->base_url ) . '/' . self::$namespace . '/distributor/list-pull-content',
$query_args
);

return $posts_response;
}

static $types_urls;
$types_urls = array();

Expand Down Expand Up @@ -370,6 +383,110 @@ public function remote_get( $args = array() ) {
}
}

/**
* Make a remote_post request.
*
* @param string $url Endpoint URL.
* @param array $args Query arguments
* @return array|\WP_Error
*/
public function remote_post( $url = '', $args = array() ) {
if ( ! $url ) {
return new \WP_Error( 'endpoint-error', esc_html__( 'Endpoint URL must be set', 'distributor' ) );
}

$request = wp_remote_post(
$url,
$this->auth_handler->format_post_args(
array(
/**
* Filter the timeout used when calling `remote_post`
*
* @since 1.6.7
* @hook dt_remote_post_timeout
*
* @param int $timeout The timeout to use for the remote post. Default `45`.
* @param array $args The request arguments
*
* @return int The timeout to use for the remote_post call.
*/
'timeout' => apply_filters( 'dt_remote_post_timeout', 45, $args ),
/**
* Filter the remote_post query arguments
*
* @since 1.6.7
* @hook dt_remote_post_query_args
*
* @param {array} $args The request arguments.
* @param {WordPressExternalConnection} $this The current connection object.
*
* @return {array} The query arguments.
*/
'body' => apply_filters( 'dt_remote_post_query_args', $args, $this ),
)
)
);

if ( is_wp_error( $request ) ) {
return $request;
}

$response_code = wp_remote_retrieve_response_code( $request );

if ( 200 !== $response_code ) {
if ( 404 === $response_code ) {
return new \WP_Error( 'bad-endpoint', esc_html__( 'Could not connect to API endpoint.', 'distributor' ) );
}

$posts_body = json_decode( wp_remote_retrieve_body( $request ), true );

$code = empty( $posts_body['code'] ) ? 'endpoint-error' : esc_html( $posts_body['code'] );
$message = empty( $posts_body['message'] ) ? esc_html__( 'API endpoint error.', 'distributor' ) : esc_html( $posts_body['message'] );

return new \WP_Error( $code, $message );
}

$posts_body = wp_remote_retrieve_body( $request );
$response_headers = wp_remote_retrieve_headers( $request );

if ( empty( $posts_body ) ) {
return new \WP_Error( 'no-response-body', esc_html__( 'Response body is empty', 'distributor' ) );
}

$posts = json_decode( $posts_body, true );
$formatted_posts = array();

foreach ( $posts as $post ) {
$post['full_connection'] = ! empty( $response_headers['X-Distributor'] );

$formatted_posts[] = $this->to_wp_post( $post );
}

$total_posts = ! empty( $response_headers['X-WP-Total'] ) ? $response_headers['X-WP-Total'] : count( $formatted_posts );

/**
* Filter the items returned when using `WordPressExternalConnection::remote_post`
*
* @since 1.6.7
* @hook dt_remote_post
*
* @param {array} $items The items returned from the POST request.
* @param {array} $args The arguments used in the POST request.
* @param {WordPressExternalConnection} $this The current connection object.
*
* @return {array} The items returned from a remote POST request.
*/
return apply_filters(
'dt_remote_post',
[
'items' => $formatted_posts,
'total_items' => $total_posts,
],
$args,
$this
);
}

/**
* Pull items. Pass array of posts, each post should look like:
* [ 'remote_post_id' => POST ID TO GET, 'post_id' (optional) => POST ID TO MAP TO ]
Expand Down
15 changes: 5 additions & 10 deletions includes/classes/PullListTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -441,8 +441,7 @@ public function prepare_items() {
'posts_per_page' => $per_page,
'paged' => $current_page,
'post_type' => $post_type,
'orderby' => 'ID', // this is because of include/exclude truncation
'order' => 'DESC', // default but specifying to be safe
'dt_pull_list' => true, // custom argument used to only run code on this screen
];

if ( ! empty( $_GET['s'] ) ) { // @codingStandardsIgnoreLine Nonce isn't required.
Expand Down Expand Up @@ -478,15 +477,11 @@ public function prepare_items() {
}

if ( empty( $_GET['status'] ) || 'new' === $_GET['status'] ) { // @codingStandardsIgnoreLine Nonce not required.
// Sort from highest ID (newest) to low so the slice only affects later pagination.
rsort( $skipped, SORT_NUMERIC );
rsort( $syndicated, SORT_NUMERIC );
$post_ids = array_merge( $skipped, $syndicated );

// This is somewhat arbitrarily set to 200 and should probably be made filterable eventually.
// IDs can get rather large and 400 easily exceeds typical header size limits.
$post_ids = array_slice( array_merge( $skipped, $syndicated ), 0, 200, true );

$remote_get_args['post__not_in'] = $post_ids;
if ( ! empty( $post_ids ) ) {
$remote_get_args['post__not_in'] = $post_ids;
}

$remote_get_args['meta_query'] = [
[
Expand Down
185 changes: 185 additions & 0 deletions includes/rest-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,73 @@ function register_rest_routes() {
'permission_callback' => '__return_true',
)
);

register_rest_route(
'wp/v2',
'distributor/list-pull-content',
array(
'methods' => 'POST',
'callback' => __NAMESPACE__ . '\get_pull_content',
'permission_callback' => 'is_user_logged_in',
'args' => get_pull_content_list_args(),
)
);
}

/**
* Set the accepted arguments for the pull content list endpoint
*
* @return array
*/
function get_pull_content_list_args() {
return array(
'exclude' => array(
'description' => esc_html__( 'Ensure result set excludes specific IDs.', 'distributor' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
),
'page' => array(
'description' => esc_html__( 'Current page of the collection.', 'distributor' ),
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
'minimum' => 1,
),
'per_page' => array(
'description' => esc_html__( 'Maximum number of items to be returned in result set.', 'distributor' ),
'type' => 'integer',
'default' => 10,
'minimum' => 1,
'maximum' => 100,
'sanitize_callback' => 'absint',
'validate_callback' => 'rest_validate_request_arg',
),
'post_type' => array(
'description' => esc_html__( 'Limit results to content matching a certain type.', 'distributor' ),
'type' => 'string',
'default' => 'post',
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => 'rest_validate_request_arg',
),
'search' => array(
'description' => esc_html__( 'Limit results to those matching a string.', 'distributor' ),
'type' => 'string',
'validate_callback' => 'rest_validate_request_arg',
),
'post_status' => array(
'default' => 'publish',
'description' => esc_html__( 'Limit result set to content assigned one or more statuses.', 'distributor' ),
'type' => 'array',
'items' => array(
'enum' => array_merge( array_keys( get_post_stati() ), array( 'any' ) ),
'type' => 'string',
),
),
);
}

/**
Expand Down Expand Up @@ -338,6 +405,124 @@ function check_post_types_permissions() {
return $response;
}

/**
* Get a list of content to show on the Pull screen
*
* @param \WP_Rest_Request $request API request arguments
* @return \WP_REST_Response|\WP_Error
*/
function get_pull_content( $request ) {
$args = [
'posts_per_page' => isset( $request['per_page'] ) ? $request['per_page'] : 10,
'paged' => isset( $request['page'] ) ? $request['page'] : 1,
'post_type' => isset( $request['post_type'] ) ? $request['post_type'] : 'post',
'post_status' => isset( $request['post_status'] ) ? $request['post_status'] : array( 'any' ),
];

if ( ! empty( $request['search'] ) ) {
$args['s'] = urldecode( $request['search'] );
}

if ( ! empty( $request['exclude'] ) ) {
$args['post__not_in'] = $request['exclude'];
}

$query = new \WP_Query( $args, $request );

if ( empty( $query->posts ) ) {
return rest_ensure_response( array() );
}

$page = (int) $args['paged'];
$total_posts = $query->found_posts;

$max_pages = ceil( $total_posts / (int) $query->query_vars['posts_per_page'] );

if ( $page > $max_pages && $total_posts > 0 ) {
return new \WP_Error(
'rest_post_invalid_page_number',
esc_html__( 'The page number requested is larger than the number of pages available.', 'distributor' ),
array( 'status' => 400 )
);
}

$formatted_posts = array();
foreach ( $query->posts as $post ) {
if ( ! check_read_permission( $post ) ) {
continue;
}

$formatted_posts[] = array(
'id' => $post->ID,
'title' => array( 'rendered' => $post->post_title ),
'excerpt' => array( 'rendered' => $post->post_excerpt ),
'content' => array( 'raw' => $post->post_content ),
'password' => $post->post_password,
'date' => $post->post_date,
'date_gmt' => $post->post_date_gmt,
'guid' => array( 'rendered' => $post->guid ),
'modified' => $post->post_modified,
'modified_gmt' => $post->post_modified_gmt,
'type' => $post->post_type,
'link' => get_the_permalink( $post ),
'comment_status' => $post->comment_status,
'ping_status' => $post->ping_status,
);
}

$response = rest_ensure_response( $formatted_posts );

$response->header( 'X-WP-Total', (int) $total_posts );
$response->header( 'X-WP-TotalPages', (int) $max_pages );

return $response;
}

/**
* Checks if a post can be read.
*
* Copied from WordPress core.
*
* @param \WP_Post $post Post object.
* @return bool
*/
function check_read_permission( $post ) {
// Validate the post type.
$post_type = \get_post_type_object( $post->post_type );

if ( empty( $post_type ) || empty( $post_type->show_in_rest ) ) {
return false;
}

// Is the post readable?
if ( 'publish' === $post->post_status || \current_user_can( 'read_post', $post->ID ) ) {
return true;
}

$post_status_obj = \get_post_status_object( $post->post_status );
if ( $post_status_obj && $post_status_obj->public ) {
return true;
}

// Can we read the parent if we're inheriting?
if ( 'inherit' === $post->post_status && $post->post_parent > 0 ) {
$parent = \get_post( $post->post_parent );
if ( $parent ) {
return check_read_permission( $parent );
}
}

/*
* If there isn't a parent, but the status is set to inherit, assume
* it's published (as per get_post_status()).
*/
if ( 'inherit' === $post->post_status ) {
return true;
}

return false;
}

/**
* Register push errors field so we can send errors over the REST API.
*/
Expand Down

0 comments on commit e113b76

Please sign in to comment.