tigerstyle-life9/includes/class-rest-endpoints.php
Ryan Malloy e92b7f8700 Initial commit: TigerStyle Life9 v1.0.0
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.
2026-05-27 14:32:00 -06:00

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');
}
}