'', 'installation_id' => '', 'installation_token' => '', 'callback_secret' => '', 'status' => 'disconnected', 'connected_at' => '', ) ); } public static function is_connected() { $c = self::get(); return 'connected' === $c['status'] && ! empty( $c['installation_id'] ) && ! empty( $c['installation_token'] ) && ! empty( $c['callback_secret'] ); } /** * التوكين بعد فك التشفير — البلج إن بيستخدمه لما هو اللي بيبعت لمنصة * WinnyPlus (إرسال حدث، فصل الاتصال). */ public static function get_token() { $c = self::get(); if ( empty( $c['installation_token'] ) ) { return ''; } return WinnyPlus_Connect_Crypto::decrypt( $c['installation_token'] ); } /** * What the platform said was wrong, from an error answer. The platform answers `{"error": {"message": ...}}`; an older or foreign answer may * carry a top-level `message`. Empty when the answer says nothing usable (the caller then shows its own words). * * @param mixed $body The decoded JSON answer. */ public static function message_from( $body ) { if ( ! is_array( $body ) ) { return ''; } if ( ! empty( $body['error']['message'] ) && is_string( $body['error']['message'] ) ) { return sanitize_text_field( $body['error']['message'] ); } if ( ! empty( $body['message'] ) && is_string( $body['message'] ) ) { return sanitize_text_field( $body['message'] ); } return ''; } /** * السر بعد فك التشفير — البلج إن بيستخدمه للتحقق لما منصة WinnyPlus * هي اللي بتبعتله (Push بتاع sync-events)، مش العكس. */ public static function get_callback_secret() { $c = self::get(); if ( empty( $c['callback_secret'] ) ) { return ''; } return WinnyPlus_Connect_Crypto::decrypt( $c['callback_secret'] ); } /** * رابط الـ REST route بتاع البلج إن نفسه، اللي منصة WinnyPlus هتنادي عليه (Push) * كل ما حالة الأحداث المفعّلة تتغيّر من لوحتها. * * مهم: ده لازم يكون الرابط الكامل الجاهز للنداء المباشر (زي ما هو، من غير أي * إضافة زيادة من منصة WinnyPlus) — يعني المفروض يطابق حرفيًا الـ route * المسجل فعليًا في البلج إن (/wp-json/winnyplus-connect/v1/sync-config). */ public static function get_callback_url() { return rest_url( 'winnyplus-connect/v1/sync-config' ); } /** * تنفيذ دورة الربط: بعت connection_code لمنصة WinnyPlus على الرابط المُدخل، * ولو نجح، نخزّن installation_id + التوكين + السر (الاتنين مشفّرين) + الرابط * نفسه محليًا. * * @return array{success:bool, installation_id?:string, error?:string} */ public static function pair( $site_url, $connection_code ) { $site_url = untrailingslashit( trim( (string) $site_url ) ); $connection_code = trim( (string) $connection_code ); if ( '' === $site_url || ! preg_match( '#^https?://#i', $site_url ) ) { return array( 'success' => false, 'error' => __( 'Invalid WinnyPlus platform URL. It must start with http:// or https://', 'winnyplus' ), ); } if ( '' === $connection_code ) { return array( 'success' => false, 'error' => __( 'Please enter the Connection Code first.', 'winnyplus' ) ); } $response = wp_remote_post( $site_url . '/v1/integrations/installations/pair', array( 'timeout' => 20, 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( array( 'connection_code' => $connection_code, 'plugin_callback_url' => self::get_callback_url(), // What this site can send; the platform decides which parts are switched on. 'platform' => 'woocommerce', 'plugin_version' => WINNYPLUS_CONNECT_VERSION, 'capabilities' => WinnyPlus_Connect_Events::get_capabilities(), ) ), ) ); if ( is_wp_error( $response ) ) { return array( 'success' => false, 'error' => $response->get_error_message() ); } $code = wp_remote_retrieve_response_code( $response ); $body = json_decode( wp_remote_retrieve_body( $response ), true ); if ( $code < 200 || $code >= 300 || empty( $body['installation_id'] ) || empty( $body['installation_token'] ) || empty( $body['callback_secret'] ) ) { // The platform's own reason first (for example "This site is already connected to another active installation") $platform_message = self::message_from( $body ); $error = '' !== $platform_message ? $platform_message : __( 'Pairing failed. Please check the code and the WinnyPlus platform URL.', 'winnyplus' ); return array( 'success' => false, 'error' => $error ); } update_option( self::OPTION, array( 'site_url' => $site_url, 'installation_id' => sanitize_text_field( $body['installation_id'] ), 'installation_token' => WinnyPlus_Connect_Crypto::encrypt( $body['installation_token'] ), 'callback_secret' => WinnyPlus_Connect_Crypto::encrypt( $body['callback_secret'] ), 'status' => 'connected', 'connected_at' => current_time( 'mysql' ), ) ); WinnyPlus_Connect_Overview::forget(); // the home reads the new brand's services, not what was kept for another // أول ما نتوصل، نبدأ بخريطة أحداث فاضية (كلها معطّلة) لحد ما WinnyPlus يبعت // أول Push بتاعه — الأحداث بتتفعل من لوحة WinnyPlus مش من هنا. if ( false === get_option( 'winnyplus_connect_enabled_events', false ) ) { add_option( 'winnyplus_connect_enabled_events', array() ); } return array( 'success' => true, 'installation_id' => $body['installation_id'] ); } /** * الفصل من داخل البلج إن. مهم: الفصل المحلي مؤكد دايمًا بغض النظر عن نجاح * أو فشل تبليغ منصة WinnyPlus (زي انقطاع الإنترنت وقتها). */ public static function disconnect() { $c = self::get(); if ( self::is_connected() ) { $token = self::get_token(); $url = trailingslashit( $c['site_url'] ) . 'v1/integrations/installations/' . rawurlencode( $c['installation_id'] ) . '/self-disconnect'; // بنحاول نبلّغ المنصة، لكن مش شرط ينجح عشان نكمل الفصل المحلي wp_remote_post( $url, array( 'timeout' => 10, 'headers' => array( 'Authorization' => 'Bearer ' . $token ), ) ); } delete_option( self::OPTION ); delete_option( 'winnyplus_connect_enabled_events' ); delete_option( WinnyPlus_Connect_Overview::LAST_GOOD ); WinnyPlus_Connect_Overview::forget(); } }