Because cats have 9 lives, but servers don't - so they need backup-restore! Complete backup solution with S3/MinIO support. - Full WordPress backup (files + database) - S3 / MinIO / S3-compatible storage backends - Scheduled automatic backups - Disaster recovery / one-click restore - Backup integrity validation - Cat-themed admin interface Includes build.sh and .distignore for WordPress-installable release ZIPs.
685 lines
23 KiB
PHP
685 lines
23 KiB
PHP
<?php
|
|
/**
|
|
* REST API Endpoints
|
|
*
|
|
* Defines REST API endpoints for TigerStyle Life9
|
|
*
|
|
* @package TigerStyleLife9
|
|
* @since 1.0.0
|
|
*/
|
|
|
|
// Exit if accessed directly
|
|
if (!defined('ABSPATH')) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* REST API endpoints class
|
|
*
|
|
* @since 1.0.0
|
|
*/
|
|
class TigerStyle_Life9_REST_Endpoints {
|
|
|
|
/**
|
|
* API namespace
|
|
*
|
|
* @var string
|
|
*/
|
|
private $namespace = 'tigerstyle-life9/v1';
|
|
|
|
/**
|
|
* Security instance
|
|
*
|
|
* @var TigerStyle_Life9_Security
|
|
*/
|
|
private $security;
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
public function __construct() {
|
|
$this->security = tigerstyle_life9()->get_security();
|
|
}
|
|
|
|
/**
|
|
* Register all REST API routes
|
|
*/
|
|
public function register_routes() {
|
|
// Dashboard endpoints
|
|
register_rest_route($this->namespace, '/dashboard/stats', [
|
|
'methods' => 'GET',
|
|
'callback' => [$this, 'get_dashboard_stats'],
|
|
'permission_callback' => [$this, 'check_permissions']
|
|
]);
|
|
|
|
// Backup endpoints
|
|
register_rest_route($this->namespace, '/backups', [
|
|
[
|
|
'methods' => 'GET',
|
|
'callback' => [$this, 'get_backups'],
|
|
'permission_callback' => [$this, 'check_permissions'],
|
|
'args' => $this->get_backups_args()
|
|
],
|
|
[
|
|
'methods' => 'POST',
|
|
'callback' => [$this, 'create_backup'],
|
|
'permission_callback' => [$this, 'check_permissions'],
|
|
'args' => $this->create_backup_args()
|
|
]
|
|
]);
|
|
|
|
register_rest_route($this->namespace, '/backups/(?P<id>\d+)', [
|
|
[
|
|
'methods' => 'GET',
|
|
'callback' => [$this, 'get_backup'],
|
|
'permission_callback' => [$this, 'check_permissions']
|
|
],
|
|
[
|
|
'methods' => 'DELETE',
|
|
'callback' => [$this, 'delete_backup'],
|
|
'permission_callback' => [$this, 'check_permissions']
|
|
]
|
|
]);
|
|
|
|
register_rest_route($this->namespace, '/backups/(?P<id>\d+)/status', [
|
|
'methods' => 'GET',
|
|
'callback' => [$this, 'get_backup_status'],
|
|
'permission_callback' => [$this, 'check_permissions']
|
|
]);
|
|
|
|
register_rest_route($this->namespace, '/backups/(?P<id>\d+)/cancel', [
|
|
'methods' => 'POST',
|
|
'callback' => [$this, 'cancel_backup'],
|
|
'permission_callback' => [$this, 'check_permissions']
|
|
]);
|
|
|
|
register_rest_route($this->namespace, '/backups/(?P<id>\d+)/download', [
|
|
'methods' => 'POST',
|
|
'callback' => [$this, 'download_backup'],
|
|
'permission_callback' => [$this, 'check_permissions']
|
|
]);
|
|
|
|
// Restore endpoints
|
|
register_rest_route($this->namespace, '/restore', [
|
|
'methods' => 'POST',
|
|
'callback' => [$this, 'start_restore'],
|
|
'permission_callback' => [$this, 'check_permissions'],
|
|
'args' => $this->restore_args()
|
|
]);
|
|
|
|
register_rest_route($this->namespace, '/restore/(?P<id>\d+)/status', [
|
|
'methods' => 'GET',
|
|
'callback' => [$this, 'get_restore_status'],
|
|
'permission_callback' => [$this, 'check_permissions']
|
|
]);
|
|
|
|
// File management endpoints
|
|
register_rest_route($this->namespace, '/files/browse', [
|
|
'methods' => 'POST',
|
|
'callback' => [$this, 'browse_files'],
|
|
'permission_callback' => [$this, 'check_permissions'],
|
|
'args' => [
|
|
'path' => [
|
|
'required' => false,
|
|
'type' => 'string',
|
|
'default' => ABSPATH,
|
|
'sanitize_callback' => [$this, 'sanitize_path']
|
|
],
|
|
'show_hidden' => [
|
|
'required' => false,
|
|
'type' => 'boolean',
|
|
'default' => false
|
|
]
|
|
]
|
|
]);
|
|
|
|
register_rest_route($this->namespace, '/files/preview', [
|
|
'methods' => 'POST',
|
|
'callback' => [$this, 'preview_file'],
|
|
'permission_callback' => [$this, 'check_permissions'],
|
|
'args' => [
|
|
'path' => [
|
|
'required' => true,
|
|
'type' => 'string',
|
|
'sanitize_callback' => [$this, 'sanitize_path'],
|
|
'validate_callback' => [$this, 'validate_file_path']
|
|
]
|
|
]
|
|
]);
|
|
|
|
// Settings endpoints
|
|
register_rest_route($this->namespace, '/settings', [
|
|
[
|
|
'methods' => 'GET',
|
|
'callback' => [$this, 'get_settings'],
|
|
'permission_callback' => [$this, 'check_permissions']
|
|
],
|
|
[
|
|
'methods' => 'POST',
|
|
'callback' => [$this, 'update_settings'],
|
|
'permission_callback' => [$this, 'check_permissions']
|
|
]
|
|
]);
|
|
|
|
// System endpoints
|
|
register_rest_route($this->namespace, '/system/status', [
|
|
'methods' => 'GET',
|
|
'callback' => [$this, 'get_system_status'],
|
|
'permission_callback' => [$this, 'check_permissions']
|
|
]);
|
|
|
|
// Progress streaming endpoint
|
|
register_rest_route($this->namespace, '/progress-stream', [
|
|
'methods' => 'GET',
|
|
'callback' => [$this, 'progress_stream'],
|
|
'permission_callback' => [$this, 'check_permissions']
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Check user permissions
|
|
*
|
|
* @param WP_REST_Request $request Request object
|
|
* @return bool
|
|
*/
|
|
public function check_permissions($request) {
|
|
if (!current_user_can('manage_options')) {
|
|
return false;
|
|
}
|
|
|
|
// Verify nonce for state-changing operations
|
|
$method = $request->get_method();
|
|
if (in_array($method, ['POST', 'PUT', 'DELETE', 'PATCH'])) {
|
|
$nonce = $request->get_header('X-WP-Nonce');
|
|
if (!wp_verify_nonce($nonce, 'wp_rest')) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get dashboard statistics
|
|
*
|
|
* @param WP_REST_Request $request Request object
|
|
* @return WP_REST_Response
|
|
*/
|
|
public function get_dashboard_stats($request) {
|
|
try {
|
|
global $wpdb;
|
|
|
|
$stats = $wpdb->get_row(
|
|
"SELECT
|
|
COUNT(*) as total_backups,
|
|
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as successful_backups,
|
|
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed_backups,
|
|
SUM(CASE WHEN status = 'completed' THEN COALESCE(file_size, 0) ELSE 0 END) as total_size,
|
|
MAX(CASE WHEN status = 'completed' THEN created_at ELSE NULL END) as last_backup
|
|
FROM {$wpdb->prefix}tigerstyle_life9_backups"
|
|
);
|
|
|
|
$response_data = [
|
|
'totalBackups' => intval($stats->total_backups ?? 0),
|
|
'successfulBackups' => intval($stats->successful_backups ?? 0),
|
|
'failedBackups' => intval($stats->failed_backups ?? 0),
|
|
'totalSize' => intval($stats->total_size ?? 0),
|
|
'lastBackup' => $stats->last_backup
|
|
];
|
|
|
|
return rest_ensure_response(['success' => true, 'data' => $response_data]);
|
|
|
|
} catch (Exception $e) {
|
|
error_log('TigerStyle Life9: Dashboard stats error - ' . $e->getMessage());
|
|
return new WP_Error('stats_error', __('Failed to get dashboard statistics', 'tigerstyle-life9'), ['status' => 500]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get backups list
|
|
*
|
|
* @param WP_REST_Request $request Request object
|
|
* @return WP_REST_Response
|
|
*/
|
|
public function get_backups($request) {
|
|
try {
|
|
global $wpdb;
|
|
|
|
$limit = intval($request->get_param('limit') ?: 20);
|
|
$offset = intval($request->get_param('offset') ?: 0);
|
|
$status = $request->get_param('status');
|
|
|
|
$where_clause = '';
|
|
$where_params = [];
|
|
|
|
if ($status) {
|
|
$where_clause = 'WHERE status = %s';
|
|
$where_params[] = $status;
|
|
}
|
|
|
|
$backups = $wpdb->get_results($wpdb->prepare(
|
|
"SELECT * FROM {$wpdb->prefix}tigerstyle_life9_backups
|
|
{$where_clause}
|
|
ORDER BY created_at DESC
|
|
LIMIT %d OFFSET %d",
|
|
array_merge($where_params, [$limit, $offset])
|
|
));
|
|
|
|
$total = $wpdb->get_var($wpdb->prepare(
|
|
"SELECT COUNT(*) FROM {$wpdb->prefix}tigerstyle_life9_backups {$where_clause}",
|
|
$where_params
|
|
));
|
|
|
|
return rest_ensure_response([
|
|
'success' => true,
|
|
'data' => $backups,
|
|
'total' => intval($total),
|
|
'limit' => $limit,
|
|
'offset' => $offset
|
|
]);
|
|
|
|
} catch (Exception $e) {
|
|
error_log('TigerStyle Life9: Get backups error - ' . $e->getMessage());
|
|
return new WP_Error('get_backups_error', __('Failed to get backups', 'tigerstyle-life9'), ['status' => 500]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create new backup
|
|
*
|
|
* @param WP_REST_Request $request Request object
|
|
* @return WP_REST_Response
|
|
*/
|
|
public function create_backup($request) {
|
|
try {
|
|
$config = $request->get_json_params();
|
|
|
|
$sanitizer = new TigerStyle_Life9_Sanitizer();
|
|
$validator = new TigerStyle_Life9_Validator();
|
|
|
|
$clean_config = $sanitizer->sanitize_backup_config($config);
|
|
|
|
if (!$validator->validate_backup_config($clean_config)) {
|
|
return new WP_Error(
|
|
'invalid_config',
|
|
__('Invalid backup configuration', 'tigerstyle-life9'),
|
|
['status' => 400, 'errors' => $validator->get_errors()]
|
|
);
|
|
}
|
|
|
|
$backup_engine = new TigerStyle_Life9_Backup_Engine();
|
|
$backup_id = $backup_engine->start_backup($clean_config);
|
|
|
|
if ($backup_id) {
|
|
return rest_ensure_response([
|
|
'success' => true,
|
|
'data' => [
|
|
'backup_id' => $backup_id,
|
|
'message' => __('Backup started successfully', 'tigerstyle-life9'),
|
|
'status_url' => rest_url($this->namespace . '/backups/' . $backup_id . '/status')
|
|
]
|
|
]);
|
|
} else {
|
|
return new WP_Error('backup_start_failed', __('Failed to start backup', 'tigerstyle-life9'), ['status' => 500]);
|
|
}
|
|
|
|
} catch (Exception $e) {
|
|
error_log('TigerStyle Life9: Create backup error - ' . $e->getMessage());
|
|
return new WP_Error('backup_error', __('An error occurred while starting backup', 'tigerstyle-life9'), ['status' => 500]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get single backup
|
|
*
|
|
* @param WP_REST_Request $request Request object
|
|
* @return WP_REST_Response
|
|
*/
|
|
public function get_backup($request) {
|
|
$backup_id = intval($request->get_param('id'));
|
|
|
|
try {
|
|
global $wpdb;
|
|
|
|
$backup = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT * FROM {$wpdb->prefix}tigerstyle_life9_backups WHERE id = %d",
|
|
$backup_id
|
|
));
|
|
|
|
if (!$backup) {
|
|
return new WP_Error('backup_not_found', __('Backup not found', 'tigerstyle-life9'), ['status' => 404]);
|
|
}
|
|
|
|
return rest_ensure_response(['success' => true, 'data' => $backup]);
|
|
|
|
} catch (Exception $e) {
|
|
error_log('TigerStyle Life9: Get backup error - ' . $e->getMessage());
|
|
return new WP_Error('get_backup_error', __('Failed to get backup', 'tigerstyle-life9'), ['status' => 500]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete backup
|
|
*
|
|
* @param WP_REST_Request $request Request object
|
|
* @return WP_REST_Response
|
|
*/
|
|
public function delete_backup($request) {
|
|
$backup_id = intval($request->get_param('id'));
|
|
|
|
try {
|
|
global $wpdb;
|
|
|
|
$backup = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT * FROM {$wpdb->prefix}tigerstyle_life9_backups WHERE id = %d",
|
|
$backup_id
|
|
));
|
|
|
|
if (!$backup) {
|
|
return new WP_Error('backup_not_found', __('Backup not found', 'tigerstyle-life9'), ['status' => 404]);
|
|
}
|
|
|
|
// Delete file if exists
|
|
if ($backup->file_path && file_exists($backup->file_path)) {
|
|
$encryption = new TigerStyle_Life9_Encryption();
|
|
$encryption->secure_delete($backup->file_path);
|
|
}
|
|
|
|
// Delete from database
|
|
$wpdb->delete(
|
|
$wpdb->prefix . 'tigerstyle_life9_backups',
|
|
['id' => $backup_id],
|
|
['%d']
|
|
);
|
|
|
|
// Log the deletion
|
|
$this->security->log_security_event('backup_deleted', [
|
|
'backup_id' => $backup_id,
|
|
'backup_name' => $backup->name
|
|
]);
|
|
|
|
return rest_ensure_response([
|
|
'success' => true,
|
|
'data' => ['message' => __('Backup deleted successfully', 'tigerstyle-life9')]
|
|
]);
|
|
|
|
} catch (Exception $e) {
|
|
error_log('TigerStyle Life9: Delete backup error - ' . $e->getMessage());
|
|
return new WP_Error('delete_backup_error', __('Failed to delete backup', 'tigerstyle-life9'), ['status' => 500]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Browse files
|
|
*
|
|
* @param WP_REST_Request $request Request object
|
|
* @return WP_REST_Response
|
|
*/
|
|
public function browse_files($request) {
|
|
$path = $request->get_param('path');
|
|
$show_hidden = $request->get_param('show_hidden');
|
|
|
|
try {
|
|
$scanner = new TigerStyle_Life9_File_Scanner();
|
|
$files = $scanner->scan_directory($path, [
|
|
'show_hidden' => $show_hidden,
|
|
'max_depth' => 1
|
|
]);
|
|
|
|
return rest_ensure_response([
|
|
'success' => true,
|
|
'data' => [
|
|
'files' => $files,
|
|
'current_path' => $path
|
|
]
|
|
]);
|
|
|
|
} catch (Exception $e) {
|
|
error_log('TigerStyle Life9: Browse files error - ' . $e->getMessage());
|
|
return new WP_Error('browse_error', __('Failed to browse files', 'tigerstyle-life9'), ['status' => 500]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get system status
|
|
*
|
|
* @param WP_REST_Request $request Request object
|
|
* @return WP_REST_Response
|
|
*/
|
|
public function get_system_status($request) {
|
|
try {
|
|
$upload_dir = wp_upload_dir();
|
|
$backup_dir = $upload_dir['basedir'] . '/tigerstyle-life9';
|
|
|
|
$status = [
|
|
'php_version' => PHP_VERSION,
|
|
'php_version_ok' => version_compare(PHP_VERSION, '8.0', '>='),
|
|
'wp_version' => get_bloginfo('version'),
|
|
'wp_version_ok' => version_compare(get_bloginfo('version'), '6.0', '>='),
|
|
'available_space' => disk_free_space($backup_dir),
|
|
'disk_space_ok' => disk_free_space($backup_dir) > (1024 * 1024 * 1024), // 1GB
|
|
'permissions_ok' => is_writable($backup_dir),
|
|
'extensions' => [
|
|
'openssl' => extension_loaded('openssl'),
|
|
'zip' => extension_loaded('zip'),
|
|
'curl' => extension_loaded('curl'),
|
|
'json' => extension_loaded('json')
|
|
]
|
|
];
|
|
|
|
return rest_ensure_response(['success' => true, 'data' => $status]);
|
|
|
|
} catch (Exception $e) {
|
|
error_log('TigerStyle Life9: System status error - ' . $e->getMessage());
|
|
return new WP_Error('system_status_error', __('Failed to get system status', 'tigerstyle-life9'), ['status' => 500]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Progress streaming endpoint (Server-Sent Events)
|
|
*
|
|
* @param WP_REST_Request $request Request object
|
|
*/
|
|
public function progress_stream($request) {
|
|
// Set headers for Server-Sent Events
|
|
header('Content-Type: text/event-stream');
|
|
header('Cache-Control: no-cache');
|
|
header('Connection: keep-alive');
|
|
|
|
// Get backup ID from query
|
|
$backup_id = intval($request->get_param('backup_id'));
|
|
|
|
if (!$backup_id) {
|
|
echo "event: error\n";
|
|
echo "data: " . json_encode(['error' => 'Invalid backup ID']) . "\n\n";
|
|
exit;
|
|
}
|
|
|
|
// Stream progress updates
|
|
for ($i = 0; $i < 60; $i++) { // Max 60 seconds
|
|
global $wpdb;
|
|
|
|
$backup = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT * FROM {$wpdb->prefix}tigerstyle_life9_backups WHERE id = %d",
|
|
$backup_id
|
|
));
|
|
|
|
if ($backup) {
|
|
$progress_data = [
|
|
'backup_id' => $backup->id,
|
|
'status' => $backup->status,
|
|
'progress' => $this->calculate_progress($backup),
|
|
'message' => $this->get_status_message($backup->status)
|
|
];
|
|
|
|
echo "event: progress\n";
|
|
echo "data: " . json_encode($progress_data) . "\n\n";
|
|
|
|
if (in_array($backup->status, ['completed', 'failed', 'cancelled'])) {
|
|
echo "event: complete\n";
|
|
echo "data: " . json_encode($progress_data) . "\n\n";
|
|
break;
|
|
}
|
|
}
|
|
|
|
ob_flush();
|
|
flush();
|
|
sleep(1);
|
|
}
|
|
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Sanitize file path
|
|
*
|
|
* @param string $path File path
|
|
* @return string|false
|
|
*/
|
|
public function sanitize_path($path) {
|
|
return $this->security->validate_path($path, ABSPATH) ? $path : false;
|
|
}
|
|
|
|
/**
|
|
* Validate file path
|
|
*
|
|
* @param string $path File path
|
|
* @return bool
|
|
*/
|
|
public function validate_file_path($path) {
|
|
return $this->security->validate_path($path, ABSPATH) && file_exists($path);
|
|
}
|
|
|
|
/**
|
|
* Get backup creation arguments
|
|
*
|
|
* @return array
|
|
*/
|
|
private function create_backup_args() {
|
|
return [
|
|
'backup_name' => [
|
|
'required' => true,
|
|
'type' => 'string',
|
|
'sanitize_callback' => 'sanitize_text_field'
|
|
],
|
|
'backup_type' => [
|
|
'required' => false,
|
|
'type' => 'string',
|
|
'default' => 'full',
|
|
'enum' => ['full', 'files', 'database']
|
|
],
|
|
'include_files' => [
|
|
'required' => false,
|
|
'type' => 'boolean',
|
|
'default' => true
|
|
],
|
|
'include_database' => [
|
|
'required' => false,
|
|
'type' => 'boolean',
|
|
'default' => true
|
|
],
|
|
'compression_method' => [
|
|
'required' => false,
|
|
'type' => 'string',
|
|
'default' => 'zip',
|
|
'enum' => ['zip', 'tar', 'gzip', 'none']
|
|
]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get backups list arguments
|
|
*
|
|
* @return array
|
|
*/
|
|
private function get_backups_args() {
|
|
return [
|
|
'limit' => [
|
|
'required' => false,
|
|
'type' => 'integer',
|
|
'default' => 20,
|
|
'minimum' => 1,
|
|
'maximum' => 100
|
|
],
|
|
'offset' => [
|
|
'required' => false,
|
|
'type' => 'integer',
|
|
'default' => 0,
|
|
'minimum' => 0
|
|
],
|
|
'status' => [
|
|
'required' => false,
|
|
'type' => 'string',
|
|
'enum' => ['pending', 'running', 'completed', 'failed', 'cancelled']
|
|
]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get restore arguments
|
|
*
|
|
* @return array
|
|
*/
|
|
private function restore_args() {
|
|
return [
|
|
'backup_id' => [
|
|
'required' => true,
|
|
'type' => 'integer',
|
|
'minimum' => 1
|
|
],
|
|
'restore_files' => [
|
|
'required' => false,
|
|
'type' => 'boolean',
|
|
'default' => true
|
|
],
|
|
'restore_database' => [
|
|
'required' => false,
|
|
'type' => 'boolean',
|
|
'default' => true
|
|
],
|
|
'replace_urls' => [
|
|
'required' => false,
|
|
'type' => 'boolean',
|
|
'default' => false
|
|
]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Calculate backup progress
|
|
*
|
|
* @param object $backup Backup record
|
|
* @return int Progress percentage
|
|
*/
|
|
private function calculate_progress($backup) {
|
|
switch ($backup->status) {
|
|
case 'completed':
|
|
return 100;
|
|
case 'failed':
|
|
case 'cancelled':
|
|
return 0;
|
|
case 'running':
|
|
return 50; // This would be more sophisticated in real implementation
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get status message
|
|
*
|
|
* @param string $status Backup status
|
|
* @return string
|
|
*/
|
|
private function get_status_message($status) {
|
|
$messages = [
|
|
'pending' => __('Backup queued', 'tigerstyle-life9'),
|
|
'running' => __('Backup in progress', 'tigerstyle-life9'),
|
|
'completed' => __('Backup completed successfully', 'tigerstyle-life9'),
|
|
'failed' => __('Backup failed', 'tigerstyle-life9'),
|
|
'cancelled' => __('Backup cancelled', 'tigerstyle-life9')
|
|
];
|
|
|
|
return $messages[$status] ?? __('Unknown status', 'tigerstyle-life9');
|
|
}
|
|
} |