bluesky-Connector/bluesky-connector.php
enki 3bb8217690 Major cleanup: Image handling fixes and plugin updates
- Fixed featured image upload and handling
- Added detailed debug logging
- Cleaned up fallback image functionality
- Updated README.md with new MIT license
- Added .gitignore file
- Fixed image reposting functionality
- Improved error handling and logging
- Added proper WordPress coding standards
2024-12-17 15:00:47 -08:00

743 lines
23 KiB
PHP

<?php
/**
* Plugin Name: Share On Bluesky
* Plugin URI: https://github.com/yourusername/bluesky-connctor
* Description: Share WordPress posts directly to Bluesky with customizable formatting
* Author: Eugene Web Doctor
* Author URI: https://eugenewebdoctor.com/
* Version: 1.0.0
* License: GPL-2.0+
* License URI: http://www.gnu.org/licenses/gpl-2.0.txt
* Text Domain: bluesky-connctor
* Domain Path: /languages
*
* @package Bluesky_Connector
*/
// Prevent direct file access
if (!defined('ABSPATH')) {
exit;
}
// Define constants
define('BLUESKY_CONNECTOR_VERSION', '1.0.0');
define('BLUESKY_CONNECTOR_FILE', __FILE__);
define('BLUESKY_CONNECTOR_DIR', plugin_dir_path(__FILE__));
define('BLUESKY_CONNECTOR_URL', plugin_dir_url(__FILE__));
define('BLUESKY_API_DOMAIN', 'https://bsky.social');
// Include required files
require_once BLUESKY_CONNECTOR_DIR . 'includes/bluesky-api.php';
require_once BLUESKY_CONNECTOR_DIR . 'includes/bluesky-auth.php';
require_once BLUESKY_CONNECTOR_DIR . 'includes/post-formatter.php';
/**
* Main plugin class
*/
class Bluesky_Connector
{
/**
* Plugin instance
*
* @var Bluesky_Connector
*/
private static $instance = null;
/**
* Plugin settings
*
* @var array
*/
private $settings = array();
/**
* Debug mode flag
*
* @var bool
*/
private $debug_mode = false;
/**
* Handle post status transitions
*
* @param string $new_status New post status.
* @param string $old_status Old post status.
* @param WP_Post $post Post object.
*/
public function handle_post_transition($new_status, $old_status, $post)
{
error_log('[Bluesky Connector] Post transition from ' . $old_status . ' to ' . $new_status . ' for post ' . $post->ID);
error_log('[Bluesky Connector] Post type: ' . $post->post_type);
error_log('[Bluesky Connector] Has featured image: ' . (has_post_thumbnail($post->ID) ? 'Yes' : 'No'));
error_log('[Bluesky Connector] Theme supports thumbnails: ' . (current_theme_supports('post-thumbnails') ? 'Yes' : 'No'));
if ($new_status === 'publish' && $old_status !== 'publish') {
$this->handle_post_save($post->ID, $post);
}
}
/**
* Get plugin instance
*
* @return Bluesky_Connector
*/
public static function get_instance()
{
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor
*/
private function __construct()
{
$this->debug_mode = defined('WP_DEBUG') && WP_DEBUG;
// Load plugin text domain
add_action('init', array($this, 'load_textdomain'));
// Initialize plugin
add_action('init', array($this, 'init'));
add_action('transition_post_status', array($this, 'handle_post_transition'), 10, 3);
// Admin hooks
if (is_admin()) {
add_action('admin_init', array($this, 'admin_init'));
add_action('admin_menu', array($this, 'add_admin_menu'));
add_action('admin_notices', array($this, 'admin_notices'));
add_action('add_meta_boxes', array($this, 'add_post_meta_box'));
add_action('save_post', array($this, 'handle_post_save'), 10, 2);
add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts'));
add_filter('plugin_action_links_' . plugin_basename(__FILE__), array($this, 'add_settings_link'));
}
// Register activation/deactivation hooks
register_activation_hook(__FILE__, array($this, 'activate'));
register_deactivation_hook(__FILE__, array($this, 'deactivate'));
// AJAX handlers
add_action('wp_ajax_bluesky_post_now', array($this, 'handle_immediate_post'));
}
/**
* Load plugin text domain
*/
public function load_textdomain()
{
load_plugin_textdomain('bluesky-connctor', false, dirname(plugin_basename(__FILE__)) . '/languages');
}
/**
* Initialize plugin
*/
public function init()
{
add_theme_support('post-thumbnails');
$this->settings = array(
'domain' => get_option('bluesky_domain', BLUESKY_API_DOMAIN),
'identifier' => get_option('bluesky_identifier', ''),
'did' => get_option('bluesky_did', ''),
'access_jwt' => get_option('bluesky_access_jwt', ''),
);
// Handle form submissions
if (is_admin() && !empty($_POST['action']) && $_POST['action'] === 'update_bluesky_settings') {
$this->handle_settings_update();
}
}
private function handle_settings_update()
{
if (!current_user_can('manage_options')) {
return;
}
check_admin_referer('bluesky_connector_settings');
if (empty($_POST['action']) || $_POST['action'] !== 'update_bluesky_settings') {
return;
}
// Get the settings from the form
$settings = isset($_POST['bluesky_connector_settings']) ? $_POST['bluesky_connector_settings'] : array();
// Validate and sanitize inputs
$identifier = isset($settings['identifier'])
? trim(sanitize_text_field(wp_unslash($settings['identifier'])))
: '';
$password = isset($settings['password'])
? trim(sanitize_text_field(wp_unslash($settings['password'])))
: '';
// Try to authenticate
$auth = new Bluesky_Auth($identifier, $password);
$result = $auth->get_access_token();
if (isset($result['error'])) {
update_option('bluesky_connection_status', 'error');
update_option('bluesky_last_error', sanitize_text_field($result['error']));
add_settings_error(
'bluesky_connector_settings',
'auth_failed',
sprintf(
esc_html__('Authentication failed: %s', 'bluesky-connctor'),
esc_html($result['error'])
)
);
} else {
// Save the credentials in the settings array
$current_settings = get_option('bluesky_connector_settings', array());
$settings_to_save = array_merge($current_settings, array(
'domain' => BLUESKY_API_DOMAIN,
'identifier' => $identifier,
'password' => $password,
'post_format' => $current_settings['post_format'] ?? 'title-excerpt-link',
'include_title' => $current_settings['include_title'] ?? true,
'title_separator' => $current_settings['title_separator'] ?? "\n\n"
));
// Update all settings
update_option('bluesky_connector_settings', $settings_to_save);
update_option('bluesky_identifier', $identifier); // Keep for backward compatibility
update_option('bluesky_connection_status', 'connected');
update_option('bluesky_last_error', '');
add_settings_error(
'bluesky_connector_settings',
'settings_updated',
esc_html__('Settings saved and connected successfully.', 'bluesky-connctor'),
'success'
);
}
}
/**
* Register plugin settings
*/
public function admin_init()
{
// Add the settings page to allowed options
add_allowed_options(array(
'bluesky-connector' => array(
'bluesky_connector_settings'
)
));
// Register the main settings
register_setting(
'bluesky-connector',
'bluesky_connector_settings',
array(
'type' => 'object',
'description' => esc_html__('Bluesky Connector Settings', 'bluesky-connctor'),
'sanitize_callback' => array($this, 'sanitize_settings'),
'default' => array(
'domain' => BLUESKY_API_DOMAIN,
'identifier' => '',
'password' => '',
'post_format' => 'title-excerpt-link',
'include_title' => true,
'title_separator' => "\n\n"
),
'show_in_rest' => false,
)
);
// Add settings sections
add_settings_section(
'bluesky_connection_section',
esc_html__('Connection Settings', 'bluesky-connctor'),
array($this, 'render_connection_section'),
'bluesky-connector'
);
add_settings_section(
'bluesky_post_format_section',
esc_html__('Post Format Settings', 'bluesky-connctor'),
array($this, 'render_format_section'),
'bluesky-connector'
);
}
public function sanitize_settings($input)
{
$sanitized = array();
// Domain is fixed
$sanitized['domain'] = BLUESKY_API_DOMAIN;
// Sanitize identifier
if (isset($input['identifier'])) {
$sanitized['identifier'] = sanitize_text_field($input['identifier']);
}
// Handle password - note we don't store this
if (isset($input['password'])) {
$sanitized['password'] = sanitize_text_field($input['password']);
}
// Sanitize post format
$sanitized['post_format'] = isset($input['post_format'])
? sanitize_text_field($input['post_format'])
: 'title-excerpt-link';
// Sanitize include_title
$sanitized['include_title'] = isset($input['include_title']);
// Sanitize title separator
$sanitized['title_separator'] = isset($input['title_separator'])
? sanitize_text_field($input['title_separator'])
: "\n\n";
return $sanitized;
}
public function render_connection_section()
{
echo '<p>' . esc_html__('Configure your Bluesky connection settings.', 'bluesky-connctor') . '</p>';
}
public function render_format_section()
{
echo '<p>' . esc_html__('Configure how your posts appear on Bluesky.', 'bluesky-connctor') . '</p>';
}
/**
* Enqueue admin scripts and styles
*
* @param string $hook The current admin page.
*/
public function enqueue_admin_scripts($hook) {
// Only load on post edit screens and settings page
if (!in_array($hook, array('post.php', 'post-new.php', 'settings_page_bluesky-settings'))) {
return;
}
// Enqueue WordPress media uploader
wp_enqueue_script(
'bluesky-admin',
BLUESKY_CONNECTOR_URL . 'assets/js/admin.js',
array('jquery'),
BLUESKY_CONNECTOR_VERSION,
true
);
wp_localize_script('bluesky-admin', 'blueskyAdmin', array(
'ajaxUrl' => admin_url('admin-ajax.php'),
'strings' => array(
'selectImage' => esc_html__('Select Fallback Image', 'bluesky-connctor'),
'useImage' => esc_html__('Use this image', 'bluesky-connctor'),
'publishing' => esc_html__('Publishing...', 'bluesky-connctor'),
'retrying' => esc_html__('Retrying...', 'bluesky-connctor'),
'reposting' => esc_html__('Reposting...', 'bluesky-connctor'),
'retry' => esc_html__('Retry Post', 'bluesky-connctor'),
'repost' => esc_html__('Post Again', 'bluesky-connctor'),
'error' => esc_html__('An error occurred. Please try again.', 'bluesky-connctor'),
'confirmRepost' => esc_html__('Are you sure you want to post this content again?', 'bluesky-connctor')
),
'nonce' => wp_create_nonce('bluesky_post_now'),
));
wp_enqueue_style(
'bluesky-admin',
BLUESKY_CONNECTOR_URL . 'assets/css/admin.css',
array(),
BLUESKY_CONNECTOR_VERSION
);
}
/**
* Handle post saving and immediate posting to Bluesky
*
* @param int $post_id Post ID.
* @param WP_Post $post Post object.
*/
public function handle_post_save($post_id, $post)
{
error_log('[Bluesky Connector] handle_post_save called for post ' . $post_id);
// Skip autosaves
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
error_log('[Bluesky Connector] Skipping - autosave');
return;
}
// Check post type
if ('post' !== $post->post_type) {
error_log('[Bluesky Connector] Skipping - not a post: ' . $post->post_type);
return;
}
// Only proceed for published posts
if ('publish' !== $post->post_status) {
error_log('[Bluesky Connector] Skipping - not published: ' . $post->post_status);
return;
}
$already_posted = get_post_meta($post_id, '_bluesky_posted', true);
$had_image = get_post_meta($post_id, '_bluesky_had_image', true);
$has_image_now = has_post_thumbnail($post->ID);
error_log('[Bluesky Connector] Post status check:');
error_log('[Bluesky Connector] - Already posted: ' . ($already_posted ? 'yes' : 'no'));
error_log('[Bluesky Connector] - Had image: ' . ($had_image ? 'yes' : 'no'));
error_log('[Bluesky Connector] - Has image now: ' . ($has_image_now ? 'yes' : 'no'));
// If already posted, only proceed if image status has changed
if ($already_posted) {
if ($had_image === $has_image_now) {
error_log('[Bluesky Connector] Skipping - already posted and image status unchanged');
return;
}
error_log('[Bluesky Connector] Image status changed, proceeding with update');
}
// Only check nonce if this is a manual save from the editor
if (isset($_POST['bluesky_post_meta_box_nonce'])) {
if (!wp_verify_nonce(
sanitize_text_field(wp_unslash($_POST['bluesky_post_meta_box_nonce'])),
'bluesky_post_meta_box'
)) {
error_log('[Bluesky Connector] Skipping - invalid nonce on manual save');
return;
}
}
error_log('[Bluesky Connector] Calling post_to_bluesky');
// Post to Bluesky
$response = $this->post_to_bluesky($post);
// If successful, update image status
if (!isset($response['error'])) {
update_post_meta($post_id, '_bluesky_had_image', $has_image_now);
}
}
/**
* Post content to Bluesky
*
* @param WP_Post $post Post object.
* @return array Response from Bluesky API.
*/
private function post_to_bluesky($post)
{
try {
error_log('[Bluesky Connector] Starting post_to_bluesky for post ' . $post->ID);
update_post_meta($post->ID, '_bluesky_status', 'pending');
$identifier = get_option('bluesky_identifier');
$password = get_option('bluesky_password');
if (empty($identifier) || empty($password)) {
throw new Exception('Bluesky identifier or password is missing.');
}
// Log settings (without password)
error_log('[Bluesky Connector] Using identifier: ' . $identifier);
// Authenticate and get access token
$auth = new Bluesky_Auth($identifier, $password);
$token = $auth->get_access_token();
if (isset($token['error'])) {
throw new Exception($token['error']);
}
// Create Post_Formatter instance
$formatter = new Post_Formatter($token, get_option('bluesky_did'));
$response = $formatter->format_and_post($post);
// Update meta data if successful
if (!isset($response['error'])) {
$record_key = $response['uri'];
if (strpos($response['uri'], 'at://') === 0) {
$parts = explode('/', $response['uri']);
$record_key = end($parts);
}
update_post_meta($post->ID, '_bluesky_posted', current_time('mysql'));
update_post_meta($post->ID, '_bluesky_post_id', sanitize_text_field($record_key));
update_post_meta($post->ID, '_bluesky_status', 'success');
update_option('bluesky_last_post_time', time());
}
return $response;
} catch (Exception $e) {
update_post_meta($post->ID, '_bluesky_status', 'error');
update_post_meta($post->ID, '_bluesky_error', sanitize_text_field($e->getMessage()));
$this->log_error('Bluesky posting failed: ' . $e->getMessage());
return array('error' => $e->getMessage());
}
}
/**
* Log error message if debug mode is enabled
*
* @param string $message Error message to log.
* @param array $data Optional data to log.
*/
// In bluesky-connector.php, inside the Bluesky_Connector class
private function log_error($message, $data = array())
{
if (defined('WP_DEBUG') && WP_DEBUG) {
if (is_array($data) || is_object($data)) {
$data = wp_json_encode($data);
}
error_log(sprintf('[Bluesky Connector] %s | Data: %s', $message, $data));
}
}
/**
* Handle AJAX immediate post request
*/
public function handle_immediate_post()
{
check_ajax_referer('bluesky_post_now', 'nonce');
if (!current_user_can('edit_posts')) {
wp_send_json_error(array(
'message' => esc_html__('Permission denied.', 'bluesky-connctor')
));
}
$post_id = filter_input(INPUT_POST, 'post_id', FILTER_VALIDATE_INT);
if (!$post_id) {
wp_send_json_error(array(
'message' => esc_html__('Invalid post ID.', 'bluesky-connctor')
));
}
$post = get_post($post_id);
if (!$post) {
wp_send_json_error(array(
'message' => esc_html__('Post not found.', 'bluesky-connctor')
));
}
$response = $this->post_to_bluesky($post);
if (isset($response['error'])) {
wp_send_json_error(array(
'message' => esc_html($response['error'])
));
} else {
wp_send_json_success(array(
'message' => esc_html__('Posted successfully to Bluesky.', 'bluesky-connctor'),
'post_id' => esc_html($response['uri'])
));
}
}
/**
* Add admin menu items
*/
public function add_admin_menu()
{
add_options_page(
esc_html__('Bluesky Settings', 'bluesky-connctor'),
esc_html__('Bluesky', 'bluesky-connctor'),
'manage_options',
'bluesky-settings',
array($this, 'render_settings_page')
);
}
/**
* Add post meta box
*/
public function add_post_meta_box()
{
add_meta_box(
'bluesky_post_meta',
esc_html__('Bluesky Status', 'bluesky-connctor'),
array($this, 'render_post_meta_box'),
'post',
'side',
'default'
);
}
/**
* Render post meta box
*
* @param WP_Post $post Post object.
*/
public function render_post_meta_box($post)
{
// Add this line at the very start of the function
wp_nonce_field('bluesky_post_meta_box', 'bluesky_post_meta_box_nonce');
$status = get_post_meta($post->ID, '_bluesky_status', true);
$posted_date = get_post_meta($post->ID, '_bluesky_posted', true);
$error = get_post_meta($post->ID, '_bluesky_error', true);
$post_id = get_post_meta($post->ID, '_bluesky_post_id', true);
require BLUESKY_CONNECTOR_DIR . 'templates/post-meta-box.php';
}
/**
* Add settings link to plugins page
*
* @param array $links Plugin action links.
* @return array Modified plugin action links.
*/
public function add_settings_link($links)
{
$settings_link = sprintf(
'<a href="%s">%s</a>',
esc_url(admin_url('options-general.php?page=bluesky-settings')),
esc_html__('Settings', 'bluesky-connctor')
);
array_unshift($links, $settings_link);
return $links;
}
/**
* Render settings page
*/
public function render_settings_page()
{
if (!current_user_can('manage_options')) {
return;
}
$settings = array(
'identifier' => esc_html(get_option('bluesky_identifier', '')),
'connection_status' => get_option('bluesky_connection_status', ''),
'last_error' => esc_html(get_option('bluesky_last_error', ''))
);
require BLUESKY_CONNECTOR_DIR . 'templates/settings-page.php';
}
/**
* Display admin notices
*/
public function admin_notices()
{
if (!current_user_can('manage_options')) {
return;
}
$screen = get_current_screen();
if ($screen->id !== 'settings_page_bluesky-settings') {
return;
}
settings_errors('bluesky_connector_settings');
}
/**
* Plugin activation
*/
public function activate()
{
// Create necessary options with default values
add_option('bluesky_domain', BLUESKY_API_DOMAIN);
add_option('bluesky_post_format', 'title-excerpt-link');
// Ensure languages directory exists
$languages_dir = dirname(plugin_basename(__FILE__)) . '/languages/';
if (!file_exists($languages_dir)) {
wp_mkdir_p($languages_dir);
}
// Clear any existing caches
wp_cache_delete('bluesky_settings', 'options');
}
/**
* Plugin deactivation
*/
public function deactivate()
{
// Clean up transients and caches
delete_transient('bluesky_auth_check');
wp_cache_delete('bluesky_settings', 'options');
// Log deactivation if in debug mode
$this->log_error('Plugin deactivated');
}
}
/**
* Clean up plugin data
* Used during uninstall
*/
function bluesky_connector_uninstall()
{
if (!defined('WP_UNINSTALL_PLUGIN')) {
return;
}
// Remove all plugin options
$options = array(
'bluesky_domain',
'bluesky_identifier',
'bluesky_password',
'bluesky_access_jwt',
'bluesky_refresh_jwt',
'bluesky_did',
'bluesky_post_queue',
'bluesky_connection_status',
'bluesky_last_error',
'bluesky_post_format',
'bluesky_connector_settings',
'bluesky_token_created',
'bluesky_last_post_time'
);
foreach ($options as $option) {
delete_option($option);
}
// Remove all post meta
$meta_keys = array(
'_bluesky_posted',
'_bluesky_status',
'_bluesky_error',
'_bluesky_post_id',
'_bluesky_had_image' // Added new meta key
);
// Get all posts
$posts = get_posts(array(
'posts_per_page' => -1,
'post_type' => 'any',
'fields' => 'ids'
));
// Delete meta and clear cache for each post
foreach ($posts as $post_id) {
foreach ($meta_keys as $meta_key) {
delete_post_meta($post_id, $meta_key);
}
clean_post_cache($post_id);
}
// Clear settings cache
wp_cache_delete('bluesky_settings', 'options');
}
// Initialize plugin
function bluesky_connector_init()
{
return Bluesky_Connector::get_instance();
}
add_action('plugins_loaded', 'bluesky_connector_init');
// Register uninstall hook
register_uninstall_hook(__FILE__, 'bluesky_connector_uninstall');