diff --git a/README.md b/README.md index 6158176..a5967b6 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Captchavel by default works on `auto` mode, allowing you minimal configuration i ### Frontend -Just add the `data-recaptcha="true"` attribute to the forms where you want to have the reCAPTCHA check. The script will detect these forms an add a reCAPTCHA token to them so they can be checked in the backend. +Just add the `data-recaptcha="true"` attribute to the forms where you want to have the reCAPTCHA check. A JavaScript will be injected in all your responses that will detect these forms an add a reCAPTCHA token to them so they can be checked in the backend. ```blade
``` -The Google reCAPTCHA script file from Google will be automatically injected on all responses for better analytics. +The Google reCAPTCHA script from Google will be automatically injected on all responses for better analytics. > Check the `manual` mode if you want control on how to deal with the frontend reCAPTCHA script. @@ -50,6 +50,8 @@ The Google reCAPTCHA script file from Google will be automatically injected on a After that, you should add the `recaptcha` middleware inside your controllers that will receive input and you want to *protect* with the reCAPTCHA check. +You can use the `isHuman()` and `isRobot()` methods in the Request instance to check if the request was made by a human or a robot, respectively. + ```php 'required|string|exists:users,username' ]); - // ... + if ($request->isRobot()) { + return response()->view('web.user.pending_approval'); + } return response()->view('web.user.success'); } - - // ... } ``` @@ -105,7 +107,8 @@ Route::post('form')->uses('CustomController@form')->middleware('recaptcha'); ### Accessing the reCAPTCHA response -You can access the reCAPTCHA response in four ways ways: +You can access the reCAPTCHA response in four ways: + * using [dependency injection](https://laravel.com/docs/container#automatic-injection), * using the `ReCaptcha` facade anywhere in your code, * the `recaptcha()` helper, @@ -139,6 +142,7 @@ class CustomController extends Controller * @param \Illuminate\Http\Request $request * @param \DarkGhostHunter\Captchavel\ReCaptcha $reCaptcha * @return \Illuminate\Http\Response + * @throws \DarkGhostHunter\Captchavel\Exceptions\RecaptchaNotResolvedException */ public function form(Request $request, ReCaptcha $reCaptcha) { @@ -164,6 +168,8 @@ The class has handy methods you can use to check the status of the reCAPTCHA inf * `isRobot()`: Detects if the Request has been made by a Robot (below threshold). * `since()`: Returns the time the reCAPTCHA challenge was resolved as a Carbon timestamp. +> If you try to check if the response while the reCAPTCHA wasn't resolved, you will get a `RecaptchaNotResolvedException`. + ## Local development and robot requests When developing, this package registers a transparent middleware that allows you to work on your application without contacting reCAPTCHA servers ever. Instead, it will always generate a successful "dummy" response with a `1.0` score. @@ -191,7 +197,7 @@ POST http://myapp.com/login?is_robot If you want to connect to the reCAPTCHA servers on `local` environment, you can set the `CAPTCHAVEL_LOCAL=true` in your `.env` file. -> The transparent middleware also registers itself on testing environment, so you can test your application using requests made by a robot and made by a human just adding an empty `_recaptcha` input. +> The transparent middleware also registers itself on testing environment, so you can test your application using requests made by a robot and made by a human just adding an empty `_recaptcha` input. Sweet! ## Configuration @@ -268,7 +274,7 @@ return [ ### Key and Secret -There parameters are self-explanatory. One is the reCAPTCHA Site Key, which is shown publicly in your views, and the Secret, which is used to recover the user interaction information privately inside your application. +These parameters are self-explanatory. One is the reCAPTCHA Site Key, which is shown publicly in your views, and the Secret, which is used to recover the user interaction information privately inside your application. If you don't have them, use the [Google reCAPTCHA Admin console](https://g.co/recaptcha/admin) to create a pair. @@ -289,8 +295,12 @@ Aside from that, you can also override the score using a parameter within the `r use Illuminate\Support\Facades\Route; -Route::post('{product]/review') - ->uses('ReviewController@create') +Route::post('{product}/comments') + ->uses('Product/CommentController@create') + ->middleware('recaptcha:0.3'); + +Route::post('{product}/review') + ->uses('Product/ReviewController@create') ->middleware('recaptcha:0.8'); ``` diff --git a/src/CaptchavelServiceProvider.php b/src/CaptchavelServiceProvider.php index 8054986..3310b52 100644 --- a/src/CaptchavelServiceProvider.php +++ b/src/CaptchavelServiceProvider.php @@ -6,12 +6,33 @@ use DarkGhostHunter\Captchavel\Http\Middleware\InjectRecaptchaScript; use DarkGhostHunter\Captchavel\Http\Middleware\TransparentRecaptcha; use Illuminate\Contracts\Http\Kernel; +use Illuminate\Http\Request; use Illuminate\Routing\Router; use Illuminate\Support\ServiceProvider; use ReCaptcha\ReCaptcha as ReCaptchaFactory; class CaptchavelServiceProvider extends ServiceProvider { + /** + * Register the application services. + * + * @return void + */ + public function register() + { + // Automatically apply the package configuration + $this->mergeConfigFrom(__DIR__ . '/../config/captchavel.php', 'captchavel'); + + // When the application tries to resolve the ReCaptcha instance, we will pass the Site Key. + $this->app->when(ReCaptchaFactory::class) + ->needs('$secret') + ->give(function ($app) { + return $app->make('config')->get('captchavel.secret'); + }); + + $this->app->singleton('recaptcha', ReCaptcha::class); + } + /** * Bootstrap the application services. * @@ -19,46 +40,21 @@ class CaptchavelServiceProvider extends ServiceProvider */ public function boot() { - /* - * Optional methods to load your package assets - */ - // $this->loadTranslationsFrom(__DIR__.'/../resources/lang', 'captchavel'); - $this->loadViewsFrom(__DIR__.'/../resources/views', 'captchavel'); - // $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); - // $this->loadRoutesFrom(__DIR__.'/routes.php'); + $this->loadViewsFrom(__DIR__ . '/../resources/views', 'captchavel'); if ($this->app->runningInConsole()) { $this->publishes([ - __DIR__.'/../config/captchavel.php' => config_path('captchavel.php'), + __DIR__ . '/../config/captchavel.php' => config_path('captchavel.php'), ], 'config'); // Publishing the views. $this->publishes([ - __DIR__.'/../resources/views' => resource_path('views/vendor/captchavel'), + __DIR__ . '/../resources/views' => resource_path('views/vendor/captchavel'), ], 'views'); } $this->bootMiddleware(); - } - - /** - * Register the application services. - * - * @void - */ - public function register() - { - // Automatically apply the package configuration - $this->mergeConfigFrom(__DIR__.'/../config/captchavel.php', 'captchavel'); - - // When the application tries to resolve the ReCaptcha instance, we will pass the Site Key. - $this->app->when(ReCaptchaFactory::class) - ->needs('$secret') - ->give(function ($app) { - return $app->make('config')->get('captchavel.secret'); - }); - - $this->app->singleton('recaptcha', ReCaptcha::class); + $this->extendRequestMacro(); } /** @@ -76,9 +72,9 @@ protected function bootMiddleware() // and package config. If we shouldn't, we will register a transparent middleware in the // application to avoid the errors when the "recaptcha" is used but not registered. if ($this->shouldEnableMiddleware()) { - $this->registerMiddleware($router); + $this->addMiddleware($router); } else { - $this->registerTransparentMiddleware($router); + $this->addTransparentMiddleware($router); } } @@ -97,10 +93,10 @@ protected function shouldEnableMiddleware() /** * Registers real middleware for the package * - * @param \Illuminate\Routing\Router $router + * @param \Illuminate\Routing\Router $router * @throws \Illuminate\Contracts\Container\BindingResolutionException */ - protected function registerMiddleware(Router $router) + protected function addMiddleware(Router $router) { $router->aliasMiddleware('recaptcha', CheckRecaptcha::class); $router->aliasMiddleware('recaptcha-inject', InjectRecaptchaScript::class); @@ -113,11 +109,27 @@ protected function registerMiddleware(Router $router) /** * Registers a Dummy (Transparent) Middleware * - * @param \Illuminate\Routing\Router $router + * @param \Illuminate\Routing\Router $router */ - protected function registerTransparentMiddleware(Router $router) + protected function addTransparentMiddleware(Router $router) { $router->aliasMiddleware('recaptcha', TransparentRecaptcha::class); } + /** + * Extend the Request with a couple of macros + * + * @return void + */ + protected function extendRequestMacro() + { + Request::macro('isHuman', function () { + return recaptcha()->isHuman(); + }); + + Request::macro('isRobot', function () { + return recaptcha()->isRobot(); + }); + } + } diff --git a/src/Exceptions/CaptchavelException.php b/src/Exceptions/CaptchavelException.php new file mode 100644 index 0000000..aa3e48a --- /dev/null +++ b/src/Exceptions/CaptchavelException.php @@ -0,0 +1,8 @@ +message = "The Google reCAPTCHA library returned the following errors: \n" . + $this->message = 'The Google reCAPTCHA library returned the following errors:' . implode("\n- ", $errorCodes); + + parent::__construct(); } } \ No newline at end of file diff --git a/src/Exceptions/InvalidCaptchavelMiddlewareMethod.php b/src/Exceptions/InvalidCaptchavelMiddlewareMethod.php index 473ead8..29cac7e 100644 --- a/src/Exceptions/InvalidCaptchavelMiddlewareMethod.php +++ b/src/Exceptions/InvalidCaptchavelMiddlewareMethod.php @@ -4,7 +4,7 @@ use Exception; -class InvalidCaptchavelMiddlewareMethod extends Exception +class InvalidCaptchavelMiddlewareMethod extends Exception implements CaptchavelException { protected $message = 'Captchavel does not work in GET routes.'; } \ No newline at end of file diff --git a/src/Exceptions/InvalidRecaptchaException.php b/src/Exceptions/InvalidRecaptchaException.php index 0ec9b10..43e3098 100644 --- a/src/Exceptions/InvalidRecaptchaException.php +++ b/src/Exceptions/InvalidRecaptchaException.php @@ -5,7 +5,7 @@ use Exception; use Throwable; -class InvalidRecaptchaException extends Exception +class InvalidRecaptchaException extends Exception implements CaptchavelException { protected $message = 'The reCAPTCHA token is empty'; diff --git a/src/Exceptions/RecaptchaNotResolvedException.php b/src/Exceptions/RecaptchaNotResolvedException.php new file mode 100644 index 0000000..fe4150d --- /dev/null +++ b/src/Exceptions/RecaptchaNotResolvedException.php @@ -0,0 +1,10 @@ +reCaptcha->setResponse( $this->reCaptchaFactory ->setExpectedAction($this->sanitizeAction($request->getRequestUri())) - ->setScoreThreshold($threshold) ->verify($request->input('_recaptcha'), $request->getClientIp()) - ); + )->setThreshold($threshold); } /** diff --git a/src/ReCaptcha.php b/src/ReCaptcha.php index b78eee1..14a3cf7 100644 --- a/src/ReCaptcha.php +++ b/src/ReCaptcha.php @@ -2,6 +2,7 @@ namespace DarkGhostHunter\Captchavel; +use DarkGhostHunter\Captchavel\Exceptions\RecaptchaNotResolvedException; use Illuminate\Support\Carbon; use ReCaptcha\Response; @@ -48,7 +49,7 @@ public function setResponse(Response $response) */ public function isResolved() { - return !is_null($this->response); + return $this->response !== null; } /** @@ -78,9 +79,14 @@ public function setThreshold(float $threshold) * Return if the Response was made by a Human * * @return bool + * @throws \DarkGhostHunter\Captchavel\Exceptions\RecaptchaNotResolvedException */ public function isHuman() { + if (!$this->response) { + throw new RecaptchaNotResolvedException(); + } + return $this->response->getScore() >= $this->threshold; } @@ -88,6 +94,7 @@ public function isHuman() * Return if the Response was made by a Robot * * @return bool + * @throws \DarkGhostHunter\Captchavel\Exceptions\RecaptchaNotResolvedException */ public function isRobot() { @@ -108,9 +115,14 @@ public function response() * Return the reCAPTCHA Response timestamp as a Carbon instance * * @return \Illuminate\Support\Carbon + * @throws \DarkGhostHunter\Captchavel\Exceptions\RecaptchaNotResolvedException */ public function since() { + if (!$this->response) { + throw new RecaptchaNotResolvedException(); + } + return $this->since ?? $this->since = Carbon::parse($this->response->getChallengeTs()); } } diff --git a/tests/Middleware/CheckRecaptchaTest.php b/tests/Middleware/CheckRecaptchaTest.php index ea1a039..869c74c 100644 --- a/tests/Middleware/CheckRecaptchaTest.php +++ b/tests/Middleware/CheckRecaptchaTest.php @@ -120,13 +120,6 @@ public function testMiddlewareAcceptsParameter() $mockReCaptchaFactory->shouldReceive('setExpectedAction') ->once() ->andReturnSelf(); - $mockReCaptchaFactory->shouldReceive('setScoreThreshold') - ->once() - ->withArgs(function ($threshold) { - $this->assertEquals(0.9, $threshold); - return true; - }) - ->andReturnSelf(); $mockReCaptchaFactory->shouldReceive('verify') ->once() ->andReturn(new Response(true, [], null, null, null, 1.0, null)); diff --git a/tests/RecaptchaResponseHolderTest.php b/tests/RecaptchaResponseHolderTest.php index 8cf9efb..2686ad9 100644 --- a/tests/RecaptchaResponseHolderTest.php +++ b/tests/RecaptchaResponseHolderTest.php @@ -2,6 +2,7 @@ namespace DarkGhostHunter\Captchavel\Tests; +use DarkGhostHunter\Captchavel\Exceptions\RecaptchaNotResolvedException; use DarkGhostHunter\Captchavel\ReCaptcha; use Illuminate\Support\Carbon; use Orchestra\Testbench\TestCase; @@ -99,4 +100,25 @@ public function testIsResolved() $this->assertTrue($holder->isResolved()); } + public function testExceptionOnHumanCheck() + { + $this->expectException(RecaptchaNotResolvedException::class); + + (new ReCaptcha())->isHuman(); + } + + public function testExceptionOnRobotCheck() + { + $this->expectException(RecaptchaNotResolvedException::class); + + (new ReCaptcha())->isRobot(); + } + + public function testExceptionOnSinceCheck() + { + $this->expectException(RecaptchaNotResolvedException::class); + + (new ReCaptcha())->since(); + } + } diff --git a/tests/ServiceProviderTest.php b/tests/ServiceProviderTest.php index eaf341e..ce22dbd 100644 --- a/tests/ServiceProviderTest.php +++ b/tests/ServiceProviderTest.php @@ -8,6 +8,7 @@ use DarkGhostHunter\Captchavel\Http\Middleware\TransparentRecaptcha; use DarkGhostHunter\Captchavel\ReCaptcha; use Illuminate\Contracts\Http\Kernel; +use Illuminate\Support\Facades\Request; use Illuminate\Support\Str; use Orchestra\Testbench\TestCase; use ReCaptcha\ReCaptcha as ReCaptchaFactory; @@ -160,4 +161,18 @@ public function testPublishesConfigFile() $this->assertTrue(unlink(config_path('captchavel.php'))); } + public function testRegistersMacros() + { + \DarkGhostHunter\Captchavel\Facades\ReCaptcha::shouldReceive('isHuman') + ->once() + ->andReturnTrue(); + + \DarkGhostHunter\Captchavel\Facades\ReCaptcha::shouldReceive('isRobot') + ->once() + ->andReturnFalse(); + + $this->assertTrue(Request::isHuman()); + $this->assertFalse(Request::isRobot()); + } + }