Changeset 3777357
- Timestamp:
- 10/31/2026 08:27:10 PM (6 hours ago)
- Location:
- akkedli-connector
- Files:
-
- 6 added
- 28 edited
- 1 copied
-
tags/1.2.0 (copied) (copied from akkedli-connector/trunk )
-
tags/1.2.0/akkedli-connector.php (modified) (2 diffs)
-
tags/1.2.0/includes/class-akkedli-api-handler.php (modified) (4 diffs)
-
tags/1.2.0/includes/class-akkedli-legacy-connection-admin.php (modified) (2 diffs)
-
tags/1.2.0/includes/class-akkedli-legacy-connection-state.php (added)
-
tags/1.2.0/includes/class-akkedli-legacy-session.php (added)
-
tags/1.2.0/includes/class-akkedli-order-handler.php (modified) (3 diffs)
-
tags/1.2.0/includes/class-akkedli-outbound-selector.php (modified) (2 diffs)
-
tags/1.2.0/includes/class-akkedli-settings.php (modified) (3 diffs)
-
tags/1.2.0/includes/class-akkedli-v2-bootstrap.php (modified) (2 diffs)
-
tags/1.2.0/includes/class-akkedli-v2-session-admin.php (modified) (2 diffs)
-
tags/1.2.0/includes/class-akkedli-v2-session-proof-auth.php (modified) (1 diff)
-
tags/1.2.0/includes/class-akkedli-v2-session-state.php (modified) (1 diff)
-
tags/1.2.0/includes/class-akkedli-v2-session-transport.php (modified) (4 diffs)
-
tags/1.2.0/languages/akkedli-connector.pot (modified) (1 diff)
-
tags/1.2.0/readme.txt (modified) (2 diffs)
-
tags/1.2.0/templates/admin-legacy-session.php (added)
-
tags/1.2.0/templates/admin-settings.php (modified) (4 diffs)
-
trunk/akkedli-connector.php (modified) (2 diffs)
-
trunk/includes/class-akkedli-api-handler.php (modified) (4 diffs)
-
trunk/includes/class-akkedli-legacy-connection-admin.php (modified) (2 diffs)
-
trunk/includes/class-akkedli-legacy-connection-state.php (added)
-
trunk/includes/class-akkedli-legacy-session.php (added)
-
trunk/includes/class-akkedli-order-handler.php (modified) (3 diffs)
-
trunk/includes/class-akkedli-outbound-selector.php (modified) (2 diffs)
-
trunk/includes/class-akkedli-settings.php (modified) (3 diffs)
-
trunk/includes/class-akkedli-v2-bootstrap.php (modified) (2 diffs)
-
trunk/includes/class-akkedli-v2-session-admin.php (modified) (2 diffs)
-
trunk/includes/class-akkedli-v2-session-proof-auth.php (modified) (1 diff)
-
trunk/includes/class-akkedli-v2-session-state.php (modified) (1 diff)
-
trunk/includes/class-akkedli-v2-session-transport.php (modified) (4 diffs)
-
trunk/languages/akkedli-connector.pot (modified) (1 diff)
-
trunk/readme.txt (modified) (2 diffs)
-
trunk/templates/admin-legacy-session.php (added)
-
trunk/templates/admin-settings.php (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
akkedli-connector/tags/1.2.0/akkedli-connector.php
r3624443 r3777357 4 4 * Plugin URI: https://akkedli.com 5 5 * Description: Connect your WooCommerce store to Akkedli for automated order verification via WhatsApp. 6 * Version: 1. 1.46 * Version: 1.2.0 7 7 * Author: Akkedli 8 8 * Author URI: https://akkedli.com … … 25 25 // Define plugin constants 26 26 if (!defined('AKKEDLI_VERSION')) { 27 define('AKKEDLI_VERSION', '1. 1.4');27 define('AKKEDLI_VERSION', '1.2.0'); 28 28 } 29 29 if (!defined('AKKEDLI_PLUGIN_URL')) { -
akkedli-connector/tags/1.2.0/includes/class-akkedli-api-handler.php
r3624443 r3777357 13 13 class Akkedli_API_Handler 14 14 { 15 private static $callback_orders = array(); 16 17 public static function is_callback_order($order_id) 18 { 19 return isset(self::$callback_orders[(string) $order_id]); 20 } 15 21 16 22 public function __construct() … … 27 33 'methods' => 'POST', 28 34 'callback' => array($this, 'update_order_status'), 35 'permission_callback' => array($this, 'verify_api_key'), 36 )); 37 38 register_rest_route('akkedli/v1', '/legacy-order-operation', array( 39 'methods' => 'POST', 40 'callback' => array($this, 'legacy_order_operation'), 29 41 'permission_callback' => array($this, 'verify_api_key'), 30 42 )); … … 191 203 } 192 204 $wc_status = $old_status; 205 } elseif ('cancelled' === $verification_result && 'cancelled' === $wc_status) { 206 $lock = Akkedli_V2_Callback_Claims::acquire_order((string) $order_id, time()); 207 if (!is_array($lock)) return $this->legacy_error('callback_busy', 409); 208 try { 209 $order = wc_get_order($order_id); 210 $old_status = $order->get_status(); 211 if ('cancelled' === $old_status) { 212 return array('success' => true, 'order_id' => $order_id, 213 'old_status' => $old_status, 'new_status' => $old_status, 214 'verification_result' => $verification_result); 215 } 216 if (!in_array($old_status, array('pending', 'processing', 'on-hold'), true)) { 217 return $this->legacy_error('transition_protected', 409); 218 } 219 self::$callback_orders[(string) $order_id] = true; 220 $order->set_status('cancelled'); 221 $order->add_order_note(sprintf( 222 /* translators: %1$s: verification result, %2$s: verification timestamp */ 223 __('Order %1$s via Akkedli verification at %2$s', 'akkedli-connector'), 224 'cancelled', 225 $verified_at ? $verified_at : current_time('c') 226 )); 227 if ('yes' !== $order->get_meta(Akkedli_Order_Confirmation::META_KEY, true)) { 228 $order->update_meta_data(Akkedli_Order_Confirmation::META_KEY, 'no'); 229 } 230 $order->update_meta_data('_akkedli_verified_at', $verified_at); 231 $order->update_meta_data('_akkedli_verification_result', $verification_result); 232 if (!$order->save()) return $this->legacy_error('storage_failed', 503); 233 $persisted = $this->reload_legacy_order($order_id); 234 if (!$persisted || 'cancelled' !== $persisted->get_status() 235 || 'cancelled' !== $persisted->get_meta('_akkedli_verification_result', true)) { 236 return $this->legacy_error('storage_failed', 503); 237 } 238 } catch (Throwable $error) { 239 return $this->legacy_error('storage_failed', 503); 240 } finally { 241 unset(self::$callback_orders[(string) $order_id]); 242 Akkedli_V2_Callback_Claims::release_order($lock); 243 } 193 244 } else { 194 $order->set_status($wc_status); 195 $order->add_order_note(sprintf( 196 /* translators: %1$s: verification result, %2$s: verification timestamp */ 197 __('Order %1$s via Akkedli verification at %2$s', 'akkedli-connector'), 198 'cancelled', 199 $verified_at ? $verified_at : current_time('c') 200 )); 201 if ('yes' !== $order->get_meta(Akkedli_Order_Confirmation::META_KEY, true)) { 202 $order->update_meta_data(Akkedli_Order_Confirmation::META_KEY, 'no'); 203 } 204 $order->update_meta_data('_akkedli_verified_at', $verified_at); 205 $order->update_meta_data('_akkedli_verification_result', $verification_result); 206 $order->save(); 245 return $this->legacy_error('invalid_operation', 400); 207 246 } 208 247 … … 215 254 'message' => __('Order status updated successfully', 'akkedli-connector') 216 255 ); 256 } 257 258 /** Authenticated, versioned receipts distinguish application from HTTP acceptance. */ 259 public function legacy_order_operation(WP_REST_Request $request) 260 { 261 $operation = $request->get_param('operation'); 262 if ('capabilities' === $operation) return array('success' => true, 'protocol' => 1, 263 'operations' => array('verified', 'cancelled', 'request_help')); 264 $id = $request->get_param('order_id'); 265 $callback_id = $request->get_param('callback_id'); 266 $revision = $request->get_param('revision'); 267 $number = $request->get_param('order_number'); 268 if (!is_string($id) || !preg_match('/^[1-9][0-9]{0,15}$/D', $id) 269 || (float) $id > PHP_INT_MAX || !is_int($revision) || $revision < 1 270 || !is_string($number) || '' === $number 271 || !is_string($callback_id) || !preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/Di', $callback_id) 272 || !in_array($operation, array('verified', 'cancelled', 'request_help'), true)) { 273 return $this->legacy_error('invalid_operation', 400); 274 } 275 global $wpdb; 276 $mutex = 'akkedli_legacy_' . hash('sha256', (defined('DB_NAME') ? DB_NAME : '') . ':' . $wpdb->prefix . ':' . $id); 277 $mutex = substr($mutex, 0, 64); 278 if ('1' !== (string) $wpdb->get_var($wpdb->prepare('SELECT GET_LOCK(%s,0)', $mutex))) { 279 return $this->legacy_error('callback_busy', 409); 280 } 281 $lock = Akkedli_V2_Callback_Claims::acquire_order($id, time()); 282 if (!is_array($lock)) { 283 $wpdb->get_var($wpdb->prepare('SELECT RELEASE_LOCK(%s)', $mutex)); 284 return $this->legacy_error('callback_busy', 409); 285 } 286 $transaction = false; 287 try { 288 $hpos = class_exists('Automattic\\WooCommerce\\Utilities\\OrderUtil') 289 && \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled(); 290 $table = $hpos ? $wpdb->prefix . 'wc_orders' : $wpdb->posts; 291 foreach (array($table, $hpos ? $wpdb->prefix . 'wc_orders_meta' : $wpdb->prefix . 'postmeta', 292 $wpdb->prefix . 'comments') as $required_table) { 293 $engine = $wpdb->get_var($wpdb->prepare( 294 'SELECT ENGINE FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=%s', $required_table)); 295 if ('innodb' !== strtolower((string) $engine)) return $this->legacy_error('transactional_storage_required', 503); 296 } 297 if (false === $wpdb->query('START TRANSACTION')) return $this->legacy_error('storage_failed', 503); 298 $transaction = true; 299 $column = $hpos ? 'id' : 'ID'; 300 $status_column = $hpos ? 'status' : 'post_status'; 301 $stored_status = $wpdb->get_var($wpdb->prepare("SELECT {$status_column} FROM {$table} WHERE {$column}=%d FOR UPDATE", (int) $id)); 302 if (null === $stored_status) { 303 return $this->legacy_error('order_not_found', 404); 304 } 305 $order = $this->reload_legacy_order((int) $id); 306 if (!$order || (string) $order->get_id() !== $id 307 || (string) $order->get_order_number() !== $number) return $this->legacy_error('order_identity_mismatch', 409); 308 if ($order->get_status() !== preg_replace('/^wc-/', '', $stored_status)) return $this->legacy_error('storage_failed', 503); 309 if (method_exists($order, 'read_meta_data')) $order->read_meta_data(true); 310 if ('absent' !== Akkedli_V2_Outbox::order_provenance((int) $id)) { 311 return $this->legacy_error('transition_protected', 409); 312 } 313 $previous = $this->legacy_meta_from_database((int) $id, '_akkedli_legacy_callback', $hpos); 314 if (is_array($previous) && isset($previous['revision']) && $previous['revision'] >= $revision) { 315 $same = $previous['revision'] === $revision && isset($previous['id'], $previous['operation']) 316 && $previous['id'] === $callback_id && $previous['operation'] === $operation; 317 if (false === $wpdb->query('COMMIT')) return $this->legacy_error('storage_failed', 503); 318 $transaction = false; 319 return $this->legacy_receipt($callback_id, $revision, $operation, 320 $same ? ('protected' === $previous['outcome'] ? 'protected' : 'duplicate') : 'stale'); 321 } 322 $current = $order->get_status(); 323 $eligible = in_array($current, array('pending', 'processing', 'on-hold'), true); 324 $outcome = $eligible ? 'applied' : 'protected'; 325 self::$callback_orders[$id] = true; 326 if ($eligible) { 327 if ('verified' === $operation) { 328 if (!Akkedli_Order_Confirmation::mark_confirmed($order)) return $this->legacy_error('storage_failed', 503); 329 } elseif ('request_help' === $operation) { 330 $order->set_status('on-hold'); 331 } else { 332 $order->set_status('cancelled'); 333 if ('yes' !== $order->get_meta(Akkedli_Order_Confirmation::META_KEY, true)) 334 $order->update_meta_data(Akkedli_Order_Confirmation::META_KEY, 'no'); 335 $order->update_meta_data('_akkedli_verification_result', 'cancelled'); 336 } 337 } 338 $record = array('id' => $callback_id, 'revision' => $revision, 'operation' => $operation, 'outcome' => $outcome); 339 $order->update_meta_data('_akkedli_legacy_callback', $record); 340 if (!$order->save()) return $this->legacy_error('storage_failed', 503); 341 $expected = !$eligible || 'verified' === $operation ? $current 342 : ('request_help' === $operation ? 'on-hold' : 'cancelled'); 343 $saved_status = $wpdb->get_var($wpdb->prepare("SELECT {$status_column} FROM {$table} WHERE {$column}=%d", (int) $id)); 344 if ('wc-' . $expected !== $saved_status 345 || $this->legacy_meta_from_database((int) $id, '_akkedli_legacy_callback', $hpos) !== $record 346 || ($eligible && 'verified' === $operation 347 && 'yes' !== $this->legacy_meta_from_database((int) $id, Akkedli_Order_Confirmation::META_KEY, $hpos))) { 348 return $this->legacy_error('storage_failed', 503); 349 } 350 if (false === $wpdb->query('COMMIT')) return $this->legacy_error('storage_failed', 503); 351 $transaction = false; 352 return $this->legacy_receipt($callback_id, $revision, $operation, $outcome); 353 } catch (Throwable $error) { 354 return $this->legacy_error('storage_failed', 503); 355 } finally { 356 if ($transaction) $wpdb->query('ROLLBACK'); 357 unset(self::$callback_orders[$id]); 358 Akkedli_V2_Callback_Claims::release_order($lock); 359 $wpdb->get_var($wpdb->prepare('SELECT RELEASE_LOCK(%s)', $mutex)); 360 } 361 } 362 363 private function legacy_error($code, $status) 364 { 365 return new WP_Error('akkedli_legacy_' . $code, __('Order operation could not be applied.', 'akkedli-connector'), array('status' => $status)); 366 } 367 368 private function legacy_meta_from_database($id, $key, $hpos) 369 { 370 global $wpdb; 371 $table = $wpdb->prefix . ($hpos ? 'wc_orders_meta' : 'postmeta'); 372 $owner = $hpos ? 'order_id' : 'post_id'; 373 $primary = $hpos ? 'id' : 'meta_id'; 374 return maybe_unserialize($wpdb->get_var($wpdb->prepare( 375 "SELECT meta_value FROM {$table} WHERE {$owner}=%d AND meta_key=%s ORDER BY {$primary} DESC LIMIT 1", $id, $key))); 376 } 377 378 private function legacy_receipt($id, $revision, $operation, $outcome) 379 { 380 return array('success' => true, 'protocol' => 1, 'callback_id' => $id, 381 'revision' => $revision, 'operation' => $operation, 'outcome' => $outcome); 382 } 383 384 private function reload_legacy_order($id) 385 { 386 $cache_class = 'Automattic\\WooCommerce\\Caches\\OrderCache'; 387 if (class_exists($cache_class) && function_exists('wc_get_container')) { 388 wc_get_container()->get($cache_class)->remove($id); 389 } 390 clean_post_cache($id); 391 return wc_get_order($id); 217 392 } 218 393 -
akkedli-connector/tags/1.2.0/includes/class-akkedli-legacy-connection-admin.php
r3624443 r3777357 84 84 exit; 85 85 } 86 update_option('akkedli_api_key', $api_key); 87 update_option('akkedli_webhook_url', $webhook_url); 88 update_option('akkedli_api_key_validated', 1); 89 update_option('akkedli_connected_at', current_time('mysql')); 90 update_option('akkedli_store_domain', home_url()); 86 $values = array('akkedli_api_key' => $api_key, 'akkedli_webhook_url' => $webhook_url, 87 'akkedli_connected_at' => current_time('mysql'), 'akkedli_store_domain' => home_url()); 88 if (!self::persist($values)) { 89 unset($_POST['api_key'], $api_key); 90 wp_safe_redirect(admin_url('admin.php?page=akkedli-connector&error=persistence_failed')); 91 exit; 92 } 91 93 unset($_POST['api_key']); 92 94 $api_key = ''; … … 104 106 } 105 107 108 public static function persist(array $values) 109 { 110 global $wpdb; 111 $suppressed = $wpdb->suppress_errors(true); 112 try { 113 update_option('akkedli_api_key_validated', 0); 114 foreach ($values as $name => $value) update_option($name, $value); 115 foreach ($values as $name => $value) { 116 $stored = $wpdb->get_var($wpdb->prepare("SELECT option_value FROM {$wpdb->options} WHERE option_name=%s", $name)); 117 if (!is_string($stored) || !hash_equals((string) $value, $stored)) return false; 118 } 119 $legacy = Akkedli_Legacy_Connection_State::load(); 120 if (null !== $legacy) { 121 if (!is_array($legacy) || 'disconnected' !== $legacy['status']) return false; 122 $raw = Akkedli_Legacy_Connection_State::raw(); 123 if (1 !== $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->options} WHERE option_name=%s AND option_value=%s", 124 Akkedli_Legacy_Connection_State::OPTION, $raw))) return false; 125 wp_cache_delete(Akkedli_Legacy_Connection_State::OPTION, 'options'); 126 } 127 update_option('akkedli_api_key_validated', 1); 128 return '1' === (string) $wpdb->get_var($wpdb->prepare( 129 "SELECT option_value FROM {$wpdb->options} WHERE option_name=%s", 'akkedli_api_key_validated')); 130 } finally { 131 $wpdb->suppress_errors($suppressed); 132 } 133 } 134 106 135 private static function is_request() 107 136 { -
akkedli-connector/tags/1.2.0/includes/class-akkedli-order-handler.php
r3624443 r3777357 45 45 private function dispatch_new_order($order_id, $order, $store_api) 46 46 { 47 if (Akkedli_API_Handler::is_callback_order($order_id)) return; 47 48 $selection = Akkedli_Outbound_Selector::order($order_id); 48 49 if (Akkedli_Outbound_Selector::is_v2($selection)) { … … 106 107 public function handle_order_processing($order_id, $order = null) 107 108 { 109 if (Akkedli_API_Handler::is_callback_order($order_id)) return; 108 110 $legacy = Akkedli_Outbound_Selector::legacy( 109 111 Akkedli_Outbound_Selector::processing($order_id) … … 153 155 public function handle_status_change($order_id, $old_status, $new_status, $order) 154 156 { 157 if (Akkedli_API_Handler::is_callback_order($order_id)) return; 155 158 $selection = Akkedli_Outbound_Selector::status($order_id, $old_status, $new_status); 156 159 if (Akkedli_Outbound_Selector::is_v2($selection)) { -
akkedli-connector/tags/1.2.0/includes/class-akkedli-outbound-selector.php
r3624443 r3777357 33 33 { 34 34 $state = Akkedli_Connector_State::load(); 35 $session = Akkedli_V2_Session_State::load(); 36 $legacy_session = is_array($session) && isset($session['mode']) && 'legacy' === $session['mode']; 35 37 if (null !== $state || Akkedli_Connector_State::exists() 36 || Akkedli_V2_Session_State::exists()) {38 || (Akkedli_V2_Session_State::exists() && !$legacy_session)) { 37 39 unset($state); 38 40 return array('route' => 'v2'); … … 40 42 if ('absent' !== Akkedli_V2_Outbox::order_provenance($order_id)) 41 43 return array('route' => 'v2'); 44 $legacy = Akkedli_Legacy_Connection_State::load(); 45 if (null !== $legacy) { 46 $active = is_array($legacy) && in_array($legacy['status'], array('pending','connected'), true); 47 return array('route' => 'legacy', 48 'api_key' => $active && isset($legacy['credential']) ? $legacy['credential'] : '', 49 'webhook_url' => $active && isset($legacy['webhook_url']) ? $legacy['webhook_url'] : ''); 50 } 42 51 return array( 43 52 'route' => 'legacy', -
akkedli-connector/tags/1.2.0/includes/class-akkedli-settings.php
r3624443 r3777357 90 90 return ''; 91 91 } 92 if (null !== Akkedli_Legacy_Connection_State::load()) return Akkedli_Legacy_Connection_State::credential(); 92 93 return get_option('akkedli_api_key', ''); 93 94 } … … 101 102 return ''; 102 103 } 104 $legacy = Akkedli_Legacy_Connection_State::load(); 105 if (null !== $legacy) return is_array($legacy) && isset($legacy['webhook_url']) ? $legacy['webhook_url'] : ''; 103 106 return get_option('akkedli_webhook_url', ''); 104 107 } … … 117 120 public static function is_validated() 118 121 { 122 $legacy = Akkedli_Legacy_Connection_State::load(); 123 if (null !== $legacy) return is_array($legacy) && 'connected' === $legacy['status']; 119 124 return (bool) get_option('akkedli_api_key_validated', false); 120 125 } -
akkedli-connector/tags/1.2.0/includes/class-akkedli-v2-bootstrap.php
r3624443 r3777357 19 19 'class-akkedli-v2-session-admin.php', 'class-akkedli-v2-session-proof-auth.php', 20 20 'class-akkedli-v2-session-proof.php', 21 'class-akkedli-legacy-connection-state.php', 'class-akkedli-legacy-session.php', 21 22 'class-akkedli-v2-callback-auth.php', 'class-akkedli-v2-host-binding-auth.php', 22 23 'class-akkedli-v2-host-binding.php', 'class-akkedli-v2-callback-claims.php', … … 42 43 new Akkedli_V2_Enrollment(); 43 44 new Akkedli_V2_Session_Admin(); 45 new Akkedli_Legacy_Session(); 44 46 new Akkedli_V2_Callback(); 45 47 $poller = new Akkedli_V2_Binding_Poller(); -
akkedli-connector/tags/1.2.0/includes/class-akkedli-v2-session-admin.php
r3624443 r3777357 21 21 { 22 22 $stored = Akkedli_V2_Session_State::load(); 23 if (is_array($stored) && isset($stored['mode']) && 'legacy' === $stored['mode']) return false; 23 24 if (is_array($stored)) return array('session' => $stored['session'], 24 25 'expected_store_url' => $stored['expected_store_url'], 'resuming' => true); … … 56 57 { 57 58 $stored = Akkedli_V2_Session_State::load(); 59 if (is_array($stored) && isset($stored['mode']) && 'legacy' === $stored['mode']) return 'session_error'; 58 60 if (is_array($stored)) { 59 61 if (strtotime($stored['expires_at']) <= time()) { -
akkedli-connector/tags/1.2.0/includes/class-akkedli-v2-session-proof-auth.php
r3624443 r3777357 16 16 $state = Akkedli_V2_Session_State::load(); 17 17 if (!is_array($state)) return false; 18 if (!empty($state['revoke_requested'])) return $this->denied(); 18 19 $now = call_user_func($this->clock); 19 20 if (!is_int($now) || strtotime($state['expires_at']) <= $now) return $this->denied(); -
akkedli-connector/tags/1.2.0/includes/class-akkedli-v2-session-state.php
r3624443 r3777357 59 59 $keys = array('session', 'bootstrap_secret', 'callback_url', 60 60 'expected_store_url', 'expires_at'); 61 if (isset($state['mode']) && 'legacy' === $state['mode']) { 62 $keys[] = 'mode'; 63 if (isset($state['revoke_requested'])) { 64 if (true !== $state['revoke_requested']) return false; 65 $keys[] = 'revoke_requested'; 66 } 67 } 61 68 if (!is_array($state) || !Akkedli_V2_Strict_JSON::exact_keys($state, $keys) 62 69 || !self::valid_uuid($state['session']) -
akkedli-connector/tags/1.2.0/includes/class-akkedli-v2-session-transport.php
r3624443 r3777357 7 7 { 8 8 const LIMIT = 4096; 9 private $legacy; 10 public function __construct($legacy = false) { $this->legacy = true === $legacy; } 9 11 private static $paths = array( 12 'revoke' => '/connection-session/revoke', 13 'confirm' => '/connection-session/confirm', 14 'cleanup' => '/connection-session/cleanup', 10 15 'claim' => '/connection-session/claim', 11 16 'status' => '/connection-session/status', … … 29 34 'callback_url' => $state['callback_url']), $state['callback_url']); 30 35 } 36 public function revoke(array $state) { return $this->operation('revoke', $state); } 37 public function confirm(array $state) { return $this->operation('confirm', $state); } 38 public function cleanup(array $state) { return $this->operation('cleanup', $state); } 39 private function operation($operation, array $state) { 40 return $this->request($operation, array('session' => $state['session'], 41 'bootstrap_secret' => $state['bootstrap_secret'], 'callback_url' => $state['callback_url']), $state['callback_url']); 42 } 31 43 private function request($operation, array $payload, $callback_url) 32 44 { 33 $url = self::endpoint($operation );45 $url = self::endpoint($operation, $this->legacy); 34 46 $session = $payload['session']; $body = wp_json_encode($payload, JSON_UNESCAPED_SLASHES); 35 47 unset($payload); … … 59 71 if (!is_string($type) || !preg_match('/^application\/json(?:;\s*charset=utf-8)?$/Di', $type) 60 72 || !is_string($raw) || strlen($raw) >= self::LIMIT) return array('kind' => 'ambiguous'); 73 if ($this->legacy) return Akkedli_Legacy_Connection_State::parse_response($raw, $operation); 61 74 if ('exchange' === $operation) { 62 75 $value = Akkedli_V2_Credential_Response::parse($raw, $callback_url, self::LIMIT); … … 83 96 return array('kind' => 'retry', 'retry_after' => $delay); 84 97 } 85 private static function endpoint($operation )98 private static function endpoint($operation, $legacy = false) 86 99 { 87 100 if (!isset(self::$paths[$operation])) return false; 88 101 $suffix = '/api/connectors/woocommerce/enroll'; 89 102 if (substr(Akkedli_V2_Enrollment::URL, -strlen($suffix)) !== $suffix) return false; 90 return substr(Akkedli_V2_Enrollment::URL, 0, -strlen('/enroll')) . self::$paths[$operation]; 103 $path = $legacy ? str_replace('/connection-session/', '/legacy-session/', self::$paths[$operation]) : self::$paths[$operation]; 104 return substr(Akkedli_V2_Enrollment::URL, 0, -strlen('/enroll')) . $path; 91 105 } 92 106 } -
akkedli-connector/tags/1.2.0/languages/akkedli-connector.pot
r3624443 r3777357 5 5 msgid "" 6 6 msgstr "" 7 "Project-Id-Version: Akkedli Connector 1. 1.4\n"7 "Project-Id-Version: Akkedli Connector 1.2.0\n" 8 8 "Report-Msgid-Bugs-To: https://akkedli.com/support\n" 9 9 "POT-Creation-Date: 2026-02-01 23:25+0000\n" -
akkedli-connector/tags/1.2.0/readme.txt
r3624443 r3777357 5 5 Tested up to: 7.0 6 6 Requires PHP: 7.4 7 Stable tag: 1. 1.47 Stable tag: 1.2.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 135 135 == Changelog == 136 136 137 = 1.2.0 = 138 * Add explicit Legacy code-free WordPress approval, durable verification, and generation-scoped disconnect/reconnection. 139 * Preserve Legacy callback reliability and Classic/HPOS behavior. 140 * Verify manual connection persistence before reporting success. 141 137 142 = 1.1.4 - 2026-07-26 = 138 143 * Security: Keep callback replay tombstones beyond the complete Backend retry horizon -
akkedli-connector/tags/1.2.0/templates/admin-settings.php
r3624443 r3777357 9 9 10 10 $akkedli_is_configured = Akkedli_Settings::is_configured(); 11 $akkedli_is_connected = $akkedli_is_configured && Akkedli_Settings::is_validated(); 11 12 $akkedli_v2_status = Akkedli_Connector_State::status(); 12 13 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display-only URL param from our own redirect. … … 22 23 </h1> 23 24 24 <?php if (false !== Akkedli_V2_Session_Admin::view()): ?> 25 <?php if (false !== Akkedli_Legacy_Session::view()): ?> 26 <?php include AKKEDLI_PLUGIN_PATH . 'templates/admin-legacy-session.php'; ?> 27 <?php elseif (false !== Akkedli_V2_Session_Admin::view()): ?> 25 28 <?php include AKKEDLI_PLUGIN_PATH . 'templates/admin-v2-session.php'; ?> 26 29 <?php elseif ('absent' !== $akkedli_v2_status || !$akkedli_is_configured): ?> … … 29 32 30 33 <?php if ('absent' === $akkedli_v2_status): ?> 31 <?php if ($akkedli_just_connected ): ?>34 <?php if ($akkedli_just_connected && $akkedli_is_connected): ?> 32 35 <div class="notice notice-success inline" style="border-left-color: #10b981;"> 33 36 <p> … … 43 46 </p> 44 47 </div> 45 <?php elseif ($akkedli_is_con figured): ?>48 <?php elseif ($akkedli_is_connected): ?> 46 49 <div class="notice notice-success inline"> 47 50 <p> 48 51 <strong><?php esc_html_e('Connected', 'akkedli-connector'); ?></strong> — 49 52 <?php esc_html_e('Orders will be sent to Akkedli automatically.', 'akkedli-connector'); ?> 53 </p> 54 </div> 55 <?php elseif ($akkedli_is_configured): ?> 56 <div class="notice notice-info inline" role="status"> 57 <p> 58 <strong><?php esc_html_e('Verifying connection', 'akkedli-connector'); ?></strong> — 59 <?php esc_html_e('Connection settings are saved. Waiting for Akkedli to confirm the connection.', 'akkedli-connector'); ?> 50 60 </p> 51 61 </div> -
akkedli-connector/trunk/akkedli-connector.php
r3624443 r3777357 4 4 * Plugin URI: https://akkedli.com 5 5 * Description: Connect your WooCommerce store to Akkedli for automated order verification via WhatsApp. 6 * Version: 1. 1.46 * Version: 1.2.0 7 7 * Author: Akkedli 8 8 * Author URI: https://akkedli.com … … 25 25 // Define plugin constants 26 26 if (!defined('AKKEDLI_VERSION')) { 27 define('AKKEDLI_VERSION', '1. 1.4');27 define('AKKEDLI_VERSION', '1.2.0'); 28 28 } 29 29 if (!defined('AKKEDLI_PLUGIN_URL')) { -
akkedli-connector/trunk/includes/class-akkedli-api-handler.php
r3624443 r3777357 13 13 class Akkedli_API_Handler 14 14 { 15 private static $callback_orders = array(); 16 17 public static function is_callback_order($order_id) 18 { 19 return isset(self::$callback_orders[(string) $order_id]); 20 } 15 21 16 22 public function __construct() … … 27 33 'methods' => 'POST', 28 34 'callback' => array($this, 'update_order_status'), 35 'permission_callback' => array($this, 'verify_api_key'), 36 )); 37 38 register_rest_route('akkedli/v1', '/legacy-order-operation', array( 39 'methods' => 'POST', 40 'callback' => array($this, 'legacy_order_operation'), 29 41 'permission_callback' => array($this, 'verify_api_key'), 30 42 )); … … 191 203 } 192 204 $wc_status = $old_status; 205 } elseif ('cancelled' === $verification_result && 'cancelled' === $wc_status) { 206 $lock = Akkedli_V2_Callback_Claims::acquire_order((string) $order_id, time()); 207 if (!is_array($lock)) return $this->legacy_error('callback_busy', 409); 208 try { 209 $order = wc_get_order($order_id); 210 $old_status = $order->get_status(); 211 if ('cancelled' === $old_status) { 212 return array('success' => true, 'order_id' => $order_id, 213 'old_status' => $old_status, 'new_status' => $old_status, 214 'verification_result' => $verification_result); 215 } 216 if (!in_array($old_status, array('pending', 'processing', 'on-hold'), true)) { 217 return $this->legacy_error('transition_protected', 409); 218 } 219 self::$callback_orders[(string) $order_id] = true; 220 $order->set_status('cancelled'); 221 $order->add_order_note(sprintf( 222 /* translators: %1$s: verification result, %2$s: verification timestamp */ 223 __('Order %1$s via Akkedli verification at %2$s', 'akkedli-connector'), 224 'cancelled', 225 $verified_at ? $verified_at : current_time('c') 226 )); 227 if ('yes' !== $order->get_meta(Akkedli_Order_Confirmation::META_KEY, true)) { 228 $order->update_meta_data(Akkedli_Order_Confirmation::META_KEY, 'no'); 229 } 230 $order->update_meta_data('_akkedli_verified_at', $verified_at); 231 $order->update_meta_data('_akkedli_verification_result', $verification_result); 232 if (!$order->save()) return $this->legacy_error('storage_failed', 503); 233 $persisted = $this->reload_legacy_order($order_id); 234 if (!$persisted || 'cancelled' !== $persisted->get_status() 235 || 'cancelled' !== $persisted->get_meta('_akkedli_verification_result', true)) { 236 return $this->legacy_error('storage_failed', 503); 237 } 238 } catch (Throwable $error) { 239 return $this->legacy_error('storage_failed', 503); 240 } finally { 241 unset(self::$callback_orders[(string) $order_id]); 242 Akkedli_V2_Callback_Claims::release_order($lock); 243 } 193 244 } else { 194 $order->set_status($wc_status); 195 $order->add_order_note(sprintf( 196 /* translators: %1$s: verification result, %2$s: verification timestamp */ 197 __('Order %1$s via Akkedli verification at %2$s', 'akkedli-connector'), 198 'cancelled', 199 $verified_at ? $verified_at : current_time('c') 200 )); 201 if ('yes' !== $order->get_meta(Akkedli_Order_Confirmation::META_KEY, true)) { 202 $order->update_meta_data(Akkedli_Order_Confirmation::META_KEY, 'no'); 203 } 204 $order->update_meta_data('_akkedli_verified_at', $verified_at); 205 $order->update_meta_data('_akkedli_verification_result', $verification_result); 206 $order->save(); 245 return $this->legacy_error('invalid_operation', 400); 207 246 } 208 247 … … 215 254 'message' => __('Order status updated successfully', 'akkedli-connector') 216 255 ); 256 } 257 258 /** Authenticated, versioned receipts distinguish application from HTTP acceptance. */ 259 public function legacy_order_operation(WP_REST_Request $request) 260 { 261 $operation = $request->get_param('operation'); 262 if ('capabilities' === $operation) return array('success' => true, 'protocol' => 1, 263 'operations' => array('verified', 'cancelled', 'request_help')); 264 $id = $request->get_param('order_id'); 265 $callback_id = $request->get_param('callback_id'); 266 $revision = $request->get_param('revision'); 267 $number = $request->get_param('order_number'); 268 if (!is_string($id) || !preg_match('/^[1-9][0-9]{0,15}$/D', $id) 269 || (float) $id > PHP_INT_MAX || !is_int($revision) || $revision < 1 270 || !is_string($number) || '' === $number 271 || !is_string($callback_id) || !preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/Di', $callback_id) 272 || !in_array($operation, array('verified', 'cancelled', 'request_help'), true)) { 273 return $this->legacy_error('invalid_operation', 400); 274 } 275 global $wpdb; 276 $mutex = 'akkedli_legacy_' . hash('sha256', (defined('DB_NAME') ? DB_NAME : '') . ':' . $wpdb->prefix . ':' . $id); 277 $mutex = substr($mutex, 0, 64); 278 if ('1' !== (string) $wpdb->get_var($wpdb->prepare('SELECT GET_LOCK(%s,0)', $mutex))) { 279 return $this->legacy_error('callback_busy', 409); 280 } 281 $lock = Akkedli_V2_Callback_Claims::acquire_order($id, time()); 282 if (!is_array($lock)) { 283 $wpdb->get_var($wpdb->prepare('SELECT RELEASE_LOCK(%s)', $mutex)); 284 return $this->legacy_error('callback_busy', 409); 285 } 286 $transaction = false; 287 try { 288 $hpos = class_exists('Automattic\\WooCommerce\\Utilities\\OrderUtil') 289 && \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled(); 290 $table = $hpos ? $wpdb->prefix . 'wc_orders' : $wpdb->posts; 291 foreach (array($table, $hpos ? $wpdb->prefix . 'wc_orders_meta' : $wpdb->prefix . 'postmeta', 292 $wpdb->prefix . 'comments') as $required_table) { 293 $engine = $wpdb->get_var($wpdb->prepare( 294 'SELECT ENGINE FROM information_schema.TABLES WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=%s', $required_table)); 295 if ('innodb' !== strtolower((string) $engine)) return $this->legacy_error('transactional_storage_required', 503); 296 } 297 if (false === $wpdb->query('START TRANSACTION')) return $this->legacy_error('storage_failed', 503); 298 $transaction = true; 299 $column = $hpos ? 'id' : 'ID'; 300 $status_column = $hpos ? 'status' : 'post_status'; 301 $stored_status = $wpdb->get_var($wpdb->prepare("SELECT {$status_column} FROM {$table} WHERE {$column}=%d FOR UPDATE", (int) $id)); 302 if (null === $stored_status) { 303 return $this->legacy_error('order_not_found', 404); 304 } 305 $order = $this->reload_legacy_order((int) $id); 306 if (!$order || (string) $order->get_id() !== $id 307 || (string) $order->get_order_number() !== $number) return $this->legacy_error('order_identity_mismatch', 409); 308 if ($order->get_status() !== preg_replace('/^wc-/', '', $stored_status)) return $this->legacy_error('storage_failed', 503); 309 if (method_exists($order, 'read_meta_data')) $order->read_meta_data(true); 310 if ('absent' !== Akkedli_V2_Outbox::order_provenance((int) $id)) { 311 return $this->legacy_error('transition_protected', 409); 312 } 313 $previous = $this->legacy_meta_from_database((int) $id, '_akkedli_legacy_callback', $hpos); 314 if (is_array($previous) && isset($previous['revision']) && $previous['revision'] >= $revision) { 315 $same = $previous['revision'] === $revision && isset($previous['id'], $previous['operation']) 316 && $previous['id'] === $callback_id && $previous['operation'] === $operation; 317 if (false === $wpdb->query('COMMIT')) return $this->legacy_error('storage_failed', 503); 318 $transaction = false; 319 return $this->legacy_receipt($callback_id, $revision, $operation, 320 $same ? ('protected' === $previous['outcome'] ? 'protected' : 'duplicate') : 'stale'); 321 } 322 $current = $order->get_status(); 323 $eligible = in_array($current, array('pending', 'processing', 'on-hold'), true); 324 $outcome = $eligible ? 'applied' : 'protected'; 325 self::$callback_orders[$id] = true; 326 if ($eligible) { 327 if ('verified' === $operation) { 328 if (!Akkedli_Order_Confirmation::mark_confirmed($order)) return $this->legacy_error('storage_failed', 503); 329 } elseif ('request_help' === $operation) { 330 $order->set_status('on-hold'); 331 } else { 332 $order->set_status('cancelled'); 333 if ('yes' !== $order->get_meta(Akkedli_Order_Confirmation::META_KEY, true)) 334 $order->update_meta_data(Akkedli_Order_Confirmation::META_KEY, 'no'); 335 $order->update_meta_data('_akkedli_verification_result', 'cancelled'); 336 } 337 } 338 $record = array('id' => $callback_id, 'revision' => $revision, 'operation' => $operation, 'outcome' => $outcome); 339 $order->update_meta_data('_akkedli_legacy_callback', $record); 340 if (!$order->save()) return $this->legacy_error('storage_failed', 503); 341 $expected = !$eligible || 'verified' === $operation ? $current 342 : ('request_help' === $operation ? 'on-hold' : 'cancelled'); 343 $saved_status = $wpdb->get_var($wpdb->prepare("SELECT {$status_column} FROM {$table} WHERE {$column}=%d", (int) $id)); 344 if ('wc-' . $expected !== $saved_status 345 || $this->legacy_meta_from_database((int) $id, '_akkedli_legacy_callback', $hpos) !== $record 346 || ($eligible && 'verified' === $operation 347 && 'yes' !== $this->legacy_meta_from_database((int) $id, Akkedli_Order_Confirmation::META_KEY, $hpos))) { 348 return $this->legacy_error('storage_failed', 503); 349 } 350 if (false === $wpdb->query('COMMIT')) return $this->legacy_error('storage_failed', 503); 351 $transaction = false; 352 return $this->legacy_receipt($callback_id, $revision, $operation, $outcome); 353 } catch (Throwable $error) { 354 return $this->legacy_error('storage_failed', 503); 355 } finally { 356 if ($transaction) $wpdb->query('ROLLBACK'); 357 unset(self::$callback_orders[$id]); 358 Akkedli_V2_Callback_Claims::release_order($lock); 359 $wpdb->get_var($wpdb->prepare('SELECT RELEASE_LOCK(%s)', $mutex)); 360 } 361 } 362 363 private function legacy_error($code, $status) 364 { 365 return new WP_Error('akkedli_legacy_' . $code, __('Order operation could not be applied.', 'akkedli-connector'), array('status' => $status)); 366 } 367 368 private function legacy_meta_from_database($id, $key, $hpos) 369 { 370 global $wpdb; 371 $table = $wpdb->prefix . ($hpos ? 'wc_orders_meta' : 'postmeta'); 372 $owner = $hpos ? 'order_id' : 'post_id'; 373 $primary = $hpos ? 'id' : 'meta_id'; 374 return maybe_unserialize($wpdb->get_var($wpdb->prepare( 375 "SELECT meta_value FROM {$table} WHERE {$owner}=%d AND meta_key=%s ORDER BY {$primary} DESC LIMIT 1", $id, $key))); 376 } 377 378 private function legacy_receipt($id, $revision, $operation, $outcome) 379 { 380 return array('success' => true, 'protocol' => 1, 'callback_id' => $id, 381 'revision' => $revision, 'operation' => $operation, 'outcome' => $outcome); 382 } 383 384 private function reload_legacy_order($id) 385 { 386 $cache_class = 'Automattic\\WooCommerce\\Caches\\OrderCache'; 387 if (class_exists($cache_class) && function_exists('wc_get_container')) { 388 wc_get_container()->get($cache_class)->remove($id); 389 } 390 clean_post_cache($id); 391 return wc_get_order($id); 217 392 } 218 393 -
akkedli-connector/trunk/includes/class-akkedli-legacy-connection-admin.php
r3624443 r3777357 84 84 exit; 85 85 } 86 update_option('akkedli_api_key', $api_key); 87 update_option('akkedli_webhook_url', $webhook_url); 88 update_option('akkedli_api_key_validated', 1); 89 update_option('akkedli_connected_at', current_time('mysql')); 90 update_option('akkedli_store_domain', home_url()); 86 $values = array('akkedli_api_key' => $api_key, 'akkedli_webhook_url' => $webhook_url, 87 'akkedli_connected_at' => current_time('mysql'), 'akkedli_store_domain' => home_url()); 88 if (!self::persist($values)) { 89 unset($_POST['api_key'], $api_key); 90 wp_safe_redirect(admin_url('admin.php?page=akkedli-connector&error=persistence_failed')); 91 exit; 92 } 91 93 unset($_POST['api_key']); 92 94 $api_key = ''; … … 104 106 } 105 107 108 public static function persist(array $values) 109 { 110 global $wpdb; 111 $suppressed = $wpdb->suppress_errors(true); 112 try { 113 update_option('akkedli_api_key_validated', 0); 114 foreach ($values as $name => $value) update_option($name, $value); 115 foreach ($values as $name => $value) { 116 $stored = $wpdb->get_var($wpdb->prepare("SELECT option_value FROM {$wpdb->options} WHERE option_name=%s", $name)); 117 if (!is_string($stored) || !hash_equals((string) $value, $stored)) return false; 118 } 119 $legacy = Akkedli_Legacy_Connection_State::load(); 120 if (null !== $legacy) { 121 if (!is_array($legacy) || 'disconnected' !== $legacy['status']) return false; 122 $raw = Akkedli_Legacy_Connection_State::raw(); 123 if (1 !== $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->options} WHERE option_name=%s AND option_value=%s", 124 Akkedli_Legacy_Connection_State::OPTION, $raw))) return false; 125 wp_cache_delete(Akkedli_Legacy_Connection_State::OPTION, 'options'); 126 } 127 update_option('akkedli_api_key_validated', 1); 128 return '1' === (string) $wpdb->get_var($wpdb->prepare( 129 "SELECT option_value FROM {$wpdb->options} WHERE option_name=%s", 'akkedli_api_key_validated')); 130 } finally { 131 $wpdb->suppress_errors($suppressed); 132 } 133 } 134 106 135 private static function is_request() 107 136 { -
akkedli-connector/trunk/includes/class-akkedli-order-handler.php
r3624443 r3777357 45 45 private function dispatch_new_order($order_id, $order, $store_api) 46 46 { 47 if (Akkedli_API_Handler::is_callback_order($order_id)) return; 47 48 $selection = Akkedli_Outbound_Selector::order($order_id); 48 49 if (Akkedli_Outbound_Selector::is_v2($selection)) { … … 106 107 public function handle_order_processing($order_id, $order = null) 107 108 { 109 if (Akkedli_API_Handler::is_callback_order($order_id)) return; 108 110 $legacy = Akkedli_Outbound_Selector::legacy( 109 111 Akkedli_Outbound_Selector::processing($order_id) … … 153 155 public function handle_status_change($order_id, $old_status, $new_status, $order) 154 156 { 157 if (Akkedli_API_Handler::is_callback_order($order_id)) return; 155 158 $selection = Akkedli_Outbound_Selector::status($order_id, $old_status, $new_status); 156 159 if (Akkedli_Outbound_Selector::is_v2($selection)) { -
akkedli-connector/trunk/includes/class-akkedli-outbound-selector.php
r3624443 r3777357 33 33 { 34 34 $state = Akkedli_Connector_State::load(); 35 $session = Akkedli_V2_Session_State::load(); 36 $legacy_session = is_array($session) && isset($session['mode']) && 'legacy' === $session['mode']; 35 37 if (null !== $state || Akkedli_Connector_State::exists() 36 || Akkedli_V2_Session_State::exists()) {38 || (Akkedli_V2_Session_State::exists() && !$legacy_session)) { 37 39 unset($state); 38 40 return array('route' => 'v2'); … … 40 42 if ('absent' !== Akkedli_V2_Outbox::order_provenance($order_id)) 41 43 return array('route' => 'v2'); 44 $legacy = Akkedli_Legacy_Connection_State::load(); 45 if (null !== $legacy) { 46 $active = is_array($legacy) && in_array($legacy['status'], array('pending','connected'), true); 47 return array('route' => 'legacy', 48 'api_key' => $active && isset($legacy['credential']) ? $legacy['credential'] : '', 49 'webhook_url' => $active && isset($legacy['webhook_url']) ? $legacy['webhook_url'] : ''); 50 } 42 51 return array( 43 52 'route' => 'legacy', -
akkedli-connector/trunk/includes/class-akkedli-settings.php
r3624443 r3777357 90 90 return ''; 91 91 } 92 if (null !== Akkedli_Legacy_Connection_State::load()) return Akkedli_Legacy_Connection_State::credential(); 92 93 return get_option('akkedli_api_key', ''); 93 94 } … … 101 102 return ''; 102 103 } 104 $legacy = Akkedli_Legacy_Connection_State::load(); 105 if (null !== $legacy) return is_array($legacy) && isset($legacy['webhook_url']) ? $legacy['webhook_url'] : ''; 103 106 return get_option('akkedli_webhook_url', ''); 104 107 } … … 117 120 public static function is_validated() 118 121 { 122 $legacy = Akkedli_Legacy_Connection_State::load(); 123 if (null !== $legacy) return is_array($legacy) && 'connected' === $legacy['status']; 119 124 return (bool) get_option('akkedli_api_key_validated', false); 120 125 } -
akkedli-connector/trunk/includes/class-akkedli-v2-bootstrap.php
r3624443 r3777357 19 19 'class-akkedli-v2-session-admin.php', 'class-akkedli-v2-session-proof-auth.php', 20 20 'class-akkedli-v2-session-proof.php', 21 'class-akkedli-legacy-connection-state.php', 'class-akkedli-legacy-session.php', 21 22 'class-akkedli-v2-callback-auth.php', 'class-akkedli-v2-host-binding-auth.php', 22 23 'class-akkedli-v2-host-binding.php', 'class-akkedli-v2-callback-claims.php', … … 42 43 new Akkedli_V2_Enrollment(); 43 44 new Akkedli_V2_Session_Admin(); 45 new Akkedli_Legacy_Session(); 44 46 new Akkedli_V2_Callback(); 45 47 $poller = new Akkedli_V2_Binding_Poller(); -
akkedli-connector/trunk/includes/class-akkedli-v2-session-admin.php
r3624443 r3777357 21 21 { 22 22 $stored = Akkedli_V2_Session_State::load(); 23 if (is_array($stored) && isset($stored['mode']) && 'legacy' === $stored['mode']) return false; 23 24 if (is_array($stored)) return array('session' => $stored['session'], 24 25 'expected_store_url' => $stored['expected_store_url'], 'resuming' => true); … … 56 57 { 57 58 $stored = Akkedli_V2_Session_State::load(); 59 if (is_array($stored) && isset($stored['mode']) && 'legacy' === $stored['mode']) return 'session_error'; 58 60 if (is_array($stored)) { 59 61 if (strtotime($stored['expires_at']) <= time()) { -
akkedli-connector/trunk/includes/class-akkedli-v2-session-proof-auth.php
r3624443 r3777357 16 16 $state = Akkedli_V2_Session_State::load(); 17 17 if (!is_array($state)) return false; 18 if (!empty($state['revoke_requested'])) return $this->denied(); 18 19 $now = call_user_func($this->clock); 19 20 if (!is_int($now) || strtotime($state['expires_at']) <= $now) return $this->denied(); -
akkedli-connector/trunk/includes/class-akkedli-v2-session-state.php
r3624443 r3777357 59 59 $keys = array('session', 'bootstrap_secret', 'callback_url', 60 60 'expected_store_url', 'expires_at'); 61 if (isset($state['mode']) && 'legacy' === $state['mode']) { 62 $keys[] = 'mode'; 63 if (isset($state['revoke_requested'])) { 64 if (true !== $state['revoke_requested']) return false; 65 $keys[] = 'revoke_requested'; 66 } 67 } 61 68 if (!is_array($state) || !Akkedli_V2_Strict_JSON::exact_keys($state, $keys) 62 69 || !self::valid_uuid($state['session']) -
akkedli-connector/trunk/includes/class-akkedli-v2-session-transport.php
r3624443 r3777357 7 7 { 8 8 const LIMIT = 4096; 9 private $legacy; 10 public function __construct($legacy = false) { $this->legacy = true === $legacy; } 9 11 private static $paths = array( 12 'revoke' => '/connection-session/revoke', 13 'confirm' => '/connection-session/confirm', 14 'cleanup' => '/connection-session/cleanup', 10 15 'claim' => '/connection-session/claim', 11 16 'status' => '/connection-session/status', … … 29 34 'callback_url' => $state['callback_url']), $state['callback_url']); 30 35 } 36 public function revoke(array $state) { return $this->operation('revoke', $state); } 37 public function confirm(array $state) { return $this->operation('confirm', $state); } 38 public function cleanup(array $state) { return $this->operation('cleanup', $state); } 39 private function operation($operation, array $state) { 40 return $this->request($operation, array('session' => $state['session'], 41 'bootstrap_secret' => $state['bootstrap_secret'], 'callback_url' => $state['callback_url']), $state['callback_url']); 42 } 31 43 private function request($operation, array $payload, $callback_url) 32 44 { 33 $url = self::endpoint($operation );45 $url = self::endpoint($operation, $this->legacy); 34 46 $session = $payload['session']; $body = wp_json_encode($payload, JSON_UNESCAPED_SLASHES); 35 47 unset($payload); … … 59 71 if (!is_string($type) || !preg_match('/^application\/json(?:;\s*charset=utf-8)?$/Di', $type) 60 72 || !is_string($raw) || strlen($raw) >= self::LIMIT) return array('kind' => 'ambiguous'); 73 if ($this->legacy) return Akkedli_Legacy_Connection_State::parse_response($raw, $operation); 61 74 if ('exchange' === $operation) { 62 75 $value = Akkedli_V2_Credential_Response::parse($raw, $callback_url, self::LIMIT); … … 83 96 return array('kind' => 'retry', 'retry_after' => $delay); 84 97 } 85 private static function endpoint($operation )98 private static function endpoint($operation, $legacy = false) 86 99 { 87 100 if (!isset(self::$paths[$operation])) return false; 88 101 $suffix = '/api/connectors/woocommerce/enroll'; 89 102 if (substr(Akkedli_V2_Enrollment::URL, -strlen($suffix)) !== $suffix) return false; 90 return substr(Akkedli_V2_Enrollment::URL, 0, -strlen('/enroll')) . self::$paths[$operation]; 103 $path = $legacy ? str_replace('/connection-session/', '/legacy-session/', self::$paths[$operation]) : self::$paths[$operation]; 104 return substr(Akkedli_V2_Enrollment::URL, 0, -strlen('/enroll')) . $path; 91 105 } 92 106 } -
akkedli-connector/trunk/languages/akkedli-connector.pot
r3624443 r3777357 5 5 msgid "" 6 6 msgstr "" 7 "Project-Id-Version: Akkedli Connector 1. 1.4\n"7 "Project-Id-Version: Akkedli Connector 1.2.0\n" 8 8 "Report-Msgid-Bugs-To: https://akkedli.com/support\n" 9 9 "POT-Creation-Date: 2026-02-01 23:25+0000\n" -
akkedli-connector/trunk/readme.txt
r3624443 r3777357 5 5 Tested up to: 7.0 6 6 Requires PHP: 7.4 7 Stable tag: 1. 1.47 Stable tag: 1.2.0 8 8 License: GPLv2 or later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 135 135 == Changelog == 136 136 137 = 1.2.0 = 138 * Add explicit Legacy code-free WordPress approval, durable verification, and generation-scoped disconnect/reconnection. 139 * Preserve Legacy callback reliability and Classic/HPOS behavior. 140 * Verify manual connection persistence before reporting success. 141 137 142 = 1.1.4 - 2026-07-26 = 138 143 * Security: Keep callback replay tombstones beyond the complete Backend retry horizon -
akkedli-connector/trunk/templates/admin-settings.php
r3624443 r3777357 9 9 10 10 $akkedli_is_configured = Akkedli_Settings::is_configured(); 11 $akkedli_is_connected = $akkedli_is_configured && Akkedli_Settings::is_validated(); 11 12 $akkedli_v2_status = Akkedli_Connector_State::status(); 12 13 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display-only URL param from our own redirect. … … 22 23 </h1> 23 24 24 <?php if (false !== Akkedli_V2_Session_Admin::view()): ?> 25 <?php if (false !== Akkedli_Legacy_Session::view()): ?> 26 <?php include AKKEDLI_PLUGIN_PATH . 'templates/admin-legacy-session.php'; ?> 27 <?php elseif (false !== Akkedli_V2_Session_Admin::view()): ?> 25 28 <?php include AKKEDLI_PLUGIN_PATH . 'templates/admin-v2-session.php'; ?> 26 29 <?php elseif ('absent' !== $akkedli_v2_status || !$akkedli_is_configured): ?> … … 29 32 30 33 <?php if ('absent' === $akkedli_v2_status): ?> 31 <?php if ($akkedli_just_connected ): ?>34 <?php if ($akkedli_just_connected && $akkedli_is_connected): ?> 32 35 <div class="notice notice-success inline" style="border-left-color: #10b981;"> 33 36 <p> … … 43 46 </p> 44 47 </div> 45 <?php elseif ($akkedli_is_con figured): ?>48 <?php elseif ($akkedli_is_connected): ?> 46 49 <div class="notice notice-success inline"> 47 50 <p> 48 51 <strong><?php esc_html_e('Connected', 'akkedli-connector'); ?></strong> — 49 52 <?php esc_html_e('Orders will be sent to Akkedli automatically.', 'akkedli-connector'); ?> 53 </p> 54 </div> 55 <?php elseif ($akkedli_is_configured): ?> 56 <div class="notice notice-info inline" role="status"> 57 <p> 58 <strong><?php esc_html_e('Verifying connection', 'akkedli-connector'); ?></strong> — 59 <?php esc_html_e('Connection settings are saved. Waiting for Akkedli to confirm the connection.', 'akkedli-connector'); ?> 50 60 </p> 51 61 </div>
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)