Skip to content

Instantly share code, notes, and snippets.

@stidges
Created March 10, 2017 18:21
Show Gist options
  • Save stidges/d74a0eca585d83d73bc781a54a2f3253 to your computer and use it in GitHub Desktop.
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)
<?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;
}
}
<?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