secure-updates-client/secure-updates-client.php
acebytes 52f06e872c feat: Promote to beta status and restructure documentation
This significant update promotes the plugin to beta status and completely restructures documentation to support production deployments. Major changes include:

- Upgraded status from alpha to beta with appropriate warnings
- Added critical security features:
  - Version compatibility checking
  - Rate limiting for API requests
  - Checksum verification pre-installation

- Restructured documentation into focused guides:
  - Split CHANGELOG.md into separate file
  - Added comprehensive SECURITY.md
  - Created detailed API_INTEGRATION.md
  - Added production DEPLOYMENT.md checklist

- Enhanced descriptions and integration notes:
  - Clarified out-of-box compatibility with Secure Updates Server
  - Updated system requirements
  - Added security considerations
  - Improved troubleshooting guides

- Reviewed and verified all documentation matches current code status

This is a major milestone preparing for production use. Testing in staging environments is still strongly recommended.
2024-11-05 17:10:44 -08:00

488 lines
No EOL
19 KiB
PHP

<?php
/*
Plugin Name: Secure Updates Client
Description: Allows specifying a custom host for plugin updates.
Version: 3.1
Author: Secure Updates Foundation
Text Domain: secure-updates-client
Domain Path: /languages
*/
if (!defined('ABSPATH')) {
exit; // Exit if accessed directly
}
if (!class_exists('Secure_Updates_Client')) {
class Secure_Updates_Client {
/**
* Custom host URL for plugin updates.
*
* @var string
*/
private $custom_host;
/**
* Flag to determine if custom host is enabled.
*
* @var bool
*/
private $custom_host_enabled;
/**
* Constructor to initialize the plugin.
*/
public function __construct() {
// Load plugin options
$this->custom_host = get_option('custom_update_host_url', '');
$this->custom_host_enabled = get_option('custom_update_host_enabled', false);
// Setup hooks
$this->setup_hooks();
// Schedule daily sync
$this->schedule_daily_sync();
}
/**
* Setup WordPress hooks.
*/
private function setup_hooks() {
// Existing hooks
add_action('admin_menu', [$this, 'setup_admin_menu']);
add_action('admin_init', [$this, 'register_settings']);
add_filter('pre_set_site_transient_update_plugins', [$this, 'override_plugin_update_url']);
add_filter('plugins_api', [$this, 'modify_plugin_information'], 10, 3);
add_action('wp_ajax_test_custom_host', [$this, 'test_custom_host_connection']);
add_action('admin_enqueue_scripts', [$this, 'enqueue_admin_scripts']);
// New hooks for plugin sync
add_action('activated_plugin', [$this, 'plugin_activated']);
add_action('deactivated_plugin', [$this, 'plugin_deactivated']);
add_action('upgrader_process_complete', [$this, 'handle_plugin_changes'], 10, 2);
add_action('secure_updates_client_daily_sync', [$this, 'send_installed_plugins_to_server']);
// Activation/Deactivation hooks
register_activation_hook(__FILE__, [$this, 'activate_plugin']);
register_deactivation_hook(__FILE__, [$this, 'deactivate_plugin']);
}
/**
* Setup the admin menu in WordPress dashboard.
*/
public function setup_admin_menu() {
add_options_page(
__('Secure Updates Client Settings', 'secure-updates-client'),
__('Secure Updates Client', 'secure-updates-client'),
'manage_options',
'secure-updates-client-settings',
[$this, 'settings_page']
);
}
/**
* Register plugin settings.
*/
public function register_settings() {
// Existing settings
register_setting('secure_update_client_options_group', 'custom_update_host_url', [
'type' => 'string',
'sanitize_callback' => 'esc_url_raw',
'default' => '',
]);
register_setting('secure_update_client_options_group', 'custom_update_host_enabled', [
'type' => 'boolean',
'sanitize_callback' => [$this, 'sanitize_boolean'],
'default' => false,
]);
// New API key setting
register_setting('secure_update_client_options_group', 'custom_update_host_api_key', [
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'default' => '',
]);
}
private function verify_plugin_compatibility($plugin_data) {
global $wp_version;
if (isset($plugin_data['requires'])) {
return version_compare($wp_version, $plugin_data['requires'], '>=');
}
return true;
}
private function check_rate_limit() {
$last_request = get_transient('secure_updates_last_request');
if ($last_request) {
if (time() - $last_request < 5) { // 5 second cooldown
return false;
}
}
set_transient('secure_updates_last_request', time(), HOUR_IN_SECONDS);
return true;
}
private function verify_plugin_checksum($file_path, $expected_checksum) {
$actual_checksum = hash_file('sha256', $file_path);
return hash_equals($expected_checksum, $actual_checksum);
}
/**
* Settings page HTML.
*/
public function settings_page() {
?>
<div class="wrap">
<h1><?php esc_html_e('Secure Updates Client Settings', 'secure-updates-client'); ?></h1>
<form method="post" action="options.php">
<?php
settings_fields('secure_update_client_options_group');
do_settings_sections('secure_update_client_options_group');
?>
<table class="form-table">
<tr valign="top">
<th scope="row">
<label for="custom_update_host_url"><?php esc_html_e('Custom Host URL', 'secure-updates-client'); ?></label>
</th>
<td>
<input type="url" id="custom_update_host_url" name="custom_update_host_url" value="<?php echo esc_attr($this->custom_host); ?>" class="regular-text" required>
<p class="description"><?php esc_html_e('Enter your custom host URL where plugin updates are hosted.', 'secure-updates-client'); ?></p>
</td>
</tr>
<tr valign="top">
<th scope="row">
<label for="custom_update_host_api_key"><?php esc_html_e('API Key', 'secure-updates-client'); ?></label>
</th>
<td>
<input type="text"
id="custom_update_host_api_key"
name="custom_update_host_api_key"
value="<?php echo esc_attr(get_option('custom_update_host_api_key', '')); ?>"
class="regular-text"
required>
<p class="description"><?php esc_html_e('Enter the API key provided by the secure updates server.', 'secure-updates-client'); ?></p>
</td>
</tr>
<tr valign="top">
<th scope="row">
<label><?php esc_html_e('Test Custom Host Connection', 'secure-updates-client'); ?></label>
</th>
<td>
<button type="button" id="test-custom-host" class="button-secondary">
<?php esc_html_e('Test Connection', 'secure-updates-client'); ?>
</button>
<p id="test-result" class="description"></p>
</td>
</tr>
<tr valign="top">
<th scope="row">
<label for="custom_host_enabled"><?php esc_html_e('Enable Custom Host', 'secure-updates-client'); ?></label>
</th>
<td>
<input type="checkbox" id="custom_host_enabled" name="custom_update_host_enabled" value="1" <?php checked(1, $this->custom_host_enabled); ?> />
<label for="custom_host_enabled"><?php esc_html_e('Enable updates from the custom host.', 'secure-updates-client'); ?></label>
<p class="description"><?php esc_html_e('Ensure that the connection test is successful before enabling.', 'secure-updates-client'); ?></p>
</td>
</tr>
</table>
<?php submit_button(); ?>
</form>
</div>
<?php
}
/**
* Schedule daily sync of plugins
*/
public function schedule_daily_sync() {
if (!wp_next_scheduled('secure_updates_client_daily_sync')) {
wp_schedule_event(time(), 'daily', 'secure_updates_client_daily_sync');
}
}
/**
* Plugin activation handler
*/
public function activate_plugin() {
$this->schedule_daily_sync();
}
/**
* Plugin deactivation handler
*/
public function deactivate_plugin() {
wp_clear_scheduled_hook('secure_updates_client_daily_sync');
}
/**
* Handle plugin activation
*/
public function plugin_activated($plugin) {
if (!$this->has_custom_update($plugin)) {
$slug = dirname($plugin);
if ($slug !== '.') {
$this->send_installed_plugins_to_server([$slug]);
}
}
}
/**
* Handle plugin deactivation
*/
public function plugin_deactivated($plugin) {
if (!$this->has_custom_update($plugin)) {
$slug = dirname($plugin);
if ($slug !== '.') {
// Optionally notify server of deactivation
$this->send_installed_plugins_to_server(); // Resend full list
}
}
}
/**
* Handle plugin installation and updates
*/
public function handle_plugin_changes($upgrader_object, $options) {
if ($options['type'] === 'plugin' && !empty($options['plugins'])) {
$plugin_slugs = [];
foreach ($options['plugins'] as $plugin_file) {
if (!$this->has_custom_update($plugin_file)) {
$slug = dirname($plugin_file);
if ($slug !== '.') {
$plugin_slugs[] = $slug;
}
}
}
if (!empty($plugin_slugs)) {
$this->send_installed_plugins_to_server($plugin_slugs);
}
}
}
/**
* Send installed plugins to server
*/
public function send_installed_plugins_to_server($plugin_slugs = []) {
if (!$this->custom_host_enabled || empty($this->custom_host)) {
return;
}
$api_key = get_option('custom_update_host_api_key', '');
if (empty($api_key)) {
return;
}
if (empty($plugin_slugs)) {
$all_plugins = get_plugins();
$plugin_slugs = [];
foreach ($all_plugins as $plugin_file => $plugin_data) {
if (!$this->has_custom_update($plugin_file)) {
$slug = dirname($plugin_file);
if ($slug !== '.') {
$plugin_slugs[] = $slug;
}
}
}
}
$response = wp_remote_post(
trailingslashit($this->custom_host) . 'wp-json/secure-updates-server/v1/plugins',
[
'headers' => [
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $api_key,
],
'body' => json_encode(['plugins' => $plugin_slugs]),
'timeout' => 15,
'sslverify' => true,
]
);
if (is_wp_error($response)) {
error_log('Secure Updates Client: Error sending plugins to server - ' . $response->get_error_message());
}
}
/**
* Sanitize boolean values.
*
* @param mixed $value The value to sanitize.
* @return bool
*/
public function sanitize_boolean($value) {
return (bool) $value;
}
/**
* Enqueue admin scripts.
*/
public function enqueue_admin_scripts($hook) {
if ($hook !== 'settings_page_secure-updates-client-settings') {
return;
}
wp_enqueue_script(
'secure-updates-client-admin-js',
trailingslashit(plugin_dir_url(__FILE__)) . 'js/admin.js',
['jquery'],
'1.0',
true
);
wp_localize_script('secure-updates-client-admin-js', 'SecureUpdatesClient', [
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('test_custom_host_nonce'),
'success_message' => __('Connection successful.', 'secure-updates-client'),
'error_message' => __('Connection failed.', 'secure-updates-client'),
]);
}
/**
* Override plugin update URLs to point to the custom host.
*
* @param stdClass $transient The update transient.
* @return stdClass
*/
public function override_plugin_update_url($transient) {
if (empty($transient->response) || !$this->custom_host_enabled) {
return $transient;
}
foreach ($transient->response as $plugin_file => $plugin_data) {
// Skip plugins with custom update mechanisms
if ($this->has_custom_update($plugin_file)) {
continue;
}
// Only override if the plugin is from WordPress.org
if ($this->is_from_wordpress_org($plugin_data)) {
$plugin_slug = $plugin_data->slug;
$transient->response[$plugin_file]->package = trailingslashit($this->custom_host) . 'wp-json/secure-updates-server/v1/download/' . sanitize_title($plugin_slug);
}
}
return $transient;
}
/**
* Check if a plugin has a custom update mechanism.
*
* @param string $plugin_file The plugin file path.
* @return bool
*/
private function has_custom_update($plugin_file) {
$plugin_data = get_plugin_data(WP_PLUGIN_DIR . '/' . $plugin_file);
// Check for a custom update URI in plugin headers
if (!empty($plugin_data['UpdateURI'])) {
return true;
}
// Additional checks can be added here (e.g., checking for known custom updater classes)
return false;
}
/**
* Determine if a plugin is from WordPress.org.
*
* @param stdClass $plugin_data The plugin data object.
* @return bool
*/
private function is_from_wordpress_org($plugin_data) {
// WordPress.org plugins typically do not have a 'package' set
return empty($plugin_data->package);
}
/**
* Modify plugin information to include secure update messages.
*
* @param mixed $result The plugin information.
* @param string $action The type of request.
* @param stdClass $args The query arguments.
* @return mixed
*/
public function modify_plugin_information($result, $action, $args) {
if ($action !== 'plugin_information' || !$this->custom_host_enabled) {
return $result;
}
// Check if the plugin is using the custom host
if ($this->is_using_custom_host($args->slug)) {
// Fetch checksum from the custom host's API
$response = wp_remote_get(trailingslashit($this->custom_host) . 'wp-json/secure-updates-server/v1/download/' . sanitize_title($args->slug));
if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
$body = wp_remote_retrieve_body($response);
$data = json_decode($body, true);
if (isset($data['checksum'])) {
$checksum = sanitize_text_field($data['checksum']);
if ($checksum) {
// Add a secure update section
$result->sections['secure_update'] = '<span style="color: green;">' . esc_html__('Secure update available.', 'secure-updates-client') . '</span>';
}
}
}
}
return $result;
}
/**
* Check if a plugin is using the custom host for updates.
*
* @param string $slug The plugin slug.
* @return bool
*/
private function is_using_custom_host($slug) {
// Logic to determine if the plugin update is served by the custom host
$mirrored_plugins = get_option('secure_updates_plugins', []);
return isset($mirrored_plugins[$slug]);
}
/**
* Test the connection to the custom host URL via AJAX.
*/
public function test_custom_host_connection() {
// Verify nonce
check_ajax_referer('test_custom_host_nonce', 'security');
// Check user capabilities
if (!current_user_can('manage_options')) {
wp_send_json_error(__('Insufficient permissions.', 'secure-updates-client'));
}
$custom_host_url = isset($_POST['custom_host_url']) ? esc_url_raw($_POST['custom_host_url']) : '';
if (empty($custom_host_url)) {
wp_send_json_error(__('Invalid URL provided.', 'secure-updates-client'));
}
// Make a GET request to the /connected endpoint
$response = wp_remote_get(trailingslashit($custom_host_url) . 'wp-json/secure-updates-server/v1/connected', [
'timeout' => 15,
'sslverify' => true,
]);
if (is_wp_error($response)) {
wp_send_json_error($response->get_error_message());
}
$status_code = wp_remote_retrieve_response_code($response);
if ($status_code !== 200) {
wp_send_json_error(__('Connection failed. Please check the URL.', 'secure-updates-client'));
}
wp_send_json_success(__('Connection successful.', 'secure-updates-client'));
}
}
// Initialize the plugin
new Secure_Updates_Client();
}