0, 'store_connected_at' => '', 'store_last_error' => '', 'connection_id' => '', 'store_public_key' => '', 'store_secret' => '', 'wc_key_id' => 0, ]; $opt = get_option(AILOQUENCE_CONNECTOR_STORE_OPTION, []); return wp_parse_args(is_array($opt) ? $opt : [], $defaults); } function ailoquence_update_store_options(array $store_opt) { update_option(AILOQUENCE_CONNECTOR_STORE_OPTION, $store_opt, false); } function ailoquence_generate_store_credentials() { return [ 'store_public_key' => wp_generate_password(24, false, false), 'store_secret' => wp_generate_password(48, false, false), ]; } function ailoquence_ensure_store_credentials() { $store_opt = ailoquence_get_store_options(); if (empty($store_opt['store_public_key']) || empty($store_opt['store_secret'])) { $store_opt = array_merge($store_opt, ailoquence_generate_store_credentials()); ailoquence_update_store_options($store_opt); } return $store_opt; } function ailoquence_is_woocommerce_active() { return class_exists('WooCommerce'); } /** * URL base de la API de Ailoquence. Para desarrollo contra un backend local/ngrok: * define('AILOQUENCE_CONNECTOR_API_URL', 'https://xxxx.ngrok-free.app/api'); // en wp-config.php * Sustituye al campo "API Host" del modo dev oculto de 0.1.x (?ailoquence-dev=true). */ function ailoquence_api_base_url() { $url = defined('AILOQUENCE_CONNECTOR_API_URL') ? AILOQUENCE_CONNECTOR_API_URL : 'https://api-pro.ailoquence.com/api'; return untrailingslashit((string) $url); } /** * INSERT directo en la tabla de WooCommerce: es lo mismo que hace internamente WooCommerce al crear * una key desde WooCommerce > Ajustes > Avanzado > REST API (WC_Auth::create_keys es protected y el * AJAX de la pantalla de keys exige un formulario de admin). La alternativa oficial sin INSERT es * el flujo /wc-auth/v1/authorize, que requiere cambios en el backend — ver * src/assets/plans/wordpress-org-plugin-submission.md (fase 2). */ function ailoquence_create_wc_api_key() { if (!ailoquence_is_woocommerce_active()) { return new WP_Error('ailoquence_woocommerce_missing', __('WooCommerce is not active.', 'ailoquence-connector')); } if (!function_exists('wc_rand_hash') || !function_exists('wc_api_hash')) { return new WP_Error('ailoquence_woocommerce_functions_missing', __('WooCommerce functions are not available.', 'ailoquence-connector')); } $user_id = get_current_user_id(); if (!$user_id) { return new WP_Error('ailoquence_invalid_user', __('Could not identify the current user.', 'ailoquence-connector')); } global $wpdb; $consumer_key = 'ck_' . wc_rand_hash(); $consumer_secret = 'cs_' . wc_rand_hash(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- see function docblock. $inserted = $wpdb->insert( $wpdb->prefix . 'woocommerce_api_keys', [ 'user_id' => $user_id, 'description' => 'Ailoquence Auto Connection', 'permissions' => 'read_write', 'consumer_key' => wc_api_hash($consumer_key), 'consumer_secret' => $consumer_secret, 'truncated_key' => substr($consumer_key, -7), ], ['%d', '%s', '%s', '%s', '%s', '%s'] ); if (!$inserted) { return new WP_Error('ailoquence_key_creation_failed', __('Could not create the WooCommerce API key.', 'ailoquence-connector')); } return [ 'key_id' => (int) $wpdb->insert_id, 'consumer_key' => $consumer_key, 'consumer_secret' => $consumer_secret, ]; } /** * Solo borra keys creadas por este plugin (misma descripción), nunca una key del usuario aunque el * key_id guardado apuntase a ella por error. */ function ailoquence_revoke_wc_api_key($key_id) { $key_id = (int) $key_id; if ($key_id <= 0 || !ailoquence_is_woocommerce_active()) return; global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- WooCommerce has no public API to revoke a key. $wpdb->delete( $wpdb->prefix . 'woocommerce_api_keys', ['key_id' => $key_id, 'description' => 'Ailoquence Auto Connection'], ['%d', '%s'] ); } function ailoquence_send_store_credentials($consumer_key, $consumer_secret) { $opt = ailoquence_get_options(); $store_opt = ailoquence_get_store_options(); $response = wp_remote_post(ailoquence_api_base_url() . '/wp/store/auto-connect', [ 'timeout' => 20, 'headers' => ['Content-Type' => 'application/json'], 'body' => wp_json_encode([ 'siteUrl' => home_url(), 'consumerKey' => $consumer_key, 'consumerSecret' => $consumer_secret, 'pluginPublicKey' => $opt['public_key'], 'storePublicKey' => $store_opt['store_public_key'], 'storeSecret' => $store_opt['store_secret'], ]), ]); if (is_wp_error($response)) { return $response; } $code = (int) wp_remote_retrieve_response_code($response); $json = json_decode(wp_remote_retrieve_body($response), true); if ($code < 200 || $code >= 300) { if (!empty($json['message'])) { $message = sanitize_text_field((string) $json['message']); } elseif ($code >= 500) { /* translators: 1: HTTP status code, 2: this site's URL. */ $message = sprintf(__('Could not connect the store (code %1$d). In Ailoquence, check that the store was added with this URL (%2$s) and that the Store Public Key and Store Secret match.', 'ailoquence-connector'), $code, home_url()); } elseif ($code === 401 || $code === 403) { /* translators: %d: HTTP status code. */ $message = sprintf(__('Wrong credentials or missing permissions (code %d). Regenerate the plugin credentials and try again.', 'ailoquence-connector'), $code); } elseif ($code === 404) { $message = __('Not found in Ailoquence (code 404). Check that your project is set up correctly.', 'ailoquence-connector'); } else { /* translators: %d: HTTP status code. */ $message = sprintf(__('Error %d while connecting to Ailoquence.', 'ailoquence-connector'), $code); } return new WP_Error('ailoquence_connect_failed', $message); } return is_array($json) ? $json : []; } /** * Devuelve true o WP_Error. Si Ailoquence rechaza la conexión se revoca la key recién creada (no * ha llegado a nadie); si la acepta, se revoca la anterior — el backend ya tiene la nueva. */ function ailoquence_connect_store() { $store_opt = ailoquence_get_store_options(); $keys = ailoquence_create_wc_api_key(); if (is_wp_error($keys)) { $store_opt['store_last_error'] = $keys->get_error_message(); ailoquence_update_store_options($store_opt); return $keys; } $result = ailoquence_send_store_credentials($keys['consumer_key'], $keys['consumer_secret']); if (is_wp_error($result)) { ailoquence_revoke_wc_api_key($keys['key_id']); $store_opt['store_last_error'] = $result->get_error_message(); ailoquence_update_store_options($store_opt); return $result; } if ((int) $store_opt['wc_key_id'] !== $keys['key_id']) { ailoquence_revoke_wc_api_key($store_opt['wc_key_id']); } // El backend responde "connectionId" (WordpressController.autoConnectStore); 0.1.x leía // "connection_id" y nunca llegaba a guardarlo. Se aceptan ambos. $connection_id = $result['connectionId'] ?? ($result['connection_id'] ?? ''); $store_opt['store_connected'] = 1; $store_opt['store_connected_at'] = current_time('mysql'); $store_opt['store_last_error'] = ''; $store_opt['wc_key_id'] = $keys['key_id']; $store_opt['connection_id'] = sanitize_text_field((string) $connection_id); ailoquence_update_store_options($store_opt); return true; } function ailoquence_disconnect_store() { $store_opt = ailoquence_get_store_options(); ailoquence_revoke_wc_api_key($store_opt['wc_key_id']); $store_opt['store_connected'] = 0; $store_opt['store_connected_at'] = ''; $store_opt['store_last_error'] = ''; $store_opt['connection_id'] = ''; $store_opt['wc_key_id'] = 0; ailoquence_update_store_options($store_opt); } /** * WooCommerce 10.3+ registra las rutas REST `wc/v3` bajo demanda. Cuando entrega un webhook justo * después de un pedido del checkout de bloques (Store API), la ruta del pedido no está cargada y el * cuerpo del webhook sale como `{"code":"rest_no_route",...}` en lugar del pedido: Ailoquence no * sabe qué pedido es y lo pierde (carritos recuperados, reseñas, recompras, ventas). Solo para los * webhooks que entregan a Ailoquence, se reconstruye el cuerpo con el controlador REST de WooCommerce, * que da exactamente los mismos datos que la ruta. */ add_filter('woocommerce_webhook_payload', function ($payload, $resource, $resource_id, $webhook_id) { if (!is_array($payload) || ($payload['code'] ?? '') !== 'rest_no_route') return $payload; $controllers = ['order' => 'WC_REST_Orders_Controller', 'product' => 'WC_REST_Products_Controller']; if (!isset($controllers[$resource]) || !class_exists($controllers[$resource]) || !is_numeric($resource_id)) return $payload; $webhook = function_exists('wc_get_webhook') ? wc_get_webhook($webhook_id) : null; if (!$webhook || strpos((string) $webhook->get_delivery_url(), '/api/woocommerce/webhooks/') === false) return $payload; try { $controller = new $controllers[$resource](); $request = new WP_REST_Request('GET'); $request->set_url_params(['id' => (int) $resource_id]); $request->set_param('context', 'view'); $response = $controller->get_item($request); if (is_wp_error($response)) return $payload; $data = rest_get_server()->response_to_data(rest_ensure_response($response), false); return json_decode(wp_json_encode($data), true); } catch (\Throwable $e) { return $payload; } }, 10, 4);