<?php
/**
* XPoster
*
* @package XPoster
* @author Joe Dolson
* @copyright 2008-2025 Joe Dolson
* @license GPL-2.0+
*
* @wordpress-plugin
* Plugin Name: XPoster - Share to Bluesky and Mastodon
* Plugin URI: https://www.joedolson.com/wp-to-twitter/
* Description: Posts a status update when you update your WordPress blog or post a link, using your URL shortener. Many options to customize and promote your statuses.
* Author: Joe Dolson
* Author URI: https://www.joedolson.com
* Text Domain: wp-to-twitter
* License: GPL-2.0+
* License URI: http://www.gnu.org/license/gpl-2.0.txt
* Version: 5.0.0-beta1
*/
/*
Copyright 2008-2025 Joe Dolson (email : joe@joedolson.com)
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
use WpToTwitter_Vendor\GuzzleHttp\Exception\RequestException;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
$wpt_debug = get_option( 'wpt_debug_tweets', false );
define( 'WPT_DEBUG', $wpt_debug );
define( 'WPT_DEBUG_BY_EMAIL', false ); // Email debugging no longer default as of 3.3.0.
define( 'WPT_DEBUG_ADDRESS', get_option( 'admin_email' ) );
define( 'WPT_FROM', 'From: \"' . get_option( 'blogname' ) . '\" <' . get_option( 'admin_email' ) . '>' );
define( 'WPT_STAGING_MODE', true );
// If current environment tests as staging, enable staging mode.
if ( function_exists( 'wp_get_environment_type' ) ) {
if ( 'staging' === wp_get_environment_type() && ! defined( 'WPT_STAGING_MODE' ) ) {
define( 'WPT_STAGING_MODE', true );
}
}
require_once plugin_dir_path( __FILE__ ) . 'vendor_prefixed/vendor/scoper-autoload.php';
require_once plugin_dir_path( __FILE__ ) . 'includes/metabox.php';
require_once plugin_dir_path( __FILE__ ) . 'includes/deprecated.php';
require_once plugin_dir_path( __FILE__ ) . 'includes/media.php';
require_once plugin_dir_path( __FILE__ ) . 'includes/post-info.php';
require_once plugin_dir_path( __FILE__ ) . 'includes/ajax.php';
require_once plugin_dir_path( __FILE__ ) . 'wpt-functions.php';
// Service post handlers.
require_once plugin_dir_path( __FILE__ ) . 'services/x/post.php';
require_once plugin_dir_path( __FILE__ ) . 'services/mastodon/post.php';
require_once plugin_dir_path( __FILE__ ) . 'services/bluesky/post.php';
// URL Shortening.
require_once plugin_dir_path( __FILE__ ) . 'wp-to-twitter-shorteners.php';
// Service settings.
require_once plugin_dir_path( __FILE__ ) . 'services/x/settings.php';
require_once plugin_dir_path( __FILE__ ) . 'services/mastodon/settings.php';
require_once plugin_dir_path( __FILE__ ) . 'services/bluesky/settings.php';
require_once plugin_dir_path( __FILE__ ) . 'wp-to-twitter-manager.php';
// Template generation.
require_once plugin_dir_path( __FILE__ ) . 'wpt-truncate.php';
require_once plugin_dir_path( __FILE__ ) . 'wpt-rate-limiting.php';
define( 'XPOSTER_VERSION', '5.0.0-beta1' );
register_activation_hook( __FILE__, 'wpt_check_version' );
/**
* Check whether version requires activation.
*/
function wpt_check_version() {
$prev_version = ( '' !== get_option( 'wp_to_twitter_version', '' ) ) ? get_option( 'wp_to_twitter_version' ) : '1.0.0';
if ( version_compare( $prev_version, XPOSTER_VERSION, '<' ) ) {
xposter_activate();
}
}
register_deactivation_hook( __FILE__, 'wpt_deactivate' );
/**
* Deactivate Plugin.
*/
function wpt_deactivate() {
// Remove rate limits cron if enabled.
wp_clear_scheduled_hook( 'wptratelimits' );
}
/**
* Pro version check.
*/
function xposter_pro_check() {
global $wptp_version;
$plugin_data = get_plugin_data( __FILE__, false );
$upgrade_now = ' <a href="https://xposterpro.com/awesome/xposter-pro/">' . esc_html__( 'Upgrade Now', 'wp-to-twitter' ) . '</a>';
if ( $wptp_version && version_compare( $wptp_version, '3.0.0', '<=' ) ) {
$message = sprintf(
// Translators: Plugin name, plugin version unsupported.
__( '%1$s is not compatible with XPoster Pro %2$s or lower; XPoster Pro has been deactivated.', 'wp-to-twitter' ) . $upgrade_now,
'<strong>' . $plugin_data['Name'] . '</strong>',
'3.0.0'
);
wp_admin_notice(
$message,
array(
'type' => 'error',
)
);
if ( '3.0.0' === $wptp_version ) {
deactivate_plugins( 'wp-tweets-pro/wpt-pro-functions.php' );
}
}
if ( $wptp_version && version_compare( $wptp_version, '3.4.0', '<=' ) ) {
$message = sprintf(
// Translators: Plugin name, plugin version unsupported.
__( '%1$s has limited compatibility with XPoster Pro %2$s or lower. Some features may not be available.', 'wp-to-twitter' ) . $upgrade_now,
'<strong>' . $plugin_data['Name'] . '</strong>',
'3.3.2'
);
wp_admin_notice(
$message,
array(
'type' => 'error',
)
);
}
}
add_action( 'admin_notices', 'xposter_pro_check' );
/**
* Activate XPoster.
*/
function xposter_activate() {
// If this has never run before, do the initial setup.
$new_install = '1' === get_option( 'wpt_twitter_setup' ) ? false : true;
if ( $new_install ) {
$initial_settings = array(
'post' => array(
'post-published-update' => '1',
'post-published-text' => 'New post: #title# #url#',
'post-edited-update' => '0',
'post-edited-text' => 'Post Edited: #title# #url#',
),
'page' => array(
'post-published-update' => '0',
'post-published-text' => 'New page: #title# #url#',
'post-edited-update' => '0',
'post-edited-text' => 'Page edited: #title# #url#',
),
);
update_option( 'wpt_post_types', $initial_settings );
update_option( 'jd_twit_blogroll', '1' );
update_option( 'newlink-published-text', 'New link: #title# #url#' );
update_option( 'jd_shortener', '3' );
update_option( 'jd_strip_nonan', '0' );
update_option( 'jd_max_tags', 4 );
update_option( 'jd_max_characters', 20 );
$administrator = get_role( 'administrator' );
if ( is_object( $administrator ) ) {
// wpt_twitter_oauth is the general permission for editing user accounts.
$administrator->add_cap( 'wpt_twitter_oauth' );
$administrator->add_cap( 'wpt_twitter_custom' );
$administrator->add_cap( 'wpt_twitter_switch' );
$administrator->add_cap( 'wpt_can_tweet' );
$administrator->add_cap( 'wpt_tweet_now' );
}
$editor = get_role( 'editor' );
if ( is_object( $editor ) ) {
$editor->add_cap( 'wpt_can_tweet' );
}
$author = get_role( 'author' );
if ( is_object( $author ) ) {
$author->add_cap( 'wpt_can_tweet' );
}
$contributor = get_role( 'contributor' );
if ( is_object( $contributor ) ) {
$contributor->add_cap( 'wpt_can_tweet' );
}
update_option( 'jd_post_excerpt', 90 );
// Use Google Analytics.
update_option( 'twitter-analytics-campaign', 'twitter' );
update_option( 'use-twitter-analytics', '0' );
update_option( 'jd_dynamic_analytics', '0' );
update_option( 'no-analytics', 1 );
update_option( 'use_dynamic_analytics', 'category' );
// Use custom external URLs to point elsewhere.
update_option( 'jd_twit_custom_url', 'external_link' );
// Error checking.
update_option( 'wp_url_failure', '0' );
// Default publishing options.
update_option( 'jd_tweet_default', '0' );
update_option( 'jd_tweet_default_edit', '0' );
update_option( 'wpt_inline_edits', '0' );
// Note that default options are set.
update_option( 'wpt_twitter_setup', '1' );
update_option( 'jd_keyword_format', '0' );
}
$prev_version = get_option( 'wp_to_twitter_version' );
$upgrade = version_compare( $prev_version, '3.4.4', '<' );
if ( $upgrade ) {
delete_option( 'bitlyapi' );
delete_option( 'bitlylogin' );
}
update_option( 'wp_to_twitter_version', XPOSTER_VERSION );
}
/**
* Save error messages for status updates.
*
* @param int $id Post ID.
* @param int $auth Current author.
* @param string $twit Update text.
* @param string $error Error string from service.
* @param int $http_code Http code from service.
* @param string $ts Current timestamp.
*/
function wpt_save_error( $id, $auth, $twit, $error, $http_code, $ts ) {
$http_code = (int) $http_code;
if ( 200 !== $http_code ) {
add_post_meta(
$id,
'_wpt_failed',
array(
'author' => $auth,
'sentence' => $twit,
'error' => $error,
'code' => $http_code,
'timestamp' => $ts,
)
);
} else {
if ( '1' === get_option( 'wpt_rate_limiting' ) ) {
wpt_log_success( $auth, $ts, $id );
}
}
}
/**
* Save a record of a successful status update.
*
* @param int $id Post ID.
* @param string $twit Status update text.
* @param int $http_code HTTP Code returned by query.
*/
function wpt_save_success( $id, $twit, $http_code ) {
if ( 200 === $http_code ) {
$jwt = get_post_meta( $id, '_jd_wp_twitter', true );
if ( ! is_array( $jwt ) ) {
$jwt = array();
}
$jwt[] = urldecode( $twit );
update_post_meta( $id, '_jd_wp_twitter', $jwt );
}
}
/**
* Checks whether XPoster has sent a tweet on this post to this author within the last 30 seconds and blocks duplicates.
*
* @param int $id Post ID.
* @param int $auth Author.
*
* @uses filter wpt_recent_tweet_threshold
* @return boolean true to send status update, false to block.
*/
function wpt_check_recent_tweet( $id, $auth ) {
if ( ! $id ) {
return false;
} else {
if ( false === $auth ) {
$transient = get_transient( "_wpt_most_recent_tweet_$id" );
} else {
$transient = get_transient( '_wpt_' . $auth . "_most_recent_tweet_$id" );
}
if ( $transient ) {
return true;
} else {
/**
* Modify the expiration window for recent status updates.
* This value does flood control, to prevent a runaway process from sending multiple status updates. Default `30` seconds.
*
* @hook wpt_recent_tweet_threshold
* @param {int} $expire Integer representing seconds. How long the transient will exist.
*
* @return {int}
*/
$expire = apply_filters( 'wpt_recent_tweet_threshold', 30 );
// if expiration is 0, don't set the transient. We don't want permanent transients.
if ( 0 !== $expire ) {
if ( false === $auth ) {
set_transient( "_wpt_most_recent_tweet_$id", true, $expire );
} else {
set_transient( '_wpt_' . $auth . "_most_recent_tweet_$id", true, $expire );
}
}
}
}
return false;
}
/**
* Check whether the current post should be sent to a given service.
*
* @param int $post_ID Post ID.
* @param string $service Service ID.
*
* @return bool
*/
function wpt_service_enabled( $post_ID = false, $service = 'bluesky' ) {
$omit = ( $post_ID ) ? get_post_meta( $post_ID, '_wpt_omit_services', true ) : array();
$omit = ( $omit && is_array( $omit ) ) ? $omit : array();
$disabled = get_option( 'wpt_disabled_services', array() );
$send_to = true;
if ( in_array( $service, $omit, true ) || in_array( $service, array_keys( $disabled ), true ) ) {
if ( $post_ID ) {
wpt_mail( ucfirst( $service ) . ' is disabled.', wpt_format_error( $disabled ), $post_ID );
}
$send_to = false;
}
/**
* Filter whether a given post should be sent to a specific service.
*
* @hook wpt_service_enabled
*
* @param {bool} $send_to True to send to a service.
* @param {int} $post_ID Post ID. False if checking globally.
* @param {string} $service Service ID.
*
* @return {bool}
*/
return apply_filters( 'wpt_service_enabled', $send_to, $post_ID, $service );
}
/**
* Performs the API post to target services. Alias for wpt_post_to_twitter.
*
* @param string $template Template for status update to be sent to service.
* @param int $auth Author ID.
* @param int $id Post ID.
* @param null|boolean $media Whether to upload media attached to the post specified in $id. Default null, using default post settings.
*
* @return boolean|array False if blocked, array of statuses if attempted.
*/
function wpt_post_to_service( $template, $auth = false, $id = false, $media = null ) {
$return = wpt_post_to_twitter( $template, $auth, $id, $media );
if ( $return && is_array( $return ) ) {
$info = array_pop( $return );
$status = isset( $info['status'] ) ? $info['status'] : '';
$notice = isset( $info['notice'] ) ? $info['notice'] : '';
$http_code = isset( $info['http'] ) ? $info['http'] : '';
wpt_save_error( $id, $auth, $status, $notice, $http_code, time() );
wpt_save_success( $id, $status, $http_code );
}
return $return;
}
/**
* Performs the API post to target services.
*
* @param string $template Template for status update to be sent to service.
* @param int $auth Author ID.
* @param int $id Post ID.
* @param null|boolean $media Whether to upload media attached to the post specified in $id. Default null, using default post settings.
*
* @return boolean|array False if blocked, array of statuses if attempted.
*/
function wpt_post_to_twitter( $template, $auth = false, $id = false, $media = null ) {
// If an ID is set but the post is not currently present or published, ignore.
$return = array();
if ( $id ) {
$status = get_post_status( $id );
if ( ! $status || 'publish' !== $status ) {
$error = __( 'This post is no longer published or has been deleted', 'wp-to-twitter' );
wpt_save_error( $id, $auth, $template, $error, '404', time() );
wpt_set_log( 'wpt_status_message', $id, $error, '404' );
return false;
}
}
$error = false;
if ( '1' === get_option( 'wpt_rate_limiting' ) ) {
// check whether this post needs to be rate limited.
$continue = wpt_test_rate_limit( $id, $auth );
if ( ! $continue ) {
wpt_mail( 'This post was blocked by XPoster rate limiting.', 'Post ID: ' . $id . '; Account: ' . $auth );
wpt_set_log( 'wpt_status_message', $id, __( 'Status update prevented due to XPoster rate limiting.', 'wp-to-twitter' ), '404' );
return false;
}
}
$recent = wpt_check_recent_tweet( $id, $auth );
if ( $recent ) {
// This is a duplicate after less than 30 seconds, which usually means an extra run of the action.
// This error used to be logged, but is now exited silently.
return false;
}
$connections = wpt_check_connections( $auth, true );
$check_twitter = $connections['x'];
$check_mastodon = $connections['mastodon'];
$check_bluesky = $connections['bluesky'];
if ( ! $check_twitter && ! $check_mastodon && ! $check_bluesky ) {
$error = __( 'This account is not authorized to post to any services.', 'wp-to-twitter' );
wpt_save_error( $id, $auth, $template, $error, '401', time() );
wpt_set_log( 'wpt_status_message', $id, $error, '401' );
return false;
} // exit silently if not authorized.
// Check if this update has already been sent to a given service.
$check = wpt_check_service_history( $id, $auth, $template, $connections );
// if has media, must have a valid attachment.
$media = ( null === $media ) ? wpt_post_with_media( $id ) : $media;
$attachment = ( $media ) ? wpt_post_attachment( $id ) : false;
$connection = false;
if ( $check['x'] && $check_twitter && wpt_service_enabled( $id, 'x' ) ) {
$text = $check['x'];
$status = array(
'text' => $text,
);
$connection = $check_twitter;
$status = wpt_upload_twitter_media( $connection, $auth, $attachment, $status, $id );
$response = wpt_send_post_to_twitter( $connection, $auth, $id, $status );
wpt_post_submit_handler( $connection, $response, $id, $auth, $text );
$return['xcom'] = $response;
wpt_mail( 'Share Connection Status: X', "$text, $auth, $id, $media, " . wpt_format_error( $response ), $id );
}
if ( $check['mastodon'] && $check_mastodon && wpt_service_enabled( $id, 'mastodon' ) ) {
$text = $check['mastodon'];
$status = array(
'text' => $text,
);
$connection = $check_mastodon;
$status = wpt_upload_mastodon_media( $connection, $auth, $attachment, $status, $id );
$response = wpt_send_post_to_mastodon( $connection, $auth, $id, $status );
wpt_post_submit_handler( $connection, $response, $id, $auth, $text );
$return['mastodon'] = $response;
wpt_mail( 'Share Connection Status: Mastodon', "$text, $auth, $id, $media, " . wpt_format_error( $response ), $id );
}
if ( $check['bluesky'] && $check_bluesky && wpt_service_enabled( $id, 'bluesky' ) ) {
$text = $check['bluesky'];
$status = array(
'text' => $text,
);
$connection = $check_bluesky;
$request_type = ( wpt_post_with_media( $id ) ) ? 'upload' : 'card';
$attachment = ( $attachment ) ? $attachment : wpt_post_attachment( $id );
$image = wpt_upload_bluesky_media( $connection, $auth, $attachment, $status, $id, $request_type );
$response = wpt_send_post_to_bluesky( $connection, $auth, $id, $status, $image );
wpt_post_submit_handler( $connection, $response, $id, $auth, $text );
$return['bluesky'] = $response;
wpt_mail( 'Share Connection Status: Bluesky', "$text, $auth, $id, $media, " . wpt_format_error( $response ), $id );
}
if ( ! empty( $return ) ) {
return $return;
} else {
wpt_set_log( 'wpt_status_message', $id, __( 'This status update has already been sent.', 'wp-to-twitter' ), '404' );
return false;
}
}
/**
* Get the saved custom template for a status update.
*
* @param int $post_ID Post ID.
* @param string $template Template passed to this post.
* @param string $service Service posting to.
*
* @return string
*/
function wpt_get_custom_template( $post_ID, $template, $service ) {
$custom_template = get_post_meta( $post_ID, '_wpt_post_template_' . $service, true );
if ( $custom_template ) {
$template = $custom_template;
}
return $template;
}
/**
* Check whether a status update has already been sent; expands the passed template to create status update to test.
*
* @param int $post_ID Post ID.
* @param int|bool $auth Author ID or false.
* @param string $template Status template.
* @param array $connections Array of active connection information.
*
* @return array Array of statuses by service or false, if blocked.
*/
function wpt_check_service_history( $post_ID, $auth, $template, $connections ) {
$checks = array();
foreach ( $connections as $service => $connected ) {
if ( ! $connected || ! wpt_service_enabled( $post_ID, $service ) ) {
$checks[ $service ] = false;
continue;
}
$template = wpt_get_custom_template( $post_ID, $template, $service );
$status = wpt_truncate_status( $template, array(), $post_ID, false, $auth, $service );
// Get last sent to this service.
$check = ( ! $auth ) ? get_option( 'wpt_last_' . $service, '' ) : get_user_meta( $auth, 'wpt_last_' . $service, true );
// prevent duplicate status updates. Checks whether this text has already been sent.
if ( $check === $status && '' !== $status ) {
wpt_mail( ucfirst( $service ) . ': Status update identical to previous update', "This Update: $status; Check Update: $check; $auth, $post_ID", $post_ID ); // DEBUG.
$error = __( 'This status update is identical to another update recently sent to this account.', 'wp-to-twitter' ) . ' ' . __( 'All status updates are expected to be unique.', 'wp-to-twitter' );
wpt_save_error( $post_ID, $auth, $status, $error, '403-1', time() );
wpt_set_log( 'wpt_status_message', $post_ID, $error, '403' );
$status = false;
} elseif ( '' === $status || ! $status ) {
wpt_mail( 'Status update check: empty sentence', "$status, $auth, $post_ID", $post_ID ); // DEBUG.
$error = __( 'This status update was blank and could not be sent to the API.', 'wp-to-twitter' );
wpt_save_error( $post_ID, $auth, $status, $error, '403-2', time() );
wpt_set_log( 'wpt_status_message', $post_ID, $error, '403' );
$status = false;
}
$checks[ $service ] = $status;
}
return $checks;
}
/**
* Handle post-sending responses for APIs.
*
* @param object $connection API connection.
* @param array $response Array of response data from API.
* @param int $id Post ID.
* @param int|bool $auth Author context.
* @param string $twit Posted status text.
*
* @return array Array with response info.
*/
function wpt_post_submit_handler( $connection, $response, $id, $auth, $twit ) {
$return = $response['return'];
$http_code = $response['http'];
$notice = $response['notice'];
$service = isset( $response['service'] ) ? $response['service'] : false;
$tweet_id = ( 'x' === $service ) ? $response['status_id'] : false;
$mastodon_id = ( 'mastodon' === $service ) ? $response['status_id'] : false;
$bluesky_id = ( 'bluesky' === $service ) ? $response['status_id'] : false;
wpt_mail( "Status Update Response: $http_code / $service", $notice, $id ); // DEBUG.
// only save last status if successful.
if ( 200 === $http_code ) {
$services = array( 'x', 'mastodon', 'bluesky' );
foreach ( $services as $service ) {
if ( ! $auth ) {
update_option( 'wpt_last_' . $service, $twit );
} else {
update_user_meta( $auth, 'wpt_last_' . $service, $twit );
}
}
}
if ( ! $return ) {
/**
* Executes an action after posting a status fails.
*
* @hook wpt_tweet_failed
*
* @since 3.6.0
*
* @param {object} $connection The current OAuth connection.
* @param {int} $id Post ID for status update.
* @param {string} $error Error message returned.
*/
do_action( 'wpt_tweet_failed', $connection, $id, $notice );
wpt_set_log( 'wpt_status_message', $id, $notice, $http_code );
} else {
/**
* Executes an action after a status is posted successfully.
*
* @hook wpt_tweet_posted
*
* @param {object} $connection The current OAuth connection.
* @param {int} $id Post ID for status update.
*/
do_action( 'wpt_tweet_posted', $connection, $id );
// Log the Status ID of the first status update on this post.
$has_tweet_id = get_post_meta( $id, '_wpt_tweet_id', true );
if ( ! $has_tweet_id && $tweet_id ) {
update_post_meta( $id, '_wpt_tweet_id', $tweet_id );
}
// Log the Status ID of the first Mastodon update on this post.
$has_mastodon_id = get_post_meta( $id, '_wpt_status_id', true );
if ( ! $has_mastodon_id && $mastodon_id ) {
update_post_meta( $id, '_wpt_status_id', $mastodon_id );
}
// Log the Status ID of the first Bluesky update on this post.
$has_bluesky_id = get_post_meta( $id, '_wpt_bluesky_id', true );
if ( ! $has_bluesky_id && $bluesky_id ) {
update_post_meta( $id, '_wpt_bluesky_id', $bluesky_id );
}
wpt_set_log( 'wpt_status_message', $id, $notice );
}
return $response;
}
/**
* Get text error message from HTTP code.
*
* @param int $http_code HTTP returned.
* @param string $notice Any already generated notification message.
* @param int|bool $auth Current authentication context.
*
* @return array
*/
function wpt_get_response_message( $http_code, $notice, $auth ) {
$return = false;
switch ( $http_code ) {
case '000':
$error = '';
break;
case 100:
$error = __( '100 Continue: X received the header of your submission, but your server did not follow through by sending the body of the data.', 'wp-to-twitter' );
break;
case 200:
$return = true;
$error = __( '200 OK: Success!', 'wp-to-twitter' );
update_option( 'wpt_authentication_missing', false );
break;
case 304:
$error = __( '304 Not Modified: There was no new data to return', 'wp-to-twitter' );
break;
case 400:
// Translators: Error description from X.com.
$error = sprintf( __( '400: %s', 'wp-to-twitter' ), $notice );
break;
case 401:
// Translators: Error description from X.com.
$error = sprintf( __( '401: %s', 'wp-to-twitter' ), $notice );
update_option( 'wpt_authentication_missing', "$auth" );
break;
case 403:
// Translators: Error description from X.com.
$error = sprintf( __( '403: %s', 'wp-to-twitter' ), $notice );
break;
case 404:
// Translators: Error description from X.com.
$error = sprintf( __( '404: %s', 'wp-to-twitter' ), $notice );
break;
case 406:
// Translators: Error description from X.com.
$error = sprintf( __( '406: %s', 'wp-to-twitter' ), $notice );
break;
case 422:
// Translators: Error description from X.com.
$error = sprintf( __( '422: %s', 'wp-to-twitter' ), $notice );
break;
case 429:
// Translators: Error description from X.com.
$error = sprintf( __( '429: %s', 'wp-to-twitter' ), $notice );
break;
case 500:
$error = __( '500 Internal Server Error: Something is broken at X.com.', 'wp-to-twitter' );
break;
case 502:
$error = __( '502 Bad Gateway: X.com is down or being upgraded.', 'wp-to-twitter' );
break;
case 503:
$error = __( '503 Service Unavailable: The X.com servers are up, but overloaded with requests - Please try again later.', 'wp-to-twitter' );
break;
case 504:
$error = __( "504 Gateway Timeout: The X.com servers are up, but the request couldn't be serviced due to some failure within our stack. Try again later.", 'wp-to-twitter' );
break;
default:
// Translators: http code.
$error = sprintf( __( '<strong>Code %s</strong>: X.com did not return a recognized response code.', 'wp-to-twitter' ), $http_code );
break;
}
return array(
'error' => $error,
'return' => $return,
);
}
/**
* For servers without PEAR normalize installed, approximates normalization. With normalizer, executes normalization on string.
*
* @param string $text Text to normalize.
*
* @return string Normalized text.
*/
function wpt_normalize( $text ) {
if ( version_compare( PHP_VERSION, '5.0.0', '>=' ) && function_exists( 'normalizer_normalize' ) ) {
if ( normalizer_is_normalized( $text ) ) {
return $text;
}
return normalizer_normalize( $text );
} else {
$normalizer = new WPT_Normalizer();
if ( $normalizer->is_normalized( $text ) ) {
return $text;
}
return $normalizer->normalize( $text );
}
}
/**
* Test URL to see if is pointing to https location.
*
* @param string $url URL to check for https.
*
* @return boolean
*/
function wpt_is_ssl( $url ) {
if ( stripos( $url, 'https' ) ) {
return true;
} else {
return false;
}
}
/**
* This function is no longer in use, but the filter within it is.
*
* @param string $post_type Type of post.
* @param array $post_info Post info.
* @param int $post_ID Post ID.
*
* @return bool False to block this from posting.
*/
function wpt_category_limit( $post_type, $post_info, $post_ID ) {
$continue = true;
$args = array(
'type' => $post_type,
'info' => $post_info,
'id' => $post_ID,
);
return apply_filters( 'wpt_filter_terms', $continue, $args );
}
/**
* Process bulk edited posts if permitted.
*
* @param array $updated Array of updated post IDs.
*/
function wpt_bulk_edit_posts( $updated ) {
if ( '1' === get_option( 'wpt_inline_edits' ) ) {
foreach ( $updated as $post_ID ) {
wpt_post_update( $post_ID, 'instant' );
}
}
}
add_action( 'bulk_edit_posts', 'wpt_bulk_edit_posts' );
/**
* Set up a status update to be sent.
*
* @param int $post_ID Post ID.
* @param string $type Publishing context: instant, future, xmlrpc.
* @param object $post Post object.
* @param boolean $updated True if updated, false if inserted.
* @param object $post_before The post prior to this update, or null for new posts.
*
* @return int $post_ID
*/
function wpt_post_update( $post_ID, $type = 'instant', $post = null, $updated = null, $post_before = null ) {
if ( wp_is_post_autosave( $post_ID ) || wp_is_post_revision( $post_ID ) ) {
return $post_ID;
}
$post_this = get_post_meta( $post_ID, '_wpt_post_this', true );
$newpost = false;
$oldpost = false;
$template = '';
$nptext = '';
if ( '0' === get_option( 'jd_tweet_default' ) ) {
// If post this value is not set or equals 'yes'.
$send_update = ( 'no' !== $post_this ) ? true : false;
$text_default = 'no';
} else {
// If post this is set and is equal to yes.
$send_update = ( 'yes' === $post_this ) ? true : false;
$text_default = 'yes';
}
wpt_mail( '1: Status Update should send: ' . $post_this, "Default: $text_default; Publication method: $type", $post_ID ); // DEBUG.
if ( $send_update ) {
$post_info = wpt_post_info( $post_ID );
$debug_post_info = $post_info;
unset( $debug_post_info['post_content'] );
unset( $debug_post_info['postContent'] );
wpt_mail( '2: XPoster Post Info (post content omitted)', wpt_format_error( $debug_post_info ), $post_ID ); // DEBUG.
/**
* Apply filters against this post to determine whether it should be allowed to be sent.
*
* @hook wpt_should_block_status
*
* @param {bool} $filter Always false by default.
* @param {array} $post_info Array of post data.
*
* @return {bool} True to block post.
*/
$filter = apply_filters( 'wpt_should_block_status', false, $post_info );
if ( true === $filter ) {
wpt_mail( '3: Post blocked by XPoster Pro custom filters', 'No additional data available', $post_ID );
return false;
}
/**
* Return true to block this post based on POST data. Default false.
*
* @hook wpt_filter_post_data
*
* @param {bool} $filter True if this post should not have a status update sent.
* @param {array} $post POST global.
*
* @return {bool}
*/
$filter = apply_filters( 'wpt_filter_post_data', false, $_POST );
if ( $filter ) {
return false;
}
if ( 'future' === $type || 'future' === get_post_meta( $post_ID, 'wpt_publishing', true ) ) {
$new = 1; // if this is a future action, then it should be published regardless of relationship.
wpt_mail( '4a: Post is a scheduled post', 'See Post Info data', $post_ID );
delete_post_meta( $post_ID, 'wpt_publishing' );
} else {
// if the post modified date and the post date are the same, this is new.
// true if first date before or equal to last date.
$new = wpt_post_is_new( $post_info['_postModified'], $post_info['_postDate'] );
}
// post is not previously published but has been backdated.
// (post date is edited, but save option is 'publish').
if ( 0 === $new && ( isset( $_POST['edit_date'] ) && '1' === $_POST['edit_date'] && ! isset( $_POST['save'] ) ) ) {
$new = 1;
}
// can't catch posts that were set to a past date as a draft, then published.
$post_type = $post_info['postType'];
$post_type_settings = get_option( 'wpt_post_types' );
if ( wpt_allowed_post_types( $post_type ) ) {
// identify whether limited by category/taxonomy.
$continue = wpt_category_limit( $post_type, $post_info, $post_ID );
if ( false === $continue ) {
wpt_mail( '4b: XPoster Pro: Limited by term filters', 'This post was limited by a taxonomy/term filter', $post_ID );
return false;
}
// create status update and ID whether current action is edit or new.
$ct = get_post_meta( $post_ID, '_jd_twitter', true );
if ( isset( $_POST['_jd_twitter'] ) && ! empty( $_POST['_jd_twitter'] ) ) {
$ct = sanitize_textarea_field( wp_unslash( $_POST['_jd_twitter'] ) );
}
$custom_tweet = ( '' !== $ct ) ? stripcslashes( trim( $ct ) ) : '';
// if ops is set and equals 'publish', this is being edited. Otherwise, it's a new post.
if ( 0 === $new ) {
// if this is an old post and editing updates are enabled.
if ( '1' === get_option( 'jd_tweet_default_edit' ) ) {
/**
* Filter whether a post defaults to send updates on edit.
*
* @hook wpt_tweet_this_edit
*
* @param {string} $post_this 'yes' or 'no'.
* @param {array} $_POST POST global.
* @param {int} $post_ID Post ID.
*
* @return {string} 'yes' to continue with posting.
*/
$post_this = apply_filters( 'wpt_tweet_this_edit', $post_this, $_POST, $post_ID );
if ( 'yes' !== $post_this ) {
return false;
}
}
wpt_mail( '4b: Post action is edit', 'This event was a post edit action.' . "\n" . 'Modified Date: ' . $post_info['_postModified'] . "\n\n" . 'Publication date:' . $post_info['_postDate'], $post_ID ); // DEBUG.
if ( '1' === (string) $post_type_settings[ $post_type ]['post-edited-update'] || $post_this ) {
$nptext = stripcslashes( $post_type_settings[ $post_type ]['post-edited-text'] );
if ( ! $nptext ) {
wpt_mail( '4b: Edited post template is empty.', 'Post Type: ' . $post_type, $post_ID ); // DEBUG.
}
$oldpost = true;
}
} else {
wpt_mail( '4c: Post action is publish', 'This event was a post publish action.' . "\n" . 'Modified Date: ' . $post_info['_postModified'] . "\n\n" . 'Publication date:' . $post_info['_postDate'], $post_ID ); // DEBUG.
if ( '1' === (string) $post_type_settings[ $post_type ]['post-published-update'] || $post_this ) {
$nptext = stripcslashes( $post_type_settings[ $post_type ]['post-published-text'] );
if ( ! $nptext ) {
wpt_mail( '4c: Published post template is empty.', 'Post Type: ' . $post_type, $post_ID ); // DEBUG.
}
$newpost = true;
}
}
if ( $newpost || $oldpost ) {
$template = ( '' !== $custom_tweet ) ? $custom_tweet : $nptext;
}
if ( '' !== $template ) {
/**
* Execute an action when a status update is executed.
*
* @hook wpt_post_to_service
*
* @param {int} $post_ID Post ID.
* @param {array} $post_info Array of post info for templates.
* @param {string} $template Template in use.
* @param {bool} $media Whether media should be included.
*/
do_action( 'wpt_post_to_service', $post_ID, $post_info, $template );
wpt_post_to_service( $template, false, $post_ID );
}
}
}
return $post_ID;
}
/**
* Send updates on links in link manager. Only active if Link plug-in is installed.
*
* @param integer $link_id Database ID for link.
*
* @return mixed boolean/integer link ID if successful, false if failure.
*/
function wpt_post_update_link( $link_id ) {
$bookmark = get_bookmark( $link_id );
$thislinkprivate = $bookmark->link_visible;
if ( 'N' !== $thislinkprivate ) {
$thislinkname = $bookmark->link_name;
$thispostlink = $bookmark->link_url;
$thislinkdescription = $bookmark->link_description;
$sentence = stripcslashes( get_option( 'newlink-published-text' ) );
$sentence = str_ireplace( '#title#', $thislinkname, $sentence );
$sentence = str_ireplace( '#description#', $thislinkdescription, $sentence );
/**
* Customize the URL shortening of a link in the link manager.
*
* @hook wptt_shorten_link
*
* @param {string} $thispostlink The passed bookmark link.
* @param {string} $thislinkname The provided link title.
* @param {bool} $post_ID False, because links don't have post IDs.
* @param {bool} $test 'link' to indicate a link is being shortened.
*
* @return {string}
*/
$shrink = apply_filters( 'wptt_shorten_link', $thispostlink, $thislinkname, false, 'link' );
if ( false === stripos( $sentence, '#url#' ) ) {
$sentence = $sentence . ' ' . $shrink;
} else {
$sentence = str_ireplace( '#url#', $shrink, $sentence );
}
if ( false === stripos( $sentence, '#longurl#' ) ) {
$sentence = $sentence . ' ' . $thispostlink;
} else {
$sentence = str_ireplace( '#longurl#', $thispostlink, $sentence );
}
if ( '' !== $sentence ) {
wpt_post_to_service( $sentence, false, $link_id );
}
return $link_id;
} else {
return false;
}
}
add_action( 'admin_menu', 'wpt_add_twitter_debug_box' );
/**
* Set up post meta box.
*/
function wpt_add_twitter_debug_box() {
if ( WPT_DEBUG && current_user_can( 'manage_options' ) ) {
// add X.com panel to post types where it's enabled.
$wpt_post_types = wpt_allowed_post_types();
foreach ( $wpt_post_types as $type ) {
add_meta_box( 'wp2t-debug', 'XPoster Debugging', 'wpt_show_debug', $type, 'advanced' );
}
}
}
add_action( 'admin_enqueue_scripts', 'wpt_admin_scripts', 10, 1 );
/**
* Enqueue admin scripts for XPoster and XPoster PRO.
*/
function wpt_admin_scripts() {
global $current_screen;
$wpt_version = XPOSTER_VERSION;
$charcount_url = 'js/charcount.min.js';
$ajax_url = 'js/ajax.min.js';
$base_url = 'js/base.min.js';
$tabs_url = 'js/tabs.min.js';
if ( SCRIPT_DEBUG ) {
$wpt_version .= '-' . wp_rand( 10000, 99999 );
$charcount_url = 'js/charcount.js';
$ajax_url = 'js/ajax.js';
$base_url = 'js/base.js';
$tabs_url = 'js/tabs.js';
}
wp_register_script( 'wpt.charcount', plugins_url( $charcount_url, __FILE__ ), array( 'jquery' ), $wpt_version, true );
if ( 'post' === $current_screen->base || 'xposter-pro_page_wp-to-twitter-schedule' === $current_screen->base ) {
wp_enqueue_script( 'wpt.charcount' );
wp_register_style( 'wpt-post-styles', plugins_url( 'css/post-styles.css', __FILE__ ), array(), $wpt_version );
wp_enqueue_style( 'wpt-post-styles' );
$config = wpt_max_length( false );
// add one; character count starts from 1.
if ( 'post' === $current_screen->base ) {
$allowed = $config['base_length'] - mb_strlen( stripslashes( get_option( 'jd_twit_prepend' ) . get_option( 'jd_twit_append' ) ) ) + 1;
} else {
$allowed = $config['base_length'] + 1;
}
wp_register_script( 'wpt-base-js', plugins_url( $base_url, __FILE__ ), array( 'jquery', 'wpt.charcount' ), $wpt_version, true );
wp_enqueue_script( 'wpt-base-js' );
wp_localize_script(
'wpt-base-js',
'wptSettings',
array(
'allowed' => $allowed,
'x_limit' => $config['x'],
'mastodon_limit' => $config['mastodon'],
'bluesky_limit' => $config['bluesky'],
'is_ssl' => ( wpt_is_ssl( home_url() ) ) ? 'true' : 'false',
'text' => __( 'Characters left: ', 'wp-to-twitter' ),
'updated' => __( 'Custom status template updated', 'wp-to-twitter' ),
)
);
}
if ( 'post' === $current_screen->base && ( current_user_can( 'wpt_tweet_now' ) || current_user_can( 'manage_options' ) ) ) {
global $post;
// AJAX posting is only possible on published posts.
if ( 'publish' === $post->post_status ) {
wp_enqueue_script( 'wpt.ajax', plugins_url( $ajax_url, __FILE__ ), array( 'jquery' ), $wpt_version, true );
wp_localize_script(
'wpt.ajax',
'wpt_data',
array(
'post_ID' => $post->ID,
'action' => 'wpt_post_update',
'security' => wp_create_nonce( 'wpt-tweet-nonce' ),
)
);
}
}
if ( 'toplevel_page_wp-tweets-pro' === $current_screen->id ) {
wp_enqueue_script( 'wpt.tabs', plugins_url( $tabs_url, __FILE__ ), array( 'jquery' ), $wpt_version, true );
wp_localize_script(
'wpt.tabs',
'wpt',
array(
'firstItem' => 'wpt_post',
'firstPerm' => 'wpt_editor',
)
);
wp_enqueue_script( 'dashboard' );
}
}
/**
* Post the Custom Update & custom Update data into the post meta table
*
* @param integer $id Post ID.
* @param object $post Post object.
*
* @return bool
*/
function wpt_save_post( $id, $post ) {
if ( empty( $_POST ) || ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) || wp_is_post_revision( $id ) || isset( $_POST['_inline_edit'] ) || ( defined( 'DOING_AJAX' ) && DOING_AJAX ) || ! wpt_in_post_type( $id ) ) {
return $id;
}
if ( isset( $_POST['wp_to_twitter_meta'] ) ) {
$nonce = ( isset( $_POST['wp_to_twitter_nonce'] ) ) ? sanitize_text_field( wp_unslash( $_POST['wp_to_twitter_nonce'] ) ) : false;
if ( ! ( $nonce && wp_verify_nonce( $nonce, 'wp-to-twitter-nonce' ) ) ) {
wp_die( 'XPoster: Security check failed' );
}
if ( isset( $_POST['_yourls_keyword'] ) ) {
$yourls = sanitize_text_field( wp_unslash( $_POST['_yourls_keyword'] ) );
update_post_meta( $id, '_yourls_keyword', $yourls );
}
if ( isset( $_POST['_jd_twitter'] ) && '' !== $_POST['_jd_twitter'] ) {
$twitter = sanitize_textarea_field( wp_unslash( $_POST['_jd_twitter'] ) );
update_post_meta( $id, '_jd_twitter', $twitter );
} elseif ( isset( $_POST['_jd_twitter'] ) && '' === $_POST['_jd_twitter'] ) {
delete_post_meta( $id, '_jd_twitter' );
}
if ( isset( $_POST['_wpt_post_template_x'] ) && '' !== $_POST['_wpt_post_template_x'] ) {
$template = sanitize_textarea_field( wp_unslash( $_POST['_wpt_post_template_x'] ) );
update_post_meta( $id, '_wpt_post_template_x', $template );
} elseif ( isset( $_POST['_wpt_post_template_x'] ) && '' === $_POST['_wpt_post_template_x'] ) {
delete_post_meta( $id, '_wpt_post_template_x' );
}
if ( isset( $_POST['_wpt_post_template_bluesky'] ) && '' !== $_POST['_wpt_post_template_bluesky'] ) {
$template = sanitize_textarea_field( wp_unslash( $_POST['_wpt_post_template_bluesky'] ) );
update_post_meta( $id, '_wpt_post_template_bluesky', $template );
} elseif ( isset( $_POST['_wpt_post_template_bluesky'] ) && '' === $_POST['_wpt_post_template_bluesky'] ) {
delete_post_meta( $id, '_wpt_post_template_bluesky' );
}
if ( isset( $_POST['_wpt_post_template_mastodon'] ) && '' !== $_POST['_wpt_post_template_mastodon'] ) {
$template = sanitize_textarea_field( wp_unslash( $_POST['_wpt_post_template_mastodon'] ) );
update_post_meta( $id, '_wpt_post_template_mastodon', $template );
} elseif ( isset( $_POST['_wpt_post_template_mastodon'] ) && '' === $_POST['_wpt_post_template_mastodon'] ) {
delete_post_meta( $id, '_wpt_post_template_mastodon' );
}
if ( isset( $_POST['_jd_wp_twitter'] ) && '' !== $_POST['_jd_wp_twitter'] ) {
$wp_twitter = sanitize_textarea_field( wp_unslash( $_POST['_jd_wp_twitter'] ) );
update_post_meta( $id, '_jd_wp_twitter', $wp_twitter );
}
if ( isset( $_POST['_wpt_post_this'] ) ) {
$post_this = ( 'no' === $_POST['_wpt_post_this'] ) ? 'no' : 'yes';
update_post_meta( $id, '_wpt_post_this', $post_this );
} else {
$post_default = ( '1' === get_option( 'jd_tweet_default' ) ) ? 'no' : 'yes';
update_post_meta( $id, '_wpt_post_this', $post_default );
}
$omit_services = ( isset( $_POST['_wpt_omit_services'] ) ) ? $_POST['_wpt_omit_services'] : array();
$services = wpt_check_connections( false, true );
$omit = array();
// The interface has you choose what you want; the DB represents what's omitted.
foreach ( array_keys( $services ) as $service ) {
if ( ! in_array( $service, $omit_services, true ) ) {
$omit[] = $service;
}
}
update_post_meta( $id, '_wpt_omit_services', $omit );
if ( isset( $_POST['wpt_clear_history'] ) && 'clear' === $_POST['wpt_clear_history'] ) {
delete_post_meta( $id, '_wpt_failed' );
delete_post_meta( $id, '_jd_wp_twitter' );
delete_post_meta( $id, '_wpt_short_url' );
delete_post_meta( $id, '_wp_jd_twitter' );
}
/**
* Runs when post data is inserted.
*
* @hook wpt_insert_post
*
* @param {array} $_POST Unaltered POST data.
* @param {int} $id Post ID
*/
do_action( 'wpt_insert_post', $_POST, $id );
// only send debug data if post meta is updated.
wpt_mail( 'Post Meta Processed', 'XPoster post meta was updated' . "\n\n" . wpt_format_error( map_deep( $_POST, 'sanitize_textarea_field' ) ), $id ); // DEBUG.
if ( isset( $_POST['wpt-delete-debug'] ) && 'true' === $_POST['wpt-delete-debug'] ) {
delete_post_meta( $id, '_wpt_debug_log' );
}
if ( isset( $_POST['wpt-delete-all-debug'] ) && 'true' === $_POST['wpt-delete-all-debug'] ) {
delete_post_meta_by_key( '_wpt_debug_log' );
}
}
return $id;
}
add_action( 'init', 'wpt_old_admin_redirect' );
/**
* Send links to old admin to new admin page
*/
function wpt_old_admin_redirect() {
if ( is_admin() && isset( $_GET['page'] ) && 'wp-to-twitter/wp-to-twitter.php' === $_GET['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
wp_safe_redirect( admin_url( 'admin.php?page=wp-tweets-pro' ) );
exit;
}
}
add_action( 'admin_menu', 'wpt_admin_page' );
/**
* Add the administrative settings to the "Settings" menu.
*/
function wpt_admin_page() {
if ( function_exists( 'add_menu_page' ) && ! function_exists( 'wpt_pro_functions' ) ) {
add_menu_page( 'XPoster', 'XPoster', 'manage_options', 'wp-tweets-pro', 'wpt_update_settings', 'dashicons-share' );
}
}
add_action( 'admin_head', 'wpt_admin_style' );
/**
* Add stylesheets to XPoster pages.
*/
function wpt_admin_style() {
$wpt_version = XPOSTER_VERSION;
if ( SCRIPT_DEBUG ) {
$wpt_version .= '-' . wp_rand( 10000, 99999 );
}
global $current_screen;
$enqueues = array(
'toplevel_page_wp-tweets-pro',
'xposter-pro_page_wp-to-twitter-schedule',
'xposter-pro_page_wp-to-twitter-tweets',
'xposter-pro_page_wp-to-twitter-errors',
'profile',
);
if ( in_array( $current_screen->base, $enqueues, true ) ) {
wp_enqueue_style( 'wpt-styles', plugins_url( 'css/styles.css', __FILE__ ), array(), $wpt_version );
}
}
/**
* Add XPoster links to plug-in information.
*
* @param array $links Array of links.
* @param string $file Current file name.
*
* @return link new array.
*/
function wpt_plugin_action( $links, $file ) {
if ( plugin_basename( __DIR__ . '/wp-to-twitter.php' ) === $file || 'wp-to-twitter/init.php' === $file ) {
$admin_url = admin_url( 'admin.php?page=wp-tweets-pro' );
$links[] = "<a href='$admin_url'>" . __( 'XPoster Settings', 'wp-to-twitter' ) . '</a>';
if ( ! function_exists( 'wpt_pro_exists' ) ) {
$links[] = "<strong><a href='https://xposterpro.com/awesome/xposter-pro/'>Get XPoster Pro</a></strong>";
}
}
return $links;
}
add_filter( 'plugin_action_links', 'wpt_plugin_action', 10, 2 );
/**
* Parse plugin update info to display in update list.
*/
function wpt_plugin_update_message() {
define( 'WPT_PLUGIN_README_URL', 'http://svn.wp-plugins.org/wp-to-twitter/trunk/readme.txt' );
$response = wp_remote_get( WPT_PLUGIN_README_URL, array( 'user-agent' => 'WordPress/XPoster ' . XPOSTER_VERSION . '; ' . get_bloginfo( 'url' ) ) );
if ( ! is_wp_error( $response ) || is_array( $response ) ) {
$data = $response['body'];
$bits = explode( '== Upgrade Notice ==', $data );
$notice = trim( str_replace( '* ', '', nl2br( trim( $bits[1] ) ) ) );
if ( $notice ) {
?>
</div><div id="wpt-upgrade" class="notice inline notice-warning"><ul><li><strong style="color:#c22;">Upgrade Notes:</strong> ' . esc_html( str_replace( '* ', '', nl2br( trim( $bits[1] ) ) ) ) . '</li></ul>
<?php
}
}
}
add_action( 'in_plugin_update_message-wp-to-twitter/wp-to-twitter.php', 'wpt_plugin_update_message' );
if ( '1' === get_option( 'jd_twit_blogroll' ) ) {
add_action( 'add_link', 'wpt_post_update_link' );
}
if ( function_exists( 'wp_after_insert_post' ) ) {
/**
* Use the `wp_after_insert_post` action to run Updates.
*
* @since WordPress 5.6
*/
add_action( 'wp_after_insert_post', 'wpt_save_post', 10, 2 );
add_action( 'wp_after_insert_post', 'wpt_do_post_update', 15, 4 );
} else {
add_action( 'save_post', 'wpt_save_post', 10, 2 );
add_action( 'save_post', 'wpt_do_post_update', 15 );
}
/**
* Check whether a given post is in an allowed post type and has an update template configured.
*
* @param integer $id Post ID.
*
* @return boolean True if post is allowed, false otherwise.
*/
function wpt_in_post_type( $id ) {
$post_types = wpt_allowed_post_types();
$type = get_post_type( $id );
if ( in_array( $type, $post_types, true ) ) {
return true;
}
if ( WPT_DEBUG ) {
wpt_mail( '0: Not an updated post type', 'This post type is not enabled for status updates: ' . $type, $id );
}
return false;
}
/**
* Get array of post types that can be updated.
*
* @param string|bool $post_type Name of post type to check if a specific type is allowed or false to return array.
*
* @return array|bool
*/
function wpt_allowed_post_types( $post_type = false ) {
$post_type_settings = get_option( 'wpt_post_types' );
$post_types = array_keys( $post_type_settings );
if ( $post_type ) {
return in_array( $post_type, $post_types, true ) ? true : false;
}
$allowed_types = array();
if ( is_array( $post_type_settings ) && ! empty( $post_type_settings ) ) {
foreach ( $post_type_settings as $type => $settings ) {
if ( '1' === (string) $settings['post-edited-update'] || '1' === (string) $settings['post-published-update'] ) {
$allowed_types[] = $type;
}
}
}
/**
* Return array of post types that can be sent as status updates.
*
* @hook wpt_allowed_post_types
* @param {array} $types Array of post type names enabled for status updates either when editing or publishing.
* @param {array} $post_type_settings Multidimensional array of post types and post type settings.
*
* @return {array}
*/
return apply_filters( 'wpt_allowed_post_types', $allowed_types, $post_type_settings );
}
add_action( 'future_to_publish', 'wpt_future_to_publish', 16 );
/**
* Handle updating posts scheduled for the future.
*
* @param object $post Post object.
*/
function wpt_future_to_publish( $post ) {
$id = $post->ID;
if ( wp_is_post_autosave( $id ) || wp_is_post_revision( $id ) || ! wpt_in_post_type( $id ) ) {
return;
}
wpt_mail( 'Transitioning future to publish', $id );
wpt_post_update_future( $id );
}
/**
* Check whether autotweeting has been allowed.
*
* @param int $post_id Post ID.
*
* @return bool
*/
function wpt_auto_tweet_allowed( $post_id ) {
$state = get_option( 'wpt_auto_tweet_allowed', '0' );
$return = ( '0' !== $state ) ? true : false;
/**
* Return true if auto tweeting of old posts is enabled.
*
* @hook wpt_auto_tweet_allowed
* @param {bool} $return true if enabled.
* @param {int} $post_id Post ID.
*
* @return {bool}
*/
return apply_filters( 'wpt_auto_tweet_allowed', $return, $post_id );
}
/**
* Handle updating posts published directly. As of 12/10/2020, supports new wp_after_insert_post to improve support when used with block editor.
*
* @param int $id Post ID.
* @param object $post Post object.
* @param boolean $updated True if updated, false if inserted.
* @param object $post_before The post prior to this update, or null for new posts.
*/
function wpt_do_post_update( $id, $post = null, $updated = null, $post_before = null ) {
if ( ( empty( $_POST ) && ! wpt_auto_tweet_allowed( $id ) ) || ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) || wp_is_post_revision( $id ) || isset( $_POST['_inline_edit'] ) || ( defined( 'DOING_AJAX' ) && DOING_AJAX && ! wpt_auto_tweet_allowed( $id ) ) || ! wpt_in_post_type( $id ) ) {
return $id;
}
$post = ( null === $post ) ? get_post( $id ) : $post;
if ( 'publish' !== $post->post_status ) {
return $id;
}
wpt_post_update_instant( $id, $post, $updated, $post_before );
}
add_action( 'xmlrpc_publish_post', 'wpt_post_update_xmlrpc' );
add_action( 'publish_phone', 'wpt_post_update_xmlrpc' );
/**
* For future posts, check transients to see whether this post has already been published. Prevents duplicate status update attempts in older versions of WP.
*
* @param integer $id Post ID.
*/
function wpt_post_update_future( $id ) {
set_transient( '_wpt_post_update_future', $id, 10 );
// instant action has already run for this post.
// prevent running actions twice (need both for older WP).
if ( get_transient( '_wpt_post_update_instant' ) && (int) get_transient( '_wpt_twit_instant' ) === $id ) {
delete_transient( '_wpt_post_update_instant' );
return;
}
wpt_post_update( $id, 'future' );
}
/**
* For immediate posts, check transients to see whether this post has already been published. Prevents duplicate status update attempts in older versions of WP or cases where a future action is being run after the initial action.
*
* @param int $id Post ID.
* @param object $post Post object.
* @param boolean $updated True if updated, false if inserted.
* @param object $post_before The post prior to this update, or null for new posts.
*/
function wpt_post_update_instant( $id, $post, $updated, $post_before ) {
set_transient( '_wpt_twit_instant', $id, 10 );
// future action has already run for this post.
if ( get_transient( '_wpt_twit_future' ) && (int) get_transient( '_wpt_twit_future' ) === $id ) {
delete_transient( '_wpt_twit_future' );
return;
}
// xmlrpc action has already run for this post.
if ( get_transient( '_wpt_twit_xmlrpc' ) && (int) get_transient( '_wpt_twit_xmlrpc' ) === $id ) {
delete_transient( '_wpt_twit_xmlrpc' );
return;
}
wpt_post_update( $id, 'instant', $post, $updated, $post_before );
}
/**
* Status updates on XMLRPC posts.
*
* @param integer $id Post ID.
*
* @return post ID.
*/
function wpt_post_update_xmlrpc( $id ) {
set_transient( '_wpt_twit_xmlrpc', $id, 10 );
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE || wp_is_post_revision( $id ) || ! wpt_in_post_type( $id ) ) {
return $id;
}
wpt_mail( 'Status update sent on XMLRPC published post', $id );
wpt_post_update( $id, 'xmlrpc' );
return $id;
}
add_action( 'admin_notices', 'wpt_debugging_enabled', 10 );
/**
* Show notice if X.com debugging is enabled.
*/
function wpt_debugging_enabled() {
if ( current_user_can( 'manage_options' ) && WPT_DEBUG ) {
$message = __( '<strong>XPoster</strong> debugging is enabled. Remember to disable debugging when you are finished.', 'wp-to-twitter' );
wp_admin_notice(
$message,
array(
'type' => 'error',
)
);
}
}
add_action( 'admin_notices', 'wpt_needs_bearer_token', 10 );
/**
* Notify users if they need to add a bearer token for XPoster.
*/
function wpt_needs_bearer_token() {
if ( current_user_can( 'manage_options' ) || current_user_can( 'wpt_twitter_oauth' ) ) {
$screen = get_current_screen();
if ( 'profile' === $screen->id && get_option( 'jd_individual_twitter_users' ) ) {
$auth = wp_get_current_user()->ID;
$authorized = wtt_oauth_test( $auth );
$bt = get_user_meta( $auth, 'bearer_token', true );
} else {
$auth = false;
$authorized = wtt_oauth_test();
$bt = get_option( 'bearer_token', '' );
}
if ( ! $bt && $authorized ) {
if ( $auth && get_option( 'jd_individual_twitter_users' ) ) {
$message = __( '<strong>XPoster</strong> needs a Bearer Token added to your profile settings to support the X.com API.', 'wp-to-twitter' );
wp_admin_notice(
$message,
array(
'type' => 'error',
)
);
} elseif ( current_user_can( 'manage_options' ) ) {
// Translators: URL to connection settings.
$message = sprintf( __( '<strong>XPoster</strong> needs a Bearer Token added to the <a href="%s">connection settings</a> to support the X.com API.', 'wp-to-twitter' ), admin_url( 'admin.php?page=wp-tweets-pro&tab=connection' ) );
wp_admin_notice(
$message,
array(
'type' => 'error',
)
);
}
}
}
}
/**
* Check connections.
*
* @param int|bool $auth User ID or false to check primary connection.
* @param bool $get_connections True to return an array with the valid connections. Default false.
*
* @return bool|array
*/
function wpt_check_connections( $auth = false, $get_connections = false ) {
$connected = false;
if ( ! $get_connections ) {
$connected = ( wtt_oauth_test( $auth, 'verify' ) || wpt_mastodon_connection( $auth ) || wpt_bluesky_connection( $auth ) ) ? true : false;
} else {
$connected = array(
'x' => wpt_oauth_connection( $auth ),
'mastodon' => wpt_mastodon_connection( $auth ),
'bluesky' => wpt_bluesky_connection( $auth ),
);
}
return $connected;
}
/**
* Dismiss the missing connection notice.
*/
function wpt_dismiss_connection() {
global $current_screen;
if ( $current_screen && 'toplevel_page_wp-tweets-pro' === $current_screen->id ) {
$nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : false;
$verify = wp_verify_nonce( $nonce, 'wpt-dismiss' );
$dismiss = isset( $_GET['dismiss'] ) && 'connection' === $_GET['dismiss'] ? true : false;
if ( $verify && $dismiss ) {
update_option( 'wpt_ignore_connection', 'true' );
}
}
}
add_action( 'admin_init', 'wpt_dismiss_connection' );
/**
* Display notices if update services are not connected.
*/
function wpt_needs_connection() {
global $current_screen;
if ( 'toplevel_page_wp-tweets-pro' === $current_screen->id && ! 'true' === get_option( 'wpt_ignore_connection' ) ) {
$message = '';
$mastodon = wpt_mastodon_connection();
$x = wpt_check_oauth();
$bluesky = wpt_bluesky_connection();
// show notification to authenticate with Mastodon.
if ( ! $mastodon ) {
$admin_url = admin_url( 'admin.php?page=wp-tweets-pro&tab=mastodon' );
// Translators: Settings page to authenticate Mastodon.
$message .= '<li>' . sprintf( __( "Mastodon requires authentication. <a href='%s'>Update your Mastodon settings</a> to enable XPoster to send updates to Mastodon.", 'wp-to-twitter' ), $admin_url ) . '</li>';
}
// show notification to authenticate with OAuth.
if ( ! $x ) {
$admin_url = admin_url( 'admin.php?page=wp-tweets-pro' );
// Translators: Settings page to authenticate X.com.
$message .= '<li>' . sprintf( __( "X.com requires authentication by OAuth. <a href='%s'>Update your X settings</a> to enable XPoster to send updates to X.com.", 'wp-to-twitter' ), $admin_url ) . '</li>';
}
// show notification to authenticate with Bluesky.
if ( ! $bluesky ) {
$admin_url = admin_url( 'admin.php?page=wp-tweets-pro&tab=bluesky' );
// Translators: Settings page to authenticate Bluesky.
$message .= '<li>' . sprintf( __( "Bluesky requires authentication. <a href='%s'>Update your Bluesky settings</a> to enable XPoster to send updates to Bluesky.", 'wp-to-twitter' ), $admin_url ) . '</li>';
}
$message = ( $message ) ? '<ul>' . $message . '</ul>' : '';
$is_dismissible = '';
$class = 'xposter-connection';
if ( $x || $mastodon || $bluesky ) {
$class = 'xposter-connection dismissible';
$args = array(
'dismiss' => 'connection',
'_wpnonce' => wp_create_nonce( 'wpt_dismiss' ),
);
$dismiss_url = add_query_arg( $args, admin_url( 'admin.php?page=wp-tweets-pro' ) );
$is_dismissible = ' <a href="' . esc_url( $dismiss_url ) . '" class="button button-secondary">' . __( 'Ignore', 'wp-to-twitter' ) . '</a>';
}
if ( $message ) {
wp_admin_notice(
"$message $is_dismissible",
array(
'type' => 'error',
'additional_classes' => array( $class ),
)
);
}
}
}
add_action( 'admin_notices', 'wpt_needs_connection' );
/**
* Get service SVG.
*
* @param string $service Service name.
*
* @return string Url.
*/
function wpt_get_svg( $service ) {
return plugins_url( 'images/' . $service . '.svg', __FILE__ );
}