Sindbad~EG File Manager

Current Path : /var/www/html/portal.sumar.com.py/wp-content/plugins/popup-maker/classes/RestAPI/
Upload File :
Current File : //var/www/html/portal.sumar.com.py/wp-content/plugins/popup-maker/classes/RestAPI/Connect.php

<?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