33namespace Pterodactyl \Services \Databases ;
44
55use Exception ;
6+ use InvalidArgumentException ;
67use Pterodactyl \Models \Server ;
78use Pterodactyl \Models \Database ;
89use Pterodactyl \Helpers \Utilities ;
910use Illuminate \Database \ConnectionInterface ;
11+ use Symfony \Component \VarDumper \Cloner \Data ;
1012use Illuminate \Contracts \Encryption \Encrypter ;
1113use Pterodactyl \Extensions \DynamicDatabaseConnection ;
12- use Pterodactyl \Contracts \Repository \DatabaseRepositoryInterface ;
14+ use Pterodactyl \Repositories \Eloquent \DatabaseRepository ;
15+ use Pterodactyl \Exceptions \Repository \DuplicateDatabaseNameException ;
1316use Pterodactyl \Exceptions \Service \Database \TooManyDatabasesException ;
1417use Pterodactyl \Exceptions \Service \Database \DatabaseClientFeatureNotEnabledException ;
1518
1619class DatabaseManagementService
1720{
21+ /**
22+ * The regex used to validate that the database name passed through to the function is
23+ * in the expected format.
24+ *
25+ * @see \Pterodactyl\Services\Databases\DatabaseManagementService::generateUniqueDatabaseName()
26+ */
27+ private const MATCH_NAME_REGEX = '/^(s[\d]+_)(.*)$/ ' ;
28+
1829 /**
1930 * @var \Illuminate\Database\ConnectionInterface
2031 */
@@ -31,7 +42,7 @@ class DatabaseManagementService
3142 private $ encrypter ;
3243
3344 /**
34- * @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface
45+ * @var \Pterodactyl\Repositories\Eloquent\DatabaseRepository
3546 */
3647 private $ repository ;
3748
@@ -50,13 +61,13 @@ class DatabaseManagementService
5061 *
5162 * @param \Illuminate\Database\ConnectionInterface $connection
5263 * @param \Pterodactyl\Extensions\DynamicDatabaseConnection $dynamic
53- * @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $repository
64+ * @param \Pterodactyl\Repositories\Eloquent\DatabaseRepository $repository
5465 * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
5566 */
5667 public function __construct (
5768 ConnectionInterface $ connection ,
5869 DynamicDatabaseConnection $ dynamic ,
59- DatabaseRepositoryInterface $ repository ,
70+ DatabaseRepository $ repository ,
6071 Encrypter $ encrypter
6172 ) {
6273 $ this ->connection = $ connection ;
@@ -65,6 +76,21 @@ public function __construct(
6576 $ this ->repository = $ repository ;
6677 }
6778
79+ /**
80+ * Generates a unique database name for the given server. This name should be passed through when
81+ * calling this handle function for this service, otherwise the database will be created with
82+ * whatever name is provided.
83+ *
84+ * @param string $name
85+ * @param int $serverId
86+ * @return string
87+ */
88+ public static function generateUniqueDatabaseName (string $ name , int $ serverId ): string
89+ {
90+ // Max of 48 characters, including the s123_ that we append to the front.
91+ return sprintf ('s%d_%s ' , $ serverId , substr ($ name , 0 , 48 - strlen ("s {$ serverId }_ " )));
92+ }
93+
6894 /**
6995 * Set wether or not this class should validate that the server has enough slots
7096 * left before creating the new database.
@@ -104,12 +130,15 @@ public function create(Server $server, array $data)
104130 }
105131 }
106132
107- // Max of 48 characters, including the s123_ that we append to the front.
108- $ truncatedName = substr ($ data ['database ' ], 0 , 48 - strlen ("s {$ server ->id }_ " ));
133+ // Protect against developer mistakes...
134+ if (empty ($ data ['database ' ]) || ! preg_match (self ::MATCH_NAME_REGEX , $ data ['database ' ])) {
135+ throw new InvalidArgumentException (
136+ 'The database name passed to DatabaseManagementService::handle MUST be prefixed with "s{server_id}_". '
137+ );
138+ }
109139
110140 $ data = array_merge ($ data , [
111141 'server_id ' => $ server ->id ,
112- 'database ' => $ truncatedName ,
113142 'username ' => sprintf ('u%d_%s ' , $ server ->id , str_random (10 )),
114143 'password ' => $ this ->encrypter ->encrypt (
115144 Utilities::randomStringWithSpecialCharacters (24 )
@@ -120,7 +149,8 @@ public function create(Server $server, array $data)
120149
121150 try {
122151 return $ this ->connection ->transaction (function () use ($ data , &$ database ) {
123- $ database = $ this ->repository ->createIfNotExists ($ data );
152+ $ database = $ this ->createModel ($ data );
153+
124154 $ this ->dynamic ->set ('dynamic ' , $ data ['database_host_id ' ]);
125155
126156 $ this ->repository ->createDatabase ($ database ->database );
@@ -139,7 +169,7 @@ public function create(Server $server, array $data)
139169 $ this ->repository ->dropUser ($ database ->username , $ database ->remote );
140170 $ this ->repository ->flush ();
141171 }
142- } catch (Exception $ exception ) {
172+ } catch (Exception $ deletionException ) {
143173 // Do nothing here. We've already encountered an issue before this point so no
144174 // reason to prioritize this error over the initial one.
145175 }
@@ -166,4 +196,33 @@ public function delete(Database $database)
166196
167197 return $ database ->delete ();
168198 }
199+
200+ /**
201+ * Create the database if there is not an identical match in the DB. While you can technically
202+ * have the same name across multiple hosts, for the sake of keeping this logic easy to understand
203+ * and avoiding user confusion we will ignore the specific host and just look across all hosts.
204+ *
205+ * @param array $data
206+ * @return \Pterodactyl\Models\Database
207+ *
208+ * @throws \Pterodactyl\Exceptions\Repository\DuplicateDatabaseNameException
209+ * @throws \Throwable
210+ */
211+ protected function createModel (array $ data ): Database
212+ {
213+ $ exists = Database::query ()->where ('server_id ' , $ data ['server_id ' ])
214+ ->where ('database ' , $ data ['database ' ])
215+ ->exists ();
216+
217+ if ($ exists ) {
218+ throw new DuplicateDatabaseNameException (
219+ 'A database with that name already exists for this server. '
220+ );
221+ }
222+
223+ $ database = (new Database )->forceFill ($ data );
224+ $ database ->saveOrFail ();
225+
226+ return $ database ;
227+ }
169228}
0 commit comments