diff --git a/.editorconfig b/.editorconfig index bc49d523e..1ba6cf9f7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,5 +8,8 @@ indent_size = 4 charset = utf-8 trim_trailing_whitespace = true +[.*yml] +indent_size = 2 + [*.md] trim_trailing_whitespace = false diff --git a/.env.travis b/.env.ci similarity index 77% rename from .env.travis rename to .env.ci index e0040b948..e99f46691 100644 --- a/.env.travis +++ b/.env.ci @@ -2,13 +2,13 @@ APP_ENV=testing APP_DEBUG=true APP_KEY=SomeRandomString3232RandomString APP_THEME=pterodactyl -APP_TIMEZONE=UTC +APP_TIMEZONE=America/Los_Angeles APP_URL=http://localhost/ TESTING_DB_HOST=127.0.0.1 -TESTING_DB_DATABASE=travis +TESTING_DB_DATABASE=panel_test TESTING_DB_USERNAME=root -TESTING_DB_PASSWORD="" +TESTING_DB_PASSWORD= CACHE_DRIVER=array SESSION_DRIVER=array diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..785de6c73 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +#github: [DaneEveritt] +custom: ["https://paypal.me/PterodactylSoftware"] diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 000000000..71328ff02 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,64 @@ +name: tests +on: + push: + pull_request: +jobs: + integration_tests: + if: "!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, '[ci skip]')" + runs-on: ubuntu-latest + services: + mysql: + image: mysql:5.7 + env: + MYSQL_ALLOW_EMPTY_PASSWORD: yes + MYSQL_DATABASE: panel_test + ports: + - 3306 + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 + strategy: + fail-fast: true + matrix: + php: [7.3, 7.4] + name: PHP ${{ matrix.php }} + steps: + - name: checkout + uses: actions/checkout@v2 + - name: get cache directory + id: composer-cache + run: | + echo "::set-output name=dir::$(composer config cache-files-dir)" + - name: cache dependencies + uses: actions/cache@v2 + with: + path: | + ~/.php_cs.cache + ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-cache-${{ matrix.php }}-${{ hashFiles('**.composer.lock') }} + restore-keys: | + ${{ runner.os }}-cache-${{ matrix.php }}- + - name: setup + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: cli, openssl, gd, mysql, pdo, mbstring, tokenizer, bcmath, xml, curl, zip + tools: composer:v1 + coverage: none + - name: configure + run: cp .env.ci .env + - name: install dependencies + run: composer install --prefer-dist --no-interaction --no-progress + - name: run cs-fixer + run: vendor/bin/php-cs-fixer fix --dry-run --diff --diff-format=udiff + continue-on-error: true + - name: execute unit tests + run: vendor/bin/phpunit --bootstrap bootstrap/app.php tests/Unit + if: ${{ always() }} + env: + DB_CONNECTION: testing + TESTING_DB_HOST: UNIT_NO_DB + - name: execute integration tests + run: vendor/bin/phpunit tests/Integration + if: ${{ always() }} + env: + TESTING_DB_PORT: ${{ job.services.mysql.ports[3306] }} + TESTING_DB_USERNAME: root diff --git a/.gitignore b/.gitignore index 3352c6ad9..58250120e 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ coverage.xml resources/lang/locales.js resources/assets/pterodactyl/scripts/helpers/ziggy.js resources/assets/scripts/helpers/ziggy.js +.phpunit.result.cache diff --git a/app/Console/Commands/Migration/CleanOrphanedApiKeysCommand.php b/app/Console/Commands/Migration/CleanOrphanedApiKeysCommand.php index b9e007ee7..e7d863992 100644 --- a/app/Console/Commands/Migration/CleanOrphanedApiKeysCommand.php +++ b/app/Console/Commands/Migration/CleanOrphanedApiKeysCommand.php @@ -38,7 +38,7 @@ class CleanOrphanedApiKeysCommand extends Command /** * Delete all orphaned API keys from the database when upgrading from 0.6 to 0.7. * - * @return null|void + * @return void|null */ public function handle() { diff --git a/app/Contracts/Repository/SessionRepositoryInterface.php b/app/Contracts/Repository/SessionRepositoryInterface.php index 496fa35e7..e1dafed1e 100644 --- a/app/Contracts/Repository/SessionRepositoryInterface.php +++ b/app/Contracts/Repository/SessionRepositoryInterface.php @@ -19,7 +19,7 @@ interface SessionRepositoryInterface extends RepositoryInterface * * @param int $user * @param string $session - * @return null|int + * @return int|null */ public function deleteUserSession(int $user, string $session); } diff --git a/app/Contracts/Repository/TaskRepositoryInterface.php b/app/Contracts/Repository/TaskRepositoryInterface.php index 11cb704e1..ea9486254 100644 --- a/app/Contracts/Repository/TaskRepositoryInterface.php +++ b/app/Contracts/Repository/TaskRepositoryInterface.php @@ -21,7 +21,7 @@ interface TaskRepositoryInterface extends RepositoryInterface * * @param int $schedule * @param int $index - * @return null|\Pterodactyl\Models\Task + * @return \Pterodactyl\Models\Task|null */ public function getNextTask(int $schedule, int $index); } diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 39709d5e2..50ac1a960 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -3,6 +3,7 @@ namespace Pterodactyl\Exceptions; use Exception; +use Throwable; use PDOException; use Psr\Log\LoggerInterface; use Swift_TransportException; @@ -72,12 +73,12 @@ class Handler extends ExceptionHandler * services such as AWS Cloudwatch or other monitoring you can replace the * contents of this function with a call to the parent reporter. * - * @param \Exception $exception + * @param \Throwable $exception * @return mixed * - * @throws \Exception + * @throws \Throwable */ - public function report(Exception $exception) + public function report(Throwable $exception) { if (! config('app.exceptions.report_all', false) && $this->shouldntReport($exception)) { return null; @@ -103,7 +104,7 @@ class Handler extends ExceptionHandler return $logger->error($exception); } - private function generateCleanedExceptionStack(Exception $exception) + private function generateCleanedExceptionStack(Throwable $exception) { $cleanedStack = ''; foreach ($exception->getTrace() as $index => $item) { @@ -133,12 +134,12 @@ class Handler extends ExceptionHandler * Render an exception into an HTTP response. * * @param \Illuminate\Http\Request $request - * @param \Exception $exception + * @param \Throwable $exception * @return \Symfony\Component\HttpFoundation\Response * - * @throws \Exception + * @throws \Throwable */ - public function render($request, Exception $exception) + public function render($request, Throwable $exception) { $connections = Container::getInstance()->make(Connection::class); @@ -200,11 +201,11 @@ class Handler extends ExceptionHandler /** * Return the exception as a JSONAPI representation for use on API requests. * - * @param \Exception $exception + * @param \Throwable $exception * @param array $override * @return array */ - public static function convertToArray(Exception $exception, array $override = []): array + public static function convertToArray(Throwable $exception, array $override = []): array { $error = [ 'code' => class_basename($exception), @@ -259,10 +260,10 @@ class Handler extends ExceptionHandler * Converts an exception into an array to render in the response. Overrides * Laravel's built-in converter to output as a JSONAPI spec compliant object. * - * @param \Exception $exception + * @param \Throwable $exception * @return array */ - protected function convertExceptionToArray(Exception $exception) + protected function convertExceptionToArray(Throwable $exception) { return self::convertToArray($exception); } diff --git a/app/Exceptions/Service/ServiceLimitExceededException.php b/app/Exceptions/Service/ServiceLimitExceededException.php new file mode 100644 index 000000000..55ee6c94b --- /dev/null +++ b/app/Exceptions/Service/ServiceLimitExceededException.php @@ -0,0 +1,21 @@ +buildModificationService->handle($server, $request->only([ - 'allocation_id', 'add_allocations', 'remove_allocations', - 'memory', 'swap', 'io', 'cpu', 'threads', 'disk', - 'database_limit', 'allocation_limit', 'backup_limit', 'oom_disabled', - ])); + try { + $this->buildModificationService->handle($server, $request->only([ + 'allocation_id', 'add_allocations', 'remove_allocations', + 'memory', 'swap', 'io', 'cpu', 'threads', 'disk', + 'database_limit', 'allocation_limit', 'backup_limit', 'oom_disabled', + ])); + } catch (DataValidationException $exception) { + throw new ValidationException($exception->validator); + } + $this->alert->success(trans('admin/server.alerts.build_updated'))->flash(); return redirect()->route('admin.servers.view.build', $server->id); @@ -341,12 +348,12 @@ class ServersController extends Controller * Creates a new database assigned to a specific server. * * @param \Pterodactyl\Http\Requests\Admin\Servers\Databases\StoreServerDatabaseRequest $request - * @param int $server + * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\RedirectResponse * - * @throws \Exception + * @throws \Throwable */ - public function newDatabase(StoreServerDatabaseRequest $request, $server) + public function newDatabase(StoreServerDatabaseRequest $request, Server $server) { $this->databaseManagementService->create($server, [ 'database' => $request->input('database'), @@ -355,7 +362,7 @@ class ServersController extends Controller 'max_connections' => $request->input('max_connections'), ]); - return redirect()->route('admin.servers.view.database', $server)->withInput(); + return redirect()->route('admin.servers.view.database', $server->id)->withInput(); } /** diff --git a/app/Http/Controllers/Api/Application/Servers/DatabaseController.php b/app/Http/Controllers/Api/Application/Servers/DatabaseController.php index f556aff57..24c8906aa 100644 --- a/app/Http/Controllers/Api/Application/Servers/DatabaseController.php +++ b/app/Http/Controllers/Api/Application/Servers/DatabaseController.php @@ -57,13 +57,12 @@ class DatabaseController extends ApplicationApiController * server. * * @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\GetServerDatabasesRequest $request + * @param \Pterodactyl\Models\Server $server * @return array */ - public function index(GetServerDatabasesRequest $request): array + public function index(GetServerDatabasesRequest $request, Server $server): array { - $databases = $this->repository->getDatabasesForServer($request->getModel(Server::class)->id); - - return $this->fractal->collection($databases) + return $this->fractal->collection($server->databases) ->transformWith($this->getTransformer(ServerDatabaseTransformer::class)) ->toArray(); } @@ -72,11 +71,13 @@ class DatabaseController extends ApplicationApiController * Return a single server database. * * @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\GetServerDatabaseRequest $request + * @param \Pterodactyl\Models\Server $server + * @param \Pterodactyl\Models\Database $database * @return array */ - public function view(GetServerDatabaseRequest $request): array + public function view(GetServerDatabaseRequest $request, Server $server, Database $database): array { - return $this->fractal->item($request->getModel(Database::class)) + return $this->fractal->item($database) ->transformWith($this->getTransformer(ServerDatabaseTransformer::class)) ->toArray(); } @@ -85,29 +86,31 @@ class DatabaseController extends ApplicationApiController * Reset the password for a specific server database. * * @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\ServerDatabaseWriteRequest $request - * @return \Illuminate\Http\Response + * @param \Pterodactyl\Models\Server $server + * @param \Pterodactyl\Models\Database $database + * @return \Illuminate\Http\JsonResponse * * @throws \Throwable */ - public function resetPassword(ServerDatabaseWriteRequest $request): Response + public function resetPassword(ServerDatabaseWriteRequest $request, Server $server, Database $database): JsonResponse { - $this->databasePasswordService->handle($request->getModel(Database::class)); + $this->databasePasswordService->handle($database); - return response('', 204); + return JsonResponse::create([], JsonResponse::HTTP_NO_CONTENT); } /** * Create a new database on the Panel for a given server. * * @param \Pterodactyl\Http\Requests\Api\Application\Servers\Databases\StoreServerDatabaseRequest $request + * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\JsonResponse * - * @throws \Exception + * @throws \Throwable */ - public function store(StoreServerDatabaseRequest $request): JsonResponse + public function store(StoreServerDatabaseRequest $request, Server $server): JsonResponse { - $server = $request->getModel(Server::class); - $database = $this->databaseManagementService->create($server->id, $request->validated()); + $database = $this->databaseManagementService->create($server, $request->validated()); return $this->fractal->item($database) ->transformWith($this->getTransformer(ServerDatabaseTransformer::class)) @@ -117,7 +120,7 @@ class DatabaseController extends ApplicationApiController 'database' => $database->id, ]), ]) - ->respond(201); + ->respond(Response::HTTP_CREATED); } /** diff --git a/app/Http/Controllers/Api/Application/Users/UserController.php b/app/Http/Controllers/Api/Application/Users/UserController.php index d067bab10..522cef8f4 100644 --- a/app/Http/Controllers/Api/Application/Users/UserController.php +++ b/app/Http/Controllers/Api/Application/Users/UserController.php @@ -100,39 +100,20 @@ class UserController extends ApplicationApiController * meta. If there are no errors this is an empty array. * * @param \Pterodactyl\Http\Requests\Api\Application\Users\UpdateUserRequest $request + * @param \Pterodactyl\Models\User $user * @return array * * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function update(UpdateUserRequest $request): array + public function update(UpdateUserRequest $request, User $user): array { $this->updateService->setUserLevel(User::USER_LEVEL_ADMIN); - $collection = $this->updateService->handle($request->getModel(User::class), $request->validated()); + $user = $this->updateService->handle($user, $request->validated()); - $errors = []; - if (! empty($collection->get('exceptions'))) { - foreach ($collection->get('exceptions') as $node => $exception) { - /** @var \GuzzleHttp\Exception\RequestException $exception */ - /** @var \GuzzleHttp\Psr7\Response|null $response */ - $response = method_exists($exception, 'getResponse') ? $exception->getResponse() : null; - $message = trans('admin/server.exceptions.daemon_exception', [ - 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), - ]); - - $errors[] = ['message' => $message, 'node' => $node]; - } - } - - $response = $this->fractal->item($collection->get('model')) + $response = $this->fractal->item($user) ->transformWith($this->getTransformer(UserTransformer::class)); - if (count($errors) > 0) { - $response->addMeta([ - 'revocation_errors' => $errors, - ]); - } - return $response->toArray(); } diff --git a/app/Http/Controllers/Api/Client/AccountController.php b/app/Http/Controllers/Api/Client/AccountController.php index 5d633c480..25900f059 100644 --- a/app/Http/Controllers/Api/Client/AccountController.php +++ b/app/Http/Controllers/Api/Client/AccountController.php @@ -52,16 +52,16 @@ class AccountController extends ClientApiController * Update the authenticated user's email address. * * @param \Pterodactyl\Http\Requests\Api\Client\Account\UpdateEmailRequest $request - * @return \Illuminate\Http\Response + * @return \Illuminate\Http\JsonResponse * * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function updateEmail(UpdateEmailRequest $request): Response + public function updateEmail(UpdateEmailRequest $request): JsonResponse { $this->updateService->handle($request->user(), $request->validated()); - return response('', Response::HTTP_CREATED); + return new JsonResponse([], Response::HTTP_NO_CONTENT); } /** @@ -74,12 +74,12 @@ class AccountController extends ClientApiController * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function updatePassword(UpdatePasswordRequest $request): \Illuminate\Http\JsonResponse + public function updatePassword(UpdatePasswordRequest $request): JsonResponse { $this->updateService->handle($request->user(), $request->validated()); $this->sessionGuard->logoutOtherDevices($request->input('password')); - return JsonResponse::create([], Response::HTTP_NO_CONTENT); + return new JsonResponse([], Response::HTTP_NO_CONTENT); } } diff --git a/app/Http/Controllers/Api/Client/ApiKeyController.php b/app/Http/Controllers/Api/Client/ApiKeyController.php index 0788e62e2..5662e19cd 100644 --- a/app/Http/Controllers/Api/Client/ApiKeyController.php +++ b/app/Http/Controllers/Api/Client/ApiKeyController.php @@ -103,6 +103,7 @@ class ApiKeyController extends ClientApiController public function delete(ClientApiRequest $request, string $identifier) { $response = $this->repository->deleteWhere([ + 'key_type' => ApiKey::TYPE_ACCOUNT, 'user_id' => $request->user()->id, 'identifier' => $identifier, ]); diff --git a/app/Http/Controllers/Api/Client/Servers/DatabaseController.php b/app/Http/Controllers/Api/Client/Servers/DatabaseController.php index 0c305c2e6..5a3e1e3ac 100644 --- a/app/Http/Controllers/Api/Client/Servers/DatabaseController.php +++ b/app/Http/Controllers/Api/Client/Servers/DatabaseController.php @@ -69,9 +69,7 @@ class DatabaseController extends ClientApiController */ public function index(GetDatabasesRequest $request, Server $server): array { - $databases = $this->repository->getDatabasesForServer($server->id); - - return $this->fractal->collection($databases) + return $this->fractal->collection($server->databases) ->transformWith($this->getTransformer(DatabaseTransformer::class)) ->toArray(); } @@ -83,6 +81,8 @@ class DatabaseController extends ClientApiController * @param \Pterodactyl\Models\Server $server * @return array * + * @throws \Throwable + * @throws \Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException * @throws \Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException */ public function store(StoreDatabaseRequest $request, Server $server): array diff --git a/app/Http/Controllers/Api/Client/Servers/ScheduleController.php b/app/Http/Controllers/Api/Client/Servers/ScheduleController.php index d1a6b9f4f..593bbc259 100644 --- a/app/Http/Controllers/Api/Client/Servers/ScheduleController.php +++ b/app/Http/Controllers/Api/Client/Servers/ScheduleController.php @@ -147,7 +147,7 @@ class ScheduleController extends ClientApiController { $this->repository->delete($schedule->id); - return JsonResponse::create([], Response::HTTP_NO_CONTENT); + return new JsonResponse([], Response::HTTP_NO_CONTENT); } /** diff --git a/app/Http/Controllers/Api/Client/Servers/ScheduleTaskController.php b/app/Http/Controllers/Api/Client/Servers/ScheduleTaskController.php index 7461a950a..0d613b6b0 100644 --- a/app/Http/Controllers/Api/Client/Servers/ScheduleTaskController.php +++ b/app/Http/Controllers/Api/Client/Servers/ScheduleTaskController.php @@ -13,6 +13,7 @@ use Pterodactyl\Exceptions\Http\HttpForbiddenException; use Pterodactyl\Transformers\Api\Client\TaskTransformer; use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest; use Pterodactyl\Http\Controllers\Api\Client\ClientApiController; +use Pterodactyl\Exceptions\Service\ServiceLimitExceededException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\StoreTaskRequest; @@ -44,11 +45,15 @@ class ScheduleTaskController extends ClientApiController * @return array * * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Service\ServiceLimitExceededException */ public function store(StoreTaskRequest $request, Server $server, Schedule $schedule) { - if ($schedule->server_id !== $server->id) { - throw new NotFoundHttpException; + $limit = config('pterodactyl.client_features.schedules.per_schedule_task_limit', 10); + if ($schedule->tasks()->count() >= $limit) { + throw new ServiceLimitExceededException( + "Schedules may not have more than {$limit} tasks associated with them. Creating this task would put this schedule over the limit." + ); } $lastTask = $schedule->tasks->last(); @@ -58,7 +63,7 @@ class ScheduleTaskController extends ClientApiController 'schedule_id' => $schedule->id, 'sequence_id' => ($lastTask->sequence_id ?? 0) + 1, 'action' => $request->input('action'), - 'payload' => $request->input('payload'), + 'payload' => $request->input('payload') ?? '', 'time_offset' => $request->input('time_offset'), ]); @@ -87,7 +92,7 @@ class ScheduleTaskController extends ClientApiController $this->repository->update($task->id, [ 'action' => $request->input('action'), - 'payload' => $request->input('payload'), + 'payload' => $request->input('payload') ?? '', 'time_offset' => $request->input('time_offset'), ]); diff --git a/app/Http/Controllers/Api/Client/Servers/SettingsController.php b/app/Http/Controllers/Api/Client/Servers/SettingsController.php index 64c73a8de..6090906ee 100644 --- a/app/Http/Controllers/Api/Client/Servers/SettingsController.php +++ b/app/Http/Controllers/Api/Client/Servers/SettingsController.php @@ -55,7 +55,7 @@ class SettingsController extends ClientApiController 'name' => $request->input('name'), ]); - return JsonResponse::create([], Response::HTTP_NO_CONTENT); + return new JsonResponse([], Response::HTTP_NO_CONTENT); } /** @@ -71,6 +71,6 @@ class SettingsController extends ClientApiController { $this->reinstallServerService->reinstall($server); - return JsonResponse::create([], Response::HTTP_ACCEPTED); + return new JsonResponse([], Response::HTTP_ACCEPTED); } } diff --git a/app/Http/Controllers/Api/Client/Servers/WebsocketController.php b/app/Http/Controllers/Api/Client/Servers/WebsocketController.php index 7cbe31631..a176f66fc 100644 --- a/app/Http/Controllers/Api/Client/Servers/WebsocketController.php +++ b/app/Http/Controllers/Api/Client/Servers/WebsocketController.php @@ -7,7 +7,6 @@ use Illuminate\Http\Response; use Pterodactyl\Models\Server; use Illuminate\Http\JsonResponse; use Pterodactyl\Models\Permission; -use Illuminate\Contracts\Cache\Repository; use Pterodactyl\Services\Nodes\NodeJWTService; use Symfony\Component\HttpKernel\Exception\HttpException; use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest; @@ -16,11 +15,6 @@ use Pterodactyl\Http\Controllers\Api\Client\ClientApiController; class WebsocketController extends ClientApiController { - /** - * @var \Illuminate\Contracts\Cache\Repository - */ - private $cache; - /** * @var \Pterodactyl\Services\Nodes\NodeJWTService */ @@ -36,16 +30,13 @@ class WebsocketController extends ClientApiController * * @param \Pterodactyl\Services\Nodes\NodeJWTService $jwtService * @param \Pterodactyl\Services\Servers\GetUserPermissionsService $permissionsService - * @param \Illuminate\Contracts\Cache\Repository $cache */ public function __construct( NodeJWTService $jwtService, - GetUserPermissionsService $permissionsService, - Repository $cache + GetUserPermissionsService $permissionsService ) { parent::__construct(); - $this->cache = $cache; $this->jwtService = $jwtService; $this->permissionsService = $permissionsService; } @@ -78,7 +69,7 @@ class WebsocketController extends ClientApiController $socket = str_replace(['https://', 'http://'], ['wss://', 'ws://'], $server->node->getConnectionAddress()); - return JsonResponse::create([ + return new JsonResponse([ 'data' => [ 'token' => $token->__toString(), 'socket' => $socket . sprintf('/api/servers/%s/ws', $server->uuid), diff --git a/app/Http/Controllers/Api/Client/TwoFactorController.php b/app/Http/Controllers/Api/Client/TwoFactorController.php index 0dae225e5..93be78fbc 100644 --- a/app/Http/Controllers/Api/Client/TwoFactorController.php +++ b/app/Http/Controllers/Api/Client/TwoFactorController.php @@ -61,11 +61,11 @@ class TwoFactorController extends ClientApiController */ public function index(Request $request) { - if ($request->user()->totp_enabled) { + if ($request->user()->use_totp) { throw new BadRequestHttpException('Two-factor authentication is already enabled on this account.'); } - return JsonResponse::create([ + return new JsonResponse([ 'data' => [ 'image_url_data' => $this->setupService->handle($request->user()), ], @@ -96,9 +96,14 @@ class TwoFactorController extends ClientApiController throw new ValidationException($validator); } - $this->toggleTwoFactorService->handle($request->user(), $request->input('code'), true); + $tokens = $this->toggleTwoFactorService->handle($request->user(), $request->input('code'), true); - return JsonResponse::create([], Response::HTTP_NO_CONTENT); + return new JsonResponse([ + 'object' => 'recovery_tokens', + 'attributes' => [ + 'tokens' => $tokens, + ], + ]); } /** @@ -124,6 +129,6 @@ class TwoFactorController extends ClientApiController 'use_totp' => false, ]); - return JsonResponse::create([], Response::HTTP_NO_CONTENT); + return new JsonResponse([], Response::HTTP_NO_CONTENT); } } diff --git a/app/Http/Controllers/Auth/AbstractLoginController.php b/app/Http/Controllers/Auth/AbstractLoginController.php index 0810d7e93..b24a1a62a 100644 --- a/app/Http/Controllers/Auth/AbstractLoginController.php +++ b/app/Http/Controllers/Auth/AbstractLoginController.php @@ -68,10 +68,11 @@ abstract class AbstractLoginController extends Controller * * @param \Illuminate\Http\Request $request * @param \Illuminate\Contracts\Auth\Authenticatable|null $user + * @param string|null $message * * @throws \Pterodactyl\Exceptions\DisplayException */ - protected function sendFailedLoginResponse(Request $request, Authenticatable $user = null) + protected function sendFailedLoginResponse(Request $request, Authenticatable $user = null, string $message = null) { $this->incrementLoginAttempts($request); $this->fireFailedLoginEvent($user, [ @@ -79,7 +80,9 @@ abstract class AbstractLoginController extends Controller ]); if ($request->route()->named('auth.login-checkpoint')) { - throw new DisplayException(trans('auth.two_factor.checkpoint_failed')); + throw new DisplayException( + $message ?? trans('auth.two_factor.checkpoint_failed') + ); } throw new DisplayException(trans('auth.failed')); @@ -116,7 +119,7 @@ abstract class AbstractLoginController extends Controller */ protected function getField(string $input = null): string { - return str_contains($input, '@') ? 'email' : 'username'; + return ($input && str_contains($input, '@')) ? 'email' : 'username'; } /** diff --git a/app/Http/Controllers/Auth/LoginCheckpointController.php b/app/Http/Controllers/Auth/LoginCheckpointController.php index 1cc2fe1af..c44f18a81 100644 --- a/app/Http/Controllers/Auth/LoginCheckpointController.php +++ b/app/Http/Controllers/Auth/LoginCheckpointController.php @@ -11,6 +11,7 @@ use Pterodactyl\Http\Requests\Auth\LoginCheckpointRequest; use Illuminate\Contracts\Cache\Repository as CacheRepository; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; +use Pterodactyl\Repositories\Eloquent\RecoveryTokenRepository; class LoginCheckpointController extends AbstractLoginController { @@ -34,6 +35,11 @@ class LoginCheckpointController extends AbstractLoginController */ private $encrypter; + /** + * @var \Pterodactyl\Repositories\Eloquent\RecoveryTokenRepository + */ + private $recoveryTokenRepository; + /** * LoginCheckpointController constructor. * @@ -42,6 +48,7 @@ class LoginCheckpointController extends AbstractLoginController * @param \PragmaRX\Google2FA\Google2FA $google2FA * @param \Illuminate\Contracts\Config\Repository $config * @param \Illuminate\Contracts\Cache\Repository $cache + * @param \Pterodactyl\Repositories\Eloquent\RecoveryTokenRepository $recoveryTokenRepository * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository */ public function __construct( @@ -50,6 +57,7 @@ class LoginCheckpointController extends AbstractLoginController Google2FA $google2FA, Repository $config, CacheRepository $cache, + RecoveryTokenRepository $recoveryTokenRepository, UserRepositoryInterface $repository ) { parent::__construct($auth, $config); @@ -58,6 +66,7 @@ class LoginCheckpointController extends AbstractLoginController $this->cache = $cache; $this->repository = $repository; $this->encrypter = $encrypter; + $this->recoveryTokenRepository = $recoveryTokenRepository; } /** @@ -76,21 +85,35 @@ class LoginCheckpointController extends AbstractLoginController public function __invoke(LoginCheckpointRequest $request): JsonResponse { $token = $request->input('confirmation_token'); + $recoveryToken = $request->input('recovery_token'); try { + /** @var \Pterodactyl\Models\User $user */ $user = $this->repository->find($this->cache->get($token, 0)); } catch (RecordNotFoundException $exception) { - return $this->sendFailedLoginResponse($request); + return $this->sendFailedLoginResponse($request, null, 'The authentication token provided has expired, please refresh the page and try again.'); } - $decrypted = $this->encrypter->decrypt($user->totp_secret); + // If we got a recovery token try to find one that matches for the user and then continue + // through the process (and delete the token). + if (! is_null($recoveryToken)) { + foreach ($user->recoveryTokens as $token) { + if (password_verify($recoveryToken, $token->token)) { + $this->recoveryTokenRepository->delete($token->id); - if ($this->google2FA->verifyKey($decrypted, (string) $request->input('authentication_code') ?? '', config('pterodactyl.auth.2fa.window'))) { - $this->cache->delete($token); + return $this->sendLoginResponse($user, $request); + } + } + } else { + $decrypted = $this->encrypter->decrypt($user->totp_secret); - return $this->sendLoginResponse($user, $request); + if ($this->google2FA->verifyKey($decrypted, (string) $request->input('authentication_code') ?? '', config('pterodactyl.auth.2fa.window'))) { + $this->cache->delete($token); + + return $this->sendLoginResponse($user, $request); + } } - return $this->sendFailedLoginResponse($request, $user); + return $this->sendFailedLoginResponse($request, $user, ! empty($recoveryToken) ? 'The recovery token provided is not valid.' : null); } } diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 0d7f21978..593189db1 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -103,7 +103,7 @@ class LoginController extends AbstractLoginController $token = Str::random(64); $this->cache->put($token, $user->id, Chronos::now()->addMinutes(5)); - return JsonResponse::create([ + return new JsonResponse([ 'data' => [ 'complete' => false, 'confirmation_token' => $token, diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index c6d537a26..fa4f8a38c 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -9,6 +9,7 @@ use Pterodactyl\Http\Middleware\TrimStrings; use Pterodactyl\Http\Middleware\TrustProxies; use Illuminate\Session\Middleware\StartSession; use Pterodactyl\Http\Middleware\EncryptCookies; +use Pterodactyl\Http\Middleware\Api\IsValidJson; use Pterodactyl\Http\Middleware\VerifyCsrfToken; use Pterodactyl\Http\Middleware\VerifyReCaptcha; use Pterodactyl\Http\Middleware\AdminAuthenticate; @@ -28,12 +29,8 @@ use Pterodactyl\Http\Middleware\Api\ApiSubstituteBindings; use Illuminate\Foundation\Http\Middleware\ValidatePostSize; use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; use Pterodactyl\Http\Middleware\Server\AccessingValidServer; -use Pterodactyl\Http\Middleware\Server\AuthenticateAsSubuser; use Pterodactyl\Http\Middleware\Api\Daemon\DaemonAuthenticate; -use Pterodactyl\Http\Middleware\Server\SubuserBelongsToServer; use Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication; -use Pterodactyl\Http\Middleware\Server\DatabaseBelongsToServer; -use Pterodactyl\Http\Middleware\Server\ScheduleBelongsToServer; use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode; use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull; use Pterodactyl\Http\Middleware\Api\Client\SubstituteClientApiBindings; @@ -72,7 +69,7 @@ class Kernel extends HttpKernel RequireTwoFactorAuthentication::class, ], 'api' => [ - 'throttle:240,1', + IsValidJson::class, ApiSubstituteBindings::class, SetSessionDriver::class, 'api..key:' . ApiKey::TYPE_APPLICATION, @@ -80,10 +77,10 @@ class Kernel extends HttpKernel AuthenticateIPAccess::class, ], 'client-api' => [ - 'throttle:240,1', StartSession::class, SetSessionDriver::class, AuthenticateSession::class, + IsValidJson::class, SubstituteClientApiBindings::class, 'api..key:' . ApiKey::TYPE_ACCOUNT, AuthenticateIPAccess::class, @@ -104,7 +101,6 @@ class Kernel extends HttpKernel 'auth.basic' => AuthenticateWithBasicAuth::class, 'guest' => RedirectIfAuthenticated::class, 'server' => AccessingValidServer::class, - 'subuser.auth' => AuthenticateAsSubuser::class, 'admin' => AdminAuthenticate::class, 'csrf' => VerifyCsrfToken::class, 'throttle' => ThrottleRequests::class, @@ -113,14 +109,6 @@ class Kernel extends HttpKernel 'recaptcha' => VerifyReCaptcha::class, 'node.maintenance' => MaintenanceMiddleware::class, - // Server specific middleware (used for authenticating access to resources) - // - // These are only used for individual server authentication, and not global - // actions from other resources. They are defined in the route files. - 'server..database' => DatabaseBelongsToServer::class, - 'server..subuser' => SubuserBelongsToServer::class, - 'server..schedule' => ScheduleBelongsToServer::class, - // API Specific Middleware 'api..key' => AuthenticateKey::class, ]; diff --git a/app/Http/Middleware/Api/Client/Server/AuthenticateServerAccess.php b/app/Http/Middleware/Api/Client/Server/AuthenticateServerAccess.php index c6acb66d3..1525755cf 100644 --- a/app/Http/Middleware/Api/Client/Server/AuthenticateServerAccess.php +++ b/app/Http/Middleware/Api/Client/Server/AuthenticateServerAccess.php @@ -65,7 +65,7 @@ class AuthenticateServerAccess } if ($server->suspended) { - throw new AccessDeniedHttpException('Cannot access a server that is marked as being suspended.'); + throw new AccessDeniedHttpException('This server is currenty suspended and the functionality requested is unavailable.'); } if (! $server->isInstalled()) { diff --git a/app/Http/Middleware/Api/Daemon/DaemonAuthenticate.php b/app/Http/Middleware/Api/Daemon/DaemonAuthenticate.php index a2b1e716e..bc365e63c 100644 --- a/app/Http/Middleware/Api/Daemon/DaemonAuthenticate.php +++ b/app/Http/Middleware/Api/Daemon/DaemonAuthenticate.php @@ -5,8 +5,8 @@ namespace Pterodactyl\Http\Middleware\Api\Daemon; use Closure; use Illuminate\Http\Request; use Illuminate\Contracts\Encryption\Encrypter; +use Pterodactyl\Repositories\Eloquent\NodeRepository; use Symfony\Component\HttpKernel\Exception\HttpException; -use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; @@ -14,10 +14,15 @@ use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; class DaemonAuthenticate { /** - * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface + * @var \Pterodactyl\Repositories\Eloquent\NodeRepository */ private $repository; + /** + * @var \Illuminate\Contracts\Encryption\Encrypter + */ + private $encrypter; + /** * Daemon routes that this middleware should be skipped on. * @@ -27,18 +32,13 @@ class DaemonAuthenticate 'daemon.configuration', ]; - /** - * @var \Illuminate\Contracts\Encryption\Encrypter - */ - private $encrypter; - /** * DaemonAuthenticate constructor. * * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter - * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository + * @param \Pterodactyl\Repositories\Eloquent\NodeRepository $repository */ - public function __construct(Encrypter $encrypter, NodeRepositoryInterface $repository) + public function __construct(Encrypter $encrypter, NodeRepository $repository) { $this->repository = $repository; $this->encrypter = $encrypter; diff --git a/app/Http/Middleware/Api/IsValidJson.php b/app/Http/Middleware/Api/IsValidJson.php new file mode 100644 index 000000000..20c54dab4 --- /dev/null +++ b/app/Http/Middleware/Api/IsValidJson.php @@ -0,0 +1,38 @@ +isJson() && ! empty($request->getContent())) { + json_decode($request->getContent(), true); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new BadRequestHttpException( + sprintf( + 'The JSON data passed in the request appears to be malformed. err_code: %d err_message: "%s"', + json_last_error(), + json_last_error_msg() + ) + ); + } + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/Server/AuthenticateAsSubuser.php b/app/Http/Middleware/Server/AuthenticateAsSubuser.php deleted file mode 100644 index 06707117a..000000000 --- a/app/Http/Middleware/Server/AuthenticateAsSubuser.php +++ /dev/null @@ -1,59 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Pterodactyl\Http\Middleware\Server; - -use Closure; -use Illuminate\Http\Request; -use Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService; -use Pterodactyl\Exceptions\Repository\RecordNotFoundException; -use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; - -class AuthenticateAsSubuser -{ - /** - * @var \Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService - */ - private $keyProviderService; - - /** - * SubuserAccessAuthenticate constructor. - * - * @param \Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService $keyProviderService - */ - public function __construct(DaemonKeyProviderService $keyProviderService) - { - $this->keyProviderService = $keyProviderService; - } - - /** - * Determine if a subuser has permissions to access a server, if so set their access token. - * - * @param \Illuminate\Http\Request $request - * @param \Closure $next - * @return mixed - * - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException - */ - public function handle(Request $request, Closure $next) - { - $server = $request->attributes->get('server'); - - try { - $token = $this->keyProviderService->handle($server, $request->user()); - } catch (RecordNotFoundException $exception) { - throw new AccessDeniedHttpException('This account does not have permission to access this server.'); - } - - $request->attributes->set('server_token', $token); - - return $next($request); - } -} diff --git a/app/Http/Middleware/Server/DatabaseBelongsToServer.php b/app/Http/Middleware/Server/DatabaseBelongsToServer.php deleted file mode 100644 index 169b67525..000000000 --- a/app/Http/Middleware/Server/DatabaseBelongsToServer.php +++ /dev/null @@ -1,56 +0,0 @@ -repository = $repository; - } - - /** - * Check if a database being requested belongs to the currently loaded server. - * If it does not, throw a 404 error, otherwise continue on with the request - * and set an attribute with the database. - * - * @param \Illuminate\Http\Request $request - * @param \Closure $next - * @return mixed - * - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - */ - public function handle(Request $request, Closure $next) - { - $server = $request->attributes->get('server'); - $database = $request->input('database') ?? $request->route()->parameter('database'); - - if (! is_digit($database)) { - throw new NotFoundHttpException; - } - - $database = $this->repository->find($database); - if (is_null($database) || $database->server_id !== $server->id) { - throw new NotFoundHttpException; - } - - $request->attributes->set('database', $database); - - return $next($request); - } -} diff --git a/app/Http/Middleware/Server/ScheduleBelongsToServer.php b/app/Http/Middleware/Server/ScheduleBelongsToServer.php deleted file mode 100644 index b76636291..000000000 --- a/app/Http/Middleware/Server/ScheduleBelongsToServer.php +++ /dev/null @@ -1,60 +0,0 @@ -hashids = $hashids; - $this->repository = $repository; - } - - /** - * Determine if a task is assigned to the active server. - * - * @param \Illuminate\Http\Request $request - * @param \Closure $next - * @return mixed - * - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException - */ - public function handle(Request $request, Closure $next) - { - $server = $request->attributes->get('server'); - - $scheduleId = $this->hashids->decodeFirst($request->route()->parameter('schedule'), 0); - $schedule = $this->repository->getScheduleWithTasks($scheduleId); - - if ($schedule->server_id !== $server->id) { - throw new NotFoundHttpException; - } - - $request->attributes->set('schedule', $schedule); - - return $next($request); - } -} diff --git a/app/Http/Middleware/Server/SubuserBelongsToServer.php b/app/Http/Middleware/Server/SubuserBelongsToServer.php deleted file mode 100644 index 7efc64990..000000000 --- a/app/Http/Middleware/Server/SubuserBelongsToServer.php +++ /dev/null @@ -1,67 +0,0 @@ -hashids = $hashids; - $this->repository = $repository; - } - - /** - * Determine if a user has permission to access and modify subuser. - * - * @param \Illuminate\Http\Request $request - * @param \Closure $next - * @return mixed - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException - */ - public function handle(Request $request, Closure $next) - { - $server = $request->attributes->get('server'); - - $hash = $request->route()->parameter('subuser', 0); - $subuser = $this->repository->find($this->hashids->decodeFirst($hash, 0)); - if (is_null($subuser) || $subuser->server_id !== $server->id) { - throw new NotFoundHttpException; - } - - if ($request->method() === 'PATCH') { - if ($subuser->user_id === $request->user()->id) { - throw new DisplayException(trans('exceptions.subusers.editing_self')); - } - } - - $request->attributes->set('subuser', $subuser); - - return $next($request); - } -} diff --git a/app/Http/Requests/Api/Application/Nodes/StoreNodeRequest.php b/app/Http/Requests/Api/Application/Nodes/StoreNodeRequest.php index 3ae474baf..8e23db439 100644 --- a/app/Http/Requests/Api/Application/Nodes/StoreNodeRequest.php +++ b/app/Http/Requests/Api/Application/Nodes/StoreNodeRequest.php @@ -21,7 +21,7 @@ class StoreNodeRequest extends ApplicationApiRequest /** * Validation rules to apply to this request. * - * @param null|array $rules + * @param array|null $rules * @return array */ public function rules(array $rules = null): array diff --git a/app/Http/Requests/Api/Client/Servers/Schedules/StoreScheduleRequest.php b/app/Http/Requests/Api/Client/Servers/Schedules/StoreScheduleRequest.php index 3db1aca6d..618cd9f4e 100644 --- a/app/Http/Requests/Api/Client/Servers/Schedules/StoreScheduleRequest.php +++ b/app/Http/Requests/Api/Client/Servers/Schedules/StoreScheduleRequest.php @@ -21,7 +21,7 @@ class StoreScheduleRequest extends ViewScheduleRequest { return [ 'name' => 'required|string|min:1', - 'is_active' => 'boolean', + 'is_active' => 'filled|boolean', 'minute' => 'required|string', 'hour' => 'required|string', 'day_of_month' => 'required|string', diff --git a/app/Http/Requests/Api/Client/Servers/Schedules/StoreTaskRequest.php b/app/Http/Requests/Api/Client/Servers/Schedules/StoreTaskRequest.php index 50fc9a2cb..cd95bf5e8 100644 --- a/app/Http/Requests/Api/Client/Servers/Schedules/StoreTaskRequest.php +++ b/app/Http/Requests/Api/Client/Servers/Schedules/StoreTaskRequest.php @@ -25,7 +25,7 @@ class StoreTaskRequest extends ViewScheduleRequest { return [ 'action' => 'required|in:command,power,backup', - 'payload' => 'required_unless:action,backup|string', + 'payload' => 'required_unless:action,backup|string|nullable', 'time_offset' => 'required|numeric|min:0|max:900', 'sequence_id' => 'sometimes|required|numeric|min:1', ]; diff --git a/app/Http/Requests/Api/Client/Servers/SendCommandRequest.php b/app/Http/Requests/Api/Client/Servers/SendCommandRequest.php index fe0dd1757..3f2f6c196 100644 --- a/app/Http/Requests/Api/Client/Servers/SendCommandRequest.php +++ b/app/Http/Requests/Api/Client/Servers/SendCommandRequest.php @@ -3,8 +3,9 @@ namespace Pterodactyl\Http\Requests\Api\Client\Servers; use Pterodactyl\Models\Permission; +use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest; -class SendCommandRequest extends GetServerRequest +class SendCommandRequest extends ClientApiRequest { /** * Determine if the API user has permission to perform this action. diff --git a/app/Http/Requests/Auth/LoginCheckpointRequest.php b/app/Http/Requests/Auth/LoginCheckpointRequest.php index 158f5c465..87d84ce97 100644 --- a/app/Http/Requests/Auth/LoginCheckpointRequest.php +++ b/app/Http/Requests/Auth/LoginCheckpointRequest.php @@ -2,6 +2,7 @@ namespace Pterodactyl\Http\Requests\Auth; +use Illuminate\Validation\Rule; use Illuminate\Foundation\Http\FormRequest; class LoginCheckpointRequest extends FormRequest @@ -25,7 +26,20 @@ class LoginCheckpointRequest extends FormRequest { return [ 'confirmation_token' => 'required|string', - 'authentication_code' => 'required|numeric', + 'authentication_code' => [ + 'nullable', + 'numeric', + Rule::requiredIf(function () { + return empty($this->input('recovery_token')); + }), + ], + 'recovery_token' => [ + 'nullable', + 'string', + Rule::requiredIf(function () { + return empty($this->input('authentication_code')); + }), + ], ]; } } diff --git a/app/Models/Database.php b/app/Models/Database.php index ae20a51c8..42fbb1acc 100644 --- a/app/Models/Database.php +++ b/app/Models/Database.php @@ -2,6 +2,21 @@ namespace Pterodactyl\Models; +/** + * @property int $id + * @property int $server_id + * @property int $database_host_id + * @property string $database + * @property string $username + * @property string $remote + * @property string $password + * @property int $max_connections + * @property \Carbon\Carbon $created_at + * @property \Carbon\Carbon $updated_at + * + * @property \Pterodactyl\Models\Server $server + * @property \Pterodactyl\Models\DatabaseHost $host + */ class Database extends Model { /** diff --git a/app/Models/Location.php b/app/Models/Location.php index 71d3b4117..17ba7e24a 100644 --- a/app/Models/Location.php +++ b/app/Models/Location.php @@ -2,6 +2,16 @@ namespace Pterodactyl\Models; +/** + * @property int $id + * @property string $short + * @property string $long + * @property \Carbon\Carbon $created_at + * @property \Carbon\Carbon $updated_at + * + * @property \Pterodactyl\Models\Node[] $nodes + * @property \Pterodactyl\Models\Server[] $servers + */ class Location extends Model { /** diff --git a/app/Models/RecoveryToken.php b/app/Models/RecoveryToken.php new file mode 100644 index 000000000..7be74f53c --- /dev/null +++ b/app/Models/RecoveryToken.php @@ -0,0 +1,39 @@ + 'required|string', + ]; + + /** + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function user() + { + return $this->belongsTo(User::class); + } +} diff --git a/app/Models/ServerTransfer.php b/app/Models/ServerTransfer.php index 94072bf0c..8d7120645 100644 --- a/app/Models/ServerTransfer.php +++ b/app/Models/ServerTransfer.php @@ -17,7 +17,7 @@ namespace Pterodactyl\Models; * * @property \Pterodactyl\Models\Server $server */ -class ServerTransfer extends Validable +class ServerTransfer extends Model { /** * The resource name for this model when it is transformed into an diff --git a/app/Models/Task.php b/app/Models/Task.php index f5a26b78a..f241b0717 100644 --- a/app/Models/Task.php +++ b/app/Models/Task.php @@ -90,7 +90,7 @@ class Task extends Model 'schedule_id' => 'required|numeric|exists:schedules,id', 'sequence_id' => 'required|numeric|min:1', 'action' => 'required|string', - 'payload' => 'required|string', + 'payload' => 'required_unless:action,backup|string', 'time_offset' => 'required|numeric|between:0,900', 'is_queued' => 'boolean', ]; diff --git a/app/Models/User.php b/app/Models/User.php index c8efc7bd6..408ceead3 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -39,6 +39,7 @@ use Pterodactyl\Notifications\SendPasswordReset as ResetPasswordNotification; * @property \Pterodactyl\Models\ApiKey[]|\Illuminate\Database\Eloquent\Collection $apiKeys * @property \Pterodactyl\Models\Server[]|\Illuminate\Database\Eloquent\Collection $servers * @property \Pterodactyl\Models\DaemonKey[]|\Illuminate\Database\Eloquent\Collection $keys + * @property \Pterodactyl\Models\RecoveryToken[]|\Illuminate\Database\Eloquent\Collection $recoveryTokens */ class User extends Model implements AuthenticatableContract, @@ -164,7 +165,7 @@ class User extends Model implements 'name_last' => 'required|string|between:1,255', 'password' => 'sometimes|nullable|string', 'root_admin' => 'boolean', - 'language' => 'required|string', + 'language' => 'string', 'use_totp' => 'boolean', 'totp_secret' => 'nullable|string', ]; @@ -251,4 +252,12 @@ class User extends Model implements return $this->hasMany(ApiKey::class) ->where('key_type', ApiKey::TYPE_ACCOUNT); } + + /** + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function recoveryTokens() + { + return $this->hasMany(RecoveryToken::class); + } } diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 4e6099b9b..0ea33b5da 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -33,16 +33,22 @@ class RouteServiceProvider extends ServiceProvider ->namespace($this->namespace . '\Auth') ->group(base_path('routes/auth.php')); - Route::middleware(['web', 'csrf', 'auth', 'server', 'subuser.auth', 'node.maintenance']) + Route::middleware(['web', 'csrf', 'auth', 'server', 'node.maintenance']) ->prefix('/api/server/{server}') ->namespace($this->namespace . '\Server') ->group(base_path('routes/server.php')); - Route::middleware(['api'])->prefix('/api/application') + Route::middleware([ + sprintf('throttle:%s,%s', config('http.rate_limit.application'), config('http.rate_limit.application_period')), + 'api', + ])->prefix('/api/application') ->namespace($this->namespace . '\Api\Application') ->group(base_path('routes/api-application.php')); - Route::middleware(['client-api'])->prefix('/api/client') + Route::middleware([ + sprintf('throttle:%s,%s', config('http.rate_limit.client'), config('http.rate_limit.client_period')), + 'client-api', + ])->prefix('/api/client') ->namespace($this->namespace . '\Api\Client') ->group(base_path('routes/api-client.php')); diff --git a/app/Repositories/Concerns/Searchable.php b/app/Repositories/Concerns/Searchable.php index 26ed6544a..8bb11a552 100644 --- a/app/Repositories/Concerns/Searchable.php +++ b/app/Repositories/Concerns/Searchable.php @@ -7,7 +7,7 @@ trait Searchable /** * The search term to use when filtering results. * - * @var null|string + * @var string|null */ protected $searchTerm; diff --git a/app/Repositories/Eloquent/DatabaseRepository.php b/app/Repositories/Eloquent/DatabaseRepository.php index df9dbb6ee..48dec217b 100644 --- a/app/Repositories/Eloquent/DatabaseRepository.php +++ b/app/Repositories/Eloquent/DatabaseRepository.php @@ -140,7 +140,7 @@ class DatabaseRepository extends EloquentRepository implements DatabaseRepositor */ public function createUser(string $username, string $remote, string $password, $max_connections): bool { - if (!$max_connections) { + if (! $max_connections) { return $this->run(sprintf('CREATE USER `%s`@`%s` IDENTIFIED BY \'%s\'', $username, $remote, $password)); } else { return $this->run(sprintf('CREATE USER `%s`@`%s` IDENTIFIED BY \'%s\' WITH MAX_USER_CONNECTIONS %s', $username, $remote, $password, $max_connections)); diff --git a/app/Repositories/Eloquent/RecoveryTokenRepository.php b/app/Repositories/Eloquent/RecoveryTokenRepository.php new file mode 100644 index 000000000..5dfeeacfe --- /dev/null +++ b/app/Repositories/Eloquent/RecoveryTokenRepository.php @@ -0,0 +1,16 @@ +getHttpClient()->post( diff --git a/app/Services/DaemonKeys/DaemonKeyCreationService.php b/app/Services/DaemonKeys/DaemonKeyCreationService.php deleted file mode 100644 index d2551324a..000000000 --- a/app/Services/DaemonKeys/DaemonKeyCreationService.php +++ /dev/null @@ -1,87 +0,0 @@ -. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -namespace Pterodactyl\Services\DaemonKeys; - -use Carbon\Carbon; -use Illuminate\Contracts\Config\Repository as ConfigRepository; -use Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface; - -class DaemonKeyCreationService -{ - /** - * @var \Carbon\Carbon - */ - protected $carbon; - - /** - * @var \Illuminate\Contracts\Config\Repository - */ - protected $config; - - /** - * @var \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface - */ - protected $repository; - - /** - * DaemonKeyCreationService constructor. - * - * @param \Carbon\Carbon $carbon - * @param \Illuminate\Contracts\Config\Repository $config - * @param \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface $repository - */ - public function __construct( - Carbon $carbon, - ConfigRepository $config, - DaemonKeyRepositoryInterface $repository - ) { - $this->carbon = $carbon; - $this->config = $config; - $this->repository = $repository; - } - - /** - * Create a new daemon key to be used when connecting to a daemon. - * - * @param int $server - * @param int $user - * @return string - * - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - */ - public function handle(int $server, int $user) - { - $secret = DaemonKeyRepositoryInterface::INTERNAL_KEY_IDENTIFIER . str_random(40); - - $this->repository->withoutFreshModel()->create([ - 'user_id' => $user, - 'server_id' => $server, - 'secret' => $secret, - 'expires_at' => $this->carbon->now()->addMinutes($this->config->get('pterodactyl.api.key_expire_time'))->toDateTimeString(), - ]); - - return $secret; - } -} diff --git a/app/Services/DaemonKeys/DaemonKeyProviderService.php b/app/Services/DaemonKeys/DaemonKeyProviderService.php deleted file mode 100644 index c875239d7..000000000 --- a/app/Services/DaemonKeys/DaemonKeyProviderService.php +++ /dev/null @@ -1,121 +0,0 @@ -. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -namespace Pterodactyl\Services\DaemonKeys; - -use Carbon\Carbon; -use Pterodactyl\Models\User; -use Pterodactyl\Models\Server; -use Pterodactyl\Exceptions\Repository\RecordNotFoundException; -use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; -use Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface; - -class DaemonKeyProviderService -{ - /** - * @var \Pterodactyl\Services\DaemonKeys\DaemonKeyCreationService - */ - private $keyCreationService; - - /** - * @var \Pterodactyl\Services\DaemonKeys\DaemonKeyUpdateService - */ - private $keyUpdateService; - - /** - * @var \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface - */ - private $repository; - - /** - * @var \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface - */ - private $subuserRepository; - - /** - * GetDaemonKeyService constructor. - * - * @param \Pterodactyl\Services\DaemonKeys\DaemonKeyCreationService $keyCreationService - * @param \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface $repository - * @param \Pterodactyl\Services\DaemonKeys\DaemonKeyUpdateService $keyUpdateService - * @param \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface $subuserRepository - */ - public function __construct( - DaemonKeyCreationService $keyCreationService, - DaemonKeyRepositoryInterface $repository, - DaemonKeyUpdateService $keyUpdateService, - SubuserRepositoryInterface $subuserRepository - ) { - $this->keyCreationService = $keyCreationService; - $this->keyUpdateService = $keyUpdateService; - $this->repository = $repository; - $this->subuserRepository = $subuserRepository; - } - - /** - * Get the access key for a user on a specific server. - * - * @param \Pterodactyl\Models\Server $server - * @param \Pterodactyl\Models\User $user - * @param bool $updateIfExpired - * @return string - * - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - */ - public function handle(Server $server, User $user, $updateIfExpired = true): string - { - try { - $key = $this->repository->findFirstWhere([ - ['user_id', '=', $user->id], - ['server_id', '=', $server->id], - ]); - } catch (RecordNotFoundException $exception) { - // If key doesn't exist but we are an admin or the server owner, - // create it. - if ($user->root_admin || $user->id === $server->owner_id) { - return $this->keyCreationService->handle($server->id, $user->id); - } - - // Check if user is a subuser for this server. Ideally they should always have - // a record associated with them in the database, but we should still handle - // that potentiality here. - // - // If no subuser is found, a RecordNotFoundException will be thrown, thus handling - // the parent error as well. - $subuser = $this->subuserRepository->findFirstWhere([ - ['user_id', '=', $user->id], - ['server_id', '=', $server->id], - ]); - - return $this->keyCreationService->handle($subuser->server_id, $subuser->user_id); - } - - if (! $updateIfExpired || Carbon::now()->diffInSeconds($key->expires_at, false) > 0) { - return $key->secret; - } - - return $this->keyUpdateService->handle($key->id); - } -} diff --git a/app/Services/DaemonKeys/DaemonKeyUpdateService.php b/app/Services/DaemonKeys/DaemonKeyUpdateService.php deleted file mode 100644 index 91427f3de..000000000 --- a/app/Services/DaemonKeys/DaemonKeyUpdateService.php +++ /dev/null @@ -1,88 +0,0 @@ -. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -namespace Pterodactyl\Services\DaemonKeys; - -use Carbon\Carbon; -use Webmozart\Assert\Assert; -use Illuminate\Contracts\Config\Repository as ConfigRepository; -use Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface; - -class DaemonKeyUpdateService -{ - /** - * @var \Carbon\Carbon - */ - protected $carbon; - - /** - * @var \Illuminate\Contracts\Config\Repository - */ - protected $config; - - /** - * @var \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface - */ - protected $repository; - - /** - * DaemonKeyUpdateService constructor. - * - * @param \Carbon\Carbon $carbon - * @param \Illuminate\Contracts\Config\Repository $config - * @param \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface $repository - */ - public function __construct( - Carbon $carbon, - ConfigRepository $config, - DaemonKeyRepositoryInterface $repository - ) { - $this->carbon = $carbon; - $this->config = $config; - $this->repository = $repository; - } - - /** - * Update a daemon key to expire the previous one. - * - * @param int $key - * @return string - * - * @throws \RuntimeException - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - */ - public function handle($key) - { - Assert::integerish($key, 'First argument passed to handle must be an integer, received %s.'); - - $secret = DaemonKeyRepositoryInterface::INTERNAL_KEY_IDENTIFIER . str_random(40); - $this->repository->withoutFreshModel()->update($key, [ - 'secret' => $secret, - 'expires_at' => $this->carbon->now()->addMinutes($this->config->get('pterodactyl.api.key_expire_time'))->toDateTimeString(), - ]); - - return $secret; - } -} diff --git a/app/Services/Databases/DatabaseManagementService.php b/app/Services/Databases/DatabaseManagementService.php index b98a757db..7de6e2929 100644 --- a/app/Services/Databases/DatabaseManagementService.php +++ b/app/Services/Databases/DatabaseManagementService.php @@ -3,19 +3,22 @@ namespace Pterodactyl\Services\Databases; use Exception; +use Pterodactyl\Models\Server; use Pterodactyl\Models\Database; use Pterodactyl\Helpers\Utilities; -use Illuminate\Database\DatabaseManager; +use Illuminate\Database\ConnectionInterface; use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Extensions\DynamicDatabaseConnection; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; +use Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException; +use Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException; class DatabaseManagementService { /** - * @var \Illuminate\Database\DatabaseManager + * @var \Illuminate\Database\ConnectionInterface */ - private $database; + private $connection; /** * @var \Pterodactyl\Extensions\DynamicDatabaseConnection @@ -33,84 +36,113 @@ class DatabaseManagementService 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 $useRandomHost = false; + protected $validateDatabaseLimit = true; /** * CreationService constructor. * - * @param \Illuminate\Database\DatabaseManager $database + * @param \Illuminate\Database\ConnectionInterface $connection * @param \Pterodactyl\Extensions\DynamicDatabaseConnection $dynamic * @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $repository * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter */ public function __construct( - DatabaseManager $database, + ConnectionInterface $connection, DynamicDatabaseConnection $dynamic, DatabaseRepositoryInterface $repository, Encrypter $encrypter ) { - $this->database = $database; + $this->connection = $connection; $this->dynamic = $dynamic; $this->encrypter = $encrypter; $this->repository = $repository; } + /** + * 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 int $server + * @param \Pterodactyl\Models\Server $server * @param array $data * @return \Pterodactyl\Models\Database * - * @throws \Exception + * @throws \Throwable + * @throws \Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException + * @throws \Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException */ - public function create($server, array $data) + public function create(Server $server, array $data) { - $data['server_id'] = $server; - $data['database'] = sprintf('s%d_%s', $server, $data['database']); - $data['username'] = sprintf('u%d_%s', $server, str_random(10)); - $data['password'] = $this->encrypter->encrypt( - Utilities::randomStringWithSpecialCharacters(24) - ); + 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; + } + } + + $data = array_merge($data, [ + 'server_id' => $server->id, + 'database' => sprintf('s%d_%s', $server->id, $data['database']), + 'username' => sprintf('u%d_%s', $server->id, str_random(10)), + 'password' => $this->encrypter->encrypt( + Utilities::randomStringWithSpecialCharacters(24) + ), + ]); + + $database = null; - $this->database->beginTransaction(); try { - $database = $this->repository->createIfNotExists($data); - $this->dynamic->set('dynamic', $data['database_host_id']); + return $this->connection->transaction(function () use ($data, &$database) { + $database = $this->repository->createIfNotExists($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(); + $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(); - $this->database->commit(); - } catch (Exception $ex) { + return $database; + }); + } catch (Exception $exception) { try { - if (isset($database) && $database instanceof Database) { + if ($database instanceof Database) { $this->repository->dropDatabase($database->database); $this->repository->dropUser($database->username, $database->remote); $this->repository->flush(); } - } catch (Exception $exTwo) { - // ignore an exception + } catch (Exception $exception) { + // Do nothing here. We've already encountered an issue before this point so no + // reason to prioritize this error over the initial one. } - $this->database->rollBack(); - throw $ex; + throw $exception; } - - return $database; } /** diff --git a/app/Services/Databases/DeployServerDatabaseService.php b/app/Services/Databases/DeployServerDatabaseService.php index b48f9e6f4..734740324 100644 --- a/app/Services/Databases/DeployServerDatabaseService.php +++ b/app/Services/Databases/DeployServerDatabaseService.php @@ -6,9 +6,7 @@ use Pterodactyl\Models\Server; use Pterodactyl\Models\Database; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; -use Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException; use Pterodactyl\Exceptions\Service\Database\NoSuitableDatabaseHostException; -use Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException; class DeployServerDatabaseService { @@ -49,20 +47,12 @@ class DeployServerDatabaseService * @param array $data * @return \Pterodactyl\Models\Database * + * @throws \Throwable + * @throws \Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException * @throws \Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException - * @throws \Exception */ public function handle(Server $server, array $data): Database { - if (! config('pterodactyl.client_features.databases.enabled')) { - throw new DatabaseClientFeatureNotEnabledException; - } - - $databases = $this->repository->findCountWhere([['server_id', '=', $server->id]]); - if (! is_null($server->database_limit) && $databases >= $server->database_limit) { - throw new TooManyDatabasesException; - } - $allowRandom = config('pterodactyl.client_features.databases.allow_random'); $hosts = $this->databaseHostRepository->setColumns(['id'])->findWhere([ ['node_id', '=', $server->node_id], @@ -81,7 +71,7 @@ class DeployServerDatabaseService $host = $hosts->random(); - return $this->managementService->create($server->id, [ + return $this->managementService->create($server, [ 'database_host_id' => $host->id, 'database' => array_get($data, 'database'), 'remote' => array_get($data, 'remote'), diff --git a/app/Services/Eggs/Sharing/EggImporterService.php b/app/Services/Eggs/Sharing/EggImporterService.php index 45eee14f4..006360cfa 100644 --- a/app/Services/Eggs/Sharing/EggImporterService.php +++ b/app/Services/Eggs/Sharing/EggImporterService.php @@ -76,7 +76,16 @@ class EggImporterService public function handle(UploadedFile $file, int $nest): Egg { if ($file->getError() !== UPLOAD_ERR_OK || ! $file->isFile()) { - throw new InvalidFileUploadException(trans('exceptions.nest.importer.file_error')); + throw new InvalidFileUploadException( + sprintf( + 'The selected file ["%s"] was not in a valid format to import. (is_file: %s is_valid: %s err_code: %s err: %s)', + $file->getFilename(), + $file->isFile() ? 'true' : 'false', + $file->isValid() ? 'true' : 'false', + $file->getError(), + $file->getErrorMessage() + ) + ); } $parsed = json_decode($file->openFile()->fread($file->getSize())); diff --git a/app/Services/Eggs/Sharing/EggUpdateImporterService.php b/app/Services/Eggs/Sharing/EggUpdateImporterService.php index b04904194..3acf3f90e 100644 --- a/app/Services/Eggs/Sharing/EggUpdateImporterService.php +++ b/app/Services/Eggs/Sharing/EggUpdateImporterService.php @@ -57,7 +57,16 @@ class EggUpdateImporterService public function handle(int $egg, UploadedFile $file) { if ($file->getError() !== UPLOAD_ERR_OK || ! $file->isFile()) { - throw new InvalidFileUploadException(trans('exceptions.nest.importer.file_error')); + throw new InvalidFileUploadException( + sprintf( + 'The selected file ["%s"] was not in a valid format to import. (is_file: %s is_valid: %s err_code: %s err: %s)', + $file->getFilename(), + $file->isFile() ? 'true' : 'false', + $file->isValid() ? 'true' : 'false', + $file->getError(), + $file->getErrorMessage() + ) + ); } $parsed = json_decode($file->openFile()->fread($file->getSize())); diff --git a/app/Services/Helpers/AssetHashService.php b/app/Services/Helpers/AssetHashService.php index c3d4c98ff..f9b6a2cd1 100644 --- a/app/Services/Helpers/AssetHashService.php +++ b/app/Services/Helpers/AssetHashService.php @@ -24,7 +24,7 @@ class AssetHashService private $application; /** - * @var null|array + * @var array|null */ protected static $manifest; diff --git a/app/Services/Helpers/SoftwareVersionService.php b/app/Services/Helpers/SoftwareVersionService.php index 6aa9c8935..893c097d0 100644 --- a/app/Services/Helpers/SoftwareVersionService.php +++ b/app/Services/Helpers/SoftwareVersionService.php @@ -4,7 +4,7 @@ namespace Pterodactyl\Services\Helpers; use Exception; use GuzzleHttp\Client; -use Cake\Chronos\Chronos; +use Carbon\CarbonImmutable; use Illuminate\Support\Arr; use Illuminate\Contracts\Cache\Repository as CacheRepository; use Pterodactyl\Exceptions\Service\Helper\CdnVersionFetchingException; @@ -120,7 +120,7 @@ class SoftwareVersionService */ protected function cacheVersionData() { - return $this->cache->remember(self::VERSION_CACHE_KEY, Chronos::now()->addMinutes(config()->get('pterodactyl.cdn.cache_time', 60)), function () { + return $this->cache->remember(self::VERSION_CACHE_KEY, CarbonImmutable::now()->addMinutes(config()->get('pterodactyl.cdn.cache_time', 60)), function () { try { $response = $this->client->request('GET', config()->get('pterodactyl.cdn.url')); diff --git a/app/Services/Nodes/NodeCreationService.php b/app/Services/Nodes/NodeCreationService.php index fabc36e05..a44c036bd 100644 --- a/app/Services/Nodes/NodeCreationService.php +++ b/app/Services/Nodes/NodeCreationService.php @@ -5,7 +5,7 @@ namespace Pterodactyl\Services\Nodes; use Ramsey\Uuid\Uuid; use Illuminate\Support\Str; use Pterodactyl\Models\Node; -use Illuminate\Encryption\Encrypter; +use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; class NodeCreationService @@ -16,14 +16,14 @@ class NodeCreationService protected $repository; /** - * @var \Illuminate\Encryption\Encrypter + * @var \Illuminate\Contracts\Encryption\Encrypter */ private $encrypter; /** * CreationService constructor. * - * @param \Illuminate\Encryption\Encrypter $encrypter + * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository */ public function __construct(Encrypter $encrypter, NodeRepositoryInterface $repository) diff --git a/app/Services/Servers/BuildModificationService.php b/app/Services/Servers/BuildModificationService.php index 5f4b10d34..b6768fdb1 100644 --- a/app/Services/Servers/BuildModificationService.php +++ b/app/Services/Servers/BuildModificationService.php @@ -71,8 +71,8 @@ class BuildModificationService * @return \Pterodactyl\Models\Server * * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ public function handle(Server $server, array $data) { @@ -91,7 +91,7 @@ class BuildModificationService } } - /** @var \Pterodactyl\Models\Server $server */ + /* @var \Pterodactyl\Models\Server $server */ $server = $this->repository->withFreshModel()->update($server->id, [ 'oom_disabled' => array_get($data, 'oom_disabled'), 'memory' => array_get($data, 'memory'), diff --git a/app/Services/Servers/ReinstallServerService.php b/app/Services/Servers/ReinstallServerService.php index 27955c475..aff0321ce 100644 --- a/app/Services/Servers/ReinstallServerService.php +++ b/app/Services/Servers/ReinstallServerService.php @@ -4,8 +4,8 @@ namespace Pterodactyl\Services\Servers; use Pterodactyl\Models\Server; use Illuminate\Database\ConnectionInterface; +use Pterodactyl\Repositories\Eloquent\ServerRepository; use Pterodactyl\Repositories\Wings\DaemonServerRepository; -use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; class ReinstallServerService { @@ -17,27 +17,27 @@ class ReinstallServerService /** * @var \Illuminate\Database\ConnectionInterface */ - private $database; + private $connection; /** - * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + * @var \Pterodactyl\Repositories\Eloquent\ServerRepository */ private $repository; /** * ReinstallService constructor. * - * @param \Illuminate\Database\ConnectionInterface $database + * @param \Illuminate\Database\ConnectionInterface $connection * @param \Pterodactyl\Repositories\Wings\DaemonServerRepository $daemonServerRepository - * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository + * @param \Pterodactyl\Repositories\Eloquent\ServerRepository $repository */ public function __construct( - ConnectionInterface $database, + ConnectionInterface $connection, DaemonServerRepository $daemonServerRepository, - ServerRepositoryInterface $repository + ServerRepository $repository ) { $this->daemonServerRepository = $daemonServerRepository; - $this->database = $database; + $this->connection = $connection; $this->repository = $repository; } @@ -51,14 +51,14 @@ class ReinstallServerService */ public function reinstall(Server $server) { - $this->database->transaction(function () use ($server) { - $this->repository->withoutFreshModel()->update($server->id, [ + return $this->connection->transaction(function () use ($server) { + $updated = $this->repository->update($server->id, [ 'installed' => Server::STATUS_INSTALLING, - ]); + ], true, true); $this->daemonServerRepository->setServer($server)->reinstall(); - }); - return $server->refresh(); + return $updated; + }); } } diff --git a/app/Services/Servers/ServerCreationService.php b/app/Services/Servers/ServerCreationService.php index cce242e5a..b84bed7c5 100644 --- a/app/Services/Servers/ServerCreationService.php +++ b/app/Services/Servers/ServerCreationService.php @@ -310,8 +310,6 @@ class ServerCreationService return $allocation->node_id; } - /** @noinspection PhpDocMissingThrowsInspection */ - /** * Create a unique UUID and UUID-Short combo for a server. * @@ -319,7 +317,6 @@ class ServerCreationService */ private function generateUniqueUuidCombo(): string { - /** @noinspection PhpUnhandledExceptionInspection */ $uuid = Uuid::uuid4()->toString(); if (! $this->repository->isUniqueUuidCombo($uuid, substr($uuid, 0, 8))) { diff --git a/app/Services/Servers/ServerDeletionService.php b/app/Services/Servers/ServerDeletionService.php index d4ce9a896..8d7217769 100644 --- a/app/Services/Servers/ServerDeletionService.php +++ b/app/Services/Servers/ServerDeletionService.php @@ -2,6 +2,7 @@ namespace Pterodactyl\Services\Servers; +use Exception; use Psr\Log\LoggerInterface; use Pterodactyl\Models\Server; use Illuminate\Database\ConnectionInterface; @@ -109,7 +110,15 @@ class ServerDeletionService $this->connection->transaction(function () use ($server) { $this->databaseRepository->setColumns('id')->findWhere([['server_id', '=', $server->id]])->each(function ($item) { - $this->databaseManagementService->delete($item->id); + try { + $this->databaseManagementService->delete($item->id); + } catch (Exception $exception) { + if ($this->force) { + $this->writer->warning($exception); + } else { + throw $exception; + } + } }); $this->repository->delete($server->id); diff --git a/app/Services/Users/ToggleTwoFactorService.php b/app/Services/Users/ToggleTwoFactorService.php index 2c393db01..f8b41b454 100644 --- a/app/Services/Users/ToggleTwoFactorService.php +++ b/app/Services/Users/ToggleTwoFactorService.php @@ -3,10 +3,13 @@ namespace Pterodactyl\Services\Users; use Carbon\Carbon; +use Illuminate\Support\Str; use Pterodactyl\Models\User; use PragmaRX\Google2FA\Google2FA; +use Illuminate\Database\ConnectionInterface; use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; +use Pterodactyl\Repositories\Eloquent\RecoveryTokenRepository; use Pterodactyl\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid; class ToggleTwoFactorService @@ -26,21 +29,37 @@ class ToggleTwoFactorService */ private $repository; + /** + * @var \Pterodactyl\Repositories\Eloquent\RecoveryTokenRepository + */ + private $recoveryTokenRepository; + + /** + * @var \Illuminate\Database\ConnectionInterface + */ + private $connection; + /** * ToggleTwoFactorService constructor. * + * @param \Illuminate\Database\ConnectionInterface $connection * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter * @param \PragmaRX\Google2FA\Google2FA $google2FA + * @param \Pterodactyl\Repositories\Eloquent\RecoveryTokenRepository $recoveryTokenRepository * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository */ public function __construct( + ConnectionInterface $connection, Encrypter $encrypter, Google2FA $google2FA, + RecoveryTokenRepository $recoveryTokenRepository, UserRepositoryInterface $repository ) { $this->encrypter = $encrypter; $this->google2FA = $google2FA; $this->repository = $repository; + $this->recoveryTokenRepository = $recoveryTokenRepository; + $this->connection = $connection; } /** @@ -49,32 +68,60 @@ class ToggleTwoFactorService * @param \Pterodactyl\Models\User $user * @param string $token * @param bool|null $toggleState - * @return bool + * @return string[] * + * @throws \Throwable * @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException * @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException * @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid */ - public function handle(User $user, string $token, bool $toggleState = null): bool + public function handle(User $user, string $token, bool $toggleState = null): array { $secret = $this->encrypter->decrypt($user->totp_secret); $isValidToken = $this->google2FA->verifyKey($secret, $token, config()->get('pterodactyl.auth.2fa.window')); if (! $isValidToken) { - throw new TwoFactorAuthenticationTokenInvalid( - 'The token provided is not valid.' - ); + throw new TwoFactorAuthenticationTokenInvalid('The token provided is not valid.'); } - $this->repository->withoutFreshModel()->update($user->id, [ - 'totp_authenticated_at' => Carbon::now(), - 'use_totp' => (is_null($toggleState) ? ! $user->use_totp : $toggleState), - ]); + return $this->connection->transaction(function () use ($user, $toggleState) { + // Now that we're enabling 2FA on the account, generate 10 recovery tokens for the account + // and store them hashed in the database. We'll return them to the caller so that the user + // can see and save them. + // + // If a user is unable to login with a 2FA token they can provide one of these backup codes + // which will then be marked as deleted from the database and will also bypass 2FA protections + // on their account. + $tokens = []; + if ((! $toggleState && ! $user->use_totp) || $toggleState) { + $inserts = []; + for ($i = 0; $i < 10; $i++) { + $token = Str::random(10); - return true; + $inserts[] = [ + 'user_id' => $user->id, + 'token' => password_hash($token, PASSWORD_DEFAULT), + ]; + + $tokens[] = $token; + } + + // Before inserting any new records make sure all of the old ones are deleted to avoid + // any issues or storing an unnecessary number of tokens in the database. + $this->recoveryTokenRepository->deleteWhere(['user_id' => $user->id]); + + // Bulk insert the hashed tokens. + $this->recoveryTokenRepository->insert($inserts); + } + + $this->repository->withoutFreshModel()->update($user->id, [ + 'totp_authenticated_at' => Carbon::now(), + 'use_totp' => (is_null($toggleState) ? ! $user->use_totp : $toggleState), + ]); + + return $tokens; + }); } } diff --git a/app/Services/Users/TwoFactorSetupService.php b/app/Services/Users/TwoFactorSetupService.php index 032a43083..5dcb7879c 100644 --- a/app/Services/Users/TwoFactorSetupService.php +++ b/app/Services/Users/TwoFactorSetupService.php @@ -71,7 +71,7 @@ class TwoFactorSetupService 'totp_secret' => $this->encrypter->encrypt($secret), ]); - $company = preg_replace('/\s/', '', $this->config->get('app.name')); + $company = urlencode(preg_replace('/\s/', '', $this->config->get('app.name'))); return sprintf( 'otpauth://totp/%1$s:%2$s?secret=%3$s&issuer=%1$s', diff --git a/app/Transformers/Api/Application/AllocationTransformer.php b/app/Transformers/Api/Application/AllocationTransformer.php index f1a35f4f2..e4fa1bb57 100644 --- a/app/Transformers/Api/Application/AllocationTransformer.php +++ b/app/Transformers/Api/Application/AllocationTransformer.php @@ -2,6 +2,8 @@ namespace Pterodactyl\Transformers\Api\Application; +use Pterodactyl\Models\Node; +use Pterodactyl\Models\Server; use Pterodactyl\Models\Allocation; use Pterodactyl\Services\Acl\Api\AdminAcl; @@ -54,10 +56,8 @@ class AllocationTransformer extends BaseTransformer return $this->null(); } - $allocation->loadMissing('node'); - return $this->item( - $allocation->getRelation('node'), $this->makeTransformer(NodeTransformer::class), 'node' + $allocation->node, $this->makeTransformer(NodeTransformer::class), Node::RESOURCE_NAME ); } @@ -70,14 +70,12 @@ class AllocationTransformer extends BaseTransformer */ public function includeServer(Allocation $allocation) { - if (! $this->authorize(AdminAcl::RESOURCE_SERVERS)) { + if (! $this->authorize(AdminAcl::RESOURCE_SERVERS) || ! $allocation->server) { return $this->null(); } - $allocation->loadMissing('server'); - return $this->item( - $allocation->getRelation('server'), $this->makeTransformer(ServerTransformer::class), 'server' + $allocation->server, $this->makeTransformer(ServerTransformer::class), Server::RESOURCE_NAME ); } } diff --git a/bootstrap/tests.php b/bootstrap/tests.php index 276786610..e22334625 100644 --- a/bootstrap/tests.php +++ b/bootstrap/tests.php @@ -17,12 +17,22 @@ $kernel->bootstrap(); $output = new ConsoleOutput; +if (config('database.default') !== 'testing') { + $output->writeln(PHP_EOL . 'Cannot run test process against non-testing database.'); + $output->writeln(PHP_EOL . 'Environment is currently pointed at: "' . config('database.default') . '".'); + exit(1); +} + /* * Perform database migrations and reseeding before continuing with * running the tests. */ -$output->writeln(PHP_EOL . 'Refreshing database for Integration tests...'); -$kernel->call('migrate:fresh', ['--database' => 'testing']); +if (! env('SKIP_MIGRATIONS')) { + $output->writeln(PHP_EOL . 'Refreshing database for Integration tests...'); + $kernel->call('migrate:fresh', ['--database' => 'testing']); -$output->writeln('Seeding database for Integration tests...' . PHP_EOL); -$kernel->call('db:seed', ['--database' => 'testing']); + $output->writeln('Seeding database for Integration tests...' . PHP_EOL); + $kernel->call('db:seed', ['--database' => 'testing']); +} else { + $output->writeln(PHP_EOL . 'Skipping database migrations...' . PHP_EOL); +} diff --git a/composer.json b/composer.json index 0a996b06d..0ccfab915 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ } ], "require": { - "php": ">=7.2", + "php": "^7.2", "ext-mbstring": "*", "ext-pdo_mysql": "*", "ext-zip": "*", @@ -23,9 +23,10 @@ "guzzlehttp/guzzle": "^6.5", "hashids/hashids": "^4.0", "laracasts/utilities": "^3.1", - "laravel/framework": "^6.18", + "laravel/framework": "^7.17", "laravel/helpers": "^1.2", - "laravel/tinker": "^1.0", + "laravel/tinker": "^2.4", + "laravel/ui": "^2.0", "lcobucci/jwt": "^3.3", "league/flysystem-aws-s3-v3": "^1.0", "league/flysystem-memory": "^1.0", @@ -33,22 +34,23 @@ "pragmarx/google2fa": "^5.0", "predis/predis": "^1.1", "prologue/alerts": "^0.4", + "psy/psysh": "^0.10.4", "s1lentium/iptools": "^1.1", "spatie/laravel-fractal": "^5.7", - "staudenmeir/belongs-to-through": "^2.9", + "staudenmeir/belongs-to-through": "^2.10", "symfony/yaml": "^4.4", - "webmozart/assert": "^1.7" + "webmozart/assert": "^1.9" }, "require-dev": { - "barryvdh/laravel-debugbar": "^3.2", - "barryvdh/laravel-ide-helper": "^2.6", - "codedungeon/phpunit-result-printer": "0.25.1", - "friendsofphp/php-cs-fixer": "^2.16.1", - "fzaninotto/faker": "^1.9.1", - "laravel/dusk": "^5.11", - "mockery/mockery": "^1.0", + "barryvdh/laravel-debugbar": "^3.3", + "barryvdh/laravel-ide-helper": "^2.7", + "codedungeon/phpunit-result-printer": "^0.28.0", + "friendsofphp/php-cs-fixer": "2.16.1", + "fzaninotto/faker": "^1.9", + "laravel/dusk": "^6.3", + "mockery/mockery": "^1.4", "php-mock/php-mock-phpunit": "^2.6", - "phpunit/phpunit": "^7" + "phpunit/phpunit": "^8.5" }, "autoload": { "classmap": [ diff --git a/composer.lock b/composer.lock index d4dfac2db..7593ee4b4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b6a885effd46e896377825b057886f23", + "content-hash": "155b8e930e604c0476fa975b1084ca3f", "packages": [ { "name": "appstract/laravel-blade-directives", - "version": "1.8.0", + "version": "1.9.0", "source": { "type": "git", "url": "https://github.com/appstract/laravel-blade-directives.git", - "reference": "ac9958e5499d21b12c317a43d96e41c00eaa3fba" + "reference": "c569529da9bf4d87c85157061c5e86b5ee1f8ff4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appstract/laravel-blade-directives/zipball/ac9958e5499d21b12c317a43d96e41c00eaa3fba", - "reference": "ac9958e5499d21b12c317a43d96e41c00eaa3fba", + "url": "https://api.github.com/repos/appstract/laravel-blade-directives/zipball/c569529da9bf4d87c85157061c5e86b5ee1f8ff4", + "reference": "c569529da9bf4d87c85157061c5e86b5ee1f8ff4", "shasum": "" }, "require": { @@ -25,8 +25,7 @@ "php": "^7.1.3" }, "require-dev": { - "orchestra/testbench": "~3.7|^4.0", - "phpunit/phpunit": "^7.0" + "orchestra/testbench": "~3.7|^4.0|^5.0" }, "type": "library", "extra": { @@ -59,20 +58,20 @@ "appstract", "laravel-blade-directives" ], - "time": "2020-03-04T08:57:34+00:00" + "time": "2020-04-14T08:41:18+00:00" }, { "name": "aws/aws-sdk-php", - "version": "3.134.3", + "version": "3.142.8", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "3de2711a47e7c3f5e93a5c83f019188fd23f852f" + "reference": "a9cffb2442b63ca8a99ed3e68aef19df221e2a54" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/3de2711a47e7c3f5e93a5c83f019188fd23f852f", - "reference": "3de2711a47e7c3f5e93a5c83f019188fd23f852f", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/a9cffb2442b63ca8a99ed3e68aef19df221e2a54", + "reference": "a9cffb2442b63ca8a99ed3e68aef19df221e2a54", "shasum": "" }, "require": { @@ -95,6 +94,7 @@ "ext-pcntl": "*", "ext-sockets": "*", "nette/neon": "^2.3", + "paragonie/random_compat": ">= 2", "phpunit/phpunit": "^4.8.35|^5.4.3", "psr/cache": "^1.0", "psr/simple-cache": "^1.0", @@ -143,7 +143,59 @@ "s3", "sdk" ], - "time": "2020-04-03T18:11:51+00:00" + "time": "2020-06-23T18:46:28+00:00" + }, + { + "name": "brick/math", + "version": "0.8.15", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "9b08d412b9da9455b210459ff71414de7e6241cd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/9b08d412b9da9455b210459ff71414de7e6241cd", + "reference": "9b08d412b9da9455b210459ff71414de7e6241cd", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.1|^8.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^7.5.15|^8.5", + "vimeo/psalm": "^3.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "brick", + "math" + ], + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/brick/math", + "type": "tidelift" + } + ], + "time": "2020-04-15T15:59:35+00:00" }, { "name": "cakephp/chronos", @@ -236,20 +288,20 @@ }, { "name": "doctrine/cache", - "version": "1.10.0", + "version": "1.10.1", "source": { "type": "git", "url": "https://github.com/doctrine/cache.git", - "reference": "382e7f4db9a12dc6c19431743a2b096041bcdd62" + "reference": "35a4a70cd94e09e2259dfae7488afc6b474ecbd3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/cache/zipball/382e7f4db9a12dc6c19431743a2b096041bcdd62", - "reference": "382e7f4db9a12dc6c19431743a2b096041bcdd62", + "url": "https://api.github.com/repos/doctrine/cache/zipball/35a4a70cd94e09e2259dfae7488afc6b474ecbd3", + "reference": "35a4a70cd94e09e2259dfae7488afc6b474ecbd3", "shasum": "" }, "require": { - "php": "~7.1" + "php": "~7.1 || ^8.0" }, "conflict": { "doctrine/common": ">2.2,<2.4" @@ -314,20 +366,34 @@ "redis", "xcache" ], - "time": "2019-11-29T15:36:20+00:00" + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcache", + "type": "tidelift" + } + ], + "time": "2020-05-27T16:24:54+00:00" }, { "name": "doctrine/dbal", - "version": "v2.10.1", + "version": "2.10.2", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "c2b8e6e82732a64ecde1cddf9e1e06cb8556e3d8" + "reference": "aab745e7b6b2de3b47019da81e7225e14dcfdac8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/c2b8e6e82732a64ecde1cddf9e1e06cb8556e3d8", - "reference": "c2b8e6e82732a64ecde1cddf9e1e06cb8556e3d8", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/aab745e7b6b2de3b47019da81e7225e14dcfdac8", + "reference": "aab745e7b6b2de3b47019da81e7225e14dcfdac8", "shasum": "" }, "require": { @@ -339,9 +405,11 @@ "require-dev": { "doctrine/coding-standard": "^6.0", "jetbrains/phpstorm-stubs": "^2019.1", - "phpstan/phpstan": "^0.11.3", + "nikic/php-parser": "^4.4", + "phpstan/phpstan": "^0.12", "phpunit/phpunit": "^8.4.1", - "symfony/console": "^2.0.5|^3.0|^4.0|^5.0" + "symfony/console": "^2.0.5|^3.0|^4.0|^5.0", + "vimeo/psalm": "^3.11" }, "suggest": { "symfony/console": "For helpful console commands such as SQL execution and import of files." @@ -406,7 +474,21 @@ "sqlserver", "sqlsrv" ], - "time": "2020-01-04T12:56:21+00:00" + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal", + "type": "tidelift" + } + ], + "time": "2020-04-20T17:19:26+00:00" }, { "name": "doctrine/event-manager", @@ -486,33 +568,37 @@ }, { "name": "doctrine/inflector", - "version": "1.3.1", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/doctrine/inflector.git", - "reference": "ec3a55242203ffa6a4b27c58176da97ff0a7aec1" + "reference": "9cf661f4eb38f7c881cac67c75ea9b00bf97b210" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/inflector/zipball/ec3a55242203ffa6a4b27c58176da97ff0a7aec1", - "reference": "ec3a55242203ffa6a4b27c58176da97ff0a7aec1", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/9cf661f4eb38f7c881cac67c75ea9b00bf97b210", + "reference": "9cf661f4eb38f7c881cac67c75ea9b00bf97b210", "shasum": "" }, "require": { - "php": "^7.1" + "php": "^7.2 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^6.2" + "doctrine/coding-standard": "^7.0", + "phpstan/phpstan": "^0.11", + "phpstan/phpstan-phpunit": "^0.11", + "phpstan/phpstan-strict-rules": "^0.11", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.3.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { "psr-4": { - "Doctrine\\Common\\Inflector\\": "lib/Doctrine/Common/Inflector" + "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" } }, "notification-url": "https://packagist.org/downloads/", @@ -541,32 +627,52 @@ "email": "schmittjoh@gmail.com" } ], - "description": "Common String Manipulations with regard to casing and singular/plural rules.", - "homepage": "http://www.doctrine-project.org", + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", "keywords": [ "inflection", - "pluralize", - "singularize", - "string" + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" ], - "time": "2019-10-30T19:59:35+00:00" + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2020-05-29T15:13:26+00:00" }, { "name": "doctrine/lexer", - "version": "1.2.0", + "version": "1.2.1", "source": { "type": "git", "url": "https://github.com/doctrine/lexer.git", - "reference": "5242d66dbeb21a30dd8a3e66bf7a73b66e05e1f6" + "reference": "e864bbf5904cb8f5bb334f99209b48018522f042" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/5242d66dbeb21a30dd8a3e66bf7a73b66e05e1f6", - "reference": "5242d66dbeb21a30dd8a3e66bf7a73b66e05e1f6", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/e864bbf5904cb8f5bb334f99209b48018522f042", + "reference": "e864bbf5904cb8f5bb334f99209b48018522f042", "shasum": "" }, "require": { - "php": "^7.2" + "php": "^7.2 || ^8.0" }, "require-dev": { "doctrine/coding-standard": "^6.0", @@ -611,7 +717,21 @@ "parser", "php" ], - "time": "2019-10-30T14:39:59+00:00" + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2020-05-25T17:44:05+00:00" }, { "name": "dragonmantank/cron-expression", @@ -669,16 +789,16 @@ }, { "name": "egulias/email-validator", - "version": "2.1.17", + "version": "2.1.18", "source": { "type": "git", "url": "https://github.com/egulias/EmailValidator.git", - "reference": "ade6887fd9bd74177769645ab5c474824f8a418a" + "reference": "cfa3d44471c7f5bfb684ac2b0da7114283d78441" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/ade6887fd9bd74177769645ab5c474824f8a418a", - "reference": "ade6887fd9bd74177769645ab5c474824f8a418a", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/cfa3d44471c7f5bfb684ac2b0da7114283d78441", + "reference": "cfa3d44471c7f5bfb684ac2b0da7114283d78441", "shasum": "" }, "require": { @@ -702,7 +822,7 @@ }, "autoload": { "psr-4": { - "Egulias\\EmailValidator\\": "EmailValidator" + "Egulias\\EmailValidator\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -723,20 +843,20 @@ "validation", "validator" ], - "time": "2020-02-13T22:36:52+00:00" + "time": "2020-06-16T20:11:17+00:00" }, { "name": "fideloper/proxy", - "version": "4.3.0", + "version": "4.4.0", "source": { "type": "git", "url": "https://github.com/fideloper/TrustedProxy.git", - "reference": "ec38ad69ee378a1eec04fb0e417a97cfaf7ed11a" + "reference": "9beebf48a1c344ed67c1d36bb1b8709db7c3c1a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fideloper/TrustedProxy/zipball/ec38ad69ee378a1eec04fb0e417a97cfaf7ed11a", - "reference": "ec38ad69ee378a1eec04fb0e417a97cfaf7ed11a", + "url": "https://api.github.com/repos/fideloper/TrustedProxy/zipball/9beebf48a1c344ed67c1d36bb1b8709db7c3c1a8", + "reference": "9beebf48a1c344ed67c1d36bb1b8709db7c3c1a8", "shasum": "" }, "require": { @@ -777,27 +897,28 @@ "proxy", "trusted proxy" ], - "time": "2020-02-22T01:51:47+00:00" + "time": "2020-06-23T01:36:47+00:00" }, { "name": "guzzlehttp/guzzle", - "version": "6.5.2", + "version": "6.5.5", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "43ece0e75098b7ecd8d13918293029e555a50f82" + "reference": "9d4290de1cfd701f38099ef7e183b64b4b7b0c5e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/43ece0e75098b7ecd8d13918293029e555a50f82", - "reference": "43ece0e75098b7ecd8d13918293029e555a50f82", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/9d4290de1cfd701f38099ef7e183b64b4b7b0c5e", + "reference": "9d4290de1cfd701f38099ef7e183b64b4b7b0c5e", "shasum": "" }, "require": { "ext-json": "*", "guzzlehttp/promises": "^1.0", "guzzlehttp/psr7": "^1.6.1", - "php": ">=5.5" + "php": ">=5.5", + "symfony/polyfill-intl-idn": "^1.17.0" }, "require-dev": { "ext-curl": "*", @@ -805,7 +926,6 @@ "psr/log": "^1.1" }, "suggest": { - "ext-intl": "Required for Internationalized Domain Name (IDN) support", "psr/log": "Required for using the Log middleware" }, "type": "library", @@ -844,7 +964,7 @@ "rest", "web service" ], - "time": "2019-12-23T11:57:10+00:00" + "time": "2020-06-16T21:01:06+00:00" }, { "name": "guzzlehttp/promises", @@ -1035,96 +1155,6 @@ ], "time": "2019-04-03T13:40:29+00:00" }, - { - "name": "jakub-onderka/php-console-color", - "version": "v0.2", - "source": { - "type": "git", - "url": "https://github.com/JakubOnderka/PHP-Console-Color.git", - "reference": "d5deaecff52a0d61ccb613bb3804088da0307191" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/JakubOnderka/PHP-Console-Color/zipball/d5deaecff52a0d61ccb613bb3804088da0307191", - "reference": "d5deaecff52a0d61ccb613bb3804088da0307191", - "shasum": "" - }, - "require": { - "php": ">=5.4.0" - }, - "require-dev": { - "jakub-onderka/php-code-style": "1.0", - "jakub-onderka/php-parallel-lint": "1.0", - "jakub-onderka/php-var-dump-check": "0.*", - "phpunit/phpunit": "~4.3", - "squizlabs/php_codesniffer": "1.*" - }, - "type": "library", - "autoload": { - "psr-4": { - "JakubOnderka\\PhpConsoleColor\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-2-Clause" - ], - "authors": [ - { - "name": "Jakub Onderka", - "email": "jakub.onderka@gmail.com" - } - ], - "abandoned": "php-parallel-lint/php-console-color", - "time": "2018-09-29T17:23:10+00:00" - }, - { - "name": "jakub-onderka/php-console-highlighter", - "version": "v0.4", - "source": { - "type": "git", - "url": "https://github.com/JakubOnderka/PHP-Console-Highlighter.git", - "reference": "9f7a229a69d52506914b4bc61bfdb199d90c5547" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/JakubOnderka/PHP-Console-Highlighter/zipball/9f7a229a69d52506914b4bc61bfdb199d90c5547", - "reference": "9f7a229a69d52506914b4bc61bfdb199d90c5547", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "jakub-onderka/php-console-color": "~0.2", - "php": ">=5.4.0" - }, - "require-dev": { - "jakub-onderka/php-code-style": "~1.0", - "jakub-onderka/php-parallel-lint": "~1.0", - "jakub-onderka/php-var-dump-check": "~0.1", - "phpunit/phpunit": "~4.0", - "squizlabs/php_codesniffer": "~1.5" - }, - "type": "library", - "autoload": { - "psr-4": { - "JakubOnderka\\PhpConsoleHighlighter\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jakub Onderka", - "email": "acci@acci.cz", - "homepage": "http://www.acci.cz/" - } - ], - "description": "Highlight PHP code in terminal", - "abandoned": "php-parallel-lint/php-console-highlighter", - "time": "2018-09-29T18:48:56+00:00" - }, { "name": "laracasts/utilities", "version": "3.1", @@ -1184,49 +1214,55 @@ }, { "name": "laravel/framework", - "version": "v6.18.3", + "version": "v7.17.1", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "4e48acfaba87f08320a2764d36c3b6a4a4112ccf" + "reference": "6633c4017b311d3aaae842edabd567c637afc39c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/4e48acfaba87f08320a2764d36c3b6a4a4112ccf", - "reference": "4e48acfaba87f08320a2764d36c3b6a4a4112ccf", + "url": "https://api.github.com/repos/laravel/framework/zipball/6633c4017b311d3aaae842edabd567c637afc39c", + "reference": "6633c4017b311d3aaae842edabd567c637afc39c", "shasum": "" }, "require": { - "doctrine/inflector": "^1.1", + "doctrine/inflector": "^1.4|^2.0", "dragonmantank/cron-expression": "^2.0", "egulias/email-validator": "^2.1.10", "ext-json": "*", "ext-mbstring": "*", "ext-openssl": "*", "league/commonmark": "^1.3", - "league/flysystem": "^1.0.8", - "monolog/monolog": "^1.12|^2.0", - "nesbot/carbon": "^2.0", + "league/flysystem": "^1.0.34", + "monolog/monolog": "^2.0", + "nesbot/carbon": "^2.17", "opis/closure": "^3.1", - "php": "^7.2", + "php": "^7.2.5", "psr/container": "^1.0", "psr/simple-cache": "^1.0", - "ramsey/uuid": "^3.7", + "ramsey/uuid": "^3.7|^4.0", "swiftmailer/swiftmailer": "^6.0", - "symfony/console": "^4.3.4", - "symfony/debug": "^4.3.4", - "symfony/finder": "^4.3.4", - "symfony/http-foundation": "^4.3.4", - "symfony/http-kernel": "^4.3.4", - "symfony/process": "^4.3.4", - "symfony/routing": "^4.3.4", - "symfony/var-dumper": "^4.3.4", - "tijsverkoyen/css-to-inline-styles": "^2.2.1", - "vlucas/phpdotenv": "^3.3" + "symfony/console": "^5.0", + "symfony/error-handler": "^5.0", + "symfony/finder": "^5.0", + "symfony/http-foundation": "^5.0", + "symfony/http-kernel": "^5.0", + "symfony/mime": "^5.0", + "symfony/polyfill-php73": "^1.17", + "symfony/process": "^5.0", + "symfony/routing": "^5.0", + "symfony/var-dumper": "^5.0", + "tijsverkoyen/css-to-inline-styles": "^2.2.2", + "vlucas/phpdotenv": "^4.0", + "voku/portable-ascii": "^1.4.8" }, "conflict": { "tightenco/collect": "<5.5.33" }, + "provide": { + "psr/container-implementation": "1.0" + }, "replace": { "illuminate/auth": "self.version", "illuminate/broadcasting": "self.version", @@ -1253,6 +1289,7 @@ "illuminate/routing": "self.version", "illuminate/session": "self.version", "illuminate/support": "self.version", + "illuminate/testing": "self.version", "illuminate/translation": "self.version", "illuminate/validation": "self.version", "illuminate/view": "self.version" @@ -1261,19 +1298,20 @@ "aws/aws-sdk-php": "^3.0", "doctrine/dbal": "^2.6", "filp/whoops": "^2.4", - "guzzlehttp/guzzle": "^6.3|^7.0", + "guzzlehttp/guzzle": "^6.3.1|^7.0", "league/flysystem-cached-adapter": "^1.0", "mockery/mockery": "^1.3.1", "moontoast/math": "^1.1", - "orchestra/testbench-core": "^4.0", + "orchestra/testbench-core": "^5.0", "pda/pheanstalk": "^4.0", - "phpunit/phpunit": "^7.5.15|^8.4|^9.0", + "phpunit/phpunit": "^8.4|^9.0", "predis/predis": "^1.1.1", - "symfony/cache": "^4.3.4" + "symfony/cache": "^5.0" }, "suggest": { "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage and SES mail driver (^3.0).", "doctrine/dbal": "Required to rename columns and drop SQLite columns (^2.6).", + "ext-ftp": "Required to use the Flysystem FTP driver.", "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().", "ext-memcached": "Required to use the memcache cache driver.", "ext-pcntl": "Required to use all features of the queue worker.", @@ -1281,24 +1319,27 @@ "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0).", "filp/whoops": "Required for friendly error pages in development (^2.4).", "fzaninotto/faker": "Required to use the eloquent factory builder (^1.9.1).", - "guzzlehttp/guzzle": "Required to use the Mailgun mail driver and the ping methods on schedules (^6.0|^7.0).", + "guzzlehttp/guzzle": "Required to use the HTTP Client, Mailgun mail driver and the ping methods on schedules (^6.3.1|^7.0).", "laravel/tinker": "Required to use the tinker console command (^2.0).", "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^1.0).", "league/flysystem-cached-adapter": "Required to use the Flysystem cache (^1.0).", "league/flysystem-sftp": "Required to use the Flysystem SFTP driver (^1.0).", + "mockery/mockery": "Required to use mocking (^1.3.1).", "moontoast/math": "Required to use ordered UUIDs (^1.1).", "nyholm/psr7": "Required to use PSR-7 bridging features (^1.2).", "pda/pheanstalk": "Required to use the beanstalk queue driver (^4.0).", + "phpunit/phpunit": "Required to use assertions and run tests (^8.4|^9.0).", "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^4.0).", - "symfony/cache": "Required to PSR-6 cache bridge (^4.3.4).", - "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^1.2).", + "symfony/cache": "Required to PSR-6 cache bridge (^5.0).", + "symfony/filesystem": "Required to create relative storage directory symbolic links (^5.0).", + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^2.0).", "wildbit/swiftmailer-postmark": "Required to use Postmark mail driver (^3.0)." }, "type": "library", "extra": { "branch-alias": { - "dev-master": "6.x-dev" + "dev-master": "7.x-dev" } }, "autoload": { @@ -1326,7 +1367,7 @@ "framework", "laravel" ], - "time": "2020-03-24T16:37:50+00:00" + "time": "2020-06-23T15:22:07+00:00" }, { "name": "laravel/helpers", @@ -1383,36 +1424,37 @@ }, { "name": "laravel/tinker", - "version": "v1.0.10", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/laravel/tinker.git", - "reference": "ad571aacbac1539c30d480908f9d0c9614eaf1a7" + "reference": "cde90a7335a2130a4488beb68f4b2141869241db" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/tinker/zipball/ad571aacbac1539c30d480908f9d0c9614eaf1a7", - "reference": "ad571aacbac1539c30d480908f9d0c9614eaf1a7", + "url": "https://api.github.com/repos/laravel/tinker/zipball/cde90a7335a2130a4488beb68f4b2141869241db", + "reference": "cde90a7335a2130a4488beb68f4b2141869241db", "shasum": "" }, "require": { - "illuminate/console": "~5.1|^6.0", - "illuminate/contracts": "~5.1|^6.0", - "illuminate/support": "~5.1|^6.0", - "php": ">=5.5.9", - "psy/psysh": "0.7.*|0.8.*|0.9.*", - "symfony/var-dumper": "~3.0|~4.0" + "illuminate/console": "^6.0|^7.0|^8.0", + "illuminate/contracts": "^6.0|^7.0|^8.0", + "illuminate/support": "^6.0|^7.0|^8.0", + "php": "^7.2", + "psy/psysh": "^0.10.3", + "symfony/var-dumper": "^4.3|^5.0" }, "require-dev": { - "phpunit/phpunit": "~4.0|~5.0" + "mockery/mockery": "^1.3.1", + "phpunit/phpunit": "^8.4|^9.0" }, "suggest": { - "illuminate/database": "The Illuminate Database package (~5.1)." + "illuminate/database": "The Illuminate Database package (^6.0|^7.0|^8.0)." }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "2.x-dev" }, "laravel": { "providers": [ @@ -1442,20 +1484,75 @@ "laravel", "psysh" ], - "time": "2019-08-07T15:10:45+00:00" + "time": "2020-04-07T15:01:31+00:00" }, { - "name": "lcobucci/jwt", - "version": "3.3.1", + "name": "laravel/ui", + "version": "v2.0.3", "source": { "type": "git", - "url": "https://github.com/lcobucci/jwt.git", - "reference": "a11ec5f4b4d75d1fcd04e133dede4c317aac9e18" + "url": "https://github.com/laravel/ui.git", + "reference": "15368c5328efb7ce94f35ca750acde9b496ab1b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lcobucci/jwt/zipball/a11ec5f4b4d75d1fcd04e133dede4c317aac9e18", - "reference": "a11ec5f4b4d75d1fcd04e133dede4c317aac9e18", + "url": "https://api.github.com/repos/laravel/ui/zipball/15368c5328efb7ce94f35ca750acde9b496ab1b1", + "reference": "15368c5328efb7ce94f35ca750acde9b496ab1b1", + "shasum": "" + }, + "require": { + "illuminate/console": "^7.0", + "illuminate/filesystem": "^7.0", + "illuminate/support": "^7.0", + "php": "^7.2.5" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^8.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Ui\\UiServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Ui\\": "src/", + "Illuminate\\Foundation\\Auth\\": "auth-backend/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel UI utilities and presets.", + "keywords": [ + "laravel", + "ui" + ], + "time": "2020-04-29T15:06:45+00:00" + }, + { + "name": "lcobucci/jwt", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/jwt.git", + "reference": "56f10808089e38623345e28af2f2d5e4eb579455" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/56f10808089e38623345e28af2f2d5e4eb579455", + "reference": "56f10808089e38623345e28af2f2d5e4eb579455", "shasum": "" }, "require": { @@ -1497,25 +1594,35 @@ "JWS", "jwt" ], - "time": "2019-05-24T18:30:49+00:00" + "funding": [ + { + "url": "https://github.com/lcobucci", + "type": "github" + }, + { + "url": "https://www.patreon.com/lcobucci", + "type": "patreon" + } + ], + "time": "2020-05-22T08:21:12+00:00" }, { "name": "league/commonmark", - "version": "1.3.2", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "75542a366ccbe1896ed79fcf3e8e68206d6c4257" + "reference": "fc33ca12575e98e57cdce7d5f38b2ca5335714b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/75542a366ccbe1896ed79fcf3e8e68206d6c4257", - "reference": "75542a366ccbe1896ed79fcf3e8e68206d6c4257", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/fc33ca12575e98e57cdce7d5f38b2ca5335714b3", + "reference": "fc33ca12575e98e57cdce7d5f38b2ca5335714b3", "shasum": "" }, "require": { "ext-mbstring": "*", - "php": "^7.1" + "php": "^7.1 || ^8.0" }, "conflict": { "scrutinizer/ocular": "1.7.*" @@ -1528,7 +1635,7 @@ "github/gfm": "0.29.0", "michelf/php-markdown": "~1.4", "mikehaertl/php-shellcommand": "^1.4", - "phpstan/phpstan-shim": "^0.11.5", + "phpstan/phpstan": "^0.12", "phpunit/phpunit": "^7.5", "scrutinizer/ocular": "^1.5", "symfony/finder": "^4.2" @@ -1537,11 +1644,6 @@ "bin/commonmark" ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4-dev" - } - }, "autoload": { "psr-4": { "League\\CommonMark\\": "src" @@ -1571,20 +1673,46 @@ "md", "parser" ], - "time": "2020-03-25T19:55:28+00:00" + "funding": [ + { + "url": "https://enjoy.gitstore.app/repositories/thephpleague/commonmark", + "type": "custom" + }, + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://www.patreon.com/colinodell", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/commonmark", + "type": "tidelift" + } + ], + "time": "2020-06-21T20:50:13+00:00" }, { "name": "league/flysystem", - "version": "1.0.66", + "version": "1.0.69", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "021569195e15f8209b1c4bebb78bd66aa4f08c21" + "reference": "7106f78428a344bc4f643c233a94e48795f10967" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/021569195e15f8209b1c4bebb78bd66aa4f08c21", - "reference": "021569195e15f8209b1c4bebb78bd66aa4f08c21", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/7106f78428a344bc4f643c233a94e48795f10967", + "reference": "7106f78428a344bc4f643c233a94e48795f10967", "shasum": "" }, "require": { @@ -1655,20 +1783,26 @@ "sftp", "storage" ], - "time": "2020-03-17T18:58:12+00:00" + "funding": [ + { + "url": "https://offset.earth/frankdejonge", + "type": "other" + } + ], + "time": "2020-05-18T15:13:39+00:00" }, { "name": "league/flysystem-aws-s3-v3", - "version": "1.0.24", + "version": "1.0.25", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git", - "reference": "4382036bde5dc926f9b8b337e5bdb15e5ec7b570" + "reference": "d409b97a50bf85fbde30cbc9fc10237475e696ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/4382036bde5dc926f9b8b337e5bdb15e5ec7b570", - "reference": "4382036bde5dc926f9b8b337e5bdb15e5ec7b570", + "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/d409b97a50bf85fbde30cbc9fc10237475e696ea", + "reference": "d409b97a50bf85fbde30cbc9fc10237475e696ea", "shasum": "" }, "require": { @@ -1702,7 +1836,7 @@ } ], "description": "Flysystem adapter for the AWS S3 SDK v3.x", - "time": "2020-02-23T13:31:58+00:00" + "time": "2020-06-02T18:41:58+00:00" }, { "name": "league/flysystem-memory", @@ -1865,20 +1999,20 @@ }, { "name": "monolog/monolog", - "version": "2.0.2", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "c861fcba2ca29404dc9e617eedd9eff4616986b8" + "reference": "38914429aac460e8e4616c8cb486ecb40ec90bb1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/c861fcba2ca29404dc9e617eedd9eff4616986b8", - "reference": "c861fcba2ca29404dc9e617eedd9eff4616986b8", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/38914429aac460e8e4616c8cb486ecb40ec90bb1", + "reference": "38914429aac460e8e4616c8cb486ecb40ec90bb1", "shasum": "" }, "require": { - "php": "^7.2", + "php": ">=7.2", "psr/log": "^1.0.1" }, "provide": { @@ -1889,11 +2023,11 @@ "doctrine/couchdb": "~1.0@dev", "elasticsearch/elasticsearch": "^6.0", "graylog2/gelf-php": "^1.4.2", - "jakub-onderka/php-parallel-lint": "^0.9", "php-amqplib/php-amqplib": "~2.4", "php-console/php-console": "^3.1.3", + "php-parallel-lint/php-parallel-lint": "^1.0", "phpspec/prophecy": "^1.6.1", - "phpunit/phpunit": "^8.3", + "phpunit/phpunit": "^8.5", "predis/predis": "^1.1", "rollbar/rollbar": "^1.3", "ruflin/elastica": ">=0.90 <3.0", @@ -1942,7 +2076,17 @@ "logging", "psr-3" ], - "time": "2019-12-20T14:22:59+00:00" + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2020-05-22T08:12:19+00:00" }, { "name": "mtdowling/jmespath.php", @@ -2003,21 +2147,22 @@ }, { "name": "nesbot/carbon", - "version": "2.32.2", + "version": "2.35.0", "source": { "type": "git", "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "f10e22cf546704fab1db4ad4b9dedbc5c797a0dc" + "reference": "4b9bd835261ef23d36397a46a76b496a458305e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/f10e22cf546704fab1db4ad4b9dedbc5c797a0dc", - "reference": "f10e22cf546704fab1db4ad4b9dedbc5c797a0dc", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/4b9bd835261ef23d36397a46a76b496a458305e5", + "reference": "4b9bd835261ef23d36397a46a76b496a458305e5", "shasum": "" }, "require": { "ext-json": "*", "php": "^7.1.8 || ^8.0", + "symfony/polyfill-mbstring": "^1.0", "symfony/translation": "^3.4 || ^4.0 || ^5.0" }, "require-dev": { @@ -2035,7 +2180,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.x-dev" + "dev-master": "2.x-dev", + "dev-3.x": "3.x-dev" }, "laravel": { "providers": [ @@ -2070,20 +2216,30 @@ "datetime", "time" ], - "time": "2020-03-31T13:43:19+00:00" + "funding": [ + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2020-05-24T18:27:52+00:00" }, { "name": "nikic/php-parser", - "version": "v4.3.0", + "version": "v4.5.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "9a9981c347c5c49d6dfe5cf826bb882b824080dc" + "reference": "53c2753d756f5adb586dca79c2ec0e2654dd9463" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/9a9981c347c5c49d6dfe5cf826bb882b824080dc", - "reference": "9a9981c347c5c49d6dfe5cf826bb882b824080dc", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/53c2753d756f5adb586dca79c2ec0e2654dd9463", + "reference": "53c2753d756f5adb586dca79c2ec0e2654dd9463", "shasum": "" }, "require": { @@ -2122,20 +2278,20 @@ "parser", "php" ], - "time": "2019-11-08T13:50:10+00:00" + "time": "2020-06-03T07:24:19+00:00" }, { "name": "opis/closure", - "version": "3.5.1", + "version": "3.5.5", "source": { "type": "git", "url": "https://github.com/opis/closure.git", - "reference": "93ebc5712cdad8d5f489b500c59d122df2e53969" + "reference": "dec9fc5ecfca93f45cd6121f8e6f14457dff372c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opis/closure/zipball/93ebc5712cdad8d5f489b500c59d122df2e53969", - "reference": "93ebc5712cdad8d5f489b500c59d122df2e53969", + "url": "https://api.github.com/repos/opis/closure/zipball/dec9fc5ecfca93f45cd6121f8e6f14457dff372c", + "reference": "dec9fc5ecfca93f45cd6121f8e6f14457dff372c", "shasum": "" }, "require": { @@ -2183,7 +2339,7 @@ "serialization", "serialize" ], - "time": "2019-11-29T22:36:02+00:00" + "time": "2020-06-17T14:59:55+00:00" }, { "name": "paragonie/constant_time_encoding", @@ -2294,16 +2450,16 @@ }, { "name": "phpoption/phpoption", - "version": "1.7.3", + "version": "1.7.4", "source": { "type": "git", "url": "https://github.com/schmittjoh/php-option.git", - "reference": "4acfd6a4b33a509d8c88f50e5222f734b6aeebae" + "reference": "b2ada2ad5d8a32b89088b8adc31ecd2e3a13baf3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/4acfd6a4b33a509d8c88f50e5222f734b6aeebae", - "reference": "4acfd6a4b33a509d8c88f50e5222f734b6aeebae", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/b2ada2ad5d8a32b89088b8adc31ecd2e3a13baf3", + "reference": "b2ada2ad5d8a32b89088b8adc31ecd2e3a13baf3", "shasum": "" }, "require": { @@ -2345,7 +2501,17 @@ "php", "type" ], - "time": "2020-03-21T18:07:53+00:00" + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2020-06-07T10:40:07+00:00" }, { "name": "pragmarx/google2fa", @@ -2455,16 +2621,16 @@ }, { "name": "prologue/alerts", - "version": "0.4.6", + "version": "0.4.7", "source": { "type": "git", "url": "https://github.com/prologuephp/alerts.git", - "reference": "4c621a541a7f16631deda9d1b2b075c182d251d1" + "reference": "631896b583129b2873df09b5295809c1244eddb1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/prologuephp/alerts/zipball/4c621a541a7f16631deda9d1b2b075c182d251d1", - "reference": "4c621a541a7f16631deda9d1b2b075c182d251d1", + "url": "https://api.github.com/repos/prologuephp/alerts/zipball/631896b583129b2873df09b5295809c1244eddb1", + "reference": "631896b583129b2873df09b5295809c1244eddb1", "shasum": "" }, "require": { @@ -2517,7 +2683,7 @@ "laravel", "messages" ], - "time": "2020-03-03T08:33:38+00:00" + "time": "2020-04-24T06:00:16+00:00" }, { "name": "psr/container", @@ -2568,6 +2734,52 @@ ], "time": "2017-02-14T16:28:37+00:00" }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "time": "2019-01-08T18:20:26+00:00" + }, { "name": "psr/http-message", "version": "1.0.1", @@ -2715,32 +2927,30 @@ }, { "name": "psy/psysh", - "version": "v0.9.12", + "version": "v0.10.4", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "90da7f37568aee36b116a030c5f99c915267edd4" + "reference": "a8aec1b2981ab66882a01cce36a49b6317dc3560" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/90da7f37568aee36b116a030c5f99c915267edd4", - "reference": "90da7f37568aee36b116a030c5f99c915267edd4", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/a8aec1b2981ab66882a01cce36a49b6317dc3560", + "reference": "a8aec1b2981ab66882a01cce36a49b6317dc3560", "shasum": "" }, "require": { "dnoegel/php-xdg-base-dir": "0.1.*", "ext-json": "*", "ext-tokenizer": "*", - "jakub-onderka/php-console-highlighter": "0.3.*|0.4.*", - "nikic/php-parser": "~1.3|~2.0|~3.0|~4.0", - "php": ">=5.4.0", - "symfony/console": "~2.3.10|^2.4.2|~3.0|~4.0|~5.0", - "symfony/var-dumper": "~2.7|~3.0|~4.0|~5.0" + "nikic/php-parser": "~4.0|~3.0|~2.0|~1.3", + "php": "^8.0 || ^7.0 || ^5.5.9", + "symfony/console": "~5.0|~4.0|~3.0|^2.4.2|~2.3.10", + "symfony/var-dumper": "~5.0|~4.0|~3.0|~2.7" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.2", - "hoa/console": "~2.15|~3.16", - "phpunit/phpunit": "~4.8.35|~5.0|~6.0|~7.0" + "hoa/console": "3.17.*" }, "suggest": { "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", @@ -2755,7 +2965,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-develop": "0.9.x-dev" + "dev-master": "0.10.x-dev" } }, "autoload": { @@ -2785,7 +2995,7 @@ "interactive", "shell" ], - "time": "2019-12-06T14:19:43+00:00" + "time": "2020-05-03T19:32:03+00:00" }, { "name": "ralouphie/getallheaders", @@ -2828,54 +3038,124 @@ "time": "2019-03-08T08:55:37+00:00" }, { - "name": "ramsey/uuid", - "version": "3.9.3", + "name": "ramsey/collection", + "version": "1.0.1", "source": { "type": "git", - "url": "https://github.com/ramsey/uuid.git", - "reference": "7e1633a6964b48589b142d60542f9ed31bd37a92" + "url": "https://github.com/ramsey/collection.git", + "reference": "925ad8cf55ba7a3fc92e332c58fd0478ace3e1ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/7e1633a6964b48589b142d60542f9ed31bd37a92", - "reference": "7e1633a6964b48589b142d60542f9ed31bd37a92", + "url": "https://api.github.com/repos/ramsey/collection/zipball/925ad8cf55ba7a3fc92e332c58fd0478ace3e1ca", + "reference": "925ad8cf55ba7a3fc92e332c58fd0478ace3e1ca", "shasum": "" }, "require": { + "php": "^7.2" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0", + "fzaninotto/faker": "^1.5", + "jakub-onderka/php-parallel-lint": "^1", + "jangregor/phpstan-prophecy": "^0.6", + "mockery/mockery": "^1.3", + "phpstan/extension-installer": "^1", + "phpstan/phpdoc-parser": "0.4.1", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-mockery": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpunit/phpunit": "^8.5", + "slevomat/coding-standard": "^6.0", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP 7.2+ library for representing and manipulating collections.", + "homepage": "https://github.com/ramsey/collection", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "time": "2020-01-05T00:22:59+00:00" + }, + { + "name": "ramsey/uuid", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "ba8fff1d3abb8bb4d35a135ed22a31c6ef3ede3d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/ba8fff1d3abb8bb4d35a135ed22a31c6ef3ede3d", + "reference": "ba8fff1d3abb8bb4d35a135ed22a31c6ef3ede3d", + "shasum": "" + }, + "require": { + "brick/math": "^0.8", "ext-json": "*", - "paragonie/random_compat": "^1 | ^2 | 9.99.99", - "php": "^5.4 | ^7 | ^8", + "php": "^7.2 || ^8", + "ramsey/collection": "^1.0", "symfony/polyfill-ctype": "^1.8" }, "replace": { "rhumsaa/uuid": "self.version" }, "require-dev": { - "codeception/aspect-mock": "^1 | ^2", - "doctrine/annotations": "^1.2", - "goaop/framework": "1.0.0-alpha.2 | ^1 | ^2.1", - "jakub-onderka/php-parallel-lint": "^1", - "mockery/mockery": "^0.9.11 | ^1", + "codeception/aspect-mock": "^3", + "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2", + "doctrine/annotations": "^1.8", + "goaop/framework": "^2", + "mockery/mockery": "^1.3", "moontoast/math": "^1.1", "paragonie/random-lib": "^2", - "php-mock/php-mock-phpunit": "^0.3 | ^1.1", - "phpunit/phpunit": "^4.8 | ^5.4 | ^6.5", - "squizlabs/php_codesniffer": "^3.5" + "php-mock/php-mock-mockery": "^1.3", + "php-mock/php-mock-phpunit": "^2.5", + "php-parallel-lint/php-parallel-lint": "^1.1", + "phpstan/extension-installer": "^1.0", + "phpstan/phpdoc-parser": "0.4.3", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-mockery": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpunit/phpunit": "^8.5", + "psy/psysh": "^0.10.0", + "slevomat/coding-standard": "^6.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "3.9.4" }, "suggest": { - "ext-ctype": "Provides support for PHP Ctype functions", - "ext-libsodium": "Provides the PECL libsodium extension for use with the SodiumRandomGenerator", - "ext-openssl": "Provides the OpenSSL extension for use with the OpenSslGenerator", - "ext-uuid": "Provides the PECL UUID extension for use with the PeclUuidTimeGenerator and PeclUuidRandomGenerator", - "moontoast/math": "Provides support for converting UUID to 128-bit integer (in string form).", + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-ctype": "Enables faster processing of character classification using ctype functions.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", - "ramsey/uuid-console": "A console application for generating UUIDs with ramsey/uuid", "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.x-dev" + "dev-master": "4.x-dev" } }, "autoload": { @@ -2890,29 +3170,20 @@ "license": [ "MIT" ], - "authors": [ - { - "name": "Ben Ramsey", - "email": "ben@benramsey.com", - "homepage": "https://benramsey.com" - }, - { - "name": "Marijn Huizendveld", - "email": "marijn.huizendveld@gmail.com" - }, - { - "name": "Thibaud Fabre", - "email": "thibaud@aztech.io" - } - ], - "description": "Formerly rhumsaa/uuid. A PHP 5.4+ library for generating RFC 4122 version 1, 3, 4, and 5 universally unique identifiers (UUID).", + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", "homepage": "https://github.com/ramsey/uuid", "keywords": [ "guid", "identifier", "uuid" ], - "time": "2020-02-21T04:36:14+00:00" + "funding": [ + { + "url": "https://github.com/ramsey", + "type": "github" + } + ], + "time": "2020-03-29T20:13:32+00:00" }, { "name": "s1lentium/iptools", @@ -3082,28 +3353,34 @@ "spatie", "transform" ], + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + } + ], "time": "2020-03-02T18:40:49+00:00" }, { "name": "staudenmeir/belongs-to-through", - "version": "v2.9", + "version": "v2.10", "source": { "type": "git", "url": "https://github.com/staudenmeir/belongs-to-through.git", - "reference": "8f16bb7b51d081d90d9b093ba6f380f71a96d79f" + "reference": "23be043a4885f696a0e5eb24da7221947e480cb5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/staudenmeir/belongs-to-through/zipball/8f16bb7b51d081d90d9b093ba6f380f71a96d79f", - "reference": "8f16bb7b51d081d90d9b093ba6f380f71a96d79f", + "url": "https://api.github.com/repos/staudenmeir/belongs-to-through/zipball/23be043a4885f696a0e5eb24da7221947e480cb5", + "reference": "23be043a4885f696a0e5eb24da7221947e480cb5", "shasum": "" }, "require": { - "illuminate/database": "^6.0", - "php": "^7.2" + "illuminate/database": "^7.0", + "php": "^7.2.5" }, "require-dev": { - "phpunit/phpunit": "^8.0" + "phpunit/phpunit": "^8.5" }, "type": "library", "autoload": { @@ -3125,8 +3402,8 @@ "email": "mail@jonas-staudenmeir.de" } ], - "description": "Laravel Eloquent BelongsToThrough relationship", - "time": "2019-12-29T10:58:12+00:00" + "description": "Laravel Eloquent BelongsToThrough relationships", + "time": "2020-01-31T11:29:47+00:00" }, { "name": "swiftmailer/swiftmailer", @@ -3192,41 +3469,44 @@ }, { "name": "symfony/console", - "version": "v4.4.7", + "version": "v5.1.2", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "10bb3ee3c97308869d53b3e3d03f6ac23ff985f7" + "reference": "34ac555a3627e324b660e318daa07572e1140123" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/10bb3ee3c97308869d53b3e3d03f6ac23ff985f7", - "reference": "10bb3ee3c97308869d53b3e3d03f6ac23ff985f7", + "url": "https://api.github.com/repos/symfony/console/zipball/34ac555a3627e324b660e318daa07572e1140123", + "reference": "34ac555a3627e324b660e318daa07572e1140123", "shasum": "" }, "require": { - "php": "^7.1.3", + "php": ">=7.2.5", "symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-php73": "^1.8", - "symfony/service-contracts": "^1.1|^2" + "symfony/polyfill-php80": "^1.15", + "symfony/service-contracts": "^1.1|^2", + "symfony/string": "^5.1" }, "conflict": { - "symfony/dependency-injection": "<3.4", - "symfony/event-dispatcher": "<4.3|>=5", + "symfony/dependency-injection": "<4.4", + "symfony/dotenv": "<5.1", + "symfony/event-dispatcher": "<4.4", "symfony/lock": "<4.4", - "symfony/process": "<3.3" + "symfony/process": "<4.4" }, "provide": { "psr/log-implementation": "1.0" }, "require-dev": { "psr/log": "~1.0", - "symfony/config": "^3.4|^4.0|^5.0", - "symfony/dependency-injection": "^3.4|^4.0|^5.0", - "symfony/event-dispatcher": "^4.3", + "symfony/config": "^4.4|^5.0", + "symfony/dependency-injection": "^4.4|^5.0", + "symfony/event-dispatcher": "^4.4|^5.0", "symfony/lock": "^4.4|^5.0", - "symfony/process": "^3.4|^4.0|^5.0", - "symfony/var-dumper": "^4.3|^5.0" + "symfony/process": "^4.4|^5.0", + "symfony/var-dumper": "^4.4|^5.0" }, "suggest": { "psr/log": "For using the console logger", @@ -3237,7 +3517,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.4-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -3264,29 +3544,43 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2020-03-30T11:41:10+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-06-15T12:59:21+00:00" }, { "name": "symfony/css-selector", - "version": "v5.0.7", + "version": "v5.1.2", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "5f8d5271303dad260692ba73dfa21777d38e124e" + "reference": "e544e24472d4c97b2d11ade7caacd446727c6bf9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/5f8d5271303dad260692ba73dfa21777d38e124e", - "reference": "5f8d5271303dad260692ba73dfa21777d38e124e", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/e544e24472d4c97b2d11ade7caacd446727c6bf9", + "reference": "e544e24472d4c97b2d11ade7caacd446727c6bf9", "shasum": "" }, "require": { - "php": "^7.2.5" + "php": ">=7.2.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -3317,44 +3611,48 @@ ], "description": "Symfony CssSelector Component", "homepage": "https://symfony.com", - "time": "2020-03-27T16:56:45+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-05-20T17:43:50+00:00" }, { - "name": "symfony/debug", - "version": "v4.4.7", + "name": "symfony/deprecation-contracts", + "version": "v2.1.2", "source": { "type": "git", - "url": "https://github.com/symfony/debug.git", - "reference": "346636d2cae417992ecfd761979b2ab98b339a45" + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "dd99cb3a0aff6cadd2a8d7d7ed72c2161e218337" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/346636d2cae417992ecfd761979b2ab98b339a45", - "reference": "346636d2cae417992ecfd761979b2ab98b339a45", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/dd99cb3a0aff6cadd2a8d7d7ed72c2161e218337", + "reference": "dd99cb3a0aff6cadd2a8d7d7ed72c2161e218337", "shasum": "" }, "require": { - "php": "^7.1.3", - "psr/log": "~1.0" - }, - "conflict": { - "symfony/http-kernel": "<3.4" - }, - "require-dev": { - "symfony/http-kernel": "^3.4|^4.0|^5.0" + "php": ">=7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.4-dev" + "dev-master": "2.1-dev" } }, "autoload": { - "psr-4": { - "Symfony\\Component\\Debug\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" + "files": [ + "function.php" ] }, "notification-url": "https://packagist.org/downloads/", @@ -3363,46 +3661,61 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Debug Component", + "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", - "time": "2020-03-27T16:54:36+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-05-27T08:34:37+00:00" }, { "name": "symfony/error-handler", - "version": "v4.4.7", + "version": "v5.1.2", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "7e9828fc98aa1cf27b422fe478a84f5b0abb7358" + "reference": "7d0b927b9d3dc41d7d46cda38cbfcd20cdcbb896" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/7e9828fc98aa1cf27b422fe478a84f5b0abb7358", - "reference": "7e9828fc98aa1cf27b422fe478a84f5b0abb7358", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/7d0b927b9d3dc41d7d46cda38cbfcd20cdcbb896", + "reference": "7d0b927b9d3dc41d7d46cda38cbfcd20cdcbb896", "shasum": "" }, "require": { - "php": "^7.1.3", - "psr/log": "~1.0", - "symfony/debug": "^4.4.5", + "php": ">=7.2.5", + "psr/log": "^1.0", + "symfony/polyfill-php80": "^1.15", "symfony/var-dumper": "^4.4|^5.0" }, "require-dev": { + "symfony/deprecation-contracts": "^2.1", "symfony/http-kernel": "^4.4|^5.0", "symfony/serializer": "^4.4|^5.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.4-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -3429,41 +3742,57 @@ ], "description": "Symfony ErrorHandler Component", "homepage": "https://symfony.com", - "time": "2020-03-30T14:07:33+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-05-30T20:35:19+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v4.4.7", + "version": "v5.1.2", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "abc8e3618bfdb55e44c8c6a00abd333f831bbfed" + "reference": "cc0d059e2e997e79ca34125a52f3e33de4424ac7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/abc8e3618bfdb55e44c8c6a00abd333f831bbfed", - "reference": "abc8e3618bfdb55e44c8c6a00abd333f831bbfed", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/cc0d059e2e997e79ca34125a52f3e33de4424ac7", + "reference": "cc0d059e2e997e79ca34125a52f3e33de4424ac7", "shasum": "" }, "require": { - "php": "^7.1.3", - "symfony/event-dispatcher-contracts": "^1.1" + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1", + "symfony/event-dispatcher-contracts": "^2", + "symfony/polyfill-php80": "^1.15" }, "conflict": { - "symfony/dependency-injection": "<3.4" + "symfony/dependency-injection": "<4.4" }, "provide": { "psr/event-dispatcher-implementation": "1.0", - "symfony/event-dispatcher-implementation": "1.1" + "symfony/event-dispatcher-implementation": "2.0" }, "require-dev": { "psr/log": "~1.0", - "symfony/config": "^3.4|^4.0|^5.0", - "symfony/dependency-injection": "^3.4|^4.0|^5.0", - "symfony/expression-language": "^3.4|^4.0|^5.0", - "symfony/http-foundation": "^3.4|^4.0|^5.0", + "symfony/config": "^4.4|^5.0", + "symfony/dependency-injection": "^4.4|^5.0", + "symfony/expression-language": "^4.4|^5.0", + "symfony/http-foundation": "^4.4|^5.0", "symfony/service-contracts": "^1.1|^2", - "symfony/stopwatch": "^3.4|^4.0|^5.0" + "symfony/stopwatch": "^4.4|^5.0" }, "suggest": { "symfony/dependency-injection": "", @@ -3472,7 +3801,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.4-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -3499,33 +3828,47 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2020-03-27T16:54:36+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-05-20T17:43:50+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v1.1.7", + "version": "v2.1.2", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "c43ab685673fb6c8d84220c77897b1d6cdbe1d18" + "reference": "405952c4e90941a17e52ef7489a2bd94870bb290" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c43ab685673fb6c8d84220c77897b1d6cdbe1d18", - "reference": "c43ab685673fb6c8d84220c77897b1d6cdbe1d18", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/405952c4e90941a17e52ef7489a2bd94870bb290", + "reference": "405952c4e90941a17e52ef7489a2bd94870bb290", "shasum": "" }, "require": { - "php": "^7.1.3" + "php": ">=7.2.5", + "psr/event-dispatcher": "^1" }, "suggest": { - "psr/event-dispatcher": "", "symfony/event-dispatcher-implementation": "" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1-dev" + "dev-master": "2.1-dev" } }, "autoload": { @@ -3557,29 +3900,43 @@ "interoperability", "standards" ], - "time": "2019-09-17T09:54:03+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-05-20T17:43:50+00:00" }, { "name": "symfony/finder", - "version": "v4.4.7", + "version": "v5.1.2", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "5729f943f9854c5781984ed4907bbb817735776b" + "reference": "4298870062bfc667cb78d2b379be4bf5dec5f187" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/5729f943f9854c5781984ed4907bbb817735776b", - "reference": "5729f943f9854c5781984ed4907bbb817735776b", + "url": "https://api.github.com/repos/symfony/finder/zipball/4298870062bfc667cb78d2b379be4bf5dec5f187", + "reference": "4298870062bfc667cb78d2b379be4bf5dec5f187", "shasum": "" }, "require": { - "php": "^7.1.3" + "php": ">=7.2.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.4-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -3606,35 +3963,55 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2020-03-27T16:54:36+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-05-20T17:43:50+00:00" }, { "name": "symfony/http-foundation", - "version": "v4.4.7", + "version": "v5.1.2", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "62f92509c9abfd1f73e17b8cf1b72c0bdac6611b" + "reference": "f93055171b847915225bd5b0a5792888419d8d75" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/62f92509c9abfd1f73e17b8cf1b72c0bdac6611b", - "reference": "62f92509c9abfd1f73e17b8cf1b72c0bdac6611b", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f93055171b847915225bd5b0a5792888419d8d75", + "reference": "f93055171b847915225bd5b0a5792888419d8d75", "shasum": "" }, "require": { - "php": "^7.1.3", - "symfony/mime": "^4.3|^5.0", - "symfony/polyfill-mbstring": "~1.1" + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php80": "^1.15" }, "require-dev": { "predis/predis": "~1.0", - "symfony/expression-language": "^3.4|^4.0|^5.0" + "symfony/cache": "^4.4|^5.0", + "symfony/expression-language": "^4.4|^5.0", + "symfony/mime": "^4.4|^5.0" + }, + "suggest": { + "symfony/mime": "To use the file extension guesser" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.4-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -3661,59 +4038,82 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2020-03-30T14:07:33+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-06-15T06:52:54+00:00" }, { "name": "symfony/http-kernel", - "version": "v4.4.7", + "version": "v5.1.2", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "f356a489e51856b99908005eb7f2c51a1dfc95dc" + "reference": "a18c27ace1ef344ffcb129a5b089bad7643b387a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/f356a489e51856b99908005eb7f2c51a1dfc95dc", - "reference": "f356a489e51856b99908005eb7f2c51a1dfc95dc", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/a18c27ace1ef344ffcb129a5b089bad7643b387a", + "reference": "a18c27ace1ef344ffcb129a5b089bad7643b387a", "shasum": "" }, "require": { - "php": "^7.1.3", + "php": ">=7.2.5", "psr/log": "~1.0", - "symfony/error-handler": "^4.4", - "symfony/event-dispatcher": "^4.4", + "symfony/deprecation-contracts": "^2.1", + "symfony/error-handler": "^4.4|^5.0", + "symfony/event-dispatcher": "^5.0", "symfony/http-foundation": "^4.4|^5.0", "symfony/polyfill-ctype": "^1.8", - "symfony/polyfill-php73": "^1.9" + "symfony/polyfill-php73": "^1.9", + "symfony/polyfill-php80": "^1.15" }, "conflict": { - "symfony/browser-kit": "<4.3", - "symfony/config": "<3.4", - "symfony/console": ">=5", - "symfony/dependency-injection": "<4.3", - "symfony/translation": "<4.2", - "twig/twig": "<1.34|<2.4,>=2" + "symfony/browser-kit": "<4.4", + "symfony/cache": "<5.0", + "symfony/config": "<5.0", + "symfony/console": "<4.4", + "symfony/dependency-injection": "<4.4", + "symfony/doctrine-bridge": "<5.0", + "symfony/form": "<5.0", + "symfony/http-client": "<5.0", + "symfony/mailer": "<5.0", + "symfony/messenger": "<5.0", + "symfony/translation": "<5.0", + "symfony/twig-bridge": "<5.0", + "symfony/validator": "<5.0", + "twig/twig": "<2.4" }, "provide": { "psr/log-implementation": "1.0" }, "require-dev": { "psr/cache": "~1.0", - "symfony/browser-kit": "^4.3|^5.0", - "symfony/config": "^3.4|^4.0|^5.0", - "symfony/console": "^3.4|^4.0", - "symfony/css-selector": "^3.4|^4.0|^5.0", - "symfony/dependency-injection": "^4.3|^5.0", - "symfony/dom-crawler": "^3.4|^4.0|^5.0", - "symfony/expression-language": "^3.4|^4.0|^5.0", - "symfony/finder": "^3.4|^4.0|^5.0", - "symfony/process": "^3.4|^4.0|^5.0", - "symfony/routing": "^3.4|^4.0|^5.0", - "symfony/stopwatch": "^3.4|^4.0|^5.0", - "symfony/templating": "^3.4|^4.0|^5.0", - "symfony/translation": "^4.2|^5.0", + "symfony/browser-kit": "^4.4|^5.0", + "symfony/config": "^5.0", + "symfony/console": "^4.4|^5.0", + "symfony/css-selector": "^4.4|^5.0", + "symfony/dependency-injection": "^4.4|^5.0", + "symfony/dom-crawler": "^4.4|^5.0", + "symfony/expression-language": "^4.4|^5.0", + "symfony/finder": "^4.4|^5.0", + "symfony/process": "^4.4|^5.0", + "symfony/routing": "^4.4|^5.0", + "symfony/stopwatch": "^4.4|^5.0", + "symfony/translation": "^4.4|^5.0", "symfony/translation-contracts": "^1.1|^2", - "twig/twig": "^1.34|^2.4|^3.0" + "twig/twig": "^2.4|^3.0" }, "suggest": { "symfony/browser-kit": "", @@ -3724,7 +4124,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.4-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -3751,26 +4151,41 @@ ], "description": "Symfony HttpKernel Component", "homepage": "https://symfony.com", - "time": "2020-03-30T14:59:15+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-06-15T13:51:38+00:00" }, { "name": "symfony/mime", - "version": "v5.0.7", + "version": "v5.1.2", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "481b7d6da88922fb1e0d86a943987722b08f3955" + "reference": "c0c418f05e727606e85b482a8591519c4712cf45" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/481b7d6da88922fb1e0d86a943987722b08f3955", - "reference": "481b7d6da88922fb1e0d86a943987722b08f3955", + "url": "https://api.github.com/repos/symfony/mime/zipball/c0c418f05e727606e85b482a8591519c4712cf45", + "reference": "c0c418f05e727606e85b482a8591519c4712cf45", "shasum": "" }, "require": { - "php": "^7.2.5", + "php": ">=7.2.5", "symfony/polyfill-intl-idn": "^1.10", - "symfony/polyfill-mbstring": "^1.0" + "symfony/polyfill-mbstring": "^1.0", + "symfony/polyfill-php80": "^1.15" }, "conflict": { "symfony/mailer": "<4.4" @@ -3782,7 +4197,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -3813,20 +4228,34 @@ "mime", "mime-type" ], - "time": "2020-03-27T16:56:45+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-06-09T15:07:35+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.15.0", + "version": "v1.17.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "4719fa9c18b0464d399f1a63bf624b42b6fa8d14" + "reference": "2edd75b8b35d62fd3eeabba73b26b8f1f60ce13d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/4719fa9c18b0464d399f1a63bf624b42b6fa8d14", - "reference": "4719fa9c18b0464d399f1a63bf624b42b6fa8d14", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/2edd75b8b35d62fd3eeabba73b26b8f1f60ce13d", + "reference": "2edd75b8b35d62fd3eeabba73b26b8f1f60ce13d", "shasum": "" }, "require": { @@ -3838,7 +4267,11 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.15-dev" + "dev-master": "1.17-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { @@ -3871,20 +4304,34 @@ "polyfill", "portable" ], - "time": "2020-02-27T09:26:54+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-06-06T08:46:27+00:00" }, { "name": "symfony/polyfill-iconv", - "version": "v1.15.0", + "version": "v1.17.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-iconv.git", - "reference": "ad6d62792bfbcfc385dd34b424d4fcf9712a32c8" + "reference": "ba6c9c18db36235b859cc29b8372d1c01298c035" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/ad6d62792bfbcfc385dd34b424d4fcf9712a32c8", - "reference": "ad6d62792bfbcfc385dd34b424d4fcf9712a32c8", + "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/ba6c9c18db36235b859cc29b8372d1c01298c035", + "reference": "ba6c9c18db36235b859cc29b8372d1c01298c035", "shasum": "" }, "require": { @@ -3896,7 +4343,11 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.15-dev" + "dev-master": "1.17-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { @@ -3930,20 +4381,112 @@ "portable", "shim" ], - "time": "2020-03-09T19:04:49+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-06-06T08:46:27+00:00" }, { - "name": "symfony/polyfill-intl-idn", - "version": "v1.15.0", + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.17.1", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "47bd6aa45beb1cd7c6a16b7d1810133b728bdfcf" + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "6e4dbcf5e81eba86e36731f94fe56b1726835846" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/47bd6aa45beb1cd7c6a16b7d1810133b728bdfcf", - "reference": "47bd6aa45beb1cd7c6a16b7d1810133b728bdfcf", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/6e4dbcf5e81eba86e36731f94fe56b1726835846", + "reference": "6e4dbcf5e81eba86e36731f94fe56b1726835846", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.17-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-06-06T08:46:27+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.17.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "a57f8161502549a742a63c09f0a604997bf47027" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/a57f8161502549a742a63c09f0a604997bf47027", + "reference": "a57f8161502549a742a63c09f0a604997bf47027", "shasum": "" }, "require": { @@ -3957,7 +4500,11 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.15-dev" + "dev-master": "1.17-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { @@ -3992,20 +4539,115 @@ "portable", "shim" ], - "time": "2020-03-09T19:04:49+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-06-06T08:46:27+00:00" }, { - "name": "symfony/polyfill-mbstring", - "version": "v1.15.0", + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.17.1", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac" + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "40309d1700e8f72447bb9e7b54af756eeea35620" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/81ffd3a9c6d707be22e3012b827de1c9775fc5ac", - "reference": "81ffd3a9c6d707be22e3012b827de1c9775fc5ac", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/40309d1700e8f72447bb9e7b54af756eeea35620", + "reference": "40309d1700e8f72447bb9e7b54af756eeea35620", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.17-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-06-14T14:40:37+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.17.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "7110338d81ce1cbc3e273136e4574663627037a7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/7110338d81ce1cbc3e273136e4574663627037a7", + "reference": "7110338d81ce1cbc3e273136e4574663627037a7", "shasum": "" }, "require": { @@ -4017,7 +4659,11 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.15-dev" + "dev-master": "1.17-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { @@ -4051,20 +4697,34 @@ "portable", "shim" ], - "time": "2020-03-09T19:04:49+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-06-06T08:46:27+00:00" }, { "name": "symfony/polyfill-php56", - "version": "v1.15.0", + "version": "v1.17.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php56.git", - "reference": "d51ec491c8ddceae7dca8dd6c7e30428f543f37d" + "reference": "a25861bb3c79b0ec2da9ede51de2ea573818b943" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php56/zipball/d51ec491c8ddceae7dca8dd6c7e30428f543f37d", - "reference": "d51ec491c8ddceae7dca8dd6c7e30428f543f37d", + "url": "https://api.github.com/repos/symfony/polyfill-php56/zipball/a25861bb3c79b0ec2da9ede51de2ea573818b943", + "reference": "a25861bb3c79b0ec2da9ede51de2ea573818b943", "shasum": "" }, "require": { @@ -4074,7 +4734,11 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.15-dev" + "dev-master": "1.17-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { @@ -4107,20 +4771,34 @@ "portable", "shim" ], - "time": "2020-03-09T19:04:49+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-06-06T08:46:27+00:00" }, { "name": "symfony/polyfill-php72", - "version": "v1.15.0", + "version": "v1.17.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "37b0976c78b94856543260ce09b460a7bc852747" + "reference": "f048e612a3905f34931127360bdd2def19a5e582" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/37b0976c78b94856543260ce09b460a7bc852747", - "reference": "37b0976c78b94856543260ce09b460a7bc852747", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/f048e612a3905f34931127360bdd2def19a5e582", + "reference": "f048e612a3905f34931127360bdd2def19a5e582", "shasum": "" }, "require": { @@ -4129,7 +4807,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.15-dev" + "dev-master": "1.17-dev" } }, "autoload": { @@ -4162,20 +4840,34 @@ "portable", "shim" ], - "time": "2020-02-27T09:26:54+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-05-12T16:47:27+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.15.0", + "version": "v1.17.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "0f27e9f464ea3da33cbe7ca3bdf4eb66def9d0f7" + "reference": "fa0837fe02d617d31fbb25f990655861bb27bd1a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f27e9f464ea3da33cbe7ca3bdf4eb66def9d0f7", - "reference": "0f27e9f464ea3da33cbe7ca3bdf4eb66def9d0f7", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fa0837fe02d617d31fbb25f990655861bb27bd1a", + "reference": "fa0837fe02d617d31fbb25f990655861bb27bd1a", "shasum": "" }, "require": { @@ -4184,7 +4876,11 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.15-dev" + "dev-master": "1.17-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { @@ -4220,20 +4916,114 @@ "portable", "shim" ], - "time": "2020-02-27T09:26:54+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-06-06T08:46:27+00:00" }, { - "name": "symfony/polyfill-util", - "version": "v1.15.0", + "name": "symfony/polyfill-php80", + "version": "v1.17.1", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-util.git", - "reference": "d8e76c104127675d0ea3df3be0f2ae24a8619027" + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "4a5b6bba3259902e386eb80dd1956181ee90b5b2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-util/zipball/d8e76c104127675d0ea3df3be0f2ae24a8619027", - "reference": "d8e76c104127675d0ea3df3be0f2ae24a8619027", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/4a5b6bba3259902e386eb80dd1956181ee90b5b2", + "reference": "4a5b6bba3259902e386eb80dd1956181ee90b5b2", + "shasum": "" + }, + "require": { + "php": ">=7.0.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.17-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-06-06T08:46:27+00:00" + }, + { + "name": "symfony/polyfill-util", + "version": "v1.17.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-util.git", + "reference": "6dd644eda43cd2f3daa883d728d8ab4120a05af6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-util/zipball/6dd644eda43cd2f3daa883d728d8ab4120a05af6", + "reference": "6dd644eda43cd2f3daa883d728d8ab4120a05af6", "shasum": "" }, "require": { @@ -4242,7 +5032,11 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.15-dev" + "dev-master": "1.17-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { @@ -4272,29 +5066,44 @@ "polyfill", "shim" ], - "time": "2020-03-02T11:55:35+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-06-06T08:46:27+00:00" }, { "name": "symfony/process", - "version": "v4.4.7", + "version": "v5.1.2", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "3e40e87a20eaf83a1db825e1fa5097ae89042db3" + "reference": "7f6378c1fa2147eeb1b4c385856ce9de0d46ebd1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/3e40e87a20eaf83a1db825e1fa5097ae89042db3", - "reference": "3e40e87a20eaf83a1db825e1fa5097ae89042db3", + "url": "https://api.github.com/repos/symfony/process/zipball/7f6378c1fa2147eeb1b4c385856ce9de0d46ebd1", + "reference": "7f6378c1fa2147eeb1b4c385856ce9de0d46ebd1", "shasum": "" }, "require": { - "php": "^7.1.3" + "php": ">=7.2.5", + "symfony/polyfill-php80": "^1.15" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.4-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -4321,38 +5130,54 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2020-03-27T16:54:36+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-05-30T20:35:19+00:00" }, { "name": "symfony/routing", - "version": "v4.4.7", + "version": "v5.1.2", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "0f562fa613e288d7dbae6c63abbc9b33ed75a8f8" + "reference": "bbd0ba121d623f66d165a55a108008968911f3eb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/0f562fa613e288d7dbae6c63abbc9b33ed75a8f8", - "reference": "0f562fa613e288d7dbae6c63abbc9b33ed75a8f8", + "url": "https://api.github.com/repos/symfony/routing/zipball/bbd0ba121d623f66d165a55a108008968911f3eb", + "reference": "bbd0ba121d623f66d165a55a108008968911f3eb", "shasum": "" }, "require": { - "php": "^7.1.3" + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1", + "symfony/polyfill-php80": "^1.15" }, "conflict": { - "symfony/config": "<4.2", - "symfony/dependency-injection": "<3.4", - "symfony/yaml": "<3.4" + "symfony/config": "<5.0", + "symfony/dependency-injection": "<4.4", + "symfony/yaml": "<4.4" }, "require-dev": { "doctrine/annotations": "~1.2", "psr/log": "~1.0", - "symfony/config": "^4.2|^5.0", - "symfony/dependency-injection": "^3.4|^4.0|^5.0", - "symfony/expression-language": "^3.4|^4.0|^5.0", - "symfony/http-foundation": "^3.4|^4.0|^5.0", - "symfony/yaml": "^3.4|^4.0|^5.0" + "symfony/config": "^5.0", + "symfony/dependency-injection": "^4.4|^5.0", + "symfony/expression-language": "^4.4|^5.0", + "symfony/http-foundation": "^4.4|^5.0", + "symfony/yaml": "^4.4|^5.0" }, "suggest": { "doctrine/annotations": "For using the annotation loader", @@ -4364,7 +5189,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.4-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -4397,24 +5222,38 @@ "uri", "url" ], - "time": "2020-03-30T11:41:10+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-06-10T11:49:58+00:00" }, { "name": "symfony/service-contracts", - "version": "v2.0.1", + "version": "v2.1.2", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "144c5e51266b281231e947b51223ba14acf1a749" + "reference": "66a8f0957a3ca54e4f724e49028ab19d75a8918b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/144c5e51266b281231e947b51223ba14acf1a749", - "reference": "144c5e51266b281231e947b51223ba14acf1a749", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/66a8f0957a3ca54e4f724e49028ab19d75a8918b", + "reference": "66a8f0957a3ca54e4f724e49028ab19d75a8918b", "shasum": "" }, "require": { - "php": "^7.2.5", + "php": ">=7.2.5", "psr/container": "^1.0" }, "suggest": { @@ -4423,7 +5262,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "2.1-dev" } }, "autoload": { @@ -4455,46 +5294,147 @@ "interoperability", "standards" ], - "time": "2019-11-18T17:27:11+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-05-20T17:43:50+00:00" }, { - "name": "symfony/translation", - "version": "v4.4.7", + "name": "symfony/string", + "version": "v5.1.2", "source": { "type": "git", - "url": "https://github.com/symfony/translation.git", - "reference": "4e54d336f2eca5facad449d0b0118bb449375b76" + "url": "https://github.com/symfony/string.git", + "reference": "ac70459db781108db7c6d8981dd31ce0e29e3298" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/4e54d336f2eca5facad449d0b0118bb449375b76", - "reference": "4e54d336f2eca5facad449d0b0118bb449375b76", + "url": "https://api.github.com/repos/symfony/string/zipball/ac70459db781108db7c6d8981dd31ce0e29e3298", + "reference": "ac70459db781108db7c6d8981dd31ce0e29e3298", "shasum": "" }, "require": { - "php": "^7.1.3", + "php": ">=7.2.5", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", "symfony/polyfill-mbstring": "~1.0", - "symfony/translation-contracts": "^1.1.6|^2" + "symfony/polyfill-php80": "~1.15" + }, + "require-dev": { + "symfony/error-handler": "^4.4|^5.0", + "symfony/http-client": "^4.4|^5.0", + "symfony/translation-contracts": "^1.1|^2", + "symfony/var-exporter": "^4.4|^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "files": [ + "Resources/functions.php" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony String component", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-06-11T12:16:36+00:00" + }, + { + "name": "symfony/translation", + "version": "v5.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "d387f07d4c15f9c09439cf3f13ddbe0b2c5e8be2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/d387f07d4c15f9c09439cf3f13ddbe0b2c5e8be2", + "reference": "d387f07d4c15f9c09439cf3f13ddbe0b2c5e8be2", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php80": "^1.15", + "symfony/translation-contracts": "^2" }, "conflict": { - "symfony/config": "<3.4", - "symfony/dependency-injection": "<3.4", - "symfony/http-kernel": "<4.4", - "symfony/yaml": "<3.4" + "symfony/config": "<4.4", + "symfony/dependency-injection": "<5.0", + "symfony/http-kernel": "<5.0", + "symfony/twig-bundle": "<5.0", + "symfony/yaml": "<4.4" }, "provide": { - "symfony/translation-implementation": "1.0" + "symfony/translation-implementation": "2.0" }, "require-dev": { "psr/log": "~1.0", - "symfony/config": "^3.4|^4.0|^5.0", - "symfony/console": "^3.4|^4.0|^5.0", - "symfony/dependency-injection": "^3.4|^4.0|^5.0", - "symfony/finder": "~2.8|~3.0|~4.0|^5.0", - "symfony/http-kernel": "^4.4", - "symfony/intl": "^3.4|^4.0|^5.0", + "symfony/config": "^4.4|^5.0", + "symfony/console": "^4.4|^5.0", + "symfony/dependency-injection": "^5.0", + "symfony/finder": "^4.4|^5.0", + "symfony/http-kernel": "^5.0", + "symfony/intl": "^4.4|^5.0", "symfony/service-contracts": "^1.1.2|^2", - "symfony/yaml": "^3.4|^4.0|^5.0" + "symfony/yaml": "^4.4|^5.0" }, "suggest": { "psr/log-implementation": "To use logging capability in translator", @@ -4504,7 +5444,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.4-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -4531,24 +5471,38 @@ ], "description": "Symfony Translation Component", "homepage": "https://symfony.com", - "time": "2020-03-27T16:54:36+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-05-30T20:35:19+00:00" }, { "name": "symfony/translation-contracts", - "version": "v2.0.1", + "version": "v2.1.2", "source": { "type": "git", "url": "https://github.com/symfony/translation-contracts.git", - "reference": "8cc682ac458d75557203b2f2f14b0b92e1c744ed" + "reference": "e5ca07c8f817f865f618aa072c2fe8e0e637340e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/8cc682ac458d75557203b2f2f14b0b92e1c744ed", - "reference": "8cc682ac458d75557203b2f2f14b0b92e1c744ed", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/e5ca07c8f817f865f618aa072c2fe8e0e637340e", + "reference": "e5ca07c8f817f865f618aa072c2fe8e0e637340e", "shasum": "" }, "require": { - "php": "^7.2.5" + "php": ">=7.2.5" }, "suggest": { "symfony/translation-implementation": "" @@ -4556,7 +5510,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "2.1-dev" } }, "autoload": { @@ -4588,36 +5542,50 @@ "interoperability", "standards" ], - "time": "2019-11-18T17:27:11+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-05-20T17:43:50+00:00" }, { "name": "symfony/var-dumper", - "version": "v4.4.7", + "version": "v5.1.2", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "5a0c2d93006131a36cf6f767d10e2ca8333b0d4a" + "reference": "46a942903059b0b05e601f00eb64179e05578c0f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/5a0c2d93006131a36cf6f767d10e2ca8333b0d4a", - "reference": "5a0c2d93006131a36cf6f767d10e2ca8333b0d4a", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/46a942903059b0b05e601f00eb64179e05578c0f", + "reference": "46a942903059b0b05e601f00eb64179e05578c0f", "shasum": "" }, "require": { - "php": "^7.1.3", + "php": ">=7.2.5", "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php72": "~1.5" + "symfony/polyfill-php80": "^1.15" }, "conflict": { - "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0", - "symfony/console": "<3.4" + "phpunit/phpunit": "<5.4.3", + "symfony/console": "<4.4" }, "require-dev": { "ext-iconv": "*", - "symfony/console": "^3.4|^4.0|^5.0", + "symfony/console": "^4.4|^5.0", "symfony/process": "^4.4|^5.0", - "twig/twig": "^1.34|^2.4|^3.0" + "twig/twig": "^2.4|^3.0" }, "suggest": { "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).", @@ -4630,7 +5598,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "4.4-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -4664,24 +5632,38 @@ "debug", "dump" ], - "time": "2020-03-27T16:54:36+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-05-30T20:35:19+00:00" }, { "name": "symfony/yaml", - "version": "v4.4.7", + "version": "v4.4.10", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "ef166890d821518106da3560086bfcbeb4fadfec" + "reference": "c2d2cc66e892322cfcc03f8f12f8340dbd7a3f8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/ef166890d821518106da3560086bfcbeb4fadfec", - "reference": "ef166890d821518106da3560086bfcbeb4fadfec", + "url": "https://api.github.com/repos/symfony/yaml/zipball/c2d2cc66e892322cfcc03f8f12f8340dbd7a3f8a", + "reference": "c2d2cc66e892322cfcc03f8f12f8340dbd7a3f8a", "shasum": "" }, "require": { - "php": "^7.1.3", + "php": ">=7.1.3", "symfony/polyfill-ctype": "~1.8" }, "conflict": { @@ -4723,7 +5705,21 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2020-03-30T11:41:10+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-05-20T08:37:50+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -4776,27 +5772,28 @@ }, { "name": "vlucas/phpdotenv", - "version": "v3.6.2", + "version": "v4.1.7", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "786a947e57086cf236cefdee80784634224b99fa" + "reference": "db63b2ea280fdcf13c4ca392121b0b2450b51193" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/786a947e57086cf236cefdee80784634224b99fa", - "reference": "786a947e57086cf236cefdee80784634224b99fa", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/db63b2ea280fdcf13c4ca392121b0b2450b51193", + "reference": "db63b2ea280fdcf13c4ca392121b0b2450b51193", "shasum": "" }, "require": { - "php": "^5.4 || ^7.0", - "phpoption/phpoption": "^1.5", - "symfony/polyfill-ctype": "^1.9" + "php": "^5.5.9 || ^7.0 || ^8.0", + "phpoption/phpoption": "^1.7.3", + "symfony/polyfill-ctype": "^1.16" }, "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", "ext-filter": "*", "ext-pcre": "*", - "phpunit/phpunit": "^4.8.35 || ^5.0 || ^6.0 || ^7.0" + "phpunit/phpunit": "^4.8.35 || ^5.7.27 || ^6.5.6 || ^7.0" }, "suggest": { "ext-filter": "Required to use the boolean validator.", @@ -4805,7 +5802,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.6-dev" + "dev-master": "4.1-dev" } }, "autoload": { @@ -4835,20 +5832,96 @@ "env", "environment" ], - "time": "2020-03-27T23:36:02+00:00" + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2020-06-07T18:25:35+00:00" }, { - "name": "webmozart/assert", - "version": "1.7.0", + "name": "voku/portable-ascii", + "version": "1.5.2", "source": { "type": "git", - "url": "https://github.com/webmozart/assert.git", - "reference": "aed98a490f9a8f78468232db345ab9cf606cf598" + "url": "https://github.com/voku/portable-ascii.git", + "reference": "618631dc601d8eb6ea0a9fbf654ec82f066c4e97" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/aed98a490f9a8f78468232db345ab9cf606cf598", - "reference": "aed98a490f9a8f78468232db345ab9cf606cf598", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/618631dc601d8eb6ea0a9fbf654ec82f066c4e97", + "reference": "618631dc601d8eb6ea0a9fbf654ec82f066c4e97", + "shasum": "" + }, + "require": { + "php": ">=7.0.0" + }, + "require-dev": { + "phpunit/phpunit": "~6.0 || ~7.0" + }, + "suggest": { + "ext-intl": "Use Intl for transliterator_transliterate() support" + }, + "type": "library", + "autoload": { + "psr-4": { + "voku\\": "src/voku/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lars Moelleken", + "homepage": "http://www.moelleken.org/" + } + ], + "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", + "homepage": "https://github.com/voku/portable-ascii", + "keywords": [ + "ascii", + "clean", + "php" + ], + "funding": [ + { + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", + "type": "github" + }, + { + "url": "https://www.patreon.com/voku", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", + "type": "tidelift" + } + ], + "time": "2020-06-15T23:49:30+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.9.0", + "source": { + "type": "git", + "url": "https://github.com/webmozart/assert.git", + "reference": "9dc4f203e36f2b486149058bade43c851dd97451" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/assert/zipball/9dc4f203e36f2b486149058bade43c851dd97451", + "reference": "9dc4f203e36f2b486149058bade43c851dd97451", "shasum": "" }, "require": { @@ -4856,7 +5929,8 @@ "symfony/polyfill-ctype": "^1.8" }, "conflict": { - "vimeo/psalm": "<3.6.0" + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<3.9.1" }, "require-dev": { "phpunit/phpunit": "^4.8.36 || ^7.5.13" @@ -4883,7 +5957,7 @@ "check", "validate" ], - "time": "2020-02-14T12:15:55+00:00" + "time": "2020-06-16T10:16:42+00:00" } ], "packages-dev": [ @@ -4934,16 +6008,16 @@ }, { "name": "barryvdh/laravel-debugbar", - "version": "v3.2.9", + "version": "v3.3.3", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-debugbar.git", - "reference": "42d5da5379a7860093f8e4032167e4cb5ebec180" + "reference": "57f2219f6d9efe41ed1bc880d86701c52f261bf5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/42d5da5379a7860093f8e4032167e4cb5ebec180", - "reference": "42d5da5379a7860093f8e4032167e4cb5ebec180", + "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/57f2219f6d9efe41ed1bc880d86701c52f261bf5", + "reference": "57f2219f6d9efe41ed1bc880d86701c52f261bf5", "shasum": "" }, "require": { @@ -4998,20 +6072,26 @@ "profiler", "webprofiler" ], - "time": "2020-02-25T20:42:23+00:00" + "funding": [ + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2020-05-05T10:53:32+00:00" }, { "name": "barryvdh/laravel-ide-helper", - "version": "v2.6.7", + "version": "v2.7.0", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-ide-helper.git", - "reference": "edd69c5e0508972c81f1f7173236de2459c45814" + "reference": "5f677edc14bdcfdcac36633e6eea71b2728a4dbc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/edd69c5e0508972c81f1f7173236de2459c45814", - "reference": "edd69c5e0508972c81f1f7173236de2459c45814", + "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/5f677edc14bdcfdcac36633e6eea71b2728a4dbc", + "reference": "5f677edc14bdcfdcac36633e6eea71b2728a4dbc", "shasum": "" }, "require": { @@ -5027,7 +6107,7 @@ "illuminate/config": "^5.5|^6|^7", "illuminate/view": "^5.5|^6|^7", "mockery/mockery": "^1.3", - "orchestra/testbench": "^3|^4", + "orchestra/testbench": "^3|^4|^5", "phpro/grumphp": "^0.17.1", "squizlabs/php_codesniffer": "^3" }, @@ -5069,7 +6149,13 @@ "phpstorm", "sublime" ], - "time": "2020-02-25T20:41:32+00:00" + "funding": [ + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2020-04-22T09:57:26+00:00" }, { "name": "barryvdh/reflection-docblock", @@ -5166,16 +6252,16 @@ }, { "name": "codedungeon/phpunit-result-printer", - "version": "0.25.1", + "version": "0.28.0", "source": { "type": "git", "url": "https://github.com/mikeerickson/phpunit-pretty-result-printer.git", - "reference": "4a689ac40366eb4adf166cf4676da7ef30d82315" + "reference": "bc023b0311589bee19047425083163ffa3f0cf88" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mikeerickson/phpunit-pretty-result-printer/zipball/4a689ac40366eb4adf166cf4676da7ef30d82315", - "reference": "4a689ac40366eb4adf166cf4676da7ef30d82315", + "url": "https://api.github.com/repos/mikeerickson/phpunit-pretty-result-printer/zipball/bc023b0311589bee19047425083163ffa3f0cf88", + "reference": "bc023b0311589bee19047425083163ffa3f0cf88", "shasum": "" }, "require": { @@ -5183,10 +6269,9 @@ "codedungeon/php-cli-colors": "^1.10.2", "hassankhan/config": "^0.11.2", "php": "^7.1", - "symfony/yaml": "^2.7|^3.0|^4.0" + "symfony/yaml": "^2.7|^3.0|^4.0|^5.0" }, "require-dev": { - "phpunit/phpunit": "7.5.*", "spatie/phpunit-watcher": "^1.6" }, "type": "library", @@ -5215,20 +6300,20 @@ "result-printer", "testing" ], - "time": "2019-02-01T19:13:43+00:00" + "time": "2020-06-24T00:16:05+00:00" }, { "name": "composer/ca-bundle", - "version": "1.2.6", + "version": "1.2.7", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "47fe531de31fca4a1b997f87308e7d7804348f7e" + "reference": "95c63ab2117a72f48f5a55da9740a3273d45b7fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/47fe531de31fca4a1b997f87308e7d7804348f7e", - "reference": "47fe531de31fca4a1b997f87308e7d7804348f7e", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/95c63ab2117a72f48f5a55da9740a3273d45b7fd", + "reference": "95c63ab2117a72f48f5a55da9740a3273d45b7fd", "shasum": "" }, "require": { @@ -5271,20 +6356,30 @@ "ssl", "tls" ], - "time": "2020-01-13T10:02:55+00:00" + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2020-04-08T08:27:21+00:00" }, { "name": "composer/composer", - "version": "1.10.1", + "version": "1.10.7", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "b912a45da3e2b22f5cb5a23e441b697a295ba011" + "reference": "956608ea4f7de9e58c53dfb019d85ae62b193c39" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/b912a45da3e2b22f5cb5a23e441b697a295ba011", - "reference": "b912a45da3e2b22f5cb5a23e441b697a295ba011", + "url": "https://api.github.com/repos/composer/composer/zipball/956608ea4f7de9e58c53dfb019d85ae62b193c39", + "reference": "956608ea4f7de9e58c53dfb019d85ae62b193c39", "shasum": "" }, "require": { @@ -5292,7 +6387,7 @@ "composer/semver": "^1.0", "composer/spdx-licenses": "^1.2", "composer/xdebug-handler": "^1.1", - "justinrainbow/json-schema": "^3.0 || ^4.0 || ^5.0", + "justinrainbow/json-schema": "^5.2.10", "php": "^5.3.2 || ^7.0", "psr/log": "^1.0", "seld/jsonlint": "^1.4", @@ -5303,7 +6398,8 @@ "symfony/process": "^2.7 || ^3.0 || ^4.0 || ^5.0" }, "conflict": { - "symfony/console": "2.8.38" + "symfony/console": "2.8.38", + "symfony/phpunit-bridge": "3.4.40" }, "require-dev": { "phpspec/prophecy": "^1.10", @@ -5351,7 +6447,21 @@ "dependency", "package" ], - "time": "2020-03-13T19:34:27+00:00" + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2020-06-03T08:03:56+00:00" }, { "name": "composer/semver", @@ -5476,16 +6586,16 @@ }, { "name": "composer/xdebug-handler", - "version": "1.4.1", + "version": "1.4.2", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "1ab9842d69e64fb3a01be6b656501032d1b78cb7" + "reference": "fa2aaf99e2087f013a14f7432c1cd2dd7d8f1f51" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/1ab9842d69e64fb3a01be6b656501032d1b78cb7", - "reference": "1ab9842d69e64fb3a01be6b656501032d1b78cb7", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/fa2aaf99e2087f013a14f7432c1cd2dd7d8f1f51", + "reference": "fa2aaf99e2087f013a14f7432c1cd2dd7d8f1f51", "shasum": "" }, "require": { @@ -5516,26 +6626,40 @@ "Xdebug", "performance" ], - "time": "2020-03-01T12:26:26+00:00" + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2020-06-04T11:16:35+00:00" }, { "name": "doctrine/annotations", - "version": "1.10.1", + "version": "1.10.3", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "5eb79f3dbdffed6544e1fc287572c0f462bd29bb" + "reference": "5db60a4969eba0e0c197a19c077780aadbc43c5d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/5eb79f3dbdffed6544e1fc287572c0f462bd29bb", - "reference": "5eb79f3dbdffed6544e1fc287572c0f462bd29bb", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/5db60a4969eba0e0c197a19c077780aadbc43c5d", + "reference": "5db60a4969eba0e0c197a19c077780aadbc43c5d", "shasum": "" }, "require": { "doctrine/lexer": "1.*", "ext-tokenizer": "*", - "php": "^7.1" + "php": "^7.1 || ^8.0" }, "require-dev": { "doctrine/cache": "1.*", @@ -5585,24 +6709,24 @@ "docblock", "parser" ], - "time": "2020-04-02T12:33:25+00:00" + "time": "2020-05-25T17:24:27+00:00" }, { "name": "doctrine/instantiator", - "version": "1.3.0", + "version": "1.3.1", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "ae466f726242e637cebdd526a7d991b9433bacf1" + "reference": "f350df0268e904597e3bd9c4685c53e0e333feea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/ae466f726242e637cebdd526a7d991b9433bacf1", - "reference": "ae466f726242e637cebdd526a7d991b9433bacf1", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/f350df0268e904597e3bd9c4685c53e0e333feea", + "reference": "f350df0268e904597e3bd9c4685c53e0e333feea", "shasum": "" }, "require": { - "php": "^7.1" + "php": "^7.1 || ^8.0" }, "require-dev": { "doctrine/coding-standard": "^6.0", @@ -5641,7 +6765,21 @@ "constructor", "instantiate" ], - "time": "2019-10-21T16:45:58+00:00" + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2020-05-29T17:27:14+00:00" }, { "name": "friendsofphp/php-cs-fixer", @@ -5889,16 +7027,16 @@ }, { "name": "justinrainbow/json-schema", - "version": "5.2.9", + "version": "5.2.10", "source": { "type": "git", "url": "https://github.com/justinrainbow/json-schema.git", - "reference": "44c6787311242a979fa15c704327c20e7221a0e4" + "reference": "2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/44c6787311242a979fa15c704327c20e7221a0e4", - "reference": "44c6787311242a979fa15c704327c20e7221a0e4", + "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b", + "reference": "2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b", "shasum": "" }, "require": { @@ -5951,38 +7089,38 @@ "json", "schema" ], - "time": "2019-09-25T14:49:45+00:00" + "time": "2020-05-27T16:41:55+00:00" }, { "name": "laravel/dusk", - "version": "v5.11.0", + "version": "v6.3.0", "source": { "type": "git", "url": "https://github.com/laravel/dusk.git", - "reference": "e07cc46a1e39767739e8197189780b4c2639806d" + "reference": "5481bfd50c80599d26529b7f2c9adeb2154a57fc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/dusk/zipball/e07cc46a1e39767739e8197189780b4c2639806d", - "reference": "e07cc46a1e39767739e8197189780b4c2639806d", + "url": "https://api.github.com/repos/laravel/dusk/zipball/5481bfd50c80599d26529b7f2c9adeb2154a57fc", + "reference": "5481bfd50c80599d26529b7f2c9adeb2154a57fc", "shasum": "" }, "require": { "ext-json": "*", "ext-zip": "*", - "illuminate/console": "~5.7.0|~5.8.0|^6.0|^7.0", - "illuminate/support": "~5.7.0|~5.8.0|^6.0|^7.0", - "nesbot/carbon": "^1.20|^2.0", - "php": ">=7.1.0", + "illuminate/console": "^6.0|^7.0", + "illuminate/support": "^6.0|^7.0", + "nesbot/carbon": "^2.0", + "php": "^7.2", "php-webdriver/webdriver": "^1.8.1", - "symfony/console": "^4.0|^5.0", - "symfony/finder": "^4.0|^5.0", - "symfony/process": "^4.0|^5.0", - "vlucas/phpdotenv": "^2.2|^3.0|^4.0" + "symfony/console": "^4.3|^5.0", + "symfony/finder": "^4.3|^5.0", + "symfony/process": "^4.3|^5.0", + "vlucas/phpdotenv": "^3.0|^4.0" }, "require-dev": { "mockery/mockery": "^1.0", - "phpunit/phpunit": "^7.5|^8.0" + "phpunit/phpunit": "^7.5.15|^8.4|^9.0" }, "suggest": { "ext-pcntl": "Used to gracefully terminate Dusk when tests are running." @@ -5990,7 +7128,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "6.x-dev" }, "laravel": { "providers": [ @@ -6019,20 +7157,20 @@ "testing", "webdriver" ], - "time": "2020-03-24T16:21:49+00:00" + "time": "2020-06-16T19:05:20+00:00" }, { "name": "maximebf/debugbar", - "version": "v1.16.1", + "version": "v1.16.3", "source": { "type": "git", "url": "https://github.com/maximebf/php-debugbar.git", - "reference": "58998b818c6567fac01e35b8a4b70c1a64530556" + "reference": "1a1605b8e9bacb34cc0c6278206d699772e1d372" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/58998b818c6567fac01e35b8a4b70c1a64530556", - "reference": "58998b818c6567fac01e35b8a4b70c1a64530556", + "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/1a1605b8e9bacb34cc0c6278206d699772e1d372", + "reference": "1a1605b8e9bacb34cc0c6278206d699772e1d372", "shasum": "" }, "require": { @@ -6080,34 +7218,37 @@ "debug", "debugbar" ], - "time": "2019-11-24T09:46:11+00:00" + "time": "2020-05-06T07:06:27+00:00" }, { "name": "mockery/mockery", - "version": "1.3.1", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/mockery/mockery.git", - "reference": "f69bbde7d7a75d6b2862d9ca8fab1cd28014b4be" + "reference": "6c6a7c533469873deacf998237e7649fc6b36223" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mockery/mockery/zipball/f69bbde7d7a75d6b2862d9ca8fab1cd28014b4be", - "reference": "f69bbde7d7a75d6b2862d9ca8fab1cd28014b4be", + "url": "https://api.github.com/repos/mockery/mockery/zipball/6c6a7c533469873deacf998237e7649fc6b36223", + "reference": "6c6a7c533469873deacf998237e7649fc6b36223", "shasum": "" }, "require": { "hamcrest/hamcrest-php": "~2.0", "lib-pcre": ">=7.0", - "php": ">=5.6.0" + "php": "^7.3.0" + }, + "conflict": { + "phpunit/phpunit": "<8.0" }, "require-dev": { - "phpunit/phpunit": "~5.7.10|~6.5|~7.0|~8.0" + "phpunit/phpunit": "^8.0.0 || ^9.0.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.3.x-dev" + "dev-master": "1.4.x-dev" } }, "autoload": { @@ -6145,7 +7286,7 @@ "test double", "testing" ], - "time": "2019-12-26T09:49:15+00:00" + "time": "2020-05-19T14:25:16+00:00" }, { "name": "myclabs/deep-copy", @@ -6350,16 +7491,16 @@ }, { "name": "php-mock/php-mock", - "version": "2.2.1", + "version": "2.2.2", "source": { "type": "git", "url": "https://github.com/php-mock/php-mock.git", - "reference": "8ca7205ad5e73fbbffa9bde9f6bc90daf5e49702" + "reference": "890d3e32e3a5f29715a8fd17debd87a0c9e614a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-mock/php-mock/zipball/8ca7205ad5e73fbbffa9bde9f6bc90daf5e49702", - "reference": "8ca7205ad5e73fbbffa9bde9f6bc90daf5e49702", + "url": "https://api.github.com/repos/php-mock/php-mock/zipball/890d3e32e3a5f29715a8fd17debd87a0c9e614a0", + "reference": "890d3e32e3a5f29715a8fd17debd87a0c9e614a0", "shasum": "" }, "require": { @@ -6410,7 +7551,7 @@ "test", "test double" ], - "time": "2020-02-08T14:50:32+00:00" + "time": "2020-04-17T16:39:00+00:00" }, { "name": "php-mock/php-mock-integration", @@ -6586,24 +7727,21 @@ }, { "name": "phpdocumentor/reflection-common", - "version": "2.0.0", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a" + "reference": "6568f4687e5b41b054365f9ae03fcb1ed5f2069b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/63a995caa1ca9e5590304cd845c15ad6d482a62a", - "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/6568f4687e5b41b054365f9ae03fcb1ed5f2069b", + "reference": "6568f4687e5b41b054365f9ae03fcb1ed5f2069b", "shasum": "" }, "require": { "php": ">=7.1" }, - "require-dev": { - "phpunit/phpunit": "~6" - }, "type": "library", "extra": { "branch-alias": { @@ -6634,7 +7772,7 @@ "reflection", "static analysis" ], - "time": "2018-08-07T13:53:10+00:00" + "time": "2020-04-27T09:25:28+00:00" }, { "name": "phpdocumentor/reflection-docblock", @@ -6691,16 +7829,16 @@ }, { "name": "phpdocumentor/type-resolver", - "version": "1.1.0", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "7462d5f123dfc080dfdf26897032a6513644fc95" + "reference": "30441f2752e493c639526b215ed81d54f369d693" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/7462d5f123dfc080dfdf26897032a6513644fc95", - "reference": "7462d5f123dfc080dfdf26897032a6513644fc95", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/30441f2752e493c639526b215ed81d54f369d693", + "reference": "30441f2752e493c639526b215ed81d54f369d693", "shasum": "" }, "require": { @@ -6714,7 +7852,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-1.x": "1.x-dev" } }, "autoload": { @@ -6733,7 +7871,7 @@ } ], "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", - "time": "2020-02-18T18:59:58+00:00" + "time": "2020-06-19T20:22:09+00:00" }, { "name": "phpspec/prophecy", @@ -6800,40 +7938,40 @@ }, { "name": "phpunit/php-code-coverage", - "version": "6.1.4", + "version": "7.0.10", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d" + "reference": "f1884187926fbb755a9aaf0b3836ad3165b478bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/807e6013b00af69b6c5d9ceb4282d0393dbb9d8d", - "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f1884187926fbb755a9aaf0b3836ad3165b478bf", + "reference": "f1884187926fbb755a9aaf0b3836ad3165b478bf", "shasum": "" }, "require": { "ext-dom": "*", "ext-xmlwriter": "*", - "php": "^7.1", - "phpunit/php-file-iterator": "^2.0", + "php": "^7.2", + "phpunit/php-file-iterator": "^2.0.2", "phpunit/php-text-template": "^1.2.1", - "phpunit/php-token-stream": "^3.0", + "phpunit/php-token-stream": "^3.1.1", "sebastian/code-unit-reverse-lookup": "^1.0.1", - "sebastian/environment": "^3.1 || ^4.0", + "sebastian/environment": "^4.2.2", "sebastian/version": "^2.0.1", - "theseer/tokenizer": "^1.1" + "theseer/tokenizer": "^1.1.3" }, "require-dev": { - "phpunit/phpunit": "^7.0" + "phpunit/phpunit": "^8.2.2" }, "suggest": { - "ext-xdebug": "^2.6.0" + "ext-xdebug": "^2.7.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "6.1-dev" + "dev-master": "7.0-dev" } }, "autoload": { @@ -6859,7 +7997,7 @@ "testing", "xunit" ], - "time": "2018-10-31T16:06:48+00:00" + "time": "2019-11-20T13:55:58+00:00" }, { "name": "phpunit/php-file-iterator", @@ -7052,53 +8190,52 @@ }, { "name": "phpunit/phpunit", - "version": "7.5.20", + "version": "8.5.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "9467db479d1b0487c99733bb1e7944d32deded2c" + "reference": "34c18baa6a44f1d1fbf0338907139e9dce95b997" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9467db479d1b0487c99733bb1e7944d32deded2c", - "reference": "9467db479d1b0487c99733bb1e7944d32deded2c", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/34c18baa6a44f1d1fbf0338907139e9dce95b997", + "reference": "34c18baa6a44f1d1fbf0338907139e9dce95b997", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.1", + "doctrine/instantiator": "^1.2.0", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", "ext-xml": "*", - "myclabs/deep-copy": "^1.7", - "phar-io/manifest": "^1.0.2", - "phar-io/version": "^2.0", - "php": "^7.1", - "phpspec/prophecy": "^1.7", - "phpunit/php-code-coverage": "^6.0.7", - "phpunit/php-file-iterator": "^2.0.1", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.9.1", + "phar-io/manifest": "^1.0.3", + "phar-io/version": "^2.0.1", + "php": "^7.2", + "phpspec/prophecy": "^1.8.1", + "phpunit/php-code-coverage": "^7.0.7", + "phpunit/php-file-iterator": "^2.0.2", "phpunit/php-text-template": "^1.2.1", - "phpunit/php-timer": "^2.1", - "sebastian/comparator": "^3.0", - "sebastian/diff": "^3.0", - "sebastian/environment": "^4.0", - "sebastian/exporter": "^3.1", - "sebastian/global-state": "^2.0", + "phpunit/php-timer": "^2.1.2", + "sebastian/comparator": "^3.0.2", + "sebastian/diff": "^3.0.2", + "sebastian/environment": "^4.2.2", + "sebastian/exporter": "^3.1.1", + "sebastian/global-state": "^3.0.0", "sebastian/object-enumerator": "^3.0.3", - "sebastian/resource-operations": "^2.0", + "sebastian/resource-operations": "^2.0.1", + "sebastian/type": "^1.1.3", "sebastian/version": "^2.0.1" }, - "conflict": { - "phpunit/phpunit-mock-objects": "*" - }, "require-dev": { "ext-pdo": "*" }, "suggest": { "ext-soap": "*", "ext-xdebug": "*", - "phpunit/php-invoker": "^2.0" + "phpunit/php-invoker": "^2.0.0" }, "bin": [ "phpunit" @@ -7106,7 +8243,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "7.5-dev" + "dev-master": "8.5-dev" } }, "autoload": { @@ -7132,7 +8269,17 @@ "testing", "xunit" ], - "time": "2020-01-08T08:45:45+00:00" + "funding": [ + { + "url": "https://phpunit.de/donate.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-22T07:06:58+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -7421,23 +8568,26 @@ }, { "name": "sebastian/global-state", - "version": "2.0.0", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4" + "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", - "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4", + "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4", "shasum": "" }, "require": { - "php": "^7.0" + "php": "^7.2", + "sebastian/object-reflector": "^1.1.1", + "sebastian/recursion-context": "^3.0" }, "require-dev": { - "phpunit/phpunit": "^6.0" + "ext-dom": "*", + "phpunit/phpunit": "^8.0" }, "suggest": { "ext-uopz": "*" @@ -7445,7 +8595,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -7468,7 +8618,7 @@ "keywords": [ "global state" ], - "time": "2017-04-27T15:39:26+00:00" + "time": "2019-02-01T05:30:01+00:00" }, { "name": "sebastian/object-enumerator", @@ -7657,6 +8807,52 @@ "homepage": "https://www.github.com/sebastianbergmann/resource-operations", "time": "2018-10-04T04:07:39+00:00" }, + { + "name": "sebastian/type", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "3aaaa15fa71d27650d62a948be022fe3b48541a3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/3aaaa15fa71d27650d62a948be022fe3b48541a3", + "reference": "3aaaa15fa71d27650d62a948be022fe3b48541a3", + "shasum": "" + }, + "require": { + "php": "^7.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "time": "2019-07-02T08:10:15+00:00" + }, { "name": "sebastian/version", "version": "2.0.1", @@ -7702,20 +8898,20 @@ }, { "name": "seld/jsonlint", - "version": "1.7.2", + "version": "1.8.0", "source": { "type": "git", "url": "https://github.com/Seldaek/jsonlint.git", - "reference": "e2e5d290e4d2a4f0eb449f510071392e00e10d19" + "reference": "ff2aa5420bfbc296cf6a0bc785fa5b35736de7c1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/e2e5d290e4d2a4f0eb449f510071392e00e10d19", - "reference": "e2e5d290e4d2a4f0eb449f510071392e00e10d19", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/ff2aa5420bfbc296cf6a0bc785fa5b35736de7c1", + "reference": "ff2aa5420bfbc296cf6a0bc785fa5b35736de7c1", "shasum": "" }, "require": { - "php": "^5.3 || ^7.0" + "php": "^5.3 || ^7.0 || ^8.0" }, "require-dev": { "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" @@ -7747,7 +8943,17 @@ "parser", "validator" ], - "time": "2019-10-24T14:27:39+00:00" + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint", + "type": "tidelift" + } + ], + "time": "2020-04-30T19:05:18+00:00" }, { "name": "seld/phar-utils", @@ -7794,27 +9000,98 @@ "time": "2020-02-14T15:25:33+00:00" }, { - "name": "symfony/filesystem", - "version": "v5.0.7", + "name": "symfony/debug", + "version": "v4.4.10", "source": { "type": "git", - "url": "https://github.com/symfony/filesystem.git", - "reference": "ca3b87dd09fff9b771731637f5379965fbfab420" + "url": "https://github.com/symfony/debug.git", + "reference": "28f92d08bb6d1fddf8158e02c194ad43870007e6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/ca3b87dd09fff9b771731637f5379965fbfab420", - "reference": "ca3b87dd09fff9b771731637f5379965fbfab420", + "url": "https://api.github.com/repos/symfony/debug/zipball/28f92d08bb6d1fddf8158e02c194ad43870007e6", + "reference": "28f92d08bb6d1fddf8158e02c194ad43870007e6", "shasum": "" }, "require": { - "php": "^7.2.5", + "php": ">=7.1.3", + "psr/log": "~1.0", + "symfony/polyfill-php80": "^1.15" + }, + "conflict": { + "symfony/http-kernel": "<3.4" + }, + "require-dev": { + "symfony/http-kernel": "^3.4|^4.0|^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Debug\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Debug Component", + "homepage": "https://symfony.com", + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-05-24T08:33:35+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v5.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "6e4320f06d5f2cce0d96530162491f4465179157" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/6e4320f06d5f2cce0d96530162491f4465179157", + "reference": "6e4320f06d5f2cce0d96530162491f4465179157", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", "symfony/polyfill-ctype": "~1.8" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -7841,29 +9118,45 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "time": "2020-03-27T16:56:45+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-05-30T20:35:19+00:00" }, { "name": "symfony/options-resolver", - "version": "v5.0.7", + "version": "v5.1.2", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "09dccfffd24b311df7f184aa80ee7b61ad61ed8d" + "reference": "663f5dd5e14057d1954fe721f9709d35837f2447" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/09dccfffd24b311df7f184aa80ee7b61ad61ed8d", - "reference": "09dccfffd24b311df7f184aa80ee7b61ad61ed8d", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/663f5dd5e14057d1954fe721f9709d35837f2447", + "reference": "663f5dd5e14057d1954fe721f9709d35837f2447", "shasum": "" }, "require": { - "php": "^7.2.5" + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1", + "symfony/polyfill-php80": "^1.15" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -7895,20 +9188,34 @@ "configuration", "options" ], - "time": "2020-03-27T16:56:45+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-05-23T13:08:13+00:00" }, { "name": "symfony/polyfill-php70", - "version": "v1.15.0", + "version": "v1.17.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php70.git", - "reference": "2a18e37a489803559284416df58c71ccebe50bf0" + "reference": "471b096aede7025bace8eb356b9ac801aaba7e2d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/2a18e37a489803559284416df58c71ccebe50bf0", - "reference": "2a18e37a489803559284416df58c71ccebe50bf0", + "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/471b096aede7025bace8eb356b9ac801aaba7e2d", + "reference": "471b096aede7025bace8eb356b9ac801aaba7e2d", "shasum": "" }, "require": { @@ -7918,7 +9225,11 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.15-dev" + "dev-master": "1.17-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { @@ -7954,30 +9265,44 @@ "portable", "shim" ], - "time": "2020-02-27T09:26:54+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-06-06T08:46:27+00:00" }, { "name": "symfony/stopwatch", - "version": "v5.0.7", + "version": "v5.1.2", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "a1d86d30d4522423afc998f32404efa34fcf5a73" + "reference": "0f7c58cf81dbb5dd67d423a89d577524a2ec0323" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/a1d86d30d4522423afc998f32404efa34fcf5a73", - "reference": "a1d86d30d4522423afc998f32404efa34fcf5a73", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/0f7c58cf81dbb5dd67d423a89d577524a2ec0323", + "reference": "0f7c58cf81dbb5dd67d423a89d577524a2ec0323", "shasum": "" }, "require": { - "php": "^7.2.5", + "php": ">=7.2.5", "symfony/service-contracts": "^1.0|^2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -8004,7 +9329,21 @@ ], "description": "Symfony Stopwatch Component", "homepage": "https://symfony.com", - "time": "2020-03-27T16:56:45+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-05-20T17:43:50+00:00" }, { "name": "theseer/tokenizer", @@ -8053,7 +9392,7 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": ">=7.2", + "php": "^7.2", "ext-mbstring": "*", "ext-pdo_mysql": "*", "ext-zip": "*" diff --git a/config/http.php b/config/http.php new file mode 100644 index 000000000..b39845239 --- /dev/null +++ b/config/http.php @@ -0,0 +1,21 @@ + [ + 'client_period' => 1, + 'client' => env('APP_API_CLIENT_RATELIMIT', 240), + + 'application_period' => 1, + 'application' => env('APP_API_APPLICATION_RATELIMIT', 240), + ], +]; diff --git a/config/logging.php b/config/logging.php index 33b6a5d0e..7da06f407 100644 --- a/config/logging.php +++ b/config/logging.php @@ -3,7 +3,6 @@ use Monolog\Handler\StreamHandler; return [ - /* |-------------------------------------------------------------------------- | Default Log Channel @@ -77,5 +76,4 @@ return [ 'level' => 'debug', ], ], - ]; diff --git a/config/pterodactyl.php b/config/pterodactyl.php index fe4f2372b..70014bc0a 100644 --- a/config/pterodactyl.php +++ b/config/pterodactyl.php @@ -162,6 +162,11 @@ return [ 'enabled' => env('PTERODACTYL_CLIENT_DATABASES_ENABLED', true), 'allow_random' => env('PTERODACTYL_CLIENT_DATABASES_ALLOW_RANDOM', true), ], + + 'schedules' => [ + // The total number of tasks that can exist for any given schedule at once. + 'per_schedule_task_limit' => 10, + ], ], /* @@ -218,5 +223,7 @@ return [ | | 'P_SERVER_CREATED_AT' => 'created_at' */ - 'environment_variables' => [], + 'environment_variables' => [ + 'P_SERVER_ALLOCATION_LIMIT' => 'allocation_limit', + ], ]; diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index b55a92804..6d6208f4f 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -20,9 +20,7 @@ use Pterodactyl\Models\ApiKey; $factory->define(Pterodactyl\Models\Server::class, function (Faker $faker) { return [ - 'id' => $faker->unique()->randomNumber(), - 'node_id' => $faker->randomNumber(), - 'uuid' => $faker->unique()->uuid, + 'uuid' => Uuid::uuid4()->toString(), 'uuidShort' => str_random(8), 'name' => $faker->firstName, 'description' => implode(' ', $faker->sentences()), @@ -34,9 +32,6 @@ $factory->define(Pterodactyl\Models\Server::class, function (Faker $faker) { 'io' => 500, 'cpu' => 0, 'oom_disabled' => 0, - 'allocation_id' => $faker->randomNumber(), - 'nest_id' => $faker->randomNumber(), - 'egg_id' => $faker->randomNumber(), 'pack_id' => null, 'installed' => 1, 'database_limit' => null, @@ -50,7 +45,6 @@ $factory->define(Pterodactyl\Models\User::class, function (Faker $faker) { static $password; return [ - 'id' => $faker->unique()->randomNumber(), 'external_id' => $faker->unique()->isbn10, 'uuid' => $faker->uuid, 'username' => $faker->userName, @@ -74,15 +68,13 @@ $factory->state(Pterodactyl\Models\User::class, 'admin', function () { $factory->define(Pterodactyl\Models\Location::class, function (Faker $faker) { return [ - 'id' => $faker->unique()->randomNumber(), - 'short' => $faker->unique()->domainWord, + 'short' => Str::random(8), 'long' => $faker->catchPhrase, ]; }); $factory->define(Pterodactyl\Models\Node::class, function (Faker $faker) { return [ - 'id' => $faker->unique()->randomNumber(), 'uuid' => Uuid::uuid4()->toString(), 'public' => true, 'name' => $faker->firstName, @@ -95,7 +87,7 @@ $factory->define(Pterodactyl\Models\Node::class, function (Faker $faker) { 'disk_overallocate' => 0, 'upload_size' => 100, 'daemon_token_id' => Str::random(Node::DAEMON_TOKEN_ID_LENGTH), - 'daemon_token' => Str::random(Node::DAEMON_TOKEN_LENGTH), + 'daemon_token' => encrypt(Str::random(Node::DAEMON_TOKEN_LENGTH)), 'daemonListen' => 8080, 'daemonSFTP' => 2022, 'daemonBase' => '/var/lib/pterodactyl/volumes', @@ -104,7 +96,6 @@ $factory->define(Pterodactyl\Models\Node::class, function (Faker $faker) { $factory->define(Pterodactyl\Models\Nest::class, function (Faker $faker) { return [ - 'id' => $faker->unique()->randomNumber(), 'uuid' => $faker->unique()->uuid, 'author' => 'testauthor@example.com', 'name' => $faker->word, @@ -114,9 +105,7 @@ $factory->define(Pterodactyl\Models\Nest::class, function (Faker $faker) { $factory->define(Pterodactyl\Models\Egg::class, function (Faker $faker) { return [ - 'id' => $faker->unique()->randomNumber(), 'uuid' => $faker->unique()->uuid, - 'nest_id' => $faker->unique()->randomNumber(), 'name' => $faker->name, 'description' => implode(' ', $faker->sentences(3)), 'startup' => 'java -jar test.jar', @@ -125,7 +114,6 @@ $factory->define(Pterodactyl\Models\Egg::class, function (Faker $faker) { $factory->define(Pterodactyl\Models\EggVariable::class, function (Faker $faker) { return [ - 'id' => $faker->unique()->randomNumber(), 'name' => $faker->firstName, 'description' => $faker->sentence(), 'env_variable' => strtoupper(str_replace(' ', '_', $faker->words(2, true))), @@ -146,8 +134,6 @@ $factory->state(Pterodactyl\Models\EggVariable::class, 'editable', function () { $factory->define(Pterodactyl\Models\Pack::class, function (Faker $faker) { return [ - 'id' => $faker->unique()->randomNumber(), - 'egg_id' => $faker->randomNumber(), 'uuid' => $faker->uuid, 'name' => $faker->word, 'description' => null, @@ -159,17 +145,11 @@ $factory->define(Pterodactyl\Models\Pack::class, function (Faker $faker) { }); $factory->define(Pterodactyl\Models\Subuser::class, function (Faker $faker) { - return [ - 'id' => $faker->unique()->randomNumber(), - 'user_id' => $faker->randomNumber(), - 'server_id' => $faker->randomNumber(), - ]; + return []; }); $factory->define(Pterodactyl\Models\Allocation::class, function (Faker $faker) { return [ - 'id' => $faker->unique()->randomNumber(), - 'node_id' => $faker->randomNumber(), 'ip' => $faker->ipv4, 'port' => $faker->randomNumber(5), ]; @@ -177,13 +157,11 @@ $factory->define(Pterodactyl\Models\Allocation::class, function (Faker $faker) { $factory->define(Pterodactyl\Models\DatabaseHost::class, function (Faker $faker) { return [ - 'id' => $faker->unique()->randomNumber(), 'name' => $faker->colorName, 'host' => $faker->unique()->ipv4, 'port' => 3306, 'username' => $faker->colorName, 'password' => Crypt::encrypt($faker->word), - 'node_id' => $faker->randomNumber(), ]; }); @@ -191,9 +169,6 @@ $factory->define(Pterodactyl\Models\Database::class, function (Faker $faker) { static $password; return [ - 'id' => $faker->unique()->randomNumber(), - 'server_id' => $faker->randomNumber(), - 'database_host_id' => $faker->randomNumber(), 'database' => str_random(10), 'username' => str_random(10), 'remote' => '%', @@ -205,16 +180,12 @@ $factory->define(Pterodactyl\Models\Database::class, function (Faker $faker) { $factory->define(Pterodactyl\Models\Schedule::class, function (Faker $faker) { return [ - 'id' => $faker->unique()->randomNumber(), - 'server_id' => $faker->randomNumber(), 'name' => $faker->firstName(), ]; }); $factory->define(Pterodactyl\Models\Task::class, function (Faker $faker) { return [ - 'id' => $faker->unique()->randomNumber(), - 'schedule_id' => $faker->randomNumber(), 'sequence_id' => $faker->randomNumber(1), 'action' => 'command', 'payload' => 'test command', @@ -225,9 +196,6 @@ $factory->define(Pterodactyl\Models\Task::class, function (Faker $faker) { $factory->define(Pterodactyl\Models\DaemonKey::class, function (Faker $faker) { return [ - 'id' => $faker->unique()->randomNumber(), - 'server_id' => $faker->randomNumber(), - 'user_id' => $faker->randomNumber(), 'secret' => 'i_' . str_random(40), 'expires_at' => \Carbon\Carbon::now()->addMinutes(10)->toDateTimeString(), ]; @@ -237,8 +205,6 @@ $factory->define(Pterodactyl\Models\ApiKey::class, function (Faker $faker) { static $token; return [ - 'id' => $faker->unique()->randomNumber(), - 'user_id' => $faker->randomNumber(), 'key_type' => ApiKey::TYPE_APPLICATION, 'identifier' => str_random(Pterodactyl\Models\ApiKey::IDENTIFIER_LENGTH), 'token' => $token ?: $token = encrypt(str_random(Pterodactyl\Models\ApiKey::KEY_LENGTH)), diff --git a/database/migrations/2017_07_24_194433_DeleteTaskWhenParentServerIsDeleted.php b/database/migrations/2017_07_24_194433_DeleteTaskWhenParentServerIsDeleted.php index 042e7564c..89e110228 100644 --- a/database/migrations/2017_07_24_194433_DeleteTaskWhenParentServerIsDeleted.php +++ b/database/migrations/2017_07_24_194433_DeleteTaskWhenParentServerIsDeleted.php @@ -23,6 +23,5 @@ class DeleteTaskWhenParentServerIsDeleted extends Migration */ public function down() { - // } } diff --git a/database/migrations/2020_04_17_203438_allow_nullable_descriptions.php b/database/migrations/2020_04_17_203438_allow_nullable_descriptions.php index c1787a9b3..dfd55fb42 100644 --- a/database/migrations/2020_04_17_203438_allow_nullable_descriptions.php +++ b/database/migrations/2020_04_17_203438_allow_nullable_descriptions.php @@ -1,8 +1,8 @@ id(); + $table->unsignedInteger('user_id'); + $table->string('token'); + $table->timestamp('created_at')->nullable(); + + $table->foreign('user_id')->references('id')->on('users')->cascadeOnDelete(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('recovery_tokens'); + } +} diff --git a/database/seeds/EggSeeder.php b/database/seeds/EggSeeder.php index 245d74ac3..c532d556c 100644 --- a/database/seeds/EggSeeder.php +++ b/database/seeds/EggSeeder.php @@ -112,14 +112,16 @@ class EggSeeder extends Seeder $files = $this->filesystem->allFiles(database_path('seeds/eggs/' . kebab_case($nest->name))); $this->command->alert('Updating Eggs for Nest: ' . $nest->name); - collect($files)->each(function ($file) use ($nest) { + Collection::make($files)->each(function ($file) use ($nest) { /* @var \Symfony\Component\Finder\SplFileInfo $file */ $decoded = json_decode($file->getContents()); if (json_last_error() !== JSON_ERROR_NONE) { - return $this->command->error('JSON decode exception for ' . $file->getFilename() . ': ' . json_last_error_msg()); + $this->command->error('JSON decode exception for ' . $file->getFilename() . ': ' . json_last_error_msg()); + + return; } - $file = new UploadedFile($file->getPathname(), $file->getFilename(), 'application/json', $file->getSize()); + $file = new UploadedFile($file->getPathname(), $file->getFilename(), 'application/json'); try { $egg = $this->repository->setColumns('id')->findFirstWhere([ @@ -130,11 +132,11 @@ class EggSeeder extends Seeder $this->updateImporterService->handle($egg->id, $file); - return $this->command->info('Updated ' . $decoded->name); + $this->command->info('Updated ' . $decoded->name); } catch (RecordNotFoundException $exception) { $this->importerService->handle($file, $nest->id); - return $this->command->comment('Created ' . $decoded->name); + $this->command->comment('Created ' . $decoded->name); } }); diff --git a/database/seeds/eggs/voice-servers/egg-mumble-server.json b/database/seeds/eggs/voice-servers/egg-mumble-server.json index e62562597..00c87d21e 100644 --- a/database/seeds/eggs/voice-servers/egg-mumble-server.json +++ b/database/seeds/eggs/voice-servers/egg-mumble-server.json @@ -36,7 +36,7 @@ "name": "Server Version", "description": "Version of Mumble Server to download and use.", "env_variable": "MUMBLE_VERSION", - "default_value": "1.2.19", + "default_value": "1.3.1", "user_viewable": 1, "user_editable": 1, "rules": "required|regex:\/^([0-9_\\.-]{5,8})$\/" diff --git a/public/themes/pterodactyl/css/terminal.css b/public/themes/pterodactyl/css/terminal.css index a9bd3db1b..d2dc4d00f 100644 --- a/public/themes/pterodactyl/css/terminal.css +++ b/public/themes/pterodactyl/css/terminal.css @@ -27,6 +27,7 @@ #terminal > .cmd { padding: 1px 0; word-wrap: break-word; + white-space: pre-wrap; } #terminal_input { diff --git a/resources/scripts/api/account/enableAccountTwoFactor.ts b/resources/scripts/api/account/enableAccountTwoFactor.ts index d44d09acb..e7a15f62d 100644 --- a/resources/scripts/api/account/enableAccountTwoFactor.ts +++ b/resources/scripts/api/account/enableAccountTwoFactor.ts @@ -1,9 +1,7 @@ import http from '@/api/http'; -export default (code: string): Promise => { - return new Promise((resolve, reject) => { - http.post('/api/client/account/two-factor', { code }) - .then(() => resolve()) - .catch(reject); - }); +export default async (code: string): Promise => { + const { data } = await http.post('/api/client/account/two-factor', { code }); + + return data.attributes.tokens; }; diff --git a/resources/scripts/api/auth/loginCheckpoint.ts b/resources/scripts/api/auth/loginCheckpoint.ts index 244d27c81..25bb715a4 100644 --- a/resources/scripts/api/auth/loginCheckpoint.ts +++ b/resources/scripts/api/auth/loginCheckpoint.ts @@ -1,13 +1,14 @@ import http from '@/api/http'; import { LoginResponse } from '@/api/auth/login'; -export default (token: string, code: string): Promise => { +export default (token: string, code: string, recoveryToken?: string): Promise => { return new Promise((resolve, reject) => { http.post('/auth/login/checkpoint', { - // eslint-disable-next-line @typescript-eslint/camelcase + /* eslint-disable @typescript-eslint/camelcase */ confirmation_token: token, - // eslint-disable-next-line @typescript-eslint/camelcase authentication_code: code, + recovery_token: (recoveryToken && recoveryToken.length > 0) ? recoveryToken : undefined, + /* eslint-enable @typescript-eslint/camelcase */ }) .then(response => resolve({ complete: response.data.data.complete, diff --git a/resources/scripts/components/auth/LoginCheckpointContainer.tsx b/resources/scripts/components/auth/LoginCheckpointContainer.tsx index 5534dd0f7..dddcd0c1c 100644 --- a/resources/scripts/components/auth/LoginCheckpointContainer.tsx +++ b/resources/scripts/components/auth/LoginCheckpointContainer.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Link, RouteComponentProps } from 'react-router-dom'; import loginCheckpoint from '@/api/auth/loginCheckpoint'; import { httpErrorToHuman } from '@/api/http'; @@ -14,6 +14,7 @@ import Field from '@/components/elements/Field'; interface Values { code: string; + recoveryCode: '', } type OwnProps = RouteComponentProps<{}, StaticContext, { token?: string }> @@ -24,7 +25,8 @@ type Props = OwnProps & { } const LoginCheckpointContainer = () => { - const { isSubmitting } = useFormikContext(); + const { isSubmitting, setFieldValue } = useFormikContext(); + const [ isMissingDevice, setIsMissingDevice ] = useState(false); return ( {
@@ -54,6 +60,18 @@ const LoginCheckpointContainer = () => { } +
+ { + setFieldValue('code', ''); + setFieldValue('recoveryCode', ''); + setIsMissingDevice(s => !s); + }} + className={'cursor-pointer text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700'} + > + {!isMissingDevice ? 'I\'ve Lost My Device' : 'I Have My Device'} + +
{ }; const EnhancedForm = withFormik({ - handleSubmit: ({ code }, { setSubmitting, props: { addError, clearFlashes, location } }) => { + handleSubmit: ({ code, recoveryCode }, { setSubmitting, props: { addError, clearFlashes, location } }) => { clearFlashes(); - console.log(location.state.token, code); - loginCheckpoint(location.state?.token || '', code) + loginCheckpoint(location.state?.token || '', code, recoveryCode) .then(response => { if (response.complete) { // @ts-ignore @@ -89,11 +106,7 @@ const EnhancedForm = withFormik({ mapPropsToValues: () => ({ code: '', - }), - - validationSchema: object().shape({ - code: string().required('An authentication code must be provided.') - .length(6, 'Authentication code must be 6 digits in length.'), + recoveryCode: '', }), })(LoginCheckpointContainer); diff --git a/resources/scripts/components/dashboard/ServerRow.tsx b/resources/scripts/components/dashboard/ServerRow.tsx index e300e7611..a212bce0c 100644 --- a/resources/scripts/components/dashboard/ServerRow.tsx +++ b/resources/scripts/components/dashboard/ServerRow.tsx @@ -52,6 +52,8 @@ export default ({ server, className }: { server: Server; className: string | und alarms.memory = isAlarmState(stats.memoryUsageInBytes, server.limits.memory); alarms.disk = server.limits.disk === 0 ? false : isAlarmState(stats.diskUsageInBytes, server.limits.disk); } + const disklimit = server.limits.disk != 0 ? bytesToHuman(server.limits.disk * 1000 * 1000) : "Unlimited"; + const memorylimit = server.limits.memory != 0 ? bytesToHuman(server.limits.memory * 1000 * 1000) : "Unlimited"; return ( @@ -127,7 +129,7 @@ export default ({ server, className }: { server: Server; className: string | und {bytesToHuman(stats.memoryUsageInBytes)}

-

of {bytesToHuman(server.limits.memory * 1000 * 1000)}

+

of {memorylimit}

@@ -147,9 +149,7 @@ export default ({ server, className }: { server: Server; className: string | und {bytesToHuman(stats.diskUsageInBytes)}

-

- of {bytesToHuman(server.limits.disk * 1000 * 1000)} -

+

of {disklimit}

} diff --git a/resources/scripts/components/dashboard/forms/SetupTwoFactorModal.tsx b/resources/scripts/components/dashboard/forms/SetupTwoFactorModal.tsx index 028b2b361..ff5eeec7f 100644 --- a/resources/scripts/components/dashboard/forms/SetupTwoFactorModal.tsx +++ b/resources/scripts/components/dashboard/forms/SetupTwoFactorModal.tsx @@ -2,21 +2,22 @@ import React, { useEffect, useState } from 'react'; import Modal, { RequiredModalProps } from '@/components/elements/Modal'; import { Form, Formik, FormikHelpers } from 'formik'; import { object, string } from 'yup'; -import Field from '@/components/elements/Field'; import getTwoFactorTokenUrl from '@/api/account/getTwoFactorTokenUrl'; import enableAccountTwoFactor from '@/api/account/enableAccountTwoFactor'; -import FlashMessageRender from '@/components/FlashMessageRender'; import { Actions, useStoreActions } from 'easy-peasy'; import { ApplicationStore } from '@/state'; import { httpErrorToHuman } from '@/api/http'; +import FlashMessageRender from '@/components/FlashMessageRender'; +import Field from '@/components/elements/Field'; interface Values { code: string; } -export default ({ ...props }: RequiredModalProps) => { +export default ({ onDismissed, ...props }: RequiredModalProps) => { const [ token, setToken ] = useState(''); const [ loading, setLoading ] = useState(true); + const [ recoveryTokens, setRecoveryTokens ] = useState([]); const updateUserData = useStoreActions((actions: Actions) => actions.user.updateUserData); const { addError, clearFlashes } = useStoreActions((actions: Actions) => actions.flashes); @@ -27,22 +28,30 @@ export default ({ ...props }: RequiredModalProps) => { .then(setToken) .catch(error => { console.error(error); + addError({ message: httpErrorToHuman(error), key: 'account:two-factor' }); }); }, []); const submit = ({ code }: Values, { setSubmitting }: FormikHelpers) => { clearFlashes('account:two-factor'); enableAccountTwoFactor(code) - .then(() => { - updateUserData({ useTotp: true }); - props.onDismissed(); + .then(tokens => { + setRecoveryTokens(tokens); }) .catch(error => { console.error(error); addError({ message: httpErrorToHuman(error), key: 'account:two-factor' }); - setSubmitting(false); - }); + }) + .then(() => setSubmitting(false)); + }; + + const dismiss = () => { + if (recoveryTokens.length > 0) { + updateUserData({ useTotp: true }); + } + + onDismissed(); }; return ( @@ -58,47 +67,73 @@ export default ({ ...props }: RequiredModalProps) => { {({ isSubmitting, isValid }) => ( -
- -
-
-
- {!token || !token.length ? - 0 ? + <> +

Two-factor authentication enabled

+

+ Two-factor authentication has been enabled on your account. Should you loose access to + this device you'll need to use on of the codes displayed below in order to access your + account. +

+

+ These codes will not be displayed again. Please take note of them now + by storing them in a secure repository such as a password manager. +

+
+                                {recoveryTokens.map(token => {token})}
+                            
+
+ +
+ + : + + +
+
+
+ {!token || !token.length ? + + : + setLoading(false)} + className={'w-full h-full shadow-none rounded-0'} + /> + } +
+
+
+
+ - : - setLoading(false)} - className={'w-full h-full shadow-none rounded-0'} - /> - } +
+
+ +
-
-
- -
-
- -
-
-
- + + } )} diff --git a/resources/scripts/components/server/ServerConsole.tsx b/resources/scripts/components/server/ServerConsole.tsx index cf6ed5f50..16675b35e 100644 --- a/resources/scripts/components/server/ServerConsole.tsx +++ b/resources/scripts/components/server/ServerConsole.tsx @@ -81,6 +81,9 @@ export default () => { }; }, [ instance, connected ]); + const disklimit = server.limits.disk != 0 ? bytesToHuman(server.limits.disk * 1000 * 1000) : "Unlimited"; + const memorylimit = server.limits.memory != 0 ? bytesToHuman(server.limits.memory * 1000 * 1000) : "Unlimited"; + return (
@@ -112,8 +115,8 @@ export default () => { className={'mr-1'} />  {bytesToHuman(memory)} - / {bytesToHuman(server.limits.memory * 1000 * 1000)} -

+ / {memorylimit} +

{ className={'mr-1'} />  {bytesToHuman(disk)} - / {bytesToHuman(server.limits.disk * 1000 * 1000)} + / {disklimit}

{!server.isInstalling ? diff --git a/resources/scripts/components/server/backups/BackupContainer.tsx b/resources/scripts/components/server/backups/BackupContainer.tsx index 438c201fb..1dbe3070e 100644 --- a/resources/scripts/components/server/backups/BackupContainer.tsx +++ b/resources/scripts/components/server/backups/BackupContainer.tsx @@ -12,7 +12,7 @@ import { ServerContext } from '@/state/server'; import PageContentBlock from '@/components/elements/PageContentBlock'; export default () => { - const { uuid } = useServer(); + const { uuid, featureLimits } = useServer(); const { addError, clearFlashes } = useFlash(); const [ loading, setLoading ] = useState(true); @@ -50,10 +50,22 @@ export default () => { />)}
} + {featureLimits.backups === 0 && +

+ Backups cannot be created for this server. +

+ } + {(featureLimits.backups > 0 && backups.length > 0) && +

+ {backups.length} of {featureLimits.backups} backups have been created for this server. +

+ } + {featureLimits.backups > 0 && featureLimits.backups !== backups.length &&
+ }
); diff --git a/resources/scripts/components/server/databases/DatabasesContainer.tsx b/resources/scripts/components/server/databases/DatabasesContainer.tsx index 7fb60f351..9213347f0 100644 --- a/resources/scripts/components/server/databases/DatabasesContainer.tsx +++ b/resources/scripts/components/server/databases/DatabasesContainer.tsx @@ -59,7 +59,12 @@ export default () => {

} - {featureLimits.databases > 0 && + {(featureLimits.databases > 0 && databases.length > 0) && +

+ {databases.length} of {featureLimits.databases} databases have been allocated to this server. +

+ } + {featureLimits.databases > 0 && featureLimits.databases !== databases.length &&
diff --git a/resources/scripts/components/server/files/FileEditContainer.tsx b/resources/scripts/components/server/files/FileEditContainer.tsx index f314744ef..f24e7bc60 100644 --- a/resources/scripts/components/server/files/FileEditContainer.tsx +++ b/resources/scripts/components/server/files/FileEditContainer.tsx @@ -110,7 +110,7 @@ export default () => { fetchContent={value => { fetchFileContent = value; }} - onContentSaved={() => null} + onContentSaved={() => save()} />
diff --git a/resources/scripts/components/server/files/FileManagerBreadcrumbs.tsx b/resources/scripts/components/server/files/FileManagerBreadcrumbs.tsx index 5e595a2e5..c125dd2c2 100644 --- a/resources/scripts/components/server/files/FileManagerBreadcrumbs.tsx +++ b/resources/scripts/components/server/files/FileManagerBreadcrumbs.tsx @@ -25,10 +25,10 @@ export default ({ withinFileEditor, isNewFile }: Props) => { .filter(directory => !!directory) .map((directory, index, dirs) => { if (!withinFileEditor && index === dirs.length - 1) { - return { name: directory }; + return { name: decodeURIComponent(directory) }; } - return { name: directory, path: `/${dirs.slice(0, index + 1).join('/')}` }; + return { name: decodeURIComponent(directory), path: `/${dirs.slice(0, index + 1).join('/')}` }; }); return ( @@ -57,7 +57,7 @@ export default ({ withinFileEditor, isNewFile }: Props) => { } {file && - {file} + {decodeURIComponent(file)} }
diff --git a/resources/scripts/components/server/files/NewDirectoryButton.tsx b/resources/scripts/components/server/files/NewDirectoryButton.tsx index 86141d213..842cddf8a 100644 --- a/resources/scripts/components/server/files/NewDirectoryButton.tsx +++ b/resources/scripts/components/server/files/NewDirectoryButton.tsx @@ -70,7 +70,12 @@ export default () => { />

This directory will be created as -  /home/container/{join(directory, values.directoryName).replace(/^(\.\.\/|\/)+/, '')} +  /home/container/ + + {decodeURIComponent( + join(directory, values.directoryName).replace(/^(\.\.\/|\/)+/, ''), + )} +

- -
-
MB
+

This server will not be allowed to boot if it is using more than this amount of space. If a server goes over this limit while running it will be safely stopped and locked until enough space is available. Set to 0 to allow unlimited disk usage.

diff --git a/resources/views/admin/servers/view/build.blade.php b/resources/views/admin/servers/view/build.blade.php index c3925c074..c1f8defca 100644 --- a/resources/views/admin/servers/view/build.blade.php +++ b/resources/views/admin/servers/view/build.blade.php @@ -66,7 +66,7 @@ MB
-

This server will not be allowed to boot if it is using more than this amount of space. If a server goes over this limit while running it will be safely stopped and locked until enough space is available.

+

This server will not be allowed to boot if it is using more than this amount of space. If a server goes over this limit while running it will be safely stopped and locked until enough space is available. Set to 0 to allow unlimited disk usage.

diff --git a/resources/views/admin/servers/view/index.blade.php b/resources/views/admin/servers/view/index.blade.php index 637c9bd33..b8c17fb01 100644 --- a/resources/views/admin/servers/view/index.blade.php +++ b/resources/views/admin/servers/view/index.blade.php @@ -97,7 +97,13 @@ Disk Space - {{ $server->disk }}MB + + @if($server->disk === 0) + Unlimited + @else + {{ $server->disk }}MB + @endif + Block IO Weight diff --git a/tests/Integration/Api/Client/AccountControllerTest.php b/tests/Integration/Api/Client/AccountControllerTest.php new file mode 100644 index 000000000..4fbef8749 --- /dev/null +++ b/tests/Integration/Api/Client/AccountControllerTest.php @@ -0,0 +1,162 @@ +create(); + + $response = $this->actingAs($user)->get('/api/client/account'); + + $response->assertOk()->assertJson([ + 'object' => 'user', + 'attributes' => [ + 'id' => $user->id, + 'admin' => false, + 'username' => $user->username, + 'email' => $user->email, + 'first_name' => $user->name_first, + 'last_name' => $user->name_last, + 'language' => $user->language, + ], + ]); + } + + /** + * Test that the user's email address can be updated via the API. + */ + public function testEmailIsUpdated() + { + /** @var \Pterodactyl\Models\User $user */ + $user = factory(User::class)->create(); + + $response = $this->actingAs($user)->putJson('/api/client/account/email', [ + 'email' => 'hodor@example.com', + 'password' => 'password', + ]); + + $response->assertStatus(Response::HTTP_NO_CONTENT); + + $this->assertDatabaseHas('users', ['id' => $user->id, 'email' => 'hodor@example.com']); + } + + /** + * Tests that an email is not updated if the password provided in the reuqest is not + * valid for the account. + */ + public function testEmailIsNotUpdatedWhenPasswordIsInvalid() + { + /** @var \Pterodactyl\Models\User $user */ + $user = factory(User::class)->create(); + + $response = $this->actingAs($user)->putJson('/api/client/account/email', [ + 'email' => 'hodor@example.com', + 'password' => 'invalid', + ]); + + $response->assertStatus(Response::HTTP_BAD_REQUEST); + $response->assertJsonPath('errors.0.code', 'InvalidPasswordProvidedException'); + $response->assertJsonPath('errors.0.detail', 'The password provided was invalid for this account.'); + } + + /** + * Tests that an email is not updated if an invalid email address is passed through + * in the request. + */ + public function testEmailIsNotUpdatedWhenNotValid() + { + /** @var \Pterodactyl\Models\User $user */ + $user = factory(User::class)->create(); + + $response = $this->actingAs($user)->putJson('/api/client/account/email', [ + 'email' => '', + 'password' => 'password', + ]); + + $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); + $response->assertJsonPath('errors.0.code', 'required'); + $response->assertJsonPath('errors.0.detail', 'The email field is required.'); + + $response = $this->actingAs($user)->putJson('/api/client/account/email', [ + 'email' => 'invalid', + 'password' => 'password', + ]); + + $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); + $response->assertJsonPath('errors.0.code', 'email'); + $response->assertJsonPath('errors.0.detail', 'The email must be a valid email address.'); + } + + /** + * Test that the password for an account can be successfully updated. + */ + public function testPasswordIsUpdated() + { + /** @var \Pterodactyl\Models\User $user */ + $user = factory(User::class)->create(); + + $mock = Mockery::mock(AuthManager::class); + $mock->expects('logoutOtherDevices')->with('New_Password1'); + + $this->app->instance(AuthManager::class, $mock); + + $response = $this->actingAs($user)->putJson('/api/client/account/password', [ + 'current_password' => 'password', + 'password' => 'New_Password1', + 'password_confirmation' => 'New_Password1', + ]); + + $response->assertStatus(Response::HTTP_NO_CONTENT); + } + + /** + * Test that the password for an account is not updated if the current password is not + * provided correctly. + */ + public function testPasswordIsNotUpdatedIfCurrentPasswordIsInvalid() + { + /** @var \Pterodactyl\Models\User $user */ + $user = factory(User::class)->create(); + + $response = $this->actingAs($user)->putJson('/api/client/account/password', [ + 'current_password' => 'invalid', + 'password' => 'New_Password1', + 'password_confirmation' => 'New_Password1', + ]); + + $response->assertStatus(Response::HTTP_BAD_REQUEST); + $response->assertJsonPath('errors.0.code', 'InvalidPasswordProvidedException'); + $response->assertJsonPath('errors.0.detail', 'The password provided was invalid for this account.'); + } + + /** + * Test that a validation error is returned if the password passed in the request + * does not have a confirmation, or the confirmation is not the same as the password. + */ + public function testErrorIsReturnedIfPasswordIsNotConfirmed() + { + /** @var \Pterodactyl\Models\User $user */ + $user = factory(User::class)->create(); + + $response = $this->actingAs($user)->putJson('/api/client/account/password', [ + 'current_password' => 'password', + 'password' => 'New_Password1', + 'password_confirmation' => 'Invalid_New_Password', + ]); + + $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); + $response->assertJsonPath('errors.0.code', 'confirmed'); + $response->assertJsonPath('errors.0.detail', 'The password confirmation does not match.'); + } +} diff --git a/tests/Integration/Api/Client/ApiKeyControllerTest.php b/tests/Integration/Api/Client/ApiKeyControllerTest.php new file mode 100644 index 000000000..13a0b9c84 --- /dev/null +++ b/tests/Integration/Api/Client/ApiKeyControllerTest.php @@ -0,0 +1,219 @@ +forceDelete(); + + parent::tearDown(); + } + + /** + * Test that the client's API key can be returned successfully. + */ + public function testApiKeysAreReturned() + { + /** @var \Pterodactyl\Models\User $user */ + $user = factory(User::class)->create(); + /** @var \Pterodactyl\Models\ApiKey $key */ + $key = factory(ApiKey::class)->create([ + 'user_id' => $user->id, + 'key_type' => ApiKey::TYPE_ACCOUNT, + ]); + + $response = $this->actingAs($user)->get('/api/client/account/api-keys'); + + $response->assertOk(); + $response->assertJson([ + 'object' => 'list', + 'data' => [ + [ + 'object' => 'api_key', + 'attributes' => [ + 'identifier' => $key->identifier, + 'description' => $key->memo, + 'allowed_ips' => $key->allowed_ips, + 'last_used_at' => null, + 'created_at' => $key->created_at->toIso8601String(), + ], + ], + ], + ]); + } + + /** + * Test that an API key can be created for the client account. This also checks that the + * API key secret is returned as metadata in the response since it will not be returned + * after that point. + */ + public function testApiKeyCanBeCreatedForAccount() + { + /** @var \Pterodactyl\Models\User $user */ + $user = factory(User::class)->create(); + + // Small sub-test to ensure we're always comparing the number of keys to the + // specific logged in account, and not just the total number of keys stored in + // the database. + factory(ApiKey::class)->times(10)->create([ + 'user_id' => factory(User::class)->create()->id, + 'key_type' => ApiKey::TYPE_ACCOUNT, + ]); + + $response = $this->actingAs($user)->postJson('/api/client/account/api-keys', [ + 'description' => 'Test Description', + 'allowed_ips' => ['127.0.0.1'], + ]); + + $response->assertOk(); + + /** @var \Pterodactyl\Models\ApiKey $key */ + $key = ApiKey::query()->where('identifier', $response->json('attributes.identifier'))->firstOrFail(); + + $response->assertJson([ + 'object' => 'api_key', + 'attributes' => [ + 'identifier' => $key->identifier, + 'description' => 'Test Description', + 'allowed_ips' => ['127.0.0.1'], + 'last_used_at' => null, + 'created_at' => $key->created_at->toIso8601String(), + ], + 'meta' => [ + 'secret_token' => decrypt($key->token), + ], + ]); + } + + /** + * Test that no more than 5 API keys can exist at any one time for an account. This prevents + * a DoS attack vector against the panel. + * + * @see https://github.com/pterodactyl/panel/security/advisories/GHSA-pjmh-7xfm-r4x9 + */ + public function testNoMoreThanFiveApiKeysCanBeCreatedForAnAccount() + { + /** @var \Pterodactyl\Models\User $user */ + $user = factory(User::class)->create(); + factory(ApiKey::class)->times(5)->create([ + 'user_id' => $user->id, + 'key_type' => ApiKey::TYPE_ACCOUNT, + ]); + + $response = $this->actingAs($user)->postJson('/api/client/account/api-keys', [ + 'description' => 'Test Description', + 'allowed_ips' => ['127.0.0.1'], + ]); + + $response->assertStatus(Response::HTTP_BAD_REQUEST); + $response->assertJsonPath('errors.0.code', 'DisplayException'); + $response->assertJsonPath('errors.0.detail', 'You have reached the account limit for number of API keys.'); + } + + /** + * Test that a bad request results in a validation error being returned by the API. + */ + public function testValidationErrorIsReturnedForBadRequests() + { + /** @var \Pterodactyl\Models\User $user */ + $user = factory(User::class)->create(); + + $response = $this->actingAs($user)->postJson('/api/client/account/api-keys', [ + 'description' => '', + 'allowed_ips' => ['127.0.0.1'], + ]); + + $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); + $response->assertJsonPath('errors.0.code', 'required'); + $response->assertJsonPath('errors.0.detail', 'The description field is required.'); + } + + /** + * Tests that an API key can be deleted from the account. + */ + public function testApiKeyCanBeDeleted() + { + /** @var \Pterodactyl\Models\User $user */ + $user = factory(User::class)->create(); + /** @var \Pterodactyl\Models\ApiKey $key */ + $key = factory(ApiKey::class)->create([ + 'user_id' => $user->id, + 'key_type' => ApiKey::TYPE_ACCOUNT, + ]); + + $response = $this->actingAs($user)->delete('/api/client/account/api-keys/' . $key->identifier); + $response->assertStatus(Response::HTTP_NO_CONTENT); + + $this->assertDatabaseMissing('api_keys', ['id' => $key->id]); + } + + /** + * Test that trying to delete an API key that does not exist results in a 404. + */ + public function testNonExistentApiKeyDeletionReturns404Error() + { + /** @var \Pterodactyl\Models\User $user */ + $user = factory(User::class)->create(); + /** @var \Pterodactyl\Models\ApiKey $key */ + $key = factory(ApiKey::class)->create([ + 'user_id' => $user->id, + 'key_type' => ApiKey::TYPE_ACCOUNT, + ]); + + $response = $this->actingAs($user)->delete('/api/client/account/api-keys/1234'); + $response->assertNotFound(); + + $this->assertDatabaseHas('api_keys', ['id' => $key->id]); + } + + /** + * Test that an API key that exists on the system cannot be deleted if the user + * who created it is not the authenticated user. + */ + public function testApiKeyBelongingToAnotherUserCannotBeDeleted() + { + /** @var \Pterodactyl\Models\User $user */ + $user = factory(User::class)->create(); + /** @var \Pterodactyl\Models\User $user2 */ + $user2 = factory(User::class)->create(); + /** @var \Pterodactyl\Models\ApiKey $key */ + $key = factory(ApiKey::class)->create([ + 'user_id' => $user2->id, + 'key_type' => ApiKey::TYPE_ACCOUNT, + ]); + + $response = $this->actingAs($user)->delete('/api/client/account/api-keys/' . $key->identifier); + $response->assertNotFound(); + + $this->assertDatabaseHas('api_keys', ['id' => $key->id]); + } + + /** + * Tests that an application API key also belonging to the logged in user cannot be + * deleted through this endpoint if it exists. + */ + public function testApplicationApiKeyCannotBeDeleted() + { + /** @var \Pterodactyl\Models\User $user */ + $user = factory(User::class)->create(); + /** @var \Pterodactyl\Models\ApiKey $key */ + $key = factory(ApiKey::class)->create([ + 'user_id' => $user->id, + 'key_type' => ApiKey::TYPE_APPLICATION, + ]); + + $response = $this->actingAs($user)->delete('/api/client/account/api-keys/' . $key->identifier); + $response->assertNotFound(); + + $this->assertDatabaseHas('api_keys', ['id' => $key->id]); + } +} diff --git a/tests/Integration/Api/Client/ClientApiIntegrationTestCase.php b/tests/Integration/Api/Client/ClientApiIntegrationTestCase.php new file mode 100644 index 000000000..e6dbf0974 --- /dev/null +++ b/tests/Integration/Api/Client/ClientApiIntegrationTestCase.php @@ -0,0 +1,121 @@ +forceDelete(); + Node::query()->forceDelete(); + Location::query()->forceDelete(); + User::query()->forceDelete(); + + parent::tearDown(); + } + + /** + * Setup tests and ensure all of the times are always the same. + */ + public function setUp(): void + { + parent::setUp(); + + Carbon::setTestNow(Carbon::now()); + CarbonImmutable::setTestNow(Carbon::now()); + } + + /** + * Returns a link to the specific resource using the client API. + * + * @param mixed $model + * @param string|null $append + * @return string + */ + protected function link($model, $append = null): string + { + Assert::isInstanceOfAny($model, [Server::class, Schedule::class, Task::class]); + + $link = ''; + switch (get_class($model)) { + case Server::class: + $link = "/api/client/servers/{$model->uuid}"; + break; + case Schedule::class: + $link = "/api/client/servers/{$model->server->uuid}/schedules/{$model->id}"; + break; + case Task::class: + $link = "/api/client/servers/{$model->schedule->server->uuid}/schedules/{$model->schedule->id}/tasks/{$model->id}"; + break; + } + + return $link . ($append ? '/' . ltrim($append, '/') : ''); + } + + /** + * Generates a user and a server for that user. If an array of permissions is passed it + * is assumed that the user is actually a subuser of the server. + * + * @param string[] $permissions + * @return array + */ + protected function generateTestAccount(array $permissions = []): array + { + /** @var \Pterodactyl\Models\User $user */ + $user = factory(User::class)->create(); + + if (empty($permissions)) { + return [$user, $this->createServerModel(['user_id' => $user->id])]; + } + + $server = $this->createServerModel(); + + Subuser::query()->create([ + 'user_id' => $user->id, + 'server_id' => $server->id, + 'permissions' => $permissions, + ]); + + return [$user, $server]; + } + + /** + * Asserts that the data passed through matches the output of the data from the transformer. This + * will remove the "relationships" key when performing the comparison. + * + * @param array $data + * @param \Pterodactyl\Models\Model|\Illuminate\Database\Eloquent\Model $model + */ + protected function assertJsonTransformedWith(array $data, $model) + { + $reflect = new ReflectionClass($model); + $transformer = sprintf('\\Pterodactyl\\Transformers\\Api\\Client\\%sTransformer', $reflect->getShortName()); + + $transformer = new $transformer; + $this->assertInstanceOf(BaseClientTransformer::class, $transformer); + + $this->assertSame( + $transformer->transform($model), + Collection::make($data)->except(['relationships'])->toArray() + ); + } +} diff --git a/tests/Integration/Api/Client/ClientControllerTest.php b/tests/Integration/Api/Client/ClientControllerTest.php new file mode 100644 index 000000000..1561f59cf --- /dev/null +++ b/tests/Integration/Api/Client/ClientControllerTest.php @@ -0,0 +1,146 @@ +times(3)->create(); + + /** @var \Pterodactyl\Models\Server[] $servers */ + $servers = [ + $this->createServerModel(['user_id' => $users[0]->id]), + $this->createServerModel(['user_id' => $users[1]->id]), + $this->createServerModel(['user_id' => $users[2]->id]), + ]; + + $response = $this->actingAs($users[0])->getJson('/api/client'); + + $response->assertOk(); + $response->assertJsonPath('object', 'list'); + $response->assertJsonPath('data.0.object', Server::RESOURCE_NAME); + $response->assertJsonPath('data.0.attributes.identifier', $servers[0]->uuidShort); + $response->assertJsonPath('data.0.attributes.server_owner', true); + $response->assertJsonPath('meta.pagination.total', 1); + $response->assertJsonPath('meta.pagination.per_page', config('pterodactyl.paginate.frontend.servers')); + } + + /** + * Tests that all of the servers on the system are returned when making the request as an + * administrator and including the ?filter=all parameter in the URL. + */ + public function testFilterIncludeAllServersWhenAdministrator() + { + /** @var \Pterodactyl\Models\User[] $users */ + $users = factory(User::class)->times(3)->create(); + $users[0]->root_admin = true; + + $servers = [ + $this->createServerModel(['user_id' => $users[0]->id]), + $this->createServerModel(['user_id' => $users[1]->id]), + $this->createServerModel(['user_id' => $users[2]->id]), + ]; + + $response = $this->actingAs($users[0])->getJson('/api/client?filter=all'); + + $response->assertOk(); + $response->assertJsonCount(3, 'data'); + + for ($i = 0; $i < 3; $i++) { + $response->assertJsonPath("data.{$i}.attributes.server_owner", $i === 0); + $response->assertJsonPath("data.{$i}.attributes.identifier", $servers[$i]->uuidShort); + } + } + + /** + * Test that servers where the user is a subuser are returned by default in the API call. + */ + public function testServersUserIsASubuserOfAreReturned() + { + /** @var \Pterodactyl\Models\User[] $users */ + $users = factory(User::class)->times(3)->create(); + $servers = [ + $this->createServerModel(['user_id' => $users[0]->id]), + $this->createServerModel(['user_id' => $users[1]->id]), + $this->createServerModel(['user_id' => $users[2]->id]), + ]; + + // Set user 0 as a subuser of server 1. Thus, we should get two servers + // back in the response when making the API call as user 0. + Subuser::query()->create([ + 'user_id' => $users[0]->id, + 'server_id' => $servers[1]->id, + 'permissions' => [Permission::ACTION_WEBSOCKET_CONNECT], + ]); + + $response = $this->actingAs($users[0])->getJson('/api/client'); + + $response->assertOk(); + $response->assertJsonCount(2, 'data'); + $response->assertJsonPath('data.0.attributes.server_owner', true); + $response->assertJsonPath('data.0.attributes.identifier', $servers[0]->uuidShort); + $response->assertJsonPath('data.1.attributes.server_owner', false); + $response->assertJsonPath('data.1.attributes.identifier', $servers[1]->uuidShort); + } + + /** + * Returns only servers that the user owns, not servers they are a subuser of. + */ + public function testFilterOnlyOwnerServers() + { + /** @var \Pterodactyl\Models\User[] $users */ + $users = factory(User::class)->times(3)->create(); + $servers = [ + $this->createServerModel(['user_id' => $users[0]->id]), + $this->createServerModel(['user_id' => $users[1]->id]), + $this->createServerModel(['user_id' => $users[2]->id]), + ]; + + // Set user 0 as a subuser of server 1. Thus, we should get two servers + // back in the response when making the API call as user 0. + Subuser::query()->create([ + 'user_id' => $users[0]->id, + 'server_id' => $servers[1]->id, + 'permissions' => [Permission::ACTION_WEBSOCKET_CONNECT], + ]); + + $response = $this->actingAs($users[0])->getJson('/api/client?filter=owner'); + + $response->assertOk(); + $response->assertJsonCount(1, 'data'); + $response->assertJsonPath('data.0.attributes.server_owner', true); + $response->assertJsonPath('data.0.attributes.identifier', $servers[0]->uuidShort); + } + + /** + * Tests that the permissions from the Panel are returned correctly. + */ + public function testPermissionsAreReturned() + { + /** @var \Pterodactyl\Models\User $user */ + $user = factory(User::class)->create(); + + $this->actingAs($user) + ->getJson('/api/client/permissions') + ->assertOk() + ->assertJson([ + 'object' => 'system_permissions', + 'attributes' => [ + 'permissions' => Permission::permissions()->toArray(), + ], + ]); + } +} diff --git a/tests/Integration/Api/Client/Server/CommandControllerTest.php b/tests/Integration/Api/Client/Server/CommandControllerTest.php new file mode 100644 index 000000000..3d7cd090f --- /dev/null +++ b/tests/Integration/Api/Client/Server/CommandControllerTest.php @@ -0,0 +1,100 @@ +repository = Mockery::mock(DaemonCommandRepository::class); + $this->app->instance(DaemonCommandRepository::class, $this->repository); + } + + /** + * Test that a validation error is returned if there is no command present in the + * request. + */ + public function testValidationErrorIsReturnedIfNoCommandIsPresent() + { + [$user, $server] = $this->generateTestAccount(); + + $response = $this->actingAs($user)->postJson("/api/client/servers/{$server->uuid}/command", [ + 'command' => '', + ]); + + $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); + $response->assertJsonPath('errors.0.code', 'required'); + } + + /** + * Test that a subuser without the required permission receives an error when trying to + * execute the command. + */ + public function testSubuserWithoutPermissionReceivesError() + { + [$user, $server] = $this->generateTestAccount([Permission::ACTION_WEBSOCKET_CONNECT]); + + $response = $this->actingAs($user)->postJson("/api/client/servers/{$server->uuid}/command", [ + 'command' => 'say Test', + ]); + + $response->assertStatus(Response::HTTP_FORBIDDEN); + } + + /** + * Test that a command can be sent to the server. + */ + public function testCommandCanSendToServer() + { + [$user, $server] = $this->generateTestAccount([Permission::ACTION_CONTROL_CONSOLE]); + + $this->repository->expects('setServer')->with(Mockery::on(function ($value) use ($server) { + return $value->uuid === $server->uuid; + }))->andReturnSelf(); + $this->repository->expects('send')->with('say Test')->andReturn(new GuzzleResponse); + + $response = $this->actingAs($user)->postJson("/api/client/servers/{$server->uuid}/command", [ + 'command' => 'say Test', + ]); + + $response->assertStatus(Response::HTTP_NO_CONTENT); + } + + /** + * Test that an error is returned when the server is offline that is more specific than the + * regular daemon connection error. + */ + public function testErrorIsReturnedWhenServerIsOffline() + { + [$user, $server] = $this->generateTestAccount(); + + $this->repository->expects('setServer->send')->andThrows( + new BadResponseException('', new Request('GET', 'test'), new GuzzleResponse(Response::HTTP_BAD_GATEWAY)) + ); + + $response = $this->actingAs($user)->postJson("/api/client/servers/{$server->uuid}/command", [ + 'command' => 'say Test', + ]); + + $response->assertStatus(Response::HTTP_BAD_GATEWAY); + $response->assertJsonPath('errors.0.code', 'HttpException'); + $response->assertJsonPath('errors.0.detail', 'Server must be online in order to send commands.'); + } +} diff --git a/tests/Integration/Api/Client/Server/PowerControllerTest.php b/tests/Integration/Api/Client/Server/PowerControllerTest.php new file mode 100644 index 000000000..80f58010a --- /dev/null +++ b/tests/Integration/Api/Client/Server/PowerControllerTest.php @@ -0,0 +1,107 @@ +generateTestAccount($permissions); + + $this->actingAs($user) + ->postJson("/api/client/servers/{$server->uuid}/power", ['signal' => $action]) + ->assertStatus(Response::HTTP_FORBIDDEN); + } + + /** + * Test that sending an invalid power signal returns an error. + */ + public function testInvalidPowerSignalResultsInError() + { + [$user, $server] = $this->generateTestAccount(); + + $response = $this->actingAs($user)->postJson("/api/client/servers/{$server->uuid}/power", [ + 'signal' => 'invalid', + ]); + + $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); + $response->assertJsonPath('errors.0.code', 'in'); + $response->assertJsonPath('errors.0.detail', 'The selected signal is invalid.'); + } + + /** + * Test that sending a valid power actions works. + * + * @param string $action + * @param string $permission + * @dataProvider validPowerActionDataProvider + */ + public function testActionCanBeSentToServer(string $action, string $permission) + { + $service = Mockery::mock(DaemonPowerRepository::class); + $this->app->instance(DaemonPowerRepository::class, $service); + + [$user, $server] = $this->generateTestAccount([$permission]); + + $service->expects('setServer') + ->with(Mockery::on(function ($value) use ($server) { + return $server->uuid === $value->uuid; + })) + ->andReturnSelf() + ->getMock() + ->expects('send') + ->with(trim($action)); + + $this->actingAs($user) + ->postJson("/api/client/servers/{$server->uuid}/power", ['signal' => $action]) + ->assertStatus(Response::HTTP_NO_CONTENT); + } + + /** + * Returns invalid permission combinations for a given power action. + * + * @return array + */ + public function invalidPermissionDataProvider(): array + { + return [ + ['start', [Permission::ACTION_CONTROL_STOP, Permission::ACTION_CONTROL_RESTART]], + ['stop', [Permission::ACTION_CONTROL_START]], + ['kill', [Permission::ACTION_CONTROL_START, Permission::ACTION_CONTROL_RESTART]], + ['restart', [Permission::ACTION_CONTROL_STOP, Permission::ACTION_CONTROL_START]], + ['random', [Permission::ACTION_CONTROL_START]], + ]; + } + + /** + * @return array + */ + public function validPowerActionDataProvider(): array + { + return [ + ['start', Permission::ACTION_CONTROL_START], + ['stop', Permission::ACTION_CONTROL_STOP], + ['restart', Permission::ACTION_CONTROL_RESTART], + ['kill', Permission::ACTION_CONTROL_STOP], + // Yes, these spaces are intentional. You should be able to send values with or without + // a space on the start/end since we should be trimming the values. + [' restart', Permission::ACTION_CONTROL_RESTART], + ['kill ', Permission::ACTION_CONTROL_STOP], + ]; + } +} diff --git a/tests/Integration/Api/Client/Server/ResourceUtilitizationControllerTest.php b/tests/Integration/Api/Client/Server/ResourceUtilitizationControllerTest.php new file mode 100644 index 000000000..7c713dd33 --- /dev/null +++ b/tests/Integration/Api/Client/Server/ResourceUtilitizationControllerTest.php @@ -0,0 +1,44 @@ +app->instance(DaemonServerRepository::class, $service); + + [$user, $server] = $this->generateTestAccount([Permission::ACTION_WEBSOCKET_CONNECT]); + + $service->expects('setServer')->with(Mockery::on(function ($value) use ($server) { + return $server->uuid === $value->uuid; + }))->andReturnSelf()->getMock()->expects('getDetails')->andReturns([]); + + $response = $this->actingAs($user)->getJson("/api/client/servers/{$server->uuid}/resources"); + + $response->assertOk(); + $response->assertJson([ + 'object' => 'stats', + 'attributes' => [ + 'current_state' => 'stopped', + 'is_suspended' => false, + 'resources' => [ + 'memory_bytes' => 0, + 'cpu_absolute' => 0, + 'disk_bytes' => 0, + 'network_rx_bytes' => 0, + 'network_tx_bytes' => 0, + ], + ], + ]); + } +} diff --git a/tests/Integration/Api/Client/Server/Schedule/CreateServerScheduleTest.php b/tests/Integration/Api/Client/Server/Schedule/CreateServerScheduleTest.php new file mode 100644 index 000000000..5b45e7fe7 --- /dev/null +++ b/tests/Integration/Api/Client/Server/Schedule/CreateServerScheduleTest.php @@ -0,0 +1,96 @@ +generateTestAccount($permissions); + + $response = $this->actingAs($user)->postJson("/api/client/servers/{$server->uuid}/schedules", [ + 'name' => 'Test Schedule', + 'is_active' => false, + 'minute' => '0', + 'hour' => '*/2', + 'day_of_week' => '2', + 'day_of_month' => '*', + ]); + + $response->assertOk(); + + $this->assertNotNull($id = $response->json('attributes.id')); + + /** @var \Pterodactyl\Models\Schedule $schedule */ + $schedule = Schedule::query()->findOrFail($id); + $this->assertFalse($schedule->is_active); + $this->assertFalse($schedule->is_processing); + $this->assertSame('0', $schedule->cron_minute); + $this->assertSame('*/2', $schedule->cron_hour); + $this->assertSame('2', $schedule->cron_day_of_week); + $this->assertSame('*', $schedule->cron_day_of_month); + $this->assertSame('Test Schedule', $schedule->name); + + $this->assertJsonTransformedWith($response->json('attributes'), $schedule); + $response->assertJsonCount(0, 'attributes.relationships.tasks.data'); + } + + /** + * Test that the validation rules for scheduling work as expected. + */ + public function testScheduleValidationRules() + { + [$user, $server] = $this->generateTestAccount(); + + $response = $this->actingAs($user)->postJson("/api/client/servers/{$server->uuid}/schedules", []); + + $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); + foreach (['name', 'minute', 'hour', 'day_of_month', 'day_of_week'] as $i => $field) { + $response->assertJsonPath("errors.{$i}.code", 'required'); + $response->assertJsonPath("errors.{$i}.source.field", $field); + } + + $this->actingAs($user) + ->postJson("/api/client/servers/{$server->uuid}/schedules", [ + 'name' => 'Testing', + 'is_active' => 'no', + 'minute' => '*', + 'hour' => '*', + 'day_of_month' => '*', + 'day_of_week' => '*', + ]) + ->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY) + ->assertJsonPath('errors.0.code', 'boolean'); + } + + /** + * Test that a subuser without required permissions cannot create a schedule. + */ + public function testSubuserCannotCreateScheduleWithoutPermissions() + { + [$user, $server] = $this->generateTestAccount([Permission::ACTION_SCHEDULE_UPDATE]); + + $this->actingAs($user) + ->postJson("/api/client/servers/{$server->uuid}/schedules", []) + ->assertForbidden(); + } + + /** + * @return array + */ + public function permissionsDataProvider(): array + { + return [[[]], [[Permission::ACTION_SCHEDULE_CREATE]]]; + } +} diff --git a/tests/Integration/Api/Client/Server/Schedule/DeleteServerScheduleTest.php b/tests/Integration/Api/Client/Server/Schedule/DeleteServerScheduleTest.php new file mode 100644 index 000000000..cff7591ba --- /dev/null +++ b/tests/Integration/Api/Client/Server/Schedule/DeleteServerScheduleTest.php @@ -0,0 +1,88 @@ +generateTestAccount($permissions); + + $schedule = factory(Schedule::class)->create(['server_id' => $server->id]); + $task = factory(Task::class)->create(['schedule_id' => $schedule->id]); + + $this->actingAs($user) + ->deleteJson("/api/client/servers/{$server->uuid}/schedules/{$schedule->id}") + ->assertStatus(Response::HTTP_NO_CONTENT); + + $this->assertDatabaseMissing('schedules', ['id' => $schedule->id]); + $this->assertDatabaseMissing('tasks', ['id' => $task->id]); + } + + /** + * Test that no error is returned if the schedule does not exist on the system at all. + */ + public function testNotFoundErrorIsReturnedIfScheduleDoesNotExistAtAll() + { + [$user, $server] = $this->generateTestAccount(); + + $this->actingAs($user) + ->deleteJson("/api/client/servers/{$server->uuid}/schedules/123456789") + ->assertStatus(Response::HTTP_NOT_FOUND); + } + + /** + * Ensure that a schedule belonging to another server cannot be deleted and its presence is not + * revealed to the user. + */ + public function testNotFoundErrorIsReturnedIfScheduleDoesNotBelongToServer() + { + [$user, $server] = $this->generateTestAccount(); + [, $server2] = $this->generateTestAccount(['user_id' => $user->id]); + + $schedule = factory(Schedule::class)->create(['server_id' => $server2->id]); + + $this->actingAs($user) + ->deleteJson("/api/client/servers/{$server->uuid}/schedules/{$schedule->id}") + ->assertStatus(Response::HTTP_NOT_FOUND); + + $this->assertDatabaseHas('schedules', ['id' => $schedule->id]); + } + + /** + * Test that an error is returned if the subuser does not have the required permissions to + * delete the schedule from the server. + */ + public function testErrorIsReturnedIfSubuserDoesNotHaveRequiredPermissions() + { + [$user, $server] = $this->generateTestAccount([Permission::ACTION_SCHEDULE_UPDATE]); + + $schedule = factory(Schedule::class)->create(['server_id' => $server->id]); + + $this->actingAs($user) + ->deleteJson("/api/client/servers/{$server->uuid}/schedules/{$schedule->id}") + ->assertStatus(Response::HTTP_FORBIDDEN); + + $this->assertDatabaseHas('schedules', ['id' => $schedule->id]); + } + + /** + * @return array + */ + public function permissionsDataProvider(): array + { + return [[[]], [[Permission::ACTION_SCHEDULE_DELETE]]]; + } +} diff --git a/tests/Integration/Api/Client/Server/Schedule/GetServerSchedulesTest.php b/tests/Integration/Api/Client/Server/Schedule/GetServerSchedulesTest.php new file mode 100644 index 000000000..67be3fd84 --- /dev/null +++ b/tests/Integration/Api/Client/Server/Schedule/GetServerSchedulesTest.php @@ -0,0 +1,106 @@ +forceDelete(); + Schedule::query()->forceDelete(); + + parent::tearDown(); + } + + /** + * Test that schedules for a server are returned. + * + * @param array $permissions + * @param bool $individual + * @dataProvider permissionsDataProvider + */ + public function testServerSchedulesAreReturned($permissions, $individual) + { + [$user, $server] = $this->generateTestAccount($permissions); + + /** @var \Pterodactyl\Models\Schedule $schedule */ + $schedule = factory(Schedule::class)->create(['server_id' => $server->id]); + /** @var \Pterodactyl\Models\Task $task */ + $task = factory(Task::class)->create(['schedule_id' => $schedule->id, 'sequence_id' => 1, 'time_offset' => 0]); + + $response = $this->actingAs($user) + ->getJson( + $individual + ? "/api/client/servers/{$server->uuid}/schedules/{$schedule->id}" + : "/api/client/servers/{$server->uuid}/schedules" + ) + ->assertOk(); + + $prefix = $individual ? '' : 'data.0.'; + if (! $individual) { + $response->assertJsonCount(1, 'data'); + } + + $response->assertJsonCount(1, $prefix . 'attributes.relationships.tasks.data'); + + $response->assertJsonPath($prefix . 'object', Schedule::RESOURCE_NAME); + $response->assertJsonPath($prefix . 'attributes.relationships.tasks.data.0.object', Task::RESOURCE_NAME); + + $this->assertJsonTransformedWith($response->json($prefix . 'attributes'), $schedule); + $this->assertJsonTransformedWith($response->json($prefix . 'attributes.relationships.tasks.data.0.attributes'), $task); + } + + /** + * Test that a schedule belonging to another server cannot be viewed. + */ + public function testScheduleBelongingToAnotherServerCannotBeViewed() + { + [$user, $server] = $this->generateTestAccount(); + [, $server2] = $this->generateTestAccount(['user_id' => $user->id]); + + $schedule = factory(Schedule::class)->create(['server_id' => $server2->id]); + + $this->actingAs($user) + ->getJson("/api/client/servers/{$server->uuid}/schedules/{$schedule->id}") + ->assertNotFound(); + } + + /** + * Test that a subuser without the required permissions is unable to access the schedules endpoint. + */ + public function testUserWithoutPermissionCannotViewSchedules() + { + [$user, $server] = $this->generateTestAccount([Permission::ACTION_WEBSOCKET_CONNECT]); + + $this->actingAs($user) + ->getJson("/api/client/servers/{$server->uuid}/schedules") + ->assertForbidden(); + + $schedule = factory(Schedule::class)->create(['server_id' => $server->id]); + + $this->actingAs($user) + ->getJson("/api/client/servers/{$server->uuid}/schedules/{$schedule->id}") + ->assertForbidden(); + } + + /** + * @return array + */ + public function permissionsDataProvider(): array + { + return [ + [[], false], + [[], true], + [[Permission::ACTION_SCHEDULE_READ], false], + [[Permission::ACTION_SCHEDULE_READ], true], + ]; + } +} diff --git a/tests/Integration/Api/Client/Server/Schedule/UpdateServerScheduleTest.php b/tests/Integration/Api/Client/Server/Schedule/UpdateServerScheduleTest.php new file mode 100644 index 000000000..d2d3132ff --- /dev/null +++ b/tests/Integration/Api/Client/Server/Schedule/UpdateServerScheduleTest.php @@ -0,0 +1,84 @@ +generateTestAccount($permissions); + + /** @var \Pterodactyl\Models\Schedule $schedule */ + $schedule = factory(Schedule::class)->create(['server_id' => $server->id]); + $expected = Utilities::getScheduleNextRunDate('5', '*', '*', '*'); + + $response = $this->actingAs($user) + ->postJson("/api/client/servers/{$server->uuid}/schedules/{$schedule->id}", [ + 'name' => 'Updated Schedule Name', + 'minute' => '5', + 'hour' => '*', + 'day_of_week' => '*', + 'day_of_month' => '*', + 'is_active' => false, + ]); + + $schedule = $schedule->refresh(); + + $response->assertOk(); + $this->assertSame('Updated Schedule Name', $schedule->name); + $this->assertFalse($schedule->is_active); + $this->assertJsonTransformedWith($response->json('attributes'), $schedule); + + $this->assertSame($expected->toIso8601String(), $schedule->next_run_at->toIso8601String()); + } + + /** + * Test that an error is returned if the schedule exists but does not belong to this + * specific server instance. + */ + public function testErrorIsReturnedIfScheduleDoesNotBelongToServer() + { + [$user, $server] = $this->generateTestAccount(); + [, $server2] = $this->generateTestAccount(['user_id' => $user->id]); + + $schedule = factory(Schedule::class)->create(['server_id' => $server2->id]); + + $this->actingAs($user) + ->postJson("/api/client/servers/{$server->uuid}/schedules/{$schedule->id}") + ->assertNotFound(); + } + + /** + * Test that an error is returned if the subuser does not have permission to modify a + * server schedule. + */ + public function testErrorIsReturnedIfSubuserDoesNotHavePermissionToModifySchedule() + { + [$user, $server] = $this->generateTestAccount([Permission::ACTION_SCHEDULE_CREATE]); + + $schedule = factory(Schedule::class)->create(['server_id' => $server->id]); + + $this->actingAs($user) + ->postJson("/api/client/servers/{$server->uuid}/schedules/{$schedule->id}") + ->assertForbidden(); + } + + /** + * @return array + */ + public function permissionsDataProvider(): array + { + return [[[]], [[Permission::ACTION_SCHEDULE_UPDATE]]]; + } +} diff --git a/tests/Integration/Api/Client/Server/ScheduleTask/CreateServerScheduleTaskTest.php b/tests/Integration/Api/Client/Server/ScheduleTask/CreateServerScheduleTaskTest.php new file mode 100644 index 000000000..2044221e5 --- /dev/null +++ b/tests/Integration/Api/Client/Server/ScheduleTask/CreateServerScheduleTaskTest.php @@ -0,0 +1,177 @@ +generateTestAccount($permissions); + + /** @var \Pterodactyl\Models\Schedule $schedule */ + $schedule = factory(Schedule::class)->create(['server_id' => $server->id]); + $this->assertEmpty($schedule->tasks); + + $response = $this->actingAs($user)->postJson($this->link($schedule, '/tasks'), [ + 'action' => 'command', + 'payload' => 'say Test', + 'time_offset' => 10, + 'sequence_id' => 1, + ]); + + $response->assertOk(); + /** @var \Pterodactyl\Models\Task $task */ + $task = Task::query()->findOrFail($response->json('attributes.id')); + + $this->assertSame($schedule->id, $task->schedule_id); + $this->assertSame(1, $task->sequence_id); + $this->assertSame('command', $task->action); + $this->assertSame('say Test', $task->payload); + $this->assertSame(10, $task->time_offset); + $this->assertJsonTransformedWith($response->json('attributes'), $task); + } + + /** + * Test that validation errors are returned correctly if bad data is passed into the API. + */ + public function testValidationErrorsAreReturned() + { + [$user, $server] = $this->generateTestAccount(); + + /** @var \Pterodactyl\Models\Schedule $schedule */ + $schedule = factory(Schedule::class)->create(['server_id' => $server->id]); + + $response = $this->actingAs($user)->postJson($this->link($schedule, '/tasks'))->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); + + foreach (['action', 'payload', 'time_offset'] as $i => $field) { + $response->assertJsonPath("errors.{$i}.code", $field === 'payload' ? 'required_unless' : 'required'); + $response->assertJsonPath("errors.{$i}.source.field", $field); + } + + $this->actingAs($user)->postJson($this->link($schedule, '/tasks'), [ + 'action' => 'hodor', + 'payload' => 'say Test', + 'time_offset' => 0, + ]) + ->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY) + ->assertJsonPath('errors.0.code', 'in') + ->assertJsonPath('errors.0.source.field', 'action'); + + $this->actingAs($user)->postJson($this->link($schedule, '/tasks'), [ + 'action' => 'command', + 'time_offset' => 0, + ]) + ->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY) + ->assertJsonPath('errors.0.code', 'required_unless') + ->assertJsonPath('errors.0.source.field', 'payload'); + + $this->actingAs($user)->postJson($this->link($schedule, '/tasks'), [ + 'action' => 'command', + 'payload' => 'say Test', + 'time_offset' => 0, + 'sequence_id' => 'hodor', + ]) + ->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY) + ->assertJsonPath('errors.0.code', 'numeric') + ->assertJsonPath('errors.0.source.field', 'sequence_id'); + } + + /** + * Test that backups can be tasked out correctly since they do not require a payload. + */ + public function testBackupsCanBeTaskedCorrectly() + { + [$user, $server] = $this->generateTestAccount(); + + /** @var \Pterodactyl\Models\Schedule $schedule */ + $schedule = factory(Schedule::class)->create(['server_id' => $server->id]); + + $this->actingAs($user)->postJson($this->link($schedule, '/tasks'), [ + 'action' => 'backup', + 'time_offset' => 0, + ])->assertOk(); + + $this->actingAs($user)->postJson($this->link($schedule, '/tasks'), [ + 'action' => 'backup', + 'payload' => "file.txt\nfile2.log", + 'time_offset' => 0, + ])->assertOk(); + } + + /** + * Test that an error is returned if the user attempts to create an additional task that + * would put the schedule over the task limit. + */ + public function testErrorIsReturnedIfTooManyTasksExistForSchedule() + { + config()->set('pterodactyl.client_features.schedules.per_schedule_task_limit', 2); + + [$user, $server] = $this->generateTestAccount(); + + /** @var \Pterodactyl\Models\Schedule $schedule */ + $schedule = factory(Schedule::class)->create(['server_id' => $server->id]); + factory(Task::class)->times(2)->create(['schedule_id' => $schedule->id]); + + $this->actingAs($user)->postJson($this->link($schedule, '/tasks'), [ + 'action' => 'command', + 'payload' => 'say test', + 'time_offset' => 0, + ]) + ->assertStatus(Response::HTTP_BAD_REQUEST) + ->assertJsonPath('errors.0.code', 'ServiceLimitExceededException') + ->assertJsonPath('errors.0.detail', 'Schedules may not have more than 2 tasks associated with them. Creating this task would put this schedule over the limit.'); + } + + /** + * Test that an error is returned if the targeted schedule does not belong to the server + * in the request. + */ + public function testErrorIsReturnedIfScheduleDoesNotBelongToServer() + { + [$user, $server] = $this->generateTestAccount(); + [, $server2] = $this->generateTestAccount(['user_id' => $user->id]); + + /** @var \Pterodactyl\Models\Schedule $schedule */ + $schedule = factory(Schedule::class)->create(['server_id' => $server2->id]); + + $this->actingAs($user) + ->postJson("/api/client/servers/{$server->uuid}/schedules/{$schedule->id}/tasks") + ->assertNotFound(); + } + + /** + * Test that an error is returned if the subuser making the request does not have permission + * to update a schedule. + */ + public function testErrorIsReturnedIfSubuserDoesNotHaveScheduleUpdatePermissions() + { + [$user, $server] = $this->generateTestAccount([Permission::ACTION_SCHEDULE_CREATE]); + + /** @var \Pterodactyl\Models\Schedule $schedule */ + $schedule = factory(Schedule::class)->create(['server_id' => $server->id]); + + $this->actingAs($user) + ->postJson($this->link($schedule, '/tasks')) + ->assertForbidden(); + } + + /** + * @return array + */ + public function permissionsDataProvider(): array + { + return [[[]], [[Permission::ACTION_SCHEDULE_UPDATE]]]; + } +} diff --git a/tests/Integration/Api/Client/Server/SettingsControllerTest.php b/tests/Integration/Api/Client/Server/SettingsControllerTest.php new file mode 100644 index 000000000..99c32a56a --- /dev/null +++ b/tests/Integration/Api/Client/Server/SettingsControllerTest.php @@ -0,0 +1,128 @@ +generateTestAccount($permissions); + $originalName = $server->name; + + $response = $this->actingAs($user)->postJson("/api/client/servers/{$server->uuid}/settings/rename", [ + 'name' => '', + ]); + + $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); + $response->assertJsonPath('errors.0.code', 'required'); + + $server = $server->refresh(); + $this->assertSame($originalName, $server->name); + + $this->actingAs($user) + ->postJson("/api/client/servers/{$server->uuid}/settings/rename", [ + 'name' => 'Test Server Name', + ]) + ->assertStatus(Response::HTTP_NO_CONTENT); + + $server = $server->refresh(); + $this->assertSame('Test Server Name', $server->name); + } + + /** + * Test that a subuser receives a permissions error if they do not have the required permission + * and attempt to change the name. + */ + public function testSubuserCannotChangeServerNameWithoutPermission() + { + [$user, $server] = $this->generateTestAccount([Permission::ACTION_WEBSOCKET_CONNECT]); + $originalName = $server->name; + + $this->actingAs($user) + ->postJson("/api/client/servers/{$server->uuid}/settings/rename", [ + 'name' => 'Test Server Name', + ]) + ->assertStatus(Response::HTTP_FORBIDDEN); + + $server = $server->refresh(); + $this->assertSame($originalName, $server->name); + } + + /** + * Test that a server can be reinstalled. Honestly this test doesn't do much of anything other + * than make sure the endpoint works since. + * + * @param array $permissions + * @dataProvider reinstallPermissionsDataProvider + */ + public function testServerCanBeReinstalled($permissions) + { + /** @var \Pterodactyl\Models\Server $server */ + [$user, $server] = $this->generateTestAccount($permissions); + $this->assertSame(Server::STATUS_INSTALLED, $server->installed); + + $service = Mockery::mock(DaemonServerRepository::class); + $this->app->instance(DaemonServerRepository::class, $service); + + $service->expects('setServer') + ->with(Mockery::on(function ($value) use ($server) { + return $value->uuid === $server->uuid; + })) + ->andReturnSelf() + ->getMock() + ->expects('reinstall') + ->andReturnUndefined(); + + $this->actingAs($user)->postJson("/api/client/servers/{$server->uuid}/settings/reinstall") + ->assertStatus(Response::HTTP_ACCEPTED); + + $server = $server->refresh(); + $this->assertSame(Server::STATUS_INSTALLING, $server->installed); + } + + /** + * Test that a subuser receives a permissions error if they do not have the required permission + * and attempt to reinstall a server. + */ + public function testSubuserCannotReinstallServerWithoutPermission() + { + [$user, $server] = $this->generateTestAccount([Permission::ACTION_WEBSOCKET_CONNECT]); + + $this->actingAs($user) + ->postJson("/api/client/servers/{$server->uuid}/settings/reinstall") + ->assertStatus(Response::HTTP_FORBIDDEN); + + $server = $server->refresh(); + $this->assertSame(Server::STATUS_INSTALLED, $server->installed); + } + + /** + * @return array + */ + public function renamePermissionsDataProvider(): array + { + return [[[]], [[Permission::ACTION_SETTINGS_RENAME]]]; + } + + /** + * @return array + */ + public function reinstallPermissionsDataProvider(): array + { + return [[[]], [[Permission::ACTION_SETTINGS_REINSTALL]]]; + } +} diff --git a/tests/Integration/Api/Client/Server/WebsocketControllerTest.php b/tests/Integration/Api/Client/Server/WebsocketControllerTest.php new file mode 100644 index 000000000..0d8851d3a --- /dev/null +++ b/tests/Integration/Api/Client/Server/WebsocketControllerTest.php @@ -0,0 +1,98 @@ +generateTestAccount([Permission::ACTION_CONTROL_RESTART]); + + $this->actingAs($user)->getJson("/api/client/servers/{$server->uuid}/websocket") + ->assertStatus(Response::HTTP_FORBIDDEN) + ->assertJsonPath('errors.0.code', 'HttpException') + ->assertJsonPath('errors.0.detail', 'You do not have permission to connect to this server\'s websocket.'); + } + + /** + * Test that the expected permissions are returned for the server owner and that the JWT is + * configured correctly. + */ + public function testJwtAndWebsocketUrlAreReturnedForServerOwner() + { + CarbonImmutable::setTestNow(Carbon::now()); + + /** @var \Pterodactyl\Models\User $user */ + /** @var \Pterodactyl\Models\Server $server */ + [$user, $server] = $this->generateTestAccount(); + + // Force the node to HTTPS since we want to confirm it gets transformed to wss:// in the URL. + $server->node->scheme = 'https'; + $server->node->save(); + + $response = $this->actingAs($user)->getJson("/api/client/servers/{$server->uuid}/websocket"); + + $response->assertOk(); + $response->assertJsonStructure(['data' => ['token', 'socket']]); + + $connection = $response->json('data.socket'); + $this->assertStringStartsWith('wss://', $connection, 'Failed asserting that websocket connection address has expected "wss://" prefix.'); + $this->assertStringEndsWith("/api/servers/{$server->uuid}/ws", $connection, 'Failed asserting that websocket connection address uses expected Wings endpoint.'); + + $token = (new Parser)->parse($response->json('data.token')); + + $this->assertTrue( + $token->verify(new Sha256, $server->node->getDecryptedKey()), + 'Failed to validate that the JWT data returned was signed using the Node\'s secret key.' + ); + + // Check that the claims are generated correctly. + $this->assertSame(config('app.url'), $token->getClaim('iss')); + $this->assertSame($server->node->getConnectionAddress(), $token->getClaim('aud')); + $this->assertSame(CarbonImmutable::now()->getTimestamp(), $token->getClaim('iat')); + $this->assertSame(CarbonImmutable::now()->subMinutes(5)->getTimestamp(), $token->getClaim('nbf')); + $this->assertSame(CarbonImmutable::now()->addMinutes(15)->getTimestamp(), $token->getClaim('exp')); + $this->assertSame($user->id, $token->getClaim('user_id')); + $this->assertSame($server->uuid, $token->getClaim('server_uuid')); + $this->assertSame(['*'], $token->getClaim('permissions')); + } + + /** + * Test that the subuser's permissions are passed along correctly in the generated JWT. + */ + public function testJwtIsConfiguredCorrectlyForServerSubuser() + { + $permissions = [Permission::ACTION_WEBSOCKET_CONNECT, Permission::ACTION_CONTROL_CONSOLE]; + + /** @var \Pterodactyl\Models\User $user */ + /** @var \Pterodactyl\Models\Server $server */ + [$user, $server] = $this->generateTestAccount($permissions); + + $response = $this->actingAs($user)->getJson("/api/client/servers/{$server->uuid}/websocket"); + + $response->assertOk(); + $response->assertJsonStructure(['data' => ['token', 'socket']]); + + $token = (new Parser)->parse($response->json('data.token')); + + $this->assertTrue( + $token->verify(new Sha256, $server->node->getDecryptedKey()), + 'Failed to validate that the JWT data returned was signed using the Node\'s secret key.' + ); + + // Check that the claims are generated correctly. + $this->assertSame($permissions, $token->getClaim('permissions')); + } +} diff --git a/tests/Integration/Api/Client/TwoFactorControllerTest.php b/tests/Integration/Api/Client/TwoFactorControllerTest.php new file mode 100644 index 000000000..8344d2b96 --- /dev/null +++ b/tests/Integration/Api/Client/TwoFactorControllerTest.php @@ -0,0 +1,147 @@ +create(['use_totp' => false]); + + $this->assertFalse($user->use_totp); + $this->assertEmpty($user->totp_secret); + $this->assertEmpty($user->totp_authenticated_at); + + $response = $this->actingAs($user)->getJson('/api/client/account/two-factor'); + + $response->assertOk(); + $response->assertJsonStructure(['data' => ['image_url_data']]); + + $user = $user->refresh(); + + $this->assertFalse($user->use_totp); + $this->assertNotEmpty($user->totp_secret); + $this->assertEmpty($user->totp_authenticated_at); + } + + /** + * Test that an error is returned if the user's account already has 2FA enabled on it. + */ + public function testErrorIsReturnedWhenTwoFactorIsAlreadyEnabled() + { + /** @var \Pterodactyl\Models\User $user */ + $user = factory(User::class)->create(['use_totp' => true]); + + $response = $this->actingAs($user)->getJson('/api/client/account/two-factor'); + + $response->assertStatus(Response::HTTP_BAD_REQUEST); + $response->assertJsonPath('errors.0.code', 'BadRequestHttpException'); + $response->assertJsonPath('errors.0.detail', 'Two-factor authentication is already enabled on this account.'); + } + + /** + * Test that a validation error is thrown if invalid data is passed to the 2FA endpoint. + */ + public function testValidationErrorIsReturnedIfInvalidDataIsPassedToEnabled2FA() + { + /** @var \Pterodactyl\Models\User $user */ + $user = factory(User::class)->create(['use_totp' => false]); + + $response = $this->actingAs($user)->postJson('/api/client/account/two-factor', [ + 'code' => '', + ]); + + $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); + $response->assertJsonPath('errors.0.code', 'required'); + } + + /** + * Tests that 2FA can be enabled on an account for the user. + */ + public function testTwoFactorCanBeEnabledOnAccount() + { + /** @var \Pterodactyl\Models\User $user */ + $user = factory(User::class)->create(['use_totp' => false]); + + // Make the initial call to get the account setup for 2FA. + $this->actingAs($user)->getJson('/api/client/account/two-factor')->assertOk(); + + $user = $user->refresh(); + $this->assertNotNull($user->totp_secret); + + /** @var \PragmaRX\Google2FA\Google2FA $service */ + $service = $this->app->make(Google2FA::class); + + $secret = decrypt($user->totp_secret); + $token = $service->getCurrentOtp($secret); + + $response = $this->actingAs($user)->postJson('/api/client/account/two-factor', [ + 'code' => $token, + ]); + + $response->assertStatus(Response::HTTP_NO_CONTENT); + + $user = $user->refresh(); + + $this->assertTrue($user->use_totp); + } + + /** + * Test that two factor authentication can be disabled on an account as long as the password + * provided is valid for the account. + */ + public function testTwoFactorCanBeDisabledOnAccount() + { + Carbon::setTestNow(Carbon::now()); + + /** @var \Pterodactyl\Models\User $user */ + $user = factory(User::class)->create(['use_totp' => true]); + + $response = $this->actingAs($user)->deleteJson('/api/client/account/two-factor', [ + 'password' => 'invalid', + ]); + + $response->assertStatus(Response::HTTP_BAD_REQUEST); + $response->assertJsonPath('errors.0.code', 'BadRequestHttpException'); + $response->assertJsonPath('errors.0.detail', 'The password provided was not valid.'); + + $response = $this->actingAs($user)->deleteJson('/api/client/account/two-factor', [ + 'password' => 'password', + ]); + + $response->assertStatus(Response::HTTP_NO_CONTENT); + + $user = $user->refresh(); + $this->assertFalse($user->use_totp); + $this->assertNotNull($user->totp_authenticated_at); + $this->assertSame(Carbon::now()->toIso8601String(), $user->totp_authenticated_at->toIso8601String()); + } + + /** + * Test that no error is returned when trying to disabled two factor on an account where it + * was not enabled in the first place. + */ + public function testNoErrorIsReturnedIfTwoFactorIsNotEnabled() + { + Carbon::setTestNow(Carbon::now()); + + /** @var \Pterodactyl\Models\User $user */ + $user = factory(User::class)->create(['use_totp' => false]); + + $response = $this->actingAs($user)->deleteJson('/api/client/account/two-factor', [ + 'password' => 'password', + ]); + + $response->assertStatus(Response::HTTP_NO_CONTENT); + } +} diff --git a/tests/Integration/IntegrationTestCase.php b/tests/Integration/IntegrationTestCase.php index cb81ef6c2..ee7229660 100644 --- a/tests/Integration/IntegrationTestCase.php +++ b/tests/Integration/IntegrationTestCase.php @@ -5,10 +5,13 @@ namespace Pterodactyl\Tests\Integration; use Tests\TestCase; use Cake\Chronos\Chronos; use Illuminate\Database\Eloquent\Model; +use Tests\Traits\Integration\CreatesTestModels; use Pterodactyl\Transformers\Api\Application\BaseTransformer; abstract class IntegrationTestCase extends TestCase { + use CreatesTestModels; + /** * Setup base integration test cases. */ diff --git a/tests/Traits/Http/IntegrationJsonRequestAssertions.php b/tests/Traits/Http/IntegrationJsonRequestAssertions.php index 471085db0..c7cce5248 100644 --- a/tests/Traits/Http/IntegrationJsonRequestAssertions.php +++ b/tests/Traits/Http/IntegrationJsonRequestAssertions.php @@ -3,14 +3,14 @@ namespace Tests\Traits\Http; use Illuminate\Http\Response; -use Illuminate\Foundation\Testing\TestResponse; +use Illuminate\Testing\TestResponse; trait IntegrationJsonRequestAssertions { /** * Make assertions about a 404 response on the API. * - * @param \Illuminate\Foundation\Testing\TestResponse $response + * @param \Illuminate\Testing\TestResponse $response */ public function assertNotFoundJson(TestResponse $response) { @@ -31,7 +31,7 @@ trait IntegrationJsonRequestAssertions /** * Make assertions about a 403 error returned by the API. * - * @param \Illuminate\Foundation\Testing\TestResponse $response + * @param \Illuminate\Testing\TestResponse $response */ public function assertAccessDeniedJson(TestResponse $response) { diff --git a/tests/Unit/Commands/Environment/EmailSettingsCommandTest.php b/tests/Unit/Commands/Environment/EmailSettingsCommandTest.php index 63489c157..d52b7b3fd 100644 --- a/tests/Unit/Commands/Environment/EmailSettingsCommandTest.php +++ b/tests/Unit/Commands/Environment/EmailSettingsCommandTest.php @@ -1,11 +1,4 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Tests\Unit\Commands\Environment; @@ -58,7 +51,7 @@ class EmailSettingsCommandTest extends CommandTestCase $display = $this->runCommand($this->command, [], array_values($data)); $this->assertNotEmpty($display); - $this->assertContains('Updating stored environment configuration file.', $display); + $this->assertStringContainsString('Updating stored environment configuration file.', $display); } /** @@ -90,7 +83,7 @@ class EmailSettingsCommandTest extends CommandTestCase ]); $this->assertNotEmpty($display); - $this->assertContains('Updating stored environment configuration file.', $display); + $this->assertStringContainsString('Updating stored environment configuration file.', $display); } /** @@ -115,7 +108,7 @@ class EmailSettingsCommandTest extends CommandTestCase $display = $this->runCommand($this->command, ['--driver' => 'mail'], array_values($data)); $this->assertNotEmpty($display); - $this->assertContains('Updating stored environment configuration file.', $display); + $this->assertStringContainsString('Updating stored environment configuration file.', $display); } /** @@ -136,7 +129,7 @@ class EmailSettingsCommandTest extends CommandTestCase $display = $this->runCommand($this->command, [], array_values($data)); $this->assertNotEmpty($display); - $this->assertContains('Updating stored environment configuration file.', $display); + $this->assertStringContainsString('Updating stored environment configuration file.', $display); } /** @@ -164,7 +157,7 @@ class EmailSettingsCommandTest extends CommandTestCase ]); $this->assertNotEmpty($display); - $this->assertContains('Updating stored environment configuration file.', $display); + $this->assertStringContainsString('Updating stored environment configuration file.', $display); } /** @@ -184,7 +177,7 @@ class EmailSettingsCommandTest extends CommandTestCase $display = $this->runCommand($this->command, [], array_values($data)); $this->assertNotEmpty($display); - $this->assertContains('Updating stored environment configuration file.', $display); + $this->assertStringContainsString('Updating stored environment configuration file.', $display); } /** @@ -210,7 +203,7 @@ class EmailSettingsCommandTest extends CommandTestCase ]); $this->assertNotEmpty($display); - $this->assertContains('Updating stored environment configuration file.', $display); + $this->assertStringContainsString('Updating stored environment configuration file.', $display); } /** @@ -235,7 +228,7 @@ class EmailSettingsCommandTest extends CommandTestCase ]); $this->assertNotEmpty($display); - $this->assertContains('Updating stored environment configuration file.', $display); + $this->assertStringContainsString('Updating stored environment configuration file.', $display); } /** @@ -264,7 +257,7 @@ class EmailSettingsCommandTest extends CommandTestCase ]); $this->assertNotEmpty($display); - $this->assertContains('Updating stored environment configuration file.', $display); + $this->assertStringContainsString('Updating stored environment configuration file.', $display); } /** diff --git a/tests/Unit/Commands/Location/DeleteLocationCommandTest.php b/tests/Unit/Commands/Location/DeleteLocationCommandTest.php index 8b292c3f6..26bd73319 100644 --- a/tests/Unit/Commands/Location/DeleteLocationCommandTest.php +++ b/tests/Unit/Commands/Location/DeleteLocationCommandTest.php @@ -63,7 +63,7 @@ class DeleteLocationCommandTest extends CommandTestCase $display = $this->runCommand($this->command, [], [$location2->short]); $this->assertNotEmpty($display); - $this->assertContains(trans('command/messages.location.deleted'), $display); + $this->assertStringContainsString(trans('command/messages.location.deleted'), $display); } /** @@ -84,7 +84,7 @@ class DeleteLocationCommandTest extends CommandTestCase ]); $this->assertNotEmpty($display); - $this->assertContains(trans('command/messages.location.deleted'), $display); + $this->assertStringContainsString(trans('command/messages.location.deleted'), $display); } /** @@ -103,8 +103,8 @@ class DeleteLocationCommandTest extends CommandTestCase $display = $this->runCommand($this->command, [], ['123_not_exist', 'another_not_exist', $location2->short]); $this->assertNotEmpty($display); - $this->assertContains(trans('command/messages.location.no_location_found'), $display); - $this->assertContains(trans('command/messages.location.deleted'), $display); + $this->assertStringContainsString(trans('command/messages.location.no_location_found'), $display); + $this->assertStringContainsString(trans('command/messages.location.deleted'), $display); } /** @@ -123,6 +123,6 @@ class DeleteLocationCommandTest extends CommandTestCase $display = $this->withoutInteraction()->runCommand($this->command, ['--short' => 'randomTestString']); $this->assertNotEmpty($display); - $this->assertContains(trans('command/messages.location.no_location_found'), $display); + $this->assertStringContainsString(trans('command/messages.location.no_location_found'), $display); } } diff --git a/tests/Unit/Commands/Location/MakeLocationCommandTest.php b/tests/Unit/Commands/Location/MakeLocationCommandTest.php index 48823acfa..6b628a6f0 100644 --- a/tests/Unit/Commands/Location/MakeLocationCommandTest.php +++ b/tests/Unit/Commands/Location/MakeLocationCommandTest.php @@ -55,7 +55,7 @@ class MakeLocationCommandTest extends CommandTestCase $display = $this->runCommand($this->command, [], [$location->short, $location->long]); $this->assertNotEmpty($display); - $this->assertContains(trans('command/messages.location.created', [ + $this->assertStringContainsString(trans('command/messages.location.created', [ 'name' => $location->short, 'id' => $location->id, ]), $display); @@ -79,7 +79,7 @@ class MakeLocationCommandTest extends CommandTestCase ]); $this->assertNotEmpty($display); - $this->assertContains(trans('command/messages.location.created', [ + $this->assertStringContainsString(trans('command/messages.location.created', [ 'name' => $location->short, 'id' => $location->id, ]), $display); diff --git a/tests/Unit/Commands/Maintenance/CleanServiceBackupFilesCommandTest.php b/tests/Unit/Commands/Maintenance/CleanServiceBackupFilesCommandTest.php index ca3e5b74f..ecb412e52 100644 --- a/tests/Unit/Commands/Maintenance/CleanServiceBackupFilesCommandTest.php +++ b/tests/Unit/Commands/Maintenance/CleanServiceBackupFilesCommandTest.php @@ -54,7 +54,7 @@ class CleanServiceBackupFilesCommandTest extends CommandTestCase $display = $this->runCommand($this->getCommand()); $this->assertNotEmpty($display); - $this->assertContains(trans('command/messages.maintenance.deleting_service_backup', ['file' => 'testfile.txt']), $display); + $this->assertStringContainsString(trans('command/messages.maintenance.deleting_service_backup', ['file' => 'testfile.txt']), $display); } /** diff --git a/tests/Unit/Commands/Schedule/ProcessRunnableCommandTest.php b/tests/Unit/Commands/Schedule/ProcessRunnableCommandTest.php index d483ede05..5efbef6c4 100644 --- a/tests/Unit/Commands/Schedule/ProcessRunnableCommandTest.php +++ b/tests/Unit/Commands/Schedule/ProcessRunnableCommandTest.php @@ -1,11 +1,4 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Tests\Unit\Commands\Schedule; @@ -64,7 +57,7 @@ class ProcessRunnableCommandTest extends CommandTestCase $display = $this->runCommand($this->command); $this->assertNotEmpty($display); - $this->assertContains(trans('command/messages.schedule.output_line', [ + $this->assertStringContainsString(trans('command/messages.schedule.output_line', [ 'schedule' => $schedule->name, 'hash' => $schedule->hashid, ]), $display); @@ -83,7 +76,7 @@ class ProcessRunnableCommandTest extends CommandTestCase $display = $this->runCommand($this->command); $this->assertNotEmpty($display); - $this->assertNotContains(trans('command/messages.schedule.output_line', [ + $this->assertStringNotContainsString(trans('command/messages.schedule.output_line', [ 'schedule' => $schedule->name, 'hash' => $schedule->hashid, ]), $display); @@ -101,7 +94,7 @@ class ProcessRunnableCommandTest extends CommandTestCase $display = $this->runCommand($this->command); $this->assertNotEmpty($display); - $this->assertNotContains(trans('command/messages.schedule.output_line', [ + $this->assertStringNotContainsString(trans('command/messages.schedule.output_line', [ 'schedule' => $schedule->name, 'hash' => $schedule->hashid, ]), $display); diff --git a/tests/Unit/Commands/Server/BulkPowerActionCommandTest.php b/tests/Unit/Commands/Server/BulkPowerActionCommandTest.php index 7bac15d70..d1ba90cf4 100644 --- a/tests/Unit/Commands/Server/BulkPowerActionCommandTest.php +++ b/tests/Unit/Commands/Server/BulkPowerActionCommandTest.php @@ -6,21 +6,23 @@ use Mockery as m; use Pterodactyl\Models\Node; use GuzzleHttp\Psr7\Response; use Pterodactyl\Models\Server; +use Illuminate\Support\Collection; use Illuminate\Validation\Factory; use Tests\Unit\Commands\CommandTestCase; +use Illuminate\Validation\ValidationException; +use Pterodactyl\Repositories\Wings\DaemonPowerRepository; use Pterodactyl\Console\Commands\Server\BulkPowerActionCommand; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; -use Pterodactyl\Contracts\Repository\Daemon\PowerRepositoryInterface; class BulkPowerActionCommandTest extends CommandTestCase { /** - * @var \Pterodactyl\Contracts\Repository\Daemon\PowerRepositoryInterface|\Mockery\Mock + * @var \Mockery\MockInterface */ private $powerRepository; /** - * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface|\Mockery\Mock + * @var \Mockery\MockInterface */ private $repository; @@ -31,7 +33,7 @@ class BulkPowerActionCommandTest extends CommandTestCase { parent::setUp(); - $this->powerRepository = m::mock(PowerRepositoryInterface::class); + $this->powerRepository = m::mock(DaemonPowerRepository::class); $this->repository = m::mock(ServerRepositoryInterface::class); } @@ -47,28 +49,18 @@ class BulkPowerActionCommandTest extends CommandTestCase $server->setRelation('node', factory(Node::class)->make()); } - $this->repository->shouldReceive('getServersForPowerActionCount') - ->once() - ->with([], []) - ->andReturn(2); - - $this->repository->shouldReceive('getServersForPowerAction') - ->once() - ->with([], []) - ->andReturn($servers); + $this->repository->expects('getServersForPowerActionCount')->with([], [])->andReturn(2); + $this->repository->expects('getServersForPowerAction')->with([], [])->andReturn($servers); for ($i = 0; $i < count($servers); $i++) { - $this->powerRepository->shouldReceive('setNode->setServer->sendSignal') - ->once() - ->with('kill') - ->andReturnNull(); + $this->powerRepository->expects('setNode->setServer->send')->with('kill')->andReturnNull(); } $display = $this->runCommand($this->getCommand(), ['action' => 'kill'], ['yes']); $this->assertNotEmpty($display); - $this->assertContains('2/2', $display); - $this->assertContains(trans('command/messages.server.power.confirm', ['action' => 'kill', 'count' => 2]), $display); + $this->assertStringContainsString('2/2', $display); + $this->assertStringContainsString(trans('command/messages.server.power.confirm', ['action' => 'kill', 'count' => 2]), $display); } /** @@ -79,19 +71,17 @@ class BulkPowerActionCommandTest extends CommandTestCase $server = factory(Server::class)->make(); $server->setRelation('node', $node = factory(Node::class)->make()); - $this->repository->shouldReceive('getServersForPowerActionCount') - ->once() + $this->repository->expects('getServersForPowerActionCount') ->with([1, 2], [3, 4]) ->andReturn(1); - $this->repository->shouldReceive('getServersForPowerAction') - ->once() + $this->repository->expects('getServersForPowerAction') ->with([1, 2], [3, 4]) - ->andReturn([$server]); + ->andReturn(Collection::make([$server])); $this->powerRepository->expects('setNode')->with($node)->andReturnSelf(); $this->powerRepository->expects('setServer')->with($server)->andReturnSelf(); - $this->powerRepository->expects('sendSignal')->with('kill')->andReturn(new Response); + $this->powerRepository->expects('send')->with('kill')->andReturn(new Response); $display = $this->runCommand($this->getCommand(), [ 'action' => 'kill', @@ -100,8 +90,8 @@ class BulkPowerActionCommandTest extends CommandTestCase ], ['yes']); $this->assertNotEmpty($display); - $this->assertContains('1/1', $display); - $this->assertContains(trans('command/messages.server.power.confirm', ['action' => 'kill', 'count' => 1]), $display); + $this->assertStringContainsString('1/1', $display); + $this->assertStringContainsString(trans('command/messages.server.power.confirm', ['action' => 'kill', 'count' => 1]), $display); } /** @@ -112,13 +102,12 @@ class BulkPowerActionCommandTest extends CommandTestCase $server = factory(Server::class)->make(); $server->setRelation('node', factory(Node::class)->make()); - $this->repository->shouldReceive('getServersForPowerActionCount') - ->once() + $this->repository->expects('getServersForPowerActionCount') ->with([], []) ->andReturn(1); - $this->repository->shouldReceive('getServersForPowerAction')->once()->with([], [])->andReturn([$server]); - $this->powerRepository->shouldReceive('setNode->setServer->sendSignal')->once()->with('kill')->andReturnNull(); + $this->repository->expects('getServersForPowerAction')->with([], [])->andReturn(Collection::make([$server])); + $this->powerRepository->expects('setNode->setServer->send')->with('kill')->andReturnNull(); $display = $this->runCommand($this->getCommand(), [ 'action' => 'kill', @@ -127,8 +116,8 @@ class BulkPowerActionCommandTest extends CommandTestCase ], ['yes']); $this->assertNotEmpty($display); - $this->assertContains('1/1', $display); - $this->assertContains(trans('command/messages.server.power.confirm', ['action' => 'kill', 'count' => 1]), $display); + $this->assertStringContainsString('1/1', $display); + $this->assertStringContainsString(trans('command/messages.server.power.confirm', ['action' => 'kill', 'count' => 1]), $display); } /** @@ -137,10 +126,10 @@ class BulkPowerActionCommandTest extends CommandTestCase * @param array $data * * @dataProvider validationFailureDataProvider - * @expectedException \Illuminate\Validation\ValidationException */ public function testValidationErrors(array $data) { + $this->expectException(ValidationException::class); $this->runCommand($this->getCommand(), $data); } diff --git a/tests/Unit/Commands/User/DeleteUserCommandTest.php b/tests/Unit/Commands/User/DeleteUserCommandTest.php index 77516bbf7..8263c2241 100644 --- a/tests/Unit/Commands/User/DeleteUserCommandTest.php +++ b/tests/Unit/Commands/User/DeleteUserCommandTest.php @@ -63,7 +63,7 @@ class DeleteUserCommandTest extends CommandTestCase $this->assertTableContains($user1->id, $display); $this->assertTableContains($user1->email, $display); $this->assertTableContains($user1->name, $display); - $this->assertContains(trans('command/messages.user.deleted'), $display); + $this->assertStringContainsString(trans('command/messages.user.deleted'), $display); } /** @@ -84,11 +84,11 @@ class DeleteUserCommandTest extends CommandTestCase $display = $this->runCommand($this->command, [], ['noResults', $user1->username, $user1->id, 'yes']); $this->assertNotEmpty($display); - $this->assertContains(trans('command/messages.user.no_users_found'), $display); + $this->assertStringContainsString(trans('command/messages.user.no_users_found'), $display); $this->assertTableContains($user1->id, $display); $this->assertTableContains($user1->email, $display); $this->assertTableContains($user1->name, $display); - $this->assertContains(trans('command/messages.user.deleted'), $display); + $this->assertStringContainsString(trans('command/messages.user.deleted'), $display); } /** @@ -107,11 +107,11 @@ class DeleteUserCommandTest extends CommandTestCase $display = $this->runCommand($this->command, [], [$user1->username, 0, $user1->username, $user1->id, 'yes']); $this->assertNotEmpty($display); - $this->assertContains(trans('command/messages.user.select_search_user'), $display); + $this->assertStringContainsString(trans('command/messages.user.select_search_user'), $display); $this->assertTableContains($user1->id, $display); $this->assertTableContains($user1->email, $display); $this->assertTableContains($user1->name, $display); - $this->assertContains(trans('command/messages.user.deleted'), $display); + $this->assertStringContainsString(trans('command/messages.user.deleted'), $display); } /** @@ -130,7 +130,7 @@ class DeleteUserCommandTest extends CommandTestCase $display = $this->runCommand($this->command, [], [$user1->username, $user1->id, 'no']); $this->assertNotEmpty($display); - $this->assertNotContains(trans('command/messages.user.deleted'), $display); + $this->assertStringNotContainsString(trans('command/messages.user.deleted'), $display); } /** @@ -149,7 +149,7 @@ class DeleteUserCommandTest extends CommandTestCase $display = $this->withoutInteraction()->runCommand($this->command, ['--user' => $user1->username]); $this->assertNotEmpty($display); - $this->assertContains(trans('command/messages.user.deleted'), $display); + $this->assertStringContainsString(trans('command/messages.user.deleted'), $display); } /** @@ -169,7 +169,7 @@ class DeleteUserCommandTest extends CommandTestCase $display = $this->withoutInteraction()->runCommand($this->command, ['--user' => $user1->username]); $this->assertNotEmpty($display); - $this->assertContains(trans('command/messages.user.multiple_found'), $display); + $this->assertStringContainsString(trans('command/messages.user.multiple_found'), $display); } /** @@ -183,6 +183,6 @@ class DeleteUserCommandTest extends CommandTestCase $display = $this->withoutInteraction()->runCommand($this->command, ['--user' => 123456]); $this->assertNotEmpty($display); - $this->assertContains(trans('command/messages.user.no_users_found'), $display); + $this->assertStringContainsString(trans('command/messages.user.no_users_found'), $display); } } diff --git a/tests/Unit/Commands/User/DisableTwoFactorCommandTest.php b/tests/Unit/Commands/User/DisableTwoFactorCommandTest.php index 28c9377ff..f2868883d 100644 --- a/tests/Unit/Commands/User/DisableTwoFactorCommandTest.php +++ b/tests/Unit/Commands/User/DisableTwoFactorCommandTest.php @@ -58,7 +58,7 @@ class DisableTwoFactorCommandTest extends CommandTestCase $display = $this->runCommand($this->command, [], [$user->email]); $this->assertNotEmpty($display); - $this->assertContains(trans('command/messages.user.2fa_disabled', ['email' => $user->email]), $display); + $this->assertStringContainsString(trans('command/messages.user.2fa_disabled', ['email' => $user->email]), $display); } /** @@ -78,6 +78,6 @@ class DisableTwoFactorCommandTest extends CommandTestCase $display = $this->withoutInteraction()->runCommand($this->command, ['--email' => $user->email]); $this->assertNotEmpty($display); - $this->assertContains(trans('command/messages.user.2fa_disabled', ['email' => $user->email]), $display); + $this->assertStringContainsString(trans('command/messages.user.2fa_disabled', ['email' => $user->email]), $display); } } diff --git a/tests/Unit/Commands/User/MakeUserCommandTest.php b/tests/Unit/Commands/User/MakeUserCommandTest.php index d67e90ab5..89baa3323 100644 --- a/tests/Unit/Commands/User/MakeUserCommandTest.php +++ b/tests/Unit/Commands/User/MakeUserCommandTest.php @@ -61,12 +61,12 @@ class MakeUserCommandTest extends CommandTestCase ]); $this->assertNotEmpty($display); - $this->assertContains(trans('command/messages.user.ask_password_help'), $display); - $this->assertContains($user->uuid, $display); - $this->assertContains($user->email, $display); - $this->assertContains($user->username, $display); - $this->assertContains($user->name, $display); - $this->assertContains('Yes', $display); + $this->assertStringContainsString(trans('command/messages.user.ask_password_help'), $display); + $this->assertStringContainsString($user->uuid, $display); + $this->assertStringContainsString($user->email, $display); + $this->assertStringContainsString($user->username, $display); + $this->assertStringContainsString($user->name, $display); + $this->assertStringContainsString('Yes', $display); } /** @@ -90,7 +90,7 @@ class MakeUserCommandTest extends CommandTestCase ]); $this->assertNotEmpty($display); - $this->assertNotContains(trans('command/messages.user.ask_password_help'), $display); + $this->assertStringNotContainsString(trans('command/messages.user.ask_password_help'), $display); } /** @@ -119,11 +119,11 @@ class MakeUserCommandTest extends CommandTestCase ]); $this->assertNotEmpty($display); - $this->assertNotContains(trans('command/messages.user.ask_password_help'), $display); - $this->assertContains($user->uuid, $display); - $this->assertContains($user->email, $display); - $this->assertContains($user->username, $display); - $this->assertContains($user->name, $display); - $this->assertContains('No', $display); + $this->assertStringNotContainsString(trans('command/messages.user.ask_password_help'), $display); + $this->assertStringContainsString($user->uuid, $display); + $this->assertStringContainsString($user->email, $display); + $this->assertStringContainsString($user->username, $display); + $this->assertStringContainsString($user->name, $display); + $this->assertStringContainsString('No', $display); } } diff --git a/tests/Unit/Http/Controllers/Admin/StatisticsControllerTest.php b/tests/Unit/Http/Controllers/Admin/StatisticsControllerTest.php deleted file mode 100644 index d9ea3ec28..000000000 --- a/tests/Unit/Http/Controllers/Admin/StatisticsControllerTest.php +++ /dev/null @@ -1,110 +0,0 @@ -allocationRepository = m::mock(AllocationRepositoryInterface::class); - $this->databaseRepository = m::mock(DatabaseRepositoryInterface::class); - $this->eggRepository = m::mock(EggRepositoryInterface::class); - $this->nodeRepository = m::mock(NodeRepositoryInterface::class); - $this->serverRepository = m::mock(ServerRepositoryInterface::class); - $this->userRepository = m::mock(UserRepositoryInterface::class); - } - - public function testIndexController() - { - $controller = $this->getController(); - - $this->serverRepository->shouldReceive('all')->withNoArgs(); - $this->nodeRepository->shouldReceive('all')->withNoArgs()->andReturn(collect([factory(Node::class)->make(), factory(Node::class)->make()])); - $this->userRepository->shouldReceive('count')->withNoArgs(); - $this->eggRepository->shouldReceive('count')->withNoArgs(); - $this->databaseRepository->shouldReceive('count')->withNoArgs(); - $this->allocationRepository->shouldReceive('count')->withNoArgs(); - $this->serverRepository->shouldReceive('getSuspendedServersCount')->withNoArgs(); - - $this->nodeRepository->shouldReceive('getUsageStatsRaw')->twice()->andReturn([ - 'memory' => [ - 'value' => 1024, - 'max' => 512, - ], - 'disk' => [ - 'value' => 1024, - 'max' => 512, - ], - ]); - - $controller->shouldReceive('injectJavascript')->once(); - - $response = $controller->index(); - - $this->assertIsViewResponse($response); - $this->assertViewNameEquals('admin.statistics', $response); - } - - private function getController() - { - return $this->buildMockedController(StatisticsController::class, [$this->allocationRepository, - $this->databaseRepository, - $this->eggRepository, - $this->nodeRepository, - $this->serverRepository, - $this->userRepository, ] - ); - } -} diff --git a/tests/Unit/Http/Controllers/Base/AccountControllerTest.php b/tests/Unit/Http/Controllers/Base/AccountControllerTest.php deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/Unit/Http/Controllers/Base/IndexControllerTest.php b/tests/Unit/Http/Controllers/Base/IndexControllerTest.php deleted file mode 100644 index 7be401ac2..000000000 --- a/tests/Unit/Http/Controllers/Base/IndexControllerTest.php +++ /dev/null @@ -1,181 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Tests\Unit\Http\Controllers\Base; - -use Mockery as m; -use Pterodactyl\Models\User; -use GuzzleHttp\Psr7\Response; -use Pterodactyl\Models\Server; -use GuzzleHttp\Psr7\ServerRequest; -use GuzzleHttp\Exception\ConnectException; -use GuzzleHttp\Exception\RequestException; -use Tests\Assertions\ControllerAssertionsTrait; -use Tests\Unit\Http\Controllers\ControllerTestCase; -use Pterodactyl\Http\Controllers\Base\IndexController; -use Illuminate\Contracts\Pagination\LengthAwarePaginator; -use Symfony\Component\HttpKernel\Exception\HttpException; -use Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService; -use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; -use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; - -class IndexControllerTest extends ControllerTestCase -{ - use ControllerAssertionsTrait; - - /** - * @var \Pterodactyl\Http\Controllers\Base\IndexController - */ - protected $controller; - - /** - * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface|\Mockery\Mock - */ - protected $daemonRepository; - - /** - * @var \Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService|\Mockery\Mock - */ - protected $keyProviderService; - - /** - * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface|\Mockery\Mock - */ - protected $repository; - - /** - * Setup tests. - */ - public function setUp(): void - { - parent::setUp(); - - $this->daemonRepository = m::mock(DaemonServerRepositoryInterface::class); - $this->keyProviderService = m::mock(DaemonKeyProviderService::class); - $this->repository = m::mock(ServerRepositoryInterface::class); - - $this->controller = new IndexController($this->keyProviderService, $this->daemonRepository, $this->repository); - } - - /** - * Test the index controller. - */ - public function testIndexController() - { - $paginator = m::mock(LengthAwarePaginator::class); - $model = $this->generateRequestUserModel(); - - $this->request->shouldReceive('input')->with('query')->once()->andReturn('searchTerm'); - $this->repository->shouldReceive('setSearchTerm')->with('searchTerm')->once()->andReturnSelf() - ->shouldReceive('filterUserAccessServers')->with($model, User::FILTER_LEVEL_ALL, config('pterodactyl.paginate.frontend.servers')) - ->once()->andReturn($paginator); - - $response = $this->controller->index($this->request); - $this->assertIsViewResponse($response); - $this->assertViewNameEquals('templates.base.core', $response); - $this->assertViewHasKey('servers', $response); - $this->assertViewKeyEquals('servers', $paginator, $response); - } - - /** - * Test the status controller. - */ - public function testStatusController() - { - $user = $this->generateRequestUserModel(); - $server = factory(Server::class)->make(['suspended' => 0, 'installed' => 1]); - $psrResponse = new Response; - - $this->repository->shouldReceive('findFirstWhere')->with([['uuidShort', '=', $server->uuidShort]])->once()->andReturn($server); - $this->keyProviderService->shouldReceive('handle')->with($server, $user)->once()->andReturn('test123'); - - $this->daemonRepository->shouldReceive('setServer')->with($server)->once()->andReturnSelf() - ->shouldReceive('setToken')->with('test123')->once()->andReturnSelf() - ->shouldReceive('details')->withNoArgs()->once()->andReturn($psrResponse); - - $response = $this->controller->status($this->request, $server->uuidShort); - $this->assertIsJsonResponse($response); - $this->assertResponseJsonEquals(json_encode($psrResponse->getBody()), $response); - } - - /** - * Test the status controller if a server is not installed. - */ - public function testStatusControllerWhenServerNotInstalled() - { - $user = $this->generateRequestUserModel(); - $server = factory(Server::class)->make(['suspended' => 0, 'installed' => 0]); - - $this->repository->shouldReceive('findFirstWhere')->with([['uuidShort', '=', $server->uuidShort]])->once()->andReturn($server); - $this->keyProviderService->shouldReceive('handle')->with($server, $user)->once()->andReturn('test123'); - - $response = $this->controller->status($this->request, $server->uuidShort); - $this->assertIsJsonResponse($response); - $this->assertResponseCodeEquals(200, $response); - $this->assertResponseJsonEquals(['status' => 20], $response); - } - - /** - * Test the status controller when a server is suspended. - */ - public function testStatusControllerWhenServerIsSuspended() - { - $user = factory(User::class)->make(); - $server = factory(Server::class)->make(['suspended' => 1, 'installed' => 1]); - - $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn($user); - $this->repository->shouldReceive('findFirstWhere')->with([['uuidShort', '=', $server->uuidShort]])->once()->andReturn($server); - $this->keyProviderService->shouldReceive('handle')->with($server, $user)->once()->andReturn('test123'); - - $response = $this->controller->status($this->request, $server->uuidShort); - $this->assertIsJsonResponse($response); - $this->assertResponseCodeEquals(200, $response); - $this->assertResponseJsonEquals(['status' => 30], $response); - } - - /** - * Test the status controller with a ServerConnectionException. - */ - public function testStatusControllerWithServerConnectionException() - { - $user = factory(User::class)->make(); - $server = factory(Server::class)->make(['suspended' => 0, 'installed' => 1]); - - $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn($user); - $this->repository->shouldReceive('findFirstWhere')->with([['uuidShort', '=', $server->uuidShort]])->once()->andReturn($server); - $this->keyProviderService->shouldReceive('handle')->with($server, $user)->once()->andReturn('test123'); - - $this->daemonRepository->shouldReceive('setServer')->with($server)->once()->andReturnSelf() - ->shouldReceive('setToken')->with('test123')->once()->andReturnSelf() - ->shouldReceive('details')->withNoArgs()->once()->andThrow(new ConnectException('bad connection', new ServerRequest('', ''))); - - $this->expectExceptionObject(new HttpException(500, 'bad connection')); - $this->controller->status($this->request, $server->uuidShort); - } - - /** - * Test the status controller with a RequestException. - */ - public function testStatusControllerWithRequestException() - { - $user = factory(User::class)->make(); - $server = factory(Server::class)->make(['suspended' => 0, 'installed' => 1]); - - $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn($user); - $this->repository->shouldReceive('findFirstWhere')->with([['uuidShort', '=', $server->uuidShort]])->once()->andReturn($server); - $this->keyProviderService->shouldReceive('handle')->with($server, $user)->once()->andReturn('test123'); - - $this->daemonRepository->shouldReceive('setServer')->with($server)->once()->andReturnSelf() - ->shouldReceive('setToken')->with('test123')->once()->andReturnSelf() - ->shouldReceive('details')->withNoArgs()->once()->andThrow(new RequestException('bad request', new ServerRequest('', ''))); - - $this->expectExceptionObject(new HttpException(500, 'bad request')); - $this->controller->status($this->request, $server->uuidShort); - } -} diff --git a/tests/Unit/Http/Controllers/Base/SecurityControllerTest.php b/tests/Unit/Http/Controllers/Base/SecurityControllerTest.php deleted file mode 100644 index fb79d3b3e..000000000 --- a/tests/Unit/Http/Controllers/Base/SecurityControllerTest.php +++ /dev/null @@ -1,156 +0,0 @@ -alert = m::mock(AlertsMessageBag::class); - $this->config = m::mock(Repository::class); - $this->repository = m::mock(SessionRepositoryInterface::class); - $this->toggleTwoFactorService = m::mock(ToggleTwoFactorService::class); - $this->twoFactorSetupService = m::mock(TwoFactorSetupService::class); - } - - /** - * Test TOTP generation controller. - */ - public function testIndexWithout2FactorEnabled() - { - $model = $this->generateRequestUserModel(['use_totp' => 0]); - - $this->twoFactorSetupService->shouldReceive('handle')->with($model)->once()->andReturn(new Collection([ - 'image' => 'test-image', - 'secret' => 'secret-code', - ])); - - $response = $this->getController()->index($this->request); - $this->assertIsJsonResponse($response); - $this->assertResponseCodeEquals(Response::HTTP_OK, $response); - $this->assertResponseJsonEquals(['enabled' => false, 'qr_image' => 'test-image', 'secret' => 'secret-code'], $response); - $this->assertResponseJsonEquals(['qrImage' => 'https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=qrCodeImage'], $response); - } - - /** - * Test TOTP setting controller when no exception is thrown by the service. - */ - public function testIndexWith2FactorEnabled() - { - $this->generateRequestUserModel(['use_totp' => 1]); - - $response = $this->getController()->index($this->request); - $this->assertIsJsonResponse($response); - $this->assertResponseCodeEquals(Response::HTTP_OK, $response); - $this->assertResponseJsonEquals(['enabled' => true], $response); - } - - /** - * Test that a 2FA token can be stored or deleted. - * - * @param string $func - * @dataProvider functionCallDataProvider - */ - public function testStore(string $func) - { - $model = $this->generateRequestUserModel(); - - $this->mockRequestInput('token', 'some-token'); - - if ($func === 'delete') { - $this->toggleTwoFactorService->shouldReceive('handle')->with($model, 'some-token', false); - } else { - $this->toggleTwoFactorService->shouldReceive('handle')->with($model, 'some-token'); - } - - $response = $this->getController()->{$func}($this->request); - $this->assertIsJsonResponse($response); - $this->assertResponseCodeEquals(Response::HTTP_OK, $response); - $this->assertResponseJsonEquals(['success' => true], $response); - } - - /** - * Test an invalid token exception is handled. - * - * @param string $func - * @dataProvider functionCallDataProvider - */ - public function testStoreWithInvalidTokenException(string $func) - { - $this->generateRequestUserModel(); - - $this->mockRequestInput('token'); - $this->toggleTwoFactorService->shouldReceive('handle')->andThrow(new TwoFactorAuthenticationTokenInvalid); - - $response = $this->getController()->{$func}($this->request); - $this->assertIsJsonResponse($response); - $this->assertResponseCodeEquals(Response::HTTP_OK, $response); - $this->assertResponseJsonEquals(['success' => false], $response); - } - - /** - * @return array - */ - public function functionCallDataProvider() - { - return [['store'], ['delete']]; - } - - /** - * Return an instance of the controller for testing with mocked dependencies. - * - * @return \Pterodactyl\Http\Controllers\Base\SecurityController - */ - private function getController(): SecurityController - { - return new SecurityController( - $this->alert, - $this->config, - $this->repository, - $this->toggleTwoFactorService, - $this->twoFactorSetupService - ); - } -} diff --git a/tests/Unit/Http/Middleware/AdminAuthenticateTest.php b/tests/Unit/Http/Middleware/AdminAuthenticateTest.php index eee9a6969..efe6e8212 100644 --- a/tests/Unit/Http/Middleware/AdminAuthenticateTest.php +++ b/tests/Unit/Http/Middleware/AdminAuthenticateTest.php @@ -4,6 +4,7 @@ namespace Tests\Unit\Http\Middleware; use Pterodactyl\Models\User; use Pterodactyl\Http\Middleware\AdminAuthenticate; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; class AdminAuthenticateTest extends MiddlewareTestCase { @@ -21,11 +22,11 @@ class AdminAuthenticateTest extends MiddlewareTestCase /** * Test that a missing user in the request triggers an error. - * - * @expectedException \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException */ public function testExceptionIsThrownIfUserDoesNotExist() { + $this->expectException(AccessDeniedHttpException::class); + $this->request->shouldReceive('user')->withNoArgs()->once()->andReturnNull(); $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); @@ -33,11 +34,11 @@ class AdminAuthenticateTest extends MiddlewareTestCase /** * Test that an exception is thrown if the user is not an admin. - * - * @expectedException \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException */ public function testExceptionIsThrownIfUserIsNotAnAdmin() { + $this->expectException(AccessDeniedHttpException::class); + $user = factory(User::class)->make(['root_admin' => 0]); $this->request->shouldReceive('user')->withNoArgs()->twice()->andReturn($user); diff --git a/tests/Unit/Http/Middleware/Api/Application/AuthenticateUserTest.php b/tests/Unit/Http/Middleware/Api/Application/AuthenticateUserTest.php index 7c0cfc9e7..3cbd7debf 100644 --- a/tests/Unit/Http/Middleware/Api/Application/AuthenticateUserTest.php +++ b/tests/Unit/Http/Middleware/Api/Application/AuthenticateUserTest.php @@ -1,19 +1,20 @@ expectException(AccessDeniedHttpException::class); + $this->setRequestUserModel(null); $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); @@ -21,11 +22,11 @@ class AuthenticateUserTest extends MiddlewareTestCase /** * Test that a non-admin user results an an exception. - * - * @expectedException \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException */ public function testNonAdminUser() { + $this->expectException(AccessDeniedHttpException::class); + $this->generateRequestUserModel(['root_admin' => false]); $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); diff --git a/tests/Unit/Http/Middleware/API/AuthenticateIPAccessTest.php b/tests/Unit/Http/Middleware/Api/AuthenticateIPAccessTest.php similarity index 92% rename from tests/Unit/Http/Middleware/API/AuthenticateIPAccessTest.php rename to tests/Unit/Http/Middleware/Api/AuthenticateIPAccessTest.php index 967fae0a3..8d47fdd54 100644 --- a/tests/Unit/Http/Middleware/API/AuthenticateIPAccessTest.php +++ b/tests/Unit/Http/Middleware/Api/AuthenticateIPAccessTest.php @@ -1,10 +1,11 @@ expectException(AccessDeniedHttpException::class); + $model = factory(ApiKey::class)->make(['allowed_ips' => '["127.0.0.1"]']); $this->setRequestAttribute('api_key', $model); diff --git a/tests/Unit/Http/Middleware/API/AuthenticateKeyTest.php b/tests/Unit/Http/Middleware/Api/AuthenticateKeyTest.php similarity index 96% rename from tests/Unit/Http/Middleware/API/AuthenticateKeyTest.php rename to tests/Unit/Http/Middleware/Api/AuthenticateKeyTest.php index 2b1b91a90..79715e4c8 100644 --- a/tests/Unit/Http/Middleware/API/AuthenticateKeyTest.php +++ b/tests/Unit/Http/Middleware/Api/AuthenticateKeyTest.php @@ -1,6 +1,6 @@ expectException(AccessDeniedHttpException::class); + $this->request->shouldReceive('bearerToken')->withNoArgs()->twice()->andReturn('abcd1234'); $this->repository->shouldReceive('findFirstWhere')->andThrow(new RecordNotFoundException); @@ -141,11 +142,11 @@ class AuthenticateKeyTest extends MiddlewareTestCase /** * Test that a valid token identifier with an invalid token attached to it * triggers an exception. - * - * @expectedException \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException */ public function testInvalidTokenForIdentifier() { + $this->expectException(AccessDeniedHttpException::class); + $model = factory(ApiKey::class)->make(); $this->request->shouldReceive('bearerToken')->withNoArgs()->twice()->andReturn($model->identifier . 'asdf'); diff --git a/tests/Unit/Http/Middleware/Api/Daemon/DaemonAuthenticateTest.php b/tests/Unit/Http/Middleware/Api/Daemon/DaemonAuthenticateTest.php index f5de32679..35699eb65 100644 --- a/tests/Unit/Http/Middleware/Api/Daemon/DaemonAuthenticateTest.php +++ b/tests/Unit/Http/Middleware/Api/Daemon/DaemonAuthenticateTest.php @@ -4,19 +4,27 @@ namespace Tests\Unit\Http\Middleware\Api\Daemon; use Mockery as m; use Pterodactyl\Models\Node; +use Illuminate\Contracts\Encryption\Encrypter; use Tests\Unit\Http\Middleware\MiddlewareTestCase; +use Pterodactyl\Repositories\Eloquent\NodeRepository; use Symfony\Component\HttpKernel\Exception\HttpException; -use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Http\Middleware\Api\Daemon\DaemonAuthenticate; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; class DaemonAuthenticateTest extends MiddlewareTestCase { /** - * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface|\Mockery\Mock + * @var \Mockery\MockInterface */ private $repository; + /** + * @var \Mockery\MockInterface + */ + private $encrypter; + /** * Setup tests. */ @@ -24,7 +32,8 @@ class DaemonAuthenticateTest extends MiddlewareTestCase { parent::setUp(); - $this->repository = m::mock(NodeRepositoryInterface::class); + $this->encrypter = m::mock(Encrypter::class); + $this->repository = m::mock(NodeRepository::class); } /** @@ -33,7 +42,7 @@ class DaemonAuthenticateTest extends MiddlewareTestCase */ public function testResponseShouldContinueIfRouteIsExempted() { - $this->request->shouldReceive('route->getName')->withNoArgs()->once()->andReturn('daemon.configuration'); + $this->request->expects('route->getName')->withNoArgs()->andReturn('daemon.configuration'); $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); } @@ -44,8 +53,8 @@ class DaemonAuthenticateTest extends MiddlewareTestCase */ public function testResponseShouldFailIfNoTokenIsProvided() { - $this->request->shouldReceive('route->getName')->withNoArgs()->once()->andReturn('random.route'); - $this->request->shouldReceive('bearerToken')->withNoArgs()->once()->andReturnNull(); + $this->request->expects('route->getName')->withNoArgs()->andReturn('random.route'); + $this->request->expects('bearerToken')->withNoArgs()->andReturnNull(); try { $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); @@ -58,17 +67,54 @@ class DaemonAuthenticateTest extends MiddlewareTestCase } /** - * Test that passing in an invalid node daemon secret will result in a HTTP/403 - * error response. + * Test that passing in an invalid node daemon secret will result in a bad request + * exception being returned. * - * @expectedException \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + * @param string $token + * @dataProvider badTokenDataProvider */ - public function testResponseShouldFailIfNoNodeIsFound() + public function testResponseShouldFailIfTokenFormatIsIncorrect(string $token) { - $this->request->shouldReceive('route->getName')->withNoArgs()->once()->andReturn('random.route'); - $this->request->shouldReceive('bearerToken')->withNoArgs()->once()->andReturn('test1234'); + $this->expectException(BadRequestHttpException::class); - $this->repository->shouldReceive('findFirstWhere')->with([['daemonSecret', '=', 'test1234']])->once()->andThrow(new RecordNotFoundException); + $this->request->expects('route->getName')->withNoArgs()->andReturn('random.route'); + $this->request->expects('bearerToken')->withNoArgs()->andReturn($token); + + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + } + + /** + * Test that an access denied error is returned if the node is valid but the token + * provided is not valid. + */ + public function testResponseShouldFailIfTokenIsNotValid() + { + $this->expectException(AccessDeniedHttpException::class); + + /** @var \Pterodactyl\Models\Node $model */ + $model = factory(Node::class)->make(); + + $this->request->expects('route->getName')->withNoArgs()->andReturn('random.route'); + $this->request->expects('bearerToken')->withNoArgs()->andReturn($model->daemon_token_id . '.random_string_123'); + + $this->repository->expects('findFirstWhere')->with(['daemon_token_id' => $model->daemon_token_id])->andReturn($model); + $this->encrypter->expects('decrypt')->with($model->daemon_token)->andReturns(decrypt($model->daemon_token)); + + $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); + } + + /** + * Test that an access denied exception is returned if the node is not found using + * the token ID provided. + */ + public function testResponseShouldFailIfNodeIsNotFound() + { + $this->expectException(AccessDeniedHttpException::class); + + $this->request->expects('route->getName')->withNoArgs()->andReturn('random.route'); + $this->request->expects('bearerToken')->withNoArgs()->andReturn('abcd1234.random_string_123'); + + $this->repository->expects('findFirstWhere')->with(['daemon_token_id' => 'abcd1234'])->andThrow(RecordNotFoundException::class); $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); } @@ -78,18 +124,39 @@ class DaemonAuthenticateTest extends MiddlewareTestCase */ public function testSuccessfulMiddlewareProcess() { + /** @var \Pterodactyl\Models\Node $model */ $model = factory(Node::class)->make(); - $this->request->shouldReceive('route->getName')->withNoArgs()->once()->andReturn('random.route'); - $this->request->shouldReceive('bearerToken')->withNoArgs()->once()->andReturn($model->daemonSecret); + $this->request->expects('route->getName')->withNoArgs()->andReturn('random.route'); + $this->request->expects('bearerToken')->withNoArgs()->andReturn($model->daemon_token_id . '.' . decrypt($model->daemon_token)); - $this->repository->shouldReceive('findFirstWhere')->with([['daemonSecret', '=', $model->daemonSecret]])->once()->andReturn($model); + $this->repository->expects('findFirstWhere')->with(['daemon_token_id' => $model->daemon_token_id])->andReturn($model); + $this->encrypter->expects('decrypt')->with($model->daemon_token)->andReturns(decrypt($model->daemon_token)); $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); $this->assertRequestHasAttribute('node'); $this->assertRequestAttributeEquals($model, 'node'); } + /** + * Provides different tokens that should trigger a bad request exception due to + * their formatting. + * + * @return array|\string[][] + */ + public function badTokenDataProvider(): array + { + return [ + ['foo'], + ['foobar'], + ['foo-bar'], + ['foo.bar.baz'], + ['.foo'], + ['foo.'], + ['foo..bar'], + ]; + } + /** * Return an instance of the middleware using mocked dependencies. * @@ -97,6 +164,6 @@ class DaemonAuthenticateTest extends MiddlewareTestCase */ private function getMiddleware(): DaemonAuthenticate { - return new DaemonAuthenticate($this->repository); + return new DaemonAuthenticate($this->encrypter, $this->repository); } } diff --git a/tests/Unit/Http/Middleware/API/SetSessionDriverTest.php b/tests/Unit/Http/Middleware/Api/SetSessionDriverTest.php similarity index 96% rename from tests/Unit/Http/Middleware/API/SetSessionDriverTest.php rename to tests/Unit/Http/Middleware/Api/SetSessionDriverTest.php index 68ed950cf..c41d742ef 100644 --- a/tests/Unit/Http/Middleware/API/SetSessionDriverTest.php +++ b/tests/Unit/Http/Middleware/Api/SetSessionDriverTest.php @@ -1,6 +1,6 @@ expectException(AuthenticationException::class); + $this->request->shouldReceive('user')->withNoArgs()->once()->andReturnNull(); $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); diff --git a/tests/Unit/Http/Middleware/DaemonAuthenticateTest.php b/tests/Unit/Http/Middleware/DaemonAuthenticateTest.php deleted file mode 100644 index 7329eb2d8..000000000 --- a/tests/Unit/Http/Middleware/DaemonAuthenticateTest.php +++ /dev/null @@ -1,78 +0,0 @@ -repository = m::mock(NodeRepositoryInterface::class); - } - - /** - * Test a valid daemon connection. - */ - public function testValidDaemonConnection() - { - $this->setRequestRouteName('random.name'); - $node = factory(Node::class)->make(); - - $this->request->shouldReceive('header')->with('X-Access-Node')->twice()->andReturn($node->daemonSecret); - - $this->repository->shouldReceive('findFirstWhere')->with(['daemonSecret' => $node->daemonSecret])->once()->andReturn($node); - - $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); - $this->assertRequestHasAttribute('node'); - $this->assertRequestAttributeEquals($node, 'node'); - } - - /** - * Test that ignored routes do not continue through the middleware. - */ - public function testIgnoredRouteShouldContinue() - { - $this->setRequestRouteName('daemon.configuration'); - - $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); - $this->assertRequestMissingAttribute('node'); - } - - /** - * Test that a request missing a X-Access-Node header causes an exception. - * - * @expectedException \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException - */ - public function testExceptionThrownIfMissingHeader() - { - $this->setRequestRouteName('random.name'); - - $this->request->shouldReceive('header')->with('X-Access-Node')->once()->andReturn(false); - - $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); - } - - /** - * Return an instance of the middleware using mocked dependencies. - * - * @return \Pterodactyl\Http\Middleware\DaemonAuthenticate - */ - private function getMiddleware(): DaemonAuthenticate - { - return new DaemonAuthenticate($this->repository); - } -} diff --git a/tests/Unit/Http/Middleware/Server/AccessingValidServerTest.php b/tests/Unit/Http/Middleware/Server/AccessingValidServerTest.php index 564902889..5cedbd9b9 100644 --- a/tests/Unit/Http/Middleware/Server/AccessingValidServerTest.php +++ b/tests/Unit/Http/Middleware/Server/AccessingValidServerTest.php @@ -9,6 +9,8 @@ use Illuminate\Contracts\Routing\ResponseFactory; use Tests\Unit\Http\Middleware\MiddlewareTestCase; use Pterodactyl\Http\Middleware\Server\AccessingValidServer; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Symfony\Component\HttpKernel\Exception\ConflictHttpException; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; class AccessingValidServerTest extends MiddlewareTestCase { @@ -41,12 +43,12 @@ class AccessingValidServerTest extends MiddlewareTestCase /** * Test that an exception is thrown if the request is an API request and the server is suspended. - * - * @expectedException \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException - * @expectedExceptionMessage Server is suspended and cannot be accessed. */ public function testExceptionIsThrownIfServerIsSuspended() { + $this->expectException(AccessDeniedHttpException::class); + $this->expectExceptionMessage('Server is suspended and cannot be accessed.'); + $model = factory(Server::class)->make(['suspended' => 1]); $this->request->shouldReceive('route->parameter')->with('server')->once()->andReturn('123456'); @@ -59,12 +61,12 @@ class AccessingValidServerTest extends MiddlewareTestCase /** * Test that an exception is thrown if the request is an API request and the server is not installed. - * - * @expectedException \Symfony\Component\HttpKernel\Exception\ConflictHttpException - * @expectedExceptionMessage Server is still completing the installation process. */ public function testExceptionIsThrownIfServerIsNotInstalled() { + $this->expectException(ConflictHttpException::class); + $this->expectExceptionMessage('Server is still completing the installation process.'); + $model = factory(Server::class)->make(['installed' => 0]); $this->request->shouldReceive('route->parameter')->with('server')->once()->andReturn('123456'); diff --git a/tests/Unit/Http/Middleware/Server/AuthenticateAsSubuserTest.php b/tests/Unit/Http/Middleware/Server/AuthenticateAsSubuserTest.php deleted file mode 100644 index b24a2c227..000000000 --- a/tests/Unit/Http/Middleware/Server/AuthenticateAsSubuserTest.php +++ /dev/null @@ -1,71 +0,0 @@ -keyProviderService = m::mock(DaemonKeyProviderService::class); - } - - /** - * Test a successful instance of the middleware. - */ - public function testSuccessfulMiddleware() - { - $model = factory(Server::class)->make(); - $user = $this->setRequestUser(); - $this->setRequestAttribute('server', $model); - - $this->keyProviderService->shouldReceive('handle')->with($model, $user)->once()->andReturn('abc123'); - - $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); - $this->assertRequestHasAttribute('server_token'); - $this->assertRequestAttributeEquals('abc123', 'server_token'); - } - - /** - * Test middleware handles missing token exception. - * - * @expectedException \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException - * @expectedExceptionMessage This account does not have permission to access this server. - */ - public function testExceptionIsThrownIfNoTokenIsFound() - { - $model = factory(Server::class)->make(); - $user = $this->setRequestUser(); - $this->setRequestAttribute('server', $model); - - $this->keyProviderService->shouldReceive('handle')->with($model, $user)->once()->andThrow(new RecordNotFoundException); - - $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); - } - - /** - * Return an instance of the middleware using mocked dependencies. - * - * @return \Pterodactyl\Http\Middleware\Server\AuthenticateAsSubuser - */ - public function getMiddleware(): AuthenticateAsSubuser - { - return new AuthenticateAsSubuser($this->keyProviderService); - } -} diff --git a/tests/Unit/Http/Middleware/Server/DatabaseBelongsToServerTest.php b/tests/Unit/Http/Middleware/Server/DatabaseBelongsToServerTest.php deleted file mode 100644 index 0eed6945d..000000000 --- a/tests/Unit/Http/Middleware/Server/DatabaseBelongsToServerTest.php +++ /dev/null @@ -1,92 +0,0 @@ -repository = m::mock(DatabaseRepositoryInterface::class); - } - - /** - * Test a successful middleware instance. - */ - public function testSuccessfulMiddleware() - { - $model = factory(Server::class)->make(); - $database = factory(Database::class)->make([ - 'server_id' => $model->id, - ]); - $this->setRequestAttribute('server', $model); - - $this->request->shouldReceive('input')->with('database')->once()->andReturn($database->id); - $this->repository->shouldReceive('find')->with($database->id)->once()->andReturn($database); - - $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); - $this->assertRequestHasAttribute('database'); - $this->assertRequestAttributeEquals($database, 'database'); - } - - /** - * Test that an exception is thrown if no database record is found. - * - * @expectedException \Symfony\Component\HttpKernel\Exception\NotFoundHttpException - */ - public function testExceptionIsThrownIfNoDatabaseRecordFound() - { - $model = factory(Server::class)->make(); - $database = factory(Database::class)->make(); - $this->setRequestAttribute('server', $model); - - $this->request->shouldReceive('input')->with('database')->once()->andReturn($database->id); - $this->repository->shouldReceive('find')->with($database->id)->once()->andReturnNull(); - - $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); - } - - /** - * Test that an exception is found if the database server does not match the - * request server. - * - * @expectedException \Symfony\Component\HttpKernel\Exception\NotFoundHttpException - */ - public function testExceptionIsThrownIfDatabaseServerDoesNotMatchCurrent() - { - $model = factory(Server::class)->make(); - $database = factory(Database::class)->make(); - $this->setRequestAttribute('server', $model); - - $this->request->shouldReceive('input')->with('database')->once()->andReturn($database->id); - $this->repository->shouldReceive('find')->with($database->id)->once()->andReturn($database); - - $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); - } - - /** - * Return an instance of the middleware using mocked dependencies. - * - * @return \Pterodactyl\Http\Middleware\Server\DatabaseBelongsToServer - */ - private function getMiddleware(): DatabaseBelongsToServer - { - return new DatabaseBelongsToServer($this->repository); - } -} diff --git a/tests/Unit/Http/Middleware/Server/ScheduleBelongsToServerTest.php b/tests/Unit/Http/Middleware/Server/ScheduleBelongsToServerTest.php deleted file mode 100644 index ac455a84a..000000000 --- a/tests/Unit/Http/Middleware/Server/ScheduleBelongsToServerTest.php +++ /dev/null @@ -1,81 +0,0 @@ -hashids = m::mock(HashidsInterface::class); - $this->repository = m::mock(ScheduleRepositoryInterface::class); - } - - /** - * Test a successful middleware instance. - */ - public function testSuccessfulMiddleware() - { - $model = factory(Server::class)->make(); - $schedule = factory(Schedule::class)->make([ - 'server_id' => $model->id, - ]); - $this->setRequestAttribute('server', $model); - - $this->request->shouldReceive('route->parameter')->with('schedule')->once()->andReturn('abc123'); - $this->hashids->shouldReceive('decodeFirst')->with('abc123', 0)->once()->andReturn($schedule->id); - $this->repository->shouldReceive('getScheduleWithTasks')->with($schedule->id)->once()->andReturn($schedule); - - $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); - $this->assertRequestHasAttribute('schedule'); - $this->assertRequestAttributeEquals($schedule, 'schedule'); - } - - /** - * Test that an exception is thrown if the schedule does not belong to - * the request server. - * - * @expectedException \Symfony\Component\HttpKernel\Exception\NotFoundHttpException - */ - public function testExceptionIsThrownIfScheduleDoesNotBelongToServer() - { - $model = factory(Server::class)->make(); - $schedule = factory(Schedule::class)->make(); - $this->setRequestAttribute('server', $model); - - $this->request->shouldReceive('route->parameter')->with('schedule')->once()->andReturn('abc123'); - $this->hashids->shouldReceive('decodeFirst')->with('abc123', 0)->once()->andReturn($schedule->id); - $this->repository->shouldReceive('getScheduleWithTasks')->with($schedule->id)->once()->andReturn($schedule); - - $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); - } - - /** - * Return an instance of the middleware using mocked dependencies. - * - * @return \Pterodactyl\Http\Middleware\Server\ScheduleBelongsToServer - */ - private function getMiddleware(): ScheduleBelongsToServer - { - return new ScheduleBelongsToServer($this->hashids, $this->repository); - } -} diff --git a/tests/Unit/Http/Middleware/Server/SubuserBelongsToServerTest.php b/tests/Unit/Http/Middleware/Server/SubuserBelongsToServerTest.php deleted file mode 100644 index 7d06ece59..000000000 --- a/tests/Unit/Http/Middleware/Server/SubuserBelongsToServerTest.php +++ /dev/null @@ -1,156 +0,0 @@ -hashids = m::mock(HashidsInterface::class); - $this->repository = m::mock(SubuserRepositoryInterface::class); - } - - /** - * Test a successful middleware instance. - */ - public function testSuccessfulMiddleware() - { - $model = factory(Server::class)->make(); - $subuser = factory(Subuser::class)->make([ - 'server_id' => $model->id, - ]); - $this->setRequestAttribute('server', $model); - - $this->request->shouldReceive('route->parameter')->with('subuser', 0)->once()->andReturn('abc123'); - $this->hashids->shouldReceive('decodeFirst')->with('abc123', 0)->once()->andReturn($subuser->id); - $this->repository->shouldReceive('find')->with($subuser->id)->once()->andReturn($subuser); - - $this->request->shouldReceive('method')->withNoArgs()->once()->andReturn('GET'); - - $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); - $this->assertRequestHasAttribute('subuser'); - $this->assertRequestAttributeEquals($subuser, 'subuser'); - } - - /** - * Test that a user can edit a user other than themselves. - */ - public function testSuccessfulMiddlewareWhenPatchRequest() - { - $this->setRequestUser(); - $model = factory(Server::class)->make(); - $subuser = factory(Subuser::class)->make([ - 'server_id' => $model->id, - ]); - $this->setRequestAttribute('server', $model); - - $this->request->shouldReceive('route->parameter')->with('subuser', 0)->once()->andReturn('abc123'); - $this->hashids->shouldReceive('decodeFirst')->with('abc123', 0)->once()->andReturn($subuser->id); - $this->repository->shouldReceive('find')->with($subuser->id)->once()->andReturn($subuser); - - $this->request->shouldReceive('method')->withNoArgs()->once()->andReturn('PATCH'); - - $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); - $this->assertRequestHasAttribute('subuser'); - $this->assertRequestAttributeEquals($subuser, 'subuser'); - } - - /** - * Test that an exception is thrown if a user attempts to edit themself. - */ - public function testExceptionIsThrownIfUserTriesToEditSelf() - { - $user = $this->setRequestUser(); - $model = factory(Server::class)->make(); - $subuser = factory(Subuser::class)->make([ - 'server_id' => $model->id, - 'user_id' => $user->id, - ]); - $this->setRequestAttribute('server', $model); - - $this->request->shouldReceive('route->parameter')->with('subuser', 0)->once()->andReturn('abc123'); - $this->hashids->shouldReceive('decodeFirst')->with('abc123', 0)->once()->andReturn($subuser->id); - $this->repository->shouldReceive('find')->with($subuser->id)->once()->andReturn($subuser); - - $this->request->shouldReceive('method')->withNoArgs()->once()->andReturn('PATCH'); - - try { - $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); - } catch (PterodactylException $exception) { - $this->assertInstanceOf(DisplayException::class, $exception); - $this->assertEquals(trans('exceptions.subusers.editing_self'), $exception->getMessage()); - } - } - - /** - * Test that an exception is thrown if a subuser server does not match the - * request server. - * - * @expectedException \Symfony\Component\HttpKernel\Exception\NotFoundHttpException - */ - public function testExceptionIsThrownIfSubuserServerDoesNotMatchRequestServer() - { - $model = factory(Server::class)->make(); - $subuser = factory(Subuser::class)->make(); - $this->setRequestAttribute('server', $model); - - $this->request->shouldReceive('route->parameter')->with('subuser', 0)->once()->andReturn('abc123'); - $this->hashids->shouldReceive('decodeFirst')->with('abc123', 0)->once()->andReturn($subuser->id); - $this->repository->shouldReceive('find')->with($subuser->id)->once()->andReturn($subuser); - - $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); - } - - /** - * Test that an exception is thrown if no subuser is found. - * - * @expectedException \Symfony\Component\HttpKernel\Exception\NotFoundHttpException - */ - public function testExceptionIsThrownIfNoSubuserIsFound() - { - $model = factory(Server::class)->make(); - $subuser = factory(Subuser::class)->make(); - $this->setRequestAttribute('server', $model); - - $this->request->shouldReceive('route->parameter')->with('subuser', 0)->once()->andReturn('abc123'); - $this->hashids->shouldReceive('decodeFirst')->with('abc123', 0)->once()->andReturn($subuser->id); - $this->repository->shouldReceive('find')->with($subuser->id)->once()->andReturnNull(); - - $this->getMiddleware()->handle($this->request, $this->getClosureAssertions()); - } - - /** - * Return an instance of the middleware using mocked dependencies. - * - * @return \Pterodactyl\Http\Middleware\Server\SubuserBelongsToServer - */ - private function getMiddleware(): SubuserBelongsToServer - { - return new SubuserBelongsToServer($this->hashids, $this->repository); - } -} diff --git a/tests/Unit/Jobs/Schedule/RunTaskJobTest.php b/tests/Unit/Jobs/Schedule/RunTaskJobTest.php index adfe0b54f..4d7688a82 100644 --- a/tests/Unit/Jobs/Schedule/RunTaskJobTest.php +++ b/tests/Unit/Jobs/Schedule/RunTaskJobTest.php @@ -3,53 +3,51 @@ namespace Tests\Unit\Jobs\Schedule; use Mockery as m; +use Carbon\Carbon; use Tests\TestCase; use Cake\Chronos\Chronos; use Pterodactyl\Models\Task; use Pterodactyl\Models\User; use GuzzleHttp\Psr7\Response; +use InvalidArgumentException; use Pterodactyl\Models\Server; use Pterodactyl\Models\Schedule; use Illuminate\Support\Facades\Bus; use Pterodactyl\Jobs\Schedule\RunTaskJob; -use Illuminate\Contracts\Config\Repository; +use Pterodactyl\Repositories\Eloquent\TaskRepository; +use Pterodactyl\Services\Backups\InitiateBackupService; +use Pterodactyl\Repositories\Eloquent\ScheduleRepository; +use Pterodactyl\Repositories\Wings\DaemonPowerRepository; +use Pterodactyl\Repositories\Wings\DaemonCommandRepository; use Pterodactyl\Contracts\Repository\TaskRepositoryInterface; -use Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService; use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface; -use Pterodactyl\Contracts\Repository\Daemon\PowerRepositoryInterface; -use Pterodactyl\Contracts\Repository\Daemon\CommandRepositoryInterface; class RunTaskJobTest extends TestCase { /** - * @var \Pterodactyl\Contracts\Repository\Daemon\CommandRepositoryInterface|\Mockery\Mock + * @var \Mockery\MockInterface */ - protected $commandRepository; + private $commandRepository; /** - * @var \Illuminate\Contracts\Config\Repository|\Mockery\Mock + * @var \Mockery\MockInterface */ - protected $config; + private $powerRepository; /** - * @var \Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService|\Mockery\Mock + * @var \Mockery\MockInterface */ - protected $keyProviderService; + private $initiateBackupService; /** - * @var \Pterodactyl\Contracts\Repository\Daemon\PowerRepositoryInterface|\Mockery\Mock + * @var \Mockery\MockInterface */ - protected $powerRepository; + private $taskRepository; /** - * @var \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface|\Mockery\Mock + * @var \Mockery\MockInterface */ - protected $scheduleRepository; - - /** - * @var \Pterodactyl\Contracts\Repository\TaskRepositoryInterface|\Mockery\Mock - */ - protected $taskRepository; + private $scheduleRepository; /** * Setup tests. @@ -57,17 +55,16 @@ class RunTaskJobTest extends TestCase public function setUp(): void { parent::setUp(); + Bus::fake(); - Chronos::setTestNow(Chronos::now()); + Carbon::setTestNow(Carbon::now()); - $this->commandRepository = m::mock(CommandRepositoryInterface::class); - $this->config = m::mock(Repository::class); - $this->keyProviderService = m::mock(DaemonKeyProviderService::class); - $this->powerRepository = m::mock(PowerRepositoryInterface::class); - $this->scheduleRepository = m::mock(ScheduleRepositoryInterface::class); - $this->taskRepository = m::mock(TaskRepositoryInterface::class); + $this->commandRepository = m::mock(DaemonCommandRepository::class); + $this->powerRepository = m::mock(DaemonPowerRepository::class); + $this->taskRepository = m::mock(TaskRepository::class); + $this->initiateBackupService = m::mock(InitiateBackupService::class); + $this->scheduleRepository = m::mock(ScheduleRepository::class); - $this->app->instance(Repository::class, $this->config); $this->app->instance(TaskRepositoryInterface::class, $this->taskRepository); $this->app->instance(ScheduleRepositoryInterface::class, $this->scheduleRepository); } @@ -77,17 +74,20 @@ class RunTaskJobTest extends TestCase */ public function testPowerAction() { - $schedule = factory(Schedule::class)->make(); + /** @var \Pterodactyl\Models\Schedule $schedule */ + $schedule = factory(Schedule::class)->make(['is_active' => true]); + + /** @var \Pterodactyl\Models\Task $task */ $task = factory(Task::class)->make(['action' => 'power', 'sequence_id' => 1]); + + /* @var \Pterodactyl\Models\Server $server */ $task->setRelation('server', $server = factory(Server::class)->make()); $task->setRelation('schedule', $schedule); $server->setRelation('user', factory(User::class)->make()); - $this->taskRepository->shouldReceive('getTaskForJobProcess')->with($task->id)->once()->andReturn($task); - $this->keyProviderService->shouldReceive('handle')->with($server, $server->user)->once()->andReturn('123456'); - $this->powerRepository->shouldReceive('setServer')->with($task->server)->once()->andReturnSelf() - ->shouldReceive('setToken')->with('123456')->once()->andReturnSelf() - ->shouldReceive('sendSignal')->with($task->payload)->once()->andReturn(new Response); + $this->taskRepository->expects('getTaskForJobProcess')->with($task->id)->andReturn($task); + $this->powerRepository->expects('setServer')->with($task->server)->andReturnSelf() + ->getMock()->expects('send')->with($task->payload)->andReturn(new Response); $this->taskRepository->shouldReceive('update')->with($task->id, ['is_queued' => false])->once()->andReturnNull(); $this->taskRepository->shouldReceive('getNextTask')->with($schedule->id, $task->sequence_id)->once()->andReturnNull(); @@ -113,14 +113,12 @@ class RunTaskJobTest extends TestCase $task->setRelation('schedule', $schedule); $server->setRelation('user', factory(User::class)->make()); - $this->taskRepository->shouldReceive('getTaskForJobProcess')->with($task->id)->once()->andReturn($task); - $this->keyProviderService->shouldReceive('handle')->with($server, $server->user)->once()->andReturn('123456'); - $this->commandRepository->shouldReceive('setServer')->with($task->server)->once()->andReturnSelf() - ->shouldReceive('setToken')->with('123456')->once()->andReturnSelf() - ->shouldReceive('send')->with($task->payload)->once()->andReturn(new Response); + $this->taskRepository->expects('getTaskForJobProcess')->with($task->id)->andReturn($task); + $this->commandRepository->expects('setServer')->with($task->server)->andReturnSelf() + ->getMock()->expects('send')->with($task->payload)->andReturn(new Response); - $this->taskRepository->shouldReceive('update')->with($task->id, ['is_queued' => false])->once()->andReturnNull(); - $this->taskRepository->shouldReceive('getNextTask')->with($schedule->id, $task->sequence_id)->once()->andReturnNull(); + $this->taskRepository->expects('update')->with($task->id, ['is_queued' => false])->andReturnNull(); + $this->taskRepository->expects('getNextTask')->with($schedule->id, $task->sequence_id)->andReturnNull(); $this->scheduleRepository->shouldReceive('withoutFreshModel->update')->with($schedule->id, [ 'is_processing' => false, @@ -143,19 +141,17 @@ class RunTaskJobTest extends TestCase $task->setRelation('schedule', $schedule); $server->setRelation('user', factory(User::class)->make()); - $this->taskRepository->shouldReceive('getTaskForJobProcess')->with($task->id)->once()->andReturn($task); - $this->keyProviderService->shouldReceive('handle')->with($server, $server->user)->once()->andReturn('123456'); - $this->commandRepository->shouldReceive('setServer')->with($task->server)->once()->andReturnSelf() - ->shouldReceive('setToken')->with('123456')->once()->andReturnSelf() - ->shouldReceive('send')->with($task->payload)->once()->andReturn(new Response); + $this->taskRepository->expects('getTaskForJobProcess')->with($task->id)->andReturn($task); + $this->commandRepository->expects('setServer')->with($task->server)->andReturnSelf() + ->getMock()->expects('send')->with($task->payload)->andReturn(new Response); $this->taskRepository->shouldReceive('update')->with($task->id, ['is_queued' => false])->once()->andReturnNull(); $nextTask = factory(Task::class)->make(); - $this->taskRepository->shouldReceive('getNextTask')->with($schedule->id, $task->sequence_id)->once()->andReturn($nextTask); - $this->taskRepository->shouldReceive('update')->with($nextTask->id, [ + $this->taskRepository->expects('getNextTask')->with($schedule->id, $task->sequence_id)->andReturn($nextTask); + $this->taskRepository->expects('update')->with($nextTask->id, [ 'is_queued' => true, - ])->once()->andReturnNull(); + ])->andReturnNull(); $this->getJobInstance($task->id, $schedule->id); @@ -170,19 +166,19 @@ class RunTaskJobTest extends TestCase /** * Test that an exception is thrown if an invalid task action is supplied. - * - * @expectedException \InvalidArgumentException - * @expectedExceptionMessage Cannot run a task that points to a non-existent action. */ public function testInvalidActionPassedToJob() { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Cannot run a task that points to a non-existent action.'); + $schedule = factory(Schedule::class)->make(); $task = factory(Task::class)->make(['action' => 'invalid', 'sequence_id' => 1]); $task->setRelation('server', $server = factory(Server::class)->make()); $task->setRelation('schedule', $schedule); $server->setRelation('user', factory(User::class)->make()); - $this->taskRepository->shouldReceive('getTaskForJobProcess')->with($task->id)->once()->andReturn($task); + $this->taskRepository->expects('getTaskForJobProcess')->with($task->id)->andReturn($task); $this->getJobInstance($task->id, 1234); } @@ -218,14 +214,12 @@ class RunTaskJobTest extends TestCase * @param int $schedule * * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\Daemon\InvalidPowerSignalException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ private function getJobInstance($task, $schedule) { return (new RunTaskJob($task, $schedule))->handle( $this->commandRepository, - $this->keyProviderService, + $this->initiateBackupService, $this->powerRepository, $this->taskRepository ); diff --git a/tests/Unit/Services/Allocations/AllocationDeletionServiceTest.php b/tests/Unit/Services/Allocations/AllocationDeletionServiceTest.php index f1ad8eb68..521aed20a 100644 --- a/tests/Unit/Services/Allocations/AllocationDeletionServiceTest.php +++ b/tests/Unit/Services/Allocations/AllocationDeletionServiceTest.php @@ -7,6 +7,7 @@ use Tests\TestCase; use Pterodactyl\Models\Allocation; use Pterodactyl\Services\Allocations\AllocationDeletionService; use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; +use Pterodactyl\Exceptions\Service\Allocation\ServerUsingAllocationException; class AllocationDeletionServiceTest extends TestCase { @@ -37,11 +38,11 @@ class AllocationDeletionServiceTest extends TestCase /** * Test that an exception gets thrown if an allocation is currently assigned to a server. - * - * @expectedException \Pterodactyl\Exceptions\Service\Allocation\ServerUsingAllocationException */ public function testExceptionThrownIfAssignedToServer() { + $this->expectException(ServerUsingAllocationException::class); + $model = factory(Allocation::class)->make(['server_id' => 123]); $this->getService()->handle($model); diff --git a/tests/Unit/Services/Allocations/AssignmentServiceTest.php b/tests/Unit/Services/Allocations/AssignmentServiceTest.php index 7449c81cc..0e6da9035 100644 --- a/tests/Unit/Services/Allocations/AssignmentServiceTest.php +++ b/tests/Unit/Services/Allocations/AssignmentServiceTest.php @@ -8,6 +8,10 @@ use Pterodactyl\Models\Node; use Illuminate\Database\ConnectionInterface; use Pterodactyl\Services\Allocations\AssignmentService; use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; +use Pterodactyl\Exceptions\Service\Allocation\CidrOutOfRangeException; +use Pterodactyl\Exceptions\Service\Allocation\PortOutOfRangeException; +use Pterodactyl\Exceptions\Service\Allocation\InvalidPortMappingException; +use Pterodactyl\Exceptions\Service\Allocation\TooManyPortsInRangeException; class AssignmentServiceTest extends TestCase { @@ -190,12 +194,12 @@ class AssignmentServiceTest extends TestCase /** * Test that a CIDR IP address with a range works properly. - * - * @expectedException \Pterodactyl\Exceptions\Service\Allocation\CidrOutOfRangeException - * @expectedExceptionMessage CIDR notation only allows masks between /25 and /32. */ public function testCIDRNotatedIPAddressOutsideRangeLimit() { + $this->expectException(CidrOutOfRangeException::class); + $this->expectExceptionMessage('CIDR notation only allows masks between /25 and /32.'); + $data = [ 'allocation_ip' => '192.168.1.100/20', 'allocation_ports' => ['2222'], @@ -206,12 +210,12 @@ class AssignmentServiceTest extends TestCase /** * Test that an exception is thrown if there are too many ports. - * - * @expectedException \Pterodactyl\Exceptions\Service\Allocation\TooManyPortsInRangeException - * @expectedExceptionMessage Adding more than 1000 ports in a single range at once is not supported. */ public function testAllocationWithPortsExceedingLimit() { + $this->expectException(TooManyPortsInRangeException::class); + $this->expectExceptionMessage('Adding more than 1000 ports in a single range at once is not supported.'); + $data = [ 'allocation_ip' => '192.168.1.1', 'allocation_ports' => ['5000-7000'], @@ -224,12 +228,12 @@ class AssignmentServiceTest extends TestCase /** * Test that an exception is thrown if an invalid port is provided. - * - * @expectedException \Pterodactyl\Exceptions\Service\Allocation\InvalidPortMappingException - * @expectedExceptionMessage The mapping provided for test123 was invalid and could not be processed. */ public function testInvalidPortProvided() { + $this->expectException(InvalidPortMappingException::class); + $this->expectExceptionMessage('The mapping provided for test123 was invalid and could not be processed.'); + $data = [ 'allocation_ip' => '192.168.1.1', 'allocation_ports' => ['test123'], @@ -245,11 +249,12 @@ class AssignmentServiceTest extends TestCase * @param array $ports * * @dataProvider invalidPortsDataProvider - * @expectedException \Pterodactyl\Exceptions\Service\Allocation\PortOutOfRangeException - * @expectedExceptionMessage Ports in an allocation must be greater than 1024 and less than or equal to 65535. */ public function testPortRangeOutsideOfRangeLimits(array $ports) { + $this->expectException(PortOutOfRangeException::class); + $this->expectExceptionMessage('Ports in an allocation must be greater than 1024 and less than or equal to 65535.'); + $data = ['allocation_ip' => '192.168.1.1', 'allocation_ports' => $ports]; $this->connection->shouldReceive('beginTransaction')->once()->withNoArgs()->andReturnNull(); diff --git a/tests/Unit/Services/Allocations/SetDefaultAllocationServiceTest.php b/tests/Unit/Services/Allocations/SetDefaultAllocationServiceTest.php deleted file mode 100644 index 75d1f32dd..000000000 --- a/tests/Unit/Services/Allocations/SetDefaultAllocationServiceTest.php +++ /dev/null @@ -1,156 +0,0 @@ -connection = m::mock(ConnectionInterface::class); - $this->daemonRepository = m::mock(DaemonRepositoryInterface::class); - $this->repository = m::mock(AllocationRepositoryInterface::class); - $this->serverRepository = m::mock(ServerRepositoryInterface::class); - } - - /** - * Test that an allocation can be updated. - * - * @dataProvider useModelDataProvider - */ - public function testAllocationIsUpdated(bool $useModel) - { - $allocations = factory(Allocation::class)->times(2)->make(); - $model = factory(Server::class)->make(); - if (! $useModel) { - $this->serverRepository->shouldReceive('find')->with(1234)->once()->andReturn($model); - } - - $this->repository->shouldReceive('findWhere')->with([['server_id', '=', $model->id]])->once()->andReturn($allocations); - $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->serverRepository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf(); - $this->serverRepository->shouldReceive('update')->with($model->id, [ - 'allocation_id' => $allocations->first()->id, - ])->once()->andReturn(new Response); - - $this->daemonRepository->shouldReceive('setServer')->with($model)->once()->andReturnSelf(); - $this->daemonRepository->shouldReceive('update')->with([ - 'build' => [ - 'default' => [ - 'ip' => $allocations->first()->ip, - 'port' => $allocations->first()->port, - ], - 'ports|overwrite' => $allocations->groupBy('ip')->map(function ($item) { - return $item->pluck('port'); - })->toArray(), - ], - ])->once()->andReturn(new Response); - $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); - - $response = $this->getService()->handle($useModel ? $model : 1234, $allocations->first()->id); - $this->assertNotEmpty($response); - $this->assertSame($allocations->first(), $response); - } - - /** - * Test that an allocation that doesn't belong to a server throws an exception. - * - * @expectedException \Pterodactyl\Exceptions\Service\Allocation\AllocationDoesNotBelongToServerException - */ - public function testAllocationNotBelongingToServerThrowsException() - { - $model = factory(Server::class)->make(); - $this->repository->shouldReceive('findWhere')->with([['server_id', '=', $model->id]])->once()->andReturn(collect()); - - $this->getService()->handle($model, 1234); - } - - /** - * Test that an exception thrown by guzzle is handled properly. - */ - public function testExceptionThrownByGuzzleIsHandled() - { - $this->configureExceptionMock(); - - $allocation = factory(Allocation::class)->make(); - $model = factory(Server::class)->make(); - - $this->repository->shouldReceive('findWhere')->with([['server_id', '=', $model->id]])->once()->andReturn(collect([$allocation])); - $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->serverRepository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf(); - $this->serverRepository->shouldReceive('update')->with($model->id, [ - 'allocation_id' => $allocation->id, - ])->once()->andReturn(new Response); - - $this->daemonRepository->shouldReceive('setServer->update')->once()->andThrow($this->getExceptionMock()); - $this->connection->shouldReceive('rollBack')->withNoArgs()->once()->andReturnNull(); - - try { - $this->getService()->handle($model, $allocation->id); - } catch (PterodactylException $exception) { - $this->assertInstanceOf(DaemonConnectionException::class, $exception); - $this->assertInstanceOf(RequestException::class, $exception->getPrevious()); - } - } - - /** - * Data provider to determine if a model should be passed or an int. - * - * @return array - */ - public function useModelDataProvider(): array - { - return [[false], [true]]; - } - - /** - * Return an instance of the service with mocked dependencies. - * - * @return \Pterodactyl\Services\Allocations\SetDefaultAllocationService - */ - private function getService(): SetDefaultAllocationService - { - return new SetDefaultAllocationService($this->repository, $this->connection, $this->daemonRepository, $this->serverRepository); - } -} diff --git a/tests/Unit/Services/DaemonKeys/DaemonKeyCreationServiceTest.php b/tests/Unit/Services/DaemonKeys/DaemonKeyCreationServiceTest.php deleted file mode 100644 index 7c5bad2c2..000000000 --- a/tests/Unit/Services/DaemonKeys/DaemonKeyCreationServiceTest.php +++ /dev/null @@ -1,98 +0,0 @@ -. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -namespace Tests\Unit\Services\DaemonKeys; - -use Mockery as m; -use Carbon\Carbon; -use Tests\TestCase; -use phpmock\phpunit\PHPMock; -use Illuminate\Contracts\Config\Repository; -use Pterodactyl\Services\DaemonKeys\DaemonKeyCreationService; -use Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface; - -class DaemonKeyCreationServiceTest extends TestCase -{ - use PHPMock; - - /** - * @var \Carbon\Carbon|\Mockery\Mock - */ - protected $carbon; - - /** - * @var \Illuminate\Contracts\Config\Repository|\Mockery\Mock - */ - protected $config; - - /** - * @var \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface|\Mockery\Mock - */ - protected $repository; - - /** - * @var \Pterodactyl\Services\DaemonKeys\DaemonKeyCreationService - */ - protected $service; - - /** - * Setup tests. - */ - public function setUp(): void - { - parent::setUp(); - - $this->carbon = m::mock(Carbon::class); - $this->config = m::Mock(Repository::class); - $this->repository = m::mock(DaemonKeyRepositoryInterface::class); - - $this->service = new DaemonKeyCreationService($this->carbon, $this->config, $this->repository); - } - - /** - * Test that a daemon key is created. - */ - public function testDaemonKeyIsCreated() - { - $this->getFunctionMock('\\Pterodactyl\\Services\\DaemonKeys', 'str_random') - ->expects($this->once())->willReturn('random_string'); - - $this->config->shouldReceive('get')->with('pterodactyl.api.key_expire_time')->once()->andReturn(100); - $this->carbon->shouldReceive('now')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('addMinutes')->with(100)->once()->andReturnSelf() - ->shouldReceive('toDateTimeString')->withNoArgs()->once()->andReturn('00:00:00'); - - $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('create')->with([ - 'user_id' => 1, - 'server_id' => 2, - 'secret' => DaemonKeyRepositoryInterface::INTERNAL_KEY_IDENTIFIER . 'random_string', - 'expires_at' => '00:00:00', - ])->once()->andReturnNull(); - - $response = $this->service->handle(2, 1); - $this->assertNotEmpty($response); - $this->assertEquals('i_random_string', $response); - } -} diff --git a/tests/Unit/Services/DaemonKeys/DaemonKeyProviderServiceTest.php b/tests/Unit/Services/DaemonKeys/DaemonKeyProviderServiceTest.php deleted file mode 100644 index 74c34d3c0..000000000 --- a/tests/Unit/Services/DaemonKeys/DaemonKeyProviderServiceTest.php +++ /dev/null @@ -1,229 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Tests\Unit\Services\DaemonKeys; - -use Mockery as m; -use Carbon\Carbon; -use Tests\TestCase; -use Pterodactyl\Models\User; -use Pterodactyl\Models\Server; -use Pterodactyl\Models\Subuser; -use Pterodactyl\Models\DaemonKey; -use Pterodactyl\Services\DaemonKeys\DaemonKeyUpdateService; -use Pterodactyl\Services\DaemonKeys\DaemonKeyCreationService; -use Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService; -use Pterodactyl\Exceptions\Repository\RecordNotFoundException; -use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; -use Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface; - -class DaemonKeyProviderServiceTest extends TestCase -{ - /** - * @var \Pterodactyl\Services\DaemonKeys\DaemonKeyCreationService|\Mockery\Mock - */ - private $keyCreationService; - - /** - * @var \Pterodactyl\Services\DaemonKeys\DaemonKeyUpdateService|\Mockery\Mock - */ - private $keyUpdateService; - - /** - * @var \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface|\Mockery\Mock - */ - private $repository; - - /** - * @var \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface|\Mockery\Mock - */ - private $subuserRepository; - - /** - * Setup tests. - */ - public function setUp(): void - { - parent::setUp(); - Carbon::setTestNow(Carbon::now()); - - $this->keyCreationService = m::mock(DaemonKeyCreationService::class); - $this->keyUpdateService = m::mock(DaemonKeyUpdateService::class); - $this->repository = m::mock(DaemonKeyRepositoryInterface::class); - $this->subuserRepository = m::mock(SubuserRepositoryInterface::class); - } - - /** - * Test that a key is returned correctly as a non-admin. - */ - public function testKeyIsReturned() - { - $server = factory(Server::class)->make(); - $user = factory(User::class)->make(); - $key = factory(DaemonKey::class)->make(); - - $this->repository->shouldReceive('findFirstWhere')->with([ - ['user_id', '=', $user->id], - ['server_id', '=', $server->id], - ])->once()->andReturn($key); - - $response = $this->getService()->handle($server, $user); - $this->assertNotEmpty($response); - $this->assertEquals($key->secret, $response); - } - - /** - * Test that an expired key is updated and then returned. - */ - public function testExpiredKeyIsUpdated() - { - $server = factory(Server::class)->make(); - $user = factory(User::class)->make(['root_admin' => 0]); - $key = factory(DaemonKey::class)->make(['expires_at' => Carbon::now()->subHour()]); - - $this->repository->shouldReceive('findFirstWhere')->with([ - ['user_id', '=', $user->id], - ['server_id', '=', $server->id], - ])->once()->andReturn($key); - - $this->keyUpdateService->shouldReceive('handle')->with($key->id)->once()->andReturn('abc123'); - - $response = $this->getService()->handle($server, $user); - $this->assertNotEmpty($response); - $this->assertEquals('abc123', $response); - } - - /** - * Test that an expired key is not updated and the expired key is returned. - */ - public function testExpiredKeyIsNotUpdated() - { - $server = factory(Server::class)->make(); - $user = factory(User::class)->make(['root_admin' => 0]); - $key = factory(DaemonKey::class)->make(['expires_at' => Carbon::now()->subHour()]); - - $this->repository->shouldReceive('findFirstWhere')->with([ - ['user_id', '=', $user->id], - ['server_id', '=', $server->id], - ])->once()->andReturn($key); - - $response = $this->getService()->handle($server, $user, false); - $this->assertNotEmpty($response); - $this->assertEquals($key->secret, $response); - } - - /** - * Test that a key is created if it is missing and the user is a - * root administrator. - */ - public function testMissingKeyIsCreatedIfRootAdmin() - { - $server = factory(Server::class)->make(); - $user = factory(User::class)->make(['root_admin' => 1]); - $key = factory(DaemonKey::class)->make(['expires_at' => Carbon::now()->subHour()]); - - $this->repository->shouldReceive('findFirstWhere')->with([ - ['user_id', '=', $user->id], - ['server_id', '=', $server->id], - ])->once()->andThrow(new RecordNotFoundException); - - $this->keyCreationService->shouldReceive('handle')->with($server->id, $user->id)->once()->andReturn($key->secret); - - $response = $this->getService()->handle($server, $user, false); - $this->assertNotEmpty($response); - $this->assertEquals($key->secret, $response); - } - - /** - * Test that a key is created if it is missing and the user is the - * server owner. - */ - public function testMissingKeyIsCreatedIfUserIsServerOwner() - { - $user = factory(User::class)->make(['root_admin' => 0]); - $server = factory(Server::class)->make(['owner_id' => $user->id]); - $key = factory(DaemonKey::class)->make(['expires_at' => Carbon::now()->subHour()]); - - $this->repository->shouldReceive('findFirstWhere')->with([ - ['user_id', '=', $user->id], - ['server_id', '=', $server->id], - ])->once()->andThrow(new RecordNotFoundException); - - $this->keyCreationService->shouldReceive('handle')->with($server->id, $user->id)->once()->andReturn($key->secret); - - $response = $this->getService()->handle($server, $user, false); - $this->assertNotEmpty($response); - $this->assertEquals($key->secret, $response); - } - - /** - * Test that a missing key is created for a subuser. - */ - public function testMissingKeyIsCreatedForSubuser() - { - $user = factory(User::class)->make(['root_admin' => 0]); - $server = factory(Server::class)->make(); - $key = factory(DaemonKey::class)->make(['expires_at' => Carbon::now()->subHour()]); - $subuser = factory(Subuser::class)->make(['user_id' => $user->id, 'server_id' => $server->id]); - - $this->repository->shouldReceive('findFirstWhere')->with([ - ['user_id', '=', $user->id], - ['server_id', '=', $server->id], - ])->once()->andThrow(new RecordNotFoundException); - - $this->subuserRepository->shouldReceive('findFirstWhere')->once()->with([ - ['user_id', '=', $user->id], - ['server_id', '=', $server->id], - ])->andReturn($subuser); - - $this->keyCreationService->shouldReceive('handle')->with($server->id, $user->id)->once()->andReturn($key->secret); - - $response = $this->getService()->handle($server, $user, false); - $this->assertNotEmpty($response); - $this->assertEquals($key->secret, $response); - } - - /** - * Test that an exception is thrown if the user should not get a key. - * - * @expectedException \Pterodactyl\Exceptions\Repository\RecordNotFoundException - */ - public function testExceptionIsThrownIfUserDoesNotDeserveKey() - { - $server = factory(Server::class)->make(); - $user = factory(User::class)->make(['root_admin' => 0]); - - $this->repository->shouldReceive('findFirstWhere')->with([ - ['user_id', '=', $user->id], - ['server_id', '=', $server->id], - ])->once()->andThrow(new RecordNotFoundException); - - $this->subuserRepository->shouldReceive('findFirstWhere')->once()->with([ - ['user_id', '=', $user->id], - ['server_id', '=', $server->id], - ])->andThrow(new RecordNotFoundException); - - $this->getService()->handle($server, $user, false); - } - - /** - * Return an instance of the service with mocked dependencies. - * - * @return \Pterodactyl\Services\DaemonKeys\DaemonKeyProviderService - */ - private function getService(): DaemonKeyProviderService - { - return new DaemonKeyProviderService( - $this->keyCreationService, - $this->repository, - $this->keyUpdateService, - $this->subuserRepository - ); - } -} diff --git a/tests/Unit/Services/DaemonKeys/DaemonKeyUpdateServiceTest.php b/tests/Unit/Services/DaemonKeys/DaemonKeyUpdateServiceTest.php deleted file mode 100644 index b1beadbce..000000000 --- a/tests/Unit/Services/DaemonKeys/DaemonKeyUpdateServiceTest.php +++ /dev/null @@ -1,83 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Tests\Unit\Services\DaemonKeys; - -use Mockery as m; -use Carbon\Carbon; -use Tests\TestCase; -use phpmock\phpunit\PHPMock; -use Illuminate\Contracts\Config\Repository; -use Pterodactyl\Services\DaemonKeys\DaemonKeyUpdateService; -use Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface; - -class DaemonKeyUpdateServiceTest extends TestCase -{ - use PHPMock; - - /** - * @var \Carbon\Carbon|\Mockery\Mock - */ - protected $carbon; - - /** - * @var \Illuminate\Contracts\Config\Repository|\Mockery\Mock - */ - protected $config; - - /** - * @var \Pterodactyl\Contracts\Repository\DaemonKeyRepositoryInterface|\Mockery\Mock - */ - protected $repository; - - /** - * @var \Pterodactyl\Services\DaemonKeys\DaemonKeyUpdateService - */ - protected $service; - - /** - * Setup tests. - */ - public function setUp(): void - { - parent::setUp(); - - $this->carbon = m::Mock(Carbon::class); - $this->config = m::mock(Repository::class); - $this->repository = m::mock(DaemonKeyRepositoryInterface::class); - - $this->service = new DaemonKeyUpdateService($this->carbon, $this->config, $this->repository); - } - - /** - * Test that a key is updated. - */ - public function testKeyIsUpdated() - { - $secret = DaemonKeyRepositoryInterface::INTERNAL_KEY_IDENTIFIER . 'random_string'; - - $this->getFunctionMock('\\Pterodactyl\\Services\\DaemonKeys', 'str_random') - ->expects($this->once())->with(40)->willReturn('random_string'); - - $this->config->shouldReceive('get')->with('pterodactyl.api.key_expire_time')->once()->andReturn(100); - $this->carbon->shouldReceive('now')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('addMinutes')->with(100)->once()->andReturnSelf() - ->shouldReceive('toDateTimeString')->withNoArgs()->once()->andReturn('00:00:00'); - - $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf(); - $this->repository->shouldReceive('update')->with(123, [ - 'secret' => $secret, - 'expires_at' => '00:00:00', - ])->once()->andReturnNull(); - - $response = $this->service->handle(123); - $this->assertNotEmpty($response); - $this->assertEquals($secret, $response); - } -} diff --git a/tests/Unit/Services/DaemonKeys/RevokeMultipleDaemonKeysServiceTest.php b/tests/Unit/Services/DaemonKeys/RevokeMultipleDaemonKeysServiceTest.php deleted file mode 100644 index a10753cc0..000000000 --- a/tests/Unit/Services/DaemonKeys/RevokeMultipleDaemonKeysServiceTest.php +++ /dev/null @@ -1,116 +0,0 @@ -daemonRepository = m::mock(ServerRepositoryInterface::class); - $this->repository = m::mock(DaemonKeyRepositoryInterface::class); - } - - /** - * Test that keys can be successfully revoked. - */ - public function testSuccessfulKeyRevocation() - { - $user = factory(User::class)->make(); - $node = factory(Node::class)->make(); - $key = factory(DaemonKey::class)->make(['user_id' => $user->id]); - $key->setRelation('node', $node); - - $this->repository->shouldReceive('getKeysForRevocation')->with($user)->once()->andReturn(collect([$key])); - $this->daemonRepository->shouldReceive('setNode')->with($node)->once()->andReturnSelf(); - $this->daemonRepository->shouldReceive('revokeAccessKey')->with([$key->secret])->once()->andReturn(new Response); - - $this->repository->shouldReceive('deleteKeys')->with([$key->id])->once()->andReturnNull(); - - $this->getService()->handle($user); - $this->assertTrue(true); - } - - /** - * Test that an exception thrown by a call to the daemon is handled. - * - * @expectedException \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException - */ - public function testExceptionThrownFromDaemonCallIsHandled() - { - $this->configureExceptionMock(); - - $user = factory(User::class)->make(); - $node = factory(Node::class)->make(); - $key = factory(DaemonKey::class)->make(['user_id' => $user->id]); - $key->setRelation('node', $node); - - $this->repository->shouldReceive('getKeysForRevocation')->with($user)->once()->andReturn(collect([$key])); - $this->daemonRepository->shouldReceive('setNode->revokeAccessKey')->with([$key->secret])->once()->andThrow($this->getExceptionMock()); - - $this->getService()->handle($user); - } - - /** - * Test that the behavior for handling exceptions that should not be thrown - * immediately is working correctly and adds them to the array. - */ - public function testIgnoredExceptionsAreHandledProperly() - { - $this->configureExceptionMock(); - - $user = factory(User::class)->make(); - $node = factory(Node::class)->make(); - $key = factory(DaemonKey::class)->make(['user_id' => $user->id]); - $key->setRelation('node', $node); - - $this->repository->shouldReceive('getKeysForRevocation')->with($user)->once()->andReturn(collect([$key])); - $this->daemonRepository->shouldReceive('setNode->revokeAccessKey')->with([$key->secret])->once()->andThrow($this->getExceptionMock()); - - $this->repository->shouldReceive('deleteKeys')->with([$key->id])->once()->andReturnNull(); - - $service = $this->getService(); - $service->handle($user, true); - $this->assertNotEmpty($service->getExceptions()); - $this->assertArrayHasKey($node->id, $service->getExceptions()); - $this->assertSame(array_get($service->getExceptions(), $node->id), $this->getExceptionMock()); - $this->assertTrue(true); - } - - /** - * Return an instance of the service for testing. - * - * @return \Pterodactyl\Services\DaemonKeys\RevokeMultipleDaemonKeysService - */ - private function getService(): RevokeMultipleDaemonKeysService - { - return new RevokeMultipleDaemonKeysService($this->repository, $this->daemonRepository); - } -} diff --git a/tests/Unit/Services/Databases/DatabasePasswordServiceTest.php b/tests/Unit/Services/Databases/DatabasePasswordServiceTest.php index 25bfee164..226723e9d 100644 --- a/tests/Unit/Services/Databases/DatabasePasswordServiceTest.php +++ b/tests/Unit/Services/Databases/DatabasePasswordServiceTest.php @@ -51,13 +51,14 @@ class DatabasePasswordServiceTest extends TestCase */ public function testPasswordIsChanged() { - $model = factory(Database::class)->make(); + /** @var \Pterodactyl\Models\Database $model */ + $model = factory(Database::class)->make(['max_connections' => 0]); $this->connection->expects('transaction')->with(m::on(function ($closure) { return is_null($closure()); })); - $this->dynamic->shouldReceive('set')->with('dynamic', $model->database_host_id)->once()->andReturnNull(); + $this->dynamic->expects('set')->with('dynamic', $model->database_host_id)->andReturnNull(); $this->encrypter->expects('encrypt')->with(m::on(function ($string) { preg_match_all('/[!@+=.^-]/', $string, $matches, PREG_SET_ORDER); @@ -67,13 +68,13 @@ class DatabasePasswordServiceTest extends TestCase return true; }))->andReturn('enc123'); - $this->repository->shouldReceive('withoutFreshModel')->withNoArgs()->once()->andReturnSelf(); - $this->repository->shouldReceive('update')->with($model->id, ['password' => 'enc123'])->once()->andReturn(true); + $this->repository->expects('withoutFreshModel')->withNoArgs()->andReturnSelf(); + $this->repository->expects('update')->with($model->id, ['password' => 'enc123'])->andReturn(true); - $this->repository->shouldReceive('dropUser')->with($model->username, $model->remote)->once()->andReturn(true); - $this->repository->shouldReceive('createUser')->with($model->username, $model->remote, m::any())->once()->andReturn(true); - $this->repository->shouldReceive('assignUserToDatabase')->with($model->database, $model->username, $model->remote)->once()->andReturn(true); - $this->repository->shouldReceive('flush')->withNoArgs()->once()->andReturn(true); + $this->repository->expects('dropUser')->with($model->username, $model->remote)->andReturn(true); + $this->repository->expects('createUser')->with($model->username, $model->remote, m::any(), 0)->andReturn(true); + $this->repository->expects('assignUserToDatabase')->with($model->database, $model->username, $model->remote)->andReturn(true); + $this->repository->expects('flush')->withNoArgs()->andReturn(true); $response = $this->getService()->handle($model); $this->assertNotEmpty($response); diff --git a/tests/Unit/Services/Databases/DeployServerDatabaseServiceTest.php b/tests/Unit/Services/Databases/DeployServerDatabaseServiceTest.php index b709417d2..fc86cdcfc 100644 --- a/tests/Unit/Services/Databases/DeployServerDatabaseServiceTest.php +++ b/tests/Unit/Services/Databases/DeployServerDatabaseServiceTest.php @@ -10,6 +10,7 @@ use Pterodactyl\Services\Databases\DatabaseManagementService; use Pterodactyl\Services\Databases\DeployServerDatabaseService; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; +use Pterodactyl\Exceptions\Service\Database\NoSuitableDatabaseHostException; class DeployServerDatabaseServiceTest extends TestCase { @@ -51,16 +52,9 @@ class DeployServerDatabaseServiceTest extends TestCase */ public function testNonRandomFoundHost($limit, $count) { - config()->set('pterodactyl.client_features.databases.allow_random', false); - $server = factory(Server::class)->make(['database_limit' => $limit]); $model = factory(Database::class)->make(); - $this->repository->shouldReceive('findCountWhere') - ->once() - ->with([['server_id', '=', $server->id]]) - ->andReturn($count); - $this->databaseHostRepository->shouldReceive('setColumns->findWhere') ->once() ->with([['node_id', '=', $server->node_id]]) @@ -68,7 +62,7 @@ class DeployServerDatabaseServiceTest extends TestCase $this->managementService->shouldReceive('create') ->once() - ->with($server->id, [ + ->with($server, [ 'database_host_id' => $model->id, 'database' => 'testdb', 'remote' => null, @@ -83,25 +77,20 @@ class DeployServerDatabaseServiceTest extends TestCase /** * Test that an exception is thrown if in non-random mode and no host is found. - * - * @expectedException \Pterodactyl\Exceptions\Service\Database\NoSuitableDatabaseHostException */ public function testNonRandomNoHost() { - config()->set('pterodactyl.client_features.databases.allow_random', false); + $this->expectException(NoSuitableDatabaseHostException::class); $server = factory(Server::class)->make(['database_limit' => 1]); - $this->repository->shouldReceive('findCountWhere') - ->once() - ->with([['server_id', '=', $server->id]]) - ->andReturn(0); - $this->databaseHostRepository->shouldReceive('setColumns->findWhere') ->once() ->with([['node_id', '=', $server->node_id]]) ->andReturn(collect()); + $this->databaseHostRepository->expects('setColumns->all')->withNoArgs()->andReturn(collect()); + $this->getService()->handle($server, []); } @@ -113,11 +102,6 @@ class DeployServerDatabaseServiceTest extends TestCase $server = factory(Server::class)->make(['database_limit' => 1]); $model = factory(Database::class)->make(); - $this->repository->shouldReceive('findCountWhere') - ->once() - ->with([['server_id', '=', $server->id]]) - ->andReturn(0); - $this->databaseHostRepository->shouldReceive('setColumns->findWhere') ->once() ->with([['node_id', '=', $server->node_id]]) @@ -129,7 +113,7 @@ class DeployServerDatabaseServiceTest extends TestCase $this->managementService->shouldReceive('create') ->once() - ->with($server->id, [ + ->with($server, [ 'database_host_id' => $model->id, 'database' => 'testdb', 'remote' => null, @@ -144,60 +128,22 @@ class DeployServerDatabaseServiceTest extends TestCase /** * Test that an exception is thrown when no host is found and random is allowed. - * - * @expectedException \Pterodactyl\Exceptions\Service\Database\NoSuitableDatabaseHostException */ public function testRandomNoHost() { + $this->expectException(NoSuitableDatabaseHostException::class); + $server = factory(Server::class)->make(['database_limit' => 1]); - $this->repository->shouldReceive('findCountWhere') - ->once() - ->with([['server_id', '=', $server->id]]) - ->andReturn(0); - - $this->databaseHostRepository->shouldReceive('setColumns->findWhere') - ->once() + $this->databaseHostRepository->expects('setColumns->findWhere') ->with([['node_id', '=', $server->node_id]]) ->andReturn(collect()); - $this->databaseHostRepository->shouldReceive('setColumns->all') - ->once() - ->andReturn(collect()); + $this->databaseHostRepository->expects('setColumns->all')->withNoArgs()->andReturn(collect()); $this->getService()->handle($server, []); } - /** - * Test that a server over the database limit throws an exception. - * - * @dataProvider databaseExceedingLimitDataProvider - * @expectedException \Pterodactyl\Exceptions\Service\Database\TooManyDatabasesException - */ - public function testServerOverDatabaseLimit($limit, $count) - { - $server = factory(Server::class)->make(['database_limit' => $limit]); - - $this->repository->shouldReceive('findCountWhere') - ->once() - ->with([['server_id', '=', $server->id]]) - ->andReturn($count); - - $this->getService()->handle($server, []); - } - - /** - * Test that an exception is thrown if the feature is not enabled. - * - * @expectedException \Pterodactyl\Exceptions\Service\Database\DatabaseClientFeatureNotEnabledException - */ - public function testFeatureNotEnabled() - { - config()->set('pterodactyl.client_features.databases.enabled', false); - - $this->getService()->handle(factory(Server::class)->make(), []); - } - /** * Provide limits and current database counts for testing. * diff --git a/tests/Unit/Services/Eggs/EggConfigurationServiceTest.php b/tests/Unit/Services/Eggs/EggConfigurationServiceTest.php deleted file mode 100644 index f6b1bebb9..000000000 --- a/tests/Unit/Services/Eggs/EggConfigurationServiceTest.php +++ /dev/null @@ -1,90 +0,0 @@ -repository = m::mock(EggRepositoryInterface::class); - - $this->service = new EggConfigurationService($this->repository); - } - - /** - * Test that the correct array is returned. - */ - public function testCorrectArrayIsReturned() - { - $egg = factory(Egg::class)->make([ - 'config_startup' => '{"test": "start"}', - 'config_stop' => 'test', - 'config_files' => '{"test": "file"}', - 'config_logs' => '{"test": "logs"}', - ]); - - $response = $this->service->handle($egg); - $this->assertNotEmpty($response); - $this->assertTrue(is_array($response), 'Assert response is an array.'); - $this->assertArrayHasKey('startup', $response); - $this->assertArrayHasKey('stop', $response); - $this->assertArrayHasKey('configs', $response); - $this->assertArrayHasKey('log', $response); - $this->assertArrayHasKey('query', $response); - $this->assertEquals('start', object_get($response['startup'], 'test')); - $this->assertEquals('test', 'test'); - $this->assertEquals('file', object_get($response['configs'], 'test')); - $this->assertEquals('logs', object_get($response['log'], 'test')); - $this->assertEquals('none', $response['query']); - } - - /** - * Test that an integer referencing a model can be passed in place of the model. - */ - public function testFunctionHandlesIntegerPassedInPlaceOfModel() - { - $egg = factory(Egg::class)->make([ - 'config_startup' => '{"test": "start"}', - 'config_stop' => 'test', - 'config_files' => '{"test": "file"}', - 'config_logs' => '{"test": "logs"}', - ]); - - $this->repository->shouldReceive('getWithCopyAttributes')->with($egg->id)->once()->andReturn($egg); - - $response = $this->service->handle($egg->id); - $this->assertNotEmpty($response); - $this->assertTrue(is_array($response), 'Assert response is an array.'); - $this->assertArrayHasKey('startup', $response); - $this->assertArrayHasKey('stop', $response); - $this->assertArrayHasKey('configs', $response); - $this->assertArrayHasKey('log', $response); - $this->assertArrayHasKey('query', $response); - $this->assertEquals('start', object_get($response['startup'], 'test')); - $this->assertEquals('test', 'test'); - $this->assertEquals('file', object_get($response['configs'], 'test')); - $this->assertEquals('logs', object_get($response['log'], 'test')); - $this->assertEquals('none', $response['query']); - } -} diff --git a/tests/Unit/Services/Eggs/Variables/VariableCreationServiceTest.php b/tests/Unit/Services/Eggs/Variables/VariableCreationServiceTest.php index 34c7bdbdd..bbac6009d 100644 --- a/tests/Unit/Services/Eggs/Variables/VariableCreationServiceTest.php +++ b/tests/Unit/Services/Eggs/Variables/VariableCreationServiceTest.php @@ -9,6 +9,8 @@ use Pterodactyl\Models\EggVariable; use Illuminate\Contracts\Validation\Factory; use Pterodactyl\Services\Eggs\Variables\VariableCreationService; use Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface; +use Pterodactyl\Exceptions\Service\Egg\Variable\BadValidationRuleException; +use Pterodactyl\Exceptions\Service\Egg\Variable\ReservedVariableNameException; class VariableCreationServiceTest extends TestCase { @@ -91,10 +93,11 @@ class VariableCreationServiceTest extends TestCase * @param string $variable * * @dataProvider reservedNamesProvider - * @expectedException \Pterodactyl\Exceptions\Service\Egg\Variable\ReservedVariableNameException */ public function testExceptionIsThrownIfEnvironmentVariableIsInListOfReservedNames(string $variable) { + $this->expectException(ReservedVariableNameException::class); + $this->getService()->handle(1, ['env_variable' => $variable]); } @@ -114,12 +117,12 @@ class VariableCreationServiceTest extends TestCase /** * Test that validation errors due to invalid rules are caught and handled properly. - * - * @expectedException \Pterodactyl\Exceptions\Service\Egg\Variable\BadValidationRuleException - * @expectedExceptionMessage The validation rule "hodor_door" is not a valid rule for this application. */ public function testInvalidValidationRulesResultInException() { + $this->expectException(BadValidationRuleException::class); + $this->expectExceptionMessage('The validation rule "hodor_door" is not a valid rule for this application.'); + $data = ['env_variable' => 'TEST_VAR_123', 'rules' => 'string|hodorDoor']; $this->validator->shouldReceive('make')->once() @@ -135,12 +138,12 @@ class VariableCreationServiceTest extends TestCase /** * Test that an exception not stemming from a bad rule is not caught. - * - * @expectedException \BadMethodCallException - * @expectedExceptionMessage Received something, but no expectations were specified. */ public function testExceptionNotCausedByBadRuleIsNotCaught() { + $this->expectException(BadMethodCallException::class); + $this->expectExceptionMessage('Received something, but no expectations were specified.'); + $data = ['env_variable' => 'TEST_VAR_123', 'rules' => 'string']; $this->validator->shouldReceive('make')->once() diff --git a/tests/Unit/Services/Eggs/Variables/VariableUpdateServiceTest.php b/tests/Unit/Services/Eggs/Variables/VariableUpdateServiceTest.php index 82dd00c0b..a812da274 100644 --- a/tests/Unit/Services/Eggs/Variables/VariableUpdateServiceTest.php +++ b/tests/Unit/Services/Eggs/Variables/VariableUpdateServiceTest.php @@ -11,6 +11,8 @@ use Illuminate\Contracts\Validation\Factory; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Services\Eggs\Variables\VariableUpdateService; use Pterodactyl\Contracts\Repository\EggVariableRepositoryInterface; +use Pterodactyl\Exceptions\Service\Egg\Variable\BadValidationRuleException; +use Pterodactyl\Exceptions\Service\Egg\Variable\ReservedVariableNameException; class VariableUpdateServiceTest extends TestCase { @@ -159,21 +161,22 @@ class VariableUpdateServiceTest extends TestCase * Test that all of the reserved variables defined in the model trigger an exception. * * @dataProvider reservedNamesProvider - * @expectedException \Pterodactyl\Exceptions\Service\Egg\Variable\ReservedVariableNameException */ public function testExceptionIsThrownIfEnvironmentVariableIsInListOfReservedNames(string $variable) { + $this->expectException(ReservedVariableNameException::class); + $this->getService()->handle($this->model, ['env_variable' => $variable]); } /** * Test that validation errors due to invalid rules are caught and handled properly. - * - * @expectedException \Pterodactyl\Exceptions\Service\Egg\Variable\BadValidationRuleException - * @expectedExceptionMessage The validation rule "hodor_door" is not a valid rule for this application. */ public function testInvalidValidationRulesResultInException() { + $this->expectException(BadValidationRuleException::class); + $this->expectExceptionMessage('The validation rule "hodor_door" is not a valid rule for this application.'); + $data = ['env_variable' => 'TEST_VAR_123', 'rules' => 'string|hodorDoor']; $this->repository->shouldReceive('setColumns->findCountWhere')->once()->andReturn(0); @@ -191,12 +194,12 @@ class VariableUpdateServiceTest extends TestCase /** * Test that an exception not stemming from a bad rule is not caught. - * - * @expectedException \BadMethodCallException - * @expectedExceptionMessage Received something, but no expectations were specified. */ public function testExceptionNotCausedByBadRuleIsNotCaught() { + $this->expectException(BadMethodCallException::class); + $this->expectExceptionMessage('Received something, but no expectations were specified.'); + $data = ['rules' => 'string']; $this->validator->shouldReceive('make')->once() diff --git a/tests/Unit/Services/Helpers/SoftwareVersionServiceTest.php b/tests/Unit/Services/Helpers/SoftwareVersionServiceTest.php deleted file mode 100644 index d0ada1b4a..000000000 --- a/tests/Unit/Services/Helpers/SoftwareVersionServiceTest.php +++ /dev/null @@ -1,168 +0,0 @@ -. - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ - -namespace Tests\Unit\Services\Helpers; - -use Closure; -use Mockery as m; -use Tests\TestCase; -use GuzzleHttp\Client; -use Pterodactyl\Services\Helpers\SoftwareVersionService; -use Illuminate\Contracts\Cache\Repository as CacheRepository; -use Illuminate\Contracts\Config\Repository as ConfigRepository; - -class SoftwareVersionServiceTest extends TestCase -{ - /** - * @var \Illuminate\Contracts\Cache\Repository - */ - protected $cache; - - /** - * @var \GuzzleHttp\Client - */ - protected $client; - - /** - * @var \Illuminate\Contracts\Config\Repository - */ - protected $config; - - /** - * @var object - */ - protected static $response = [ - 'panel' => '0.2.0', - 'daemon' => '0.1.0', - 'discord' => 'https://pterodactyl.io/discord', - ]; - - /** - * @var \Pterodactyl\Services\Helpers\SoftwareVersionService - */ - protected $service; - - /** - * Setup tests. - */ - public function setUp(): void - { - parent::setUp(); - - self::$response = (object) self::$response; - - $this->cache = m::mock(CacheRepository::class); - $this->client = m::mock(Client::class); - $this->config = m::mock(ConfigRepository::class); - - $this->config->shouldReceive('get')->with('pterodactyl.cdn.cache_time')->once()->andReturn(60); - - $this->cache->shouldReceive('remember')->with(SoftwareVersionService::VERSION_CACHE_KEY, 60, Closure::class)->once()->andReturnNull(); - - $this->service = m::mock(SoftwareVersionService::class, [$this->cache, $this->client, $this->config])->makePartial(); - } - - /** - * Test that the panel version is returned. - */ - public function testPanelVersionIsReturned() - { - $this->cache->shouldReceive('get')->with(SoftwareVersionService::VERSION_CACHE_KEY)->once()->andReturn(self::$response); - $this->assertEquals(self::$response->panel, $this->service->getPanel()); - } - - /** - * Test that the panel version is returned as error. - */ - public function testPanelVersionIsReturnedAsErrorIfNoKeyIsFound() - { - $this->cache->shouldReceive('get')->with(SoftwareVersionService::VERSION_CACHE_KEY)->once()->andReturn((object) []); - $this->assertEquals('error', $this->service->getPanel()); - } - - /** - * Test that the daemon version is returned. - */ - public function testDaemonVersionIsReturned() - { - $this->cache->shouldReceive('get')->with(SoftwareVersionService::VERSION_CACHE_KEY)->once()->andReturn(self::$response); - $this->assertEquals(self::$response->daemon, $this->service->getDaemon()); - } - - /** - * Test that the daemon version is returned as an error. - */ - public function testDaemonVersionIsReturnedAsErrorIfNoKeyIsFound() - { - $this->cache->shouldReceive('get')->with(SoftwareVersionService::VERSION_CACHE_KEY)->once()->andReturn((object) []); - $this->assertEquals('error', $this->service->getDaemon()); - } - - /** - * Test that the discord URL is returned. - */ - public function testDiscordUrlIsReturned() - { - $this->cache->shouldReceive('get')->with(SoftwareVersionService::VERSION_CACHE_KEY)->once()->andReturn(self::$response); - $this->assertEquals(self::$response->discord, $this->service->getDiscord()); - } - - /** - * Test that the correct boolean value is returned by the helper for each version passed. - * - * @dataProvider panelVersionProvider - */ - public function testCorrectBooleanValueIsReturnedWhenCheckingPanelVersion($version, $response) - { - $this->config->shouldReceive('get')->with('app.version')->andReturn($version); - $this->service->shouldReceive('getPanel')->withNoArgs()->andReturn(self::$response->panel); - - $this->assertEquals($response, $this->service->isLatestPanel()); - } - - /** - * Test that the correct boolean value is returned. - * - * @dataProvider daemonVersionProvider - */ - public function testCorrectBooleanValueIsReturnedWhenCheckingDaemonVersion($version, $response) - { - $this->service->shouldReceive('getDaemon')->withNoArgs()->andReturn(self::$response->daemon); - - $this->assertEquals($response, $this->service->isLatestDaemon($version)); - } - - /** - * Provide data for testing boolean response on panel version. - * - * @return array - */ - public function panelVersionProvider() - { - return [ - [self::$response['panel'], true], - ['0.0.1', false], - ['canary', true], - ]; - } - - /** - * Provide data for testing boolean response for daemon version. - * - * @return array - */ - public function daemonVersionProvider() - { - return [ - [self::$response['daemon'], true], - ['0.0.1', false], - ['0.0.0-canary', true], - ]; - } -} diff --git a/tests/Unit/Services/Nodes/NodeCreationServiceTest.php b/tests/Unit/Services/Nodes/NodeCreationServiceTest.php index bf7cb05ed..561a14acc 100644 --- a/tests/Unit/Services/Nodes/NodeCreationServiceTest.php +++ b/tests/Unit/Services/Nodes/NodeCreationServiceTest.php @@ -1,17 +1,14 @@ . - * - * This software is licensed under the terms of the MIT license. - * https://opensource.org/licenses/MIT - */ namespace Tests\Unit\Services\Nodes; use Mockery as m; use Tests\TestCase; +use Ramsey\Uuid\Uuid; use phpmock\phpunit\PHPMock; +use Pterodactyl\Models\Node; +use Ramsey\Uuid\UuidFactory; +use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Services\Nodes\NodeCreationService; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; @@ -20,14 +17,14 @@ class NodeCreationServiceTest extends TestCase use PHPMock; /** - * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface + * @var \Mockery\MockInterface */ - protected $repository; + private $repository; /** - * @var \Pterodactyl\Services\Nodes\NodeCreationService + * @var \Mockery\MockInterface */ - protected $service; + private $encrypter; /** * Setup tests. @@ -36,9 +33,15 @@ class NodeCreationServiceTest extends TestCase { parent::setUp(); - $this->repository = m::mock(NodeRepositoryInterface::class); + /* @noinspection PhpParamsInspection */ + Uuid::setFactory( + m::mock(UuidFactory::class . '[uuid4]', [ + 'uuid4' => Uuid::fromString('00000000-0000-0000-0000-000000000000'), + ]) + ); - $this->service = new NodeCreationService($this->repository); + $this->repository = m::mock(NodeRepositoryInterface::class); + $this->encrypter = m::mock(Encrypter::class); } /** @@ -46,14 +49,31 @@ class NodeCreationServiceTest extends TestCase */ public function testNodeIsCreatedAndDaemonSecretIsGenerated() { - $this->getFunctionMock('\\Pterodactyl\\Services\\Nodes', 'str_random') - ->expects($this->once())->willReturn('random_string'); + /** @var \Pterodactyl\Models\Node $node */ + $node = factory(Node::class)->make(); - $this->repository->shouldReceive('create')->with([ - 'name' => 'NodeName', - 'daemonSecret' => 'random_string', - ])->once()->andReturnNull(); + $this->encrypter->expects('encrypt')->with(m::on(function ($value) { + return strlen($value) === Node::DAEMON_TOKEN_LENGTH; + }))->andReturns('encrypted_value'); - $this->assertNull($this->service->handle(['name' => 'NodeName'])); + $this->repository->expects('create')->with(m::on(function ($value) { + $this->assertTrue(is_array($value)); + $this->assertSame('NodeName', $value['name']); + $this->assertSame('00000000-0000-0000-0000-000000000000', $value['uuid']); + $this->assertSame('encrypted_value', $value['daemon_token']); + $this->assertTrue(strlen($value['daemon_token_id']) === Node::DAEMON_TOKEN_ID_LENGTH); + + return true; + }), true, true)->andReturn($node); + + $this->assertSame($node, $this->getService()->handle(['name' => 'NodeName'])); + } + + /** + * @return \Pterodactyl\Services\Nodes\NodeCreationService + */ + private function getService() + { + return new NodeCreationService($this->encrypter, $this->repository); } } diff --git a/tests/Unit/Services/Nodes/NodeDeletionServiceTest.php b/tests/Unit/Services/Nodes/NodeDeletionServiceTest.php index eb2f05f69..88ebaaaf5 100644 --- a/tests/Unit/Services/Nodes/NodeDeletionServiceTest.php +++ b/tests/Unit/Services/Nodes/NodeDeletionServiceTest.php @@ -12,6 +12,7 @@ namespace Tests\Unit\Services\Nodes; use Mockery as m; use Tests\TestCase; use Pterodactyl\Models\Node; +use Pterodactyl\Exceptions\DisplayException; use Illuminate\Contracts\Translation\Translator; use Pterodactyl\Services\Nodes\NodeDeletionService; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; @@ -71,11 +72,11 @@ class NodeDeletionServiceTest extends TestCase /** * Test that an exception is thrown if servers are attached to the node. - * - * @expectedException \Pterodactyl\Exceptions\DisplayException */ public function testExceptionIsThrownIfServersAreAttachedToNode() { + $this->expectException(DisplayException::class); + $this->serverRepository->shouldReceive('setColumns')->with('id')->once()->andReturnSelf() ->shouldReceive('findCountWhere')->with([['node_id', '=', 1]])->once()->andReturn(1); $this->translator->shouldReceive('trans')->with('exceptions.node.servers_attached')->once()->andReturnNull(); diff --git a/tests/Unit/Services/Nodes/NodeUpdateServiceTest.php b/tests/Unit/Services/Nodes/NodeUpdateServiceTest.php index c8596b66b..c8138185d 100644 --- a/tests/Unit/Services/Nodes/NodeUpdateServiceTest.php +++ b/tests/Unit/Services/Nodes/NodeUpdateServiceTest.php @@ -2,34 +2,44 @@ namespace Tests\Unit\Services\Nodes; +use Exception; use Mockery as m; use Tests\TestCase; +use GuzzleHttp\Psr7\Request; use phpmock\phpunit\PHPMock; use Pterodactyl\Models\Node; -use GuzzleHttp\Psr7\Response; use Tests\Traits\MocksRequestException; use GuzzleHttp\Exception\ConnectException; +use GuzzleHttp\Exception\TransferException; use Illuminate\Database\ConnectionInterface; +use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Services\Nodes\NodeUpdateService; -use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; -use Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface; +use Pterodactyl\Repositories\Eloquent\NodeRepository; +use Pterodactyl\Repositories\Wings\DaemonConfigurationRepository; +use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException; +use Pterodactyl\Exceptions\Service\Node\ConfigurationNotPersistedException; class NodeUpdateServiceTest extends TestCase { use PHPMock, MocksRequestException; /** - * @var \Illuminate\Database\ConnectionInterface|\Mockery\Mock + * @var \Mockery\MockInterface */ private $connection; /** - * @var \Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface|\Mockery\Mock + * @var \Mockery\MockInterface */ - private $configRepository; + private $configurationRepository; /** - * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface|\Mockery\Mock + * @var \Mockery\MockInterface + */ + private $encrypter; + + /** + * @var \Mockery\MockInterface */ private $repository; @@ -41,8 +51,9 @@ class NodeUpdateServiceTest extends TestCase parent::setUp(); $this->connection = m::mock(ConnectionInterface::class); - $this->configRepository = m::mock(ConfigurationRepositoryInterface::class); - $this->repository = m::mock(NodeRepositoryInterface::class); + $this->encrypter = m::mock(Encrypter::class); + $this->configurationRepository = m::mock(DaemonConfigurationRepository::class); + $this->repository = m::mock(NodeRepository::class); } /** @@ -50,36 +61,59 @@ class NodeUpdateServiceTest extends TestCase */ public function testNodeIsUpdatedAndDaemonSecretIsReset() { - $model = factory(Node::class)->make(); - $updatedModel = factory(Node::class)->make([ - 'name' => 'New Name', - 'daemonSecret' => 'abcd1234', + /** @var \Pterodactyl\Models\Node $model */ + $model = factory(Node::class)->make([ + 'fqdn' => 'https://example.com', ]); - $this->getFunctionMock('\\Pterodactyl\\Services\\Nodes', 'str_random') - ->expects($this->once())->willReturn($updatedModel->daemonSecret); + /** @var \Pterodactyl\Models\Node $updatedModel */ + $updatedModel = factory(Node::class)->make([ + 'name' => 'New Name', + 'fqdn' => 'https://example2.com', + ]); - $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('update')->with($model->id, [ + $this->connection->expects('transaction')->with(m::on(function ($closure) use ($updatedModel) { + $response = $closure(); + + $this->assertIsArray($response); + $this->assertTrue(count($response) === 2); + $this->assertSame($updatedModel, $response[0]); + $this->assertFalse($response[1]); + + return true; + }))->andReturns([$updatedModel, false]); + + $this->encrypter->expects('encrypt')->with(m::on(function ($value) { + return strlen($value) === Node::DAEMON_TOKEN_LENGTH; + }))->andReturns('encrypted_value'); + + $this->repository->expects('withFreshModel->update')->with($model->id, m::on(function ($value) { + $this->assertTrue(is_array($value)); + $this->assertSame('New Name', $value['name']); + $this->assertSame('encrypted_value', $value['daemon_token']); + $this->assertTrue(strlen($value['daemon_token_id']) === Node::DAEMON_TOKEN_ID_LENGTH); + + return true; + }), true, true)->andReturns($updatedModel); + + $this->configurationRepository->expects('setNode')->with(m::on(function ($value) use ($model, $updatedModel) { + $this->assertInstanceOf(Node::class, $value); + $this->assertSame($model->uuid, $value->uuid); + + // Yes, this is correct. Always use the updated model's FQDN when making requests to + // the Daemon so that any changes to that are properly propagated down to the daemon. + // + // @see https://github.com/pterodactyl/panel/issues/1931 + $this->assertSame($updatedModel->fqdn, $value->fqdn); + + return true; + }))->andReturnSelf(); + + $this->configurationRepository->expects('update')->with($updatedModel); + + $this->getService()->handle($model, [ 'name' => $updatedModel->name, - 'daemonSecret' => $updatedModel->daemonSecret, - ])->andReturn($model); - - $cloned = $updatedModel->replicate(['daemonSecret']); - $cloned->daemonSecret = $model->daemonSecret; - - $this->configRepository->shouldReceive('setNode')->with(m::on(function ($model) use ($updatedModel) { - return $model->daemonSecret !== $updatedModel->daemonSecret; - }))->once()->andReturnSelf(); - - $this->configRepository->shouldReceive('update')->with([ - 'keys' => ['abcd1234'], - ])->once()->andReturn(new Response); - - $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); - - $response = $this->getService()->handle($model, ['name' => $updatedModel->name], true); - $this->assertInstanceOf(Node::class, $response); + ], true); } /** @@ -87,56 +121,115 @@ class NodeUpdateServiceTest extends TestCase */ public function testNodeIsUpdatedAndDaemonSecretIsNotChanged() { - $model = factory(Node::class)->make(); + /** @var \Pterodactyl\Models\Node $model */ + $model = factory(Node::class)->make(['fqdn' => 'https://example.com']); - $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('update')->with($model->id, [ - 'name' => 'NewName', - ])->andReturn($model); + /** @var \Pterodactyl\Models\Node $updatedModel */ + $updatedModel = factory(Node::class)->make(['name' => 'New Name', 'fqdn' => $model->fqdn]); - $this->configRepository->shouldReceive('setNode')->with($model)->once()->andReturnSelf() - ->shouldReceive('update')->withNoArgs()->once()->andReturn(new Response); - $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + $this->connection->expects('transaction')->with(m::on(function ($closure) use ($updatedModel) { + $response = $closure(); - $response = $this->getService()->handle($model, ['name' => 'NewName']); - $this->assertInstanceOf(Node::class, $response); + $this->assertIsArray($response); + $this->assertTrue(count($response) === 2); + $this->assertSame($updatedModel, $response[0]); + $this->assertFalse($response[1]); + + return true; + }))->andReturns([$updatedModel, false]); + + $this->repository->expects('withFreshModel->update')->with($model->id, m::on(function ($value) { + $this->assertTrue(is_array($value)); + $this->assertSame('New Name', $value['name']); + $this->assertArrayNotHasKey('daemon_token', $value); + $this->assertArrayNotHasKey('daemon_token_id', $value); + + return true; + }), true, true)->andReturns($updatedModel); + + $this->configurationRepository->expects('setNode->update')->with($updatedModel); + + $this->getService()->handle($model, ['name' => $updatedModel->name]); } /** * Test that an exception caused by a connection error is handled. - * - * @expectedException \Pterodactyl\Exceptions\Service\Node\ConfigurationNotPersistedException */ public function testExceptionRelatedToConnection() { - $this->configureExceptionMock(ConnectException::class); - $model = factory(Node::class)->make(); + $this->configureExceptionMock(DaemonConnectionException::class); + $this->expectException(ConfigurationNotPersistedException::class); - $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('update')->andReturn($model); + /** @var \Pterodactyl\Models\Node $model */ + $model = factory(Node::class)->make(['fqdn' => 'https://example.com']); - $this->configRepository->shouldReceive('setNode->update')->once()->andThrow($this->getExceptionMock()); - $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + /** @var \Pterodactyl\Models\Node $updatedModel */ + $updatedModel = factory(Node::class)->make(['name' => 'New Name', 'fqdn' => $model->fqdn]); - $this->getService()->handle($model, ['name' => 'NewName']); + $this->connection->expects('transaction')->with(m::on(function ($closure) use ($updatedModel) { + $response = $closure(); + + $this->assertIsArray($response); + $this->assertTrue(count($response) === 2); + $this->assertSame($updatedModel, $response[0]); + $this->assertTrue($response[1]); + + return true; + }))->andReturn([$updatedModel, true]); + + $this->repository->expects('withFreshModel->update')->with($model->id, m::on(function ($value) { + $this->assertTrue(is_array($value)); + $this->assertSame('New Name', $value['name']); + $this->assertArrayNotHasKey('daemon_token', $value); + $this->assertArrayNotHasKey('daemon_token_id', $value); + + return true; + }), true, true)->andReturns($updatedModel); + + $this->configurationRepository->expects('setNode->update')->with($updatedModel)->andThrow( + new DaemonConnectionException( + new ConnectException('', new Request('GET', 'Test'), new Exception) + ) + ); + + $this->getService()->handle($model, ['name' => $updatedModel->name]); } /** * Test that an exception not caused by a daemon connection error is handled. - * - * @expectedException \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException */ public function testExceptionNotRelatedToConnection() { - $this->configureExceptionMock(); - $model = factory(Node::class)->make(); + /** @var \Pterodactyl\Models\Node $model */ + $model = factory(Node::class)->make(['fqdn' => 'https://example.com']); - $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('update')->andReturn($model); + /** @var \Pterodactyl\Models\Node $updatedModel */ + $updatedModel = factory(Node::class)->make(['name' => 'New Name', 'fqdn' => $model->fqdn]); - $this->configRepository->shouldReceive('setNode->update')->once()->andThrow($this->getExceptionMock()); + $this->connection->expects('transaction')->with(m::on(function ($closure) use ($updatedModel) { + try { + $closure(); + } catch (Exception $exception) { + $this->assertInstanceOf(DaemonConnectionException::class, $exception); + $this->assertSame( + 'There was an exception while attempting to communicate with the daemon resulting in a HTTP/E_CONN_REFUSED response code. This exception has been logged.', + $exception->getMessage() + ); - $this->getService()->handle($model, ['name' => 'NewName']); + return true; + } + + return false; + })); + + $this->repository->expects('withFreshModel->update')->andReturns($updatedModel); + $this->configurationRepository->expects('setNode->update')->andThrow( + new DaemonConnectionException( + new TransferException('', 500, new Exception) + ) + ); + + $this->getService()->handle($model, ['name' => $updatedModel->name]); } /** @@ -146,6 +239,8 @@ class NodeUpdateServiceTest extends TestCase */ private function getService(): NodeUpdateService { - return new NodeUpdateService($this->connection, $this->configRepository, $this->repository); + return new NodeUpdateService( + $this->connection, $this->encrypter, $this->configurationRepository, $this->repository + ); } } diff --git a/tests/Unit/Services/Packs/ExportPackServiceTest.php b/tests/Unit/Services/Packs/ExportPackServiceTest.php index 031e1fb39..936b7b06d 100644 --- a/tests/Unit/Services/Packs/ExportPackServiceTest.php +++ b/tests/Unit/Services/Packs/ExportPackServiceTest.php @@ -17,6 +17,7 @@ use Pterodactyl\Models\Pack; use Illuminate\Contracts\Filesystem\Factory; use Pterodactyl\Services\Packs\ExportPackService; use Pterodactyl\Contracts\Repository\PackRepositoryInterface; +use Pterodactyl\Exceptions\Service\Pack\ZipArchiveCreationException; class ExportPackServiceTest extends TestCase { @@ -132,11 +133,11 @@ class ExportPackServiceTest extends TestCase /** * Test that an exception is thrown when a ZipArchive cannot be created. - * - * @expectedException \Pterodactyl\Exceptions\Service\Pack\ZipArchiveCreationException */ public function testExceptionIsThrownIfZipArchiveCannotBeCreated() { + $this->expectException(ZipArchiveCreationException::class); + $this->setupTestData(); $this->getFunctionMock('\\Pterodactyl\\Services\\Packs', 'tempnam') diff --git a/tests/Unit/Services/Servers/ReinstallServerServiceTest.php b/tests/Unit/Services/Servers/ReinstallServerServiceTest.php index 8bd95f0b8..22cc35199 100644 --- a/tests/Unit/Services/Servers/ReinstallServerServiceTest.php +++ b/tests/Unit/Services/Servers/ReinstallServerServiceTest.php @@ -9,48 +9,30 @@ namespace Tests\Unit\Services\Servers; -use Exception; use Mockery as m; use Tests\TestCase; -use GuzzleHttp\Psr7\Response; use Pterodactyl\Models\Server; -use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; +use Pterodactyl\Repositories\Eloquent\ServerRepository; use Pterodactyl\Services\Servers\ReinstallServerService; -use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; -use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; +use Pterodactyl\Repositories\Wings\DaemonServerRepository; class ReinstallServerServiceTest extends TestCase { /** - * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface + * @var \Pterodactyl\Repositories\Wings\DaemonServerRepository */ - protected $daemonServerRepository; + private $daemonServerRepository; /** * @var \Illuminate\Database\ConnectionInterface */ - protected $database; - - /** - * @var \GuzzleHttp\Exception\RequestException - */ - protected $exception; + private $connection; /** * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface */ - protected $repository; - - /** - * @var \Pterodactyl\Models\Server - */ - protected $server; - - /** - * @var \Pterodactyl\Services\Servers\ReinstallServerService - */ - protected $service; + private $repository; /** * Setup tests. @@ -59,18 +41,9 @@ class ReinstallServerServiceTest extends TestCase { parent::setUp(); - $this->daemonServerRepository = m::mock(DaemonServerRepositoryInterface::class); - $this->database = m::mock(ConnectionInterface::class); - $this->exception = m::mock(RequestException::class)->makePartial(); - $this->repository = m::mock(ServerRepositoryInterface::class); - - $this->server = factory(Server::class)->make(['node_id' => 1]); - - $this->service = new ReinstallServerService( - $this->database, - $this->daemonServerRepository, - $this->repository - ); + $this->repository = m::mock(ServerRepository::class); + $this->connection = m::mock(ConnectionInterface::class); + $this->daemonServerRepository = m::mock(DaemonServerRepository::class); } /** @@ -78,70 +51,32 @@ class ReinstallServerServiceTest extends TestCase */ public function testServerShouldBeReinstalledWhenModelIsPassed() { - $this->repository->shouldNotReceive('find'); + /** @var \Pterodactyl\Models\Server $server */ + $server = factory(Server::class)->make(['id' => 123]); + $updated = clone $server; + $updated->installed = Server::STATUS_INSTALLING; - $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('withoutFreshModel->update')->with($this->server->id, [ - 'installed' => 0, - ], true, true)->once()->andReturnNull(); + $this->connection->expects('transaction')->with(m::on(function ($closure) use ($updated) { + return $closure() instanceof Server; + }))->andReturn($updated); - $this->daemonServerRepository->shouldReceive('setServer')->with($this->server)->once()->andReturnSelf() - ->shouldReceive('reinstall')->withNoArgs()->once()->andReturn(new Response); - $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + $this->repository->expects('update')->with($server->id, [ + 'installed' => Server::STATUS_INSTALLING, + ])->andReturns($updated); - $this->service->reinstall($this->server); + $this->daemonServerRepository->expects('setServer')->with($server)->andReturnSelf(); + $this->daemonServerRepository->expects('reinstall')->withNoArgs(); + + $this->assertSame($updated, $this->getService()->reinstall($server)); } /** - * Test that a server is reinstalled when the ID of the server is passed to the function. + * @return \Pterodactyl\Services\Servers\ReinstallServerService */ - public function testServerShouldBeReinstalledWhenServerIdIsPassed() + private function getService() { - $this->repository->shouldReceive('find')->with($this->server->id)->once()->andReturn($this->server); - - $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('withoutFreshModel->update')->with($this->server->id, [ - 'installed' => 0, - ], true, true)->once()->andReturnNull(); - - $this->daemonServerRepository->shouldReceive('setServer')->with($this->server)->once()->andReturnSelf() - ->shouldReceive('reinstall')->withNoArgs()->once()->andReturn(new Response); - $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); - - $this->service->reinstall($this->server->id); - } - - /** - * Test that an exception thrown by guzzle is rendered as a displayable exception. - * - * @expectedException \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException - */ - public function testExceptionThrownByGuzzleShouldBeReRenderedAsDisplayable() - { - $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('withoutFreshModel->update')->with($this->server->id, [ - 'installed' => 0, - ], true, true)->once()->andReturnNull(); - - $this->daemonServerRepository->shouldReceive('setServer')->with($this->server)->once()->andThrow($this->exception); - - $this->service->reinstall($this->server); - } - - /** - * Test that an exception thrown by something other than guzzle is not transformed to a displayable. - * - * @expectedException \Exception - */ - public function testExceptionNotThrownByGuzzleShouldNotBeTransformedToDisplayable() - { - $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('withoutFreshModel->update')->with($this->server->id, [ - 'installed' => 0, - ], true, true)->once()->andReturnNull(); - - $this->daemonServerRepository->shouldReceive('setServer')->with($this->server)->once()->andThrow(new Exception()); - - $this->service->reinstall($this->server); + return new ReinstallServerService( + $this->connection, $this->daemonServerRepository, $this->repository + ); } } diff --git a/tests/Unit/Services/Servers/ServerConfigurationStructureServiceTest.php b/tests/Unit/Services/Servers/ServerConfigurationStructureServiceTest.php index aa63cfa4b..e6c301b92 100644 --- a/tests/Unit/Services/Servers/ServerConfigurationStructureServiceTest.php +++ b/tests/Unit/Services/Servers/ServerConfigurationStructureServiceTest.php @@ -40,53 +40,62 @@ class ServerConfigurationStructureServiceTest extends TestCase */ public function testCorrectStructureIsReturned() { + /** @var \Pterodactyl\Models\Server $model */ $model = factory(Server::class)->make(); $model->setRelation('pack', null); $model->setRelation('allocation', factory(Allocation::class)->make()); $model->setRelation('allocations', collect(factory(Allocation::class)->times(2)->make())); $model->setRelation('egg', factory(Egg::class)->make()); - $portListing = $model->allocations->groupBy('ip')->map(function ($item) { - return $item->pluck('port'); - })->toArray(); - - $this->repository->shouldReceive('getDataForCreation')->with($model)->once()->andReturn($model); - $this->environment->shouldReceive('handle')->with($model)->once()->andReturn(['environment_array']); + $this->environment->expects('handle')->with($model)->andReturn(['environment_array']); $response = $this->getService()->handle($model); $this->assertNotEmpty($response); $this->assertArrayNotHasKey('user', $response); $this->assertArrayNotHasKey('keys', $response); + $this->assertArrayHasKey('uuid', $response); + $this->assertArrayHasKey('suspended', $response); + $this->assertArrayHasKey('environment', $response); + $this->assertArrayHasKey('invocation', $response); $this->assertArrayHasKey('build', $response); $this->assertArrayHasKey('service', $response); - $this->assertArrayHasKey('rebuild', $response); - $this->assertArrayHasKey('suspended', $response); + $this->assertArrayHasKey('container', $response); + $this->assertArrayHasKey('allocations', $response); - $this->assertArraySubset([ + $this->assertSame([ 'default' => [ 'ip' => $model->allocation->ip, 'port' => $model->allocation->port, ], - ], $response['build'], true, 'Assert server default allocation is correct.'); - $this->assertArraySubset(['ports' => $portListing], $response['build'], true, 'Assert server ports are correct.'); - $this->assertArraySubset([ - 'env' => ['environment_array'], - 'swap' => (int) $model->swap, - 'io' => (int) $model->io, - 'cpu' => (int) $model->cpu, - 'disk' => (int) $model->disk, - 'image' => $model->image, - ], $response['build'], true, 'Assert server build data is correct.'); + 'mappings' => $model->getAllocationMappings(), + ], $response['allocations']); - $this->assertArraySubset([ + $this->assertSame([ + 'memory_limit' => $model->memory, + 'swap' => $model->swap, + 'io_weight' => $model->io, + 'cpu_limit' => $model->cpu, + 'threads' => $model->threads, + 'disk_space' => $model->disk, + ], $response['build']); + + $this->assertSame([ 'egg' => $model->egg->uuid, 'pack' => null, 'skip_scripts' => $model->skip_scripts, ], $response['service']); - $this->assertFalse($response['rebuild']); - $this->assertSame((int) $model->suspended, $response['suspended']); + $this->assertSame([ + 'image' => $model->image, + 'oom_disabled' => $model->oom_disabled, + 'requires_rebuild' => false, + ], $response['container']); + + $this->assertSame($model->uuid, $response['uuid']); + $this->assertSame((bool) $model->suspended, $response['suspended']); + $this->assertSame(['environment_array'], $response['environment']); + $this->assertSame($model->startup, $response['invocation']); } /** diff --git a/tests/Unit/Services/Servers/ServerCreationServiceTest.php b/tests/Unit/Services/Servers/ServerCreationServiceTest.php index 27c7892b6..4efdd926d 100644 --- a/tests/Unit/Services/Servers/ServerCreationServiceTest.php +++ b/tests/Unit/Services/Servers/ServerCreationServiceTest.php @@ -5,24 +5,27 @@ namespace Tests\Unit\Services\Servers; use Mockery as m; use Tests\TestCase; use Pterodactyl\Models\Egg; +use GuzzleHttp\Psr7\Request; use Pterodactyl\Models\User; use Tests\Traits\MocksUuids; use Pterodactyl\Models\Server; use Pterodactyl\Models\Allocation; use Tests\Traits\MocksRequestException; +use GuzzleHttp\Exception\ConnectException; use Illuminate\Database\ConnectionInterface; use Pterodactyl\Models\Objects\DeploymentObject; +use Pterodactyl\Repositories\Eloquent\EggRepository; +use Pterodactyl\Repositories\Eloquent\ServerRepository; use Pterodactyl\Services\Servers\ServerCreationService; +use Pterodactyl\Services\Servers\ServerDeletionService; +use Pterodactyl\Repositories\Wings\DaemonServerRepository; use Pterodactyl\Services\Servers\VariableValidatorService; +use Pterodactyl\Repositories\Eloquent\AllocationRepository; use Pterodactyl\Services\Deployment\FindViableNodesService; -use Pterodactyl\Contracts\Repository\EggRepositoryInterface; -use Pterodactyl\Contracts\Repository\UserRepositoryInterface; -use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Repositories\Eloquent\ServerVariableRepository; use Pterodactyl\Services\Deployment\AllocationSelectionService; -use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; +use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException; use Pterodactyl\Services\Servers\ServerConfigurationStructureService; -use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; -use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; /** * @preserveGlobalState disabled @@ -32,60 +35,60 @@ class ServerCreationServiceTest extends TestCase use MocksRequestException, MocksUuids; /** - * @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface|\Mockery\Mock + * @var \Mockery\MockInterface */ private $allocationRepository; /** - * @var \Pterodactyl\Services\Deployment\AllocationSelectionService|\Mockery\Mock + * @var \Mockery\MockInterface */ private $allocationSelectionService; /** - * @var \Pterodactyl\Services\Servers\ServerConfigurationStructureService|\Mockery\Mock + * @var \Mockery\MockInterface */ private $configurationStructureService; /** - * @var \Illuminate\Database\ConnectionInterface|\Mockery\Mock + * @var \Mockery\MockInterface */ private $connection; /** - * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface|\Mockery\Mock + * @var \Mockery\MockInterface */ private $daemonServerRepository; /** - * @var \Pterodactyl\Contracts\Repository\EggRepositoryInterface|\Mockery\Mock + * @var \Mockery\MockInterface */ private $eggRepository; /** - * @var \Pterodactyl\Services\Deployment\FindViableNodesService|\Mockery\Mock + * @var \Mockery\MockInterface */ private $findViableNodesService; /** - * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface|\Mockery\Mock + * @var \Mockery\MockInterface */ private $repository; /** - * @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface|\Mockery\Mock + * @var \Mockery\MockInterface */ private $serverVariableRepository; /** - * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface|\Mockery\Mock - */ - private $userRepository; - - /** - * @var \Pterodactyl\Services\Servers\VariableValidatorService|\Mockery\Mock + * @var \Mockery\MockInterface */ private $validatorService; + /** + * @var \Mockery\MockInterface + */ + private $serverDeletionService; + /** * Setup tests. */ @@ -93,17 +96,17 @@ class ServerCreationServiceTest extends TestCase { parent::setUp(); - $this->allocationRepository = m::mock(AllocationRepositoryInterface::class); + $this->allocationRepository = m::mock(AllocationRepository::class); $this->allocationSelectionService = m::mock(AllocationSelectionService::class); $this->configurationStructureService = m::mock(ServerConfigurationStructureService::class); $this->connection = m::mock(ConnectionInterface::class); - $this->daemonServerRepository = m::mock(DaemonServerRepositoryInterface::class); - $this->eggRepository = m::mock(EggRepositoryInterface::class); $this->findViableNodesService = m::mock(FindViableNodesService::class); - $this->repository = m::mock(ServerRepositoryInterface::class); - $this->serverVariableRepository = m::mock(ServerVariableRepositoryInterface::class); - $this->userRepository = m::mock(UserRepositoryInterface::class); $this->validatorService = m::mock(VariableValidatorService::class); + $this->eggRepository = m::mock(EggRepository::class); + $this->repository = m::mock(ServerRepository::class); + $this->serverVariableRepository = m::mock(ServerVariableRepository::class); + $this->daemonServerRepository = m::mock(DaemonServerRepository::class); + $this->serverDeletionService = m::mock(ServerDeletionService::class); } /** @@ -148,7 +151,7 @@ class ServerCreationServiceTest extends TestCase $this->configurationStructureService->shouldReceive('handle')->with($model)->once()->andReturn(['test' => 'struct']); $this->daemonServerRepository->shouldReceive('setServer')->with($model)->once()->andReturnSelf(); - $this->daemonServerRepository->shouldReceive('create')->with(['test' => 'struct'], ['start_on_completion' => false])->once(); + $this->daemonServerRepository->shouldReceive('create')->with(['test' => 'struct'])->once(); $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); $response = $this->getService()->handle($model->toArray()); @@ -250,12 +253,10 @@ class ServerCreationServiceTest extends TestCase /** * Test handling of node timeout or other daemon error. - * - * @expectedException \Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException */ public function testExceptionShouldBeThrownIfTheRequestFails() { - $this->configureExceptionMock(); + $this->expectException(DaemonConnectionException::class); $model = factory(Server::class)->make([ 'uuid' => $this->getKnownUuid(), @@ -269,8 +270,16 @@ class ServerCreationServiceTest extends TestCase $this->validatorService->shouldReceive('handle')->once()->andReturn(collect([])); $this->configurationStructureService->shouldReceive('handle')->once()->andReturn([]); - $this->daemonServerRepository->shouldReceive('setServer')->with($model)->once()->andThrow($this->getExceptionMock()); - $this->connection->shouldReceive('rollBack')->withNoArgs()->once()->andReturnNull(); + $this->connection->expects('commit')->withNoArgs(); + + $this->daemonServerRepository->shouldReceive('setServer')->with($model)->once()->andThrow( + new DaemonConnectionException( + new ConnectException('', new Request('GET', 'test')) + ) + ); + + $this->serverDeletionService->expects('withForce')->with(true)->andReturnSelf(); + $this->serverDeletionService->expects('handle')->with($model); $this->getService()->handle($model->toArray()); } @@ -290,6 +299,7 @@ class ServerCreationServiceTest extends TestCase $this->eggRepository, $this->findViableNodesService, $this->configurationStructureService, + $this->serverDeletionService, $this->repository, $this->serverVariableRepository, $this->validatorService