diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index 794ba72..3f115d5 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -38,3 +38,42 @@ with additional error codes based on the client's checks. When adding a new
[`RequestMethod`](./src/ReCaptcha/RequestMethod.php) ensure that it returns the
`ReCaptcha::E_CONNECTION_FAILED` and `ReCaptcha::E_BAD_RESPONSE` where
appropriate.
+
+## Public API compatibility
+
+The 1.x line treats the following classes and interfaces as public API:
+`ReCaptcha`, `RequestMethod`, `Response`, `RequestParameters`,
+`RequestMethod\Post`, `RequestMethod\CurlPost`, and
+`RequestMethod\SocketPost`, plus the request wrapper classes
+`RequestMethod\Curl` and `RequestMethod\Socket`.
+
+Changes that narrow those APIs, such as adding native scalar parameter types,
+adding native return types to existing public methods, making public non-final
+classes `readonly` or `final`, removing public classes, or removing existing
+constructor argument forms, should be reserved for a major release.
+
+The `RequestMethod::submit()` interface intentionally keeps its 1.x-compatible
+native signature. Implementations are still expected to return the body of the
+reCAPTCHA response as a string.
+
+### 1.5.1 Compatibility Focus
+
+Release 1.5.1 restores 1.4.x compatibility and keeps the 1.5.0 reliability fixes.
+
+- Restored 1.x public API contracts by removing narrowing type hints and `readonly` modifiers from public non-final DTOs.
+- Kept custom `RequestMethod` implementations working without native return types.
+- Kept legacy input handling for compatibility.
+- Fixed deprecated `curl_close()` usage for PHP 8.5+.
+- Kept the cURL handle cleanup, HTTP/1.1 handling, and TLS verification changes.
+
+### BC Testing
+
+The test suite covers the backward compatibility cases below:
+
+- `testLegacyRequestMethodImplementationWithoutReturnTypeCanBeUsed()`: Custom implementations without strict return types.
+- `testNonStringRequestMethodResponseReturnsBadResponse()`: Non-string responses from custom implementations.
+- `testScalarResponseIsAccepted()`: Scalar inputs such as integers in `verify()`.
+- `testZeroAsStringIsValidResponse()`: The "0" response token.
+- `testVerifyReturnsErrorOnNullResponse()`: Null input handling.
+
+These tests document expected 1.x behavior.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 18c44ae..e4f6c89 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -36,6 +36,25 @@ a good idea to run them locally before submission to avoid getting things
bounced back. That said, tests can be a little daunting so feel free to submit
your PR and ask for help.
+### Backward Compatibility Testing
+
+Changes to public APIs must preserve backward compatibility within the 1.x
+release line. When changes affect public methods, parameters, or return types,
+include tests for:
+
+1. Legacy implementation support. If interface contracts change, verify that old
+ implementations still work without modification.
+2. Type coercion scenarios. Verify that null, scalar, and non-standard inputs
+ behave as they did in 1.4.x.
+3. Custom `RequestMethod` implementations. Verify that user-provided
+ implementations continue to work without return type declarations.
+
+See the BC-focused tests in [`ReCaptchaTest.php`](./tests/ReCaptcha/ReCaptchaTest.php):
+- `testLegacyRequestMethodImplementationWithoutReturnTypeCanBeUsed()`
+- `testNonStringRequestMethodResponseReturnsBadResponse()`
+- `testScalarResponseIsAccepted()`
+- `testZeroAsStringIsValidResponse()`
+
## Code reviews
All submissions, including submissions by project members, require review.
diff --git a/README.md b/README.md
index 55f77ee..a4a32b2 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@ and v3.
- reCAPTCHA: https://cloud.google.com/security/products/recaptcha
- This repo: https://github.com/google/recaptcha
- Hosted demo: https://recaptcha-demo.appspot.com/
-- Version: 1.5.0
+- Version: 1.5.1
- License: BSD, see [LICENSE](LICENSE)
> [!IMPORTANT]
@@ -76,7 +76,7 @@ This library comes in when you need to verify the user's response. On the PHP
side you need the response from the reCAPTCHA service and secret key from your
credentials. Instantiate the `ReCaptcha` class with your secret key, specify any
additional validation rules, and then call `verify()` with the reCAPTCHA
-response (usually in `$_POST[\ReCaptcha\ReCaptcha::USER_TOKEN_PARAMETER]` or the
+response (usually in `$_POST[\ReCaptcha\ReCaptcha::RESPONSE_KEY]` or the
response from `grecaptcha.execute()` in JS which is in `$gRecaptchaResponse` in
the example) and user's IP address. For example:
@@ -169,6 +169,11 @@ $recaptcha = new \ReCaptcha\ReCaptcha($secret, new \ReCaptcha\RequestMethod\Sock
For more details on usage and structure, see [ARCHITECTURE](ARCHITECTURE.md).
+The 1.x line preserves compatibility for the public request and response APIs.
+See [Public API compatibility](ARCHITECTURE.md#public-api-compatibility) for
+details on which API changes require a major release.
+
+
### Examples
You can see examples of each reCAPTCHA type in [examples/](examples/). You can
diff --git a/composer.json b/composer.json
index 0639a0d..b5d7641 100644
--- a/composer.json
+++ b/composer.json
@@ -25,14 +25,14 @@
},
"extra": {
"branch-alias": {
- "dev-main": "1.4.x-dev"
+ "dev-main": "1.5.x-dev"
}
},
"scripts": {
"lint": "vendor/bin/php-cs-fixer -vvv check --using-cache=no",
"lint-fix": "vendor/bin/php-cs-fixer -vvv fix --using-cache=no",
"phpstan": "vendor/bin/phpstan",
- "test": "XDEBUG_MODE=coverage vendor/bin/phpunit",
+ "test": "@php -d xdebug.mode=coverage vendor/bin/phpunit",
"serve-examples": "@php -S localhost:8080 -t examples"
},
"config": {
diff --git a/composer.lock b/composer.lock
index 7f2cfe6..ca029ca 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "6f9f7f08c81bbc8765f5b1ea64677736",
+ "content-hash": "5a1a38d86c0d9b0bff77a744deb968ab",
"packages": [],
"packages-dev": [
{
@@ -1222,11 +1222,11 @@
},
{
"name": "phpstan/phpstan",
- "version": "2.1.51",
+ "version": "2.1.54",
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dc3b523c45e714c70de2ac5113b958223b55dc59",
- "reference": "dc3b523c45e714c70de2ac5113b958223b55dc59",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/8be50c3992107dc837b17da4d140fbbdf9a5c5bd",
+ "reference": "8be50c3992107dc837b17da4d140fbbdf9a5c5bd",
"shasum": ""
},
"require": {
@@ -1271,7 +1271,7 @@
"type": "github"
}
],
- "time": "2026-04-21T18:22:01+00:00"
+ "time": "2026-04-29T13:31:09+00:00"
},
{
"name": "phpunit/php-code-coverage",
@@ -1658,16 +1658,16 @@
},
{
"name": "phpunit/phpunit",
- "version": "13.1.7",
+ "version": "13.1.8",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "ddd6401641861cdef94b922ef10d484f436e8dcd"
+ "reference": "f49a2b5e51ffb33421745368cc099cf66830d71b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/ddd6401641861cdef94b922ef10d484f436e8dcd",
- "reference": "ddd6401641861cdef94b922ef10d484f436e8dcd",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f49a2b5e51ffb33421745368cc099cf66830d71b",
+ "reference": "f49a2b5e51ffb33421745368cc099cf66830d71b",
"shasum": ""
},
"require": {
@@ -1681,7 +1681,7 @@
"phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1",
"php": ">=8.4.1",
- "phpunit/php-code-coverage": "^14.1.3",
+ "phpunit/php-code-coverage": "^14.1.6",
"phpunit/php-file-iterator": "^7.0.0",
"phpunit/php-invoker": "^7.0.0",
"phpunit/php-text-template": "^6.0.0",
@@ -1737,7 +1737,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
- "source": "https://github.com/sebastianbergmann/phpunit/tree/13.1.7"
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/13.1.8"
},
"funding": [
{
@@ -1745,7 +1745,7 @@
"type": "other"
}
],
- "time": "2026-04-18T06:14:52+00:00"
+ "time": "2026-05-01T04:22:45+00:00"
},
{
"name": "psr/container",
@@ -3722,16 +3722,16 @@
},
{
"name": "symfony/config",
- "version": "v8.0.8",
+ "version": "v8.0.9",
"source": {
"type": "git",
"url": "https://github.com/symfony/config.git",
- "reference": "c7369cc1da250fcbfe0c5a9d109e419661549c39"
+ "reference": "7e712ee3c98ec114f674adc4fbad4c2fe7526b9c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/config/zipball/c7369cc1da250fcbfe0c5a9d109e419661549c39",
- "reference": "c7369cc1da250fcbfe0c5a9d109e419661549c39",
+ "url": "https://api.github.com/repos/symfony/config/zipball/7e712ee3c98ec114f674adc4fbad4c2fe7526b9c",
+ "reference": "7e712ee3c98ec114f674adc4fbad4c2fe7526b9c",
"shasum": ""
},
"require": {
@@ -3776,7 +3776,7 @@
"description": "Helps you find, load, combine, autofill and validate configuration values of any kind",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/config/tree/v8.0.8"
+ "source": "https://github.com/symfony/config/tree/v8.0.9"
},
"funding": [
{
@@ -3796,20 +3796,20 @@
"type": "tidelift"
}
],
- "time": "2026-03-30T15:14:47+00:00"
+ "time": "2026-04-29T15:02:55+00:00"
},
{
"name": "symfony/console",
- "version": "v8.0.8",
+ "version": "v8.0.9",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
- "reference": "5b66d385dc58f69652e56f78a4184615e3f2b7f7"
+ "reference": "7113778e2e91f4709cb3194a75dfa9c0d028d94d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/5b66d385dc58f69652e56f78a4184615e3f2b7f7",
- "reference": "5b66d385dc58f69652e56f78a4184615e3f2b7f7",
+ "url": "https://api.github.com/repos/symfony/console/zipball/7113778e2e91f4709cb3194a75dfa9c0d028d94d",
+ "reference": "7113778e2e91f4709cb3194a75dfa9c0d028d94d",
"shasum": ""
},
"require": {
@@ -3866,7 +3866,7 @@
"terminal"
],
"support": {
- "source": "https://github.com/symfony/console/tree/v8.0.8"
+ "source": "https://github.com/symfony/console/tree/v8.0.9"
},
"funding": [
{
@@ -3886,7 +3886,7 @@
"type": "tidelift"
}
],
- "time": "2026-03-30T15:14:47+00:00"
+ "time": "2026-04-29T15:02:55+00:00"
},
{
"name": "symfony/deprecation-contracts",
@@ -3957,16 +3957,16 @@
},
{
"name": "symfony/event-dispatcher",
- "version": "v8.0.8",
+ "version": "v8.0.9",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
- "reference": "f662acc6ab22a3d6d716dcb44c381c6002940df6"
+ "reference": "0c3c1a17604c4dbbec4b93fe162c538482096e1f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/f662acc6ab22a3d6d716dcb44c381c6002940df6",
- "reference": "f662acc6ab22a3d6d716dcb44c381c6002940df6",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/0c3c1a17604c4dbbec4b93fe162c538482096e1f",
+ "reference": "0c3c1a17604c4dbbec4b93fe162c538482096e1f",
"shasum": ""
},
"require": {
@@ -4018,7 +4018,7 @@
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.8"
+ "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.9"
},
"funding": [
{
@@ -4038,7 +4038,7 @@
"type": "tidelift"
}
],
- "time": "2026-03-30T15:14:47+00:00"
+ "time": "2026-04-18T13:51:42+00:00"
},
{
"name": "symfony/event-dispatcher-contracts",
@@ -4118,16 +4118,16 @@
},
{
"name": "symfony/filesystem",
- "version": "v8.0.8",
+ "version": "v8.0.9",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
- "reference": "66b769ae743ce2d13e435528fbef4af03d623e5a"
+ "reference": "d1ec4543d5c6c2dac78503c2fae5ea0b3608ce40"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/filesystem/zipball/66b769ae743ce2d13e435528fbef4af03d623e5a",
- "reference": "66b769ae743ce2d13e435528fbef4af03d623e5a",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/d1ec4543d5c6c2dac78503c2fae5ea0b3608ce40",
+ "reference": "d1ec4543d5c6c2dac78503c2fae5ea0b3608ce40",
"shasum": ""
},
"require": {
@@ -4164,7 +4164,7 @@
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/filesystem/tree/v8.0.8"
+ "source": "https://github.com/symfony/filesystem/tree/v8.0.9"
},
"funding": [
{
@@ -4184,7 +4184,7 @@
"type": "tidelift"
}
],
- "time": "2026-03-30T15:14:47+00:00"
+ "time": "2026-04-18T13:51:42+00:00"
},
{
"name": "symfony/finder",
@@ -5347,5 +5347,5 @@
"php": ">=8.4"
},
"platform-dev": {},
- "plugin-api-version": "2.9.0"
+ "plugin-api-version": "2.6.0"
}
diff --git a/examples/recaptcha-request-curl.php b/examples/recaptcha-request-curl.php
index 5fe0629..4be10a9 100644
--- a/examples/recaptcha-request-curl.php
+++ b/examples/recaptcha-request-curl.php
@@ -89,14 +89,14 @@
POST data
POST data
POST data
POST data
POST data
POST data
secret, $response, $remoteIp, self::VERSION);
$rawResponse = $this->requestMethod->submit($params);
+
+ // @phpstan-ignore function.alreadyNarrowedType
+ if (!is_string($rawResponse)) {
+ return new Response(false, [self::E_BAD_RESPONSE]);
+ }
+
$initialResponse = Response::fromJson($rawResponse);
$validationErrors = [];
@@ -244,9 +257,9 @@ public function verify(string $response, ?string $remoteIp = null): Response
*
* @return ReCaptcha Current instance for fluent interface
*/
- public function setExpectedHostname(string $hostname): self
+ public function setExpectedHostname($hostname)
{
- $this->hostname = $hostname;
+ $this->hostname = self::stringValue($hostname);
return $this;
}
@@ -258,9 +271,9 @@ public function setExpectedHostname(string $hostname): self
*
* @return ReCaptcha Current instance for fluent interface
*/
- public function setExpectedApkPackageName(string $apkPackageName): self
+ public function setExpectedApkPackageName($apkPackageName)
{
- $this->apkPackageName = $apkPackageName;
+ $this->apkPackageName = self::stringValue($apkPackageName);
return $this;
}
@@ -273,9 +286,9 @@ public function setExpectedApkPackageName(string $apkPackageName): self
*
* @return ReCaptcha Current instance for fluent interface
*/
- public function setExpectedAction(string $action): self
+ public function setExpectedAction($action)
{
- $this->action = $action;
+ $this->action = self::stringValue($action);
return $this;
}
@@ -288,9 +301,9 @@ public function setExpectedAction(string $action): self
*
* @return ReCaptcha Current instance for fluent interface
*/
- public function setScoreThreshold(float $threshold): self
+ public function setScoreThreshold($threshold)
{
- $this->threshold = $threshold;
+ $this->threshold = self::floatValue($threshold);
return $this;
}
@@ -302,10 +315,46 @@ public function setScoreThreshold(float $threshold): self
*
* @return ReCaptcha Current instance for fluent interface
*/
- public function setChallengeTimeout(int $timeoutSeconds): self
+ public function setChallengeTimeout($timeoutSeconds)
{
- $this->timeoutSeconds = $timeoutSeconds;
+ $this->timeoutSeconds = self::intValue($timeoutSeconds);
return $this;
}
+
+ private static function nullableStringValue(mixed $value): ?string
+ {
+ if (is_null($value)) {
+ return null;
+ }
+
+ return self::stringValue($value);
+ }
+
+ private static function stringValue(mixed $value): string
+ {
+ if (is_scalar($value) || $value instanceof \Stringable) {
+ return (string) $value;
+ }
+
+ return '';
+ }
+
+ private static function floatValue(mixed $value): float
+ {
+ if (is_null($value) || is_scalar($value)) {
+ return floatval($value);
+ }
+
+ return 0.0;
+ }
+
+ private static function intValue(mixed $value): int
+ {
+ if (is_null($value) || is_scalar($value)) {
+ return intval($value);
+ }
+
+ return 0;
+ }
}
diff --git a/src/ReCaptcha/RequestMethod.php b/src/ReCaptcha/RequestMethod.php
index f39234a..85d9ac1 100644
--- a/src/ReCaptcha/RequestMethod.php
+++ b/src/ReCaptcha/RequestMethod.php
@@ -51,5 +51,5 @@ interface RequestMethod
*
* @return string Body of the reCAPTCHA response
*/
- public function submit(RequestParameters $params): string;
+ public function submit(RequestParameters $params);
}
diff --git a/src/ReCaptcha/RequestMethod/Curl.php b/src/ReCaptcha/RequestMethod/Curl.php
new file mode 100644
index 0000000..b783ba7
--- /dev/null
+++ b/src/ReCaptcha/RequestMethod/Curl.php
@@ -0,0 +1,89 @@
+ $options
+ *
+ * @return bool
+ */
+ public function setoptArray($ch, array $options)
+ {
+ // @phpstan-ignore argument.type
+ return curl_setopt_array($ch, $options);
+ }
+
+ /**
+ * @param mixed $ch
+ *
+ * @return mixed
+ */
+ public function exec($ch)
+ {
+ // @phpstan-ignore argument.type
+ return curl_exec($ch);
+ }
+
+ /**
+ * @param mixed $ch
+ *
+ * @phpstan-return void
+ */
+ public function close($ch)
+ {
+ // PHP automatically closes curl resources on destruction.
+ }
+}
diff --git a/src/ReCaptcha/RequestMethod/CurlPost.php b/src/ReCaptcha/RequestMethod/CurlPost.php
index 5ff831a..d9ccaea 100644
--- a/src/ReCaptcha/RequestMethod/CurlPost.php
+++ b/src/ReCaptcha/RequestMethod/CurlPost.php
@@ -51,6 +51,8 @@
*/
class CurlPost implements RequestMethod
{
+ private Curl $curl;
+
/**
* URL for reCAPTCHA siteverify API.
*/
@@ -59,11 +61,19 @@ class CurlPost implements RequestMethod
/**
* Only needed if you want to override the defaults.
*
- * @param null|string $siteVerifyUrl URL for reCAPTCHA siteverify API
+ * @param null|Curl|string $curl Curl wrapper or URL for reCAPTCHA siteverify API
+ * @param null|string $siteVerifyUrl URL for reCAPTCHA siteverify API
*/
- public function __construct(?string $siteVerifyUrl = null)
+ public function __construct($curl = null, $siteVerifyUrl = null)
{
- $this->siteVerifyUrl = (is_null($siteVerifyUrl)) ? ReCaptcha::SITE_VERIFY_URL : $siteVerifyUrl;
+ if ($curl instanceof Curl) {
+ $this->curl = $curl;
+ } else {
+ $this->curl = new Curl();
+ $siteVerifyUrl = $curl ?? $siteVerifyUrl;
+ }
+
+ $this->siteVerifyUrl = (is_null($siteVerifyUrl)) ? ReCaptcha::SITE_VERIFY_URL : (string) $siteVerifyUrl;
}
/**
@@ -73,9 +83,9 @@ public function __construct(?string $siteVerifyUrl = null)
*
* @return string Body of the reCAPTCHA response
*/
- public function submit(RequestParameters $params): string
+ public function submit(RequestParameters $params)
{
- $handle = curl_init($this->siteVerifyUrl);
+ $handle = $this->curl->init($this->siteVerifyUrl);
$options = [
CURLOPT_POST => true,
@@ -89,10 +99,10 @@ public function submit(RequestParameters $params): string
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_TIMEOUT => 60,
];
- curl_setopt_array($handle, $options);
+ $this->curl->setoptArray($handle, $options);
try {
- $response = curl_exec($handle);
+ $response = $this->curl->exec($handle);
if (is_string($response)) {
return $response;
@@ -100,7 +110,7 @@ public function submit(RequestParameters $params): string
return '{"success": false, "error-codes": ["'.ReCaptcha::E_CONNECTION_FAILED.'"]}';
} finally {
- curl_close($handle);
+ $this->curl->close($handle);
}
}
}
diff --git a/src/ReCaptcha/RequestMethod/Post.php b/src/ReCaptcha/RequestMethod/Post.php
index 6d2c04f..48ae65a 100644
--- a/src/ReCaptcha/RequestMethod/Post.php
+++ b/src/ReCaptcha/RequestMethod/Post.php
@@ -58,9 +58,9 @@ class Post implements RequestMethod
*
* @param null|string $siteVerifyUrl URL for reCAPTCHA siteverify API
*/
- public function __construct(?string $siteVerifyUrl = null)
+ public function __construct($siteVerifyUrl = null)
{
- $this->siteVerifyUrl = (is_null($siteVerifyUrl)) ? ReCaptcha::SITE_VERIFY_URL : $siteVerifyUrl;
+ $this->siteVerifyUrl = (is_null($siteVerifyUrl)) ? ReCaptcha::SITE_VERIFY_URL : (string) $siteVerifyUrl;
}
/**
@@ -70,7 +70,7 @@ public function __construct(?string $siteVerifyUrl = null)
*
* @return string Body of the reCAPTCHA response
*/
- public function submit(RequestParameters $params): string
+ public function submit(RequestParameters $params)
{
$options = [
'ssl' => [
diff --git a/src/ReCaptcha/RequestMethod/Socket.php b/src/ReCaptcha/RequestMethod/Socket.php
new file mode 100644
index 0000000..c40bbea
--- /dev/null
+++ b/src/ReCaptcha/RequestMethod/Socket.php
@@ -0,0 +1,155 @@
+handle = fsockopen($hostname, $port, $errno, $errstr, $timeout);
+
+ if (false != $this->handle && 0 === $errno && '' === $errstr) {
+ return $this->handle;
+ }
+
+ if (false !== $this->handle) {
+ $this->fclose();
+ }
+
+ return false;
+ }
+
+ /**
+ * @param int $seconds
+ * @param int $microseconds
+ *
+ * @return bool
+ */
+ public function streamSetTimeout($seconds, $microseconds = 0)
+ {
+ // @phpstan-ignore argument.type
+ return stream_set_timeout($this->handle, $seconds, $microseconds);
+ }
+
+ /**
+ * @param string $string
+ * @param null|int $length
+ *
+ * @return false|int
+ */
+ public function fwrite($string, $length = null)
+ {
+ $string = (string) $string;
+
+ // @phpstan-ignore argument.type
+ return fwrite($this->handle, $string, is_null($length) ? strlen($string) : $length);
+ }
+
+ /**
+ * @param null|int $length
+ * @param int $offset
+ *
+ * @return false|string
+ */
+ public function streamGetContents($length = null, $offset = -1)
+ {
+ if (is_null($length)) {
+ // @phpstan-ignore argument.type
+ return stream_get_contents($this->handle);
+ }
+
+ // @phpstan-ignore argument.type
+ return stream_get_contents($this->handle, $length, $offset);
+ }
+
+ /**
+ * @param null|int $length
+ *
+ * @return false|string
+ */
+ public function fgets($length = null)
+ {
+ if (is_null($length)) {
+ // @phpstan-ignore argument.type
+ return fgets($this->handle);
+ }
+
+ $length = max(0, intval($length));
+
+ // @phpstan-ignore argument.type
+ return fgets($this->handle, $length);
+ }
+
+ /**
+ * @return bool
+ */
+ public function feof()
+ {
+ // @phpstan-ignore argument.type
+ return feof($this->handle);
+ }
+
+ /**
+ * @return bool
+ */
+ public function fclose()
+ {
+ // @phpstan-ignore argument.type
+ return fclose($this->handle);
+ }
+}
diff --git a/src/ReCaptcha/RequestMethod/SocketPost.php b/src/ReCaptcha/RequestMethod/SocketPost.php
index 955a49d..f2e8051 100644
--- a/src/ReCaptcha/RequestMethod/SocketPost.php
+++ b/src/ReCaptcha/RequestMethod/SocketPost.php
@@ -45,21 +45,31 @@
/**
* Sends a POST request to the reCAPTCHA service, but makes use of fsockopen()
- * instead of get_file_contents(). This is to account for people who may be on
- * servers where allow_url_open is disabled.
+ * instead of file_get_contents(). This is to account for people who may be on
+ * servers where allow_url_fopen is disabled.
*/
class SocketPost implements RequestMethod
{
+ private Socket $socket;
+
private string $siteVerifyUrl;
/**
* Only needed if you want to override the defaults.
*
- * @param null|string $siteVerifyUrl URL for reCAPTCHA siteverify API
+ * @param null|Socket|string $socket Socket wrapper or URL for reCAPTCHA siteverify API
+ * @param null|string $siteVerifyUrl URL for reCAPTCHA siteverify API
*/
- public function __construct(?string $siteVerifyUrl = null)
+ public function __construct($socket = null, $siteVerifyUrl = null)
{
- $this->siteVerifyUrl = (is_null($siteVerifyUrl)) ? ReCaptcha::SITE_VERIFY_URL : $siteVerifyUrl;
+ if ($socket instanceof Socket) {
+ $this->socket = $socket;
+ } else {
+ $this->socket = new Socket();
+ $siteVerifyUrl = $socket ?? $siteVerifyUrl;
+ }
+
+ $this->siteVerifyUrl = (is_null($siteVerifyUrl)) ? ReCaptcha::SITE_VERIFY_URL : (string) $siteVerifyUrl;
}
/**
@@ -69,7 +79,7 @@ public function __construct(?string $siteVerifyUrl = null)
*
* @return string Body of the reCAPTCHA response
*/
- public function submit(RequestParameters $params): string
+ public function submit(RequestParameters $params)
{
$errno = 0;
$errstr = '';
@@ -79,13 +89,15 @@ public function submit(RequestParameters $params): string
return '{"success": false, "error-codes": ["'.ReCaptcha::E_CONNECTION_FAILED.'"]}';
}
- $handle = fsockopen('ssl://'.$urlParsed['host'], 443, $errno, $errstr, 30);
+ $handle = $this->socket->fsockopen('ssl://'.$urlParsed['host'], 443, $errno, $errstr, 30);
if (false === $handle || 0 !== $errno || '' !== $errstr) {
return '{"success": false, "error-codes": ["'.ReCaptcha::E_CONNECTION_FAILED.'"]}';
}
- if (false === stream_set_timeout($handle, 60)) {
+ if (false === $this->socket->streamSetTimeout(60)) {
+ $this->socket->fclose();
+
return '{"success": false, "error-codes": ["'.ReCaptcha::E_CONNECTION_FAILED.'"]}';
}
@@ -98,10 +110,10 @@ public function submit(RequestParameters $params): string
$request .= "Connection: close\r\n\r\n";
$request .= $content."\r\n\r\n";
- fwrite($handle, $request);
- $response = stream_get_contents($handle);
+ $this->socket->fwrite($request);
+ $response = $this->socket->streamGetContents();
- fclose($handle);
+ $this->socket->fclose();
if (!is_string($response)) {
$response = '';
diff --git a/src/ReCaptcha/RequestParameters.php b/src/ReCaptcha/RequestParameters.php
index fd20b14..80dcda6 100644
--- a/src/ReCaptcha/RequestParameters.php
+++ b/src/ReCaptcha/RequestParameters.php
@@ -42,29 +42,38 @@
/**
* Stores and formats the parameters for the request to the reCAPTCHA service.
*/
-readonly class RequestParameters
+class RequestParameters
{
+ private string $secret;
+
+ private string $response;
+
+ private ?string $remoteIp;
+
+ private ?string $version;
+
/**
* Initialise parameters.
*
* @param string $secret site secret
- * @param string $response value from g-captcha-response form field
+ * @param string $response value from g-recaptcha-response form field
* @param null|string $remoteIp user's IP address
* @param null|string $version version of this client library
*/
- public function __construct(
- private string $secret,
- private string $response,
- private ?string $remoteIp = null,
- private ?string $version = null,
- ) {}
+ public function __construct($secret, $response, $remoteIp = null, $version = null)
+ {
+ $this->secret = self::stringValue($secret);
+ $this->response = self::stringValue($response);
+ $this->remoteIp = self::nullableStringValue($remoteIp);
+ $this->version = self::nullableStringValue($version);
+ }
/**
* Array representation.
*
* @return array array formatted parameters
*/
- public function toArray(): array
+ public function toArray()
{
$params = ['secret' => $this->secret, 'response' => $this->response];
@@ -84,8 +93,26 @@ public function toArray(): array
*
* @return string query string formatted parameters
*/
- public function toQueryString(): string
+ public function toQueryString()
{
return http_build_query($this->toArray(), '', '&');
}
+
+ private static function nullableStringValue(mixed $value): ?string
+ {
+ if (is_null($value)) {
+ return null;
+ }
+
+ return self::stringValue($value);
+ }
+
+ private static function stringValue(mixed $value): string
+ {
+ if (is_scalar($value) || $value instanceof \Stringable) {
+ return (string) $value;
+ }
+
+ return '';
+ }
}
diff --git a/src/ReCaptcha/Response.php b/src/ReCaptcha/Response.php
index 7cdf9e0..a927d32 100644
--- a/src/ReCaptcha/Response.php
+++ b/src/ReCaptcha/Response.php
@@ -42,8 +42,25 @@
/**
* The response returned from the service.
*/
-readonly class Response
+class Response
{
+ private bool $success = false;
+
+ /**
+ * @var array
+ */
+ private array $errorCodes = [];
+
+ private string $hostname = '';
+
+ private string $challengeTs = '';
+
+ private string $apkPackageName = '';
+
+ private ?float $score = null;
+
+ private string $action = '';
+
/**
* Constructor.
*
@@ -55,34 +72,43 @@
* @param ?float $score score assigned to the request
* @param string $action action as specified by the page
*/
- public function __construct(
- private bool $success,
- private array $errorCodes = [],
- private string $hostname = '',
- private string $challengeTs = '',
- private string $apkPackageName = '',
- private ?float $score = null,
- private string $action = '',
- ) {}
+ public function __construct($success, array $errorCodes = [], $hostname = '', $challengeTs = '', $apkPackageName = '', $score = null, $action = '')
+ {
+ $this->success = (bool) $success;
+ $this->errorCodes = $errorCodes;
+ $this->hostname = self::stringValue($hostname);
+ $this->challengeTs = self::stringValue($challengeTs);
+ $this->apkPackageName = self::stringValue($apkPackageName);
+ $this->score = self::nullableFloatValue($score);
+ $this->action = self::stringValue($action);
+ }
/**
* Build the response from the expected JSON returned by the service.
+ *
+ * @param mixed $json
+ *
+ * @return Response
*/
- public static function fromJson(string $json): Response
+ public static function fromJson($json)
{
+ if (!is_string($json)) {
+ return new Response(false, [ReCaptcha::E_INVALID_JSON]);
+ }
+
$responseData = json_decode($json, true);
- if (!is_array($responseData)) {
+ if (!$responseData || !is_array($responseData)) {
return new Response(false, [ReCaptcha::E_INVALID_JSON]);
}
- $hostname = isset($responseData['hostname']) && is_string($responseData['hostname']) ? $responseData['hostname'] : '';
- $challengeTs = isset($responseData['challenge_ts']) && is_string($responseData['challenge_ts']) ? $responseData['challenge_ts'] : '';
- $apkPackageName = isset($responseData['apk_package_name']) && is_string($responseData['apk_package_name']) ? $responseData['apk_package_name'] : '';
- $score = isset($responseData['score']) && is_numeric($responseData['score']) ? floatval($responseData['score']) : null;
- $action = isset($responseData['action']) && is_string($responseData['action']) ? $responseData['action'] : '';
+ $hostname = self::stringValue($responseData['hostname'] ?? '');
+ $challengeTs = self::stringValue($responseData['challenge_ts'] ?? '');
+ $apkPackageName = self::stringValue($responseData['apk_package_name'] ?? '');
+ $score = self::nullableFloatValue($responseData['score'] ?? null);
+ $action = self::stringValue($responseData['action'] ?? '');
- if (isset($responseData['success']) && true === $responseData['success']) {
+ if (isset($responseData['success']) && true == $responseData['success']) {
return new Response(true, [], $hostname, $challengeTs, $apkPackageName, $score, $action);
}
@@ -98,8 +124,10 @@ public static function fromJson(string $json): Response
/**
* Is success?
+ *
+ * @return bool
*/
- public function isSuccess(): bool
+ public function isSuccess()
{
return $this->success;
}
@@ -109,47 +137,57 @@ public function isSuccess(): bool
*
* @return array
*/
- public function getErrorCodes(): array
+ public function getErrorCodes()
{
return $this->errorCodes;
}
/**
* Get hostname.
+ *
+ * @return string
*/
- public function getHostname(): string
+ public function getHostname()
{
return $this->hostname;
}
/**
* Get challenge timestamp.
+ *
+ * @return string
*/
- public function getChallengeTs(): string
+ public function getChallengeTs()
{
return $this->challengeTs;
}
/**
* Get APK package name.
+ *
+ * @return string
*/
- public function getApkPackageName(): string
+ public function getApkPackageName()
{
return $this->apkPackageName;
}
/**
* Get score.
+ *
+ * @return null|float
*/
- public function getScore(): ?float
+ public function getScore()
{
return $this->score;
}
/**
* Get action.
+ *
+ * @return string
*/
- public function getAction(): string
+ public function getAction()
{
return $this->action;
}
@@ -167,7 +205,7 @@ public function getAction(): string
* error-codes: string[]
* }
*/
- public function toArray(): array
+ public function toArray()
{
return [
'success' => $this->isSuccess(),
@@ -179,4 +217,26 @@ public function toArray(): array
'error-codes' => $this->getErrorCodes(),
];
}
+
+ private static function stringValue(mixed $value): string
+ {
+ if (is_scalar($value) || $value instanceof \Stringable) {
+ return (string) $value;
+ }
+
+ return '';
+ }
+
+ private static function nullableFloatValue(mixed $value): ?float
+ {
+ if (is_null($value)) {
+ return null;
+ }
+
+ if (is_scalar($value)) {
+ return floatval($value);
+ }
+
+ return null;
+ }
}
diff --git a/tests/ReCaptcha/ReCaptchaTest.php b/tests/ReCaptcha/ReCaptchaTest.php
index 57f6e25..2e78f4c 100644
--- a/tests/ReCaptcha/ReCaptchaTest.php
+++ b/tests/ReCaptcha/ReCaptchaTest.php
@@ -77,9 +77,8 @@ protected function tearDown(): void
#[DataProvider('invalidSecretProvider')]
public function testExceptionThrownOnInvalidSecretType(mixed $invalid): void
{
- $this->expectException(\TypeError::class);
+ $this->expectException(\RuntimeException::class);
- /** @phpstan-ignore argument.type */
$rc = new ReCaptcha($invalid);
}
@@ -101,7 +100,6 @@ public function testExceptionThrownOnEmptySecret(mixed $emptySecret): void
{
$this->expectException(\RuntimeException::class);
- /** @phpstan-ignore argument.type */
$rc = new ReCaptcha($emptySecret);
}
@@ -123,6 +121,61 @@ public function testVerifyReturnsErrorOnMissingResponse(): void
$this->assertEquals([ReCaptcha::E_MISSING_INPUT_RESPONSE], $response->getErrorCodes());
}
+ public function testVerifyReturnsErrorOnNullResponse(): void
+ {
+ $rc = new ReCaptcha('secret');
+
+ /** @phpstan-ignore argument.type */
+ $response = $rc->verify(null);
+ $this->assertFalse($response->isSuccess());
+ $this->assertEquals([ReCaptcha::E_MISSING_INPUT_RESPONSE], $response->getErrorCodes());
+ }
+
+ public function testLegacyRequestMethodImplementationWithoutReturnTypeCanBeUsed(): void
+ {
+ // BC: Legacy custom RequestMethod implementations without strict return types should still work
+ $method = new class implements RequestMethod {
+ public function submit(RequestParameters $params)
+ {
+ return '{"success": true}';
+ }
+ };
+
+ $rc = new ReCaptcha('secret', $method);
+ $response = $rc->verify('response');
+
+ $this->assertTrue($response->isSuccess());
+ }
+
+ public function testNonStringRequestMethodResponseReturnsBadResponse(): void
+ {
+ // Keep the 1.x compatibility path open for older custom RequestMethod implementations.
+ $method = new class implements RequestMethod {
+ public function submit(RequestParameters $params)
+ {
+ // @phpstan-ignore return.type
+ return false;
+ }
+ };
+
+ $rc = new ReCaptcha('secret', $method);
+ $response = $rc->verify('response');
+
+ $this->assertFalse($response->isSuccess());
+ $this->assertEquals([ReCaptcha::E_BAD_RESPONSE], $response->getErrorCodes());
+ }
+
+ public function testObjectValuesDoNotCrashLegacySetters(): void
+ {
+ $method = $this->getMockRequestMethod('{"success": true, "hostname": ""}');
+ $rc = new ReCaptcha('secret', $method);
+
+ /** @phpstan-ignore argument.type */
+ $response = $rc->setExpectedHostname(new \stdClass())->verify('response');
+
+ $this->assertTrue($response->isSuccess());
+ }
+
public function testZeroAsStringIsValidSecret(): void
{
$rc = new ReCaptcha('0');
@@ -137,6 +190,16 @@ public function testZeroAsStringIsValidResponse(): void
$this->assertTrue($response->isSuccess());
}
+ public function testScalarResponseIsAccepted(): void
+ {
+ $method = $this->getMockRequestMethod('{"success": true}');
+ $rc = new ReCaptcha('secret', $method);
+
+ /** @phpstan-ignore argument.type */
+ $response = $rc->verify(12345);
+ $this->assertTrue($response->isSuccess());
+ }
+
public function testDefaultRequestMethodWithCurl(): void
{
GlobalState::$isCurlAvailable = true;
@@ -256,6 +319,16 @@ public function testVerifyBelowThreshold(): void
$this->assertEquals([ReCaptcha::E_SCORE_THRESHOLD_NOT_MET], $response->getErrorCodes());
}
+ public function testScoreThresholdAcceptsNumericString(): void
+ {
+ $method = $this->getMockRequestMethod('{"success": true, "score": "0.9"}');
+ $rc = new ReCaptcha('secret', $method);
+
+ /** @phpstan-ignore argument.type */
+ $response = $rc->setScoreThreshold('0.5')->verify('response');
+ $this->assertTrue($response->isSuccess());
+ }
+
public function testVerifyWithinTimeout(): void
{
// Responses come back like 2018-07-31T13:48:41Z
diff --git a/tests/ReCaptcha/RequestMethod/CurlPostTest.php b/tests/ReCaptcha/RequestMethod/CurlPostTest.php
index 88e4d39..fdbe6d2 100644
--- a/tests/ReCaptcha/RequestMethod/CurlPostTest.php
+++ b/tests/ReCaptcha/RequestMethod/CurlPostTest.php
@@ -56,6 +56,8 @@ class CurlPostGlobalState
public static ?array $setoptArrayOptions = null;
public static bool|string $execResponse = 'RESPONSEBODY';
+
+ public static bool $closeCalled = false;
}
/**
@@ -93,7 +95,7 @@ function curl_exec(\stdClass $ch): bool|string
*/
function curl_close(\stdClass $ch): void
{
- // no-op mock
+ CurlPostGlobalState::$closeCalled = true;
}
/**
@@ -108,6 +110,7 @@ protected function setUp(): void
CurlPostGlobalState::$initUrl = null;
CurlPostGlobalState::$setoptArrayOptions = null;
CurlPostGlobalState::$execResponse = 'RESPONSEBODY';
+ CurlPostGlobalState::$closeCalled = false;
}
public function testSubmit(): void
@@ -133,6 +136,35 @@ public function testOverrideSiteVerifyUrl(): void
$this->assertEquals('RESPONSEBODY', $response);
}
+ public function testLegacyConstructorOverrideSiteVerifyUrl(): void
+ {
+ $url = 'OVERRIDE';
+ $pc = new CurlPost(null, $url);
+ $response = $pc->submit(new RequestParameters('secret', 'response'));
+
+ $this->assertEquals($url, CurlPostGlobalState::$initUrl);
+ $this->assertEquals('RESPONSEBODY', $response);
+ }
+
+ public function testLegacyNamedArgumentsCanInjectCurlWrapper(): void
+ {
+ $url = 'OVERRIDE';
+ $pc = new CurlPost(curl: new Curl(), siteVerifyUrl: $url);
+ $response = $pc->submit(new RequestParameters('secret', 'response'));
+
+ $this->assertEquals($url, CurlPostGlobalState::$initUrl);
+ $this->assertEquals('RESPONSEBODY', $response);
+ }
+
+ public function testCurlWrapperMethodsRemainUntyped(): void
+ {
+ $setoptArray = new \ReflectionMethod(Curl::class, 'setoptArray');
+ $close = new \ReflectionMethod(Curl::class, 'close');
+
+ $this->assertFalse($setoptArray->hasReturnType());
+ $this->assertFalse($close->hasReturnType());
+ }
+
public function testConnectionFailureReturnsError(): void
{
CurlPostGlobalState::$execResponse = false;
diff --git a/tests/ReCaptcha/RequestMethod/SocketPostTest.php b/tests/ReCaptcha/RequestMethod/SocketPostTest.php
index ec6193e..8e7181d 100644
--- a/tests/ReCaptcha/RequestMethod/SocketPostTest.php
+++ b/tests/ReCaptcha/RequestMethod/SocketPostTest.php
@@ -182,6 +182,52 @@ public function testOverrideSiteVerifyUrl(): void
$this->assertTrue(SocketPostGlobalState::$fcloseCalled);
}
+ public function testLegacyConstructorOverrideSiteVerifyUrl(): void
+ {
+ SocketPostGlobalState::$fgetsResponses = [
+ "HTTP/1.0 200 OK\r\n",
+ "Content-Type: application/json\r\n",
+ "\r\n",
+ 'RESPONSEBODY',
+ ];
+
+ $url = 'https://custom.recaptcha.net/recaptcha/api/siteverify';
+ $sp = new SocketPost(null, $url);
+ $response = $sp->submit(new RequestParameters('secret', 'response'));
+
+ $this->assertEquals('ssl://custom.recaptcha.net', SocketPostGlobalState::$fsockopenHostname);
+ $this->assertStringContainsString('POST /recaptcha/api/siteverify HTTP/1.0', SocketPostGlobalState::$fwriteData);
+ $this->assertEquals('RESPONSEBODY', $response);
+ $this->assertTrue(SocketPostGlobalState::$fcloseCalled);
+ }
+
+ public function testLegacyNamedArgumentsCanInjectSocketWrapper(): void
+ {
+ SocketPostGlobalState::$fgetsResponses = [
+ "HTTP/1.0 200 OK\r\n",
+ "Content-Type: application/json\r\n",
+ "\r\n",
+ 'RESPONSEBODY',
+ ];
+
+ $url = 'https://custom.recaptcha.net/recaptcha/api/siteverify';
+ $sp = new SocketPost(socket: new Socket(), siteVerifyUrl: $url);
+ $response = $sp->submit(new RequestParameters('secret', 'response'));
+
+ $this->assertEquals('ssl://custom.recaptcha.net', SocketPostGlobalState::$fsockopenHostname);
+ $this->assertStringContainsString('POST /recaptcha/api/siteverify HTTP/1.0', SocketPostGlobalState::$fwriteData);
+ $this->assertEquals('RESPONSEBODY', $response);
+ $this->assertTrue(SocketPostGlobalState::$fcloseCalled);
+ }
+
+ public function testSocketWrapperKeepsLegacyMethods(): void
+ {
+ $class = new \ReflectionClass(Socket::class);
+
+ $this->assertTrue($class->hasMethod('fgets'));
+ $this->assertTrue($class->hasMethod('feof'));
+ }
+
public function testSubmitReturnsResponseWhenHttp11(): void
{
SocketPostGlobalState::$fgetsResponses = [
@@ -205,6 +251,7 @@ public function testStreamTimeoutFailureReturnsError(): void
$response = $sp->submit(new RequestParameters('secret', 'response'));
$this->assertEquals('{"success": false, "error-codes": ["'.ReCaptcha::E_CONNECTION_FAILED.'"]}', $response);
+ $this->assertTrue(SocketPostGlobalState::$fcloseCalled);
}
public function testConnectionFailureWithValidHandleReturnsError(): void
@@ -216,6 +263,7 @@ public function testConnectionFailureWithValidHandleReturnsError(): void
$response = $sp->submit(new RequestParameters('secret', 'response'));
$this->assertEquals('{"success": false, "error-codes": ["'.ReCaptcha::E_CONNECTION_FAILED.'"]}', $response);
+ $this->assertTrue(SocketPostGlobalState::$fcloseCalled);
}
public function testUrlFailureReturnsError(): void
diff --git a/tests/ReCaptcha/RequestParametersTest.php b/tests/ReCaptcha/RequestParametersTest.php
index 8b5bcdc..194bbe9 100644
--- a/tests/ReCaptcha/RequestParametersTest.php
+++ b/tests/ReCaptcha/RequestParametersTest.php
@@ -87,4 +87,11 @@ public static function provideValidData(): array
],
];
}
+
+ public function testClassRemainsExtendable(): void
+ {
+ $class = new \ReflectionClass(RequestParameters::class);
+
+ $this->assertFalse($class->isReadOnly());
+ }
}
diff --git a/tests/ReCaptcha/ResponseTest.php b/tests/ReCaptcha/ResponseTest.php
index c18a8a4..45b8bdc 100644
--- a/tests/ReCaptcha/ResponseTest.php
+++ b/tests/ReCaptcha/ResponseTest.php
@@ -53,7 +53,7 @@ class ResponseTest extends TestCase
* @param array $errorCodes
*/
#[DataProvider('provideJson')]
- public function testFromJson(string $json, bool $success, array $errorCodes, ?string $hostname, ?string $challengeTs, ?string $apkPackageName, ?float $score, ?string $action): void
+ public function testFromJson(mixed $json, bool $success, array $errorCodes, ?string $hostname, ?string $challengeTs, ?string $apkPackageName, ?float $score, ?string $action): void
{
$response = Response::fromJson($json);
$this->assertEquals($success, $response->isSuccess());
@@ -66,7 +66,7 @@ public function testFromJson(string $json, bool $success, array $errorCodes, ?st
}
/**
- * @return array, 3: null|string, 4: null|string, 5: null|string, 6: null|float, 7: null|string}>
+ * @return array, 3: null|string, 4: null|string, 5: null|string, 6: null|float, 7: null|string}>
*/
public static function provideJson(): array
{
@@ -99,10 +99,22 @@ public static function provideJson(): array
'{"success": true, "error-codes": ["test"], "hostname": "google.com"}',
true, [], 'google.com', null, null, null, null,
],
+ [
+ '{"success": "true"}',
+ true, [], null, null, null, null, null,
+ ],
+ [
+ '{"success": 1}',
+ true, [], null, null, null, null, null,
+ ],
[
'{"success": false}',
false, [ReCaptcha::E_UNKNOWN_ERROR], null, null, null, null, null,
],
+ [
+ '{}',
+ false, [ReCaptcha::E_INVALID_JSON], null, null, null, null, null,
+ ],
[
'{"success": false, "hostname": "google.com"}',
false, [ReCaptcha::E_UNKNOWN_ERROR], 'google.com', null, null, null, null,
@@ -111,6 +123,10 @@ public static function provideJson(): array
'BAD JSON',
false, [ReCaptcha::E_INVALID_JSON], null, null, null, null, null,
],
+ [
+ null,
+ false, [ReCaptcha::E_INVALID_JSON], null, null, null, null, null,
+ ],
[
'{"success": false, "error-codes": "invalid-input-secret"}',
false, [ReCaptcha::E_UNKNOWN_ERROR], null, null, null, null, null,
@@ -126,6 +142,13 @@ public static function provideJson(): array
];
}
+ public function testClassRemainsExtendable(): void
+ {
+ $class = new \ReflectionClass(Response::class);
+
+ $this->assertFalse($class->isReadOnly());
+ }
+
public function testIsSuccess(): void
{
$response = new Response(true);