├── .gitignore ├── assets └── sslcommerz-logo.png ├── vendor ├── composer │ ├── installed.json │ ├── autoload_namespaces.php │ ├── autoload_classmap.php │ ├── autoload_psr4.php │ ├── installed.php │ ├── LICENSE │ ├── autoload_real.php │ ├── autoload_static.php │ ├── InstalledVersions.php │ └── ClassLoader.php └── autoload.php ├── composer.lock ├── composer.json ├── integration ├── SslcommerzGateway.php ├── SslcommerzConfig.php ├── Init.php └── SslcommerzOrderProcess.php ├── tutor-sslcommerz.php ├── languages └── tutor-sslcommerz.pot ├── readme.txt ├── README.md └── payments └── Sslcommerz.php /.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/sslcommerz-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hasinhayder/tutor-sslcommerz/HEAD/assets/sslcommerz-logo.png -------------------------------------------------------------------------------- /vendor/composer/installed.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [], 3 | "dev": true, 4 | "dev-package-names": [] 5 | } 6 | -------------------------------------------------------------------------------- /vendor/composer/autoload_namespaces.php: -------------------------------------------------------------------------------- 1 | $vendorDir . '/composer/InstalledVersions.php', 10 | ); 11 | -------------------------------------------------------------------------------- /vendor/composer/autoload_psr4.php: -------------------------------------------------------------------------------- 1 | array($baseDir . '/integration'), 10 | 'Payments\\Sslcommerz\\' => array($baseDir . '/payments'), 11 | ); 12 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "89023a050ad48aa51d80081e26c765cc", 8 | "packages": [], 9 | "packages-dev": [], 10 | "aliases": [], 11 | "minimum-stability": "stable", 12 | "stability-flags": {}, 13 | "prefer-stable": false, 14 | "prefer-lowest": false, 15 | "platform": {}, 16 | "platform-dev": {}, 17 | "plugin-api-version": "2.6.0" 18 | } 19 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.3", 3 | "name": "hasinhayder/tutor-sslcommerz", 4 | "description": "SSLCommerz payment gateway integration for Tutor LMS", 5 | "type": "wordpress-plugin", 6 | "license": "GPL-2.0-or-later", 7 | "authors": [ 8 | { 9 | "name": "Hasin Hayder", 10 | "email": "hasin@hasin.me", 11 | "homepage": "https://github.com/hasinhayder", 12 | "role": "Developer" 13 | } 14 | ], 15 | "homepage": "https://github.com/hasinhayder/tutor-sslcommerz", 16 | "autoload": { 17 | "psr-4": { 18 | "Payments\\Sslcommerz\\": "payments/", 19 | "TutorSslcommerz\\": "integration/" 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /vendor/composer/installed.php: -------------------------------------------------------------------------------- 1 | array( 3 | 'name' => 'themeum/tutor-custom-payment', 4 | 'pretty_version' => '1.0.0', 5 | 'version' => '1.0.0.0', 6 | 'reference' => null, 7 | 'type' => 'library', 8 | 'install_path' => __DIR__ . '/../../', 9 | 'aliases' => array(), 10 | 'dev' => true, 11 | ), 12 | 'versions' => array( 13 | 'themeum/tutor-custom-payment' => array( 14 | 'pretty_version' => '1.0.0', 15 | 'version' => '1.0.0.0', 16 | 'reference' => null, 17 | 'type' => 'library', 18 | 'install_path' => __DIR__ . '/../../', 19 | 'aliases' => array(), 20 | 'dev_requirement' => false, 21 | ), 22 | ), 23 | ); 24 | -------------------------------------------------------------------------------- /vendor/autoload.php: -------------------------------------------------------------------------------- 1 | register(true); 33 | 34 | return $loader; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /vendor/composer/autoload_static.php: -------------------------------------------------------------------------------- 1 | 11 | array ( 12 | 'TutorSslcommerz\\' => 16, 13 | ), 14 | 'P' => 15 | array ( 16 | 'Payments\\Sslcommerz\\' => 20, 17 | ), 18 | ); 19 | 20 | public static $prefixDirsPsr4 = array ( 21 | 'TutorSslcommerz\\' => 22 | array ( 23 | 0 => __DIR__ . '/../..' . '/integration', 24 | ), 25 | 'Payments\\Sslcommerz\\' => 26 | array ( 27 | 0 => __DIR__ . '/../..' . '/payments', 28 | ), 29 | ); 30 | 31 | public static $classMap = array ( 32 | 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', 33 | ); 34 | 35 | public static function getInitializer(ClassLoader $loader) 36 | { 37 | return \Closure::bind(function () use ($loader) { 38 | $loader->prefixLengthsPsr4 = ComposerStaticInit89023a050ad48aa51d80081e26c765cc::$prefixLengthsPsr4; 39 | $loader->prefixDirsPsr4 = ComposerStaticInit89023a050ad48aa51d80081e26c765cc::$prefixDirsPsr4; 40 | $loader->classMap = ComposerStaticInit89023a050ad48aa51d80081e26c765cc::$classMap; 41 | 42 | }, null, ClassLoader::class); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /integration/SslcommerzGateway.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/hasinhayder/tutor-sslcommerz 11 | */ 12 | 13 | namespace TutorSslcommerz; 14 | 15 | use Payments\Sslcommerz\Sslcommerz; 16 | use Tutor\PaymentGateways\GatewayBase; 17 | 18 | /** 19 | * SSLCommerz Payment Gateway Class 20 | * 21 | * This class extends GatewayBase to provide SSLCommerz payment gateway functionality 22 | * for Tutor LMS. It defines the gateway's directory structure, payment class, and 23 | * configuration class for seamless integration with the Tutor payment system. 24 | */ 25 | class SslcommerzGateway extends GatewayBase { 26 | 27 | /** 28 | * Get the root directory name for the SSLCommerz payment gateway source files. 29 | * 30 | * This method returns the directory name where SSLCommerz payment gateway 31 | * source files are located within the payments directory structure. 32 | * 33 | * @return string The directory name ('Sslcommerz'). 34 | */ 35 | public function get_root_dir_name(): string { 36 | return 'Sslcommerz'; 37 | } 38 | 39 | /** 40 | * Get the payment class name for SSLCommerz integration. 41 | * 42 | * Returns the fully qualified class name of the SSLCommerz payment processor 43 | * from the PaymentHub library, used for handling payment transactions. 44 | * 45 | * @return string The SSLCommerz payment class name. 46 | */ 47 | public function get_payment_class(): string { 48 | return Sslcommerz::class; 49 | } 50 | 51 | /** 52 | * Get the configuration class name for SSLCommerz gateway. 53 | * 54 | * Returns the fully qualified class name of the SSLCommerz configuration class 55 | * that manages gateway settings, credentials, and environment configuration. 56 | * 57 | * @return string The SSLCommerz configuration class name. 58 | */ 59 | public function get_config_class(): string { 60 | return SslcommerzConfig::class; 61 | } 62 | 63 | /** 64 | * Get the autoload file path for the SSLCommerz payment gateway. 65 | * 66 | * Returns an empty string as SSLCommerz uses Composer autoloading 67 | * and doesn't require a custom autoload file. 68 | * 69 | * @return string Empty string (Composer autoloading is used). 70 | */ 71 | public static function get_autoload_file(): string { 72 | return ''; 73 | } 74 | } -------------------------------------------------------------------------------- /tutor-sslcommerz.php: -------------------------------------------------------------------------------- 1 | init(); 48 | } 49 | 50 | /** 51 | * Initialize the plugin 52 | */ 53 | private function init(): void { 54 | $this->load_dependencies(); 55 | $this->define_constants(); 56 | $this->init_hooks(); 57 | } 58 | 59 | /** 60 | * Load plugin dependencies 61 | */ 62 | private function load_dependencies(): void { 63 | require_once __DIR__ . '/vendor/autoload.php'; 64 | 65 | if (!function_exists('is_plugin_active')) { 66 | require_once ABSPATH . 'wp-admin/includes/plugin.php'; 67 | } 68 | } 69 | 70 | /** 71 | * Define plugin constants 72 | */ 73 | private function define_constants(): void { 74 | define('TUTOR_SSLCOMMERZ_VERSION', '1.0.7'); 75 | define('TUTOR_SSLCOMMERZ_URL', plugin_dir_url(__FILE__)); 76 | define('TUTOR_SSLCOMMERZ_PATH', plugin_dir_path(__FILE__)); 77 | } 78 | 79 | /** 80 | * Initialize WordPress hooks 81 | */ 82 | private function init_hooks(): void { 83 | add_action('plugins_loaded', [$this, 'load_textdomain'], 1); 84 | add_action('plugins_loaded', [$this, 'init_gateway'], 100); 85 | } 86 | 87 | /** 88 | * Initialize the SSLCommerz payment gateway 89 | */ 90 | public function init_gateway(): void { 91 | //works with the free version of Tutor LMS 92 | if (is_plugin_active('tutor/tutor.php')) { 93 | new TutorSslcommerz\Init(); 94 | } 95 | } 96 | } 97 | 98 | // Initialize the plugin 99 | Tutor_SSLCommerz_Plugin::get_instance(); -------------------------------------------------------------------------------- /languages/tutor-sslcommerz.pot: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2025 Hasin Hayder 2 | # This file is distributed under the GPLv2 or later. 3 | msgid "" 4 | msgstr "" 5 | "Project-Id-Version: Tutor SSLCommerz Payment Gateway 1.0.3\n" 6 | "Report-Msgid-Bugs-To: https://github.com/hasinhayder/tutor-sslcommerz/issues\n" 7 | "POT-Creation-Date: 2025-01-27 10:00+0000\n" 8 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 9 | "Last-Translator: FULL NAME \n" 10 | "Language-Team: LANGUAGE \n" 11 | "MIME-Version: 1.0\n" 12 | "Content-Type: text/plain; charset=UTF-8\n" 13 | "Content-Transfer-Encoding: 8bit\n" 14 | "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" 15 | 16 | #: integration/Init.php:67 17 | msgid "SSLCommerz" 18 | msgstr "" 19 | 20 | #: integration/Init.php:72 21 | msgid "Environment" 22 | msgstr "" 23 | 24 | #: integration/Init.php:74 25 | msgid "Sandbox" 26 | msgstr "" 27 | 28 | #: integration/Init.php:75 29 | msgid "Live" 30 | msgstr "" 31 | 32 | #: integration/Init.php:81 33 | msgid "Store ID" 34 | msgstr "" 35 | 36 | #: integration/Init.php:84 37 | msgid "Your SSLCommerz Store ID. For sandbox, register at https://developer.sslcommerz.com/registration/" 38 | msgstr "" 39 | 40 | #: integration/Init.php:87 41 | msgid "Store Password" 42 | msgstr "" 43 | 44 | #: integration/Init.php:90 45 | msgid "Your SSLCommerz Store Password (NOT your merchant panel password)" 46 | msgstr "" 47 | 48 | #: integration/Init.php:93 49 | msgid "IPN URL" 50 | msgstr "" 51 | 52 | #: integration/Init.php:96 53 | msgid "Copy this URL and add it to your SSLCommerz merchant panel as IPN URL" 54 | msgstr "" 55 | 56 | #: integration/SslcommerzConfig.php:87 57 | msgid "Unable to load SSLCommerz gateway settings" 58 | msgstr "" 59 | 60 | #: payments/Sslcommerz.php:108 61 | msgid "Order ID is required for payment processing" 62 | msgstr "" 63 | 64 | #: payments/Sslcommerz.php:112 65 | msgid "Currency information is required for payment processing" 66 | msgstr "" 67 | 68 | #: payments/Sslcommerz.php:116 69 | msgid "Customer email is required for payment processing" 70 | msgstr "" 71 | 72 | #: payments/Sslcommerz.php:125 73 | msgid "Payment amount must be greater than zero" 74 | msgstr "" 75 | 76 | #: payments/Sslcommerz.php:137 77 | msgid "Course Purchase" 78 | msgstr "" 79 | 80 | #: payments/Sslcommerz.php:145 81 | msgid "Customer" 82 | msgstr "" 83 | 84 | #: payments/Sslcommerz.php:146 85 | msgid "N/A" 86 | msgstr "" 87 | 88 | #: payments/Sslcommerz.php:158 89 | msgid "Tutor LMS" 90 | msgstr "" 91 | 92 | #: payments/Sslcommerz.php:200 93 | msgid "Gateway URL not found in response" 94 | msgstr "" 95 | 96 | #: payments/Sslcommerz.php:203 97 | msgid "Unknown error occurred" 98 | msgstr "" 99 | 100 | #: payments/Sslcommerz.php:205 101 | msgid "SSLCommerz Payment Failed: " 102 | msgstr "" 103 | 104 | #: payments/Sslcommerz.php:230 105 | msgid "Failed to connect with SSLCommerz API: " 106 | msgstr "" 107 | 108 | #: payments/Sslcommerz.php:237 109 | msgid "Invalid JSON response from SSLCommerz API" 110 | msgstr "" 111 | 112 | #: payments/Sslcommerz.php:241 113 | msgid "Failed to connect with SSLCommerz API (HTTP " 114 | msgstr "" 115 | 116 | #: payments/Sslcommerz.php:280 117 | msgid "No transaction data received. IPN endpoint should only receive POST requests from SSLCommerz." 118 | msgstr "" 119 | 120 | #: payments/Sslcommerz.php:285 121 | msgid "Invalid transaction data: Missing transaction ID or status." 122 | msgstr "" 123 | 124 | #: payments/Sslcommerz.php:304 125 | msgid "Payment failed" 126 | msgstr "" 127 | 128 | #: payments/Sslcommerz.php:318 129 | msgid "Transaction validation with SSLCommerz API failed." 130 | msgstr "" 131 | 132 | #: payments/Sslcommerz.php:329 133 | msgid "Error processing payment: " 134 | msgstr "" -------------------------------------------------------------------------------- /integration/SslcommerzConfig.php: -------------------------------------------------------------------------------- 1 | 6 | * @link https://github.com/hasinhayder/tutor-sslcommerz 7 | */ 8 | 9 | namespace TutorSslcommerz; 10 | 11 | use Tutor\Ecommerce\Settings; 12 | use Ollyo\PaymentHub\Core\Payment\BaseConfig; 13 | use Tutor\PaymentGateways\Configs\PaymentUrlsTrait; 14 | use Ollyo\PaymentHub\Contracts\Payment\ConfigContract; 15 | 16 | /** 17 | * SslcommerzConfig class. 18 | * 19 | * This class is used to manage the configuration settings for the "SSLCommerz" gateway. It extends the `BaseConfig` 20 | * class and implements the `ConfigContract` interface. 21 | */ 22 | class SslcommerzConfig extends BaseConfig implements ConfigContract { 23 | 24 | /** 25 | * Configuration keys and their types for SSLCommerz gateway 26 | */ 27 | private const CONFIG_KEYS = [ 28 | 'environment' => 'select', 29 | 'store_id' => 'text', 30 | 'store_password' => 'secret_key', 31 | ]; 32 | 33 | /** 34 | * This trait provides methods to retrieve the URLs used in the payment process for success, cancellation, and webhook 35 | * notifications. 36 | */ 37 | use PaymentUrlsTrait; 38 | 39 | /** 40 | * Stores the environment setting for the payment gateway, such as 'sandbox' or 'live'. 41 | * 42 | * @var string 43 | */ 44 | private $environment; 45 | 46 | /** 47 | * Stores the SSLCommerz Store ID. 48 | * 49 | * @var string 50 | */ 51 | private $store_id; 52 | 53 | /** 54 | * Stores the SSLCommerz Store Password. 55 | * 56 | * @var string 57 | */ 58 | private $store_password; 59 | 60 | /** 61 | * The name of the payment gateway. 62 | * 63 | * @var string 64 | */ 65 | protected $name = 'sslcommerz'; 66 | 67 | /** 68 | * Constructor. 69 | * 70 | * Initializes the SSLCommerz configuration by loading gateway settings from Tutor's 71 | * payment gateway settings and populating the corresponding properties. 72 | * Excludes webhook_url as it's handled separately by the PaymentUrlsTrait. 73 | * 74 | * @throws \RuntimeException If unable to load gateway settings. 75 | */ 76 | public function __construct() { 77 | parent::__construct(); 78 | 79 | $settings = Settings::get_payment_gateway_settings('sslcommerz'); 80 | 81 | if (!is_array($settings)) { 82 | throw new \RuntimeException(__('Unable to load SSLCommerz gateway settings', 'tutor-sslcommerz')); 83 | } 84 | 85 | $config_keys = array_keys(self::CONFIG_KEYS); 86 | 87 | foreach ($config_keys as $key) { 88 | if ('webhook_url' !== $key) { 89 | $this->$key = $this->get_field_value($settings, $key); 90 | } 91 | } 92 | } 93 | 94 | /** 95 | * Retrieves the mode of the SSLCommerz payment gateway. 96 | * 97 | * @return string The mode of the payment gateway ('sandbox' or 'live'). 98 | */ 99 | public function getMode(): string { 100 | return $this->environment; 101 | } 102 | 103 | /** 104 | * Retrieves the Store ID for the SSLCommerz payment gateway. 105 | * 106 | * The Store ID is used to identify the merchant account in SSLCommerz API calls. 107 | * 108 | * @return string The configured Store ID. 109 | */ 110 | public function getStoreId(): string { 111 | return $this->store_id; 112 | } 113 | 114 | /** 115 | * Retrieves the Store Password for the SSLCommerz payment gateway. 116 | * 117 | * The Store Password is used for authentication in SSLCommerz API calls. 118 | * Note: This is NOT the merchant panel password. 119 | * 120 | * @return string The configured Store Password. 121 | */ 122 | public function getStorePassword(): string { 123 | return $this->store_password; 124 | } 125 | 126 | /** 127 | * Get the SSLCommerz API domain based on the configured environment. 128 | * 129 | * @return string The appropriate API domain URL for sandbox or live environment. 130 | */ 131 | public function getApiDomain(): string { 132 | return $this->environment === 'sandbox' 133 | ? 'https://sandbox.sslcommerz.com' 134 | : 'https://securepay.sslcommerz.com'; 135 | } 136 | 137 | /** 138 | * Checks if the SSLCommerz payment gateway is properly configured. 139 | * 140 | * Verifies that both the Store ID and Store Password are configured 141 | * and not empty, which are required for SSLCommerz API communication. 142 | * 143 | * @return bool True if both store ID and password are configured, false otherwise. 144 | */ 145 | public function is_configured(): bool { 146 | return !empty($this->store_id) && !empty($this->store_password); 147 | } 148 | 149 | /** 150 | * Creates and updates the SSLCommerz payment gateway configuration. 151 | * 152 | * This method extends the parent class configuration and adds SSLCommerz-specific 153 | * settings including store credentials and API domain for use by the payment gateway. 154 | * 155 | * @return void 156 | */ 157 | public function createConfig(): void { 158 | parent::createConfig(); 159 | 160 | $config = [ 161 | 'store_id' => $this->getStoreId(), 162 | 'store_password' => $this->getStorePassword(), 163 | 'api_domain' => $this->getApiDomain(), 164 | ]; 165 | 166 | $this->updateConfig($config); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /integration/Init.php: -------------------------------------------------------------------------------- 1 | 6 | * @link https://github.com/hasinhayder/tutor-sslcommerz 7 | */ 8 | 9 | namespace TutorSslcommerz; 10 | use TutorSslcommerz\SslcommerzOrderProcess; 11 | 12 | /** 13 | * Init class 14 | * 15 | * This class initializes the SSLCommerz Payment Gateway by registering hooks and filters for integrating with Tutor's payment 16 | * system. It adds the SSLCommerz method to Tutor's list of payment gateways. 17 | */ 18 | final class Init { 19 | /** 20 | * SSLCommerz gateway configuration array 21 | */ 22 | private const SSLCOMMERZ_GATEWAY_CONFIG = [ 23 | 'sslcommerz' => [ 24 | 'gateway_class' => SslcommerzGateway::class, 25 | 'config_class' => SslcommerzConfig::class, 26 | ], 27 | ]; 28 | 29 | /** 30 | * Constructor - Register hooks and filters 31 | * 32 | * Registers WordPress filters to integrate SSLCommerz payment gateway with Tutor LMS: 33 | * - tutor_gateways_with_class: Adds gateway class references for webhook processing 34 | * - tutor_payment_gateways_with_class: Adds gateway to checkout integration 35 | * - tutor_payment_gateways: Adds payment method settings to Tutor admin 36 | */ 37 | public function __construct() { 38 | add_filter('tutor_gateways_with_class', [self::class, 'payment_gateways_with_ref'], 10, 2); 39 | add_filter('tutor_payment_gateways_with_class', [self::class, 'add_payment_gateways']); 40 | add_filter('tutor_payment_gateways', [$this, 'add_tutor_sslcommerz_payment_method'], 100); 41 | add_filter('init', [$this, 'process_sslcommerz_form_submission']); 42 | } 43 | 44 | /** 45 | * Add SSLCommerz gateway class references for webhook processing 46 | * 47 | * Used by the tutor_gateways_with_class filter to provide class references 48 | * for SSLCommerz gateway when processing webhook notifications. 49 | * 50 | * @param array $value Existing gateway class references array. 51 | * @param string $gateway Gateway identifier being requested. 52 | * 53 | * @return array Modified gateway class references array. 54 | */ 55 | public static function payment_gateways_with_ref(array $value, string $gateway): array { 56 | if (isset(self::SSLCOMMERZ_GATEWAY_CONFIG[$gateway])) { 57 | $value[$gateway] = self::SSLCOMMERZ_GATEWAY_CONFIG[$gateway]; 58 | } 59 | 60 | return $value; 61 | } 62 | 63 | /** 64 | * Add SSLCommerz payment gateway to checkout integration 65 | * 66 | * Used by the tutor_payment_gateways_with_class filter to register 67 | * SSLCommerz gateway classes for checkout processing. 68 | * 69 | * @param array $gateways Existing payment gateways array. 70 | * 71 | * @return array Modified payment gateways array with SSLCommerz added. 72 | */ 73 | public static function add_payment_gateways(array $gateways): array { 74 | return $gateways + self::SSLCOMMERZ_GATEWAY_CONFIG; 75 | } 76 | 77 | /** 78 | * Add SSLCommerz payment method configuration to Tutor settings 79 | * 80 | * Defines the complete configuration structure for SSLCommerz payment method 81 | * including all required fields (environment, store credentials, webhook URL) 82 | * and adds it to Tutor's payment methods list for admin configuration. 83 | * 84 | * @param array $methods Existing Tutor payment methods array. 85 | * 86 | * @return array Modified payment methods array with SSLCommerz configuration added. 87 | */ 88 | public function add_tutor_sslcommerz_payment_method(array $methods): array { 89 | $sslcommerz_payment_method = [ 90 | 'name' => 'sslcommerz', 91 | 'label' => __('SSLCommerz', 'tutor-sslcommerz'), 92 | 'is_installed' => true, 93 | 'is_active' => true, 94 | 'icon' => TUTOR_SSLCOMMERZ_URL . 'assets/sslcommerz-logo.png', 95 | 'support_subscription' => false, // SSLCommerz doesn't support subscriptions 96 | 'fields' => [ 97 | [ 98 | 'name' => 'environment', 99 | 'type' => 'select', 100 | 'label' => __('Environment', 'tutor-sslcommerz'), 101 | 'options' => [ 102 | 'sandbox' => __('Sandbox', 'tutor-sslcommerz'), 103 | 'live' => __('Live', 'tutor-sslcommerz'), 104 | ], 105 | 'value' => 'sandbox', 106 | ], 107 | [ 108 | 'name' => 'store_id', 109 | 'type' => 'text', 110 | 'label' => __('Store ID', 'tutor-sslcommerz'), 111 | 'value' => '', 112 | 'desc' => __('Your SSLCommerz Store ID. For sandbox, register at https://developer.sslcommerz.com/registration/', 'tutor-sslcommerz'), 113 | ], 114 | [ 115 | 'name' => 'store_password', 116 | 'type' => 'secret_key', 117 | 'label' => __('Store Password', 'tutor-sslcommerz'), 118 | 'value' => '', 119 | 'desc' => __('Your SSLCommerz Store Password (NOT your merchant panel password)', 'tutor-sslcommerz'), 120 | ], 121 | [ 122 | 'name' => 'webhook_url', 123 | 'type' => 'webhook_url', 124 | 'label' => __('IPN URL', 'tutor-sslcommerz'), 125 | 'value' => '', 126 | 'desc' => __('Copy this URL and add it to your SSLCommerz merchant panel as IPN URL', 'tutor-sslcommerz'), 127 | ], 128 | ], 129 | ]; 130 | 131 | $methods[] = $sslcommerz_payment_method; 132 | return $methods; 133 | } 134 | 135 | /** 136 | * Handle template redirect for SSLCommerz payment gateway 137 | * 138 | * This method is hooked to the 'init' action to handle any necessary 139 | * template redirects related to SSLCommerz payment processing. 140 | * 141 | * @return void 142 | */ 143 | public function process_sslcommerz_form_submission(): void { 144 | $sslcommerz = new SslcommerzOrderProcess(); 145 | $sslcommerz->process_sslcommerz_form_submission(); 146 | } 147 | 148 | } -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | === Tutor SSLCommerz === 2 | Contributors: hasinhayder 3 | Tags: tutor, lms, sslcommerz, payment, bangladesh, e-commerce, gateway 4 | Requires at least: 5.3 5 | Tested up to: 6.8 6 | Requires PHP: 7.4 7 | Stable tag: 1.0.7 8 | License: GPLv2 or later 9 | License URI: https://www.gnu.org/licenses/gpl-2.0.html 10 | 11 | SSLCommerz payment gateway integration for Tutor LMS. This plugin enables one-time course payments through SSLCommerz, supporting multiple currencies and secure payment processing. 12 | 13 | == Description == 14 | 15 | Tutor SSLCommerz Payment Gateway integrates SSLCommerz, Bangladesh's leading payment gateway, with Tutor LMS to enable seamless course purchases. Accept payments from local and international customers using cards, mobile banking, and internet banking. 16 | 17 | = Features = 18 | 19 | * One-time payments for course purchases 20 | * Multi-currency support (BDT, USD, EUR, GBP, SGD, INR, MYR) 21 | * Sandbox and Live environments for testing and production 22 | * IPN (Instant Payment Notification) integration for automatic order updates 23 | * Secure payment processing with hash validation and transaction verification 24 | * All SSLCommerz payment methods (Cards, Mobile Banking, Internet Banking) 25 | * WordPress HTTP API for secure external communications 26 | * Comprehensive error handling and logging 27 | 28 | = Requirements = 29 | 30 | * WordPress 5.3 or higher 31 | * PHP 7.4 or higher 32 | * Tutor LMS (Free version) 33 | * SSLCommerz merchant account 34 | 35 | = How It Works = 36 | 37 | 1. Student initiates course purchase 38 | 2. Plugin sends payment request to SSLCommerz 39 | 3. Student completes payment on SSLCommerz secure page 40 | 4. SSLCommerz sends IPN notification to your site 41 | 5. Plugin validates transaction and updates order status 42 | 6. Student gains course access upon successful payment 43 | 44 | = Security Features = 45 | 46 | * Hash verification for callback signatures 47 | * Transaction validation through SSLCommerz API 48 | * Amount verification to prevent tampering 49 | * SSL-secured API communications 50 | 51 | == Installation == 52 | 53 | 1. Upload the plugin folder to `/wp-content/plugins` 54 | 2. Activate the plugin through the WordPress admin 55 | 3. Ensure Tutor LMS is installed and activated 56 | 4. Go to **Tutor LMS > Settings > Payments** 57 | 5. Enable SSLCommerz and configure settings 58 | 59 | = Configuration = 60 | 61 | **Step 1: Get SSLCommerz Credentials** 62 | 63 | *Sandbox (Testing):* 64 | 1. Register at https://developer.sslcommerz.com/registration/ 65 | 2. Receive Store ID and Store Password via email 66 | 67 | *Live (Production):* 68 | 1. Apply for merchant account at https://sslcommerz.com/ 69 | 2. Complete KYC verification 70 | 3. Get Store ID and Store Password from merchant panel 71 | 72 | **Step 2: Configure Plugin** 73 | 74 | 1. Go to **Tutor LMS > Settings > Payments** 75 | 2. Find **SSLCommerz** in payment gateways 76 | 3. Enable and configure: 77 | * **Environment**: Sandbox for testing, Live for production 78 | * **Store ID**: Your SSLCommerz Store ID 79 | * **Store Password**: Your Store Password (not login password) 80 | * **IPN URL**: Copy this URL 81 | 82 | **Step 3: Configure SSLCommerz Panel** 83 | 84 | 1. Login to SSLCommerz merchant panel 85 | 2. Go to IPN Settings 86 | 3. Add the IPN URL from plugin settings 87 | 4. Save settings 88 | 89 | == Frequently Asked Questions == 90 | 91 | = Do I need a SSLCommerz account? = 92 | 93 | Yes, you need a merchant account. Sign up at https://sslcommerz.com/ for live or https://developer.sslcommerz.com/registration/ for sandbox. 94 | 95 | = Does this support subscriptions? = 96 | 97 | No, only one-time payments are supported. SSLCommerz doesn't provide native recurring payment functionality. 98 | 99 | = Can I test before going live? = 100 | 101 | Yes, use Sandbox environment with test credentials. Test cards available in SSLCommerz documentation. 102 | 103 | = What currencies are supported? = 104 | 105 | BDT (primary), USD, EUR, GBP, SGD, INR, MYR. Non-BDT currencies are auto-converted to BDT at current rates. 106 | 107 | = How do I troubleshoot payment issues? = 108 | 109 | 1. Verify Store ID and Password are correct 110 | 2. Ensure IPN URL is configured in SSLCommerz panel 111 | 3. Check environment settings (Sandbox vs Live) 112 | 4. Enable WordPress debug logging 113 | 5. Verify SSL certificate on your site 114 | 115 | = What payment methods are supported? = 116 | 117 | All SSLCommerz methods: Credit/Debit Cards, Mobile Banking (bKash, Nagad, Rocket), Internet Banking, and others available in Bangladesh. 118 | 119 | = Is there a transaction fee? = 120 | 121 | Transaction fees depend on your SSLCommerz merchant agreement. Contact SSLCommerz for pricing details. 122 | 123 | = Can I process refunds? = 124 | 125 | Refunds must be processed manually through the SSLCommerz merchant panel. The plugin doesn't handle automatic refunds. 126 | 127 | == Changelog == 128 | 129 | = 1.0.7 = 130 | * Security: Added comprehensive input sanitization to prevent XSS attacks 131 | * Security: Implemented proper data validation for all user inputs 132 | * Security: Enhanced hash verification with sanitized inputs 133 | * Improvement: Enhanced error handling and logging 134 | * Improvement: Code organization and structure improvements 135 | 136 | = 1.0.6 = 137 | * Feature: Added complete internationalization (i18n) support 138 | * Feature: Created translation template (.pot file) 139 | * Improvement: Added languages directory for translation files 140 | * Improvement: Updated plugin constants and code structure 141 | * Improvement: Enhanced documentation with translation information 142 | 143 | = 1.0.5 = 144 | Minor Fixes 145 | 146 | = 1.0.4 = 147 | Minor Fixes 148 | 149 | = 1.0.3 = 150 | * Improvement: Replaced cURL with WordPress HTTP API for better compatibility 151 | * Improvement: Enhanced error handling and JSON validation 152 | * Improvement: More descriptive error messages 153 | 154 | = 1.0.2 = 155 | * Security: Fixed fatal errors in IPN handling 156 | * Security: Improved validation for webhook requests 157 | * Improved: Better error logging and debugging 158 | 159 | = 1.0.1 = 160 | * Fixed: Corrected payment amount sending (was sending 0) 161 | * Fixed: Updated to use correct Tutor LMS field names 162 | * Improved: Added payment amount validation 163 | 164 | = 1.0.0 = 165 | * Initial release 166 | * One-time payment support 167 | * Sandbox and Live environments 168 | * IPN integration 169 | * Multi-currency support 170 | * Transaction validation 171 | 172 | == Upgrade Notice == 173 | 174 | = 1.0.7 = 175 | Critical security update with input sanitization and validation improvements. Update immediately for enhanced security. 176 | 177 | = 1.0.6 = 178 | Adds internationalization support and improved documentation. 179 | 180 | = 1.0.5 = 181 | Minor Fixes 182 | 183 | = 1.0.4 = 184 | Minor Fixes 185 | 186 | = 1.0.3 = 187 | Replaces cURL with WordPress HTTP API for improved security and compatibility. 188 | 189 | = 1.0.2 = 190 | Fixes for IPN endpoint. Update immediately. 191 | 192 | = 1.0.1 = 193 | Fixes payment amount issue. Required for payments to work. 194 | 195 | == Support == 196 | 197 | For plugin issues: [GitHub Issues](https://github.com/hasinhayder/tutor-sslcommerz/issues) 198 | For SSLCommerz API: support@sslcommerz.com 199 | For Tutor LMS: Themeum support 200 | 201 | == Credits == 202 | 203 | Developed by Hasin Hayder 204 | Based on Tutor LMS Payment Gateway framework 205 | SSLCommerz API integration -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tutor SSLCommerz Payment Gateway 2 | 3 | **Author:** Hasin Hayder 4 | **GitHub:** [https://github.com/hasinhayder](https://github.com/hasinhayder) 5 | **Plugin Repository:** [https://github.com/hasinhayder/tutor-sslcommerz](https://github.com/hasinhayder/tutor-sslcommerz) 6 | 7 | SSLCommerz payment gateway integration for Tutor LMS. This plugin enables one-time course payments through SSLCommerz. 8 | 9 | ## Features 10 | 11 | - ✅ One-time payments for course purchases 12 | - ✅ Support for multiple currencies (BDT, USD, EUR, GBP, etc.) 13 | - ✅ Sandbox and Live environment support 14 | - ✅ IPN (Instant Payment Notification) integration 15 | - ✅ Secure payment processing with hash validation 16 | - ✅ Transaction validation through SSLCommerz API 17 | - ✅ Support for all SSLCommerz payment methods (Cards, Mobile Banking, Internet Banking) 18 | - ✅ Internationalization (i18n) support for translations 19 | - ✅ WordPress HTTP API for secure external communications 20 | 21 | ## Requirements 22 | 23 | - WordPress 5.3 or higher 24 | - PHP 7.4 or higher 25 | - Tutor LMS (Free version) 26 | - SSLCommerz merchant account 27 | 28 | ## Installation 29 | 30 | 1. Upload the plugin folder to `/wp-content/plugins` 31 | 2. Activate the plugin through WordPress admin 32 | 3. Ensure Tutor LMS is activated 33 | 4. Configure settings in Tutor LMS > Settings > Payments 34 | 35 | ## Configuration 36 | 37 | ### Step 1: Get SSLCommerz Credentials 38 | 39 | **For Sandbox (Testing):** 40 | 1. Register at [https://developer.sslcommerz.com/registration/](https://developer.sslcommerz.com/registration/) 41 | 2. You'll receive Store ID and Store Password via email 42 | 43 | **For Live (Production):** 44 | 1. Apply for merchant account at [https://sslcommerz.com/](https://sslcommerz.com/) 45 | 2. Complete KYC verification 46 | 3. Get your Store ID and Store Password from merchant panel 47 | 48 | ### Step 2: Configure Plugin 49 | 50 | 1. Go to **Tutor LMS > Settings > Payments** 51 | 2. Find **SSLCommerz** in the payment gateways list 52 | 3. Click to enable and configure: 53 | - **Environment**: Select `Sandbox` for testing or `Live` for production 54 | - **Store ID**: Enter your SSLCommerz Store ID 55 | - **Store Password**: Enter your Store Password (NOT your merchant panel login password) 56 | - **IPN URL**: Copy this URL 57 | 58 | ![SSLCommerz Configuration](https://h1.lwhh.org/sslcommerz/image-1x.jpg) 59 | 60 | ### Step 3: Configure SSLCommerz Merchant Panel 61 | 62 | 1. Login to your SSLCommerz merchant panel 63 | 2. Go to IPN Settings for your store 64 | 3. Add the IPN URL from step 2 65 | 4. Save settings 66 | 67 | ![SSLCommerz Configuration](https://h1.lwhh.org/sslcommerz/image-2.jpg) 68 | 69 | ## Testing 70 | 71 | ### Using Sandbox Environment 72 | 73 | 1. Set environment to "Sandbox" 74 | 2. Use sandbox credentials 75 | 3. Test with SSLCommerz test cards: 76 | - Test Card Number: `4111111111111111` 77 | - Any future expiry date 78 | - Any CVV 79 | 80 | ### Test Transaction Flow 81 | 82 | 1. Create a test course in your LMS 83 | 2. Set a price for the course 84 | 3. Add course to cart and proceed to checkout 85 | 4. Select SSLCommerz as payment method 86 | 5. Complete payment on SSLCommerz page 87 | 6. Verify order status in Tutor LMS 88 | 89 | ## How It Works 90 | 91 | ### Payment Flow 92 | 93 | ``` 94 | Student clicks "Purchase" 95 | ↓ 96 | Plugin sends payment request to SSLCommerz 97 | ↓ 98 | Student redirected to SSLCommerz payment page 99 | ↓ 100 | Student completes payment 101 | ↓ 102 | SSLCommerz sends IPN notification to your site 103 | ↓ 104 | Plugin validates transaction with SSLCommerz API 105 | ↓ 106 | Order status updated (Success/Failed/Cancelled) 107 | ↓ 108 | Student gets access to course (if successful) 109 | ``` 110 | 111 | ### Security Features 112 | 113 | 1. **Hash Verification**: Validates SSLCommerz callback signatures 114 | 2. **Transaction Validation**: Double-checks payment status with SSLCommerz API 115 | 3. **Amount Verification**: Ensures paid amount matches order amount 116 | 4. **SSL Communication**: All API calls use HTTPS 117 | 118 | ## Supported Currencies 119 | 120 | SSLCommerz supports the following currencies: 121 | - BDT (Bangladeshi Taka) - Primary 122 | - USD (US Dollar) 123 | - EUR (Euro) 124 | - GBP (British Pound) 125 | - SGD (Singapore Dollar) 126 | - INR (Indian Rupee) 127 | - MYR (Malaysian Ringgit) 128 | 129 | **Note:** For non-BDT currencies, SSLCommerz converts to BDT at current exchange rates. 130 | 131 | ## API Integration Details 132 | 133 | ### Payment Initiation 134 | - **Endpoint**: `{api_domain}/gwprocess/v4/api.php` 135 | - **Method**: POST 136 | - **Authentication**: Store ID & Store Password 137 | 138 | ### Transaction Validation 139 | - **Endpoint**: `{api_domain}/validator/api/validationserverAPI.php` 140 | - **Method**: GET 141 | - **Purpose**: Verify payment status 142 | 143 | ### IPN Callback 144 | - Receives POST data from SSLCommerz 145 | - Validates transaction 146 | - Updates order status 147 | 148 | ## File Structure 149 | 150 | ``` 151 | tutor-sslcommerz/ 152 | ├── tutor-sslcommerz.php # Main plugin file 153 | ├── composer.json # Composer dependencies 154 | ├── composer.lock # Composer lock file 155 | ├── readme.txt # WordPress plugin readme 156 | ├── README.md # This file 157 | ├── .gitignore # Git ignore rules 158 | ├── assets/ # Plugin assets 159 | │ └── sslcommerz-logo.png # Gateway logo 160 | ├── integration/ # Tutor LMS integration 161 | │ ├── Init.php # Plugin initialization 162 | │ ├── SslcommerzConfig.php # Configuration class 163 | │ ├── SslcommerzGateway.php # Gateway registration 164 | │ └── SslcommerzOrderProcess.php # Order processing and callbacks 165 | ├── languages/ # Translation files 166 | │ └── tutor-sslcommerz.pot # Translation template 167 | ├── payments/ # Payment processing 168 | │ └── Sslcommerz.php # Core payment logic 169 | └── vendor/ # Composer autoload 170 | ``` 171 | 172 | ## Internationalization (i18n) 173 | 174 | This plugin supports internationalization and is translation-ready. All user-facing strings are wrapped with WordPress translation functions. 175 | 176 | ### For Translators 177 | 178 | 1. Use the `languages/tutor-sslcommerz.pot` file as a template 179 | 2. Create language-specific `.po` files using tools like Poedit or Loco Translate 180 | 3. Compile `.mo` files and place them in the `languages/` directory 181 | 4. File naming: `tutor-sslcommerz-{locale}.mo` (e.g., `tutor-sslcommerz-bn_BD.mo` for Bengali) 182 | 183 | ### Text Domain 184 | 185 | - **Text Domain:** `tutor-sslcommerz` 186 | - **Domain Path:** `/languages/` 187 | 188 | ### Available Languages 189 | 190 | Currently available in: 191 | - English (default) 192 | 193 | Contributions for additional language translations are welcome! 194 | 195 | ## Troubleshooting 196 | 197 | ### Payment Not Processing 198 | 199 | 1. **Check Store Credentials**: Ensure Store ID and Password are correct 200 | 2. **Environment Mismatch**: Sandbox credentials won't work in Live mode 201 | 3. **IPN URL**: Verify IPN URL is correctly configured in SSLCommerz panel 202 | 4. **SSL Certificate**: Ensure your site has valid SSL certificate 203 | 204 | ### Transaction Validation Failed 205 | 206 | 1. Check if IPN URL is accessible (not blocked by firewall) 207 | 2. Verify webhook_url in plugin settings 208 | 3. Enable debug logging in WordPress (WP_DEBUG) 209 | 4. Check error logs for detailed messages 210 | 211 | ### Order Status Not Updating 212 | 213 | 1. Verify IPN is configured correctly 214 | 2. Check if order ID is being passed correctly (value_a parameter) 215 | 3. Ensure hash verification is working 216 | 4. Check webhook response in browser console 217 | 218 | ## Known Limitations 219 | 220 | 1. **No Subscription Support**: SSLCommerz doesn't provide native recurring payment functionality 221 | 2. **Currency Conversion**: Non-BDT transactions are auto-converted to BDT 222 | 3. **Refunds**: Manual refund processing through SSLCommerz merchant panel required 223 | 224 | ## Changelog 225 | 226 | ### Version 1.0.7 (October 28, 2025) 227 | - **Security**: Added comprehensive input sanitization to prevent XSS attacks 228 | - **Security**: Implemented proper data validation for all user inputs 229 | - **Security**: Enhanced hash verification with sanitized inputs 230 | - **Improvement**: Enhanced error handling and logging 231 | - **Improvement**: Code organization and structure improvements 232 | 233 | ### Version 1.0.6 234 | - **Feature**: Added complete internationalization (i18n) support 235 | - **Feature**: Created translation template (.pot file) 236 | - **Improvement**: Added languages directory for translation files 237 | - **Improvement**: Updated plugin constants and code structure 238 | - **Improvement**: Enhanced documentation with translation information 239 | 240 | ### Version 1.0.5 241 | - Minor fixes and improvements 242 | 243 | ### Version 1.0.4 244 | - Minor fixes and improvements 245 | 246 | ### Version 1.0.3 247 | - **Improvement**: Replaced cURL with WordPress HTTP API for better compatibility 248 | - **Improvement**: Enhanced error handling and JSON validation 249 | - **Improvement**: More descriptive error messages 250 | 251 | ### Version 1.0.2 252 | - **Security**: Fixed fatal errors in IPN handling 253 | - **Security**: Improved validation for webhook requests 254 | - **Improvement**: Better error logging and debugging 255 | 256 | ### Version 1.0.1 257 | - **Fix**: Corrected payment amount sending (was sending 0) 258 | - **Fix**: Updated to use correct Tutor LMS field names 259 | - **Improvement**: Added payment amount validation 260 | 261 | ### Version 1.0.0 262 | - Initial release 263 | - One-time payment support 264 | - Sandbox and Live environments 265 | - IPN integration 266 | - Multi-currency support 267 | - Transaction validation 268 | 269 | ## Support 270 | 271 | For issues related to: 272 | - **Plugin functionality**: Create issue on GitHub or contact plugin developer 273 | - **SSLCommerz API**: Contact SSLCommerz support at support@sslcommerz.com 274 | - **Tutor LMS**: Contact Themeum support 275 | 276 | ## License 277 | 278 | This plugin is licensed under GPLv2 or later. 279 | 280 | ## Credits 281 | 282 | - Developed for Tutor LMS 283 | - SSLCommerz API integration 284 | - Based on Tutor LMS Payment Gateway framework 285 | 286 | ## Additional Resources 287 | 288 | - [SSLCommerz Documentation](https://developer.sslcommerz.com/documentation/) 289 | - [Tutor LMS Documentation](https://docs.themeum.com/tutor-lms/) 290 | - [SSLCommerz Merchant Panel](https://merchant.sslcommerz.com/) 291 | - [SSLCommerz Developer Portal](https://developer.sslcommerz.com/) 292 | 293 | -------------------------------------------------------------------------------- /integration/SslcommerzOrderProcess.php: -------------------------------------------------------------------------------- 1 | 10 | * @link https://github.com/hasinhayder/tutor-sslcommerz 11 | */ 12 | 13 | namespace TutorSslcommerz; 14 | 15 | /** 16 | * SSLCommerz Order Process Class 17 | * 18 | * This class is responsible for processing SSLCommerz payment callbacks and updating 19 | * Tutor LMS orders based on the payment status. It validates transactions using 20 | * SSLCommerz's validation API and ensures secure payment processing. 21 | */ 22 | class SslcommerzOrderProcess { 23 | 24 | private const API_PROCESS_ENDPOINT = '/gwprocess/v4/api.php'; 25 | private const API_VALIDATION_ENDPOINT = '/validator/api/validationserverAPI.php'; 26 | 27 | private const STATUS_MAP = [ 28 | 'VALID' => 'paid', 29 | 'VALIDATED' => 'paid', 30 | 'FAILED' => 'failed', 31 | 'CANCELLED' => 'cancelled', 32 | 'PENDING' => 'pending', 33 | ]; 34 | 35 | /** 36 | * SSLCommerz client configuration array 37 | * 38 | * Stores store credentials and API domain information retrieved from Tutor settings. 39 | * 40 | * @var array 41 | */ 42 | protected $client; 43 | 44 | /** 45 | * Processes SSLCommerz form submission callback 46 | * 47 | * Handles the payment callback from SSLCommerz after a payment attempt. 48 | * Validates the transaction, updates the order status in the database, 49 | * and ensures secure processing of payment data. 50 | * 51 | * This method is triggered when SSLCommerz redirects back to the success URL 52 | * with payment result data in POST parameters. 53 | * 54 | * @return void 55 | */ 56 | public function process_sslcommerz_form_submission(): void { 57 | // Sanitize GET parameter 58 | $order_placement = isset($_GET['tutor_order_placement']) ? sanitize_text_field(wp_unslash($_GET['tutor_order_placement'])) : ''; 59 | if ($order_placement !== 'success') { 60 | return; 61 | } 62 | 63 | if (empty($_POST) || !isset($_POST['tran_id'])) { 64 | return; 65 | } 66 | 67 | // Sanitize POST data 68 | $tran_id = isset($_POST['tran_id']) ? sanitize_text_field(wp_unslash($_POST['tran_id'])) : ''; 69 | if (empty($tran_id)) { 70 | return; 71 | } 72 | 73 | // Get order_id from POST data (value_a) since SSLCommerz doesn't pass it as query param 74 | $value_a = isset($_POST['value_a']) ? sanitize_text_field(wp_unslash($_POST['value_a'])) : ''; 75 | $order_id = absint($value_a); 76 | if (!$order_id) { 77 | return; 78 | } 79 | 80 | // Retrieve SSLCommerz settings from Tutor options 81 | $options = get_option('tutor_option'); 82 | $payment_settings = json_decode($options['payment_settings'], true); 83 | 84 | // Find SSLCommerz payment method settings 85 | $sslcommerz_settings = null; 86 | foreach ($payment_settings['payment_methods'] as $method) { 87 | if ($method['name'] === 'sslcommerz') { 88 | $sslcommerz_settings = $method; 89 | break; 90 | } 91 | } 92 | 93 | // Skip if SSLCommerz settings not found 94 | if (!$sslcommerz_settings) { 95 | return; 96 | } 97 | try { 98 | // Extract store credentials and environment from settings 99 | foreach ($sslcommerz_settings['fields'] as $field) { 100 | if (!isset($field['name']) || !isset($field['value'])) { 101 | continue; 102 | } 103 | 104 | switch ($field['name']) { 105 | case 'store_id': 106 | $this->client['store_id'] = $field['value']; 107 | break; 108 | case 'store_password': 109 | $this->client['store_password'] = $field['value']; 110 | break; 111 | case 'environment': 112 | $this->client['environment'] = $field['value']; 113 | break; 114 | } 115 | } 116 | 117 | // Validate required client configuration 118 | if (empty($this->client['store_id']) || empty($this->client['store_password']) || empty($this->client['environment'])) { 119 | return; 120 | } 121 | 122 | // Determine API domain based on environment 123 | $this->client['api_domain'] = $this->client['environment'] === 'sandbox' 124 | ? 'https://sandbox.sslcommerz.com' 125 | : 'https://securepay.sslcommerz.com'; 126 | 127 | // Sanitize POST data for validation 128 | $sanitized_post = []; 129 | foreach ($_POST as $key => $value) { 130 | $sanitized_post[$key] = is_array($value) ? array_map('sanitize_text_field', array_map('wp_unslash', $value)) : sanitize_text_field(wp_unslash($value)); 131 | } 132 | 133 | // Validate transaction with SSLCommerz API 134 | if ($this->validateTransaction($sanitized_post)) { 135 | $status = isset($sanitized_post['status']) ? $sanitized_post['status'] : 'FAILED'; 136 | $payment_status = self::STATUS_MAP[$status] ?? 'failed'; 137 | 138 | // Update order in database with payment status 139 | self::update_order_in_database($order_id, $payment_status, $sanitized_post['tran_id'] ?? ''); 140 | } 141 | } catch (\Exception $e) { 142 | // Log error for debugging (consider using WordPress logging in production) 143 | } 144 | 145 | 146 | 147 | } 148 | 149 | /** 150 | * Verifies SSLCommerz hash for transaction security 151 | * 152 | * Validates the hash provided by SSLCommerz to ensure the callback data 153 | * has not been tampered with. Hash verification is optional but recommended 154 | * for enhanced security. 155 | * 156 | * @param array $post_data POST data containing hash verification fields 157 | * @return bool True if hash is valid or not provided, false if invalid 158 | */ 159 | private function verifyHash(array $post_data): bool { 160 | // Hash verification is optional but recommended 161 | if (!isset($post_data['verify_sign']) || !isset($post_data['verify_key'])) { 162 | return true; // If no hash provided, skip verification 163 | } 164 | 165 | // Sanitize verify_key 166 | $verify_key = sanitize_text_field(wp_unslash($post_data['verify_key'])); 167 | $pre_define_key = explode(',', $verify_key); 168 | $new_data = []; 169 | 170 | foreach ($pre_define_key as $value) { 171 | $sanitized_key = sanitize_key($value); 172 | if (isset($post_data[$sanitized_key])) { 173 | $new_data[$sanitized_key] = sanitize_text_field(wp_unslash($post_data[$sanitized_key])); 174 | } 175 | } 176 | 177 | $new_data['store_passwd'] = md5($this->client['store_password']); 178 | ksort($new_data); 179 | 180 | $hash_string = ""; 181 | foreach ($new_data as $key => $value) { 182 | $hash_string .= $key . '=' . $value . '&'; 183 | } 184 | $hash_string = rtrim($hash_string, '&'); 185 | 186 | // Sanitize verify_sign for comparison 187 | $verify_sign = sanitize_text_field(wp_unslash($post_data['verify_sign'])); 188 | return md5($hash_string) === $verify_sign; 189 | } 190 | 191 | /** 192 | * Validates transaction with SSLCommerz validation API 193 | * 194 | * @param array $post_data POST data from callback 195 | * @param string $tran_id Transaction ID 196 | * @param float $amount Transaction amount 197 | * @param string $currency Currency code 198 | * @return bool 199 | */ 200 | private function validateTransaction(array $post_data): bool { 201 | // First verify hash if present 202 | if (!$this->verifyHash($post_data)) { 203 | return false; 204 | } 205 | 206 | // Sanitize transaction data 207 | $tran_id = sanitize_text_field($post_data['tran_id'] ?? ''); 208 | $amount = isset($post_data['amount']) ? floatval($post_data['amount']) : 0.0; 209 | $currency = sanitize_text_field($post_data['currency'] ?? 'BDT'); 210 | 211 | // Call SSLCommerz validation API using WordPress HTTP API 212 | $val_id = urlencode(sanitize_text_field($post_data['val_id'] ?? '')); 213 | $store_id = urlencode(sanitize_text_field($this->client['store_id'])); 214 | $store_passwd = urlencode(sanitize_text_field($this->client['store_password'])); 215 | 216 | $validationUrl = $this->client['api_domain'] . self::API_VALIDATION_ENDPOINT . '?val_id=' . $val_id . '&store_id=' . $store_id . '&store_passwd=' . $store_passwd . '&v=1&format=json'; 217 | 218 | // Set SSL verification based on environment 219 | $isLocalhost = $this->client['environment'] === 'sandbox'; 220 | $ssl_verify = !$isLocalhost; 221 | 222 | // Make GET request using wp_remote_get 223 | $args = [ 224 | 'timeout' => 30, 225 | 'sslverify' => $ssl_verify, 226 | ]; 227 | 228 | $response = wp_remote_get($validationUrl, $args); 229 | 230 | // Check for errors 231 | if (is_wp_error($response)) { 232 | return false; 233 | } 234 | 235 | // Get response code and body 236 | $code = wp_remote_retrieve_response_code($response); 237 | $body = wp_remote_retrieve_body($response); 238 | 239 | if ($code == 200 && !empty($body)) { 240 | $result = json_decode($body); 241 | 242 | if (json_last_error() === JSON_ERROR_NONE && isset($result->status) && ($result->status === 'VALID' || $result->status === 'VALIDATED')) { 243 | // Verify transaction details match 244 | if ($currency === 'BDT') { 245 | return trim($tran_id) === trim($result->tran_id) && abs($amount - $result->amount) < 1; 246 | } else { 247 | return trim($tran_id) === trim($result->tran_id) && abs($amount - $result->currency_amount) < 1; 248 | } 249 | } 250 | } 251 | 252 | return false; 253 | } 254 | 255 | /** 256 | * Update order status in the database 257 | * 258 | * Static method that updates the tutor_orders table with payment status and transaction details. 259 | * Also updates the order_status to 'completed' when payment is marked as paid. 260 | * 261 | * @param int $order_id The order ID 262 | * @param string $payment_status The payment status 263 | * @param string $transaction_id The transaction ID 264 | * @return void 265 | */ 266 | private static function update_order_in_database(int $order_id, string $payment_status, string $transaction_id): void { 267 | global $wpdb; 268 | 269 | // Sanitize inputs 270 | $sanitized_payment_status = sanitize_text_field($payment_status); 271 | $sanitized_transaction_id = sanitize_text_field($transaction_id); 272 | 273 | $update_data = [ 274 | 'payment_status' => $sanitized_payment_status, 275 | 'transaction_id' => $sanitized_transaction_id, 276 | ]; 277 | 278 | // If payment is successful, mark order as completed 279 | if ($sanitized_payment_status === 'paid') { 280 | $update_data['order_status'] = 'completed'; 281 | } 282 | 283 | $wpdb->update( 284 | $wpdb->prefix . 'tutor_orders', 285 | $update_data, 286 | ['id' => $order_id], 287 | array_fill(0, count($update_data), '%s'), 288 | ['%d'] 289 | ); 290 | } 291 | 292 | } -------------------------------------------------------------------------------- /vendor/composer/InstalledVersions.php: -------------------------------------------------------------------------------- 1 | 7 | * Jordi Boggiano 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace Composer; 14 | 15 | use Composer\Autoload\ClassLoader; 16 | use Composer\Semver\VersionParser; 17 | 18 | /** 19 | * This class is copied in every Composer installed project and available to all 20 | * 21 | * See also https://getcomposer.org/doc/07-runtime.md#installed-versions 22 | * 23 | * To require its presence, you can require `composer-runtime-api ^2.0` 24 | * 25 | * @final 26 | */ 27 | class InstalledVersions 28 | { 29 | /** 30 | * @var mixed[]|null 31 | * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}|array{}|null 32 | */ 33 | private static $installed; 34 | 35 | /** 36 | * @var bool|null 37 | */ 38 | private static $canGetVendors; 39 | 40 | /** 41 | * @var array[] 42 | * @psalm-var array}> 43 | */ 44 | private static $installedByVendor = array(); 45 | 46 | /** 47 | * Returns a list of all package names which are present, either by being installed, replaced or provided 48 | * 49 | * @return string[] 50 | * @psalm-return list 51 | */ 52 | public static function getInstalledPackages() 53 | { 54 | $packages = array(); 55 | foreach (self::getInstalled() as $installed) { 56 | $packages[] = array_keys($installed['versions']); 57 | } 58 | 59 | if (1 === \count($packages)) { 60 | return $packages[0]; 61 | } 62 | 63 | return array_keys(array_flip(\call_user_func_array('array_merge', $packages))); 64 | } 65 | 66 | /** 67 | * Returns a list of all package names with a specific type e.g. 'library' 68 | * 69 | * @param string $type 70 | * @return string[] 71 | * @psalm-return list 72 | */ 73 | public static function getInstalledPackagesByType($type) 74 | { 75 | $packagesByType = array(); 76 | 77 | foreach (self::getInstalled() as $installed) { 78 | foreach ($installed['versions'] as $name => $package) { 79 | if (isset($package['type']) && $package['type'] === $type) { 80 | $packagesByType[] = $name; 81 | } 82 | } 83 | } 84 | 85 | return $packagesByType; 86 | } 87 | 88 | /** 89 | * Checks whether the given package is installed 90 | * 91 | * This also returns true if the package name is provided or replaced by another package 92 | * 93 | * @param string $packageName 94 | * @param bool $includeDevRequirements 95 | * @return bool 96 | */ 97 | public static function isInstalled($packageName, $includeDevRequirements = true) 98 | { 99 | foreach (self::getInstalled() as $installed) { 100 | if (isset($installed['versions'][$packageName])) { 101 | return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false; 102 | } 103 | } 104 | 105 | return false; 106 | } 107 | 108 | /** 109 | * Checks whether the given package satisfies a version constraint 110 | * 111 | * e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call: 112 | * 113 | * Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3') 114 | * 115 | * @param VersionParser $parser Install composer/semver to have access to this class and functionality 116 | * @param string $packageName 117 | * @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package 118 | * @return bool 119 | */ 120 | public static function satisfies(VersionParser $parser, $packageName, $constraint) 121 | { 122 | $constraint = $parser->parseConstraints((string) $constraint); 123 | $provided = $parser->parseConstraints(self::getVersionRanges($packageName)); 124 | 125 | return $provided->matches($constraint); 126 | } 127 | 128 | /** 129 | * Returns a version constraint representing all the range(s) which are installed for a given package 130 | * 131 | * It is easier to use this via isInstalled() with the $constraint argument if you need to check 132 | * whether a given version of a package is installed, and not just whether it exists 133 | * 134 | * @param string $packageName 135 | * @return string Version constraint usable with composer/semver 136 | */ 137 | public static function getVersionRanges($packageName) 138 | { 139 | foreach (self::getInstalled() as $installed) { 140 | if (!isset($installed['versions'][$packageName])) { 141 | continue; 142 | } 143 | 144 | $ranges = array(); 145 | if (isset($installed['versions'][$packageName]['pretty_version'])) { 146 | $ranges[] = $installed['versions'][$packageName]['pretty_version']; 147 | } 148 | if (array_key_exists('aliases', $installed['versions'][$packageName])) { 149 | $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']); 150 | } 151 | if (array_key_exists('replaced', $installed['versions'][$packageName])) { 152 | $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']); 153 | } 154 | if (array_key_exists('provided', $installed['versions'][$packageName])) { 155 | $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']); 156 | } 157 | 158 | return implode(' || ', $ranges); 159 | } 160 | 161 | throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); 162 | } 163 | 164 | /** 165 | * @param string $packageName 166 | * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present 167 | */ 168 | public static function getVersion($packageName) 169 | { 170 | foreach (self::getInstalled() as $installed) { 171 | if (!isset($installed['versions'][$packageName])) { 172 | continue; 173 | } 174 | 175 | if (!isset($installed['versions'][$packageName]['version'])) { 176 | return null; 177 | } 178 | 179 | return $installed['versions'][$packageName]['version']; 180 | } 181 | 182 | throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); 183 | } 184 | 185 | /** 186 | * @param string $packageName 187 | * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present 188 | */ 189 | public static function getPrettyVersion($packageName) 190 | { 191 | foreach (self::getInstalled() as $installed) { 192 | if (!isset($installed['versions'][$packageName])) { 193 | continue; 194 | } 195 | 196 | if (!isset($installed['versions'][$packageName]['pretty_version'])) { 197 | return null; 198 | } 199 | 200 | return $installed['versions'][$packageName]['pretty_version']; 201 | } 202 | 203 | throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); 204 | } 205 | 206 | /** 207 | * @param string $packageName 208 | * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference 209 | */ 210 | public static function getReference($packageName) 211 | { 212 | foreach (self::getInstalled() as $installed) { 213 | if (!isset($installed['versions'][$packageName])) { 214 | continue; 215 | } 216 | 217 | if (!isset($installed['versions'][$packageName]['reference'])) { 218 | return null; 219 | } 220 | 221 | return $installed['versions'][$packageName]['reference']; 222 | } 223 | 224 | throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); 225 | } 226 | 227 | /** 228 | * @param string $packageName 229 | * @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path. 230 | */ 231 | public static function getInstallPath($packageName) 232 | { 233 | foreach (self::getInstalled() as $installed) { 234 | if (!isset($installed['versions'][$packageName])) { 235 | continue; 236 | } 237 | 238 | return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null; 239 | } 240 | 241 | throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed'); 242 | } 243 | 244 | /** 245 | * @return array 246 | * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool} 247 | */ 248 | public static function getRootPackage() 249 | { 250 | $installed = self::getInstalled(); 251 | 252 | return $installed[0]['root']; 253 | } 254 | 255 | /** 256 | * Returns the raw installed.php data for custom implementations 257 | * 258 | * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. 259 | * @return array[] 260 | * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} 261 | */ 262 | public static function getRawData() 263 | { 264 | @trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED); 265 | 266 | if (null === self::$installed) { 267 | // only require the installed.php file if this file is loaded from its dumped location, 268 | // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 269 | if (substr(__DIR__, -8, 1) !== 'C') { 270 | self::$installed = include __DIR__ . '/installed.php'; 271 | } else { 272 | self::$installed = array(); 273 | } 274 | } 275 | 276 | return self::$installed; 277 | } 278 | 279 | /** 280 | * Returns the raw data of all installed.php which are currently loaded for custom implementations 281 | * 282 | * @return array[] 283 | * @psalm-return list}> 284 | */ 285 | public static function getAllRawData() 286 | { 287 | return self::getInstalled(); 288 | } 289 | 290 | /** 291 | * Lets you reload the static array from another file 292 | * 293 | * This is only useful for complex integrations in which a project needs to use 294 | * this class but then also needs to execute another project's autoloader in process, 295 | * and wants to ensure both projects have access to their version of installed.php. 296 | * 297 | * A typical case would be PHPUnit, where it would need to make sure it reads all 298 | * the data it needs from this class, then call reload() with 299 | * `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure 300 | * the project in which it runs can then also use this class safely, without 301 | * interference between PHPUnit's dependencies and the project's dependencies. 302 | * 303 | * @param array[] $data A vendor/composer/installed.php data set 304 | * @return void 305 | * 306 | * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $data 307 | */ 308 | public static function reload($data) 309 | { 310 | self::$installed = $data; 311 | self::$installedByVendor = array(); 312 | } 313 | 314 | /** 315 | * @return array[] 316 | * @psalm-return list}> 317 | */ 318 | private static function getInstalled() 319 | { 320 | if (null === self::$canGetVendors) { 321 | self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders'); 322 | } 323 | 324 | $installed = array(); 325 | 326 | if (self::$canGetVendors) { 327 | foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { 328 | if (isset(self::$installedByVendor[$vendorDir])) { 329 | $installed[] = self::$installedByVendor[$vendorDir]; 330 | } elseif (is_file($vendorDir.'/composer/installed.php')) { 331 | /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ 332 | $required = require $vendorDir.'/composer/installed.php'; 333 | $installed[] = self::$installedByVendor[$vendorDir] = $required; 334 | if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) { 335 | self::$installed = $installed[count($installed) - 1]; 336 | } 337 | } 338 | } 339 | } 340 | 341 | if (null === self::$installed) { 342 | // only require the installed.php file if this file is loaded from its dumped location, 343 | // and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937 344 | if (substr(__DIR__, -8, 1) !== 'C') { 345 | /** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $required */ 346 | $required = require __DIR__ . '/installed.php'; 347 | self::$installed = $required; 348 | } else { 349 | self::$installed = array(); 350 | } 351 | } 352 | 353 | if (self::$installed !== array()) { 354 | $installed[] = self::$installed; 355 | } 356 | 357 | return $installed; 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /vendor/composer/ClassLoader.php: -------------------------------------------------------------------------------- 1 | 7 | * Jordi Boggiano 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | namespace Composer\Autoload; 14 | 15 | /** 16 | * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. 17 | * 18 | * $loader = new \Composer\Autoload\ClassLoader(); 19 | * 20 | * // register classes with namespaces 21 | * $loader->add('Symfony\Component', __DIR__.'/component'); 22 | * $loader->add('Symfony', __DIR__.'/framework'); 23 | * 24 | * // activate the autoloader 25 | * $loader->register(); 26 | * 27 | * // to enable searching the include path (eg. for PEAR packages) 28 | * $loader->setUseIncludePath(true); 29 | * 30 | * In this example, if you try to use a class in the Symfony\Component 31 | * namespace or one of its children (Symfony\Component\Console for instance), 32 | * the autoloader will first look for the class under the component/ 33 | * directory, and it will then fallback to the framework/ directory if not 34 | * found before giving up. 35 | * 36 | * This class is loosely based on the Symfony UniversalClassLoader. 37 | * 38 | * @author Fabien Potencier 39 | * @author Jordi Boggiano 40 | * @see https://www.php-fig.org/psr/psr-0/ 41 | * @see https://www.php-fig.org/psr/psr-4/ 42 | */ 43 | class ClassLoader 44 | { 45 | /** @var \Closure(string):void */ 46 | private static $includeFile; 47 | 48 | /** @var string|null */ 49 | private $vendorDir; 50 | 51 | // PSR-4 52 | /** 53 | * @var array> 54 | */ 55 | private $prefixLengthsPsr4 = array(); 56 | /** 57 | * @var array> 58 | */ 59 | private $prefixDirsPsr4 = array(); 60 | /** 61 | * @var list 62 | */ 63 | private $fallbackDirsPsr4 = array(); 64 | 65 | // PSR-0 66 | /** 67 | * List of PSR-0 prefixes 68 | * 69 | * Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2'))) 70 | * 71 | * @var array>> 72 | */ 73 | private $prefixesPsr0 = array(); 74 | /** 75 | * @var list 76 | */ 77 | private $fallbackDirsPsr0 = array(); 78 | 79 | /** @var bool */ 80 | private $useIncludePath = false; 81 | 82 | /** 83 | * @var array 84 | */ 85 | private $classMap = array(); 86 | 87 | /** @var bool */ 88 | private $classMapAuthoritative = false; 89 | 90 | /** 91 | * @var array 92 | */ 93 | private $missingClasses = array(); 94 | 95 | /** @var string|null */ 96 | private $apcuPrefix; 97 | 98 | /** 99 | * @var array 100 | */ 101 | private static $registeredLoaders = array(); 102 | 103 | /** 104 | * @param string|null $vendorDir 105 | */ 106 | public function __construct($vendorDir = null) 107 | { 108 | $this->vendorDir = $vendorDir; 109 | self::initializeIncludeClosure(); 110 | } 111 | 112 | /** 113 | * @return array> 114 | */ 115 | public function getPrefixes() 116 | { 117 | if (!empty($this->prefixesPsr0)) { 118 | return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); 119 | } 120 | 121 | return array(); 122 | } 123 | 124 | /** 125 | * @return array> 126 | */ 127 | public function getPrefixesPsr4() 128 | { 129 | return $this->prefixDirsPsr4; 130 | } 131 | 132 | /** 133 | * @return list 134 | */ 135 | public function getFallbackDirs() 136 | { 137 | return $this->fallbackDirsPsr0; 138 | } 139 | 140 | /** 141 | * @return list 142 | */ 143 | public function getFallbackDirsPsr4() 144 | { 145 | return $this->fallbackDirsPsr4; 146 | } 147 | 148 | /** 149 | * @return array Array of classname => path 150 | */ 151 | public function getClassMap() 152 | { 153 | return $this->classMap; 154 | } 155 | 156 | /** 157 | * @param array $classMap Class to filename map 158 | * 159 | * @return void 160 | */ 161 | public function addClassMap(array $classMap) 162 | { 163 | if ($this->classMap) { 164 | $this->classMap = array_merge($this->classMap, $classMap); 165 | } else { 166 | $this->classMap = $classMap; 167 | } 168 | } 169 | 170 | /** 171 | * Registers a set of PSR-0 directories for a given prefix, either 172 | * appending or prepending to the ones previously set for this prefix. 173 | * 174 | * @param string $prefix The prefix 175 | * @param list|string $paths The PSR-0 root directories 176 | * @param bool $prepend Whether to prepend the directories 177 | * 178 | * @return void 179 | */ 180 | public function add($prefix, $paths, $prepend = false) 181 | { 182 | $paths = (array) $paths; 183 | if (!$prefix) { 184 | if ($prepend) { 185 | $this->fallbackDirsPsr0 = array_merge( 186 | $paths, 187 | $this->fallbackDirsPsr0 188 | ); 189 | } else { 190 | $this->fallbackDirsPsr0 = array_merge( 191 | $this->fallbackDirsPsr0, 192 | $paths 193 | ); 194 | } 195 | 196 | return; 197 | } 198 | 199 | $first = $prefix[0]; 200 | if (!isset($this->prefixesPsr0[$first][$prefix])) { 201 | $this->prefixesPsr0[$first][$prefix] = $paths; 202 | 203 | return; 204 | } 205 | if ($prepend) { 206 | $this->prefixesPsr0[$first][$prefix] = array_merge( 207 | $paths, 208 | $this->prefixesPsr0[$first][$prefix] 209 | ); 210 | } else { 211 | $this->prefixesPsr0[$first][$prefix] = array_merge( 212 | $this->prefixesPsr0[$first][$prefix], 213 | $paths 214 | ); 215 | } 216 | } 217 | 218 | /** 219 | * Registers a set of PSR-4 directories for a given namespace, either 220 | * appending or prepending to the ones previously set for this namespace. 221 | * 222 | * @param string $prefix The prefix/namespace, with trailing '\\' 223 | * @param list|string $paths The PSR-4 base directories 224 | * @param bool $prepend Whether to prepend the directories 225 | * 226 | * @throws \InvalidArgumentException 227 | * 228 | * @return void 229 | */ 230 | public function addPsr4($prefix, $paths, $prepend = false) 231 | { 232 | $paths = (array) $paths; 233 | if (!$prefix) { 234 | // Register directories for the root namespace. 235 | if ($prepend) { 236 | $this->fallbackDirsPsr4 = array_merge( 237 | $paths, 238 | $this->fallbackDirsPsr4 239 | ); 240 | } else { 241 | $this->fallbackDirsPsr4 = array_merge( 242 | $this->fallbackDirsPsr4, 243 | $paths 244 | ); 245 | } 246 | } elseif (!isset($this->prefixDirsPsr4[$prefix])) { 247 | // Register directories for a new namespace. 248 | $length = strlen($prefix); 249 | if ('\\' !== $prefix[$length - 1]) { 250 | throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); 251 | } 252 | $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; 253 | $this->prefixDirsPsr4[$prefix] = $paths; 254 | } elseif ($prepend) { 255 | // Prepend directories for an already registered namespace. 256 | $this->prefixDirsPsr4[$prefix] = array_merge( 257 | $paths, 258 | $this->prefixDirsPsr4[$prefix] 259 | ); 260 | } else { 261 | // Append directories for an already registered namespace. 262 | $this->prefixDirsPsr4[$prefix] = array_merge( 263 | $this->prefixDirsPsr4[$prefix], 264 | $paths 265 | ); 266 | } 267 | } 268 | 269 | /** 270 | * Registers a set of PSR-0 directories for a given prefix, 271 | * replacing any others previously set for this prefix. 272 | * 273 | * @param string $prefix The prefix 274 | * @param list|string $paths The PSR-0 base directories 275 | * 276 | * @return void 277 | */ 278 | public function set($prefix, $paths) 279 | { 280 | if (!$prefix) { 281 | $this->fallbackDirsPsr0 = (array) $paths; 282 | } else { 283 | $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; 284 | } 285 | } 286 | 287 | /** 288 | * Registers a set of PSR-4 directories for a given namespace, 289 | * replacing any others previously set for this namespace. 290 | * 291 | * @param string $prefix The prefix/namespace, with trailing '\\' 292 | * @param list|string $paths The PSR-4 base directories 293 | * 294 | * @throws \InvalidArgumentException 295 | * 296 | * @return void 297 | */ 298 | public function setPsr4($prefix, $paths) 299 | { 300 | if (!$prefix) { 301 | $this->fallbackDirsPsr4 = (array) $paths; 302 | } else { 303 | $length = strlen($prefix); 304 | if ('\\' !== $prefix[$length - 1]) { 305 | throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); 306 | } 307 | $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; 308 | $this->prefixDirsPsr4[$prefix] = (array) $paths; 309 | } 310 | } 311 | 312 | /** 313 | * Turns on searching the include path for class files. 314 | * 315 | * @param bool $useIncludePath 316 | * 317 | * @return void 318 | */ 319 | public function setUseIncludePath($useIncludePath) 320 | { 321 | $this->useIncludePath = $useIncludePath; 322 | } 323 | 324 | /** 325 | * Can be used to check if the autoloader uses the include path to check 326 | * for classes. 327 | * 328 | * @return bool 329 | */ 330 | public function getUseIncludePath() 331 | { 332 | return $this->useIncludePath; 333 | } 334 | 335 | /** 336 | * Turns off searching the prefix and fallback directories for classes 337 | * that have not been registered with the class map. 338 | * 339 | * @param bool $classMapAuthoritative 340 | * 341 | * @return void 342 | */ 343 | public function setClassMapAuthoritative($classMapAuthoritative) 344 | { 345 | $this->classMapAuthoritative = $classMapAuthoritative; 346 | } 347 | 348 | /** 349 | * Should class lookup fail if not found in the current class map? 350 | * 351 | * @return bool 352 | */ 353 | public function isClassMapAuthoritative() 354 | { 355 | return $this->classMapAuthoritative; 356 | } 357 | 358 | /** 359 | * APCu prefix to use to cache found/not-found classes, if the extension is enabled. 360 | * 361 | * @param string|null $apcuPrefix 362 | * 363 | * @return void 364 | */ 365 | public function setApcuPrefix($apcuPrefix) 366 | { 367 | $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; 368 | } 369 | 370 | /** 371 | * The APCu prefix in use, or null if APCu caching is not enabled. 372 | * 373 | * @return string|null 374 | */ 375 | public function getApcuPrefix() 376 | { 377 | return $this->apcuPrefix; 378 | } 379 | 380 | /** 381 | * Registers this instance as an autoloader. 382 | * 383 | * @param bool $prepend Whether to prepend the autoloader or not 384 | * 385 | * @return void 386 | */ 387 | public function register($prepend = false) 388 | { 389 | spl_autoload_register(array($this, 'loadClass'), true, $prepend); 390 | 391 | if (null === $this->vendorDir) { 392 | return; 393 | } 394 | 395 | if ($prepend) { 396 | self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders; 397 | } else { 398 | unset(self::$registeredLoaders[$this->vendorDir]); 399 | self::$registeredLoaders[$this->vendorDir] = $this; 400 | } 401 | } 402 | 403 | /** 404 | * Unregisters this instance as an autoloader. 405 | * 406 | * @return void 407 | */ 408 | public function unregister() 409 | { 410 | spl_autoload_unregister(array($this, 'loadClass')); 411 | 412 | if (null !== $this->vendorDir) { 413 | unset(self::$registeredLoaders[$this->vendorDir]); 414 | } 415 | } 416 | 417 | /** 418 | * Loads the given class or interface. 419 | * 420 | * @param string $class The name of the class 421 | * @return true|null True if loaded, null otherwise 422 | */ 423 | public function loadClass($class) 424 | { 425 | if ($file = $this->findFile($class)) { 426 | $includeFile = self::$includeFile; 427 | $includeFile($file); 428 | 429 | return true; 430 | } 431 | 432 | return null; 433 | } 434 | 435 | /** 436 | * Finds the path to the file where the class is defined. 437 | * 438 | * @param string $class The name of the class 439 | * 440 | * @return string|false The path if found, false otherwise 441 | */ 442 | public function findFile($class) 443 | { 444 | // class map lookup 445 | if (isset($this->classMap[$class])) { 446 | return $this->classMap[$class]; 447 | } 448 | if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { 449 | return false; 450 | } 451 | if (null !== $this->apcuPrefix) { 452 | $file = apcu_fetch($this->apcuPrefix.$class, $hit); 453 | if ($hit) { 454 | return $file; 455 | } 456 | } 457 | 458 | $file = $this->findFileWithExtension($class, '.php'); 459 | 460 | // Search for Hack files if we are running on HHVM 461 | if (false === $file && defined('HHVM_VERSION')) { 462 | $file = $this->findFileWithExtension($class, '.hh'); 463 | } 464 | 465 | if (null !== $this->apcuPrefix) { 466 | apcu_add($this->apcuPrefix.$class, $file); 467 | } 468 | 469 | if (false === $file) { 470 | // Remember that this class does not exist. 471 | $this->missingClasses[$class] = true; 472 | } 473 | 474 | return $file; 475 | } 476 | 477 | /** 478 | * Returns the currently registered loaders keyed by their corresponding vendor directories. 479 | * 480 | * @return array 481 | */ 482 | public static function getRegisteredLoaders() 483 | { 484 | return self::$registeredLoaders; 485 | } 486 | 487 | /** 488 | * @param string $class 489 | * @param string $ext 490 | * @return string|false 491 | */ 492 | private function findFileWithExtension($class, $ext) 493 | { 494 | // PSR-4 lookup 495 | $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; 496 | 497 | $first = $class[0]; 498 | if (isset($this->prefixLengthsPsr4[$first])) { 499 | $subPath = $class; 500 | while (false !== $lastPos = strrpos($subPath, '\\')) { 501 | $subPath = substr($subPath, 0, $lastPos); 502 | $search = $subPath . '\\'; 503 | if (isset($this->prefixDirsPsr4[$search])) { 504 | $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); 505 | foreach ($this->prefixDirsPsr4[$search] as $dir) { 506 | if (file_exists($file = $dir . $pathEnd)) { 507 | return $file; 508 | } 509 | } 510 | } 511 | } 512 | } 513 | 514 | // PSR-4 fallback dirs 515 | foreach ($this->fallbackDirsPsr4 as $dir) { 516 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { 517 | return $file; 518 | } 519 | } 520 | 521 | // PSR-0 lookup 522 | if (false !== $pos = strrpos($class, '\\')) { 523 | // namespaced class name 524 | $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) 525 | . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); 526 | } else { 527 | // PEAR-like class name 528 | $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; 529 | } 530 | 531 | if (isset($this->prefixesPsr0[$first])) { 532 | foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { 533 | if (0 === strpos($class, $prefix)) { 534 | foreach ($dirs as $dir) { 535 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { 536 | return $file; 537 | } 538 | } 539 | } 540 | } 541 | } 542 | 543 | // PSR-0 fallback dirs 544 | foreach ($this->fallbackDirsPsr0 as $dir) { 545 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { 546 | return $file; 547 | } 548 | } 549 | 550 | // PSR-0 include paths. 551 | if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { 552 | return $file; 553 | } 554 | 555 | return false; 556 | } 557 | 558 | /** 559 | * @return void 560 | */ 561 | private static function initializeIncludeClosure() 562 | { 563 | if (self::$includeFile !== null) { 564 | return; 565 | } 566 | 567 | /** 568 | * Scope isolated include. 569 | * 570 | * Prevents access to $this/self from included files. 571 | * 572 | * @param string $file 573 | * @return void 574 | */ 575 | self::$includeFile = \Closure::bind(static function($file) { 576 | include $file; 577 | }, null, null); 578 | } 579 | } 580 | -------------------------------------------------------------------------------- /payments/Sslcommerz.php: -------------------------------------------------------------------------------- 1 | 17 | * @link https://github.com/hasinhayder/tutor-sslcommerz 18 | */ 19 | 20 | namespace Payments\Sslcommerz; 21 | 22 | use Throwable; 23 | use ErrorException; 24 | use Ollyo\PaymentHub\Core\Support\Arr; 25 | use Ollyo\PaymentHub\Core\Support\System; 26 | use GuzzleHttp\Exception\RequestException; 27 | use Ollyo\PaymentHub\Core\Payment\BasePayment; 28 | 29 | /** 30 | * SSLCommerz Payment Gateway Class 31 | * 32 | * This class extends BasePayment to provide SSLCommerz payment gateway functionality. 33 | * It implements the complete payment lifecycle from initiation to completion, 34 | * including validation and webhook processing. 35 | */ 36 | class Sslcommerz extends BasePayment { 37 | /** 38 | * SSLCommerz API endpoints and configuration constants 39 | */ 40 | private const API_PROCESS_ENDPOINT = '/gwprocess/v4/api.php'; 41 | private const API_VALIDATION_ENDPOINT = '/validator/api/validationserverAPI.php'; 42 | private const DEFAULT_CURRENCY = 'BDT'; 43 | private const DEFAULT_COUNTRY = 'Bangladesh'; 44 | private const DEFAULT_PHONE = '01700000000'; 45 | private const DEFAULT_POSTCODE = '0000'; 46 | private const TRANSACTION_PREFIX = 'TUTOR-'; 47 | private const PRODUCT_CATEGORY = 'education'; 48 | private const PRODUCT_PROFILE = 'non-physical-goods'; 49 | private const SHIPPING_METHOD = 'NO'; 50 | 51 | /** 52 | * Payment status mapping constants 53 | */ 54 | private const STATUS_MAP = [ 55 | 'VALID' => 'paid', 56 | 'VALIDATED' => 'paid', 57 | 'FAILED' => 'failed', 58 | 'CANCELLED' => 'cancelled', 59 | 'PENDING' => 'pending', 60 | ]; 61 | 62 | /** 63 | * Stores the SSLCommerz API client configuration 64 | * 65 | * @var array 66 | */ 67 | protected $client; 68 | 69 | /** 70 | * Checks if all required configuration keys are present and not empty. 71 | * 72 | * Validates that the essential SSLCommerz configuration parameters 73 | * (store_id, store_password, mode) are properly configured before 74 | * allowing payment processing. 75 | * 76 | * @return bool Returns true if all required configuration keys are present and not empty, otherwise false. 77 | */ 78 | public function check(): bool { 79 | $configKeys = Arr::make(['store_id', 'store_password', 'mode']); 80 | 81 | $isConfigOk = $configKeys->every(function ($key) { 82 | return $this->config->has($key) && !empty($this->config->get($key)); 83 | }); 84 | 85 | return $isConfigOk; 86 | } 87 | 88 | /** 89 | * Initializes the necessary configurations for the SSLCommerz payment gateway. 90 | * 91 | * Sets up the client configuration array with store credentials and API domain 92 | * required for SSLCommerz API communication. This method must be called before 93 | * any payment processing operations. 94 | * 95 | * @throws Throwable If configuration retrieval fails or invalid data is provided. 96 | */ 97 | public function setup(): void { 98 | try { 99 | $this->client = [ 100 | 'store_id' => $this->config->get('store_id'), 101 | 'store_password' => $this->config->get('store_password'), 102 | 'api_domain' => $this->config->get('api_domain'), 103 | ]; 104 | } catch (Throwable $error) { 105 | throw $error; 106 | } 107 | } 108 | 109 | /** 110 | * Sets the payment data according to SSLCommerz requirements. 111 | * 112 | * Processes and structures the payment data from Tutor LMS into the format 113 | * expected by the SSLCommerz API. This includes generating transaction IDs, 114 | * formatting amounts, and organizing customer and product information. 115 | * 116 | * @param object $data The payment data object from Tutor LMS. 117 | * @throws Throwable If the parent `setData` method throws an error or data processing fails. 118 | */ 119 | public function setData($data): void { 120 | try { 121 | // Structure the payment data according to SSLCommerz requirements 122 | $structuredData = $this->prepareData($data); 123 | parent::setData($structuredData); 124 | } catch (Throwable $error) { 125 | throw $error; 126 | } 127 | } 128 | 129 | /** 130 | * Prepares the payment data according to SSLCommerz API requirements. 131 | * 132 | * @param object $data Payment data from Tutor 133 | * @return array Formatted data for SSLCommerz 134 | */ 135 | private function prepareData(object $data): array { 136 | // Validate required data 137 | if (!isset($data->order_id) || empty($data->order_id)) { 138 | throw new \InvalidArgumentException(__('Order ID is required for payment processing', 'tutor-sslcommerz')); 139 | } 140 | 141 | if (!isset($data->currency) || !isset($data->currency->code)) { 142 | throw new \InvalidArgumentException(__('Currency information is required for payment processing', 'tutor-sslcommerz')); 143 | } 144 | 145 | if (!isset($data->customer) || !isset($data->customer->email)) { 146 | throw new \InvalidArgumentException(__('Customer email is required for payment processing', 'tutor-sslcommerz')); 147 | } 148 | 149 | // Generate unique transaction ID 150 | $tran_id = self::TRANSACTION_PREFIX . $data->order_id . '-' . time(); 151 | 152 | // Get total price - Tutor uses 'total_price' property 153 | $total_price = isset($data->total_price) && !empty($data->total_price) ? (float) $data->total_price : 0; 154 | 155 | // Validate amount 156 | if ($total_price <= 0) { 157 | throw new \InvalidArgumentException(__('Payment amount must be greater than zero', 'tutor-sslcommerz')); 158 | } 159 | 160 | // Format amounts for SSLCommerz 161 | $total_amount = number_format($total_price, 2, '.', ''); 162 | $product_amount = number_format($total_price, 2, '.', ''); 163 | 164 | // Prepare SSLCommerz required fields 165 | $sslcommerzData = [ 166 | // Required transaction information 167 | 'total_amount' => $total_amount, 168 | 'currency' => $data->currency->code, 169 | 'tran_id' => $tran_id, 170 | 'product_category' => self::PRODUCT_CATEGORY, 171 | 'product_name' => $data->order_description ?? __('Course Purchase', 'tutor-sslcommerz'), 172 | 'product_profile' => self::PRODUCT_PROFILE, 173 | 174 | // URLs 175 | 'success_url' => $this->config->get('success_url'), 176 | 'fail_url' => $this->config->get('cancel_url'), 177 | 'cancel_url' => $this->config->get('cancel_url'), 178 | 'ipn_url' => $this->config->get('webhook_url'), 179 | 180 | // Customer information 181 | 'cus_name' => $data->customer->name ?? __('Customer', 'tutor-sslcommerz'), 182 | 'cus_email' => $data->customer->email, 183 | 'cus_add1' => $data->billing_address->address1 ?? __('N/A', 'tutor-sslcommerz'), 184 | 'cus_add2' => $data->billing_address->address2 ?? '', 185 | 'cus_city' => $data->billing_address->city ?? __('N/A', 'tutor-sslcommerz'), 186 | 'cus_state' => $data->billing_address->state ?? '', 187 | 'cus_postcode' => $data->billing_address->postal_code ?? self::DEFAULT_POSTCODE, 188 | 'cus_country' => $data->billing_address->country->name ?? ($data->currency->code === self::DEFAULT_CURRENCY ? self::DEFAULT_COUNTRY : __('N/A', 'tutor-sslcommerz')), 189 | 'cus_phone' => $data->customer->phone_number ?? self::DEFAULT_PHONE, 190 | 191 | // Shipping information (same as billing for digital products) 192 | 'shipping_method' => self::SHIPPING_METHOD, 193 | 'num_of_item' => 1, 194 | 'ship_name' => $data->customer->name ?? __('Customer', 'tutor-sslcommerz'), 195 | 'ship_add1' => $data->billing_address->address1 ?? __('N/A', 'tutor-sslcommerz'), 196 | 'ship_add2' => $data->billing_address->address2 ?? '', 197 | 'ship_city' => $data->billing_address->city ?? __('N/A', 'tutor-sslcommerz'), 198 | 'ship_state' => $data->billing_address->state ?? '', 199 | 'ship_postcode' => $data->billing_address->postal_code ?? self::DEFAULT_POSTCODE, 200 | 'ship_country' => $data->billing_address->country->name ?? ($data->currency->code === self::DEFAULT_CURRENCY ? self::DEFAULT_COUNTRY : __('N/A', 'tutor-sslcommerz')), 201 | 202 | // Additional information 203 | 'value_a' => $data->order_id, // Store our order ID for reference 204 | 'value_b' => $data->customer->email, 205 | 'value_c' => $data->store_name ?? __('Tutor LMS', 'tutor-sslcommerz'), 206 | 'product_amount' => $product_amount, 207 | ]; 208 | 209 | return $sslcommerzData; 210 | } 211 | 212 | /** 213 | * Creates the payment process by sending data to SSLCommerz gateway. 214 | * 215 | * @throws ErrorException 216 | */ 217 | public function createPayment(): void { 218 | try { 219 | $paymentData = $this->getData(); 220 | 221 | // Add store credentials 222 | $paymentData['store_id'] = $this->client['store_id']; 223 | $paymentData['store_passwd'] = $this->client['store_password']; 224 | 225 | // Make API call to SSLCommerz 226 | $apiUrl = $this->client['api_domain'] . self::API_PROCESS_ENDPOINT; 227 | $response = $this->callSslcommerzApi($apiUrl, $paymentData); 228 | 229 | if ($response && isset($response['status']) && $response['status'] === 'SUCCESS') { 230 | if (isset($response['GatewayPageURL']) && !empty($response['GatewayPageURL'])) { 231 | // Redirect to SSLCommerz payment page 232 | header("Location: " . $response['GatewayPageURL']); 233 | exit; 234 | } else { 235 | throw new ErrorException(__('Gateway URL not found in response', 'tutor-sslcommerz')); 236 | } 237 | } else { 238 | $errorMessage = $response['failedreason'] ?? __('Unknown error occurred', 'tutor-sslcommerz'); 239 | throw new ErrorException(__('SSLCommerz Payment Failed: ', 'tutor-sslcommerz') . $errorMessage); 240 | } 241 | 242 | } catch (RequestException $error) { 243 | throw new ErrorException($error->getMessage()); 244 | } 245 | } 246 | 247 | /** 248 | * Makes a request to SSLCommerz API using WordPress HTTP API 249 | * 250 | * @param string $url API endpoint 251 | * @param array $data Post data 252 | * @return array Response data 253 | */ 254 | private function callSslcommerzApi(string $url, array $data): array { 255 | // Set SSL verification based on environment 256 | $isLocalhost = $this->config->get('mode') === 'sandbox'; 257 | $ssl_verify = !$isLocalhost; // Verify SSL in production, skip in sandbox 258 | 259 | // Prepare arguments for wp_remote_post 260 | $args = [ 261 | 'method' => 'POST', 262 | 'timeout' => 60, 263 | 'redirection' => 5, 264 | 'httpversion' => '1.1', 265 | 'blocking' => true, 266 | 'headers' => [ 267 | 'Content-Type' => 'application/x-www-form-urlencoded', 268 | ], 269 | 'body' => $data, 270 | 'sslverify' => $ssl_verify, 271 | ]; 272 | 273 | // Make the request 274 | $response = wp_remote_post($url, $args); 275 | 276 | // Check for errors 277 | if (is_wp_error($response)) { 278 | return ['status' => 'FAILED', 'failedreason' => __('Failed to connect with SSLCommerz API: ', 'tutor-sslcommerz') . $response->get_error_message()]; 279 | } 280 | 281 | // Get response code and body 282 | $http_code = wp_remote_retrieve_response_code($response); 283 | $body = wp_remote_retrieve_body($response); 284 | 285 | if ($http_code == 200 && !empty($body)) { 286 | $decoded = json_decode($body, true); 287 | if (json_last_error() === JSON_ERROR_NONE) { 288 | return $decoded; 289 | } else { 290 | return ['status' => 'FAILED', 'failedreason' => __('Invalid JSON response from SSLCommerz API', 'tutor-sslcommerz')]; 291 | } 292 | } else { 293 | return ['status' => 'FAILED', 'failedreason' => __('Failed to connect with SSLCommerz API (HTTP ', 'tutor-sslcommerz') . $http_code . ')']; 294 | } 295 | } 296 | 297 | /** 298 | * Verifies and processes the order data received from SSLCommerz. 299 | * 300 | * @param object $payload Webhook payload 301 | * @return object Order data 302 | * @throws Throwable 303 | */ 304 | public function verifyAndCreateOrderData(object $payload): object { 305 | $returnData = System::defaultOrderData(); 306 | 307 | try { 308 | // Get POST data from SSLCommerz IPN/Success callback 309 | $post_data = $payload->post; 310 | 311 | // Validate that we have POST data 312 | if (empty($post_data) || !is_array($post_data)) { 313 | $returnData->payment_status = 'failed'; 314 | $returnData->payment_error_reason = __('No transaction data received. IPN endpoint should only receive POST requests from SSLCommerz.', 'tutor-sslcommerz'); 315 | return $returnData; 316 | } 317 | 318 | // Sanitize POST data 319 | $sanitized_post = []; 320 | foreach ($post_data as $key => $value) { 321 | $sanitized_post[$key] = is_array($value) ? array_map('sanitize_text_field', array_map('wp_unslash', $value)) : sanitize_text_field(wp_unslash($value)); 322 | } 323 | 324 | if (empty($sanitized_post['tran_id']) || empty($sanitized_post['status'])) { 325 | $returnData->payment_status = 'failed'; 326 | $returnData->payment_error_reason = __('Invalid transaction data: Missing transaction ID or status.', 'tutor-sslcommerz'); 327 | return $returnData; 328 | } 329 | 330 | $tran_id = $sanitized_post['tran_id']; 331 | $amount = $sanitized_post['amount'] ?? 0; 332 | $currency = $sanitized_post['currency'] ?? 'BDT'; 333 | $status = $sanitized_post['status']; 334 | 335 | // Validate the transaction with SSLCommerz 336 | $validated = $this->validateTransaction($sanitized_post); 337 | 338 | if ($validated) { 339 | // Extract order ID from value_a or tran_id 340 | $order_id = $sanitized_post['value_a'] ?? ''; 341 | 342 | // Map SSLCommerz status to Tutor status 343 | $payment_status = $this->mapPaymentStatus($status); 344 | 345 | $returnData->id = $order_id; 346 | $returnData->payment_status = $payment_status; 347 | $returnData->transaction_id = $sanitized_post['bank_tran_id'] ?? $tran_id; 348 | $returnData->payment_payload = json_encode($sanitized_post); 349 | $returnData->payment_error_reason = $status !== 'VALID' && $status !== 'VALIDATED' ? ($sanitized_post['error'] ?? __('Payment failed', 'tutor-sslcommerz')) : ''; 350 | 351 | // Calculate fees and earnings (SSLCommerz deducts their fee) 352 | $store_amount = floatval($sanitized_post['store_amount'] ?? $amount); 353 | $gateway_fee = floatval($amount) - $store_amount; 354 | 355 | $returnData->fees = number_format($gateway_fee, 2, '.', ''); 356 | $returnData->earnings = number_format($store_amount, 2, '.', ''); 357 | $returnData->tax_amount = 0; // SSLCommerz doesn't provide tax information 358 | 359 | } else { 360 | // Validation failed 361 | $returnData->payment_status = 'failed'; 362 | $returnData->payment_error_reason = __('Transaction validation with SSLCommerz API failed.', 'tutor-sslcommerz'); 363 | } 364 | 365 | return $returnData; 366 | 367 | } catch (Throwable $error) { 368 | // Log the error for debugging if WP_DEBUG is enabled 369 | if (defined('WP_DEBUG') && WP_DEBUG) { 370 | error_log('SSLCommerz IPN Error: ' . $error->getMessage()); 371 | } 372 | 373 | // Return failed status instead of throwing 374 | $returnData->payment_status = 'failed'; 375 | $returnData->payment_error_reason = __('Error processing payment: ', 'tutor-sslcommerz') . $error->getMessage(); 376 | return $returnData; 377 | } 378 | } 379 | 380 | /** 381 | * Validates transaction with SSLCommerz validation API 382 | * 383 | * @param array $post_data POST data from callback 384 | * @param string $tran_id Transaction ID 385 | * @param float $amount Transaction amount 386 | * @param string $currency Currency code 387 | * @return bool 388 | */ 389 | private function validateTransaction(array $post_data): bool { 390 | // First verify hash if present 391 | if (!$this->verifyHash($post_data)) { 392 | return false; 393 | } 394 | 395 | $tran_id = $post_data['tran_id']; 396 | $amount = $post_data['amount'] ?? 0; 397 | $currency = $post_data['currency'] ?? 'BDT'; 398 | 399 | // Call SSLCommerz validation API using WordPress HTTP API 400 | $val_id = urlencode($post_data['val_id'] ?? ''); 401 | $store_id = urlencode($this->client['store_id']); 402 | $store_passwd = urlencode($this->client['store_password']); 403 | 404 | $validationUrl = $this->client['api_domain'] . self::API_VALIDATION_ENDPOINT . '?val_id=' . $val_id . '&store_id=' . $store_id . '&store_passwd=' . $store_passwd . '&v=1&format=json'; 405 | 406 | // Set SSL verification based on environment 407 | $isLocalhost = $this->config->get('mode') === 'sandbox'; 408 | $ssl_verify = !$isLocalhost; 409 | 410 | // Make GET request using wp_remote_get 411 | $args = [ 412 | 'timeout' => 30, 413 | 'sslverify' => $ssl_verify, 414 | ]; 415 | 416 | $response = wp_remote_get($validationUrl, $args); 417 | 418 | // Check for errors 419 | if (is_wp_error($response)) { 420 | return false; 421 | } 422 | 423 | // Get response code and body 424 | $code = wp_remote_retrieve_response_code($response); 425 | $body = wp_remote_retrieve_body($response); 426 | 427 | if ($code == 200 && !empty($body)) { 428 | $result = json_decode($body); 429 | 430 | if (json_last_error() === JSON_ERROR_NONE && isset($result->status) && ($result->status === 'VALID' || $result->status === 'VALIDATED')) { 431 | // Verify transaction details match 432 | if ($currency === 'BDT') { 433 | return trim($tran_id) === trim($result->tran_id) && abs($amount - $result->amount) < 1; 434 | } else { 435 | return trim($tran_id) === trim($result->tran_id) && abs($amount - $result->currency_amount) < 1; 436 | } 437 | } 438 | } 439 | 440 | return false; 441 | } 442 | 443 | /** 444 | * Verifies the hash signature from SSLCommerz 445 | * 446 | * @param array $post_data POST data 447 | * @return bool 448 | */ 449 | private function verifyHash(array $post_data): bool { 450 | // Hash verification is optional but recommended 451 | if (!isset($post_data['verify_sign']) || !isset($post_data['verify_key'])) { 452 | return true; // If no hash provided, skip verification 453 | } 454 | 455 | $pre_define_key = explode(',', $post_data['verify_key']); 456 | $new_data = []; 457 | 458 | foreach ($pre_define_key as $value) { 459 | if (isset($post_data[$value])) { 460 | $new_data[$value] = $post_data[$value]; 461 | } 462 | } 463 | 464 | $new_data['store_passwd'] = md5($this->client['store_password']); 465 | ksort($new_data); 466 | 467 | $hash_string = ""; 468 | foreach ($new_data as $key => $value) { 469 | $hash_string .= $key . '=' . $value . '&'; 470 | } 471 | $hash_string = rtrim($hash_string, '&'); 472 | 473 | return md5($hash_string) === $post_data['verify_sign']; 474 | } 475 | 476 | /** 477 | * Maps SSLCommerz payment status to Tutor payment status 478 | * 479 | * @param string $sslcommerzStatus 480 | * @return string 481 | */ 482 | private function mapPaymentStatus(string $sslcommerzStatus): string { 483 | return self::STATUS_MAP[$sslcommerzStatus] ?? 'failed'; 484 | } 485 | } --------------------------------------------------------------------------------