Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
pterodactyl
GitHub Repository: pterodactyl/panel
Path: blob/1.0-develop/app/Services/Databases/DatabaseManagementService.php
10262 views
1
<?php
2
3
namespace Pterodactyl\Services\Databases;
4
5
use Pterodactyl\Models\Server;
6
use Pterodactyl\Models\Database;
7
use Pterodactyl\Helpers\Utilities;
8
use Illuminate\Database\ConnectionInterface;
9
use Illuminate\Contracts\Encryption\Encrypter;
10
use Pterodactyl\Extensions\DynamicDatabaseConnection;
11
use Pterodactyl\Repositories\Eloquent\DatabaseRepository;
12
use Pterodactyl\Exceptions\Repository\DuplicateDatabaseNameException;
13
use Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException;
14
use Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException;
15
16
class DatabaseManagementService
17
{
18
/**
19
* The regex used to validate that the database name passed through to the function is
20
* in the expected format.
21
*
22
* @see \Pterodactyl\Services\Databases\DatabaseManagementService::generateUniqueDatabaseName()
23
*/
24
private const MATCH_NAME_REGEX = '/^(s[\d]+_)(.*)$/';
25
26
/**
27
* Determines if the service should validate the user's ability to create an additional
28
* database for this server. In almost all cases this should be true, but to keep things
29
* flexible you can also set it to false and create more databases than the server is
30
* allocated.
31
*/
32
protected bool $validateDatabaseLimit = true;
33
34
public function __construct(
35
protected ConnectionInterface $connection,
36
protected DynamicDatabaseConnection $dynamic,
37
protected Encrypter $encrypter,
38
protected DatabaseRepository $repository,
39
) {
40
}
41
42
/**
43
* Generates a unique database name for the given server. This name should be passed through when
44
* calling this handle function for this service, otherwise the database will be created with
45
* whatever name is provided.
46
*/
47
public static function generateUniqueDatabaseName(string $name, int $serverId): string
48
{
49
// Max of 48 characters, including the s123_ that we append to the front.
50
return sprintf('s%d_%s', $serverId, substr($name, 0, 48 - strlen("s{$serverId}_")));
51
}
52
53
/**
54
* Set whether this class should validate that the server has enough slots
55
* left before creating the new database.
56
*/
57
public function setValidateDatabaseLimit(bool $validate): self
58
{
59
$this->validateDatabaseLimit = $validate;
60
61
return $this;
62
}
63
64
/**
65
* Create a new database that is linked to a specific host.
66
*
67
* @throws \Throwable
68
* @throws TooManyDatabasesException
69
* @throws DatabaseClientFeatureNotEnabledException
70
*/
71
public function create(Server $server, array $data): Database
72
{
73
if (!config('pterodactyl.client_features.databases.enabled')) {
74
throw new DatabaseClientFeatureNotEnabledException();
75
}
76
77
if ($this->validateDatabaseLimit) {
78
// If the server has a limit assigned and we've already reached that limit, throw back
79
// an exception and kill the process.
80
if (!is_null($server->database_limit) && $server->databases()->count() >= $server->database_limit) {
81
throw new TooManyDatabasesException();
82
}
83
}
84
85
// Protect against developer mistakes...
86
if (empty($data['database']) || !preg_match(self::MATCH_NAME_REGEX, $data['database'])) {
87
throw new \InvalidArgumentException('The database name passed to DatabaseManagementService::handle MUST be prefixed with "s{server_id}_".');
88
}
89
90
$data = array_merge($data, [
91
'server_id' => $server->id,
92
'username' => sprintf('u%d_%s', $server->id, str_random(10)),
93
'password' => $this->encrypter->encrypt(
94
Utilities::randomStringWithSpecialCharacters(24)
95
),
96
]);
97
98
$database = null;
99
100
try {
101
return $this->connection->transaction(function () use ($data, &$database) {
102
$database = $this->createModel($data);
103
104
$this->dynamic->set('dynamic', $data['database_host_id']);
105
106
$this->repository->createDatabase($database->database);
107
$this->repository->createUser(
108
$database->username,
109
$database->remote,
110
$this->encrypter->decrypt($database->password),
111
$database->max_connections
112
);
113
$this->repository->assignUserToDatabase($database->database, $database->username, $database->remote);
114
$this->repository->flush();
115
116
return $database;
117
});
118
} catch (\Exception $exception) {
119
try {
120
// This is actually incorrect, it can be null in the case that the $database model
121
// itself isn't able to be created in Pterodactyl's database.
122
//
123
// @phpstan-ignore-next-line instanceof.alwaysFalse
124
if ($database instanceof Database) {
125
$this->repository->dropDatabase($database->database);
126
$this->repository->dropUser($database->username, $database->remote);
127
$this->repository->flush();
128
}
129
} catch (\Throwable $deletionException) { // @phpstan-ignore catch.neverThrown
130
// Do nothing here. We've already encountered an issue before this point so no
131
// reason to prioritize this error over the initial one.
132
}
133
134
throw $exception;
135
}
136
}
137
138
/**
139
* Delete a database from the given host server.
140
*
141
* @throws \Exception
142
*/
143
public function delete(Database $database): ?bool
144
{
145
$this->dynamic->set('dynamic', $database->database_host_id);
146
147
$this->repository->dropDatabase($database->database);
148
$this->repository->dropUser($database->username, $database->remote);
149
$this->repository->flush();
150
151
return $database->delete();
152
}
153
154
/**
155
* Create the database if there is not an identical match in the DB. While you can technically
156
* have the same name across multiple hosts, for the sake of keeping this logic easy to understand
157
* and avoiding user confusion we will ignore the specific host and just look across all hosts.
158
*
159
* @throws DuplicateDatabaseNameException
160
* @throws \Throwable
161
*/
162
protected function createModel(array $data): Database
163
{
164
$exists = Database::query()->where('server_id', $data['server_id'])
165
->where('database', $data['database'])
166
->exists();
167
168
if ($exists) {
169
throw new DuplicateDatabaseNameException('A database with that name already exists for this server.');
170
}
171
172
$database = (new Database())->forceFill($data);
173
$database->saveOrFail();
174
175
return $database;
176
}
177
}
178
179