misc_pterodactyl-panel/app/Services/Databases/DatabaseManagementService.php

211 lines
7.2 KiB
PHP
Raw Normal View History

2017-07-15 16:52:34 +00:00
<?php
namespace Pterodactyl\Services\Databases;
2017-07-15 16:52:34 +00:00
2019-09-06 04:32:57 +00:00
use Exception;
use InvalidArgumentException;
use Pterodactyl\Models\Server;
2017-10-24 01:12:15 +00:00
use Pterodactyl\Models\Database;
use Pterodactyl\Helpers\Utilities;
use Illuminate\Database\ConnectionInterface;
2017-07-15 16:52:34 +00:00
use Illuminate\Contracts\Encryption\Encrypter;
use Pterodactyl\Extensions\DynamicDatabaseConnection;
use Pterodactyl\Repositories\Eloquent\DatabaseRepository;
use Pterodactyl\Exceptions\Repository\DuplicateDatabaseNameException;
use Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException;
use Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException;
2017-07-15 16:52:34 +00:00
class DatabaseManagementService
2017-07-15 16:52:34 +00:00
{
/**
* The regex used to validate that the database name passed through to the function is
* in the expected format.
*
* @see \Pterodactyl\Services\Databases\DatabaseManagementService::generateUniqueDatabaseName()
*/
private const MATCH_NAME_REGEX = '/^(s[\d]+_)(.*)$/';
2017-07-15 16:52:34 +00:00
/**
* @var \Illuminate\Database\ConnectionInterface
2017-07-15 16:52:34 +00:00
*/
private $connection;
2017-07-15 16:52:34 +00:00
/**
* @var \Pterodactyl\Extensions\DynamicDatabaseConnection
*/
private $dynamic;
2017-07-15 16:52:34 +00:00
/**
* @var \Illuminate\Contracts\Encryption\Encrypter
*/
private $encrypter;
2017-07-15 16:52:34 +00:00
/**
* @var \Pterodactyl\Repositories\Eloquent\DatabaseRepository
2017-07-15 16:52:34 +00:00
*/
private $repository;
/**
* Determines if the service should validate the user's ability to create an additional
* database for this server. In almost all cases this should be true, but to keep things
* flexible you can also set it to false and create more databases than the server is
* allocated.
*
* @var bool
*/
protected $validateDatabaseLimit = true;
2017-07-15 16:52:34 +00:00
/**
* CreationService constructor.
*/
public function __construct(
ConnectionInterface $connection,
2017-07-15 16:52:34 +00:00
DynamicDatabaseConnection $dynamic,
DatabaseRepository $repository,
2017-07-15 16:52:34 +00:00
Encrypter $encrypter
) {
$this->connection = $connection;
2017-07-15 16:52:34 +00:00
$this->dynamic = $dynamic;
$this->encrypter = $encrypter;
$this->repository = $repository;
}
/**
* Generates a unique database name for the given server. This name should be passed through when
* calling this handle function for this service, otherwise the database will be created with
* whatever name is provided.
*/
public static function generateUniqueDatabaseName(string $name, int $serverId): string
{
// Max of 48 characters, including the s123_ that we append to the front.
return sprintf('s%d_%s', $serverId, substr($name, 0, 48 - strlen("s{$serverId}_")));
}
/**
* Set wether or not this class should validate that the server has enough slots
* left before creating the new database.
*
* @return $this
*/
public function setValidateDatabaseLimit(bool $validate): self
{
$this->validateDatabaseLimit = $validate;
return $this;
}
2017-07-15 16:52:34 +00:00
/**
* Create a new database that is linked to a specific host.
*
* @return \Pterodactyl\Models\Database
2017-07-15 16:52:34 +00:00
*
* @throws \Throwable
* @throws \Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException
* @throws \Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException
2017-07-15 16:52:34 +00:00
*/
public function create(Server $server, array $data)
2017-07-15 16:52:34 +00:00
{
2021-01-23 20:33:34 +00:00
if (!config('pterodactyl.client_features.databases.enabled')) {
throw new DatabaseClientFeatureNotEnabledException();
}
if ($this->validateDatabaseLimit) {
// If the server has a limit assigned and we've already reached that limit, throw back
// an exception and kill the process.
2021-01-23 20:33:34 +00:00
if (!is_null($server->database_limit) && $server->databases()->count() >= $server->database_limit) {
throw new TooManyDatabasesException();
}
}
// Protect against developer mistakes...
2021-01-23 20:33:34 +00:00
if (empty($data['database']) || !preg_match(self::MATCH_NAME_REGEX, $data['database'])) {
throw new InvalidArgumentException('The database name passed to DatabaseManagementService::handle MUST be prefixed with "s{server_id}_".');
}
$data = array_merge($data, [
'server_id' => $server->id,
'username' => sprintf('u%d_%s', $server->id, str_random(10)),
'password' => $this->encrypter->encrypt(
Utilities::randomStringWithSpecialCharacters(24)
),
]);
$database = null;
2017-07-15 16:52:34 +00:00
try {
return $this->connection->transaction(function () use ($data, &$database) {
$database = $this->createModel($data);
$this->dynamic->set('dynamic', $data['database_host_id']);
$this->repository->createDatabase($database->database);
$this->repository->createUser(
2021-01-23 20:33:34 +00:00
$database->username,
$database->remote,
$this->encrypter->decrypt($database->password),
$database->max_connections
);
$this->repository->assignUserToDatabase($database->database, $database->username, $database->remote);
$this->repository->flush();
return $database;
});
} catch (Exception $exception) {
2017-07-15 16:52:34 +00:00
try {
if ($database instanceof Database) {
$this->repository->dropDatabase($database->database);
$this->repository->dropUser($database->username, $database->remote);
$this->repository->flush();
2017-07-15 16:52:34 +00:00
}
} catch (Exception $deletionException) {
// Do nothing here. We've already encountered an issue before this point so no
// reason to prioritize this error over the initial one.
2017-07-15 16:52:34 +00:00
}
throw $exception;
2017-07-15 16:52:34 +00:00
}
}
/**
* Delete a database from the given host server.
*
* @return bool|null
*
* @throws \Exception
2017-07-15 16:52:34 +00:00
*/
public function delete(Database $database)
2017-07-15 16:52:34 +00:00
{
$this->dynamic->set('dynamic', $database->database_host_id);
$this->repository->dropDatabase($database->database);
$this->repository->dropUser($database->username, $database->remote);
$this->repository->flush();
2017-07-15 16:52:34 +00:00
return $database->delete();
2017-07-15 16:52:34 +00:00
}
/**
* Create the database if there is not an identical match in the DB. While you can technically
* have the same name across multiple hosts, for the sake of keeping this logic easy to understand
* and avoiding user confusion we will ignore the specific host and just look across all hosts.
*
* @throws \Pterodactyl\Exceptions\Repository\DuplicateDatabaseNameException
* @throws \Throwable
*/
protected function createModel(array $data): Database
{
$exists = Database::query()->where('server_id', $data['server_id'])
->where('database', $data['database'])
->exists();
if ($exists) {
2021-01-23 20:33:34 +00:00
throw new DuplicateDatabaseNameException('A database with that name already exists for this server.');
}
2021-01-23 20:33:34 +00:00
$database = (new Database())->forceFill($data);
$database->saveOrFail();
return $database;
}
2017-07-15 16:52:34 +00:00
}