mirror of
https://gh.wpcy.net/https://github.com/secure-updates/secure-updates-library.git
synced 2026-07-14 20:46:46 +08:00
- Added package verification with checksum validation and auto-backup - Added health monitoring with WordPress Site Health integration - Added enhanced logging system with multiple log levels - Added rate limiting to prevent server overload - Added test mode for server connection verification - Improved error handling and security measures - Refactored using PHP Traits for better organization - Updated minimum WordPress requirement to 5.0
627 lines
23 KiB
PHP
627 lines
23 KiB
PHP
<?php
|
|
/*
|
|
Library Name: Secure Updates Library
|
|
Description: A library for WordPress plugin authors to easily host their own plugin updates on a secure mirror.
|
|
Version: 2.0
|
|
Author: Secure Updates Foundation
|
|
Text Domain: secure-updates-library
|
|
*/
|
|
|
|
if (!defined('ABSPATH')) {
|
|
exit; // Exit if accessed directly
|
|
}
|
|
|
|
if (!class_exists('Secure_Updates_Library')) {
|
|
// Use the new traits in the main class
|
|
class Secure_Updates_Library {
|
|
use Package_Verification;
|
|
use Enhanced_Logging;
|
|
use Health_Monitoring;
|
|
use Rate_Limiting;
|
|
|
|
/**
|
|
* Base URL of the secure mirror.
|
|
*
|
|
* @var string
|
|
*/
|
|
private $mirror_host;
|
|
|
|
/**
|
|
* Plugin slug.
|
|
*
|
|
* @var string
|
|
*/
|
|
private $plugin_slug;
|
|
|
|
/**
|
|
* Current version of the plugin.
|
|
*
|
|
* @var string
|
|
*/
|
|
private $plugin_version;
|
|
|
|
/**
|
|
* API Key for authenticating with the secure server.
|
|
*
|
|
* @var string
|
|
*/
|
|
private $api_key;
|
|
|
|
/**
|
|
* Constructor to initialize the library.
|
|
*
|
|
* @param string $mirror_host The base URL of the secure mirror.
|
|
* @param string $plugin_version The current version of the plugin.
|
|
* @param string $api_key (Optional) API key for authenticated requests.
|
|
*/
|
|
public function __construct($mirror_host, $plugin_version, $api_key = '') {
|
|
$this->mirror_host = trailingslashit(esc_url_raw($mirror_host));
|
|
$this->plugin_version = sanitize_text_field($plugin_version);
|
|
$this->api_key = sanitize_text_field($api_key);
|
|
$this->plugin_slug = $this->get_plugin_slug(); // Automatically detect the slug
|
|
|
|
// Setup hooks for checking plugin updates
|
|
add_filter('pre_set_site_transient_update_plugins', [$this, 'check_for_updates']);
|
|
add_filter('plugins_api', [$this, 'plugin_information'], 10, 3);
|
|
add_action('admin_notices', [$this, 'display_update_notices']);
|
|
}
|
|
|
|
/**
|
|
* Automatically get plugin slug from plugin directory name.
|
|
*
|
|
* @return string
|
|
*/
|
|
private function get_plugin_slug() {
|
|
// Assuming the library is included from the main plugin file
|
|
$plugin_file = plugin_basename($this->get_main_plugin_file());
|
|
$parts = explode('/', $plugin_file);
|
|
return sanitize_title_with_dashes($parts[0]);
|
|
}
|
|
|
|
/**
|
|
* Get the main plugin file path.
|
|
*
|
|
* @return string
|
|
*/
|
|
private function get_main_plugin_file() {
|
|
// This function assumes the library is included directly from the main plugin file
|
|
$backtrace = debug_backtrace();
|
|
foreach ($backtrace as $call) {
|
|
if (isset($call['file']) && strpos($call['file'], WP_PLUGIN_DIR) !== false) {
|
|
return $call['file'];
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Fetch the latest plugin information from the secure updates server.
|
|
*
|
|
* @return object|false The plugin information object or false on failure.
|
|
*/
|
|
private function fetch_latest_plugin_info() {
|
|
$info_endpoint = $this->mirror_host . 'wp-json/secure-updates-server/v1/info/' . $this->plugin_slug;
|
|
|
|
$args = [
|
|
'timeout' => 15,
|
|
'sslverify' => true,
|
|
'headers' => [
|
|
'Accept' => 'application/json',
|
|
'User-Agent' => 'WordPress/' . get_bloginfo('version') . '; ' . get_bloginfo('url'),
|
|
]
|
|
];
|
|
|
|
// Include API key if provided
|
|
if (!empty($this->api_key)) {
|
|
$args['headers']['Authorization'] = 'Bearer ' . $this->api_key;
|
|
}
|
|
|
|
$response = wp_remote_get($info_endpoint, $args);
|
|
|
|
if (is_wp_error($response)) {
|
|
$this->log_error('Error fetching plugin info: ' . $response->get_error_message());
|
|
return false;
|
|
}
|
|
|
|
$response_code = wp_remote_retrieve_response_code($response);
|
|
if ($response_code !== 200) {
|
|
$this->log_error(sprintf('Server returned %d status code', $response_code));
|
|
return false;
|
|
}
|
|
|
|
$plugin_info = json_decode(wp_remote_retrieve_body($response));
|
|
|
|
if (empty($plugin_info) || !is_object($plugin_info) || empty($plugin_info->version)) {
|
|
$this->log_error('Invalid plugin info received');
|
|
return false;
|
|
}
|
|
|
|
return $this->sanitize_plugin_info($plugin_info);
|
|
}
|
|
|
|
/**
|
|
* Log error messages
|
|
*
|
|
* @param string $message Error message
|
|
*/
|
|
private function log_error($message) {
|
|
if (defined('WP_DEBUG') && WP_DEBUG) {
|
|
error_log(sprintf('[Secure Updates Library] %s: %s', $this->plugin_slug, $message));
|
|
}
|
|
$this->store_update_status('error', $message);
|
|
}
|
|
|
|
/**
|
|
* Get plugin basename from main file path
|
|
*
|
|
* @return string
|
|
*/
|
|
private function get_plugin_basename() {
|
|
static $basename = null;
|
|
if ($basename === null) {
|
|
$basename = plugin_basename($this->get_main_plugin_file());
|
|
}
|
|
return $basename;
|
|
}
|
|
|
|
/**
|
|
* Sanitize plugin information from the server
|
|
*
|
|
* @param object $plugin_info Raw plugin information
|
|
* @return object Sanitized plugin information
|
|
*/
|
|
private function sanitize_plugin_info($plugin_info) {
|
|
return (object) [
|
|
'name' => sanitize_text_field($plugin_info->name ?? ''),
|
|
'slug' => sanitize_title($plugin_info->slug ?? $this->plugin_slug),
|
|
'version' => sanitize_text_field($plugin_info->version ?? ''),
|
|
'author' => wp_kses($plugin_info->author ?? '', ['a' => ['href' => []]]),
|
|
'homepage' => esc_url_raw($plugin_info->homepage ?? ''),
|
|
'requires' => sanitize_text_field($plugin_info->requires ?? ''),
|
|
'tested' => sanitize_text_field($plugin_info->tested ?? ''),
|
|
'requires_php' => sanitize_text_field($plugin_info->requires_php ?? ''),
|
|
'download_link' => esc_url_raw($plugin_info->download_link ?? ''),
|
|
'sections' => [
|
|
'description' => wp_kses_post($plugin_info->sections->description ?? ''),
|
|
'installation' => wp_kses_post($plugin_info->sections->installation ?? ''),
|
|
'changelog' => wp_kses_post($plugin_info->sections->changelog ?? ''),
|
|
],
|
|
];
|
|
}
|
|
|
|
public function cleanup() {
|
|
// Clear all transients and options when plugin is deactivated
|
|
global $wpdb;
|
|
$wpdb->query("DELETE FROM $wpdb->options WHERE option_name LIKE 'secure_updates_%'");
|
|
$wpdb->query("DELETE FROM $wpdb->options WHERE option_name LIKE '_transient_sup_%'");
|
|
}
|
|
|
|
private function validate_server_response($response) {
|
|
if (!is_object($response)) return false;
|
|
$required_fields = ['version', 'download_link', 'requires', 'tested'];
|
|
foreach ($required_fields as $field) {
|
|
if (!isset($response->$field)) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private function is_compatible() {
|
|
global $wp_version;
|
|
return version_compare($wp_version, '5.0', '>=');
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Check for plugin updates.
|
|
*
|
|
* @param stdClass $transient The update transient.
|
|
* @return stdClass
|
|
*/
|
|
public function check_for_updates($transient) {
|
|
if (empty($transient->checked)) {
|
|
return $transient;
|
|
}
|
|
|
|
// Get cached plugin info
|
|
$cache_key = 'sup_' . md5($this->plugin_slug . $this->plugin_version);
|
|
$plugin_info = get_transient($cache_key);
|
|
|
|
if (false === $plugin_info) {
|
|
// Fetch latest plugin info from the server
|
|
$plugin_info = $this->fetch_latest_plugin_info();
|
|
|
|
if ($plugin_info) {
|
|
// Cache for 6 hours
|
|
set_transient($cache_key, $plugin_info, 6 * HOUR_IN_SECONDS);
|
|
} else {
|
|
// Cache failures for 1 hour to prevent hammering the server
|
|
set_transient($cache_key, 'failed', HOUR_IN_SECONDS);
|
|
$this->log_update_error('Failed to fetch plugin info');
|
|
return $transient;
|
|
}
|
|
} elseif ($plugin_info === 'failed') {
|
|
return $transient;
|
|
}
|
|
|
|
// Compare versions
|
|
if (version_compare($plugin_info->version, $this->plugin_version, '>')) {
|
|
$plugin_basename = plugin_basename($this->get_main_plugin_file());
|
|
|
|
$response = new stdClass();
|
|
$response->slug = $this->plugin_slug;
|
|
$response->new_version = $plugin_info->version;
|
|
$response->url = esc_url_raw($plugin_info->homepage);
|
|
$response->package = esc_url_raw($plugin_info->download_link);
|
|
$response->tested = isset($plugin_info->tested) ? $plugin_info->tested : get_bloginfo('version');
|
|
$response->requires = isset($plugin_info->requires) ? $plugin_info->requires : '5.0';
|
|
$response->requires_php = isset($plugin_info->requires_php) ? $plugin_info->requires_php : '5.6';
|
|
|
|
$transient->response[$plugin_basename] = $response;
|
|
|
|
// Store update status for admin notice
|
|
$this->store_update_status('success', sprintf(
|
|
__('Update available for %s: version %s', 'secure-updates-library'),
|
|
$this->plugin_slug,
|
|
$plugin_info->version
|
|
));
|
|
}
|
|
|
|
return $transient;
|
|
}
|
|
|
|
/**
|
|
* Store update status for admin notices
|
|
*
|
|
* @param string $status Status of the update (success/error)
|
|
* @param string $message Status message
|
|
*/
|
|
private function store_update_status($status, $message) {
|
|
set_transient(
|
|
'secure_updates_' . $this->plugin_slug . '_status',
|
|
['status' => $status, 'message' => $message],
|
|
30
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Provide detailed plugin information.
|
|
*
|
|
* @param mixed $result The plugin information.
|
|
* @param string $action The type of request.
|
|
* @param stdClass $args The query arguments.
|
|
* @return mixed
|
|
*/
|
|
public function plugin_information($result, $action, $args) {
|
|
if ($action !== 'plugin_information' || $args->slug !== $this->plugin_slug) {
|
|
return $result;
|
|
}
|
|
|
|
// Fetch plugin data from the secure mirror
|
|
$info_endpoint = $this->mirror_host . 'wp-json/secure-updates-server/v1/info/' . sanitize_title($this->plugin_slug);
|
|
|
|
$request_args = [
|
|
'timeout' => 15,
|
|
'sslverify' => true,
|
|
];
|
|
|
|
// Include API key if provided
|
|
if (!empty($this->api_key)) {
|
|
$request_args['headers'] = [
|
|
'Authorization' => 'Bearer ' . $this->api_key,
|
|
];
|
|
}
|
|
|
|
$response = wp_remote_get($info_endpoint, $request_args);
|
|
|
|
if (is_wp_error($response)) {
|
|
if (defined('WP_DEBUG') && WP_DEBUG) {
|
|
error_log('[Secure Updates Library] Error fetching plugin information: ' . $response->get_error_message());
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
$plugin_data = json_decode(wp_remote_retrieve_body($response));
|
|
|
|
if (isset($plugin_data->name)) {
|
|
$result = new stdClass();
|
|
$result->name = sanitize_text_field($plugin_data->name);
|
|
$result->slug = sanitize_title($plugin_data->slug);
|
|
$result->version = sanitize_text_field($plugin_data->version);
|
|
$result->author = sanitize_text_field($plugin_data->author);
|
|
$result->homepage = esc_url_raw($plugin_data->homepage);
|
|
$result->sections = [
|
|
'description' => wp_kses_post($plugin_data->description),
|
|
'installation' => wp_kses_post($plugin_data->installation),
|
|
'changelog' => wp_kses_post($plugin_data->changelog),
|
|
];
|
|
$result->download_link = esc_url_raw($plugin_data->download_link);
|
|
|
|
if (defined('WP_DEBUG') && WP_DEBUG) {
|
|
error_log('[Secure Updates Library] Fetched plugin information for ' . $this->plugin_slug);
|
|
}
|
|
} else {
|
|
if (defined('WP_DEBUG') && WP_DEBUG) {
|
|
error_log('[Secure Updates Library] Invalid plugin information received for ' . $this->plugin_slug);
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Display update notices in the admin dashboard.
|
|
*/
|
|
public function display_update_notices() {
|
|
// Implement logic to display admin notices based on update status
|
|
// This could include success messages for updates or error messages if update checks failed
|
|
}
|
|
|
|
/**
|
|
* Display diagnostic information in WP Site Health
|
|
*/
|
|
public function register_health_tests() {
|
|
add_filter('site_status_tests', function($tests) {
|
|
$tests['direct']['secure_updates_' . $this->plugin_slug] = [
|
|
'label' => sprintf(
|
|
__('%s Update System', 'secure-updates-library'),
|
|
ucfirst($this->plugin_slug)
|
|
),
|
|
'test' => [$this, 'perform_health_test']
|
|
];
|
|
return $tests;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Perform health test for Site Health
|
|
*/
|
|
public function perform_health_test() {
|
|
$health_checks = $this->check_system_health();
|
|
$all_passed = true;
|
|
$result = [
|
|
'label' => sprintf(
|
|
__('%s Update System', 'secure-updates-library'),
|
|
ucfirst($this->plugin_slug)
|
|
),
|
|
'status' => 'good',
|
|
'badge' => [
|
|
'label' => __('Security', 'secure-updates-library'),
|
|
'color' => 'blue',
|
|
],
|
|
'description' => '<p>' . __('Your plugin update system is working correctly.', 'secure-updates-library') . '</p>',
|
|
'actions' => '',
|
|
'test' => 'secure_updates_' . $this->plugin_slug,
|
|
];
|
|
|
|
foreach ($health_checks as $check) {
|
|
if (!$check['status']) {
|
|
$all_passed = false;
|
|
$result['status'] = 'critical';
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!$all_passed) {
|
|
$result['description'] = '<p>' . __('There are issues with your plugin update system.', 'secure-updates-library') . '</p>';
|
|
$result['actions'] = sprintf(
|
|
'<a href="%s">%s</a>',
|
|
admin_url('admin.php?page=secure-updates-server'),
|
|
__('View Details', 'secure-updates-library')
|
|
);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Package verification and backup functionality.
|
|
*/
|
|
trait Package_Verification {
|
|
/**
|
|
* Verify package checksum before installation
|
|
*
|
|
* @param string $package Package URL
|
|
* @return bool|WP_Error True on success, WP_Error on failure
|
|
*/
|
|
private function verify_package($package) {
|
|
$download_file = download_url($package);
|
|
|
|
if (is_wp_error($download_file)) {
|
|
return $download_file;
|
|
}
|
|
|
|
// Get expected checksum from server
|
|
$checksum_url = add_query_arg(
|
|
['action' => 'checksum', 'version' => $this->plugin_version],
|
|
$this->mirror_host . 'wp-json/secure-updates-server/v1/verify_file/' . $this->plugin_slug
|
|
);
|
|
|
|
$expected_checksum = wp_remote_get($checksum_url, [
|
|
'headers' => ['Authorization' => 'Bearer ' . $this->api_key]
|
|
]);
|
|
|
|
if (is_wp_error($expected_checksum)) {
|
|
unlink($download_file);
|
|
return $expected_checksum;
|
|
}
|
|
|
|
$actual_checksum = hash_file('sha256', $download_file);
|
|
$expected_checksum = trim(wp_remote_retrieve_body($expected_checksum));
|
|
|
|
if ($actual_checksum !== $expected_checksum) {
|
|
unlink($download_file);
|
|
return new WP_Error(
|
|
'checksum_mismatch',
|
|
__('Package verification failed. Security check did not match.', 'secure-updates-library')
|
|
);
|
|
}
|
|
|
|
unlink($download_file);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Backup current plugin version before update
|
|
*/
|
|
private function backup_current_version() {
|
|
$plugin_dir = plugin_dir_path($this->get_main_plugin_file());
|
|
$backup_dir = WP_CONTENT_DIR . '/upgrade/plugin-backups/';
|
|
$backup_name = $this->plugin_slug . '-' . $this->plugin_version . '-' . date('Y-m-d-H-i-s') . '.zip';
|
|
|
|
// Create backup directory if it doesn't exist
|
|
wp_mkdir_p($backup_dir);
|
|
|
|
if (!class_exists('PclZip')) {
|
|
require_once(ABSPATH . 'wp-admin/includes/class-pclzip.php');
|
|
}
|
|
|
|
$zip = new PclZip($backup_dir . $backup_name);
|
|
$zip->create($plugin_dir, PCLZIP_OPT_REMOVE_PATH, dirname($plugin_dir));
|
|
|
|
return $backup_dir . $backup_name;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extended error handling and logging
|
|
*/
|
|
trait Enhanced_Logging {
|
|
/**
|
|
* Log levels for different types of messages
|
|
*/
|
|
private $log_levels = ['error', 'warning', 'info', 'debug'];
|
|
|
|
/**
|
|
* Enhanced logging with levels and context
|
|
*
|
|
* @param string $level Log level
|
|
* @param string $message Message to log
|
|
* @param array $context Additional context
|
|
*/
|
|
private function enhanced_log($level, $message, $context = []) {
|
|
if (!in_array($level, $this->log_levels)) {
|
|
$level = 'info';
|
|
}
|
|
|
|
$log_entry = [
|
|
'timestamp' => current_time('mysql'),
|
|
'level' => $level,
|
|
'message' => $message,
|
|
'plugin' => $this->plugin_slug,
|
|
'version' => $this->plugin_version,
|
|
'context' => $context
|
|
];
|
|
|
|
// Store in WordPress options for admin viewing
|
|
$logs = get_option('secure_updates_logs_' . $this->plugin_slug, []);
|
|
array_unshift($logs, $log_entry);
|
|
$logs = array_slice($logs, 0, 100); // Keep last 100 logs
|
|
update_option('secure_updates_logs_' . $this->plugin_slug, $logs);
|
|
|
|
// Also write to debug.log if enabled
|
|
if (defined('WP_DEBUG') && WP_DEBUG) {
|
|
error_log(
|
|
sprintf(
|
|
'[Secure Updates Library] [%s] %s: %s | Context: %s',
|
|
strtoupper($level),
|
|
$this->plugin_slug,
|
|
$message,
|
|
json_encode($context)
|
|
)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Health monitoring and diagnostics
|
|
*/
|
|
trait Health_Monitoring {
|
|
/**
|
|
* Run system health checks
|
|
*
|
|
* @return array Health check results
|
|
*/
|
|
public function check_system_health() {
|
|
$results = [];
|
|
|
|
// Check WordPress version compatibility
|
|
$results['wp_version'] = [
|
|
'status' => version_compare(get_bloginfo('version'), '5.0', '>='),
|
|
'message' => sprintf(
|
|
__('WordPress version %s', 'secure-updates-library'),
|
|
get_bloginfo('version')
|
|
)
|
|
];
|
|
|
|
// Check PHP version
|
|
$results['php_version'] = [
|
|
'status' => version_compare(PHP_VERSION, '5.6', '>='),
|
|
'message' => sprintf(
|
|
__('PHP version %s', 'secure-updates-library'),
|
|
PHP_VERSION
|
|
)
|
|
];
|
|
|
|
// Check SSL/TLS capabilities
|
|
$results['ssl_support'] = [
|
|
'status' => extension_loaded('openssl'),
|
|
'message' => __('OpenSSL extension', 'secure-updates-library')
|
|
];
|
|
|
|
// Check write permissions
|
|
$plugin_dir = plugin_dir_path($this->get_main_plugin_file());
|
|
$results['write_permissions'] = [
|
|
'status' => wp_is_writable($plugin_dir),
|
|
'message' => sprintf(
|
|
__('Write permissions for %s', 'secure-updates-library'),
|
|
$plugin_dir
|
|
)
|
|
];
|
|
|
|
// Check server connection
|
|
$connection_test = $this->test_server_connection();
|
|
$results['server_connection'] = [
|
|
'status' => $connection_test['connection']['status'] === 'success',
|
|
'message' => __('Update server connection', 'secure-updates-library')
|
|
];
|
|
|
|
return $results;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Rate limiting for API requests
|
|
*/
|
|
trait Rate_Limiting {
|
|
/**
|
|
* Check and apply rate limiting
|
|
*
|
|
* @param string $endpoint Endpoint being accessed
|
|
* @return bool|WP_Error True if allowed, WP_Error if rate limited
|
|
*/
|
|
private function check_rate_limit($endpoint) {
|
|
$rate_limit_key = 'sup_rate_' . md5($this->plugin_slug . $endpoint);
|
|
$minute_limit = 30; // Maximum requests per minute
|
|
|
|
$current_count = get_transient($rate_limit_key);
|
|
|
|
if ($current_count === false) {
|
|
set_transient($rate_limit_key, 1, MINUTE_IN_SECONDS);
|
|
return true;
|
|
}
|
|
|
|
if ($current_count >= $minute_limit) {
|
|
return new WP_Error(
|
|
'rate_limited',
|
|
__('Too many requests. Please try again later.', 'secure-updates-library')
|
|
);
|
|
}
|
|
|
|
set_transient($rate_limit_key, $current_count + 1, MINUTE_IN_SECONDS);
|
|
return true;
|
|
}
|
|
}
|
|
}
|