├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── php.yml ├── .gitignore ├── routes └── larapoke.php ├── tests ├── RegistersPackages.php ├── ScaffoldAuth.php └── Unit │ ├── Script │ ├── ScriptTest.php │ └── ScriptRouteTest.php │ ├── LarapokeServiceProviderTest.php │ ├── Routes │ └── RouteGeneratorTest.php │ └── Modes │ ├── ModeBladeTest.php │ ├── ModeAutoTest.php │ └── ModeMiddlewareTest.php ├── src ├── Http │ ├── Controllers │ │ └── LarapokeController.php │ ├── Middleware │ │ ├── LarapokeGlobalMiddleware.php │ │ ├── LarapokeMiddleware.php │ │ └── InjectsLarapokeScript.php │ └── RouteGenerator.php ├── Blade │ └── LarapokeDirective.php └── LarapokeServiceProvider.php ├── phpunit.xml ├── LICENSE ├── resources └── views │ └── script.blade.php ├── composer.json ├── config └── larapoke.php └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # Help me support this package 2 | 3 | ko_fi: DarkGhostHunter 4 | custom: ['https://paypal.me/darkghosthunter'] 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /tests/Browser/console 3 | /tests/Browser/screenshots 4 | composer.phar 5 | composer.lock 6 | .idea 7 | /build/ -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: composer 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "09:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /routes/larapoke.php: -------------------------------------------------------------------------------- 1 | setRoutes(); -------------------------------------------------------------------------------- /tests/RegistersPackages.php: -------------------------------------------------------------------------------- 1 | isInjectable($request, $response)) { 24 | $this->injectScript($response); 25 | } 26 | 27 | return $response; 28 | } 29 | } -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | src/ 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | tests/Unit 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2018] [Italo Israel Baeza Cabrera] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /resources/views/script.blade.php: -------------------------------------------------------------------------------- 1 | 39 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "darkghosthunter/larapoke", 3 | "description": "Keep your forms alive, avoid TokenMismatchException by gently poking your Laravel app", 4 | "minimum-stability": "dev", 5 | "prefer-stable": true, 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Italo Baeza C.", 10 | "email": "darkghosthunter@gmail.com" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=7.4", 15 | "illuminate/http": "^7.0||^8.0", 16 | "illuminate/routing": "^7.0||^8.0", 17 | "illuminate/support": "^7.0||^8.0", 18 | "illuminate/view": "^7.0||^8.0" 19 | }, 20 | "require-dev": { 21 | "mockery/mockery": "^1.3.10||^1.4.2", 22 | "laravel/ui" : "^2.0||^3.0", 23 | "orchestra/testbench": "^5.0||^6.0", 24 | "phpunit/phpunit": "^9.5.4" 25 | }, 26 | "autoload-dev": { 27 | "psr-4": { 28 | "Tests\\": "tests" 29 | } 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "DarkGhostHunter\\Larapoke\\": "src" 34 | } 35 | }, 36 | "extra": { 37 | "laravel": { 38 | "providers": [ 39 | "DarkGhostHunter\\Larapoke\\LarapokeServiceProvider" 40 | ] 41 | } 42 | }, 43 | "scripts": { 44 | "test": "vendor/bin/phpunit --coverage-clover build/logs/clover.xml", 45 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage" 46 | }, 47 | "config": { 48 | "sort-packages": true 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/ScaffoldAuth.php: -------------------------------------------------------------------------------- 1 | cleanScaffold(); 19 | 20 | mkdir($app->basePath('routes'), 0777, true); 21 | mkdir($app->resourcePath('sass'), 0777, true); 22 | mkdir($app->resourcePath('js'), 0777, true); 23 | mkdir($app->path('Http/Controllers'), 0777, true); 24 | 25 | $app[Kernel::class]->call('ui bootstrap --auth'); 26 | } 27 | 28 | /** 29 | * Clear the auth scaffold files 30 | * 31 | * @return void 32 | */ 33 | protected function cleanScaffold() 34 | { 35 | $this->cleanDir(base_path('routes')); 36 | $this->cleanDir(app_path('Http')); 37 | $this->cleanDir(resource_path('js')); 38 | $this->cleanDir(resource_path('sass')); 39 | $this->cleanDir(resource_path('views')); 40 | } 41 | 42 | /** 43 | * Recursive directory cleaning. 44 | * 45 | * @param $dir 46 | * @return bool 47 | */ 48 | protected function cleanDir($dir) 49 | { 50 | if (! is_dir($dir)) { 51 | return true; 52 | } 53 | 54 | $files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST); 55 | 56 | foreach ($files as $fileinfo) { 57 | $fileinfo->isDir() 58 | ? rmdir($fileinfo->getRealPath()) 59 | : unlink($fileinfo->getRealPath()); 60 | } 61 | 62 | return rmdir($dir); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | test: 9 | 10 | runs-on: ubuntu-latest 11 | strategy: 12 | fail-fast: true 13 | matrix: 14 | php: [8.0, 7.4] 15 | laravel: [7.*, 8.*] 16 | dependency-version: [prefer-lowest, prefer-stable] 17 | include: 18 | - laravel: 7.* 19 | testbench: ^5.18 20 | - laravel: 8.* 21 | testbench: ^6.16 22 | 23 | name: PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }} - ${{ matrix.dependency-version }} 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v2 28 | 29 | - name: Setup PHP 30 | uses: shivammathur/setup-php@v2 31 | with: 32 | php-version: ${{ matrix.php }} 33 | extensions: mbstring, intl 34 | coverage: xdebug 35 | 36 | - name: Cache dependencies 37 | uses: actions/cache@v2 38 | with: 39 | path: ~/.composer/cache/files 40 | key: ${{ runner.os }}-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} 41 | restore-keys: ${{ runner.os }}-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer- 42 | 43 | - name: Install dependencies 44 | run: | 45 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-progress --no-update 46 | composer update --${{ matrix.dependency-version }} --prefer-dist --no-progress 47 | 48 | - name: Run Tests 49 | run: composer run-script test 50 | 51 | - name: Upload Coverage to Coveralls 52 | env: 53 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | COVERALLS_SERVICE_NAME: github 55 | run: | 56 | rm -rf composer.* vendor/ 57 | composer require php-coveralls/php-coveralls 58 | vendor/bin/php-coveralls -------------------------------------------------------------------------------- /src/Blade/LarapokeDirective.php: -------------------------------------------------------------------------------- 1 | view = $view; 45 | $this->config = $config; 46 | $this->url = $url; 47 | } 48 | 49 | /** 50 | * Parse de Config and save it 51 | * 52 | * @return array 53 | */ 54 | protected function parseConfig(): array 55 | { 56 | $session = $this->config->get('session.lifetime') * 60 * 1000; 57 | 58 | return [ 59 | 'route' => $this->url->to($this->config->get('larapoke.poking.route')), 60 | 'interval' => (int)($session / $this->config->get('larapoke.times')), 61 | 'lifetime' => $session, 62 | ]; 63 | } 64 | 65 | /** 66 | * Renders the scripts using the Larapoke configuration 67 | * 68 | * @return string 69 | */ 70 | public function toHtml(): string 71 | { 72 | return $this->view->make($this->config->get('larapoke.view'), $this->parseConfig())->render(); 73 | } 74 | } -------------------------------------------------------------------------------- /config/larapoke.php: -------------------------------------------------------------------------------- 1 | env('LARAPOKE_MODE', 'auto'), 18 | 19 | /* 20 | |-------------------------------------------------------------------------- 21 | | Times 22 | |-------------------------------------------------------------------------- 23 | | 24 | | You can set by how much times in the session lifetime the poking will be 25 | | made to your application. For example, the default 120 minutes session 26 | | lifetime, divided by 4 times, means poking at a 30 minutes intervals. 27 | | 28 | */ 29 | 30 | 'times' => 4, 31 | 32 | /* 33 | |-------------------------------------------------------------------------- 34 | | View 35 | |-------------------------------------------------------------------------- 36 | | 37 | | Laravel comes with a Blade template containing the script to keep the 38 | | forms alive. The default is very complete, but if you need something 39 | | more advanced, you can override it with your own, or use another. 40 | | 41 | */ 42 | 43 | 'view' => 'larapoke::script', 44 | 45 | /* 46 | |-------------------------------------------------------------------------- 47 | | Route for Poking 48 | |-------------------------------------------------------------------------- 49 | | 50 | | Here you may specify how the poking route will live in your application. 51 | | You can set a specific route to be hit, a custom name to identify it, 52 | | and a custom subdomain if you don't want to be available app-wide. 53 | | 54 | */ 55 | 56 | 'poking' => [ 57 | 'route' => 'poke', 58 | 'name' => 'larapoke', 59 | 'domain' => null, 60 | 'middleware' => ['web'], 61 | ] 62 | 63 | 64 | ]; -------------------------------------------------------------------------------- /src/Http/Middleware/LarapokeMiddleware.php: -------------------------------------------------------------------------------- 1 | modeIsMiddleware = $config->get('larapoke.mode') === 'middleware'; 29 | } 30 | 31 | /** 32 | * Handle the incoming request. 33 | * 34 | * @param \Illuminate\Http\Request $request 35 | * @param \Closure $next 36 | * @param string|null $detect 37 | * 38 | * @return mixed 39 | */ 40 | public function handle(Request $request, Closure $next, string $detect = null) 41 | { 42 | $response = $next($request); 43 | 44 | // Don't evaluate the response under "json", "auto" or "blade" modes. 45 | if ($response instanceof Response && $this->shouldInjectScript($request, $response, $detect)) { 46 | return $this->injectScript($response); 47 | } 48 | 49 | return $response; 50 | } 51 | 52 | /** 53 | * Determine if we should inject the script into the response. 54 | * 55 | * @param \Illuminate\Http\Request $request 56 | * @param \Illuminate\Http\Response $response 57 | * @param string|null $detect 58 | * 59 | * @return bool 60 | */ 61 | public function shouldInjectScript(Request $request, Response $response, ?string $detect): bool 62 | { 63 | if (! $this->modeIsMiddleware) { 64 | return false; 65 | } 66 | 67 | // Check first if the middleware has to detect if there is a CSRF token 68 | // before injecting the script in the response. When not detecting, 69 | // then we tell to inject the script anyway into the Response. 70 | $injectAnyway = $detect !== 'detect'; 71 | 72 | return $injectAnyway || $this->isInjectable($request, $response); 73 | } 74 | } -------------------------------------------------------------------------------- /src/Http/Middleware/InjectsLarapokeScript.php: -------------------------------------------------------------------------------- 1 | isSuccessful() 22 | && $this->isNormalResponse($response) 23 | && $this->wantsFullHtml($request) 24 | && $this->hasCsrf($response); 25 | } 26 | 27 | /** 28 | * Detect if the Response is normal. 29 | * 30 | * @param \Illuminate\Http\Response $response 31 | * 32 | * @return bool 33 | */ 34 | protected function isNormalResponse(Response $response): bool 35 | { 36 | return $response instanceof Response; 37 | } 38 | 39 | /** 40 | * Return if the Request wants a full page instead of a part (AJAX requests). 41 | * 42 | * @param \Illuminate\Http\Request $request 43 | * @return bool 44 | */ 45 | protected function wantsFullHtml(Request $request): bool 46 | { 47 | return $request->acceptsHtml() && ! $request->ajax() && ! $request->pjax(); 48 | } 49 | 50 | /** 51 | * Detect if the Response has form or CSRF Token. 52 | * 53 | * @param \Illuminate\Http\Response $response 54 | * @return bool 55 | */ 56 | protected function hasCsrf(Response $response): bool 57 | { 58 | $content = $response->content(); 59 | 60 | return strpos($content, 'name="csrf-token"') || strpos($content, 'name="_token"'); 61 | } 62 | 63 | /** 64 | * Sets the Script in the body 65 | * 66 | * @param \Illuminate\Http\Response $response 67 | * @return \Illuminate\Http\Response 68 | */ 69 | protected function injectScript($response): Response 70 | { 71 | // To inject the script automatically, we will do it before the ending 72 | // body tag. If it's not found, the response may not be valid HTML, 73 | // so we will bail out returning the original untouched content. 74 | if (! $endBodyPosition = stripos($content = $response->content(), '')) { 75 | return $response; 76 | } 77 | 78 | return $response->setContent( 79 | substr_replace($content, app(LarapokeDirective::class)->toHtml(), $endBodyPosition, 0) 80 | ); 81 | } 82 | } -------------------------------------------------------------------------------- /src/Http/RouteGenerator.php: -------------------------------------------------------------------------------- 1 | config = $config; 35 | $this->router = $router; 36 | } 37 | 38 | /** 39 | * Parses the configuration from Larapoke 40 | * 41 | * @return array 42 | */ 43 | protected function parseConfig(): array 44 | { 45 | $configs = array_flip([ 46 | 'route', 47 | 'name', 48 | 'domain', 49 | 'middleware', 50 | ]); 51 | 52 | foreach ($configs as $key => &$config) { 53 | $config = $this->config->get('larapoke.poking.'.$key); 54 | } 55 | 56 | return $configs; 57 | } 58 | 59 | /** 60 | * Automatically registers routes 61 | * 62 | * @return void 63 | */ 64 | public function setRoutes() 65 | { 66 | $config = $this->parseConfig(); 67 | 68 | // When the "domain" config is null, we will just register a global route 69 | // that will respond to all domains. Otherwise, we will wrap the value 70 | // and traverse the array to register each to its own domain name. 71 | if ($config['domain'] === null) { 72 | $this->route($config)->name($config['name']); 73 | return; 74 | } 75 | 76 | // If its just one domain, we will register it and then exit 77 | if (is_string($config['domain'])) { 78 | $this->route($config)->name($config['domain'].'.'.$config['name'])->domain($config['domain']); 79 | return; 80 | } 81 | 82 | foreach (Arr::wrap($config['domain']) as $domain) { 83 | $this->route($config)->name($domain.'.'.$config['name'])->domain($domain); 84 | } 85 | } 86 | 87 | /** 88 | * Returns a Larapoke route 89 | * 90 | * @param array $config 91 | * @return \Illuminate\Routing\Route 92 | */ 93 | protected function route(array $config) 94 | { 95 | $route = $this->router 96 | ->match('head', $config['route']) 97 | ->uses(LarapokeController::class); 98 | 99 | $route->middleware($config['middleware']); 100 | 101 | return $route; 102 | } 103 | 104 | } -------------------------------------------------------------------------------- /src/LarapokeServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom(__DIR__ . '/../config/larapoke.php', 'larapoke'); 24 | 25 | $this->app->singleton(LarapokeDirective::class, function ($app) { 26 | return new LarapokeDirective($app['config'], $app['view'], $app['url']); 27 | }); 28 | } 29 | 30 | /** 31 | * Bootstrap any application services. 32 | * 33 | * @param \Illuminate\Routing\Router $router 34 | * @param \Illuminate\Contracts\Config\Repository $config 35 | * @param \Illuminate\View\Compilers\BladeCompiler $blade 36 | * @return void 37 | * @throws \Illuminate\Contracts\Container\BindingResolutionException 38 | */ 39 | public function boot(Router $router, Repository $config, BladeCompiler $blade): void 40 | { 41 | $this->loadRoutesFrom(__DIR__ . '/../routes/larapoke.php'); 42 | $this->loadViewsFrom(__DIR__ . '/../resources/views', 'larapoke'); 43 | 44 | $this->bootMiddleware($router, $config); 45 | 46 | $this->bootBladeDirective($blade); 47 | 48 | if ($this->app->runningInConsole()) { 49 | $this->publishes([__DIR__ . '/../config/larapoke.php' => config_path('larapoke.php')], 'config'); 50 | $this->publishes([__DIR__ . '/../resources/views' => resource_path('views/vendor/larapoke')], 'views'); 51 | } 52 | } 53 | 54 | /** 55 | * Registers (or push globally) the Middleware 56 | * 57 | * @param \Illuminate\Routing\Router $router 58 | * @param \Illuminate\Contracts\Config\Repository $config 59 | * @return void 60 | * @throws \Illuminate\Contracts\Container\BindingResolutionException 61 | */ 62 | protected function bootMiddleware(Router $router, Repository $config): void 63 | { 64 | $router->aliasMiddleware('larapoke', LarapokeMiddleware::class); 65 | 66 | // If Larapoke is set to auto, push the global middleware. 67 | if ($config->get('larapoke.mode') === 'auto') { 68 | $this->app->make(Kernel::class)->pushMiddleware(LarapokeGlobalMiddleware::class); 69 | } 70 | } 71 | 72 | /** 73 | * Registers the Blade Directive 74 | * 75 | * @param \Illuminate\View\Compilers\BladeCompiler $blade 76 | * @return void 77 | */ 78 | protected function bootBladeDirective(BladeCompiler $blade): void 79 | { 80 | $blade->directive('larapoke', function () { 81 | return $this->app->make(LarapokeDirective::class)->toHtml(); 82 | }); 83 | } 84 | } -------------------------------------------------------------------------------- /tests/Unit/Script/ScriptTest.php: -------------------------------------------------------------------------------- 1 | mockConfig = \Mockery::mock(\Illuminate\Config\Repository::class); 29 | 30 | $this->mockView = \Mockery::mock(\Illuminate\View\Factory::class); 31 | 32 | $this->mockUrl = \Mockery::spy(\Illuminate\Contracts\Routing\UrlGenerator::class); 33 | } 34 | 35 | public function testReceivesConfig() 36 | { 37 | $this->mockView 38 | ->shouldReceive('make') 39 | ->with('custom-larapoke-view', \Mockery::type('array')) 40 | ->andReturnUsing(function ($script, $config) { 41 | return new class ($config) 42 | { 43 | protected $config; 44 | 45 | public function __construct($config) 46 | { 47 | $this->config = $config; 48 | } 49 | 50 | public function render() 51 | { 52 | return json_encode($this->config); 53 | } 54 | }; 55 | }); 56 | 57 | $this->mockConfig->shouldReceive('get') 58 | ->with('session.lifetime') 59 | ->andReturn($this->sessionLifetime = rand(10, 240)); 60 | 61 | $this->mockConfig->shouldReceive('get') 62 | ->with('larapoke.poking.route') 63 | ->andReturn($route = 'test-larapoke-route'); 64 | 65 | $this->mockConfig->shouldReceive('get') 66 | ->with('larapoke.times') 67 | ->andReturn($this->times = rand(2, 16)); 68 | 69 | $this->mockConfig->shouldReceive('get') 70 | ->with('larapoke.view') 71 | ->andReturn('custom-larapoke-view'); 72 | 73 | 74 | $this->mockUrl->shouldReceive('to') 75 | ->once() 76 | ->with($route) 77 | ->andReturn('http://test-app.com/'.$route); 78 | 79 | $script = (new LarapokeDirective( 80 | $this->mockConfig, $this->mockView, $this->mockUrl) 81 | )->toHtml(); 82 | 83 | $this->assertEquals( 84 | 'http://test-app.com/test-larapoke-route', 85 | json_decode($script, true)['route'] 86 | ); 87 | $this->assertEquals( 88 | (int)((($this->sessionLifetime * 60 * 1000) / $this->times)), 89 | json_decode($script, true)['interval'] 90 | ); 91 | $this->assertEquals( 92 | $this->sessionLifetime * 60 * 1000, 93 | json_decode($script, true)['lifetime'] 94 | ); 95 | } 96 | } -------------------------------------------------------------------------------- /tests/Unit/LarapokeServiceProviderTest.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'view', 'config' 19 | ] 20 | ]; 21 | 22 | protected function getEnvironmentSetUp($app) 23 | { 24 | $router = $app->make('router'); 25 | 26 | $router->group(['web'], function() use ($router) { 27 | $router->get('/test', function () { 28 | return 'ok'; 29 | }); 30 | }); 31 | } 32 | 33 | public function testReceivesDefaultConfig() 34 | { 35 | $this->assertEquals( 36 | include __DIR__ . '/../../config/larapoke.php', 37 | $this->app['config']['larapoke'] 38 | ); 39 | } 40 | 41 | public function testPublishesConfigFile() 42 | { 43 | $this->artisan('vendor:publish', [ 44 | '--provider' => 'DarkGhostHunter\Larapoke\LarapokeServiceProvider' 45 | ]); 46 | 47 | $this->assertFileExists(config_path('larapoke.php')); 48 | $this->assertFileIsReadable(config_path('larapoke.php')); 49 | $this->assertFileEquals(config_path('larapoke.php'), __DIR__ . '/../../config/larapoke.php'); 50 | $this->assertTrue(unlink(config_path('larapoke.php'))); 51 | } 52 | 53 | public function testLoadDefaultRoute() 54 | { 55 | /** @var \Illuminate\Routing\Router $router */ 56 | $router = $this->app->make('router'); 57 | 58 | /** @var \Illuminate\Routing\Route $route */ 59 | $route = $router->getRoutes()->match( 60 | $this->app->make('request')->create('/poke', 'HEAD') 61 | ); 62 | 63 | $this->assertEquals('larapoke', $route->getName()); 64 | $this->assertInstanceOf(LarapokeController::class, $route->getController()); 65 | } 66 | 67 | public function testLoadDefaultView() 68 | { 69 | $script = $this->app->make('view') 70 | ->make('larapoke::script') 71 | ->with([ 72 | 'route' => '/poke', 73 | 'interval' => 100, 74 | 'timeout' => true, 75 | 'lifetime' => 400000, 76 | ]) 77 | ->render(); 78 | 79 | $this->assertIsString($script); 80 | $this->assertStringContainsString('larapoke_', $script); 81 | } 82 | 83 | public function testRegistersGlobalMiddleware() 84 | { 85 | /** @var \Illuminate\Routing\Router $router */ 86 | $router = $this->app->make('router'); 87 | 88 | $this->assertTrue($this->app->make(Kernel::class)->hasMiddleware(LarapokeGlobalMiddleware::class)); 89 | } 90 | 91 | public function testRegistersMiddlewareAlias() 92 | { 93 | /** @var \Illuminate\Routing\Router $router */ 94 | $router = $this->app->make('router'); 95 | 96 | $this->assertArrayHasKey('larapoke', $router->getMiddleware()); 97 | } 98 | 99 | public function testRegistersBladeDirective() 100 | { 101 | /** @var \Illuminate\View\Factory $view */ 102 | $view = $this->app->make('view'); 103 | 104 | $directives = $view->getEngineResolver() 105 | ->resolve('blade') 106 | ->getCompiler() 107 | ->getCustomDirectives(); 108 | 109 | $this->assertArrayHasKey('larapoke', $directives); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /tests/Unit/Script/ScriptRouteTest.php: -------------------------------------------------------------------------------- 1 | set('session.lifetime', 90); 20 | 21 | $app['config']->set('larapoke', [ 22 | 'mode' => 'auto', 23 | 'times' => 8, 24 | 'timeout' => false, 25 | 'poking' => [ 26 | 'route' => 'test-larapoke-route', 27 | 'name' => 'test-larapoke-name', 28 | 'domain' => 'test-subdomain.app.com', 29 | 'middleware' => ['web', 'testgroup'], 30 | ] 31 | ]); 32 | } 33 | 34 | protected function getEnvironmentSetUp($app) 35 | { 36 | $this->scaffoldAuth($app); 37 | 38 | $app->bind('testgroup', function() { 39 | return new class() { 40 | public function handle($request, $next) 41 | { 42 | return $next($request); 43 | } 44 | }; 45 | }); 46 | } 47 | 48 | protected function tearDown() : void 49 | { 50 | parent::tearDown(); 51 | 52 | $this->cleanScaffold(); 53 | } 54 | 55 | protected function setUp() : void 56 | { 57 | parent::setUp(); 58 | 59 | /** @var \Illuminate\Routing\Router $router */ 60 | $router = $this->app->make('router'); 61 | 62 | $router->group(['middleware' => ['web']], function () use ($router) { 63 | $router->get('/register', function () { 64 | return $this->app->make(Factory::class)->make('auth.register'); 65 | })->name('register'); 66 | $router->get('/login', function () { 67 | return $this->app->make(Factory::class)->make('auth.login'); 68 | })->name('login'); 69 | $router->get('/home', function () { 70 | return $this->app->make(Factory::class)->make('home'); 71 | })->name('home'); 72 | }); 73 | } 74 | 75 | public function testPokeExpired() 76 | { 77 | $content = $this->get('/register')->content(); 78 | 79 | $matches = []; 80 | 81 | preg_match( 82 | '//', 83 | $content, 84 | $matches 85 | ); 86 | 87 | $csrfToken = $matches[1]; 88 | 89 | $this->app->make('session')->flush(); 90 | 91 | $response = $this->get('/test-larapoke-route', [ 92 | '_token' => $csrfToken, 93 | ]); 94 | 95 | $response->assertStatus(404); 96 | } 97 | 98 | public function testDifferentRouteAndSubdomain() 99 | { 100 | $request = $this->call( 101 | 'HEAD', 102 | 'http://test-subdomain.app.com/test-larapoke-route', [], [], [], 103 | $this->transformHeadersToServerVars([]) 104 | ); 105 | 106 | $request->assertStatus(204); 107 | $this->assertEmpty($request->content()); 108 | } 109 | 110 | public function testWrongMethodGives405() 111 | { 112 | foreach (['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] as $method) { 113 | 114 | $request = $this->call( 115 | $method, 116 | 'http://test-subdomain.app.com/test-larapoke-route', [], [], [], 117 | $this->transformHeadersToServerVars([]) 118 | ); 119 | $request->assertStatus(405); 120 | } 121 | } 122 | 123 | public function testHasNamedRoute() 124 | { 125 | $this->assertTrue( 126 | $this->app->make('router')->getRoutes()->hasNamedRoute('test-subdomain.app.com.test-larapoke-name') 127 | ); 128 | } 129 | 130 | public function testHasMiddlewareGroup() 131 | { 132 | /** @var \Illuminate\Routing\Router $router */ 133 | $router = $this->app->make('router'); 134 | 135 | $route = $router->getRoutes()->getByName('test-subdomain.app.com.test-larapoke-name'); 136 | 137 | $this->assertTrue(in_array('testgroup', $route->getAction('middleware'))); 138 | $this->assertTrue(in_array('web', $route->getAction('middleware'))); 139 | } 140 | } -------------------------------------------------------------------------------- /tests/Unit/Routes/RouteGeneratorTest.php: -------------------------------------------------------------------------------- 1 | config = \Mockery::spy(\Illuminate\Contracts\Config\Repository::class); 22 | $this->router = \Mockery::spy(\Illuminate\Routing\Router::class); 23 | 24 | $this->config->shouldReceive('get') 25 | ->once() 26 | ->with('larapoke.poking.route') 27 | ->andReturn($route = 'test-poke'); 28 | $this->config->shouldReceive('get') 29 | ->once() 30 | ->with('larapoke.poking.name') 31 | ->andReturn('test-name'); 32 | 33 | $this->config->shouldReceive('get') 34 | ->once() 35 | ->with('larapoke.poking.middleware') 36 | ->andReturn('test-middleware'); 37 | } 38 | 39 | public function testSetGlobalRoute() 40 | { 41 | $this->config->shouldReceive('get') 42 | ->once() 43 | ->with('larapoke.poking.domain') 44 | ->andReturn(null); 45 | 46 | $this->router->shouldReceive('match') 47 | ->once() 48 | ->with('head', 'test-poke') 49 | ->andReturnSelf(); 50 | $this->router->shouldReceive('name') 51 | ->once() 52 | ->with('test-name') 53 | ->andReturnSelf(); 54 | $this->router->shouldReceive('uses') 55 | ->once() 56 | ->with('DarkGhostHunter\Larapoke\Http\Controllers\LarapokeController') 57 | ->andReturnSelf(); 58 | $this->router->shouldReceive('middleware') 59 | ->once() 60 | ->with('test-middleware') 61 | ->andReturnSelf(); 62 | 63 | $generator = new RouteGenerator($this->router, $this->config); 64 | $generator->setRoutes(); 65 | 66 | $this->config->shouldHaveReceived('get') 67 | ->with('larapoke.poking.domain') 68 | ->once(); 69 | 70 | $this->router->shouldHaveReceived('match') 71 | ->with('head', 'test-poke') 72 | ->once(); 73 | $this->router->shouldHaveReceived('name') 74 | ->with('test-name') 75 | ->once(); 76 | $this->router->shouldHaveReceived('uses') 77 | ->with('DarkGhostHunter\Larapoke\Http\Controllers\LarapokeController') 78 | ->once(); 79 | $this->router->shouldHaveReceived('middleware') 80 | ->with('test-middleware') 81 | ->once(); 82 | 83 | } 84 | 85 | public function testSetOneDomainRoute() 86 | { 87 | $this->config->shouldReceive('get') 88 | ->once() 89 | ->with('larapoke.poking.domain') 90 | ->andReturn('one'); 91 | 92 | $this->router->shouldReceive('match') 93 | ->once() 94 | ->with('head', 'test-poke') 95 | ->andReturnSelf(); 96 | $this->router->shouldReceive('uses') 97 | ->once() 98 | ->with('DarkGhostHunter\Larapoke\Http\Controllers\LarapokeController') 99 | ->andReturnSelf(); 100 | $this->router->shouldReceive('middleware') 101 | ->once() 102 | ->with('test-middleware') 103 | ->andReturnSelf(); 104 | $this->router->shouldReceive('name') 105 | ->once() 106 | ->with("one.test-name") 107 | ->andReturnSelf(); 108 | 109 | $generator = new RouteGenerator($this->router, $this->config); 110 | $generator->setRoutes(); 111 | 112 | $this->config->shouldHaveReceived('get') 113 | ->with('larapoke.poking.domain') 114 | ->once(); 115 | 116 | $this->router->shouldHaveReceived('match') 117 | ->with('head', 'test-poke') 118 | ->once(); 119 | $this->router->shouldHaveReceived('uses') 120 | ->with('DarkGhostHunter\Larapoke\Http\Controllers\LarapokeController') 121 | ->once(); 122 | $this->router->shouldHaveReceived('middleware') 123 | ->with('test-middleware') 124 | ->once(); 125 | $this->router->shouldHaveReceived('name') 126 | ->with('one.test-name') 127 | ->once(); 128 | 129 | } 130 | 131 | public function testSetMultipleDomainRoutes() 132 | { 133 | $this->config->shouldReceive('get') 134 | ->once() 135 | ->with('larapoke.poking.domain') 136 | ->andReturn($domains = [ 137 | 'one', 'two', 'three' 138 | ]); 139 | 140 | $this->router->shouldReceive('match') 141 | ->times(count($domains)) 142 | ->with('head', 'test-poke') 143 | ->andReturnSelf(); 144 | $this->router->shouldReceive('uses') 145 | ->times(count($domains)) 146 | ->with('DarkGhostHunter\Larapoke\Http\Controllers\LarapokeController') 147 | ->andReturnSelf(); 148 | $this->router->shouldReceive('middleware') 149 | ->times(count($domains)) 150 | ->with('test-middleware') 151 | ->andReturnSelf(); 152 | 153 | foreach ($domains as $domain) { 154 | $this->router->shouldReceive('name') 155 | ->once() 156 | ->with("$domain.test-name") 157 | ->andReturnSelf(); 158 | } 159 | 160 | $generator = new RouteGenerator($this->router, $this->config); 161 | $generator->setRoutes(); 162 | 163 | $this->config->shouldHaveReceived('get') 164 | ->with('larapoke.poking.domain') 165 | ->once(); 166 | 167 | $this->router->shouldHaveReceived('match') 168 | ->with('head', 'test-poke') 169 | ->times(count($domains)); 170 | $this->router->shouldHaveReceived('uses') 171 | ->with('DarkGhostHunter\Larapoke\Http\Controllers\LarapokeController') 172 | ->times(count($domains)); 173 | $this->router->shouldHaveReceived('middleware') 174 | ->with('test-middleware') 175 | ->times(count($domains)); 176 | 177 | foreach ($domains as $domain) { 178 | $this->router->shouldHaveReceived('name') 179 | ->with("$domain.test-name") 180 | ->once(); 181 | } 182 | } 183 | 184 | protected function tearDown() : void 185 | { 186 | parent::tearDown(); 187 | 188 | \Mockery::close(); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /tests/Unit/Modes/ModeBladeTest.php: -------------------------------------------------------------------------------- 1 | scaffoldAuth($app); 18 | 19 | $app->make('config')->set('larapoke.mode', 'blade'); 20 | } 21 | 22 | protected function setUp() : void 23 | { 24 | parent::setUp(); 25 | 26 | /** @var \Illuminate\Routing\Router $router */ 27 | $router = $this->app->make('router'); 28 | 29 | $router->group(['middleware' => ['web']], function () use ($router) { 30 | $router->get('/register', function () { 31 | return $this->app->make(\Illuminate\Contracts\View\Factory::class)->make('auth.register'); 32 | })->name('register'); 33 | $router->get('/login', function () { 34 | return $this->app->make(\Illuminate\Contracts\View\Factory::class)->make('auth.login'); 35 | })->name('login'); 36 | $router->get('/home', function () { 37 | return $this->app->make(\Illuminate\Contracts\View\Factory::class)->make('home'); 38 | })->name('home'); 39 | $router->get('/form-only', function () { 40 | return $this->viewWithFormOnly(); 41 | })->name('form-only'); 42 | $router->get('/multiple-form', function () { 43 | return $this->viewMultipleForms(); 44 | })->name('multiple-form'); 45 | $router->get('/multiple-form-with-middleware', function () { 46 | return $this->viewMultipleForms(); 47 | }) 48 | ->name('multiple-form-with-middleware')->middleware('larapoke'); 49 | $router->get('/not-successful', function () { return new Response('', 400); }); 50 | }); 51 | } 52 | 53 | protected function viewWithFormOnly() 54 | { 55 | /** @var \Illuminate\View\Compilers\BladeCompiler $blade */ 56 | $blade = $this->app->make(\Illuminate\View\Compilers\BladeCompiler::class); 57 | 58 | return $blade->compileString(' 59 | 60 | 61 | 62 | 63 | 64 | 65 | Document 66 | 67 | 68 |
69 | ' . csrf_field() . ' @larapoke 70 |
71 | 72 | 73 | '); 74 | } 75 | 76 | protected function viewMultipleForms() 77 | { 78 | /** @var \Illuminate\View\Compilers\BladeCompiler $blade */ 79 | $blade = $this->app->make(\Illuminate\View\Compilers\BladeCompiler::class); 80 | 81 | return $blade->compileString(' 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | Document 90 | 91 | 92 |
93 | ' . csrf_field() . ' @larapoke 94 |
95 | 96 |
97 | ' . csrf_field() . ' @larapoke 98 |
99 | 100 | 101 | '); 102 | } 103 | 104 | protected function viewWithNothing() 105 | { 106 | /** @var \Illuminate\View\Compilers\BladeCompiler $blade */ 107 | $blade = $this->app->make(\Illuminate\View\Compilers\BladeCompiler::class); 108 | 109 | return $blade->compileString(' 110 | 111 | 112 | 113 | 114 | 115 | 116 | Document 117 | 118 | 119 | 120 | 121 | '); 122 | } 123 | 124 | protected function tearDown() : void 125 | { 126 | parent::tearDown(); 127 | 128 | $this->cleanScaffold(); 129 | } 130 | 131 | protected function recurseRmdir($dir) 132 | { 133 | $files = array_diff(scandir($dir), ['.', '..']); 134 | foreach ($files as $file) { 135 | (is_dir("$dir/$file")) ? $this->recurseRmdir("$dir/$file") : unlink("$dir/$file"); 136 | } 137 | 138 | return rmdir($dir); 139 | } 140 | 141 | public function testNoScriptOnRouteWithoutMiddleware() 142 | { 143 | $response = $this->get('/register'); 144 | $this->assertStringNotContainsString('start-larapoke-script', $response->content()); 145 | $this->assertStringNotContainsString('end-larapoke-script', $response->content()); 146 | } 147 | 148 | public function testInjectsScriptOnForm() 149 | { 150 | $response = $this->get('/form-only'); 151 | $this->assertStringContainsString('start-larapoke-script', $response->content()); 152 | $this->assertStringContainsString('end-larapoke-script', $response->content()); 153 | } 154 | 155 | public function testDoesntInjectOnNotSuccessful() 156 | { 157 | $response = $this->get('/not-successful'); 158 | $this->assertStringNotContainsString('start-larapoke-script', $response->content()); 159 | $this->assertStringNotContainsString('end-larapoke-script', $response->content()); 160 | } 161 | 162 | public function testInjectsOnceOnMultipleForms() 163 | { 164 | $response = $this->get('/multiple-form'); 165 | 166 | $this->assertStringContainsString('start-larapoke-script', $response->content()); 167 | $this->assertStringContainsString('end-larapoke-script', $response->content()); 168 | 169 | $this->assertEquals(2, substr_count($response->content(), 'start-larapoke-script')); 170 | $this->assertEquals(2, substr_count($response->content(), 'end-larapoke-script')); 171 | } 172 | 173 | } 174 | -------------------------------------------------------------------------------- /tests/Unit/Modes/ModeAutoTest.php: -------------------------------------------------------------------------------- 1 | scaffoldAuth($app); 21 | 22 | $app['config']->set('larapoke.mode', 'auto'); 23 | } 24 | 25 | protected function setUp() : void 26 | { 27 | parent::setUp(); 28 | 29 | /** @var \Illuminate\Routing\Router $router */ 30 | $router = $this->app->make('router'); 31 | 32 | $router->group(['middleware' => ['web']], function () use ($router) { 33 | $router->get('/register', function () { 34 | return $this->app->make(Factory::class)->make('auth.register'); 35 | })->name('register'); 36 | $router->get('/login', function () { 37 | return $this->app->make(Factory::class)->make('auth.login'); 38 | })->name('login'); 39 | $router->get('/home', function () { 40 | return $this->app->make(Factory::class)->make('home'); 41 | })->name('home'); 42 | $router->get('/json', function () { 43 | return $this->app->make(JsonResponse::class, [ 44 | 'example' => 'name="_token"', 45 | 'csrf' => 'name="csrf-token"', 46 | ]); 47 | }); 48 | $router->get('/form-only', function () { 49 | return $this->viewWithFormOnly(); 50 | })->name('form-only'); 51 | $router->get('/header-only', function () { 52 | return $this->viewWithHeaderOnly(); 53 | })->name('header-only'); 54 | $router->get('/nothing', function () { 55 | return $this->viewWithNothing(); 56 | })->name('nothing'); 57 | $router->get('/not-successful', function () { return new Response('', 400); }); 58 | }); 59 | } 60 | 61 | protected function viewWithFormOnly() 62 | { 63 | /** @var BladeCompiler $blade */ 64 | $blade = $this->app->make(BladeCompiler::class); 65 | 66 | return $blade->compileString(' 67 | 68 | 69 | 70 | 71 | 72 | 73 | Document 74 | 75 | 76 |
77 | ' . csrf_field() . ' 78 |
79 | 80 | 81 | '); 82 | } 83 | 84 | protected function viewWithHeaderOnly() 85 | { 86 | /** @var BladeCompiler $blade */ 87 | $blade = $this->app->make(BladeCompiler::class); 88 | 89 | return $blade->compileString(' 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | Document 98 | 99 | 100 | 101 | 102 | '); 103 | } 104 | 105 | protected function viewWithNothing() 106 | { 107 | /** @var BladeCompiler $blade */ 108 | $blade = $this->app->make(BladeCompiler::class); 109 | 110 | return $blade->compileString(' 111 | 112 | 113 | 114 | 115 | 116 | 117 | Document 118 | 119 | 120 | 121 | 122 | '); 123 | } 124 | 125 | protected function tearDown() : void 126 | { 127 | parent::tearDown(); 128 | 129 | $this->cleanScaffold(); 130 | } 131 | 132 | public function testDoesntInjectsOnJson() 133 | { 134 | $response = $this->get('/json'); 135 | $this->assertStringNotContainsString('start-larapoke-script', $response->content()); 136 | $this->assertStringNotContainsString('end-larapoke-script', $response->content()); 137 | } 138 | 139 | public function testDoesntInjectsOnAjax() 140 | { 141 | $response = $this->get('/form-only', [ 142 | 'X-Requested-With' => 'XMLHttpRequest', 143 | ]); 144 | $this->assertStringNotContainsString('start-larapoke-script', $response->content()); 145 | $this->assertStringNotContainsString('end-larapoke-script', $response->content()); 146 | } 147 | 148 | public function testDoesntInjectOnNotSuccessful() 149 | { 150 | $response = $this->get('/not-successful'); 151 | $this->assertStringNotContainsString('start-larapoke-script', $response->content()); 152 | $this->assertStringNotContainsString('end-larapoke-script', $response->content()); 153 | } 154 | 155 | public function testInjectsScriptOnFormWithHeader() 156 | { 157 | $response = $this->get('/register'); 158 | 159 | $this->assertStringContainsString('start-larapoke-script', $response->content()); 160 | $this->assertStringContainsString('end-larapoke-script', $response->content()); 161 | } 162 | 163 | public function testInjectsScriptOnForm() 164 | { 165 | $response = $this->get('/form-only'); 166 | $this->assertStringContainsString('start-larapoke-script', $response->content()); 167 | $this->assertStringContainsString('end-larapoke-script', $response->content()); 168 | } 169 | 170 | public function testInjectsScriptOnHeader() 171 | { 172 | $response = $this->get('/header-only'); 173 | $this->assertStringContainsString('start-larapoke-script', $response->content()); 174 | $this->assertStringContainsString('end-larapoke-script', $response->content()); 175 | } 176 | 177 | public function testInjectsScriptOnNothing() 178 | { 179 | $response = $this->get('/nothing'); 180 | $this->assertStringNotContainsString('start-larapoke-script', $response->content()); 181 | $this->assertStringNotContainsString('end-larapoke-script', $response->content()); 182 | } 183 | 184 | } 185 | -------------------------------------------------------------------------------- /tests/Unit/Modes/ModeMiddlewareTest.php: -------------------------------------------------------------------------------- 1 | scaffoldAuth($app); 20 | 21 | $app->make('config')->set('larapoke.mode', 'middleware'); 22 | } 23 | 24 | protected function setUp() : void 25 | { 26 | parent::setUp(); 27 | 28 | /** @var \Illuminate\Routing\Router $router */ 29 | $router = $this->app->make('router'); 30 | 31 | $router->group(['middleware' => ['web']], function() use ($router) { 32 | $router->get('/register', function() { 33 | return $this->app->make(Factory::class)->make('auth.register'); 34 | })->name('register')->middleware('larapoke:detect'); 35 | $router->get('/login', function() { 36 | return $this->app->make(Factory::class)->make('auth.login'); 37 | })->name('login')->middleware('larapoke'); 38 | $router->get('/json', function () { 39 | return $this->app->make(JsonResponse::class, [ 40 | 'example' => 'name="_token"', 41 | 'csrf' => 'name="csrf-token"', 42 | ]); 43 | })->middleware('larapoke'); 44 | $router->get('/form-only', function() { return $this->viewWithFormOnly(); }) 45 | ->name('form-only')->middleware('larapoke:detect'); 46 | $router->get('/header-only', function() { return $this->viewWithHeaderOnly(); }) 47 | ->name('header-only')->middleware('larapoke:detect'); 48 | $router->get('/nothing', function() { return $this->viewWithNothing(); }) 49 | ->name('nothing')->middleware('larapoke:detect'); 50 | $router->get('/nothing-with-middleware', function() { return $this->viewWithNothing(); }) 51 | ->name('nothing')->middleware('larapoke'); 52 | $router->get('/no-middleware', function() { return $this->viewWithNothing(); }) 53 | ->name('nothing'); 54 | $router->get('/not-successful', function () { return new Response('', 400); })->middleware('larapoke:detect'); 55 | }); 56 | } 57 | 58 | protected function viewWithFormOnly() 59 | { 60 | /** @var \Illuminate\View\Compilers\BladeCompiler $blade */ 61 | $blade = $this->app->make(\Illuminate\View\Compilers\BladeCompiler::class); 62 | 63 | return $blade->compileString(' 64 | 65 | 66 | 67 | 68 | 69 | 70 | Document 71 | 72 | 73 |
74 | ' . csrf_field() . ' 75 |
76 | 77 | 78 | '); 79 | } 80 | 81 | protected function viewWithHeaderOnly() 82 | { 83 | /** @var \Illuminate\View\Compilers\BladeCompiler $blade */ 84 | $blade = $this->app->make(\Illuminate\View\Compilers\BladeCompiler::class); 85 | 86 | return $blade->compileString(' 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | Document 95 | 96 | 97 | 98 | 99 | '); 100 | } 101 | 102 | protected function viewWithNothing() 103 | { 104 | /** @var \Illuminate\View\Compilers\BladeCompiler $blade */ 105 | $blade = $this->app->make(\Illuminate\View\Compilers\BladeCompiler::class); 106 | 107 | return $blade->compileString(' 108 | 109 | 110 | 111 | 112 | 113 | 114 | Document 115 | 116 | 117 | 118 | 119 | '); 120 | } 121 | 122 | protected function tearDown() : void 123 | { 124 | parent::tearDown(); 125 | 126 | $this->cleanScaffold(); 127 | } 128 | 129 | public function testDoesntInjectsOnJson() 130 | { 131 | $response = $this->get('/json'); 132 | 133 | $this->assertStringNotContainsString('start-larapoke-script', $response->content()); 134 | $this->assertStringNotContainsString('end-larapoke-script', $response->content()); 135 | } 136 | 137 | 138 | public function testNoScriptOnNoMiddleware() 139 | { 140 | $response = $this->get('/no-middleware'); 141 | $this->assertStringNotContainsString('start-larapoke-script', $response->content()); 142 | $this->assertStringNotContainsString('end-larapoke-script', $response->content()); 143 | } 144 | 145 | public function testDoesntInjectOnNotSuccessful() 146 | { 147 | $response = $this->get('/not-successful'); 148 | $this->assertStringNotContainsString('start-larapoke-script', $response->content()); 149 | $this->assertStringNotContainsString('end-larapoke-script', $response->content()); 150 | } 151 | 152 | public function testDetectsHeaderOrForm() 153 | { 154 | $response = $this->get('/register'); 155 | $this->assertStringContainsString('start-larapoke-script', $response->content()); 156 | $this->assertStringContainsString('end-larapoke-script', $response->content()); 157 | } 158 | 159 | public function testDetectsHeader() 160 | { 161 | $response = $this->get('/header-only'); 162 | $this->assertStringContainsString('start-larapoke-script', $response->content()); 163 | $this->assertStringContainsString('end-larapoke-script', $response->content()); 164 | } 165 | 166 | public function testDetectsForm() 167 | { 168 | $response = $this->get('/form-only'); 169 | $this->assertStringContainsString('start-larapoke-script', $response->content()); 170 | $this->assertStringContainsString('end-larapoke-script', $response->content()); 171 | } 172 | 173 | public function testDetectsNothing() 174 | { 175 | $response = $this->get('/nothing'); 176 | $this->assertStringNotContainsString('start-larapoke-script', $response->content()); 177 | $this->assertStringNotContainsString('end-larapoke-script', $response->content()); 178 | } 179 | 180 | public function testInjectsForcefullyWithoutDetectNothingWithMiddleware() 181 | { 182 | $response = $this->get('/nothing-with-middleware'); 183 | $this->assertStringContainsString('start-larapoke-script', $response->content()); 184 | $this->assertStringContainsString('end-larapoke-script', $response->content()); 185 | } 186 | 187 | public function testInjectsForcefullyWithoutDetectLogin() 188 | { 189 | $response = $this->get('/login'); 190 | $this->assertStringContainsString('start-larapoke-script', $response->content()); 191 | $this->assertStringContainsString('end-larapoke-script', $response->content()); 192 | } 193 | 194 | public function testDoesntInjectsOnExceptionResponse() 195 | { 196 | $response = $this->get('non-existant-route-triggers-exception'); 197 | 198 | $response->assertDontSee('start-larapoke-script'); 199 | } 200 | 201 | } 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Package superseeded by [Laragear/Poke](https://github.com/Laragear/Poke) 2 | 3 | --- 4 | 5 | # Larapoke 6 | 7 | Keep your forms alive, avoid `TokenMismatchException` by gently poking your Laravel app. 8 | 9 | ## Requirements 10 | 11 | * PHP 7.4, 8.0 or later. 12 | * Laravel 7.x, 8.x or later. 13 | 14 | > For older versions support, consider helping by sponsoring or donating. 15 | 16 | ## Installation 17 | 18 | Require this package into your project using Composer: 19 | 20 | ```bash 21 | composer require darkghosthunter/larapoke 22 | ``` 23 | 24 | ## How does it work? 25 | 26 | Larapoke pokes your App with an HTTP `HEAD` request to the `/poke` route at given intervals. In return, while your application renews the session lifetime, it sends an `HTTP 204` status code, which is an OK Response without body. 27 | 28 | This amounts to **barely 800 bytes sent!** 29 | 30 | ### Automatic Reloading on CSRF token expiration 31 | 32 | Larapoke script will detect if the CSRF session token is expired based on the last successful poke, and forcefully reload the page if there is Internet connection. 33 | 34 | This is done by detecting [when the browser or tab becomes active](https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API), or [when the device user becomes online again](https://developer.mozilla.org/en-US/docs/Web/API/NavigatorOnLine/onLine). 35 | 36 | This is handy in situations when the user laptop is put to sleep, or the phone loses signal. Because the session may expire during these moments, when the browser wakes up or the phone becomes online, the page is reloaded to get the new CSRF token. 37 | 38 | ## Usage 39 | 40 | There are three ways to turn on Larapoke in your app. 41 | 42 | * `auto` (easy hands-off default) 43 | * `middleware` 44 | * `manual` 45 | 46 | You can change the default mode using your environment file: 47 | 48 | ```dotenv 49 | LARAPOKE_MODE=auto 50 | ``` 51 | 52 | ### `auto` 53 | 54 | Just install this package and *look at it go*. This will push a global middleware that will look into all your Responses content where: 55 | 56 | * the content is HTML, 57 | * an input where `csrf` token is present, 58 | * or the meta tag `csrf-token`, are present. 59 | 60 | If there is any case-insensitive match, this will inject the Larapoke script in charge to keep the forms alive just before the `` tag. 61 | 62 | This mode won't inject the script on no-successful responses (anything not HTTP 2xx), like on errors or redirection. 63 | 64 | > It's recommended to use the other modes if your application has many routes or Responses with a lot of text. 65 | 66 | ### `middleware` 67 | 68 | This will disable the global middleware, allowing you to use the `larapoke` middleware only in the routes you explicitly decide. 69 | 70 | ```php 71 | middleware('larapoke'); 78 | ``` 79 | 80 | This will forcefully inject the script, even if there is no form, into the route. You can also apply this to a [route group](https://laravel.com/docs/routing#route-groups). 81 | 82 | Since a route group may contain routes without any form, you can add the `detect` option to the middleware which will scan the Response for a CSRF token and inject the script only if it finds one. 83 | 84 | ```php 85 | middleware('larapoke:detect') 92 | ->group(function () { 93 | 94 | // Here it will be injected 95 | Route::get('register', [RegisterController::class, 'showForm']); 96 | 97 | // But not here since there is no form 98 | Route::get('status', [RegisterController::class, 'status']); 99 | }); 100 | ``` 101 | 102 | This mode won't inject the script on no-successful responses (anything not HTTP 2xx), like on errors or redirection. 103 | 104 | ### `blade` 105 | 106 | The `blade` method allows you to use the `@larapoke` directive to inject the script anywhere in your view, keeping the forms of that Response alive. 107 | 108 | ```html 109 | 110 |

