'POST', 'permission_callback' => [self::class, 'pair_permission'], 'callback' => [self::class, 'pair']]); register_rest_route('refref/v1', '/command', ['methods' => 'POST', 'permission_callback' => [self::class, 'signed_permission'], 'callback' => [self::class, 'command']]); }); } public static function pair_permission($request) { $pair = get_option('refref_pair', []); $token = $request->get_param('token'); if (!is_ssl() || !RefRef_Config::valid_site() || !is_string($token) || strlen($token) !== 64 || ($pair['expires'] ?? 0) < time() || !hash_equals($pair['hash'] ?? '', hash('sha256', $token))) { return new WP_Error('refref_auth', 'Connection approval is invalid or expired.', ['status' => 401]); } return true; } public static function pair($request) { if ($request->get_param('mode') !== RefRef_Config::mode()) { return new WP_Error('refref_mode', 'The store and Project environments differ.', ['status' => 409]); } return ['accountId' => RefRef_Config::identity()['id'], 'url' => untrailingslashit(home_url()), 'mode' => RefRef_Config::mode(), 'label' => get_bloginfo('name'), 'secret' => get_option('refref_secret'), 'currency' => get_woocommerce_currency()]; } public static function signed_permission($request) { $time = $request->get_header('x-refref-timestamp'); $secret = get_option('refref_secret'); $signature = $request->get_header('x-refref-signature'); if (!is_ssl() || !RefRef_Config::valid_site() || !$secret || !ctype_digit($time) || abs(time() - (int) $time) > 300 || !hash_equals(hash_hmac('sha256', $time . '.' . $request->get_body(), $secret), $signature)) { return new WP_Error('refref_auth', 'Invalid store signature.', ['status' => 401]); } return true; } public static function command($request) { $input = $request->get_json_params(); try { switch ($input['operation'] ?? '') { case 'activate': $id = $input['connectionId'] ?? ''; $console = $input['consoleUrl'] ?? ''; $billing = $input['billingUrl'] ?? ''; if (!RefRef_Config::valid_billing_url($billing)) { throw new RuntimeException(esc_html__('Invalid billing link.', 'refref')); } if (!is_string($id) || !preg_match('/^[a-zA-Z0-9_-]+$/', $id) || !str_starts_with($console, RefRef_Config::console_url() . '/')) { throw new RuntimeException(esc_html__('Invalid connection binding.', 'refref')); } update_option('refref_connection', ['id' => $id, 'consoleUrl' => $console, 'billingUrl' => $billing], false); delete_option('refref_pair'); return ['active' => true]; case 'health': return ['accountId' => RefRef_Config::identity()['id'], 'mode' => RefRef_Config::mode(), 'currency' => get_woocommerce_currency()]; case 'order': return RefRef_Orders::read(absint($input['id'] ?? 0)); case 'history': $customer = absint($input['customerId'] ?? 0); if (!$customer) { throw new RuntimeException(esc_html__('History requires a customer account.', 'refref')); } $orders = wc_get_orders(['customer_id' => $customer, 'limit' => 10001, 'return' => 'ids']); $ids = []; $complete = count($orders) <= 10000; foreach (RefRef_Orders::history_orders($orders) as $order) { $id = $order->get_id(); $paid = $order->get_date_paid(); if (!$paid || $paid->getTimestamp() < strtotime($input['from']) || $paid->getTimestamp() > strtotime($input['through'])) { continue; } if (!$order->get_meta('_refref_purchase')) { $complete = false; } $ids[] = (string) $id; } return ['ids' => $ids, 'complete' => $complete]; case 'orders': $page = max(1, absint($input['page'] ?? 1)); $orders = wc_get_orders(['limit' => 50, 'page' => $page, 'orderby' => 'ID', 'order' => 'ASC', 'return' => 'ids', 'date_modified' => '>=' . absint($input['since'] ?? 0)]); return ['ids' => array_map('strval', $orders), 'more' => count($orders) === 50]; case 'coupon_read': return RefRef_Coupons::read($input['coupon'] ?? []); case 'coupon': return RefRef_Coupons::issue($input['coupon'] ?? []); case 'coupon_revoke': $reference = $input['reference'] ?? (string) wc_get_coupon_id_by_code($input['code'] ?? ''); return RefRef_Coupons::revoke((string) $reference); default: throw new RuntimeException(esc_html__('Unsupported store operation.', 'refref')); } } catch (Throwable $error) { return new WP_Error('refref_request', $error->getMessage(), ['status' => 400]); } } }