base = $base; // Actions. add_action( 'wp_loaded', array( $this, 'register_publish_hooks' ), 1 ); add_filter( $this->base->plugin->name, array( $this, 'publish' ), 1, 2 ); } /** * Registers publish hooks against all public Post Types, * * @since 3.0.0 */ public function register_publish_hooks() { add_action( 'transition_post_status', array( $this, 'transition_post_status' ), 10, 3 ); } /** * Fired when a Post's status transitions. Called by WordPress when wp_insert_post() is called, * and wp_insert_post() is called by WordPress and the REST API whenever creating or updating a Post. * * @since 3.1.6 * * @param string $new_status New Status. * @param string $old_status Old Status. * @param \WP_Post $post Post. */ public function transition_post_status( $new_status, $old_status, $post ) { // Bail if the Post Type isn't public. // This prevents the rest of this routine running on e.g. ACF Free, when saving Fields (which results in Field loss). $post_types = array_keys( $this->base->get_class( 'common' )->get_post_types() ); if ( ! in_array( $post->post_type, $post_types, true ) ) { return; } // New Post Screen loading. // Draft saved. if ( $new_status === 'auto-draft' || $new_status === 'draft' || $new_status === 'inherit' || $new_status === 'trash' ) { return; } // Remove actions registered by this Plugin. // This ensures that when Page Builders call publish or update events via AJAX, we don't run this multiple times. remove_action( 'wp_insert_post', array( $this, 'wp_insert_post_publish' ), 999 ); remove_action( 'rest_after_insert_' . $post->post_type, array( $this, 'rest_api_post_publish' ), 10 ); remove_action( 'wp_insert_post', array( $this, 'wp_insert_post_update' ), 999 ); remove_action( 'rest_after_insert_' . $post->post_type, array( $this, 'rest_api_post_update' ), 10 ); /** * = REST API = * If this is a REST API Request, we can't use the wp_insert_post action, because the metadata * is *not* included in the call to wp_insert_post(). Instead, we must use a late REST API action * that gives the REST API time to save metadata. * Note that the meta being supplied in the REST API Request must be registered with WordPress using * register_meta() * * = Gutenberg = * If Gutenberg is being used on the given Post Type, two requests are sent: * - a REST API request, comprising of Post Data and Metadata registered in Gutenberg, * - a standard request, comprising of Post Metadata registered outside of Gutenberg (i.e. add_meta_box() data) * The second request will be seen by transition_post_status() as an update. * Therefore, we set a meta flag on the first Gutenberg REST API request to defer publishing the status until * the second, standard request - at which point, all Post metadata will be available to the Plugin. * * = Classic Editor = * Metadata is included in the call to wp_insert_post(), meaning that it's saved to the Post before we use it. */ $this->base->get_class( 'log' )->add_to_debug_log( 'Post ID: #' . $post->ID ); // If transitioning from future to publish, this is a scheduled Post being published by WordPress Cron. // We don't need to know whether it's a Gutenberg, Classic Editor or REST API request. if ( $old_status === 'future' && $new_status === 'publish' ) { $this->base->get_class( 'log' )->add_to_debug_log( 'Scheduled Post being published by WordPress' ); add_action( 'wp_insert_post', array( $this, 'wp_insert_post_publish' ), 999 ); // Don't need to do anything else, so exit. return; } // Flag to determine if the current Post is a Gutenberg Post or Rest API Request. $is_gutenberg_request = $this->is_gutenberg_request(); $is_rest_api_request = $this->is_rest_api_request(); $this->base->get_class( 'log' )->add_to_debug_log( 'Gutenberg Post: ' . ( $is_gutenberg_request ? 'Yes' : 'No' ) ); $this->base->get_class( 'log' )->add_to_debug_log( 'REST API Request: ' . ( $is_rest_api_request ? 'Yes' : 'No' ) ); // If a previous request flagged that an 'update' request should be treated as a publish request (i.e. // we're using Gutenberg and request to post.php was made after the REST API), do this now. $needs_publishing = get_post_meta( $post->ID, $this->base->plugin->filter_name . '_needs_publishing', true ); if ( $needs_publishing ) { $this->base->get_class( 'log' )->add_to_debug_log( 'Gutenberg: Needs Publishing' ); // Run Publish Status Action now. delete_post_meta( $post->ID, $this->base->plugin->filter_name . '_needs_publishing' ); add_action( 'wp_insert_post', array( $this, 'wp_insert_post_publish' ), 999 ); // Don't need to do anything else, so exit. return; } // If a previous request flagged that an update request be deferred (i.e. // we're using Gutenberg and request to post.php was made after the REST API), do this now. $needs_updating = get_post_meta( $post->ID, $this->base->plugin->filter_name . '_needs_updating', true ); if ( $needs_updating ) { $this->base->get_class( 'log' )->add_to_debug_log( 'Gutenberg: Needs Updating' ); // Run Publish Status Action now. delete_post_meta( $post->ID, $this->base->plugin->filter_name . '_needs_updating' ); add_action( 'wp_insert_post', array( $this, 'wp_insert_post_update' ), 999 ); // Don't need to do anything else, so exit. return; } // Publish. if ( $new_status === 'publish' && $new_status !== $old_status ) { /** * Gutenberg Editor REST API Request * - Non-Gutenberg metaboxes are POSTed via a second, separate request to post.php, which appears * as an 'update'. Define a meta key that we'll check on the separate request later. */ if ( $is_gutenberg_request ) { $this->base->get_class( 'log' )->add_to_debug_log( 'Gutenberg: Defer Publish' ); update_post_meta( $post->ID, $this->base->plugin->filter_name . '_needs_publishing', 1 ); // Don't need to do anything else, so exit. return; } /** * REST API */ if ( $is_rest_api_request ) { $this->base->get_class( 'log' )->add_to_debug_log( 'REST API: Publish' ); add_action( 'rest_after_insert_' . $post->post_type, array( $this, 'rest_api_post_publish' ), 10, 1 ); // Don't need to do anything else, so exit. return; } /** * Classic Editor */ $this->base->get_class( 'log' )->add_to_debug_log( 'Classic Editor: Publish' ); add_action( 'wp_insert_post', array( $this, 'wp_insert_post_publish' ), 999 ); // Don't need to do anything else, so exit. return; } // Update. if ( $old_status === 'publish' ) { /** * Gutenberg Editor REST API Request * - Non-Gutenberg metaboxes are POSTed via a second, separate request to post.php, which appears * as an 'update'. Define a meta key that we'll check on the separate request later. */ if ( $is_gutenberg_request ) { $this->base->get_class( 'log' )->add_to_debug_log( 'Gutenberg: Defer Update' ); update_post_meta( $post->ID, $this->base->plugin->filter_name . '_needs_updating', 1 ); // Don't need to do anything else, so exit. return; } /** * REST API */ if ( $is_rest_api_request ) { $this->base->get_class( 'log' )->add_to_debug_log( 'REST API: Update' ); add_action( 'rest_after_insert_' . $post->post_type, array( $this, 'rest_api_post_update' ), 10, 1 ); // Don't need to do anything else, so exit. return; } /** * Classic Editor */ $this->base->get_class( 'log' )->add_to_debug_log( 'Classic Editor: Update' ); add_action( 'wp_insert_post', array( $this, 'wp_insert_post_update' ), 999 ); // Don't need to do anything else, so exit. return; } } /** * Helper function to determine if the request is a Gutenberg REST API request. * * @since 3.9.1 * * @return bool Is Gutenberg REST API Request */ private function is_gutenberg_request() { if ( ! defined( 'REST_REQUEST' ) ) { return false; } if ( ! REST_REQUEST ) { return false; } // Gutenberg requests are REST API requests, but include a _locale key. // 'True' REST API requests do not include this key. if ( ! filter_has_var( INPUT_POST, '_locale' ) && ! filter_has_var( INPUT_GET, '_locale' ) ) { return false; } return true; } /** * Helper function to determine if the request is a REST API request. * * @since 3.9.1 * * @return bool Is REST API Request */ private function is_rest_api_request() { if ( ! defined( 'REST_REQUEST' ) ) { return false; } if ( ! REST_REQUEST ) { return false; } // Gutenberg requests are REST API requests, but include a _locale key. // 'True' REST API requests do not include this key. if ( filter_has_var( INPUT_POST, '_locale' ) || filter_has_var( INPUT_GET, '_locale' ) ) { return false; } return true; } /** * Helper function to determine if the Post contains Gutenberg Content. * * @since 3.9.1 * * @param \WP_Post $post Post. * @return bool Post Content contains Gutenberg Block Markup */ private function is_gutenberg_post_content( $post ) { if ( strpos( $post->post_content, ' tag. * @return string Content */ private function get_content( $post, $to_more_tag = false ) { // Fetch content. // get_the_content() only works for WordPress 5.2+, which added the $post param. if ( $to_more_tag ) { $extended = get_extended( $post->post_content ); if ( ! empty( $extended['main'] ) ) { $content = $extended['main']; } else { // Fallback to the Post Content. $content = $post->post_content; } } else { $content = $post->post_content; } // Strip shortcodes. $content = strip_shortcodes( $content ); // Remove the wpautop filter, as this converts double newlines into
tags. // In turn,
tags are correctly discarded later on in this function, as social networks don't support HTML.
// However, this results in separation between paragraphs going from two newlines to one newline.
// Some social media services further drop a single newline, meaning paragraphs become one long block of text, which isn't
// intended.
remove_filter( 'the_content', 'wpautop' );
// Remove some third party Plugin filters that wrongly output content that we don't want in a status.
remove_filter( 'the_content', 'powerpress_content' );
// Apply filters to get true output.
$content = apply_filters( 'the_content', $content );
// Restore wpautop that we just removed.
add_filter( 'the_content', 'wpautop' );
// If the content originates from Gutenberg, remove double newlines and convert breaklines
// into newlines.
$is_gutenberg_request_content = $this->is_gutenberg_post_content( $post );
if ( $is_gutenberg_request_content ) {
// Remove double newlines, which may occur due to using Gutenberg blocks.
// (blocks are separated with HTML comments, stripped using apply_filters( 'the_content' ), which results in double, or even triple, breaklines).
$content = preg_replace( '/(?:(?:\r\n|\r|\n)\s*){2}/s', "\n\n", $content );
// Convert
and
into newlines.
$content = preg_replace( '/
/i', "\n", $content );
}
// Convert to plain text.
$content = $this->convert_to_plain_text( $content );
/**
* Filters the dynamic {content} replacement, when a Post's status is being built.
*
* @since 3.7.3
*
* @param string $content Post Content.
* @param \WP_Post $post WordPress Post.
* @param bool $is_gutenberg_request_content Is Gutenberg Post Content.
*/
$content = apply_filters( $this->base->plugin->filter_name . '_publish_get_content', $content, $post, $is_gutenberg_request_content );
// Return.
return $content;
}
/**
* Returns the date in the locale specified in WordPress.
*
* @since 4.7.7
*
* @param \WP_Post $post WordPress Post.
* @return string Date
*/
private function get_date( $post ) {
$date = date_i18n( get_option( 'date_format' ), strtotime( $post->post_date ) );
/*
* Filters the dynamic {date} replacement, when a Post's status is being built.
*
* @since 4.7.7
*
* @param string $date Date.
* @param \WP_Post $post WordPress Post.
*/
$date = apply_filters( $this->base->plugin->filter_name . '_publish_get_date', $date, $post );
// Return.
return $date;
}
/**
* Returns the Permalink, including or excluding a trailing slash, depending on the Plugin settings.
*
* @since 4.0.6
*
* @param \WP_Post $post WordPress Post.
* @return string WordPress Post Permalink
*/
private function get_permalink( $post ) {
$force_trailing_forwardslash = $this->base->get_class( 'settings' )->get_option( 'force_trailing_forwardslash', false );
// Define the URL, depending on whether it should end with a forwardslash or not.
// This is by design; more users complain that they get 301 redirects from site.com/post/ to site.com/post
// than from site.com/post to site.com/post/.
// We can't control misconfigured WordPress installs, so this option gives them the choice.
if ( $force_trailing_forwardslash ) {
$url = get_permalink( $post->ID );
// If the Permalink doesn't have a forwardslash at the end of it, add it now.
if ( substr( $url, -1 ) !== '/' ) {
$url .= '/';
}
} else {
$url = rtrim( get_permalink( $post->ID ), '/' );
}
/**
* Filters the Post's Permalink, including or excluding a trailing slash, depending on the Plugin settings
*
* @since 4.0.6
*
* @param string $url WordPress Post Permalink.
* @param \WP_Post $post WordPress Post.
* @param bool $force_trailing_forwardslash Force Trailing Forwardslash.
*/
$url = apply_filters( $this->base->plugin->filter_name . '_publish_get_permalink', $url, $post, $force_trailing_forwardslash );
// Return.
return $url;
}
/**
* Returns the Short Permalink
*
* @since 4.2.7
*
* @param \WP_Post $post WordPress Post.
* @return string WordPress Post Permalink
*/
private function get_short_permalink( $post ) {
// Define short permalink e.g http://yoursite.com/?p=1.
$url = rtrim( get_bloginfo( 'url' ), '/' ) . '/?p=' . $post->ID;
/**
* Filters the Post's Permalink, including or excluding a trailing slash, depending on the Plugin settings
*
* @since 4.2.7
*
* @param string $url WordPress Post Permalink.
* @param \WP_Post $post WordPress Post.
*/
$url = apply_filters( $this->base->plugin->filter_name . '_publish_get_short_permalink', $url, $post );
// Return.
return $url;
}
/**
* Converts the given string (which is typically HTML from a WordPress Post or Post Meta Field)
* to plain text, by performing several functions:
* - stripping shortcodes (if shortcodes need processing, do so before calling this function)
* - removing all inline style elements and their contents,
* - stripping HTML tags, excluding
,
, ,
and
to newlines
* - removing double spaces
* - trimming the final result of any leading or trailing spaces
*
* @since 4.6.9
*
* @param string $text Text.
* @param bool $convert_links_to_inline true: Convert e.g. `text` to `text (http://foo.com)`.
* false: Convert e.g. `text` to `text`.
* @param bool $strip_urls Whether to strip URLs from the text.
* @return string Text
*/
private function convert_to_plain_text( $text, $convert_links_to_inline = true, $strip_urls = false ) {
// Strip any shortcodes still remaining.
// If shortcodes need to be processed, they should be processed before calling this function.
$text = strip_shortcodes( $text );
// Wrap content in , and tags with an UTF-8 Content-Type meta tag.
// Forcibly tell DOMDocument that this HTML uses the UTF-8 charset.
// isn't enough, as DOMDocument still interprets the HTML as ISO-8859, which breaks character encoding
// Use of mb_convert_encoding() with HTML-ENTITIES is deprecated in PHP 8.2, so we have to use this method.
// If we don't, special characters render incorrectly.
$text = '' . $text . '';
// Load the HTML into a DOMDocument.
libxml_use_internal_errors( true );
$html = new \DOMDocument();
$html->loadHTML( $text );
// Load DOMDocument into XPath.
$xpath = new \DOMXPath( $html );
// Remove inline style tags and their contents.
foreach ( $xpath->query( '//style' ) as $node ) {
$node->parentNode->removeChild( $node ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
}
// Fetch revised HTML.
$text = $html->saveHTML();
// Remove HTML, except breaklines, links and unordered list items.
$retain_tags = ( $strip_urls ? '
and
into newlines.
$text = preg_replace( '/
/i', "\n", $text );
// Convert to text and inline link.
if ( $convert_links_to_inline ) {
// Extract the text from the link, and add the link in brackets after the text.
$text = preg_replace( '/]+href=\"(.*?)\"[^>]*>(.*?)<\/a>/i', '$2 ($1)', $text );
} else {
// Just extract the text from the link and output it.
$text = preg_replace( '/]+href=\"(.*?)\"[^>]*>(.*?)<\/a>/i', '$2', $text );
}
// If URLs are to be stripped, remove them.
if ( $strip_urls ) {
$text = preg_replace( '/https?:\/\/[^\s]+/', '', $text );
}
// Convert