Added CaptchaInstanceManager

This commit is contained in:
Zi Xing 2022-01-26 18:06:05 -05:00
parent 490e4f7a9e
commit d65f9ae897
9 changed files with 389 additions and 5 deletions

View File

@ -0,0 +1,41 @@
<?php
namespace vCaptcha\Classes;
class Security
{
/**
* Peppers a data input into irreversible hash that is iterated randomly between
* the minimum and maximum value of the given parameters
*
* @param string $data
* @param int $min
* @param int $max
* @return string
*/
public static function pepper(string $data, int $min = 100, int $max = 1000): string
{
$n = rand($min, $max);
$res = '';
$data = hash('whirlpool', $data);
for ($i=0, $l=strlen($data) ; $l ; $l--)
{
$i = ($i+$n-1) % $l;
$res = $res . $data[$i];
$data = ($i ? substr($data, 0, $i) : '') . ($i < $l-1 ? substr($data, $i+1) : '');
}
return($res);
}
/**
* Returns a 48 long unique ID for the captcha instance
*
* @param string $id
* @param string $owner_id
* @return string
*/
public static function generateCaptchaInstanceSecret(string $id, string $owner_id): string
{
return hash('crc32', $id) . hash('crc32', $owner_id) . hash('haval128,3', self::pepper($id . $owner_id . time()));
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace vCaptcha\Classes;
class Validate
{
/**
* Validates the captcha instance name
*
* @param string $input
* @return bool
*/
public static function captchaInstanceName(string $input): bool
{
if(strlen($input) > 120)
{
return false;
}
if(strlen($input) < 3)
{
return false;
}
if(preg_match("/^[a-zA-Z0-9 ]*$/", $input))
{
return true;
}
return false;
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace vCaptcha\Exceptions;
use Exception;
use Throwable;
class CaptchaInstanceNotFoundException extends Exception
{
/**
* @param string $message
* @param int $code
* @param Throwable|null $previous
*/
public function __construct(string $message = "", int $code = 0, Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace vCaptcha\Exceptions;
use Exception;
use Throwable;
class DatabaseException extends Exception
{
private string $query;
/**
* @param string $message
* @param string $query
* @param int $code
* @param Throwable|null $previous
*/
public function __construct(string $message = "", string $query="", int $code = 0, ?Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
$this->message = $message;
$this->query = $query;
$this->code = $code;
}
/**
* @return string
*/
public function getQuery(): string
{
return $this->query;
}
}

View File

@ -0,0 +1,20 @@
<?php
namespace vCaptcha\Exceptions;
use Throwable;
class InvalidCaptchaNameException extends \Exception
{
/**
* @param string $message
* @param int $code
* @param Throwable|null $previous
*/
public function __construct(string $message = "", int $code = 0, Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
$this->message = $message;
$this->code = $code;
}
}

View File

@ -6,8 +6,14 @@
use msqg\QueryBuilder;
use Symfony\Component\Uid\Uuid;
use vCaptcha\Classes\Security;
use vCaptcha\Classes\Validate;
use vCaptcha\Exceptions\CaptchaInstanceNotFoundException;
use vCaptcha\Exceptions\DatabaseException;
use vCaptcha\Exceptions\InvalidCaptchaNameException;
use vCaptcha\Objects\CaptchaInstance;
use vCaptcha\Objects\CaptchaInstance\FirewallOptions;
use vCaptcha\Objects\SampleCaptchaInstance;
use vCaptcha\vCaptcha;
use ZiProto\ZiProto;
@ -23,10 +29,25 @@
$this->vcaptcha = $vcaptcha;
}
public function createCaptcha(string $captcha_type, string $owner_id): CaptchaInstance
/**
* Creates a new Captcha instance
*
* @param string $captcha_type
* @param string $owner_id
* @param string $name
* @return CaptchaInstance
* @throws DatabaseException
* @throws InvalidCaptchaNameException
*/
public function createInstance(string $captcha_type, string $owner_id, string $name): CaptchaInstance
{
if(Validate::captchaInstanceName($name) == false)
throw new InvalidCaptchaNameException('The given name must not be greater than 120 characters or less than 3 and must be alphanumeric.');
$CaptchaInstance = new CaptchaInstance();
$CaptchaInstance->ID = Uuid::v4()->toRfc4122();
$CaptchaInstance->Name = $name;
$CaptchaInstance->SecretKey = Security::generateCaptchaInstanceSecret($CaptchaInstance->ID, $owner_id);
$CaptchaInstance->CaptchaType = $captcha_type;
$CaptchaInstance->OwnerID = $owner_id;
$CaptchaInstance->Enabled = true;
@ -34,14 +55,127 @@
$CaptchaInstance->CreatedTimestamp = time();
$CaptchaInstance->LastUpdatedTimestamp = time();
$Query = QUeryBuilder::insert_into('instances', [
$Query = QueryBuilder::insert_into('instances', [
'id' => $this->vcaptcha->getDatabase()->real_escape_string($CaptchaInstance->ID),
'name' => $this->vcaptcha->getDatabase()->real_escape_string(urlencode($CaptchaInstance->Name)),
'captcha_type' => $this->vcaptcha->getDatabase()->real_escape_string($CaptchaInstance->CaptchaType),
'owner_id' => $this->vcaptcha->getDatabase()->real_escape_string($CaptchaInstance->OwnerID),
'secret_key' => $this->vcaptcha->getDatabase()->real_escape_string($CaptchaInstance->SecretKey),
'enabled' => (int)$CaptchaInstance->Enabled,
'firewall_options' => $this->vcaptcha->getDatabase()->real_escape_string(ZiProto::encode($CaptchaInstance->FirewallOptions->toArray())),
'created_timestamp' => $this->vcaptcha->getDatabase()->real_escape_string($CaptchaInstance->CreatedTimestamp),
'last_updated_timestamp' => $this->vcaptcha->getDatabase()->real_escape_string($CaptchaInstance->LastUpdatedTimestamp)
]);
$QueryResults = $this->vcaptcha->getDatabase()->query($Query);
if($QueryResults == false)
throw new DatabaseException($this->vcaptcha->getDatabase()->error, $Query, $this->vcaptcha->getDatabase()->errno);
return $CaptchaInstance;
}
/**
* Returns an existing instance from the database
*
* @param string $id
* @return CaptchaInstance
* @throws CaptchaInstanceNotFoundException
* @throws DatabaseException
*/
public function getInstance(string $id): CaptchaInstance
{
$Query = QueryBuilder::select('instances', [
'id',
'name',
'captcha_type',
'owner_id',
'secret_key',
'enabled',
'firewall_options',
'created_timestamp',
'last_updated_timestamp'
], 'id', $this->vcaptcha->getDatabase()->real_escape_string($id), null, null, 1);
$QueryResults = $this->vcaptcha->getDatabase()->query($Query);
if($QueryResults == false)
throw new DatabaseException($this->vcaptcha->getDatabase()->error, $Query, $this->vcaptcha->getDatabase()->errno);
if($QueryResults->num_rows == 0)
throw new CaptchaInstanceNotFoundException();
$Row = $QueryResults->fetch_array(MYSQLI_ASSOC);
$Row['firewall_options'] = ZiProto::decode($Row['firewall_options']);
$Row['name'] = urldecode($Row['name']);
return CaptchaInstance::fromArray($Row);
}
/**
* Returns an array of captcha sample instance objects associated with the owner ID
*
* @param string $owner_id
* @return SampleCaptchaInstance[]
* @throws DatabaseException
*/
public function getInstances(string $owner_id): array
{
$Query = QueryBuilder::select('instances', [
'id',
'captcha_type',
'name'
], 'owner_id', $this->vcaptcha->getDatabase()->real_escape_string($owner_id));
$QueryResults = $this->vcaptcha->getDatabase()->query($Query);
if($QueryResults == false)
throw new DatabaseException($this->vcaptcha->getDatabase()->error, $Query, $this->vcaptcha->getDatabase()->errno);
if($QueryResults->num_rows == 0)
return [];
$ResultsArray = [];
while($Row = $QueryResults->fetch_assoc())
{
$Row['name'] = urldecode($Row['name']);
$ResultsArray[] = SampleCaptchaInstance::fromArray($Row);
}
return $ResultsArray;
}
/**
* Updates an existing captcha instance in the database
*
* @param CaptchaInstance $captchaInstance
* @return CaptchaInstance
* @throws DatabaseException
* @throws InvalidCaptchaNameException
* @noinspection PhpCastIsUnnecessaryInspection
*/
public function updateInstance(CaptchaInstance $captchaInstance): CaptchaInstance
{
if(Validate::captchaInstanceName($captchaInstance->Name) == false)
throw new InvalidCaptchaNameException('The given name must not be greater than 120 characters or less than 3 and must be alphanumeric.');
$captchaInstance->LastUpdatedTimestamp = time();
$firewall_options = $captchaInstance->FirewallOptions->toArray()['firewall_options'];
$Query = QueryBuilder::update('instances', [
'name' => $this->vcaptcha->getDatabase()->real_escape_string(urlencode($captchaInstance->Name)),
'secret_key' => $this->vcaptcha->getDatabase()->real_escape_string($captchaInstance->SecretKey),
'captcha_type' => $this->vcaptcha->getDatabase()->real_escape_string($captchaInstance->CaptchaType),
'enabled' => (int)$captchaInstance->Enabled,
'firewall_options' => $this->vcaptcha->getDatabase()->real_escape_string(ZiProto::encode($firewall_options)),
'last_updated_timestamp' => (int)$captchaInstance->LastUpdatedTimestamp
]);
$QueryResults = $this->vcaptcha->getDatabase()->query($Query);
if($QueryResults == false)
throw new DatabaseException($this->vcaptcha->getDatabase()->error, $Query, $this->vcaptcha->getDatabase()->errno);
return $captchaInstance;
}
}

View File

@ -23,6 +23,13 @@
*/
public $OwnerID;
/**
* The name of the captcha instance
*
* @var string
*/
public $Name;
/**
* The secret key used for creating and validating
*
@ -104,7 +111,7 @@
$CaptchaInstanceObject->SecretKey = $data['secret_key'];
if(isset($data['enabled']))
$CaptchaInstanceObject->Enabled = $data['enabled'];
$CaptchaInstanceObject->Enabled = (bool)$data['enabled'];
if(isset($data['captcha_type']))
$CaptchaInstanceObject->CaptchaType = $data['captcha_type'];
@ -113,10 +120,10 @@
$CaptchaInstanceObject->FirewallOptions = FirewallOptions::fromArray($data['firewall_options']);
if(isset($data['last_updated_timestamp']))
$CaptchaInstanceObject->LastUpdatedTimestamp = $data['last_updated_timestamp'];
$CaptchaInstanceObject->LastUpdatedTimestamp = (int)$data['last_updated_timestamp'];
if(isset($data['created_timestamp']))
$CaptchaInstanceObject->CreatedTimestamp = $data['created_timestamp'];
$CaptchaInstanceObject->CreatedTimestamp = (int)$data['created_timestamp'];
return $CaptchaInstanceObject;
}

View File

@ -0,0 +1,74 @@
<?php
/** @noinspection PhpMissingFieldTypeInspection */
namespace vCaptcha\Objects;
class SampleCaptchaInstance
{
/**
* The ID of the captcha instance
*
* @var string
*/
public $ID;
/**
* The name of the captcha instance
*
* @var string
*/
public $Name;
/**
* The type of captcha
*
* @var string
*/
public $CaptchaType;
/**
* Indicates if the captcha instance is enabled or not
*
* @var bool
*/
public $Enabled;
/**
* @return array
*/
public function toArray(): array
{
return [
'id' => $this->ID,
'name' => $this->Name,
'captcha_type' => $this->CaptchaType,
'enabled' => $this->Enabled
];
}
/**
* Constructs object from an array representation
*
* @param array $data
* @return SampleCaptchaInstance
*/
public static function fromArray(array $data): SampleCaptchaInstance
{
$SampleCaptchaInstance = new SampleCaptchaInstance();
if(isset($data['id']))
$SampleCaptchaInstance->ID = $data['id'];
if(isset($data['name']))
$SampleCaptchaInstance->Name = $data['name'];
if(isset($data['captcha_type']))
$SampleCaptchaInstance->CaptchaType = $data['captcha_type'];
if(isset($data['enabled']))
$SampleCaptchaInstance->Enabled = (bool)$data['enabled'];
return $SampleCaptchaInstance;
}
}

View File

@ -49,10 +49,34 @@
"required": true,
"file": "Abstracts/CaptchaType.php"
},
{
"required": true,
"file": "Classes/Validate.php"
},
{
"required": true,
"file": "Classes/Security.php"
},
{
"required": true,
"file": "vCaptcha.php"
},
{
"required": true,
"file": "Exceptions/DatabaseException.php"
},
{
"required": true,
"file": "Exceptions/CaptchaInstanceNotFoundException.php"
},
{
"required": true,
"file": "Exceptions/InvalidCaptchaNameException.php"
},
{
"required": true,
"file": "Objects/SampleCaptchaInstance.php"
},
{
"required": true,
"file": "Objects/CaptchaInstance.php"