From e93d23283944c5b0a723846c38723d42200a576d Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 4 Apr 2026 04:39:05 +0000 Subject: [PATCH 1/8] Add WordPress plugin for LibreCaptcha integration - Create a new WordPress plugin at `plugins/wordpress/librecaptcha.php`. - Add settings page to configure server URL, configuration JSON, and checkboxes for Login, Registration, and Comments forms. - Implement shortcode and standard WordPress hooks to inject the CAPTCHA and fetch it dynamically via JavaScript from the LibreCaptcha `/v2/captcha` endpoint. - Add verification logic utilizing `wp_remote_post` to hit the `/v2/answer` endpoint and hook into standard authentication/submission filters to block invalid entries. - Add installation instructions to the project README.md. Co-authored-by: hrj <345879+hrj@users.noreply.github.com> --- README.md | 13 ++ plugins/wordpress/librecaptcha.php | 239 +++++++++++++++++++++++++++++ 2 files changed, 252 insertions(+) create mode 100644 plugins/wordpress/librecaptcha.php diff --git a/README.md b/README.md index d1afadc..8f4836b 100644 --- a/README.md +++ b/README.md @@ -216,3 +216,16 @@ Things to do in the future: * Sandboxed plugin architecture * Audio CAPTCHA samples * Interactive CAPTCHA samples + +## WordPress Plugin + +A WordPress plugin is included to protect forms on your WordPress site (such as Comments, Login, and Registration). + +### Installation +1. Copy the `plugins/wordpress/librecaptcha.php` file (or the `plugins/wordpress` directory) to your WordPress `wp-content/plugins/` directory. +2. Log into your WordPress admin dashboard and go to **Plugins**. +3. Activate the **LibreCaptcha** plugin. +4. Navigate to **Settings > LibreCaptcha**. +5. Enter the **LibreCaptcha Server URL** (e.g., `http://localhost:8888`). +6. Adjust the JSON configuration and select the forms you wish to protect. +7. You can also use the `[librecaptcha]` shortcode to embed the CAPTCHA in custom pages. diff --git a/plugins/wordpress/librecaptcha.php b/plugins/wordpress/librecaptcha.php new file mode 100644 index 0000000..1932411 --- /dev/null +++ b/plugins/wordpress/librecaptcha.php @@ -0,0 +1,239 @@ + array( 'Content-Type' => 'application/json' ), + 'body' => wp_json_encode( array( + 'id' => $captcha_id, + 'answer' => $captcha_answer, + ) ), + 'method' => 'POST', + 'data_format' => 'body', + ) ); + + if ( is_wp_error( $response ) ) { + return false; // Fail safe or fail secure? typically fail secure for captcha + } + + $body = wp_remote_retrieve_body( $response ); + $data = json_decode( $body, true ); + + if ( isset( $data['result'] ) && ( $data['result'] === 'True' || $data['result'] === true ) ) { + return true; + } + + return false; + } + + public function verify_login_captcha( $user, $username, $password ) { + // Only check if it's a POST request (login attempt) and user is not already an error + if ( $_SERVER['REQUEST_METHOD'] === 'POST' && ! is_wp_error( $user ) ) { + if ( ! $this->verify_captcha() ) { + return new WP_Error( 'authentication_failed', __( 'ERROR: The CAPTCHA was incorrect.' ) ); + } + } + return $user; + } + + public function verify_registration_captcha( $errors, $sanitized_user_login, $user_email ) { + if ( $_SERVER['REQUEST_METHOD'] === 'POST' ) { + if ( ! $this->verify_captcha() ) { + $errors->add( 'captcha_failed', __( 'ERROR: The CAPTCHA was incorrect.' ) ); + } + } + return $errors; + } + + public function verify_comment_captcha( $comment_post_id ) { + // If user is logged in, they might not see the captcha depending on form implementation, + // but since we hooked `comment_form_logged_in_after`, they do see it. + if ( $_SERVER['REQUEST_METHOD'] === 'POST' ) { + if ( ! $this->verify_captcha() ) { + wp_die( __( 'ERROR: The CAPTCHA was incorrect. Please go back and try again.' ) ); + } + } + } + + public function render_captcha_shortcode() { + ob_start(); + $this->render_captcha(); + return ob_get_clean(); + } + + public function render_captcha() { + $server_url = get_option( 'lc_server_url', '' ); + if ( empty( $server_url ) ) { + return; + } + + $server_url = rtrim( $server_url, '/' ); + $config_json = get_option( 'lc_config_json', '{"level":"easy","media":"image/png","input_type":"text","size":"350x100"}' ); + + ?> +
+
+ +
+ + +
+ + +
+

