diff --git a/CHANGELOG.md b/CHANGELOG.md index 780f2ad0..7f99715d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # CHANGELOG +## 2.4.3 (2020-12-10) + +Improvements: + +- Improve callback endpoint security to check order number from source of truth + ## 2.4.2 (2020-11-23) Improvements: diff --git a/Xendit/M2Invoice/Controller/Checkout/CCCallback.m22.php b/Xendit/M2Invoice/Controller/Checkout/CCCallback.m22.php index 31dd6a79..ef9ce27e 100644 --- a/Xendit/M2Invoice/Controller/Checkout/CCCallback.m22.php +++ b/Xendit/M2Invoice/Controller/Checkout/CCCallback.m22.php @@ -6,12 +6,35 @@ use Magento\Sales\Model\Order; use Xendit\M2Invoice\Enum\LogDNALevel; +/** + * This callback is only for order in multishipping flow. For order + * created in onepage checkout is handled in ProcessHosted.php + */ class CCCallback extends ProcessHosted { public function execute() { try { - $orderIds = explode('-', $this->getRequest()->getParam('order_ids')); + $post = $this->getRequest()->getContent(); + $callbackPayload = json_decode($post, true); + + if ( + !isset($callbackPayload['id']) || + !isset($callbackPayload['hp_token']) || + !isset($callbackPayload['order_number']) + ) { + $result = $this->getJsonResultFactory()->create(); + $result->setHttpResponseCode(\Magento\Framework\Webapi\Exception::HTTP_BAD_REQUEST); + $result->setData([ + 'status' => __('ERROR'), + 'message' => 'Callback body is invalid' + ]); + + return $result; + } + $orderIds = explode('-', $callbackPayload['order_number']); + $hostedPaymentId = $callbackPayload['id']; + $hostedPaymentToken = $callbackPayload['hp_token']; $shouldRedirect = false; $isError = false; @@ -23,40 +46,49 @@ public function execute() $payment = $order->getPayment(); - if ($payment->getAdditionalInformation('xendit_hosted_payment_id') !== null) { - $requestData = [ - 'id' => $payment->getAdditionalInformation('xendit_hosted_payment_id'), - 'hp_token' => $payment->getAdditionalInformation('xendit_hosted_payment_token') - ]; - - if ($flag) { // complete hosted payment only once as status will be changed to USED - $hostedPayment = $this->getCompletedHostedPayment($requestData); - $flag = false; - } - - if (isset($hostedPayment['error_code'])) { - $isError = true; + $requestData = [ + 'id' => $hostedPaymentId, + 'hp_token' => $hostedPaymentToken + ]; + + if ($flag) { // complete hosted payment only once as status will be changed to USED + $hostedPayment = $this->getCompletedHostedPayment($requestData); + $flag = false; + } + + if (isset($hostedPayment['error_code'])) { + $isError = true; + } + else { + if ($hostedPayment['order_number'] !== $callbackPayload['order_number']) { + $result = $this->getJsonResultFactory()->create(); + $result->setHttpResponseCode(\Magento\Framework\Webapi\Exception::HTTP_BAD_REQUEST); + $result->setData([ + 'status' => __('ERROR'), + 'message' => 'Hosted payment is not for this order' + ]); + + return $result; } - else { - if ($hostedPayment['paid_amount'] != $hostedPayment['amount']) { - $order->setBaseDiscountAmount($hostedPayment['paid_amount'] - $hostedPayment['amount']); - $order->setDiscountAmount($hostedPayment['paid_amount'] - $hostedPayment['amount']); - $order->save(); - - $order->setBaseGrandTotal($order->getBaseGrandTotal() + $order->getBaseDiscountAmount()); - $order->setGrandTotal($order->getGrandTotal() + $order->getDiscountAmount()); - $order->save(); - } - $payment->setAdditionalInformation('token_id', $hostedPayment['token_id']); - $payment->setAdditionalInformation('xendit_installment', $hostedPayment['installment']); + + if ($hostedPayment['paid_amount'] != $hostedPayment['amount']) { + $order->setBaseDiscountAmount($hostedPayment['paid_amount'] - $hostedPayment['amount']); + $order->setDiscountAmount($hostedPayment['paid_amount'] - $hostedPayment['amount']); + $order->save(); - $this->processSuccessfulTransaction( - $order, - $payment, - 'Xendit Credit Card payment completed. Transaction ID: ', - $hostedPayment['charge_id'] - ); + $order->setBaseGrandTotal($order->getBaseGrandTotal() + $order->getBaseDiscountAmount()); + $order->setGrandTotal($order->getGrandTotal() + $order->getDiscountAmount()); + $order->save(); } + $payment->setAdditionalInformation('token_id', $hostedPayment['token_id']); + $payment->setAdditionalInformation('xendit_installment', $hostedPayment['installment']); + + $this->processSuccessfulTransaction( + $order, + $payment, + 'Xendit Credit Card payment completed. Transaction ID: ', + $hostedPayment['charge_id'] + ); } } diff --git a/Xendit/M2Invoice/Controller/Checkout/CCCallback.m23.php b/Xendit/M2Invoice/Controller/Checkout/CCCallback.m23.php index 50a7efdb..d501d8dd 100644 --- a/Xendit/M2Invoice/Controller/Checkout/CCCallback.m23.php +++ b/Xendit/M2Invoice/Controller/Checkout/CCCallback.m23.php @@ -10,12 +10,35 @@ use Magento\Sales\Model\Order; use Xendit\M2Invoice\Enum\LogDNALevel; +/** + * This callback is only for order in multishipping flow. For order + * created in onepage checkout is handled in ProcessHosted.php + */ class CCCallback extends ProcessHosted implements CsrfAwareActionInterface { public function execute() { try { - $orderIds = explode('-', $this->getRequest()->getParam('order_ids')); + $post = $this->getRequest()->getContent(); + $callbackPayload = json_decode($post, true); + + if ( + !isset($callbackPayload['id']) || + !isset($callbackPayload['hp_token']) || + !isset($callbackPayload['order_number']) + ) { + $result = $this->getJsonResultFactory()->create(); + $result->setHttpResponseCode(\Magento\Framework\Webapi\Exception::HTTP_BAD_REQUEST); + $result->setData([ + 'status' => __('ERROR'), + 'message' => 'Callback body is invalid' + ]); + + return $result; + } + $orderIds = explode('-', $callbackPayload['order_number']); + $hostedPaymentId = $callbackPayload['id']; + $hostedPaymentToken = $callbackPayload['hp_token']; $shouldRedirect = false; $isError = false; @@ -27,40 +50,49 @@ public function execute() $payment = $order->getPayment(); - if ($payment->getAdditionalInformation('xendit_hosted_payment_id') !== null) { - $requestData = [ - 'id' => $payment->getAdditionalInformation('xendit_hosted_payment_id'), - 'hp_token' => $payment->getAdditionalInformation('xendit_hosted_payment_token') - ]; - - if ($flag) { // complete hosted payment only once as status will be changed to USED - $hostedPayment = $this->getCompletedHostedPayment($requestData); - $flag = false; - } - - if (isset($hostedPayment['error_code'])) { - $isError = true; + $requestData = [ + 'id' => $hostedPaymentId, + 'hp_token' => $hostedPaymentToken + ]; + + if ($flag) { // complete hosted payment only once as status will be changed to USED + $hostedPayment = $this->getCompletedHostedPayment($requestData); + $flag = false; + } + + if (isset($hostedPayment['error_code'])) { + $isError = true; + } + else { + if ($hostedPayment['order_number'] !== $callbackPayload['order_number']) { + $result = $this->getJsonResultFactory()->create(); + $result->setHttpResponseCode(\Magento\Framework\Webapi\Exception::HTTP_BAD_REQUEST); + $result->setData([ + 'status' => __('ERROR'), + 'message' => 'Hosted payment is not for this order' + ]); + + return $result; } - else { - if ($hostedPayment['paid_amount'] != $hostedPayment['amount']) { - $order->setBaseDiscountAmount($hostedPayment['paid_amount'] - $hostedPayment['amount']); - $order->setDiscountAmount($hostedPayment['paid_amount'] - $hostedPayment['amount']); - $order->save(); - - $order->setBaseGrandTotal($order->getBaseGrandTotal() + $order->getBaseDiscountAmount()); - $order->setGrandTotal($order->getGrandTotal() + $order->getDiscountAmount()); - $order->save(); - } - $payment->setAdditionalInformation('token_id', $hostedPayment['token_id']); - $payment->setAdditionalInformation('xendit_installment', $hostedPayment['installment']); + + if ($hostedPayment['paid_amount'] != $hostedPayment['amount']) { + $order->setBaseDiscountAmount($hostedPayment['paid_amount'] - $hostedPayment['amount']); + $order->setDiscountAmount($hostedPayment['paid_amount'] - $hostedPayment['amount']); + $order->save(); - $this->processSuccessfulTransaction( - $order, - $payment, - 'Xendit Credit Card payment completed. Transaction ID: ', - $hostedPayment['charge_id'] - ); + $order->setBaseGrandTotal($order->getBaseGrandTotal() + $order->getBaseDiscountAmount()); + $order->setGrandTotal($order->getGrandTotal() + $order->getDiscountAmount()); + $order->save(); } + $payment->setAdditionalInformation('token_id', $hostedPayment['token_id']); + $payment->setAdditionalInformation('xendit_installment', $hostedPayment['installment']); + + $this->processSuccessfulTransaction( + $order, + $payment, + 'Xendit Credit Card payment completed. Transaction ID: ', + $hostedPayment['charge_id'] + ); } } diff --git a/Xendit/M2Invoice/Controller/Checkout/Notification.m22.php b/Xendit/M2Invoice/Controller/Checkout/Notification.m22.php index 2e0f60cd..e2fcc536 100755 --- a/Xendit/M2Invoice/Controller/Checkout/Notification.m22.php +++ b/Xendit/M2Invoice/Controller/Checkout/Notification.m22.php @@ -150,12 +150,11 @@ public function handleEwalletCallback($callbackPayload) { if (isset($callbackPayload['failure_code'])) { $failureCode = $callbackPayload['failure_code']; } + $prefix = $this->dataHelper->getExternalIdPrefix(); + $trimmedExternalId = str_replace($prefix . "-", "", $callbackPayload['external_id']); + $order = $this->getOrderById($trimmedExternalId); - $temp = explode('-', $callbackPayload['external_id']); - $orderId = end($temp); - $order = $this->getOrderById($orderId); - - return $this->checkOrder($order, true, $callbackPayload, null, $orderId); + return $this->checkOrder($order, true, $callbackPayload, null, $trimmedExternalId); } private function checkOrder($order, $isEwallet, $callbackPayload, $invoice, $callbackDescription) { @@ -184,7 +183,20 @@ private function checkOrder($order, $isEwallet, $callbackPayload, $invoice, $cal } if ($isEwallet) { - $paymentStatus = $this->getEwalletStatus($callbackPayload['ewallet_type'], $callbackPayload['external_id']); + $ewallet = $this->getEwallet($callbackPayload['ewallet_type'], $callbackPayload['external_id']); + $paymentStatus = $ewallet['status']; + + if ($ewallet['external_id'] !== $callbackPayload['external_id']) { + $result = $this->jsonResultFactory->create(); + /** You may introduce your own constants for this custom REST API */ + $result->setHttpResponseCode(\Magento\Framework\Webapi\Exception::HTTP_BAD_REQUEST); + $result->setData([ + 'status' => __('ERROR'), + 'message' => 'Ewallet is not for this order' + ]); + + return $result; + } } else { $paymentStatus = $invoice['status']; @@ -272,7 +284,7 @@ private function getXenditInvoice($invoiceId) return $invoice; } - private function getEwalletStatus($ewalletType, $externalId) + private function getEwallet($ewalletType, $externalId) { $ewalletUrl = $this->dataHelper->getCheckoutUrl() . "/payment/xendit/ewallets?ewallet_type=".$ewalletType."&external_id=".$externalId; $ewalletMethod = \Zend\Http\Request::METHOD_GET; @@ -280,17 +292,18 @@ private function getEwalletStatus($ewalletType, $externalId) try { $response = $this->apiHelper->request($ewalletUrl, $ewalletMethod); } catch (\Magento\Framework\Exception\LocalizedException $e) { - throw new \Magento\Framework\Exception\LocalizedException( + throw new LocalizedException( new Phrase($e->getMessage()) ); } + $status = $response['status']; $statusList = array("COMPLETED", "PAID", "SUCCESS_COMPLETED"); //OVO, DANA, LINKAJA - if (in_array($response['status'], $statusList)) { - return "COMPLETED"; + if (in_array($status, $statusList)) { + $response['status'] = "COMPLETED"; } - return $response['status']; + return $response; } private function invoiceOrder($order, $transactionId) diff --git a/Xendit/M2Invoice/Controller/Checkout/Notification.m23.php b/Xendit/M2Invoice/Controller/Checkout/Notification.m23.php index ddd0e6b9..6217eaa4 100755 --- a/Xendit/M2Invoice/Controller/Checkout/Notification.m23.php +++ b/Xendit/M2Invoice/Controller/Checkout/Notification.m23.php @@ -161,12 +161,11 @@ public function handleEwalletCallback($callbackPayload) { if (isset($callbackPayload['failure_code'])) { $failureCode = $callbackPayload['failure_code']; } + $prefix = $this->dataHelper->getExternalIdPrefix(); + $trimmedExternalId = str_replace($prefix . "-", "", $callbackPayload['external_id']); + $order = $this->getOrderById($trimmedExternalId); - $temp = explode('-', $callbackPayload['external_id']); - $orderId = end($temp); - $order = $this->getOrderById($orderId); - - return $this->checkOrder($order, true, $callbackPayload, null, $orderId); + return $this->checkOrder($order, true, $callbackPayload, null, $trimmedExternalId); } private function checkOrder($order, $isEwallet, $callbackPayload, $invoice, $callbackDescription) { @@ -195,7 +194,20 @@ private function checkOrder($order, $isEwallet, $callbackPayload, $invoice, $cal } if ($isEwallet) { - $paymentStatus = $this->getEwalletStatus($callbackPayload['ewallet_type'], $callbackPayload['external_id']); + $ewallet = $this->getEwallet($callbackPayload['ewallet_type'], $callbackPayload['external_id']); + $paymentStatus = $ewallet['status']; + + if ($ewallet['external_id'] !== $callbackPayload['external_id']) { + $result = $this->jsonResultFactory->create(); + /** You may introduce your own constants for this custom REST API */ + $result->setHttpResponseCode(\Magento\Framework\Webapi\Exception::HTTP_BAD_REQUEST); + $result->setData([ + 'status' => __('ERROR'), + 'message' => 'Ewallet is not for this order' + ]); + + return $result; + } } else { $paymentStatus = $invoice['status']; @@ -283,7 +295,7 @@ private function getXenditInvoice($invoiceId) return $invoice; } - private function getEwalletStatus($ewalletType, $externalId) + private function getEwallet($ewalletType, $externalId) { $ewalletUrl = $this->dataHelper->getCheckoutUrl() . "/payment/xendit/ewallets?ewallet_type=".$ewalletType."&external_id=".$externalId; $ewalletMethod = \Zend\Http\Request::METHOD_GET; @@ -296,12 +308,13 @@ private function getEwalletStatus($ewalletType, $externalId) ); } + $status = $response['status']; $statusList = array("COMPLETED", "PAID", "SUCCESS_COMPLETED"); //OVO, DANA, LINKAJA - if (in_array($response['status'], $statusList)) { - return "COMPLETED"; + if (in_array($status, $statusList)) { + $response['status'] = "COMPLETED"; } - return $response['status']; + return $response; } private function invoiceOrder($order, $transactionId) diff --git a/Xendit/M2Invoice/Controller/Checkout/SubscriptionCallback.m22.php b/Xendit/M2Invoice/Controller/Checkout/SubscriptionCallback.m22.php index 813b8c2e..c36f7dc8 100644 --- a/Xendit/M2Invoice/Controller/Checkout/SubscriptionCallback.m22.php +++ b/Xendit/M2Invoice/Controller/Checkout/SubscriptionCallback.m22.php @@ -15,7 +15,7 @@ public function execute() $invoiceId = $payload['id']; $chargeId = $payload['credit_card_charge_id']; - //verify callback + // verify callback to ensure payment exist in xendit side $callback = $this->getCallbackByInvoiceId($invoiceId); if (isset($callback['error_code']) || !isset($callback['status'])) { $result->setData([ diff --git a/Xendit/M2Invoice/Controller/Checkout/SubscriptionCallback.m23.php b/Xendit/M2Invoice/Controller/Checkout/SubscriptionCallback.m23.php index 4bff24cc..c2a2c49c 100644 --- a/Xendit/M2Invoice/Controller/Checkout/SubscriptionCallback.m23.php +++ b/Xendit/M2Invoice/Controller/Checkout/SubscriptionCallback.m23.php @@ -19,7 +19,7 @@ public function execute() $invoiceId = $payload['id']; $chargeId = $payload['credit_card_charge_id']; - //verify callback + // verify callback to ensure payment exist in xendit side $callback = $this->getCallbackByInvoiceId($invoiceId); if (isset($callback['error_code']) || !isset($callback['status'])) { $result->setData([ diff --git a/Xendit/M2Invoice/Helper/ApiRequest.php b/Xendit/M2Invoice/Helper/ApiRequest.php index fe4c4fa3..81a48605 100644 --- a/Xendit/M2Invoice/Helper/ApiRequest.php +++ b/Xendit/M2Invoice/Helper/ApiRequest.php @@ -85,7 +85,7 @@ private function getHeaders($isPublicRequest, $preferredMethod = null, $customHe 'Content-Type' => 'application/json', 'x-plugin-name' => 'MAGENTO2', 'user-agent' => 'Magento 2 Module', - 'x-plugin-version' => '2.4.2' + 'x-plugin-version' => '2.4.3' ]; if ($preferredMethod !== null) { diff --git a/Xendit/M2Invoice/composer.json b/Xendit/M2Invoice/composer.json index a2360869..7fb90e1c 100644 --- a/Xendit/M2Invoice/composer.json +++ b/Xendit/M2Invoice/composer.json @@ -2,7 +2,7 @@ "name": "xendit/m2invoice", "description": "Xendit Payment Gateway Module", "type": "magento2-module", - "version": "2.4.2", + "version": "2.4.3", "license": [ "GPL-3.0" ], diff --git a/Xendit/M2Invoice/etc/module.xml b/Xendit/M2Invoice/etc/module.xml index 4afed789..91f5fa09 100644 --- a/Xendit/M2Invoice/etc/module.xml +++ b/Xendit/M2Invoice/etc/module.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file