From 949ee9b342da249a66390ede5eb8a0ab0e742870 Mon Sep 17 00:00:00 2001 From: Rowan Merewood Date: Tue, 24 Mar 2026 15:47:38 +0000 Subject: [PATCH 1/6] PHPStan configuration --- composer.json | 4 +++- composer.lock | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++- phpstan.neon | 6 ++++++ 3 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 phpstan.neon diff --git a/composer.json b/composer.json index 70a0c1c..8569fdb 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,8 @@ "require-dev": { "phpunit/phpunit": "^13", "friendsofphp/php-cs-fixer": "^3.94", - "php-coveralls/php-coveralls": "^2.9" + "php-coveralls/php-coveralls": "^2.9", + "phpstan/phpstan": "^2.1" }, "autoload": { "psr-4": { @@ -30,6 +31,7 @@ "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", "serve-examples": "@php -S localhost:8080 -t examples" }, diff --git a/composer.lock b/composer.lock index 81aea7b..789a78c 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": "b99f580de2abf676796df50ae6addd59", + "content-hash": "edc273ca7a74db1ddeb047d4e20e7351", "packages": [], "packages-dev": [ { @@ -1150,6 +1150,59 @@ }, "time": "2025-12-18T13:08:37+00:00" }, + { + "name": "phpstan/phpstan", + "version": "2.1.42", + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/1279e1ce86ba768f0780c9d889852b4e02ff40d0", + "reference": "1279e1ce86ba768f0780c9d889852b4e02ff40d0", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2026-03-17T14:58:32+00:00" + }, { "name": "phpunit/php-code-coverage", "version": "13.0.1", diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..efbd19a --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,6 @@ +parameters: + level: max + paths: + - src + - tests + - examples From 14b243b7d3e256f2a8cf70c3ac04fa1b9ac7a75f Mon Sep 17 00:00:00 2001 From: Rowan Merewood Date: Tue, 24 Mar 2026 15:57:33 +0000 Subject: [PATCH 2/6] Type hint updates for ReCaptcha --- src/ReCaptcha/ReCaptcha.php | 36 ++++++++++++--------------- src/ReCaptcha/Response.php | 41 ++++++++++++++++--------------- tests/ReCaptcha/ReCaptchaTest.php | 21 +++++++++++++--- 3 files changed, 54 insertions(+), 44 deletions(-) diff --git a/src/ReCaptcha/ReCaptcha.php b/src/ReCaptcha/ReCaptcha.php index 5dd19f0..1cf5298 100644 --- a/src/ReCaptcha/ReCaptcha.php +++ b/src/ReCaptcha/ReCaptcha.php @@ -138,20 +138,20 @@ class ReCaptcha * * @var string */ - private $secret; + private string $secret; /** * Method used to communicate with service. Defaults to POST request. * * @var RequestMethod */ - private $requestMethod; + private RequestMethod $requestMethod; - private $hostname; - private $apkPackageName; - private $action; - private $threshold; - private $timeoutSeconds; + private string $hostname; + private string $apkPackageName; + private string $action; + private float $threshold; + private int $timeoutSeconds; /** * Create a configured instance to use the reCAPTCHA service. @@ -161,16 +161,12 @@ class ReCaptcha * * @throws \RuntimeException if $secret is invalid */ - public function __construct($secret, ?RequestMethod $requestMethod = null) + public function __construct(string $secret, ?RequestMethod $requestMethod = null) { if (empty($secret)) { throw new \RuntimeException('No secret provided'); } - if (!is_string($secret)) { - throw new \RuntimeException('The provided secret must be a string'); - } - $this->secret = $secret; if (!is_null($requestMethod)) { @@ -187,11 +183,11 @@ public function __construct($secret, ?RequestMethod $requestMethod = null) * CAPTCHA test and additionally runs any specified additional checks. * * @param string $response the user response token provided by reCAPTCHA, verifying the user on your site - * @param string $remoteIp the end user's IP address + * @param string|null $remoteIp the end user's IP address * * @return Response response from the service */ - public function verify($response, $remoteIp = null) + public function verify(string $response, ?string $remoteIp = null): Response { // Discard empty solution submissions if (empty($response)) { @@ -250,7 +246,7 @@ public function verify($response, $remoteIp = null) * * @return ReCaptcha Current instance for fluent interface */ - public function setExpectedHostname($hostname) + public function setExpectedHostname(string $hostname): self { $this->hostname = $hostname; @@ -264,7 +260,7 @@ public function setExpectedHostname($hostname) * * @return ReCaptcha Current instance for fluent interface */ - public function setExpectedApkPackageName($apkPackageName) + public function setExpectedApkPackageName(string $apkPackageName): self { $this->apkPackageName = $apkPackageName; @@ -279,7 +275,7 @@ public function setExpectedApkPackageName($apkPackageName) * * @return ReCaptcha Current instance for fluent interface */ - public function setExpectedAction($action) + public function setExpectedAction(string $action): self { $this->action = $action; @@ -294,9 +290,9 @@ public function setExpectedAction($action) * * @return ReCaptcha Current instance for fluent interface */ - public function setScoreThreshold($threshold) + public function setScoreThreshold(float $threshold): self { - $this->threshold = floatval($threshold); + $this->threshold = $threshold; return $this; } @@ -308,7 +304,7 @@ public function setScoreThreshold($threshold) * * @return ReCaptcha Current instance for fluent interface */ - public function setChallengeTimeout($timeoutSeconds) + public function setChallengeTimeout(int $timeoutSeconds): self { $this->timeoutSeconds = $timeoutSeconds; diff --git a/src/ReCaptcha/Response.php b/src/ReCaptcha/Response.php index 4876338..774a5d6 100644 --- a/src/ReCaptcha/Response.php +++ b/src/ReCaptcha/Response.php @@ -47,61 +47,62 @@ class Response * * @var bool */ - private $success = false; + private bool $success = false; /** * Error code strings. * * @var array */ - private $errorCodes = []; + private array $errorCodes = []; /** * The hostname of the site where the reCAPTCHA was solved. * * @var string */ - private $hostname; + private string $hostname; /** * Timestamp of the challenge load (ISO format yyyy-MM-dd'T'HH:mm:ssZZ). * * @var string */ - private $challengeTs; + private string $challengeTs; /** * APK package name. * * @var string */ - private $apkPackageName; + private string $apkPackageName; /** * Score assigned to the request. * - * @var float + * @var float|null */ - private $score; + private ?float $score; /** * Action as specified by the page. * * @var string */ - private $action; + private string $action; /** * Constructor. * * @param bool $success + * @param array $errorCodes * @param string $hostname * @param string $challengeTs * @param string $apkPackageName - * @param float $score + * @param float|null $score * @param string $action */ - public function __construct($success, array $errorCodes = [], $hostname = '', $challengeTs = '', $apkPackageName = '', $score = null, $action = '') + public function __construct(bool $success, array $errorCodes = [], string $hostname = '', string $challengeTs = '', string $apkPackageName = '', ?float $score = null, string $action = '') { $this->success = $success; $this->hostname = $hostname; @@ -119,7 +120,7 @@ public function __construct($success, array $errorCodes = [], $hostname = '', $c * * @return Response */ - public static function fromJson($json) + public static function fromJson(string $json): Response { $responseData = json_decode($json, true); @@ -149,7 +150,7 @@ public static function fromJson($json) * * @return bool */ - public function isSuccess() + public function isSuccess(): bool { return $this->success; } @@ -159,7 +160,7 @@ public function isSuccess() * * @return array */ - public function getErrorCodes() + public function getErrorCodes(): array { return $this->errorCodes; } @@ -169,7 +170,7 @@ public function getErrorCodes() * * @return string */ - public function getHostname() + public function getHostname(): string { return $this->hostname; } @@ -179,7 +180,7 @@ public function getHostname() * * @return string */ - public function getChallengeTs() + public function getChallengeTs(): string { return $this->challengeTs; } @@ -189,7 +190,7 @@ public function getChallengeTs() * * @return string */ - public function getApkPackageName() + public function getApkPackageName(): string { return $this->apkPackageName; } @@ -197,9 +198,9 @@ public function getApkPackageName() /** * Get score. * - * @return float + * @return float|null */ - public function getScore() + public function getScore(): ?float { return $this->score; } @@ -209,7 +210,7 @@ public function getScore() * * @return string */ - public function getAction() + public function getAction(): string { return $this->action; } @@ -227,7 +228,7 @@ public function getAction() * error-codes: string[] * } */ - public function toArray() + public function toArray(): array { return [ 'success' => $this->isSuccess(), diff --git a/tests/ReCaptcha/ReCaptchaTest.php b/tests/ReCaptcha/ReCaptchaTest.php index 405a34f..004a727 100644 --- a/tests/ReCaptcha/ReCaptchaTest.php +++ b/tests/ReCaptcha/ReCaptchaTest.php @@ -75,23 +75,36 @@ protected function tearDown(): void } #[DataProvider('invalidSecretProvider')] - public function testExceptionThrownOnInvalidSecret($invalid) + public function testExceptionThrownOnInvalidSecretType($invalid) { - $this->expectException(\RuntimeException::class); + $this->expectException(\TypeError::class); $rc = new ReCaptcha($invalid); } public static function invalidSecretProvider() { return [ - [''], [null], - [0], [new \stdClass()], [[]], ]; } + #[DataProvider('emptySecretProvider')] + public function testExceptionThrownOnEmptySecret($emptySecret) + { + $this->expectException(\RuntimeException::class); + $rc = new ReCaptcha($emptySecret); + } + + public static function emptySecretProvider() + { + return [ + [''], + [0], + ]; + } + public function testVerifyReturnsErrorOnMissingResponse() { $rc = new ReCaptcha('secret'); From acdefde10227bcf8ac25fee5329182d297041499 Mon Sep 17 00:00:00 2001 From: Rowan Merewood Date: Tue, 24 Mar 2026 19:20:35 +0000 Subject: [PATCH 3/6] Type hint updates --- examples/appengine-https.php | 8 +- .../recaptcha-content-security-policy.php | 9 +- examples/recaptcha-v2-checkbox-explicit.php | 19 +++- examples/recaptcha-v2-checkbox.php | 19 +++- examples/recaptcha-v2-invisible.php | 19 +++- examples/recaptcha-v3-request-scores.php | 9 +- examples/recaptcha-v3-verify.php | 23 +++- src/ReCaptcha/ReCaptcha.php | 8 +- src/ReCaptcha/RequestMethod.php | 2 +- src/ReCaptcha/RequestMethod/CurlPost.php | 12 +- src/ReCaptcha/RequestMethod/Post.php | 12 +- src/ReCaptcha/RequestMethod/SocketPost.php | 21 +++- src/ReCaptcha/RequestParameters.php | 32 ++---- src/ReCaptcha/Response.php | 59 +++------- tests/ReCaptcha/ReCaptchaTest.php | 70 +++++++----- .../ReCaptcha/RequestMethod/CurlPostTest.php | 34 +++--- tests/ReCaptcha/RequestMethod/PostTest.php | 107 ++++++++++++++---- .../RequestMethod/SocketPostTest.php | 59 ++++------ tests/ReCaptcha/RequestParametersTest.php | 27 +++-- tests/ReCaptcha/ResponseTest.php | 41 ++++--- 20 files changed, 344 insertions(+), 246 deletions(-) diff --git a/examples/appengine-https.php b/examples/appengine-https.php index cee9162..7ef8474 100644 --- a/examples/appengine-https.php +++ b/examples/appengine-https.php @@ -37,7 +37,13 @@ if (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) { if ('http' === $_SERVER['HTTP_X_FORWARDED_PROTO']) { header('HTTP/1.1 301 Moved Permanently'); - header('Location: https://'.$_SERVER['SERVER_NAME'].$_SERVER['REQUEST_URI']); + + /** @var string $serverName */ + $serverName = $_SERVER['SERVER_NAME']; + + /** @var string $requestUri */ + $requestUri = $_SERVER['REQUEST_URI']; + header('Location: https://'.$serverName.$requestUri); exit(0); } diff --git a/examples/recaptcha-content-security-policy.php b/examples/recaptcha-content-security-policy.php index 6498f5e..e4e427f 100644 --- a/examples/recaptcha-content-security-policy.php +++ b/examples/recaptcha-content-security-policy.php @@ -70,7 +70,8 @@ $secret = ''; // Copy the config.php.dist file to config.php and update it with your keys to run the examples -if ('' == $siteKey && is_readable(__DIR__.'/config.php')) { +if (is_readable(__DIR__.'/config.php')) { + /** @var array{v3: array{site: string, secret: string}} $config */ $config = include __DIR__.'/config.php'; $siteKey = $config['v3']['site']; $secret = $config['v3']['secret']; @@ -115,7 +116,7 @@

NOTE:This is a sample implementation, the score returned here is not a reflection on your Google account or type of traffic. In production, refer to the distribution of scores shown in your admin interface and adjust your own threshold accordingly. Do not raise issues regarding the score you see here.

  1. reCAPTCHA script loading
  2. - + @@ -128,7 +129,7 @@ const steps = document.getElementById('recaptcha-steps'); grecaptcha.ready(function() { document.querySelector('.step1').classList.remove('hidden'); - grecaptcha.execute('', {action: ''}).then(function(token) { + grecaptcha.execute('', {action: ''}).then(function(token) { document.querySelector('.token').innerHTML = 'fetch(\'/recaptcha-v3-verify.php?action=&token=\'' + token; document.querySelector('.step2').classList.remove('hidden'); @@ -143,7 +144,7 @@ }; - + diff --git a/examples/recaptcha-v2-checkbox-explicit.php b/examples/recaptcha-v2-checkbox-explicit.php index 1cf637f..3393c68 100644 --- a/examples/recaptcha-v2-checkbox-explicit.php +++ b/examples/recaptcha-v2-checkbox-explicit.php @@ -47,7 +47,8 @@ $secret = ''; // Copy the config.php.dist file to config.php and update it with your keys to run the examples -if ('' == $siteKey && is_readable(__DIR__.'/config.php')) { +if (is_readable(__DIR__.'/config.php')) { + /** @var array{'v2-standard': array{site: string, secret: string}} $config */ $config = include __DIR__.'/config.php'; $siteKey = $config['v2-standard']['site']; $secret = $config['v2-standard']['secret']; @@ -91,6 +92,7 @@ setExpectedHostname($_SERVER['SERVER_NAME']) - ->verify($_POST[ReCaptcha::RESPONSE_KEY], $_SERVER['REMOTE_ADDR']) + /** @var string $serverName */ + $serverName = $_SERVER['SERVER_NAME']; + + /** @var string $responseKey */ + $responseKey = $_POST[ReCaptcha::RESPONSE_KEY]; + + /** @var null|string $remoteAddr */ + $remoteAddr = $_SERVER['REMOTE_ADDR']; + + $resp = $recaptcha->setExpectedHostname($serverName) + ->verify($responseKey, $remoteAddr) ; if ($resp->isSuccess()) { @@ -139,7 +150,7 @@ var onloadCallback = function() { var captchaContainer = document.querySelector('.g-recaptcha'); grecaptcha.render(captchaContainer, { - 'sitekey' : '' + 'sitekey' : '' }); document.querySelector('button[type="submit"]').disabled = false; }; diff --git a/examples/recaptcha-v2-checkbox.php b/examples/recaptcha-v2-checkbox.php index 2031946..25ad3fd 100644 --- a/examples/recaptcha-v2-checkbox.php +++ b/examples/recaptcha-v2-checkbox.php @@ -47,7 +47,8 @@ $secret = ''; // Copy the config.php.dist file to config.php and update it with your keys to run the examples -if ('' == $siteKey && is_readable(__DIR__.'/config.php')) { +if (is_readable(__DIR__.'/config.php')) { + /** @var array{'v2-standard': array{site: string, secret: string}} $config */ $config = include __DIR__.'/config.php'; $siteKey = $config['v2-standard']['site']; $secret = $config['v2-standard']['secret']; @@ -91,6 +92,7 @@ setExpectedHostname($_SERVER['SERVER_NAME']) - ->verify($_POST[ReCaptcha::RESPONSE_KEY], $_SERVER['REMOTE_ADDR']) + /** @var string $serverName */ + $serverName = $_SERVER['SERVER_NAME']; + + /** @var string $responseKey */ + $responseKey = $_POST[ReCaptcha::RESPONSE_KEY]; + + /** @var null|string $remoteAddr */ + $remoteAddr = $_SERVER['REMOTE_ADDR']; + + $resp = $recaptcha->setExpectedHostname($serverName) + ->verify($responseKey, $remoteAddr) ; if ($resp->isSuccess()) { // If the response is a success, that's it! @@ -130,7 +141,7 @@ -
    +
    diff --git a/examples/recaptcha-v2-invisible.php b/examples/recaptcha-v2-invisible.php index 2552c55..f075e6b 100644 --- a/examples/recaptcha-v2-invisible.php +++ b/examples/recaptcha-v2-invisible.php @@ -47,7 +47,8 @@ $secret = ''; // Copy the config.php.dist file to config.php and update it with your keys to run the examples -if ('' == $siteKey && is_readable(__DIR__.'/config.php')) { +if (is_readable(__DIR__.'/config.php')) { + /** @var array{'v2-invisible': array{site: string, secret: string}} $config */ $config = include __DIR__.'/config.php'; $siteKey = $config['v2-invisible']['site']; $secret = $config['v2-invisible']['secret']; @@ -91,6 +92,7 @@ setExpectedHostname($_SERVER['SERVER_NAME']) - ->verify($_POST[ReCaptcha::RESPONSE_KEY], $_SERVER['REMOTE_ADDR']) + /** @var string $serverName */ + $serverName = $_SERVER['SERVER_NAME']; + + /** @var string $responseKey */ + $responseKey = $_POST[ReCaptcha::RESPONSE_KEY]; + + /** @var null|string $remoteAddr */ + $remoteAddr = $_SERVER['REMOTE_ADDR']; + + $resp = $recaptcha->setExpectedHostname($serverName) + ->verify($responseKey, $remoteAddr) ; if ($resp->isSuccess()) { // If the response is a success, that's it! @@ -129,7 +140,7 @@ An example form - + diff --git a/examples/recaptcha-v3-request-scores.php b/examples/recaptcha-v3-request-scores.php index d92a7ee..80368b0 100644 --- a/examples/recaptcha-v3-request-scores.php +++ b/examples/recaptcha-v3-request-scores.php @@ -44,7 +44,8 @@ $secret = ''; // Copy the config.php.dist file to config.php and update it with your keys to run the examples -if ('' == $siteKey && is_readable(__DIR__.'/config.php')) { +if (is_readable(__DIR__.'/config.php')) { + /** @var array{v3: array{site: string, secret: string}} $config */ $config = include __DIR__.'/config.php'; $siteKey = $config['v3']['site']; $secret = $config['v3']['secret']; @@ -90,18 +91,18 @@

    NOTE:This is a sample implementation, the score returned here is not a reflection on your Google account or type of traffic. In production, refer to the distribution of scores shown in your admin interface and adjust your own threshold accordingly. Do not raise issues regarding the score you see here.

    1. reCAPTCHA script loading
    2. - +

    Try again

    - +