Created
March 10, 2017 18:21
-
-
Save stidges/d74a0eca585d83d73bc781a54a2f3253 to your computer and use it in GitHub Desktop.
Code to accompany the 'Writing an Allowed Username Validator in Laravel' (http://blog.stidges.com/post/writing-an-allowed-username-validator-in-laravel)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
# app/Validation/AllowedUsernameValidator.php | |
namespace App\Validation; | |
use Illuminate\Filesystem\Filesystem; | |
use Illuminate\Routing\Router; | |
use Illuminate\Config\Repository; | |
class AllowedUsernameValidator | |
{ | |
/** | |
* The router instance used to check the username against application routes. | |
* | |
* @var \Illuminate\Routing\Router | |
*/ | |
private $router; | |
/** | |
* The filesystem class used to retrieve public files and directories. | |
* | |
* @var \Illuminate\Filesystem\Filesystem | |
*/ | |
private $files; | |
/** | |
* The config repository used to retrieve reserved usernames. | |
* | |
* @var \Illuminate\Config\Repository | |
*/ | |
private $config; | |
/** | |
* Create a new allowed username validator instance. | |
* | |
* @param \Illuminate\Routing\Router $router | |
* @param \Illuminate\Filesystem\Filesystem $files | |
* @param \Illuminate\Config\Repository $config | |
*/ | |
public function __construct(Router $router, Filesystem $files, Repository $config) | |
{ | |
$this->config = $config; | |
$this->router = $router; | |
$this->files = $files; | |
} | |
/** | |
* Validate whether the given username is allowed. | |
* | |
* @param string $attribute | |
* @param string $username | |
* @return bool | |
*/ | |
public function validate($attribute, $username) | |
{ | |
$username = trim(strtolower($username)); | |
if ($this->isReservedUsername($username)) { | |
return false; | |
} | |
if ($this->matchesRoute($username)) { | |
return false; | |
} | |
if ($this->matchesPublicFileOrDirectory($username)) { | |
return false; | |
} | |
return true; | |
} | |
/** | |
* Determine whether the given username is in the reserved usernames list. | |
* | |
* @param string $username | |
* @return bool | |
*/ | |
private function isReservedUsername($username) | |
{ | |
return in_array($username, $this->config->get('auth.reserved_usernames')); | |
} | |
/** | |
* Determine whether the given username matches an application route. | |
* | |
* @param string $username | |
* @return bool | |
*/ | |
private function matchesRoute($username) | |
{ | |
foreach ($this->router->getRoutes() as $route) { | |
if (strtolower($route->uri) === $username) { | |
return true; | |
} | |
} | |
return false; | |
} | |
/** | |
* Determine whether the given username matches a public file or directory. | |
* | |
* @param string $username | |
* @return bool | |
*/ | |
private function matchesPublicFileOrDirectory($username) | |
{ | |
foreach ($this->files->glob(public_path().'/*') as $path) { | |
if (strtolower(basename($path)) === $username) { | |
return true; | |
} | |
} | |
return false; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
# tests/Unit/Validation/AllowedUsernameValidatorTest.php | |
namespace Tests\Unit\Validation; | |
use App\Validation\AllowedUsernameValidator; | |
use Illuminate\Filesystem\Filesystem; | |
use Illuminate\Routing\Route; | |
use Illuminate\Routing\RouteCollection; | |
use Illuminate\Routing\Router; | |
use Mockery; | |
use Tests\TestCase; | |
class AllowedUsernameValidatorTest extends TestCase | |
{ | |
/** @var \Mockery\MockInterface */ | |
private $router; | |
/** @var \Mockery\MockInterface */ | |
private $filesystem; | |
/** @var \Illuminate\Config\Repository */ | |
private $config; | |
/** @var \App\Validation\AllowedUsernameValidator */ | |
private $validator; | |
/** | |
* Initialize mocks, config and class under test. | |
* | |
* @return void | |
*/ | |
protected function setUp() | |
{ | |
parent::setUp(); | |
$this->router = Mockery::mock(Router::class); | |
$this->filesystem = Mockery::mock(Filesystem::class); | |
$this->config = $this->app['config']; | |
$this->config->set('auth.reserved_usernames', []); | |
$this->validator = new AllowedUsernameValidator($this->router, $this->filesystem, $this->config); | |
} | |
/** @test */ | |
public function it_passes_when_a_username_does_not_match_any_routes_or_public_directories_or_reserved_names() | |
{ | |
$this->router->shouldReceive('getRoutes')->once()->andReturn($this->getRouteCollection()); | |
$this->filesystem->shouldReceive('glob')->once()->with(public_path('*'))->andReturn([]); | |
$this->assertTrue($this->validator->validate('username', 'valid')); | |
} | |
/** @test */ | |
public function it_fails_when_a_username_matches_a_reserved_username() | |
{ | |
$this->config->set('auth.reserved_usernames', ['admin']); | |
$this->router->shouldNotReceive('getRoutes'); | |
$this->filesystem->shouldNotReceive('glob'); | |
$this->assertFalse($this->validator->validate('username', 'admin')); | |
} | |
/** @test */ | |
public function it_normalizes_the_username_before_checking() | |
{ | |
$this->config->set('auth.reserved_usernames', ['admin']); | |
$this->router->shouldNotReceive('getRoutes'); | |
$this->filesystem->shouldNotReceive('glob'); | |
$this->assertFalse($this->validator->validate('username', ' ADmIn ')); | |
} | |
/** @test */ | |
public function it_fails_when_a_username_matches_a_route_uri() | |
{ | |
$this->filesystem->shouldNotReceive('glob'); | |
$this->router->shouldReceive('getRoutes')->once()->andReturn($this->getRouteCollection(['login'])); | |
$this->assertFalse($this->validator->validate('username', 'login')); | |
} | |
/** @test */ | |
public function it_fails_when_a_username_matches_a_public_file_or_directory_name() | |
{ | |
$this->router->shouldReceive('getRoutes')->once()->andReturn($this->getRouteCollection()); | |
$this->filesystem->shouldReceive('glob')->once()->with(public_path().'/*')->andReturn(['/path/to/dirname']); | |
$this->assertFalse($this->validator->validate('username', 'dirname')); | |
} | |
/** | |
* Get a new RouteCollection instance for the given URIs. | |
* | |
* @param array $uris | |
* @return \Illuminate\Routing\RouteCollection | |
*/ | |
private function getRouteCollection(array $uris = []) | |
{ | |
$routeCollection = new RouteCollection; | |
foreach ($uris as $uri) { | |
$routeCollection->add(new Route('GET', $uri, [])); | |
} | |
return $routeCollection; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment