fix/security-audit
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
class ControllerExtensionPaymentPayanyway extends Controller {
|
||||
const MODULE_NAME = 'payanyway';
|
||||
const MODULE_VERSION = '1.0.5';
|
||||
const MODULE_VERSION = '1.0.6';
|
||||
const CMS_NAME = 'OpenCart';
|
||||
|
||||
const PAYMENT_SERVER_PROD = 'prod';
|
||||
@@ -20,8 +20,12 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
||||
const TEXT_CONTENT_TYPE = 'text/plain; charset=UTF-8';
|
||||
|
||||
const TRANSACTION_ID_DELIMITER = '_';
|
||||
const LEGACY_TRANSACTION_ID_DELIMITER = '|';
|
||||
const TRANSACTION_ID_PATTERN = '/^([1-9][0-9]*)[%s]([0-9]{14})([A-Z0-9+-]{1,6})?$/';
|
||||
const TRANSACTION_ID_ZONE = 'UTC';
|
||||
const TRANSACTION_ID_PATTERN = '/^([1-9][0-9]*)_(0|[1-9][0-9]{0,15})_([0-9]{14})UTC$/D';
|
||||
const LEGACY_TRANSACTION_ID_PATTERN = '/^([1-9][0-9]*)\\|([0-9]{14})$/D';
|
||||
const LEGACY_ZONED_TRANSACTION_ID_PATTERN = '/^([1-9][0-9]*)_([0-9]{14})([A-Z]{1,6}|[+-][0-9]{2}|[+-][0-9]{4})?$/D';
|
||||
const LEGACY_TRANSACTION_MAX_AGE_DAYS = 14;
|
||||
const OPERATION_ID_PATTERN = '/^[0-9]{1,20}$/D';
|
||||
const DESCRIPTION_MAX_LENGTH = 500;
|
||||
|
||||
const CLIENT_NAME_MAX_LENGTH = 256;
|
||||
@@ -40,6 +44,8 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
||||
const DEFAULT_CANCELED_ORDER_STATUS_IDS = '7,9,16';
|
||||
|
||||
const JSON_NUMBER_PREFIX = '__payanyway_number:';
|
||||
const JSON_NUMBER_KEYS = 'amount|price';
|
||||
const ORDER_LOCK_TIMEOUT_SECONDS = 10;
|
||||
|
||||
/** @var int */
|
||||
private $mntId;
|
||||
@@ -71,7 +77,7 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
||||
/** @var bool */
|
||||
private $fiscalization;
|
||||
|
||||
/** @var string «Email для чеков»: подставляется, если у покупателя нет ни email, ни телефона */
|
||||
/** @var string */
|
||||
private $receiptEmail;
|
||||
|
||||
/** @var string */
|
||||
@@ -113,8 +119,8 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
||||
$data['action'] = (self::PAYMENT_SERVER_PROD === $this->server) ? self::PAYMENT_URL_PROD : self::PAYMENT_URL_DEMO;
|
||||
|
||||
$data['MNT_ID'] = $this->mntId;
|
||||
$data['MNT_TRANSACTION_ID'] = $this->createTransactionId();
|
||||
$data['MNT_AMOUNT'] = $this->getOrderAmount();
|
||||
$data['MNT_TRANSACTION_ID'] = $this->createTransactionId($data['MNT_AMOUNT']);
|
||||
$data['MNT_CURRENCY_CODE'] = $this->currency;
|
||||
$data['MNT_TEST_MODE'] = $this->testMode ? '1' : '0';
|
||||
$data['MNT_DESCRIPTION'] = $this->getDescription();
|
||||
@@ -159,17 +165,7 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
||||
|
||||
$this->load->language('extension/payment/payanyway');
|
||||
|
||||
$orderStatusId = isset($this->orderInfo['order_status_id']) ? (int)$this->orderInfo['order_status_id'] : null;
|
||||
$paymentCode = isset($this->orderInfo['payment_code']) ? $this->orderInfo['payment_code'] : '';
|
||||
if (self::MODULE_NAME !== $paymentCode) {
|
||||
$json['success'] = false;
|
||||
} else {
|
||||
if ((null !== $orderStatusId) && ($orderStatusId !== $this->successOrderStatusId)) {
|
||||
$this->addOrderHistory($this->pendingOrderStatusId, $this->language->get('text_order_confirmed'));
|
||||
}
|
||||
|
||||
$json['success'] = true;
|
||||
}
|
||||
$json['success'] = $this->changeStatusUnderOrderLock(true, $this->pendingOrderStatusId, $this->language->get('text_order_confirmed'));
|
||||
|
||||
$this->response->addHeader('Content-Type: application/json');
|
||||
$this->response->setOutput(json_encode($json));
|
||||
@@ -182,10 +178,7 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
||||
public function success() {
|
||||
$this->load->language('extension/payment/payanyway');
|
||||
|
||||
$orderStatusId = isset($this->orderInfo['order_status_id']) ? (int)$this->orderInfo['order_status_id'] : null;
|
||||
if ((null !== $orderStatusId) && ($orderStatusId !== $this->successOrderStatusId)) {
|
||||
$this->addOrderHistory($this->pendingOrderStatusId, $this->language->get('text_payment_processing'));
|
||||
}
|
||||
$this->changeStatusUnderOrderLock(false, $this->pendingOrderStatusId, $this->language->get('text_payment_processing'));
|
||||
|
||||
$this->response->redirect($this->url->link('checkout/success', '', true));
|
||||
}
|
||||
@@ -197,10 +190,7 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
||||
public function fail() {
|
||||
$this->load->language('extension/payment/payanyway');
|
||||
|
||||
$orderStatusId = isset($this->orderInfo['order_status_id']) ? (int)$this->orderInfo['order_status_id'] : null;
|
||||
if ((null !== $orderStatusId) && ($orderStatusId !== $this->successOrderStatusId)) {
|
||||
$this->addOrderHistory($this->errorOrderStatusId, $this->language->get('text_payment_error'));
|
||||
}
|
||||
$this->changeStatusUnderOrderLock(false, $this->errorOrderStatusId, $this->language->get('text_payment_error'));
|
||||
|
||||
$this->response->redirect($this->url->link('checkout/failure', '', true));
|
||||
}
|
||||
@@ -218,9 +208,18 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
||||
return $this->model_checkout_order->getOrder($this->orderId);
|
||||
}
|
||||
|
||||
/** @return string */
|
||||
private function createTransactionId() {
|
||||
return $this->orderId . self::TRANSACTION_ID_DELIMITER . date('YmdHisT');
|
||||
/**
|
||||
* @param string $amount
|
||||
* @return string
|
||||
* @throws Exception
|
||||
*/
|
||||
private function createTransactionId($amount) {
|
||||
$kopecks = ltrim(str_replace('.', '', $amount), '0');
|
||||
$issuedAt = new \DateTime('now', new \DateTimeZone(self::TRANSACTION_ID_ZONE));
|
||||
|
||||
return $this->orderId
|
||||
. self::TRANSACTION_ID_DELIMITER . (('' !== $kopecks) ? $kopecks : '0')
|
||||
. self::TRANSACTION_ID_DELIMITER . $issuedAt->format('YmdHis') . self::TRANSACTION_ID_ZONE;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -313,6 +312,61 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
||||
return (null !== $phone) ? $phone : '';
|
||||
}
|
||||
|
||||
/** @return bool */
|
||||
private function isPaidWithModule() {
|
||||
return isset($this->orderInfo['payment_code']) && (self::MODULE_NAME === $this->orderInfo['payment_code']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $orderStatusId
|
||||
* @return bool
|
||||
*/
|
||||
private function isAwaitingPayment($orderStatusId) {
|
||||
return in_array($orderStatusId, array(0, $this->pendingOrderStatusId, $this->errorOrderStatusId), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $allowFailed подтверждение формы допускает повторную попытку после «Неуспешного»
|
||||
* @param int $orderStatusId
|
||||
* @param string $comment
|
||||
* @return bool
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function changeStatusUnderOrderLock($allowFailed, $orderStatusId, $comment) {
|
||||
if (!$this->isStatusChangeableByCustomer($allowFailed)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->load->model('extension/payment/payanyway');
|
||||
if (!$this->model_extension_payment_payanyway->acquireOrderLock($this->orderId, self::ORDER_LOCK_TIMEOUT_SECONDS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->orderInfo = $this->getOrderInfo();
|
||||
$changeable = $this->isStatusChangeableByCustomer($allowFailed)
|
||||
&& !$this->model_extension_payment_payanyway->hasOrderStatusInHistory($this->orderId, $this->successOrderStatusId);
|
||||
if ($changeable) {
|
||||
$this->addOrderHistory($orderStatusId, $comment);
|
||||
}
|
||||
$this->model_extension_payment_payanyway->releaseOrderLock($this->orderId);
|
||||
|
||||
return $changeable;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $allowFailed
|
||||
* @return bool
|
||||
*/
|
||||
private function isStatusChangeableByCustomer($allowFailed) {
|
||||
if (empty($this->orderInfo) || !$this->isPaidWithModule()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$orderStatusId = (int)$this->orderInfo['order_status_id'];
|
||||
|
||||
return $allowFailed ? $this->isAwaitingPayment($orderStatusId) : in_array($orderStatusId, array(0, $this->pendingOrderStatusId), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $orderStatusId
|
||||
* @param string $comment
|
||||
@@ -338,22 +392,40 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
||||
|
||||
$this->validateCallback($callback);
|
||||
|
||||
$isCheck = 'CHECK' === $callback['MNT_COMMAND'];
|
||||
if ($isCheck && !$this->checkSignature($callback, $callback['MNT_SIGNATURE'])) {
|
||||
if (!$this->hasRequiredOperationId($callback)) {
|
||||
$this->log->write('PayAnyWay callback: MNT_OPERATION_ID is missing or not numeric');
|
||||
$this->sendResponse(self::FAIL_RESPONSE);
|
||||
}
|
||||
|
||||
$this->orderId = $this->parseOrderIdFromTransactionId($callback['MNT_TRANSACTION_ID']);
|
||||
if (!$this->checkSignature($callback, $callback['MNT_SIGNATURE'])) {
|
||||
$this->log->write('PayAnyWay callback: MNT_SIGNATURE does not match');
|
||||
$this->sendResponse(self::FAIL_RESPONSE);
|
||||
}
|
||||
|
||||
$transaction = $this->parseTransactionId($callback['MNT_TRANSACTION_ID']);
|
||||
$this->orderId = (null !== $transaction) ? $transaction['order_id'] : null;
|
||||
if (null === $this->orderId) {
|
||||
$this->log->write('PayAnyWay callback: MNT_TRANSACTION_ID has unexpected format');
|
||||
$this->sendResponse(self::FAIL_RESPONSE);
|
||||
}
|
||||
|
||||
$isCheck = 'CHECK' === $callback['MNT_COMMAND'];
|
||||
$this->load->model('extension/payment/payanyway');
|
||||
if (!$isCheck && !$this->model_extension_payment_payanyway->acquireOrderLock($this->orderId, self::ORDER_LOCK_TIMEOUT_SECONDS)) {
|
||||
$this->log->write('PayAnyWay callback: order is locked by another request');
|
||||
$this->sendResponse(self::FAIL_RESPONSE);
|
||||
}
|
||||
|
||||
$this->orderInfo = $this->getOrderInfo();
|
||||
if (empty($this->orderInfo)) {
|
||||
$this->sendResponse($isCheck ? $this->buildUnknownOrderResponse($callback) : self::FAIL_RESPONSE);
|
||||
}
|
||||
|
||||
if (!$this->isPaidWithModule()) {
|
||||
$this->log->write('PayAnyWay callback: order is not paid with PayAnyWay');
|
||||
$this->sendResponse($isCheck ? $this->buildUnknownOrderResponse($callback) : self::FAIL_RESPONSE);
|
||||
}
|
||||
|
||||
$isCheck ? $this->handleCheckCallback($callback) : $this->handlePayCallback($callback);
|
||||
}
|
||||
|
||||
@@ -384,19 +456,31 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
||||
|
||||
/**
|
||||
* @param mixed $transactionId
|
||||
* @return int|null
|
||||
* @return array{order_id: int, amount: string|null, issued_at: string}|null
|
||||
*/
|
||||
private function parseOrderIdFromTransactionId($transactionId) {
|
||||
private function parseTransactionId($transactionId) {
|
||||
if (!is_string($transactionId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$delimiters = preg_quote(self::TRANSACTION_ID_DELIMITER . self::LEGACY_TRANSACTION_ID_DELIMITER, '/');
|
||||
if (!preg_match(sprintf(self::TRANSACTION_ID_PATTERN, $delimiters), $transactionId, $matches)) {
|
||||
return null;
|
||||
if (preg_match(self::TRANSACTION_ID_PATTERN, $transactionId, $matches)) {
|
||||
$kopecks = str_pad($matches[2], 3, '0', STR_PAD_LEFT);
|
||||
|
||||
return array(
|
||||
'order_id' => (int)$matches[1],
|
||||
'amount' => substr($kopecks, 0, -2) . '.' . substr($kopecks, -2),
|
||||
'issued_at' => $matches[3],
|
||||
);
|
||||
}
|
||||
|
||||
return (int)$matches[1];
|
||||
if (preg_match(self::LEGACY_TRANSACTION_ID_PATTERN, $transactionId, $matches)
|
||||
|| (preg_match(self::LEGACY_ZONED_TRANSACTION_ID_PATTERN, $transactionId, $matches)
|
||||
&& (!isset($matches[3]) || !preg_match('/^[+-]/', $matches[3]) || ($matches[3] === date('T'))))
|
||||
) {
|
||||
return array('order_id' => (int)$matches[1], 'amount' => null, 'issued_at' => $matches[2]);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @return int|null */
|
||||
@@ -433,7 +517,7 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
||||
$this->sendResponse(self::FAIL_RESPONSE);
|
||||
}
|
||||
|
||||
if ($this->mntId !== (int)$callback['MNT_ID']) {
|
||||
if (!$this->isOwnAccount($callback)) {
|
||||
$this->sendResponse(self::FAIL_RESPONSE);
|
||||
}
|
||||
|
||||
@@ -443,6 +527,23 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $callback
|
||||
* @return bool
|
||||
*/
|
||||
private function isOwnAccount(array $callback) {
|
||||
return (string)$this->mntId === $callback['MNT_ID'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $callback
|
||||
* @return bool
|
||||
*/
|
||||
private function hasRequiredOperationId(array $callback) {
|
||||
return ('CHECK' === $callback['MNT_COMMAND'])
|
||||
|| (1 === preg_match(self::OPERATION_ID_PATTERN, $callback['MNT_OPERATION_ID']));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
* @return bool
|
||||
@@ -475,6 +576,10 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function handleCheckCallback(array $callbackData) {
|
||||
if (!$this->isOrderPaid()) {
|
||||
$this->rejectPaymentMismatch($callbackData);
|
||||
}
|
||||
|
||||
list($resultCode, $description) = $this->determineCheckResult($callbackData);
|
||||
$responseData = $this->buildBaseResponseData($callbackData, $resultCode, $description);
|
||||
|
||||
@@ -494,26 +599,15 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function handlePayCallback(array $callbackData) {
|
||||
$currencyCode = isset($this->orderInfo['currency_code']) ? $this->orderInfo['currency_code'] : 'RUB';
|
||||
$shopData = array(
|
||||
'MNT_ID' => $this->mntId,
|
||||
'MNT_TRANSACTION_ID' => $callbackData['MNT_TRANSACTION_ID'],
|
||||
'MNT_OPERATION_ID' => $callbackData['MNT_OPERATION_ID'],
|
||||
'MNT_AMOUNT' => $this->getOrderAmount(),
|
||||
'MNT_CURRENCY_CODE' => $currencyCode,
|
||||
'MNT_SUBSCRIBER_ID' => $callbackData['MNT_SUBSCRIBER_ID'],
|
||||
'MNT_TEST_MODE' => $callbackData['MNT_TEST_MODE'],
|
||||
);
|
||||
|
||||
if (!$this->checkSignature($shopData, $callbackData['MNT_SIGNATURE'])) {
|
||||
$this->sendResponse(self::FAIL_RESPONSE);
|
||||
}
|
||||
|
||||
$isPayedOrder = $this->isOrderPaid();
|
||||
if (!$isPayedOrder) {
|
||||
$this->rejectPaymentMismatch($callbackData);
|
||||
$this->rejectCanceledOrder($callbackData);
|
||||
|
||||
$this->load->language('extension/payment/payanyway');
|
||||
$this->addOrderHistory($this->successOrderStatusId, $this->getPaymentCompletedComment($callbackData['MNT_OPERATION_ID']));
|
||||
}
|
||||
$this->model_extension_payment_payanyway->releaseOrderLock($this->orderId);
|
||||
|
||||
$resultCode = 200;
|
||||
$description = $isPayedOrder ? 'Order already paid' : 'Order success paid';
|
||||
@@ -522,6 +616,80 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
||||
$this->sendResponse($this->buildXmlResponse($responseData + $this->getResponseAttributes()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $callbackData
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function rejectPaymentMismatch(array $callbackData) {
|
||||
$mismatch = $this->getPaymentMismatch($callbackData);
|
||||
if (null !== $mismatch) {
|
||||
$this->log->write('PayAnyWay callback: ' . $mismatch);
|
||||
$this->sendResponse(self::FAIL_RESPONSE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Заказ отменили до оплаты — оплата его не проводит: подписанный 500 без атрибутов прекращает
|
||||
* повторы, номер операции в логе нужен администратору для возврата в кабинете MONETA.RU.
|
||||
* Как Check URL и остальные модули.
|
||||
*
|
||||
* @param array<string, mixed> $callbackData
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function rejectCanceledOrder(array $callbackData) {
|
||||
$response = $this->buildCanceledOrderPayResponse($callbackData);
|
||||
if (null !== $response) {
|
||||
$this->log->write('PayAnyWay callback: order ' . $this->orderId . ' is canceled, payment ' . $callbackData['MNT_OPERATION_ID'] . ' must be refunded');
|
||||
$this->sendResponse($response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $callbackData
|
||||
* @return array{data: string, contentType: string}|null
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function buildCanceledOrderPayResponse(array $callbackData) {
|
||||
$orderStatusId = isset($this->orderInfo['order_status_id']) ? (int)$this->orderInfo['order_status_id'] : 0;
|
||||
if (!in_array($orderStatusId, $this->canceledOrderStatusIds, true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->buildXmlResponse($this->buildBaseResponseData($callbackData, 500, 'Order status is ' . $this->getOrderStatusById($orderStatusId)), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $callbackData
|
||||
* @return string|null
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function getPaymentMismatch(array $callbackData) {
|
||||
$transaction = $this->parseTransactionId($callbackData['MNT_TRANSACTION_ID']);
|
||||
if ((null === $transaction) || ($transaction['order_id'] !== $this->orderId)) {
|
||||
return 'MNT_TRANSACTION_ID has unexpected format';
|
||||
}
|
||||
|
||||
if (null === $transaction['amount']) {
|
||||
$issuedAt = \DateTime::createFromFormat('!YmdHis', $transaction['issued_at']);
|
||||
if ((false === $issuedAt) || ($issuedAt->modify('+' . self::LEGACY_TRANSACTION_MAX_AGE_DAYS . ' days') < new \DateTime())) {
|
||||
return 'MNT_TRANSACTION_ID of the previous format has expired';
|
||||
}
|
||||
} elseif (('' !== $callbackData['MNT_AMOUNT']) && ($callbackData['MNT_AMOUNT'] !== $transaction['amount'])) {
|
||||
return 'MNT_AMOUNT does not match MNT_TRANSACTION_ID';
|
||||
} elseif ($transaction['amount'] !== $this->getOrderAmount()) {
|
||||
return 'order amount changed after the payment form was issued';
|
||||
}
|
||||
|
||||
$isCheck = 'CHECK' === $callbackData['MNT_COMMAND'];
|
||||
if ((!$isCheck || ('' !== $callbackData['MNT_AMOUNT'])) && !$this->isOrderAmountMatched($callbackData)) {
|
||||
return 'MNT_AMOUNT or MNT_CURRENCY_CODE does not match the order';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $operationId
|
||||
* @return string
|
||||
@@ -603,7 +771,7 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
||||
return array(100, "Order status is {$orderStatus}");
|
||||
}
|
||||
|
||||
if ($this->successOrderStatusId === $orderStatusId) {
|
||||
if ($this->isOrderPaid()) {
|
||||
return array(200, 'Order Paid');
|
||||
}
|
||||
|
||||
@@ -614,13 +782,34 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
||||
return array(402, 'Order created, but not paid');
|
||||
}
|
||||
|
||||
/** @return bool */
|
||||
/**
|
||||
* @return bool
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function isOrderPaid() {
|
||||
if (!isset($this->orderInfo['order_status_id'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->successOrderStatusId === (int)$this->orderInfo['order_status_id'];
|
||||
if ($this->successOrderStatusId === (int)$this->orderInfo['order_status_id']) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->load->model('extension/payment/payanyway');
|
||||
|
||||
return $this->model_extension_payment_payanyway->hasOrderStatusInHistory((int)$this->orderId, $this->successOrderStatusId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $callbackData
|
||||
* @return bool
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function isOrderAmountMatched(array $callbackData) {
|
||||
$currencyCode = isset($this->orderInfo['currency_code']) ? (string)$this->orderInfo['currency_code'] : '';
|
||||
|
||||
return ($callbackData['MNT_AMOUNT'] === $this->getOrderAmount())
|
||||
&& ($callbackData['MNT_CURRENCY_CODE'] === $currencyCode);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -936,7 +1125,7 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
||||
* @param string $vatRate
|
||||
* @return string
|
||||
*/
|
||||
public function getVat($vatRate) {
|
||||
private function getVat($vatRate) {
|
||||
$map = array(
|
||||
'1104' => 'vat0',
|
||||
'1108' => 'vat5',
|
||||
@@ -959,7 +1148,7 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
||||
* @return string
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getOrderStatusById($orderStatusId) {
|
||||
private function getOrderStatusById($orderStatusId) {
|
||||
$this->load->model('localisation/order_status');
|
||||
/**@var array<int, array{order_status_id: string, name: string}> $orderStatuses */
|
||||
$orderStatuses = $this->model_localisation_order_status->getOrderStatuses();
|
||||
@@ -988,7 +1177,7 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
||||
* @return array
|
||||
* @throws \JsonException
|
||||
*/
|
||||
public function buildJsonResponse(array $data) {
|
||||
private function buildJsonResponse(array $data) {
|
||||
$client = $data['client'];
|
||||
if (isset($client['phone']) && (strlen($client['phone']) > self::CHECK_CLIENT_PHONE_MAX_LENGTH)) {
|
||||
unset($client['phone']);
|
||||
@@ -1014,7 +1203,7 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
||||
|
||||
$json = json_encode($jsonData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
if (false !== $json) {
|
||||
$json = preg_replace('/"' . preg_quote(self::JSON_NUMBER_PREFIX, '/') . '([0-9]+\.[0-9]{2})"/', '$1', $json);
|
||||
$json = preg_replace('/"(' . self::JSON_NUMBER_KEYS . ')":"' . preg_quote(self::JSON_NUMBER_PREFIX, '/') . '([0-9]+\.[0-9]{2})"/', '"$1":$2', $json);
|
||||
}
|
||||
|
||||
return array(
|
||||
@@ -1057,7 +1246,7 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
||||
|
||||
foreach ($requiredFields as $field) {
|
||||
if (isset($data[$field]) && ($data[$field] !== '')) {
|
||||
$root->appendChild($dom->createElement($field, (string)$data[$field]));
|
||||
$root->appendChild($this->createTextElement($dom, $field, (string)$data[$field]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1078,8 +1267,8 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
||||
foreach ($attributeMap as $key => $value) {
|
||||
if (!empty($value)) {
|
||||
$attrElement = $dom->createElement('ATTRIBUTE');
|
||||
$attrElement->appendChild($dom->createElement('KEY', $key));
|
||||
$attrElement->appendChild($dom->createElement('VALUE', (string)$value));
|
||||
$attrElement->appendChild($this->createTextElement($dom, 'KEY', $key));
|
||||
$attrElement->appendChild($this->createTextElement($dom, 'VALUE', (string)$value));
|
||||
$attributes->appendChild($attrElement);
|
||||
}
|
||||
}
|
||||
@@ -1087,6 +1276,22 @@ class ControllerExtensionPaymentPayanyway extends Controller {
|
||||
return $this->saveXmlResponse($dom);
|
||||
}
|
||||
|
||||
/**
|
||||
* createElement($name, $value) не экранирует «&» — текст только через createTextNode().
|
||||
*
|
||||
* @param \DOMDocument $dom
|
||||
* @param string $name
|
||||
* @param string $value
|
||||
* @return \DOMElement
|
||||
* @throws DOMException
|
||||
*/
|
||||
private function createTextElement(\DOMDocument $dom, $name, $value) {
|
||||
$element = $dom->createElement($name);
|
||||
$element->appendChild($dom->createTextNode($value));
|
||||
|
||||
return $element;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DOMDocument $dom
|
||||
* @return array
|
||||
|
||||
@@ -23,6 +23,54 @@ class ModelExtensionPaymentPayanyway extends Model {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Блокировка заказа на время Pay URL (GET_LOCK MySQL/MariaDB): два уведомления подряд
|
||||
* не обрабатываются параллельно. Снимается releaseOrderLock() или с закрытием соединения.
|
||||
*
|
||||
* @param int $orderId
|
||||
* @param int $timeoutSeconds
|
||||
* @return bool
|
||||
*/
|
||||
public function acquireOrderLock($orderId, $timeoutSeconds) {
|
||||
$query = $this->db->query("SELECT GET_LOCK('" . $this->db->escape($this->getOrderLockName($orderId)) . "', " . (int)$timeoutSeconds . ") AS acquired");
|
||||
|
||||
return isset($query->row['acquired']) && (1 === (int)$query->row['acquired']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $orderId
|
||||
* @return void
|
||||
*/
|
||||
public function releaseOrderLock($orderId) {
|
||||
$this->db->query("SELECT RELEASE_LOCK('" . $this->db->escape($this->getOrderLockName($orderId)) . "')");
|
||||
}
|
||||
|
||||
/**
|
||||
* Заказ когда-либо получал этот статус — по истории заказа.
|
||||
*
|
||||
* @param int $orderId
|
||||
* @param int $orderStatusId
|
||||
* @return bool
|
||||
*/
|
||||
public function hasOrderStatusInHistory($orderId, $orderStatusId) {
|
||||
$query = $this->db->query("SELECT 1 FROM `" . DB_PREFIX . "order_history`
|
||||
WHERE `order_id` = '" . (int)$orderId . "'
|
||||
AND `order_status_id` = '" . (int)$orderStatusId . "'
|
||||
LIMIT 1");
|
||||
|
||||
return $query->num_rows > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Имя блокировки — не длиннее 64 символов и своё для каждой БД магазина.
|
||||
*
|
||||
* @param int $orderId
|
||||
* @return string
|
||||
*/
|
||||
private function getOrderLockName($orderId) {
|
||||
return 'payanyway_' . substr(md5(DB_DATABASE . '.' . DB_PREFIX), 0, 16) . '_' . (int)$orderId;
|
||||
}
|
||||
|
||||
/** @return bool */
|
||||
private function isModuleEnabled() {
|
||||
return (bool)$this->config->get('payment_payanyway_status');
|
||||
|
||||
Reference in New Issue
Block a user