diff --git a/cleantalk.php b/cleantalk.php index 74d231ed5..fb92bea6e 100644 --- a/cleantalk.php +++ b/cleantalk.php @@ -11,6 +11,7 @@ Domain Path: /i18n */ +use Cleantalk\Antispam\ScriptsIntegration\CleantalkScriptsIntegrator; use Cleantalk\Antispam\ProtectByShortcode; use Cleantalk\ApbctWP\Activator; use Cleantalk\ApbctWP\AdminNotices; @@ -597,27 +598,8 @@ function apbct_write_js_errors($data) // Public actions if ( ! is_admin() && ! apbct_is_ajax() && ! apbct_is_customize_preview() ) { - if ( - apbct_is_plugin_active('fluentformpro/fluentformpro.php') && - ( - apbct_is_in_uri('ff_landing=') || - ( - // Load scripts for logged in users if constant is defined - apbct_is_user_logged_in() && - (defined('APBCT_FF_JS_SCRIPTS_LOAD') && - APBCT_FF_JS_SCRIPTS_LOAD == true) - ) - ) - ) { - add_action('wp_head', function () { - echo ''; - echo ''; - }, 100); - } + $sci = new CleantalkScriptsIntegrator(); + $sci->run(); SFWUpdateHelper::processSFWOutdatedError($apbct); diff --git a/lib/Cleantalk/Antispam/ScriptsIntegration/CleantalkScriptsIntegrator.php b/lib/Cleantalk/Antispam/ScriptsIntegration/CleantalkScriptsIntegrator.php new file mode 100644 index 000000000..3b3b43fdd --- /dev/null +++ b/lib/Cleantalk/Antispam/ScriptsIntegration/CleantalkScriptsIntegrator.php @@ -0,0 +1,99 @@ +getIntegrations(); + + $this->plugins_loaded = !empty($integrations) + ? $this->getPluginsToInline($integrations) + : []; + + if (!empty($this->plugins_loaded)) { + foreach ($this->plugins_loaded as $_hook => $plugin) { + if ($plugin instanceof ScriptIntegrationPlugin) { + add_action($_hook, function () use ($plugin) { + $plugin->integrate(); + }, 100); + } + } + } + } + + /** + * Filters plugins that are eligible for inline script integration. + * + * A plugin is included only if: + * - It is active + * - It matches the current URI context + * - It passes additional runtime checks + * + * Each hook can only be assigned to one plugin (first match wins). + * + * @param ScriptIntegrationPlugin[] $integrations List of available plugin integrations. + * @return ScriptIntegrationPlugin[] Filtered plugins indexed by hook name. + */ + public function getPluginsToInline($integrations) + { + $plugins_loaded = []; + + foreach ($integrations as $plugin) { + if ( + $plugin->is_plugin_active && + $plugin->is_in_uri && + $plugin->additional_checks_passed + ) { + if (!isset($plugins_loaded[$plugin->hook_name])) { + $plugins_loaded[$plugin->hook_name] = $plugin; + } + } + } + + return $plugins_loaded; + } + + /** + * Returns a list of all available plugin integrations. + * + * Each integration defines: + * - Activation rules + * - Context conditions (URI, environment, etc.) + * - Hook target for script injection + * + * @return ScriptIntegrationPlugin[] + */ + public function getIntegrations() + { + try { + $integrations = [ + new GiveWPScript(), + new FluentFormScript(), + ]; + } catch (\Exception $e) { + $integrations = []; + } + + return $integrations; + } +} diff --git a/lib/Cleantalk/Antispam/ScriptsIntegration/FluentFormScript.php b/lib/Cleantalk/Antispam/ScriptsIntegration/FluentFormScript.php new file mode 100644 index 000000000..7c021302d --- /dev/null +++ b/lib/Cleantalk/Antispam/ScriptsIntegration/FluentFormScript.php @@ -0,0 +1,29 @@ +'; + echo ''; + } + + public function additionalChecks() + { + return $this->is_in_uri || ( + function_exists('apbct_is_user_logged_in') && + apbct_is_user_logged_in() && + (defined('APBCT_FF_JS_SCRIPTS_LOAD') && APBCT_FF_JS_SCRIPTS_LOAD == true) + ); + } +} diff --git a/lib/Cleantalk/Antispam/ScriptsIntegration/GiveWPScript.php b/lib/Cleantalk/Antispam/ScriptsIntegration/GiveWPScript.php new file mode 100644 index 000000000..28c129ae0 --- /dev/null +++ b/lib/Cleantalk/Antispam/ScriptsIntegration/GiveWPScript.php @@ -0,0 +1,32 @@ + true, + 'strategy' => 'async' + ) + ); + } + } +} diff --git a/lib/Cleantalk/Antispam/ScriptsIntegration/ScriptIntegrationPlugin.php b/lib/Cleantalk/Antispam/ScriptsIntegration/ScriptIntegrationPlugin.php new file mode 100644 index 000000000..f5518b5c2 --- /dev/null +++ b/lib/Cleantalk/Antispam/ScriptsIntegration/ScriptIntegrationPlugin.php @@ -0,0 +1,77 @@ +plugin_file) || + !is_string($this->plugin_file) || + !isset($this->uri_chunk) || + !is_string($this->uri_chunk) || + !isset($this->hook_name) || + !is_string($this->hook_name) + ) { + throw new \Exception('Plugin file, URI chunk and hook name must be set'); + } + + $this->is_plugin_active = $this->isPluginActive($this->plugin_file); + $this->is_in_uri = $this->isInUri($this->uri_chunk); + $this->additional_checks_passed = $this->additionalChecks(); + } + + /** + * Executes the plugin integration logic. + * + * This method must be implemented by each concrete integration class + * and is responsible for registering scripts, hooks, or other behaviors. + * + * @return void + */ + abstract public function integrate(); + + /** + * Checks whether a given WordPress plugin is active. + * + * @param string $plugin_file Path to the plugin main file. + * @return bool True if the plugin is active, false otherwise. + */ + public function isPluginActive($plugin_file) + { + return apbct_is_plugin_active($plugin_file); + } + + /** + * Checks whether the current request URI contains a specific substring. + * + * @param string $uri_chunk URI fragment to search for in the current request URI. + * @return bool True if the URI fragment is found, false otherwise. + */ + public function isInUri($uri_chunk) + { + return apbct_is_in_uri($uri_chunk); + } + + /** + * Performs additional runtime checks required for plugin activation. + * + * This method can be overridden in child classes to implement + * custom validation logic. + * + * @return bool True if all additional checks pass, false otherwise. + */ + public function additionalChecks() + { + return true; + } +} diff --git a/tests/Antispam/Integrations/TestScriptsIntegrator.php b/tests/Antispam/Integrations/TestScriptsIntegrator.php new file mode 100644 index 000000000..5843b37f8 --- /dev/null +++ b/tests/Antispam/Integrations/TestScriptsIntegrator.php @@ -0,0 +1,208 @@ +getPluginsToInline($plugins); + + $this->assertCount(1, $result); + $this->assertArrayHasKey('hook1', $result); + } + + public function testGetPluginsToInlineSkipsInvalidPlugin() + { + $obj = new CleantalkScriptsIntegrator(); + + $plugins = [ + new TestPlugin(false, true, true, 'hook1'), + ]; + + $result = $obj->getPluginsToInline($plugins); + + $this->assertEmpty($result); + } + + public function testFirstPluginWinsForSameHook() + { + $obj = new CleantalkScriptsIntegrator(); + + $plugins = [ + new TestPlugin(true, true, true, 'same_hook'), + new TestPlugin(true, true, true, 'same_hook'), + ]; + + $result = $obj->getPluginsToInline($plugins); + + $this->assertCount(1, $result); + } + + public function testMultipleHooksLoaded() + { + $obj = new CleantalkScriptsIntegrator(); + + $plugins = [ + new TestPlugin(true, true, true, 'hook1'), + new TestPlugin(true, true, true, 'hook2'), + ]; + + $result = $obj->getPluginsToInline($plugins); + + $this->assertCount(2, $result); + $this->assertArrayHasKey('hook1', $result); + $this->assertArrayHasKey('hook2', $result); + } + + /** + * run() + */ + public function testRunRegistersHooks() + { + $obj = $this->getMockBuilder(CleantalkScriptsIntegrator::class) + ->onlyMethods(['getIntegrations']) + ->getMock(); + + $obj->method('getIntegrations')->willReturn([ + new TestPlugin(true, true, true, 'hook1'), + ]); + + $obj->run(); + + $this->assertTrue(has_action('hook1') !== false); + } + + /** + * run() + */ + public function testRunDoesNothingIfNoPlugins() + { + $obj = $this->getMockBuilder(CleantalkScriptsIntegrator::class) + ->onlyMethods(['getIntegrations']) + ->getMock(); + + $obj->method('getIntegrations')->willReturn([]); + + $obj->run(); + + $this->assertEmpty($obj->plugins_loaded); + $this->assertFalse(has_action('hook1')); + } + + /** + * run() + */ + public function testRunResetsState() + { + $obj = new CleantalkScriptsIntegrator(); + + $obj->plugins_loaded = [ + 'old_hook' => new TestPlugin() + ]; + + $mock = $this->getMockBuilder(CleantalkScriptsIntegrator::class) + ->onlyMethods(['getIntegrations']) + ->getMock(); + + $mock->plugins_loaded = $obj->plugins_loaded; + + $mock->method('getIntegrations')->willReturn([]); + + $mock->run(); + + $this->assertEmpty($mock->plugins_loaded); + } + + /** + * run() + */ + public function testRunActuallyExecutesIntegrate() + { + $plugin = new TestPlugin(true, true, true, 'hook1'); + + $obj = $this->getMockBuilder(CleantalkScriptsIntegrator::class) + ->onlyMethods(['getIntegrations']) + ->getMock(); + + $obj->method('getIntegrations')->willReturn([$plugin]); + + $obj->run(); + + $callbacks = $this->get_registered_hooks('hook1'); + + $this->assertNotEmpty($callbacks); + + $callback = null; + + foreach ($callbacks as $priority => $group) { + foreach ($group as $cb) { + $callback = $cb['function']; + break 2; + } + } + + $this->assertIsCallable($callback); + + $callback(); + + $this->assertTrue($plugin->called); + } + + /** + * helper: достать callbacks из WP + */ + private function get_registered_hooks($hook) + { + global $wp_filter; + return isset($wp_filter[$hook]) ? $wp_filter[$hook]->callbacks : []; + } +} + + +/** + * Test plugin + */ +class TestPlugin extends ScriptIntegrationPlugin +{ + public $hook_name = 'test_hook'; + public $plugin_file = 'test/plugin.php'; + public $uri_chunk = 'test'; + + public $called = false; + + public function __construct($active = true, $in_uri = true, $additional = true, $hook = 'test_hook') + { + $this->plugin_file = 'test/plugin.php'; + $this->uri_chunk = 'test'; + $this->hook_name = $hook; + + $this->is_plugin_active = $active; + $this->is_in_uri = $in_uri; + $this->additional_checks_passed = $additional; + } + + public function integrate() + { + $this->called = true; + } +}