|
|
|
|
@@ -1,492 +0,0 @@
|
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* AVSDev UF Organisations (https://avsdev.uk)
|
|
|
|
|
*
|
|
|
|
|
* @link https://git.avsdev.uk/avsdev/sprinkle-organisations
|
|
|
|
|
* @license https://git.avsdev.uk/avsdev/sprinkle-organisations/blob/master/LICENSE.md (LGPL-3.0 License)
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
namespace UserFrosting\Sprinkle\Organisations\Repository;
|
|
|
|
|
|
|
|
|
|
use Carbon\Carbon;
|
|
|
|
|
use UserFrosting\Sprinkle\Organisations\Repository\Interfaces\TokenOwnerInterface;
|
|
|
|
|
use UserFrosting\Sprinkle\Core\Database\Models\Model;
|
|
|
|
|
use UserFrosting\Sprinkle\Core\Util\ClassMapper;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* An abstract class for interacting with a repository of time-sensitive tokens which can be owned by any object.
|
|
|
|
|
*
|
|
|
|
|
* Tokens are used, for example, to perform password resets and new account email verifications.
|
|
|
|
|
*
|
|
|
|
|
* @author Craig Williams (https://avsdev.uk)
|
|
|
|
|
*/
|
|
|
|
|
abstract class BasicTokenRepository
|
|
|
|
|
{
|
|
|
|
|
/**
|
|
|
|
|
* @var ClassMapper
|
|
|
|
|
*/
|
|
|
|
|
protected $classMapper;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @var string
|
|
|
|
|
*/
|
|
|
|
|
protected $algorithm;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @var string
|
|
|
|
|
*/
|
|
|
|
|
protected $modelIdentifier;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @var TokenLogger
|
|
|
|
|
*/
|
|
|
|
|
protected $tokenLogger;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create a new TokenRepository object.
|
|
|
|
|
*
|
|
|
|
|
* @param ClassMapper $classMapper Maps generic class identifiers to specific class names.
|
|
|
|
|
* @param string $algorithm The hashing algorithm to use when storing generated tokens.
|
|
|
|
|
*/
|
|
|
|
|
public function __construct(ClassMapper $classMapper, $algorithm = 'sha512', $tokenLogger = null, $debug = false)
|
|
|
|
|
{
|
|
|
|
|
$this->classMapper = $classMapper;
|
|
|
|
|
$this->algorithm = $algorithm;
|
|
|
|
|
$this->tokenLogger = ($debug ? $tokenLogger : null);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create a new token.
|
|
|
|
|
*
|
|
|
|
|
* @param TokenOwnerInterface $tokenOwner The object to associate with this token, be it an organisation, user, group etc.
|
|
|
|
|
* @param int $timeout The time, in seconds, after which this token should expire.
|
|
|
|
|
*
|
|
|
|
|
* @return Model The model (PasswordReset, Verification, etc) object that stores the token.
|
|
|
|
|
*/
|
|
|
|
|
public function create(TokenOwnerInterface $tokenOwner, $timeout)
|
|
|
|
|
{
|
|
|
|
|
// Remove any previous tokens for this tokenOwner
|
|
|
|
|
$this->removeExisting($tokenOwner);
|
|
|
|
|
|
|
|
|
|
if ($this->tokenLogger) {
|
|
|
|
|
$this->tokenLogger->debug('Creating new token for {{owner}} which expires in {{timeout}} seconds', [
|
|
|
|
|
'tokenOwner' => $tokenOwner,
|
|
|
|
|
'timeout' => $timeout
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Compute expiration time
|
|
|
|
|
$expiresAt = Carbon::now()->addSeconds($timeout);
|
|
|
|
|
|
|
|
|
|
$model = $this->classMapper->createInstance($this->modelIdentifier);
|
|
|
|
|
|
|
|
|
|
// Generate a random token
|
|
|
|
|
$model->setToken($this->generateRandomToken());
|
|
|
|
|
|
|
|
|
|
// Hash the password reset token for the stored version
|
|
|
|
|
$hash = hash($this->algorithm, $model->getToken());
|
|
|
|
|
|
|
|
|
|
$model->fill([
|
|
|
|
|
'owner_id' => $tokenOwner->getId(),
|
|
|
|
|
'hash' => $hash,
|
|
|
|
|
'completed' => false,
|
|
|
|
|
'expires_at' => ($timeout >= 0 ? $expiresAt : null),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$model->save();
|
|
|
|
|
|
|
|
|
|
if ($this->tokenLogger) {
|
|
|
|
|
$this->tokenLogger->debug('Completed new token', ['model' => $model]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $model;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Cancels a specified token by removing it from the database.
|
|
|
|
|
*
|
|
|
|
|
* @param int $token The token to remove.
|
|
|
|
|
*
|
|
|
|
|
* @return Model|false
|
|
|
|
|
*/
|
|
|
|
|
public function cancel($token)
|
|
|
|
|
{
|
|
|
|
|
if ($this->tokenLogger) {
|
|
|
|
|
$this->tokenLogger->debug('Cancelling token {{token}}', [
|
|
|
|
|
'token' => $token
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Hash the password reset token for the stored version
|
|
|
|
|
$hash = hash($this->algorithm, $token);
|
|
|
|
|
|
|
|
|
|
// Find an incomplete reset request for the specified hash
|
|
|
|
|
$model = $this->classMapper->getClassMapping($this->modelIdentifier)::query()
|
|
|
|
|
->where('hash', $hash)
|
|
|
|
|
->where('completed', false)
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
if ($model === null) {
|
|
|
|
|
if ($this->tokenLogger) {
|
|
|
|
|
$this->tokenLogger->warn('Token not found!');
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($this->tokenLogger) {
|
|
|
|
|
$this->tokenLogger->debug('Deleting matched model', [
|
|
|
|
|
'model' => $model
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$model->delete();
|
|
|
|
|
|
|
|
|
|
return $model;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Completes a token-based process, invoking updateTokenOwner() in the child object to do the actual action.
|
|
|
|
|
*
|
|
|
|
|
* @param int $token The token to complete.
|
|
|
|
|
* @param mixed[] $params An optional list of parameters to pass to updateUser().
|
|
|
|
|
*
|
|
|
|
|
* @return Model|false
|
|
|
|
|
*/
|
|
|
|
|
public function complete($token, $params = [])
|
|
|
|
|
{
|
|
|
|
|
if ($this->tokenLogger) {
|
|
|
|
|
$this->tokenLogger->debug('Completing token for {{token}}', [
|
|
|
|
|
'token' => $token
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Hash the token for the stored version
|
|
|
|
|
$hash = hash($this->algorithm, $token);
|
|
|
|
|
|
|
|
|
|
// Find an unexpired, incomplete token for the specified hash
|
|
|
|
|
$model = $this->classMapper->getClassMapping($this->modelIdentifier)::query()
|
|
|
|
|
->where('hash', $hash)
|
|
|
|
|
->where('completed', false)
|
|
|
|
|
->where(function($query) {
|
|
|
|
|
return $query->where('expires_at', '>', Carbon::now())->orWhereNull('expires_at');
|
|
|
|
|
})
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
if ($model === null) {
|
|
|
|
|
if ($this->tokenLogger) {
|
|
|
|
|
$this->tokenLogger->warn('Token not found!');
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($this->tokenLogger) {
|
|
|
|
|
$this->tokenLogger->debug('Found:', [
|
|
|
|
|
'model' => $model
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$ret = $this->updateTokenOwner($model->owner_id, $model, $params);
|
|
|
|
|
|
|
|
|
|
if ($this->tokenLogger) {
|
|
|
|
|
$this->tokenLogger->debug('Return of updateTokenOwner: {{ret}}', [
|
|
|
|
|
'ret' => $ret
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$model->fill([
|
|
|
|
|
'completed' => true,
|
|
|
|
|
'completed_at' => Carbon::now(),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$model->save();
|
|
|
|
|
|
|
|
|
|
return $model;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Completes a token-based process using the owner instead of the token,
|
|
|
|
|
* invoking updateTokenOwner() in the child object to do the actual action.
|
|
|
|
|
*
|
|
|
|
|
* @param int $token The token to complete.
|
|
|
|
|
* @param mixed[] $params An optional list of parameters to pass to updateUser().
|
|
|
|
|
*
|
|
|
|
|
* @return Model|false
|
|
|
|
|
*/
|
|
|
|
|
public function completeForOwner(TokenOwnerInterface $tokenOwner, $params = [])
|
|
|
|
|
{
|
|
|
|
|
if ($this->tokenLogger) {
|
|
|
|
|
$this->tokenLogger->debug('Completing token for {{owner}}', [
|
|
|
|
|
'owner' => $tokenOwner
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Find an unexpired, incomplete tokens for the specified owner.
|
|
|
|
|
// Using first() works because owners can only have at most 1 active token
|
|
|
|
|
$model = $this->classMapper->getClassMapping($this->modelIdentifier)::query()
|
|
|
|
|
->where('owner_id', $tokenOwner->getId())
|
|
|
|
|
->where('completed', false)
|
|
|
|
|
->where(function($query) {
|
|
|
|
|
return $query->where('expires_at', '>', Carbon::now())->orWhereNull('expires_at');
|
|
|
|
|
})
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
if ($model === null) {
|
|
|
|
|
if ($this->tokenLogger) {
|
|
|
|
|
$this->tokenLogger->warn('Token not found!');
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($this->tokenLogger) {
|
|
|
|
|
$this->tokenLogger->debug('Found:', [
|
|
|
|
|
'model' => $model
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$ret = $this->updateTokenOwner($model->owner_id, $model, $params);
|
|
|
|
|
|
|
|
|
|
if ($this->tokenLogger) {
|
|
|
|
|
$this->tokenLogger->debug('Return of updateTokenOwner: {{ret}}', [
|
|
|
|
|
'ret' => $ret
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$model->fill([
|
|
|
|
|
'completed' => true,
|
|
|
|
|
'completed_at' => Carbon::now(),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$model->save();
|
|
|
|
|
|
|
|
|
|
return $model;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Reverts a completed token request to its previously incomplete state.
|
|
|
|
|
*
|
|
|
|
|
* @param TokenOwnerInterface $tokenOwner The owner of the token to revert.
|
|
|
|
|
*
|
|
|
|
|
* @return Model|false
|
|
|
|
|
*/
|
|
|
|
|
public function revert(TokenOwnerInterface $tokenOwner)
|
|
|
|
|
{
|
|
|
|
|
if ($this->tokenLogger) {
|
|
|
|
|
$this->tokenLogger->debug('Reverting token for {{owner}}', [
|
|
|
|
|
'owner' => $tokenOwner
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Find an unexpired, incomplete tokens for the specified owner.
|
|
|
|
|
// Using first() works because owners can only have at most 1 active token
|
|
|
|
|
$model = $this->classMapper->getClassMapping($this->modelIdentifier)::query()
|
|
|
|
|
->where('owner_id', $tokenOwner->getId())
|
|
|
|
|
->where('completed', true)
|
|
|
|
|
->where(function($query) {
|
|
|
|
|
return $query->where('expires_at', '>', Carbon::now())->orWhereNull('expires_at');
|
|
|
|
|
})
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
if ($model === null) {
|
|
|
|
|
if ($this->tokenLogger) {
|
|
|
|
|
$this->tokenLogger->warn('Token not found!');
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($this->tokenLogger) {
|
|
|
|
|
$this->tokenLogger->debug('Found:', [
|
|
|
|
|
'model' => $model
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$model->fill([
|
|
|
|
|
'completed' => false,
|
|
|
|
|
'completed_at' => null,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$model->save();
|
|
|
|
|
|
|
|
|
|
return $model;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Determine if a specified token owner has an incomplete and unexpired token.
|
|
|
|
|
*
|
|
|
|
|
* @param TokenOwnerInterface $tokenOwner The token owner object to look up.
|
|
|
|
|
* @param int $token Optionally, try to match a specific token.
|
|
|
|
|
*
|
|
|
|
|
* @return Model|false
|
|
|
|
|
*/
|
|
|
|
|
public function exists(TokenOwnerInterface $tokenOwner, $token = null)
|
|
|
|
|
{
|
|
|
|
|
if ($this->tokenLogger) {
|
|
|
|
|
$this->tokenLogger->debug('Searching for token for {{owner}} (with optional {{token}})', [
|
|
|
|
|
'owner' => $tokenOwner,
|
|
|
|
|
'token' => $token
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$model = $this->classMapper->getClassMapping($this->modelIdentifier)::query()
|
|
|
|
|
->where('owner_id', $tokenOwner->id)
|
|
|
|
|
->where('completed', false)
|
|
|
|
|
->where(function($query) {
|
|
|
|
|
return $query->where('expires_at', '>', Carbon::now())->orWhereNull('expires_at');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if ($token) {
|
|
|
|
|
// get token hash
|
|
|
|
|
$hash = hash($this->algorithm, $token);
|
|
|
|
|
if ($this->tokenLogger) {
|
|
|
|
|
$this->tokenLogger->debug('Token hash: {{hash}}', [
|
|
|
|
|
'hash' => $hash
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
$model->where('hash', $hash);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$result = $model->first() ?: false;
|
|
|
|
|
|
|
|
|
|
if ($this->tokenLogger) {
|
|
|
|
|
$this->tokenLogger->debug('Found:', [
|
|
|
|
|
'result' => $result
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Find an unexpired and un-completed token for the specified owner.
|
|
|
|
|
*
|
|
|
|
|
* @param TokenOwnerInterface $tokenOwner The token owner object to look up.
|
|
|
|
|
* @param int $token Optionally, try to match a specific token.
|
|
|
|
|
*
|
|
|
|
|
* @return owner_id|null
|
|
|
|
|
*/
|
|
|
|
|
public function findOwner($token)
|
|
|
|
|
{
|
|
|
|
|
if ($this->tokenLogger) {
|
|
|
|
|
$this->tokenLogger->debug('Searching token for owner of {{token}}', [
|
|
|
|
|
'token' => $token
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// get token hash
|
|
|
|
|
$hash = hash($this->algorithm, $token);
|
|
|
|
|
|
|
|
|
|
if ($this->tokenLogger) {
|
|
|
|
|
$this->tokenLogger->debug('Token hash: {{hash}}', [
|
|
|
|
|
'hash' => $hash
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$model = $this->classMapper->getClassMapping($this->modelIdentifier)::query()
|
|
|
|
|
->where('hash', $hash)
|
|
|
|
|
->where('completed', false)
|
|
|
|
|
->where(function($query) {
|
|
|
|
|
return $query->where('expires_at', '>', Carbon::now())->orWhereNull('expires_at');
|
|
|
|
|
})
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
if ($this->tokenLogger) {
|
|
|
|
|
$this->tokenLogger->debug('Found:', [
|
|
|
|
|
'model' => $model
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $model ? $model->owner_id : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Delete all existing tokens from the database for a particular owner.
|
|
|
|
|
*
|
|
|
|
|
* @param TokenOwnerInterface $tokenOwner
|
|
|
|
|
*
|
|
|
|
|
* @return int
|
|
|
|
|
*/
|
|
|
|
|
public function removeExisting(TokenOwnerInterface $tokenOwner)
|
|
|
|
|
{
|
|
|
|
|
if ($this->tokenLogger) {
|
|
|
|
|
$this->tokenLogger->debug('Removing all tokens for {{owner}}', [
|
|
|
|
|
'owner' => $tokenOwner
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$result = $this->classMapper->getClassMapping($this->modelIdentifier)::query()
|
|
|
|
|
->where('owner_id', $tokenOwner->getId())
|
|
|
|
|
->delete();
|
|
|
|
|
|
|
|
|
|
if ($this->tokenLogger) {
|
|
|
|
|
$this->tokenLogger->debug('Deletion result: {{result}}', [
|
|
|
|
|
'result' => $result
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Remove all expired tokens from the database.
|
|
|
|
|
*
|
|
|
|
|
* @return bool|null
|
|
|
|
|
*/
|
|
|
|
|
public function removeExpired()
|
|
|
|
|
{
|
|
|
|
|
if ($this->tokenLogger) {
|
|
|
|
|
$this->tokenLogger->debug('Removing expired tokens...');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$result = $this->classMapper->getClassMapping($this->modelIdentifier)::query()
|
|
|
|
|
->where('completed', false)
|
|
|
|
|
->whereNotNull('expires_at')
|
|
|
|
|
->where('expires_at', '<', Carbon::now())
|
|
|
|
|
->delete();
|
|
|
|
|
|
|
|
|
|
if ($this->tokenLogger) {
|
|
|
|
|
$this->tokenLogger->debug('Deletion result: {{result}}', [
|
|
|
|
|
'result' => $result
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Generate a new random token.
|
|
|
|
|
*
|
|
|
|
|
* This generates a token to use for verifying anything you like, with a unique reference.
|
|
|
|
|
*
|
|
|
|
|
* @return string
|
|
|
|
|
*/
|
|
|
|
|
protected function generateRandomToken()
|
|
|
|
|
{
|
|
|
|
|
do {
|
|
|
|
|
$gen = md5(uniqid(mt_rand(), false));
|
|
|
|
|
} while ($this->classMapper->getClassMapping($this->modelIdentifier)::query()
|
|
|
|
|
->where('hash', hash($this->algorithm, $gen))
|
|
|
|
|
->first());
|
|
|
|
|
|
|
|
|
|
if ($this->tokenLogger) {
|
|
|
|
|
$this->tokenLogger->debug('Generated token {{token}}', [
|
|
|
|
|
'token' => $gen
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $gen;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Modify the owner of the token during the token completion process.
|
|
|
|
|
*
|
|
|
|
|
* This method is called during complete(), and is a way for concrete implementations to modify the owner.
|
|
|
|
|
*
|
|
|
|
|
* @param integer $owner_id The id of the token owner
|
|
|
|
|
* @param mixed[] $args
|
|
|
|
|
*
|
|
|
|
|
* @return mixed[] $args the list of parameters that were supplied to the call to `complete()`
|
|
|
|
|
*/
|
|
|
|
|
abstract protected function updateTokenOwner($owner_id, $model, $args);
|
|
|
|
|
}
|