관리-도구
편집 파일: rest-yoco-webhook-controller.php
<?php /** * REST API YOCO PAYMENTS controller * * Handles requests to the yoco/webhook endpoint. * * @package Kkart\RestApi */ defined( 'ABSPATH' ) || exit; /** * REST API Order Notes controller class. * * @package Kkart\RestApi */ class KKART_REST_Yoco_Webhook_Controller extends KKART_REST_Controller { protected $namespace = 'kkart/v3'; protected $rest_base = 'webhook'; public function register_routes(){ register_rest_route( $this->namespace, '/' . $this->rest_base, array( 'methods'=> 'GET, POST', 'callback'=> array( $this, 'yoco_webhook_handler' ), 'permission_callback' => array( $this, 'permit' ), ) ); } public function yoco_webhook_handler($request){ $body = $request->get_params(); $eventType = isset( $body['type'] ) && ! empty( $body['type'] ) ? $body['type'] : ''; $checkout_id = isset( $body['payload']['metadata']['checkoutId'] ) ? $body['payload']['metadata']['checkoutId'] : ''; $payment_id = isset( $body['payload']['id'] ) ? $body['payload']['id'] : ''; if($eventType == 'payment.succeeded'){ $this->update_payment_status($checkout_id, $payment_id); } if($eventType === 'refund.failed'){ $this->update_refund_failed($checkout_id, $payment_id); } if($eventType === 'refund.succeeded'){ $this->update_refund_succeeded($checkout_id, $payment_id); } } public function update_payment_status($checkout_id, $payment_id){ $args = array( 'meta_key' => 'kkart_yoco_order_checkout_id', 'meta_value' => $checkout_id, 'meta_compare' => "=", ); $orders = kkart_get_orders($args); if( empty( $orders ) ){ return null; } $order = array_shift( $orders ); $order_status = is_a( $order, KKART_Order::class ) ? $order : null; if( null === $order_status ){ return new WP_REST_Response( array( 'description' => sprintf( 'No order found for CheckoutId %s.', $checkout_id ), ), 404, ); } if( true === $order_status->update_status( 'processing' ) ){ $order->update_meta_data( 'kkart_yoco_order_payment_id', $payment_id ); $order->save_meta_data(); return new WP_REST_Response(); }else{ return new WP_REST_Response( array( 'description' => sprintf( 'Failed to complete payment of order #%s.', $order->get_id() ), ), 500, ); } } public function update_refund_succeeded($checkout_id, $payment_id){ $args = array( 'meta_key' => 'kkart_yoco_order_checkout_id', 'meta_value' => $checkout_id, 'meta_compare' => "=", ); $orders = kkart_get_orders($args); if( empty( $orders ) ){ return null; } $order = array_shift( $orders ); $order_status = is_a( $order, KKART_Order::class ) ? $order : null; if( null === $order_status ){ return new WP_REST_Response( array( 'description' => sprintf( 'Could not find the order for checkout id %s.', $checkout_id ), ), 403, ); } if( 'refunded' === $order_status->get_status() ){ return new WP_REST_Response( array( 'description' => sprintf( 'Order for checkout id %s is already refunded.', $checkout_id ), ), 403, ); } try{ $refund = $this->refund_amc($order_status); if( null === $refund ){ return new WP_REST_Response(); } if( 'completed' === $refund->get_status() ){ $order_status->update_meta_data( 'kkart_yoco_order_payment_id', $payment_id ); $order_status->save_meta_data(); return new WP_REST_Response(); } return new WP_REST_Response( array( 'description' => sprintf( 'Failed to complete refund of order #%s - wrong order status.', $order_status->get_id() ), ), 403, ); }catch (\Throwable $th){ return new WP_REST_Response( array( 'description' => sprintf('Could not find the order for checkout id %s.', $checkout_id), ), 403 ); } } public function update_refund_failed($checkout_id, $payment_id){ $args = array( 'meta_key' => 'kkart_yoco_order_checkout_id', 'meta_value' => $checkout_id, 'meta_compare' => "=", ); $orders = kkart_get_orders($args); if( empty( $orders ) ){ return null; } $order = array_shift( $orders ); $order_status = is_a( $order, KKART_Order::class ) ? $order : null; if( null === $order_status ){ return new WP_REST_Response( array( 'description' => sprintf( 'Could not find the order for checkout id %s.', $checkout_id ), ), 403, ); } if ( 'refunded' === $order_status->get_status() ) { return new WP_REST_Response( array( 'description' => sprintf( 'Order for checkout id %s is already refunded.', $checkout_id ), ), 403, ); } return new WP_REST_Response(); } public function permit($request) { $headers = array( 'webhook_id' => $request->get_header('webhook_id'), 'webhook_timestamp' => $request->get_header('webhook_timestamp'), 'webhook_signature' => $request->get_header('webhook_signature'), ); return $this->validate($request->get_body(), $headers); } public function validate(string $payload, array $webhookHeaders){ try{ $this->verify($payload, $webhookHeaders); return true; }catch(\Throwable $th){ return false; } } public function verify($payload, $headers){ if( !isset($headers['webhook_id']) || !isset($headers['webhook_timestamp']) || !isset($headers['webhook_signature']) ){ throw new Exception('Webhook Signature Validator is missing required headers'); } $msgId = $headers['webhook_id']; $msgTimestamp = $headers['webhook_timestamp']; $msgSignature = $headers['webhook_signature']; $timestamp = $this->verifyTimestamp($msgTimestamp); $signature = $this->sign($msgId, $timestamp, $payload); $expectedSignature = explode(',', $signature, 2)[1]; $passedSignatures = explode(' ', $msgSignature); foreach($passedSignatures as $versionedSignature){ $sigParts = explode(',', $versionedSignature, 2); $version = $sigParts[0]; $passedSignature = $sigParts[1]; if(0 !== strcmp($version, 'v1')){ continue; } if(hash_equals($expectedSignature, $passedSignature)){ return json_decode($payload, true); } } throw new Exception('Webhook no matching signature found'); } public function sign(string $msgId, int $timestamp, string $payload): string { if (!is_int($timestamp)) { throw new Exception('Invalid timestamp format'); } $toSign = "{$msgId}.{$timestamp}.{$payload}"; $secret = $this->secret(); $hex_hash = hash_hmac('sha256', $toSign, $secret); $signature = base64_encode(pack('H*', $hex_hash)); return "v1,{$signature}"; } public function secret(){ $settings = get_option( 'kkart_yoco_settings', null ); if( ! isset( $settings['mode'] ) ){ return ''; } $key = 'live' === $settings['mode'] ? 'yoco_payment_gateway_live_webhook_secret' : 'yoco_payment_gateway_test_webhook_secret'; $SECRET_PREFIX = 'whsec_'; $secret = get_option( $key, '' ); if(substr($secret, 0, strlen($SECRET_PREFIX)) === $SECRET_PREFIX){ $secret = substr($secret, strlen($SECRET_PREFIX)); return base64_decode($secret); } } public function verifyTimestamp($timestampHeader): int{ $now = time(); $timestamp = intval($timestampHeader, 10); if ($timestamp < ($now - 5 * 60)) { throw new Exception('Webhook timestamp is too old'); } if ($timestamp > ($now + 5 * 60)) { throw new Exception('Webhook timestamp is too new'); } return $timestamp; } public function refund_amc($order){ if( ! empty( $refunds = $order->get_refunds() ) ){ return array_shift( $refunds ); } $args = array( 'amount' => $order->get_total(), 'reason' => __( 'Refund requested via webhook.', 'kkart' ), 'order_id' => $order->get_id(), 'refund_payment_method' => 'yoco', 'line_items' => $order->get_items(), ); $refund = kkart_create_refund( apply_filters( 'yoco_payment_gateway/request/refund/args', $args ) ); if( is_wp_error( $refund ) ){ throw new Error( $refund->get_error_message(), (int) $refund->get_error_code() ); } return $refund; } } ?>