Files
OpenCart4/catalog/library/payanyway/traits/payanyway.php
T

326 lines
11 KiB
PHP
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace Opencart\Catalog\Library\Extension\PayAnyWay\Traits;
trait PayAnyWay
{
// Статические свойства, а не константы: константы в трейтах — только с PHP 8.2, модуль — 8.0+.
private static string $transactionIdDelimiter = '_';
private static string $transactionIdZone = 'UTC';
private static string $transactionIdPattern = '/^([1-9][0-9]*)_(0|[1-9][0-9]{0,15})_([0-9]{14})UTC$/D';
// Номер модуля 1.0.2 и раньше: <номер заказа>|<YmdHis> в поясе сервера, без суммы.
private static string $legacyTransactionIdPattern = '/^([1-9][0-9]*)\\|([0-9]{14})$/D';
private static string $operationIdPattern = '/^[0-9]{1,20}$/D';
private static int $orderLockTimeoutSeconds = 10;
private static string $moduleCode = 'payanyway';
private static string $moduleVersion = '1.0.3';
private static string $defaultCmsName = 'OpenCart';
private static int $clientEmailMaxLength = 64;
private static int $clientPhoneMaxLength = 19;
private static string $defaultPaymentMethod = 'full_payment';
private static string $defaultPaymentObject = 'commodity';
private static array $defaultCanceledOrderStatusIds = [7, 9, 16];
private static string $coreOrderMailFile = '/catalog/controller/mail/order.php';
private int $mntId;
private string $integrityCode;
private string $server;
private string $vatRate;
private string $currency;
private int $pendingOrderStatusId;
private int $successOrderStatusId;
private int $errorOrderStatusId;
private bool $testMode;
private bool $fiscalization;
private string $receiptEmail;
private string $paymentMethod;
private string $paymentObject;
/** @var int[] */
private array $canceledOrderStatusIds;
private string $cmsName;
private ?int $orderId = null;
private array $orderInfo = [];
private function initializeModule(): void
{
$this->mntId = (int)$this->config->get('payment_payanyway_account');
$this->integrityCode = (string)$this->config->get('payment_payanyway_integrity_code');
$this->server = (string)$this->config->get('payment_payanyway_server');
$this->vatRate = (string)$this->config->get('payment_payanyway_vat_rate');
$this->currency = (string)$this->config->get('payment_payanyway_currency');
$this->pendingOrderStatusId = (int)$this->config->get('payment_payanyway_pending_order_status_id');
$this->successOrderStatusId = (int)$this->config->get('payment_payanyway_success_order_status_id');
$this->errorOrderStatusId = (int)$this->config->get('payment_payanyway_error_order_status_id');
$this->testMode = (bool)$this->config->get('payment_payanyway_test_mode');
$this->fiscalization = !$this->config->has('payment_payanyway_fiscalization')
|| (bool)$this->config->get('payment_payanyway_fiscalization');
$this->receiptEmail = $this->normalizeEmail($this->config->get('payment_payanyway_receipt_email')) ?? '';
$paymentMethod = (string)$this->config->get('payment_payanyway_payment_method');
$this->paymentMethod = ('' !== $paymentMethod) ? $paymentMethod : self::$defaultPaymentMethod;
$paymentObject = (string)$this->config->get('payment_payanyway_payment_object');
$this->paymentObject = ('' !== $paymentObject) ? $paymentObject : self::$defaultPaymentObject;
$this->canceledOrderStatusIds = $this->config->has('payment_payanyway_canceled_order_status_ids')
? array_map('intval', (array)$this->config->get('payment_payanyway_canceled_order_status_ids'))
: self::$defaultCanceledOrderStatusIds;
$this->cmsName = $this->detectCmsName();
}
private function detectCmsName(): string
{
$cmsName = (string)$this->config->get('payment_payanyway_cms_name');
if ('' !== $cmsName) {
return $cmsName;
}
$legacy = (string)$this->config->get('payment_payanyway_cms_module_version');
if (preg_match('/^(.+?) v/', $legacy, $matches)) {
return $matches[1];
}
return self::$defaultCmsName;
}
private function getCms(): string
{
return sprintf('%s v%s_PHP %s_%s v%s',
$this->cmsName,
defined('VERSION') ? constant('VERSION') : 'unknown',
PHP_VERSION,
self::$moduleCode,
self::$moduleVersion
);
}
private function loadOrder(?int $orderId): void
{
$this->orderId = $orderId;
$this->orderInfo = [];
if (null === $orderId) {
return;
}
$this->load->model('checkout/order');
$this->orderInfo = $this->model_checkout_order->getOrder($orderId);
}
private function validateString(string $value, int $maxLength): string
{
$maxLength = max(0, $maxLength);
$value = $this->sanitizeString($value);
if (mb_strlen($value, 'UTF-8') <= $maxLength) {
return $value;
}
$trimLength = max(0, $maxLength - 3);
return mb_substr($value, 0, $trimLength, 'UTF-8') . ($maxLength > 3 ? '...' : '');
}
private function sanitizeString(string $value): string
{
$decoded = html_entity_decode($value, ENT_QUOTES | ENT_HTML5, 'UTF-8');
$cleaned = preg_replace('/[^\p{L}\p{N}\s:.,()_№+-]/u', '', $decoded);
$cleaned = str_replace(['&', '/', '\\', ';', '%', '#', '"', "'"], '', $cleaned);
return trim(preg_replace('/\s+/', ' ', $cleaned));
}
private function formatPrice(float $price): string
{
return number_format($price, 2, '.', '');
}
private function toKopecks(float|int|string $amount): int
{
$currencyValue = (float)($this->orderInfo['currency_value'] ?? 0);
if ($currencyValue <= 0) {
$currencyValue = 1.0;
}
return (int)round((float)$amount * $currencyValue * 100);
}
private function getOrderAmount(): string
{
return $this->formatPrice($this->toKopecks($this->orderInfo['total'] ?? 0) / 100);
}
/**
* @return array<string, mixed>
*/
private function getCallbackRequestData(): array
{
return array_merge($_GET, $_POST);
}
/**
* @return array{order_id: int, amount: ?string, issued_at: string}|null
*/
private function parseTransactionId(mixed $transactionId): ?array
{
if (!is_string($transactionId)) {
return null;
}
if (preg_match(self::$transactionIdPattern, $transactionId, $matches)) {
$kopecks = str_pad($matches[2], 3, '0', STR_PAD_LEFT);
return [
'order_id' => (int)$matches[1],
'amount' => substr($kopecks, 0, -2) . '.' . substr($kopecks, -2),
'issued_at' => $matches[3],
];
}
if (preg_match(self::$legacyTransactionIdPattern, $transactionId, $matches)) {
return ['order_id' => (int)$matches[1], 'amount' => null, 'issued_at' => $matches[2]];
}
return null;
}
private function getOrderIdFromSession(): ?int
{
return isset($this->session->data['order_id']) ? (int)$this->session->data['order_id'] : null;
}
private function isPaidWithModule(): bool
{
$code = $this->orderInfo['payment_method']['code'] ?? '';
return is_string($code) && (self::$moduleCode === strtok($code, '.'));
}
/**
* @param string $amount MNT_AMOUNT формы — с двумя знаками после точки
* @throws \DateInvalidTimeZoneException
*/
private function createTransactionId(int $orderId, string $amount): string
{
$kopecks = ltrim(str_replace('.', '', $amount), '0');
$issuedAt = new \DateTimeImmutable('now', new \DateTimeZone(self::$transactionIdZone));
return $orderId
. self::$transactionIdDelimiter . (('' !== $kopecks) ? $kopecks : '0')
. self::$transactionIdDelimiter . $issuedAt->format('YmdHis') . self::$transactionIdZone;
}
private function addOrderHistory(int $orderId, int $orderStatusId, string $comment): void
{
$this->load->model('checkout/order');
// OpenCart 4.0.2.x: письмо о заказе (событие mail/order до addHistory) читает
// shipping_method['name'] у заказа без доставки — Warning, а обработчик ядра выводит его
// в ответ или уходит на error_page с exit: confirm() отвечает не JSON, колбэк — не XML,
// статус не меняется. Глушится только это предупреждение, письмо уходит без способа доставки.
$previousHandler = set_error_handler(static function (int $severity, string $message, string $file, int $line) use (&$previousHandler): bool {
if ((E_WARNING === $severity)
&& str_ends_with(str_replace('\\', '/', $file), self::$coreOrderMailFile)
&& str_starts_with($message, 'Trying to access array offset on')
) {
return true;
}
return (null !== $previousHandler)
&& (false !== $previousHandler(...self::errorHandlerArguments($previousHandler, [$severity, $message, $file, $line])));
});
try {
$this->model_checkout_order->addHistory($orderId, $orderStatusId, $comment);
} finally {
restore_error_handler();
}
}
/**
* Аргументы для прежнего обработчика ошибок под его сигнатуру: из файла со strict_types
* int в string-параметр не приводится. Обработчик ядра 4.0.x — error(string $code, …,
* string $line), 4.1 — (int $code, …, int $line).
*
* @param array{0: int, 1: string, 2: string, 3: int} $arguments
* @return array<int, int|string>
* @throws \ReflectionException
*/
private static function errorHandlerArguments(callable $handler, array $arguments): array
{
$parameters = (new \ReflectionFunction(\Closure::fromCallable($handler)))->getParameters();
foreach ($parameters as $index => $parameter) {
$type = $parameter->getType();
if (isset($arguments[$index]) && ($type instanceof \ReflectionNamedType) && ('string' === $type->getName())) {
$arguments[$index] = (string)$arguments[$index];
}
}
return $arguments;
}
private function isAwaitingPayment(int $orderStatusId): bool
{
return in_array($orderStatusId, [0, $this->pendingOrderStatusId, $this->errorOrderStatusId], true);
}
/** @return non-empty-string|null */
private function getClientName(): ?string
{
$name = trim(($this->orderInfo['firstname'] ?? '') . ' ' . ($this->orderInfo['lastname'] ?? ''));
return ('' !== $name) ? $name : null;
}
/** @return non-empty-string|null */
private function getClientEmail(): ?string
{
return $this->normalizeEmail($this->orderInfo['email'] ?? '');
}
/** @return non-empty-string|null */
private function getClientPhone(): ?string
{
$digits = preg_replace('/\D/', '', trim((string)($this->orderInfo['telephone'] ?? '')));
return (('' !== $digits) && (strlen($digits) + 1 <= self::$clientPhoneMaxLength)) ? '+' . $digits : null;
}
private function normalizeEmail(mixed $email): ?string
{
$email = trim((string)$email);
if (('' === $email) || (strlen($email) > self::$clientEmailMaxLength) || (false === filter_var($email, FILTER_VALIDATE_EMAIL))) {
return null;
}
return $email;
}
/** @return non-empty-string|null */
private function getClientName(): ?string
{
$name = trim(($this->orderInfo['firstname'] ?? '') . ' ' . ($this->orderInfo['lastname'] ?? ''));
return ('' !== $name) ? $name : null;
}
/** @return non-empty-string|null */
private function getClientEmail(): ?string
{
$email = trim((string)($this->orderInfo['email'] ?? ''));
return ('' !== $email) ? filter_var($email, FILTER_SANITIZE_EMAIL) : null;
}
/** @return non-empty-string|null */
private function getClientPhone(): ?string
{
$digits = preg_replace('/\D/', '', trim((string)($this->orderInfo['telephone'] ?? '')));
return ('' !== $digits) ? '+' . $digits : null;
}
}