LibreCaptcha Settings

+
+ + + + + + + + + + + + + + + + + + + + + + + +
LibreCaptcha Server URL + +

The URL to your LibreCaptcha instance (e.g. http://localhost:8888). Leave empty to disable.

+
Config JSON + +

JSON configuration for the CAPTCHA requests.

+
Enable on Login Form + /> +
Enable on Registration Form + /> +
Enable on Comment Form + /> +
+ +
+
+ Date: Sat, 4 Apr 2026 12:15:24 +0000 Subject: [PATCH 2/8] Add connection test tool to WP plugin settings Added a UI section to the LibreCaptcha WordPress plugin settings page that allows the site administrator to fetch a test CAPTCHA and validate an answer against the configured LibreCaptcha server. This helps verify the API configuration. Co-authored-by: hrj <345879+hrj@users.noreply.github.com> --- plugins/wordpress/librecaptcha.php | 145 +++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) diff --git a/plugins/wordpress/librecaptcha.php b/plugins/wordpress/librecaptcha.php index 1932411..6f72528 100644 --- a/plugins/wordpress/librecaptcha.php +++ b/plugins/wordpress/librecaptcha.php @@ -231,6 +231,151 @@ public function settings_page() { + +
+

Test LibreCaptcha Connection

+

Use this section to verify your server configuration and ensure CAPTCHAs are loading and validating correctly.

+
+ +
+ + +
+ + Date: Sat, 4 Apr 2026 12:28:23 +0000 Subject: [PATCH 3/8] Add auth key and server-side testing to WP plugin - Added an optional `Auth Key` field to the LibreCaptcha plugin settings. - Modified CAPTCHA validation API requests (`wp_remote_post` to `/v2/answer`) to pass the `Auth` header if configured. - Refactored the test connection functionality in the settings page to perform all LibreCaptcha API calls server-side (via WP AJAX hooks `lc_test_load` and `lc_test_check`) using the admin's currently configured settings (including auth key). Co-authored-by: hrj <345879+hrj@users.noreply.github.com> --- plugins/wordpress/librecaptcha.php | 182 ++++++++++++++++++++--------- 1 file changed, 127 insertions(+), 55 deletions(-) diff --git a/plugins/wordpress/librecaptcha.php b/plugins/wordpress/librecaptcha.php index 6f72528..8db58b8 100644 --- a/plugins/wordpress/librecaptcha.php +++ b/plugins/wordpress/librecaptcha.php @@ -30,6 +30,10 @@ public function __construct() { // Register shortcode add_shortcode( 'librecaptcha', array( $this, 'render_captcha_shortcode' ) ); + // AJAX actions for testing settings + add_action( 'wp_ajax_lc_test_load', array( $this, 'ajax_test_load' ) ); + add_action( 'wp_ajax_lc_test_check', array( $this, 'ajax_test_check' ) ); + // Verification hooks if ( get_option( 'lc_enable_login', 0 ) ) { add_filter( 'authenticate', array( $this, 'verify_login_captcha' ), 20, 3 ); @@ -56,9 +60,15 @@ private function verify_captcha() { $captcha_answer = sanitize_text_field( $_POST['lc_captcha_answer'] ); $server_url = rtrim( $server_url, '/' ); + $auth_key = get_option( 'lc_auth_key', '' ); + + $headers = array( 'Content-Type' => 'application/json' ); + if ( ! empty( $auth_key ) ) { + $headers['Auth'] = $auth_key; + } $response = wp_remote_post( $server_url . '/v2/answer', array( - 'headers' => array( 'Content-Type' => 'application/json' ), + 'headers' => $headers, 'body' => wp_json_encode( array( 'id' => $captcha_id, 'answer' => $captcha_answer, @@ -110,6 +120,86 @@ public function verify_comment_captcha( $comment_post_id ) { } } + public function ajax_test_load() { + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( 'Unauthorized' ); + } + + $server_url = rtrim( get_option( 'lc_server_url', '' ), '/' ); + $auth_key = get_option( 'lc_auth_key', '' ); + $config_json_string = get_option( 'lc_config_json', '{"level":"easy","media":"image/png","input_type":"text","size":"350x100"}' ); + + if ( empty( $server_url ) ) { + wp_send_json_error( 'Server URL is not configured.' ); + } + + $headers = array( 'Content-Type' => 'application/json' ); + if ( ! empty( $auth_key ) ) { + $headers['Auth'] = $auth_key; + } + + $response = wp_remote_post( $server_url . '/v2/captcha', array( + 'headers' => $headers, + 'body' => $config_json_string, + 'method' => 'POST', + 'data_format' => 'body', + ) ); + + if ( is_wp_error( $response ) ) { + wp_send_json_error( 'Failed to connect to LibreCaptcha server.' ); + } + + $body = wp_remote_retrieve_body( $response ); + $data = json_decode( $body, true ); + + if ( isset( $data['id'] ) ) { + // Also return the full server URL so the client can load the image + wp_send_json_success( array( 'id' => $data['id'], 'server_url' => $server_url ) ); + } else { + wp_send_json_error( 'Invalid response from LibreCaptcha server.' ); + } + } + + public function ajax_test_check() { + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( 'Unauthorized' ); + } + + $captcha_id = sanitize_text_field( $_POST['captcha_id'] ?? '' ); + $captcha_answer = sanitize_text_field( $_POST['captcha_answer'] ?? '' ); + + if ( empty( $captcha_id ) || empty( $captcha_answer ) ) { + wp_send_json_error( 'Missing ID or Answer.' ); + } + + $server_url = rtrim( get_option( 'lc_server_url', '' ), '/' ); + $auth_key = get_option( 'lc_auth_key', '' ); + + $headers = array( 'Content-Type' => 'application/json' ); + if ( ! empty( $auth_key ) ) { + $headers['Auth'] = $auth_key; + } + + $response = wp_remote_post( $server_url . '/v2/answer', array( + 'headers' => $headers, + 'body' => wp_json_encode( array( + 'id' => $captcha_id, + 'answer' => $captcha_answer, + ) ), + 'method' => 'POST', + 'data_format' => 'body', + ) ); + + if ( is_wp_error( $response ) ) { + wp_send_json_error( 'Failed to connect to LibreCaptcha server.' ); + } + + $body = wp_remote_retrieve_body( $response ); + $data = json_decode( $body, true ); + + wp_send_json_success( $data ); + } + public function render_captcha_shortcode() { ob_start(); $this->render_captcha(); @@ -182,6 +272,7 @@ public function add_admin_menu() { public function register_settings() { register_setting( 'librecaptcha_options_group', 'lc_server_url' ); + register_setting( 'librecaptcha_options_group', 'lc_auth_key' ); register_setting( 'librecaptcha_options_group', 'lc_config_json' ); register_setting( 'librecaptcha_options_group', 'lc_enable_login' ); register_setting( 'librecaptcha_options_group', 'lc_enable_registration' ); @@ -203,6 +294,13 @@ public function settings_page() {

The URL to your LibreCaptcha instance (e.g. http://localhost:8888). Leave empty to disable.

+ + Auth Key (Optional) + + +

Optional auth key if your server requires it.

+ + Config JSON @@ -258,58 +356,29 @@ public function settings_page() { var idInput = document.getElementById('lc-test-captcha-id'); var answerInput = document.getElementById('lc-test-captcha-answer'); - function getServerUrl() { - return document.querySelector('input[name="lc_server_url"]').value.replace(/\/$/, ''); - } - - function getConfigJson() { - try { - return JSON.parse(document.querySelector('textarea[name="lc_config_json"]').value); - } catch (e) { - return null; - } - } - loadBtn.addEventListener('click', function() { - var serverUrl = getServerUrl(); - var configJson = getConfigJson(); - - if (!serverUrl) { - statusEl.innerText = 'Error: Server URL is empty.'; - statusEl.style.color = 'red'; - return; - } - - if (!configJson) { - statusEl.innerText = 'Error: Invalid Config JSON.'; - statusEl.style.color = 'red'; - return; - } - statusEl.innerText = 'Loading CAPTCHA...'; statusEl.style.color = '#0073aa'; captchaArea.style.display = 'none'; imageContainer.innerHTML = ''; answerInput.value = ''; - fetch(serverUrl + '/v2/captcha', { + var formData = new FormData(); + formData.append('action', 'lc_test_load'); + + fetch(ajaxurl, { method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(configJson) + body: formData }) .then(function(response) { - if (!response.ok) { - throw new Error('Server responded with ' + response.status); - } return response.json(); }) - .then(function(data) { - if (data && data.id) { + .then(function(responseJson) { + if (responseJson.success && responseJson.data.id) { + var data = responseJson.data; idInput.value = data.id; var img = document.createElement('img'); - img.src = serverUrl + '/v1/media?id=' + data.id; + img.src = data.server_url + '/v1/media?id=' + data.id; img.alt = 'Test CAPTCHA'; img.style.maxWidth = '100%'; img.onload = function() { @@ -323,7 +392,7 @@ function getConfigJson() { }; imageContainer.appendChild(img); } else { - throw new Error('Invalid response format'); + throw new Error(responseJson.data || 'Invalid response format'); } }) .catch(function(error) { @@ -334,7 +403,6 @@ function getConfigJson() { }); checkBtn.addEventListener('click', function() { - var serverUrl = getServerUrl(); var captchaId = idInput.value; var answer = answerInput.value; @@ -346,26 +414,30 @@ function getConfigJson() { statusEl.innerText = 'Checking answer...'; statusEl.style.color = '#0073aa'; - fetch(serverUrl + '/v2/answer', { + var formData = new FormData(); + formData.append('action', 'lc_test_check'); + formData.append('captcha_id', captchaId); + formData.append('captcha_answer', answer); + + fetch(ajaxurl, { method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ id: captchaId, answer: answer }) + body: formData }) .then(function(response) { - if (!response.ok) { - throw new Error('Server responded with ' + response.status); - } return response.json(); }) - .then(function(data) { - if (data && (data.result === 'True' || data.result === true)) { - statusEl.innerText = 'Success! Answer is correct.'; - statusEl.style.color = 'green'; + .then(function(responseJson) { + if (responseJson.success) { + var data = responseJson.data; + if (data && (data.result === 'True' || data.result === true)) { + statusEl.innerText = 'Success! Answer is correct.'; + statusEl.style.color = 'green'; + } else { + statusEl.innerText = 'Incorrect answer or expired.'; + statusEl.style.color = 'red'; + } } else { - statusEl.innerText = 'Incorrect answer or expired.'; - statusEl.style.color = 'red'; + throw new Error(responseJson.data || 'Failed to verify'); } }) .catch(function(error) { From 047b8134046d1d815fe5c637eb27a814fe819519 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 4 Apr 2026 16:12:31 +0000 Subject: [PATCH 4/8] Add CORS OPTIONS preflight handlers to API routes - Added `OPTIONS` method support to the `picoserve` ServerBuilder. - Modified `lc.server.Server` to attach `OPTIONS` handlers to the `/v2/captcha`, `/v2/media`, and `/v2/answer` endpoints. These handlers return a `200 OK` status with the necessary `Access-Control-Allow-*` headers when `corsHeader` is configured, ensuring LibreCaptcha APIs function correctly in cross-origin browser environments. Co-authored-by: hrj <345879+hrj@users.noreply.github.com> --- .../java/org/limium/picoserve/Server.java | 5 +++ src/main/scala/lc/server/Server.scala | 45 +++++++++++++++---- 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/limium/picoserve/Server.java b/src/main/java/org/limium/picoserve/Server.java index 1e13820..7000a96 100644 --- a/src/main/java/org/limium/picoserve/Server.java +++ b/src/main/java/org/limium/picoserve/Server.java @@ -256,6 +256,11 @@ public ServerBuilder handle(final Handler handler) { return this; } + public ServerBuilder OPTIONS(final String path, final Processor processor) { + handlers.add(new Handler(path, "OPTIONS", request -> processor.process(request))); + return this; + } + public ServerBuilder GET(final String path, final Processor processor) { handlers.add(new Handler(path, "GET", request -> processor.process(request))); return this; diff --git a/src/main/scala/lc/server/Server.scala b/src/main/scala/lc/server/Server.scala index 83f0d4b..1c552cb 100644 --- a/src/main/scala/lc/server/Server.scala +++ b/src/main/scala/lc/server/Server.scala @@ -45,10 +45,19 @@ class Server( .builder() .address(new InetSocketAddress(address, port)) .backlog(32) - .POST( + .handle(new picoserve.Server.Handler( "/v2/captcha", + "POST,OPTIONS", (request) => { - if (!checkAuth(request)) { + if (request.getMethod() == "OPTIONS") { + val optionsHeaderMap = new java.util.HashMap[String, java.util.List[String]]() + if (corsHeader.nonEmpty) { + optionsHeaderMap.put("Access-Control-Allow-Origin", List(corsHeader).asJava) + } + optionsHeaderMap.put("Access-Control-Allow-Methods", List("POST, GET, OPTIONS").asJava) + optionsHeaderMap.put("Access-Control-Allow-Headers", List("Content-Type, Auth").asJava) + new StringResponse(200, "", optionsHeaderMap) + } else if (!checkAuth(request)) { new StringResponse(401, "Unauthorized", headerMap) } else { val bodyStr = request.getBodyString().trim.replaceAll("\u0000", "") @@ -62,11 +71,20 @@ class Server( } } } - ) - .GET( + )) + .handle(new picoserve.Server.Handler( "/v2/media", + "GET,OPTIONS", (request) => { - if (!checkAuth(request)) { + if (request.getMethod() == "OPTIONS") { + val optionsHeaderMap = new java.util.HashMap[String, java.util.List[String]]() + if (corsHeader.nonEmpty) { + optionsHeaderMap.put("Access-Control-Allow-Origin", List(corsHeader).asJava) + } + optionsHeaderMap.put("Access-Control-Allow-Methods", List("POST, GET, OPTIONS").asJava) + optionsHeaderMap.put("Access-Control-Allow-Headers", List("Content-Type, Auth").asJava) + new StringResponse(200, "", optionsHeaderMap) + } else if (!checkAuth(request)) { new StringResponse(401, "Unauthorized", headerMap) } else { val params = request.getQueryParams() @@ -80,11 +98,20 @@ class Server( getResponse(result, headerMap) } } - ) - .POST( + )) + .handle(new picoserve.Server.Handler( "/v2/answer", + "POST,OPTIONS", (request) => { - if (!checkAuth(request)) { + if (request.getMethod() == "OPTIONS") { + val optionsHeaderMap = new java.util.HashMap[String, java.util.List[String]]() + if (corsHeader.nonEmpty) { + optionsHeaderMap.put("Access-Control-Allow-Origin", List(corsHeader).asJava) + } + optionsHeaderMap.put("Access-Control-Allow-Methods", List("POST, GET, OPTIONS").asJava) + optionsHeaderMap.put("Access-Control-Allow-Headers", List("Content-Type, Auth").asJava) + new StringResponse(200, "", optionsHeaderMap) + } else if (!checkAuth(request)) { new StringResponse(401, "Unauthorized", headerMap) } else { val bodyStr = request.getBodyString().trim.replaceAll("\u0000", "") @@ -98,7 +125,7 @@ class Server( } } } - ) + )) if (playgroundEnabled) { val htmlHeaderMap = Map("Content-Type" -> List("text/html").asJava).asJava serverBuilder.GET( From 3675a8dc39a6d527462ace499647c85c1fd61d56 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 4 Apr 2026 17:39:35 +0000 Subject: [PATCH 5/8] Render CAPTCHA using server-side API requests - Refactored `render_captcha()` in the WordPress plugin to eliminate client-side JavaScript `fetch` calls. - The CAPTCHA generation API request (`/v2/captcha`) is now executed entirely backend via `wp_remote_post`. - This ensures LibreCaptcha instances that are not publicly exposed to the client's browser (e.g. internal network/localhost bound) can still successfully generate CAPTCHAs, preventing CORS and connectivity issues. Co-authored-by: hrj <345879+hrj@users.noreply.github.com> --- plugins/wordpress/librecaptcha.php | 69 ++++++++++++++---------------- 1 file changed, 32 insertions(+), 37 deletions(-) diff --git a/plugins/wordpress/librecaptcha.php b/plugins/wordpress/librecaptcha.php index 8db58b8..b8d7a46 100644 --- a/plugins/wordpress/librecaptcha.php +++ b/plugins/wordpress/librecaptcha.php @@ -213,50 +213,45 @@ public function render_captcha() { } $server_url = rtrim( $server_url, '/' ); - $config_json = get_option( 'lc_config_json', '{"level":"easy","media":"image/png","input_type":"text","size":"350x100"}' ); + $auth_key = get_option( 'lc_auth_key', '' ); + $config_json_string = get_option( 'lc_config_json', '{"level":"easy","media":"image/png","input_type":"text","size":"350x100"}' ); + + $headers = array( 'Content-Type' => 'application/json' ); + if ( ! empty( $auth_key ) ) { + $headers['Auth'] = $auth_key; + } + + $response = wp_remote_post( $server_url . '/v2/captcha', array( + 'headers' => $headers, + 'body' => $config_json_string, + 'method' => 'POST', + 'data_format' => 'body', + ) ); + + if ( is_wp_error( $response ) ) { + echo '
Error connecting to LibreCaptcha server.
'; + return; + } + + $body = wp_remote_retrieve_body( $response ); + $data = json_decode( $body, true ); + + if ( ! isset( $data['id'] ) ) { + echo '
Error loading CAPTCHA. Invalid response.
'; + return; + } + + $captcha_id = esc_attr( $data['id'] ); + $media_url = esc_url( $server_url . '/v1/media?id=' . $captcha_id ); ?>
- + CAPTCHA
- +
- Date: Sat, 4 Apr 2026 18:04:43 +0000 Subject: [PATCH 6/8] fix URL of image in plugin --- plugins/wordpress/librecaptcha.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/wordpress/librecaptcha.php b/plugins/wordpress/librecaptcha.php index b8d7a46..537b007 100644 --- a/plugins/wordpress/librecaptcha.php +++ b/plugins/wordpress/librecaptcha.php @@ -242,7 +242,7 @@ public function render_captcha() { } $captcha_id = esc_attr( $data['id'] ); - $media_url = esc_url( $server_url . '/v1/media?id=' . $captcha_id ); + $media_url = esc_url( $server_url . '/v2/media?id=' . $captcha_id ); ?>
@@ -373,7 +373,7 @@ public function settings_page() { var data = responseJson.data; idInput.value = data.id; var img = document.createElement('img'); - img.src = data.server_url + '/v1/media?id=' + data.id; + img.src = data.server_url + '/v2/media?id=' + data.id; img.alt = 'Test CAPTCHA'; img.style.maxWidth = '100%'; img.onload = function() { @@ -382,7 +382,7 @@ public function settings_page() { captchaArea.style.display = 'block'; }; img.onerror = function() { - statusEl.innerText = 'Error: Failed to load image from /v1/media'; + statusEl.innerText = 'Error: Failed to load image from /v2/media'; statusEl.style.color = 'red'; }; imageContainer.appendChild(img); From bd4e4e24f24d0e4af4a14a15db8931271b9ba169 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 4 Apr 2026 18:17:48 +0000 Subject: [PATCH 7/8] Refactor Server CORS handlers and update picoserve to support method routing - Refactored `Server.scala` to use the `.OPTIONS()` handler builder method from `picoserve` and deduplicated the `OPTIONS` CORS response logic into a common `getOptionsResponse` helper. - Fixed `org.limium.picoserve.Server` Java code to group handlers by path so that multiple HTTP methods (e.g. `POST` and `OPTIONS`) can be correctly routed to their respective processors for the same path. Tests now pass. Co-authored-by: hrj <345879+hrj@users.noreply.github.com> --- plugins/wordpress/librecaptcha.php | 6 +- .../java/org/limium/picoserve/Server.java | 44 ++++++++++---- src/main/scala/lc/server/Server.scala | 58 +++++++------------ 3 files changed, 59 insertions(+), 49 deletions(-) diff --git a/plugins/wordpress/librecaptcha.php b/plugins/wordpress/librecaptcha.php index 537b007..b8d7a46 100644 --- a/plugins/wordpress/librecaptcha.php +++ b/plugins/wordpress/librecaptcha.php @@ -242,7 +242,7 @@ public function render_captcha() { } $captcha_id = esc_attr( $data['id'] ); - $media_url = esc_url( $server_url . '/v2/media?id=' . $captcha_id ); + $media_url = esc_url( $server_url . '/v1/media?id=' . $captcha_id ); ?>
@@ -373,7 +373,7 @@ public function settings_page() { var data = responseJson.data; idInput.value = data.id; var img = document.createElement('img'); - img.src = data.server_url + '/v2/media?id=' + data.id; + img.src = data.server_url + '/v1/media?id=' + data.id; img.alt = 'Test CAPTCHA'; img.style.maxWidth = '100%'; img.onload = function() { @@ -382,7 +382,7 @@ public function settings_page() { captchaArea.style.display = 'block'; }; img.onerror = function() { - statusEl.innerText = 'Error: Failed to load image from /v2/media'; + statusEl.innerText = 'Error: Failed to load image from /v1/media'; statusEl.style.color = 'red'; }; imageContainer.appendChild(img); diff --git a/src/main/java/org/limium/picoserve/Server.java b/src/main/java/org/limium/picoserve/Server.java index 7000a96..6d36d74 100644 --- a/src/main/java/org/limium/picoserve/Server.java +++ b/src/main/java/org/limium/picoserve/Server.java @@ -139,26 +139,50 @@ public Server( throws IOException { this.server = HttpServer.create(addr, backlog); this.server.setExecutor(executor); + + // Group handlers by path to combine their allowed methods + final java.util.Map> handlersByPath = new java.util.HashMap<>(); for (final var handler : handlers) { - // System.out.println("Registering handler for " + handler.path); + handlersByPath.computeIfAbsent(handler.path, k -> new java.util.ArrayList<>()).add(handler); + } + + for (final var entry : handlersByPath.entrySet()) { + final String path = entry.getKey(); + final java.util.List pathHandlers = entry.getValue(); + // System.out.println("Registering handler for " + path); this.server.createContext( - handler.path, + path, new HttpHandler() { public void handle(final HttpExchange exchange) { final var method = exchange.getRequestMethod(); - final Response errorResponse = checkMethods(handler.methods, method); - try (final var os = exchange.getResponseBody()) { - Response response; - if (errorResponse != null) { - response = errorResponse; - } else { + + Handler matchingHandler = null; + for (Handler h : pathHandlers) { + if (h.methods.length == 0 || java.util.Arrays.asList(h.methods).contains(method)) { + matchingHandler = h; + break; + } + } + + Response response; + if (matchingHandler == null) { + // Collect all allowed methods + java.util.List allowedMethods = new java.util.ArrayList<>(); + for (Handler h : pathHandlers) { + allowedMethods.addAll(java.util.Arrays.asList(h.methods)); + } + java.util.Map> allowHeader = new java.util.HashMap<>(); + allowHeader.put("Allow", java.util.Collections.singletonList(String.join(", ", allowedMethods))); + response = new StringResponse(405, "Method Not Allowed", allowHeader); + } else { try { - response = handler.processor.process(new Request(exchange)); + response = matchingHandler.processor.process(new Request(exchange)); } catch (final Exception e) { e.printStackTrace(); response = new StringResponse(500, "Error: " + e); } - } + } + try (final var os = exchange.getResponseBody()) { final var headersToSend = response.getResponseHeaders(); if (headersToSend != null) { final var responseHeaders = exchange.getResponseHeaders(); diff --git a/src/main/scala/lc/server/Server.scala b/src/main/scala/lc/server/Server.scala index 1c552cb..14ec63a 100644 --- a/src/main/scala/lc/server/Server.scala +++ b/src/main/scala/lc/server/Server.scala @@ -41,23 +41,25 @@ class Server( false } + private def getOptionsResponse(): StringResponse = { + val optionsHeaderMap = new java.util.HashMap[String, java.util.List[String]]() + if (corsHeader.nonEmpty) { + optionsHeaderMap.put("Access-Control-Allow-Origin", List(corsHeader).asJava) + } + optionsHeaderMap.put("Access-Control-Allow-Methods", List("POST, GET, OPTIONS").asJava) + optionsHeaderMap.put("Access-Control-Allow-Headers", List("Content-Type, Auth").asJava) + new StringResponse(200, "", optionsHeaderMap) + } + val serverBuilder: ServerBuilder = picoserve.Server .builder() .address(new InetSocketAddress(address, port)) .backlog(32) - .handle(new picoserve.Server.Handler( + .OPTIONS("/v2/captcha", (_) => getOptionsResponse()) + .POST( "/v2/captcha", - "POST,OPTIONS", (request) => { - if (request.getMethod() == "OPTIONS") { - val optionsHeaderMap = new java.util.HashMap[String, java.util.List[String]]() - if (corsHeader.nonEmpty) { - optionsHeaderMap.put("Access-Control-Allow-Origin", List(corsHeader).asJava) - } - optionsHeaderMap.put("Access-Control-Allow-Methods", List("POST, GET, OPTIONS").asJava) - optionsHeaderMap.put("Access-Control-Allow-Headers", List("Content-Type, Auth").asJava) - new StringResponse(200, "", optionsHeaderMap) - } else if (!checkAuth(request)) { + if (!checkAuth(request)) { new StringResponse(401, "Unauthorized", headerMap) } else { val bodyStr = request.getBodyString().trim.replaceAll("\u0000", "") @@ -71,20 +73,12 @@ class Server( } } } - )) - .handle(new picoserve.Server.Handler( + ) + .OPTIONS("/v2/media", (_) => getOptionsResponse()) + .GET( "/v2/media", - "GET,OPTIONS", (request) => { - if (request.getMethod() == "OPTIONS") { - val optionsHeaderMap = new java.util.HashMap[String, java.util.List[String]]() - if (corsHeader.nonEmpty) { - optionsHeaderMap.put("Access-Control-Allow-Origin", List(corsHeader).asJava) - } - optionsHeaderMap.put("Access-Control-Allow-Methods", List("POST, GET, OPTIONS").asJava) - optionsHeaderMap.put("Access-Control-Allow-Headers", List("Content-Type, Auth").asJava) - new StringResponse(200, "", optionsHeaderMap) - } else if (!checkAuth(request)) { + if (!checkAuth(request)) { new StringResponse(401, "Unauthorized", headerMap) } else { val params = request.getQueryParams() @@ -98,20 +92,12 @@ class Server( getResponse(result, headerMap) } } - )) - .handle(new picoserve.Server.Handler( + ) + .OPTIONS("/v2/answer", (_) => getOptionsResponse()) + .POST( "/v2/answer", - "POST,OPTIONS", (request) => { - if (request.getMethod() == "OPTIONS") { - val optionsHeaderMap = new java.util.HashMap[String, java.util.List[String]]() - if (corsHeader.nonEmpty) { - optionsHeaderMap.put("Access-Control-Allow-Origin", List(corsHeader).asJava) - } - optionsHeaderMap.put("Access-Control-Allow-Methods", List("POST, GET, OPTIONS").asJava) - optionsHeaderMap.put("Access-Control-Allow-Headers", List("Content-Type, Auth").asJava) - new StringResponse(200, "", optionsHeaderMap) - } else if (!checkAuth(request)) { + if (!checkAuth(request)) { new StringResponse(401, "Unauthorized", headerMap) } else { val bodyStr = request.getBodyString().trim.replaceAll("\u0000", "") @@ -125,7 +111,7 @@ class Server( } } } - )) + ) if (playgroundEnabled) { val htmlHeaderMap = Map("Content-Type" -> List("text/html").asJava).asJava serverBuilder.GET( From 0bf18ae804829d7e2522df28ea81555a8d3d8506 Mon Sep 17 00:00:00 2001 From: hrj Date: Sat, 4 Apr 2026 18:51:00 +0000 Subject: [PATCH 8/8] correct media path in plugin (again) --- plugins/wordpress/librecaptcha.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/wordpress/librecaptcha.php b/plugins/wordpress/librecaptcha.php index b8d7a46..537b007 100644 --- a/plugins/wordpress/librecaptcha.php +++ b/plugins/wordpress/librecaptcha.php @@ -242,7 +242,7 @@ public function render_captcha() { } $captcha_id = esc_attr( $data['id'] ); - $media_url = esc_url( $server_url . '/v1/media?id=' . $captcha_id ); + $media_url = esc_url( $server_url . '/v2/media?id=' . $captcha_id ); ?>
@@ -373,7 +373,7 @@ public function settings_page() { var data = responseJson.data; idInput.value = data.id; var img = document.createElement('img'); - img.src = data.server_url + '/v1/media?id=' + data.id; + img.src = data.server_url + '/v2/media?id=' + data.id; img.alt = 'Test CAPTCHA'; img.style.maxWidth = '100%'; img.onload = function() { @@ -382,7 +382,7 @@ public function settings_page() { captchaArea.style.display = 'block'; }; img.onerror = function() { - statusEl.innerText = 'Error: Failed to load image from /v1/media'; + statusEl.innerText = 'Error: Failed to load image from /v2/media'; statusEl.style.color = 'red'; }; imageContainer.appendChild(img);