Add modules/gateways/hutko/hutko_helper.php
This commit is contained in:
329
modules/gateways/hutko/hutko_helper.php
Normal file
329
modules/gateways/hutko/hutko_helper.php
Normal file
@@ -0,0 +1,329 @@
|
||||
<?php
|
||||
|
||||
class Hutko_Helper
|
||||
{
|
||||
const ORDER_APPROVED = 'approved';
|
||||
const ORDER_DECLINED = 'declined';
|
||||
|
||||
const ORDER_SEPARATOR = '#';
|
||||
|
||||
const SIGNATURE_SEPARATOR = '|';
|
||||
const REDIRECT_URL = 'https://pay.hutko.org/api/checkout/redirect/';
|
||||
const REFUND_URL = 'https://pay.hutko.org/api/reverse/order_id';
|
||||
|
||||
|
||||
/**
|
||||
* Initiates a refund (reverse) request via Hutko API.
|
||||
*
|
||||
* @param string $order_id The gateway's order ID to refund.
|
||||
* @param float $amount The amount to refund (in base units, e.g., 100.50).
|
||||
* @param string $currency The currency code (e.g., 'UAH').
|
||||
* @param string $comment Optional comment for the refund.
|
||||
* @return array Decoded API response array. Returns an error structure on failure.
|
||||
*/
|
||||
public static function refund(array $params): array
|
||||
{
|
||||
// 1. Prepare the data payload
|
||||
$data = [
|
||||
'order_id' => $params['transid'],
|
||||
// Assuming Configuration::get is available to fetch the merchant ID
|
||||
'merchant_id' => $params['accountID'],
|
||||
'version' => '1.0',
|
||||
// Amount should be in minor units (cents, kopecks) and converted to string as per API example
|
||||
'amount' => round($params['amount'] * 100),
|
||||
'currency' => $params['currency'],
|
||||
];
|
||||
|
||||
if (isset($params['description']) && !empty($params['description'])) {
|
||||
$data['comment'] = 'Return ' . $params['description'];
|
||||
}
|
||||
|
||||
// 2. Calculate the signature based on the data array *before* wrapping in 'request'
|
||||
$data['signature'] = self::getSignature($data, $params['secretKey']);
|
||||
return self::sendAPICall(self::REFUND_URL, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates a request via Hutko API.
|
||||
*
|
||||
* @param string $url The gateway's url.
|
||||
* @param array $data The data.
|
||||
* @return array Decoded API response array. Returns an error structure on failure.
|
||||
*/
|
||||
public static function sendAPICall(string $url, array $data, int $timeout = 60): array
|
||||
{
|
||||
|
||||
|
||||
// Wrap the prepared data inside the 'request' key as required by the API
|
||||
$requestPayload = ['request' => $data];
|
||||
|
||||
// Convert the payload to JSON string
|
||||
$jsonPayload = json_encode($requestPayload);
|
||||
|
||||
if ($jsonPayload === false) {
|
||||
// Handle JSON encoding error
|
||||
return [
|
||||
'response' => [
|
||||
'response_status' => 'failure',
|
||||
'error_message' => 'Failed to encode request data to JSON: ' . json_last_error_msg(),
|
||||
'error_code' => 'JSON_ENCODE_ERROR'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
// Initialize CURL
|
||||
$ch = curl_init();
|
||||
|
||||
// 4. Set CURL options
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_POST, true); // Use POST method
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonPayload); // Set the JSON body
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // Return the response as a string
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: application/json',
|
||||
'Content-Length: ' . strlen($jsonPayload), // Good practice
|
||||
]);
|
||||
|
||||
// Recommended for production: Verify SSL certificate
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); // Verify hostname against certificate
|
||||
|
||||
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); // Timeout in seconds
|
||||
|
||||
// Execute the CURL request
|
||||
$response = curl_exec($ch);
|
||||
|
||||
// Check for CURL errors
|
||||
$curl_error = curl_error($ch);
|
||||
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
if ($curl_error) {
|
||||
// Log the error or handle it appropriately
|
||||
return [
|
||||
'response' => [
|
||||
'response_status' => 'failure',
|
||||
'error_message' => 'CURL Error: ' . $curl_error,
|
||||
'error_code' => 'CURL_' . curl_errno($ch),
|
||||
'http_code' => $http_code // Include http code for context
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
// Process the response
|
||||
// Decode the JSON response into a PHP array
|
||||
$responseData = json_decode($response, true);
|
||||
|
||||
// Check if JSON decoding failed
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
// Log the error or handle it appropriately
|
||||
return [
|
||||
'response' => [
|
||||
'response_status' => 'failure',
|
||||
'error_message' => 'Invalid JSON response from API: ' . json_last_error_msg(),
|
||||
'error_code' => 'JSON_DECODE_ERROR',
|
||||
'http_code' => $http_code,
|
||||
'raw_response' => $response // Include raw response for debugging
|
||||
]
|
||||
];
|
||||
}
|
||||
return $responseData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the signature of a payment gateway response.
|
||||
*
|
||||
* This method verifies that the received response originates from the expected merchant
|
||||
* and that the signature matches the calculated signature based on the response data
|
||||
* and the merchant's secret key.
|
||||
*
|
||||
* @param array $response An associative array containing the payment gateway's response data.
|
||||
* This array is expected to include keys 'merchant_id' and 'signature'.
|
||||
* It might also contain temporary signature-related keys that will be unset
|
||||
* during the validation process.
|
||||
* @return bool True if the response is valid (merchant ID matches and signature is correct),
|
||||
* false otherwise.
|
||||
*/
|
||||
public static function validateResponse(array $response, array $gatewayParams): bool
|
||||
{
|
||||
// 1. Verify the Merchant ID
|
||||
if ((string)$gatewayParams['accountID'] !== (string)$response['merchant_id']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. Prepare Response Data for Signature Verification
|
||||
$responseSignature = $response['signature'];
|
||||
|
||||
// Unset signature-related keys that should not be part of the signature calculation.
|
||||
// This ensures consistency with how the signature was originally generated.
|
||||
if (isset($response['response_signature_string'])) unset($response['response_signature_string']);
|
||||
if (isset($response['signature'])) unset($response['signature']);
|
||||
|
||||
// FIX: WHMCS sanitizes $_POST (converts " to ").
|
||||
// We must revert this to get the raw JSON for valid signature calculation.
|
||||
foreach ($response as $k => $v) {
|
||||
if (is_string($v)) {
|
||||
$response[$k] = html_entity_decode($v, ENT_QUOTES);
|
||||
}
|
||||
}
|
||||
// 3. Calculate and Compare Signatures
|
||||
$calculatedSignature = self::getSignature($response, $gatewayParams['secretKey']);
|
||||
|
||||
return hash_equals($calculatedSignature, $responseSignature);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Generates a URL-friendly slug from a given text.
|
||||
*
|
||||
* This method transliterates non-ASCII characters to their closest ASCII equivalents,
|
||||
* removes any characters that are not alphanumeric or spaces, trims leading/trailing
|
||||
* spaces, optionally replaces spaces with hyphens, and optionally converts the
|
||||
* entire string to lowercase.
|
||||
*
|
||||
* @param string $text The input string to convert into a slug.
|
||||
* @param bool $removeSpaces Optional. Whether to replace spaces with hyphens (true) or keep them (false). Defaults to false.
|
||||
* @param bool $lowerCase Optional. Whether to convert the resulting slug to lowercase (true) or keep the original casing (false). Defaults to false.
|
||||
* @return string The generated slug.
|
||||
*/
|
||||
public static function getSlug(string $text, bool $removeSpaces = false, bool $lowerCase = false): string
|
||||
{
|
||||
// 1. Transliterate non-ASCII characters to ASCII.
|
||||
$text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);
|
||||
|
||||
// 2. Remove any characters that are not alphanumeric or spaces.
|
||||
$text = preg_replace("/[^a-zA-Z0-9 ]/", "", $text);
|
||||
|
||||
// 3. Trim leading and trailing spaces.
|
||||
$text = trim($text, ' ');
|
||||
|
||||
// 4. Optionally replace spaces with hyphens.
|
||||
if ($removeSpaces) {
|
||||
$text = str_replace(' ', '-', $text);
|
||||
}
|
||||
|
||||
// 5. Optionally convert the slug to lowercase.
|
||||
if ($lowerCase) {
|
||||
$text = strtolower($text);
|
||||
}
|
||||
|
||||
// 6. Return the generated slug.
|
||||
return $text;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Generates a signature based on the provided data and a secret password.
|
||||
*
|
||||
* This method filters out empty and null values from the input data, sorts the remaining
|
||||
* data alphabetically by key, concatenates the values with a pipe delimiter, prepends
|
||||
* the secret password, and then generates a SHA1 hash of the resulting string.
|
||||
*
|
||||
* @param array $data An associative array of data to be included in the signature generation.
|
||||
* Empty strings and null values in this array will be excluded.
|
||||
* @param string $password The secret key used to generate the signature. This should be
|
||||
* kept confidential.
|
||||
* @param bool $encoded Optional. Whether to return the SHA1 encoded signature (true by default)
|
||||
* or the raw string before encoding (false).
|
||||
* @return string The generated signature (SHA1 hash by default) or the raw string.
|
||||
*/
|
||||
public static function getSignature(array $data, string $secretKey, bool $encoded = true): string
|
||||
{
|
||||
|
||||
if (!$secretKey || empty($secretKey)) {
|
||||
throw new Exception('Merchant secret not set');
|
||||
}
|
||||
// 1. Filter out empty and null values from the data array.
|
||||
$filteredData = array_filter($data, function ($value) {
|
||||
return $value !== '' && $value !== null;
|
||||
});
|
||||
|
||||
// 2. Sort the filtered data array alphabetically by key.
|
||||
ksort($filteredData);
|
||||
|
||||
// 3. Construct the string to be hashed. Start with the password.
|
||||
$stringToHash = $secretKey;
|
||||
|
||||
// 4. Append the values from the sorted data array, separated by a pipe.
|
||||
foreach ($filteredData as $value) {
|
||||
$stringToHash .= self::SIGNATURE_SEPARATOR . $value;
|
||||
}
|
||||
|
||||
// 5. Return the SHA1 hash of the string or the raw string based on the $encoded flag.
|
||||
if ($encoded) {
|
||||
return sha1($stringToHash);
|
||||
} else {
|
||||
return $stringToHash;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Builds a base64 encoded JSON string containing reservation-related data.
|
||||
*
|
||||
* This method gathers information about the current cart, customer's delivery
|
||||
* address, shop details, and products in the cart to create an array. This
|
||||
* array is then encoded as a JSON string and subsequently base64 encoded
|
||||
* for transmission or storage.
|
||||
*
|
||||
* @return string A base64 encoded JSON string containing the reservation data.
|
||||
*/
|
||||
public static function buildReservationData(array $params): string
|
||||
{
|
||||
|
||||
// 3. Construct the data array.
|
||||
$data = [
|
||||
"cms_name" => "WHMCS",
|
||||
"cms_version" => $params['whmcsVersion'],
|
||||
"shop_domain" => $params['systemurl'],
|
||||
"phonemobile" => $params['clientdetails']['phonenumber'],
|
||||
"customer_address" => self::getSlug($params['clientdetails']['address1']),
|
||||
"customer_country" => self::getSlug($params['clientdetails']['country']),
|
||||
"customer_state" => self::getSlug($params['clientdetails']['state']),
|
||||
"customer_name" => self::getSlug($params['clientdetails']['lastname'] . ' ' . $params['clientdetails']['firstname']),
|
||||
"customer_city" => self::getSlug($params['clientdetails']['city']),
|
||||
"customer_zip" => $params['clientdetails']['postcode'],
|
||||
"account" => $params['clientdetails']['email'],
|
||||
"uuid" => hash('sha256', $params['clientdetails']['email'] . '|' . $params['systemurl']),
|
||||
];
|
||||
|
||||
|
||||
|
||||
|
||||
return base64_encode(json_encode($data));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Helper method to parse the request body from raw input.
|
||||
*
|
||||
* @return array The parsed request body.
|
||||
*/
|
||||
public static function getCallbackContent($gatewayParams): array
|
||||
{
|
||||
if (!empty($_POST)) {
|
||||
$calbackContent = $_POST;
|
||||
} else {
|
||||
$calbackContent = json_decode(file_get_contents("php://input"), true);
|
||||
}
|
||||
if (!is_array($calbackContent) || !count($calbackContent)) {
|
||||
throw new Exception('Empty or malformed request');
|
||||
}
|
||||
// Assuming validateResponse returns true on success, or a string error message on failure.
|
||||
$isSignatureValid = self::validateResponse($calbackContent, $gatewayParams);
|
||||
if ($isSignatureValid !== true) {
|
||||
\logTransaction($gatewayParams['name'] . ' [callback]', $calbackContent, 'Invalid hutko signature');
|
||||
|
||||
throw new Exception('Invalid hutko signature');
|
||||
}
|
||||
|
||||
return $calbackContent;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user