wenpai.org/wp-content/plugins/o2/inc/read-api.php
2024-06-21 23:57:39 +08:00

505 lines
15 KiB
PHP

<?php
class o2_Read_API extends o2_API_Base {
public static function init() {
do_action( 'o2_read_api' );
// Need a 'method' of some sort
if ( empty( $_GET['method'] ) ) {
self::die_failure( 'no_method', __( 'No method supplied', 'o2' ) );
}
$method = strtolower( $_GET['method'] );
// ?since=unixtimestamp only return posts/comments since the specified time
global $o2_since;
$o2_since = false;
if ( isset( $_REQUEST['since'] ) ) {
// JS Date.now() sends milliseconds, so just substr to be safe
// Also, substract two seconds to allow for differences in client and
// server clocks
$o2_since = absint( substr( $_REQUEST['since'], 0, 10 ) ) - 2;
}
// sanity check. don't allow arbitrarily low since values
// or we could end up serving all the posts and comments for the blog
// so, let's lower bound since to 1 day ago
$min_since = time() - 24 * 60 * 60;
if ( $o2_since < $min_since ) {
$o2_since = $min_since;
}
// Only allow whitelisted methods
if ( in_array( $method, apply_filters( 'o2_read_api_methods', array( 'poll', 'query', 'preview' ) ) ) ) {
// Handle different methods
if ( method_exists( 'o2_Read_API', $method ) ) {
o2_Read_API::$method();
} else {
self::die_success( '1' );
}
}
self::die_failure( 'unknown_method', __( 'Unknown/unsupported method supplied', 'o2' ) );
}
public static function poll() {
// This is a super lightweight API to get posts and comments from WP
// It's intended for use with o2
// @todo Allow requesting a specific post or comment, and a post with all comments
// Need to sort things because they're queried separately
function o2_date_sort( $a, $b ) {
if ( $a['unixtime'] == $b['unixtime'] )
return 0;
return $a['unixtime'] > $b['unixtime'] ? -1 : 1;
}
$ok_to_serve_data = true;
$ok_to_serve_data = apply_filters( 'o2_read_api_ok_to_serve_data', $ok_to_serve_data );
$data = array();
if ( $ok_to_serve_data ) {
$posts = self::get_posts();
$comments = self::get_comments();
// Clean up posts and comments
$data = array();
if ( count( $posts ) ) {
foreach ( $posts as $post ) {
$data[] = o2_Fragment::get_fragment( $post );
}
}
if ( count( $comments ) ) {
foreach ( $comments as $comment ) {
$data[] = o2_Fragment::get_fragment( $comment );
}
}
// Shuffle up and deal
usort( $data, 'o2_date_sort' );
}
// Let the client know if the user is logged in or not
$is_logged_in = is_user_logged_in();
// Generate an updated nonce (they expire after all, and our "app" may be open for a long time)
$new_nonce = wp_create_nonce( 'o2_nonce' );
if ( $is_logged_in ) {
// @todo change to another way, and one that is less costly - see also below
// $current_user_id = get_current_user_id();
// update_user_meta( $current_user_id, 'o2_last_poll_gmt', time() );
}
$response = array(
"data" => $data,
"newNonce" => $new_nonce,
"loggedIn" => $is_logged_in
);
// Check for unloaded scripts and styles if there are polled posts
if ( !empty( $data ) ) {
// Attach scripts
if ( isset( $_REQUEST['scripts'] ) ) {
// Parse and sanitize the script handles already output
if ( ! is_array( $_REQUEST['scripts'] ) ) {
$_REQUEST['scripts'] = explode( ',', $_REQUEST['scripts'] );
}
$initial_scripts = is_array( $_REQUEST['scripts'] ) ? array_map( 'sanitize_text_field', $_REQUEST['scripts'] ) : null;
if ( is_array( $initial_scripts ) ) {
global $wp_scripts;
if ( ! ( $wp_scripts instanceof WP_Scripts ) ) {
$wp_scripts = new WP_Scripts();
}
// Identify new scripts needed by the polled posts
$polled_scripts = array_diff( $wp_scripts->done, $initial_scripts );
// If new scripts are needed, extract relevant data from $wp_scripts
if ( !empty( $polled_scripts ) ) {
$response['scripts'] = array();
foreach ( $polled_scripts as $handle ) {
// Abort if the handle doesn't match a registered script
if ( !isset( $wp_scripts->registered[ $handle ] ) )
continue;
// Provide basic script data
$script_data = array(
'handle' => $handle,
'footer' => ( is_array( $wp_scripts->in_footer ) && in_array( $handle, $wp_scripts->in_footer ) ),
'extra_data' => $wp_scripts->print_extra_script( $handle, false ),
);
// Base source
$src = $wp_scripts->registered[ $handle ]->src;
// Take base_url into account
if ( strpos( $src, '//' ) === 0 )
$src = is_ssl() ? 'https:' . $src : 'http:' . $src;
// Deal with root-relative URLs
if ( strpos( $src, '/' ) === 0 )
$src = $wp_scripts->base_url . $src;
if ( strpos( $src, 'http' ) !== 0 )
$src = $wp_scripts->base_url . $src;
// Version and additional arguments
if ( null === $wp_scripts->registered[ $handle ]->ver )
$ver = '';
else
$ver = $wp_scripts->registered[ $handle ]->ver ? $wp_scripts->registered[ $handle ]->ver : $wp_scripts->default_version;
if ( isset( $wp_scripts->args[ $handle ] ) )
$ver = $ver ? $ver . '&amp;' . $wp_scripts->args[$handle] : $wp_scripts->args[$handle];
// Full script source with version info
$script_data['src'] = add_query_arg( 'ver', $ver, $src );
// Add script to data that will be returned to o2
array_push( $response['scripts'], $script_data );
}
}
}
}
// Attach styles
if ( isset( $_REQUEST['styles'] ) ) {
// Parse and sanitize the script handles already output
if ( ! is_array( $_REQUEST['styles'] ) ) {
$_REQUEST['styles'] = explode( ',', $_REQUEST['styles'] );
}
// Parse and sanitize the style handles already output
$initial_styles = is_array( $_REQUEST['styles'] ) ? array_map( 'sanitize_text_field', $_REQUEST['styles'] ) : null;
if ( is_array( $initial_styles ) ) {
global $wp_styles;
// Identify new styles needed by the polled posts
$polled_styles = array_diff( $wp_styles->done, $initial_styles );
// If new styles are needed, extract relevant data from $wp_styles
if ( !empty( $polled_styles ) ) {
$response['styles'] = array();
foreach ( $polled_styles as $handle ) {
// Abort if the handle doesn't match a registered stylesheet
if ( !isset( $wp_styles->registered[ $handle ] ) )
continue;
// Provide basic style data
$style_data = array(
'handle' => $handle,
'media' => 'all',
);
// Base source
$src = $wp_styles->registered[ $handle ]->src;
// Take base_url into account
if ( strpos( $src, 'http' ) !== 0 )
$src = $wp_styles->base_url . $src;
// Version and additional arguments
if ( null === $wp_styles->registered[ $handle ]->ver )
$ver = '';
else
$ver = $wp_styles->registered[ $handle ]->ver ? $wp_styles->registered[ $handle ]->ver : $wp_styles->default_version;
if ( isset( $wp_styles->args[ $handle ] ) )
$ver = $ver ? $ver . '&amp;' . $wp_styles->args[$handle] : $wp_styles->args[$handle];
// Full script source with version info
$style_data['src'] = add_query_arg( 'ver', $ver, $src );
// @todo Handle parsing conditional comments
// Parse requested media context for stylesheet
if ( isset( $wp_styles->registered[ $handle ]->args ) )
$style_data['media'] = esc_attr( $wp_styles->registered[ $handle ]->args );
// Add script to data that will be returned to o2
array_push( $response['styles'], $style_data );
}
}
}
}
}
wp_send_json_success( $response );
}
/**
* Checks if a post is OK to add before adding to posts array in get_posts method
*
* @param WP_Post $post
*
* @return boolean True if post is ok to add.
*/
public static function is_ok_to_add( $post ) {
$ok_to_add = false;
// No password required? Add it right away
if ( empty( $post->post_password ) ) {
$ok_to_add = true;
} else if ( $is_page ) {
$ok_to_add = ! post_password_required( $post->ID );
}
return $ok_to_add;
}
/**
* Return an array of recent posts
*/
public static function get_posts() {
$is_page = false;
// Apply our 'since' parameter
function o2_filter_posts_where( $where = '' ) {
global $o2_since, $wpdb;
if ( !$o2_since )
return $where;
$holdoff = 3; // seconds - gives time for posts to "bake" (e.g. if meta is added after post insertion or update)
$since = date( 'Y-m-d H:i:s', $o2_since ); // start of range
$until = date( 'Y-m-d H:i:s', time() - $holdoff ); // end of range
$where .= $wpdb->prepare( " AND post_modified_gmt > %s AND post_modified_gmt < %s", $since, $until );
return $where;
}
// Add filters to posts query
add_filter( 'posts_where', 'o2_filter_posts_where' );
// Post stati
$post_stati = array( 'publish', 'trash' );
if ( current_user_can( 'read_private_posts' ) )
array_push( $post_stati, 'private' );
// Override post stati on $args as read is unauthenticated
$args = array();
if ( isset( $_REQUEST['queryVars'] ) ) {
$args = $_REQUEST['queryVars'];
}
$args['post_status'] = $post_stati;
$defaults = array(
'post_type' => 'post',
'post_status' => $post_stati,
'posts_per_page' => 20,
'ignore_sticky_posts' => true,
'suppress_filters' => false,
);
$query_args = wp_parse_args( $args, $defaults );
// If we are on a page, change the query post_type to page, otherwise
// we will get no data (and updates to pages will not be sent to polling
// clients on that page) [using an array of post and page doesn't work either]
if ( isset( $query_args['pagename'] ) ) {
$query_args['post_type'] = 'page';
}
// Filter for plugins
$query_args = apply_filters( 'o2_get_posts_query_args', $query_args );
// Run the query for posts
$GLOBALS['wp_the_query'] = $GLOBALS['wp_query'] = new WP_Query( $query_args );
// Set as is_home() to ensure that plugin filters run on the_content()
$GLOBALS['wp_query']->is_home = true;
// Remove filters from query
remove_filter( 'pre_option_posts_per_rss', 'o2_posts_per_rss' );
remove_filter( 'posts_where', 'o2_filter_posts_where' );
$posts = array();
// Run through The Loop
global $post;
$post = isset( $GLOBALS['wp_query']->post ) ? $GLOBALS['wp_query']->post : null;
if ( have_posts() ) {
ob_start();
wp_head();
ob_end_clean();
while ( have_posts() ) {
the_post();
if ( self::is_ok_to_add( $post ) ) {
$posts[] = $post;
}
}
ob_start();
wp_footer();
ob_end_clean();
} else if ( isset( $_REQUEST['postId'] ) && 0 != intval( $_REQUEST['postId'] ) ) {
/*
* If WP_Query returned no posts and if currently on a single page, then
* query the postID and check if it has been trashed. This is workaround
* for the ticket below.
*
* @todo Remove second query with get_post when the below ticket is addressed.
* https://core.trac.wordpress.org/ticket/29167
*/
$post = get_post( intval( $_REQUEST['postId'] ) );
if ( $post && 'trash' == $post->post_status ) {
if ( self::is_ok_to_add( $post ) ) {
$posts[] = $post;
}
}
}
return $posts;
}
/**
* Return an array of recent comments
*/
public static function get_comments() {
global $o2_since;
// Apply our 'since' parameter
function o2_comments_clauses( $clauses ) {
global $o2_since, $wpdb;
if ( !$o2_since )
return $clauses;
$clauses['where'] .= $wpdb->prepare( " AND comment_date_gmt > %s", date( 'Y-m-d H:i:s', $o2_since ) );
return $clauses;
}
// First pass - retrieve comments CREATED since o2_since
// Add o2_comments_clauses filter to the comments query
add_filter( 'comments_clauses', 'o2_comments_clauses' );
$query_args = array(
'status' => 'approve',
);
// Filter for plugins
$query_args = apply_filters( 'o2_get_comments_query_args', $query_args );
// Run the query for comments
$comments = get_comments( $query_args );
// Remove filters from query
remove_filter( 'comments_clauses', 'o2_comments_clauses' );
// Second pass - add comments MODIFIED since o2_since
if ( $o2_since ) {
$query_args = array(
'status' => 'approve',
'meta_query' => array(
array(
'key' => 'o2_comment_gmt_modified',
'value' => $o2_since,
'compare' => '>=',
'type' => 'numeric',
)
)
);
// Add filters for plugins
$query_args = apply_filters( 'o2_get_comments_query_args', $query_args );
$approved_comments = get_comments( $query_args );
$query_args = array(
'status' => 'trash',
'meta_query' => array(
array(
'key' => 'o2_comment_gmt_modified',
'value' => $o2_since,
'compare' => '>=',
'type' => 'numeric',
)
)
);
$query_args = apply_filters( 'o2_get_comments_query_args', $query_args );
$trashed_comments = get_comments( $query_args );
// Merge all of the comments together.
$comments = array_merge( $comments, $approved_comments, $trashed_comments );
}
return $comments;
}
/**
* Multipurpose action hook for plugins
*/
public static function query() {
if ( apply_filters( 'o2_read_api_ok_to_serve_data', true ) ) {
if ( isset( $_REQUEST['callback'] ) && ! empty( $_REQUEST['callback'] ) ) {
$callback = sanitize_key( $_REQUEST['callback'] );
} else {
$callback = 'default';
}
do_action( 'o2_read_api_' . $callback );
self::die_success( '1' );
} else {
self::die_failure( 'noop', __( 'Unable to complete this action', 'o2' ) );
}
}
/**
* Generates a preview version of a post or comment being created/edited
*/
public static function preview() {
$response = '<p>' . __( 'Nothing to preview.', 'o2' ) . '</p>';
// Only users that can edit posts should be able to see the preview
if ( ! current_user_can( 'edit_posts' ) ) {
self::die_failure( 'cannot_edit_posts', __( 'Sorry, you are not allowed to edit posts on this site.', 'o2' ) );
}
if ( ! empty( $_REQUEST['data'] ) ) {
switch ( $_REQUEST['type'] ) {
case 'comment':
$response = apply_filters( 'o2_preview_comment', wp_unslash( $_REQUEST['data'] ) );
$response = wp_unslash( apply_filters( 'pre_comment_content', $response ) );
$response = trim( apply_filters( 'comment_text', $response ) );
break;
case 'post':
$message = new stdClass;
$message->titleRaw = '';
$message->contentRaw = wp_unslash( $_REQUEST['data'] );
$message = o2_Write_API::generate_title( $message );
add_filter( 'o2_should_process_terms', '__return_false' );
add_filter( 'o2_process_the_content', '__return_false' );
$response = apply_filters( 'o2_preview_post', $message->contentRaw );
$response = wp_unslash( apply_filters( 'content_save_pre', $response ) );
$response = trim( apply_filters( 'the_content', $response ) );
$title = apply_filters( 'title_save_pre', $message->titleRaw );
if ( ! empty( $title ) ) {
$response = "<h1>$title</h1>" . $response;
}
break;
default:
// This page left intentionally blank
}
}
self::die_success( $response );
}
}