449 lines
17 KiB
PHP
449 lines
17 KiB
PHP
<?php
|
|
/**
|
|
* FuzeWorks Authentication Plugin.
|
|
*
|
|
* The FuzeWorks PHP FrameWork
|
|
*
|
|
* Copyright (C) 2013 - 2023 i15
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in all
|
|
* copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
* SOFTWARE.
|
|
*
|
|
* @author i15
|
|
* @copyright Copyright (C) 2013 - 2023, i15. (https://i15.nl)
|
|
* @license https://opensource.org/licenses/MIT MIT License
|
|
*
|
|
* @since Version 1.3.0
|
|
*
|
|
* @version Version 1.3.0
|
|
*/
|
|
|
|
namespace FuzeWorks\Authentication\Drivers;
|
|
|
|
use FuzeWorks\Authentication\Exceptions\AuthenticationException;
|
|
use FuzeWorks\Authentication\Model\User;
|
|
use FuzeWorks\Authentication\UsersModelDriverInterface;
|
|
use FuzeWorks\DatabaseEngine\iDatabaseEngine;
|
|
use FuzeWorks\DatabaseEngine\PDOEngine;
|
|
use FuzeWorks\Exception\DatabaseException;
|
|
use FuzeWorks\Exception\TransactionException;
|
|
use FuzeWorks\Factory;
|
|
use FuzeWorks\Logger;
|
|
use FuzeWorks\ObjectStorage\ObjectStorageComponent;
|
|
use PDO;
|
|
|
|
class PdoUsersModelDriver implements UsersModelDriverInterface
|
|
{
|
|
|
|
/** @var PDOEngine $engine */
|
|
protected iDatabaseEngine $engine;
|
|
protected array $schema;
|
|
|
|
public function __construct(iDatabaseEngine $engine)
|
|
{
|
|
$this->engine = $engine;
|
|
|
|
// Load object storage
|
|
/** @var ObjectStorageComponent $storage */
|
|
$storage = Factory::getInstance("storage");
|
|
$cache = $storage->getCache();
|
|
|
|
// Lookup the current schema
|
|
$schema = $cache->get('authPdoUsersSchema');
|
|
if (is_null($schema))
|
|
{
|
|
$schema = $this->loadSchema();
|
|
$cache->set('authPdoUsersSchema', $schema, 3600);
|
|
}
|
|
|
|
$this->schema = $schema;
|
|
}
|
|
|
|
/**
|
|
* Reads out the database for the current schema. Important to know which columns are available
|
|
* in the properties.
|
|
*
|
|
* @return array
|
|
* @throws AuthenticationException
|
|
*/
|
|
private function loadSchema(): array
|
|
{
|
|
// Check if the users table exist
|
|
$statement = $this->engine->prepare("SELECT * FROM information_schema.tables WHERE `table_name` = :table_name LIMIT 1");
|
|
$statement->execute(['table_name' => "users"]);
|
|
if ($statement->rowCount() !== 1 && !$this->populateSchema())
|
|
throw new AuthenticationException("Could not initiate Authentication. Database schema is missing.");
|
|
|
|
// Read the properties table and find all existing columns
|
|
$statement = $this->engine->prepare("SELECT `column_name` FROM information_schema.columns WHERE `table_name` = :table_name");
|
|
$statement->execute(['table_name' => "users_properties"]);
|
|
if ($statement->rowCount() < 1)
|
|
throw new AuthenticationException("Could not initiate Authentication. Database error.");
|
|
|
|
// Fetch columns and remove standard columns
|
|
$columns = $statement->fetchAll(PDO::FETCH_COLUMN);
|
|
$columns = array_splice($columns, 1);
|
|
|
|
return ['properties' => $columns];
|
|
}
|
|
|
|
private function populateSchema(): bool
|
|
{
|
|
// @todo Implement schema populator
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Adds the property to the schema. This will add the column to the properties table and save the
|
|
* new schema to the cache.
|
|
*
|
|
* @param string $propertyName
|
|
* @param mixed $propertyExample
|
|
* @return bool
|
|
* @throws DatabaseException
|
|
*/
|
|
private function addSchemaProperty(string $propertyName, mixed $propertyExample): bool
|
|
{
|
|
// First check if the property is not already in the schema
|
|
$illegal = ['id', 'primaryEmail', 'password', 'mfaSecret', 'permissions', 'active', 'emailVerifyToken', 'emailVerifyExpiry', 'properties', 'user_id'];
|
|
if (in_array($propertyName, $illegal))
|
|
{
|
|
Logger::logError("Could not add property to schema. Illegal property name: " . $propertyName);
|
|
return false;
|
|
}
|
|
|
|
// Check the type of the variable
|
|
$type = gettype($propertyExample);
|
|
switch ($type)
|
|
{
|
|
case "integer":
|
|
$type = "INT";
|
|
break;
|
|
case "double":
|
|
$type = "DOUBLE";
|
|
break;
|
|
case "string":
|
|
$type = "VARCHAR(255)";
|
|
break;
|
|
case "boolean":
|
|
$type = "TINYINT(1)";
|
|
break;
|
|
default:
|
|
Logger::logError("Could not add property to schema. Unknown type: " . $type);
|
|
return false;
|
|
}
|
|
|
|
// Add the column to the properties table
|
|
$this->engine->query("ALTER TABLE `users_properties` ADD $propertyName $type NULL");
|
|
|
|
// Save the new schema
|
|
$this->schema['properties'][] = $propertyName;
|
|
|
|
/** @var ObjectStorageComponent $storage */
|
|
$storage = Factory::getInstance("storage");
|
|
$cache = $storage->getCache();
|
|
$cache->set('authPdoUsersSchema', $this->schema, 3600);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
public function createUsers(array $users): bool
|
|
{
|
|
try {
|
|
// Prepare statements
|
|
$users_sql = "INSERT INTO `users` (id, primaryEmail, password, mfaSecret, permissions, active, emailVerifyToken, emailVerifyExpiry) VALUES (:id, :primaryEmail, :password, :mfaSecret, :permissions, :active, :emailVerifyToken, :emailVerifyExpiry)";
|
|
$properties_sql = "INSERT INTO `users_properties` (user_id) VALUES (:id)";
|
|
$users_statement = $this->engine->prepare($users_sql);
|
|
$properties_statement = $this->engine->prepare($properties_sql);
|
|
|
|
// Loop over each property
|
|
foreach ($users as $user)
|
|
foreach ($user->properties as $property => $value)
|
|
// Check if the property is already in the schema
|
|
if (!in_array($property, $this->schema['properties']))
|
|
// Add the property to the schema
|
|
if (!$this->addSchemaProperty($property, $value))
|
|
throw new DatabaseException("Could not add property to schema.");
|
|
|
|
// Start transaction
|
|
$this->engine->transactionStart();
|
|
|
|
// Loop over each user
|
|
foreach ($users as $user)
|
|
{
|
|
// First insert the user into the general users table
|
|
$users_statement->execute([
|
|
"id" => $user->id,
|
|
"primaryEmail" => $user->primaryEmail,
|
|
"password" => $user->password,
|
|
"mfaSecret" => $user->mfaSecret,
|
|
"permissions" => implode(";", $user->permissions),
|
|
"active" => (int) $user->active,
|
|
"emailVerifyToken" => $user->emailVerifyToken,
|
|
"emailVerifyExpiry" => $user->emailVerifyExpiry
|
|
]);
|
|
if ($users_statement->rowCount() !== 1)
|
|
{
|
|
$this->engine->transactionRollback();
|
|
return false;
|
|
}
|
|
|
|
// Then append the user id to the properties table
|
|
$properties_statement->execute(['id' => $user->id]);
|
|
|
|
// Insert all properties
|
|
foreach ($user->properties as $propertyKey => $propertyValue)
|
|
{
|
|
// Insert the property
|
|
$statement = $this->engine->prepare("UPDATE `users_properties` SET $propertyKey = :propertyValue WHERE `user_id` = :id");
|
|
$statement->execute([
|
|
"propertyValue" => $propertyValue,
|
|
"id" => $user->id
|
|
]);
|
|
if ($statement->rowCount() !== 1)
|
|
{
|
|
$this->engine->transactionRollback();
|
|
Logger::logError("Could not create users. Database error.");
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Commit the transaction
|
|
return $this->engine->transactionCommit();
|
|
} catch (TransactionException $e) {
|
|
Logger::logError("Could not create users. Transaction failure: " . $e->getMessage());
|
|
} catch (DatabaseException $e) {
|
|
Logger::logError("Could not create users. Database error: " . $e->getMessage());
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
public function readUsers(array $filter, array $options = []): array
|
|
{
|
|
// Prepare the where statement
|
|
if (empty($filter))
|
|
$where = '';
|
|
else
|
|
{
|
|
$whereKeys = [];
|
|
foreach ($filter as $filterKey => $filterVal)
|
|
$whereKeys[] = $filterKey . '=:' . $filterKey;
|
|
|
|
$where = 'WHERE ' . implode(' AND ', $whereKeys);
|
|
}
|
|
|
|
// Prepare statement
|
|
$sql = "SELECT * FROM `users` LEFT JOIN `users_properties` ON users.`id`=users_properties.`user_id` $where";
|
|
$statement = $this->engine->prepare($sql);
|
|
|
|
// And execute the query
|
|
foreach ($filter as $key => $val)
|
|
{
|
|
if (is_null($val))
|
|
$statement->bindValue(':' . $key, $val, PDO::PARAM_NULL);
|
|
else
|
|
$statement->bindValue(':' . $key, $val);
|
|
}
|
|
|
|
// And at last, execute
|
|
$statement->execute();
|
|
$results = $statement->fetchAll(PDO::FETCH_OBJ);
|
|
$out = [];
|
|
foreach ($results as $result)
|
|
{
|
|
$user = new User($result->id);
|
|
if (isset($result->primaryEmail))
|
|
$user->primaryEmail = $result->primaryEmail;
|
|
if (isset($result->password))
|
|
$user->password = $result->password;
|
|
if (isset($result->mfaSecret))
|
|
$user->mfaSecret = $result->mfaSecret;
|
|
if (isset($result->permissions))
|
|
$user->permissions = explode(";", $result->permissions);
|
|
if (isset($result->active))
|
|
$user->active = (bool) $result->active;
|
|
if (isset($result->emailVerifyToken))
|
|
$user->emailVerifyToken = $result->emailVerifyToken;
|
|
if (isset($result->emailVerifyExpiry))
|
|
$user->emailVerifyExpiry = $result->emailVerifyExpiry;
|
|
|
|
// And map properties
|
|
foreach ($this->schema['properties'] as $property)
|
|
if (isset($result->$property))
|
|
$user->properties[$property] = $result->$property;
|
|
|
|
$out[] = $user;
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
public function updateUsers(array $users): bool
|
|
{
|
|
try {
|
|
// Loop over each property
|
|
foreach ($users as $user)
|
|
foreach ($user->properties as $property => $value)
|
|
// Check if the property is already in the schema
|
|
if (!in_array($property, $this->schema['properties']))
|
|
// Add the property to the schema
|
|
if (!$this->addSchemaProperty($property, $value))
|
|
throw new DatabaseException("Could not add property to schema.");
|
|
|
|
// Start transaction
|
|
$this->engine->transactionStart();
|
|
|
|
foreach ($users as $user)
|
|
{
|
|
// Get the unaltered user
|
|
$oldUser = $this->readUsers(['id' => $user->id])[0];
|
|
|
|
// Check if there is a difference
|
|
$vars = [];
|
|
if ($user->primaryEmail != $oldUser->primaryEmail)
|
|
$vars['primaryEmail'] = $user->primaryEmail;
|
|
if ($user->password != $oldUser->password)
|
|
$vars['password'] = $user->password;
|
|
if ($user->mfaSecret != $oldUser->mfaSecret)
|
|
$vars['mfaSecret'] = $user->mfaSecret;
|
|
if ($user->permissions != $oldUser->permissions)
|
|
$vars['permissions'] = implode(";", $user->permissions);
|
|
if ($user->active != $oldUser->active)
|
|
$vars['active'] = (int) $user->active;
|
|
if ($user->emailVerifyToken != $oldUser->emailVerifyToken)
|
|
$vars['emailVerifyToken'] = $user->emailVerifyToken;
|
|
if ($user->emailVerifyExpiry != $oldUser->emailVerifyExpiry)
|
|
$vars['emailVerifyExpiry'] = $user->emailVerifyExpiry;
|
|
|
|
// Prepare statement
|
|
$sql = "UPDATE `users` SET " . implode(', ', array_map(function ($key) {
|
|
return $key . '=:' . $key;
|
|
}, array_keys($vars))) . " WHERE `id`=:id";
|
|
$statement = $this->engine->prepare($sql);
|
|
|
|
// Bind values
|
|
foreach ($vars as $key => $val)
|
|
$statement->bindValue(':' . $key, $val);
|
|
|
|
// Bind id
|
|
$statement->bindValue(':id', $user->id);
|
|
|
|
// And execute
|
|
if (!empty($vars))
|
|
$statement->execute();
|
|
|
|
// Update all properties
|
|
$vars = [];
|
|
|
|
// First check if any properties of oldUser are removed
|
|
foreach ($oldUser->properties as $property => $value)
|
|
if (!isset($user->properties[$property]))
|
|
$vars[$property] = null;
|
|
|
|
// Then check if any properties of newUser are added
|
|
foreach ($user->properties as $property => $value)
|
|
if (!isset($oldUser->properties[$property]) || $oldUser->properties[$property] !== $value)
|
|
$vars[$property] = $value;
|
|
|
|
// If there are no changes, continue
|
|
if (empty($vars))
|
|
continue;
|
|
|
|
// Prepare statement
|
|
$sql = "UPDATE `users_properties` SET " . implode(', ', array_map(function ($key) {
|
|
return $key . '=:' . $key;
|
|
}, array_keys($vars))) . " WHERE `user_id`=:id";
|
|
$statement = $this->engine->prepare($sql);
|
|
|
|
// Bind values
|
|
foreach ($vars as $key => $val)
|
|
$statement->bindValue(':' . $key, $val);
|
|
|
|
// Bind id
|
|
$statement->bindValue(':id', $user->id);
|
|
|
|
// And execute
|
|
$statement->execute();
|
|
}
|
|
|
|
// Commit the transaction
|
|
return $this->engine->transactionCommit();
|
|
} catch (TransactionException $e) {
|
|
Logger::logError("Could not create users. Transaction failure: " . $e->getMessage());
|
|
} catch (DatabaseException $e) {
|
|
Logger::logError("Could not create users. Database error: " . $e->getMessage());
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @inheritDoc
|
|
*/
|
|
public function deleteUsers(array $users): bool
|
|
{
|
|
try {
|
|
// Start transaction
|
|
$this->engine->transactionStart();
|
|
|
|
// Prepare statements
|
|
$statement = $this->engine->prepare("DELETE FROM `users` WHERE `id`=:id");
|
|
$statement2 = $this->engine->prepare("DELETE FROM `users_properties` WHERE `user_id`=:id");
|
|
|
|
// Loop over each user
|
|
foreach ($users as $user)
|
|
{
|
|
// Bind id
|
|
$statement->bindValue(':id', $user->id);
|
|
$statement2->bindValue(':id', $user->id);
|
|
|
|
// And execute
|
|
$statement2->execute();
|
|
$statement->execute();
|
|
|
|
// Verify that the user was deleted with rowCount
|
|
if ($statement->rowCount() !== 1 || $statement2->rowCount() !== 1)
|
|
{
|
|
$this->engine->transactionRollback();
|
|
Logger::logError("Could not delete users. Database error.");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Commit the transaction
|
|
return $this->engine->transactionCommit();
|
|
} catch (TransactionException $e) {
|
|
Logger::logError("Could not delete users. Transaction failure: " . $e->getMessage());
|
|
} catch (DatabaseException $e) {
|
|
Logger::logError("Could not delete users. Database error: " . $e->getMessage());
|
|
}
|
|
|
|
return false;
|
|
}
|
|
} |