mirror of
				https://github.com/pelican-dev/panel.git
				synced 2025-11-04 11:56:52 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			229 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			229 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?php
 | 
						|
 | 
						|
namespace Pterodactyl\Services\Databases;
 | 
						|
 | 
						|
use Exception;
 | 
						|
use InvalidArgumentException;
 | 
						|
use Pterodactyl\Models\Server;
 | 
						|
use Pterodactyl\Models\Database;
 | 
						|
use Pterodactyl\Helpers\Utilities;
 | 
						|
use Illuminate\Database\ConnectionInterface;
 | 
						|
use Symfony\Component\VarDumper\Cloner\Data;
 | 
						|
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;
 | 
						|
 | 
						|
class DatabaseManagementService
 | 
						|
{
 | 
						|
    /**
 | 
						|
     * 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]+_)(.*)$/';
 | 
						|
 | 
						|
    /**
 | 
						|
     * @var \Illuminate\Database\ConnectionInterface
 | 
						|
     */
 | 
						|
    private $connection;
 | 
						|
 | 
						|
    /**
 | 
						|
     * @var \Pterodactyl\Extensions\DynamicDatabaseConnection
 | 
						|
     */
 | 
						|
    private $dynamic;
 | 
						|
 | 
						|
    /**
 | 
						|
     * @var \Illuminate\Contracts\Encryption\Encrypter
 | 
						|
     */
 | 
						|
    private $encrypter;
 | 
						|
 | 
						|
    /**
 | 
						|
     * @var \Pterodactyl\Repositories\Eloquent\DatabaseRepository
 | 
						|
     */
 | 
						|
    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;
 | 
						|
 | 
						|
    /**
 | 
						|
     * CreationService constructor.
 | 
						|
     *
 | 
						|
     * @param \Illuminate\Database\ConnectionInterface $connection
 | 
						|
     * @param \Pterodactyl\Extensions\DynamicDatabaseConnection $dynamic
 | 
						|
     * @param \Pterodactyl\Repositories\Eloquent\DatabaseRepository $repository
 | 
						|
     * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
 | 
						|
     */
 | 
						|
    public function __construct(
 | 
						|
        ConnectionInterface $connection,
 | 
						|
        DynamicDatabaseConnection $dynamic,
 | 
						|
        DatabaseRepository $repository,
 | 
						|
        Encrypter $encrypter
 | 
						|
    ) {
 | 
						|
        $this->connection = $connection;
 | 
						|
        $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.
 | 
						|
     *
 | 
						|
     * @param string $name
 | 
						|
     * @param int $serverId
 | 
						|
     * @return string
 | 
						|
     */
 | 
						|
    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.
 | 
						|
     *
 | 
						|
     * @param bool $validate
 | 
						|
     * @return $this
 | 
						|
     */
 | 
						|
    public function setValidateDatabaseLimit(bool $validate): self
 | 
						|
    {
 | 
						|
        $this->validateDatabaseLimit = $validate;
 | 
						|
 | 
						|
        return $this;
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Create a new database that is linked to a specific host.
 | 
						|
     *
 | 
						|
     * @param \Pterodactyl\Models\Server $server
 | 
						|
     * @param array $data
 | 
						|
     * @return \Pterodactyl\Models\Database
 | 
						|
     *
 | 
						|
     * @throws \Throwable
 | 
						|
     * @throws \Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException
 | 
						|
     * @throws \Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException
 | 
						|
     */
 | 
						|
    public function create(Server $server, array $data)
 | 
						|
    {
 | 
						|
        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.
 | 
						|
            if (! is_null($server->database_limit) && $server->databases()->count() >= $server->database_limit) {
 | 
						|
                throw new TooManyDatabasesException;
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        // Protect against developer mistakes...
 | 
						|
        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;
 | 
						|
 | 
						|
        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(
 | 
						|
                    $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) {
 | 
						|
            try {
 | 
						|
                if ($database instanceof Database) {
 | 
						|
                    $this->repository->dropDatabase($database->database);
 | 
						|
                    $this->repository->dropUser($database->username, $database->remote);
 | 
						|
                    $this->repository->flush();
 | 
						|
                }
 | 
						|
            } 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.
 | 
						|
            }
 | 
						|
 | 
						|
            throw $exception;
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * Delete a database from the given host server.
 | 
						|
     *
 | 
						|
     * @param \Pterodactyl\Models\Database $database
 | 
						|
     * @return bool|null
 | 
						|
     *
 | 
						|
     * @throws \Exception
 | 
						|
     */
 | 
						|
    public function delete(Database $database)
 | 
						|
    {
 | 
						|
        $this->dynamic->set('dynamic', $database->database_host_id);
 | 
						|
 | 
						|
        $this->repository->dropDatabase($database->database);
 | 
						|
        $this->repository->dropUser($database->username, $database->remote);
 | 
						|
        $this->repository->flush();
 | 
						|
 | 
						|
        return $database->delete();
 | 
						|
    }
 | 
						|
 | 
						|
    /**
 | 
						|
     * 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.
 | 
						|
     *
 | 
						|
     * @param array $data
 | 
						|
     * @return \Pterodactyl\Models\Database
 | 
						|
     *
 | 
						|
     * @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) {
 | 
						|
            throw new DuplicateDatabaseNameException(
 | 
						|
                'A database with that name already exists for this server.'
 | 
						|
            );
 | 
						|
        }
 | 
						|
 | 
						|
        $database = (new Database)->forceFill($data);
 | 
						|
        $database->saveOrFail();
 | 
						|
 | 
						|
        return $database;
 | 
						|
    }
 | 
						|
}
 |