'one/inventory', 'inventory_withdraw' => 'one/inventory/withdraw', ); const TERMINAL_CODES = array(400, 409, 413, 422); public static function init() { add_action('rest_api_init', array(__CLASS__, 'register_routes')); add_action(self::CRON_FLUSH, array(__CLASS__, 'flush')); add_action('admin_init', array(__CLASS__, 'maybe_flush')); } public static function fp() { if (class_exists('AIEO_Rooster_Signup') && is_callable(array('AIEO_Rooster_Signup', 'fp'))) { return AIEO_Rooster_Signup::fp(); } $fp = (string) get_option('aieo_rooster_install_fp', ''); if (32 === strlen($fp)) { return $fp; } $fp = substr(hash('sha256', home_url() . '|' . wp_salt('auth')), 0, 32); update_option('aieo_rooster_install_fp', $fp, false); return $fp; } public static function portal_url() { if (class_exists('AIEO_Rooster_Signup') && is_callable(array('AIEO_Rooster_Signup', 'portal_url'))) { return AIEO_Rooster_Signup::portal_url(); } $url = (string) get_option('aieo_rooster_hub_url', 'https://rooster.systems'); return rtrim($url ?: 'https://rooster.systems', '/'); } private static function endpoint($path) { $path = ltrim((string) $path, '/'); if (isset(self::ROUTES[$path])) { return self::portal_url() . '/wp-json/om-portal/v1/' . self::ROUTES[$path]; } return self::portal_url() . '/wp-json/om-portal/v1/content-install/' . $path; } private static function is_one_route($path) { return isset(self::ROUTES[ltrim((string) $path, '/')]); } public static function register_routes() { register_rest_route( 'aieo/v1', '/install-echo', array( 'methods' => 'GET', 'permission_callback' => '__return_true', 'callback' => array(__CLASS__, 'rest_echo'), ) ); } public static function rest_echo(WP_REST_Request $req) { $given = (string) $req->get_param('c'); $stored = (string) get_transient(self::TR_CHALLENGE); if ('' === $given || '' === $stored || !hash_equals($stored, $given)) { return new WP_REST_Response(array('ok' => false), 404); } return new WP_REST_Response(array( 'ok' => true, 'fp' => self::fp(), 'challenge' => $stored, 'home' => home_url(), ), 200); } public static function secret() { return (string) get_option(self::OPT_SECRET, ''); } public static function enrol_error() { $e = get_option(self::OPT_ENROL_ERROR, array()); return is_array($e) && !empty($e['code']) ? $e : array(); } private static function remember_enrol_error($code, $detail = '') { update_option(self::OPT_ENROL_ERROR, array( 'code' => sanitize_key((string) $code), 'detail' => sanitize_key((string) $detail), 'at' => time(), ), false); } public static function ensure_enrolled() { $have = self::secret(); if ('' !== $have) { return $have; } if (get_transient(self::TR_ENROL_BLOCK)) { return ''; } $fp = self::fp(); $r = wp_remote_post(self::endpoint('enrol'), array( 'headers' => array('Content-Type' => 'application/json', 'Accept' => 'application/json'), 'body' => wp_json_encode(array( 'fp' => $fp, 'site_url' => home_url(), 'plugin_version' => defined('AIEO_VERSION') ? AIEO_VERSION : '', )), 'timeout' => 15, 'sslverify' => (bool) apply_filters('aieo_portal_sslverify', true), )); $body = self::body($r); $code = (int) wp_remote_retrieve_response_code($r); if (404 === $code) { set_transient(self::TR_ENROL_BLOCK, 1, DAY_IN_SECONDS); self::remember_enrol_error('portal_not_ready'); return ''; } if (is_wp_error($r) || empty($body['challenge'])) { return ''; } set_transient(self::TR_CHALLENGE, (string) $body['challenge'], 10 * MINUTE_IN_SECONDS); $r2 = wp_remote_post(self::endpoint('enrol/complete'), array( 'headers' => array('Content-Type' => 'application/json', 'Accept' => 'application/json'), 'body' => wp_json_encode(array('fp' => $fp, 'site_url' => home_url())), 'timeout' => 20, 'sslverify' => (bool) apply_filters('aieo_portal_sslverify', true), )); $body2 = self::body($r2); delete_transient(self::TR_CHALLENGE); if (!empty($body2['install_secret'])) { update_option(self::OPT_SECRET, (string) $body2['install_secret'], false); delete_option(self::OPT_ENROL_ERROR); return (string) $body2['install_secret']; } if (!empty($body2['terminal'])) { set_transient(self::TR_ENROL_BLOCK, 1, DAY_IN_SECONDS); self::remember_enrol_error( isset($body2['error_code']) ? $body2['error_code'] : 'enrol_refused', isset($body2['detail']) ? $body2['detail'] : '' ); } return ''; } private static function body($response) { if (is_wp_error($response)) { return array(); } $decoded = json_decode((string) wp_remote_retrieve_body($response), true); return is_array($decoded) ? $decoded : array(); } public static function send($path, array $payload) { $payload['fp'] = self::fp(); $payload['site_url'] = home_url(); $json = wp_json_encode($payload); $headers = array('Content-Type' => 'application/json', 'Accept' => 'application/json'); $secret = self::ensure_enrolled(); if ('' !== $secret) { $ts = (string) time(); $headers['X-OM-Install'] = self::fp(); $headers['X-OM-Install-Ts'] = $ts; $headers['X-OM-Install-Sig'] = hash_hmac('sha256', $json . ':' . $ts, $secret); } elseif (self::is_one_route($path)) { return array( 'ok' => false, 'terminal' => false, 'code' => 0, 'body' => array('code' => 'site_unverified', 'enrol' => self::enrol_error()), ); } if (self::is_one_route($path)) { $key = (string) get_option('aieo_rooster_api_key', ''); if ('' !== $key) { $headers['X-OM-API-Key'] = $key; } } $r = wp_remote_post(self::endpoint($path), array( 'headers' => $headers, 'body' => $json, 'timeout' => self::is_one_route($path) ? 30 : 15, 'sslverify' => (bool) apply_filters('aieo_portal_sslverify', true), )); $code = (int) wp_remote_retrieve_response_code($r); $body = self::body($r); if (!empty($body['rotate'])) { update_option(self::OPT_SECRET_PREV, self::secret(), false); update_option(self::OPT_SECRET, (string) $body['rotate'], false); } if (!empty($body['resync'])) { delete_option(self::OPT_SECRET); } $ok = ($code >= 200 && $code < 300); $terminal = !empty($body['terminal']) || (self::is_one_route($path) && in_array($code, self::TERMINAL_CODES, true)); return array( 'ok' => $ok, 'terminal' => $terminal, 'code' => $code, 'body' => is_array($body) ? $body : array(), ); } public static function submit($path, array $payload) { $res = self::send($path, $payload); if ($res['ok'] || $res['terminal']) { return $res; } self::queue($path, $payload); return $res; } private static function queue($path, array $payload) { $q = get_option(self::OPT_QUEUE, array()); if (!is_array($q)) { $q = array(); } $q[] = array('path' => $path, 'payload' => $payload, 'queued_at' => time()); if (count($q) > self::QUEUE_MAX) { $q = array_slice($q, -self::QUEUE_MAX); } update_option(self::OPT_QUEUE, $q, false); } public static function drop_queued($path) { $q = get_option(self::OPT_QUEUE, array()); if (empty($q) || !is_array($q)) { return; } $keep = array(); foreach ($q as $item) { if (!isset($item['path']) || (string) $item['path'] !== (string) $path) { $keep[] = $item; } } if (count($keep) !== count($q)) { update_option(self::OPT_QUEUE, $keep, false); } } public static function maybe_flush() { if (get_transient(self::TR_FLUSH_THROTTLE)) { return; } $q = get_option(self::OPT_QUEUE, array()); if (empty($q) || !is_array($q)) { return; } set_transient(self::TR_FLUSH_THROTTLE, 1, 15 * MINUTE_IN_SECONDS); self::flush(); } public static function flush() { $q = get_option(self::OPT_QUEUE, array()); if (empty($q) || !is_array($q)) { return; } $keep = array(); foreach ($q as $item) { if (empty($item['path']) || !isset($item['payload']) || !is_array($item['payload'])) { continue; } $res = self::send($item['path'], $item['payload']); if (!$res['ok'] && !$res['terminal']) { $keep[] = $item; } } update_option(self::OPT_QUEUE, $keep, false); } public static function queue_depth() { $q = get_option(self::OPT_QUEUE, array()); return is_array($q) ? count($q) : 0; } } }