time()+86400,'path'=>'/','secure'=>is_ssl(),'httponly'=>true,'samesite'=>'Strict']); return $value; } private static function browser(WP_REST_Request $request): bool { return Chat4U_Commerce_Context::origin() && Chat4U_Commerce::enabled('order_status') && isset($_COOKIE['chat4u_woo_csrf']) && hash_equals((string)$_COOKIE['chat4u_woo_csrf'],(string)$request->get_param('csrf')); } private static function cookie(): string { return 'chat4u_woo_order_'.substr(hash('sha256',Chat4U_Protocol::connection()['scope'] ?? ''),0,12); } private static function scope(): array { $s=Chat4U_Protocol::connection();return ['scope'=>$s['scope'] ?? '', 'generation'=>$s['generation'] ?? 0]; } private static function grant(): array { $cookie=$_COOKIE[self::cookie()] ?? ''; return preg_match('/^[a-f0-9]{64}$/D',$cookie) ? Chat4U_Commerce_Storage::get('grant:'.hash('sha256',$cookie),'order') : []; } public static function identity(string $challenge): ?string { if (!Chat4U_Commerce::enabled('order_status') || !preg_match('/^[a-f0-9]{32}$/D',$challenge)) { return null; } $user=(int)wp_validate_auth_cookie('','logged_in');$grant=self::grant(); if (!$user && !$grant) { return null; } $handle=bin2hex(random_bytes(32)); Chat4U_Commerce_Storage::put('identity:'.hash('sha256',$handle),'identity',[ 'user'=>$user,'session_token'=>$user ? wp_get_session_token() : null,'grant'=>$user ? null : $grant, 'email_hash'=>$user ? hash_hmac('sha256',strtolower(get_userdata($user)->user_email),wp_salt()) : ($grant['email_hash'] ?? ''),'expires'=>time()+60]+self::scope(),60,'order'); $payload=base64_encode(wp_json_encode(['challenge'=>$challenge,'handle'=>$handle,'email_hash'=>$user ? hash_hmac('sha256',strtolower(get_userdata($user)->user_email),wp_salt()) : ($grant['email_hash'] ?? ''),'expires'=>time()+60]+self::scope())); return $payload.'.'.hash_hmac('sha256',"woo-shopper-v1\n".$payload,Chat4U_Protocol::connection()['secret']); } private static function authority(string $handle): array { if (!Chat4U_Commerce::enabled('order_status') || !preg_match('/^[a-f0-9]{64}$/D',$handle)) { throw new RuntimeException('Verify order access again'); } $value=Chat4U_Commerce_Storage::get('identity:'.hash('sha256',$handle),'order');$scope=self::scope(); if (!$value || $value['scope']!==$scope['scope'] || $value['generation']!==$scope['generation'] || $value['expires']<=time()) { throw new RuntimeException('Verify order access again'); } if ($value['user']) { $user=get_userdata($value['user']); if (!$user || !hash_equals($value['email_hash'],hash_hmac('sha256',strtolower($user->user_email),wp_salt())) || !WP_Session_Tokens::get_instance($value['user'])->verify($value['session_token'])) { throw new RuntimeException('Sign in again'); } } else { $grant=$value['grant'];$order=wc_get_order($grant['order_id'] ?? 0); if (!$order || ($grant['expires'] ?? 0)<=time() || $grant['scope']!==$scope['scope'] || $grant['generation']!==$scope['generation'] || $order->get_customer_id() || !hash_equals($grant['email_hash'],hash_hmac('sha256',strtolower($order->get_billing_email()),wp_salt()))) { throw new RuntimeException('Verify order access again'); } } return $value; } public static function proof(WP_REST_Request $request) { try { self::authority((string)$request->get_param('handle'));return Chat4U_Transfer::response($request,['valid'=>true]); } catch (Throwable $error) { return Chat4U_Transfer::response($request,['valid'=>false]); } } public static function read(WP_REST_Request $request) { try { $identity=self::authority((string)$request->get_param('handle'));$number=(string)($request->get_param('order_number') ?? ''); if (strlen($number)>255) { throw new RuntimeException('Invalid order selection'); } $orders=$identity['user'] ? ($number!=='' ? [self::lookup($number)] : wc_get_orders(['customer_id'=>$identity['user'],'limit'=>5,'orderby'=>'date','order'=>'DESC'])) : [wc_get_order($identity['grant']['order_id'])]; $result=[]; foreach ($orders as $order) { if (!$order || ($identity['user'] && (int)$order->get_customer_id()!==$identity['user'])) { continue; } if ($number!=='' && ltrim($number,'#')!==ltrim($order->get_order_number(),'#')) { continue; } $tracking=[]; foreach (array_slice((array)$order->get_meta('_wc_shipment_tracking_items'),0,5) as $item) { if(!is_array($item)){continue;} $tracking[]=['company'=>mb_substr(sanitize_text_field($item['tracking_provider'] ?? ''),0,100),'number'=>mb_substr(sanitize_text_field($item['tracking_number'] ?? ''),0,200), 'url'=>self::tracking_url($item['custom_tracking_link'] ?? '')]; } $result[]=['order_number'=>$order->get_order_number(),'created_at'=>$order->get_date_created()->date('c'), 'status'=>$order->get_status(),'paid'=>(bool)$order->get_date_paid(),'shipments'=>$tracking, 'items'=>($number!=='' || !$identity['user']) ? self::items($order) : null]; } self::authority((string)$request->get_param('handle')); return Chat4U_Transfer::response($request,['state'=>$result ? 'ready' : 'not_found','orders'=>$result, 'note'=>'Only orders authorized by the current store login or verified one-order grant are shown. Tracking is supplied by the store. No order or address was changed.']); } catch (Throwable $error) { return new WP_Error('chat4u_order_unavailable','Verify order access in this store again.',['status'=>403]); } } public static function code(WP_REST_Request $request) { if (!self::browser($request)) { return new WP_Error('chat4u_verification_unavailable','Open the store page to verify an order.',['status'=>403]); } global $wpdb;$table=Chat4U_Commerce_Storage::table();$verify=$request->get_param('verification'); if ($verify!==null) { if (!is_string($verify) || !preg_match('/^[a-f0-9]{32}$/D',$verify)) { return new WP_Error('chat4u_code_invalid','Check the code or request a new one.',['status'=>409]); } $key='code:'.$verify;$lock='c4u-code:'.$verify; if ((int)$wpdb->get_var($wpdb->prepare('SELECT GET_LOCK(%s,0)',$lock))!==1) { return new WP_Error('chat4u_code_busy','Wait a moment and try again.',['status'=>429]); } try { $value=Chat4U_Commerce_Storage::get($key,'order');$scope=self::scope();$code=(string)$request->get_param('code'); if (!$value || $value['attempts']>=5 || $value['scope']!==$scope['scope'] || $value['generation']!==$scope['generation'] || !hash_equals($value['browser_hash'],hash('sha256',self::csrf()))) { throw new RuntimeException(); } $value['attempts']++;$remaining=max(1,$value['expires']-time());Chat4U_Commerce_Storage::put($key,'code',$value,$remaining,'order'); if (!preg_match('/^[0-9]{6}$/D',$code) || !hash_equals($value['code_hash'],hash_hmac('sha256',$verify.':'.$code,wp_salt())) || !$value['order_id']) { throw new RuntimeException(); } $order=wc_get_order($value['order_id']); if (!$order || $order->get_customer_id() || !hash_equals($value['email_hash'],hash_hmac('sha256',strtolower($order->get_billing_email()),wp_salt()))) { throw new RuntimeException(); } $grant=bin2hex(random_bytes(32)); Chat4U_Commerce_Storage::put('grant:'.hash('sha256',$grant),'grant',['order_id'=>$order->get_id(),'email_hash'=>$value['email_hash'],'expires'=>time()+600]+$scope,600,'order'); $wpdb->delete($table,['record_key'=>$key]); setcookie(self::cookie(),$grant,['expires'=>time()+600,'path'=>'/','secure'=>is_ssl(),'httponly'=>true,'samesite'=>'Strict']); return new WP_REST_Response(['verified'=>true],200,['Cache-Control'=>'no-store']); } catch (Throwable $error) { return new WP_Error('chat4u_code_invalid','Check the code or request a new one.',['status'=>409]); } finally { $wpdb->get_var($wpdb->prepare('SELECT RELEASE_LOCK(%s)',$lock)); } } $email=sanitize_email((string)$request->get_param('email'));$number=(string)$request->get_param('order_number'); if (strlen($email)>254 || strlen($number)>100) { return new WP_Error('chat4u_verification_invalid','Check the order and email details.',['status'=>422]); } $email_hash=hash_hmac('sha256',strtolower($email),wp_salt()); $rate=substr(hash('sha256',$email_hash),0,32); $ip_hash=substr(hash_hmac('sha256',$_SERVER['REMOTE_ADDR'] ?? '',wp_salt()),0,32); $browser_hash=hash('sha256',self::csrf());$limits=self::limits();$lock='c4u-mail-installation'; if ((int)$wpdb->get_var($wpdb->prepare('SELECT GET_LOCK(%s,0)',$lock))!==1) { return self::neutral(); } try { $keys=['mail:'.$rate=>$limits['recipient_per_hour'],'mail-ip:'.$ip_hash=>$limits['ip_per_hour'], 'mail-browser:'.substr($browser_hash,0,32)=>$limits['browser_per_hour'],'mail-installation'=>$limits['installation_per_hour']]; $values=[]; foreach($keys as $key=>$maximum) { $value=Chat4U_Commerce_Storage::get($key,'order'); if (($value['count'] ?? 0)>=$maximum || ($key==='mail:'.$rate && ($value['last'] ?? 0)>time()-$limits['resend_seconds'])) { return self::neutral(); } $values[$key]=['count'=>($value['count'] ?? 0)+1,'last'=>time()]; } foreach($values as $key=>$value) { Chat4U_Commerce_Storage::put($key,'mail',$value,3600,'order'); } } finally { $wpdb->get_var($wpdb->prepare('SELECT RELEASE_LOCK(%s)',$lock)); } $order=self::lookup($number); $matches=$order instanceof WC_Order && !$order->get_customer_id() && ltrim($order->get_order_number(),'#')===ltrim($number,'#') && hash_equals($email_hash,hash_hmac('sha256',strtolower($order->get_billing_email()),wp_salt())); $slot='code-slot:'.hash_hmac('sha256',$browser_hash.'|'.$email_hash.'|'.ltrim($number,'#'),wp_salt()); $previous=Chat4U_Commerce_Storage::get($slot,'order'); if (!empty($previous['verification'])) { $wpdb->delete($table,['record_key'=>'code:'.$previous['verification']]); } $verification=bin2hex(random_bytes(16));$code=(string)random_int(100000,999999); Chat4U_Commerce_Storage::put('code:'.$verification,'code',['order_id'=>$matches ? $order->get_id() : null,'email_hash'=>$email_hash, 'code_hash'=>hash_hmac('sha256',$verification.':'.$code,wp_salt()),'attempts'=>0,'expires'=>time()+600,'browser_hash'=>$browser_hash]+self::scope(),600,'order'); Chat4U_Commerce_Storage::put($slot,'code_slot',['verification'=>$verification,'email_hash'=>$email_hash],600,'order'); if ($matches) { $sent=wp_mail($order->get_billing_email(),__('Your store order verification code','chat4u'),sprintf(__('Your code is %s. It expires in 10 minutes. Do not share it.','chat4u'),$code)); update_option('chat4u_order_mail_health',['state'=>$sent ? 'accepted':'failed','checked_at'=>time()],false); } return self::neutral($verification); } private static function neutral(?string $verification=null): WP_REST_Response { return new WP_REST_Response(['verification'=>$verification ?? bin2hex(random_bytes(16)), 'message'=>'If these details match an eligible guest order, a code has been sent.'],200,['Cache-Control'=>'no-store']); } private static function limits(): array { $defaults=['recipient_per_hour'=>6,'ip_per_hour'=>12,'browser_per_hour'=>12,'installation_per_hour'=>1000,'resend_seconds'=>60]; $configured=apply_filters('chat4u_order_verification_limits',$defaults); foreach($defaults as $key=>$value) { $defaults[$key]=max(1,min(100000,(int)($configured[$key] ?? $value))); } return $defaults; } private static function lookup(string $number) { $order=ctype_digit(ltrim($number,'#')) ? wc_get_order((int)ltrim($number,'#')) : null; return apply_filters('chat4u_order_number_lookup',$order,$number); } private static function tracking_url(string $url): string { if(strlen($url)>2048){return '';} $parts=wp_parse_url($url); if(!$parts || ($parts['scheme'] ?? '')!=='https' || !empty($parts['user']) || !empty($parts['pass'])){return '';} $host=strtolower($parts['host'] ?? ''); foreach((array)apply_filters('chat4u_order_tracking_hosts',['ups.com','fedex.com','dhl.com','usps.com']) as $allowed){ if($host===$allowed || str_ends_with($host,'.'.$allowed)){return esc_url_raw($url,['https']);} } return ''; } private static function items($order): array { global $wpdb; // Order items use this shared Woo table in both qualified HPOS and CPT // storage. Count first so native CRUD cannot materialize an unbounded order. $count=(int)$wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM %i WHERE order_id=%d AND order_item_type='line_item'",$wpdb->prefix.'woocommerce_order_items',$order->get_id())); if($count>20){return ['complete'=>false,'items'=>[],'note'=>'Open the native store for the full item list.'];} $items=[]; foreach($order->get_items('line_item') as $item){$items[]=['name'=>mb_substr(wp_strip_all_tags($item->get_name()),0,300),'quantity'=>$item->get_quantity()];} return ['complete'=>true,'items'=>$items]; } }