Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions data-for-tests/mu-plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,35 @@

define( 'PROSOPO_PROCAPTCHA_ALLOW_BYPASS', true );

/**
* Reserved Prosopo test site key, see https://docs.prosopo.io/en/basics/test-keys/
*
* The reserved keys are constant across all the Prosopo environments, require no registration,
* and their requests aren't billed, so the test suite doesn't depend on any personal account.
*
* The always-fail key is the one we need: the suite submits its own tokens
* ('bypass' and 'wrong', see the ALLOW_BYPASS constant above), so the widget must not
* emit a token of its own and overwrite them. The always-pass key would do exactly that.
*
* The secret is ignored for the reserved keys, but it must be non-empty
* for the plugin to consider itself configured.
*/
define( 'PROSOPO_PROCAPTCHA_TEST_SITE_KEY', '5ETtechmZkn3CUVeJX7Z511oiuiu742aHLm91D5ZZw4fqoAG' );
define( 'PROSOPO_PROCAPTCHA_TEST_SECRET_KEY', 'test-secret-key-is-ignored-for-reserved-site-keys' );

add_filter(
'option_prosopo-procaptcha__settings',
function ( $settings ) {
$settings = is_array( $settings ) ? $settings : array();

$settings['site_key'] = PROSOPO_PROCAPTCHA_TEST_SITE_KEY;
$settings['secret_key'] = PROSOPO_PROCAPTCHA_TEST_SECRET_KEY;

return $settings;
},
999
);

add_filter( 'comment_flood_filter', '__return_false', 999 );
add_filter( 'pre_wp_mail', '__return_true' );
add_filter( 'bbp_bypass_check_for_flood', '__return_true' );
Expand Down
93 changes: 93 additions & 0 deletions php-tools/code-quality/stubs/Jet_Form_Builder_Captcha.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php

namespace JFB_Modules\Captcha {

class Module {

const SPAM_EXCEPTION = 'captcha_failed';
}
}

namespace JFB_Modules\Captcha\Abstract_Captcha {

abstract class Base_Captcha {

const FIELD = '_captcha_token';
const FIELD_CLASS = 'captcha-token';

/**
* @var array<string,mixed>
*/
protected $options;

abstract public function get_id(): string;

abstract public function get_title(): string;

/**
* @param array<string,mixed> $request
*
* @return void
*/
abstract public function verify( array $request );

abstract protected function render(): string;

public function get_output(): string {
return $this->render();
}

/**
* @param array<string,mixed> $options
*/
public function sanitize_options( array $options ): Base_Captcha {
return $this;
}

/**
* @return string
*/
public function rep_item_id() {
return $this->get_id();
}

/**
* @return array<string,string>
*/
public function to_array(): array {
return array(
'label' => $this->get_title(),
'value' => $this->get_id(),
);
}
}

interface Captcha_Settings_From_Options {

/**
* @param array<string,mixed> $post_request
*
* @return array<string,mixed>
*/
public function on_save_options( array $post_request ): array;

/**
* @return array<string,mixed>
*/
public function on_load_options(): array;
}
}

