mirror of
https://github.com/discourse/wp-discourse.git
synced 2025-10-03 08:59:21 +08:00
* Refactor xmlrpc_publish_post_to_discourse; Return wp_mail success response from publish_failure_notification. * Add missing doc comments; don't use magic method to access title. * Pass post title to sync_to_discourse without setting it as a variable; Use '__return_true' in add_filter callback.
967 lines
33 KiB
PHP
967 lines
33 KiB
PHP
<?php
|
|
/**
|
|
* Publishes a post to Discourse.
|
|
*
|
|
* @package WPDiscourse
|
|
* @todo Periodically review phpcs exclusions.
|
|
*/
|
|
|
|
namespace WPDiscourse\DiscoursePublish;
|
|
|
|
use WPDiscourse\DiscourseBase;
|
|
use WPDiscourse\Templates\HTMLTemplates as Templates;
|
|
use WPDiscourse\Shared\TemplateFunctions;
|
|
|
|
/**
|
|
* Class DiscoursePublish
|
|
*/
|
|
class DiscoursePublish extends DiscourseBase {
|
|
use TemplateFunctions;
|
|
|
|
/**
|
|
* An email_notification object that has a publish_failure_notification method.
|
|
*
|
|
* @access protected
|
|
* @var \WPDiscourse\EmailNotification\EmailNotification
|
|
*/
|
|
protected $email_notifier;
|
|
|
|
/**
|
|
* Logger context
|
|
*
|
|
* @access protected
|
|
* @var string
|
|
*/
|
|
protected $logger_context = 'publish';
|
|
|
|
/**
|
|
* Instance store for log args
|
|
*
|
|
* @access protected
|
|
* @var mixed|void
|
|
*/
|
|
protected $log_args;
|
|
|
|
/**
|
|
* Allows the `force_publish_allowed` method to return `true` in unit tests.
|
|
*
|
|
* @access public
|
|
* @var bool
|
|
*/
|
|
public $force_publish_allowed = false;
|
|
|
|
|
|
|
|
/**
|
|
* DiscoursePublish constructor.
|
|
*
|
|
* @param object $email_notifier An object for sending an email verification notice.
|
|
* @param bool $register_actions Flag determines whether to register publish actions.
|
|
*/
|
|
public function __construct( $email_notifier, $register_actions = true ) {
|
|
$this->email_notifier = $email_notifier;
|
|
|
|
add_action( 'init', array( $this, 'setup_options' ) );
|
|
add_action( 'init', array( $this, 'setup_logger' ) );
|
|
|
|
// Registration is conditional to make testing easier.
|
|
if ( $register_actions ) {
|
|
if ( version_compare( get_bloginfo( 'version' ), '5.6', '>=' ) ) {
|
|
// On the difference between wp_after_insert_post and save_post see https://make.wordpress.org/core/2020/11/20/new-action-wp_after_insert_post-in-wordpress-5-6/.
|
|
add_action( 'wp_after_insert_post', array( $this, 'publish_post_after_save' ), 10, 2 );
|
|
} else {
|
|
// Priority is set to 13 so that 'publish_post_after_save' is called after the meta-box is saved.
|
|
add_action( 'save_post', array( $this, 'publish_post_after_save' ), 13, 2 );
|
|
}
|
|
|
|
add_action( 'xmlrpc_publish_post', array( $this, 'xmlrpc_publish_post_to_discourse' ) );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determines if a post should be published to Discourse after it is saved on WordPress.
|
|
*
|
|
* @param int $post_id The id of the post that has been saved.
|
|
* @param \WP_Post $post The Post object.
|
|
*
|
|
* @return null
|
|
*/
|
|
public function publish_post_after_save( $post_id, $post ) {
|
|
if ( $this->exclude_post( $post_id, $post ) ) {
|
|
return null;
|
|
}
|
|
|
|
$post_should_be_auto_published = $this->auto_publish( $post_id );
|
|
$post_already_published = $this->dc_get_post_meta( $post_id, 'discourse_post_id', true );
|
|
$post_marked_to_be_published = $this->dc_get_post_meta( $post_id, 'publish_to_discourse', true );
|
|
$publish_new_post_to_discourse = ( $post_marked_to_be_published || $post_should_be_auto_published ) && ! $post_already_published;
|
|
$topic_should_be_updated = $this->dc_get_post_meta( $post_id, 'update_discourse_topic', true );
|
|
$force_publish_post = $this->force_publish_post( $post );
|
|
$publish_to_discourse = $publish_new_post_to_discourse || $topic_should_be_updated || $force_publish_post;
|
|
$publish_to_discourse = apply_filters( 'wpdc_publish_after_save', $publish_to_discourse, $post_id, $post );
|
|
|
|
if ( $publish_to_discourse ) {
|
|
// Clear existing publishing errors.
|
|
delete_post_meta( $post_id, 'wpdc_publishing_error' );
|
|
$this->sync_to_discourse( $post_id, $post->post_title, $post->post_content );
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Excludes a post from being published under various conditions.
|
|
*
|
|
* Posts are excluded from publishing if the plugin is unconfigured, the post's status is not set to 'publish',
|
|
* the post is a revision, doesn't have a title, is not a valid post type, or has an excluded tag.
|
|
*
|
|
* @param int $post_id The ID of the post.
|
|
* @param \WP_Post $post The Post object.
|
|
*
|
|
* @return bool
|
|
*/
|
|
protected function exclude_post( $post_id, $post ) {
|
|
$plugin_unconfigured = empty( $this->options['url'] ) || empty( $this->options['api-key'] ) || empty( $this->options['publish-username'] );
|
|
$publish_status_not_set = 'publish' !== get_post_status( $post_id );
|
|
$publish_private = apply_filters( 'wpdc_publish_private_post', false, $post_id );
|
|
return wp_is_post_revision( $post_id )
|
|
|| ( $publish_status_not_set && ! $publish_private )
|
|
|| $plugin_unconfigured
|
|
|| empty( $post->post_title )
|
|
|| ! $this->is_valid_sync_post_type( $post_id )
|
|
|| $this->has_excluded_tag( $post );
|
|
}
|
|
|
|
/**
|
|
* Determines if the plugin's 'auto-publish' option is enabled and if it has been overridden for a particular post.
|
|
*
|
|
* The auto-publish option causes the 'Publish to Discourse' checkbox in the post editor to be pre-checked for all new
|
|
* posts. It is 'overridden' if the checkbox is manually unchecked.
|
|
*
|
|
* @param int $post_id The ID of the post.
|
|
*
|
|
* @return bool
|
|
*/
|
|
protected function auto_publish( $post_id ) {
|
|
// If the auto-publish option is enabled publish unpublished topics, unless the setting has been overridden.
|
|
$auto_publish_overridden = intval( $this->dc_get_post_meta( $post_id, 'wpdc_auto_publish_overridden', true ) ) === 1;
|
|
return ! $auto_publish_overridden && ! empty( $this->options['auto-publish'] );
|
|
}
|
|
|
|
/**
|
|
* Determines if a post should be 'force published.'
|
|
*
|
|
* Posts are force published if the 'force-publish' option is enabled and the post was created within the time period
|
|
* set by the 'force-publish-max-age' setting (ignored when 'force-publish-max-age is set to 0.)
|
|
*
|
|
* @param object $post The Post object.
|
|
*
|
|
* @return bool
|
|
*/
|
|
protected function force_publish_post( $post ) {
|
|
if ( empty( $this->options['force-publish'] ) || ! $this->force_publish_allowed() ) {
|
|
return false;
|
|
}
|
|
|
|
$force_publish_max_age = ! empty( $this->options['force-publish-max-age'] ) ? intval( $this->options['force-publish-max-age'] ) : 0;
|
|
$min_date = date_create()->modify( "-{$force_publish_max_age} day" )->format( 'U' );
|
|
$post_time = strtotime( $post->post_date );
|
|
|
|
return 0 === $force_publish_max_age || $post_time >= $min_date;
|
|
}
|
|
|
|
/**
|
|
* Checks if the post was published via REST_REQUEST.
|
|
*
|
|
* Currently, the force-publish option is only supported for posts published via the Block editor (a REST_REQUEST.)
|
|
* The `force_publish_allowed` property is used in unit tests.
|
|
*
|
|
* @return bool
|
|
*/
|
|
protected function force_publish_allowed() {
|
|
return ( defined( 'REST_REQUEST' ) && REST_REQUEST ) || $this->force_publish_allowed;
|
|
}
|
|
|
|
/**
|
|
* For publishing via xmlrpc.
|
|
*
|
|
* Hooks into the 'xmlrpc_publish_post' action. Publishing via xmlrpc is disabled by default. It can be enabled by
|
|
* adding a function that hooks into 'wp_discourse_before_xmlrpc_publish'. This method exists for historical reasons.
|
|
* Its main purpose is to allow a publish-failure-notice to be sent when authors publish posts from blogging software.
|
|
*
|
|
* @param int $post_id The post ID.
|
|
*
|
|
* @return null|bool
|
|
*/
|
|
public function xmlrpc_publish_post_to_discourse( $post_id ) {
|
|
$post = get_post( $post_id );
|
|
$post_is_published = 'publish' === get_post_status( $post_id );
|
|
$publish_to_discourse = false;
|
|
$publish_to_discourse = apply_filters( 'wp_discourse_before_xmlrpc_publish', $publish_to_discourse, $post );
|
|
if ( $publish_to_discourse ) {
|
|
update_post_meta( $post_id, 'publish_to_discourse', 1 );
|
|
$this->publish_post_after_save( $post_id, $post );
|
|
} elseif ( $post_is_published && ! empty( $this->options['auto-publish'] ) ) {
|
|
// Allows a notification to be sent about posts that have been published from a blogging app.
|
|
$success = $this->email_notifier->publish_failure_notification(
|
|
$post,
|
|
array(
|
|
'location' => 'after_xmlrpc_publish',
|
|
)
|
|
);
|
|
return $success;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Calls `sync_to_discourse_work` after getting the lock.
|
|
*
|
|
* @param int $post_id The post id.
|
|
* @param string $title The title.
|
|
* @param string $raw The raw content of the post.
|
|
*/
|
|
public function sync_to_discourse( $post_id, $title, $raw ) {
|
|
global $wpdb;
|
|
|
|
// this avoids a double sync, just 1 is allowed to go through at a time.
|
|
$got_lock = $wpdb->get_row( "SELECT GET_LOCK('discourse_sync_lock', 0) got_it" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
|
if ( 1 === intval( $got_lock->got_it ) ) {
|
|
$this->sync_to_discourse_work( $post_id, $title, $raw );
|
|
$wpdb->get_results( "SELECT RELEASE_LOCK('discourse_sync_lock')" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calls `sync_to_discourse_work` without a lock. Only used for testing.
|
|
* Should not be used elsewhere in plugin.
|
|
*
|
|
* @param int $post_id The post id.
|
|
* @param string $title The title.
|
|
* @param string $raw The raw content of the post.
|
|
*/
|
|
public function sync_to_discourse_without_lock( $post_id, $title, $raw ) {
|
|
return $this->sync_to_discourse_work( $post_id, $title, $raw );
|
|
}
|
|
|
|
/**
|
|
* Syncs a post to Discourse.
|
|
*
|
|
* @param int $post_id The post id.
|
|
* @param string $title The post title.
|
|
* @param string $raw The content of the post.
|
|
*
|
|
* @return null
|
|
*/
|
|
protected function sync_to_discourse_work( $post_id, $title, $raw ) {
|
|
$options = $this->options;
|
|
$discourse_id = $this->dc_get_post_meta( $post_id, 'discourse_post_id', true );
|
|
$post = get_post( $post_id );
|
|
$author_id = $post->post_author;
|
|
$use_full_post = ! empty( $options['full-post-content'] );
|
|
$use_multisite_configuration = is_multisite() && ! empty( $options['multisite-configuration-enabled'] );
|
|
$add_featured_link = ! empty( $options['add-featured-link'] );
|
|
$permalink = get_permalink( $post_id );
|
|
$title = $this->sanitize_title( $title );
|
|
$title = apply_filters( 'wpdc_publish_format_title', $title, $post_id );
|
|
|
|
$this->log_args = array(
|
|
'wp_title' => $title,
|
|
'wp_author_id' => $author_id,
|
|
'wp_post_id' => $post_id,
|
|
);
|
|
|
|
if ( $use_full_post ) {
|
|
$blocks = parse_blocks( $raw );
|
|
$parsed = '';
|
|
foreach ( $blocks as $block ) {
|
|
if ( 'core/image' === $block['blockName'] || 'core/gallery' === $block['blockName'] ) {
|
|
$parsed .= $this->extract_images_from_html( $block['innerHTML'] );
|
|
} elseif ( 'core-embed/youtube' === $block['blockName'] || 'core-embed/vimeo' === $block['blockName'] ) {
|
|
if ( ! empty( $block['attrs'] ) && ! empty( $block['attrs']['url'] ) ) {
|
|
$video_url = esc_url( $block['attrs']['url'] );
|
|
$parsed .= "\r\n\r\n{$video_url}\r\n\r\n";
|
|
}
|
|
} else {
|
|
$parsed .= apply_filters( 'the_content', render_block( $block ) );
|
|
}
|
|
}
|
|
$parsed = $this->remove_html_comments( $parsed );
|
|
$excerpt = apply_filters( 'wp_discourse_excerpt', $parsed, $options['custom-excerpt-length'], $use_full_post );
|
|
} else {
|
|
if ( has_excerpt( $post_id ) ) {
|
|
$wp_excerpt = apply_filters( 'get_the_excerpt', $post->post_excerpt, $post );
|
|
$excerpt = apply_filters( 'wp_discourse_excerpt', $wp_excerpt, $options['custom-excerpt-length'], $use_full_post );
|
|
}
|
|
|
|
// Check empty() here in case the excerpt has been set to an empty string.
|
|
if ( empty( $excerpt ) ) {
|
|
$excerpt = apply_filters( 'the_content', $raw );
|
|
$excerpt = apply_filters( 'wp_discourse_excerpt', wp_trim_words( $excerpt, $options['custom-excerpt-length'] ), $options['custom-excerpt-length'], $use_full_post );
|
|
}
|
|
}
|
|
|
|
// Trim to keep the Discourse markdown parser from treating this as code.
|
|
$baked = trim( Templates::publish_format_html( $post_id ) );
|
|
$baked = str_replace( '{excerpt}', $excerpt, $baked );
|
|
$baked = str_replace( '{blogurl}', $permalink, $baked );
|
|
$author = get_the_author_meta( 'display_name', $author_id );
|
|
$baked = str_replace( '{author}', $author, $baked );
|
|
$thumb = wp_get_attachment_image_src( get_post_thumbnail_id( $post_id ), 'thumbnail' );
|
|
if ( ! empty( $thumb ) ) {
|
|
$baked = str_replace( '{thumbnail}', '', $baked );
|
|
}
|
|
$featured = wp_get_attachment_image_src( get_post_thumbnail_id( $post_id ), 'full' );
|
|
if ( ! empty( $featured ) ) {
|
|
$baked = str_replace( '{featuredimage}', '', $baked );
|
|
}
|
|
|
|
// Get publish category of a post.
|
|
$publish_post_category = get_post_meta( $post_id, 'publish_post_category', true );
|
|
// This would be used if a post is published through XML-RPC.
|
|
$default_category = isset( $options['publish-category'] ) ? $options['publish-category'] : '';
|
|
$category = ! empty( $publish_post_category ) ? $publish_post_category : $default_category;
|
|
$category = apply_filters( 'wpdc_publish_post_category', $category, $post_id );
|
|
if ( ! empty( $this->options['allow-tags'] ) ) {
|
|
$tags = get_post_meta( $post_id, 'wpdc_topic_tags', true );
|
|
// For the Block Editor, tags are being set through the API. For this case, it's easier to handle the data as a string.
|
|
if ( ! is_array( $tags ) ) {
|
|
$tags = explode( ',', $tags );
|
|
}
|
|
} else {
|
|
$tags = array();
|
|
}
|
|
|
|
$remote_post_type = '';
|
|
|
|
// The post hasn't been published to Discourse yet.
|
|
if ( ! $discourse_id > 0 ) {
|
|
// Unlisted has been moved from post metadata to a site option. This is awkward for now.
|
|
$unlisted_post = get_post_meta( $post_id, 'wpdc_unlisted_topic', true );
|
|
$unlisted_option = $this->options['publish-as-unlisted'];
|
|
$unlisted = apply_filters( 'wpdc_publish_unlisted', ! empty( $unlisted_post ) || ! empty( $unlisted_option ), $post, $post_id );
|
|
if ( $unlisted ) {
|
|
update_post_meta( $post_id, 'wpdc_unlisted_topic', 1 );
|
|
}
|
|
|
|
$body = array(
|
|
'embed_url' => $permalink,
|
|
'featured_link' => $add_featured_link ? $permalink : null,
|
|
'title' => $title,
|
|
'raw' => $baked,
|
|
'category' => $category,
|
|
'skip_validations' => 'true',
|
|
'auto_track' => ( ! empty( $options['auto-track'] ) ? 'true' : 'false' ),
|
|
'visible' => $unlisted ? 'false' : 'true',
|
|
);
|
|
|
|
$tags = array_filter( $tags );
|
|
if ( ! empty( $tags ) ) {
|
|
$body['tags'] = $tags;
|
|
}
|
|
|
|
$path = '/posts';
|
|
$remote_post_options = array(
|
|
'method' => 'POST',
|
|
'body' => $body,
|
|
);
|
|
$remote_post_type = 'create_post';
|
|
} else {
|
|
// The post has already been published.
|
|
$body = array(
|
|
'title' => $title,
|
|
'post' => array(
|
|
'raw' => $baked,
|
|
),
|
|
'skip_validations' => 'true',
|
|
);
|
|
$path = '/posts/' . $discourse_id;
|
|
$remote_post_options = array(
|
|
'method' => 'PUT',
|
|
'body' => $body,
|
|
);
|
|
$remote_post_type = 'update_post';
|
|
}
|
|
|
|
$remote_post_options['body'] = apply_filters( 'wpdc_publish_body', $remote_post_options['body'], $remote_post_type, $post_id );
|
|
|
|
$username = apply_filters( 'wpdc_discourse_username', get_the_author_meta( 'discourse_username', $post->post_author ), $author_id );
|
|
$username_exists = $username && strlen( $username ) > 1;
|
|
$single_user_api_key = ! empty( $this->options['single-user-api-key-publication'] );
|
|
|
|
if ( 'create_post' === $remote_post_type && ! $single_user_api_key && $username_exists ) {
|
|
$remote_post_options['api_username'] = $username;
|
|
}
|
|
|
|
$response = $this->remote_post( $path, $remote_post_options, $remote_post_type, $post_id );
|
|
|
|
if ( is_wp_error( $response ) ) {
|
|
return $response;
|
|
}
|
|
|
|
$body = $this->validate_response_body( $response, $remote_post_type, $post_id );
|
|
|
|
if ( is_wp_error( $body ) ) {
|
|
return $body;
|
|
}
|
|
|
|
if ( 'create_post' === $remote_post_type ) {
|
|
$discourse_id = intval( $body->id );
|
|
$topic_slug = sanitize_text_field( $body->topic_slug );
|
|
$topic_id = intval( $body->topic_id );
|
|
$topic_url = esc_url_raw( $options['url'] . '/t/' . $topic_slug . '/' . $topic_id );
|
|
|
|
$this->dc_add_post_meta( $post_id, 'discourse_post_id', $discourse_id, true );
|
|
$this->dc_add_post_meta( $post_id, 'discourse_topic_id', $topic_id, true );
|
|
$this->dc_add_post_meta( $post_id, 'discourse_permalink', $topic_url, true );
|
|
update_post_meta( $post_id, 'publish_post_category', $category );
|
|
|
|
$this->log_args['discourse_post_id'] = $discourse_id;
|
|
|
|
// Used for resetting the error notification, if one was being displayed.
|
|
update_post_meta( $post_id, 'wpdc_publishing_response', 'success' );
|
|
if ( $use_multisite_configuration ) {
|
|
$blog_id = intval( get_current_blog_id() );
|
|
$this->save_topic_blog_id( $body->topic_id, $blog_id );
|
|
}
|
|
|
|
$pin_until = get_post_meta( $post_id, 'wpdc_pin_until', true );
|
|
if ( ! empty( $pin_until ) ) {
|
|
$pin_response = $this->pin_discourse_topic( $post_id, $topic_id, $pin_until );
|
|
|
|
if ( is_wp_error( $pin_response ) ) {
|
|
return $pin_response;
|
|
}
|
|
}
|
|
|
|
if ( $single_user_api_key && $username_exists ) {
|
|
$change_response = $this->change_post_owner( $post_id, $username );
|
|
|
|
if ( is_wp_error( $change_response ) ) {
|
|
return $change_response;
|
|
}
|
|
}
|
|
|
|
$this->after_publish( $post_id, $remote_post_type );
|
|
|
|
// The topic has been created and its associated post's metadata has been updated.
|
|
return null;
|
|
}
|
|
|
|
if ( 'update_post' === $remote_post_type ) {
|
|
$discourse_post = $body->post;
|
|
$topic_slug = sanitize_text_field( $discourse_post->topic_slug );
|
|
$topic_id = intval( $discourse_post->topic_id );
|
|
$topic_url = esc_url_raw( $options['url'] . '/t/' . $topic_slug . '/' . $topic_id );
|
|
|
|
update_post_meta( $post_id, 'discourse_permalink', $topic_url );
|
|
update_post_meta( $post_id, 'discourse_topic_id', $topic_id );
|
|
update_post_meta( $post_id, 'wpdc_publishing_response', 'success' );
|
|
// Allows the publish_post_category to be set by clicking the "Update Discourse Topic" button.
|
|
update_post_meta( $post_id, 'publish_post_category', $category );
|
|
|
|
if ( $use_multisite_configuration ) {
|
|
// Used when use_multisite_configuration is enabled, if an existing post is not yet associated with a topic_id/blog_id.
|
|
if ( ! $this->topic_blog_id_exists( $topic_id ) ) {
|
|
$blog_id = get_current_blog_id();
|
|
$this->save_topic_blog_id( $topic_id, $blog_id );
|
|
}
|
|
}
|
|
|
|
// Update the topic's featured_link property.
|
|
if ( ! empty( $options['add-featured-link'] ) ) {
|
|
$body = array(
|
|
'featured_link' => $permalink,
|
|
);
|
|
$remote_post_options = array(
|
|
'method' => 'PUT',
|
|
'body' => $body,
|
|
);
|
|
|
|
$featured_response = $this->remote_post( $topic_url, $remote_post_options, 'featured_link', $post_id );
|
|
|
|
if ( is_wp_error( $featured_response ) ) {
|
|
return $featured_response;
|
|
}
|
|
}
|
|
|
|
$this->after_publish( $post_id, $remote_post_type );
|
|
|
|
// The topic has been updated, and its associated post's metadata has been updated.
|
|
return null;
|
|
}
|
|
|
|
return $this->handle_error( 'unknown', $response, $post_id );
|
|
}
|
|
|
|
/**
|
|
* Pins a Discourse topic.
|
|
*
|
|
* @param int $post_id The WordPress id of the pinned post.
|
|
* @param int $topic_id The Discourse topic_id of the pinned post.
|
|
* @param string $pin_until A string that sets the pin_until date.
|
|
*
|
|
* @return null|\WP_Error
|
|
*/
|
|
protected function pin_discourse_topic( $post_id, $topic_id, $pin_until ) {
|
|
$status_path = "/t/$topic_id/status";
|
|
$body = array(
|
|
'status' => 'pinned',
|
|
'enabled' => 'true',
|
|
'until' => $pin_until,
|
|
);
|
|
$post_options = array(
|
|
'method' => 'PUT',
|
|
'body' => $body,
|
|
);
|
|
|
|
$response = $this->remote_post( $status_path, $post_options, 'pin_topic', $post_id );
|
|
|
|
delete_post_meta( $post_id, 'wpdc_pin_until' );
|
|
|
|
return $response;
|
|
}
|
|
|
|
/**
|
|
* Changes the owner of a Discourse topic associated with a WordPress post.
|
|
*
|
|
* @param int $post_id The WordPress post_id.
|
|
* @param string $username The username of the Discourse user to change ownership to.
|
|
*
|
|
* @return null|\WP_Error
|
|
*/
|
|
protected function change_post_owner( $post_id, $username ) {
|
|
$discourse_post_id = get_post_meta( $post_id, 'discourse_post_id', true );
|
|
$discourse_topic_id = get_post_meta( $post_id, 'discourse_topic_id', true );
|
|
|
|
$path = "/t/$discourse_topic_id/change-owner";
|
|
$body = array(
|
|
'username' => $username,
|
|
'post_ids' => array( $discourse_post_id ),
|
|
);
|
|
$options = array(
|
|
'method' => 'POST',
|
|
'body' => $body,
|
|
);
|
|
|
|
return $this->remote_post( $path, $options, 'change_owner', $post_id );
|
|
}
|
|
|
|
/**
|
|
* Creates an admin_notice and calls the publish_failure_notification method after a bad response is returned from Discourse.
|
|
*
|
|
* @param object $error The error returned from the request.
|
|
* @param int $post_id The post for which the notifications are being created.
|
|
*/
|
|
protected function create_bad_response_notifications( $error, $post_id ) {
|
|
$post = get_post( $post_id );
|
|
|
|
if ( empty( $post ) ) {
|
|
return;
|
|
}
|
|
|
|
$this->email_notifier->publish_failure_notification(
|
|
$post,
|
|
array(
|
|
'location' => 'after_bad_response',
|
|
'error_message' => $error->message,
|
|
'error_code' => $error->code,
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Wrapper of discourse_request to handle validation, logging and error handling.
|
|
*
|
|
* @param string $url Url or path of the remote post.
|
|
* @param object $remote_options Options to pass to remote post.
|
|
* @param string $remote_type Remote post type.
|
|
* @param int $post_id ID of post being sent.
|
|
*/
|
|
public function remote_post( $url, $remote_options, $remote_type, $post_id ) {
|
|
$remote_options['raw'] = true;
|
|
$response = $this->discourse_request( $url, $remote_options );
|
|
|
|
if ( ! $this->validate( $response ) ) {
|
|
$response = $this->handle_error( $remote_type, $response, $post_id );
|
|
} elseif ( ! empty( $this->options['verbose-publication-logs'] ) ) {
|
|
$this->logger->info( "$remote_type.post_success", $this->log_args );
|
|
}
|
|
|
|
return $response;
|
|
}
|
|
|
|
/**
|
|
* Validation for post response body.
|
|
*
|
|
* @param object $response Response to be validated.
|
|
* @param string $remote_type Remote post type.
|
|
* @param int $post_id ID of post being sent.
|
|
*/
|
|
protected function validate_response_body( $response, $remote_type, $post_id ) {
|
|
$body = json_decode( wp_remote_retrieve_body( $response ) );
|
|
$error_type = 'body_validation';
|
|
|
|
if ( $this->post_is_enqueued( $body ) ) {
|
|
return $this->handle_notice( $remote_type, $response, $post_id, 'queued_topic' );
|
|
}
|
|
|
|
if ( 'create_post' === $remote_type && ! $this->validate_create_post_body( $body ) ) {
|
|
return $this->handle_error( $remote_type, $response, $post_id, $error_type );
|
|
}
|
|
|
|
if ( 'update_post' === $remote_type ) {
|
|
if ( ! $this->validate_update_post_body( $body ) ) {
|
|
return $this->handle_error( $remote_type, $response, $post_id, $error_type );
|
|
}
|
|
|
|
if ( $this->post_is_deleted( $body ) ) {
|
|
return $this->handle_notice( $remote_type, $response, $post_id, 'deleted_topic' );
|
|
}
|
|
}
|
|
|
|
if ( ! empty( $this->options['verbose-publication-logs'] ) ) {
|
|
$this->logger->info( "$remote_type.body_valid", $this->log_args );
|
|
}
|
|
|
|
return $body;
|
|
}
|
|
|
|
/**
|
|
* Validate the body of a response when creating a post.
|
|
*
|
|
* @param object $body Body to be validated.
|
|
*/
|
|
protected function validate_create_post_body( $body ) {
|
|
return ! empty( $body->id ) && ! empty( $body->topic_slug ) && ! empty( $body->topic_id );
|
|
}
|
|
|
|
/**
|
|
* Validate the body of a response when updating a post.
|
|
*
|
|
* @param object $body Body to be validated.
|
|
*/
|
|
protected function validate_update_post_body( $body ) {
|
|
return ! empty( $body->post ) && ! empty( $body->post->topic_slug ) && ! empty( $body->post->topic_id );
|
|
}
|
|
|
|
/**
|
|
* Test for whether post was enqueued.
|
|
*
|
|
* @param object $body Body to be validated.
|
|
*/
|
|
protected function post_is_enqueued( $body ) {
|
|
return empty( $body );
|
|
}
|
|
|
|
/**
|
|
* Test for whether topic has been deleted.
|
|
*
|
|
* @param object $body Body to be validated.
|
|
*/
|
|
protected function post_is_deleted( $body ) {
|
|
return ! empty( $body->post->deleted_at );
|
|
}
|
|
|
|
/**
|
|
* Handle publication errors
|
|
*
|
|
* @param string $remote_type Remote post type.
|
|
* @param object $response Remote post response.
|
|
* @param string $post_id ID of post sent.
|
|
* @param bool $error_type Error type.
|
|
*/
|
|
protected function handle_error( $remote_type, $response, $post_id, $error_type = 'post' ) {
|
|
$atts = $this->get_response_attributes( $response );
|
|
|
|
if ( 'create_post' === $remote_type ) {
|
|
// This is a fix for a bug that was introduced by not setting the wpdc_auto_publish_overridden post_metadata
|
|
// when posts are unlinked from Discourse. That metadata is now being set. This fix is for dealing with
|
|
// previously unlinked posts.
|
|
if ( 'Embed url has already been taken' === $atts->message ) {
|
|
update_post_meta( $post_id, 'wpdc_auto_publish_overridden', 1 );
|
|
}
|
|
|
|
update_post_meta( $post_id, 'wpdc_publishing_error', sanitize_text_field( $atts->message ) );
|
|
delete_post_meta( $post_id, 'publish_to_discourse' );
|
|
|
|
if ( 'body_validation' === $error_type ) {
|
|
update_post_meta( $post_id, 'wpdc_publishing_response', 'error' );
|
|
}
|
|
}
|
|
|
|
$this->create_bad_response_notifications( $atts, $post_id );
|
|
|
|
if ( 'body_validation' === $error_type ) {
|
|
$message = __( 'An invalid response was returned from Discourse', 'wp-discourse' );
|
|
} else {
|
|
$message = __( 'An error occurred when communicating with Discourse', 'wp-discourse' );
|
|
}
|
|
|
|
$this->logger->error( "{$remote_type}.{$error_type}_error", $this->log_args );
|
|
|
|
return new \WP_Error( 'discourse_publishing_response_error', $message );
|
|
}
|
|
|
|
/**
|
|
* Handle publication notices.
|
|
*
|
|
* @param string $remote_type Type of remote post.
|
|
* @param object $response Remote post response.
|
|
* @param string $post_id ID of post sent.
|
|
* @param string $notice_type Type of notice.
|
|
*/
|
|
protected function handle_notice( $remote_type, $response, $post_id, $notice_type ) {
|
|
// The presence of notice types 'queued_topic' and 'deleted_topic' in
|
|
// wpdc_publising_error are currently used for determining whether a
|
|
// post can be published in discourse-sidebar/src/index.js.
|
|
update_post_meta( $post_id, 'wpdc_publishing_error', $notice_type );
|
|
|
|
$this->get_response_attributes( $response );
|
|
$this->logger->warn( "{$remote_type}.{$notice_type}_notice", $this->log_args );
|
|
|
|
$notice_messages = array(
|
|
'queued_topic' => __( 'The published post has been added to the Discourse approval queue', 'wp-discourse' ),
|
|
'deleted_topic' => __( 'The Discourse topic associated with this post has been deleted', 'wp-discourse' ),
|
|
);
|
|
$message = $notice_messages[ $notice_type ];
|
|
|
|
return new \WP_Error( 'discourse_publishing_response_notice', $message );
|
|
}
|
|
|
|
/**
|
|
* Retrieve the message and code from a response.
|
|
*
|
|
* @param object $response Remote post response.
|
|
*/
|
|
protected function get_response_attributes( $response ) {
|
|
$atts = (object) array(
|
|
'message' => null,
|
|
'code' => null,
|
|
);
|
|
|
|
if ( is_wp_error( $response ) ) {
|
|
$atts->message = $response->get_error_message();
|
|
} else {
|
|
$body = json_decode( wp_remote_retrieve_body( $response ) );
|
|
|
|
if ( ! empty( $body ) && ! empty( $body->errors ) && ! empty( $body->errors[0] ) ) {
|
|
$atts->message = $body->errors[0];
|
|
} else {
|
|
$atts->message = wp_remote_retrieve_response_message( $response );
|
|
}
|
|
}
|
|
|
|
if ( ! empty( $atts->message ) ) {
|
|
$this->log_args['response_message'] = $atts->message;
|
|
}
|
|
|
|
$raw_code = wp_remote_retrieve_response_code( $response );
|
|
|
|
if ( ! empty( $raw_code ) ) {
|
|
$atts->code = intval( $raw_code );
|
|
$this->log_args['http_code'] = $atts->code;
|
|
}
|
|
|
|
return $atts;
|
|
}
|
|
|
|
/**
|
|
* Checks if a post_type can be synced.
|
|
*
|
|
* @param null $post_id The ID of the post in question.
|
|
*
|
|
* @return bool
|
|
*/
|
|
protected function is_valid_sync_post_type( $post_id = null ) {
|
|
$allowed_post_types = $this->get_allowed_post_types();
|
|
$current_post_type = get_post_type( $post_id );
|
|
|
|
return in_array( $current_post_type, $allowed_post_types, true );
|
|
}
|
|
|
|
/**
|
|
* Checks if a post has an excluded tag.
|
|
*
|
|
* @param \WP_Post $post The Post object.
|
|
*
|
|
* @return bool
|
|
*/
|
|
protected function has_excluded_tag( $post ) {
|
|
if ( version_compare( get_bloginfo( 'version' ), '5.6', '<' ) ) {
|
|
return false;
|
|
}
|
|
|
|
$post_tags = get_the_terms( $post->ID, 'post_tag' );
|
|
if ( empty( $post_tags ) || is_wp_error( $post_tags ) ) {
|
|
return false;
|
|
}
|
|
|
|
$excluded_tag_slugs = $this->get_excluded_tag_slugs();
|
|
if ( empty( $excluded_tag_slugs ) ) {
|
|
return false;
|
|
}
|
|
|
|
$post_tag_slugs = wp_list_pluck( $post_tags, 'slug' );
|
|
|
|
return count( array_intersect( $post_tag_slugs, $excluded_tag_slugs ) ) > 0;
|
|
}
|
|
|
|
/**
|
|
* Returns the array of allowed post types.
|
|
*
|
|
* @return mixed
|
|
*/
|
|
protected function get_allowed_post_types() {
|
|
if ( isset( $this->options['allowed_post_types'] ) && is_array( $this->options['allowed_post_types'] ) ) {
|
|
$selected_post_types = $this->options['allowed_post_types'];
|
|
|
|
return $selected_post_types;
|
|
} else {
|
|
// Return an empty array, otherwise if all post types have been deselectd on the options page
|
|
// functions using this function will be trying to access the key of `null`.
|
|
return array();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the array of excluded tags.
|
|
*
|
|
* @return mixed
|
|
*/
|
|
protected function get_excluded_tag_slugs() {
|
|
if ( isset( $this->options['exclude_tags'] ) && is_array( $this->options['exclude_tags'] ) ) {
|
|
$excluded_tag_slugs = $this->options['exclude_tags'];
|
|
|
|
return $excluded_tag_slugs;
|
|
} else {
|
|
return array();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Strip html tags from titles before passing them to Discourse.
|
|
*
|
|
* @param string $title The title of the post.
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function sanitize_title( $title ) {
|
|
return wp_strip_all_tags( $title );
|
|
}
|
|
|
|
/**
|
|
* Checks if a given topic_id already exists in the wpdc_topic_blog table.
|
|
*
|
|
* Only used for multisite installations.
|
|
*
|
|
* @param int $topic_id The topic_id to search for in the database.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function topic_blog_id_exists( $topic_id ) {
|
|
global $wpdb;
|
|
// phpcs:disable WordPress.DB.DirectDatabaseQuery
|
|
$row = $wpdb->get_row(
|
|
$wpdb->prepare(
|
|
"SELECT * FROM {$wpdb->base_prefix}wpdc_topic_blog WHERE topic_id = %d",
|
|
$topic_id
|
|
)
|
|
);
|
|
// phpcs:enable WordPress.DB.DirectDatabaseQuery
|
|
|
|
return $row ? true : false;
|
|
}
|
|
|
|
/**
|
|
* Gets post metadata via wp method, or directly from db, depending on the direct-db-publication-flags option.
|
|
*
|
|
* @param int $post_id Post ID.
|
|
* @param string $key The meta key to retrieve.
|
|
* @param bool $single (optional) Whether to return a single value.
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function dc_get_post_meta( $post_id, $key, $single = false ) {
|
|
if ( empty( $this->options['direct-db-publication-flags'] ) ) {
|
|
return get_post_meta( $post_id, $key, $single );
|
|
}
|
|
|
|
global $wpdb;
|
|
$limit = $single ? 'LIMIT 1' : '';
|
|
// phpcs:disable WordPress.DB.DirectDatabaseQuery
|
|
$value = $wpdb->get_var(
|
|
$wpdb->prepare(
|
|
"SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = %s %1s;", // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnquotedComplexPlaceholder
|
|
$post_id,
|
|
$key,
|
|
$limit
|
|
)
|
|
);
|
|
// phpcs:enable WordPress.DB.DirectDatabaseQuery
|
|
|
|
return $value;
|
|
}
|
|
|
|
/**
|
|
* Adds post metadata via wp method, or directly to db, depending on the direct-db-publication-flags option.
|
|
*
|
|
* @param int $post_id Post ID.
|
|
* @param string $key The meta key.
|
|
* @param string $value The meta value.
|
|
* @param bool $unique (optional) Whether the same key should not be added.
|
|
*
|
|
* @return bool
|
|
*/
|
|
protected function dc_add_post_meta( $post_id, $key, $value, $unique = false ) {
|
|
if ( empty( $this->options['direct-db-publication-flags'] ) ) {
|
|
return add_post_meta( $post_id, $key, $value, $unique );
|
|
}
|
|
|
|
global $wpdb;
|
|
|
|
if ( $unique && ! is_null( $this->dc_get_post_meta( $post_id, $key ) ) ) {
|
|
return false;
|
|
}
|
|
|
|
// phpcs:disable WordPress.DB.DirectDatabaseQuery
|
|
$result = $wpdb->insert(
|
|
$wpdb->postmeta,
|
|
array(
|
|
'post_id' => $post_id,
|
|
'meta_key' => $key, // phpcs:ignore WordPress.DB.SlowDBQuery
|
|
'meta_value' => $value, // phpcs:ignore WordPress.DB.SlowDBQuery
|
|
),
|
|
array(
|
|
'%d',
|
|
'%s',
|
|
'%s',
|
|
)
|
|
);
|
|
// phpcs:disable WordPress.DB.DirectDatabaseQuery
|
|
|
|
return $result ? true : false;
|
|
}
|
|
|
|
/**
|
|
* Runs after publication successfully completes
|
|
*
|
|
* @param int $post_id Post ID.
|
|
* @param string $remote_post_type The remote post type.
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function after_publish( $post_id, $remote_post_type ) {
|
|
if ( ! empty( $this->options['verbose-publication-logs'] ) ) {
|
|
$log_args = array(
|
|
'post_id' => $post_id,
|
|
'remote_post_type' => $remote_post_type,
|
|
'discourse_post_id' => get_post_meta( $post_id, 'discourse_post_id', true ),
|
|
'discourse_topic_id' => get_post_meta( $post_id, 'discourse_topic_id', true ),
|
|
'discourse_permalink' => get_post_meta( $post_id, 'discourse_permalink', true ),
|
|
);
|
|
$this->logger->info( "$remote_post_type.after_publish", $log_args );
|
|
}
|
|
|
|
do_action( 'wp_discourse_after_publish', $post_id, $remote_post_type );
|
|
}
|
|
}
|