Path: blob/1.0-develop/app/Services/Databases/DatabaseManagementService.php
10262 views
<?php12namespace Pterodactyl\Services\Databases;34use Pterodactyl\Models\Server;5use Pterodactyl\Models\Database;6use Pterodactyl\Helpers\Utilities;7use Illuminate\Database\ConnectionInterface;8use Illuminate\Contracts\Encryption\Encrypter;9use Pterodactyl\Extensions\DynamicDatabaseConnection;10use Pterodactyl\Repositories\Eloquent\DatabaseRepository;11use Pterodactyl\Exceptions\Repository\DuplicateDatabaseNameException;12use Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException;13use Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException;1415class DatabaseManagementService16{17/**18* The regex used to validate that the database name passed through to the function is19* in the expected format.20*21* @see \Pterodactyl\Services\Databases\DatabaseManagementService::generateUniqueDatabaseName()22*/23private const MATCH_NAME_REGEX = '/^(s[\d]+_)(.*)$/';2425/**26* Determines if the service should validate the user's ability to create an additional27* database for this server. In almost all cases this should be true, but to keep things28* flexible you can also set it to false and create more databases than the server is29* allocated.30*/31protected bool $validateDatabaseLimit = true;3233public function __construct(34protected ConnectionInterface $connection,35protected DynamicDatabaseConnection $dynamic,36protected Encrypter $encrypter,37protected DatabaseRepository $repository,38) {39}4041/**42* Generates a unique database name for the given server. This name should be passed through when43* calling this handle function for this service, otherwise the database will be created with44* whatever name is provided.45*/46public static function generateUniqueDatabaseName(string $name, int $serverId): string47{48// Max of 48 characters, including the s123_ that we append to the front.49return sprintf('s%d_%s', $serverId, substr($name, 0, 48 - strlen("s{$serverId}_")));50}5152/**53* Set whether this class should validate that the server has enough slots54* left before creating the new database.55*/56public function setValidateDatabaseLimit(bool $validate): self57{58$this->validateDatabaseLimit = $validate;5960return $this;61}6263/**64* Create a new database that is linked to a specific host.65*66* @throws \Throwable67* @throws TooManyDatabasesException68* @throws DatabaseClientFeatureNotEnabledException69*/70public function create(Server $server, array $data): Database71{72if (!config('pterodactyl.client_features.databases.enabled')) {73throw new DatabaseClientFeatureNotEnabledException();74}7576if ($this->validateDatabaseLimit) {77// If the server has a limit assigned and we've already reached that limit, throw back78// an exception and kill the process.79if (!is_null($server->database_limit) && $server->databases()->count() >= $server->database_limit) {80throw new TooManyDatabasesException();81}82}8384// Protect against developer mistakes...85if (empty($data['database']) || !preg_match(self::MATCH_NAME_REGEX, $data['database'])) {86throw new \InvalidArgumentException('The database name passed to DatabaseManagementService::handle MUST be prefixed with "s{server_id}_".');87}8889$data = array_merge($data, [90'server_id' => $server->id,91'username' => sprintf('u%d_%s', $server->id, str_random(10)),92'password' => $this->encrypter->encrypt(93Utilities::randomStringWithSpecialCharacters(24)94),95]);9697$database = null;9899try {100return $this->connection->transaction(function () use ($data, &$database) {101$database = $this->createModel($data);102103$this->dynamic->set('dynamic', $data['database_host_id']);104105$this->repository->createDatabase($database->database);106$this->repository->createUser(107$database->username,108$database->remote,109$this->encrypter->decrypt($database->password),110$database->max_connections111);112$this->repository->assignUserToDatabase($database->database, $database->username, $database->remote);113$this->repository->flush();114115return $database;116});117} catch (\Exception $exception) {118try {119// This is actually incorrect, it can be null in the case that the $database model120// itself isn't able to be created in Pterodactyl's database.121//122// @phpstan-ignore-next-line instanceof.alwaysFalse123if ($database instanceof Database) {124$this->repository->dropDatabase($database->database);125$this->repository->dropUser($database->username, $database->remote);126$this->repository->flush();127}128} catch (\Throwable $deletionException) { // @phpstan-ignore catch.neverThrown129// Do nothing here. We've already encountered an issue before this point so no130// reason to prioritize this error over the initial one.131}132133throw $exception;134}135}136137/**138* Delete a database from the given host server.139*140* @throws \Exception141*/142public function delete(Database $database): ?bool143{144$this->dynamic->set('dynamic', $database->database_host_id);145146$this->repository->dropDatabase($database->database);147$this->repository->dropUser($database->username, $database->remote);148$this->repository->flush();149150return $database->delete();151}152153/**154* Create the database if there is not an identical match in the DB. While you can technically155* have the same name across multiple hosts, for the sake of keeping this logic easy to understand156* and avoiding user confusion we will ignore the specific host and just look across all hosts.157*158* @throws DuplicateDatabaseNameException159* @throws \Throwable160*/161protected function createModel(array $data): Database162{163$exists = Database::query()->where('server_id', $data['server_id'])164->where('database', $data['database'])165->exists();166167if ($exists) {168throw new DuplicateDatabaseNameException('A database with that name already exists for this server.');169}170171$database = (new Database())->forceFill($data);172$database->saveOrFail();173174return $database;175}176}177178179