namespace JFB_Modules\Security\Exceptions {

class Spam_Exception extends \Exception {

/**
* @param string $message
* @param mixed ...$additional_data
*/
public function __construct( $message = '', ...$additional_data ) {
parent::__construct( (string) $message );
}
}
}
2 changes: 1 addition & 1 deletion prosopo-procaptcha/prosopo-procaptcha.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
/**
* Plugin Name: Prosopo Procaptcha
* Description: GDPR compliant, privacy friendly and better value captcha.
* Version: 1.20.6
* Version: 1.21.0
* Author: Prosopo Team
* Author URI: https://prosopo.io/
* License: GPLv2 or later
Expand Down
25 changes: 21 additions & 4 deletions prosopo-procaptcha/readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Tags: Captcha, Procaptcha, antispam, anibot, spam.
Requires at least: 5.5
Tested up to: 6.9
Requires PHP: 7.4
Stable tag: 1.20.6
Stable tag: 1.21.0
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Expand Down Expand Up @@ -46,8 +46,9 @@ Please review the [Prosopo Privacy Policy](https://prosopo.io/privacy-policy/) a
4. [Formidable Forms](https://docs.prosopo.io/en/wordpress-plugin/formidable/) - Add the `Prosopo Procaptcha` field to your form.
5. [Gravity Forms](https://docs.prosopo.io/en/wordpress-plugin/gravity-forms/) - Add the `Prosopo Procaptcha` field to your form (the `Advanced Fields` group).
6. [Ninja Forms](https://docs.prosopo.io/en/wordpress-plugin/ninja-forms/) - Add the `Prosopo Procaptcha` field to your form (the `Miscellaneous` group).
7. [User Registration](https://docs.prosopo.io/en/wordpress-plugin/user-registration/) - Add the `Prosopo Procaptcha` field to your form (the `Extra Fields` group).
8. [WP Forms](https://docs.prosopo.io/en/wordpress-plugin/wpforms/) - Add the `Prosopo Procaptcha` field to your form (the `Standard Fields` group).
7. [JetFormBuilder](https://jetformbuilder.com/) - Open the form settings and pick `Prosopo Procaptcha` as the `Captcha Provider`.
8. [User Registration](https://docs.prosopo.io/en/wordpress-plugin/user-registration/) - Add the `Prosopo Procaptcha` field to your form (the `Extra Fields` group).
9. [WP Forms](https://docs.prosopo.io/en/wordpress-plugin/wpforms/) - Add the `Prosopo Procaptcha` field to your form (the `Standard Fields` group).

**WordPress Core Forms**:

Expand Down Expand Up @@ -87,8 +88,13 @@ Account-related:
1. [Simple Membership](https://wordpress.org/plugins/simple-membership/)
2. [User Registration](https://wordpress.org/plugins/user-registration/)
3. [Memberpress](https://memberpress.com/) - Login, Reset password: via Procaptcha plugin settings -> Core forms. Membership register - via on-membership settings
4. [LearnDash LMS](https://www.learndash.com/) - Registration form: enabled automatically. Login, Reset password: via Procaptcha plugin settings -> Core forms

While only the items above are tested, overall, the Procaptcha plugin supports all the plugins with custom account forms that use the native WordPress account hooks.
Themes:
1. [Blocksy](https://creativethemes.com/blocksy/) - Account modal (Login, Register, Lost password): via Procaptcha plugin settings -> Core forms
2. [Astra](https://wpastra.com/) and [Zakra](https://zakratheme.com/) - Account forms: via Procaptcha plugin settings -> Core forms

While only the items above are tested, overall, the Procaptcha plugin supports all the plugins and themes with custom account forms that use the native WordPress account hooks.

**Other Integrations**:

Expand All @@ -98,6 +104,9 @@ While only the items above are tested, overall, the Procaptcha plugin supports a
4. [WooCommerce](https://docs.prosopo.io/en/wordpress-plugin/woocommerce/) - My Account forms; Classic Checkout, Blocks Checkout, Order Tracking forms: enable protection in the `WooCommerce` tab of the plugin settings.
5. [Spectra](https://docs.prosopo.io/en/wordpress-plugin/spectra/) - Form block: add hidden input with the `prosopo_procaptcha` name
6. [Beaver Builder](https://wordpress.org/plugins/beaver-builder-lite-version/) - Contact, Subscribe forms: enabled protection in the target form settings; Login form: Procaptcha plugin settings->Core forms.
7. [Divi](https://www.elegantthemes.com/gallery/divi/) - Contact Form module: enable the built-in `Use Basic Captcha` option of the target module, and Procaptcha replaces the Divi arithmetic captcha; Login module: Procaptcha plugin settings->Core forms.
8. [Bricks](https://bricksbuilder.io/) (2.0+) - Form element: add a `Hidden` field with the `prosopo_procaptcha` label.
9. [Blocksy](https://creativethemes.com/blocksy/) - Newsletter subscribe block and shortcode: enabled automatically.

**Built-In Integrations**:

Expand Down Expand Up @@ -135,6 +144,14 @@ Absolutely! The plugin has a [public GitHub repository](https://github.com/proso

== Changelog ==

= 1.21.0 (2026-09-01) =
* Feature: [JetFormBuilder](https://jetformbuilder.com/) integration - Procaptcha is selectable as a form's Captcha Provider
* Feature: [LearnDash LMS](https://www.learndash.com/) integration - registration, login and password recovery forms
* Feature: [Divi](https://www.elegantthemes.com/gallery/divi/) integration - Contact Form and Login modules
* Feature: [Bricks](https://bricksbuilder.io/) integration - form element
* Feature: [Blocksy](https://creativethemes.com/blocksy/) integration - newsletter subscribe form
* Maintenance: documented that the Blocksy, Astra and Zakra account forms are covered by the WordPress Core Forms integration

= 1.20.6 (2026-08-31) =
* Maintenance: simplify the statistics page to the monthly captcha counts and the traffic chart

Expand Down
36 changes: 36 additions & 0 deletions prosopo-procaptcha/src/Integrations/Plugins/Blocksy/Blocksy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare( strict_types=1 );

namespace Io\Prosopo\Procaptcha\Integrations\Plugins\Blocksy;

defined( 'ABSPATH' ) || exit;

use Io\Prosopo\Procaptcha\Integration\Module\About_Module_Integration;
use Io\Prosopo\Procaptcha\Integration\Plugin\Plugin_Integration_Base;
use Io\Prosopo\Procaptcha\Integrations\Plugins\Blocksy\Forms\Blocksy_Newsletter;

/**
* The Blocksy account modal forms are covered by the WordPress Core Forms integration,
* as Blocksy renders them with the native account hooks and processes them with the native flow.
*/
final class Blocksy extends Plugin_Integration_Base {
public function get_about_integration(): About_Module_Integration {
$about = new About_Module_Integration();

$about->name = 'Blocksy';
$about->docs_url = self::get_docs_url( 'blocksy' );

return $about;
}

public function is_active(): bool {
return defined( 'BLOCKSY_PATH' );
}

protected function get_hookable_integrations(): array {
return array(
new Blocksy_Newsletter( $this->widget ),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php

declare( strict_types=1 );

namespace Io\Prosopo\Procaptcha\Integrations\Plugins\Blocksy\Forms;

defined( 'ABSPATH' ) || exit;

use Io\Prosopo\Procaptcha\Integration\Widget\Widget_Integration_Base;
use Io\Prosopo\Procaptcha\Utils\Screen_Detector\Screen_Detector;
use Io\Prosopo\Procaptcha\Widget\Widget_Settings;
use function Io\Prosopo\Procaptcha\Vendors\WPLake\Typed\string;

/**
* Blocksy Companion newsletter subscribe form (both the block and the shortcode).
*
* The form is submitted with the whole FormData, so the widget's own input reaches the server as is.
*/
final class Blocksy_Newsletter extends Widget_Integration_Base {
const AJAX_ACTION = 'blc_newsletter_subscribe_process_ajax_subscribe';
const BLOCK_NAME = 'blocksy/newsletter';
const MESSAGE_BLOCK = '<div class="ct-newsletter-subscribe-message">';
const SHORTCODE = 'blocksy_newsletter_subscribe';

public function set_hooks( Screen_Detector $screen_detector ): void {
add_filter( 'render_block', array( $this, 'integrate_widget_into_block' ), 10, 2 );
add_filter( 'do_shortcode_tag', array( $this, 'integrate_widget_into_shortcode' ), 10, 2 );

// With the low priority to be processed before the primary handler.
add_action( 'wp_ajax_' . self::AJAX_ACTION, array( $this, 'verify_form_submission' ), -999 );
add_action( 'wp_ajax_nopriv_' . self::AJAX_ACTION, array( $this, 'verify_form_submission' ), -999 );
}

/**
* @param mixed $block_content
* @param mixed $block
*
* @return mixed
*/
public function integrate_widget_into_block( $block_content, $block ) {
if ( ! is_string( $block_content ) ||
self::BLOCK_NAME !== string( $block, 'blockName' ) ) {
return $block_content;
}

return $this->add_widget_field( $block_content );
}

/**
* @param mixed $shortcode_content
*
* @return mixed
*/
public function integrate_widget_into_shortcode( $shortcode_content, string $shortcode_name ) {
if ( ! is_string( $shortcode_content ) ||
self::SHORTCODE !== $shortcode_name ) {
return $shortcode_content;
}

return $this->add_widget_field( $shortcode_content );
}

public function verify_form_submission(): void {
$widget = $this->widget;

if ( ! $widget->is_protection_enabled() ||
$widget->is_verification_token_valid() ) {
return;
}

// Blocksy prints the 'message' key of the response, regardless of its success status.
wp_send_json_error(
array(
'message' => $widget->get_validation_error_message(),
'result' => 'no',
)
);
}

protected function add_widget_field( string $form_content ): string {
if ( false === strpos( $form_content, self::MESSAGE_BLOCK ) ) {
return $form_content;
}

$widget_field = $this->widget->print_form_field(
array(
Widget_Settings::ELEMENT_ATTRIBUTES => array(
'style' => 'margin:10px 0 0',
),
Widget_Settings::IS_RETURN_ONLY => true,
)
);

return str_replace(
self::MESSAGE_BLOCK,
$widget_field . "\n" . self::MESSAGE_BLOCK,
$form_content
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<?php
// silence is golden.
2 changes: 2 additions & 0 deletions prosopo-procaptcha/src/Integrations/Plugins/Blocksy/index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<?php
// silence is golden.
34 changes: 34 additions & 0 deletions prosopo-procaptcha/src/Integrations/Plugins/Bricks/Bricks.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare( strict_types=1 );

namespace Io\Prosopo\Procaptcha\Integrations\Plugins\Bricks;

defined( 'ABSPATH' ) || exit;

use Io\Prosopo\Procaptcha\Integration\Module\About_Module_Integration;
use Io\Prosopo\Procaptcha\Integration\Plugin\Plugin_Integration_Base;

final class Bricks extends Plugin_Integration_Base {
const THEME_NAME = 'bricks';

public function get_about_integration(): About_Module_Integration {
$about = new About_Module_Integration();

$about->name = 'Bricks';
$about->docs_url = self::get_docs_url( 'bricks' );

return $about;
}

public function is_active(): bool {
// Bricks is a theme, so it's not loaded yet on the 'plugins_loaded' hook, hence the template check.
return self::THEME_NAME === get_template();
}

protected function get_hookable_integrations(): array {
return array(
new Bricks_Form_Integration( $this->widget ),
);
}
}
Loading
Loading