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

548 lines
18 KiB
PHP

<?php
class o2_Write_API extends o2_API_Base {
public static function init() {
do_action( 'o2_write_api' );
// Need a 'method' of some sort
if ( empty( $_REQUEST['method'] ) ) {
self::die_failure( 'no_method', __( 'No method supplied', 'o2' ) );
}
$method = strtolower( $_REQUEST['method'] );
// Only allow Backbone core methods. Read is not supported (use o2_read feed for that)
if ( in_array( $method, array( 'create', 'update', 'patch', 'delete' ) ) ) {
// Need a 'message' to work with
if ( empty( $_REQUEST['message'] ) ) {
self::die_failure( 'no_message', __( 'No message supplied', 'o2' ) );
}
// Check the nonce
$nonce = stripslashes( $_REQUEST['nonce'] );
if ( ! wp_verify_nonce( $nonce, 'o2_nonce' ) ) {
self::die_failure( 'invalid_nonce', __( 'Invalid or expired nonce', 'o2' ) );
}
// Make sure we can decode the Message
$message = json_decode( stripslashes( $_REQUEST['message'] ) );
if ( ! is_object( $message ) ) {
self::die_failure( 'invalid_message', __( 'Message could not be decoded', 'o2' ) );
}
// Sanitize data
if ( property_exists( $message, 'parentID' ) ) {
$message->parentID = (int) $message->parentID;
}
if ( property_exists( $message, 'postID' ) ) {
$message->postID = (int) $message->postID;
}
if ( property_exists( $message, 'id' ) ) {
$message->id = (int) $message->id;
}
// We only support post and comment messages (for now)
if ( !in_array( $message->type, apply_filters( 'o2_supported_message_types', array( 'post', 'comment', 'upload' ) ) ) ) {
self::die_failure( 'unknown_message_type', __( 'Unknown message type passed', 'o2' ) );
}
// Handle different message types
switch ( $message->type ) {
case 'comment':
o2_Write_API::init_comment( $message );
o2_Write_API::write_comment( $message, $method );
break;
case 'post':
o2_Write_API::write_post( $message, $method );
break;
case 'upload':
o2_Write_API::write_upload();
break;
default:
self::die_success( '1' );
}
}
self::die_failure( 'unknown_method', __( 'Unknown/unsupported method supplied', 'o2' ) );
}
public static function write_post( $message, $method ) {
global $post;
switch ( $method ) {
case 'delete':
if ( ! current_user_can( 'delete_post', $message->postID ) ) {
self::die_failure( 'cannot_trash_post', __( 'You are not allowed to trash this post', 'o2' ) );
}
apply_filters( 'o2_trash_post', $post, $message, $method );
wp_trash_post( $message->postID );
do_action( 'o2_writeapi_post_trashed', $message->postID );
die( '1' );
case 'update':
if ( ! current_user_can( 'edit_post', $message->postID ) ) {
self::die_failure( 'cannot_edit_post', __( 'Sorry, you are not allowed to edit this post.', 'o2' ) );
}
// Load existing post
$post = get_post( $message->postID );
if ( ! $post ) {
self::die_failure( 'post_not_found', __( 'Post not found.', 'o2' ) );
}
// Allow plugins to hook in
apply_filters( 'o2_update_post', $post, $message, $method );
// Merge data that the user can modify on the front end
$post->post_content = $message->contentRaw; // Don't addslashes() here
$post->post_title = $message->titleRaw;
if ( empty( $post->post_title ) ) {
$post->post_title = wp_trim_words( $message->contentRaw, 5 );
}
// Save it
$id = wp_update_post( $post );
// Set post format
$postFormat = $message->postFormat;
if ( 'standard' == $postFormat ) {
$postFormat = ''; // a "standard" post actually takes no format at all
}
set_post_format( $id, $postFormat );
// We must store this in postmeta, because WP doesn't allow us to manually
// control wp_posts.post_modified[_gmt].
if ( !empty( $message->unixtimeModified ) ) {
update_post_meta( $id, 'client-modified', $message->unixtimeModified );
}
// Reload full object from WP
$post = get_post( $id );
setup_postdata( $post );
o2_Write_API::update_orphan_attachments( $post );
do_action( 'o2_writeapi_post_updated', $id, $message );
// Send back updated Fragment
self::die_success( o2_Fragment::get_fragment( $post ) );
case 'patch':
if ( ! current_user_can( 'edit_post', $message->postID ) ) {
self::die_failure( 'cannot_edit_post', __( 'Sorry, you are not allowed to edit this post.', 'o2' ) );
}
// We must store this in postmeta, because WP doesn't allow us to manually
// control wp_posts.post_modified[_gmt].
if ( !empty( $message->unixtimeModified ) ) {
update_post_meta( $message->postID, 'client-modified', $message->unixtimeModified );
}
do_action( 'o2_callback_' . $message->pluginData->callback, $message->pluginData->data );
self::die_failure( 'no_callback_action_taken', __( 'No callback action taken.', 'o2' ) );
case 'create':
if ( ! current_user_can( 'publish_posts' ) ) {
self::die_failure( 'cannot_publish_posts', __( 'Sorry, you are not allowed to publish posts on this site.', 'o2' ) );
}
if ( 'standard' !== $message->postFormat || empty( $message->titleRaw ) ) {
$message->titleRaw = wp_trim_words( $message->contentRaw, 5, '...' );
}
if ( 'aside' === $message->postFormat && ! $message->disableAutoTitle ) {
$message = o2_Write_API::generate_title( $message );
}
// Set up a basic Post object
$post = get_default_post_to_edit( 'post', true );
$post->post_author = get_current_user_id();
$post->post_content = addslashes( $message->contentRaw );
$post->post_title = $message->titleRaw;
$post->post_status = 'publish';
$post = apply_filters( 'o2_create_post', $post, $message, $method );
// if syntaxhighlighter is being used...
// because we use get_default_post_to_edit above,
// syntax highlighter thinks it has already applied its encoding magic
// to tags inside code blocks
// so we need to tell it to do it again when we hit content_save_pre
// during wp_insert_post below in order for tags inside code blocks
// to survive the trip
global $SyntaxHighlighter;
if ( is_a( $SyntaxHighlighter, 'SyntaxHighlighter' ) ) {
$SyntaxHighlighter->content_save_pre_ran = false;
}
// Save it
$id = wp_insert_post( $post );
// Set post format
if ( 'standard' == $message->postFormat ) {
$message->postFormat = ''; // a "standard" post actually takes no format at all
}
set_post_format( $id, $message->postFormat );
// We must store this in postmeta, because WP doesn't allow us to manually
// control wp_posts.post_modified[_gmt].
if ( !empty( $message->unixtimeModified ) ) {
update_post_meta( $id, 'client-modified', $message->unixtimeModified );
}
// Reload full object from WP
$post = get_post( $id );
setup_postdata( $post );
o2_Write_API::update_orphan_attachments( $post );
do_action( 'o2_writeapi_post_created', $id, $message );
// Send back updated Message in standard form
self::die_success( o2_Fragment::get_fragment( $post ) );
}
}
public static function generate_title( $message ) {
$lines = explode( "\n", trim( $message->contentRaw ) );
if ( count( $lines ) <= 1 ) {
return $message;
}
$firstLine = trim( $lines[0] );
preg_match_all( '/\S+/', $firstLine, $words );
// We have a bunch of reasons to not auto-title the first line...
if ( empty( $words[0] ) ) {
return $message;
}
if ( count( $words[0] ) > 8 ) {
return $message;
}
// Don't auto-title if it's part of a list
if ( preg_match( '/^([ox\-*]|1\.) /', $firstLine ) ) {
return $message;
}
// Special case: don't auto-title if it's part of a numbered list, but it might be a Markdown title
if ( '# ' === substr( $firstLine, 0, 2 ) && count( $lines ) > 1 && '# ' === substr( $lines[1], 0, 2 ) ) {
return $message;
}
// Don't auto-title if there's a mention/tag/xpost
if ( preg_match( '/(?:^|\s|>|\b|\()[@+#]([\w\-\.]*\w+)(?:$|\s|<|\b|\))/', $firstLine ) ) {
return $message;
}
// Don't auto-title if there's a URL
if ( $firstLine !== make_clickable( $firstLine ) ) {
return $message;
}
// Don't auto-title if there are HTML tags
if ( $firstLine !== strip_tags( $firstLine ) ) {
return $message;
}
// Don't auto-title if there's a shortcode
if ( $firstLine !== do_shortcode( $firstLine ) ) {
return $message;
}
$message->postFormat = 'standard';
$message->titleRaw = $firstLine;
$message->contentRaw = trim( substr_replace( $message->contentRaw, '', 0, strlen( $message->titleRaw ) ) );
return $message;
}
/**
* Get things set up to handle a comment. We need to hook into some things because
* we use the core file (wp-comments-post.php) to do all the processing.
*/
public static function init_comment( $message ) {
if ( !empty( $message->postID ) ) {
global $post;
$post = get_post( $message->postID );
}
// Catch a bunch of actions that are fired during errors and JSON-ify them
$catch_actions = array(
'comment_id_not_found',
'comment_closed',
'comment_on_trash',
'comment_on_draft',
'comment_on_password_protected'
);
foreach ( $catch_actions as $action )
add_action( $action, array( 'o2_Write_API', 'comment_fail' ) );
// This is called once a comment has successfully posted
add_action( 'set_comment_cookies', array( 'o2_Write_API', 'comment_success' ) );
}
/**
* Handle a variety of hard comment posting failures. See ::init_comment() for the list
* of actions that trigger this.
*/
public static function comment_fail( $id ) {
self::die_failure( 'comment_failed', sprintf( __( 'Failed to post comment on post ID %s. %s', 'o2' ), $id, current_filter() ) );
}
// http://scribu.net/wordpress/prevent-blog-authors-from-editing-comments.html
// Allows all users to edit their own comments, and Editors and above to edit everyone's
public static function restrict_comment_editing( $caps, $cap, $user_id, $args ) {
if ( 'edit_comment' === $cap ) {
$comment = get_comment( $args[0] );
if ( $comment->user_id != $user_id ) {
// Only Editors can edit all comments
$caps[] = 'moderate_comments';
} else {
// Everyone can edit their own comments, always
$caps = array( 'read' );
}
}
return $caps;
}
/**
* Handle a few different variations of writing a comment.
*/
public static function write_comment( $message, $method ) {
switch ( $method ) {
case 'delete':
// @todo comment delete
// @todo capability check
self::die_failure( 'delete_comment_not_supported', __( 'Deleting comments is not supported yet', 'o2' ) );
case 'update':
// Update an existing comment
add_filter( 'map_meta_cap', array( 'o2_Write_API', 'restrict_comment_editing' ), 10, 4 );
if ( ! current_user_can( 'edit_comment', $message->id ) ) {
self::die_failure( 'cannot_edit_comment', __( 'Sorry, you are not allowed to edit this comment.', 'o2' ) );
}
remove_filter( 'map_meta_cap', array( 'o2_Write_API', 'restrict_comment_editing' ), 10 );
// Get current comment data
$comment_status = wp_get_comment_status( $message->id );
// Assume that if comment_status is false, that the comment has been deleted.
if ( false == $comment_status ) {
self::die_failure( 'comment_already_deleted', __( 'Comment has already been deleted by another session.', 'o2' ) );
} else if ( 'trash' != $comment_status && $message->isTrashed ) {
if ( ! wp_trash_comment( $message->id ) ) {
self::die_failure( 'trash_comment_failed', __( 'Trashing that comment failed.', 'o2' ) );
}
do_action( 'o2_writeapi_comment_trashed', $message->id, $message );
} else if ( 'trash' == $comment_status && ! $message->isTrashed ) {
if ( ! wp_untrash_comment( $message->id ) ) {
self::die_failure( 'untrash_comment_failed', __( 'Untrashing that comment failed.', 'o2' ) );
}
do_action( 'o2_writeapi_comment_untrashed', $message->id, $message );
} else {
// Load comment data, merge in new stuff, then save again
$comment = get_comment( $message->id );
$comment->comment_content = addslashes( $message->contentRaw );
wp_update_comment( (array) $comment );
// Modifying trash status is bumped in o2:bump_trashed_comment based on trash actions.
o2_Fragment::bump_comment_modified_time( $message->id );
do_action( 'o2_writeapi_comment_updated', $message->id, $message );
}
// Reload the full, clean object and output it
$comment = get_comment( $message->id );
self::die_success( o2_Fragment::get_fragment( $comment ) );
case 'create':
// Posting a new comment. We use the core-provided file to
// avoid re-implementing a bunch of logic
// Re-map some incoming data to match the expected _POST elements
$remap = array(
'comment_post_ID' => $message->postID,
'comment' => addslashes( $message->contentRaw ),
'comment_parent' => $message->parentID,
);
$_POST = array_merge( $_POST, $remap );
// Let the core comment handler take it from here
// Have to suppress warnings from wp-comments-post because ABSPATH gets redefined
global $wpdb, $user_ID;
@require_once ABSPATH . 'wp-comments-post.php';
// If we get here, it means the core actions weren't fired, so
// something most likely went wrong
self::die_failure( 'unknown_comment_error', __( 'Failed to post comment.', 'o2' ) );
}
}
/**
* Called from the end of the core commenting process. If we get here
* then the comment was posted successfully, so just output it and die.
*/
public static function comment_success( $comment ) {
$message = json_decode( stripslashes( $_REQUEST['message'] ) );
do_action( 'o2_writeapi_comment_created', $comment->comment_ID , $message );
self::die_success( o2_Fragment::get_fragment( $comment ) );
}
/**
*
* Ceate an attachment for images and return image tag or gallery shortcode to be rendered in the editor
*/
public static function write_upload() {
global $content_width;
$_POST['action'] = 'wp_handle_upload';
$images = array();
$files = array();
$videos = array();
$errors = array();
$output = '';
for ( $i = 0; $i < $_POST['num_files']; $i++ ) {
// Create attachment for the image.
$attachment_id = media_handle_upload( "file_$i", 0 );
if ( is_wp_error( $attachment_id ) ) {
do_action( 'o2_error', 'o2_image_upload' );
$error = array( $_FILES["file_$i"]['name'], $attachment_id->get_error_message() );
array_push( $errors, $error );
} else {
$type = wp_check_filetype( $_FILES["file_$i"]['name'] );
if ( wp_attachment_is_image( $attachment_id ) ) {
// If it's an image, add it to the image stack
array_push( $images, $attachment_id );
} else if ( 0 === strpos( $type['type'], 'video' ) ) {
array_push( $videos, $attachment_id );
} else {
// Otherwise add it to a list of files that we'll just link to directly
array_push( $files, $attachment_id );
}
}
}
// Known upload errors (@todo allow partial success, send errors in response payload)
if ( count( $errors ) ) {
self::die_failure( 'upload_failed_errors', $errors );
}
// Nothing successfully uploaded
if ( 0 == count( $images ) + count( $files ) + count( $videos ) ) {
self::die_failure( 'no_valid_uploads', __( 'Your upload failed. Perhaps try from within wp-admin.', 'o2' ) );
}
switch ( count( $images ) ) {
default: // return multiple images as a gallery shortcode
$output .= '[gallery ids="' . esc_attr( implode( ',', $images ) ) . '"]' . "\n";
break;
case 1: // return single image as html element
$image_id = $images[0];
// Get the image
$image_src = $image_src_w = get_the_guid( $image_id );
$image_dims = wp_get_attachment_image_src( $image_id, 'full' );
// if the image is NOT an animated gif, append the image_width to the src
if ( ! self::is_animated_gif( get_attached_file( $image_id ) ) && !empty( $content_width ) ) {
$image_src_w = $image_src . '?w=' . $content_width;
}
$output .= '<a href="' . esc_attr( $image_src ) .'"><img src="' . esc_attr( $image_src_w ) . '" alt="' . esc_attr( get_the_title( $image_id ) ) . '" class="size-full wp-image" id="i-' . esc_attr( $image_id ) . '" /></a>' . "\n";
break;
case 0:
break; // catch this so that it doesn't do a gallery (default)
}
// Add embed shortcodes for uploaded videos
$video_output = '';
foreach ( (array) $videos as $video ) {
$video_output .= '[video src=' . esc_url( wp_get_attachment_url( $video ) ) . "]\n";
}
$output .= apply_filters( 'o2_video_attachment', $video_output, $videos );
// Add links to each file on a line of their own
foreach ( (array) $files as $file ) {
$output .= '<a href="' . esc_url( wp_get_attachment_url( $file ) ) . '" id="i-' . esc_attr( $image_id ) . '">' . get_the_title( $file ) . '</a>' . "\n";
}
self::die_success( $output );
exit;
}
/*
* is_animated_gif
* Based on http://www.php.net/manual/en/function.imagecreatefromgif.php#104473
* An animated gif contains multiple "frames", with each frame having a
* header made up of:
* * a static 4-byte sequence (\x00\x21\xF9\x04)
* * 4 variable bytes
* * a static 2-byte sequence (\x00\x2C) (some variants may use \x00\x21 ?)
* We read through the file til we reach the end of the file, or we've found
* at least 2 frame headers
*/
public static function is_animated_gif( $image_pathname ) {
if ( ! ( $fh = @fopen( $image_pathname, 'rb' ) ) )
return false;
$count = 0;
while( ! feof( $fh ) && $count < 2 ) {
$chunk = fread( $fh, 1024 * 100 ); //read 100kb at a time
$count += preg_match_all( '#\x00\x21\xF9\x04.{4}\x00(\x2C|\x21)#s', $chunk, $matches );
}
fclose( $fh );
return ( $count > 1 );
}
/**
* Called after creating/editing to link orphan attachments to post/comment parent
*/
public static function update_orphan_attachments( $post ) {
global $wpdb;
// find all single-uploaded images/files
preg_match_all( '/\sid\=\"i\-(\d+)\"/', $post->post_content, $match );
$image_ids = $match[1];
// find all image galleries
preg_match_all( '/\[gallery ids\=\"([0-9,]+)\"/', $post->post_content, $match );
$gallery_lists = $match[1];
if ( count( $gallery_lists ) ) {
// join gallery strings into one csv, then break into array
$gallery_list = implode( ",", $gallery_lists );
$gallery_ids = explode( ",", $gallery_list );
$image_ids = array_merge( $image_ids, $gallery_ids );
}
foreach ( $image_ids as $image_id ) {
wp_update_post( array(
'ID' => $image_id,
'post_parent' => $post->ID,
) );
}
}
}