Skip to content

Instantly share code, notes, and snippets.

@freddyheppell
Last active May 25, 2019 13:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save freddyheppell/935b96276f651e8d0e1acf88b816d5ca to your computer and use it in GitHub Desktop.
Save freddyheppell/935b96276f651e8d0e1acf88b816d5ca to your computer and use it in GitHub Desktop.
Laravel Rule for valid Discourse usernames
<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
/**
* PHP implementation of
* https://github.com/discourse/discourse/blob/888e68a1637ca784a7bf51a6bbb524dcf7413b13/app/models/username_validator.rb
* @package App\Rules
*/
class DiscourseUsernameRule implements Rule
{
private $value;
// usernames must consist of a-z A-Z 0-9 _ - .
private const ASCII_INVALID_CHARACTERS = "/[^\w.-]/";
// usernames can start with a-z A-Z 0-0 _
private const VALID_LEADING_CHARACTERS = "/^[a-zA-Z0-9_]/";
// usernames must end with a-z A-Z 0-9
private const VALID_TRAILING_CHARACTERS = "/[a-zA-Z0-9]$/";
// underscores, dashes and dots can't be repeated consecutively
private const REPEATING_CONFUSING_CHARACTERS = "/[-_.]{2,}/";
private const CONFUSING_EXTENSIONS = "/\.(js|json|css|htm|html|xml|jpg|jpeg|png|gif|bmp|ico|tif|tiff|woff)$/i";
/**
* Create a new rule instance.
*
* @return void
*/
public function __construct()
{
//
}
private function minimumLength()
{
return strlen($this->value) > 3;
}
private function maximumLength()
{
return strlen($this->value) <= 20;
}
private function charactersValid()
{
return !preg_match(self::ASCII_INVALID_CHARACTERS, $this->value);
}
private function firstCharacterValid()
{
return preg_match(self::VALID_LEADING_CHARACTERS, $this->value);
}
private function lastCharacterValid()
{
return preg_match(self::VALID_TRAILING_CHARACTERS, $this->value);
}
private function noDoubleSpecial()
{
return !preg_match(self::REPEATING_CONFUSING_CHARACTERS, $this->value);
}
private function noConfusingExtension()
{
return !preg_match(self::CONFUSING_EXTENSIONS, $this->value);
}
/**
* Determine if the validation rule passes.
*
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
{
$this->value = $value;
return $this->minimumLength() &&
$this->maximumLength() &&
$this->charactersValid() &&
$this->firstCharacterValid() &&
$this->lastCharacterValid() &&
$this->noDoubleSpecial() &&
$this->noConfusingExtension();
}
/**
* Get the validation error message.
*
* @return string
*/
public function message()
{
return 'The validation error message.';
}
}
<?php
namespace Tests\Rules;
use App\Rules\DiscourseUsernameRule;
use Tests\TestCase;
class DiscourseUsernameRule_Test extends TestCase
{
private $rule;
protected function setUp(): void
{
parent::setUp();
$this->rule = new DiscourseUsernameRule();
}
public function testBlankUsernameIsInvalid()
{
$this->assertFalse($this->rule->passes("username", ""));
}
public function testUsernameTooShort()
{
$this->assertFalse($this->rule->passes("username", "abc"));
}
public function testUsernameWithMinLength()
{
$this->assertTrue($this->rule->passes("username", "abcd"));
}
public function testUsernameTooLong()
{
$this->assertFalse($this->rule->passes("username", "abcdefgjijklmnopqrstu"));
}
public function testUsernameWithMaxLength()
{
$this->assertTrue($this->rule->passes("username", "abcdefgjijklmnopqrst"));
}
public function testValidCharacters()
{
$this->assertTrue($this->rule->passes("username", "ab-cd.123_ABC-xYz"));
}
public function testInvalidCharacters()
{
$this->assertFalse($this->rule->passes("username", "abc|"));
$this->assertFalse($this->rule->passes("username", "a#bc"));
$this->assertFalse($this->rule->passes("username", "abc xyz"));
}
public function testStartingWithAlphanumericOrUnderscore()
{
$this->assertTrue($this->rule->passes("username", "abcd"));
$this->assertTrue($this->rule->passes("username", "1abc"));
$this->assertTrue($this->rule->passes("username", "_abc"));
}
public function testStartingWithDotOrDash()
{
$this->assertFalse($this->rule->passes("username", ".abc"));
$this->assertFalse($this->rule->passes("username", "-abc"));
}
public function testEndingWithAlphanumeric()
{
$this->assertTrue($this->rule->passes("username", "abcd"));
$this->assertTrue($this->rule->passes("username", "abc9"));
}
public function testEndingWithInvalidCharacter()
{
$this->assertFalse($this->rule->passes("username", "abc_"));
$this->assertFalse($this->rule->passes("username", "abc."));
$this->assertFalse($this->rule->passes("username", "abc-"));
}
public function testConsecutiveSpecialCharacters()
{
$this->assertFalse($this->rule->passes("username", "a__bc"));
$this->assertFalse($this->rule->passes("username", "a..bc"));
$this->assertFalse($this->rule->passes("username", "a--bc"));
}
public function testFileExtensionEnding()
{
$this->assertFalse($this->rule->passes("username", "abc.json"));
$this->assertFalse($this->rule->passes("username", "abc.png"));
}
public function testUnicodeCharacters()
{
$this->assertFalse($this->rule->passes("username", "abcö"));
$this->assertFalse($this->rule->passes("username", "abc象"));
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment