Sindbad~EG File Manager
<?php
/**
* REST API Webhook Controller.
*
* @package PopupMaker
* @copyright Copyright (c) 2024, Code Atlantic LLC
*/
namespace PopupMaker\RestAPI;
use WP_REST_Controller;
use WP_REST_Server;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
use function PopupMaker\plugin;
defined( 'ABSPATH' ) || exit;
/**
* Connect REST API Controller.
*
* Handles secure connection endpoints for Pro installation workflow.
* Implements multi-layer security with authentication, signature verification,
* and referrer validation.
*
* @since 1.21.0
*/
class Connect extends WP_REST_Controller {
/**
* Endpoint namespace.
*
* @var string
*/
protected $namespace = 'popup-maker/v2';
/**
* Route base.
*
* @var string
*/
protected $rest_base = 'connect';
/**
* Connect service instance.
*
* @var \PopupMaker\Services\Connect
*/
private $connect_service;
/**
* Constructor.
*/
public function __construct() {
$this->connect_service = plugin( 'connect' );
}
/**
* Register the routes for the connection endpoints.
*
* @return void
*/
public function register_routes() {
// POST /connect/install - Install Pro plugin via secure connection.
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/install',
[
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'install_webhook' ],
'permission_callback' => [ $this, 'webhook_permissions_check' ],
'args' => $this->get_install_webhook_args(),
],
]
);
// POST /connect/verify - Verify connection for testing.
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/verify',
[
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'verify_webhook' ],
'permission_callback' => [ $this, 'webhook_permissions_check' ],
'args' => [],
],
]
);
}
/**
* Handle secure install webhook.
*
* This endpoint receives secure requests from the upgrade server to install Pro.
* Multiple security layers are enforced:
* 1. User agent verification
* 2. Referrer domain validation (production only)
* 3. Bearer token authentication
* 4. HMAC signature verification
* 5. License validation
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_REST_Response|WP_Error Response object or WP_Error on failure.
*/
public function install_webhook( $request ) {
try {
// Validate the connection using multi-layer security.
$this->validate_secure_connection();
// Check if this is a verification request
$json_data = json_decode( file_get_contents( 'php://input' ), true );
if ( is_array( $json_data ) && isset( $json_data['action'] ) && 'verify' === $json_data['action'] ) {
$this->connect_service->debug_log( 'Processing webhook verification request', 'DEBUG' );
return new WP_REST_Response(
[
'success' => true,
'message' => __( 'Webhook verification successful.', 'popup-maker' ),
'verified' => true,
],
200
);
}
// Get webhook installation arguments.
$args = $this->get_webhook_install_args( $request );
// Validate license is active before proceeding.
if ( ! plugin( 'license' )->is_license_active() ) {
$this->connect_service->debug_log( 'License not active for webhook install', 'ERROR' );
return new WP_Error(
'license_inactive',
__( 'License must be active to install Pro.', 'popup-maker' ),
[ 'status' => 403 ]
);
}
// Install the plugin based on type.
switch ( $args['type'] ) {
case 'plugin':
return $this->install_plugin_via_webhook( $args );
default:
return new WP_Error(
'invalid_install_type',
__( 'Invalid installation type.', 'popup-maker' ),
[ 'status' => 400 ]
);
}
} catch ( \Exception $e ) {
$this->connect_service->debug_log( 'Webhook install failed: ' . $e->getMessage(), 'ERROR' );
return new WP_Error(
'webhook_install_failed',
$e->getMessage(),
[ 'status' => 500 ]
);
} finally {
// Only clean up token for actual installation, not verification
$json_data = json_decode( file_get_contents( 'php://input' ), true );
$is_verification = is_array( $json_data ) && isset( $json_data['action'] ) && 'verify' === $json_data['action'];
if ( ! $is_verification && ! $this->connect_service->debug_mode_enabled() ) {
$this->connect_service->debug_log( 'Cleaning up access token after successful installation', 'DEBUG' );
$this->clean_up_access_token();
} elseif ( $is_verification ) {
$this->connect_service->debug_log( 'Skipping token cleanup for verification request - token preserved for installation', 'DEBUG' );
}
}
}
/**
* Verify webhook connection for testing.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_REST_Response|WP_Error Response object or WP_Error on failure.
*/
public function verify_webhook( $request ) {
try {
// Validate the connection using multi-layer security.
$this->validate_secure_connection();
return new WP_REST_Response(
[
'success' => true,
'message' => __( 'Webhook connection verified successfully.', 'popup-maker' ),
],
200
);
} catch ( \Exception $e ) {
$this->connect_service->debug_log( 'Webhook verification failed: ' . $e->getMessage(), 'ERROR' );
return new WP_Error(
'webhook_verify_failed',
$e->getMessage(),
[ 'status' => 403 ]
);
}
}
/**
* Validate secure connection with multi-layer security.
*
* @throws \Exception If validation fails.
* @return void
*/
private function validate_secure_connection() {
// Layer 1: User Agent Verification.
$this->verify_user_agent();
// Layer 2: Referrer Domain Validation (production only).
if ( 'production' === wp_get_environment_type() ) {
$this->verify_referrer();
}
// Layer 3: Bearer Token Authentication.
$this->verify_authentication();
// Layer 4: HMAC Signature Verification.
$this->verify_signature();
$this->connect_service->debug_log( 'All security layers validated successfully', 'DEBUG' );
}
/**
* Verify user agent matches expected value.
*
* @throws \Exception If user agent is invalid.
* @return void
*/
private function verify_user_agent() {
$user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '';
$this->connect_service->debug_log( 'Received User Agent: ' . $user_agent, 'DEBUG' );
$this->connect_service->debug_log( 'Expected User Agent pattern: PopupMakerUpgrader/*', 'DEBUG' );
// Check if User-Agent starts with "PopupMakerUpgrader/" (version-flexible)
if ( ! empty( $user_agent ) && ! preg_match( '/^PopupMakerUpgrader\/\d+\.\d+\.\d+$/', $user_agent ) ) {
throw new \Exception(
// translators: %s is the user agent.
esc_html( sprintf( __( 'Invalid user agent: %s', 'popup-maker' ), $user_agent ) )
);
}
$this->connect_service->debug_log( 'User agent validation passed', 'DEBUG' );
}
/**
* Verify referrer domain is allowed.
*
* @throws \Exception If referrer is invalid.
* @return void
*/
private function verify_referrer() {
// Upgrade server sends X-Sending-Domain header
$referer = isset( $_SERVER['HTTP_X_SENDING_DOMAIN'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_SENDING_DOMAIN'] ) ) : '';
$this->connect_service->debug_log( 'Referrer validation - X-Sending-Domain: ' . $referer, 'DEBUG' );
if ( empty( $referer ) ) {
$this->connect_service->debug_log( 'Missing X-Sending-Domain header', 'ERROR' );
throw new \Exception( esc_html__( 'Missing referrer domain.', 'popup-maker' ) );
}
// Direct comparison - upgrade server sends the domain directly
if ( 'upgrade.wppopupmaker.com' !== $referer ) {
$this->connect_service->debug_log( 'Invalid referrer domain: ' . $referer, 'ERROR' );
throw new \Exception(
// translators: %s is the referrer domain.
esc_html( sprintf( __( 'Referrer domain not allowed: %s', 'popup-maker' ), $referer ) )
);
}
$this->connect_service->debug_log( 'Referrer validation passed', 'DEBUG' );
}
/**
* Verify bearer token authentication.
*
* @throws \Exception If authentication fails.
* @return void
*/
private function verify_authentication() {
// Identify request type for debugging
$json_data = json_decode( file_get_contents( 'php://input' ), true );
$request_type = 'unknown';
if ( is_array( $json_data ) ) {
if ( isset( $json_data['action'] ) && 'verify' === $json_data['action'] ) {
$request_type = 'verification';
} elseif ( isset( $json_data['download_url'] ) || isset( $json_data['file'] ) ) {
$request_type = 'installation';
}
}
// Add timing debug to track token lifecycle
$this->connect_service->debug_log( "Authentication verification for {$request_type} request started at: " . current_time( 'mysql' ), 'DEBUG' );
$stored_token = $this->connect_service->get_access_token();
$request_token = $this->connect_service->get_request_token();
// Validate authentication tokens.
if ( ! $stored_token || ! $request_token ) {
throw new \Exception( esc_html__( 'Missing authentication token.', 'popup-maker' ) );
}
if ( ! hash_equals( $stored_token, $request_token ) ) {
throw new \Exception( esc_html__( 'Invalid authentication token.', 'popup-maker' ) );
}
$this->connect_service->debug_log( 'Authentication verification passed', 'DEBUG' );
}
/**
* Verify HMAC signature.
*
* @throws \Exception If signature verification fails.
* @return void
*/
private function verify_signature() {
// Try both possible signature header names for compatibility
$signature_header = '';
if ( isset( $_SERVER['HTTP_X_CONTENTCONTROL_SIGNATURE'] ) ) {
$signature_header = sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_CONTENTCONTROL_SIGNATURE'] ) );
} elseif ( isset( $_SERVER['HTTP_X_POPUPMAKER_SIGNATURE'] ) ) {
$signature_header = sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_POPUPMAKER_SIGNATURE'] ) );
}
$this->connect_service->debug_log( 'Signature header: ' . substr( $signature_header, 0, 20 ) . '...', 'DEBUG' );
if ( empty( $signature_header ) ) {
$this->connect_service->debug_log( 'No signature header found - signature verification skipped', 'DEBUG' );
// Signature is optional for some endpoints, but recommended.
return;
}
$signature = sanitize_text_field( wp_unslash( $signature_header ) );
$token = $this->connect_service->get_access_token();
// Get the request data for signature calculation
$request_data = json_decode( file_get_contents( 'php://input' ), true );
// Fallback to $_POST if JSON body is empty
if ( empty( $request_data ) ) {
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$request_data = $_POST;
$this->connect_service->debug_log( 'Using $_POST for signature verification', 'DEBUG' );
} else {
$this->connect_service->debug_log( 'Using JSON body for signature verification', 'DEBUG' );
}
// Handle empty request data - upgrade server now sends {"action": "verify"} for verification calls
if ( empty( $request_data ) ) {
// For POST requests with empty body, use empty array for signature verification
$request_data = [];
$this->connect_service->debug_log( 'Using empty array for signature verification (fallback)', 'DEBUG' );
}
$this->connect_service->debug_log( 'Request data for signature: ' . wp_json_encode( $request_data ), 'DEBUG' );
$expected_signature = $this->connect_service->generate_hash( $request_data, $token );
$this->connect_service->debug_log( 'Expected signature: ' . substr( $expected_signature, 0, 20 ) . '...', 'DEBUG' );
$this->connect_service->debug_log( 'Received signature: ' . substr( $signature, 0, 20 ) . '...', 'DEBUG' );
if ( ! hash_equals( $expected_signature, $signature ) ) {
$this->connect_service->debug_log( "Signature mismatch:\nReceived: " . $signature . "\nExpected: " . $expected_signature . "\nData: " . wp_json_encode( $request_data ), 'ERROR' );
throw new \Exception( esc_html__( 'Invalid request signature.', 'popup-maker' ) );
}
$this->connect_service->debug_log( 'Signature verification passed', 'DEBUG' );
}
/**
* Get webhook installation arguments from request.
*
* @param WP_REST_Request $request The request object.
* @return array<string,mixed> Validated installation arguments.
* @throws \Exception If required arguments are missing or invalid.
*/
private function get_webhook_install_args( $request ) {
// Try multiple parameter names and sources to handle different formats
$args = [
// Map upgrade server parameter names to our internal names
'file' => $this->get_param_from_multiple_sources( $request, 'download_url' )
?: $this->get_param_from_multiple_sources( $request, 'file' ),
'type' => $this->get_param_from_multiple_sources( $request, 'type' ) ?: 'plugin',
'slug' => $this->get_param_from_multiple_sources( $request, 'plugin_slug' )
?: $this->get_param_from_multiple_sources( $request, 'slug' ),
'force' => (bool) ( $this->get_param_from_multiple_sources( $request, 'force_update' )
?: $this->get_param_from_multiple_sources( $request, 'force' ) ),
];
$this->connect_service->debug_log( 'Webhook install args: ' . wp_json_encode( $args, JSON_PRETTY_PRINT ), 'DEBUG' );
// Validate required parameters.
if ( empty( $args['file'] ) || empty( $args['slug'] ) ) {
$this->connect_service->debug_log( 'Missing required parameters - file: ' . ( $args['file'] ?: 'MISSING' ) . ', slug: ' . ( $args['slug'] ?: 'MISSING' ), 'ERROR' );
throw new \Exception( esc_html__( 'Missing required installation parameters.', 'popup-maker' ) );
}
// Validate installation type.
if ( ! in_array( $args['type'], [ 'plugin', 'theme' ], true ) ) {
throw new \Exception( esc_html__( 'Invalid installation type.', 'popup-maker' ) );
}
return $args;
}
/**
* Get parameter from multiple sources (REST params, JSON body, $_REQUEST).
*
* @param WP_REST_Request $request The request object.
* @param string $param_name Parameter name.
* @return mixed Parameter value or null if not found.
*/
private function get_param_from_multiple_sources( $request, $param_name ) {
// First try REST API parameters.
$value = $request->get_param( $param_name );
if ( ! empty( $value ) ) {
return $value;
}
// Try JSON body.
$json_data = json_decode( file_get_contents( 'php://input' ), true );
if ( is_array( $json_data ) && isset( $json_data[ $param_name ] ) ) {
return $json_data[ $param_name ];
}
// Fallback to $_REQUEST.
// phpcs:disable WordPress.Security.NonceVerification.Recommended
if ( isset( $_REQUEST[ $param_name ] ) ) {
return sanitize_text_field( wp_unslash( $_REQUEST[ $param_name ] ) );
}
// phpcs:enable WordPress.Security.NonceVerification.Recommended
return null;
}
/**
* Install plugin via webhook.
*
* @param array<string,mixed> $args Installation arguments.
* @return WP_REST_Response|WP_Error Response object or WP_Error on failure.
*/
private function install_plugin_via_webhook( $args ) {
$this->connect_service->debug_log( 'Installing plugin via webhook...', 'DEBUG' );
// Check if plugin is already active and not forcing reinstall.
$plugin_file = "{$args['slug']}/{$args['slug']}.php";
if ( ! $args['force'] && is_plugin_active( $plugin_file ) ) {
$this->connect_service->debug_log( 'Plugin already installed and active', 'DEBUG' );
return new WP_REST_Response(
[
'success' => true,
'message' => __( 'Plugin is already installed and activated.', 'popup-maker' ),
],
200
);
}
// Load required WordPress files for plugin installation in REST context
if ( ! function_exists( 'request_filesystem_credentials' ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
}
if ( ! function_exists( 'get_plugin_data' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
if ( ! class_exists( 'WP_Upgrader' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
}
if ( ! class_exists( 'Plugin_Upgrader' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-plugin-upgrader.php';
}
$this->connect_service->debug_log( 'Required WordPress files loaded for plugin installation', 'DEBUG' );
// Get the upgrader service and install the plugin.
$upgrader = plugin( 'upgrader' );
$result = $upgrader->install_plugin( $args['file'] );
if ( is_wp_error( $result ) ) {
$this->connect_service->debug_log( 'Plugin installation failed: ' . $result->get_error_message(), 'ERROR' );
return new WP_Error(
'plugin_install_failed',
$result->get_error_message(),
[ 'status' => 500 ]
);
}
$this->connect_service->debug_log( 'Plugin installed successfully', 'DEBUG' );
return new WP_REST_Response(
[
'success' => true,
'message' => __( 'Plugin installed and activated successfully.', 'popup-maker' ),
],
200
);
}
/**
* Clean up access token after use.
*
* @return void
*/
private function clean_up_access_token() {
$this->connect_service->debug_log( 'Cleaning up access token', 'DEBUG' );
delete_site_transient( \PopupMaker\Services\Connect::TOKEN_OPTION_NAME );
}
/**
* Check webhook permissions.
*
* This is a specialized permission check that doesn't rely on WordPress user capabilities
* since webhook requests come from external servers. Instead, it validates the secure
* connection through the multi-layer security system.
*
* @param WP_REST_Request $request Full data about the request.
* @return true|WP_Error True if authorized, WP_Error otherwise.
*/
public function webhook_permissions_check( $request ) {
// For webhook endpoints, we don't check user capabilities since these are
// server-to-server requests. The security is handled through the multi-layer
// validation system in the actual endpoint methods.
return true;
}
/**
* Get the arguments for install webhook endpoint.
*
* @return array<string,array<string,mixed>>
*/
public function get_install_webhook_args() {
return [
'file' => [
'description' => __( 'Download URL for the plugin file.', 'popup-maker' ),
'type' => 'string',
'required' => false,
'format' => 'uri',
'sanitize_callback' => 'esc_url_raw',
'validate_callback' => function ( $param ) {
if ( empty( $param ) || ! filter_var( $param, FILTER_VALIDATE_URL ) ) {
return new WP_Error(
'invalid_file_url',
__( 'Valid file URL is required.', 'popup-maker' ),
[ 'status' => 400 ]
);
}
return true;
},
],
'type' => [
'description' => __( 'Type of installation (plugin or theme).', 'popup-maker' ),
'type' => 'string',
'default' => 'plugin',
'enum' => [ 'plugin', 'theme' ],
'sanitize_callback' => 'sanitize_text_field',
],
'slug' => [
'description' => __( 'Plugin or theme slug.', 'popup-maker' ),
'type' => 'string',
'required' => false,
'sanitize_callback' => 'sanitize_text_field',
'validate_callback' => function ( $param ) {
if ( empty( $param ) || ! preg_match( '/^[a-z0-9-_]+$/', $param ) ) {
return new WP_Error(
'invalid_slug',
__( 'Valid slug is required (letters, numbers, hyphens, and underscores only).', 'popup-maker' ),
[ 'status' => 400 ]
);
}
return true;
},
],
'force' => [
'description' => __( 'Force reinstallation even if already installed.', 'popup-maker' ),
'type' => 'boolean',
'default' => false,
'sanitize_callback' => function ( $param ) {
return (bool) $param;
},
],
];
}
}
Sindbad File Manager Version 1.0, Coded By Sindbad EG ~ The Terrorists