Try to Login:

111 |
112 | @csrf 113 | @larapoke 114 | 115 | 116 | 117 |
118 |

Or reset your password

119 |
120 | @csrf 121 | @larapoke 122 | 123 | 124 |
125 | ``` 126 | 127 | Don't worry if you use many `@larapoke` directives in your view, like in this example. The script can be injected multiple times, but only the first script will run and poke the site. 128 | 129 | ## Configuration 130 | 131 | For fine-tuning, you can publish the `larapoke.php` config file. 132 | 133 | ```bash 134 | php artisan vendor:publish --provider=DarkGhostHunter\Larapoke\LarapokeServiceProvider 135 | ``` 136 | 137 | Let's examine the configuration array for Larapoke: 138 | 139 | ```php 140 | env('LARAPOKE_MODE', 'auto'), 142 | 'times' => 4, 143 | 'view' => 'larapoke::script', 144 | 'poking' => [ 145 | 'route' => 'poke', 146 | 'name' => 'larapoke', 147 | 'domain' => null, 148 | 'middleware' => ['web'], 149 | ] 150 | ]; 151 | ``` 152 | 153 | ### Times (Interval) 154 | 155 | How many times the poking will be done relative to the global session lifetime. The more times, the shorter the poking interval. The default `4` should be fine for any normal application. 156 | 157 | For example, if our session lifetime is the default of 120 minutes: 158 | 159 | - 3 times will poke the application each 40 minutes, 160 | - 4 times will poke the application each 30 minutes, 161 | - 5 times will poke the application each 24 minutes, 162 | - 6 times will poke the application each 20 minutes, and so on... 163 | 164 | So, basically, `session lifetime / times = poking interval`. 165 | 166 | You should raise it if you expect your users to have a lot of doing nothing and may quit at any given time. 167 | 168 | ### Script View 169 | 170 | Larapoke uses its own Blade template to inject the script. 171 | 172 | You can use other view with the script or overriding the default by creating a `views/vendor/larapoke/script.blade.php` file. The latter option doesn't need to publish the config file. 173 | 174 | Why would you? Some people may want to change this because they want to use a Javascript HTTP library, minify the response, make it compatible for older browsers, or even [create a custom Event](https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events) when CSRF token expires. 175 | 176 | The view receives three variables: 177 | 178 | * `$route`: The relative route where the poking will be done. 179 | * `$interval`: The interval in milliseconds the poking should be done. 180 | * `$lifetime`: The session lifetime in milliseconds. 181 | 182 | ### Poking 183 | 184 | This is the array of settings for the poking route which receives the script HTTP HEAD Request. 185 | 186 | ```php 187 | [ 192 | 'route' => 'poke', 193 | 'name' => 'larapoke', 194 | 'domain' => null, 195 | 'middleware' => ['web'], 196 | ] 197 | ]; 198 | ``` 199 | 200 | #### Route 201 | 202 | The route (relative to the root URL of your application) that will be using to receive the pokes. 203 | 204 | ```php 205 | [ 208 | 'route' => '/dont-sleep' 209 | ], 210 | ]; 211 | ``` 212 | 213 | > The poke routes are registered before any set in your application. You *could* override the poke route with your own logic before responding with HTTP 204. 214 | 215 | #### Name 216 | 217 | Name of the route, to find the poke route in your app for whatever reason. 218 | 219 | ```php 220 | [ 223 | 'name' => 'my-custom-poking-route' 224 | ], 225 | ]; 226 | ``` 227 | 228 | > If you're using an array of domains or subdomains, this string will be appended to the route name. 229 | 230 | #### Domains 231 | 232 | In case you are using different domains of subdomains, it may be convenient to allow this route only under a certain one instead of all domains. A classic example is to make the poking available at `http://user.myapp.com/poke` but no `http://myapp.com/poke`. 233 | 234 | - `null` (default): the poke route will be applied in **every domain or subdomain**. 235 | - `mydomain.com`: the poke route will be applied only to that domain, like so: `http://mydomain.com/poke`. 236 | - `[array]`: the poke route will be available only on the domains inside the array. 237 | 238 | ```php 239 | [ 242 | 'domain' => ['mysubdomain.myapp.com', 'myotherdomain.com'] 243 | ], 244 | ]; 245 | ``` 246 | 247 | > If you use an array, the route names will be conveniently names using the domain name as a prefix, like `myotherdomain.com-larapoke`. 248 | 249 | #### Middleware 250 | 251 | The default Larapoke route uses the "web" middleware group, which is the default for handling web requests in a fresh installation. If you are using another group, or want to use a particular middleware, you can modify where here. 252 | 253 | ```php 254 | [ 257 | 'middleware' => ['auth:api', 'validates-ip', 'my-custom-middleware'] 258 | ], 259 | ]; 260 | ``` 261 | 262 | ## License 263 | 264 | This package is licenced by the [MIT License](LICENSE). 265 | --------------------------------------------------------------------------------