'POST', 'permission_callback' => [$this, 'authorize'], 'callback' => [$this, 'receive'], ]); } public function authorize(WP_REST_Request $request): bool|WP_Error { $body = $request->get_body(); if (strlen($body) > 65536) { return new WP_Error('kommentti_body_size', __('The notification is too large.', 'kommentti'), ['status' => 413]); } $secret = $this->settings->credential('webhook_secret'); if (!$this->settings->connected() || strlen($secret) < 40 || !preg_match('/^t=([0-9]{10}),v1=([a-f0-9]{64})$/D', (string) $request->get_header('x-kommentti-signature'), $parts) || abs(time() - (int) $parts[1]) > 300 || !hash_equals(hash_hmac('sha256', $parts[1].'.'.$body, $secret), $parts[2])) { return new WP_Error('kommentti_signature', __('A valid notification signature is required.', 'kommentti'), ['status' => 401]); } $event = json_decode($body, true, 16); if (!is_array($event) || ($event['site'] ?? null) !== $this->settings->all()['site_id'] || !Settings::uuid($event['id'] ?? null) || !in_array($event['type'] ?? null, ['thread.updated', 'endpoint.test'], true) || (isset($event['thread']) && (!is_string($event['thread']) || strlen($event['thread']) > 800))) { return new WP_Error('kommentti_event', __('The notification does not match this site.', 'kommentti'), ['status' => 401]); } return true; } public function receive(WP_REST_Request $request): WP_REST_Response { $event = json_decode($request->get_body(), true); $seenKey = 'kommentti_event_'.hash('sha256', $event['id']); if (!get_transient($seenKey) && $event['type'] === 'thread.updated') { if (!isset($event['thread'])) { update_option('kommentti_cache_generation', wp_generate_uuid4(), false); do_action('kommentti_purge_site'); // A site-wide setting affects all cached articles, not only the plugin object cache. if (function_exists('rocket_clean_domain')) { rocket_clean_domain(); } if (function_exists('wp_cache_clear_cache')) { wp_cache_clear_cache(get_current_blog_id()); } if (function_exists('w3tc_flush_all')) { w3tc_flush_all(); } // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- LiteSpeed’s documented public API. do_action('litespeed_purge_all'); } else { foreach ($this->posts_for_thread($event['thread']) as $postId) { $this->invalidate_post($postId); } } delete_transient('kommentti_api_backoff'); } set_transient($seenKey, true, 600); update_option('kommentti_webhook_status', ['code' => 204, 'type' => $event['type'], 'delivered_at' => time()], false); $response = new WP_REST_Response(null, 204); $response->header('Cache-Control', 'no-store'); return $response; } private function posts_for_thread(string $thread): array { $posts = get_posts(['post_type' => $this->settings->all()['post_types'], 'post_status' => 'publish', 'fields' => 'ids', 'posts_per_page' => 100, 'meta_key' => '_kommentti_thread_id', 'meta_value' => $thread, 'no_found_rows' => true]); if (ctype_digit($thread) && strlen($thread) < 18) { $post = get_post((int) $thread); if ($post && (string) $post->ID === $thread && !get_post_meta($post->ID, '_kommentti_thread_id', true)) { $posts[] = $post->ID; } } return array_values(array_unique(array_map('intval', $posts))); } private function invalidate_post(int $postId): void { update_post_meta($postId, '_kommentti_cache_generation', wp_generate_uuid4()); clean_post_cache($postId); if (function_exists('rocket_clean_post')) { rocket_clean_post($postId); } if (function_exists('wp_cache_post_change')) { wp_cache_post_change($postId); } if (function_exists('w3tc_flush_post')) { w3tc_flush_post($postId); } // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- LiteSpeed’s documented public API. do_action('litespeed_purge_post', $postId); do_action('kommentti_purge_post', $postId); } }