PluginProbe
MStore API – Create Native Android & iOS Apps On The Cloud / trunk
MStore API – Create Native Android & iOS Apps On The Cloud vtrunk
4.21.3 4.21.2 4.21.1 trunk 1.1.5 2.9.4 2.9.5 2.9.6 2.9.7 2.9.8 2.9.9 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.1 3.1.2 3.1.3 All 192 releases
mstore-api / controllers / flutter-expresspay.php

flutter-expresspay.php in MStore API – Create Native Android & iOS Apps On The Cloud trunk, at controllers/flutter-expresspay.php

400 lines 16.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 if ( ! defined( 'ABSPATH' ) ) {
4 exit;
5 }
6
7 require_once(__DIR__ . '/flutter-base.php');
8
9 /*
10 * Base REST Controller for flutter
11 *
12 * @since 1.4.0
13 *
14 * @package PayStack
15 */
16
17 class FlutterExpressPay extends FlutterBaseController
18 {
19 private function expresspay_post_json( $url, $payload ) {
20 return wp_remote_post(
21 $url,
22 array(
23 'timeout' => 15,
24 'headers' => array(
25 'Content-Type' => 'application/json',
26 ),
27 'body' => wp_json_encode( $payload ),
28 )
29 );
30 }
31
32 private function expresspay_post_form( $url, $payload ) {
33 return wp_remote_post(
34 $url,
35 array(
36 'timeout' => 15,
37 'headers' => array(
38 'Content-Type' => 'application/x-www-form-urlencoded',
39 ),
40 'body' => $payload,
41 )
42 );
43 }
44
45 /**
46 * Endpoint namespace
47 *
48 * @var string
49 */
50 protected $namespace = 'api/flutter_expresspay';
51
52 /**
53 * Register all routes related with stores
54 *
55 * @return void
56 */
57 public function __construct()
58 {
59 add_action('rest_api_init', array($this, 'register_flutter_expresspay_routes'));
60 }
61
62 public function register_flutter_expresspay_routes()
63 {
64 register_rest_route($this->namespace, '/card_checkout', array(
65 array(
66 'methods' => "POST",
67 'callback' => array($this, 'card_checkout'),
68 'permission_callback' => function () {
69 return parent::checkApiPermission();
70 }
71 ),
72 ));
73
74 register_rest_route($this->namespace, '/verify_payment', array(
75 array(
76 'methods' => "POST",
77 'callback' => array($this, 'verify_payment'),
78 'permission_callback' => function () {
79 return parent::checkApiPermission();
80 }
81 ),
82 ));
83 }
84
85 public function verify_payment($request)
86 {
87
88 if (!is_plugin_active('woo-web-payment-getaway/web-payment-gateway.php')) {
89 return parent::send_invalid_plugin_error("You need to install ShahbandrPay plugin to use this api");
90 }
91
92 $json = file_get_contents('php://input');
93 $body = json_decode($json, TRUE);
94 $order_id = sanitize_text_field($body['order_id']);
95 $transaction_id = sanitize_text_field($body['transaction_id']);
96
97 if (empty($order_id) || empty($transaction_id)) {
98 return new WP_Error('missing_parameters', 'Order ID and Transaction ID are required.', array('status' => 400));
99 }
100
101 $order = wc_get_order($order_id);
102 if (!$order) {
103 return new WP_Error('order_not_found', 'Order not found.', array('status' => 404));
104 }
105
106 // SECURITY CHECK 0: Authorise the caller against this specific order.
107 // These routes are otherwise gated only by the site-global purchase-code
108 // check, so nothing ties the caller to the order they name.
109 $key_check = mstore_api_check_payment_order_key($order, $body);
110 if (is_wp_error($key_check)) {
111 return $key_check;
112 }
113
114 // SECURITY CHECK 1: Prevent double processing
115 if (!$order->needs_payment()) {
116 return rest_ensure_response(['success' => true, 'message' => 'Order already processed.']);
117 }
118
119 // SECURITY CHECK 2: Replay Attack Prevention (HPOS Compatible)
120 $existing_orders = wc_get_orders(array(
121 'transaction_id' => $transaction_id,
122 'status' => array('processing', 'completed', 'on-hold'),
123 'exclude' => array($order_id),
124 'limit' => 1,
125 'return' => 'ids'
126 ));
127
128 if (!empty($existing_orders)) {
129 $order->add_order_note(sprintf('Security Alert: ExpressPay transaction ID %s already used for another order. Replay attack blocked.', $transaction_id));
130 return new WP_Error('replay_attack', 'This payment transaction was already used for another order.', array('status' => 403));
131 }
132
133 $options = get_option( 'woocommerce_shahbandrpay_settings');
134 $password = isset($options['password']) ? $options['password'] : '';
135 $secret = isset($options['secret']) ? $options['secret'] : '';
136 $new_order_status = !empty($options['new_order_status']) ? $options['new_order_status'] : 'processing';
137
138 if (empty($password) || empty($secret)) {
139 return new WP_Error('expresspay_misconfigured', 'ExpressPay (ShahbandrPay) is not configured.', array('status' => 500));
140 }
141
142 $hash = sha1(md5(strtoupper($transaction_id . $password)));
143 $url = 'https://pay.expresspay.sa/api/v1/payment/status';
144
145 $main_json = [
146 "merchant_key" => $secret,
147 "payment_id" => $transaction_id,
148 "hash" => $hash
149 ];
150
151 $result = $this->expresspay_post_json( $url, $main_json );
152 $http_code = is_wp_error( $result ) ? 0 : wp_remote_retrieve_response_code( $result );
153 $response_body = is_wp_error( $result ) ? '' : wp_remote_retrieve_body( $result );
154
155 if ( is_wp_error( $result ) || $http_code !== 200 ) {
156 $order->add_order_note('Security Alert: ExpressPay S2S verification request failed or returned HTTP ' . $http_code);
157 return new WP_Error('s2s_verification_failed', 'Could not verify payment with ExpressPay.', array('status' => 502));
158 }
159
160 $response = json_decode($response_body, true);
161
162 // SECURITY CHECK 3: Validate Payment Status
163 if (isset($response['status']) && $response['status'] == 'settled') {
164
165 // SECURITY CHECK 4: Bind the transaction to THIS order's amount and currency.
166 //
167 // 'settled' on its own only proves the transaction is real, not that it
168 // paid for this order. Without this an attacker can settle a cheap order,
169 // withhold the payment_success call so it stays pending (and therefore
170 // out of the replay check above), then present that transaction id
171 // against an expensive order.
172 //
173 // ExpressPay echoes the fields card_checkout() submits (order_amount /
174 // order_currency); the alternatives are accepted so a response shape
175 // change does not silently disable the check.
176 $paid_amount = $this->first_present($response, array('order_amount', 'amount', 'total'));
177 $paid_currency = $this->first_present($response, array('order_currency', 'currency'));
178
179 if ($paid_amount === null) {
180 // Fail closed: an unverifiable amount is exactly the case being exploited.
181 $order->add_order_note('Security Alert: ExpressPay status response carried no amount field, so the transaction could not be bound to this order. Payment not applied. Response keys: ' . esc_html(implode(', ', array_keys((array) $response))));
182 return new WP_Error('amount_unverifiable', 'Could not verify the paid amount with ExpressPay.', array('status' => 502));
183 }
184
185 $order_total = (float) $order->get_total();
186
187 if (abs($order_total - (float) $paid_amount) > 0.01) {
188 $order->add_order_note(sprintf(
189 'Security Alert: ExpressPay amount mismatch. Order total: %s, Paid: %s. Order status unchanged.',
190 esc_html($order_total),
191 esc_html($paid_amount)
192 ));
193 return new WP_Error('amount_mismatch', 'Paid amount does not match order total.', array('status' => 400));
194 }
195
196 if ($paid_currency !== null
197 && strtoupper((string) $paid_currency) !== strtoupper($order->get_currency())) {
198 $order->add_order_note(sprintf(
199 'Security Alert: ExpressPay currency mismatch. Order currency: %s, Paid: %s. Order status unchanged.',
200 esc_html($order->get_currency()),
201 esc_html($paid_currency)
202 ));
203 return new WP_Error('currency_mismatch', 'Paid currency does not match order currency.', array('status' => 400));
204 }
205
206 // All checks passed.
207 $order->payment_complete($transaction_id);
208 $order->add_order_note('ExpressPay Verified: S2S Payment successful.<br/>Transaction ID: ' . esc_html($transaction_id));
209 $order->update_meta_data('_expresspay_transaction_id', $transaction_id);
210 $order->save();
211
212 if ($order->get_status() !== $new_order_status) {
213 $order->update_status($new_order_status);
214 }
215
216 return ['success' => true];
217 } else {
218 $order->add_order_note('Security Alert: ExpressPay payment not settled. Status: ' . esc_html($response['status'] ?? 'unknown'));
219 return new WP_Error('payment_not_settled', $response['reason'] ?? 'ExpressPay payment failed.', array('status' => 400));
220 }
221 }
222
223 /**
224 * Return the first key present and non-empty-string in $data, or null.
225 *
226 * @param array $data
227 * @param array $keys Candidate keys, in priority order.
228 * @return mixed|null
229 */
230 private function first_present($data, $keys)
231 {
232 if (!is_array($data)) {
233 return null;
234 }
235
236 foreach ($keys as $key) {
237 if (isset($data[$key]) && $data[$key] !== '') {
238 return $data[$key];
239 }
240 }
241
242 return null;
243 }
244
245 public function card_checkout($request)
246 {
247 if (!is_plugin_active('woo-web-payment-getaway/web-payment-gateway.php')) {
248 return parent::send_invalid_plugin_error("You need to install ShahbandrPay plugin to use this api");
249 }
250
251 $json = file_get_contents('php://input');
252 $body = json_decode($json, TRUE);
253 $order_id = sanitize_text_field($body['order_id']);
254 $card_number = sanitize_text_field($body['card_number']);
255 $card_exp = sanitize_text_field($body['card_exp']);
256 $card_cvc = sanitize_text_field($body['card_cvc']);
257 $return_url = sanitize_text_field($body['return_url']);
258
259
260 $options = get_option( 'woocommerce_shahbandrpay_settings');
261 $password = $options['password'];
262 $secret = $options['secret'];
263
264 global $woocommerce;
265
266 $order = new WC_Order($order_id);
267 $user = $order->get_user();
268 $user_id = $order->get_user_id();
269 $currency = method_exists( $order, 'get_currency' ) ? $order->get_currency() : $order->order_currency;
270
271 $action_adr = 'https://api.expresspay.sa/post';
272 $customerName = '';
273 if(mb_detect_encoding($order->get_billing_first_name()) !== 'UTF-8' && mb_detect_encoding($order->get_billing_last_name()) !== 'UTF-8') {
274 $customerName = $order->get_billing_first_name() . ' ' . $order->get_billing_last_name();
275 }
276 $email = $order->get_billing_email() ? $order->get_billing_email() : $user->email;
277
278 if ($customerName == '') {
279 $customer = array(
280 'email' => $email
281 );
282 } else {
283 $customer = array(
284 'name' => $customerName,
285 'email' => $email
286 );
287 }
288
289 $billing_address = array(
290 'country' => $order->get_billing_country() ? $order->get_billing_country() : 'NA',
291 'state' => $order->get_billing_state() ? $order->get_billing_state() : 'NA',
292 'city' => $order->get_billing_city() ? $order->get_billing_city() : 'NA',
293 'address' => $order->get_billing_address_1() ? $order->get_billing_address_1() : 'NA',
294 'zip' => $order->get_billing_postcode() ? $order->get_billing_postcode() : '12271',
295 'phone' => $order->get_billing_phone() ? $order->get_billing_phone() : '',
296 'email' => $email
297 );
298
299 $amount = number_format($order->get_total(), 2, '.', '');
300
301 $order_json = array(
302 'number' => "$order_id",
303 'description' => __('Payment Order # ', 'mstore-api') . $order_id . __(' in the store ', 'mstore-api') . home_url('/'),
304 'amount' => $amount,
305 'currency' => $currency,
306 );
307
308 $card_number = str_replace(" ","",$card_number);
309 if ($card_exp) {
310 $exp_array = explode('/', $card_exp);
311 $month = str_replace(" ", "",$exp_array[0]);
312 $year = str_replace(" ", "",'20'.$exp_array[1]);
313 } else {
314 $month = '';
315 $year = '';
316 }
317
318 $hash = md5(strtoupper(strrev($email).$password.strrev(substr($card_number,0,6).substr($card_number,-4))));
319
320 $data = [
321 'action' => 'SALE',
322 'client_key' => $secret,
323 'order_id' => 'ORDER-' . $order_id . time(),
324 'order_amount' => $amount,
325 'order_currency' => $currency,
326 'order_description' => __('Product Order # ', 'mstore-api') . $order_id,
327 'card_number' => $card_number,
328 'card_exp_month' => $month,
329 'card_exp_year' => $year,
330 'card_cvv2' => $card_cvc,
331 'payer_first_name' => $order->get_billing_first_name(),
332 'payer_last_name' => $order->get_billing_last_name(),
333 'payer_address' => $billing_address['address'],
334 'payer_country' => $billing_address['country'],
335 'payer_city' => $billing_address['city'],
336 'payer_zip' => $billing_address['zip'],
337 'payer_email' => $billing_address['email'],
338 'payer_phone' => $billing_address['phone'],
339 'payer_ip' => '123.123.123.123',
340 'term_url_3ds' => $return_url,
341 'hash' => $hash,
342 ];
343
344
345 $result = $this->expresspay_post_form( $action_adr, $data );
346 $httpcode = is_wp_error( $result ) ? 0 : wp_remote_retrieve_response_code( $result );
347 $response_body = is_wp_error( $result ) ? '' : wp_remote_retrieve_body( $result );
348 $response = json_decode($response_body, true);
349
350 if (is_wp_error($result) || $httpcode != 200) {
351 $errors = '';
352 if (isset($response['errors']) && is_array($response['errors'])) {
353 foreach($response['errors'] as $value){
354 $errors .= $value['error_code'] . ' : ' .$value['error_message'].'<br>';
355 }
356 }
357 if ($errors === '') {
358 $errors = 'Please try again.';
359 }
360 return parent::sendError("invalid_payment", $errors, 400);
361 }
362
363 if ($response['result'] == 'SUCCESS' && $response['status'] == 'SETTLED') {
364
365 $order->payment_complete($order_id);
366 $order->update_status($new_order_status, 'ShahbandrPay successfully paid');
367 $order->add_order_note( 'ShahbandrPay successfully paid' );
368
369 update_post_meta( $order_id, 'trans_id', $response['trans_id'] );
370 update_post_meta( $order_id, 'trans_date', $response['trans_date'] );
371 update_post_meta( $order_id, 'trans_hash', $hash );
372
373 return array(
374 'success' => true,
375 );
376 }elseif($response['result'] == 'REDIRECT' && $response['status'] == 'REDIRECT' ){
377 $order->update_status('on-hold', 'Awaiting 3-D Secure Payment');
378 update_post_meta( $order_id, 'trans_id', $response['trans_id'] );
379 update_post_meta( $order_id, 'trans_date', $response['trans_date'] );
380 update_post_meta( $order_id, 'trans_hash', $hash );
381
382 $body = $response['redirect_params']['body'];
383 $url = $response['redirect_url'];
384 $method = $response['redirect_method'];
385
386 return array(
387 'body' => $response['redirect_params']['body'],
388 'url' => $response['redirect_url'],
389 'method' => $response['redirect_method'],
390 'trans_id' => $response['trans_id']
391 );
392 }
393 else {
394 return parent::sendError("invalid_payment", 'Please try again.', 400);
395 }
396 }
397 }
398
399 new FlutterExpressPay;
400