'POST', 'callback' => 'prautoblogger_deploy_handle', 'permission_callback' => 'prautoblogger_deploy_check_auth', ] ); // Health check endpoint (GET) — useful for monitoring. register_rest_route( 'prautoblogger-deploy/v1', '/status', [ 'methods' => 'GET', 'callback' => function () { return [ 'status' => 'ready', 'plugin' => is_dir( WP_PLUGIN_DIR . '/prautoblogger' ) ? 'installed' : 'not_installed', 'active' => is_plugin_active( 'prautoblogger/prautoblogger.php' ), 'version' => prautoblogger_deploy_get_installed_version(), ]; }, 'permission_callback' => 'prautoblogger_deploy_check_auth', ] ); } /** * Validate the deploy key from the request header. * Uses hash_equals() for timing-safe comparison. */ function prautoblogger_deploy_check_auth( WP_REST_Request $request ): bool { $key = $request->get_header( 'X-Deploy-Key' ); if ( null === $key || '' === $key ) { return false; } return hash_equals( (string) PRAUTOBLOGGER_DEPLOY_KEY, $key ); } /** * Handle a deploy request: receive zip, validate, extract, activate. */ function prautoblogger_deploy_handle( WP_REST_Request $request ): WP_REST_Response|WP_Error { // Rate limit: 1 deploy per 60 seconds. $lock = get_transient( 'prautoblogger_deploy_lock' ); if ( false !== $lock ) { return new WP_Error( 'rate_limited', 'Deploy in progress or recently completed. Wait 60 seconds.', [ 'status' => 429 ] ); } set_transient( 'prautoblogger_deploy_lock', time(), 60 ); // 1. Validate uploaded file exists. $files = $request->get_file_params(); if ( empty( $files['plugin']['tmp_name'] ) || ! is_uploaded_file( $files['plugin']['tmp_name'] ) ) { delete_transient( 'prautoblogger_deploy_lock' ); return new WP_Error( 'no_file', 'No plugin zip uploaded.', [ 'status' => 400 ] ); } $tmp_file = $files['plugin']['tmp_name']; // 2. Validate it's a real zip and contains our plugin. $zip = new ZipArchive(); if ( true !== $zip->open( $tmp_file ) ) { delete_transient( 'prautoblogger_deploy_lock' ); return new WP_Error( 'invalid_zip', 'Uploaded file is not a valid zip.', [ 'status' => 400 ] ); } $has_main_file = false; for ( $i = 0; $i < $zip->numFiles; $i++ ) { if ( 'prautoblogger/prautoblogger.php' === $zip->getNameIndex( $i ) ) { $has_main_file = true; break; } } if ( ! $has_main_file ) { $zip->close(); delete_transient( 'prautoblogger_deploy_lock' ); return new WP_Error( 'invalid_plugin', 'Zip must contain prautoblogger/prautoblogger.php at root.', [ 'status' => 400 ] ); } // 3. Read version from the incoming zip before extraction. $incoming_main = $zip->getFromName( 'prautoblogger/prautoblogger.php' ); $incoming_version = 'unknown'; if ( $incoming_main && preg_match( '/Version:\s*([0-9]+\.[0-9]+\.[0-9]+)/', $incoming_main, $m ) ) { $incoming_version = $m[1]; } $plugin_dir = WP_PLUGIN_DIR . '/prautoblogger'; $backup_dir = null; // 4. Deactivate plugin before file operations (prevent fatal errors). $was_active = is_plugin_active( 'prautoblogger/prautoblogger.php' ); if ( $was_active ) { deactivate_plugins( 'prautoblogger/prautoblogger.php', true ); } // 5. Backup current installation if it exists. if ( is_dir( $plugin_dir ) ) { $backup_dir = WP_PLUGIN_DIR . '/prautoblogger-backup-' . time(); if ( ! rename( $plugin_dir, $backup_dir ) ) { $zip->close(); // Re-activate if we deactivated. if ( $was_active ) { activate_plugin( 'prautoblogger/prautoblogger.php' ); } delete_transient( 'prautoblogger_deploy_lock' ); return new WP_Error( 'backup_failed', 'Could not backup current plugin.', [ 'status' => 500 ] ); } } // 6. Extract the zip. $extract_ok = $zip->extractTo( WP_PLUGIN_DIR ); $zip->close(); if ( ! $extract_ok || ! file_exists( $plugin_dir . '/prautoblogger.php' ) ) { // Rollback: restore backup. if ( $backup_dir && is_dir( $backup_dir ) ) { rename( $backup_dir, $plugin_dir ); } if ( $was_active ) { activate_plugin( 'prautoblogger/prautoblogger.php' ); } delete_transient( 'prautoblogger_deploy_lock' ); error_log( '[PRAutoBlogger Deploy] Extraction failed. Rolled back to previous version.' ); return new WP_Error( 'extract_failed', 'Zip extraction failed. Rolled back.', [ 'status' => 500 ] ); } // 7. Activate plugin. $activation_result = activate_plugin( 'prautoblogger/prautoblogger.php' ); if ( is_wp_error( $activation_result ) ) { // Activation failed — rollback. prautoblogger_deploy_rmdir_recursive( $plugin_dir ); if ( $backup_dir && is_dir( $backup_dir ) ) { rename( $backup_dir, $plugin_dir ); activate_plugin( 'prautoblogger/prautoblogger.php' ); } delete_transient( 'prautoblogger_deploy_lock' ); error_log( '[PRAutoBlogger Deploy] Activation failed: ' . $activation_result->get_error_message() . '. Rolled back.' ); return new WP_Error( 'activation_failed', 'Plugin activation failed: ' . $activation_result->get_error_message() . '. Rolled back.', [ 'status' => 500 ] ); } // 8. Clean up backup. if ( $backup_dir && is_dir( $backup_dir ) ) { prautoblogger_deploy_rmdir_recursive( $backup_dir ); } $old_version = prautoblogger_deploy_get_installed_version(); error_log( sprintf( '[PRAutoBlogger Deploy] Successfully deployed v%s (was: %s) at %s', $incoming_version, $old_version ?: 'fresh install', gmdate( 'Y-m-d H:i:s' ) ) ); return new WP_REST_Response( [ 'success' => true, 'version' => $incoming_version, 'message' => "PRAutoBlogger v{$incoming_version} deployed and activated.", ], 200 ); } /** * Get the currently installed plugin version from the main file header. */ function prautoblogger_deploy_get_installed_version(): string { $main_file = WP_PLUGIN_DIR . '/prautoblogger/prautoblogger.php'; if ( ! file_exists( $main_file ) ) { return ''; } $data = get_file_data( $main_file, [ 'Version' => 'Version' ] ); return $data['Version'] ?? ''; } /** * Recursively delete a directory. */ function prautoblogger_deploy_rmdir_recursive( string $dir ): void { if ( ! is_dir( $dir ) ) { return; } $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $dir, RecursiveDirectoryIterator::SKIP_DOTS ), RecursiveIteratorIterator::CHILD_FIRST ); foreach ( $iterator as $file ) { if ( $file->isDir() ) { rmdir( $file->getPathname() ); } else { unlink( $file->getPathname() ); } } rmdir( $dir ); }