diff --git a/lib/Cleantalk/Antispam/IntegrationsByClass/WPSearchForm.php b/lib/Cleantalk/Antispam/IntegrationsByClass/WPSearchForm.php index 03f302061..0d228c17f 100644 --- a/lib/Cleantalk/Antispam/IntegrationsByClass/WPSearchForm.php +++ b/lib/Cleantalk/Antispam/IntegrationsByClass/WPSearchForm.php @@ -3,6 +3,8 @@ namespace Cleantalk\Antispam\IntegrationsByClass; use Cleantalk\ApbctWP\Honeypot; +use Cleantalk\ApbctWP\Variables\AltSessions; +use Cleantalk\ApbctWP\Variables\Server; use DOMDocument; /** @@ -90,12 +92,86 @@ public function apbctFormSearchAddFields($form_html) $result = str_replace('', Honeypot::generateHoneypotField('search_form', $form_method) . '', $result); + self::setSearchFormDrawn(); return $result; } return $form_html; } + /** + * Marks a protected native WordPress search form as submitted for the given URI. + * + * Removes the URI from the alternative-session storage after the search request is + * received, so the same rendered form state cannot be reused for repeated checks. + * If no tracked forms remain, stores false because alternative sessions do not + * support an empty array value. + * + * @param string $drawn_for_uri URI path where the protected search form was rendered. + * @return void + */ + public static function setSearchFormSent($drawn_for_uri) + { + $current = AltSessions::get('search_form_ready'); + $current = is_string($current) ? json_decode($current, true) : $current; + if (!empty($current) && is_array($current) && isset($current[$drawn_for_uri])) { + unset($current[$drawn_for_uri]); + } + if (empty($current)) { + // prepare for alt sessions, empty array is restricted :( + $current = false; + } + AltSessions::set('search_form_ready', $current); + } + + /** + * Stores the current URI as having a protected native WordPress search form rendered. + * + * The stored URI is later used to verify that an incoming native search request + * came from a page where CleanTalk added protection fields to the search form. + * + * @return void + */ + public static function setSearchFormDrawn() + { + $drawn_for_uri = parse_url(Server::getString('REQUEST_URI'), PHP_URL_PATH); + if (!is_string($drawn_for_uri) || $drawn_for_uri === '') { + return; + } + + $current = AltSessions::get('search_form_ready'); + $current = is_string($current) ? json_decode($current, true) : $current; + $current = is_array($current) ? $current : []; + + if (!isset($current[$drawn_for_uri])) { + $current[$drawn_for_uri] = 1; + AltSessions::set('search_form_ready', $current); + } + } + + /** + * Checks whether the current request refers to a previously rendered protected search form. + * + * Reads the stored search form URI list from alternative sessions and compares each + * stored URI with the current HTTP referer. Returns the matched URI path when found, + * otherwise returns false. + * + * @return false|string URI path where the protected form was rendered, or false if no match exists. + */ + public static function isSearchFormDrawn() + { + $current = AltSessions::get('search_form_ready'); + $current = is_string($current) ? json_decode($current, true) : $current; + if (!empty($current) && is_array($current)) { + foreach ($current as $drawn_for_uri => $_val) { + if (is_string($drawn_for_uri) && apbct_is_in_referer($drawn_for_uri)) { + return $drawn_for_uri; + } + } + } + return false; + } + /** * Test default search string for spam * @@ -117,6 +193,15 @@ public function testSpam($search) return $search; } + // do checks only if the form was built via apbct for the visitor on the uri + $form_is_ready_for_uri = self::isSearchFormDrawn(); + if (false !== $form_is_ready_for_uri) { + self::setSearchFormSent($form_is_ready_for_uri); + } else { + do_action('apbct_skipped_request', __FILE__ . ' -> ' . __FUNCTION__ . '(): native form has not been drawn ' . __LINE__, $_GET); + return $search; + } + $user = apbct_is_user_logged_in() ? wp_get_current_user() : null; $data = array( diff --git a/tests/Antispam/IntegrationsByClass/TestWPSearchForm.php b/tests/Antispam/IntegrationsByClass/TestWPSearchForm.php new file mode 100644 index 000000000..b54452cce --- /dev/null +++ b/tests/Antispam/IntegrationsByClass/TestWPSearchForm.php @@ -0,0 +1,120 @@ +serverBackup = $_SERVER; + Server::getInstance()->variables = []; + + $_SERVER['HTTP_USER_AGENT'] = 'phpunit-user-agent'; + $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'en-US,en;q=0.9'; + $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; + $_SERVER['REQUEST_URI'] = '/search-source-page/'; + $_SERVER['HTTP_REFERER'] = ''; + + $creator = new DbTablesCreator(); + $creator->createTable($wpdb->prefix . 'cleantalk_sessions'); + + AltSessions::wipe(); + } + + public function tearDown(): void + { + AltSessions::wipe(); + + $_SERVER = $this->serverBackup; + + parent::tearDown(); + } + + public function testSetSearchFormDrawnStoresCurrentRequestPath() + { + $_SERVER['REQUEST_URI'] = '/search-source-page/?foo=bar'; + + WPSearchForm::setSearchFormDrawn(); + + $stored = AltSessions::get('search_form_ready'); + $stored = is_string($stored) ? json_decode($stored, true) : $stored; + $this->assertIsArray($stored); + $this->assertArrayHasKey('/search-source-page/', $stored); + $this->assertSame(1, $stored['/search-source-page/']); + } + + public function testIsSearchFormDrawnReturnsStoredPathIfRefererMatches() + { + $_SERVER['REQUEST_URI'] = '/search-source-page/'; + WPSearchForm::setSearchFormDrawn(); + + $_SERVER['HTTP_REFERER'] = 'https://example.test/search-source-page/?s=query'; + + $this->assertSame( + '/search-source-page/', + WPSearchForm::isSearchFormDrawn() + ); + } + + public function testIsSearchFormDrawnReturnsFalseIfRefererDoesNotMatch() + { + $_SERVER['REQUEST_URI'] = '/search-source-page/'; + WPSearchForm::setSearchFormDrawn(); + + $_SERVER['HTTP_REFERER'] = 'https://example.test/another-page/?s=query'; + + $this->assertFalse(WPSearchForm::isSearchFormDrawn()); + } + + public function testIsSearchFormDrawnReturnsFalseIfNoStoredSearchFormExists() + { + $_SERVER['HTTP_REFERER'] = 'https://example.test/search-source-page/?s=query'; + + $this->assertFalse(WPSearchForm::isSearchFormDrawn()); + } + + public function testSetSearchFormSentRemovesStoredPath() + { + $_SERVER['REQUEST_URI'] = '/search-source-page/'; + WPSearchForm::setSearchFormDrawn(); + + WPSearchForm::setSearchFormSent('/search-source-page/'); + + $this->assertSame(0, AltSessions::get('search_form_ready')); + } + + public function testSetSearchFormSentKeepsOtherStoredPaths() + { + AltSessions::set( + 'search_form_ready', + array( + '/first-page/' => 1, + '/second-page/' => 1, + ) + ); + + WPSearchForm::setSearchFormSent('/first-page/'); + + $stored = AltSessions::get('search_form_ready'); + $stored = is_string($stored) ? json_decode($stored, true) : $stored; + + $this->assertIsArray($stored); + $this->assertArrayNotHasKey('/first-page/', $stored); + $this->assertArrayHasKey('/second-page/', $stored); + $this->assertSame(1, $stored['/second-page/']); + } +}