From 65957e7ea51f5ed9826e960d8469bf3b270ac056 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 3 Jun 2017 18:41:35 -0500 Subject: [PATCH 01/99] Begin implementation of new request validation, closes #470 --- .gitignore | 1 + .styleci.yml | 1 + .../Controllers/Admin/OptionController.php | 16 ++-- .../Admin/Service/StoreOptionVariable.php | 75 +++++++++++++++++++ resources/lang/en/base.php | 6 +- 5 files changed, 86 insertions(+), 13 deletions(-) create mode 100644 app/Http/Requests/Admin/Service/StoreOptionVariable.php diff --git a/.gitignore b/.gitignore index 3a02506e..9d8eca6a 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ Dockerfile docker-compose.yml # for image related files misc +.phpstorm.meta.php diff --git a/.styleci.yml b/.styleci.yml index 7595f554..87848d02 100644 --- a/.styleci.yml +++ b/.styleci.yml @@ -4,3 +4,4 @@ disabled: - concat_without_spaces enabled: - concat_with_spaces + - no_unused_imports diff --git a/app/Http/Controllers/Admin/OptionController.php b/app/Http/Controllers/Admin/OptionController.php index f4a70363..cdcf2f3b 100644 --- a/app/Http/Controllers/Admin/OptionController.php +++ b/app/Http/Controllers/Admin/OptionController.php @@ -35,6 +35,7 @@ use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Repositories\OptionRepository; use Pterodactyl\Repositories\VariableRepository; use Pterodactyl\Exceptions\DisplayValidationException; +use Pterodactyl\Http\Requests\Admin\Service\StoreOptionVariable; class OptionController extends Controller { @@ -198,28 +199,23 @@ class OptionController extends Controller /** * Handles POST when editing a configration for a service option. * - * @param \Illuminate\Http\Request $request - * @param int $option - * @param int $variable + * @param \Pterodactyl\Http\Requests\Admin\Service\StoreOptionVariable $request + * @param int $option + * @param int $variable * @return \Illuminate\Http\RedirectResponse */ - public function editVariable(Request $request, $option, $variable) + public function editVariable(StoreOptionVariable $request, $option, $variable) { $repo = new VariableRepository; try { if ($request->input('action') !== 'delete') { - $variable = $repo->update($variable, $request->intersect([ - 'name', 'description', 'env_variable', - 'default_value', 'options', 'rules', - ])); + $variable = $repo->update($variable, $request->normalize()); Alert::success("The service variable '{$variable->name}' has been updated.")->flash(); } else { $repo->delete($variable); Alert::success('That service variable has been deleted.')->flash(); } - } catch (DisplayValidationException $ex) { - return redirect()->route('admin.services.option.variables', $option)->withErrors(json_decode($ex->getMessage())); } catch (DisplayException $ex) { Alert::danger($ex->getMessage())->flash(); } catch (\Exception $ex) { diff --git a/app/Http/Requests/Admin/Service/StoreOptionVariable.php b/app/Http/Requests/Admin/Service/StoreOptionVariable.php new file mode 100644 index 00000000..cb37e6a1 --- /dev/null +++ b/app/Http/Requests/Admin/Service/StoreOptionVariable.php @@ -0,0 +1,75 @@ +. + * + * 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\Http\Requests\Admin\Service; + +use Pterodactyl\Models\User; +use Illuminate\Foundation\Http\FormRequest; + +class StoreOptionVariable extends FormRequest +{ + /** + * Determine if user is allowed to access this request. + * + * @return bool + */ + public function authorize() + { + if (! $this->user() instanceof User) { + return false; + } + + return $this->user()->isRootAdmin(); + } + + /** + * Set the rules to be used for data passed to the request. + * + * @return array + */ + public function rules() + { + return [ + 'name' => 'required|string|min:1|max:255', + 'description' => 'nullable|string', + 'env_variable' => 'required|regex:/^[\w]{1,255}$/', + 'rules' => 'bail|required|string', + 'default_value' => explode('|', $this->input('rules')), + 'options' => 'sometimes|required|array', + ]; + } + + /** + * Return only the fields that we are interested in from the request. + * This will include empty fields as a null value. + * + * @return array + */ + public function normalize() + { + return $this->only( + array_keys($this->rules()) + ); + } +} diff --git a/resources/lang/en/base.php b/resources/lang/en/base.php index fdd7157c..a32ab253 100644 --- a/resources/lang/en/base.php +++ b/resources/lang/en/base.php @@ -57,15 +57,15 @@ return [ ], 'view' => [ 'title' => 'View Server', - 'desc'=> 'Allows viewing of specific server user can access.', + 'desc' => 'Allows viewing of specific server user can access.', ], 'power' => [ 'title' => 'Toggle Power', - 'desc'=> 'Allow toggling of power status for a server.', + 'desc' => 'Allow toggling of power status for a server.', ], 'command' => [ 'title' => 'Send Command', - 'desc'=> 'Allow sending of a command to a running server.', + 'desc' => 'Allow sending of a command to a running server.', ], ], ], From 5c2b9deb096491528a01946b03bec100fa814439 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 10 Jun 2017 22:28:44 -0500 Subject: [PATCH 02/99] Push initial implementations of new repository structure This breaks almost the entire panel, do not pull this branch in this state. Mostly just moved old repository files to a new folder without updating anything else in order to start doing new things. Structure is not finalized. --- app/Console/Commands/MakeUser.php | 4 +- app/Contracts/Criteria/CriteriaInterface.php | 39 +++ .../Repositories/RepositoryInterface.php | 164 +++++++++++++ .../SearchableRepositoryInterface.php | 36 +++ app/Contracts/Repositories/UserInterface.php | 30 +++ app/Exceptions/DisplayException.php | 2 +- app/Exceptions/DisplayValidationException.php | 2 +- app/Exceptions/PterodactylException.php | 30 +++ .../Repository/RepositoryException.php | 30 +++ .../Controllers/API/Admin/UserController.php | 8 +- .../Controllers/Admin/OptionController.php | 36 ++- app/Http/Controllers/Admin/UserController.php | 112 ++++----- .../Controllers/Base/AccountController.php | 4 +- app/Http/Requests/Admin/AdminFormRequest.php | 59 +++++ .../Admin/Service/EditOptionScript.php | 46 ++++ .../Admin/Service/StoreOptionVariable.php | 32 +-- app/Http/Requests/Admin/UserFormRequest.php | 79 ++++++ app/Models/User.php | 18 +- app/Observers/UserObserver.php | 15 +- app/Providers/RepositoryServiceProvider.php | 40 +++ app/Repositories/Eloquent/UserRepository.php | 78 ++++++ app/Repositories/{ => Old}/APIRepository.php | 0 .../{ => Old}/DatabaseRepository.php | 0 .../{ => Old}/HelperRepository.php | 0 .../{ => Old}/LocationRepository.php | 0 app/Repositories/{ => Old}/NodeRepository.php | 0 .../{ => Old}/OptionRepository.php | 94 ++++--- app/Repositories/{ => Old}/PackRepository.php | 0 .../{ => Old}/ServiceRepository.php | 0 .../{ => Old}/SubuserRepository.php | 2 +- app/Repositories/{ => Old}/TaskRepository.php | 0 .../{ => Old}/VariableRepository.php | 0 .../old_ServerRepository.php} | 2 +- .../old_UserRepository.php} | 2 +- app/Repositories/Repository.php | 229 ++++++++++++++++++ config/app.php | 1 + ..._06_10_152951_add_external_id_to_users.php | 32 +++ .../pterodactyl/admin/users/view.blade.php | 1 + routes/admin.php | 22 +- 39 files changed, 1083 insertions(+), 166 deletions(-) create mode 100644 app/Contracts/Criteria/CriteriaInterface.php create mode 100644 app/Contracts/Repositories/RepositoryInterface.php create mode 100644 app/Contracts/Repositories/SearchableRepositoryInterface.php create mode 100644 app/Contracts/Repositories/UserInterface.php create mode 100644 app/Exceptions/PterodactylException.php create mode 100644 app/Exceptions/Repository/RepositoryException.php create mode 100644 app/Http/Requests/Admin/AdminFormRequest.php create mode 100644 app/Http/Requests/Admin/Service/EditOptionScript.php create mode 100644 app/Http/Requests/Admin/UserFormRequest.php create mode 100644 app/Providers/RepositoryServiceProvider.php create mode 100644 app/Repositories/Eloquent/UserRepository.php rename app/Repositories/{ => Old}/APIRepository.php (100%) rename app/Repositories/{ => Old}/DatabaseRepository.php (100%) rename app/Repositories/{ => Old}/HelperRepository.php (100%) rename app/Repositories/{ => Old}/LocationRepository.php (100%) rename app/Repositories/{ => Old}/NodeRepository.php (100%) rename app/Repositories/{ => Old}/OptionRepository.php (79%) rename app/Repositories/{ => Old}/PackRepository.php (100%) rename app/Repositories/{ => Old}/ServiceRepository.php (100%) rename app/Repositories/{ => Old}/SubuserRepository.php (99%) rename app/Repositories/{ => Old}/TaskRepository.php (100%) rename app/Repositories/{ => Old}/VariableRepository.php (100%) rename app/Repositories/{ServerRepository.php => Old/old_ServerRepository.php} (99%) rename app/Repositories/{UserRepository.php => Old/old_UserRepository.php} (99%) create mode 100644 app/Repositories/Repository.php create mode 100644 database/migrations/2017_06_10_152951_add_external_id_to_users.php diff --git a/app/Console/Commands/MakeUser.php b/app/Console/Commands/MakeUser.php index 05803dd3..81038cb4 100644 --- a/app/Console/Commands/MakeUser.php +++ b/app/Console/Commands/MakeUser.php @@ -25,7 +25,7 @@ namespace Pterodactyl\Console\Commands; use Illuminate\Console\Command; -use Pterodactyl\Repositories\UserRepository; +use Pterodactyl\Repositories\oldUserRepository; class MakeUser extends Command { @@ -80,7 +80,7 @@ class MakeUser extends Command $data['root_admin'] = is_null($this->option('admin')) ? $this->confirm('Is this user a root administrator?') : $this->option('admin'); try { - $user = new UserRepository; + $user = new oldUserRepository; $user->create($data); return $this->info('User successfully created.'); diff --git a/app/Contracts/Criteria/CriteriaInterface.php b/app/Contracts/Criteria/CriteriaInterface.php new file mode 100644 index 00000000..dba7a688 --- /dev/null +++ b/app/Contracts/Criteria/CriteriaInterface.php @@ -0,0 +1,39 @@ +. + * + * 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\Contracts\Criteria; + +use Pterodactyl\Repositories\Repository; + +interface CriteriaInterface +{ + /** + * Apply selected criteria to a repository call. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @param \Pterodactyl\Repositories\Repository $repository + * @return mixed + */ + public function apply($model, Repository $repository); +} diff --git a/app/Contracts/Repositories/RepositoryInterface.php b/app/Contracts/Repositories/RepositoryInterface.php new file mode 100644 index 00000000..16771e70 --- /dev/null +++ b/app/Contracts/Repositories/RepositoryInterface.php @@ -0,0 +1,164 @@ +. + * + * 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\Contracts\Repositories; + +use Illuminate\Container\Container; + +interface RepositoryInterface +{ + /** + * RepositoryInterface constructor. + * + * @param \Illuminate\Container\Container $container + */ + public function __construct(Container $container); + + /** + * Define the model class to be loaded. + * + * @return string + */ + public function model(); + + /** + * Returns the raw model class. + * + * @return \Illuminate\Database\Eloquent\Model + */ + public function getModel(); + + /** + * Make the model instance. + * + * @return \Illuminate\Database\Eloquent\Model + * @throws \Pterodactyl\Exceptions\Repository\RepositoryException + */ + public function makeModel(); + + /** + * Return all of the currently defined rules. + * + * @return array + */ + public function getRules(); + + /** + * Return the rules to apply when updating a model. + * + * @return array + */ + public function getUpdateRules(); + + /** + * Return the rules to apply when creating a model. + * + * @return array + */ + public function getCreateRules(); + + /** + * Add relations to a model for retrieval. + * + * @param array $params + * @return $this + */ + public function with(...$params); + + /** + * Add count of related items to model when retrieving. + * + * @param array $params + * @return $this + */ + public function withCount(...$params); + + /** + * Get all records from the database. + * + * @param array $columns + * @return mixed + */ + public function all(array $columns = ['*']); + + /** + * Return a paginated result set. + * + * @param int $limit + * @param array $columns + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator + */ + public function paginate($limit = 15, array $columns = ['*']); + + /** + * Create a new record on the model. + * + * @param array $data + * @return \Illuminate\Database\Eloquent\Model + */ + public function create(array $data); + + /** + * Update the model. + * + * @param $attributes + * @param array $data + * @return int + */ + public function update($attributes, array $data); + + /** + * Delete a model from the database. Handles soft deletion. + * + * @param int $id + * @return mixed + */ + public function delete($id); + + /** + * Destroy the model from the database. Ignores soft deletion. + * + * @param int $id + * @return mixed + */ + public function destroy($id); + + /** + * Find a given model by ID or IDs. + * + * @param int|array $id + * @param array $columns + * @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection + */ + public function find($id, array $columns = ['*']); + + /** + * Finds the first record matching a passed array of values. + * + * @param array $attributes + * @param array $columns + * @return \Illuminate\Database\Eloquent\Model + */ + public function findBy(array $attributes, array $columns = ['*']); +} diff --git a/app/Contracts/Repositories/SearchableRepositoryInterface.php b/app/Contracts/Repositories/SearchableRepositoryInterface.php new file mode 100644 index 00000000..6a6b4537 --- /dev/null +++ b/app/Contracts/Repositories/SearchableRepositoryInterface.php @@ -0,0 +1,36 @@ +. + * + * 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\Contracts\Repositories; + +interface SearchableRepositoryInterface extends RepositoryInterface +{ + /** + * Pass parameters to search trait on model. + * + * @param string $term + * @return mixed + */ + public function search($term); +} diff --git a/app/Contracts/Repositories/UserInterface.php b/app/Contracts/Repositories/UserInterface.php new file mode 100644 index 00000000..a7bf4964 --- /dev/null +++ b/app/Contracts/Repositories/UserInterface.php @@ -0,0 +1,30 @@ +. + * + * 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\Contracts\Repositories; + +interface UserInterface extends RepositoryInterface, SearchableRepositoryInterface +{ + // +} diff --git a/app/Exceptions/DisplayException.php b/app/Exceptions/DisplayException.php index f3f8fd71..530ad40c 100644 --- a/app/Exceptions/DisplayException.php +++ b/app/Exceptions/DisplayException.php @@ -26,7 +26,7 @@ namespace Pterodactyl\Exceptions; use Log; -class DisplayException extends \Exception +class DisplayException extends PterodactylException { /** * Exception constructor. diff --git a/app/Exceptions/DisplayValidationException.php b/app/Exceptions/DisplayValidationException.php index 3d8a4fda..ae97318a 100644 --- a/app/Exceptions/DisplayValidationException.php +++ b/app/Exceptions/DisplayValidationException.php @@ -24,7 +24,7 @@ namespace Pterodactyl\Exceptions; -class DisplayValidationException extends \Exception +class DisplayValidationException extends PterodactylException { // } diff --git a/app/Exceptions/PterodactylException.php b/app/Exceptions/PterodactylException.php new file mode 100644 index 00000000..4ec48d66 --- /dev/null +++ b/app/Exceptions/PterodactylException.php @@ -0,0 +1,30 @@ +. + * + * 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\Exceptions; + +class PterodactylException extends \Exception +{ + // +} diff --git a/app/Exceptions/Repository/RepositoryException.php b/app/Exceptions/Repository/RepositoryException.php new file mode 100644 index 00000000..b55b6304 --- /dev/null +++ b/app/Exceptions/Repository/RepositoryException.php @@ -0,0 +1,30 @@ +. + * + * 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\Exceptions\Repository; + +class RepositoryException extends \Exception +{ + // +} diff --git a/app/Http/Controllers/API/Admin/UserController.php b/app/Http/Controllers/API/Admin/UserController.php index c94fe809..b524d1ee 100644 --- a/app/Http/Controllers/API/Admin/UserController.php +++ b/app/Http/Controllers/API/Admin/UserController.php @@ -30,7 +30,7 @@ use Illuminate\Http\Request; use Pterodactyl\Models\User; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Repositories\UserRepository; +use Pterodactyl\Repositories\oldUserRepository; use Pterodactyl\Transformers\Admin\UserTransformer; use Pterodactyl\Exceptions\DisplayValidationException; use League\Fractal\Pagination\IlluminatePaginatorAdapter; @@ -91,7 +91,7 @@ class UserController extends Controller { $this->authorize('user-create', $request->apiKey()); - $repo = new UserRepository; + $repo = new oldUserRepository; try { $user = $repo->create($request->only([ 'custom_id', 'email', 'password', 'name_first', @@ -128,7 +128,7 @@ class UserController extends Controller { $this->authorize('user-edit', $request->apiKey()); - $repo = new UserRepository; + $repo = new oldUserRepository; try { $user = $repo->update($user, $request->intersect([ 'email', 'password', 'name_first', @@ -165,7 +165,7 @@ class UserController extends Controller { $this->authorize('user-delete', $request->apiKey()); - $repo = new UserRepository; + $repo = new oldUserRepository; try { $repo->delete($id); diff --git a/app/Http/Controllers/Admin/OptionController.php b/app/Http/Controllers/Admin/OptionController.php index cdcf2f3b..afabcfd2 100644 --- a/app/Http/Controllers/Admin/OptionController.php +++ b/app/Http/Controllers/Admin/OptionController.php @@ -26,8 +26,10 @@ namespace Pterodactyl\Http\Controllers\Admin; use Log; use Alert; +use Route; use Javascript; use Illuminate\Http\Request; +use Pterodactyl\Http\Requests\Admin\Service\EditOptionScript; use Pterodactyl\Models\Service; use Pterodactyl\Models\ServiceOption; use Pterodactyl\Exceptions\DisplayException; @@ -39,6 +41,21 @@ use Pterodactyl\Http\Requests\Admin\Service\StoreOptionVariable; class OptionController extends Controller { + /** + * Store the repository instance. + * + * @var \Pterodactyl\Repositories\OptionRepository + */ + protected $repository; + + /** + * OptionController constructor. + */ + public function __construct() + { + $this->repository = new OptionRepository(Route::current()->parameter('option')); + } + /** * Handles request to view page for adding new option. * @@ -227,24 +244,17 @@ class OptionController extends Controller } /** - * Handles POST when updating scripts for a service option. + * Handles POST when updating script for a service option. * - * @param Request $request - * @param int $id - * @return \Illuminate\Response\RedirectResponse + * @param \Pterodactyl\Http\Requests\Admin\Service\EditOptionScript $request + * @return \Illuminate\Http\RedirectResponse */ - public function updateScripts(Request $request, $id) + public function updateScripts(EditOptionScript $request) { - $repo = new OptionRepository; - try { - $repo->scripts($id, $request->only([ - 'script_install', 'script_entry', - 'script_container', 'copy_script_from', - ])); + $this->repository->scripts($request->normalize()); + Alert::success('Successfully updated option scripts to be run when servers are installed.')->flash(); - } catch (DisplayValidationException $ex) { - return redirect()->route('admin.services.option.scripts', $id)->withErrors(json_decode($ex->getMessage())); } catch (DisplayException $ex) { Alert::danger($ex->getMessage())->flash(); } catch (\Exception $ex) { diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index 0e943eb4..1b963228 100644 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -25,17 +25,31 @@ namespace Pterodactyl\Http\Controllers\Admin; -use Log; use Alert; use Illuminate\Http\Request; -use Pterodactyl\Models\User; +use Pterodactyl\Contracts\Repositories\UserInterface; use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Http\Requests\Admin\UserFormRequest; +use Pterodactyl\Models\User; use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Repositories\UserRepository; -use Pterodactyl\Exceptions\DisplayValidationException; class UserController extends Controller { + /** + * @var \Pterodactyl\Repositories\Eloquent\UserRepository + */ + protected $repository; + + /** + * UserController constructor. + * + * @param \Pterodactyl\Contracts\Repositories\UserInterface $repository + */ + public function __construct(UserInterface $repository) + { + $this->repository = $repository; + } + /** * Display user index page. * @@ -44,7 +58,7 @@ class UserController extends Controller */ public function index(Request $request) { - $users = User::withCount('servers', 'subuserOf'); + $users = $this->repository->withCount('servers', 'subuserOf'); if (! is_null($request->input('query'))) { $users->search($request->input('query')); @@ -58,10 +72,9 @@ class UserController extends Controller /** * Display new user page. * - * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ - public function create(Request $request) + public function create() { return view('admin.users.new'); } @@ -69,96 +82,61 @@ class UserController extends Controller /** * Display user view page. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Pterodactyl\Models\User $user * @return \Illuminate\View\View */ - public function view(Request $request, $id) + public function view(User $user) { return view('admin.users.view', [ - 'user' => User::with('servers.node')->findOrFail($id), + 'user' => $user, ]); } /** - * Delete a user. + * Delete a user from the system. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Pterodactyl\Models\User $user * @return \Illuminate\Http\RedirectResponse */ - public function delete(Request $request, $id) + public function delete(User $user) { try { - $repo = new UserRepository; - $repo->delete($id); - Alert::success('Successfully deleted user from system.')->flash(); + $this->repository->delete($user->id); return redirect()->route('admin.users'); } catch (DisplayException $ex) { Alert::danger($ex->getMessage())->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An exception was encountered while attempting to delete this user.')->flash(); } - return redirect()->route('admin.users.view', $id); + return redirect()->route('admin.users.view', $user->id); } /** * Create a user. * - * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Http\Requests\Admin\UserFormRequest $request * @return \Illuminate\Http\RedirectResponse */ - public function store(Request $request) + public function store(UserFormRequest $request) { - try { - $user = new UserRepository; - $userid = $user->create($request->only([ - 'email', 'password', 'name_first', - 'name_last', 'username', 'root_admin', - ])); - Alert::success('Account has been successfully created.')->flash(); + $user = $this->repository->create($request->normalize()); + Alert::success('Account has been successfully created.')->flash(); - return redirect()->route('admin.users.view', $userid); - } catch (DisplayValidationException $ex) { - return redirect()->route('admin.users.new')->withErrors(json_decode($ex->getMessage()))->withInput(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An error occured while attempting to add a new user.')->flash(); - - return redirect()->route('admin.users.new'); - } + return redirect()->route('admin.users.view', $user->id); } /** - * Update a user. + * Update a user on the system. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Pterodactyl\Http\Requests\Admin\UserFormRequest $request + * @param \Pterodactyl\Models\User $user * @return \Illuminate\Http\RedirectResponse */ - public function update(Request $request, $id) + public function update(UserFormRequest $request, User $user) { - try { - $repo = new UserRepository; - $user = $repo->update($id, array_merge( - $request->only('root_admin'), - $request->intersect([ - 'email', 'password', 'name_first', - 'name_last', 'username', - ]) - )); - Alert::success('User account was successfully updated.')->flash(); - } catch (DisplayValidationException $ex) { - return redirect()->route('admin.users.view', $id)->withErrors(json_decode($ex->getMessage())); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An error occured while attempting to update this user.')->flash(); - } + $this->repository->update($user->id, $request->normalize()); - return redirect()->route('admin.users.view', $id); + return redirect()->route('admin.users.view', $user->id); } /** @@ -169,12 +147,12 @@ class UserController extends Controller */ public function json(Request $request) { - return User::select('id', 'email', 'username', 'name_first', 'name_last') - ->search($request->input('q')) - ->get()->transform(function ($item) { - $item->md5 = md5(strtolower($item->email)); + return $this->repository->search($request->input('q'))->all([ + 'id', 'email', 'username', 'name_first', 'name_last', + ])->transform(function ($item) { + $item->md5 = md5(strtolower($item->email)); - return $item; - }); + return $item; + }); } } diff --git a/app/Http/Controllers/Base/AccountController.php b/app/Http/Controllers/Base/AccountController.php index 10c33e38..7163045a 100644 --- a/app/Http/Controllers/Base/AccountController.php +++ b/app/Http/Controllers/Base/AccountController.php @@ -30,7 +30,7 @@ use Alert; use Illuminate\Http\Request; use Pterodactyl\Models\User; use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Repositories\UserRepository; +use Pterodactyl\Repositories\oldUserRepository; use Pterodactyl\Exceptions\DisplayValidationException; class AccountController extends Controller @@ -90,7 +90,7 @@ class AccountController extends Controller } try { - $repo = new UserRepository; + $repo = new oldUserRepository; $repo->update($request->user()->id, $data); Alert::success('Your account details were successfully updated.')->flash(); } catch (DisplayValidationException $ex) { diff --git a/app/Http/Requests/Admin/AdminFormRequest.php b/app/Http/Requests/Admin/AdminFormRequest.php new file mode 100644 index 00000000..e3e0be37 --- /dev/null +++ b/app/Http/Requests/Admin/AdminFormRequest.php @@ -0,0 +1,59 @@ +. + * + * 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\Http\Requests\Admin; + +use Pterodactyl\Models\User; +use Illuminate\Foundation\Http\FormRequest; + +abstract class AdminFormRequest extends FormRequest +{ + /** + * Determine if the user is an admin and has permission to access this + * form controller in the first place. + * + * @return bool + */ + public function authorize() + { + if (! $this->user() instanceof User) { + return false; + } + + return $this->user()->isRootAdmin(); + } + + /** + * Return only the fields that we are interested in from the request. + * This will include empty fields as a null value. + * + * @return array + */ + public function normalize() + { + return $this->only( + array_keys($this->rules()) + ); + } +} diff --git a/app/Http/Requests/Admin/Service/EditOptionScript.php b/app/Http/Requests/Admin/Service/EditOptionScript.php new file mode 100644 index 00000000..52cfff4e --- /dev/null +++ b/app/Http/Requests/Admin/Service/EditOptionScript.php @@ -0,0 +1,46 @@ +. + * + * 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\Http\Requests\Admin\Service; + +use Pterodactyl\Http\Requests\Admin\AdminFormRequest; + +class EditOptionScript extends AdminFormRequest +{ + /** + * Return the rules to be used when validating the sent data in the request. + * + * @return array + */ + public function rules() + { + return [ + 'script_install' => 'sometimes|nullable|string', + 'script_is_privileged' => 'sometimes|required|boolean', + 'script_entry' => 'sometimes|required|string', + 'script_container' => 'sometimes|required|string', + 'copy_script_from' => 'sometimes|nullable|numeric', + ]; + } +} diff --git a/app/Http/Requests/Admin/Service/StoreOptionVariable.php b/app/Http/Requests/Admin/Service/StoreOptionVariable.php index cb37e6a1..869b2efc 100644 --- a/app/Http/Requests/Admin/Service/StoreOptionVariable.php +++ b/app/Http/Requests/Admin/Service/StoreOptionVariable.php @@ -24,25 +24,10 @@ namespace Pterodactyl\Http\Requests\Admin\Service; -use Pterodactyl\Models\User; -use Illuminate\Foundation\Http\FormRequest; +use Pterodactyl\Http\Requests\Admin\AdminFormRequest; -class StoreOptionVariable extends FormRequest +class StoreOptionVariable extends AdminFormRequest { - /** - * Determine if user is allowed to access this request. - * - * @return bool - */ - public function authorize() - { - if (! $this->user() instanceof User) { - return false; - } - - return $this->user()->isRootAdmin(); - } - /** * Set the rules to be used for data passed to the request. * @@ -59,17 +44,4 @@ class StoreOptionVariable extends FormRequest 'options' => 'sometimes|required|array', ]; } - - /** - * Return only the fields that we are interested in from the request. - * This will include empty fields as a null value. - * - * @return array - */ - public function normalize() - { - return $this->only( - array_keys($this->rules()) - ); - } } diff --git a/app/Http/Requests/Admin/UserFormRequest.php b/app/Http/Requests/Admin/UserFormRequest.php new file mode 100644 index 00000000..10d55977 --- /dev/null +++ b/app/Http/Requests/Admin/UserFormRequest.php @@ -0,0 +1,79 @@ +. + * + * 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\Http\Requests\Admin; + +use Pterodactyl\Models\User; +use Illuminate\Support\Facades\Hash; +use Pterodactyl\Contracts\Repositories\UserInterface; + +class UserFormRequest extends AdminFormRequest +{ + /** + * {@inheritdoc} + */ + public function repository() + { + return UserInterface::class; + } + + /** + * {@inheritdoc} + */ + public function rules() + { + if ($this->method() === 'PATCH') { + return [ + 'email' => 'sometimes|required|email|unique:users,email,' . $this->user->id, + 'username' => 'sometimes|required|alpha_dash|between:1,255|unique:users,username, ' . $this->user->id . '|' . User::USERNAME_RULES, + 'name_first' => 'sometimes|required|string|between:1,255', + 'name_last' => 'sometimes|required|string|between:1,255', + 'password' => 'sometimes|nullable|' . User::PASSWORD_RULES, + 'root_admin' => 'sometimes|required|boolean', + 'language' => 'sometimes|required|string|min:1|max:5', + 'use_totp' => 'sometimes|required|boolean', + 'totp_secret' => 'sometimes|required|size:16', + ]; + } + + return [ + 'email' => 'required|email|unique:users,email,' . $this->user->id, + 'username' => 'required|alpha_dash|between:1,255|unique:users,username,' . $this->user->id . '|' . User::USERNAME_RULES, + 'name_first' => 'required|string|between:1,255', + 'name_last' => 'required|string|between:1,255', + 'password' => 'sometimes|nullable|' . User::PASSWORD_RULES, + 'root_admin' => 'required|boolean', + 'external_id' => 'sometimes|nullable|numeric|unique:users,external_id', + ]; + } + + public function normalize() + { + if ($this->has('password')) { + $this->merge(['password' => Hash::make($this->input('password'))]); + } + + return parent::normalize(); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 12504b6b..a4f06f4b 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -117,6 +117,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac * * @param int $token * @return bool + * @deprecated */ public function toggleTotp($token) { @@ -136,9 +137,11 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac * - at least one lowercase character * - at least one number. * - * @param string $password - * @param string $regex + * @param string $password + * @param string $regex * @return void + * @throws \Pterodactyl\Exceptions\DisplayException + * @deprecated */ public function setPassword($password, $regex = '((?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,})') { @@ -165,6 +168,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac * Return true or false depending on wether the user is root admin or not. * * @return bool + * @deprecated */ public function isRootAdmin() { @@ -256,6 +260,16 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac return $query; } + /** + * Store the username as a lowecase string. + * + * @param string $value + */ + public function setUsernameAttribute($value) + { + $this->attributes['username'] = strtolower($value); + } + /** * Returns all permissions that a user has. * diff --git a/app/Observers/UserObserver.php b/app/Observers/UserObserver.php index e7b07851..f6e16026 100644 --- a/app/Observers/UserObserver.php +++ b/app/Observers/UserObserver.php @@ -30,9 +30,17 @@ use Carbon; use Pterodactyl\Events; use Pterodactyl\Models\User; use Pterodactyl\Notifications\AccountCreated; +use Pterodactyl\Services\UuidService; class UserObserver { + protected $uuid; + + public function __construct(UuidService $uuid) + { + $this->uuid = $uuid; + } + /** * Listen to the User creating event. * @@ -41,6 +49,8 @@ class UserObserver */ public function creating(User $user) { + $user->uuid = $this->uuid->generate(); + event(new Events\User\Creating($user)); } @@ -52,8 +62,7 @@ class UserObserver */ public function created(User $user) { - event(new Events\User\Created($user)); - + dd($user); if ($user->password === 'unset') { $token = hash_hmac('sha256', str_random(40), config('app.key')); DB::table('password_resets')->insert([ @@ -68,6 +77,8 @@ class UserObserver 'username' => $user->username, 'token' => (isset($token)) ? $token : null, ])); + + event(new Events\User\Created($user)); } /** diff --git a/app/Providers/RepositoryServiceProvider.php b/app/Providers/RepositoryServiceProvider.php new file mode 100644 index 00000000..745e6d05 --- /dev/null +++ b/app/Providers/RepositoryServiceProvider.php @@ -0,0 +1,40 @@ +. + * + * 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\Providers; + +use Illuminate\Support\ServiceProvider; +use Pterodactyl\Contracts\Repositories\UserInterface; +use Pterodactyl\Repositories\Eloquent\UserRepository; + +class RepositoryServiceProvider extends ServiceProvider +{ + /** + * Register the repositories. + */ + public function register() + { + $this->app->bind(UserInterface::class, UserRepository::class); + } +} diff --git a/app/Repositories/Eloquent/UserRepository.php b/app/Repositories/Eloquent/UserRepository.php new file mode 100644 index 00000000..2ca9c521 --- /dev/null +++ b/app/Repositories/Eloquent/UserRepository.php @@ -0,0 +1,78 @@ +. + * + * 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\Repositories\Eloquent; + +use Pterodactyl\Models\User; +use Illuminate\Contracts\Auth\Guard; +use Pterodactyl\Repositories\Repository; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Contracts\Repositories\UserInterface; + +class UserRepository extends Repository implements UserInterface +{ + /** + * Dependencies to automatically inject into the repository. + * + * @var array + */ + protected $inject = [ + 'guard' => Guard::class, + ]; + + /** + * Return the model to be used for the repository. + * + * @return string + */ + public function model() + { + return User::class; + } + + /** + * {@inheritdoc} + */ + public function search($term) + { + $this->model->search($term); + + return $this; + } + + public function delete($id) + { + $user = $this->model->withCount('servers')->find($id); + + if ($this->guard->user() && $this->guard->user()->id === $user->id) { + throw new DisplayException('You cannot delete your own account.'); + } + + if ($user->server_count > 0) { + throw new DisplayException('Cannot delete an account that has active servers attached to it.'); + } + + return $user->delete(); + } +} diff --git a/app/Repositories/APIRepository.php b/app/Repositories/Old/APIRepository.php similarity index 100% rename from app/Repositories/APIRepository.php rename to app/Repositories/Old/APIRepository.php diff --git a/app/Repositories/DatabaseRepository.php b/app/Repositories/Old/DatabaseRepository.php similarity index 100% rename from app/Repositories/DatabaseRepository.php rename to app/Repositories/Old/DatabaseRepository.php diff --git a/app/Repositories/HelperRepository.php b/app/Repositories/Old/HelperRepository.php similarity index 100% rename from app/Repositories/HelperRepository.php rename to app/Repositories/Old/HelperRepository.php diff --git a/app/Repositories/LocationRepository.php b/app/Repositories/Old/LocationRepository.php similarity index 100% rename from app/Repositories/LocationRepository.php rename to app/Repositories/Old/LocationRepository.php diff --git a/app/Repositories/NodeRepository.php b/app/Repositories/Old/NodeRepository.php similarity index 100% rename from app/Repositories/NodeRepository.php rename to app/Repositories/Old/NodeRepository.php diff --git a/app/Repositories/OptionRepository.php b/app/Repositories/Old/OptionRepository.php similarity index 79% rename from app/Repositories/OptionRepository.php rename to app/Repositories/Old/OptionRepository.php index 1a0ce450..6e99583c 100644 --- a/app/Repositories/OptionRepository.php +++ b/app/Repositories/Old/OptionRepository.php @@ -26,12 +26,67 @@ namespace Pterodactyl\Repositories; use DB; use Validator; +use InvalidArgumentException; use Pterodactyl\Models\ServiceOption; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Exceptions\DisplayValidationException; class OptionRepository { + /** + * Store the requested service option. + * + * @var \Pterodactyl\Models\ServiceOption + */ + protected $model; + + /** + * OptionRepository constructor. + * + * @param null|int|\Pterodactyl\Models\ServiceOption $option + */ + public function __construct($option = null) + { + if (is_null($option)) { + return; + } + + if ($option instanceof ServiceOption) { + $this->model = $option; + } else { + if (! is_numeric($option)) { + throw new InvalidArgumentException( + sprintf('Variable passed to constructor must be integer or instance of \Pterodactyl\Models\ServiceOption.') + ); + } + + $this->model = ServiceOption::findOrFail($option); + } + } + + /** + * Return the eloquent model for the given repository. + * + * @return null|\Pterodactyl\Models\ServiceOption + */ + public function getModel() + { + return $this->model; + } + + /** + * Update the currently assigned model by re-initalizing the class. + * + * @param null|int|\Pterodactyl\Models\ServiceOption $option + * @return $this + */ + public function setModel($option) + { + self::__construct($option); + + return $this; + } + /** * Creates a new service option on the system. * @@ -67,7 +122,7 @@ class OptionRepository } } - return ServiceOption::create($data); + return $this->setModel(ServiceOption::create($data))->getModel(); } /** @@ -76,13 +131,15 @@ class OptionRepository * @param int $id * @return void * + * @throws \Exception * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Throwable */ public function delete($id) { - $option = ServiceOption::with('variables')->withCount('servers')->findOrFail($id); + $this->model->load('variables', 'servers'); - if ($option->servers_count > 0) { + if ($this->model->servers->count() > 0) { throw new DisplayException('You cannot delete a service option that has servers associated with it.'); } @@ -158,32 +215,19 @@ class OptionRepository /** * Updates a service option's scripts in the database. * - * @param int $id * @param array $data - * @return \Pterodactyl\Models\ServiceOption * * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\DisplayValidationException */ - public function scripts($id, array $data) + public function scripts(array $data) { - $option = ServiceOption::findOrFail($id); - $data['script_install'] = empty($data['script_install']) ? null : $data['script_install']; - $validator = Validator::make($data, [ - 'script_install' => 'sometimes|nullable|string', - 'script_is_privileged' => 'sometimes|required|boolean', - 'script_entry' => 'sometimes|required|string', - 'script_container' => 'sometimes|required|string', - 'copy_script_from' => 'sometimes|nullable|numeric', - ]); - if (isset($data['copy_script_from']) && ! empty($data['copy_script_from'])) { - $select = ServiceOption::whereNull('copy_script_from')->where([ - ['id', $data['copy_script_from']], - ['service_id', $option->service_id], - ])->first(); + $select = ServiceOption::whereNull('copy_script_from') + ->where('id', $data['copy_script_from']) + ->where('service_id', $this->model->service_id) + ->first(); if (! $select) { throw new DisplayException('The service option selected to copy a script from either does not exist, or is copying from a higher level.'); @@ -192,12 +236,6 @@ class OptionRepository $data['copy_script_from'] = null; } - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - $option->fill($data)->save(); - - return $option; + $this->model->fill($data)->save(); } } diff --git a/app/Repositories/PackRepository.php b/app/Repositories/Old/PackRepository.php similarity index 100% rename from app/Repositories/PackRepository.php rename to app/Repositories/Old/PackRepository.php diff --git a/app/Repositories/ServiceRepository.php b/app/Repositories/Old/ServiceRepository.php similarity index 100% rename from app/Repositories/ServiceRepository.php rename to app/Repositories/Old/ServiceRepository.php diff --git a/app/Repositories/SubuserRepository.php b/app/Repositories/Old/SubuserRepository.php similarity index 99% rename from app/Repositories/SubuserRepository.php rename to app/Repositories/Old/SubuserRepository.php index 7a2aef8c..b7d73655 100644 --- a/app/Repositories/SubuserRepository.php +++ b/app/Repositories/Old/SubuserRepository.php @@ -79,7 +79,7 @@ class SubuserRepository $user = User::where('email', $data['email'])->first(); if (! $user) { try { - $repo = new UserRepository; + $repo = new oldUserRepository; $user = $repo->create([ 'email' => $data['email'], 'username' => str_random(8), diff --git a/app/Repositories/TaskRepository.php b/app/Repositories/Old/TaskRepository.php similarity index 100% rename from app/Repositories/TaskRepository.php rename to app/Repositories/Old/TaskRepository.php diff --git a/app/Repositories/VariableRepository.php b/app/Repositories/Old/VariableRepository.php similarity index 100% rename from app/Repositories/VariableRepository.php rename to app/Repositories/Old/VariableRepository.php diff --git a/app/Repositories/ServerRepository.php b/app/Repositories/Old/old_ServerRepository.php similarity index 99% rename from app/Repositories/ServerRepository.php rename to app/Repositories/Old/old_ServerRepository.php index c13613bf..e7547404 100644 --- a/app/Repositories/ServerRepository.php +++ b/app/Repositories/Old/old_ServerRepository.php @@ -43,7 +43,7 @@ use Pterodactyl\Services\DeploymentService; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Exceptions\DisplayValidationException; -class ServerRepository +class old_ServerRepository { /** * An array of daemon permission to assign to this server. diff --git a/app/Repositories/UserRepository.php b/app/Repositories/Old/old_UserRepository.php similarity index 99% rename from app/Repositories/UserRepository.php rename to app/Repositories/Old/old_UserRepository.php index 05a1a53c..06538a4d 100644 --- a/app/Repositories/UserRepository.php +++ b/app/Repositories/Old/old_UserRepository.php @@ -35,7 +35,7 @@ use Pterodactyl\Services\UuidService; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Exceptions\DisplayValidationException; -class UserRepository +class old_UserRepository { /** * Creates a user on the panel. Returns the created user's ID. diff --git a/app/Repositories/Repository.php b/app/Repositories/Repository.php new file mode 100644 index 00000000..37e2c421 --- /dev/null +++ b/app/Repositories/Repository.php @@ -0,0 +1,229 @@ +. + * + * 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\Repositories; + +use Illuminate\Container\Container; +use Illuminate\Database\Eloquent\Model; +use Pterodactyl\Contracts\Repositories\RepositoryInterface; +use Pterodactyl\Exceptions\Repository\RepositoryException; + +abstract class Repository implements RepositoryInterface +{ + const RULE_UPDATED = 'updated'; + const RULE_CREATED = 'created'; + + /** + * @var \Illuminate\Container\Container + */ + protected $container; + + /** + * Array of classes to inject automatically into the container. + * + * @var array + */ + protected $inject = []; + + /** + * @var \Illuminate\Database\Eloquent\Model + */ + protected $model; + + /** + * Array of validation rules that can be accessed from this repository. + * + * @var array + */ + protected $rules = []; + + /** + * {@inheritdoc} + */ + public function __construct(Container $container) + { + $this->container = $container; + + foreach ($this->inject as $key => $value) { + if (isset($this->{$key})) { + throw new \Exception('Cannot override a defined object in this class.'); + } + + $this->{$key} = $this->container->make($value); + } + + $this->makeModel(); + } + + /** + * {@inheritdoc} + */ + abstract public function model(); + + /** + * {@inheritdoc} + */ + public function getModel() + { + return $this->model; + } + + /** + * {@inheritdoc} + */ + public function makeModel() + { + $model = $this->container->make($this->model()); + + if (! $model instanceof Model) { + throw new RepositoryException( + "Class {$this->model()} must be an instance of \\Illuminate\\Database\\Eloquent\\Model" + ); + } + + return $this->model = $model->newQuery(); + } + + /** + * {@inheritdoc} + */ + public function getRules() + { + return $this->rules; + } + + /** + * {@inheritdoc} + */ + public function getUpdateRules() + { + if (array_key_exists(self::RULE_UPDATED, $this->rules)) { + return $this->rules[self::RULE_UPDATED]; + } + + return []; + } + + /** + * {@inheritdoc} + */ + public function getCreateRules() + { + if (array_key_exists(self::RULE_CREATED, $this->rules)) { + return $this->rules[self::RULE_CREATED]; + } + + return []; + } + + /** + * {@inheritdoc} + */ + public function with(...$params) + { + $this->model = $this->model->with($params); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function withCount(...$params) + { + $this->model = $this->model->withCount($params); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function all(array $columns = ['*']) + { + return $this->model->get($columns); + } + + /** + * {@inheritdoc} + */ + public function paginate($limit = 15, array $columns = ['*']) + { + return $this->model->paginate($limit, $columns); + } + + /** + * {@inheritdoc} + */ + public function create(array $data) + { + return $this->model->create($data); + } + + /** + * {@inheritdoc} + */ + public function update($attributes, array $data) + { + // If only a number is passed, we assume it is an ID + // for the specific model at hand. + if (is_numeric($attributes)) { + $attributes = [['id', '=', $attributes]]; + } + + return $this->model->where($attributes)->get()->each->update($data); + } + + /** + * {@inheritdoc} + */ + public function delete($id) + { + return $this->model->find($id)->delete(); + } + + /** + * {@inheritdoc} + */ + public function destroy($id) + { + return $this->model->find($id)->forceDelete(); + } + + /** + * {@inheritdoc} + */ + public function find($id, array $columns = ['*']) + { + return $this->model->find($id, $columns); + } + + /** + * {@inheritdoc} + */ + public function findBy(array $attributes, array $columns = ['*']) + { + return $this->model->where($attributes)->first($columns); + } +} diff --git a/config/app.php b/config/app.php index 3c3fb772..0b6dbeeb 100644 --- a/config/app.php +++ b/config/app.php @@ -163,6 +163,7 @@ return [ Pterodactyl\Providers\AppServiceProvider::class, Pterodactyl\Providers\AuthServiceProvider::class, Pterodactyl\Providers\EventServiceProvider::class, + Pterodactyl\Providers\RepositoryServiceProvider::class, Pterodactyl\Providers\RouteServiceProvider::class, Pterodactyl\Providers\MacroServiceProvider::class, Pterodactyl\Providers\PhraseAppTranslationProvider::class, diff --git a/database/migrations/2017_06_10_152951_add_external_id_to_users.php b/database/migrations/2017_06_10_152951_add_external_id_to_users.php new file mode 100644 index 00000000..696f10f4 --- /dev/null +++ b/database/migrations/2017_06_10_152951_add_external_id_to_users.php @@ -0,0 +1,32 @@ +unsignedInteger('external_id')->after('id')->nullable()->unique(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('external_id'); + }); + } +} diff --git a/resources/themes/pterodactyl/admin/users/view.blade.php b/resources/themes/pterodactyl/admin/users/view.blade.php index 39461e46..3e23bb26 100644 --- a/resources/themes/pterodactyl/admin/users/view.blade.php +++ b/resources/themes/pterodactyl/admin/users/view.blade.php @@ -68,6 +68,7 @@ diff --git a/routes/admin.php b/routes/admin.php index 7b6c459e..2fe4a0b2 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -81,12 +81,12 @@ Route::group(['prefix' => 'users'], function () { Route::get('/', 'UserController@index')->name('admin.users'); Route::get('/accounts.json', 'UserController@json')->name('admin.users.json'); Route::get('/new', 'UserController@create')->name('admin.users.new'); - Route::get('/view/{id}', 'UserController@view')->name('admin.users.view'); + Route::get('/view/{user}', 'UserController@view')->name('admin.users.view'); Route::post('/new', 'UserController@store'); - Route::post('/view/{id}', 'UserController@update'); + Route::patch('/view/{user}', 'UserController@update'); - Route::delete('/view/{id}', 'UserController@delete'); + Route::delete('/view/{user}', 'UserController@delete'); }); /* @@ -168,17 +168,17 @@ Route::group(['prefix' => 'services'], function () { Route::get('/view/{id}', 'ServiceController@view')->name('admin.services.view'); Route::get('/view/{id}/functions', 'ServiceController@viewFunctions')->name('admin.services.view.functions'); Route::get('/option/new', 'OptionController@create')->name('admin.services.option.new'); - Route::get('/option/{id}', 'OptionController@viewConfiguration')->name('admin.services.option.view'); - Route::get('/option/{id}/variables', 'OptionController@viewVariables')->name('admin.services.option.variables'); - Route::get('/option/{id}/scripts', 'OptionController@viewScripts')->name('admin.services.option.scripts'); + Route::get('/option/{option}', 'OptionController@viewConfiguration')->name('admin.services.option.view'); + Route::get('/option/{option}/variables', 'OptionController@viewVariables')->name('admin.services.option.variables'); + Route::get('/option/{option}/scripts', 'OptionController@viewScripts')->name('admin.services.option.scripts'); Route::post('/new', 'ServiceController@store'); - Route::post('/view/{id}', 'ServiceController@edit'); + Route::post('/view/{option}', 'ServiceController@edit'); Route::post('/option/new', 'OptionController@store'); - Route::post('/option/{id}', 'OptionController@editConfiguration'); - Route::post('/option/{id}/scripts', 'OptionController@updateScripts'); - Route::post('/option/{id}/variables', 'OptionController@createVariable'); - Route::post('/option/{id}/variables/{variable}', 'OptionController@editVariable')->name('admin.services.option.variables.edit'); + Route::post('/option/{option}', 'OptionController@editConfiguration'); + Route::post('/option/{option}/scripts', 'OptionController@updateScripts'); + Route::post('/option/{option}/variables', 'OptionController@createVariable'); + Route::post('/option/{option}/variables/{variable}', 'OptionController@editVariable')->name('admin.services.option.variables.edit'); Route::delete('/view/{id}', 'ServiceController@delete'); }); From 26e476a794ae568b005b8f6e483be7a5375a039b Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 13 Jun 2017 23:25:37 -0500 Subject: [PATCH 03/99] Push updates, removes repositories, begins moving functionality to services. First integration tests included. --- app/Http/Controllers/Admin/UserController.php | 35 ++-- app/Http/Requests/Admin/UserFormRequest.php | 28 +-- app/Observers/UserObserver.php | 24 +-- app/Providers/RouteServiceProvider.php | 1 + app/Services/{ => Components}/UuidService.php | 2 +- app/Services/UserService.php | 187 ++++++++++++++++++ config/app.php | 1 - database/factories/ModelFactory.php | 20 +- phpunit.xml | 31 +++ tests/CreatesApplication.php | 22 +++ tests/ExampleTest.php | 16 -- tests/Feature/Services/UserServiceTest.php | 156 +++++++++++++++ tests/TestCase.php | 28 +-- .../Unit/Services/UserServiceTest.php | 43 ++-- 14 files changed, 492 insertions(+), 102 deletions(-) rename app/Services/{ => Components}/UuidService.php (98%) create mode 100644 app/Services/UserService.php create mode 100644 phpunit.xml create mode 100644 tests/CreatesApplication.php delete mode 100644 tests/ExampleTest.php create mode 100644 tests/Feature/Services/UserServiceTest.php rename app/Providers/RepositoryServiceProvider.php => tests/Unit/Services/UserServiceTest.php (55%) diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index 1b963228..1f1bbd56 100644 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -27,27 +27,30 @@ namespace Pterodactyl\Http\Controllers\Admin; use Alert; use Illuminate\Http\Request; -use Pterodactyl\Contracts\Repositories\UserInterface; +use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Http\Requests\Admin\UserFormRequest; use Pterodactyl\Models\User; use Pterodactyl\Http\Controllers\Controller; +use Pterodactyl\Services\UserService; class UserController extends Controller { /** - * @var \Pterodactyl\Repositories\Eloquent\UserRepository + * @var \Pterodactyl\Services\UserService */ - protected $repository; + protected $service; /** * UserController constructor. * - * @param \Pterodactyl\Contracts\Repositories\UserInterface $repository + * @param \Prologue\Alerts\AlertsMessageBag $alert + * @param \Pterodactyl\Services\UserService $service */ - public function __construct(UserInterface $repository) + public function __construct(AlertsMessageBag $alert, UserService $service) { - $this->repository = $repository; + $this->alert = $alert; + $this->service = $service; } /** @@ -58,7 +61,7 @@ class UserController extends Controller */ public function index(Request $request) { - $users = $this->repository->withCount('servers', 'subuserOf'); + $users = User::withCount('servers', 'subuserOf'); if (! is_null($request->input('query'))) { $users->search($request->input('query')); @@ -97,15 +100,17 @@ class UserController extends Controller * * @param \Pterodactyl\Models\User $user * @return \Illuminate\Http\RedirectResponse + * + * @throws \Exception */ public function delete(User $user) { try { - $this->repository->delete($user->id); + $this->service->delete($user); return redirect()->route('admin.users'); } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); + $this->alert->danger($ex->getMessage())->flash(); } return redirect()->route('admin.users.view', $user->id); @@ -116,11 +121,14 @@ class UserController extends Controller * * @param \Pterodactyl\Http\Requests\Admin\UserFormRequest $request * @return \Illuminate\Http\RedirectResponse + * + * @throws \Exception + * @throws \Throwable */ public function store(UserFormRequest $request) { - $user = $this->repository->create($request->normalize()); - Alert::success('Account has been successfully created.')->flash(); + $user = $this->service->create($request->normalize()); + $this->alert->success('Account has been successfully created.')->flash(); return redirect()->route('admin.users.view', $user->id); } @@ -134,7 +142,8 @@ class UserController extends Controller */ public function update(UserFormRequest $request, User $user) { - $this->repository->update($user->id, $request->normalize()); + $this->service->update($user, $request->normalize()); + $this->alert->success('User account has been updated.')->flash(); return redirect()->route('admin.users.view', $user->id); } @@ -147,7 +156,7 @@ class UserController extends Controller */ public function json(Request $request) { - return $this->repository->search($request->input('q'))->all([ + return User::search($request->input('q'))->all([ 'id', 'email', 'username', 'name_first', 'name_last', ])->transform(function ($item) { $item->md5 = md5(strtolower($item->email)); diff --git a/app/Http/Requests/Admin/UserFormRequest.php b/app/Http/Requests/Admin/UserFormRequest.php index 10d55977..09605a31 100644 --- a/app/Http/Requests/Admin/UserFormRequest.php +++ b/app/Http/Requests/Admin/UserFormRequest.php @@ -25,7 +25,6 @@ namespace Pterodactyl\Http\Requests\Admin; use Pterodactyl\Models\User; -use Illuminate\Support\Facades\Hash; use Pterodactyl\Contracts\Repositories\UserInterface; class UserFormRequest extends AdminFormRequest @@ -45,21 +44,21 @@ class UserFormRequest extends AdminFormRequest { if ($this->method() === 'PATCH') { return [ - 'email' => 'sometimes|required|email|unique:users,email,' . $this->user->id, - 'username' => 'sometimes|required|alpha_dash|between:1,255|unique:users,username, ' . $this->user->id . '|' . User::USERNAME_RULES, - 'name_first' => 'sometimes|required|string|between:1,255', - 'name_last' => 'sometimes|required|string|between:1,255', + 'email' => 'required|email|unique:users,email,' . $this->user->id, + 'username' => 'required|alpha_dash|between:1,255|unique:users,username, ' . $this->user->id . '|' . User::USERNAME_RULES, + 'name_first' => 'required|string|between:1,255', + 'name_last' => 'required|string|between:1,255', 'password' => 'sometimes|nullable|' . User::PASSWORD_RULES, - 'root_admin' => 'sometimes|required|boolean', - 'language' => 'sometimes|required|string|min:1|max:5', - 'use_totp' => 'sometimes|required|boolean', - 'totp_secret' => 'sometimes|required|size:16', + 'root_admin' => 'required|boolean', +// 'language' => 'sometimes|required|string|min:1|max:5', +// 'use_totp' => 'sometimes|required|boolean', +// 'totp_secret' => 'sometimes|required|size:16', ]; } return [ - 'email' => 'required|email|unique:users,email,' . $this->user->id, - 'username' => 'required|alpha_dash|between:1,255|unique:users,username,' . $this->user->id . '|' . User::USERNAME_RULES, + 'email' => 'required|email|unique:users,email', + 'username' => 'required|alpha_dash|between:1,255|unique:users,username|' . User::USERNAME_RULES, 'name_first' => 'required|string|between:1,255', 'name_last' => 'required|string|between:1,255', 'password' => 'sometimes|nullable|' . User::PASSWORD_RULES, @@ -70,8 +69,11 @@ class UserFormRequest extends AdminFormRequest public function normalize() { - if ($this->has('password')) { - $this->merge(['password' => Hash::make($this->input('password'))]); + if ($this->method === 'PATCH') { + return array_merge( + $this->intersect('password'), + $this->only(['email', 'username', 'name_first', 'name_last', 'root_admin']) + ); } return parent::normalize(); diff --git a/app/Observers/UserObserver.php b/app/Observers/UserObserver.php index f6e16026..e75c053b 100644 --- a/app/Observers/UserObserver.php +++ b/app/Observers/UserObserver.php @@ -24,13 +24,9 @@ namespace Pterodactyl\Observers; -use DB; -use Hash; -use Carbon; use Pterodactyl\Events; use Pterodactyl\Models\User; -use Pterodactyl\Notifications\AccountCreated; -use Pterodactyl\Services\UuidService; +use Pterodactyl\Services\Components\UuidService; class UserObserver { @@ -49,7 +45,7 @@ class UserObserver */ public function creating(User $user) { - $user->uuid = $this->uuid->generate(); + $user->uuid = $this->uuid->generate('users', 'uuid'); event(new Events\User\Creating($user)); } @@ -62,22 +58,6 @@ class UserObserver */ public function created(User $user) { - dd($user); - if ($user->password === 'unset') { - $token = hash_hmac('sha256', str_random(40), config('app.key')); - DB::table('password_resets')->insert([ - 'email' => $user->email, - 'token' => Hash::make($token), - 'created_at' => Carbon::now()->toDateTimeString(), - ]); - } - - $user->notify(new AccountCreated([ - 'name' => $user->name_first, - 'username' => $user->username, - 'token' => (isset($token)) ? $token : null, - ])); - event(new Events\User\Created($user)); } diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 00d40bdb..6a8403ff 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -4,6 +4,7 @@ namespace Pterodactyl\Providers; use Illuminate\Support\Facades\Route; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider; +use Pterodactyl\Models\User; class RouteServiceProvider extends ServiceProvider { diff --git a/app/Services/UuidService.php b/app/Services/Components/UuidService.php similarity index 98% rename from app/Services/UuidService.php rename to app/Services/Components/UuidService.php index 2b043731..468a97f8 100644 --- a/app/Services/UuidService.php +++ b/app/Services/Components/UuidService.php @@ -22,7 +22,7 @@ * SOFTWARE. */ -namespace Pterodactyl\Services; +namespace Pterodactyl\Services\Components; use DB; use Uuid; diff --git a/app/Services/UserService.php b/app/Services/UserService.php new file mode 100644 index 00000000..81119f11 --- /dev/null +++ b/app/Services/UserService.php @@ -0,0 +1,187 @@ +. + * + * 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; + +use Illuminate\Config\Repository as ConfigRepository; +use Illuminate\Contracts\Auth\Guard; +use Illuminate\Contracts\Hashing\Hasher; +use Illuminate\Database\Connection; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Models\User; +use Pterodactyl\Notifications\AccountCreated; +use Pterodactyl\Services\Components\UuidService; + +class UserService +{ + const HMAC_ALGO = 'sha256'; + + /** + * @var \Illuminate\Config\Repository + */ + protected $config; + + /** + * @var \Illuminate\Database\Connection + */ + protected $database; + + /** + * @var \Illuminate\Contracts\Auth\Guard + */ + protected $guard; + + /** + * @var \Illuminate\Contracts\Hashing\Hasher + */ + protected $hasher; + + /** + * @var \Pterodactyl\Services\Components\UuidService + */ + protected $uuid; + + /** + * UserService constructor. + * + * @param \Illuminate\Config\Repository $config + * @param \Illuminate\Database\Connection $database + * @param \Illuminate\Contracts\Auth\Guard $guard + * @param \Illuminate\Contracts\Hashing\Hasher $hasher + * @param \Pterodactyl\Services\Components\UuidService $uuid + */ + public function __construct( + ConfigRepository $config, + Connection $database, + Guard $guard, + Hasher $hasher, + UuidService $uuid + ) { + $this->config = $config; + $this->database = $database; + $this->guard = $guard; + $this->hasher = $hasher; + $this->uuid = $uuid; + } + + /** + * Assign a temporary password to an account and return an authentication token to + * email to the user for resetting their password. + * + * @param \Pterodactyl\Models\User $user + * @return string + */ + protected function assignTemporaryPassword(User $user) + { + $user->password = $this->hasher->make(str_random(30)); + + $token = hash_hmac(self::HMAC_ALGO, str_random(40), $this->config->get('app.key')); + + $this->database->table('password_resets')->insert([ + 'email' => $user->email, + 'token' => $this->hasher->make($token), + ]); + + return $token; + } + + /** + * Create a new user on the system. + * + * @param array $data + * @return \Pterodactyl\Models\User + * + * @throws \Exception + * @throws \Throwable + */ + public function create(array $data) + { + if (array_key_exists('password', $data) && ! empty($data['password'])) { + $data['password'] = $this->hasher->make($data['password']); + } + + $user = new User; + $user->fill($data); + + // Persist the data + $token = $this->database->transaction(function () use ($user) { + if (empty($user->password)) { + $token = $this->assignTemporaryPassword($user); + } + + $user->save(); + + return $token ?? null; + }); + + $user->notify(new AccountCreated([ + 'name' => $user->name_first, + 'username' => $user->username, + 'token' => $token, + ])); + + return $user; + } + + /** + * Update the user model. + * + * @param \Pterodactyl\Models\User $user + * @param array $data + * @return \Pterodactyl\Models\User + */ + public function update(User $user, array $data) + { + if (isset($data['password'])) { + $data['password'] = $this->hasher->make($data['password']); + } + + $user->fill($data)->save(); + + return $user; + } + + /** + * @param \Pterodactyl\Models\User $user + * @return bool|null + * @throws \Exception + * @throws \Pterodactyl\Exceptions\DisplayException + */ + public function delete(User $user) + { + if ($user->servers()->count() > 0) { + throw new DisplayException('Cannot delete an account that has active servers attached to it.'); + } + + if ($this->guard->check() && $this->guard->id() === $user->id) { + throw new DisplayException('You cannot delete your own account.'); + } + + if ($user->servers()->count() > 0) { + throw new DisplayException('Cannot delete an account that has active servers attached to it.'); + } + + return $user->delete(); + } +} diff --git a/config/app.php b/config/app.php index 0b6dbeeb..3c3fb772 100644 --- a/config/app.php +++ b/config/app.php @@ -163,7 +163,6 @@ return [ Pterodactyl\Providers\AppServiceProvider::class, Pterodactyl\Providers\AuthServiceProvider::class, Pterodactyl\Providers\EventServiceProvider::class, - Pterodactyl\Providers\RepositoryServiceProvider::class, Pterodactyl\Providers\RouteServiceProvider::class, Pterodactyl\Providers\MacroServiceProvider::class, Pterodactyl\Providers\PhraseAppTranslationProvider::class, diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index 7295947c..fe45d9de 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -13,9 +13,21 @@ $factory->define(Pterodactyl\Models\User::class, function (Faker\Generator $faker) { return [ - 'name' => $faker->name, - 'email' => $faker->email, - 'password' => bcrypt(str_random(10)), - 'remember_token' => str_random(10), + 'external_id' => null, + 'uuid' => $faker->uuid, + 'username' => $faker->userName, + 'email' => $faker->safeEmail, + 'name_first' => $faker->firstName, + 'name_last' => $faker->lastName, + 'password' => bcrypt('password'), + 'language' => 'en', + 'root_admin' => false, + 'use_totp' => false, + ]; +}); + +$factory->state(Pterodactyl\Models\User::class, 'admin', function () { + return [ + 'root_admin' => true, ]; }); diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 00000000..9ecda835 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,31 @@ + + + + + ./tests/Feature + + + + ./tests/Unit + + + + + ./app + + + + + + + + + diff --git a/tests/CreatesApplication.php b/tests/CreatesApplication.php new file mode 100644 index 00000000..547152f6 --- /dev/null +++ b/tests/CreatesApplication.php @@ -0,0 +1,22 @@ +make(Kernel::class)->bootstrap(); + + return $app; + } +} diff --git a/tests/ExampleTest.php b/tests/ExampleTest.php deleted file mode 100644 index 2c3a83c8..00000000 --- a/tests/ExampleTest.php +++ /dev/null @@ -1,16 +0,0 @@ -visit('/') - ->see('Laravel 5'); - } -} diff --git a/tests/Feature/Services/UserServiceTest.php b/tests/Feature/Services/UserServiceTest.php new file mode 100644 index 00000000..1f95b53b --- /dev/null +++ b/tests/Feature/Services/UserServiceTest.php @@ -0,0 +1,156 @@ +. + * + * 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\Feature\Services; + +use Illuminate\Support\Facades\Notification; +use Pterodactyl\Models\User; +use Pterodactyl\Notifications\AccountCreated; +use Pterodactyl\Services\UserService; +use Tests\TestCase; + +class UserServiceTest extends TestCase +{ + protected $service; + + public function setUp() + { + parent::setUp(); + + $this->service = $this->app->make(UserService::class); + } + + public function testShouldReturnNewUserWithValidData() + { + Notification::fake(); + + $user = $this->service->create([ + 'email' => 'test_account@example.com', + 'username' => 'test_account', + 'password' => 'test_password', + 'name_first' => 'Test', + 'name_last' => 'Account', + 'root_admin' => false, + ]); + + $this->assertNotNull($user->uuid); + $this->assertNotEquals($user->password, 'test_password'); + + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'uuid' => $user->uuid, + 'email' => 'test_account@example.com', + 'root_admin' => '0', + ]); + + Notification::assertSentTo($user, AccountCreated::class, function ($notification) use ($user) { + $this->assertEquals($user->username, $notification->user->username); + $this->assertNull($notification->user->token); + + return true; + }); + } + + public function testShouldReturnNewUserWithPasswordTokenIfNoPasswordProvided() + { + Notification::fake(); + + $user = $this->service->create([ + 'email' => 'test_account@example.com', + 'username' => 'test_account', + 'name_first' => 'Test', + 'name_last' => 'Account', + 'root_admin' => false, + ]); + + $this->assertNotNull($user->uuid); + $this->assertNotNull($user->password); + + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'uuid' => $user->uuid, + 'email' => 'test_account@example.com', + 'root_admin' => '0', + ]); + + Notification::assertSentTo($user, AccountCreated::class, function ($notification) use ($user) { + $this->assertEquals($user->username, $notification->user->username); + $this->assertNotNull($notification->user->token); + + $this->assertDatabaseHas('password_resets', [ + 'email' => $user->email, + ]); + + return true; + }); + } + + public function testShouldUpdateUserModelInDatabase() + { + $user = factory(User::class)->create(); + + $response = $this->service->update($user, [ + 'email' => 'test_change@example.com', + 'password' => 'test_password', + ]); + + $this->assertInstanceOf(User::class, $response); + $this->assertEquals('test_change@example.com', $response->email); + $this->assertNotEquals($response->password, 'test_password'); + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'email' => 'test_change@example.com', + ]); + } + + public function testShouldDeleteUserFromDatabase() + { + $user = factory(User::class)->create(); + $service = $this->app->make(UserService::class); + + $response = $service->delete($user); + + $this->assertTrue($response); + $this->assertDatabaseMissing('users', [ + 'id' => $user->id, + 'uuid' => $user->uuid, + ]); + } + + /** + * @expectedException \Pterodactyl\Exceptions\DisplayException + */ + public function testShouldBlockDeletionOfOwnAccount() + { + $user = factory(User::class)->create(); + $this->actingAs($user); + + $this->service->delete($user); + } + + public function testAlgoForHashingShouldBeRegistered() + { + $this->assertArrayHasKey(UserService::HMAC_ALGO, array_flip(hash_algos())); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 916b8ad1..c1ac8acc 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -1,25 +1,11 @@ make(Illuminate\Contracts\Console\Kernel::class)->bootstrap(); - - return $app; - } + use CreatesApplication, DatabaseTransactions; } diff --git a/app/Providers/RepositoryServiceProvider.php b/tests/Unit/Services/UserServiceTest.php similarity index 55% rename from app/Providers/RepositoryServiceProvider.php rename to tests/Unit/Services/UserServiceTest.php index 745e6d05..c4547469 100644 --- a/app/Providers/RepositoryServiceProvider.php +++ b/tests/Unit/Services/UserServiceTest.php @@ -1,5 +1,5 @@ . * @@ -22,19 +22,40 @@ * SOFTWARE. */ -namespace Pterodactyl\Providers; +namespace Tests\Unit\Services; -use Illuminate\Support\ServiceProvider; -use Pterodactyl\Contracts\Repositories\UserInterface; -use Pterodactyl\Repositories\Eloquent\UserRepository; +use Illuminate\Config\Repository; +use Illuminate\Contracts\Auth\Guard; +use Illuminate\Contracts\Hashing\Hasher; +use Illuminate\Database\Connection; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Facades\Queue; +use \Mockery as m; +use Pterodactyl\Models\User; +use Pterodactyl\Services\Components\UuidService; +use Pterodactyl\Services\UserService; +use Tests\TestCase; -class RepositoryServiceProvider extends ServiceProvider +class UserServiceTest extends TestCase { - /** - * Register the repositories. - */ - public function register() + protected $service; + + public function setUp() { - $this->app->bind(UserInterface::class, UserRepository::class); + parent::setUp(); + + $this->config = m::mock(Repository::class); + $this->database = m::mock(Connection::class); + $this->guard = m::mock(Guard::class); + $this->hasher = m::mock(Hasher::class); + $this->uuid = m::mock(UuidService::class); + + $this->service = new UserService( + $this->config, + $this->database, + $this->guard, + $this->hasher, + $this->uuid + );; } } From fe4977f0facc69e4fb06091df2f0721818c438ee Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Wed, 14 Jun 2017 23:53:24 -0500 Subject: [PATCH 04/99] Update admin location routes and controller to use service Needs tests written, uses new validation on model. --- .../Controllers/Admin/LocationController.php | 133 +++++++++++------- app/Http/Requests/Admin/LocationRequest.php | 40 ++++++ app/Models/Location.php | 13 ++ app/Services/LocationService.php | 98 +++++++++++++ composer.json | 7 +- composer.lock | 58 +++++++- routes/admin.php | 4 +- 7 files changed, 293 insertions(+), 60 deletions(-) create mode 100644 app/Http/Requests/Admin/LocationRequest.php create mode 100644 app/Services/LocationService.php diff --git a/app/Http/Controllers/Admin/LocationController.php b/app/Http/Controllers/Admin/LocationController.php index 4e602f7c..8325405f 100644 --- a/app/Http/Controllers/Admin/LocationController.php +++ b/app/Http/Controllers/Admin/LocationController.php @@ -24,96 +24,127 @@ namespace Pterodactyl\Http\Controllers\Admin; -use Log; -use Alert; -use Illuminate\Http\Request; use Pterodactyl\Models\Location; -use Pterodactyl\Exceptions\DisplayException; +use Prologue\Alerts\AlertsMessageBag; +use Pterodactyl\Services\LocationService; use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Repositories\LocationRepository; -use Pterodactyl\Exceptions\DisplayValidationException; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Http\Requests\Admin\LocationRequest; class LocationController extends Controller { + /** + * @var \Prologue\Alerts\AlertsMessageBag + */ + protected $alert; + + /** + * @var \Pterodactyl\Models\Location + */ + protected $location; + + /** + * @var \Pterodactyl\Services\LocationService + */ + protected $service; + + /** + * LocationController constructor. + * + * @param \Prologue\Alerts\AlertsMessageBag $alert + * @param \Pterodactyl\Models\Location $location + * @param \Pterodactyl\Services\LocationService $service + */ + public function __construct(AlertsMessageBag $alert, Location $location, LocationService $service) + { + $this->alert = $alert; + $this->location = $location; + $this->service = $service; + } + /** * Return the location overview page. * - * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ - public function index(Request $request) + public function index() { return view('admin.locations.index', [ - 'locations' => Location::withCount('nodes', 'servers')->get(), + 'locations' => $this->location->withCount('nodes', 'servers')->get(), ]); } /** * Return the location view page. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Pterodactyl\Models\Location $location * @return \Illuminate\View\View */ - public function view(Request $request, $id) + public function view(Location $location) { - return view('admin.locations.view', ['location' => Location::with('nodes.servers')->findOrFail($id)]); + $location->load('nodes.servers'); + + return view('admin.locations.view', ['location' => $location]); } /** * Handle request to create new location. * - * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Http\Requests\Admin\LocationRequest $request * @return \Illuminate\Http\RedirectResponse + * + * @throws \Throwable + * @throws \Watson\Validating\ValidationException */ - public function create(Request $request) + public function create(LocationRequest $request) { - $repo = new LocationRepository; + $location = $this->service->create($request->normalize()); + $this->alert->success('Location was created successfully.')->flash(); - try { - $location = $repo->create($request->intersect(['short', 'long'])); - Alert::success('Location was created successfully.')->flash(); - - return redirect()->route('admin.locations.view', $location->id); - } catch (DisplayValidationException $ex) { - return redirect()->route('admin.locations')->withErrors(json_decode($ex->getMessage())); - } catch (\Exception $ex) { - Log::error($ex); - Alert::error('An unhandled exception occurred while processing this request. This error has been logged.')->flash(); - } - - return redirect()->route('admin.locations'); + return redirect()->route('admin.locations.view', $location->id); } /** * Handle request to update or delete location. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Pterodactyl\Http\Requests\Admin\LocationRequest $request + * @param \Pterodactyl\Models\Location $location * @return \Illuminate\Http\RedirectResponse + * + * @throws \Throwable + * @throws \Watson\Validating\ValidationException */ - public function update(Request $request, $id) + public function update(LocationRequest $request, Location $location) { - $repo = new LocationRepository; - - try { - if ($request->input('action') !== 'delete') { - $location = $repo->update($id, $request->intersect(['short', 'long'])); - Alert::success('Location was updated successfully.')->flash(); - } else { - $repo->delete($id); - - return redirect()->route('admin.locations'); - } - } catch (DisplayValidationException $ex) { - return redirect()->route('admin.locations.view', $id)->withErrors(json_decode($ex->getMessage())); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::error('An unhandled exception occurred while processing this request. This error has been logged.')->flash(); + if ($request->input('action') === 'delete') { + return $this->delete($location); } - return redirect()->route('admin.locations.view', $id); + $this->service->update($location, $request->normalize()); + $this->alert->success('Location was updated successfully.')->flash(); + + return redirect()->route('admin.locations.view', $location->id); + } + + /** + * Delete a location from the system. + * + * @param \Pterodactyl\Models\Location $location + * @return \Illuminate\Http\RedirectResponse + * + * @throws \Exception + * @throws \Pterodactyl\Exceptions\DisplayException + */ + public function delete(Location $location) + { + try { + $this->service->delete($location); + + return redirect()->route('admin.locations'); + } catch (DisplayException $ex) { + $this->alert->danger($ex->getMessage())->flash(); + } + + return redirect()->route('admin.locations.view', $location->id); } } diff --git a/app/Http/Requests/Admin/LocationRequest.php b/app/Http/Requests/Admin/LocationRequest.php new file mode 100644 index 00000000..c2f80d54 --- /dev/null +++ b/app/Http/Requests/Admin/LocationRequest.php @@ -0,0 +1,40 @@ +. + * + * 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\Http\Requests\Admin; + +use Pterodactyl\Models\Location; + +class LocationRequest extends AdminFormRequest +{ + /** + * Setup the validation rules to use for these requests. + * + * @return array + */ + public function rules() + { + return app()->make(Location::class)->getRules(); + } +} diff --git a/app/Models/Location.php b/app/Models/Location.php index f9ceec76..bb152940 100644 --- a/app/Models/Location.php +++ b/app/Models/Location.php @@ -25,9 +25,12 @@ namespace Pterodactyl\Models; use Illuminate\Database\Eloquent\Model; +use Watson\Validating\ValidatingTrait; class Location extends Model { + use ValidatingTrait; + /** * The table associated with the model. * @@ -42,6 +45,16 @@ class Location extends Model */ protected $guarded = ['id', 'created_at', 'updated_at']; + /** + * Validation rules to apply when attempting to save a model to the DB. + * + * @var array + */ + protected $rules = [ + 'short' => 'required|string|between:1,60|unique:locations,short', + 'long' => 'required|string|between:1,255', + ]; + /** * Gets the nodes in a specificed location. * diff --git a/app/Services/LocationService.php b/app/Services/LocationService.php new file mode 100644 index 00000000..8db67592 --- /dev/null +++ b/app/Services/LocationService.php @@ -0,0 +1,98 @@ +. + * + * 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; + +use Pterodactyl\Models\Location; +use Pterodactyl\Exceptions\DisplayException; + +class LocationService +{ + /** + * @var \Pterodactyl\Models\Location + */ + protected $model; + + /** + * LocationService constructor. + * + * @param \Pterodactyl\Models\Location $location + */ + public function __construct(Location $location) + { + $this->model = $location; + } + + /** + * Create the location in the database and return it. + * + * @param array $data + * @return \Pterodactyl\Models\Location + * + * @throws \Throwable + * @throws \Watson\Validating\ValidationException + */ + public function create(array $data) + { + $location = $this->model->fill($data); + $location->saveOrFail(); + + return $location; + } + + /** + * Update location model in the DB. + * + * @param \Pterodactyl\Models\Location $location + * @param array $data + * @return \Pterodactyl\Models\Location + * + * @throws \Throwable + * @throws \Watson\Validating\ValidationException + */ + public function update(Location $location, array $data) + { + $location->fill($data)->saveOrFail(); + + return $location; + } + + /** + * Delete a model from the DB. + * + * @param \Pterodactyl\Models\Location $location + * @return bool + * + * @throws \Exception + * @throws \Pterodactyl\Exceptions\DisplayException + */ + public function delete(Location $location) + { + if ($location->nodes()->count() > 0) { + throw new DisplayException('Cannot delete a location that has nodes assigned to it.'); + } + + return $location->delete(); + } +} diff --git a/composer.json b/composer.json index c1fea93a..861b11d4 100644 --- a/composer.json +++ b/composer.json @@ -12,14 +12,14 @@ ], "require": { "php": ">=7.0.0", + "ext-mbstring": "*", + "ext-pdo_mysql": "*", + "ext-zip": "*", "aws/aws-sdk-php": "3.26.5", "barryvdh/laravel-debugbar": "2.3.2", "daneeveritt/login-notifications": "1.0.0", "doctrine/dbal": "2.5.12", "edvinaskrucas/settings": "2.0.0", - "ext-mbstring": "*", - "ext-zip": "*", - "ext-pdo_mysql": "*", "fideloper/proxy": "3.3.0", "guzzlehttp/guzzle": "6.2.3", "igaster/laravel-theme": "1.14.0", @@ -35,6 +35,7 @@ "prologue/alerts": "0.4.1", "s1lentium/iptools": "1.1.0", "spatie/laravel-fractal": "4.0.0", + "watson/validating": "3.0.s1", "webpatser/laravel-uuid": "2.0.1" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 03b4a067..a8466d76 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "84c501086eb1d2505bf6ce8bb4d06f61", - "content-hash": "5d52bbe44333fc6d4a0d922958510fde", + "hash": "3a539370a2c653dbe460cad3d03c3db5", + "content-hash": "ad3015e0fe97ab992635581a6c72fddd", "packages": [ { "name": "aws/aws-sdk-php", @@ -3640,6 +3640,56 @@ ], "time": "2016-09-01 10:05:43" }, + { + "name": "watson/validating", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/dwightwatson/validating.git", + "reference": "3cef5b4cd0af2dc26d2c7ca668bd12f4d4ab421b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dwightwatson/validating/zipball/3cef5b4cd0af2dc26d2c7ca668bd12f4d4ab421b", + "reference": "3cef5b4cd0af2dc26d2c7ca668bd12f4d4ab421b", + "shasum": "" + }, + "require": { + "illuminate/contracts": ">=5.3", + "illuminate/database": ">=5.3", + "illuminate/events": ">=5.3", + "illuminate/support": ">=5.3", + "illuminate/validation": ">=5.3", + "php": ">=5.4.0" + }, + "require-dev": { + "mockery/mockery": "0.9.*", + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Watson\\Validating\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dwight Watson", + "email": "dwight@studiousapp.com" + } + ], + "description": "Eloquent model validating trait.", + "keywords": [ + "eloquent", + "laravel", + "validation" + ], + "time": "2016-10-31 21:53:17" + }, { "name": "webpatser/laravel-uuid", "version": "2.0.1", @@ -5938,8 +5988,8 @@ "platform": { "php": ">=7.0.0", "ext-mbstring": "*", - "ext-zip": "*", - "ext-pdo_mysql": "*" + "ext-pdo_mysql": "*", + "ext-zip": "*" }, "platform-dev": [] } diff --git a/routes/admin.php b/routes/admin.php index 2fe4a0b2..6d2730ce 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -33,10 +33,10 @@ Route::get('/', 'BaseController@getIndex')->name('admin.index'); */ Route::group(['prefix' => 'locations'], function () { Route::get('/', 'LocationController@index')->name('admin.locations'); - Route::get('/view/{id}', 'LocationController@view')->name('admin.locations.view'); + Route::get('/view/{location}', 'LocationController@view')->name('admin.locations.view'); Route::post('/', 'LocationController@create'); - Route::post('/view/{id}', 'LocationController@update'); + Route::post('/view/{location}', 'LocationController@update'); }); /* From 13cd01cfe6ce64565c218ccb99b5f11d3748b4e1 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Wed, 14 Jun 2017 23:55:11 -0500 Subject: [PATCH 05/99] Use valid version... :crab: --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 861b11d4..617a07fd 100644 --- a/composer.json +++ b/composer.json @@ -35,7 +35,7 @@ "prologue/alerts": "0.4.1", "s1lentium/iptools": "1.1.0", "spatie/laravel-fractal": "4.0.0", - "watson/validating": "3.0.s1", + "watson/validating": "3.0.1", "webpatser/laravel-uuid": "2.0.1" }, "require-dev": { From 760525a673a40bd400735eca70977b38d500cb9c Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 15 Jun 2017 23:03:22 -0500 Subject: [PATCH 06/99] Push more tests for location services, setup travis CI integration --- .env.travis | 12 ++ .travis.yml | 32 +++ app/Services/LocationService.php | 17 +- config/database.php | 13 ++ database/factories/ModelFactory.php | 7 + phpunit.xml | 2 + .../Feature/Services/LocationServiceTest.php | 204 ++++++++++++++++++ tests/Unit/Services/UserServiceTest.php | 61 ------ 8 files changed, 279 insertions(+), 69 deletions(-) create mode 100644 .env.travis create mode 100644 .travis.yml create mode 100644 tests/Feature/Services/LocationServiceTest.php delete mode 100644 tests/Unit/Services/UserServiceTest.php diff --git a/.env.travis b/.env.travis new file mode 100644 index 00000000..c3bd0801 --- /dev/null +++ b/.env.travis @@ -0,0 +1,12 @@ +APP_ENV=testing +APP_DEBUG=true +APP_KEY=SomeRandomString32SomeRandomString32 + +DB_CONNECTION=tests +DB_TEST_USERNAME=root +DB_TEST_PASSWORD= + +CACHE_DRIVER=array +SESSION_DRIVER=array +QUEUE_DRIVER=sync +MAIL_DRIVER=array diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..47148c7b --- /dev/null +++ b/.travis.yml @@ -0,0 +1,32 @@ +langauge: php + +dist: trusty + +php: + - 7.0 + - 7.1 + - 7.2 + +sudo: required + +cache: + directories: + - $HOME/.composer/cache + +services: + - mysql + +before_install: + - mysql -e 'CREATE DATABASE travis;' + +before_script: + - phpenv config-rm xdebug.ini + - cp .env.travis .env + - composer self-update + - composer install --no-interaction + - php artisan key:generate --force + - php artisan migrate --force + - php artisan db:seed --force + +script: + - vendor/bin/phpunit --coverage-clover=coverage.xml diff --git a/app/Services/LocationService.php b/app/Services/LocationService.php index 8db67592..e49304f0 100644 --- a/app/Services/LocationService.php +++ b/app/Services/LocationService.php @@ -64,15 +64,16 @@ class LocationService /** * Update location model in the DB. * - * @param \Pterodactyl\Models\Location $location - * @param array $data + * @param int $id + * @param array $data * @return \Pterodactyl\Models\Location * * @throws \Throwable * @throws \Watson\Validating\ValidationException */ - public function update(Location $location, array $data) + public function update($id, array $data) { + $location = $this->model->findOrFail($id); $location->fill($data)->saveOrFail(); return $location; @@ -81,15 +82,15 @@ class LocationService /** * Delete a model from the DB. * - * @param \Pterodactyl\Models\Location $location + * @param int $id * @return bool - * - * @throws \Exception * @throws \Pterodactyl\Exceptions\DisplayException */ - public function delete(Location $location) + public function delete($id) { - if ($location->nodes()->count() > 0) { + $location = $this->model->withCount('nodes')->findOrFail($id); + + if ($location->nodes_count > 0) { throw new DisplayException('Cannot delete a location that has nodes assigned to it.'); } diff --git a/config/database.php b/config/database.php index 58324a0b..00d44762 100644 --- a/config/database.php +++ b/config/database.php @@ -44,6 +44,19 @@ return [ 'prefix' => '', 'strict' => false, ], + + 'tests' => [ + 'driver' => 'mysql', + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'travis'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => 'utf8', + 'collation' => 'utf8_unicode_ci', + 'prefix' => '', + 'strict' => false, + ], ], /* diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index fe45d9de..ee2adc3e 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -31,3 +31,10 @@ $factory->state(Pterodactyl\Models\User::class, 'admin', function () { 'root_admin' => true, ]; }); + +$factory->define(Pterodactyl\Models\Location::class, function (Faker\Generator $faker) { + return [ + 'short' => $faker->domainWord, + 'long' => $faker->catchPhrase, + ]; +}); diff --git a/phpunit.xml b/phpunit.xml index 9ecda835..ed342074 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -24,8 +24,10 @@ + + diff --git a/tests/Feature/Services/LocationServiceTest.php b/tests/Feature/Services/LocationServiceTest.php new file mode 100644 index 00000000..f57ce147 --- /dev/null +++ b/tests/Feature/Services/LocationServiceTest.php @@ -0,0 +1,204 @@ +. + * + * 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\Feature\Services; + +use Illuminate\Validation\ValidationException; +use Tests\TestCase; +use Pterodactyl\Models\Location; +use Pterodactyl\Services\LocationService; + +class LocationServiceTest extends TestCase +{ + /** + * @var \Pterodactyl\Services\LocationService + */ + protected $service; + + /** + * Setup the test instance. + */ + public function setUp() + { + parent::setUp(); + + $this->service = $this->app->make(LocationService::class); + } + + /** + * Test that a new location can be successfully added to the database. + */ + public function testShouldCreateANewLocation() + { + $data = [ + 'long' => 'Long Name', + 'short' => 'short', + ]; + + $response = $this->service->create($data); + + $this->assertInstanceOf(Location::class, $response); + $this->assertEquals($data['long'], $response->long); + $this->assertEquals($data['short'], $response->short); + $this->assertDatabaseHas('locations', [ + 'short' => $data['short'], + 'long' => $data['long'] + ]); + } + + /** + * Test that a validation error is thrown if a required field is missing. + * + * @expectedException \Watson\Validating\ValidationException + */ + public function testShouldFailToCreateLocationIfMissingParameter() + { + $data = ['long' => 'Long Name']; + + try { + $this->service->create($data); + } catch (\Exception $ex) { + $this->assertInstanceOf(ValidationException::class, $ex); + + $bag = $ex->getMessageBag()->messages(); + $this->assertArraySubset(['short' => [0]], $bag); + $this->assertEquals('The short field is required.', $bag['short'][0]); + + throw $ex; + } + } + + /** + * Test that a validation error is thrown if the short code provided is already in use. + * + * @expectedException \Watson\Validating\ValidationException + */ + public function testShouldFailToCreateLocationIfShortCodeIsAlreadyInUse() + { + factory(Location::class)->create(['short' => 'inuse']); + $data = [ + 'long' => 'Long Name', + 'short' => 'inuse', + ]; + + try { + $this->service->create($data); + } catch (\Exception $ex) { + $this->assertInstanceOf(ValidationException::class, $ex); + + $bag = $ex->getMessageBag()->messages(); + $this->assertArraySubset(['short' => [0]], $bag); + $this->assertEquals('The short has already been taken.', $bag['short'][0]); + + throw $ex; + } + } + + /** + * Test that a validation error is thrown if the short code is too long. + * + * @expectedException \Watson\Validating\ValidationException + */ + public function testShouldFailToCreateLocationIfShortCodeIsTooLong() + { + $data = [ + 'long' => 'Long Name', + 'short' => str_random(200), + ]; + + try { + $this->service->create($data); + } catch (\Exception $ex) { + $this->assertInstanceOf(ValidationException::class, $ex); + + $bag = $ex->getMessageBag()->messages(); + $this->assertArraySubset(['short' => [0]], $bag); + $this->assertEquals('The short must be between 1 and 60 characters.', $bag['short'][0]); + + throw $ex; + } + } + + /** + * Test that updating a model returns the updated data in a persisted form. + */ + public function testShouldUpdateLocationModelInDatabase() + { + $location = factory(Location::class)->create(); + $data = ['short' => 'test_short']; + + $model = $this->service->update($location->id, $data); + + $this->assertInstanceOf(Location::class, $model); + $this->assertEquals($data['short'], $model->short); + $this->assertNotEquals($model->short, $location->short); + $this->assertEquals($location->long, $model->long); + $this->assertDatabaseHas('locations', [ + 'short' => $data['short'], + 'long' => $location->long, + ]); + } + + /** + * Test that passing the same short-code into the update function as the model + * is currently using will not throw a validation exception. + */ + public function testShouldUpdateModelWithoutErrorWhenValidatingShortCodeIsUnique() + { + $location = factory(Location::class)->create(); + $data = ['short' => $location->short]; + + $model = $this->service->update($location->id, $data); + + $this->assertInstanceOf(Location::class, $model); + $this->assertEquals($model->short, $location->short); + + // Timestamps don't change if no data is modified. + $this->assertEquals($model->updated_at, $location->updated_at); + } + + /** + * Test that passing invalid data to the update method will throw a validation + * exception. + * + * @expectedException \Watson\Validating\ValidationException + */ + public function testShouldNotUpdateModelIfPassedDataIsInvalid() + { + $location = factory(Location::class)->create(); + $data = ['short' => str_random(200)]; + + $this->service->update($location->id, $data); + } + + /** + * Test that an invalid model exception is thrown if a model doesn't exist. + * + * @expectedException \Illuminate\Database\Eloquent\ModelNotFoundException + */ + public function testShouldThrowExceptionIfInvalidModelIdIsProvided() + { + $this->service->update(0, []); + } +} diff --git a/tests/Unit/Services/UserServiceTest.php b/tests/Unit/Services/UserServiceTest.php deleted file mode 100644 index c4547469..00000000 --- a/tests/Unit/Services/UserServiceTest.php +++ /dev/null @@ -1,61 +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; - -use Illuminate\Config\Repository; -use Illuminate\Contracts\Auth\Guard; -use Illuminate\Contracts\Hashing\Hasher; -use Illuminate\Database\Connection; -use Illuminate\Database\Eloquent\Model; -use Illuminate\Support\Facades\Queue; -use \Mockery as m; -use Pterodactyl\Models\User; -use Pterodactyl\Services\Components\UuidService; -use Pterodactyl\Services\UserService; -use Tests\TestCase; - -class UserServiceTest extends TestCase -{ - protected $service; - - public function setUp() - { - parent::setUp(); - - $this->config = m::mock(Repository::class); - $this->database = m::mock(Connection::class); - $this->guard = m::mock(Guard::class); - $this->hasher = m::mock(Hasher::class); - $this->uuid = m::mock(UuidService::class); - - $this->service = new UserService( - $this->config, - $this->database, - $this->guard, - $this->hasher, - $this->uuid - );; - } -} From 9292887328da2bbc3a31961f2527eaa9ff198218 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 15 Jun 2017 23:07:39 -0500 Subject: [PATCH 07/99] TravisCI Fixes --- .travis.yml | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index 47148c7b..786ea17c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,24 +1,17 @@ -langauge: php - +language: php dist: trusty - php: - - 7.0 - - 7.1 - - 7.2 - + - '7.0' + - '7.1' + - nightly sudo: required - cache: directories: - $HOME/.composer/cache - services: - mysql - before_install: - - mysql -e 'CREATE DATABASE travis;' - + - mysql -e 'CREATE DATABASE IF NOT EXISTS travis;' before_script: - phpenv config-rm xdebug.ini - cp .env.travis .env @@ -27,6 +20,7 @@ before_script: - php artisan key:generate --force - php artisan migrate --force - php artisan db:seed --force - script: - vendor/bin/phpunit --coverage-clover=coverage.xml +notifications: + email: false From a2fe871217780c72b3847dbb875212bd16f5c604 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 15 Jun 2017 23:23:14 -0500 Subject: [PATCH 08/99] Update composer lock, update travis to skip post-install scripts for composer --- .travis.yml | 4 +- composer.lock | 504 +++++++++++++++----------------------------------- 2 files changed, 156 insertions(+), 352 deletions(-) diff --git a/.travis.yml b/.travis.yml index 786ea17c..05786ccb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ before_script: - phpenv config-rm xdebug.ini - cp .env.travis .env - composer self-update - - composer install --no-interaction + - composer install --no-interaction --no-scripts - php artisan key:generate --force - php artisan migrate --force - php artisan db:seed --force @@ -24,3 +24,5 @@ script: - vendor/bin/phpunit --coverage-clover=coverage.xml notifications: email: false +after_success: + - codecov diff --git a/composer.lock b/composer.lock index a8466d76..9f8d616d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "3a539370a2c653dbe460cad3d03c3db5", - "content-hash": "ad3015e0fe97ab992635581a6c72fddd", + "hash": "d08f2ba04e5528d9068da1d4277af151", + "content-hash": "a8eaa6be0153dea7558dc1ca59dd7195", "packages": [ { "name": "aws/aws-sdk-php", @@ -2333,16 +2333,16 @@ }, { "name": "psy/psysh", - "version": "v0.8.3", + "version": "v0.8.6", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "1dd4bbbc64d71e7ec075ffe82b42d9e096dc8d5e" + "reference": "7028d6d525fb183d50b249b7c07598e3d386b27d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/1dd4bbbc64d71e7ec075ffe82b42d9e096dc8d5e", - "reference": "1dd4bbbc64d71e7ec075ffe82b42d9e096dc8d5e", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/7028d6d525fb183d50b249b7c07598e3d386b27d", + "reference": "7028d6d525fb183d50b249b7c07598e3d386b27d", "shasum": "" }, "require": { @@ -2372,7 +2372,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-develop": "0.9.x-dev" + "dev-develop": "0.8.x-dev" } }, "autoload": { @@ -2402,7 +2402,7 @@ "interactive", "shell" ], - "time": "2017-03-19 21:40:44" + "time": "2017-06-04 10:34:20" }, { "name": "ramsey/uuid", @@ -2538,16 +2538,16 @@ }, { "name": "spatie/fractalistic", - "version": "2.0.0", + "version": "2.2.0", "source": { "type": "git", "url": "https://github.com/spatie/fractalistic.git", - "reference": "313faddbbe415d720fdfa0849484726e5c6bb31e" + "reference": "8f00c666a8b8dfb06f79286f97255e6ab1c89639" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/fractalistic/zipball/313faddbbe415d720fdfa0849484726e5c6bb31e", - "reference": "313faddbbe415d720fdfa0849484726e5c6bb31e", + "url": "https://api.github.com/repos/spatie/fractalistic/zipball/8f00c666a8b8dfb06f79286f97255e6ab1c89639", + "reference": "8f00c666a8b8dfb06f79286f97255e6ab1c89639", "shasum": "" }, "require": { @@ -2585,7 +2585,7 @@ "spatie", "transform" ], - "time": "2017-04-26 15:03:58" + "time": "2017-05-29 14:16:20" }, { "name": "spatie/laravel-fractal", @@ -2647,16 +2647,16 @@ }, { "name": "swiftmailer/swiftmailer", - "version": "v5.4.7", + "version": "v5.4.8", "source": { "type": "git", "url": "https://github.com/swiftmailer/swiftmailer.git", - "reference": "56db4ed32a6d5c9824c3ecc1d2e538f663f47eb4" + "reference": "9a06dc570a0367850280eefd3f1dc2da45aef517" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/56db4ed32a6d5c9824c3ecc1d2e538f663f47eb4", - "reference": "56db4ed32a6d5c9824c3ecc1d2e538f663f47eb4", + "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/9a06dc570a0367850280eefd3f1dc2da45aef517", + "reference": "9a06dc570a0367850280eefd3f1dc2da45aef517", "shasum": "" }, "require": { @@ -2697,20 +2697,20 @@ "mail", "mailer" ], - "time": "2017-04-20 17:32:18" + "time": "2017-05-01 15:54:03" }, { "name": "symfony/console", - "version": "v3.3.0", + "version": "v3.3.2", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "c80e63f3f5e3a331bfc25e6e9332b10422eb9b05" + "reference": "70d2a29b2911cbdc91a7e268046c395278238b2e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/c80e63f3f5e3a331bfc25e6e9332b10422eb9b05", - "reference": "c80e63f3f5e3a331bfc25e6e9332b10422eb9b05", + "url": "https://api.github.com/repos/symfony/console/zipball/70d2a29b2911cbdc91a7e268046c395278238b2e", + "reference": "70d2a29b2911cbdc91a7e268046c395278238b2e", "shasum": "" }, "require": { @@ -2723,6 +2723,7 @@ }, "require-dev": { "psr/log": "~1.0", + "symfony/config": "~3.3", "symfony/dependency-injection": "~3.3", "symfony/event-dispatcher": "~2.8|~3.0", "symfony/filesystem": "~2.8|~3.0", @@ -2765,20 +2766,20 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2017-05-28 14:08:56" + "time": "2017-06-02 19:24:58" }, { "name": "symfony/css-selector", - "version": "v3.2.8", + "version": "v3.3.2", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "02983c144038e697c959e6b06ef6666de759ccbc" + "reference": "4d882dced7b995d5274293039370148e291808f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/02983c144038e697c959e6b06ef6666de759ccbc", - "reference": "02983c144038e697c959e6b06ef6666de759ccbc", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/4d882dced7b995d5274293039370148e291808f2", + "reference": "4d882dced7b995d5274293039370148e291808f2", "shasum": "" }, "require": { @@ -2787,7 +2788,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -2818,20 +2819,20 @@ ], "description": "Symfony CssSelector Component", "homepage": "https://symfony.com", - "time": "2017-05-01 14:55:58" + "time": "2017-05-01 15:01:29" }, { "name": "symfony/debug", - "version": "v3.3.0", + "version": "v3.3.2", "source": { "type": "git", "url": "https://github.com/symfony/debug.git", - "reference": "ef5f19a7a68075a0bd05969a329ead3b0776fb7a" + "reference": "e9c50482841ef696e8fa1470d950a79c8921f45d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/ef5f19a7a68075a0bd05969a329ead3b0776fb7a", - "reference": "ef5f19a7a68075a0bd05969a329ead3b0776fb7a", + "url": "https://api.github.com/repos/symfony/debug/zipball/e9c50482841ef696e8fa1470d950a79c8921f45d", + "reference": "e9c50482841ef696e8fa1470d950a79c8921f45d", "shasum": "" }, "require": { @@ -2874,29 +2875,32 @@ ], "description": "Symfony Debug Component", "homepage": "https://symfony.com", - "time": "2017-05-27 16:02:27" + "time": "2017-06-01 21:01:25" }, { "name": "symfony/event-dispatcher", - "version": "v3.2.8", + "version": "v3.3.2", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "b8a401f733b43251e1d088c589368b2a94155e40" + "reference": "4054a102470665451108f9b59305c79176ef98f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b8a401f733b43251e1d088c589368b2a94155e40", - "reference": "b8a401f733b43251e1d088c589368b2a94155e40", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/4054a102470665451108f9b59305c79176ef98f0", + "reference": "4054a102470665451108f9b59305c79176ef98f0", "shasum": "" }, "require": { "php": ">=5.5.9" }, + "conflict": { + "symfony/dependency-injection": "<3.3" + }, "require-dev": { "psr/log": "~1.0", "symfony/config": "~2.8|~3.0", - "symfony/dependency-injection": "~2.8|~3.0", + "symfony/dependency-injection": "~3.3", "symfony/expression-language": "~2.8|~3.0", "symfony/stopwatch": "~2.8|~3.0" }, @@ -2907,7 +2911,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -2934,20 +2938,20 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2017-05-01 14:58:48" + "time": "2017-06-04 18:15:29" }, { "name": "symfony/finder", - "version": "v3.2.8", + "version": "v3.3.2", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "9cf076f8f492f4b1ffac40aae9c2d287b4ca6930" + "reference": "baea7f66d30854ad32988c11a09d7ffd485810c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/9cf076f8f492f4b1ffac40aae9c2d287b4ca6930", - "reference": "9cf076f8f492f4b1ffac40aae9c2d287b4ca6930", + "url": "https://api.github.com/repos/symfony/finder/zipball/baea7f66d30854ad32988c11a09d7ffd485810c4", + "reference": "baea7f66d30854ad32988c11a09d7ffd485810c4", "shasum": "" }, "require": { @@ -2956,7 +2960,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -2983,20 +2987,20 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2017-04-12 14:13:17" + "time": "2017-06-01 21:01:25" }, { "name": "symfony/http-foundation", - "version": "v3.2.8", + "version": "v3.3.2", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "9de6add7f731e5af7f5b2e9c0da365e43383ebef" + "reference": "80eb5a1f968448b77da9e8b2c0827f6e8d767846" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/9de6add7f731e5af7f5b2e9c0da365e43383ebef", - "reference": "9de6add7f731e5af7f5b2e9c0da365e43383ebef", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/80eb5a1f968448b77da9e8b2c0827f6e8d767846", + "reference": "80eb5a1f968448b77da9e8b2c0827f6e8d767846", "shasum": "" }, "require": { @@ -3009,7 +3013,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -3036,20 +3040,20 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2017-05-01 14:55:58" + "time": "2017-06-05 13:06:51" }, { "name": "symfony/http-kernel", - "version": "v3.2.8", + "version": "v3.3.2", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "46e8b209abab55c072c47d72d5cd1d62c0585e05" + "reference": "be8280f7fa8e95b86514f1e1be997668a53b2888" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/46e8b209abab55c072c47d72d5cd1d62c0585e05", - "reference": "46e8b209abab55c072c47d72d5cd1d62c0585e05", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/be8280f7fa8e95b86514f1e1be997668a53b2888", + "reference": "be8280f7fa8e95b86514f1e1be997668a53b2888", "shasum": "" }, "require": { @@ -3057,18 +3061,22 @@ "psr/log": "~1.0", "symfony/debug": "~2.8|~3.0", "symfony/event-dispatcher": "~2.8|~3.0", - "symfony/http-foundation": "~2.8.13|~3.1.6|~3.2" + "symfony/http-foundation": "~3.3" }, "conflict": { - "symfony/config": "<2.8" + "symfony/config": "<2.8", + "symfony/dependency-injection": "<3.3", + "symfony/var-dumper": "<3.3", + "twig/twig": "<1.34|<2.4,>=2" }, "require-dev": { + "psr/cache": "~1.0", "symfony/browser-kit": "~2.8|~3.0", "symfony/class-loader": "~2.8|~3.0", "symfony/config": "~2.8|~3.0", "symfony/console": "~2.8|~3.0", "symfony/css-selector": "~2.8|~3.0", - "symfony/dependency-injection": "~2.8|~3.0", + "symfony/dependency-injection": "~3.3", "symfony/dom-crawler": "~2.8|~3.0", "symfony/expression-language": "~2.8|~3.0", "symfony/finder": "~2.8|~3.0", @@ -3077,7 +3085,7 @@ "symfony/stopwatch": "~2.8|~3.0", "symfony/templating": "~2.8|~3.0", "symfony/translation": "~2.8|~3.0", - "symfony/var-dumper": "~3.2" + "symfony/var-dumper": "~3.3" }, "suggest": { "symfony/browser-kit": "", @@ -3091,7 +3099,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -3118,20 +3126,20 @@ ], "description": "Symfony HttpKernel Component", "homepage": "https://symfony.com", - "time": "2017-05-01 17:46:48" + "time": "2017-06-06 03:59:58" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.3.0", + "version": "v1.4.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "e79d363049d1c2128f133a2667e4f4190904f7f4" + "reference": "f29dca382a6485c3cbe6379f0c61230167681937" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/e79d363049d1c2128f133a2667e4f4190904f7f4", - "reference": "e79d363049d1c2128f133a2667e4f4190904f7f4", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/f29dca382a6485c3cbe6379f0c61230167681937", + "reference": "f29dca382a6485c3cbe6379f0c61230167681937", "shasum": "" }, "require": { @@ -3143,7 +3151,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.3-dev" + "dev-master": "1.4-dev" } }, "autoload": { @@ -3177,20 +3185,20 @@ "portable", "shim" ], - "time": "2016-11-14 01:06:16" + "time": "2017-06-09 14:24:12" }, { "name": "symfony/polyfill-php56", - "version": "v1.3.0", + "version": "v1.4.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php56.git", - "reference": "1dd42b9b89556f18092f3d1ada22cb05ac85383c" + "reference": "bc0b7d6cb36b10cfabb170a3e359944a95174929" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php56/zipball/1dd42b9b89556f18092f3d1ada22cb05ac85383c", - "reference": "1dd42b9b89556f18092f3d1ada22cb05ac85383c", + "url": "https://api.github.com/repos/symfony/polyfill-php56/zipball/bc0b7d6cb36b10cfabb170a3e359944a95174929", + "reference": "bc0b7d6cb36b10cfabb170a3e359944a95174929", "shasum": "" }, "require": { @@ -3200,7 +3208,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.3-dev" + "dev-master": "1.4-dev" } }, "autoload": { @@ -3233,20 +3241,20 @@ "portable", "shim" ], - "time": "2016-11-14 01:06:16" + "time": "2017-06-09 08:25:21" }, { "name": "symfony/polyfill-util", - "version": "v1.3.0", + "version": "v1.4.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-util.git", - "reference": "746bce0fca664ac0a575e465f65c6643faddf7fb" + "reference": "ebccbde4aad410f6438d86d7d261c6b4d2b9a51d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-util/zipball/746bce0fca664ac0a575e465f65c6643faddf7fb", - "reference": "746bce0fca664ac0a575e465f65c6643faddf7fb", + "url": "https://api.github.com/repos/symfony/polyfill-util/zipball/ebccbde4aad410f6438d86d7d261c6b4d2b9a51d", + "reference": "ebccbde4aad410f6438d86d7d261c6b4d2b9a51d", "shasum": "" }, "require": { @@ -3255,7 +3263,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.3-dev" + "dev-master": "1.4-dev" } }, "autoload": { @@ -3285,20 +3293,20 @@ "polyfill", "shim" ], - "time": "2016-11-14 01:06:16" + "time": "2017-06-09 08:25:21" }, { "name": "symfony/process", - "version": "v3.2.8", + "version": "v3.3.2", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "999c2cf5061e627e6cd551dc9ebf90dd1d11d9f0" + "reference": "8e30690c67aafb6c7992d6d8eb0d707807dd3eaf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/999c2cf5061e627e6cd551dc9ebf90dd1d11d9f0", - "reference": "999c2cf5061e627e6cd551dc9ebf90dd1d11d9f0", + "url": "https://api.github.com/repos/symfony/process/zipball/8e30690c67aafb6c7992d6d8eb0d707807dd3eaf", + "reference": "8e30690c67aafb6c7992d6d8eb0d707807dd3eaf", "shasum": "" }, "require": { @@ -3307,7 +3315,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -3334,36 +3342,39 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2017-04-12 14:13:17" + "time": "2017-05-22 12:32:03" }, { "name": "symfony/routing", - "version": "v3.2.8", + "version": "v3.3.2", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "5029745d6d463585e8b487dbc83d6333f408853a" + "reference": "39804eeafea5cca851946e1eed122eb94459fdb4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/5029745d6d463585e8b487dbc83d6333f408853a", - "reference": "5029745d6d463585e8b487dbc83d6333f408853a", + "url": "https://api.github.com/repos/symfony/routing/zipball/39804eeafea5cca851946e1eed122eb94459fdb4", + "reference": "39804eeafea5cca851946e1eed122eb94459fdb4", "shasum": "" }, "require": { "php": ">=5.5.9" }, "conflict": { - "symfony/config": "<2.8" + "symfony/config": "<2.8", + "symfony/dependency-injection": "<3.3", + "symfony/yaml": "<3.3" }, "require-dev": { "doctrine/annotations": "~1.0", "doctrine/common": "~2.2", "psr/log": "~1.0", "symfony/config": "~2.8|~3.0", + "symfony/dependency-injection": "~3.3", "symfony/expression-language": "~2.8|~3.0", "symfony/http-foundation": "~2.8|~3.0", - "symfony/yaml": "~2.8|~3.0" + "symfony/yaml": "~3.3" }, "suggest": { "doctrine/annotations": "For using the annotation loader", @@ -3376,7 +3387,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -3409,20 +3420,20 @@ "uri", "url" ], - "time": "2017-04-12 14:13:17" + "time": "2017-06-02 09:51:43" }, { "name": "symfony/translation", - "version": "v3.2.8", + "version": "v3.3.2", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "f4a04d2df710f81515df576b2de06bdeee518b83" + "reference": "dc3b2a0c6cfff60327ba1c043a82092735397543" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/f4a04d2df710f81515df576b2de06bdeee518b83", - "reference": "f4a04d2df710f81515df576b2de06bdeee518b83", + "url": "https://api.github.com/repos/symfony/translation/zipball/dc3b2a0c6cfff60327ba1c043a82092735397543", + "reference": "dc3b2a0c6cfff60327ba1c043a82092735397543", "shasum": "" }, "require": { @@ -3430,13 +3441,14 @@ "symfony/polyfill-mbstring": "~1.0" }, "conflict": { - "symfony/config": "<2.8" + "symfony/config": "<2.8", + "symfony/yaml": "<3.3" }, "require-dev": { "psr/log": "~1.0", "symfony/config": "~2.8|~3.0", "symfony/intl": "^2.8.18|^3.2.5", - "symfony/yaml": "~2.8|~3.0" + "symfony/yaml": "~3.3" }, "suggest": { "psr/log": "To use logging capability in translator", @@ -3446,7 +3458,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -3473,20 +3485,20 @@ ], "description": "Symfony Translation Component", "homepage": "https://symfony.com", - "time": "2017-04-12 14:13:17" + "time": "2017-05-22 07:42:36" }, { "name": "symfony/var-dumper", - "version": "v3.2.8", + "version": "v3.3.2", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "fa47963ac7979ddbd42b2d646d1b056bddbf7bb8" + "reference": "347c4247a3e40018810b476fcd5dec36d46d08dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/fa47963ac7979ddbd42b2d646d1b056bddbf7bb8", - "reference": "fa47963ac7979ddbd42b2d646d1b056bddbf7bb8", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/347c4247a3e40018810b476fcd5dec36d46d08dc", + "reference": "347c4247a3e40018810b476fcd5dec36d46d08dc", "shasum": "" }, "require": { @@ -3498,7 +3510,7 @@ }, "require-dev": { "ext-iconv": "*", - "twig/twig": "~1.20|~2.0" + "twig/twig": "~1.34|~2.4" }, "suggest": { "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).", @@ -3507,7 +3519,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -3541,7 +3553,7 @@ "debug", "dump" ], - "time": "2017-05-01 14:55:58" + "time": "2017-06-02 09:10:29" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -4076,45 +4088,6 @@ ], "time": "2016-04-29 12:21:54" }, - { - "name": "gecko-packages/gecko-php-unit", - "version": "v2.0", - "source": { - "type": "git", - "url": "https://github.com/GeckoPackages/GeckoPHPUnit.git", - "reference": "40a697ec261f3526e8196363b481b24383740c13" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/GeckoPackages/GeckoPHPUnit/zipball/40a697ec261f3526e8196363b481b24383740c13", - "reference": "40a697ec261f3526e8196363b481b24383740c13", - "shasum": "" - }, - "require": { - "php": "^5.3.6 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "4.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "GeckoPackages\\PHPUnit\\": "src\\PHPUnit" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Additional PHPUnit tests.", - "homepage": "https://github.com/GeckoPackages", - "keywords": [ - "extension", - "filesystem", - "phpunit" - ], - "time": "2016-11-22 11:01:27" - }, { "name": "hamcrest/hamcrest-php", "version": "v1.2.2", @@ -4727,16 +4700,16 @@ }, { "name": "phpunit/phpunit", - "version": "5.7.19", + "version": "5.7.20", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "69c4f49ff376af2692bad9cebd883d17ebaa98a1" + "reference": "3cb94a5f8c07a03c8b7527ed7468a2926203f58b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/69c4f49ff376af2692bad9cebd883d17ebaa98a1", - "reference": "69c4f49ff376af2692bad9cebd883d17ebaa98a1", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3cb94a5f8c07a03c8b7527ed7468a2926203f58b", + "reference": "3cb94a5f8c07a03c8b7527ed7468a2926203f58b", "shasum": "" }, "require": { @@ -4754,7 +4727,7 @@ "phpunit/php-timer": "^1.0.6", "phpunit/phpunit-mock-objects": "^3.2", "sebastian/comparator": "^1.2.4", - "sebastian/diff": "~1.2", + "sebastian/diff": "^1.4.3", "sebastian/environment": "^1.3.4 || ^2.0", "sebastian/exporter": "~2.0", "sebastian/global-state": "^1.1", @@ -4805,7 +4778,7 @@ "testing", "xunit" ], - "time": "2017-04-03 02:22:27" + "time": "2017-05-22 07:42:55" }, { "name": "phpunit/phpunit-mock-objects", @@ -4977,23 +4950,23 @@ }, { "name": "sebastian/diff", - "version": "1.4.1", + "version": "1.4.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e" + "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/13edfd8706462032c2f52b4b862974dd46b71c9e", - "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7f066a26a962dbe58ddea9f72a4e82874a3975a4", + "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": "^5.3.3 || ^7.0" }, "require-dev": { - "phpunit/phpunit": "~4.8" + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" }, "type": "library", "extra": { @@ -5025,7 +4998,7 @@ "keywords": [ "diff" ], - "time": "2015-12-08 07:14:41" + "time": "2017-05-22 07:24:03" }, { "name": "sebastian/environment", @@ -5491,16 +5464,16 @@ }, { "name": "symfony/class-loader", - "version": "v3.2.8", + "version": "v3.3.2", "source": { "type": "git", "url": "https://github.com/symfony/class-loader.git", - "reference": "fc4c04bfd17130a9dccfded9578353f311967da7" + "reference": "386a294d621576302e7cc36965d6ed53b8c73c4f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/class-loader/zipball/fc4c04bfd17130a9dccfded9578353f311967da7", - "reference": "fc4c04bfd17130a9dccfded9578353f311967da7", + "url": "https://api.github.com/repos/symfony/class-loader/zipball/386a294d621576302e7cc36965d6ed53b8c73c4f", + "reference": "386a294d621576302e7cc36965d6ed53b8c73c4f", "shasum": "" }, "require": { @@ -5516,7 +5489,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -5543,20 +5516,20 @@ ], "description": "Symfony ClassLoader Component", "homepage": "https://symfony.com", - "time": "2017-04-12 14:13:17" + "time": "2017-06-02 09:51:43" }, { "name": "symfony/config", - "version": "v3.3.0", + "version": "v3.3.2", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "79f86253ba482ca7f17718e886e6d164e5ba6d45" + "reference": "35716d4904e0506a7a5a9bcf23f854aeb5719bca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/79f86253ba482ca7f17718e886e6d164e5ba6d45", - "reference": "79f86253ba482ca7f17718e886e6d164e5ba6d45", + "url": "https://api.github.com/repos/symfony/config/zipball/35716d4904e0506a7a5a9bcf23f854aeb5719bca", + "reference": "35716d4904e0506a7a5a9bcf23f854aeb5719bca", "shasum": "" }, "require": { @@ -5603,11 +5576,11 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2017-05-29 18:41:32" + "time": "2017-06-02 18:07:20" }, { "name": "symfony/filesystem", - "version": "v3.3.0", + "version": "v3.3.2", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", @@ -5654,180 +5627,9 @@ "homepage": "https://symfony.com", "time": "2017-05-28 14:08:56" }, - { - "name": "symfony/options-resolver", - "version": "v3.3.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/options-resolver.git", - "reference": "ff48982d295bcac1fd861f934f041ebc73ae40f0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/ff48982d295bcac1fd861f934f041ebc73ae40f0", - "reference": "ff48982d295bcac1fd861f934f041ebc73ae40f0", - "shasum": "" - }, - "require": { - "php": ">=5.5.9" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.3-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\OptionsResolver\\": "" - }, - "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 OptionsResolver Component", - "homepage": "https://symfony.com", - "keywords": [ - "config", - "configuration", - "options" - ], - "time": "2017-04-12 14:14:56" - }, - { - "name": "symfony/polyfill-php70", - "version": "v1.3.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php70.git", - "reference": "13ce343935f0f91ca89605a2f6ca6f5c2f3faac2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/13ce343935f0f91ca89605a2f6ca6f5c2f3faac2", - "reference": "13ce343935f0f91ca89605a2f6ca6f5c2f3faac2", - "shasum": "" - }, - "require": { - "paragonie/random_compat": "~1.0|~2.0", - "php": ">=5.3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php70\\": "" - }, - "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 backporting some PHP 7.0+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "time": "2016-11-14 01:06:16" - }, - { - "name": "symfony/polyfill-xml", - "version": "v1.3.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-xml.git", - "reference": "64b6a864f18ab4fddad49f5025f805f6781dfabd" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-xml/zipball/64b6a864f18ab4fddad49f5025f805f6781dfabd", - "reference": "64b6a864f18ab4fddad49f5025f805f6781dfabd", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "suggest": { - "ext-xml": "For best performance" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Xml\\": "" - }, - "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 xml's utf8_encode and utf8_decode functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "time": "2016-11-14 01:06:16" - }, { "name": "symfony/stopwatch", - "version": "v3.3.0", + "version": "v3.3.2", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", @@ -5876,16 +5678,16 @@ }, { "name": "symfony/yaml", - "version": "v3.3.0", + "version": "v3.3.2", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "885db865f6b2b918404a1fae28f9ac640f71f994" + "reference": "9752a30000a8ca9f4b34b5227d15d0101b96b063" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/885db865f6b2b918404a1fae28f9ac640f71f994", - "reference": "885db865f6b2b918404a1fae28f9ac640f71f994", + "url": "https://api.github.com/repos/symfony/yaml/zipball/9752a30000a8ca9f4b34b5227d15d0101b96b063", + "reference": "9752a30000a8ca9f4b34b5227d15d0101b96b063", "shasum": "" }, "require": { @@ -5927,7 +5729,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2017-05-28 10:56:20" + "time": "2017-06-02 22:05:06" }, { "name": "webmozart/assert", From 579cc86910ce19d4fdd5cc4e8735fedc5744d849 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Thu, 15 Jun 2017 23:29:19 -0500 Subject: [PATCH 09/99] Try to fix Travis CI failures --- .travis.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 05786ccb..d4669f04 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,11 +15,9 @@ before_install: before_script: - phpenv config-rm xdebug.ini - cp .env.travis .env - - composer self-update - - composer install --no-interaction --no-scripts - - php artisan key:generate --force - - php artisan migrate --force - - php artisan db:seed --force + - composer install --no-interaction --no-scripts --prefer-dist --no-suggest + - php artisan migrate --force -v + - php artisan db:seed --force -v script: - vendor/bin/phpunit --coverage-clover=coverage.xml notifications: From a527949939fd2eda768f58a649de76846271fe73 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Fri, 16 Jun 2017 00:29:19 -0500 Subject: [PATCH 10/99] Add more location tests, more travis CI fix attempts --- .env.travis | 9 ++-- .travis.yml | 9 ++-- config/database.php | 10 ++--- database/factories/ModelFactory.php | 19 ++++++++ .../Feature/Services/LocationServiceTest.php | 44 +++++++++++++++++++ 5 files changed, 77 insertions(+), 14 deletions(-) diff --git a/.env.travis b/.env.travis index c3bd0801..cd94ca8e 100644 --- a/.env.travis +++ b/.env.travis @@ -1,10 +1,11 @@ APP_ENV=testing APP_DEBUG=true -APP_KEY=SomeRandomString32SomeRandomString32 +APP_KEY=aaaaabbbbbcccccdddddeeeeefffff12 -DB_CONNECTION=tests -DB_TEST_USERNAME=root -DB_TEST_PASSWORD= +TEST_DB_CONNECTION=tests +TEST_DB_TEST_USERNAME=root +TEST_DB_TEST_PASSWORD= +TEST_DB_HOST=127.0.0.1 CACHE_DRIVER=array SESSION_DRIVER=array diff --git a/.travis.yml b/.travis.yml index d4669f04..5fbbb113 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,8 +3,7 @@ dist: trusty php: - '7.0' - '7.1' - - nightly -sudo: required +sudo: false cache: directories: - $HOME/.composer/cache @@ -15,9 +14,9 @@ before_install: before_script: - phpenv config-rm xdebug.ini - cp .env.travis .env - - composer install --no-interaction --no-scripts --prefer-dist --no-suggest - - php artisan migrate --force -v - - php artisan db:seed --force -v + - composer install --no-interaction --prefer-dist --no-suggest --verbose + - php artisan migrate -v + - php artisan db:seed -v script: - vendor/bin/phpunit --coverage-clover=coverage.xml notifications: diff --git a/config/database.php b/config/database.php index 00d44762..01fa1623 100644 --- a/config/database.php +++ b/config/database.php @@ -47,11 +47,11 @@ return [ 'tests' => [ 'driver' => 'mysql', - 'host' => env('DB_HOST', 'localhost'), - 'port' => env('DB_PORT', '3306'), - 'database' => env('DB_DATABASE', 'travis'), - 'username' => env('DB_USERNAME', 'root'), - 'password' => env('DB_PASSWORD', ''), + 'host' => env('TEST_DB_HOST', 'localhost'), + 'port' => env('TEST_DB_PORT', '3306'), + 'database' => env('TEST_DB_DATABASE', 'travis'), + 'username' => env('TEST_DB_USERNAME', 'root'), + 'password' => env('TEST_DB_PASSWORD', ''), 'charset' => 'utf8', 'collation' => 'utf8_unicode_ci', 'prefix' => '', diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index ee2adc3e..be96cd02 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -38,3 +38,22 @@ $factory->define(Pterodactyl\Models\Location::class, function (Faker\Generator $ 'long' => $faker->catchPhrase, ]; }); + +$factory->define(Pterodactyl\Models\Node::class, function (Faker\Generator $faker) { + return [ + 'public' => true, + 'name' => $faker->firstName, + 'fqdn' => $faker->ipv4, + 'scheme' => 'http', + 'behind_proxy' => false, + 'memory' => 1024, + 'memory_overallocate' => 0, + 'disk' => 10240, + 'disk_overallocate' => 0, + 'upload_size' => 100, + 'daemonSecret' => $faker->uuid, + 'daemonListen' => 8080, + 'daemonSFTP' => 2022, + 'daemonBase' => '/srv/daemon', + ]; +}); diff --git a/tests/Feature/Services/LocationServiceTest.php b/tests/Feature/Services/LocationServiceTest.php index f57ce147..fbf8f8f1 100644 --- a/tests/Feature/Services/LocationServiceTest.php +++ b/tests/Feature/Services/LocationServiceTest.php @@ -25,6 +25,8 @@ namespace Tests\Feature\Services; use Illuminate\Validation\ValidationException; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Models\Node; use Tests\TestCase; use Pterodactyl\Models\Location; use Pterodactyl\Services\LocationService; @@ -201,4 +203,46 @@ class LocationServiceTest extends TestCase { $this->service->update(0, []); } + + /** + * Test that a location can be deleted normally when no nodes are attached. + */ + public function testShouldDeleteExistingLocation() + { + $location = factory(Location::class)->create(); + + $this->assertDatabaseHas('locations', [ + 'id' => $location->id, + ]); + + $model = $this->service->delete($location); + + $this->assertTrue($model); + $this->assertDatabaseMissing('locations', [ + 'id' => $location->id, + ]); + } + + /** + * Test that a location cannot be deleted if a node is attached to it. + * + * @expectedException \Pterodactyl\Exceptions\DisplayException + */ + public function testShouldFailToDeleteExistingLocationWithAttachedNodes() + { + $location = factory(Location::class)->create(); + $node = factory(Node::class)->create(['location_id' => $location->id]); + + $this->assertDatabaseHas('locations', ['id' => $location->id]); + $this->assertDatabaseHas('nodes', ['id' => $node->id]); + + try { + $this->service->delete($location->id); + } catch (\Exception $ex) { + $this->assertInstanceOf(DisplayException::class, $ex); + $this->assertNotEmpty($ex->getMessage()); + + throw $ex; + } + } } From 8ea907e97ac9bed44fa13ea8a95b5ce91f4d5dab Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Fri, 16 Jun 2017 00:50:10 -0500 Subject: [PATCH 11/99] Include code coverage --- .travis.yml | 4 +- coverage.xml | 5812 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 5814 insertions(+), 2 deletions(-) create mode 100644 coverage.xml diff --git a/.travis.yml b/.travis.yml index 5fbbb113..a5635548 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,11 +14,11 @@ before_install: before_script: - phpenv config-rm xdebug.ini - cp .env.travis .env - - composer install --no-interaction --prefer-dist --no-suggest --verbose + - composer install --no-interaction --prefer-dist --no-suggest --no-scripts --verbose - php artisan migrate -v - php artisan db:seed -v script: - - vendor/bin/phpunit --coverage-clover=coverage.xml + - vendor/bin/phpunit --coverage-clover coverage.xml notifications: email: false after_success: diff --git a/coverage.xml b/coverage.xml new file mode 100644 index 00000000..e392d529 --- /dev/null +++ b/coverage.xml @@ -0,0 +1,5812 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From cede747442a010f6bd25e50d6d6d376235df11d2 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 17 Jun 2017 17:36:39 -0500 Subject: [PATCH 12/99] Cleanup user and location controllers. --- .../Controllers/Admin/LocationController.php | 23 ++++++------ app/Http/Controllers/Admin/UserController.php | 36 ++++++++++++------- config/pterodactyl.php | 3 ++ 3 files changed, 39 insertions(+), 23 deletions(-) diff --git a/app/Http/Controllers/Admin/LocationController.php b/app/Http/Controllers/Admin/LocationController.php index 8325405f..1dd3bee2 100644 --- a/app/Http/Controllers/Admin/LocationController.php +++ b/app/Http/Controllers/Admin/LocationController.php @@ -41,7 +41,7 @@ class LocationController extends Controller /** * @var \Pterodactyl\Models\Location */ - protected $location; + protected $locationModel; /** * @var \Pterodactyl\Services\LocationService @@ -52,13 +52,16 @@ class LocationController extends Controller * LocationController constructor. * * @param \Prologue\Alerts\AlertsMessageBag $alert - * @param \Pterodactyl\Models\Location $location - * @param \Pterodactyl\Services\LocationService $service + * @param \Pterodactyl\Models\Location $locationModel + * @param \Pterodactyl\Services\LocationService $service */ - public function __construct(AlertsMessageBag $alert, Location $location, LocationService $service) - { + public function __construct( + AlertsMessageBag $alert, + Location $locationModel, + LocationService $service + ) { $this->alert = $alert; - $this->location = $location; + $this->locationModel = $locationModel; $this->service = $service; } @@ -70,7 +73,7 @@ class LocationController extends Controller public function index() { return view('admin.locations.index', [ - 'locations' => $this->location->withCount('nodes', 'servers')->get(), + 'locations' => $this->locationModel->withCount('nodes', 'servers')->get(), ]); } @@ -120,7 +123,7 @@ class LocationController extends Controller return $this->delete($location); } - $this->service->update($location, $request->normalize()); + $this->service->update($location->id, $request->normalize()); $this->alert->success('Location was updated successfully.')->flash(); return redirect()->route('admin.locations.view', $location->id); @@ -129,7 +132,7 @@ class LocationController extends Controller /** * Delete a location from the system. * - * @param \Pterodactyl\Models\Location $location + * @param \Pterodactyl\Models\Location $location * @return \Illuminate\Http\RedirectResponse * * @throws \Exception @@ -138,7 +141,7 @@ class LocationController extends Controller public function delete(Location $location) { try { - $this->service->delete($location); + $this->service->delete($location->id); return redirect()->route('admin.locations'); } catch (DisplayException $ex) { diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index 1f1bbd56..2e9a2541 100644 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -1,8 +1,7 @@ - * Some Modifications (c) 2015 Dylan Seidt . + * Copyright (c) 2015 - 2017 Dane Everitt . * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -25,32 +24,43 @@ namespace Pterodactyl\Http\Controllers\Admin; -use Alert; use Illuminate\Http\Request; -use Prologue\Alerts\AlertsMessageBag; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Http\Requests\Admin\UserFormRequest; use Pterodactyl\Models\User; -use Pterodactyl\Http\Controllers\Controller; +use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Services\UserService; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Http\Controllers\Controller; +use Pterodactyl\Http\Requests\Admin\UserFormRequest; class UserController extends Controller { + /** + * @var \Prologue\Alerts\AlertsMessageBag + */ + protected $alert; + /** * @var \Pterodactyl\Services\UserService */ protected $service; + /** + * @var \Pterodactyl\Models\User + */ + protected $userModel; + /** * UserController constructor. * * @param \Prologue\Alerts\AlertsMessageBag $alert * @param \Pterodactyl\Services\UserService $service + * @param \Pterodactyl\Models\User $userModel */ - public function __construct(AlertsMessageBag $alert, UserService $service) + public function __construct(AlertsMessageBag $alert, UserService $service, User $userModel) { $this->alert = $alert; $this->service = $service; + $this->userModel = $userModel; } /** @@ -61,14 +71,14 @@ class UserController extends Controller */ public function index(Request $request) { - $users = User::withCount('servers', 'subuserOf'); + $users = $this->userModel->withCount('servers', 'subuserOf'); if (! is_null($request->input('query'))) { $users->search($request->input('query')); } return view('admin.users.index', [ - 'users' => $users->paginate(25), + 'users' => $users->paginate(config('pterodactyl.paginate.admin.users')), ]); } @@ -106,7 +116,7 @@ class UserController extends Controller public function delete(User $user) { try { - $this->service->delete($user); + $this->service->delete($user->id); return redirect()->route('admin.users'); } catch (DisplayException $ex) { @@ -142,7 +152,7 @@ class UserController extends Controller */ public function update(UserFormRequest $request, User $user) { - $this->service->update($user, $request->normalize()); + $this->service->update($user->id, $request->normalize()); $this->alert->success('User account has been updated.')->flash(); return redirect()->route('admin.users.view', $user->id); @@ -156,7 +166,7 @@ class UserController extends Controller */ public function json(Request $request) { - return User::search($request->input('q'))->all([ + return $this->userModel->search($request->input('q'))->all([ 'id', 'email', 'username', 'name_first', 'name_last', ])->transform(function ($item) { $item->md5 = md5(strtolower($item->email)); diff --git a/config/pterodactyl.php b/config/pterodactyl.php index bd10183c..d1e74ae0 100644 --- a/config/pterodactyl.php +++ b/config/pterodactyl.php @@ -39,6 +39,9 @@ return [ 'frontend' => [ 'servers' => env('APP_PAGINATE_FRONT_SERVERS', 15), ], + 'admin' => [ + 'users' => env('APP_PAGINATE_ADMIN_USERS', 25), + ], 'api' => [ 'nodes' => env('APP_PAGINATE_API_NODES', 25), 'servers' => env('APP_PAGINATE_API_SERVERS', 25), From 0111ca7768b8ce1d1dc1b28a8ff8b81f2907a8cf Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 17 Jun 2017 19:48:31 -0500 Subject: [PATCH 13/99] Push more changes to DBHost service. Currently updating via the frontend is broken if you don't provide an actual node to attach it to. --- app/Extensions/DynamicDatabaseConnection.php | 93 +++++++++++ app/Http/Controllers/Admin/BaseController.php | 47 +++--- .../Controllers/Admin/DatabaseController.php | 152 +++++++++++------- app/Http/Requests/Admin/BaseFormRequest.php | 35 ++++ .../Admin/DatabaseHostFormRequest.php | 49 ++++++ app/Models/DatabaseHost.php | 31 ++-- app/Models/Location.php | 2 +- app/Services/DatabaseHostService.php | 151 +++++++++++++++++ .../admin/databases/view.blade.php | 1 + routes/admin.php | 4 +- 10 files changed, 462 insertions(+), 103 deletions(-) create mode 100644 app/Extensions/DynamicDatabaseConnection.php create mode 100644 app/Http/Requests/Admin/BaseFormRequest.php create mode 100644 app/Http/Requests/Admin/DatabaseHostFormRequest.php create mode 100644 app/Services/DatabaseHostService.php diff --git a/app/Extensions/DynamicDatabaseConnection.php b/app/Extensions/DynamicDatabaseConnection.php new file mode 100644 index 00000000..862336f4 --- /dev/null +++ b/app/Extensions/DynamicDatabaseConnection.php @@ -0,0 +1,93 @@ +. + * + * 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\Extensions; + +use Illuminate\Config\Repository as ConfigRepository; +use Illuminate\Contracts\Encryption\Encrypter; +use Pterodactyl\Models\DatabaseHost; + +class DynamicDatabaseConnection +{ + const DB_CHARSET = 'utf8'; + const DB_COLLATION = 'utf8_unicode_ci'; + const DB_DRIVER = 'mysql'; + + /** + * @var \Illuminate\Config\Repository + */ + protected $config; + + /** + * @var \Illuminate\Contracts\Encryption\Encrypter + */ + protected $encrypter; + + /** + * @var \Pterodactyl\Models\DatabaseHost + */ + protected $model; + + /** + * DynamicDatabaseConnection constructor. + * + * @param \Illuminate\Config\Repository $config + * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter + * @param \Pterodactyl\Models\DatabaseHost $model + */ + public function __construct( + ConfigRepository $config, + Encrypter $encrypter, + DatabaseHost $model + ) { + $this->config = $config; + $this->encrypter = $encrypter; + $this->model = $model; + } + + /** + * Adds a dynamic database connection entry to the runtime config. + * + * @param string $connection + * @param \Pterodactyl\Models\DatabaseHost|int $host + * @param string $database + */ + public function set($connection, $host, $database = 'mysql') + { + if (! $host instanceof DatabaseHost) { + $host = $this->model->findOrFail($host); + } + + $this->config->set('database.connections.' . $connection, [ + 'driver' => self::DB_DRIVER, + 'host' => $host->host, + 'port' => $host->port, + 'database' => $database, + 'username' => $host->username, + 'password' => $this->encrypter->decrypt($host->password), + 'charset' => self::DB_CHARSET, + 'collation' => self::DB_COLLATION, + ]); + } +} diff --git a/app/Http/Controllers/Admin/BaseController.php b/app/Http/Controllers/Admin/BaseController.php index 8c571982..d4dd5dc8 100644 --- a/app/Http/Controllers/Admin/BaseController.php +++ b/app/Http/Controllers/Admin/BaseController.php @@ -24,21 +24,35 @@ namespace Pterodactyl\Http\Controllers\Admin; -use Alert; -use Settings; -use Validator; -use Illuminate\Http\Request; +use Krucas\Settings\Settings; +use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Http\Controllers\Controller; +use Pterodactyl\Http\Requests\Admin\BaseFormRequest; class BaseController extends Controller { + /** + * @var \Prologue\Alerts\AlertsMessageBag + */ + protected $alert; + + /** + * @var \Krucas\Settings\Settings + */ + protected $settings; + + public function __construct(AlertsMessageBag $alert, Settings $settings) + { + $this->alert = $alert; + $this->settings = $settings; + } + /** * Return the admin index view. * - * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ - public function getIndex(Request $request) + public function getIndex() { return view('admin.index'); } @@ -46,10 +60,9 @@ class BaseController extends Controller /** * Return the admin settings view. * - * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ - public function getSettings(Request $request) + public function getSettings() { return view('admin.settings'); } @@ -57,24 +70,14 @@ class BaseController extends Controller /** * Handle settings post request. * - * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Http\Requests\Admin\BaseFormRequest $request * @return \Illuminate\Http\RedirectResponse */ - public function postSettings(Request $request) + public function postSettings(BaseFormRequest $request) { - $validator = Validator::make($request->all(), [ - 'company' => 'required|between:1,256', - // 'default_language' => 'required|alpha_dash|min:2|max:5', - ]); + $this->settings->set('company', $request->input('company')); - if ($validator->fails()) { - return redirect()->route('admin.settings')->withErrors($validator->errors())->withInput(); - } - - Settings::set('company', $request->input('company')); - // Settings::set('default_language', $request->input('default_language')); - - Alert::success('Settings have been successfully updated.')->flash(); + $this->alert->success('Settings have been successfully updated.')->flash(); return redirect()->route('admin.settings'); } diff --git a/app/Http/Controllers/Admin/DatabaseController.php b/app/Http/Controllers/Admin/DatabaseController.php index 6b4e12a1..94d60a0c 100644 --- a/app/Http/Controllers/Admin/DatabaseController.php +++ b/app/Http/Controllers/Admin/DatabaseController.php @@ -24,112 +24,144 @@ namespace Pterodactyl\Http\Controllers\Admin; -use Log; -use Alert; -use Illuminate\Http\Request; -use Pterodactyl\Models\Database; use Pterodactyl\Models\Location; use Pterodactyl\Models\DatabaseHost; -use Pterodactyl\Exceptions\DisplayException; +use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Repositories\DatabaseRepository; -use Pterodactyl\Exceptions\DisplayValidationException; +use Pterodactyl\Services\DatabaseHostService; +use Pterodactyl\Http\Requests\Admin\DatabaseHostFormRequest; class DatabaseController extends Controller { + /** + * @var \Prologue\Alerts\AlertsMessageBag + */ + protected $alert; + + /** + * @var \Pterodactyl\Models\DatabaseHost + */ + protected $hostModel; + + /** + * @var \Pterodactyl\Models\Location + */ + protected $locationModel; + + /** + * @var \Pterodactyl\Services\DatabaseHostService + */ + protected $service; + + /** + * DatabaseController constructor. + * + * @param \Prologue\Alerts\AlertsMessageBag $alert + * @param \Pterodactyl\Models\DatabaseHost $hostModel + * @param \Pterodactyl\Models\Location $locationModel + * @param \Pterodactyl\Services\DatabaseHostService $service + */ + public function __construct( + AlertsMessageBag $alert, + DatabaseHost $hostModel, + Location $locationModel, + DatabaseHostService $service + ) { + $this->alert = $alert; + $this->hostModel = $hostModel; + $this->locationModel = $locationModel; + $this->service = $service; + } + /** * Display database host index. * - * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ - public function index(Request $request) + public function index() { return view('admin.databases.index', [ - 'locations' => Location::with('nodes')->get(), - 'hosts' => DatabaseHost::withCount('databases')->with('node')->get(), + 'locations' => $this->locationModel->with('nodes')->get(), + 'hosts' => $this->hostModel->withCount('databases')->with('node')->get(), ]); } /** * Display database host to user. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Pterodactyl\Models\DatabaseHost $host * @return \Illuminate\View\View */ - public function view(Request $request, $id) + public function view(DatabaseHost $host) { + $host->load('databases.server'); + return view('admin.databases.view', [ - 'locations' => Location::with('nodes')->get(), - 'host' => DatabaseHost::with('databases.server')->findOrFail($id), + 'locations' => $this->locationModel->with('nodes')->get(), + 'host' => $host, ]); } /** - * Handle post request to create database host. + * Handle request to create a new database host. * - * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Http\Requests\Admin\DatabaseHostFormRequest $request * @return \Illuminate\Http\RedirectResponse + * + * @throws \Throwable */ - public function create(Request $request) + public function create(DatabaseHostFormRequest $request) { - $repo = new DatabaseRepository; - try { - $host = $repo->add($request->intersect([ - 'name', 'username', 'password', - 'host', 'port', 'node_id', - ])); - Alert::success('Successfully created new database host on the system.')->flash(); + $host = $this->service->create($request->normalize()); + $this->alert->success('Successfully created a new database host on the system.')->flash(); return redirect()->route('admin.databases.view', $host->id); } catch (\PDOException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (DisplayValidationException $ex) { - return redirect()->route('admin.databases')->withErrors(json_decode($ex->getMessage())); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An error was encountered while trying to process this request. This error has been logged.')->flash(); + $this->alert->danger($ex->getMessage())->flash(); } return redirect()->route('admin.databases'); } /** - * Handle post request to update a database host. + * Handle updating database host. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Pterodactyl\Http\Requests\Admin\DatabaseHostFormRequest $request + * @param \Pterodactyl\Models\DatabaseHost $host * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\DisplayException */ - public function update(Request $request, $id) + public function update(DatabaseHostFormRequest $request, DatabaseHost $host) { - $repo = new DatabaseRepository; - - try { - if ($request->input('action') !== 'delete') { - $host = $repo->update($id, $request->intersect([ - 'name', 'username', 'password', - 'host', 'port', 'node_id', - ])); - Alert::success('Database host was updated successfully.')->flash(); - } else { - $repo->delete($id); - - return redirect()->route('admin.databases'); - } - } catch (\PDOException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (DisplayValidationException $ex) { - return redirect()->route('admin.databases.view', $id)->withErrors(json_decode($ex->getMessage())); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An error was encountered while trying to process this request. This error has been logged.')->flash(); + if ($request->input('action') === 'delete') { + return $this->delete($host); } - return redirect()->route('admin.databases.view', $id); + try { + $host = $this->service->update($host->id, $request->normalize()); + $this->alert->success('Database host was updated successfully.')->flash(); + } catch (\PDOException $ex) { + $this->alert->danger($ex->getMessage())->flash(); + } + + return redirect()->route('admin.databases.view', $host->id); + } + + /** + * Handle request to delete a database host. + * + * @param \Pterodactyl\Models\DatabaseHost $host + * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\DisplayException + */ + public function delete(DatabaseHost $host) + { + $this->service->delete($host->id); + $this->alert->success('The requested database host has been deleted from the system.')->flash(); + + return redirect()->route('admin.databases'); } } diff --git a/app/Http/Requests/Admin/BaseFormRequest.php b/app/Http/Requests/Admin/BaseFormRequest.php new file mode 100644 index 00000000..0d988425 --- /dev/null +++ b/app/Http/Requests/Admin/BaseFormRequest.php @@ -0,0 +1,35 @@ +. + * + * 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\Http\Requests\Admin; + +class BaseFormRequest extends AdminFormRequest +{ + public function rules() + { + return [ + 'company' => 'required|between:1,256', + ]; + } +} diff --git a/app/Http/Requests/Admin/DatabaseHostFormRequest.php b/app/Http/Requests/Admin/DatabaseHostFormRequest.php new file mode 100644 index 00000000..42052f5b --- /dev/null +++ b/app/Http/Requests/Admin/DatabaseHostFormRequest.php @@ -0,0 +1,49 @@ +. + * + * 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\Http\Requests\Admin; + +use Pterodactyl\Models\DatabaseHost; + +class DatabaseHostFormRequest extends AdminFormRequest +{ + /** + * @return mixed + */ + public function rules() + { + $this->merge([ + 'node_id' => ($this->input('node_id') < 1) ? null : $this->input('node_id'), + 'host' => gethostbyname($this->input('host')), + ]); + + $rules = app()->make(DatabaseHost::class)->getRules(); + + if ($this->method() === 'PATCH') { + $rules['host'] = $rules['host'] . ',' . $this->route()->parameter('host')->id; + } + + return $rules; + } +} diff --git a/app/Models/DatabaseHost.php b/app/Models/DatabaseHost.php index 165a99c5..a6599cc4 100644 --- a/app/Models/DatabaseHost.php +++ b/app/Models/DatabaseHost.php @@ -24,12 +24,13 @@ namespace Pterodactyl\Models; -use Crypt; -use Config; +use Watson\Validating\ValidatingTrait; use Illuminate\Database\Eloquent\Model; class DatabaseHost extends Model { + use ValidatingTrait; + /** * The table associated with the model. * @@ -65,24 +66,18 @@ class DatabaseHost extends Model ]; /** - * Sets the database connection name with the details of the host. + * Validation rules to assign to this model. * - * @param string $connection - * @return void + * @var array */ - public function setDynamicConnection($connection = 'dynamic') - { - Config::set('database.connections.' . $connection, [ - 'driver' => 'mysql', - 'host' => $this->host, - 'port' => $this->port, - 'database' => 'mysql', - 'username' => $this->username, - 'password' => Crypt::decrypt($this->password), - 'charset' => 'utf8', - 'collation' => 'utf8_unicode_ci', - ]); - } + protected $rules = [ + 'name' => 'required|string|max:255', + 'host' => 'required|ip|unique:database_hosts,host', + 'port' => 'required|numeric|between:1,65535', + 'username' => 'required|string|max:32', + 'password' => 'sometimes|nullable|string', + 'node_id' => 'sometimes|required|nullable|exists:nodes,id', + ]; /** * Gets the node associated with a database host. diff --git a/app/Models/Location.php b/app/Models/Location.php index bb152940..e7a7cd3f 100644 --- a/app/Models/Location.php +++ b/app/Models/Location.php @@ -46,7 +46,7 @@ class Location extends Model protected $guarded = ['id', 'created_at', 'updated_at']; /** - * Validation rules to apply when attempting to save a model to the DB. + * Validation rules to apply to this model. * * @var array */ diff --git a/app/Services/DatabaseHostService.php b/app/Services/DatabaseHostService.php new file mode 100644 index 00000000..ed285020 --- /dev/null +++ b/app/Services/DatabaseHostService.php @@ -0,0 +1,151 @@ +. + * + * 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; + +use Pterodactyl\Models\DatabaseHost; +use Illuminate\Database\DatabaseManager; +use Pterodactyl\Exceptions\DisplayException; +use Illuminate\Contracts\Encryption\Encrypter; +use Pterodactyl\Extensions\DynamicDatabaseConnection; + +class DatabaseHostService +{ + /** + * @var \Illuminate\Database\DatabaseManager + */ + protected $database; + + /** + * @var \Pterodactyl\Extensions\DynamicDatabaseConnection + */ + protected $dynamic; + + /** + * @var \Illuminate\Contracts\Encryption\Encrypter + */ + protected $encrypter; + + /** + * @var \Pterodactyl\Models\DatabaseHost + */ + protected $model; + + /** + * DatabaseHostService constructor. + * + * @param \Illuminate\Database\DatabaseManager $database + * @param \Pterodactyl\Extensions\DynamicDatabaseConnection $dynamic + * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter + * @param \Pterodactyl\Models\DatabaseHost $model + */ + public function __construct( + DatabaseManager $database, + DynamicDatabaseConnection $dynamic, + Encrypter $encrypter, + DatabaseHost $model + ) { + $this->database = $database; + $this->dynamic = $dynamic; + $this->encrypter = $encrypter; + $this->model = $model; + } + + /** + * Create a new database host and persist it to the database. + * + * @param array $data + * @return \Pterodactyl\Models\DatabaseHost + * + * @throws \Throwable + * @throws \PDOException + */ + public function create(array $data) + { + $instance = $this->model->newInstance(); + $instance->password = $this->encrypter->encrypt(array_get($data, 'password')); + + $instance->fill([ + 'name' => array_get($data, 'name'), + 'host' => array_get($data, 'host'), + 'port' => array_get($data, 'port'), + 'username' => array_get($data, 'username'), + 'max_databases' => null, + 'node_id' => array_get($data, 'node_id'), + ]); + + // Check Access + $this->dynamic->set('dynamic', $instance); + $this->database->connection('dynamic')->select('SELECT 1 FROM dual'); + + $instance->saveOrFail(); + + return $instance; + } + + /** + * Update a database host and persist to the database. + * + * @param int $id + * @param array $data + * @return mixed + * + * @throws \PDOException + */ + public function update($id, array $data) + { + $model = $this->model->findOrFail($id); + + if (! empty(array_get($data, 'password'))) { + $model->password = $this->encrypter->encrypt($data['password']); + } + + $model->fill($data); + $this->dynamic->set('dynamic', $model); + $this->database->connection('dynamic')->select('SELECT 1 FROM dual'); + + $model->saveOrFail(); + + return $model; + } + + /** + * Delete a database host if it has no active databases attached to it. + * + * @param int $id + * @return bool|null + * + * @throws \Pterodactyl\Exceptions\DisplayException + */ + public function delete($id) + { + $model = $this->model->withCount('databases')->findOrFail($id); + + if ($model->databases_count > 0) { + throw new DisplayException('Cannot delete a database host that has active databases attached to it.'); + } + + return $model->delete(); + } +} diff --git a/resources/themes/pterodactyl/admin/databases/view.blade.php b/resources/themes/pterodactyl/admin/databases/view.blade.php index ab331921..bade35b4 100644 --- a/resources/themes/pterodactyl/admin/databases/view.blade.php +++ b/resources/themes/pterodactyl/admin/databases/view.blade.php @@ -93,6 +93,7 @@ diff --git a/routes/admin.php b/routes/admin.php index 6d2730ce..5d67b45b 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -49,10 +49,10 @@ Route::group(['prefix' => 'locations'], function () { */ Route::group(['prefix' => 'databases'], function () { Route::get('/', 'DatabaseController@index')->name('admin.databases'); - Route::get('/view/{id}', 'DatabaseController@view')->name('admin.databases.view'); + Route::get('/view/{host}', 'DatabaseController@view')->name('admin.databases.view'); Route::post('/', 'DatabaseController@create'); - Route::post('/view/{id}', 'DatabaseController@update'); + Route::patch('/view/{host}', 'DatabaseController@update'); }); /* From ce2b2447d0a0f132c84c745686055dccdadf93d3 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 17 Jun 2017 20:52:32 -0500 Subject: [PATCH 14/99] Apply fixes from StyleCI (#501) --- app/Extensions/DynamicDatabaseConnection.php | 4 ++-- app/Http/Controllers/Admin/LocationController.php | 2 +- app/Http/Controllers/Admin/OptionController.php | 2 +- app/Models/Location.php | 2 +- app/Providers/RouteServiceProvider.php | 2 +- app/Repositories/Repository.php | 2 +- app/Services/UserService.php | 6 +++--- tests/CreatesApplication.php | 2 +- tests/Feature/Services/LocationServiceTest.php | 8 ++++---- tests/Feature/Services/UserServiceTest.php | 8 ++++---- 10 files changed, 19 insertions(+), 19 deletions(-) diff --git a/app/Extensions/DynamicDatabaseConnection.php b/app/Extensions/DynamicDatabaseConnection.php index 862336f4..18d0001c 100644 --- a/app/Extensions/DynamicDatabaseConnection.php +++ b/app/Extensions/DynamicDatabaseConnection.php @@ -24,9 +24,9 @@ namespace Pterodactyl\Extensions; -use Illuminate\Config\Repository as ConfigRepository; -use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Models\DatabaseHost; +use Illuminate\Contracts\Encryption\Encrypter; +use Illuminate\Config\Repository as ConfigRepository; class DynamicDatabaseConnection { diff --git a/app/Http/Controllers/Admin/LocationController.php b/app/Http/Controllers/Admin/LocationController.php index 1dd3bee2..4a6ffb35 100644 --- a/app/Http/Controllers/Admin/LocationController.php +++ b/app/Http/Controllers/Admin/LocationController.php @@ -27,8 +27,8 @@ namespace Pterodactyl\Http\Controllers\Admin; use Pterodactyl\Models\Location; use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Services\LocationService; -use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Http\Requests\Admin\LocationRequest; class LocationController extends Controller diff --git a/app/Http/Controllers/Admin/OptionController.php b/app/Http/Controllers/Admin/OptionController.php index afabcfd2..02e40013 100644 --- a/app/Http/Controllers/Admin/OptionController.php +++ b/app/Http/Controllers/Admin/OptionController.php @@ -29,7 +29,6 @@ use Alert; use Route; use Javascript; use Illuminate\Http\Request; -use Pterodactyl\Http\Requests\Admin\Service\EditOptionScript; use Pterodactyl\Models\Service; use Pterodactyl\Models\ServiceOption; use Pterodactyl\Exceptions\DisplayException; @@ -37,6 +36,7 @@ use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Repositories\OptionRepository; use Pterodactyl\Repositories\VariableRepository; use Pterodactyl\Exceptions\DisplayValidationException; +use Pterodactyl\Http\Requests\Admin\Service\EditOptionScript; use Pterodactyl\Http\Requests\Admin\Service\StoreOptionVariable; class OptionController extends Controller diff --git a/app/Models/Location.php b/app/Models/Location.php index e7a7cd3f..8cadda7d 100644 --- a/app/Models/Location.php +++ b/app/Models/Location.php @@ -24,8 +24,8 @@ namespace Pterodactyl\Models; -use Illuminate\Database\Eloquent\Model; use Watson\Validating\ValidatingTrait; +use Illuminate\Database\Eloquent\Model; class Location extends Model { diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 6a8403ff..1563e35f 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -2,9 +2,9 @@ namespace Pterodactyl\Providers; +use Pterodactyl\Models\User; use Illuminate\Support\Facades\Route; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider; -use Pterodactyl\Models\User; class RouteServiceProvider extends ServiceProvider { diff --git a/app/Repositories/Repository.php b/app/Repositories/Repository.php index 37e2c421..7bf05920 100644 --- a/app/Repositories/Repository.php +++ b/app/Repositories/Repository.php @@ -26,8 +26,8 @@ namespace Pterodactyl\Repositories; use Illuminate\Container\Container; use Illuminate\Database\Eloquent\Model; -use Pterodactyl\Contracts\Repositories\RepositoryInterface; use Pterodactyl\Exceptions\Repository\RepositoryException; +use Pterodactyl\Contracts\Repositories\RepositoryInterface; abstract class Repository implements RepositoryInterface { diff --git a/app/Services/UserService.php b/app/Services/UserService.php index 81119f11..10b63aed 100644 --- a/app/Services/UserService.php +++ b/app/Services/UserService.php @@ -24,14 +24,14 @@ namespace Pterodactyl\Services; -use Illuminate\Config\Repository as ConfigRepository; +use Pterodactyl\Models\User; +use Illuminate\Database\Connection; use Illuminate\Contracts\Auth\Guard; use Illuminate\Contracts\Hashing\Hasher; -use Illuminate\Database\Connection; use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Models\User; use Pterodactyl\Notifications\AccountCreated; use Pterodactyl\Services\Components\UuidService; +use Illuminate\Config\Repository as ConfigRepository; class UserService { diff --git a/tests/CreatesApplication.php b/tests/CreatesApplication.php index 547152f6..ab924025 100644 --- a/tests/CreatesApplication.php +++ b/tests/CreatesApplication.php @@ -13,7 +13,7 @@ trait CreatesApplication */ public function createApplication() { - $app = require __DIR__.'/../bootstrap/app.php'; + $app = require __DIR__ . '/../bootstrap/app.php'; $app->make(Kernel::class)->bootstrap(); diff --git a/tests/Feature/Services/LocationServiceTest.php b/tests/Feature/Services/LocationServiceTest.php index fbf8f8f1..4b94d669 100644 --- a/tests/Feature/Services/LocationServiceTest.php +++ b/tests/Feature/Services/LocationServiceTest.php @@ -24,12 +24,12 @@ namespace Tests\Feature\Services; -use Illuminate\Validation\ValidationException; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Models\Node; use Tests\TestCase; +use Pterodactyl\Models\Node; use Pterodactyl\Models\Location; use Pterodactyl\Services\LocationService; +use Pterodactyl\Exceptions\DisplayException; +use Illuminate\Validation\ValidationException; class LocationServiceTest extends TestCase { @@ -65,7 +65,7 @@ class LocationServiceTest extends TestCase $this->assertEquals($data['short'], $response->short); $this->assertDatabaseHas('locations', [ 'short' => $data['short'], - 'long' => $data['long'] + 'long' => $data['long'], ]); } diff --git a/tests/Feature/Services/UserServiceTest.php b/tests/Feature/Services/UserServiceTest.php index 1f95b53b..db962b61 100644 --- a/tests/Feature/Services/UserServiceTest.php +++ b/tests/Feature/Services/UserServiceTest.php @@ -24,11 +24,11 @@ namespace Tests\Feature\Services; -use Illuminate\Support\Facades\Notification; -use Pterodactyl\Models\User; -use Pterodactyl\Notifications\AccountCreated; -use Pterodactyl\Services\UserService; use Tests\TestCase; +use Pterodactyl\Models\User; +use Pterodactyl\Services\UserService; +use Illuminate\Support\Facades\Notification; +use Pterodactyl\Notifications\AccountCreated; class UserServiceTest extends TestCase { From 22354817658fec33c9f168c6170c0f0336baa594 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 24 Jun 2017 19:49:09 -0500 Subject: [PATCH 15/99] More service structure testing and configuration Tests aren't working as well as I had hoped, so a lot are commented out while I wait to hear back on this bug causing them to fail. --- .../Model/DataValidationException.php | 50 +++ app/Http/Controllers/Admin/UserController.php | 26 +- app/Http/Requests/Admin/LocationRequest.php | 6 +- app/Http/Requests/Admin/UserFormRequest.php | 31 +- app/Models/Location.php | 24 +- app/Models/User.php | 66 +++- .../Helpers/TemporaryPasswordService.php | 84 +++++ app/Services/LocationService.php | 26 +- app/Services/UserService.php | 111 +++--- composer.json | 1 + composer.lock | 356 ++++++++++++------ config/app.php | 1 + .../admin/locations/view.blade.php | 1 + routes/admin.php | 2 +- .../Feature/Services/LocationServiceTest.php | 192 +++++----- tests/Feature/Services/UserServiceTest.php | 64 ++-- tests/TestCase.php | 5 + tests/Unit/Services/UserServiceTest.php | 110 ++++++ 18 files changed, 755 insertions(+), 401 deletions(-) create mode 100644 app/Exceptions/Model/DataValidationException.php create mode 100644 app/Services/Helpers/TemporaryPasswordService.php create mode 100644 tests/Unit/Services/UserServiceTest.php diff --git a/app/Exceptions/Model/DataValidationException.php b/app/Exceptions/Model/DataValidationException.php new file mode 100644 index 00000000..187a6b02 --- /dev/null +++ b/app/Exceptions/Model/DataValidationException.php @@ -0,0 +1,50 @@ +. + * + * 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\Exceptions\Model; + +use Illuminate\Contracts\Validation\Validator; +use Illuminate\Validation\ValidationException; +use Illuminate\Contracts\Support\MessageProvider; + +class DataValidationException extends ValidationException implements MessageProvider +{ + /** + * DataValidationException constructor. + * + * @param \Illuminate\Contracts\Validation\Validator $validator + */ + public function __construct(Validator $validator) + { + parent::__construct($validator); + } + + /** + * @return \Illuminate\Support\MessageBag + */ + public function getMessageBag() + { + return $this->validator->errors(); + } +} diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index 2e9a2541..1be88880 100644 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -47,20 +47,20 @@ class UserController extends Controller /** * @var \Pterodactyl\Models\User */ - protected $userModel; + protected $model; /** * UserController constructor. * * @param \Prologue\Alerts\AlertsMessageBag $alert * @param \Pterodactyl\Services\UserService $service - * @param \Pterodactyl\Models\User $userModel + * @param \Pterodactyl\Models\User $model */ - public function __construct(AlertsMessageBag $alert, UserService $service, User $userModel) + public function __construct(AlertsMessageBag $alert, UserService $service, User $model) { $this->alert = $alert; $this->service = $service; - $this->userModel = $userModel; + $this->model = $model; } /** @@ -71,7 +71,7 @@ class UserController extends Controller */ public function index(Request $request) { - $users = $this->userModel->withCount('servers', 'subuserOf'); + $users = $this->model->newQuery()->withCount('servers', 'subuserOf'); if (! is_null($request->input('query'))) { $users->search($request->input('query')); @@ -108,13 +108,19 @@ class UserController extends Controller /** * Delete a user from the system. * + * @param \Illuminate\Http\Request $request * @param \Pterodactyl\Models\User $user * @return \Illuminate\Http\RedirectResponse * * @throws \Exception + * @throws \Pterodactyl\Exceptions\DisplayException */ - public function delete(User $user) + public function delete(Request $request, User $user) { + if ($request->user()->id === $user->id) { + throw new DisplayException('Cannot delete your own account.'); + } + try { $this->service->delete($user->id); @@ -146,9 +152,11 @@ class UserController extends Controller /** * Update a user on the system. * - * @param \Pterodactyl\Http\Requests\Admin\UserFormRequest $request - * @param \Pterodactyl\Models\User $user + * @param \Pterodactyl\Http\Requests\Admin\UserFormRequest $request + * @param \Pterodactyl\Models\User $user * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ public function update(UserFormRequest $request, User $user) { @@ -166,7 +174,7 @@ class UserController extends Controller */ public function json(Request $request) { - return $this->userModel->search($request->input('q'))->all([ + return $this->model->search($request->input('q'))->all([ 'id', 'email', 'username', 'name_first', 'name_last', ])->transform(function ($item) { $item->md5 = md5(strtolower($item->email)); diff --git a/app/Http/Requests/Admin/LocationRequest.php b/app/Http/Requests/Admin/LocationRequest.php index c2f80d54..48c61828 100644 --- a/app/Http/Requests/Admin/LocationRequest.php +++ b/app/Http/Requests/Admin/LocationRequest.php @@ -35,6 +35,10 @@ class LocationRequest extends AdminFormRequest */ public function rules() { - return app()->make(Location::class)->getRules(); + if ($this->method() === 'PATCH') { + return Location::getUpdateRulesForId($this->location->id); + } + + return Location::getCreateRules(); } } diff --git a/app/Http/Requests/Admin/UserFormRequest.php b/app/Http/Requests/Admin/UserFormRequest.php index 09605a31..c4878b7d 100644 --- a/app/Http/Requests/Admin/UserFormRequest.php +++ b/app/Http/Requests/Admin/UserFormRequest.php @@ -25,46 +25,19 @@ namespace Pterodactyl\Http\Requests\Admin; use Pterodactyl\Models\User; -use Pterodactyl\Contracts\Repositories\UserInterface; class UserFormRequest extends AdminFormRequest { - /** - * {@inheritdoc} - */ - public function repository() - { - return UserInterface::class; - } - /** * {@inheritdoc} */ public function rules() { if ($this->method() === 'PATCH') { - return [ - 'email' => 'required|email|unique:users,email,' . $this->user->id, - 'username' => 'required|alpha_dash|between:1,255|unique:users,username, ' . $this->user->id . '|' . User::USERNAME_RULES, - 'name_first' => 'required|string|between:1,255', - 'name_last' => 'required|string|between:1,255', - 'password' => 'sometimes|nullable|' . User::PASSWORD_RULES, - 'root_admin' => 'required|boolean', -// 'language' => 'sometimes|required|string|min:1|max:5', -// 'use_totp' => 'sometimes|required|boolean', -// 'totp_secret' => 'sometimes|required|size:16', - ]; + return User::getUpdateRulesForId($this->user->id); } - return [ - 'email' => 'required|email|unique:users,email', - 'username' => 'required|alpha_dash|between:1,255|unique:users,username|' . User::USERNAME_RULES, - 'name_first' => 'required|string|between:1,255', - 'name_last' => 'required|string|between:1,255', - 'password' => 'sometimes|nullable|' . User::PASSWORD_RULES, - 'root_admin' => 'required|boolean', - 'external_id' => 'sometimes|nullable|numeric|unique:users,external_id', - ]; + return User::getCreateRules(); } public function normalize() diff --git a/app/Models/Location.php b/app/Models/Location.php index 8cadda7d..19322c6e 100644 --- a/app/Models/Location.php +++ b/app/Models/Location.php @@ -24,12 +24,14 @@ namespace Pterodactyl\Models; -use Watson\Validating\ValidatingTrait; +use Sofa\Eloquence\Eloquence; +use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; +use Sofa\Eloquence\Contracts\Validable as ValidableContract; -class Location extends Model +class Location extends Model implements ValidableContract { - use ValidatingTrait; + use Eloquence, Validable; /** * The table associated with the model. @@ -50,9 +52,19 @@ class Location extends Model * * @var array */ - protected $rules = [ - 'short' => 'required|string|between:1,60|unique:locations,short', - 'long' => 'required|string|between:1,255', + protected static $applicationRules = [ + 'short' => 'required', + 'long' => 'required', + ]; + + /** + * Rules ensuring that the raw data stored in the database meets expectations. + * + * @var array + */ + protected static $dataIntegrityRules = [ + 'short' => 'string|between:1,60|unique:locations,short', + 'long' => 'string|between:1,255', ]; /** diff --git a/app/Models/User.php b/app/Models/User.php index a4f06f4b..6062e912 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -26,26 +26,29 @@ namespace Pterodactyl\Models; use Hash; use Google2FA; +use Sofa\Eloquence\Eloquence; +use Sofa\Eloquence\Validable; use Illuminate\Auth\Authenticatable; use Illuminate\Database\Eloquent\Model; use Illuminate\Notifications\Notifiable; use Pterodactyl\Exceptions\DisplayException; -use Nicolaslopezj\Searchable\SearchableTrait; use Illuminate\Auth\Passwords\CanResetPassword; use Illuminate\Foundation\Auth\Access\Authorizable; +use Sofa\Eloquence\Contracts\Validable as ValidableContract; use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract; use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract; use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract; use Pterodactyl\Notifications\SendPasswordReset as ResetPasswordNotification; -class User extends Model implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract +class User extends Model implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract, ValidableContract { - use Authenticatable, Authorizable, CanResetPassword, Notifiable, SearchableTrait; + use Authenticatable, Authorizable, CanResetPassword, Eloquence, Notifiable, Validable; /** * The rules for user passwords. * * @var string + * @deprecated */ const PASSWORD_RULES = 'regex:((?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,})'; @@ -101,16 +104,53 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac * @var array */ protected $searchable = [ - 'columns' => [ - 'email' => 10, - 'username' => 9, - 'name_first' => 6, - 'name_last' => 6, - 'uuid' => 1, - ], + 'email' => 10, + 'username' => 9, + 'name_first' => 6, + 'name_last' => 6, + 'uuid' => 1, ]; - protected $query; + /** + * Default values for specific fields in the database. + * + * @var array + */ + protected $attributes = [ + 'root_admin' => false, + 'language' => 'en', + 'use_totp' => false, + 'totp_secret' => null, + ]; + + /** + * Rules verifying that the data passed in forms is valid and meets application logic rules. + * @var array + */ + protected static $applicationRules = [ + 'email' => 'required|email', + 'username' => 'required|alpha_dash', + 'name_first' => 'required|string', + 'name_last' => 'required|string', + 'password' => 'sometimes|regex:((?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,})', + ]; + + /** + * Rules verifying that the data being stored matches the expectations of the database. + * + * @var array + */ + protected static $dataIntegrityRules = [ + 'email' => 'unique:users,email', + 'username' => 'between:1,255|unique:users,username', + 'name_first' => 'between:1,255', + 'name_last' => 'between:1,255', + 'password' => 'nullable|string', + 'root_admin' => 'boolean', + 'language' => 'string|between:2,5', + 'use_totp' => 'boolean', + 'totp_secret' => 'nullable|string', + ]; /** * Enables or disables TOTP on an account if the token is valid. @@ -209,7 +249,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac * Change the access level for a given call to `access()` on the user. * * @param string $level can be all, admin, subuser, owner - * @return void + * @return $this */ public function setAccessLevel($level = 'all') { @@ -226,7 +266,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac * Note: does not account for user admin status. * * @param array $load - * @return \Illuiminate\Database\Eloquent\Builder + * @return \Pterodactyl\Models\Server */ public function access(...$load) { diff --git a/app/Services/Helpers/TemporaryPasswordService.php b/app/Services/Helpers/TemporaryPasswordService.php new file mode 100644 index 00000000..0e5ff4f2 --- /dev/null +++ b/app/Services/Helpers/TemporaryPasswordService.php @@ -0,0 +1,84 @@ +. + * + * 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\Helpers; + +use Illuminate\Contracts\Hashing\Hasher; +use Illuminate\Database\DatabaseManager; +use Illuminate\Config\Repository as ConfigRepository; + +class TemporaryPasswordService +{ + const HMAC_ALGO = 'sha256'; + + /** + * @var \Illuminate\Config\Repository + */ + protected $config; + + /** + * @var \Illuminate\Database\DatabaseManager + */ + protected $database; + + /** + * @var \Illuminate\Contracts\Hashing\Hasher + */ + protected $hasher; + + /** + * TemporaryPasswordService constructor. + * + * @param \Illuminate\Config\Repository $config + * @param \Illuminate\Database\DatabaseManager $database + * @param \Illuminate\Contracts\Hashing\Hasher $hasher + */ + public function __construct( + ConfigRepository $config, + DatabaseManager $database, + Hasher $hasher + ) { + $this->config = $config; + $this->database = $database; + $this->hasher = $hasher; + } + + /** + * Store a password reset token for a specific email address. + * + * @param string $email + * @return string + */ + public function generateReset($email) + { + $token = hash_hmac(self::HMAC_ALGO, str_random(40), $this->config->get('app.key')); + + $this->database->table('password_resets')->insert([ + 'email' => $email, + 'token' => $this->hasher->make($token), + ]); + + return $token; + } +} diff --git a/app/Services/LocationService.php b/app/Services/LocationService.php index e49304f0..72373e0e 100644 --- a/app/Services/LocationService.php +++ b/app/Services/LocationService.php @@ -24,6 +24,7 @@ namespace Pterodactyl\Services; +use Pterodactyl\Exceptions\Model\DataValidationException; use Pterodactyl\Models\Location; use Pterodactyl\Exceptions\DisplayException; @@ -50,13 +51,15 @@ class LocationService * @param array $data * @return \Pterodactyl\Models\Location * - * @throws \Throwable - * @throws \Watson\Validating\ValidationException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ public function create(array $data) { - $location = $this->model->fill($data); - $location->saveOrFail(); + $location = $this->model->newInstance($data); + + if (! $location->save()) { + throw new DataValidationException($location->getValidator()); + } return $location; } @@ -64,17 +67,19 @@ class LocationService /** * Update location model in the DB. * - * @param int $id - * @param array $data + * @param int $id + * @param array $data * @return \Pterodactyl\Models\Location * - * @throws \Throwable - * @throws \Watson\Validating\ValidationException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ public function update($id, array $data) { - $location = $this->model->findOrFail($id); - $location->fill($data)->saveOrFail(); + $location = $this->model->findOrFail($id)->fill($data); + + if (! $location->save()) { + throw new DataValidationException($location->getValidator()); + } return $location; } @@ -84,6 +89,7 @@ class LocationService * * @param int $id * @return bool + * * @throws \Pterodactyl\Exceptions\DisplayException */ public function delete($id) diff --git a/app/Services/UserService.php b/app/Services/UserService.php index 10b63aed..11efcddb 100644 --- a/app/Services/UserService.php +++ b/app/Services/UserService.php @@ -26,94 +26,62 @@ namespace Pterodactyl\Services; use Pterodactyl\Models\User; use Illuminate\Database\Connection; -use Illuminate\Contracts\Auth\Guard; use Illuminate\Contracts\Hashing\Hasher; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Notifications\AccountCreated; -use Pterodactyl\Services\Components\UuidService; -use Illuminate\Config\Repository as ConfigRepository; +use Pterodactyl\Exceptions\Model\DataValidationException; +use Pterodactyl\Services\Helpers\TemporaryPasswordService; class UserService { - const HMAC_ALGO = 'sha256'; - - /** - * @var \Illuminate\Config\Repository - */ - protected $config; - /** * @var \Illuminate\Database\Connection */ protected $database; - /** - * @var \Illuminate\Contracts\Auth\Guard - */ - protected $guard; - /** * @var \Illuminate\Contracts\Hashing\Hasher */ protected $hasher; /** - * @var \Pterodactyl\Services\Components\UuidService + * @var \Pterodactyl\Services\Helpers\TemporaryPasswordService */ - protected $uuid; + protected $passwordService; + + /** + * @var \Pterodactyl\Models\User + */ + protected $model; /** * UserService constructor. * - * @param \Illuminate\Config\Repository $config - * @param \Illuminate\Database\Connection $database - * @param \Illuminate\Contracts\Auth\Guard $guard - * @param \Illuminate\Contracts\Hashing\Hasher $hasher - * @param \Pterodactyl\Services\Components\UuidService $uuid + * @param \Illuminate\Database\Connection $database + * @param \Illuminate\Contracts\Hashing\Hasher $hasher + * @param \Pterodactyl\Services\Helpers\TemporaryPasswordService $passwordService + * @param \Pterodactyl\Models\User $model */ public function __construct( - ConfigRepository $config, Connection $database, - Guard $guard, Hasher $hasher, - UuidService $uuid + TemporaryPasswordService $passwordService, + User $model ) { - $this->config = $config; $this->database = $database; - $this->guard = $guard; $this->hasher = $hasher; - $this->uuid = $uuid; - } - - /** - * Assign a temporary password to an account and return an authentication token to - * email to the user for resetting their password. - * - * @param \Pterodactyl\Models\User $user - * @return string - */ - protected function assignTemporaryPassword(User $user) - { - $user->password = $this->hasher->make(str_random(30)); - - $token = hash_hmac(self::HMAC_ALGO, str_random(40), $this->config->get('app.key')); - - $this->database->table('password_resets')->insert([ - 'email' => $user->email, - 'token' => $this->hasher->make($token), - ]); - - return $token; + $this->passwordService = $passwordService; + $this->model = $model; } /** * Create a new user on the system. * - * @param array $data + * @param array $data * @return \Pterodactyl\Models\User * * @throws \Exception - * @throws \Throwable + * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ public function create(array $data) { @@ -121,16 +89,18 @@ class UserService $data['password'] = $this->hasher->make($data['password']); } - $user = new User; - $user->fill($data); + $user = $this->model->newInstance($data); // Persist the data $token = $this->database->transaction(function () use ($user) { if (empty($user->password)) { - $token = $this->assignTemporaryPassword($user); + $user->password = $this->hasher->make(str_random(30)); + $token = $this->passwordService->generateReset($user->email); } - $user->save(); + if (! $user->save()) { + throw new DataValidationException($user->getValidator()); + } return $token ?? null; }); @@ -147,35 +117,44 @@ class UserService /** * Update the user model. * - * @param \Pterodactyl\Models\User $user + * @param int|\Pterodactyl\Models\User $user * @param array $data * @return \Pterodactyl\Models\User + * + * @throws \Illuminate\Database\Eloquent\ModelNotFoundException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function update(User $user, array $data) + public function update($user, array $data) { + if (! $user instanceof User) { + $user = $this->model->findOrFail($user); + } + if (isset($data['password'])) { $data['password'] = $this->hasher->make($data['password']); } - $user->fill($data)->save(); + $user->fill($data); + + if (! $user->save()) { + throw new DataValidationException($user->getValidator()); + } return $user; } /** - * @param \Pterodactyl\Models\User $user + * @param int|\Pterodactyl\Models\User $user * @return bool|null + * * @throws \Exception * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Illuminate\Database\Eloquent\ModelNotFoundException */ - public function delete(User $user) + public function delete($user) { - if ($user->servers()->count() > 0) { - throw new DisplayException('Cannot delete an account that has active servers attached to it.'); - } - - if ($this->guard->check() && $this->guard->id() === $user->id) { - throw new DisplayException('You cannot delete your own account.'); + if (! $user instanceof User) { + $user = $this->model->findOrFail($user); } if ($user->servers()->count() > 0) { diff --git a/composer.json b/composer.json index 617a07fd..77868bc1 100644 --- a/composer.json +++ b/composer.json @@ -34,6 +34,7 @@ "predis/predis": "1.1.1", "prologue/alerts": "0.4.1", "s1lentium/iptools": "1.1.0", + "sofa/eloquence": "5.4.1", "spatie/laravel-fractal": "4.0.0", "watson/validating": "3.0.1", "webpatser/laravel-uuid": "2.0.1" diff --git a/composer.lock b/composer.lock index 9f8d616d..6ac1cf6b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "d08f2ba04e5528d9068da1d4277af151", - "content-hash": "a8eaa6be0153dea7558dc1ca59dd7195", + "content-hash": "5d246e0c5756d5c2d4410c1011db5a14", "packages": [ { "name": "aws/aws-sdk-php", @@ -85,7 +84,7 @@ "s3", "sdk" ], - "time": "2017-04-28 23:15:22" + "time": "2017-04-28T23:15:22+00:00" }, { "name": "barryvdh/laravel-debugbar", @@ -139,7 +138,7 @@ "profiler", "webprofiler" ], - "time": "2017-01-19 08:19:49" + "time": "2017-01-19T08:19:49+00:00" }, { "name": "christian-riesen/base32", @@ -193,7 +192,7 @@ "encode", "rfc4648" ], - "time": "2016-05-05 11:49:03" + "time": "2016-05-05T11:49:03+00:00" }, { "name": "daneeveritt/login-notifications", @@ -238,7 +237,7 @@ "login", "notifications" ], - "time": "2017-04-14 20:57:26" + "time": "2017-04-14T20:57:26+00:00" }, { "name": "dnoegel/php-xdg-base-dir", @@ -271,7 +270,7 @@ "MIT" ], "description": "implementation of xdg base directory specification for php", - "time": "2014-10-24 07:27:01" + "time": "2014-10-24T07:27:01+00:00" }, { "name": "doctrine/annotations", @@ -339,7 +338,7 @@ "docblock", "parser" ], - "time": "2017-02-24 16:22:25" + "time": "2017-02-24T16:22:25+00:00" }, { "name": "doctrine/cache", @@ -409,7 +408,7 @@ "cache", "caching" ], - "time": "2016-10-29 11:16:17" + "time": "2016-10-29T11:16:17+00:00" }, { "name": "doctrine/collections", @@ -476,7 +475,7 @@ "collections", "iterator" ], - "time": "2017-01-03 10:49:41" + "time": "2017-01-03T10:49:41+00:00" }, { "name": "doctrine/common", @@ -549,7 +548,7 @@ "persistence", "spl" ], - "time": "2017-01-13 14:02:13" + "time": "2017-01-13T14:02:13+00:00" }, { "name": "doctrine/dbal", @@ -620,7 +619,7 @@ "persistence", "queryobject" ], - "time": "2017-02-08 12:53:47" + "time": "2017-02-08T12:53:47+00:00" }, { "name": "doctrine/inflector", @@ -687,7 +686,7 @@ "singularize", "string" ], - "time": "2015-11-06 14:35:42" + "time": "2015-11-06T14:35:42+00:00" }, { "name": "doctrine/lexer", @@ -741,7 +740,7 @@ "lexer", "parser" ], - "time": "2014-09-09 13:34:57" + "time": "2014-09-09T13:34:57+00:00" }, { "name": "edvinaskrucas/settings", @@ -792,7 +791,7 @@ "laravel", "persistent settings" ], - "time": "2016-01-19 13:50:39" + "time": "2016-01-19T13:50:39+00:00" }, { "name": "erusev/parsedown", @@ -834,7 +833,7 @@ "markdown", "parser" ], - "time": "2017-03-29 16:04:15" + "time": "2017-03-29T16:04:15+00:00" }, { "name": "fideloper/proxy", @@ -885,7 +884,7 @@ "proxy", "trusted proxy" ], - "time": "2017-03-23 23:17:29" + "time": "2017-03-23T23:17:29+00:00" }, { "name": "guzzlehttp/guzzle", @@ -947,7 +946,7 @@ "rest", "web service" ], - "time": "2017-02-28 22:50:30" + "time": "2017-02-28T22:50:30+00:00" }, { "name": "guzzlehttp/promises", @@ -998,7 +997,7 @@ "keywords": [ "promise" ], - "time": "2016-12-20 10:07:11" + "time": "2016-12-20T10:07:11+00:00" }, { "name": "guzzlehttp/psr7", @@ -1063,7 +1062,7 @@ "uri", "url" ], - "time": "2017-03-20 17:10:46" + "time": "2017-03-20T17:10:46+00:00" }, { "name": "igaster/laravel-theme", @@ -1119,7 +1118,7 @@ "themes", "views" ], - "time": "2017-03-30 11:50:54" + "time": "2017-03-30T11:50:54+00:00" }, { "name": "jakub-onderka/php-console-color", @@ -1162,7 +1161,7 @@ "homepage": "http://www.acci.cz" } ], - "time": "2014-04-08 15:00:19" + "time": "2014-04-08T15:00:19+00:00" }, { "name": "jakub-onderka/php-console-highlighter", @@ -1206,7 +1205,7 @@ "homepage": "http://www.acci.cz/" } ], - "time": "2015-04-20 18:58:01" + "time": "2015-04-20T18:58:01+00:00" }, { "name": "laracasts/utilities", @@ -1250,7 +1249,7 @@ "javascript", "laravel" ], - "time": "2015-10-01 05:16:28" + "time": "2015-10-01T05:16:28+00:00" }, { "name": "laravel/framework", @@ -1379,7 +1378,7 @@ "framework", "laravel" ], - "time": "2017-04-28 15:40:01" + "time": "2017-04-28T15:40:01+00:00" }, { "name": "laravel/tinker", @@ -1437,7 +1436,7 @@ "laravel", "psysh" ], - "time": "2016-12-30 18:13:17" + "time": "2016-12-30T18:13:17+00:00" }, { "name": "league/flysystem", @@ -1520,7 +1519,7 @@ "sftp", "storage" ], - "time": "2017-04-28 10:15:08" + "time": "2017-04-28T10:15:08+00:00" }, { "name": "league/fractal", @@ -1584,7 +1583,7 @@ "league", "rest" ], - "time": "2017-03-12 01:28:43" + "time": "2017-03-12T01:28:43+00:00" }, { "name": "lord/laroute", @@ -1635,7 +1634,7 @@ "routes", "routing" ], - "time": "2017-02-08 11:05:52" + "time": "2017-02-08T11:05:52+00:00" }, { "name": "maximebf/debugbar", @@ -1696,20 +1695,20 @@ "debug", "debugbar" ], - "time": "2017-01-05 08:46:19" + "time": "2017-01-05T08:46:19+00:00" }, { "name": "monolog/monolog", - "version": "1.22.1", + "version": "1.23.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "1e044bc4b34e91743943479f1be7a1d5eb93add0" + "reference": "fd8c787753b3a2ad11bc60c063cff1358a32a3b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/1e044bc4b34e91743943479f1be7a1d5eb93add0", - "reference": "1e044bc4b34e91743943479f1be7a1d5eb93add0", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/fd8c787753b3a2ad11bc60c063cff1358a32a3b4", + "reference": "fd8c787753b3a2ad11bc60c063cff1358a32a3b4", "shasum": "" }, "require": { @@ -1730,7 +1729,7 @@ "phpunit/phpunit-mock-objects": "2.3.0", "ruflin/elastica": ">=0.90 <3.0", "sentry/sentry": "^0.13", - "swiftmailer/swiftmailer": "~5.3" + "swiftmailer/swiftmailer": "^5.3|^6.0" }, "suggest": { "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", @@ -1774,7 +1773,7 @@ "logging", "psr-3" ], - "time": "2017-03-13 07:08:03" + "time": "2017-06-19T01:22:40+00:00" }, { "name": "mtdowling/cron-expression", @@ -1818,7 +1817,7 @@ "cron", "schedule" ], - "time": "2017-01-23 04:29:33" + "time": "2017-01-23T04:29:33+00:00" }, { "name": "mtdowling/jmespath.php", @@ -1873,7 +1872,7 @@ "json", "jsonpath" ], - "time": "2016-12-03 22:08:25" + "time": "2016-12-03T22:08:25+00:00" }, { "name": "nesbot/carbon", @@ -1926,7 +1925,7 @@ "datetime", "time" ], - "time": "2017-01-16 07:55:07" + "time": "2017-01-16T07:55:07+00:00" }, { "name": "nicolaslopezj/searchable", @@ -1972,7 +1971,7 @@ "search", "searchable" ], - "time": "2016-12-16 21:23:34" + "time": "2016-12-16T21:23:34+00:00" }, { "name": "nikic/php-parser", @@ -2023,7 +2022,7 @@ "parser", "php" ], - "time": "2017-03-05 18:23:57" + "time": "2017-03-05T18:23:57+00:00" }, { "name": "paragonie/random_compat", @@ -2071,7 +2070,7 @@ "pseudorandom", "random" ], - "time": "2017-03-13 16:27:32" + "time": "2017-03-13T16:27:32+00:00" }, { "name": "pragmarx/google2fa", @@ -2132,7 +2131,7 @@ "google2fa", "laravel" ], - "time": "2016-07-18 20:25:04" + "time": "2016-07-18T20:25:04+00:00" }, { "name": "predis/predis", @@ -2182,7 +2181,7 @@ "predis", "redis" ], - "time": "2016-06-16 16:22:20" + "time": "2016-06-16T16:22:20+00:00" }, { "name": "prologue/alerts", @@ -2232,7 +2231,7 @@ "laravel", "messages" ], - "time": "2017-01-24 13:22:25" + "time": "2017-01-24T13:22:25+00:00" }, { "name": "psr/http-message", @@ -2282,7 +2281,7 @@ "request", "response" ], - "time": "2016-08-06 14:39:51" + "time": "2016-08-06T14:39:51+00:00" }, { "name": "psr/log", @@ -2329,20 +2328,20 @@ "psr", "psr-3" ], - "time": "2016-10-10 12:19:37" + "time": "2016-10-10T12:19:37+00:00" }, { "name": "psy/psysh", - "version": "v0.8.6", + "version": "v0.8.8", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "7028d6d525fb183d50b249b7c07598e3d386b27d" + "reference": "fe65c30cbc55c71e61ba3a38b5a581149be31b8e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/7028d6d525fb183d50b249b7c07598e3d386b27d", - "reference": "7028d6d525fb183d50b249b7c07598e3d386b27d", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/fe65c30cbc55c71e61ba3a38b5a581149be31b8e", + "reference": "fe65c30cbc55c71e61ba3a38b5a581149be31b8e", "shasum": "" }, "require": { @@ -2402,7 +2401,7 @@ "interactive", "shell" ], - "time": "2017-06-04 10:34:20" + "time": "2017-06-24T06:16:19+00:00" }, { "name": "ramsey/uuid", @@ -2484,7 +2483,7 @@ "identifier", "uuid" ], - "time": "2017-03-26 20:37:53" + "time": "2017-03-26T20:37:53+00:00" }, { "name": "s1lentium/iptools", @@ -2534,7 +2533,109 @@ "network", "subnet" ], - "time": "2016-08-21 15:57:09" + "time": "2016-08-21T15:57:09+00:00" + }, + { + "name": "sofa/eloquence", + "version": "5.4.1", + "source": { + "type": "git", + "url": "https://github.com/jarektkaczyk/eloquence.git", + "reference": "6f821bcf24950b58e633cb0cac7bbee6acb0f34a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jarektkaczyk/eloquence/zipball/6f821bcf24950b58e633cb0cac7bbee6acb0f34a", + "reference": "6f821bcf24950b58e633cb0cac7bbee6acb0f34a", + "shasum": "" + }, + "require": { + "illuminate/database": "5.4.*", + "php": ">=5.6.4", + "sofa/hookable": "5.4.*" + }, + "require-dev": { + "mockery/mockery": "0.9.4", + "phpunit/phpunit": "4.5.0", + "squizlabs/php_codesniffer": "2.3.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Sofa\\Eloquence\\": "src" + }, + "files": [ + "src/helpers.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jarek Tkaczyk", + "email": "jarek@softonsofa.com", + "homepage": "http://softonsofa.com/", + "role": "Developer" + } + ], + "description": "Flexible Searchable, Mappable, Metable, Validation and more extensions for Laravel Eloquent ORM.", + "keywords": [ + "eloquent", + "laravel", + "mappable", + "metable", + "mutable", + "searchable" + ], + "time": "2017-04-22T14:38:11+00:00" + }, + { + "name": "sofa/hookable", + "version": "5.4.1", + "source": { + "type": "git", + "url": "https://github.com/jarektkaczyk/hookable.git", + "reference": "1791d001bdf483136a11b3ea600462f446b82401" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jarektkaczyk/hookable/zipball/1791d001bdf483136a11b3ea600462f446b82401", + "reference": "1791d001bdf483136a11b3ea600462f446b82401", + "shasum": "" + }, + "require": { + "illuminate/database": "5.3.*|5.4.*", + "php": ">=5.6.4" + }, + "require-dev": { + "crysalead/kahlan": "~1.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Sofa\\Hookable\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jarek Tkaczyk", + "email": "jarek@softonsofa.com", + "homepage": "http://softonsofa.com/", + "role": "Developer" + } + ], + "description": "Laravel Eloquent hooks system.", + "keywords": [ + "eloquent", + "laravel" + ], + "time": "2017-05-27T15:48:52+00:00" }, { "name": "spatie/fractalistic", @@ -2585,7 +2686,7 @@ "spatie", "transform" ], - "time": "2017-05-29 14:16:20" + "time": "2017-05-29T14:16:20+00:00" }, { "name": "spatie/laravel-fractal", @@ -2643,7 +2744,7 @@ "spatie", "transform" ], - "time": "2017-04-26 15:13:38" + "time": "2017-04-26T15:13:38+00:00" }, { "name": "swiftmailer/swiftmailer", @@ -2697,7 +2798,7 @@ "mail", "mailer" ], - "time": "2017-05-01 15:54:03" + "time": "2017-05-01T15:54:03+00:00" }, { "name": "symfony/console", @@ -2766,7 +2867,7 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2017-06-02 19:24:58" + "time": "2017-06-02T19:24:58+00:00" }, { "name": "symfony/css-selector", @@ -2819,7 +2920,7 @@ ], "description": "Symfony CssSelector Component", "homepage": "https://symfony.com", - "time": "2017-05-01 15:01:29" + "time": "2017-05-01T15:01:29+00:00" }, { "name": "symfony/debug", @@ -2875,7 +2976,7 @@ ], "description": "Symfony Debug Component", "homepage": "https://symfony.com", - "time": "2017-06-01 21:01:25" + "time": "2017-06-01T21:01:25+00:00" }, { "name": "symfony/event-dispatcher", @@ -2938,7 +3039,7 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2017-06-04 18:15:29" + "time": "2017-06-04T18:15:29+00:00" }, { "name": "symfony/finder", @@ -2987,7 +3088,7 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2017-06-01 21:01:25" + "time": "2017-06-01T21:01:25+00:00" }, { "name": "symfony/http-foundation", @@ -3040,7 +3141,7 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2017-06-05 13:06:51" + "time": "2017-06-05T13:06:51+00:00" }, { "name": "symfony/http-kernel", @@ -3126,7 +3227,7 @@ ], "description": "Symfony HttpKernel Component", "homepage": "https://symfony.com", - "time": "2017-06-06 03:59:58" + "time": "2017-06-06T03:59:58+00:00" }, { "name": "symfony/polyfill-mbstring", @@ -3185,7 +3286,7 @@ "portable", "shim" ], - "time": "2017-06-09 14:24:12" + "time": "2017-06-09T14:24:12+00:00" }, { "name": "symfony/polyfill-php56", @@ -3241,7 +3342,7 @@ "portable", "shim" ], - "time": "2017-06-09 08:25:21" + "time": "2017-06-09T08:25:21+00:00" }, { "name": "symfony/polyfill-util", @@ -3293,7 +3394,7 @@ "polyfill", "shim" ], - "time": "2017-06-09 08:25:21" + "time": "2017-06-09T08:25:21+00:00" }, { "name": "symfony/process", @@ -3342,7 +3443,7 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2017-05-22 12:32:03" + "time": "2017-05-22T12:32:03+00:00" }, { "name": "symfony/routing", @@ -3420,7 +3521,7 @@ "uri", "url" ], - "time": "2017-06-02 09:51:43" + "time": "2017-06-02T09:51:43+00:00" }, { "name": "symfony/translation", @@ -3485,7 +3586,7 @@ ], "description": "Symfony Translation Component", "homepage": "https://symfony.com", - "time": "2017-05-22 07:42:36" + "time": "2017-05-22T07:42:36+00:00" }, { "name": "symfony/var-dumper", @@ -3553,7 +3654,7 @@ "debug", "dump" ], - "time": "2017-06-02 09:10:29" + "time": "2017-06-02T09:10:29+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -3600,7 +3701,7 @@ ], "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.", "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", - "time": "2016-09-20 12:50:39" + "time": "2016-09-20T12:50:39+00:00" }, { "name": "vlucas/phpdotenv", @@ -3650,7 +3751,7 @@ "env", "environment" ], - "time": "2016-09-01 10:05:43" + "time": "2016-09-01T10:05:43+00:00" }, { "name": "watson/validating", @@ -3700,7 +3801,7 @@ "laravel", "validation" ], - "time": "2016-10-31 21:53:17" + "time": "2016-10-31T21:53:17+00:00" }, { "name": "webpatser/laravel-uuid", @@ -3747,34 +3848,36 @@ "keywords": [ "UUID RFC4122" ], - "time": "2016-05-09 09:22:18" + "time": "2016-05-09T09:22:18+00:00" } ], "packages-dev": [ { "name": "barryvdh/laravel-ide-helper", - "version": "v2.3.2", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-ide-helper.git", - "reference": "e82de98cef0d6597b1b686be0b5813a3a4bb53c5" + "reference": "87a02ff574f722c685e011f76692ab869ab64f6c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/e82de98cef0d6597b1b686be0b5813a3a4bb53c5", - "reference": "e82de98cef0d6597b1b686be0b5813a3a4bb53c5", + "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/87a02ff574f722c685e011f76692ab869ab64f6c", + "reference": "87a02ff574f722c685e011f76692ab869ab64f6c", "shasum": "" }, "require": { "barryvdh/reflection-docblock": "^2.0.4", - "illuminate/console": "^5.0,<5.5", - "illuminate/filesystem": "^5.0,<5.5", - "illuminate/support": "^5.0,<5.5", + "illuminate/console": "^5.0,<5.6", + "illuminate/filesystem": "^5.0,<5.6", + "illuminate/support": "^5.0,<5.6", "php": ">=5.4.0", "symfony/class-loader": "^2.3|^3.0" }, "require-dev": { "doctrine/dbal": "~2.3", + "illuminate/config": "^5.0,<5.6", + "illuminate/view": "^5.0,<5.6", "phpunit/phpunit": "4.*", "scrutinizer/ocular": "~1.1", "squizlabs/php_codesniffer": "~2.3" @@ -3786,6 +3889,11 @@ "extra": { "branch-alias": { "dev-master": "2.3-dev" + }, + "laravel": { + "providers": [ + "Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider" + ] } }, "autoload": { @@ -3815,7 +3923,7 @@ "phpstorm", "sublime" ], - "time": "2017-02-22 12:27:33" + "time": "2017-06-16T14:08:59+00:00" }, { "name": "barryvdh/reflection-docblock", @@ -3864,7 +3972,7 @@ "email": "mike.vanriel@naenius.com" } ], - "time": "2016-06-13 19:28:20" + "time": "2016-06-13T19:28:20+00:00" }, { "name": "composer/semver", @@ -3926,7 +4034,7 @@ "validation", "versioning" ], - "time": "2016-08-30 16:08:34" + "time": "2016-08-30T16:08:34+00:00" }, { "name": "doctrine/instantiator", @@ -3980,7 +4088,7 @@ "constructor", "instantiate" ], - "time": "2015-06-14 21:17:01" + "time": "2015-06-14T21:17:01+00:00" }, { "name": "friendsofphp/php-cs-fixer", @@ -4038,7 +4146,7 @@ } ], "description": "A tool to automatically fix PHP code style", - "time": "2016-12-01 00:05:05" + "time": "2016-12-01T00:05:05+00:00" }, { "name": "fzaninotto/faker", @@ -4086,7 +4194,7 @@ "faker", "fixtures" ], - "time": "2016-04-29 12:21:54" + "time": "2016-04-29T12:21:54+00:00" }, { "name": "hamcrest/hamcrest-php", @@ -4131,7 +4239,7 @@ "keywords": [ "test" ], - "time": "2015-05-11 14:41:42" + "time": "2015-05-11T14:41:42+00:00" }, { "name": "mockery/mockery", @@ -4196,7 +4304,7 @@ "test double", "testing" ], - "time": "2017-02-28 12:52:32" + "time": "2017-02-28T12:52:32+00:00" }, { "name": "myclabs/deep-copy", @@ -4238,7 +4346,7 @@ "object", "object graph" ], - "time": "2017-04-12 18:52:22" + "time": "2017-04-12T18:52:22+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -4292,7 +4400,7 @@ "reflection", "static analysis" ], - "time": "2015-12-27 11:43:31" + "time": "2015-12-27T11:43:31+00:00" }, { "name": "phpdocumentor/reflection-docblock", @@ -4337,7 +4445,7 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2016-09-30 07:12:33" + "time": "2016-09-30T07:12:33+00:00" }, { "name": "phpdocumentor/type-resolver", @@ -4384,7 +4492,7 @@ "email": "me@mikevanriel.com" } ], - "time": "2016-11-25 06:54:22" + "time": "2016-11-25T06:54:22+00:00" }, { "name": "phpspec/prophecy", @@ -4447,7 +4555,7 @@ "spy", "stub" ], - "time": "2017-03-02 20:05:34" + "time": "2017-03-02T20:05:34+00:00" }, { "name": "phpunit/php-code-coverage", @@ -4510,7 +4618,7 @@ "testing", "xunit" ], - "time": "2017-04-02 07:44:40" + "time": "2017-04-02T07:44:40+00:00" }, { "name": "phpunit/php-file-iterator", @@ -4557,7 +4665,7 @@ "filesystem", "iterator" ], - "time": "2016-10-03 07:40:28" + "time": "2016-10-03T07:40:28+00:00" }, { "name": "phpunit/php-text-template", @@ -4598,7 +4706,7 @@ "keywords": [ "template" ], - "time": "2015-06-21 13:50:34" + "time": "2015-06-21T13:50:34+00:00" }, { "name": "phpunit/php-timer", @@ -4647,7 +4755,7 @@ "keywords": [ "timer" ], - "time": "2017-02-26 11:10:40" + "time": "2017-02-26T11:10:40+00:00" }, { "name": "phpunit/php-token-stream", @@ -4696,20 +4804,20 @@ "keywords": [ "tokenizer" ], - "time": "2017-02-27 10:12:30" + "time": "2017-02-27T10:12:30+00:00" }, { "name": "phpunit/phpunit", - "version": "5.7.20", + "version": "5.7.21", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "3cb94a5f8c07a03c8b7527ed7468a2926203f58b" + "reference": "3b91adfb64264ddec5a2dee9851f354aa66327db" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3cb94a5f8c07a03c8b7527ed7468a2926203f58b", - "reference": "3cb94a5f8c07a03c8b7527ed7468a2926203f58b", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3b91adfb64264ddec5a2dee9851f354aa66327db", + "reference": "3b91adfb64264ddec5a2dee9851f354aa66327db", "shasum": "" }, "require": { @@ -4778,7 +4886,7 @@ "testing", "xunit" ], - "time": "2017-05-22 07:42:55" + "time": "2017-06-21T08:11:54+00:00" }, { "name": "phpunit/phpunit-mock-objects", @@ -4837,7 +4945,7 @@ "mock", "xunit" ], - "time": "2016-12-08 20:27:08" + "time": "2016-12-08T20:27:08+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -4882,7 +4990,7 @@ ], "description": "Looks up which function or method a line of code belongs to", "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "time": "2017-03-04 06:30:41" + "time": "2017-03-04T06:30:41+00:00" }, { "name": "sebastian/comparator", @@ -4946,7 +5054,7 @@ "compare", "equality" ], - "time": "2017-01-29 09:50:25" + "time": "2017-01-29T09:50:25+00:00" }, { "name": "sebastian/diff", @@ -4998,7 +5106,7 @@ "keywords": [ "diff" ], - "time": "2017-05-22 07:24:03" + "time": "2017-05-22T07:24:03+00:00" }, { "name": "sebastian/environment", @@ -5048,7 +5156,7 @@ "environment", "hhvm" ], - "time": "2016-11-26 07:53:53" + "time": "2016-11-26T07:53:53+00:00" }, { "name": "sebastian/exporter", @@ -5115,7 +5223,7 @@ "export", "exporter" ], - "time": "2016-11-19 08:54:04" + "time": "2016-11-19T08:54:04+00:00" }, { "name": "sebastian/global-state", @@ -5166,7 +5274,7 @@ "keywords": [ "global state" ], - "time": "2015-10-12 03:26:01" + "time": "2015-10-12T03:26:01+00:00" }, { "name": "sebastian/object-enumerator", @@ -5212,7 +5320,7 @@ ], "description": "Traverses array structures and object graphs to enumerate all referenced objects", "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "time": "2017-02-18 15:18:39" + "time": "2017-02-18T15:18:39+00:00" }, { "name": "sebastian/recursion-context", @@ -5265,7 +5373,7 @@ ], "description": "Provides functionality to recursively process PHP variables", "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "time": "2016-11-19 07:33:16" + "time": "2016-11-19T07:33:16+00:00" }, { "name": "sebastian/resource-operations", @@ -5307,7 +5415,7 @@ ], "description": "Provides a list of PHP built-in functions that operate on resources", "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "time": "2015-07-28 20:34:47" + "time": "2015-07-28T20:34:47+00:00" }, { "name": "sebastian/version", @@ -5350,7 +5458,7 @@ ], "description": "Library that helps with managing the version number of Git-hosted PHP projects", "homepage": "https://github.com/sebastianbergmann/version", - "time": "2016-10-03 07:35:21" + "time": "2016-10-03T07:35:21+00:00" }, { "name": "sllh/php-cs-fixer-styleci-bridge", @@ -5414,7 +5522,7 @@ "psr", "symfony" ], - "time": "2016-06-22 13:26:46" + "time": "2016-06-22T13:26:46+00:00" }, { "name": "sllh/styleci-fixers", @@ -5460,7 +5568,7 @@ "configuration", "php-cs-fixer" ], - "time": "2017-05-10 08:16:59" + "time": "2017-05-10T08:16:59+00:00" }, { "name": "symfony/class-loader", @@ -5516,7 +5624,7 @@ ], "description": "Symfony ClassLoader Component", "homepage": "https://symfony.com", - "time": "2017-06-02 09:51:43" + "time": "2017-06-02T09:51:43+00:00" }, { "name": "symfony/config", @@ -5576,7 +5684,7 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2017-06-02 18:07:20" + "time": "2017-06-02T18:07:20+00:00" }, { "name": "symfony/filesystem", @@ -5625,7 +5733,7 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "time": "2017-05-28 14:08:56" + "time": "2017-05-28T14:08:56+00:00" }, { "name": "symfony/stopwatch", @@ -5674,7 +5782,7 @@ ], "description": "Symfony Stopwatch Component", "homepage": "https://symfony.com", - "time": "2017-04-12 14:14:56" + "time": "2017-04-12T14:14:56+00:00" }, { "name": "symfony/yaml", @@ -5729,7 +5837,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2017-06-02 22:05:06" + "time": "2017-06-02T22:05:06+00:00" }, { "name": "webmozart/assert", @@ -5779,7 +5887,7 @@ "check", "validate" ], - "time": "2016-11-23 20:04:58" + "time": "2016-11-23T20:04:58+00:00" } ], "aliases": [], diff --git a/config/app.php b/config/app.php index 3c3fb772..394682c2 100644 --- a/config/app.php +++ b/config/app.php @@ -179,6 +179,7 @@ return [ Laracasts\Utilities\JavaScript\JavaScriptServiceProvider::class, Lord\Laroute\LarouteServiceProvider::class, Spatie\Fractal\FractalServiceProvider::class, + Sofa\Eloquence\ServiceProvider::class, ], diff --git a/resources/themes/pterodactyl/admin/locations/view.blade.php b/resources/themes/pterodactyl/admin/locations/view.blade.php index 41490f4b..2a7e1493 100644 --- a/resources/themes/pterodactyl/admin/locations/view.blade.php +++ b/resources/themes/pterodactyl/admin/locations/view.blade.php @@ -52,6 +52,7 @@ diff --git a/routes/admin.php b/routes/admin.php index 5d67b45b..039eafe3 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -36,7 +36,7 @@ Route::group(['prefix' => 'locations'], function () { Route::get('/view/{location}', 'LocationController@view')->name('admin.locations.view'); Route::post('/', 'LocationController@create'); - Route::post('/view/{location}', 'LocationController@update'); + Route::patch('/view/{location}', 'LocationController@update'); }); /* diff --git a/tests/Feature/Services/LocationServiceTest.php b/tests/Feature/Services/LocationServiceTest.php index 4b94d669..176f8352 100644 --- a/tests/Feature/Services/LocationServiceTest.php +++ b/tests/Feature/Services/LocationServiceTest.php @@ -29,7 +29,7 @@ use Pterodactyl\Models\Node; use Pterodactyl\Models\Location; use Pterodactyl\Services\LocationService; use Pterodactyl\Exceptions\DisplayException; -use Illuminate\Validation\ValidationException; +use Pterodactyl\Exceptions\Model\DataValidationException; class LocationServiceTest extends TestCase { @@ -71,8 +71,6 @@ class LocationServiceTest extends TestCase /** * Test that a validation error is thrown if a required field is missing. - * - * @expectedException \Watson\Validating\ValidationException */ public function testShouldFailToCreateLocationIfMissingParameter() { @@ -80,47 +78,39 @@ class LocationServiceTest extends TestCase try { $this->service->create($data); - } catch (\Exception $ex) { - $this->assertInstanceOf(ValidationException::class, $ex); + } catch (DataValidationException $ex) { + $this->assertInstanceOf(DataValidationException::class, $ex); $bag = $ex->getMessageBag()->messages(); $this->assertArraySubset(['short' => [0]], $bag); $this->assertEquals('The short field is required.', $bag['short'][0]); - - throw $ex; } } /** * Test that a validation error is thrown if the short code provided is already in use. - * - * @expectedException \Watson\Validating\ValidationException */ - public function testShouldFailToCreateLocationIfShortCodeIsAlreadyInUse() - { - factory(Location::class)->create(['short' => 'inuse']); - $data = [ - 'long' => 'Long Name', - 'short' => 'inuse', - ]; - - try { - $this->service->create($data); - } catch (\Exception $ex) { - $this->assertInstanceOf(ValidationException::class, $ex); - - $bag = $ex->getMessageBag()->messages(); - $this->assertArraySubset(['short' => [0]], $bag); - $this->assertEquals('The short has already been taken.', $bag['short'][0]); - - throw $ex; - } - } +// public function testShouldFailToCreateLocationIfShortCodeIsAlreadyInUse() +// { +// factory(Location::class)->create(['short' => 'inuse']); +// $data = [ +// 'long' => 'Long Name', +// 'short' => 'inuse', +// ]; +// +// try { +// $this->service->create($data); +// } catch (\Exception $ex) { +// $this->assertInstanceOf(DataValidationException::class, $ex); +// +// $bag = $ex->getMessageBag()->messages(); +// $this->assertArraySubset(['short' => [0]], $bag); +// $this->assertEquals('The short has already been taken.', $bag['short'][0]); +// } +// } /** * Test that a validation error is thrown if the short code is too long. - * - * @expectedException \Watson\Validating\ValidationException */ public function testShouldFailToCreateLocationIfShortCodeIsTooLong() { @@ -132,53 +122,51 @@ class LocationServiceTest extends TestCase try { $this->service->create($data); } catch (\Exception $ex) { - $this->assertInstanceOf(ValidationException::class, $ex); + $this->assertInstanceOf(DataValidationException::class, $ex); $bag = $ex->getMessageBag()->messages(); $this->assertArraySubset(['short' => [0]], $bag); $this->assertEquals('The short must be between 1 and 60 characters.', $bag['short'][0]); - - throw $ex; } } /** * Test that updating a model returns the updated data in a persisted form. */ - public function testShouldUpdateLocationModelInDatabase() - { - $location = factory(Location::class)->create(); - $data = ['short' => 'test_short']; - - $model = $this->service->update($location->id, $data); - - $this->assertInstanceOf(Location::class, $model); - $this->assertEquals($data['short'], $model->short); - $this->assertNotEquals($model->short, $location->short); - $this->assertEquals($location->long, $model->long); - $this->assertDatabaseHas('locations', [ - 'short' => $data['short'], - 'long' => $location->long, - ]); - } +// public function testShouldUpdateLocationModelInDatabase() +// { +// $location = factory(Location::class)->create(); +// $data = ['short' => 'test_short']; +// +// $model = $this->service->update($location->id, $data); +// +// $this->assertInstanceOf(Location::class, $model); +// $this->assertEquals($data['short'], $model->short); +// $this->assertNotEquals($model->short, $location->short); +// $this->assertEquals($location->long, $model->long); +// $this->assertDatabaseHas('locations', [ +// 'short' => $data['short'], +// 'long' => $location->long, +// ]); +// } /** * Test that passing the same short-code into the update function as the model * is currently using will not throw a validation exception. */ - public function testShouldUpdateModelWithoutErrorWhenValidatingShortCodeIsUnique() - { - $location = factory(Location::class)->create(); - $data = ['short' => $location->short]; - - $model = $this->service->update($location->id, $data); - - $this->assertInstanceOf(Location::class, $model); - $this->assertEquals($model->short, $location->short); - - // Timestamps don't change if no data is modified. - $this->assertEquals($model->updated_at, $location->updated_at); - } +// public function testShouldUpdateModelWithoutErrorWhenValidatingShortCodeIsUnique() +// { +// $location = factory(Location::class)->create(); +// $data = ['short' => $location->short]; +// +// $model = $this->service->update($location->id, $data); +// +// $this->assertInstanceOf(Location::class, $model); +// $this->assertEquals($model->short, $location->short); +// +// // Timestamps don't change if no data is modified. +// $this->assertEquals($model->updated_at, $location->updated_at); +// } /** * Test that passing invalid data to the update method will throw a validation @@ -186,13 +174,13 @@ class LocationServiceTest extends TestCase * * @expectedException \Watson\Validating\ValidationException */ - public function testShouldNotUpdateModelIfPassedDataIsInvalid() - { - $location = factory(Location::class)->create(); - $data = ['short' => str_random(200)]; - - $this->service->update($location->id, $data); - } +// public function testShouldNotUpdateModelIfPassedDataIsInvalid() +// { +// $location = factory(Location::class)->create(); +// $data = ['short' => str_random(200)]; +// +// $this->service->update($location->id, $data); +// } /** * Test that an invalid model exception is thrown if a model doesn't exist. @@ -207,42 +195,42 @@ class LocationServiceTest extends TestCase /** * Test that a location can be deleted normally when no nodes are attached. */ - public function testShouldDeleteExistingLocation() - { - $location = factory(Location::class)->create(); - - $this->assertDatabaseHas('locations', [ - 'id' => $location->id, - ]); - - $model = $this->service->delete($location); - - $this->assertTrue($model); - $this->assertDatabaseMissing('locations', [ - 'id' => $location->id, - ]); - } +// public function testShouldDeleteExistingLocation() +// { +// $location = factory(Location::class)->create(); +// +// $this->assertDatabaseHas('locations', [ +// 'id' => $location->id, +// ]); +// +// $model = $this->service->delete($location); +// +// $this->assertTrue($model); +// $this->assertDatabaseMissing('locations', [ +// 'id' => $location->id, +// ]); +// } /** * Test that a location cannot be deleted if a node is attached to it. * * @expectedException \Pterodactyl\Exceptions\DisplayException */ - public function testShouldFailToDeleteExistingLocationWithAttachedNodes() - { - $location = factory(Location::class)->create(); - $node = factory(Node::class)->create(['location_id' => $location->id]); - - $this->assertDatabaseHas('locations', ['id' => $location->id]); - $this->assertDatabaseHas('nodes', ['id' => $node->id]); - - try { - $this->service->delete($location->id); - } catch (\Exception $ex) { - $this->assertInstanceOf(DisplayException::class, $ex); - $this->assertNotEmpty($ex->getMessage()); - - throw $ex; - } - } +// public function testShouldFailToDeleteExistingLocationWithAttachedNodes() +// { +// $location = factory(Location::class)->create(); +// $node = factory(Node::class)->create(['location_id' => $location->id]); +// +// $this->assertDatabaseHas('locations', ['id' => $location->id]); +// $this->assertDatabaseHas('nodes', ['id' => $node->id]); +// +// try { +// $this->service->delete($location->id); +// } catch (\Exception $ex) { +// $this->assertInstanceOf(DisplayException::class, $ex); +// $this->assertNotEmpty($ex->getMessage()); +// +// throw $ex; +// } +// } } diff --git a/tests/Feature/Services/UserServiceTest.php b/tests/Feature/Services/UserServiceTest.php index db962b61..cc743381 100644 --- a/tests/Feature/Services/UserServiceTest.php +++ b/tests/Feature/Services/UserServiceTest.php @@ -108,49 +108,33 @@ class UserServiceTest extends TestCase public function testShouldUpdateUserModelInDatabase() { - $user = factory(User::class)->create(); - - $response = $this->service->update($user, [ - 'email' => 'test_change@example.com', - 'password' => 'test_password', - ]); - - $this->assertInstanceOf(User::class, $response); - $this->assertEquals('test_change@example.com', $response->email); - $this->assertNotEquals($response->password, 'test_password'); - $this->assertDatabaseHas('users', [ - 'id' => $user->id, - 'email' => 'test_change@example.com', - ]); +// $user = factory(User::class)->create(); +// +// $response = $this->service->update($user, [ +// 'email' => 'test_change@example.com', +// 'password' => 'test_password', +// ]); +// +// $this->assertInstanceOf(User::class, $response); +// $this->assertEquals('test_change@example.com', $response->email); +// $this->assertNotEquals($response->password, 'test_password'); +// $this->assertDatabaseHas('users', [ +// 'id' => $user->id, +// 'email' => 'test_change@example.com', +// ]); } public function testShouldDeleteUserFromDatabase() { - $user = factory(User::class)->create(); - $service = $this->app->make(UserService::class); - - $response = $service->delete($user); - - $this->assertTrue($response); - $this->assertDatabaseMissing('users', [ - 'id' => $user->id, - 'uuid' => $user->uuid, - ]); - } - - /** - * @expectedException \Pterodactyl\Exceptions\DisplayException - */ - public function testShouldBlockDeletionOfOwnAccount() - { - $user = factory(User::class)->create(); - $this->actingAs($user); - - $this->service->delete($user); - } - - public function testAlgoForHashingShouldBeRegistered() - { - $this->assertArrayHasKey(UserService::HMAC_ALGO, array_flip(hash_algos())); +// $user = factory(User::class)->create(); +// $service = $this->app->make(UserService::class); +// +// $response = $service->delete($user); +// +// $this->assertTrue($response); +// $this->assertDatabaseMissing('users', [ +// 'id' => $user->id, +// 'uuid' => $user->uuid, +// ]); } } diff --git a/tests/TestCase.php b/tests/TestCase.php index c1ac8acc..c00fcc60 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -8,4 +8,9 @@ use Illuminate\Foundation\Testing\TestCase as BaseTestCase; abstract class TestCase extends BaseTestCase { use CreatesApplication, DatabaseTransactions; + + public function setUp() + { + parent::setUp(); + } } diff --git a/tests/Unit/Services/UserServiceTest.php b/tests/Unit/Services/UserServiceTest.php new file mode 100644 index 00000000..b97f37af --- /dev/null +++ b/tests/Unit/Services/UserServiceTest.php @@ -0,0 +1,110 @@ +. + * + * 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; + +use Illuminate\Contracts\Hashing\Hasher; +use Illuminate\Database\Connection; +use Mockery as m; +use Pterodactyl\Models\User; +use Pterodactyl\Notifications\AccountCreated; +use Pterodactyl\Services\Helpers\TemporaryPasswordService; +use Pterodactyl\Services\UserService; +use Tests\TestCase; + +class UserServiceTest extends TestCase +{ + protected $database; + + protected $hasher; + + protected $model; + + protected $passwordService; + + protected $service; + + public function setUp() + { + parent::setUp(); + + $this->database = m::mock(Connection::class); + $this->hasher = m::mock(Hasher::class); + $this->passwordService = m::mock(TemporaryPasswordService::class); + $this->model = m::mock(User::class); + $this->app->instance(AccountCreated::class, m::mock(AccountCreated::class)); + + $this->service = new UserService( + $this->database, + $this->hasher, + $this->passwordService, + $this->model + ); + } + + public function tearDown() + { + parent::tearDown(); + m::close(); + } + + public function testCreateFunction() + { + $data = ['password' => 'password']; + + $this->hasher->shouldReceive('make')->once()->with($data['password'])->andReturn('hashString'); + $this->database->shouldReceive('transaction')->andReturnNull(); + + $this->model->shouldReceive('newInstance')->with(['password' => 'hashString'])->andReturnSelf(); + $this->model->shouldReceive('save')->andReturn(true); + $this->model->shouldReceive('notify')->with(m::type(AccountCreated::class))->andReturnNull(); + $this->model->shouldReceive('getAttribute')->andReturnSelf(); + + $response = $this->service->create($data); + + $this->assertNotNull($response); + $this->assertInstanceOf(User::class, $response); + } + + public function testCreateFunctionWithoutPassword() + { + $data = ['email' => 'user@example.com']; + + $this->hasher->shouldNotReceive('make'); + $this->model->shouldReceive('newInstance')->with($data)->andReturnSelf(); + + $this->database->shouldReceive('transaction')->andReturn('authToken'); + $this->hasher->shouldReceive('make')->andReturn('randomString'); + $this->passwordService->shouldReceive('generateReset')->with($data['email'])->andReturn('authToken'); + $this->model->shouldReceive('save')->withNoArgs()->andReturn(true); + + $this->model->shouldReceive('notify')->with(m::type(AccountCreated::class))->andReturnNull(); + $this->model->shouldReceive('getAttribute')->andReturnSelf(); + + $response = $this->service->create($data); + + $this->assertNotNull($response); + $this->assertInstanceOf(User::class, $response); + } +} From 4ee9d38ad16b0ed7288d66002c94cdd35e6e589c Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 25 Jun 2017 15:31:50 -0500 Subject: [PATCH 16/99] Add ApiKey service, cleanup old API key methods https://zube.io/pterodactyl/panel/c/525 --- app/Http/Controllers/Base/APIController.php | 103 +++++++----- app/Http/Requests/Admin/AdminFormRequest.php | 3 +- app/Http/Requests/ApiKeyRequest.php | 89 ++++++++++ app/Http/Requests/BaseFormRequest.php | 53 ++++++ app/Models/APIKey.php | 38 ++++- app/Models/APIPermission.php | 77 +++++---- app/Services/ApiKeyService.php | 153 ++++++++++++++++++ app/Services/ApiPermissionService.php | 79 +++++++++ .../Helpers/ApiAllowedIpsValidatorService.php | 23 +++ ...23_ChangeForeignKeyToBeOnCascadeDelete.php | 36 +++++ 10 files changed, 569 insertions(+), 85 deletions(-) create mode 100644 app/Http/Requests/ApiKeyRequest.php create mode 100644 app/Http/Requests/BaseFormRequest.php create mode 100644 app/Services/ApiKeyService.php create mode 100644 app/Services/ApiPermissionService.php create mode 100644 app/Services/Helpers/ApiAllowedIpsValidatorService.php create mode 100644 database/migrations/2017_06_25_133923_ChangeForeignKeyToBeOnCascadeDelete.php diff --git a/app/Http/Controllers/Base/APIController.php b/app/Http/Controllers/Base/APIController.php index 9c3816db..72995d00 100644 --- a/app/Http/Controllers/Base/APIController.php +++ b/app/Http/Controllers/Base/APIController.php @@ -25,22 +25,48 @@ namespace Pterodactyl\Http\Controllers\Base; -use Log; -use Alert; use Illuminate\Http\Request; use Pterodactyl\Models\APIKey; +use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Models\APIPermission; -use Pterodactyl\Repositories\APIRepository; -use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Services\ApiKeyService; use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Exceptions\DisplayValidationException; +use Pterodactyl\Http\Requests\ApiKeyRequest; class APIController extends Controller { + /** + * @var \Prologue\Alerts\AlertsMessageBag + */ + protected $alert; + + /** + * @var \Pterodactyl\Models\APIKey + */ + protected $model; + + /** + * @var \Pterodactyl\Services\ApiKeyService + */ + protected $service; + + /** + * APIController constructor. + * + * @param \Prologue\Alerts\AlertsMessageBag $alert + * @param \Pterodactyl\Services\ApiKeyService $service + */ + public function __construct(AlertsMessageBag $alert, ApiKeyService $service, APIKey $model) + { + $this->alert = $alert; + $this->model = $model; + $this->service = $service; + } + /** * Display base API index page. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ public function index(Request $request) @@ -53,15 +79,14 @@ class APIController extends Controller /** * Display API key creation page. * - * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ - public function create(Request $request) + public function create() { return view('base.api.new', [ 'permissions' => [ - 'user' => collect(APIPermission::permissions())->pull('_user'), - 'admin' => collect(APIPermission::permissions())->except('_user')->toArray(), + 'user' => collect(APIPermission::PERMISSIONS)->pull('_user'), + 'admin' => collect(APIPermission::PERMISSIONS)->except('_user')->toArray(), ], ]); } @@ -69,52 +94,46 @@ class APIController extends Controller /** * Handle saving new API key. * - * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Http\Requests\ApiKeyRequest $request * @return \Illuminate\Http\RedirectResponse + * + * @throws \Exception + * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function store(Request $request) + public function store(ApiKeyRequest $request) { - try { - $repo = new APIRepository($request->user()); - $secret = $repo->create($request->intersect([ - 'memo', 'allowed_ips', - 'admin_permissions', 'permissions', - ])); - Alert::success('An API Key-Pair has successfully been generated. The API secret for this public key is shown below and will not be shown again.

' . $secret . '')->flash(); - - return redirect()->route('account.api'); - } catch (DisplayValidationException $ex) { - return redirect()->route('account.api.new')->withErrors(json_decode($ex->getMessage()))->withInput(); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An unhandled exception occured while attempting to add this API key.')->flash(); + $adminPermissions = []; + if ($request->user()->isRootAdmin()) { + $adminPermissions = $request->input('admin_permissions') ?? []; } - return redirect()->route('account.api.new')->withInput(); + $secret = $this->service->create([ + 'user_id' => $request->user()->id, + 'allowed_ips' => $request->input('allowed_ips'), + 'memo' => $request->input('memo'), + ], $request->input('permissions') ?? [], $adminPermissions); + + $this->alert->success('An API Key-Pair has successfully been generated. The API secret for this public key is shown below and will not be shown again.

' . $secret . '')->flash(); + + return redirect()->route('account.api'); } /** - * Handle revoking API key. - * * @param \Illuminate\Http\Request $request * @param string $key - * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\Response + * @return \Illuminate\Http\Response + * + * @throws \Exception */ public function revoke(Request $request, $key) { - try { - $repo = new APIRepository($request->user()); - $repo->revoke($key); + $key = $this->model->newQuery() + ->where('user_id', $request->user()->id) + ->where('public', $key) + ->firstOrFail(); - return response('', 204); - } catch (\Exception $ex) { - Log::error($ex); + $this->service->revoke($key); - return response()->json([ - 'error' => 'An error occured while attempting to remove this key.', - ], 503); - } + return response('', 204); } } diff --git a/app/Http/Requests/Admin/AdminFormRequest.php b/app/Http/Requests/Admin/AdminFormRequest.php index e3e0be37..a92a89c6 100644 --- a/app/Http/Requests/Admin/AdminFormRequest.php +++ b/app/Http/Requests/Admin/AdminFormRequest.php @@ -24,7 +24,6 @@ namespace Pterodactyl\Http\Requests\Admin; -use Pterodactyl\Models\User; use Illuminate\Foundation\Http\FormRequest; abstract class AdminFormRequest extends FormRequest @@ -37,7 +36,7 @@ abstract class AdminFormRequest extends FormRequest */ public function authorize() { - if (! $this->user() instanceof User) { + if (is_null($this->user())) { return false; } diff --git a/app/Http/Requests/ApiKeyRequest.php b/app/Http/Requests/ApiKeyRequest.php new file mode 100644 index 00000000..52b8f90e --- /dev/null +++ b/app/Http/Requests/ApiKeyRequest.php @@ -0,0 +1,89 @@ +. + * + * 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\Http\Requests; + +use IPTools\Network; + +class ApiKeyRequest extends BaseFormRequest +{ + /** + * Rules applied to data passed in this request. + * + * @return array + */ + public function rules() + { + $this->parseAllowedIntoArray(); + + return [ + 'memo' => 'required|nullable|string|max:500', + 'permissions' => 'sometimes|present|array', + 'admin_permissions' => 'sometimes|present|array', + 'allowed_ips' => 'present', + 'allowed_ips.*' => 'sometimes|string', + ]; + } + + /** + * Parse the string of allowed IPs into an array. + */ + protected function parseAllowedIntoArray() + { + $loop = []; + if (! empty($this->input('allowed_ips'))) { + foreach (explode(PHP_EOL, $this->input('allowed_ips')) as $ip) { + $loop[] = trim($ip); + } + } + + $this->merge(['allowed_ips' => $loop], $this->except('allowed_ips')); + } + + /** + * Run additional validation rules on the request to ensure all of the data is good. + * + * @param \Illuminate\Validation\Validator $validator + */ + public function withValidator($validator) + { + $validator->after(function ($validator) { + if (empty($this->input('permissions')) && empty($this->input('admin_permissions'))) { + $validator->errors()->add('permissions', 'At least one permission must be selected.'); + } + }); + + $validator->after(function ($validator) { + foreach ($this->input('allowed_ips') as $ip) { + $ip = trim($ip); + + try { + Network::parse($ip); + } catch (\Exception $ex) { + $validator->errors()->add('allowed_ips', 'Could not parse IP ' . $ip . ' because it is in an invalid format.'); + } + } + }); + } +} diff --git a/app/Http/Requests/BaseFormRequest.php b/app/Http/Requests/BaseFormRequest.php new file mode 100644 index 00000000..7d5274bb --- /dev/null +++ b/app/Http/Requests/BaseFormRequest.php @@ -0,0 +1,53 @@ +. + * + * 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\Http\Requests; + +use Illuminate\Foundation\Http\FormRequest; + +class BaseFormRequest extends FormRequest +{ + /** + * Determine if a user is authorized to access this endpoint. + * + * @return bool + */ + public function authorize() + { + return ! is_null($this->user()); + } + + /** + * Return only the fields that we are interested in from the request. + * This will include empty fields as a null value. + * + * @return array + */ + public function normalize() + { + return $this->only( + array_keys($this->rules()) + ); + } +} diff --git a/app/Models/APIKey.php b/app/Models/APIKey.php index 6ed73b7c..b62b6eb2 100644 --- a/app/Models/APIKey.php +++ b/app/Models/APIKey.php @@ -24,16 +24,14 @@ namespace Pterodactyl\Models; +use Sofa\Eloquence\Eloquence; +use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; +use Sofa\Eloquence\Contracts\Validable as ValidableContract; -class APIKey extends Model +class APIKey extends Model implements ValidableContract { - /** - * Public key defined length used in verification methods. - * - * @var int - */ - const PUBLIC_KEY_LEN = 16; + use Eloquence, Validable; /** * The table associated with the model. @@ -65,6 +63,32 @@ class APIKey extends Model */ protected $guarded = ['id', 'created_at', 'updated_at']; + /** + * Rules defining what fields must be passed when making a model. + * + * @var array + */ + protected static $applicationRules = [ + 'memo' => 'required', + 'user_id' => 'required', + 'secret' => 'required', + 'public' => 'required', + ]; + + /** + * Rules to protect aganist invalid data entry to DB. + * + * @var array + */ + protected static $dataIntegrityRules = [ + 'user_id' => 'exists:users,id', + 'public' => 'string|size:16', + 'secret' => 'string', + 'memo' => 'nullable|string|max:500', + 'allowed_ips' => 'nullable|json', + 'expires_at' => 'nullable|datetime', + ]; + /** * Gets the permissions associated with a key. * diff --git a/app/Models/APIPermission.php b/app/Models/APIPermission.php index bfe2fc90..9361d31b 100644 --- a/app/Models/APIPermission.php +++ b/app/Models/APIPermission.php @@ -24,46 +24,19 @@ namespace Pterodactyl\Models; +use Sofa\Eloquence\Eloquence; +use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; +use Sofa\Eloquence\Contracts\Validable as ValidableContract; -class APIPermission extends Model +class APIPermission extends Model implements ValidableContract { - /** - * The table associated with the model. - * - * @var string - */ - protected $table = 'api_permissions'; - - /** - * Fields that are not mass assignable. - * - * @var array - */ - protected $guarded = ['id']; - - /** - * Cast values to correct type. - * - * @var array - */ - protected $casts = [ - 'key_id' => 'integer', - ]; - - /** - * Disable timestamps for this table. - * - * @var bool - */ - public $timestamps = false; + use Eloquence, Validable; /** * List of permissions available for the API. - * - * @var array */ - protected static $permissions = [ + const PERMISSIONS = [ // Items within this block are available to non-adminitrative users. '_user' => [ 'server' => [ @@ -119,13 +92,49 @@ class APIPermission extends Model ], ]; + /** + * The table associated with the model. + * + * @var string + */ + protected $table = 'api_permissions'; + + /** + * Fields that are not mass assignable. + * + * @var array + */ + protected $guarded = ['id']; + + /** + * Cast values to correct type. + * + * @var array + */ + protected $casts = [ + 'key_id' => 'integer', + ]; + + protected static $dataIntegrityRules = [ + 'key_id' => 'required|numeric', + 'permission' => 'required|string|max:200', + ]; + + /** + * Disable timestamps for this table. + * + * @var bool + */ + public $timestamps = false; + /** * Return permissions for API. * * @return array + * @deprecated */ public static function permissions() { - return self::$permissions; + return []; } } diff --git a/app/Services/ApiKeyService.php b/app/Services/ApiKeyService.php new file mode 100644 index 00000000..91e703ea --- /dev/null +++ b/app/Services/ApiKeyService.php @@ -0,0 +1,153 @@ +. + * + * 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; + +use Pterodactyl\Models\APIKey; +use Illuminate\Database\Connection; +use Illuminate\Contracts\Encryption\Encrypter; +use Pterodactyl\Exceptions\Model\DataValidationException; + +class ApiKeyService +{ + const PUB_CRYPTO_BYTES = 8; + const PRIV_CRYPTO_BYTES = 32; + + /** + * @var \Illuminate\Contracts\Encryption\Encrypter + */ + protected $encrypter; + + /** + * @var \Pterodactyl\Models\APIKey + */ + protected $model; + + /** + * @var \Pterodactyl\Services\ApiPermissionService + */ + protected $permissionService; + + /** + * ApiKeyService constructor. + * + * @param \Pterodactyl\Models\APIKey $model + * @param \Illuminate\Database\Connection $database + * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter + * @param \Pterodactyl\Services\ApiPermissionService $permissionService + */ + public function __construct( + APIKey $model, + Connection $database, + Encrypter $encrypter, + ApiPermissionService $permissionService + ) { + $this->database = $database; + $this->encrypter = $encrypter; + $this->model = $model; + $this->permissionService = $permissionService; + } + + /** + * Create a new API Key on the system with the given permissions. + * + * @param array $data + * @param array $permissions + * @param array $administrative + * @return string + * + * @throws \Exception + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function create(array $data, array $permissions, array $administrative = []) + { + $publicKey = bin2hex(random_bytes(self::PUB_CRYPTO_BYTES)); + $secretKey = bin2hex(random_bytes(self::PRIV_CRYPTO_BYTES)); + + // Start a Transaction + $this->database->beginTransaction(); + + $instance = $this->model->newInstance($data); + $instance->public = $publicKey; + $instance->secret = $this->encrypter->encrypt($secretKey); + + if (! $instance->save()) { + $this->database->rollBack(); + throw new DataValidationException($instance->getValidator()); + } + + $key = $instance->fresh(); + $nodes = $this->permissionService->getPermissions(); + + foreach ($permissions as $permission) { + @list($block, $search) = explode('-', $permission); + + if ( + (empty($block) || empty($search)) || + ! array_key_exists($block, $nodes['_user']) || + ! in_array($search, $nodes['_user'][$block]) + ) { + continue; + } + + $this->permissionService->create($key->id, sprintf('user.%s', $permission)); + } + + foreach ($administrative as $permission) { + @list($block, $search) = explode('-', $permission); + + if ( + (empty($block) || empty($search)) || + ! array_key_exists($block, $nodes) || + ! in_array($search, $nodes[$block]) + ) { + continue; + } + + $this->permissionService->create($key->id, $permission); + } + + $this->database->commit(); + + return $secretKey; + } + + /** + * Delete the API key and associated permissions from the database. + * + * @param int|\Pterodactyl\Models\APIKey $key + * @return bool|null + * + * @throws \Exception + * @throws \Illuminate\Database\Eloquent\ModelNotFoundException + */ + public function revoke($key) + { + if (! $key instanceof APIKey) { + $key = $this->model->findOrFail($key); + } + + return $key->delete(); + } +} diff --git a/app/Services/ApiPermissionService.php b/app/Services/ApiPermissionService.php new file mode 100644 index 00000000..20a722bf --- /dev/null +++ b/app/Services/ApiPermissionService.php @@ -0,0 +1,79 @@ +. + * + * 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; + +use Pterodactyl\Models\APIPermission; +use Pterodactyl\Exceptions\Model\DataValidationException; + +class ApiPermissionService +{ + /** + * @var \Pterodactyl\Models\APIPermission + */ + protected $model; + + /** + * ApiPermissionService constructor. + * + * @param \Pterodactyl\Models\APIPermission $model + */ + public function __construct(APIPermission $model) + { + $this->model = $model; + } + + /** + * Store a permission key in the database. + * + * @param string $key + * @param string $permission + * @return bool + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function create($key, $permission) + { + $instance = $this->model->newInstance([ + 'key_id' => $key, + 'permission' => $permission, + ]); + + if (! $instance->save()) { + throw new DataValidationException($instance->getValidator()); + } + + return true; + } + + /** + * Return all of the permissions available for an API Key. + * + * @return array + */ + public function getPermissions() + { + return APIPermission::PERMISSIONS; + } +} diff --git a/app/Services/Helpers/ApiAllowedIpsValidatorService.php b/app/Services/Helpers/ApiAllowedIpsValidatorService.php new file mode 100644 index 00000000..2b420f9c --- /dev/null +++ b/app/Services/Helpers/ApiAllowedIpsValidatorService.php @@ -0,0 +1,23 @@ +. + * + * 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. + */ diff --git a/database/migrations/2017_06_25_133923_ChangeForeignKeyToBeOnCascadeDelete.php b/database/migrations/2017_06_25_133923_ChangeForeignKeyToBeOnCascadeDelete.php new file mode 100644 index 00000000..17dbe822 --- /dev/null +++ b/database/migrations/2017_06_25_133923_ChangeForeignKeyToBeOnCascadeDelete.php @@ -0,0 +1,36 @@ +dropForeign(['key_id']); + + $table->foreign('key_id')->references('id')->on('api_keys')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('api_permissions', function (Blueprint $table) { + $table->dropForeign(['key_id']); + + $table->foreign('key_id')->references('id')->on('api_keys'); + }); + } +} From d90867264479b62691cd82a088dcad780e723c18 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 25 Jun 2017 15:37:45 -0500 Subject: [PATCH 17/99] Apply fixes from StyleCI (#519) --- app/Services/LocationService.php | 2 +- tests/Feature/Services/LocationServiceTest.php | 4 ++-- tests/Feature/Services/UserServiceTest.php | 4 ++-- tests/Unit/Services/UserServiceTest.php | 8 ++++---- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/Services/LocationService.php b/app/Services/LocationService.php index 72373e0e..a6139e0d 100644 --- a/app/Services/LocationService.php +++ b/app/Services/LocationService.php @@ -24,9 +24,9 @@ namespace Pterodactyl\Services; -use Pterodactyl\Exceptions\Model\DataValidationException; use Pterodactyl\Models\Location; use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Exceptions\Model\DataValidationException; class LocationService { diff --git a/tests/Feature/Services/LocationServiceTest.php b/tests/Feature/Services/LocationServiceTest.php index 176f8352..7e885070 100644 --- a/tests/Feature/Services/LocationServiceTest.php +++ b/tests/Feature/Services/LocationServiceTest.php @@ -192,7 +192,7 @@ class LocationServiceTest extends TestCase $this->service->update(0, []); } - /** + /* * Test that a location can be deleted normally when no nodes are attached. */ // public function testShouldDeleteExistingLocation() @@ -211,7 +211,7 @@ class LocationServiceTest extends TestCase // ]); // } - /** + /* * Test that a location cannot be deleted if a node is attached to it. * * @expectedException \Pterodactyl\Exceptions\DisplayException diff --git a/tests/Feature/Services/UserServiceTest.php b/tests/Feature/Services/UserServiceTest.php index cc743381..85775e5a 100644 --- a/tests/Feature/Services/UserServiceTest.php +++ b/tests/Feature/Services/UserServiceTest.php @@ -108,7 +108,7 @@ class UserServiceTest extends TestCase public function testShouldUpdateUserModelInDatabase() { -// $user = factory(User::class)->create(); + // $user = factory(User::class)->create(); // // $response = $this->service->update($user, [ // 'email' => 'test_change@example.com', @@ -126,7 +126,7 @@ class UserServiceTest extends TestCase public function testShouldDeleteUserFromDatabase() { -// $user = factory(User::class)->create(); + // $user = factory(User::class)->create(); // $service = $this->app->make(UserService::class); // // $response = $service->delete($user); diff --git a/tests/Unit/Services/UserServiceTest.php b/tests/Unit/Services/UserServiceTest.php index b97f37af..0f395195 100644 --- a/tests/Unit/Services/UserServiceTest.php +++ b/tests/Unit/Services/UserServiceTest.php @@ -24,14 +24,14 @@ namespace Tests\Unit\Services; -use Illuminate\Contracts\Hashing\Hasher; -use Illuminate\Database\Connection; use Mockery as m; +use Tests\TestCase; use Pterodactyl\Models\User; +use Illuminate\Database\Connection; +use Pterodactyl\Services\UserService; +use Illuminate\Contracts\Hashing\Hasher; use Pterodactyl\Notifications\AccountCreated; use Pterodactyl\Services\Helpers\TemporaryPasswordService; -use Pterodactyl\Services\UserService; -use Tests\TestCase; class UserServiceTest extends TestCase { From 5c3dc60d1ee03954f77250e63cc0d753fee5f4bd Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 1 Jul 2017 15:29:49 -0500 Subject: [PATCH 18/99] Addition of repository to ease testing and maintainability --- .../Repositories/RepositoryInterface.php | 164 ------------ .../Attributes/SearchableInterface.php} | 6 +- .../Repository/RepositoryInterface.php | 50 ++++ .../Repository/UserRepositoryInterface.php | 34 +++ .../Repository/RecordNotFoundException.php} | 14 +- app/Http/Controllers/Admin/UserController.php | 33 ++- app/Http/Requests/Admin/AdminFormRequest.php | 10 +- app/Http/Requests/Admin/UserFormRequest.php | 2 +- app/Models/User.php | 2 +- app/Providers/RepositoryServiceProvider.php | 40 +++ .../Eloquent/EloquentRepository.php | 150 +++++++++++ app/Repositories/Eloquent/UserRepository.php | 91 ++++--- app/Repositories/Repository.php | 215 +++++----------- app/Services/{ => Old}/APILogService.php | 0 app/Services/{ => Old}/DeploymentService.php | 0 app/Services/{ => Old}/VersionService.php | 0 app/Services/UserService.php | 117 ++++----- config/app.php | 1 + .../Feature/Services/LocationServiceTest.php | 236 ------------------ tests/Feature/Services/UserServiceTest.php | 140 ----------- tests/TestCase.php | 5 +- tests/Unit/Services/UserServiceTest.php | 160 +++++++++--- 22 files changed, 617 insertions(+), 853 deletions(-) delete mode 100644 app/Contracts/Repositories/RepositoryInterface.php rename app/Contracts/{Repositories/UserInterface.php => Repository/Attributes/SearchableInterface.php} (89%) create mode 100644 app/Contracts/Repository/RepositoryInterface.php create mode 100644 app/Contracts/Repository/UserRepositoryInterface.php rename app/{Contracts/Repositories/SearchableRepositoryInterface.php => Exceptions/Repository/RecordNotFoundException.php} (81%) create mode 100644 app/Providers/RepositoryServiceProvider.php create mode 100644 app/Repositories/Eloquent/EloquentRepository.php rename app/Services/{ => Old}/APILogService.php (100%) rename app/Services/{ => Old}/DeploymentService.php (100%) rename app/Services/{ => Old}/VersionService.php (100%) delete mode 100644 tests/Feature/Services/LocationServiceTest.php delete mode 100644 tests/Feature/Services/UserServiceTest.php diff --git a/app/Contracts/Repositories/RepositoryInterface.php b/app/Contracts/Repositories/RepositoryInterface.php deleted file mode 100644 index 16771e70..00000000 --- a/app/Contracts/Repositories/RepositoryInterface.php +++ /dev/null @@ -1,164 +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\Contracts\Repositories; - -use Illuminate\Container\Container; - -interface RepositoryInterface -{ - /** - * RepositoryInterface constructor. - * - * @param \Illuminate\Container\Container $container - */ - public function __construct(Container $container); - - /** - * Define the model class to be loaded. - * - * @return string - */ - public function model(); - - /** - * Returns the raw model class. - * - * @return \Illuminate\Database\Eloquent\Model - */ - public function getModel(); - - /** - * Make the model instance. - * - * @return \Illuminate\Database\Eloquent\Model - * @throws \Pterodactyl\Exceptions\Repository\RepositoryException - */ - public function makeModel(); - - /** - * Return all of the currently defined rules. - * - * @return array - */ - public function getRules(); - - /** - * Return the rules to apply when updating a model. - * - * @return array - */ - public function getUpdateRules(); - - /** - * Return the rules to apply when creating a model. - * - * @return array - */ - public function getCreateRules(); - - /** - * Add relations to a model for retrieval. - * - * @param array $params - * @return $this - */ - public function with(...$params); - - /** - * Add count of related items to model when retrieving. - * - * @param array $params - * @return $this - */ - public function withCount(...$params); - - /** - * Get all records from the database. - * - * @param array $columns - * @return mixed - */ - public function all(array $columns = ['*']); - - /** - * Return a paginated result set. - * - * @param int $limit - * @param array $columns - * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator - */ - public function paginate($limit = 15, array $columns = ['*']); - - /** - * Create a new record on the model. - * - * @param array $data - * @return \Illuminate\Database\Eloquent\Model - */ - public function create(array $data); - - /** - * Update the model. - * - * @param $attributes - * @param array $data - * @return int - */ - public function update($attributes, array $data); - - /** - * Delete a model from the database. Handles soft deletion. - * - * @param int $id - * @return mixed - */ - public function delete($id); - - /** - * Destroy the model from the database. Ignores soft deletion. - * - * @param int $id - * @return mixed - */ - public function destroy($id); - - /** - * Find a given model by ID or IDs. - * - * @param int|array $id - * @param array $columns - * @return \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection - */ - public function find($id, array $columns = ['*']); - - /** - * Finds the first record matching a passed array of values. - * - * @param array $attributes - * @param array $columns - * @return \Illuminate\Database\Eloquent\Model - */ - public function findBy(array $attributes, array $columns = ['*']); -} diff --git a/app/Contracts/Repositories/UserInterface.php b/app/Contracts/Repository/Attributes/SearchableInterface.php similarity index 89% rename from app/Contracts/Repositories/UserInterface.php rename to app/Contracts/Repository/Attributes/SearchableInterface.php index a7bf4964..37d9316e 100644 --- a/app/Contracts/Repositories/UserInterface.php +++ b/app/Contracts/Repository/Attributes/SearchableInterface.php @@ -22,9 +22,9 @@ * SOFTWARE. */ -namespace Pterodactyl\Contracts\Repositories; +namespace Pterodactyl\Contracts\Repository\Attributes; -interface UserInterface extends RepositoryInterface, SearchableRepositoryInterface +interface SearchableInterface { - // + public function search($term); } diff --git a/app/Contracts/Repository/RepositoryInterface.php b/app/Contracts/Repository/RepositoryInterface.php new file mode 100644 index 00000000..5eba06b1 --- /dev/null +++ b/app/Contracts/Repository/RepositoryInterface.php @@ -0,0 +1,50 @@ +. + * + * 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\Contracts\Repository; + +interface RepositoryInterface +{ + public function model(); + + public function getModel(); + + public function getBuilder(); + + public function getColumns(); + + public function withColumns($columns = ['*']); + + public function create($fields); + + public function delete($id); + + public function find($id); + + public function findWhere($fields); + + public function update($id, $fields); + + public function massUpdate($fields); +} diff --git a/app/Contracts/Repository/UserRepositoryInterface.php b/app/Contracts/Repository/UserRepositoryInterface.php new file mode 100644 index 00000000..cf61251e --- /dev/null +++ b/app/Contracts/Repository/UserRepositoryInterface.php @@ -0,0 +1,34 @@ +. + * + * 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\Contracts\Repository; + +use Pterodactyl\Contracts\Repository\Attributes\SearchableInterface; + +interface UserRepositoryInterface extends RepositoryInterface, SearchableInterface +{ + public function getAllUsersWithCounts(); + + public function deleteIfNoServers($id); +} diff --git a/app/Contracts/Repositories/SearchableRepositoryInterface.php b/app/Exceptions/Repository/RecordNotFoundException.php similarity index 81% rename from app/Contracts/Repositories/SearchableRepositoryInterface.php rename to app/Exceptions/Repository/RecordNotFoundException.php index 6a6b4537..932b83d1 100644 --- a/app/Contracts/Repositories/SearchableRepositoryInterface.php +++ b/app/Exceptions/Repository/RecordNotFoundException.php @@ -22,15 +22,11 @@ * SOFTWARE. */ -namespace Pterodactyl\Contracts\Repositories; +namespace Pterodactyl\Exceptions\Repository; -interface SearchableRepositoryInterface extends RepositoryInterface +use Illuminate\Database\Eloquent\ModelNotFoundException; + +class RecordNotFoundException extends ModelNotFoundException { - /** - * Pass parameters to search trait on model. - * - * @param string $term - * @return mixed - */ - public function search($term); + // } diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index 1be88880..40379f0f 100644 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -25,6 +25,7 @@ namespace Pterodactyl\Http\Controllers\Admin; use Illuminate\Http\Request; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Models\User; use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Services\UserService; @@ -49,18 +50,29 @@ class UserController extends Controller */ protected $model; + /** + * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface + */ + protected $repository; + /** * UserController constructor. * - * @param \Prologue\Alerts\AlertsMessageBag $alert - * @param \Pterodactyl\Services\UserService $service - * @param \Pterodactyl\Models\User $model + * @param \Prologue\Alerts\AlertsMessageBag $alert + * @param \Pterodactyl\Services\UserService $service + * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository + * @param \Pterodactyl\Models\User $model */ - public function __construct(AlertsMessageBag $alert, UserService $service, User $model) - { + public function __construct( + AlertsMessageBag $alert, + UserService $service, + UserRepositoryInterface $repository, + User $model + ) { $this->alert = $alert; $this->service = $service; $this->model = $model; + $this->repository = $repository; } /** @@ -71,14 +83,10 @@ class UserController extends Controller */ public function index(Request $request) { - $users = $this->model->newQuery()->withCount('servers', 'subuserOf'); - - if (! is_null($request->input('query'))) { - $users->search($request->input('query')); - } + $users = $this->repository->search($request->input('query'))->getAllUsersWithCounts(); return view('admin.users.index', [ - 'users' => $users->paginate(config('pterodactyl.paginate.admin.users')), + 'users' => $users, ]); } @@ -122,7 +130,7 @@ class UserController extends Controller } try { - $this->service->delete($user->id); + $this->repository->deleteIfNoServers($user->id); return redirect()->route('admin.users'); } catch (DisplayException $ex) { @@ -144,6 +152,7 @@ class UserController extends Controller public function store(UserFormRequest $request) { $user = $this->service->create($request->normalize()); + $this->alert->success('Account has been successfully created.')->flash(); return redirect()->route('admin.users.view', $user->id); diff --git a/app/Http/Requests/Admin/AdminFormRequest.php b/app/Http/Requests/Admin/AdminFormRequest.php index a92a89c6..769cf9dd 100644 --- a/app/Http/Requests/Admin/AdminFormRequest.php +++ b/app/Http/Requests/Admin/AdminFormRequest.php @@ -28,6 +28,8 @@ use Illuminate\Foundation\Http\FormRequest; abstract class AdminFormRequest extends FormRequest { + abstract public function rules(); + /** * Determine if the user is an admin and has permission to access this * form controller in the first place. @@ -47,12 +49,14 @@ abstract class AdminFormRequest extends FormRequest * Return only the fields that we are interested in from the request. * This will include empty fields as a null value. * + * @param array $only * @return array */ - public function normalize() + public function normalize($only = []) { - return $this->only( - array_keys($this->rules()) + return array_merge( + $this->only($only), + $this->intersect(array_keys($this->rules())) ); } } diff --git a/app/Http/Requests/Admin/UserFormRequest.php b/app/Http/Requests/Admin/UserFormRequest.php index c4878b7d..71e1a29d 100644 --- a/app/Http/Requests/Admin/UserFormRequest.php +++ b/app/Http/Requests/Admin/UserFormRequest.php @@ -40,7 +40,7 @@ class UserFormRequest extends AdminFormRequest return User::getCreateRules(); } - public function normalize() + public function normalize($only = []) { if ($this->method === 'PATCH') { return array_merge( diff --git a/app/Models/User.php b/app/Models/User.php index 6062e912..f95a5242 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -103,7 +103,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac * * @var array */ - protected $searchable = [ + protected $searchableColumns = [ 'email' => 10, 'username' => 9, 'name_first' => 6, diff --git a/app/Providers/RepositoryServiceProvider.php b/app/Providers/RepositoryServiceProvider.php new file mode 100644 index 00000000..d7b2d02d --- /dev/null +++ b/app/Providers/RepositoryServiceProvider.php @@ -0,0 +1,40 @@ +. + * + * 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\Providers; + +use Illuminate\Support\ServiceProvider; +use Pterodactyl\Repositories\Eloquent\UserRepository; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; + +class RepositoryServiceProvider extends ServiceProvider +{ + /** + * Register all of the repository bindings. + */ + public function register() + { + $this->app->bind(UserRepositoryInterface::class, UserRepository::class); + } +} diff --git a/app/Repositories/Eloquent/EloquentRepository.php b/app/Repositories/Eloquent/EloquentRepository.php new file mode 100644 index 00000000..614d8039 --- /dev/null +++ b/app/Repositories/Eloquent/EloquentRepository.php @@ -0,0 +1,150 @@ +. + * + * 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\Repositories\Eloquent; + +use Pterodactyl\Exceptions\Model\DataValidationException; +use Pterodactyl\Exceptions\Repository\RecordNotFoundException; +use Pterodactyl\Repository\Repository; +use Pterodactyl\Contracts\Repository\RepositoryInterface; + +abstract class EloquentRepository extends Repository implements RepositoryInterface +{ + /** + * @return \Illuminate\Database\Eloquent\Builder + */ + public function getBuilder() + { + return $this->getModel()->newQuery(); + } + + /** + * Create a new model instance and persist it to the database. + * @param array $fields + * @param bool $validate + * @param bool $force + * @return bool|\Illuminate\Database\Eloquent\Model + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function create($fields, $validate = true, $force = false) + { + $instance = $this->getBuilder()->newModelInstance(); + + if ($force) { + $instance->forceFill($fields); + } else { + $instance->fill($fields); + } + + if (! $validate) { + $saved = $instance->skipValidation()->save(); + } else { + if (! $saved = $instance->save()) { + throw new DataValidationException($instance->getValidator()); + } + } + + return ($this->withFresh) ? $instance->fresh() : $saved; + } + + /** + * Return a record from the database for a given ID. + * + * @param int $id + * @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Model + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function find($id) + { + $instance = $this->getBuilder()->find($id, $this->getColumns()); + + if (! $instance) { + throw new RecordNotFoundException(); + } + + return $instance; + } + + public function findWhere($fields) + { + // TODO: Implement findWhere() method. + } + + /** + * Delete a record from the DB given an ID. + * + * @param int $id + * @param bool $destroy + * @return bool|null + */ + public function delete($id, $destroy = false) + { + if ($destroy) { + return $this->getBuilder()->where($this->getModel()->getKeyName(), $id)->forceDelete(); + } + + return $this->getBuilder()->where($this->getModel()->getKeyName(), $id)->delete(); + } + + /** + * @param int $id + * @param array $fields + * @param bool $validate + * @param bool $force + * @return mixed + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function update($id, $fields, $validate = true, $force = false) + { + $instance = $this->getBuilder()->where('id', $id)->first(); + + if (! $instance) { + throw new RecordNotFoundException(); + } + + if ($force) { + $instance->forceFill($fields); + } else { + $instance->fill($fields); + } + + if (! $validate) { + $saved = $instance->skipValidation()->save(); + } else { + if (! $saved = $instance->save()) { + throw new DataValidationException($instance->getValidator()); + } + } + + return ($this->withFresh) ? $instance->fresh($this->getColumns()) : $saved; + } + + public function massUpdate($fields) + { + // TODO: Implement massUpdate() method. + } +} diff --git a/app/Repositories/Eloquent/UserRepository.php b/app/Repositories/Eloquent/UserRepository.php index 2ca9c521..96b85ffd 100644 --- a/app/Repositories/Eloquent/UserRepository.php +++ b/app/Repositories/Eloquent/UserRepository.php @@ -24,52 +24,85 @@ namespace Pterodactyl\Repositories\Eloquent; -use Pterodactyl\Models\User; -use Illuminate\Contracts\Auth\Guard; -use Pterodactyl\Repositories\Repository; +use Illuminate\Contracts\Config\Repository as ConfigRepository; +use Illuminate\Foundation\Application; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Contracts\Repositories\UserInterface; +use Pterodactyl\Exceptions\Repository\RecordNotFoundException; +use Pterodactyl\Models\User; -class UserRepository extends Repository implements UserInterface +class UserRepository extends EloquentRepository implements UserRepositoryInterface { /** - * Dependencies to automatically inject into the repository. - * - * @var array + * @var \Illuminate\Contracts\Config\Repository */ - protected $inject = [ - 'guard' => Guard::class, - ]; + protected $config; /** - * Return the model to be used for the repository. - * - * @return string + * @var bool|array */ + protected $searchTerm = false; + + /** + * UserRepository constructor. + * + * @param \Illuminate\Foundation\Application $application + * @param \Illuminate\Contracts\Config\Repository $config + */ + public function __construct(Application $application, ConfigRepository $config) + { + parent::__construct($application); + + $this->config = $config; + } + public function model() { return User::class; } - /** - * {@inheritdoc} - */ public function search($term) { - $this->model->search($term); - - return $this; - } - - public function delete($id) - { - $user = $this->model->withCount('servers')->find($id); - - if ($this->guard->user() && $this->guard->user()->id === $user->id) { - throw new DisplayException('You cannot delete your own account.'); + if (empty($term)) { + return $this; } - if ($user->server_count > 0) { + $clone = clone $this; + $clone->searchTerm = $term; + + return $clone; + } + + public function getAllUsersWithCounts() + { + $users = $this->getBuilder()->withCount('servers', 'subuserOf'); + + if ($this->searchTerm) { + $users->search($this->searchTerm); + } + + return $users->paginate( + $this->config->get('pterodactyl.paginate.admin.users'), $this->getColumns() + ); + } + + /** + * Delete a user if they have no servers attached to their account. + * + * @param int $id + * @return bool + * + * @throws \Pterodactyl\Exceptions\DisplayException + */ + public function deleteIfNoServers($id) + { + $user = $this->getBuilder()->withCount('servers')->where('id', $id)->first(); + + if (! $user) { + throw new RecordNotFoundException(); + } + + if ($user->servers_count > 0) { throw new DisplayException('Cannot delete an account that has active servers attached to it.'); } diff --git a/app/Repositories/Repository.php b/app/Repositories/Repository.php index 7bf05920..32beb8cb 100644 --- a/app/Repositories/Repository.php +++ b/app/Repositories/Repository.php @@ -22,67 +22,77 @@ * SOFTWARE. */ -namespace Pterodactyl\Repositories; +namespace Pterodactyl\Repository; -use Illuminate\Container\Container; -use Illuminate\Database\Eloquent\Model; -use Pterodactyl\Exceptions\Repository\RepositoryException; -use Pterodactyl\Contracts\Repositories\RepositoryInterface; +use Illuminate\Foundation\Application; +use Pterodactyl\Contracts\Repository\RepositoryInterface; abstract class Repository implements RepositoryInterface { - const RULE_UPDATED = 'updated'; - const RULE_CREATED = 'created'; - /** - * @var \Illuminate\Container\Container + * @var \Illuminate\Foundation\Application */ - protected $container; + protected $app; /** - * Array of classes to inject automatically into the container. - * * @var array */ - protected $inject = []; + protected $columns = ['*']; /** - * @var \Illuminate\Database\Eloquent\Model + * @var mixed */ protected $model; /** - * Array of validation rules that can be accessed from this repository. - * - * @var array + * @var bool */ - protected $rules = []; + protected $withFresh = true; /** - * {@inheritdoc} + * Repository constructor. + * + * @param \Illuminate\Foundation\Application $application */ - public function __construct(Container $container) + public function __construct(Application $application) { - $this->container = $container; + $this->app = $application; - foreach ($this->inject as $key => $value) { - if (isset($this->{$key})) { - throw new \Exception('Cannot override a defined object in this class.'); - } - - $this->{$key} = $this->container->make($value); - } - - $this->makeModel(); + $this->setModel($this->model()); } /** - * {@inheritdoc} + * Take the provided model and make it accessible to the rest of the repository. + * + * @param string|array $model + * @return mixed + */ + protected function setModel($model) + { + if (is_array($model)) { + if (count($model) !== 2) { + throw new \InvalidArgumentException( + printf('setModel expected exactly 2 parameters, %d received.', count($model)) + ); + } + + return $this->model = call_user_func( + $model[1], $this->app->make($model[0]) + ); + } + + return $this->model = $this->app->make($model); + } + + /** + * @return mixed */ abstract public function model(); /** - * {@inheritdoc} + * Return the model being used for this repository. + * + * @return mixed */ public function getModel() { @@ -90,140 +100,39 @@ abstract class Repository implements RepositoryInterface } /** - * {@inheritdoc} + * Setup column selection functionality. + * + * @param array $columns + * @return $this */ - public function makeModel() + public function withColumns($columns = ['*']) { - $model = $this->container->make($this->model()); + $clone = clone $this; + $clone->columns = is_array($columns) ? $columns : func_get_args(); - if (! $model instanceof Model) { - throw new RepositoryException( - "Class {$this->model()} must be an instance of \\Illuminate\\Database\\Eloquent\\Model" - ); - } - - return $this->model = $model->newQuery(); + return $clone; } /** - * {@inheritdoc} + * Return the columns to be selected in the repository call. + * + * @return array */ - public function getRules() + public function getColumns() { - return $this->rules; + return $this->columns; } /** - * {@inheritdoc} + * Set repository to not return a fresh record from the DB when completed. + * + * @return $this */ - public function getUpdateRules() + public function withoutFresh() { - if (array_key_exists(self::RULE_UPDATED, $this->rules)) { - return $this->rules[self::RULE_UPDATED]; - } + $clone = clone $this; + $clone->withFresh = false; - return []; - } - - /** - * {@inheritdoc} - */ - public function getCreateRules() - { - if (array_key_exists(self::RULE_CREATED, $this->rules)) { - return $this->rules[self::RULE_CREATED]; - } - - return []; - } - - /** - * {@inheritdoc} - */ - public function with(...$params) - { - $this->model = $this->model->with($params); - - return $this; - } - - /** - * {@inheritdoc} - */ - public function withCount(...$params) - { - $this->model = $this->model->withCount($params); - - return $this; - } - - /** - * {@inheritdoc} - */ - public function all(array $columns = ['*']) - { - return $this->model->get($columns); - } - - /** - * {@inheritdoc} - */ - public function paginate($limit = 15, array $columns = ['*']) - { - return $this->model->paginate($limit, $columns); - } - - /** - * {@inheritdoc} - */ - public function create(array $data) - { - return $this->model->create($data); - } - - /** - * {@inheritdoc} - */ - public function update($attributes, array $data) - { - // If only a number is passed, we assume it is an ID - // for the specific model at hand. - if (is_numeric($attributes)) { - $attributes = [['id', '=', $attributes]]; - } - - return $this->model->where($attributes)->get()->each->update($data); - } - - /** - * {@inheritdoc} - */ - public function delete($id) - { - return $this->model->find($id)->delete(); - } - - /** - * {@inheritdoc} - */ - public function destroy($id) - { - return $this->model->find($id)->forceDelete(); - } - - /** - * {@inheritdoc} - */ - public function find($id, array $columns = ['*']) - { - return $this->model->find($id, $columns); - } - - /** - * {@inheritdoc} - */ - public function findBy(array $attributes, array $columns = ['*']) - { - return $this->model->where($attributes)->first($columns); + return $clone; } } diff --git a/app/Services/APILogService.php b/app/Services/Old/APILogService.php similarity index 100% rename from app/Services/APILogService.php rename to app/Services/Old/APILogService.php diff --git a/app/Services/DeploymentService.php b/app/Services/Old/DeploymentService.php similarity index 100% rename from app/Services/DeploymentService.php rename to app/Services/Old/DeploymentService.php diff --git a/app/Services/VersionService.php b/app/Services/Old/VersionService.php similarity index 100% rename from app/Services/VersionService.php rename to app/Services/Old/VersionService.php diff --git a/app/Services/UserService.php b/app/Services/UserService.php index 11efcddb..a7c87c57 100644 --- a/app/Services/UserService.php +++ b/app/Services/UserService.php @@ -24,16 +24,21 @@ namespace Pterodactyl\Services; -use Pterodactyl\Models\User; -use Illuminate\Database\Connection; +use Illuminate\Foundation\Application; use Illuminate\Contracts\Hashing\Hasher; -use Pterodactyl\Exceptions\DisplayException; +use Illuminate\Database\ConnectionInterface; +use Illuminate\Notifications\ChannelManager; use Pterodactyl\Notifications\AccountCreated; -use Pterodactyl\Exceptions\Model\DataValidationException; use Pterodactyl\Services\Helpers\TemporaryPasswordService; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; class UserService { + /** + * @var \Illuminate\Foundation\Application + */ + protected $app; + /** * @var \Illuminate\Database\Connection */ @@ -44,34 +49,45 @@ class UserService */ protected $hasher; + /** + * @var \Illuminate\Notifications\ChannelManager + */ + protected $notification; + /** * @var \Pterodactyl\Services\Helpers\TemporaryPasswordService */ protected $passwordService; /** - * @var \Pterodactyl\Models\User + * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface */ - protected $model; + protected $repository; /** * UserService constructor. * - * @param \Illuminate\Database\Connection $database - * @param \Illuminate\Contracts\Hashing\Hasher $hasher - * @param \Pterodactyl\Services\Helpers\TemporaryPasswordService $passwordService - * @param \Pterodactyl\Models\User $model + * @param \Illuminate\Foundation\Application $application + * @param \Illuminate\Notifications\ChannelManager $notification + * @param \Illuminate\Database\ConnectionInterface $database + * @param \Illuminate\Contracts\Hashing\Hasher $hasher + * @param \Pterodactyl\Services\Helpers\TemporaryPasswordService $passwordService + * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository */ public function __construct( - Connection $database, + Application $application, + ChannelManager $notification, + ConnectionInterface $database, Hasher $hasher, TemporaryPasswordService $passwordService, - User $model + UserRepositoryInterface $repository ) { + $this->app = $application; $this->database = $database; $this->hasher = $hasher; + $this->notification = $notification; $this->passwordService = $passwordService; - $this->model = $model; + $this->repository = $repository; } /** @@ -89,78 +105,47 @@ class UserService $data['password'] = $this->hasher->make($data['password']); } - $user = $this->model->newInstance($data); + // Begin Transaction + $this->database->beginTransaction(); + + if (! isset($data['password']) || empty($data['password'])) { + $data['password'] = $this->hasher->make(str_random(30)); + $token = $this->passwordService->generateReset($data['email']); + } + + $user = $this->repository->create($data); // Persist the data - $token = $this->database->transaction(function () use ($user) { - if (empty($user->password)) { - $user->password = $this->hasher->make(str_random(30)); - $token = $this->passwordService->generateReset($user->email); - } + $this->database->commit(); - if (! $user->save()) { - throw new DataValidationException($user->getValidator()); - } - - return $token ?? null; - }); - - $user->notify(new AccountCreated([ - 'name' => $user->name_first, - 'username' => $user->username, - 'token' => $token, + $this->notification->send($user, $this->app->makeWith(AccountCreated::class, [ + 'user' => [ + 'name' => $user->name_first, + 'username' => $user->username, + 'token' => $token ?? null, + ], ])); return $user; } /** - * Update the user model. + * Update the user model instance. * - * @param int|\Pterodactyl\Models\User $user - * @param array $data - * @return \Pterodactyl\Models\User + * @param int $id + * @param array $data + * @return mixed * - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function update($user, array $data) + public function update($id, array $data) { - if (! $user instanceof User) { - $user = $this->model->findOrFail($user); - } - if (isset($data['password'])) { $data['password'] = $this->hasher->make($data['password']); } - $user->fill($data); - - if (! $user->save()) { - throw new DataValidationException($user->getValidator()); - } + $user = $this->repository->update($id, $data); return $user; } - - /** - * @param int|\Pterodactyl\Models\User $user - * @return bool|null - * - * @throws \Exception - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException - */ - public function delete($user) - { - if (! $user instanceof User) { - $user = $this->model->findOrFail($user); - } - - if ($user->servers()->count() > 0) { - throw new DisplayException('Cannot delete an account that has active servers attached to it.'); - } - - return $user->delete(); - } } diff --git a/config/app.php b/config/app.php index 394682c2..c211d37f 100644 --- a/config/app.php +++ b/config/app.php @@ -166,6 +166,7 @@ return [ Pterodactyl\Providers\RouteServiceProvider::class, Pterodactyl\Providers\MacroServiceProvider::class, Pterodactyl\Providers\PhraseAppTranslationProvider::class, + Pterodactyl\Providers\RepositoryServiceProvider::class, /* * Additional Dependencies diff --git a/tests/Feature/Services/LocationServiceTest.php b/tests/Feature/Services/LocationServiceTest.php deleted file mode 100644 index 7e885070..00000000 --- a/tests/Feature/Services/LocationServiceTest.php +++ /dev/null @@ -1,236 +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\Feature\Services; - -use Tests\TestCase; -use Pterodactyl\Models\Node; -use Pterodactyl\Models\Location; -use Pterodactyl\Services\LocationService; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Exceptions\Model\DataValidationException; - -class LocationServiceTest extends TestCase -{ - /** - * @var \Pterodactyl\Services\LocationService - */ - protected $service; - - /** - * Setup the test instance. - */ - public function setUp() - { - parent::setUp(); - - $this->service = $this->app->make(LocationService::class); - } - - /** - * Test that a new location can be successfully added to the database. - */ - public function testShouldCreateANewLocation() - { - $data = [ - 'long' => 'Long Name', - 'short' => 'short', - ]; - - $response = $this->service->create($data); - - $this->assertInstanceOf(Location::class, $response); - $this->assertEquals($data['long'], $response->long); - $this->assertEquals($data['short'], $response->short); - $this->assertDatabaseHas('locations', [ - 'short' => $data['short'], - 'long' => $data['long'], - ]); - } - - /** - * Test that a validation error is thrown if a required field is missing. - */ - public function testShouldFailToCreateLocationIfMissingParameter() - { - $data = ['long' => 'Long Name']; - - try { - $this->service->create($data); - } catch (DataValidationException $ex) { - $this->assertInstanceOf(DataValidationException::class, $ex); - - $bag = $ex->getMessageBag()->messages(); - $this->assertArraySubset(['short' => [0]], $bag); - $this->assertEquals('The short field is required.', $bag['short'][0]); - } - } - - /** - * Test that a validation error is thrown if the short code provided is already in use. - */ -// public function testShouldFailToCreateLocationIfShortCodeIsAlreadyInUse() -// { -// factory(Location::class)->create(['short' => 'inuse']); -// $data = [ -// 'long' => 'Long Name', -// 'short' => 'inuse', -// ]; -// -// try { -// $this->service->create($data); -// } catch (\Exception $ex) { -// $this->assertInstanceOf(DataValidationException::class, $ex); -// -// $bag = $ex->getMessageBag()->messages(); -// $this->assertArraySubset(['short' => [0]], $bag); -// $this->assertEquals('The short has already been taken.', $bag['short'][0]); -// } -// } - - /** - * Test that a validation error is thrown if the short code is too long. - */ - public function testShouldFailToCreateLocationIfShortCodeIsTooLong() - { - $data = [ - 'long' => 'Long Name', - 'short' => str_random(200), - ]; - - try { - $this->service->create($data); - } catch (\Exception $ex) { - $this->assertInstanceOf(DataValidationException::class, $ex); - - $bag = $ex->getMessageBag()->messages(); - $this->assertArraySubset(['short' => [0]], $bag); - $this->assertEquals('The short must be between 1 and 60 characters.', $bag['short'][0]); - } - } - - /** - * Test that updating a model returns the updated data in a persisted form. - */ -// public function testShouldUpdateLocationModelInDatabase() -// { -// $location = factory(Location::class)->create(); -// $data = ['short' => 'test_short']; -// -// $model = $this->service->update($location->id, $data); -// -// $this->assertInstanceOf(Location::class, $model); -// $this->assertEquals($data['short'], $model->short); -// $this->assertNotEquals($model->short, $location->short); -// $this->assertEquals($location->long, $model->long); -// $this->assertDatabaseHas('locations', [ -// 'short' => $data['short'], -// 'long' => $location->long, -// ]); -// } - - /** - * Test that passing the same short-code into the update function as the model - * is currently using will not throw a validation exception. - */ -// public function testShouldUpdateModelWithoutErrorWhenValidatingShortCodeIsUnique() -// { -// $location = factory(Location::class)->create(); -// $data = ['short' => $location->short]; -// -// $model = $this->service->update($location->id, $data); -// -// $this->assertInstanceOf(Location::class, $model); -// $this->assertEquals($model->short, $location->short); -// -// // Timestamps don't change if no data is modified. -// $this->assertEquals($model->updated_at, $location->updated_at); -// } - - /** - * Test that passing invalid data to the update method will throw a validation - * exception. - * - * @expectedException \Watson\Validating\ValidationException - */ -// public function testShouldNotUpdateModelIfPassedDataIsInvalid() -// { -// $location = factory(Location::class)->create(); -// $data = ['short' => str_random(200)]; -// -// $this->service->update($location->id, $data); -// } - - /** - * Test that an invalid model exception is thrown if a model doesn't exist. - * - * @expectedException \Illuminate\Database\Eloquent\ModelNotFoundException - */ - public function testShouldThrowExceptionIfInvalidModelIdIsProvided() - { - $this->service->update(0, []); - } - - /* - * Test that a location can be deleted normally when no nodes are attached. - */ -// public function testShouldDeleteExistingLocation() -// { -// $location = factory(Location::class)->create(); -// -// $this->assertDatabaseHas('locations', [ -// 'id' => $location->id, -// ]); -// -// $model = $this->service->delete($location); -// -// $this->assertTrue($model); -// $this->assertDatabaseMissing('locations', [ -// 'id' => $location->id, -// ]); -// } - - /* - * Test that a location cannot be deleted if a node is attached to it. - * - * @expectedException \Pterodactyl\Exceptions\DisplayException - */ -// public function testShouldFailToDeleteExistingLocationWithAttachedNodes() -// { -// $location = factory(Location::class)->create(); -// $node = factory(Node::class)->create(['location_id' => $location->id]); -// -// $this->assertDatabaseHas('locations', ['id' => $location->id]); -// $this->assertDatabaseHas('nodes', ['id' => $node->id]); -// -// try { -// $this->service->delete($location->id); -// } catch (\Exception $ex) { -// $this->assertInstanceOf(DisplayException::class, $ex); -// $this->assertNotEmpty($ex->getMessage()); -// -// throw $ex; -// } -// } -} diff --git a/tests/Feature/Services/UserServiceTest.php b/tests/Feature/Services/UserServiceTest.php deleted file mode 100644 index 85775e5a..00000000 --- a/tests/Feature/Services/UserServiceTest.php +++ /dev/null @@ -1,140 +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\Feature\Services; - -use Tests\TestCase; -use Pterodactyl\Models\User; -use Pterodactyl\Services\UserService; -use Illuminate\Support\Facades\Notification; -use Pterodactyl\Notifications\AccountCreated; - -class UserServiceTest extends TestCase -{ - protected $service; - - public function setUp() - { - parent::setUp(); - - $this->service = $this->app->make(UserService::class); - } - - public function testShouldReturnNewUserWithValidData() - { - Notification::fake(); - - $user = $this->service->create([ - 'email' => 'test_account@example.com', - 'username' => 'test_account', - 'password' => 'test_password', - 'name_first' => 'Test', - 'name_last' => 'Account', - 'root_admin' => false, - ]); - - $this->assertNotNull($user->uuid); - $this->assertNotEquals($user->password, 'test_password'); - - $this->assertDatabaseHas('users', [ - 'id' => $user->id, - 'uuid' => $user->uuid, - 'email' => 'test_account@example.com', - 'root_admin' => '0', - ]); - - Notification::assertSentTo($user, AccountCreated::class, function ($notification) use ($user) { - $this->assertEquals($user->username, $notification->user->username); - $this->assertNull($notification->user->token); - - return true; - }); - } - - public function testShouldReturnNewUserWithPasswordTokenIfNoPasswordProvided() - { - Notification::fake(); - - $user = $this->service->create([ - 'email' => 'test_account@example.com', - 'username' => 'test_account', - 'name_first' => 'Test', - 'name_last' => 'Account', - 'root_admin' => false, - ]); - - $this->assertNotNull($user->uuid); - $this->assertNotNull($user->password); - - $this->assertDatabaseHas('users', [ - 'id' => $user->id, - 'uuid' => $user->uuid, - 'email' => 'test_account@example.com', - 'root_admin' => '0', - ]); - - Notification::assertSentTo($user, AccountCreated::class, function ($notification) use ($user) { - $this->assertEquals($user->username, $notification->user->username); - $this->assertNotNull($notification->user->token); - - $this->assertDatabaseHas('password_resets', [ - 'email' => $user->email, - ]); - - return true; - }); - } - - public function testShouldUpdateUserModelInDatabase() - { - // $user = factory(User::class)->create(); -// -// $response = $this->service->update($user, [ -// 'email' => 'test_change@example.com', -// 'password' => 'test_password', -// ]); -// -// $this->assertInstanceOf(User::class, $response); -// $this->assertEquals('test_change@example.com', $response->email); -// $this->assertNotEquals($response->password, 'test_password'); -// $this->assertDatabaseHas('users', [ -// 'id' => $user->id, -// 'email' => 'test_change@example.com', -// ]); - } - - public function testShouldDeleteUserFromDatabase() - { - // $user = factory(User::class)->create(); -// $service = $this->app->make(UserService::class); -// -// $response = $service->delete($user); -// -// $this->assertTrue($response); -// $this->assertDatabaseMissing('users', [ -// 'id' => $user->id, -// 'uuid' => $user->uuid, -// ]); - } -} diff --git a/tests/TestCase.php b/tests/TestCase.php index c00fcc60..d8c7f6ff 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,15 +2,16 @@ namespace Tests; -use Illuminate\Foundation\Testing\DatabaseTransactions; +use Mockery as m; use Illuminate\Foundation\Testing\TestCase as BaseTestCase; abstract class TestCase extends BaseTestCase { - use CreatesApplication, DatabaseTransactions; + use CreatesApplication; public function setUp() { parent::setUp(); + m::close(); } } diff --git a/tests/Unit/Services/UserServiceTest.php b/tests/Unit/Services/UserServiceTest.php index 0f395195..ede6adee 100644 --- a/tests/Unit/Services/UserServiceTest.php +++ b/tests/Unit/Services/UserServiceTest.php @@ -26,85 +26,177 @@ namespace Tests\Unit\Services; use Mockery as m; use Tests\TestCase; -use Pterodactyl\Models\User; -use Illuminate\Database\Connection; use Pterodactyl\Services\UserService; +use Illuminate\Foundation\Application; use Illuminate\Contracts\Hashing\Hasher; +use Illuminate\Database\ConnectionInterface; +use Illuminate\Notifications\ChannelManager; use Pterodactyl\Notifications\AccountCreated; use Pterodactyl\Services\Helpers\TemporaryPasswordService; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; class UserServiceTest extends TestCase { + /** + * @var \Illuminate\Foundation\Application + */ + protected $appMock; + + /** + * @var \Illuminate\Database\ConnectionInterface + */ protected $database; + /** + * @var \Illuminate\Contracts\Hashing\Hasher + */ protected $hasher; - protected $model; + /** + * @var \Illuminate\Notifications\ChannelManager + */ + protected $notification; + /** + * @var \Pterodactyl\Services\Helpers\TemporaryPasswordService + */ protected $passwordService; + /** + * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\UserService + */ protected $service; + /** + * Setup tests. + */ public function setUp() { parent::setUp(); - $this->database = m::mock(Connection::class); + $this->appMock = m::mock(Application::class); + $this->database = m::mock(ConnectionInterface::class); $this->hasher = m::mock(Hasher::class); + $this->notification = m::mock(ChannelManager::class); $this->passwordService = m::mock(TemporaryPasswordService::class); - $this->model = m::mock(User::class); - $this->app->instance(AccountCreated::class, m::mock(AccountCreated::class)); + $this->repository = m::mock(UserRepositoryInterface::class); $this->service = new UserService( + $this->appMock, + $this->notification, $this->database, $this->hasher, $this->passwordService, - $this->model + $this->repository ); } - public function tearDown() + /** + * Test that a user is created when a password is passed. + */ + public function test_user_creation_with_password() { - parent::tearDown(); - m::close(); - } + $user = (object) [ + 'name_first' => 'FirstName', + 'username' => 'user_name', + ]; - public function testCreateFunction() - { - $data = ['password' => 'password']; + $this->hasher->shouldReceive('make')->with('raw-password')->once()->andReturn('enc-password'); + $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->hasher->shouldNotReceive('make'); + $this->passwordService->shouldNotReceive('generateReset'); + $this->repository->shouldReceive('create')->with(['password' => 'enc-password'])->once()->andReturn($user); + $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + $this->appMock->shouldReceive('makeWith')->with(AccountCreated::class, [ + 'user' => [ + 'name' => 'FirstName', + 'username' => 'user_name', + 'token' => null, + ], + ])->once()->andReturnNull(); - $this->hasher->shouldReceive('make')->once()->with($data['password'])->andReturn('hashString'); - $this->database->shouldReceive('transaction')->andReturnNull(); + $this->notification->shouldReceive('send')->with($user, null)->once()->andReturnNull(); - $this->model->shouldReceive('newInstance')->with(['password' => 'hashString'])->andReturnSelf(); - $this->model->shouldReceive('save')->andReturn(true); - $this->model->shouldReceive('notify')->with(m::type(AccountCreated::class))->andReturnNull(); - $this->model->shouldReceive('getAttribute')->andReturnSelf(); - - $response = $this->service->create($data); + $response = $this->service->create([ + 'password' => 'raw-password', + ]); $this->assertNotNull($response); - $this->assertInstanceOf(User::class, $response); + $this->assertEquals($user->username, $response->username); + $this->assertEquals($user->name_first, 'FirstName'); } - public function testCreateFunctionWithoutPassword() + /** + * Test that a user is created with a random password when no password is provided. + */ + public function test_user_creation_without_password() { - $data = ['email' => 'user@example.com']; + $user = (object) [ + 'name_first' => 'FirstName', + 'username' => 'user_name', + 'email' => 'user@example.com', + ]; $this->hasher->shouldNotReceive('make'); - $this->model->shouldReceive('newInstance')->with($data)->andReturnSelf(); + $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->hasher->shouldReceive('make')->once()->andReturn('created-enc-password'); + $this->passwordService->shouldReceive('generateReset')->with('user@example.com')->once()->andReturn('random-token'); - $this->database->shouldReceive('transaction')->andReturn('authToken'); - $this->hasher->shouldReceive('make')->andReturn('randomString'); - $this->passwordService->shouldReceive('generateReset')->with($data['email'])->andReturn('authToken'); - $this->model->shouldReceive('save')->withNoArgs()->andReturn(true); + $this->repository->shouldReceive('create')->with([ + 'password' => 'created-enc-password', + 'email' => 'user@example.com', + ])->once()->andReturn($user); - $this->model->shouldReceive('notify')->with(m::type(AccountCreated::class))->andReturnNull(); - $this->model->shouldReceive('getAttribute')->andReturnSelf(); + $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + $this->appMock->shouldReceive('makeWith')->with(AccountCreated::class, [ + 'user' => [ + 'name' => 'FirstName', + 'username' => 'user_name', + 'token' => 'random-token', + ], + ])->once()->andReturnNull(); - $response = $this->service->create($data); + $this->notification->shouldReceive('send')->with($user, null)->once()->andReturnNull(); + + $response = $this->service->create([ + 'email' => 'user@example.com', + ]); $this->assertNotNull($response); - $this->assertInstanceOf(User::class, $response); + $this->assertEquals($user->username, $response->username); + $this->assertEquals($user->name_first, 'FirstName'); + $this->assertEquals($user->email, $response->email); } + + /** + * Test that passing no password will not attempt any hashing. + */ + public function test_user_update_without_password() + { + $this->hasher->shouldNotReceive('make'); + $this->repository->shouldReceive('update')->with(1, ['email' => 'new@example.com'])->once()->andReturnNull(); + + $response = $this->service->update(1, ['email' => 'new@example.com']); + + $this->assertNull($response); + } + + /** + * Test that passing a password will hash it before storage. + */ + public function test_user_update_with_password() + { + $this->hasher->shouldReceive('make')->with('password')->once()->andReturn('enc-password'); + $this->repository->shouldReceive('update')->with(1, ['password' => 'enc-password'])->once()->andReturnNull(); + + $response = $this->service->update(1, ['password' => 'password']); + + $this->assertNull($response); + } + } From 50588a1f545c97fbdea6b60cd0b87684c26478d4 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 2 Jul 2017 21:29:58 -0500 Subject: [PATCH 19/99] Update location and databasehost services to use repositories Includes unit tests for both services --- .../Repository/DatabaseHostInterface.php | 30 +++ .../LocationRepositoryInterface.php | 32 +++ app/Extensions/DynamicDatabaseConnection.php | 19 +- app/Providers/RepositoryServiceProvider.php | 6 + .../Eloquent/DatabaseHostRepository.php | 67 ++++++ .../Eloquent/LocationRepository.php | 90 ++++++++ app/Services/DatabaseHostService.php | 56 +++-- app/Services/LocationService.php | 38 +--- .../Unit/Services/DatabaseHostServiceTest.php | 193 ++++++++++++++++++ tests/Unit/Services/LocationServiceTest.php | 101 +++++++++ 10 files changed, 564 insertions(+), 68 deletions(-) create mode 100644 app/Contracts/Repository/DatabaseHostInterface.php create mode 100644 app/Contracts/Repository/LocationRepositoryInterface.php create mode 100644 app/Repositories/Eloquent/DatabaseHostRepository.php create mode 100644 app/Repositories/Eloquent/LocationRepository.php create mode 100644 tests/Unit/Services/DatabaseHostServiceTest.php create mode 100644 tests/Unit/Services/LocationServiceTest.php diff --git a/app/Contracts/Repository/DatabaseHostInterface.php b/app/Contracts/Repository/DatabaseHostInterface.php new file mode 100644 index 00000000..e1fcee8b --- /dev/null +++ b/app/Contracts/Repository/DatabaseHostInterface.php @@ -0,0 +1,30 @@ +. + * + * 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\Contracts\Repository; + +interface DatabaseHostInterface extends RepositoryInterface +{ + public function deleteIfNoDatabases($id); +} diff --git a/app/Contracts/Repository/LocationRepositoryInterface.php b/app/Contracts/Repository/LocationRepositoryInterface.php new file mode 100644 index 00000000..81a75ff8 --- /dev/null +++ b/app/Contracts/Repository/LocationRepositoryInterface.php @@ -0,0 +1,32 @@ +. + * + * 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\Contracts\Repository; + +use Pterodactyl\Contracts\Repository\Attributes\SearchableInterface; + +interface LocationRepositoryInterface extends RepositoryInterface, SearchableInterface +{ + public function deleteIfNoNodes($id); +} diff --git a/app/Extensions/DynamicDatabaseConnection.php b/app/Extensions/DynamicDatabaseConnection.php index 18d0001c..01308190 100644 --- a/app/Extensions/DynamicDatabaseConnection.php +++ b/app/Extensions/DynamicDatabaseConnection.php @@ -24,6 +24,7 @@ namespace Pterodactyl\Extensions; +use Pterodactyl\Contracts\Repository\DatabaseHostInterface; use Pterodactyl\Models\DatabaseHost; use Illuminate\Contracts\Encryption\Encrypter; use Illuminate\Config\Repository as ConfigRepository; @@ -45,25 +46,25 @@ class DynamicDatabaseConnection protected $encrypter; /** - * @var \Pterodactyl\Models\DatabaseHost + * @var \Pterodactyl\Contracts\Repository\DatabaseHostInterface */ - protected $model; + protected $repository; /** * DynamicDatabaseConnection constructor. * - * @param \Illuminate\Config\Repository $config - * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter - * @param \Pterodactyl\Models\DatabaseHost $model + * @param \Illuminate\Config\Repository $config + * @param \Pterodactyl\Contracts\Repository\DatabaseHostInterface $repository + * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter */ public function __construct( ConfigRepository $config, - Encrypter $encrypter, - DatabaseHost $model + DatabaseHostInterface $repository, + Encrypter $encrypter ) { $this->config = $config; $this->encrypter = $encrypter; - $this->model = $model; + $this->repository = $repository; } /** @@ -76,7 +77,7 @@ class DynamicDatabaseConnection public function set($connection, $host, $database = 'mysql') { if (! $host instanceof DatabaseHost) { - $host = $this->model->findOrFail($host); + $host = $this->repository->find($host); } $this->config->set('database.connections.' . $connection, [ diff --git a/app/Providers/RepositoryServiceProvider.php b/app/Providers/RepositoryServiceProvider.php index d7b2d02d..739b9132 100644 --- a/app/Providers/RepositoryServiceProvider.php +++ b/app/Providers/RepositoryServiceProvider.php @@ -25,6 +25,10 @@ namespace Pterodactyl\Providers; use Illuminate\Support\ServiceProvider; +use Pterodactyl\Contracts\Repository\DatabaseHostInterface; +use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; +use Pterodactyl\Repositories\Eloquent\DatabaseHostRepository; +use Pterodactyl\Repositories\Eloquent\LocationRepository; use Pterodactyl\Repositories\Eloquent\UserRepository; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; @@ -35,6 +39,8 @@ class RepositoryServiceProvider extends ServiceProvider */ public function register() { + $this->app->bind(DatabaseHostInterface::class, DatabaseHostRepository::class); + $this->app->bind(LocationRepositoryInterface::class, LocationRepository::class); $this->app->bind(UserRepositoryInterface::class, UserRepository::class); } } diff --git a/app/Repositories/Eloquent/DatabaseHostRepository.php b/app/Repositories/Eloquent/DatabaseHostRepository.php new file mode 100644 index 00000000..921bc7b0 --- /dev/null +++ b/app/Repositories/Eloquent/DatabaseHostRepository.php @@ -0,0 +1,67 @@ +. + * + * 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\Repositories\Eloquent; + +use Pterodactyl\Contracts\Repository\DatabaseHostInterface; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Exceptions\Repository\RecordNotFoundException; +use Pterodactyl\Models\DatabaseHost; + +class DatabaseHostRepository extends EloquentRepository implements DatabaseHostInterface +{ + /** + * Setup the model to be used. + * + * @return string + */ + public function model() + { + return DatabaseHost::class; + } + + /** + * Delete a database host from the DB if there are no databases using it. + * + * @param int $id + * @return bool|null + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function deleteIfNoDatabases($id) + { + $instance = $this->getBuilder()->withCount('databases')->find($id); + + if (! $instance) { + throw new RecordNotFoundException(); + } + + if ($instance->databases_count > 0) { + throw new DisplayException('Cannot delete a database host that has active databases attached to it.'); + } + + return $instance->delete(); + } +} diff --git a/app/Repositories/Eloquent/LocationRepository.php b/app/Repositories/Eloquent/LocationRepository.php new file mode 100644 index 00000000..fffbe61b --- /dev/null +++ b/app/Repositories/Eloquent/LocationRepository.php @@ -0,0 +1,90 @@ +. + * + * 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\Repositories\Eloquent; + +use Pterodactyl\Models\Location; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Exceptions\Repository\RecordNotFoundException; +use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; + +class LocationRepository extends EloquentRepository implements LocationRepositoryInterface +{ + /** + * @var string + */ + protected $searchTerm; + + /** + * Setup model. + * + * @return string + */ + public function model() + { + return Location::class; + } + + /** + * Setup the model for search abilities. + * + * @param $term + * @return $this + */ + public function search($term) + { + if (empty($term)) { + return $this; + } + + $clone = clone $this; + $clone->searchTerm = $term; + + return $clone; + } + + /** + * Delete a location only if there are no nodes attached to it. + * + * @param $id + * @return bool|mixed|null + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function deleteIfNoNodes($id) + { + $location = $this->getBuilder()->with('nodes')->find($id); + + if (! $location) { + throw new RecordNotFoundException(); + } + + if ($location->nodes_count > 0) { + throw new DisplayException('Cannot delete a location that has nodes assigned to it.'); + } + + return $location->delete(); + } +} diff --git a/app/Services/DatabaseHostService.php b/app/Services/DatabaseHostService.php index ed285020..b3f2be41 100644 --- a/app/Services/DatabaseHostService.php +++ b/app/Services/DatabaseHostService.php @@ -24,11 +24,10 @@ namespace Pterodactyl\Services; -use Pterodactyl\Models\DatabaseHost; use Illuminate\Database\DatabaseManager; -use Pterodactyl\Exceptions\DisplayException; use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Extensions\DynamicDatabaseConnection; +use Pterodactyl\Contracts\Repository\DatabaseHostInterface; class DatabaseHostService { @@ -48,28 +47,28 @@ class DatabaseHostService protected $encrypter; /** - * @var \Pterodactyl\Models\DatabaseHost + * @var \Pterodactyl\Contracts\Repository\DatabaseHostInterface */ - protected $model; + protected $repository; /** * DatabaseHostService constructor. * - * @param \Illuminate\Database\DatabaseManager $database - * @param \Pterodactyl\Extensions\DynamicDatabaseConnection $dynamic - * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter - * @param \Pterodactyl\Models\DatabaseHost $model + * @param \Pterodactyl\Contracts\Repository\DatabaseHostInterface $repository + * @param \Illuminate\Database\DatabaseManager $database + * @param \Pterodactyl\Extensions\DynamicDatabaseConnection $dynamic + * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter */ public function __construct( + DatabaseHostInterface $repository, DatabaseManager $database, DynamicDatabaseConnection $dynamic, - Encrypter $encrypter, - DatabaseHost $model + Encrypter $encrypter ) { $this->database = $database; $this->dynamic = $dynamic; $this->encrypter = $encrypter; - $this->model = $model; + $this->repository = $repository; } /** @@ -83,10 +82,10 @@ class DatabaseHostService */ public function create(array $data) { - $instance = $this->model->newInstance(); - $instance->password = $this->encrypter->encrypt(array_get($data, 'password')); + $this->database->beginTransaction(); - $instance->fill([ + $host = $this->repository->create([ + 'password' => $this->encrypter->encrypt(array_get($data, 'password')), 'name' => array_get($data, 'name'), 'host' => array_get($data, 'host'), 'port' => array_get($data, 'port'), @@ -96,12 +95,12 @@ class DatabaseHostService ]); // Check Access - $this->dynamic->set('dynamic', $instance); + $this->dynamic->set('dynamic', $host); $this->database->connection('dynamic')->select('SELECT 1 FROM dual'); - $instance->saveOrFail(); + $this->database->commit(); - return $instance; + return $host; } /** @@ -115,19 +114,22 @@ class DatabaseHostService */ public function update($id, array $data) { - $model = $this->model->findOrFail($id); + $this->database->beginTransaction(); if (! empty(array_get($data, 'password'))) { - $model->password = $this->encrypter->encrypt($data['password']); + $data['password'] = $this->encrypter->encrypt($data['password']); + } else { + unset($data['password']); } - $model->fill($data); - $this->dynamic->set('dynamic', $model); + $host = $this->repository->update($id, $data); + + $this->dynamic->set('dynamic', $host); $this->database->connection('dynamic')->select('SELECT 1 FROM dual'); - $model->saveOrFail(); + $this->database->commit(); - return $model; + return $host; } /** @@ -140,12 +142,6 @@ class DatabaseHostService */ public function delete($id) { - $model = $this->model->withCount('databases')->findOrFail($id); - - if ($model->databases_count > 0) { - throw new DisplayException('Cannot delete a database host that has active databases attached to it.'); - } - - return $model->delete(); + return $this->repository->deleteIfNoDatabases($id); } } diff --git a/app/Services/LocationService.php b/app/Services/LocationService.php index a6139e0d..2bf7a41e 100644 --- a/app/Services/LocationService.php +++ b/app/Services/LocationService.php @@ -24,25 +24,23 @@ namespace Pterodactyl\Services; -use Pterodactyl\Models\Location; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Exceptions\Model\DataValidationException; +use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; class LocationService { /** - * @var \Pterodactyl\Models\Location + * @var \Pterodactyl\Contracts\Repository\LocationRepositoryInterface */ - protected $model; + protected $repository; /** * LocationService constructor. * - * @param \Pterodactyl\Models\Location $location + * @param \Pterodactyl\Contracts\Repository\LocationRepositoryInterface $repository */ - public function __construct(Location $location) + public function __construct(LocationRepositoryInterface $repository) { - $this->model = $location; + $this->repository = $repository; } /** @@ -55,13 +53,7 @@ class LocationService */ public function create(array $data) { - $location = $this->model->newInstance($data); - - if (! $location->save()) { - throw new DataValidationException($location->getValidator()); - } - - return $location; + return $this->repository->create($data); } /** @@ -75,13 +67,7 @@ class LocationService */ public function update($id, array $data) { - $location = $this->model->findOrFail($id)->fill($data); - - if (! $location->save()) { - throw new DataValidationException($location->getValidator()); - } - - return $location; + return $this->repository->update($id, $data); } /** @@ -94,12 +80,6 @@ class LocationService */ public function delete($id) { - $location = $this->model->withCount('nodes')->findOrFail($id); - - if ($location->nodes_count > 0) { - throw new DisplayException('Cannot delete a location that has nodes assigned to it.'); - } - - return $location->delete(); + return $this->repository->deleteIfNoNodes($id); } } diff --git a/tests/Unit/Services/DatabaseHostServiceTest.php b/tests/Unit/Services/DatabaseHostServiceTest.php new file mode 100644 index 00000000..1239c1ec --- /dev/null +++ b/tests/Unit/Services/DatabaseHostServiceTest.php @@ -0,0 +1,193 @@ +. + * + * 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; + +use Mockery as m; +use Tests\TestCase; +use Illuminate\Database\DatabaseManager; +use Pterodactyl\Services\DatabaseHostService; +use Illuminate\Contracts\Encryption\Encrypter; +use Pterodactyl\Extensions\DynamicDatabaseConnection; +use Pterodactyl\Contracts\Repository\DatabaseHostInterface; + +class DatabaseHostServiceTest extends TestCase +{ + /** + * @var \Illuminate\Database\DatabaseManager + */ + protected $database; + + /** + * @var \Pterodactyl\Extensions\DynamicDatabaseConnection + */ + protected $dynamic; + + /** + * @var \Illuminate\Contracts\Encryption\Encrypter + */ + protected $encrypter; + + /** + * @var \Pterodactyl\Contracts\Repository\DatabaseHostInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\DatabaseHostService + */ + protected $service; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->database = m::mock(DatabaseManager::class); + $this->dynamic = m::mock(DynamicDatabaseConnection::class); + $this->encrypter = m::mock(Encrypter::class); + $this->repository = m::mock(DatabaseHostInterface::class); + + $this->service = new DatabaseHostService( + $this->repository, + $this->database, + $this->dynamic, + $this->encrypter + ); + } + + /** + * Test that creating a host returns the correct data. + */ + public function test_create_host_function() + { + $data = [ + 'password' => 'raw-password', + 'name' => 'HostName', + 'host' => '127.0.0.1', + 'port' => 3306, + 'username' => 'someusername', + 'node_id' => null, + ]; + + $finalData = (object) array_replace($data, ['password' => 'enc-password']); + + $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->encrypter->shouldReceive('encrypt')->with('raw-password')->once()->andReturn('enc-password'); + + $this->repository->shouldReceive('create')->with([ + 'password' => 'enc-password', + 'name' => 'HostName', + 'host' => '127.0.0.1', + 'port' => 3306, + 'username' => 'someusername', + 'max_databases' => null, + 'node_id' => null, + ])->once()->andReturn($finalData); + + $this->dynamic->shouldReceive('set')->with('dynamic', $finalData)->once()->andReturnNull(); + $this->database->shouldReceive('connection')->with('dynamic')->once()->andReturnSelf() + ->shouldReceive('select')->with('SELECT 1 FROM dual')->once()->andReturnNull(); + + $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $response = $this->service->create($data); + + $this->assertNotNull($response); + $this->assertTrue(is_object($response), 'Assert that response is an object.'); + + $this->assertEquals('enc-password', $response->password); + $this->assertEquals('HostName', $response->name); + $this->assertEquals('127.0.0.1', $response->host); + $this->assertEquals(3306, $response->port); + $this->assertEquals('someusername', $response->username); + $this->assertNull($response->node_id); + } + + /** + * Test that passing a password will store an encrypted version in the DB. + */ + public function test_update_with_password() + { + $finalData = (object) ['password' => 'enc-pass', 'host' => '123.456.78.9']; + + $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->encrypter->shouldReceive('encrypt')->with('raw-pass')->once()->andReturn('enc-pass'); + + $this->repository->shouldReceive('update')->with(1, [ + 'password' => 'enc-pass', + 'host' => '123.456.78.9', + ])->once()->andReturn($finalData); + + $this->dynamic->shouldReceive('set')->with('dynamic', $finalData)->once()->andReturnNull(); + $this->database->shouldReceive('connection')->with('dynamic')->once()->andReturnSelf() + ->shouldReceive('select')->with('SELECT 1 FROM dual')->once()->andReturnNull(); + + $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $response = $this->service->update(1, ['password' => 'raw-pass', 'host' => '123.456.78.9']); + + $this->assertNotNull($response); + $this->assertEquals('enc-pass', $response->password); + $this->assertEquals('123.456.78.9', $response->host); + } + + /** + * Test that passing no or empty password will skip storing it + */ + public function test_update_without_password() + { + $finalData = (object) ['host' => '123.456.78.9']; + + $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->encrypter->shouldNotReceive('encrypt'); + + $this->repository->shouldReceive('update')->with(1, ['host' => '123.456.78.9'])->once()->andReturn($finalData); + + $this->dynamic->shouldReceive('set')->with('dynamic', $finalData)->once()->andReturnNull(); + $this->database->shouldReceive('connection')->with('dynamic')->once()->andReturnSelf() + ->shouldReceive('select')->with('SELECT 1 FROM dual')->once()->andReturnNull(); + + $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $response = $this->service->update(1, ['password' => '', 'host' => '123.456.78.9']); + + $this->assertNotNull($response); + $this->assertEquals('123.456.78.9', $response->host); + } + + /** + * Test that a database host can be deleted. + */ + public function test_delete_function() + { + $this->repository->shouldReceive('deleteIfNoDatabases')->with(1)->once()->andReturn(true); + + $response = $this->service->delete(1); + + $this->assertTrue($response, 'Assert that response is true.'); + } +} diff --git a/tests/Unit/Services/LocationServiceTest.php b/tests/Unit/Services/LocationServiceTest.php new file mode 100644 index 00000000..442b02b5 --- /dev/null +++ b/tests/Unit/Services/LocationServiceTest.php @@ -0,0 +1,101 @@ +. + * + * 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; + +use Mockery as m; +use Tests\TestCase; +use Pterodactyl\Services\LocationService; +use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; + +class LocationServiceTest extends TestCase +{ + /** + * @var \Pterodactyl\Contracts\Repository\LocationRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\LocationService + */ + protected $service; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->repository = m::mock(LocationRepositoryInterface::class); + + $this->service = new LocationService($this->repository); + } + + /** + * Test that creating a location returns the correct information. + */ + public function test_create_location() + { + $data = ['short' => 'shortCode', 'long' => 'longCode']; + + $this->repository->shouldReceive('create')->with($data)->once()->andReturn((object) $data); + + $response = $this->service->create($data); + + $this->assertNotNull($response); + $this->assertObjectHasAttribute('short', $response); + $this->assertObjectHasAttribute('long', $response); + $this->assertEquals('shortCode', $response->short); + $this->assertEquals('longCode', $response->long); + } + + /** + * Test that updating a location updates it correctly. + */ + public function test_update_location() + { + $data = ['short' => 'newShort']; + + $this->repository->shouldReceive('update')->with(1, $data)->once()->andReturn((object) $data); + + $response = $this->service->update(1, $data); + + $this->assertNotNull($response); + $this->assertObjectHasAttribute('short', $response); + $this->assertEquals('newShort', $response->short); + } + + /** + * Test that a location deletion returns valid data. + */ + public function test_delete_location() + { + $this->repository->shouldReceive('deleteIfNoNodes')->with(1)->once()->andReturn(true); + + $response = $this->service->delete(1); + + $this->assertTrue($response); + } +} From 0deb02209319f73fc3c8061989c2fc301ff34043 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 8 Jul 2017 14:07:51 -0500 Subject: [PATCH 20/99] Update last of existing services to use repositories, includes unit tests Also update PHPDocs on all the repository interfaces and classes to be correct. --- .../Repository/ApiKeyRepositoryInterface.php | 30 ++ .../ApiPermissionRepositoryInterface.php | 30 ++ .../Attributes/SearchableInterface.php | 6 + .../Repository/DatabaseHostInterface.php | 9 + .../LocationRepositoryInterface.php | 9 + .../Repository/RepositoryInterface.php | 91 ++++- .../Repository/UserRepositoryInterface.php | 13 + app/Models/APIPermission.php | 2 +- app/Providers/RepositoryServiceProvider.php | 6 + .../Eloquent/ApiKeyRepository.php | 39 ++ .../Eloquent/ApiPermissionRepository.php | 39 ++ .../Eloquent/DatabaseHostRepository.php | 12 +- .../Eloquent/EloquentRepository.php | 49 +-- .../Eloquent/LocationRepository.php | 17 +- app/Repositories/Eloquent/UserRepository.php | 16 +- app/Services/ApiKeyService.php | 65 ++-- app/Services/ApiPermissionService.php | 24 +- composer.json | 1 + composer.lock | 353 +++++++++++++----- tests/Unit/Services/ApiKeyServiceTest.php | 127 +++++++ .../Services/ApiPermissionServiceTest.php | 77 ++++ 21 files changed, 808 insertions(+), 207 deletions(-) create mode 100644 app/Contracts/Repository/ApiKeyRepositoryInterface.php create mode 100644 app/Contracts/Repository/ApiPermissionRepositoryInterface.php create mode 100644 app/Repositories/Eloquent/ApiKeyRepository.php create mode 100644 app/Repositories/Eloquent/ApiPermissionRepository.php create mode 100644 tests/Unit/Services/ApiKeyServiceTest.php create mode 100644 tests/Unit/Services/ApiPermissionServiceTest.php diff --git a/app/Contracts/Repository/ApiKeyRepositoryInterface.php b/app/Contracts/Repository/ApiKeyRepositoryInterface.php new file mode 100644 index 00000000..bfd44e92 --- /dev/null +++ b/app/Contracts/Repository/ApiKeyRepositoryInterface.php @@ -0,0 +1,30 @@ +. + * + * 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\Contracts\Repository; + +interface ApiKeyRepositoryInterface extends RepositoryInterface +{ + // +} diff --git a/app/Contracts/Repository/ApiPermissionRepositoryInterface.php b/app/Contracts/Repository/ApiPermissionRepositoryInterface.php new file mode 100644 index 00000000..f0556b45 --- /dev/null +++ b/app/Contracts/Repository/ApiPermissionRepositoryInterface.php @@ -0,0 +1,30 @@ +. + * + * 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\Contracts\Repository; + +interface ApiPermissionRepositoryInterface extends RepositoryInterface +{ + // +} diff --git a/app/Contracts/Repository/Attributes/SearchableInterface.php b/app/Contracts/Repository/Attributes/SearchableInterface.php index 37d9316e..60ca52b9 100644 --- a/app/Contracts/Repository/Attributes/SearchableInterface.php +++ b/app/Contracts/Repository/Attributes/SearchableInterface.php @@ -26,5 +26,11 @@ namespace Pterodactyl\Contracts\Repository\Attributes; interface SearchableInterface { + /** + * Filter results by search term. + * + * @param string $term + * @return $this + */ public function search($term); } diff --git a/app/Contracts/Repository/DatabaseHostInterface.php b/app/Contracts/Repository/DatabaseHostInterface.php index e1fcee8b..2fd26b16 100644 --- a/app/Contracts/Repository/DatabaseHostInterface.php +++ b/app/Contracts/Repository/DatabaseHostInterface.php @@ -26,5 +26,14 @@ namespace Pterodactyl\Contracts\Repository; interface DatabaseHostInterface extends RepositoryInterface { + /** + * Delete a database host from the DB if there are no databases using it. + * + * @param int $id + * @return bool|null + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ public function deleteIfNoDatabases($id); } diff --git a/app/Contracts/Repository/LocationRepositoryInterface.php b/app/Contracts/Repository/LocationRepositoryInterface.php index 81a75ff8..9a52e298 100644 --- a/app/Contracts/Repository/LocationRepositoryInterface.php +++ b/app/Contracts/Repository/LocationRepositoryInterface.php @@ -28,5 +28,14 @@ use Pterodactyl\Contracts\Repository\Attributes\SearchableInterface; interface LocationRepositoryInterface extends RepositoryInterface, SearchableInterface { + /** + * Delete a location only if there are no nodes attached to it. + * + * @param $id + * @return bool|mixed|null + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ public function deleteIfNoNodes($id); } diff --git a/app/Contracts/Repository/RepositoryInterface.php b/app/Contracts/Repository/RepositoryInterface.php index 5eba06b1..470dc3eb 100644 --- a/app/Contracts/Repository/RepositoryInterface.php +++ b/app/Contracts/Repository/RepositoryInterface.php @@ -26,25 +26,108 @@ namespace Pterodactyl\Contracts\Repository; interface RepositoryInterface { + /** + * Return an identifier or Model object to be used by the repository. + * + * @return string|\Closure|object + */ public function model(); + /** + * Return the model being used for this repository instance. + * + * @return mixed + */ public function getModel(); + /** + * Returns an instance of a query builder. + * + * @return mixed + */ public function getBuilder(); + /** + * Returns the colummns to be selected or returned by the query. + * + * @return mixed + */ public function getColumns(); + /** + * An array of columns to filter the response by. + * + * @param array $columns + * @return $this + */ public function withColumns($columns = ['*']); - public function create($fields); + /** + * Disable returning a fresh model when data is inserted or updated. + * + * @return $this + */ + public function withoutFresh(); + /** + * Create a new model instance and persist it to the database. + * + * @param array $fields + * @param bool $validate + * @return mixed + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function create(array $fields, $validate = true); + + /** + * Delete a given record from the database. + * + * @param int $id + * @return bool|null + */ public function delete($id); + /** + * Find a model that has the specific ID passed. + * + * @param int $id + * @return mixed + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ public function find($id); - public function findWhere($fields); + /** + * Find a model matching an array of where clauses. + * + * @param array $fields + * @return mixed + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function findWhere(array $fields); - public function update($id, $fields); + /** + * Update a given ID with the passed array of fields. + * + * @param int $id + * @param array $fields + * @param bool $validate + * @param bool $force + * @return mixed + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function update($id, array $fields, $validate = true, $force = false); - public function massUpdate($fields); + /** + * Update multiple records matching the passed clauses. + * + * @param array $where + * @param array $fields + * @return mixed + */ + public function massUpdate(array $where, array $fields); } diff --git a/app/Contracts/Repository/UserRepositoryInterface.php b/app/Contracts/Repository/UserRepositoryInterface.php index cf61251e..d6a02c92 100644 --- a/app/Contracts/Repository/UserRepositoryInterface.php +++ b/app/Contracts/Repository/UserRepositoryInterface.php @@ -28,7 +28,20 @@ use Pterodactyl\Contracts\Repository\Attributes\SearchableInterface; interface UserRepositoryInterface extends RepositoryInterface, SearchableInterface { + /** + * Return all users with counts of servers and subusers of servers. + * + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator + */ public function getAllUsersWithCounts(); + /** + * Delete a user if they have no servers attached to their account. + * + * @param int $id + * @return bool + * + * @throws \Pterodactyl\Exceptions\DisplayException + */ public function deleteIfNoServers($id); } diff --git a/app/Models/APIPermission.php b/app/Models/APIPermission.php index 9361d31b..626185fc 100644 --- a/app/Models/APIPermission.php +++ b/app/Models/APIPermission.php @@ -36,7 +36,7 @@ class APIPermission extends Model implements ValidableContract /** * List of permissions available for the API. */ - const PERMISSIONS = [ + const CONST_PERMISSIONS = [ // Items within this block are available to non-adminitrative users. '_user' => [ 'server' => [ diff --git a/app/Providers/RepositoryServiceProvider.php b/app/Providers/RepositoryServiceProvider.php index 739b9132..9d0f59b3 100644 --- a/app/Providers/RepositoryServiceProvider.php +++ b/app/Providers/RepositoryServiceProvider.php @@ -25,8 +25,12 @@ namespace Pterodactyl\Providers; use Illuminate\Support\ServiceProvider; +use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; +use Pterodactyl\Contracts\Repository\ApiPermissionRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseHostInterface; use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; +use Pterodactyl\Repositories\Eloquent\ApiKeyRepository; +use Pterodactyl\Repositories\Eloquent\ApiPermissionRepository; use Pterodactyl\Repositories\Eloquent\DatabaseHostRepository; use Pterodactyl\Repositories\Eloquent\LocationRepository; use Pterodactyl\Repositories\Eloquent\UserRepository; @@ -39,6 +43,8 @@ class RepositoryServiceProvider extends ServiceProvider */ public function register() { + $this->app->bind(ApiKeyRepositoryInterface::class, ApiKeyRepository::class); + $this->app->bind(ApiPermissionRepositoryInterface::class, ApiPermissionRepository::class); $this->app->bind(DatabaseHostInterface::class, DatabaseHostRepository::class); $this->app->bind(LocationRepositoryInterface::class, LocationRepository::class); $this->app->bind(UserRepositoryInterface::class, UserRepository::class); diff --git a/app/Repositories/Eloquent/ApiKeyRepository.php b/app/Repositories/Eloquent/ApiKeyRepository.php new file mode 100644 index 00000000..c82088f2 --- /dev/null +++ b/app/Repositories/Eloquent/ApiKeyRepository.php @@ -0,0 +1,39 @@ +. + * + * 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\Repositories\Eloquent; + +use Pterodactyl\Models\APIKey; +use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; + +class ApiKeyRepository extends EloquentRepository implements ApiKeyRepositoryInterface +{ + /** + * {@inheritdoc} + */ + public function model() + { + return APIKey::class; + } +} diff --git a/app/Repositories/Eloquent/ApiPermissionRepository.php b/app/Repositories/Eloquent/ApiPermissionRepository.php new file mode 100644 index 00000000..5c87e813 --- /dev/null +++ b/app/Repositories/Eloquent/ApiPermissionRepository.php @@ -0,0 +1,39 @@ +. + * + * 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\Repositories\Eloquent; + +use Pterodactyl\Models\APIPermission; +use Pterodactyl\Contracts\Repository\ApiPermissionRepositoryInterface; + +class ApiPermissionRepository extends EloquentRepository implements ApiPermissionRepositoryInterface +{ + /** + * {@inheritdoc} + */ + public function model() + { + return APIPermission::class; + } +} diff --git a/app/Repositories/Eloquent/DatabaseHostRepository.php b/app/Repositories/Eloquent/DatabaseHostRepository.php index 921bc7b0..76c57212 100644 --- a/app/Repositories/Eloquent/DatabaseHostRepository.php +++ b/app/Repositories/Eloquent/DatabaseHostRepository.php @@ -32,9 +32,7 @@ use Pterodactyl\Models\DatabaseHost; class DatabaseHostRepository extends EloquentRepository implements DatabaseHostInterface { /** - * Setup the model to be used. - * - * @return string + * {@inheritdoc} */ public function model() { @@ -42,13 +40,7 @@ class DatabaseHostRepository extends EloquentRepository implements DatabaseHostI } /** - * Delete a database host from the DB if there are no databases using it. - * - * @param int $id - * @return bool|null - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * {@inheritdoc} */ public function deleteIfNoDatabases($id) { diff --git a/app/Repositories/Eloquent/EloquentRepository.php b/app/Repositories/Eloquent/EloquentRepository.php index 614d8039..2da2eccd 100644 --- a/app/Repositories/Eloquent/EloquentRepository.php +++ b/app/Repositories/Eloquent/EloquentRepository.php @@ -24,14 +24,15 @@ namespace Pterodactyl\Repositories\Eloquent; -use Pterodactyl\Exceptions\Model\DataValidationException; -use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Repository\Repository; use Pterodactyl\Contracts\Repository\RepositoryInterface; +use Pterodactyl\Exceptions\Model\DataValidationException; +use Pterodactyl\Exceptions\Repository\RecordNotFoundException; abstract class EloquentRepository extends Repository implements RepositoryInterface { /** + * {@inheritdoc} * @return \Illuminate\Database\Eloquent\Builder */ public function getBuilder() @@ -40,14 +41,11 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf } /** - * Create a new model instance and persist it to the database. - * @param array $fields - * @param bool $validate - * @param bool $force - * @return bool|\Illuminate\Database\Eloquent\Model - * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * {@inheritdoc} + * @param bool $force + * @return \Illuminate\Database\Eloquent\Model|bool */ - public function create($fields, $validate = true, $force = false) + public function create(array $fields, $validate = true, $force = false) { $instance = $this->getBuilder()->newModelInstance(); @@ -69,12 +67,8 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf } /** - * Return a record from the database for a given ID. - * - * @param int $id + * {@inheritdoc} * @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Model - * - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function find($id) { @@ -87,17 +81,16 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf return $instance; } - public function findWhere($fields) + /** + * {@inheritdoc} + */ + public function findWhere(array $fields) { // TODO: Implement findWhere() method. } /** - * Delete a record from the DB given an ID. - * - * @param int $id - * @param bool $destroy - * @return bool|null + * {@inheritdoc} */ public function delete($id, $destroy = false) { @@ -109,16 +102,9 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf } /** - * @param int $id - * @param array $fields - * @param bool $validate - * @param bool $force - * @return mixed - * - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * {@inheritdoc} */ - public function update($id, $fields, $validate = true, $force = false) + public function update($id, array $fields, $validate = true, $force = false) { $instance = $this->getBuilder()->where('id', $id)->first(); @@ -143,7 +129,10 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf return ($this->withFresh) ? $instance->fresh($this->getColumns()) : $saved; } - public function massUpdate($fields) + /** + * {@inheritdoc} + */ + public function massUpdate(array $where, array $fields) { // TODO: Implement massUpdate() method. } diff --git a/app/Repositories/Eloquent/LocationRepository.php b/app/Repositories/Eloquent/LocationRepository.php index fffbe61b..43e2e15d 100644 --- a/app/Repositories/Eloquent/LocationRepository.php +++ b/app/Repositories/Eloquent/LocationRepository.php @@ -37,9 +37,7 @@ class LocationRepository extends EloquentRepository implements LocationRepositor protected $searchTerm; /** - * Setup model. - * - * @return string + * {@inheritdoc} */ public function model() { @@ -47,10 +45,7 @@ class LocationRepository extends EloquentRepository implements LocationRepositor } /** - * Setup the model for search abilities. - * - * @param $term - * @return $this + * {@inheritdoc} */ public function search($term) { @@ -65,13 +60,7 @@ class LocationRepository extends EloquentRepository implements LocationRepositor } /** - * Delete a location only if there are no nodes attached to it. - * - * @param $id - * @return bool|mixed|null - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * {@inheritdoc} */ public function deleteIfNoNodes($id) { diff --git a/app/Repositories/Eloquent/UserRepository.php b/app/Repositories/Eloquent/UserRepository.php index 96b85ffd..00776bb7 100644 --- a/app/Repositories/Eloquent/UserRepository.php +++ b/app/Repositories/Eloquent/UserRepository.php @@ -56,11 +56,17 @@ class UserRepository extends EloquentRepository implements UserRepositoryInterfa $this->config = $config; } + /** + * {@inheritdoc} + */ public function model() { return User::class; } + /** + * {@inheritdoc} + */ public function search($term) { if (empty($term)) { @@ -73,6 +79,9 @@ class UserRepository extends EloquentRepository implements UserRepositoryInterfa return $clone; } + /** + * {@inheritdoc} + */ public function getAllUsersWithCounts() { $users = $this->getBuilder()->withCount('servers', 'subuserOf'); @@ -87,12 +96,7 @@ class UserRepository extends EloquentRepository implements UserRepositoryInterfa } /** - * Delete a user if they have no servers attached to their account. - * - * @param int $id - * @return bool - * - * @throws \Pterodactyl\Exceptions\DisplayException + * {@inheritdoc} */ public function deleteIfNoServers($id) { diff --git a/app/Services/ApiKeyService.php b/app/Services/ApiKeyService.php index 91e703ea..d4a3c6e2 100644 --- a/app/Services/ApiKeyService.php +++ b/app/Services/ApiKeyService.php @@ -24,48 +24,52 @@ namespace Pterodactyl\Services; -use Pterodactyl\Models\APIKey; -use Illuminate\Database\Connection; +use Illuminate\Database\ConnectionInterface; use Illuminate\Contracts\Encryption\Encrypter; -use Pterodactyl\Exceptions\Model\DataValidationException; +use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; class ApiKeyService { const PUB_CRYPTO_BYTES = 8; const PRIV_CRYPTO_BYTES = 32; + /** + * @var \Illuminate\Database\ConnectionInterface + */ + protected $database; + /** * @var \Illuminate\Contracts\Encryption\Encrypter */ protected $encrypter; - /** - * @var \Pterodactyl\Models\APIKey - */ - protected $model; - /** * @var \Pterodactyl\Services\ApiPermissionService */ protected $permissionService; + /** + * @var \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface + */ + protected $repository; + /** * ApiKeyService constructor. * - * @param \Pterodactyl\Models\APIKey $model - * @param \Illuminate\Database\Connection $database - * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter - * @param \Pterodactyl\Services\ApiPermissionService $permissionService + * @param \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface $repository + * @param \Illuminate\Database\ConnectionInterface $database + * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter + * @param \Pterodactyl\Services\ApiPermissionService $permissionService */ public function __construct( - APIKey $model, - Connection $database, + ApiKeyRepositoryInterface $repository, + ConnectionInterface $database, Encrypter $encrypter, ApiPermissionService $permissionService ) { + $this->repository = $repository; $this->database = $database; $this->encrypter = $encrypter; - $this->model = $model; $this->permissionService = $permissionService; } @@ -88,16 +92,12 @@ class ApiKeyService // Start a Transaction $this->database->beginTransaction(); - $instance = $this->model->newInstance($data); - $instance->public = $publicKey; - $instance->secret = $this->encrypter->encrypt($secretKey); + $data = array_merge($data, [ + 'public' => $publicKey, + 'secret' => $this->encrypter->encrypt($secretKey), + ]); - if (! $instance->save()) { - $this->database->rollBack(); - throw new DataValidationException($instance->getValidator()); - } - - $key = $instance->fresh(); + $instance = $this->repository->create($data, true, true); $nodes = $this->permissionService->getPermissions(); foreach ($permissions as $permission) { @@ -111,7 +111,7 @@ class ApiKeyService continue; } - $this->permissionService->create($key->id, sprintf('user.%s', $permission)); + $this->permissionService->create($instance->id, sprintf('user.%s', $permission)); } foreach ($administrative as $permission) { @@ -125,7 +125,7 @@ class ApiKeyService continue; } - $this->permissionService->create($key->id, $permission); + $this->permissionService->create($instance->id, $permission); } $this->database->commit(); @@ -136,18 +136,11 @@ class ApiKeyService /** * Delete the API key and associated permissions from the database. * - * @param int|\Pterodactyl\Models\APIKey $key + * @param int $id * @return bool|null - * - * @throws \Exception - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException */ - public function revoke($key) + public function revoke($id) { - if (! $key instanceof APIKey) { - $key = $this->model->findOrFail($key); - } - - return $key->delete(); + return $this->repository->delete($id); } } diff --git a/app/Services/ApiPermissionService.php b/app/Services/ApiPermissionService.php index 20a722bf..40ca4a1c 100644 --- a/app/Services/ApiPermissionService.php +++ b/app/Services/ApiPermissionService.php @@ -24,24 +24,23 @@ namespace Pterodactyl\Services; -use Pterodactyl\Models\APIPermission; -use Pterodactyl\Exceptions\Model\DataValidationException; +use Pterodactyl\Contracts\Repository\ApiPermissionRepositoryInterface; class ApiPermissionService { /** - * @var \Pterodactyl\Models\APIPermission + * @var \Pterodactyl\Contracts\Repository\ApiPermissionRepositoryInterface */ - protected $model; + protected $repository; /** * ApiPermissionService constructor. * - * @param \Pterodactyl\Models\APIPermission $model + * @param \Pterodactyl\Contracts\Repository\ApiPermissionRepositoryInterface $repository */ - public function __construct(APIPermission $model) + public function __construct(ApiPermissionRepositoryInterface $repository) { - $this->model = $model; + $this->repository = $repository; } /** @@ -55,16 +54,11 @@ class ApiPermissionService */ public function create($key, $permission) { - $instance = $this->model->newInstance([ + // @todo handle an array of permissions to do a mass assignment? + return $this->repository->withoutFresh()->create([ 'key_id' => $key, 'permission' => $permission, ]); - - if (! $instance->save()) { - throw new DataValidationException($instance->getValidator()); - } - - return true; } /** @@ -74,6 +68,6 @@ class ApiPermissionService */ public function getPermissions() { - return APIPermission::PERMISSIONS; + return $this->repository->getModel()::CONST_PERMISSIONS; } } diff --git a/composer.json b/composer.json index f025b24c..bd8f49da 100644 --- a/composer.json +++ b/composer.json @@ -44,6 +44,7 @@ "friendsofphp/php-cs-fixer": "1.*", "fzaninotto/faker": "~1.4", "mockery/mockery": "0.9.*", + "php-mock/php-mock-phpunit": "^1.1", "phpunit/phpunit": "~5.7", "sllh/php-cs-fixer-styleci-bridge": "^2.1" }, diff --git a/composer.lock b/composer.lock index d263f6ee..a6afca00 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "d3edf73b6618705ee34a76fa0319f0de", + "content-hash": "f1afab5cf73088c6034bfb2b13631600", "packages": [ { "name": "aws/aws-sdk-php", @@ -803,16 +803,16 @@ }, { "name": "erusev/parsedown", - "version": "1.6.2", + "version": "1.6.3", "source": { "type": "git", "url": "https://github.com/erusev/parsedown.git", - "reference": "1bf24f7334fe16c88bf9d467863309ceaf285b01" + "reference": "728952b90a333b5c6f77f06ea9422b94b585878d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/erusev/parsedown/zipball/1bf24f7334fe16c88bf9d467863309ceaf285b01", - "reference": "1bf24f7334fe16c88bf9d467863309ceaf285b01", + "url": "https://api.github.com/repos/erusev/parsedown/zipball/728952b90a333b5c6f77f06ea9422b94b585878d", + "reference": "728952b90a333b5c6f77f06ea9422b94b585878d", "shasum": "" }, "require": { @@ -841,7 +841,7 @@ "markdown", "parser" ], - "time": "2017-03-29T16:04:15+00:00" + "time": "2017-05-14T14:47:48+00:00" }, { "name": "fideloper/proxy", @@ -1989,16 +1989,16 @@ }, { "name": "nikic/php-parser", - "version": "v3.0.5", + "version": "v3.0.6", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "2b9e2f71b722f7c53918ab0c25f7646c2013f17d" + "reference": "0808939f81c1347a3c8a82a5925385a08074b0f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/2b9e2f71b722f7c53918ab0c25f7646c2013f17d", - "reference": "2b9e2f71b722f7c53918ab0c25f7646c2013f17d", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/0808939f81c1347a3c8a82a5925385a08074b0f1", + "reference": "0808939f81c1347a3c8a82a5925385a08074b0f1", "shasum": "" }, "require": { @@ -2036,7 +2036,7 @@ "parser", "php" ], - "time": "2017-03-05T18:23:57+00:00" + "time": "2017-06-28T20:53:48+00:00" }, { "name": "paragonie/random_compat", @@ -2346,16 +2346,16 @@ }, { "name": "psy/psysh", - "version": "v0.8.8", + "version": "v0.8.9", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "fe65c30cbc55c71e61ba3a38b5a581149be31b8e" + "reference": "58a31cc4404c8f632d8c557bc72056af2d3a83db" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/fe65c30cbc55c71e61ba3a38b5a581149be31b8e", - "reference": "fe65c30cbc55c71e61ba3a38b5a581149be31b8e", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/58a31cc4404c8f632d8c557bc72056af2d3a83db", + "reference": "58a31cc4404c8f632d8c557bc72056af2d3a83db", "shasum": "" }, "require": { @@ -2415,7 +2415,7 @@ "interactive", "shell" ], - "time": "2017-06-24T06:16:19+00:00" + "time": "2017-07-06T14:53:52+00:00" }, { "name": "ramsey/uuid", @@ -2653,16 +2653,16 @@ }, { "name": "spatie/fractalistic", - "version": "2.2.0", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/spatie/fractalistic.git", - "reference": "8f00c666a8b8dfb06f79286f97255e6ab1c89639" + "reference": "79a48d949bc053a1c60c934f727f5901bf35fa74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/fractalistic/zipball/8f00c666a8b8dfb06f79286f97255e6ab1c89639", - "reference": "8f00c666a8b8dfb06f79286f97255e6ab1c89639", + "url": "https://api.github.com/repos/spatie/fractalistic/zipball/79a48d949bc053a1c60c934f727f5901bf35fa74", + "reference": "79a48d949bc053a1c60c934f727f5901bf35fa74", "shasum": "" }, "require": { @@ -2700,7 +2700,7 @@ "spatie", "transform" ], - "time": "2017-05-29T14:16:20+00:00" + "time": "2017-07-03T08:20:31+00:00" }, { "name": "spatie/laravel-fractal", @@ -2816,16 +2816,16 @@ }, { "name": "symfony/console", - "version": "v3.3.2", + "version": "v3.3.4", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "70d2a29b2911cbdc91a7e268046c395278238b2e" + "reference": "a97e45d98c59510f085fa05225a1acb74dfe0546" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/70d2a29b2911cbdc91a7e268046c395278238b2e", - "reference": "70d2a29b2911cbdc91a7e268046c395278238b2e", + "url": "https://api.github.com/repos/symfony/console/zipball/a97e45d98c59510f085fa05225a1acb74dfe0546", + "reference": "a97e45d98c59510f085fa05225a1acb74dfe0546", "shasum": "" }, "require": { @@ -2881,11 +2881,11 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2017-06-02T19:24:58+00:00" + "time": "2017-07-03T13:19:36+00:00" }, { "name": "symfony/css-selector", - "version": "v3.3.2", + "version": "v3.3.4", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", @@ -2938,16 +2938,16 @@ }, { "name": "symfony/debug", - "version": "v3.3.2", + "version": "v3.3.4", "source": { "type": "git", "url": "https://github.com/symfony/debug.git", - "reference": "e9c50482841ef696e8fa1470d950a79c8921f45d" + "reference": "63b85a968486d95ff9542228dc2e4247f16f9743" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/e9c50482841ef696e8fa1470d950a79c8921f45d", - "reference": "e9c50482841ef696e8fa1470d950a79c8921f45d", + "url": "https://api.github.com/repos/symfony/debug/zipball/63b85a968486d95ff9542228dc2e4247f16f9743", + "reference": "63b85a968486d95ff9542228dc2e4247f16f9743", "shasum": "" }, "require": { @@ -2990,20 +2990,20 @@ ], "description": "Symfony Debug Component", "homepage": "https://symfony.com", - "time": "2017-06-01T21:01:25+00:00" + "time": "2017-07-05T13:02:37+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v3.3.2", + "version": "v3.3.4", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "4054a102470665451108f9b59305c79176ef98f0" + "reference": "67535f1e3fd662bdc68d7ba317c93eecd973617e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/4054a102470665451108f9b59305c79176ef98f0", - "reference": "4054a102470665451108f9b59305c79176ef98f0", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/67535f1e3fd662bdc68d7ba317c93eecd973617e", + "reference": "67535f1e3fd662bdc68d7ba317c93eecd973617e", "shasum": "" }, "require": { @@ -3053,11 +3053,11 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2017-06-04T18:15:29+00:00" + "time": "2017-06-09T14:53:08+00:00" }, { "name": "symfony/finder", - "version": "v3.3.2", + "version": "v3.3.4", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", @@ -3106,16 +3106,16 @@ }, { "name": "symfony/http-foundation", - "version": "v3.3.2", + "version": "v3.3.4", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "80eb5a1f968448b77da9e8b2c0827f6e8d767846" + "reference": "f347a5f561b03db95ed666959db42bbbf429b7e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/80eb5a1f968448b77da9e8b2c0827f6e8d767846", - "reference": "80eb5a1f968448b77da9e8b2c0827f6e8d767846", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f347a5f561b03db95ed666959db42bbbf429b7e5", + "reference": "f347a5f561b03db95ed666959db42bbbf429b7e5", "shasum": "" }, "require": { @@ -3155,20 +3155,20 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2017-06-05T13:06:51+00:00" + "time": "2017-06-24T09:29:48+00:00" }, { "name": "symfony/http-kernel", - "version": "v3.3.2", + "version": "v3.3.4", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "be8280f7fa8e95b86514f1e1be997668a53b2888" + "reference": "33f87c957122cfbd9d90de48698ee074b71106ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/be8280f7fa8e95b86514f1e1be997668a53b2888", - "reference": "be8280f7fa8e95b86514f1e1be997668a53b2888", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/33f87c957122cfbd9d90de48698ee074b71106ea", + "reference": "33f87c957122cfbd9d90de48698ee074b71106ea", "shasum": "" }, "require": { @@ -3241,7 +3241,7 @@ ], "description": "Symfony HttpKernel Component", "homepage": "https://symfony.com", - "time": "2017-06-06T03:59:58+00:00" + "time": "2017-07-05T13:28:15+00:00" }, { "name": "symfony/polyfill-mbstring", @@ -3412,16 +3412,16 @@ }, { "name": "symfony/process", - "version": "v3.3.2", + "version": "v3.3.4", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "8e30690c67aafb6c7992d6d8eb0d707807dd3eaf" + "reference": "5ab8949b682b1bf9d4511a228b5e045c96758c30" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/8e30690c67aafb6c7992d6d8eb0d707807dd3eaf", - "reference": "8e30690c67aafb6c7992d6d8eb0d707807dd3eaf", + "url": "https://api.github.com/repos/symfony/process/zipball/5ab8949b682b1bf9d4511a228b5e045c96758c30", + "reference": "5ab8949b682b1bf9d4511a228b5e045c96758c30", "shasum": "" }, "require": { @@ -3457,20 +3457,20 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2017-05-22T12:32:03+00:00" + "time": "2017-07-03T08:12:02+00:00" }, { "name": "symfony/routing", - "version": "v3.3.2", + "version": "v3.3.4", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "39804eeafea5cca851946e1eed122eb94459fdb4" + "reference": "dc70bbd0ca7b19259f63cdacc8af370bc32a4728" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/39804eeafea5cca851946e1eed122eb94459fdb4", - "reference": "39804eeafea5cca851946e1eed122eb94459fdb4", + "url": "https://api.github.com/repos/symfony/routing/zipball/dc70bbd0ca7b19259f63cdacc8af370bc32a4728", + "reference": "dc70bbd0ca7b19259f63cdacc8af370bc32a4728", "shasum": "" }, "require": { @@ -3535,20 +3535,20 @@ "uri", "url" ], - "time": "2017-06-02T09:51:43+00:00" + "time": "2017-06-24T09:29:48+00:00" }, { "name": "symfony/translation", - "version": "v3.3.2", + "version": "v3.3.4", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "dc3b2a0c6cfff60327ba1c043a82092735397543" + "reference": "35dd5fb003c90e8bd4d8cabdf94bf9c96d06fdc3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/dc3b2a0c6cfff60327ba1c043a82092735397543", - "reference": "dc3b2a0c6cfff60327ba1c043a82092735397543", + "url": "https://api.github.com/repos/symfony/translation/zipball/35dd5fb003c90e8bd4d8cabdf94bf9c96d06fdc3", + "reference": "35dd5fb003c90e8bd4d8cabdf94bf9c96d06fdc3", "shasum": "" }, "require": { @@ -3600,20 +3600,20 @@ ], "description": "Symfony Translation Component", "homepage": "https://symfony.com", - "time": "2017-05-22T07:42:36+00:00" + "time": "2017-06-24T16:45:30+00:00" }, { "name": "symfony/var-dumper", - "version": "v3.3.2", + "version": "v3.3.4", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "347c4247a3e40018810b476fcd5dec36d46d08dc" + "reference": "9ee920bba1d2ce877496dcafca7cbffff4dbe08a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/347c4247a3e40018810b476fcd5dec36d46d08dc", - "reference": "347c4247a3e40018810b476fcd5dec36d46d08dc", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/9ee920bba1d2ce877496dcafca7cbffff4dbe08a", + "reference": "9ee920bba1d2ce877496dcafca7cbffff4dbe08a", "shasum": "" }, "require": { @@ -3668,7 +3668,7 @@ "debug", "dump" ], - "time": "2017-06-02T09:10:29+00:00" + "time": "2017-07-05T13:02:37+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -4362,6 +4362,175 @@ ], "time": "2017-04-12T18:52:22+00:00" }, + { + "name": "php-mock/php-mock", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-mock/php-mock.git", + "reference": "bfa2d17d64dbf129073a7ba2051a96ce52749570" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-mock/php-mock/zipball/bfa2d17d64dbf129073a7ba2051a96ce52749570", + "reference": "bfa2d17d64dbf129073a7ba2051a96ce52749570", + "shasum": "" + }, + "require": { + "php": ">=5.5", + "phpunit/php-text-template": "^1" + }, + "replace": { + "malkusch/php-mock": "*" + }, + "require-dev": { + "phpunit/phpunit": "^4|^5" + }, + "suggest": { + "php-mock/php-mock-mockery": "Allows using PHPMockery for Mockery integration", + "php-mock/php-mock-phpunit": "Allows integration into PHPUnit testcase with the trait PHPMock." + }, + "type": "library", + "autoload": { + "psr-4": { + "phpmock\\": [ + "classes/", + "tests/unit/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "WTFPL" + ], + "authors": [ + { + "name": "Markus Malkusch", + "email": "markus@malkusch.de", + "homepage": "http://markus.malkusch.de", + "role": "Developer" + } + ], + "description": "PHP-Mock can mock built-in PHP functions (e.g. time()). PHP-Mock relies on PHP's namespace fallback policy. No further extension is needed.", + "homepage": "https://github.com/php-mock/php-mock", + "keywords": [ + "BDD", + "TDD", + "function", + "mock", + "stub", + "test", + "test double" + ], + "time": "2015-11-11T22:37:09+00:00" + }, + { + "name": "php-mock/php-mock-integration", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-mock/php-mock-integration.git", + "reference": "e83fb65dd20cd3cf250d554cbd4682b96b684f4b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-mock/php-mock-integration/zipball/e83fb65dd20cd3cf250d554cbd4682b96b684f4b", + "reference": "e83fb65dd20cd3cf250d554cbd4682b96b684f4b", + "shasum": "" + }, + "require": { + "php": ">=5.5", + "php-mock/php-mock": "^1", + "phpunit/php-text-template": "^1" + }, + "require-dev": { + "phpunit/phpunit": "^4|^5" + }, + "type": "library", + "autoload": { + "psr-4": { + "phpmock\\integration\\": "classes/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "WTFPL" + ], + "authors": [ + { + "name": "Markus Malkusch", + "email": "markus@malkusch.de", + "homepage": "http://markus.malkusch.de", + "role": "Developer" + } + ], + "description": "Integration package for PHP-Mock", + "homepage": "https://github.com/php-mock/php-mock-integration", + "keywords": [ + "BDD", + "TDD", + "function", + "mock", + "stub", + "test", + "test double" + ], + "time": "2015-10-26T21:21:42+00:00" + }, + { + "name": "php-mock/php-mock-phpunit", + "version": "1.1.2", + "source": { + "type": "git", + "url": "https://github.com/php-mock/php-mock-phpunit.git", + "reference": "359e3038c016cee4c8f8db6387bcab3fcdebada0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-mock/php-mock-phpunit/zipball/359e3038c016cee4c8f8db6387bcab3fcdebada0", + "reference": "359e3038c016cee4c8f8db6387bcab3fcdebada0", + "shasum": "" + }, + "require": { + "php": ">=5.5", + "php-mock/php-mock-integration": "^1", + "phpunit/phpunit": "^4.0.0 || ^5.0.0" + }, + "conflict": { + "phpunit/phpunit-mock-objects": "3.2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "phpmock\\phpunit\\": "classes/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "WTFPL" + ], + "authors": [ + { + "name": "Markus Malkusch", + "email": "markus@malkusch.de", + "homepage": "http://markus.malkusch.de", + "role": "Developer" + } + ], + "description": "Mock built-in PHP functions (e.g. time()) with PHPUnit. This package relies on PHP's namespace fallback policy. No further extension is needed.", + "homepage": "https://github.com/php-mock/php-mock-phpunit", + "keywords": [ + "BDD", + "TDD", + "function", + "mock", + "phpunit", + "stub", + "test", + "test double" + ], + "time": "2016-06-15T23:36:13+00:00" + }, { "name": "phpdocumentor/reflection-common", "version": "1.0", @@ -4904,16 +5073,16 @@ }, { "name": "phpunit/phpunit-mock-objects", - "version": "3.4.3", + "version": "3.4.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "3ab72b65b39b491e0c011e2e09bb2206c2aa8e24" + "reference": "a23b761686d50a560cc56233b9ecf49597cc9118" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/3ab72b65b39b491e0c011e2e09bb2206c2aa8e24", - "reference": "3ab72b65b39b491e0c011e2e09bb2206c2aa8e24", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/a23b761686d50a560cc56233b9ecf49597cc9118", + "reference": "a23b761686d50a560cc56233b9ecf49597cc9118", "shasum": "" }, "require": { @@ -4959,7 +5128,7 @@ "mock", "xunit" ], - "time": "2016-12-08T20:27:08+00:00" + "time": "2017-06-30T09:13:00+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -5586,7 +5755,7 @@ }, { "name": "symfony/class-loader", - "version": "v3.3.2", + "version": "v3.3.4", "source": { "type": "git", "url": "https://github.com/symfony/class-loader.git", @@ -5642,16 +5811,16 @@ }, { "name": "symfony/config", - "version": "v3.3.2", + "version": "v3.3.4", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "35716d4904e0506a7a5a9bcf23f854aeb5719bca" + "reference": "a094618deb9a3fe1c3cf500a796e167d0495a274" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/35716d4904e0506a7a5a9bcf23f854aeb5719bca", - "reference": "35716d4904e0506a7a5a9bcf23f854aeb5719bca", + "url": "https://api.github.com/repos/symfony/config/zipball/a094618deb9a3fe1c3cf500a796e167d0495a274", + "reference": "a094618deb9a3fe1c3cf500a796e167d0495a274", "shasum": "" }, "require": { @@ -5659,10 +5828,12 @@ "symfony/filesystem": "~2.8|~3.0" }, "conflict": { - "symfony/dependency-injection": "<3.3" + "symfony/dependency-injection": "<3.3", + "symfony/finder": "<3.3" }, "require-dev": { "symfony/dependency-injection": "~3.3", + "symfony/finder": "~3.3", "symfony/yaml": "~3.0" }, "suggest": { @@ -5698,20 +5869,20 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2017-06-02T18:07:20+00:00" + "time": "2017-06-16T12:40:34+00:00" }, { "name": "symfony/filesystem", - "version": "v3.3.2", + "version": "v3.3.4", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "c709670bf64721202ddbe4162846f250735842c0" + "reference": "311fa718389efbd8b627c272b9324a62437018cc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/c709670bf64721202ddbe4162846f250735842c0", - "reference": "c709670bf64721202ddbe4162846f250735842c0", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/311fa718389efbd8b627c272b9324a62437018cc", + "reference": "311fa718389efbd8b627c272b9324a62437018cc", "shasum": "" }, "require": { @@ -5747,11 +5918,11 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "time": "2017-05-28T14:08:56+00:00" + "time": "2017-06-24T09:29:48+00:00" }, { "name": "symfony/stopwatch", - "version": "v3.3.2", + "version": "v3.3.4", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", @@ -5800,16 +5971,16 @@ }, { "name": "symfony/yaml", - "version": "v3.3.2", + "version": "v3.3.4", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "9752a30000a8ca9f4b34b5227d15d0101b96b063" + "reference": "1f93a8d19b8241617f5074a123e282575b821df8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/9752a30000a8ca9f4b34b5227d15d0101b96b063", - "reference": "9752a30000a8ca9f4b34b5227d15d0101b96b063", + "url": "https://api.github.com/repos/symfony/yaml/zipball/1f93a8d19b8241617f5074a123e282575b821df8", + "reference": "1f93a8d19b8241617f5074a123e282575b821df8", "shasum": "" }, "require": { @@ -5851,7 +6022,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2017-06-02T22:05:06+00:00" + "time": "2017-06-15T12:58:50+00:00" }, { "name": "webmozart/assert", diff --git a/tests/Unit/Services/ApiKeyServiceTest.php b/tests/Unit/Services/ApiKeyServiceTest.php new file mode 100644 index 00000000..e48ead4c --- /dev/null +++ b/tests/Unit/Services/ApiKeyServiceTest.php @@ -0,0 +1,127 @@ +. + * + * 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; + +use Illuminate\Contracts\Encryption\Encrypter; +use Illuminate\Database\ConnectionInterface; +use Mockery as m; +use phpmock\phpunit\PHPMock; +use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; +use Pterodactyl\Services\ApiKeyService; +use Pterodactyl\Services\ApiPermissionService; +use Tests\TestCase; + +class ApiKeyServiceTest extends TestCase +{ + use PHPMock; + + /** + * @var \Illuminate\Database\ConnectionInterface + */ + protected $database; + + /** + * @var \Illuminate\Contracts\Encryption\Encrypter + */ + protected $encrypter; + + /** + * @var \Pterodactyl\Services\ApiPermissionService + */ + protected $permissions; + + /** + * @var \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\ApiKeyService + */ + protected $service; + + public function setUp() + { + parent::setUp(); + + $this->database = m::mock(ConnectionInterface::class); + $this->encrypter = m::mock(Encrypter::class); + $this->permissions = m::mock(ApiPermissionService::class); + $this->repository = m::mock(ApiKeyRepositoryInterface::class); + + $this->service = new ApiKeyService( + $this->repository, $this->database, $this->encrypter, $this->permissions + ); + } + + /** + * Test that the service is able to create a keypair and assign the correct permissions. + */ + public function test_create_function() + { + $this->getFunctionMock('\\Pterodactyl\\Services', 'random_bytes') + ->expects($this->exactly(2)) + ->willReturnCallback(function ($bytes) { + return hex2bin(str_pad('', $bytes * 2, '0')); + }); + + $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->encrypter->shouldReceive('encrypt')->with(str_pad('', 64, '0')) + ->once()->andReturn('encrypted-secret'); + + $this->repository->shouldReceive('create')->with([ + 'test-data' => 'test', + 'public' => str_pad('', 16, '0'), + 'secret' => 'encrypted-secret', + ], true, true)->once()->andReturn((object) ['id' => 1]); + + $this->permissions->shouldReceive('getPermissions')->withNoArgs()->once()->andReturn([ + '_user' => ['server' => ['list']], + 'server' => ['create'], + ]); + + $this->permissions->shouldReceive('create')->with(1, 'user.server-list')->once()->andReturnNull(); + $this->permissions->shouldReceive('create')->with(1, 'server-create')->once()->andReturnNull(); + + $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $response = $this->service->create( + ['test-data' => 'test'], ['invalid-node', 'server-list'], ['invalid-node', 'server-create'] + ); + + $this->assertNotEmpty($response); + $this->assertEquals(str_pad('', 64, '0'), $response); + } + + /** + * Test that an API key can be revoked. + */ + public function test_revoke_function() + { + $this->repository->shouldReceive('delete')->with(1)->once()->andReturn(true); + + $this->assertTrue($this->service->revoke(1)); + } +} diff --git a/tests/Unit/Services/ApiPermissionServiceTest.php b/tests/Unit/Services/ApiPermissionServiceTest.php new file mode 100644 index 00000000..8e730623 --- /dev/null +++ b/tests/Unit/Services/ApiPermissionServiceTest.php @@ -0,0 +1,77 @@ +. + * + * 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; + +use Mockery as m; +use Pterodactyl\Contracts\Repository\ApiPermissionRepositoryInterface; +use Pterodactyl\Models\APIPermission; +use Pterodactyl\Services\ApiPermissionService; +use Tests\TestCase; + +class ApiPermissionServiceTest extends TestCase +{ + /** + * @var \Pterodactyl\Contracts\Repository\ApiPermissionRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\ApiPermissionService + */ + protected $service; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->repository = m::mock(ApiPermissionRepositoryInterface::class); + $this->service = new ApiPermissionService($this->repository); + } + + /** + * Test that a new API permission can be assigned to a key. + */ + public function test_create_function() + { + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('create')->with(['key_id' => 1, 'permission' => 'test-permission']) + ->once()->andReturn(true); + + $this->assertTrue($this->service->create(1, 'test-permission')); + } + + /** + * Test that function returns an array of all the permissions available as defined on the model. + */ + public function test_get_permissions_function() + { + $this->repository->shouldReceive('getModel')->withNoArgs()->once()->andReturn(new APIPermission()); + + $this->assertEquals(APIPermission::CONST_PERMISSIONS, $this->service->getPermissions()); + } +} From 761d34f17801859eeae8949d01bcfa60d1c69680 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 8 Jul 2017 14:17:07 -0500 Subject: [PATCH 21/99] don't try to apply columns in the relations field... --- app/Repositories/Eloquent/EloquentRepository.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Repositories/Eloquent/EloquentRepository.php b/app/Repositories/Eloquent/EloquentRepository.php index 2da2eccd..86f07db8 100644 --- a/app/Repositories/Eloquent/EloquentRepository.php +++ b/app/Repositories/Eloquent/EloquentRepository.php @@ -126,7 +126,7 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf } } - return ($this->withFresh) ? $instance->fresh($this->getColumns()) : $saved; + return ($this->withFresh) ? $instance->fresh() : $saved; } /** From 2588c25b0b3db85613cdc2bcc6989621a6872b35 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 8 Jul 2017 15:04:59 -0500 Subject: [PATCH 22/99] Service refactor to improve organization --- .../Repository/RepositoryInterface.php | 8 ++++ .../Controllers/Admin/DatabaseController.php | 12 ++--- .../Controllers/Admin/LocationController.php | 10 ++-- app/Http/Controllers/Admin/UserController.php | 6 +-- app/Http/Controllers/Base/APIController.php | 47 ++++++++++++------- .../Eloquent/EloquentRepository.php | 18 ++++++- .../DatabaseHostService.php | 2 +- .../{ => Administrative}/LocationService.php | 2 +- .../{ => Administrative}/UserService.php | 2 +- .../DatabaseHostServiceTest.php | 6 +-- .../LocationServiceTest.php | 4 +- .../{ => Administrative}/UserServiceTest.php | 4 +- 12 files changed, 79 insertions(+), 42 deletions(-) rename app/Services/{ => Administrative}/DatabaseHostService.php (98%) rename app/Services/{ => Administrative}/LocationService.php (98%) rename app/Services/{ => Administrative}/UserService.php (99%) rename tests/Unit/Services/{ => Administrative}/DatabaseHostServiceTest.php (97%) rename tests/Unit/Services/{ => Administrative}/LocationServiceTest.php (96%) rename tests/Unit/Services/{ => Administrative}/UserServiceTest.php (98%) diff --git a/app/Contracts/Repository/RepositoryInterface.php b/app/Contracts/Repository/RepositoryInterface.php index 470dc3eb..747fb03f 100644 --- a/app/Contracts/Repository/RepositoryInterface.php +++ b/app/Contracts/Repository/RepositoryInterface.php @@ -108,6 +108,14 @@ interface RepositoryInterface */ public function findWhere(array $fields); + /** + * Find and return the first matching instance for the given fields. + * + * @param array $fields + * @return mixed + */ + public function findFirstWhere(array $fields); + /** * Update a given ID with the passed array of fields. * diff --git a/app/Http/Controllers/Admin/DatabaseController.php b/app/Http/Controllers/Admin/DatabaseController.php index 94d60a0c..0f48987f 100644 --- a/app/Http/Controllers/Admin/DatabaseController.php +++ b/app/Http/Controllers/Admin/DatabaseController.php @@ -28,7 +28,7 @@ use Pterodactyl\Models\Location; use Pterodactyl\Models\DatabaseHost; use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Services\DatabaseHostService; +use Pterodactyl\Services\Administrative\DatabaseHostService; use Pterodactyl\Http\Requests\Admin\DatabaseHostFormRequest; class DatabaseController extends Controller @@ -49,17 +49,17 @@ class DatabaseController extends Controller protected $locationModel; /** - * @var \Pterodactyl\Services\DatabaseHostService + * @var \Pterodactyl\Services\Administrative\DatabaseHostService */ protected $service; /** * DatabaseController constructor. * - * @param \Prologue\Alerts\AlertsMessageBag $alert - * @param \Pterodactyl\Models\DatabaseHost $hostModel - * @param \Pterodactyl\Models\Location $locationModel - * @param \Pterodactyl\Services\DatabaseHostService $service + * @param \Prologue\Alerts\AlertsMessageBag $alert + * @param \Pterodactyl\Models\DatabaseHost $hostModel + * @param \Pterodactyl\Models\Location $locationModel + * @param \Pterodactyl\Services\Administrative\DatabaseHostService $service */ public function __construct( AlertsMessageBag $alert, diff --git a/app/Http/Controllers/Admin/LocationController.php b/app/Http/Controllers/Admin/LocationController.php index 4a6ffb35..db358e5c 100644 --- a/app/Http/Controllers/Admin/LocationController.php +++ b/app/Http/Controllers/Admin/LocationController.php @@ -26,10 +26,10 @@ namespace Pterodactyl\Http\Controllers\Admin; use Pterodactyl\Models\Location; use Prologue\Alerts\AlertsMessageBag; -use Pterodactyl\Services\LocationService; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Http\Requests\Admin\LocationRequest; +use Pterodactyl\Services\Administrative\LocationService; class LocationController extends Controller { @@ -44,16 +44,16 @@ class LocationController extends Controller protected $locationModel; /** - * @var \Pterodactyl\Services\LocationService + * @var \Pterodactyl\Services\Administrative\\LocationService */ protected $service; /** * LocationController constructor. * - * @param \Prologue\Alerts\AlertsMessageBag $alert - * @param \Pterodactyl\Models\Location $locationModel - * @param \Pterodactyl\Services\LocationService $service + * @param \Prologue\Alerts\AlertsMessageBag $alert + * @param \Pterodactyl\Models\Location $locationModel + * @param \Pterodactyl\Services\Administrative\LocationService $service */ public function __construct( AlertsMessageBag $alert, diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index 40379f0f..151d9774 100644 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -28,7 +28,7 @@ use Illuminate\Http\Request; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Models\User; use Prologue\Alerts\AlertsMessageBag; -use Pterodactyl\Services\UserService; +use Pterodactyl\Services\Administrative\UserService; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Http\Requests\Admin\UserFormRequest; @@ -41,7 +41,7 @@ class UserController extends Controller protected $alert; /** - * @var \Pterodactyl\Services\UserService + * @var \Pterodactyl\Services\Administrative\UserService */ protected $service; @@ -59,7 +59,7 @@ class UserController extends Controller * UserController constructor. * * @param \Prologue\Alerts\AlertsMessageBag $alert - * @param \Pterodactyl\Services\UserService $service + * @param \Pterodactyl\Services\Administrative\UserService $service * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository * @param \Pterodactyl\Models\User $model */ diff --git a/app/Http/Controllers/Base/APIController.php b/app/Http/Controllers/Base/APIController.php index 72995d00..94783036 100644 --- a/app/Http/Controllers/Base/APIController.php +++ b/app/Http/Controllers/Base/APIController.php @@ -26,12 +26,13 @@ namespace Pterodactyl\Http\Controllers\Base; use Illuminate\Http\Request; -use Pterodactyl\Models\APIKey; use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Models\APIPermission; use Pterodactyl\Services\ApiKeyService; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Http\Requests\ApiKeyRequest; +use Pterodactyl\Exceptions\Repository\RecordNotFoundException; +use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; class APIController extends Controller { @@ -41,9 +42,9 @@ class APIController extends Controller protected $alert; /** - * @var \Pterodactyl\Models\APIKey + * @var \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface */ - protected $model; + protected $repository; /** * @var \Pterodactyl\Services\ApiKeyService @@ -53,13 +54,17 @@ class APIController extends Controller /** * APIController constructor. * - * @param \Prologue\Alerts\AlertsMessageBag $alert - * @param \Pterodactyl\Services\ApiKeyService $service + * @param \Prologue\Alerts\AlertsMessageBag $alert + * @param \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface $repository + * @param \Pterodactyl\Services\ApiKeyService $service */ - public function __construct(AlertsMessageBag $alert, ApiKeyService $service, APIKey $model) - { + public function __construct( + AlertsMessageBag $alert, + ApiKeyRepositoryInterface $repository, + ApiKeyService $service + ) { $this->alert = $alert; - $this->model = $model; + $this->repository = $repository; $this->service = $service; } @@ -72,7 +77,7 @@ class APIController extends Controller public function index(Request $request) { return view('base.api.index', [ - 'keys' => APIKey::where('user_id', $request->user()->id)->get(), + 'keys' => $this->repository->findWhere([['user_id', '=', $request->user()->id]]), ]); } @@ -85,8 +90,8 @@ class APIController extends Controller { return view('base.api.new', [ 'permissions' => [ - 'user' => collect(APIPermission::PERMISSIONS)->pull('_user'), - 'admin' => collect(APIPermission::PERMISSIONS)->except('_user')->toArray(), + 'user' => collect(APIPermission::CONST_PERMISSIONS)->pull('_user'), + 'admin' => collect(APIPermission::CONST_PERMISSIONS)->except('_user')->toArray(), ], ]); } @@ -113,7 +118,11 @@ class APIController extends Controller 'memo' => $request->input('memo'), ], $request->input('permissions') ?? [], $adminPermissions); - $this->alert->success('An API Key-Pair has successfully been generated. The API secret for this public key is shown below and will not be shown again.

' . $secret . '')->flash(); + $this->alert->success( + "An API Key-Pair has successfully been generated. The API secret + for this public key is shown below and will not be shown again. +

{$secret}" + )->flash(); return redirect()->route('account.api'); } @@ -127,12 +136,16 @@ class APIController extends Controller */ public function revoke(Request $request, $key) { - $key = $this->model->newQuery() - ->where('user_id', $request->user()->id) - ->where('public', $key) - ->firstOrFail(); + try { + $key = $this->repository->withColumns('id')->findFirstWhere([ + ['user_id', '=', $request->user()->id], + ['public', $key], + ]); - $this->service->revoke($key); + $this->service->revoke($key->id); + } catch (RecordNotFoundException $ex) { + return abort(404); + } return response('', 204); } diff --git a/app/Repositories/Eloquent/EloquentRepository.php b/app/Repositories/Eloquent/EloquentRepository.php index 86f07db8..b6af22f7 100644 --- a/app/Repositories/Eloquent/EloquentRepository.php +++ b/app/Repositories/Eloquent/EloquentRepository.php @@ -83,10 +83,26 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf /** * {@inheritdoc} + * @return \Illuminate\Database\Eloquent\Collection */ public function findWhere(array $fields) { - // TODO: Implement findWhere() method. + return $this->getBuilder()->where($fields)->get($this->getColumns()); + } + + /** + * {@inheritdoc} + * @return \Illuminate\Database\Eloquent\Model + */ + public function findFirstWhere(array $fields) + { + $instance = $this->getBuilder()->where($fields)->first($this->getColumns()); + + if (! $instance) { + throw new RecordNotFoundException(); + } + + return $instance; } /** diff --git a/app/Services/DatabaseHostService.php b/app/Services/Administrative/DatabaseHostService.php similarity index 98% rename from app/Services/DatabaseHostService.php rename to app/Services/Administrative/DatabaseHostService.php index b3f2be41..24acd63a 100644 --- a/app/Services/DatabaseHostService.php +++ b/app/Services/Administrative/DatabaseHostService.php @@ -22,7 +22,7 @@ * SOFTWARE. */ -namespace Pterodactyl\Services; +namespace Pterodactyl\Services\Administrative; use Illuminate\Database\DatabaseManager; use Illuminate\Contracts\Encryption\Encrypter; diff --git a/app/Services/LocationService.php b/app/Services/Administrative/LocationService.php similarity index 98% rename from app/Services/LocationService.php rename to app/Services/Administrative/LocationService.php index 2bf7a41e..ae050ce3 100644 --- a/app/Services/LocationService.php +++ b/app/Services/Administrative/LocationService.php @@ -22,7 +22,7 @@ * SOFTWARE. */ -namespace Pterodactyl\Services; +namespace Pterodactyl\Services\Administrative; use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; diff --git a/app/Services/UserService.php b/app/Services/Administrative/UserService.php similarity index 99% rename from app/Services/UserService.php rename to app/Services/Administrative/UserService.php index a7c87c57..a7503b6b 100644 --- a/app/Services/UserService.php +++ b/app/Services/Administrative/UserService.php @@ -22,7 +22,7 @@ * SOFTWARE. */ -namespace Pterodactyl\Services; +namespace Pterodactyl\Services\Administrative; use Illuminate\Foundation\Application; use Illuminate\Contracts\Hashing\Hasher; diff --git a/tests/Unit/Services/DatabaseHostServiceTest.php b/tests/Unit/Services/Administrative/DatabaseHostServiceTest.php similarity index 97% rename from tests/Unit/Services/DatabaseHostServiceTest.php rename to tests/Unit/Services/Administrative/DatabaseHostServiceTest.php index 1239c1ec..0d4e31be 100644 --- a/tests/Unit/Services/DatabaseHostServiceTest.php +++ b/tests/Unit/Services/Administrative/DatabaseHostServiceTest.php @@ -22,15 +22,15 @@ * SOFTWARE. */ -namespace Tests\Unit\Services; +namespace Tests\Unit\Services\Administrative; use Mockery as m; use Tests\TestCase; use Illuminate\Database\DatabaseManager; -use Pterodactyl\Services\DatabaseHostService; use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Extensions\DynamicDatabaseConnection; use Pterodactyl\Contracts\Repository\DatabaseHostInterface; +use Pterodactyl\Services\Administrative\DatabaseHostService; class DatabaseHostServiceTest extends TestCase { @@ -55,7 +55,7 @@ class DatabaseHostServiceTest extends TestCase protected $repository; /** - * @var \Pterodactyl\Services\DatabaseHostService + * @var \Pterodactyl\Services\Administrative\DatabaseHostService */ protected $service; diff --git a/tests/Unit/Services/LocationServiceTest.php b/tests/Unit/Services/Administrative/LocationServiceTest.php similarity index 96% rename from tests/Unit/Services/LocationServiceTest.php rename to tests/Unit/Services/Administrative/LocationServiceTest.php index 442b02b5..1feda6f1 100644 --- a/tests/Unit/Services/LocationServiceTest.php +++ b/tests/Unit/Services/Administrative/LocationServiceTest.php @@ -26,7 +26,7 @@ namespace Tests\Unit\Services; use Mockery as m; use Tests\TestCase; -use Pterodactyl\Services\LocationService; +use Pterodactyl\Services\Administrative\LocationService; use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; class LocationServiceTest extends TestCase @@ -37,7 +37,7 @@ class LocationServiceTest extends TestCase protected $repository; /** - * @var \Pterodactyl\Services\LocationService + * @var \Pterodactyl\Services\Administrative\LocationService */ protected $service; diff --git a/tests/Unit/Services/UserServiceTest.php b/tests/Unit/Services/Administrative/UserServiceTest.php similarity index 98% rename from tests/Unit/Services/UserServiceTest.php rename to tests/Unit/Services/Administrative/UserServiceTest.php index ede6adee..a80a277f 100644 --- a/tests/Unit/Services/UserServiceTest.php +++ b/tests/Unit/Services/Administrative/UserServiceTest.php @@ -26,12 +26,12 @@ namespace Tests\Unit\Services; use Mockery as m; use Tests\TestCase; -use Pterodactyl\Services\UserService; use Illuminate\Foundation\Application; use Illuminate\Contracts\Hashing\Hasher; use Illuminate\Database\ConnectionInterface; use Illuminate\Notifications\ChannelManager; use Pterodactyl\Notifications\AccountCreated; +use Pterodactyl\Services\Administrative\UserService; use Pterodactyl\Services\Helpers\TemporaryPasswordService; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; @@ -68,7 +68,7 @@ class UserServiceTest extends TestCase protected $repository; /** - * @var \Pterodactyl\Services\UserService + * @var \Pterodactyl\Services\Administrative\UserService */ protected $service; From 8953f83f87c6d0d038e1e5506779156f011f7b70 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 8 Jul 2017 15:51:13 -0500 Subject: [PATCH 23/99] Add migrations to handle cascade deletions for servers and users --- app/Providers/MacroServiceProvider.php | 3 +- ...eUserPermissionsToDeleteOnUserDeletion.php | 52 +++++++++++++++++++ ...llocationToReferenceNullOnServerDelete.php | 36 +++++++++++++ ...DeletionWhenAServerOrVariableIsDeleted.php | 40 ++++++++++++++ 4 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 database/migrations/2017_07_08_152806_ChangeUserPermissionsToDeleteOnUserDeletion.php create mode 100644 database/migrations/2017_07_08_154416_SetAllocationToReferenceNullOnServerDelete.php create mode 100644 database/migrations/2017_07_08_154650_CascadeDeletionWhenAServerOrVariableIsDeleted.php diff --git a/app/Providers/MacroServiceProvider.php b/app/Providers/MacroServiceProvider.php index 8dd08f73..600c0d3f 100644 --- a/app/Providers/MacroServiceProvider.php +++ b/app/Providers/MacroServiceProvider.php @@ -30,6 +30,7 @@ use Carbon; use Request; use Pterodactyl\Models\APIKey; use Illuminate\Support\ServiceProvider; +use Pterodactyl\Services\ApiKeyService; class MacroServiceProvider extends ServiceProvider { @@ -60,7 +61,7 @@ class MacroServiceProvider extends ServiceProvider $parts = explode('.', Request::bearerToken()); - if (count($parts) === 2 && strlen($parts[0]) === APIKey::PUBLIC_KEY_LEN) { + if (count($parts) === 2 && strlen($parts[0]) === ApiKeyService::PUB_CRYPTO_BYTES * 2) { // Because the key itself isn't changing frequently, we simply cache this for // 15 minutes to speed up the API and keep requests flowing. return Cache::tags([ diff --git a/database/migrations/2017_07_08_152806_ChangeUserPermissionsToDeleteOnUserDeletion.php b/database/migrations/2017_07_08_152806_ChangeUserPermissionsToDeleteOnUserDeletion.php new file mode 100644 index 00000000..eaa7a4bf --- /dev/null +++ b/database/migrations/2017_07_08_152806_ChangeUserPermissionsToDeleteOnUserDeletion.php @@ -0,0 +1,52 @@ +dropForeign(['subuser_id']); + + $table->foreign('subuser_id')->references('id')->on('subusers')->onDelete('cascade'); + }); + + Schema::table('subusers', function (Blueprint $table) { + $table->dropForeign(['user_id']); + $table->dropForeign(['server_id']); + + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + $table->foreign('server_id')->references('id')->on('servers')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('subusers', function (Blueprint $table) { + $table->dropForeign(['user_id']); + $table->dropForeign(['server_id']); + + $table->foreign('user_id')->references('id')->on('users'); + $table->foreign('server_id')->references('id')->on('servers'); + }); + + Schema::table('permissions', function (Blueprint $table) { + $table->dropForeign(['subuser_id']); + + $table->foreign('subuser_id')->references('id')->on('subusers'); + }); + } +} diff --git a/database/migrations/2017_07_08_154416_SetAllocationToReferenceNullOnServerDelete.php b/database/migrations/2017_07_08_154416_SetAllocationToReferenceNullOnServerDelete.php new file mode 100644 index 00000000..76e475b0 --- /dev/null +++ b/database/migrations/2017_07_08_154416_SetAllocationToReferenceNullOnServerDelete.php @@ -0,0 +1,36 @@ +dropForeign(['server_id']); + + $table->foreign('server_id')->references('id')->on('servers')->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('allocations', function (Blueprint $table) { + $table->dropForeign(['server_id']); + + $table->foreign('server_id')->references('id')->on('servers'); + }); + } +} diff --git a/database/migrations/2017_07_08_154650_CascadeDeletionWhenAServerOrVariableIsDeleted.php b/database/migrations/2017_07_08_154650_CascadeDeletionWhenAServerOrVariableIsDeleted.php new file mode 100644 index 00000000..f599f02c --- /dev/null +++ b/database/migrations/2017_07_08_154650_CascadeDeletionWhenAServerOrVariableIsDeleted.php @@ -0,0 +1,40 @@ +dropForeign(['server_id']); + $table->dropForeign(['variable_id']); + + $table->foreign('server_id')->references('id')->on('servers')->onDelete('cascade'); + $table->foreign('variable_id')->references('id')->on('service_variables')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('server_variables', function (Blueprint $table) { + $table->dropForeign(['server_id']); + $table->dropForeign(['variable_id']); + + $table->foreign('server_id')->references('id')->on('servers'); + $table->foreign('variable_id')->references('id')->on('service_variables'); + }); + } +} From 1f4f6024cc2e217278d66e94b638880fac7f02dc Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 9 Jul 2017 12:29:18 -0500 Subject: [PATCH 24/99] Refactor (again) --- .../{ApiKeyService.php => Api/KeyService.php} | 10 +++++----- .../PermissionService.php} | 4 ++-- .../DatabaseHostService.php | 2 +- .../{Administrative => }/LocationService.php | 2 +- .../{Administrative => }/UserService.php | 2 +- .../KeyServiceTest.php} | 18 +++++++++--------- .../PermissionServiceTest.php} | 8 ++++---- .../DatabaseHostServiceTest.php | 4 ++-- .../LocationServiceTest.php | 4 ++-- .../{Administrative => }/UserServiceTest.php | 4 ++-- 10 files changed, 29 insertions(+), 29 deletions(-) rename app/Services/{ApiKeyService.php => Api/KeyService.php} (95%) rename app/Services/{ApiPermissionService.php => Api/PermissionService.php} (97%) rename app/Services/{Administrative => Database}/DatabaseHostService.php (98%) rename app/Services/{Administrative => }/LocationService.php (98%) rename app/Services/{Administrative => }/UserService.php (99%) rename tests/Unit/Services/{ApiKeyServiceTest.php => Api/KeyServiceTest.php} (89%) rename tests/Unit/Services/{ApiPermissionServiceTest.php => Api/PermissionServiceTest.php} (92%) rename tests/Unit/Services/{Administrative => Database}/DatabaseHostServiceTest.php (98%) rename tests/Unit/Services/{Administrative => }/LocationServiceTest.php (96%) rename tests/Unit/Services/{Administrative => }/UserServiceTest.php (98%) diff --git a/app/Services/ApiKeyService.php b/app/Services/Api/KeyService.php similarity index 95% rename from app/Services/ApiKeyService.php rename to app/Services/Api/KeyService.php index d4a3c6e2..4bf03da5 100644 --- a/app/Services/ApiKeyService.php +++ b/app/Services/Api/KeyService.php @@ -22,13 +22,13 @@ * SOFTWARE. */ -namespace Pterodactyl\Services; +namespace Pterodactyl\Services\Api; use Illuminate\Database\ConnectionInterface; use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; -class ApiKeyService +class KeyService { const PUB_CRYPTO_BYTES = 8; const PRIV_CRYPTO_BYTES = 32; @@ -44,7 +44,7 @@ class ApiKeyService protected $encrypter; /** - * @var \Pterodactyl\Services\ApiPermissionService + * @var \Pterodactyl\Services\Api\PermissionService */ protected $permissionService; @@ -59,13 +59,13 @@ class ApiKeyService * @param \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface $repository * @param \Illuminate\Database\ConnectionInterface $database * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter - * @param \Pterodactyl\Services\ApiPermissionService $permissionService + * @param \Pterodactyl\Services\Api\PermissionService $permissionService */ public function __construct( ApiKeyRepositoryInterface $repository, ConnectionInterface $database, Encrypter $encrypter, - ApiPermissionService $permissionService + PermissionService $permissionService ) { $this->repository = $repository; $this->database = $database; diff --git a/app/Services/ApiPermissionService.php b/app/Services/Api/PermissionService.php similarity index 97% rename from app/Services/ApiPermissionService.php rename to app/Services/Api/PermissionService.php index 40ca4a1c..a669c726 100644 --- a/app/Services/ApiPermissionService.php +++ b/app/Services/Api/PermissionService.php @@ -22,11 +22,11 @@ * SOFTWARE. */ -namespace Pterodactyl\Services; +namespace Pterodactyl\Services\Api; use Pterodactyl\Contracts\Repository\ApiPermissionRepositoryInterface; -class ApiPermissionService +class PermissionService { /** * @var \Pterodactyl\Contracts\Repository\ApiPermissionRepositoryInterface diff --git a/app/Services/Administrative/DatabaseHostService.php b/app/Services/Database/DatabaseHostService.php similarity index 98% rename from app/Services/Administrative/DatabaseHostService.php rename to app/Services/Database/DatabaseHostService.php index 24acd63a..b0dbb157 100644 --- a/app/Services/Administrative/DatabaseHostService.php +++ b/app/Services/Database/DatabaseHostService.php @@ -22,7 +22,7 @@ * SOFTWARE. */ -namespace Pterodactyl\Services\Administrative; +namespace Pterodactyl\Services\Database; use Illuminate\Database\DatabaseManager; use Illuminate\Contracts\Encryption\Encrypter; diff --git a/app/Services/Administrative/LocationService.php b/app/Services/LocationService.php similarity index 98% rename from app/Services/Administrative/LocationService.php rename to app/Services/LocationService.php index ae050ce3..2bf7a41e 100644 --- a/app/Services/Administrative/LocationService.php +++ b/app/Services/LocationService.php @@ -22,7 +22,7 @@ * SOFTWARE. */ -namespace Pterodactyl\Services\Administrative; +namespace Pterodactyl\Services; use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; diff --git a/app/Services/Administrative/UserService.php b/app/Services/UserService.php similarity index 99% rename from app/Services/Administrative/UserService.php rename to app/Services/UserService.php index a7503b6b..a7c87c57 100644 --- a/app/Services/Administrative/UserService.php +++ b/app/Services/UserService.php @@ -22,7 +22,7 @@ * SOFTWARE. */ -namespace Pterodactyl\Services\Administrative; +namespace Pterodactyl\Services; use Illuminate\Foundation\Application; use Illuminate\Contracts\Hashing\Hasher; diff --git a/tests/Unit/Services/ApiKeyServiceTest.php b/tests/Unit/Services/Api/KeyServiceTest.php similarity index 89% rename from tests/Unit/Services/ApiKeyServiceTest.php rename to tests/Unit/Services/Api/KeyServiceTest.php index e48ead4c..ef48277a 100644 --- a/tests/Unit/Services/ApiKeyServiceTest.php +++ b/tests/Unit/Services/Api/KeyServiceTest.php @@ -22,18 +22,18 @@ * SOFTWARE. */ -namespace Tests\Unit\Services; +namespace Tests\Unit\Services\Api; use Illuminate\Contracts\Encryption\Encrypter; use Illuminate\Database\ConnectionInterface; use Mockery as m; use phpmock\phpunit\PHPMock; use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; -use Pterodactyl\Services\ApiKeyService; -use Pterodactyl\Services\ApiPermissionService; +use Pterodactyl\Services\Api\KeyService; +use Pterodactyl\Services\Api\PermissionService; use Tests\TestCase; -class ApiKeyServiceTest extends TestCase +class KeyServiceTest extends TestCase { use PHPMock; @@ -48,7 +48,7 @@ class ApiKeyServiceTest extends TestCase protected $encrypter; /** - * @var \Pterodactyl\Services\ApiPermissionService + * @var \Pterodactyl\Services\Api\PermissionService */ protected $permissions; @@ -58,7 +58,7 @@ class ApiKeyServiceTest extends TestCase protected $repository; /** - * @var \Pterodactyl\Services\ApiKeyService + * @var \Pterodactyl\Services\Api\KeyService */ protected $service; @@ -68,10 +68,10 @@ class ApiKeyServiceTest extends TestCase $this->database = m::mock(ConnectionInterface::class); $this->encrypter = m::mock(Encrypter::class); - $this->permissions = m::mock(ApiPermissionService::class); + $this->permissions = m::mock(PermissionService::class); $this->repository = m::mock(ApiKeyRepositoryInterface::class); - $this->service = new ApiKeyService( + $this->service = new KeyService( $this->repository, $this->database, $this->encrypter, $this->permissions ); } @@ -81,7 +81,7 @@ class ApiKeyServiceTest extends TestCase */ public function test_create_function() { - $this->getFunctionMock('\\Pterodactyl\\Services', 'random_bytes') + $this->getFunctionMock('\\Pterodactyl\\Services\\Api', 'random_bytes') ->expects($this->exactly(2)) ->willReturnCallback(function ($bytes) { return hex2bin(str_pad('', $bytes * 2, '0')); diff --git a/tests/Unit/Services/ApiPermissionServiceTest.php b/tests/Unit/Services/Api/PermissionServiceTest.php similarity index 92% rename from tests/Unit/Services/ApiPermissionServiceTest.php rename to tests/Unit/Services/Api/PermissionServiceTest.php index 8e730623..5c687c9b 100644 --- a/tests/Unit/Services/ApiPermissionServiceTest.php +++ b/tests/Unit/Services/Api/PermissionServiceTest.php @@ -27,10 +27,10 @@ namespace Tests\Unit\Services; use Mockery as m; use Pterodactyl\Contracts\Repository\ApiPermissionRepositoryInterface; use Pterodactyl\Models\APIPermission; -use Pterodactyl\Services\ApiPermissionService; +use Pterodactyl\Services\Api\PermissionService; use Tests\TestCase; -class ApiPermissionServiceTest extends TestCase +class PermissionServiceTest extends TestCase { /** * @var \Pterodactyl\Contracts\Repository\ApiPermissionRepositoryInterface @@ -38,7 +38,7 @@ class ApiPermissionServiceTest extends TestCase protected $repository; /** - * @var \Pterodactyl\Services\ApiPermissionService + * @var \Pterodactyl\Services\Api\PermissionService */ protected $service; @@ -50,7 +50,7 @@ class ApiPermissionServiceTest extends TestCase parent::setUp(); $this->repository = m::mock(ApiPermissionRepositoryInterface::class); - $this->service = new ApiPermissionService($this->repository); + $this->service = new PermissionService($this->repository); } /** diff --git a/tests/Unit/Services/Administrative/DatabaseHostServiceTest.php b/tests/Unit/Services/Database/DatabaseHostServiceTest.php similarity index 98% rename from tests/Unit/Services/Administrative/DatabaseHostServiceTest.php rename to tests/Unit/Services/Database/DatabaseHostServiceTest.php index 0d4e31be..55a97ae3 100644 --- a/tests/Unit/Services/Administrative/DatabaseHostServiceTest.php +++ b/tests/Unit/Services/Database/DatabaseHostServiceTest.php @@ -30,7 +30,7 @@ use Illuminate\Database\DatabaseManager; use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Extensions\DynamicDatabaseConnection; use Pterodactyl\Contracts\Repository\DatabaseHostInterface; -use Pterodactyl\Services\Administrative\DatabaseHostService; +use Pterodactyl\Services\Database\DatabaseHostService; class DatabaseHostServiceTest extends TestCase { @@ -55,7 +55,7 @@ class DatabaseHostServiceTest extends TestCase protected $repository; /** - * @var \Pterodactyl\Services\Administrative\DatabaseHostService + * @var \Pterodactyl\Services\Database\DatabaseHostService */ protected $service; diff --git a/tests/Unit/Services/Administrative/LocationServiceTest.php b/tests/Unit/Services/LocationServiceTest.php similarity index 96% rename from tests/Unit/Services/Administrative/LocationServiceTest.php rename to tests/Unit/Services/LocationServiceTest.php index 1feda6f1..442b02b5 100644 --- a/tests/Unit/Services/Administrative/LocationServiceTest.php +++ b/tests/Unit/Services/LocationServiceTest.php @@ -26,7 +26,7 @@ namespace Tests\Unit\Services; use Mockery as m; use Tests\TestCase; -use Pterodactyl\Services\Administrative\LocationService; +use Pterodactyl\Services\LocationService; use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; class LocationServiceTest extends TestCase @@ -37,7 +37,7 @@ class LocationServiceTest extends TestCase protected $repository; /** - * @var \Pterodactyl\Services\Administrative\LocationService + * @var \Pterodactyl\Services\LocationService */ protected $service; diff --git a/tests/Unit/Services/Administrative/UserServiceTest.php b/tests/Unit/Services/UserServiceTest.php similarity index 98% rename from tests/Unit/Services/Administrative/UserServiceTest.php rename to tests/Unit/Services/UserServiceTest.php index a80a277f..f02c5652 100644 --- a/tests/Unit/Services/Administrative/UserServiceTest.php +++ b/tests/Unit/Services/UserServiceTest.php @@ -31,7 +31,7 @@ use Illuminate\Contracts\Hashing\Hasher; use Illuminate\Database\ConnectionInterface; use Illuminate\Notifications\ChannelManager; use Pterodactyl\Notifications\AccountCreated; -use Pterodactyl\Services\Administrative\UserService; +use Pterodactyl\Services\UserService; use Pterodactyl\Services\Helpers\TemporaryPasswordService; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; @@ -68,7 +68,7 @@ class UserServiceTest extends TestCase protected $repository; /** - * @var \Pterodactyl\Services\Administrative\UserService + * @var \Pterodactyl\Services\UserService */ protected $service; From bc3366b10dac490b995139a9d252e9f4c064a634 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 15 Jul 2017 11:52:34 -0500 Subject: [PATCH 25/99] Repository interface improvements --- ...hp => DatabaseHostRepositoryInterface.php} | 2 +- .../DatabaseRepositoryInterface.php | 98 +++++++++ .../Repository/RepositoryInterface.php | 7 + .../Repository/ServerRepositoryInterface.php | 38 ++++ .../Repository/ServiceRepositoryInterface.php | 36 ++++ app/Extensions/DynamicDatabaseConnection.php | 12 +- .../Controllers/Admin/DatabaseController.php | 6 +- .../Controllers/Admin/ServersController.php | 69 +++++-- app/Models/Database.php | 20 ++ app/Models/DatabaseHost.php | 32 ++- app/Providers/RepositoryServiceProvider.php | 13 +- .../Attributes/SearchableRepository.php | 51 +++++ .../Eloquent/DatabaseHostRepository.php | 4 +- .../Eloquent/DatabaseRepository.php | 155 +++++++++++++++ .../Eloquent/EloquentRepository.php | 8 + .../Eloquent/ServerRepository.php | 54 +++++ .../Eloquent/ServiceRepository.php | 60 ++++++ app/Repositories/Eloquent/UserRepository.php | 23 +-- app/Repositories/Old/DatabaseRepository.php | 111 ----------- app/Services/Database/CreationService.php | 188 ++++++++++++++++++ app/Services/Database/DatabaseHostService.php | 14 +- config/pterodactyl.php | 1 + .../Database/DatabaseHostServiceTest.php | 6 +- 23 files changed, 829 insertions(+), 179 deletions(-) rename app/Contracts/Repository/{DatabaseHostInterface.php => DatabaseHostRepositoryInterface.php} (95%) create mode 100644 app/Contracts/Repository/DatabaseRepositoryInterface.php create mode 100644 app/Contracts/Repository/ServerRepositoryInterface.php create mode 100644 app/Contracts/Repository/ServiceRepositoryInterface.php create mode 100644 app/Repositories/Eloquent/Attributes/SearchableRepository.php create mode 100644 app/Repositories/Eloquent/DatabaseRepository.php create mode 100644 app/Repositories/Eloquent/ServerRepository.php create mode 100644 app/Repositories/Eloquent/ServiceRepository.php create mode 100644 app/Services/Database/CreationService.php diff --git a/app/Contracts/Repository/DatabaseHostInterface.php b/app/Contracts/Repository/DatabaseHostRepositoryInterface.php similarity index 95% rename from app/Contracts/Repository/DatabaseHostInterface.php rename to app/Contracts/Repository/DatabaseHostRepositoryInterface.php index 2fd26b16..eeb24fdc 100644 --- a/app/Contracts/Repository/DatabaseHostInterface.php +++ b/app/Contracts/Repository/DatabaseHostRepositoryInterface.php @@ -24,7 +24,7 @@ namespace Pterodactyl\Contracts\Repository; -interface DatabaseHostInterface extends RepositoryInterface +interface DatabaseHostRepositoryInterface extends RepositoryInterface { /** * Delete a database host from the DB if there are no databases using it. diff --git a/app/Contracts/Repository/DatabaseRepositoryInterface.php b/app/Contracts/Repository/DatabaseRepositoryInterface.php new file mode 100644 index 00000000..1f49a54d --- /dev/null +++ b/app/Contracts/Repository/DatabaseRepositoryInterface.php @@ -0,0 +1,98 @@ +. + * + * 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\Contracts\Repository; + +interface DatabaseRepositoryInterface extends RepositoryInterface +{ + /** + * Create a new database if it does not already exist on the host with + * the provided details. + * + * @param array $data + * @return mixed + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function createIfNotExists(array $data); + + /** + * Create a new database on a given connection. + * + * @param string $database + * @param null|string $connection + * @return bool + */ + public function createDatabase($database, $connection = null); + + /** + * Create a new database user on a given connection. + * + * @param string $username + * @param string $remote + * @param string $password + * @param null|string $connection + * @return bool + */ + public function createUser($username, $remote, $password, $connection = null); + + /** + * Give a specific user access to a given database. + * + * @param string $database + * @param string $username + * @param string $remote + * @param null|string $connection + * @return bool + */ + public function assignUserToDatabase($database, $username, $remote, $connection = null); + + /** + * Flush the privileges for a given connection. + * + * @param null|string $connection + * @return mixed + */ + public function flush($connection = null); + + /** + * Drop a given database on a specific connection. + * + * @param string $database + * @param null|string $connection + * @return bool + */ + public function dropDatabase($database, $connection = null); + + /** + * Drop a given user on a specific connection. + * + * @param string $username + * @param string $remote + * @param null|string $connection + * @return mixed + */ + public function dropUser($username, $remote, $connection = null); +} diff --git a/app/Contracts/Repository/RepositoryInterface.php b/app/Contracts/Repository/RepositoryInterface.php index 747fb03f..2a19810f 100644 --- a/app/Contracts/Repository/RepositoryInterface.php +++ b/app/Contracts/Repository/RepositoryInterface.php @@ -138,4 +138,11 @@ interface RepositoryInterface * @return mixed */ public function massUpdate(array $where, array $fields); + + /** + * Return all records from the model. + * + * @return mixed + */ + public function all(); } diff --git a/app/Contracts/Repository/ServerRepositoryInterface.php b/app/Contracts/Repository/ServerRepositoryInterface.php new file mode 100644 index 00000000..5619e3fd --- /dev/null +++ b/app/Contracts/Repository/ServerRepositoryInterface.php @@ -0,0 +1,38 @@ +. + * + * 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\Contracts\Repository; + +use Pterodactyl\Contracts\Repository\Attributes\SearchableInterface; + +interface ServerRepositoryInterface extends RepositoryInterface, SearchableInterface +{ + /** + * Returns a listing of all servers that exist including relationships. + * + * @param int $paginate + * @return mixed + */ + public function getAllServers($paginate); +} diff --git a/app/Contracts/Repository/ServiceRepositoryInterface.php b/app/Contracts/Repository/ServiceRepositoryInterface.php new file mode 100644 index 00000000..def30c95 --- /dev/null +++ b/app/Contracts/Repository/ServiceRepositoryInterface.php @@ -0,0 +1,36 @@ +. + * + * 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\Contracts\Repository; + +interface ServiceRepositoryInterface extends RepositoryInterface +{ + /** + * Return a service or all services with their associated options, variables, and packs. + * + * @param int $id + * @return \Illuminate\Support\Collection + */ + public function getWithOptions($id = null); +} diff --git a/app/Extensions/DynamicDatabaseConnection.php b/app/Extensions/DynamicDatabaseConnection.php index 01308190..68081df2 100644 --- a/app/Extensions/DynamicDatabaseConnection.php +++ b/app/Extensions/DynamicDatabaseConnection.php @@ -24,7 +24,7 @@ namespace Pterodactyl\Extensions; -use Pterodactyl\Contracts\Repository\DatabaseHostInterface; +use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; use Pterodactyl\Models\DatabaseHost; use Illuminate\Contracts\Encryption\Encrypter; use Illuminate\Config\Repository as ConfigRepository; @@ -46,20 +46,20 @@ class DynamicDatabaseConnection protected $encrypter; /** - * @var \Pterodactyl\Contracts\Repository\DatabaseHostInterface + * @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface */ protected $repository; /** * DynamicDatabaseConnection constructor. * - * @param \Illuminate\Config\Repository $config - * @param \Pterodactyl\Contracts\Repository\DatabaseHostInterface $repository - * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter + * @param \Illuminate\Config\Repository $config + * @param \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface $repository + * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter */ public function __construct( ConfigRepository $config, - DatabaseHostInterface $repository, + DatabaseHostRepositoryInterface $repository, Encrypter $encrypter ) { $this->config = $config; diff --git a/app/Http/Controllers/Admin/DatabaseController.php b/app/Http/Controllers/Admin/DatabaseController.php index 0f48987f..a383558b 100644 --- a/app/Http/Controllers/Admin/DatabaseController.php +++ b/app/Http/Controllers/Admin/DatabaseController.php @@ -28,7 +28,7 @@ use Pterodactyl\Models\Location; use Pterodactyl\Models\DatabaseHost; use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Services\Administrative\DatabaseHostService; +use Pterodactyl\Services\Database\DatabaseHostService; use Pterodactyl\Http\Requests\Admin\DatabaseHostFormRequest; class DatabaseController extends Controller @@ -49,7 +49,7 @@ class DatabaseController extends Controller protected $locationModel; /** - * @var \Pterodactyl\Services\Administrative\DatabaseHostService + * @var \Pterodactyl\Services\Database\DatabaseHostService */ protected $service; @@ -59,7 +59,7 @@ class DatabaseController extends Controller * @param \Prologue\Alerts\AlertsMessageBag $alert * @param \Pterodactyl\Models\DatabaseHost $hostModel * @param \Pterodactyl\Models\Location $locationModel - * @param \Pterodactyl\Services\Administrative\DatabaseHostService $service + * @param \Pterodactyl\Services\Database\DatabaseHostService $service */ public function __construct( AlertsMessageBag $alert, diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php index 76715c84..0c27bd59 100644 --- a/app/Http/Controllers/Admin/ServersController.php +++ b/app/Http/Controllers/Admin/ServersController.php @@ -24,9 +24,14 @@ namespace Pterodactyl\Http\Controllers\Admin; +use Illuminate\Contracts\Config\Repository as ConfigRepository; use Log; use Alert; use Javascript; +use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; +use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; use Pterodactyl\Models; use Illuminate\Http\Request; use GuzzleHttp\Exception\TransferException; @@ -39,34 +44,70 @@ use Pterodactyl\Exceptions\DisplayValidationException; class ServersController extends Controller { + /** + * @var \Illuminate\Contracts\Config\Repository + */ + protected $config; + + /** + * @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface + */ + protected $databaseRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\LocationRepositoryInterface + */ + protected $locationRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface + */ + protected $serviceRepository; + + public function __construct( + ConfigRepository $config, + DatabaseRepositoryInterface $databaseRepository, + LocationRepositoryInterface $locationRepository, + ServerRepositoryInterface $repository, + ServiceRepositoryInterface $serviceRepository + ) { + $this->config = $config; + $this->databaseRepository = $databaseRepository; + $this->locationRepository = $locationRepository; + $this->repository = $repository; + $this->serviceRepository = $serviceRepository; + } + /** * Display the index page with all servers currently on the system. * - * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ - public function index(Request $request) + public function index() { - $servers = Models\Server::with('node', 'user', 'allocation'); - - if (! is_null($request->input('query'))) { - $servers->search($request->input('query')); - } - return view('admin.servers.index', [ - 'servers' => $servers->paginate(25), + 'servers' => $this->repository->getAllServers( + $this->config->get('pterodactyl.paginate.admin.servers') + ), ]); } /** * Display create new server page. * - * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View + * + * @throws \Exception */ - public function create(Request $request) + public function create() { - $services = Models\Service::with('options.packs', 'options.variables')->get(); + $services = $this->serviceRepository->getWithOptions(); + Javascript::put([ 'services' => $services->map(function ($item) { return array_merge($item->toArray(), [ @@ -76,7 +117,7 @@ class ServersController extends Controller ]); return view('admin.servers.new', [ - 'locations' => Models\Location::all(), + 'locations' => $this->locationRepository->all(), 'services' => $services, ]); } @@ -115,7 +156,7 @@ class ServersController extends Controller * Returns a tree of all avaliable nodes in a given location. * * @param \Illuminate\Http\Request $request - * @return array + * @return \Illuminate\Support\Collection */ public function nodes(Request $request) { diff --git a/app/Models/Database.php b/app/Models/Database.php index 20ad3c1b..a7ee970e 100644 --- a/app/Models/Database.php +++ b/app/Models/Database.php @@ -25,9 +25,13 @@ namespace Pterodactyl\Models; use Illuminate\Database\Eloquent\Model; +use Sofa\Eloquence\Eloquence; +use Sofa\Eloquence\Validable; class Database extends Model { + use Eloquence, Validable; + /** * The table associated with the model. * @@ -61,6 +65,22 @@ class Database extends Model 'database_host_id' => 'integer', ]; + protected static $applicationRules = [ + 'server_id' => 'required', + 'database_host_id' => 'required', + 'database' => 'required', + 'remote' => 'required', + ]; + + protected static $dataIntegrityRules = [ + 'server_id' => 'numeric|exists:servers,id', + 'database_host_id' => 'exists:database_hosts,id', + 'database' => 'string|alpha_dash|between:3,100', + 'username' => 'string|alpha_dash|between:3,100', + 'remote' => 'string|regex:/^[0-9%.]{1,15}$/', + 'password' => 'string', + ]; + /** * Gets the host database server associated with a database. * diff --git a/app/Models/DatabaseHost.php b/app/Models/DatabaseHost.php index a6599cc4..54a16d3e 100644 --- a/app/Models/DatabaseHost.php +++ b/app/Models/DatabaseHost.php @@ -24,12 +24,13 @@ namespace Pterodactyl\Models; -use Watson\Validating\ValidatingTrait; +use Sofa\Eloquence\Eloquence; +use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; class DatabaseHost extends Model { - use ValidatingTrait; + use Eloquence, Validable; /** * The table associated with the model. @@ -65,18 +66,31 @@ class DatabaseHost extends Model 'node_id' => 'integer', ]; + /** + * Application validation rules. + * + * @var array + */ + protected static $applicationRules = [ + 'name' => 'required', + 'host' => 'required', + 'port' => 'required', + 'username' => 'required', + 'node_id' => 'sometimes|required', + ]; + /** * Validation rules to assign to this model. * * @var array */ - protected $rules = [ - 'name' => 'required|string|max:255', - 'host' => 'required|ip|unique:database_hosts,host', - 'port' => 'required|numeric|between:1,65535', - 'username' => 'required|string|max:32', - 'password' => 'sometimes|nullable|string', - 'node_id' => 'sometimes|required|nullable|exists:nodes,id', + protected static $dataIntegrityRules = [ + 'name' => 'string|max:255', + 'host' => 'ip|unique:database_hosts,host', + 'port' => 'numeric|between:1,65535', + 'username' => 'string|max:32', + 'password' => 'nullable|string', + 'node_id' => 'nullable|exists:nodes,id', ]; /** diff --git a/app/Providers/RepositoryServiceProvider.php b/app/Providers/RepositoryServiceProvider.php index 9d0f59b3..189ca6eb 100644 --- a/app/Providers/RepositoryServiceProvider.php +++ b/app/Providers/RepositoryServiceProvider.php @@ -27,12 +27,18 @@ namespace Pterodactyl\Providers; use Illuminate\Support\ServiceProvider; use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; use Pterodactyl\Contracts\Repository\ApiPermissionRepositoryInterface; -use Pterodactyl\Contracts\Repository\DatabaseHostInterface; +use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; +use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; use Pterodactyl\Repositories\Eloquent\ApiKeyRepository; use Pterodactyl\Repositories\Eloquent\ApiPermissionRepository; use Pterodactyl\Repositories\Eloquent\DatabaseHostRepository; +use Pterodactyl\Repositories\Eloquent\DatabaseRepository; use Pterodactyl\Repositories\Eloquent\LocationRepository; +use Pterodactyl\Repositories\Eloquent\ServerRepository; +use Pterodactyl\Repositories\Eloquent\ServiceRepository; use Pterodactyl\Repositories\Eloquent\UserRepository; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; @@ -45,8 +51,11 @@ class RepositoryServiceProvider extends ServiceProvider { $this->app->bind(ApiKeyRepositoryInterface::class, ApiKeyRepository::class); $this->app->bind(ApiPermissionRepositoryInterface::class, ApiPermissionRepository::class); - $this->app->bind(DatabaseHostInterface::class, DatabaseHostRepository::class); + $this->app->bind(DatabaseRepositoryInterface::class, DatabaseRepository::class); + $this->app->bind(DatabaseHostRepositoryInterface::class, DatabaseHostRepository::class); $this->app->bind(LocationRepositoryInterface::class, LocationRepository::class); + $this->app->bind(ServerRepositoryInterface::class, ServerRepository::class); + $this->app->bind(ServiceRepositoryInterface::class, ServiceRepository::class); $this->app->bind(UserRepositoryInterface::class, UserRepository::class); } } diff --git a/app/Repositories/Eloquent/Attributes/SearchableRepository.php b/app/Repositories/Eloquent/Attributes/SearchableRepository.php new file mode 100644 index 00000000..5965d1f5 --- /dev/null +++ b/app/Repositories/Eloquent/Attributes/SearchableRepository.php @@ -0,0 +1,51 @@ +. + * + * 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\Repositories\Eloquent\Attributes; + +use Pterodactyl\Repositories\Eloquent\EloquentRepository; +use Pterodactyl\Contracts\Repository\Attributes\SearchableInterface; + +abstract class SearchableRepository extends EloquentRepository implements SearchableInterface +{ + /** + * @var bool|string + */ + protected $searchTerm = false; + + /** + * {@inheritdoc} + */ + public function search($term) + { + if (empty($term)) { + return $this; + } + + $clone = clone $this; + $clone->searchTerm = $term; + + return $clone; + } +} diff --git a/app/Repositories/Eloquent/DatabaseHostRepository.php b/app/Repositories/Eloquent/DatabaseHostRepository.php index 76c57212..8a4d7b3c 100644 --- a/app/Repositories/Eloquent/DatabaseHostRepository.php +++ b/app/Repositories/Eloquent/DatabaseHostRepository.php @@ -24,12 +24,12 @@ namespace Pterodactyl\Repositories\Eloquent; -use Pterodactyl\Contracts\Repository\DatabaseHostInterface; +use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Models\DatabaseHost; -class DatabaseHostRepository extends EloquentRepository implements DatabaseHostInterface +class DatabaseHostRepository extends EloquentRepository implements DatabaseHostRepositoryInterface { /** * {@inheritdoc} diff --git a/app/Repositories/Eloquent/DatabaseRepository.php b/app/Repositories/Eloquent/DatabaseRepository.php new file mode 100644 index 00000000..3a2a589c --- /dev/null +++ b/app/Repositories/Eloquent/DatabaseRepository.php @@ -0,0 +1,155 @@ +. + * + * 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\Repositories\Eloquent; + +use Pterodactyl\Models\Database; +use Illuminate\Foundation\Application; +use Illuminate\Database\ConnectionResolver; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; + +class DatabaseRepository extends EloquentRepository implements DatabaseRepositoryInterface +{ + /** + * @var \Illuminate\Database\ConnectionResolverInterface + */ + protected $connection; + + /** + * DatabaseRepository constructor. + * + * @param \Illuminate\Foundation\Application $application + * @param \Illuminate\Database\ConnectionResolver $connection + */ + public function __construct( + Application $application, + ConnectionResolver $connection + ) { + parent::__construct($application); + + $this->connection = $connection; + } + + /** + * {@inheritdoc} + */ + public function model() + { + return Database::class; + } + + /** + * {@inheritdoc} + * @return bool|\Illuminate\Database\Eloquent\Model + */ + public function createIfNotExists(array $data) + { + $instance = $this->getBuilder()->where([ + ['server_id', $data['server_id']], + ['database_host_id', $data['database_host_id']], + ['database', $data['database']], + ])->count(); + + if ($instance > 0) { + throw new DisplayException('A database with those details already exists for the specified server.'); + } + + return $this->create($data); + } + + /** + * {@inheritdoc} + */ + public function createDatabase($database, $connection = null) + { + return $this->runStatement( + sprintf('CREATE DATABASE IF NOT EXISTS `%s`', $database), $connection + ); + } + + /** + * {@inheritdoc} + */ + public function createUser($username, $remote, $password, $connection = null) + { + return $this->runStatement( + sprintf('CREATE USER `%s`@`%s` IDENTIFIED BY \'%s\'', $username, $remote, $password), $connection + ); + } + + /** + * {@inheritdoc} + */ + public function assignUserToDatabase($database, $username, $remote, $connection = null) + { + return $this->runStatement( + sprintf( + 'GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, INDEX ON `%s`.* TO `%s`@`%s`', + $database, $username, $remote + ), + $connection + ); + } + + /** + * {@inheritdoc} + */ + public function flush($connection = null) + { + return $this->runStatement('FLUSH PRIVILEGES', $connection); + } + + /** + * {@inheritdoc} + */ + public function dropDatabase($database, $connection = null) + { + return $this->runStatement( + sprintf('DROP DATABASE IF EXISTS `%s`', $database), $connection + ); + } + + /** + * {@inheritdoc} + */ + public function dropUser($username, $remote, $connection = null) + { + return $this->runStatement( + sprintf('DROP USER IF EXISTS `%s`@`%s`', $username, $remote), $connection + ); + } + + /** + * Run the provided statement against the database on a given connection. + * + * @param string $statement + * @param null|string $connection + * @return bool + */ + protected function runStatement($statement, $connection = null) + { + return $this->connection->connection($connection)->statement($statement); + } +} diff --git a/app/Repositories/Eloquent/EloquentRepository.php b/app/Repositories/Eloquent/EloquentRepository.php index b6af22f7..793291bb 100644 --- a/app/Repositories/Eloquent/EloquentRepository.php +++ b/app/Repositories/Eloquent/EloquentRepository.php @@ -152,4 +152,12 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf { // TODO: Implement massUpdate() method. } + + /** + * {@inheritdoc} + */ + public function all() + { + return $this->getBuilder()->get($this->getColumns()); + } } diff --git a/app/Repositories/Eloquent/ServerRepository.php b/app/Repositories/Eloquent/ServerRepository.php new file mode 100644 index 00000000..4f605b64 --- /dev/null +++ b/app/Repositories/Eloquent/ServerRepository.php @@ -0,0 +1,54 @@ +. + * + * 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\Repositories\Eloquent; + +use Pterodactyl\Models\Server; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Repositories\Eloquent\Attributes\SearchableRepository; + +class ServerRepository extends SearchableRepository implements ServerRepositoryInterface +{ + /** + * {@inheritdoc} + */ + public function model() + { + return Server::class; + } + + /** + * {@inheritdoc} + */ + public function getAllServers($paginate = 25) + { + $instance = $this->getBuilder()->with('node', 'user', 'allocation'); + + if ($this->searchTerm) { + $instance->search($this->searchTerm); + } + + return $instance->paginate($paginate); + } +} diff --git a/app/Repositories/Eloquent/ServiceRepository.php b/app/Repositories/Eloquent/ServiceRepository.php new file mode 100644 index 00000000..6fd8a4bc --- /dev/null +++ b/app/Repositories/Eloquent/ServiceRepository.php @@ -0,0 +1,60 @@ +. + * + * 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\Repositories\Eloquent; + +use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; +use Pterodactyl\Exceptions\Repository\RecordNotFoundException; +use Pterodactyl\Models\Service; + +class ServiceRepository extends EloquentRepository implements ServiceRepositoryInterface +{ + /** + * {@inheritdoc} + */ + public function model() + { + return Service::class; + } + + /** + * {@inheritdoc} + */ + public function getWithOptions($id = null) + { + $instance = $this->getBuilder()->with('options.packs', 'options.variables'); + + if (! is_null($id)) { + $instance = $instance->find($id, $this->getColumns()); + + if (! $instance) { + throw new RecordNotFoundException(); + } + + return $instance; + } + + return $instance->get($this->getColumns()); + } +} diff --git a/app/Repositories/Eloquent/UserRepository.php b/app/Repositories/Eloquent/UserRepository.php index 00776bb7..4ad33419 100644 --- a/app/Repositories/Eloquent/UserRepository.php +++ b/app/Repositories/Eloquent/UserRepository.php @@ -30,19 +30,15 @@ use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Models\User; +use Pterodactyl\Repositories\Eloquent\Attributes\SearchableRepository; -class UserRepository extends EloquentRepository implements UserRepositoryInterface +class UserRepository extends SearchableRepository implements UserRepositoryInterface { /** * @var \Illuminate\Contracts\Config\Repository */ protected $config; - /** - * @var bool|array - */ - protected $searchTerm = false; - /** * UserRepository constructor. * @@ -64,21 +60,6 @@ class UserRepository extends EloquentRepository implements UserRepositoryInterfa return User::class; } - /** - * {@inheritdoc} - */ - public function search($term) - { - if (empty($term)) { - return $this; - } - - $clone = clone $this; - $clone->searchTerm = $term; - - return $clone; - } - /** * {@inheritdoc} */ diff --git a/app/Repositories/Old/DatabaseRepository.php b/app/Repositories/Old/DatabaseRepository.php index f7f428a6..1e4bc75a 100644 --- a/app/Repositories/Old/DatabaseRepository.php +++ b/app/Repositories/Old/DatabaseRepository.php @@ -170,115 +170,4 @@ class DatabaseRepository $database->delete(); }); } - - /** - * Deletes a database host from the system if it has no associated databases. - * - * @param int $id - * @return void - * - * @throws \Pterodactyl\Exceptions\DisplayException - */ - public function delete($id) - { - $host = DatabaseHost::withCount('databases')->findOrFail($id); - - if ($host->databases_count > 0) { - throw new DisplayException('You cannot delete a database host that has active databases attached to it.'); - } - - $host->delete(); - } - - /** - * Adds a new Database Host to the system. - * - * @param array $data - * @return \Pterodactyl\Models\DatabaseHost - * - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function add(array $data) - { - if (isset($data['host'])) { - $data['host'] = gethostbyname($data['host']); - } - - $validator = Validator::make($data, [ - 'name' => 'required|string|max:255', - 'host' => 'required|ip|unique:database_hosts,host', - 'port' => 'required|numeric|between:1,65535', - 'username' => 'required|string|max:32', - 'password' => 'required|string', - 'node_id' => 'sometimes|required|exists:nodes,id', - ]); - - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - return DB::transaction(function () use ($data) { - $host = new DatabaseHost; - $host->password = Crypt::encrypt($data['password']); - - $host->fill([ - 'name' => $data['name'], - 'host' => $data['host'], - 'port' => $data['port'], - 'username' => $data['username'], - 'max_databases' => null, - 'node_id' => (isset($data['node_id'])) ? $data['node_id'] : null, - ])->save(); - - // Allows us to check that we can connect to things. - $host->setDynamicConnection(); - DB::connection('dynamic')->select('SELECT 1 FROM dual'); - - return $host; - }); - } - - /** - * Updates a Database Host on the system. - * - * @param int $id - * @param array $data - * @return \Pterodactyl\Models\DatabaseHost - * - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function update($id, array $data) - { - $host = DatabaseHost::findOrFail($id); - - if (isset($data['host'])) { - $data['host'] = gethostbyname($data['host']); - } - - $validator = Validator::make($data, [ - 'name' => 'sometimes|required|string|max:255', - 'host' => 'sometimes|required|ip|unique:database_hosts,host,' . $host->id, - 'port' => 'sometimes|required|numeric|between:1,65535', - 'username' => 'sometimes|required|string|max:32', - 'password' => 'sometimes|required|string', - 'node_id' => 'sometimes|required|exists:nodes,id', - ]); - - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - return DB::transaction(function () use ($data, $host) { - if (isset($data['password'])) { - $host->password = Crypt::encrypt($data['password']); - } - $host->fill($data)->save(); - - // Check that we can still connect with these details. - $host->setDynamicConnection(); - DB::connection('dynamic')->select('SELECT 1 FROM dual'); - - return $host; - }); - } } diff --git a/app/Services/Database/CreationService.php b/app/Services/Database/CreationService.php new file mode 100644 index 00000000..b4887453 --- /dev/null +++ b/app/Services/Database/CreationService.php @@ -0,0 +1,188 @@ +. + * + * 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\Database; + +use Illuminate\Database\ConnectionResolver; +use Illuminate\Database\ConnectionInterface; +use Illuminate\Contracts\Encryption\Encrypter; +use Pterodactyl\Extensions\DynamicDatabaseConnection; +use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; + +class CreationService +{ + /** + * @var \Illuminate\Database\ConnectionResolver + */ + protected $connection; + + /** + * @var \Illuminate\Database\ConnectionInterface + */ + protected $database; + + /** + * @var \Pterodactyl\Extensions\DynamicDatabaseConnection + */ + protected $dynamic; + + /** + * @var \Illuminate\Contracts\Encryption\Encrypter + */ + protected $encrypter; + + /** + * @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface + */ + protected $repository; + + /** + * CreationService constructor. + * + * @param \Illuminate\Database\ConnectionInterface $database + * @param \Illuminate\Database\ConnectionResolver $connection + * @param \Pterodactyl\Extensions\DynamicDatabaseConnection $dynamic + * @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $repository + * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter + */ + public function __construct( + ConnectionInterface $database, + ConnectionResolver $connection, + DynamicDatabaseConnection $dynamic, + DatabaseRepositoryInterface $repository, + Encrypter $encrypter + ) { + $this->connection = $connection; + $this->database = $database; + $this->dynamic = $dynamic; + $this->encrypter = $encrypter; + $this->repository = $repository; + } + + /** + * Create a new database that is linked to a specific host. + * + * @param array $data + * @return \Illuminate\Database\Eloquent\Model + * + * @throws \Exception + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function create(array $data) + { + $data['database'] = sprintf('d%d_%s', $data['server_id'], $data['database']); + $data['username'] = sprintf('u%d_%s', $data['server_id'], str_random(10)); + $data['password'] = $this->encrypter->encrypt(str_random(16)); + + $this->database->beginTransaction(); + try { + $database = $this->repository->createIfNotExists($data); + $this->dynamic->set('dynamic', $data['database_host_id']); + + $this->repository->createDatabase($database->database, 'dynamic'); + $this->repository->createUser( + $database->username, $database->remote, $this->encrypter->decrypt($database->password), 'dynamic' + ); + $this->repository->assignUserToDatabase( + $database->database, $database->username, $database->remote, 'dynamic' + ); + $this->repository->flush('dynamic'); + + $this->database->commit(); + } catch (\Exception $ex) { + try { + if (isset($database)) { + $this->repository->dropDatabase($database->database, 'dynamic'); + $this->repository->dropUser($database->username, $database->remote, 'dynamic'); + $this->repository->flush('dynamic'); + } + } catch (\Exception $ex) { + // ignore an exception + } + + $this->database->rollBack(); + throw $ex; + } + + return $database; + } + + /** + * Change the password for a specific user and database combination. + * + * @param int $id + * @param string $password + * @return bool + * + * @throws \Exception + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function changePassword($id, $password) + { + $database = $this->repository->find($id); + $this->dynamic->set('dynamic', $database->database_host_id); + + $this->database->beginTransaction(); + + try { + $updated = $this->repository->withoutFresh()->update($id, [ + 'password' => $this->encrypter->encrypt($password), + ]); + + $this->repository->dropUser($database->username, $database->remote, 'dynamic'); + $this->repository->createUser($database->username, $database->remote, $password); + $this->repository->assignUserToDatabase( + $database->database, $database->username, $database->remote, 'dynamic' + ); + $this->repository->flush(); + + $this->database->commit(); + } catch (\Exception $ex) { + $this->database->rollBack(); + throw $ex; + } + + return $updated; + } + + /** + * Delete a database from the given host server. + * + * @param int $id + * @return bool|null + */ + public function delete($id) + { + $database = $this->repository->find($id); + $this->dynamic->set('dynamic', $database->database_host_id); + + $this->repository->dropDatabase($database->database, 'dynamic'); + $this->repository->dropUser($database->username, $database->remote, 'dynamic'); + $this->repository->flush('dynamic'); + + return $this->repository->delete($id); + } +} diff --git a/app/Services/Database/DatabaseHostService.php b/app/Services/Database/DatabaseHostService.php index b0dbb157..ad54a1cd 100644 --- a/app/Services/Database/DatabaseHostService.php +++ b/app/Services/Database/DatabaseHostService.php @@ -27,7 +27,7 @@ namespace Pterodactyl\Services\Database; use Illuminate\Database\DatabaseManager; use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Extensions\DynamicDatabaseConnection; -use Pterodactyl\Contracts\Repository\DatabaseHostInterface; +use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; class DatabaseHostService { @@ -47,20 +47,20 @@ class DatabaseHostService protected $encrypter; /** - * @var \Pterodactyl\Contracts\Repository\DatabaseHostInterface + * @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface */ protected $repository; /** * DatabaseHostService constructor. * - * @param \Pterodactyl\Contracts\Repository\DatabaseHostInterface $repository - * @param \Illuminate\Database\DatabaseManager $database - * @param \Pterodactyl\Extensions\DynamicDatabaseConnection $dynamic - * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter + * @param \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface $repository + * @param \Illuminate\Database\DatabaseManager $database + * @param \Pterodactyl\Extensions\DynamicDatabaseConnection $dynamic + * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter */ public function __construct( - DatabaseHostInterface $repository, + DatabaseHostRepositoryInterface $repository, DatabaseManager $database, DynamicDatabaseConnection $dynamic, Encrypter $encrypter diff --git a/config/pterodactyl.php b/config/pterodactyl.php index d1e74ae0..29d8bbe7 100644 --- a/config/pterodactyl.php +++ b/config/pterodactyl.php @@ -40,6 +40,7 @@ return [ 'servers' => env('APP_PAGINATE_FRONT_SERVERS', 15), ], 'admin' => [ + 'servers' => env('APP_PAGINATE_ADMIN_SERVERS', 25), 'users' => env('APP_PAGINATE_ADMIN_USERS', 25), ], 'api' => [ diff --git a/tests/Unit/Services/Database/DatabaseHostServiceTest.php b/tests/Unit/Services/Database/DatabaseHostServiceTest.php index 55a97ae3..e728d467 100644 --- a/tests/Unit/Services/Database/DatabaseHostServiceTest.php +++ b/tests/Unit/Services/Database/DatabaseHostServiceTest.php @@ -29,7 +29,7 @@ use Tests\TestCase; use Illuminate\Database\DatabaseManager; use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Extensions\DynamicDatabaseConnection; -use Pterodactyl\Contracts\Repository\DatabaseHostInterface; +use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; use Pterodactyl\Services\Database\DatabaseHostService; class DatabaseHostServiceTest extends TestCase @@ -50,7 +50,7 @@ class DatabaseHostServiceTest extends TestCase protected $encrypter; /** - * @var \Pterodactyl\Contracts\Repository\DatabaseHostInterface + * @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface */ protected $repository; @@ -69,7 +69,7 @@ class DatabaseHostServiceTest extends TestCase $this->database = m::mock(DatabaseManager::class); $this->dynamic = m::mock(DynamicDatabaseConnection::class); $this->encrypter = m::mock(Encrypter::class); - $this->repository = m::mock(DatabaseHostInterface::class); + $this->repository = m::mock(DatabaseHostRepositoryInterface::class); $this->service = new DatabaseHostService( $this->repository, From 0c513f24d581ab62077f8b6e8ca4d88e55531472 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Wed, 19 Jul 2017 20:49:41 -0500 Subject: [PATCH 26/99] Move server creation over to new service/repository setup. Moves tons of functions around, but the basic implementation is working again. Some features are still missing, and the service never actually commits the server to the database right now. This push is mostly just to get the code into Github and backed up. --- .../AllocationRepositoryInterface.php | 37 ++++ .../Daemon/BaseRepositoryInterface.php | 81 ++++++++ .../Daemon/ServerRepositoryInterface.php | 38 ++++ .../Repository/NodeRepositoryInterface.php | 32 ++++ .../OptionVariableRepositoryInterface.php | 30 +++ .../Repository/RepositoryInterface.php | 9 + .../Repository/ServerRepositoryInterface.php | 17 ++ .../ServerVariableRepositoryInterface.php | 30 +++ .../RequiredVariableMissingException.php | 32 ++++ .../Controllers/API/User/ServerController.php | 4 +- .../Controllers/Admin/ServersController.php | 54 +++--- .../Controllers/Server/AjaxController.php | 4 +- .../Controllers/Server/ServerController.php | 2 +- app/Http/Requests/Admin/ServerFormRequest.php | 83 +++++++++ app/Jobs/SendScheduledTask.php | 4 +- app/Models/Server.php | 45 ++++- app/Providers/RepositoryServiceProvider.php | 20 +- app/Repositories/Daemon/BaseRepository.php | 105 +++++++++++ app/Repositories/Daemon/ServerRepository.php | 86 +++++++++ .../Eloquent/AllocationRepository.php | 47 +++++ .../Eloquent/EloquentRepository.php | 8 + app/Repositories/Eloquent/NodeRepository.php | 40 ++++ .../Eloquent/OptionVariableRepository.php | 39 ++++ .../Eloquent/ServerRepository.php | 52 ++++++ .../Eloquent/ServerVariableRepository.php | 39 ++++ .../CommandRepository.php | 2 +- .../{Daemon => old_Daemon}/FileRepository.php | 2 +- .../PowerRepository.php | 2 +- app/Services/Components/UuidService.php | 2 + app/Services/Servers/EnvironmentService.php | 106 +++++++++++ app/Services/Servers/ServerService.php | 149 +++++++++++++++ .../Servers/UsernameGenerationService.php | 55 ++++++ .../Servers/VariableValidatorService.php | 173 ++++++++++++++++++ .../themes/pterodactyl/js/admin/new-server.js | 2 +- .../pterodactyl/admin/servers/new.blade.php | 11 +- 35 files changed, 1398 insertions(+), 44 deletions(-) create mode 100644 app/Contracts/Repository/AllocationRepositoryInterface.php create mode 100644 app/Contracts/Repository/Daemon/BaseRepositoryInterface.php create mode 100644 app/Contracts/Repository/Daemon/ServerRepositoryInterface.php create mode 100644 app/Contracts/Repository/NodeRepositoryInterface.php create mode 100644 app/Contracts/Repository/OptionVariableRepositoryInterface.php create mode 100644 app/Contracts/Repository/ServerVariableRepositoryInterface.php create mode 100644 app/Exceptions/Services/Servers/RequiredVariableMissingException.php create mode 100644 app/Http/Requests/Admin/ServerFormRequest.php create mode 100644 app/Repositories/Daemon/BaseRepository.php create mode 100644 app/Repositories/Daemon/ServerRepository.php create mode 100644 app/Repositories/Eloquent/AllocationRepository.php create mode 100644 app/Repositories/Eloquent/NodeRepository.php create mode 100644 app/Repositories/Eloquent/OptionVariableRepository.php create mode 100644 app/Repositories/Eloquent/ServerVariableRepository.php rename app/Repositories/{Daemon => old_Daemon}/CommandRepository.php (98%) rename app/Repositories/{Daemon => old_Daemon}/FileRepository.php (99%) rename app/Repositories/{Daemon => old_Daemon}/PowerRepository.php (98%) create mode 100644 app/Services/Servers/EnvironmentService.php create mode 100644 app/Services/Servers/ServerService.php create mode 100644 app/Services/Servers/UsernameGenerationService.php create mode 100644 app/Services/Servers/VariableValidatorService.php diff --git a/app/Contracts/Repository/AllocationRepositoryInterface.php b/app/Contracts/Repository/AllocationRepositoryInterface.php new file mode 100644 index 00000000..4dca0af8 --- /dev/null +++ b/app/Contracts/Repository/AllocationRepositoryInterface.php @@ -0,0 +1,37 @@ +. + * + * 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\Contracts\Repository; + +interface AllocationRepositoryInterface extends RepositoryInterface +{ + /** + * Set an array of allocation IDs to be assigned to a specific server. + * + * @param int|null $server + * @param array $ids + * @return int + */ + public function assignAllocationsToServer($server, array $ids); +} diff --git a/app/Contracts/Repository/Daemon/BaseRepositoryInterface.php b/app/Contracts/Repository/Daemon/BaseRepositoryInterface.php new file mode 100644 index 00000000..490f38a4 --- /dev/null +++ b/app/Contracts/Repository/Daemon/BaseRepositoryInterface.php @@ -0,0 +1,81 @@ +. + * + * 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\Contracts\Repository\Daemon; + +interface BaseRepositoryInterface +{ + /** + * Set the node model to be used for this daemon connection. + * + * @param int $id + * @return $this + */ + public function setNode($id); + + /** + * Return the node model being used. + * + * @return \Pterodactyl\Models\Node + */ + public function getNode(); + + /** + * Set the UUID for the server to be used in the X-Access-Server header for daemon requests. + * + * @param null|string $server + * @return $this + */ + public function setAccessServer($server = null); + + /** + * Return the UUID of the server being used in requests. + * + * @return string + */ + public function getAccessServer(); + + /** + * Set the token to be used in the X-Access-Token header for requests to the daemon. + * + * @param null|string $token + * @return $this + */ + public function setAccessToken($token = null); + + /** + * Return the access token being used for requests. + * + * @return string + */ + public function getAccessToken(); + + /** + * Return an instance of the Guzzle HTTP Client to be used for requests. + * + * @param array $headers + * @return \GuzzleHttp\Client + */ + public function getHttpClient($headers = []); +} diff --git a/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php b/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php new file mode 100644 index 00000000..a4e398bc --- /dev/null +++ b/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php @@ -0,0 +1,38 @@ +. + * + * 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\Contracts\Repository\Daemon; + +interface ServerRepositoryInterface extends BaseRepositoryInterface +{ + /** + * Create a new server on the daemon for the panel. + * + * @param int $id + * @param array $overrides + * @param bool $start + * @return mixed + */ + public function create($id, $overrides = [], $start = false); +} diff --git a/app/Contracts/Repository/NodeRepositoryInterface.php b/app/Contracts/Repository/NodeRepositoryInterface.php new file mode 100644 index 00000000..c72e2fe8 --- /dev/null +++ b/app/Contracts/Repository/NodeRepositoryInterface.php @@ -0,0 +1,32 @@ +. + * + * 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\Contracts\Repository; + +use Pterodactyl\Contracts\Repository\Attributes\SearchableInterface; + +interface NodeRepositoryInterface extends RepositoryInterface, SearchableInterface +{ + // +} diff --git a/app/Contracts/Repository/OptionVariableRepositoryInterface.php b/app/Contracts/Repository/OptionVariableRepositoryInterface.php new file mode 100644 index 00000000..794bfc79 --- /dev/null +++ b/app/Contracts/Repository/OptionVariableRepositoryInterface.php @@ -0,0 +1,30 @@ +. + * + * 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\Contracts\Repository; + +interface OptionVariableRepositoryInterface extends RepositoryInterface +{ + // +} diff --git a/app/Contracts/Repository/RepositoryInterface.php b/app/Contracts/Repository/RepositoryInterface.php index 2a19810f..b3226095 100644 --- a/app/Contracts/Repository/RepositoryInterface.php +++ b/app/Contracts/Repository/RepositoryInterface.php @@ -145,4 +145,13 @@ interface RepositoryInterface * @return mixed */ public function all(); + + /** + * Insert a single or multiple records into the database at once skipping + * validation and mass assignment checking. + * + * @param array $data + * @return bool + */ + public function insert(array $data); } diff --git a/app/Contracts/Repository/ServerRepositoryInterface.php b/app/Contracts/Repository/ServerRepositoryInterface.php index 5619e3fd..99a95ccc 100644 --- a/app/Contracts/Repository/ServerRepositoryInterface.php +++ b/app/Contracts/Repository/ServerRepositoryInterface.php @@ -35,4 +35,21 @@ interface ServerRepositoryInterface extends RepositoryInterface, SearchableInter * @return mixed */ public function getAllServers($paginate); + + /** + * Return a server model and all variables associated with the server. + * + * @param int $id + * @return mixed + */ + public function findWithVariables($id); + + /** + * Return all of the server variables possible and default to the variable + * default if there is no value defined for the specific server requested. + * + * @param int $id + * @return array + */ + public function getVariablesWithValues($id); } diff --git a/app/Contracts/Repository/ServerVariableRepositoryInterface.php b/app/Contracts/Repository/ServerVariableRepositoryInterface.php new file mode 100644 index 00000000..b0ca226c --- /dev/null +++ b/app/Contracts/Repository/ServerVariableRepositoryInterface.php @@ -0,0 +1,30 @@ +. + * + * 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\Contracts\Repository; + +interface ServerVariableRepositoryInterface extends RepositoryInterface +{ + // +} diff --git a/app/Exceptions/Services/Servers/RequiredVariableMissingException.php b/app/Exceptions/Services/Servers/RequiredVariableMissingException.php new file mode 100644 index 00000000..f4a1a631 --- /dev/null +++ b/app/Exceptions/Services/Servers/RequiredVariableMissingException.php @@ -0,0 +1,32 @@ +. + * + * 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\Exceptions\Services\Servers; + +use Exception; + +class RequiredVariableMissingException extends Exception +{ + // +} diff --git a/app/Http/Controllers/API/User/ServerController.php b/app/Http/Controllers/API/User/ServerController.php index 90493518..f7e652c2 100644 --- a/app/Http/Controllers/API/User/ServerController.php +++ b/app/Http/Controllers/API/User/ServerController.php @@ -28,9 +28,9 @@ use Fractal; use Illuminate\Http\Request; use Pterodactyl\Models\Server; use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Repositories\Daemon\PowerRepository; +use Pterodactyl\Repositories\old_Daemon\PowerRepository; use Pterodactyl\Transformers\User\ServerTransformer; -use Pterodactyl\Repositories\Daemon\CommandRepository; +use Pterodactyl\Repositories\old_Daemon\CommandRepository; class ServerController extends Controller { diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php index 0c27bd59..e1059d74 100644 --- a/app/Http/Controllers/Admin/ServersController.php +++ b/app/Http/Controllers/Admin/ServersController.php @@ -32,6 +32,7 @@ use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; +use Pterodactyl\Http\Requests\Admin\ServerFormRequest; use Pterodactyl\Models; use Illuminate\Http\Request; use GuzzleHttp\Exception\TransferException; @@ -41,6 +42,7 @@ use Pterodactyl\Repositories\ServerRepository; use Pterodactyl\Repositories\DatabaseRepository; use Pterodactyl\Exceptions\AutoDeploymentException; use Pterodactyl\Exceptions\DisplayValidationException; +use Pterodactyl\Services\Servers\ServerService; class ServersController extends Controller { @@ -64,6 +66,11 @@ class ServersController extends Controller */ protected $repository; + /** + * @var \Pterodactyl\Services\Servers\ServerService + */ + protected $service; + /** * @var \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface */ @@ -73,6 +80,7 @@ class ServersController extends Controller ConfigRepository $config, DatabaseRepositoryInterface $databaseRepository, LocationRepositoryInterface $locationRepository, + ServerService $service, ServerRepositoryInterface $repository, ServiceRepositoryInterface $serviceRepository ) { @@ -80,6 +88,7 @@ class ServersController extends Controller $this->databaseRepository = $databaseRepository; $this->locationRepository = $locationRepository; $this->repository = $repository; + $this->service = $service; $this->serviceRepository = $serviceRepository; } @@ -125,31 +134,33 @@ class ServersController extends Controller /** * Create server controller method. * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Response\RedirectResponse + * @param \Illuminate\Http\Request $request */ public function store(Request $request) { - try { - $repo = new ServerRepository; - $server = $repo->create($request->except('_token')); - - return redirect()->route('admin.servers.view', $server->id); - } catch (DisplayValidationException $ex) { - return redirect()->route('admin.servers.new')->withErrors(json_decode($ex->getMessage()))->withInput(); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (AutoDeploymentException $ex) { - Alert::danger('Auto-Deployment Exception: ' . $ex->getMessage())->flash(); - } catch (TransferException $ex) { - Log::warning($ex); - Alert::danger('A TransferException was encountered while trying to contact the daemon, please ensure it is online and accessible. This error has been logged.')->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An unhandled exception occured while attemping to add this server. Please try again.')->flash(); - } + $this->service->create($request->all()); return redirect()->route('admin.servers.new')->withInput(); + // try { +// $repo = new ServerRepository; +// $server = $repo->create($request->except('_token')); +// +// return redirect()->route('admin.servers.view', $server->id); +// } catch (DisplayValidationException $ex) { +// return redirect()->route('admin.servers.new')->withErrors(json_decode($ex->getMessage()))->withInput(); +// } catch (DisplayException $ex) { +// Alert::danger($ex->getMessage())->flash(); +// } catch (AutoDeploymentException $ex) { +// Alert::danger('Auto-Deployment Exception: ' . $ex->getMessage())->flash(); +// } catch (TransferException $ex) { +// Log::warning($ex); +// Alert::danger('A TransferException was encountered while trying to contact the daemon, please ensure it is online and accessible. This error has been logged.')->flash(); +// } catch (\Exception $ex) { +// Log::error($ex); +// Alert::danger('An unhandled exception occured while attemping to add this server. Please try again.')->flash(); +// } +// +// return redirect()->route('admin.servers.new')->withInput(); } /** @@ -310,8 +321,9 @@ class ServersController extends Controller * @param int $id * @return \Illuminate\Http\RedirectResponse */ - public function setDetails(Request $request, $id) + public function setDetails(ServerFormRequest $request, Models\Server $server) { + dd($server); $repo = new ServerRepository; try { $repo->updateDetails($id, array_merge( diff --git a/app/Http/Controllers/Server/AjaxController.php b/app/Http/Controllers/Server/AjaxController.php index c22b0c20..1d824ddf 100644 --- a/app/Http/Controllers/Server/AjaxController.php +++ b/app/Http/Controllers/Server/AjaxController.php @@ -79,7 +79,7 @@ class AjaxController extends Controller $prevDir['link_show'] = implode('/', $goBack) . '/'; } - $controller = new Repositories\Daemon\FileRepository($uuid); + $controller = new Repositories\old_Daemon\FileRepository($uuid); try { $directoryContents = $controller->returnDirectoryListing($this->directory); @@ -112,7 +112,7 @@ class AjaxController extends Controller $server = Models\Server::byUuid($uuid); $this->authorize('save-files', $server); - $controller = new Repositories\Daemon\FileRepository($uuid); + $controller = new Repositories\old_Daemon\FileRepository($uuid); try { $controller->saveFileContents($request->input('file'), $request->input('contents')); diff --git a/app/Http/Controllers/Server/ServerController.php b/app/Http/Controllers/Server/ServerController.php index ce6f5959..c15af88c 100644 --- a/app/Http/Controllers/Server/ServerController.php +++ b/app/Http/Controllers/Server/ServerController.php @@ -32,7 +32,7 @@ use Illuminate\Http\Request; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Repositories\ServerRepository; -use Pterodactyl\Repositories\Daemon\FileRepository; +use Pterodactyl\Repositories\old_Daemon\FileRepository; use Pterodactyl\Exceptions\DisplayValidationException; class ServerController extends Controller diff --git a/app/Http/Requests/Admin/ServerFormRequest.php b/app/Http/Requests/Admin/ServerFormRequest.php new file mode 100644 index 00000000..1efe00ac --- /dev/null +++ b/app/Http/Requests/Admin/ServerFormRequest.php @@ -0,0 +1,83 @@ +. + * + * 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\Http\Requests\Admin; + +use Illuminate\Validation\Rule; +use Pterodactyl\Models\Server; + +class ServerFormRequest extends AdminFormRequest +{ + /** + * Rules to be applied to this request. + * + * @return array + */ + public function rules() + { + if ($this->method() === 'PATCH') { + return Server::getUpdateRulesForId($this->id); + } + + return Server::getCreateRules(); + } + + /** + * Run validation after the rules above have been applied. + * + * @param \Illuminate\Validation\Validator $validator + */ + public function withValidator($validator) + { + $validator->after(function ($validator) { + $validator->sometimes('node_id', 'required|numeric|bail|exists:nodes,id', function ($input) { + return ! ($input->auto_deploy); + }); + + $validator->sometimes('allocation_id', [ + 'required', + 'numeric', + 'bail', + Rule::exists('allocations', 'id')->where(function ($query) { + $query->where('node_id', $this->input('node_id')); + $query->whereNull('server_id'); + }), + ], function ($input) { + return ! ($input->auto_deploy); + }); + + $validator->sometimes('allocation_additional.*', [ + 'sometimes', + 'required', + 'numeric', + Rule::exists('allocations', 'id')->where(function ($query) { + $query->where('node_id', $this->input('node_id')); + $query->whereNull('server_id'); + }), + ], function ($input) { + return ! ($input->auto_deploy); + }); + }); + } +} diff --git a/app/Jobs/SendScheduledTask.php b/app/Jobs/SendScheduledTask.php index 525b670d..8f988973 100644 --- a/app/Jobs/SendScheduledTask.php +++ b/app/Jobs/SendScheduledTask.php @@ -31,8 +31,8 @@ use Pterodactyl\Models\TaskLog; use Illuminate\Queue\SerializesModels; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Contracts\Queue\ShouldQueue; -use Pterodactyl\Repositories\Daemon\PowerRepository; -use Pterodactyl\Repositories\Daemon\CommandRepository; +use Pterodactyl\Repositories\old_Daemon\PowerRepository; +use Pterodactyl\Repositories\old_Daemon\CommandRepository; class SendScheduledTask extends Job implements ShouldQueue { diff --git a/app/Models/Server.php b/app/Models/Server.php index 1a26243e..5a147c3e 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -29,13 +29,15 @@ use Cache; use Carbon; use Schema; use Javascript; +use Sofa\Eloquence\Eloquence; +use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; use Illuminate\Notifications\Notifiable; -use Nicolaslopezj\Searchable\SearchableTrait; +use Sofa\Eloquence\Contracts\Validable as ValidableContract; -class Server extends Model +class Server extends Model implements ValidableContract { - use Notifiable, SearchableTrait; + use Eloquence, Notifiable, Validable; /** * The table associated with the model. @@ -65,6 +67,43 @@ class Server extends Model */ protected $guarded = ['id', 'installed', 'created_at', 'updated_at', 'deleted_at']; + protected static $applicationRules = [ + 'owner_id' => 'required', + 'name' => 'required', + 'memory' => 'required', + 'swap' => 'required', + 'io' => 'required', + 'cpu' => 'required', + 'disk' => 'required', + 'service_id' => 'required', + 'option_id' => 'required', + 'pack_id' => 'sometimes', + 'auto_deploy' => 'sometimes', + 'custom_id' => 'sometimes', + 'skip_scripts' => 'sometimes', + ]; + + protected static $dataIntegrityRules = [ + 'owner_id' => 'exists:users,id', + 'name' => 'regex:/^([\w .-]{1,200})$/', + 'node_id' => 'exists:nodes,id', + 'description' => 'nullable|string', + 'memory' => 'numeric|min:0', + 'swap' => 'numeric|min:-1', + 'io' => 'numeric|between:10,1000', + 'cpu' => 'numeric|min:0', + 'disk' => 'numeric|min:0', + 'allocation_id' => 'exists:allocations,id', + 'service_id' => 'exists:services,id', + 'option_id' => 'exists:service_options,id', + 'pack_id' => 'nullable|numeric|min:0', + 'custom_container' => 'nullable|string', + 'startup' => 'nullable|string', + 'auto_deploy' => 'accepted', + 'custom_id' => 'numeric|unique:servers,id', + 'skip_scripts' => 'boolean', + ]; + /** * Cast values to correct type. * diff --git a/app/Providers/RepositoryServiceProvider.php b/app/Providers/RepositoryServiceProvider.php index 189ca6eb..07fc2d28 100644 --- a/app/Providers/RepositoryServiceProvider.php +++ b/app/Providers/RepositoryServiceProvider.php @@ -25,19 +25,27 @@ namespace Pterodactyl\Providers; use Illuminate\Support\ServiceProvider; +use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; use Pterodactyl\Contracts\Repository\ApiPermissionRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; +use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Pterodactyl\Contracts\Repository\OptionVariableRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; +use Pterodactyl\Repositories\Eloquent\AllocationRepository; use Pterodactyl\Repositories\Eloquent\ApiKeyRepository; use Pterodactyl\Repositories\Eloquent\ApiPermissionRepository; use Pterodactyl\Repositories\Eloquent\DatabaseHostRepository; use Pterodactyl\Repositories\Eloquent\DatabaseRepository; use Pterodactyl\Repositories\Eloquent\LocationRepository; +use Pterodactyl\Repositories\Eloquent\NodeRepository; +use Pterodactyl\Repositories\Eloquent\OptionVariableRepository; use Pterodactyl\Repositories\Eloquent\ServerRepository; +use Pterodactyl\Repositories\Eloquent\ServerVariableRepository; use Pterodactyl\Repositories\Eloquent\ServiceRepository; use Pterodactyl\Repositories\Eloquent\UserRepository; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; @@ -49,13 +57,23 @@ class RepositoryServiceProvider extends ServiceProvider */ public function register() { + $this->app->bind(AllocationRepositoryInterface::class, AllocationRepository::class); $this->app->bind(ApiKeyRepositoryInterface::class, ApiKeyRepository::class); $this->app->bind(ApiPermissionRepositoryInterface::class, ApiPermissionRepository::class); - $this->app->bind(DatabaseRepositoryInterface::class, DatabaseRepository::class); $this->app->bind(DatabaseHostRepositoryInterface::class, DatabaseHostRepository::class); + $this->app->bind(DatabaseRepositoryInterface::class, DatabaseRepository::class); $this->app->bind(LocationRepositoryInterface::class, LocationRepository::class); + $this->app->bind(NodeRepositoryInterface::class, NodeRepository::class); + $this->app->bind(OptionVariableRepositoryInterface::class, OptionVariableRepository::class); $this->app->bind(ServerRepositoryInterface::class, ServerRepository::class); + $this->app->bind(ServerVariableRepositoryInterface::class, ServerVariableRepository::class); $this->app->bind(ServiceRepositoryInterface::class, ServiceRepository::class); $this->app->bind(UserRepositoryInterface::class, UserRepository::class); + + // Daemon Repositories + $this->app->bind( + \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface::class, + \Pterodactyl\Repositories\Daemon\ServerRepository::class + ); } } diff --git a/app/Repositories/Daemon/BaseRepository.php b/app/Repositories/Daemon/BaseRepository.php new file mode 100644 index 00000000..5f6f92b6 --- /dev/null +++ b/app/Repositories/Daemon/BaseRepository.php @@ -0,0 +1,105 @@ +. + * + * 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\Repositories\Daemon; + +use GuzzleHttp\Client; +use Illuminate\Foundation\Application; +use Pterodactyl\Contracts\Repository\Daemon\BaseRepositoryInterface; +use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Illuminate\Contracts\Config\Repository as ConfigRepository; + +class BaseRepository implements BaseRepositoryInterface +{ + protected $app; + protected $accessServer; + protected $accessToken; + protected $node; + protected $config; + protected $nodeRepository; + + public function __construct( + Application $app, + ConfigRepository $config, + NodeRepositoryInterface $nodeRepository + ) { + $this->app = $app; + $this->config = $config; + $this->nodeRepository = $nodeRepository; + } + + public function setNode($id) + { + $this->node = $this->nodeRepository->find($id); + + return $this; + } + + public function getNode() + { + return $this->node; + } + + public function setAccessServer($server = null) + { + $this->accessServer = $server; + + return $this; + } + + public function getAccessServer() + { + return $this->accessServer; + } + + public function setAccessToken($token = null) + { + $this->accessToken = $token; + + return $this; + } + + public function getAccessToken() + { + return $this->accessToken; + } + + public function getHttpClient($headers = []) + { + if (! is_null($this->accessServer)) { + $headers[] = ['X-Access-Server' => $this->getAccessServer()]; + } + + if (! is_null($this->accessToken)) { + $headers[] = ['X-Access-Token' => $this->getAccessToken()]; + } + + return new Client([ + 'base_uri' => sprintf('%s://%s:%s/', $this->getNode()->scheme, $this->getNode()->fqdn, $this->getNode()->daemonListen), + 'timeout' => $this->config->get('pterodactyl.guzzle.timeout'), + 'connect_timeout' => $this->config->get('pterodactyl.guzzle.connect_timeout'), + 'headers' => $headers, + ]); + } +} diff --git a/app/Repositories/Daemon/ServerRepository.php b/app/Repositories/Daemon/ServerRepository.php new file mode 100644 index 00000000..2ee01183 --- /dev/null +++ b/app/Repositories/Daemon/ServerRepository.php @@ -0,0 +1,86 @@ +. + * + * 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\Repositories\Daemon; + +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface as DatabaseServerRepositoryInterface; +use Pterodactyl\Services\Servers\EnvironmentService; + +class ServerRepository extends BaseRepository implements ServerRepositoryInterface +{ + const DAEMON_PERMISSIONS = ['s:*']; + + /** + * {@inheritdoc} + */ + public function create($id, $overrides = [], $start = false) + { + $repository = $this->app->make(DatabaseServerRepositoryInterface::class); + $environment = $this->app->make(EnvironmentService::class); + + $server = $repository->getDataForCreation($id); + + $data = [ + 'uuid' => (string) $server->uuid, + 'user' => $server->username, + 'build' => [ + 'default' => [ + 'ip' => $server->allocation->ip, + 'port' => $server->allocation->port, + ], + 'ports' => $server->allocations->groupBy('ip')->map(function ($item) { + return $item->pluck('port'); + })->toArray(), + 'env' => $environment->process($server), + 'memory' => (int) $server->memory, + 'swap' => (int) $server->swap, + 'io' => (int) $server->io, + 'cpu' => (int) $server->cpu, + 'disk' => (int) $server->disk, + 'image' => (int) $server->image, + ], + 'service' => [ + 'type' => $server->option->service->folder, + 'option' => $server->option->tag, + 'pack' => object_get($server, 'pack.uuid'), + 'skip_scripts' => $server->skip_scripts, + ], + 'rebuild' => false, + 'start_on_completion' => $start, + 'keys' => [ + (string) $server->daemonSecret => self::DAEMON_PERMISSIONS, + ], + ]; + + // Loop through overrides. + foreach ($overrides as $key => $value) { + array_set($data, $key, $value); + } + +// $this->getHttpClient()->request('POST', '/servers', [ +// 'json' => $data, +// ]); + } +} diff --git a/app/Repositories/Eloquent/AllocationRepository.php b/app/Repositories/Eloquent/AllocationRepository.php new file mode 100644 index 00000000..21dc85ee --- /dev/null +++ b/app/Repositories/Eloquent/AllocationRepository.php @@ -0,0 +1,47 @@ +. + * + * 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\Repositories\Eloquent; + +use Pterodactyl\Models\Allocation; +use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; + +class AllocationRepository extends EloquentRepository implements AllocationRepositoryInterface +{ + /** + * {@inheritdoc} + */ + public function model() + { + return Allocation::class; + } + + /** + * {@inheritdoc} + */ + public function assignAllocationsToServer($server, array $ids) + { + return $this->getBuilder()->whereIn('id', $ids)->update(['server_id' => $server]); + } +} diff --git a/app/Repositories/Eloquent/EloquentRepository.php b/app/Repositories/Eloquent/EloquentRepository.php index 793291bb..994647df 100644 --- a/app/Repositories/Eloquent/EloquentRepository.php +++ b/app/Repositories/Eloquent/EloquentRepository.php @@ -160,4 +160,12 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf { return $this->getBuilder()->get($this->getColumns()); } + + /** + * {@inheritdoc} + */ + public function insert(array $data) + { + return $this->getBuilder()->insert($data); + } } diff --git a/app/Repositories/Eloquent/NodeRepository.php b/app/Repositories/Eloquent/NodeRepository.php new file mode 100644 index 00000000..cc2e5303 --- /dev/null +++ b/app/Repositories/Eloquent/NodeRepository.php @@ -0,0 +1,40 @@ +. + * + * 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\Repositories\Eloquent; + +use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Pterodactyl\Models\Node; +use Pterodactyl\Repositories\Eloquent\Attributes\SearchableRepository; + +class NodeRepository extends SearchableRepository implements NodeRepositoryInterface +{ + /** + * {@inheritdoc} + */ + public function model() + { + return Node::class; + } +} diff --git a/app/Repositories/Eloquent/OptionVariableRepository.php b/app/Repositories/Eloquent/OptionVariableRepository.php new file mode 100644 index 00000000..45cc9110 --- /dev/null +++ b/app/Repositories/Eloquent/OptionVariableRepository.php @@ -0,0 +1,39 @@ +. + * + * 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\Repositories\Eloquent; + +use Pterodactyl\Models\ServiceVariable; +use Pterodactyl\Contracts\Repository\OptionVariableRepositoryInterface; + +class OptionVariableRepository extends EloquentRepository implements OptionVariableRepositoryInterface +{ + /** + * {@inheritdoc} + */ + public function model() + { + return ServiceVariable::class; + } +} diff --git a/app/Repositories/Eloquent/ServerRepository.php b/app/Repositories/Eloquent/ServerRepository.php index 4f605b64..2221ceb0 100644 --- a/app/Repositories/Eloquent/ServerRepository.php +++ b/app/Repositories/Eloquent/ServerRepository.php @@ -24,6 +24,7 @@ namespace Pterodactyl\Repositories\Eloquent; +use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Models\Server; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Repositories\Eloquent\Attributes\SearchableRepository; @@ -51,4 +52,55 @@ class ServerRepository extends SearchableRepository implements ServerRepositoryI return $instance->paginate($paginate); } + + /** + * {@inheritdoc} + * @return \Illuminate\Database\Eloquent\Model + */ + public function findWithVariables($id) + { + $instance = $this->getBuilder()->with('option.variables', 'variables') + ->where($this->getModel()->getKeyName(), '=', $id) + ->first($this->getColumns()); + + if (is_null($instance)) { + throw new RecordNotFoundException(); + } + + return $instance; + } + + /** + * {@inheritdoc} + */ + public function getVariablesWithValues($id) + { + $instance = $this->getBuilder()->with('variables', 'option.variables') + ->find($id, $this->getColumns()); + + if (! $instance) { + throw new RecordNotFoundException(); + } + + $data = []; + $instance->option->variables->each(function ($item) use (&$data, $instance) { + $display = $instance->variables->where('variable_id', $item->id)->pluck('variable_value')->first(); + + $data[$item->env_variable] = $display ?? $item->default_value; + }); + + return $data; + } + + public function getDataForCreation($id) + { + $instance = $this->getBuilder()->with('allocation', 'allocations', 'pack', 'option.service') + ->find($id, $this->getColumns()); + + if (! $instance) { + throw new RecordNotFoundException(); + } + + return $instance; + } } diff --git a/app/Repositories/Eloquent/ServerVariableRepository.php b/app/Repositories/Eloquent/ServerVariableRepository.php new file mode 100644 index 00000000..e309b88d --- /dev/null +++ b/app/Repositories/Eloquent/ServerVariableRepository.php @@ -0,0 +1,39 @@ +. + * + * 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\Repositories\Eloquent; + +use Pterodactyl\Models\ServerVariable; +use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; + +class ServerVariableRepository extends EloquentRepository implements ServerVariableRepositoryInterface +{ + /** + * {@inheritdoc} + */ + public function model() + { + return ServerVariable::class; + } +} diff --git a/app/Repositories/Daemon/CommandRepository.php b/app/Repositories/old_Daemon/CommandRepository.php similarity index 98% rename from app/Repositories/Daemon/CommandRepository.php rename to app/Repositories/old_Daemon/CommandRepository.php index beb9e853..2f1a41ee 100644 --- a/app/Repositories/Daemon/CommandRepository.php +++ b/app/Repositories/old_Daemon/CommandRepository.php @@ -22,7 +22,7 @@ * SOFTWARE. */ -namespace Pterodactyl\Repositories\Daemon; +namespace Pterodactyl\Repositories\old_Daemon; use Pterodactyl\Models\User; use Pterodactyl\Models\Server; diff --git a/app/Repositories/Daemon/FileRepository.php b/app/Repositories/old_Daemon/FileRepository.php similarity index 99% rename from app/Repositories/Daemon/FileRepository.php rename to app/Repositories/old_Daemon/FileRepository.php index e789f746..8254c227 100644 --- a/app/Repositories/Daemon/FileRepository.php +++ b/app/Repositories/old_Daemon/FileRepository.php @@ -22,7 +22,7 @@ * SOFTWARE. */ -namespace Pterodactyl\Repositories\Daemon; +namespace Pterodactyl\Repositories\old_Daemon; use Exception; use GuzzleHttp\Client; diff --git a/app/Repositories/Daemon/PowerRepository.php b/app/Repositories/old_Daemon/PowerRepository.php similarity index 98% rename from app/Repositories/Daemon/PowerRepository.php rename to app/Repositories/old_Daemon/PowerRepository.php index 92537909..9e0cbc29 100644 --- a/app/Repositories/Daemon/PowerRepository.php +++ b/app/Repositories/old_Daemon/PowerRepository.php @@ -22,7 +22,7 @@ * SOFTWARE. */ -namespace Pterodactyl\Repositories\Daemon; +namespace Pterodactyl\Repositories\old_Daemon; use Pterodactyl\Models\User; use Pterodactyl\Models\Server; diff --git a/app/Services/Components/UuidService.php b/app/Services/Components/UuidService.php index 468a97f8..27d7e541 100644 --- a/app/Services/Components/UuidService.php +++ b/app/Services/Components/UuidService.php @@ -37,6 +37,7 @@ class UuidService * @param string $field * @param int $type * @return string + * @deprecated */ public function generate($table = 'users', $field = 'uuid', $type = 4) { @@ -58,6 +59,7 @@ class UuidService * @param string $field * @param null|string $attachedUuid * @return string + * @deprecated */ public function generateShort($table = 'servers', $field = 'uuidShort', $attachedUuid = null) { diff --git a/app/Services/Servers/EnvironmentService.php b/app/Services/Servers/EnvironmentService.php new file mode 100644 index 00000000..0bdb5131 --- /dev/null +++ b/app/Services/Servers/EnvironmentService.php @@ -0,0 +1,106 @@ +. + * + * 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\Servers; + +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Models\Server; + +class EnvironmentService +{ + const ENVIRONMENT_CASTS = [ + 'STARTUP' => 'startup', + 'P_SERVER_LOCATION' => 'location.short', + 'P_SERVER_UUID' => 'uuid', + ]; + + /** + * @var array + */ + protected $additional = []; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $repository; + + /** + * EnvironmentService constructor. + * + * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository + */ + public function __construct(ServerRepositoryInterface $repository) + { + $this->repository = $repository; + } + + /** + * Dynamically configure additional environment variables to be assigned + * with a specific server. + * + * @param string $key + * @param callable $closure + * @return $this + */ + public function setEnvironmentKey($key, callable $closure) + { + $this->additional[] = [$key, $closure]; + + return $this; + } + + /** + * Take all of the environment variables configured for this server and return + * them in an easy to process format. + * + * @param int|\Pterodactyl\Models\Server $server + * @return array + */ + public function process($server) + { + if (! $server instanceof Server) { + if (! is_numeric($server)) { + throw new \InvalidArgumentException( + 'First argument passed to process() must be an instance of \\Pterodactyl\\Models\\Server or numeric.' + ); + } + + $server = $this->repository->find($server); + } + + $variables = $this->repository->getVariablesWithValues($server->id); + + // Process static environment variables defined in this file. + foreach (self::ENVIRONMENT_CASTS as $key => $object) { + $variables[$key] = object_get($server, $object); + } + + // Process dynamically included environment variables. + foreach ($this->additional as $item) { + $variables[$item[0]] = call_user_func($item[1], $server); + } + + return $variables; + } +} diff --git a/app/Services/Servers/ServerService.php b/app/Services/Servers/ServerService.php new file mode 100644 index 00000000..dd7ba913 --- /dev/null +++ b/app/Services/Servers/ServerService.php @@ -0,0 +1,149 @@ +. + * + * 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\Servers; + +use Ramsey\Uuid\Uuid; +use Illuminate\Database\ConnectionInterface; +use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; + +class ServerService +{ + /** + * @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface + */ + protected $allocationRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface + */ + protected $nodeRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface + */ + protected $userRepository; + + protected $database; + protected $repository; + protected $usernameService; + protected $serverVariableRepository; + protected $daemonServerRepository; + + /** + * @var \Pterodactyl\Services\Servers\VariableValidatorService + */ + protected $validatorService; + + public function __construct( + AllocationRepositoryInterface $allocationRepository, + ConnectionInterface $database, + ServerRepositoryInterface $repository, + DaemonServerRepositoryInterface $daemonServerRepository, + ServerVariableRepositoryInterface $serverVariableRepository, + NodeRepositoryInterface $nodeRepository, + UsernameGenerationService $usernameService, + UserRepositoryInterface $userRepository, + VariableValidatorService $validatorService + ) { + $this->allocationRepository = $allocationRepository; + $this->database = $database; + $this->repository = $repository; + $this->nodeRepository = $nodeRepository; + $this->userRepository = $userRepository; + $this->usernameService = $usernameService; + $this->validatorService = $validatorService; + $this->serverVariableRepository = $serverVariableRepository; + $this->daemonServerRepository = $daemonServerRepository; + } + + public function create(array $data) + { + // @todo auto-deployment and packs + $data['user_id'] = 1; + + $node = $this->nodeRepository->find($data['node_id']); + $validator = $this->validatorService->setAdmin()->setFields($data['environment'])->validate($data['option_id']); + $uniqueShort = bin2hex(random_bytes(4)); + + $this->database->beginTransaction(); + + $server = $this->repository->create([ + 'uuid' => Uuid::uuid4()->toString(), + 'uuidShort' => bin2hex(random_bytes(4)), + 'node_id' => $data['node_id'], + 'name' => $data['name'], + 'description' => $data['description'], + 'skip_scripts' => isset($data['skip_scripts']), + 'suspended' => false, + 'owner_id' => $data['user_id'], + 'memory' => $data['memory'], + 'swap' => $data['swap'], + 'disk' => $data['disk'], + 'io' => $data['io'], + 'cpu' => $data['cpu'], + 'oom_disabled' => isset($data['oom_disabled']), + 'allocation_id' => $data['allocation_id'], + 'service_id' => $data['service_id'], + 'option_id' => $data['option_id'], + 'pack_id' => ($data['pack_id'] == 0) ? null : $data['pack_id'], + 'startup' => $data['startup'], + 'daemonSecret' => bin2hex(random_bytes(18)), + 'image' => $data['docker_image'], + 'username' => $this->usernameService->generate($data['name'], $uniqueShort), + 'sftp_password' => null, + ]); + + // Process allocations and assign them to the server in the database. + $records = [$data['allocation_id']]; + if (isset($data['allocation_additional']) && is_array($data['allocation_additional'])) { + $records = array_merge($records, $data['allocation_additional']); + } + + $this->allocationRepository->assignAllocationsToServer($server->id, $records); + + // Process the passed variables and store them in the database. + $records = []; + foreach ($validator->getResults() as $result) { + $records[] = [ + 'server_id' => $server->id, + 'variable_id' => $result['id'], + 'variable_value' => $result['value'], + ]; + } + + $this->serverVariableRepository->insert($records); + + // Create the server on the daemon & commit it to the database. + $this->daemonServerRepository->setNode($server->node_id)->setAccessToken($node->daemonSecret)->create($server->id); + $this->database->rollBack(); + + return $server; + } +} diff --git a/app/Services/Servers/UsernameGenerationService.php b/app/Services/Servers/UsernameGenerationService.php new file mode 100644 index 00000000..10e3382f --- /dev/null +++ b/app/Services/Servers/UsernameGenerationService.php @@ -0,0 +1,55 @@ +. + * + * 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\Servers; + +class UsernameGenerationService +{ + /** + * Generate a unique username to be used for SFTP connections and identification + * of the server docker container on the host system. + * + * @param string $name + * @param null $identifier + * @return string + */ + public function generate($name, $identifier = null) + { + if (is_null($identifier) || ! ctype_alnum($identifier)) { + $unique = bin2hex(random_bytes(4)); + } else { + if (strlen($identifier) < 8) { + $unique = $identifier . str_random((8 - strlen($identifier))); + } else { + $unique = substr($identifier, 0, 8); + } + } + + // Filter the Server Name + $name = trim(preg_replace('/[^\w]+/', '', $name), '_'); + $name = (strlen($name) < 1) ? str_random(6) : $name; + + return strtolower(substr($name, 0, 6) . '_' . $unique); + } +} diff --git a/app/Services/Servers/VariableValidatorService.php b/app/Services/Servers/VariableValidatorService.php new file mode 100644 index 00000000..9337f38c --- /dev/null +++ b/app/Services/Servers/VariableValidatorService.php @@ -0,0 +1,173 @@ +. + * + * 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\Servers; + +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; +use Pterodactyl\Exceptions\DisplayValidationException; +use Illuminate\Validation\Factory as ValidationFactory; +use Pterodactyl\Contracts\Repository\OptionVariableRepositoryInterface; +use Pterodactyl\Exceptions\Services\Servers\RequiredVariableMissingException; + +class VariableValidatorService +{ + /** + * @var bool + */ + protected $isAdmin = false; + + /** + * @var array + */ + protected $fields = []; + + /** + * @var array + */ + protected $results = []; + + /** + * @var \Pterodactyl\Contracts\Repository\OptionVariableRepositoryInterface + */ + protected $optionVariableRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $serverRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface + */ + protected $serverVariableRepository; + + /** + * @var \Illuminate\Validation\Factory + */ + protected $validator; + + public function __construct( + OptionVariableRepositoryInterface $optionVariableRepository, + ServerRepositoryInterface $serverRepository, + ServerVariableRepositoryInterface $serverVariableRepository, + ValidationFactory $validator + ) { + $this->optionVariableRepository = $optionVariableRepository; + $this->serverRepository = $serverRepository; + $this->serverVariableRepository = $serverVariableRepository; + $this->validator = $validator; + } + + /** + * Set the fields with populated data to validate. + * + * @param array $fields + * @return $this + */ + public function setFields(array $fields) + { + $this->fields = $fields; + + return $this; + } + + /** + * Set this function to be running at the administrative level. + * + * @return $this + */ + public function setAdmin() + { + $this->isAdmin = true; + + return $this; + } + + /** + * Validate all of the passed data aganist the given service option variables. + * + * @param int $option + * @return $this + */ + public function validate($option) + { + $variables = $this->optionVariableRepository->findWhere([['option_id', '=', $option]]); + if (count($variables) === 0) { + $this->results = []; + + return $this; + } + + $variables->each(function ($item) { + if (! isset($this->fields[$item->env_variable]) && $item->required) { + if ($item->required) { + throw new RequiredVariableMissingException( + sprintf('Required service option variable %s was missing from this request.', $item->env_variable) + ); + } + } + + // Skip doing anything if user is not an admin and variable is not user viewable + // or editable. + if (! $this->isAdmin && (! $item->user_editable || ! $item->user_viewable)) { + return; + } + + $validator = $this->validator->make([ + 'variable_value' => array_key_exists($item->env_variable, $this->fields) ? $this->fields[$item->env_variable] : null, + ], [ + 'variable_value' => $item->rules, + ]); + + if ($validator->fails()) { + throw new DisplayValidationException(json_encode( + collect([ + 'notice' => [ + sprintf('There was a validation error with the %s variable.', $item->name), + ], + ])->merge($validator->errors()->toArray()) + )); + } + + $this->results[] = [ + 'id' => $item->id, + 'key' => $item->env_variable, + 'value' => $this->fields[$item->env_variable], + ]; + }); + + return $this; + } + + /** + * Return the final results after everything has been validated. + * + * @return array + */ + public function getResults() + { + return $this->results; + } +} diff --git a/public/themes/pterodactyl/js/admin/new-server.js b/public/themes/pterodactyl/js/admin/new-server.js index f3de55be..ecc0b9fb 100644 --- a/public/themes/pterodactyl/js/admin/new-server.js +++ b/public/themes/pterodactyl/js/admin/new-server.js @@ -179,7 +179,7 @@ $('#pOptionId').on('change', function (event) { var dataAppend = ' \
\ \ - \ + \

' + item.description + '
\ Access in Startup: {{' + item.env_variable + '}}
\ Validation Rules: ' + item.rules + '

\ diff --git a/resources/themes/pterodactyl/admin/servers/new.blade.php b/resources/themes/pterodactyl/admin/servers/new.blade.php index eaf20445..8a68b197 100644 --- a/resources/themes/pterodactyl/admin/servers/new.blade.php +++ b/resources/themes/pterodactyl/admin/servers/new.blade.php @@ -83,7 +83,7 @@ @foreach($locations as $location) @endforeach @@ -229,15 +229,10 @@
- - + +

This is the default Docker container that will be used to run this server.

-
- - -

If you would like to use a custom Docker container please enter it here, otherwise leave empty.

-
From 580e5ac569935045b72696026c1bee279267c6d7 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Fri, 21 Jul 2017 21:17:42 -0500 Subject: [PATCH 27/99] Begin working on administrative server view changes Also includes tests for the DatabaseCreation service. --- .../AllocationRepositoryInterface.php | 8 + .../Daemon/ServerRepositoryInterface.php | 2 +- .../Repository/NodeRepositoryInterface.php | 8 +- .../Repository/ServerRepositoryInterface.php | 29 +- app/Exceptions/Handler.php | 17 +- .../Controllers/Admin/ServersController.php | 139 ++++---- app/Http/Requests/Admin/ServerFormRequest.php | 9 + app/Repositories/Daemon/ServerRepository.php | 6 +- .../Eloquent/AllocationRepository.php | 8 + app/Repositories/Eloquent/NodeRepository.php | 27 ++ .../Eloquent/ServerRepository.php | 38 ++- app/Services/Database/CreationService.php | 4 +- app/Services/LocationService.php | 2 +- ...{ServerService.php => CreationService.php} | 59 +++- app/Services/Servers/EnvironmentService.php | 2 +- .../Servers/VariableValidatorService.php | 10 +- .../admin/servers/view/startup.blade.php | 31 +- .../Services/Database/CreationServiceTest.php | 320 ++++++++++++++++++ 18 files changed, 584 insertions(+), 135 deletions(-) rename app/Services/Servers/{ServerService.php => CreationService.php} (76%) create mode 100644 tests/Unit/Services/Database/CreationServiceTest.php diff --git a/app/Contracts/Repository/AllocationRepositoryInterface.php b/app/Contracts/Repository/AllocationRepositoryInterface.php index 4dca0af8..e2c2abb1 100644 --- a/app/Contracts/Repository/AllocationRepositoryInterface.php +++ b/app/Contracts/Repository/AllocationRepositoryInterface.php @@ -34,4 +34,12 @@ interface AllocationRepositoryInterface extends RepositoryInterface * @return int */ public function assignAllocationsToServer($server, array $ids); + + /** + * Return all of the allocations for a specific node. + * + * @param int $node + * @return \Illuminate\Database\Eloquent\Collection + */ + public function getAllocationsForNode($node); } diff --git a/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php b/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php index a4e398bc..d4554cc7 100644 --- a/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php +++ b/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php @@ -32,7 +32,7 @@ interface ServerRepositoryInterface extends BaseRepositoryInterface * @param int $id * @param array $overrides * @param bool $start - * @return mixed + * @return \Psr\Http\Message\ResponseInterface */ public function create($id, $overrides = [], $start = false); } diff --git a/app/Contracts/Repository/NodeRepositoryInterface.php b/app/Contracts/Repository/NodeRepositoryInterface.php index c72e2fe8..1dcdad8e 100644 --- a/app/Contracts/Repository/NodeRepositoryInterface.php +++ b/app/Contracts/Repository/NodeRepositoryInterface.php @@ -28,5 +28,11 @@ use Pterodactyl\Contracts\Repository\Attributes\SearchableInterface; interface NodeRepositoryInterface extends RepositoryInterface, SearchableInterface { - // + /** + * Return a collection of nodes beloning to a specific location for use on frontend display. + * + * @param int $location + * @return mixed + */ + public function getNodesForLocation($location); } diff --git a/app/Contracts/Repository/ServerRepositoryInterface.php b/app/Contracts/Repository/ServerRepositoryInterface.php index 99a95ccc..ece4cee6 100644 --- a/app/Contracts/Repository/ServerRepositoryInterface.php +++ b/app/Contracts/Repository/ServerRepositoryInterface.php @@ -41,6 +41,8 @@ interface ServerRepositoryInterface extends RepositoryInterface, SearchableInter * * @param int $id * @return mixed + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function findWithVariables($id); @@ -49,7 +51,30 @@ interface ServerRepositoryInterface extends RepositoryInterface, SearchableInter * default if there is no value defined for the specific server requested. * * @param int $id - * @return array + * @param bool $returnAsObject + * @return array|object + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function getVariablesWithValues($id); + public function getVariablesWithValues($id, $returnAsObject = false); + + /** + * Return enough data to be used for the creation of a server via the daemon. + * + * @param int $id + * @return \Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Model + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function getDataForCreation($id); + + /** + * Return a server as well as associated databases and their hosts. + * + * @param int $id + * @return mixed + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function getWithDatabases($id); } diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index a801cdce..b068f4cc 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -2,7 +2,6 @@ namespace Pterodactyl\Exceptions; -use Log; use Exception; use Illuminate\Auth\AuthenticationException; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; @@ -21,6 +20,7 @@ class Handler extends ExceptionHandler \Illuminate\Database\Eloquent\ModelNotFoundException::class, \Illuminate\Session\TokenMismatchException::class, \Illuminate\Validation\ValidationException::class, + \Pterodactyl\Exceptions\Model\DataValidationException::class, ]; /** @@ -28,20 +28,23 @@ class Handler extends ExceptionHandler * * This is a great spot to send exceptions to Sentry, Bugsnag, etc. * - * @param \Exception $exception - * @return void + * @param \Exception $exception + * + * @throws \Exception */ public function report(Exception $exception) { - return parent::report($exception); + parent::report($exception); } /** * Render an exception into an HTTP response. * - * @param \Illuminate\Http\Request $request - * @param \Exception $exception - * @return \Illuminate\Http\Response + * @param \Illuminate\Http\Request $request + * @param \Exception $exception + * @return \Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response + * + * @throws \Exception */ public function render($request, Exception $exception) { diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php index e1059d74..7af2e22a 100644 --- a/app/Http/Controllers/Admin/ServersController.php +++ b/app/Http/Controllers/Admin/ServersController.php @@ -28,8 +28,10 @@ use Illuminate\Contracts\Config\Repository as ConfigRepository; use Log; use Alert; use Javascript; +use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; +use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; use Pterodactyl\Http\Requests\Admin\ServerFormRequest; @@ -38,14 +40,19 @@ use Illuminate\Http\Request; use GuzzleHttp\Exception\TransferException; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Http\Controllers\Controller; +use Pterodactyl\Repositories\Eloquent\DatabaseHostRepository; use Pterodactyl\Repositories\ServerRepository; use Pterodactyl\Repositories\DatabaseRepository; -use Pterodactyl\Exceptions\AutoDeploymentException; use Pterodactyl\Exceptions\DisplayValidationException; -use Pterodactyl\Services\Servers\ServerService; +use Pterodactyl\Services\Servers\CreationService; class ServersController extends Controller { + /** + * @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface + */ + protected $allocationRepository; + /** * @var \Illuminate\Contracts\Config\Repository */ @@ -56,18 +63,28 @@ class ServersController extends Controller */ protected $databaseRepository; + /** + * @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface + */ + protected $databaseHostRepository; + /** * @var \Pterodactyl\Contracts\Repository\LocationRepositoryInterface */ protected $locationRepository; + /** + * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface + */ + protected $nodeRepository; + /** * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface */ protected $repository; /** - * @var \Pterodactyl\Services\Servers\ServerService + * @var \Pterodactyl\Services\Servers\CreationService */ protected $service; @@ -77,16 +94,22 @@ class ServersController extends Controller protected $serviceRepository; public function __construct( + AllocationRepositoryInterface $allocationRepository, ConfigRepository $config, + CreationService $service, DatabaseRepositoryInterface $databaseRepository, + DatabaseHostRepository $databaseHostRepository, LocationRepositoryInterface $locationRepository, - ServerService $service, + NodeRepositoryInterface $nodeRepository, ServerRepositoryInterface $repository, ServiceRepositoryInterface $serviceRepository ) { + $this->allocationRepository = $allocationRepository; $this->config = $config; $this->databaseRepository = $databaseRepository; + $this->databaseHostRepository = $databaseHostRepository; $this->locationRepository = $locationRepository; + $this->nodeRepository = $nodeRepository; $this->repository = $repository; $this->service = $service; $this->serviceRepository = $serviceRepository; @@ -132,35 +155,25 @@ class ServersController extends Controller } /** - * Create server controller method. + * Handle POST of server creation form. * - * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Http\Requests\Admin\ServerFormRequest $request + * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function store(Request $request) + public function store(ServerFormRequest $request) { - $this->service->create($request->all()); + try { + $server = $this->service->create($request->except('_token')); + + return redirect()->route('admin.servers.view', $server->id); + } catch (TransferException $ex) { + Log::warning($ex); + Alert::danger('A TransferException was encountered while trying to contact the daemon, please ensure it is online and accessible. This error has been logged.')->flash(); + } return redirect()->route('admin.servers.new')->withInput(); - // try { -// $repo = new ServerRepository; -// $server = $repo->create($request->except('_token')); -// -// return redirect()->route('admin.servers.view', $server->id); -// } catch (DisplayValidationException $ex) { -// return redirect()->route('admin.servers.new')->withErrors(json_decode($ex->getMessage()))->withInput(); -// } catch (DisplayException $ex) { -// Alert::danger($ex->getMessage())->flash(); -// } catch (AutoDeploymentException $ex) { -// Alert::danger('Auto-Deployment Exception: ' . $ex->getMessage())->flash(); -// } catch (TransferException $ex) { -// Log::warning($ex); -// Alert::danger('A TransferException was encountered while trying to contact the daemon, please ensure it is online and accessible. This error has been logged.')->flash(); -// } catch (\Exception $ex) { -// Log::error($ex); -// Alert::danger('An unhandled exception occured while attemping to add this server. Please try again.')->flash(); -// } -// -// return redirect()->route('admin.servers.new')->withInput(); } /** @@ -171,26 +184,7 @@ class ServersController extends Controller */ public function nodes(Request $request) { - $nodes = Models\Node::with('allocations')->where('location_id', $request->input('location'))->get(); - - return $nodes->map(function ($item) { - $filtered = $item->allocations->where('server_id', null)->map(function ($map) { - return collect($map)->only(['id', 'ip', 'port']); - }); - - $item->ports = $filtered->map(function ($map) use ($item) { - return [ - 'id' => $map['id'], - 'text' => $map['ip'] . ':' . $map['port'], - ]; - })->values(); - - return [ - 'id' => $item->id, - 'text' => $item->name, - 'allocations' => $item->ports, - ]; - })->values(); + return $this->nodeRepository->getNodesForLocation($request->input('location')); } /** @@ -202,7 +196,7 @@ class ServersController extends Controller */ public function viewIndex(Request $request, $id) { - return view('admin.servers.view.index', ['server' => Models\Server::findOrFail($id)]); + return view('admin.servers.view.index', ['server' => $this->repository->find($id)]); } /** @@ -214,9 +208,12 @@ class ServersController extends Controller */ public function viewDetails(Request $request, $id) { - $server = Models\Server::where('installed', 1)->findOrFail($id); - - return view('admin.servers.view.details', ['server' => $server]); + return view('admin.servers.view.details', [ + 'server' => $this->repository->findFirstWhere([ + ['id', '=', $id], + ['installed', '=', 1], + ]), + ]); } /** @@ -228,12 +225,17 @@ class ServersController extends Controller */ public function viewBuild(Request $request, $id) { - $server = Models\Server::where('installed', 1)->with('node.allocations')->findOrFail($id); + $server = $this->repository->findFirstWhere([ + ['id', '=', $id], + ['installed', '=', 1], + ]); + + $allocations = $this->allocationRepository->getAllocationsForNode($server->node_id); return view('admin.servers.view.build', [ 'server' => $server, - 'assigned' => $server->node->allocations->where('server_id', $server->id)->sortBy('port')->sortBy('ip'), - 'unassigned' => $server->node->allocations->where('server_id', null)->sortBy('port')->sortBy('ip'), + 'assigned' => $allocations->where('server_id', $server->id)->sortBy('port')->sortBy('ip'), + 'unassigned' => $allocations->where('server_id', null)->sortBy('port')->sortBy('ip'), ]); } @@ -246,29 +248,24 @@ class ServersController extends Controller */ public function viewStartup(Request $request, $id) { - $server = Models\Server::where('installed', 1)->with('option.variables', 'variables')->findOrFail($id); - $server->option->variables->transform(function ($item, $key) use ($server) { - $item->server_value = $server->variables->where('variable_id', $item->id)->pluck('variable_value')->first(); + $parameters = $this->repository->getVariablesWithValues($id, true); + if (! $parameters->server->installed) { + abort(404); + } - return $item; - }); + $services = $this->serviceRepository->getWithOptions(); - $services = Models\Service::with('options.packs', 'options.variables')->get(); Javascript::put([ 'services' => $services->map(function ($item) { return array_merge($item->toArray(), [ 'options' => $item->options->keyBy('id')->toArray(), ]); })->keyBy('id'), - 'server_variables' => $server->variables->mapWithKeys(function ($item) { - return ['env_' . $item->variable_id => [ - 'value' => $item->variable_value, - ]]; - })->toArray(), + 'server_variables' => $parameters->data, ]); return view('admin.servers.view.startup', [ - 'server' => $server, + 'server' => $parameters->server, 'services' => $services, ]); } @@ -282,10 +279,10 @@ class ServersController extends Controller */ public function viewDatabase(Request $request, $id) { - $server = Models\Server::where('installed', 1)->with('databases.host')->findOrFail($id); + $server = $this->repository->getWithDatabases($id); return view('admin.servers.view.database', [ - 'hosts' => Models\DatabaseHost::all(), + 'hosts' => $this->databaseHostRepository->all(), 'server' => $server, ]); } @@ -299,7 +296,7 @@ class ServersController extends Controller */ public function viewManage(Request $request, $id) { - return view('admin.servers.view.manage', ['server' => Models\Server::findOrFail($id)]); + return view('admin.servers.view.manage', ['server' => $this->repository->find($id)]); } /** @@ -311,7 +308,7 @@ class ServersController extends Controller */ public function viewDelete(Request $request, $id) { - return view('admin.servers.view.delete', ['server' => Models\Server::findOrFail($id)]); + return view('admin.servers.view.delete', ['server' => $this->repository->find($id)]); } /** diff --git a/app/Http/Requests/Admin/ServerFormRequest.php b/app/Http/Requests/Admin/ServerFormRequest.php index 1efe00ac..569de168 100644 --- a/app/Http/Requests/Admin/ServerFormRequest.php +++ b/app/Http/Requests/Admin/ServerFormRequest.php @@ -78,6 +78,15 @@ class ServerFormRequest extends AdminFormRequest ], function ($input) { return ! ($input->auto_deploy); }); + + if ($this->input('pack_id') !== 0) { + $validator->sometimes('pack_id', [ + Rule::exists('packs', 'id')->where(function ($query) { + $query->where('selectable', 1); + $query->where('option_id', $this->input('option_id')); + }), + ]); + } }); } } diff --git a/app/Repositories/Daemon/ServerRepository.php b/app/Repositories/Daemon/ServerRepository.php index 2ee01183..476f927b 100644 --- a/app/Repositories/Daemon/ServerRepository.php +++ b/app/Repositories/Daemon/ServerRepository.php @@ -79,8 +79,8 @@ class ServerRepository extends BaseRepository implements ServerRepositoryInterfa array_set($data, $key, $value); } -// $this->getHttpClient()->request('POST', '/servers', [ -// 'json' => $data, -// ]); + return $this->getHttpClient()->request('POST', '/servers', [ + 'json' => $data, + ]); } } diff --git a/app/Repositories/Eloquent/AllocationRepository.php b/app/Repositories/Eloquent/AllocationRepository.php index 21dc85ee..8903fd9a 100644 --- a/app/Repositories/Eloquent/AllocationRepository.php +++ b/app/Repositories/Eloquent/AllocationRepository.php @@ -44,4 +44,12 @@ class AllocationRepository extends EloquentRepository implements AllocationRepos { return $this->getBuilder()->whereIn('id', $ids)->update(['server_id' => $server]); } + + /** + * {@inheritdoc} + */ + public function getAllocationsForNode($node) + { + return $this->getBuilder()->where('node_id', $node)->get(); + } } diff --git a/app/Repositories/Eloquent/NodeRepository.php b/app/Repositories/Eloquent/NodeRepository.php index cc2e5303..7a53ddac 100644 --- a/app/Repositories/Eloquent/NodeRepository.php +++ b/app/Repositories/Eloquent/NodeRepository.php @@ -37,4 +37,31 @@ class NodeRepository extends SearchableRepository implements NodeRepositoryInter { return Node::class; } + + /** + * {@inheritdoc} + */ + public function getNodesForLocation($location) + { + $instance = $this->getBuilder()->with('allocations')->where('location_id', $location)->get(); + + return $instance->map(function ($item) { + $filtered = $item->allocations->where('server_id', null)->map(function ($map) { + return collect($map)->only(['id', 'ip', 'port']); + }); + + $item->ports = $filtered->map(function ($map) { + return [ + 'id' => $map['id'], + 'text' => sprintf('%s:%s', $map['ip'], $map['port']), + ]; + })->values(); + + return [ + 'id' => $item->id, + 'text' => $item->name, + 'allocations' => $item->ports, + ]; + })->values(); + } } diff --git a/app/Repositories/Eloquent/ServerRepository.php b/app/Repositories/Eloquent/ServerRepository.php index 2221ceb0..073950e2 100644 --- a/app/Repositories/Eloquent/ServerRepository.php +++ b/app/Repositories/Eloquent/ServerRepository.php @@ -24,8 +24,8 @@ namespace Pterodactyl\Repositories\Eloquent; -use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Models\Server; +use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Repositories\Eloquent\Attributes\SearchableRepository; @@ -60,8 +60,8 @@ class ServerRepository extends SearchableRepository implements ServerRepositoryI public function findWithVariables($id) { $instance = $this->getBuilder()->with('option.variables', 'variables') - ->where($this->getModel()->getKeyName(), '=', $id) - ->first($this->getColumns()); + ->where($this->getModel()->getKeyName(), '=', $id) + ->first($this->getColumns()); if (is_null($instance)) { throw new RecordNotFoundException(); @@ -73,10 +73,10 @@ class ServerRepository extends SearchableRepository implements ServerRepositoryI /** * {@inheritdoc} */ - public function getVariablesWithValues($id) + public function getVariablesWithValues($id, $returnWithObject = false) { $instance = $this->getBuilder()->with('variables', 'option.variables') - ->find($id, $this->getColumns()); + ->find($id, $this->getColumns()); if (! $instance) { throw new RecordNotFoundException(); @@ -89,13 +89,39 @@ class ServerRepository extends SearchableRepository implements ServerRepositoryI $data[$item->env_variable] = $display ?? $item->default_value; }); + if ($returnWithObject) { + return (object) [ + 'data' => $data, + 'server' => $instance, + ]; + } + return $data; } + /** + * {@inheritdoc} + */ public function getDataForCreation($id) { $instance = $this->getBuilder()->with('allocation', 'allocations', 'pack', 'option.service') - ->find($id, $this->getColumns()); + ->find($id, $this->getColumns()); + + if (! $instance) { + throw new RecordNotFoundException(); + } + + return $instance; + } + + /** + * {@inheritdoc} + */ + public function getWithDatabases($id) + { + $instance = $this->getBuilder()->with('databases.host') + ->where('installed', 1) + ->find($id, $this->getColumns()); if (! $instance) { throw new RecordNotFoundException(); diff --git a/app/Services/Database/CreationService.php b/app/Services/Database/CreationService.php index b4887453..b1f480af 100644 --- a/app/Services/Database/CreationService.php +++ b/app/Services/Database/CreationService.php @@ -118,7 +118,7 @@ class CreationService $this->repository->dropUser($database->username, $database->remote, 'dynamic'); $this->repository->flush('dynamic'); } - } catch (\Exception $ex) { + } catch (\Exception $exTwo) { // ignore an exception } @@ -153,7 +153,7 @@ class CreationService ]); $this->repository->dropUser($database->username, $database->remote, 'dynamic'); - $this->repository->createUser($database->username, $database->remote, $password); + $this->repository->createUser($database->username, $database->remote, $password, 'dynamic'); $this->repository->assignUserToDatabase( $database->database, $database->username, $database->remote, 'dynamic' ); diff --git a/app/Services/LocationService.php b/app/Services/LocationService.php index 2bf7a41e..98253452 100644 --- a/app/Services/LocationService.php +++ b/app/Services/LocationService.php @@ -73,7 +73,7 @@ class LocationService /** * Delete a model from the DB. * - * @param int $id + * @param int $id * @return bool * * @throws \Pterodactyl\Exceptions\DisplayException diff --git a/app/Services/Servers/ServerService.php b/app/Services/Servers/CreationService.php similarity index 76% rename from app/Services/Servers/ServerService.php rename to app/Services/Servers/CreationService.php index dd7ba913..9073dfb1 100644 --- a/app/Services/Servers/ServerService.php +++ b/app/Services/Servers/CreationService.php @@ -33,34 +33,66 @@ use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; -class ServerService +class CreationService { /** * @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface */ protected $allocationRepository; + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface + */ + protected $daemonServerRepository; + + /** + * @var \Illuminate\Database\ConnectionInterface + */ + protected $database; + /** * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface */ protected $nodeRepository; + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface + */ + protected $serverVariableRepository; + /** * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface */ protected $userRepository; - protected $database; - protected $repository; + /** + * @var \Pterodactyl\Services\Servers\UsernameGenerationService + */ protected $usernameService; - protected $serverVariableRepository; - protected $daemonServerRepository; /** * @var \Pterodactyl\Services\Servers\VariableValidatorService */ protected $validatorService; + /** + * CreationService constructor. + * + * @param \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface $allocationRepository + * @param \Illuminate\Database\ConnectionInterface $database + * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository + * @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonServerRepository + * @param \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface $serverVariableRepository + * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $nodeRepository + * @param \Pterodactyl\Services\Servers\UsernameGenerationService $usernameService + * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $userRepository + * @param \Pterodactyl\Services\Servers\VariableValidatorService $validatorService + */ public function __construct( AllocationRepositoryInterface $allocationRepository, ConnectionInterface $database, @@ -83,9 +115,17 @@ class ServerService $this->daemonServerRepository = $daemonServerRepository; } + /** + * Create a server on both the panel and daemon. + * + * @param array $data + * @return mixed + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ public function create(array $data) { - // @todo auto-deployment and packs + // @todo auto-deployment $data['user_id'] = 1; $node = $this->nodeRepository->find($data['node_id']); @@ -141,8 +181,11 @@ class ServerService $this->serverVariableRepository->insert($records); // Create the server on the daemon & commit it to the database. - $this->daemonServerRepository->setNode($server->node_id)->setAccessToken($node->daemonSecret)->create($server->id); - $this->database->rollBack(); + $this->daemonServerRepository->setNode($server->node_id) + ->setAccessToken($node->daemonSecret) + ->create($server->id); + + $this->database->commit(); return $server; } diff --git a/app/Services/Servers/EnvironmentService.php b/app/Services/Servers/EnvironmentService.php index 0bdb5131..94920534 100644 --- a/app/Services/Servers/EnvironmentService.php +++ b/app/Services/Servers/EnvironmentService.php @@ -24,8 +24,8 @@ namespace Pterodactyl\Services\Servers; -use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Models\Server; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; class EnvironmentService { diff --git a/app/Services/Servers/VariableValidatorService.php b/app/Services/Servers/VariableValidatorService.php index 9337f38c..1ff3b9cb 100644 --- a/app/Services/Servers/VariableValidatorService.php +++ b/app/Services/Servers/VariableValidatorService.php @@ -143,11 +143,11 @@ class VariableValidatorService if ($validator->fails()) { throw new DisplayValidationException(json_encode( - collect([ - 'notice' => [ - sprintf('There was a validation error with the %s variable.', $item->name), - ], - ])->merge($validator->errors()->toArray()) + collect([ + 'notice' => [ + sprintf('There was a validation error with the %s variable.', $item->name), + ], + ])->merge($validator->errors()->toArray()) )); } diff --git a/resources/themes/pterodactyl/admin/servers/view/startup.blade.php b/resources/themes/pterodactyl/admin/servers/view/startup.blade.php index c83b48cb..175bbeff 100644 --- a/resources/themes/pterodactyl/admin/servers/view/startup.blade.php +++ b/resources/themes/pterodactyl/admin/servers/view/startup.blade.php @@ -97,7 +97,7 @@ @foreach($services as $service) @endforeach @@ -125,30 +125,7 @@
-
- @foreach($server->option->variables as $variable) -
-
-
-

{{ $variable->name }}

-
-
- -

{{ $variable->description }}

-

- @if($variable->required)Required@elseOptional@endif - @if($variable->user_viewable)Visible@elseHidden@endif - @if($variable->user_editable)Editable@elseLocked@endif -

-
- -
-
- @endforeach -
+
@@ -217,7 +194,7 @@ $('#appendVariablesTo').html(''); $.each(_.get(objectChain, 'variables', []), function (i, item) { - var setValue = _.get(Pterodactyl.server_variables, 'env_' + item.id + '.value', item.default_value); + var setValue = _.get(Pterodactyl.server_variables, item.env_variable, item.default_value); var isRequired = (item.required === 1) ? 'Required ' : ''; var dataAppend = ' \
\ @@ -226,7 +203,7 @@

' + isRequired + item.name + '

\
\
\ - \ + \

' + item.description + '

\
\ diff --git a/tests/Unit/Services/Database/CreationServiceTest.php b/tests/Unit/Services/Database/CreationServiceTest.php index 777dbb43..331d55b9 100644 --- a/tests/Unit/Services/Database/CreationServiceTest.php +++ b/tests/Unit/Services/Database/CreationServiceTest.php @@ -25,13 +25,14 @@ namespace Tests\Unit\Services\Database; use Exception; +use Illuminate\Database\DatabaseManager; use Mockery as m; use Tests\TestCase; use phpmock\phpunit\PHPMock; -use Illuminate\Database\ConnectionResolver; use Illuminate\Database\ConnectionInterface; use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Services\Database\CreationService; +use Illuminate\Database\ConnectionResolver; use Pterodactyl\Extensions\DynamicDatabaseConnection; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; @@ -49,12 +50,7 @@ class CreationServiceTest extends TestCase ]; /** - * @var \Illuminate\Database\ConnectionResolver - */ - protected $connection; - - /** - * @var \Illuminate\Database\ConnectionInterface + * @var \Illuminate\Database\DatabaseManager */ protected $database; @@ -85,8 +81,7 @@ class CreationServiceTest extends TestCase { parent::setUp(); - $this->connection = m::mock(ConnectionResolver::class); - $this->database = m::mock(ConnectionInterface::class); + $this->database = m::mock(DatabaseManager::class); $this->dynamic = m::mock(DynamicDatabaseConnection::class); $this->encrypter = m::mock(Encrypter::class); $this->repository = m::mock(DatabaseRepositoryInterface::class); @@ -96,7 +91,6 @@ class CreationServiceTest extends TestCase $this->service = new CreationService( $this->database, - $this->connection, $this->dynamic, $this->repository, $this->encrypter @@ -110,10 +104,8 @@ class CreationServiceTest extends TestCase { $this->encrypter->shouldReceive('encrypt')->with('str_random')->once()->andReturn('enc_password'); $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('createIfNotExists')->with( - collect(self::TEST_DATA)->except('remote')->toArray() - )->once()->andReturn((object) self::TEST_DATA); + $this->repository->shouldReceive('createIfNotExists')->with(self::TEST_DATA)->once()->andReturn((object) self::TEST_DATA); $this->dynamic->shouldReceive('set')->with('dynamic', self::TEST_DATA['database_host_id'])->once()->andReturnNull(); $this->repository->shouldReceive('createDatabase')->with( self::TEST_DATA['database'], 'dynamic' @@ -131,16 +123,15 @@ class CreationServiceTest extends TestCase $this->repository->shouldReceive('flush')->with('dynamic')->once()->andReturnNull(); $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); - $response = $this->service->create([ - 'server_id' => 1, + $response = $this->service->create(1, [ 'database' => 'dbname', + 'remote' => '%', 'database_host_id' => 3, ]); $this->assertNotEmpty($response); $this->assertTrue(is_object($response), 'Assert that response is an object.'); - $this->assertEquals(self::TEST_DATA['server_id'], $response->server_id); $this->assertEquals(self::TEST_DATA['database'], $response->database); $this->assertEquals(self::TEST_DATA['remote'], $response->remote); $this->assertEquals(self::TEST_DATA['username'], $response->username); @@ -157,16 +148,13 @@ class CreationServiceTest extends TestCase { $this->encrypter->shouldReceive('encrypt')->with('str_random')->once()->andReturn('enc_password'); $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('createIfNotExists')->with( - collect(self::TEST_DATA)->except('remote')->toArray() - )->once()->andThrow(new Exception('Test Message')); - + $this->repository->shouldReceive('createIfNotExists')->with(self::TEST_DATA)->once()->andThrow(new Exception('Test Message')); $this->repository->shouldNotReceive('dropDatabase'); $this->database->shouldReceive('rollBack')->withNoArgs()->once()->andReturnNull(); - $this->service->create([ - 'server_id' => 1, + $this->service->create(1, [ 'database' => 'dbname', + 'remote' => '%', 'database_host_id' => 3, ]); } @@ -180,10 +168,7 @@ class CreationServiceTest extends TestCase { $this->encrypter->shouldReceive('encrypt')->with('str_random')->once()->andReturn('enc_password'); $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('createIfNotExists')->with( - collect(self::TEST_DATA)->except('remote')->toArray() - )->once()->andReturn((object) self::TEST_DATA); - + $this->repository->shouldReceive('createIfNotExists')->with(self::TEST_DATA)->once()->andReturn((object) self::TEST_DATA); $this->dynamic->shouldReceive('set')->with('dynamic', self::TEST_DATA['database_host_id'])->once()->andReturnNull(); $this->repository->shouldReceive('createDatabase')->with( self::TEST_DATA['database'], 'dynamic' @@ -197,9 +182,9 @@ class CreationServiceTest extends TestCase $this->database->shouldReceive('rollBack')->withNoArgs()->once()->andReturnNull(); - $this->service->create([ - 'server_id' => 1, + $this->service->create(1, [ 'database' => 'dbname', + 'remote' => '%', 'database_host_id' => 3, ]); } @@ -211,10 +196,7 @@ class CreationServiceTest extends TestCase { $this->encrypter->shouldReceive('encrypt')->with('str_random')->once()->andReturn('enc_password'); $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('createIfNotExists')->with( - collect(self::TEST_DATA)->except('remote')->toArray() - )->once()->andReturn((object) self::TEST_DATA); - + $this->repository->shouldReceive('createIfNotExists')->with(self::TEST_DATA)->once()->andReturn((object) self::TEST_DATA); $this->dynamic->shouldReceive('set')->with('dynamic', self::TEST_DATA['database_host_id'])->once()->andReturnNull(); $this->repository->shouldReceive('createDatabase')->with( self::TEST_DATA['database'], 'dynamic' @@ -226,9 +208,9 @@ class CreationServiceTest extends TestCase $this->database->shouldReceive('rollBack')->withNoArgs()->once()->andReturnNull(); try { - $this->service->create([ - 'server_id' => 1, + $this->service->create(1, [ 'database' => 'dbname', + 'remote' => '%', 'database_host_id' => 3, ]); } catch (Exception $ex) { diff --git a/tests/Unit/Services/Database/DatabaseHostServiceTest.php b/tests/Unit/Services/Database/DatabaseHostServiceTest.php index e728d467..5140dfea 100644 --- a/tests/Unit/Services/Database/DatabaseHostServiceTest.php +++ b/tests/Unit/Services/Database/DatabaseHostServiceTest.php @@ -24,9 +24,11 @@ namespace Tests\Unit\Services\Administrative; +use Illuminate\Database\ConnectionInterface; +use Illuminate\Database\ConnectionResolver; +use Illuminate\Database\DatabaseManager; use Mockery as m; use Tests\TestCase; -use Illuminate\Database\DatabaseManager; use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Extensions\DynamicDatabaseConnection; use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; @@ -72,8 +74,8 @@ class DatabaseHostServiceTest extends TestCase $this->repository = m::mock(DatabaseHostRepositoryInterface::class); $this->service = new DatabaseHostService( - $this->repository, $this->database, + $this->repository, $this->dynamic, $this->encrypter ); From 3add44d342c09d99de56cad8b42e083658386e0b Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 22 Jul 2017 14:07:51 -0500 Subject: [PATCH 29/99] Fix database management for servers --- .../Controllers/Admin/ServersController.php | 132 ++++++++---------- app/Services/Database/CreationService.php | 2 +- .../Services/Database/CreationServiceTest.php | 2 +- 3 files changed, 62 insertions(+), 74 deletions(-) diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php index 725eff0b..35587cd2 100644 --- a/app/Http/Controllers/Admin/ServersController.php +++ b/app/Http/Controllers/Admin/ServersController.php @@ -186,7 +186,7 @@ class ServersController extends Controller /** * Returns a tree of all avaliable nodes in a given location. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return \Illuminate\Support\Collection */ public function nodes(Request $request) @@ -197,8 +197,8 @@ class ServersController extends Controller /** * Display the index when viewing a specific server. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param int $id * @return \Illuminate\View\View */ public function viewIndex(Request $request, $id) @@ -209,8 +209,8 @@ class ServersController extends Controller /** * Display the details page when viewing a specific server. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param int $id * @return \Illuminate\View\View */ public function viewDetails(Request $request, $id) @@ -226,8 +226,8 @@ class ServersController extends Controller /** * Display the build details page when viewing a specific server. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param int $id * @return \Illuminate\View\View */ public function viewBuild(Request $request, $id) @@ -249,8 +249,8 @@ class ServersController extends Controller /** * Display startup configuration page for a server. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param int $id * @return \Illuminate\View\View */ public function viewStartup(Request $request, $id) @@ -297,8 +297,8 @@ class ServersController extends Controller /** * Display the management page when viewing a specific server. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param int $id * @return \Illuminate\View\View */ public function viewManage(Request $request, $id) @@ -309,8 +309,8 @@ class ServersController extends Controller /** * Display the deletion page for a server. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param int $id * @return \Illuminate\View\View */ public function viewDelete(Request $request, $id) @@ -321,8 +321,8 @@ class ServersController extends Controller /** * Update the details for a server. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param int $id * @return \Illuminate\Http\RedirectResponse */ public function setDetails(ServerFormRequest $request, Models\Server $server) @@ -353,8 +353,8 @@ class ServersController extends Controller /** * Set the new docker container for a server. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param int $id * @return \Illuminate\Http\RedirectResponse */ public function setContainer(Request $request, $id) @@ -381,8 +381,8 @@ class ServersController extends Controller /** * Toggles the install status for a server. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param int $id * @return \Illuminate\Http\RedirectResponse */ public function toggleInstall(Request $request, $id) @@ -405,8 +405,8 @@ class ServersController extends Controller /** * Reinstalls the server with the currently assigned pack and service. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param int $id * @return \Illuminate\Http\RedirectResponse */ public function reinstallServer(Request $request, $id) @@ -429,8 +429,8 @@ class ServersController extends Controller /** * Setup a server to have a container rebuild. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param int $id * @return \Illuminate\Http\RedirectResponse */ public function rebuildContainer(Request $request, $id) @@ -455,8 +455,8 @@ class ServersController extends Controller /** * Manage the suspension status for a server. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param int $id * @return \Illuminate\Http\RedirectResponse */ public function manageSuspension(Request $request, $id) @@ -488,8 +488,8 @@ class ServersController extends Controller /** * Update the build configuration for a server. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param int $id * @return \Illuminate\Http\RedirectResponse */ public function updateBuild(Request $request, $id) @@ -521,8 +521,8 @@ class ServersController extends Controller /** * Start the server deletion process. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param int $id * @return \Illuminate\Http\RedirectResponse */ public function delete(Request $request, $id) @@ -550,8 +550,8 @@ class ServersController extends Controller /** * Update the startup command as well as variables. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param int $id * @return \Illuminate\Http\RedirectResponse */ public function saveStartup(Request $request, $id) @@ -584,9 +584,13 @@ class ServersController extends Controller /** * Creates a new database assigned to a specific server. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param int $id * @return \Illuminate\Http\RedirectResponse + * + * @throws \Exception + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ public function newDatabase(Request $request, $id) { @@ -595,20 +599,6 @@ class ServersController extends Controller 'remote' => $request->input('remote'), 'database_host_id' => $request->input('database_host_id'), ]); -// $repo = new DatabaseRepository; -// -// try { -// $repo->create($id, $request->only(['host', 'database', 'connection'])); -// -// Alert::success('A new database was assigned to this server successfully.')->flash(); -// } catch (DisplayValidationException $ex) { -// return redirect()->route('admin.servers.view.database', $id)->withInput()->withErrors(json_decode($ex->getMessage()))->withInput(); -// } catch (DisplayException $ex) { -// Alert::danger($ex->getMessage())->flash(); -// } catch (\Exception $ex) { -// Log::error($ex); -// Alert::danger('An exception occured while attempting to add a new database for this server. This error has been logged.')->flash(); -// } return redirect()->route('admin.servers.view.database', $id)->withInput(); } @@ -616,47 +606,45 @@ class ServersController extends Controller /** * Resets the database password for a specific database on this server. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param int $id * @return \Illuminate\Http\RedirectResponse + * + * @throws \Exception + * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ public function resetDatabasePassword(Request $request, $id) { - $database = Models\Database::where('server_id', $id)->findOrFail($request->input('database')); - $repo = new DatabaseRepository; + $database = $this->databaseRepository->findFirstWhere([ + ['server_id', '=', $id], + ['id', '=', $request->input('database')], + ]); - try { - $repo->password($database->id, str_random(20)); + $this->databaseCreationService->changePassword($database->id, str_random(20)); - return response('', 204); - } catch (\Exception $ex) { - Log::error($ex); - - return response()->json(['error' => 'A unhandled exception occurred while attempting to reset this password. This error has been logged.'], 503); - } + return response('', 204); } /** * Deletes a database from a server. * - * @param \Illuminate\Http\Request $request - * @param int $id - * @param int $database + * @param \Illuminate\Http\Request $request + * @param int $id + * @param int $database * @return \Illuminate\Http\RedirectResponse + * + * @throws \Exception + * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ public function deleteDatabase(Request $request, $id, $database) { - $database = Models\Database::where('server_id', $id)->findOrFail($database); - $repo = new DatabaseRepository; + $database = $this->databaseRepository->findFirstWhere([ + ['server_id', '=', $id], + ['id', '=', $database], + ]); - try { - $repo->drop($database->id); + $this->databaseCreationService->delete($database->id); - return response('', 204); - } catch (\Exception $ex) { - Log::error($ex); - - return response()->json(['error' => 'A unhandled exception occurred while attempting to drop this database. This error has been logged.'], 503); - } + return response('', 204); } } diff --git a/app/Services/Database/CreationService.php b/app/Services/Database/CreationService.php index 339a0b8f..71e58941 100644 --- a/app/Services/Database/CreationService.php +++ b/app/Services/Database/CreationService.php @@ -150,7 +150,7 @@ class CreationService $this->repository->assignUserToDatabase( $database->database, $database->username, $database->remote, 'dynamic' ); - $this->repository->flush(); + $this->repository->flush('dynamic'); $this->database->commit(); } catch (\Exception $ex) { diff --git a/tests/Unit/Services/Database/CreationServiceTest.php b/tests/Unit/Services/Database/CreationServiceTest.php index 331d55b9..c566fde6 100644 --- a/tests/Unit/Services/Database/CreationServiceTest.php +++ b/tests/Unit/Services/Database/CreationServiceTest.php @@ -246,7 +246,7 @@ class CreationServiceTest extends TestCase self::TEST_DATA['database'], self::TEST_DATA['username'], self::TEST_DATA['remote'], 'dynamic' )->once()->andReturnNull(); - $this->repository->shouldReceive('flush')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('flush')->with('dynamic')->once()->andReturnNull(); $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); $response = $this->service->changePassword(1, 'new_password'); From acbc52506c255ef60d4f84e0dcd29da7df954bd2 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 22 Jul 2017 20:15:01 -0500 Subject: [PATCH 30/99] Finish unit tests for all server services --- .../Daemon/ServerRepositoryInterface.php | 8 + app/Exceptions/Handler.php | 16 +- .../Controllers/Admin/ServersController.php | 80 ++-- app/Http/Requests/Admin/ServerFormRequest.php | 4 - app/Repositories/Daemon/BaseRepository.php | 6 +- app/Repositories/Daemon/ServerRepository.php | 10 + app/Services/Servers/CreationService.php | 37 +- .../Servers/DetailsModificationService.php | 156 +++++++ .../Servers/UsernameGenerationService.php | 2 +- .../Servers/VariableValidatorService.php | 23 +- composer.json | 7 +- composer.lock | 230 +++++----- database/factories/ModelFactory.php | 51 +++ resources/lang/en/admin/server.php | 34 ++ .../admin/servers/view/details.blade.php | 4 +- routes/admin.php | 4 +- .../Services/Servers/CreationServiceTest.php | 224 ++++++++++ .../DetailsModificationServiceTest.php | 394 ++++++++++++++++++ .../Servers/EnvironmentServiceTest.php | 155 +++++++ .../Servers/UsernameGenerationServiceTest.php | 127 ++++++ .../Servers/VariableValidatorServiceTest.php | 243 +++++++++++ 21 files changed, 1609 insertions(+), 206 deletions(-) create mode 100644 app/Services/Servers/DetailsModificationService.php create mode 100644 resources/lang/en/admin/server.php create mode 100644 tests/Unit/Services/Servers/CreationServiceTest.php create mode 100644 tests/Unit/Services/Servers/DetailsModificationServiceTest.php create mode 100644 tests/Unit/Services/Servers/EnvironmentServiceTest.php create mode 100644 tests/Unit/Services/Servers/UsernameGenerationServiceTest.php create mode 100644 tests/Unit/Services/Servers/VariableValidatorServiceTest.php diff --git a/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php b/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php index d4554cc7..a69e1bb6 100644 --- a/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php +++ b/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php @@ -35,4 +35,12 @@ interface ServerRepositoryInterface extends BaseRepositoryInterface * @return \Psr\Http\Message\ResponseInterface */ public function create($id, $overrides = [], $start = false); + + /** + * Update server details on the daemon. + * + * @param array $data + * @return \Psr\Http\Message\ResponseInterface + */ + public function update(array $data); } diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index b068f4cc..ba41dce6 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -5,6 +5,8 @@ namespace Pterodactyl\Exceptions; use Exception; use Illuminate\Auth\AuthenticationException; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; +use Prologue\Alerts\Facades\Alert; +use Pterodactyl\Exceptions\Model\DataValidationException; class Handler extends ExceptionHandler { @@ -20,7 +22,9 @@ class Handler extends ExceptionHandler \Illuminate\Database\Eloquent\ModelNotFoundException::class, \Illuminate\Session\TokenMismatchException::class, \Illuminate\Validation\ValidationException::class, - \Pterodactyl\Exceptions\Model\DataValidationException::class, + DisplayException::class, + DisplayValidationException::class, + DataValidationException::class, ]; /** @@ -51,7 +55,7 @@ class Handler extends ExceptionHandler if ($request->expectsJson() || $request->isJson() || $request->is(...config('pterodactyl.json_routes'))) { $exception = $this->prepareException($exception); - if (config('app.debug') || $this->isHttpException($exception)) { + if (config('app.debug') || $this->isHttpException($exception) || $exception instanceof DisplayException) { $displayError = $exception->getMessage(); } else { $displayError = 'An unhandled exception was encountered with this request.'; @@ -64,6 +68,10 @@ class Handler extends ExceptionHandler ], ($this->isHttpException($exception)) ? $exception->getStatusCode() : 500, [], JSON_UNESCAPED_SLASHES); parent::report($exception); + } elseif ($exception instanceof DisplayException) { + Alert::danger($exception->getMessage())->flash(); + + return redirect()->back()->withInput(); } return (isset($response)) ? $response : parent::render($request, $exception); @@ -72,8 +80,8 @@ class Handler extends ExceptionHandler /** * Convert an authentication exception into an unauthenticated response. * - * @param \Illuminate\Http\Request $request - * @param \Illuminate\Auth\AuthenticationException $exception + * @param \Illuminate\Http\Request $request + * @param \Illuminate\Auth\AuthenticationException $exception * @return \Illuminate\Http\Response */ protected function unauthenticated($request, AuthenticationException $exception) diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php index 35587cd2..16747c01 100644 --- a/app/Http/Controllers/Admin/ServersController.php +++ b/app/Http/Controllers/Admin/ServersController.php @@ -28,6 +28,7 @@ use Illuminate\Contracts\Config\Repository as ConfigRepository; use Log; use Alert; use Javascript; +use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; @@ -36,18 +37,23 @@ use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; use Pterodactyl\Http\Requests\Admin\ServerFormRequest; use Pterodactyl\Models; +use Pterodactyl\Models\Server; use Illuminate\Http\Request; use GuzzleHttp\Exception\TransferException; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Repositories\Eloquent\DatabaseHostRepository; -use Pterodactyl\Repositories\ServerRepository; -use Pterodactyl\Repositories\DatabaseRepository; use Pterodactyl\Exceptions\DisplayValidationException; use Pterodactyl\Services\Servers\CreationService; +use Pterodactyl\Services\Servers\DetailsModificationService; class ServersController extends Controller { + /** + * @var \Prologue\Alerts\AlertsMessageBag + */ + protected $alert; + /** * @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface */ @@ -73,6 +79,11 @@ class ServersController extends Controller */ protected $databaseHostRepository; + /** + * @var \Pterodactyl\Services\Servers\DetailsModificationService + */ + protected $detailsModificationService; + /** * @var \Pterodactyl\Contracts\Repository\LocationRepositoryInterface */ @@ -99,22 +110,26 @@ class ServersController extends Controller protected $serviceRepository; public function __construct( + AlertsMessageBag $alert, AllocationRepositoryInterface $allocationRepository, ConfigRepository $config, CreationService $service, \Pterodactyl\Services\Database\CreationService $databaseCreationService, DatabaseRepositoryInterface $databaseRepository, DatabaseHostRepository $databaseHostRepository, + DetailsModificationService $detailsModificationService, LocationRepositoryInterface $locationRepository, NodeRepositoryInterface $nodeRepository, ServerRepositoryInterface $repository, ServiceRepositoryInterface $serviceRepository ) { + $this->alert = $alert; $this->allocationRepository = $allocationRepository; $this->config = $config; $this->databaseCreationService = $databaseCreationService; $this->databaseRepository = $databaseRepository; $this->databaseHostRepository = $databaseHostRepository; + $this->detailsModificationService = $detailsModificationService; $this->locationRepository = $locationRepository; $this->nodeRepository = $nodeRepository; $this->repository = $repository; @@ -321,61 +336,40 @@ class ServersController extends Controller /** * Update the details for a server. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function setDetails(ServerFormRequest $request, Models\Server $server) + public function setDetails(Request $request, Server $server) { - dd($server); - $repo = new ServerRepository; - try { - $repo->updateDetails($id, array_merge( - $request->only('description'), - $request->intersect([ - 'owner_id', 'name', 'reset_token', - ]) - )); + $this->detailsModificationService->edit($server, $request->only([ + 'owner_id', 'name', 'description', 'reset_token', + ])); - Alert::success('Server details were successfully updated.')->flash(); - } catch (DisplayValidationException $ex) { - return redirect()->route('admin.servers.view.details', $id)->withErrors(json_decode($ex->getMessage()))->withInput(); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An unhandled exception occured while attemping to update this server. This error has been logged.')->flash(); - } + $this->alert->success(trans('admin/server.alerts.details_updated'))->flash(); - return redirect()->route('admin.servers.view.details', $id)->withInput(); + return redirect()->route('admin.servers.view.details', $server->id); } /** * Set the new docker container for a server. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function setContainer(Request $request, $id) + public function setContainer(Request $request, Server $server) { - $repo = new ServerRepository; + $this->detailsModificationService->setDockerImage($server, $request->input('docker_image')); + $this->alert->success(trans('admin/server.alerts.docker_image_updated'))->flash(); - try { - $repo->updateContainer($id, $request->intersect('docker_image')); - - Alert::success('Successfully updated this server\'s docker image.')->flash(); - } catch (DisplayValidationException $ex) { - return redirect()->route('admin.servers.view.details', $id)->withErrors(json_decode($ex->getMessage()))->withInput(); - } catch (TransferException $ex) { - Log::warning($ex); - Alert::danger('A TransferException occured while attempting to update the container image. Is the daemon online? This error has been logged.'); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An unhandled exception occured while attemping to update this server\'s docker image. This error has been logged.')->flash(); - } - - return redirect()->route('admin.servers.view.details', $id); + return redirect()->route('admin.servers.view.details', $server->id); } /** diff --git a/app/Http/Requests/Admin/ServerFormRequest.php b/app/Http/Requests/Admin/ServerFormRequest.php index 569de168..19445b58 100644 --- a/app/Http/Requests/Admin/ServerFormRequest.php +++ b/app/Http/Requests/Admin/ServerFormRequest.php @@ -36,10 +36,6 @@ class ServerFormRequest extends AdminFormRequest */ public function rules() { - if ($this->method() === 'PATCH') { - return Server::getUpdateRulesForId($this->id); - } - return Server::getCreateRules(); } diff --git a/app/Repositories/Daemon/BaseRepository.php b/app/Repositories/Daemon/BaseRepository.php index 5f6f92b6..c56b2e42 100644 --- a/app/Repositories/Daemon/BaseRepository.php +++ b/app/Repositories/Daemon/BaseRepository.php @@ -88,11 +88,13 @@ class BaseRepository implements BaseRepositoryInterface public function getHttpClient($headers = []) { if (! is_null($this->accessServer)) { - $headers[] = ['X-Access-Server' => $this->getAccessServer()]; + $headers['X-Access-Server'] = $this->getAccessServer(); } if (! is_null($this->accessToken)) { - $headers[] = ['X-Access-Token' => $this->getAccessToken()]; + $headers['X-Access-Token'] = $this->getAccessToken(); + } elseif (! is_null($this->node)) { + $headers['X-Access-Token'] = $this->getNode()->daemonSecret; } return new Client([ diff --git a/app/Repositories/Daemon/ServerRepository.php b/app/Repositories/Daemon/ServerRepository.php index 476f927b..f398abfa 100644 --- a/app/Repositories/Daemon/ServerRepository.php +++ b/app/Repositories/Daemon/ServerRepository.php @@ -83,4 +83,14 @@ class ServerRepository extends BaseRepository implements ServerRepositoryInterfa 'json' => $data, ]); } + + /** + * {@inheritdoc} + */ + public function update(array $data) + { + return $this->getHttpClient()->request('PATCH', '/server', [ + 'json' => $data, + ]); + } } diff --git a/app/Services/Servers/CreationService.php b/app/Services/Servers/CreationService.php index 9073dfb1..290f68a8 100644 --- a/app/Services/Servers/CreationService.php +++ b/app/Services/Servers/CreationService.php @@ -25,7 +25,7 @@ namespace Pterodactyl\Services\Servers; use Ramsey\Uuid\Uuid; -use Illuminate\Database\ConnectionInterface; +use Illuminate\Database\DatabaseManager; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; @@ -46,7 +46,7 @@ class CreationService protected $daemonServerRepository; /** - * @var \Illuminate\Database\ConnectionInterface + * @var \Illuminate\Database\DatabaseManager */ protected $database; @@ -84,35 +84,35 @@ class CreationService * CreationService constructor. * * @param \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface $allocationRepository - * @param \Illuminate\Database\ConnectionInterface $database - * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository * @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonServerRepository - * @param \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface $serverVariableRepository + * @param \Illuminate\Database\DatabaseManager $database * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $nodeRepository - * @param \Pterodactyl\Services\Servers\UsernameGenerationService $usernameService + * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository + * @param \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface $serverVariableRepository * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $userRepository + * @param \Pterodactyl\Services\Servers\UsernameGenerationService $usernameService * @param \Pterodactyl\Services\Servers\VariableValidatorService $validatorService */ public function __construct( AllocationRepositoryInterface $allocationRepository, - ConnectionInterface $database, - ServerRepositoryInterface $repository, DaemonServerRepositoryInterface $daemonServerRepository, - ServerVariableRepositoryInterface $serverVariableRepository, + DatabaseManager $database, NodeRepositoryInterface $nodeRepository, - UsernameGenerationService $usernameService, + ServerRepositoryInterface $repository, + ServerVariableRepositoryInterface $serverVariableRepository, UserRepositoryInterface $userRepository, + UsernameGenerationService $usernameService, VariableValidatorService $validatorService ) { $this->allocationRepository = $allocationRepository; + $this->daemonServerRepository = $daemonServerRepository; $this->database = $database; - $this->repository = $repository; $this->nodeRepository = $nodeRepository; + $this->repository = $repository; + $this->serverVariableRepository = $serverVariableRepository; $this->userRepository = $userRepository; $this->usernameService = $usernameService; $this->validatorService = $validatorService; - $this->serverVariableRepository = $serverVariableRepository; - $this->daemonServerRepository = $daemonServerRepository; } /** @@ -126,9 +126,6 @@ class CreationService public function create(array $data) { // @todo auto-deployment - $data['user_id'] = 1; - - $node = $this->nodeRepository->find($data['node_id']); $validator = $this->validatorService->setAdmin()->setFields($data['environment'])->validate($data['option_id']); $uniqueShort = bin2hex(random_bytes(4)); @@ -136,7 +133,7 @@ class CreationService $server = $this->repository->create([ 'uuid' => Uuid::uuid4()->toString(), - 'uuidShort' => bin2hex(random_bytes(4)), + 'uuidShort' => $uniqueShort, 'node_id' => $data['node_id'], 'name' => $data['name'], 'description' => $data['description'], @@ -152,7 +149,7 @@ class CreationService 'allocation_id' => $data['allocation_id'], 'service_id' => $data['service_id'], 'option_id' => $data['option_id'], - 'pack_id' => ($data['pack_id'] == 0) ? null : $data['pack_id'], + 'pack_id' => (! isset($data['pack_id']) || $data['pack_id'] == 0) ? null : $data['pack_id'], 'startup' => $data['startup'], 'daemonSecret' => bin2hex(random_bytes(18)), 'image' => $data['docker_image'], @@ -181,9 +178,7 @@ class CreationService $this->serverVariableRepository->insert($records); // Create the server on the daemon & commit it to the database. - $this->daemonServerRepository->setNode($server->node_id) - ->setAccessToken($node->daemonSecret) - ->create($server->id); + $this->daemonServerRepository->setNode($server->node_id)->create($server->id); $this->database->commit(); diff --git a/app/Services/Servers/DetailsModificationService.php b/app/Services/Servers/DetailsModificationService.php new file mode 100644 index 00000000..f1fb3d27 --- /dev/null +++ b/app/Services/Servers/DetailsModificationService.php @@ -0,0 +1,156 @@ +. + * + * 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\Servers; + +use GuzzleHttp\Exception\RequestException; +use Illuminate\Database\DatabaseManager; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Models\Server; +use Pterodactyl\Repositories\Eloquent\ServerRepository; +use Pterodactyl\Repositories\Daemon\ServerRepository as DaemonServerRepository; + +class DetailsModificationService +{ + /** + * @var \Illuminate\Database\DatabaseManager + */ + protected $database; + + /** + * @var \Pterodactyl\Repositories\Daemon\ServerRepository + */ + protected $daemonServerRepository; + + /** + * @var \Pterodactyl\Repositories\Eloquent\ServerRepository + */ + protected $repository; + + /** + * DetailsModificationService constructor. + * + * @param \Illuminate\Database\DatabaseManager $database + * @param \Pterodactyl\Repositories\Daemon\ServerRepository $daemonServerRepository + * @param \Pterodactyl\Repositories\Eloquent\ServerRepository $repository + */ + public function __construct( + DatabaseManager $database, + DaemonServerRepository $daemonServerRepository, + ServerRepository $repository + ) { + $this->database = $database; + $this->daemonServerRepository = $daemonServerRepository; + $this->repository = $repository; + } + + /** + * Update the details for a single server instance. + * + * @param int|\Pterodactyl\Models\Server $server + * @param array $data + * @return bool + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function edit($server, array $data) + { + if (! $server instanceof Server) { + $server = $this->repository->find($server); + } + + $this->database->beginTransaction(); + $currentSecret = $server->daemonSecret; + + if ( + (isset($data['reset_token']) && ! is_null($data['reset_token'])) || + (isset($data['owner_id']) && $data['owner_id'] != $server->owner_id) + ) { + $data['daemonSecret'] = bin2hex(random_bytes(18)); + $shouldUpdate = true; + } + + $this->repository->withoutFresh()->update($server->id, [ + 'owner_id' => array_get($data, 'owner_id') ?? $server->owner_id, + 'name' => array_get($data, 'name') ?? $server->name, + 'description' => array_get($data, 'description') ?? $server->description, + 'daemonSecret' => array_get($data, 'daemonSecret') ?? $server->daemonSecret, + ], true, true); + + // If there are no updates, lets save the changes and return. + if (! isset($shouldUpdate)) { + return $this->database->commit(); + } + + try { + $this->daemonServerRepository->setNode($server->node_id)->setAccessServer($server->uuid)->update([ + 'keys' => [ + (string) $currentSecret => [], + (string) $data['daemonSecret'] => $this->daemonServerRepository::DAEMON_PERMISSIONS, + ], + ]); + + return $this->database->commit(); + } catch (RequestException $exception) { + throw new DisplayException(trans('admin/server.exceptions.daemon_exception', [ + 'code' => $exception->getResponse()->getStatusCode(), + ])); + } + } + + /** + * Update the docker container for a specified server. + * + * @param int|\Pterodactyl\Models\Server $server + * @param string $image + * @return bool + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function setDockerImage($server, $image) + { + if (! $server instanceof Server) { + $server = $this->repository->find($server); + } + + $this->database->beginTransaction(); + $this->repository->withoutFresh()->update($server->id, ['image' => $image]); + + try { + $this->daemonServerRepository->setNode($server->node_id)->setAccessServer($server->uuid)->update([ + 'build' => [ + 'image' => $image, + ], + ]); + + return $this->database->commit(); + } catch (RequestException $exception) { + throw new DisplayException(trans('admin/server.exceptions.daemon_exception', [ + 'code' => $exception->getResponse()->getStatusCode(), + ])); + } + } +} diff --git a/app/Services/Servers/UsernameGenerationService.php b/app/Services/Servers/UsernameGenerationService.php index 10e3382f..4c356924 100644 --- a/app/Services/Servers/UsernameGenerationService.php +++ b/app/Services/Servers/UsernameGenerationService.php @@ -47,7 +47,7 @@ class UsernameGenerationService } // Filter the Server Name - $name = trim(preg_replace('/[^\w]+/', '', $name), '_'); + $name = trim(preg_replace('/[^A-Za-z0-9]+/', '', $name), '_'); $name = (strlen($name) < 1) ? str_random(6) : $name; return strtolower(substr($name, 0, 6) . '_' . $unique); diff --git a/app/Services/Servers/VariableValidatorService.php b/app/Services/Servers/VariableValidatorService.php index 1ff3b9cb..3be00494 100644 --- a/app/Services/Servers/VariableValidatorService.php +++ b/app/Services/Servers/VariableValidatorService.php @@ -27,9 +27,8 @@ namespace Pterodactyl\Services\Servers; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; use Pterodactyl\Exceptions\DisplayValidationException; -use Illuminate\Validation\Factory as ValidationFactory; +use Illuminate\Contracts\Validation\Factory as ValidationFactory; use Pterodactyl\Contracts\Repository\OptionVariableRepositoryInterface; -use Pterodactyl\Exceptions\Services\Servers\RequiredVariableMissingException; class VariableValidatorService { @@ -64,10 +63,18 @@ class VariableValidatorService protected $serverVariableRepository; /** - * @var \Illuminate\Validation\Factory + * @var \Illuminate\Contracts\Validation\Factory */ protected $validator; + /** + * VariableValidatorService constructor. + * + * @param \Pterodactyl\Contracts\Repository\OptionVariableRepositoryInterface $optionVariableRepository + * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $serverRepository + * @param \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface $serverVariableRepository + * @param \Illuminate\Contracts\Validation\Factory $validator + */ public function __construct( OptionVariableRepositoryInterface $optionVariableRepository, ServerRepositoryInterface $serverRepository, @@ -121,14 +128,6 @@ class VariableValidatorService } $variables->each(function ($item) { - if (! isset($this->fields[$item->env_variable]) && $item->required) { - if ($item->required) { - throw new RequiredVariableMissingException( - sprintf('Required service option variable %s was missing from this request.', $item->env_variable) - ); - } - } - // Skip doing anything if user is not an admin and variable is not user viewable // or editable. if (! $this->isAdmin && (! $item->user_editable || ! $item->user_viewable)) { @@ -145,7 +144,7 @@ class VariableValidatorService throw new DisplayValidationException(json_encode( collect([ 'notice' => [ - sprintf('There was a validation error with the %s variable.', $item->name), + trans('admin/server.exceptions.bad_variable', ['name' => $item->name]), ], ])->merge($validator->errors()->toArray()) )); diff --git a/composer.json b/composer.json index bd8f49da..34de41f7 100644 --- a/composer.json +++ b/composer.json @@ -12,14 +12,14 @@ ], "require": { "php": ">=7.0.0", + "ext-mbstring": "*", + "ext-pdo_mysql": "*", + "ext-zip": "*", "aws/aws-sdk-php": "3.29.7", "barryvdh/laravel-debugbar": "2.4.0", "daneeveritt/login-notifications": "1.0.0", "doctrine/dbal": "2.5.12", "edvinaskrucas/settings": "2.0.0", - "ext-mbstring": "*", - "ext-zip": "*", - "ext-pdo_mysql": "*", "fideloper/proxy": "3.3.3", "guzzlehttp/guzzle": "6.2.3", "igaster/laravel-theme": "1.16.0", @@ -33,6 +33,7 @@ "pragmarx/google2fa": "1.0.1", "predis/predis": "1.1.1", "prologue/alerts": "0.4.1", + "ramsey/uuid": "3.6.1", "s1lentium/iptools": "1.1.0", "sofa/eloquence": "5.4.1", "spatie/laravel-fractal": "4.0.1", diff --git a/composer.lock b/composer.lock index a6afca00..d6b01a57 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "f1afab5cf73088c6034bfb2b13631600", + "content-hash": "48a6ed67ba0a480511075590af7f8eba", "packages": [ { "name": "aws/aws-sdk-php", @@ -282,21 +282,21 @@ }, { "name": "doctrine/annotations", - "version": "v1.4.0", + "version": "v1.5.0", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "54cacc9b81758b14e3ce750f205a393d52339e97" + "reference": "5beebb01b025c94e93686b7a0ed3edae81fe3e7f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/54cacc9b81758b14e3ce750f205a393d52339e97", - "reference": "54cacc9b81758b14e3ce750f205a393d52339e97", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/5beebb01b025c94e93686b7a0ed3edae81fe3e7f", + "reference": "5beebb01b025c94e93686b7a0ed3edae81fe3e7f", "shasum": "" }, "require": { "doctrine/lexer": "1.*", - "php": "^5.6 || ^7.0" + "php": "^7.1" }, "require-dev": { "doctrine/cache": "1.*", @@ -305,7 +305,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4.x-dev" + "dev-master": "1.5.x-dev" } }, "autoload": { @@ -346,37 +346,41 @@ "docblock", "parser" ], - "time": "2017-02-24T16:22:25+00:00" + "time": "2017-07-22T10:58:02+00:00" }, { "name": "doctrine/cache", - "version": "v1.6.1", + "version": "v1.7.0", "source": { "type": "git", "url": "https://github.com/doctrine/cache.git", - "reference": "b6f544a20f4807e81f7044d31e679ccbb1866dc3" + "reference": "53d9518ffeb019c51d542ff60cb578f076d3ff16" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/cache/zipball/b6f544a20f4807e81f7044d31e679ccbb1866dc3", - "reference": "b6f544a20f4807e81f7044d31e679ccbb1866dc3", + "url": "https://api.github.com/repos/doctrine/cache/zipball/53d9518ffeb019c51d542ff60cb578f076d3ff16", + "reference": "53d9518ffeb019c51d542ff60cb578f076d3ff16", "shasum": "" }, "require": { - "php": "~5.5|~7.0" + "php": "~7.1" }, "conflict": { "doctrine/common": ">2.2,<2.4" }, "require-dev": { - "phpunit/phpunit": "~4.8|~5.0", - "predis/predis": "~1.0", - "satooshi/php-coveralls": "~0.6" + "alcaeus/mongo-php-adapter": "^1.1", + "mongodb/mongodb": "^1.1", + "phpunit/phpunit": "^5.7", + "predis/predis": "~1.0" + }, + "suggest": { + "alcaeus/mongo-php-adapter": "Required to use legacy MongoDB driver" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.6.x-dev" + "dev-master": "1.7.x-dev" } }, "autoload": { @@ -416,24 +420,24 @@ "cache", "caching" ], - "time": "2016-10-29T11:16:17+00:00" + "time": "2017-07-22T13:00:15+00:00" }, { "name": "doctrine/collections", - "version": "v1.4.0", + "version": "v1.5.0", "source": { "type": "git", "url": "https://github.com/doctrine/collections.git", - "reference": "1a4fb7e902202c33cce8c55989b945612943c2ba" + "reference": "a01ee38fcd999f34d9bfbcee59dbda5105449cbf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/collections/zipball/1a4fb7e902202c33cce8c55989b945612943c2ba", - "reference": "1a4fb7e902202c33cce8c55989b945612943c2ba", + "url": "https://api.github.com/repos/doctrine/collections/zipball/a01ee38fcd999f34d9bfbcee59dbda5105449cbf", + "reference": "a01ee38fcd999f34d9bfbcee59dbda5105449cbf", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0" + "php": "^7.1" }, "require-dev": { "doctrine/coding-standard": "~0.1@dev", @@ -483,20 +487,20 @@ "collections", "iterator" ], - "time": "2017-01-03T10:49:41+00:00" + "time": "2017-07-22T10:37:32+00:00" }, { "name": "doctrine/common", - "version": "v2.7.2", + "version": "v2.7.3", "source": { "type": "git", "url": "https://github.com/doctrine/common.git", - "reference": "930297026c8009a567ac051fd545bf6124150347" + "reference": "4acb8f89626baafede6ee5475bc5844096eba8a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/common/zipball/930297026c8009a567ac051fd545bf6124150347", - "reference": "930297026c8009a567ac051fd545bf6124150347", + "url": "https://api.github.com/repos/doctrine/common/zipball/4acb8f89626baafede6ee5475bc5844096eba8a9", + "reference": "4acb8f89626baafede6ee5475bc5844096eba8a9", "shasum": "" }, "require": { @@ -556,7 +560,7 @@ "persistence", "spl" ], - "time": "2017-01-13T14:02:13+00:00" + "time": "2017-07-22T08:35:12+00:00" }, { "name": "doctrine/dbal", @@ -631,33 +635,33 @@ }, { "name": "doctrine/inflector", - "version": "v1.1.0", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/doctrine/inflector.git", - "reference": "90b2128806bfde671b6952ab8bea493942c1fdae" + "reference": "e11d84c6e018beedd929cff5220969a3c6d1d462" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/inflector/zipball/90b2128806bfde671b6952ab8bea493942c1fdae", - "reference": "90b2128806bfde671b6952ab8bea493942c1fdae", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/e11d84c6e018beedd929cff5220969a3c6d1d462", + "reference": "e11d84c6e018beedd929cff5220969a3c6d1d462", "shasum": "" }, "require": { - "php": ">=5.3.2" + "php": "^7.0" }, "require-dev": { - "phpunit/phpunit": "4.*" + "phpunit/phpunit": "^6.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1.x-dev" + "dev-master": "1.2.x-dev" } }, "autoload": { - "psr-0": { - "Doctrine\\Common\\Inflector\\": "lib/" + "psr-4": { + "Doctrine\\Common\\Inflector\\": "lib/Doctrine/Common/Inflector" } }, "notification-url": "https://packagist.org/downloads/", @@ -694,7 +698,7 @@ "singularize", "string" ], - "time": "2015-11-06T14:35:42+00:00" + "time": "2017-07-22T12:18:28+00:00" }, { "name": "doctrine/lexer", @@ -2346,16 +2350,16 @@ }, { "name": "psy/psysh", - "version": "v0.8.9", + "version": "v0.8.10", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "58a31cc4404c8f632d8c557bc72056af2d3a83db" + "reference": "7ab97e5a32202585309f3ee35a0c08d2a8e588b1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/58a31cc4404c8f632d8c557bc72056af2d3a83db", - "reference": "58a31cc4404c8f632d8c557bc72056af2d3a83db", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/7ab97e5a32202585309f3ee35a0c08d2a8e588b1", + "reference": "7ab97e5a32202585309f3ee35a0c08d2a8e588b1", "shasum": "" }, "require": { @@ -2415,7 +2419,7 @@ "interactive", "shell" ], - "time": "2017-07-06T14:53:52+00:00" + "time": "2017-07-22T15:14:19+00:00" }, { "name": "ramsey/uuid", @@ -2653,16 +2657,16 @@ }, { "name": "spatie/fractalistic", - "version": "2.3.0", + "version": "2.3.1", "source": { "type": "git", "url": "https://github.com/spatie/fractalistic.git", - "reference": "79a48d949bc053a1c60c934f727f5901bf35fa74" + "reference": "2bba98fd266d4691395904be6d981bd09150802f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/fractalistic/zipball/79a48d949bc053a1c60c934f727f5901bf35fa74", - "reference": "79a48d949bc053a1c60c934f727f5901bf35fa74", + "url": "https://api.github.com/repos/spatie/fractalistic/zipball/2bba98fd266d4691395904be6d981bd09150802f", + "reference": "2bba98fd266d4691395904be6d981bd09150802f", "shasum": "" }, "require": { @@ -2700,7 +2704,7 @@ "spatie", "transform" ], - "time": "2017-07-03T08:20:31+00:00" + "time": "2017-07-21T23:08:30+00:00" }, { "name": "spatie/laravel-fractal", @@ -2816,7 +2820,7 @@ }, { "name": "symfony/console", - "version": "v3.3.4", + "version": "v3.3.5", "source": { "type": "git", "url": "https://github.com/symfony/console.git", @@ -2885,7 +2889,7 @@ }, { "name": "symfony/css-selector", - "version": "v3.3.4", + "version": "v3.3.5", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", @@ -2938,7 +2942,7 @@ }, { "name": "symfony/debug", - "version": "v3.3.4", + "version": "v3.3.5", "source": { "type": "git", "url": "https://github.com/symfony/debug.git", @@ -2994,7 +2998,7 @@ }, { "name": "symfony/event-dispatcher", - "version": "v3.3.4", + "version": "v3.3.5", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", @@ -3057,7 +3061,7 @@ }, { "name": "symfony/finder", - "version": "v3.3.4", + "version": "v3.3.5", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", @@ -3106,16 +3110,16 @@ }, { "name": "symfony/http-foundation", - "version": "v3.3.4", + "version": "v3.3.5", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "f347a5f561b03db95ed666959db42bbbf429b7e5" + "reference": "e307abe4b79ccbbfdced9b91c132fd128f456bc5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f347a5f561b03db95ed666959db42bbbf429b7e5", - "reference": "f347a5f561b03db95ed666959db42bbbf429b7e5", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e307abe4b79ccbbfdced9b91c132fd128f456bc5", + "reference": "e307abe4b79ccbbfdced9b91c132fd128f456bc5", "shasum": "" }, "require": { @@ -3155,20 +3159,20 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2017-06-24T09:29:48+00:00" + "time": "2017-07-17T14:07:10+00:00" }, { "name": "symfony/http-kernel", - "version": "v3.3.4", + "version": "v3.3.5", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "33f87c957122cfbd9d90de48698ee074b71106ea" + "reference": "16ceea64d23abddf58797a782ae96a5242282cd8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/33f87c957122cfbd9d90de48698ee074b71106ea", - "reference": "33f87c957122cfbd9d90de48698ee074b71106ea", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/16ceea64d23abddf58797a782ae96a5242282cd8", + "reference": "16ceea64d23abddf58797a782ae96a5242282cd8", "shasum": "" }, "require": { @@ -3241,7 +3245,7 @@ ], "description": "Symfony HttpKernel Component", "homepage": "https://symfony.com", - "time": "2017-07-05T13:28:15+00:00" + "time": "2017-07-17T19:08:23+00:00" }, { "name": "symfony/polyfill-mbstring", @@ -3412,16 +3416,16 @@ }, { "name": "symfony/process", - "version": "v3.3.4", + "version": "v3.3.5", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "5ab8949b682b1bf9d4511a228b5e045c96758c30" + "reference": "07432804942b9f6dd7b7377faf9920af5f95d70a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/5ab8949b682b1bf9d4511a228b5e045c96758c30", - "reference": "5ab8949b682b1bf9d4511a228b5e045c96758c30", + "url": "https://api.github.com/repos/symfony/process/zipball/07432804942b9f6dd7b7377faf9920af5f95d70a", + "reference": "07432804942b9f6dd7b7377faf9920af5f95d70a", "shasum": "" }, "require": { @@ -3457,11 +3461,11 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2017-07-03T08:12:02+00:00" + "time": "2017-07-13T13:05:09+00:00" }, { "name": "symfony/routing", - "version": "v3.3.4", + "version": "v3.3.5", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", @@ -3539,7 +3543,7 @@ }, { "name": "symfony/translation", - "version": "v3.3.4", + "version": "v3.3.5", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", @@ -3604,16 +3608,16 @@ }, { "name": "symfony/var-dumper", - "version": "v3.3.4", + "version": "v3.3.5", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "9ee920bba1d2ce877496dcafca7cbffff4dbe08a" + "reference": "0f32b62d21991700250fed5109b092949007c5b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/9ee920bba1d2ce877496dcafca7cbffff4dbe08a", - "reference": "9ee920bba1d2ce877496dcafca7cbffff4dbe08a", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/0f32b62d21991700250fed5109b092949007c5b3", + "reference": "0f32b62d21991700250fed5109b092949007c5b3", "shasum": "" }, "require": { @@ -3668,7 +3672,7 @@ "debug", "dump" ], - "time": "2017-07-05T13:02:37+00:00" + "time": "2017-07-10T14:18:27+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -3868,16 +3872,16 @@ "packages-dev": [ { "name": "barryvdh/laravel-ide-helper", - "version": "v2.4.0", + "version": "v2.4.1", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-ide-helper.git", - "reference": "87a02ff574f722c685e011f76692ab869ab64f6c" + "reference": "2b1273c45e2f8df7a625563e2283a17c14f02ae8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/87a02ff574f722c685e011f76692ab869ab64f6c", - "reference": "87a02ff574f722c685e011f76692ab869ab64f6c", + "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/2b1273c45e2f8df7a625563e2283a17c14f02ae8", + "reference": "2b1273c45e2f8df7a625563e2283a17c14f02ae8", "shasum": "" }, "require": { @@ -3937,7 +3941,7 @@ "phpstorm", "sublime" ], - "time": "2017-06-16T14:08:59+00:00" + "time": "2017-07-16T00:24:12+00:00" }, { "name": "barryvdh/reflection-docblock", @@ -4052,32 +4056,32 @@ }, { "name": "doctrine/instantiator", - "version": "1.0.5", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" + "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", - "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", + "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", "shasum": "" }, "require": { - "php": ">=5.3,<8.0-DEV" + "php": "^7.1" }, "require-dev": { "athletic/athletic": "~0.1.8", "ext-pdo": "*", "ext-phar": "*", - "phpunit/phpunit": "~4.0", - "squizlabs/php_codesniffer": "~2.0" + "phpunit/phpunit": "^6.2.3", + "squizlabs/php_codesniffer": "^3.0.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.2.x-dev" } }, "autoload": { @@ -4102,7 +4106,7 @@ "constructor", "instantiate" ], - "time": "2015-06-14T21:17:01+00:00" + "time": "2017-07-22T11:58:36+00:00" }, { "name": "friendsofphp/php-cs-fixer", @@ -4587,22 +4591,22 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "3.1.1", + "version": "3.2.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "8331b5efe816ae05461b7ca1e721c01b46bafb3e" + "reference": "46f7e8bb075036c92695b15a1ddb6971c751e585" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/8331b5efe816ae05461b7ca1e721c01b46bafb3e", - "reference": "8331b5efe816ae05461b7ca1e721c01b46bafb3e", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/46f7e8bb075036c92695b15a1ddb6971c751e585", + "reference": "46f7e8bb075036c92695b15a1ddb6971c751e585", "shasum": "" }, "require": { "php": ">=5.5", "phpdocumentor/reflection-common": "^1.0@dev", - "phpdocumentor/type-resolver": "^0.2.0", + "phpdocumentor/type-resolver": "^0.4.0", "webmozart/assert": "^1.0" }, "require-dev": { @@ -4628,24 +4632,24 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2016-09-30T07:12:33+00:00" + "time": "2017-07-15T11:38:20+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "0.2.1", + "version": "0.4.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb" + "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb", - "reference": "e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7", + "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7", "shasum": "" }, "require": { - "php": ">=5.5", + "php": "^5.5 || ^7.0", "phpdocumentor/reflection-common": "^1.0" }, "require-dev": { @@ -4675,7 +4679,7 @@ "email": "me@mikevanriel.com" } ], - "time": "2016-11-25T06:54:22+00:00" + "time": "2017-07-14T14:27:02+00:00" }, { "name": "phpspec/prophecy", @@ -5755,7 +5759,7 @@ }, { "name": "symfony/class-loader", - "version": "v3.3.4", + "version": "v3.3.5", "source": { "type": "git", "url": "https://github.com/symfony/class-loader.git", @@ -5811,7 +5815,7 @@ }, { "name": "symfony/config", - "version": "v3.3.4", + "version": "v3.3.5", "source": { "type": "git", "url": "https://github.com/symfony/config.git", @@ -5873,16 +5877,16 @@ }, { "name": "symfony/filesystem", - "version": "v3.3.4", + "version": "v3.3.5", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "311fa718389efbd8b627c272b9324a62437018cc" + "reference": "427987eb4eed764c3b6e38d52a0f87989e010676" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/311fa718389efbd8b627c272b9324a62437018cc", - "reference": "311fa718389efbd8b627c272b9324a62437018cc", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/427987eb4eed764c3b6e38d52a0f87989e010676", + "reference": "427987eb4eed764c3b6e38d52a0f87989e010676", "shasum": "" }, "require": { @@ -5918,11 +5922,11 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "time": "2017-06-24T09:29:48+00:00" + "time": "2017-07-11T07:17:58+00:00" }, { "name": "symfony/stopwatch", - "version": "v3.3.4", + "version": "v3.3.5", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", @@ -5971,7 +5975,7 @@ }, { "name": "symfony/yaml", - "version": "v3.3.4", + "version": "v3.3.5", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", @@ -6083,8 +6087,8 @@ "platform": { "php": ">=7.0.0", "ext-mbstring": "*", - "ext-zip": "*", - "ext-pdo_mysql": "*" + "ext-pdo_mysql": "*", + "ext-zip": "*" }, "platform-dev": [] } diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index be96cd02..e517d580 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -11,8 +11,36 @@ | */ +\Sofa\Eloquence\Model::unsetEventDispatcher(); + +$factory->define(Pterodactyl\Models\Server::class, function (Faker\Generator $faker) { + return [ + 'id' => $faker->randomNumber(), + 'uuid' => $faker->uuid, + 'uuidShort' => str_random(8), + 'name' => $faker->firstName, + 'description' => implode(' ', $faker->sentences()), + 'skip_scripts' => 0, + 'suspended' => 0, + 'memory' => 512, + 'swap' => 0, + 'disk' => 512, + 'io' => 500, + 'cpu' => 0, + 'oom_disabled' => 0, + 'pack_id' => null, + 'daemonSecret' => $faker->uuid, + 'username' => $faker->userName, + 'sftp_password' => null, + 'installed' => 1, + 'created_at' => \Carbon\Carbon::now(), + 'updated_at' => \Carbon\Carbon::now(), + ]; +}); + $factory->define(Pterodactyl\Models\User::class, function (Faker\Generator $faker) { return [ + 'id' => $faker->randomNumber(), 'external_id' => null, 'uuid' => $faker->uuid, 'username' => $faker->userName, @@ -57,3 +85,26 @@ $factory->define(Pterodactyl\Models\Node::class, function (Faker\Generator $fake 'daemonBase' => '/srv/daemon', ]; }); + +$factory->define(Pterodactyl\Models\ServiceVariable::class, function (Faker\Generator $faker) { + return [ + 'id' => $faker->randomNumber(), + 'name' => $faker->firstName, + 'description' => $faker->sentence(), + 'env_variable' => strtoupper(str_replace(' ', '_', $faker->words(2, true))), + 'default_value' => $faker->colorName, + 'user_viewable' => 0, + 'user_editable' => 0, + 'rules' => 'required|string', + 'created_at' => \Carbon\Carbon::now(), + 'updated_at' => \Carbon\Carbon::now(), + ]; +}); + +$factory->state(Pterodactyl\Models\ServiceVariable::class, 'viewable', function () { + return ['user_viewable' => 1]; +}); + +$factory->state(Pterodactyl\Models\ServiceVariable::class, 'editable', function () { + return ['user_editable' => 1]; +}); diff --git a/resources/lang/en/admin/server.php b/resources/lang/en/admin/server.php new file mode 100644 index 00000000..6ded5880 --- /dev/null +++ b/resources/lang/en/admin/server.php @@ -0,0 +1,34 @@ +. + * + * 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. + */ + +return [ + 'exceptions' => [ + 'bad_variable' => 'There was a validation error with the :name variable.', + 'daemon_exception' => 'There was an exception while attempting to communicate with the daemon resulting in a HTTP/:code response code. This exception has been logged.', + ], + 'alerts' => [ + 'details_updated' => 'Server details have been successfully updated.', + 'docker_image_updated' => 'Successfully changed the default Docker image to use for this server. A reboot is required to apply this change.', + ], +]; diff --git a/resources/themes/pterodactyl/admin/servers/view/details.blade.php b/resources/themes/pterodactyl/admin/servers/view/details.blade.php index 8519a16c..886df32d 100644 --- a/resources/themes/pterodactyl/admin/servers/view/details.blade.php +++ b/resources/themes/pterodactyl/admin/servers/view/details.blade.php @@ -89,6 +89,7 @@ @@ -102,13 +103,14 @@
- +

The docker image to use for this server. The default image for this service and option combination is {{ $server->option->docker_image }}.

diff --git a/routes/admin.php b/routes/admin.php index 039eafe3..38785ee9 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -110,8 +110,6 @@ Route::group(['prefix' => 'servers'], function () { Route::post('/new', 'ServersController@store'); Route::post('/new/nodes', 'ServersController@nodes')->name('admin.servers.new.nodes'); - Route::post('/view/{id}/details', 'ServersController@setDetails'); - Route::post('/view/{id}/details/container', 'ServersController@setContainer')->name('admin.servers.view.details.container'); Route::post('/view/{id}/build', 'ServersController@updateBuild'); Route::post('/view/{id}/startup', 'ServersController@saveStartup'); Route::post('/view/{id}/database', 'ServersController@newDatabase'); @@ -121,6 +119,8 @@ Route::group(['prefix' => 'servers'], function () { Route::post('/view/{id}/manage/reinstall', 'ServersController@reinstallServer')->name('admin.servers.view.manage.reinstall'); Route::post('/view/{id}/delete', 'ServersController@delete'); + Route::patch('/view/{server}/details', 'ServersController@setDetails'); + Route::patch('/view/{server}/details/container', 'ServersController@setContainer')->name('admin.servers.view.details.container'); Route::patch('/view/{id}/database', 'ServersController@resetDatabasePassword'); Route::delete('/view/{id}/database/{database}/delete', 'ServersController@deleteDatabase')->name('admin.servers.view.database.delete'); diff --git a/tests/Unit/Services/Servers/CreationServiceTest.php b/tests/Unit/Services/Servers/CreationServiceTest.php new file mode 100644 index 00000000..3f1ac7c8 --- /dev/null +++ b/tests/Unit/Services/Servers/CreationServiceTest.php @@ -0,0 +1,224 @@ +. + * + * 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\Servers; + +use Mockery as m; +use phpmock\phpunit\PHPMock; +use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; +use Pterodactyl\Services\Servers\CreationService; +use Pterodactyl\Services\Servers\UsernameGenerationService; +use Pterodactyl\Services\Servers\VariableValidatorService; +use Ramsey\Uuid\Uuid; +use Tests\TestCase; +use Illuminate\Database\DatabaseManager; +use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; + +class CreationServiceTest extends TestCase +{ + use PHPMock; + + /** + * @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface + */ + protected $allocationRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface + */ + protected $daemonServerRepository; + + /** + * @var \Illuminate\Database\DatabaseManager + */ + protected $database; + + /** + * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface + */ + protected $nodeRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface + */ + protected $serverVariableRepository; + + /** + * @var \Pterodactyl\Services\Servers\CreationService + */ + protected $service; + + /** + * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface + */ + protected $userRepository; + + /** + * @var \Pterodactyl\Services\Servers\UsernameGenerationService + */ + protected $usernameService; + + /** + * @var \Pterodactyl\Services\Servers\VariableValidatorService + */ + protected $validatorService; + + /** + * @var \Ramsey\Uuid\Uuid + */ + protected $uuid; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->allocationRepository = m::mock(AllocationRepositoryInterface::class); + $this->daemonServerRepository = m::mock(DaemonServerRepositoryInterface::class); + $this->database = m::mock(DatabaseManager::class); + $this->nodeRepository = m::mock(NodeRepositoryInterface::class); + $this->repository = m::mock(ServerRepositoryInterface::class); + $this->serverVariableRepository = m::mock(ServerVariableRepositoryInterface::class); + $this->userRepository = m::mock(UserRepositoryInterface::class); + $this->usernameService = m::mock(UsernameGenerationService::class); + $this->validatorService = m::mock(VariableValidatorService::class); + $this->uuid = m::mock('overload:Ramsey\Uuid\Uuid'); + + $this->getFunctionMock('\\Pterodactyl\\Services\\Servers', 'bin2hex') + ->expects($this->any())->willReturn('randomstring'); + + $this->getFunctionMock('\\Ramsey\\Uuid\\Uuid', 'uuid4') + ->expects($this->any())->willReturn('s'); + + $this->service = new CreationService( + $this->allocationRepository, + $this->daemonServerRepository, + $this->database, + $this->nodeRepository, + $this->repository, + $this->serverVariableRepository, + $this->userRepository, + $this->usernameService, + $this->validatorService + ); + } + + /** + * Test core functionality of the creation process. + */ + public function testCreateShouldHitAllOfTheNecessaryServicesAndStoreTheServer() + { + $data = [ + 'node_id' => 1, + 'name' => 'SomeName', + 'description' => null, + 'user_id' => 1, + 'memory' => 128, + 'disk' => 128, + 'swap' => 0, + 'io' => 500, + 'cpu' => 0, + 'allocation_id' => 1, + 'allocation_additional' => [2, 3], + 'environment' => [ + 'TEST_VAR_1' => 'var1-value', + ], + 'service_id' => 1, + 'option_id' => 1, + 'startup' => 'startup-param', + 'docker_image' => 'some/image', + ]; + + $this->validatorService->shouldReceive('setAdmin')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('setFields')->with($data['environment'])->once()->andReturnSelf() + ->shouldReceive('validate')->with($data['option_id'])->once()->andReturnSelf(); + + $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->uuid->shouldReceive('uuid4')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('toString')->withNoArgs()->once()->andReturn('uuid-0000'); + $this->usernameService->shouldReceive('generate')->with($data['name'], 'randomstring') + ->once()->andReturn('user_name'); + + $this->repository->shouldReceive('create')->with([ + 'uuid' => 'uuid-0000', + 'uuidShort' => 'randomstring', + 'node_id' => $data['node_id'], + 'name' => $data['name'], + 'description' => $data['description'], + 'skip_scripts' => false, + 'suspended' => false, + 'owner_id' => $data['user_id'], + 'memory' => $data['memory'], + 'swap' => $data['swap'], + 'disk' => $data['disk'], + 'io' => $data['io'], + 'cpu' => $data['cpu'], + 'oom_disabled' => false, + 'allocation_id' => $data['allocation_id'], + 'service_id' => $data['service_id'], + 'option_id' => $data['option_id'], + 'pack_id' => null, + 'startup' => $data['startup'], + 'daemonSecret' => 'randomstring', + 'image' => $data['docker_image'], + 'username' => 'user_name', + 'sftp_password' => null, + ])->once()->andReturn((object) [ + 'node_id' => 1, + 'id' => 1, + ]); + + $this->allocationRepository->shouldReceive('assignAllocationsToServer')->with(1, [1, 2, 3]); + $this->validatorService->shouldReceive('getResults')->withNoArgs()->once()->andReturn([[ + 'id' => 1, + 'key' => 'TEST_VAR_1', + 'value' => 'var1-value', + ]]); + + $this->serverVariableRepository->shouldReceive('insert')->with([[ + 'server_id' => 1, + 'variable_id' => 1, + 'variable_value' => 'var1-value', + ]])->once()->andReturnNull(); + $this->daemonServerRepository->shouldReceive('setNode')->with(1)->once()->andReturnSelf() + ->shouldReceive('create')->with(1)->once()->andReturnNull(); + $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $response = $this->service->create($data); + + $this->assertEquals(1, $response->id); + $this->assertEquals(1, $response->node_id); + } +} diff --git a/tests/Unit/Services/Servers/DetailsModificationServiceTest.php b/tests/Unit/Services/Servers/DetailsModificationServiceTest.php new file mode 100644 index 00000000..3ed8208a --- /dev/null +++ b/tests/Unit/Services/Servers/DetailsModificationServiceTest.php @@ -0,0 +1,394 @@ +. + * + * 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\Servers; + +use Exception; +use Mockery as m; +use Tests\TestCase; +use phpmock\phpunit\PHPMock; +use Pterodactyl\Models\Server; +use Illuminate\Database\DatabaseManager; +use GuzzleHttp\Exception\RequestException; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Repositories\Eloquent\ServerRepository; +use Pterodactyl\Services\Servers\DetailsModificationService; +use Pterodactyl\Repositories\Daemon\ServerRepository as DaemonServerRepository; + +class DetailsModificationServiceTest extends TestCase +{ + use PHPMock; + + /** + * @var \Illuminate\Database\DatabaseManager + */ + protected $database; + + /** + * @var \Pterodactyl\Repositories\Daemon\ServerRepository + */ + protected $daemonServerRepository; + + /** + * @var \GuzzleHttp\Exception\RequestException + */ + protected $exception; + + /** + * @var \Pterodactyl\Repositories\Eloquent\ServerRepository + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Servers\DetailsModificationService + */ + protected $service; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->database = m::mock(DatabaseManager::class); + $this->exception = m::mock(RequestException::class)->makePartial(); + $this->daemonServerRepository = m::mock(DaemonServerRepository::class); + $this->repository = m::mock(ServerRepository::class); + + $this->getFunctionMock('\\Pterodactyl\\Services\\Servers', 'bin2hex') + ->expects($this->any())->willReturn('randomString'); + + $this->service = new DetailsModificationService( + $this->database, + $this->daemonServerRepository, + $this->repository + ); + } + + /** + * Test basic updating of core variables when a model is provided. + */ + public function testEditShouldSkipDatabaseSearchIfModelIsPassed() + { + $server = factory(Server::class)->make([ + 'owner_id' => 1, + ]); + + $data = ['owner_id' => 1, 'name' => 'New Name', 'description' => 'New Description']; + + $this->repository->shouldNotReceive('find'); + $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($server->id, [ + 'owner_id' => $data['owner_id'], + 'name' => $data['name'], + 'description' => $data['description'], + 'daemonSecret' => $server->daemonSecret, + ], true, true)->once()->andReturnNull(); + + $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturn(true); + + $response = $this->service->edit($server, $data); + $this->assertTrue($response); + } + + /** + * Test that repository attempts to find model in database if no model is passed. + */ + public function testEditShouldGetModelFromRepositoryIfNotPassed() + { + $server = factory(Server::class)->make([ + 'owner_id' => 1, + ]); + + $data = ['owner_id' => 1, 'name' => 'New Name', 'description' => 'New Description']; + + $this->repository->shouldReceive('find')->with($server->id)->once()->andReturn($server); + $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($server->id, [ + 'owner_id' => $data['owner_id'], + 'name' => $data['name'], + 'description' => $data['description'], + 'daemonSecret' => $server->daemonSecret, + ], true, true)->once()->andReturnNull(); + + $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturn(true); + + $response = $this->service->edit($server->id, $data); + $this->assertTrue($response); + } + + /** + * Test that the daemon secret is reset if the owner id changes. + */ + public function testEditShouldResetDaemonSecretIfOwnerIdIsChanged() + { + $server = factory(Server::class)->make([ + 'owner_id' => 1, + 'node_id' => 1, + ]); + + $data = ['owner_id' => 2, 'name' => 'New Name', 'description' => 'New Description']; + + $this->repository->shouldNotReceive('find'); + $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($server->id, [ + 'owner_id' => $data['owner_id'], + 'name' => $data['name'], + 'description' => $data['description'], + 'daemonSecret' => 'randomString', + ], true, true)->once()->andReturnNull(); + + $this->daemonServerRepository->shouldReceive('setNode')->with($server->node_id)->once()->andReturnSelf() + ->shouldReceive('setAccessServer')->with($server->uuid)->once()->andReturnSelf() + ->shouldReceive('update')->with([ + 'keys' => [ + $server->daemonSecret => [], + 'randomString' => DaemonServerRepository::DAEMON_PERMISSIONS, + ], + ])->once()->andReturnNull(); + + $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturn(true); + + $response = $this->service->edit($server, $data); + $this->assertTrue($response); + } + + public function testEditShouldResetDaemonSecretIfBooleanValueIsPassed() + { + $server = factory(Server::class)->make([ + 'owner_id' => 1, + 'node_id' => 1, + ]); + + $data = ['owner_id' => 1, 'name' => 'New Name', 'description' => 'New Description', 'reset_token' => true]; + + $this->repository->shouldNotReceive('find'); + $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($server->id, [ + 'owner_id' => $data['owner_id'], + 'name' => $data['name'], + 'description' => $data['description'], + 'daemonSecret' => 'randomString', + ], true, true)->once()->andReturnNull(); + + $this->daemonServerRepository->shouldReceive('setNode')->with($server->node_id)->once()->andReturnSelf() + ->shouldReceive('setAccessServer')->with($server->uuid)->once()->andReturnSelf() + ->shouldReceive('update')->with([ + 'keys' => [ + $server->daemonSecret => [], + 'randomString' => DaemonServerRepository::DAEMON_PERMISSIONS, + ], + ])->once()->andReturnNull(); + + $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturn(true); + + $response = $this->service->edit($server, $data); + $this->assertTrue($response); + } + + /** + * Test that a displayable exception is thrown if the daemon responds with an error. + */ + public function testEditShouldThrowADisplayableExceptionIfDaemonResponseErrors() + { + $server = factory(Server::class)->make([ + 'owner_id' => 1, + 'node_id' => 1, + ]); + + $data = ['owner_id' => 2, 'name' => 'New Name', 'description' => 'New Description']; + + $this->repository->shouldNotReceive('find'); + $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($server->id, [ + 'owner_id' => $data['owner_id'], + 'name' => $data['name'], + 'description' => $data['description'], + 'daemonSecret' => 'randomString', + ], true, true)->once()->andReturnNull(); + + $this->daemonServerRepository->shouldReceive('setNode')->andThrow($this->exception); + $this->exception->shouldReceive('getResponse')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('getStatusCode')->withNoArgs()->once()->andReturn(400); + + $this->database->shouldNotReceive('commit'); + + try { + $this->service->edit($server, $data); + } catch (Exception $exception) { + $this->assertInstanceOf(DisplayException::class, $exception); + $this->assertEquals( + trans('admin/server.exceptions.daemon_exception', ['code' => 400,]), $exception->getMessage() + ); + } + } + + /** + * Test that an exception not stemming from Guzzle is not thrown as a displayable exception. + * + * @expectedException \Exception + */ + public function testEditShouldNotThrowDisplayableExceptionIfExceptionIsNotThrownByGuzzle() + { + $server = factory(Server::class)->make([ + 'owner_id' => 1, + 'node_id' => 1, + ]); + + $data = ['owner_id' => 2, 'name' => 'New Name', 'description' => 'New Description']; + + $this->repository->shouldNotReceive('find'); + $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($server->id, [ + 'owner_id' => $data['owner_id'], + 'name' => $data['name'], + 'description' => $data['description'], + 'daemonSecret' => 'randomString', + ], true, true)->once()->andReturnNull(); + + $this->daemonServerRepository->shouldReceive('setNode')->andThrow(new Exception()); + $this->database->shouldNotReceive('commit'); + + $this->service->edit($server, $data); + } + + /** + * Test that the docker image for a server can be updated if a model is provided. + */ + public function testDockerImageCanBeUpdatedWhenAServerModelIsProvided() + { + $server = factory(Server::class)->make(['node_id' => 1]); + + $this->repository->shouldNotReceive('find'); + $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($server->id, [ + 'image' => 'new/image', + ])->once()->andReturnNull(); + + $this->daemonServerRepository->shouldReceive('setNode')->with($server->node_id)->once()->andReturnSelf() + ->shouldReceive('setAccessServer')->with($server->uuid)->once()->andReturnSelf() + ->shouldReceive('update')->with([ + 'build' => [ + 'image' => 'new/image', + ], + ])->once()->andReturnNull(); + + $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturn(true); + + $response = $this->service->setDockerImage($server, 'new/image'); + $this->assertTrue($response); + } + + /** + * Test that the docker image for a server can be updated if a model is provided. + */ + public function testDockerImageCanBeUpdatedWhenNoModelIsProvided() + { + $server = factory(Server::class)->make(['node_id' => 1]); + + $this->repository->shouldReceive('find')->with($server->id)->once()->andReturn($server); + $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($server->id, [ + 'image' => 'new/image', + ])->once()->andReturnNull(); + + $this->daemonServerRepository->shouldReceive('setNode')->with($server->node_id)->once()->andReturnSelf() + ->shouldReceive('setAccessServer')->with($server->uuid)->once()->andReturnSelf() + ->shouldReceive('update')->with([ + 'build' => [ + 'image' => 'new/image', + ], + ])->once()->andReturnNull(); + + $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturn(true); + + $response = $this->service->setDockerImage($server->id, 'new/image'); + $this->assertTrue($response); + } + + /** + * Test that an exception thrown by Guzzle is rendered as a displayable exception. + */ + public function testExceptionThrownByGuzzleWhenSettingDockerImageShouldBeRenderedAsADisplayableException() + { + $server = factory(Server::class)->make(['node_id' => 1]); + + $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($server->id, [ + 'image' => 'new/image', + ])->once()->andReturnNull(); + + $this->daemonServerRepository->shouldReceive('setNode')->andThrow($this->exception); + $this->exception->shouldReceive('getResponse')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('getStatusCode')->withNoArgs()->once()->andReturn(400); + + $this->database->shouldNotReceive('commit'); + + try { + $this->service->setDockerImage($server, 'new/image'); + } catch (Exception $exception) { + $this->assertInstanceOf(DisplayException::class, $exception); + $this->assertEquals( + trans('admin/server.exceptions.daemon_exception', ['code' => 400,]), $exception->getMessage() + ); + } + } + + /** + * Test that an exception not thrown by Guzzle is not transformed to a displayable exception. + * + * @expectedException \Exception + */ + public function testExceptionNotThrownByGuzzleWhenSettingDockerImageShouldNotBeRenderedAsADisplayableException() + { + $server = factory(Server::class)->make(['node_id' => 1]); + + $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($server->id, [ + 'image' => 'new/image', + ])->once()->andReturnNull(); + + $this->daemonServerRepository->shouldReceive('setNode')->andThrow(new Exception()); + $this->database->shouldNotReceive('commit'); + + $this->service->setDockerImage($server, 'new/image'); + } +} diff --git a/tests/Unit/Services/Servers/EnvironmentServiceTest.php b/tests/Unit/Services/Servers/EnvironmentServiceTest.php new file mode 100644 index 00000000..7dc5c271 --- /dev/null +++ b/tests/Unit/Services/Servers/EnvironmentServiceTest.php @@ -0,0 +1,155 @@ +. + * + * 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\Servers; + +use Mockery as m; +use Tests\TestCase; +use Pterodactyl\Models\Server; +use Pterodactyl\Models\Location; +use Pterodactyl\Services\Servers\EnvironmentService; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; + +class EnvironmentServiceTest extends TestCase +{ + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Servers\EnvironmentService + */ + protected $service; + + /** + * @var \Pterodactyl\Models\Server + */ + protected $server; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->repository = m::mock(ServerRepositoryInterface::class); + $this->server = factory(Server::class)->make([ + 'location' => factory(Location::class)->make(), + ]); + + $this->service = new EnvironmentService($this->repository); + } + + /** + * Test that set environment key function returns an instance of the class. + */ + public function testSettingEnvironmentKeyShouldReturnInstanceOfSelf() + { + $instance = $this->service->setEnvironmentKey('TEST_KEY', function () { + return true; + }); + + $this->assertInstanceOf(EnvironmentService::class, $instance); + } + + /** + * Test that environment defaults are returned by the process function. + */ + public function testProcessShouldReturnDefaultEnvironmentVariablesForAServer() + { + $this->repository->shouldReceive('getVariablesWithValues')->with($this->server->id)->once()->andReturn([ + 'TEST_VARIABLE' => 'Test Variable', + ]); + + $response = $this->service->process($this->server); + + $this->assertEquals(count(EnvironmentService::ENVIRONMENT_CASTS) + 1, count($response), 'Assert response contains correct amount of items.'); + $this->assertTrue(is_array($response), 'Assert that response is an array.'); + + $this->assertArrayHasKey('TEST_VARIABLE', $response); + $this->assertEquals('Test Variable', $response['TEST_VARIABLE']); + + foreach (EnvironmentService::ENVIRONMENT_CASTS as $key => $value) { + $this->assertArrayHasKey($key, $response); + $this->assertEquals(object_get($this->server, $value), $response[$key]); + } + } + + /** + * Test that variables included at run-time are also included. + */ + public function testProcessShouldReturnKeySetAtRuntime() + { + $this->repository->shouldReceive('getVariablesWithValues')->with($this->server->id)->once()->andReturn([]); + + $response = $this->service->setEnvironmentKey('TEST_VARIABLE', function ($server) { + return $server->uuidShort; + })->process($this->server); + + $this->assertTrue(is_array($response), 'Assert response is an array.'); + $this->assertArrayHasKey('TEST_VARIABLE', $response); + $this->assertEquals($this->server->uuidShort, $response['TEST_VARIABLE']); + } + + /** + * Test that duplicate variables provided at run-time override the defaults. + */ + public function testProcessShouldAllowOverwritingDefaultVariablesWithRuntimeProvided() + { + $this->repository->shouldReceive('getVariablesWithValues')->with($this->server->id)->once()->andReturn([]); + + $response = $this->service->setEnvironmentKey('P_SERVER_UUID', function ($server) { + return 'overwritten'; + })->process($this->server); + + $this->assertTrue(is_array($response), 'Assert response is an array.'); + $this->assertArrayHasKey('P_SERVER_UUID', $response); + $this->assertEquals('overwritten', $response['P_SERVER_UUID']); + } + + /** + * Test that function can run when an ID is provided rather than a server model. + */ + public function testProcessShouldAcceptAnIntegerInPlaceOfAServerModel() + { + $this->repository->shouldReceive('find')->with($this->server->id)->once()->andReturn($this->server); + $this->repository->shouldReceive('getVariablesWithValues')->with($this->server->id)->once()->andReturn([]); + + $response = $this->service->process($this->server->id); + + $this->assertTrue(is_array($response), 'Assert that response is an array.'); + } + + /** + * Test that an exception is thrown when no model or valid ID is provided. + * + * @expectedException \InvalidArgumentException + */ + public function testProcessShouldThrowExceptionIfInvalidServerIsProvided() + { + $this->service->process('abcd'); + } +} diff --git a/tests/Unit/Services/Servers/UsernameGenerationServiceTest.php b/tests/Unit/Services/Servers/UsernameGenerationServiceTest.php new file mode 100644 index 00000000..c0d80cd5 --- /dev/null +++ b/tests/Unit/Services/Servers/UsernameGenerationServiceTest.php @@ -0,0 +1,127 @@ +. + * + * 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\Servers; + +use Tests\TestCase; +use phpmock\phpunit\PHPMock; +use Pterodactyl\Services\Servers\UsernameGenerationService; + +class UsernameGenerationServiceTest extends TestCase +{ + use PHPMock; + + /** + * @var UsernameGenerationService + */ + protected $service; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->service = new UsernameGenerationService(); + + $this->getFunctionMock('\\Pterodactyl\\Services\\Servers', 'bin2hex') + ->expects($this->any())->willReturn('dddddddd'); + + $this->getFunctionMock('\\Pterodactyl\\Services\\Servers', 'str_random') + ->expects($this->any())->willReturnCallback(function ($count) { + return str_pad('', $count, 'a'); + }); + } + + /** + * Test that a valid username is returned and is the correct length. + */ + public function testShouldReturnAValidUsernameWithASelfGeneratedIdentifier() + { + $response = $this->service->generate('testname'); + + $this->assertEquals('testna_dddddddd', $response); + } + + /** + * Test that a name and identifier provided returns the expected username. + */ + public function testShouldReturnAValidUsernameWithAnIdentifierProvided() + { + $response = $this->service->generate('testname', 'identifier'); + + $this->assertEquals('testna_identifi', $response); + } + + /** + * Test that the identifier is extended to 8 characters if it is shorter. + */ + public function testShouldExtendIdentifierToBe8CharactersIfItIsShorter() + { + $response = $this->service->generate('testname', 'xyz'); + + $this->assertEquals('testna_xyzaaaaa', $response); + } + + /** + * Test that special characters are removed from the username. + */ + public function testShouldStripSpecialCharactersFromName() + { + $response = $this->service->generate('te!st_n$ame', 'identifier'); + + $this->assertEquals('testna_identifi', $response); + } + + /** + * Test that an empty name is replaced with 6 random characters. + */ + public function testEmptyNamesShouldBeReplacedWithRandomCharacters() + { + $response = $this->service->generate(''); + + $this->assertEquals('aaaaaa_dddddddd', $response); + } + + /** + * Test that a name consisting entirely of special characters is handled. + */ + public function testNameOfOnlySpecialCharactersIsHandledProperly() + { + $response = $this->service->generate('$%#*#(@#(#*$&#(#!#@'); + + $this->assertEquals('aaaaaa_dddddddd', $response); + } + + /** + * Test that passing a name shorter than 6 characters returns the entire name. + */ + public function testNameShorterThan6CharactersShouldBeRenderedEntirely() + { + $response = $this->service->generate('test', 'identifier'); + + $this->assertEquals('test_identifi', $response); + } +} diff --git a/tests/Unit/Services/Servers/VariableValidatorServiceTest.php b/tests/Unit/Services/Servers/VariableValidatorServiceTest.php new file mode 100644 index 00000000..b2e87cf0 --- /dev/null +++ b/tests/Unit/Services/Servers/VariableValidatorServiceTest.php @@ -0,0 +1,243 @@ +. + * + * 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\Servers; + +use Mockery as m; +use Tests\TestCase; +use Pterodactyl\Models\ServiceVariable; +use Illuminate\Contracts\Validation\Factory; +use Pterodactyl\Exceptions\DisplayValidationException; +use Pterodactyl\Services\Servers\VariableValidatorService; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\OptionVariableRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; + +class VariableValidatorServiceTest extends TestCase +{ + /** + * @var \Pterodactyl\Contracts\Repository\OptionVariableRepositoryInterface + */ + protected $optionVariableRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $serverRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface + */ + protected $serverVariableRepository; + + /** + * @var \Pterodactyl\Services\Servers\VariableValidatorService + */ + protected $service; + + /** + * @var \Illuminate\Validation\Factory + */ + protected $validator; + + /** + * @var \Illuminate\Support\Collection + */ + protected $variables; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->variables = collect( + [ + factory(ServiceVariable::class)->states('editable', 'viewable')->make(), + factory(ServiceVariable::class)->states('viewable')->make(), + factory(ServiceVariable::class)->states('editable')->make(), + factory(ServiceVariable::class)->make(), + ] + ); + + $this->optionVariableRepository = m::mock(OptionVariableRepositoryInterface::class); + $this->serverRepository = m::mock(ServerRepositoryInterface::class); + $this->serverVariableRepository = m::mock(ServerVariableRepositoryInterface::class); + $this->validator = m::mock(Factory::class); + + $this->service = new VariableValidatorService( + $this->optionVariableRepository, + $this->serverRepository, + $this->serverVariableRepository, + $this->validator + ); + } + + /** + * Test that setting fields returns an instance of the class. + */ + public function testSettingFieldsShouldReturnInstanceOfSelf() + { + $response = $this->service->setFields([]); + + $this->assertInstanceOf(VariableValidatorService::class, $response); + } + + /** + * Test that setting administrator value returns an instance of the class. + */ + public function testSettingAdminShouldReturnInstanceOfSelf() + { + $response = $this->service->setAdmin(); + + $this->assertInstanceOf(VariableValidatorService::class, $response); + } + + /** + * Test that getting the results returns an array of values. + */ + public function testGettingResultsReturnsAnArrayOfValues() + { + $response = $this->service->getResults(); + + $this->assertTrue(is_array($response)); + } + + /** + * Test that when no variables are found for an option no data is returned. + */ + public function testEmptyResultSetShouldBeReturnedIfNoVariablesAreFound() + { + $this->optionVariableRepository->shouldReceive('findWhere')->with([['option_id', '=', 1]])->andReturn([]); + + $response = $this->service->validate(1); + + $this->assertInstanceOf(VariableValidatorService::class, $response); + $this->assertTrue(is_array($response->getResults())); + $this->assertEmpty($response->getResults()); + } + + /** + * Test that variables set as user_editable=0 and/or user_viewable=0 are skipped when admin flag is not set. + */ + public function testValidatorShouldNotProcessVariablesSetAsNotUserEditableWhenAdminFlagIsNotPassed() + { + $this->optionVariableRepository->shouldReceive('findWhere')->with([['option_id', '=', 1]])->andReturn($this->variables); + + $this->validator->shouldReceive('make')->with([ + 'variable_value' => 'Test_SomeValue_0', + ], [ + 'variable_value' => $this->variables{0}->rules, + ])->once()->andReturnSelf() + ->shouldReceive('fails')->withNoArgs()->once()->andReturn(false); + + $response = $this->service->setFields([ + $this->variables{0}->env_variable => 'Test_SomeValue_0', + $this->variables{1}->env_variable => 'Test_SomeValue_1', + $this->variables{2}->env_variable => 'Test_SomeValue_2', + $this->variables{3}->env_variable => 'Test_SomeValue_3', + ])->validate(1)->getResults(); + + $this->assertEquals(1, count($response), 'Assert response has a single item in array.'); + $this->assertArrayHasKey('0', $response); + $this->assertArrayHasKey('id', $response[0]); + $this->assertArrayHasKey('key', $response[0]); + $this->assertArrayHasKey('value', $response[0]); + + $this->assertEquals($this->variables{0}->id, $response[0]['id']); + $this->assertEquals($this->variables{0}->env_variable, $response[0]['key']); + $this->assertEquals('Test_SomeValue_0', $response[0]['value']); + } + + /** + * Test that all variables are processed correctly if admin flag is set. + */ + public function testValidatorShouldProcessAllVariablesWhenAdminFlagIsSet() + { + $this->optionVariableRepository->shouldReceive('findWhere')->with([['option_id', '=', 1]])->andReturn($this->variables); + + foreach($this->variables as $key => $variable) { + $this->validator->shouldReceive('make')->with([ + 'variable_value' => 'Test_SomeValue_' . $key, + ], [ + 'variable_value' => $this->variables{$key}->rules, + ])->andReturnSelf() + ->shouldReceive('fails')->withNoArgs()->once()->andReturn(false); + } + + $response = $this->service->setAdmin()->setFields([ + $this->variables{0}->env_variable => 'Test_SomeValue_0', + $this->variables{1}->env_variable => 'Test_SomeValue_1', + $this->variables{2}->env_variable => 'Test_SomeValue_2', + $this->variables{3}->env_variable => 'Test_SomeValue_3', + ])->validate(1)->getResults(); + + $this->assertEquals(4, count($response), 'Assert response has all four items in array.'); + + foreach($response as $key => $values) { + $this->assertArrayHasKey($key, $response); + $this->assertArrayHasKey('id', $response[$key]); + $this->assertArrayHasKey('key', $response[$key]); + $this->assertArrayHasKey('value', $response[$key]); + + $this->assertEquals($this->variables{$key}->id, $response[$key]['id']); + $this->assertEquals($this->variables{$key}->env_variable, $response[$key]['key']); + $this->assertEquals('Test_SomeValue_' . $key, $response[$key]['value']); + } + } + + /** + * Test that a DisplayValidationError is thrown when a variable is not validated. + */ + public function testValidatorShouldThrowExceptionWhenAValidationErrorIsEncountered() + { + $this->optionVariableRepository->shouldReceive('findWhere')->with([['option_id', '=', 1]])->andReturn($this->variables); + + $this->validator->shouldReceive('make')->with([ + 'variable_value' => null, + ], [ + 'variable_value' => $this->variables{0}->rules, + ])->once()->andReturnSelf() + ->shouldReceive('fails')->withNoArgs()->once()->andReturn(true); + + $this->validator->shouldReceive('errors')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('toArray')->withNoArgs()->once()->andReturn([]); + + try { + $this->service->setFields([ + $this->variables{0}->env_variable => null, + ])->validate(1); + } catch (DisplayValidationException $exception) { + $decoded = json_decode($exception->getMessage()); + + $this->assertEquals(0, json_last_error(), 'Assert that response is decodable JSON.'); + $this->assertObjectHasAttribute('notice', $decoded); + $this->assertEquals( + trans('admin/server.exceptions.bad_variable', ['name' => $this->variables{0}->name]), + $decoded->notice[0] + ); + } + } +} From 5144e0126baceefcd885fd9a52161b9674f75eb7 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 23 Jul 2017 14:51:18 -0500 Subject: [PATCH 31/99] Add support for more server functionality --- .../Daemon/ServerRepositoryInterface.php | 28 +++ .../LocationRepositoryInterface.php | 17 ++ .../Controllers/Admin/LocationController.php | 33 +-- .../Controllers/Admin/ServersController.php | 187 ++++++++++------- app/Repositories/Daemon/ServerRepository.php | 32 +++ .../Eloquent/LocationRepository.php | 22 ++ .../Servers/ContainerRebuildService.php | 80 +++++++ .../Servers/DetailsModificationService.php | 2 +- app/Services/Servers/ReinstallService.php | 94 +++++++++ app/Services/Servers/SuspensionService.php | 114 ++++++++++ resources/lang/en/admin/server.php | 5 + routes/admin.php | 34 +-- .../Servers/ContainerRebuildServiceTest.php | 139 ++++++++++++ .../DetailsModificationServiceTest.php | 6 +- .../Services/Servers/ReinstallServiceTest.php | 177 ++++++++++++++++ .../Servers/SuspensionServiceTest.php | 198 ++++++++++++++++++ 16 files changed, 1049 insertions(+), 119 deletions(-) create mode 100644 app/Services/Servers/ContainerRebuildService.php create mode 100644 app/Services/Servers/ReinstallService.php create mode 100644 app/Services/Servers/SuspensionService.php create mode 100644 tests/Unit/Services/Servers/ContainerRebuildServiceTest.php create mode 100644 tests/Unit/Services/Servers/ReinstallServiceTest.php create mode 100644 tests/Unit/Services/Servers/SuspensionServiceTest.php diff --git a/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php b/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php index a69e1bb6..d8659de3 100644 --- a/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php +++ b/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php @@ -43,4 +43,32 @@ interface ServerRepositoryInterface extends BaseRepositoryInterface * @return \Psr\Http\Message\ResponseInterface */ public function update(array $data); + + /** + * Mark a server to be reinstalled on the system. + * + * @return \Psr\Http\Message\ResponseInterface + */ + public function reinstall(); + + /** + * Mark a server as needing a container rebuild the next time the server is booted. + * + * @return \Psr\Http\Message\ResponseInterface + */ + public function rebuild(); + + /** + * Suspend a server on the daemon. + * + * @return \Psr\Http\Message\ResponseInterface + */ + public function suspend(); + + /** + * Un-suspend a server on the daemon. + * + * @return \Psr\Http\Message\ResponseInterface + */ + public function unsuspend(); } diff --git a/app/Contracts/Repository/LocationRepositoryInterface.php b/app/Contracts/Repository/LocationRepositoryInterface.php index 9a52e298..a5d0c665 100644 --- a/app/Contracts/Repository/LocationRepositoryInterface.php +++ b/app/Contracts/Repository/LocationRepositoryInterface.php @@ -38,4 +38,21 @@ interface LocationRepositoryInterface extends RepositoryInterface, SearchableInt * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function deleteIfNoNodes($id); + + /** + * Return locations with a count of nodes and servers attached to it. + * + * @return mixed + */ + public function allWithDetails(); + + /** + * Return all of the nodes and their respective count of servers for a location. + * + * @param int $id + * @return mixed + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function getWithNodes($id); } diff --git a/app/Http/Controllers/Admin/LocationController.php b/app/Http/Controllers/Admin/LocationController.php index db358e5c..4c33368a 100644 --- a/app/Http/Controllers/Admin/LocationController.php +++ b/app/Http/Controllers/Admin/LocationController.php @@ -24,12 +24,13 @@ namespace Pterodactyl\Http\Controllers\Admin; +use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; use Pterodactyl\Models\Location; use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Http\Requests\Admin\LocationRequest; -use Pterodactyl\Services\Administrative\LocationService; +use Pterodactyl\Services\LocationService; class LocationController extends Controller { @@ -39,29 +40,29 @@ class LocationController extends Controller protected $alert; /** - * @var \Pterodactyl\Models\Location + * @var \Pterodactyl\Contracts\Repository\LocationRepositoryInterface */ - protected $locationModel; + protected $repository; /** - * @var \Pterodactyl\Services\Administrative\\LocationService + * @var \Pterodactyl\Services\\LocationService */ protected $service; /** * LocationController constructor. * - * @param \Prologue\Alerts\AlertsMessageBag $alert - * @param \Pterodactyl\Models\Location $locationModel - * @param \Pterodactyl\Services\Administrative\LocationService $service + * @param \Prologue\Alerts\AlertsMessageBag $alert + * @param \Pterodactyl\Contracts\Repository\LocationRepositoryInterface $repository + * @param \Pterodactyl\Services\LocationService $service */ public function __construct( AlertsMessageBag $alert, - Location $locationModel, + LocationRepositoryInterface $repository, LocationService $service ) { $this->alert = $alert; - $this->locationModel = $locationModel; + $this->repository = $repository; $this->service = $service; } @@ -73,21 +74,21 @@ class LocationController extends Controller public function index() { return view('admin.locations.index', [ - 'locations' => $this->locationModel->withCount('nodes', 'servers')->get(), + 'locations' => $this->repository->allWithDetails(), ]); } /** * Return the location view page. * - * @param \Pterodactyl\Models\Location $location + * @param int $id * @return \Illuminate\View\View */ - public function view(Location $location) + public function view($id) { - $location->load('nodes.servers'); - - return view('admin.locations.view', ['location' => $location]); + return view('admin.locations.view', [ + 'location' => $this->repository->getWithNodes($id), + ]); } /** @@ -132,7 +133,7 @@ class LocationController extends Controller /** * Delete a location from the system. * - * @param \Pterodactyl\Models\Location $location + * @param \Pterodactyl\Models\Location $location * @return \Illuminate\Http\RedirectResponse * * @throws \Exception diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php index 16747c01..5b7508d6 100644 --- a/app/Http/Controllers/Admin/ServersController.php +++ b/app/Http/Controllers/Admin/ServersController.php @@ -36,7 +36,6 @@ use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; use Pterodactyl\Http\Requests\Admin\ServerFormRequest; -use Pterodactyl\Models; use Pterodactyl\Models\Server; use Illuminate\Http\Request; use GuzzleHttp\Exception\TransferException; @@ -44,8 +43,12 @@ use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Repositories\Eloquent\DatabaseHostRepository; use Pterodactyl\Exceptions\DisplayValidationException; +use Pterodactyl\Services\Database\CreationService as DatabaseCreationService; +use Pterodactyl\Services\Servers\ContainerRebuildService; use Pterodactyl\Services\Servers\CreationService; use Pterodactyl\Services\Servers\DetailsModificationService; +use Pterodactyl\Services\Servers\ReinstallService; +use Pterodactyl\Services\Servers\SuspensionService; class ServersController extends Controller { @@ -64,6 +67,11 @@ class ServersController extends Controller */ protected $config; + /** + * @var \Pterodactyl\Services\Servers\ContainerRebuildService + */ + protected $containerRebuildService; + /** * @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface */ @@ -94,6 +102,11 @@ class ServersController extends Controller */ protected $nodeRepository; + /** + * @var \Pterodactyl\Services\Servers\ReinstallService + */ + protected $reinstallService; + /** * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface */ @@ -109,32 +122,62 @@ class ServersController extends Controller */ protected $serviceRepository; + /** + * @var \Pterodactyl\Services\Servers\SuspensionService + */ + protected $suspensionService; + + /** + * ServersController constructor. + * + * @param \Prologue\Alerts\AlertsMessageBag $alert + * @param \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface $allocationRepository + * @param \Illuminate\Contracts\Config\Repository $config + * @param \Pterodactyl\Services\Servers\ContainerRebuildService $containerRebuildService + * @param \Pterodactyl\Services\Servers\CreationService $service + * @param \Pterodactyl\Services\Database\CreationService $databaseCreationService + * @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $databaseRepository + * @param \Pterodactyl\Repositories\Eloquent\DatabaseHostRepository $databaseHostRepository + * @param \Pterodactyl\Services\Servers\DetailsModificationService $detailsModificationService + * @param \Pterodactyl\Contracts\Repository\LocationRepositoryInterface $locationRepository + * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $nodeRepository + * @param \Pterodactyl\Services\Servers\ReinstallService $reinstallService + * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository + * @param \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface $serviceRepository + * @param \Pterodactyl\Services\Servers\SuspensionService $suspensionService + */ public function __construct( AlertsMessageBag $alert, AllocationRepositoryInterface $allocationRepository, ConfigRepository $config, + ContainerRebuildService $containerRebuildService, CreationService $service, - \Pterodactyl\Services\Database\CreationService $databaseCreationService, + DatabaseCreationService $databaseCreationService, DatabaseRepositoryInterface $databaseRepository, DatabaseHostRepository $databaseHostRepository, DetailsModificationService $detailsModificationService, LocationRepositoryInterface $locationRepository, NodeRepositoryInterface $nodeRepository, + ReinstallService $reinstallService, ServerRepositoryInterface $repository, - ServiceRepositoryInterface $serviceRepository + ServiceRepositoryInterface $serviceRepository, + SuspensionService $suspensionService ) { $this->alert = $alert; $this->allocationRepository = $allocationRepository; $this->config = $config; + $this->containerRebuildService = $containerRebuildService; $this->databaseCreationService = $databaseCreationService; $this->databaseRepository = $databaseRepository; $this->databaseHostRepository = $databaseHostRepository; $this->detailsModificationService = $detailsModificationService; $this->locationRepository = $locationRepository; $this->nodeRepository = $nodeRepository; + $this->reinstallService = $reinstallService; $this->repository = $repository; $this->service = $service; $this->serviceRepository = $serviceRepository; + $this->suspensionService = $suspensionService; } /** @@ -192,7 +235,8 @@ class ServersController extends Controller return redirect()->route('admin.servers.view', $server->id); } catch (TransferException $ex) { Log::warning($ex); - Alert::danger('A TransferException was encountered while trying to contact the daemon, please ensure it is online and accessible. This error has been logged.')->flash(); + Alert::danger('A TransferException was encountered while trying to contact the daemon, please ensure it is online and accessible. This error has been logged.') + ->flash(); } return redirect()->route('admin.servers.new')->withInput(); @@ -375,47 +419,41 @@ class ServersController extends Controller /** * Toggles the install status for a server. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function toggleInstall(Request $request, $id) + public function toggleInstall(Server $server) { - $repo = new ServerRepository; - try { - $repo->toggleInstall($id); - - Alert::success('Server install status was successfully toggled.')->flash(); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An unhandled exception occured while attemping to toggle this servers status. This error has been logged.')->flash(); + if ($server->installed > 1) { + throw new DisplayException(trans('admin/server.exceptions.marked_as_failed')); } - return redirect()->route('admin.servers.view.manage', $id); + $this->repository->update($server->id, [ + 'installed' => ! $server->installed, + ]); + + $this->alert->success(trans('admin/server.alerts.install_toggled'))->flash(); + + return redirect()->route('admin.servers.view.manage', $server->id); } /** * Reinstalls the server with the currently assigned pack and service. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param int $id * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function reinstallServer(Request $request, $id) + public function reinstallServer($id) { - $repo = new ServerRepository; - try { - $repo->reinstall($id); - - Alert::success('Server successfully marked for reinstallation.')->flash(); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An unhandled exception occured while attemping to perform this reinstallation. This error has been logged.')->flash(); - } + $this->reinstallService->reinstall($id); + $this->alert->success(trans('admin/server.alerts.server_reinstalled'))->flash(); return redirect()->route('admin.servers.view.manage', $id); } @@ -423,60 +461,37 @@ class ServersController extends Controller /** * Setup a server to have a container rebuild. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\DisplayException */ - public function rebuildContainer(Request $request, $id) + public function rebuildContainer(Server $server) { - $server = Models\Server::with('node')->findOrFail($id); + $this->containerRebuildService->rebuild($server); + $this->alert->success(trans('admin/server.alerts.rebuild_on_boot'))->flash(); - try { - $server->node->guzzleClient([ - 'X-Access-Server' => $server->uuid, - 'X-Access-Token' => $server->node->daemonSecret, - ])->request('POST', '/server/rebuild'); - - Alert::success('A rebuild has been queued successfully. It will run the next time this server is booted.')->flash(); - } catch (TransferException $ex) { - Log::warning($ex); - Alert::danger('A TransferException was encountered while trying to contact the daemon, please ensure it is online and accessible. This error has been logged.')->flash(); - } - - return redirect()->route('admin.servers.view.manage', $id); + return redirect()->route('admin.servers.view.manage', $server->id); } /** * Manage the suspension status for a server. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function manageSuspension(Request $request, $id) + public function manageSuspension(Request $request, Server $server) { - $repo = new ServerRepository; - $action = $request->input('action'); + $this->suspensionService->toggle($server, $request->input('action')); + $this->alert->success(trans('admin/server.alerts.suspension_toggled', [ + 'status' => $request->input('action') . 'ed', + ]))->flash(); - if (! in_array($action, ['suspend', 'unsuspend'])) { - Alert::danger('Invalid action was passed to function.')->flash(); - - return redirect()->route('admin.servers.view.manage', $id); - } - - try { - $repo->toggleAccess($id, ($action === 'unsuspend')); - - Alert::success('Server has been ' . $action . 'ed.'); - } catch (TransferException $ex) { - Log::warning($ex); - Alert::danger('A TransferException was encountered while trying to contact the daemon, please ensure it is online and accessible. This error has been logged.')->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An unhandled exception occured while attemping to ' . $action . ' this server. This error has been logged.')->flash(); - } - - return redirect()->route('admin.servers.view.manage', $id); + return redirect()->route('admin.servers.view.manage', $server->id); } /** @@ -498,15 +513,20 @@ class ServersController extends Controller Alert::success('Server details were successfully updated.')->flash(); } catch (DisplayValidationException $ex) { - return redirect()->route('admin.servers.view.build', $id)->withErrors(json_decode($ex->getMessage()))->withInput(); + return redirect() + ->route('admin.servers.view.build', $id) + ->withErrors(json_decode($ex->getMessage())) + ->withInput(); } catch (DisplayException $ex) { Alert::danger($ex->getMessage())->flash(); } catch (TransferException $ex) { Log::warning($ex); - Alert::danger('A TransferException was encountered while trying to contact the daemon, please ensure it is online and accessible. This error has been logged.')->flash(); + Alert::danger('A TransferException was encountered while trying to contact the daemon, please ensure it is online and accessible. This error has been logged.') + ->flash(); } catch (\Exception $ex) { Log::error($ex); - Alert::danger('An unhandled exception occured while attemping to add this server. This error has been logged.')->flash(); + Alert::danger('An unhandled exception occured while attemping to add this server. This error has been logged.') + ->flash(); } return redirect()->route('admin.servers.view.build', $id); @@ -532,10 +552,12 @@ class ServersController extends Controller Alert::danger($ex->getMessage())->flash(); } catch (TransferException $ex) { Log::warning($ex); - Alert::danger('A TransferException occurred while attempting to delete this server from the daemon, please ensure it is running. This error has been logged.')->flash(); + Alert::danger('A TransferException occurred while attempting to delete this server from the daemon, please ensure it is running. This error has been logged.') + ->flash(); } catch (\Exception $ex) { Log::error($ex); - Alert::danger('An unhandled exception occured while attemping to delete this server. This error has been logged.')->flash(); + Alert::danger('An unhandled exception occured while attemping to delete this server. This error has been logged.') + ->flash(); } return redirect()->route('admin.servers.view.delete', $id); @@ -554,7 +576,8 @@ class ServersController extends Controller try { if ($repo->updateStartup($id, $request->except('_token'), true)) { - Alert::success('Service configuration successfully modfied for this server, reinstalling now.')->flash(); + Alert::success('Service configuration successfully modfied for this server, reinstalling now.') + ->flash(); return redirect()->route('admin.servers.view', $id); } else { @@ -566,10 +589,12 @@ class ServersController extends Controller Alert::danger($ex->getMessage())->flash(); } catch (TransferException $ex) { Log::warning($ex); - Alert::danger('A TransferException occurred while attempting to update the startup for this server, please ensure the daemon is running. This error has been logged.')->flash(); + Alert::danger('A TransferException occurred while attempting to update the startup for this server, please ensure the daemon is running. This error has been logged.') + ->flash(); } catch (\Exception $ex) { Log::error($ex); - Alert::danger('An unhandled exception occured while attemping to update startup variables for this server. This error has been logged.')->flash(); + Alert::danger('An unhandled exception occured while attemping to update startup variables for this server. This error has been logged.') + ->flash(); } return redirect()->route('admin.servers.view.startup', $id); diff --git a/app/Repositories/Daemon/ServerRepository.php b/app/Repositories/Daemon/ServerRepository.php index f398abfa..e3e197db 100644 --- a/app/Repositories/Daemon/ServerRepository.php +++ b/app/Repositories/Daemon/ServerRepository.php @@ -93,4 +93,36 @@ class ServerRepository extends BaseRepository implements ServerRepositoryInterfa 'json' => $data, ]); } + + /** + * {@inheritdoc} + */ + public function reinstall() + { + return $this->getHttpClient()->request('POST', '/server/reinstall'); + } + + /** + * {@inheritdoc} + */ + public function rebuild() + { + return $this->getHttpClient()->request('POST', '/server/rebuild'); + } + + /** + * {@inheritdoc} + */ + public function suspend() + { + return $this->getHttpClient()->request('POST', '/server/suspend'); + } + + /** + * {@inheritdoc} + */ + public function unsuspend() + { + return $this->getHttpClient()->request('POST', '/server/unsuspend'); + } } diff --git a/app/Repositories/Eloquent/LocationRepository.php b/app/Repositories/Eloquent/LocationRepository.php index 43e2e15d..50d40073 100644 --- a/app/Repositories/Eloquent/LocationRepository.php +++ b/app/Repositories/Eloquent/LocationRepository.php @@ -76,4 +76,26 @@ class LocationRepository extends EloquentRepository implements LocationRepositor return $location->delete(); } + + /** + * {@inheritdoc} + */ + public function allWithDetails() + { + return $this->getBuilder()->withCount('nodes', 'servers')->get($this->getColumns()); + } + + /** + * {@inheritdoc} + */ + public function getWithNodes($id) + { + $instance = $this->getBuilder()->with('nodes.servers')->find($id, $this->getColumns()); + + if (! $instance) { + throw new RecordNotFoundException(); + } + + return $instance; + } } diff --git a/app/Services/Servers/ContainerRebuildService.php b/app/Services/Servers/ContainerRebuildService.php new file mode 100644 index 00000000..4a022426 --- /dev/null +++ b/app/Services/Servers/ContainerRebuildService.php @@ -0,0 +1,80 @@ +. + * + * 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\Servers; + +use GuzzleHttp\Exception\RequestException; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Models\Server; + +class ContainerRebuildService +{ + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface + */ + protected $daemonServerRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $repository; + + /** + * ContainerRebuildService constructor. + * + * @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonServerRepository + * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository + */ + public function __construct( + DaemonServerRepositoryInterface $daemonServerRepository, + ServerRepositoryInterface $repository + ) { + $this->daemonServerRepository = $daemonServerRepository; + $this->repository = $repository; + } + + /** + * Mark a server for rebuild on next boot cycle. + * + * @param int|\Pterodactyl\Models\Server $server + * + * @throws \Pterodactyl\Exceptions\DisplayException + */ + public function rebuild($server) + { + if (! $server instanceof Server) { + $server = $this->repository->find($server); + } + + try { + $this->daemonServerRepository->setNode($server->node_id)->setAccessServer($server->uuid)->rebuild(); + } catch (RequestException $exception) { + throw new DisplayException(trans('admin/server.exceptions.daemon_exception', [ + 'code' => $exception->getResponse()->getStatusCode(), + ])); + } + } +} diff --git a/app/Services/Servers/DetailsModificationService.php b/app/Services/Servers/DetailsModificationService.php index f1fb3d27..ea275970 100644 --- a/app/Services/Servers/DetailsModificationService.php +++ b/app/Services/Servers/DetailsModificationService.php @@ -146,7 +146,7 @@ class DetailsModificationService ], ]); - return $this->database->commit(); + $this->database->commit(); } catch (RequestException $exception) { throw new DisplayException(trans('admin/server.exceptions.daemon_exception', [ 'code' => $exception->getResponse()->getStatusCode(), diff --git a/app/Services/Servers/ReinstallService.php b/app/Services/Servers/ReinstallService.php new file mode 100644 index 00000000..b99fafde --- /dev/null +++ b/app/Services/Servers/ReinstallService.php @@ -0,0 +1,94 @@ +. + * + * 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\Servers; + +use GuzzleHttp\Exception\RequestException; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Models\Server; +use Illuminate\Database\ConnectionInterface; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; + +class ReinstallService +{ + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface + */ + protected $daemonServerRepository; + + /** + * @var \Illuminate\Database\ConnectionInterface + */ + protected $database; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $repository; + + /** + * ReinstallService constructor. + * + * @param \Illuminate\Database\ConnectionInterface $database + * @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonServerRepository + * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository + */ + public function __construct( + ConnectionInterface $database, + DaemonServerRepositoryInterface $daemonServerRepository, + ServerRepositoryInterface $repository + ) { + $this->daemonServerRepository = $daemonServerRepository; + $this->database = $database; + $this->repository = $repository; + } + + /** + * @param int|\Pterodactyl\Models\Server $server + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function reinstall($server) + { + if (! $server instanceof Server) { + $server = $this->repository->find($server); + } + + $this->database->beginTransaction(); + $this->repository->withoutFresh()->update($server->id, [ + 'installed' => 0, + ]); + + try { + $this->daemonServerRepository->setNode($server->node_id)->setAccessServer($server->uuid)->reinstall(); + $this->database->commit(); + } catch (RequestException $exception) { + throw new DisplayException(trans('admin/server.exceptions.daemon_exception', [ + 'code' => $exception->getResponse()->getStatusCode(), + ])); + } + } +} diff --git a/app/Services/Servers/SuspensionService.php b/app/Services/Servers/SuspensionService.php new file mode 100644 index 00000000..b272d06d --- /dev/null +++ b/app/Services/Servers/SuspensionService.php @@ -0,0 +1,114 @@ +. + * + * 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\Servers; + +use GuzzleHttp\Exception\RequestException; +use Illuminate\Database\ConnectionInterface; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Models\Server; + +class SuspensionService +{ + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface + */ + protected $daemonServerRepository; + + /** + * @var \Illuminate\Database\ConnectionInterface + */ + protected $database; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $repository; + + /** + * SuspensionService constructor. + * + * @param \Illuminate\Database\ConnectionInterface $database + * @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonServerRepository + * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository + */ + public function __construct( + ConnectionInterface $database, + DaemonServerRepositoryInterface $daemonServerRepository, + ServerRepositoryInterface $repository + ) { + $this->daemonServerRepository = $daemonServerRepository; + $this->database = $database; + $this->repository = $repository; + } + + /** + * Suspends a server on the system. + * + * @param int|\Pterodactyl\Models\Server $server + * @param string $action + * @return bool + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function toggle($server, $action = 'suspend') + { + if (! $server instanceof Server) { + $server = $this->repository->find($server); + } + + if (! in_array($action, ['suspend', 'unsuspend'])) { + throw new \InvalidArgumentException(sprintf( + 'Action must be either suspend or unsuspend, %s passed.', $action + )); + } + + if ( + $action === 'suspend' && $server->suspended || + $action === 'unsuspend' && ! $server->suspended + ) { + return true; + } + + $this->database->beginTransaction(); + $this->repository->withoutFresh()->update($server->id, [ + 'suspended' => $action === 'suspend', + ]); + + try { + $this->daemonServerRepository->setNode($server->node_id)->setAccessServer($server->uuid)->$action(); + $this->database->commit(); + + return true; + } catch (RequestException $exception) { + throw new DisplayException(trans('admin/server.exceptions.daemon_exception', [ + 'code' => $exception->getResponse()->getStatusCode(), + ])); + } + } +} diff --git a/resources/lang/en/admin/server.php b/resources/lang/en/admin/server.php index 6ded5880..9b977ece 100644 --- a/resources/lang/en/admin/server.php +++ b/resources/lang/en/admin/server.php @@ -24,10 +24,15 @@ return [ 'exceptions' => [ + 'marked_as_failed' => 'This server was marked as having failed a previous installation. Current status cannot be toggled in this state.', 'bad_variable' => 'There was a validation error with the :name variable.', 'daemon_exception' => 'There was an exception while attempting to communicate with the daemon resulting in a HTTP/:code response code. This exception has been logged.', ], 'alerts' => [ + 'suspension_toggled' => 'Server suspension status has been changed to :status.', + 'rebuild_on_boot' => 'This server has been marked as requiring a Docker Container rebuild. This will happen the next time the server is started.', + 'install_toggled' => 'The installation status for this server has been toggled.', + 'server_reinstalled' => 'This server has been queued for a reinstallation beginning now.', 'details_updated' => 'Server details have been successfully updated.', 'docker_image_updated' => 'Successfully changed the default Docker image to use for this server. A reboot is required to apply this change.', ], diff --git a/routes/admin.php b/routes/admin.php index 38785ee9..157109c1 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -100,30 +100,30 @@ Route::group(['prefix' => 'users'], function () { Route::group(['prefix' => 'servers'], function () { Route::get('/', 'ServersController@index')->name('admin.servers'); Route::get('/new', 'ServersController@create')->name('admin.servers.new'); - Route::get('/view/{id}', 'ServersController@viewIndex')->name('admin.servers.view'); - Route::get('/view/{id}/details', 'ServersController@viewDetails')->name('admin.servers.view.details'); - Route::get('/view/{id}/build', 'ServersController@viewBuild')->name('admin.servers.view.build'); - Route::get('/view/{id}/startup', 'ServersController@viewStartup')->name('admin.servers.view.startup'); - Route::get('/view/{id}/database', 'ServersController@viewDatabase')->name('admin.servers.view.database'); - Route::get('/view/{id}/manage', 'ServersController@viewManage')->name('admin.servers.view.manage'); - Route::get('/view/{id}/delete', 'ServersController@viewDelete')->name('admin.servers.view.delete'); + Route::get('/view/{server}', 'ServersController@viewIndex')->name('admin.servers.view'); + Route::get('/view/{server}/details', 'ServersController@viewDetails')->name('admin.servers.view.details'); + Route::get('/view/{server}/build', 'ServersController@viewBuild')->name('admin.servers.view.build'); + Route::get('/view/{server}/startup', 'ServersController@viewStartup')->name('admin.servers.view.startup'); + Route::get('/view/{server}/database', 'ServersController@viewDatabase')->name('admin.servers.view.database'); + Route::get('/view/{server}/manage', 'ServersController@viewManage')->name('admin.servers.view.manage'); + Route::get('/view/{server}/delete', 'ServersController@viewDelete')->name('admin.servers.view.delete'); Route::post('/new', 'ServersController@store'); Route::post('/new/nodes', 'ServersController@nodes')->name('admin.servers.new.nodes'); - Route::post('/view/{id}/build', 'ServersController@updateBuild'); - Route::post('/view/{id}/startup', 'ServersController@saveStartup'); - Route::post('/view/{id}/database', 'ServersController@newDatabase'); - Route::post('/view/{id}/manage/toggle', 'ServersController@toggleInstall')->name('admin.servers.view.manage.toggle'); - Route::post('/view/{id}/manage/rebuild', 'ServersController@rebuildContainer')->name('admin.servers.view.manage.rebuild'); - Route::post('/view/{id}/manage/suspension', 'ServersController@manageSuspension')->name('admin.servers.view.manage.suspension'); - Route::post('/view/{id}/manage/reinstall', 'ServersController@reinstallServer')->name('admin.servers.view.manage.reinstall'); - Route::post('/view/{id}/delete', 'ServersController@delete'); + Route::post('/view/{server}/build', 'ServersController@updateBuild'); + Route::post('/view/{server}/startup', 'ServersController@saveStartup'); + Route::post('/view/{server}/database', 'ServersController@newDatabase'); + Route::post('/view/{server}/manage/toggle', 'ServersController@toggleInstall')->name('admin.servers.view.manage.toggle'); + Route::post('/view/{server}/manage/rebuild', 'ServersController@rebuildContainer')->name('admin.servers.view.manage.rebuild'); + Route::post('/view/{server}/manage/suspension', 'ServersController@manageSuspension')->name('admin.servers.view.manage.suspension'); + Route::post('/view/{server}/manage/reinstall', 'ServersController@reinstallServer')->name('admin.servers.view.manage.reinstall'); + Route::post('/view/{server}/delete', 'ServersController@delete'); Route::patch('/view/{server}/details', 'ServersController@setDetails'); Route::patch('/view/{server}/details/container', 'ServersController@setContainer')->name('admin.servers.view.details.container'); - Route::patch('/view/{id}/database', 'ServersController@resetDatabasePassword'); + Route::patch('/view/{server}/database', 'ServersController@resetDatabasePassword'); - Route::delete('/view/{id}/database/{database}/delete', 'ServersController@deleteDatabase')->name('admin.servers.view.database.delete'); + Route::delete('/view/{server}/database/{database}/delete', 'ServersController@deleteDatabase')->name('admin.servers.view.database.delete'); }); /* diff --git a/tests/Unit/Services/Servers/ContainerRebuildServiceTest.php b/tests/Unit/Services/Servers/ContainerRebuildServiceTest.php new file mode 100644 index 00000000..2ccd487f --- /dev/null +++ b/tests/Unit/Services/Servers/ContainerRebuildServiceTest.php @@ -0,0 +1,139 @@ +. + * + * 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\Servers; + +use Exception; +use GuzzleHttp\Exception\RequestException; +use Mockery as m; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Models\Server; +use Pterodactyl\Services\Servers\ContainerRebuildService; +use Tests\TestCase; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; + +class ContainerRebuildServiceTest extends TestCase +{ + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface + */ + protected $daemonServerRepository; + + /** + * @var \GuzzleHttp\Exception\RequestException + */ + protected $exception; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Models\Server + */ + protected $server; + + /** + * @var \Pterodactyl\Services\Servers\ContainerRebuildService + */ + protected $service; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->daemonServerRepository = m::mock(DaemonServerRepositoryInterface::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 ContainerRebuildService($this->daemonServerRepository, $this->repository); + } + + /** + * Test that a server is marked for rebuild when it's model is passed to the function. + */ + public function testServerShouldBeMarkedForARebuildWhenModelIsPassed() + { + $this->repository->shouldNotReceive('find'); + $this->daemonServerRepository->shouldReceive('setNode')->with($this->server->node_id)->once()->andReturnSelf() + ->shouldReceive('setAccessServer')->with($this->server->uuid)->once()->andReturnSelf() + ->shouldReceive('rebuild')->withNoArgs()->once()->andReturnNull(); + + $this->service->rebuild($this->server); + } + + /** + * Test that a server is marked for rebuild when the ID of the server is passed to the function. + */ + public function testServerShouldBeMarkedForARebuildWhenServerIdIsPassed() + { + $this->repository->shouldReceive('find')->with($this->server->id)->once()->andReturn($this->server); + + $this->daemonServerRepository->shouldReceive('setNode')->with($this->server->node_id)->once()->andReturnSelf() + ->shouldReceive('setAccessServer')->with($this->server->uuid)->once()->andReturnSelf() + ->shouldReceive('rebuild')->withNoArgs()->once()->andReturnNull(); + + $this->service->rebuild($this->server->id); + } + + /** + * Test that an exception thrown by guzzle is rendered as a displayable exception. + */ + public function testExceptionThrownByGuzzleShouldBeReRenderedAsDisplayable() + { + $this->daemonServerRepository->shouldReceive('setNode')->with($this->server->node_id) + ->once()->andThrow($this->exception); + + $this->exception->shouldReceive('getResponse')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('getStatusCode')->withNoArgs()->once()->andReturn(400); + + try { + $this->service->rebuild($this->server); + } catch (Exception $exception) { + $this->assertInstanceOf(DisplayException::class, $exception); + $this->assertEquals( + trans('admin/server.exceptions.daemon_exception', ['code' => 400,]), $exception->getMessage() + ); + } + } + + /** + * Test that an exception thrown by something other than guzzle is not transformed to a displayable. + * + * @expectedException \Exception + */ + public function testExceptionNotThrownByGuzzleShouldNotBeTransformedToDisplayable() + { + $this->daemonServerRepository->shouldReceive('setNode')->with($this->server->node_id) + ->once()->andThrow(new Exception()); + + $this->service->rebuild($this->server); + } +} diff --git a/tests/Unit/Services/Servers/DetailsModificationServiceTest.php b/tests/Unit/Services/Servers/DetailsModificationServiceTest.php index 3ed8208a..581b5e6d 100644 --- a/tests/Unit/Services/Servers/DetailsModificationServiceTest.php +++ b/tests/Unit/Services/Servers/DetailsModificationServiceTest.php @@ -310,8 +310,7 @@ class DetailsModificationServiceTest extends TestCase $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturn(true); - $response = $this->service->setDockerImage($server, 'new/image'); - $this->assertTrue($response); + $this->service->setDockerImage($server, 'new/image'); } /** @@ -338,8 +337,7 @@ class DetailsModificationServiceTest extends TestCase $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturn(true); - $response = $this->service->setDockerImage($server->id, 'new/image'); - $this->assertTrue($response); + $this->service->setDockerImage($server->id, 'new/image'); } /** diff --git a/tests/Unit/Services/Servers/ReinstallServiceTest.php b/tests/Unit/Services/Servers/ReinstallServiceTest.php new file mode 100644 index 00000000..471ff3f9 --- /dev/null +++ b/tests/Unit/Services/Servers/ReinstallServiceTest.php @@ -0,0 +1,177 @@ +. + * + * 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\Servers; + +use Exception; +use GuzzleHttp\Exception\RequestException; +use Illuminate\Database\ConnectionInterface; +use Mockery as m; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Models\Server; +use Pterodactyl\Services\Servers\ReinstallService; +use Tests\TestCase; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; + +class ReinstallServiceTest extends TestCase +{ + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface + */ + protected $daemonServerRepository; + + /** + * @var \Illuminate\Database\ConnectionInterface + */ + protected $database; + + /** + * @var \GuzzleHttp\Exception\RequestException + */ + protected $exception; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Models\Server + */ + protected $server; + + /** + * @var \Pterodactyl\Services\Servers\ReinstallService + */ + protected $service; + + /** + * Setup tests. + */ + public function setUp() + { + 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 ReinstallService( + $this->database, + $this->daemonServerRepository, + $this->repository + ); + } + + /** + * Test that a server is reinstalled when it's model is passed to the function. + */ + public function testServerShouldBeReinstalledWhenModelIsPassed() + { + $this->repository->shouldNotReceive('find'); + + $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($this->server->id, [ + 'installed' => 0, + ])->once()->andReturnNull(); + + $this->daemonServerRepository->shouldReceive('setNode')->with($this->server->node_id)->once()->andReturnSelf() + ->shouldReceive('setAccessServer')->with($this->server->uuid)->once()->andReturnSelf() + ->shouldReceive('reinstall')->withNoArgs()->once()->andReturnNull(); + $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $this->service->reinstall($this->server); + } + + /** + * Test that a server is reinstalled when the ID of the server is passed to the function. + */ + public function testServerShouldBeReinstalledWhenServerIdIsPassed() + { + $this->repository->shouldReceive('find')->with($this->server->id)->once()->andReturn($this->server); + + $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($this->server->id, [ + 'installed' => 0, + ])->once()->andReturnNull(); + + $this->daemonServerRepository->shouldReceive('setNode')->with($this->server->node_id)->once()->andReturnSelf() + ->shouldReceive('setAccessServer')->with($this->server->uuid)->once()->andReturnSelf() + ->shouldReceive('reinstall')->withNoArgs()->once()->andReturnNull(); + $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. + */ + public function testExceptionThrownByGuzzleShouldBeReRenderedAsDisplayable() + { + $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($this->server->id, [ + 'installed' => 0, + ])->once()->andReturnNull(); + + $this->daemonServerRepository->shouldReceive('setNode')->with($this->server->node_id) + ->once()->andThrow($this->exception); + + $this->exception->shouldReceive('getResponse')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('getStatusCode')->withNoArgs()->once()->andReturn(400); + + try { + $this->service->reinstall($this->server); + } catch (Exception $exception) { + $this->assertInstanceOf(DisplayException::class, $exception); + $this->assertEquals( + trans('admin/server.exceptions.daemon_exception', ['code' => 400,]), $exception->getMessage() + ); + } + } + + /** + * 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('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($this->server->id, [ + 'installed' => 0, + ])->once()->andReturnNull(); + + $this->daemonServerRepository->shouldReceive('setNode')->with($this->server->node_id) + ->once()->andThrow(new Exception()); + + $this->service->reinstall($this->server); + } +} diff --git a/tests/Unit/Services/Servers/SuspensionServiceTest.php b/tests/Unit/Services/Servers/SuspensionServiceTest.php new file mode 100644 index 00000000..ff8beeb1 --- /dev/null +++ b/tests/Unit/Services/Servers/SuspensionServiceTest.php @@ -0,0 +1,198 @@ +. + * + * 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\Servers; + +use Exception; +use GuzzleHttp\Exception\RequestException; +use Illuminate\Database\ConnectionInterface; +use Mockery as m; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Models\Server; +use Pterodactyl\Services\Servers\SuspensionService; +use Tests\TestCase; + +class SuspensionServiceTest extends TestCase +{ + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface + */ + protected $daemonServerRepository; + + /** + * @var \Illuminate\Database\ConnectionInterface + */ + protected $database; + + /** + * @var \GuzzleHttp\Exception\RequestException + */ + protected $exception; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Models\Server + */ + protected $server; + + /** + * @var \Pterodactyl\Services\Servers\SuspensionService + */ + protected $service; + + /** + * Setup tests. + */ + public function setUp() + { + 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(['suspended' => 0, 'node_id' => 1]); + + $this->service = new SuspensionService( + $this->database, + $this->daemonServerRepository, + $this->repository + ); + } + + /** + * Test that the function accepts an integer in place of the server model. + * + * @expectedException \Exception + */ + public function testFunctionShouldAcceptAnIntegerInPlaceOfAServerModel() + { + $this->repository->shouldReceive('find')->with($this->server->id)->once()->andThrow(new Exception()); + + $this->service->toggle($this->server->id); + } + + /** + * Test that no action being passed suspends a server. + */ + public function testServerShouldBeSuspendedWhenNoActionIsPassed() + { + $this->server->suspended = 0; + + $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($this->server->id, ['suspended' => true])->once()->andReturnNull(); + + $this->daemonServerRepository->shouldReceive('setNode')->with($this->server->node_id)->once()->andReturnSelf() + ->shouldReceive('setAccessServer')->with($this->server->uuid)->once()->andReturnSelf() + ->shouldReceive('suspend')->withNoArgs()->once()->andReturnNull(); + $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $this->assertTrue($this->service->toggle($this->server)); + } + + + /** + * Test that server is unsuspended if action=unsuspend + */ + public function testServerShouldBeUnsuspendedWhenUnsuspendActionIsPassed() + { + $this->server->suspended = 1; + + $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($this->server->id, ['suspended' => false])->once()->andReturnNull(); + + $this->daemonServerRepository->shouldReceive('setNode')->with($this->server->node_id)->once()->andReturnSelf() + ->shouldReceive('setAccessServer')->with($this->server->uuid)->once()->andReturnSelf() + ->shouldReceive('unsuspend')->withNoArgs()->once()->andReturnNull(); + $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $this->assertTrue($this->service->toggle($this->server, 'unsuspend')); + } + + /** + * Test that nothing happens if a server is already unsuspended and action=unsuspend + */ + public function testNoActionShouldHappenIfServerIsAlreadyUnsuspendedAndActionIsUnsuspend() + { + $this->server->suspended = 0; + + $this->assertTrue($this->service->toggle($this->server, 'unsuspend')); + } + + /** + * Test that nothing happens if a server is already suspended and action=suspend + */ + public function testNoActionShouldHappenIfServerIsAlreadySuspendedAndActionIsSuspend() + { + $this->server->suspended = 1; + + $this->assertTrue($this->service->toggle($this->server, 'suspend')); + } + + /** + * Test that an exception thrown by Guzzle is caught and transformed to a displayable exception. + */ + public function testExceptionThrownByGuzzleShouldBeCaughtAndTransformedToDisplayable() + { + $this->server->suspended = 0; + + $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($this->server->id, ['suspended' => true])->once()->andReturnNull(); + + $this->daemonServerRepository->shouldReceive('setNode')->with($this->server->node_id) + ->once()->andThrow($this->exception); + + $this->exception->shouldReceive('getResponse')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('getStatusCode')->withNoArgs()->once()->andReturn(400); + + try { + $this->service->toggle($this->server); + } catch (Exception $exception) { + $this->assertInstanceOf(DisplayException::class, $exception); + $this->assertEquals( + trans('admin/server.exceptions.daemon_exception', ['code' => 400,]), $exception->getMessage() + ); + } + } + + /** + * Test that if action is not suspend or unsuspend an exception is thrown. + * + * @expectedException \InvalidArgumentException + */ + public function testExceptionShouldBeThrownIfActionIsNotValid() + { + $this->service->toggle($this->server, 'random'); + } +} From 7f0130100db8b0d1b042f0037554e1954d8ecd7f Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 23 Jul 2017 15:09:25 -0500 Subject: [PATCH 32/99] Fix routes file --- .../Controllers/Admin/ServersController.php | 84 +++++++++---------- 1 file changed, 38 insertions(+), 46 deletions(-) diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php index 5b7508d6..ca5b862d 100644 --- a/app/Http/Controllers/Admin/ServersController.php +++ b/app/Http/Controllers/Admin/ServersController.php @@ -256,27 +256,25 @@ class ServersController extends Controller /** * Display the index when viewing a specific server. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Pterodactyl\Models\Server $server * @return \Illuminate\View\View */ - public function viewIndex(Request $request, $id) + public function viewIndex(Server $server) { - return view('admin.servers.view.index', ['server' => $this->repository->find($id)]); + return view('admin.servers.view.index', ['server' => $server]); } /** * Display the details page when viewing a specific server. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param int $server * @return \Illuminate\View\View */ - public function viewDetails(Request $request, $id) + public function viewDetails($server) { return view('admin.servers.view.details', [ 'server' => $this->repository->findFirstWhere([ - ['id', '=', $id], + ['id', '=', $server], ['installed', '=', 1], ]), ]); @@ -285,14 +283,13 @@ class ServersController extends Controller /** * Display the build details page when viewing a specific server. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param int $server * @return \Illuminate\View\View */ - public function viewBuild(Request $request, $id) + public function viewBuild($server) { $server = $this->repository->findFirstWhere([ - ['id', '=', $id], + ['id', '=', $server], ['installed', '=', 1], ]); @@ -308,13 +305,12 @@ class ServersController extends Controller /** * Display startup configuration page for a server. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param int $server * @return \Illuminate\View\View */ - public function viewStartup(Request $request, $id) + public function viewStartup($server) { - $parameters = $this->repository->getVariablesWithValues($id, true); + $parameters = $this->repository->getVariablesWithValues($server, true); if (! $parameters->server->installed) { abort(404); } @@ -339,13 +335,12 @@ class ServersController extends Controller /** * Display the database management page for a specific server. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param int $server * @return \Illuminate\View\View */ - public function viewDatabase(Request $request, $id) + public function viewDatabase($server) { - $server = $this->repository->getWithDatabases($id); + $server = $this->repository->getWithDatabases($server); return view('admin.servers.view.database', [ 'hosts' => $this->databaseHostRepository->all(), @@ -356,32 +351,30 @@ class ServersController extends Controller /** * Display the management page when viewing a specific server. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Pterodactyl\Models\Server $server * @return \Illuminate\View\View */ - public function viewManage(Request $request, $id) + public function viewManage(Server $server) { - return view('admin.servers.view.manage', ['server' => $this->repository->find($id)]); + return view('admin.servers.view.manage', ['server' => $server]); } /** * Display the deletion page for a server. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Pterodactyl\Models\Server $server * @return \Illuminate\View\View */ - public function viewDelete(Request $request, $id) + public function viewDelete(Server $server) { - return view('admin.servers.view.delete', ['server' => $this->repository->find($id)]); + return view('admin.servers.view.delete', ['server' => $server]); } /** * Update the details for a server. * - * @param \Illuminate\Http\Request $request - * @param \Pterodactyl\Models\Server $server + * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\DisplayException @@ -443,19 +436,19 @@ class ServersController extends Controller /** * Reinstalls the server with the currently assigned pack and service. * - * @param int $id + * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function reinstallServer($id) + public function reinstallServer(Server $server) { - $this->reinstallService->reinstall($id); + $this->reinstallService->reinstall($server); $this->alert->success(trans('admin/server.alerts.server_reinstalled'))->flash(); - return redirect()->route('admin.servers.view.manage', $id); + return redirect()->route('admin.servers.view.manage', $server->id); } /** @@ -604,38 +597,38 @@ class ServersController extends Controller * Creates a new database assigned to a specific server. * * @param \Illuminate\Http\Request $request - * @param int $id + * @param int $server * @return \Illuminate\Http\RedirectResponse * * @throws \Exception * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function newDatabase(Request $request, $id) + public function newDatabase(Request $request, $server) { - $this->databaseCreationService->create($id, [ + $this->databaseCreationService->create($server, [ 'database' => $request->input('database'), 'remote' => $request->input('remote'), 'database_host_id' => $request->input('database_host_id'), ]); - return redirect()->route('admin.servers.view.database', $id)->withInput(); + return redirect()->route('admin.servers.view.database', $server)->withInput(); } /** * Resets the database password for a specific database on this server. * * @param \Illuminate\Http\Request $request - * @param int $id + * @param int $server * @return \Illuminate\Http\RedirectResponse * * @throws \Exception * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function resetDatabasePassword(Request $request, $id) + public function resetDatabasePassword(Request $request, $server) { $database = $this->databaseRepository->findFirstWhere([ - ['server_id', '=', $id], + ['server_id', '=', $server], ['id', '=', $request->input('database')], ]); @@ -647,18 +640,17 @@ class ServersController extends Controller /** * Deletes a database from a server. * - * @param \Illuminate\Http\Request $request - * @param int $id - * @param int $database + * @param int $server + * @param int $database * @return \Illuminate\Http\RedirectResponse * * @throws \Exception * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function deleteDatabase(Request $request, $id, $database) + public function deleteDatabase($server, $database) { $database = $this->databaseRepository->findFirstWhere([ - ['server_id', '=', $id], + ['server_id', '=', $server], ['id', '=', $database], ]); From ebb3a01036d079083f93b46a7a3907941817055a Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 23 Jul 2017 17:55:38 -0500 Subject: [PATCH 33/99] Should fix failing travis builds --- .env.example | 4 ---- .env.travis | 15 +++++++++------ .travis.yml | 5 ++--- app/Providers/AppServiceProvider.php | 5 ++++- config/database.php | 13 ------------- phpunit.xml | 5 ++++- 6 files changed, 19 insertions(+), 28 deletions(-) diff --git a/.env.example b/.env.example index fa7e2096..c7a972be 100644 --- a/.env.example +++ b/.env.example @@ -24,10 +24,6 @@ MAIL_PASSWORD=null MAIL_ENCRYPTION=null MAIL_FROM=you@example.com -API_PREFIX=api -API_VERSION=v1 -API_DEBUG=false - QUEUE_DRIVER=database QUEUE_HIGH=high QUEUE_STANDARD=standard diff --git a/.env.travis b/.env.travis index cd94ca8e..1b6ed1af 100644 --- a/.env.travis +++ b/.env.travis @@ -1,13 +1,16 @@ APP_ENV=testing APP_DEBUG=true -APP_KEY=aaaaabbbbbcccccdddddeeeeefffff12 +APP_KEY=SomeRandomString3232RandomString +APP_THEME=pterodactyl +APP_TIMEZONE=UTC +APP_URL=http://localhost/ -TEST_DB_CONNECTION=tests -TEST_DB_TEST_USERNAME=root -TEST_DB_TEST_PASSWORD= -TEST_DB_HOST=127.0.0.1 +DB_HOST=127.0.0.1 +DB_DATABASE=travis +DB_USERNAME=root +DB_PASSWORD="" CACHE_DRIVER=array SESSION_DRIVER=array -QUEUE_DRIVER=sync MAIL_DRIVER=array +QUEUE_DRIVER=sync diff --git a/.travis.yml b/.travis.yml index a5635548..788969b9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,9 +14,8 @@ before_install: before_script: - phpenv config-rm xdebug.ini - cp .env.travis .env - - composer install --no-interaction --prefer-dist --no-suggest --no-scripts --verbose - - php artisan migrate -v - - php artisan db:seed -v + - composer install --no-interaction --prefer-dist --no-suggest --verbose + - php artisan migrate --seed -v script: - vendor/bin/phpunit --coverage-clover coverage.xml notifications: diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 03fafc49..9bb72d1f 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -73,7 +73,10 @@ class AppServiceProvider extends ServiceProvider return Cache::remember('git-version', 5, function () { if (file_exists(base_path('.git/HEAD'))) { $head = explode(' ', file_get_contents(base_path('.git/HEAD'))); - $path = base_path('.git/' . trim($head[1])); + + if (array_key_exists(1, $head)) { + $path = base_path('.git/' . trim($head[1])); + } } if (isset($path) && file_exists($path)) { diff --git a/config/database.php b/config/database.php index 01fa1623..58324a0b 100644 --- a/config/database.php +++ b/config/database.php @@ -44,19 +44,6 @@ return [ 'prefix' => '', 'strict' => false, ], - - 'tests' => [ - 'driver' => 'mysql', - 'host' => env('TEST_DB_HOST', 'localhost'), - 'port' => env('TEST_DB_PORT', '3306'), - 'database' => env('TEST_DB_DATABASE', 'travis'), - 'username' => env('TEST_DB_USERNAME', 'root'), - 'password' => env('TEST_DB_PASSWORD', ''), - 'charset' => 'utf8', - 'collation' => 'utf8_unicode_ci', - 'prefix' => '', - 'strict' => false, - ], ], /* diff --git a/phpunit.xml b/phpunit.xml index ed342074..ceb832d0 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -24,7 +24,10 @@ - + + + + From fe7a6fb977252293093e59284764f253277cdb2e Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 23 Jul 2017 18:05:03 -0500 Subject: [PATCH 34/99] Fix code coverage --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 788969b9..e02b7a0e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,4 +21,4 @@ script: notifications: email: false after_success: - - codecov + - bash <(curl -s https://codecov.io/bash) From ace70a3599197b7befc30e347a3a8872ede02107 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 23 Jul 2017 18:09:19 -0500 Subject: [PATCH 35/99] Should probably leave xdebug seeing as we use it for code coverage. :thumbsup: --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e02b7a0e..2e93a545 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ services: before_install: - mysql -e 'CREATE DATABASE IF NOT EXISTS travis;' before_script: - - phpenv config-rm xdebug.ini +# - phpenv config-rm xdebug.ini - cp .env.travis .env - composer install --no-interaction --prefer-dist --no-suggest --verbose - php artisan migrate --seed -v From f842aae3d31ebe87a5ec0bb6eac6fbfa97f96484 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 23 Jul 2017 19:57:43 -0500 Subject: [PATCH 36/99] Add build modification settings, fix exception handling to log to file --- .../Repository/RepositoryInterface.php | 13 +- .../Controllers/Admin/ServersController.php | 46 ++-- .../Eloquent/EloquentRepository.php | 8 + .../Servers/BuildModificationService.php | 244 ++++++++++++++++++ .../Servers/ContainerRebuildService.php | 16 +- app/Services/Servers/CreationService.php | 26 +- .../Servers/DetailsModificationService.php | 22 +- app/Services/Servers/ReinstallService.php | 16 +- app/Services/Servers/SuspensionService.php | 16 +- resources/lang/en/admin/server.php | 3 + .../Servers/ContainerRebuildServiceTest.php | 16 +- .../Services/Servers/CreationServiceTest.php | 10 +- .../DetailsModificationServiceTest.php | 16 +- .../Services/Servers/ReinstallServiceTest.php | 13 +- .../Servers/SuspensionServiceTest.php | 12 +- 15 files changed, 427 insertions(+), 50 deletions(-) create mode 100644 app/Services/Servers/BuildModificationService.php diff --git a/app/Contracts/Repository/RepositoryInterface.php b/app/Contracts/Repository/RepositoryInterface.php index b3226095..cb8241e3 100644 --- a/app/Contracts/Repository/RepositoryInterface.php +++ b/app/Contracts/Repository/RepositoryInterface.php @@ -83,7 +83,7 @@ interface RepositoryInterface /** * Delete a given record from the database. * - * @param int $id + * @param int $id * @return bool|null */ public function delete($id); @@ -130,6 +130,17 @@ interface RepositoryInterface */ public function update($id, array $fields, $validate = true, $force = false); + /** + * Perform a mass update where matching records are updated using whereIn. + * This does not perform any model data validation. + * + * @param string $column + * @param array $values + * @param array $fields + * @return int + */ + public function updateWhereIn($column, array $values, array $fields); + /** * Update multiple records matching the passed clauses. * diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php index ca5b862d..851cd287 100644 --- a/app/Http/Controllers/Admin/ServersController.php +++ b/app/Http/Controllers/Admin/ServersController.php @@ -44,6 +44,7 @@ use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Repositories\Eloquent\DatabaseHostRepository; use Pterodactyl\Exceptions\DisplayValidationException; use Pterodactyl\Services\Database\CreationService as DatabaseCreationService; +use Pterodactyl\Services\Servers\BuildModificationService; use Pterodactyl\Services\Servers\ContainerRebuildService; use Pterodactyl\Services\Servers\CreationService; use Pterodactyl\Services\Servers\DetailsModificationService; @@ -62,6 +63,11 @@ class ServersController extends Controller */ protected $allocationRepository; + /** + * @var \Pterodactyl\Services\Servers\BuildModificationService + */ + protected $buildModificationService; + /** * @var \Illuminate\Contracts\Config\Repository */ @@ -132,6 +138,7 @@ class ServersController extends Controller * * @param \Prologue\Alerts\AlertsMessageBag $alert * @param \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface $allocationRepository + * @param \Pterodactyl\Services\Servers\BuildModificationService $buildModificationService * @param \Illuminate\Contracts\Config\Repository $config * @param \Pterodactyl\Services\Servers\ContainerRebuildService $containerRebuildService * @param \Pterodactyl\Services\Servers\CreationService $service @@ -149,6 +156,7 @@ class ServersController extends Controller public function __construct( AlertsMessageBag $alert, AllocationRepositoryInterface $allocationRepository, + BuildModificationService $buildModificationService, ConfigRepository $config, ContainerRebuildService $containerRebuildService, CreationService $service, @@ -165,6 +173,7 @@ class ServersController extends Controller ) { $this->alert = $alert; $this->allocationRepository = $allocationRepository; + $this->buildModificationService = $buildModificationService; $this->config = $config; $this->containerRebuildService = $containerRebuildService; $this->databaseCreationService = $databaseCreationService; @@ -490,39 +499,22 @@ class ServersController extends Controller /** * Update the build configuration for a server. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\RedirectResponse + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @internal param int $id */ - public function updateBuild(Request $request, $id) + public function updateBuild(Request $request, Server $server) { - $repo = new ServerRepository; - - try { - $repo->changeBuild($id, $request->intersect([ + $this->buildModificationService->handle($server, $request->intersect([ 'allocation_id', 'add_allocations', 'remove_allocations', 'memory', 'swap', 'io', 'cpu', 'disk', - ])); + ])); + $this->alert->success(trans('admin/server.alerts.build_updated'))->flash(); - Alert::success('Server details were successfully updated.')->flash(); - } catch (DisplayValidationException $ex) { - return redirect() - ->route('admin.servers.view.build', $id) - ->withErrors(json_decode($ex->getMessage())) - ->withInput(); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (TransferException $ex) { - Log::warning($ex); - Alert::danger('A TransferException was encountered while trying to contact the daemon, please ensure it is online and accessible. This error has been logged.') - ->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An unhandled exception occured while attemping to add this server. This error has been logged.') - ->flash(); - } - - return redirect()->route('admin.servers.view.build', $id); + return redirect()->route('admin.servers.view.build', $server->id); } /** diff --git a/app/Repositories/Eloquent/EloquentRepository.php b/app/Repositories/Eloquent/EloquentRepository.php index 994647df..5746a562 100644 --- a/app/Repositories/Eloquent/EloquentRepository.php +++ b/app/Repositories/Eloquent/EloquentRepository.php @@ -145,6 +145,14 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf return ($this->withFresh) ? $instance->fresh() : $saved; } + /** + * {@inheritdoc} + */ + public function updateWhereIn($column, array $values, array $fields) + { + return $this->getBuilder()->whereIn($column, $values)->update($fields); + } + /** * {@inheritdoc} */ diff --git a/app/Services/Servers/BuildModificationService.php b/app/Services/Servers/BuildModificationService.php new file mode 100644 index 00000000..26f64632 --- /dev/null +++ b/app/Services/Servers/BuildModificationService.php @@ -0,0 +1,244 @@ +. + * + * 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\Servers; + +use GuzzleHttp\Exception\RequestException; +use Illuminate\Database\ConnectionInterface; +use Illuminate\Log\Writer; +use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Exceptions\Repository\RecordNotFoundException; +use Pterodactyl\Models\Server; + +class BuildModificationService +{ + /** + * @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface + */ + protected $allocationRepository; + + /** + * @var array + */ + protected $build = []; + + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface + */ + protected $daemonServerRepository; + + /** + * @var \Illuminate\Database\ConnectionInterface + */ + protected $database; + + /** + * @var null|int + */ + protected $firstAllocationId = null; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $repository; + + /** + * @var \Illuminate\Log\Writer + */ + protected $writer; + + /** + * BuildModificationService constructor. + * + * @param \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface $allocationRepository + * @param \Illuminate\Database\ConnectionInterface $database + * @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonServerRepository + * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository + * @param \Illuminate\Log\Writer $writer + */ + public function __construct( + AllocationRepositoryInterface $allocationRepository, + ConnectionInterface $database, + DaemonServerRepositoryInterface $daemonServerRepository, + ServerRepositoryInterface $repository, + Writer $writer + ) { + $this->allocationRepository = $allocationRepository; + $this->daemonServerRepository = $daemonServerRepository; + $this->database = $database; + $this->repository = $repository; + $this->writer = $writer; + } + + /** + * Set build array parameters. + * + * @param string $key + * @param mixed $value + */ + public function setBuild($key, $value) + { + $this->build[$key] = $value; + } + + /** + * Return the build array. + * + * @return array + */ + public function getBuild() + { + return $this->build; + } + + /** + * Change the build details for a specified server. + * + * @param int|\Pterodactyl\Models\Server $server + * @param array $data + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function handle($server, array $data) + { + if (! $server instanceof Server) { + $server = $this->repository->find($server); + } + + $data['allocation_id'] = array_get($data, 'allocation_id', $server->allocation_id); + $this->database->beginTransaction(); + + $this->setBuild('memory', (int) array_get($data, 'memory', $server->memory)); + $this->setBuild('swap', (int) array_get($data, 'swap', $server->swap)); + $this->setBuild('io', (int) array_get($data, 'io', $server->io)); + $this->setBuild('cpu', (int) array_get($data, 'cpu', $server->cpu)); + $this->setBuild('disk', (int) array_get($data, 'disk', $server->disk)); + + $this->processAllocations($server, $data); + $allocations = $this->allocationRepository->findWhere([ + ['server_id', '=', $server->id], + ]); + + if (isset($data['allocation_id']) && $data['allocation_id'] != $server->allocation_id) { + try { + $allocation = $this->allocationRepository->findFirstWhere([ + ['id', '=', $data['allocation_id']], + ['server_id', '=', $server->id], + ]); + } catch (RecordNotFoundException $ex) { + throw new DisplayException(trans('admin/server.exceptions.default_allocation_not_found')); + } + + $this->setBuild('default', ['ip' => $allocation->ip, 'port' => $allocation->port]); + } + + $this->setBuild('ports|overwrite', $allocations->groupBy('ip')->map(function ($item) { + return $item->pluck('port'); + })->toArray()); + + try { + $this->daemonServerRepository->setNode($server->node_id)->setAccessServer($server->uuid)->update([ + 'build' => $this->getBuild(), + ]); + + $this->database->commit(); + } catch (RequestException $exception) { + $response = $exception->getResponse(); + $this->writer->warning($exception); + + throw new DisplayException(trans('admin/server.exceptions.daemon_exception', [ + 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), + ])); + } + } + + /** + * Process the allocations being assigned in the data and ensure they are available for a server. + * + * @param \Pterodactyl\Models\Server $server + * @param array $data + * + * @throws \Pterodactyl\Exceptions\DisplayException + */ + public function processAllocations(Server $server, array &$data) + { + if (! array_key_exists('add_allocations', $data) && ! array_key_exists('remove_allocations', $data)) { + return; + } + + // Loop through allocations to add. + if (array_key_exists('add_allocations', $data) && ! empty($data['add_allocations'])) { + $unassigned = $this->allocationRepository->findWhere([ + ['server_id', '=', null], + ['node_id', '=', $server->node_id], + ])->pluck('id')->toArray(); + + foreach ($data['add_allocations'] as $allocation) { + if (! in_array($allocation, $unassigned)) { + continue; + } + + $this->firstAllocationId = $this->firstAllocationId ?? $allocation; + $toUpdate[] = [$allocation]; + } + + if (isset($toUpdate)) { + $this->allocationRepository->updateWhereIn('id', $toUpdate, ['server_id' => $server->id]); + unset($toUpdate); + } + } + + // Loop through allocations to remove. + if (array_key_exists('remove_allocations', $data) && ! empty($data['remove_allocations'])) { + $assigned = $this->allocationRepository->findWhere([ + ['server_id', '=', $server->id], + ])->pluck('id')->toArray(); + + foreach ($data['remove_allocations'] as $allocation) { + if (! in_array($allocation, $assigned)) { + continue; + } + + if ($allocation == $data['allocation_id']) { + if (is_null($this->firstAllocationId)) { + throw new DisplayException(trans('admin/server.exceptions.no_new_default_allocation')); + } + + $data['allocation_id'] = $this->firstAllocationId; + } + + $toUpdate[] = [$allocation]; + } + + if (isset($toUpdate)) { + $this->allocationRepository->updateWhereIn('id', $toUpdate, ['server_id' => null]); + unset($toUpdate); + } + } + } +} diff --git a/app/Services/Servers/ContainerRebuildService.php b/app/Services/Servers/ContainerRebuildService.php index 4a022426..20ce9e0b 100644 --- a/app/Services/Servers/ContainerRebuildService.php +++ b/app/Services/Servers/ContainerRebuildService.php @@ -25,6 +25,7 @@ namespace Pterodactyl\Services\Servers; use GuzzleHttp\Exception\RequestException; +use Illuminate\Log\Writer; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; use Pterodactyl\Exceptions\DisplayException; @@ -42,18 +43,26 @@ class ContainerRebuildService */ protected $repository; + /** + * @var \Illuminate\Log\Writer + */ + protected $writer; + /** * ContainerRebuildService constructor. * * @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonServerRepository * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository + * @param \Illuminate\Log\Writer $writer */ public function __construct( DaemonServerRepositoryInterface $daemonServerRepository, - ServerRepositoryInterface $repository + ServerRepositoryInterface $repository, + Writer $writer ) { $this->daemonServerRepository = $daemonServerRepository; $this->repository = $repository; + $this->writer = $writer; } /** @@ -72,8 +81,11 @@ class ContainerRebuildService try { $this->daemonServerRepository->setNode($server->node_id)->setAccessServer($server->uuid)->rebuild(); } catch (RequestException $exception) { + $response = $exception->getResponse(); + $this->writer->warning($exception); + throw new DisplayException(trans('admin/server.exceptions.daemon_exception', [ - 'code' => $exception->getResponse()->getStatusCode(), + 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), ])); } } diff --git a/app/Services/Servers/CreationService.php b/app/Services/Servers/CreationService.php index 290f68a8..fbe89505 100644 --- a/app/Services/Servers/CreationService.php +++ b/app/Services/Servers/CreationService.php @@ -24,6 +24,9 @@ namespace Pterodactyl\Services\Servers; +use GuzzleHttp\Exception\RequestException; +use Illuminate\Log\Writer; +use Pterodactyl\Exceptions\DisplayException; use Ramsey\Uuid\Uuid; use Illuminate\Database\DatabaseManager; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; @@ -80,6 +83,11 @@ class CreationService */ protected $validatorService; + /** + * @var \Illuminate\Log\Writer + */ + protected $writer; + /** * CreationService constructor. * @@ -92,6 +100,7 @@ class CreationService * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $userRepository * @param \Pterodactyl\Services\Servers\UsernameGenerationService $usernameService * @param \Pterodactyl\Services\Servers\VariableValidatorService $validatorService + * @param \Illuminate\Log\Writer $writer */ public function __construct( AllocationRepositoryInterface $allocationRepository, @@ -102,7 +111,8 @@ class CreationService ServerVariableRepositoryInterface $serverVariableRepository, UserRepositoryInterface $userRepository, UsernameGenerationService $usernameService, - VariableValidatorService $validatorService + VariableValidatorService $validatorService, + Writer $writer ) { $this->allocationRepository = $allocationRepository; $this->daemonServerRepository = $daemonServerRepository; @@ -113,6 +123,7 @@ class CreationService $this->userRepository = $userRepository; $this->usernameService = $usernameService; $this->validatorService = $validatorService; + $this->writer = $writer; } /** @@ -121,6 +132,7 @@ class CreationService * @param array $data * @return mixed * + * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ public function create(array $data) @@ -178,9 +190,17 @@ class CreationService $this->serverVariableRepository->insert($records); // Create the server on the daemon & commit it to the database. - $this->daemonServerRepository->setNode($server->node_id)->create($server->id); + try { + $this->daemonServerRepository->setNode($server->node_id)->create($server->id); + $this->database->commit(); + } catch (RequestException $exception) { + $response = $exception->getResponse(); + $this->writer->warning($exception); - $this->database->commit(); + throw new DisplayException(trans('admin/server.exceptions.daemon_exception', [ + 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), + ])); + } return $server; } diff --git a/app/Services/Servers/DetailsModificationService.php b/app/Services/Servers/DetailsModificationService.php index ea275970..c2897021 100644 --- a/app/Services/Servers/DetailsModificationService.php +++ b/app/Services/Servers/DetailsModificationService.php @@ -26,6 +26,7 @@ namespace Pterodactyl\Services\Servers; use GuzzleHttp\Exception\RequestException; use Illuminate\Database\DatabaseManager; +use Illuminate\Log\Writer; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Models\Server; use Pterodactyl\Repositories\Eloquent\ServerRepository; @@ -48,21 +49,29 @@ class DetailsModificationService */ protected $repository; + /** + * @var \Illuminate\Log\Writer + */ + protected $writer; + /** * DetailsModificationService constructor. * * @param \Illuminate\Database\DatabaseManager $database * @param \Pterodactyl\Repositories\Daemon\ServerRepository $daemonServerRepository * @param \Pterodactyl\Repositories\Eloquent\ServerRepository $repository + * @param \Illuminate\Log\Writer $writer */ public function __construct( DatabaseManager $database, DaemonServerRepository $daemonServerRepository, - ServerRepository $repository + ServerRepository $repository, + Writer $writer ) { $this->database = $database; $this->daemonServerRepository = $daemonServerRepository; $this->repository = $repository; + $this->writer = $writer; } /** @@ -114,8 +123,11 @@ class DetailsModificationService return $this->database->commit(); } catch (RequestException $exception) { + $response = $exception->getResponse(); + $this->writer->warning($exception); + throw new DisplayException(trans('admin/server.exceptions.daemon_exception', [ - 'code' => $exception->getResponse()->getStatusCode(), + 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), ])); } } @@ -125,7 +137,6 @@ class DetailsModificationService * * @param int|\Pterodactyl\Models\Server $server * @param string $image - * @return bool * * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException @@ -148,8 +159,11 @@ class DetailsModificationService $this->database->commit(); } catch (RequestException $exception) { + $response = $exception->getResponse(); + $this->writer->warning($exception); + throw new DisplayException(trans('admin/server.exceptions.daemon_exception', [ - 'code' => $exception->getResponse()->getStatusCode(), + 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), ])); } } diff --git a/app/Services/Servers/ReinstallService.php b/app/Services/Servers/ReinstallService.php index b99fafde..5b1b24dd 100644 --- a/app/Services/Servers/ReinstallService.php +++ b/app/Services/Servers/ReinstallService.php @@ -25,6 +25,7 @@ namespace Pterodactyl\Services\Servers; use GuzzleHttp\Exception\RequestException; +use Illuminate\Log\Writer; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Models\Server; use Illuminate\Database\ConnectionInterface; @@ -48,21 +49,29 @@ class ReinstallService */ protected $repository; + /** + * @var \Illuminate\Log\Writer + */ + protected $writer; + /** * ReinstallService constructor. * * @param \Illuminate\Database\ConnectionInterface $database * @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonServerRepository * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository + * @param \Illuminate\Log\Writer $writer */ public function __construct( ConnectionInterface $database, DaemonServerRepositoryInterface $daemonServerRepository, - ServerRepositoryInterface $repository + ServerRepositoryInterface $repository, + Writer $writer ) { $this->daemonServerRepository = $daemonServerRepository; $this->database = $database; $this->repository = $repository; + $this->writer = $writer; } /** @@ -86,8 +95,11 @@ class ReinstallService $this->daemonServerRepository->setNode($server->node_id)->setAccessServer($server->uuid)->reinstall(); $this->database->commit(); } catch (RequestException $exception) { + $response = $exception->getResponse(); + $this->writer->warning($exception); + throw new DisplayException(trans('admin/server.exceptions.daemon_exception', [ - 'code' => $exception->getResponse()->getStatusCode(), + 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), ])); } } diff --git a/app/Services/Servers/SuspensionService.php b/app/Services/Servers/SuspensionService.php index b272d06d..96462db3 100644 --- a/app/Services/Servers/SuspensionService.php +++ b/app/Services/Servers/SuspensionService.php @@ -26,6 +26,7 @@ namespace Pterodactyl\Services\Servers; use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; +use Illuminate\Log\Writer; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; use Pterodactyl\Exceptions\DisplayException; @@ -48,21 +49,29 @@ class SuspensionService */ protected $repository; + /** + * @var \Illuminate\Log\Writer + */ + protected $writer; + /** * SuspensionService constructor. * * @param \Illuminate\Database\ConnectionInterface $database * @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonServerRepository * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository + * @param \Illuminate\Log\Writer $writer */ public function __construct( ConnectionInterface $database, DaemonServerRepositoryInterface $daemonServerRepository, - ServerRepositoryInterface $repository + ServerRepositoryInterface $repository, + Writer $writer ) { $this->daemonServerRepository = $daemonServerRepository; $this->database = $database; $this->repository = $repository; + $this->writer = $writer; } /** @@ -106,8 +115,11 @@ class SuspensionService return true; } catch (RequestException $exception) { + $response = $exception->getResponse(); + $this->writer->warning($exception); + throw new DisplayException(trans('admin/server.exceptions.daemon_exception', [ - 'code' => $exception->getResponse()->getStatusCode(), + 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), ])); } } diff --git a/resources/lang/en/admin/server.php b/resources/lang/en/admin/server.php index 9b977ece..ab4057d9 100644 --- a/resources/lang/en/admin/server.php +++ b/resources/lang/en/admin/server.php @@ -24,11 +24,14 @@ return [ 'exceptions' => [ + 'no_new_default_allocation' => 'You are attempting to delete the default allocation for this server but there is no fallback allocation to use.', 'marked_as_failed' => 'This server was marked as having failed a previous installation. Current status cannot be toggled in this state.', 'bad_variable' => 'There was a validation error with the :name variable.', 'daemon_exception' => 'There was an exception while attempting to communicate with the daemon resulting in a HTTP/:code response code. This exception has been logged.', + 'default_allocation_not_found' => 'The requested default allocation was not found in this server\'s allocations.', ], 'alerts' => [ + 'build_updated' => 'The build details for this server have been updated. Some changes may require a restart to take effect.', 'suspension_toggled' => 'Server suspension status has been changed to :status.', 'rebuild_on_boot' => 'This server has been marked as requiring a Docker Container rebuild. This will happen the next time the server is started.', 'install_toggled' => 'The installation status for this server has been toggled.', diff --git a/tests/Unit/Services/Servers/ContainerRebuildServiceTest.php b/tests/Unit/Services/Servers/ContainerRebuildServiceTest.php index 2ccd487f..fac637c5 100644 --- a/tests/Unit/Services/Servers/ContainerRebuildServiceTest.php +++ b/tests/Unit/Services/Servers/ContainerRebuildServiceTest.php @@ -26,6 +26,7 @@ namespace Tests\Unit\Services\Servers; use Exception; use GuzzleHttp\Exception\RequestException; +use Illuminate\Log\Writer; use Mockery as m; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Models\Server; @@ -61,6 +62,11 @@ class ContainerRebuildServiceTest extends TestCase */ protected $service; + /** + * @var \Illuminate\Log\Writer + */ + protected $writer; + /** * Setup tests. */ @@ -71,9 +77,15 @@ class ContainerRebuildServiceTest extends TestCase $this->daemonServerRepository = m::mock(DaemonServerRepositoryInterface::class); $this->exception = m::mock(RequestException::class)->makePartial(); $this->repository = m::mock(ServerRepositoryInterface::class); + $this->writer = m::mock(Writer::class); + $this->server = factory(Server::class)->make(['node_id' => 1]); - $this->service = new ContainerRebuildService($this->daemonServerRepository, $this->repository); + $this->service = new ContainerRebuildService( + $this->daemonServerRepository, + $this->repository, + $this->writer + ); } /** @@ -114,6 +126,8 @@ class ContainerRebuildServiceTest extends TestCase $this->exception->shouldReceive('getResponse')->withNoArgs()->once()->andReturnSelf() ->shouldReceive('getStatusCode')->withNoArgs()->once()->andReturn(400); + $this->writer->shouldReceive('warning')->with($this->exception)->once()->andReturnNull(); + try { $this->service->rebuild($this->server); } catch (Exception $exception) { diff --git a/tests/Unit/Services/Servers/CreationServiceTest.php b/tests/Unit/Services/Servers/CreationServiceTest.php index 3f1ac7c8..1ff7c3ab 100644 --- a/tests/Unit/Services/Servers/CreationServiceTest.php +++ b/tests/Unit/Services/Servers/CreationServiceTest.php @@ -24,6 +24,7 @@ namespace Tests\Unit\Services\Servers; +use Illuminate\Log\Writer; use Mockery as m; use phpmock\phpunit\PHPMock; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; @@ -98,6 +99,11 @@ class CreationServiceTest extends TestCase */ protected $uuid; + /** + * @var \Illuminate\Log\Writer + */ + protected $writer; + /** * Setup tests. */ @@ -115,6 +121,7 @@ class CreationServiceTest extends TestCase $this->usernameService = m::mock(UsernameGenerationService::class); $this->validatorService = m::mock(VariableValidatorService::class); $this->uuid = m::mock('overload:Ramsey\Uuid\Uuid'); + $this->writer = m::mock(Writer::class); $this->getFunctionMock('\\Pterodactyl\\Services\\Servers', 'bin2hex') ->expects($this->any())->willReturn('randomstring'); @@ -131,7 +138,8 @@ class CreationServiceTest extends TestCase $this->serverVariableRepository, $this->userRepository, $this->usernameService, - $this->validatorService + $this->validatorService, + $this->writer ); } diff --git a/tests/Unit/Services/Servers/DetailsModificationServiceTest.php b/tests/Unit/Services/Servers/DetailsModificationServiceTest.php index 581b5e6d..37164b4f 100644 --- a/tests/Unit/Services/Servers/DetailsModificationServiceTest.php +++ b/tests/Unit/Services/Servers/DetailsModificationServiceTest.php @@ -25,6 +25,7 @@ namespace Tests\Unit\Services\Servers; use Exception; +use Illuminate\Log\Writer; use Mockery as m; use Tests\TestCase; use phpmock\phpunit\PHPMock; @@ -65,6 +66,11 @@ class DetailsModificationServiceTest extends TestCase */ protected $service; + /** + * @var \Illuminate\Log\Writer + */ + protected $writer; + /** * Setup tests. */ @@ -76,6 +82,7 @@ class DetailsModificationServiceTest extends TestCase $this->exception = m::mock(RequestException::class)->makePartial(); $this->daemonServerRepository = m::mock(DaemonServerRepository::class); $this->repository = m::mock(ServerRepository::class); + $this->writer = m::mock(Writer::class); $this->getFunctionMock('\\Pterodactyl\\Services\\Servers', 'bin2hex') ->expects($this->any())->willReturn('randomString'); @@ -83,7 +90,8 @@ class DetailsModificationServiceTest extends TestCase $this->service = new DetailsModificationService( $this->database, $this->daemonServerRepository, - $this->repository + $this->repository, + $this->writer ); } @@ -243,7 +251,7 @@ class DetailsModificationServiceTest extends TestCase $this->exception->shouldReceive('getResponse')->withNoArgs()->once()->andReturnSelf() ->shouldReceive('getStatusCode')->withNoArgs()->once()->andReturn(400); - $this->database->shouldNotReceive('commit'); + $this->writer->shouldReceive('warning')->with($this->exception)->once()->andReturnNull(); try { $this->service->edit($server, $data); @@ -281,7 +289,6 @@ class DetailsModificationServiceTest extends TestCase ], true, true)->once()->andReturnNull(); $this->daemonServerRepository->shouldReceive('setNode')->andThrow(new Exception()); - $this->database->shouldNotReceive('commit'); $this->service->edit($server, $data); } @@ -357,7 +364,7 @@ class DetailsModificationServiceTest extends TestCase $this->exception->shouldReceive('getResponse')->withNoArgs()->once()->andReturnSelf() ->shouldReceive('getStatusCode')->withNoArgs()->once()->andReturn(400); - $this->database->shouldNotReceive('commit'); + $this->writer->shouldReceive('warning')->with($this->exception)->once()->andReturnNull(); try { $this->service->setDockerImage($server, 'new/image'); @@ -385,7 +392,6 @@ class DetailsModificationServiceTest extends TestCase ])->once()->andReturnNull(); $this->daemonServerRepository->shouldReceive('setNode')->andThrow(new Exception()); - $this->database->shouldNotReceive('commit'); $this->service->setDockerImage($server, 'new/image'); } diff --git a/tests/Unit/Services/Servers/ReinstallServiceTest.php b/tests/Unit/Services/Servers/ReinstallServiceTest.php index 471ff3f9..ee023012 100644 --- a/tests/Unit/Services/Servers/ReinstallServiceTest.php +++ b/tests/Unit/Services/Servers/ReinstallServiceTest.php @@ -27,6 +27,7 @@ namespace Tests\Unit\Services\Servers; use Exception; use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; +use Illuminate\Log\Writer; use Mockery as m; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Models\Server; @@ -67,6 +68,11 @@ class ReinstallServiceTest extends TestCase */ protected $service; + /** + * @var \Illuminate\Log\Writer + */ + protected $writer; + /** * Setup tests. */ @@ -78,12 +84,15 @@ class ReinstallServiceTest extends TestCase $this->database = m::mock(ConnectionInterface::class); $this->exception = m::mock(RequestException::class)->makePartial(); $this->repository = m::mock(ServerRepositoryInterface::class); + $this->writer = m::mock(Writer::class); + $this->server = factory(Server::class)->make(['node_id' => 1]); $this->service = new ReinstallService( $this->database, $this->daemonServerRepository, - $this->repository + $this->repository, + $this->writer ); } @@ -146,6 +155,8 @@ class ReinstallServiceTest extends TestCase $this->exception->shouldReceive('getResponse')->withNoArgs()->once()->andReturnSelf() ->shouldReceive('getStatusCode')->withNoArgs()->once()->andReturn(400); + $this->writer->shouldReceive('warning')->with($this->exception)->once()->andReturnNull(); + try { $this->service->reinstall($this->server); } catch (Exception $exception) { diff --git a/tests/Unit/Services/Servers/SuspensionServiceTest.php b/tests/Unit/Services/Servers/SuspensionServiceTest.php index ff8beeb1..0347038e 100644 --- a/tests/Unit/Services/Servers/SuspensionServiceTest.php +++ b/tests/Unit/Services/Servers/SuspensionServiceTest.php @@ -27,6 +27,7 @@ namespace Tests\Unit\Services\Servers; use Exception; use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; +use Illuminate\Log\Writer; use Mockery as m; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; @@ -67,6 +68,11 @@ class SuspensionServiceTest extends TestCase */ protected $service; + /** + * @var \Illuminate\Log\Writer + */ + protected $writer; + /** * Setup tests. */ @@ -78,13 +84,15 @@ class SuspensionServiceTest extends TestCase $this->database = m::mock(ConnectionInterface::class); $this->exception = m::mock(RequestException::class)->makePartial(); $this->repository = m::mock(ServerRepositoryInterface::class); + $this->writer = m::mock(Writer::class); $this->server = factory(Server::class)->make(['suspended' => 0, 'node_id' => 1]); $this->service = new SuspensionService( $this->database, $this->daemonServerRepository, - $this->repository + $this->repository, + $this->writer ); } @@ -176,6 +184,8 @@ class SuspensionServiceTest extends TestCase $this->exception->shouldReceive('getResponse')->withNoArgs()->once()->andReturnSelf() ->shouldReceive('getStatusCode')->withNoArgs()->once()->andReturn(400); + $this->writer->shouldReceive('warning')->with($this->exception)->once()->andReturnNull(); + try { $this->service->toggle($this->server); } catch (Exception $exception) { From 8daec38622c96f7e61d67d59ff9a75b90353b3dc Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Mon, 24 Jul 2017 21:34:10 -0500 Subject: [PATCH 37/99] Complete base implementation of services for administrative server creation --- .../Daemon/ServerRepositoryInterface.php | 10 +- .../Repository/RepositoryInterface.php | 13 ++ .../Repository/ServerRepositoryInterface.php | 10 + .../Controllers/Admin/ServersController.php | 128 +++++------- app/Http/Requests/Admin/ServerFormRequest.php | 16 +- app/Repositories/Daemon/ServerRepository.php | 20 +- .../Eloquent/EloquentRepository.php | 15 ++ .../Eloquent/ServerRepository.php | 18 ++ ...vice.php => DatabaseManagementService.php} | 2 +- .../Servers/BuildModificationService.php | 41 ++-- app/Services/Servers/CreationService.php | 4 +- app/Services/Servers/DeletionService.php | 153 ++++++++++++++ .../Servers/StartupModificationService.php | 195 ++++++++++++++++++ .../Servers/VariableValidatorService.php | 5 +- ...33_DeleteTaskWhenParentServerIsDeleted.php | 36 ++++ resources/lang/en/admin/server.php | 3 + .../pterodactyl/admin/servers/new.blade.php | 2 +- .../admin/servers/view/startup.blade.php | 9 +- .../Database/DatabaseHostServiceTest.php | 6 +- ....php => DatabaseManagementServiceTest.php} | 78 +++++-- .../Services/Servers/CreationServiceTest.php | 6 +- .../Servers/VariableValidatorServiceTest.php | 4 +- 22 files changed, 633 insertions(+), 141 deletions(-) rename app/Services/Database/{CreationService.php => DatabaseManagementService.php} (99%) create mode 100644 app/Services/Servers/DeletionService.php create mode 100644 app/Services/Servers/StartupModificationService.php create mode 100644 database/migrations/2017_07_24_194433_DeleteTaskWhenParentServerIsDeleted.php rename tests/Unit/Services/Database/{CreationServiceTest.php => DatabaseManagementServiceTest.php} (82%) diff --git a/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php b/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php index d8659de3..a0cfc8e2 100644 --- a/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php +++ b/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php @@ -47,9 +47,10 @@ interface ServerRepositoryInterface extends BaseRepositoryInterface /** * Mark a server to be reinstalled on the system. * + * @param array|null $data * @return \Psr\Http\Message\ResponseInterface */ - public function reinstall(); + public function reinstall($data = null); /** * Mark a server as needing a container rebuild the next time the server is booted. @@ -71,4 +72,11 @@ interface ServerRepositoryInterface extends BaseRepositoryInterface * @return \Psr\Http\Message\ResponseInterface */ public function unsuspend(); + + /** + * Delete a server on the daemon. + * + * @return \Psr\Http\Message\ResponseInterface + */ + public function delete(); } diff --git a/app/Contracts/Repository/RepositoryInterface.php b/app/Contracts/Repository/RepositoryInterface.php index cb8241e3..1f498b6e 100644 --- a/app/Contracts/Repository/RepositoryInterface.php +++ b/app/Contracts/Repository/RepositoryInterface.php @@ -141,6 +141,19 @@ interface RepositoryInterface */ public function updateWhereIn($column, array $values, array $fields); + /** + * Update a record if it exists in the database, otherwise create it. + * + * @param array $where + * @param array $fields + * @param bool $validate + * @param bool $force + * @return mixed + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function updateOrCreate(array $where, array $fields, $validate = true, $force = false); + /** * Update multiple records matching the passed clauses. * diff --git a/app/Contracts/Repository/ServerRepositoryInterface.php b/app/Contracts/Repository/ServerRepositoryInterface.php index ece4cee6..4b9d3b96 100644 --- a/app/Contracts/Repository/ServerRepositoryInterface.php +++ b/app/Contracts/Repository/ServerRepositoryInterface.php @@ -77,4 +77,14 @@ interface ServerRepositoryInterface extends RepositoryInterface, SearchableInter * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function getWithDatabases($id); + + /** + * Return data about the daemon service in a consumable format. + * + * @param int $id + * @return array + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function getDaemonServiceData($id); } diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php index 851cd287..dd69f2d5 100644 --- a/app/Http/Controllers/Admin/ServersController.php +++ b/app/Http/Controllers/Admin/ServersController.php @@ -25,7 +25,6 @@ namespace Pterodactyl\Http\Controllers\Admin; use Illuminate\Contracts\Config\Repository as ConfigRepository; -use Log; use Alert; use Javascript; use Prologue\Alerts\AlertsMessageBag; @@ -38,17 +37,17 @@ use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; use Pterodactyl\Http\Requests\Admin\ServerFormRequest; use Pterodactyl\Models\Server; use Illuminate\Http\Request; -use GuzzleHttp\Exception\TransferException; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Repositories\Eloquent\DatabaseHostRepository; -use Pterodactyl\Exceptions\DisplayValidationException; -use Pterodactyl\Services\Database\CreationService as DatabaseCreationService; +use Pterodactyl\Services\Database\DatabaseManagementService; use Pterodactyl\Services\Servers\BuildModificationService; use Pterodactyl\Services\Servers\ContainerRebuildService; use Pterodactyl\Services\Servers\CreationService; +use Pterodactyl\Services\Servers\DeletionService; use Pterodactyl\Services\Servers\DetailsModificationService; use Pterodactyl\Services\Servers\ReinstallService; +use Pterodactyl\Services\Servers\StartupModificationService; use Pterodactyl\Services\Servers\SuspensionService; class ServersController extends Controller @@ -84,15 +83,20 @@ class ServersController extends Controller protected $databaseRepository; /** - * @var \Pterodactyl\Services\Database\CreationService + * @var \Pterodactyl\Services\Database\DatabaseManagementService */ - protected $databaseCreationService; + protected $databaseManagementService; /** * @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface */ protected $databaseHostRepository; + /** + * @var \Pterodactyl\Services\Servers\DeletionService + */ + protected $deletionService; + /** * @var \Pterodactyl\Services\Servers\DetailsModificationService */ @@ -128,6 +132,11 @@ class ServersController extends Controller */ protected $serviceRepository; + /** + * @var \Pterodactyl\Services\Servers\StartupModificationService + */ + private $startupModificationService; + /** * @var \Pterodactyl\Services\Servers\SuspensionService */ @@ -142,15 +151,17 @@ class ServersController extends Controller * @param \Illuminate\Contracts\Config\Repository $config * @param \Pterodactyl\Services\Servers\ContainerRebuildService $containerRebuildService * @param \Pterodactyl\Services\Servers\CreationService $service - * @param \Pterodactyl\Services\Database\CreationService $databaseCreationService + * @param \Pterodactyl\Services\Database\DatabaseManagementService $databaseManagementService * @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $databaseRepository * @param \Pterodactyl\Repositories\Eloquent\DatabaseHostRepository $databaseHostRepository + * @param \Pterodactyl\Services\Servers\DeletionService $deletionService * @param \Pterodactyl\Services\Servers\DetailsModificationService $detailsModificationService * @param \Pterodactyl\Contracts\Repository\LocationRepositoryInterface $locationRepository * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $nodeRepository * @param \Pterodactyl\Services\Servers\ReinstallService $reinstallService * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository * @param \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface $serviceRepository + * @param \Pterodactyl\Services\Servers\StartupModificationService $startupModificationService * @param \Pterodactyl\Services\Servers\SuspensionService $suspensionService */ public function __construct( @@ -160,15 +171,17 @@ class ServersController extends Controller ConfigRepository $config, ContainerRebuildService $containerRebuildService, CreationService $service, - DatabaseCreationService $databaseCreationService, + DatabaseManagementService $databaseManagementService, DatabaseRepositoryInterface $databaseRepository, DatabaseHostRepository $databaseHostRepository, + DeletionService $deletionService, DetailsModificationService $detailsModificationService, LocationRepositoryInterface $locationRepository, NodeRepositoryInterface $nodeRepository, ReinstallService $reinstallService, ServerRepositoryInterface $repository, ServiceRepositoryInterface $serviceRepository, + StartupModificationService $startupModificationService, SuspensionService $suspensionService ) { $this->alert = $alert; @@ -176,16 +189,18 @@ class ServersController extends Controller $this->buildModificationService = $buildModificationService; $this->config = $config; $this->containerRebuildService = $containerRebuildService; - $this->databaseCreationService = $databaseCreationService; + $this->databaseManagementService = $databaseManagementService; $this->databaseRepository = $databaseRepository; $this->databaseHostRepository = $databaseHostRepository; $this->detailsModificationService = $detailsModificationService; + $this->deletionService = $deletionService; $this->locationRepository = $locationRepository; $this->nodeRepository = $nodeRepository; $this->reinstallService = $reinstallService; $this->repository = $repository; $this->service = $service; $this->serviceRepository = $serviceRepository; + $this->startupModificationService = $startupModificationService; $this->suspensionService = $suspensionService; } @@ -234,21 +249,15 @@ class ServersController extends Controller * @param \Pterodactyl\Http\Requests\Admin\ServerFormRequest $request * @return \Illuminate\Http\RedirectResponse * + * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ public function store(ServerFormRequest $request) { - try { - $server = $this->service->create($request->except('_token')); + $server = $this->service->create($request->except('_token')); + $this->alert->success(trans('admin/server.alerts.server_created'))->flash(); - return redirect()->route('admin.servers.view', $server->id); - } catch (TransferException $ex) { - Log::warning($ex); - Alert::danger('A TransferException was encountered while trying to contact the daemon, please ensure it is online and accessible. This error has been logged.') - ->flash(); - } - - return redirect()->route('admin.servers.new')->withInput(); + return redirect()->route('admin.servers.view', $server->id); } /** @@ -508,9 +517,9 @@ class ServersController extends Controller */ public function updateBuild(Request $request, Server $server) { - $this->buildModificationService->handle($server, $request->intersect([ - 'allocation_id', 'add_allocations', 'remove_allocations', - 'memory', 'swap', 'io', 'cpu', 'disk', + $this->buildModificationService->handle($server, $request->only([ + 'allocation_id', 'add_allocations', 'remove_allocations', + 'memory', 'swap', 'io', 'cpu', 'disk', ])); $this->alert->success(trans('admin/server.alerts.build_updated'))->flash(); @@ -520,69 +529,38 @@ class ServersController extends Controller /** * Start the server deletion process. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\DisplayException */ - public function delete(Request $request, $id) + public function delete(Request $request, Server $server) { - $repo = new ServerRepository; + $this->deletionService->withForce($request->has('force_delete'))->handle($server); + $this->alert->success(trans('admin/server.alerts.server_deleted'))->flash(); - try { - $repo->delete($id, $request->has('force_delete')); - Alert::success('Server was successfully deleted from the system.')->flash(); - - return redirect()->route('admin.servers'); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (TransferException $ex) { - Log::warning($ex); - Alert::danger('A TransferException occurred while attempting to delete this server from the daemon, please ensure it is running. This error has been logged.') - ->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An unhandled exception occured while attemping to delete this server. This error has been logged.') - ->flash(); - } - - return redirect()->route('admin.servers.view.delete', $id); + return redirect()->route('admin.servers'); } /** * Update the startup command as well as variables. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function saveStartup(Request $request, $id) + public function saveStartup(Request $request, Server $server) { - $repo = new ServerRepository; + $this->startupModificationService->isAdmin()->handle( + $server, $request->except('_token') + ); + $this->alert->success(trans('admin/server.alerts.startup_changed'))->flash(); - try { - if ($repo->updateStartup($id, $request->except('_token'), true)) { - Alert::success('Service configuration successfully modfied for this server, reinstalling now.') - ->flash(); - - return redirect()->route('admin.servers.view', $id); - } else { - Alert::success('Startup variables were successfully modified and assigned for this server.')->flash(); - } - } catch (DisplayValidationException $ex) { - return redirect()->route('admin.servers.view.startup', $id)->withErrors(json_decode($ex->getMessage())); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (TransferException $ex) { - Log::warning($ex); - Alert::danger('A TransferException occurred while attempting to update the startup for this server, please ensure the daemon is running. This error has been logged.') - ->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An unhandled exception occured while attemping to update startup variables for this server. This error has been logged.') - ->flash(); - } - - return redirect()->route('admin.servers.view.startup', $id); + return redirect()->route('admin.servers.view.startup', $server->id); } /** @@ -598,7 +576,7 @@ class ServersController extends Controller */ public function newDatabase(Request $request, $server) { - $this->databaseCreationService->create($server, [ + $this->databaseManagementService->create($server, [ 'database' => $request->input('database'), 'remote' => $request->input('remote'), 'database_host_id' => $request->input('database_host_id'), @@ -624,7 +602,7 @@ class ServersController extends Controller ['id', '=', $request->input('database')], ]); - $this->databaseCreationService->changePassword($database->id, str_random(20)); + $this->databaseManagementService->changePassword($database->id, str_random(20)); return response('', 204); } @@ -646,7 +624,7 @@ class ServersController extends Controller ['id', '=', $database], ]); - $this->databaseCreationService->delete($database->id); + $this->databaseManagementService->delete($database->id); return response('', 204); } diff --git a/app/Http/Requests/Admin/ServerFormRequest.php b/app/Http/Requests/Admin/ServerFormRequest.php index 19445b58..c521a2f8 100644 --- a/app/Http/Requests/Admin/ServerFormRequest.php +++ b/app/Http/Requests/Admin/ServerFormRequest.php @@ -75,14 +75,14 @@ class ServerFormRequest extends AdminFormRequest return ! ($input->auto_deploy); }); - if ($this->input('pack_id') !== 0) { - $validator->sometimes('pack_id', [ - Rule::exists('packs', 'id')->where(function ($query) { - $query->where('selectable', 1); - $query->where('option_id', $this->input('option_id')); - }), - ]); - } + $validator->sometimes('pack_id', [ + Rule::exists('packs', 'id')->where(function ($query) { + $query->where('selectable', 1); + $query->where('option_id', $this->input('option_id')); + }), + ], function ($input) { + return $input->pack_id !== 0 && $input->pack_id !== null; + }); }); } } diff --git a/app/Repositories/Daemon/ServerRepository.php b/app/Repositories/Daemon/ServerRepository.php index e3e197db..359963eb 100644 --- a/app/Repositories/Daemon/ServerRepository.php +++ b/app/Repositories/Daemon/ServerRepository.php @@ -59,7 +59,7 @@ class ServerRepository extends BaseRepository implements ServerRepositoryInterfa 'io' => (int) $server->io, 'cpu' => (int) $server->cpu, 'disk' => (int) $server->disk, - 'image' => (int) $server->image, + 'image' => $server->image, ], 'service' => [ 'type' => $server->option->service->folder, @@ -97,9 +97,15 @@ class ServerRepository extends BaseRepository implements ServerRepositoryInterfa /** * {@inheritdoc} */ - public function reinstall() + public function reinstall($data = null) { - return $this->getHttpClient()->request('POST', '/server/reinstall'); + if (is_null($data)) { + return $this->getHttpClient()->request('POST', '/server/reinstall'); + } + + return $this->getHttpClient()->request('POST', '/server/reinstall', [ + 'json' => $data, + ]); } /** @@ -125,4 +131,12 @@ class ServerRepository extends BaseRepository implements ServerRepositoryInterfa { return $this->getHttpClient()->request('POST', '/server/unsuspend'); } + + /** + * {@inheritdoc} + */ + public function delete() + { + return $this->getHttpClient()->request('DELETE', '/servers'); + } } diff --git a/app/Repositories/Eloquent/EloquentRepository.php b/app/Repositories/Eloquent/EloquentRepository.php index 5746a562..fa39848a 100644 --- a/app/Repositories/Eloquent/EloquentRepository.php +++ b/app/Repositories/Eloquent/EloquentRepository.php @@ -176,4 +176,19 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf { return $this->getBuilder()->insert($data); } + + /** + * {@inheritdoc} + * @return bool|\Illuminate\Database\Eloquent\Model + */ + public function updateOrCreate(array $where, array $fields, $validate = true, $force = false) + { + $instance = $this->withColumns('id')->findWhere($where)->first(); + + if (! $instance) { + return $this->create(array_merge($where, $fields), $validate, $force); + } + + return $this->update($instance->id, $fields, $validate, $force); + } } diff --git a/app/Repositories/Eloquent/ServerRepository.php b/app/Repositories/Eloquent/ServerRepository.php index 073950e2..967e208d 100644 --- a/app/Repositories/Eloquent/ServerRepository.php +++ b/app/Repositories/Eloquent/ServerRepository.php @@ -129,4 +129,22 @@ class ServerRepository extends SearchableRepository implements ServerRepositoryI return $instance; } + + /** + * {@inheritdoc} + */ + public function getDaemonServiceData($id) + { + $instance = $this->getBuilder()->with('option.service', 'pack')->find($id, $this->getColumns()); + + if (! $instance) { + throw new RecordNotFoundException(); + } + + return [ + 'type' => $instance->option->service->folder, + 'option' => $instance->option->tag, + 'pack' => (! is_null($instance->pack_id)) ? $instance->pack->uuid : null, + ]; + } } diff --git a/app/Services/Database/CreationService.php b/app/Services/Database/DatabaseManagementService.php similarity index 99% rename from app/Services/Database/CreationService.php rename to app/Services/Database/DatabaseManagementService.php index 71e58941..a0343fc7 100644 --- a/app/Services/Database/CreationService.php +++ b/app/Services/Database/DatabaseManagementService.php @@ -29,7 +29,7 @@ use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Extensions\DynamicDatabaseConnection; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; -class CreationService +class DatabaseManagementService { /** * @var \Illuminate\Database\DatabaseManager diff --git a/app/Services/Servers/BuildModificationService.php b/app/Services/Servers/BuildModificationService.php index 26f64632..19ce0c3a 100644 --- a/app/Services/Servers/BuildModificationService.php +++ b/app/Services/Servers/BuildModificationService.php @@ -106,13 +106,18 @@ class BuildModificationService } /** - * Return the build array. + * Return the build array or an item out of the build array. * - * @return array + * @param string|null $attribute + * @return array|mixed|null */ - public function getBuild() + public function getBuild($attribute = null) { - return $this->build; + if (is_null($attribute)) { + return $this->build; + } + + return array_get($this->build, $attribute); } /** @@ -133,17 +138,7 @@ class BuildModificationService $data['allocation_id'] = array_get($data, 'allocation_id', $server->allocation_id); $this->database->beginTransaction(); - $this->setBuild('memory', (int) array_get($data, 'memory', $server->memory)); - $this->setBuild('swap', (int) array_get($data, 'swap', $server->swap)); - $this->setBuild('io', (int) array_get($data, 'io', $server->io)); - $this->setBuild('cpu', (int) array_get($data, 'cpu', $server->cpu)); - $this->setBuild('disk', (int) array_get($data, 'disk', $server->disk)); - $this->processAllocations($server, $data); - $allocations = $this->allocationRepository->findWhere([ - ['server_id', '=', $server->id], - ]); - if (isset($data['allocation_id']) && $data['allocation_id'] != $server->allocation_id) { try { $allocation = $this->allocationRepository->findFirstWhere([ @@ -157,6 +152,24 @@ class BuildModificationService $this->setBuild('default', ['ip' => $allocation->ip, 'port' => $allocation->port]); } + $server = $this->repository->update($server->id, [ + 'memory' => array_get($data, 'memory', $server->memory), + 'swap' => array_get($data, 'swap', $server->swap), + 'io' => array_get($data, 'io', $server->io), + 'cpu' => array_get($data, 'cpu', $server->cpu), + 'disk' => array_get($data, 'disk', $server->disk), + 'allocation_id' => array_get($data, 'allocation_id', $server->allocation_id), + ]); + + $allocations = $this->allocationRepository->findWhere([ + ['server_id', '=', $server->id], + ]); + + $this->setBuild('memory', (int) $server->memory); + $this->setBuild('swap', (int) $server->swap); + $this->setBuild('io', (int) $server->io); + $this->setBuild('cpu', (int) $server->cpu); + $this->setBuild('disk', (int) $server->disk); $this->setBuild('ports|overwrite', $allocations->groupBy('ip')->map(function ($item) { return $item->pluck('port'); })->toArray()); diff --git a/app/Services/Servers/CreationService.php b/app/Services/Servers/CreationService.php index fbe89505..5c641d2d 100644 --- a/app/Services/Servers/CreationService.php +++ b/app/Services/Servers/CreationService.php @@ -138,7 +138,7 @@ class CreationService public function create(array $data) { // @todo auto-deployment - $validator = $this->validatorService->setAdmin()->setFields($data['environment'])->validate($data['option_id']); + $validator = $this->validatorService->isAdmin()->setFields($data['environment'])->validate($data['option_id']); $uniqueShort = bin2hex(random_bytes(4)); $this->database->beginTransaction(); @@ -151,7 +151,7 @@ class CreationService 'description' => $data['description'], 'skip_scripts' => isset($data['skip_scripts']), 'suspended' => false, - 'owner_id' => $data['user_id'], + 'owner_id' => $data['owner_id'], 'memory' => $data['memory'], 'swap' => $data['swap'], 'disk' => $data['disk'], diff --git a/app/Services/Servers/DeletionService.php b/app/Services/Servers/DeletionService.php new file mode 100644 index 00000000..5d63ed09 --- /dev/null +++ b/app/Services/Servers/DeletionService.php @@ -0,0 +1,153 @@ +. + * + * 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\Servers; + +use Illuminate\Log\Writer; +use Pterodactyl\Models\Server; +use GuzzleHttp\Exception\RequestException; +use Illuminate\Database\ConnectionInterface; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Services\Database\DatabaseManagementService; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; + +class DeletionService +{ + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface + */ + protected $daemonServerRepository; + + /** + * @var \Illuminate\Database\ConnectionInterface + */ + protected $database; + + /** + * @var \Pterodactyl\Services\Database\DatabaseManagementService + */ + protected $databaseManagementService; + + /** + * @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface + */ + protected $databaseRepository; + + /** + * @var bool + */ + protected $force = false; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $repository; + + /** + * @var \Illuminate\Log\Writer + */ + protected $writer; + + /** + * DeletionService constructor. + * + * @param \Illuminate\Database\ConnectionInterface $database + * @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonServerRepository + * @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $databaseRepository + * @param \Pterodactyl\Services\Database\DatabaseManagementService $databaseManagementService + * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository + * @param \Illuminate\Log\Writer $writer + */ + public function __construct( + ConnectionInterface $database, + DaemonServerRepositoryInterface $daemonServerRepository, + DatabaseRepositoryInterface $databaseRepository, + DatabaseManagementService $databaseManagementService, + ServerRepositoryInterface $repository, + Writer $writer + ) { + $this->daemonServerRepository = $daemonServerRepository; + $this->database = $database; + $this->databaseManagementService = $databaseManagementService; + $this->databaseRepository = $databaseRepository; + $this->repository = $repository; + $this->writer = $writer; + } + + /** + * Set if the server should be forcibly deleted from the panel (ignoring daemon errors) or not. + * + * @param bool $bool + * @return $this + */ + public function withForce($bool = true) + { + $this->force = $bool; + + return $this; + } + + /** + * Delete a server from the panel and remove any associated databases from hosts. + * + * @param int|\Pterodactyl\Models\Server $server + * @throws \Pterodactyl\Exceptions\DisplayException + */ + public function handle($server) + { + if (! $server instanceof Server) { + $server = $this->repository->withColumns(['id', 'node_id', 'uuid'])->find($server); + } + + try { + $this->daemonServerRepository->setNode($server->node_id)->setAccessServer($server->uuid)->delete(); + } catch (RequestException $exception) { + $response = $exception->getResponse(); + + if (is_null($response) || (! is_null($response) && $response->getStatusCode() !== 404)) { + $this->writer->warning($exception); + + // If not forcing the deletion, throw an exception, otherwise just log it and + // continue with server deletion process in the panel. + if (! $this->force) { + throw new DisplayException(trans('admin/server.exceptions.daemon_exception', [ + 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), + ])); + } + } + } + + $this->database->beginTransaction(); + + $this->databaseRepository->withColumns('id')->findWhere([['server_id', '=', $server->id]])->each(function ($item) { + $this->databaseManagementService->delete($item->id); + }); + + $this->repository->delete($server->id); + + $this->database->commit(); + } +} diff --git a/app/Services/Servers/StartupModificationService.php b/app/Services/Servers/StartupModificationService.php new file mode 100644 index 00000000..00eaaafd --- /dev/null +++ b/app/Services/Servers/StartupModificationService.php @@ -0,0 +1,195 @@ +. + * + * 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\Servers; + +use GuzzleHttp\Exception\RequestException; +use Illuminate\Database\ConnectionInterface; +use Illuminate\Log\Writer; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Models\Server; + +class StartupModificationService +{ + /** + * @var bool + */ + protected $admin = false; + + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface + */ + protected $daemonServerRepository; + + /** + * @var \Illuminate\Database\ConnectionInterface + */ + protected $database; + + /** + * @var \Pterodactyl\Services\Servers\EnvironmentService + */ + protected $environmentService; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface + */ + protected $serverVariableRepository; + + /** + * @var \Pterodactyl\Services\Servers\VariableValidatorService + */ + protected $validatorService; + + /** + * @var \Illuminate\Log\Writer + */ + protected $writer; + + /** + * StartupModificationService constructor. + * + * @param \Illuminate\Database\ConnectionInterface $database + * @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonServerRepository + * @param \Pterodactyl\Services\Servers\EnvironmentService $environmentService + * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository + * @param \Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface $serverVariableRepository + * @param \Pterodactyl\Services\Servers\VariableValidatorService $validatorService + * @param \Illuminate\Log\Writer $writer + */ + public function __construct( + ConnectionInterface $database, + DaemonServerRepositoryInterface $daemonServerRepository, + EnvironmentService $environmentService, + ServerRepositoryInterface $repository, + ServerVariableRepositoryInterface $serverVariableRepository, + VariableValidatorService $validatorService, + Writer $writer + ) { + $this->daemonServerRepository = $daemonServerRepository; + $this->database = $database; + $this->environmentService = $environmentService; + $this->repository = $repository; + $this->serverVariableRepository = $serverVariableRepository; + $this->validatorService = $validatorService; + $this->writer = $writer; + } + + /** + * Determine if this function should run at an administrative level. + * + * @param bool $bool + * @return $this + */ + public function isAdmin($bool = true) + { + $this->admin = $bool; + + return $this; + } + + /** + * Process startup modification for a server. + * + * @param int|\Pterodactyl\Models\Server $server + * @param array $data + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function handle($server, array $data) + { + if (! $server instanceof Server) { + $server = $this->repository->find($server); + } + + if ( + $server->service_id != array_get($data, 'service_id', $server->service_id) || + $server->option_id != array_get($data, 'option_id', $server->option_id) || + $server->pack_id != array_get($data, 'pack_id', $server->pack_id) + ) { + $hasServiceChanges = true; + } + + $this->database->beginTransaction(); + if (isset($data['environment'])) { + $validator = $this->validatorService->isAdmin($this->admin) + ->setFields($data['environment']) + ->validate(array_get($data, 'option_id', $server->option_id)); + + foreach ($validator->getResults() as $result) { + $this->serverVariableRepository->withoutFresh()->updateOrCreate([ + 'server_id' => $server->id, + 'variable_id' => $result['id'], + ], [ + 'variable_value' => $result['value'], + ]); + } + } + + $daemonData = [ + 'build' => [ + 'env|overwrite' => $this->environmentService->process($server), + ], + ]; + + if ($this->admin) { + $server = $this->repository->update($server->id, [ + 'installed' => 0, + 'startup' => array_get($data, 'startup', $server->startup), + 'service_id' => array_get($data, 'service_id', $server->service_id), + 'option_id' => array_get($data, 'option_id', $server->service_id), + 'pack_id' => array_get($data, 'pack_id', $server->pack_id), + 'skip_scripts' => isset($data['skip_scripts']), + ]); + + if (isset($hasServiceChanges)) { + $daemonData['service'] = array_merge( + $this->repository->withColumns(['id', 'option_id', 'pack_id'])->getDaemonServiceData($server), + ['skip_scripts' => isset($data['skip_scripts'])] + ); + } + } + + try { + $this->daemonServerRepository->setNode($server->node_id)->setAccessServer($server->uuid)->update($daemonData); + $this->database->commit(); + } catch (RequestException $exception) { + $response = $exception->getResponse(); + $this->writer->warning($exception); + + throw new DisplayException(trans('admin/server.exceptions.daemon_exception', [ + 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), + ])); + } + } +} diff --git a/app/Services/Servers/VariableValidatorService.php b/app/Services/Servers/VariableValidatorService.php index 3be00494..d0bb2ccd 100644 --- a/app/Services/Servers/VariableValidatorService.php +++ b/app/Services/Servers/VariableValidatorService.php @@ -103,11 +103,12 @@ class VariableValidatorService /** * Set this function to be running at the administrative level. * + * @param bool $bool * @return $this */ - public function setAdmin() + public function isAdmin($bool = true) { - $this->isAdmin = true; + $this->isAdmin = $bool; return $this; } diff --git a/database/migrations/2017_07_24_194433_DeleteTaskWhenParentServerIsDeleted.php b/database/migrations/2017_07_24_194433_DeleteTaskWhenParentServerIsDeleted.php new file mode 100644 index 00000000..8a3d7842 --- /dev/null +++ b/database/migrations/2017_07_24_194433_DeleteTaskWhenParentServerIsDeleted.php @@ -0,0 +1,36 @@ +dropForeign(['server_id']); + + $table->foreign('server_id')->references('id')->on('servers')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('tasks', function (Blueprint $table) { + $table->dropForeign(['server_id']); + + $table->foreign('server_id')->references('id')->on('servers'); + }); + } +} diff --git a/resources/lang/en/admin/server.php b/resources/lang/en/admin/server.php index ab4057d9..6cf48223 100644 --- a/resources/lang/en/admin/server.php +++ b/resources/lang/en/admin/server.php @@ -31,6 +31,9 @@ return [ 'default_allocation_not_found' => 'The requested default allocation was not found in this server\'s allocations.', ], 'alerts' => [ + 'startup_changed' => 'The startup configuration for this server has been updated. If this server\'s service or option was changed a reinstall will be occuring now.', + 'server_deleted' => 'Server has successfully been deleted from the system.', + 'server_created' => 'Server was successfully created on the panel. Please allow the daemon a few minutes to completely install this server.', 'build_updated' => 'The build details for this server have been updated. Some changes may require a restart to take effect.', 'suspension_toggled' => 'Server suspension status has been changed to :status.', 'rebuild_on_boot' => 'This server has been marked as requiring a Docker Container rebuild. This will happen the next time the server is started.', diff --git a/resources/themes/pterodactyl/admin/servers/new.blade.php b/resources/themes/pterodactyl/admin/servers/new.blade.php index 8a68b197..874dc55f 100644 --- a/resources/themes/pterodactyl/admin/servers/new.blade.php +++ b/resources/themes/pterodactyl/admin/servers/new.blade.php @@ -49,7 +49,7 @@
- +
diff --git a/resources/themes/pterodactyl/admin/servers/view/startup.blade.php b/resources/themes/pterodactyl/admin/servers/view/startup.blade.php index 175bbeff..32ad9d2a 100644 --- a/resources/themes/pterodactyl/admin/servers/view/startup.blade.php +++ b/resources/themes/pterodactyl/admin/servers/view/startup.blade.php @@ -160,7 +160,6 @@ $('#pServiceId').on('change', function (event) { $('#pOptionId').html('').select2({ data: $.map(_.get(Pterodactyl.services, $(this).val() + '.options', []), function (item) { - console.log(item); return { id: item.id, text: item.name, @@ -182,7 +181,7 @@ } $('#pPackId').html('').select2({ - data: [{ id: 0, text: 'No Service Pack' }].concat( + data: [{ id: '', text: 'No Service Pack' }].concat( $.map(_.get(objectChain, 'packs', []), function (item, i) { return { id: item.id, @@ -190,7 +189,11 @@ }; }) ), - }).val('{{ is_null($server->pack_id) ? 0 : $server->pack_id }}'); + }); + + @if(! is_null($server->pack_id)) + $('#pPackId').val({{ $server->pack_id }}); + @endif $('#appendVariablesTo').html(''); $.each(_.get(objectChain, 'variables', []), function (i, item) { diff --git a/tests/Unit/Services/Database/DatabaseHostServiceTest.php b/tests/Unit/Services/Database/DatabaseHostServiceTest.php index 5140dfea..fae03862 100644 --- a/tests/Unit/Services/Database/DatabaseHostServiceTest.php +++ b/tests/Unit/Services/Database/DatabaseHostServiceTest.php @@ -24,15 +24,13 @@ namespace Tests\Unit\Services\Administrative; -use Illuminate\Database\ConnectionInterface; -use Illuminate\Database\ConnectionResolver; -use Illuminate\Database\DatabaseManager; use Mockery as m; use Tests\TestCase; +use Illuminate\Database\DatabaseManager; use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Extensions\DynamicDatabaseConnection; -use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; use Pterodactyl\Services\Database\DatabaseHostService; +use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; class DatabaseHostServiceTest extends TestCase { diff --git a/tests/Unit/Services/Database/CreationServiceTest.php b/tests/Unit/Services/Database/DatabaseManagementServiceTest.php similarity index 82% rename from tests/Unit/Services/Database/CreationServiceTest.php rename to tests/Unit/Services/Database/DatabaseManagementServiceTest.php index c566fde6..ca1e29ce 100644 --- a/tests/Unit/Services/Database/CreationServiceTest.php +++ b/tests/Unit/Services/Database/DatabaseManagementServiceTest.php @@ -25,18 +25,16 @@ namespace Tests\Unit\Services\Database; use Exception; -use Illuminate\Database\DatabaseManager; use Mockery as m; use Tests\TestCase; use phpmock\phpunit\PHPMock; -use Illuminate\Database\ConnectionInterface; +use Illuminate\Database\DatabaseManager; use Illuminate\Contracts\Encryption\Encrypter; -use Pterodactyl\Services\Database\CreationService; -use Illuminate\Database\ConnectionResolver; use Pterodactyl\Extensions\DynamicDatabaseConnection; +use Pterodactyl\Services\Database\DatabaseManagementService; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; -class CreationServiceTest extends TestCase +class DatabaseManagementServiceTest extends TestCase { use PHPMock; @@ -70,7 +68,7 @@ class CreationServiceTest extends TestCase protected $repository; /** - * @var \Pterodactyl\Services\Database\CreationService + * @var \Pterodactyl\Services\Database\DatabaseManagementService */ protected $service; @@ -87,9 +85,9 @@ class CreationServiceTest extends TestCase $this->repository = m::mock(DatabaseRepositoryInterface::class); $this->getFunctionMock('\\Pterodactyl\\Services\\Database', 'str_random') - ->expects($this->any())->willReturn('str_random'); + ->expects($this->any())->willReturn('str_random'); - $this->service = new CreationService( + $this->service = new DatabaseManagementService( $this->database, $this->dynamic, $this->repository, @@ -105,8 +103,14 @@ class CreationServiceTest extends TestCase $this->encrypter->shouldReceive('encrypt')->with('str_random')->once()->andReturn('enc_password'); $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('createIfNotExists')->with(self::TEST_DATA)->once()->andReturn((object) self::TEST_DATA); - $this->dynamic->shouldReceive('set')->with('dynamic', self::TEST_DATA['database_host_id'])->once()->andReturnNull(); + $this->repository->shouldReceive('createIfNotExists') + ->with(self::TEST_DATA) + ->once() + ->andReturn((object) self::TEST_DATA); + $this->dynamic->shouldReceive('set') + ->with('dynamic', self::TEST_DATA['database_host_id']) + ->once() + ->andReturnNull(); $this->repository->shouldReceive('createDatabase')->with( self::TEST_DATA['database'], 'dynamic' )->once()->andReturnNull(); @@ -148,7 +152,10 @@ class CreationServiceTest extends TestCase { $this->encrypter->shouldReceive('encrypt')->with('str_random')->once()->andReturn('enc_password'); $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('createIfNotExists')->with(self::TEST_DATA)->once()->andThrow(new Exception('Test Message')); + $this->repository->shouldReceive('createIfNotExists') + ->with(self::TEST_DATA) + ->once() + ->andThrow(new Exception('Test Message')); $this->repository->shouldNotReceive('dropDatabase'); $this->database->shouldReceive('rollBack')->withNoArgs()->once()->andReturnNull(); @@ -168,13 +175,22 @@ class CreationServiceTest extends TestCase { $this->encrypter->shouldReceive('encrypt')->with('str_random')->once()->andReturn('enc_password'); $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('createIfNotExists')->with(self::TEST_DATA)->once()->andReturn((object) self::TEST_DATA); - $this->dynamic->shouldReceive('set')->with('dynamic', self::TEST_DATA['database_host_id'])->once()->andReturnNull(); + $this->repository->shouldReceive('createIfNotExists') + ->with(self::TEST_DATA) + ->once() + ->andReturn((object) self::TEST_DATA); + $this->dynamic->shouldReceive('set') + ->with('dynamic', self::TEST_DATA['database_host_id']) + ->once() + ->andReturnNull(); $this->repository->shouldReceive('createDatabase')->with( self::TEST_DATA['database'], 'dynamic' )->once()->andThrow(new Exception('Test Message')); - $this->repository->shouldReceive('dropDatabase')->with(self::TEST_DATA['database'], 'dynamic')->once()->andReturnNull(); + $this->repository->shouldReceive('dropDatabase') + ->with(self::TEST_DATA['database'], 'dynamic') + ->once() + ->andReturnNull(); $this->repository->shouldReceive('dropUser')->with( self::TEST_DATA['username'], self::TEST_DATA['remote'], 'dynamic' )->once()->andReturnNull(); @@ -196,14 +212,20 @@ class CreationServiceTest extends TestCase { $this->encrypter->shouldReceive('encrypt')->with('str_random')->once()->andReturn('enc_password'); $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->repository->shouldReceive('createIfNotExists')->with(self::TEST_DATA)->once()->andReturn((object) self::TEST_DATA); - $this->dynamic->shouldReceive('set')->with('dynamic', self::TEST_DATA['database_host_id'])->once()->andReturnNull(); + $this->repository->shouldReceive('createIfNotExists') + ->with(self::TEST_DATA) + ->once() + ->andReturn((object) self::TEST_DATA); + $this->dynamic->shouldReceive('set') + ->with('dynamic', self::TEST_DATA['database_host_id']) + ->once() + ->andReturnNull(); $this->repository->shouldReceive('createDatabase')->with( self::TEST_DATA['database'], 'dynamic' )->once()->andThrow(new Exception('Test One')); $this->repository->shouldReceive('dropDatabase')->with(self::TEST_DATA['database'], 'dynamic') - ->once()->andThrow(new Exception('Test Two')); + ->once()->andThrow(new Exception('Test Two')); $this->database->shouldReceive('rollBack')->withNoArgs()->once()->andReturnNull(); @@ -225,7 +247,10 @@ class CreationServiceTest extends TestCase public function testDatabasePasswordShouldBeChanged() { $this->repository->shouldReceive('find')->with(1)->once()->andReturn((object) self::TEST_DATA); - $this->dynamic->shouldReceive('set')->with('dynamic', self::TEST_DATA['database_host_id'])->once()->andReturnNull(); + $this->dynamic->shouldReceive('set') + ->with('dynamic', self::TEST_DATA['database_host_id']) + ->once() + ->andReturnNull(); $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); $this->encrypter->shouldReceive('encrypt')->with('new_password')->once()->andReturn('new_enc_password'); @@ -262,12 +287,15 @@ class CreationServiceTest extends TestCase public function testExceptionThrownWhileChangingDatabasePasswordShouldRollBack() { $this->repository->shouldReceive('find')->with(1)->once()->andReturn((object) self::TEST_DATA); - $this->dynamic->shouldReceive('set')->with('dynamic', self::TEST_DATA['database_host_id'])->once()->andReturnNull(); + $this->dynamic->shouldReceive('set') + ->with('dynamic', self::TEST_DATA['database_host_id']) + ->once() + ->andReturnNull(); $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); $this->encrypter->shouldReceive('encrypt')->with('new_password')->once()->andReturn('new_enc_password'); $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('update')->with(1, [ + ->shouldReceive('update')->with(1, [ 'password' => 'new_enc_password', ])->andReturn(true); @@ -286,9 +314,15 @@ class CreationServiceTest extends TestCase public function testDatabaseShouldBeDeleted() { $this->repository->shouldReceive('find')->with(1)->once()->andReturn((object) self::TEST_DATA); - $this->dynamic->shouldReceive('set')->with('dynamic', self::TEST_DATA['database_host_id'])->once()->andReturnNull(); + $this->dynamic->shouldReceive('set') + ->with('dynamic', self::TEST_DATA['database_host_id']) + ->once() + ->andReturnNull(); - $this->repository->shouldReceive('dropDatabase')->with(self::TEST_DATA['database'], 'dynamic')->once()->andReturnNull(); + $this->repository->shouldReceive('dropDatabase') + ->with(self::TEST_DATA['database'], 'dynamic') + ->once() + ->andReturnNull(); $this->repository->shouldReceive('dropUser')->with( self::TEST_DATA['username'], self::TEST_DATA['remote'], 'dynamic' )->once()->andReturnNull(); diff --git a/tests/Unit/Services/Servers/CreationServiceTest.php b/tests/Unit/Services/Servers/CreationServiceTest.php index 1ff7c3ab..284d9c28 100644 --- a/tests/Unit/Services/Servers/CreationServiceTest.php +++ b/tests/Unit/Services/Servers/CreationServiceTest.php @@ -152,7 +152,7 @@ class CreationServiceTest extends TestCase 'node_id' => 1, 'name' => 'SomeName', 'description' => null, - 'user_id' => 1, + 'owner_id' => 1, 'memory' => 128, 'disk' => 128, 'swap' => 0, @@ -169,7 +169,7 @@ class CreationServiceTest extends TestCase 'docker_image' => 'some/image', ]; - $this->validatorService->shouldReceive('setAdmin')->withNoArgs()->once()->andReturnSelf() + $this->validatorService->shouldReceive('isAdmin')->withNoArgs()->once()->andReturnSelf() ->shouldReceive('setFields')->with($data['environment'])->once()->andReturnSelf() ->shouldReceive('validate')->with($data['option_id'])->once()->andReturnSelf(); @@ -187,7 +187,7 @@ class CreationServiceTest extends TestCase 'description' => $data['description'], 'skip_scripts' => false, 'suspended' => false, - 'owner_id' => $data['user_id'], + 'owner_id' => $data['owner_id'], 'memory' => $data['memory'], 'swap' => $data['swap'], 'disk' => $data['disk'], diff --git a/tests/Unit/Services/Servers/VariableValidatorServiceTest.php b/tests/Unit/Services/Servers/VariableValidatorServiceTest.php index b2e87cf0..60e8705a 100644 --- a/tests/Unit/Services/Servers/VariableValidatorServiceTest.php +++ b/tests/Unit/Services/Servers/VariableValidatorServiceTest.php @@ -110,7 +110,7 @@ class VariableValidatorServiceTest extends TestCase */ public function testSettingAdminShouldReturnInstanceOfSelf() { - $response = $this->service->setAdmin(); + $response = $this->service->isAdmin(); $this->assertInstanceOf(VariableValidatorService::class, $response); } @@ -187,7 +187,7 @@ class VariableValidatorServiceTest extends TestCase ->shouldReceive('fails')->withNoArgs()->once()->andReturn(false); } - $response = $this->service->setAdmin()->setFields([ + $response = $this->service->isAdmin()->setFields([ $this->variables{0}->env_variable => 'Test_SomeValue_0', $this->variables{1}->env_variable => 'Test_SomeValue_1', $this->variables{2}->env_variable => 'Test_SomeValue_2', From 275c01bc37df5cfb25d83c123350a9ddb5a53fc3 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Fri, 4 Aug 2017 19:11:41 -0500 Subject: [PATCH 38/99] Update user service to be more separated --- .../Repository/UserRepositoryInterface.php | 10 - app/Http/Controllers/Admin/UserController.php | 82 +- app/Repositories/Eloquent/UserRepository.php | 20 - app/Repositories/Old/old_ServerRepository.php | 1036 ----------------- app/Repositories/Old/old_UserRepository.php | 182 --- .../CreationService.php} | 57 +- app/Services/Users/DeletionService.php | 88 ++ app/Services/Users/UpdateService.php | 75 ++ resources/lang/en/admin/user.php | 33 + .../CreationServiceTest.php} | 48 +- .../Services/Users/DeletionServiceTest.php | 120 ++ .../Unit/Services/Users/UpdateServiceTest.php | 83 ++ 12 files changed, 473 insertions(+), 1361 deletions(-) delete mode 100644 app/Repositories/Old/old_ServerRepository.php delete mode 100644 app/Repositories/Old/old_UserRepository.php rename app/Services/{UserService.php => Users/CreationService.php} (72%) create mode 100644 app/Services/Users/DeletionService.php create mode 100644 app/Services/Users/UpdateService.php create mode 100644 resources/lang/en/admin/user.php rename tests/Unit/Services/{UserServiceTest.php => Users/CreationServiceTest.php} (80%) create mode 100644 tests/Unit/Services/Users/DeletionServiceTest.php create mode 100644 tests/Unit/Services/Users/UpdateServiceTest.php diff --git a/app/Contracts/Repository/UserRepositoryInterface.php b/app/Contracts/Repository/UserRepositoryInterface.php index 0265d6f4..b35914c0 100644 --- a/app/Contracts/Repository/UserRepositoryInterface.php +++ b/app/Contracts/Repository/UserRepositoryInterface.php @@ -35,16 +35,6 @@ interface UserRepositoryInterface extends RepositoryInterface, SearchableInterfa */ public function getAllUsersWithCounts(); - /** - * Delete a user if they have no servers attached to their account. - * - * @param int $id - * @return bool - * - * @throws \Pterodactyl\Exceptions\DisplayException - */ - public function deleteIfNoServers($id); - /** * Return all matching models for a user in a format that can be used for dropdowns. * diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index 42da3f57..81a4a778 100644 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -25,13 +25,16 @@ namespace Pterodactyl\Http\Controllers\Admin; use Illuminate\Http\Request; -use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Models\User; use Prologue\Alerts\AlertsMessageBag; -use Pterodactyl\Services\UserService; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Http\Controllers\Controller; +use Pterodactyl\Services\Users\UpdateService; +use Pterodactyl\Services\Users\CreationService; +use Pterodactyl\Services\Users\DeletionService; +use Illuminate\Contracts\Translation\Translator; use Pterodactyl\Http\Requests\Admin\UserFormRequest; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; class UserController extends Controller { @@ -41,53 +44,67 @@ class UserController extends Controller protected $alert; /** - * @var \Pterodactyl\Services\UserService + * @var \Pterodactyl\Services\Users\CreationService */ - protected $service; + protected $creationService; /** - * @var \Pterodactyl\Models\User + * @var \Pterodactyl\Services\Users\DeletionService */ - protected $model; + protected $deletionService; /** * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface */ protected $repository; + /** + * @var \Illuminate\Contracts\Translation\Translator + */ + protected $translator; + + /** + * @var \Pterodactyl\Services\Users\UpdateService + */ + protected $updateService; + /** * UserController constructor. * * @param \Prologue\Alerts\AlertsMessageBag $alert - * @param \Pterodactyl\Services\UserService $service + * @param \Pterodactyl\Services\Users\CreationService $creationService + * @param \Pterodactyl\Services\Users\DeletionService $deletionService + * @param \Illuminate\Contracts\Translation\Translator $translator + * @param \Pterodactyl\Services\Users\UpdateService $updateService * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository - * @param \Pterodactyl\Models\User $model */ public function __construct( AlertsMessageBag $alert, - UserService $service, - UserRepositoryInterface $repository, - User $model + CreationService $creationService, + DeletionService $deletionService, + Translator $translator, + UpdateService $updateService, + UserRepositoryInterface $repository ) { $this->alert = $alert; - $this->service = $service; - $this->model = $model; + $this->creationService = $creationService; + $this->deletionService = $deletionService; $this->repository = $repository; + $this->translator = $translator; + $this->updateService = $updateService; } /** * Display user index page. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ public function index(Request $request) { $users = $this->repository->search($request->input('query'))->getAllUsersWithCounts(); - return view('admin.users.index', [ - 'users' => $users, - ]); + return view('admin.users.index', ['users' => $users]); } /** @@ -103,21 +120,19 @@ class UserController extends Controller /** * Display user view page. * - * @param \Pterodactyl\Models\User $user + * @param \Pterodactyl\Models\User $user * @return \Illuminate\View\View */ public function view(User $user) { - return view('admin.users.view', [ - 'user' => $user, - ]); + return view('admin.users.view', ['user' => $user]); } /** * Delete a user from the system. * - * @param \Illuminate\Http\Request $request - * @param \Pterodactyl\Models\User $user + * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Models\User $user * @return \Illuminate\Http\RedirectResponse * * @throws \Exception @@ -126,16 +141,10 @@ class UserController extends Controller public function delete(Request $request, User $user) { if ($request->user()->id === $user->id) { - throw new DisplayException('Cannot delete your own account.'); + throw new DisplayException($this->translator->trans('admin/user.exceptions.user_has_servers')); } - try { - $this->repository->deleteIfNoServers($user->id); - - return redirect()->route('admin.users'); - } catch (DisplayException $ex) { - $this->alert->danger($ex->getMessage())->flash(); - } + $this->deletionService->handle($user); return redirect()->route('admin.users.view', $user->id); } @@ -143,7 +152,7 @@ class UserController extends Controller /** * Create a user. * - * @param \Pterodactyl\Http\Requests\Admin\UserFormRequest $request + * @param \Pterodactyl\Http\Requests\Admin\UserFormRequest $request * @return \Illuminate\Http\RedirectResponse * * @throws \Exception @@ -151,9 +160,8 @@ class UserController extends Controller */ public function store(UserFormRequest $request) { - $user = $this->service->create($request->normalize()); - - $this->alert->success('Account has been successfully created.')->flash(); + $user = $this->creationService->handle($request->normalize()); + $this->alert->success($this->translator->trans('admin/user.notices.account_created'))->flash(); return redirect()->route('admin.users.view', $user->id); } @@ -169,8 +177,8 @@ class UserController extends Controller */ public function update(UserFormRequest $request, User $user) { - $this->service->update($user->id, $request->normalize()); - $this->alert->success('User account has been updated.')->flash(); + $this->updateService->handle($user->id, $request->normalize()); + $this->alert->success($this->translator->trans('admin/user.notices.account_updated'))->flash(); return redirect()->route('admin.users.view', $user->id); } diff --git a/app/Repositories/Eloquent/UserRepository.php b/app/Repositories/Eloquent/UserRepository.php index 8915b33a..633b92fd 100644 --- a/app/Repositories/Eloquent/UserRepository.php +++ b/app/Repositories/Eloquent/UserRepository.php @@ -27,8 +27,6 @@ namespace Pterodactyl\Repositories\Eloquent; use Illuminate\Contracts\Config\Repository as ConfigRepository; use Illuminate\Foundation\Application; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Models\User; use Pterodactyl\Repositories\Eloquent\Attributes\SearchableRepository; @@ -76,24 +74,6 @@ class UserRepository extends SearchableRepository implements UserRepositoryInter ); } - /** - * {@inheritdoc} - */ - public function deleteIfNoServers($id) - { - $user = $this->getBuilder()->withCount('servers')->where('id', $id)->first(); - - if (! $user) { - throw new RecordNotFoundException(); - } - - if ($user->servers_count > 0) { - throw new DisplayException('Cannot delete an account that has active servers attached to it.'); - } - - return $user->delete(); - } - /** * {@inheritdoc} */ diff --git a/app/Repositories/Old/old_ServerRepository.php b/app/Repositories/Old/old_ServerRepository.php deleted file mode 100644 index 8cfe9ca4..00000000 --- a/app/Repositories/Old/old_ServerRepository.php +++ /dev/null @@ -1,1036 +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\Repositories; - -use DB; -use Crypt; -use Validator; -use Pterodactyl\Models\Node; -use Pterodactyl\Models\Pack; -use Pterodactyl\Models\User; -use Pterodactyl\Models\Server; -use Pterodactyl\Models\Service; -use Pterodactyl\Models\Allocation; -use Pterodactyl\Models\ServiceOption; -use Pterodactyl\Services\UuidService; -use Pterodactyl\Models\ServerVariable; -use Pterodactyl\Models\ServiceVariable; -use GuzzleHttp\Exception\ClientException; -use GuzzleHttp\Exception\TransferException; -use Pterodactyl\Services\DeploymentService; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Exceptions\DisplayValidationException; - -class old_ServerRepository -{ - /** - * An array of daemon permission to assign to this server. - * - * @var array - */ - protected $daemonPermissions = [ - 's:*', - ]; - - /** - * Generates a SFTP username for a server given a server name. - * format: mumble_67c7a4b0. - * - * @param string $name - * @param null|string $identifier - * @return string - */ - protected function generateSFTPUsername($name, $identifier = null) - { - if (is_null($identifier) || ! ctype_alnum($identifier)) { - $unique = str_random(8); - } else { - if (strlen($identifier) < 8) { - $unique = $identifier . str_random((8 - strlen($identifier))); - } else { - $unique = substr($identifier, 0, 8); - } - } - - // Filter the Server Name - $name = trim(preg_replace('/[^\w]+/', '', $name), '_'); - $name = (strlen($name) < 1) ? str_random(6) : $name; - - return strtolower(substr($name, 0, 6) . '_' . $unique); - } - - /** - * Adds a new server to the system. - * - * @param array $data - * @return \Pterodactyl\Models\Server - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\AutoDeploymentException - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function create(array $data) - { - $validator = Validator::make($data, [ - 'user_id' => 'required|exists:users,id', - 'name' => 'required|regex:/^([\w .-]{1,200})$/', - 'description' => 'sometimes|nullable|string', - 'memory' => 'required|numeric|min:0', - 'swap' => 'required|numeric|min:-1', - 'io' => 'required|numeric|min:10|max:1000', - 'cpu' => 'required|numeric|min:0', - 'disk' => 'required|numeric|min:0', - 'service_id' => 'required|numeric|min:1|exists:services,id', - 'option_id' => 'required|numeric|min:1|exists:service_options,id', - 'location_id' => 'required|numeric|min:1|exists:locations,id', - 'pack_id' => 'sometimes|nullable|numeric|min:0', - 'custom_container' => 'string', - 'startup' => 'string', - 'auto_deploy' => 'sometimes|required|accepted', - 'custom_id' => 'sometimes|required|numeric|unique:servers,id', - 'skip_scripts' => 'sometimes|required|boolean', - ]); - - $validator->sometimes('node_id', 'required|numeric|min:1|exists:nodes,id', function ($input) { - return ! ($input->auto_deploy); - }); - - $validator->sometimes('allocation_id', 'required|numeric|exists:allocations,id', function ($input) { - return ! ($input->auto_deploy); - }); - - $validator->sometimes('allocation_additional.*', 'sometimes|required|numeric|exists:allocations,id', function ($input) { - return ! ($input->auto_deploy); - }); - - // Run validator, throw catchable and displayable exception if it fails. - // Exception includes a JSON result of failed validation rules. - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - $user = User::findOrFail($data['user_id']); - - $deployment = false; - if (isset($data['auto_deploy'])) { - $deployment = new DeploymentService; - - if (isset($data['location_id'])) { - $deployment->setLocation($data['location_id']); - } - - $deployment->setMemory($data['memory'])->setDisk($data['disk'])->select(); - } - - $node = (! $deployment) ? Node::findOrFail($data['node_id']) : $deployment->node(); - - // Verify IP & Port are a.) free and b.) assigned to the node. - // We know the node exists because of 'exists:nodes,id' in the validation - if (! $deployment) { - $allocation = Allocation::where('id', $data['allocation_id'])->where('node_id', $data['node_id'])->whereNull('server_id')->first(); - } else { - $allocation = $deployment->allocation(); - } - - // Something failed in the query, either that combo doesn't exist, or it is in use. - if (! $allocation) { - throw new DisplayException('The selected Allocation ID is either already in use, or unavaliable for this node.'); - } - - // Validate those Service Option Variables - // We know the service and option exists because of the validation. - // We need to verify that the option exists for the service, and then check for - // any required variable fields. (fields are labeled env_) - $option = ServiceOption::where('id', $data['option_id'])->where('service_id', $data['service_id'])->first(); - if (! $option) { - throw new DisplayException('The requested service option does not exist for the specified service.'); - } - - // Validate the Pack - if (! isset($data['pack_id']) || (int) $data['pack_id'] < 1) { - $data['pack_id'] = null; - } else { - $pack = Pack::where('id', $data['pack_id'])->where('option_id', $data['option_id'])->first(); - if (! $pack) { - throw new DisplayException('The requested service pack does not seem to exist for this combination.'); - } - } - - // Load up the Service Information - $service = Service::find($option->service_id); - - // Check those Variables - $variables = ServiceVariable::where('option_id', $data['option_id'])->get(); - $variableList = []; - if ($variables) { - foreach ($variables as $variable) { - - // Is the variable required? - if (! isset($data['env_' . $variable->env_variable])) { - if ($variable->required) { - throw new DisplayException('A required service option variable field (env_' . $variable->env_variable . ') was missing from the request.'); - } - $variableList[] = [ - 'id' => $variable->id, - 'env' => $variable->env_variable, - 'val' => $variable->default_value, - ]; - continue; - } - - // Check aganist Regex Pattern - if (! is_null($variable->regex) && ! preg_match($variable->regex, $data['env_' . $variable->env_variable])) { - throw new DisplayException('Failed to validate service option variable field (env_' . $variable->env_variable . ') aganist regex (' . $variable->regex . ').'); - } - - $variableList[] = [ - 'id' => $variable->id, - 'env' => $variable->env_variable, - 'val' => $data['env_' . $variable->env_variable], - ]; - continue; - } - } - - // Check Overallocation - if (! $deployment) { - if (is_numeric($node->memory_overallocate) || is_numeric($node->disk_overallocate)) { - $totals = Server::select(DB::raw('SUM(memory) as memory, SUM(disk) as disk'))->where('node_id', $node->id)->first(); - - // Check memory limits - if (is_numeric($node->memory_overallocate)) { - $newMemory = $totals->memory + $data['memory']; - $memoryLimit = ($node->memory * (1 + ($node->memory_overallocate / 100))); - if ($newMemory > $memoryLimit) { - throw new DisplayException('The amount of memory allocated to this server would put the node over its allocation limits. This node is allowed ' . ($node->memory_overallocate + 100) . '% of its assigned ' . $node->memory . 'Mb of memory (' . $memoryLimit . 'Mb) of which ' . (($totals->memory / $node->memory) * 100) . '% (' . $totals->memory . 'Mb) is in use already. By allocating this server the node would be at ' . (($newMemory / $node->memory) * 100) . '% (' . $newMemory . 'Mb) usage.'); - } - } - - // Check Disk Limits - if (is_numeric($node->disk_overallocate)) { - $newDisk = $totals->disk + $data['disk']; - $diskLimit = ($node->disk * (1 + ($node->disk_overallocate / 100))); - if ($newDisk > $diskLimit) { - throw new DisplayException('The amount of disk allocated to this server would put the node over its allocation limits. This node is allowed ' . ($node->disk_overallocate + 100) . '% of its assigned ' . $node->disk . 'Mb of disk (' . $diskLimit . 'Mb) of which ' . (($totals->disk / $node->disk) * 100) . '% (' . $totals->disk . 'Mb) is in use already. By allocating this server the node would be at ' . (($newDisk / $node->disk) * 100) . '% (' . $newDisk . 'Mb) usage.'); - } - } - } - } - - DB::beginTransaction(); - - try { - $uuid = new UuidService; - - // Add Server to the Database - $server = new Server; - $genUuid = $uuid->generate('servers', 'uuid'); - $genShortUuid = $uuid->generateShort('servers', 'uuidShort', $genUuid); - - if (isset($data['custom_id'])) { - $server->id = $data['custom_id']; - } - - $server->fill([ - 'uuid' => $genUuid, - 'uuidShort' => $genShortUuid, - 'node_id' => $node->id, - 'name' => $data['name'], - 'description' => $data['description'], - 'skip_scripts' => isset($data['skip_scripts']), - 'suspended' => false, - 'owner_id' => $user->id, - 'memory' => $data['memory'], - 'swap' => $data['swap'], - 'disk' => $data['disk'], - 'io' => $data['io'], - 'cpu' => $data['cpu'], - 'oom_disabled' => isset($data['oom_disabled']), - 'allocation_id' => $allocation->id, - 'service_id' => $data['service_id'], - 'option_id' => $data['option_id'], - 'pack_id' => $data['pack_id'], - 'startup' => $data['startup'], - 'daemonSecret' => $uuid->generate('servers', 'daemonSecret'), - 'image' => (isset($data['custom_container']) && ! empty($data['custom_container'])) ? $data['custom_container'] : $option->docker_image, - 'username' => $this->generateSFTPUsername($data['name'], $genShortUuid), - 'sftp_password' => Crypt::encrypt('not set'), - ]); - $server->save(); - - // Mark Allocation in Use - $allocation->server_id = $server->id; - $allocation->save(); - - // Add Additional Allocations - if (isset($data['allocation_additional']) && is_array($data['allocation_additional'])) { - foreach ($data['allocation_additional'] as $allocation) { - $model = Allocation::where('id', $allocation)->where('node_id', $data['node_id'])->whereNull('server_id')->first(); - if (! $model) { - continue; - } - - $model->server_id = $server->id; - $model->save(); - } - } - - foreach ($variableList as $item) { - ServerVariable::create([ - 'server_id' => $server->id, - 'variable_id' => $item['id'], - 'variable_value' => $item['val'], - ]); - } - - $environment = $this->parseVariables($server); - $server->load('allocation', 'allocations'); - - $node->guzzleClient(['X-Access-Token' => $node->daemonSecret])->request('POST', '/servers', [ - 'json' => [ - 'uuid' => (string) $server->uuid, - 'user' => $server->username, - 'build' => [ - 'default' => [ - 'ip' => $server->allocation->ip, - 'port' => $server->allocation->port, - ], - 'ports' => $server->allocations->groupBy('ip')->map(function ($item) { - return $item->pluck('port'); - })->toArray(), - 'env' => $environment->pluck('value', 'variable')->toArray(), - 'memory' => (int) $server->memory, - 'swap' => (int) $server->swap, - 'io' => (int) $server->io, - 'cpu' => (int) $server->cpu, - 'disk' => (int) $server->disk, - 'image' => $server->image, - ], - 'service' => [ - 'type' => $service->folder, - 'option' => $option->tag, - 'pack' => (isset($pack)) ? $pack->uuid : null, - 'skip_scripts' => $server->skip_scripts, - ], - 'keys' => [ - (string) $server->daemonSecret => $this->daemonPermissions, - ], - 'rebuild' => false, - 'start_on_completion' => isset($data['start_on_completion']), - ], - ]); - - DB::commit(); - - return $server; - } catch (\Exception $ex) { - DB::rollBack(); - throw $ex; - } - } - - /** - * Update the details for a server. - * - * @param int $id - * @param array $data - * @return \Pterodactyl\Models\Server - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function updateDetails($id, array $data) - { - $uuid = new UuidService; - $resetDaemonKey = false; - - // Validate Fields - $validator = Validator::make($data, [ - 'owner_id' => 'sometimes|required|integer|exists:users,id', - 'name' => 'sometimes|required|regex:([\w .-]{1,200})', - 'description' => 'sometimes|nullable|string', - 'reset_token' => 'sometimes|required|accepted', - ]); - - // Run validator, throw catchable and displayable exception if it fails. - // Exception includes a JSON result of failed validation rules. - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - DB::beginTransaction(); - - try { - $server = Server::with('user')->findOrFail($id); - - // Update daemon secret if it was passed. - if (isset($data['reset_token']) || (isset($data['owner_id']) && (int) $data['owner_id'] !== $server->user->id)) { - $oldDaemonKey = $server->daemonSecret; - $server->daemonSecret = $uuid->generate('servers', 'daemonSecret'); - $resetDaemonKey = true; - } - - // Save our changes - $server->fill($data)->save(); - - // Do we need to update? If not, return successful. - if (! $resetDaemonKey) { - return DB::commit(); - } - - $res = $server->node->guzzleClient([ - 'X-Access-Server' => $server->uuid, - 'X-Access-Token' => $server->node->daemonSecret, - ])->request('PATCH', '/server', [ - 'exceptions' => false, - 'json' => [ - 'keys' => [ - (string) $oldDaemonKey => [], - (string) $server->daemonSecret => $this->daemonPermissions, - ], - ], - ]); - - if ($res->getStatusCode() === 204) { - DB::commit(); - - return $server; - } else { - throw new DisplayException('Daemon returned a a non HTTP/204 error code. HTTP/' + $res->getStatusCode()); - } - } catch (\Exception $ex) { - DB::rollBack(); - throw $ex; - } - } - - /** - * Update the container for a server. - * - * @param int $id - * @param array $data - * @return \Pterodactyl\Models\Server - * - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function updateContainer($id, array $data) - { - $validator = Validator::make($data, [ - 'docker_image' => 'required|string', - ]); - - // Run validator, throw catchable and displayable exception if it fails. - // Exception includes a JSON result of failed validation rules. - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - DB::beginTransaction(); - try { - $server = Server::findOrFail($id); - - $server->image = $data['docker_image']; - $server->save(); - - $server->node->guzzleClient([ - 'X-Access-Server' => $server->uuid, - 'X-Access-Token' => $server->node->daemonSecret, - ])->request('PATCH', '/server', [ - 'json' => [ - 'build' => [ - 'image' => $server->image, - ], - ], - ]); - - DB::commit(); - - return $server; - } catch (\Exception $ex) { - DB::rollBack(); - throw $ex; - } - } - - /** - * Update the build details for a server. - * - * @param int $id - * @param array $data - * @return \Pterodactyl\Models\Server - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function changeBuild($id, array $data) - { - $validator = Validator::make($data, [ - 'allocation_id' => 'sometimes|required|exists:allocations,id', - 'add_allocations' => 'sometimes|required|array', - 'remove_allocations' => 'sometimes|required|array', - 'memory' => 'sometimes|required|integer|min:0', - 'swap' => 'sometimes|required|integer|min:-1', - 'io' => 'sometimes|required|integer|min:10|max:1000', - 'cpu' => 'sometimes|required|integer|min:0', - 'disk' => 'sometimes|required|integer|min:0', - ]); - - // Run validator, throw catchable and displayable exception if it fails. - // Exception includes a JSON result of failed validation rules. - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - DB::beginTransaction(); - - try { - $server = Server::with('allocation', 'allocations')->findOrFail($id); - $newBuild = []; - $newAllocations = []; - - if (isset($data['allocation_id'])) { - if ((int) $data['allocation_id'] !== $server->allocation_id) { - $selection = $server->allocations->where('id', $data['allocation_id'])->first(); - if (! $selection) { - throw new DisplayException('The requested default connection is not allocated to this server.'); - } - - $server->allocation_id = $selection->id; - $newBuild['default'] = ['ip' => $selection->ip, 'port' => $selection->port]; - - $server->load('allocation'); - } - } - - $newPorts = false; - $firstNewAllocation = null; - // Add Assignments - if (isset($data['add_allocations'])) { - foreach ($data['add_allocations'] as $allocation) { - $model = Allocation::where('id', $allocation)->whereNull('server_id')->first(); - if (! $model) { - continue; - } - - $newPorts = true; - $firstNewAllocation = $firstNewAllocation ?? $model; - $model->update([ - 'server_id' => $server->id, - ]); - } - - $server->load('allocations'); - } - - // Remove Assignments - if (isset($data['remove_allocations'])) { - foreach ($data['remove_allocations'] as $allocation) { - // Can't remove the assigned IP/Port combo - if ((int) $allocation === $server->allocation_id) { - // No New Allocation - if (is_null($firstNewAllocation)) { - continue; - } - - // New Allocation, set as the default. - $server->allocation_id = $firstNewAllocation->id; - $newBuild['default'] = ['ip' => $firstNewAllocation->ip, 'port' => $firstNewAllocation->port]; - } - - $newPorts = true; - Allocation::where('id', $allocation)->where('server_id', $server->id)->update([ - 'server_id' => null, - ]); - } - - $server->load('allocations'); - } - - if ($newPorts) { - $newBuild['ports|overwrite'] = $server->allocations->groupBy('ip')->map(function ($item) { - return $item->pluck('port'); - })->toArray(); - - $newBuild['env|overwrite'] = $this->parseVariables($server)->pluck('value', 'variable')->toArray(); - } - - // @TODO: verify that server can be set to this much memory without - // going over node limits. - if (isset($data['memory']) && $server->memory !== (int) $data['memory']) { - $server->memory = $data['memory']; - $newBuild['memory'] = (int) $server->memory; - } - - if (isset($data['swap']) && $server->swap !== (int) $data['swap']) { - $server->swap = $data['swap']; - $newBuild['swap'] = (int) $server->swap; - } - - // @TODO: verify that server can be set to this much disk without - // going over node limits. - if (isset($data['disk']) && $server->disk !== (int) $data['disk']) { - $server->disk = $data['disk']; - $newBuild['disk'] = (int) $server->disk; - } - - if (isset($data['cpu']) && $server->cpu !== (int) $data['cpu']) { - $server->cpu = $data['cpu']; - $newBuild['cpu'] = (int) $server->cpu; - } - - if (isset($data['io']) && $server->io !== (int) $data['io']) { - $server->io = $data['io']; - $newBuild['io'] = (int) $server->io; - } - - // Try save() here so if it fails we haven't contacted the daemon - // This won't be committed unless the HTTP request succeedes anyways - $server->save(); - - if (! empty($newBuild)) { - $server->node->guzzleClient([ - 'X-Access-Server' => $server->uuid, - 'X-Access-Token' => $server->node->daemonSecret, - ])->request('PATCH', '/server', [ - 'json' => [ - 'build' => $newBuild, - ], - ]); - } - - DB::commit(); - - return $server; - } catch (\Exception $ex) { - DB::rollBack(); - throw $ex; - } - } - - /** - * Process the variables for a server, and save to the database. - * - * @param \Pterodactyl\Models\Server $server - * @param array $data - * @param bool $admin - * @return \Illuminate\Support\Collection - * - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - protected function processVariables(Server $server, $data, $admin = false) - { - $server->load('option.variables'); - - if ($admin) { - $server->startup = $data['startup']; - $server->save(); - } - - if ($server->option->variables) { - foreach ($server->option->variables as &$variable) { - $set = isset($data['env_' . $variable->id]); - - // If user is not an admin and are trying to edit a non-editable field - // or an invisible field just silently skip the variable. - if (! $admin && (! $variable->user_editable || ! $variable->user_viewable)) { - continue; - } - - // Perform Field Validation - $validator = Validator::make([ - 'variable_value' => ($set) ? $data['env_' . $variable->id] : null, - ], [ - 'variable_value' => $variable->rules, - ]); - - if ($validator->fails()) { - throw new DisplayValidationException(json_encode( - collect([ - 'notice' => ['There was a validation error with the `' . $variable->name . '` variable.'], - ])->merge($validator->errors()->toArray()) - )); - } - - $svar = ServerVariable::firstOrNew([ - 'server_id' => $server->id, - 'variable_id' => $variable->id, - ]); - - // Set the value; if one was not passed set it to the default value - if ($set) { - $svar->variable_value = $data['env_' . $variable->id]; - - // Not passed, check if this record exists if so keep value, otherwise set default - } else { - $svar->variable_value = ($svar->exists) ? $svar->variable_value : $variable->default_value; - } - - $svar->save(); - } - } - - return $this->parseVariables($server); - } - - /** - * Parse the variables and return in a standardized format. - * - * @param \Pterodactyl\Models\Server $server - * @return \Illuminate\Support\Collection - */ - protected function parseVariables(Server $server) - { - // Reload Variables - $server->load('variables'); - - $parsed = $server->option->variables->map(function ($item, $key) use ($server) { - $display = $server->variables->where('variable_id', $item->id)->pluck('variable_value')->first(); - - return [ - 'variable' => $item->env_variable, - 'value' => (! is_null($display)) ? $display : $item->default_value, - ]; - }); - - $merge = [[ - 'variable' => 'STARTUP', - 'value' => $server->startup, - ], [ - 'variable' => 'P_VARIABLE__LOCATION', - 'value' => $server->location->short, - ]]; - - $allocations = $server->allocations->where('id', '!=', $server->allocation_id); - $i = 0; - - foreach ($allocations as $allocation) { - $merge[] = [ - 'variable' => 'ALLOC_' . $i . '__PORT', - 'value' => $allocation->port, - ]; - - $i++; - } - - if ($parsed->count() === 0) { - return collect($merge); - } - - return $parsed->merge($merge); - } - - /** - * Update the startup details for a server. - * - * @param int $id - * @param array $data - * @param bool $admin - * @return bool - * - * @throws \GuzzleHttp\Exception\RequestException - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function updateStartup($id, array $data, $admin = false) - { - $server = Server::with('variables', 'option.variables')->findOrFail($id); - $hasServiceChanges = false; - - if ($admin) { - // User is an admin, lots of things to do here. - $validator = Validator::make($data, [ - 'startup' => 'required|string', - 'skip_scripts' => 'sometimes|required|boolean', - 'service_id' => 'required|numeric|min:1|exists:services,id', - 'option_id' => 'required|numeric|min:1|exists:service_options,id', - 'pack_id' => 'sometimes|nullable|numeric|min:0', - ]); - - if ((int) $data['pack_id'] < 1) { - $data['pack_id'] = null; - } - - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - if ( - $server->service_id != $data['service_id'] || - $server->option_id != $data['option_id'] || - $server->pack_id != $data['pack_id'] - ) { - $hasServiceChanges = true; - } - } - - // If user isn't an administrator, this function is being access from the front-end - // Just try to update specific variables. - if (! $admin || ! $hasServiceChanges) { - return DB::transaction(function () use ($admin, $data, $server) { - $environment = $this->processVariables($server, $data, $admin); - - $server->node->guzzleClient([ - 'X-Access-Server' => $server->uuid, - 'X-Access-Token' => $server->node->daemonSecret, - ])->request('PATCH', '/server', [ - 'json' => [ - 'build' => [ - 'env|overwrite' => $environment->pluck('value', 'variable')->toArray(), - ], - ], - ]); - - return false; - }); - } - - // Validate those Service Option Variables - // We know the service and option exists because of the validation. - // We need to verify that the option exists for the service, and then check for - // any required variable fields. (fields are labeled env_) - $option = ServiceOption::where('id', $data['option_id'])->where('service_id', $data['service_id'])->first(); - if (! $option) { - throw new DisplayException('The requested service option does not exist for the specified service.'); - } - - // Validate the Pack - if (! isset($data['pack_id']) || (int) $data['pack_id'] < 1) { - $data['pack_id'] = null; - } else { - $pack = Pack::where('id', $data['pack_id'])->where('option_id', $data['option_id'])->first(); - if (! $pack) { - throw new DisplayException('The requested service pack does not seem to exist for this combination.'); - } - } - - return DB::transaction(function () use ($admin, $data, $server) { - $server->installed = 0; - $server->service_id = $data['service_id']; - $server->option_id = $data['option_id']; - $server->pack_id = $data['pack_id']; - $server->skip_scripts = isset($data['skip_scripts']); - $server->save(); - - $server->variables->each->delete(); - - $server->load('service', 'pack'); - - // Send New Environment - $environment = $this->processVariables($server, $data, $admin); - - $server->node->guzzleClient([ - 'X-Access-Server' => $server->uuid, - 'X-Access-Token' => $server->node->daemonSecret, - ])->request('POST', '/server/reinstall', [ - 'json' => [ - 'build' => [ - 'env|overwrite' => $environment->pluck('value', 'variable')->toArray(), - ], - 'service' => [ - 'type' => $server->option->service->folder, - 'option' => $server->option->tag, - 'pack' => (! is_null($server->pack_id)) ? $server->pack->uuid : null, - 'skip_scripts' => $server->skip_scripts, - ], - ], - ]); - - return true; - }); - } - - /** - * Delete a server from the system permanetly. - * - * @param int $id - * @param bool $force - * @return void - * - * @throws \Pterodactyl\Exceptions\DisplayException - */ - public function delete($id, $force = false) - { - $server = Server::with('node', 'allocations', 'variables')->findOrFail($id); - - // Due to MySQL lockouts if the daemon response fails, we need to - // delete the server from the daemon first. If it succeedes and then - // MySQL fails, users just need to force delete the server. - // - // If this is a force delete, continue anyways. - try { - $server->node->guzzleClient([ - 'X-Access-Token' => $server->node->daemonSecret, - 'X-Access-Server' => $server->uuid, - ])->request('DELETE', '/servers'); - } catch (ClientException $ex) { - // Exception is thrown on 4XX HTTP errors, so catch and determine - // if we should continue, or if there is a permissions error. - // - // Daemon throws a 404 if the server doesn't exist, if that is returned - // continue with deletion, even if not a force deletion. - $response = $ex->getResponse(); - if ($ex->getResponse()->getStatusCode() !== 404 && ! $force) { - throw new DisplayException($ex->getMessage()); - } - } catch (TransferException $ex) { - if (! $force) { - throw new DisplayException($ex->getMessage()); - } - } catch (\Exception $ex) { - throw $ex; - } - - DB::transaction(function () use ($server) { - $server->allocations->each(function ($item) { - $item->server_id = null; - $item->save(); - }); - - $server->variables->each->delete(); - - $server->load('subusers.permissions'); - $server->subusers->each(function ($subuser) { - $subuser->permissions->each->delete(); - $subuser->delete(); - }); - - $server->tasks->each->delete(); - - // Delete Databases - // This is the one un-recoverable point where - // transactions will not save us. - $repository = new DatabaseRepository; - $server->databases->each(function ($item) use ($repository) { - $repository->drop($item->id); - }); - - // Fully delete the server. - $server->delete(); - }); - } - - /** - * Toggle the install status of a serve. - * - * @param int $id - * @return bool - * - * @throws \Pterodactyl\Exceptions\DisplayException - */ - public function toggleInstall($id) - { - $server = Server::findOrFail($id); - if ($server->installed > 1) { - throw new DisplayException('This server was marked as having a failed install or being deleted, you cannot override this.'); - } - $server->installed = ! $server->installed; - - return $server->save(); - } - - /** - * Suspends or unsuspends a server. - * - * @param int $id - * @param bool $unsuspend - * @return void - */ - public function toggleAccess($id, $unsuspend = true) - { - $server = Server::with('node')->findOrFail($id); - - DB::transaction(function () use ($server, $unsuspend) { - if ( - (! $unsuspend && $server->suspended) || - ($unsuspend && ! $server->suspended) - ) { - return true; - } - - $server->suspended = ! $unsuspend; - $server->save(); - - $server->node->guzzleClient([ - 'X-Access-Token' => $server->node->daemonSecret, - 'X-Access-Server' => $server->uuid, - ])->request('POST', ($unsuspend) ? '/server/unsuspend' : '/server/suspend'); - }); - } - - /** - * Updates the SFTP password for a server. - * - * @param int $id - * @param string $password - * @return void - * - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function updateSFTPPassword($id, $password) - { - $server = Server::with('node')->findOrFail($id); - - $validator = Validator::make(['password' => $password], [ - 'password' => 'required|regex:/^((?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,})$/', - ]); - - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - DB::transaction(function () use ($password, $server) { - $server->sftp_password = Crypt::encrypt($password); - $server->save(); - - $server->node->guzzleClient([ - 'X-Access-Token' => $server->node->daemonSecret, - 'X-Access-Server' => $server->uuid, - ])->request('POST', '/server/password', [ - 'json' => ['password' => $password], - ]); - }); - } - - /** - * Marks a server for reinstallation on the node. - * - * @param int $id - * @return void - */ - public function reinstall($id) - { - $server = Server::with('node')->findOrFail($id); - - DB::transaction(function () use ($server) { - $server->installed = 0; - $server->save(); - - $server->node->guzzleClient([ - 'X-Access-Token' => $server->node->daemonSecret, - 'X-Access-Server' => $server->uuid, - ])->request('POST', '/server/reinstall'); - }); - } -} diff --git a/app/Repositories/Old/old_UserRepository.php b/app/Repositories/Old/old_UserRepository.php deleted file mode 100644 index 6f028a20..00000000 --- a/app/Repositories/Old/old_UserRepository.php +++ /dev/null @@ -1,182 +0,0 @@ - - * Some Modifications (c) 2015 Dylan Seidt . - * - * 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\Repositories; - -use DB; -use Auth; -use Hash; -use Settings; -use Validator; -use Pterodactyl\Models; -use Pterodactyl\Services\UuidService; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Exceptions\DisplayValidationException; - -class old_UserRepository -{ - /** - * Creates a user on the panel. Returns the created user's ID. - * - * @param array $data - * @return \Pterodactyl\Models\User - * - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function create(array $data) - { - $validator = Validator::make($data, [ - 'email' => 'required|email|unique:users,email', - 'username' => 'required|string|between:1,255|unique:users,username|' . Models\User::USERNAME_RULES, - 'name_first' => 'required|string|between:1,255', - 'name_last' => 'required|string|between:1,255', - 'password' => 'sometimes|nullable|' . Models\User::PASSWORD_RULES, - 'root_admin' => 'required|boolean', - 'custom_id' => 'sometimes|nullable|unique:users,id', - ]); - - // Run validator, throw catchable and displayable exception if it fails. - // Exception includes a JSON result of failed validation rules. - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - DB::beginTransaction(); - - try { - $user = new Models\User; - $uuid = new UuidService; - - // Support for API Services - if (isset($data['custom_id']) && ! is_null($data['custom_id'])) { - $user->id = $token; - } - - // UUIDs are not mass-fillable. - $user->uuid = $uuid->generate('users', 'uuid'); - - $user->fill([ - 'email' => $data['email'], - 'username' => $data['username'], - 'name_first' => $data['name_first'], - 'name_last' => $data['name_last'], - 'password' => (empty($data['password'])) ? 'unset' : Hash::make($data['password']), - 'root_admin' => $data['root_admin'], - 'language' => Settings::get('default_language', 'en'), - ]); - $user->save(); - - DB::commit(); - - return $user; - } catch (\Exception $ex) { - DB::rollBack(); - throw $ex; - } - } - - /** - * Updates a user on the panel. - * - * @param int $id - * @param array $data - * @return \Pterodactyl\Models\User - * - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function update($id, array $data) - { - $user = Models\User::findOrFail($id); - - $validator = Validator::make($data, [ - 'email' => 'sometimes|required|email|unique:users,email,' . $id, - 'username' => 'sometimes|required|string|between:1,255|unique:users,username,' . $user->id . '|' . Models\User::USERNAME_RULES, - 'name_first' => 'sometimes|required|string|between:1,255', - 'name_last' => 'sometimes|required|string|between:1,255', - 'password' => 'sometimes|nullable|' . Models\User::PASSWORD_RULES, - 'root_admin' => 'sometimes|required|boolean', - 'language' => 'sometimes|required|string|min:1|max:5', - 'use_totp' => 'sometimes|required|boolean', - 'totp_secret' => 'sometimes|required|size:16', - ]); - - // Run validator, throw catchable and displayable exception if it fails. - // Exception includes a JSON result of failed validation rules. - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - // The password and root_admin fields are not mass assignable. - if (! empty($data['password'])) { - $data['password'] = Hash::make($data['password']); - } else { - unset($data['password']); - } - - $user->fill($data)->save(); - - return $user; - } - - /** - * Deletes a user on the panel. - * - * @param int $id - * @return void - * @todo Move user self-deletion checking to the controller, rather than the repository. - * - * @throws \Pterodactyl\Exceptions\DisplayException - */ - public function delete($id) - { - $user = Models\User::findOrFail($id); - - if (Models\Server::where('owner_id', $id)->count() > 0) { - throw new DisplayException('Cannot delete a user with active servers attached to thier account.'); - } - - if (! is_null(Auth::user()) && (int) Auth::user()->id === (int) $id) { - throw new DisplayException('Cannot delete your own account.'); - } - - DB::beginTransaction(); - - try { - foreach (Models\Subuser::with('permissions')->where('user_id', $id)->get() as &$subuser) { - foreach ($subuser->permissions as &$permission) { - $permission->delete(); - } - - $subuser->delete(); - } - - $user->delete(); - DB::commit(); - } catch (\Exception $ex) { - DB::rollBack(); - throw $ex; - } - } -} diff --git a/app/Services/UserService.php b/app/Services/Users/CreationService.php similarity index 72% rename from app/Services/UserService.php rename to app/Services/Users/CreationService.php index a7c87c57..f6a60f1c 100644 --- a/app/Services/UserService.php +++ b/app/Services/Users/CreationService.php @@ -22,7 +22,7 @@ * SOFTWARE. */ -namespace Pterodactyl\Services; +namespace Pterodactyl\Services\Users; use Illuminate\Foundation\Application; use Illuminate\Contracts\Hashing\Hasher; @@ -32,7 +32,7 @@ use Pterodactyl\Notifications\AccountCreated; use Pterodactyl\Services\Helpers\TemporaryPasswordService; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; -class UserService +class CreationService { /** * @var \Illuminate\Foundation\Application @@ -40,9 +40,9 @@ class UserService protected $app; /** - * @var \Illuminate\Database\Connection + * @var \Illuminate\Database\ConnectionInterface */ - protected $database; + protected $connection; /** * @var \Illuminate\Contracts\Hashing\Hasher @@ -65,25 +65,25 @@ class UserService protected $repository; /** - * UserService constructor. + * CreationService constructor. * - * @param \Illuminate\Foundation\Application $application - * @param \Illuminate\Notifications\ChannelManager $notification - * @param \Illuminate\Database\ConnectionInterface $database - * @param \Illuminate\Contracts\Hashing\Hasher $hasher - * @param \Pterodactyl\Services\Helpers\TemporaryPasswordService $passwordService - * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository + * @param \Illuminate\Foundation\Application $application + * @param \Illuminate\Notifications\ChannelManager $notification + * @param \Illuminate\Database\ConnectionInterface $connection + * @param \Illuminate\Contracts\Hashing\Hasher $hasher + * @param \Pterodactyl\Services\Helpers\TemporaryPasswordService $passwordService + * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository */ public function __construct( Application $application, ChannelManager $notification, - ConnectionInterface $database, + ConnectionInterface $connection, Hasher $hasher, TemporaryPasswordService $passwordService, UserRepositoryInterface $repository ) { $this->app = $application; - $this->database = $database; + $this->connection = $connection; $this->hasher = $hasher; $this->notification = $notification; $this->passwordService = $passwordService; @@ -99,25 +99,22 @@ class UserService * @throws \Exception * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function create(array $data) + public function handle(array $data) { if (array_key_exists('password', $data) && ! empty($data['password'])) { $data['password'] = $this->hasher->make($data['password']); } - // Begin Transaction - $this->database->beginTransaction(); - + $this->connection->beginTransaction(); if (! isset($data['password']) || empty($data['password'])) { $data['password'] = $this->hasher->make(str_random(30)); $token = $this->passwordService->generateReset($data['email']); } $user = $this->repository->create($data); + $this->connection->commit(); - // Persist the data - $this->database->commit(); - + // @todo fire event, handle notification there $this->notification->send($user, $this->app->makeWith(AccountCreated::class, [ 'user' => [ 'name' => $user->name_first, @@ -128,24 +125,4 @@ class UserService return $user; } - - /** - * Update the user model instance. - * - * @param int $id - * @param array $data - * @return mixed - * - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - */ - public function update($id, array $data) - { - if (isset($data['password'])) { - $data['password'] = $this->hasher->make($data['password']); - } - - $user = $this->repository->update($id, $data); - - return $user; - } } diff --git a/app/Services/Users/DeletionService.php b/app/Services/Users/DeletionService.php new file mode 100644 index 00000000..5bf6a5b0 --- /dev/null +++ b/app/Services/Users/DeletionService.php @@ -0,0 +1,88 @@ +. + * + * 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\Users; + +use Illuminate\Contracts\Translation\Translator; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Models\User; + +class DeletionService +{ + /** + * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface + */ + protected $repository; + + /** + * @var \Illuminate\Contracts\Translation\Translator + */ + protected $translator; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $serverRepository; + + /** + * DeletionService constructor. + * + * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $serverRepository + * @param \Illuminate\Contracts\Translation\Translator $translator + * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository + */ + public function __construct( + ServerRepositoryInterface $serverRepository, + Translator $translator, + UserRepositoryInterface $repository + ) { + $this->repository = $repository; + $this->translator = $translator; + $this->serverRepository = $serverRepository; + } + + /** + * Delete a user from the panel only if they have no servers attached to their account. + * + * @param int|\Pterodactyl\Models\User $user + * @return bool|null + * + * @throws \Pterodactyl\Exceptions\DisplayException + */ + public function handle($user) + { + if (! $user instanceof User) { + $user = $this->repository->find($user); + } + + $servers = $this->serverRepository->findWhere([['owner_id', '=', $user->id]]); + if (count($servers) > 0) { + throw new DisplayException($this->translator->trans('admin/user.exceptions.user_has_servers')); + } + + return $this->repository->delete($user->id); + } +} diff --git a/app/Services/Users/UpdateService.php b/app/Services/Users/UpdateService.php new file mode 100644 index 00000000..6df7dc58 --- /dev/null +++ b/app/Services/Users/UpdateService.php @@ -0,0 +1,75 @@ +. + * + * 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\Users; + +use Illuminate\Contracts\Hashing\Hasher; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; + +class UpdateService +{ + /** + * @var \Illuminate\Contracts\Hashing\Hasher + */ + protected $hasher; + + /** + * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface + */ + protected $repository; + + /** + * UpdateService constructor. + * + * @param \Illuminate\Contracts\Hashing\Hasher $hasher + * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository + */ + public function __construct( + Hasher $hasher, + UserRepositoryInterface $repository + ) { + $this->hasher = $hasher; + $this->repository = $repository; + } + + /** + * Update the user model instance. + * + * @param int $id + * @param array $data + * @return mixed + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function handle($id, array $data) + { + if (isset($data['password'])) { + $data['password'] = $this->hasher->make($data['password']); + } + + $user = $this->repository->update($id, $data); + + return $user; + } +} diff --git a/resources/lang/en/admin/user.php b/resources/lang/en/admin/user.php new file mode 100644 index 00000000..b8d38d32 --- /dev/null +++ b/resources/lang/en/admin/user.php @@ -0,0 +1,33 @@ +. + * + * 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. + */ + +return [ + 'exceptions' => [ + 'user_has_servers' => 'Cannot delete a user with active servers attached to their account. Please delete their server\'s before continuing.', + ], + 'notices' => [ + 'account_created' => 'Account has been created successfully.', + 'account_updated' => 'Account has been successfully updated.', + ], +]; diff --git a/tests/Unit/Services/UserServiceTest.php b/tests/Unit/Services/Users/CreationServiceTest.php similarity index 80% rename from tests/Unit/Services/UserServiceTest.php rename to tests/Unit/Services/Users/CreationServiceTest.php index f02c5652..59067f9d 100644 --- a/tests/Unit/Services/UserServiceTest.php +++ b/tests/Unit/Services/Users/CreationServiceTest.php @@ -25,17 +25,17 @@ namespace Tests\Unit\Services; use Mockery as m; +use Pterodactyl\Services\Users\CreationService; use Tests\TestCase; use Illuminate\Foundation\Application; use Illuminate\Contracts\Hashing\Hasher; use Illuminate\Database\ConnectionInterface; use Illuminate\Notifications\ChannelManager; use Pterodactyl\Notifications\AccountCreated; -use Pterodactyl\Services\UserService; use Pterodactyl\Services\Helpers\TemporaryPasswordService; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; -class UserServiceTest extends TestCase +class CreationServiceTest extends TestCase { /** * @var \Illuminate\Foundation\Application @@ -68,7 +68,7 @@ class UserServiceTest extends TestCase protected $repository; /** - * @var \Pterodactyl\Services\UserService + * @var \Pterodactyl\Services\Users\CreationService */ protected $service; @@ -86,7 +86,7 @@ class UserServiceTest extends TestCase $this->passwordService = m::mock(TemporaryPasswordService::class); $this->repository = m::mock(UserRepositoryInterface::class); - $this->service = new UserService( + $this->service = new CreationService( $this->appMock, $this->notification, $this->database, @@ -99,7 +99,7 @@ class UserServiceTest extends TestCase /** * Test that a user is created when a password is passed. */ - public function test_user_creation_with_password() + public function testUserIsCreatedWhenPasswordIsProvided() { $user = (object) [ 'name_first' => 'FirstName', @@ -122,7 +122,7 @@ class UserServiceTest extends TestCase $this->notification->shouldReceive('send')->with($user, null)->once()->andReturnNull(); - $response = $this->service->create([ + $response = $this->service->handle([ 'password' => 'raw-password', ]); @@ -134,7 +134,7 @@ class UserServiceTest extends TestCase /** * Test that a user is created with a random password when no password is provided. */ - public function test_user_creation_without_password() + public function testUserIsCreatedWhenNoPasswordIsProvided() { $user = (object) [ 'name_first' => 'FirstName', @@ -145,7 +145,10 @@ class UserServiceTest extends TestCase $this->hasher->shouldNotReceive('make'); $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); $this->hasher->shouldReceive('make')->once()->andReturn('created-enc-password'); - $this->passwordService->shouldReceive('generateReset')->with('user@example.com')->once()->andReturn('random-token'); + $this->passwordService->shouldReceive('generateReset') + ->with('user@example.com') + ->once() + ->andReturn('random-token'); $this->repository->shouldReceive('create')->with([ 'password' => 'created-enc-password', @@ -163,7 +166,7 @@ class UserServiceTest extends TestCase $this->notification->shouldReceive('send')->with($user, null)->once()->andReturnNull(); - $response = $this->service->create([ + $response = $this->service->handle([ 'email' => 'user@example.com', ]); @@ -172,31 +175,4 @@ class UserServiceTest extends TestCase $this->assertEquals($user->name_first, 'FirstName'); $this->assertEquals($user->email, $response->email); } - - /** - * Test that passing no password will not attempt any hashing. - */ - public function test_user_update_without_password() - { - $this->hasher->shouldNotReceive('make'); - $this->repository->shouldReceive('update')->with(1, ['email' => 'new@example.com'])->once()->andReturnNull(); - - $response = $this->service->update(1, ['email' => 'new@example.com']); - - $this->assertNull($response); - } - - /** - * Test that passing a password will hash it before storage. - */ - public function test_user_update_with_password() - { - $this->hasher->shouldReceive('make')->with('password')->once()->andReturn('enc-password'); - $this->repository->shouldReceive('update')->with(1, ['password' => 'enc-password'])->once()->andReturnNull(); - - $response = $this->service->update(1, ['password' => 'password']); - - $this->assertNull($response); - } - } diff --git a/tests/Unit/Services/Users/DeletionServiceTest.php b/tests/Unit/Services/Users/DeletionServiceTest.php new file mode 100644 index 00000000..6f21096e --- /dev/null +++ b/tests/Unit/Services/Users/DeletionServiceTest.php @@ -0,0 +1,120 @@ +. + * + * 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\Users; + +use Illuminate\Contracts\Translation\Translator; +use Mockery as m; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; +use Pterodactyl\Models\User; +use Pterodactyl\Services\Users\DeletionService; +use Tests\TestCase; + +class DeletionServiceTest extends TestCase +{ + /** + * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface + */ + protected $repository; + + /** + * @var \Illuminate\Contracts\Translation\Translator + */ + protected $translator; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $serverRepository; + + /** + * @var \Pterodactyl\Services\Users\DeletionService + */ + protected $service; + + /** + * @var User + */ + protected $user; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->user = factory(User::class)->make(); + $this->repository = m::mock(UserRepositoryInterface::class); + $this->translator = m::mock(Translator::class); + $this->serverRepository = m::mock(ServerRepositoryInterface::class); + + $this->service = new DeletionService( + $this->serverRepository, $this->translator, $this->repository + ); + } + + /** + * Test that a user is deleted if they have no servers. + */ + public function testUserIsDeletedIfNoServersAreAttachedToAccount() + { + $this->serverRepository->shouldReceive('findWhere')->with([['owner_id', '=', $this->user->id]])->once()->andReturn([]); + $this->repository->shouldReceive('delete')->with($this->user->id)->once()->andReturn(true); + + $this->assertTrue( + $this->service->handle($this->user), + 'Assert that service responds true.' + ); + } + + /** + * Test that an exception is thrown if trying to delete a user with servers. + * + * @expectedException \Pterodactyl\Exceptions\DisplayException + */ + public function testExceptionIsThrownIfServersAreAttachedToAccount() + { + $this->serverRepository->shouldReceive('findWhere')->with([['owner_id', '=', $this->user->id]])->once()->andReturn(['item']); + $this->translator->shouldReceive('trans')->with('admin/user.exceptions.user_has_servers')->once()->andReturnNull(); + + $this->service->handle($this->user); + } + + /** + * Test that the function supports passing in a model or an ID. + */ + public function testIntegerCanBePassedInPlaceOfUserModel() + { + $this->repository->shouldReceive('find')->with($this->user->id)->once()->andReturn($this->user); + $this->serverRepository->shouldReceive('findWhere')->with([['owner_id', '=', $this->user->id]])->once()->andReturn([]); + $this->repository->shouldReceive('delete')->with($this->user->id)->once()->andReturn(true); + + $this->assertTrue( + $this->service->handle($this->user->id), + 'Assert that service responds true.' + ); + } +} diff --git a/tests/Unit/Services/Users/UpdateServiceTest.php b/tests/Unit/Services/Users/UpdateServiceTest.php new file mode 100644 index 00000000..399a2f85 --- /dev/null +++ b/tests/Unit/Services/Users/UpdateServiceTest.php @@ -0,0 +1,83 @@ +. + * + * 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\Users; + +use Illuminate\Contracts\Hashing\Hasher; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; +use Pterodactyl\Services\Users\UpdateService; +use Tests\TestCase; +use Mockery as m; + +class UpdateServiceTest extends TestCase +{ + /** + * @var \Illuminate\Contracts\Hashing\Hasher + */ + protected $hasher; + + /** + * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Users\UpdateService + */ + protected $service; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->hasher = m::mock(Hasher::class); + $this->repository = m::mock(UserRepositoryInterface::class); + + $this->service = new UpdateService($this->hasher, $this->repository); + } + + /** + * Test that the handle function does not attempt to hash a password if no password is passed. + */ + public function testUpdateUserWithoutTouchingHasherIfNoPasswordPassed() + { + $this->repository->shouldReceive('update')->with(1, ['test-data' => 'value'])->once()->andReturnNull(); + + $this->assertNull($this->service->handle(1, ['test-data' => 'value'])); + } + + /** + * Test that the handle function hashes a password if passed in the data array. + */ + public function testUpdateUserAndHashPasswordIfProvided() + { + $this->hasher->shouldReceive('make')->with('raw_pass')->once()->andReturn('enc_pass'); + $this->repository->shouldReceive('update')->with(1, ['password' => 'enc_pass'])->once()->andReturnNull(); + + $this->assertNull($this->service->handle(1, ['password' => 'raw_pass'])); + } +} From 4391defb9fb74b7699fa82055f41d8f2678e4843 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Fri, 4 Aug 2017 19:22:56 -0500 Subject: [PATCH 39/99] Fix PHP7.0 builds failing due to cache --- .travis.yml | 7 +- composer.json | 16 ++--- composer.lock | 176 +++++++++++++++++++++++++------------------------- 3 files changed, 100 insertions(+), 99 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2e93a545..4ebd5b84 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,9 +4,10 @@ php: - '7.0' - '7.1' sudo: false -cache: - directories: - - $HOME/.composer/cache +cache: false +#cache: +# directories: +# - $HOME/.composer/cache services: - mysql before_install: diff --git a/composer.json b/composer.json index 34de41f7..a39f963c 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ "aws/aws-sdk-php": "3.29.7", "barryvdh/laravel-debugbar": "2.4.0", "daneeveritt/login-notifications": "1.0.0", - "doctrine/dbal": "2.5.12", + "doctrine/dbal": "2.5.13", "edvinaskrucas/settings": "2.0.0", "fideloper/proxy": "3.3.3", "guzzlehttp/guzzle": "6.2.3", @@ -41,13 +41,13 @@ "webpatser/laravel-uuid": "2.0.1" }, "require-dev": { - "barryvdh/laravel-ide-helper": "^2.3", - "friendsofphp/php-cs-fixer": "1.*", - "fzaninotto/faker": "~1.4", - "mockery/mockery": "0.9.*", - "php-mock/php-mock-phpunit": "^1.1", - "phpunit/phpunit": "~5.7", - "sllh/php-cs-fixer-styleci-bridge": "^2.1" + "barryvdh/laravel-ide-helper": "2.4.1", + "friendsofphp/php-cs-fixer": "1.13.1", + "fzaninotto/faker": "1.6.0", + "mockery/mockery": "0.9.9", + "php-mock/php-mock-phpunit": "1.1.2", + "phpunit/phpunit": "5.7.21", + "sllh/php-cs-fixer-styleci-bridge": "2.1.1" }, "autoload": { "classmap": [ diff --git a/composer.lock b/composer.lock index d6b01a57..97d0f37c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "48a6ed67ba0a480511075590af7f8eba", + "content-hash": "d4f8198c8d3d27408b5be1a525e8ad4b", "packages": [ { "name": "aws/aws-sdk-php", @@ -564,16 +564,16 @@ }, { "name": "doctrine/dbal", - "version": "v2.5.12", + "version": "v2.5.13", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "7b9e911f9d8b30d43b96853dab26898c710d8f44" + "reference": "729340d8d1eec8f01bff708e12e449a3415af873" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/7b9e911f9d8b30d43b96853dab26898c710d8f44", - "reference": "7b9e911f9d8b30d43b96853dab26898c710d8f44", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/729340d8d1eec8f01bff708e12e449a3415af873", + "reference": "729340d8d1eec8f01bff708e12e449a3415af873", "shasum": "" }, "require": { @@ -631,7 +631,7 @@ "persistence", "queryobject" ], - "time": "2017-02-08T12:53:47+00:00" + "time": "2017-07-22T20:44:48+00:00" }, { "name": "doctrine/inflector", @@ -1993,16 +1993,16 @@ }, { "name": "nikic/php-parser", - "version": "v3.0.6", + "version": "v3.1.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "0808939f81c1347a3c8a82a5925385a08074b0f1" + "reference": "4d4896e553f2094e657fe493506dc37c509d4e2b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/0808939f81c1347a3c8a82a5925385a08074b0f1", - "reference": "0808939f81c1347a3c8a82a5925385a08074b0f1", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4d4896e553f2094e657fe493506dc37c509d4e2b", + "reference": "4d4896e553f2094e657fe493506dc37c509d4e2b", "shasum": "" }, "require": { @@ -2040,7 +2040,7 @@ "parser", "php" ], - "time": "2017-06-28T20:53:48+00:00" + "time": "2017-07-28T14:45:09+00:00" }, { "name": "paragonie/random_compat", @@ -2350,16 +2350,16 @@ }, { "name": "psy/psysh", - "version": "v0.8.10", + "version": "v0.8.11", "source": { "type": "git", "url": "https://github.com/bobthecow/psysh.git", - "reference": "7ab97e5a32202585309f3ee35a0c08d2a8e588b1" + "reference": "b193cd020e8c6b66cea6457826ae005e94e6d2c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/bobthecow/psysh/zipball/7ab97e5a32202585309f3ee35a0c08d2a8e588b1", - "reference": "7ab97e5a32202585309f3ee35a0c08d2a8e588b1", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/b193cd020e8c6b66cea6457826ae005e94e6d2c0", + "reference": "b193cd020e8c6b66cea6457826ae005e94e6d2c0", "shasum": "" }, "require": { @@ -2419,7 +2419,7 @@ "interactive", "shell" ], - "time": "2017-07-22T15:14:19+00:00" + "time": "2017-07-29T19:30:02+00:00" }, { "name": "ramsey/uuid", @@ -2657,16 +2657,16 @@ }, { "name": "spatie/fractalistic", - "version": "2.3.1", + "version": "2.3.2", "source": { "type": "git", "url": "https://github.com/spatie/fractalistic.git", - "reference": "2bba98fd266d4691395904be6d981bd09150802f" + "reference": "012c4182203ba9127bb0a31cec3c211ce68227d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/fractalistic/zipball/2bba98fd266d4691395904be6d981bd09150802f", - "reference": "2bba98fd266d4691395904be6d981bd09150802f", + "url": "https://api.github.com/repos/spatie/fractalistic/zipball/012c4182203ba9127bb0a31cec3c211ce68227d9", + "reference": "012c4182203ba9127bb0a31cec3c211ce68227d9", "shasum": "" }, "require": { @@ -2704,7 +2704,7 @@ "spatie", "transform" ], - "time": "2017-07-21T23:08:30+00:00" + "time": "2017-07-24T08:06:12+00:00" }, { "name": "spatie/laravel-fractal", @@ -2820,16 +2820,16 @@ }, { "name": "symfony/console", - "version": "v3.3.5", + "version": "v3.3.6", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "a97e45d98c59510f085fa05225a1acb74dfe0546" + "reference": "b0878233cb5c4391347e5495089c7af11b8e6201" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/a97e45d98c59510f085fa05225a1acb74dfe0546", - "reference": "a97e45d98c59510f085fa05225a1acb74dfe0546", + "url": "https://api.github.com/repos/symfony/console/zipball/b0878233cb5c4391347e5495089c7af11b8e6201", + "reference": "b0878233cb5c4391347e5495089c7af11b8e6201", "shasum": "" }, "require": { @@ -2885,11 +2885,11 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2017-07-03T13:19:36+00:00" + "time": "2017-07-29T21:27:59+00:00" }, { "name": "symfony/css-selector", - "version": "v3.3.5", + "version": "v3.3.6", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", @@ -2942,16 +2942,16 @@ }, { "name": "symfony/debug", - "version": "v3.3.5", + "version": "v3.3.6", "source": { "type": "git", "url": "https://github.com/symfony/debug.git", - "reference": "63b85a968486d95ff9542228dc2e4247f16f9743" + "reference": "7c13ae8ce1e2adbbd574fc39de7be498e1284e13" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/63b85a968486d95ff9542228dc2e4247f16f9743", - "reference": "63b85a968486d95ff9542228dc2e4247f16f9743", + "url": "https://api.github.com/repos/symfony/debug/zipball/7c13ae8ce1e2adbbd574fc39de7be498e1284e13", + "reference": "7c13ae8ce1e2adbbd574fc39de7be498e1284e13", "shasum": "" }, "require": { @@ -2994,11 +2994,11 @@ ], "description": "Symfony Debug Component", "homepage": "https://symfony.com", - "time": "2017-07-05T13:02:37+00:00" + "time": "2017-07-28T15:27:31+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v3.3.5", + "version": "v3.3.6", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", @@ -3061,7 +3061,7 @@ }, { "name": "symfony/finder", - "version": "v3.3.5", + "version": "v3.3.6", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", @@ -3110,16 +3110,16 @@ }, { "name": "symfony/http-foundation", - "version": "v3.3.5", + "version": "v3.3.6", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "e307abe4b79ccbbfdced9b91c132fd128f456bc5" + "reference": "49e8cd2d59a7aa9bfab19e46de680c76e500a031" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e307abe4b79ccbbfdced9b91c132fd128f456bc5", - "reference": "e307abe4b79ccbbfdced9b91c132fd128f456bc5", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/49e8cd2d59a7aa9bfab19e46de680c76e500a031", + "reference": "49e8cd2d59a7aa9bfab19e46de680c76e500a031", "shasum": "" }, "require": { @@ -3159,20 +3159,20 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2017-07-17T14:07:10+00:00" + "time": "2017-07-21T11:04:46+00:00" }, { "name": "symfony/http-kernel", - "version": "v3.3.5", + "version": "v3.3.6", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "16ceea64d23abddf58797a782ae96a5242282cd8" + "reference": "db10d05f1d95e4168e638db7a81c79616f568ea5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/16ceea64d23abddf58797a782ae96a5242282cd8", - "reference": "16ceea64d23abddf58797a782ae96a5242282cd8", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/db10d05f1d95e4168e638db7a81c79616f568ea5", + "reference": "db10d05f1d95e4168e638db7a81c79616f568ea5", "shasum": "" }, "require": { @@ -3245,7 +3245,7 @@ ], "description": "Symfony HttpKernel Component", "homepage": "https://symfony.com", - "time": "2017-07-17T19:08:23+00:00" + "time": "2017-08-01T10:25:59+00:00" }, { "name": "symfony/polyfill-mbstring", @@ -3416,7 +3416,7 @@ }, { "name": "symfony/process", - "version": "v3.3.5", + "version": "v3.3.6", "source": { "type": "git", "url": "https://github.com/symfony/process.git", @@ -3465,16 +3465,16 @@ }, { "name": "symfony/routing", - "version": "v3.3.5", + "version": "v3.3.6", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "dc70bbd0ca7b19259f63cdacc8af370bc32a4728" + "reference": "4aee1a917fd4859ff8b51b9fd1dfb790a5ecfa26" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/dc70bbd0ca7b19259f63cdacc8af370bc32a4728", - "reference": "dc70bbd0ca7b19259f63cdacc8af370bc32a4728", + "url": "https://api.github.com/repos/symfony/routing/zipball/4aee1a917fd4859ff8b51b9fd1dfb790a5ecfa26", + "reference": "4aee1a917fd4859ff8b51b9fd1dfb790a5ecfa26", "shasum": "" }, "require": { @@ -3539,11 +3539,11 @@ "uri", "url" ], - "time": "2017-06-24T09:29:48+00:00" + "time": "2017-07-21T17:43:13+00:00" }, { "name": "symfony/translation", - "version": "v3.3.5", + "version": "v3.3.6", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", @@ -3608,16 +3608,16 @@ }, { "name": "symfony/var-dumper", - "version": "v3.3.5", + "version": "v3.3.6", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "0f32b62d21991700250fed5109b092949007c5b3" + "reference": "b2623bccb969ad595c2090f9be498b74670d0663" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/0f32b62d21991700250fed5109b092949007c5b3", - "reference": "0f32b62d21991700250fed5109b092949007c5b3", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/b2623bccb969ad595c2090f9be498b74670d0663", + "reference": "b2623bccb969ad595c2090f9be498b74670d0663", "shasum": "" }, "require": { @@ -3672,7 +3672,7 @@ "debug", "dump" ], - "time": "2017-07-10T14:18:27+00:00" + "time": "2017-07-28T06:06:09+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -4591,22 +4591,22 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "3.2.0", + "version": "3.2.1", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "46f7e8bb075036c92695b15a1ddb6971c751e585" + "reference": "183824db76118b9dddffc7e522b91fa175f75119" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/46f7e8bb075036c92695b15a1ddb6971c751e585", - "reference": "46f7e8bb075036c92695b15a1ddb6971c751e585", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/183824db76118b9dddffc7e522b91fa175f75119", + "reference": "183824db76118b9dddffc7e522b91fa175f75119", "shasum": "" }, "require": { "php": ">=5.5", "phpdocumentor/reflection-common": "^1.0@dev", - "phpdocumentor/type-resolver": "^0.4.0", + "phpdocumentor/type-resolver": "^0.3.0", "webmozart/assert": "^1.0" }, "require-dev": { @@ -4632,20 +4632,20 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2017-07-15T11:38:20+00:00" + "time": "2017-08-04T20:55:59+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "0.4.0", + "version": "0.3.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7" + "reference": "fb3933512008d8162b3cdf9e18dba9309b7c3773" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7", - "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/fb3933512008d8162b3cdf9e18dba9309b7c3773", + "reference": "fb3933512008d8162b3cdf9e18dba9309b7c3773", "shasum": "" }, "require": { @@ -4679,7 +4679,7 @@ "email": "me@mikevanriel.com" } ], - "time": "2017-07-14T14:27:02+00:00" + "time": "2017-06-03T08:32:36+00:00" }, { "name": "phpspec/prophecy", @@ -4946,29 +4946,29 @@ }, { "name": "phpunit/php-token-stream", - "version": "1.4.11", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "e03f8f67534427a787e21a385a67ec3ca6978ea7" + "reference": "ecb0b2cdaa0add708fe6f329ef65ae0c5225130b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/e03f8f67534427a787e21a385a67ec3ca6978ea7", - "reference": "e03f8f67534427a787e21a385a67ec3ca6978ea7", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/ecb0b2cdaa0add708fe6f329ef65ae0c5225130b", + "reference": "ecb0b2cdaa0add708fe6f329ef65ae0c5225130b", "shasum": "" }, "require": { "ext-tokenizer": "*", - "php": ">=5.3.3" + "php": "^7.0" }, "require-dev": { - "phpunit/phpunit": "~4.2" + "phpunit/phpunit": "^6.2.4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -4991,7 +4991,7 @@ "keywords": [ "tokenizer" ], - "time": "2017-02-27T10:12:30+00:00" + "time": "2017-08-03T14:17:41+00:00" }, { "name": "phpunit/phpunit", @@ -5759,7 +5759,7 @@ }, { "name": "symfony/class-loader", - "version": "v3.3.5", + "version": "v3.3.6", "source": { "type": "git", "url": "https://github.com/symfony/class-loader.git", @@ -5815,16 +5815,16 @@ }, { "name": "symfony/config", - "version": "v3.3.5", + "version": "v3.3.6", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "a094618deb9a3fe1c3cf500a796e167d0495a274" + "reference": "54ee12b0dd60f294132cabae6f5da9573d2e5297" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/a094618deb9a3fe1c3cf500a796e167d0495a274", - "reference": "a094618deb9a3fe1c3cf500a796e167d0495a274", + "url": "https://api.github.com/repos/symfony/config/zipball/54ee12b0dd60f294132cabae6f5da9573d2e5297", + "reference": "54ee12b0dd60f294132cabae6f5da9573d2e5297", "shasum": "" }, "require": { @@ -5873,11 +5873,11 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2017-06-16T12:40:34+00:00" + "time": "2017-07-19T07:37:29+00:00" }, { "name": "symfony/filesystem", - "version": "v3.3.5", + "version": "v3.3.6", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", @@ -5926,7 +5926,7 @@ }, { "name": "symfony/stopwatch", - "version": "v3.3.5", + "version": "v3.3.6", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", @@ -5975,16 +5975,16 @@ }, { "name": "symfony/yaml", - "version": "v3.3.5", + "version": "v3.3.6", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "1f93a8d19b8241617f5074a123e282575b821df8" + "reference": "ddc23324e6cfe066f3dd34a37ff494fa80b617ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/1f93a8d19b8241617f5074a123e282575b821df8", - "reference": "1f93a8d19b8241617f5074a123e282575b821df8", + "url": "https://api.github.com/repos/symfony/yaml/zipball/ddc23324e6cfe066f3dd34a37ff494fa80b617ed", + "reference": "ddc23324e6cfe066f3dd34a37ff494fa80b617ed", "shasum": "" }, "require": { @@ -6026,7 +6026,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2017-06-15T12:58:50+00:00" + "time": "2017-07-23T12:43:26+00:00" }, { "name": "webmozart/assert", From c1a078bdcfa325dbd52dbce608324cbe16d0c5b1 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 5 Aug 2017 17:20:07 -0500 Subject: [PATCH 40/99] Add support for node management actions using new services --- .../ConfigurationRepositoryInterface.php | 36 ++ .../Repository/NodeRepositoryInterface.php | 46 +++ .../Repository/RepositoryInterface.php | 8 + .../Controllers/Admin/DatabaseController.php | 1 + .../Controllers/Admin/NodesController.php | 310 +++++++++--------- app/Http/Requests/Admin/NodeFormRequest.php | 62 ++++ app/Models/Node.php | 107 ++++-- app/Models/Server.php | 38 +-- app/Providers/RepositoryServiceProvider.php | 10 +- app/Repositories/Daemon/BaseRepository.php | 1 + .../Daemon/ConfigurationRepository.php | 63 ++++ .../Eloquent/EloquentRepository.php | 8 + .../Eloquent/LocationRepository.php | 18 +- app/Repositories/Eloquent/NodeRepository.php | 99 ++++++ app/Repositories/Old/APIRepository.php | 207 ------------ app/Repositories/Old/DatabaseRepository.php | 173 ---------- app/Repositories/Old/LocationRepository.php | 104 ------ app/Services/Nodes/CreationService.php | 62 ++++ app/Services/Nodes/DeletionService.php | 88 +++++ app/Services/Nodes/UpdateService.php | 104 ++++++ app/Services/Users/DeletionService.php | 8 +- database/factories/ModelFactory.php | 18 +- ...ValuesForDatabaseHostWhenNodeIsDeleted.php | 34 ++ ...4_AllowNegativeValuesForOverallocation.php | 34 ++ resources/lang/en/admin/exceptions.php | 31 ++ resources/lang/en/admin/node.php | 36 ++ .../admin/nodes/view/settings.blade.php | 1 + routes/admin.php | 25 +- tests/TestCase.php | 2 - .../Services/Nodes/CreationServiceTest.php | 74 +++++ .../Services/Nodes/DeletionServiceTest.php | 121 +++++++ .../Unit/Services/Nodes/UpdateServiceTest.php | 182 ++++++++++ .../Services/Users/DeletionServiceTest.php | 9 +- 33 files changed, 1375 insertions(+), 745 deletions(-) create mode 100644 app/Contracts/Repository/Daemon/ConfigurationRepositoryInterface.php create mode 100644 app/Http/Requests/Admin/NodeFormRequest.php create mode 100644 app/Repositories/Daemon/ConfigurationRepository.php delete mode 100644 app/Repositories/Old/APIRepository.php delete mode 100644 app/Repositories/Old/DatabaseRepository.php delete mode 100644 app/Repositories/Old/LocationRepository.php create mode 100644 app/Services/Nodes/CreationService.php create mode 100644 app/Services/Nodes/DeletionService.php create mode 100644 app/Services/Nodes/UpdateService.php create mode 100644 database/migrations/2017_08_05_115800_CascadeNullValuesForDatabaseHostWhenNodeIsDeleted.php create mode 100644 database/migrations/2017_08_05_144104_AllowNegativeValuesForOverallocation.php create mode 100644 resources/lang/en/admin/exceptions.php create mode 100644 resources/lang/en/admin/node.php create mode 100644 tests/Unit/Services/Nodes/CreationServiceTest.php create mode 100644 tests/Unit/Services/Nodes/DeletionServiceTest.php create mode 100644 tests/Unit/Services/Nodes/UpdateServiceTest.php diff --git a/app/Contracts/Repository/Daemon/ConfigurationRepositoryInterface.php b/app/Contracts/Repository/Daemon/ConfigurationRepositoryInterface.php new file mode 100644 index 00000000..c56dde57 --- /dev/null +++ b/app/Contracts/Repository/Daemon/ConfigurationRepositoryInterface.php @@ -0,0 +1,36 @@ +. + * + * 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\Contracts\Repository\Daemon; + +interface ConfigurationRepositoryInterface extends BaseRepositoryInterface +{ + /** + * Update the configuration details for the specified node using data from the database. + * + * @param array $overrides + * @return \Psr\Http\Message\ResponseInterface + */ + public function update(array $overrides = []); +} diff --git a/app/Contracts/Repository/NodeRepositoryInterface.php b/app/Contracts/Repository/NodeRepositoryInterface.php index 1dcdad8e..51c6540e 100644 --- a/app/Contracts/Repository/NodeRepositoryInterface.php +++ b/app/Contracts/Repository/NodeRepositoryInterface.php @@ -28,6 +28,52 @@ use Pterodactyl\Contracts\Repository\Attributes\SearchableInterface; interface NodeRepositoryInterface extends RepositoryInterface, SearchableInterface { + /** + * Return the usage stats for a single node. + * + * @param int $id + * @return array + */ + public function getUsageStats($id); + + /** + * Return all available nodes with a searchable interface. + * + * @param int $count + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator + */ + public function getNodeListingData($count = 25); + + /** + * Return a single node with location and server information. + * + * @param int $id + * @return mixed + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function getSingleNode($id); + + /** + * Return a node with all of the associated allocations and servers that are attached to said allocations. + * + * @param int $id + * @return mixed + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function getNodeAllocations($id); + + /** + * Return a node with all of the servers attached to that node. + * + * @param int $id + * @return mixed + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function getNodeServers($id); + /** * Return a collection of nodes beloning to a specific location for use on frontend display. * diff --git a/app/Contracts/Repository/RepositoryInterface.php b/app/Contracts/Repository/RepositoryInterface.php index 1f498b6e..ad600817 100644 --- a/app/Contracts/Repository/RepositoryInterface.php +++ b/app/Contracts/Repository/RepositoryInterface.php @@ -116,6 +116,14 @@ interface RepositoryInterface */ public function findFirstWhere(array $fields); + /** + * Return a count of records matching the passed arguments. + * + * @param array $fields + * @return int + */ + public function findCountWhere(array $fields); + /** * Update a given ID with the passed array of fields. * diff --git a/app/Http/Controllers/Admin/DatabaseController.php b/app/Http/Controllers/Admin/DatabaseController.php index a383558b..4f61c848 100644 --- a/app/Http/Controllers/Admin/DatabaseController.php +++ b/app/Http/Controllers/Admin/DatabaseController.php @@ -132,6 +132,7 @@ class DatabaseController extends Controller * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ public function update(DatabaseHostFormRequest $request, DatabaseHost $host) { diff --git a/app/Http/Controllers/Admin/NodesController.php b/app/Http/Controllers/Admin/NodesController.php index ce02feba..76b83caf 100644 --- a/app/Http/Controllers/Admin/NodesController.php +++ b/app/Http/Controllers/Admin/NodesController.php @@ -24,48 +24,112 @@ namespace Pterodactyl\Http\Controllers\Admin; -use DB; +use Illuminate\Cache\Repository as CacheRepository; +use Illuminate\Contracts\Translation\Translator; use Log; use Alert; use Cache; use Javascript; -use Pterodactyl\Models; +use Prologue\Alerts\AlertsMessageBag; +use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; +use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Pterodactyl\Http\Requests\Admin\NodeFormRequest; +use Pterodactyl\Models\Allocation; +use Pterodactyl\Models\Node; use Illuminate\Http\Request; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Repositories\NodeRepository; use Pterodactyl\Exceptions\DisplayValidationException; +use Pterodactyl\Services\Nodes\CreationService; +use Pterodactyl\Services\Nodes\DeletionService; +use Pterodactyl\Services\Nodes\UpdateService; class NodesController extends Controller { + /** + * @var \Prologue\Alerts\AlertsMessageBag + */ + protected $alert; + + /** + * @var \Illuminate\Cache\Repository + */ + protected $cache; + + /** + * @var \Pterodactyl\Services\Nodes\CreationService + */ + protected $creationService; + + /** + * @var \Pterodactyl\Services\Nodes\DeletionService + */ + protected $deletionService; + + /** + * @var \Pterodactyl\Contracts\Repository\LocationRepositoryInterface + */ + protected $locationRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface + */ + protected $repository; + + /** + * @var \Illuminate\Contracts\Translation\Translator + */ + protected $translator; + + /** + * @var \Pterodactyl\Services\Nodes\UpdateService + */ + protected $updateService; + + public function __construct( + AlertsMessageBag $alert, + CacheRepository $cache, + CreationService $creationService, + DeletionService $deletionService, + LocationRepositoryInterface $locationRepository, + NodeRepositoryInterface $repository, + Translator $translator, + UpdateService $updateService + ) { + $this->alert = $alert; + $this->cache = $cache; + $this->creationService = $creationService; + $this->deletionService = $deletionService; + $this->locationRepository = $locationRepository; + $this->repository = $repository; + $this->translator = $translator; + $this->updateService = $updateService; + } + /** * Displays the index page listing all nodes on the panel. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ public function index(Request $request) { - $nodes = Models\Node::with('location')->withCount('servers'); - - if (! is_null($request->input('query'))) { - $nodes->search($request->input('query')); - } - - return view('admin.nodes.index', ['nodes' => $nodes->paginate(25)]); + return view('admin.nodes.index', [ + 'nodes' => $this->repository->search($request->input('query'))->getNodeListingData(), + ]); } /** * Displays create new node page. * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\View\View|\Illuminate\Http\RedirectResponse + * @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View */ - public function create(Request $request) + public function create() { - $locations = Models\Location::all(); - if (! $locations->count()) { - Alert::warning('You must add a location before you can add a new node.')->flash(); + $locations = $this->locationRepository->all(); + if (count($locations) < 1) { + $this->alert->warning($this->translator->trans('admin/node.notices.location_required'))->flash(); return redirect()->route('admin.locations'); } @@ -76,117 +140,68 @@ class NodesController extends Controller /** * Post controller to create a new node on the system. * - * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Http\Requests\Admin\NodeFormRequest $request * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function store(Request $request) + public function store(NodeFormRequest $request) { - try { - $repo = new NodeRepository; - $node = $repo->create(array_merge( - $request->only([ - 'public', 'disk_overallocate', - 'memory_overallocate', 'behind_proxy', - ]), - $request->intersect([ - 'name', 'location_id', 'fqdn', - 'scheme', 'memory', 'disk', - 'daemonBase', 'daemonSFTP', 'daemonListen', - ]) - )); - Alert::success('Successfully created new node that can be configured automatically on your remote machine by visiting the configuration tab. Before you can add any servers you need to first assign some IP addresses and ports by adding an allocation.')->flash(); + $node = $this->creationService->handle($request->normalize()); + $this->alert->info($this->translator->trans('admin/node.notices.node_created'))->flash(); - return redirect()->route('admin.nodes.view.allocation', $node->id); - } catch (DisplayValidationException $e) { - return redirect()->route('admin.nodes.new')->withErrors(json_decode($e->getMessage()))->withInput(); - } catch (DisplayException $e) { - Alert::danger($e->getMessage())->flash(); - } catch (\Exception $e) { - Log::error($e); - Alert::danger('An unhandled exception occured while attempting to add this node. Please try again.')->flash(); - } - - return redirect()->route('admin.nodes.new')->withInput(); + return redirect()->route('admin.nodes.view.allocation', $node->id); } /** * Shows the index overview page for a specific node. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param int $node * @return \Illuminate\View\View */ - public function viewIndex(Request $request, $id) + public function viewIndex($node) { - $node = Models\Node::with('location')->withCount('servers')->findOrFail($id); - $stats = collect( - Models\Server::select( - DB::raw('SUM(memory) as memory, SUM(disk) as disk') - )->where('node_id', $node->id)->first() - )->mapWithKeys(function ($item, $key) use ($node) { - if ($node->{$key . '_overallocate'} > 0) { - $withover = $node->{$key} * (1 + ($node->{$key . '_overallocate'} / 100)); - } else { - $withover = $node->{$key}; - } - - $percent = ($item / $withover) * 100; - - return [$key => [ - 'value' => number_format($item), - 'max' => number_format($withover), - 'percent' => $percent, - 'css' => ($percent <= 75) ? 'green' : (($percent > 90) ? 'red' : 'yellow'), - ]]; - })->toArray(); - - return view('admin.nodes.view.index', ['node' => $node, 'stats' => $stats]); + return view('admin.nodes.view.index', [ + 'node' => $this->repository->getSingleNode($node), + 'stats' => $this->repository->getUsageStats($node), + ]); } /** * Shows the settings page for a specific node. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Pterodactyl\Models\Node $node * @return \Illuminate\View\View */ - public function viewSettings(Request $request, $id) + public function viewSettings(Node $node) { return view('admin.nodes.view.settings', [ - 'node' => Models\Node::findOrFail($id), - 'locations' => Models\Location::all(), + 'node' => $node, + 'locations' => $this->locationRepository->all(), ]); } /** * Shows the configuration page for a specific node. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Pterodactyl\Models\Node $node * @return \Illuminate\View\View */ - public function viewConfiguration(Request $request, $id) + public function viewConfiguration(Node $node) { - return view('admin.nodes.view.configuration', [ - 'node' => Models\Node::findOrFail($id), - ]); + return view('admin.nodes.view.configuration', ['node' => $node]); } /** * Shows the allocation page for a specific node. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param int $node * @return \Illuminate\View\View */ - public function viewAllocation(Request $request, $id) + public function viewAllocation($node) { - $node = Models\Node::findOrFail($id); - $node->setRelation('allocations', $node->allocations()->orderBy('ip', 'asc')->orderBy('port', 'asc')->with('server')->paginate(50)); - - Javascript::put([ - 'node' => collect($node)->only(['id']), - ]); + $node = $this->repository->getNodeAllocations($node); + Javascript::put(['node' => collect($node)->only(['id'])]); return view('admin.nodes.view.allocation', ['node' => $node]); } @@ -194,69 +209,48 @@ class NodesController extends Controller /** * Shows the server listing page for a specific node. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param int $node * @return \Illuminate\View\View */ - public function viewServers(Request $request, $id) + public function viewServers($node) { - $node = Models\Node::with('servers.user', 'servers.service', 'servers.option')->findOrFail($id); + $node = $this->repository->getNodeServers($node); Javascript::put([ 'node' => collect($node->makeVisible('daemonSecret'))->only(['scheme', 'fqdn', 'daemonListen', 'daemonSecret']), ]); - return view('admin.nodes.view.servers', [ - 'node' => $node, - ]); + return view('admin.nodes.view.servers', ['node' => $node]); } /** * Updates settings for a node. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Pterodactyl\Http\Requests\Admin\NodeFormRequest $request + * @param \Pterodactyl\Models\Node $node * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function updateSettings(Request $request, $id) + public function updateSettings(NodeFormRequest $request, Node $node) { - $repo = new NodeRepository; + $this->updateService->handle($node, $request->normalize()); + $this->alert->success($this->translator->trans('admin/node.notices.node_updated'))->flash(); - try { - $node = $repo->update($id, array_merge( - $request->only([ - 'public', 'disk_overallocate', - 'memory_overallocate', 'behind_proxy', - ]), - $request->intersect([ - 'name', 'location_id', 'fqdn', - 'scheme', 'memory', 'disk', 'upload_size', - 'reset_secret', 'daemonSFTP', 'daemonListen', - ]) - )); - Alert::success('Successfully updated this node\'s information. If you changed any daemon settings you will need to restart it now.')->flash(); - } catch (DisplayValidationException $ex) { - return redirect()->route('admin.nodes.view.settings', $id)->withErrors(json_decode($ex->getMessage()))->withInput(); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An unhandled exception occured while attempting to edit this node. Please try again.')->flash(); - } - - return redirect()->route('admin.nodes.view.settings', $id)->withInput(); + return redirect()->route('admin.nodes.view.settings', $node->id)->withInput(); } /** * Removes a single allocation from a node. * - * @param \Illuminate\Http\Request $request - * @param int $node - * @param int $allocation + * @param \Illuminate\Http\Request $request + * @param int $node + * @param int $allocation * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse */ public function allocationRemoveSingle(Request $request, $node, $allocation) { - $query = Models\Allocation::where('node_id', $node)->whereNull('server_id')->where('id', $allocation)->delete(); + $query = Allocation::where('node_id', $node)->whereNull('server_id')->where('id', $allocation)->delete(); if ($query < 1) { return response()->json([ 'error' => 'Unable to find an allocation matching those details to delete.', @@ -269,13 +263,16 @@ class NodesController extends Controller /** * Remove all allocations for a specific IP at once on a node. * - * @param \Illuminate\Http\Request $request - * @param int $node + * @param \Illuminate\Http\Request $request + * @param int $node * @return \Illuminate\Http\RedirectResponse */ public function allocationRemoveBlock(Request $request, $node) { - $query = Models\Allocation::where('node_id', $node)->whereNull('server_id')->where('ip', $request->input('ip'))->delete(); + $query = Allocation::where('node_id', $node) + ->whereNull('server_id') + ->where('ip', $request->input('ip')) + ->delete(); if ($query < 1) { Alert::danger('There was an error while attempting to delete allocations on that IP.')->flash(); } else { @@ -288,8 +285,8 @@ class NodesController extends Controller /** * Sets an alias for a specific allocation on a node. * - * @param \Illuminate\Http\Request $request - * @param int $node + * @param \Illuminate\Http\Request $request + * @param int $node * @return \Illuminate\Http\Response */ public function allocationSetAlias(Request $request, $node) @@ -299,7 +296,7 @@ class NodesController extends Controller } try { - $update = Models\Allocation::findOrFail($request->input('allocation_id')); + $update = Allocation::findOrFail($request->input('allocation_id')); $update->ip_alias = (empty($request->input('alias'))) ? null : $request->input('alias'); $update->save(); @@ -312,8 +309,8 @@ class NodesController extends Controller /** * Creates new allocations on a node. * - * @param \Illuminate\Http\Request $request - * @param int $node + * @param \Illuminate\Http\Request $request + * @param int $node * @return \Illuminate\Http\RedirectResponse */ public function createAllocation(Request $request, $node) @@ -324,12 +321,16 @@ class NodesController extends Controller $repo->addAllocations($node, $request->intersect(['allocation_ip', 'allocation_alias', 'allocation_ports'])); Alert::success('Successfully added new allocations!')->flash(); } catch (DisplayValidationException $ex) { - return redirect()->route('admin.nodes.view.allocation', $node)->withErrors(json_decode($ex->getMessage()))->withInput(); + return redirect() + ->route('admin.nodes.view.allocation', $node) + ->withErrors(json_decode($ex->getMessage())) + ->withInput(); } catch (DisplayException $ex) { Alert::danger($ex->getMessage())->flash(); } catch (\Exception $ex) { Log::error($ex); - Alert::danger('An unhandled exception occured while attempting to add allocations this node. This error has been logged.')->flash(); + Alert::danger('An unhandled exception occured while attempting to add allocations this node. This error has been logged.') + ->flash(); } return redirect()->route('admin.nodes.view.allocation', $node); @@ -338,42 +339,29 @@ class NodesController extends Controller /** * Deletes a node from the system. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param $node * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\DisplayException */ - public function delete(Request $request, $id) + public function delete($node) { - $repo = new NodeRepository; + $this->deletionService->handle($node); + $this->alert->success($this->translator->trans('admin/node.notices.node_deleted'))->flash(); - try { - $repo->delete($id); - Alert::success('Successfully deleted the requested node from the panel.')->flash(); - - return redirect()->route('admin.nodes'); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An unhandled exception occured while attempting to delete this node. Please try again.')->flash(); - } - - return redirect()->route('admin.nodes.view', $id); + return redirect()->route('admin.nodes'); } /** * Returns the configuration token to auto-deploy a node. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Pterodactyl\Models\Node $node * @return \Illuminate\Http\JsonResponse */ - public function setToken(Request $request, $id) + public function setToken(Node $node) { - $node = Models\Node::findOrFail($id); - - $token = str_random(32); - Cache::tags(['Node:Configuration'])->put($token, $node->id, 5); + $token = bin2hex(random_bytes(16)); + $this->cache->tags(['Node:Configuration'])->put($token, $node->id, 5); return response()->json(['token' => $token]); } diff --git a/app/Http/Requests/Admin/NodeFormRequest.php b/app/Http/Requests/Admin/NodeFormRequest.php new file mode 100644 index 00000000..97080c3b --- /dev/null +++ b/app/Http/Requests/Admin/NodeFormRequest.php @@ -0,0 +1,62 @@ +. + * + * 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\Http\Requests\Admin; + +use Pterodactyl\Models\Node; + +class NodeFormRequest extends AdminFormRequest +{ + /** + * Get rules to apply to data in this request. + */ + public function rules() + { + if ($this->method() === 'PATCH') { + return Node::getUpdateRulesForId($this->route()->parameter('node')->id); + } + + return Node::getCreateRules(); + } + + /** + * Run validation after the rules above have been applied. + * + * @param \Illuminate\Validation\Validator $validator + */ + public function withValidator($validator) + { + $validator->after(function ($validator) { + // Check that the FQDN is a valid IP address. + if (! filter_var(gethostbyname($this->input('fqdn')), FILTER_VALIDATE_IP)) { + $validator->errors()->add('fqdn', trans('admin/node.validation.fqdn_not_resolvable')); + } + + // Check that if using HTTPS the FQDN is not an IP address. + if (filter_var($this->input('fqdn'), FILTER_VALIDATE_IP) && $this->input('scheme') === 'https') { + $validator->errors()->add('fqdn', trans('admin/node.validation.fqdn_required_for_ssl')); + } + }); + } +} diff --git a/app/Models/Node.php b/app/Models/Node.php index 9c454cfc..3f43a28a 100644 --- a/app/Models/Node.php +++ b/app/Models/Node.php @@ -25,13 +25,15 @@ namespace Pterodactyl\Models; use GuzzleHttp\Client; +use Sofa\Eloquence\Eloquence; +use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; use Illuminate\Notifications\Notifiable; -use Nicolaslopezj\Searchable\SearchableTrait; +use Sofa\Eloquence\Contracts\Validable as ValidableContract; -class Node extends Model +class Node extends Model implements ValidableContract { - use Notifiable, SearchableTrait; + use Eloquence, Notifiable, Validable; /** * The table associated with the model. @@ -47,20 +49,20 @@ class Node extends Model */ protected $hidden = ['daemonSecret']; - /** - * Cast values to correct type. - * - * @var array - */ - protected $casts = [ - 'public' => 'integer', - 'location_id' => 'integer', - 'memory' => 'integer', - 'disk' => 'integer', - 'daemonListen' => 'integer', - 'daemonSFTP' => 'integer', - 'behind_proxy' => 'boolean', - ]; + /** + * Cast values to correct type. + * + * @var array + */ + protected $casts = [ + 'public' => 'integer', + 'location_id' => 'integer', + 'memory' => 'integer', + 'disk' => 'integer', + 'daemonListen' => 'integer', + 'daemonSFTP' => 'integer', + 'behind_proxy' => 'boolean', + ]; /** * Fields that are mass assignable. @@ -81,22 +83,67 @@ class Node extends Model * * @var array */ - protected $searchable = [ - 'columns' => [ - 'nodes.name' => 10, - 'nodes.fqdn' => 8, - 'locations.short' => 4, - 'locations.long' => 4, - ], - 'joins' => [ - 'locations' => ['locations.id', 'nodes.location_id'], - ], - ]; + protected $searchableColumns = [ + 'name' => 10, + 'fqdn' => 8, + 'location.short' => 4, + 'location.long' => 4, + ]; + + /** + * @var array + */ + protected static $applicationRules = [ + 'name' => 'required', + 'location_id' => 'required', + 'fqdn' => 'required', + 'scheme' => 'required', + 'memory' => 'required', + 'memory_overallocate' => 'required', + 'disk' => 'required', + 'disk_overallocate' => 'required', + 'daemonBase' => 'sometimes|required', + 'daemonSFTP' => 'required', + 'daemonListen' => 'required', + ]; + + /** + * @var array + */ + protected static $dataIntegrityRules = [ + 'name' => 'regex:/^([\w .-]{1,100})$/', + 'location_id' => 'exists:locations,id', + 'public' => 'boolean', + 'fqdn' => 'string', + 'behind_proxy' => 'boolean', + 'memory' => 'numeric|min:1', + 'memory_overallocate' => 'numeric|min:-1', + 'disk' => 'numeric|min:1', + 'disk_overallocate' => 'numeric|min:-1', + 'daemonBase' => 'regex:/^([\/][\d\w.\-\/]+)$/', + 'daemonSFTP' => 'numeric|between:1024,65535', + 'daemonListen' => 'numeric|between:1024,65535', + ]; + + /** + * Default values for specific columns that are generally not changed on base installs. + * + * @var array + */ + protected $attributes = [ + 'public' => true, + 'behind_proxy' => false, + 'memory_overallocate' => 0, + 'disk_overallocate' => 0, + 'daemonBase' => '/srv/daemon-data', + 'daemonSFTP' => 2022, + 'daemonListen' => 8080, + ]; /** * Return an instance of the Guzzle client for this specific node. * - * @param array $headers + * @param array $headers * @return \GuzzleHttp\Client */ public function guzzleClient($headers = []) @@ -112,7 +159,7 @@ class Node extends Model /** * Returns the configuration in JSON format. * - * @param bool $pretty + * @param bool $pretty * @return string */ public function getConfigurationAsJson($pretty = false) diff --git a/app/Models/Server.php b/app/Models/Server.php index 5a147c3e..98871201 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -67,6 +67,9 @@ class Server extends Model implements ValidableContract */ protected $guarded = ['id', 'installed', 'created_at', 'updated_at', 'deleted_at']; + /** + * @var array + */ protected static $applicationRules = [ 'owner_id' => 'required', 'name' => 'required', @@ -83,6 +86,9 @@ class Server extends Model implements ValidableContract 'skip_scripts' => 'sometimes', ]; + /** + * @var array + */ protected static $dataIntegrityRules = [ 'owner_id' => 'exists:users,id', 'name' => 'regex:/^([\w .-]{1,200})$/', @@ -132,22 +138,15 @@ class Server extends Model implements ValidableContract * * @var array */ - protected $searchable = [ - 'columns' => [ - 'servers.name' => 10, - 'servers.username' => 10, - 'servers.uuidShort' => 9, - 'servers.uuid' => 8, - 'packs.name' => 7, - 'users.email' => 6, - 'users.username' => 6, - 'nodes.name' => 2, - ], - 'joins' => [ - 'packs' => ['packs.id', 'servers.pack_id'], - 'users' => ['users.id', 'servers.owner_id'], - 'nodes' => ['nodes.id', 'servers.node_id'], - ], + protected $searchableColumns = [ + 'name' => 10, + 'username' => 10, + 'uuidShort' => 9, + 'uuid' => 8, + 'pack.name' => 7, + 'user.email' => 6, + 'user.username' => 6, + 'node.name' => 2, ]; /** @@ -155,10 +154,11 @@ class Server extends Model implements ValidableContract * DO NOT USE THIS TO MODIFY SERVER DETAILS OR SAVE THOSE DETAILS. * YOU WILL OVERWRITE THE SECRET KEY AND BREAK THINGS. * - * @param string $uuid - * @param array $with - * @param array $withCount + * @param string $uuid + * @param array $with + * @param array $withCount * @return \Pterodactyl\Models\Server + * @throws \Exception * @todo Remove $with and $withCount due to cache issues, they aren't used anyways. */ public static function byUuid($uuid, array $with = [], array $withCount = []) diff --git a/app/Providers/RepositoryServiceProvider.php b/app/Providers/RepositoryServiceProvider.php index 07fc2d28..38c16371 100644 --- a/app/Providers/RepositoryServiceProvider.php +++ b/app/Providers/RepositoryServiceProvider.php @@ -28,6 +28,8 @@ use Illuminate\Support\ServiceProvider; use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; use Pterodactyl\Contracts\Repository\ApiPermissionRepositoryInterface; +use Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; @@ -36,6 +38,8 @@ use Pterodactyl\Contracts\Repository\OptionVariableRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; +use Pterodactyl\Repositories\Daemon\ConfigurationRepository; +use Pterodactyl\Repositories\Daemon\ServerRepository as DaemonServerRepository; use Pterodactyl\Repositories\Eloquent\AllocationRepository; use Pterodactyl\Repositories\Eloquent\ApiKeyRepository; use Pterodactyl\Repositories\Eloquent\ApiPermissionRepository; @@ -71,9 +75,7 @@ class RepositoryServiceProvider extends ServiceProvider $this->app->bind(UserRepositoryInterface::class, UserRepository::class); // Daemon Repositories - $this->app->bind( - \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface::class, - \Pterodactyl\Repositories\Daemon\ServerRepository::class - ); + $this->app->bind(ConfigurationRepositoryInterface::class, ConfigurationRepository::class); + $this->app->bind(DaemonServerRepositoryInterface::class, DaemonServerRepository::class); } } diff --git a/app/Repositories/Daemon/BaseRepository.php b/app/Repositories/Daemon/BaseRepository.php index c56b2e42..8a637e9f 100644 --- a/app/Repositories/Daemon/BaseRepository.php +++ b/app/Repositories/Daemon/BaseRepository.php @@ -51,6 +51,7 @@ class BaseRepository implements BaseRepositoryInterface public function setNode($id) { + // @todo accept a model $this->node = $this->nodeRepository->find($id); return $this; diff --git a/app/Repositories/Daemon/ConfigurationRepository.php b/app/Repositories/Daemon/ConfigurationRepository.php new file mode 100644 index 00000000..14f9436d --- /dev/null +++ b/app/Repositories/Daemon/ConfigurationRepository.php @@ -0,0 +1,63 @@ +. + * + * 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\Repositories\Daemon; + +use Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface; + +class ConfigurationRepository extends BaseRepository implements ConfigurationRepositoryInterface +{ + /** + * {@inheritdoc} + */ + public function update(array $overrides = []) + { + $node = $this->getNode(); + $structure = [ + 'web' => [ + 'listen' => $node->daemonListen, + 'ssl' => [ + 'enabled' => (! $node->behind_proxy && $node->scheme === 'https'), + ], + ], + 'sftp' => [ + 'path' => $node->daemonBase, + 'port' => $node->daemonSFTP, + ], + 'remote' => [ + 'base' => $this->config->get('app.url'), + ], + 'uploads' => [ + 'size_limit' => $node->upload_size, + ], + 'keys' => [ + $node->daemonSecret, + ], + ]; + + return $this->getHttpClient()->request('PATCH', '/config', [ + 'json' => array_merge($structure, $overrides), + ]); + } +} diff --git a/app/Repositories/Eloquent/EloquentRepository.php b/app/Repositories/Eloquent/EloquentRepository.php index fa39848a..c73f0935 100644 --- a/app/Repositories/Eloquent/EloquentRepository.php +++ b/app/Repositories/Eloquent/EloquentRepository.php @@ -105,6 +105,14 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf return $instance; } + /** + * {@inheritdoc}. + */ + public function findCountWhere(array $fields) + { + return $this->getBuilder()->where($fields)->count($this->getColumns()); + } + /** * {@inheritdoc} */ diff --git a/app/Repositories/Eloquent/LocationRepository.php b/app/Repositories/Eloquent/LocationRepository.php index 50d40073..0c04f39e 100644 --- a/app/Repositories/Eloquent/LocationRepository.php +++ b/app/Repositories/Eloquent/LocationRepository.php @@ -28,8 +28,9 @@ use Pterodactyl\Models\Location; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; +use Pterodactyl\Repositories\Eloquent\Attributes\SearchableRepository; -class LocationRepository extends EloquentRepository implements LocationRepositoryInterface +class LocationRepository extends SearchableRepository implements LocationRepositoryInterface { /** * @var string @@ -44,21 +45,6 @@ class LocationRepository extends EloquentRepository implements LocationRepositor return Location::class; } - /** - * {@inheritdoc} - */ - public function search($term) - { - if (empty($term)) { - return $this; - } - - $clone = clone $this; - $clone->searchTerm = $term; - - return $clone; - } - /** * {@inheritdoc} */ diff --git a/app/Repositories/Eloquent/NodeRepository.php b/app/Repositories/Eloquent/NodeRepository.php index 7a53ddac..2e18b1d4 100644 --- a/app/Repositories/Eloquent/NodeRepository.php +++ b/app/Repositories/Eloquent/NodeRepository.php @@ -25,6 +25,7 @@ namespace Pterodactyl\Repositories\Eloquent; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Models\Node; use Pterodactyl\Repositories\Eloquent\Attributes\SearchableRepository; @@ -38,6 +39,104 @@ class NodeRepository extends SearchableRepository implements NodeRepositoryInter return Node::class; } + /** + * {@inheritdoc} + */ + public function getUsageStats($id) + { + $node = $this->getBuilder()->select( + 'nodes.disk_overallocate', 'nodes.memory_overallocate', 'nodes.disk', 'nodes.memory', + $this->getBuilder()->raw('SUM(servers.memory) as sum_memory, SUM(servers.disk) as sum_disk') + )->join('servers', 'servers.node_id', '=', 'nodes.id') + ->where('nodes.id', $id) + ->first(); + + return collect(['disk' => $node->sum_disk, 'memory' => $node->sum_memory]) + ->mapWithKeys(function ($value, $key) use ($node) { + $maxUsage = $node->{$key}; + if ($node->{$key . '_overallocate'} > 0) { + $maxUsage = $node->{$key} * (1 + ($node->{$key . '_overallocate'} / 100)); + } + + $percent = ($value / $maxUsage) * 100; + + return [ + $key => [ + 'value' => number_format($value), + 'max' => number_format($maxUsage), + 'percent' => $percent, + 'css' => ($percent <= 75) ? 'green' : (($percent > 90) ? 'red' : 'yellow'), + ], + ]; + }) + ->toArray(); + } + + /** + * {@inheritdoc} + */ + public function getNodeListingData($count = 25) + { + $instance = $this->getBuilder()->with('location')->withCount('servers'); + + if ($this->searchTerm) { + $instance->search($this->searchTerm); + } + + return $instance->paginate($count, $this->getColumns()); + } + + /** + * {@inheritdoc} + */ + public function getSingleNode($id) + { + $instance = $this->getBuilder()->with('location')->withCount('servers')->find($id, $this->getColumns()); + + if (! $instance) { + throw new RecordNotFoundException(); + } + + return $instance; + } + + /** + * {@inheritdoc} + */ + public function getNodeAllocations($id) + { + $instance = $this->getBuilder()->find($id, $this->getColumns()); + + if (! $instance) { + throw new RecordNotFoundException(); + } + + $instance->setRelation( + 'allocations', + $this->getModel()->allocations()->orderBy('ip', 'asc') + ->orderBy('port', 'asc') + ->with('server') + ->paginate(50) + ); + + return $instance; + } + + /** + * {@inheritdoc} + */ + public function getNodeServers($id) + { + $instance = $this->getBuilder()->with('servers.user', 'servers.service', 'servers.option') + ->find($id, $this->getColumns()); + + if (! $instance) { + throw new RecordNotFoundException(); + } + + return $instance; + } + /** * {@inheritdoc} */ diff --git a/app/Repositories/Old/APIRepository.php b/app/Repositories/Old/APIRepository.php deleted file mode 100644 index 10af2515..00000000 --- a/app/Repositories/Old/APIRepository.php +++ /dev/null @@ -1,207 +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\Repositories; - -use DB; -use Auth; -use Crypt; -use Validator; -use IPTools\Network; -use Pterodactyl\Models\User; -use Pterodactyl\Models\APIKey as Key; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Models\APIPermission as Permission; -use Pterodactyl\Exceptions\DisplayValidationException; - -class APIRepository -{ - /** - * Holder for listing of allowed IPs when creating a new key. - * - * @var array - */ - protected $allowed = []; - - /** - * The eloquent model for a user. - * - * @var \Pterodactyl\Models\User - */ - protected $user; - - /** - * Constructor for API Repository. - * - * @param null|\Pterodactyl\Models\User $user - * @return void - */ - public function __construct(User $user = null) - { - $this->user = is_null($user) ? Auth::user() : $user; - if (is_null($this->user)) { - throw new \Exception('Unable to initialize user for API repository instance.'); - } - } - - /** - * Create a New API Keypair on the system. - * - * @param array $data - * @return string - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function create(array $data) - { - $validator = Validator::make($data, [ - 'memo' => 'string|max:500', - 'allowed_ips' => 'sometimes|string', - 'permissions' => 'sometimes|required|array', - 'admin_permissions' => 'sometimes|required|array', - ]); - - $validator->after(function ($validator) use ($data) { - if (array_key_exists('allowed_ips', $data) && ! empty($data['allowed_ips'])) { - foreach (explode("\n", $data['allowed_ips']) as $ip) { - $ip = trim($ip); - try { - Network::parse($ip); - array_push($this->allowed, $ip); - } catch (\Exception $ex) { - $validator->errors()->add('allowed_ips', 'Could not parse IP <' . $ip . '> because it is in an invalid format.'); - } - } - } - }); - - // Run validator, throw catchable and displayable exception if it fails. - // Exception includes a JSON result of failed validation rules. - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - DB::beginTransaction(); - try { - $secretKey = str_random(16) . '.' . str_random(7) . '.' . str_random(7); - $key = Key::create([ - 'user_id' => $this->user->id, - 'public' => str_random(16), - 'secret' => Crypt::encrypt($secretKey), - 'allowed_ips' => empty($this->allowed) ? null : json_encode($this->allowed), - 'memo' => $data['memo'], - 'expires_at' => null, - ]); - - $totalPermissions = 0; - $pNodes = Permission::permissions(); - - if (isset($data['permissions'])) { - foreach ($data['permissions'] as $permission) { - $parts = explode('-', $permission); - - if (count($parts) !== 2) { - continue; - } - - list($block, $search) = $parts; - - if (! array_key_exists($block, $pNodes['_user'])) { - continue; - } - - if (! in_array($search, $pNodes['_user'][$block])) { - continue; - } - - $totalPermissions++; - Permission::create([ - 'key_id' => $key->id, - 'permission' => 'user.' . $permission, - ]); - } - } - - if ($this->user->isRootAdmin() && isset($data['admin_permissions'])) { - unset($pNodes['_user']); - - foreach ($data['admin_permissions'] as $permission) { - $parts = explode('-', $permission); - - if (count($parts) !== 2) { - continue; - } - - list($block, $search) = $parts; - - if (! array_key_exists($block, $pNodes)) { - continue; - } - - if (! in_array($search, $pNodes[$block])) { - continue; - } - - $totalPermissions++; - Permission::create([ - 'key_id' => $key->id, - 'permission' => $permission, - ]); - } - } - - if ($totalPermissions < 1) { - throw new DisplayException('No valid permissions were passed.'); - } - - DB::commit(); - - return $secretKey; - } catch (\Exception $ex) { - DB::rollBack(); - throw $ex; - } - } - - /** - * Revokes an API key and associated permissions. - * - * @param string $key - * @return void - * - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException - */ - public function revoke($key) - { - DB::transaction(function () use ($key) { - $model = Key::with('permissions')->where('public', $key)->where('user_id', $this->user->id)->firstOrFail(); - foreach ($model->permissions as &$permission) { - $permission->delete(); - } - - $model->delete(); - }); - } -} diff --git a/app/Repositories/Old/DatabaseRepository.php b/app/Repositories/Old/DatabaseRepository.php deleted file mode 100644 index 1e4bc75a..00000000 --- a/app/Repositories/Old/DatabaseRepository.php +++ /dev/null @@ -1,173 +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\Repositories; - -use DB; -use Crypt; -use Validator; -use Pterodactyl\Models\Server; -use Pterodactyl\Models\Database; -use Pterodactyl\Models\DatabaseHost; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Exceptions\DisplayValidationException; - -class DatabaseRepository -{ - /** - * Adds a new database to a specified database host server. - * - * @param int $id - * @param array $data - * @return \Pterodactyl\Models\Database - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function create($id, array $data) - { - $server = Server::findOrFail($id); - - $validator = Validator::make($data, [ - 'host' => 'required|exists:database_hosts,id', - 'database' => 'required|regex:/^\w{1,100}$/', - 'connection' => 'required|regex:/^[0-9%.]{1,15}$/', - ]); - - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - $host = DatabaseHost::findOrFail($data['host']); - DB::beginTransaction(); - - try { - $database = Database::firstOrNew([ - 'server_id' => $server->id, - 'database_host_id' => $data['host'], - 'database' => sprintf('s%d_%s', $server->id, $data['database']), - ]); - - if ($database->exists) { - throw new DisplayException('A database with those details already exists in the system.'); - } - - $database->username = sprintf('s%d_%s', $server->id, str_random(10)); - $database->remote = $data['connection']; - $database->password = Crypt::encrypt(str_random(20)); - - $database->save(); - } catch (\Exception $ex) { - DB::rollBack(); - throw $ex; - } - - try { - $host->setDynamicConnection(); - - DB::connection('dynamic')->statement(sprintf('CREATE DATABASE IF NOT EXISTS `%s`', $database->database)); - DB::connection('dynamic')->statement(sprintf( - 'CREATE USER `%s`@`%s` IDENTIFIED BY \'%s\'', - $database->username, $database->remote, Crypt::decrypt($database->password) - )); - DB::connection('dynamic')->statement(sprintf( - 'GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, INDEX ON `%s`.* TO `%s`@`%s`', - $database->database, $database->username, $database->remote - )); - - DB::connection('dynamic')->statement('FLUSH PRIVILEGES'); - - // Save Everything - DB::commit(); - - return $database; - } catch (\Exception $ex) { - try { - DB::connection('dynamic')->statement(sprintf('DROP DATABASE IF EXISTS `%s`', $database->database)); - DB::connection('dynamic')->statement(sprintf('DROP USER IF EXISTS `%s`@`%s`', $database->username, $database->remote)); - DB::connection('dynamic')->statement('FLUSH PRIVILEGES'); - } catch (\Exception $ex) { - } - - DB::rollBack(); - throw $ex; - } - } - - /** - * Updates the password for a given database. - * - * @param int $id - * @param string $password - * @return void - * - * @todo Fix logic behind resetting passwords. - */ - public function password($id, $password) - { - $database = Database::with('host')->findOrFail($id); - $database->host->setDynamicConnection(); - - DB::transaction(function () use ($database, $password) { - $database->password = Crypt::encrypt($password); - - // We have to do the whole delete user, create user thing rather than - // SET PASSWORD ... because MariaDB and PHP statements ends up inserting - // a corrupted password. A way around this is strtoupper(sha1(sha1($password, true))) - // but no garuntees that will work correctly with every system. - DB::connection('dynamic')->statement(sprintf('DROP USER IF EXISTS `%s`@`%s`', $database->username, $database->remote)); - DB::connection('dynamic')->statement(sprintf( - 'CREATE USER `%s`@`%s` IDENTIFIED BY \'%s\'', - $database->username, $database->remote, $password - )); - DB::connection('dynamic')->statement(sprintf( - 'GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, INDEX ON `%s`.* TO `%s`@`%s`', - $database->database, $database->username, $database->remote - )); - DB::connection('dynamic')->statement('FLUSH PRIVILEGES'); - - $database->save(); - }); - } - - /** - * Drops a database from the associated database host. - * - * @param int $id - * @return void - */ - public function drop($id) - { - $database = Database::with('host')->findOrFail($id); - $database->host->setDynamicConnection(); - - DB::transaction(function () use ($database) { - DB::connection('dynamic')->statement(sprintf('DROP DATABASE IF EXISTS `%s`', $database->database)); - DB::connection('dynamic')->statement(sprintf('DROP USER IF EXISTS `%s`@`%s`', $database->username, $database->remote)); - DB::connection('dynamic')->statement('FLUSH PRIVILEGES'); - - $database->delete(); - }); - } -} diff --git a/app/Repositories/Old/LocationRepository.php b/app/Repositories/Old/LocationRepository.php deleted file mode 100644 index 5f08cfc1..00000000 --- a/app/Repositories/Old/LocationRepository.php +++ /dev/null @@ -1,104 +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\Repositories; - -use Validator; -use Pterodactyl\Models\Location; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Exceptions\DisplayValidationException; - -class LocationRepository -{ - /** - * Creates a new location on the system. - * - * @param array $data - * @return \Pterodactyl\Models\Location - * - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function create(array $data) - { - $validator = Validator::make($data, [ - 'short' => 'required|string|between:1,60|unique:locations,short', - 'long' => 'required|string|between:1,255', - ]); - - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - return Location::create([ - 'long' => $data['long'], - 'short' => $data['short'], - ]); - } - - /** - * Modifies a location. - * - * @param int $id - * @param array $data - * @return \Pterodactyl\Models\Location - * - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function update($id, array $data) - { - $location = Location::findOrFail($id); - - $validator = Validator::make($data, [ - 'short' => 'sometimes|required|string|between:1,60|unique:locations,short,' . $location->id, - 'long' => 'sometimes|required|string|between:1,255', - ]); - - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - $location->fill($data)->save(); - - return $location; - } - - /** - * Deletes a location from the system. - * - * @param int $id - * @return void - * - * @throws \Pterodactyl\Exceptions\DisplayException - */ - public function delete($id) - { - $location = Location::withCount('nodes')->findOrFail($id); - - if ($location->nodes_count > 0) { - throw new DisplayException('Cannot delete a location that has nodes assigned to it.'); - } - - $location->delete(); - } -} diff --git a/app/Services/Nodes/CreationService.php b/app/Services/Nodes/CreationService.php new file mode 100644 index 00000000..b33817f3 --- /dev/null +++ b/app/Services/Nodes/CreationService.php @@ -0,0 +1,62 @@ +. + * + * 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\Nodes; + +use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; + +class CreationService +{ + const DAEMON_SECRET_LENGTH = 18; + + /** + * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface + */ + protected $repository; + + /** + * CreationService constructor. + * + * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository + */ + public function __construct(NodeRepositoryInterface $repository) + { + $this->repository = $repository; + } + + /** + * Create a new node on the panel. + * + * @param array $data + * @return \Pterodactyl\Models\Node + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function handle(array $data) + { + $data['daemonSecret'] = bin2hex(random_bytes(self::DAEMON_SECRET_LENGTH)); + + return $this->repository->create($data); + } +} diff --git a/app/Services/Nodes/DeletionService.php b/app/Services/Nodes/DeletionService.php new file mode 100644 index 00000000..1b57c591 --- /dev/null +++ b/app/Services/Nodes/DeletionService.php @@ -0,0 +1,88 @@ +. + * + * 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\Nodes; + +use Illuminate\Contracts\Translation\Translator; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Models\Node; + +class DeletionService +{ + /** + * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $serverRepository; + + /** + * @var \Illuminate\Contracts\Translation\Translator + */ + protected $translator; + + /** + * DeletionService constructor. + * + * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository + * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $serverRepository + * @param \Illuminate\Contracts\Translation\Translator $translator + */ + public function __construct( + NodeRepositoryInterface $repository, + ServerRepositoryInterface $serverRepository, + Translator $translator + ) { + $this->repository = $repository; + $this->serverRepository = $serverRepository; + $this->translator = $translator; + } + + /** + * Delete a node from the panel if no servers are attached to it. + * + * @param int|\Pterodactyl\Models\Node $node + * @return bool|null + * + * @throws \Pterodactyl\Exceptions\DisplayException + */ + public function handle($node) + { + if ($node instanceof Node) { + $node = $node->id; + } + + $servers = $this->serverRepository->withColumns('id')->findCountWhere([['node_id', '=', $node]]); + if ($servers > 0) { + throw new DisplayException($this->translator->trans('admin/exceptions.node.servers_attached')); + } + + return $this->repository->delete($node); + } +} diff --git a/app/Services/Nodes/UpdateService.php b/app/Services/Nodes/UpdateService.php new file mode 100644 index 00000000..58336793 --- /dev/null +++ b/app/Services/Nodes/UpdateService.php @@ -0,0 +1,104 @@ +. + * + * 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\Nodes; + +use GuzzleHttp\Exception\RequestException; +use Illuminate\Log\Writer; +use Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface; +use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Models\Node; + +class UpdateService +{ + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface + */ + protected $configRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface + */ + protected $repository; + + /** + * @var \Illuminate\Log\Writer + */ + protected $writer; + + /** + * UpdateService constructor. + * + * @param \Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface $configurationRepository + * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository + * @param \Illuminate\Log\Writer $writer + */ + public function __construct( + ConfigurationRepositoryInterface $configurationRepository, + NodeRepositoryInterface $repository, + Writer $writer + ) { + $this->configRepository = $configurationRepository; + $this->repository = $repository; + $this->writer = $writer; + } + + /** + * Update the configuration values for a given node on the machine. + * + * @param int|\Pterodactyl\Models\Node $node + * @param array $data + * @return mixed + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function handle($node, array $data) + { + if (! $node instanceof Node) { + $node = $this->repository->find($node); + } + + if (! is_null(array_get($data, 'reset_secret'))) { + $data['daemonSecret'] = bin2hex(random_bytes(CreationService::DAEMON_SECRET_LENGTH)); + unset($data['reset_secret']); + } + + $updateResponse = $this->repository->withoutFresh()->update($node->id, $data); + + try { + $this->configRepository->setNode($node->id)->setAccessToken($node->daemonSecret)->update(); + } catch (RequestException $exception) { + $response = $exception->getResponse(); + $this->writer->warning($exception); + + throw new DisplayException(trans('admin/exceptions.node.daemon_off_config_updated', [ + 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), + ])); + } + + return $updateResponse; + } +} diff --git a/app/Services/Users/DeletionService.php b/app/Services/Users/DeletionService.php index 5bf6a5b0..3d307785 100644 --- a/app/Services/Users/DeletionService.php +++ b/app/Services/Users/DeletionService.php @@ -74,15 +74,15 @@ class DeletionService */ public function handle($user) { - if (! $user instanceof User) { - $user = $this->repository->find($user); + if ($user instanceof User) { + $user = $user->id; } - $servers = $this->serverRepository->findWhere([['owner_id', '=', $user->id]]); + $servers = $this->serverRepository->findWhere([['owner_id', '=', $user]]); if (count($servers) > 0) { throw new DisplayException($this->translator->trans('admin/user.exceptions.user_has_servers')); } - return $this->repository->delete($user->id); + return $this->repository->delete($user); } } diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index e517d580..c16652f0 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -15,7 +15,7 @@ $factory->define(Pterodactyl\Models\Server::class, function (Faker\Generator $faker) { return [ - 'id' => $faker->randomNumber(), + 'id' => $faker->unique()->randomNumber(), 'uuid' => $faker->uuid, 'uuidShort' => str_random(8), 'name' => $faker->firstName, @@ -40,7 +40,7 @@ $factory->define(Pterodactyl\Models\Server::class, function (Faker\Generator $fa $factory->define(Pterodactyl\Models\User::class, function (Faker\Generator $faker) { return [ - 'id' => $faker->randomNumber(), + 'id' => $faker->unique()->randomNumber(), 'external_id' => null, 'uuid' => $faker->uuid, 'username' => $faker->userName, @@ -56,19 +56,21 @@ $factory->define(Pterodactyl\Models\User::class, function (Faker\Generator $fake $factory->state(Pterodactyl\Models\User::class, 'admin', function () { return [ - 'root_admin' => true, + 'root_admin' => true, ]; }); $factory->define(Pterodactyl\Models\Location::class, function (Faker\Generator $faker) { return [ - 'short' => $faker->domainWord, - 'long' => $faker->catchPhrase, - ]; + 'id' => $faker->unique()->randomNumber(), + 'short' => $faker->domainWord, + 'long' => $faker->catchPhrase, + ]; }); $factory->define(Pterodactyl\Models\Node::class, function (Faker\Generator $faker) { return [ + 'id' => $faker->unique()->randomNumber(), 'public' => true, 'name' => $faker->firstName, 'fqdn' => $faker->ipv4, @@ -88,7 +90,7 @@ $factory->define(Pterodactyl\Models\Node::class, function (Faker\Generator $fake $factory->define(Pterodactyl\Models\ServiceVariable::class, function (Faker\Generator $faker) { return [ - 'id' => $faker->randomNumber(), + 'id' => $faker->unique()->randomNumber(), 'name' => $faker->firstName, 'description' => $faker->sentence(), 'env_variable' => strtoupper(str_replace(' ', '_', $faker->words(2, true))), @@ -98,7 +100,7 @@ $factory->define(Pterodactyl\Models\ServiceVariable::class, function (Faker\Gene 'rules' => 'required|string', 'created_at' => \Carbon\Carbon::now(), 'updated_at' => \Carbon\Carbon::now(), - ]; + ]; }); $factory->state(Pterodactyl\Models\ServiceVariable::class, 'viewable', function () { diff --git a/database/migrations/2017_08_05_115800_CascadeNullValuesForDatabaseHostWhenNodeIsDeleted.php b/database/migrations/2017_08_05_115800_CascadeNullValuesForDatabaseHostWhenNodeIsDeleted.php new file mode 100644 index 00000000..137384a8 --- /dev/null +++ b/database/migrations/2017_08_05_115800_CascadeNullValuesForDatabaseHostWhenNodeIsDeleted.php @@ -0,0 +1,34 @@ +dropForeign(['node_id']); + $table->foreign('node_id')->references('id')->on('nodes')->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('database_hosts', function (Blueprint $table) { + $table->dropForeign(['node_id']); + $table->foreign('node_id')->references('id')->on('nodes'); + }); + } +} diff --git a/database/migrations/2017_08_05_144104_AllowNegativeValuesForOverallocation.php b/database/migrations/2017_08_05_144104_AllowNegativeValuesForOverallocation.php new file mode 100644 index 00000000..60eadcaf --- /dev/null +++ b/database/migrations/2017_08_05_144104_AllowNegativeValuesForOverallocation.php @@ -0,0 +1,34 @@ +integer('disk_overallocate')->default(0)->nullable(false)->change(); + $table->integer('memory_overallocate')->default(0)->nullable(false)->change(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('nodes', function (Blueprint $table) { + $table->mediumInteger('disk_overallocate')->unsigned()->nullable()->change(); + $table->mediumInteger('memory_overallocate')->unsigned()->nullable()->change(); + }); + } +} diff --git a/resources/lang/en/admin/exceptions.php b/resources/lang/en/admin/exceptions.php new file mode 100644 index 00000000..1a5bcaa3 --- /dev/null +++ b/resources/lang/en/admin/exceptions.php @@ -0,0 +1,31 @@ +. + * + * 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. + */ + +return [ + 'daemon_connection_failed' => 'There was an exception while attempting to communicate with the daemon resulting in a HTTP/:code response code. This exception has been logged.', + 'node' => [ + 'servers_attached' => 'A node must have no servers linked to it in order to be deleted.', + 'daemon_off_config_updated' => 'The daemon configuration has been updated, however there was an error encountered while attempting to automatically update the configuration file on the Daemon. You will need to manually update the configuration file (core.json) for the daemon to apply these changes. The daemon responded with a HTTP/:code response code and the error has been logged.', + ], +]; diff --git a/resources/lang/en/admin/node.php b/resources/lang/en/admin/node.php new file mode 100644 index 00000000..fc5b0b1c --- /dev/null +++ b/resources/lang/en/admin/node.php @@ -0,0 +1,36 @@ +. + * + * 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. + */ + +return [ + 'validation' => [ + 'fqdn_not_resolvable' => 'The FQDN or IP address provided does not resolve to a valid IP address.', + 'fqdn_required_for_ssl' => 'A fully qualified domain name that resolves to a public IP address is required in order to use SSL for this node.', + ], + 'notices' => [ + 'node_deleted' => 'Node has been successfully removed from the panel.', + 'location_required' => 'You must have at least one location configured before you can add a node to this panel.', + 'node_created' => 'Successfully created new node. You can automatically configure the daemon on this machine by visiting the \'Configuration\' tab. Before you can add any servers you must first allocate at least one IP address and port.', + 'node_updated' => 'Node information has been updated. If any daemon settings were changed you will need to reboot it for those changes to take effect.', + ], +]; diff --git a/resources/themes/pterodactyl/admin/nodes/view/settings.blade.php b/resources/themes/pterodactyl/admin/nodes/view/settings.blade.php index b0624af2..a5b50d40 100644 --- a/resources/themes/pterodactyl/admin/nodes/view/settings.blade.php +++ b/resources/themes/pterodactyl/admin/nodes/view/settings.blade.php @@ -218,6 +218,7 @@
diff --git a/routes/admin.php b/routes/admin.php index 157109c1..31bc47a3 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -137,21 +137,22 @@ Route::group(['prefix' => 'servers'], function () { Route::group(['prefix' => 'nodes'], function () { Route::get('/', 'NodesController@index')->name('admin.nodes'); Route::get('/new', 'NodesController@create')->name('admin.nodes.new'); - Route::get('/view/{id}', 'NodesController@viewIndex')->name('admin.nodes.view'); - Route::get('/view/{id}/settings', 'NodesController@viewSettings')->name('admin.nodes.view.settings'); - Route::get('/view/{id}/configuration', 'NodesController@viewConfiguration')->name('admin.nodes.view.configuration'); - Route::get('/view/{id}/allocation', 'NodesController@viewAllocation')->name('admin.nodes.view.allocation'); - Route::get('/view/{id}/servers', 'NodesController@viewServers')->name('admin.nodes.view.servers'); - Route::get('/view/{id}/settings/token', 'NodesController@setToken')->name('admin.nodes.view.configuration.token'); + Route::get('/view/{node}', 'NodesController@viewIndex')->name('admin.nodes.view'); + Route::get('/view/{node}/settings', 'NodesController@viewSettings')->name('admin.nodes.view.settings'); + Route::get('/view/{node}/configuration', 'NodesController@viewConfiguration')->name('admin.nodes.view.configuration'); + Route::get('/view/{node}/allocation', 'NodesController@viewAllocation')->name('admin.nodes.view.allocation'); + Route::get('/view/{node}/servers', 'NodesController@viewServers')->name('admin.nodes.view.servers'); + Route::get('/view/{node}/settings/token', 'NodesController@setToken')->name('admin.nodes.view.configuration.token'); Route::post('/new', 'NodesController@store'); - Route::post('/view/{id}/settings', 'NodesController@updateSettings'); - Route::post('/view/{id}/allocation', 'NodesController@createAllocation'); - Route::post('/view/{id}/allocation/remove', 'NodesController@allocationRemoveBlock')->name('admin.nodes.view.allocation.removeBlock'); - Route::post('/view/{id}/allocation/alias', 'NodesController@allocationSetAlias')->name('admin.nodes.view.allocation.setAlias'); + Route::post('/view/{node}/allocation', 'NodesController@createAllocation'); + Route::post('/view/{node}/allocation/remove', 'NodesController@allocationRemoveBlock')->name('admin.nodes.view.allocation.removeBlock'); + Route::post('/view/{node}/allocation/alias', 'NodesController@allocationSetAlias')->name('admin.nodes.view.allocation.setAlias'); - Route::delete('/view/{id}/delete', 'NodesController@delete')->name('admin.nodes.view.delete'); - Route::delete('/view/{id}/allocation/remove/{allocation}', 'NodesController@allocationRemoveSingle')->name('admin.nodes.view.allocation.removeSingle'); + Route::patch('/view/{node}/settings', 'NodesController@updateSettings'); + + Route::delete('/view/{node}/delete', 'NodesController@delete')->name('admin.nodes.view.delete'); + Route::delete('/view/{node}/allocation/remove/{allocation}', 'NodesController@allocationRemoveSingle')->name('admin.nodes.view.allocation.removeSingle'); }); /* diff --git a/tests/TestCase.php b/tests/TestCase.php index d8c7f6ff..664e4a9c 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,7 +2,6 @@ namespace Tests; -use Mockery as m; use Illuminate\Foundation\Testing\TestCase as BaseTestCase; abstract class TestCase extends BaseTestCase @@ -12,6 +11,5 @@ abstract class TestCase extends BaseTestCase public function setUp() { parent::setUp(); - m::close(); } } diff --git a/tests/Unit/Services/Nodes/CreationServiceTest.php b/tests/Unit/Services/Nodes/CreationServiceTest.php new file mode 100644 index 00000000..5932e709 --- /dev/null +++ b/tests/Unit/Services/Nodes/CreationServiceTest.php @@ -0,0 +1,74 @@ +. + * + * 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\Nodes; + +use Mockery as m; +use phpmock\phpunit\PHPMock; +use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Pterodactyl\Services\Nodes\CreationService; +use Tests\TestCase; + +class CreationServiceTest extends TestCase +{ + use PHPMock; + + /** + * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Nodes\CreationService + */ + protected $service; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->repository = m::mock(NodeRepositoryInterface::class); + + $this->service = new CreationService($this->repository); + } + + /** + * Test that a node is created and a daemon secret token is created. + */ + public function testNodeIsCreatedAndDaemonSecretIsGenerated() + { + $this->getFunctionMock('\\Pterodactyl\\Services\\Nodes', 'bin2hex') + ->expects($this->once())->willReturn('hexResult'); + + $this->repository->shouldReceive('create')->with([ + 'name' => 'NodeName', + 'daemonSecret' => 'hexResult', + ])->once()->andReturnNull(); + + $this->assertNull($this->service->handle(['name' => 'NodeName'])); + } +} diff --git a/tests/Unit/Services/Nodes/DeletionServiceTest.php b/tests/Unit/Services/Nodes/DeletionServiceTest.php new file mode 100644 index 00000000..266fbc37 --- /dev/null +++ b/tests/Unit/Services/Nodes/DeletionServiceTest.php @@ -0,0 +1,121 @@ +. + * + * 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\Nodes; + +use Illuminate\Contracts\Translation\Translator; +use Mockery as m; +use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Models\Node; +use Pterodactyl\Services\Nodes\DeletionService; +use Tests\TestCase; + +class DeletionServiceTest extends TestCase +{ + /** + * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $serverRepository; + + /** + * @var \Illuminate\Contracts\Translation\Translator + */ + protected $translator; + + /** + * @var \Pterodactyl\Services\Nodes\DeletionService + */ + protected $service; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->repository = m::mock(NodeRepositoryInterface::class); + $this->serverRepository = m::mock(ServerRepositoryInterface::class); + $this->translator = m::mock(Translator::class); + + $this->service = new DeletionService( + $this->repository, + $this->serverRepository, + $this->translator + ); + } + + /** + * Test that a node is deleted if there are no servers attached to it. + */ + public function testNodeIsDeletedIfNoServersAreAttached() + { + $this->serverRepository->shouldReceive('withColumns')->with('id')->once()->andReturnSelf() + ->shouldReceive('findCountWhere')->with([['node_id', '=', 1]])->once()->andReturn(0); + $this->repository->shouldReceive('delete')->with(1)->once()->andReturn(true); + + $this->assertTrue( + $this->service->handle(1), + 'Assert that deletion returns a positive boolean value.' + ); + } + + /** + * Test that an exception is thrown if servers are attached to the node. + * + * @expectedException \Pterodactyl\Exceptions\DisplayException + */ + public function testExceptionIsThrownIfServersAreAttachedToNode() + { + $this->serverRepository->shouldReceive('withColumns')->with('id')->once()->andReturnSelf() + ->shouldReceive('findCountWhere')->with([['node_id', '=', 1]])->once()->andReturn(1); + $this->translator->shouldReceive('trans')->with('admin/exceptions.node.servers_attached')->once()->andReturnNull(); + $this->repository->shouldNotReceive('delete'); + + $this->service->handle(1); + } + + /** + * Test that a model can be passed into the handle function rather than an ID. + */ + public function testModelCanBePassedToFunctionInPlaceOfNodeId() + { + $node = factory(Node::class)->make(); + + $this->serverRepository->shouldReceive('withColumns')->with('id')->once()->andReturnSelf() + ->shouldReceive('findCountWhere')->with([['node_id', '=', $node->id]])->once()->andReturn(0); + $this->repository->shouldReceive('delete')->with($node->id)->once()->andReturn(true); + + $this->assertTrue( + $this->service->handle($node->id), + 'Assert that deletion returns a positive boolean value.' + ); + } +} diff --git a/tests/Unit/Services/Nodes/UpdateServiceTest.php b/tests/Unit/Services/Nodes/UpdateServiceTest.php new file mode 100644 index 00000000..9bccf2d4 --- /dev/null +++ b/tests/Unit/Services/Nodes/UpdateServiceTest.php @@ -0,0 +1,182 @@ +. + * + * 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\Nodes; + +use Exception; +use GuzzleHttp\Exception\RequestException; +use Illuminate\Log\Writer; +use Mockery as m; +use phpmock\phpunit\PHPMock; +use Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface; +use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Models\Node; +use Pterodactyl\Services\Nodes\CreationService; +use Pterodactyl\Services\Nodes\UpdateService; +use Tests\TestCase; + +class UpdateServiceTest extends TestCase +{ + use PHPMock; + + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface + */ + protected $configRepository; + + /** + * @var \GuzzleHttp\Exception\RequestException + */ + protected $exception; + + /** + * @var \Pterodactyl\Models\Node + */ + protected $node; + + /** + * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Nodes\UpdateService + */ + protected $service; + + /** + * @var \Illuminate\Log\Writer + */ + protected $writer; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->node = factory(Node::class)->make(); + + $this->configRepository = m::mock(ConfigurationRepositoryInterface::class); + $this->exception = m::mock(RequestException::class); + $this->repository = m::mock(NodeRepositoryInterface::class); + $this->writer = m::mock(Writer::class); + + $this->service = new UpdateService( + $this->configRepository, + $this->repository, + $this->writer + ); + } + + /** + * Test that the daemon secret is reset when `reset_secret` is passed in the data. + */ + public function testNodeIsUpdatedAndDaemonSecretIsReset() + { + $this->getFunctionMock('\\Pterodactyl\\Services\\Nodes', 'random_bytes') + ->expects($this->once())->willReturnCallback(function ($bytes) { + $this->assertEquals(CreationService::DAEMON_SECRET_LENGTH, $bytes); + + return '\00'; + }); + + $this->getFunctionMock('\\Pterodactyl\\Services\\Nodes', 'bin2hex') + ->expects($this->once())->willReturn('hexResponse'); + + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($this->node->id, [ + 'name' => 'NewName', + 'daemonSecret' => 'hexResponse', + ])->andReturn(true); + + $this->configRepository->shouldReceive('setNode')->with($this->node->id)->once()->andReturnSelf() + ->shouldReceive('setAccessToken')->with($this->node->daemonSecret)->once()->andReturnSelf() + ->shouldReceive('update')->withNoArgs()->once()->andReturnNull(); + + $this->assertTrue($this->service->handle($this->node, ['name' => 'NewName', 'reset_secret' => true])); + } + + /** + * Test that daemon secret is not modified when no variable is passed in data. + */ + public function testNodeIsUpdatedAndDaemonSecretIsNotChanged() + { + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($this->node->id, [ + 'name' => 'NewName', + ])->andReturn(true); + + $this->configRepository->shouldReceive('setNode')->with($this->node->id)->once()->andReturnSelf() + ->shouldReceive('setAccessToken')->with($this->node->daemonSecret)->once()->andReturnSelf() + ->shouldReceive('update')->withNoArgs()->once()->andReturnNull(); + + $this->assertTrue($this->service->handle($this->node, ['name' => 'NewName'])); + } + + /** + * Test that an exception caused by the daemon is handled properly. + */ + public function testExceptionCausedByDaemonIsHandled() + { + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($this->node->id, [ + 'name' => 'NewName', + ])->andReturn(true); + + $this->configRepository->shouldReceive('setNode')->with($this->node->id)->once()->andThrow($this->exception); + $this->writer->shouldReceive('warning')->with($this->exception)->once()->andReturnNull(); + $this->exception->shouldReceive('getResponse')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('getStatusCode')->withNoArgs()->once()->andReturn(400); + + try { + $this->service->handle($this->node, ['name' => 'NewName']); + } catch (Exception $exception) { + $this->assertInstanceOf(DisplayException::class, $exception); + $this->assertEquals( + trans('admin/exceptions.node.daemon_off_config_updated', ['code' => 400]), $exception->getMessage() + ); + } + } + + /** + * Test that an ID can be passed in place of a model. + */ + public function testFunctionCanAcceptANodeIdInPlaceOfModel() + { + $this->repository->shouldReceive('find')->with($this->node->id)->once()->andReturn($this->node); + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($this->node->id, [ + 'name' => 'NewName', + ])->andReturn(true); + + $this->configRepository->shouldReceive('setNode')->with($this->node->id)->once()->andReturnSelf() + ->shouldReceive('setAccessToken')->with($this->node->daemonSecret)->once()->andReturnSelf() + ->shouldReceive('update')->withNoArgs()->once()->andReturnNull(); + + $this->assertTrue($this->service->handle($this->node->id, ['name' => 'NewName'])); + } +} diff --git a/tests/Unit/Services/Users/DeletionServiceTest.php b/tests/Unit/Services/Users/DeletionServiceTest.php index 6f21096e..85f7400b 100644 --- a/tests/Unit/Services/Users/DeletionServiceTest.php +++ b/tests/Unit/Services/Users/DeletionServiceTest.php @@ -85,7 +85,7 @@ class DeletionServiceTest extends TestCase $this->repository->shouldReceive('delete')->with($this->user->id)->once()->andReturn(true); $this->assertTrue( - $this->service->handle($this->user), + $this->service->handle($this->user->id), 'Assert that service responds true.' ); } @@ -100,20 +100,19 @@ class DeletionServiceTest extends TestCase $this->serverRepository->shouldReceive('findWhere')->with([['owner_id', '=', $this->user->id]])->once()->andReturn(['item']); $this->translator->shouldReceive('trans')->with('admin/user.exceptions.user_has_servers')->once()->andReturnNull(); - $this->service->handle($this->user); + $this->service->handle($this->user->id); } /** * Test that the function supports passing in a model or an ID. */ - public function testIntegerCanBePassedInPlaceOfUserModel() + public function testModelCanBePassedInPlaceOfUserId() { - $this->repository->shouldReceive('find')->with($this->user->id)->once()->andReturn($this->user); $this->serverRepository->shouldReceive('findWhere')->with([['owner_id', '=', $this->user->id]])->once()->andReturn([]); $this->repository->shouldReceive('delete')->with($this->user->id)->once()->andReturn(true); $this->assertTrue( - $this->service->handle($this->user->id), + $this->service->handle($this->user), 'Assert that service responds true.' ); } From 4da7922de607f42fc75b1132b47eae37b778ffd6 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 5 Aug 2017 17:23:02 -0500 Subject: [PATCH 41/99] Code cleanup to use new findCountWhere function --- app/Services/Servers/DeletionService.php | 2 -- app/Services/Users/DeletionService.php | 4 ++-- app/Services/Users/UpdateService.php | 4 +--- tests/Unit/Services/Users/DeletionServiceTest.php | 9 ++++++--- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/app/Services/Servers/DeletionService.php b/app/Services/Servers/DeletionService.php index 5d63ed09..869f17e3 100644 --- a/app/Services/Servers/DeletionService.php +++ b/app/Services/Servers/DeletionService.php @@ -141,13 +141,11 @@ class DeletionService } $this->database->beginTransaction(); - $this->databaseRepository->withColumns('id')->findWhere([['server_id', '=', $server->id]])->each(function ($item) { $this->databaseManagementService->delete($item->id); }); $this->repository->delete($server->id); - $this->database->commit(); } } diff --git a/app/Services/Users/DeletionService.php b/app/Services/Users/DeletionService.php index 3d307785..2c97c203 100644 --- a/app/Services/Users/DeletionService.php +++ b/app/Services/Users/DeletionService.php @@ -78,8 +78,8 @@ class DeletionService $user = $user->id; } - $servers = $this->serverRepository->findWhere([['owner_id', '=', $user]]); - if (count($servers) > 0) { + $servers = $this->serverRepository->withColumns('id')->findCountWhere([['owner_id', '=', $user]]); + if ($servers > 0) { throw new DisplayException($this->translator->trans('admin/user.exceptions.user_has_servers')); } diff --git a/app/Services/Users/UpdateService.php b/app/Services/Users/UpdateService.php index 6df7dc58..5c123467 100644 --- a/app/Services/Users/UpdateService.php +++ b/app/Services/Users/UpdateService.php @@ -68,8 +68,6 @@ class UpdateService $data['password'] = $this->hasher->make($data['password']); } - $user = $this->repository->update($id, $data); - - return $user; + return $this->repository->update($id, $data); } } diff --git a/tests/Unit/Services/Users/DeletionServiceTest.php b/tests/Unit/Services/Users/DeletionServiceTest.php index 85f7400b..f067a7e0 100644 --- a/tests/Unit/Services/Users/DeletionServiceTest.php +++ b/tests/Unit/Services/Users/DeletionServiceTest.php @@ -81,7 +81,8 @@ class DeletionServiceTest extends TestCase */ public function testUserIsDeletedIfNoServersAreAttachedToAccount() { - $this->serverRepository->shouldReceive('findWhere')->with([['owner_id', '=', $this->user->id]])->once()->andReturn([]); + $this->serverRepository->shouldReceive('withColumns')->with('id')->once()->andReturnSelf() + ->shouldReceive('findCountWhere')->with([['owner_id', '=', $this->user->id]])->once()->andReturn(0); $this->repository->shouldReceive('delete')->with($this->user->id)->once()->andReturn(true); $this->assertTrue( @@ -97,7 +98,8 @@ class DeletionServiceTest extends TestCase */ public function testExceptionIsThrownIfServersAreAttachedToAccount() { - $this->serverRepository->shouldReceive('findWhere')->with([['owner_id', '=', $this->user->id]])->once()->andReturn(['item']); + $this->serverRepository->shouldReceive('withColumns')->with('id')->once()->andReturnSelf() + ->shouldReceive('findCountWhere')->with([['owner_id', '=', $this->user->id]])->once()->andReturn(1); $this->translator->shouldReceive('trans')->with('admin/user.exceptions.user_has_servers')->once()->andReturnNull(); $this->service->handle($this->user->id); @@ -108,7 +110,8 @@ class DeletionServiceTest extends TestCase */ public function testModelCanBePassedInPlaceOfUserId() { - $this->serverRepository->shouldReceive('findWhere')->with([['owner_id', '=', $this->user->id]])->once()->andReturn([]); + $this->serverRepository->shouldReceive('withColumns')->with('id')->once()->andReturnSelf() + ->shouldReceive('findCountWhere')->with([['owner_id', '=', $this->user->id]])->once()->andReturn(0); $this->repository->shouldReceive('delete')->with($this->user->id)->once()->andReturn(true); $this->assertTrue( From a4b61846ace1e75603608998a9f06518d6eacd73 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 5 Aug 2017 17:26:30 -0500 Subject: [PATCH 42/99] Apply fixes from StyleCI (#577) --- app/Exceptions/Handler.php | 4 +- app/Extensions/DynamicDatabaseConnection.php | 2 +- .../Controllers/API/User/ServerController.php | 2 +- .../Controllers/Admin/LocationController.php | 4 +- .../Controllers/Admin/NodesController.php | 21 ++++---- .../Controllers/Admin/ServersController.php | 33 +++++++------ .../Controllers/Server/ServerController.php | 2 +- app/Http/Requests/Admin/ServerFormRequest.php | 2 +- app/Models/DatabaseHost.php | 10 ++-- app/Providers/RepositoryServiceProvider.php | 48 +++++++++---------- app/Repositories/Daemon/BaseRepository.php | 2 +- app/Repositories/Daemon/ServerRepository.php | 2 +- .../Eloquent/DatabaseHostRepository.php | 4 +- .../Eloquent/DatabaseRepository.php | 2 +- app/Repositories/Eloquent/NodeRepository.php | 2 +- .../Eloquent/ServiceRepository.php | 4 +- app/Repositories/Eloquent/UserRepository.php | 4 +- app/Services/Database/DatabaseHostService.php | 2 +- app/Services/Nodes/DeletionService.php | 8 ++-- app/Services/Nodes/UpdateService.php | 8 ++-- .../Servers/BuildModificationService.php | 10 ++-- .../Servers/ContainerRebuildService.php | 6 +-- app/Services/Servers/CreationService.php | 6 +-- .../Servers/DetailsModificationService.php | 6 +-- app/Services/Servers/ReinstallService.php | 4 +- .../Servers/StartupModificationService.php | 8 ++-- app/Services/Servers/SuspensionService.php | 6 +-- .../Servers/VariableValidatorService.php | 4 +- app/Services/Users/DeletionService.php | 8 ++-- tests/Unit/Services/Api/KeyServiceTest.php | 12 ++--- .../Services/Api/PermissionServiceTest.php | 4 +- .../Database/DatabaseHostServiceTest.php | 2 +- .../Services/Nodes/CreationServiceTest.php | 6 +-- .../Services/Nodes/DeletionServiceTest.php | 8 ++-- .../Unit/Services/Nodes/UpdateServiceTest.php | 18 +++---- .../Servers/ContainerRebuildServiceTest.php | 12 ++--- .../Services/Servers/CreationServiceTest.php | 20 ++++---- .../DetailsModificationServiceTest.php | 6 +-- .../Services/Servers/ReinstallServiceTest.php | 10 ++-- .../Servers/SuspensionServiceTest.php | 21 ++++---- .../Servers/VariableValidatorServiceTest.php | 38 +++++++-------- .../Services/Users/CreationServiceTest.php | 2 +- .../Services/Users/DeletionServiceTest.php | 8 ++-- .../Unit/Services/Users/UpdateServiceTest.php | 8 ++-- 44 files changed, 198 insertions(+), 201 deletions(-) diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index ba41dce6..ed83b200 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -3,10 +3,10 @@ namespace Pterodactyl\Exceptions; use Exception; -use Illuminate\Auth\AuthenticationException; -use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; use Prologue\Alerts\Facades\Alert; +use Illuminate\Auth\AuthenticationException; use Pterodactyl\Exceptions\Model\DataValidationException; +use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; class Handler extends ExceptionHandler { diff --git a/app/Extensions/DynamicDatabaseConnection.php b/app/Extensions/DynamicDatabaseConnection.php index 68081df2..3b5f1247 100644 --- a/app/Extensions/DynamicDatabaseConnection.php +++ b/app/Extensions/DynamicDatabaseConnection.php @@ -24,10 +24,10 @@ namespace Pterodactyl\Extensions; -use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; use Pterodactyl\Models\DatabaseHost; use Illuminate\Contracts\Encryption\Encrypter; use Illuminate\Config\Repository as ConfigRepository; +use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; class DynamicDatabaseConnection { diff --git a/app/Http/Controllers/API/User/ServerController.php b/app/Http/Controllers/API/User/ServerController.php index f7e652c2..dcdb7f6b 100644 --- a/app/Http/Controllers/API/User/ServerController.php +++ b/app/Http/Controllers/API/User/ServerController.php @@ -28,8 +28,8 @@ use Fractal; use Illuminate\Http\Request; use Pterodactyl\Models\Server; use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Repositories\old_Daemon\PowerRepository; use Pterodactyl\Transformers\User\ServerTransformer; +use Pterodactyl\Repositories\old_Daemon\PowerRepository; use Pterodactyl\Repositories\old_Daemon\CommandRepository; class ServerController extends Controller diff --git a/app/Http/Controllers/Admin/LocationController.php b/app/Http/Controllers/Admin/LocationController.php index 4c33368a..a37d4c61 100644 --- a/app/Http/Controllers/Admin/LocationController.php +++ b/app/Http/Controllers/Admin/LocationController.php @@ -24,13 +24,13 @@ namespace Pterodactyl\Http\Controllers\Admin; -use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; use Pterodactyl\Models\Location; use Prologue\Alerts\AlertsMessageBag; +use Pterodactyl\Services\LocationService; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Http\Requests\Admin\LocationRequest; -use Pterodactyl\Services\LocationService; +use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; class LocationController extends Controller { diff --git a/app/Http/Controllers/Admin/NodesController.php b/app/Http/Controllers/Admin/NodesController.php index 76b83caf..74e6cfa0 100644 --- a/app/Http/Controllers/Admin/NodesController.php +++ b/app/Http/Controllers/Admin/NodesController.php @@ -24,26 +24,25 @@ namespace Pterodactyl\Http\Controllers\Admin; -use Illuminate\Cache\Repository as CacheRepository; -use Illuminate\Contracts\Translation\Translator; use Log; use Alert; -use Cache; use Javascript; -use Prologue\Alerts\AlertsMessageBag; -use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; -use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; -use Pterodactyl\Http\Requests\Admin\NodeFormRequest; -use Pterodactyl\Models\Allocation; -use Pterodactyl\Models\Node; use Illuminate\Http\Request; +use Pterodactyl\Models\Node; +use Pterodactyl\Models\Allocation; +use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Repositories\NodeRepository; -use Pterodactyl\Exceptions\DisplayValidationException; +use Pterodactyl\Services\Nodes\UpdateService; use Pterodactyl\Services\Nodes\CreationService; use Pterodactyl\Services\Nodes\DeletionService; -use Pterodactyl\Services\Nodes\UpdateService; +use Illuminate\Contracts\Translation\Translator; +use Illuminate\Cache\Repository as CacheRepository; +use Pterodactyl\Http\Requests\Admin\NodeFormRequest; +use Pterodactyl\Exceptions\DisplayValidationException; +use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; class NodesController extends Controller { diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php index dd69f2d5..84b7ecbc 100644 --- a/app/Http/Controllers/Admin/ServersController.php +++ b/app/Http/Controllers/Admin/ServersController.php @@ -24,31 +24,30 @@ namespace Pterodactyl\Http\Controllers\Admin; -use Illuminate\Contracts\Config\Repository as ConfigRepository; -use Alert; use Javascript; -use Prologue\Alerts\AlertsMessageBag; -use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; -use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; -use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; -use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; -use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; -use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; -use Pterodactyl\Http\Requests\Admin\ServerFormRequest; -use Pterodactyl\Models\Server; use Illuminate\Http\Request; +use Pterodactyl\Models\Server; +use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Repositories\Eloquent\DatabaseHostRepository; -use Pterodactyl\Services\Database\DatabaseManagementService; -use Pterodactyl\Services\Servers\BuildModificationService; -use Pterodactyl\Services\Servers\ContainerRebuildService; use Pterodactyl\Services\Servers\CreationService; use Pterodactyl\Services\Servers\DeletionService; -use Pterodactyl\Services\Servers\DetailsModificationService; use Pterodactyl\Services\Servers\ReinstallService; -use Pterodactyl\Services\Servers\StartupModificationService; use Pterodactyl\Services\Servers\SuspensionService; +use Pterodactyl\Http\Requests\Admin\ServerFormRequest; +use Pterodactyl\Services\Servers\ContainerRebuildService; +use Pterodactyl\Services\Servers\BuildModificationService; +use Pterodactyl\Services\Database\DatabaseManagementService; +use Pterodactyl\Services\Servers\DetailsModificationService; +use Pterodactyl\Services\Servers\StartupModificationService; +use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Pterodactyl\Repositories\Eloquent\DatabaseHostRepository; +use Illuminate\Contracts\Config\Repository as ConfigRepository; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; +use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; +use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; +use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; class ServersController extends Controller { diff --git a/app/Http/Controllers/Server/ServerController.php b/app/Http/Controllers/Server/ServerController.php index c15af88c..6b70a829 100644 --- a/app/Http/Controllers/Server/ServerController.php +++ b/app/Http/Controllers/Server/ServerController.php @@ -32,8 +32,8 @@ use Illuminate\Http\Request; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Repositories\ServerRepository; -use Pterodactyl\Repositories\old_Daemon\FileRepository; use Pterodactyl\Exceptions\DisplayValidationException; +use Pterodactyl\Repositories\old_Daemon\FileRepository; class ServerController extends Controller { diff --git a/app/Http/Requests/Admin/ServerFormRequest.php b/app/Http/Requests/Admin/ServerFormRequest.php index c521a2f8..c18d8c16 100644 --- a/app/Http/Requests/Admin/ServerFormRequest.php +++ b/app/Http/Requests/Admin/ServerFormRequest.php @@ -24,8 +24,8 @@ namespace Pterodactyl\Http\Requests\Admin; -use Illuminate\Validation\Rule; use Pterodactyl\Models\Server; +use Illuminate\Validation\Rule; class ServerFormRequest extends AdminFormRequest { diff --git a/app/Models/DatabaseHost.php b/app/Models/DatabaseHost.php index d9eb48ff..12b40474 100644 --- a/app/Models/DatabaseHost.php +++ b/app/Models/DatabaseHost.php @@ -80,11 +80,11 @@ class DatabaseHost extends Model implements ValidableContract 'node_id' => 'sometimes|required', ]; -/** - * Validation rules to assign to this model. - * - * @var array - */ + /** + * Validation rules to assign to this model. + * + * @var array + */ // @todo the node_id field doesn't validate correctly if no node is provided in request protected static $dataIntegrityRules = [ 'name' => 'string|max:255', diff --git a/app/Providers/RepositoryServiceProvider.php b/app/Providers/RepositoryServiceProvider.php index 38c16371..549a41af 100644 --- a/app/Providers/RepositoryServiceProvider.php +++ b/app/Providers/RepositoryServiceProvider.php @@ -25,34 +25,34 @@ namespace Pterodactyl\Providers; use Illuminate\Support\ServiceProvider; -use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; -use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; -use Pterodactyl\Contracts\Repository\ApiPermissionRepositoryInterface; -use Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface; -use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; -use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; -use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; -use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; -use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; -use Pterodactyl\Contracts\Repository\OptionVariableRepositoryInterface; -use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; -use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; -use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; -use Pterodactyl\Repositories\Daemon\ConfigurationRepository; -use Pterodactyl\Repositories\Daemon\ServerRepository as DaemonServerRepository; -use Pterodactyl\Repositories\Eloquent\AllocationRepository; +use Pterodactyl\Repositories\Eloquent\NodeRepository; +use Pterodactyl\Repositories\Eloquent\UserRepository; use Pterodactyl\Repositories\Eloquent\ApiKeyRepository; -use Pterodactyl\Repositories\Eloquent\ApiPermissionRepository; -use Pterodactyl\Repositories\Eloquent\DatabaseHostRepository; +use Pterodactyl\Repositories\Eloquent\ServerRepository; +use Pterodactyl\Repositories\Eloquent\ServiceRepository; use Pterodactyl\Repositories\Eloquent\DatabaseRepository; use Pterodactyl\Repositories\Eloquent\LocationRepository; -use Pterodactyl\Repositories\Eloquent\NodeRepository; -use Pterodactyl\Repositories\Eloquent\OptionVariableRepository; -use Pterodactyl\Repositories\Eloquent\ServerRepository; -use Pterodactyl\Repositories\Eloquent\ServerVariableRepository; -use Pterodactyl\Repositories\Eloquent\ServiceRepository; -use Pterodactyl\Repositories\Eloquent\UserRepository; +use Pterodactyl\Repositories\Eloquent\AllocationRepository; +use Pterodactyl\Repositories\Daemon\ConfigurationRepository; +use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; +use Pterodactyl\Repositories\Eloquent\DatabaseHostRepository; +use Pterodactyl\Repositories\Eloquent\ApiPermissionRepository; +use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Repositories\Eloquent\OptionVariableRepository; +use Pterodactyl\Repositories\Eloquent\ServerVariableRepository; +use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; +use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; +use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; +use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; +use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; +use Pterodactyl\Contracts\Repository\ApiPermissionRepositoryInterface; +use Pterodactyl\Contracts\Repository\OptionVariableRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; +use Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface; +use Pterodactyl\Repositories\Daemon\ServerRepository as DaemonServerRepository; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; class RepositoryServiceProvider extends ServiceProvider { diff --git a/app/Repositories/Daemon/BaseRepository.php b/app/Repositories/Daemon/BaseRepository.php index 8a637e9f..43e2e229 100644 --- a/app/Repositories/Daemon/BaseRepository.php +++ b/app/Repositories/Daemon/BaseRepository.php @@ -26,9 +26,9 @@ namespace Pterodactyl\Repositories\Daemon; use GuzzleHttp\Client; use Illuminate\Foundation\Application; -use Pterodactyl\Contracts\Repository\Daemon\BaseRepositoryInterface; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Illuminate\Contracts\Config\Repository as ConfigRepository; +use Pterodactyl\Contracts\Repository\Daemon\BaseRepositoryInterface; class BaseRepository implements BaseRepositoryInterface { diff --git a/app/Repositories/Daemon/ServerRepository.php b/app/Repositories/Daemon/ServerRepository.php index 359963eb..c1cbee1c 100644 --- a/app/Repositories/Daemon/ServerRepository.php +++ b/app/Repositories/Daemon/ServerRepository.php @@ -24,9 +24,9 @@ namespace Pterodactyl\Repositories\Daemon; +use Pterodactyl\Services\Servers\EnvironmentService; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface as DatabaseServerRepositoryInterface; -use Pterodactyl\Services\Servers\EnvironmentService; class ServerRepository extends BaseRepository implements ServerRepositoryInterface { diff --git a/app/Repositories/Eloquent/DatabaseHostRepository.php b/app/Repositories/Eloquent/DatabaseHostRepository.php index 8a4d7b3c..6cb48bbf 100644 --- a/app/Repositories/Eloquent/DatabaseHostRepository.php +++ b/app/Repositories/Eloquent/DatabaseHostRepository.php @@ -24,10 +24,10 @@ namespace Pterodactyl\Repositories\Eloquent; -use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; +use Pterodactyl\Models\DatabaseHost; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; -use Pterodactyl\Models\DatabaseHost; +use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; class DatabaseHostRepository extends EloquentRepository implements DatabaseHostRepositoryInterface { diff --git a/app/Repositories/Eloquent/DatabaseRepository.php b/app/Repositories/Eloquent/DatabaseRepository.php index 0e2bd6c1..347c6b9d 100644 --- a/app/Repositories/Eloquent/DatabaseRepository.php +++ b/app/Repositories/Eloquent/DatabaseRepository.php @@ -24,9 +24,9 @@ namespace Pterodactyl\Repositories\Eloquent; -use Illuminate\Database\DatabaseManager; use Pterodactyl\Models\Database; use Illuminate\Foundation\Application; +use Illuminate\Database\DatabaseManager; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; diff --git a/app/Repositories/Eloquent/NodeRepository.php b/app/Repositories/Eloquent/NodeRepository.php index 2e18b1d4..7375570e 100644 --- a/app/Repositories/Eloquent/NodeRepository.php +++ b/app/Repositories/Eloquent/NodeRepository.php @@ -24,9 +24,9 @@ namespace Pterodactyl\Repositories\Eloquent; +use Pterodactyl\Models\Node; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; -use Pterodactyl\Models\Node; use Pterodactyl\Repositories\Eloquent\Attributes\SearchableRepository; class NodeRepository extends SearchableRepository implements NodeRepositoryInterface diff --git a/app/Repositories/Eloquent/ServiceRepository.php b/app/Repositories/Eloquent/ServiceRepository.php index 6fd8a4bc..aca36490 100644 --- a/app/Repositories/Eloquent/ServiceRepository.php +++ b/app/Repositories/Eloquent/ServiceRepository.php @@ -24,9 +24,9 @@ namespace Pterodactyl\Repositories\Eloquent; -use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; -use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Models\Service; +use Pterodactyl\Exceptions\Repository\RecordNotFoundException; +use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; class ServiceRepository extends EloquentRepository implements ServiceRepositoryInterface { diff --git a/app/Repositories/Eloquent/UserRepository.php b/app/Repositories/Eloquent/UserRepository.php index 633b92fd..e7dfab60 100644 --- a/app/Repositories/Eloquent/UserRepository.php +++ b/app/Repositories/Eloquent/UserRepository.php @@ -24,10 +24,10 @@ namespace Pterodactyl\Repositories\Eloquent; -use Illuminate\Contracts\Config\Repository as ConfigRepository; +use Pterodactyl\Models\User; use Illuminate\Foundation\Application; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; -use Pterodactyl\Models\User; +use Illuminate\Contracts\Config\Repository as ConfigRepository; use Pterodactyl\Repositories\Eloquent\Attributes\SearchableRepository; class UserRepository extends SearchableRepository implements UserRepositoryInterface diff --git a/app/Services/Database/DatabaseHostService.php b/app/Services/Database/DatabaseHostService.php index 94170d83..33d46c31 100644 --- a/app/Services/Database/DatabaseHostService.php +++ b/app/Services/Database/DatabaseHostService.php @@ -24,8 +24,8 @@ namespace Pterodactyl\Services\Database; -use Illuminate\Contracts\Encryption\Encrypter; use Illuminate\Database\DatabaseManager; +use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Extensions\DynamicDatabaseConnection; use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; diff --git a/app/Services/Nodes/DeletionService.php b/app/Services/Nodes/DeletionService.php index 1b57c591..519aed42 100644 --- a/app/Services/Nodes/DeletionService.php +++ b/app/Services/Nodes/DeletionService.php @@ -24,11 +24,11 @@ namespace Pterodactyl\Services\Nodes; -use Illuminate\Contracts\Translation\Translator; -use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; -use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; -use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Models\Node; +use Pterodactyl\Exceptions\DisplayException; +use Illuminate\Contracts\Translation\Translator; +use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; class DeletionService { diff --git a/app/Services/Nodes/UpdateService.php b/app/Services/Nodes/UpdateService.php index 58336793..e34c9ff2 100644 --- a/app/Services/Nodes/UpdateService.php +++ b/app/Services/Nodes/UpdateService.php @@ -24,12 +24,12 @@ namespace Pterodactyl\Services\Nodes; -use GuzzleHttp\Exception\RequestException; use Illuminate\Log\Writer; -use Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface; -use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; -use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Models\Node; +use GuzzleHttp\Exception\RequestException; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface; class UpdateService { diff --git a/app/Services/Servers/BuildModificationService.php b/app/Services/Servers/BuildModificationService.php index 19ce0c3a..24c52a16 100644 --- a/app/Services/Servers/BuildModificationService.php +++ b/app/Services/Servers/BuildModificationService.php @@ -24,15 +24,15 @@ namespace Pterodactyl\Services\Servers; +use Illuminate\Log\Writer; +use Pterodactyl\Models\Server; use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; -use Illuminate\Log\Writer; -use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; -use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; -use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; -use Pterodactyl\Models\Server; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; class BuildModificationService { diff --git a/app/Services/Servers/ContainerRebuildService.php b/app/Services/Servers/ContainerRebuildService.php index 20ce9e0b..d6e63268 100644 --- a/app/Services/Servers/ContainerRebuildService.php +++ b/app/Services/Servers/ContainerRebuildService.php @@ -24,12 +24,12 @@ namespace Pterodactyl\Services\Servers; -use GuzzleHttp\Exception\RequestException; use Illuminate\Log\Writer; +use Pterodactyl\Models\Server; +use GuzzleHttp\Exception\RequestException; +use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Models\Server; class ContainerRebuildService { diff --git a/app/Services/Servers/CreationService.php b/app/Services/Servers/CreationService.php index 5c641d2d..96e1527d 100644 --- a/app/Services/Servers/CreationService.php +++ b/app/Services/Servers/CreationService.php @@ -24,11 +24,11 @@ namespace Pterodactyl\Services\Servers; -use GuzzleHttp\Exception\RequestException; -use Illuminate\Log\Writer; -use Pterodactyl\Exceptions\DisplayException; use Ramsey\Uuid\Uuid; +use Illuminate\Log\Writer; use Illuminate\Database\DatabaseManager; +use GuzzleHttp\Exception\RequestException; +use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; diff --git a/app/Services/Servers/DetailsModificationService.php b/app/Services/Servers/DetailsModificationService.php index c2897021..7b1aac37 100644 --- a/app/Services/Servers/DetailsModificationService.php +++ b/app/Services/Servers/DetailsModificationService.php @@ -24,11 +24,11 @@ namespace Pterodactyl\Services\Servers; -use GuzzleHttp\Exception\RequestException; -use Illuminate\Database\DatabaseManager; use Illuminate\Log\Writer; -use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Models\Server; +use Illuminate\Database\DatabaseManager; +use GuzzleHttp\Exception\RequestException; +use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Repositories\Eloquent\ServerRepository; use Pterodactyl\Repositories\Daemon\ServerRepository as DaemonServerRepository; diff --git a/app/Services/Servers/ReinstallService.php b/app/Services/Servers/ReinstallService.php index 5b1b24dd..f9b0f16e 100644 --- a/app/Services/Servers/ReinstallService.php +++ b/app/Services/Servers/ReinstallService.php @@ -24,11 +24,11 @@ namespace Pterodactyl\Services\Servers; -use GuzzleHttp\Exception\RequestException; use Illuminate\Log\Writer; -use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Models\Server; +use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; +use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; diff --git a/app/Services/Servers/StartupModificationService.php b/app/Services/Servers/StartupModificationService.php index 00eaaafd..8c3ffa5c 100644 --- a/app/Services/Servers/StartupModificationService.php +++ b/app/Services/Servers/StartupModificationService.php @@ -24,14 +24,14 @@ namespace Pterodactyl\Services\Servers; +use Illuminate\Log\Writer; +use Pterodactyl\Models\Server; use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; -use Illuminate\Log\Writer; -use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; +use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Models\Server; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; class StartupModificationService { diff --git a/app/Services/Servers/SuspensionService.php b/app/Services/Servers/SuspensionService.php index 96462db3..63f4aaca 100644 --- a/app/Services/Servers/SuspensionService.php +++ b/app/Services/Servers/SuspensionService.php @@ -24,13 +24,13 @@ namespace Pterodactyl\Services\Servers; +use Illuminate\Log\Writer; +use Pterodactyl\Models\Server; use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; -use Illuminate\Log\Writer; +use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Models\Server; class SuspensionService { diff --git a/app/Services/Servers/VariableValidatorService.php b/app/Services/Servers/VariableValidatorService.php index d0bb2ccd..1904f675 100644 --- a/app/Services/Servers/VariableValidatorService.php +++ b/app/Services/Servers/VariableValidatorService.php @@ -24,11 +24,11 @@ namespace Pterodactyl\Services\Servers; -use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; -use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; use Pterodactyl\Exceptions\DisplayValidationException; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Illuminate\Contracts\Validation\Factory as ValidationFactory; use Pterodactyl\Contracts\Repository\OptionVariableRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; class VariableValidatorService { diff --git a/app/Services/Users/DeletionService.php b/app/Services/Users/DeletionService.php index 2c97c203..ab88068f 100644 --- a/app/Services/Users/DeletionService.php +++ b/app/Services/Users/DeletionService.php @@ -24,11 +24,11 @@ namespace Pterodactyl\Services\Users; -use Illuminate\Contracts\Translation\Translator; -use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; -use Pterodactyl\Contracts\Repository\UserRepositoryInterface; -use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Models\User; +use Pterodactyl\Exceptions\DisplayException; +use Illuminate\Contracts\Translation\Translator; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; class DeletionService { diff --git a/tests/Unit/Services/Api/KeyServiceTest.php b/tests/Unit/Services/Api/KeyServiceTest.php index ef48277a..33d01cb0 100644 --- a/tests/Unit/Services/Api/KeyServiceTest.php +++ b/tests/Unit/Services/Api/KeyServiceTest.php @@ -24,14 +24,14 @@ namespace Tests\Unit\Services\Api; -use Illuminate\Contracts\Encryption\Encrypter; -use Illuminate\Database\ConnectionInterface; use Mockery as m; -use phpmock\phpunit\PHPMock; -use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; -use Pterodactyl\Services\Api\KeyService; -use Pterodactyl\Services\Api\PermissionService; use Tests\TestCase; +use phpmock\phpunit\PHPMock; +use Pterodactyl\Services\Api\KeyService; +use Illuminate\Database\ConnectionInterface; +use Illuminate\Contracts\Encryption\Encrypter; +use Pterodactyl\Services\Api\PermissionService; +use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; class KeyServiceTest extends TestCase { diff --git a/tests/Unit/Services/Api/PermissionServiceTest.php b/tests/Unit/Services/Api/PermissionServiceTest.php index 5c687c9b..98e3bf0b 100644 --- a/tests/Unit/Services/Api/PermissionServiceTest.php +++ b/tests/Unit/Services/Api/PermissionServiceTest.php @@ -25,10 +25,10 @@ namespace Tests\Unit\Services; use Mockery as m; -use Pterodactyl\Contracts\Repository\ApiPermissionRepositoryInterface; +use Tests\TestCase; use Pterodactyl\Models\APIPermission; use Pterodactyl\Services\Api\PermissionService; -use Tests\TestCase; +use Pterodactyl\Contracts\Repository\ApiPermissionRepositoryInterface; class PermissionServiceTest extends TestCase { diff --git a/tests/Unit/Services/Database/DatabaseHostServiceTest.php b/tests/Unit/Services/Database/DatabaseHostServiceTest.php index fae03862..84df7f02 100644 --- a/tests/Unit/Services/Database/DatabaseHostServiceTest.php +++ b/tests/Unit/Services/Database/DatabaseHostServiceTest.php @@ -156,7 +156,7 @@ class DatabaseHostServiceTest extends TestCase } /** - * Test that passing no or empty password will skip storing it + * Test that passing no or empty password will skip storing it. */ public function test_update_without_password() { diff --git a/tests/Unit/Services/Nodes/CreationServiceTest.php b/tests/Unit/Services/Nodes/CreationServiceTest.php index 5932e709..84efcbde 100644 --- a/tests/Unit/Services/Nodes/CreationServiceTest.php +++ b/tests/Unit/Services/Nodes/CreationServiceTest.php @@ -25,10 +25,10 @@ namespace Tests\Unit\Services\Nodes; use Mockery as m; -use phpmock\phpunit\PHPMock; -use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; -use Pterodactyl\Services\Nodes\CreationService; use Tests\TestCase; +use phpmock\phpunit\PHPMock; +use Pterodactyl\Services\Nodes\CreationService; +use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; class CreationServiceTest extends TestCase { diff --git a/tests/Unit/Services/Nodes/DeletionServiceTest.php b/tests/Unit/Services/Nodes/DeletionServiceTest.php index 266fbc37..845117db 100644 --- a/tests/Unit/Services/Nodes/DeletionServiceTest.php +++ b/tests/Unit/Services/Nodes/DeletionServiceTest.php @@ -24,13 +24,13 @@ namespace Tests\Unit\Services\Nodes; -use Illuminate\Contracts\Translation\Translator; use Mockery as m; -use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; -use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Tests\TestCase; use Pterodactyl\Models\Node; use Pterodactyl\Services\Nodes\DeletionService; -use Tests\TestCase; +use Illuminate\Contracts\Translation\Translator; +use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; class DeletionServiceTest extends TestCase { diff --git a/tests/Unit/Services/Nodes/UpdateServiceTest.php b/tests/Unit/Services/Nodes/UpdateServiceTest.php index 9bccf2d4..74db802b 100644 --- a/tests/Unit/Services/Nodes/UpdateServiceTest.php +++ b/tests/Unit/Services/Nodes/UpdateServiceTest.php @@ -25,17 +25,17 @@ namespace Tests\Unit\Services\Nodes; use Exception; -use GuzzleHttp\Exception\RequestException; -use Illuminate\Log\Writer; use Mockery as m; -use phpmock\phpunit\PHPMock; -use Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface; -use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Models\Node; -use Pterodactyl\Services\Nodes\CreationService; -use Pterodactyl\Services\Nodes\UpdateService; use Tests\TestCase; +use Illuminate\Log\Writer; +use phpmock\phpunit\PHPMock; +use Pterodactyl\Models\Node; +use GuzzleHttp\Exception\RequestException; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Services\Nodes\UpdateService; +use Pterodactyl\Services\Nodes\CreationService; +use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface; class UpdateServiceTest extends TestCase { diff --git a/tests/Unit/Services/Servers/ContainerRebuildServiceTest.php b/tests/Unit/Services/Servers/ContainerRebuildServiceTest.php index fac637c5..ad18baa4 100644 --- a/tests/Unit/Services/Servers/ContainerRebuildServiceTest.php +++ b/tests/Unit/Services/Servers/ContainerRebuildServiceTest.php @@ -25,13 +25,13 @@ namespace Tests\Unit\Services\Servers; use Exception; -use GuzzleHttp\Exception\RequestException; -use Illuminate\Log\Writer; use Mockery as m; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Models\Server; -use Pterodactyl\Services\Servers\ContainerRebuildService; use Tests\TestCase; +use Illuminate\Log\Writer; +use Pterodactyl\Models\Server; +use GuzzleHttp\Exception\RequestException; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Services\Servers\ContainerRebuildService; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; @@ -133,7 +133,7 @@ class ContainerRebuildServiceTest extends TestCase } catch (Exception $exception) { $this->assertInstanceOf(DisplayException::class, $exception); $this->assertEquals( - trans('admin/server.exceptions.daemon_exception', ['code' => 400,]), $exception->getMessage() + trans('admin/server.exceptions.daemon_exception', ['code' => 400]), $exception->getMessage() ); } } diff --git a/tests/Unit/Services/Servers/CreationServiceTest.php b/tests/Unit/Services/Servers/CreationServiceTest.php index 284d9c28..dad92475 100644 --- a/tests/Unit/Services/Servers/CreationServiceTest.php +++ b/tests/Unit/Services/Servers/CreationServiceTest.php @@ -24,20 +24,20 @@ namespace Tests\Unit\Services\Servers; -use Illuminate\Log\Writer; use Mockery as m; -use phpmock\phpunit\PHPMock; -use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; -use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; -use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; -use Pterodactyl\Contracts\Repository\UserRepositoryInterface; -use Pterodactyl\Services\Servers\CreationService; -use Pterodactyl\Services\Servers\UsernameGenerationService; -use Pterodactyl\Services\Servers\VariableValidatorService; -use Ramsey\Uuid\Uuid; use Tests\TestCase; +use Ramsey\Uuid\Uuid; +use Illuminate\Log\Writer; +use phpmock\phpunit\PHPMock; use Illuminate\Database\DatabaseManager; +use Pterodactyl\Services\Servers\CreationService; +use Pterodactyl\Services\Servers\VariableValidatorService; +use Pterodactyl\Services\Servers\UsernameGenerationService; +use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; class CreationServiceTest extends TestCase diff --git a/tests/Unit/Services/Servers/DetailsModificationServiceTest.php b/tests/Unit/Services/Servers/DetailsModificationServiceTest.php index 37164b4f..a617fbaa 100644 --- a/tests/Unit/Services/Servers/DetailsModificationServiceTest.php +++ b/tests/Unit/Services/Servers/DetailsModificationServiceTest.php @@ -25,9 +25,9 @@ namespace Tests\Unit\Services\Servers; use Exception; -use Illuminate\Log\Writer; use Mockery as m; use Tests\TestCase; +use Illuminate\Log\Writer; use phpmock\phpunit\PHPMock; use Pterodactyl\Models\Server; use Illuminate\Database\DatabaseManager; @@ -258,7 +258,7 @@ class DetailsModificationServiceTest extends TestCase } catch (Exception $exception) { $this->assertInstanceOf(DisplayException::class, $exception); $this->assertEquals( - trans('admin/server.exceptions.daemon_exception', ['code' => 400,]), $exception->getMessage() + trans('admin/server.exceptions.daemon_exception', ['code' => 400]), $exception->getMessage() ); } } @@ -371,7 +371,7 @@ class DetailsModificationServiceTest extends TestCase } catch (Exception $exception) { $this->assertInstanceOf(DisplayException::class, $exception); $this->assertEquals( - trans('admin/server.exceptions.daemon_exception', ['code' => 400,]), $exception->getMessage() + trans('admin/server.exceptions.daemon_exception', ['code' => 400]), $exception->getMessage() ); } } diff --git a/tests/Unit/Services/Servers/ReinstallServiceTest.php b/tests/Unit/Services/Servers/ReinstallServiceTest.php index ee023012..a2fee951 100644 --- a/tests/Unit/Services/Servers/ReinstallServiceTest.php +++ b/tests/Unit/Services/Servers/ReinstallServiceTest.php @@ -25,14 +25,14 @@ namespace Tests\Unit\Services\Servers; use Exception; +use Mockery as m; +use Tests\TestCase; +use Illuminate\Log\Writer; +use Pterodactyl\Models\Server; use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; -use Illuminate\Log\Writer; -use Mockery as m; use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Models\Server; use Pterodactyl\Services\Servers\ReinstallService; -use Tests\TestCase; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; @@ -162,7 +162,7 @@ class ReinstallServiceTest extends TestCase } catch (Exception $exception) { $this->assertInstanceOf(DisplayException::class, $exception); $this->assertEquals( - trans('admin/server.exceptions.daemon_exception', ['code' => 400,]), $exception->getMessage() + trans('admin/server.exceptions.daemon_exception', ['code' => 400]), $exception->getMessage() ); } } diff --git a/tests/Unit/Services/Servers/SuspensionServiceTest.php b/tests/Unit/Services/Servers/SuspensionServiceTest.php index 0347038e..3445e02b 100644 --- a/tests/Unit/Services/Servers/SuspensionServiceTest.php +++ b/tests/Unit/Services/Servers/SuspensionServiceTest.php @@ -25,16 +25,16 @@ namespace Tests\Unit\Services\Servers; use Exception; +use Mockery as m; +use Tests\TestCase; +use Illuminate\Log\Writer; +use Pterodactyl\Models\Server; use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; -use Illuminate\Log\Writer; -use Mockery as m; -use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; -use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Models\Server; use Pterodactyl\Services\Servers\SuspensionService; -use Tests\TestCase; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; class SuspensionServiceTest extends TestCase { @@ -127,9 +127,8 @@ class SuspensionServiceTest extends TestCase $this->assertTrue($this->service->toggle($this->server)); } - /** - * Test that server is unsuspended if action=unsuspend + * Test that server is unsuspended if action=unsuspend. */ public function testServerShouldBeUnsuspendedWhenUnsuspendActionIsPassed() { @@ -148,7 +147,7 @@ class SuspensionServiceTest extends TestCase } /** - * Test that nothing happens if a server is already unsuspended and action=unsuspend + * Test that nothing happens if a server is already unsuspended and action=unsuspend. */ public function testNoActionShouldHappenIfServerIsAlreadyUnsuspendedAndActionIsUnsuspend() { @@ -158,7 +157,7 @@ class SuspensionServiceTest extends TestCase } /** - * Test that nothing happens if a server is already suspended and action=suspend + * Test that nothing happens if a server is already suspended and action=suspend. */ public function testNoActionShouldHappenIfServerIsAlreadySuspendedAndActionIsSuspend() { @@ -191,7 +190,7 @@ class SuspensionServiceTest extends TestCase } catch (Exception $exception) { $this->assertInstanceOf(DisplayException::class, $exception); $this->assertEquals( - trans('admin/server.exceptions.daemon_exception', ['code' => 400,]), $exception->getMessage() + trans('admin/server.exceptions.daemon_exception', ['code' => 400]), $exception->getMessage() ); } } diff --git a/tests/Unit/Services/Servers/VariableValidatorServiceTest.php b/tests/Unit/Services/Servers/VariableValidatorServiceTest.php index 60e8705a..c6b6a0a4 100644 --- a/tests/Unit/Services/Servers/VariableValidatorServiceTest.php +++ b/tests/Unit/Services/Servers/VariableValidatorServiceTest.php @@ -149,15 +149,15 @@ class VariableValidatorServiceTest extends TestCase $this->validator->shouldReceive('make')->with([ 'variable_value' => 'Test_SomeValue_0', ], [ - 'variable_value' => $this->variables{0}->rules, + 'variable_value' => $this->variables[0]->rules, ])->once()->andReturnSelf() ->shouldReceive('fails')->withNoArgs()->once()->andReturn(false); $response = $this->service->setFields([ - $this->variables{0}->env_variable => 'Test_SomeValue_0', - $this->variables{1}->env_variable => 'Test_SomeValue_1', - $this->variables{2}->env_variable => 'Test_SomeValue_2', - $this->variables{3}->env_variable => 'Test_SomeValue_3', + $this->variables[0]->env_variable => 'Test_SomeValue_0', + $this->variables[1]->env_variable => 'Test_SomeValue_1', + $this->variables[2]->env_variable => 'Test_SomeValue_2', + $this->variables[3]->env_variable => 'Test_SomeValue_3', ])->validate(1)->getResults(); $this->assertEquals(1, count($response), 'Assert response has a single item in array.'); @@ -166,8 +166,8 @@ class VariableValidatorServiceTest extends TestCase $this->assertArrayHasKey('key', $response[0]); $this->assertArrayHasKey('value', $response[0]); - $this->assertEquals($this->variables{0}->id, $response[0]['id']); - $this->assertEquals($this->variables{0}->env_variable, $response[0]['key']); + $this->assertEquals($this->variables[0]->id, $response[0]['id']); + $this->assertEquals($this->variables[0]->env_variable, $response[0]['key']); $this->assertEquals('Test_SomeValue_0', $response[0]['value']); } @@ -178,32 +178,32 @@ class VariableValidatorServiceTest extends TestCase { $this->optionVariableRepository->shouldReceive('findWhere')->with([['option_id', '=', 1]])->andReturn($this->variables); - foreach($this->variables as $key => $variable) { + foreach ($this->variables as $key => $variable) { $this->validator->shouldReceive('make')->with([ 'variable_value' => 'Test_SomeValue_' . $key, ], [ - 'variable_value' => $this->variables{$key}->rules, + 'variable_value' => $this->variables[$key]->rules, ])->andReturnSelf() ->shouldReceive('fails')->withNoArgs()->once()->andReturn(false); } $response = $this->service->isAdmin()->setFields([ - $this->variables{0}->env_variable => 'Test_SomeValue_0', - $this->variables{1}->env_variable => 'Test_SomeValue_1', - $this->variables{2}->env_variable => 'Test_SomeValue_2', - $this->variables{3}->env_variable => 'Test_SomeValue_3', + $this->variables[0]->env_variable => 'Test_SomeValue_0', + $this->variables[1]->env_variable => 'Test_SomeValue_1', + $this->variables[2]->env_variable => 'Test_SomeValue_2', + $this->variables[3]->env_variable => 'Test_SomeValue_3', ])->validate(1)->getResults(); $this->assertEquals(4, count($response), 'Assert response has all four items in array.'); - foreach($response as $key => $values) { + foreach ($response as $key => $values) { $this->assertArrayHasKey($key, $response); $this->assertArrayHasKey('id', $response[$key]); $this->assertArrayHasKey('key', $response[$key]); $this->assertArrayHasKey('value', $response[$key]); - $this->assertEquals($this->variables{$key}->id, $response[$key]['id']); - $this->assertEquals($this->variables{$key}->env_variable, $response[$key]['key']); + $this->assertEquals($this->variables[$key]->id, $response[$key]['id']); + $this->assertEquals($this->variables[$key]->env_variable, $response[$key]['key']); $this->assertEquals('Test_SomeValue_' . $key, $response[$key]['value']); } } @@ -218,7 +218,7 @@ class VariableValidatorServiceTest extends TestCase $this->validator->shouldReceive('make')->with([ 'variable_value' => null, ], [ - 'variable_value' => $this->variables{0}->rules, + 'variable_value' => $this->variables[0]->rules, ])->once()->andReturnSelf() ->shouldReceive('fails')->withNoArgs()->once()->andReturn(true); @@ -227,7 +227,7 @@ class VariableValidatorServiceTest extends TestCase try { $this->service->setFields([ - $this->variables{0}->env_variable => null, + $this->variables[0]->env_variable => null, ])->validate(1); } catch (DisplayValidationException $exception) { $decoded = json_decode($exception->getMessage()); @@ -235,7 +235,7 @@ class VariableValidatorServiceTest extends TestCase $this->assertEquals(0, json_last_error(), 'Assert that response is decodable JSON.'); $this->assertObjectHasAttribute('notice', $decoded); $this->assertEquals( - trans('admin/server.exceptions.bad_variable', ['name' => $this->variables{0}->name]), + trans('admin/server.exceptions.bad_variable', ['name' => $this->variables[0]->name]), $decoded->notice[0] ); } diff --git a/tests/Unit/Services/Users/CreationServiceTest.php b/tests/Unit/Services/Users/CreationServiceTest.php index 59067f9d..ac18c2ed 100644 --- a/tests/Unit/Services/Users/CreationServiceTest.php +++ b/tests/Unit/Services/Users/CreationServiceTest.php @@ -25,13 +25,13 @@ namespace Tests\Unit\Services; use Mockery as m; -use Pterodactyl\Services\Users\CreationService; use Tests\TestCase; use Illuminate\Foundation\Application; use Illuminate\Contracts\Hashing\Hasher; use Illuminate\Database\ConnectionInterface; use Illuminate\Notifications\ChannelManager; use Pterodactyl\Notifications\AccountCreated; +use Pterodactyl\Services\Users\CreationService; use Pterodactyl\Services\Helpers\TemporaryPasswordService; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; diff --git a/tests/Unit/Services/Users/DeletionServiceTest.php b/tests/Unit/Services/Users/DeletionServiceTest.php index f067a7e0..e98a375f 100644 --- a/tests/Unit/Services/Users/DeletionServiceTest.php +++ b/tests/Unit/Services/Users/DeletionServiceTest.php @@ -24,13 +24,13 @@ namespace Tests\Unit\Services\Users; -use Illuminate\Contracts\Translation\Translator; use Mockery as m; -use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; -use Pterodactyl\Contracts\Repository\UserRepositoryInterface; +use Tests\TestCase; use Pterodactyl\Models\User; use Pterodactyl\Services\Users\DeletionService; -use Tests\TestCase; +use Illuminate\Contracts\Translation\Translator; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; class DeletionServiceTest extends TestCase { diff --git a/tests/Unit/Services/Users/UpdateServiceTest.php b/tests/Unit/Services/Users/UpdateServiceTest.php index 399a2f85..2a5bd295 100644 --- a/tests/Unit/Services/Users/UpdateServiceTest.php +++ b/tests/Unit/Services/Users/UpdateServiceTest.php @@ -24,11 +24,11 @@ namespace Tests\Unit\Services\Users; -use Illuminate\Contracts\Hashing\Hasher; -use Pterodactyl\Contracts\Repository\UserRepositoryInterface; -use Pterodactyl\Services\Users\UpdateService; -use Tests\TestCase; use Mockery as m; +use Tests\TestCase; +use Illuminate\Contracts\Hashing\Hasher; +use Pterodactyl\Services\Users\UpdateService; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; class UpdateServiceTest extends TestCase { From 396b5c22d9ddb71c86263bf6469f5e05ee6c9464 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 5 Aug 2017 17:29:15 -0500 Subject: [PATCH 43/99] Fix formatting issue --- app/Models/DatabaseHost.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Models/DatabaseHost.php b/app/Models/DatabaseHost.php index 12b40474..d4ec484f 100644 --- a/app/Models/DatabaseHost.php +++ b/app/Models/DatabaseHost.php @@ -84,8 +84,8 @@ class DatabaseHost extends Model implements ValidableContract * Validation rules to assign to this model. * * @var array + * @todo the node_id field doesn't validate correctly if no node is provided in request */ - // @todo the node_id field doesn't validate correctly if no node is provided in request protected static $dataIntegrityRules = [ 'name' => 'string|max:255', 'host' => 'ip|unique:database_hosts,host', From 669119c8f8e3c9e879d7fa3e61519da89e1f9cd4 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 5 Aug 2017 21:10:32 -0500 Subject: [PATCH 44/99] Handle allocation assignment using services Function is significantly quicker and uses 1 SQL query per IP rather than 1 query per port. --- .../Repository/RepositoryInterface.php | 8 + .../Controllers/Admin/LocationController.php | 12 +- .../Controllers/Admin/NodesController.php | 105 +- ...ionRequest.php => LocationFormRequest.php} | 4 +- .../Admin/Node/AllocationAliasFormRequest.php | 41 + .../Admin/Node/AllocationFormRequest.php | 42 + .../Admin/{ => Node}/NodeFormRequest.php | 3 +- app/Http/Requests/Admin/UserFormRequest.php | 2 +- app/Models/Allocation.php | 105 +- .../Eloquent/EloquentRepository.php | 39 + app/Repositories/Eloquent/NodeRepository.php | 6 +- .../Allocations/AssignmentService.php | 125 + coverage.xml | 5812 ----------------- ...SetAllocationUnqiueUsingMultipleFields.php | 32 + resources/lang/en/admin/exceptions.php | 5 + resources/lang/en/admin/node.php | 1 + .../Allocations/AssignmentServiceTest.php | 327 + 17 files changed, 754 insertions(+), 5915 deletions(-) rename app/Http/Requests/Admin/{LocationRequest.php => LocationFormRequest.php} (91%) create mode 100644 app/Http/Requests/Admin/Node/AllocationAliasFormRequest.php create mode 100644 app/Http/Requests/Admin/Node/AllocationFormRequest.php rename app/Http/Requests/Admin/{ => Node}/NodeFormRequest.php (95%) create mode 100644 app/Services/Allocations/AssignmentService.php delete mode 100644 coverage.xml create mode 100644 database/migrations/2017_08_05_174811_SetAllocationUnqiueUsingMultipleFields.php create mode 100644 tests/Unit/Services/Allocations/AssignmentServiceTest.php diff --git a/app/Contracts/Repository/RepositoryInterface.php b/app/Contracts/Repository/RepositoryInterface.php index ad600817..f48755d6 100644 --- a/app/Contracts/Repository/RepositoryInterface.php +++ b/app/Contracts/Repository/RepositoryInterface.php @@ -186,4 +186,12 @@ interface RepositoryInterface * @return bool */ public function insert(array $data); + + /** + * Insert multiple records into the database and ignore duplicates. + * + * @param array $values + * @return bool + */ + public function insertIgnore(array $values); } diff --git a/app/Http/Controllers/Admin/LocationController.php b/app/Http/Controllers/Admin/LocationController.php index a37d4c61..11d67ac0 100644 --- a/app/Http/Controllers/Admin/LocationController.php +++ b/app/Http/Controllers/Admin/LocationController.php @@ -29,7 +29,7 @@ use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Services\LocationService; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Http\Requests\Admin\LocationRequest; +use Pterodactyl\Http\Requests\Admin\LocationFormRequest; use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; class LocationController extends Controller @@ -94,13 +94,13 @@ class LocationController extends Controller /** * Handle request to create new location. * - * @param \Pterodactyl\Http\Requests\Admin\LocationRequest $request + * @param \Pterodactyl\Http\Requests\Admin\LocationFormRequest $request * @return \Illuminate\Http\RedirectResponse * * @throws \Throwable * @throws \Watson\Validating\ValidationException */ - public function create(LocationRequest $request) + public function create(LocationFormRequest $request) { $location = $this->service->create($request->normalize()); $this->alert->success('Location was created successfully.')->flash(); @@ -111,14 +111,14 @@ class LocationController extends Controller /** * Handle request to update or delete location. * - * @param \Pterodactyl\Http\Requests\Admin\LocationRequest $request - * @param \Pterodactyl\Models\Location $location + * @param \Pterodactyl\Http\Requests\Admin\LocationFormRequest $request + * @param \Pterodactyl\Models\Location $location * @return \Illuminate\Http\RedirectResponse * * @throws \Throwable * @throws \Watson\Validating\ValidationException */ - public function update(LocationRequest $request, Location $location) + public function update(LocationFormRequest $request, Location $location) { if ($request->input('action') === 'delete') { return $this->delete($location); diff --git a/app/Http/Controllers/Admin/NodesController.php b/app/Http/Controllers/Admin/NodesController.php index 74e6cfa0..97b85abc 100644 --- a/app/Http/Controllers/Admin/NodesController.php +++ b/app/Http/Controllers/Admin/NodesController.php @@ -24,25 +24,25 @@ namespace Pterodactyl\Http\Controllers\Admin; -use Log; use Alert; use Javascript; use Illuminate\Http\Request; use Pterodactyl\Models\Node; use Pterodactyl\Models\Allocation; use Prologue\Alerts\AlertsMessageBag; -use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Repositories\NodeRepository; use Pterodactyl\Services\Nodes\UpdateService; use Pterodactyl\Services\Nodes\CreationService; use Pterodactyl\Services\Nodes\DeletionService; use Illuminate\Contracts\Translation\Translator; use Illuminate\Cache\Repository as CacheRepository; -use Pterodactyl\Http\Requests\Admin\NodeFormRequest; -use Pterodactyl\Exceptions\DisplayValidationException; +use Pterodactyl\Services\Allocations\AssignmentService; +use Pterodactyl\Http\Requests\Admin\Node\NodeFormRequest; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Pterodactyl\Http\Requests\Admin\Node\AllocationFormRequest; use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; +use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; +use Pterodactyl\Http\Requests\Admin\Node\AllocationAliasFormRequest; class NodesController extends Controller { @@ -51,6 +51,16 @@ class NodesController extends Controller */ protected $alert; + /** + * @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface + */ + protected $allocationRepository; + + /** + * @var \Pterodactyl\Services\Allocations\AssignmentService + */ + protected $assignmentService; + /** * @var \Illuminate\Cache\Repository */ @@ -86,8 +96,24 @@ class NodesController extends Controller */ protected $updateService; + /** + * NodesController constructor. + * + * @param \Prologue\Alerts\AlertsMessageBag $alert + * @param \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface $allocationRepository + * @param \Pterodactyl\Services\Allocations\AssignmentService $assignmentService + * @param \Illuminate\Cache\Repository $cache + * @param \Pterodactyl\Services\Nodes\CreationService $creationService + * @param \Pterodactyl\Services\Nodes\DeletionService $deletionService + * @param \Pterodactyl\Contracts\Repository\LocationRepositoryInterface $locationRepository + * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository + * @param \Illuminate\Contracts\Translation\Translator $translator + * @param \Pterodactyl\Services\Nodes\UpdateService $updateService + */ public function __construct( AlertsMessageBag $alert, + AllocationRepositoryInterface $allocationRepository, + AssignmentService $assignmentService, CacheRepository $cache, CreationService $creationService, DeletionService $deletionService, @@ -97,6 +123,8 @@ class NodesController extends Controller UpdateService $updateService ) { $this->alert = $alert; + $this->allocationRepository = $allocationRepository; + $this->assignmentService = $assignmentService; $this->cache = $cache; $this->creationService = $creationService; $this->deletionService = $deletionService; @@ -139,7 +167,7 @@ class NodesController extends Controller /** * Post controller to create a new node on the system. * - * @param \Pterodactyl\Http\Requests\Admin\NodeFormRequest $request + * @param \Pterodactyl\Http\Requests\Admin\Node\NodeFormRequest $request * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\Model\DataValidationException @@ -224,8 +252,8 @@ class NodesController extends Controller /** * Updates settings for a node. * - * @param \Pterodactyl\Http\Requests\Admin\NodeFormRequest $request - * @param \Pterodactyl\Models\Node $node + * @param \Pterodactyl\Http\Requests\Admin\Node\NodeFormRequest $request + * @param \Pterodactyl\Models\Node $node * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\DisplayException @@ -242,12 +270,11 @@ class NodesController extends Controller /** * Removes a single allocation from a node. * - * @param \Illuminate\Http\Request $request - * @param int $node - * @param int $allocation + * @param int $node + * @param int $allocation * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse */ - public function allocationRemoveSingle(Request $request, $node, $allocation) + public function allocationRemoveSingle($node, $allocation) { $query = Allocation::where('node_id', $node)->whereNull('server_id')->where('id', $allocation)->delete(); if ($query < 1) { @@ -284,55 +311,35 @@ class NodesController extends Controller /** * Sets an alias for a specific allocation on a node. * - * @param \Illuminate\Http\Request $request - * @param int $node - * @return \Illuminate\Http\Response + * @param \Pterodactyl\Http\Requests\Admin\Node\AllocationAliasFormRequest $request + * @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function allocationSetAlias(Request $request, $node) + public function allocationSetAlias(AllocationAliasFormRequest $request) { - if (! $request->input('allocation_id')) { - return response('Missing required parameters.', 422); - } + $this->allocationRepository->update($request->input('allocation_id'), [ + 'ip_alias' => (empty($request->input('alias'))) ? null : $request->input('alias'), + ]); - try { - $update = Allocation::findOrFail($request->input('allocation_id')); - $update->ip_alias = (empty($request->input('alias'))) ? null : $request->input('alias'); - $update->save(); - - return response('', 204); - } catch (\Exception $ex) { - throw $ex; - } + return response('', 204); } /** * Creates new allocations on a node. * - * @param \Illuminate\Http\Request $request - * @param int $node + * @param \Pterodactyl\Http\Requests\Admin\Node\AllocationFormRequest $request + * @param int|\Pterodactyl\Models\Node $node * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\DisplayException */ - public function createAllocation(Request $request, $node) + public function createAllocation(AllocationFormRequest $request, Node $node) { - $repo = new NodeRepository; + $this->assignmentService->handle($node, $request->normalize()); + $this->alert->success($this->translator->trans('admin/node.notices.allocations_added'))->flash(); - try { - $repo->addAllocations($node, $request->intersect(['allocation_ip', 'allocation_alias', 'allocation_ports'])); - Alert::success('Successfully added new allocations!')->flash(); - } catch (DisplayValidationException $ex) { - return redirect() - ->route('admin.nodes.view.allocation', $node) - ->withErrors(json_decode($ex->getMessage())) - ->withInput(); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An unhandled exception occured while attempting to add allocations this node. This error has been logged.') - ->flash(); - } - - return redirect()->route('admin.nodes.view.allocation', $node); + return redirect()->route('admin.nodes.view.allocation', $node->id); } /** diff --git a/app/Http/Requests/Admin/LocationRequest.php b/app/Http/Requests/Admin/LocationFormRequest.php similarity index 91% rename from app/Http/Requests/Admin/LocationRequest.php rename to app/Http/Requests/Admin/LocationFormRequest.php index 48c61828..a4c8ca8d 100644 --- a/app/Http/Requests/Admin/LocationRequest.php +++ b/app/Http/Requests/Admin/LocationFormRequest.php @@ -26,7 +26,7 @@ namespace Pterodactyl\Http\Requests\Admin; use Pterodactyl\Models\Location; -class LocationRequest extends AdminFormRequest +class LocationFormRequest extends AdminFormRequest { /** * Setup the validation rules to use for these requests. @@ -36,7 +36,7 @@ class LocationRequest extends AdminFormRequest public function rules() { if ($this->method() === 'PATCH') { - return Location::getUpdateRulesForId($this->location->id); + return Location::getUpdateRulesForId($this->route()->parameter('location')->id); } return Location::getCreateRules(); diff --git a/app/Http/Requests/Admin/Node/AllocationAliasFormRequest.php b/app/Http/Requests/Admin/Node/AllocationAliasFormRequest.php new file mode 100644 index 00000000..a2832567 --- /dev/null +++ b/app/Http/Requests/Admin/Node/AllocationAliasFormRequest.php @@ -0,0 +1,41 @@ +. + * + * 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\Http\Requests\Admin\Node; + +use Pterodactyl\Http\Requests\Admin\AdminFormRequest; + +class AllocationAliasFormRequest extends AdminFormRequest +{ + /** + * @return array + */ + public function rules() + { + return [ + 'alias' => 'required|nullable|string', + 'allocation_id' => 'required|numeric|exists:allocations,id', + ]; + } +} diff --git a/app/Http/Requests/Admin/Node/AllocationFormRequest.php b/app/Http/Requests/Admin/Node/AllocationFormRequest.php new file mode 100644 index 00000000..162896b5 --- /dev/null +++ b/app/Http/Requests/Admin/Node/AllocationFormRequest.php @@ -0,0 +1,42 @@ +. + * + * 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\Http\Requests\Admin\Node; + +use Pterodactyl\Http\Requests\Admin\AdminFormRequest; + +class AllocationFormRequest extends AdminFormRequest +{ + /** + * @return array + */ + public function rules() + { + return [ + 'allocation_ip' => 'required|string', + 'allocation_alias' => 'sometimes|string|max:255', + 'allocation_ports' => 'required|array', + ]; + } +} diff --git a/app/Http/Requests/Admin/NodeFormRequest.php b/app/Http/Requests/Admin/Node/NodeFormRequest.php similarity index 95% rename from app/Http/Requests/Admin/NodeFormRequest.php rename to app/Http/Requests/Admin/Node/NodeFormRequest.php index 97080c3b..4ab23aad 100644 --- a/app/Http/Requests/Admin/NodeFormRequest.php +++ b/app/Http/Requests/Admin/Node/NodeFormRequest.php @@ -22,9 +22,10 @@ * SOFTWARE. */ -namespace Pterodactyl\Http\Requests\Admin; +namespace Pterodactyl\Http\Requests\Admin\Node; use Pterodactyl\Models\Node; +use Pterodactyl\Http\Requests\Admin\AdminFormRequest; class NodeFormRequest extends AdminFormRequest { diff --git a/app/Http/Requests/Admin/UserFormRequest.php b/app/Http/Requests/Admin/UserFormRequest.php index 71e1a29d..c067e14d 100644 --- a/app/Http/Requests/Admin/UserFormRequest.php +++ b/app/Http/Requests/Admin/UserFormRequest.php @@ -34,7 +34,7 @@ class UserFormRequest extends AdminFormRequest public function rules() { if ($this->method() === 'PATCH') { - return User::getUpdateRulesForId($this->user->id); + return User::getUpdateRulesForId($this->route()->parameter('user')->id); } return User::getCreateRules(); diff --git a/app/Models/Allocation.php b/app/Models/Allocation.php index d37e52a0..65458be1 100644 --- a/app/Models/Allocation.php +++ b/app/Models/Allocation.php @@ -24,10 +24,15 @@ namespace Pterodactyl\Models; +use Sofa\Eloquence\Eloquence; +use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; +use Sofa\Eloquence\Contracts\Validable as ValidableContract; -class Allocation extends Model +class Allocation extends Model implements ValidableContract { + use Eloquence, Validable; + /** * The table associated with the model. * @@ -42,46 +47,66 @@ class Allocation extends Model */ protected $guarded = ['id', 'created_at', 'updated_at']; - /** - * Cast values to correct type. - * - * @var array - */ - protected $casts = [ - 'node_id' => 'integer', - 'port' => 'integer', - 'server_id' => 'integer', - ]; + /** + * Cast values to correct type. + * + * @var array + */ + protected $casts = [ + 'node_id' => 'integer', + 'port' => 'integer', + 'server_id' => 'integer', + ]; - /** - * Accessor to automatically provide the IP alias if defined. - * - * @param null|string $value - * @return string - */ - public function getAliasAttribute($value) - { - return (is_null($this->ip_alias)) ? $this->ip : $this->ip_alias; - } + /** + * @var array + */ + protected static $applicationRules = [ + 'node_id' => 'required', + 'ip' => 'required', + 'port' => 'required', + ]; - /** - * Accessor to quickly determine if this allocation has an alias. - * - * @param null|string $value - * @return bool - */ - public function getHasAliasAttribute($value) - { - return ! is_null($this->ip_alias); - } + /** + * @var array + */ + protected static $dataIntegrityRules = [ + 'node_id' => 'exists:nodes,id', + 'ip' => 'ip', + 'port' => 'numeric|between:1024,65553', + 'alias' => 'string', + 'server_id' => 'nullable|exists:servers,id', + ]; - /** - * Gets information for the server associated with this allocation. - * - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo - */ - public function server() - { - return $this->belongsTo(Server::class); - } + /** + * Accessor to automatically provide the IP alias if defined. + * + * @param null|string $value + * @return string + */ + public function getAliasAttribute($value) + { + return (is_null($this->ip_alias)) ? $this->ip : $this->ip_alias; + } + + /** + * Accessor to quickly determine if this allocation has an alias. + * + * @param null|string $value + * @return bool + */ + public function getHasAliasAttribute($value) + { + return ! is_null($this->ip_alias); + } + + /** + * Gets information for the server associated with this allocation. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function server() + { + return $this->belongsTo(Server::class); + } } diff --git a/app/Repositories/Eloquent/EloquentRepository.php b/app/Repositories/Eloquent/EloquentRepository.php index c73f0935..98c54622 100644 --- a/app/Repositories/Eloquent/EloquentRepository.php +++ b/app/Repositories/Eloquent/EloquentRepository.php @@ -24,6 +24,7 @@ namespace Pterodactyl\Repositories\Eloquent; +use Illuminate\Database\Query\Expression; use Pterodactyl\Repository\Repository; use Pterodactyl\Contracts\Repository\RepositoryInterface; use Pterodactyl\Exceptions\Model\DataValidationException; @@ -185,6 +186,44 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf return $this->getBuilder()->insert($data); } + /** + * Insert multiple records into the database and ignore duplicates. + * + * @param array $values + * @return bool + */ + public function insertIgnore(array $values) + { + if (empty($values)) { + return true; + } + + if (! is_array(reset($values))) { + $values = [$values]; + } else { + foreach ($values as $key => $value) { + ksort($value); + $values[$key] = $value; + } + } + + $bindings = array_values(array_filter(array_flatten($values, 1), function ($binding) { + return ! $binding instanceof Expression; + })); + + $grammar = $this->getBuilder()->toBase()->getGrammar(); + $table = $grammar->wrapTable($this->getModel()->getTable()); + $columns = $grammar->columnize(array_keys(reset($values))); + + $parameters = collect($values)->map(function ($record) use ($grammar) { + return sprintf('(%s)', $grammar->parameterize($record)); + })->implode(', '); + + $statement = "insert ignore into $table ($columns) values $parameters"; + + return $this->getBuilder()->getConnection()->statement($statement, $bindings); + } + /** * {@inheritdoc} * @return bool|\Illuminate\Database\Eloquent\Model diff --git a/app/Repositories/Eloquent/NodeRepository.php b/app/Repositories/Eloquent/NodeRepository.php index 7375570e..7dd0cb40 100644 --- a/app/Repositories/Eloquent/NodeRepository.php +++ b/app/Repositories/Eloquent/NodeRepository.php @@ -113,10 +113,8 @@ class NodeRepository extends SearchableRepository implements NodeRepositoryInter $instance->setRelation( 'allocations', - $this->getModel()->allocations()->orderBy('ip', 'asc') - ->orderBy('port', 'asc') - ->with('server') - ->paginate(50) + $instance->allocations()->orderBy('ip', 'asc')->orderBy('port', 'asc') + ->with('server')->paginate(50) ); return $instance; diff --git a/app/Services/Allocations/AssignmentService.php b/app/Services/Allocations/AssignmentService.php new file mode 100644 index 00000000..bc7ea35c --- /dev/null +++ b/app/Services/Allocations/AssignmentService.php @@ -0,0 +1,125 @@ +. + * + * 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\Allocations; + +use IPTools\Network; +use Pterodactyl\Models\Node; +use Illuminate\Database\ConnectionInterface; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; + +class AssignmentService +{ + const CIDR_MAX_BITS = 27; + const CIDR_MIN_BITS = 32; + const PORT_RANGE_LIMIT = 1000; + const PORT_RANGE_REGEX = '/^(\d{1,5})-(\d{1,5})$/'; + + /** + * @var \Illuminate\Database\ConnectionInterface + */ + protected $connection; + + /** + * @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface + */ + protected $repository; + + /** + * AssignmentService constructor. + * + * @param \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface $repository + * @param \Illuminate\Database\ConnectionInterface $connection + */ + public function __construct( + AllocationRepositoryInterface $repository, + ConnectionInterface $connection + ) { + $this->connection = $connection; + $this->repository = $repository; + } + + /** + * Insert allocations into the database and link them to a specific node. + * + * @param int|\Pterodactyl\Models\Node $node + * @param array $data + * + * @throws \Pterodactyl\Exceptions\DisplayException + */ + public function handle($node, array $data) + { + if ($node instanceof Node) { + $node = $node->id; + } + + $explode = explode('/', $data['allocation_ip']); + if (count($explode) !== 1) { + if (! ctype_digit($explode[1]) || ($explode[1] > self::CIDR_MIN_BITS || $explode[1] < self::CIDR_MAX_BITS)) { + throw new DisplayException(trans('admin/exceptions.allocations.cidr_out_of_range')); + } + } + + $this->connection->beginTransaction(); + foreach (Network::parse(gethostbyname($data['allocation_ip'])) as $ip) { + foreach ($data['allocation_ports'] as $port) { + if (! ctype_digit($port) && ! preg_match(self::PORT_RANGE_REGEX, $port)) { + throw new DisplayException(trans('admin/exceptions.allocations.invalid_mapping', ['port' => $port])); + } + + $insertData = []; + if (preg_match(self::PORT_RANGE_REGEX, $port, $matches)) { + $block = range($matches[1], $matches[2]); + + if (count($block) > self::PORT_RANGE_LIMIT) { + throw new DisplayException(trans('admin/exceptions.allocations.too_many_ports')); + } + + foreach ($block as $unit) { + $insertData[] = [ + 'node_id' => $node, + 'ip' => $ip->__toString(), + 'port' => (int) $unit, + 'ip_alias' => array_get($data, 'allocation_alias'), + 'server_id' => null, + ]; + } + } else { + $insertData[] = [ + 'node_id' => $node, + 'ip' => $ip->__toString(), + 'port' => (int) $port, + 'ip_alias' => array_get($data, 'allocation_alias'), + 'server_id' => null, + ]; + } + + $this->repository->insertIgnore($insertData); + } + } + + $this->connection->commit(); + } +} diff --git a/coverage.xml b/coverage.xml deleted file mode 100644 index e392d529..00000000 --- a/coverage.xml +++ /dev/null @@ -1,5812 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/database/migrations/2017_08_05_174811_SetAllocationUnqiueUsingMultipleFields.php b/database/migrations/2017_08_05_174811_SetAllocationUnqiueUsingMultipleFields.php new file mode 100644 index 00000000..56e149d4 --- /dev/null +++ b/database/migrations/2017_08_05_174811_SetAllocationUnqiueUsingMultipleFields.php @@ -0,0 +1,32 @@ +unique(['node_id', 'ip', 'port']); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('allocations', function (Blueprint $table) { + $table->dropUnique(['node_id', 'ip', 'port']); + }); + } +} diff --git a/resources/lang/en/admin/exceptions.php b/resources/lang/en/admin/exceptions.php index 1a5bcaa3..f2d9e33f 100644 --- a/resources/lang/en/admin/exceptions.php +++ b/resources/lang/en/admin/exceptions.php @@ -28,4 +28,9 @@ return [ 'servers_attached' => 'A node must have no servers linked to it in order to be deleted.', 'daemon_off_config_updated' => 'The daemon configuration has been updated, however there was an error encountered while attempting to automatically update the configuration file on the Daemon. You will need to manually update the configuration file (core.json) for the daemon to apply these changes. The daemon responded with a HTTP/:code response code and the error has been logged.', ], + 'allocations' => [ + 'too_many_ports' => 'Adding more than 1000 ports at a single time is not supported. Please use a smaller range.', + 'invalid_mapping' => 'The mapping provided for :port was invalid and could not be processed.', + 'cidr_out_of_range' => 'CIDR notation only allows masks between /25 and /32.', + ], ]; diff --git a/resources/lang/en/admin/node.php b/resources/lang/en/admin/node.php index fc5b0b1c..2b3a4bf5 100644 --- a/resources/lang/en/admin/node.php +++ b/resources/lang/en/admin/node.php @@ -28,6 +28,7 @@ return [ 'fqdn_required_for_ssl' => 'A fully qualified domain name that resolves to a public IP address is required in order to use SSL for this node.', ], 'notices' => [ + 'allocations_added' => 'Allocations have successfully been added to this node.', 'node_deleted' => 'Node has been successfully removed from the panel.', 'location_required' => 'You must have at least one location configured before you can add a node to this panel.', 'node_created' => 'Successfully created new node. You can automatically configure the daemon on this machine by visiting the \'Configuration\' tab. Before you can add any servers you must first allocate at least one IP address and port.', diff --git a/tests/Unit/Services/Allocations/AssignmentServiceTest.php b/tests/Unit/Services/Allocations/AssignmentServiceTest.php new file mode 100644 index 00000000..2c222859 --- /dev/null +++ b/tests/Unit/Services/Allocations/AssignmentServiceTest.php @@ -0,0 +1,327 @@ +. + * + * 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\Allocations; + +use Exception; +use Mockery as m; +use Tests\TestCase; +use phpmock\phpunit\PHPMock; +use Pterodactyl\Models\Node; +use Illuminate\Database\ConnectionInterface; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Services\Allocations\AssignmentService; +use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; + +class AssignmentServiceTest extends TestCase +{ + use PHPMock; + + /** + * @var \Illuminate\Database\ConnectionInterface + */ + protected $connection; + + /** + * @var \Pterodactyl\Models\Node + */ + protected $node; + + /** + * @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Allocations\AssignmentService + */ + protected $service; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + // Due to a bug in PHP, this is necessary since we only have a single test + // that relies on this mock. If this does not exist the test will fail to register + // correctly. + // + // This can also be avoided if tests were run in isolated processes, or if that test + // came first, but neither of those are good solutions, so this is the next best option. + PHPMock::defineFunctionMock('\\Pterodactyl\\Services\\Allocations', 'gethostbyname'); + + $this->node = factory(Node::class)->make(); + $this->connection = m::mock(ConnectionInterface::class); + $this->repository = m::mock(AllocationRepositoryInterface::class); + + $this->service = new AssignmentService($this->repository, $this->connection); + } + + /** + * Test a non-CIDR notated IP address without a port range. + */ + public function testIndividualIpAddressWithoutRange() + { + $data = [ + 'allocation_ip' => '192.168.1.1', + 'allocation_ports' => ['1024'] + ]; + + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('insertIgnore')->with([ + [ + 'node_id' => $this->node->id, + 'ip' => '192.168.1.1', + 'port' => 1024, + 'ip_alias' => null, + 'server_id' => null, + ], + ])->once()->andReturnNull(); + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $this->service->handle($this->node->id, $data); + } + + /** + * Test a non-CIDR IP address with a port range provided. + */ + public function testIndividualIpAddressWithRange() + { + $data = [ + 'allocation_ip' => '192.168.1.1', + 'allocation_ports' => ['1024-1026'] + ]; + + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('insertIgnore')->with([ + [ + 'node_id' => $this->node->id, + 'ip' => '192.168.1.1', + 'port' => 1024, + 'ip_alias' => null, + 'server_id' => null, + ], + [ + 'node_id' => $this->node->id, + 'ip' => '192.168.1.1', + 'port' => 1025, + 'ip_alias' => null, + 'server_id' => null, + ], + [ + 'node_id' => $this->node->id, + 'ip' => '192.168.1.1', + 'port' => 1026, + 'ip_alias' => null, + 'server_id' => null, + ], + ])->once()->andReturnNull(); + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $this->service->handle($this->node->id, $data); + + } + + /** + * Test a non-CIRD IP address with a single port and an alias. + */ + public function testIndividualIPAddressWithAlias() + { + $data = [ + 'allocation_ip' => '192.168.1.1', + 'allocation_ports' => ['1024'], + 'allocation_alias' => 'my.alias.net', + ]; + + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('insertIgnore')->with([ + [ + 'node_id' => $this->node->id, + 'ip' => '192.168.1.1', + 'port' => 1024, + 'ip_alias' => 'my.alias.net', + 'server_id' => null, + ], + ])->once()->andReturnNull(); + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $this->service->handle($this->node->id, $data); + } + + /** + * Test that a domain name can be passed in place of an IP address. + */ + public function testDomainNamePassedInPlaceOfIPAddress() + { + $data = [ + 'allocation_ip' => 'test-domain.com', + 'allocation_ports' => ['1024'], + ]; + + $this->getFunctionMock('\\Pterodactyl\\Services\\Allocations', 'gethostbyname') + ->expects($this->once())->willReturn('192.168.1.1'); + + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('insertIgnore')->with([ + [ + 'node_id' => $this->node->id, + 'ip' => '192.168.1.1', + 'port' => 1024, + 'ip_alias' => null, + 'server_id' => null, + ], + ])->once()->andReturnNull(); + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $this->service->handle($this->node->id, $data); + } + + /** + * Test that a CIDR IP address without a range works properly. + */ + public function testCIDRNotatedIPAddressWithoutRange() + { + $data = [ + 'allocation_ip' => '192.168.1.100/31', + 'allocation_ports' => ['1024'], + ]; + + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('insertIgnore')->with([ + [ + 'node_id' => $this->node->id, + 'ip' => '192.168.1.100', + 'port' => 1024, + 'ip_alias' => null, + 'server_id' => null, + ], + ])->once()->andReturnNull(); + + $this->repository->shouldReceive('insertIgnore')->with([ + [ + 'node_id' => $this->node->id, + 'ip' => '192.168.1.101', + 'port' => 1024, + 'ip_alias' => null, + 'server_id' => null, + ], + ])->once()->andReturnNull(); + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $this->service->handle($this->node->id, $data); + } + + /** + * Test that a CIDR IP address with a range works properly. + */ + public function testCIDRNotatedIPAddressOutsideRangeLimit() + { + $data = [ + 'allocation_ip' => '192.168.1.100/20', + 'allocation_ports' => ['1024'], + ]; + + try { + $this->service->handle($this->node->id, $data); + } catch (Exception $exception) { + $this->assertInstanceOf(DisplayException::class, $exception); + $this->assertEquals(trans('admin/exceptions.allocations.cidr_out_of_range'), $exception->getMessage()); + } + } + + /** + * Test that an exception is thrown if there are too many ports. + */ + public function testAllocationWithPortsExceedingLimit() + { + $data = [ + 'allocation_ip' => '192.168.1.1', + 'allocation_ports' => ['5000-7000'], + ]; + + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + + try { + $this->service->handle($this->node->id, $data); + } catch (Exception $exception) { + if (! $exception instanceof DisplayException) { + throw $exception; + } + + $this->assertInstanceOf(DisplayException::class, $exception); + $this->assertEquals(trans('admin/exceptions.allocations.too_many_ports'), $exception->getMessage()); + } + } + + /** + * Test that an exception is thrown if an invalid port is provided. + */ + public function testInvalidPortProvided() + { + $data = [ + 'allocation_ip' => '192.168.1.1', + 'allocation_ports' => ['test123'], + ]; + + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + + try { + $this->service->handle($this->node->id, $data); + } catch (Exception $exception) { + if (! $exception instanceof DisplayException) { + throw $exception; + } + + $this->assertInstanceOf(DisplayException::class, $exception); + $this->assertEquals(trans('admin/exceptions.allocations.invalid_mapping', ['port' => 'test123']), $exception->getMessage()); + } + } + + /** + * Test that a model can be passed in place of an ID. + */ + public function testModelCanBePassedInPlaceOfNodeModel() + { + $data = [ + 'allocation_ip' => '192.168.1.1', + 'allocation_ports' => ['1024'] + ]; + + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('insertIgnore')->with([ + [ + 'node_id' => $this->node->id, + 'ip' => '192.168.1.1', + 'port' => 1024, + 'ip_alias' => null, + 'server_id' => null, + ], + ])->once()->andReturnNull(); + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $this->service->handle($this->node, $data); + } +} From 7277f728a9c06f132539462ed8cbc8f07ba85b68 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 8 Aug 2017 21:21:10 -0500 Subject: [PATCH 45/99] Complete migration of node controllers/repositories to new service structure --- .../Repository/RepositoryInterface.php | 10 +++- .../Controllers/Admin/NodesController.php | 49 +++++++------------ .../Eloquent/EloquentRepository.php | 16 ++++-- resources/lang/en/admin/node.php | 1 + 4 files changed, 40 insertions(+), 36 deletions(-) diff --git a/app/Contracts/Repository/RepositoryInterface.php b/app/Contracts/Repository/RepositoryInterface.php index f48755d6..a26646d0 100644 --- a/app/Contracts/Repository/RepositoryInterface.php +++ b/app/Contracts/Repository/RepositoryInterface.php @@ -84,10 +84,18 @@ interface RepositoryInterface * Delete a given record from the database. * * @param int $id - * @return bool|null + * @return int */ public function delete($id); + /** + * Delete records matching the given attributes. + * + * @param array $attributes + * @return int + */ + public function deleteWhere(array $attributes); + /** * Find a model that has the specific ID passed. * diff --git a/app/Http/Controllers/Admin/NodesController.php b/app/Http/Controllers/Admin/NodesController.php index 97b85abc..4be09917 100644 --- a/app/Http/Controllers/Admin/NodesController.php +++ b/app/Http/Controllers/Admin/NodesController.php @@ -24,17 +24,14 @@ namespace Pterodactyl\Http\Controllers\Admin; -use Alert; use Javascript; use Illuminate\Http\Request; use Pterodactyl\Models\Node; -use Pterodactyl\Models\Allocation; use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Services\Nodes\UpdateService; use Pterodactyl\Services\Nodes\CreationService; use Pterodactyl\Services\Nodes\DeletionService; -use Illuminate\Contracts\Translation\Translator; use Illuminate\Cache\Repository as CacheRepository; use Pterodactyl\Services\Allocations\AssignmentService; use Pterodactyl\Http\Requests\Admin\Node\NodeFormRequest; @@ -86,11 +83,6 @@ class NodesController extends Controller */ protected $repository; - /** - * @var \Illuminate\Contracts\Translation\Translator - */ - protected $translator; - /** * @var \Pterodactyl\Services\Nodes\UpdateService */ @@ -107,7 +99,6 @@ class NodesController extends Controller * @param \Pterodactyl\Services\Nodes\DeletionService $deletionService * @param \Pterodactyl\Contracts\Repository\LocationRepositoryInterface $locationRepository * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository - * @param \Illuminate\Contracts\Translation\Translator $translator * @param \Pterodactyl\Services\Nodes\UpdateService $updateService */ public function __construct( @@ -119,7 +110,6 @@ class NodesController extends Controller DeletionService $deletionService, LocationRepositoryInterface $locationRepository, NodeRepositoryInterface $repository, - Translator $translator, UpdateService $updateService ) { $this->alert = $alert; @@ -130,7 +120,6 @@ class NodesController extends Controller $this->deletionService = $deletionService; $this->locationRepository = $locationRepository; $this->repository = $repository; - $this->translator = $translator; $this->updateService = $updateService; } @@ -156,7 +145,7 @@ class NodesController extends Controller { $locations = $this->locationRepository->all(); if (count($locations) < 1) { - $this->alert->warning($this->translator->trans('admin/node.notices.location_required'))->flash(); + $this->alert->warning(trans('admin/node.notices.location_required'))->flash(); return redirect()->route('admin.locations'); } @@ -175,7 +164,7 @@ class NodesController extends Controller public function store(NodeFormRequest $request) { $node = $this->creationService->handle($request->normalize()); - $this->alert->info($this->translator->trans('admin/node.notices.node_created'))->flash(); + $this->alert->info(trans('admin/node.notices.node_created'))->flash(); return redirect()->route('admin.nodes.view.allocation', $node->id); } @@ -262,7 +251,7 @@ class NodesController extends Controller public function updateSettings(NodeFormRequest $request, Node $node) { $this->updateService->handle($node, $request->normalize()); - $this->alert->success($this->translator->trans('admin/node.notices.node_updated'))->flash(); + $this->alert->success(trans('admin/node.notices.node_updated'))->flash(); return redirect()->route('admin.nodes.view.settings', $node->id)->withInput(); } @@ -276,12 +265,11 @@ class NodesController extends Controller */ public function allocationRemoveSingle($node, $allocation) { - $query = Allocation::where('node_id', $node)->whereNull('server_id')->where('id', $allocation)->delete(); - if ($query < 1) { - return response()->json([ - 'error' => 'Unable to find an allocation matching those details to delete.', - ], 400); - } + $this->allocationRepository->deleteWhere([ + ['id', '=', $allocation], + ['node_id', '=', $node], + ['server_id', '=', null], + ]); return response('', 204); } @@ -295,15 +283,14 @@ class NodesController extends Controller */ public function allocationRemoveBlock(Request $request, $node) { - $query = Allocation::where('node_id', $node) - ->whereNull('server_id') - ->where('ip', $request->input('ip')) - ->delete(); - if ($query < 1) { - Alert::danger('There was an error while attempting to delete allocations on that IP.')->flash(); - } else { - Alert::success('Deleted all unallocated ports for ' . $request->input('ip') . '.')->flash(); - } + $this->allocationRepository->deleteWhere([ + ['node_id', '=', $node], + ['server_id', '=', null], + ['ip', '=', $request->input('ip')], + ]); + + $this->alert->success(trans('admin/node.notices.unallocated_deleted', ['ip' => $request->input('ip')])) + ->flash(); return redirect()->route('admin.nodes.view.allocation', $node); } @@ -337,7 +324,7 @@ class NodesController extends Controller public function createAllocation(AllocationFormRequest $request, Node $node) { $this->assignmentService->handle($node, $request->normalize()); - $this->alert->success($this->translator->trans('admin/node.notices.allocations_added'))->flash(); + $this->alert->success(trans('admin/node.notices.allocations_added'))->flash(); return redirect()->route('admin.nodes.view.allocation', $node->id); } @@ -353,7 +340,7 @@ class NodesController extends Controller public function delete($node) { $this->deletionService->handle($node); - $this->alert->success($this->translator->trans('admin/node.notices.node_deleted'))->flash(); + $this->alert->success(trans('admin/node.notices.node_deleted'))->flash(); return redirect()->route('admin.nodes'); } diff --git a/app/Repositories/Eloquent/EloquentRepository.php b/app/Repositories/Eloquent/EloquentRepository.php index 98c54622..77108b59 100644 --- a/app/Repositories/Eloquent/EloquentRepository.php +++ b/app/Repositories/Eloquent/EloquentRepository.php @@ -119,11 +119,19 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf */ public function delete($id, $destroy = false) { - if ($destroy) { - return $this->getBuilder()->where($this->getModel()->getKeyName(), $id)->forceDelete(); - } + $instance = $this->getBuilder()->where($this->getModel()->getKeyName(), $id); - return $this->getBuilder()->where($this->getModel()->getKeyName(), $id)->delete(); + return ($destroy) ? $instance->forceDelete() : $instance->delete(); + } + + /** + * {@inheritdoc} + */ + public function deleteWhere(array $attributes, $force = false) + { + $instance = $this->getBuilder()->where($attributes); + + return ($force) ? $instance->forceDelete() : $instance->delete(); } /** diff --git a/resources/lang/en/admin/node.php b/resources/lang/en/admin/node.php index 2b3a4bf5..8e685a8f 100644 --- a/resources/lang/en/admin/node.php +++ b/resources/lang/en/admin/node.php @@ -33,5 +33,6 @@ return [ 'location_required' => 'You must have at least one location configured before you can add a node to this panel.', 'node_created' => 'Successfully created new node. You can automatically configure the daemon on this machine by visiting the \'Configuration\' tab. Before you can add any servers you must first allocate at least one IP address and port.', 'node_updated' => 'Node information has been updated. If any daemon settings were changed you will need to reboot it for those changes to take effect.', + 'unallocated_deleted' => 'Deleted all unallocatred ports for :ip.', ], ]; From 2c77d5c44d53fa5e319e5377b6a5ffca73858077 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 8 Aug 2017 23:24:55 -0500 Subject: [PATCH 46/99] Begin implementation of services for services/service options --- .../DatabaseHostRepositoryInterface.php | 7 + .../LocationRepositoryInterface.php | 9 +- .../ServiceOptionRepositoryInterface.php | 30 ++ .../Controllers/Admin/DatabaseController.php | 35 ++- .../Controllers/Admin/LocationController.php | 2 +- .../Controllers/Admin/OptionController.php | 141 +++++---- .../Admin/OptionVariableFormRequest.php | 57 ++++ .../Admin/ServiceOptionFormRequest.php | 42 +++ app/Models/ServiceOption.php | 76 ++++- app/Models/ServiceVariable.php | 53 +++- app/Providers/RepositoryServiceProvider.php | 4 + .../Eloquent/DatabaseHostRepository.php | 8 + .../Eloquent/LocationRepository.php | 10 +- .../Eloquent/ServiceOptionRepository.php | 39 +++ app/Repositories/Old/NodeRepository.php | 291 ------------------ .../Services/Options/CreationService.php | 73 +++++ .../Variables/VariableCreationService.php | 52 ++++ resources/lang/en/admin/exceptions.php | 5 + resources/lang/en/admin/services.php | 36 +++ .../admin/services/options/new.blade.php | 2 +- routes/admin.php | 3 +- 21 files changed, 567 insertions(+), 408 deletions(-) create mode 100644 app/Contracts/Repository/ServiceOptionRepositoryInterface.php create mode 100644 app/Http/Requests/Admin/OptionVariableFormRequest.php create mode 100644 app/Http/Requests/Admin/ServiceOptionFormRequest.php create mode 100644 app/Repositories/Eloquent/ServiceOptionRepository.php delete mode 100644 app/Repositories/Old/NodeRepository.php create mode 100644 app/Services/Services/Options/CreationService.php create mode 100644 app/Services/Services/Variables/VariableCreationService.php create mode 100644 resources/lang/en/admin/services.php diff --git a/app/Contracts/Repository/DatabaseHostRepositoryInterface.php b/app/Contracts/Repository/DatabaseHostRepositoryInterface.php index eeb24fdc..25a6ebf2 100644 --- a/app/Contracts/Repository/DatabaseHostRepositoryInterface.php +++ b/app/Contracts/Repository/DatabaseHostRepositoryInterface.php @@ -26,6 +26,13 @@ namespace Pterodactyl\Contracts\Repository; interface DatabaseHostRepositoryInterface extends RepositoryInterface { + /** + * Return database hosts with a count of databases and the node information for which it is attached. + * + * @return \Illuminate\Support\Collection + */ + public function getWithViewDetails(); + /** * Delete a database host from the DB if there are no databases using it. * diff --git a/app/Contracts/Repository/LocationRepositoryInterface.php b/app/Contracts/Repository/LocationRepositoryInterface.php index a5d0c665..10ba143e 100644 --- a/app/Contracts/Repository/LocationRepositoryInterface.php +++ b/app/Contracts/Repository/LocationRepositoryInterface.php @@ -44,7 +44,14 @@ interface LocationRepositoryInterface extends RepositoryInterface, SearchableInt * * @return mixed */ - public function allWithDetails(); + public function getAllWithDetails(); + + /** + * Return all of the available locations with the nodes as a relationship. + * + * @return \Illuminate\Support\Collection + */ + public function getAllWithNodes(); /** * Return all of the nodes and their respective count of servers for a location. diff --git a/app/Contracts/Repository/ServiceOptionRepositoryInterface.php b/app/Contracts/Repository/ServiceOptionRepositoryInterface.php new file mode 100644 index 00000000..b50d66a7 --- /dev/null +++ b/app/Contracts/Repository/ServiceOptionRepositoryInterface.php @@ -0,0 +1,30 @@ +. + * + * 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\Contracts\Repository; + +interface ServiceOptionRepositoryInterface extends RepositoryInterface +{ + // +} diff --git a/app/Http/Controllers/Admin/DatabaseController.php b/app/Http/Controllers/Admin/DatabaseController.php index 4f61c848..bfea288d 100644 --- a/app/Http/Controllers/Admin/DatabaseController.php +++ b/app/Http/Controllers/Admin/DatabaseController.php @@ -24,12 +24,13 @@ namespace Pterodactyl\Http\Controllers\Admin; -use Pterodactyl\Models\Location; use Pterodactyl\Models\DatabaseHost; use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Services\Database\DatabaseHostService; use Pterodactyl\Http\Requests\Admin\DatabaseHostFormRequest; +use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; +use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; class DatabaseController extends Controller { @@ -39,14 +40,14 @@ class DatabaseController extends Controller protected $alert; /** - * @var \Pterodactyl\Models\DatabaseHost + * @var \Pterodactyl\Contracts\Repository\LocationRepositoryInterface */ - protected $hostModel; + protected $locationRepository; /** - * @var \Pterodactyl\Models\Location + * @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface */ - protected $locationModel; + protected $repository; /** * @var \Pterodactyl\Services\Database\DatabaseHostService @@ -56,21 +57,21 @@ class DatabaseController extends Controller /** * DatabaseController constructor. * - * @param \Prologue\Alerts\AlertsMessageBag $alert - * @param \Pterodactyl\Models\DatabaseHost $hostModel - * @param \Pterodactyl\Models\Location $locationModel - * @param \Pterodactyl\Services\Database\DatabaseHostService $service + * @param \Prologue\Alerts\AlertsMessageBag $alert + * @param \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface $repository + * @param \Pterodactyl\Services\Database\DatabaseHostService $service + * @param \Pterodactyl\Contracts\Repository\LocationRepositoryInterface $locationRepository */ public function __construct( AlertsMessageBag $alert, - DatabaseHost $hostModel, - Location $locationModel, - DatabaseHostService $service + DatabaseHostRepositoryInterface $repository, + DatabaseHostService $service, + LocationRepositoryInterface $locationRepository ) { $this->alert = $alert; - $this->hostModel = $hostModel; - $this->locationModel = $locationModel; + $this->repository = $repository; $this->service = $service; + $this->locationRepository = $locationRepository; } /** @@ -81,8 +82,8 @@ class DatabaseController extends Controller public function index() { return view('admin.databases.index', [ - 'locations' => $this->locationModel->with('nodes')->get(), - 'hosts' => $this->hostModel->withCount('databases')->with('node')->get(), + 'locations' => $this->locationRepository->getAllWithNodes(), + 'hosts' => $this->repository->getWithViewDetails(), ]); } @@ -97,7 +98,7 @@ class DatabaseController extends Controller $host->load('databases.server'); return view('admin.databases.view', [ - 'locations' => $this->locationModel->with('nodes')->get(), + 'locations' => $this->locationRepository->getAllWithNodes(), 'host' => $host, ]); } diff --git a/app/Http/Controllers/Admin/LocationController.php b/app/Http/Controllers/Admin/LocationController.php index 11d67ac0..63375cc9 100644 --- a/app/Http/Controllers/Admin/LocationController.php +++ b/app/Http/Controllers/Admin/LocationController.php @@ -74,7 +74,7 @@ class LocationController extends Controller public function index() { return view('admin.locations.index', [ - 'locations' => $this->repository->allWithDetails(), + 'locations' => $this->repository->getAllWithDetails(), ]); } diff --git a/app/Http/Controllers/Admin/OptionController.php b/app/Http/Controllers/Admin/OptionController.php index 02e40013..84954ef1 100644 --- a/app/Http/Controllers/Admin/OptionController.php +++ b/app/Http/Controllers/Admin/OptionController.php @@ -26,6 +26,12 @@ namespace Pterodactyl\Http\Controllers\Admin; use Log; use Alert; +use Prologue\Alerts\AlertsMessageBag; +use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; +use Pterodactyl\Http\Requests\Admin\OptionVariableFormRequest; +use Pterodactyl\Http\Requests\Admin\ServiceOptionFormRequest; +use Pterodactyl\Services\Services\Options\CreationService; +use Pterodactyl\Services\Services\Variables\VariableCreationService; use Route; use Javascript; use Illuminate\Http\Request; @@ -42,100 +48,95 @@ use Pterodactyl\Http\Requests\Admin\Service\StoreOptionVariable; class OptionController extends Controller { /** - * Store the repository instance. - * - * @var \Pterodactyl\Repositories\OptionRepository + * @var \Prologue\Alerts\AlertsMessageBag */ - protected $repository; + protected $alert; + + /** + * @var \Pterodactyl\Services\Services\Options\CreationService + */ + protected $creationService; + + /** + * @var \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface + */ + protected $serviceRepository; + + /** + * @var \Pterodactyl\Services\Services\Variables\VariableCreationService + */ + protected $variableCreationService; /** * OptionController constructor. + * + * @param \Prologue\Alerts\AlertsMessageBag $alert + * @param \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface $serviceRepository + * @param \Pterodactyl\Services\Services\Options\CreationService $creationService + * @param \Pterodactyl\Services\Services\Variables\VariableCreationService $variableCreationService */ - public function __construct() - { - $this->repository = new OptionRepository(Route::current()->parameter('option')); + public function __construct( + AlertsMessageBag $alert, + ServiceRepositoryInterface $serviceRepository, + CreationService $creationService, + VariableCreationService $variableCreationService + ) { + $this->alert = $alert; + $this->creationService = $creationService; + $this->serviceRepository = $serviceRepository; + $this->variableCreationService = $variableCreationService; } /** * Handles request to view page for adding new option. * - * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ - public function create(Request $request) + public function create() { - $services = Service::with('options')->get(); + $services = $this->serviceRepository->getWithOptions(); Javascript::put(['services' => $services->keyBy('id')]); return view('admin.services.options.new', ['services' => $services]); } /** - * Handles POST request to create a new option. + * Handle adding a new service option. * - * @param \Illuminate\Http\Request $request - * @return \Illuminate\Response\RedirectResponse + * @param \Pterodactyl\Http\Requests\Admin\ServiceOptionFormRequest $request + * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function store(Request $request) + public function store(ServiceOptionFormRequest $request) { - $repo = new OptionRepository; + $option = $this->creationService->handle($request->normalize()); + $this->alert->success(trans('admin/services.options.notices.option_created'))->flash(); - try { - $option = $repo->create($request->intersect([ - 'service_id', 'name', 'description', 'tag', - 'docker_image', 'startup', 'config_from', 'config_startup', - 'config_logs', 'config_files', 'config_stop', - ])); - Alert::success('Successfully created new service option.')->flash(); - - return redirect()->route('admin.services.option.view', $option->id); - } catch (DisplayValidationException $ex) { - return redirect()->route('admin.services.option.new')->withErrors(json_decode($ex->getMessage()))->withInput(); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An unhandled exception occurred while attempting to create this service. This error has been logged.')->flash(); - } - - return redirect()->route('admin.services.option.new')->withInput(); + return redirect()->route('admin.services.option.view', $option->id); } /** * Handles POST request to create a new option variable. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Pterodactyl\Http\Requests\Admin\OptionVariableFormRequest $request + * @param \Pterodactyl\Models\ServiceOption $option * @return \Illuminate\Http\RedirectResponse */ - public function createVariable(Request $request, $id) + public function createVariable(OptionVariableFormRequest $request, ServiceOption $option) { - $repo = new VariableRepository; + $this->variableCreationService->handle($option->id, $request->normalize()); + $this->alert->success(trans('admin/services.variables.notices.variable_created'))->flash(); - try { - $variable = $repo->create($id, $request->intersect([ - 'name', 'description', 'env_variable', - 'default_value', 'options', 'rules', - ])); - - Alert::success('New variable successfully assigned to this service option.')->flash(); - } catch (DisplayValidationException $ex) { - return redirect()->route('admin.services.option.variables', $id)->withErrors(json_decode($ex->getMessage())); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An unhandled exception was encountered while attempting to process that request. This error has been logged.')->flash(); - } - - return redirect()->route('admin.services.option.variables', $id); + return redirect()->route('admin.services.option.variables', $option->id); } /** * Display option overview page. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param int $id * @return \Illuminate\View\View */ public function viewConfiguration(Request $request, $id) @@ -146,13 +147,14 @@ class OptionController extends Controller /** * Display variable overview page for a service option. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param int $id * @return \Illuminate\View\View */ public function viewVariables(Request $request, $id) { - return view('admin.services.options.variables', ['option' => ServiceOption::with('variables')->findOrFail($id)]); + return view('admin.services.options.variables', ['option' => ServiceOption::with('variables') + ->findOrFail($id), ]); } /** @@ -179,8 +181,8 @@ class OptionController extends Controller /** * Handles POST when editing a configration for a service option. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param int $id * @return \Illuminate\Http\RedirectResponse */ public function editConfiguration(Request $request, $id) @@ -207,7 +209,8 @@ class OptionController extends Controller Alert::danger($ex->getMessage())->flash(); } catch (\Exception $ex) { Log::error($ex); - Alert::danger('An unhandled exception occurred while attempting to perform that action. This error has been logged.')->flash(); + Alert::danger('An unhandled exception occurred while attempting to perform that action. This error has been logged.') + ->flash(); } return redirect()->route('admin.services.option.view', $id); @@ -216,9 +219,9 @@ class OptionController extends Controller /** * Handles POST when editing a configration for a service option. * - * @param \Pterodactyl\Http\Requests\Admin\Service\StoreOptionVariable $request - * @param int $option - * @param int $variable + * @param \Pterodactyl\Http\Requests\Admin\Service\StoreOptionVariable $request + * @param int $option + * @param int $variable * @return \Illuminate\Http\RedirectResponse */ public function editVariable(StoreOptionVariable $request, $option, $variable) @@ -237,7 +240,8 @@ class OptionController extends Controller Alert::danger($ex->getMessage())->flash(); } catch (\Exception $ex) { Log::error($ex); - Alert::danger('An unhandled exception was encountered while attempting to process that request. This error has been logged.')->flash(); + Alert::danger('An unhandled exception was encountered while attempting to process that request. This error has been logged.') + ->flash(); } return redirect()->route('admin.services.option.variables', $option); @@ -259,7 +263,8 @@ class OptionController extends Controller Alert::danger($ex->getMessage())->flash(); } catch (\Exception $ex) { Log::error($ex); - Alert::danger('An unhandled exception was encountered while attempting to process that request. This error has been logged.')->flash(); + Alert::danger('An unhandled exception was encountered while attempting to process that request. This error has been logged.') + ->flash(); } return redirect()->route('admin.services.option.scripts', $id); diff --git a/app/Http/Requests/Admin/OptionVariableFormRequest.php b/app/Http/Requests/Admin/OptionVariableFormRequest.php new file mode 100644 index 00000000..fe0a43e5 --- /dev/null +++ b/app/Http/Requests/Admin/OptionVariableFormRequest.php @@ -0,0 +1,57 @@ +. + * + * 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\Http\Requests\Admin; + +use Pterodactyl\Models\ServiceVariable; + +class OptionVariableFormRequest extends AdminFormRequest +{ + /** + * @return array + */ + public function rules() + { + return [ + 'name' => 'required|string|min:1|max:255', + 'description' => 'sometimes|nullable|string', + 'env_variable' => 'required|regex:/^[\w]{1,255}$/|notIn:' . ServiceVariable::RESERVED_ENV_NAMES, + 'default_value' => 'string', + 'options' => 'sometimes|required|array', + 'rules' => 'bail|required|string', + ]; + } + + /** + * Run validation after the rules above have been applied. + * + * @param \Illuminate\Validation\Validator $validator + */ + public function withValidator($validator) + { + $validator->sometimes('default_value', $this->input('rules') ?? null, function ($input) { + return $input->default_value; + }); + } +} diff --git a/app/Http/Requests/Admin/ServiceOptionFormRequest.php b/app/Http/Requests/Admin/ServiceOptionFormRequest.php new file mode 100644 index 00000000..5986e6d7 --- /dev/null +++ b/app/Http/Requests/Admin/ServiceOptionFormRequest.php @@ -0,0 +1,42 @@ +. + * + * 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\Http\Requests\Admin; + +use Pterodactyl\Models\ServiceOption; + +class ServiceOptionFormRequest extends AdminFormRequest +{ + /** + * @return array + */ + public function rules() + { + if ($this->method() === 'PATCH') { + return ServiceOption::getUpdateRulesForId($this->route()->parameter('option')->id); + } + + return ServiceOption::getCreateRules(); + } +} diff --git a/app/Models/ServiceOption.php b/app/Models/ServiceOption.php index 5f0cd9c9..9c0343bd 100644 --- a/app/Models/ServiceOption.php +++ b/app/Models/ServiceOption.php @@ -24,10 +24,15 @@ namespace Pterodactyl\Models; +use Sofa\Eloquence\Eloquence; +use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; +use Sofa\Eloquence\Contracts\Validable as ValidableContract; -class ServiceOption extends Model +class ServiceOption extends Model implements ValidableContract { + use Eloquence, Validable; + /** * The table associated with the model. * @@ -42,15 +47,61 @@ class ServiceOption extends Model */ protected $guarded = ['id', 'created_at', 'updated_at']; - /** - * Cast values to correct type. - * - * @var array - */ - protected $casts = [ - 'service_id' => 'integer', - 'script_is_privileged' => 'boolean', - ]; + /** + * Cast values to correct type. + * + * @var array + */ + protected $casts = [ + 'service_id' => 'integer', + 'script_is_privileged' => 'boolean', + ]; + + /** + * @var array + */ + protected static $applicationRules = [ + 'service_id' => 'required', + 'name' => 'required', + 'description' => 'required', + 'tag' => 'required', + 'docker_image' => 'sometimes', + 'startup' => 'sometimes', + 'config_from' => 'sometimes', + 'config_stop' => 'required_without:config_from', + 'config_startup' => 'required_without:config_from', + 'config_logs' => 'required_without:config_from', + 'config_files' => 'required_without:config_from', + ]; + + /** + * @var array + */ + protected static $dataIntegrityRules = [ + 'service_id' => 'numeric|exists:services,id', + 'name' => 'string|max:255', + 'description' => 'string', + 'tag' => 'alpha_num|max:60|unique:service_options,tag', + 'docker_image' => 'string|max:255', + 'startup' => 'nullable|string', + 'config_from' => 'nullable|numeric|exists:service_options,id', + 'config_stop' => 'nullable|string|max:255', + 'config_startup' => 'nullable|json', + 'config_logs' => 'nullable|json', + 'config_files' => 'nullable|json', + ]; + + /** + * @var array + */ + protected $attributes = [ + 'config_stop' => null, + 'config_startup' => null, + 'config_logs' => null, + 'config_files' => null, + 'startup' => null, + 'docker_image' => null, + ]; /** * Returns the display startup string for the option and will use the parent @@ -136,6 +187,11 @@ class ServiceOption extends Model return $this->hasMany(Pack::class, 'option_id'); } + /** + * Get the parent service option from which to copy scripts. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ public function copyFrom() { return $this->belongsTo(self::class, 'copy_script_from'); diff --git a/app/Models/ServiceVariable.php b/app/Models/ServiceVariable.php index 93e93e7e..a7838bcd 100644 --- a/app/Models/ServiceVariable.php +++ b/app/Models/ServiceVariable.php @@ -24,10 +24,22 @@ namespace Pterodactyl\Models; +use Sofa\Eloquence\Eloquence; +use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; +use Sofa\Eloquence\Contracts\Validable as ValidableContract; -class ServiceVariable extends Model +class ServiceVariable extends Model implements ValidableContract { + use Eloquence, Validable; + + /** + * Reserved environment variable names. + * + * @var array + */ + const RESERVED_ENV_NAMES = 'SERVER_MEMORY,SERVER_IP,SERVER_PORT,ENV,HOME,USER,STARTUP,SERVER_UUID,UUID'; + /** * The table associated with the model. * @@ -54,28 +66,35 @@ class ServiceVariable extends Model ]; /** - * Reserved environment variable names. - * * @var array */ - protected static $reservedNames = [ - 'SERVER_MEMORY', - 'SERVER_IP', - 'SERVER_PORT', - 'ENV', - 'HOME', - 'USER', + protected static $applicationRules = [ + 'name' => 'required', + 'env_variable' => 'required', + 'rules' => 'required', ]; /** - * Returns an array of environment variable names that cannot be used. - * - * @return array + * @var array */ - public static function reservedNames() - { - return self::$reservedNames; - } + protected static $dataIntegrityRules = [ + 'option_id' => 'exists:service_options,id', + 'name' => 'string|between:1,255', + 'description' => 'nullable|string', + 'env_variable' => 'regex:/^[\w]{1,255}$/|notIn:' . self::RESERVED_ENV_NAMES, + 'default_value' => 'string', + 'user_viewable' => 'boolean', + 'user_editable' => 'boolean', + 'rules' => 'string', + ]; + + /** + * @var array + */ + protected $attributes = [ + 'user_editable' => 0, + 'user_viewable' => 0, + ]; /** * Returns the display executable for the option and will use the parent diff --git a/app/Providers/RepositoryServiceProvider.php b/app/Providers/RepositoryServiceProvider.php index 549a41af..5206501d 100644 --- a/app/Providers/RepositoryServiceProvider.php +++ b/app/Providers/RepositoryServiceProvider.php @@ -38,6 +38,7 @@ use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Repositories\Eloquent\DatabaseHostRepository; use Pterodactyl\Repositories\Eloquent\ApiPermissionRepository; +use Pterodactyl\Repositories\Eloquent\ServiceOptionRepository; use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Repositories\Eloquent\OptionVariableRepository; @@ -48,6 +49,7 @@ use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; use Pterodactyl\Contracts\Repository\ApiPermissionRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; use Pterodactyl\Contracts\Repository\OptionVariableRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface; @@ -61,6 +63,7 @@ class RepositoryServiceProvider extends ServiceProvider */ public function register() { + // Eloquent Repositories $this->app->bind(AllocationRepositoryInterface::class, AllocationRepository::class); $this->app->bind(ApiKeyRepositoryInterface::class, ApiKeyRepository::class); $this->app->bind(ApiPermissionRepositoryInterface::class, ApiPermissionRepository::class); @@ -72,6 +75,7 @@ class RepositoryServiceProvider extends ServiceProvider $this->app->bind(ServerRepositoryInterface::class, ServerRepository::class); $this->app->bind(ServerVariableRepositoryInterface::class, ServerVariableRepository::class); $this->app->bind(ServiceRepositoryInterface::class, ServiceRepository::class); + $this->app->bind(ServiceOptionRepositoryInterface::class, ServiceOptionRepository::class); $this->app->bind(UserRepositoryInterface::class, UserRepository::class); // Daemon Repositories diff --git a/app/Repositories/Eloquent/DatabaseHostRepository.php b/app/Repositories/Eloquent/DatabaseHostRepository.php index 6cb48bbf..166fd881 100644 --- a/app/Repositories/Eloquent/DatabaseHostRepository.php +++ b/app/Repositories/Eloquent/DatabaseHostRepository.php @@ -39,6 +39,14 @@ class DatabaseHostRepository extends EloquentRepository implements DatabaseHostR return DatabaseHost::class; } + /** + * {@inheritdoc} + */ + public function getWithViewDetails() + { + return $this->getBuilder()->withCount('databases')->with('node')->get(); + } + /** * {@inheritdoc} */ diff --git a/app/Repositories/Eloquent/LocationRepository.php b/app/Repositories/Eloquent/LocationRepository.php index 0c04f39e..41e7811a 100644 --- a/app/Repositories/Eloquent/LocationRepository.php +++ b/app/Repositories/Eloquent/LocationRepository.php @@ -66,11 +66,19 @@ class LocationRepository extends SearchableRepository implements LocationReposit /** * {@inheritdoc} */ - public function allWithDetails() + public function getAllWithDetails() { return $this->getBuilder()->withCount('nodes', 'servers')->get($this->getColumns()); } + /** + * {@inheritdoc} + */ + public function getAllWithNodes() + { + return $this->getBuilder()->with('nodes')->get($this->getColumns()); + } + /** * {@inheritdoc} */ diff --git a/app/Repositories/Eloquent/ServiceOptionRepository.php b/app/Repositories/Eloquent/ServiceOptionRepository.php new file mode 100644 index 00000000..dc05fac0 --- /dev/null +++ b/app/Repositories/Eloquent/ServiceOptionRepository.php @@ -0,0 +1,39 @@ +. + * + * 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\Repositories\Eloquent; + +use Pterodactyl\Models\ServiceOption; +use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; + +class ServiceOptionRepository extends EloquentRepository implements ServiceOptionRepositoryInterface +{ + /** + * {@inheritdoc} + */ + public function model() + { + return ServiceOption::class; + } +} diff --git a/app/Repositories/Old/NodeRepository.php b/app/Repositories/Old/NodeRepository.php deleted file mode 100644 index 2d6fd3c9..00000000 --- a/app/Repositories/Old/NodeRepository.php +++ /dev/null @@ -1,291 +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\Repositories; - -use DB; -use Validator; -use IPTools\Network; -use Pterodactyl\Models; -use Pterodactyl\Services\UuidService; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Exceptions\DisplayValidationException; - -class NodeRepository -{ - /** - * Creates a new node on the system. - * - * @param array $data - * @return \Pterodactyl\Models\Node - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function create(array $data) - { - // Validate Fields - $validator = Validator::make($data, [ - 'name' => 'required|regex:/^([\w .-]{1,100})$/', - 'location_id' => 'required|numeric|min:1|exists:locations,id', - 'public' => 'required|numeric|between:0,1', - 'fqdn' => 'required|string|unique:nodes,fqdn', - 'scheme' => 'required|regex:/^(http(s)?)$/', - 'behind_proxy' => 'required|boolean', - 'memory' => 'required|numeric|min:1', - 'memory_overallocate' => 'required|numeric|min:-1', - 'disk' => 'required|numeric|min:1', - 'disk_overallocate' => 'required|numeric|min:-1', - 'daemonBase' => 'required|regex:/^([\/][\d\w.\-\/]+)$/', - 'daemonSFTP' => 'required|numeric|between:1,65535', - 'daemonListen' => 'required|numeric|between:1,65535', - ]); - - // Run validator, throw catchable and displayable exception if it fails. - // Exception includes a JSON result of failed validation rules. - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - // Verify the FQDN if using SSL - if (filter_var($data['fqdn'], FILTER_VALIDATE_IP) && $data['scheme'] === 'https') { - throw new DisplayException('A fully qualified domain name is required to use a secure comunication method on this node.'); - } - - // Verify FQDN is resolvable, or if not using SSL that the IP is valid. - if (! filter_var(gethostbyname($data['fqdn']), FILTER_VALIDATE_IP)) { - throw new DisplayException('The FQDN (or IP Address) provided does not resolve to a valid IP address.'); - } - - // Should we be nulling the overallocations? - $data['memory_overallocate'] = ($data['memory_overallocate'] < 0) ? null : $data['memory_overallocate']; - $data['disk_overallocate'] = ($data['disk_overallocate'] < 0) ? null : $data['disk_overallocate']; - - // Set the Secret - $uuid = new UuidService; - $data['daemonSecret'] = (string) $uuid->generate('nodes', 'daemonSecret'); - - return Models\Node::create($data); - } - - /** - * Updates a node on the system. - * - * @param int $id - * @param array $data - * @return \Pterodactyl\Models\Node - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function update($id, array $data) - { - $node = Models\Node::findOrFail($id); - - // Validate Fields - $validator = $validator = Validator::make($data, [ - 'name' => 'regex:/^([\w .-]{1,100})$/', - 'location_id' => 'numeric|min:1|exists:locations,id', - 'public' => 'numeric|between:0,1', - 'fqdn' => 'string|unique:nodes,fqdn,' . $id, - 'scheme' => 'regex:/^(http(s)?)$/', - 'behind_proxy' => 'boolean', - 'memory' => 'numeric|min:1', - 'memory_overallocate' => 'numeric|min:-1', - 'disk' => 'numeric|min:1', - 'disk_overallocate' => 'numeric|min:-1', - 'upload_size' => 'numeric|min:0', - 'daemonBase' => 'sometimes|regex:/^([\/][\d\w.\-\/]+)$/', - 'daemonSFTP' => 'numeric|between:1,65535', - 'daemonListen' => 'numeric|between:1,65535', - 'reset_secret' => 'sometimes|nullable|accepted', - ]); - - // Run validator, throw catchable and displayable exception if it fails. - // Exception includes a JSON result of failed validation rules. - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - // Verify the FQDN - if (isset($data['fqdn'])) { - - // Verify the FQDN if using SSL - if ((isset($data['scheme']) && $data['scheme'] === 'https') || (! isset($data['scheme']) && $node->scheme === 'https')) { - if (filter_var($data['fqdn'], FILTER_VALIDATE_IP)) { - throw new DisplayException('A fully qualified domain name is required to use secure comunication on this node.'); - } - } - - // Verify FQDN is resolvable, or if not using SSL that the IP is valid. - if (! filter_var(gethostbyname($data['fqdn']), FILTER_VALIDATE_IP)) { - throw new DisplayException('The FQDN (or IP Address) provided does not resolve to a valid IP address.'); - } - } - - // Should we be nulling the overallocations? - if (isset($data['memory_overallocate'])) { - $data['memory_overallocate'] = ($data['memory_overallocate'] < 0) ? null : $data['memory_overallocate']; - } - - if (isset($data['disk_overallocate'])) { - $data['disk_overallocate'] = ($data['disk_overallocate'] < 0) ? null : $data['disk_overallocate']; - } - - // Set the Secret - if (isset($data['reset_secret']) && ! is_null($data['reset_secret'])) { - $uuid = new UuidService; - $data['daemonSecret'] = (string) $uuid->generate('nodes', 'daemonSecret'); - unset($data['reset_secret']); - } - - $oldDaemonKey = $node->daemonSecret; - $node->update($data); - try { - $node->guzzleClient(['X-Access-Token' => $oldDaemonKey])->request('PATCH', '/config', [ - 'json' => [ - 'web' => [ - 'listen' => $node->daemonListen, - 'ssl' => [ - 'enabled' => (! $node->behind_proxy && $node->scheme === 'https'), - ], - ], - 'sftp' => [ - 'path' => $node->daemonBase, - 'port' => $node->daemonSFTP, - ], - 'remote' => [ - 'base' => config('app.url'), - ], - 'uploads' => [ - 'size_limit' => $node->upload_size, - ], - 'keys' => [ - $node->daemonSecret, - ], - ], - ]); - } catch (\Exception $ex) { - throw new DisplayException('Failed to update the node configuration, however your changes have been saved to the database. You will need to manually update the configuration file for the node to apply these changes.'); - } - } - - /** - * Adds allocations to a provided node. - * - * @param int $id - * @param array $data - * @return void - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function addAllocations($id, array $data) - { - $node = Models\Node::findOrFail($id); - - $validator = Validator::make($data, [ - 'allocation_ip' => 'required|string', - 'allocation_alias' => 'sometimes|required|string|max:255', - 'allocation_ports' => 'required|array', - ]); - - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - $explode = explode('/', $data['allocation_ip']); - if (count($explode) !== 1) { - if (! ctype_digit($explode[1]) || ($explode[1] > 32 || $explode[1] < 25)) { - throw new DisplayException('CIDR notation only allows masks between /32 and /25.'); - } - } - - DB::transaction(function () use ($data, $node) { - foreach (Network::parse(gethostbyname($data['allocation_ip'])) as $ip) { - foreach ($data['allocation_ports'] as $port) { - // Determine if this is a valid single port, or a valid port range. - if (! ctype_digit($port) && ! preg_match('/^(\d{1,5})-(\d{1,5})$/', $port)) { - throw new DisplayException('The mapping for ' . $port . ' is invalid and cannot be processed.'); - } - - if (preg_match('/^(\d{1,5})-(\d{1,5})$/', $port, $matches)) { - $block = range($matches[1], $matches[2]); - - if (count($block) > 1000) { - throw new DisplayException('Adding more than 1000 ports at once is not supported. Please use a smaller port range.'); - } - - foreach ($block as $unit) { - // Insert into Database - Models\Allocation::firstOrCreate([ - 'node_id' => $node->id, - 'ip' => $ip, - 'port' => $unit, - 'ip_alias' => isset($data['allocation_alias']) ? $data['allocation_alias'] : null, - 'server_id' => null, - ]); - } - } else { - // Insert into Database - Models\Allocation::firstOrCreate([ - 'node_id' => $node->id, - 'ip' => $ip, - 'port' => $port, - 'ip_alias' => isset($data['allocation_alias']) ? $data['allocation_alias'] : null, - 'server_id' => null, - ]); - } - } - } - }); - } - - /** - * Deletes a node on the system. - * - * @param int $id - * @return void - * - * @throws \Pterodactyl\Exceptions\DisplayException - */ - public function delete($id) - { - $node = Models\Node::withCount('servers')->findOrFail($id); - if ($node->servers_count > 0) { - throw new DisplayException('You cannot delete a node with servers currently attached to it.'); - } - - DB::transaction(function () use ($node) { - // Unlink Database Servers - Models\DatabaseHost::where('node_id', $node->id)->update(['node_id' => null]); - - // Delete Allocations - Models\Allocation::where('node_id', $node->id)->delete(); - - // Delete Node - $node->delete(); - }); - } -} diff --git a/app/Services/Services/Options/CreationService.php b/app/Services/Services/Options/CreationService.php new file mode 100644 index 00000000..dafc9f8f --- /dev/null +++ b/app/Services/Services/Options/CreationService.php @@ -0,0 +1,73 @@ +. + * + * 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\Services\Options; + +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; + +class CreationService +{ + /** + * @var \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface + */ + protected $repository; + + /** + * CreationService constructor. + * + * @param \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface $repository + */ + public function __construct(ServiceOptionRepositoryInterface $repository) + { + $this->repository = $repository; + } + + /** + * Create a new service option and assign it to the given service. + * + * @param array $data + * @return \Pterodactyl\Models\ServiceOption + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function handle(array $data) + { + if (! is_null(array_get($data, 'config_from'))) { + $results = $this->repository->findCountWhere([ + ['service_id', '=', array_get($data, 'service_id')], + ['id', '=', array_get($data, 'config_from')], + ]); + + if ($results !== 1) { + throw new DisplayException(trans('admin/exceptions.service.options.must_be_child')); + } + } else { + $data['config_from'] = null; + } + + return $this->repository->create($data); + } +} diff --git a/app/Services/Services/Variables/VariableCreationService.php b/app/Services/Services/Variables/VariableCreationService.php new file mode 100644 index 00000000..db8d9f24 --- /dev/null +++ b/app/Services/Services/Variables/VariableCreationService.php @@ -0,0 +1,52 @@ +. + * + * 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\Services\Variables; + +use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; + +class VariableCreationService +{ + /** + * @var \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface + */ + protected $serviceOptionRepository; + + public function __construct(ServiceOptionRepositoryInterface $serviceOptionRepository) + { + $this->serviceOptionRepository = $serviceOptionRepository; + } + + /** + * Create a new variable for a given service option. + * + * @param int $optionId + * @param array $data + * @return \Pterodactyl\Models\ServiceVariable + */ + public function handle($optionId, array $data) + { + $option = $this->serviceOptionRepository->find($optionId); + } +} diff --git a/resources/lang/en/admin/exceptions.php b/resources/lang/en/admin/exceptions.php index f2d9e33f..5699bd10 100644 --- a/resources/lang/en/admin/exceptions.php +++ b/resources/lang/en/admin/exceptions.php @@ -33,4 +33,9 @@ return [ 'invalid_mapping' => 'The mapping provided for :port was invalid and could not be processed.', 'cidr_out_of_range' => 'CIDR notation only allows masks between /25 and /32.', ], + 'service' => [ + 'options' => [ + 'must_be_child' => 'The "Configuration From" directive for this option must be a child option for the selected service.', + ], + ], ]; diff --git a/resources/lang/en/admin/services.php b/resources/lang/en/admin/services.php new file mode 100644 index 00000000..fcf59c14 --- /dev/null +++ b/resources/lang/en/admin/services.php @@ -0,0 +1,36 @@ +. + * + * 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. + */ + +return [ + 'options' => [ + 'notices' => [ + 'option_created' => 'New service option was created successfully. You will need to restart any running daemons to apply this new service.', + ], + ], + 'variables' => [ + 'notices' => [ + 'variable_created' => 'New variable has successfully been created and assigned to this service option.', + ], + ], +]; diff --git a/resources/themes/pterodactyl/admin/services/options/new.blade.php b/resources/themes/pterodactyl/admin/services/options/new.blade.php index b69f539b..03e2a05c 100644 --- a/resources/themes/pterodactyl/admin/services/options/new.blade.php +++ b/resources/themes/pterodactyl/admin/services/options/new.blade.php @@ -146,7 +146,7 @@ $('#pConfigFrom').select2(); }); $('#pServiceId').on('change', function (event) { - $('#pConfigFrom').html('').select2({ + $('#pConfigFrom').html('').select2({ data: $.map(_.get(Pterodactyl.services, $(this).val() + '.options', []), function (item) { return { id: item.id, diff --git a/routes/admin.php b/routes/admin.php index 31bc47a3..b205199a 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -176,11 +176,12 @@ Route::group(['prefix' => 'services'], function () { Route::post('/new', 'ServiceController@store'); Route::post('/view/{option}', 'ServiceController@edit'); Route::post('/option/new', 'OptionController@store'); - Route::post('/option/{option}', 'OptionController@editConfiguration'); Route::post('/option/{option}/scripts', 'OptionController@updateScripts'); Route::post('/option/{option}/variables', 'OptionController@createVariable'); Route::post('/option/{option}/variables/{variable}', 'OptionController@editVariable')->name('admin.services.option.variables.edit'); + Route::patch('/option/{option}', 'OptionController@editConfiguration'); + Route::delete('/view/{id}', 'ServiceController@delete'); }); From b8d7d9909650f9918f4029cf7d143e3e763c4fec Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 12 Aug 2017 15:29:01 -0500 Subject: [PATCH 47/99] More repository/service/refactor changes --- .../ServiceOptionRepositoryInterface.php | 25 +- .../ServiceVariableRepositoryInterface.php | 30 +++ app/Exceptions/Handler.php | 19 +- .../Repository/RecordNotFoundException.php | 4 +- .../RequiredVariableMissingException.php | 2 +- .../HasActiveServersException.php | 30 +++ .../InvalidCopyFromException.php | 30 +++ .../NoParentConfigurationFoundException.php | 30 +++ .../ReservedVariableNameException.php | 32 +++ .../Controllers/Admin/OptionController.php | 238 ++++++++--------- .../Controllers/Admin/VariableController.php | 147 +++++++++++ .../OptionVariableFormRequest.php | 7 +- .../ServiceOptionFormRequest.php | 9 +- app/Models/APIKey.php | 3 +- app/Models/APIPermission.php | 3 +- app/Models/Allocation.php | 3 +- app/Models/Database.php | 3 +- app/Models/DatabaseHost.php | 3 +- app/Models/Location.php | 3 +- app/Models/Node.php | 3 +- app/Models/Server.php | 3 +- app/Models/ServiceOption.php | 3 +- app/Models/ServiceVariable.php | 5 +- app/Models/User.php | 21 +- app/Providers/RepositoryServiceProvider.php | 3 + .../Eloquent/EloquentRepository.php | 22 +- .../Eloquent/ServiceOptionRepository.php | 27 ++ .../Eloquent/ServiceVariableRepository.php} | 22 +- app/Repositories/Old/OptionRepository.php | 241 ------------------ app/Repositories/Old/VariableRepository.php | 170 ------------ .../Options/InstallScriptUpdateService.php | 77 ++++++ ...nService.php => OptionCreationService.php} | 8 +- .../Options/OptionDeletionService.php | 77 ++++++ .../Services/Options/OptionUpdateService.php | 77 ++++++ .../Variables/VariableCreationService.php | 39 ++- .../Variables/VariableUpdateService.php | 88 +++++++ composer.json | 1 + composer.lock | 102 ++++---- resources/lang/en/admin/exceptions.php | 8 +- resources/lang/en/admin/services.php | 4 + .../services/options/variables.blade.php | 4 +- .../admin/services/options/view.blade.php | 4 +- .../pterodactyl/admin/services/view.blade.php | 4 +- routes/admin.php | 12 +- 44 files changed, 977 insertions(+), 669 deletions(-) create mode 100644 app/Contracts/Repository/ServiceVariableRepositoryInterface.php rename app/Exceptions/Services/{Servers => Server}/RequiredVariableMissingException.php (96%) create mode 100644 app/Exceptions/Services/ServiceOption/HasActiveServersException.php create mode 100644 app/Exceptions/Services/ServiceOption/InvalidCopyFromException.php create mode 100644 app/Exceptions/Services/ServiceOption/NoParentConfigurationFoundException.php create mode 100644 app/Exceptions/Services/ServiceVariable/ReservedVariableNameException.php create mode 100644 app/Http/Controllers/Admin/VariableController.php rename app/Http/Requests/Admin/{ => Service}/OptionVariableFormRequest.php (88%) rename app/Http/Requests/Admin/{ => Service}/ServiceOptionFormRequest.php (86%) rename app/{Http/Requests/Admin/Service/StoreOptionVariable.php => Repositories/Eloquent/ServiceVariableRepository.php} (65%) delete mode 100644 app/Repositories/Old/OptionRepository.php delete mode 100644 app/Repositories/Old/VariableRepository.php create mode 100644 app/Services/Services/Options/InstallScriptUpdateService.php rename app/Services/Services/Options/{CreationService.php => OptionCreationService.php} (87%) create mode 100644 app/Services/Services/Options/OptionDeletionService.php create mode 100644 app/Services/Services/Options/OptionUpdateService.php create mode 100644 app/Services/Services/Variables/VariableUpdateService.php diff --git a/app/Contracts/Repository/ServiceOptionRepositoryInterface.php b/app/Contracts/Repository/ServiceOptionRepositoryInterface.php index b50d66a7..e662c7b1 100644 --- a/app/Contracts/Repository/ServiceOptionRepositoryInterface.php +++ b/app/Contracts/Repository/ServiceOptionRepositoryInterface.php @@ -26,5 +26,28 @@ namespace Pterodactyl\Contracts\Repository; interface ServiceOptionRepositoryInterface extends RepositoryInterface { - // + /** + * Return a service option with the variables relation attached. + * + * @param int $id + * @return mixed + */ + public function getWithVariables($id); + + /** + * Return a service option with the copyFrom relation loaded onto the model. + * + * @param int $id + * @return mixed + */ + public function getWithCopyFrom($id); + + /** + * Confirm a copy script belongs to the same service as the item trying to use it. + * + * @param int $copyFromId + * @param int $service + * @return bool + */ + public function isCopiableScript($copyFromId, $service); } diff --git a/app/Contracts/Repository/ServiceVariableRepositoryInterface.php b/app/Contracts/Repository/ServiceVariableRepositoryInterface.php new file mode 100644 index 00000000..bcdc6196 --- /dev/null +++ b/app/Contracts/Repository/ServiceVariableRepositoryInterface.php @@ -0,0 +1,30 @@ +. + * + * 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\Contracts\Repository; + +interface ServiceVariableRepositoryInterface extends RepositoryInterface +{ + // +} diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index ed83b200..4fb28768 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -5,7 +5,12 @@ namespace Pterodactyl\Exceptions; use Exception; use Prologue\Alerts\Facades\Alert; use Illuminate\Auth\AuthenticationException; +use Illuminate\Session\TokenMismatchException; +use Illuminate\Validation\ValidationException; +use Illuminate\Auth\Access\AuthorizationException; +use Illuminate\Database\Eloquent\ModelNotFoundException; use Pterodactyl\Exceptions\Model\DataValidationException; +use Symfony\Component\HttpKernel\Exception\HttpException; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; class Handler extends ExceptionHandler @@ -16,15 +21,15 @@ class Handler extends ExceptionHandler * @var array */ protected $dontReport = [ - \Illuminate\Auth\AuthenticationException::class, - \Illuminate\Auth\Access\AuthorizationException::class, - \Symfony\Component\HttpKernel\Exception\HttpException::class, - \Illuminate\Database\Eloquent\ModelNotFoundException::class, - \Illuminate\Session\TokenMismatchException::class, - \Illuminate\Validation\ValidationException::class, + AuthenticationException::class, + AuthorizationException::class, DisplayException::class, - DisplayValidationException::class, DataValidationException::class, + DisplayValidationException::class, + HttpException::class, + ModelNotFoundException::class, + TokenMismatchException::class, + ValidationException::class, ]; /** diff --git a/app/Exceptions/Repository/RecordNotFoundException.php b/app/Exceptions/Repository/RecordNotFoundException.php index 932b83d1..1e9ac987 100644 --- a/app/Exceptions/Repository/RecordNotFoundException.php +++ b/app/Exceptions/Repository/RecordNotFoundException.php @@ -24,9 +24,7 @@ namespace Pterodactyl\Exceptions\Repository; -use Illuminate\Database\Eloquent\ModelNotFoundException; - -class RecordNotFoundException extends ModelNotFoundException +class RecordNotFoundException extends \Exception { // } diff --git a/app/Exceptions/Services/Servers/RequiredVariableMissingException.php b/app/Exceptions/Services/Server/RequiredVariableMissingException.php similarity index 96% rename from app/Exceptions/Services/Servers/RequiredVariableMissingException.php rename to app/Exceptions/Services/Server/RequiredVariableMissingException.php index f4a1a631..bf166a62 100644 --- a/app/Exceptions/Services/Servers/RequiredVariableMissingException.php +++ b/app/Exceptions/Services/Server/RequiredVariableMissingException.php @@ -22,7 +22,7 @@ * SOFTWARE. */ -namespace Pterodactyl\Exceptions\Services\Servers; +namespace Pterodactyl\Exceptions\Services\Server; use Exception; diff --git a/app/Exceptions/Services/ServiceOption/HasActiveServersException.php b/app/Exceptions/Services/ServiceOption/HasActiveServersException.php new file mode 100644 index 00000000..e1ea03b3 --- /dev/null +++ b/app/Exceptions/Services/ServiceOption/HasActiveServersException.php @@ -0,0 +1,30 @@ +. + * + * 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\Exceptions\Services\ServiceOption; + +class HasActiveServersException extends \Exception +{ + // +} diff --git a/app/Exceptions/Services/ServiceOption/InvalidCopyFromException.php b/app/Exceptions/Services/ServiceOption/InvalidCopyFromException.php new file mode 100644 index 00000000..34601313 --- /dev/null +++ b/app/Exceptions/Services/ServiceOption/InvalidCopyFromException.php @@ -0,0 +1,30 @@ +. + * + * 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\Exceptions\Services\ServiceOption; + +class InvalidCopyFromException extends \Exception +{ + // +} diff --git a/app/Exceptions/Services/ServiceOption/NoParentConfigurationFoundException.php b/app/Exceptions/Services/ServiceOption/NoParentConfigurationFoundException.php new file mode 100644 index 00000000..7d793504 --- /dev/null +++ b/app/Exceptions/Services/ServiceOption/NoParentConfigurationFoundException.php @@ -0,0 +1,30 @@ +. + * + * 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\Exceptions\Services\ServiceOption; + +class NoParentConfigurationFoundException extends \Exception +{ + // +} diff --git a/app/Exceptions/Services/ServiceVariable/ReservedVariableNameException.php b/app/Exceptions/Services/ServiceVariable/ReservedVariableNameException.php new file mode 100644 index 00000000..9777b0e9 --- /dev/null +++ b/app/Exceptions/Services/ServiceVariable/ReservedVariableNameException.php @@ -0,0 +1,32 @@ +. + * + * 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\Exceptions\Services\ServiceVariable; + +use Exception; + +class ReservedVariableNameException extends Exception +{ + // +} diff --git a/app/Http/Controllers/Admin/OptionController.php b/app/Http/Controllers/Admin/OptionController.php index 84954ef1..fa994d8b 100644 --- a/app/Http/Controllers/Admin/OptionController.php +++ b/app/Http/Controllers/Admin/OptionController.php @@ -24,26 +24,22 @@ namespace Pterodactyl\Http\Controllers\Admin; -use Log; -use Alert; -use Prologue\Alerts\AlertsMessageBag; -use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; -use Pterodactyl\Http\Requests\Admin\OptionVariableFormRequest; -use Pterodactyl\Http\Requests\Admin\ServiceOptionFormRequest; -use Pterodactyl\Services\Services\Options\CreationService; -use Pterodactyl\Services\Services\Variables\VariableCreationService; -use Route; use Javascript; use Illuminate\Http\Request; -use Pterodactyl\Models\Service; +use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Models\ServiceOption; -use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Repositories\OptionRepository; -use Pterodactyl\Repositories\VariableRepository; -use Pterodactyl\Exceptions\DisplayValidationException; use Pterodactyl\Http\Requests\Admin\Service\EditOptionScript; -use Pterodactyl\Http\Requests\Admin\Service\StoreOptionVariable; +use Pterodactyl\Services\Services\Options\OptionUpdateService; +use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; +use Pterodactyl\Services\Services\Options\OptionCreationService; +use Pterodactyl\Services\Services\Options\OptionDeletionService; +use Pterodactyl\Http\Requests\Admin\Service\ServiceOptionFormRequest; +use Pterodactyl\Services\Services\Options\InstallScriptUpdateService; +use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; +use Pterodactyl\Exceptions\Services\ServiceOption\InvalidCopyFromException; +use Pterodactyl\Exceptions\Services\ServiceOption\HasActiveServersException; +use Pterodactyl\Exceptions\Services\ServiceOption\NoParentConfigurationFoundException; class OptionController extends Controller { @@ -53,9 +49,24 @@ class OptionController extends Controller protected $alert; /** - * @var \Pterodactyl\Services\Services\Options\CreationService + * @var \Pterodactyl\Services\Services\Options\InstallScriptUpdateService */ - protected $creationService; + protected $installScriptUpdateService; + + /** + * @var \Pterodactyl\Services\Services\Options\OptionCreationService + */ + protected $optionCreationService; + + /** + * @var \Pterodactyl\Services\Services\Options\OptionDeletionService + */ + protected $optionDeletionService; + + /** + * @var \Pterodactyl\Services\Services\Options\OptionUpdateService + */ + protected $optionUpdateService; /** * @var \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface @@ -63,28 +74,37 @@ class OptionController extends Controller protected $serviceRepository; /** - * @var \Pterodactyl\Services\Services\Variables\VariableCreationService + * @var \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface */ - protected $variableCreationService; + protected $serviceOptionRepository; /** * OptionController constructor. * - * @param \Prologue\Alerts\AlertsMessageBag $alert - * @param \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface $serviceRepository - * @param \Pterodactyl\Services\Services\Options\CreationService $creationService - * @param \Pterodactyl\Services\Services\Variables\VariableCreationService $variableCreationService + * @param \Prologue\Alerts\AlertsMessageBag $alert + * @param \Pterodactyl\Services\Services\Options\InstallScriptUpdateService $installScriptUpdateService + * @param \Pterodactyl\Services\Services\Options\OptionCreationService $optionCreationService + * @param \Pterodactyl\Services\Services\Options\OptionDeletionService $optionDeletionService + * @param \Pterodactyl\Services\Services\Options\OptionUpdateService $optionUpdateService + * @param \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface $serviceRepository + * @param \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface $serviceOptionRepository */ public function __construct( AlertsMessageBag $alert, + InstallScriptUpdateService $installScriptUpdateService, + OptionCreationService $optionCreationService, + OptionDeletionService $optionDeletionService, + OptionUpdateService $optionUpdateService, ServiceRepositoryInterface $serviceRepository, - CreationService $creationService, - VariableCreationService $variableCreationService + ServiceOptionRepositoryInterface $serviceOptionRepository ) { $this->alert = $alert; - $this->creationService = $creationService; + $this->installScriptUpdateService = $installScriptUpdateService; + $this->optionCreationService = $optionCreationService; + $this->optionDeletionService = $optionDeletionService; + $this->optionUpdateService = $optionUpdateService; $this->serviceRepository = $serviceRepository; - $this->variableCreationService = $variableCreationService; + $this->serviceOptionRepository = $serviceOptionRepository; } /** @@ -103,77 +123,77 @@ class OptionController extends Controller /** * Handle adding a new service option. * - * @param \Pterodactyl\Http\Requests\Admin\ServiceOptionFormRequest $request + * @param \Pterodactyl\Http\Requests\Admin\Service\ServiceOptionFormRequest $request * @return \Illuminate\Http\RedirectResponse * - * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ public function store(ServiceOptionFormRequest $request) { - $option = $this->creationService->handle($request->normalize()); - $this->alert->success(trans('admin/services.options.notices.option_created'))->flash(); + try { + $option = $this->optionCreationService->handle($request->normalize()); + $this->alert->success(trans('admin/services.options.notices.option_created'))->flash(); + } catch (NoParentConfigurationFoundException $exception) { + $this->alert->danger($exception->getMessage())->flash(); + + return redirect()->back()->withInput(); + } return redirect()->route('admin.services.option.view', $option->id); } /** - * Handles POST request to create a new option variable. + * Delete a given option from the database. * - * @param \Pterodactyl\Http\Requests\Admin\OptionVariableFormRequest $request - * @param \Pterodactyl\Models\ServiceOption $option + * @param \Pterodactyl\Models\ServiceOption $option * @return \Illuminate\Http\RedirectResponse */ - public function createVariable(OptionVariableFormRequest $request, ServiceOption $option) + public function delete(ServiceOption $option) { - $this->variableCreationService->handle($option->id, $request->normalize()); - $this->alert->success(trans('admin/services.variables.notices.variable_created'))->flash(); + try { + $this->optionDeletionService->handle($option->id); + $this->alert->success()->flash(); + } catch (HasActiveServersException $exception) { + $this->alert->danger($exception->getMessage())->flash(); - return redirect()->route('admin.services.option.variables', $option->id); + return redirect()->route('admin.services.option.view', $option->id); + } + + return redirect()->route('admin.services.view', $option->service_id); } /** * Display option overview page. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Pterodactyl\Models\ServiceOption $option * @return \Illuminate\View\View */ - public function viewConfiguration(Request $request, $id) + public function viewConfiguration(ServiceOption $option) { - return view('admin.services.options.view', ['option' => ServiceOption::findOrFail($id)]); - } - - /** - * Display variable overview page for a service option. - * - * @param \Illuminate\Http\Request $request - * @param int $id - * @return \Illuminate\View\View - */ - public function viewVariables(Request $request, $id) - { - return view('admin.services.options.variables', ['option' => ServiceOption::with('variables') - ->findOrFail($id), ]); + return view('admin.services.options.view', ['option' => $option]); } /** * Display script management page for an option. * - * @param Request $request - * @param int $id + * @param int $option * @return \Illuminate\View\View + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function viewScripts(Request $request, $id) + public function viewScripts($option) { - $option = ServiceOption::with('copyFrom')->findOrFail($id); + $option = $this->serviceOptionRepository->getWithCopyFrom($option); + $copyOptions = $this->serviceOptionRepository->findWhere([ + ['copy_script_from', '=', null], + ['service_id', '=', $option->service_id], + ['id', '!=', $option], + ]); + $relyScript = $this->serviceOptionRepository->findWhere([['copy_script_from', '=', $option]]); return view('admin.services.options.scripts', [ - 'copyFromOptions' => ServiceOption::whereNull('copy_script_from')->where([ - ['service_id', $option->service_id], - ['id', '!=', $option->id], - ])->get(), - 'relyOnScript' => ServiceOption::where('copy_script_from', $option->id)->get(), + 'copyFromOptions' => $copyOptions, + 'relyOnScript' => $relyScript, 'option' => $option, ]); } @@ -181,92 +201,44 @@ class OptionController extends Controller /** * Handles POST when editing a configration for a service option. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Models\ServiceOption $option * @return \Illuminate\Http\RedirectResponse - */ - public function editConfiguration(Request $request, $id) - { - $repo = new OptionRepository; - - try { - if ($request->input('action') !== 'delete') { - $repo->update($id, $request->intersect([ - 'name', 'description', 'tag', 'docker_image', 'startup', - 'config_from', 'config_stop', 'config_logs', 'config_files', 'config_startup', - ])); - Alert::success('Service option configuration has been successfully updated.')->flash(); - } else { - $option = ServiceOption::with('service')->where('id', $id)->first(); - $repo->delete($id); - Alert::success('Successfully deleted service option from the system.')->flash(); - - return redirect()->route('admin.services.view', $option->service_id); - } - } catch (DisplayValidationException $ex) { - return redirect()->route('admin.services.option.view', $id)->withErrors(json_decode($ex->getMessage())); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An unhandled exception occurred while attempting to perform that action. This error has been logged.') - ->flash(); - } - - return redirect()->route('admin.services.option.view', $id); - } - - /** - * Handles POST when editing a configration for a service option. * - * @param \Pterodactyl\Http\Requests\Admin\Service\StoreOptionVariable $request - * @param int $option - * @param int $variable - * @return \Illuminate\Http\RedirectResponse + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function editVariable(StoreOptionVariable $request, $option, $variable) + public function editConfiguration(Request $request, ServiceOption $option) { - $repo = new VariableRepository; - try { - if ($request->input('action') !== 'delete') { - $variable = $repo->update($variable, $request->normalize()); - Alert::success("The service variable '{$variable->name}' has been updated.")->flash(); - } else { - $repo->delete($variable); - Alert::success('That service variable has been deleted.')->flash(); - } - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An unhandled exception was encountered while attempting to process that request. This error has been logged.') - ->flash(); + $this->optionUpdateService->handle($option, $request->all()); + $this->alert->success(trans('admin/services.options.notices.option_updated'))->flash(); + } catch (NoParentConfigurationFoundException $exception) { + dd('hodor'); + $this->alert->danger($exception->getMessage())->flash(); } - return redirect()->route('admin.services.option.variables', $option); + return redirect()->route('admin.services.option.view', $option->id); } /** * Handles POST when updating script for a service option. * - * @param \Pterodactyl\Http\Requests\Admin\Service\EditOptionScript $request + * @param \Pterodactyl\Http\Requests\Admin\Service\EditOptionScript $request + * @param \Pterodactyl\Models\ServiceOption $option * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function updateScripts(EditOptionScript $request) + public function updateScripts(EditOptionScript $request, ServiceOption $option) { try { - $this->repository->scripts($request->normalize()); - - Alert::success('Successfully updated option scripts to be run when servers are installed.')->flash(); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An unhandled exception was encountered while attempting to process that request. This error has been logged.') - ->flash(); + $this->installScriptUpdateService->handle($option, $request->normalize()); + $this->alert->success(trans('admin/services.options.notices.script_updated'))->flash(); + } catch (InvalidCopyFromException $exception) { + $this->alert->danger($exception->getMessage())->flash(); } - return redirect()->route('admin.services.option.scripts', $id); + return redirect()->route('admin.services.option.scripts', $option->id); } } diff --git a/app/Http/Controllers/Admin/VariableController.php b/app/Http/Controllers/Admin/VariableController.php new file mode 100644 index 00000000..3d2a4fa4 --- /dev/null +++ b/app/Http/Controllers/Admin/VariableController.php @@ -0,0 +1,147 @@ +. + * + * 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\Http\Controllers\Admin; + +use Prologue\Alerts\AlertsMessageBag; +use Pterodactyl\Http\Controllers\Controller; +use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; +use Pterodactyl\Http\Requests\Admin\OptionVariableFormRequest; +use Pterodactyl\Models\ServiceOption; +use Pterodactyl\Models\ServiceVariable; +use Pterodactyl\Repositories\Eloquent\ServiceVariableRepository; +use Pterodactyl\Services\Services\Variables\VariableCreationService; +use Pterodactyl\Services\Services\Variables\VariableUpdateService; + +class VariableController extends Controller +{ + /** + * @var \Prologue\Alerts\AlertsMessageBag + */ + protected $alert; + + /** + * @var \Pterodactyl\Services\Services\Variables\VariableCreationService + */ + protected $creationService; + + /** + * @var \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface + */ + protected $serviceOptionRepository; + + /** + * @var \Pterodactyl\Repositories\Eloquent\ServiceVariableRepository + */ + protected $serviceVariableRepository; + + /** + * @var \Pterodactyl\Services\Services\Variables\VariableUpdateService + */ + protected $updateService; + + public function __construct( + AlertsMessageBag $alert, + ServiceOptionRepositoryInterface $serviceOptionRepository, + ServiceVariableRepository $serviceVariableRepository, + VariableCreationService $creationService, + VariableUpdateService $updateService + ) { + $this->alert = $alert; + $this->creationService = $creationService; + $this->serviceOptionRepository = $serviceOptionRepository; + $this->serviceVariableRepository = $serviceVariableRepository; + $this->updateService = $updateService; + } + + /** + * Handles POST request to create a new option variable. + * + * @param \Pterodactyl\Http\Requests\Admin\OptionVariableFormRequest $request + * @param \Pterodactyl\Models\ServiceOption $option + * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Services\ServiceVariable\ReservedVariableNameException + */ + public function store(OptionVariableFormRequest $request, ServiceOption $option) + { + $this->creationService->handle($option->id, $request->normalize()); + $this->alert->success(trans('admin/services.variables.notices.variable_created'))->flash(); + + return redirect()->route('admin.services.option.variables', $option->id); + } + + /** + * Display variable overview page for a service option. + * + * @param int $option + * @return \Illuminate\View\View + */ + public function view($option) + { + $option = $this->serviceOptionRepository->getWithVariables($option); + + return view('admin.services.options.variables', ['option' => $option]); + } + + /** + * Handles POST when editing a configration for a service variable. + * + * @param \Pterodactyl\Http\Requests\Admin\OptionVariableFormRequest $request + * @param \Pterodactyl\Models\ServiceOption $option + * @param \Pterodactyl\Models\ServiceVariable $variable + * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Services\ServiceVariable\ReservedVariableNameException + */ + public function update(OptionVariableFormRequest $request, ServiceOption $option, ServiceVariable $variable) + { + $this->updateService->handle($variable, $request->normalize()); + $this->alert->success(trans('admin/services.variables.notices.variable_updated', [ + 'variable' => $variable->name, + ]))->flash(); + + return redirect()->route('admin.services.option.variables', $option->id); + } + + /** + * Delete a service variable from the system. + * + * @param \Pterodactyl\Models\ServiceOption $option + * @param \Pterodactyl\Models\ServiceVariable $variable + * @return \Illuminate\Http\RedirectResponse + */ + public function delete(ServiceOption $option, ServiceVariable $variable) + { + $this->serviceVariableRepository->delete($variable->id); + $this->alert->success(trans('admin/services.variables.notices.variable_deleted', [ + 'variable' => $variable->name, + ]))->flash(); + + return redirect()->route('admin.services.option.variables', $option->id); + } +} diff --git a/app/Http/Requests/Admin/OptionVariableFormRequest.php b/app/Http/Requests/Admin/Service/OptionVariableFormRequest.php similarity index 88% rename from app/Http/Requests/Admin/OptionVariableFormRequest.php rename to app/Http/Requests/Admin/Service/OptionVariableFormRequest.php index fe0a43e5..477e3353 100644 --- a/app/Http/Requests/Admin/OptionVariableFormRequest.php +++ b/app/Http/Requests/Admin/Service/OptionVariableFormRequest.php @@ -50,7 +50,12 @@ class OptionVariableFormRequest extends AdminFormRequest */ public function withValidator($validator) { - $validator->sometimes('default_value', $this->input('rules') ?? null, function ($input) { + $rules = $this->input('rules'); + if ($this->method() === 'PATCH') { + $rules = $this->input('rules', $this->route()->parameter('variable')->rules); + } + + $validator->sometimes('default_value', $rules, function ($input) { return $input->default_value; }); } diff --git a/app/Http/Requests/Admin/ServiceOptionFormRequest.php b/app/Http/Requests/Admin/Service/ServiceOptionFormRequest.php similarity index 86% rename from app/Http/Requests/Admin/ServiceOptionFormRequest.php rename to app/Http/Requests/Admin/Service/ServiceOptionFormRequest.php index 5986e6d7..8867a27f 100644 --- a/app/Http/Requests/Admin/ServiceOptionFormRequest.php +++ b/app/Http/Requests/Admin/Service/ServiceOptionFormRequest.php @@ -22,21 +22,18 @@ * SOFTWARE. */ -namespace Pterodactyl\Http\Requests\Admin; +namespace Pterodactyl\Http\Requests\Admin\Service; use Pterodactyl\Models\ServiceOption; +use Pterodactyl\Http\Requests\Admin\AdminFormRequest; class ServiceOptionFormRequest extends AdminFormRequest { /** - * @return array + * {@inheritdoc} */ public function rules() { - if ($this->method() === 'PATCH') { - return ServiceOption::getUpdateRulesForId($this->route()->parameter('option')->id); - } - return ServiceOption::getCreateRules(); } } diff --git a/app/Models/APIKey.php b/app/Models/APIKey.php index b62b6eb2..1df3f6aa 100644 --- a/app/Models/APIKey.php +++ b/app/Models/APIKey.php @@ -27,9 +27,10 @@ namespace Pterodactyl\Models; use Sofa\Eloquence\Eloquence; use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; +use Sofa\Eloquence\Contracts\CleansAttributes; use Sofa\Eloquence\Contracts\Validable as ValidableContract; -class APIKey extends Model implements ValidableContract +class APIKey extends Model implements CleansAttributes, ValidableContract { use Eloquence, Validable; diff --git a/app/Models/APIPermission.php b/app/Models/APIPermission.php index 626185fc..4072cd56 100644 --- a/app/Models/APIPermission.php +++ b/app/Models/APIPermission.php @@ -27,9 +27,10 @@ namespace Pterodactyl\Models; use Sofa\Eloquence\Eloquence; use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; +use Sofa\Eloquence\Contracts\CleansAttributes; use Sofa\Eloquence\Contracts\Validable as ValidableContract; -class APIPermission extends Model implements ValidableContract +class APIPermission extends Model implements CleansAttributes, ValidableContract { use Eloquence, Validable; diff --git a/app/Models/Allocation.php b/app/Models/Allocation.php index 65458be1..cbdba2e1 100644 --- a/app/Models/Allocation.php +++ b/app/Models/Allocation.php @@ -27,9 +27,10 @@ namespace Pterodactyl\Models; use Sofa\Eloquence\Eloquence; use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; +use Sofa\Eloquence\Contracts\CleansAttributes; use Sofa\Eloquence\Contracts\Validable as ValidableContract; -class Allocation extends Model implements ValidableContract +class Allocation extends Model implements CleansAttributes, ValidableContract { use Eloquence, Validable; diff --git a/app/Models/Database.php b/app/Models/Database.php index ee8be0c5..4453c790 100644 --- a/app/Models/Database.php +++ b/app/Models/Database.php @@ -27,9 +27,10 @@ namespace Pterodactyl\Models; use Sofa\Eloquence\Eloquence; use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; +use Sofa\Eloquence\Contracts\CleansAttributes; use Sofa\Eloquence\Contracts\Validable as ValidableContract; -class Database extends Model implements ValidableContract +class Database extends Model implements CleansAttributes, ValidableContract { use Eloquence, Validable; diff --git a/app/Models/DatabaseHost.php b/app/Models/DatabaseHost.php index d4ec484f..9533c0a5 100644 --- a/app/Models/DatabaseHost.php +++ b/app/Models/DatabaseHost.php @@ -27,9 +27,10 @@ namespace Pterodactyl\Models; use Sofa\Eloquence\Eloquence; use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; +use Sofa\Eloquence\Contracts\CleansAttributes; use Sofa\Eloquence\Contracts\Validable as ValidableContract; -class DatabaseHost extends Model implements ValidableContract +class DatabaseHost extends Model implements CleansAttributes, ValidableContract { use Eloquence, Validable; diff --git a/app/Models/Location.php b/app/Models/Location.php index 19322c6e..0337bcc3 100644 --- a/app/Models/Location.php +++ b/app/Models/Location.php @@ -27,9 +27,10 @@ namespace Pterodactyl\Models; use Sofa\Eloquence\Eloquence; use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; +use Sofa\Eloquence\Contracts\CleansAttributes; use Sofa\Eloquence\Contracts\Validable as ValidableContract; -class Location extends Model implements ValidableContract +class Location extends Model implements CleansAttributes, ValidableContract { use Eloquence, Validable; diff --git a/app/Models/Node.php b/app/Models/Node.php index 3f43a28a..138d29d8 100644 --- a/app/Models/Node.php +++ b/app/Models/Node.php @@ -29,9 +29,10 @@ use Sofa\Eloquence\Eloquence; use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; use Illuminate\Notifications\Notifiable; +use Sofa\Eloquence\Contracts\CleansAttributes; use Sofa\Eloquence\Contracts\Validable as ValidableContract; -class Node extends Model implements ValidableContract +class Node extends Model implements CleansAttributes, ValidableContract { use Eloquence, Notifiable, Validable; diff --git a/app/Models/Server.php b/app/Models/Server.php index 98871201..45091372 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -33,9 +33,10 @@ use Sofa\Eloquence\Eloquence; use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; use Illuminate\Notifications\Notifiable; +use Sofa\Eloquence\Contracts\CleansAttributes; use Sofa\Eloquence\Contracts\Validable as ValidableContract; -class Server extends Model implements ValidableContract +class Server extends Model implements CleansAttributes, ValidableContract { use Eloquence, Notifiable, Validable; diff --git a/app/Models/ServiceOption.php b/app/Models/ServiceOption.php index 9c0343bd..2b553f49 100644 --- a/app/Models/ServiceOption.php +++ b/app/Models/ServiceOption.php @@ -27,9 +27,10 @@ namespace Pterodactyl\Models; use Sofa\Eloquence\Eloquence; use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; +use Sofa\Eloquence\Contracts\CleansAttributes; use Sofa\Eloquence\Contracts\Validable as ValidableContract; -class ServiceOption extends Model implements ValidableContract +class ServiceOption extends Model implements CleansAttributes, ValidableContract { use Eloquence, Validable; diff --git a/app/Models/ServiceVariable.php b/app/Models/ServiceVariable.php index a7838bcd..a341165e 100644 --- a/app/Models/ServiceVariable.php +++ b/app/Models/ServiceVariable.php @@ -27,16 +27,17 @@ namespace Pterodactyl\Models; use Sofa\Eloquence\Eloquence; use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; +use Sofa\Eloquence\Contracts\CleansAttributes; use Sofa\Eloquence\Contracts\Validable as ValidableContract; -class ServiceVariable extends Model implements ValidableContract +class ServiceVariable extends Model implements CleansAttributes, ValidableContract { use Eloquence, Validable; /** * Reserved environment variable names. * - * @var array + * @var string */ const RESERVED_ENV_NAMES = 'SERVER_MEMORY,SERVER_IP,SERVER_PORT,ENV,HOME,USER,STARTUP,SERVER_UUID,UUID'; diff --git a/app/Models/User.php b/app/Models/User.php index f95a5242..972d0943 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -32,6 +32,7 @@ use Illuminate\Auth\Authenticatable; use Illuminate\Database\Eloquent\Model; use Illuminate\Notifications\Notifiable; use Pterodactyl\Exceptions\DisplayException; +use Sofa\Eloquence\Contracts\CleansAttributes; use Illuminate\Auth\Passwords\CanResetPassword; use Illuminate\Foundation\Auth\Access\Authorizable; use Sofa\Eloquence\Contracts\Validable as ValidableContract; @@ -40,7 +41,12 @@ use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract; use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract; use Pterodactyl\Notifications\SendPasswordReset as ResetPasswordNotification; -class User extends Model implements AuthenticatableContract, AuthorizableContract, CanResetPasswordContract, ValidableContract +class User extends Model implements + AuthenticatableContract, + AuthorizableContract, + CanResetPasswordContract, + CleansAttributes, + ValidableContract { use Authenticatable, Authorizable, CanResetPassword, Eloquence, Notifiable, Validable; @@ -125,6 +131,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac /** * Rules verifying that the data passed in forms is valid and meets application logic rules. + * * @var array */ protected static $applicationRules = [ @@ -155,7 +162,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac /** * Enables or disables TOTP on an account if the token is valid. * - * @param int $token + * @param int $token * @return bool * @deprecated */ @@ -196,7 +203,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac /** * Send the password reset notification. * - * @param string $token + * @param string $token * @return void */ public function sendPasswordResetNotification($token) @@ -218,7 +225,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac /** * Returns the user's daemon secret for a given server. * - * @param \Pterodactyl\Models\Server $server + * @param \Pterodactyl\Models\Server $server * @return null|string */ public function daemonToken(Server $server) @@ -248,7 +255,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac /** * Change the access level for a given call to `access()` on the user. * - * @param string $level can be all, admin, subuser, owner + * @param string $level can be all, admin, subuser, owner * @return $this */ public function setAccessLevel($level = 'all') @@ -265,7 +272,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac * Returns an array of all servers a user is able to access. * Note: does not account for user admin status. * - * @param array $load + * @param array $load * @return \Pterodactyl\Models\Server */ public function access(...$load) @@ -303,7 +310,7 @@ class User extends Model implements AuthenticatableContract, AuthorizableContrac /** * Store the username as a lowecase string. * - * @param string $value + * @param string $value */ public function setUsernameAttribute($value) { diff --git a/app/Providers/RepositoryServiceProvider.php b/app/Providers/RepositoryServiceProvider.php index 5206501d..8a98e662 100644 --- a/app/Providers/RepositoryServiceProvider.php +++ b/app/Providers/RepositoryServiceProvider.php @@ -25,7 +25,9 @@ namespace Pterodactyl\Providers; use Illuminate\Support\ServiceProvider; +use Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface; use Pterodactyl\Repositories\Eloquent\NodeRepository; +use Pterodactyl\Repositories\Eloquent\ServiceVariableRepository; use Pterodactyl\Repositories\Eloquent\UserRepository; use Pterodactyl\Repositories\Eloquent\ApiKeyRepository; use Pterodactyl\Repositories\Eloquent\ServerRepository; @@ -76,6 +78,7 @@ class RepositoryServiceProvider extends ServiceProvider $this->app->bind(ServerVariableRepositoryInterface::class, ServerVariableRepository::class); $this->app->bind(ServiceRepositoryInterface::class, ServiceRepository::class); $this->app->bind(ServiceOptionRepositoryInterface::class, ServiceOptionRepository::class); + $this->app->bind(ServiceVariableRepositoryInterface::class, ServiceVariableRepository::class); $this->app->bind(UserRepositoryInterface::class, UserRepository::class); // Daemon Repositories diff --git a/app/Repositories/Eloquent/EloquentRepository.php b/app/Repositories/Eloquent/EloquentRepository.php index 77108b59..0dd1beee 100644 --- a/app/Repositories/Eloquent/EloquentRepository.php +++ b/app/Repositories/Eloquent/EloquentRepository.php @@ -24,8 +24,9 @@ namespace Pterodactyl\Repositories\Eloquent; -use Illuminate\Database\Query\Expression; +use Webmozart\Assert\Assert; use Pterodactyl\Repository\Repository; +use Illuminate\Database\Query\Expression; use Pterodactyl\Contracts\Repository\RepositoryInterface; use Pterodactyl\Exceptions\Model\DataValidationException; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; @@ -48,6 +49,9 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf */ public function create(array $fields, $validate = true, $force = false) { + Assert::boolean($validate, 'Second argument passed to create should be boolean, recieved %s.'); + Assert::boolean($force, 'Third argument passed to create should be boolean, received %s.'); + $instance = $this->getBuilder()->newModelInstance(); if ($force) { @@ -73,6 +77,8 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf */ public function find($id) { + Assert::integer($id, 'First argument passed to find should be integer, received %s.'); + $instance = $this->getBuilder()->find($id, $this->getColumns()); if (! $instance) { @@ -119,6 +125,9 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf */ public function delete($id, $destroy = false) { + Assert::integer($id, 'First argument passed to delete should be integer, received %s.'); + Assert::boolean($destroy, 'Second argument passed to delete should be boolean, received %s.'); + $instance = $this->getBuilder()->where($this->getModel()->getKeyName(), $id); return ($destroy) ? $instance->forceDelete() : $instance->delete(); @@ -129,6 +138,8 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf */ public function deleteWhere(array $attributes, $force = false) { + Assert::boolean($force, 'Second argument passed to deleteWhere should be boolean, received %s.'); + $instance = $this->getBuilder()->where($attributes); return ($force) ? $instance->forceDelete() : $instance->delete(); @@ -139,6 +150,10 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf */ public function update($id, array $fields, $validate = true, $force = false) { + Assert::integer($id, 'First argument passed to update expected to be integer, received %s.'); + Assert::boolean($validate, 'Third argument passed to update should be boolean, received %s.'); + Assert::boolean($force, 'Fourth argument passed to update should be boolean, received %s.'); + $instance = $this->getBuilder()->where('id', $id)->first(); if (! $instance) { @@ -167,6 +182,8 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf */ public function updateWhereIn($column, array $values, array $fields) { + Assert::stringNotEmpty($column, 'First argument passed to updateWhereIn expected to be a string, received %s.'); + return $this->getBuilder()->whereIn($column, $values)->update($fields); } @@ -238,6 +255,9 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf */ public function updateOrCreate(array $where, array $fields, $validate = true, $force = false) { + Assert::boolean($validate, 'Third argument passed to updateOrCreate should be boolean, received %s.'); + Assert::boolean($force, 'Fourth argument passed to updateOrCreate should be boolean, received %s.'); + $instance = $this->withColumns('id')->findWhere($where)->first(); if (! $instance) { diff --git a/app/Repositories/Eloquent/ServiceOptionRepository.php b/app/Repositories/Eloquent/ServiceOptionRepository.php index dc05fac0..14869dfb 100644 --- a/app/Repositories/Eloquent/ServiceOptionRepository.php +++ b/app/Repositories/Eloquent/ServiceOptionRepository.php @@ -36,4 +36,31 @@ class ServiceOptionRepository extends EloquentRepository implements ServiceOptio { return ServiceOption::class; } + + /** + * {@inheritdoc} + */ + public function getWithVariables($id) + { + return $this->getBuilder()->with('variables')->find($id, $this->getColumns()); + } + + /** + * {@inheritdoc} + */ + public function getWithCopyFrom($id) + { + return $this->getBuilder()->with('copyFrom')->find($id, $this->getColumns()); + } + + /** + * {@inheritdoc} + */ + public function isCopiableScript($copyFromId, $service) + { + return $this->getBuilder()->whereNull('copy_script_from') + ->where('id', '=', $copyFromId) + ->where('service_id', '=', $service) + ->exists(); + } } diff --git a/app/Http/Requests/Admin/Service/StoreOptionVariable.php b/app/Repositories/Eloquent/ServiceVariableRepository.php similarity index 65% rename from app/Http/Requests/Admin/Service/StoreOptionVariable.php rename to app/Repositories/Eloquent/ServiceVariableRepository.php index 869b2efc..bf780582 100644 --- a/app/Http/Requests/Admin/Service/StoreOptionVariable.php +++ b/app/Repositories/Eloquent/ServiceVariableRepository.php @@ -22,26 +22,18 @@ * SOFTWARE. */ -namespace Pterodactyl\Http\Requests\Admin\Service; +namespace Pterodactyl\Repositories\Eloquent; -use Pterodactyl\Http\Requests\Admin\AdminFormRequest; +use Pterodactyl\Models\ServiceVariable; +use Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface; -class StoreOptionVariable extends AdminFormRequest +class ServiceVariableRepository extends EloquentRepository implements ServiceVariableRepositoryInterface { /** - * Set the rules to be used for data passed to the request. - * - * @return array + * {@inheritdoc} */ - public function rules() + public function model() { - return [ - 'name' => 'required|string|min:1|max:255', - 'description' => 'nullable|string', - 'env_variable' => 'required|regex:/^[\w]{1,255}$/', - 'rules' => 'bail|required|string', - 'default_value' => explode('|', $this->input('rules')), - 'options' => 'sometimes|required|array', - ]; + return ServiceVariable::class; } } diff --git a/app/Repositories/Old/OptionRepository.php b/app/Repositories/Old/OptionRepository.php deleted file mode 100644 index 6e99583c..00000000 --- a/app/Repositories/Old/OptionRepository.php +++ /dev/null @@ -1,241 +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\Repositories; - -use DB; -use Validator; -use InvalidArgumentException; -use Pterodactyl\Models\ServiceOption; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Exceptions\DisplayValidationException; - -class OptionRepository -{ - /** - * Store the requested service option. - * - * @var \Pterodactyl\Models\ServiceOption - */ - protected $model; - - /** - * OptionRepository constructor. - * - * @param null|int|\Pterodactyl\Models\ServiceOption $option - */ - public function __construct($option = null) - { - if (is_null($option)) { - return; - } - - if ($option instanceof ServiceOption) { - $this->model = $option; - } else { - if (! is_numeric($option)) { - throw new InvalidArgumentException( - sprintf('Variable passed to constructor must be integer or instance of \Pterodactyl\Models\ServiceOption.') - ); - } - - $this->model = ServiceOption::findOrFail($option); - } - } - - /** - * Return the eloquent model for the given repository. - * - * @return null|\Pterodactyl\Models\ServiceOption - */ - public function getModel() - { - return $this->model; - } - - /** - * Update the currently assigned model by re-initalizing the class. - * - * @param null|int|\Pterodactyl\Models\ServiceOption $option - * @return $this - */ - public function setModel($option) - { - self::__construct($option); - - return $this; - } - - /** - * Creates a new service option on the system. - * - * @param array $data - * @return \Pterodactyl\Models\ServiceOption - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function create(array $data) - { - $validator = Validator::make($data, [ - 'service_id' => 'required|numeric|exists:services,id', - 'name' => 'required|string|max:255', - 'description' => 'required|string', - 'tag' => 'required|alpha_num|max:60|unique:service_options,tag', - 'docker_image' => 'sometimes|string|max:255', - 'startup' => 'sometimes|nullable|string', - 'config_from' => 'sometimes|required|numeric|exists:service_options,id', - 'config_startup' => 'required_without:config_from|json', - 'config_stop' => 'required_without:config_from|string|max:255', - 'config_logs' => 'required_without:config_from|json', - 'config_files' => 'required_without:config_from|json', - ]); - - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - if (isset($data['config_from'])) { - if (! ServiceOption::where('service_id', $data['service_id'])->where('id', $data['config_from'])->first()) { - throw new DisplayException('The `configuration from` directive must be a child of the assigned service.'); - } - } - - return $this->setModel(ServiceOption::create($data))->getModel(); - } - - /** - * Deletes a service option from the system. - * - * @param int $id - * @return void - * - * @throws \Exception - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Throwable - */ - public function delete($id) - { - $this->model->load('variables', 'servers'); - - if ($this->model->servers->count() > 0) { - throw new DisplayException('You cannot delete a service option that has servers associated with it.'); - } - - DB::transaction(function () use ($option) { - foreach ($option->variables as $variable) { - (new VariableRepository)->delete($variable->id); - } - - $option->delete(); - }); - } - - /** - * Updates a service option in the database which can then be used - * on nodes. - * - * @param int $id - * @param array $data - * @return \Pterodactyl\Models\ServiceOption - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function update($id, array $data) - { - $option = ServiceOption::findOrFail($id); - - // Due to code limitations (at least when I am writing this currently) - // we have to make an assumption that if config_from is not passed - // that we should be telling it that no config is wanted anymore. - // - // This really is only an issue if we open API access to this function, - // in which case users will always need to pass `config_from` in order - // to keep it assigned. - if (! isset($data['config_from']) && ! is_null($option->config_from)) { - $option->config_from = null; - } - - $validator = Validator::make($data, [ - 'name' => 'sometimes|required|string|max:255', - 'description' => 'sometimes|required|string', - 'tag' => 'sometimes|required|string|max:255|unique:service_options,tag,' . $option->id, - 'docker_image' => 'sometimes|required|string|max:255', - 'startup' => 'sometimes|required|string', - 'config_from' => 'sometimes|required|numeric|exists:service_options,id', - ]); - - $validator->sometimes([ - 'config_startup', 'config_logs', 'config_files', - ], 'required_without:config_from|json', function ($input) use ($option) { - return ! (! $input->config_from && ! is_null($option->config_from)); - }); - - $validator->sometimes('config_stop', 'required_without:config_from|string|max:255', function ($input) use ($option) { - return ! (! $input->config_from && ! is_null($option->config_from)); - }); - - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - if (isset($data['config_from'])) { - if (! ServiceOption::where('service_id', $option->service_id)->where('id', $data['config_from'])->first()) { - throw new DisplayException('The `configuration from` directive must be a child of the assigned service.'); - } - } - - $option->fill($data)->save(); - - return $option; - } - - /** - * Updates a service option's scripts in the database. - * - * @param array $data - * - * @throws \Pterodactyl\Exceptions\DisplayException - */ - public function scripts(array $data) - { - $data['script_install'] = empty($data['script_install']) ? null : $data['script_install']; - - if (isset($data['copy_script_from']) && ! empty($data['copy_script_from'])) { - $select = ServiceOption::whereNull('copy_script_from') - ->where('id', $data['copy_script_from']) - ->where('service_id', $this->model->service_id) - ->first(); - - if (! $select) { - throw new DisplayException('The service option selected to copy a script from either does not exist, or is copying from a higher level.'); - } - } else { - $data['copy_script_from'] = null; - } - - $this->model->fill($data)->save(); - } -} diff --git a/app/Repositories/Old/VariableRepository.php b/app/Repositories/Old/VariableRepository.php deleted file mode 100644 index 1aded829..00000000 --- a/app/Repositories/Old/VariableRepository.php +++ /dev/null @@ -1,170 +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\Repositories; - -use DB; -use Validator; -use Pterodactyl\Models\ServiceOption; -use Pterodactyl\Models\ServiceVariable; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Exceptions\DisplayValidationException; - -class VariableRepository -{ - /** - * Create a new service variable. - * - * @param int $option - * @param array $data - * @return \Pterodactyl\Models\ServiceVariable - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function create($option, array $data) - { - $option = ServiceOption::select('id')->findOrFail($option); - - $validator = Validator::make($data, [ - 'name' => 'required|string|min:1|max:255', - 'description' => 'sometimes|nullable|string', - 'env_variable' => 'required|regex:/^[\w]{1,255}$/', - 'default_value' => 'string', - 'options' => 'sometimes|required|array', - 'rules' => 'bail|required|string', - ]); - - // Ensure the default value is allowed by the rules provided. - $validator->sometimes('default_value', $data['rules'] ?? null, function ($input) { - return $input->default_value; - }); - - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - if (in_array($data['env_variable'], ServiceVariable::reservedNames())) { - throw new DisplayException('The environment variable name provided is a reserved keyword for the daemon.'); - } - - $search = ServiceVariable::where('env_variable', $data['env_variable'])->where('option_id', $option->id); - if ($search->first()) { - throw new DisplayException('The envionment variable name assigned to this variable must be unique for this service option.'); - } - - if (! isset($data['options']) || ! is_array($data['options'])) { - $data['options'] = []; - } - - $data['option_id'] = $option->id; - $data['user_viewable'] = (in_array('user_viewable', $data['options'])); - $data['user_editable'] = (in_array('user_editable', $data['options'])); - - // Remove field that isn't used. - unset($data['options']); - - return ServiceVariable::create($data); - } - - /** - * Deletes a specified option variable as well as all server - * variables currently assigned. - * - * @param int $id - * @return void - */ - public function delete($id) - { - $variable = ServiceVariable::with('serverVariable')->findOrFail($id); - - DB::transaction(function () use ($variable) { - foreach ($variable->serverVariable as $v) { - $v->delete(); - } - - $variable->delete(); - }); - } - - /** - * Updates a given service variable. - * - * @param int $id - * @param array $data - * @return \Pterodactyl\Models\ServiceVariable - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function update($id, array $data) - { - $variable = ServiceVariable::findOrFail($id); - - $validator = Validator::make($data, [ - 'name' => 'required|string|min:1|max:255', - 'description' => 'nullable|string', - 'env_variable' => 'required|regex:/^[\w]{1,255}$/', - 'rules' => 'bail|required|string', - 'options' => 'sometimes|required|array', - ]); - - // Ensure the default value is allowed by the rules provided. - $rules = (isset($data['rules'])) ? $data['rules'] : $variable->rules; - $validator->sometimes('default_value', $rules, function ($input) { - return $input->default_value; - }); - - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - if (isset($data['env_variable'])) { - if (in_array($data['env_variable'], ServiceVariable::reservedNames())) { - throw new DisplayException('The environment variable name provided is a reserved keyword for the daemon.'); - } - - $search = ServiceVariable::where('env_variable', $data['env_variable']) - ->where('option_id', $variable->option_id) - ->where('id', '!=', $variable->id); - if ($search->first()) { - throw new DisplayException('The envionment variable name assigned to this variable must be unique for this service option.'); - } - } - - if (! isset($data['options']) || ! is_array($data['options'])) { - $data['options'] = []; - } - - $data['user_viewable'] = (in_array('user_viewable', $data['options'])); - $data['user_editable'] = (in_array('user_editable', $data['options'])); - - // Remove field that isn't used. - unset($data['options']); - - $variable->fill($data)->save(); - - return $variable; - } -} diff --git a/app/Services/Services/Options/InstallScriptUpdateService.php b/app/Services/Services/Options/InstallScriptUpdateService.php new file mode 100644 index 00000000..f2f13559 --- /dev/null +++ b/app/Services/Services/Options/InstallScriptUpdateService.php @@ -0,0 +1,77 @@ +. + * + * 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\Services\Options; + +use Pterodactyl\Models\ServiceOption; +use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; +use Pterodactyl\Exceptions\Services\ServiceOption\InvalidCopyFromException; + +class InstallScriptUpdateService +{ + /** + * @var \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface + */ + protected $serviceOptionRepository; + + /** + * InstallScriptUpdateService constructor. + * + * @param \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface $serviceOptionRepository + */ + public function __construct(ServiceOptionRepositoryInterface $serviceOptionRepository) + { + $this->serviceOptionRepository = $serviceOptionRepository; + } + + /** + * Modify the option install script for a given service option. + * + * @param int|\Pterodactyl\Models\ServiceOption $option + * @param array $data + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Services\ServiceOption\InvalidCopyFromException + */ + public function handle($option, array $data) + { + if (! $option instanceof ServiceOption) { + $option = $this->serviceOptionRepository->find($option); + } + + if (! is_null(array_get($data, 'copy_script_from'))) { + if (! $this->serviceOptionRepository->isCopiableScript(array_get($data, 'copy_script_from'), $option->service_id)) { + throw new InvalidCopyFromException(trans('admin/exceptions.service.options.invalid_copy_id')); + } + } + + $this->serviceOptionRepository->withoutFresh()->update($option->id, [ + 'script_install' => array_get($data, 'script_install'), + 'script_is_privileged' => array_get($data, 'script_is_privileged'), + 'script_entry' => array_get($data, 'script_entry'), + 'script_container' => array_get($data, 'script_container'), + 'copy_script_from' => array_get($data, 'copy_script_from'), + ]); + } +} diff --git a/app/Services/Services/Options/CreationService.php b/app/Services/Services/Options/OptionCreationService.php similarity index 87% rename from app/Services/Services/Options/CreationService.php rename to app/Services/Services/Options/OptionCreationService.php index dafc9f8f..83cda6b1 100644 --- a/app/Services/Services/Options/CreationService.php +++ b/app/Services/Services/Options/OptionCreationService.php @@ -24,10 +24,10 @@ namespace Pterodactyl\Services\Services\Options; -use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; +use Pterodactyl\Exceptions\Services\ServiceOption\NoParentConfigurationFoundException; -class CreationService +class OptionCreationService { /** * @var \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface @@ -50,8 +50,8 @@ class CreationService * @param array $data * @return \Pterodactyl\Models\ServiceOption * - * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Services\ServiceOption\NoParentConfigurationFoundException */ public function handle(array $data) { @@ -62,7 +62,7 @@ class CreationService ]); if ($results !== 1) { - throw new DisplayException(trans('admin/exceptions.service.options.must_be_child')); + throw new NoParentConfigurationFoundException(trans('admin/exceptions.service.options.must_be_child')); } } else { $data['config_from'] = null; diff --git a/app/Services/Services/Options/OptionDeletionService.php b/app/Services/Services/Options/OptionDeletionService.php new file mode 100644 index 00000000..aa392f35 --- /dev/null +++ b/app/Services/Services/Options/OptionDeletionService.php @@ -0,0 +1,77 @@ +. + * + * 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\Services\Options; + +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; +use Pterodactyl\Exceptions\Services\ServiceOption\HasActiveServersException; + +class OptionDeletionService +{ + /** + * @var \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $serverRepository; + + /** + * OptionDeletionService constructor. + * + * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $serverRepository + * @param \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface $repository + */ + public function __construct( + ServerRepositoryInterface $serverRepository, + ServiceOptionRepositoryInterface $repository + ) { + $this->repository = $repository; + $this->serverRepository = $serverRepository; + } + + /** + * Delete an option from the database if it has no active servers attached to it. + * + * @param int $option + * @return int + * + * @throws \Pterodactyl\Exceptions\Services\ServiceOption\HasActiveServersException + */ + public function handle($option) + { + $servers = $this->serverRepository->findCountWhere([ + ['option_id', '=', $option], + ]); + + if ($servers > 0) { + throw new HasActiveServersException(trans('admin/exceptions.service.options.delete_has_servers')); + } + + return $this->repository->delete($option); + } +} diff --git a/app/Services/Services/Options/OptionUpdateService.php b/app/Services/Services/Options/OptionUpdateService.php new file mode 100644 index 00000000..4e922b21 --- /dev/null +++ b/app/Services/Services/Options/OptionUpdateService.php @@ -0,0 +1,77 @@ +. + * + * 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\Services\Options; + +use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; +use Pterodactyl\Exceptions\Services\ServiceOption\NoParentConfigurationFoundException; +use Pterodactyl\Models\ServiceOption; + +class OptionUpdateService +{ + /** + * @var \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface + */ + protected $repository; + + /** + * OptionUpdateService constructor. + * + * @param \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface $repository + */ + public function __construct(ServiceOptionRepositoryInterface $repository) + { + $this->repository = $repository; + } + + /** + * Update a service option. + * + * @param int|\Pterodactyl\Models\ServiceOption $option + * @param array $data + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Pterodactyl\Exceptions\Services\ServiceOption\NoParentConfigurationFoundException + */ + public function handle($option, array $data) + { + if (! $option instanceof ServiceOption) { + $option = $this->repository->find($option); + } + + if (! is_null(array_get($data, 'config_from'))) { + $results = $this->repository->findCountWhere([ + ['service_id', '=', $option->service_id], + ['id', '=', array_get($data, 'config_from')], + ]); + + if ($results !== 1) { + throw new NoParentConfigurationFoundException(trans('admin/exceptions.service.options.must_be_child')); + } + } + + $this->repository->withoutFresh()->update($option->id, $data); + } +} diff --git a/app/Services/Services/Variables/VariableCreationService.php b/app/Services/Services/Variables/VariableCreationService.php index db8d9f24..a079baab 100644 --- a/app/Services/Services/Variables/VariableCreationService.php +++ b/app/Services/Services/Variables/VariableCreationService.php @@ -25,6 +25,10 @@ namespace Pterodactyl\Services\Services\Variables; use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface; +use Pterodactyl\Exceptions\Services\ServiceVariable\ReservedVariableNameException; +use Pterodactyl\Models\ServiceOption; +use Pterodactyl\Models\ServiceVariable; class VariableCreationService { @@ -33,20 +37,45 @@ class VariableCreationService */ protected $serviceOptionRepository; - public function __construct(ServiceOptionRepositoryInterface $serviceOptionRepository) - { + /** + * @var \Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface + */ + protected $serviceVariableRepository; + + public function __construct( + ServiceOptionRepositoryInterface $serviceOptionRepository, + ServiceVariableRepositoryInterface $serviceVariableRepository + ) { $this->serviceOptionRepository = $serviceOptionRepository; + $this->serviceVariableRepository = $serviceVariableRepository; } /** * Create a new variable for a given service option. * - * @param int $optionId + * @param int $option * @param array $data * @return \Pterodactyl\Models\ServiceVariable + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Services\ServiceVariable\ReservedVariableNameException */ - public function handle($optionId, array $data) + public function handle($option, array $data) { - $option = $this->serviceOptionRepository->find($optionId); + if ($option instanceof ServiceOption) { + $option = $option->id; + } + + if (in_array(strtoupper(array_get($data, 'env_variable')), explode(',', ServiceVariable::RESERVED_ENV_NAMES))) { + throw new ReservedVariableNameException(sprintf('Cannot use the protected name %s for this environment variable.')); + } + + $options = array_get($data, 'options', []); + + return $this->serviceVariableRepository->create(array_merge([ + 'option_id' => $option, + 'user_viewable' => in_array('user_viewable', $options), + 'user_editable' => in_array('user_editable', $options), + ], $data)); } } diff --git a/app/Services/Services/Variables/VariableUpdateService.php b/app/Services/Services/Variables/VariableUpdateService.php new file mode 100644 index 00000000..c3fb8693 --- /dev/null +++ b/app/Services/Services/Variables/VariableUpdateService.php @@ -0,0 +1,88 @@ +. + * + * 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\Services\Variables; + +use Pterodactyl\Models\ServiceVariable; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface; +use Pterodactyl\Exceptions\Services\ServiceVariable\ReservedVariableNameException; + +class VariableUpdateService +{ + /** + * @var \Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface + */ + protected $serviceVariableRepository; + + public function __construct(ServiceVariableRepositoryInterface $serviceVariableRepository) + { + $this->serviceVariableRepository = $serviceVariableRepository; + } + + /** + * Update a specific service variable. + * + * @param int|\Pterodactyl\Models\ServiceVariable $variable + * @param array $data + * @return \Pterodactyl\Models\ServiceVariable + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Services\ServiceVariable\ReservedVariableNameException + */ + public function handle($variable, array $data) + { + if (! $variable instanceof ServiceVariable) { + $variable = $this->serviceVariableRepository->find($variable); + } + + if (! is_null(array_get($data, 'env_variable'))) { + if (in_array(strtoupper(array_get($data, 'env_variable')), explode(',', ServiceVariable::RESERVED_ENV_NAMES))) { + throw new ReservedVariableNameException(trans('admin/exceptions.service.variables.reserved_name', [ + 'name' => array_get($data, 'env_variable'), + ])); + } + + $search = $this->serviceVariableRepository->withColumns('id')->findCountWhere([ + ['env_variable', '=', array_get($data, 'env_variable')], + ['option_id', '=', $variable->option_id], + ['id', '!=', $variable->id], + ]); + + if ($search > 0) { + throw new DisplayException(trans('admin/exceptions.service.variables.env_not_unique', [ + 'name' => array_get($data, 'env_variable'), + ])); + } + } + + $options = array_get($data, 'options', []); + + return $this->serviceVariableRepository->update($variable->id, array_merge([ + 'user_viewable' => in_array('user_viewable', $options, $variable->user_viewable), + 'user_editable' => in_array('user_editable', $options, $variable->user_editable), + ], $data)); + } +} diff --git a/composer.json b/composer.json index a39f963c..59e683ec 100644 --- a/composer.json +++ b/composer.json @@ -38,6 +38,7 @@ "sofa/eloquence": "5.4.1", "spatie/laravel-fractal": "4.0.1", "watson/validating": "3.0.1", + "webmozart/assert": "^1.2", "webpatser/laravel-uuid": "2.0.1" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 97d0f37c..4f49bd19 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "d4f8198c8d3d27408b5be1a525e8ad4b", + "content-hash": "76f4864c9d8653bb8a6be22a115b7489", "packages": [ { "name": "aws/aws-sdk-php", @@ -3821,6 +3821,56 @@ ], "time": "2016-10-31T21:53:17+00:00" }, + { + "name": "webmozart/assert", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/webmozart/assert.git", + "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozart/assert/zipball/2db61e59ff05fe5126d152bd0655c9ea113e550f", + "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.6", + "sebastian/version": "^1.0.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "time": "2016-11-23T20:04:58+00:00" + }, { "name": "webpatser/laravel-uuid", "version": "2.0.1", @@ -6027,56 +6077,6 @@ "description": "Symfony Yaml Component", "homepage": "https://symfony.com", "time": "2017-07-23T12:43:26+00:00" - }, - { - "name": "webmozart/assert", - "version": "1.2.0", - "source": { - "type": "git", - "url": "https://github.com/webmozart/assert.git", - "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/2db61e59ff05fe5126d152bd0655c9ea113e550f", - "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f", - "shasum": "" - }, - "require": { - "php": "^5.3.3 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.6", - "sebastian/version": "^1.0.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3-dev" - } - }, - "autoload": { - "psr-4": { - "Webmozart\\Assert\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Bernhard Schussek", - "email": "bschussek@gmail.com" - } - ], - "description": "Assertions to validate method input/output with nice error messages.", - "keywords": [ - "assert", - "check", - "validate" - ], - "time": "2016-11-23T20:04:58+00:00" } ], "aliases": [], diff --git a/resources/lang/en/admin/exceptions.php b/resources/lang/en/admin/exceptions.php index 5699bd10..1a575625 100644 --- a/resources/lang/en/admin/exceptions.php +++ b/resources/lang/en/admin/exceptions.php @@ -35,7 +35,13 @@ return [ ], 'service' => [ 'options' => [ - 'must_be_child' => 'The "Configuration From" directive for this option must be a child option for the selected service.', + 'delete_has_servers' => 'A service option with active servers attached to it cannot be deleted from the Panel.', + 'invalid_copy_id' => 'The service option selected for copying a script from either does not exist, or is copying a script itself.', + 'must_be_child' => 'The "Copy Settings From" directive for this option must be a child option for the selected service.', + ], + 'variables' => [ + 'env_not_unique' => 'The environment variable :name must be unique to this service option.', + 'reserved_name' => 'The environment variable :name is protected and cannot be assigned to a variable.', ], ], ]; diff --git a/resources/lang/en/admin/services.php b/resources/lang/en/admin/services.php index fcf59c14..8390140e 100644 --- a/resources/lang/en/admin/services.php +++ b/resources/lang/en/admin/services.php @@ -25,11 +25,15 @@ return [ 'options' => [ 'notices' => [ + 'option_updated' => 'Service option configuration has been updated successfully.', + 'script_updated' => 'Service option install script has been updated and will run whenever servers are installed.', 'option_created' => 'New service option was created successfully. You will need to restart any running daemons to apply this new service.', ], ], 'variables' => [ 'notices' => [ + 'variable_deleted' => 'The variable ":variable" has been deleted and will no longer be available to servers once rebuilt.', + 'variable_updated' => 'The variable ":variable" has been updated. You will need to rebuild any servers using this variable in order to apply changes.', 'variable_created' => 'New variable has successfully been created and assigned to this service option.', ], ], diff --git a/resources/themes/pterodactyl/admin/services/options/variables.blade.php b/resources/themes/pterodactyl/admin/services/options/variables.blade.php index dcc8882c..d0baa8c4 100644 --- a/resources/themes/pterodactyl/admin/services/options/variables.blade.php +++ b/resources/themes/pterodactyl/admin/services/options/variables.blade.php @@ -92,8 +92,8 @@ diff --git a/resources/themes/pterodactyl/admin/services/options/view.blade.php b/resources/themes/pterodactyl/admin/services/options/view.blade.php index 3d302580..24104f79 100644 --- a/resources/themes/pterodactyl/admin/services/options/view.blade.php +++ b/resources/themes/pterodactyl/admin/services/options/view.blade.php @@ -143,10 +143,10 @@ diff --git a/resources/themes/pterodactyl/admin/services/view.blade.php b/resources/themes/pterodactyl/admin/services/view.blade.php index 64cf367c..346a99c8 100644 --- a/resources/themes/pterodactyl/admin/services/view.blade.php +++ b/resources/themes/pterodactyl/admin/services/view.blade.php @@ -84,8 +84,8 @@ diff --git a/routes/admin.php b/routes/admin.php index b205199a..229cf188 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -170,19 +170,21 @@ Route::group(['prefix' => 'services'], function () { Route::get('/view/{id}/functions', 'ServiceController@viewFunctions')->name('admin.services.view.functions'); Route::get('/option/new', 'OptionController@create')->name('admin.services.option.new'); Route::get('/option/{option}', 'OptionController@viewConfiguration')->name('admin.services.option.view'); - Route::get('/option/{option}/variables', 'OptionController@viewVariables')->name('admin.services.option.variables'); + Route::get('/option/{option}/variables', 'VariableController@view')->name('admin.services.option.variables'); Route::get('/option/{option}/scripts', 'OptionController@viewScripts')->name('admin.services.option.scripts'); Route::post('/new', 'ServiceController@store'); - Route::post('/view/{option}', 'ServiceController@edit'); Route::post('/option/new', 'OptionController@store'); - Route::post('/option/{option}/scripts', 'OptionController@updateScripts'); - Route::post('/option/{option}/variables', 'OptionController@createVariable'); - Route::post('/option/{option}/variables/{variable}', 'OptionController@editVariable')->name('admin.services.option.variables.edit'); + Route::post('/option/{option}/variables', 'VariableController@store'); + Route::patch('/view/{option}', 'ServiceController@edit'); Route::patch('/option/{option}', 'OptionController@editConfiguration'); + Route::patch('/option/{option}/scripts', 'OptionController@updateScripts'); + Route::patch('/option/{option}/variables/{variable}', 'VariableController@update')->name('admin.services.option.variables.edit'); Route::delete('/view/{id}', 'ServiceController@delete'); + Route::delete('/option/{option}', 'OptionController@delete'); + Route::delete('/option/{option}/variables/{variable}', 'VariableController@delete'); }); /* From 340193c0133ac69552b1066a6c01d14b4a1b9b73 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 12 Aug 2017 15:32:34 -0500 Subject: [PATCH 48/99] Apply fixes from StyleCI (#581) --- app/Console/Commands/AddLocation.php | 12 +-- app/Events/Auth/FailedCaptcha.php | 42 +++++----- .../Controllers/Admin/VariableController.php | 8 +- .../Controllers/Base/AccountController.php | 6 +- app/Models/ServerVariable.php | 84 +++++++++---------- app/Providers/RepositoryServiceProvider.php | 4 +- .../Services/Options/OptionUpdateService.php | 2 +- .../Variables/VariableCreationService.php | 4 +- ...6_10_23_201624_add_foreign_allocations.php | 30 +++---- ..._23_202703_add_foreign_api_permissions.php | 26 +++--- ...6_10_23_203522_add_foreign_permissions.php | 28 +++---- ...23_203857_add_foreign_server_variables.php | 26 +++--- ..._23_204157_add_foreign_service_options.php | 26 +++--- ...3_204321_add_foreign_service_variables.php | 26 +++--- .../Allocations/AssignmentServiceTest.php | 7 +- 15 files changed, 165 insertions(+), 166 deletions(-) diff --git a/app/Console/Commands/AddLocation.php b/app/Console/Commands/AddLocation.php index d9da9246..7d14cf0e 100644 --- a/app/Console/Commands/AddLocation.php +++ b/app/Console/Commands/AddLocation.php @@ -31,12 +31,12 @@ class AddLocation extends Command { protected $data = []; - /** - * The name and signature of the console command. - * - * @var string - */ - protected $signature = 'pterodactyl:location + /** + * The name and signature of the console command. + * + * @var string + */ + protected $signature = 'pterodactyl:location {--short= : The shortcode name of this location (ex. us1).} {--long= : A longer description of this location.}'; diff --git a/app/Events/Auth/FailedCaptcha.php b/app/Events/Auth/FailedCaptcha.php index ba741cf6..90311726 100644 --- a/app/Events/Auth/FailedCaptcha.php +++ b/app/Events/Auth/FailedCaptcha.php @@ -1,26 +1,26 @@ . -* -* 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. -*/ + * Pterodactyl - Panel + * Copyright (c) 2015 - 2017 Dane Everitt . + * + * 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\Events\Auth; diff --git a/app/Http/Controllers/Admin/VariableController.php b/app/Http/Controllers/Admin/VariableController.php index 3d2a4fa4..4394b42f 100644 --- a/app/Http/Controllers/Admin/VariableController.php +++ b/app/Http/Controllers/Admin/VariableController.php @@ -25,14 +25,14 @@ namespace Pterodactyl\Http\Controllers\Admin; use Prologue\Alerts\AlertsMessageBag; -use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; -use Pterodactyl\Http\Requests\Admin\OptionVariableFormRequest; use Pterodactyl\Models\ServiceOption; use Pterodactyl\Models\ServiceVariable; +use Pterodactyl\Http\Controllers\Controller; +use Pterodactyl\Http\Requests\Admin\OptionVariableFormRequest; use Pterodactyl\Repositories\Eloquent\ServiceVariableRepository; -use Pterodactyl\Services\Services\Variables\VariableCreationService; use Pterodactyl\Services\Services\Variables\VariableUpdateService; +use Pterodactyl\Services\Services\Variables\VariableCreationService; +use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; class VariableController extends Controller { diff --git a/app/Http/Controllers/Base/AccountController.php b/app/Http/Controllers/Base/AccountController.php index 7163045a..7167a6f2 100644 --- a/app/Http/Controllers/Base/AccountController.php +++ b/app/Http/Controllers/Base/AccountController.php @@ -67,15 +67,15 @@ class AccountController extends Controller $data['password'] = $request->input('new_password'); - // Request to update account Email + // Request to update account Email } elseif ($request->input('do_action') === 'email') { $data['email'] = $request->input('new_email'); - // Request to update account Identity + // Request to update account Identity } elseif ($request->input('do_action') === 'identity') { $data = $request->only(['name_first', 'name_last', 'username']); - // Unknown, hit em with a 404 + // Unknown, hit em with a 404 } else { return abort(404); } diff --git a/app/Models/ServerVariable.php b/app/Models/ServerVariable.php index 165d5b3d..fdff100e 100644 --- a/app/Models/ServerVariable.php +++ b/app/Models/ServerVariable.php @@ -42,53 +42,53 @@ class ServerVariable extends Model */ protected $guarded = ['id', 'created_at', 'updated_at']; - /** - * Cast values to correct type. - * - * @var array - */ - protected $casts = [ + /** + * Cast values to correct type. + * + * @var array + */ + protected $casts = [ 'server_id' => 'integer', 'variable_id' => 'integer', ]; - /** - * Determine if variable is viewable by users. - * - * @return bool - */ - public function getUserCanViewAttribute() - { - return (bool) $this->variable->user_viewable; - } + /** + * Determine if variable is viewable by users. + * + * @return bool + */ + public function getUserCanViewAttribute() + { + return (bool) $this->variable->user_viewable; + } - /** - * Determine if variable is editable by users. - * - * @return bool - */ - public function getUserCanEditAttribute() - { - return (bool) $this->variable->user_editable; - } + /** + * Determine if variable is editable by users. + * + * @return bool + */ + public function getUserCanEditAttribute() + { + return (bool) $this->variable->user_editable; + } - /** - * Determine if variable is required. - * - * @return bool - */ - public function getRequiredAttribute() - { - return $this->variable->required; - } + /** + * Determine if variable is required. + * + * @return bool + */ + public function getRequiredAttribute() + { + return $this->variable->required; + } - /** - * Returns information about a given variables parent. - * - * @return \Illuminate\Database\Eloquent\Relations\BelongsTo - */ - public function variable() - { - return $this->belongsTo(ServiceVariable::class, 'variable_id'); - } + /** + * Returns information about a given variables parent. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function variable() + { + return $this->belongsTo(ServiceVariable::class, 'variable_id'); + } } diff --git a/app/Providers/RepositoryServiceProvider.php b/app/Providers/RepositoryServiceProvider.php index 8a98e662..0dd26a4b 100644 --- a/app/Providers/RepositoryServiceProvider.php +++ b/app/Providers/RepositoryServiceProvider.php @@ -25,9 +25,7 @@ namespace Pterodactyl\Providers; use Illuminate\Support\ServiceProvider; -use Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface; use Pterodactyl\Repositories\Eloquent\NodeRepository; -use Pterodactyl\Repositories\Eloquent\ServiceVariableRepository; use Pterodactyl\Repositories\Eloquent\UserRepository; use Pterodactyl\Repositories\Eloquent\ApiKeyRepository; use Pterodactyl\Repositories\Eloquent\ServerRepository; @@ -46,6 +44,7 @@ use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Repositories\Eloquent\OptionVariableRepository; use Pterodactyl\Repositories\Eloquent\ServerVariableRepository; use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; +use Pterodactyl\Repositories\Eloquent\ServiceVariableRepository; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; @@ -54,6 +53,7 @@ use Pterodactyl\Contracts\Repository\ApiPermissionRepositoryInterface; use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; use Pterodactyl\Contracts\Repository\OptionVariableRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface; use Pterodactyl\Repositories\Daemon\ServerRepository as DaemonServerRepository; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; diff --git a/app/Services/Services/Options/OptionUpdateService.php b/app/Services/Services/Options/OptionUpdateService.php index 4e922b21..4ba133fa 100644 --- a/app/Services/Services/Options/OptionUpdateService.php +++ b/app/Services/Services/Options/OptionUpdateService.php @@ -24,9 +24,9 @@ namespace Pterodactyl\Services\Services\Options; +use Pterodactyl\Models\ServiceOption; use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; use Pterodactyl\Exceptions\Services\ServiceOption\NoParentConfigurationFoundException; -use Pterodactyl\Models\ServiceOption; class OptionUpdateService { diff --git a/app/Services/Services/Variables/VariableCreationService.php b/app/Services/Services/Variables/VariableCreationService.php index a079baab..7730380c 100644 --- a/app/Services/Services/Variables/VariableCreationService.php +++ b/app/Services/Services/Variables/VariableCreationService.php @@ -24,11 +24,11 @@ namespace Pterodactyl\Services\Services\Variables; +use Pterodactyl\Models\ServiceOption; +use Pterodactyl\Models\ServiceVariable; use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; use Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface; use Pterodactyl\Exceptions\Services\ServiceVariable\ReservedVariableNameException; -use Pterodactyl\Models\ServiceOption; -use Pterodactyl\Models\ServiceVariable; class VariableCreationService { diff --git a/database/migrations/2016_10_23_201624_add_foreign_allocations.php b/database/migrations/2016_10_23_201624_add_foreign_allocations.php index 8ff9bdd2..d2d869cd 100644 --- a/database/migrations/2016_10_23_201624_add_foreign_allocations.php +++ b/database/migrations/2016_10_23_201624_add_foreign_allocations.php @@ -24,24 +24,24 @@ class AddForeignAllocations extends Migration }); } - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Schema::table('allocations', function (Blueprint $table) { - $table->dropForeign('allocations_assigned_to_foreign'); - $table->dropForeign('allocations_node_foreign'); + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('allocations', function (Blueprint $table) { + $table->dropForeign('allocations_assigned_to_foreign'); + $table->dropForeign('allocations_node_foreign'); - $table->dropIndex('allocations_assigned_to_foreign'); - $table->dropIndex('allocations_node_foreign'); - }); + $table->dropIndex('allocations_assigned_to_foreign'); + $table->dropIndex('allocations_node_foreign'); + }); - DB::statement('ALTER TABLE allocations + DB::statement('ALTER TABLE allocations MODIFY COLUMN assigned_to MEDIUMINT(8) UNSIGNED NULL, MODIFY COLUMN node MEDIUMINT(8) UNSIGNED NOT NULL '); - } + } } diff --git a/database/migrations/2016_10_23_202703_add_foreign_api_permissions.php b/database/migrations/2016_10_23_202703_add_foreign_api_permissions.php index 58bf1a5a..16e1eebd 100644 --- a/database/migrations/2016_10_23_202703_add_foreign_api_permissions.php +++ b/database/migrations/2016_10_23_202703_add_foreign_api_permissions.php @@ -20,18 +20,18 @@ class AddForeignApiPermissions extends Migration }); } - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Schema::table('api_permissions', function (Blueprint $table) { - $table->dropForeign('api_permissions_key_id_foreign'); - $table->dropIndex('api_permissions_key_id_foreign'); - }); + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('api_permissions', function (Blueprint $table) { + $table->dropForeign('api_permissions_key_id_foreign'); + $table->dropIndex('api_permissions_key_id_foreign'); + }); - DB::statement('ALTER TABLE api_permissions MODIFY key_id MEDIUMINT(8) UNSIGNED NOT NULL'); - } + DB::statement('ALTER TABLE api_permissions MODIFY key_id MEDIUMINT(8) UNSIGNED NOT NULL'); + } } diff --git a/database/migrations/2016_10_23_203522_add_foreign_permissions.php b/database/migrations/2016_10_23_203522_add_foreign_permissions.php index 153ab27c..2f872de6 100644 --- a/database/migrations/2016_10_23_203522_add_foreign_permissions.php +++ b/database/migrations/2016_10_23_203522_add_foreign_permissions.php @@ -19,19 +19,19 @@ class AddForeignPermissions extends Migration }); } - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Schema::table('permissions', function (Blueprint $table) { - $table->dropForeign('permissions_user_id_foreign'); - $table->dropForeign('permissions_server_id_foreign'); + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('permissions', function (Blueprint $table) { + $table->dropForeign('permissions_user_id_foreign'); + $table->dropForeign('permissions_server_id_foreign'); - $table->dropIndex('permissions_user_id_foreign'); - $table->dropIndex('permissions_server_id_foreign'); - }); - } + $table->dropIndex('permissions_user_id_foreign'); + $table->dropIndex('permissions_server_id_foreign'); + }); + } } diff --git a/database/migrations/2016_10_23_203857_add_foreign_server_variables.php b/database/migrations/2016_10_23_203857_add_foreign_server_variables.php index af78a161..0a975dc8 100644 --- a/database/migrations/2016_10_23_203857_add_foreign_server_variables.php +++ b/database/migrations/2016_10_23_203857_add_foreign_server_variables.php @@ -24,21 +24,21 @@ class AddForeignServerVariables extends Migration }); } - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Schema::table('server_variables', function (Blueprint $table) { - $table->dropForeign(['server_id']); - $table->dropForeign(['variable_id']); - }); + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('server_variables', function (Blueprint $table) { + $table->dropForeign(['server_id']); + $table->dropForeign(['variable_id']); + }); - DB::statement('ALTER TABLE server_variables + DB::statement('ALTER TABLE server_variables MODIFY COLUMN server_id MEDIUMINT(8) UNSIGNED NULL, MODIFY COLUMN variable_id MEDIUMINT(8) UNSIGNED NOT NULL '); - } + } } diff --git a/database/migrations/2016_10_23_204157_add_foreign_service_options.php b/database/migrations/2016_10_23_204157_add_foreign_service_options.php index 0baad0c3..ff6e3b35 100644 --- a/database/migrations/2016_10_23_204157_add_foreign_service_options.php +++ b/database/migrations/2016_10_23_204157_add_foreign_service_options.php @@ -20,18 +20,18 @@ class AddForeignServiceOptions extends Migration }); } - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Schema::table('service_options', function (Blueprint $table) { - $table->dropForeign('service_options_parent_service_foreign'); - $table->dropIndex('service_options_parent_service_foreign'); - }); + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('service_options', function (Blueprint $table) { + $table->dropForeign('service_options_parent_service_foreign'); + $table->dropIndex('service_options_parent_service_foreign'); + }); - DB::statement('ALTER TABLE service_options MODIFY parent_service MEDIUMINT(8) UNSIGNED NOT NULL'); - } + DB::statement('ALTER TABLE service_options MODIFY parent_service MEDIUMINT(8) UNSIGNED NOT NULL'); + } } diff --git a/database/migrations/2016_10_23_204321_add_foreign_service_variables.php b/database/migrations/2016_10_23_204321_add_foreign_service_variables.php index 291ca24e..5a543898 100644 --- a/database/migrations/2016_10_23_204321_add_foreign_service_variables.php +++ b/database/migrations/2016_10_23_204321_add_foreign_service_variables.php @@ -20,18 +20,18 @@ class AddForeignServiceVariables extends Migration }); } - /** - * Reverse the migrations. - * - * @return void - */ - public function down() - { - Schema::table('service_variables', function (Blueprint $table) { - $table->dropForeign('service_variables_option_id_foreign'); - $table->dropIndex('service_variables_option_id_foreign'); - }); + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('service_variables', function (Blueprint $table) { + $table->dropForeign('service_variables_option_id_foreign'); + $table->dropIndex('service_variables_option_id_foreign'); + }); - DB::statement('ALTER TABLE service_variables MODIFY option_id MEDIUMINT(8) UNSIGNED NOT NULL'); - } + DB::statement('ALTER TABLE service_variables MODIFY option_id MEDIUMINT(8) UNSIGNED NOT NULL'); + } } diff --git a/tests/Unit/Services/Allocations/AssignmentServiceTest.php b/tests/Unit/Services/Allocations/AssignmentServiceTest.php index 2c222859..f55fdc5f 100644 --- a/tests/Unit/Services/Allocations/AssignmentServiceTest.php +++ b/tests/Unit/Services/Allocations/AssignmentServiceTest.php @@ -87,7 +87,7 @@ class AssignmentServiceTest extends TestCase { $data = [ 'allocation_ip' => '192.168.1.1', - 'allocation_ports' => ['1024'] + 'allocation_ports' => ['1024'], ]; $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); @@ -112,7 +112,7 @@ class AssignmentServiceTest extends TestCase { $data = [ 'allocation_ip' => '192.168.1.1', - 'allocation_ports' => ['1024-1026'] + 'allocation_ports' => ['1024-1026'], ]; $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); @@ -142,7 +142,6 @@ class AssignmentServiceTest extends TestCase $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); $this->service->handle($this->node->id, $data); - } /** @@ -307,7 +306,7 @@ class AssignmentServiceTest extends TestCase { $data = [ 'allocation_ip' => '192.168.1.1', - 'allocation_ports' => ['1024'] + 'allocation_ports' => ['1024'], ]; $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); From 364adb1f840d24430ca21e9a32546911cfff4d6d Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 12 Aug 2017 16:30:27 -0500 Subject: [PATCH 49/99] Add tests for service option services --- .../Options/InstallScriptUpdateService.php | 15 +- database/factories/ModelFactory.php | 10 ++ .../InstallScriptUpdateServiceTest.php | 128 ++++++++++++++++++ .../Options/OptionCreationServiceTest.php | 122 +++++++++++++++++ .../Options/OptionDeletionServiceTest.php | 86 ++++++++++++ .../Options/OptionUpdateServiceTest.php | 121 +++++++++++++++++ 6 files changed, 475 insertions(+), 7 deletions(-) create mode 100644 tests/Unit/Services/Services/Options/InstallScriptUpdateServiceTest.php create mode 100644 tests/Unit/Services/Services/Options/OptionCreationServiceTest.php create mode 100644 tests/Unit/Services/Services/Options/OptionDeletionServiceTest.php create mode 100644 tests/Unit/Services/Services/Options/OptionUpdateServiceTest.php diff --git a/app/Services/Services/Options/InstallScriptUpdateService.php b/app/Services/Services/Options/InstallScriptUpdateService.php index f2f13559..1a63c800 100644 --- a/app/Services/Services/Options/InstallScriptUpdateService.php +++ b/app/Services/Services/Options/InstallScriptUpdateService.php @@ -33,16 +33,16 @@ class InstallScriptUpdateService /** * @var \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface */ - protected $serviceOptionRepository; + protected $repository; /** * InstallScriptUpdateService constructor. * - * @param \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface $serviceOptionRepository + * @param \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface $repository */ - public function __construct(ServiceOptionRepositoryInterface $serviceOptionRepository) + public function __construct(ServiceOptionRepositoryInterface $repository) { - $this->serviceOptionRepository = $serviceOptionRepository; + $this->repository = $repository; } /** @@ -52,21 +52,22 @@ class InstallScriptUpdateService * @param array $data * * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Services\ServiceOption\InvalidCopyFromException */ public function handle($option, array $data) { if (! $option instanceof ServiceOption) { - $option = $this->serviceOptionRepository->find($option); + $option = $this->repository->find($option); } if (! is_null(array_get($data, 'copy_script_from'))) { - if (! $this->serviceOptionRepository->isCopiableScript(array_get($data, 'copy_script_from'), $option->service_id)) { + if (! $this->repository->isCopiableScript(array_get($data, 'copy_script_from'), $option->service_id)) { throw new InvalidCopyFromException(trans('admin/exceptions.service.options.invalid_copy_id')); } } - $this->serviceOptionRepository->withoutFresh()->update($option->id, [ + $this->repository->withoutFresh()->update($option->id, [ 'script_install' => array_get($data, 'script_install'), 'script_is_privileged' => array_get($data, 'script_is_privileged'), 'script_entry' => array_get($data, 'script_entry'), diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index c16652f0..b7e84eaf 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -88,6 +88,16 @@ $factory->define(Pterodactyl\Models\Node::class, function (Faker\Generator $fake ]; }); +$factory->define(Pterodactyl\Models\ServiceOption::class, function (Faker\Generator $faker) { + return [ + 'id' => $faker->unique()->randomNumber(), + 'service_id' => $faker->unique()->randomNumber(), + 'name' => $faker->name, + 'description' => $faker->sentences(3), + 'tag' => $faker->unique()->randomNumber(5), + ]; +}); + $factory->define(Pterodactyl\Models\ServiceVariable::class, function (Faker\Generator $faker) { return [ 'id' => $faker->unique()->randomNumber(), diff --git a/tests/Unit/Services/Services/Options/InstallScriptUpdateServiceTest.php b/tests/Unit/Services/Services/Options/InstallScriptUpdateServiceTest.php new file mode 100644 index 00000000..ea0b881c --- /dev/null +++ b/tests/Unit/Services/Services/Options/InstallScriptUpdateServiceTest.php @@ -0,0 +1,128 @@ +. + * + * 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\Services\Options; + +use Exception; +use Mockery as m; +use Tests\TestCase; +use Pterodactyl\Models\ServiceOption; +use Pterodactyl\Services\Services\Options\InstallScriptUpdateService; +use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; +use Pterodactyl\Exceptions\Services\ServiceOption\InvalidCopyFromException; + +class InstallScriptUpdateServiceTest extends TestCase +{ + /** + * @var array + */ + protected $data = [ + 'script_install' => 'test-script', + 'script_is_privileged' => true, + 'script_entry' => '/bin/bash', + 'script_container' => 'ubuntu', + 'copy_script_from' => null, + ]; + + /** + * @var \Pterodactyl\Models\ServiceOption + */ + protected $model; + + /** + * @var \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Services\Options\InstallScriptUpdateService + */ + protected $service; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->model = factory(ServiceOption::class)->make(); + $this->repository = m::mock(ServiceOptionRepositoryInterface::class); + + $this->service = new InstallScriptUpdateService($this->repository); + } + + /** + * Test that passing a new copy_script_from attribute works properly. + */ + public function testUpdateWithValidCopyScriptFromAttribute() + { + $this->data['copy_script_from'] = 1; + + $this->repository->shouldReceive('isCopiableScript')->with(1, $this->model->service_id)->once()->andReturn(true); + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($this->model->id, $this->data)->andReturnNull(); + + $this->service->handle($this->model, $this->data); + } + + /** + * Test that an exception gets raised when the script is not copiable. + */ + public function testUpdateWithInvalidCopyScriptFromAttribute() + { + $this->data['copy_script_from'] = 1; + + $this->repository->shouldReceive('isCopiableScript')->with(1, $this->model->service_id)->once()->andReturn(false); + try { + $this->service->handle($this->model, $this->data); + } catch (Exception $exception) { + $this->assertInstanceOf(InvalidCopyFromException::class, $exception); + $this->assertEquals(trans('admin/exceptions.service.options.invalid_copy_id'), $exception->getMessage()); + } + } + + /** + * Test standard functionality. + */ + public function testUpdateWithoutNewCopyScriptFromAttribute() + { + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($this->model->id, $this->data)->andReturnNull(); + + $this->service->handle($this->model, $this->data); + } + + /** + * Test that an integer can be passed in place of a model. + */ + public function testFunctionAcceptsIntegerInPlaceOfModel() + { + $this->repository->shouldReceive('find')->with($this->model->id)->once()->andReturn($this->model); + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($this->model->id, $this->data)->andReturnNull(); + + $this->service->handle($this->model->id, $this->data); + } +} diff --git a/tests/Unit/Services/Services/Options/OptionCreationServiceTest.php b/tests/Unit/Services/Services/Options/OptionCreationServiceTest.php new file mode 100644 index 00000000..95016864 --- /dev/null +++ b/tests/Unit/Services/Services/Options/OptionCreationServiceTest.php @@ -0,0 +1,122 @@ +. + * + * 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\Services\Options; + +use Exception; +use Mockery as m; +use Tests\TestCase; +use Pterodactyl\Models\ServiceOption; +use Pterodactyl\Services\Services\Options\OptionCreationService; +use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; +use Pterodactyl\Exceptions\Services\ServiceOption\NoParentConfigurationFoundException; + +class OptionCreationServiceTest extends TestCase +{ + /** + * @var \Pterodactyl\Models\ServiceOption + */ + protected $model; + + /** + * @var \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Services\Options\OptionCreationService + */ + protected $service; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->model = factory(ServiceOption::class)->make(); + $this->repository = m::mock(ServiceOptionRepositoryInterface::class); + + $this->service = new OptionCreationService($this->repository); + } + + /** + * Test that a new model is created when not using the config from attribute. + */ + public function testCreateNewModelWithoutUsingConfigFrom() + { + $this->repository->shouldReceive('create')->with(['name' => $this->model->name, 'config_from' => null]) + ->once()->andReturn($this->model); + + $response = $this->service->handle(['name' => $this->model->name]); + + $this->assertNotEmpty($response); + $this->assertNull(object_get($response, 'config_from')); + $this->assertEquals($this->model->name, $response->name); + } + + /** + * Test that a new model is created when using the config from attribute. + */ + public function testCreateNewModelUsingConfigFrom() + { + $data = [ + 'name' => $this->model->name, + 'service_id' => $this->model->service_id, + 'config_from' => 1, + ]; + + $this->repository->shouldReceive('findCountWhere')->with([ + ['service_id', '=', $data['service_id']], + ['id', '=', $data['config_from']] + ])->once()->andReturn(1); + + $this->repository->shouldReceive('create')->with($data) + ->once()->andReturn($this->model); + + $response = $this->service->handle($data); + + $this->assertNotEmpty($response); + $this->assertEquals($response, $this->model); + } + + /** + * Test that an exception is thrown if no parent configuration can be located. + */ + public function testExceptionIsThrownIfNoParentConfigurationIsFound() + { + $this->repository->shouldReceive('findCountWhere')->with([ + ['service_id', '=', null], + ['id', '=', 1] + ])->once()->andReturn(0); + + try { + $this->service->handle(['config_from' => 1]); + } catch (Exception $exception) { + $this->assertInstanceOf(NoParentConfigurationFoundException::class, $exception); + $this->assertEquals(trans('admin/exceptions.service.options.must_be_child'), $exception->getMessage()); + } + } +} diff --git a/tests/Unit/Services/Services/Options/OptionDeletionServiceTest.php b/tests/Unit/Services/Services/Options/OptionDeletionServiceTest.php new file mode 100644 index 00000000..725f6d0c --- /dev/null +++ b/tests/Unit/Services/Services/Options/OptionDeletionServiceTest.php @@ -0,0 +1,86 @@ +. + * + * 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\Services\Options; + +use Mockery as m; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; +use Pterodactyl\Exceptions\Services\ServiceOption\HasActiveServersException; +use Pterodactyl\Services\Services\Options\OptionDeletionService; +use Tests\TestCase; + +class OptionDeletionServiceTest extends TestCase +{ + /** + * @var \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $serverRepository; + + /** + * @var \Pterodactyl\Services\Services\Options\OptionDeletionService + */ + protected $service; + + public function setUp() + { + parent::setUp(); + + $this->repository = m::mock(ServiceOptionRepositoryInterface::class); + $this->serverRepository = m::mock(ServerRepositoryInterface::class); + + $this->service = new OptionDeletionService($this->serverRepository, $this->repository); + } + + /** + * Test that option is deleted if no servers are found. + */ + public function testOptionIsDeletedIfNoServersAreFound() + { + $this->serverRepository->shouldReceive('findCountWhere')->with([['option_id', '=', 1]])->once()->andReturn(0); + $this->repository->shouldReceive('delete')->with(1)->once()->andReturn(1); + + $this->assertEquals(1, $this->service->handle(1)); + } + + /** + * Test that option is not deleted if servers are found. + */ + public function testExceptionIsThrownIfServersAreFound() + { + $this->serverRepository->shouldReceive('findCountWhere')->with([['option_id', '=', 1]])->once()->andReturn(1); + + try { + $this->service->handle(1); + } catch (\Exception $exception) { + $this->assertInstanceOf(HasActiveServersException::class, $exception); + $this->assertEquals(trans('admin/exceptions.service.options.delete_has_servers'), $exception->getMessage()); + } + } +} diff --git a/tests/Unit/Services/Services/Options/OptionUpdateServiceTest.php b/tests/Unit/Services/Services/Options/OptionUpdateServiceTest.php new file mode 100644 index 00000000..20d23761 --- /dev/null +++ b/tests/Unit/Services/Services/Options/OptionUpdateServiceTest.php @@ -0,0 +1,121 @@ +. + * + * 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\Services\Options; + +use Exception; +use Mockery as m; +use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; +use Pterodactyl\Exceptions\Services\ServiceOption\NoParentConfigurationFoundException; +use Pterodactyl\Models\ServiceOption; +use Pterodactyl\Services\Services\Options\OptionUpdateService; +use Tests\TestCase; + +class OptionUpdateServiceTest extends TestCase +{ + /** + * @var \Pterodactyl\Models\ServiceOption + */ + protected $model; + + /** + * @var \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Services\Options\OptionUpdateService + */ + protected $service; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->model = factory(ServiceOption::class)->make(); + $this->repository = m::mock(ServiceOptionRepositoryInterface::class); + + $this->service = new OptionUpdateService($this->repository); + } + + /** + * Test that an option is updated when no config_from attribute is passed. + */ + public function testOptionIsUpdatedWhenNoConfigFromIsProvided() + { + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($this->model->id, ['test_field' => 'field_value'])->once()->andReturnNull(); + + $this->service->handle($this->model, ['test_field' => 'field_value']); + } + + /** + * Test that option is updated when a valid config_from attribute is passed. + */ + public function testOptionIsUpdatedWhenValidConfigFromIsPassed() + { + $this->repository->shouldReceive('findCountWhere')->with([ + ['service_id', '=', $this->model->service_id], + ['id', '=', 1], + ])->once()->andReturn(1); + + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($this->model->id, ['config_from' => 1])->once()->andReturnNull(); + + $this->service->handle($this->model, ['config_from' => 1]); + } + + /** + * Test that an exception is thrown if an invalid config_from attribute is passed. + */ + public function testExceptionIsThrownIfInvalidParentConfigIsPassed() + { + $this->repository->shouldReceive('findCountWhere')->with([ + ['service_id', '=', $this->model->service_id], + ['id', '=', 1], + ])->once()->andReturn(0); + + try { + $this->service->handle($this->model, ['config_from' => 1]); + } catch (Exception $exception) { + $this->assertInstanceOf(NoParentConfigurationFoundException::class, $exception); + $this->assertEquals(trans('admin/exceptions.service.options.must_be_child'), $exception->getMessage()); + } + } + + /** + * Test that an integer linking to a model can be passed in place of the ServiceOption model. + */ + public function testIntegerCanBePassedInPlaceOfModel() + { + $this->repository->shouldReceive('find')->with($this->model->id)->once()->andReturn($this->model); + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($this->model->id, ['test_field' => 'field_value'])->once()->andReturnNull(); + + $this->service->handle($this->model->id, ['test_field' => 'field_value']); + } +} From 6d1b994b7d6108a71276f4d667c6d45a441b1d10 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 13 Aug 2017 14:55:09 -0500 Subject: [PATCH 50/99] More tests --- .../Controllers/Admin/VariableController.php | 1 + app/Services/Servers/CreationService.php | 1 + app/Services/Servers/DeletionService.php | 22 +- .../Servers/StartupModificationService.php | 1 + .../Variables/VariableCreationService.php | 8 +- .../Variables/VariableUpdateService.php | 24 +- database/factories/ModelFactory.php | 2 +- .../Services/Servers/CreationServiceTest.php | 124 ++++++---- .../Services/Servers/DeletionServiceTest.php | 213 ++++++++++++++++++ .../Variables/VariableCreationServiceTest.php | 143 ++++++++++++ .../Variables/VariableUpdateServiceTest.php | 151 +++++++++++++ 11 files changed, 626 insertions(+), 64 deletions(-) create mode 100644 tests/Unit/Services/Servers/DeletionServiceTest.php create mode 100644 tests/Unit/Services/Services/Variables/VariableCreationServiceTest.php create mode 100644 tests/Unit/Services/Services/Variables/VariableUpdateServiceTest.php diff --git a/app/Http/Controllers/Admin/VariableController.php b/app/Http/Controllers/Admin/VariableController.php index 4394b42f..eef95ec2 100644 --- a/app/Http/Controllers/Admin/VariableController.php +++ b/app/Http/Controllers/Admin/VariableController.php @@ -116,6 +116,7 @@ class VariableController extends Controller * * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Services\ServiceVariable\ReservedVariableNameException */ public function update(OptionVariableFormRequest $request, ServiceOption $option, ServiceVariable $variable) diff --git a/app/Services/Servers/CreationService.php b/app/Services/Servers/CreationService.php index 96e1527d..ff0b50f7 100644 --- a/app/Services/Servers/CreationService.php +++ b/app/Services/Servers/CreationService.php @@ -196,6 +196,7 @@ class CreationService } catch (RequestException $exception) { $response = $exception->getResponse(); $this->writer->warning($exception); + $this->database->rollBack(); throw new DisplayException(trans('admin/server.exceptions.daemon_exception', [ 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), diff --git a/app/Services/Servers/DeletionService.php b/app/Services/Servers/DeletionService.php index 869f17e3..a3e34e3a 100644 --- a/app/Services/Servers/DeletionService.php +++ b/app/Services/Servers/DeletionService.php @@ -36,16 +36,16 @@ use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonS class DeletionService { + /** + * @var \Illuminate\Database\ConnectionInterface + */ + protected $connection; + /** * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface */ protected $daemonServerRepository; - /** - * @var \Illuminate\Database\ConnectionInterface - */ - protected $database; - /** * @var \Pterodactyl\Services\Database\DatabaseManagementService */ @@ -74,7 +74,7 @@ class DeletionService /** * DeletionService constructor. * - * @param \Illuminate\Database\ConnectionInterface $database + * @param \Illuminate\Database\ConnectionInterface $connection * @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonServerRepository * @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $databaseRepository * @param \Pterodactyl\Services\Database\DatabaseManagementService $databaseManagementService @@ -82,7 +82,7 @@ class DeletionService * @param \Illuminate\Log\Writer $writer */ public function __construct( - ConnectionInterface $database, + ConnectionInterface $connection, DaemonServerRepositoryInterface $daemonServerRepository, DatabaseRepositoryInterface $databaseRepository, DatabaseManagementService $databaseManagementService, @@ -90,7 +90,7 @@ class DeletionService Writer $writer ) { $this->daemonServerRepository = $daemonServerRepository; - $this->database = $database; + $this->connection = $connection; $this->databaseManagementService = $databaseManagementService; $this->databaseRepository = $databaseRepository; $this->repository = $repository; @@ -114,7 +114,9 @@ class DeletionService * Delete a server from the panel and remove any associated databases from hosts. * * @param int|\Pterodactyl\Models\Server $server + * * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function handle($server) { @@ -140,12 +142,12 @@ class DeletionService } } - $this->database->beginTransaction(); + $this->connection->beginTransaction(); $this->databaseRepository->withColumns('id')->findWhere([['server_id', '=', $server->id]])->each(function ($item) { $this->databaseManagementService->delete($item->id); }); $this->repository->delete($server->id); - $this->database->commit(); + $this->connection->commit(); } } diff --git a/app/Services/Servers/StartupModificationService.php b/app/Services/Servers/StartupModificationService.php index 8c3ffa5c..c9ea2391 100644 --- a/app/Services/Servers/StartupModificationService.php +++ b/app/Services/Servers/StartupModificationService.php @@ -125,6 +125,7 @@ class StartupModificationService * * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function handle($server, array $data) { diff --git a/app/Services/Services/Variables/VariableCreationService.php b/app/Services/Services/Variables/VariableCreationService.php index 7730380c..1dc1f8ae 100644 --- a/app/Services/Services/Variables/VariableCreationService.php +++ b/app/Services/Services/Variables/VariableCreationService.php @@ -53,8 +53,8 @@ class VariableCreationService /** * Create a new variable for a given service option. * - * @param int $option - * @param array $data + * @param int|\Pterodactyl\Models\ServiceOption $option + * @param array $data * @return \Pterodactyl\Models\ServiceVariable * * @throws \Pterodactyl\Exceptions\Model\DataValidationException @@ -67,7 +67,9 @@ class VariableCreationService } if (in_array(strtoupper(array_get($data, 'env_variable')), explode(',', ServiceVariable::RESERVED_ENV_NAMES))) { - throw new ReservedVariableNameException(sprintf('Cannot use the protected name %s for this environment variable.')); + throw new ReservedVariableNameException(sprintf( + 'Cannot use the protected name %s for this environment variable.', array_get($data, 'env_variable') + )); } $options = array_get($data, 'options', []); diff --git a/app/Services/Services/Variables/VariableUpdateService.php b/app/Services/Services/Variables/VariableUpdateService.php index c3fb8693..90c10a54 100644 --- a/app/Services/Services/Variables/VariableUpdateService.php +++ b/app/Services/Services/Variables/VariableUpdateService.php @@ -34,11 +34,16 @@ class VariableUpdateService /** * @var \Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface */ - protected $serviceVariableRepository; + protected $repository; - public function __construct(ServiceVariableRepositoryInterface $serviceVariableRepository) + /** + * VariableUpdateService constructor. + * + * @param \Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface $repository + */ + public function __construct(ServiceVariableRepositoryInterface $repository) { - $this->serviceVariableRepository = $serviceVariableRepository; + $this->repository = $repository; } /** @@ -46,16 +51,17 @@ class VariableUpdateService * * @param int|\Pterodactyl\Models\ServiceVariable $variable * @param array $data - * @return \Pterodactyl\Models\ServiceVariable + * @return mixed * * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Services\ServiceVariable\ReservedVariableNameException */ public function handle($variable, array $data) { if (! $variable instanceof ServiceVariable) { - $variable = $this->serviceVariableRepository->find($variable); + $variable = $this->repository->find($variable); } if (! is_null(array_get($data, 'env_variable'))) { @@ -65,7 +71,7 @@ class VariableUpdateService ])); } - $search = $this->serviceVariableRepository->withColumns('id')->findCountWhere([ + $search = $this->repository->withColumns('id')->findCountWhere([ ['env_variable', '=', array_get($data, 'env_variable')], ['option_id', '=', $variable->option_id], ['id', '!=', $variable->id], @@ -80,9 +86,9 @@ class VariableUpdateService $options = array_get($data, 'options', []); - return $this->serviceVariableRepository->update($variable->id, array_merge([ - 'user_viewable' => in_array('user_viewable', $options, $variable->user_viewable), - 'user_editable' => in_array('user_editable', $options, $variable->user_editable), + return $this->repository->withoutFresh()->update($variable->id, array_merge([ + 'user_viewable' => in_array('user_viewable', $options), + 'user_editable' => in_array('user_editable', $options), ], $data)); } } diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index b7e84eaf..4da5007d 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -93,7 +93,7 @@ $factory->define(Pterodactyl\Models\ServiceOption::class, function (Faker\Genera 'id' => $faker->unique()->randomNumber(), 'service_id' => $faker->unique()->randomNumber(), 'name' => $faker->name, - 'description' => $faker->sentences(3), + 'description' => implode(' ', $faker->sentences(3)), 'tag' => $faker->unique()->randomNumber(5), ]; }); diff --git a/tests/Unit/Services/Servers/CreationServiceTest.php b/tests/Unit/Services/Servers/CreationServiceTest.php index dad92475..c14384cf 100644 --- a/tests/Unit/Services/Servers/CreationServiceTest.php +++ b/tests/Unit/Services/Servers/CreationServiceTest.php @@ -24,9 +24,11 @@ namespace Tests\Unit\Services\Servers; +use Exception; +use GuzzleHttp\Exception\RequestException; use Mockery as m; +use Pterodactyl\Exceptions\DisplayException; use Tests\TestCase; -use Ramsey\Uuid\Uuid; use Illuminate\Log\Writer; use phpmock\phpunit\PHPMock; use Illuminate\Database\DatabaseManager; @@ -54,11 +56,40 @@ class CreationServiceTest extends TestCase */ protected $daemonServerRepository; + /** + * @var array + */ + protected $data = [ + 'node_id' => 1, + 'name' => 'SomeName', + 'description' => null, + 'owner_id' => 1, + 'memory' => 128, + 'disk' => 128, + 'swap' => 0, + 'io' => 500, + 'cpu' => 0, + 'allocation_id' => 1, + 'allocation_additional' => [2, 3], + 'environment' => [ + 'TEST_VAR_1' => 'var1-value', + ], + 'service_id' => 1, + 'option_id' => 1, + 'startup' => 'startup-param', + 'docker_image' => 'some/image', + ]; + /** * @var \Illuminate\Database\DatabaseManager */ protected $database; + /** + * @var \GuzzleHttp\Exception\RequestException + */ + protected $exception; + /** * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface */ @@ -114,6 +145,7 @@ class CreationServiceTest extends TestCase $this->allocationRepository = m::mock(AllocationRepositoryInterface::class); $this->daemonServerRepository = m::mock(DaemonServerRepositoryInterface::class); $this->database = m::mock(DatabaseManager::class); + $this->exception = m::mock(RequestException::class); $this->nodeRepository = m::mock(NodeRepositoryInterface::class); $this->repository = m::mock(ServerRepositoryInterface::class); $this->serverVariableRepository = m::mock(ServerVariableRepositoryInterface::class); @@ -148,59 +180,38 @@ class CreationServiceTest extends TestCase */ public function testCreateShouldHitAllOfTheNecessaryServicesAndStoreTheServer() { - $data = [ - 'node_id' => 1, - 'name' => 'SomeName', - 'description' => null, - 'owner_id' => 1, - 'memory' => 128, - 'disk' => 128, - 'swap' => 0, - 'io' => 500, - 'cpu' => 0, - 'allocation_id' => 1, - 'allocation_additional' => [2, 3], - 'environment' => [ - 'TEST_VAR_1' => 'var1-value', - ], - 'service_id' => 1, - 'option_id' => 1, - 'startup' => 'startup-param', - 'docker_image' => 'some/image', - ]; - $this->validatorService->shouldReceive('isAdmin')->withNoArgs()->once()->andReturnSelf() - ->shouldReceive('setFields')->with($data['environment'])->once()->andReturnSelf() - ->shouldReceive('validate')->with($data['option_id'])->once()->andReturnSelf(); + ->shouldReceive('setFields')->with($this->data['environment'])->once()->andReturnSelf() + ->shouldReceive('validate')->with($this->data['option_id'])->once()->andReturnSelf(); $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); $this->uuid->shouldReceive('uuid4')->withNoArgs()->once()->andReturnSelf() ->shouldReceive('toString')->withNoArgs()->once()->andReturn('uuid-0000'); - $this->usernameService->shouldReceive('generate')->with($data['name'], 'randomstring') + $this->usernameService->shouldReceive('generate')->with($this->data['name'], 'randomstring') ->once()->andReturn('user_name'); $this->repository->shouldReceive('create')->with([ 'uuid' => 'uuid-0000', 'uuidShort' => 'randomstring', - 'node_id' => $data['node_id'], - 'name' => $data['name'], - 'description' => $data['description'], + 'node_id' => $this->data['node_id'], + 'name' => $this->data['name'], + 'description' => $this->data['description'], 'skip_scripts' => false, 'suspended' => false, - 'owner_id' => $data['owner_id'], - 'memory' => $data['memory'], - 'swap' => $data['swap'], - 'disk' => $data['disk'], - 'io' => $data['io'], - 'cpu' => $data['cpu'], + 'owner_id' => $this->data['owner_id'], + 'memory' => $this->data['memory'], + 'swap' => $this->data['swap'], + 'disk' => $this->data['disk'], + 'io' => $this->data['io'], + 'cpu' => $this->data['cpu'], 'oom_disabled' => false, - 'allocation_id' => $data['allocation_id'], - 'service_id' => $data['service_id'], - 'option_id' => $data['option_id'], + 'allocation_id' => $this->data['allocation_id'], + 'service_id' => $this->data['service_id'], + 'option_id' => $this->data['option_id'], 'pack_id' => null, - 'startup' => $data['startup'], + 'startup' => $this->data['startup'], 'daemonSecret' => 'randomstring', - 'image' => $data['docker_image'], + 'image' => $this->data['docker_image'], 'username' => 'user_name', 'sftp_password' => null, ])->once()->andReturn((object) [ @@ -208,7 +219,7 @@ class CreationServiceTest extends TestCase 'id' => 1, ]); - $this->allocationRepository->shouldReceive('assignAllocationsToServer')->with(1, [1, 2, 3]); + $this->allocationRepository->shouldReceive('assignAllocationsToServer')->with(1, [1, 2, 3])->once()->andReturnNull(); $this->validatorService->shouldReceive('getResults')->withNoArgs()->once()->andReturn([[ 'id' => 1, 'key' => 'TEST_VAR_1', @@ -224,9 +235,40 @@ class CreationServiceTest extends TestCase ->shouldReceive('create')->with(1)->once()->andReturnNull(); $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); - $response = $this->service->create($data); + $response = $this->service->create($this->data); $this->assertEquals(1, $response->id); $this->assertEquals(1, $response->node_id); } + + /** + * Test handling of node timeout or other daemon error. + */ + public function testExceptionShouldBeThrownIfTheRequestFails() + { + $this->validatorService->shouldReceive('isAdmin->setFields->validate->getResults')->once()->andReturn([]); + $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->uuid->shouldReceive('uuid4->toString')->once()->andReturn('uuid-0000'); + $this->usernameService->shouldReceive('generate')->once()->andReturn('user_name'); + $this->repository->shouldReceive('create')->once()->andReturn((object) [ + 'node_id' => 1, + 'id' => 1, + ]); + + $this->allocationRepository->shouldReceive('assignAllocationsToServer')->once()->andReturnNull(); + $this->serverVariableRepository->shouldReceive('insert')->with([])->once()->andReturnNull(); + $this->daemonServerRepository->shouldReceive('setNode->create')->once()->andThrow($this->exception); + $this->exception->shouldReceive('getResponse')->withNoArgs()->once()->andReturnNull(); + $this->writer->shouldReceive('warning')->with($this->exception)->once()->andReturnNull(); + $this->database->shouldReceive('rollBack')->withNoArgs()->once()->andReturnNull(); + + try { + $this->service->create($this->data); + } catch (Exception $exception) { + $this->assertInstanceOf(DisplayException::class, $exception); + $this->assertEquals(trans('admin/server.exceptions.daemon_exception', [ + 'code' => 'E_CONN_REFUSED', + ]), $exception->getMessage()); + } + } } diff --git a/tests/Unit/Services/Servers/DeletionServiceTest.php b/tests/Unit/Services/Servers/DeletionServiceTest.php new file mode 100644 index 00000000..37295ed8 --- /dev/null +++ b/tests/Unit/Services/Servers/DeletionServiceTest.php @@ -0,0 +1,213 @@ +. + * + * 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\Servers; + +use Exception; +use GuzzleHttp\Exception\RequestException; +use Illuminate\Log\Writer; +use Mockery as m; +use Illuminate\Database\ConnectionInterface; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Models\Server; +use Pterodactyl\Services\Database\DatabaseManagementService; +use Pterodactyl\Services\Servers\DeletionService; +use Tests\TestCase; + +class DeletionServiceTest extends TestCase +{ + /** + * @var \Illuminate\Database\ConnectionInterface + */ + protected $connection; + + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface + */ + protected $daemonServerRepository; + + /** + * @var \Pterodactyl\Services\Database\DatabaseManagementService + */ + protected $databaseManagementService; + + /** + * @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface + */ + protected $databaseRepository; + + /** + * @var \GuzzleHttp\Exception\RequestException + */ + protected $exception; + + /** + * @var \Pterodactyl\Models\Server + */ + protected $model; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Servers\DeletionService + */ + protected $service; + + /** + * @var \Illuminate\Log\Writer + */ + protected $writer; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->connection = m::mock(ConnectionInterface::class); + $this->daemonServerRepository = m::mock(DaemonServerRepositoryInterface::class); + $this->databaseRepository = m::mock(DatabaseRepositoryInterface::class); + $this->databaseManagementService = m::mock(DatabaseManagementService::class); + $this->exception = m::mock(RequestException::class); + $this->model = factory(Server::class)->make(); + $this->repository = m::mock(ServerRepositoryInterface::class); + $this->writer = m::mock(Writer::class); + + $this->service = new DeletionService( + $this->connection, + $this->daemonServerRepository, + $this->databaseRepository, + $this->databaseManagementService, + $this->repository, + $this->writer + ); + } + + /** + * Test that a server can be force deleted by setting it in a function call. + */ + public function testForceParameterCanBeSet() + { + $response = $this->service->withForce(true); + + $this->assertInstanceOf(DeletionService::class, $response); + } + + /** + * Test that a server can be deleted when force is not set. + */ + public function testServerCanBeDeletedWithoutForce() + { + $this->daemonServerRepository->shouldReceive('setNode')->with($this->model->node_id)->once()->andReturnSelf() + ->shouldReceive('setAccessServer')->with($this->model->uuid)->once()->andReturnSelf() + ->shouldReceive('delete')->withNoArgs()->once()->andReturnNull(); + + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->databaseRepository->shouldReceive('withColumns')->with('id')->once()->andReturnSelf() + ->shouldReceive('findWhere')->with([ + ['server_id', '=', $this->model->id], + ])->once()->andReturn(collect([(object) ['id' => 50]])); + + $this->databaseManagementService->shouldReceive('delete')->with(50)->once()->andReturnNull(); + $this->repository->shouldReceive('delete')->with($this->model->id)->once()->andReturnNull(); + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $this->service->handle($this->model); + } + + /** + * Test that a server is deleted when force is set. + */ + public function testServerShouldBeDeletedEvenWhenFailureOccursIfForceIsSet() + { + $this->daemonServerRepository->shouldReceive('setNode')->with($this->model->node_id)->once()->andReturnSelf() + ->shouldReceive('setAccessServer')->with($this->model->uuid)->once()->andReturnSelf() + ->shouldReceive('delete')->withNoArgs()->once()->andThrow($this->exception); + + $this->exception->shouldReceive('getResponse')->withNoArgs()->once()->andReturnNull(); + $this->writer->shouldReceive('warning')->with($this->exception)->once()->andReturnNull(); + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->databaseRepository->shouldReceive('withColumns')->with('id')->once()->andReturnSelf() + ->shouldReceive('findWhere')->with([ + ['server_id', '=', $this->model->id], + ])->once()->andReturn(collect([(object) ['id' => 50]])); + + $this->databaseManagementService->shouldReceive('delete')->with(50)->once()->andReturnNull(); + $this->repository->shouldReceive('delete')->with($this->model->id)->once()->andReturnNull(); + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $this->service->withForce()->handle($this->model); + } + + /** + * Test that an exception is thrown if a server cannot be deleted from the node and force is not set. + */ + public function testExceptionShouldBeThrownIfDaemonReturnsAnErrorAndForceIsNotSet() + { + $this->daemonServerRepository->shouldReceive('setNode->setAccessServer->delete')->once()->andThrow($this->exception); + $this->exception->shouldReceive('getResponse')->withNoArgs()->once()->andReturnNull(); + $this->writer->shouldReceive('warning')->with($this->exception)->once()->andReturnNull(); + + try { + $this->service->handle($this->model); + } catch (Exception $exception) { + $this->assertInstanceOf(DisplayException::class, $exception); + $this->assertEquals(trans('admin/server.exceptions.daemon_exception', [ + 'code' => 'E_CONN_REFUSED', + ]), $exception->getMessage()); + } + } + + /** + * Test that an integer can be passed in place of the Server model. + */ + public function testIntegerCanBePassedInPlaceOfServerModel() + { + $this->repository->shouldReceive('withColumns')->with(['id', 'node_id', 'uuid'])->once()->andReturnSelf() + ->shouldReceive('find')->with($this->model->id)->once()->andReturn($this->model); + + $this->daemonServerRepository->shouldReceive('setNode')->with($this->model->node_id)->once()->andReturnSelf() + ->shouldReceive('setAccessServer')->with($this->model->uuid)->once()->andReturnSelf() + ->shouldReceive('delete')->withNoArgs()->once()->andReturnNull(); + + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->databaseRepository->shouldReceive('withColumns')->with('id')->once()->andReturnSelf() + ->shouldReceive('findWhere')->with([ + ['server_id', '=', $this->model->id], + ])->once()->andReturn(collect([(object) ['id' => 50]])); + + $this->databaseManagementService->shouldReceive('delete')->with(50)->once()->andReturnNull(); + $this->repository->shouldReceive('delete')->with($this->model->id)->once()->andReturnNull(); + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $this->service->handle($this->model->id); + } +} diff --git a/tests/Unit/Services/Services/Variables/VariableCreationServiceTest.php b/tests/Unit/Services/Services/Variables/VariableCreationServiceTest.php new file mode 100644 index 00000000..320f06e7 --- /dev/null +++ b/tests/Unit/Services/Services/Variables/VariableCreationServiceTest.php @@ -0,0 +1,143 @@ +. + * + * 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\Services\Variables; + +use Mockery as m; +use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface; +use Pterodactyl\Exceptions\Services\ServiceVariable\ReservedVariableNameException; +use Pterodactyl\Models\ServiceOption; +use Pterodactyl\Models\ServiceVariable; +use Pterodactyl\Services\Services\Variables\VariableCreationService; +use Tests\TestCase; + +class VariableCreationServiceTest extends TestCase +{ + /** + * @var \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface + */ + protected $serviceOptionRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface + */ + protected $serviceVariableRepository; + + /** + * @var \Pterodactyl\Services\Services\Variables\VariableCreationService + */ + protected $service; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->serviceOptionRepository = m::mock(ServiceOptionRepositoryInterface::class); + $this->serviceVariableRepository = m::mock(ServiceVariableRepositoryInterface::class); + + $this->service = new VariableCreationService($this->serviceOptionRepository, $this->serviceVariableRepository); + } + + /** + * Test basic functionality, data should be stored in the database. + */ + public function testVariableIsCreatedAndStored() + { + $data = ['env_variable' => 'TEST_VAR_123']; + $this->serviceVariableRepository->shouldReceive('create')->with([ + 'option_id' => 1, + 'user_viewable' => false, + 'user_editable' => false, + 'env_variable' => 'TEST_VAR_123', + ])->once()->andReturn(new ServiceVariable); + + $this->assertInstanceOf(ServiceVariable::class, $this->service->handle(1, $data)); + } + + /** + * Test that the option key in the data array is properly parsed. + */ + public function testOptionsPassedInArrayKeyAreParsedProperly() + { + $data = ['env_variable' => 'TEST_VAR_123', 'options' => ['user_viewable', 'user_editable']]; + $this->serviceVariableRepository->shouldReceive('create')->with([ + 'option_id' => 1, + 'user_viewable' => true, + 'user_editable' => true, + 'env_variable' => 'TEST_VAR_123', + 'options' => ['user_viewable', 'user_editable'], + ])->once()->andReturn(new ServiceVariable); + + $this->assertInstanceOf(ServiceVariable::class, $this->service->handle(1, $data)); + } + + /** + * Test that all of the reserved variables defined in the model trigger an exception. + * + * @dataProvider reservedNamesProvider + * @expectedException \Pterodactyl\Exceptions\Services\ServiceVariable\ReservedVariableNameException + */ + public function testExceptionIsThrownIfEnvironmentVariableIsInListOfReservedNames($variable) + { + $this->service->handle(1, ['env_variable' => $variable]); + } + + /** + * Test that a model can be passed in place of an integer. + */ + public function testModelCanBePassedInPlaceOfInteger() + { + $model = factory(ServiceOption::class)->make(); + $data = ['env_variable' => 'TEST_VAR_123']; + + $this->serviceVariableRepository->shouldReceive('create')->with([ + 'option_id' => $model->id, + 'user_viewable' => false, + 'user_editable' => false, + 'env_variable' => 'TEST_VAR_123', + ])->once()->andReturn(new ServiceVariable); + + $this->assertInstanceOf(ServiceVariable::class, $this->service->handle($model, $data)); + } + + /** + * Provides the data to be used in the tests. + * + * @return array + */ + public function reservedNamesProvider() + { + $data = []; + $exploded = explode(',', ServiceVariable::RESERVED_ENV_NAMES); + foreach ($exploded as $e) { + $data[] = [$e]; + } + + return $data; + } +} diff --git a/tests/Unit/Services/Services/Variables/VariableUpdateServiceTest.php b/tests/Unit/Services/Services/Variables/VariableUpdateServiceTest.php new file mode 100644 index 00000000..5e067387 --- /dev/null +++ b/tests/Unit/Services/Services/Variables/VariableUpdateServiceTest.php @@ -0,0 +1,151 @@ +. + * + * 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\Services\Variables; + +use Exception; +use Mockery as m; +use PhpParser\Node\Expr\Variable; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Models\ServiceVariable; +use Pterodactyl\Services\Services\Variables\VariableUpdateService; +use Tests\TestCase; +use Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface; + +class VariableUpdateServiceTest extends TestCase +{ + /** + * @var \Pterodactyl\Models\ServiceVariable + */ + protected $model; + + /** + * @var \Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Services\Variables\VariableUpdateService + */ + protected $service; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->model = factory(ServiceVariable::class)->make(); + $this->repository = m::mock(ServiceVariableRepositoryInterface::class); + + $this->service = new VariableUpdateService($this->repository); + } + + /** + * Test the function when no env_variable key is passed into the function. + */ + public function testVariableIsUpdatedWhenNoEnvironmentVariableIsPassed() + { + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($this->model->id, [ + 'user_viewable' => false, + 'user_editable' => false, + 'test-data' => 'test-value', + ])->once()->andReturn(true); + + $this->assertTrue($this->service->handle($this->model, ['test-data' => 'test-value'])); + } + + /** + * Test the function when a valid env_variable key is passed into the function. + */ + public function testVariableIsUpdatedWhenValidEnvironmentVariableIsPassed() + { + $this->repository->shouldReceive('withColumns')->with('id')->once()->andReturnSelf() + ->shouldReceive('findCountWhere')->with([ + ['env_variable', '=', 'TEST_VAR_123'], + ['option_id', '=', $this->model->option_id], + ['id', '!=', $this->model->id] + ])->once()->andReturn(0); + + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($this->model->id, [ + 'user_viewable' => false, + 'user_editable' => false, + 'env_variable' => 'TEST_VAR_123', + ])->once()->andReturn(true); + + $this->assertTrue($this->service->handle($this->model, ['env_variable' => 'TEST_VAR_123'])); + } + + /** + * Test that a non-unique environment variable triggers an exception. + */ + public function testExceptionIsThrownIfEnvironmentVariableIsNotUnique() + { + $this->repository->shouldReceive('withColumns')->with('id')->once()->andReturnSelf() + ->shouldReceive('findCountWhere')->with([ + ['env_variable', '=', 'TEST_VAR_123'], + ['option_id', '=', $this->model->option_id], + ['id', '!=', $this->model->id] + ])->once()->andReturn(1); + + try { + $this->service->handle($this->model, ['env_variable' => 'TEST_VAR_123']); + } catch (Exception $exception) { + $this->assertInstanceOf(DisplayException::class, $exception); + $this->assertEquals(trans('admin/exceptions.service.variables.env_not_unique', [ + 'name' => 'TEST_VAR_123', + ]), $exception->getMessage()); + } + } + + /** + * Test that all of the reserved variables defined in the model trigger an exception. + * + * @dataProvider reservedNamesProvider + * @expectedException \Pterodactyl\Exceptions\Services\ServiceVariable\ReservedVariableNameException + */ + public function testExceptionIsThrownIfEnvironmentVariableIsInListOfReservedNames($variable) + { + $this->service->handle($this->model, ['env_variable' => $variable]); + } + + /** + * Provides the data to be used in the tests. + * + * @return array + */ + public function reservedNamesProvider() + { + $data = []; + $exploded = explode(',', ServiceVariable::RESERVED_ENV_NAMES); + foreach ($exploded as $e) { + $data[] = [$e]; + } + + return $data; + } +} From e91079d128109717ab4f077eef075f3faf24cc28 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 13 Aug 2017 14:56:47 -0500 Subject: [PATCH 51/99] use travis caches again --- .travis.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4ebd5b84..2e93a545 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,10 +4,9 @@ php: - '7.0' - '7.1' sudo: false -cache: false -#cache: -# directories: -# - $HOME/.composer/cache +cache: + directories: + - $HOME/.composer/cache services: - mysql before_install: From 90bbe571487e62dbf93935072171fa1cf27cf06f Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 15 Aug 2017 22:21:47 -0500 Subject: [PATCH 52/99] Move services onto new services system, includes tests --- .travis.yml | 1 - .../Repository/ServiceRepositoryInterface.php | 8 + .../HasActiveServersException.php | 2 +- .../Controllers/Admin/OptionController.php | 9 +- .../Controllers/Admin/ServiceController.php | 172 +++++++++++------- .../Admin/Service/ServiceFormRequest.php | 52 ++++++ .../Service/ServiceFunctionsFormRequest.php | 40 ++++ app/Models/Service.php | 71 +++----- .../Eloquent/EloquentRepository.php | 24 +-- .../Eloquent/ServiceRepository.php | 19 +- app/Repositories/Old/ServiceRepository.php | 135 -------------- .../Options/OptionDeletionService.php | 4 +- .../Services/ServiceCreationService.php | 79 ++++++++ .../Services/ServiceDeletionService.php | 74 ++++++++ .../Services/ServiceUpdateService.php | 62 +++++++ app/Traits/Services/CreatesServiceIndex.php | 71 ++++++++ database/factories/ModelFactory.php | 11 ++ ...adeDeletionWhenAParentServiceIsDeleted.php | 36 ++++ resources/lang/en/admin/exceptions.php | 1 + resources/lang/en/admin/services.php | 7 + .../admin/services/functions.blade.php | 5 +- routes/admin.php | 11 +- .../Options/OptionDeletionServiceTest.php | 2 +- .../Services/ServiceCreationServiceTest.php | 94 ++++++++++ .../Services/ServiceDeletionServiceTest.php | 104 +++++++++++ .../Services/ServiceUpdateServiceTest.php | 77 ++++++++ 26 files changed, 899 insertions(+), 272 deletions(-) rename app/Exceptions/Services/{ServiceOption => }/HasActiveServersException.php (95%) create mode 100644 app/Http/Requests/Admin/Service/ServiceFormRequest.php create mode 100644 app/Http/Requests/Admin/Service/ServiceFunctionsFormRequest.php delete mode 100644 app/Repositories/Old/ServiceRepository.php create mode 100644 app/Services/Services/ServiceCreationService.php create mode 100644 app/Services/Services/ServiceDeletionService.php create mode 100644 app/Services/Services/ServiceUpdateService.php create mode 100644 app/Traits/Services/CreatesServiceIndex.php create mode 100644 database/migrations/2017_08_15_214555_CascadeDeletionWhenAParentServiceIsDeleted.php create mode 100644 tests/Unit/Services/Services/ServiceCreationServiceTest.php create mode 100644 tests/Unit/Services/Services/ServiceDeletionServiceTest.php create mode 100644 tests/Unit/Services/Services/ServiceUpdateServiceTest.php diff --git a/.travis.yml b/.travis.yml index 2e93a545..960a8c19 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,6 @@ services: before_install: - mysql -e 'CREATE DATABASE IF NOT EXISTS travis;' before_script: -# - phpenv config-rm xdebug.ini - cp .env.travis .env - composer install --no-interaction --prefer-dist --no-suggest --verbose - php artisan migrate --seed -v diff --git a/app/Contracts/Repository/ServiceRepositoryInterface.php b/app/Contracts/Repository/ServiceRepositoryInterface.php index def30c95..7459d1c4 100644 --- a/app/Contracts/Repository/ServiceRepositoryInterface.php +++ b/app/Contracts/Repository/ServiceRepositoryInterface.php @@ -33,4 +33,12 @@ interface ServiceRepositoryInterface extends RepositoryInterface * @return \Illuminate\Support\Collection */ public function getWithOptions($id = null); + + /** + * Return a service along with its associated options and the servers relation on those options. + * + * @param int $id + * @return mixed + */ + public function getWithOptionServers($id); } diff --git a/app/Exceptions/Services/ServiceOption/HasActiveServersException.php b/app/Exceptions/Services/HasActiveServersException.php similarity index 95% rename from app/Exceptions/Services/ServiceOption/HasActiveServersException.php rename to app/Exceptions/Services/HasActiveServersException.php index e1ea03b3..d560f568 100644 --- a/app/Exceptions/Services/ServiceOption/HasActiveServersException.php +++ b/app/Exceptions/Services/HasActiveServersException.php @@ -22,7 +22,7 @@ * SOFTWARE. */ -namespace Pterodactyl\Exceptions\Services\ServiceOption; +namespace Pterodactyl\Exceptions\Services; class HasActiveServersException extends \Exception { diff --git a/app/Http/Controllers/Admin/OptionController.php b/app/Http/Controllers/Admin/OptionController.php index fa994d8b..184f90f9 100644 --- a/app/Http/Controllers/Admin/OptionController.php +++ b/app/Http/Controllers/Admin/OptionController.php @@ -38,7 +38,7 @@ use Pterodactyl\Http\Requests\Admin\Service\ServiceOptionFormRequest; use Pterodactyl\Services\Services\Options\InstallScriptUpdateService; use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; use Pterodactyl\Exceptions\Services\ServiceOption\InvalidCopyFromException; -use Pterodactyl\Exceptions\Services\ServiceOption\HasActiveServersException; +use Pterodactyl\Exceptions\Services\HasActiveServersException; use Pterodactyl\Exceptions\Services\ServiceOption\NoParentConfigurationFoundException; class OptionController extends Controller @@ -145,14 +145,14 @@ class OptionController extends Controller /** * Delete a given option from the database. * - * @param \Pterodactyl\Models\ServiceOption $option + * @param \Pterodactyl\Models\ServiceOption $option * @return \Illuminate\Http\RedirectResponse */ - public function delete(ServiceOption $option) + public function destroy(ServiceOption $option) { try { $this->optionDeletionService->handle($option->id); - $this->alert->success()->flash(); + $this->alert->success(trans('admin/services.options.notices.option_deleted'))->flash(); } catch (HasActiveServersException $exception) { $this->alert->danger($exception->getMessage())->flash(); @@ -229,6 +229,7 @@ class OptionController extends Controller * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function updateScripts(EditOptionScript $request, ServiceOption $option) { diff --git a/app/Http/Controllers/Admin/ServiceController.php b/app/Http/Controllers/Admin/ServiceController.php index beaac534..ae10cb34 100644 --- a/app/Http/Controllers/Admin/ServiceController.php +++ b/app/Http/Controllers/Admin/ServiceController.php @@ -24,37 +24,76 @@ namespace Pterodactyl\Http\Controllers\Admin; -use Log; -use Alert; -use Pterodactyl\Models; -use Illuminate\Http\Request; -use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Models\Service; +use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Repositories\ServiceRepository; -use Pterodactyl\Exceptions\DisplayValidationException; +use Pterodactyl\Services\Services\ServiceUpdateService; +use Pterodactyl\Services\Services\ServiceCreationService; +use Pterodactyl\Services\Services\ServiceDeletionService; +use Pterodactyl\Exceptions\Services\HasActiveServersException; +use Pterodactyl\Http\Requests\Admin\Service\ServiceFormRequest; +use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; +use Pterodactyl\Http\Requests\Admin\Service\ServiceFunctionsFormRequest; class ServiceController extends Controller { + /** + * @var \Prologue\Alerts\AlertsMessageBag + */ + protected $alert; + + /** + * @var \Pterodactyl\Services\Services\ServiceCreationService + */ + protected $creationService; + + /** + * @var \Pterodactyl\Services\Services\ServiceDeletionService + */ + protected $deletionService; + + /** + * @var \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Services\ServiceUpdateService + */ + protected $updateService; + + public function __construct( + AlertsMessageBag $alert, + ServiceCreationService $creationService, + ServiceDeletionService $deletionService, + ServiceRepositoryInterface $repository, + ServiceUpdateService $updateService + ) { + $this->alert = $alert; + $this->creationService = $creationService; + $this->deletionService = $deletionService; + $this->repository = $repository; + $this->updateService = $updateService; + } + /** * Display service overview page. * - * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ - public function index(Request $request) + public function index() { return view('admin.services.index', [ - 'services' => Models\Service::withCount('servers', 'options', 'packs')->get(), + 'services' => $this->repository->getWithOptions(), ]); } /** * Display create service page. * - * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ - public function create(Request $request) + public function create() { return view('admin.services.new'); } @@ -62,91 +101,96 @@ class ServiceController extends Controller /** * Return base view for a service. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param int $service * @return \Illuminate\View\View */ - public function view(Request $request, $id) + public function view($service) { return view('admin.services.view', [ - 'service' => Models\Service::with('options', 'options.servers')->findOrFail($id), + 'service' => $this->repository->getWithOptionServers($service), ]); } /** * Return function editing view for a service. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Pterodactyl\Models\Service $service * @return \Illuminate\View\View */ - public function viewFunctions(Request $request, $id) + public function viewFunctions(Service $service) { - return view('admin.services.functions', ['service' => Models\Service::findOrFail($id)]); + return view('admin.services.functions', ['service' => $service]); } /** * Handle post action for new service. * - * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Http\Requests\Admin\Service\ServiceFormRequest $request * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function store(Request $request) + public function store(ServiceFormRequest $request) { - $repo = new ServiceRepository; + $service = $this->creationService->handle($request->normalize()); + $this->alert->success(trans('admin/services.notices.service_created', ['name' => $service->name]))->flash(); - try { - $service = $repo->create($request->intersect([ - 'name', 'description', 'folder', 'startup', - ])); - Alert::success('Successfully created new service!')->flash(); - - return redirect()->route('admin.services.view', $service->id); - } catch (DisplayValidationException $ex) { - return redirect()->route('admin.services.new')->withErrors(json_decode($ex->getMessage()))->withInput(); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An error occured while attempting to add a new service. This error has been logged.')->flash(); - } - - return redirect()->route('admin.services.new')->withInput(); + return redirect()->route('admin.services.view', $service->id); } /** * Edits configuration for a specific service. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Pterodactyl\Http\Requests\Admin\Service\ServiceFormRequest $request + * @param \Pterodactyl\Models\Service $service + * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function update(ServiceFormRequest $request, Service $service) + { + $this->updateService->handle($service->id, $request->normalize()); + $this->alert->success(trans('admin/services.notices.service_updated'))->flash(); + + return redirect()->route('admin.services.view', $service); + } + + /** + * Update the functions file for a service. + * + * @param \Pterodactyl\Http\Requests\Admin\Service\ServiceFunctionsFormRequest $request + * @param \Pterodactyl\Models\Service $service + * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function updateFunctions(ServiceFunctionsFormRequest $request, Service $service) + { + $this->updateService->handle($service->id, $request->normalize()); + $this->alert->success(trans('admin/services.notices.functions_updated'))->flash(); + + return redirect()->route('admin.services.view.functions', $service->id); + } + + /** + * Delete a service from the panel. + * + * @param \Pterodactyl\Models\Service $service * @return \Illuminate\Http\RedirectResponse */ - public function edit(Request $request, $id) + public function destroy(Service $service) { - $repo = new ServiceRepository; - $redirectTo = ($request->input('redirect_to')) ? 'admin.services.view.functions' : 'admin.services.view'; - try { - if ($request->input('action') !== 'delete') { - $repo->update($id, $request->intersect([ - 'name', 'description', 'folder', 'startup', 'index_file', - ])); - Alert::success('Service has been updated successfully.')->flash(); - } else { - $repo->delete($id); - Alert::success('Successfully deleted service from the system.')->flash(); + $this->deletionService->handle($service->id); + $this->alert->success(trans('admin/services.notices.service_deleted'))->flash(); + } catch (HasActiveServersException $exception) { + $this->alert->danger($exception->getMessage())->flash(); - return redirect()->route('admin.services'); - } - } catch (DisplayValidationException $ex) { - return redirect()->route($redirectTo, $id)->withErrors(json_decode($ex->getMessage()))->withInput(); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An error occurred while attempting to update this service. This error has been logged.')->flash(); + return redirect()->back(); } - return redirect()->route($redirectTo, $id); + return redirect()->route('admin.services'); } } diff --git a/app/Http/Requests/Admin/Service/ServiceFormRequest.php b/app/Http/Requests/Admin/Service/ServiceFormRequest.php new file mode 100644 index 00000000..6d6be5e1 --- /dev/null +++ b/app/Http/Requests/Admin/Service/ServiceFormRequest.php @@ -0,0 +1,52 @@ +. + * + * 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\Http\Requests\Admin\Service; + +use Pterodactyl\Http\Requests\Admin\AdminFormRequest; + +class ServiceFormRequest extends AdminFormRequest +{ + /** + * @return array + */ + public function rules() + { + $rules = [ + 'name' => 'required|string|min:1|max:255', + 'description' => 'required|nullable|string', + 'folder' => 'required|regex:/^[\w.-]{1,50}$/|unique:services,folder', + 'startup' => 'required|nullable|string', + ]; + + if ($this->method() === 'PATCH') { + $service = $this->route()->parameter('service'); + $rules['folder'] = $rules['folder'] . ',' . $service->id; + + return $rules; + } + + return $rules; + } +} diff --git a/app/Http/Requests/Admin/Service/ServiceFunctionsFormRequest.php b/app/Http/Requests/Admin/Service/ServiceFunctionsFormRequest.php new file mode 100644 index 00000000..d5836c23 --- /dev/null +++ b/app/Http/Requests/Admin/Service/ServiceFunctionsFormRequest.php @@ -0,0 +1,40 @@ +. + * + * 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\Http\Requests\Admin\Service; + +use Pterodactyl\Http\Requests\Admin\AdminFormRequest; + +class ServiceFunctionsFormRequest extends AdminFormRequest +{ + /** + * @return array + */ + public function rules() + { + return [ + 'index_file' => 'required|nullable|string', + ]; + } +} diff --git a/app/Models/Service.php b/app/Models/Service.php index b0536688..25355981 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -24,10 +24,16 @@ namespace Pterodactyl\Models; +use Sofa\Eloquence\Eloquence; +use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; +use Sofa\Eloquence\Contracts\CleansAttributes; +use Sofa\Eloquence\Contracts\Validable as ValidableContract; -class Service extends Model +class Service extends Model implements CleansAttributes, ValidableContract { + use Eloquence, Validable; + /** * The table associated with the model. * @@ -40,52 +46,31 @@ class Service extends Model * * @var array */ - protected $fillable = [ - 'name', 'description', 'folder', 'startup', 'index_file', + protected $fillable = ['name', 'author', 'description', 'folder', 'startup', 'index_file']; + + /** + * @var array + */ + protected static $applicationRules = [ + 'author' => 'required', + 'name' => 'required', + 'description' => 'sometimes', + 'folder' => 'required', + 'startup' => 'sometimes', + 'index_file' => 'required', ]; /** - * Returns the default contents of the index.js file for a service. - * - * @return string + * @var array */ - public static function defaultIndexFile() - { - return <<<'EOF' -'use strict'; - -/** - * Pterodactyl - Daemon - * Copyright (c) 2015 - 2017 Dane Everitt - * - * 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. - */ -const rfr = require('rfr'); -const _ = require('lodash'); - -const Core = rfr('src/services/index.js'); - -class Service extends Core {} - -module.exports = Service; -EOF; - } + protected static $dataIntegrityRules = [ + 'author' => 'string|size:36', + 'name' => 'string|max:255', + 'description' => 'nullable|string', + 'folder' => 'string|max:255|regex:/^[\w.-]{1,50}$/|unique:services,folder', + 'startup' => 'nullable|string', + 'index_file' => 'string', + ]; /** * Gets all service options associated with this service. diff --git a/app/Repositories/Eloquent/EloquentRepository.php b/app/Repositories/Eloquent/EloquentRepository.php index 0dd1beee..fce97403 100644 --- a/app/Repositories/Eloquent/EloquentRepository.php +++ b/app/Repositories/Eloquent/EloquentRepository.php @@ -49,8 +49,8 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf */ public function create(array $fields, $validate = true, $force = false) { - Assert::boolean($validate, 'Second argument passed to create should be boolean, recieved %s.'); - Assert::boolean($force, 'Third argument passed to create should be boolean, received %s.'); + Assert::boolean($validate, 'Second argument passed to create must be boolean, recieved %s.'); + Assert::boolean($force, 'Third argument passed to create must be boolean, received %s.'); $instance = $this->getBuilder()->newModelInstance(); @@ -77,7 +77,7 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf */ public function find($id) { - Assert::integer($id, 'First argument passed to find should be integer, received %s.'); + Assert::numeric($id, 'First argument passed to find must be numeric, received %s.'); $instance = $this->getBuilder()->find($id, $this->getColumns()); @@ -125,8 +125,8 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf */ public function delete($id, $destroy = false) { - Assert::integer($id, 'First argument passed to delete should be integer, received %s.'); - Assert::boolean($destroy, 'Second argument passed to delete should be boolean, received %s.'); + Assert::numeric($id, 'First argument passed to delete must be numeric, received %s.'); + Assert::boolean($destroy, 'Second argument passed to delete must be boolean, received %s.'); $instance = $this->getBuilder()->where($this->getModel()->getKeyName(), $id); @@ -138,7 +138,7 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf */ public function deleteWhere(array $attributes, $force = false) { - Assert::boolean($force, 'Second argument passed to deleteWhere should be boolean, received %s.'); + Assert::boolean($force, 'Second argument passed to deleteWhere must be boolean, received %s.'); $instance = $this->getBuilder()->where($attributes); @@ -150,9 +150,9 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf */ public function update($id, array $fields, $validate = true, $force = false) { - Assert::integer($id, 'First argument passed to update expected to be integer, received %s.'); - Assert::boolean($validate, 'Third argument passed to update should be boolean, received %s.'); - Assert::boolean($force, 'Fourth argument passed to update should be boolean, received %s.'); + Assert::numeric($id, 'First argument passed to update must be numeric, received %s.'); + Assert::boolean($validate, 'Third argument passed to update must be boolean, received %s.'); + Assert::boolean($force, 'Fourth argument passed to update must be boolean, received %s.'); $instance = $this->getBuilder()->where('id', $id)->first(); @@ -182,7 +182,7 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf */ public function updateWhereIn($column, array $values, array $fields) { - Assert::stringNotEmpty($column, 'First argument passed to updateWhereIn expected to be a string, received %s.'); + Assert::stringNotEmpty($column, 'First argument passed to updateWhereIn must be a non-empty string, received %s.'); return $this->getBuilder()->whereIn($column, $values)->update($fields); } @@ -255,8 +255,8 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf */ public function updateOrCreate(array $where, array $fields, $validate = true, $force = false) { - Assert::boolean($validate, 'Third argument passed to updateOrCreate should be boolean, received %s.'); - Assert::boolean($force, 'Fourth argument passed to updateOrCreate should be boolean, received %s.'); + Assert::boolean($validate, 'Third argument passed to updateOrCreate must be boolean, received %s.'); + Assert::boolean($force, 'Fourth argument passed to updateOrCreate must be boolean, received %s.'); $instance = $this->withColumns('id')->findWhere($where)->first(); diff --git a/app/Repositories/Eloquent/ServiceRepository.php b/app/Repositories/Eloquent/ServiceRepository.php index aca36490..9e2cd408 100644 --- a/app/Repositories/Eloquent/ServiceRepository.php +++ b/app/Repositories/Eloquent/ServiceRepository.php @@ -24,6 +24,7 @@ namespace Pterodactyl\Repositories\Eloquent; +use Webmozart\Assert\Assert; use Pterodactyl\Models\Service; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; @@ -43,11 +44,12 @@ class ServiceRepository extends EloquentRepository implements ServiceRepositoryI */ public function getWithOptions($id = null) { + Assert::nullOrNumeric($id, 'First argument passed to getWithOptions must be null or numeric, received %s.'); + $instance = $this->getBuilder()->with('options.packs', 'options.variables'); if (! is_null($id)) { $instance = $instance->find($id, $this->getColumns()); - if (! $instance) { throw new RecordNotFoundException(); } @@ -57,4 +59,19 @@ class ServiceRepository extends EloquentRepository implements ServiceRepositoryI return $instance->get($this->getColumns()); } + + /** + * {@inheritdoc} + */ + public function getWithOptionServers($id) + { + Assert::numeric($id, 'First argument passed to getWithOptionServers must be numeric, received %s.'); + + $instance = $this->getBuilder()->with('options.servers')->find($id, $this->getColumns()); + if (! $instance) { + throw new RecordNotFoundException(); + } + + return $instance; + } } diff --git a/app/Repositories/Old/ServiceRepository.php b/app/Repositories/Old/ServiceRepository.php deleted file mode 100644 index a0d1716c..00000000 --- a/app/Repositories/Old/ServiceRepository.php +++ /dev/null @@ -1,135 +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\Repositories; - -use DB; -use Validator; -use Pterodactyl\Models\Service; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Exceptions\DisplayValidationException; - -class ServiceRepository -{ - /** - * Creates a new service on the system. - * - * @param array $data - * @return \Pterodactyl\Models\Service - * - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function create(array $data) - { - $validator = Validator::make($data, [ - 'name' => 'required|string|min:1|max:255', - 'description' => 'required|nullable|string', - 'folder' => 'required|unique:services,folder|regex:/^[\w.-]{1,50}$/', - 'startup' => 'required|nullable|string', - ]); - - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - return DB::transaction(function () use ($data) { - $service = new Service; - $service->author = config('pterodactyl.service.author'); - $service->fill([ - 'name' => $data['name'], - 'description' => (isset($data['description'])) ? $data['description'] : null, - 'folder' => $data['folder'], - 'startup' => (isset($data['startup'])) ? $data['startup'] : null, - 'index_file' => Service::defaultIndexFile(), - ])->save(); - - // It is possible for an event to return false or throw an exception - // which won't necessarily be detected by this transaction. - // - // This check ensures the model was actually saved. - if (! $service->exists) { - throw new \Exception('Service model was created however the response appears to be invalid. Did an event fire wrongly?'); - } - - return $service; - }); - } - - /** - * Updates a service. - * - * @param int $id - * @param array $data - * @return \Pterodactyl\Models\Service - * - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function update($id, array $data) - { - $service = Service::findOrFail($id); - - $validator = Validator::make($data, [ - 'name' => 'sometimes|required|string|min:1|max:255', - 'description' => 'sometimes|required|nullable|string', - 'folder' => 'sometimes|required|regex:/^[\w.-]{1,50}$/', - 'startup' => 'sometimes|required|nullable|string', - 'index_file' => 'sometimes|required|string', - ]); - - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - return DB::transaction(function () use ($data, $service) { - $service->fill($data)->save(); - - return $service; - }); - } - - /** - * Deletes a service and associated files and options. - * - * @param int $id - * @return void - * - * @throws \Pterodactyl\Exceptions\DisplayException - */ - public function delete($id) - { - $service = Service::withCount('servers')->with('options')->findOrFail($id); - - if ($service->servers_count > 0) { - throw new DisplayException('You cannot delete a service that has servers associated with it.'); - } - - DB::transaction(function () use ($service) { - foreach ($service->options as $option) { - (new OptionRepository)->delete($option->id); - } - - $service->delete(); - }); - } -} diff --git a/app/Services/Services/Options/OptionDeletionService.php b/app/Services/Services/Options/OptionDeletionService.php index aa392f35..8afd6e2e 100644 --- a/app/Services/Services/Options/OptionDeletionService.php +++ b/app/Services/Services/Options/OptionDeletionService.php @@ -26,7 +26,7 @@ namespace Pterodactyl\Services\Services\Options; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; -use Pterodactyl\Exceptions\Services\ServiceOption\HasActiveServersException; +use Pterodactyl\Exceptions\Services\HasActiveServersException; class OptionDeletionService { @@ -60,7 +60,7 @@ class OptionDeletionService * @param int $option * @return int * - * @throws \Pterodactyl\Exceptions\Services\ServiceOption\HasActiveServersException + * @throws \Pterodactyl\Exceptions\Services\HasActiveServersException */ public function handle($option) { diff --git a/app/Services/Services/ServiceCreationService.php b/app/Services/Services/ServiceCreationService.php new file mode 100644 index 00000000..9e9b4e20 --- /dev/null +++ b/app/Services/Services/ServiceCreationService.php @@ -0,0 +1,79 @@ +. + * + * 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\Services; + +use Pterodactyl\Traits\Services\CreatesServiceIndex; +use Illuminate\Contracts\Config\Repository as ConfigRepository; +use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; + +class ServiceCreationService +{ + use CreatesServiceIndex; + + /** + * @var \Illuminate\Contracts\Config\Repository + */ + protected $config; + + /** + * @var \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface + */ + protected $repository; + + /** + * ServiceCreationService constructor. + * + * @param \Illuminate\Contracts\Config\Repository $config + * @param \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface $repository + */ + public function __construct( + ConfigRepository $config, + ServiceRepositoryInterface $repository + ) { + $this->config = $config; + $this->repository = $repository; + } + + /** + * Create a new service on the system. + * + * @param array $data + * @return \Pterodactyl\Models\Service + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function handle(array $data) + { + return $this->repository->create(array_merge([ + 'author' => $this->config->get('pterodactyl.service.author'), + ], [ + 'name' => array_get($data, 'name'), + 'description' => array_get($data, 'description'), + 'folder' => array_get($data, 'folder'), + 'startup' => array_get($data, 'startup'), + 'index_file' => $this->getIndexScript(), + ])); + } +} diff --git a/app/Services/Services/ServiceDeletionService.php b/app/Services/Services/ServiceDeletionService.php new file mode 100644 index 00000000..5d0405b1 --- /dev/null +++ b/app/Services/Services/ServiceDeletionService.php @@ -0,0 +1,74 @@ +. + * + * 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\Services; + +use Pterodactyl\Exceptions\Services\HasActiveServersException; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; + +class ServiceDeletionService +{ + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $serverRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface + */ + protected $repository; + + /** + * ServiceDeletionService constructor. + * + * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $serverRepository + * @param \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface $repository + */ + public function __construct( + ServerRepositoryInterface $serverRepository, + ServiceRepositoryInterface $repository + ) { + $this->serverRepository = $serverRepository; + $this->repository = $repository; + } + + /** + * Delete a service from the system only if there are no servers attached to it. + * + * @param int $service + * @return int + * + * @throws \Pterodactyl\Exceptions\Services\HasActiveServersException + */ + public function handle($service) + { + $count = $this->serverRepository->findCountWhere([['service_id', '=', $service]]); + if ($count > 0) { + throw new HasActiveServersException(trans('admin/exceptions.service.delete_has_servers')); + } + + return $this->repository->delete($service); + } +} diff --git a/app/Services/Services/ServiceUpdateService.php b/app/Services/Services/ServiceUpdateService.php new file mode 100644 index 00000000..203f52df --- /dev/null +++ b/app/Services/Services/ServiceUpdateService.php @@ -0,0 +1,62 @@ +. + * + * 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\Services; + +use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; + +class ServiceUpdateService +{ + /** + * @var \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface + */ + protected $repository; + + /** + * ServiceUpdateService constructor. + * + * @param \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface $repository + */ + public function __construct(ServiceRepositoryInterface $repository) + { + $this->repository = $repository; + } + + /** + * Update a service and prevent changing the author once it is set. + * + * @param int $service + * @param array $data + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function handle($service, array $data) + { + if (! is_null(array_get($data, 'author'))) { + unset($data['author']); + } + + $this->repository->withoutFresh()->update($service, $data); + } +} diff --git a/app/Traits/Services/CreatesServiceIndex.php b/app/Traits/Services/CreatesServiceIndex.php new file mode 100644 index 00000000..dcff053b --- /dev/null +++ b/app/Traits/Services/CreatesServiceIndex.php @@ -0,0 +1,71 @@ +. + * + * 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\Traits\Services; + +trait CreatesServiceIndex +{ + /** + * Returns the default index.js file that is used for services on the daemon. + * + * @return string + */ + public function getIndexScript() + { + return <<<'EOF' +'use strict'; + +/** + * Pterodactyl - Daemon + * Copyright (c) 2015 - 2017 Dane Everitt + * + * 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. + */ +const rfr = require('rfr'); +const _ = require('lodash'); + +const Core = rfr('src/services/index.js'); + +class Service extends Core {} + +module.exports = Service; +EOF; + } +} diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index 4da5007d..fdef71ae 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -88,6 +88,17 @@ $factory->define(Pterodactyl\Models\Node::class, function (Faker\Generator $fake ]; }); +$factory->define(Pterodactyl\Models\Service::class, function (Faker\Generator $faker) { + return [ + 'author' => $faker->unique()->uuid, + 'name' => $faker->word, + 'description' => null, + 'folder' => strtolower($faker->unique()->word), + 'startup' => 'java -jar test.jar', + 'index_file' => 'indexjs', + ]; +}); + $factory->define(Pterodactyl\Models\ServiceOption::class, function (Faker\Generator $faker) { return [ 'id' => $faker->unique()->randomNumber(), diff --git a/database/migrations/2017_08_15_214555_CascadeDeletionWhenAParentServiceIsDeleted.php b/database/migrations/2017_08_15_214555_CascadeDeletionWhenAParentServiceIsDeleted.php new file mode 100644 index 00000000..aef29902 --- /dev/null +++ b/database/migrations/2017_08_15_214555_CascadeDeletionWhenAParentServiceIsDeleted.php @@ -0,0 +1,36 @@ +dropForeign(['service_id']); + + $table->foreign('service_id')->references('id')->on('services')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('service_options', function (Blueprint $table) { + $table->dropForeign(['service_id']); + + $table->foreign('service_id')->references('id')->on('services'); + }); + } +} diff --git a/resources/lang/en/admin/exceptions.php b/resources/lang/en/admin/exceptions.php index 1a575625..bea2c83f 100644 --- a/resources/lang/en/admin/exceptions.php +++ b/resources/lang/en/admin/exceptions.php @@ -34,6 +34,7 @@ return [ 'cidr_out_of_range' => 'CIDR notation only allows masks between /25 and /32.', ], 'service' => [ + 'delete_has_servers' => 'A service with active servers attached to it cannot be deleted from the Panel.', 'options' => [ 'delete_has_servers' => 'A service option with active servers attached to it cannot be deleted from the Panel.', 'invalid_copy_id' => 'The service option selected for copying a script from either does not exist, or is copying a script itself.', diff --git a/resources/lang/en/admin/services.php b/resources/lang/en/admin/services.php index 8390140e..91c851f3 100644 --- a/resources/lang/en/admin/services.php +++ b/resources/lang/en/admin/services.php @@ -23,8 +23,15 @@ */ return [ + 'notices' => [ + 'service_created' => 'A new service, :name, has been successfully created.', + 'service_deleted' => 'Successfully deleted the requested service from the Panel.', + 'service_updated' => 'Successfully updated the service configuration options.', + 'functions_updated' => 'The service functions file has been updated. You will need to reboot your Nodes in order for these changes to be applied.', + ], 'options' => [ 'notices' => [ + 'option_deleted' => 'Successfully deleted the requested service option from the Panel.', 'option_updated' => 'Service option configuration has been updated successfully.', 'script_updated' => 'Service option install script has been updated and will run whenever servers are installed.', 'option_created' => 'New service option was created successfully. You will need to restart any running daemons to apply this new service.', diff --git a/resources/themes/pterodactyl/admin/services/functions.blade.php b/resources/themes/pterodactyl/admin/services/functions.blade.php index 3f0e89d4..ce06f8f6 100644 --- a/resources/themes/pterodactyl/admin/services/functions.blade.php +++ b/resources/themes/pterodactyl/admin/services/functions.blade.php @@ -50,15 +50,14 @@

Functions Control

-
+
{{ $service->index_file }}
diff --git a/routes/admin.php b/routes/admin.php index 229cf188..664ccf52 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -166,8 +166,8 @@ Route::group(['prefix' => 'nodes'], function () { Route::group(['prefix' => 'services'], function () { Route::get('/', 'ServiceController@index')->name('admin.services'); Route::get('/new', 'ServiceController@create')->name('admin.services.new'); - Route::get('/view/{id}', 'ServiceController@view')->name('admin.services.view'); - Route::get('/view/{id}/functions', 'ServiceController@viewFunctions')->name('admin.services.view.functions'); + Route::get('/view/{service}', 'ServiceController@view')->name('admin.services.view'); + Route::get('/view/{service}/functions', 'ServiceController@viewFunctions')->name('admin.services.view.functions'); Route::get('/option/new', 'OptionController@create')->name('admin.services.option.new'); Route::get('/option/{option}', 'OptionController@viewConfiguration')->name('admin.services.option.view'); Route::get('/option/{option}/variables', 'VariableController@view')->name('admin.services.option.variables'); @@ -177,13 +177,14 @@ Route::group(['prefix' => 'services'], function () { Route::post('/option/new', 'OptionController@store'); Route::post('/option/{option}/variables', 'VariableController@store'); - Route::patch('/view/{option}', 'ServiceController@edit'); + Route::patch('/view/{service}', 'ServiceController@update'); + Route::patch('/view/{service}/functions', 'ServiceController@updateFunctions'); Route::patch('/option/{option}', 'OptionController@editConfiguration'); Route::patch('/option/{option}/scripts', 'OptionController@updateScripts'); Route::patch('/option/{option}/variables/{variable}', 'VariableController@update')->name('admin.services.option.variables.edit'); - Route::delete('/view/{id}', 'ServiceController@delete'); - Route::delete('/option/{option}', 'OptionController@delete'); + Route::delete('/view/{service}', 'ServiceController@destroy'); + Route::delete('/option/{option}', 'OptionController@destroy'); Route::delete('/option/{option}/variables/{variable}', 'VariableController@delete'); }); diff --git a/tests/Unit/Services/Services/Options/OptionDeletionServiceTest.php b/tests/Unit/Services/Services/Options/OptionDeletionServiceTest.php index 725f6d0c..1c2aa5c4 100644 --- a/tests/Unit/Services/Services/Options/OptionDeletionServiceTest.php +++ b/tests/Unit/Services/Services/Options/OptionDeletionServiceTest.php @@ -27,7 +27,7 @@ namespace Tests\Unit\Services\Services\Options; use Mockery as m; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; -use Pterodactyl\Exceptions\Services\ServiceOption\HasActiveServersException; +use Pterodactyl\Exceptions\Services\HasActiveServersException; use Pterodactyl\Services\Services\Options\OptionDeletionService; use Tests\TestCase; diff --git a/tests/Unit/Services/Services/ServiceCreationServiceTest.php b/tests/Unit/Services/Services/ServiceCreationServiceTest.php new file mode 100644 index 00000000..e0989a40 --- /dev/null +++ b/tests/Unit/Services/Services/ServiceCreationServiceTest.php @@ -0,0 +1,94 @@ +. + * + * 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\Services; + +use Mockery as m; +use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; +use Pterodactyl\Models\Service; +use Pterodactyl\Services\Services\ServiceCreationService; +use Pterodactyl\Traits\Services\CreatesServiceIndex; +use Tests\TestCase; +use Illuminate\Contracts\Config\Repository; + +class ServiceCreationServiceTest extends TestCase +{ + use CreatesServiceIndex; + + /** + * @var \Illuminate\Contracts\Config\Repository + */ + protected $config; + + /** + * @var \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Services\ServiceCreationService + */ + protected $service; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->config = m::mock(Repository::class); + $this->repository = m::mock(ServiceRepositoryInterface::class); + + $this->service = new ServiceCreationService($this->config, $this->repository); + } + + /** + * Test that a new service can be created using the correct data. + */ + public function testCreateNewService() + { + $model = factory(Service::class)->make(); + $data = [ + 'name' => $model->name, + 'description' => $model->description, + 'folder' => $model->folder, + 'startup' => $model->startup, + ]; + + $this->config->shouldReceive('get')->with('pterodactyl.service.author')->once()->andReturn('0000-author'); + $this->repository->shouldReceive('create')->with([ + 'author' => '0000-author', + 'name' => $data['name'], + 'description' => $data['description'], + 'folder' => $data['folder'], + 'startup' => $data['startup'], + 'index_file' => $this->getIndexScript(), + ])->once()->andReturn($model); + + $response = $this->service->handle($data); + $this->assertInstanceOf(Service::class, $response); + $this->assertEquals($model, $response); + } +} diff --git a/tests/Unit/Services/Services/ServiceDeletionServiceTest.php b/tests/Unit/Services/Services/ServiceDeletionServiceTest.php new file mode 100644 index 00000000..66ded0d7 --- /dev/null +++ b/tests/Unit/Services/Services/ServiceDeletionServiceTest.php @@ -0,0 +1,104 @@ +. + * + * 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\Services; + +use Exception; +use Mockery as m; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; +use Pterodactyl\Exceptions\Services\HasActiveServersException; +use Pterodactyl\Services\Services\ServiceDeletionService; +use Tests\TestCase; + +class ServiceDeletionServiceTest extends TestCase +{ + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $serverRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Services\ServiceDeletionService + */ + protected $service; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->serverRepository = m::mock(ServerRepositoryInterface::class); + $this->repository = m::mock(ServiceRepositoryInterface::class); + + $this->service = new ServiceDeletionService($this->serverRepository, $this->repository); + } + + /** + * Test that a service is deleted when there are no servers attached to a service. + */ + public function testServiceIsDeleted() + { + $this->serverRepository->shouldReceive('findCountWhere')->with([['service_id', '=', 1]])->once()->andReturn(0); + $this->repository->shouldReceive('delete')->with(1)->once()->andReturn(1); + + $this->assertEquals(1, $this->service->handle(1)); + } + + /** + * Test that an exception is thrown when there are servers attached to a service. + * + * @dataProvider serverCountProvider + */ + public function testExceptionIsThrownIfServersAreAttached($count) + { + $this->serverRepository->shouldReceive('findCountWhere')->with([['service_id', '=', 1]])->once()->andReturn($count); + + try { + $this->service->handle(1); + } catch (Exception $exception) { + $this->assertInstanceOf(HasActiveServersException::class, $exception); + $this->assertEquals(trans('admin/exceptions.service.delete_has_servers'), $exception->getMessage()); + } + } + + /** + * Provide assorted server counts to ensure that an exception is always thrown when more than 0 servers are found. + * + * @return array + */ + public function serverCountProvider() + { + return [ + [1], [2], [5], [10], + ]; + } +} diff --git a/tests/Unit/Services/Services/ServiceUpdateServiceTest.php b/tests/Unit/Services/Services/ServiceUpdateServiceTest.php new file mode 100644 index 00000000..40baa811 --- /dev/null +++ b/tests/Unit/Services/Services/ServiceUpdateServiceTest.php @@ -0,0 +1,77 @@ +. + * + * 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\Services; + +use Mockery as m; +use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; +use Pterodactyl\Services\Services\ServiceUpdateService; +use Tests\TestCase; + +class ServiceUpdateServiceTest extends TestCase +{ + /** + * @var \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Services\ServiceUpdateService + */ + protected $service; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->repository = m::mock(ServiceRepositoryInterface::class); + + $this->service = new ServiceUpdateService($this->repository); + } + + /** + * Test that the author key is removed from the data array before updating the record. + */ + public function testAuthorArrayKeyIsRemovedIfPassed() + { + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with(1, ['otherfield' => 'value'])->once()->andReturnNull(); + + $this->service->handle(1, ['author' => 'author1', 'otherfield' => 'value']); + } + + /** + * Test that the function continues to work when no author key is passed. + */ + public function testServiceIsUpdatedWhenNoAuthorKeyIsPassed() + { + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with(1, ['otherfield' => 'value'])->once()->andReturnNull(); + + $this->service->handle(1, ['otherfield' => 'value']); + } +} From 1260a8384a301fccfbe523d6e1c1b3e146de7115 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 15 Aug 2017 23:16:00 -0500 Subject: [PATCH 53/99] Initial implementation of controller unit tests. --- .../DatabaseHostRepositoryInterface.php | 10 ++ .../Controllers/Admin/DatabaseController.php | 10 +- .../Eloquent/DatabaseHostRepository.php | 16 ++- .../Assertions/ControllerAssertionsTrait.php | 86 +++++++++++++ .../Admin/DatabaseControllerTest.php | 115 ++++++++++++++++++ 5 files changed, 231 insertions(+), 6 deletions(-) create mode 100644 tests/Assertions/ControllerAssertionsTrait.php create mode 100644 tests/Unit/Http/Controllers/Admin/DatabaseControllerTest.php diff --git a/app/Contracts/Repository/DatabaseHostRepositoryInterface.php b/app/Contracts/Repository/DatabaseHostRepositoryInterface.php index 25a6ebf2..3abd9be8 100644 --- a/app/Contracts/Repository/DatabaseHostRepositoryInterface.php +++ b/app/Contracts/Repository/DatabaseHostRepositoryInterface.php @@ -33,6 +33,16 @@ interface DatabaseHostRepositoryInterface extends RepositoryInterface */ public function getWithViewDetails(); + /** + * Return a database host with the databases and associated servers that are attached to said databases. + * + * @param int $id + * @return mixed + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function getWithServers($id); + /** * Delete a database host from the DB if there are no databases using it. * diff --git a/app/Http/Controllers/Admin/DatabaseController.php b/app/Http/Controllers/Admin/DatabaseController.php index bfea288d..7964d38c 100644 --- a/app/Http/Controllers/Admin/DatabaseController.php +++ b/app/Http/Controllers/Admin/DatabaseController.php @@ -90,16 +90,16 @@ class DatabaseController extends Controller /** * Display database host to user. * - * @param \Pterodactyl\Models\DatabaseHost $host + * @param int $host * @return \Illuminate\View\View + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function view(DatabaseHost $host) + public function view($host) { - $host->load('databases.server'); - return view('admin.databases.view', [ 'locations' => $this->locationRepository->getAllWithNodes(), - 'host' => $host, + 'host' => $this->repository->getWithServers($host), ]); } diff --git a/app/Repositories/Eloquent/DatabaseHostRepository.php b/app/Repositories/Eloquent/DatabaseHostRepository.php index 166fd881..5aff2674 100644 --- a/app/Repositories/Eloquent/DatabaseHostRepository.php +++ b/app/Repositories/Eloquent/DatabaseHostRepository.php @@ -24,6 +24,7 @@ namespace Pterodactyl\Repositories\Eloquent; +use Webmozart\Assert\Assert; use Pterodactyl\Models\DatabaseHost; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; @@ -47,13 +48,26 @@ class DatabaseHostRepository extends EloquentRepository implements DatabaseHostR return $this->getBuilder()->withCount('databases')->with('node')->get(); } + public function getWithServers($id) + { + Assert::numeric($id, 'First argument passed to getWithServers must be numeric, recieved %s.'); + + $instance = $this->getBuilder()->with('databases.server')->find($id, $this->getColumns()); + if (! $instance) { + throw new RecordNotFoundException(); + } + + return $instance; + } + /** * {@inheritdoc} */ public function deleteIfNoDatabases($id) { - $instance = $this->getBuilder()->withCount('databases')->find($id); + Assert::numeric($id, 'First argument passed to deleteIfNoDatabases must be numeric, recieved %s.'); + $instance = $this->getBuilder()->withCount('databases')->find($id); if (! $instance) { throw new RecordNotFoundException(); } diff --git a/tests/Assertions/ControllerAssertionsTrait.php b/tests/Assertions/ControllerAssertionsTrait.php new file mode 100644 index 00000000..5e04512d --- /dev/null +++ b/tests/Assertions/ControllerAssertionsTrait.php @@ -0,0 +1,86 @@ +. + * + * 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\Assertions; + +use PHPUnit_Framework_Assert; + +trait ControllerAssertionsTrait +{ + /** + * Assert that a view name equals the passed name. + * + * @param string $name + * @param \Illuminate\View\View $view + */ + public function assertViewNameEquals($name, $view) + { + PHPUnit_Framework_Assert::assertEquals($name, $view->getName()); + } + + /** + * Assert that a view name does not equal a provided name. + * + * @param string $name + * @param \Illuminate\View\View $view + */ + public function assertViewNameNotEquals($name, $view) + { + PHPUnit_Framework_Assert::assertNotEquals($name, $view->getName()); + } + + /** + * Assert that a view has an attribute passed into it. + * + * @param string $attribute + * @param \Illuminate\View\View $view + */ + public function assertViewHasKey($attribute, $view) + { + PHPUnit_Framework_Assert::assertArrayHasKey($attribute, $view->getData()); + } + + /** + * Assert that a view does not have a specific attribute passed in. + * + * @param string $attribute + * @param \Illuminate\View\View $view + */ + public function assertViewNotHasKey($attribute, $view) + { + PHPUnit_Framework_Assert::assertArrayNotHasKey($attribute, $view->getData()); + } + + /** + * Assert that a view attribute equals a given parameter. + * + * @param string $attribute + * @param mixed $value + * @param \Illuminate\View\View $view + */ + public function assertViewKeyEquals($attribute, $value, $view) + { + PHPUnit_Framework_Assert::assertEquals($value, array_get($view->getData(), $attribute)); + } +} diff --git a/tests/Unit/Http/Controllers/Admin/DatabaseControllerTest.php b/tests/Unit/Http/Controllers/Admin/DatabaseControllerTest.php new file mode 100644 index 00000000..d930b2ff --- /dev/null +++ b/tests/Unit/Http/Controllers/Admin/DatabaseControllerTest.php @@ -0,0 +1,115 @@ +. + * + * 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\Http\Controllers\Admin; + +use Mockery as m; +use Prologue\Alerts\AlertsMessageBag; +use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; +use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; +use Pterodactyl\Http\Controllers\Admin\DatabaseController; +use Pterodactyl\Services\Database\DatabaseHostService; +use Tests\Assertions\ControllerAssertionsTrait; +use Tests\TestCase; + +class DatabaseControllerTest extends TestCase +{ + use ControllerAssertionsTrait; + + /** + * @var \Prologue\Alerts\AlertsMessageBag + */ + protected $alert; + + /** + * @var \Pterodactyl\Http\Controllers\Admin\DatabaseController + */ + protected $controller; + + /** + * @var \Pterodactyl\Contracts\Repository\LocationRepositoryInterface + */ + protected $locationRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Database\DatabaseHostService + */ + protected $service; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->alert = m::mock(AlertsMessageBag::class); + $this->locationRepository = m::mock(LocationRepositoryInterface::class); + $this->repository = m::mock(DatabaseHostRepositoryInterface::class); + $this->service = m::mock(DatabaseHostService::class); + + $this->controller = new DatabaseController( + $this->alert, + $this->repository, + $this->service, + $this->locationRepository + ); + } + + /** + * Test the index controller. + */ + public function testIndexController() + { + $this->locationRepository->shouldReceive('getAllWithNodes')->withNoArgs()->once()->andReturn('getAllWithNodes'); + $this->repository->shouldReceive('getWithViewDetails')->withNoArgs()->once()->andReturn('getWithViewDetails'); + + $view = $this->controller->index(); + + $this->assertViewNameEquals('admin.databases.index', $view); + $this->assertViewHasKey('locations', $view); + $this->assertViewHasKey('hosts', $view); + $this->assertViewKeyEquals('locations', 'getAllWithNodes', $view); + $this->assertViewKeyEquals('hosts', 'getWithViewDetails', $view); + } + + public function testViewController() + { + $this->locationRepository->shouldReceive('getAllWithNodes')->withNoArgs()->once()->andReturn('getAllWithNodes'); + $this->repository->shouldReceive('getWithServers')->with(1)->once()->andReturn('getWithServers'); + + $view = $this->controller->view(1); + + $this->assertViewNameEquals('admin.databases.view', $view); + $this->assertViewHasKey('locations', $view); + $this->assertViewHasKey('host', $view); + $this->assertViewKeyEquals('locations', 'getAllWithNodes', $view); + $this->assertViewKeyEquals('host', 'getWithServers', $view); + } +} From 46cb71e69d6185f8881d214b49afb9daba5decf3 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Tue, 15 Aug 2017 23:21:01 -0500 Subject: [PATCH 54/99] Apply fixes from StyleCI (#590) --- .../Controllers/Admin/OptionController.php | 2 +- .../Options/OptionDeletionService.php | 2 +- .../Admin/DatabaseControllerTest.php | 12 +++++------ .../Services/Servers/CreationServiceTest.php | 4 ++-- .../Services/Servers/DeletionServiceTest.php | 20 +++++++++---------- .../Options/OptionCreationServiceTest.php | 4 ++-- .../Options/OptionDeletionServiceTest.php | 8 ++++---- .../Options/OptionUpdateServiceTest.php | 6 +++--- .../Services/ServiceCreationServiceTest.php | 8 ++++---- .../Services/ServiceDeletionServiceTest.php | 6 +++--- .../Services/ServiceUpdateServiceTest.php | 4 ++-- .../Variables/VariableCreationServiceTest.php | 7 +++---- .../Variables/VariableUpdateServiceTest.php | 12 +++++------ 13 files changed, 47 insertions(+), 48 deletions(-) diff --git a/app/Http/Controllers/Admin/OptionController.php b/app/Http/Controllers/Admin/OptionController.php index 184f90f9..46403e08 100644 --- a/app/Http/Controllers/Admin/OptionController.php +++ b/app/Http/Controllers/Admin/OptionController.php @@ -30,6 +30,7 @@ use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Models\ServiceOption; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Http\Requests\Admin\Service\EditOptionScript; +use Pterodactyl\Exceptions\Services\HasActiveServersException; use Pterodactyl\Services\Services\Options\OptionUpdateService; use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; use Pterodactyl\Services\Services\Options\OptionCreationService; @@ -38,7 +39,6 @@ use Pterodactyl\Http\Requests\Admin\Service\ServiceOptionFormRequest; use Pterodactyl\Services\Services\Options\InstallScriptUpdateService; use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; use Pterodactyl\Exceptions\Services\ServiceOption\InvalidCopyFromException; -use Pterodactyl\Exceptions\Services\HasActiveServersException; use Pterodactyl\Exceptions\Services\ServiceOption\NoParentConfigurationFoundException; class OptionController extends Controller diff --git a/app/Services/Services/Options/OptionDeletionService.php b/app/Services/Services/Options/OptionDeletionService.php index 8afd6e2e..b489b7a4 100644 --- a/app/Services/Services/Options/OptionDeletionService.php +++ b/app/Services/Services/Options/OptionDeletionService.php @@ -24,9 +24,9 @@ namespace Pterodactyl\Services\Services\Options; +use Pterodactyl\Exceptions\Services\HasActiveServersException; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; -use Pterodactyl\Exceptions\Services\HasActiveServersException; class OptionDeletionService { diff --git a/tests/Unit/Http/Controllers/Admin/DatabaseControllerTest.php b/tests/Unit/Http/Controllers/Admin/DatabaseControllerTest.php index d930b2ff..03402adb 100644 --- a/tests/Unit/Http/Controllers/Admin/DatabaseControllerTest.php +++ b/tests/Unit/Http/Controllers/Admin/DatabaseControllerTest.php @@ -25,13 +25,13 @@ namespace Tests\Unit\Http\Controllers\Admin; use Mockery as m; -use Prologue\Alerts\AlertsMessageBag; -use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; -use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; -use Pterodactyl\Http\Controllers\Admin\DatabaseController; -use Pterodactyl\Services\Database\DatabaseHostService; -use Tests\Assertions\ControllerAssertionsTrait; use Tests\TestCase; +use Prologue\Alerts\AlertsMessageBag; +use Tests\Assertions\ControllerAssertionsTrait; +use Pterodactyl\Services\Database\DatabaseHostService; +use Pterodactyl\Http\Controllers\Admin\DatabaseController; +use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; +use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; class DatabaseControllerTest extends TestCase { diff --git a/tests/Unit/Services/Servers/CreationServiceTest.php b/tests/Unit/Services/Servers/CreationServiceTest.php index c14384cf..d9a5c245 100644 --- a/tests/Unit/Services/Servers/CreationServiceTest.php +++ b/tests/Unit/Services/Servers/CreationServiceTest.php @@ -25,13 +25,13 @@ namespace Tests\Unit\Services\Servers; use Exception; -use GuzzleHttp\Exception\RequestException; use Mockery as m; -use Pterodactyl\Exceptions\DisplayException; use Tests\TestCase; use Illuminate\Log\Writer; use phpmock\phpunit\PHPMock; use Illuminate\Database\DatabaseManager; +use GuzzleHttp\Exception\RequestException; +use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Services\Servers\CreationService; use Pterodactyl\Services\Servers\VariableValidatorService; use Pterodactyl\Services\Servers\UsernameGenerationService; diff --git a/tests/Unit/Services/Servers/DeletionServiceTest.php b/tests/Unit/Services/Servers/DeletionServiceTest.php index 37295ed8..f61b53a2 100644 --- a/tests/Unit/Services/Servers/DeletionServiceTest.php +++ b/tests/Unit/Services/Servers/DeletionServiceTest.php @@ -25,18 +25,18 @@ namespace Tests\Unit\Services\Servers; use Exception; -use GuzzleHttp\Exception\RequestException; -use Illuminate\Log\Writer; use Mockery as m; -use Illuminate\Database\ConnectionInterface; -use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; -use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; -use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Models\Server; -use Pterodactyl\Services\Database\DatabaseManagementService; -use Pterodactyl\Services\Servers\DeletionService; use Tests\TestCase; +use Illuminate\Log\Writer; +use Pterodactyl\Models\Server; +use GuzzleHttp\Exception\RequestException; +use Illuminate\Database\ConnectionInterface; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Services\Servers\DeletionService; +use Pterodactyl\Services\Database\DatabaseManagementService; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; class DeletionServiceTest extends TestCase { diff --git a/tests/Unit/Services/Services/Options/OptionCreationServiceTest.php b/tests/Unit/Services/Services/Options/OptionCreationServiceTest.php index 95016864..931367d0 100644 --- a/tests/Unit/Services/Services/Options/OptionCreationServiceTest.php +++ b/tests/Unit/Services/Services/Options/OptionCreationServiceTest.php @@ -90,7 +90,7 @@ class OptionCreationServiceTest extends TestCase $this->repository->shouldReceive('findCountWhere')->with([ ['service_id', '=', $data['service_id']], - ['id', '=', $data['config_from']] + ['id', '=', $data['config_from']], ])->once()->andReturn(1); $this->repository->shouldReceive('create')->with($data) @@ -109,7 +109,7 @@ class OptionCreationServiceTest extends TestCase { $this->repository->shouldReceive('findCountWhere')->with([ ['service_id', '=', null], - ['id', '=', 1] + ['id', '=', 1], ])->once()->andReturn(0); try { diff --git a/tests/Unit/Services/Services/Options/OptionDeletionServiceTest.php b/tests/Unit/Services/Services/Options/OptionDeletionServiceTest.php index 1c2aa5c4..bb369563 100644 --- a/tests/Unit/Services/Services/Options/OptionDeletionServiceTest.php +++ b/tests/Unit/Services/Services/Options/OptionDeletionServiceTest.php @@ -25,11 +25,11 @@ namespace Tests\Unit\Services\Services\Options; use Mockery as m; -use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; -use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; -use Pterodactyl\Exceptions\Services\HasActiveServersException; -use Pterodactyl\Services\Services\Options\OptionDeletionService; use Tests\TestCase; +use Pterodactyl\Exceptions\Services\HasActiveServersException; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Services\Services\Options\OptionDeletionService; +use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; class OptionDeletionServiceTest extends TestCase { diff --git a/tests/Unit/Services/Services/Options/OptionUpdateServiceTest.php b/tests/Unit/Services/Services/Options/OptionUpdateServiceTest.php index 20d23761..7e34ad19 100644 --- a/tests/Unit/Services/Services/Options/OptionUpdateServiceTest.php +++ b/tests/Unit/Services/Services/Options/OptionUpdateServiceTest.php @@ -26,11 +26,11 @@ namespace Tests\Unit\Services\Services\Options; use Exception; use Mockery as m; -use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; -use Pterodactyl\Exceptions\Services\ServiceOption\NoParentConfigurationFoundException; +use Tests\TestCase; use Pterodactyl\Models\ServiceOption; use Pterodactyl\Services\Services\Options\OptionUpdateService; -use Tests\TestCase; +use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; +use Pterodactyl\Exceptions\Services\ServiceOption\NoParentConfigurationFoundException; class OptionUpdateServiceTest extends TestCase { diff --git a/tests/Unit/Services/Services/ServiceCreationServiceTest.php b/tests/Unit/Services/Services/ServiceCreationServiceTest.php index e0989a40..758b6427 100644 --- a/tests/Unit/Services/Services/ServiceCreationServiceTest.php +++ b/tests/Unit/Services/Services/ServiceCreationServiceTest.php @@ -25,12 +25,12 @@ namespace Tests\Unit\Services\Services; use Mockery as m; -use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; -use Pterodactyl\Models\Service; -use Pterodactyl\Services\Services\ServiceCreationService; -use Pterodactyl\Traits\Services\CreatesServiceIndex; use Tests\TestCase; +use Pterodactyl\Models\Service; use Illuminate\Contracts\Config\Repository; +use Pterodactyl\Traits\Services\CreatesServiceIndex; +use Pterodactyl\Services\Services\ServiceCreationService; +use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; class ServiceCreationServiceTest extends TestCase { diff --git a/tests/Unit/Services/Services/ServiceDeletionServiceTest.php b/tests/Unit/Services/Services/ServiceDeletionServiceTest.php index 66ded0d7..0290b625 100644 --- a/tests/Unit/Services/Services/ServiceDeletionServiceTest.php +++ b/tests/Unit/Services/Services/ServiceDeletionServiceTest.php @@ -26,11 +26,11 @@ namespace Tests\Unit\Services\Services; use Exception; use Mockery as m; +use Tests\TestCase; +use Pterodactyl\Services\Services\ServiceDeletionService; +use Pterodactyl\Exceptions\Services\HasActiveServersException; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; -use Pterodactyl\Exceptions\Services\HasActiveServersException; -use Pterodactyl\Services\Services\ServiceDeletionService; -use Tests\TestCase; class ServiceDeletionServiceTest extends TestCase { diff --git a/tests/Unit/Services/Services/ServiceUpdateServiceTest.php b/tests/Unit/Services/Services/ServiceUpdateServiceTest.php index 40baa811..cac11ea6 100644 --- a/tests/Unit/Services/Services/ServiceUpdateServiceTest.php +++ b/tests/Unit/Services/Services/ServiceUpdateServiceTest.php @@ -25,9 +25,9 @@ namespace Tests\Unit\Services\Services; use Mockery as m; -use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; -use Pterodactyl\Services\Services\ServiceUpdateService; use Tests\TestCase; +use Pterodactyl\Services\Services\ServiceUpdateService; +use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; class ServiceUpdateServiceTest extends TestCase { diff --git a/tests/Unit/Services/Services/Variables/VariableCreationServiceTest.php b/tests/Unit/Services/Services/Variables/VariableCreationServiceTest.php index 320f06e7..87ca4ed9 100644 --- a/tests/Unit/Services/Services/Variables/VariableCreationServiceTest.php +++ b/tests/Unit/Services/Services/Variables/VariableCreationServiceTest.php @@ -25,13 +25,12 @@ namespace Tests\Unit\Services\Services\Variables; use Mockery as m; -use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; -use Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface; -use Pterodactyl\Exceptions\Services\ServiceVariable\ReservedVariableNameException; +use Tests\TestCase; use Pterodactyl\Models\ServiceOption; use Pterodactyl\Models\ServiceVariable; use Pterodactyl\Services\Services\Variables\VariableCreationService; -use Tests\TestCase; +use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface; class VariableCreationServiceTest extends TestCase { diff --git a/tests/Unit/Services/Services/Variables/VariableUpdateServiceTest.php b/tests/Unit/Services/Services/Variables/VariableUpdateServiceTest.php index 5e067387..2a19d9f0 100644 --- a/tests/Unit/Services/Services/Variables/VariableUpdateServiceTest.php +++ b/tests/Unit/Services/Services/Variables/VariableUpdateServiceTest.php @@ -26,11 +26,11 @@ namespace Tests\Unit\Services\Services\Variables; use Exception; use Mockery as m; -use PhpParser\Node\Expr\Variable; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Models\ServiceVariable; -use Pterodactyl\Services\Services\Variables\VariableUpdateService; use Tests\TestCase; +use PhpParser\Node\Expr\Variable; +use Pterodactyl\Models\ServiceVariable; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Services\Services\Variables\VariableUpdateService; use Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface; class VariableUpdateServiceTest extends TestCase @@ -87,7 +87,7 @@ class VariableUpdateServiceTest extends TestCase ->shouldReceive('findCountWhere')->with([ ['env_variable', '=', 'TEST_VAR_123'], ['option_id', '=', $this->model->option_id], - ['id', '!=', $this->model->id] + ['id', '!=', $this->model->id], ])->once()->andReturn(0); $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() @@ -109,7 +109,7 @@ class VariableUpdateServiceTest extends TestCase ->shouldReceive('findCountWhere')->with([ ['env_variable', '=', 'TEST_VAR_123'], ['option_id', '=', $this->model->option_id], - ['id', '!=', $this->model->id] + ['id', '!=', $this->model->id], ])->once()->andReturn(1); try { From 9d3dca87f2944593775f4774b0fae1a957701650 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Fri, 18 Aug 2017 22:19:06 -0500 Subject: [PATCH 55/99] Begin moving packs to new service mechanisms, refactor exceptions for services --- .dev/vagrant/motd.txt | 2 +- .travis.yml | 1 + CHANGELOG.md | 2 +- .../Repository/PackRepositoryInterface.php | 39 +++++ .../HasActiveServersException.php | 2 +- .../Pack/InvalidFileMimeTypeException.php | 30 ++++ .../Pack/InvalidFileUploadException.php | 30 ++++ .../RequiredVariableMissingException.php | 2 +- .../InvalidCopyFromException.php | 2 +- .../NoParentConfigurationFoundException.php | 2 +- .../ReservedVariableNameException.php | 2 +- .../Controllers/Admin/OptionController.php | 6 +- .../Controllers/Admin/ServiceController.php | 2 +- .../Controllers/Admin/VariableController.php | 4 +- app/Models/Pack.php | 57 ++++-- .../Searchable.php} | 14 +- .../Eloquent/LocationRepository.php | 6 +- app/Repositories/Eloquent/NodeRepository.php | 6 +- app/Repositories/Eloquent/PackRepository.php | 65 +++++++ .../Eloquent/ServerRepository.php | 6 +- app/Repositories/Eloquent/UserRepository.php | 6 +- app/Repositories/Old/TaskRepository.php | 163 ------------------ app/Services/Packs/PackCreationService.php | 119 +++++++++++++ .../Options/InstallScriptUpdateService.php | 4 +- .../Options/OptionCreationService.php | 4 +- .../Options/OptionDeletionService.php | 4 +- .../Services/Options/OptionUpdateService.php | 4 +- .../Services/ServiceDeletionService.php | 4 +- .../Variables/VariableCreationService.php | 4 +- .../Variables/VariableUpdateService.php | 4 +- .../Old/HelperRepository.php => helpers.php} | 50 +----- composer.json | 3 + config/pterodactyl.php | 27 +++ config/services.php | 2 +- ...vePackWhenParentServiceOptionIsDeleted.php | 36 ++++ .../pterodactyl/vendor/ace/mode-objectivec.js | 2 +- resources/lang/en/base.php | 2 +- .../admin/services/functions.blade.php | 4 +- .../admin/services/index.blade.php | 8 +- .../pterodactyl/admin/services/new.blade.php | 4 +- .../admin/services/options/new.blade.php | 4 +- .../admin/services/options/scripts.blade.php | 4 +- .../services/options/variables.blade.php | 2 +- .../admin/services/options/view.blade.php | 4 +- .../pterodactyl/admin/services/view.blade.php | 6 +- .../pterodactyl/layouts/admin.blade.php | 2 +- .../Admin/DatabaseControllerTest.php | 3 + .../Allocations/AssignmentServiceTest.php | 4 +- tests/Unit/Services/Api/KeyServiceTest.php | 2 +- .../DatabaseManagementServiceTest.php | 2 +- .../Services/Nodes/CreationServiceTest.php | 2 +- .../Unit/Services/Nodes/UpdateServiceTest.php | 4 +- .../Services/Servers/CreationServiceTest.php | 2 +- .../DetailsModificationServiceTest.php | 2 +- .../Servers/UsernameGenerationServiceTest.php | 4 +- .../InstallScriptUpdateServiceTest.php | 2 +- .../Options/OptionCreationServiceTest.php | 2 +- .../Options/OptionDeletionServiceTest.php | 2 +- .../Options/OptionUpdateServiceTest.php | 2 +- .../Services/ServiceDeletionServiceTest.php | 2 +- .../Variables/VariableCreationServiceTest.php | 2 +- .../Variables/VariableUpdateServiceTest.php | 2 +- 62 files changed, 492 insertions(+), 303 deletions(-) create mode 100644 app/Contracts/Repository/PackRepositoryInterface.php rename app/Exceptions/{Services => Service}/HasActiveServersException.php (96%) create mode 100644 app/Exceptions/Service/Pack/InvalidFileMimeTypeException.php create mode 100644 app/Exceptions/Service/Pack/InvalidFileUploadException.php rename app/Exceptions/{Services => Service}/Server/RequiredVariableMissingException.php (96%) rename app/Exceptions/{Services => Service}/ServiceOption/InvalidCopyFromException.php (95%) rename app/Exceptions/{Services => Service}/ServiceOption/NoParentConfigurationFoundException.php (95%) rename app/Exceptions/{Services => Service}/ServiceVariable/ReservedVariableNameException.php (95%) rename app/Repositories/{Eloquent/Attributes/SearchableRepository.php => Concerns/Searchable.php} (83%) create mode 100644 app/Repositories/Eloquent/PackRepository.php delete mode 100644 app/Repositories/Old/TaskRepository.php create mode 100644 app/Services/Packs/PackCreationService.php rename app/{Repositories/Old/HelperRepository.php => helpers.php} (53%) create mode 100644 database/migrations/2017_08_18_215428_RemovePackWhenParentServiceOptionIsDeleted.php diff --git a/.dev/vagrant/motd.txt b/.dev/vagrant/motd.txt index 823172e4..22089d55 100644 --- a/.dev/vagrant/motd.txt +++ b/.dev/vagrant/motd.txt @@ -13,5 +13,5 @@ Default panel users: MySQL is accessible using root/pterodactyl or pterodactyl/pterodactyl -Services for pteroq and mailhog are running +Service for pteroq and mailhog are running ##################################################### diff --git a/.travis.yml b/.travis.yml index 960a8c19..48ca49a0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ dist: trusty php: - '7.0' - '7.1' + - '7.2' sudo: false cache: directories: diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e0f00e1..db9ce290 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -245,7 +245,7 @@ spatie/laravel-fractal (4.0.0 => 4.0.1) * New theme applied to Admin CP. Many graphical changes were made, some data was moved around and some display data changed. Too much was changed to feasibly log it all in here. Major breaking changes or notable new features will be logged. * New server creation page now makes significantly less AJAX calls and is much quicker to respond. * Server and Node view pages wee modified to split tabs into individual pages to make re-themeing and modifications significantly easier, and reduce MySQL query loads on page. -* `[pre.4]` — Services and Pack magement overhauled to be faster, cleaner, and more extensible in the future. +* `[pre.4]` — Service and Pack magement overhauled to be faster, cleaner, and more extensible in the future. * Most of the backend `UnhandledException` display errors now include a clearer error that directs admins to the program's logs. * Table seeders for services now can be run during upgrades and will attempt to locate and update, or create new if not found in the database. * Many structural changes to the database and `Pterodactyl\Models` classes that would flood this changelog if they were all included. All required migrations included to handle database changes. diff --git a/app/Contracts/Repository/PackRepositoryInterface.php b/app/Contracts/Repository/PackRepositoryInterface.php new file mode 100644 index 00000000..5d1bb022 --- /dev/null +++ b/app/Contracts/Repository/PackRepositoryInterface.php @@ -0,0 +1,39 @@ +. + * + * 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\Contracts\Repository; + +use Pterodactyl\Contracts\Repository\Attributes\SearchableInterface; + +interface PackRepositoryInterface extends RepositoryInterface, SearchableInterface +{ + /** + * Return all of the file archives for a given pack. + * + * @param int $id + * @param bool $collection + * @return object|\Illuminate\Support\Collection + */ + public function getFileArchives($id, $collection = false); +} diff --git a/app/Exceptions/Services/HasActiveServersException.php b/app/Exceptions/Service/HasActiveServersException.php similarity index 96% rename from app/Exceptions/Services/HasActiveServersException.php rename to app/Exceptions/Service/HasActiveServersException.php index d560f568..851052d0 100644 --- a/app/Exceptions/Services/HasActiveServersException.php +++ b/app/Exceptions/Service/HasActiveServersException.php @@ -22,7 +22,7 @@ * SOFTWARE. */ -namespace Pterodactyl\Exceptions\Services; +namespace Pterodactyl\Exceptions\Service; class HasActiveServersException extends \Exception { diff --git a/app/Exceptions/Service/Pack/InvalidFileMimeTypeException.php b/app/Exceptions/Service/Pack/InvalidFileMimeTypeException.php new file mode 100644 index 00000000..f34e1be8 --- /dev/null +++ b/app/Exceptions/Service/Pack/InvalidFileMimeTypeException.php @@ -0,0 +1,30 @@ +. + * + * 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\Exceptions\Service\Pack; + +class InvalidFileMimeTypeException extends \Exception +{ + // +} diff --git a/app/Exceptions/Service/Pack/InvalidFileUploadException.php b/app/Exceptions/Service/Pack/InvalidFileUploadException.php new file mode 100644 index 00000000..ffef85b8 --- /dev/null +++ b/app/Exceptions/Service/Pack/InvalidFileUploadException.php @@ -0,0 +1,30 @@ +. + * + * 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\Exceptions\Service\Pack; + +class InvalidFileUploadException extends \Exception +{ + // +} diff --git a/app/Exceptions/Services/Server/RequiredVariableMissingException.php b/app/Exceptions/Service/Server/RequiredVariableMissingException.php similarity index 96% rename from app/Exceptions/Services/Server/RequiredVariableMissingException.php rename to app/Exceptions/Service/Server/RequiredVariableMissingException.php index bf166a62..61ed0197 100644 --- a/app/Exceptions/Services/Server/RequiredVariableMissingException.php +++ b/app/Exceptions/Service/Server/RequiredVariableMissingException.php @@ -22,7 +22,7 @@ * SOFTWARE. */ -namespace Pterodactyl\Exceptions\Services\Server; +namespace Pterodactyl\Exceptions\Service\Server; use Exception; diff --git a/app/Exceptions/Services/ServiceOption/InvalidCopyFromException.php b/app/Exceptions/Service/ServiceOption/InvalidCopyFromException.php similarity index 95% rename from app/Exceptions/Services/ServiceOption/InvalidCopyFromException.php rename to app/Exceptions/Service/ServiceOption/InvalidCopyFromException.php index 34601313..8aab35b4 100644 --- a/app/Exceptions/Services/ServiceOption/InvalidCopyFromException.php +++ b/app/Exceptions/Service/ServiceOption/InvalidCopyFromException.php @@ -22,7 +22,7 @@ * SOFTWARE. */ -namespace Pterodactyl\Exceptions\Services\ServiceOption; +namespace Pterodactyl\Exceptions\Service\ServiceOption; class InvalidCopyFromException extends \Exception { diff --git a/app/Exceptions/Services/ServiceOption/NoParentConfigurationFoundException.php b/app/Exceptions/Service/ServiceOption/NoParentConfigurationFoundException.php similarity index 95% rename from app/Exceptions/Services/ServiceOption/NoParentConfigurationFoundException.php rename to app/Exceptions/Service/ServiceOption/NoParentConfigurationFoundException.php index 7d793504..c5ec1e72 100644 --- a/app/Exceptions/Services/ServiceOption/NoParentConfigurationFoundException.php +++ b/app/Exceptions/Service/ServiceOption/NoParentConfigurationFoundException.php @@ -22,7 +22,7 @@ * SOFTWARE. */ -namespace Pterodactyl\Exceptions\Services\ServiceOption; +namespace Pterodactyl\Exceptions\Service\ServiceOption; class NoParentConfigurationFoundException extends \Exception { diff --git a/app/Exceptions/Services/ServiceVariable/ReservedVariableNameException.php b/app/Exceptions/Service/ServiceVariable/ReservedVariableNameException.php similarity index 95% rename from app/Exceptions/Services/ServiceVariable/ReservedVariableNameException.php rename to app/Exceptions/Service/ServiceVariable/ReservedVariableNameException.php index 9777b0e9..c5b001be 100644 --- a/app/Exceptions/Services/ServiceVariable/ReservedVariableNameException.php +++ b/app/Exceptions/Service/ServiceVariable/ReservedVariableNameException.php @@ -22,7 +22,7 @@ * SOFTWARE. */ -namespace Pterodactyl\Exceptions\Services\ServiceVariable; +namespace Pterodactyl\Exceptions\Service\ServiceVariable; use Exception; diff --git a/app/Http/Controllers/Admin/OptionController.php b/app/Http/Controllers/Admin/OptionController.php index 46403e08..d0789799 100644 --- a/app/Http/Controllers/Admin/OptionController.php +++ b/app/Http/Controllers/Admin/OptionController.php @@ -30,7 +30,7 @@ use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Models\ServiceOption; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Http\Requests\Admin\Service\EditOptionScript; -use Pterodactyl\Exceptions\Services\HasActiveServersException; +use Pterodactyl\Exceptions\Service\HasActiveServersException; use Pterodactyl\Services\Services\Options\OptionUpdateService; use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; use Pterodactyl\Services\Services\Options\OptionCreationService; @@ -38,8 +38,8 @@ use Pterodactyl\Services\Services\Options\OptionDeletionService; use Pterodactyl\Http\Requests\Admin\Service\ServiceOptionFormRequest; use Pterodactyl\Services\Services\Options\InstallScriptUpdateService; use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; -use Pterodactyl\Exceptions\Services\ServiceOption\InvalidCopyFromException; -use Pterodactyl\Exceptions\Services\ServiceOption\NoParentConfigurationFoundException; +use Pterodactyl\Exceptions\Service\ServiceOption\InvalidCopyFromException; +use Pterodactyl\Exceptions\Service\ServiceOption\NoParentConfigurationFoundException; class OptionController extends Controller { diff --git a/app/Http/Controllers/Admin/ServiceController.php b/app/Http/Controllers/Admin/ServiceController.php index ae10cb34..b2341bf5 100644 --- a/app/Http/Controllers/Admin/ServiceController.php +++ b/app/Http/Controllers/Admin/ServiceController.php @@ -30,7 +30,7 @@ use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Services\Services\ServiceUpdateService; use Pterodactyl\Services\Services\ServiceCreationService; use Pterodactyl\Services\Services\ServiceDeletionService; -use Pterodactyl\Exceptions\Services\HasActiveServersException; +use Pterodactyl\Exceptions\Service\HasActiveServersException; use Pterodactyl\Http\Requests\Admin\Service\ServiceFormRequest; use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; use Pterodactyl\Http\Requests\Admin\Service\ServiceFunctionsFormRequest; diff --git a/app/Http/Controllers/Admin/VariableController.php b/app/Http/Controllers/Admin/VariableController.php index eef95ec2..78caf406 100644 --- a/app/Http/Controllers/Admin/VariableController.php +++ b/app/Http/Controllers/Admin/VariableController.php @@ -83,7 +83,7 @@ class VariableController extends Controller * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Services\ServiceVariable\ReservedVariableNameException + * @throws \Pterodactyl\Exceptions\Service\ServiceVariable\ReservedVariableNameException */ public function store(OptionVariableFormRequest $request, ServiceOption $option) { @@ -117,7 +117,7 @@ class VariableController extends Controller * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - * @throws \Pterodactyl\Exceptions\Services\ServiceVariable\ReservedVariableNameException + * @throws \Pterodactyl\Exceptions\Service\ServiceVariable\ReservedVariableNameException */ public function update(OptionVariableFormRequest $request, ServiceOption $option, ServiceVariable $variable) { diff --git a/app/Models/Pack.php b/app/Models/Pack.php index 9cda1979..0139fcf8 100644 --- a/app/Models/Pack.php +++ b/app/Models/Pack.php @@ -26,12 +26,15 @@ namespace Pterodactyl\Models; use File; use Storage; +use Sofa\Eloquence\Eloquence; +use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; -use Nicolaslopezj\Searchable\SearchableTrait; +use Sofa\Eloquence\Contracts\CleansAttributes; +use Sofa\Eloquence\Contracts\Validable as ValidableContract; -class Pack extends Model +class Pack extends Model implements CleansAttributes, ValidableContract { - use SearchableTrait; + use Eloquence, Validable; /** * The table associated with the model. @@ -46,7 +49,33 @@ class Pack extends Model * @var array */ protected $fillable = [ - 'option_id', 'name', 'version', 'description', 'selectable', 'visible', 'locked', + 'option_id', 'uuid', 'name', 'version', 'description', 'selectable', 'visible', 'locked', + ]; + + /** + * @var array + */ + protected static $applicationRules = [ + 'name' => 'required', + 'version' => 'required', + 'description' => 'sometimes', + 'selectable' => 'sometimes|required', + 'visible' => 'sometimes|required', + 'locked' => 'sometimes|required', + 'option_id' => 'required', + ]; + + /** + * @var array + */ + protected static $dataIntegrityRules = [ + 'name' => 'string', + 'version' => 'string', + 'description' => 'nullable|string', + 'selectable' => 'boolean', + 'visible' => 'boolean', + 'locked' => 'boolean', + 'option_id' => 'exists:service_options,id', ]; /** @@ -66,18 +95,13 @@ class Pack extends Model * * @var array */ - protected $searchable = [ - 'columns' => [ - 'packs.name' => 10, - 'packs.uuid' => 8, - 'service_options.name' => 6, - 'service_options.tag' => 5, - 'service_options.docker_image' => 5, - 'packs.version' => 2, - ], - 'joins' => [ - 'service_options' => ['packs.option_id', 'service_options.id'], - ], + protected $searchableColumns = [ + 'name' => 10, + 'uuid' => 8, + 'option.name' => 6, + 'option.tag' => 5, + 'option.docker_image' => 5, + 'version' => 2, ]; /** @@ -85,6 +109,7 @@ class Pack extends Model * * @param bool $collection * @return \Illuminate\Support\Collection|object + * @deprecated */ public function files($collection = false) { diff --git a/app/Repositories/Eloquent/Attributes/SearchableRepository.php b/app/Repositories/Concerns/Searchable.php similarity index 83% rename from app/Repositories/Eloquent/Attributes/SearchableRepository.php rename to app/Repositories/Concerns/Searchable.php index 5965d1f5..ec957824 100644 --- a/app/Repositories/Eloquent/Attributes/SearchableRepository.php +++ b/app/Repositories/Concerns/Searchable.php @@ -22,20 +22,22 @@ * SOFTWARE. */ -namespace Pterodactyl\Repositories\Eloquent\Attributes; +namespace Pterodactyl\Repositories\Concerns; -use Pterodactyl\Repositories\Eloquent\EloquentRepository; -use Pterodactyl\Contracts\Repository\Attributes\SearchableInterface; - -abstract class SearchableRepository extends EloquentRepository implements SearchableInterface +trait Searchable { /** + * The term to search. + * * @var bool|string */ protected $searchTerm = false; /** - * {@inheritdoc} + * Perform a search of the model using the given term. + * + * @param string $term + * @return $this */ public function search($term) { diff --git a/app/Repositories/Eloquent/LocationRepository.php b/app/Repositories/Eloquent/LocationRepository.php index 41e7811a..70bece64 100644 --- a/app/Repositories/Eloquent/LocationRepository.php +++ b/app/Repositories/Eloquent/LocationRepository.php @@ -28,10 +28,12 @@ use Pterodactyl\Models\Location; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; -use Pterodactyl\Repositories\Eloquent\Attributes\SearchableRepository; +use Pterodactyl\Repositories\Concerns\Searchable; -class LocationRepository extends SearchableRepository implements LocationRepositoryInterface +class LocationRepository extends EloquentRepository implements LocationRepositoryInterface { + use Searchable; + /** * @var string */ diff --git a/app/Repositories/Eloquent/NodeRepository.php b/app/Repositories/Eloquent/NodeRepository.php index 7dd0cb40..9a63ca93 100644 --- a/app/Repositories/Eloquent/NodeRepository.php +++ b/app/Repositories/Eloquent/NodeRepository.php @@ -27,10 +27,12 @@ namespace Pterodactyl\Repositories\Eloquent; use Pterodactyl\Models\Node; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; -use Pterodactyl\Repositories\Eloquent\Attributes\SearchableRepository; +use Pterodactyl\Repositories\Concerns\Searchable; -class NodeRepository extends SearchableRepository implements NodeRepositoryInterface +class NodeRepository extends EloquentRepository implements NodeRepositoryInterface { + use Searchable; + /** * {@inheritdoc} */ diff --git a/app/Repositories/Eloquent/PackRepository.php b/app/Repositories/Eloquent/PackRepository.php new file mode 100644 index 00000000..38a82471 --- /dev/null +++ b/app/Repositories/Eloquent/PackRepository.php @@ -0,0 +1,65 @@ +. + * + * 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\Repositories\Eloquent; + +use Pterodactyl\Models\Pack; +use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory; +use Pterodactyl\Repositories\Concerns\Searchable; +use Pterodactyl\Contracts\Repository\PackRepositoryInterface; + +class PackRepository extends EloquentRepository implements PackRepositoryInterface +{ + use Searchable; + + /** + * {@inheritdoc} + */ + public function model() + { + return Pack::class; + } + + /** + * {@inheritdoc} + */ + public function getFileArchives($id, $collection = false) + { + $pack = $this->getBuilder()->find($id, ['id', 'uuid']); + $storage = $this->app->make(FilesystemFactory::class); + $files = collect($storage->disk('default')->files('packs/' . $pack->uuid)); + + $files = $files->map(function ($file) { + $path = storage_path('app/' . $file); + + return (object) [ + 'name' => basename($file), + 'hash' => sha1_file($path), + 'size' => human_readable($path), + ]; + }); + + return ($collection) ? $files : (object) $files->all(); + } +} diff --git a/app/Repositories/Eloquent/ServerRepository.php b/app/Repositories/Eloquent/ServerRepository.php index 967e208d..05ba8039 100644 --- a/app/Repositories/Eloquent/ServerRepository.php +++ b/app/Repositories/Eloquent/ServerRepository.php @@ -25,12 +25,14 @@ namespace Pterodactyl\Repositories\Eloquent; use Pterodactyl\Models\Server; +use Pterodactyl\Repositories\Concerns\Searchable; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; -use Pterodactyl\Repositories\Eloquent\Attributes\SearchableRepository; -class ServerRepository extends SearchableRepository implements ServerRepositoryInterface +class ServerRepository extends EloquentRepository implements ServerRepositoryInterface { + use Searchable; + /** * {@inheritdoc} */ diff --git a/app/Repositories/Eloquent/UserRepository.php b/app/Repositories/Eloquent/UserRepository.php index e7dfab60..ec1abe57 100644 --- a/app/Repositories/Eloquent/UserRepository.php +++ b/app/Repositories/Eloquent/UserRepository.php @@ -26,12 +26,14 @@ namespace Pterodactyl\Repositories\Eloquent; use Pterodactyl\Models\User; use Illuminate\Foundation\Application; +use Pterodactyl\Repositories\Concerns\Searchable; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Illuminate\Contracts\Config\Repository as ConfigRepository; -use Pterodactyl\Repositories\Eloquent\Attributes\SearchableRepository; -class UserRepository extends SearchableRepository implements UserRepositoryInterface +class UserRepository extends EloquentRepository implements UserRepositoryInterface { + use Searchable; + /** * @var \Illuminate\Contracts\Config\Repository */ diff --git a/app/Repositories/Old/TaskRepository.php b/app/Repositories/Old/TaskRepository.php deleted file mode 100644 index 24290e25..00000000 --- a/app/Repositories/Old/TaskRepository.php +++ /dev/null @@ -1,163 +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\Repositories; - -use Cron; -use Validator; -use Pterodactyl\Models\Task; -use Pterodactyl\Models\User; -use Pterodactyl\Models\Server; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Exceptions\DisplayValidationException; - -class TaskRepository -{ - /** - * The default values to use for new tasks. - * - * @var array - */ - protected $defaults = [ - 'year' => '*', - 'day_of_week' => '*', - 'month' => '*', - 'day_of_month' => '*', - 'hour' => '*', - 'minute' => '*/30', - ]; - - /** - * Task action types. - * - * @var array - */ - protected $actions = [ - 'command', - 'power', - ]; - - /** - * Deletes a given task. - * - * @param int $id - * @return bool - */ - public function delete($id) - { - $task = Task::findOrFail($id); - $task->delete(); - } - - /** - * Toggles a task active or inactive. - * - * @param int $id - * @return bool - */ - public function toggle($id) - { - $task = Task::findOrFail($id); - - $task->active = ! $task->active; - $task->queued = false; - $task->save(); - - return $task->active; - } - - /** - * Create a new scheduled task for a given server. - * - * @param int $server - * @param int $user - * @param array $data - * @return \Pterodactyl\Models\Task - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function create($server, $user, $data) - { - $server = Server::findOrFail($server); - $user = User::findOrFail($user); - - $validator = Validator::make($data, [ - 'action' => 'string|required', - 'data' => 'string|required', - 'year' => 'string|sometimes', - 'day_of_week' => 'string|sometimes', - 'month' => 'string|sometimes', - 'day_of_month' => 'string|sometimes', - 'hour' => 'string|sometimes', - 'minute' => 'string|sometimes', - ]); - - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - if (! in_array($data['action'], $this->actions)) { - throw new DisplayException('The action provided is not valid.'); - } - - $cron = $this->defaults; - foreach ($this->defaults as $setting => $value) { - if (array_key_exists($setting, $data) && ! is_null($data[$setting]) && $data[$setting] !== '') { - $cron[$setting] = $data[$setting]; - } - } - - // Check that is this a valid Cron Entry - try { - $buildCron = Cron::factory(sprintf('%s %s %s %s %s %s', - $cron['minute'], - $cron['hour'], - $cron['day_of_month'], - $cron['month'], - $cron['day_of_week'], - $cron['year'] - )); - } catch (\Exception $ex) { - throw $ex; - } - - return Task::create([ - 'user_id' => $user->id, - 'server_id' => $server->id, - 'active' => 1, - 'action' => $data['action'], - 'data' => $data['data'], - 'queued' => 0, - 'year' => $cron['year'], - 'day_of_week' => $cron['day_of_week'], - 'month' => $cron['month'], - 'day_of_month' => $cron['day_of_month'], - 'hour' => $cron['hour'], - 'minute' => $cron['minute'], - 'last_run' => null, - 'next_run' => $buildCron->getNextRunDate(), - ]); - } -} diff --git a/app/Services/Packs/PackCreationService.php b/app/Services/Packs/PackCreationService.php new file mode 100644 index 00000000..ff74b27c --- /dev/null +++ b/app/Services/Packs/PackCreationService.php @@ -0,0 +1,119 @@ +. + * + * 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\Packs; + +use Ramsey\Uuid\Uuid; +use Illuminate\Database\ConnectionInterface; +use Pterodactyl\Contracts\Repository\PackRepositoryInterface; +use Illuminate\Contracts\Config\Repository as ConfigRepository; +use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory; +use Pterodactyl\Exceptions\Service\Pack\InvalidFileUploadException; +use Pterodactyl\Exceptions\Service\Pack\InvalidFileMimeTypeException; + +class PackCreationService +{ + /** + * @var \Illuminate\Contracts\Config\Repository + */ + protected $config; + + /** + * @var \Illuminate\Database\ConnectionInterface + */ + protected $connection; + + /** + * @var \Pterodactyl\Contracts\Repository\PackRepositoryInterface + */ + protected $repository; + + /** + * @var \Illuminate\Contracts\Filesystem\Factory + */ + protected $storage; + + /** + * PackCreationService constructor. + * + * @param \Illuminate\Contracts\Config\Repository $config + * @param \Illuminate\Database\ConnectionInterface $connection + * @param \Illuminate\Contracts\Filesystem\Factory $storage + * @param \Pterodactyl\Contracts\Repository\PackRepositoryInterface $repository + */ + public function __construct( + ConfigRepository $config, + ConnectionInterface $connection, + FilesystemFactory $storage, + PackRepositoryInterface $repository + ) { + $this->config = $config; + $this->connection = $connection; + $this->repository = $repository; + $this->storage = $storage; + } + + /** + * Add a new service pack to the system. + * + * @param array $data + * @param \Illuminate\Http\UploadedFile|null $file + * @return \Pterodactyl\Models\Pack + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Service\Pack\InvalidFileMimeTypeException + * @throws \Pterodactyl\Exceptions\Service\Pack\InvalidFileUploadException + */ + public function handle(array $data, $file = null) + { + if (! is_null($file)) { + if (! $file->isValid()) { + throw new InvalidFileUploadException; + } + + if (! in_array($file->getMimeType(), $this->config->get('pterodactyl.files.pack_types'))) { + throw new InvalidFileMimeTypeException; + } + } + + // Transform values to boolean + $data['selectable'] = isset($data['selectable']); + $data['visible'] = isset($data['visible']); + $data['locked'] = isset($data['locked']); + + $this->connection->beginTransaction(); + $pack = $this->repository->create(array_merge( + ['uuid' => Uuid::uuid4()], $data + )); + + $this->storage->disk('default')->makeDirectory('packs/' . $pack->uuid); + if (! is_null($file)) { + $file->storeAs('packs/' . $pack->uuid, 'archive.tar.gz'); + } + + $this->connection->commit(); + + return $pack; + } +} diff --git a/app/Services/Services/Options/InstallScriptUpdateService.php b/app/Services/Services/Options/InstallScriptUpdateService.php index 1a63c800..f1fd9ae2 100644 --- a/app/Services/Services/Options/InstallScriptUpdateService.php +++ b/app/Services/Services/Options/InstallScriptUpdateService.php @@ -26,7 +26,7 @@ namespace Pterodactyl\Services\Services\Options; use Pterodactyl\Models\ServiceOption; use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; -use Pterodactyl\Exceptions\Services\ServiceOption\InvalidCopyFromException; +use Pterodactyl\Exceptions\Service\ServiceOption\InvalidCopyFromException; class InstallScriptUpdateService { @@ -53,7 +53,7 @@ class InstallScriptUpdateService * * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - * @throws \Pterodactyl\Exceptions\Services\ServiceOption\InvalidCopyFromException + * @throws \Pterodactyl\Exceptions\Service\ServiceOption\InvalidCopyFromException */ public function handle($option, array $data) { diff --git a/app/Services/Services/Options/OptionCreationService.php b/app/Services/Services/Options/OptionCreationService.php index 83cda6b1..ac4f179f 100644 --- a/app/Services/Services/Options/OptionCreationService.php +++ b/app/Services/Services/Options/OptionCreationService.php @@ -25,7 +25,7 @@ namespace Pterodactyl\Services\Services\Options; use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; -use Pterodactyl\Exceptions\Services\ServiceOption\NoParentConfigurationFoundException; +use Pterodactyl\Exceptions\Service\ServiceOption\NoParentConfigurationFoundException; class OptionCreationService { @@ -51,7 +51,7 @@ class OptionCreationService * @return \Pterodactyl\Models\ServiceOption * * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Services\ServiceOption\NoParentConfigurationFoundException + * @throws \Pterodactyl\Exceptions\Service\ServiceOption\NoParentConfigurationFoundException */ public function handle(array $data) { diff --git a/app/Services/Services/Options/OptionDeletionService.php b/app/Services/Services/Options/OptionDeletionService.php index b489b7a4..b94132a5 100644 --- a/app/Services/Services/Options/OptionDeletionService.php +++ b/app/Services/Services/Options/OptionDeletionService.php @@ -24,7 +24,7 @@ namespace Pterodactyl\Services\Services\Options; -use Pterodactyl\Exceptions\Services\HasActiveServersException; +use Pterodactyl\Exceptions\Service\HasActiveServersException; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; @@ -60,7 +60,7 @@ class OptionDeletionService * @param int $option * @return int * - * @throws \Pterodactyl\Exceptions\Services\HasActiveServersException + * @throws \Pterodactyl\Exceptions\Service\HasActiveServersException */ public function handle($option) { diff --git a/app/Services/Services/Options/OptionUpdateService.php b/app/Services/Services/Options/OptionUpdateService.php index 4ba133fa..b2e310fb 100644 --- a/app/Services/Services/Options/OptionUpdateService.php +++ b/app/Services/Services/Options/OptionUpdateService.php @@ -26,7 +26,7 @@ namespace Pterodactyl\Services\Services\Options; use Pterodactyl\Models\ServiceOption; use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; -use Pterodactyl\Exceptions\Services\ServiceOption\NoParentConfigurationFoundException; +use Pterodactyl\Exceptions\Service\ServiceOption\NoParentConfigurationFoundException; class OptionUpdateService { @@ -53,7 +53,7 @@ class OptionUpdateService * * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - * @throws \Pterodactyl\Exceptions\Services\ServiceOption\NoParentConfigurationFoundException + * @throws \Pterodactyl\Exceptions\Service\ServiceOption\NoParentConfigurationFoundException */ public function handle($option, array $data) { diff --git a/app/Services/Services/ServiceDeletionService.php b/app/Services/Services/ServiceDeletionService.php index 5d0405b1..f127d361 100644 --- a/app/Services/Services/ServiceDeletionService.php +++ b/app/Services/Services/ServiceDeletionService.php @@ -24,7 +24,7 @@ namespace Pterodactyl\Services\Services; -use Pterodactyl\Exceptions\Services\HasActiveServersException; +use Pterodactyl\Exceptions\Service\HasActiveServersException; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; @@ -60,7 +60,7 @@ class ServiceDeletionService * @param int $service * @return int * - * @throws \Pterodactyl\Exceptions\Services\HasActiveServersException + * @throws \Pterodactyl\Exceptions\Service\HasActiveServersException */ public function handle($service) { diff --git a/app/Services/Services/Variables/VariableCreationService.php b/app/Services/Services/Variables/VariableCreationService.php index 1dc1f8ae..90820732 100644 --- a/app/Services/Services/Variables/VariableCreationService.php +++ b/app/Services/Services/Variables/VariableCreationService.php @@ -28,7 +28,7 @@ use Pterodactyl\Models\ServiceOption; use Pterodactyl\Models\ServiceVariable; use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; use Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface; -use Pterodactyl\Exceptions\Services\ServiceVariable\ReservedVariableNameException; +use Pterodactyl\Exceptions\Service\ServiceVariable\ReservedVariableNameException; class VariableCreationService { @@ -58,7 +58,7 @@ class VariableCreationService * @return \Pterodactyl\Models\ServiceVariable * * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Services\ServiceVariable\ReservedVariableNameException + * @throws \Pterodactyl\Exceptions\Service\ServiceVariable\ReservedVariableNameException */ public function handle($option, array $data) { diff --git a/app/Services/Services/Variables/VariableUpdateService.php b/app/Services/Services/Variables/VariableUpdateService.php index 90c10a54..bce5dd70 100644 --- a/app/Services/Services/Variables/VariableUpdateService.php +++ b/app/Services/Services/Variables/VariableUpdateService.php @@ -27,7 +27,7 @@ namespace Pterodactyl\Services\Services\Variables; use Pterodactyl\Models\ServiceVariable; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface; -use Pterodactyl\Exceptions\Services\ServiceVariable\ReservedVariableNameException; +use Pterodactyl\Exceptions\Service\ServiceVariable\ReservedVariableNameException; class VariableUpdateService { @@ -56,7 +56,7 @@ class VariableUpdateService * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - * @throws \Pterodactyl\Exceptions\Services\ServiceVariable\ReservedVariableNameException + * @throws \Pterodactyl\Exceptions\Service\ServiceVariable\ReservedVariableNameException */ public function handle($variable, array $data) { diff --git a/app/Repositories/Old/HelperRepository.php b/app/helpers.php similarity index 53% rename from app/Repositories/Old/HelperRepository.php rename to app/helpers.php index 204480ec..3f218cc5 100644 --- a/app/Repositories/Old/HelperRepository.php +++ b/app/helpers.php @@ -1,5 +1,5 @@ . * @@ -22,52 +22,16 @@ * SOFTWARE. */ -namespace Pterodactyl\Repositories; - -class HelperRepository -{ +if (! function_exists('human_readable')) { /** - * Listing of editable files in the control panel. + * Generate a human-readable filesize for a given file path. * - * @var array - */ - protected static $editable = [ - 'application/json', - 'application/javascript', - 'application/xml', - 'application/xhtml+xml', - 'text/xml', - 'text/css', - 'text/html', - 'text/plain', - 'text/x-perl', - 'text/x-shellscript', - 'inode/x-empty', - ]; - - /** - * Converts from bytes to the largest possible size that is still readable. - * - * @param int $bytes - * @param int $decimals + * @param string $path + * @param int $precision * @return string - * @deprecated */ - public static function bytesToHuman($bytes, $decimals = 2) + function human_readable($path, $precision = 2) { - $sz = explode(',', 'B,KB,MB,GB'); - $factor = floor((strlen($bytes) - 1) / 3); - - return sprintf("%.{$decimals}f", $bytes / pow(1024, $factor)) . ' ' . $sz[$factor]; - } - - /** - * Returns array of editable files. - * - * @return array - */ - public static function editableFiles() - { - return self::$editable; + return app('file')->humanReadableSize($path, $precision); } } diff --git a/composer.json b/composer.json index 59e683ec..f4f46e98 100644 --- a/composer.json +++ b/composer.json @@ -54,6 +54,9 @@ "classmap": [ "database" ], + "files": [ + "app/helpers.php" + ], "psr-4": { "Pterodactyl\\": "app/" } diff --git a/config/pterodactyl.php b/config/pterodactyl.php index 29d8bbe7..6e6d92aa 100644 --- a/config/pterodactyl.php +++ b/config/pterodactyl.php @@ -135,6 +135,33 @@ return [ 'in_context' => env('PHRASE_IN_CONTEXT', false), ], + /* + |-------------------------------------------------------------------------- + | File Editor + |-------------------------------------------------------------------------- + | + | This array includes the MIME filetypes that can be edited via the web. + */ + 'files' => [ + 'editable' => [ + 'application/json', + 'application/javascript', + 'application/xml', + 'application/xhtml+xml', + 'inode/x-empty', + 'text/xml', + 'text/css', + 'text/html', + 'text/plain', + 'text/x-perl', + 'text/x-shellscript', + ], + 'pack_types' => [ + 'application/gzip', + 'application/x-gzip', + ], + ], + /* |-------------------------------------------------------------------------- | JSON Response Routes diff --git a/config/services.php b/config/services.php index 6ea80674..e16817cc 100644 --- a/config/services.php +++ b/config/services.php @@ -4,7 +4,7 @@ return [ /* |-------------------------------------------------------------------------- - | Third Party Services + | Third Party Service |-------------------------------------------------------------------------- | | This file is for storing the credentials for third party services such diff --git a/database/migrations/2017_08_18_215428_RemovePackWhenParentServiceOptionIsDeleted.php b/database/migrations/2017_08_18_215428_RemovePackWhenParentServiceOptionIsDeleted.php new file mode 100644 index 00000000..694b3993 --- /dev/null +++ b/database/migrations/2017_08_18_215428_RemovePackWhenParentServiceOptionIsDeleted.php @@ -0,0 +1,36 @@ +dropForeign(['option_id']); + + $table->foreign('option_id')->references('id')->on('service_options')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('packs', function (Blueprint $table) { + $table->dropForeign(['option_id']); + + $table->foreign('option_id')->references('id')->on('service_options'); + }); + } +} diff --git a/public/themes/pterodactyl/vendor/ace/mode-objectivec.js b/public/themes/pterodactyl/vendor/ace/mode-objectivec.js index 08c1cc56..98645e5f 100644 --- a/public/themes/pterodactyl/vendor/ace/mode-objectivec.js +++ b/public/themes/pterodactyl/vendor/ace/mode-objectivec.js @@ -1 +1 @@ -define("ace/mode/doc_comment_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/text_highlight_rules"],function(e,t,n){"use strict";var r=e("../lib/oop"),i=e("./text_highlight_rules").TextHighlightRules,s=function(){this.$rules={start:[{token:"comment.doc.tag",regex:"@[\\w\\d_]+"},s.getTagRule(),{defaultToken:"comment.doc",caseInsensitive:!0}]}};r.inherits(s,i),s.getTagRule=function(e){return{token:"comment.doc.tag.storage.type",regex:"\\b(?:TODO|FIXME|XXX|HACK)\\b"}},s.getStartRule=function(e){return{token:"comment.doc",regex:"\\/\\*(?=\\*)",next:e}},s.getEndRule=function(e){return{token:"comment.doc",regex:"\\*\\/",next:e}},t.DocCommentHighlightRules=s}),define("ace/mode/c_cpp_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/doc_comment_highlight_rules","ace/mode/text_highlight_rules"],function(e,t,n){"use strict";var r=e("../lib/oop"),i=e("./doc_comment_highlight_rules").DocCommentHighlightRules,s=e("./text_highlight_rules").TextHighlightRules,o=t.cFunctions="\\b(?:hypot(?:f|l)?|s(?:scanf|ystem|nprintf|ca(?:nf|lb(?:n(?:f|l)?|ln(?:f|l)?))|i(?:n(?:h(?:f|l)?|f|l)?|gn(?:al|bit))|tr(?:s(?:tr|pn)|nc(?:py|at|mp)|c(?:spn|hr|oll|py|at|mp)|to(?:imax|d|u(?:l(?:l)?|max)|k|f|l(?:d|l)?)|error|pbrk|ftime|len|rchr|xfrm)|printf|et(?:jmp|vbuf|locale|buf)|qrt(?:f|l)?|w(?:scanf|printf)|rand)|n(?:e(?:arbyint(?:f|l)?|xt(?:toward(?:f|l)?|after(?:f|l)?))|an(?:f|l)?)|c(?:s(?:in(?:h(?:f|l)?|f|l)?|qrt(?:f|l)?)|cos(?:h(?:f)?|f|l)?|imag(?:f|l)?|t(?:ime|an(?:h(?:f|l)?|f|l)?)|o(?:s(?:h(?:f|l)?|f|l)?|nj(?:f|l)?|pysign(?:f|l)?)|p(?:ow(?:f|l)?|roj(?:f|l)?)|e(?:il(?:f|l)?|xp(?:f|l)?)|l(?:o(?:ck|g(?:f|l)?)|earerr)|a(?:sin(?:h(?:f|l)?|f|l)?|cos(?:h(?:f|l)?|f|l)?|tan(?:h(?:f|l)?|f|l)?|lloc|rg(?:f|l)?|bs(?:f|l)?)|real(?:f|l)?|brt(?:f|l)?)|t(?:ime|o(?:upper|lower)|an(?:h(?:f|l)?|f|l)?|runc(?:f|l)?|gamma(?:f|l)?|mp(?:nam|file))|i(?:s(?:space|n(?:ormal|an)|cntrl|inf|digit|u(?:nordered|pper)|p(?:unct|rint)|finite|w(?:space|c(?:ntrl|type)|digit|upper|p(?:unct|rint)|lower|al(?:num|pha)|graph|xdigit|blank)|l(?:ower|ess(?:equal|greater)?)|al(?:num|pha)|gr(?:eater(?:equal)?|aph)|xdigit|blank)|logb(?:f|l)?|max(?:div|abs))|di(?:v|fftime)|_Exit|unget(?:c|wc)|p(?:ow(?:f|l)?|ut(?:s|c(?:har)?|wc(?:har)?)|error|rintf)|e(?:rf(?:c(?:f|l)?|f|l)?|x(?:it|p(?:2(?:f|l)?|f|l|m1(?:f|l)?)?))|v(?:s(?:scanf|nprintf|canf|printf|w(?:scanf|printf))|printf|f(?:scanf|printf|w(?:scanf|printf))|w(?:scanf|printf)|a_(?:start|copy|end|arg))|qsort|f(?:s(?:canf|e(?:tpos|ek))|close|tell|open|dim(?:f|l)?|p(?:classify|ut(?:s|c|w(?:s|c))|rintf)|e(?:holdexcept|set(?:e(?:nv|xceptflag)|round)|clearexcept|testexcept|of|updateenv|r(?:aiseexcept|ror)|get(?:e(?:nv|xceptflag)|round))|flush|w(?:scanf|ide|printf|rite)|loor(?:f|l)?|abs(?:f|l)?|get(?:s|c|pos|w(?:s|c))|re(?:open|e|ad|xp(?:f|l)?)|m(?:in(?:f|l)?|od(?:f|l)?|a(?:f|l|x(?:f|l)?)?))|l(?:d(?:iv|exp(?:f|l)?)|o(?:ngjmp|cal(?:time|econv)|g(?:1(?:p(?:f|l)?|0(?:f|l)?)|2(?:f|l)?|f|l|b(?:f|l)?)?)|abs|l(?:div|abs|r(?:int(?:f|l)?|ound(?:f|l)?))|r(?:int(?:f|l)?|ound(?:f|l)?)|gamma(?:f|l)?)|w(?:scanf|c(?:s(?:s(?:tr|pn)|nc(?:py|at|mp)|c(?:spn|hr|oll|py|at|mp)|to(?:imax|d|u(?:l(?:l)?|max)|k|f|l(?:d|l)?|mbs)|pbrk|ftime|len|r(?:chr|tombs)|xfrm)|to(?:b|mb)|rtomb)|printf|mem(?:set|c(?:hr|py|mp)|move))|a(?:s(?:sert|ctime|in(?:h(?:f|l)?|f|l)?)|cos(?:h(?:f|l)?|f|l)?|t(?:o(?:i|f|l(?:l)?)|exit|an(?:h(?:f|l)?|2(?:f|l)?|f|l)?)|b(?:s|ort))|g(?:et(?:s|c(?:har)?|env|wc(?:har)?)|mtime)|r(?:int(?:f|l)?|ound(?:f|l)?|e(?:name|alloc|wind|m(?:ove|quo(?:f|l)?|ainder(?:f|l)?))|a(?:nd|ise))|b(?:search|towc)|m(?:odf(?:f|l)?|em(?:set|c(?:hr|py|mp)|move)|ktime|alloc|b(?:s(?:init|towcs|rtowcs)|towc|len|r(?:towc|len))))\\b",u=function(){var e="break|case|continue|default|do|else|for|goto|if|_Pragma|return|switch|while|catch|operator|try|throw|using",t="asm|__asm__|auto|bool|_Bool|char|_Complex|double|enum|float|_Imaginary|int|long|short|signed|struct|typedef|union|unsigned|void|class|wchar_t|template|char16_t|char32_t",n="const|extern|register|restrict|static|volatile|inline|private|protected|public|friend|explicit|virtual|export|mutable|typename|constexpr|new|delete|alignas|alignof|decltype|noexcept|thread_local",r="and|and_eq|bitand|bitor|compl|not|not_eq|or|or_eq|typeid|xor|xor_eqconst_cast|dynamic_cast|reinterpret_cast|static_cast|sizeof|namespace",s="NULL|true|false|TRUE|FALSE|nullptr",u=this.$keywords=this.createKeywordMapper({"keyword.control":e,"storage.type":t,"storage.modifier":n,"keyword.operator":r,"variable.language":"this","constant.language":s},"identifier"),a="[a-zA-Z\\$_\u00a1-\uffff][a-zA-Z\\d\\$_\u00a1-\uffff]*\\b",f=/\\(?:['"?\\abfnrtv]|[0-7]{1,3}|x[a-fA-F\d]{2}|u[a-fA-F\d]{4}U[a-fA-F\d]{8}|.)/.source;this.$rules={start:[{token:"comment",regex:"//$",next:"start"},{token:"comment",regex:"//",next:"singleLineComment"},i.getStartRule("doc-start"),{token:"comment",regex:"\\/\\*",next:"comment"},{token:"string",regex:"'(?:"+f+"|.)?'"},{token:"string.start",regex:'"',stateName:"qqstring",next:[{token:"string",regex:/\\\s*$/,next:"qqstring"},{token:"constant.language.escape",regex:f},{token:"constant.language.escape",regex:/%[^'"\\]/},{token:"string.end",regex:'"|$',next:"start"},{defaultToken:"string"}]},{token:"string.start",regex:'R"\\(',stateName:"rawString",next:[{token:"string.end",regex:'\\)"',next:"start"},{defaultToken:"string"}]},{token:"constant.numeric",regex:"0[xX][0-9a-fA-F]+(L|l|UL|ul|u|U|F|f|ll|LL|ull|ULL)?\\b"},{token:"constant.numeric",regex:"[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?(L|l|UL|ul|u|U|F|f|ll|LL|ull|ULL)?\\b"},{token:"keyword",regex:"#\\s*(?:include|import|pragma|line|define|undef)\\b",next:"directive"},{token:"keyword",regex:"#\\s*(?:endif|if|ifdef|else|elif|ifndef)\\b"},{token:"support.function.C99.c",regex:o},{token:u,regex:"[a-zA-Z_$][a-zA-Z0-9_$]*"},{token:"keyword.operator",regex:/--|\+\+|<<=|>>=|>>>=|<>|&&|\|\||\?:|[*%\/+\-&\^|~!<>=]=?/},{token:"punctuation.operator",regex:"\\?|\\:|\\,|\\;|\\."},{token:"paren.lparen",regex:"[[({]"},{token:"paren.rparen",regex:"[\\])}]"},{token:"text",regex:"\\s+"}],comment:[{token:"comment",regex:".*?\\*\\/",next:"start"},{token:"comment",regex:".+"}],singleLineComment:[{token:"comment",regex:/\\$/,next:"singleLineComment"},{token:"comment",regex:/$/,next:"start"},{defaultToken:"comment"}],directive:[{token:"constant.other.multiline",regex:/\\/},{token:"constant.other.multiline",regex:/.*\\/},{token:"constant.other",regex:"\\s*<.+?>",next:"start"},{token:"constant.other",regex:'\\s*["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]',next:"start"},{token:"constant.other",regex:"\\s*['](?:(?:\\\\.)|(?:[^'\\\\]))*?[']",next:"start"},{token:"constant.other",regex:/[^\\\/]+/,next:"start"}]},this.embedRules(i,"doc-",[i.getEndRule("start")]),this.normalizeRules()};r.inherits(u,s),t.c_cppHighlightRules=u}),define("ace/mode/objectivec_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/doc_comment_highlight_rules","ace/mode/c_cpp_highlight_rules"],function(e,t,n){"use strict";var r=e("../lib/oop"),i=e("./doc_comment_highlight_rules").DocCommentHighlightRules,s=e("./c_cpp_highlight_rules"),o=s.c_cppHighlightRules,u=function(){var e="\\\\(?:[abefnrtv'\"?\\\\]|[0-3]\\d{1,2}|[4-7]\\d?|222|x[a-zA-Z0-9]+)",t=[{regex:"\\b_cmd\\b",token:"variable.other.selector.objc"},{regex:"\\b(?:self|super)\\b",token:"variable.language.objc"}],n=new o,r=n.getRules();this.$rules={start:[{token:"comment",regex:"\\/\\/.*$"},i.getStartRule("doc-start"),{token:"comment",regex:"\\/\\*",next:"comment"},{token:["storage.type.objc","punctuation.definition.storage.type.objc","entity.name.type.objc","text","entity.other.inherited-class.objc"],regex:"(@)(interface|protocol)(?!.+;)(\\s+[A-Za-z_][A-Za-z0-9_]*)(\\s*:\\s*)([A-Za-z]+)"},{token:["storage.type.objc"],regex:"(@end)"},{token:["storage.type.objc","entity.name.type.objc","entity.other.inherited-class.objc"],regex:"(@implementation)(\\s+[A-Za-z_][A-Za-z0-9_]*)(\\s*?::\\s*(?:[A-Za-z][A-Za-z0-9]*))?"},{token:"string.begin.objc",regex:'@"',next:"constant_NSString"},{token:"storage.type.objc",regex:"\\bid\\s*<",next:"protocol_list"},{token:"keyword.control.macro.objc",regex:"\\bNS_DURING|NS_HANDLER|NS_ENDHANDLER\\b"},{token:["punctuation.definition.keyword.objc","keyword.control.exception.objc"],regex:"(@)(try|catch|finally|throw)\\b"},{token:["punctuation.definition.keyword.objc","keyword.other.objc"],regex:"(@)(defs|encode)\\b"},{token:["storage.type.id.objc","text"],regex:"(\\bid\\b)(\\s|\\n)?"},{token:"storage.type.objc",regex:"\\bIBOutlet|IBAction|BOOL|SEL|id|unichar|IMP|Class\\b"},{token:["punctuation.definition.storage.type.objc","storage.type.objc"],regex:"(@)(class|protocol)\\b"},{token:["punctuation.definition.storage.type.objc","punctuation"],regex:"(@selector)(\\s*\\()",next:"selectors"},{token:["punctuation.definition.storage.modifier.objc","storage.modifier.objc"],regex:"(@)(synchronized|public|private|protected|package)\\b"},{token:"constant.language.objc",regex:"\\bYES|NO|Nil|nil\\b"},{token:"support.variable.foundation",regex:"\\bNSApp\\b"},{token:["support.function.cocoa.leopard"],regex:"(?:\\b)(NS(?:Rect(?:ToCGRect|FromCGRect)|MakeCollectable|S(?:tringFromProtocol|ize(?:ToCGSize|FromCGSize))|Draw(?:NinePartImage|ThreePartImage)|P(?:oint(?:ToCGPoint|FromCGPoint)|rotocolFromString)|EventMaskFromType|Value))(?:\\b)"},{token:["support.function.cocoa"],regex:"(?:\\b)(NS(?:R(?:ound(?:DownToMultipleOfPageSize|UpToMultipleOfPageSize)|un(?:CriticalAlertPanel(?:RelativeToWindow)?|InformationalAlertPanel(?:RelativeToWindow)?|AlertPanel(?:RelativeToWindow)?)|e(?:set(?:MapTable|HashTable)|c(?:ycleZone|t(?:Clip(?:List)?|F(?:ill(?:UsingOperation|List(?:UsingOperation|With(?:Grays|Colors(?:UsingOperation)?))?)?|romString))|ordAllocationEvent)|turnAddress|leaseAlertPanel|a(?:dPixel|l(?:MemoryAvailable|locateCollectable))|gisterServicesProvider)|angeFromString)|Get(?:SizeAndAlignment|CriticalAlertPanel|InformationalAlertPanel|UncaughtExceptionHandler|FileType(?:s)?|WindowServerMemory|AlertPanel)|M(?:i(?:n(?:X|Y)|d(?:X|Y))|ouseInRect|a(?:p(?:Remove|Get|Member|Insert(?:IfAbsent|KnownAbsent)?)|ke(?:R(?:ect|ange)|Size|Point)|x(?:Range|X|Y)))|B(?:itsPer(?:SampleFromDepth|PixelFromDepth)|e(?:stDepth|ep|gin(?:CriticalAlertSheet|InformationalAlertSheet|AlertSheet)))|S(?:ho(?:uldRetainWithZone|w(?:sServicesMenuItem|AnimationEffect))|tringFrom(?:R(?:ect|ange)|MapTable|S(?:ize|elector)|HashTable|Class|Point)|izeFromString|e(?:t(?:ShowsServicesMenuItem|ZoneName|UncaughtExceptionHandler|FocusRingStyle)|lectorFromString|archPathForDirectoriesInDomains)|wap(?:Big(?:ShortToHost|IntToHost|DoubleToHost|FloatToHost|Long(?:ToHost|LongToHost))|Short|Host(?:ShortTo(?:Big|Little)|IntTo(?:Big|Little)|DoubleTo(?:Big|Little)|FloatTo(?:Big|Little)|Long(?:To(?:Big|Little)|LongTo(?:Big|Little)))|Int|Double|Float|L(?:ittle(?:ShortToHost|IntToHost|DoubleToHost|FloatToHost|Long(?:ToHost|LongToHost))|ong(?:Long)?)))|H(?:ighlightRect|o(?:stByteOrder|meDirectory(?:ForUser)?)|eight|ash(?:Remove|Get|Insert(?:IfAbsent|KnownAbsent)?)|FSType(?:CodeFromFileType|OfFile))|N(?:umberOfColorComponents|ext(?:MapEnumeratorPair|HashEnumeratorItem))|C(?:o(?:n(?:tainsRect|vert(?:GlyphsToPackedGlyphs|Swapped(?:DoubleToHost|FloatToHost)|Host(?:DoubleToSwapped|FloatToSwapped)))|unt(?:MapTable|HashTable|Frames|Windows(?:ForContext)?)|py(?:M(?:emoryPages|apTableWithZone)|Bits|HashTableWithZone|Object)|lorSpaceFromDepth|mpare(?:MapTables|HashTables))|lassFromString|reate(?:MapTable(?:WithZone)?|HashTable(?:WithZone)?|Zone|File(?:namePboardType|ContentsPboardType)))|TemporaryDirectory|I(?:s(?:ControllerMarker|EmptyRect|FreedObject)|n(?:setRect|crementExtraRefCount|te(?:r(?:sect(?:sRect|ionR(?:ect|ange))|faceStyleForKey)|gralRect)))|Zone(?:Realloc|Malloc|Name|Calloc|Fr(?:omPointer|ee))|O(?:penStepRootDirectory|ffsetRect)|D(?:i(?:sableScreenUpdates|videRect)|ottedFrameRect|e(?:c(?:imal(?:Round|Multiply|S(?:tring|ubtract)|Normalize|Co(?:py|mpa(?:ct|re))|IsNotANumber|Divide|Power|Add)|rementExtraRefCountWasZero)|faultMallocZone|allocate(?:MemoryPages|Object))|raw(?:Gr(?:oove|ayBezel)|B(?:itmap|utton)|ColorTiledRects|TiledRects|DarkBezel|W(?:hiteBezel|indowBackground)|LightBezel))|U(?:serName|n(?:ionR(?:ect|ange)|registerServicesProvider)|pdateDynamicServices)|Java(?:Bundle(?:Setup|Cleanup)|Setup(?:VirtualMachine)?|Needs(?:ToLoadClasses|VirtualMachine)|ClassesF(?:orBundle|romPath)|ObjectNamedInPath|ProvidesClasses)|P(?:oint(?:InRect|FromString)|erformService|lanarFromDepth|ageSize)|E(?:n(?:d(?:MapTableEnumeration|HashTableEnumeration)|umerate(?:MapTable|HashTable)|ableScreenUpdates)|qual(?:R(?:ects|anges)|Sizes|Points)|raseRect|xtraRefCount)|F(?:ileTypeForHFSTypeCode|ullUserName|r(?:ee(?:MapTable|HashTable)|ame(?:Rect(?:WithWidth(?:UsingOperation)?)?|Address)))|Wi(?:ndowList(?:ForContext)?|dth)|Lo(?:cationInRange|g(?:v|PageSize)?)|A(?:ccessibility(?:R(?:oleDescription(?:ForUIElement)?|aiseBadArgumentException)|Unignored(?:Children(?:ForOnlyChild)?|Descendant|Ancestor)|PostNotification|ActionDescription)|pplication(?:Main|Load)|vailableWindowDepths|ll(?:MapTable(?:Values|Keys)|HashTableObjects|ocate(?:MemoryPages|Collectable|Object)))))(?:\\b)"},{token:["support.class.cocoa.leopard"],regex:"(?:\\b)(NS(?:RuleEditor|G(?:arbageCollector|radient)|MapTable|HashTable|Co(?:ndition|llectionView(?:Item)?)|T(?:oolbarItemGroup|extInputClient|r(?:eeNode|ackingArea))|InvocationOperation|Operation(?:Queue)?|D(?:ictionaryController|ockTile)|P(?:ointer(?:Functions|Array)|athC(?:o(?:ntrol(?:Delegate)?|mponentCell)|ell(?:Delegate)?)|r(?:intPanelAccessorizing|edicateEditor(?:RowTemplate)?))|ViewController|FastEnumeration|Animat(?:ionContext|ablePropertyContainer)))(?:\\b)"},{token:["support.class.cocoa"],regex:"(?:\\b)(NS(?:R(?:u(?:nLoop|ler(?:Marker|View))|e(?:sponder|cursiveLock|lativeSpecifier)|an(?:domSpecifier|geSpecifier))|G(?:etCommand|lyph(?:Generator|Storage|Info)|raphicsContext)|XML(?:Node|D(?:ocument|TD(?:Node)?)|Parser|Element)|M(?:iddleSpecifier|ov(?:ie(?:View)?|eCommand)|utable(?:S(?:tring|et)|C(?:haracterSet|opying)|IndexSet|D(?:ictionary|ata)|URLRequest|ParagraphStyle|A(?:ttributedString|rray))|e(?:ssagePort(?:NameServer)?|nu(?:Item(?:Cell)?|View)?|t(?:hodSignature|adata(?:Item|Query(?:ResultGroup|AttributeValueTuple)?)))|a(?:ch(?:BootstrapServer|Port)|trix))|B(?:itmapImageRep|ox|u(?:ndle|tton(?:Cell)?)|ezierPath|rowser(?:Cell)?)|S(?:hadow|c(?:anner|r(?:ipt(?:SuiteRegistry|C(?:o(?:ercionHandler|mmand(?:Description)?)|lassDescription)|ObjectSpecifier|ExecutionContext|WhoseTest)|oll(?:er|View)|een))|t(?:epper(?:Cell)?|atus(?:Bar|Item)|r(?:ing|eam))|imple(?:HorizontalTypesetter|CString)|o(?:cketPort(?:NameServer)?|und|rtDescriptor)|p(?:e(?:cifierTest|ech(?:Recognizer|Synthesizer)|ll(?:Server|Checker))|litView)|e(?:cureTextField(?:Cell)?|t(?:Command)?|archField(?:Cell)?|rializer|gmentedC(?:ontrol|ell))|lider(?:Cell)?|avePanel)|H(?:ost|TTP(?:Cookie(?:Storage)?|URLResponse)|elpManager)|N(?:ib(?:Con(?:nector|trolConnector)|OutletConnector)?|otification(?:Center|Queue)?|u(?:ll|mber(?:Formatter)?)|etService(?:Browser)?|ameSpecifier)|C(?:ha(?:ngeSpelling|racterSet)|o(?:n(?:stantString|nection|trol(?:ler)?|ditionLock)|d(?:ing|er)|unt(?:Command|edSet)|pying|lor(?:Space|P(?:ick(?:ing(?:Custom|Default)|er)|anel)|Well|List)?|m(?:p(?:oundPredicate|arisonPredicate)|boBox(?:Cell)?))|u(?:stomImageRep|rsor)|IImageRep|ell|l(?:ipView|o(?:seCommand|neCommand)|assDescription)|a(?:ched(?:ImageRep|URLResponse)|lendar(?:Date)?)|reateCommand)|T(?:hread|ypesetter|ime(?:Zone|r)|o(?:olbar(?:Item(?:Validations)?)?|kenField(?:Cell)?)|ext(?:Block|Storage|Container|Tab(?:le(?:Block)?)?|Input|View|Field(?:Cell)?|List|Attachment(?:Cell)?)?|a(?:sk|b(?:le(?:Header(?:Cell|View)|Column|View)|View(?:Item)?))|reeController)|I(?:n(?:dex(?:S(?:pecifier|et)|Path)|put(?:Manager|S(?:tream|erv(?:iceProvider|er(?:MouseTracker)?)))|vocation)|gnoreMisspelledWords|mage(?:Rep|Cell|View)?)|O(?:ut(?:putStream|lineView)|pen(?:GL(?:Context|Pixel(?:Buffer|Format)|View)|Panel)|bj(?:CTypeSerializationCallBack|ect(?:Controller)?))|D(?:i(?:st(?:antObject(?:Request)?|ributed(?:NotificationCenter|Lock))|ctionary|rectoryEnumerator)|ocument(?:Controller)?|e(?:serializer|cimalNumber(?:Behaviors|Handler)?|leteCommand)|at(?:e(?:Components|Picker(?:Cell)?|Formatter)?|a)|ra(?:wer|ggingInfo))|U(?:ser(?:InterfaceValidations|Defaults(?:Controller)?)|RL(?:Re(?:sponse|quest)|Handle(?:Client)?|C(?:onnection|ache|redential(?:Storage)?)|Download(?:Delegate)?|Prot(?:ocol(?:Client)?|ectionSpace)|AuthenticationChallenge(?:Sender)?)?|n(?:iqueIDSpecifier|doManager|archiver))|P(?:ipe|o(?:sitionalSpecifier|pUpButton(?:Cell)?|rt(?:Message|NameServer|Coder)?)|ICTImageRep|ersistentDocument|DFImageRep|a(?:steboard|nel|ragraphStyle|geLayout)|r(?:int(?:Info|er|Operation|Panel)|o(?:cessInfo|tocolChecker|perty(?:Specifier|ListSerialization)|gressIndicator|xy)|edicate))|E(?:numerator|vent|PSImageRep|rror|x(?:ception|istsCommand|pression))|V(?:iew(?:Animation)?|al(?:idated(?:ToobarItem|UserInterfaceItem)|ue(?:Transformer)?))|Keyed(?:Unarchiver|Archiver)|Qui(?:ckDrawView|tCommand)|F(?:ile(?:Manager|Handle|Wrapper)|o(?:nt(?:Manager|Descriptor|Panel)?|rm(?:Cell|atter)))|W(?:hoseSpecifier|indow(?:Controller)?|orkspace)|L(?:o(?:c(?:k(?:ing)?|ale)|gicalTest)|evelIndicator(?:Cell)?|ayoutManager)|A(?:ssertionHandler|nimation|ctionCell|ttributedString|utoreleasePool|TSTypesetter|ppl(?:ication|e(?:Script|Event(?:Manager|Descriptor)))|ffineTransform|lert|r(?:chiver|ray(?:Controller)?))))(?:\\b)"},{token:["support.type.cocoa.leopard"],regex:"(?:\\b)(NS(?:R(?:u(?:nLoop|ler(?:Marker|View))|e(?:sponder|cursiveLock|lativeSpecifier)|an(?:domSpecifier|geSpecifier))|G(?:etCommand|lyph(?:Generator|Storage|Info)|raphicsContext)|XML(?:Node|D(?:ocument|TD(?:Node)?)|Parser|Element)|M(?:iddleSpecifier|ov(?:ie(?:View)?|eCommand)|utable(?:S(?:tring|et)|C(?:haracterSet|opying)|IndexSet|D(?:ictionary|ata)|URLRequest|ParagraphStyle|A(?:ttributedString|rray))|e(?:ssagePort(?:NameServer)?|nu(?:Item(?:Cell)?|View)?|t(?:hodSignature|adata(?:Item|Query(?:ResultGroup|AttributeValueTuple)?)))|a(?:ch(?:BootstrapServer|Port)|trix))|B(?:itmapImageRep|ox|u(?:ndle|tton(?:Cell)?)|ezierPath|rowser(?:Cell)?)|S(?:hadow|c(?:anner|r(?:ipt(?:SuiteRegistry|C(?:o(?:ercionHandler|mmand(?:Description)?)|lassDescription)|ObjectSpecifier|ExecutionContext|WhoseTest)|oll(?:er|View)|een))|t(?:epper(?:Cell)?|atus(?:Bar|Item)|r(?:ing|eam))|imple(?:HorizontalTypesetter|CString)|o(?:cketPort(?:NameServer)?|und|rtDescriptor)|p(?:e(?:cifierTest|ech(?:Recognizer|Synthesizer)|ll(?:Server|Checker))|litView)|e(?:cureTextField(?:Cell)?|t(?:Command)?|archField(?:Cell)?|rializer|gmentedC(?:ontrol|ell))|lider(?:Cell)?|avePanel)|H(?:ost|TTP(?:Cookie(?:Storage)?|URLResponse)|elpManager)|N(?:ib(?:Con(?:nector|trolConnector)|OutletConnector)?|otification(?:Center|Queue)?|u(?:ll|mber(?:Formatter)?)|etService(?:Browser)?|ameSpecifier)|C(?:ha(?:ngeSpelling|racterSet)|o(?:n(?:stantString|nection|trol(?:ler)?|ditionLock)|d(?:ing|er)|unt(?:Command|edSet)|pying|lor(?:Space|P(?:ick(?:ing(?:Custom|Default)|er)|anel)|Well|List)?|m(?:p(?:oundPredicate|arisonPredicate)|boBox(?:Cell)?))|u(?:stomImageRep|rsor)|IImageRep|ell|l(?:ipView|o(?:seCommand|neCommand)|assDescription)|a(?:ched(?:ImageRep|URLResponse)|lendar(?:Date)?)|reateCommand)|T(?:hread|ypesetter|ime(?:Zone|r)|o(?:olbar(?:Item(?:Validations)?)?|kenField(?:Cell)?)|ext(?:Block|Storage|Container|Tab(?:le(?:Block)?)?|Input|View|Field(?:Cell)?|List|Attachment(?:Cell)?)?|a(?:sk|b(?:le(?:Header(?:Cell|View)|Column|View)|View(?:Item)?))|reeController)|I(?:n(?:dex(?:S(?:pecifier|et)|Path)|put(?:Manager|S(?:tream|erv(?:iceProvider|er(?:MouseTracker)?)))|vocation)|gnoreMisspelledWords|mage(?:Rep|Cell|View)?)|O(?:ut(?:putStream|lineView)|pen(?:GL(?:Context|Pixel(?:Buffer|Format)|View)|Panel)|bj(?:CTypeSerializationCallBack|ect(?:Controller)?))|D(?:i(?:st(?:antObject(?:Request)?|ributed(?:NotificationCenter|Lock))|ctionary|rectoryEnumerator)|ocument(?:Controller)?|e(?:serializer|cimalNumber(?:Behaviors|Handler)?|leteCommand)|at(?:e(?:Components|Picker(?:Cell)?|Formatter)?|a)|ra(?:wer|ggingInfo))|U(?:ser(?:InterfaceValidations|Defaults(?:Controller)?)|RL(?:Re(?:sponse|quest)|Handle(?:Client)?|C(?:onnection|ache|redential(?:Storage)?)|Download(?:Delegate)?|Prot(?:ocol(?:Client)?|ectionSpace)|AuthenticationChallenge(?:Sender)?)?|n(?:iqueIDSpecifier|doManager|archiver))|P(?:ipe|o(?:sitionalSpecifier|pUpButton(?:Cell)?|rt(?:Message|NameServer|Coder)?)|ICTImageRep|ersistentDocument|DFImageRep|a(?:steboard|nel|ragraphStyle|geLayout)|r(?:int(?:Info|er|Operation|Panel)|o(?:cessInfo|tocolChecker|perty(?:Specifier|ListSerialization)|gressIndicator|xy)|edicate))|E(?:numerator|vent|PSImageRep|rror|x(?:ception|istsCommand|pression))|V(?:iew(?:Animation)?|al(?:idated(?:ToobarItem|UserInterfaceItem)|ue(?:Transformer)?))|Keyed(?:Unarchiver|Archiver)|Qui(?:ckDrawView|tCommand)|F(?:ile(?:Manager|Handle|Wrapper)|o(?:nt(?:Manager|Descriptor|Panel)?|rm(?:Cell|atter)))|W(?:hoseSpecifier|indow(?:Controller)?|orkspace)|L(?:o(?:c(?:k(?:ing)?|ale)|gicalTest)|evelIndicator(?:Cell)?|ayoutManager)|A(?:ssertionHandler|nimation|ctionCell|ttributedString|utoreleasePool|TSTypesetter|ppl(?:ication|e(?:Script|Event(?:Manager|Descriptor)))|ffineTransform|lert|r(?:chiver|ray(?:Controller)?))))(?:\\b)"},{token:["support.class.quartz"],regex:"(?:\\b)(C(?:I(?:Sampler|Co(?:ntext|lor)|Image(?:Accumulator)?|PlugIn(?:Registration)?|Vector|Kernel|Filter(?:Generator|Shape)?)|A(?:Renderer|MediaTiming(?:Function)?|BasicAnimation|ScrollLayer|Constraint(?:LayoutManager)?|T(?:iledLayer|extLayer|rans(?:ition|action))|OpenGLLayer|PropertyAnimation|KeyframeAnimation|Layer|A(?:nimation(?:Group)?|ction))))(?:\\b)"},{token:["support.type.quartz"],regex:"(?:\\b)(C(?:G(?:Float|Point|Size|Rect)|IFormat|AConstraintAttribute))(?:\\b)"},{token:["support.type.cocoa"],regex:"(?:\\b)(NS(?:R(?:ect(?:Edge)?|ange)|G(?:lyph(?:Relation|LayoutMode)?|radientType)|M(?:odalSession|a(?:trixMode|p(?:Table|Enumerator)))|B(?:itmapImageFileType|orderType|uttonType|ezelStyle|ackingStoreType|rowserColumnResizingType)|S(?:cr(?:oll(?:er(?:Part|Arrow)|ArrowPosition)|eenAuxiliaryOpaque)|tringEncoding|ize|ocketNativeHandle|election(?:Granularity|Direction|Affinity)|wapped(?:Double|Float)|aveOperationType)|Ha(?:sh(?:Table|Enumerator)|ndler(?:2)?)|C(?:o(?:ntrol(?:Size|Tint)|mp(?:ositingOperation|arisonResult))|ell(?:State|Type|ImagePosition|Attribute))|T(?:hreadPrivate|ypesetterGlyphInfo|i(?:ckMarkPosition|tlePosition|meInterval)|o(?:ol(?:TipTag|bar(?:SizeMode|DisplayMode))|kenStyle)|IFFCompression|ext(?:TabType|Alignment)|ab(?:State|leViewDropOperation|ViewType)|rackingRectTag)|ImageInterpolation|Zone|OpenGL(?:ContextAuxiliary|PixelFormatAuxiliary)|D(?:ocumentChangeType|atePickerElementFlags|ra(?:werState|gOperation))|UsableScrollerParts|P(?:oint|r(?:intingPageOrder|ogressIndicator(?:Style|Th(?:ickness|readInfo))))|EventType|KeyValueObservingOptions|Fo(?:nt(?:SymbolicTraits|TraitMask|Action)|cusRingType)|W(?:indow(?:OrderingMode|Depth)|orkspace(?:IconCreationOptions|LaunchOptions)|ritingDirection)|L(?:ineBreakMode|ayout(?:Status|Direction))|A(?:nimation(?:Progress|Effect)|ppl(?:ication(?:TerminateReply|DelegateReply|PrintReply)|eEventManagerSuspensionID)|ffineTransformStruct|lertStyle)))(?:\\b)"},{token:["support.constant.cocoa"],regex:"(?:\\b)(NS(?:NotFound|Ordered(?:Ascending|Descending|Same)))(?:\\b)"},{token:["support.constant.notification.cocoa.leopard"],regex:"(?:\\b)(NS(?:MenuDidBeginTracking|ViewDidUpdateTrackingAreas)?Notification)(?:\\b)"},{token:["support.constant.notification.cocoa"],regex:"(?:\\b)(NS(?:Menu(?:Did(?:RemoveItem|SendAction|ChangeItem|EndTracking|AddItem)|WillSendAction)|S(?:ystemColorsDidChange|plitView(?:DidResizeSubviews|WillResizeSubviews))|C(?:o(?:nt(?:extHelpModeDid(?:Deactivate|Activate)|rolT(?:intDidChange|extDid(?:BeginEditing|Change|EndEditing)))|lor(?:PanelColorDidChange|ListDidChange)|mboBox(?:Selection(?:IsChanging|DidChange)|Will(?:Dismiss|PopUp)))|lassDescriptionNeededForClass)|T(?:oolbar(?:DidRemoveItem|WillAddItem)|ext(?:Storage(?:DidProcessEditing|WillProcessEditing)|Did(?:BeginEditing|Change|EndEditing)|View(?:DidChange(?:Selection|TypingAttributes)|WillChangeNotifyingTextView))|ableView(?:Selection(?:IsChanging|DidChange)|ColumnDid(?:Resize|Move)))|ImageRepRegistryDidChange|OutlineView(?:Selection(?:IsChanging|DidChange)|ColumnDid(?:Resize|Move)|Item(?:Did(?:Collapse|Expand)|Will(?:Collapse|Expand)))|Drawer(?:Did(?:Close|Open)|Will(?:Close|Open))|PopUpButton(?:CellWillPopUp|WillPopUp)|View(?:GlobalFrameDidChange|BoundsDidChange|F(?:ocusDidChange|rameDidChange))|FontSetChanged|W(?:indow(?:Did(?:Resi(?:ze|gn(?:Main|Key))|M(?:iniaturize|ove)|Become(?:Main|Key)|ChangeScreen(?:|Profile)|Deminiaturize|Update|E(?:ndSheet|xpose))|Will(?:M(?:iniaturize|ove)|BeginSheet|Close))|orkspace(?:SessionDid(?:ResignActive|BecomeActive)|Did(?:Mount|TerminateApplication|Unmount|PerformFileOperation|Wake|LaunchApplication)|Will(?:Sleep|Unmount|PowerOff|LaunchApplication)))|A(?:ntialiasThresholdChanged|ppl(?:ication(?:Did(?:ResignActive|BecomeActive|Hide|ChangeScreenParameters|U(?:nhide|pdate)|FinishLaunching)|Will(?:ResignActive|BecomeActive|Hide|Terminate|U(?:nhide|pdate)|FinishLaunching))|eEventManagerWillProcessFirstEvent)))Notification)(?:\\b)"},{token:["support.constant.cocoa.leopard"],regex:"(?:\\b)(NS(?:RuleEditor(?:RowType(?:Simple|Compound)|NestingMode(?:Si(?:ngle|mple)|Compound|List))|GradientDraws(?:BeforeStartingLocation|AfterEndingLocation)|M(?:inusSetExpressionType|a(?:chPortDeallocate(?:ReceiveRight|SendRight|None)|pTable(?:StrongMemory|CopyIn|ZeroingWeakMemory|ObjectPointerPersonality)))|B(?:oxCustom|undleExecutableArchitecture(?:X86|I386|PPC(?:64)?)|etweenPredicateOperatorType|ackgroundStyle(?:Raised|Dark|L(?:ight|owered)))|S(?:tring(?:DrawingTruncatesLastVisibleLine|EncodingConversion(?:ExternalRepresentation|AllowLossy))|ubqueryExpressionType|p(?:e(?:ech(?:SentenceBoundary|ImmediateBoundary|WordBoundary)|llingState(?:GrammarFlag|SpellingFlag))|litViewDividerStyleThi(?:n|ck))|e(?:rvice(?:RequestTimedOutError|M(?:iscellaneousError|alformedServiceDictionaryError)|InvalidPasteboardDataError|ErrorM(?:inimum|aximum)|Application(?:NotFoundError|LaunchFailedError))|gmentStyle(?:Round(?:Rect|ed)|SmallSquare|Capsule|Textured(?:Rounded|Square)|Automatic)))|H(?:UDWindowMask|ashTable(?:StrongMemory|CopyIn|ZeroingWeakMemory|ObjectPointerPersonality))|N(?:oModeColorPanel|etServiceNoAutoRename)|C(?:hangeRedone|o(?:ntainsPredicateOperatorType|l(?:orRenderingIntent(?:RelativeColorimetric|Saturation|Default|Perceptual|AbsoluteColorimetric)|lectorDisabledOption))|ellHit(?:None|ContentArea|TrackableArea|EditableTextArea))|T(?:imeZoneNameStyle(?:S(?:hort(?:Standard|DaylightSaving)|tandard)|DaylightSaving)|extFieldDatePickerStyle|ableViewSelectionHighlightStyle(?:Regular|SourceList)|racking(?:Mouse(?:Moved|EnteredAndExited)|CursorUpdate|InVisibleRect|EnabledDuringMouseDrag|A(?:ssumeInside|ctive(?:In(?:KeyWindow|ActiveApp)|WhenFirstResponder|Always))))|I(?:n(?:tersectSetExpressionType|dexedColorSpaceModel)|mageScale(?:None|Proportionally(?:Down|UpOrDown)|AxesIndependently))|Ope(?:nGLPFAAllowOfflineRenderers|rationQueue(?:DefaultMaxConcurrentOperationCount|Priority(?:High|Normal|Very(?:High|Low)|Low)))|D(?:iacriticInsensitiveSearch|ownloadsDirectory)|U(?:nionSetExpressionType|TF(?:16(?:BigEndianStringEncoding|StringEncoding|LittleEndianStringEncoding)|32(?:BigEndianStringEncoding|StringEncoding|LittleEndianStringEncoding)))|P(?:ointerFunctions(?:Ma(?:chVirtualMemory|llocMemory)|Str(?:ongMemory|uctPersonality)|C(?:StringPersonality|opyIn)|IntegerPersonality|ZeroingWeakMemory|O(?:paque(?:Memory|Personality)|bjectP(?:ointerPersonality|ersonality)))|at(?:hStyle(?:Standard|NavigationBar|PopUp)|ternColorSpaceModel)|rintPanelShows(?:Scaling|Copies|Orientation|P(?:a(?:perSize|ge(?:Range|SetupAccessory))|review)))|Executable(?:RuntimeMismatchError|NotLoadableError|ErrorM(?:inimum|aximum)|L(?:inkError|oadError)|ArchitectureMismatchError)|KeyValueObservingOption(?:Initial|Prior)|F(?:i(?:ndPanelSubstringMatchType(?:StartsWith|Contains|EndsWith|FullWord)|leRead(?:TooLargeError|UnknownStringEncodingError))|orcedOrderingSearch)|Wi(?:ndow(?:BackingLocation(?:MainMemory|Default|VideoMemory)|Sharing(?:Read(?:Only|Write)|None)|CollectionBehavior(?:MoveToActiveSpace|CanJoinAllSpaces|Default))|dthInsensitiveSearch)|AggregateExpressionType))(?:\\b)"},{token:["support.constant.cocoa"],regex:"(?:\\b)(NS(?:R(?:GB(?:ModeColorPanel|ColorSpaceModel)|ight(?:Mouse(?:D(?:own(?:Mask)?|ragged(?:Mask)?)|Up(?:Mask)?)|T(?:ext(?:Movement|Alignment)|ab(?:sBezelBorder|StopType))|ArrowFunctionKey)|ound(?:RectBezelStyle|Bankers|ed(?:BezelStyle|TokenStyle|DisclosureBezelStyle)|Down|Up|Plain|Line(?:CapStyle|JoinStyle))|un(?:StoppedResponse|ContinuesResponse|AbortedResponse)|e(?:s(?:izableWindowMask|et(?:CursorRectsRunLoopOrdering|FunctionKey))|ce(?:ssedBezelStyle|iver(?:sCantHandleCommandScriptError|EvaluationScriptError))|turnTextMovement|doFunctionKey|quiredArgumentsMissingScriptError|l(?:evancyLevelIndicatorStyle|ative(?:Before|After))|gular(?:SquareBezelStyle|ControlSize)|moveTraitFontAction)|a(?:n(?:domSubelement|geDateMode)|tingLevelIndicatorStyle|dio(?:ModeMatrix|Button)))|G(?:IFFileType|lyph(?:Below|Inscribe(?:B(?:elow|ase)|Over(?:strike|Below)|Above)|Layout(?:WithPrevious|A(?:tAPoint|gainstAPoint))|A(?:ttribute(?:BidiLevel|Soft|Inscribe|Elastic)|bove))|r(?:ooveBorder|eaterThan(?:Comparison|OrEqualTo(?:Comparison|PredicateOperatorType)|PredicateOperatorType)|a(?:y(?:ModeColorPanel|ColorSpaceModel)|dient(?:None|Con(?:cave(?:Strong|Weak)|vex(?:Strong|Weak)))|phiteControlTint)))|XML(?:N(?:o(?:tationDeclarationKind|de(?:CompactEmptyElement|IsCDATA|OptionsNone|Use(?:SingleQuotes|DoubleQuotes)|Pre(?:serve(?:NamespaceOrder|C(?:haracterReferences|DATA)|DTD|Prefixes|E(?:ntities|mptyElements)|Quotes|Whitespace|A(?:ttributeOrder|ll))|ttyPrint)|ExpandEmptyElement))|amespaceKind)|CommentKind|TextKind|InvalidKind|D(?:ocument(?:X(?:MLKind|HTMLKind|Include)|HTMLKind|T(?:idy(?:XML|HTML)|extKind)|IncludeContentTypeDeclaration|Validate|Kind)|TDKind)|P(?:arser(?:GTRequiredError|XMLDeclNot(?:StartedError|FinishedError)|Mi(?:splaced(?:XMLDeclarationError|CDATAEndStringError)|xedContentDeclNot(?:StartedError|FinishedError))|S(?:t(?:andaloneValueError|ringNot(?:StartedError|ClosedError))|paceRequiredError|eparatorRequiredError)|N(?:MTOKENRequiredError|o(?:t(?:ationNot(?:StartedError|FinishedError)|WellBalancedError)|DTDError)|amespaceDeclarationError|AMERequiredError)|C(?:haracterRef(?:In(?:DTDError|PrologError|EpilogError)|AtEOFError)|o(?:nditionalSectionNot(?:StartedError|FinishedError)|mment(?:NotFinishedError|ContainsDoubleHyphenError))|DATANotFinishedError)|TagNameMismatchError|In(?:ternalError|valid(?:HexCharacterRefError|C(?:haracter(?:RefError|InEntityError|Error)|onditionalSectionError)|DecimalCharacterRefError|URIError|Encoding(?:NameError|Error)))|OutOfMemoryError|D(?:ocumentStartError|elegateAbortedParseError|OCTYPEDeclNotFinishedError)|U(?:RI(?:RequiredError|FragmentError)|n(?:declaredEntityError|parsedEntityError|knownEncodingError|finishedTagError))|P(?:CDATARequiredError|ublicIdentifierRequiredError|arsedEntityRef(?:MissingSemiError|NoNameError|In(?:Internal(?:SubsetError|Error)|PrologError|EpilogError)|AtEOFError)|r(?:ocessingInstructionNot(?:StartedError|FinishedError)|ematureDocumentEndError))|E(?:n(?:codingNotSupportedError|tity(?:Ref(?:In(?:DTDError|PrologError|EpilogError)|erence(?:MissingSemiError|WithoutNameError)|LoopError|AtEOFError)|BoundaryError|Not(?:StartedError|FinishedError)|Is(?:ParameterError|ExternalError)|ValueRequiredError))|qualExpectedError|lementContentDeclNot(?:StartedError|FinishedError)|xt(?:ernalS(?:tandaloneEntityError|ubsetNotFinishedError)|raContentError)|mptyDocumentError)|L(?:iteralNot(?:StartedError|FinishedError)|T(?:RequiredError|SlashRequiredError)|essThanSymbolInAttributeError)|Attribute(?:RedefinedError|HasNoValueError|Not(?:StartedError|FinishedError)|ListNot(?:StartedError|FinishedError)))|rocessingInstructionKind)|E(?:ntity(?:GeneralKind|DeclarationKind|UnparsedKind|P(?:ar(?:sedKind|ameterKind)|redefined))|lement(?:Declaration(?:MixedKind|UndefinedKind|E(?:lementKind|mptyKind)|Kind|AnyKind)|Kind))|Attribute(?:N(?:MToken(?:sKind|Kind)|otationKind)|CDATAKind|ID(?:Ref(?:sKind|Kind)|Kind)|DeclarationKind|En(?:tit(?:yKind|iesKind)|umerationKind)|Kind))|M(?:i(?:n(?:XEdge|iaturizableWindowMask|YEdge|uteCalendarUnit)|terLineJoinStyle|ddleSubelement|xedState)|o(?:nthCalendarUnit|deSwitchFunctionKey|use(?:Moved(?:Mask)?|E(?:ntered(?:Mask)?|ventSubtype|xited(?:Mask)?))|veToBezierPathElement|mentary(?:ChangeButton|Push(?:Button|InButton)|Light(?:Button)?))|enuFunctionKey|a(?:c(?:intoshInterfaceStyle|OSRomanStringEncoding)|tchesPredicateOperatorType|ppedRead|x(?:XEdge|YEdge))|ACHOperatingSystem)|B(?:MPFileType|o(?:ttomTabsBezelBorder|ldFontMask|rderlessWindowMask|x(?:Se(?:condary|parator)|OldStyle|Primary))|uttLineCapStyle|e(?:zelBorder|velLineJoinStyle|low(?:Bottom|Top)|gin(?:sWith(?:Comparison|PredicateOperatorType)|FunctionKey))|lueControlTint|ack(?:spaceCharacter|tabTextMovement|ingStore(?:Retained|Buffered|Nonretained)|TabCharacter|wardsSearch|groundTab)|r(?:owser(?:NoColumnResizing|UserColumnResizing|AutoColumnResizing)|eakFunctionKey))|S(?:h(?:ift(?:JISStringEncoding|KeyMask)|ow(?:ControlGlyphs|InvisibleGlyphs)|adowlessSquareBezelStyle)|y(?:s(?:ReqFunctionKey|tem(?:D(?:omainMask|efined(?:Mask)?)|FunctionKey))|mbolStringEncoding)|c(?:a(?:nnedOption|le(?:None|ToFit|Proportionally))|r(?:oll(?:er(?:NoPart|Increment(?:Page|Line|Arrow)|Decrement(?:Page|Line|Arrow)|Knob(?:Slot)?|Arrows(?:M(?:inEnd|axEnd)|None|DefaultSetting))|Wheel(?:Mask)?|LockFunctionKey)|eenChangedEventType))|t(?:opFunctionKey|r(?:ingDrawing(?:OneShot|DisableScreenFontSubstitution|Uses(?:DeviceMetrics|FontLeading|LineFragmentOrigin))|eam(?:Status(?:Reading|NotOpen|Closed|Open(?:ing)?|Error|Writing|AtEnd)|Event(?:Has(?:BytesAvailable|SpaceAvailable)|None|OpenCompleted|E(?:ndEncountered|rrorOccurred)))))|i(?:ngle(?:DateMode|UnderlineStyle)|ze(?:DownFontAction|UpFontAction))|olarisOperatingSystem|unOSOperatingSystem|pecialPageOrder|e(?:condCalendarUnit|lect(?:By(?:Character|Paragraph|Word)|i(?:ng(?:Next|Previous)|onAffinity(?:Downstream|Upstream))|edTab|FunctionKey)|gmentSwitchTracking(?:Momentary|Select(?:One|Any)))|quareLineCapStyle|witchButton|ave(?:ToOperation|Op(?:tions(?:Yes|No|Ask)|eration)|AsOperation)|mall(?:SquareBezelStyle|C(?:ontrolSize|apsFontMask)|IconButtonBezelStyle))|H(?:ighlightModeMatrix|SBModeColorPanel|o(?:ur(?:Minute(?:SecondDatePickerElementFlag|DatePickerElementFlag)|CalendarUnit)|rizontalRuler|meFunctionKey)|TTPCookieAcceptPolicy(?:Never|OnlyFromMainDocumentDomain|Always)|e(?:lp(?:ButtonBezelStyle|KeyMask|FunctionKey)|avierFontAction)|PUXOperatingSystem)|Year(?:MonthDa(?:yDatePickerElementFlag|tePickerElementFlag)|CalendarUnit)|N(?:o(?:n(?:StandardCharacterSetFontMask|ZeroWindingRule|activatingPanelMask|LossyASCIIStringEncoding)|Border|t(?:ification(?:SuspensionBehavior(?:Hold|Coalesce|D(?:eliverImmediately|rop))|NoCoalescing|CoalescingOn(?:Sender|Name)|DeliverImmediately|PostToAllSessions)|PredicateType|EqualToPredicateOperatorType)|S(?:cr(?:iptError|ollerParts)|ubelement|pecifierError)|CellMask|T(?:itle|opLevelContainersSpecifierError|abs(?:BezelBorder|NoBorder|LineBorder))|I(?:nterfaceStyle|mage)|UnderlineStyle|FontChangeAction)|u(?:ll(?:Glyph|CellType)|m(?:eric(?:Search|PadKeyMask)|berFormatter(?:Round(?:Half(?:Down|Up|Even)|Ceiling|Down|Up|Floor)|Behavior(?:10|Default)|S(?:cientificStyle|pellOutStyle)|NoStyle|CurrencyStyle|DecimalStyle|P(?:ercentStyle|ad(?:Before(?:Suffix|Prefix)|After(?:Suffix|Prefix))))))|e(?:t(?:Services(?:BadArgumentError|NotFoundError|C(?:ollisionError|ancelledError)|TimeoutError|InvalidError|UnknownError|ActivityInProgress)|workDomainMask)|wlineCharacter|xt(?:StepInterfaceStyle|FunctionKey))|EXTSTEPStringEncoding|a(?:t(?:iveShortGlyphPacking|uralTextAlignment)|rrowFontMask))|C(?:hange(?:ReadOtherContents|GrayCell(?:Mask)?|BackgroundCell(?:Mask)?|Cleared|Done|Undone|Autosaved)|MYK(?:ModeColorPanel|ColorSpaceModel)|ircular(?:BezelStyle|Slider)|o(?:n(?:stantValueExpressionType|t(?:inuousCapacityLevelIndicatorStyle|entsCellMask|ain(?:sComparison|erSpecifierError)|rol(?:Glyph|KeyMask))|densedFontMask)|lor(?:Panel(?:RGBModeMask|GrayModeMask|HSBModeMask|C(?:MYKModeMask|olorListModeMask|ustomPaletteModeMask|rayonModeMask)|WheelModeMask|AllModesMask)|ListModeColorPanel)|reServiceDirectory|m(?:p(?:osite(?:XOR|Source(?:In|O(?:ut|ver)|Atop)|Highlight|C(?:opy|lear)|Destination(?:In|O(?:ut|ver)|Atop)|Plus(?:Darker|Lighter))|ressedFontMask)|mandKeyMask))|u(?:stom(?:SelectorPredicateOperatorType|PaletteModeColorPanel)|r(?:sor(?:Update(?:Mask)?|PointingDevice)|veToBezierPathElement))|e(?:nterT(?:extAlignment|abStopType)|ll(?:State|H(?:ighlighted|as(?:Image(?:Horizontal|OnLeftOrBottom)|OverlappingImage))|ChangesContents|Is(?:Bordered|InsetButton)|Disabled|Editable|LightsBy(?:Gray|Background|Contents)|AllowsMixedState))|l(?:ipPagination|o(?:s(?:ePathBezierPathElement|ableWindowMask)|ckAndCalendarDatePickerStyle)|ear(?:ControlTint|DisplayFunctionKey|LineFunctionKey))|a(?:seInsensitive(?:Search|PredicateOption)|n(?:notCreateScriptCommandError|cel(?:Button|TextMovement))|chesDirectory|lculation(?:NoError|Overflow|DivideByZero|Underflow|LossOfPrecision)|rriageReturnCharacter)|r(?:itical(?:Request|AlertStyle)|ayonModeColorPanel))|T(?:hick(?:SquareBezelStyle|erSquareBezelStyle)|ypesetter(?:Behavior|HorizontalTabAction|ContainerBreakAction|ZeroAdvancementAction|OriginalBehavior|ParagraphBreakAction|WhitespaceAction|L(?:ineBreakAction|atestBehavior))|i(?:ckMark(?:Right|Below|Left|Above)|tledWindowMask|meZoneDatePickerElementFlag)|o(?:olbarItemVisibilityPriority(?:Standard|High|User|Low)|pTabsBezelBorder|ggleButton)|IFF(?:Compression(?:N(?:one|EXT)|CCITTFAX(?:3|4)|OldJPEG|JPEG|PackBits|LZW)|FileType)|e(?:rminate(?:Now|Cancel|Later)|xt(?:Read(?:InapplicableDocumentTypeError|WriteErrorM(?:inimum|aximum))|Block(?:M(?:i(?:nimum(?:Height|Width)|ddleAlignment)|a(?:rgin|ximum(?:Height|Width)))|B(?:o(?:ttomAlignment|rder)|aselineAlignment)|Height|TopAlignment|P(?:ercentageValueType|adding)|Width|AbsoluteValueType)|StorageEdited(?:Characters|Attributes)|CellType|ured(?:RoundedBezelStyle|BackgroundWindowMask|SquareBezelStyle)|Table(?:FixedLayoutAlgorithm|AutomaticLayoutAlgorithm)|Field(?:RoundedBezel|SquareBezel|AndStepperDatePickerStyle)|WriteInapplicableDocumentTypeError|ListPrependEnclosingMarker))|woByteGlyphPacking|ab(?:Character|TextMovement|le(?:tP(?:oint(?:Mask|EventSubtype)?|roximity(?:Mask|EventSubtype)?)|Column(?:NoResizing|UserResizingMask|AutoresizingMask)|View(?:ReverseSequentialColumnAutoresizingStyle|GridNone|S(?:olid(?:HorizontalGridLineMask|VerticalGridLineMask)|equentialColumnAutoresizingStyle)|NoColumnAutoresizing|UniformColumnAutoresizingStyle|FirstColumnOnlyAutoresizingStyle|LastColumnOnlyAutoresizingStyle)))|rackModeMatrix)|I(?:n(?:sert(?:CharFunctionKey|FunctionKey|LineFunctionKey)|t(?:Type|ernalS(?:criptError|pecifierError))|dexSubelement|validIndexSpecifierError|formational(?:Request|AlertStyle)|PredicateOperatorType)|talicFontMask|SO(?:2022JPStringEncoding|Latin(?:1StringEncoding|2StringEncoding))|dentityMappingCharacterCollection|llegalTextMovement|mage(?:R(?:ight|ep(?:MatchesDevice|LoadStatus(?:ReadingHeader|Completed|InvalidData|Un(?:expectedEOF|knownType)|WillNeedAllData)))|Below|C(?:ellType|ache(?:BySize|Never|Default|Always))|Interpolation(?:High|None|Default|Low)|O(?:nly|verlaps)|Frame(?:Gr(?:oove|ayBezel)|Button|None|Photo)|L(?:oadStatus(?:ReadError|C(?:ompleted|ancelled)|InvalidData|UnexpectedEOF)|eft)|A(?:lign(?:Right|Bottom(?:Right|Left)?|Center|Top(?:Right|Left)?|Left)|bove)))|O(?:n(?:State|eByteGlyphPacking|OffButton|lyScrollerArrows)|ther(?:Mouse(?:D(?:own(?:Mask)?|ragged(?:Mask)?)|Up(?:Mask)?)|TextMovement)|SF1OperatingSystem|pe(?:n(?:GL(?:GO(?:Re(?:setLibrary|tainRenderers)|ClearFormatCache|FormatCacheSize)|PFA(?:R(?:obust|endererID)|M(?:inimumPolicy|ulti(?:sample|Screen)|PSafe|aximumPolicy)|BackingStore|S(?:creenMask|te(?:ncilSize|reo)|ingleRenderer|upersample|ample(?:s|Buffers|Alpha))|NoRecovery|C(?:o(?:lor(?:Size|Float)|mpliant)|losestPolicy)|OffScreen|D(?:oubleBuffer|epthSize)|PixelBuffer|VirtualScreenCount|FullScreen|Window|A(?:cc(?:umSize|elerated)|ux(?:Buffers|DepthStencil)|l(?:phaSize|lRenderers))))|StepUnicodeReservedBase)|rationNotSupportedForKeyS(?:criptError|pecifierError))|ffState|KButton|rPredicateType|bjC(?:B(?:itfield|oolType)|S(?:hortType|tr(?:ingType|uctType)|electorType)|NoType|CharType|ObjectType|DoubleType|UnionType|PointerType|VoidType|FloatType|Long(?:Type|longType)|ArrayType))|D(?:i(?:s(?:c(?:losureBezelStyle|reteCapacityLevelIndicatorStyle)|playWindowRunLoopOrdering)|acriticInsensitivePredicateOption|rect(?:Selection|PredicateModifier))|o(?:c(?:ModalWindowMask|ument(?:Directory|ationDirectory))|ubleType|wn(?:TextMovement|ArrowFunctionKey))|e(?:s(?:cendingPageOrder|ktopDirectory)|cimalTabStopType|v(?:ice(?:NColorSpaceModel|IndependentModifierFlagsMask)|eloper(?:Directory|ApplicationDirectory))|fault(?:ControlTint|TokenStyle)|lete(?:Char(?:acter|FunctionKey)|FunctionKey|LineFunctionKey)|moApplicationDirectory)|a(?:yCalendarUnit|teFormatter(?:MediumStyle|Behavior(?:10|Default)|ShortStyle|NoStyle|FullStyle|LongStyle))|ra(?:wer(?:Clos(?:ingState|edState)|Open(?:ingState|State))|gOperation(?:Generic|Move|None|Copy|Delete|Private|Every|Link|All)))|U(?:ser(?:CancelledError|D(?:irectory|omainMask)|FunctionKey)|RL(?:Handle(?:NotLoaded|Load(?:Succeeded|InProgress|Failed))|CredentialPersistence(?:None|Permanent|ForSession))|n(?:scaledWindowMask|cachedRead|i(?:codeStringEncoding|talicFontMask|fiedTitleAndToolbarWindowMask)|d(?:o(?:CloseGroupingRunLoopOrdering|FunctionKey)|e(?:finedDateComponent|rline(?:Style(?:Single|None|Thick|Double)|Pattern(?:Solid|D(?:ot|ash(?:Dot(?:Dot)?)?)))))|known(?:ColorSpaceModel|P(?:ointingDevice|ageOrder)|KeyS(?:criptError|pecifierError))|boldFontMask)|tilityWindowMask|TF8StringEncoding|p(?:dateWindowsRunLoopOrdering|TextMovement|ArrowFunctionKey))|J(?:ustifiedTextAlignment|PEG(?:2000FileType|FileType)|apaneseEUC(?:GlyphPacking|StringEncoding))|P(?:o(?:s(?:t(?:Now|erFontMask|WhenIdle|ASAP)|iti(?:on(?:Replace|Be(?:fore|ginning)|End|After)|ve(?:IntType|DoubleType|FloatType)))|pUp(?:NoArrow|ArrowAt(?:Bottom|Center))|werOffEventType|rtraitOrientation)|NGFileType|ush(?:InCell(?:Mask)?|OnPushOffButton)|e(?:n(?:TipMask|UpperSideMask|PointingDevice|LowerSideMask)|riodic(?:Mask)?)|P(?:S(?:caleField|tatus(?:Title|Field)|aveButton)|N(?:ote(?:Title|Field)|ame(?:Title|Field))|CopiesField|TitleField|ImageButton|OptionsButton|P(?:a(?:perFeedButton|ge(?:Range(?:To|From)|ChoiceMatrix))|reviewButton)|LayoutButton)|lainTextTokenStyle|a(?:useFunctionKey|ragraphSeparatorCharacter|ge(?:DownFunctionKey|UpFunctionKey))|r(?:int(?:ing(?:ReplyLater|Success|Cancelled|Failure)|ScreenFunctionKey|erTable(?:NotFound|OK|Error)|FunctionKey)|o(?:p(?:ertyList(?:XMLFormat|MutableContainers(?:AndLeaves)?|BinaryFormat|Immutable|OpenStepFormat)|rietaryStringEncoding)|gressIndicator(?:BarStyle|SpinningStyle|Preferred(?:SmallThickness|Thickness|LargeThickness|AquaThickness)))|e(?:ssedTab|vFunctionKey))|L(?:HeightForm|CancelButton|TitleField|ImageButton|O(?:KButton|rientationMatrix)|UnitsButton|PaperNameButton|WidthForm))|E(?:n(?:terCharacter|d(?:sWith(?:Comparison|PredicateOperatorType)|FunctionKey))|v(?:e(?:nOddWindingRule|rySubelement)|aluatedObjectExpressionType)|qualTo(?:Comparison|PredicateOperatorType)|ra(?:serPointingDevice|CalendarUnit|DatePickerElementFlag)|x(?:clude(?:10|QuickDrawElementsIconCreationOption)|pandedFontMask|ecuteFunctionKey))|V(?:i(?:ew(?:M(?:in(?:XMargin|YMargin)|ax(?:XMargin|YMargin))|HeightSizable|NotSizable|WidthSizable)|aPanelFontAction)|erticalRuler|a(?:lidationErrorM(?:inimum|aximum)|riableExpressionType))|Key(?:SpecifierEvaluationScriptError|Down(?:Mask)?|Up(?:Mask)?|PathExpressionType|Value(?:MinusSetMutation|SetSetMutation|Change(?:Re(?:placement|moval)|Setting|Insertion)|IntersectSetMutation|ObservingOption(?:New|Old)|UnionSetMutation|ValidationError))|QTMovie(?:NormalPlayback|Looping(?:BackAndForthPlayback|Playback))|F(?:1(?:1FunctionKey|7FunctionKey|2FunctionKey|8FunctionKey|3FunctionKey|9FunctionKey|4FunctionKey|5FunctionKey|FunctionKey|0FunctionKey|6FunctionKey)|7FunctionKey|i(?:nd(?:PanelAction(?:Replace(?:A(?:ndFind|ll(?:InSelection)?))?|S(?:howFindPanel|e(?:tFindString|lectAll(?:InSelection)?))|Next|Previous)|FunctionKey)|tPagination|le(?:Read(?:No(?:SuchFileError|PermissionError)|CorruptFileError|In(?:validFileNameError|applicableStringEncodingError)|Un(?:supportedSchemeError|knownError))|HandlingPanel(?:CancelButton|OKButton)|NoSuchFileError|ErrorM(?:inimum|aximum)|Write(?:NoPermissionError|In(?:validFileNameError|applicableStringEncodingError)|OutOfSpaceError|Un(?:supportedSchemeError|knownError))|LockingError)|xedPitchFontMask)|2(?:1FunctionKey|7FunctionKey|2FunctionKey|8FunctionKey|3FunctionKey|9FunctionKey|4FunctionKey|5FunctionKey|FunctionKey|0FunctionKey|6FunctionKey)|o(?:nt(?:Mo(?:noSpaceTrait|dernSerifsClass)|BoldTrait|S(?:ymbolicClass|criptsClass|labSerifsClass|ansSerifClass)|C(?:o(?:ndensedTrait|llectionApplicationOnlyMask)|larendonSerifsClass)|TransitionalSerifsClass|I(?:ntegerAdvancementsRenderingMode|talicTrait)|O(?:ldStyleSerifsClass|rnamentalsClass)|DefaultRenderingMode|U(?:nknownClass|IOptimizedTrait)|Panel(?:S(?:hadowEffectModeMask|t(?:andardModesMask|rikethroughEffectModeMask)|izeModeMask)|CollectionModeMask|TextColorEffectModeMask|DocumentColorEffectModeMask|UnderlineEffectModeMask|FaceModeMask|All(?:ModesMask|EffectsModeMask))|ExpandedTrait|VerticalTrait|F(?:amilyClassMask|reeformSerifsClass)|Antialiased(?:RenderingMode|IntegerAdvancementsRenderingMode))|cusRing(?:Below|Type(?:None|Default|Exterior)|Only|Above)|urByteGlyphPacking|rm(?:attingError(?:M(?:inimum|aximum))?|FeedCharacter))|8FunctionKey|unction(?:ExpressionType|KeyMask)|3(?:1FunctionKey|2FunctionKey|3FunctionKey|4FunctionKey|5FunctionKey|FunctionKey|0FunctionKey)|9FunctionKey|4FunctionKey|P(?:RevertButton|S(?:ize(?:Title|Field)|etButton)|CurrentField|Preview(?:Button|Field))|l(?:oat(?:ingPointSamplesBitmapFormat|Type)|agsChanged(?:Mask)?)|axButton|5FunctionKey|6FunctionKey)|W(?:heelModeColorPanel|indow(?:s(?:NTOperatingSystem|CP125(?:1StringEncoding|2StringEncoding|3StringEncoding|4StringEncoding|0StringEncoding)|95(?:InterfaceStyle|OperatingSystem))|M(?:iniaturizeButton|ovedEventType)|Below|CloseButton|ToolbarButton|ZoomButton|Out|DocumentIconButton|ExposedEventType|Above)|orkspaceLaunch(?:NewInstance|InhibitingBackgroundOnly|Default|PreferringClassic|WithoutA(?:ctivation|ddingToRecents)|A(?:sync|nd(?:Hide(?:Others)?|Print)|llowingClassicStartup))|eek(?:day(?:CalendarUnit|OrdinalCalendarUnit)|CalendarUnit)|a(?:ntsBidiLevels|rningAlertStyle)|r(?:itingDirection(?:RightToLeft|Natural|LeftToRight)|apCalendarComponents))|L(?:i(?:stModeMatrix|ne(?:Moves(?:Right|Down|Up|Left)|B(?:order|reakBy(?:C(?:harWrapping|lipping)|Truncating(?:Middle|Head|Tail)|WordWrapping))|S(?:eparatorCharacter|weep(?:Right|Down|Up|Left))|ToBezierPathElement|DoesntMove|arSlider)|teralSearch|kePredicateOperatorType|ghterFontAction|braryDirectory)|ocalDomainMask|e(?:ssThan(?:Comparison|OrEqualTo(?:Comparison|PredicateOperatorType)|PredicateOperatorType)|ft(?:Mouse(?:D(?:own(?:Mask)?|ragged(?:Mask)?)|Up(?:Mask)?)|T(?:ext(?:Movement|Alignment)|ab(?:sBezelBorder|StopType))|ArrowFunctionKey))|a(?:yout(?:RightToLeft|NotDone|CantFit|OutOfGlyphs|Done|LeftToRight)|ndscapeOrientation)|ABColorSpaceModel)|A(?:sc(?:iiWithDoubleByteEUCGlyphPacking|endingPageOrder)|n(?:y(?:Type|PredicateModifier|EventMask)|choredSearch|imation(?:Blocking|Nonblocking(?:Threaded)?|E(?:ffect(?:DisappearingItemDefault|Poof)|ase(?:In(?:Out)?|Out))|Linear)|dPredicateType)|t(?:Bottom|tachmentCharacter|omicWrite|Top)|SCIIStringEncoding|d(?:obe(?:GB1CharacterCollection|CNS1CharacterCollection|Japan(?:1CharacterCollection|2CharacterCollection)|Korea1CharacterCollection)|dTraitFontAction|minApplicationDirectory)|uto(?:saveOperation|Pagination)|pp(?:lication(?:SupportDirectory|D(?:irectory|e(?:fined(?:Mask)?|legateReply(?:Success|Cancel|Failure)|activatedEventType))|ActivatedEventType)|KitDefined(?:Mask)?)|l(?:ternateKeyMask|pha(?:ShiftKeyMask|NonpremultipliedBitmapFormat|FirstBitmapFormat)|ert(?:SecondButtonReturn|ThirdButtonReturn|OtherReturn|DefaultReturn|ErrorReturn|FirstButtonReturn|AlternateReturn)|l(?:ScrollerParts|DomainsMask|PredicateModifier|LibrariesDirectory|ApplicationsDirectory))|rgument(?:sWrongScriptError|EvaluationScriptError)|bove(?:Bottom|Top)|WTEventType)))(?:\\b)"},{token:"support.function.C99.c",regex:s.cFunctions},{token:n.getKeywords(),regex:"[a-zA-Z_$][a-zA-Z0-9_$]*\\b"},{token:"punctuation.section.scope.begin.objc",regex:"\\[",next:"bracketed_content"},{token:"meta.function.objc",regex:"^(?:-|\\+)\\s*"}],constant_NSString:[{token:"constant.character.escape.objc",regex:e},{token:"invalid.illegal.unknown-escape.objc",regex:"\\\\."},{token:"string",regex:'[^"\\\\]+'},{token:"punctuation.definition.string.end",regex:'"',next:"start"}],protocol_list:[{token:"punctuation.section.scope.end.objc",regex:">",next:"start"},{token:"support.other.protocol.objc",regex:"\bNS(?:GlyphStorage|M(?:utableCopying|enuItem)|C(?:hangeSpelling|o(?:ding|pying|lorPicking(?:Custom|Default)))|T(?:oolbarItemValidations|ext(?:Input|AttachmentCell))|I(?:nputServ(?:iceProvider|erMouseTracker)|gnoreMisspelledWords)|Obj(?:CTypeSerializationCallBack|ect)|D(?:ecimalNumberBehaviors|raggingInfo)|U(?:serInterfaceValidations|RL(?:HandleClient|DownloadDelegate|ProtocolClient|AuthenticationChallengeSender))|Validated(?:ToobarItem|UserInterfaceItem)|Locking)\b"}],selectors:[{token:"support.function.any-method.name-of-parameter.objc",regex:"\\b(?:[a-zA-Z_:][\\w]*)+"},{token:"punctuation",regex:"\\)",next:"start"}],bracketed_content:[{token:"punctuation.section.scope.end.objc",regex:"]",next:"start"},{token:["support.function.any-method.objc"],regex:"(?:predicateWithFormat:| NSPredicate predicateWithFormat:)",next:"start"},{token:"support.function.any-method.objc",regex:"\\w+(?::|(?=]))",next:"start"}],bracketed_strings:[{token:"punctuation.section.scope.end.objc",regex:"]",next:"start"},{token:"keyword.operator.logical.predicate.cocoa",regex:"\\b(?:AND|OR|NOT|IN)\\b"},{token:["invalid.illegal.unknown-method.objc","punctuation.separator.arguments.objc"],regex:"\\b(\\w+)(:)"},{regex:"\\b(?:ALL|ANY|SOME|NONE)\\b",token:"constant.language.predicate.cocoa"},{regex:"\\b(?:NULL|NIL|SELF|TRUE|YES|FALSE|NO|FIRST|LAST|SIZE)\\b",token:"constant.language.predicate.cocoa"},{regex:"\\b(?:MATCHES|CONTAINS|BEGINSWITH|ENDSWITH|BETWEEN)\\b",token:"keyword.operator.comparison.predicate.cocoa"},{regex:"\\bC(?:ASEINSENSITIVE|I)\\b",token:"keyword.other.modifier.predicate.cocoa"},{regex:"\\b(?:ANYKEY|SUBQUERY|CAST|TRUEPREDICATE|FALSEPREDICATE)\\b",token:"keyword.other.predicate.cocoa"},{regex:e,token:"constant.character.escape.objc"},{regex:"\\\\.",token:"invalid.illegal.unknown-escape.objc"},{token:"string",regex:'[^"\\\\]'},{token:"punctuation.definition.string.end.objc",regex:'"',next:"predicates"}],comment:[{token:"comment",regex:".*?\\*\\/",next:"start"},{token:"comment",regex:".+"}],methods:[{token:"meta.function.objc",regex:"(?=\\{|#)|;",next:"start"}]};for(var u in r)this.$rules[u]?this.$rules[u].push&&this.$rules[u].push.apply(this.$rules[u],r[u]):this.$rules[u]=r[u];this.$rules.bracketed_content=this.$rules.bracketed_content.concat(this.$rules.start,t),this.embedRules(i,"doc-",[i.getEndRule("start")])};r.inherits(u,o),t.ObjectiveCHighlightRules=u}),define("ace/mode/folding/cstyle",["require","exports","module","ace/lib/oop","ace/range","ace/mode/folding/fold_mode"],function(e,t,n){"use strict";var r=e("../../lib/oop"),i=e("../../range").Range,s=e("./fold_mode").FoldMode,o=t.FoldMode=function(e){e&&(this.foldingStartMarker=new RegExp(this.foldingStartMarker.source.replace(/\|[^|]*?$/,"|"+e.start)),this.foldingStopMarker=new RegExp(this.foldingStopMarker.source.replace(/\|[^|]*?$/,"|"+e.end)))};r.inherits(o,s),function(){this.foldingStartMarker=/(\{|\[)[^\}\]]*$|^\s*(\/\*)/,this.foldingStopMarker=/^[^\[\{]*(\}|\])|^[\s\*]*(\*\/)/,this.singleLineBlockCommentRe=/^\s*(\/\*).*\*\/\s*$/,this.tripleStarBlockCommentRe=/^\s*(\/\*\*\*).*\*\/\s*$/,this.startRegionRe=/^\s*(\/\*|\/\/)#?region\b/,this._getFoldWidgetBase=this.getFoldWidget,this.getFoldWidget=function(e,t,n){var r=e.getLine(n);if(this.singleLineBlockCommentRe.test(r)&&!this.startRegionRe.test(r)&&!this.tripleStarBlockCommentRe.test(r))return"";var i=this._getFoldWidgetBase(e,t,n);return!i&&this.startRegionRe.test(r)?"start":i},this.getFoldWidgetRange=function(e,t,n,r){var i=e.getLine(n);if(this.startRegionRe.test(i))return this.getCommentRegionBlock(e,i,n);var s=i.match(this.foldingStartMarker);if(s){var o=s.index;if(s[1])return this.openingBracketBlock(e,s[1],n,o);var u=e.getCommentFoldRange(n,o+s[0].length,1);return u&&!u.isMultiLine()&&(r?u=this.getSectionRange(e,n):t!="all"&&(u=null)),u}if(t==="markbegin")return;var s=i.match(this.foldingStopMarker);if(s){var o=s.index+s[0].length;return s[1]?this.closingBracketBlock(e,s[1],n,o):e.getCommentFoldRange(n,o,-1)}},this.getSectionRange=function(e,t){var n=e.getLine(t),r=n.search(/\S/),s=t,o=n.length;t+=1;var u=t,a=e.getLength();while(++tf)break;var l=this.getFoldWidgetRange(e,"all",t);if(l){if(l.start.row<=s)break;if(l.isMultiLine())t=l.end.row;else if(r==f)break}u=t}return new i(s,o,u,e.getLine(u).length)},this.getCommentRegionBlock=function(e,t,n){var r=t.search(/\s*$/),s=e.getLength(),o=n,u=/^\s*(?:\/\*|\/\/|--)#?(end)?region\b/,a=1;while(++no)return new i(o,r,l,t.length)}}.call(o.prototype)}),define("ace/mode/objectivec",["require","exports","module","ace/lib/oop","ace/mode/text","ace/mode/objectivec_highlight_rules","ace/mode/folding/cstyle"],function(e,t,n){"use strict";var r=e("../lib/oop"),i=e("./text").Mode,s=e("./objectivec_highlight_rules").ObjectiveCHighlightRules,o=e("./folding/cstyle").FoldMode,u=function(){this.HighlightRules=s,this.foldingRules=new o,this.$behaviour=this.$defaultBehaviour};r.inherits(u,i),function(){this.lineCommentStart="//",this.blockComment={start:"/*",end:"*/"},this.$id="ace/mode/objectivec"}.call(u.prototype),t.Mode=u}) \ No newline at end of file +define("ace/mode/doc_comment_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/text_highlight_rules"],function(e,t,n){"use strict";var r=e("../lib/oop"),i=e("./text_highlight_rules").TextHighlightRules,s=function(){this.$rules={start:[{token:"comment.doc.tag",regex:"@[\\w\\d_]+"},s.getTagRule(),{defaultToken:"comment.doc",caseInsensitive:!0}]}};r.inherits(s,i),s.getTagRule=function(e){return{token:"comment.doc.tag.storage.type",regex:"\\b(?:TODO|FIXME|XXX|HACK)\\b"}},s.getStartRule=function(e){return{token:"comment.doc",regex:"\\/\\*(?=\\*)",next:e}},s.getEndRule=function(e){return{token:"comment.doc",regex:"\\*\\/",next:e}},t.DocCommentHighlightRules=s}),define("ace/mode/c_cpp_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/doc_comment_highlight_rules","ace/mode/text_highlight_rules"],function(e,t,n){"use strict";var r=e("../lib/oop"),i=e("./doc_comment_highlight_rules").DocCommentHighlightRules,s=e("./text_highlight_rules").TextHighlightRules,o=t.cFunctions="\\b(?:hypot(?:f|l)?|s(?:scanf|ystem|nprintf|ca(?:nf|lb(?:n(?:f|l)?|ln(?:f|l)?))|i(?:n(?:h(?:f|l)?|f|l)?|gn(?:al|bit))|tr(?:s(?:tr|pn)|nc(?:py|at|mp)|c(?:spn|hr|oll|py|at|mp)|to(?:imax|d|u(?:l(?:l)?|max)|k|f|l(?:d|l)?)|error|pbrk|ftime|len|rchr|xfrm)|printf|et(?:jmp|vbuf|locale|buf)|qrt(?:f|l)?|w(?:scanf|printf)|rand)|n(?:e(?:arbyint(?:f|l)?|xt(?:toward(?:f|l)?|after(?:f|l)?))|an(?:f|l)?)|c(?:s(?:in(?:h(?:f|l)?|f|l)?|qrt(?:f|l)?)|cos(?:h(?:f)?|f|l)?|imag(?:f|l)?|t(?:ime|an(?:h(?:f|l)?|f|l)?)|o(?:s(?:h(?:f|l)?|f|l)?|nj(?:f|l)?|pysign(?:f|l)?)|p(?:ow(?:f|l)?|roj(?:f|l)?)|e(?:il(?:f|l)?|xp(?:f|l)?)|l(?:o(?:ck|g(?:f|l)?)|earerr)|a(?:sin(?:h(?:f|l)?|f|l)?|cos(?:h(?:f|l)?|f|l)?|tan(?:h(?:f|l)?|f|l)?|lloc|rg(?:f|l)?|bs(?:f|l)?)|real(?:f|l)?|brt(?:f|l)?)|t(?:ime|o(?:upper|lower)|an(?:h(?:f|l)?|f|l)?|runc(?:f|l)?|gamma(?:f|l)?|mp(?:nam|file))|i(?:s(?:space|n(?:ormal|an)|cntrl|inf|digit|u(?:nordered|pper)|p(?:unct|rint)|finite|w(?:space|c(?:ntrl|type)|digit|upper|p(?:unct|rint)|lower|al(?:num|pha)|graph|xdigit|blank)|l(?:ower|ess(?:equal|greater)?)|al(?:num|pha)|gr(?:eater(?:equal)?|aph)|xdigit|blank)|logb(?:f|l)?|max(?:div|abs))|di(?:v|fftime)|_Exit|unget(?:c|wc)|p(?:ow(?:f|l)?|ut(?:s|c(?:har)?|wc(?:har)?)|error|rintf)|e(?:rf(?:c(?:f|l)?|f|l)?|x(?:it|p(?:2(?:f|l)?|f|l|m1(?:f|l)?)?))|v(?:s(?:scanf|nprintf|canf|printf|w(?:scanf|printf))|printf|f(?:scanf|printf|w(?:scanf|printf))|w(?:scanf|printf)|a_(?:start|copy|end|arg))|qsort|f(?:s(?:canf|e(?:tpos|ek))|close|tell|open|dim(?:f|l)?|p(?:classify|ut(?:s|c|w(?:s|c))|rintf)|e(?:holdexcept|set(?:e(?:nv|xceptflag)|round)|clearexcept|testexcept|of|updateenv|r(?:aiseexcept|ror)|get(?:e(?:nv|xceptflag)|round))|flush|w(?:scanf|ide|printf|rite)|loor(?:f|l)?|abs(?:f|l)?|get(?:s|c|pos|w(?:s|c))|re(?:open|e|ad|xp(?:f|l)?)|m(?:in(?:f|l)?|od(?:f|l)?|a(?:f|l|x(?:f|l)?)?))|l(?:d(?:iv|exp(?:f|l)?)|o(?:ngjmp|cal(?:time|econv)|g(?:1(?:p(?:f|l)?|0(?:f|l)?)|2(?:f|l)?|f|l|b(?:f|l)?)?)|abs|l(?:div|abs|r(?:int(?:f|l)?|ound(?:f|l)?))|r(?:int(?:f|l)?|ound(?:f|l)?)|gamma(?:f|l)?)|w(?:scanf|c(?:s(?:s(?:tr|pn)|nc(?:py|at|mp)|c(?:spn|hr|oll|py|at|mp)|to(?:imax|d|u(?:l(?:l)?|max)|k|f|l(?:d|l)?|mbs)|pbrk|ftime|len|r(?:chr|tombs)|xfrm)|to(?:b|mb)|rtomb)|printf|mem(?:set|c(?:hr|py|mp)|move))|a(?:s(?:sert|ctime|in(?:h(?:f|l)?|f|l)?)|cos(?:h(?:f|l)?|f|l)?|t(?:o(?:i|f|l(?:l)?)|exit|an(?:h(?:f|l)?|2(?:f|l)?|f|l)?)|b(?:s|ort))|g(?:et(?:s|c(?:har)?|env|wc(?:har)?)|mtime)|r(?:int(?:f|l)?|ound(?:f|l)?|e(?:name|alloc|wind|m(?:ove|quo(?:f|l)?|ainder(?:f|l)?))|a(?:nd|ise))|b(?:search|towc)|m(?:odf(?:f|l)?|em(?:set|c(?:hr|py|mp)|move)|ktime|alloc|b(?:s(?:init|towcs|rtowcs)|towc|len|r(?:towc|len))))\\b",u=function(){var e="break|case|continue|default|do|else|for|goto|if|_Pragma|return|switch|while|catch|operator|try|throw|using",t="asm|__asm__|auto|bool|_Bool|char|_Complex|double|enum|float|_Imaginary|int|long|short|signed|struct|typedef|union|unsigned|void|class|wchar_t|template|char16_t|char32_t",n="const|extern|register|restrict|static|volatile|inline|private|protected|public|friend|explicit|virtual|export|mutable|typename|constexpr|new|delete|alignas|alignof|decltype|noexcept|thread_local",r="and|and_eq|bitand|bitor|compl|not|not_eq|or|or_eq|typeid|xor|xor_eqconst_cast|dynamic_cast|reinterpret_cast|static_cast|sizeof|namespace",s="NULL|true|false|TRUE|FALSE|nullptr",u=this.$keywords=this.createKeywordMapper({"keyword.control":e,"storage.type":t,"storage.modifier":n,"keyword.operator":r,"variable.language":"this","constant.language":s},"identifier"),a="[a-zA-Z\\$_\u00a1-\uffff][a-zA-Z\\d\\$_\u00a1-\uffff]*\\b",f=/\\(?:['"?\\abfnrtv]|[0-7]{1,3}|x[a-fA-F\d]{2}|u[a-fA-F\d]{4}U[a-fA-F\d]{8}|.)/.source;this.$rules={start:[{token:"comment",regex:"//$",next:"start"},{token:"comment",regex:"//",next:"singleLineComment"},i.getStartRule("doc-start"),{token:"comment",regex:"\\/\\*",next:"comment"},{token:"string",regex:"'(?:"+f+"|.)?'"},{token:"string.start",regex:'"',stateName:"qqstring",next:[{token:"string",regex:/\\\s*$/,next:"qqstring"},{token:"constant.language.escape",regex:f},{token:"constant.language.escape",regex:/%[^'"\\]/},{token:"string.end",regex:'"|$',next:"start"},{defaultToken:"string"}]},{token:"string.start",regex:'R"\\(',stateName:"rawString",next:[{token:"string.end",regex:'\\)"',next:"start"},{defaultToken:"string"}]},{token:"constant.numeric",regex:"0[xX][0-9a-fA-F]+(L|l|UL|ul|u|U|F|f|ll|LL|ull|ULL)?\\b"},{token:"constant.numeric",regex:"[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?(L|l|UL|ul|u|U|F|f|ll|LL|ull|ULL)?\\b"},{token:"keyword",regex:"#\\s*(?:include|import|pragma|line|define|undef)\\b",next:"directive"},{token:"keyword",regex:"#\\s*(?:endif|if|ifdef|else|elif|ifndef)\\b"},{token:"support.function.C99.c",regex:o},{token:u,regex:"[a-zA-Z_$][a-zA-Z0-9_$]*"},{token:"keyword.operator",regex:/--|\+\+|<<=|>>=|>>>=|<>|&&|\|\||\?:|[*%\/+\-&\^|~!<>=]=?/},{token:"punctuation.operator",regex:"\\?|\\:|\\,|\\;|\\."},{token:"paren.lparen",regex:"[[({]"},{token:"paren.rparen",regex:"[\\])}]"},{token:"text",regex:"\\s+"}],comment:[{token:"comment",regex:".*?\\*\\/",next:"start"},{token:"comment",regex:".+"}],singleLineComment:[{token:"comment",regex:/\\$/,next:"singleLineComment"},{token:"comment",regex:/$/,next:"start"},{defaultToken:"comment"}],directive:[{token:"constant.other.multiline",regex:/\\/},{token:"constant.other.multiline",regex:/.*\\/},{token:"constant.other",regex:"\\s*<.+?>",next:"start"},{token:"constant.other",regex:'\\s*["](?:(?:\\\\.)|(?:[^"\\\\]))*?["]',next:"start"},{token:"constant.other",regex:"\\s*['](?:(?:\\\\.)|(?:[^'\\\\]))*?[']",next:"start"},{token:"constant.other",regex:/[^\\\/]+/,next:"start"}]},this.embedRules(i,"doc-",[i.getEndRule("start")]),this.normalizeRules()};r.inherits(u,s),t.c_cppHighlightRules=u}),define("ace/mode/objectivec_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/doc_comment_highlight_rules","ace/mode/c_cpp_highlight_rules"],function(e,t,n){"use strict";var r=e("../lib/oop"),i=e("./doc_comment_highlight_rules").DocCommentHighlightRules,s=e("./c_cpp_highlight_rules"),o=s.c_cppHighlightRules,u=function(){var e="\\\\(?:[abefnrtv'\"?\\\\]|[0-3]\\d{1,2}|[4-7]\\d?|222|x[a-zA-Z0-9]+)",t=[{regex:"\\b_cmd\\b",token:"variable.other.selector.objc"},{regex:"\\b(?:self|super)\\b",token:"variable.language.objc"}],n=new o,r=n.getRules();this.$rules={start:[{token:"comment",regex:"\\/\\/.*$"},i.getStartRule("doc-start"),{token:"comment",regex:"\\/\\*",next:"comment"},{token:["storage.type.objc","punctuation.definition.storage.type.objc","entity.name.type.objc","text","entity.other.inherited-class.objc"],regex:"(@)(interface|protocol)(?!.+;)(\\s+[A-Za-z_][A-Za-z0-9_]*)(\\s*:\\s*)([A-Za-z]+)"},{token:["storage.type.objc"],regex:"(@end)"},{token:["storage.type.objc","entity.name.type.objc","entity.other.inherited-class.objc"],regex:"(@implementation)(\\s+[A-Za-z_][A-Za-z0-9_]*)(\\s*?::\\s*(?:[A-Za-z][A-Za-z0-9]*))?"},{token:"string.begin.objc",regex:'@"',next:"constant_NSString"},{token:"storage.type.objc",regex:"\\bid\\s*<",next:"protocol_list"},{token:"keyword.control.macro.objc",regex:"\\bNS_DURING|NS_HANDLER|NS_ENDHANDLER\\b"},{token:["punctuation.definition.keyword.objc","keyword.control.exception.objc"],regex:"(@)(try|catch|finally|throw)\\b"},{token:["punctuation.definition.keyword.objc","keyword.other.objc"],regex:"(@)(defs|encode)\\b"},{token:["storage.type.id.objc","text"],regex:"(\\bid\\b)(\\s|\\n)?"},{token:"storage.type.objc",regex:"\\bIBOutlet|IBAction|BOOL|SEL|id|unichar|IMP|Class\\b"},{token:["punctuation.definition.storage.type.objc","storage.type.objc"],regex:"(@)(class|protocol)\\b"},{token:["punctuation.definition.storage.type.objc","punctuation"],regex:"(@selector)(\\s*\\()",next:"selectors"},{token:["punctuation.definition.storage.modifier.objc","storage.modifier.objc"],regex:"(@)(synchronized|public|private|protected|package)\\b"},{token:"constant.language.objc",regex:"\\bYES|NO|Nil|nil\\b"},{token:"support.variable.foundation",regex:"\\bNSApp\\b"},{token:["support.function.cocoa.leopard"],regex:"(?:\\b)(NS(?:Rect(?:ToCGRect|FromCGRect)|MakeCollectable|S(?:tringFromProtocol|ize(?:ToCGSize|FromCGSize))|Draw(?:NinePartImage|ThreePartImage)|P(?:oint(?:ToCGPoint|FromCGPoint)|rotocolFromString)|EventMaskFromType|Value))(?:\\b)"},{token:["support.function.cocoa"],regex:"(?:\\b)(NS(?:R(?:ound(?:DownToMultipleOfPageSize|UpToMultipleOfPageSize)|un(?:CriticalAlertPanel(?:RelativeToWindow)?|InformationalAlertPanel(?:RelativeToWindow)?|AlertPanel(?:RelativeToWindow)?)|e(?:set(?:MapTable|HashTable)|c(?:ycleZone|t(?:Clip(?:List)?|F(?:ill(?:UsingOperation|List(?:UsingOperation|With(?:Grays|Colors(?:UsingOperation)?))?)?|romString))|ordAllocationEvent)|turnAddress|leaseAlertPanel|a(?:dPixel|l(?:MemoryAvailable|locateCollectable))|gisterServicesProvider)|angeFromString)|Get(?:SizeAndAlignment|CriticalAlertPanel|InformationalAlertPanel|UncaughtExceptionHandler|FileType(?:s)?|WindowServerMemory|AlertPanel)|M(?:i(?:n(?:X|Y)|d(?:X|Y))|ouseInRect|a(?:p(?:Remove|Get|Member|Insert(?:IfAbsent|KnownAbsent)?)|ke(?:R(?:ect|ange)|Size|Point)|x(?:Range|X|Y)))|B(?:itsPer(?:SampleFromDepth|PixelFromDepth)|e(?:stDepth|ep|gin(?:CriticalAlertSheet|InformationalAlertSheet|AlertSheet)))|S(?:ho(?:uldRetainWithZone|w(?:sServicesMenuItem|AnimationEffect))|tringFrom(?:R(?:ect|ange)|MapTable|S(?:ize|elector)|HashTable|Class|Point)|izeFromString|e(?:t(?:ShowsServicesMenuItem|ZoneName|UncaughtExceptionHandler|FocusRingStyle)|lectorFromString|archPathForDirectoriesInDomains)|wap(?:Big(?:ShortToHost|IntToHost|DoubleToHost|FloatToHost|Long(?:ToHost|LongToHost))|Short|Host(?:ShortTo(?:Big|Little)|IntTo(?:Big|Little)|DoubleTo(?:Big|Little)|FloatTo(?:Big|Little)|Long(?:To(?:Big|Little)|LongTo(?:Big|Little)))|Int|Double|Float|L(?:ittle(?:ShortToHost|IntToHost|DoubleToHost|FloatToHost|Long(?:ToHost|LongToHost))|ong(?:Long)?)))|H(?:ighlightRect|o(?:stByteOrder|meDirectory(?:ForUser)?)|eight|ash(?:Remove|Get|Insert(?:IfAbsent|KnownAbsent)?)|FSType(?:CodeFromFileType|OfFile))|N(?:umberOfColorComponents|ext(?:MapEnumeratorPair|HashEnumeratorItem))|C(?:o(?:n(?:tainsRect|vert(?:GlyphsToPackedGlyphs|Swapped(?:DoubleToHost|FloatToHost)|Host(?:DoubleToSwapped|FloatToSwapped)))|unt(?:MapTable|HashTable|Frames|Windows(?:ForContext)?)|py(?:M(?:emoryPages|apTableWithZone)|Bits|HashTableWithZone|Object)|lorSpaceFromDepth|mpare(?:MapTables|HashTables))|lassFromString|reate(?:MapTable(?:WithZone)?|HashTable(?:WithZone)?|Zone|File(?:namePboardType|ContentsPboardType)))|TemporaryDirectory|I(?:s(?:ControllerMarker|EmptyRect|FreedObject)|n(?:setRect|crementExtraRefCount|te(?:r(?:sect(?:sRect|ionR(?:ect|ange))|faceStyleForKey)|gralRect)))|Zone(?:Realloc|Malloc|Name|Calloc|Fr(?:omPointer|ee))|O(?:penStepRootDirectory|ffsetRect)|D(?:i(?:sableScreenUpdates|videRect)|ottedFrameRect|e(?:c(?:imal(?:Round|Multiply|S(?:tring|ubtract)|Normalize|Co(?:py|mpa(?:ct|re))|IsNotANumber|Divide|Power|Add)|rementExtraRefCountWasZero)|faultMallocZone|allocate(?:MemoryPages|Object))|raw(?:Gr(?:oove|ayBezel)|B(?:itmap|utton)|ColorTiledRects|TiledRects|DarkBezel|W(?:hiteBezel|indowBackground)|LightBezel))|U(?:serName|n(?:ionR(?:ect|ange)|registerServicesProvider)|pdateDynamicServices)|Java(?:Bundle(?:Setup|Cleanup)|Setup(?:VirtualMachine)?|Needs(?:ToLoadClasses|VirtualMachine)|ClassesF(?:orBundle|romPath)|ObjectNamedInPath|ProvidesClasses)|P(?:oint(?:InRect|FromString)|erformService|lanarFromDepth|ageSize)|E(?:n(?:d(?:MapTableEnumeration|HashTableEnumeration)|umerate(?:MapTable|HashTable)|ableScreenUpdates)|qual(?:R(?:ects|anges)|Sizes|Points)|raseRect|xtraRefCount)|F(?:ileTypeForHFSTypeCode|ullUserName|r(?:ee(?:MapTable|HashTable)|ame(?:Rect(?:WithWidth(?:UsingOperation)?)?|Address)))|Wi(?:ndowList(?:ForContext)?|dth)|Lo(?:cationInRange|g(?:v|PageSize)?)|A(?:ccessibility(?:R(?:oleDescription(?:ForUIElement)?|aiseBadArgumentException)|Unignored(?:Children(?:ForOnlyChild)?|Descendant|Ancestor)|PostNotification|ActionDescription)|pplication(?:Main|Load)|vailableWindowDepths|ll(?:MapTable(?:Values|Keys)|HashTableObjects|ocate(?:MemoryPages|Collectable|Object)))))(?:\\b)"},{token:["support.class.cocoa.leopard"],regex:"(?:\\b)(NS(?:RuleEditor|G(?:arbageCollector|radient)|MapTable|HashTable|Co(?:ndition|llectionView(?:Item)?)|T(?:oolbarItemGroup|extInputClient|r(?:eeNode|ackingArea))|InvocationOperation|Operation(?:Queue)?|D(?:ictionaryController|ockTile)|P(?:ointer(?:Functions|Array)|athC(?:o(?:ntrol(?:Delegate)?|mponentCell)|ell(?:Delegate)?)|r(?:intPanelAccessorizing|edicateEditor(?:RowTemplate)?))|ViewController|FastEnumeration|Animat(?:ionContext|ablePropertyContainer)))(?:\\b)"},{token:["support.class.cocoa"],regex:"(?:\\b)(NS(?:R(?:u(?:nLoop|ler(?:Marker|View))|e(?:sponder|cursiveLock|lativeSpecifier)|an(?:domSpecifier|geSpecifier))|G(?:etCommand|lyph(?:Generator|Storage|Info)|raphicsContext)|XML(?:Node|D(?:ocument|TD(?:Node)?)|Parser|Element)|M(?:iddleSpecifier|ov(?:ie(?:View)?|eCommand)|utable(?:S(?:tring|et)|C(?:haracterSet|opying)|IndexSet|D(?:ictionary|ata)|URLRequest|ParagraphStyle|A(?:ttributedString|rray))|e(?:ssagePort(?:NameServer)?|nu(?:Item(?:Cell)?|View)?|t(?:hodSignature|adata(?:Item|Query(?:ResultGroup|AttributeValueTuple)?)))|a(?:ch(?:BootstrapServer|Port)|trix))|B(?:itmapImageRep|ox|u(?:ndle|tton(?:Cell)?)|ezierPath|rowser(?:Cell)?)|S(?:hadow|c(?:anner|r(?:ipt(?:SuiteRegistry|C(?:o(?:ercionHandler|mmand(?:Description)?)|lassDescription)|ObjectSpecifier|ExecutionContext|WhoseTest)|oll(?:er|View)|een))|t(?:epper(?:Cell)?|atus(?:Bar|Item)|r(?:ing|eam))|imple(?:HorizontalTypesetter|CString)|o(?:cketPort(?:NameServer)?|und|rtDescriptor)|p(?:e(?:cifierTest|ech(?:Recognizer|Synthesizer)|ll(?:Server|Checker))|litView)|e(?:cureTextField(?:Cell)?|t(?:Command)?|archField(?:Cell)?|rializer|gmentedC(?:ontrol|ell))|lider(?:Cell)?|avePanel)|H(?:ost|TTP(?:Cookie(?:Storage)?|URLResponse)|elpManager)|N(?:ib(?:Con(?:nector|trolConnector)|OutletConnector)?|otification(?:Center|Queue)?|u(?:ll|mber(?:Formatter)?)|etService(?:Browser)?|ameSpecifier)|C(?:ha(?:ngeSpelling|racterSet)|o(?:n(?:stantString|nection|trol(?:ler)?|ditionLock)|d(?:ing|er)|unt(?:Command|edSet)|pying|lor(?:Space|P(?:ick(?:ing(?:Custom|Default)|er)|anel)|Well|List)?|m(?:p(?:oundPredicate|arisonPredicate)|boBox(?:Cell)?))|u(?:stomImageRep|rsor)|IImageRep|ell|l(?:ipView|o(?:seCommand|neCommand)|assDescription)|a(?:ched(?:ImageRep|URLResponse)|lendar(?:Date)?)|reateCommand)|T(?:hread|ypesetter|ime(?:Zone|r)|o(?:olbar(?:Item(?:Validations)?)?|kenField(?:Cell)?)|ext(?:Block|Storage|Container|Tab(?:le(?:Block)?)?|Input|View|Field(?:Cell)?|List|Attachment(?:Cell)?)?|a(?:sk|b(?:le(?:Header(?:Cell|View)|Column|View)|View(?:Item)?))|reeController)|I(?:n(?:dex(?:S(?:pecifier|et)|Path)|put(?:Manager|S(?:tream|erv(?:iceProvider|er(?:MouseTracker)?)))|vocation)|gnoreMisspelledWords|mage(?:Rep|Cell|View)?)|O(?:ut(?:putStream|lineView)|pen(?:GL(?:Context|Pixel(?:Buffer|Format)|View)|Panel)|bj(?:CTypeSerializationCallBack|ect(?:Controller)?))|D(?:i(?:st(?:antObject(?:Request)?|ributed(?:NotificationCenter|Lock))|ctionary|rectoryEnumerator)|ocument(?:Controller)?|e(?:serializer|cimalNumber(?:Behaviors|Handler)?|leteCommand)|at(?:e(?:Components|Picker(?:Cell)?|Formatter)?|a)|ra(?:wer|ggingInfo))|U(?:ser(?:InterfaceValidations|Defaults(?:Controller)?)|RL(?:Re(?:sponse|quest)|Handle(?:Client)?|C(?:onnection|ache|redential(?:Storage)?)|Download(?:Delegate)?|Prot(?:ocol(?:Client)?|ectionSpace)|AuthenticationChallenge(?:Sender)?)?|n(?:iqueIDSpecifier|doManager|archiver))|P(?:ipe|o(?:sitionalSpecifier|pUpButton(?:Cell)?|rt(?:Message|NameServer|Coder)?)|ICTImageRep|ersistentDocument|DFImageRep|a(?:steboard|nel|ragraphStyle|geLayout)|r(?:int(?:Info|er|Operation|Panel)|o(?:cessInfo|tocolChecker|perty(?:Specifier|ListSerialization)|gressIndicator|xy)|edicate))|E(?:numerator|vent|PSImageRep|rror|x(?:ception|istsCommand|pression))|V(?:iew(?:Animation)?|al(?:idated(?:ToobarItem|UserInterfaceItem)|ue(?:Transformer)?))|Keyed(?:Unarchiver|Archiver)|Qui(?:ckDrawView|tCommand)|F(?:ile(?:Manager|Handle|Wrapper)|o(?:nt(?:Manager|Descriptor|Panel)?|rm(?:Cell|atter)))|W(?:hoseSpecifier|indow(?:Controller)?|orkspace)|L(?:o(?:c(?:k(?:ing)?|ale)|gicalTest)|evelIndicator(?:Cell)?|ayoutManager)|A(?:ssertionHandler|nimation|ctionCell|ttributedString|utoreleasePool|TSTypesetter|ppl(?:ication|e(?:Script|Event(?:Manager|Descriptor)))|ffineTransform|lert|r(?:chiver|ray(?:Controller)?))))(?:\\b)"},{token:["support.type.cocoa.leopard"],regex:"(?:\\b)(NS(?:R(?:u(?:nLoop|ler(?:Marker|View))|e(?:sponder|cursiveLock|lativeSpecifier)|an(?:domSpecifier|geSpecifier))|G(?:etCommand|lyph(?:Generator|Storage|Info)|raphicsContext)|XML(?:Node|D(?:ocument|TD(?:Node)?)|Parser|Element)|M(?:iddleSpecifier|ov(?:ie(?:View)?|eCommand)|utable(?:S(?:tring|et)|C(?:haracterSet|opying)|IndexSet|D(?:ictionary|ata)|URLRequest|ParagraphStyle|A(?:ttributedString|rray))|e(?:ssagePort(?:NameServer)?|nu(?:Item(?:Cell)?|View)?|t(?:hodSignature|adata(?:Item|Query(?:ResultGroup|AttributeValueTuple)?)))|a(?:ch(?:BootstrapServer|Port)|trix))|B(?:itmapImageRep|ox|u(?:ndle|tton(?:Cell)?)|ezierPath|rowser(?:Cell)?)|S(?:hadow|c(?:anner|r(?:ipt(?:SuiteRegistry|C(?:o(?:ercionHandler|mmand(?:Description)?)|lassDescription)|ObjectSpecifier|ExecutionContext|WhoseTest)|oll(?:er|View)|een))|t(?:epper(?:Cell)?|atus(?:Bar|Item)|r(?:ing|eam))|imple(?:HorizontalTypesetter|CString)|o(?:cketPort(?:NameServer)?|und|rtDescriptor)|p(?:e(?:cifierTest|ech(?:Recognizer|Synthesizer)|ll(?:Server|Checker))|litView)|e(?:cureTextField(?:Cell)?|t(?:Command)?|archField(?:Cell)?|rializer|gmentedC(?:ontrol|ell))|lider(?:Cell)?|avePanel)|H(?:ost|TTP(?:Cookie(?:Storage)?|URLResponse)|elpManager)|N(?:ib(?:Con(?:nector|trolConnector)|OutletConnector)?|otification(?:Center|Queue)?|u(?:ll|mber(?:Formatter)?)|etService(?:Browser)?|ameSpecifier)|C(?:ha(?:ngeSpelling|racterSet)|o(?:n(?:stantString|nection|trol(?:ler)?|ditionLock)|d(?:ing|er)|unt(?:Command|edSet)|pying|lor(?:Space|P(?:ick(?:ing(?:Custom|Default)|er)|anel)|Well|List)?|m(?:p(?:oundPredicate|arisonPredicate)|boBox(?:Cell)?))|u(?:stomImageRep|rsor)|IImageRep|ell|l(?:ipView|o(?:seCommand|neCommand)|assDescription)|a(?:ched(?:ImageRep|URLResponse)|lendar(?:Date)?)|reateCommand)|T(?:hread|ypesetter|ime(?:Zone|r)|o(?:olbar(?:Item(?:Validations)?)?|kenField(?:Cell)?)|ext(?:Block|Storage|Container|Tab(?:le(?:Block)?)?|Input|View|Field(?:Cell)?|List|Attachment(?:Cell)?)?|a(?:sk|b(?:le(?:Header(?:Cell|View)|Column|View)|View(?:Item)?))|reeController)|I(?:n(?:dex(?:S(?:pecifier|et)|Path)|put(?:Manager|S(?:tream|erv(?:iceProvider|er(?:MouseTracker)?)))|vocation)|gnoreMisspelledWords|mage(?:Rep|Cell|View)?)|O(?:ut(?:putStream|lineView)|pen(?:GL(?:Context|Pixel(?:Buffer|Format)|View)|Panel)|bj(?:CTypeSerializationCallBack|ect(?:Controller)?))|D(?:i(?:st(?:antObject(?:Request)?|ributed(?:NotificationCenter|Lock))|ctionary|rectoryEnumerator)|ocument(?:Controller)?|e(?:serializer|cimalNumber(?:Behaviors|Handler)?|leteCommand)|at(?:e(?:Components|Picker(?:Cell)?|Formatter)?|a)|ra(?:wer|ggingInfo))|U(?:ser(?:InterfaceValidations|Defaults(?:Controller)?)|RL(?:Re(?:sponse|quest)|Handle(?:Client)?|C(?:onnection|ache|redential(?:Storage)?)|Download(?:Delegate)?|Prot(?:ocol(?:Client)?|ectionSpace)|AuthenticationChallenge(?:Sender)?)?|n(?:iqueIDSpecifier|doManager|archiver))|P(?:ipe|o(?:sitionalSpecifier|pUpButton(?:Cell)?|rt(?:Message|NameServer|Coder)?)|ICTImageRep|ersistentDocument|DFImageRep|a(?:steboard|nel|ragraphStyle|geLayout)|r(?:int(?:Info|er|Operation|Panel)|o(?:cessInfo|tocolChecker|perty(?:Specifier|ListSerialization)|gressIndicator|xy)|edicate))|E(?:numerator|vent|PSImageRep|rror|x(?:ception|istsCommand|pression))|V(?:iew(?:Animation)?|al(?:idated(?:ToobarItem|UserInterfaceItem)|ue(?:Transformer)?))|Keyed(?:Unarchiver|Archiver)|Qui(?:ckDrawView|tCommand)|F(?:ile(?:Manager|Handle|Wrapper)|o(?:nt(?:Manager|Descriptor|Panel)?|rm(?:Cell|atter)))|W(?:hoseSpecifier|indow(?:Controller)?|orkspace)|L(?:o(?:c(?:k(?:ing)?|ale)|gicalTest)|evelIndicator(?:Cell)?|ayoutManager)|A(?:ssertionHandler|nimation|ctionCell|ttributedString|utoreleasePool|TSTypesetter|ppl(?:ication|e(?:Script|Event(?:Manager|Descriptor)))|ffineTransform|lert|r(?:chiver|ray(?:Controller)?))))(?:\\b)"},{token:["support.class.quartz"],regex:"(?:\\b)(C(?:I(?:Sampler|Co(?:ntext|lor)|Image(?:Accumulator)?|PlugIn(?:Registration)?|Vector|Kernel|Filter(?:Generator|Shape)?)|A(?:Renderer|MediaTiming(?:Function)?|BasicAnimation|ScrollLayer|Constraint(?:LayoutManager)?|T(?:iledLayer|extLayer|rans(?:ition|action))|OpenGLLayer|PropertyAnimation|KeyframeAnimation|Layer|A(?:nimation(?:Group)?|ction))))(?:\\b)"},{token:["support.type.quartz"],regex:"(?:\\b)(C(?:G(?:Float|Point|Size|Rect)|IFormat|AConstraintAttribute))(?:\\b)"},{token:["support.type.cocoa"],regex:"(?:\\b)(NS(?:R(?:ect(?:Edge)?|ange)|G(?:lyph(?:Relation|LayoutMode)?|radientType)|M(?:odalSession|a(?:trixMode|p(?:Table|Enumerator)))|B(?:itmapImageFileType|orderType|uttonType|ezelStyle|ackingStoreType|rowserColumnResizingType)|S(?:cr(?:oll(?:er(?:Part|Arrow)|ArrowPosition)|eenAuxiliaryOpaque)|tringEncoding|ize|ocketNativeHandle|election(?:Granularity|Direction|Affinity)|wapped(?:Double|Float)|aveOperationType)|Ha(?:sh(?:Table|Enumerator)|ndler(?:2)?)|C(?:o(?:ntrol(?:Size|Tint)|mp(?:ositingOperation|arisonResult))|ell(?:State|Type|ImagePosition|Attribute))|T(?:hreadPrivate|ypesetterGlyphInfo|i(?:ckMarkPosition|tlePosition|meInterval)|o(?:ol(?:TipTag|bar(?:SizeMode|DisplayMode))|kenStyle)|IFFCompression|ext(?:TabType|Alignment)|ab(?:State|leViewDropOperation|ViewType)|rackingRectTag)|ImageInterpolation|Zone|OpenGL(?:ContextAuxiliary|PixelFormatAuxiliary)|D(?:ocumentChangeType|atePickerElementFlags|ra(?:werState|gOperation))|UsableScrollerParts|P(?:oint|r(?:intingPageOrder|ogressIndicator(?:Style|Th(?:ickness|readInfo))))|EventType|KeyValueObservingOptions|Fo(?:nt(?:SymbolicTraits|TraitMask|Action)|cusRingType)|W(?:indow(?:OrderingMode|Depth)|orkspace(?:IconCreationOptions|LaunchOptions)|ritingDirection)|L(?:ineBreakMode|ayout(?:Status|Direction))|A(?:nimation(?:Progress|Effect)|ppl(?:ication(?:TerminateReply|DelegateReply|PrintReply)|eEventManagerSuspensionID)|ffineTransformStruct|lertStyle)))(?:\\b)"},{token:["support.constant.cocoa"],regex:"(?:\\b)(NS(?:NotFound|Ordered(?:Ascending|Descending|Same)))(?:\\b)"},{token:["support.constant.notification.cocoa.leopard"],regex:"(?:\\b)(NS(?:MenuDidBeginTracking|ViewDidUpdateTrackingAreas)?Notification)(?:\\b)"},{token:["support.constant.notification.cocoa"],regex:"(?:\\b)(NS(?:Menu(?:Did(?:RemoveItem|SendAction|ChangeItem|EndTracking|AddItem)|WillSendAction)|S(?:ystemColorsDidChange|plitView(?:DidResizeSubviews|WillResizeSubviews))|C(?:o(?:nt(?:extHelpModeDid(?:Deactivate|Activate)|rolT(?:intDidChange|extDid(?:BeginEditing|Change|EndEditing)))|lor(?:PanelColorDidChange|ListDidChange)|mboBox(?:Selection(?:IsChanging|DidChange)|Will(?:Dismiss|PopUp)))|lassDescriptionNeededForClass)|T(?:oolbar(?:DidRemoveItem|WillAddItem)|ext(?:Storage(?:DidProcessEditing|WillProcessEditing)|Did(?:BeginEditing|Change|EndEditing)|View(?:DidChange(?:Selection|TypingAttributes)|WillChangeNotifyingTextView))|ableView(?:Selection(?:IsChanging|DidChange)|ColumnDid(?:Resize|Move)))|ImageRepRegistryDidChange|OutlineView(?:Selection(?:IsChanging|DidChange)|ColumnDid(?:Resize|Move)|Item(?:Did(?:Collapse|Expand)|Will(?:Collapse|Expand)))|Drawer(?:Did(?:Close|Open)|Will(?:Close|Open))|PopUpButton(?:CellWillPopUp|WillPopUp)|View(?:GlobalFrameDidChange|BoundsDidChange|F(?:ocusDidChange|rameDidChange))|FontSetChanged|W(?:indow(?:Did(?:Resi(?:ze|gn(?:Main|Key))|M(?:iniaturize|ove)|Become(?:Main|Key)|ChangeScreen(?:|Profile)|Deminiaturize|Update|E(?:ndSheet|xpose))|Will(?:M(?:iniaturize|ove)|BeginSheet|Close))|orkspace(?:SessionDid(?:ResignActive|BecomeActive)|Did(?:Mount|TerminateApplication|Unmount|PerformFileOperation|Wake|LaunchApplication)|Will(?:Sleep|Unmount|PowerOff|LaunchApplication)))|A(?:ntialiasThresholdChanged|ppl(?:ication(?:Did(?:ResignActive|BecomeActive|Hide|ChangeScreenParameters|U(?:nhide|pdate)|FinishLaunching)|Will(?:ResignActive|BecomeActive|Hide|Terminate|U(?:nhide|pdate)|FinishLaunching))|eEventManagerWillProcessFirstEvent)))Notification)(?:\\b)"},{token:["support.constant.cocoa.leopard"],regex:"(?:\\b)(NS(?:RuleEditor(?:RowType(?:Simple|Compound)|NestingMode(?:Si(?:ngle|mple)|Compound|List))|GradientDraws(?:BeforeStartingLocation|AfterEndingLocation)|M(?:inusSetExpressionType|a(?:chPortDeallocate(?:ReceiveRight|SendRight|None)|pTable(?:StrongMemory|CopyIn|ZeroingWeakMemory|ObjectPointerPersonality)))|B(?:oxCustom|undleExecutableArchitecture(?:X86|I386|PPC(?:64)?)|etweenPredicateOperatorType|ackgroundStyle(?:Raised|Dark|L(?:ight|owered)))|S(?:tring(?:DrawingTruncatesLastVisibleLine|EncodingConversion(?:ExternalRepresentation|AllowLossy))|ubqueryExpressionType|p(?:e(?:ech(?:SentenceBoundary|ImmediateBoundary|WordBoundary)|llingState(?:GrammarFlag|SpellingFlag))|litViewDividerStyleThi(?:n|ck))|e(?:rvice(?:RequestTimedOutError|M(?:iscellaneousError|alformedServiceDictionaryError)|InvalidPasteboardDataError|ErrorM(?:inimum|aximum)|Application(?:NotFoundError|LaunchFailedError))|gmentStyle(?:Round(?:Rect|ed)|SmallSquare|Capsule|Textured(?:Rounded|Square)|Automatic)))|H(?:UDWindowMask|ashTable(?:StrongMemory|CopyIn|ZeroingWeakMemory|ObjectPointerPersonality))|N(?:oModeColorPanel|etServiceNoAutoRename)|C(?:hangeRedone|o(?:ntainsPredicateOperatorType|l(?:orRenderingIntent(?:RelativeColorimetric|Saturation|Default|Perceptual|AbsoluteColorimetric)|lectorDisabledOption))|ellHit(?:None|ContentArea|TrackableArea|EditableTextArea))|T(?:imeZoneNameStyle(?:S(?:hort(?:Standard|DaylightSaving)|tandard)|DaylightSaving)|extFieldDatePickerStyle|ableViewSelectionHighlightStyle(?:Regular|SourceList)|racking(?:Mouse(?:Moved|EnteredAndExited)|CursorUpdate|InVisibleRect|EnabledDuringMouseDrag|A(?:ssumeInside|ctive(?:In(?:KeyWindow|ActiveApp)|WhenFirstResponder|Always))))|I(?:n(?:tersectSetExpressionType|dexedColorSpaceModel)|mageScale(?:None|Proportionally(?:Down|UpOrDown)|AxesIndependently))|Ope(?:nGLPFAAllowOfflineRenderers|rationQueue(?:DefaultMaxConcurrentOperationCount|Priority(?:High|Normal|Very(?:High|Low)|Low)))|D(?:iacriticInsensitiveSearch|ownloadsDirectory)|U(?:nionSetExpressionType|TF(?:16(?:BigEndianStringEncoding|StringEncoding|LittleEndianStringEncoding)|32(?:BigEndianStringEncoding|StringEncoding|LittleEndianStringEncoding)))|P(?:ointerFunctions(?:Ma(?:chVirtualMemory|llocMemory)|Str(?:ongMemory|uctPersonality)|C(?:StringPersonality|opyIn)|IntegerPersonality|ZeroingWeakMemory|O(?:paque(?:Memory|Personality)|bjectP(?:ointerPersonality|ersonality)))|at(?:hStyle(?:Standard|NavigationBar|PopUp)|ternColorSpaceModel)|rintPanelShows(?:Scaling|Copies|Orientation|P(?:a(?:perSize|ge(?:Range|SetupAccessory))|review)))|Executable(?:RuntimeMismatchError|NotLoadableError|ErrorM(?:inimum|aximum)|L(?:inkError|oadError)|ArchitectureMismatchError)|KeyValueObservingOption(?:Initial|Prior)|F(?:i(?:ndPanelSubstringMatchType(?:StartsWith|Contains|EndsWith|FullWord)|leRead(?:TooLargeError|UnknownStringEncodingError))|orcedOrderingSearch)|Wi(?:ndow(?:BackingLocation(?:MainMemory|Default|VideoMemory)|Sharing(?:Read(?:Only|Write)|None)|CollectionBehavior(?:MoveToActiveSpace|CanJoinAllSpaces|Default))|dthInsensitiveSearch)|AggregateExpressionType))(?:\\b)"},{token:["support.constant.cocoa"],regex:"(?:\\b)(NS(?:R(?:GB(?:ModeColorPanel|ColorSpaceModel)|ight(?:Mouse(?:D(?:own(?:Mask)?|ragged(?:Mask)?)|Up(?:Mask)?)|T(?:ext(?:Movement|Alignment)|ab(?:sBezelBorder|StopType))|ArrowFunctionKey)|ound(?:RectBezelStyle|Bankers|ed(?:BezelStyle|TokenStyle|DisclosureBezelStyle)|Down|Up|Plain|Line(?:CapStyle|JoinStyle))|un(?:StoppedResponse|ContinuesResponse|AbortedResponse)|e(?:s(?:izableWindowMask|et(?:CursorRectsRunLoopOrdering|FunctionKey))|ce(?:ssedBezelStyle|iver(?:sCantHandleCommandScriptError|EvaluationScriptError))|turnTextMovement|doFunctionKey|quiredArgumentsMissingScriptError|l(?:evancyLevelIndicatorStyle|ative(?:Before|After))|gular(?:SquareBezelStyle|ControlSize)|moveTraitFontAction)|a(?:n(?:domSubelement|geDateMode)|tingLevelIndicatorStyle|dio(?:ModeMatrix|Button)))|G(?:IFFileType|lyph(?:Below|Inscribe(?:B(?:elow|ase)|Over(?:strike|Below)|Above)|Layout(?:WithPrevious|A(?:tAPoint|gainstAPoint))|A(?:ttribute(?:BidiLevel|Soft|Inscribe|Elastic)|bove))|r(?:ooveBorder|eaterThan(?:Comparison|OrEqualTo(?:Comparison|PredicateOperatorType)|PredicateOperatorType)|a(?:y(?:ModeColorPanel|ColorSpaceModel)|dient(?:None|Con(?:cave(?:Strong|Weak)|vex(?:Strong|Weak)))|phiteControlTint)))|XML(?:N(?:o(?:tationDeclarationKind|de(?:CompactEmptyElement|IsCDATA|OptionsNone|Use(?:SingleQuotes|DoubleQuotes)|Pre(?:serve(?:NamespaceOrder|C(?:haracterReferences|DATA)|DTD|Prefixes|E(?:ntities|mptyElements)|Quotes|Whitespace|A(?:ttributeOrder|ll))|ttyPrint)|ExpandEmptyElement))|amespaceKind)|CommentKind|TextKind|InvalidKind|D(?:ocument(?:X(?:MLKind|HTMLKind|Include)|HTMLKind|T(?:idy(?:XML|HTML)|extKind)|IncludeContentTypeDeclaration|Validate|Kind)|TDKind)|P(?:arser(?:GTRequiredError|XMLDeclNot(?:StartedError|FinishedError)|Mi(?:splaced(?:XMLDeclarationError|CDATAEndStringError)|xedContentDeclNot(?:StartedError|FinishedError))|S(?:t(?:andaloneValueError|ringNot(?:StartedError|ClosedError))|paceRequiredError|eparatorRequiredError)|N(?:MTOKENRequiredError|o(?:t(?:ationNot(?:StartedError|FinishedError)|WellBalancedError)|DTDError)|amespaceDeclarationError|AMERequiredError)|C(?:haracterRef(?:In(?:DTDError|PrologError|EpilogError)|AtEOFError)|o(?:nditionalSectionNot(?:StartedError|FinishedError)|mment(?:NotFinishedError|ContainsDoubleHyphenError))|DATANotFinishedError)|TagNameMismatchError|In(?:ternalError|valid(?:HexCharacterRefError|C(?:haracter(?:RefError|InEntityError|Error)|onditionalSectionError)|DecimalCharacterRefError|URIError|Encoding(?:NameError|Error)))|OutOfMemoryError|D(?:ocumentStartError|elegateAbortedParseError|OCTYPEDeclNotFinishedError)|U(?:RI(?:RequiredError|FragmentError)|n(?:declaredEntityError|parsedEntityError|knownEncodingError|finishedTagError))|P(?:CDATARequiredError|ublicIdentifierRequiredError|arsedEntityRef(?:MissingSemiError|NoNameError|In(?:Internal(?:SubsetError|Error)|PrologError|EpilogError)|AtEOFError)|r(?:ocessingInstructionNot(?:StartedError|FinishedError)|ematureDocumentEndError))|E(?:n(?:codingNotSupportedError|tity(?:Ref(?:In(?:DTDError|PrologError|EpilogError)|erence(?:MissingSemiError|WithoutNameError)|LoopError|AtEOFError)|BoundaryError|Not(?:StartedError|FinishedError)|Is(?:ParameterError|ExternalError)|ValueRequiredError))|qualExpectedError|lementContentDeclNot(?:StartedError|FinishedError)|xt(?:ernalS(?:tandaloneEntityError|ubsetNotFinishedError)|raContentError)|mptyDocumentError)|L(?:iteralNot(?:StartedError|FinishedError)|T(?:RequiredError|SlashRequiredError)|essThanSymbolInAttributeError)|Attribute(?:RedefinedError|HasNoValueError|Not(?:StartedError|FinishedError)|ListNot(?:StartedError|FinishedError)))|rocessingInstructionKind)|E(?:ntity(?:GeneralKind|DeclarationKind|UnparsedKind|P(?:ar(?:sedKind|ameterKind)|redefined))|lement(?:Declaration(?:MixedKind|UndefinedKind|E(?:lementKind|mptyKind)|Kind|AnyKind)|Kind))|Attribute(?:N(?:MToken(?:sKind|Kind)|otationKind)|CDATAKind|ID(?:Ref(?:sKind|Kind)|Kind)|DeclarationKind|En(?:tit(?:yKind|iesKind)|umerationKind)|Kind))|M(?:i(?:n(?:XEdge|iaturizableWindowMask|YEdge|uteCalendarUnit)|terLineJoinStyle|ddleSubelement|xedState)|o(?:nthCalendarUnit|deSwitchFunctionKey|use(?:Moved(?:Mask)?|E(?:ntered(?:Mask)?|ventSubtype|xited(?:Mask)?))|veToBezierPathElement|mentary(?:ChangeButton|Push(?:Button|InButton)|Light(?:Button)?))|enuFunctionKey|a(?:c(?:intoshInterfaceStyle|OSRomanStringEncoding)|tchesPredicateOperatorType|ppedRead|x(?:XEdge|YEdge))|ACHOperatingSystem)|B(?:MPFileType|o(?:ttomTabsBezelBorder|ldFontMask|rderlessWindowMask|x(?:Se(?:condary|parator)|OldStyle|Primary))|uttLineCapStyle|e(?:zelBorder|velLineJoinStyle|low(?:Bottom|Top)|gin(?:sWith(?:Comparison|PredicateOperatorType)|FunctionKey))|lueControlTint|ack(?:spaceCharacter|tabTextMovement|ingStore(?:Retained|Buffered|Nonretained)|TabCharacter|wardsSearch|groundTab)|r(?:owser(?:NoColumnResizing|UserColumnResizing|AutoColumnResizing)|eakFunctionKey))|S(?:h(?:ift(?:JISStringEncoding|KeyMask)|ow(?:ControlGlyphs|InvisibleGlyphs)|adowlessSquareBezelStyle)|y(?:s(?:ReqFunctionKey|tem(?:D(?:omainMask|efined(?:Mask)?)|FunctionKey))|mbolStringEncoding)|c(?:a(?:nnedOption|le(?:None|ToFit|Proportionally))|r(?:oll(?:er(?:NoPart|Increment(?:Page|Line|Arrow)|Decrement(?:Page|Line|Arrow)|Knob(?:Slot)?|Arrows(?:M(?:inEnd|axEnd)|None|DefaultSetting))|Wheel(?:Mask)?|LockFunctionKey)|eenChangedEventType))|t(?:opFunctionKey|r(?:ingDrawing(?:OneShot|DisableScreenFontSubstitution|Uses(?:DeviceMetrics|FontLeading|LineFragmentOrigin))|eam(?:Status(?:Reading|NotOpen|Closed|Open(?:ing)?|Error|Writing|AtEnd)|Event(?:Has(?:BytesAvailable|SpaceAvailable)|None|OpenCompleted|E(?:ndEncountered|rrorOccurred)))))|i(?:ngle(?:DateMode|UnderlineStyle)|ze(?:DownFontAction|UpFontAction))|olarisOperatingSystem|unOSOperatingSystem|pecialPageOrder|e(?:condCalendarUnit|lect(?:By(?:Character|Paragraph|Word)|i(?:ng(?:Next|Previous)|onAffinity(?:Downstream|Upstream))|edTab|FunctionKey)|gmentSwitchTracking(?:Momentary|Select(?:One|Any)))|quareLineCapStyle|witchButton|ave(?:ToOperation|Op(?:tions(?:Yes|No|Ask)|eration)|AsOperation)|mall(?:SquareBezelStyle|C(?:ontrolSize|apsFontMask)|IconButtonBezelStyle))|H(?:ighlightModeMatrix|SBModeColorPanel|o(?:ur(?:Minute(?:SecondDatePickerElementFlag|DatePickerElementFlag)|CalendarUnit)|rizontalRuler|meFunctionKey)|TTPCookieAcceptPolicy(?:Never|OnlyFromMainDocumentDomain|Always)|e(?:lp(?:ButtonBezelStyle|KeyMask|FunctionKey)|avierFontAction)|PUXOperatingSystem)|Year(?:MonthDa(?:yDatePickerElementFlag|tePickerElementFlag)|CalendarUnit)|N(?:o(?:n(?:StandardCharacterSetFontMask|ZeroWindingRule|activatingPanelMask|LossyASCIIStringEncoding)|Border|t(?:ification(?:SuspensionBehavior(?:Hold|Coalesce|D(?:eliverImmediately|rop))|NoCoalescing|CoalescingOn(?:Sender|Name)|DeliverImmediately|PostToAllSessions)|PredicateType|EqualToPredicateOperatorType)|S(?:cr(?:iptError|ollerParts)|ubelement|pecifierError)|CellMask|T(?:itle|opLevelContainersSpecifierError|abs(?:BezelBorder|NoBorder|LineBorder))|I(?:nterfaceStyle|mage)|UnderlineStyle|FontChangeAction)|u(?:ll(?:Glyph|CellType)|m(?:eric(?:Search|PadKeyMask)|berFormatter(?:Round(?:Half(?:Down|Up|Even)|Ceiling|Down|Up|Floor)|Behavior(?:10|Default)|S(?:cientificStyle|pellOutStyle)|NoStyle|CurrencyStyle|DecimalStyle|P(?:ercentStyle|ad(?:Before(?:Suffix|Prefix)|After(?:Suffix|Prefix))))))|e(?:t(?:Service(?:BadArgumentError|NotFoundError|C(?:ollisionError|ancelledError)|TimeoutError|InvalidError|UnknownError|ActivityInProgress)|workDomainMask)|wlineCharacter|xt(?:StepInterfaceStyle|FunctionKey))|EXTSTEPStringEncoding|a(?:t(?:iveShortGlyphPacking|uralTextAlignment)|rrowFontMask))|C(?:hange(?:ReadOtherContents|GrayCell(?:Mask)?|BackgroundCell(?:Mask)?|Cleared|Done|Undone|Autosaved)|MYK(?:ModeColorPanel|ColorSpaceModel)|ircular(?:BezelStyle|Slider)|o(?:n(?:stantValueExpressionType|t(?:inuousCapacityLevelIndicatorStyle|entsCellMask|ain(?:sComparison|erSpecifierError)|rol(?:Glyph|KeyMask))|densedFontMask)|lor(?:Panel(?:RGBModeMask|GrayModeMask|HSBModeMask|C(?:MYKModeMask|olorListModeMask|ustomPaletteModeMask|rayonModeMask)|WheelModeMask|AllModesMask)|ListModeColorPanel)|reServiceDirectory|m(?:p(?:osite(?:XOR|Source(?:In|O(?:ut|ver)|Atop)|Highlight|C(?:opy|lear)|Destination(?:In|O(?:ut|ver)|Atop)|Plus(?:Darker|Lighter))|ressedFontMask)|mandKeyMask))|u(?:stom(?:SelectorPredicateOperatorType|PaletteModeColorPanel)|r(?:sor(?:Update(?:Mask)?|PointingDevice)|veToBezierPathElement))|e(?:nterT(?:extAlignment|abStopType)|ll(?:State|H(?:ighlighted|as(?:Image(?:Horizontal|OnLeftOrBottom)|OverlappingImage))|ChangesContents|Is(?:Bordered|InsetButton)|Disabled|Editable|LightsBy(?:Gray|Background|Contents)|AllowsMixedState))|l(?:ipPagination|o(?:s(?:ePathBezierPathElement|ableWindowMask)|ckAndCalendarDatePickerStyle)|ear(?:ControlTint|DisplayFunctionKey|LineFunctionKey))|a(?:seInsensitive(?:Search|PredicateOption)|n(?:notCreateScriptCommandError|cel(?:Button|TextMovement))|chesDirectory|lculation(?:NoError|Overflow|DivideByZero|Underflow|LossOfPrecision)|rriageReturnCharacter)|r(?:itical(?:Request|AlertStyle)|ayonModeColorPanel))|T(?:hick(?:SquareBezelStyle|erSquareBezelStyle)|ypesetter(?:Behavior|HorizontalTabAction|ContainerBreakAction|ZeroAdvancementAction|OriginalBehavior|ParagraphBreakAction|WhitespaceAction|L(?:ineBreakAction|atestBehavior))|i(?:ckMark(?:Right|Below|Left|Above)|tledWindowMask|meZoneDatePickerElementFlag)|o(?:olbarItemVisibilityPriority(?:Standard|High|User|Low)|pTabsBezelBorder|ggleButton)|IFF(?:Compression(?:N(?:one|EXT)|CCITTFAX(?:3|4)|OldJPEG|JPEG|PackBits|LZW)|FileType)|e(?:rminate(?:Now|Cancel|Later)|xt(?:Read(?:InapplicableDocumentTypeError|WriteErrorM(?:inimum|aximum))|Block(?:M(?:i(?:nimum(?:Height|Width)|ddleAlignment)|a(?:rgin|ximum(?:Height|Width)))|B(?:o(?:ttomAlignment|rder)|aselineAlignment)|Height|TopAlignment|P(?:ercentageValueType|adding)|Width|AbsoluteValueType)|StorageEdited(?:Characters|Attributes)|CellType|ured(?:RoundedBezelStyle|BackgroundWindowMask|SquareBezelStyle)|Table(?:FixedLayoutAlgorithm|AutomaticLayoutAlgorithm)|Field(?:RoundedBezel|SquareBezel|AndStepperDatePickerStyle)|WriteInapplicableDocumentTypeError|ListPrependEnclosingMarker))|woByteGlyphPacking|ab(?:Character|TextMovement|le(?:tP(?:oint(?:Mask|EventSubtype)?|roximity(?:Mask|EventSubtype)?)|Column(?:NoResizing|UserResizingMask|AutoresizingMask)|View(?:ReverseSequentialColumnAutoresizingStyle|GridNone|S(?:olid(?:HorizontalGridLineMask|VerticalGridLineMask)|equentialColumnAutoresizingStyle)|NoColumnAutoresizing|UniformColumnAutoresizingStyle|FirstColumnOnlyAutoresizingStyle|LastColumnOnlyAutoresizingStyle)))|rackModeMatrix)|I(?:n(?:sert(?:CharFunctionKey|FunctionKey|LineFunctionKey)|t(?:Type|ernalS(?:criptError|pecifierError))|dexSubelement|validIndexSpecifierError|formational(?:Request|AlertStyle)|PredicateOperatorType)|talicFontMask|SO(?:2022JPStringEncoding|Latin(?:1StringEncoding|2StringEncoding))|dentityMappingCharacterCollection|llegalTextMovement|mage(?:R(?:ight|ep(?:MatchesDevice|LoadStatus(?:ReadingHeader|Completed|InvalidData|Un(?:expectedEOF|knownType)|WillNeedAllData)))|Below|C(?:ellType|ache(?:BySize|Never|Default|Always))|Interpolation(?:High|None|Default|Low)|O(?:nly|verlaps)|Frame(?:Gr(?:oove|ayBezel)|Button|None|Photo)|L(?:oadStatus(?:ReadError|C(?:ompleted|ancelled)|InvalidData|UnexpectedEOF)|eft)|A(?:lign(?:Right|Bottom(?:Right|Left)?|Center|Top(?:Right|Left)?|Left)|bove)))|O(?:n(?:State|eByteGlyphPacking|OffButton|lyScrollerArrows)|ther(?:Mouse(?:D(?:own(?:Mask)?|ragged(?:Mask)?)|Up(?:Mask)?)|TextMovement)|SF1OperatingSystem|pe(?:n(?:GL(?:GO(?:Re(?:setLibrary|tainRenderers)|ClearFormatCache|FormatCacheSize)|PFA(?:R(?:obust|endererID)|M(?:inimumPolicy|ulti(?:sample|Screen)|PSafe|aximumPolicy)|BackingStore|S(?:creenMask|te(?:ncilSize|reo)|ingleRenderer|upersample|ample(?:s|Buffers|Alpha))|NoRecovery|C(?:o(?:lor(?:Size|Float)|mpliant)|losestPolicy)|OffScreen|D(?:oubleBuffer|epthSize)|PixelBuffer|VirtualScreenCount|FullScreen|Window|A(?:cc(?:umSize|elerated)|ux(?:Buffers|DepthStencil)|l(?:phaSize|lRenderers))))|StepUnicodeReservedBase)|rationNotSupportedForKeyS(?:criptError|pecifierError))|ffState|KButton|rPredicateType|bjC(?:B(?:itfield|oolType)|S(?:hortType|tr(?:ingType|uctType)|electorType)|NoType|CharType|ObjectType|DoubleType|UnionType|PointerType|VoidType|FloatType|Long(?:Type|longType)|ArrayType))|D(?:i(?:s(?:c(?:losureBezelStyle|reteCapacityLevelIndicatorStyle)|playWindowRunLoopOrdering)|acriticInsensitivePredicateOption|rect(?:Selection|PredicateModifier))|o(?:c(?:ModalWindowMask|ument(?:Directory|ationDirectory))|ubleType|wn(?:TextMovement|ArrowFunctionKey))|e(?:s(?:cendingPageOrder|ktopDirectory)|cimalTabStopType|v(?:ice(?:NColorSpaceModel|IndependentModifierFlagsMask)|eloper(?:Directory|ApplicationDirectory))|fault(?:ControlTint|TokenStyle)|lete(?:Char(?:acter|FunctionKey)|FunctionKey|LineFunctionKey)|moApplicationDirectory)|a(?:yCalendarUnit|teFormatter(?:MediumStyle|Behavior(?:10|Default)|ShortStyle|NoStyle|FullStyle|LongStyle))|ra(?:wer(?:Clos(?:ingState|edState)|Open(?:ingState|State))|gOperation(?:Generic|Move|None|Copy|Delete|Private|Every|Link|All)))|U(?:ser(?:CancelledError|D(?:irectory|omainMask)|FunctionKey)|RL(?:Handle(?:NotLoaded|Load(?:Succeeded|InProgress|Failed))|CredentialPersistence(?:None|Permanent|ForSession))|n(?:scaledWindowMask|cachedRead|i(?:codeStringEncoding|talicFontMask|fiedTitleAndToolbarWindowMask)|d(?:o(?:CloseGroupingRunLoopOrdering|FunctionKey)|e(?:finedDateComponent|rline(?:Style(?:Single|None|Thick|Double)|Pattern(?:Solid|D(?:ot|ash(?:Dot(?:Dot)?)?)))))|known(?:ColorSpaceModel|P(?:ointingDevice|ageOrder)|KeyS(?:criptError|pecifierError))|boldFontMask)|tilityWindowMask|TF8StringEncoding|p(?:dateWindowsRunLoopOrdering|TextMovement|ArrowFunctionKey))|J(?:ustifiedTextAlignment|PEG(?:2000FileType|FileType)|apaneseEUC(?:GlyphPacking|StringEncoding))|P(?:o(?:s(?:t(?:Now|erFontMask|WhenIdle|ASAP)|iti(?:on(?:Replace|Be(?:fore|ginning)|End|After)|ve(?:IntType|DoubleType|FloatType)))|pUp(?:NoArrow|ArrowAt(?:Bottom|Center))|werOffEventType|rtraitOrientation)|NGFileType|ush(?:InCell(?:Mask)?|OnPushOffButton)|e(?:n(?:TipMask|UpperSideMask|PointingDevice|LowerSideMask)|riodic(?:Mask)?)|P(?:S(?:caleField|tatus(?:Title|Field)|aveButton)|N(?:ote(?:Title|Field)|ame(?:Title|Field))|CopiesField|TitleField|ImageButton|OptionsButton|P(?:a(?:perFeedButton|ge(?:Range(?:To|From)|ChoiceMatrix))|reviewButton)|LayoutButton)|lainTextTokenStyle|a(?:useFunctionKey|ragraphSeparatorCharacter|ge(?:DownFunctionKey|UpFunctionKey))|r(?:int(?:ing(?:ReplyLater|Success|Cancelled|Failure)|ScreenFunctionKey|erTable(?:NotFound|OK|Error)|FunctionKey)|o(?:p(?:ertyList(?:XMLFormat|MutableContainers(?:AndLeaves)?|BinaryFormat|Immutable|OpenStepFormat)|rietaryStringEncoding)|gressIndicator(?:BarStyle|SpinningStyle|Preferred(?:SmallThickness|Thickness|LargeThickness|AquaThickness)))|e(?:ssedTab|vFunctionKey))|L(?:HeightForm|CancelButton|TitleField|ImageButton|O(?:KButton|rientationMatrix)|UnitsButton|PaperNameButton|WidthForm))|E(?:n(?:terCharacter|d(?:sWith(?:Comparison|PredicateOperatorType)|FunctionKey))|v(?:e(?:nOddWindingRule|rySubelement)|aluatedObjectExpressionType)|qualTo(?:Comparison|PredicateOperatorType)|ra(?:serPointingDevice|CalendarUnit|DatePickerElementFlag)|x(?:clude(?:10|QuickDrawElementsIconCreationOption)|pandedFontMask|ecuteFunctionKey))|V(?:i(?:ew(?:M(?:in(?:XMargin|YMargin)|ax(?:XMargin|YMargin))|HeightSizable|NotSizable|WidthSizable)|aPanelFontAction)|erticalRuler|a(?:lidationErrorM(?:inimum|aximum)|riableExpressionType))|Key(?:SpecifierEvaluationScriptError|Down(?:Mask)?|Up(?:Mask)?|PathExpressionType|Value(?:MinusSetMutation|SetSetMutation|Change(?:Re(?:placement|moval)|Setting|Insertion)|IntersectSetMutation|ObservingOption(?:New|Old)|UnionSetMutation|ValidationError))|QTMovie(?:NormalPlayback|Looping(?:BackAndForthPlayback|Playback))|F(?:1(?:1FunctionKey|7FunctionKey|2FunctionKey|8FunctionKey|3FunctionKey|9FunctionKey|4FunctionKey|5FunctionKey|FunctionKey|0FunctionKey|6FunctionKey)|7FunctionKey|i(?:nd(?:PanelAction(?:Replace(?:A(?:ndFind|ll(?:InSelection)?))?|S(?:howFindPanel|e(?:tFindString|lectAll(?:InSelection)?))|Next|Previous)|FunctionKey)|tPagination|le(?:Read(?:No(?:SuchFileError|PermissionError)|CorruptFileError|In(?:validFileNameError|applicableStringEncodingError)|Un(?:supportedSchemeError|knownError))|HandlingPanel(?:CancelButton|OKButton)|NoSuchFileError|ErrorM(?:inimum|aximum)|Write(?:NoPermissionError|In(?:validFileNameError|applicableStringEncodingError)|OutOfSpaceError|Un(?:supportedSchemeError|knownError))|LockingError)|xedPitchFontMask)|2(?:1FunctionKey|7FunctionKey|2FunctionKey|8FunctionKey|3FunctionKey|9FunctionKey|4FunctionKey|5FunctionKey|FunctionKey|0FunctionKey|6FunctionKey)|o(?:nt(?:Mo(?:noSpaceTrait|dernSerifsClass)|BoldTrait|S(?:ymbolicClass|criptsClass|labSerifsClass|ansSerifClass)|C(?:o(?:ndensedTrait|llectionApplicationOnlyMask)|larendonSerifsClass)|TransitionalSerifsClass|I(?:ntegerAdvancementsRenderingMode|talicTrait)|O(?:ldStyleSerifsClass|rnamentalsClass)|DefaultRenderingMode|U(?:nknownClass|IOptimizedTrait)|Panel(?:S(?:hadowEffectModeMask|t(?:andardModesMask|rikethroughEffectModeMask)|izeModeMask)|CollectionModeMask|TextColorEffectModeMask|DocumentColorEffectModeMask|UnderlineEffectModeMask|FaceModeMask|All(?:ModesMask|EffectsModeMask))|ExpandedTrait|VerticalTrait|F(?:amilyClassMask|reeformSerifsClass)|Antialiased(?:RenderingMode|IntegerAdvancementsRenderingMode))|cusRing(?:Below|Type(?:None|Default|Exterior)|Only|Above)|urByteGlyphPacking|rm(?:attingError(?:M(?:inimum|aximum))?|FeedCharacter))|8FunctionKey|unction(?:ExpressionType|KeyMask)|3(?:1FunctionKey|2FunctionKey|3FunctionKey|4FunctionKey|5FunctionKey|FunctionKey|0FunctionKey)|9FunctionKey|4FunctionKey|P(?:RevertButton|S(?:ize(?:Title|Field)|etButton)|CurrentField|Preview(?:Button|Field))|l(?:oat(?:ingPointSamplesBitmapFormat|Type)|agsChanged(?:Mask)?)|axButton|5FunctionKey|6FunctionKey)|W(?:heelModeColorPanel|indow(?:s(?:NTOperatingSystem|CP125(?:1StringEncoding|2StringEncoding|3StringEncoding|4StringEncoding|0StringEncoding)|95(?:InterfaceStyle|OperatingSystem))|M(?:iniaturizeButton|ovedEventType)|Below|CloseButton|ToolbarButton|ZoomButton|Out|DocumentIconButton|ExposedEventType|Above)|orkspaceLaunch(?:NewInstance|InhibitingBackgroundOnly|Default|PreferringClassic|WithoutA(?:ctivation|ddingToRecents)|A(?:sync|nd(?:Hide(?:Others)?|Print)|llowingClassicStartup))|eek(?:day(?:CalendarUnit|OrdinalCalendarUnit)|CalendarUnit)|a(?:ntsBidiLevels|rningAlertStyle)|r(?:itingDirection(?:RightToLeft|Natural|LeftToRight)|apCalendarComponents))|L(?:i(?:stModeMatrix|ne(?:Moves(?:Right|Down|Up|Left)|B(?:order|reakBy(?:C(?:harWrapping|lipping)|Truncating(?:Middle|Head|Tail)|WordWrapping))|S(?:eparatorCharacter|weep(?:Right|Down|Up|Left))|ToBezierPathElement|DoesntMove|arSlider)|teralSearch|kePredicateOperatorType|ghterFontAction|braryDirectory)|ocalDomainMask|e(?:ssThan(?:Comparison|OrEqualTo(?:Comparison|PredicateOperatorType)|PredicateOperatorType)|ft(?:Mouse(?:D(?:own(?:Mask)?|ragged(?:Mask)?)|Up(?:Mask)?)|T(?:ext(?:Movement|Alignment)|ab(?:sBezelBorder|StopType))|ArrowFunctionKey))|a(?:yout(?:RightToLeft|NotDone|CantFit|OutOfGlyphs|Done|LeftToRight)|ndscapeOrientation)|ABColorSpaceModel)|A(?:sc(?:iiWithDoubleByteEUCGlyphPacking|endingPageOrder)|n(?:y(?:Type|PredicateModifier|EventMask)|choredSearch|imation(?:Blocking|Nonblocking(?:Threaded)?|E(?:ffect(?:DisappearingItemDefault|Poof)|ase(?:In(?:Out)?|Out))|Linear)|dPredicateType)|t(?:Bottom|tachmentCharacter|omicWrite|Top)|SCIIStringEncoding|d(?:obe(?:GB1CharacterCollection|CNS1CharacterCollection|Japan(?:1CharacterCollection|2CharacterCollection)|Korea1CharacterCollection)|dTraitFontAction|minApplicationDirectory)|uto(?:saveOperation|Pagination)|pp(?:lication(?:SupportDirectory|D(?:irectory|e(?:fined(?:Mask)?|legateReply(?:Success|Cancel|Failure)|activatedEventType))|ActivatedEventType)|KitDefined(?:Mask)?)|l(?:ternateKeyMask|pha(?:ShiftKeyMask|NonpremultipliedBitmapFormat|FirstBitmapFormat)|ert(?:SecondButtonReturn|ThirdButtonReturn|OtherReturn|DefaultReturn|ErrorReturn|FirstButtonReturn|AlternateReturn)|l(?:ScrollerParts|DomainsMask|PredicateModifier|LibrariesDirectory|ApplicationsDirectory))|rgument(?:sWrongScriptError|EvaluationScriptError)|bove(?:Bottom|Top)|WTEventType)))(?:\\b)"},{token:"support.function.C99.c",regex:s.cFunctions},{token:n.getKeywords(),regex:"[a-zA-Z_$][a-zA-Z0-9_$]*\\b"},{token:"punctuation.section.scope.begin.objc",regex:"\\[",next:"bracketed_content"},{token:"meta.function.objc",regex:"^(?:-|\\+)\\s*"}],constant_NSString:[{token:"constant.character.escape.objc",regex:e},{token:"invalid.illegal.unknown-escape.objc",regex:"\\\\."},{token:"string",regex:'[^"\\\\]+'},{token:"punctuation.definition.string.end",regex:'"',next:"start"}],protocol_list:[{token:"punctuation.section.scope.end.objc",regex:">",next:"start"},{token:"support.other.protocol.objc",regex:"\bNS(?:GlyphStorage|M(?:utableCopying|enuItem)|C(?:hangeSpelling|o(?:ding|pying|lorPicking(?:Custom|Default)))|T(?:oolbarItemValidations|ext(?:Input|AttachmentCell))|I(?:nputServ(?:iceProvider|erMouseTracker)|gnoreMisspelledWords)|Obj(?:CTypeSerializationCallBack|ect)|D(?:ecimalNumberBehaviors|raggingInfo)|U(?:serInterfaceValidations|RL(?:HandleClient|DownloadDelegate|ProtocolClient|AuthenticationChallengeSender))|Validated(?:ToobarItem|UserInterfaceItem)|Locking)\b"}],selectors:[{token:"support.function.any-method.name-of-parameter.objc",regex:"\\b(?:[a-zA-Z_:][\\w]*)+"},{token:"punctuation",regex:"\\)",next:"start"}],bracketed_content:[{token:"punctuation.section.scope.end.objc",regex:"]",next:"start"},{token:["support.function.any-method.objc"],regex:"(?:predicateWithFormat:| NSPredicate predicateWithFormat:)",next:"start"},{token:"support.function.any-method.objc",regex:"\\w+(?::|(?=]))",next:"start"}],bracketed_strings:[{token:"punctuation.section.scope.end.objc",regex:"]",next:"start"},{token:"keyword.operator.logical.predicate.cocoa",regex:"\\b(?:AND|OR|NOT|IN)\\b"},{token:["invalid.illegal.unknown-method.objc","punctuation.separator.arguments.objc"],regex:"\\b(\\w+)(:)"},{regex:"\\b(?:ALL|ANY|SOME|NONE)\\b",token:"constant.language.predicate.cocoa"},{regex:"\\b(?:NULL|NIL|SELF|TRUE|YES|FALSE|NO|FIRST|LAST|SIZE)\\b",token:"constant.language.predicate.cocoa"},{regex:"\\b(?:MATCHES|CONTAINS|BEGINSWITH|ENDSWITH|BETWEEN)\\b",token:"keyword.operator.comparison.predicate.cocoa"},{regex:"\\bC(?:ASEINSENSITIVE|I)\\b",token:"keyword.other.modifier.predicate.cocoa"},{regex:"\\b(?:ANYKEY|SUBQUERY|CAST|TRUEPREDICATE|FALSEPREDICATE)\\b",token:"keyword.other.predicate.cocoa"},{regex:e,token:"constant.character.escape.objc"},{regex:"\\\\.",token:"invalid.illegal.unknown-escape.objc"},{token:"string",regex:'[^"\\\\]'},{token:"punctuation.definition.string.end.objc",regex:'"',next:"predicates"}],comment:[{token:"comment",regex:".*?\\*\\/",next:"start"},{token:"comment",regex:".+"}],methods:[{token:"meta.function.objc",regex:"(?=\\{|#)|;",next:"start"}]};for(var u in r)this.$rules[u]?this.$rules[u].push&&this.$rules[u].push.apply(this.$rules[u],r[u]):this.$rules[u]=r[u];this.$rules.bracketed_content=this.$rules.bracketed_content.concat(this.$rules.start,t),this.embedRules(i,"doc-",[i.getEndRule("start")])};r.inherits(u,o),t.ObjectiveCHighlightRules=u}),define("ace/mode/folding/cstyle",["require","exports","module","ace/lib/oop","ace/range","ace/mode/folding/fold_mode"],function(e,t,n){"use strict";var r=e("../../lib/oop"),i=e("../../range").Range,s=e("./fold_mode").FoldMode,o=t.FoldMode=function(e){e&&(this.foldingStartMarker=new RegExp(this.foldingStartMarker.source.replace(/\|[^|]*?$/,"|"+e.start)),this.foldingStopMarker=new RegExp(this.foldingStopMarker.source.replace(/\|[^|]*?$/,"|"+e.end)))};r.inherits(o,s),function(){this.foldingStartMarker=/(\{|\[)[^\}\]]*$|^\s*(\/\*)/,this.foldingStopMarker=/^[^\[\{]*(\}|\])|^[\s\*]*(\*\/)/,this.singleLineBlockCommentRe=/^\s*(\/\*).*\*\/\s*$/,this.tripleStarBlockCommentRe=/^\s*(\/\*\*\*).*\*\/\s*$/,this.startRegionRe=/^\s*(\/\*|\/\/)#?region\b/,this._getFoldWidgetBase=this.getFoldWidget,this.getFoldWidget=function(e,t,n){var r=e.getLine(n);if(this.singleLineBlockCommentRe.test(r)&&!this.startRegionRe.test(r)&&!this.tripleStarBlockCommentRe.test(r))return"";var i=this._getFoldWidgetBase(e,t,n);return!i&&this.startRegionRe.test(r)?"start":i},this.getFoldWidgetRange=function(e,t,n,r){var i=e.getLine(n);if(this.startRegionRe.test(i))return this.getCommentRegionBlock(e,i,n);var s=i.match(this.foldingStartMarker);if(s){var o=s.index;if(s[1])return this.openingBracketBlock(e,s[1],n,o);var u=e.getCommentFoldRange(n,o+s[0].length,1);return u&&!u.isMultiLine()&&(r?u=this.getSectionRange(e,n):t!="all"&&(u=null)),u}if(t==="markbegin")return;var s=i.match(this.foldingStopMarker);if(s){var o=s.index+s[0].length;return s[1]?this.closingBracketBlock(e,s[1],n,o):e.getCommentFoldRange(n,o,-1)}},this.getSectionRange=function(e,t){var n=e.getLine(t),r=n.search(/\S/),s=t,o=n.length;t+=1;var u=t,a=e.getLength();while(++tf)break;var l=this.getFoldWidgetRange(e,"all",t);if(l){if(l.start.row<=s)break;if(l.isMultiLine())t=l.end.row;else if(r==f)break}u=t}return new i(s,o,u,e.getLine(u).length)},this.getCommentRegionBlock=function(e,t,n){var r=t.search(/\s*$/),s=e.getLength(),o=n,u=/^\s*(?:\/\*|\/\/|--)#?(end)?region\b/,a=1;while(++no)return new i(o,r,l,t.length)}}.call(o.prototype)}),define("ace/mode/objectivec",["require","exports","module","ace/lib/oop","ace/mode/text","ace/mode/objectivec_highlight_rules","ace/mode/folding/cstyle"],function(e,t,n){"use strict";var r=e("../lib/oop"),i=e("./text").Mode,s=e("./objectivec_highlight_rules").ObjectiveCHighlightRules,o=e("./folding/cstyle").FoldMode,u=function(){this.HighlightRules=s,this.foldingRules=new o,this.$behaviour=this.$defaultBehaviour};r.inherits(u,i),function(){this.lineCommentStart="//",this.blockComment={start:"/*",end:"*/"},this.$id="ace/mode/objectivec"}.call(u.prototype),t.Mode=u}) diff --git a/resources/lang/en/base.php b/resources/lang/en/base.php index 15554fd1..2c5242df 100644 --- a/resources/lang/en/base.php +++ b/resources/lang/en/base.php @@ -173,7 +173,7 @@ return [ 'service_header' => 'Service Control', 'service' => [ 'list' => [ - 'title' => 'List Services', + 'title' => 'List Service', 'desc' => 'Allows listing of all services configured on the system.', ], 'view' => [ diff --git a/resources/themes/pterodactyl/admin/services/functions.blade.php b/resources/themes/pterodactyl/admin/services/functions.blade.php index ce06f8f6..1de52763 100644 --- a/resources/themes/pterodactyl/admin/services/functions.blade.php +++ b/resources/themes/pterodactyl/admin/services/functions.blade.php @@ -20,14 +20,14 @@ @extends('layouts.admin') @section('title') - Services → {{ $service->name }} → Functions + Service → {{ $service->name }} → Functions @endsection @section('content-header')

{{ $service->name }}Extend the default daemon functions using this service file.

diff --git a/resources/themes/pterodactyl/admin/services/index.blade.php b/resources/themes/pterodactyl/admin/services/index.blade.php index a943355e..a60ffc22 100644 --- a/resources/themes/pterodactyl/admin/services/index.blade.php +++ b/resources/themes/pterodactyl/admin/services/index.blade.php @@ -20,14 +20,14 @@ @extends('layouts.admin') @section('title') - Services + Service @endsection @section('content-header') -

ServicesAll services currently available on this system.

+

ServiceAll services currently available on this system.

@endsection @@ -36,7 +36,7 @@
-

Configured Services

+

Configured Service

diff --git a/resources/themes/pterodactyl/admin/services/new.blade.php b/resources/themes/pterodactyl/admin/services/new.blade.php index aa3bd0b3..6697aa98 100644 --- a/resources/themes/pterodactyl/admin/services/new.blade.php +++ b/resources/themes/pterodactyl/admin/services/new.blade.php @@ -27,7 +27,7 @@

New ServiceConfigure a new service to deploy to all nodes.

@endsection @@ -64,7 +64,7 @@
-

Services are downloaded by the daemon and stored in a folder using this name. The storage location is /srv/daemon/services/{NAME} by default.

+

Service are downloaded by the daemon and stored in a folder using this name. The storage location is /srv/daemon/services/{NAME} by default.

diff --git a/resources/themes/pterodactyl/admin/services/options/new.blade.php b/resources/themes/pterodactyl/admin/services/options/new.blade.php index 03e2a05c..80e61101 100644 --- a/resources/themes/pterodactyl/admin/services/options/new.blade.php +++ b/resources/themes/pterodactyl/admin/services/options/new.blade.php @@ -20,14 +20,14 @@ @extends('layouts.admin') @section('title') - Services → New Option + Service → New Option @endsection @section('content-header')

New OptionCreate a new service option to assign to servers.

@endsection diff --git a/resources/themes/pterodactyl/admin/services/options/scripts.blade.php b/resources/themes/pterodactyl/admin/services/options/scripts.blade.php index f94f9f9a..0d1881dd 100644 --- a/resources/themes/pterodactyl/admin/services/options/scripts.blade.php +++ b/resources/themes/pterodactyl/admin/services/options/scripts.blade.php @@ -20,14 +20,14 @@ @extends('layouts.admin') @section('title') - Services → Option: {{ $option->name }} → Scripts + Service → Option: {{ $option->name }} → Scripts @endsection @section('content-header')

{{ $option->name }}Manage install and upgrade scripts for this service option.

diff --git a/resources/themes/pterodactyl/admin/services/options/variables.blade.php b/resources/themes/pterodactyl/admin/services/options/variables.blade.php index d0baa8c4..975745df 100644 --- a/resources/themes/pterodactyl/admin/services/options/variables.blade.php +++ b/resources/themes/pterodactyl/admin/services/options/variables.blade.php @@ -27,7 +27,7 @@

{{ $option->name }}Managing variables for this service option.

diff --git a/resources/themes/pterodactyl/layouts/admin.blade.php b/resources/themes/pterodactyl/layouts/admin.blade.php index e568ef0a..66dd63d4 100644 --- a/resources/themes/pterodactyl/layouts/admin.blade.php +++ b/resources/themes/pterodactyl/layouts/admin.blade.php @@ -128,7 +128,7 @@
  • SERVICE MANAGEMENT
  • - Services + Service
  • diff --git a/tests/Unit/Http/Controllers/Admin/DatabaseControllerTest.php b/tests/Unit/Http/Controllers/Admin/DatabaseControllerTest.php index 03402adb..c79ddd71 100644 --- a/tests/Unit/Http/Controllers/Admin/DatabaseControllerTest.php +++ b/tests/Unit/Http/Controllers/Admin/DatabaseControllerTest.php @@ -99,6 +99,9 @@ class DatabaseControllerTest extends TestCase $this->assertViewKeyEquals('hosts', 'getWithViewDetails', $view); } + /** + * Test the view controller for displaying a specific database host. + */ public function testViewController() { $this->locationRepository->shouldReceive('getAllWithNodes')->withNoArgs()->once()->andReturn('getAllWithNodes'); diff --git a/tests/Unit/Services/Allocations/AssignmentServiceTest.php b/tests/Unit/Services/Allocations/AssignmentServiceTest.php index f55fdc5f..1d479938 100644 --- a/tests/Unit/Services/Allocations/AssignmentServiceTest.php +++ b/tests/Unit/Services/Allocations/AssignmentServiceTest.php @@ -71,7 +71,7 @@ class AssignmentServiceTest extends TestCase // // This can also be avoided if tests were run in isolated processes, or if that test // came first, but neither of those are good solutions, so this is the next best option. - PHPMock::defineFunctionMock('\\Pterodactyl\\Services\\Allocations', 'gethostbyname'); + PHPMock::defineFunctionMock('\\Pterodactyl\\Service\\Allocations', 'gethostbyname'); $this->node = factory(Node::class)->make(); $this->connection = m::mock(ConnectionInterface::class); @@ -180,7 +180,7 @@ class AssignmentServiceTest extends TestCase 'allocation_ports' => ['1024'], ]; - $this->getFunctionMock('\\Pterodactyl\\Services\\Allocations', 'gethostbyname') + $this->getFunctionMock('\\Pterodactyl\\Service\\Allocations', 'gethostbyname') ->expects($this->once())->willReturn('192.168.1.1'); $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); diff --git a/tests/Unit/Services/Api/KeyServiceTest.php b/tests/Unit/Services/Api/KeyServiceTest.php index 33d01cb0..0ac71382 100644 --- a/tests/Unit/Services/Api/KeyServiceTest.php +++ b/tests/Unit/Services/Api/KeyServiceTest.php @@ -81,7 +81,7 @@ class KeyServiceTest extends TestCase */ public function test_create_function() { - $this->getFunctionMock('\\Pterodactyl\\Services\\Api', 'random_bytes') + $this->getFunctionMock('\\Pterodactyl\\Service\\Api', 'random_bytes') ->expects($this->exactly(2)) ->willReturnCallback(function ($bytes) { return hex2bin(str_pad('', $bytes * 2, '0')); diff --git a/tests/Unit/Services/Database/DatabaseManagementServiceTest.php b/tests/Unit/Services/Database/DatabaseManagementServiceTest.php index ca1e29ce..2f3f6ace 100644 --- a/tests/Unit/Services/Database/DatabaseManagementServiceTest.php +++ b/tests/Unit/Services/Database/DatabaseManagementServiceTest.php @@ -84,7 +84,7 @@ class DatabaseManagementServiceTest extends TestCase $this->encrypter = m::mock(Encrypter::class); $this->repository = m::mock(DatabaseRepositoryInterface::class); - $this->getFunctionMock('\\Pterodactyl\\Services\\Database', 'str_random') + $this->getFunctionMock('\\Pterodactyl\\Service\\Database', 'str_random') ->expects($this->any())->willReturn('str_random'); $this->service = new DatabaseManagementService( diff --git a/tests/Unit/Services/Nodes/CreationServiceTest.php b/tests/Unit/Services/Nodes/CreationServiceTest.php index 84efcbde..f3562cc1 100644 --- a/tests/Unit/Services/Nodes/CreationServiceTest.php +++ b/tests/Unit/Services/Nodes/CreationServiceTest.php @@ -61,7 +61,7 @@ class CreationServiceTest extends TestCase */ public function testNodeIsCreatedAndDaemonSecretIsGenerated() { - $this->getFunctionMock('\\Pterodactyl\\Services\\Nodes', 'bin2hex') + $this->getFunctionMock('\\Pterodactyl\\Service\\Nodes', 'bin2hex') ->expects($this->once())->willReturn('hexResult'); $this->repository->shouldReceive('create')->with([ diff --git a/tests/Unit/Services/Nodes/UpdateServiceTest.php b/tests/Unit/Services/Nodes/UpdateServiceTest.php index 74db802b..bb660985 100644 --- a/tests/Unit/Services/Nodes/UpdateServiceTest.php +++ b/tests/Unit/Services/Nodes/UpdateServiceTest.php @@ -97,14 +97,14 @@ class UpdateServiceTest extends TestCase */ public function testNodeIsUpdatedAndDaemonSecretIsReset() { - $this->getFunctionMock('\\Pterodactyl\\Services\\Nodes', 'random_bytes') + $this->getFunctionMock('\\Pterodactyl\\Service\\Nodes', 'random_bytes') ->expects($this->once())->willReturnCallback(function ($bytes) { $this->assertEquals(CreationService::DAEMON_SECRET_LENGTH, $bytes); return '\00'; }); - $this->getFunctionMock('\\Pterodactyl\\Services\\Nodes', 'bin2hex') + $this->getFunctionMock('\\Pterodactyl\\Service\\Nodes', 'bin2hex') ->expects($this->once())->willReturn('hexResponse'); $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() diff --git a/tests/Unit/Services/Servers/CreationServiceTest.php b/tests/Unit/Services/Servers/CreationServiceTest.php index d9a5c245..4cdaea1a 100644 --- a/tests/Unit/Services/Servers/CreationServiceTest.php +++ b/tests/Unit/Services/Servers/CreationServiceTest.php @@ -155,7 +155,7 @@ class CreationServiceTest extends TestCase $this->uuid = m::mock('overload:Ramsey\Uuid\Uuid'); $this->writer = m::mock(Writer::class); - $this->getFunctionMock('\\Pterodactyl\\Services\\Servers', 'bin2hex') + $this->getFunctionMock('\\Pterodactyl\\Service\\Servers', 'bin2hex') ->expects($this->any())->willReturn('randomstring'); $this->getFunctionMock('\\Ramsey\\Uuid\\Uuid', 'uuid4') diff --git a/tests/Unit/Services/Servers/DetailsModificationServiceTest.php b/tests/Unit/Services/Servers/DetailsModificationServiceTest.php index a617fbaa..916a313c 100644 --- a/tests/Unit/Services/Servers/DetailsModificationServiceTest.php +++ b/tests/Unit/Services/Servers/DetailsModificationServiceTest.php @@ -84,7 +84,7 @@ class DetailsModificationServiceTest extends TestCase $this->repository = m::mock(ServerRepository::class); $this->writer = m::mock(Writer::class); - $this->getFunctionMock('\\Pterodactyl\\Services\\Servers', 'bin2hex') + $this->getFunctionMock('\\Pterodactyl\\Service\\Servers', 'bin2hex') ->expects($this->any())->willReturn('randomString'); $this->service = new DetailsModificationService( diff --git a/tests/Unit/Services/Servers/UsernameGenerationServiceTest.php b/tests/Unit/Services/Servers/UsernameGenerationServiceTest.php index c0d80cd5..b3e3f788 100644 --- a/tests/Unit/Services/Servers/UsernameGenerationServiceTest.php +++ b/tests/Unit/Services/Servers/UsernameGenerationServiceTest.php @@ -46,10 +46,10 @@ class UsernameGenerationServiceTest extends TestCase $this->service = new UsernameGenerationService(); - $this->getFunctionMock('\\Pterodactyl\\Services\\Servers', 'bin2hex') + $this->getFunctionMock('\\Pterodactyl\\Service\\Servers', 'bin2hex') ->expects($this->any())->willReturn('dddddddd'); - $this->getFunctionMock('\\Pterodactyl\\Services\\Servers', 'str_random') + $this->getFunctionMock('\\Pterodactyl\\Service\\Servers', 'str_random') ->expects($this->any())->willReturnCallback(function ($count) { return str_pad('', $count, 'a'); }); diff --git a/tests/Unit/Services/Services/Options/InstallScriptUpdateServiceTest.php b/tests/Unit/Services/Services/Options/InstallScriptUpdateServiceTest.php index ea0b881c..4bb12a46 100644 --- a/tests/Unit/Services/Services/Options/InstallScriptUpdateServiceTest.php +++ b/tests/Unit/Services/Services/Options/InstallScriptUpdateServiceTest.php @@ -30,7 +30,7 @@ use Tests\TestCase; use Pterodactyl\Models\ServiceOption; use Pterodactyl\Services\Services\Options\InstallScriptUpdateService; use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; -use Pterodactyl\Exceptions\Services\ServiceOption\InvalidCopyFromException; +use Pterodactyl\Exceptions\Service\ServiceOption\InvalidCopyFromException; class InstallScriptUpdateServiceTest extends TestCase { diff --git a/tests/Unit/Services/Services/Options/OptionCreationServiceTest.php b/tests/Unit/Services/Services/Options/OptionCreationServiceTest.php index 931367d0..1d864b57 100644 --- a/tests/Unit/Services/Services/Options/OptionCreationServiceTest.php +++ b/tests/Unit/Services/Services/Options/OptionCreationServiceTest.php @@ -30,7 +30,7 @@ use Tests\TestCase; use Pterodactyl\Models\ServiceOption; use Pterodactyl\Services\Services\Options\OptionCreationService; use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; -use Pterodactyl\Exceptions\Services\ServiceOption\NoParentConfigurationFoundException; +use Pterodactyl\Exceptions\Service\ServiceOption\NoParentConfigurationFoundException; class OptionCreationServiceTest extends TestCase { diff --git a/tests/Unit/Services/Services/Options/OptionDeletionServiceTest.php b/tests/Unit/Services/Services/Options/OptionDeletionServiceTest.php index bb369563..381ac3a5 100644 --- a/tests/Unit/Services/Services/Options/OptionDeletionServiceTest.php +++ b/tests/Unit/Services/Services/Options/OptionDeletionServiceTest.php @@ -26,7 +26,7 @@ namespace Tests\Unit\Services\Services\Options; use Mockery as m; use Tests\TestCase; -use Pterodactyl\Exceptions\Services\HasActiveServersException; +use Pterodactyl\Exceptions\Service\HasActiveServersException; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Services\Services\Options\OptionDeletionService; use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; diff --git a/tests/Unit/Services/Services/Options/OptionUpdateServiceTest.php b/tests/Unit/Services/Services/Options/OptionUpdateServiceTest.php index 7e34ad19..ecc5f76c 100644 --- a/tests/Unit/Services/Services/Options/OptionUpdateServiceTest.php +++ b/tests/Unit/Services/Services/Options/OptionUpdateServiceTest.php @@ -30,7 +30,7 @@ use Tests\TestCase; use Pterodactyl\Models\ServiceOption; use Pterodactyl\Services\Services\Options\OptionUpdateService; use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; -use Pterodactyl\Exceptions\Services\ServiceOption\NoParentConfigurationFoundException; +use Pterodactyl\Exceptions\Service\ServiceOption\NoParentConfigurationFoundException; class OptionUpdateServiceTest extends TestCase { diff --git a/tests/Unit/Services/Services/ServiceDeletionServiceTest.php b/tests/Unit/Services/Services/ServiceDeletionServiceTest.php index 0290b625..f986de51 100644 --- a/tests/Unit/Services/Services/ServiceDeletionServiceTest.php +++ b/tests/Unit/Services/Services/ServiceDeletionServiceTest.php @@ -28,7 +28,7 @@ use Exception; use Mockery as m; use Tests\TestCase; use Pterodactyl\Services\Services\ServiceDeletionService; -use Pterodactyl\Exceptions\Services\HasActiveServersException; +use Pterodactyl\Exceptions\Service\HasActiveServersException; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; diff --git a/tests/Unit/Services/Services/Variables/VariableCreationServiceTest.php b/tests/Unit/Services/Services/Variables/VariableCreationServiceTest.php index 87ca4ed9..933e284c 100644 --- a/tests/Unit/Services/Services/Variables/VariableCreationServiceTest.php +++ b/tests/Unit/Services/Services/Variables/VariableCreationServiceTest.php @@ -99,7 +99,7 @@ class VariableCreationServiceTest extends TestCase * Test that all of the reserved variables defined in the model trigger an exception. * * @dataProvider reservedNamesProvider - * @expectedException \Pterodactyl\Exceptions\Services\ServiceVariable\ReservedVariableNameException + * @expectedException \Pterodactyl\Exceptions\Service\ServiceVariable\ReservedVariableNameException */ public function testExceptionIsThrownIfEnvironmentVariableIsInListOfReservedNames($variable) { diff --git a/tests/Unit/Services/Services/Variables/VariableUpdateServiceTest.php b/tests/Unit/Services/Services/Variables/VariableUpdateServiceTest.php index 2a19d9f0..905389ef 100644 --- a/tests/Unit/Services/Services/Variables/VariableUpdateServiceTest.php +++ b/tests/Unit/Services/Services/Variables/VariableUpdateServiceTest.php @@ -126,7 +126,7 @@ class VariableUpdateServiceTest extends TestCase * Test that all of the reserved variables defined in the model trigger an exception. * * @dataProvider reservedNamesProvider - * @expectedException \Pterodactyl\Exceptions\Services\ServiceVariable\ReservedVariableNameException + * @expectedException \Pterodactyl\Exceptions\Service\ServiceVariable\ReservedVariableNameException */ public function testExceptionIsThrownIfEnvironmentVariableIsInListOfReservedNames($variable) { From 280633b28ae417f9329bcc0df5a5ce37a4a898f2 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 19 Aug 2017 20:40:00 -0500 Subject: [PATCH 56/99] More service classes for pack management --- .../Pack/InvalidFileMimeTypeException.php | 4 +- .../Pack/InvalidFileUploadException.php | 4 +- .../InvalidPackArchiveFormatException.php | 32 ++++ .../Pack/UnreadableZipArchiveException.php | 32 ++++ .../Service/Pack/ZipExtractionException.php | 31 ++++ app/Http/Controllers/Admin/PackController.php | 120 ++++++++++----- app/Services/Packs/PackCreationService.php | 25 ++-- app/Services/Packs/PackDeletionService.php | 100 +++++++++++++ app/Services/Packs/PackUpdateService.php | 90 +++++++++++ app/Services/Packs/TemplateUploadService.php | 140 ++++++++++++++++++ config/pterodactyl.php | 4 - resources/lang/en/admin/exceptions.php | 9 ++ resources/lang/en/admin/pack.php | 29 ++++ 13 files changed, 568 insertions(+), 52 deletions(-) create mode 100644 app/Exceptions/Service/Pack/InvalidPackArchiveFormatException.php create mode 100644 app/Exceptions/Service/Pack/UnreadableZipArchiveException.php create mode 100644 app/Exceptions/Service/Pack/ZipExtractionException.php create mode 100644 app/Services/Packs/PackDeletionService.php create mode 100644 app/Services/Packs/PackUpdateService.php create mode 100644 app/Services/Packs/TemplateUploadService.php create mode 100644 resources/lang/en/admin/pack.php diff --git a/app/Exceptions/Service/Pack/InvalidFileMimeTypeException.php b/app/Exceptions/Service/Pack/InvalidFileMimeTypeException.php index f34e1be8..bbd5d410 100644 --- a/app/Exceptions/Service/Pack/InvalidFileMimeTypeException.php +++ b/app/Exceptions/Service/Pack/InvalidFileMimeTypeException.php @@ -24,7 +24,9 @@ namespace Pterodactyl\Exceptions\Service\Pack; -class InvalidFileMimeTypeException extends \Exception +use Pterodactyl\Exceptions\DisplayException; + +class InvalidFileMimeTypeException extends DisplayException { // } diff --git a/app/Exceptions/Service/Pack/InvalidFileUploadException.php b/app/Exceptions/Service/Pack/InvalidFileUploadException.php index ffef85b8..4861512c 100644 --- a/app/Exceptions/Service/Pack/InvalidFileUploadException.php +++ b/app/Exceptions/Service/Pack/InvalidFileUploadException.php @@ -24,7 +24,9 @@ namespace Pterodactyl\Exceptions\Service\Pack; -class InvalidFileUploadException extends \Exception +use Pterodactyl\Exceptions\DisplayException; + +class InvalidFileUploadException extends DisplayException { // } diff --git a/app/Exceptions/Service/Pack/InvalidPackArchiveFormatException.php b/app/Exceptions/Service/Pack/InvalidPackArchiveFormatException.php new file mode 100644 index 00000000..f13a3358 --- /dev/null +++ b/app/Exceptions/Service/Pack/InvalidPackArchiveFormatException.php @@ -0,0 +1,32 @@ +. + * + * 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\Exceptions\Service\Pack; + +use Pterodactyl\Exceptions\DisplayException; + +class InvalidPackArchiveFormatException extends DisplayException +{ + // +} diff --git a/app/Exceptions/Service/Pack/UnreadableZipArchiveException.php b/app/Exceptions/Service/Pack/UnreadableZipArchiveException.php new file mode 100644 index 00000000..a803d158 --- /dev/null +++ b/app/Exceptions/Service/Pack/UnreadableZipArchiveException.php @@ -0,0 +1,32 @@ +. + * + * 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\Exceptions\Service\Pack; + +use Pterodactyl\Exceptions\DisplayException; + +class UnreadableZipArchiveException extends DisplayException +{ + // +} diff --git a/app/Exceptions/Service/Pack/ZipExtractionException.php b/app/Exceptions/Service/Pack/ZipExtractionException.php new file mode 100644 index 00000000..b465075e --- /dev/null +++ b/app/Exceptions/Service/Pack/ZipExtractionException.php @@ -0,0 +1,31 @@ +. + * + * 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\Exceptions\Service\Pack; + +use Pterodactyl\Exceptions\DisplayException; + +class ZipExtractionException extends DisplayException +{ +} diff --git a/app/Http/Controllers/Admin/PackController.php b/app/Http/Controllers/Admin/PackController.php index d0227367..9a1b089f 100644 --- a/app/Http/Controllers/Admin/PackController.php +++ b/app/Http/Controllers/Admin/PackController.php @@ -26,6 +26,12 @@ namespace Pterodactyl\Http\Controllers\Admin; use Log; use Alert; +use Prologue\Alerts\AlertsMessageBag; +use Pterodactyl\Contracts\Repository\PackRepositoryInterface; +use Pterodactyl\Services\Packs\PackCreationService; +use Pterodactyl\Services\Packs\PackDeletionService; +use Pterodactyl\Services\Packs\PackUpdateService; +use Pterodactyl\Services\Packs\TemplateUploadService; use Storage; use Illuminate\Http\Request; use Pterodactyl\Models\Pack; @@ -37,10 +43,66 @@ use Pterodactyl\Exceptions\DisplayValidationException; class PackController extends Controller { + /** + * @var \Prologue\Alerts\AlertsMessageBag + */ + protected $alert; + + /** + * @var \Pterodactyl\Services\Packs\PackCreationService + */ + protected $creationService; + + /** + * @var \Pterodactyl\Services\Packs\PackDeletionService + */ + protected $deletionService; + + /** + * @var \Pterodactyl\Contracts\Repository\PackRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Packs\PackUpdateService + */ + protected $packUpdateService; + + /** + * @var \Pterodactyl\Services\Packs\TemplateUploadService + */ + protected $templateUploadService; + + /** + * PackController constructor. + * + * @param \Prologue\Alerts\AlertsMessageBag $alert + * @param \Pterodactyl\Services\Packs\PackCreationService $creationService + * @param \Pterodactyl\Services\Packs\PackDeletionService $deletionService + * @param \Pterodactyl\Contracts\Repository\PackRepositoryInterface $repository + * @param \Pterodactyl\Services\Packs\PackUpdateService $packUpdateService + * @param \Pterodactyl\Services\Packs\TemplateUploadService $templateUploadService + */ + public function __construct( + AlertsMessageBag $alert, + PackCreationService $creationService, + PackDeletionService $deletionService, + PackRepositoryInterface $repository, + PackUpdateService $packUpdateService, + TemplateUploadService $templateUploadService + ) { + $this->alert = $alert; + $this->creationService = $creationService; + $this->deletionService = $deletionService; + $this->repository = $repository; + $this->packUpdateService = $packUpdateService; + $this->templateUploadService = $templateUploadService; + } + /** * Display listing of all packs on the system. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ public function index(Request $request) @@ -57,7 +119,7 @@ class PackController extends Controller /** * Display new pack creation form. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ public function create(Request $request) @@ -70,7 +132,7 @@ class PackController extends Controller /** * Display new pack creation modal for use with template upload. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ public function newTemplate(Request $request) @@ -83,42 +145,34 @@ class PackController extends Controller /** * Handle create pack request and route user to location. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Service\Pack\InvalidFileMimeTypeException + * @throws \Pterodactyl\Exceptions\Service\Pack\InvalidFileUploadException + * @throws \Pterodactyl\Exceptions\Service\Pack\InvalidPackArchiveFormatException + * @throws \Pterodactyl\Exceptions\Service\Pack\UnreadableZipArchiveException + * @throws \Pterodactyl\Exceptions\Service\Pack\ZipExtractionException */ public function store(Request $request) { - $repo = new PackRepository; - - try { - if ($request->input('action') === 'from_template') { - $pack = $repo->createWithTemplate($request->intersect(['option_id', 'file_upload'])); - } else { - $pack = $repo->create($request->intersect([ - 'name', 'description', 'version', 'option_id', - 'selectable', 'visible', 'locked', 'file_upload', - ])); - } - Alert::success('Pack successfully created on the system.')->flash(); - - return redirect()->route('admin.packs.view', $pack->id); - } catch (DisplayValidationException $ex) { - return redirect()->route('admin.packs.new')->withErrors(json_decode($ex->getMessage()))->withInput(); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An error occured while attempting to add a new service pack. This error has been logged.')->flash(); + if ($request->has('from_template')) { + $pack = $this->templateUploadService->handle($request->input('option_id'), $request->input('file_upload')); + } else { + $pack = $this->creationService->handle($request->normalize(), $request->input('file_upload')); } - return redirect()->route('admin.packs.new')->withInput(); + $this->alert->success(trans('admin/pack.notices.pack_created'))->flash(); + + return redirect()->route('admin.packs.view', $pack->id); } /** * Display pack view template to user. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param int $id * @return \Illuminate\View\View */ public function view(Request $request, $id) @@ -132,8 +186,8 @@ class PackController extends Controller /** * Handle updating or deleting pack information. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param int $id * @return \Illuminate\Http\RedirectResponse */ public function update(Request $request, $id) @@ -168,9 +222,9 @@ class PackController extends Controller /** * Creates an archive of the pack and downloads it to the browser. * - * @param \Illuminate\Http\Request $request - * @param int $id - * @param bool $files + * @param \Illuminate\Http\Request $request + * @param int $id + * @param bool $files * @return \Symfony\Component\HttpFoundation\BinaryFileResponse */ public function export(Request $request, $id, $files = false) diff --git a/app/Services/Packs/PackCreationService.php b/app/Services/Packs/PackCreationService.php index ff74b27c..10daaeb3 100644 --- a/app/Services/Packs/PackCreationService.php +++ b/app/Services/Packs/PackCreationService.php @@ -24,20 +24,20 @@ namespace Pterodactyl\Services\Packs; +use Illuminate\Http\UploadedFile; use Ramsey\Uuid\Uuid; use Illuminate\Database\ConnectionInterface; use Pterodactyl\Contracts\Repository\PackRepositoryInterface; -use Illuminate\Contracts\Config\Repository as ConfigRepository; use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory; use Pterodactyl\Exceptions\Service\Pack\InvalidFileUploadException; use Pterodactyl\Exceptions\Service\Pack\InvalidFileMimeTypeException; class PackCreationService { - /** - * @var \Illuminate\Contracts\Config\Repository - */ - protected $config; + const VALID_UPLOAD_TYPES = [ + 'application/gzip', + 'application/x-gzip', + ]; /** * @var \Illuminate\Database\ConnectionInterface @@ -57,18 +57,15 @@ class PackCreationService /** * PackCreationService constructor. * - * @param \Illuminate\Contracts\Config\Repository $config * @param \Illuminate\Database\ConnectionInterface $connection * @param \Illuminate\Contracts\Filesystem\Factory $storage * @param \Pterodactyl\Contracts\Repository\PackRepositoryInterface $repository */ public function __construct( - ConfigRepository $config, ConnectionInterface $connection, FilesystemFactory $storage, PackRepositoryInterface $repository ) { - $this->config = $config; $this->connection = $connection; $this->repository = $repository; $this->storage = $storage; @@ -85,15 +82,17 @@ class PackCreationService * @throws \Pterodactyl\Exceptions\Service\Pack\InvalidFileMimeTypeException * @throws \Pterodactyl\Exceptions\Service\Pack\InvalidFileUploadException */ - public function handle(array $data, $file = null) + public function handle(array $data, UploadedFile $file = null) { if (! is_null($file)) { if (! $file->isValid()) { - throw new InvalidFileUploadException; + throw new InvalidFileUploadException(trans('admin/exceptions.packs.invalid_upload')); } - if (! in_array($file->getMimeType(), $this->config->get('pterodactyl.files.pack_types'))) { - throw new InvalidFileMimeTypeException; + if (! in_array($file->getMimeType(), self::VALID_UPLOAD_TYPES)) { + throw new InvalidFileMimeTypeException(trans('admin/exceptions.packs.invalid_mime', [ + 'type' => implode(', ', self::VALID_UPLOAD_TYPES), + ])); } } @@ -107,7 +106,7 @@ class PackCreationService ['uuid' => Uuid::uuid4()], $data )); - $this->storage->disk('default')->makeDirectory('packs/' . $pack->uuid); + $this->storage->disk()->makeDirectory('packs/' . $pack->uuid); if (! is_null($file)) { $file->storeAs('packs/' . $pack->uuid, 'archive.tar.gz'); } diff --git a/app/Services/Packs/PackDeletionService.php b/app/Services/Packs/PackDeletionService.php new file mode 100644 index 00000000..f38a2df7 --- /dev/null +++ b/app/Services/Packs/PackDeletionService.php @@ -0,0 +1,100 @@ +. + * + * 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\Packs; + +use Pterodactyl\Models\Pack; +use Illuminate\Database\ConnectionInterface; +use Pterodactyl\Contracts\Repository\PackRepositoryInterface; +use Pterodactyl\Exceptions\Service\HasActiveServersException; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory; + +class PackDeletionService +{ + /** + * @var \Illuminate\Database\ConnectionInterface + */ + protected $connection; + + /** + * @var \Pterodactyl\Contracts\Repository\PackRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $serverRepository; + + /** + * @var \Illuminate\Contracts\Filesystem\Factory + */ + protected $storage; + + /** + * PackDeletionService constructor. + * + * @param \Illuminate\Database\ConnectionInterface $connection + * @param \Illuminate\Contracts\Filesystem\Factory $storage + * @param \Pterodactyl\Contracts\Repository\PackRepositoryInterface $repository + * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $serverRepository + */ + public function __construct( + ConnectionInterface $connection, + FilesystemFactory $storage, + PackRepositoryInterface $repository, + ServerRepositoryInterface $serverRepository + ) { + $this->connection = $connection; + $this->repository = $repository; + $this->serverRepository = $serverRepository; + $this->storage = $storage; + } + + /** + * Delete a pack from the database as well as the archive stored on the server. + * + * @param int|\Pterodactyl\Models\Pack$pack + * + * @throws \Pterodactyl\Exceptions\Service\HasActiveServersException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function handle($pack) + { + if (! $pack instanceof Pack) { + $pack = $this->repository->withColumns(['id', 'uuid'])->find($pack); + } + + $count = $this->serverRepository->findCountWhere([['pack_id', '=', $pack]]); + if ($count !== 0) { + throw new HasActiveServersException(trans('admin/exceptions.packs.delete_has_servers')); + } + + $this->connection->beginTransaction(); + $this->repository->delete($pack->id); + $this->storage->disk()->deleteDirectory('packs/' . $pack->uuid); + $this->connection->commit(); + } +} diff --git a/app/Services/Packs/PackUpdateService.php b/app/Services/Packs/PackUpdateService.php new file mode 100644 index 00000000..22e38727 --- /dev/null +++ b/app/Services/Packs/PackUpdateService.php @@ -0,0 +1,90 @@ +. + * + * 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\Packs; + +use Pterodactyl\Models\Pack; +use Pterodactyl\Contracts\Repository\PackRepositoryInterface; +use Pterodactyl\Exceptions\Service\HasActiveServersException; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; + +class PackUpdateService +{ + /** + * @var \Pterodactyl\Contracts\Repository\PackRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $serverRepository; + + /** + * PackUpdateService constructor. + * + * @param \Pterodactyl\Contracts\Repository\PackRepositoryInterface $repository + * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $serverRepository + */ + public function __construct( + PackRepositoryInterface $repository, + ServerRepositoryInterface $serverRepository + ) { + $this->repository = $repository; + $this->serverRepository = $serverRepository; + } + + /** + * Update a pack. + * + * @param int|\Pterodactyl\Models\Pack $pack + * @param array $data + * @return bool + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Service\HasActiveServersException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function handle($pack, array $data) + { + if (! $pack instanceof Pack) { + $pack = $this->repository->withColumns(['id', 'option_id'])->find($pack); + } + + if ((int) array_get($data, 'option_id', $pack->option_id) !== $pack->option_id) { + $count = $this->serverRepository->findCountWhere([['pack_id', '=', $pack->id]]); + + if ($count !== 0) { + throw new HasActiveServersException(trans('admin/exceptions.packs.update_has_servers')); + } + } + + // Transform values to boolean + $data['selectable'] = isset($data['selectable']); + $data['visible'] = isset($data['visible']); + $data['locked'] = isset($data['locked']); + + return $this->repository->withoutFresh()->update($pack->id, $data); + } +} diff --git a/app/Services/Packs/TemplateUploadService.php b/app/Services/Packs/TemplateUploadService.php new file mode 100644 index 00000000..c4e5b5fb --- /dev/null +++ b/app/Services/Packs/TemplateUploadService.php @@ -0,0 +1,140 @@ +. + * + * 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\Packs; + +use ZipArchive; +use Illuminate\Http\UploadedFile; +use Pterodactyl\Exceptions\Service\Pack\ZipExtractionException; +use Pterodactyl\Exceptions\Service\Pack\InvalidFileUploadException; +use Pterodactyl\Exceptions\Service\Pack\InvalidFileMimeTypeException; +use Pterodactyl\Exceptions\Service\Pack\UnreadableZipArchiveException; +use Pterodactyl\Exceptions\Service\Pack\InvalidPackArchiveFormatException; + +class TemplateUploadService +{ + const VALID_UPLOAD_TYPES = [ + 'application/zip', + 'text/plain', + 'application/json', + ]; + + /** + * @var \ZipArchive + */ + protected $archive; + + /** + * @var \Pterodactyl\Services\Packs\PackCreationService + */ + protected $creationService; + + /** + * TemplateUploadService constructor. + * + * @param \Pterodactyl\Services\Packs\PackCreationService $creationService + * @param \ZipArchive $archive + */ + public function __construct( + PackCreationService $creationService, + ZipArchive $archive + ) { + $this->archive = $archive; + $this->creationService = $creationService; + } + + /** + * Process an uploaded file to create a new pack from a JSON or ZIP format. + * + * @param int $option + * @param \Illuminate\Http\UploadedFile $file + * @return \Pterodactyl\Models\Pack + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Service\Pack\ZipExtractionException + * @throws \Pterodactyl\Exceptions\Service\Pack\InvalidFileUploadException + * @throws \Pterodactyl\Exceptions\Service\Pack\InvalidFileMimeTypeException + * @throws \Pterodactyl\Exceptions\Service\Pack\UnreadableZipArchiveException + * @throws \Pterodactyl\Exceptions\Service\Pack\InvalidPackArchiveFormatException + */ + public function handle($option, UploadedFile $file) + { + if (! $file->isValid()) { + throw new InvalidFileUploadException(trans('admin/exceptions.packs.invalid_upload')); + } + + if (! in_array($file->getMimeType(), self::VALID_UPLOAD_TYPES)) { + throw new InvalidFileMimeTypeException(trans('admin/exceptions.packs.invalid_mime', [ + 'type' => implode(', ', self::VALID_UPLOAD_TYPES), + ])); + } + + if ($file->getMimeType() === 'application/zip') { + return $this->handleArchive($option, $file); + } else { + $json = json_decode($file->openFile()->fread($file->getSize()), true); + $json['option_id'] = $option; + + return $this->creationService->handle($json); + } + } + + /** + * Process a ZIP file to create a pack and stored archive. + * + * @param int $option + * @param \Illuminate\Http\UploadedFile $file + * @return \Pterodactyl\Models\Pack + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Service\Pack\ZipExtractionException + * @throws \Pterodactyl\Exceptions\Service\Pack\InvalidFileUploadException + * @throws \Pterodactyl\Exceptions\Service\Pack\InvalidFileMimeTypeException + * @throws \Pterodactyl\Exceptions\Service\Pack\UnreadableZipArchiveException + * @throws \Pterodactyl\Exceptions\Service\Pack\InvalidPackArchiveFormatException + */ + protected function handleArchive($option, $file) + { + if (! $this->archive->open($file->getRealPath())) { + throw new UnreadableZipArchiveException(trans('admin/exceptions.packs.unreadable')); + } + + if (! $this->archive->locateName('import.json') || ! $this->archive->locateName('archive.tar.gz')) { + throw new InvalidPackArchiveFormatException(trans('admin/exceptions.packs.invalid_archive_exception')); + } + + $json = json_decode($this->archive->getFromName('import.json'), true); + $json['option_id'] = $option; + + $pack = $this->creationService->handle($json); + if (! $this->archive->extractTo(storage_path('app/packs/' . $pack->uuid), 'archive.tar.gz')) { + // @todo delete the pack that was created. + throw new ZipExtractionException(trans('admin/exceptions.packs.zip_extraction')); + } + + $this->archive->close(); + + return $pack; + } +} diff --git a/config/pterodactyl.php b/config/pterodactyl.php index 6e6d92aa..6e7767db 100644 --- a/config/pterodactyl.php +++ b/config/pterodactyl.php @@ -156,10 +156,6 @@ return [ 'text/x-perl', 'text/x-shellscript', ], - 'pack_types' => [ - 'application/gzip', - 'application/x-gzip', - ], ], /* diff --git a/resources/lang/en/admin/exceptions.php b/resources/lang/en/admin/exceptions.php index bea2c83f..4ff0c4b4 100644 --- a/resources/lang/en/admin/exceptions.php +++ b/resources/lang/en/admin/exceptions.php @@ -45,4 +45,13 @@ return [ 'reserved_name' => 'The environment variable :name is protected and cannot be assigned to a variable.', ], ], + 'packs' => [ + 'delete_has_servers' => 'Cannot delete a pack that is attached to active servers.', + 'update_has_servers' => 'Cannot modify the associated option ID when servers are currently attached to a pack.', + 'invalid_upload' => 'The file provided does not appear to be valid.', + 'invalid_mime' => 'The file provided does not meet the required type :type', + 'unreadable' => 'The archive provided could not be opened by the server.', + 'zip_extraction' => 'An exception was encountered while attempting to extract the archive provided onto the server.', + 'invalid_archive_exception' => 'The pack archive provided appears to be missing a required archive.tar.gz or import.json file in the base directory.', + ], ]; diff --git a/resources/lang/en/admin/pack.php b/resources/lang/en/admin/pack.php new file mode 100644 index 00000000..6e51903a --- /dev/null +++ b/resources/lang/en/admin/pack.php @@ -0,0 +1,29 @@ +. + * + * 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. + */ + +return [ + 'notices' => [ + 'pack_created' => 'A new pack was successfully created on the system and is now available for deployment to servers.', + ], +]; From cdfbc600304216164200659e550700c48d098f64 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 20 Aug 2017 19:23:50 -0500 Subject: [PATCH 57/99] Push pack services and fix for failing tests --- .../Repository/PackRepositoryInterface.php | 20 ++ .../Service/HasActiveServersException.php | 4 +- .../Pack/ZipArchiveCreationException.php | 30 +++ .../Controllers/Admin/NodesController.php | 9 +- .../Controllers/Admin/OptionController.php | 27 +- app/Http/Controllers/Admin/PackController.php | 194 +++++++------- .../Controllers/Admin/ServiceController.php | 13 +- app/Http/Requests/Admin/PackFormRequest.php | 64 +++++ app/Providers/RepositoryServiceProvider.php | 3 + app/Repositories/Eloquent/PackRepository.php | 36 +++ app/Repositories/Old/PackRepository.php | 239 ------------------ app/Services/Nodes/DeletionService.php | 6 +- app/Services/Packs/ExportPackService.php | 112 ++++++++ config/pterodactyl.php | 1 + database/seeds/RustServiceTableSeeder.php | 5 +- database/seeds/SourceServiceTableSeeder.php | 5 +- database/seeds/TerrariaServiceTableSeeder.php | 5 +- database/seeds/VoiceServiceTableSeeder.php | 5 +- resources/lang/en/admin/pack.php | 2 + .../pterodactyl/admin/packs/view.blade.php | 4 +- routes/admin.php | 9 +- 21 files changed, 415 insertions(+), 378 deletions(-) create mode 100644 app/Exceptions/Service/Pack/ZipArchiveCreationException.php create mode 100644 app/Http/Requests/Admin/PackFormRequest.php delete mode 100644 app/Repositories/Old/PackRepository.php create mode 100644 app/Services/Packs/ExportPackService.php diff --git a/app/Contracts/Repository/PackRepositoryInterface.php b/app/Contracts/Repository/PackRepositoryInterface.php index 5d1bb022..5160245c 100644 --- a/app/Contracts/Repository/PackRepositoryInterface.php +++ b/app/Contracts/Repository/PackRepositoryInterface.php @@ -28,12 +28,32 @@ use Pterodactyl\Contracts\Repository\Attributes\SearchableInterface; interface PackRepositoryInterface extends RepositoryInterface, SearchableInterface { + /** + * Return a paginated listing of packs with their associated option and server count. + * + * @param int $paginate + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator + */ + public function paginateWithOptionAndServerCount($paginate = 50); + + /** + * Return a pack with the associated server models attached to it. + * + * @param int $id + * @return \Illuminate\Database\Eloquent\Collection + * + * @throws \Illuminate\Database\Eloquent\ModelNotFoundException + */ + public function getWithServers($id); + /** * Return all of the file archives for a given pack. * * @param int $id * @param bool $collection * @return object|\Illuminate\Support\Collection + * + * @throws \Illuminate\Database\Eloquent\ModelNotFoundException */ public function getFileArchives($id, $collection = false); } diff --git a/app/Exceptions/Service/HasActiveServersException.php b/app/Exceptions/Service/HasActiveServersException.php index 851052d0..09c13c18 100644 --- a/app/Exceptions/Service/HasActiveServersException.php +++ b/app/Exceptions/Service/HasActiveServersException.php @@ -24,7 +24,9 @@ namespace Pterodactyl\Exceptions\Service; -class HasActiveServersException extends \Exception +use Pterodactyl\Exceptions\DisplayException; + +class HasActiveServersException extends DisplayException { // } diff --git a/app/Exceptions/Service/Pack/ZipArchiveCreationException.php b/app/Exceptions/Service/Pack/ZipArchiveCreationException.php new file mode 100644 index 00000000..14eb8ce9 --- /dev/null +++ b/app/Exceptions/Service/Pack/ZipArchiveCreationException.php @@ -0,0 +1,30 @@ +. + * + * 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\Exceptions\Service\Pack; + +class ZipArchiveCreationException extends \Exception +{ + // +} diff --git a/app/Http/Controllers/Admin/NodesController.php b/app/Http/Controllers/Admin/NodesController.php index 4be09917..854ab486 100644 --- a/app/Http/Controllers/Admin/NodesController.php +++ b/app/Http/Controllers/Admin/NodesController.php @@ -174,6 +174,8 @@ class NodesController extends Controller * * @param int $node * @return \Illuminate\View\View + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function viewIndex($node) { @@ -213,6 +215,8 @@ class NodesController extends Controller * * @param int $node * @return \Illuminate\View\View + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function viewAllocation($node) { @@ -227,6 +231,8 @@ class NodesController extends Controller * * @param int $node * @return \Illuminate\View\View + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function viewServers($node) { @@ -299,9 +305,10 @@ class NodesController extends Controller * Sets an alias for a specific allocation on a node. * * @param \Pterodactyl\Http\Requests\Admin\Node\AllocationAliasFormRequest $request - * @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response + * @return \Symfony\Component\HttpFoundation\Response * * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function allocationSetAlias(AllocationAliasFormRequest $request) { diff --git a/app/Http/Controllers/Admin/OptionController.php b/app/Http/Controllers/Admin/OptionController.php index d0789799..04e69e85 100644 --- a/app/Http/Controllers/Admin/OptionController.php +++ b/app/Http/Controllers/Admin/OptionController.php @@ -30,7 +30,6 @@ use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Models\ServiceOption; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Http\Requests\Admin\Service\EditOptionScript; -use Pterodactyl\Exceptions\Service\HasActiveServersException; use Pterodactyl\Services\Services\Options\OptionUpdateService; use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; use Pterodactyl\Services\Services\Options\OptionCreationService; @@ -81,13 +80,13 @@ class OptionController extends Controller /** * OptionController constructor. * - * @param \Prologue\Alerts\AlertsMessageBag $alert - * @param \Pterodactyl\Services\Services\Options\InstallScriptUpdateService $installScriptUpdateService - * @param \Pterodactyl\Services\Services\Options\OptionCreationService $optionCreationService - * @param \Pterodactyl\Services\Services\Options\OptionDeletionService $optionDeletionService - * @param \Pterodactyl\Services\Services\Options\OptionUpdateService $optionUpdateService - * @param \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface $serviceRepository - * @param \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface $serviceOptionRepository + * @param \Prologue\Alerts\AlertsMessageBag $alert + * @param \Pterodactyl\Services\Services\Options\InstallScriptUpdateService $installScriptUpdateService + * @param \Pterodactyl\Services\Services\Options\OptionCreationService $optionCreationService + * @param \Pterodactyl\Services\Services\Options\OptionDeletionService $optionDeletionService + * @param \Pterodactyl\Services\Services\Options\OptionUpdateService $optionUpdateService + * @param \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface $serviceRepository + * @param \Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface $serviceOptionRepository */ public function __construct( AlertsMessageBag $alert, @@ -147,17 +146,13 @@ class OptionController extends Controller * * @param \Pterodactyl\Models\ServiceOption $option * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\Service\HasActiveServersException */ public function destroy(ServiceOption $option) { - try { - $this->optionDeletionService->handle($option->id); - $this->alert->success(trans('admin/services.options.notices.option_deleted'))->flash(); - } catch (HasActiveServersException $exception) { - $this->alert->danger($exception->getMessage())->flash(); - - return redirect()->route('admin.services.option.view', $option->id); - } + $this->optionDeletionService->handle($option->id); + $this->alert->success(trans('admin/services.options.notices.option_deleted'))->flash(); return redirect()->route('admin.services.view', $option->service_id); } diff --git a/app/Http/Controllers/Admin/PackController.php b/app/Http/Controllers/Admin/PackController.php index 9a1b089f..ab87807c 100644 --- a/app/Http/Controllers/Admin/PackController.php +++ b/app/Http/Controllers/Admin/PackController.php @@ -24,22 +24,19 @@ namespace Pterodactyl\Http\Controllers\Admin; -use Log; -use Alert; +use Illuminate\Contracts\Config\Repository as ConfigRepository; use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Contracts\Repository\PackRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; +use Pterodactyl\Http\Requests\Admin\PackFormRequest; +use Pterodactyl\Services\Packs\ExportPackService; use Pterodactyl\Services\Packs\PackCreationService; use Pterodactyl\Services\Packs\PackDeletionService; use Pterodactyl\Services\Packs\PackUpdateService; use Pterodactyl\Services\Packs\TemplateUploadService; -use Storage; use Illuminate\Http\Request; use Pterodactyl\Models\Pack; -use Pterodactyl\Models\Service; -use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Repositories\PackRepository; -use Pterodactyl\Exceptions\DisplayValidationException; class PackController extends Controller { @@ -48,6 +45,11 @@ class PackController extends Controller */ protected $alert; + /** + * @var \Illuminate\Contracts\Config\Repository + */ + protected $config; + /** * @var \Pterodactyl\Services\Packs\PackCreationService */ @@ -58,6 +60,11 @@ class PackController extends Controller */ protected $deletionService; + /** + * @var \Pterodactyl\Services\Packs\ExportPackService + */ + protected $exportService; + /** * @var \Pterodactyl\Contracts\Repository\PackRepositoryInterface */ @@ -66,7 +73,12 @@ class PackController extends Controller /** * @var \Pterodactyl\Services\Packs\PackUpdateService */ - protected $packUpdateService; + protected $updateService; + + /** + * @var \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface + */ + protected $serviceRepository; /** * @var \Pterodactyl\Services\Packs\TemplateUploadService @@ -76,26 +88,35 @@ class PackController extends Controller /** * PackController constructor. * - * @param \Prologue\Alerts\AlertsMessageBag $alert - * @param \Pterodactyl\Services\Packs\PackCreationService $creationService - * @param \Pterodactyl\Services\Packs\PackDeletionService $deletionService - * @param \Pterodactyl\Contracts\Repository\PackRepositoryInterface $repository - * @param \Pterodactyl\Services\Packs\PackUpdateService $packUpdateService - * @param \Pterodactyl\Services\Packs\TemplateUploadService $templateUploadService + * @param \Prologue\Alerts\AlertsMessageBag $alert + * @param \Illuminate\Contracts\Config\Repository $config + * @param \Pterodactyl\Services\Packs\ExportPackService $exportService + * @param \Pterodactyl\Services\Packs\PackCreationService $creationService + * @param \Pterodactyl\Services\Packs\PackDeletionService $deletionService + * @param \Pterodactyl\Contracts\Repository\PackRepositoryInterface $repository + * @param \Pterodactyl\Services\Packs\PackUpdateService $updateService + * @param \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface $serviceRepository + * @param \Pterodactyl\Services\Packs\TemplateUploadService $templateUploadService */ public function __construct( AlertsMessageBag $alert, + ConfigRepository $config, + ExportPackService $exportService, PackCreationService $creationService, PackDeletionService $deletionService, PackRepositoryInterface $repository, - PackUpdateService $packUpdateService, + PackUpdateService $updateService, + ServiceRepositoryInterface $serviceRepository, TemplateUploadService $templateUploadService ) { $this->alert = $alert; + $this->config = $config; $this->creationService = $creationService; $this->deletionService = $deletionService; + $this->exportService = $exportService; $this->repository = $repository; - $this->packUpdateService = $packUpdateService; + $this->updateService = $updateService; + $this->serviceRepository = $serviceRepository; $this->templateUploadService = $templateUploadService; } @@ -107,45 +128,41 @@ class PackController extends Controller */ public function index(Request $request) { - $packs = Pack::with('option')->withCount('servers'); - - if (! is_null($request->input('query'))) { - $packs->search($request->input('query')); - } - - return view('admin.packs.index', ['packs' => $packs->paginate(50)]); + return view('admin.packs.index', [ + 'packs' => $this->repository->search($request->input('query'))->paginateWithOptionAndServerCount( + $this->config->get('pterodactyl.paginate.admin.packs') + ), + ]); } /** * Display new pack creation form. * - * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ - public function create(Request $request) + public function create() { return view('admin.packs.new', [ - 'services' => Service::with('options')->get(), + 'services' => $this->serviceRepository->getWithOptions(), ]); } /** * Display new pack creation modal for use with template upload. * - * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ - public function newTemplate(Request $request) + public function newTemplate() { return view('admin.packs.modal', [ - 'services' => Service::with('options')->get(), + 'services' => $this->serviceRepository->getWithOptions(), ]); } /** * Handle create pack request and route user to location. * - * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Http\Requests\Admin\PackFormRequest $request * @return \Illuminate\View\View * * @throws \Pterodactyl\Exceptions\Model\DataValidationException @@ -155,12 +172,12 @@ class PackController extends Controller * @throws \Pterodactyl\Exceptions\Service\Pack\UnreadableZipArchiveException * @throws \Pterodactyl\Exceptions\Service\Pack\ZipExtractionException */ - public function store(Request $request) + public function store(PackFormRequest $request) { if ($request->has('from_template')) { - $pack = $this->templateUploadService->handle($request->input('option_id'), $request->input('file_upload')); + $pack = $this->templateUploadService->handle($request->input('option_id'), $request->file('file_upload')); } else { - $pack = $this->creationService->handle($request->normalize(), $request->input('file_upload')); + $pack = $this->creationService->handle($request->normalize(), $request->file('file_upload')); } $this->alert->success(trans('admin/pack.notices.pack_created'))->flash(); @@ -171,98 +188,75 @@ class PackController extends Controller /** * Display pack view template to user. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param int $pack * @return \Illuminate\View\View */ - public function view(Request $request, $id) + public function view($pack) { return view('admin.packs.view', [ - 'pack' => Pack::with('servers.node', 'servers.user')->findOrFail($id), - 'services' => Service::with('options')->get(), + 'pack' => $this->repository->getWithServers($pack), + 'services' => $this->serviceRepository->getWithOptions(), ]); } /** * Handle updating or deleting pack information. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Pterodactyl\Http\Requests\Admin\PackFormRequest $request + * @param \Pterodactyl\Models\Pack $pack * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Pterodactyl\Exceptions\Service\HasActiveServersException */ - public function update(Request $request, $id) + public function update(PackFormRequest $request, Pack $pack) { - $repo = new PackRepository; + $this->updateService->handle($pack, $request->normalize()); + $this->alert->success(trans('admin/pack.notices.pack_updated'))->flash(); - try { - if ($request->input('action') !== 'delete') { - $pack = $repo->update($id, $request->intersect([ - 'name', 'description', 'version', - 'option_id', 'selectable', 'visible', 'locked', - ])); - Alert::success('Pack successfully updated.')->flash(); - } else { - $repo->delete($id); - Alert::success('Pack was successfully deleted from the system.')->flash(); + return redirect()->route('admin.packs.view', $pack->id); + } - return redirect()->route('admin.packs'); - } - } catch (DisplayValidationException $ex) { - return redirect()->route('admin.packs.view', $id)->withErrors(json_decode($ex->getMessage())); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An error occured while attempting to edit this service pack. This error has been logged.')->flash(); - } + /** + * Delete a pack if no servers are attached to it currently. + * + * @param \Pterodactyl\Models\Pack $pack + * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Pterodactyl\Exceptions\Service\HasActiveServersException + */ + public function destroy(Pack $pack) + { + $this->deletionService->handle($pack->id); + $this->alert->success(trans('admin/pack.notices.pack_deleted', [ + 'name' => $pack->name, + ]))->flash(); - return redirect()->route('admin.packs.view', $id); + return redirect()->route('admin.packs'); } /** * Creates an archive of the pack and downloads it to the browser. * - * @param \Illuminate\Http\Request $request - * @param int $id - * @param bool $files - * @return \Symfony\Component\HttpFoundation\BinaryFileResponse + * @param \Pterodactyl\Models\Pack $pack + * @param bool|string $files + * @return \Symfony\Component\HttpFoundation\Response + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Pterodactyl\Exceptions\Service\Pack\ZipArchiveCreationException */ - public function export(Request $request, $id, $files = false) + public function export(Pack $pack, $files = false) { - $pack = Pack::findOrFail($id); - $json = [ - 'name' => $pack->name, - 'version' => $pack->version, - 'description' => $pack->description, - 'selectable' => $pack->selectable, - 'visible' => $pack->visible, - 'locked' => $pack->locked, - ]; - - $filename = tempnam(sys_get_temp_dir(), 'pterodactyl_'); - if ($files === 'with-files') { - $zip = new \ZipArchive; - if (! $zip->open($filename, \ZipArchive::CREATE)) { - abort(503, 'Unable to open file for writing.'); - } - - $files = Storage::files('packs/' . $pack->uuid); - foreach ($files as $file) { - $zip->addFile(storage_path('app/' . $file), basename(storage_path('app/' . $file))); - } - - $zip->addFromString('import.json', json_encode($json, JSON_PRETTY_PRINT)); - $zip->close(); + $filename = $this->exportService->handle($pack, is_string($files)); + if (is_string($files)) { return response()->download($filename, 'pack-' . $pack->name . '.zip')->deleteFileAfterSend(true); - } else { - $fp = fopen($filename, 'a+'); - fwrite($fp, json_encode($json, JSON_PRETTY_PRINT)); - fclose($fp); - - return response()->download($filename, 'pack-' . $pack->name . '.json', [ - 'Content-Type' => 'application/json', - ])->deleteFileAfterSend(true); } + + return response()->download($filename, 'pack-' . $pack->name . '.json', [ + 'Content-Type' => 'application/json', + ])->deleteFileAfterSend(true); } } diff --git a/app/Http/Controllers/Admin/ServiceController.php b/app/Http/Controllers/Admin/ServiceController.php index b2341bf5..26f6c0ce 100644 --- a/app/Http/Controllers/Admin/ServiceController.php +++ b/app/Http/Controllers/Admin/ServiceController.php @@ -30,7 +30,6 @@ use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Services\Services\ServiceUpdateService; use Pterodactyl\Services\Services\ServiceCreationService; use Pterodactyl\Services\Services\ServiceDeletionService; -use Pterodactyl\Exceptions\Service\HasActiveServersException; use Pterodactyl\Http\Requests\Admin\Service\ServiceFormRequest; use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; use Pterodactyl\Http\Requests\Admin\Service\ServiceFunctionsFormRequest; @@ -179,17 +178,13 @@ class ServiceController extends Controller * * @param \Pterodactyl\Models\Service $service * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\Service\HasActiveServersException */ public function destroy(Service $service) { - try { - $this->deletionService->handle($service->id); - $this->alert->success(trans('admin/services.notices.service_deleted'))->flash(); - } catch (HasActiveServersException $exception) { - $this->alert->danger($exception->getMessage())->flash(); - - return redirect()->back(); - } + $this->deletionService->handle($service->id); + $this->alert->success(trans('admin/services.notices.service_deleted'))->flash(); return redirect()->route('admin.services'); } diff --git a/app/Http/Requests/Admin/PackFormRequest.php b/app/Http/Requests/Admin/PackFormRequest.php new file mode 100644 index 00000000..75fd9063 --- /dev/null +++ b/app/Http/Requests/Admin/PackFormRequest.php @@ -0,0 +1,64 @@ +. + * + * 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\Http\Requests\Admin; + +use Pterodactyl\Models\Pack; +use Pterodactyl\Services\Packs\PackCreationService; + +class PackFormRequest extends AdminFormRequest +{ + /** + * @return array + */ + public function rules() + { + if ($this->method() === 'PATCH') { + return Pack::getUpdateRulesForId($this->route()->parameter('pack')->id); + } + + return Pack::getCreateRules(); + } + + /** + * Run validation after the rules above have been applied. + * + * @param \Illuminate\Validation\Validator $validator + */ + public function withValidator($validator) + { + if ($this->method() !== 'POST') { + return; + } + + $validator->after(function ($validator) { + $mimetypes = implode(',', PackCreationService::VALID_UPLOAD_TYPES); + + /* @var $validator \Illuminate\Validation\Validator */ + $validator->sometimes('file_upload', 'sometimes|required|file|mimetypes:' . $mimetypes, function () { + return true; + }); + }); + } +} diff --git a/app/Providers/RepositoryServiceProvider.php b/app/Providers/RepositoryServiceProvider.php index 0dd26a4b..b1be571e 100644 --- a/app/Providers/RepositoryServiceProvider.php +++ b/app/Providers/RepositoryServiceProvider.php @@ -25,7 +25,9 @@ namespace Pterodactyl\Providers; use Illuminate\Support\ServiceProvider; +use Pterodactyl\Contracts\Repository\PackRepositoryInterface; use Pterodactyl\Repositories\Eloquent\NodeRepository; +use Pterodactyl\Repositories\Eloquent\PackRepository; use Pterodactyl\Repositories\Eloquent\UserRepository; use Pterodactyl\Repositories\Eloquent\ApiKeyRepository; use Pterodactyl\Repositories\Eloquent\ServerRepository; @@ -74,6 +76,7 @@ class RepositoryServiceProvider extends ServiceProvider $this->app->bind(LocationRepositoryInterface::class, LocationRepository::class); $this->app->bind(NodeRepositoryInterface::class, NodeRepository::class); $this->app->bind(OptionVariableRepositoryInterface::class, OptionVariableRepository::class); + $this->app->bind(PackRepositoryInterface::class, PackRepository::class); $this->app->bind(ServerRepositoryInterface::class, ServerRepository::class); $this->app->bind(ServerVariableRepositoryInterface::class, ServerVariableRepository::class); $this->app->bind(ServiceRepositoryInterface::class, ServiceRepository::class); diff --git a/app/Repositories/Eloquent/PackRepository.php b/app/Repositories/Eloquent/PackRepository.php index 38a82471..5f1641f7 100644 --- a/app/Repositories/Eloquent/PackRepository.php +++ b/app/Repositories/Eloquent/PackRepository.php @@ -24,10 +24,12 @@ namespace Pterodactyl\Repositories\Eloquent; +use Illuminate\Database\Eloquent\ModelNotFoundException; use Pterodactyl\Models\Pack; use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory; use Pterodactyl\Repositories\Concerns\Searchable; use Pterodactyl\Contracts\Repository\PackRepositoryInterface; +use Webmozart\Assert\Assert; class PackRepository extends EloquentRepository implements PackRepositoryInterface { @@ -46,7 +48,14 @@ class PackRepository extends EloquentRepository implements PackRepositoryInterfa */ public function getFileArchives($id, $collection = false) { + Assert::numeric($id, 'First argument passed to getFileArchives must be numeric, received %s.'); + Assert::boolean($collection, 'Second argument passed to getFileArchives must be boolean, received %s.'); + $pack = $this->getBuilder()->find($id, ['id', 'uuid']); + if (! $pack) { + throw new ModelNotFoundException; + } + $storage = $this->app->make(FilesystemFactory::class); $files = collect($storage->disk('default')->files('packs/' . $pack->uuid)); @@ -62,4 +71,31 @@ class PackRepository extends EloquentRepository implements PackRepositoryInterfa return ($collection) ? $files : (object) $files->all(); } + + /** + * {@inheritdoc} + */ + public function getWithServers($id) + { + Assert::numeric($id, 'First argument passed to getWithServers must be numeric, received %s.'); + + $instance = $this->getBuilder()->with('servers.node', 'servers.user')->find($id, $this->getColumns()); + if (! $instance) { + throw new ModelNotFoundException; + } + + return $instance; + } + + /** + * {@inheritdoc} + */ + public function paginateWithOptionAndServerCount($paginate = 50) + { + Assert::integer($paginate, 'First argument passed to paginateWithOptionAndServerCount must be integer, received %s.'); + + return $this->getBuilder()->with('option')->withCount('servers') + ->search($this->searchTerm) + ->paginate($paginate, $this->getColumns()); + } } diff --git a/app/Repositories/Old/PackRepository.php b/app/Repositories/Old/PackRepository.php deleted file mode 100644 index 0a885446..00000000 --- a/app/Repositories/Old/PackRepository.php +++ /dev/null @@ -1,239 +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\Repositories; - -use DB; -use Uuid; -use Storage; -use Validator; -use Pterodactyl\Models\Pack; -use Pterodactyl\Services\UuidService; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Exceptions\DisplayValidationException; - -class PackRepository -{ - /** - * Creates a new pack on the system. - * - * @param array $data - * @return \Pterodactyl\Models\Pack - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function create(array $data) - { - $validator = Validator::make($data, [ - 'name' => 'required|string', - 'version' => 'required|string', - 'description' => 'sometimes|nullable|string', - 'selectable' => 'sometimes|required|boolean', - 'visible' => 'sometimes|required|boolean', - 'locked' => 'sometimes|required|boolean', - 'option_id' => 'required|exists:service_options,id', - ]); - - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - if (isset($data['file_upload'])) { - if (! $data['file_upload']->isValid()) { - throw new DisplayException('The file provided does not appear to be valid.'); - } - - if (! in_array($data['file_upload']->getMimeType(), ['application/gzip', 'application/x-gzip'])) { - throw new DisplayException('The file provided (' . $data['file_upload']->getMimeType() . ') does not meet the required filetype of application/gzip.'); - } - } - - return DB::transaction(function () use ($data) { - $uuid = new UuidService(); - - $pack = new Pack; - $pack->uuid = $uuid->generate('packs', 'uuid'); - $pack->fill([ - 'option_id' => $data['option_id'], - 'name' => $data['name'], - 'version' => $data['version'], - 'description' => (empty($data['description'])) ? null : $data['description'], - 'selectable' => isset($data['selectable']), - 'visible' => isset($data['visible']), - 'locked' => isset($data['locked']), - ])->save(); - - if (! $pack->exists) { - throw new DisplayException('Model does not exist after creation. Did an event prevent it from saving?'); - } - - Storage::makeDirectory('packs/' . $pack->uuid); - if (isset($data['file_upload'])) { - $data['file_upload']->storeAs('packs/' . $pack->uuid, 'archive.tar.gz'); - } - - return $pack; - }); - } - - /** - * Creates a new pack on the system given a template file. - * - * @param array $data - * @return \Pterodactyl\Models\Pack - * - * @throws \Pterodactyl\Exceptions\DisplayException - */ - public function createWithTemplate(array $data) - { - if (! isset($data['file_upload'])) { - throw new DisplayException('No template file was found submitted with this request.'); - } - - if (! $data['file_upload']->isValid()) { - throw new DisplayException('The file provided does not appear to be valid.'); - } - - if (! in_array($data['file_upload']->getMimeType(), [ - 'application/zip', - 'text/plain', - 'application/json', - ])) { - throw new DisplayException('The file provided (' . $data['file_upload']->getMimeType() . ') does not meet the required filetypes of application/zip or application/json.'); - } - - if ($data['file_upload']->getMimeType() === 'application/zip') { - $zip = new \ZipArchive; - if (! $zip->open($data['file_upload']->path())) { - throw new DisplayException('The uploaded archive was unable to be opened.'); - } - - $isTar = $zip->locateName('archive.tar.gz'); - - if (! $zip->locateName('import.json') || ! $isTar) { - throw new DisplayException('This contents of the provided archive were in an invalid format.'); - } - - $json = json_decode($zip->getFromName('import.json')); - $pack = $this->create([ - 'name' => $json->name, - 'version' => $json->version, - 'description' => $json->description, - 'option_id' => $data['option_id'], - 'selectable' => $json->selectable, - 'visible' => $json->visible, - 'locked' => $json->locked, - ]); - - if (! $zip->extractTo(storage_path('app/packs/' . $pack->uuid), 'archive.tar.gz')) { - $pack->delete(); - throw new DisplayException('Unable to extract the archive file to the correct location.'); - } - - $zip->close(); - - return $pack; - } else { - $json = json_decode(file_get_contents($data['file_upload']->path())); - - return $this->create([ - 'name' => $json->name, - 'version' => $json->version, - 'description' => $json->description, - 'option_id' => $data['option_id'], - 'selectable' => $json->selectable, - 'visible' => $json->visible, - 'locked' => $json->locked, - ]); - } - } - - /** - * Updates a pack on the system. - * - * @param int $id - * @param array $data - * @return \Pterodactyl\Models\Pack - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function update($id, array $data) - { - $validator = Validator::make($data, [ - 'name' => 'sometimes|required|string', - 'option_id' => 'sometimes|required|exists:service_options,id', - 'version' => 'sometimes|required|string', - 'description' => 'sometimes|string', - 'selectable' => 'sometimes|required|boolean', - 'visible' => 'sometimes|required|boolean', - 'locked' => 'sometimes|required|boolean', - ]); - - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - $pack = Pack::withCount('servers')->findOrFail($id); - - if ($pack->servers_count > 0 && (isset($data['option_id']) && (int) $data['option_id'] !== $pack->option_id)) { - throw new DisplayException('You cannot modify the associated option if servers are attached to a pack.'); - } - - $pack->fill([ - 'name' => isset($data['name']) ? $data['name'] : $pack->name, - 'option_id' => isset($data['option_id']) ? $data['option_id'] : $pack->option_id, - 'version' => isset($data['version']) ? $data['version'] : $pack->version, - 'description' => (empty($data['description'])) ? null : $data['description'], - 'selectable' => isset($data['selectable']), - 'visible' => isset($data['visible']), - 'locked' => isset($data['locked']), - ])->save(); - - return $pack; - } - - /** - * Deletes a pack and files from the system. - * - * @param int $id - * @return void - * - * @throws \Pterodactyl\Exceptions\DisplayException - */ - public function delete($id) - { - $pack = Pack::withCount('servers')->findOrFail($id); - - if ($pack->servers_count > 0) { - throw new DisplayException('Cannot delete a pack from the system if servers are assocaited with it.'); - } - - DB::transaction(function () use ($pack) { - $pack->delete(); - Storage::deleteDirectory('packs/' . $pack->uuid); - }); - } -} diff --git a/app/Services/Nodes/DeletionService.php b/app/Services/Nodes/DeletionService.php index 519aed42..6cbab264 100644 --- a/app/Services/Nodes/DeletionService.php +++ b/app/Services/Nodes/DeletionService.php @@ -24,8 +24,8 @@ namespace Pterodactyl\Services\Nodes; +use Pterodactyl\Exceptions\Service\HasActiveServersException; use Pterodactyl\Models\Node; -use Pterodactyl\Exceptions\DisplayException; use Illuminate\Contracts\Translation\Translator; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; @@ -70,7 +70,7 @@ class DeletionService * @param int|\Pterodactyl\Models\Node $node * @return bool|null * - * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Service\HasActiveServersException */ public function handle($node) { @@ -80,7 +80,7 @@ class DeletionService $servers = $this->serverRepository->withColumns('id')->findCountWhere([['node_id', '=', $node]]); if ($servers > 0) { - throw new DisplayException($this->translator->trans('admin/exceptions.node.servers_attached')); + throw new HasActiveServersException($this->translator->trans('admin/exceptions.node.servers_attached')); } return $this->repository->delete($node); diff --git a/app/Services/Packs/ExportPackService.php b/app/Services/Packs/ExportPackService.php new file mode 100644 index 00000000..1432efb9 --- /dev/null +++ b/app/Services/Packs/ExportPackService.php @@ -0,0 +1,112 @@ +. + * + * 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\Packs; + +use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory; +use Pterodactyl\Contracts\Repository\PackRepositoryInterface; +use Pterodactyl\Exceptions\Service\Pack\ZipArchiveCreationException; +use Pterodactyl\Models\Pack; +use ZipArchive; + +class ExportPackService +{ + /** + * @var \ZipArchive + */ + protected $archive; + + /** + * @var \Pterodactyl\Contracts\Repository\PackRepositoryInterface + */ + protected $repository; + + /** + * @var \Illuminate\Contracts\Filesystem\Factory + */ + protected $storage; + + /** + * ExportPackService constructor. + * + * @param \Illuminate\Contracts\Filesystem\Factory $storage + * @param \Pterodactyl\Contracts\Repository\PackRepositoryInterface $repository + * @param \ZipArchive $archive + */ + public function __construct( + FilesystemFactory $storage, + PackRepositoryInterface $repository, + ZipArchive $archive + ) { + $this->archive = $archive; + $this->repository = $repository; + $this->storage = $storage; + } + + /** + * Prepare a pack for export. + * + * @param int|\Pterodactyl\Models\Pack $pack + * @param bool $files + * @return string + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Pterodactyl\Exceptions\Service\Pack\ZipArchiveCreationException + */ + public function handle($pack, $files = false) + { + if (! $pack instanceof Pack) { + $pack = $this->repository->find($pack); + } + + $json = [ + 'name' => $pack->name, + 'version' => $pack->version, + 'description' => $pack->description, + 'selectable' => $pack->selectable, + 'visible' => $pack->visible, + 'locked' => $pack->locked, + ]; + + $filename = tempnam(sys_get_temp_dir(), 'pterodactyl_'); + if ($files) { + if (! $this->archive->open($filename, $this->archive::CREATE)) { + throw new ZipArchiveCreationException; + } + + foreach ($this->storage->disk()->files('packs/' . $pack->uuid) as $file) { + $this->archive->addFile(storage_path('app/' . $file), basename(storage_path('app/' . $file))); + } + + $this->archive->addFromString('import.json', json_encode($json, JSON_PRETTY_PRINT)); + $this->archive->close(); + } else { + $fp = fopen($filename, 'a+'); + fwrite($fp, json_encode($json, JSON_PRETTY_PRINT)); + fclose($fp); + } + + return $filename; + } +} diff --git a/config/pterodactyl.php b/config/pterodactyl.php index 6e7767db..ba604d1b 100644 --- a/config/pterodactyl.php +++ b/config/pterodactyl.php @@ -42,6 +42,7 @@ return [ 'admin' => [ 'servers' => env('APP_PAGINATE_ADMIN_SERVERS', 25), 'users' => env('APP_PAGINATE_ADMIN_USERS', 25), + 'packs' => env('APP_PAGINATE_ADMIN_PACKS', 50), ], 'api' => [ 'nodes' => env('APP_PAGINATE_API_NODES', 25), diff --git a/database/seeds/RustServiceTableSeeder.php b/database/seeds/RustServiceTableSeeder.php index 681d9717..e6688ef8 100644 --- a/database/seeds/RustServiceTableSeeder.php +++ b/database/seeds/RustServiceTableSeeder.php @@ -25,9 +25,12 @@ use Illuminate\Database\Seeder; use Pterodactyl\Models\Service; use Pterodactyl\Models\ServiceOption; use Pterodactyl\Models\ServiceVariable; +use Pterodactyl\Traits\Services\CreatesServiceIndex; class RustServiceTableSeeder extends Seeder { + use CreatesServiceIndex; + /** * The core service ID. * @@ -63,7 +66,7 @@ class RustServiceTableSeeder extends Seeder 'name' => 'Rust', 'description' => 'The only aim in Rust is to survive. To do this you will need to overcome struggles such as hunger, thirst and cold. Build a fire. Build a shelter. Kill animals for meat. Protect yourself from other players, and kill them for meat. Create alliances with other players and form a town. Do whatever it takes to survive.', 'startup' => './RustDedicated -batchmode +server.port {{SERVER_PORT}} +server.identity "rust" +rcon.port {{RCON_PORT}} +rcon.web true +server.hostname \"{{HOSTNAME}}\" +server.level \"{{LEVEL}}\" +server.description \"{{DESCRIPTION}}\" +server.url \"{{URL}}\" +server.headerimage \"{{SERVER_IMG}}\" +server.worldsize \"{{WORLD_SIZE}}\" +server.seed \"{{SEED}}\" +server.maxplayers {{MAX_PLAYERS}} +rcon.password \"{{RCON_PASS}}\" {{ADDITIONAL_ARGS}}', - 'index_file' => Service::defaultIndexFile(), + 'index_file' => $this->getIndexScript(), ]); } diff --git a/database/seeds/SourceServiceTableSeeder.php b/database/seeds/SourceServiceTableSeeder.php index f41d1a87..a20ce255 100644 --- a/database/seeds/SourceServiceTableSeeder.php +++ b/database/seeds/SourceServiceTableSeeder.php @@ -25,9 +25,12 @@ use Illuminate\Database\Seeder; use Pterodactyl\Models\Service; use Pterodactyl\Models\ServiceOption; use Pterodactyl\Models\ServiceVariable; +use Pterodactyl\Traits\Services\CreatesServiceIndex; class SourceServiceTableSeeder extends Seeder { + use CreatesServiceIndex; + /** * The core service ID. * @@ -63,7 +66,7 @@ class SourceServiceTableSeeder extends Seeder 'name' => 'Source Engine', 'description' => 'Includes support for most Source Dedicated Server games.', 'startup' => './srcds_run -game {{SRCDS_GAME}} -console -port {{SERVER_PORT}} +ip 0.0.0.0 -strictportbind -norestart', - 'index_file' => Service::defaultIndexFile(), + 'index_file' => $this->getIndexScript(), ]); } diff --git a/database/seeds/TerrariaServiceTableSeeder.php b/database/seeds/TerrariaServiceTableSeeder.php index 6d451f12..72afdd86 100644 --- a/database/seeds/TerrariaServiceTableSeeder.php +++ b/database/seeds/TerrariaServiceTableSeeder.php @@ -25,9 +25,12 @@ use Illuminate\Database\Seeder; use Pterodactyl\Models\Service; use Pterodactyl\Models\ServiceOption; use Pterodactyl\Models\ServiceVariable; +use Pterodactyl\Traits\Services\CreatesServiceIndex; class TerrariaServiceTableSeeder extends Seeder { + use CreatesServiceIndex; + /** * The core service ID. * @@ -63,7 +66,7 @@ class TerrariaServiceTableSeeder extends Seeder 'name' => 'Terraria', 'description' => 'Terraria is a land of adventure! A land of mystery! A land that\'s yours to shape, defend, and enjoy. Your options in Terraria are limitless. Are you an action gamer with an itchy trigger finger? A master builder? A collector? An explorer? There\'s something for everyone.', 'startup' => 'mono TerrariaServer.exe -port {{SERVER_PORT}} -autocreate 2 -worldname World', - 'index_file' => Service::defaultIndexFile(), + 'index_file' => $this->getIndexScript(), ]); } diff --git a/database/seeds/VoiceServiceTableSeeder.php b/database/seeds/VoiceServiceTableSeeder.php index 1b3a0554..cd0ba033 100644 --- a/database/seeds/VoiceServiceTableSeeder.php +++ b/database/seeds/VoiceServiceTableSeeder.php @@ -25,9 +25,12 @@ use Illuminate\Database\Seeder; use Pterodactyl\Models\Service; use Pterodactyl\Models\ServiceOption; use Pterodactyl\Models\ServiceVariable; +use Pterodactyl\Traits\Services\CreatesServiceIndex; class VoiceServiceTableSeeder extends Seeder { + use CreatesServiceIndex; + /** * The core service ID. * @@ -63,7 +66,7 @@ class VoiceServiceTableSeeder extends Seeder 'name' => 'Voice Servers', 'description' => 'Voice servers such as Mumble and Teamspeak 3.', 'startup' => '', - 'index_file' => Service::defaultIndexFile(), + 'index_file' => $this->getIndexScript(), ]); } diff --git a/resources/lang/en/admin/pack.php b/resources/lang/en/admin/pack.php index 6e51903a..d41cfd4d 100644 --- a/resources/lang/en/admin/pack.php +++ b/resources/lang/en/admin/pack.php @@ -24,6 +24,8 @@ return [ 'notices' => [ + 'pack_updated' => 'Pack has been successfully updated.', + 'pack_deleted' => 'Successfully deleted the pack ":name" from the system.', 'pack_created' => 'A new pack was successfully created on the system and is now available for deployment to servers.', ], ]; diff --git a/resources/themes/pterodactyl/admin/packs/view.blade.php b/resources/themes/pterodactyl/admin/packs/view.blade.php index 03b331be..44f65686 100644 --- a/resources/themes/pterodactyl/admin/packs/view.blade.php +++ b/resources/themes/pterodactyl/admin/packs/view.blade.php @@ -107,8 +107,8 @@
  • diff --git a/routes/admin.php b/routes/admin.php index 664ccf52..26980254 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -200,9 +200,12 @@ Route::group(['prefix' => 'packs'], function () { Route::get('/', 'PackController@index')->name('admin.packs'); Route::get('/new', 'PackController@create')->name('admin.packs.new'); Route::get('/new/template', 'PackController@newTemplate')->name('admin.packs.new.template'); - Route::get('/view/{id}', 'PackController@view')->name('admin.packs.view'); + Route::get('/view/{pack}', 'PackController@view')->name('admin.packs.view'); Route::post('/new', 'PackController@store'); - Route::post('/view/{id}', 'PackController@update'); - Route::post('/view/{id}/export/{files?}', 'PackController@export')->name('admin.packs.view.export'); + Route::post('/view/{pack}/export/{files?}', 'PackController@export')->name('admin.packs.view.export'); + + Route::patch('/view/{pack}', 'PackController@update'); + + Route::delete('/view/{pack}', 'PackController@destroy'); }); From 47eec0398df432e025147d50551ffe07c00bec24 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 20 Aug 2017 19:51:15 -0500 Subject: [PATCH 58/99] Fix broken tests due to grapping around... --- tests/Unit/Services/Allocations/AssignmentServiceTest.php | 4 ++-- tests/Unit/Services/Api/KeyServiceTest.php | 2 +- .../Unit/Services/Database/DatabaseManagementServiceTest.php | 2 +- tests/Unit/Services/Nodes/CreationServiceTest.php | 2 +- tests/Unit/Services/Nodes/UpdateServiceTest.php | 4 ++-- tests/Unit/Services/Servers/CreationServiceTest.php | 2 +- .../Unit/Services/Servers/DetailsModificationServiceTest.php | 2 +- tests/Unit/Services/Servers/UsernameGenerationServiceTest.php | 4 ++-- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/Unit/Services/Allocations/AssignmentServiceTest.php b/tests/Unit/Services/Allocations/AssignmentServiceTest.php index 1d479938..f55fdc5f 100644 --- a/tests/Unit/Services/Allocations/AssignmentServiceTest.php +++ b/tests/Unit/Services/Allocations/AssignmentServiceTest.php @@ -71,7 +71,7 @@ class AssignmentServiceTest extends TestCase // // This can also be avoided if tests were run in isolated processes, or if that test // came first, but neither of those are good solutions, so this is the next best option. - PHPMock::defineFunctionMock('\\Pterodactyl\\Service\\Allocations', 'gethostbyname'); + PHPMock::defineFunctionMock('\\Pterodactyl\\Services\\Allocations', 'gethostbyname'); $this->node = factory(Node::class)->make(); $this->connection = m::mock(ConnectionInterface::class); @@ -180,7 +180,7 @@ class AssignmentServiceTest extends TestCase 'allocation_ports' => ['1024'], ]; - $this->getFunctionMock('\\Pterodactyl\\Service\\Allocations', 'gethostbyname') + $this->getFunctionMock('\\Pterodactyl\\Services\\Allocations', 'gethostbyname') ->expects($this->once())->willReturn('192.168.1.1'); $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); diff --git a/tests/Unit/Services/Api/KeyServiceTest.php b/tests/Unit/Services/Api/KeyServiceTest.php index 0ac71382..33d01cb0 100644 --- a/tests/Unit/Services/Api/KeyServiceTest.php +++ b/tests/Unit/Services/Api/KeyServiceTest.php @@ -81,7 +81,7 @@ class KeyServiceTest extends TestCase */ public function test_create_function() { - $this->getFunctionMock('\\Pterodactyl\\Service\\Api', 'random_bytes') + $this->getFunctionMock('\\Pterodactyl\\Services\\Api', 'random_bytes') ->expects($this->exactly(2)) ->willReturnCallback(function ($bytes) { return hex2bin(str_pad('', $bytes * 2, '0')); diff --git a/tests/Unit/Services/Database/DatabaseManagementServiceTest.php b/tests/Unit/Services/Database/DatabaseManagementServiceTest.php index 2f3f6ace..ca1e29ce 100644 --- a/tests/Unit/Services/Database/DatabaseManagementServiceTest.php +++ b/tests/Unit/Services/Database/DatabaseManagementServiceTest.php @@ -84,7 +84,7 @@ class DatabaseManagementServiceTest extends TestCase $this->encrypter = m::mock(Encrypter::class); $this->repository = m::mock(DatabaseRepositoryInterface::class); - $this->getFunctionMock('\\Pterodactyl\\Service\\Database', 'str_random') + $this->getFunctionMock('\\Pterodactyl\\Services\\Database', 'str_random') ->expects($this->any())->willReturn('str_random'); $this->service = new DatabaseManagementService( diff --git a/tests/Unit/Services/Nodes/CreationServiceTest.php b/tests/Unit/Services/Nodes/CreationServiceTest.php index f3562cc1..84efcbde 100644 --- a/tests/Unit/Services/Nodes/CreationServiceTest.php +++ b/tests/Unit/Services/Nodes/CreationServiceTest.php @@ -61,7 +61,7 @@ class CreationServiceTest extends TestCase */ public function testNodeIsCreatedAndDaemonSecretIsGenerated() { - $this->getFunctionMock('\\Pterodactyl\\Service\\Nodes', 'bin2hex') + $this->getFunctionMock('\\Pterodactyl\\Services\\Nodes', 'bin2hex') ->expects($this->once())->willReturn('hexResult'); $this->repository->shouldReceive('create')->with([ diff --git a/tests/Unit/Services/Nodes/UpdateServiceTest.php b/tests/Unit/Services/Nodes/UpdateServiceTest.php index bb660985..74db802b 100644 --- a/tests/Unit/Services/Nodes/UpdateServiceTest.php +++ b/tests/Unit/Services/Nodes/UpdateServiceTest.php @@ -97,14 +97,14 @@ class UpdateServiceTest extends TestCase */ public function testNodeIsUpdatedAndDaemonSecretIsReset() { - $this->getFunctionMock('\\Pterodactyl\\Service\\Nodes', 'random_bytes') + $this->getFunctionMock('\\Pterodactyl\\Services\\Nodes', 'random_bytes') ->expects($this->once())->willReturnCallback(function ($bytes) { $this->assertEquals(CreationService::DAEMON_SECRET_LENGTH, $bytes); return '\00'; }); - $this->getFunctionMock('\\Pterodactyl\\Service\\Nodes', 'bin2hex') + $this->getFunctionMock('\\Pterodactyl\\Services\\Nodes', 'bin2hex') ->expects($this->once())->willReturn('hexResponse'); $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() diff --git a/tests/Unit/Services/Servers/CreationServiceTest.php b/tests/Unit/Services/Servers/CreationServiceTest.php index 4cdaea1a..d9a5c245 100644 --- a/tests/Unit/Services/Servers/CreationServiceTest.php +++ b/tests/Unit/Services/Servers/CreationServiceTest.php @@ -155,7 +155,7 @@ class CreationServiceTest extends TestCase $this->uuid = m::mock('overload:Ramsey\Uuid\Uuid'); $this->writer = m::mock(Writer::class); - $this->getFunctionMock('\\Pterodactyl\\Service\\Servers', 'bin2hex') + $this->getFunctionMock('\\Pterodactyl\\Services\\Servers', 'bin2hex') ->expects($this->any())->willReturn('randomstring'); $this->getFunctionMock('\\Ramsey\\Uuid\\Uuid', 'uuid4') diff --git a/tests/Unit/Services/Servers/DetailsModificationServiceTest.php b/tests/Unit/Services/Servers/DetailsModificationServiceTest.php index 916a313c..a617fbaa 100644 --- a/tests/Unit/Services/Servers/DetailsModificationServiceTest.php +++ b/tests/Unit/Services/Servers/DetailsModificationServiceTest.php @@ -84,7 +84,7 @@ class DetailsModificationServiceTest extends TestCase $this->repository = m::mock(ServerRepository::class); $this->writer = m::mock(Writer::class); - $this->getFunctionMock('\\Pterodactyl\\Service\\Servers', 'bin2hex') + $this->getFunctionMock('\\Pterodactyl\\Services\\Servers', 'bin2hex') ->expects($this->any())->willReturn('randomString'); $this->service = new DetailsModificationService( diff --git a/tests/Unit/Services/Servers/UsernameGenerationServiceTest.php b/tests/Unit/Services/Servers/UsernameGenerationServiceTest.php index b3e3f788..c0d80cd5 100644 --- a/tests/Unit/Services/Servers/UsernameGenerationServiceTest.php +++ b/tests/Unit/Services/Servers/UsernameGenerationServiceTest.php @@ -46,10 +46,10 @@ class UsernameGenerationServiceTest extends TestCase $this->service = new UsernameGenerationService(); - $this->getFunctionMock('\\Pterodactyl\\Service\\Servers', 'bin2hex') + $this->getFunctionMock('\\Pterodactyl\\Services\\Servers', 'bin2hex') ->expects($this->any())->willReturn('dddddddd'); - $this->getFunctionMock('\\Pterodactyl\\Service\\Servers', 'str_random') + $this->getFunctionMock('\\Pterodactyl\\Services\\Servers', 'str_random') ->expects($this->any())->willReturnCallback(function ($count) { return str_pad('', $count, 'a'); }); From 2e3476298d4850ba9cb7b792d7ae6ae279596c31 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 20 Aug 2017 20:02:24 -0500 Subject: [PATCH 59/99] Add test for pack exporting --- database/factories/ModelFactory.php | 14 ++ .../Services/Packs/ExportPackServiceTest.php | 164 ++++++++++++++++++ 2 files changed, 178 insertions(+) create mode 100644 tests/Unit/Services/Packs/ExportPackServiceTest.php diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index fdef71ae..a6fbc069 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -131,3 +131,17 @@ $factory->state(Pterodactyl\Models\ServiceVariable::class, 'viewable', function $factory->state(Pterodactyl\Models\ServiceVariable::class, 'editable', function () { return ['user_editable' => 1]; }); + +$factory->define(Pterodactyl\Models\Pack::class, function (Faker\Generator $faker) { + return [ + 'id' => $faker->unique()->randomNumber(), + 'option_id' => $faker->randomNumber(), + 'uuid' => $faker->uuid, + 'name' => $faker->word, + 'description' => null, + 'version' => $faker->randomNumber(), + 'selectable' => 1, + 'visible' => 1, + 'locked' => 0, + ]; +}); diff --git a/tests/Unit/Services/Packs/ExportPackServiceTest.php b/tests/Unit/Services/Packs/ExportPackServiceTest.php new file mode 100644 index 00000000..06674c7c --- /dev/null +++ b/tests/Unit/Services/Packs/ExportPackServiceTest.php @@ -0,0 +1,164 @@ +. + * + * 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\Packs; + +use Illuminate\Contracts\Filesystem\Factory; +use Mockery as m; +use phpmock\phpunit\PHPMock; +use Pterodactyl\Contracts\Repository\PackRepositoryInterface; +use Pterodactyl\Models\Pack; +use Pterodactyl\Services\Packs\ExportPackService; +use Tests\TestCase; +use ZipArchive; + +class ExportPackServiceTest extends TestCase +{ + use PHPMock; + + /** + * @var \ZipArchive + */ + protected $archive; + + /** + * @var \Pterodactyl\Contracts\Repository\PackRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Packs\ExportPackService + */ + protected $service; + + /** + * @var \Illuminate\Contracts\Filesystem\Factory + */ + protected $storage; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->archive = m::mock(ZipArchive::class); + $this->repository = m::mock(PackRepositoryInterface::class); + $this->storage = m::mock(Factory::class); + + $this->service = new ExportPackService($this->storage, $this->repository, $this->archive); + } + + /** + * Provide standard data to all tests. + */ + protected function setupTestData() + { + $this->model = factory(Pack::class)->make(); + $this->json = [ + 'name' => $this->model->name, + 'version' => $this->model->version, + 'description' => $this->model->description, + 'selectable' => $this->model->selectable, + 'visible' => $this->model->visible, + 'locked' => $this->model->locked, + ]; + } + + /** + * Test that an archive of the entire pack can be exported. + */ + public function testFilesAreBundledIntoZipWhenRequested() + { + $this->setupTestData(); + + $this->getFunctionMock('\\Pterodactyl\\Services\\Packs', 'tempnam') + ->expects($this->once())->willReturn('/tmp/myfile.test'); + + $this->getFunctionMock('\\Pterodactyl\\Services\\Packs', 'fopen')->expects($this->never()); + + $this->archive->shouldReceive('open')->with('/tmp/myfile.test', $this->archive::CREATE)->once()->andReturnSelf(); + $this->storage->shouldReceive('disk->files')->with('packs/' . $this->model->uuid)->once()->andReturn(['file_one']); + $this->archive->shouldReceive('addFile')->with(storage_path('app/file_one'), 'file_one')->once()->andReturnSelf(); + $this->archive->shouldReceive('addFromString')->with('import.json', json_encode($this->json, JSON_PRETTY_PRINT))->once()->andReturnSelf(); + $this->archive->shouldReceive('close')->withNoArgs()->once()->andReturnNull(); + + $response = $this->service->handle($this->model, true); + $this->assertEquals('/tmp/myfile.test', $response); + } + + /** + * Test that the pack configuration can be saved as a json file. + */ + public function testPackConfigurationIsSavedAsJsonFile() + { + $this->setupTestData(); + + $this->getFunctionMock('\\Pterodactyl\\Services\\Packs', 'tempnam') + ->expects($this->once())->willReturn('/tmp/myfile.test'); + $this->getFunctionMock('\\Pterodactyl\\Services\\Packs', 'fopen')->expects($this->once())->wilLReturn('fp'); + $this->getFunctionMock('\\Pterodactyl\\Services\\Packs', 'fwrite') + ->expects($this->once())->with('fp', json_encode($this->json, JSON_PRETTY_PRINT))->willReturn(null); + $this->getFunctionMock('\\Pterodactyl\\Services\\Packs', 'fclose') + ->expects($this->once())->with('fp')->willReturn(null); + + $response = $this->service->handle($this->model); + $this->assertEquals('/tmp/myfile.test', $response); + } + + /** + * Test that a model ID can be passed in place of the model itself. + */ + public function testPackIdCanBePassedInPlaceOfModel() + { + $this->setupTestData(); + + $this->repository->shouldReceive('find')->with($this->model->id)->once()->andReturn($this->model); + $this->getFunctionMock('\\Pterodactyl\\Services\\Packs', 'tempnam')->expects($this->once())->willReturn('/tmp/myfile.test'); + $this->getFunctionMock('\\Pterodactyl\\Services\\Packs', 'fopen')->expects($this->once())->wilLReturn(null); + $this->getFunctionMock('\\Pterodactyl\\Services\\Packs', 'fwrite')->expects($this->once())->willReturn(null); + $this->getFunctionMock('\\Pterodactyl\\Services\\Packs', 'fclose')->expects($this->once())->willReturn(null); + + $response = $this->service->handle($this->model->id); + $this->assertEquals('/tmp/myfile.test', $response); + } + + /** + * Test that an exception is thrown when a ZipArchive cannot be created. + * + * @expectedException \Pterodactyl\Exceptions\Service\Pack\ZipArchiveCreationException + */ + public function testExceptionIsThrownIfZipArchiveCannotBeCreated() + { + $this->setupTestData(); + + $this->getFunctionMock('\\Pterodactyl\\Services\\Packs', 'tempnam') + ->expects($this->once())->willReturn('/tmp/myfile.test'); + + $this->archive->shouldReceive('open')->once()->andReturn(false); + + $this->service->handle($this->model, true); + } +} From b2ec9960a1aa8ebf74a1a8203be0009632635e21 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Mon, 21 Aug 2017 21:00:14 -0500 Subject: [PATCH 60/99] Unit tests for pack service --- app/Services/Packs/PackDeletionService.php | 2 +- .../Packs/PackCreationServiceTest.php | 204 ++++++++++++++ .../Packs/PackDeletionServiceTest.php | 136 +++++++++ .../Services/Packs/PackUpdateServiceTest.php | 116 ++++++++ .../Packs/TemplateUploadServiceTest.php | 261 ++++++++++++++++++ 5 files changed, 718 insertions(+), 1 deletion(-) create mode 100644 tests/Unit/Services/Packs/PackCreationServiceTest.php create mode 100644 tests/Unit/Services/Packs/PackDeletionServiceTest.php create mode 100644 tests/Unit/Services/Packs/PackUpdateServiceTest.php create mode 100644 tests/Unit/Services/Packs/TemplateUploadServiceTest.php diff --git a/app/Services/Packs/PackDeletionService.php b/app/Services/Packs/PackDeletionService.php index f38a2df7..590bdb4d 100644 --- a/app/Services/Packs/PackDeletionService.php +++ b/app/Services/Packs/PackDeletionService.php @@ -87,7 +87,7 @@ class PackDeletionService $pack = $this->repository->withColumns(['id', 'uuid'])->find($pack); } - $count = $this->serverRepository->findCountWhere([['pack_id', '=', $pack]]); + $count = $this->serverRepository->findCountWhere([['pack_id', '=', $pack->id]]); if ($count !== 0) { throw new HasActiveServersException(trans('admin/exceptions.packs.delete_has_servers')); } diff --git a/tests/Unit/Services/Packs/PackCreationServiceTest.php b/tests/Unit/Services/Packs/PackCreationServiceTest.php new file mode 100644 index 00000000..7a21b7a9 --- /dev/null +++ b/tests/Unit/Services/Packs/PackCreationServiceTest.php @@ -0,0 +1,204 @@ +. + * + * 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\Packs; + +use Exception; +use Illuminate\Contracts\Filesystem\Factory; +use Illuminate\Http\UploadedFile; +use Mockery as m; +use Illuminate\Database\ConnectionInterface; +use Pterodactyl\Contracts\Repository\PackRepositoryInterface; +use Pterodactyl\Exceptions\Service\Pack\InvalidFileMimeTypeException; +use Pterodactyl\Exceptions\Service\Pack\InvalidFileUploadException; +use Pterodactyl\Models\Pack; +use Pterodactyl\Services\Packs\PackCreationService; +use Tests\TestCase; + +class PackCreationServiceTest extends TestCase +{ + /** + * @var \Illuminate\Database\ConnectionInterface + */ + protected $connection; + + /** + * @var \Illuminate\Http\UploadedFile + */ + protected $file; + + /** + * @var \Pterodactyl\Contracts\Repository\PackRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Packs\PackCreationService + */ + protected $service; + + /** + * @var \Illuminate\Contracts\Filesystem\Factory + */ + protected $storage; + + /** + * @var \Ramsey\Uuid\Uuid + */ + protected $uuid; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->connection = m::mock(ConnectionInterface::class); + $this->file = m::mock(UploadedFile::class); + $this->repository = m::mock(PackRepositoryInterface::class); + $this->storage = m::mock(Factory::class); + $this->uuid = m::mock('overload:\Ramsey\Uuid\Uuid'); + + $this->service = new PackCreationService($this->connection, $this->storage, $this->repository); + } + + /** + * Test that a pack is created when no file upload is provided. + */ + public function testPackIsCreatedWhenNoUploadedFileIsPassed() + { + $model = factory(Pack::class)->make(); + + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->uuid->shouldReceive('uuid4')->withNoArgs()->once()->andReturn($model->uuid); + $this->repository->shouldReceive('create')->with([ + 'uuid' => $model->uuid, + 'selectable' => false, + 'visible' => false, + 'locked' => false, + 'test-data' => 'value', + ])->once()->andReturn($model); + + $this->storage->shouldReceive('disk')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('makeDirectory')->with('packs/' . $model->uuid)->once()->andReturnNull(); + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $response = $this->service->handle(['test-data' => 'value']); + $this->assertInstanceOf(Pack::class, $response); + $this->assertEquals($model, $response); + } + + /** + * Test that a pack can be created when an uploaded file is provided. + * + * @dataProvider mimetypeProvider + */ + public function testPackIsCreatedWhenUploadedFileIsProvided($mime) + { + $model = factory(Pack::class)->make(); + + $this->file->shouldReceive('isValid')->withNoArgs()->once()->andReturn(true); + $this->file->shouldReceive('getMimeType')->withNoArgs()->once()->andReturn($mime); + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->uuid->shouldReceive('uuid4')->withNoArgs()->once()->andReturn($model->uuid); + $this->repository->shouldReceive('create')->with([ + 'uuid' => $model->uuid, + 'selectable' => false, + 'visible' => false, + 'locked' => false, + 'test-data' => 'value', + ])->once()->andReturn($model); + + $this->storage->shouldReceive('disk')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('makeDirectory')->with('packs/' . $model->uuid)->once()->andReturnNull(); + $this->file->shouldReceive('storeAs')->with('packs/' . $model->uuid, 'archive.tar.gz')->once()->andReturnNull(); + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $response = $this->service->handle(['test-data' => 'value'], $this->file); + $this->assertInstanceOf(Pack::class, $response); + $this->assertEquals($model, $response); + } + + /** + * Test that an exception is thrown if the file upload is not valid. + */ + public function testExceptionIsThrownIfInvalidUploadIsProvided() + { + $this->file->shouldReceive('isValid')->withNoArgs()->once()->andReturn(false); + + try { + $this->service->handle([], $this->file); + } catch (Exception $exception) { + $this->assertInstanceOf(InvalidFileUploadException::class, $exception); + $this->assertEquals(trans('admin/exceptions.packs.invalid_upload'), $exception->getMessage()); + } + } + + /** + * Test that an exception is thrown when an invalid mimetype is provided. + * + * @dataProvider invalidMimetypeProvider + */ + public function testExceptionIsThrownIfInvalidMimetypeIsFound($mime) + { + $this->file->shouldReceive('isValid')->withNoArgs()->once()->andReturn(true); + $this->file->shouldReceive('getMimeType')->withNoArgs()->once()->andReturn($mime); + + try { + $this->service->handle([], $this->file); + } catch (InvalidFileMimeTypeException $exception) { + $this->assertEquals(trans('admin/exceptions.packs.invalid_mime', [ + 'type' => implode(', ', PackCreationService::VALID_UPLOAD_TYPES), + ]), $exception->getMessage()); + } + } + + /** + * Return an array of valid mimetypes to test aganist. + * + * @return array + */ + public function mimetypeProvider() + { + return [ + ['application/gzip'], + ['application/x-gzip'], + ]; + } + + /** + * Provide invalid mimetypes to test exceptions aganist. + * + * @return array + */ + public function invalidMimetypeProvider() + { + return [ + ['application/zip'], + ['text/plain'], + ['image/jpeg'], + ]; + } +} diff --git a/tests/Unit/Services/Packs/PackDeletionServiceTest.php b/tests/Unit/Services/Packs/PackDeletionServiceTest.php new file mode 100644 index 00000000..74ec01d5 --- /dev/null +++ b/tests/Unit/Services/Packs/PackDeletionServiceTest.php @@ -0,0 +1,136 @@ +. + * + * 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\Packs; + +use Exception; +use Illuminate\Contracts\Filesystem\Factory; +use Illuminate\Database\ConnectionInterface; +use Mockery as m; +use Pterodactyl\Contracts\Repository\PackRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Exceptions\Service\HasActiveServersException; +use Pterodactyl\Models\Pack; +use Pterodactyl\Services\Packs\PackDeletionService; +use Tests\TestCase; + +class PackDeletionServiceTest extends TestCase +{ + /** + * @var \Illuminate\Database\ConnectionInterface + */ + protected $connection; + + /** + * @var \Pterodactyl\Contracts\Repository\PackRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $serverRepository; + + /** + * @var \Pterodactyl\Services\Packs\PackDeletionService + */ + protected $service; + + /** + * @var \Illuminate\Contracts\Filesystem\Factory + */ + protected $storage; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->connection = m::mock(ConnectionInterface::class); + $this->repository = m::mock(PackRepositoryInterface::class); + $this->serverRepository = m::mock(ServerRepositoryInterface::class); + $this->storage = m::mock(Factory::class); + + $this->service = new PackDeletionService( + $this->connection, + $this->storage, + $this->repository, + $this->serverRepository + ); + } + + /** + * Test that a pack is deleted. + */ + public function testPackIsDeleted() + { + $model = factory(Pack::class)->make(); + + $this->serverRepository->shouldReceive('findCountWhere')->with([['pack_id', '=', $model->id]])->once()->andReturn(0); + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('delete')->with($model->id)->once()->andReturnNull(); + $this->storage->shouldReceive('disk')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('deleteDirectory')->with('packs/' . $model->uuid)->once()->andReturnNull(); + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $this->service->handle($model); + } + + /** + * Test that a pack ID can be passed in place of the model. + */ + public function testPackIdCanBePassedInPlaceOfModel() + { + $model = factory(Pack::class)->make(); + + $this->repository->shouldReceive('withColumns')->with(['id', 'uuid'])->once()->andReturnSelf() + ->shouldReceive('find')->with($model->id)->once()->andReturn($model); + $this->serverRepository->shouldReceive('findCountWhere')->with([['pack_id', '=', $model->id]])->once()->andReturn(0); + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('delete')->with($model->id)->once()->andReturnNull(); + $this->storage->shouldReceive('disk')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('deleteDirectory')->with('packs/' . $model->uuid)->once()->andReturnNull(); + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $this->service->handle($model->id); + } + + /** + * Test that an exception gets thrown if a server is attached to a pack. + */ + public function testExceptionIsThrownIfServerIsAttachedToPack() + { + $model = factory(Pack::class)->make(); + + $this->serverRepository->shouldReceive('findCountWhere')->with([['pack_id', '=', $model->id]])->once()->andReturn(1); + + try { + $this->service->handle($model); + } catch (HasActiveServersException $exception) { + $this->assertEquals(trans('admin/exceptions.packs.delete_has_servers'), $exception->getMessage()); + } + } +} diff --git a/tests/Unit/Services/Packs/PackUpdateServiceTest.php b/tests/Unit/Services/Packs/PackUpdateServiceTest.php new file mode 100644 index 00000000..628979bd --- /dev/null +++ b/tests/Unit/Services/Packs/PackUpdateServiceTest.php @@ -0,0 +1,116 @@ +. + * + * 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\Packs; + +use Mockery as m; +use Pterodactyl\Contracts\Repository\PackRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Exceptions\Service\HasActiveServersException; +use Pterodactyl\Models\Pack; +use Pterodactyl\Services\Packs\PackUpdateService; +use Tests\TestCase; + +class PackUpdateServiceTest extends TestCase +{ + /** + * @var \Pterodactyl\Contracts\Repository\PackRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $serverRepository; + + /** + * @var \Pterodactyl\Services\Packs\PackUpdateService + */ + protected $service; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->repository = m::mock(PackRepositoryInterface::class); + $this->serverRepository = m::mock(ServerRepositoryInterface::class); + + $this->service = new PackUpdateService($this->repository, $this->serverRepository); + } + + /** + * Test that a pack is updated. + */ + public function testPackIsUpdated() + { + $model = factory(Pack::class)->make(); + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($model->id, [ + 'locked' => false, + 'visible' => false, + 'selectable' => false, + 'test-data' => 'value' + ])->once()->andReturn(1); + + $this->assertEquals(1, $this->service->handle($model, ['test-data' => 'value'])); + } + + /** + * Test that an exception is thrown if the pack option ID is changed while servers are using the pack. + */ + public function testExceptionIsThrownIfModifyingOptionIdWhenServersAreAttached() + { + $model = factory(Pack::class)->make(); + $this->serverRepository->shouldReceive('findCountWhere')->with([['pack_id', '=', $model->id]])->once()->andReturn(1); + + try { + $this->service->handle($model, ['option_id' => 0]); + } catch (HasActiveServersException $exception) { + $this->assertEquals(trans('admin/exceptions.packs.update_has_servers'), $exception->getMessage()); + } + } + + /** + * Test that an ID for a pack can be passed in place of the model. + */ + public function testPackIdCanBePassedInPlaceOfModel() + { + $model = factory(Pack::class)->make(); + + $this->repository->shouldReceive('withColumns')->with(['id', 'option_id'])->once()->andReturnSelf() + ->shouldReceive('find')->with($model->id)->once()->andReturn($model); + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($model->id, [ + 'locked' => false, + 'visible' => false, + 'selectable' => false, + 'test-data' => 'value' + ])->once()->andReturn(1); + + $this->assertEquals(1, $this->service->handle($model->id, ['test-data' => 'value'])); + } +} diff --git a/tests/Unit/Services/Packs/TemplateUploadServiceTest.php b/tests/Unit/Services/Packs/TemplateUploadServiceTest.php new file mode 100644 index 00000000..4f8a81d2 --- /dev/null +++ b/tests/Unit/Services/Packs/TemplateUploadServiceTest.php @@ -0,0 +1,261 @@ +. + * + * 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\Packs; + +use Illuminate\Http\UploadedFile; +use Mockery as m; +use Pterodactyl\Exceptions\Service\Pack\InvalidFileMimeTypeException; +use Pterodactyl\Exceptions\Service\Pack\InvalidFileUploadException; +use Pterodactyl\Exceptions\Service\Pack\InvalidPackArchiveFormatException; +use Pterodactyl\Exceptions\Service\Pack\UnreadableZipArchiveException; +use Pterodactyl\Exceptions\Service\Pack\ZipExtractionException; +use Pterodactyl\Models\Pack; +use Pterodactyl\Services\Packs\PackCreationService; +use Pterodactyl\Services\Packs\TemplateUploadService; +use Tests\TestCase; +use ZipArchive; + +class TemplateUploadServiceTest extends TestCase +{ + const JSON_FILE_CONTENTS = '{"test_content": "value"}'; + + /** + * @var \ZipArchive + */ + protected $archive; + + /** + * @var \Pterodactyl\Services\Packs\PackCreationService + */ + protected $creationService; + + /** + * @var \Illuminate\Http\UploadedFile + */ + protected $file; + + /** + * @var \Pterodactyl\Services\Packs\TemplateUploadService + */ + protected $service; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->archive = m::mock(ZipArchive::class); + $this->creationService = m::mock(PackCreationService::class); + $this->file = m::mock(UploadedFile::class); + + $this->service = new TemplateUploadService($this->creationService, $this->archive); + } + + /** + * Test that a JSON file can be processed and turned into a pack. + * + * @dataProvider jsonMimetypeProvider + */ + public function testJsonFileIsProcessed($mime) + { + $this->file->shouldReceive('isValid')->withNoArgs()->once()->andReturn(true); + $this->file->shouldReceive('getMimeType')->withNoArgs()->twice()->andReturn($mime); + $this->file->shouldReceive('getSize')->withNoArgs()->once()->andReturn(128); + $this->file->shouldReceive('openFile')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('fread')->with(128)->once()->andReturn(self::JSON_FILE_CONTENTS); + + $this->creationService->shouldReceive('handle')->with(['test_content' => 'value', 'option_id' => 1]) + ->once()->andReturn(factory(Pack::class)->make()); + + $this->assertInstanceOf(Pack::class, $this->service->handle(1, $this->file)); + } + + /** + * Test that a zip file can be processed. + */ + public function testZipfileIsProcessed() + { + $model = factory(Pack::class)->make(); + + $this->file->shouldReceive('isValid')->withNoArgs()->once()->andReturn(true); + $this->file->shouldReceive('getMimeType')->withNoArgs()->twice()->andReturn('application/zip'); + + $this->file->shouldReceive('getRealPath')->withNoArgs()->once()->andReturn('/test/real'); + $this->archive->shouldReceive('open')->with('/test/real')->once()->andReturn(true); + $this->archive->shouldReceive('locateName')->with('import.json')->once()->andReturn(true); + $this->archive->shouldReceive('locateName')->with('archive.tar.gz')->once()->andReturn(true); + $this->archive->shouldReceive('getFromName')->with('import.json')->once()->andReturn(self::JSON_FILE_CONTENTS); + $this->creationService->shouldReceive('handle')->with(['test_content' => 'value', 'option_id' => 1]) + ->once()->andReturn($model); + $this->archive->shouldReceive('extractTo')->with(storage_path('app/packs/' . $model->uuid), 'archive.tar.gz') + ->once()->andReturn(true); + $this->archive->shouldReceive('close')->withNoArgs()->once()->andReturnNull(); + + $this->assertInstanceOf(Pack::class, $this->service->handle(1, $this->file)); + } + + /** + * Test that an exception is thrown if the file upload is invalid. + */ + public function testExceptionIsThrownIfFileUploadIsInvalid() + { + $this->file->shouldReceive('isValid')->withNoArgs()->once()->andReturn(false); + + try { + $this->service->handle(1, $this->file); + } catch (InvalidFileUploadException $exception) { + $this->assertEquals(trans('admin/exceptions.packs.invalid_upload'), $exception->getMessage()); + } + } + + /** + * Test that an invalid mimetype throws an exception. + * + * @dataProvider invalidMimetypeProvider + */ + public function testExceptionIsThrownIfMimetypeIsInvalid($mime) + { + $this->file->shouldReceive('isValid')->withNoArgs()->once()->andReturn(true); + $this->file->shouldReceive('getMimeType')->withNoArgs()->once()->andReturn($mime); + + try { + $this->service->handle(1, $this->file); + } catch (InvalidFileMimeTypeException $exception) { + $this->assertEquals(trans('admin/exceptions.packs.invalid_mime', [ + 'type' => implode(', ', TemplateUploadService::VALID_UPLOAD_TYPES), + ]), $exception->getMessage()); + } + } + + /** + * Test that an exception is thrown if the zip is unreadable. + */ + public function testExceptionIsThrownIfZipArchiveIsUnreadable() + { + $this->file->shouldReceive('isValid')->withNoArgs()->once()->andReturn(true); + $this->file->shouldReceive('getMimeType')->withNoArgs()->twice()->andReturn('application/zip'); + + $this->file->shouldReceive('getRealPath')->withNoArgs()->once()->andReturn('/test/path'); + $this->archive->shouldReceive('open')->with('/test/path')->once()->andReturn(false); + + try { + $this->service->handle(1, $this->file); + } catch (UnreadableZipArchiveException $exception) { + $this->assertEquals(trans('admin/exceptions.packs.unreadable'), $exception->getMessage()); + } + } + + /** + * Test that a zip missing the required files throws an exception. + * + * @dataProvider filenameProvider + */ + public function testExceptionIsThrownIfZipDoesNotContainProperFiles($a, $b) + { + $this->file->shouldReceive('isValid')->withNoArgs()->once()->andReturn(true); + $this->file->shouldReceive('getMimeType')->withNoArgs()->twice()->andReturn('application/zip'); + + $this->file->shouldReceive('getRealPath')->withNoArgs()->once()->andReturn('/test/path'); + $this->archive->shouldReceive('open')->with('/test/path')->once()->andReturn(true); + $this->archive->shouldReceive('locateName')->with('import.json')->once()->andReturn($a); + + if ($a) { + $this->archive->shouldReceive('locateName')->with('archive.tar.gz')->once()->andReturn($b); + } + + try { + $this->service->handle(1, $this->file); + } catch (InvalidPackArchiveFormatException $exception) { + $this->assertEquals(trans('admin/exceptions.packs.invalid_archive_exception'), $exception->getMessage()); + } + } + + /** + * Test that an exception is thrown if an archive cannot be extracted from the zip file. + */ + public function testExceptionIsThrownIfArchiveCannotBeExtractedFromZip() + { + $model = factory(Pack::class)->make(); + + $this->file->shouldReceive('isValid')->withNoArgs()->once()->andReturn(true); + $this->file->shouldReceive('getMimeType')->withNoArgs()->twice()->andReturn('application/zip'); + + $this->file->shouldReceive('getRealPath')->withNoArgs()->once()->andReturn('/test/real'); + $this->archive->shouldReceive('open')->once()->andReturn(true); + $this->archive->shouldReceive('locateName')->twice()->andReturn(true); + $this->archive->shouldReceive('getFromName')->once()->andReturn(self::JSON_FILE_CONTENTS); + $this->creationService->shouldReceive('handle')->once()->andReturn($model); + $this->archive->shouldReceive('extractTo')->once()->andReturn(false); + + try { + $this->service->handle(1, $this->file); + } catch (ZipExtractionException $exception) { + $this->assertEquals(trans('admin/exceptions.packs.zip_extraction'), $exception->getMessage()); + } + } + + /** + * Provide valid JSON mimetypes to use in tests. + * + * @return array + */ + public function jsonMimetypeProvider() + { + return [ + ['text/plain'], + ['application/json'], + ]; + } + + /** + * Return invalid mimetypes for testing. + * + * @return array + */ + public function invalidMimetypeProvider() + { + return [ + ['application/gzip'], + ['application/x-gzip'], + ['image/jpeg'], + ]; + } + + /** + * Return values for archive->locateName function, import.json and archive.tar.gz respectively + * + * @return array + */ + public function filenameProvider() + { + return [ + [true, false], + [false, true], + [false, false], + ]; + } +} From 78c8b8d8ea0c80e8e17a5dd2af0d2c9944663dc4 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Mon, 21 Aug 2017 22:06:52 -0500 Subject: [PATCH 61/99] Upgrade PHPCS --- .gitignore | 1 + .php_cs | 41 ++- composer.json | 5 +- composer.lock | 912 ++++++++++++++++++++++++++++++-------------------- 4 files changed, 597 insertions(+), 362 deletions(-) diff --git a/.gitignore b/.gitignore index 9d8eca6a..d03bf768 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,4 @@ docker-compose.yml # for image related files misc .phpstorm.meta.php +.php_cs.cache diff --git a/.php_cs b/.php_cs index 791899b7..c1e6da7e 100644 --- a/.php_cs +++ b/.php_cs @@ -1,7 +1,40 @@ in([ + 'app', + 'bootstrap', + 'config', + 'database', + 'resources/lang', + 'routes', + 'tests', + ]); -use SLLH\StyleCIBridge\ConfigBridge; - -return ConfigBridge::create(); +return PhpCsFixer\Config::create() + ->setRules([ + '@Symfony' => true, + '@PSR1' => true, + '@PSR2' => true, + 'align_multiline_comment' => ['comment_type' => 'phpdocs_like'], + 'array_syntax' => ['syntax' => 'short'], + 'blank_line_before_return' => true, + 'blank_line_before_statement' => false, + 'combine_consecutive_unsets' => true, + 'concat_space' => ['spacing' => 'one'], + 'declare_equal_normalize' => ['space' => 'single'], + 'heredoc_to_nowdoc' => true, + 'linebreak_after_opening_tag' => true, + 'new_with_braces' => false, + 'no_alias_functions' => true, + 'no_multiline_whitespace_before_semicolons' => true, + 'no_unreachable_default_argument_value' => true, + 'no_useless_return' => true, + 'not_operator_with_successor_space' => true, + 'phpdoc_separation' => false, + 'protected_to_private' => false, + 'psr0' => ['dir' => 'app'], + 'psr4' => true, + 'random_api_migration' => true, + 'standardize_not_equals' => true, + ])->setRiskyAllowed(true)->setFinder($finder); diff --git a/composer.json b/composer.json index f4f46e98..86d2c16c 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,7 @@ "doctrine/dbal": "2.5.13", "edvinaskrucas/settings": "2.0.0", "fideloper/proxy": "3.3.3", + "friendsofphp/php-cs-fixer": "2.4.0", "guzzlehttp/guzzle": "6.2.3", "igaster/laravel-theme": "1.16.0", "laracasts/utilities": "2.1.0", @@ -43,12 +44,10 @@ }, "require-dev": { "barryvdh/laravel-ide-helper": "2.4.1", - "friendsofphp/php-cs-fixer": "1.13.1", "fzaninotto/faker": "1.6.0", "mockery/mockery": "0.9.9", "php-mock/php-mock-phpunit": "1.1.2", - "phpunit/phpunit": "5.7.21", - "sllh/php-cs-fixer-styleci-bridge": "2.1.1" + "phpunit/phpunit": "5.7.21" }, "autoload": { "classmap": [ diff --git a/composer.lock b/composer.lock index 4f49bd19..7b299fd3 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "76f4864c9d8653bb8a6be22a115b7489", + "content-hash": "e641798f79e2865a130f8b2d4f31a91b", "packages": [ { "name": "aws/aws-sdk-php", @@ -899,6 +899,128 @@ ], "time": "2017-05-31T12:50:41+00:00" }, + { + "name": "friendsofphp/php-cs-fixer", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", + "reference": "63661f3add3609e90e4ab8115113e189ae547bb4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/63661f3add3609e90e4ab8115113e189ae547bb4", + "reference": "63661f3add3609e90e4ab8115113e189ae547bb4", + "shasum": "" + }, + "require": { + "doctrine/annotations": "^1.2", + "ext-json": "*", + "ext-tokenizer": "*", + "gecko-packages/gecko-php-unit": "^2.0", + "php": "^5.6 || >=7.0 <7.2", + "sebastian/diff": "^1.4", + "symfony/console": "^3.0", + "symfony/event-dispatcher": "^3.0", + "symfony/filesystem": "^3.0", + "symfony/finder": "^3.0", + "symfony/options-resolver": "^3.0", + "symfony/polyfill-php70": "^1.0", + "symfony/polyfill-php72": "^1.4", + "symfony/process": "^3.0", + "symfony/stopwatch": "^3.0" + }, + "conflict": { + "hhvm": "*" + }, + "require-dev": { + "johnkary/phpunit-speedtrap": "^1.1", + "justinrainbow/json-schema": "^5.0", + "phpunit/phpunit": "^4.8.35 || ^5.4.3", + "satooshi/php-coveralls": "^1.0", + "symfony/phpunit-bridge": "^3.2.2" + }, + "suggest": { + "ext-mbstring": "For handling non-UTF8 characters in cache signature.", + "symfony/polyfill-mbstring": "When enabling `ext-mbstring` is not possible." + }, + "bin": [ + "php-cs-fixer" + ], + "type": "application", + "extra": { + "branch-alias": { + "dev-master": "2.4-dev" + } + }, + "autoload": { + "psr-4": { + "PhpCsFixer\\": "src/" + }, + "classmap": [ + "tests/Test/Assert/AssertTokensTrait.php", + "tests/Test/AbstractFixerTestCase.php", + "tests/Test/AbstractIntegrationTestCase.php", + "tests/Test/IntegrationCase.php", + "tests/Test/IntegrationCaseFactory.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dariusz RumiÅ„ski", + "email": "dariusz.ruminski@gmail.com" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "A tool to automatically fix PHP code style", + "time": "2017-07-18T15:35:40+00:00" + }, + { + "name": "gecko-packages/gecko-php-unit", + "version": "v2.1", + "source": { + "type": "git", + "url": "https://github.com/GeckoPackages/GeckoPHPUnit.git", + "reference": "5b9e9622c7efd3b22655270b80c03f9e52878a6e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GeckoPackages/GeckoPHPUnit/zipball/5b9e9622c7efd3b22655270b80c03f9e52878a6e", + "reference": "5b9e9622c7efd3b22655270b80c03f9e52878a6e", + "shasum": "" + }, + "require": { + "php": "^5.3.6 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.4.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "GeckoPackages\\PHPUnit\\": "src\\PHPUnit" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Additional PHPUnit tests.", + "homepage": "https://github.com/GeckoPackages", + "keywords": [ + "extension", + "filesystem", + "phpunit" + ], + "time": "2017-06-20T11:22:48+00:00" + }, { "name": "guzzlehttp/guzzle", "version": "6.2.3", @@ -2553,6 +2675,58 @@ ], "time": "2016-08-21T15:57:09+00:00" }, + { + "name": "sebastian/diff", + "version": "1.4.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7f066a26a962dbe58ddea9f72a4e82874a3975a4", + "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff" + ], + "time": "2017-05-22T07:24:03+00:00" + }, { "name": "sofa/eloquence", "version": "5.4.1", @@ -3059,6 +3233,55 @@ "homepage": "https://symfony.com", "time": "2017-06-09T14:53:08+00:00" }, + { + "name": "symfony/filesystem", + "version": "v3.3.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "427987eb4eed764c3b6e38d52a0f87989e010676" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/427987eb4eed764c3b6e38d52a0f87989e010676", + "reference": "427987eb4eed764c3b6e38d52a0f87989e010676", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "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 Filesystem Component", + "homepage": "https://symfony.com", + "time": "2017-07-11T07:17:58+00:00" + }, { "name": "symfony/finder", "version": "v3.3.6", @@ -3248,17 +3471,71 @@ "time": "2017-08-01T10:25:59+00:00" }, { - "name": "symfony/polyfill-mbstring", - "version": "v1.4.0", + "name": "symfony/options-resolver", + "version": "v3.3.6", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "f29dca382a6485c3cbe6379f0c61230167681937" + "url": "https://github.com/symfony/options-resolver.git", + "reference": "ff48982d295bcac1fd861f934f041ebc73ae40f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/f29dca382a6485c3cbe6379f0c61230167681937", - "reference": "f29dca382a6485c3cbe6379f0c61230167681937", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/ff48982d295bcac1fd861f934f041ebc73ae40f0", + "reference": "ff48982d295bcac1fd861f934f041ebc73ae40f0", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "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 OptionsResolver Component", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "time": "2017-04-12T14:14:56+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "7c8fae0ac1d216eb54349e6a8baa57d515fe8803" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/7c8fae0ac1d216eb54349e6a8baa57d515fe8803", + "reference": "7c8fae0ac1d216eb54349e6a8baa57d515fe8803", "shasum": "" }, "require": { @@ -3270,7 +3547,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "1.5-dev" } }, "autoload": { @@ -3304,7 +3581,7 @@ "portable", "shim" ], - "time": "2017-06-09T14:24:12+00:00" + "time": "2017-06-14T15:44:48+00:00" }, { "name": "symfony/polyfill-php56", @@ -3362,6 +3639,120 @@ ], "time": "2017-06-09T08:25:21+00:00" }, + { + "name": "symfony/polyfill-php70", + "version": "v1.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php70.git", + "reference": "b6482e68974486984f59449ecea1fbbb22ff840f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/b6482e68974486984f59449ecea1fbbb22ff840f", + "reference": "b6482e68974486984f59449ecea1fbbb22ff840f", + "shasum": "" + }, + "require": { + "paragonie/random_compat": "~1.0|~2.0", + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php70\\": "" + }, + "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 backporting some PHP 7.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "time": "2017-06-14T15:44:48+00:00" + }, + { + "name": "symfony/polyfill-php72", + "version": "v1.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php72.git", + "reference": "8abc9097f5001d310f0edba727469c988acc6ea7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/8abc9097f5001d310f0edba727469c988acc6ea7", + "reference": "8abc9097f5001d310f0edba727469c988acc6ea7", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.5-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php72\\": "" + }, + "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 backporting some PHP 7.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "time": "2017-07-11T13:25:55+00:00" + }, { "name": "symfony/polyfill-util", "version": "v1.4.0", @@ -3541,6 +3932,55 @@ ], "time": "2017-07-21T17:43:13+00:00" }, + { + "name": "symfony/stopwatch", + "version": "v3.3.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/stopwatch.git", + "reference": "602a15299dc01556013b07167d4f5d3a60e90d15" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/602a15299dc01556013b07167d4f5d3a60e90d15", + "reference": "602a15299dc01556013b07167d4f5d3a60e90d15", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Stopwatch\\": "" + }, + "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 Stopwatch Component", + "homepage": "https://symfony.com", + "time": "2017-04-12T14:14:56+00:00" + }, { "name": "symfony/translation", "version": "v3.3.6", @@ -4042,68 +4482,6 @@ ], "time": "2016-06-13T19:28:20+00:00" }, - { - "name": "composer/semver", - "version": "1.4.2", - "source": { - "type": "git", - "url": "https://github.com/composer/semver.git", - "reference": "c7cb9a2095a074d131b65a8a0cd294479d785573" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/c7cb9a2095a074d131b65a8a0cd294479d785573", - "reference": "c7cb9a2095a074d131b65a8a0cd294479d785573", - "shasum": "" - }, - "require": { - "php": "^5.3.2 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.5 || ^5.0.5", - "phpunit/phpunit-mock-objects": "2.3.0 || ^3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Composer\\Semver\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nils Adermann", - "email": "naderman@naderman.de", - "homepage": "http://www.naderman.de" - }, - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" - }, - { - "name": "Rob Bast", - "email": "rob.bast@gmail.com", - "homepage": "http://robbast.nl" - } - ], - "description": "Semver library that offers utilities, version constraint parsing and validation.", - "keywords": [ - "semantic", - "semver", - "validation", - "versioning" - ], - "time": "2016-08-30T16:08:34+00:00" - }, { "name": "doctrine/instantiator", "version": "1.1.0", @@ -4158,64 +4536,6 @@ ], "time": "2017-07-22T11:58:36+00:00" }, - { - "name": "friendsofphp/php-cs-fixer", - "version": "v1.13.1", - "source": { - "type": "git", - "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "0ea4f7ed06ca55da1d8fc45da26ff87f261c4088" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/0ea4f7ed06ca55da1d8fc45da26ff87f261c4088", - "reference": "0ea4f7ed06ca55da1d8fc45da26ff87f261c4088", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": "^5.3.6 || >=7.0 <7.2", - "sebastian/diff": "^1.1", - "symfony/console": "^2.3 || ^3.0", - "symfony/event-dispatcher": "^2.1 || ^3.0", - "symfony/filesystem": "^2.1 || ^3.0", - "symfony/finder": "^2.1 || ^3.0", - "symfony/process": "^2.3 || ^3.0", - "symfony/stopwatch": "^2.5 || ^3.0" - }, - "conflict": { - "hhvm": "<3.9" - }, - "require-dev": { - "phpunit/phpunit": "^4.5|^5", - "satooshi/php-coveralls": "^1.0" - }, - "bin": [ - "php-cs-fixer" - ], - "type": "application", - "autoload": { - "psr-4": { - "Symfony\\CS\\": "Symfony/CS/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Dariusz RumiÅ„ski", - "email": "dariusz.ruminski@gmail.com" - }, - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - } - ], - "description": "A tool to automatically fix PHP code style", - "time": "2016-12-01T00:05:05+00:00" - }, { "name": "fzaninotto/faker", "version": "v1.6.0", @@ -4309,6 +4629,48 @@ ], "time": "2015-05-11T14:41:42+00:00" }, + { + "name": "ircmaxell/password-compat", + "version": "v1.0.4", + "source": { + "type": "git", + "url": "https://github.com/ircmaxell/password_compat.git", + "reference": "5c5cde8822a69545767f7c7f3058cb15ff84614c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ircmaxell/password_compat/zipball/5c5cde8822a69545767f7c7f3058cb15ff84614c", + "reference": "5c5cde8822a69545767f7c7f3058cb15ff84614c", + "shasum": "" + }, + "require-dev": { + "phpunit/phpunit": "4.*" + }, + "type": "library", + "autoload": { + "files": [ + "lib/password.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Anthony Ferrara", + "email": "ircmaxell@php.net", + "homepage": "http://blog.ircmaxell.com" + } + ], + "description": "A compatibility library for the proposed simplified password hashing algorithm: https://wiki.php.net/rfc/password_hash", + "homepage": "https://github.com/ircmaxell/password_compat", + "keywords": [ + "hashing", + "password" + ], + "time": "2014-11-20T16:49:30+00:00" + }, { "name": "mockery/mockery", "version": "0.9.9", @@ -5293,58 +5655,6 @@ ], "time": "2017-01-29T09:50:25+00:00" }, - { - "name": "sebastian/diff", - "version": "1.4.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7f066a26a962dbe58ddea9f72a4e82874a3975a4", - "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4", - "shasum": "" - }, - "require": { - "php": "^5.3.3 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Diff implementation", - "homepage": "https://github.com/sebastianbergmann/diff", - "keywords": [ - "diff" - ], - "time": "2017-05-22T07:24:03+00:00" - }, { "name": "sebastian/environment", "version": "2.0.0", @@ -5697,116 +6007,6 @@ "homepage": "https://github.com/sebastianbergmann/version", "time": "2016-10-03T07:35:21+00:00" }, - { - "name": "sllh/php-cs-fixer-styleci-bridge", - "version": "v2.1.1", - "source": { - "type": "git", - "url": "https://github.com/Soullivaneuh/php-cs-fixer-styleci-bridge.git", - "reference": "2406ac6ab7a243e1b4d4f5f73c12e5905c629877" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Soullivaneuh/php-cs-fixer-styleci-bridge/zipball/2406ac6ab7a243e1b4d4f5f73c12e5905c629877", - "reference": "2406ac6ab7a243e1b4d4f5f73c12e5905c629877", - "shasum": "" - }, - "require": { - "composer/semver": "^1.0", - "doctrine/inflector": "^1.0", - "php": "^5.3 || ^7.0", - "sllh/styleci-fixers": "^3.0 || ^4.0", - "symfony/config": "^2.3 || ^3.0", - "symfony/console": "^2.3 || ^3.0", - "symfony/yaml": "^2.3 || ^3.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^1.6.1", - "matthiasnoback/symfony-config-test": "^1.2", - "symfony/phpunit-bridge": "^2.7.4 || ^3.0", - "twig/twig": "^1.22" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.x-dev" - } - }, - "autoload": { - "psr-4": { - "SLLH\\StyleCIBridge\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Sullivan SENECHAL", - "email": "soullivaneuh@gmail.com" - } - ], - "description": "Auto configure PHP-CS-Fixer from StyleCI config file", - "keywords": [ - "PSR-1", - "PSR-2", - "PSR-4", - "StyleCI", - "configuration", - "laravel", - "php-cs-fixer", - "psr", - "symfony" - ], - "time": "2016-06-22T13:26:46+00:00" - }, - { - "name": "sllh/styleci-fixers", - "version": "v4.10.0", - "source": { - "type": "git", - "url": "https://github.com/Soullivaneuh/styleci-fixers.git", - "reference": "f2547f5cd465325bc7b996d53657189019e7ac05" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Soullivaneuh/styleci-fixers/zipball/f2547f5cd465325bc7b996d53657189019e7ac05", - "reference": "f2547f5cd465325bc7b996d53657189019e7ac05", - "shasum": "" - }, - "require": { - "php": "^5.3 || ^7.0" - }, - "require-dev": { - "styleci/sdk": "^1.0", - "symfony/console": "^3.0", - "twig/twig": "^2.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "SLLH\\StyleCIFixers\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Sullivan SENECHAL", - "email": "soullivaneuh@gmail.com" - } - ], - "description": "Auto configure PHP-CS-Fixer from StyleCI config file", - "keywords": [ - "StyleCI", - "configuration", - "php-cs-fixer" - ], - "time": "2017-05-10T08:16:59+00:00" - }, { "name": "symfony/class-loader", "version": "v3.3.6", @@ -5864,47 +6064,37 @@ "time": "2017-06-02T09:51:43+00:00" }, { - "name": "symfony/config", - "version": "v3.3.6", + "name": "symfony/polyfill-php54", + "version": "v1.5.0", "source": { "type": "git", - "url": "https://github.com/symfony/config.git", - "reference": "54ee12b0dd60f294132cabae6f5da9573d2e5297" + "url": "https://github.com/symfony/polyfill-php54.git", + "reference": "b7763422a5334c914ef0298ed21b253d25913a6e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/54ee12b0dd60f294132cabae6f5da9573d2e5297", - "reference": "54ee12b0dd60f294132cabae6f5da9573d2e5297", + "url": "https://api.github.com/repos/symfony/polyfill-php54/zipball/b7763422a5334c914ef0298ed21b253d25913a6e", + "reference": "b7763422a5334c914ef0298ed21b253d25913a6e", "shasum": "" }, "require": { - "php": ">=5.5.9", - "symfony/filesystem": "~2.8|~3.0" - }, - "conflict": { - "symfony/dependency-injection": "<3.3", - "symfony/finder": "<3.3" - }, - "require-dev": { - "symfony/dependency-injection": "~3.3", - "symfony/finder": "~3.3", - "symfony/yaml": "~3.0" - }, - "suggest": { - "symfony/yaml": "To use the yaml reference dumper" + "php": ">=5.3.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.3-dev" + "dev-master": "1.5-dev" } }, "autoload": { "psr-4": { - "Symfony\\Component\\Config\\": "" + "Symfony\\Polyfill\\Php54\\": "" }, - "exclude-from-classmap": [ - "/Tests/" + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", @@ -5913,47 +6103,54 @@ ], "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 Config Component", + "description": "Symfony polyfill backporting some PHP 5.4+ features to lower PHP versions", "homepage": "https://symfony.com", - "time": "2017-07-19T07:37:29+00:00" + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "time": "2017-06-14T15:44:48+00:00" }, { - "name": "symfony/filesystem", - "version": "v3.3.6", + "name": "symfony/polyfill-php55", + "version": "v1.5.0", "source": { "type": "git", - "url": "https://github.com/symfony/filesystem.git", - "reference": "427987eb4eed764c3b6e38d52a0f87989e010676" + "url": "https://github.com/symfony/polyfill-php55.git", + "reference": "29b1381d66f16e0581aab0b9f678ccf073288f68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/427987eb4eed764c3b6e38d52a0f87989e010676", - "reference": "427987eb4eed764c3b6e38d52a0f87989e010676", + "url": "https://api.github.com/repos/symfony/polyfill-php55/zipball/29b1381d66f16e0581aab0b9f678ccf073288f68", + "reference": "29b1381d66f16e0581aab0b9f678ccf073288f68", "shasum": "" }, "require": { - "php": ">=5.5.9" + "ircmaxell/password-compat": "~1.0", + "php": ">=5.3.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.3-dev" + "dev-master": "1.5-dev" } }, "autoload": { "psr-4": { - "Symfony\\Component\\Filesystem\\": "" + "Symfony\\Polyfill\\Php55\\": "" }, - "exclude-from-classmap": [ - "/Tests/" + "files": [ + "bootstrap.php" ] }, "notification-url": "https://packagist.org/downloads/", @@ -5962,66 +6159,71 @@ ], "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 Filesystem Component", + "description": "Symfony polyfill backporting some PHP 5.5+ features to lower PHP versions", "homepage": "https://symfony.com", - "time": "2017-07-11T07:17:58+00:00" + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "time": "2017-06-14T15:44:48+00:00" }, { - "name": "symfony/stopwatch", - "version": "v3.3.6", + "name": "symfony/polyfill-xml", + "version": "v1.5.0", "source": { "type": "git", - "url": "https://github.com/symfony/stopwatch.git", - "reference": "602a15299dc01556013b07167d4f5d3a60e90d15" + "url": "https://github.com/symfony/polyfill-xml.git", + "reference": "7d536462e554da7b05600a926303bf9b99153275" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/602a15299dc01556013b07167d4f5d3a60e90d15", - "reference": "602a15299dc01556013b07167d4f5d3a60e90d15", + "url": "https://api.github.com/repos/symfony/polyfill-xml/zipball/7d536462e554da7b05600a926303bf9b99153275", + "reference": "7d536462e554da7b05600a926303bf9b99153275", "shasum": "" }, "require": { - "php": ">=5.5.9" + "php": ">=5.3.3", + "symfony/polyfill-php72": "~1.4" }, - "type": "library", + "type": "metapackage", "extra": { "branch-alias": { - "dev-master": "3.3-dev" + "dev-master": "1.5-dev" } }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Stopwatch\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "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 Stopwatch Component", + "description": "Symfony polyfill for xml's utf8_encode and utf8_decode functions", "homepage": "https://symfony.com", - "time": "2017-04-12T14:14:56+00:00" + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "time": "2017-06-14T15:44:48+00:00" }, { "name": "symfony/yaml", From 3ee5803416c03352b2b2a93224a5e3d6c0959f61 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Mon, 21 Aug 2017 22:10:48 -0500 Subject: [PATCH 62/99] Massive PHPCS linting --- .php_cs | 1 + app/Console/Commands/AddLocation.php | 2 - app/Console/Commands/AddNode.php | 2 - app/Console/Commands/CleanServiceBackup.php | 2 - app/Console/Commands/ClearServices.php | 2 - app/Console/Commands/ClearTasks.php | 2 - app/Console/Commands/MakeUser.php | 2 - app/Console/Commands/RebuildServer.php | 2 - app/Console/Commands/RunTasks.php | 2 - app/Console/Commands/ShowVersion.php | 2 - app/Console/Commands/UpdateEmailSettings.php | 2 - app/Console/Commands/UpdateEnvironment.php | 2 - app/Console/Kernel.php | 3 +- app/Contracts/Criteria/CriteriaInterface.php | 4 +- .../AllocationRepositoryInterface.php | 6 +- .../Repository/ApiKeyRepositoryInterface.php | 1 - .../ApiPermissionRepositoryInterface.php | 1 - .../Attributes/SearchableInterface.php | 2 +- .../Daemon/BaseRepositoryInterface.php | 8 +- .../ConfigurationRepositoryInterface.php | 2 +- .../Daemon/ServerRepositoryInterface.php | 10 +- .../DatabaseHostRepositoryInterface.php | 4 +- .../DatabaseRepositoryInterface.php | 34 ++--- .../LocationRepositoryInterface.php | 2 +- .../Repository/NodeRepositoryInterface.php | 12 +- .../OptionVariableRepositoryInterface.php | 1 - .../Repository/PackRepositoryInterface.php | 6 +- .../Repository/RepositoryInterface.php | 48 +++---- .../Repository/ServerRepositoryInterface.php | 12 +- .../ServerVariableRepositoryInterface.php | 1 - .../ServiceOptionRepositoryInterface.php | 8 +- .../Repository/ServiceRepositoryInterface.php | 4 +- .../ServiceVariableRepositoryInterface.php | 1 - .../Repository/UserRepositoryInterface.php | 2 +- app/Events/Auth/FailedCaptcha.php | 5 +- app/Events/Auth/FailedPasswordReset.php | 5 +- app/Events/Event.php | 1 - app/Events/Server/Created.php | 3 +- app/Events/Server/Creating.php | 3 +- app/Events/Server/Deleted.php | 3 +- app/Events/Server/Deleting.php | 3 +- app/Events/Server/Saved.php | 3 +- app/Events/Server/Saving.php | 3 +- app/Events/Server/Updated.php | 3 +- app/Events/Server/Updating.php | 3 +- app/Events/Subuser/Created.php | 3 +- app/Events/Subuser/Creating.php | 3 +- app/Events/Subuser/Deleted.php | 3 +- app/Events/Subuser/Deleting.php | 3 +- app/Events/User/Created.php | 3 +- app/Events/User/Creating.php | 3 +- app/Events/User/Deleted.php | 3 +- app/Events/User/Deleting.php | 3 +- app/Exceptions/AccountNotFoundException.php | 1 - app/Exceptions/AutoDeploymentException.php | 1 - app/Exceptions/DisplayException.php | 5 +- app/Exceptions/DisplayValidationException.php | 1 - app/Exceptions/Handler.php | 10 +- app/Exceptions/PterodactylException.php | 1 - .../Repository/RecordNotFoundException.php | 1 - .../Repository/RepositoryException.php | 1 - .../Service/HasActiveServersException.php | 1 - .../Pack/InvalidFileMimeTypeException.php | 1 - .../Pack/InvalidFileUploadException.php | 1 - .../InvalidPackArchiveFormatException.php | 1 - .../Pack/UnreadableZipArchiveException.php | 1 - .../Pack/ZipArchiveCreationException.php | 1 - .../RequiredVariableMissingException.php | 1 - .../InvalidCopyFromException.php | 1 - .../NoParentConfigurationFoundException.php | 1 - .../ReservedVariableNameException.php | 1 - app/Extensions/DynamicDatabaseConnection.php | 6 +- app/Extensions/PhraseAppTranslator.php | 8 +- .../API/Admin/LocationController.php | 2 +- .../Controllers/API/Admin/NodeController.php | 16 +-- .../API/Admin/ServerController.php | 40 +++--- .../API/Admin/ServiceController.php | 6 +- .../Controllers/API/Admin/UserController.php | 16 +-- .../Controllers/API/User/CoreController.php | 2 +- .../Controllers/API/User/ServerController.php | 12 +- app/Http/Controllers/Admin/BaseController.php | 2 +- .../Controllers/Admin/DatabaseController.php | 10 +- .../Controllers/Admin/LocationController.php | 16 +-- .../Controllers/Admin/NodesController.php | 36 ++--- .../Controllers/Admin/OptionController.php | 16 +-- app/Http/Controllers/Admin/PackController.php | 14 +- .../Controllers/Admin/ServersController.php | 59 ++++---- .../Controllers/Admin/ServiceController.php | 16 +-- app/Http/Controllers/Admin/UserController.php | 28 ++-- .../Controllers/Admin/VariableController.php | 16 +-- .../Auth/ForgotPasswordController.php | 4 +- app/Http/Controllers/Auth/LoginController.php | 10 +- .../Controllers/Auth/RegisterController.php | 6 +- .../Auth/ResetPasswordController.php | 2 - app/Http/Controllers/Base/APIController.php | 8 +- .../Controllers/Base/AccountController.php | 4 +- app/Http/Controllers/Base/IndexController.php | 11 +- .../Controllers/Base/LanguageController.php | 4 +- .../Controllers/Base/SecurityController.php | 12 +- .../Controllers/Daemon/ActionController.php | 8 +- .../Controllers/Daemon/PackController.php | 12 +- .../Controllers/Daemon/ServiceController.php | 10 +- .../Controllers/Server/AjaxController.php | 16 +-- .../Controllers/Server/ServerController.php | 52 +++---- .../Controllers/Server/SubuserController.php | 30 ++-- .../Controllers/Server/TaskController.php | 24 ++-- app/Http/Middleware/AdminAuthenticate.php | 7 +- app/Http/Middleware/Authenticate.php | 7 +- app/Http/Middleware/CheckServer.php | 4 +- app/Http/Middleware/DaemonAuthenticate.php | 7 +- app/Http/Middleware/EncryptCookies.php | 1 - app/Http/Middleware/HMACAuthorization.php | 9 +- app/Http/Middleware/LanguageMiddleware.php | 4 +- .../Middleware/RedirectIfAuthenticated.php | 6 +- app/Http/Middleware/VerifyReCaptcha.php | 4 +- app/Http/Requests/Admin/AdminFormRequest.php | 2 +- app/Http/Requests/Request.php | 1 - app/Jobs/SendScheduledTask.php | 7 +- app/Models/Allocation.php | 4 +- app/Models/Node.php | 4 +- app/Models/Pack.php | 2 +- app/Models/Permission.php | 12 +- app/Models/Server.php | 14 +- app/Models/Service.php | 6 +- app/Models/User.php | 18 ++- app/Notifications/AccountCreated.php | 7 +- app/Notifications/AddedToServer.php | 7 +- app/Notifications/RemovedFromServer.php | 7 +- app/Notifications/SendPasswordReset.php | 7 +- app/Notifications/ServerCreated.php | 7 +- app/Observers/ServerObserver.php | 24 ++-- app/Observers/SubuserObserver.php | 12 +- app/Observers/UserObserver.php | 12 +- app/Policies/APIKeyPolicy.php | 6 +- app/Policies/ServerPolicy.php | 6 +- app/Providers/AppServiceProvider.php | 4 - app/Providers/AuthServiceProvider.php | 3 +- app/Providers/BroadcastServiceProvider.php | 2 - app/Providers/EventServiceProvider.php | 2 - app/Providers/MacroServiceProvider.php | 4 +- .../PhraseAppTranslationProvider.php | 2 - app/Providers/RouteServiceProvider.php | 4 - app/Repositories/Concerns/Searchable.php | 2 +- .../Eloquent/DatabaseRepository.php | 20 ++- .../Eloquent/EloquentRepository.php | 4 +- app/Repositories/Eloquent/NodeRepository.php | 5 +- app/Repositories/Eloquent/UserRepository.php | 3 +- app/Repositories/Old/SubuserRepository.php | 12 +- app/Repositories/Repository.php | 7 +- .../old_Daemon/CommandRepository.php | 7 +- .../old_Daemon/FileRepository.php | 11 +- .../old_Daemon/PowerRepository.php | 15 +- .../Allocations/AssignmentService.php | 4 +- app/Services/Api/KeyService.php | 8 +- app/Services/Api/PermissionService.php | 4 +- app/Services/Components/UuidService.php | 12 +- app/Services/Database/DatabaseHostService.php | 8 +- .../Database/DatabaseManagementService.php | 25 ++-- .../Helpers/TemporaryPasswordService.php | 2 +- app/Services/LocationService.php | 8 +- app/Services/Nodes/CreationService.php | 2 +- app/Services/Nodes/UpdateService.php | 4 +- app/Services/Old/APILogService.php | 7 +- app/Services/Old/DeploymentService.php | 19 +-- app/Services/Old/VersionService.php | 2 - app/Services/Packs/ExportPackService.php | 4 +- app/Services/Packs/PackCreationService.php | 7 +- app/Services/Packs/PackUpdateService.php | 4 +- app/Services/Packs/TemplateUploadService.php | 8 +- .../Servers/BuildModificationService.php | 10 +- app/Services/Servers/CreationService.php | 2 +- app/Services/Servers/DeletionService.php | 2 +- .../Servers/DetailsModificationService.php | 4 +- app/Services/Servers/EnvironmentService.php | 6 +- app/Services/Servers/ReinstallService.php | 2 +- .../Servers/StartupModificationService.php | 6 +- app/Services/Servers/SuspensionService.php | 7 +- .../Servers/UsernameGenerationService.php | 4 +- .../Servers/VariableValidatorService.php | 6 +- .../Options/InstallScriptUpdateService.php | 4 +- .../Options/OptionCreationService.php | 2 +- .../Options/OptionDeletionService.php | 2 +- .../Services/Options/OptionUpdateService.php | 4 +- .../Services/ServiceCreationService.php | 2 +- .../Services/ServiceDeletionService.php | 2 +- .../Services/ServiceUpdateService.php | 4 +- .../Variables/VariableCreationService.php | 7 +- .../Variables/VariableUpdateService.php | 4 +- app/Services/Users/CreationService.php | 2 +- app/Services/Users/UpdateService.php | 4 +- .../Admin/AllocationTransformer.php | 5 +- .../Admin/LocationTransformer.php | 3 +- app/Transformers/Admin/NodeTransformer.php | 3 +- app/Transformers/Admin/OptionTransformer.php | 3 +- app/Transformers/Admin/PackTransformer.php | 3 +- app/Transformers/Admin/ServerTransformer.php | 3 +- .../Admin/ServerVariableTransformer.php | 3 +- app/Transformers/Admin/ServiceTransformer.php | 3 +- .../Admin/ServiceVariableTransformer.php | 3 +- app/Transformers/Admin/SubuserTransformer.php | 3 +- app/Transformers/Admin/UserTransformer.php | 3 +- .../User/AllocationTransformer.php | 2 - app/helpers.php | 4 +- config/app.php | 86 ++++++------ config/auth.php | 2 - config/broadcasting.php | 4 - config/cache.php | 4 - config/compile.php | 4 - config/database.php | 20 ++- config/debugbar.php | 46 +++---- config/filesystems.php | 4 - config/javascript.php | 2 - config/laravel-fractal.php | 2 - config/laroute.php | 6 +- config/mail.php | 2 - config/prologue/alerts.php | 2 - config/pterodactyl.php | 1 - config/queue.php | 10 +- config/recaptcha.php | 2 - config/services.php | 4 +- config/session.php | 2 - config/settings.php | 5 - config/themes.php | 6 +- config/trustedproxy.php | 7 +- config/view.php | 2 - ...016_01_23_195641_add_allocations_table.php | 4 - .../2016_01_23_195851_add_api_keys.php | 4 - .../2016_01_23_200044_add_api_permissions.php | 4 - .../2016_01_23_200159_add_downloads.php | 4 - ..._01_23_200421_create_failed_jobs_table.php | 4 - .../2016_01_23_200440_create_jobs_table.php | 4 - .../2016_01_23_200528_add_locations.php | 4 - .../2016_01_23_200648_add_nodes.php | 4 - .../2016_01_23_201433_add_password_resets.php | 4 - .../2016_01_23_201531_add_permissions.php | 4 - ...2016_01_23_201649_add_server_variables.php | 4 - .../2016_01_23_201748_add_servers.php | 4 - .../2016_01_23_202544_add_service_options.php | 4 - ...2016_01_23_202731_add_service_varibles.php | 4 - .../2016_01_23_202943_add_services.php | 4 - ...016_01_23_203119_create_settings_table.php | 4 - .../2016_01_23_203150_add_subusers.php | 4 - .../2016_01_23_203159_add_users.php | 4 - ...016_01_23_203947_create_sessions_table.php | 4 - ...01_25_234418_rename_permissions_column.php | 5 - ...2016_02_07_172148_add_databases_tables.php | 4 - ...2_07_181319_add_database_servers_table.php | 4 - ...306_add_service_option_default_startup.php | 4 - ..._02_20_155318_add_unique_service_field.php | 4 - .../2016_02_27_163411_add_tasks_table.php | 4 - .../2016_02_27_163447_add_tasks_log_table.php | 4 - ...3_18_155649_add_nullable_field_lastrun.php | 4 - .../2016_08_30_212718_add_ip_alias.php | 4 - ..._08_30_213301_modify_ip_storage_method.php | 4 - ...9_01_193520_add_suspension_for_servers.php | 4 - ...2016_09_01_211924_remove_active_column.php | 4 - ...09_02_190647_add_sftp_password_storage.php | 4 - .../2016_09_04_171338_update_jobs_tables.php | 4 - ..._09_04_172028_update_failed_jobs_table.php | 4 - ...9_04_182835_create_notifications_table.php | 4 - ...016_09_07_163017_add_unique_identifier.php | 4 - ..._09_14_145945_allow_longer_regex_field.php | 4 - ...6_09_17_194246_add_docker_image_column.php | 4 - ...9_21_165554_update_servers_column_name.php | 4 - ..._09_29_213518_rename_double_insurgency.php | 5 - .../2016_10_07_152117_build_api_log_table.php | 4 - .../2016_10_14_164802_update_api_keys.php | 4 - ...16_10_23_181719_update_misnamed_bungee.php | 4 - ..._10_23_193810_add_foreign_keys_servers.php | 4 - ...6_10_23_201624_add_foreign_allocations.php | 4 - ...2016_10_23_202222_add_foreign_api_keys.php | 4 - ..._23_202703_add_foreign_api_permissions.php | 4 - ...23_202953_add_foreign_database_servers.php | 4 - ...016_10_23_203105_add_foreign_databases.php | 4 - .../2016_10_23_203335_add_foreign_nodes.php | 4 - ...6_10_23_203522_add_foreign_permissions.php | 4 - ...23_203857_add_foreign_server_variables.php | 4 - ..._23_204157_add_foreign_service_options.php | 4 - ...3_204321_add_foreign_service_variables.php | 4 - ...2016_10_23_204454_add_foreign_subusers.php | 4 - .../2016_10_23_204610_add_foreign_tasks.php | 4 - ...04_000949_add_ark_service_option_fixed.php | 4 - .../2016_11_11_220649_add_pack_support.php | 4 - ...6_11_11_231731_set_service_name_unique.php | 4 - .../2016_11_27_142519_add_pack_column.php | 4 - ...1_173018_add_configurable_upload_limit.php | 4 - ...12_02_185206_correct_service_variables.php | 4 - ...7_01_03_150436_fix_misnamed_option_tag.php | 4 - ...create_node_configuration_tokens_table.php | 4 - .../2017_01_12_135449_add_more_user_data.php | 4 - .../2017_02_02_175548_UpdateColumnNames.php | 4 - .../2017_02_03_140948_UpdateNodesTable.php | 4 - .../2017_02_03_155554_RenameColumns.php | 4 - .../2017_02_05_164123_AdjustColumnNames.php | 4 - ...64516_AdjustColumnNamesForServicePacks.php | 4 - ...2_09_174834_SetupPermissionsPivotTable.php | 4 - ...7_02_10_171858_UpdateAPIKeyColumnNames.php | 4 - ...3_224254_UpdateNodeConfigTokensColumns.php | 4 - ...5_212803_DeleteServiceExecutableOption.php | 4 - ..._10_162934_AddNewServiceOptionsColumns.php | 4 - ...03_10_173607_MigrateToNewServiceSystem.php | 4 - ..._ChangeServiceVariablesValidationRules.php | 4 - ...150648_MoveFunctionsFromFileToDatabase.php | 4 - ...5631_RenameServicePacksToSingluarPacks.php | 4 - ...17_03_14_200326_AddLockedStatusToTable.php | 4 - ...eOrganizeDatabaseServersToDatabaseHost.php | 4 - ..._03_16_181515_CleanupDatabasesDatabase.php | 4 - ...2017_03_18_204953_AddForeignKeyToPacks.php | 4 - ...3_31_221948_AddServerDescriptionColumn.php | 4 - ..._163232_DropDeletedAtColumnFromServers.php | 4 - .../2017_04_15_125021_UpgradeTaskSystem.php | 4 - ...4_20_171943_AddScriptsToServiceOptions.php | 4 - ...1432_AddServiceScriptTrackingToServers.php | 4 - ...7_04_27_145300_AddCopyScriptFromColumn.php | 4 - ...ConnectionOverSSLWithDaemonBehindProxy.php | 4 - .../2017_05_01_141528_DeleteDownloadTable.php | 4 - ...01_141559_DeleteNodeConfigurationTable.php | 4 - ..._06_10_152951_add_external_id_to_users.php | 4 - ...23_ChangeForeignKeyToBeOnCascadeDelete.php | 4 - ...eUserPermissionsToDeleteOnUserDeletion.php | 4 - ...llocationToReferenceNullOnServerDelete.php | 4 - ...DeletionWhenAServerOrVariableIsDeleted.php | 4 - ...33_DeleteTaskWhenParentServerIsDeleted.php | 4 - ...ValuesForDatabaseHostWhenNodeIsDeleted.php | 4 - ...4_AllowNegativeValuesForOverallocation.php | 4 - ...SetAllocationUnqiueUsingMultipleFields.php | 4 - ...adeDeletionWhenAParentServiceIsDeleted.php | 4 - ...vePackWhenParentServiceOptionIsDeleted.php | 4 - database/seeds/DatabaseSeeder.php | 2 - .../seeds/MinecraftServiceTableSeeder.php | 2 - database/seeds/RustServiceTableSeeder.php | 2 - database/seeds/SourceServiceTableSeeder.php | 2 - database/seeds/TerrariaServiceTableSeeder.php | 2 - database/seeds/VoiceServiceTableSeeder.php | 2 - resources/lang/en/pagination.php | 4 +- resources/lang/en/validation.php | 128 +++++++++--------- tests/Unit/Services/Api/KeyServiceTest.php | 9 +- .../DatabaseManagementServiceTest.php | 45 ++++-- .../Unit/Services/Nodes/UpdateServiceTest.php | 3 +- .../Services/Packs/PackUpdateServiceTest.php | 4 +- .../Packs/TemplateUploadServiceTest.php | 2 +- .../Servers/ContainerRebuildServiceTest.php | 3 +- .../DetailsModificationServiceTest.php | 6 +- .../Services/Servers/ReinstallServiceTest.php | 3 +- .../Servers/SuspensionServiceTest.php | 3 +- .../Services/Users/DeletionServiceTest.php | 4 +- 346 files changed, 834 insertions(+), 1424 deletions(-) diff --git a/.php_cs b/.php_cs index c1e6da7e..aca934c8 100644 --- a/.php_cs +++ b/.php_cs @@ -31,6 +31,7 @@ return PhpCsFixer\Config::create() 'no_unreachable_default_argument_value' => true, 'no_useless_return' => true, 'not_operator_with_successor_space' => true, + 'phpdoc_align' => ['tags' => ['param']], 'phpdoc_separation' => false, 'protected_to_private' => false, 'psr0' => ['dir' => 'app'], diff --git a/app/Console/Commands/AddLocation.php b/app/Console/Commands/AddLocation.php index 7d14cf0e..80338a33 100644 --- a/app/Console/Commands/AddLocation.php +++ b/app/Console/Commands/AddLocation.php @@ -49,8 +49,6 @@ class AddLocation extends Command /** * Create a new command instance. - * - * @return void */ public function __construct() { diff --git a/app/Console/Commands/AddNode.php b/app/Console/Commands/AddNode.php index 0aac540c..9f31527e 100644 --- a/app/Console/Commands/AddNode.php +++ b/app/Console/Commands/AddNode.php @@ -57,8 +57,6 @@ class AddNode extends Command /** * Create a new command instance. - * - * @return void */ public function __construct() { diff --git a/app/Console/Commands/CleanServiceBackup.php b/app/Console/Commands/CleanServiceBackup.php index 0a6a3e27..bb56ab7f 100644 --- a/app/Console/Commands/CleanServiceBackup.php +++ b/app/Console/Commands/CleanServiceBackup.php @@ -46,8 +46,6 @@ class CleanServiceBackup extends Command /** * Create a new command instance. - * - * @return void */ public function __construct() { diff --git a/app/Console/Commands/ClearServices.php b/app/Console/Commands/ClearServices.php index db62d268..c0438b36 100644 --- a/app/Console/Commands/ClearServices.php +++ b/app/Console/Commands/ClearServices.php @@ -45,8 +45,6 @@ class ClearServices extends Command /** * Create a new command instance. - * - * @return void */ public function __construct() { diff --git a/app/Console/Commands/ClearTasks.php b/app/Console/Commands/ClearTasks.php index 569caf02..027f9915 100644 --- a/app/Console/Commands/ClearTasks.php +++ b/app/Console/Commands/ClearTasks.php @@ -49,8 +49,6 @@ class ClearTasks extends Command /** * Create a new command instance. - * - * @return void */ public function __construct() { diff --git a/app/Console/Commands/MakeUser.php b/app/Console/Commands/MakeUser.php index 81038cb4..a4c0d442 100644 --- a/app/Console/Commands/MakeUser.php +++ b/app/Console/Commands/MakeUser.php @@ -51,8 +51,6 @@ class MakeUser extends Command /** * Create a new command instance. - * - * @return void */ public function __construct() { diff --git a/app/Console/Commands/RebuildServer.php b/app/Console/Commands/RebuildServer.php index 2177b112..c0e36535 100644 --- a/app/Console/Commands/RebuildServer.php +++ b/app/Console/Commands/RebuildServer.php @@ -49,8 +49,6 @@ class RebuildServer extends Command /** * Create a new command instance. - * - * @return void */ public function __construct() { diff --git a/app/Console/Commands/RunTasks.php b/app/Console/Commands/RunTasks.php index 6c3ccc6c..9b1bff25 100644 --- a/app/Console/Commands/RunTasks.php +++ b/app/Console/Commands/RunTasks.php @@ -50,8 +50,6 @@ class RunTasks extends Command /** * Create a new command instance. - * - * @return void */ public function __construct() { diff --git a/app/Console/Commands/ShowVersion.php b/app/Console/Commands/ShowVersion.php index 199a905b..a5a91bfa 100644 --- a/app/Console/Commands/ShowVersion.php +++ b/app/Console/Commands/ShowVersion.php @@ -45,8 +45,6 @@ class ShowVersion extends Command /** * Create a new command instance. - * - * @return void */ public function __construct() { diff --git a/app/Console/Commands/UpdateEmailSettings.php b/app/Console/Commands/UpdateEmailSettings.php index 1e316d6c..93d28dfa 100644 --- a/app/Console/Commands/UpdateEmailSettings.php +++ b/app/Console/Commands/UpdateEmailSettings.php @@ -51,8 +51,6 @@ class UpdateEmailSettings extends Command /** * Create a new command instance. - * - * @return void */ public function __construct() { diff --git a/app/Console/Commands/UpdateEnvironment.php b/app/Console/Commands/UpdateEnvironment.php index 6252a282..5ee66390 100644 --- a/app/Console/Commands/UpdateEnvironment.php +++ b/app/Console/Commands/UpdateEnvironment.php @@ -55,8 +55,6 @@ class UpdateEnvironment extends Command /** * Create a new command instance. - * - * @return void */ public function __construct() { diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index bba5bb66..e5fa22ee 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -30,8 +30,7 @@ class Kernel extends ConsoleKernel /** * Define the application's command schedule. * - * @param \Illuminate\Console\Scheduling\Schedule $schedule - * @return void + * @param \Illuminate\Console\Scheduling\Schedule $schedule */ protected function schedule(Schedule $schedule) { diff --git a/app/Contracts/Criteria/CriteriaInterface.php b/app/Contracts/Criteria/CriteriaInterface.php index dba7a688..8dc3599f 100644 --- a/app/Contracts/Criteria/CriteriaInterface.php +++ b/app/Contracts/Criteria/CriteriaInterface.php @@ -31,8 +31,8 @@ interface CriteriaInterface /** * Apply selected criteria to a repository call. * - * @param \Illuminate\Database\Eloquent\Model $model - * @param \Pterodactyl\Repositories\Repository $repository + * @param \Illuminate\Database\Eloquent\Model $model + * @param \Pterodactyl\Repositories\Repository $repository * @return mixed */ public function apply($model, Repository $repository); diff --git a/app/Contracts/Repository/AllocationRepositoryInterface.php b/app/Contracts/Repository/AllocationRepositoryInterface.php index e2c2abb1..ec54de97 100644 --- a/app/Contracts/Repository/AllocationRepositoryInterface.php +++ b/app/Contracts/Repository/AllocationRepositoryInterface.php @@ -29,8 +29,8 @@ interface AllocationRepositoryInterface extends RepositoryInterface /** * Set an array of allocation IDs to be assigned to a specific server. * - * @param int|null $server - * @param array $ids + * @param int|null $server + * @param array $ids * @return int */ public function assignAllocationsToServer($server, array $ids); @@ -38,7 +38,7 @@ interface AllocationRepositoryInterface extends RepositoryInterface /** * Return all of the allocations for a specific node. * - * @param int $node + * @param int $node * @return \Illuminate\Database\Eloquent\Collection */ public function getAllocationsForNode($node); diff --git a/app/Contracts/Repository/ApiKeyRepositoryInterface.php b/app/Contracts/Repository/ApiKeyRepositoryInterface.php index bfd44e92..613a629e 100644 --- a/app/Contracts/Repository/ApiKeyRepositoryInterface.php +++ b/app/Contracts/Repository/ApiKeyRepositoryInterface.php @@ -26,5 +26,4 @@ namespace Pterodactyl\Contracts\Repository; interface ApiKeyRepositoryInterface extends RepositoryInterface { - // } diff --git a/app/Contracts/Repository/ApiPermissionRepositoryInterface.php b/app/Contracts/Repository/ApiPermissionRepositoryInterface.php index f0556b45..4e549297 100644 --- a/app/Contracts/Repository/ApiPermissionRepositoryInterface.php +++ b/app/Contracts/Repository/ApiPermissionRepositoryInterface.php @@ -26,5 +26,4 @@ namespace Pterodactyl\Contracts\Repository; interface ApiPermissionRepositoryInterface extends RepositoryInterface { - // } diff --git a/app/Contracts/Repository/Attributes/SearchableInterface.php b/app/Contracts/Repository/Attributes/SearchableInterface.php index 60ca52b9..d33c1e41 100644 --- a/app/Contracts/Repository/Attributes/SearchableInterface.php +++ b/app/Contracts/Repository/Attributes/SearchableInterface.php @@ -29,7 +29,7 @@ interface SearchableInterface /** * Filter results by search term. * - * @param string $term + * @param string $term * @return $this */ public function search($term); diff --git a/app/Contracts/Repository/Daemon/BaseRepositoryInterface.php b/app/Contracts/Repository/Daemon/BaseRepositoryInterface.php index 490f38a4..a18924f9 100644 --- a/app/Contracts/Repository/Daemon/BaseRepositoryInterface.php +++ b/app/Contracts/Repository/Daemon/BaseRepositoryInterface.php @@ -29,7 +29,7 @@ interface BaseRepositoryInterface /** * Set the node model to be used for this daemon connection. * - * @param int $id + * @param int $id * @return $this */ public function setNode($id); @@ -44,7 +44,7 @@ interface BaseRepositoryInterface /** * Set the UUID for the server to be used in the X-Access-Server header for daemon requests. * - * @param null|string $server + * @param null|string $server * @return $this */ public function setAccessServer($server = null); @@ -59,7 +59,7 @@ interface BaseRepositoryInterface /** * Set the token to be used in the X-Access-Token header for requests to the daemon. * - * @param null|string $token + * @param null|string $token * @return $this */ public function setAccessToken($token = null); @@ -74,7 +74,7 @@ interface BaseRepositoryInterface /** * Return an instance of the Guzzle HTTP Client to be used for requests. * - * @param array $headers + * @param array $headers * @return \GuzzleHttp\Client */ public function getHttpClient($headers = []); diff --git a/app/Contracts/Repository/Daemon/ConfigurationRepositoryInterface.php b/app/Contracts/Repository/Daemon/ConfigurationRepositoryInterface.php index c56dde57..0d4421f8 100644 --- a/app/Contracts/Repository/Daemon/ConfigurationRepositoryInterface.php +++ b/app/Contracts/Repository/Daemon/ConfigurationRepositoryInterface.php @@ -29,7 +29,7 @@ interface ConfigurationRepositoryInterface extends BaseRepositoryInterface /** * Update the configuration details for the specified node using data from the database. * - * @param array $overrides + * @param array $overrides * @return \Psr\Http\Message\ResponseInterface */ public function update(array $overrides = []); diff --git a/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php b/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php index a0cfc8e2..de9fe0c3 100644 --- a/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php +++ b/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php @@ -29,9 +29,9 @@ interface ServerRepositoryInterface extends BaseRepositoryInterface /** * Create a new server on the daemon for the panel. * - * @param int $id - * @param array $overrides - * @param bool $start + * @param int $id + * @param array $overrides + * @param bool $start * @return \Psr\Http\Message\ResponseInterface */ public function create($id, $overrides = [], $start = false); @@ -39,7 +39,7 @@ interface ServerRepositoryInterface extends BaseRepositoryInterface /** * Update server details on the daemon. * - * @param array $data + * @param array $data * @return \Psr\Http\Message\ResponseInterface */ public function update(array $data); @@ -47,7 +47,7 @@ interface ServerRepositoryInterface extends BaseRepositoryInterface /** * Mark a server to be reinstalled on the system. * - * @param array|null $data + * @param array|null $data * @return \Psr\Http\Message\ResponseInterface */ public function reinstall($data = null); diff --git a/app/Contracts/Repository/DatabaseHostRepositoryInterface.php b/app/Contracts/Repository/DatabaseHostRepositoryInterface.php index 3abd9be8..59ff2405 100644 --- a/app/Contracts/Repository/DatabaseHostRepositoryInterface.php +++ b/app/Contracts/Repository/DatabaseHostRepositoryInterface.php @@ -36,7 +36,7 @@ interface DatabaseHostRepositoryInterface extends RepositoryInterface /** * Return a database host with the databases and associated servers that are attached to said databases. * - * @param int $id + * @param int $id * @return mixed * * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException @@ -46,7 +46,7 @@ interface DatabaseHostRepositoryInterface extends RepositoryInterface /** * Delete a database host from the DB if there are no databases using it. * - * @param int $id + * @param int $id * @return bool|null * * @throws \Pterodactyl\Exceptions\DisplayException diff --git a/app/Contracts/Repository/DatabaseRepositoryInterface.php b/app/Contracts/Repository/DatabaseRepositoryInterface.php index 1f49a54d..97a2c7d8 100644 --- a/app/Contracts/Repository/DatabaseRepositoryInterface.php +++ b/app/Contracts/Repository/DatabaseRepositoryInterface.php @@ -30,7 +30,7 @@ interface DatabaseRepositoryInterface extends RepositoryInterface * Create a new database if it does not already exist on the host with * the provided details. * - * @param array $data + * @param array $data * @return mixed * * @throws \Pterodactyl\Exceptions\DisplayException @@ -41,8 +41,8 @@ interface DatabaseRepositoryInterface extends RepositoryInterface /** * Create a new database on a given connection. * - * @param string $database - * @param null|string $connection + * @param string $database + * @param null|string $connection * @return bool */ public function createDatabase($database, $connection = null); @@ -50,10 +50,10 @@ interface DatabaseRepositoryInterface extends RepositoryInterface /** * Create a new database user on a given connection. * - * @param string $username - * @param string $remote - * @param string $password - * @param null|string $connection + * @param string $username + * @param string $remote + * @param string $password + * @param null|string $connection * @return bool */ public function createUser($username, $remote, $password, $connection = null); @@ -61,10 +61,10 @@ interface DatabaseRepositoryInterface extends RepositoryInterface /** * Give a specific user access to a given database. * - * @param string $database - * @param string $username - * @param string $remote - * @param null|string $connection + * @param string $database + * @param string $username + * @param string $remote + * @param null|string $connection * @return bool */ public function assignUserToDatabase($database, $username, $remote, $connection = null); @@ -72,7 +72,7 @@ interface DatabaseRepositoryInterface extends RepositoryInterface /** * Flush the privileges for a given connection. * - * @param null|string $connection + * @param null|string $connection * @return mixed */ public function flush($connection = null); @@ -80,8 +80,8 @@ interface DatabaseRepositoryInterface extends RepositoryInterface /** * Drop a given database on a specific connection. * - * @param string $database - * @param null|string $connection + * @param string $database + * @param null|string $connection * @return bool */ public function dropDatabase($database, $connection = null); @@ -89,9 +89,9 @@ interface DatabaseRepositoryInterface extends RepositoryInterface /** * Drop a given user on a specific connection. * - * @param string $username - * @param string $remote - * @param null|string $connection + * @param string $username + * @param string $remote + * @param null|string $connection * @return mixed */ public function dropUser($username, $remote, $connection = null); diff --git a/app/Contracts/Repository/LocationRepositoryInterface.php b/app/Contracts/Repository/LocationRepositoryInterface.php index 10ba143e..600c41c1 100644 --- a/app/Contracts/Repository/LocationRepositoryInterface.php +++ b/app/Contracts/Repository/LocationRepositoryInterface.php @@ -56,7 +56,7 @@ interface LocationRepositoryInterface extends RepositoryInterface, SearchableInt /** * Return all of the nodes and their respective count of servers for a location. * - * @param int $id + * @param int $id * @return mixed * * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException diff --git a/app/Contracts/Repository/NodeRepositoryInterface.php b/app/Contracts/Repository/NodeRepositoryInterface.php index 51c6540e..fe0449a3 100644 --- a/app/Contracts/Repository/NodeRepositoryInterface.php +++ b/app/Contracts/Repository/NodeRepositoryInterface.php @@ -31,7 +31,7 @@ interface NodeRepositoryInterface extends RepositoryInterface, SearchableInterfa /** * Return the usage stats for a single node. * - * @param int $id + * @param int $id * @return array */ public function getUsageStats($id); @@ -39,7 +39,7 @@ interface NodeRepositoryInterface extends RepositoryInterface, SearchableInterfa /** * Return all available nodes with a searchable interface. * - * @param int $count + * @param int $count * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator */ public function getNodeListingData($count = 25); @@ -47,7 +47,7 @@ interface NodeRepositoryInterface extends RepositoryInterface, SearchableInterfa /** * Return a single node with location and server information. * - * @param int $id + * @param int $id * @return mixed * * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException @@ -57,7 +57,7 @@ interface NodeRepositoryInterface extends RepositoryInterface, SearchableInterfa /** * Return a node with all of the associated allocations and servers that are attached to said allocations. * - * @param int $id + * @param int $id * @return mixed * * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException @@ -67,7 +67,7 @@ interface NodeRepositoryInterface extends RepositoryInterface, SearchableInterfa /** * Return a node with all of the servers attached to that node. * - * @param int $id + * @param int $id * @return mixed * * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException @@ -77,7 +77,7 @@ interface NodeRepositoryInterface extends RepositoryInterface, SearchableInterfa /** * Return a collection of nodes beloning to a specific location for use on frontend display. * - * @param int $location + * @param int $location * @return mixed */ public function getNodesForLocation($location); diff --git a/app/Contracts/Repository/OptionVariableRepositoryInterface.php b/app/Contracts/Repository/OptionVariableRepositoryInterface.php index 794bfc79..7a8f055d 100644 --- a/app/Contracts/Repository/OptionVariableRepositoryInterface.php +++ b/app/Contracts/Repository/OptionVariableRepositoryInterface.php @@ -26,5 +26,4 @@ namespace Pterodactyl\Contracts\Repository; interface OptionVariableRepositoryInterface extends RepositoryInterface { - // } diff --git a/app/Contracts/Repository/PackRepositoryInterface.php b/app/Contracts/Repository/PackRepositoryInterface.php index 5160245c..e644e039 100644 --- a/app/Contracts/Repository/PackRepositoryInterface.php +++ b/app/Contracts/Repository/PackRepositoryInterface.php @@ -39,7 +39,7 @@ interface PackRepositoryInterface extends RepositoryInterface, SearchableInterfa /** * Return a pack with the associated server models attached to it. * - * @param int $id + * @param int $id * @return \Illuminate\Database\Eloquent\Collection * * @throws \Illuminate\Database\Eloquent\ModelNotFoundException @@ -49,8 +49,8 @@ interface PackRepositoryInterface extends RepositoryInterface, SearchableInterfa /** * Return all of the file archives for a given pack. * - * @param int $id - * @param bool $collection + * @param int $id + * @param bool $collection * @return object|\Illuminate\Support\Collection * * @throws \Illuminate\Database\Eloquent\ModelNotFoundException diff --git a/app/Contracts/Repository/RepositoryInterface.php b/app/Contracts/Repository/RepositoryInterface.php index a26646d0..15c3a416 100644 --- a/app/Contracts/Repository/RepositoryInterface.php +++ b/app/Contracts/Repository/RepositoryInterface.php @@ -57,7 +57,7 @@ interface RepositoryInterface /** * An array of columns to filter the response by. * - * @param array $columns + * @param array $columns * @return $this */ public function withColumns($columns = ['*']); @@ -72,8 +72,8 @@ interface RepositoryInterface /** * Create a new model instance and persist it to the database. * - * @param array $fields - * @param bool $validate + * @param array $fields + * @param bool $validate * @return mixed * * @throws \Pterodactyl\Exceptions\Model\DataValidationException @@ -83,7 +83,7 @@ interface RepositoryInterface /** * Delete a given record from the database. * - * @param int $id + * @param int $id * @return int */ public function delete($id); @@ -91,7 +91,7 @@ interface RepositoryInterface /** * Delete records matching the given attributes. * - * @param array $attributes + * @param array $attributes * @return int */ public function deleteWhere(array $attributes); @@ -99,7 +99,7 @@ interface RepositoryInterface /** * Find a model that has the specific ID passed. * - * @param int $id + * @param int $id * @return mixed * * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException @@ -109,7 +109,7 @@ interface RepositoryInterface /** * Find a model matching an array of where clauses. * - * @param array $fields + * @param array $fields * @return mixed * * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException @@ -119,7 +119,7 @@ interface RepositoryInterface /** * Find and return the first matching instance for the given fields. * - * @param array $fields + * @param array $fields * @return mixed */ public function findFirstWhere(array $fields); @@ -127,7 +127,7 @@ interface RepositoryInterface /** * Return a count of records matching the passed arguments. * - * @param array $fields + * @param array $fields * @return int */ public function findCountWhere(array $fields); @@ -135,10 +135,10 @@ interface RepositoryInterface /** * Update a given ID with the passed array of fields. * - * @param int $id - * @param array $fields - * @param bool $validate - * @param bool $force + * @param int $id + * @param array $fields + * @param bool $validate + * @param bool $force * @return mixed * * @throws \Pterodactyl\Exceptions\Model\DataValidationException @@ -150,9 +150,9 @@ interface RepositoryInterface * Perform a mass update where matching records are updated using whereIn. * This does not perform any model data validation. * - * @param string $column - * @param array $values - * @param array $fields + * @param string $column + * @param array $values + * @param array $fields * @return int */ public function updateWhereIn($column, array $values, array $fields); @@ -160,10 +160,10 @@ interface RepositoryInterface /** * Update a record if it exists in the database, otherwise create it. * - * @param array $where - * @param array $fields - * @param bool $validate - * @param bool $force + * @param array $where + * @param array $fields + * @param bool $validate + * @param bool $force * @return mixed * * @throws \Pterodactyl\Exceptions\Model\DataValidationException @@ -173,8 +173,8 @@ interface RepositoryInterface /** * Update multiple records matching the passed clauses. * - * @param array $where - * @param array $fields + * @param array $where + * @param array $fields * @return mixed */ public function massUpdate(array $where, array $fields); @@ -190,7 +190,7 @@ interface RepositoryInterface * Insert a single or multiple records into the database at once skipping * validation and mass assignment checking. * - * @param array $data + * @param array $data * @return bool */ public function insert(array $data); @@ -198,7 +198,7 @@ interface RepositoryInterface /** * Insert multiple records into the database and ignore duplicates. * - * @param array $values + * @param array $values * @return bool */ public function insertIgnore(array $values); diff --git a/app/Contracts/Repository/ServerRepositoryInterface.php b/app/Contracts/Repository/ServerRepositoryInterface.php index 4b9d3b96..d71a349a 100644 --- a/app/Contracts/Repository/ServerRepositoryInterface.php +++ b/app/Contracts/Repository/ServerRepositoryInterface.php @@ -31,7 +31,7 @@ interface ServerRepositoryInterface extends RepositoryInterface, SearchableInter /** * Returns a listing of all servers that exist including relationships. * - * @param int $paginate + * @param int $paginate * @return mixed */ public function getAllServers($paginate); @@ -39,7 +39,7 @@ interface ServerRepositoryInterface extends RepositoryInterface, SearchableInter /** * Return a server model and all variables associated with the server. * - * @param int $id + * @param int $id * @return mixed * * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException @@ -50,8 +50,8 @@ interface ServerRepositoryInterface extends RepositoryInterface, SearchableInter * Return all of the server variables possible and default to the variable * default if there is no value defined for the specific server requested. * - * @param int $id - * @param bool $returnAsObject + * @param int $id + * @param bool $returnAsObject * @return array|object * * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException @@ -71,7 +71,7 @@ interface ServerRepositoryInterface extends RepositoryInterface, SearchableInter /** * Return a server as well as associated databases and their hosts. * - * @param int $id + * @param int $id * @return mixed * * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException @@ -81,7 +81,7 @@ interface ServerRepositoryInterface extends RepositoryInterface, SearchableInter /** * Return data about the daemon service in a consumable format. * - * @param int $id + * @param int $id * @return array * * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException diff --git a/app/Contracts/Repository/ServerVariableRepositoryInterface.php b/app/Contracts/Repository/ServerVariableRepositoryInterface.php index b0ca226c..dc5b761c 100644 --- a/app/Contracts/Repository/ServerVariableRepositoryInterface.php +++ b/app/Contracts/Repository/ServerVariableRepositoryInterface.php @@ -26,5 +26,4 @@ namespace Pterodactyl\Contracts\Repository; interface ServerVariableRepositoryInterface extends RepositoryInterface { - // } diff --git a/app/Contracts/Repository/ServiceOptionRepositoryInterface.php b/app/Contracts/Repository/ServiceOptionRepositoryInterface.php index e662c7b1..84ce0bea 100644 --- a/app/Contracts/Repository/ServiceOptionRepositoryInterface.php +++ b/app/Contracts/Repository/ServiceOptionRepositoryInterface.php @@ -29,7 +29,7 @@ interface ServiceOptionRepositoryInterface extends RepositoryInterface /** * Return a service option with the variables relation attached. * - * @param int $id + * @param int $id * @return mixed */ public function getWithVariables($id); @@ -37,7 +37,7 @@ interface ServiceOptionRepositoryInterface extends RepositoryInterface /** * Return a service option with the copyFrom relation loaded onto the model. * - * @param int $id + * @param int $id * @return mixed */ public function getWithCopyFrom($id); @@ -45,8 +45,8 @@ interface ServiceOptionRepositoryInterface extends RepositoryInterface /** * Confirm a copy script belongs to the same service as the item trying to use it. * - * @param int $copyFromId - * @param int $service + * @param int $copyFromId + * @param int $service * @return bool */ public function isCopiableScript($copyFromId, $service); diff --git a/app/Contracts/Repository/ServiceRepositoryInterface.php b/app/Contracts/Repository/ServiceRepositoryInterface.php index 7459d1c4..da8543a6 100644 --- a/app/Contracts/Repository/ServiceRepositoryInterface.php +++ b/app/Contracts/Repository/ServiceRepositoryInterface.php @@ -29,7 +29,7 @@ interface ServiceRepositoryInterface extends RepositoryInterface /** * Return a service or all services with their associated options, variables, and packs. * - * @param int $id + * @param int $id * @return \Illuminate\Support\Collection */ public function getWithOptions($id = null); @@ -37,7 +37,7 @@ interface ServiceRepositoryInterface extends RepositoryInterface /** * Return a service along with its associated options and the servers relation on those options. * - * @param int $id + * @param int $id * @return mixed */ public function getWithOptionServers($id); diff --git a/app/Contracts/Repository/ServiceVariableRepositoryInterface.php b/app/Contracts/Repository/ServiceVariableRepositoryInterface.php index bcdc6196..3c975a04 100644 --- a/app/Contracts/Repository/ServiceVariableRepositoryInterface.php +++ b/app/Contracts/Repository/ServiceVariableRepositoryInterface.php @@ -26,5 +26,4 @@ namespace Pterodactyl\Contracts\Repository; interface ServiceVariableRepositoryInterface extends RepositoryInterface { - // } diff --git a/app/Contracts/Repository/UserRepositoryInterface.php b/app/Contracts/Repository/UserRepositoryInterface.php index b35914c0..1638c7dd 100644 --- a/app/Contracts/Repository/UserRepositoryInterface.php +++ b/app/Contracts/Repository/UserRepositoryInterface.php @@ -38,7 +38,7 @@ interface UserRepositoryInterface extends RepositoryInterface, SearchableInterfa /** * Return all matching models for a user in a format that can be used for dropdowns. * - * @param string $query + * @param string $query * @return \Illuminate\Database\Eloquent\Collection */ public function filterUsersByQuery($query); diff --git a/app/Events/Auth/FailedCaptcha.php b/app/Events/Auth/FailedCaptcha.php index 90311726..4915f4c4 100644 --- a/app/Events/Auth/FailedCaptcha.php +++ b/app/Events/Auth/FailedCaptcha.php @@ -47,9 +47,8 @@ class FailedCaptcha /** * Create a new event instance. * - * @param string $ip - * @param string $domain - * @return void + * @param string $ip + * @param string $domain */ public function __construct($ip, $domain) { diff --git a/app/Events/Auth/FailedPasswordReset.php b/app/Events/Auth/FailedPasswordReset.php index ec2de3b4..8a06261e 100644 --- a/app/Events/Auth/FailedPasswordReset.php +++ b/app/Events/Auth/FailedPasswordReset.php @@ -47,9 +47,8 @@ class FailedPasswordReset /** * Create a new event instance. * - * @param string $ip - * @param string $email - * @return void + * @param string $ip + * @param string $email */ public function __construct($ip, $email) { diff --git a/app/Events/Event.php b/app/Events/Event.php index af6056df..2db145df 100644 --- a/app/Events/Event.php +++ b/app/Events/Event.php @@ -4,5 +4,4 @@ namespace Pterodactyl\Events; abstract class Event { - // } diff --git a/app/Events/Server/Created.php b/app/Events/Server/Created.php index 2591cd5a..46b25e0d 100644 --- a/app/Events/Server/Created.php +++ b/app/Events/Server/Created.php @@ -41,8 +41,7 @@ class Created /** * Create a new event instance. * - * @param \Pterodactyl\Models\Server $server - * @return void + * @param \Pterodactyl\Models\Server $server */ public function __construct(Server $server) { diff --git a/app/Events/Server/Creating.php b/app/Events/Server/Creating.php index 46e4898c..79b358c5 100644 --- a/app/Events/Server/Creating.php +++ b/app/Events/Server/Creating.php @@ -41,8 +41,7 @@ class Creating /** * Create a new event instance. * - * @param \Pterodactyl\Models\Server $server - * @return void + * @param \Pterodactyl\Models\Server $server */ public function __construct(Server $server) { diff --git a/app/Events/Server/Deleted.php b/app/Events/Server/Deleted.php index 6f8709b8..18fb674d 100644 --- a/app/Events/Server/Deleted.php +++ b/app/Events/Server/Deleted.php @@ -41,8 +41,7 @@ class Deleted /** * Create a new event instance. * - * @param \Pterodactyl\Models\Server $server - * @return void + * @param \Pterodactyl\Models\Server $server */ public function __construct(Server $server) { diff --git a/app/Events/Server/Deleting.php b/app/Events/Server/Deleting.php index 3152ed96..f75c9184 100644 --- a/app/Events/Server/Deleting.php +++ b/app/Events/Server/Deleting.php @@ -41,8 +41,7 @@ class Deleting /** * Create a new event instance. * - * @param \Pterodactyl\Models\Server $server - * @return void + * @param \Pterodactyl\Models\Server $server */ public function __construct(Server $server) { diff --git a/app/Events/Server/Saved.php b/app/Events/Server/Saved.php index d1965904..91d704b7 100644 --- a/app/Events/Server/Saved.php +++ b/app/Events/Server/Saved.php @@ -41,8 +41,7 @@ class Saved /** * Create a new event instance. * - * @param \Pterodactyl\Models\Server $server - * @return void + * @param \Pterodactyl\Models\Server $server */ public function __construct(Server $server) { diff --git a/app/Events/Server/Saving.php b/app/Events/Server/Saving.php index 1ae31da3..0ce98adc 100644 --- a/app/Events/Server/Saving.php +++ b/app/Events/Server/Saving.php @@ -41,8 +41,7 @@ class Saving /** * Create a new event instance. * - * @param \Pterodactyl\Models\Server $server - * @return void + * @param \Pterodactyl\Models\Server $server */ public function __construct(Server $server) { diff --git a/app/Events/Server/Updated.php b/app/Events/Server/Updated.php index 1284ea50..b0f76820 100644 --- a/app/Events/Server/Updated.php +++ b/app/Events/Server/Updated.php @@ -41,8 +41,7 @@ class Updated /** * Create a new event instance. * - * @param \Pterodactyl\Models\Server $server - * @return void + * @param \Pterodactyl\Models\Server $server */ public function __construct(Server $server) { diff --git a/app/Events/Server/Updating.php b/app/Events/Server/Updating.php index b2fc255c..c0ce6ad9 100644 --- a/app/Events/Server/Updating.php +++ b/app/Events/Server/Updating.php @@ -41,8 +41,7 @@ class Updating /** * Create a new event instance. * - * @param \Pterodactyl\Models\Server $server - * @return void + * @param \Pterodactyl\Models\Server $server */ public function __construct(Server $server) { diff --git a/app/Events/Subuser/Created.php b/app/Events/Subuser/Created.php index 9a2d2853..4e661da6 100644 --- a/app/Events/Subuser/Created.php +++ b/app/Events/Subuser/Created.php @@ -41,8 +41,7 @@ class Created /** * Create a new event instance. * - * @param \Pterodactyl\Models\Subuser $subuser - * @return void + * @param \Pterodactyl\Models\Subuser $subuser */ public function __construct(Subuser $subuser) { diff --git a/app/Events/Subuser/Creating.php b/app/Events/Subuser/Creating.php index 5083c497..17ae1d9e 100644 --- a/app/Events/Subuser/Creating.php +++ b/app/Events/Subuser/Creating.php @@ -41,8 +41,7 @@ class Creating /** * Create a new event instance. * - * @param \Pterodactyl\Models\Subuser $subuser - * @return void + * @param \Pterodactyl\Models\Subuser $subuser */ public function __construct(Subuser $subuser) { diff --git a/app/Events/Subuser/Deleted.php b/app/Events/Subuser/Deleted.php index 1e419ab8..837bfc50 100644 --- a/app/Events/Subuser/Deleted.php +++ b/app/Events/Subuser/Deleted.php @@ -41,8 +41,7 @@ class Deleted /** * Create a new event instance. * - * @param \Pterodactyl\Models\Subuser $subuser - * @return void + * @param \Pterodactyl\Models\Subuser $subuser */ public function __construct(Subuser $subuser) { diff --git a/app/Events/Subuser/Deleting.php b/app/Events/Subuser/Deleting.php index a06ebe2d..7f7ef944 100644 --- a/app/Events/Subuser/Deleting.php +++ b/app/Events/Subuser/Deleting.php @@ -41,8 +41,7 @@ class Deleting /** * Create a new event instance. * - * @param \Pterodactyl\Models\Subuser $subuser - * @return void + * @param \Pterodactyl\Models\Subuser $subuser */ public function __construct(Subuser $subuser) { diff --git a/app/Events/User/Created.php b/app/Events/User/Created.php index 4e1fd9e7..2b614a90 100644 --- a/app/Events/User/Created.php +++ b/app/Events/User/Created.php @@ -41,8 +41,7 @@ class Created /** * Create a new event instance. * - * @param \Pterodactyl\Models\User $user - * @return void + * @param \Pterodactyl\Models\User $user */ public function __construct(User $user) { diff --git a/app/Events/User/Creating.php b/app/Events/User/Creating.php index cc544af9..ca4c517b 100644 --- a/app/Events/User/Creating.php +++ b/app/Events/User/Creating.php @@ -41,8 +41,7 @@ class Creating /** * Create a new event instance. * - * @param \Pterodactyl\Models\User $user - * @return void + * @param \Pterodactyl\Models\User $user */ public function __construct(User $user) { diff --git a/app/Events/User/Deleted.php b/app/Events/User/Deleted.php index a3cc3cff..ce6ca1c9 100644 --- a/app/Events/User/Deleted.php +++ b/app/Events/User/Deleted.php @@ -41,8 +41,7 @@ class Deleted /** * Create a new event instance. * - * @param \Pterodactyl\Models\User $user - * @return void + * @param \Pterodactyl\Models\User $user */ public function __construct(User $user) { diff --git a/app/Events/User/Deleting.php b/app/Events/User/Deleting.php index ad9397e4..581d4e2f 100644 --- a/app/Events/User/Deleting.php +++ b/app/Events/User/Deleting.php @@ -41,8 +41,7 @@ class Deleting /** * Create a new event instance. * - * @param \Pterodactyl\Models\User $user - * @return void + * @param \Pterodactyl\Models\User $user */ public function __construct(User $user) { diff --git a/app/Exceptions/AccountNotFoundException.php b/app/Exceptions/AccountNotFoundException.php index b35bd2fc..f8e36ed4 100644 --- a/app/Exceptions/AccountNotFoundException.php +++ b/app/Exceptions/AccountNotFoundException.php @@ -26,5 +26,4 @@ namespace Pterodactyl\Exceptions; class AccountNotFoundException extends \Exception { - // } diff --git a/app/Exceptions/AutoDeploymentException.php b/app/Exceptions/AutoDeploymentException.php index 109fe109..4e56a008 100644 --- a/app/Exceptions/AutoDeploymentException.php +++ b/app/Exceptions/AutoDeploymentException.php @@ -26,5 +26,4 @@ namespace Pterodactyl\Exceptions; class AutoDeploymentException extends \Exception { - // } diff --git a/app/Exceptions/DisplayException.php b/app/Exceptions/DisplayException.php index 530ad40c..ba88486a 100644 --- a/app/Exceptions/DisplayException.php +++ b/app/Exceptions/DisplayException.php @@ -31,9 +31,8 @@ class DisplayException extends PterodactylException /** * Exception constructor. * - * @param string $message - * @param mixed $log - * @return void + * @param string $message + * @param mixed $log */ public function __construct($message, $log = null) { diff --git a/app/Exceptions/DisplayValidationException.php b/app/Exceptions/DisplayValidationException.php index ae97318a..2c4e586a 100644 --- a/app/Exceptions/DisplayValidationException.php +++ b/app/Exceptions/DisplayValidationException.php @@ -26,5 +26,4 @@ namespace Pterodactyl\Exceptions; class DisplayValidationException extends PterodactylException { - // } diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 4fb28768..31fb975f 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -37,7 +37,7 @@ class Handler extends ExceptionHandler * * This is a great spot to send exceptions to Sentry, Bugsnag, etc. * - * @param \Exception $exception + * @param \Exception $exception * * @throws \Exception */ @@ -49,8 +49,8 @@ class Handler extends ExceptionHandler /** * Render an exception into an HTTP response. * - * @param \Illuminate\Http\Request $request - * @param \Exception $exception + * @param \Illuminate\Http\Request $request + * @param \Exception $exception * @return \Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response * * @throws \Exception @@ -85,8 +85,8 @@ class Handler extends ExceptionHandler /** * Convert an authentication exception into an unauthenticated response. * - * @param \Illuminate\Http\Request $request - * @param \Illuminate\Auth\AuthenticationException $exception + * @param \Illuminate\Http\Request $request + * @param \Illuminate\Auth\AuthenticationException $exception * @return \Illuminate\Http\Response */ protected function unauthenticated($request, AuthenticationException $exception) diff --git a/app/Exceptions/PterodactylException.php b/app/Exceptions/PterodactylException.php index 4ec48d66..4766deb3 100644 --- a/app/Exceptions/PterodactylException.php +++ b/app/Exceptions/PterodactylException.php @@ -26,5 +26,4 @@ namespace Pterodactyl\Exceptions; class PterodactylException extends \Exception { - // } diff --git a/app/Exceptions/Repository/RecordNotFoundException.php b/app/Exceptions/Repository/RecordNotFoundException.php index 1e9ac987..796b4c08 100644 --- a/app/Exceptions/Repository/RecordNotFoundException.php +++ b/app/Exceptions/Repository/RecordNotFoundException.php @@ -26,5 +26,4 @@ namespace Pterodactyl\Exceptions\Repository; class RecordNotFoundException extends \Exception { - // } diff --git a/app/Exceptions/Repository/RepositoryException.php b/app/Exceptions/Repository/RepositoryException.php index b55b6304..07ce6b22 100644 --- a/app/Exceptions/Repository/RepositoryException.php +++ b/app/Exceptions/Repository/RepositoryException.php @@ -26,5 +26,4 @@ namespace Pterodactyl\Exceptions\Repository; class RepositoryException extends \Exception { - // } diff --git a/app/Exceptions/Service/HasActiveServersException.php b/app/Exceptions/Service/HasActiveServersException.php index 09c13c18..fe24a0c0 100644 --- a/app/Exceptions/Service/HasActiveServersException.php +++ b/app/Exceptions/Service/HasActiveServersException.php @@ -28,5 +28,4 @@ use Pterodactyl\Exceptions\DisplayException; class HasActiveServersException extends DisplayException { - // } diff --git a/app/Exceptions/Service/Pack/InvalidFileMimeTypeException.php b/app/Exceptions/Service/Pack/InvalidFileMimeTypeException.php index bbd5d410..dd3b8945 100644 --- a/app/Exceptions/Service/Pack/InvalidFileMimeTypeException.php +++ b/app/Exceptions/Service/Pack/InvalidFileMimeTypeException.php @@ -28,5 +28,4 @@ use Pterodactyl\Exceptions\DisplayException; class InvalidFileMimeTypeException extends DisplayException { - // } diff --git a/app/Exceptions/Service/Pack/InvalidFileUploadException.php b/app/Exceptions/Service/Pack/InvalidFileUploadException.php index 4861512c..719f02e8 100644 --- a/app/Exceptions/Service/Pack/InvalidFileUploadException.php +++ b/app/Exceptions/Service/Pack/InvalidFileUploadException.php @@ -28,5 +28,4 @@ use Pterodactyl\Exceptions\DisplayException; class InvalidFileUploadException extends DisplayException { - // } diff --git a/app/Exceptions/Service/Pack/InvalidPackArchiveFormatException.php b/app/Exceptions/Service/Pack/InvalidPackArchiveFormatException.php index f13a3358..eb57fd4c 100644 --- a/app/Exceptions/Service/Pack/InvalidPackArchiveFormatException.php +++ b/app/Exceptions/Service/Pack/InvalidPackArchiveFormatException.php @@ -28,5 +28,4 @@ use Pterodactyl\Exceptions\DisplayException; class InvalidPackArchiveFormatException extends DisplayException { - // } diff --git a/app/Exceptions/Service/Pack/UnreadableZipArchiveException.php b/app/Exceptions/Service/Pack/UnreadableZipArchiveException.php index a803d158..013b15e2 100644 --- a/app/Exceptions/Service/Pack/UnreadableZipArchiveException.php +++ b/app/Exceptions/Service/Pack/UnreadableZipArchiveException.php @@ -28,5 +28,4 @@ use Pterodactyl\Exceptions\DisplayException; class UnreadableZipArchiveException extends DisplayException { - // } diff --git a/app/Exceptions/Service/Pack/ZipArchiveCreationException.php b/app/Exceptions/Service/Pack/ZipArchiveCreationException.php index 14eb8ce9..30043ee9 100644 --- a/app/Exceptions/Service/Pack/ZipArchiveCreationException.php +++ b/app/Exceptions/Service/Pack/ZipArchiveCreationException.php @@ -26,5 +26,4 @@ namespace Pterodactyl\Exceptions\Service\Pack; class ZipArchiveCreationException extends \Exception { - // } diff --git a/app/Exceptions/Service/Server/RequiredVariableMissingException.php b/app/Exceptions/Service/Server/RequiredVariableMissingException.php index 61ed0197..f026ccce 100644 --- a/app/Exceptions/Service/Server/RequiredVariableMissingException.php +++ b/app/Exceptions/Service/Server/RequiredVariableMissingException.php @@ -28,5 +28,4 @@ use Exception; class RequiredVariableMissingException extends Exception { - // } diff --git a/app/Exceptions/Service/ServiceOption/InvalidCopyFromException.php b/app/Exceptions/Service/ServiceOption/InvalidCopyFromException.php index 8aab35b4..c7ba7790 100644 --- a/app/Exceptions/Service/ServiceOption/InvalidCopyFromException.php +++ b/app/Exceptions/Service/ServiceOption/InvalidCopyFromException.php @@ -26,5 +26,4 @@ namespace Pterodactyl\Exceptions\Service\ServiceOption; class InvalidCopyFromException extends \Exception { - // } diff --git a/app/Exceptions/Service/ServiceOption/NoParentConfigurationFoundException.php b/app/Exceptions/Service/ServiceOption/NoParentConfigurationFoundException.php index c5ec1e72..6a9ff9bc 100644 --- a/app/Exceptions/Service/ServiceOption/NoParentConfigurationFoundException.php +++ b/app/Exceptions/Service/ServiceOption/NoParentConfigurationFoundException.php @@ -26,5 +26,4 @@ namespace Pterodactyl\Exceptions\Service\ServiceOption; class NoParentConfigurationFoundException extends \Exception { - // } diff --git a/app/Exceptions/Service/ServiceVariable/ReservedVariableNameException.php b/app/Exceptions/Service/ServiceVariable/ReservedVariableNameException.php index c5b001be..07769f4c 100644 --- a/app/Exceptions/Service/ServiceVariable/ReservedVariableNameException.php +++ b/app/Exceptions/Service/ServiceVariable/ReservedVariableNameException.php @@ -28,5 +28,4 @@ use Exception; class ReservedVariableNameException extends Exception { - // } diff --git a/app/Extensions/DynamicDatabaseConnection.php b/app/Extensions/DynamicDatabaseConnection.php index 3b5f1247..1e13e231 100644 --- a/app/Extensions/DynamicDatabaseConnection.php +++ b/app/Extensions/DynamicDatabaseConnection.php @@ -70,9 +70,9 @@ class DynamicDatabaseConnection /** * Adds a dynamic database connection entry to the runtime config. * - * @param string $connection - * @param \Pterodactyl\Models\DatabaseHost|int $host - * @param string $database + * @param string $connection + * @param \Pterodactyl\Models\DatabaseHost|int $host + * @param string $database */ public function set($connection, $host, $database = 'mysql') { diff --git a/app/Extensions/PhraseAppTranslator.php b/app/Extensions/PhraseAppTranslator.php index f79cf282..b61344cf 100644 --- a/app/Extensions/PhraseAppTranslator.php +++ b/app/Extensions/PhraseAppTranslator.php @@ -31,10 +31,10 @@ class PhraseAppTranslator extends LaravelTranslator /** * Get the translation for the given key. * - * @param string $key - * @param array $replace - * @param string|null $locale - * @param bool $fallback + * @param string $key + * @param array $replace + * @param string|null $locale + * @param bool $fallback * @return string */ public function get($key, array $replace = [], $locale = null, $fallback = true) diff --git a/app/Http/Controllers/API/Admin/LocationController.php b/app/Http/Controllers/API/Admin/LocationController.php index 41405e91..34230ea7 100644 --- a/app/Http/Controllers/API/Admin/LocationController.php +++ b/app/Http/Controllers/API/Admin/LocationController.php @@ -35,7 +35,7 @@ class LocationController extends Controller /** * Controller to handle returning all locations on the system. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return array */ public function index(Request $request) diff --git a/app/Http/Controllers/API/Admin/NodeController.php b/app/Http/Controllers/API/Admin/NodeController.php index 74784fdb..3c658654 100644 --- a/app/Http/Controllers/API/Admin/NodeController.php +++ b/app/Http/Controllers/API/Admin/NodeController.php @@ -40,7 +40,7 @@ class NodeController extends Controller /** * Controller to handle returning all nodes on the system. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return array */ public function index(Request $request) @@ -63,8 +63,8 @@ class NodeController extends Controller /** * Display information about a single node on the system. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param int $id * @return array */ public function view(Request $request, $id) @@ -84,8 +84,8 @@ class NodeController extends Controller /** * Display information about a single node on the system. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param int $id * @return \Illuminate\Http\JsonResponse */ public function viewConfig(Request $request, $id) @@ -100,7 +100,7 @@ class NodeController extends Controller /** * Create a new node on the system. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\JsonResponse|array */ public function store(Request $request) @@ -146,8 +146,8 @@ class NodeController extends Controller /** * Delete a node from the system. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param int $id * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse */ public function delete(Request $request, $id) diff --git a/app/Http/Controllers/API/Admin/ServerController.php b/app/Http/Controllers/API/Admin/ServerController.php index 28cb4064..409cf6d6 100644 --- a/app/Http/Controllers/API/Admin/ServerController.php +++ b/app/Http/Controllers/API/Admin/ServerController.php @@ -41,7 +41,7 @@ class ServerController extends Controller /** * Controller to handle returning all servers on the system. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return array */ public function index(Request $request) @@ -64,8 +64,8 @@ class ServerController extends Controller /** * Controller to handle returning information on a single server. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param int $id * @return array */ public function view(Request $request, $id) @@ -87,7 +87,7 @@ class ServerController extends Controller /** * Create a new server on the system. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\JsonResponse|array */ public function store(Request $request) @@ -130,8 +130,8 @@ class ServerController extends Controller /** * Delete a server from the system. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param int $id * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse */ public function delete(Request $request, $id) @@ -165,8 +165,8 @@ class ServerController extends Controller /** * Update the details for a server. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param int $id * @return \Illuminate\Http\JsonResponse|array */ public function details(Request $request, $id) @@ -205,8 +205,8 @@ class ServerController extends Controller /** * Set the new docker container for a server. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param int $id * @return \Illuminate\Http\RedirectResponse|array */ public function container(Request $request, $id) @@ -245,8 +245,8 @@ class ServerController extends Controller /** * Toggles the install status for a server. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param int $id * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse */ public function install(Request $request, $id) @@ -274,8 +274,8 @@ class ServerController extends Controller /** * Setup a server to have a container rebuild. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param int $id * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse */ public function rebuild(Request $request, $id) @@ -302,8 +302,8 @@ class ServerController extends Controller /** * Manage the suspension status for a server. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param int $id * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse */ public function suspend(Request $request, $id) @@ -344,8 +344,8 @@ class ServerController extends Controller /** * Update the build configuration for a server. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param int $id * @return \Illuminate\Http\JsonResponse|array */ public function build(Request $request, $id) @@ -391,8 +391,8 @@ class ServerController extends Controller /** * Update the startup command as well as variables. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param int $id * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse */ public function startup(Request $request, $id) diff --git a/app/Http/Controllers/API/Admin/ServiceController.php b/app/Http/Controllers/API/Admin/ServiceController.php index fbe911f1..a7b03b0c 100644 --- a/app/Http/Controllers/API/Admin/ServiceController.php +++ b/app/Http/Controllers/API/Admin/ServiceController.php @@ -35,7 +35,7 @@ class ServiceController extends Controller /** * Controller to handle returning all locations on the system. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return array */ public function index(Request $request) @@ -52,8 +52,8 @@ class ServiceController extends Controller /** * Controller to handle returning information on a single server. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param int $id * @return array */ public function view(Request $request, $id) diff --git a/app/Http/Controllers/API/Admin/UserController.php b/app/Http/Controllers/API/Admin/UserController.php index b524d1ee..d2b70f7d 100644 --- a/app/Http/Controllers/API/Admin/UserController.php +++ b/app/Http/Controllers/API/Admin/UserController.php @@ -40,7 +40,7 @@ class UserController extends Controller /** * Controller to handle returning all users on the system. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return array */ public function index(Request $request) @@ -63,8 +63,8 @@ class UserController extends Controller /** * Display information about a single user on the system. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param int $id * @return array */ public function view(Request $request, $id) @@ -84,7 +84,7 @@ class UserController extends Controller /** * Create a new user on the system. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\JsonResponse|array */ public function store(Request $request) @@ -120,8 +120,8 @@ class UserController extends Controller /** * Update a user. * - * @param \Illuminate\Http\Request $request - * @param int $user + * @param \Illuminate\Http\Request $request + * @param int $user * @return \Illuminate\Http\RedirectResponse */ public function update(Request $request, $user) @@ -157,8 +157,8 @@ class UserController extends Controller /** * Delete a user from the system. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param int $id * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse */ public function delete(Request $request, $id) diff --git a/app/Http/Controllers/API/User/CoreController.php b/app/Http/Controllers/API/User/CoreController.php index 5c1d0740..851ff6b6 100644 --- a/app/Http/Controllers/API/User/CoreController.php +++ b/app/Http/Controllers/API/User/CoreController.php @@ -34,7 +34,7 @@ class CoreController extends Controller /** * Controller to handle base user request for all of their servers. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return array */ public function index(Request $request) diff --git a/app/Http/Controllers/API/User/ServerController.php b/app/Http/Controllers/API/User/ServerController.php index dcdb7f6b..dfb625be 100644 --- a/app/Http/Controllers/API/User/ServerController.php +++ b/app/Http/Controllers/API/User/ServerController.php @@ -37,8 +37,8 @@ class ServerController extends Controller /** * Controller to handle base request for individual server information. * - * @param \Illuminate\Http\Request $request - * @param string $uuid + * @param \Illuminate\Http\Request $request + * @param string $uuid * @return array */ public function index(Request $request, $uuid) @@ -60,8 +60,8 @@ class ServerController extends Controller /** * Controller to handle request for server power toggle. * - * @param \Illuminate\Http\Request $request - * @param string $uuid + * @param \Illuminate\Http\Request $request + * @param string $uuid * @return \Illuminate\Http\Response */ public function power(Request $request, $uuid) @@ -80,8 +80,8 @@ class ServerController extends Controller /** * Controller to handle base request for individual server information. * - * @param \Illuminate\Http\Request $request - * @param string $uuid + * @param \Illuminate\Http\Request $request + * @param string $uuid * @return \Illuminate\Http\Response */ public function command(Request $request, $uuid) diff --git a/app/Http/Controllers/Admin/BaseController.php b/app/Http/Controllers/Admin/BaseController.php index d4dd5dc8..14665c32 100644 --- a/app/Http/Controllers/Admin/BaseController.php +++ b/app/Http/Controllers/Admin/BaseController.php @@ -70,7 +70,7 @@ class BaseController extends Controller /** * Handle settings post request. * - * @param \Pterodactyl\Http\Requests\Admin\BaseFormRequest $request + * @param \Pterodactyl\Http\Requests\Admin\BaseFormRequest $request * @return \Illuminate\Http\RedirectResponse */ public function postSettings(BaseFormRequest $request) diff --git a/app/Http/Controllers/Admin/DatabaseController.php b/app/Http/Controllers/Admin/DatabaseController.php index 7964d38c..0a9dbad8 100644 --- a/app/Http/Controllers/Admin/DatabaseController.php +++ b/app/Http/Controllers/Admin/DatabaseController.php @@ -90,7 +90,7 @@ class DatabaseController extends Controller /** * Display database host to user. * - * @param int $host + * @param int $host * @return \Illuminate\View\View * * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException @@ -106,7 +106,7 @@ class DatabaseController extends Controller /** * Handle request to create a new database host. * - * @param \Pterodactyl\Http\Requests\Admin\DatabaseHostFormRequest $request + * @param \Pterodactyl\Http\Requests\Admin\DatabaseHostFormRequest $request * @return \Illuminate\Http\RedirectResponse * * @throws \Throwable @@ -128,8 +128,8 @@ class DatabaseController extends Controller /** * Handle updating database host. * - * @param \Pterodactyl\Http\Requests\Admin\DatabaseHostFormRequest $request - * @param \Pterodactyl\Models\DatabaseHost $host + * @param \Pterodactyl\Http\Requests\Admin\DatabaseHostFormRequest $request + * @param \Pterodactyl\Models\DatabaseHost $host * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\DisplayException @@ -154,7 +154,7 @@ class DatabaseController extends Controller /** * Handle request to delete a database host. * - * @param \Pterodactyl\Models\DatabaseHost $host + * @param \Pterodactyl\Models\DatabaseHost $host * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\DisplayException diff --git a/app/Http/Controllers/Admin/LocationController.php b/app/Http/Controllers/Admin/LocationController.php index 63375cc9..f3e12196 100644 --- a/app/Http/Controllers/Admin/LocationController.php +++ b/app/Http/Controllers/Admin/LocationController.php @@ -52,9 +52,9 @@ class LocationController extends Controller /** * LocationController constructor. * - * @param \Prologue\Alerts\AlertsMessageBag $alert - * @param \Pterodactyl\Contracts\Repository\LocationRepositoryInterface $repository - * @param \Pterodactyl\Services\LocationService $service + * @param \Prologue\Alerts\AlertsMessageBag $alert + * @param \Pterodactyl\Contracts\Repository\LocationRepositoryInterface $repository + * @param \Pterodactyl\Services\LocationService $service */ public function __construct( AlertsMessageBag $alert, @@ -81,7 +81,7 @@ class LocationController extends Controller /** * Return the location view page. * - * @param int $id + * @param int $id * @return \Illuminate\View\View */ public function view($id) @@ -94,7 +94,7 @@ class LocationController extends Controller /** * Handle request to create new location. * - * @param \Pterodactyl\Http\Requests\Admin\LocationFormRequest $request + * @param \Pterodactyl\Http\Requests\Admin\LocationFormRequest $request * @return \Illuminate\Http\RedirectResponse * * @throws \Throwable @@ -111,8 +111,8 @@ class LocationController extends Controller /** * Handle request to update or delete location. * - * @param \Pterodactyl\Http\Requests\Admin\LocationFormRequest $request - * @param \Pterodactyl\Models\Location $location + * @param \Pterodactyl\Http\Requests\Admin\LocationFormRequest $request + * @param \Pterodactyl\Models\Location $location * @return \Illuminate\Http\RedirectResponse * * @throws \Throwable @@ -133,7 +133,7 @@ class LocationController extends Controller /** * Delete a location from the system. * - * @param \Pterodactyl\Models\Location $location + * @param \Pterodactyl\Models\Location $location * @return \Illuminate\Http\RedirectResponse * * @throws \Exception diff --git a/app/Http/Controllers/Admin/NodesController.php b/app/Http/Controllers/Admin/NodesController.php index 854ab486..24e7ce10 100644 --- a/app/Http/Controllers/Admin/NodesController.php +++ b/app/Http/Controllers/Admin/NodesController.php @@ -126,7 +126,7 @@ class NodesController extends Controller /** * Displays the index page listing all nodes on the panel. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ public function index(Request $request) @@ -139,7 +139,7 @@ class NodesController extends Controller /** * Displays create new node page. * - * @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View + * @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View */ public function create() { @@ -156,7 +156,7 @@ class NodesController extends Controller /** * Post controller to create a new node on the system. * - * @param \Pterodactyl\Http\Requests\Admin\Node\NodeFormRequest $request + * @param \Pterodactyl\Http\Requests\Admin\Node\NodeFormRequest $request * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\Model\DataValidationException @@ -172,7 +172,7 @@ class NodesController extends Controller /** * Shows the index overview page for a specific node. * - * @param int $node + * @param int $node * @return \Illuminate\View\View * * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException @@ -188,7 +188,7 @@ class NodesController extends Controller /** * Shows the settings page for a specific node. * - * @param \Pterodactyl\Models\Node $node + * @param \Pterodactyl\Models\Node $node * @return \Illuminate\View\View */ public function viewSettings(Node $node) @@ -202,7 +202,7 @@ class NodesController extends Controller /** * Shows the configuration page for a specific node. * - * @param \Pterodactyl\Models\Node $node + * @param \Pterodactyl\Models\Node $node * @return \Illuminate\View\View */ public function viewConfiguration(Node $node) @@ -213,7 +213,7 @@ class NodesController extends Controller /** * Shows the allocation page for a specific node. * - * @param int $node + * @param int $node * @return \Illuminate\View\View * * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException @@ -229,7 +229,7 @@ class NodesController extends Controller /** * Shows the server listing page for a specific node. * - * @param int $node + * @param int $node * @return \Illuminate\View\View * * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException @@ -247,8 +247,8 @@ class NodesController extends Controller /** * Updates settings for a node. * - * @param \Pterodactyl\Http\Requests\Admin\Node\NodeFormRequest $request - * @param \Pterodactyl\Models\Node $node + * @param \Pterodactyl\Http\Requests\Admin\Node\NodeFormRequest $request + * @param \Pterodactyl\Models\Node $node * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\DisplayException @@ -265,8 +265,8 @@ class NodesController extends Controller /** * Removes a single allocation from a node. * - * @param int $node - * @param int $allocation + * @param int $node + * @param int $allocation * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse */ public function allocationRemoveSingle($node, $allocation) @@ -283,8 +283,8 @@ class NodesController extends Controller /** * Remove all allocations for a specific IP at once on a node. * - * @param \Illuminate\Http\Request $request - * @param int $node + * @param \Illuminate\Http\Request $request + * @param int $node * @return \Illuminate\Http\RedirectResponse */ public function allocationRemoveBlock(Request $request, $node) @@ -304,7 +304,7 @@ class NodesController extends Controller /** * Sets an alias for a specific allocation on a node. * - * @param \Pterodactyl\Http\Requests\Admin\Node\AllocationAliasFormRequest $request + * @param \Pterodactyl\Http\Requests\Admin\Node\AllocationAliasFormRequest $request * @return \Symfony\Component\HttpFoundation\Response * * @throws \Pterodactyl\Exceptions\Model\DataValidationException @@ -322,8 +322,8 @@ class NodesController extends Controller /** * Creates new allocations on a node. * - * @param \Pterodactyl\Http\Requests\Admin\Node\AllocationFormRequest $request - * @param int|\Pterodactyl\Models\Node $node + * @param \Pterodactyl\Http\Requests\Admin\Node\AllocationFormRequest $request + * @param int|\Pterodactyl\Models\Node $node * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\DisplayException @@ -355,7 +355,7 @@ class NodesController extends Controller /** * Returns the configuration token to auto-deploy a node. * - * @param \Pterodactyl\Models\Node $node + * @param \Pterodactyl\Models\Node $node * @return \Illuminate\Http\JsonResponse */ public function setToken(Node $node) diff --git a/app/Http/Controllers/Admin/OptionController.php b/app/Http/Controllers/Admin/OptionController.php index 04e69e85..22b3ea4c 100644 --- a/app/Http/Controllers/Admin/OptionController.php +++ b/app/Http/Controllers/Admin/OptionController.php @@ -122,7 +122,7 @@ class OptionController extends Controller /** * Handle adding a new service option. * - * @param \Pterodactyl\Http\Requests\Admin\Service\ServiceOptionFormRequest $request + * @param \Pterodactyl\Http\Requests\Admin\Service\ServiceOptionFormRequest $request * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\Model\DataValidationException @@ -144,7 +144,7 @@ class OptionController extends Controller /** * Delete a given option from the database. * - * @param \Pterodactyl\Models\ServiceOption $option + * @param \Pterodactyl\Models\ServiceOption $option * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\Service\HasActiveServersException @@ -160,7 +160,7 @@ class OptionController extends Controller /** * Display option overview page. * - * @param \Pterodactyl\Models\ServiceOption $option + * @param \Pterodactyl\Models\ServiceOption $option * @return \Illuminate\View\View */ public function viewConfiguration(ServiceOption $option) @@ -171,7 +171,7 @@ class OptionController extends Controller /** * Display script management page for an option. * - * @param int $option + * @param int $option * @return \Illuminate\View\View * * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException @@ -196,8 +196,8 @@ class OptionController extends Controller /** * Handles POST when editing a configration for a service option. * - * @param \Illuminate\Http\Request $request - * @param \Pterodactyl\Models\ServiceOption $option + * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Models\ServiceOption $option * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\Model\DataValidationException @@ -219,8 +219,8 @@ class OptionController extends Controller /** * Handles POST when updating script for a service option. * - * @param \Pterodactyl\Http\Requests\Admin\Service\EditOptionScript $request - * @param \Pterodactyl\Models\ServiceOption $option + * @param \Pterodactyl\Http\Requests\Admin\Service\EditOptionScript $request + * @param \Pterodactyl\Models\ServiceOption $option * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\Model\DataValidationException diff --git a/app/Http/Controllers/Admin/PackController.php b/app/Http/Controllers/Admin/PackController.php index ab87807c..604ba333 100644 --- a/app/Http/Controllers/Admin/PackController.php +++ b/app/Http/Controllers/Admin/PackController.php @@ -123,7 +123,7 @@ class PackController extends Controller /** * Display listing of all packs on the system. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ public function index(Request $request) @@ -188,7 +188,7 @@ class PackController extends Controller /** * Display pack view template to user. * - * @param int $pack + * @param int $pack * @return \Illuminate\View\View */ public function view($pack) @@ -202,8 +202,8 @@ class PackController extends Controller /** * Handle updating or deleting pack information. * - * @param \Pterodactyl\Http\Requests\Admin\PackFormRequest $request - * @param \Pterodactyl\Models\Pack $pack + * @param \Pterodactyl\Http\Requests\Admin\PackFormRequest $request + * @param \Pterodactyl\Models\Pack $pack * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\Model\DataValidationException @@ -221,7 +221,7 @@ class PackController extends Controller /** * Delete a pack if no servers are attached to it currently. * - * @param \Pterodactyl\Models\Pack $pack + * @param \Pterodactyl\Models\Pack $pack * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException @@ -240,8 +240,8 @@ class PackController extends Controller /** * Creates an archive of the pack and downloads it to the browser. * - * @param \Pterodactyl\Models\Pack $pack - * @param bool|string $files + * @param \Pterodactyl\Models\Pack $pack + * @param bool|string $files * @return \Symfony\Component\HttpFoundation\Response * * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php index 84b7ecbc..efd74b2e 100644 --- a/app/Http/Controllers/Admin/ServersController.php +++ b/app/Http/Controllers/Admin/ServersController.php @@ -245,7 +245,7 @@ class ServersController extends Controller /** * Handle POST of server creation form. * - * @param \Pterodactyl\Http\Requests\Admin\ServerFormRequest $request + * @param \Pterodactyl\Http\Requests\Admin\ServerFormRequest $request * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\DisplayException @@ -262,7 +262,7 @@ class ServersController extends Controller /** * Returns a tree of all avaliable nodes in a given location. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return \Illuminate\Support\Collection */ public function nodes(Request $request) @@ -273,7 +273,7 @@ class ServersController extends Controller /** * Display the index when viewing a specific server. * - * @param \Pterodactyl\Models\Server $server + * @param \Pterodactyl\Models\Server $server * @return \Illuminate\View\View */ public function viewIndex(Server $server) @@ -284,7 +284,7 @@ class ServersController extends Controller /** * Display the details page when viewing a specific server. * - * @param int $server + * @param int $server * @return \Illuminate\View\View */ public function viewDetails($server) @@ -300,7 +300,7 @@ class ServersController extends Controller /** * Display the build details page when viewing a specific server. * - * @param int $server + * @param int $server * @return \Illuminate\View\View */ public function viewBuild($server) @@ -322,7 +322,7 @@ class ServersController extends Controller /** * Display startup configuration page for a server. * - * @param int $server + * @param int $server * @return \Illuminate\View\View */ public function viewStartup($server) @@ -352,7 +352,7 @@ class ServersController extends Controller /** * Display the database management page for a specific server. * - * @param int $server + * @param int $server * @return \Illuminate\View\View */ public function viewDatabase($server) @@ -368,7 +368,7 @@ class ServersController extends Controller /** * Display the management page when viewing a specific server. * - * @param \Pterodactyl\Models\Server $server + * @param \Pterodactyl\Models\Server $server * @return \Illuminate\View\View */ public function viewManage(Server $server) @@ -379,7 +379,7 @@ class ServersController extends Controller /** * Display the deletion page for a server. * - * @param \Pterodactyl\Models\Server $server + * @param \Pterodactyl\Models\Server $server * @return \Illuminate\View\View */ public function viewDelete(Server $server) @@ -390,8 +390,8 @@ class ServersController extends Controller /** * Update the details for a server. * - * @param \Illuminate\Http\Request $request - * @param \Pterodactyl\Models\Server $server + * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\DisplayException @@ -411,8 +411,8 @@ class ServersController extends Controller /** * Set the new docker container for a server. * - * @param \Illuminate\Http\Request $request - * @param \Pterodactyl\Models\Server $server + * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\DisplayException @@ -429,7 +429,7 @@ class ServersController extends Controller /** * Toggles the install status for a server. * - * @param \Pterodactyl\Models\Server $server + * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\DisplayException @@ -453,7 +453,7 @@ class ServersController extends Controller /** * Reinstalls the server with the currently assigned pack and service. * - * @param \Pterodactyl\Models\Server $server + * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\DisplayException @@ -471,7 +471,7 @@ class ServersController extends Controller /** * Setup a server to have a container rebuild. * - * @param \Pterodactyl\Models\Server $server + * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\DisplayException @@ -487,8 +487,8 @@ class ServersController extends Controller /** * Manage the suspension status for a server. * - * @param \Illuminate\Http\Request $request - * @param \Pterodactyl\Models\Server $server + * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\DisplayException @@ -507,7 +507,7 @@ class ServersController extends Controller /** * Update the build configuration for a server. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\RedirectResponse * @throws \Pterodactyl\Exceptions\DisplayException @@ -528,8 +528,8 @@ class ServersController extends Controller /** * Start the server deletion process. * - * @param \Illuminate\Http\Request $request - * @param \Pterodactyl\Models\Server $server + * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\DisplayException @@ -545,7 +545,7 @@ class ServersController extends Controller /** * Update the startup command as well as variables. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Http\RedirectResponse * @@ -555,7 +555,8 @@ class ServersController extends Controller public function saveStartup(Request $request, Server $server) { $this->startupModificationService->isAdmin()->handle( - $server, $request->except('_token') + $server, + $request->except('_token') ); $this->alert->success(trans('admin/server.alerts.startup_changed'))->flash(); @@ -565,8 +566,8 @@ class ServersController extends Controller /** * Creates a new database assigned to a specific server. * - * @param \Illuminate\Http\Request $request - * @param int $server + * @param \Illuminate\Http\Request $request + * @param int $server * @return \Illuminate\Http\RedirectResponse * * @throws \Exception @@ -587,8 +588,8 @@ class ServersController extends Controller /** * Resets the database password for a specific database on this server. * - * @param \Illuminate\Http\Request $request - * @param int $server + * @param \Illuminate\Http\Request $request + * @param int $server * @return \Illuminate\Http\RedirectResponse * * @throws \Exception @@ -609,8 +610,8 @@ class ServersController extends Controller /** * Deletes a database from a server. * - * @param int $server - * @param int $database + * @param int $server + * @param int $database * @return \Illuminate\Http\RedirectResponse * * @throws \Exception diff --git a/app/Http/Controllers/Admin/ServiceController.php b/app/Http/Controllers/Admin/ServiceController.php index 26f6c0ce..817f082f 100644 --- a/app/Http/Controllers/Admin/ServiceController.php +++ b/app/Http/Controllers/Admin/ServiceController.php @@ -100,7 +100,7 @@ class ServiceController extends Controller /** * Return base view for a service. * - * @param int $service + * @param int $service * @return \Illuminate\View\View */ public function view($service) @@ -113,7 +113,7 @@ class ServiceController extends Controller /** * Return function editing view for a service. * - * @param \Pterodactyl\Models\Service $service + * @param \Pterodactyl\Models\Service $service * @return \Illuminate\View\View */ public function viewFunctions(Service $service) @@ -124,7 +124,7 @@ class ServiceController extends Controller /** * Handle post action for new service. * - * @param \Pterodactyl\Http\Requests\Admin\Service\ServiceFormRequest $request + * @param \Pterodactyl\Http\Requests\Admin\Service\ServiceFormRequest $request * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\Model\DataValidationException @@ -140,8 +140,8 @@ class ServiceController extends Controller /** * Edits configuration for a specific service. * - * @param \Pterodactyl\Http\Requests\Admin\Service\ServiceFormRequest $request - * @param \Pterodactyl\Models\Service $service + * @param \Pterodactyl\Http\Requests\Admin\Service\ServiceFormRequest $request + * @param \Pterodactyl\Models\Service $service * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\Model\DataValidationException @@ -158,8 +158,8 @@ class ServiceController extends Controller /** * Update the functions file for a service. * - * @param \Pterodactyl\Http\Requests\Admin\Service\ServiceFunctionsFormRequest $request - * @param \Pterodactyl\Models\Service $service + * @param \Pterodactyl\Http\Requests\Admin\Service\ServiceFunctionsFormRequest $request + * @param \Pterodactyl\Models\Service $service * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\Model\DataValidationException @@ -176,7 +176,7 @@ class ServiceController extends Controller /** * Delete a service from the panel. * - * @param \Pterodactyl\Models\Service $service + * @param \Pterodactyl\Models\Service $service * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\Service\HasActiveServersException diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index 81a4a778..2daa4015 100644 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -71,12 +71,12 @@ class UserController extends Controller /** * UserController constructor. * - * @param \Prologue\Alerts\AlertsMessageBag $alert - * @param \Pterodactyl\Services\Users\CreationService $creationService - * @param \Pterodactyl\Services\Users\DeletionService $deletionService - * @param \Illuminate\Contracts\Translation\Translator $translator - * @param \Pterodactyl\Services\Users\UpdateService $updateService - * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository + * @param \Prologue\Alerts\AlertsMessageBag $alert + * @param \Pterodactyl\Services\Users\CreationService $creationService + * @param \Pterodactyl\Services\Users\DeletionService $deletionService + * @param \Illuminate\Contracts\Translation\Translator $translator + * @param \Pterodactyl\Services\Users\UpdateService $updateService + * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository */ public function __construct( AlertsMessageBag $alert, @@ -97,7 +97,7 @@ class UserController extends Controller /** * Display user index page. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ public function index(Request $request) @@ -120,7 +120,7 @@ class UserController extends Controller /** * Display user view page. * - * @param \Pterodactyl\Models\User $user + * @param \Pterodactyl\Models\User $user * @return \Illuminate\View\View */ public function view(User $user) @@ -131,8 +131,8 @@ class UserController extends Controller /** * Delete a user from the system. * - * @param \Illuminate\Http\Request $request - * @param \Pterodactyl\Models\User $user + * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Models\User $user * @return \Illuminate\Http\RedirectResponse * * @throws \Exception @@ -152,7 +152,7 @@ class UserController extends Controller /** * Create a user. * - * @param \Pterodactyl\Http\Requests\Admin\UserFormRequest $request + * @param \Pterodactyl\Http\Requests\Admin\UserFormRequest $request * @return \Illuminate\Http\RedirectResponse * * @throws \Exception @@ -169,8 +169,8 @@ class UserController extends Controller /** * Update a user on the system. * - * @param \Pterodactyl\Http\Requests\Admin\UserFormRequest $request - * @param \Pterodactyl\Models\User $user + * @param \Pterodactyl\Http\Requests\Admin\UserFormRequest $request + * @param \Pterodactyl\Models\User $user * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\Model\DataValidationException @@ -186,7 +186,7 @@ class UserController extends Controller /** * Get a JSON response of users on the system. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return \Illuminate\Database\Eloquent\Collection */ public function json(Request $request) diff --git a/app/Http/Controllers/Admin/VariableController.php b/app/Http/Controllers/Admin/VariableController.php index 78caf406..d634052a 100644 --- a/app/Http/Controllers/Admin/VariableController.php +++ b/app/Http/Controllers/Admin/VariableController.php @@ -78,8 +78,8 @@ class VariableController extends Controller /** * Handles POST request to create a new option variable. * - * @param \Pterodactyl\Http\Requests\Admin\OptionVariableFormRequest $request - * @param \Pterodactyl\Models\ServiceOption $option + * @param \Pterodactyl\Http\Requests\Admin\OptionVariableFormRequest $request + * @param \Pterodactyl\Models\ServiceOption $option * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\Model\DataValidationException @@ -96,7 +96,7 @@ class VariableController extends Controller /** * Display variable overview page for a service option. * - * @param int $option + * @param int $option * @return \Illuminate\View\View */ public function view($option) @@ -109,9 +109,9 @@ class VariableController extends Controller /** * Handles POST when editing a configration for a service variable. * - * @param \Pterodactyl\Http\Requests\Admin\OptionVariableFormRequest $request - * @param \Pterodactyl\Models\ServiceOption $option - * @param \Pterodactyl\Models\ServiceVariable $variable + * @param \Pterodactyl\Http\Requests\Admin\OptionVariableFormRequest $request + * @param \Pterodactyl\Models\ServiceOption $option + * @param \Pterodactyl\Models\ServiceVariable $variable * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\DisplayException @@ -132,8 +132,8 @@ class VariableController extends Controller /** * Delete a service variable from the system. * - * @param \Pterodactyl\Models\ServiceOption $option - * @param \Pterodactyl\Models\ServiceVariable $variable + * @param \Pterodactyl\Models\ServiceOption $option + * @param \Pterodactyl\Models\ServiceVariable $variable * @return \Illuminate\Http\RedirectResponse */ public function delete(ServiceOption $option, ServiceVariable $variable) diff --git a/app/Http/Controllers/Auth/ForgotPasswordController.php b/app/Http/Controllers/Auth/ForgotPasswordController.php index 9dd80824..fd3e6147 100644 --- a/app/Http/Controllers/Auth/ForgotPasswordController.php +++ b/app/Http/Controllers/Auth/ForgotPasswordController.php @@ -47,8 +47,6 @@ class ForgotPasswordController extends Controller /** * Create a new controller instance. - * - * @return void */ public function __construct() { @@ -59,7 +57,7 @@ class ForgotPasswordController extends Controller * Get the response for a failed password reset link. * * @param \Illuminate\Http\Request - * @param string $response + * @param string $response * @return \Illuminate\Http\RedirectResponse */ protected function sendResetLinkFailedResponse(Request $request, $response) diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index e4ca0d2c..12f3df53 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -71,8 +71,6 @@ class LoginController extends Controller /** * Create a new controller instance. - * - * @return void */ public function __construct() { @@ -82,7 +80,7 @@ class LoginController extends Controller /** * Get the failed login response instance. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\RedirectResponse */ protected function sendFailedLoginResponse(Request $request) @@ -103,7 +101,7 @@ class LoginController extends Controller /** * Handle a login request to the application. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response|\Illuminate\Response\RedirectResponse */ public function login(Request $request) @@ -156,7 +154,7 @@ class LoginController extends Controller /** * Handle a TOTP implementation page. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\RedirectResponse|\Illuminate\View\View */ public function totp(Request $request) @@ -176,7 +174,7 @@ class LoginController extends Controller /** * Handle a TOTP input. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\RedirectResponse */ public function totpCheckpoint(Request $request) diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index d2869229..c0621270 100644 --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -31,8 +31,6 @@ class RegisterController extends Controller /** * Create a new controller instance. - * - * @return void */ public function __construct() { @@ -42,7 +40,7 @@ class RegisterController extends Controller /** * Get a validator for an incoming registration request. * - * @param array $data + * @param array $data * @return \Illuminate\Contracts\Validation\Validator */ protected function validator(array $data) @@ -57,7 +55,7 @@ class RegisterController extends Controller /** * Create a new user instance after a valid registration. * - * @param array $data + * @param array $data * @return User */ protected function create(array $data) diff --git a/app/Http/Controllers/Auth/ResetPasswordController.php b/app/Http/Controllers/Auth/ResetPasswordController.php index 628f55f4..67d9b8f3 100644 --- a/app/Http/Controllers/Auth/ResetPasswordController.php +++ b/app/Http/Controllers/Auth/ResetPasswordController.php @@ -30,8 +30,6 @@ class ResetPasswordController extends Controller /** * Create a new controller instance. - * - * @return void */ public function __construct() { diff --git a/app/Http/Controllers/Base/APIController.php b/app/Http/Controllers/Base/APIController.php index 94783036..7d7e3952 100644 --- a/app/Http/Controllers/Base/APIController.php +++ b/app/Http/Controllers/Base/APIController.php @@ -71,7 +71,7 @@ class APIController extends Controller /** * Display base API index page. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ public function index(Request $request) @@ -99,7 +99,7 @@ class APIController extends Controller /** * Handle saving new API key. * - * @param \Pterodactyl\Http\Requests\ApiKeyRequest $request + * @param \Pterodactyl\Http\Requests\ApiKeyRequest $request * @return \Illuminate\Http\RedirectResponse * * @throws \Exception @@ -128,8 +128,8 @@ class APIController extends Controller } /** - * @param \Illuminate\Http\Request $request - * @param string $key + * @param \Illuminate\Http\Request $request + * @param string $key * @return \Illuminate\Http\Response * * @throws \Exception diff --git a/app/Http/Controllers/Base/AccountController.php b/app/Http/Controllers/Base/AccountController.php index 7167a6f2..332ceadd 100644 --- a/app/Http/Controllers/Base/AccountController.php +++ b/app/Http/Controllers/Base/AccountController.php @@ -38,7 +38,7 @@ class AccountController extends Controller /** * Display base account information page. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ public function index(Request $request) @@ -49,7 +49,7 @@ class AccountController extends Controller /** * Update details for a users account. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\RedirectResponse * @throws \Symfony\Component\HttpKernel\Exception\HttpException */ diff --git a/app/Http/Controllers/Base/IndexController.php b/app/Http/Controllers/Base/IndexController.php index 556ea157..e9d9e768 100644 --- a/app/Http/Controllers/Base/IndexController.php +++ b/app/Http/Controllers/Base/IndexController.php @@ -34,7 +34,7 @@ class IndexController extends Controller /** * Returns listing of user's servers. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ public function getIndex(Request $request) @@ -53,8 +53,8 @@ class IndexController extends Controller /** * Generate a random string. * - * @param \Illuminate\Http\Request $request - * @param int $length + * @param \Illuminate\Http\Request $request + * @param int $length * @return string * @deprecated */ @@ -76,8 +76,8 @@ class IndexController extends Controller /** * Returns status of the server in a JSON response used for populating active status list. * - * @param \Illuminate\Http\Request $request - * @param string $uuid + * @param \Illuminate\Http\Request $request + * @param string $uuid * @return \Illuminate\Http\JsonResponse */ public function status(Request $request, $uuid) @@ -102,7 +102,6 @@ class IndexController extends Controller return response()->json(json_decode($res->getBody())); } } catch (\Exception $e) { - // } return response()->json([]); diff --git a/app/Http/Controllers/Base/LanguageController.php b/app/Http/Controllers/Base/LanguageController.php index 67d04f66..0addd218 100644 --- a/app/Http/Controllers/Base/LanguageController.php +++ b/app/Http/Controllers/Base/LanguageController.php @@ -51,8 +51,8 @@ class LanguageController extends Controller /** * Sets the language for a user. * - * @param \Illuminate\Http\Request $request - * @param string $language + * @param \Illuminate\Http\Request $request + * @param string $language * @return \Illuminate\Http\RedirectResponse */ public function setLanguage(Request $request, $language) diff --git a/app/Http/Controllers/Base/SecurityController.php b/app/Http/Controllers/Base/SecurityController.php index 052a7a52..5a143e65 100644 --- a/app/Http/Controllers/Base/SecurityController.php +++ b/app/Http/Controllers/Base/SecurityController.php @@ -36,7 +36,7 @@ class SecurityController extends Controller /** * Returns Security Management Page. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ public function index(Request $request) @@ -50,7 +50,7 @@ class SecurityController extends Controller * Generates TOTP Secret and returns popup data for user to verify * that they can generate a valid response. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\JsonResponse */ public function generateTotp(Request $request) @@ -73,7 +73,7 @@ class SecurityController extends Controller /** * Verifies that 2FA token recieved is valid and will work on the account. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function setTotp(Request $request) @@ -95,7 +95,7 @@ class SecurityController extends Controller /** * Disables TOTP on an account. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\RedirectResponse */ public function disableTotp(Request $request) @@ -119,8 +119,8 @@ class SecurityController extends Controller /** * Revokes a user session. * - * @param \Illuminate\Http\Request $request - * @param int $id + * @param \Illuminate\Http\Request $request + * @param int $id * @return \Illuminate\Http\RedirectResponse */ public function revoke(Request $request, $id) diff --git a/app/Http/Controllers/Daemon/ActionController.php b/app/Http/Controllers/Daemon/ActionController.php index 0a054218..f0e84ed0 100644 --- a/app/Http/Controllers/Daemon/ActionController.php +++ b/app/Http/Controllers/Daemon/ActionController.php @@ -35,7 +35,7 @@ class ActionController extends Controller /** * Handles download request from daemon. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\JsonResponse */ public function authenticateDownload(Request $request) @@ -57,7 +57,7 @@ class ActionController extends Controller /** * Handles install toggle request from daemon. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\JsonResponse */ public function markInstall(Request $request) @@ -87,8 +87,8 @@ class ActionController extends Controller /** * Handles configuration data request from daemon. * - * @param \Illuminate\Http\Request $request - * @param string $token + * @param \Illuminate\Http\Request $request + * @param string $token * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\Response */ public function configuration(Request $request, $token) diff --git a/app/Http/Controllers/Daemon/PackController.php b/app/Http/Controllers/Daemon/PackController.php index 419ae7c9..21866ed5 100644 --- a/app/Http/Controllers/Daemon/PackController.php +++ b/app/Http/Controllers/Daemon/PackController.php @@ -34,8 +34,8 @@ class PackController extends Controller /** * Pulls an install pack archive from the system. * - * @param \Illuminate\Http\Request $request - * @param string $uuid + * @param \Illuminate\Http\Request $request + * @param string $uuid * @return \Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\BinaryFileResponse */ public function pull(Request $request, $uuid) @@ -56,8 +56,8 @@ class PackController extends Controller /** * Returns the hash information for a pack. * - * @param \Illuminate\Http\Request $request - * @param string $uuid + * @param \Illuminate\Http\Request $request + * @param string $uuid * @return \Illuminate\Http\JsonResponse */ public function hash(Request $request, $uuid) @@ -80,11 +80,9 @@ class PackController extends Controller /** * Pulls an update pack archive from the system. * - * @param \Illuminate\Http\Request $request - * @return void + * @param \Illuminate\Http\Request $request */ public function pullUpdate(Request $request) { - // } } diff --git a/app/Http/Controllers/Daemon/ServiceController.php b/app/Http/Controllers/Daemon/ServiceController.php index d461786f..a9de34d9 100644 --- a/app/Http/Controllers/Daemon/ServiceController.php +++ b/app/Http/Controllers/Daemon/ServiceController.php @@ -36,7 +36,7 @@ class ServiceController extends Controller * as well as the associated files and the file hashes for * caching purposes. * - * @param \Illuminate\Http\Request $request + * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\JsonResponse */ public function listServices(Request $request) @@ -55,9 +55,9 @@ class ServiceController extends Controller /** * Returns the contents of the requested file for the given service. * - * @param \Illuminate\Http\Request $request - * @param string $folder - * @param string $file + * @param \Illuminate\Http\Request $request + * @param string $folder + * @param string $file * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\FileResponse */ public function pull(Request $request, $folder, $file) @@ -77,7 +77,7 @@ class ServiceController extends Controller * Returns a `main.json` file based on the configuration * of each service option. * - * @param int $id + * @param int $id * @return \Illuminate\Support\Collection */ protected function getConfiguration($id) diff --git a/app/Http/Controllers/Server/AjaxController.php b/app/Http/Controllers/Server/AjaxController.php index 1d824ddf..01adb250 100644 --- a/app/Http/Controllers/Server/AjaxController.php +++ b/app/Http/Controllers/Server/AjaxController.php @@ -52,8 +52,8 @@ class AjaxController extends Controller /** * Returns a listing of files in a given directory for a server. * - * @param \Illuminate\Http\Request $request - * @param string $uuid + * @param \Illuminate\Http\Request $request + * @param string $uuid * @return \Illuminate\View\View|\Illuminate\Http\Response */ public function postDirectoryList(Request $request, $uuid) @@ -103,8 +103,8 @@ class AjaxController extends Controller /** * Handles a POST request to save a file. * - * @param \Illuminate\Http\Request $request - * @param string $uuid + * @param \Illuminate\Http\Request $request + * @param string $uuid * @return \Illuminate\Http\Response */ public function postSaveFile(Request $request, $uuid) @@ -130,8 +130,8 @@ class AjaxController extends Controller /** * Sets the primary allocation for a server. * - * @param \Illuminate\Http\Request $request - * @param string $uuid + * @param \Illuminate\Http\Request $request + * @param string $uuid * @return \Illuminate\Http\JsonResponse * @deprecated */ @@ -180,8 +180,8 @@ class AjaxController extends Controller /** * Resets a database password for a server. * - * @param \Illuminate\Http\Request $request - * @param string $uuid + * @param \Illuminate\Http\Request $request + * @param string $uuid * @return \Illuminate\Http\JsonResponse * @deprecated */ diff --git a/app/Http/Controllers/Server/ServerController.php b/app/Http/Controllers/Server/ServerController.php index 6b70a829..86382186 100644 --- a/app/Http/Controllers/Server/ServerController.php +++ b/app/Http/Controllers/Server/ServerController.php @@ -40,8 +40,8 @@ class ServerController extends Controller /** * Renders server index page for specified server. * - * @param \Illuminate\Http\Request $request - * @param string $uuid + * @param \Illuminate\Http\Request $request + * @param string $uuid * @return \Illuminate\View\View */ public function getIndex(Request $request, $uuid) @@ -68,8 +68,8 @@ class ServerController extends Controller /** * Renders server console as an individual item. * - * @param \Illuminate\Http\Request $request - * @param string $uuid + * @param \Illuminate\Http\Request $request + * @param string $uuid * @return \Illuminate\View\View */ public function getConsole(Request $request, $uuid) @@ -93,8 +93,8 @@ class ServerController extends Controller /** * Renders file overview page. * - * @param \Illuminate\Http\Request $request - * @param string $uuid + * @param \Illuminate\Http\Request $request + * @param string $uuid * @return \Illuminate\View\View */ public function getFiles(Request $request, $uuid) @@ -127,8 +127,8 @@ class ServerController extends Controller /** * Renders add file page. * - * @param \Illuminate\Http\Request $request - * @param string $uuid + * @param \Illuminate\Http\Request $request + * @param string $uuid * @return \Illuminate\View\View */ public function getAddFile(Request $request, $uuid) @@ -148,9 +148,9 @@ class ServerController extends Controller /** * Renders edit file page for a given file. * - * @param \Illuminate\Http\Request $request - * @param string $uuid - * @param string $file + * @param \Illuminate\Http\Request $request + * @param string $uuid + * @param string $file * @return \Illuminate\View\View */ public function getEditFile(Request $request, $uuid, $file) @@ -191,9 +191,9 @@ class ServerController extends Controller /** * Handles downloading a file for the user. * - * @param \Illuminate\Http\Request $request - * @param string $uuid - * @param string $file + * @param \Illuminate\Http\Request $request + * @param string $uuid + * @param string $file * @return \Illuminate\View\View */ public function getDownloadFile(Request $request, $uuid, $file) @@ -213,8 +213,8 @@ class ServerController extends Controller /** * Returns the allocation overview for a server. * - * @param \Illuminate\Http\Request $request - * @param string $uuid + * @param \Illuminate\Http\Request $request + * @param string $uuid * @return \Illuminate\View\View */ public function getAllocation(Request $request, $uuid) @@ -235,8 +235,8 @@ class ServerController extends Controller /** * Returns the startup overview for a server. * - * @param \Illuminate\Http\Request $request - * @param string $uuid + * @param \Illuminate\Http\Request $request + * @param string $uuid * @return \Illuminate\View\View */ public function getStartup(Request $request, $uuid) @@ -280,8 +280,8 @@ class ServerController extends Controller /** * Returns the database overview for a server. * - * @param \Illuminate\Http\Request $request - * @param string $uuid + * @param \Illuminate\Http\Request $request + * @param string $uuid * @return \Illuminate\View\View */ public function getDatabases(Request $request, $uuid) @@ -302,8 +302,8 @@ class ServerController extends Controller /** * Returns the SFTP overview for a server. * - * @param \Illuminate\Http\Request $request - * @param string $uuid + * @param \Illuminate\Http\Request $request + * @param string $uuid * @return \Illuminate\View\View */ public function getSFTP(Request $request, $uuid) @@ -321,8 +321,8 @@ class ServerController extends Controller /** * Handles changing the SFTP password for a server. * - * @param \Illuminate\Http\Request $request - * @param string $uuid + * @param \Illuminate\Http\Request $request + * @param string $uuid * @return \Illuminate\Http\RedirectResponse */ public function postSettingsSFTP(Request $request, $uuid) @@ -349,8 +349,8 @@ class ServerController extends Controller /** * Handles changing the startup settings for a server. * - * @param \Illuminate\Http\Request $request - * @param string $uuid + * @param \Illuminate\Http\Request $request + * @param string $uuid * @return \Illuminate\Http\RedirectResponse */ public function postSettingsStartup(Request $request, $uuid) diff --git a/app/Http/Controllers/Server/SubuserController.php b/app/Http/Controllers/Server/SubuserController.php index 9a8b3507..2eaa8df0 100644 --- a/app/Http/Controllers/Server/SubuserController.php +++ b/app/Http/Controllers/Server/SubuserController.php @@ -39,8 +39,8 @@ class SubuserController extends Controller /** * Displays the subuser overview index. * - * @param \Illuminate\Http\Request $request - * @param string $uuid + * @param \Illuminate\Http\Request $request + * @param string $uuid * @return \Illuminate\View\View */ public function index(Request $request, $uuid) @@ -60,9 +60,9 @@ class SubuserController extends Controller /** * Displays the a single subuser overview. * - * @param \Illuminate\Http\Request $request - * @param string $uuid - * @param int $id + * @param \Illuminate\Http\Request $request + * @param string $uuid + * @param int $id * @return \Illuminate\View\View */ public function view(Request $request, $uuid, $id) @@ -89,9 +89,9 @@ class SubuserController extends Controller /** * Handles editing a subuser. * - * @param \Illuminate\Http\Request $request - * @param string $uuid - * @param int $id + * @param \Illuminate\Http\Request $request + * @param string $uuid + * @param int $id * @return \Illuminate\Http\RedirectResponse */ public function update(Request $request, $uuid, $id) @@ -135,8 +135,8 @@ class SubuserController extends Controller /** * Display new subuser creation page. * - * @param \Illuminate\Http\Request $request - * @param string $uuid + * @param \Illuminate\Http\Request $request + * @param string $uuid * @return \Illuminate\View\View */ public function create(Request $request, $uuid) @@ -155,8 +155,8 @@ class SubuserController extends Controller /** * Handles creating a new subuser. * - * @param \Illuminate\Http\Request $request - * @param string $uuid + * @param \Illuminate\Http\Request $request + * @param string $uuid * @return \Illuminate\Http\RedirectResponse */ public function store(Request $request, $uuid) @@ -190,9 +190,9 @@ class SubuserController extends Controller /** * Handles deleting a subuser. * - * @param \Illuminate\Http\Request $request - * @param string $uuid - * @param int $id + * @param \Illuminate\Http\Request $request + * @param string $uuid + * @param int $id * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\Response */ public function delete(Request $request, $uuid, $id) diff --git a/app/Http/Controllers/Server/TaskController.php b/app/Http/Controllers/Server/TaskController.php index a3908943..618a1490 100644 --- a/app/Http/Controllers/Server/TaskController.php +++ b/app/Http/Controllers/Server/TaskController.php @@ -38,8 +38,8 @@ class TaskController extends Controller /** * Display task index page. * - * @param \Illuminate\Http\Request $request - * @param string $uuid + * @param \Illuminate\Http\Request $request + * @param string $uuid * @return \Illuminate\View\View */ public function index(Request $request, $uuid) @@ -62,8 +62,8 @@ class TaskController extends Controller /** * Display new task page. * - * @param \Illuminate\Http\Request $request - * @param string $uuid + * @param \Illuminate\Http\Request $request + * @param string $uuid * @return \Illuminate\View\View */ public function create(Request $request, $uuid) @@ -81,8 +81,8 @@ class TaskController extends Controller /** * Handle creation of new task. * - * @param \Illuminate\Http\Request $request - * @param string $uuid + * @param \Illuminate\Http\Request $request + * @param string $uuid * @return \Illuminate\Http\RedirectResponse */ public function store(Request $request, $uuid) @@ -112,9 +112,9 @@ class TaskController extends Controller /** * Handle deletion of a task. * - * @param \Illuminate\Http\Request $request - * @param string $uuid - * @param int $id + * @param \Illuminate\Http\Request $request + * @param string $uuid + * @param int $id * @return \Illuminate\Http\JsonResponse */ public function delete(Request $request, $uuid, $id) @@ -146,9 +146,9 @@ class TaskController extends Controller /** * Toggle the status of a task. * - * @param \Illuminate\Http\Request $request - * @param string $uuid - * @param int $id + * @param \Illuminate\Http\Request $request + * @param string $uuid + * @param int $id * @return \Illuminate\Http\JsonResponse */ public function toggle(Request $request, $uuid, $id) diff --git a/app/Http/Middleware/AdminAuthenticate.php b/app/Http/Middleware/AdminAuthenticate.php index 17521092..e5fbdc41 100644 --- a/app/Http/Middleware/AdminAuthenticate.php +++ b/app/Http/Middleware/AdminAuthenticate.php @@ -39,8 +39,7 @@ class AdminAuthenticate /** * Create a new filter instance. * - * @param \Illuminate\Contracts\Auth\Guard $auth - * @return void + * @param \Illuminate\Contracts\Auth\Guard $auth */ public function __construct(Guard $auth) { @@ -50,8 +49,8 @@ class AdminAuthenticate /** * Handle an incoming request. * - * @param \Illuminate\Http\Request $request - * @param \Closure $next + * @param \Illuminate\Http\Request $request + * @param \Closure $next * @return mixed */ public function handle($request, Closure $next) diff --git a/app/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php index b6db005e..06e2d6b7 100644 --- a/app/Http/Middleware/Authenticate.php +++ b/app/Http/Middleware/Authenticate.php @@ -17,8 +17,7 @@ class Authenticate /** * Create a new filter instance. * - * @param \Illuminate\Contracts\Auth\Guard $auth - * @return void + * @param \Illuminate\Contracts\Auth\Guard $auth */ public function __construct(Guard $auth) { @@ -28,8 +27,8 @@ class Authenticate /** * Handle an incoming request. * - * @param \Illuminate\Http\Request $request - * @param \Closure $next + * @param \Illuminate\Http\Request $request + * @param \Closure $next * @return mixed */ public function handle($request, Closure $next) diff --git a/app/Http/Middleware/CheckServer.php b/app/Http/Middleware/CheckServer.php index 4cfe0819..c1da9ea1 100644 --- a/app/Http/Middleware/CheckServer.php +++ b/app/Http/Middleware/CheckServer.php @@ -51,8 +51,8 @@ class CheckServer /** * Handle an incoming request. * - * @param \Illuminate\Http\Request $request - * @param \Closure $next + * @param \Illuminate\Http\Request $request + * @param \Closure $next * @return mixed */ public function handle(Request $request, Closure $next) diff --git a/app/Http/Middleware/DaemonAuthenticate.php b/app/Http/Middleware/DaemonAuthenticate.php index b924b6fb..9e190ba9 100644 --- a/app/Http/Middleware/DaemonAuthenticate.php +++ b/app/Http/Middleware/DaemonAuthenticate.php @@ -49,8 +49,7 @@ class DaemonAuthenticate /** * Create a new filter instance. * - * @param \Illuminate\Contracts\Auth\Guard $auth - * @return void + * @param \Illuminate\Contracts\Auth\Guard $auth */ public function __construct(Guard $auth) { @@ -60,8 +59,8 @@ class DaemonAuthenticate /** * Handle an incoming request. * - * @param \Illuminate\Http\Request $request - * @param \Closure $next + * @param \Illuminate\Http\Request $request + * @param \Closure $next * @return mixed */ public function handle($request, Closure $next) diff --git a/app/Http/Middleware/EncryptCookies.php b/app/Http/Middleware/EncryptCookies.php index 8e8559f2..eefb3359 100644 --- a/app/Http/Middleware/EncryptCookies.php +++ b/app/Http/Middleware/EncryptCookies.php @@ -12,6 +12,5 @@ class EncryptCookies extends BaseEncrypter * @var array */ protected $except = [ - // ]; } diff --git a/app/Http/Middleware/HMACAuthorization.php b/app/Http/Middleware/HMACAuthorization.php index 4f5c86f2..12b6ac40 100644 --- a/app/Http/Middleware/HMACAuthorization.php +++ b/app/Http/Middleware/HMACAuthorization.php @@ -68,8 +68,6 @@ class HMACAuthorization /** * Construct class instance. - * - * @return void */ public function __construct() { @@ -80,8 +78,8 @@ class HMACAuthorization /** * Handle an incoming request for the API. * - * @param \Illuminate\Http\Request $request - * @param \Closure $next + * @param \Illuminate\Http\Request $request + * @param \Closure $next * @return mixed */ public function handle(Request $request, Closure $next) @@ -101,7 +99,6 @@ class HMACAuthorization /** * Checks that the Bearer token is provided and in a valid format. * - * @return void * * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException */ @@ -126,7 +123,6 @@ class HMACAuthorization * Determine if the request contains a valid public API key * as well as permissions for the resource. * - * @return void * * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException */ @@ -164,7 +160,6 @@ class HMACAuthorization * Determine if the HMAC sent in the request matches the one generated * on the panel side. * - * @return void * * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException */ diff --git a/app/Http/Middleware/LanguageMiddleware.php b/app/Http/Middleware/LanguageMiddleware.php index 44553ebe..c83f6aa1 100644 --- a/app/Http/Middleware/LanguageMiddleware.php +++ b/app/Http/Middleware/LanguageMiddleware.php @@ -35,8 +35,8 @@ class LanguageMiddleware /** * Handle an incoming request. * - * @param \Illuminate\Http\Request $request - * @param \Closure $next + * @param \Illuminate\Http\Request $request + * @param \Closure $next * @return mixed */ public function handle($request, Closure $next) diff --git a/app/Http/Middleware/RedirectIfAuthenticated.php b/app/Http/Middleware/RedirectIfAuthenticated.php index 731a1767..a25e05fb 100644 --- a/app/Http/Middleware/RedirectIfAuthenticated.php +++ b/app/Http/Middleware/RedirectIfAuthenticated.php @@ -10,9 +10,9 @@ class RedirectIfAuthenticated /** * Handle an incoming request. * - * @param \Illuminate\Http\Request $request - * @param \Closure $next - * @param string|null $guard + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * @param string|null $guard * @return mixed */ public function handle($request, Closure $next, $guard = null) diff --git a/app/Http/Middleware/VerifyReCaptcha.php b/app/Http/Middleware/VerifyReCaptcha.php index 58c0597a..07a0783c 100644 --- a/app/Http/Middleware/VerifyReCaptcha.php +++ b/app/Http/Middleware/VerifyReCaptcha.php @@ -10,8 +10,8 @@ class VerifyReCaptcha /** * Handle an incoming request. * - * @param \Illuminate\Http\Request $request - * @param \Closure $next + * @param \Illuminate\Http\Request $request + * @param \Closure $next * @return \Illuminate\Http\RediectResponse */ public function handle($request, Closure $next) diff --git a/app/Http/Requests/Admin/AdminFormRequest.php b/app/Http/Requests/Admin/AdminFormRequest.php index 769cf9dd..4399d56c 100644 --- a/app/Http/Requests/Admin/AdminFormRequest.php +++ b/app/Http/Requests/Admin/AdminFormRequest.php @@ -49,7 +49,7 @@ abstract class AdminFormRequest extends FormRequest * Return only the fields that we are interested in from the request. * This will include empty fields as a null value. * - * @param array $only + * @param array $only * @return array */ public function normalize($only = []) diff --git a/app/Http/Requests/Request.php b/app/Http/Requests/Request.php index 5e3ae0bc..a9771726 100644 --- a/app/Http/Requests/Request.php +++ b/app/Http/Requests/Request.php @@ -6,5 +6,4 @@ use Illuminate\Foundation\Http\FormRequest; abstract class Request extends FormRequest { - // } diff --git a/app/Jobs/SendScheduledTask.php b/app/Jobs/SendScheduledTask.php index 8f988973..8e44e3e9 100644 --- a/app/Jobs/SendScheduledTask.php +++ b/app/Jobs/SendScheduledTask.php @@ -45,8 +45,6 @@ class SendScheduledTask extends Job implements ShouldQueue /** * Create a new job instance. - * - * @return void */ public function __construct(Task $task) { @@ -58,8 +56,6 @@ class SendScheduledTask extends Job implements ShouldQueue /** * Execute the job. - * - * @return void */ public function handle() { @@ -96,7 +92,8 @@ class SendScheduledTask extends Job implements ShouldQueue 'response' => $ex->getMessage(), ]); } finally { - $cron = Cron::factory(sprintf('%s %s %s %s %s %s', + $cron = Cron::factory(sprintf( + '%s %s %s %s %s %s', $this->task->minute, $this->task->hour, $this->task->day_of_month, diff --git a/app/Models/Allocation.php b/app/Models/Allocation.php index cbdba2e1..e5d862bf 100644 --- a/app/Models/Allocation.php +++ b/app/Models/Allocation.php @@ -82,7 +82,7 @@ class Allocation extends Model implements CleansAttributes, ValidableContract /** * Accessor to automatically provide the IP alias if defined. * - * @param null|string $value + * @param null|string $value * @return string */ public function getAliasAttribute($value) @@ -93,7 +93,7 @@ class Allocation extends Model implements CleansAttributes, ValidableContract /** * Accessor to quickly determine if this allocation has an alias. * - * @param null|string $value + * @param null|string $value * @return bool */ public function getHasAliasAttribute($value) diff --git a/app/Models/Node.php b/app/Models/Node.php index 138d29d8..76f04e9c 100644 --- a/app/Models/Node.php +++ b/app/Models/Node.php @@ -144,7 +144,7 @@ class Node extends Model implements CleansAttributes, ValidableContract /** * Return an instance of the Guzzle client for this specific node. * - * @param array $headers + * @param array $headers * @return \GuzzleHttp\Client */ public function guzzleClient($headers = []) @@ -160,7 +160,7 @@ class Node extends Model implements CleansAttributes, ValidableContract /** * Returns the configuration in JSON format. * - * @param bool $pretty + * @param bool $pretty * @return string */ public function getConfigurationAsJson($pretty = false) diff --git a/app/Models/Pack.php b/app/Models/Pack.php index 0139fcf8..48382a42 100644 --- a/app/Models/Pack.php +++ b/app/Models/Pack.php @@ -107,7 +107,7 @@ class Pack extends Model implements CleansAttributes, ValidableContract /** * Returns all of the archived files for a given pack. * - * @param bool $collection + * @param bool $collection * @return \Illuminate\Support\Collection|object * @deprecated */ diff --git a/app/Models/Permission.php b/app/Models/Permission.php index c126967e..086586cd 100644 --- a/app/Models/Permission.php +++ b/app/Models/Permission.php @@ -81,7 +81,7 @@ class Permission extends Model 'server' => [ 'set-connection' => null, 'view-startup' => null, - 'edit-startup' => null, + 'edit-startup' => null, ], 'sftp' => [ 'view-sftp' => null, @@ -118,7 +118,7 @@ class Permission extends Model /** * Return a collection of permissions available. * - * @param array $single + * @param array $single * @return \Illuminate\Support\Collection|array */ public static function listPermissions($single = false) @@ -135,8 +135,8 @@ class Permission extends Model /** * Find permission by permission node. * - * @param \Illuminate\Database\Query\Builder $query - * @param string $permission + * @param \Illuminate\Database\Query\Builder $query + * @param string $permission * @return \Illuminate\Database\Query\Builder */ public function scopePermission($query, $permission) @@ -147,8 +147,8 @@ class Permission extends Model /** * Filter permission by server. * - * @param \Illuminate\Database\Query\Builder $query - * @param \Pterodactyl\Models\Server $server + * @param \Illuminate\Database\Query\Builder $query + * @param \Pterodactyl\Models\Server $server * @return \Illuminate\Database\Query\Builder */ public function scopeServer($query, Server $server) diff --git a/app/Models/Server.php b/app/Models/Server.php index 45091372..b31a89a7 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -155,9 +155,9 @@ class Server extends Model implements CleansAttributes, ValidableContract * DO NOT USE THIS TO MODIFY SERVER DETAILS OR SAVE THOSE DETAILS. * YOU WILL OVERWRITE THE SECRET KEY AND BREAK THINGS. * - * @param string $uuid - * @param array $with - * @param array $withCount + * @param string $uuid + * @param array $with + * @param array $withCount * @return \Pterodactyl\Models\Server * @throws \Exception * @todo Remove $with and $withCount due to cache issues, they aren't used anyways. @@ -191,7 +191,7 @@ class Server extends Model implements CleansAttributes, ValidableContract /** * Returns non-administrative headers for accessing a server on the daemon. * - * @param Pterodactyl\Models\User|null $user + * @param Pterodactyl\Models\User|null $user * @return array */ public function guzzleHeaders(User $user = null) @@ -211,7 +211,7 @@ class Server extends Model implements CleansAttributes, ValidableContract /** * Return an instance of the Guzzle client for this specific server using defined access token. * - * @param Pterodactyl\Models\User|null $user + * @param Pterodactyl\Models\User|null $user * @return \GuzzleHttp\Client */ public function guzzleClient(User $user = null) @@ -222,8 +222,8 @@ class Server extends Model implements CleansAttributes, ValidableContract /** * Returns javascript object to be embedded on server view pages with relevant information. * - * @param array|null $additional - * @param array|null $overwrite + * @param array|null $additional + * @param array|null $overwrite * @return \Laracasts\Utilities\JavaScript\JavaScriptFacade */ public function js($additional = null, $overwrite = null) diff --git a/app/Models/Service.php b/app/Models/Service.php index 25355981..391f4ae1 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -90,8 +90,10 @@ class Service extends Model implements CleansAttributes, ValidableContract public function packs() { return $this->hasManyThrough( - 'Pterodactyl\Models\Pack', 'Pterodactyl\Models\ServiceOption', - 'service_id', 'option_id' + 'Pterodactyl\Models\Pack', + 'Pterodactyl\Models\ServiceOption', + 'service_id', + 'option_id' ); } diff --git a/app/Models/User.php b/app/Models/User.php index 972d0943..d9f99d01 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -162,7 +162,7 @@ class User extends Model implements /** * Enables or disables TOTP on an account if the token is valid. * - * @param int $token + * @param int $token * @return bool * @deprecated */ @@ -184,9 +184,8 @@ class User extends Model implements * - at least one lowercase character * - at least one number. * - * @param string $password - * @param string $regex - * @return void + * @param string $password + * @param string $regex * @throws \Pterodactyl\Exceptions\DisplayException * @deprecated */ @@ -203,8 +202,7 @@ class User extends Model implements /** * Send the password reset notification. * - * @param string $token - * @return void + * @param string $token */ public function sendPasswordResetNotification($token) { @@ -225,7 +223,7 @@ class User extends Model implements /** * Returns the user's daemon secret for a given server. * - * @param \Pterodactyl\Models\Server $server + * @param \Pterodactyl\Models\Server $server * @return null|string */ public function daemonToken(Server $server) @@ -255,7 +253,7 @@ class User extends Model implements /** * Change the access level for a given call to `access()` on the user. * - * @param string $level can be all, admin, subuser, owner + * @param string $level can be all, admin, subuser, owner * @return $this */ public function setAccessLevel($level = 'all') @@ -272,7 +270,7 @@ class User extends Model implements * Returns an array of all servers a user is able to access. * Note: does not account for user admin status. * - * @param array $load + * @param array $load * @return \Pterodactyl\Models\Server */ public function access(...$load) @@ -310,7 +308,7 @@ class User extends Model implements /** * Store the username as a lowecase string. * - * @param string $value + * @param string $value */ public function setUsernameAttribute($value) { diff --git a/app/Notifications/AccountCreated.php b/app/Notifications/AccountCreated.php index f92a7a47..47fbf755 100644 --- a/app/Notifications/AccountCreated.php +++ b/app/Notifications/AccountCreated.php @@ -43,8 +43,7 @@ class AccountCreated extends Notification implements ShouldQueue /** * Create a new notification instance. * - * @param aray $user - * @return void + * @param aray $user */ public function __construct(array $user) { @@ -54,7 +53,7 @@ class AccountCreated extends Notification implements ShouldQueue /** * Get the notification's delivery channels. * - * @param mixed $notifiable + * @param mixed $notifiable * @return array */ public function via($notifiable) @@ -65,7 +64,7 @@ class AccountCreated extends Notification implements ShouldQueue /** * Get the mail representation of the notification. * - * @param mixed $notifiable + * @param mixed $notifiable * @return \Illuminate\Notifications\Messages\MailMessage */ public function toMail($notifiable) diff --git a/app/Notifications/AddedToServer.php b/app/Notifications/AddedToServer.php index 415b39fb..4d8bf838 100644 --- a/app/Notifications/AddedToServer.php +++ b/app/Notifications/AddedToServer.php @@ -41,8 +41,7 @@ class AddedToServer extends Notification implements ShouldQueue /** * Create a new notification instance. * - * @param array $server - * @return void + * @param array $server */ public function __construct(array $server) { @@ -52,7 +51,7 @@ class AddedToServer extends Notification implements ShouldQueue /** * Get the notification's delivery channels. * - * @param mixed $notifiable + * @param mixed $notifiable * @return array */ public function via($notifiable) @@ -63,7 +62,7 @@ class AddedToServer extends Notification implements ShouldQueue /** * Get the mail representation of the notification. * - * @param mixed $notifiable + * @param mixed $notifiable * @return \Illuminate\Notifications\Messages\MailMessage */ public function toMail($notifiable) diff --git a/app/Notifications/RemovedFromServer.php b/app/Notifications/RemovedFromServer.php index d4831dbe..46b7143a 100644 --- a/app/Notifications/RemovedFromServer.php +++ b/app/Notifications/RemovedFromServer.php @@ -41,8 +41,7 @@ class RemovedFromServer extends Notification implements ShouldQueue /** * Create a new notification instance. * - * @param array $server - * @return void + * @param array $server */ public function __construct(array $server) { @@ -52,7 +51,7 @@ class RemovedFromServer extends Notification implements ShouldQueue /** * Get the notification's delivery channels. * - * @param mixed $notifiable + * @param mixed $notifiable * @return array */ public function via($notifiable) @@ -63,7 +62,7 @@ class RemovedFromServer extends Notification implements ShouldQueue /** * Get the mail representation of the notification. * - * @param mixed $notifiable + * @param mixed $notifiable * @return \Illuminate\Notifications\Messages\MailMessage */ public function toMail($notifiable) diff --git a/app/Notifications/SendPasswordReset.php b/app/Notifications/SendPasswordReset.php index 367f863e..1ba616ea 100644 --- a/app/Notifications/SendPasswordReset.php +++ b/app/Notifications/SendPasswordReset.php @@ -43,8 +43,7 @@ class SendPasswordReset extends Notification implements ShouldQueue /** * Create a new notification instance. * - * @param string $token - * @return void + * @param string $token */ public function __construct($token) { @@ -54,7 +53,7 @@ class SendPasswordReset extends Notification implements ShouldQueue /** * Get the notification's delivery channels. * - * @param mixed $notifiable + * @param mixed $notifiable * @return array */ public function via($notifiable) @@ -65,7 +64,7 @@ class SendPasswordReset extends Notification implements ShouldQueue /** * Get the mail representation of the notification. * - * @param mixed $notifiable + * @param mixed $notifiable * @return \Illuminate\Notifications\Messages\MailMessage */ public function toMail($notifiable) diff --git a/app/Notifications/ServerCreated.php b/app/Notifications/ServerCreated.php index 9f881729..054b4adb 100644 --- a/app/Notifications/ServerCreated.php +++ b/app/Notifications/ServerCreated.php @@ -41,8 +41,7 @@ class ServerCreated extends Notification implements ShouldQueue /** * Create a new notification instance. * - * @param array $server - * @return void + * @param array $server */ public function __construct(array $server) { @@ -52,7 +51,7 @@ class ServerCreated extends Notification implements ShouldQueue /** * Get the notification's delivery channels. * - * @param mixed $notifiable + * @param mixed $notifiable * @return array */ public function via($notifiable) @@ -63,7 +62,7 @@ class ServerCreated extends Notification implements ShouldQueue /** * Get the mail representation of the notification. * - * @param mixed $notifiable + * @param mixed $notifiable * @return \Illuminate\Notifications\Messages\MailMessage */ public function toMail($notifiable) diff --git a/app/Observers/ServerObserver.php b/app/Observers/ServerObserver.php index cd8c2187..557da2eb 100644 --- a/app/Observers/ServerObserver.php +++ b/app/Observers/ServerObserver.php @@ -37,8 +37,7 @@ class ServerObserver /** * Listen to the Server creating event. * - * @param \Pterodactyl\Models\Server $server - * @return void + * @param \Pterodactyl\Models\Server $server */ public function creating(Server $server) { @@ -48,8 +47,7 @@ class ServerObserver /** * Listen to the Server created event. * - * @param \Pterodactyl\Models\Server $server - * @return void + * @param \Pterodactyl\Models\Server $server */ public function created(Server $server) { @@ -69,8 +67,7 @@ class ServerObserver /** * Listen to the Server deleting event. * - * @param \Pterodactyl\Models\Server $server - * @return void + * @param \Pterodactyl\Models\Server $server */ public function deleting(Server $server) { @@ -80,8 +77,7 @@ class ServerObserver /** * Listen to the Server deleted event. * - * @param \Pterodactyl\Models\Server $server - * @return void + * @param \Pterodactyl\Models\Server $server */ public function deleted(Server $server) { @@ -91,8 +87,7 @@ class ServerObserver /** * Listen to the Server saving event. * - * @param \Pterodactyl\Models\Server $server - * @return void + * @param \Pterodactyl\Models\Server $server */ public function saving(Server $server) { @@ -102,8 +97,7 @@ class ServerObserver /** * Listen to the Server saved event. * - * @param \Pterodactyl\Models\Server $server - * @return void + * @param \Pterodactyl\Models\Server $server */ public function saved(Server $server) { @@ -113,8 +107,7 @@ class ServerObserver /** * Listen to the Server updating event. * - * @param \Pterodactyl\Models\Server $server - * @return void + * @param \Pterodactyl\Models\Server $server */ public function updating(Server $server) { @@ -124,8 +117,7 @@ class ServerObserver /** * Listen to the Server saved event. * - * @param \Pterodactyl\Models\Server $server - * @return void + * @param \Pterodactyl\Models\Server $server */ public function updated(Server $server) { diff --git a/app/Observers/SubuserObserver.php b/app/Observers/SubuserObserver.php index 57eab268..a6bcdacc 100644 --- a/app/Observers/SubuserObserver.php +++ b/app/Observers/SubuserObserver.php @@ -34,8 +34,7 @@ class SubuserObserver /** * Listen to the Subuser creating event. * - * @param \Pterodactyl\Models\Subuser $subuser - * @return void + * @param \Pterodactyl\Models\Subuser $subuser */ public function creating(Subuser $subuser) { @@ -45,8 +44,7 @@ class SubuserObserver /** * Listen to the Subuser created event. * - * @param \Pterodactyl\Models\Subuser $subuser - * @return void + * @param \Pterodactyl\Models\Subuser $subuser */ public function created(Subuser $subuser) { @@ -62,8 +60,7 @@ class SubuserObserver /** * Listen to the Subuser deleting event. * - * @param \Pterodactyl\Models\Subuser $subuser - * @return void + * @param \Pterodactyl\Models\Subuser $subuser */ public function deleting(Subuser $subuser) { @@ -73,8 +70,7 @@ class SubuserObserver /** * Listen to the Subuser deleted event. * - * @param \Pterodactyl\Models\Subuser $subuser - * @return void + * @param \Pterodactyl\Models\Subuser $subuser */ public function deleted(Subuser $subuser) { diff --git a/app/Observers/UserObserver.php b/app/Observers/UserObserver.php index e75c053b..5b1ee844 100644 --- a/app/Observers/UserObserver.php +++ b/app/Observers/UserObserver.php @@ -40,8 +40,7 @@ class UserObserver /** * Listen to the User creating event. * - * @param \Pterodactyl\Models\User $user - * @return void + * @param \Pterodactyl\Models\User $user */ public function creating(User $user) { @@ -53,8 +52,7 @@ class UserObserver /** * Listen to the User created event. * - * @param \Pterodactyl\Models\User $user - * @return void + * @param \Pterodactyl\Models\User $user */ public function created(User $user) { @@ -64,8 +62,7 @@ class UserObserver /** * Listen to the User deleting event. * - * @param \Pterodactyl\Models\User $user - * @return void + * @param \Pterodactyl\Models\User $user */ public function deleting(User $user) { @@ -75,8 +72,7 @@ class UserObserver /** * Listen to the User deleted event. * - * @param \Pterodactyl\Models\User $user - * @return void + * @param \Pterodactyl\Models\User $user */ public function deleted(User $user) { diff --git a/app/Policies/APIKeyPolicy.php b/app/Policies/APIKeyPolicy.php index 95846b9e..31b888a7 100644 --- a/app/Policies/APIKeyPolicy.php +++ b/app/Policies/APIKeyPolicy.php @@ -61,9 +61,9 @@ class APIKeyPolicy /** * Determine if a user has permission to perform this action against the system. * - * @param \Pterodactyl\Models\User $user - * @param string $permission - * @param \Pterodactyl\Models\APIKey $key + * @param \Pterodactyl\Models\User $user + * @param string $permission + * @param \Pterodactyl\Models\APIKey $key * @return bool */ public function before(User $user, $permission, Key $key) diff --git a/app/Policies/ServerPolicy.php b/app/Policies/ServerPolicy.php index 56cd359e..618deebf 100644 --- a/app/Policies/ServerPolicy.php +++ b/app/Policies/ServerPolicy.php @@ -53,9 +53,9 @@ class ServerPolicy /** * Runs before any of the functions are called. Used to determine if user is root admin, if so, ignore permissions. * - * @param \Pterodactyl\Models\User $user - * @param string $ability - * @param \Pterodactyl\Models\Server $server + * @param \Pterodactyl\Models\User $user + * @param string $ability + * @param \Pterodactyl\Models\Server $server * @return bool */ public function before(User $user, $ability, Server $server) diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 9bb72d1f..1fc8b742 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -34,8 +34,6 @@ class AppServiceProvider extends ServiceProvider { /** * Bootstrap any application services. - * - * @return void */ public function boot() { @@ -49,8 +47,6 @@ class AppServiceProvider extends ServiceProvider /** * Register any application services. - * - * @return void */ public function register() { diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index e1401e84..0cdb82a2 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -19,8 +19,7 @@ class AuthServiceProvider extends ServiceProvider /** * Register any application authentication / authorization services. * - * @param \Illuminate\Contracts\Auth\Access\Gate $gate - * @return void + * @param \Illuminate\Contracts\Auth\Access\Gate $gate */ public function boot() { diff --git a/app/Providers/BroadcastServiceProvider.php b/app/Providers/BroadcastServiceProvider.php index e61e610d..3f7c84be 100644 --- a/app/Providers/BroadcastServiceProvider.php +++ b/app/Providers/BroadcastServiceProvider.php @@ -9,8 +9,6 @@ class BroadcastServiceProvider extends ServiceProvider { /** * Bootstrap any application services. - * - * @return void */ public function boot() { diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 9126aa6a..1f48d33d 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -16,8 +16,6 @@ class EventServiceProvider extends ServiceProvider /** * Register any other events for your application. - * - * @return void */ public function boot() { diff --git a/app/Providers/MacroServiceProvider.php b/app/Providers/MacroServiceProvider.php index 600c0d3f..50e20515 100644 --- a/app/Providers/MacroServiceProvider.php +++ b/app/Providers/MacroServiceProvider.php @@ -36,8 +36,6 @@ class MacroServiceProvider extends ServiceProvider { /** * Bootstrap the application services. - * - * @return void */ public function boot() { @@ -48,7 +46,7 @@ class MacroServiceProvider extends ServiceProvider $i = 0; while (($size / 1024) > 0.9) { $size = $size / 1024; - $i++; + ++$i; } return round($size, ($i < 2) ? 0 : $precision) . ' ' . $units[$i]; diff --git a/app/Providers/PhraseAppTranslationProvider.php b/app/Providers/PhraseAppTranslationProvider.php index 84023491..cfc68bb3 100644 --- a/app/Providers/PhraseAppTranslationProvider.php +++ b/app/Providers/PhraseAppTranslationProvider.php @@ -32,8 +32,6 @@ class PhraseAppTranslationProvider extends TranslationServiceProvider { /** * Register the service provider. - * - * @return void */ public function register() { diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 1563e35f..9876208c 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -19,8 +19,6 @@ class RouteServiceProvider extends ServiceProvider /** * Define your route model bindings, pattern filters, etc. - * - * @return void */ public function boot() { @@ -29,8 +27,6 @@ class RouteServiceProvider extends ServiceProvider /** * Define the routes for the application. - * - * @return void */ public function map() { diff --git a/app/Repositories/Concerns/Searchable.php b/app/Repositories/Concerns/Searchable.php index ec957824..75b95ba5 100644 --- a/app/Repositories/Concerns/Searchable.php +++ b/app/Repositories/Concerns/Searchable.php @@ -36,7 +36,7 @@ trait Searchable /** * Perform a search of the model using the given term. * - * @param string $term + * @param string $term * @return $this */ public function search($term) diff --git a/app/Repositories/Eloquent/DatabaseRepository.php b/app/Repositories/Eloquent/DatabaseRepository.php index 347c6b9d..e274f193 100644 --- a/app/Repositories/Eloquent/DatabaseRepository.php +++ b/app/Repositories/Eloquent/DatabaseRepository.php @@ -85,7 +85,8 @@ class DatabaseRepository extends EloquentRepository implements DatabaseRepositor public function createDatabase($database, $connection = null) { return $this->runStatement( - sprintf('CREATE DATABASE IF NOT EXISTS `%s`', $database), $connection + sprintf('CREATE DATABASE IF NOT EXISTS `%s`', $database), + $connection ); } @@ -95,7 +96,8 @@ class DatabaseRepository extends EloquentRepository implements DatabaseRepositor public function createUser($username, $remote, $password, $connection = null) { return $this->runStatement( - sprintf('CREATE USER `%s`@`%s` IDENTIFIED BY \'%s\'', $username, $remote, $password), $connection + sprintf('CREATE USER `%s`@`%s` IDENTIFIED BY \'%s\'', $username, $remote, $password), + $connection ); } @@ -107,7 +109,9 @@ class DatabaseRepository extends EloquentRepository implements DatabaseRepositor return $this->runStatement( sprintf( 'GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, INDEX ON `%s`.* TO `%s`@`%s`', - $database, $username, $remote + $database, + $username, + $remote ), $connection ); @@ -127,7 +131,8 @@ class DatabaseRepository extends EloquentRepository implements DatabaseRepositor public function dropDatabase($database, $connection = null) { return $this->runStatement( - sprintf('DROP DATABASE IF EXISTS `%s`', $database), $connection + sprintf('DROP DATABASE IF EXISTS `%s`', $database), + $connection ); } @@ -137,15 +142,16 @@ class DatabaseRepository extends EloquentRepository implements DatabaseRepositor public function dropUser($username, $remote, $connection = null) { return $this->runStatement( - sprintf('DROP USER IF EXISTS `%s`@`%s`', $username, $remote), $connection + sprintf('DROP USER IF EXISTS `%s`@`%s`', $username, $remote), + $connection ); } /** * Run the provided statement against the database on a given connection. * - * @param string $statement - * @param null|string $connection + * @param string $statement + * @param null|string $connection * @return bool */ protected function runStatement($statement, $connection = null) diff --git a/app/Repositories/Eloquent/EloquentRepository.php b/app/Repositories/Eloquent/EloquentRepository.php index fce97403..f29418d0 100644 --- a/app/Repositories/Eloquent/EloquentRepository.php +++ b/app/Repositories/Eloquent/EloquentRepository.php @@ -44,7 +44,7 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf /** * {@inheritdoc} - * @param bool $force + * @param bool $force * @return \Illuminate\Database\Eloquent\Model|bool */ public function create(array $fields, $validate = true, $force = false) @@ -214,7 +214,7 @@ abstract class EloquentRepository extends Repository implements RepositoryInterf /** * Insert multiple records into the database and ignore duplicates. * - * @param array $values + * @param array $values * @return bool */ public function insertIgnore(array $values) diff --git a/app/Repositories/Eloquent/NodeRepository.php b/app/Repositories/Eloquent/NodeRepository.php index 9a63ca93..e421d778 100644 --- a/app/Repositories/Eloquent/NodeRepository.php +++ b/app/Repositories/Eloquent/NodeRepository.php @@ -47,7 +47,10 @@ class NodeRepository extends EloquentRepository implements NodeRepositoryInterfa public function getUsageStats($id) { $node = $this->getBuilder()->select( - 'nodes.disk_overallocate', 'nodes.memory_overallocate', 'nodes.disk', 'nodes.memory', + 'nodes.disk_overallocate', + 'nodes.memory_overallocate', + 'nodes.disk', + 'nodes.memory', $this->getBuilder()->raw('SUM(servers.memory) as sum_memory, SUM(servers.disk) as sum_disk') )->join('servers', 'servers.node_id', '=', 'nodes.id') ->where('nodes.id', $id) diff --git a/app/Repositories/Eloquent/UserRepository.php b/app/Repositories/Eloquent/UserRepository.php index ec1abe57..38f7e355 100644 --- a/app/Repositories/Eloquent/UserRepository.php +++ b/app/Repositories/Eloquent/UserRepository.php @@ -72,7 +72,8 @@ class UserRepository extends EloquentRepository implements UserRepositoryInterfa } return $users->paginate( - $this->config->get('pterodactyl.paginate.admin.users'), $this->getColumns() + $this->config->get('pterodactyl.paginate.admin.users'), + $this->getColumns() ); } diff --git a/app/Repositories/Old/SubuserRepository.php b/app/Repositories/Old/SubuserRepository.php index b7d73655..26c95107 100644 --- a/app/Repositories/Old/SubuserRepository.php +++ b/app/Repositories/Old/SubuserRepository.php @@ -52,8 +52,8 @@ class SubuserRepository /** * Creates a new subuser on the server. * - * @param int $sid - * @param array $data + * @param int $sid + * @param array $data * @return \Pterodactyl\Models\Subuser * * @throws \Pterodactyl\Exceptions\DisplayException @@ -152,8 +152,7 @@ class SubuserRepository /** * Revokes a users permissions on a server. * - * @param int $id - * @return void + * @param int $id * * @throws \Pterodactyl\Exceptions\DisplayException */ @@ -193,9 +192,8 @@ class SubuserRepository /** * Updates permissions for a given subuser. * - * @param int $id - * @param array $data - * @return void + * @param int $id + * @param array $data * * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\DisplayValidationException diff --git a/app/Repositories/Repository.php b/app/Repositories/Repository.php index 32beb8cb..375001b0 100644 --- a/app/Repositories/Repository.php +++ b/app/Repositories/Repository.php @@ -64,7 +64,7 @@ abstract class Repository implements RepositoryInterface /** * Take the provided model and make it accessible to the rest of the repository. * - * @param string|array $model + * @param string|array $model * @return mixed */ protected function setModel($model) @@ -77,7 +77,8 @@ abstract class Repository implements RepositoryInterface } return $this->model = call_user_func( - $model[1], $this->app->make($model[0]) + $model[1], + $this->app->make($model[0]) ); } @@ -102,7 +103,7 @@ abstract class Repository implements RepositoryInterface /** * Setup column selection functionality. * - * @param array $columns + * @param array $columns * @return $this */ public function withColumns($columns = ['*']) diff --git a/app/Repositories/old_Daemon/CommandRepository.php b/app/Repositories/old_Daemon/CommandRepository.php index 2f1a41ee..ce12e12d 100644 --- a/app/Repositories/old_Daemon/CommandRepository.php +++ b/app/Repositories/old_Daemon/CommandRepository.php @@ -48,9 +48,8 @@ class CommandRepository /** * Constuctor for repository. * - * @param \Pterodactyl\Models\Server $server - * @param \Pterodactyl\Models\User|null $user - * @return void + * @param \Pterodactyl\Models\Server $server + * @param \Pterodactyl\Models\User|null $user */ public function __construct(Server $server, User $user = null) { @@ -61,7 +60,7 @@ class CommandRepository /** * Sends a command to the daemon. * - * @param string $command + * @param string $command * @return string * * @throws \Pterodactyl\Exceptions\DisplayException diff --git a/app/Repositories/old_Daemon/FileRepository.php b/app/Repositories/old_Daemon/FileRepository.php index 8254c227..b4dc5f7f 100644 --- a/app/Repositories/old_Daemon/FileRepository.php +++ b/app/Repositories/old_Daemon/FileRepository.php @@ -42,8 +42,7 @@ class FileRepository /** * Constructor. * - * @param string $uuid - * @return void + * @param string $uuid */ public function __construct($uuid) { @@ -53,7 +52,7 @@ class FileRepository /** * Get the contents of a requested file for the server. * - * @param string $file + * @param string $file * @return array * * @throws \GuzzleHttp\Exception\RequestException @@ -99,8 +98,8 @@ class FileRepository /** * Save the contents of a requested file on the daemon. * - * @param string $file - * @param string $content + * @param string $file + * @param string $content * @return bool * * @throws \GuzzleHttp\Exception\RequestException @@ -132,7 +131,7 @@ class FileRepository /** * Returns a listing of all files and folders within a specified directory on the daemon. * - * @param string $directory + * @param string $directory * @return object * * @throws \GuzzleHttp\Exception\RequestException diff --git a/app/Repositories/old_Daemon/PowerRepository.php b/app/Repositories/old_Daemon/PowerRepository.php index 9e0cbc29..7b941f12 100644 --- a/app/Repositories/old_Daemon/PowerRepository.php +++ b/app/Repositories/old_Daemon/PowerRepository.php @@ -48,9 +48,8 @@ class PowerRepository /** * Constuctor for repository. * - * @param \Pterodactyl\Models\Server $server - * @param \Pterodactyl\Models\User|null $user - * @return void + * @param \Pterodactyl\Models\Server $server + * @param \Pterodactyl\Models\User|null $user */ public function __construct(Server $server, User $user = null) { @@ -61,7 +60,7 @@ class PowerRepository /** * Sends a power option to the daemon. * - * @param string $action + * @param string $action * @return string * * @throws \GuzzleHttp\Exception\RequestException @@ -89,8 +88,6 @@ class PowerRepository /** * Starts a server. - * - * @return void */ public function start() { @@ -99,8 +96,6 @@ class PowerRepository /** * Stops a server. - * - * @return void */ public function stop() { @@ -109,8 +104,6 @@ class PowerRepository /** * Restarts a server. - * - * @return void */ public function restart() { @@ -119,8 +112,6 @@ class PowerRepository /** * Kills a server. - * - * @return void */ public function kill() { diff --git a/app/Services/Allocations/AssignmentService.php b/app/Services/Allocations/AssignmentService.php index bc7ea35c..abf31079 100644 --- a/app/Services/Allocations/AssignmentService.php +++ b/app/Services/Allocations/AssignmentService.php @@ -64,8 +64,8 @@ class AssignmentService /** * Insert allocations into the database and link them to a specific node. * - * @param int|\Pterodactyl\Models\Node $node - * @param array $data + * @param int|\Pterodactyl\Models\Node $node + * @param array $data * * @throws \Pterodactyl\Exceptions\DisplayException */ diff --git a/app/Services/Api/KeyService.php b/app/Services/Api/KeyService.php index 4bf03da5..fc67b892 100644 --- a/app/Services/Api/KeyService.php +++ b/app/Services/Api/KeyService.php @@ -76,9 +76,9 @@ class KeyService /** * Create a new API Key on the system with the given permissions. * - * @param array $data - * @param array $permissions - * @param array $administrative + * @param array $data + * @param array $permissions + * @param array $administrative * @return string * * @throws \Exception @@ -136,7 +136,7 @@ class KeyService /** * Delete the API key and associated permissions from the database. * - * @param int $id + * @param int $id * @return bool|null */ public function revoke($id) diff --git a/app/Services/Api/PermissionService.php b/app/Services/Api/PermissionService.php index a669c726..05059979 100644 --- a/app/Services/Api/PermissionService.php +++ b/app/Services/Api/PermissionService.php @@ -46,8 +46,8 @@ class PermissionService /** * Store a permission key in the database. * - * @param string $key - * @param string $permission + * @param string $key + * @param string $permission * @return bool * * @throws \Pterodactyl\Exceptions\Model\DataValidationException diff --git a/app/Services/Components/UuidService.php b/app/Services/Components/UuidService.php index 27d7e541..7f5b0535 100644 --- a/app/Services/Components/UuidService.php +++ b/app/Services/Components/UuidService.php @@ -33,9 +33,9 @@ class UuidService * Generate a unique UUID validating against specified table and column. * Defaults to `users.uuid`. * - * @param string $table - * @param string $field - * @param int $type + * @param string $table + * @param string $field + * @param int $type * @return string * @deprecated */ @@ -55,9 +55,9 @@ class UuidService /** * Generates a ShortUUID code which is 8 characters long and is used for identifying servers in the system. * - * @param string $table - * @param string $field - * @param null|string $attachedUuid + * @param string $table + * @param string $field + * @param null|string $attachedUuid * @return string * @deprecated */ diff --git a/app/Services/Database/DatabaseHostService.php b/app/Services/Database/DatabaseHostService.php index 33d46c31..654ce612 100644 --- a/app/Services/Database/DatabaseHostService.php +++ b/app/Services/Database/DatabaseHostService.php @@ -74,7 +74,7 @@ class DatabaseHostService /** * Create a new database host and persist it to the database. * - * @param array $data + * @param array $data * @return \Pterodactyl\Models\DatabaseHost * * @throws \Throwable @@ -106,8 +106,8 @@ class DatabaseHostService /** * Update a database host and persist to the database. * - * @param int $id - * @param array $data + * @param int $id + * @param array $data * @return mixed * * @throws \Pterodactyl\Exceptions\Model\DataValidationException @@ -135,7 +135,7 @@ class DatabaseHostService /** * Delete a database host if it has no active databases attached to it. * - * @param int $id + * @param int $id * @return bool|null * * @throws \Pterodactyl\Exceptions\DisplayException diff --git a/app/Services/Database/DatabaseManagementService.php b/app/Services/Database/DatabaseManagementService.php index a0343fc7..beecfa9a 100644 --- a/app/Services/Database/DatabaseManagementService.php +++ b/app/Services/Database/DatabaseManagementService.php @@ -74,8 +74,8 @@ class DatabaseManagementService /** * Create a new database that is linked to a specific host. * - * @param int $server - * @param array $data + * @param int $server + * @param array $data * @return \Illuminate\Database\Eloquent\Model * * @throws \Exception @@ -96,10 +96,16 @@ class DatabaseManagementService $this->repository->createDatabase($database->database, 'dynamic'); $this->repository->createUser( - $database->username, $database->remote, $this->encrypter->decrypt($database->password), 'dynamic' + $database->username, + $database->remote, + $this->encrypter->decrypt($database->password), + 'dynamic' ); $this->repository->assignUserToDatabase( - $database->database, $database->username, $database->remote, 'dynamic' + $database->database, + $database->username, + $database->remote, + 'dynamic' ); $this->repository->flush('dynamic'); @@ -125,8 +131,8 @@ class DatabaseManagementService /** * Change the password for a specific user and database combination. * - * @param int $id - * @param string $password + * @param int $id + * @param string $password * @return bool * * @throws \Exception @@ -148,7 +154,10 @@ class DatabaseManagementService $this->repository->dropUser($database->username, $database->remote, 'dynamic'); $this->repository->createUser($database->username, $database->remote, $password, 'dynamic'); $this->repository->assignUserToDatabase( - $database->database, $database->username, $database->remote, 'dynamic' + $database->database, + $database->username, + $database->remote, + 'dynamic' ); $this->repository->flush('dynamic'); @@ -164,7 +173,7 @@ class DatabaseManagementService /** * Delete a database from the given host server. * - * @param int $id + * @param int $id * @return bool|null */ public function delete($id) diff --git a/app/Services/Helpers/TemporaryPasswordService.php b/app/Services/Helpers/TemporaryPasswordService.php index 0e5ff4f2..58bff6b7 100644 --- a/app/Services/Helpers/TemporaryPasswordService.php +++ b/app/Services/Helpers/TemporaryPasswordService.php @@ -67,7 +67,7 @@ class TemporaryPasswordService /** * Store a password reset token for a specific email address. * - * @param string $email + * @param string $email * @return string */ public function generateReset($email) diff --git a/app/Services/LocationService.php b/app/Services/LocationService.php index 98253452..fc888033 100644 --- a/app/Services/LocationService.php +++ b/app/Services/LocationService.php @@ -46,7 +46,7 @@ class LocationService /** * Create the location in the database and return it. * - * @param array $data + * @param array $data * @return \Pterodactyl\Models\Location * * @throws \Pterodactyl\Exceptions\Model\DataValidationException @@ -59,8 +59,8 @@ class LocationService /** * Update location model in the DB. * - * @param int $id - * @param array $data + * @param int $id + * @param array $data * @return \Pterodactyl\Models\Location * * @throws \Pterodactyl\Exceptions\Model\DataValidationException @@ -73,7 +73,7 @@ class LocationService /** * Delete a model from the DB. * - * @param int $id + * @param int $id * @return bool * * @throws \Pterodactyl\Exceptions\DisplayException diff --git a/app/Services/Nodes/CreationService.php b/app/Services/Nodes/CreationService.php index b33817f3..e2327c55 100644 --- a/app/Services/Nodes/CreationService.php +++ b/app/Services/Nodes/CreationService.php @@ -48,7 +48,7 @@ class CreationService /** * Create a new node on the panel. * - * @param array $data + * @param array $data * @return \Pterodactyl\Models\Node * * @throws \Pterodactyl\Exceptions\Model\DataValidationException diff --git a/app/Services/Nodes/UpdateService.php b/app/Services/Nodes/UpdateService.php index e34c9ff2..0b7f7b12 100644 --- a/app/Services/Nodes/UpdateService.php +++ b/app/Services/Nodes/UpdateService.php @@ -68,8 +68,8 @@ class UpdateService /** * Update the configuration values for a given node on the machine. * - * @param int|\Pterodactyl\Models\Node $node - * @param array $data + * @param int|\Pterodactyl\Models\Node $node + * @param array $data * @return mixed * * @throws \Pterodactyl\Exceptions\DisplayException diff --git a/app/Services/Old/APILogService.php b/app/Services/Old/APILogService.php index c8e5c098..d4467041 100644 --- a/app/Services/Old/APILogService.php +++ b/app/Services/Old/APILogService.php @@ -33,10 +33,9 @@ class APILogService /** * Log an API Request. * - * @param \Illuminate\Http\Request $request - * @param null|string $error - * @param bool $authorized - * @return void + * @param \Illuminate\Http\Request $request + * @param null|string $error + * @param bool $authorized */ public static function log(Request $request, $error = null, $authorized = false) { diff --git a/app/Services/Old/DeploymentService.php b/app/Services/Old/DeploymentService.php index f25c04e9..eed48c58 100644 --- a/app/Services/Old/DeploymentService.php +++ b/app/Services/Old/DeploymentService.php @@ -70,8 +70,7 @@ class DeploymentService /** * Set the location to use when auto-deploying. * - * @param int|\Pterodactyl\Models\Location $location - * @return void + * @param int|\Pterodactyl\Models\Location $location */ public function setLocation($location) { @@ -90,8 +89,7 @@ class DeploymentService /** * Set the node to use when auto-deploying. * - * @param int|\Pterodactyl\Models\Node $node - * @return void + * @param int|\Pterodactyl\Models\Node $node */ public function setNode($node) { @@ -108,8 +106,7 @@ class DeploymentService /** * Set the amount of disk space to be used by the new server. * - * @param int $disk - * @return void + * @param int $disk */ public function setDisk(int $disk) { @@ -121,8 +118,7 @@ class DeploymentService /** * Set the amount of memory to be used by the new server. * - * @param int $memory - * @return void + * @param int $memory */ public function setMemory(int $memory) { @@ -134,8 +130,7 @@ class DeploymentService /** * Return a random location model. * - * @param array $exclude - * @return void; + * @param array $exclude */ protected function findLocation(array $exclude = []) { @@ -154,8 +149,6 @@ class DeploymentService /** * Return a model instance of a random node. - * - * @return void; */ protected function findNode(array $exclude = []) { @@ -240,8 +233,6 @@ class DeploymentService /** * Select and return the node to be used by the auto-deployment system. - * - * @return void */ public function select() { diff --git a/app/Services/Old/VersionService.php b/app/Services/Old/VersionService.php index e3eaf8ad..7134b31f 100644 --- a/app/Services/Old/VersionService.php +++ b/app/Services/Old/VersionService.php @@ -38,8 +38,6 @@ class VersionService /** * Version constructor. - * - * @return void */ public function __construct() { diff --git a/app/Services/Packs/ExportPackService.php b/app/Services/Packs/ExportPackService.php index 1432efb9..42f1cf30 100644 --- a/app/Services/Packs/ExportPackService.php +++ b/app/Services/Packs/ExportPackService.php @@ -67,8 +67,8 @@ class ExportPackService /** * Prepare a pack for export. * - * @param int|\Pterodactyl\Models\Pack $pack - * @param bool $files + * @param int|\Pterodactyl\Models\Pack $pack + * @param bool $files * @return string * * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException diff --git a/app/Services/Packs/PackCreationService.php b/app/Services/Packs/PackCreationService.php index 10daaeb3..9890882f 100644 --- a/app/Services/Packs/PackCreationService.php +++ b/app/Services/Packs/PackCreationService.php @@ -74,8 +74,8 @@ class PackCreationService /** * Add a new service pack to the system. * - * @param array $data - * @param \Illuminate\Http\UploadedFile|null $file + * @param array $data + * @param \Illuminate\Http\UploadedFile|null $file * @return \Pterodactyl\Models\Pack * * @throws \Pterodactyl\Exceptions\Model\DataValidationException @@ -103,7 +103,8 @@ class PackCreationService $this->connection->beginTransaction(); $pack = $this->repository->create(array_merge( - ['uuid' => Uuid::uuid4()], $data + ['uuid' => Uuid::uuid4()], + $data )); $this->storage->disk()->makeDirectory('packs/' . $pack->uuid); diff --git a/app/Services/Packs/PackUpdateService.php b/app/Services/Packs/PackUpdateService.php index 22e38727..5928b95a 100644 --- a/app/Services/Packs/PackUpdateService.php +++ b/app/Services/Packs/PackUpdateService.php @@ -58,8 +58,8 @@ class PackUpdateService /** * Update a pack. * - * @param int|\Pterodactyl\Models\Pack $pack - * @param array $data + * @param int|\Pterodactyl\Models\Pack $pack + * @param array $data * @return bool * * @throws \Pterodactyl\Exceptions\Model\DataValidationException diff --git a/app/Services/Packs/TemplateUploadService.php b/app/Services/Packs/TemplateUploadService.php index c4e5b5fb..248ccfcf 100644 --- a/app/Services/Packs/TemplateUploadService.php +++ b/app/Services/Packs/TemplateUploadService.php @@ -67,8 +67,8 @@ class TemplateUploadService /** * Process an uploaded file to create a new pack from a JSON or ZIP format. * - * @param int $option - * @param \Illuminate\Http\UploadedFile $file + * @param int $option + * @param \Illuminate\Http\UploadedFile $file * @return \Pterodactyl\Models\Pack * * @throws \Pterodactyl\Exceptions\Model\DataValidationException @@ -103,8 +103,8 @@ class TemplateUploadService /** * Process a ZIP file to create a pack and stored archive. * - * @param int $option - * @param \Illuminate\Http\UploadedFile $file + * @param int $option + * @param \Illuminate\Http\UploadedFile $file * @return \Pterodactyl\Models\Pack * * @throws \Pterodactyl\Exceptions\Model\DataValidationException diff --git a/app/Services/Servers/BuildModificationService.php b/app/Services/Servers/BuildModificationService.php index 24c52a16..3b40b9d2 100644 --- a/app/Services/Servers/BuildModificationService.php +++ b/app/Services/Servers/BuildModificationService.php @@ -97,8 +97,8 @@ class BuildModificationService /** * Set build array parameters. * - * @param string $key - * @param mixed $value + * @param string $key + * @param mixed $value */ public function setBuild($key, $value) { @@ -108,7 +108,7 @@ class BuildModificationService /** * Return the build array or an item out of the build array. * - * @param string|null $attribute + * @param string|null $attribute * @return array|mixed|null */ public function getBuild($attribute = null) @@ -123,8 +123,8 @@ class BuildModificationService /** * Change the build details for a specified server. * - * @param int|\Pterodactyl\Models\Server $server - * @param array $data + * @param int|\Pterodactyl\Models\Server $server + * @param array $data * * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException diff --git a/app/Services/Servers/CreationService.php b/app/Services/Servers/CreationService.php index ff0b50f7..baad7307 100644 --- a/app/Services/Servers/CreationService.php +++ b/app/Services/Servers/CreationService.php @@ -129,7 +129,7 @@ class CreationService /** * Create a server on both the panel and daemon. * - * @param array $data + * @param array $data * @return mixed * * @throws \Pterodactyl\Exceptions\DisplayException diff --git a/app/Services/Servers/DeletionService.php b/app/Services/Servers/DeletionService.php index a3e34e3a..850ca9ff 100644 --- a/app/Services/Servers/DeletionService.php +++ b/app/Services/Servers/DeletionService.php @@ -100,7 +100,7 @@ class DeletionService /** * Set if the server should be forcibly deleted from the panel (ignoring daemon errors) or not. * - * @param bool $bool + * @param bool $bool * @return $this */ public function withForce($bool = true) diff --git a/app/Services/Servers/DetailsModificationService.php b/app/Services/Servers/DetailsModificationService.php index 7b1aac37..6e31e2de 100644 --- a/app/Services/Servers/DetailsModificationService.php +++ b/app/Services/Servers/DetailsModificationService.php @@ -135,8 +135,8 @@ class DetailsModificationService /** * Update the docker container for a specified server. * - * @param int|\Pterodactyl\Models\Server $server - * @param string $image + * @param int|\Pterodactyl\Models\Server $server + * @param string $image * * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException diff --git a/app/Services/Servers/EnvironmentService.php b/app/Services/Servers/EnvironmentService.php index 94920534..a8bd8517 100644 --- a/app/Services/Servers/EnvironmentService.php +++ b/app/Services/Servers/EnvironmentService.php @@ -59,8 +59,8 @@ class EnvironmentService * Dynamically configure additional environment variables to be assigned * with a specific server. * - * @param string $key - * @param callable $closure + * @param string $key + * @param callable $closure * @return $this */ public function setEnvironmentKey($key, callable $closure) @@ -74,7 +74,7 @@ class EnvironmentService * Take all of the environment variables configured for this server and return * them in an easy to process format. * - * @param int|\Pterodactyl\Models\Server $server + * @param int|\Pterodactyl\Models\Server $server * @return array */ public function process($server) diff --git a/app/Services/Servers/ReinstallService.php b/app/Services/Servers/ReinstallService.php index f9b0f16e..4dc5d125 100644 --- a/app/Services/Servers/ReinstallService.php +++ b/app/Services/Servers/ReinstallService.php @@ -75,7 +75,7 @@ class ReinstallService } /** - * @param int|\Pterodactyl\Models\Server $server + * @param int|\Pterodactyl\Models\Server $server * * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException diff --git a/app/Services/Servers/StartupModificationService.php b/app/Services/Servers/StartupModificationService.php index c9ea2391..73af5c79 100644 --- a/app/Services/Servers/StartupModificationService.php +++ b/app/Services/Servers/StartupModificationService.php @@ -107,7 +107,7 @@ class StartupModificationService /** * Determine if this function should run at an administrative level. * - * @param bool $bool + * @param bool $bool * @return $this */ public function isAdmin($bool = true) @@ -120,8 +120,8 @@ class StartupModificationService /** * Process startup modification for a server. * - * @param int|\Pterodactyl\Models\Server $server - * @param array $data + * @param int|\Pterodactyl\Models\Server $server + * @param array $data * * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException diff --git a/app/Services/Servers/SuspensionService.php b/app/Services/Servers/SuspensionService.php index 63f4aaca..4996bb60 100644 --- a/app/Services/Servers/SuspensionService.php +++ b/app/Services/Servers/SuspensionService.php @@ -77,8 +77,8 @@ class SuspensionService /** * Suspends a server on the system. * - * @param int|\Pterodactyl\Models\Server $server - * @param string $action + * @param int|\Pterodactyl\Models\Server $server + * @param string $action * @return bool * * @throws \Pterodactyl\Exceptions\DisplayException @@ -93,7 +93,8 @@ class SuspensionService if (! in_array($action, ['suspend', 'unsuspend'])) { throw new \InvalidArgumentException(sprintf( - 'Action must be either suspend or unsuspend, %s passed.', $action + 'Action must be either suspend or unsuspend, %s passed.', + $action )); } diff --git a/app/Services/Servers/UsernameGenerationService.php b/app/Services/Servers/UsernameGenerationService.php index 4c356924..488288b4 100644 --- a/app/Services/Servers/UsernameGenerationService.php +++ b/app/Services/Servers/UsernameGenerationService.php @@ -30,8 +30,8 @@ class UsernameGenerationService * Generate a unique username to be used for SFTP connections and identification * of the server docker container on the host system. * - * @param string $name - * @param null $identifier + * @param string $name + * @param null $identifier * @return string */ public function generate($name, $identifier = null) diff --git a/app/Services/Servers/VariableValidatorService.php b/app/Services/Servers/VariableValidatorService.php index 1904f675..74934904 100644 --- a/app/Services/Servers/VariableValidatorService.php +++ b/app/Services/Servers/VariableValidatorService.php @@ -90,7 +90,7 @@ class VariableValidatorService /** * Set the fields with populated data to validate. * - * @param array $fields + * @param array $fields * @return $this */ public function setFields(array $fields) @@ -103,7 +103,7 @@ class VariableValidatorService /** * Set this function to be running at the administrative level. * - * @param bool $bool + * @param bool $bool * @return $this */ public function isAdmin($bool = true) @@ -116,7 +116,7 @@ class VariableValidatorService /** * Validate all of the passed data aganist the given service option variables. * - * @param int $option + * @param int $option * @return $this */ public function validate($option) diff --git a/app/Services/Services/Options/InstallScriptUpdateService.php b/app/Services/Services/Options/InstallScriptUpdateService.php index f1fd9ae2..efdd08df 100644 --- a/app/Services/Services/Options/InstallScriptUpdateService.php +++ b/app/Services/Services/Options/InstallScriptUpdateService.php @@ -48,8 +48,8 @@ class InstallScriptUpdateService /** * Modify the option install script for a given service option. * - * @param int|\Pterodactyl\Models\ServiceOption $option - * @param array $data + * @param int|\Pterodactyl\Models\ServiceOption $option + * @param array $data * * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException diff --git a/app/Services/Services/Options/OptionCreationService.php b/app/Services/Services/Options/OptionCreationService.php index ac4f179f..8755f0e9 100644 --- a/app/Services/Services/Options/OptionCreationService.php +++ b/app/Services/Services/Options/OptionCreationService.php @@ -47,7 +47,7 @@ class OptionCreationService /** * Create a new service option and assign it to the given service. * - * @param array $data + * @param array $data * @return \Pterodactyl\Models\ServiceOption * * @throws \Pterodactyl\Exceptions\Model\DataValidationException diff --git a/app/Services/Services/Options/OptionDeletionService.php b/app/Services/Services/Options/OptionDeletionService.php index b94132a5..c614348f 100644 --- a/app/Services/Services/Options/OptionDeletionService.php +++ b/app/Services/Services/Options/OptionDeletionService.php @@ -57,7 +57,7 @@ class OptionDeletionService /** * Delete an option from the database if it has no active servers attached to it. * - * @param int $option + * @param int $option * @return int * * @throws \Pterodactyl\Exceptions\Service\HasActiveServersException diff --git a/app/Services/Services/Options/OptionUpdateService.php b/app/Services/Services/Options/OptionUpdateService.php index b2e310fb..6bfe634e 100644 --- a/app/Services/Services/Options/OptionUpdateService.php +++ b/app/Services/Services/Options/OptionUpdateService.php @@ -48,8 +48,8 @@ class OptionUpdateService /** * Update a service option. * - * @param int|\Pterodactyl\Models\ServiceOption $option - * @param array $data + * @param int|\Pterodactyl\Models\ServiceOption $option + * @param array $data * * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException diff --git a/app/Services/Services/ServiceCreationService.php b/app/Services/Services/ServiceCreationService.php index 9e9b4e20..fc76f99d 100644 --- a/app/Services/Services/ServiceCreationService.php +++ b/app/Services/Services/ServiceCreationService.php @@ -59,7 +59,7 @@ class ServiceCreationService /** * Create a new service on the system. * - * @param array $data + * @param array $data * @return \Pterodactyl\Models\Service * * @throws \Pterodactyl\Exceptions\Model\DataValidationException diff --git a/app/Services/Services/ServiceDeletionService.php b/app/Services/Services/ServiceDeletionService.php index f127d361..9a299bee 100644 --- a/app/Services/Services/ServiceDeletionService.php +++ b/app/Services/Services/ServiceDeletionService.php @@ -57,7 +57,7 @@ class ServiceDeletionService /** * Delete a service from the system only if there are no servers attached to it. * - * @param int $service + * @param int $service * @return int * * @throws \Pterodactyl\Exceptions\Service\HasActiveServersException diff --git a/app/Services/Services/ServiceUpdateService.php b/app/Services/Services/ServiceUpdateService.php index 203f52df..3c573ff1 100644 --- a/app/Services/Services/ServiceUpdateService.php +++ b/app/Services/Services/ServiceUpdateService.php @@ -46,8 +46,8 @@ class ServiceUpdateService /** * Update a service and prevent changing the author once it is set. * - * @param int $service - * @param array $data + * @param int $service + * @param array $data * @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ diff --git a/app/Services/Services/Variables/VariableCreationService.php b/app/Services/Services/Variables/VariableCreationService.php index 90820732..78301ac7 100644 --- a/app/Services/Services/Variables/VariableCreationService.php +++ b/app/Services/Services/Variables/VariableCreationService.php @@ -53,8 +53,8 @@ class VariableCreationService /** * Create a new variable for a given service option. * - * @param int|\Pterodactyl\Models\ServiceOption $option - * @param array $data + * @param int|\Pterodactyl\Models\ServiceOption $option + * @param array $data * @return \Pterodactyl\Models\ServiceVariable * * @throws \Pterodactyl\Exceptions\Model\DataValidationException @@ -68,7 +68,8 @@ class VariableCreationService if (in_array(strtoupper(array_get($data, 'env_variable')), explode(',', ServiceVariable::RESERVED_ENV_NAMES))) { throw new ReservedVariableNameException(sprintf( - 'Cannot use the protected name %s for this environment variable.', array_get($data, 'env_variable') + 'Cannot use the protected name %s for this environment variable.', + array_get($data, 'env_variable') )); } diff --git a/app/Services/Services/Variables/VariableUpdateService.php b/app/Services/Services/Variables/VariableUpdateService.php index bce5dd70..1806c11c 100644 --- a/app/Services/Services/Variables/VariableUpdateService.php +++ b/app/Services/Services/Variables/VariableUpdateService.php @@ -49,8 +49,8 @@ class VariableUpdateService /** * Update a specific service variable. * - * @param int|\Pterodactyl\Models\ServiceVariable $variable - * @param array $data + * @param int|\Pterodactyl\Models\ServiceVariable $variable + * @param array $data * @return mixed * * @throws \Pterodactyl\Exceptions\DisplayException diff --git a/app/Services/Users/CreationService.php b/app/Services/Users/CreationService.php index f6a60f1c..a5e5a55b 100644 --- a/app/Services/Users/CreationService.php +++ b/app/Services/Users/CreationService.php @@ -93,7 +93,7 @@ class CreationService /** * Create a new user on the system. * - * @param array $data + * @param array $data * @return \Pterodactyl\Models\User * * @throws \Exception diff --git a/app/Services/Users/UpdateService.php b/app/Services/Users/UpdateService.php index 5c123467..5ba352f6 100644 --- a/app/Services/Users/UpdateService.php +++ b/app/Services/Users/UpdateService.php @@ -56,8 +56,8 @@ class UpdateService /** * Update the user model instance. * - * @param int $id - * @param array $data + * @param int $id + * @param array $data * @return mixed * * @throws \Pterodactyl\Exceptions\Model\DataValidationException diff --git a/app/Transformers/Admin/AllocationTransformer.php b/app/Transformers/Admin/AllocationTransformer.php index e7bd15c3..246d1a75 100644 --- a/app/Transformers/Admin/AllocationTransformer.php +++ b/app/Transformers/Admin/AllocationTransformer.php @@ -47,9 +47,8 @@ class AllocationTransformer extends TransformerAbstract /** * Setup request object for transformer. * - * @param \Illuminate\Http\Request|bool $request - * @param bool $filter - * @return void + * @param \Illuminate\Http\Request|bool $request + * @param bool $filter */ public function __construct($request = false, $filter = false) { diff --git a/app/Transformers/Admin/LocationTransformer.php b/app/Transformers/Admin/LocationTransformer.php index f3fd9588..c3277ea1 100644 --- a/app/Transformers/Admin/LocationTransformer.php +++ b/app/Transformers/Admin/LocationTransformer.php @@ -50,8 +50,7 @@ class LocationTransformer extends TransformerAbstract /** * Setup request object for transformer. * - * @param \Illuminate\Http\Request|bool $request - * @return void + * @param \Illuminate\Http\Request|bool $request */ public function __construct($request = false) { diff --git a/app/Transformers/Admin/NodeTransformer.php b/app/Transformers/Admin/NodeTransformer.php index d18b64f2..bba4162a 100644 --- a/app/Transformers/Admin/NodeTransformer.php +++ b/app/Transformers/Admin/NodeTransformer.php @@ -51,8 +51,7 @@ class NodeTransformer extends TransformerAbstract /** * Setup request object for transformer. * - * @param \Illuminate\Http\Request|bool $request - * @return void + * @param \Illuminate\Http\Request|bool $request */ public function __construct($request = false) { diff --git a/app/Transformers/Admin/OptionTransformer.php b/app/Transformers/Admin/OptionTransformer.php index 5b86b53d..b0836fea 100644 --- a/app/Transformers/Admin/OptionTransformer.php +++ b/app/Transformers/Admin/OptionTransformer.php @@ -52,8 +52,7 @@ class OptionTransformer extends TransformerAbstract /** * Setup request object for transformer. * - * @param \Illuminate\Http\Request|bool $request - * @return void + * @param \Illuminate\Http\Request|bool $request */ public function __construct($request = false) { diff --git a/app/Transformers/Admin/PackTransformer.php b/app/Transformers/Admin/PackTransformer.php index 8d059faa..e1239c0f 100644 --- a/app/Transformers/Admin/PackTransformer.php +++ b/app/Transformers/Admin/PackTransformer.php @@ -50,8 +50,7 @@ class PackTransformer extends TransformerAbstract /** * Setup request object for transformer. * - * @param \Illuminate\Http\Request|bool $request - * @return void + * @param \Illuminate\Http\Request|bool $request */ public function __construct($request = false) { diff --git a/app/Transformers/Admin/ServerTransformer.php b/app/Transformers/Admin/ServerTransformer.php index 4d94b7e1..8a9bf3a6 100644 --- a/app/Transformers/Admin/ServerTransformer.php +++ b/app/Transformers/Admin/ServerTransformer.php @@ -57,8 +57,7 @@ class ServerTransformer extends TransformerAbstract /** * Setup request object for transformer. * - * @param \Illuminate\Http\Request|bool $request - * @return void + * @param \Illuminate\Http\Request|bool $request */ public function __construct($request = false) { diff --git a/app/Transformers/Admin/ServerVariableTransformer.php b/app/Transformers/Admin/ServerVariableTransformer.php index 3211e029..120f8ef3 100644 --- a/app/Transformers/Admin/ServerVariableTransformer.php +++ b/app/Transformers/Admin/ServerVariableTransformer.php @@ -47,8 +47,7 @@ class ServerVariableTransformer extends TransformerAbstract /** * Setup request object for transformer. * - * @param \Illuminate\Http\Request|bool $request - * @return void + * @param \Illuminate\Http\Request|bool $request */ public function __construct($request = false) { diff --git a/app/Transformers/Admin/ServiceTransformer.php b/app/Transformers/Admin/ServiceTransformer.php index 2df1fc8c..57639df9 100644 --- a/app/Transformers/Admin/ServiceTransformer.php +++ b/app/Transformers/Admin/ServiceTransformer.php @@ -51,8 +51,7 @@ class ServiceTransformer extends TransformerAbstract /** * Setup request object for transformer. * - * @param \Illuminate\Http\Request|bool $request - * @return void + * @param \Illuminate\Http\Request|bool $request */ public function __construct($request = false) { diff --git a/app/Transformers/Admin/ServiceVariableTransformer.php b/app/Transformers/Admin/ServiceVariableTransformer.php index aa10428d..190a08a6 100644 --- a/app/Transformers/Admin/ServiceVariableTransformer.php +++ b/app/Transformers/Admin/ServiceVariableTransformer.php @@ -47,8 +47,7 @@ class ServiceVariableTransformer extends TransformerAbstract /** * Setup request object for transformer. * - * @param \Illuminate\Http\Request|bool $request - * @return void + * @param \Illuminate\Http\Request|bool $request */ public function __construct($request = false) { diff --git a/app/Transformers/Admin/SubuserTransformer.php b/app/Transformers/Admin/SubuserTransformer.php index 129da7ad..cfaf28b7 100644 --- a/app/Transformers/Admin/SubuserTransformer.php +++ b/app/Transformers/Admin/SubuserTransformer.php @@ -41,8 +41,7 @@ class SubuserTransformer extends TransformerAbstract /** * Setup request object for transformer. * - * @param \Illuminate\Http\Request|bool $request - * @return void + * @param \Illuminate\Http\Request|bool $request */ public function __construct($request = false) { diff --git a/app/Transformers/Admin/UserTransformer.php b/app/Transformers/Admin/UserTransformer.php index 0d26961b..2cbc37e7 100644 --- a/app/Transformers/Admin/UserTransformer.php +++ b/app/Transformers/Admin/UserTransformer.php @@ -50,8 +50,7 @@ class UserTransformer extends TransformerAbstract /** * Setup request object for transformer. * - * @param \Illuminate\Http\Request|bool $request - * @return void + * @param \Illuminate\Http\Request|bool $request */ public function __construct($request = false) { diff --git a/app/Transformers/User/AllocationTransformer.php b/app/Transformers/User/AllocationTransformer.php index 0fd0be45..eb0f28c9 100644 --- a/app/Transformers/User/AllocationTransformer.php +++ b/app/Transformers/User/AllocationTransformer.php @@ -39,8 +39,6 @@ class AllocationTransformer extends TransformerAbstract /** * Setup allocation transformer with access to server data. - * - * @return void */ public function __construct(Server $server) { diff --git a/app/helpers.php b/app/helpers.php index 3f218cc5..763886f0 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -26,8 +26,8 @@ if (! function_exists('human_readable')) { /** * Generate a human-readable filesize for a given file path. * - * @param string $path - * @param int $precision + * @param string $path + * @param int $precision * @return string */ function human_readable($path, $precision = 2) diff --git a/config/app.php b/config/app.php index c211d37f..af3cf02f 100644 --- a/config/app.php +++ b/config/app.php @@ -1,7 +1,6 @@ env('APP_ENV', 'production'), 'version' => env('APP_VERSION', 'canary'), @@ -126,7 +125,6 @@ return [ */ 'providers' => [ - /* * Laravel Framework Service Providers... */ @@ -181,7 +179,6 @@ return [ Lord\Laroute\LarouteServiceProvider::class, Spatie\Fractal\FractalServiceProvider::class, Sofa\Eloquence\ServiceProvider::class, - ], /* @@ -196,53 +193,50 @@ return [ */ 'aliases' => [ - - 'Alert' => Prologue\Alerts\Facades\Alert::class, - 'App' => Illuminate\Support\Facades\App::class, - 'Artisan' => Illuminate\Support\Facades\Artisan::class, - 'Auth' => Illuminate\Support\Facades\Auth::class, - 'Blade' => Illuminate\Support\Facades\Blade::class, - 'Bus' => Illuminate\Support\Facades\Bus::class, - 'Cache' => Illuminate\Support\Facades\Cache::class, - 'Carbon' => Carbon\Carbon::class, - 'Config' => Illuminate\Support\Facades\Config::class, - 'Cookie' => Illuminate\Support\Facades\Cookie::class, - 'Cron' => Cron\CronExpression::class, - 'Crypt' => Illuminate\Support\Facades\Crypt::class, - 'DB' => Illuminate\Support\Facades\DB::class, - 'Debugbar' => Barryvdh\Debugbar\Facade::class, - 'Eloquent' => Illuminate\Database\Eloquent\Model::class, - 'Event' => Illuminate\Support\Facades\Event::class, - 'File' => Illuminate\Support\Facades\File::class, - 'Fractal' => Spatie\Fractal\FractalFacade::class, - 'Gate' => Illuminate\Support\Facades\Gate::class, + 'Alert' => Prologue\Alerts\Facades\Alert::class, + 'App' => Illuminate\Support\Facades\App::class, + 'Artisan' => Illuminate\Support\Facades\Artisan::class, + 'Auth' => Illuminate\Support\Facades\Auth::class, + 'Blade' => Illuminate\Support\Facades\Blade::class, + 'Bus' => Illuminate\Support\Facades\Bus::class, + 'Cache' => Illuminate\Support\Facades\Cache::class, + 'Carbon' => Carbon\Carbon::class, + 'Config' => Illuminate\Support\Facades\Config::class, + 'Cookie' => Illuminate\Support\Facades\Cookie::class, + 'Cron' => Cron\CronExpression::class, + 'Crypt' => Illuminate\Support\Facades\Crypt::class, + 'DB' => Illuminate\Support\Facades\DB::class, + 'Debugbar' => Barryvdh\Debugbar\Facade::class, + 'Eloquent' => Illuminate\Database\Eloquent\Model::class, + 'Event' => Illuminate\Support\Facades\Event::class, + 'File' => Illuminate\Support\Facades\File::class, + 'Fractal' => Spatie\Fractal\FractalFacade::class, + 'Gate' => Illuminate\Support\Facades\Gate::class, 'Google2FA' => PragmaRX\Google2FA\Vendor\Laravel\Facade::class, - 'Hash' => Illuminate\Support\Facades\Hash::class, - 'Input' => Illuminate\Support\Facades\Input::class, + 'Hash' => Illuminate\Support\Facades\Hash::class, + 'Input' => Illuminate\Support\Facades\Input::class, 'Inspiring' => Illuminate\Foundation\Inspiring::class, 'Javascript' => Laracasts\Utilities\JavaScript\JavaScriptFacade::class, - 'Lang' => Illuminate\Support\Facades\Lang::class, - 'Log' => Illuminate\Support\Facades\Log::class, - 'Mail' => Illuminate\Support\Facades\Mail::class, + 'Lang' => Illuminate\Support\Facades\Lang::class, + 'Log' => Illuminate\Support\Facades\Log::class, + 'Mail' => Illuminate\Support\Facades\Mail::class, 'Notification' => Illuminate\Support\Facades\Notification::class, - 'Password' => Illuminate\Support\Facades\Password::class, - 'Queue' => Illuminate\Support\Facades\Queue::class, - 'Redirect' => Illuminate\Support\Facades\Redirect::class, - 'Redis' => Illuminate\Support\Facades\Redis::class, - 'Request' => Illuminate\Support\Facades\Request::class, - 'Response' => Illuminate\Support\Facades\Response::class, - 'Route' => Illuminate\Support\Facades\Route::class, - 'Schema' => Illuminate\Support\Facades\Schema::class, - 'Settings' => Krucas\Settings\Facades\Settings::class, - 'Session' => Illuminate\Support\Facades\Session::class, - 'Storage' => Illuminate\Support\Facades\Storage::class, - 'Theme' => igaster\laravelTheme\Facades\Theme::class, - 'URL' => Illuminate\Support\Facades\URL::class, - 'Uuid' => Webpatser\Uuid\Uuid::class, + 'Password' => Illuminate\Support\Facades\Password::class, + 'Queue' => Illuminate\Support\Facades\Queue::class, + 'Redirect' => Illuminate\Support\Facades\Redirect::class, + 'Redis' => Illuminate\Support\Facades\Redis::class, + 'Request' => Illuminate\Support\Facades\Request::class, + 'Response' => Illuminate\Support\Facades\Response::class, + 'Route' => Illuminate\Support\Facades\Route::class, + 'Schema' => Illuminate\Support\Facades\Schema::class, + 'Settings' => Krucas\Settings\Facades\Settings::class, + 'Session' => Illuminate\Support\Facades\Session::class, + 'Storage' => Illuminate\Support\Facades\Storage::class, + 'Theme' => igaster\laravelTheme\Facades\Theme::class, + 'URL' => Illuminate\Support\Facades\URL::class, + 'Uuid' => Webpatser\Uuid\Uuid::class, 'Validator' => Illuminate\Support\Facades\Validator::class, - 'Version' => Pterodactyl\Facades\Version::class, - 'View' => Illuminate\Support\Facades\View::class, - + 'Version' => Pterodactyl\Facades\Version::class, + 'View' => Illuminate\Support\Facades\View::class, ], - ]; diff --git a/config/auth.php b/config/auth.php index 6f7fc83c..b83dd9ec 100644 --- a/config/auth.php +++ b/config/auth.php @@ -1,7 +1,6 @@ 60, ], ], - ]; diff --git a/config/broadcasting.php b/config/broadcasting.php index 85e04512..9c4c792d 100644 --- a/config/broadcasting.php +++ b/config/broadcasting.php @@ -1,7 +1,6 @@ [ - 'pusher' => [ 'driver' => 'pusher', 'key' => env('PUSHER_KEY'), @@ -48,7 +46,5 @@ return [ 'null' => [ 'driver' => 'null', ], - ], - ]; diff --git a/config/cache.php b/config/cache.php index 7bd9dd70..fb619fb5 100644 --- a/config/cache.php +++ b/config/cache.php @@ -1,7 +1,6 @@ [ - 'apc' => [ 'driver' => 'apc', ], @@ -69,7 +67,6 @@ return [ 'driver' => 'redis', 'connection' => 'default', ], - ], /* @@ -84,5 +81,4 @@ return [ */ 'prefix' => 'laravel', - ]; diff --git a/config/compile.php b/config/compile.php index 04807eac..cbf7739a 100644 --- a/config/compile.php +++ b/config/compile.php @@ -1,7 +1,6 @@ [ - // ], /* @@ -29,7 +27,5 @@ return [ */ 'providers' => [ - // ], - ]; diff --git a/config/database.php b/config/database.php index 58324a0b..b9ce78c0 100644 --- a/config/database.php +++ b/config/database.php @@ -1,7 +1,6 @@ [ 'mysql' => [ - 'driver' => 'mysql', - 'host' => env('DB_HOST', 'localhost'), - 'port' => env('DB_PORT', '3306'), - 'database' => env('DB_DATABASE', 'forge'), - 'username' => env('DB_USERNAME', 'forge'), - 'password' => env('DB_PASSWORD', ''), - 'charset' => 'utf8', + 'driver' => 'mysql', + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'forge'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => 'utf8', 'collation' => 'utf8_unicode_ci', - 'prefix' => '', - 'strict' => false, + 'prefix' => '', + 'strict' => false, ], ], @@ -79,5 +78,4 @@ return [ 'database' => 0, ], ], - ]; diff --git a/config/debugbar.php b/config/debugbar.php index 05e78c34..f1a1fd75 100644 --- a/config/debugbar.php +++ b/config/debugbar.php @@ -1,7 +1,6 @@ [ - 'phpinfo' => true, // Php version - 'messages' => true, // Messages - 'time' => true, // Time Datalogger - 'memory' => true, // Memory usage - 'exceptions' => true, // Exception displayer - 'log' => true, // Logs from Monolog (merged in messages if enabled) - 'db' => true, // Show database (PDO) queries and bindings - 'views' => true, // Views with their data - 'route' => true, // Current route information - 'laravel' => false, // Laravel version and environment - 'events' => true, // All events fired + 'phpinfo' => true, // Php version + 'messages' => true, // Messages + 'time' => true, // Time Datalogger + 'memory' => true, // Memory usage + 'exceptions' => true, // Exception displayer + 'log' => true, // Logs from Monolog (merged in messages if enabled) + 'db' => true, // Show database (PDO) queries and bindings + 'views' => true, // Views with their data + 'route' => true, // Current route information + 'laravel' => false, // Laravel version and environment + 'events' => true, // All events fired 'default_request' => false, // Regular or special Symfony request logger 'symfony_request' => true, // Only one can be enabled.. - 'mail' => true, // Catch mail messages - 'logs' => false, // Add the latest log messages - 'files' => false, // Show the included files - 'config' => false, // Display config settings - 'auth' => false, // Display Laravel authentication status - 'gate' => false, // Display Laravel Gate checks - 'session' => true, // Display session data + 'mail' => true, // Catch mail messages + 'logs' => false, // Add the latest log messages + 'files' => false, // Show the included files + 'config' => false, // Display config settings + 'auth' => false, // Display Laravel authentication status + 'gate' => false, // Display Laravel Gate checks + 'session' => true, // Display session data ], /* @@ -118,14 +117,14 @@ return [ 'show_name' => false, // Also show the users name/email in the debugbar ], 'db' => [ - 'with_params' => true, // Render SQL with the parameters substituted - 'timeline' => true, // Add the queries to the timeline - 'backtrace' => true, // EXPERIMENTAL: Use a backtrace to find the origin of the query in your files. + 'with_params' => true, // Render SQL with the parameters substituted + 'timeline' => true, // Add the queries to the timeline + 'backtrace' => true, // EXPERIMENTAL: Use a backtrace to find the origin of the query in your files. 'explain' => [ // EXPERIMENTAL: Show EXPLAIN output on queries 'enabled' => false, 'types' => ['SELECT', 'INSERT', 'UPDATE', 'DELETE'], // array('SELECT', 'INSERT', 'UPDATE', 'DELETE'); for MySQL 5.6.3+ ], - 'hints' => false, // Show hints for common mistakes + 'hints' => false, // Show hints for common mistakes ], 'mail' => [ 'full_log' => false, @@ -165,5 +164,4 @@ return [ | */ 'route_prefix' => '_debugbar', - ]; diff --git a/config/filesystems.php b/config/filesystems.php index e726fda0..30514293 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -1,7 +1,6 @@ [ - 'local' => [ 'driver' => 'local', 'root' => storage_path('app'), @@ -63,7 +61,5 @@ return [ 'region' => env('AWS_REGION'), 'bucket' => env('AWS_BUCKET'), ], - ], - ]; diff --git a/config/javascript.php b/config/javascript.php index 1aeb222e..57504395 100644 --- a/config/javascript.php +++ b/config/javascript.php @@ -1,7 +1,6 @@ 'Pterodactyl', - ]; diff --git a/config/laravel-fractal.php b/config/laravel-fractal.php index 32ced203..92bfe2ce 100644 --- a/config/laravel-fractal.php +++ b/config/laravel-fractal.php @@ -1,7 +1,6 @@ League\Fractal\Serializer\JsonApiSerializer::class, - ]; diff --git a/config/laroute.php b/config/laroute.php index 7b332c40..b2b4a2f3 100644 --- a/config/laroute.php +++ b/config/laroute.php @@ -1,7 +1,6 @@ '', - ]; diff --git a/config/mail.php b/config/mail.php index 146e1b11..f4779343 100644 --- a/config/mail.php +++ b/config/mail.php @@ -1,7 +1,6 @@ 'alert_messages', - ]; diff --git a/config/pterodactyl.php b/config/pterodactyl.php index ba604d1b..32b239bd 100644 --- a/config/pterodactyl.php +++ b/config/pterodactyl.php @@ -1,7 +1,6 @@ [ - 'sync' => [ 'driver' => 'sync', ], @@ -43,20 +41,19 @@ return [ 'sqs' => [ 'driver' => 'sqs', - 'key' => env('SQS_KEY'), + 'key' => env('SQS_KEY'), 'secret' => env('SQS_SECRET'), 'prefix' => env('SQS_QUEUE_PREFIX'), - 'queue' => env('QUEUE_STANDARD', 'standard'), + 'queue' => env('QUEUE_STANDARD', 'standard'), 'region' => env('SQS_REGION', 'us-east-1'), ], 'redis' => [ 'driver' => 'redis', 'connection' => 'default', - 'queue' => env('QUEUE_STANDARD', 'standard'), + 'queue' => env('QUEUE_STANDARD', 'standard'), 'retry_after' => 90, ], - ], /* @@ -74,5 +71,4 @@ return [ 'database' => 'mysql', 'table' => 'failed_jobs', ], - ]; diff --git a/config/recaptcha.php b/config/recaptcha.php index 646c9931..1bac5a87 100644 --- a/config/recaptcha.php +++ b/config/recaptcha.php @@ -1,7 +1,6 @@ true, - ]; diff --git a/config/services.php b/config/services.php index e16817cc..be637e50 100644 --- a/config/services.php +++ b/config/services.php @@ -1,7 +1,6 @@ [ - 'key' => env('SES_KEY'), + 'key' => env('SES_KEY'), 'secret' => env('SES_SECRET'), 'region' => 'us-east-1', ], @@ -32,5 +31,4 @@ return [ 'sparkpost' => [ 'secret' => env('SPARKPOST_SECRET'), ], - ]; diff --git a/config/session.php b/config/session.php index 0c1b3bb8..08d97fd5 100644 --- a/config/session.php +++ b/config/session.php @@ -1,7 +1,6 @@ true, - ]; diff --git a/config/settings.php b/config/settings.php index e5c82eea..c6a75fb0 100644 --- a/config/settings.php +++ b/config/settings.php @@ -1,7 +1,6 @@ [ - 'database' => [ 'driver' => 'database', 'connection' => env('DB_CONNECTION', 'mysql'), 'table' => 'settings', ], - ], /* @@ -112,7 +109,5 @@ return [ */ 'override' => [ - ], - ]; diff --git a/config/themes.php b/config/themes.php index f90a29f6..1a708368 100644 --- a/config/themes.php +++ b/config/themes.php @@ -8,9 +8,9 @@ return [ 'themes' => [ 'pterodactyl' => [ - 'extends' => null, - 'views-path' => 'pterodactyl', - 'asset-path' => 'themes/pterodactyl', + 'extends' => null, + 'views-path' => 'pterodactyl', + 'asset-path' => 'themes/pterodactyl', ], ], ]; diff --git a/config/trustedproxy.php b/config/trustedproxy.php index a0621149..c60aa6a0 100644 --- a/config/trustedproxy.php +++ b/config/trustedproxy.php @@ -1,7 +1,6 @@ [ - \Illuminate\Http\Request::HEADER_CLIENT_IP => 'X_FORWARDED_FOR', - \Illuminate\Http\Request::HEADER_CLIENT_HOST => 'X_FORWARDED_HOST', + \Illuminate\Http\Request::HEADER_CLIENT_IP => 'X_FORWARDED_FOR', + \Illuminate\Http\Request::HEADER_CLIENT_HOST => 'X_FORWARDED_HOST', \Illuminate\Http\Request::HEADER_CLIENT_PROTO => 'X_FORWARDED_PROTO', - \Illuminate\Http\Request::HEADER_CLIENT_PORT => 'X_FORWARDED_PORT', + \Illuminate\Http\Request::HEADER_CLIENT_PORT => 'X_FORWARDED_PORT', ], ]; diff --git a/config/view.php b/config/view.php index 2acfd9cc..8796b0ab 100644 --- a/config/view.php +++ b/config/view.php @@ -1,7 +1,6 @@ realpath(storage_path('framework/views')), - ]; diff --git a/database/migrations/2016_01_23_195641_add_allocations_table.php b/database/migrations/2016_01_23_195641_add_allocations_table.php index 7384b7de..cfff2b35 100644 --- a/database/migrations/2016_01_23_195641_add_allocations_table.php +++ b/database/migrations/2016_01_23_195641_add_allocations_table.php @@ -7,8 +7,6 @@ class AddAllocationsTable extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -24,8 +22,6 @@ class AddAllocationsTable extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_01_23_195851_add_api_keys.php b/database/migrations/2016_01_23_195851_add_api_keys.php index 290c124a..af7deb62 100644 --- a/database/migrations/2016_01_23_195851_add_api_keys.php +++ b/database/migrations/2016_01_23_195851_add_api_keys.php @@ -7,8 +7,6 @@ class AddApiKeys extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -23,8 +21,6 @@ class AddApiKeys extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_01_23_200044_add_api_permissions.php b/database/migrations/2016_01_23_200044_add_api_permissions.php index f1843aad..e6f6bcbf 100644 --- a/database/migrations/2016_01_23_200044_add_api_permissions.php +++ b/database/migrations/2016_01_23_200044_add_api_permissions.php @@ -7,8 +7,6 @@ class AddApiPermissions extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -21,8 +19,6 @@ class AddApiPermissions extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_01_23_200159_add_downloads.php b/database/migrations/2016_01_23_200159_add_downloads.php index f1c19243..b1771c5e 100644 --- a/database/migrations/2016_01_23_200159_add_downloads.php +++ b/database/migrations/2016_01_23_200159_add_downloads.php @@ -7,8 +7,6 @@ class AddDownloads extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -23,8 +21,6 @@ class AddDownloads extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_01_23_200421_create_failed_jobs_table.php b/database/migrations/2016_01_23_200421_create_failed_jobs_table.php index 63eaf53a..83923e7d 100644 --- a/database/migrations/2016_01_23_200421_create_failed_jobs_table.php +++ b/database/migrations/2016_01_23_200421_create_failed_jobs_table.php @@ -7,8 +7,6 @@ class CreateFailedJobsTable extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -23,8 +21,6 @@ class CreateFailedJobsTable extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_01_23_200440_create_jobs_table.php b/database/migrations/2016_01_23_200440_create_jobs_table.php index 01d3a9e6..277acae3 100644 --- a/database/migrations/2016_01_23_200440_create_jobs_table.php +++ b/database/migrations/2016_01_23_200440_create_jobs_table.php @@ -7,8 +7,6 @@ class CreateJobsTable extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -27,8 +25,6 @@ class CreateJobsTable extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_01_23_200528_add_locations.php b/database/migrations/2016_01_23_200528_add_locations.php index 6ad7de75..b34a5fbc 100644 --- a/database/migrations/2016_01_23_200528_add_locations.php +++ b/database/migrations/2016_01_23_200528_add_locations.php @@ -7,8 +7,6 @@ class AddLocations extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -22,8 +20,6 @@ class AddLocations extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_01_23_200648_add_nodes.php b/database/migrations/2016_01_23_200648_add_nodes.php index 57613cab..52c0a29e 100644 --- a/database/migrations/2016_01_23_200648_add_nodes.php +++ b/database/migrations/2016_01_23_200648_add_nodes.php @@ -7,8 +7,6 @@ class AddNodes extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -33,8 +31,6 @@ class AddNodes extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_01_23_201433_add_password_resets.php b/database/migrations/2016_01_23_201433_add_password_resets.php index 45454b80..0584e361 100644 --- a/database/migrations/2016_01_23_201433_add_password_resets.php +++ b/database/migrations/2016_01_23_201433_add_password_resets.php @@ -7,8 +7,6 @@ class AddPasswordResets extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -21,8 +19,6 @@ class AddPasswordResets extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_01_23_201531_add_permissions.php b/database/migrations/2016_01_23_201531_add_permissions.php index bf6327ac..12c9bbe0 100644 --- a/database/migrations/2016_01_23_201531_add_permissions.php +++ b/database/migrations/2016_01_23_201531_add_permissions.php @@ -7,8 +7,6 @@ class AddPermissions extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -23,8 +21,6 @@ class AddPermissions extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_01_23_201649_add_server_variables.php b/database/migrations/2016_01_23_201649_add_server_variables.php index 229b33c6..d9a436e6 100644 --- a/database/migrations/2016_01_23_201649_add_server_variables.php +++ b/database/migrations/2016_01_23_201649_add_server_variables.php @@ -7,8 +7,6 @@ class AddServerVariables extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -23,8 +21,6 @@ class AddServerVariables extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_01_23_201748_add_servers.php b/database/migrations/2016_01_23_201748_add_servers.php index f4ae554c..5e106106 100644 --- a/database/migrations/2016_01_23_201748_add_servers.php +++ b/database/migrations/2016_01_23_201748_add_servers.php @@ -7,8 +7,6 @@ class AddServers extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -40,8 +38,6 @@ class AddServers extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_01_23_202544_add_service_options.php b/database/migrations/2016_01_23_202544_add_service_options.php index 172f1eb7..7b0a3360 100644 --- a/database/migrations/2016_01_23_202544_add_service_options.php +++ b/database/migrations/2016_01_23_202544_add_service_options.php @@ -7,8 +7,6 @@ class AddServiceOptions extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -25,8 +23,6 @@ class AddServiceOptions extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_01_23_202731_add_service_varibles.php b/database/migrations/2016_01_23_202731_add_service_varibles.php index 5ea93898..e79fa1fe 100644 --- a/database/migrations/2016_01_23_202731_add_service_varibles.php +++ b/database/migrations/2016_01_23_202731_add_service_varibles.php @@ -7,8 +7,6 @@ class AddServiceVaribles extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -29,8 +27,6 @@ class AddServiceVaribles extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_01_23_202943_add_services.php b/database/migrations/2016_01_23_202943_add_services.php index c837dc92..31f72344 100644 --- a/database/migrations/2016_01_23_202943_add_services.php +++ b/database/migrations/2016_01_23_202943_add_services.php @@ -7,8 +7,6 @@ class AddServices extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -25,8 +23,6 @@ class AddServices extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_01_23_203119_create_settings_table.php b/database/migrations/2016_01_23_203119_create_settings_table.php index 0e2c0cbf..2cd6922c 100644 --- a/database/migrations/2016_01_23_203119_create_settings_table.php +++ b/database/migrations/2016_01_23_203119_create_settings_table.php @@ -7,8 +7,6 @@ class CreateSettingsTable extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -20,8 +18,6 @@ class CreateSettingsTable extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_01_23_203150_add_subusers.php b/database/migrations/2016_01_23_203150_add_subusers.php index 1cc8e334..2f0e4631 100644 --- a/database/migrations/2016_01_23_203150_add_subusers.php +++ b/database/migrations/2016_01_23_203150_add_subusers.php @@ -7,8 +7,6 @@ class AddSubusers extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -23,8 +21,6 @@ class AddSubusers extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_01_23_203159_add_users.php b/database/migrations/2016_01_23_203159_add_users.php index 14e28eeb..05ace7e2 100644 --- a/database/migrations/2016_01_23_203159_add_users.php +++ b/database/migrations/2016_01_23_203159_add_users.php @@ -7,8 +7,6 @@ class AddUsers extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -28,8 +26,6 @@ class AddUsers extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_01_23_203947_create_sessions_table.php b/database/migrations/2016_01_23_203947_create_sessions_table.php index f1c84aa4..533fa8aa 100644 --- a/database/migrations/2016_01_23_203947_create_sessions_table.php +++ b/database/migrations/2016_01_23_203947_create_sessions_table.php @@ -7,8 +7,6 @@ class CreateSessionsTable extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -24,8 +22,6 @@ class CreateSessionsTable extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_01_25_234418_rename_permissions_column.php b/database/migrations/2016_01_25_234418_rename_permissions_column.php index c0632b1d..ae46dceb 100644 --- a/database/migrations/2016_01_25_234418_rename_permissions_column.php +++ b/database/migrations/2016_01_25_234418_rename_permissions_column.php @@ -7,8 +7,6 @@ class RenamePermissionsColumn extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -19,13 +17,10 @@ class RenamePermissionsColumn extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { Schema::table('permissions', function (Blueprint $table) { - // }); } } diff --git a/database/migrations/2016_02_07_172148_add_databases_tables.php b/database/migrations/2016_02_07_172148_add_databases_tables.php index 9a36a690..7b1048b1 100644 --- a/database/migrations/2016_02_07_172148_add_databases_tables.php +++ b/database/migrations/2016_02_07_172148_add_databases_tables.php @@ -7,8 +7,6 @@ class AddDatabasesTables extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -26,8 +24,6 @@ class AddDatabasesTables extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_02_07_181319_add_database_servers_table.php b/database/migrations/2016_02_07_181319_add_database_servers_table.php index 9d6da726..5a6740ae 100644 --- a/database/migrations/2016_02_07_181319_add_database_servers_table.php +++ b/database/migrations/2016_02_07_181319_add_database_servers_table.php @@ -7,8 +7,6 @@ class AddDatabaseServersTable extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -27,8 +25,6 @@ class AddDatabaseServersTable extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_02_13_154306_add_service_option_default_startup.php b/database/migrations/2016_02_13_154306_add_service_option_default_startup.php index 1f8ad36f..c8255ff4 100644 --- a/database/migrations/2016_02_13_154306_add_service_option_default_startup.php +++ b/database/migrations/2016_02_13_154306_add_service_option_default_startup.php @@ -7,8 +7,6 @@ class AddServiceOptionDefaultStartup extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -20,8 +18,6 @@ class AddServiceOptionDefaultStartup extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_02_20_155318_add_unique_service_field.php b/database/migrations/2016_02_20_155318_add_unique_service_field.php index 798ac4ba..01ff9135 100644 --- a/database/migrations/2016_02_20_155318_add_unique_service_field.php +++ b/database/migrations/2016_02_20_155318_add_unique_service_field.php @@ -7,8 +7,6 @@ class AddUniqueServiceField extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -19,8 +17,6 @@ class AddUniqueServiceField extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_02_27_163411_add_tasks_table.php b/database/migrations/2016_02_27_163411_add_tasks_table.php index 2cdaa30b..aee1e7ca 100644 --- a/database/migrations/2016_02_27_163411_add_tasks_table.php +++ b/database/migrations/2016_02_27_163411_add_tasks_table.php @@ -7,8 +7,6 @@ class AddTasksTable extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -33,8 +31,6 @@ class AddTasksTable extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_02_27_163447_add_tasks_log_table.php b/database/migrations/2016_02_27_163447_add_tasks_log_table.php index 6d9352df..265e7fd9 100644 --- a/database/migrations/2016_02_27_163447_add_tasks_log_table.php +++ b/database/migrations/2016_02_27_163447_add_tasks_log_table.php @@ -7,8 +7,6 @@ class AddTasksLogTable extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -24,8 +22,6 @@ class AddTasksLogTable extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_03_18_155649_add_nullable_field_lastrun.php b/database/migrations/2016_03_18_155649_add_nullable_field_lastrun.php index 9065947d..9d4752eb 100644 --- a/database/migrations/2016_03_18_155649_add_nullable_field_lastrun.php +++ b/database/migrations/2016_03_18_155649_add_nullable_field_lastrun.php @@ -6,8 +6,6 @@ class AddNullableFieldLastrun extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -17,8 +15,6 @@ class AddNullableFieldLastrun extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_08_30_212718_add_ip_alias.php b/database/migrations/2016_08_30_212718_add_ip_alias.php index e75930ed..26aa5eaa 100644 --- a/database/migrations/2016_08_30_212718_add_ip_alias.php +++ b/database/migrations/2016_08_30_212718_add_ip_alias.php @@ -7,8 +7,6 @@ class AddIpAlias extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -30,8 +28,6 @@ class AddIpAlias extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_08_30_213301_modify_ip_storage_method.php b/database/migrations/2016_08_30_213301_modify_ip_storage_method.php index b77ccbea..ee7e704f 100644 --- a/database/migrations/2016_08_30_213301_modify_ip_storage_method.php +++ b/database/migrations/2016_08_30_213301_modify_ip_storage_method.php @@ -7,8 +7,6 @@ class ModifyIpStorageMethod extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -48,8 +46,6 @@ class ModifyIpStorageMethod extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_09_01_193520_add_suspension_for_servers.php b/database/migrations/2016_09_01_193520_add_suspension_for_servers.php index 39717253..7bfb75b2 100644 --- a/database/migrations/2016_09_01_193520_add_suspension_for_servers.php +++ b/database/migrations/2016_09_01_193520_add_suspension_for_servers.php @@ -7,8 +7,6 @@ class AddSuspensionForServers extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -19,8 +17,6 @@ class AddSuspensionForServers extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_09_01_211924_remove_active_column.php b/database/migrations/2016_09_01_211924_remove_active_column.php index 746559a8..22a2bde1 100644 --- a/database/migrations/2016_09_01_211924_remove_active_column.php +++ b/database/migrations/2016_09_01_211924_remove_active_column.php @@ -7,8 +7,6 @@ class RemoveActiveColumn extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -19,8 +17,6 @@ class RemoveActiveColumn extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_09_02_190647_add_sftp_password_storage.php b/database/migrations/2016_09_02_190647_add_sftp_password_storage.php index b950c631..565957d5 100644 --- a/database/migrations/2016_09_02_190647_add_sftp_password_storage.php +++ b/database/migrations/2016_09_02_190647_add_sftp_password_storage.php @@ -7,8 +7,6 @@ class AddSftpPasswordStorage extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -19,8 +17,6 @@ class AddSftpPasswordStorage extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_09_04_171338_update_jobs_tables.php b/database/migrations/2016_09_04_171338_update_jobs_tables.php index 482bb08c..840ecacb 100644 --- a/database/migrations/2016_09_04_171338_update_jobs_tables.php +++ b/database/migrations/2016_09_04_171338_update_jobs_tables.php @@ -8,8 +8,6 @@ class UpdateJobsTables extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -22,8 +20,6 @@ class UpdateJobsTables extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_09_04_172028_update_failed_jobs_table.php b/database/migrations/2016_09_04_172028_update_failed_jobs_table.php index 38f2ef3a..a00f5f18 100644 --- a/database/migrations/2016_09_04_172028_update_failed_jobs_table.php +++ b/database/migrations/2016_09_04_172028_update_failed_jobs_table.php @@ -8,8 +8,6 @@ class UpdateFailedJobsTable extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -20,8 +18,6 @@ class UpdateFailedJobsTable extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_09_04_182835_create_notifications_table.php b/database/migrations/2016_09_04_182835_create_notifications_table.php index 160a1c42..30fc23a5 100644 --- a/database/migrations/2016_09_04_182835_create_notifications_table.php +++ b/database/migrations/2016_09_04_182835_create_notifications_table.php @@ -7,8 +7,6 @@ class CreateNotificationsTable extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -24,8 +22,6 @@ class CreateNotificationsTable extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_09_07_163017_add_unique_identifier.php b/database/migrations/2016_09_07_163017_add_unique_identifier.php index d26e5995..e1bab9cc 100644 --- a/database/migrations/2016_09_07_163017_add_unique_identifier.php +++ b/database/migrations/2016_09_07_163017_add_unique_identifier.php @@ -8,8 +8,6 @@ class AddUniqueIdentifier extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -20,8 +18,6 @@ class AddUniqueIdentifier extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_09_14_145945_allow_longer_regex_field.php b/database/migrations/2016_09_14_145945_allow_longer_regex_field.php index d8a4e8c6..a7df1ca1 100644 --- a/database/migrations/2016_09_14_145945_allow_longer_regex_field.php +++ b/database/migrations/2016_09_14_145945_allow_longer_regex_field.php @@ -8,8 +8,6 @@ class AllowLongerRegexField extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -20,8 +18,6 @@ class AllowLongerRegexField extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_09_17_194246_add_docker_image_column.php b/database/migrations/2016_09_17_194246_add_docker_image_column.php index 58e4b87a..05d26112 100644 --- a/database/migrations/2016_09_17_194246_add_docker_image_column.php +++ b/database/migrations/2016_09_17_194246_add_docker_image_column.php @@ -8,8 +8,6 @@ class AddDockerImageColumn extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -33,8 +31,6 @@ class AddDockerImageColumn extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_09_21_165554_update_servers_column_name.php b/database/migrations/2016_09_21_165554_update_servers_column_name.php index 064c57c3..14ae07c4 100644 --- a/database/migrations/2016_09_21_165554_update_servers_column_name.php +++ b/database/migrations/2016_09_21_165554_update_servers_column_name.php @@ -8,8 +8,6 @@ class UpdateServersColumnName extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -20,8 +18,6 @@ class UpdateServersColumnName extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_09_29_213518_rename_double_insurgency.php b/database/migrations/2016_09_29_213518_rename_double_insurgency.php index 4fecb8bd..adb57775 100644 --- a/database/migrations/2016_09_29_213518_rename_double_insurgency.php +++ b/database/migrations/2016_09_29_213518_rename_double_insurgency.php @@ -6,8 +6,6 @@ class RenameDoubleInsurgency extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -22,11 +20,8 @@ class RenameDoubleInsurgency extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { - // } } diff --git a/database/migrations/2016_10_07_152117_build_api_log_table.php b/database/migrations/2016_10_07_152117_build_api_log_table.php index 5f1ceb22..08ea312d 100644 --- a/database/migrations/2016_10_07_152117_build_api_log_table.php +++ b/database/migrations/2016_10_07_152117_build_api_log_table.php @@ -8,8 +8,6 @@ class BuildApiLogTable extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -29,8 +27,6 @@ class BuildApiLogTable extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_10_14_164802_update_api_keys.php b/database/migrations/2016_10_14_164802_update_api_keys.php index 80d7f950..56c3e809 100644 --- a/database/migrations/2016_10_14_164802_update_api_keys.php +++ b/database/migrations/2016_10_14_164802_update_api_keys.php @@ -8,8 +8,6 @@ class UpdateApiKeys extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -22,8 +20,6 @@ class UpdateApiKeys extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_10_23_181719_update_misnamed_bungee.php b/database/migrations/2016_10_23_181719_update_misnamed_bungee.php index 0a531675..70ec18b3 100644 --- a/database/migrations/2016_10_23_181719_update_misnamed_bungee.php +++ b/database/migrations/2016_10_23_181719_update_misnamed_bungee.php @@ -6,8 +6,6 @@ class UpdateMisnamedBungee extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -18,8 +16,6 @@ class UpdateMisnamedBungee extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_10_23_193810_add_foreign_keys_servers.php b/database/migrations/2016_10_23_193810_add_foreign_keys_servers.php index 59bc0ac3..3a266352 100644 --- a/database/migrations/2016_10_23_193810_add_foreign_keys_servers.php +++ b/database/migrations/2016_10_23_193810_add_foreign_keys_servers.php @@ -8,8 +8,6 @@ class AddForeignKeysServers extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -33,8 +31,6 @@ class AddForeignKeysServers extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_10_23_201624_add_foreign_allocations.php b/database/migrations/2016_10_23_201624_add_foreign_allocations.php index d2d869cd..0660081c 100644 --- a/database/migrations/2016_10_23_201624_add_foreign_allocations.php +++ b/database/migrations/2016_10_23_201624_add_foreign_allocations.php @@ -8,8 +8,6 @@ class AddForeignAllocations extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -26,8 +24,6 @@ class AddForeignAllocations extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_10_23_202222_add_foreign_api_keys.php b/database/migrations/2016_10_23_202222_add_foreign_api_keys.php index 67dcd3ce..700342d7 100644 --- a/database/migrations/2016_10_23_202222_add_foreign_api_keys.php +++ b/database/migrations/2016_10_23_202222_add_foreign_api_keys.php @@ -8,8 +8,6 @@ class AddForeignApiKeys extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -20,8 +18,6 @@ class AddForeignApiKeys extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_10_23_202703_add_foreign_api_permissions.php b/database/migrations/2016_10_23_202703_add_foreign_api_permissions.php index 16e1eebd..d8eb3504 100644 --- a/database/migrations/2016_10_23_202703_add_foreign_api_permissions.php +++ b/database/migrations/2016_10_23_202703_add_foreign_api_permissions.php @@ -8,8 +8,6 @@ class AddForeignApiPermissions extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -22,8 +20,6 @@ class AddForeignApiPermissions extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_10_23_202953_add_foreign_database_servers.php b/database/migrations/2016_10_23_202953_add_foreign_database_servers.php index b6f012df..769b7daa 100644 --- a/database/migrations/2016_10_23_202953_add_foreign_database_servers.php +++ b/database/migrations/2016_10_23_202953_add_foreign_database_servers.php @@ -8,8 +8,6 @@ class AddForeignDatabaseServers extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -20,8 +18,6 @@ class AddForeignDatabaseServers extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_10_23_203105_add_foreign_databases.php b/database/migrations/2016_10_23_203105_add_foreign_databases.php index 2d6b6e9a..be26e3cb 100644 --- a/database/migrations/2016_10_23_203105_add_foreign_databases.php +++ b/database/migrations/2016_10_23_203105_add_foreign_databases.php @@ -8,8 +8,6 @@ class AddForeignDatabases extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -21,8 +19,6 @@ class AddForeignDatabases extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_10_23_203335_add_foreign_nodes.php b/database/migrations/2016_10_23_203335_add_foreign_nodes.php index 8fa516c0..f861e0a7 100644 --- a/database/migrations/2016_10_23_203335_add_foreign_nodes.php +++ b/database/migrations/2016_10_23_203335_add_foreign_nodes.php @@ -8,8 +8,6 @@ class AddForeignNodes extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -22,8 +20,6 @@ class AddForeignNodes extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_10_23_203522_add_foreign_permissions.php b/database/migrations/2016_10_23_203522_add_foreign_permissions.php index 2f872de6..a43f0eac 100644 --- a/database/migrations/2016_10_23_203522_add_foreign_permissions.php +++ b/database/migrations/2016_10_23_203522_add_foreign_permissions.php @@ -8,8 +8,6 @@ class AddForeignPermissions extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -21,8 +19,6 @@ class AddForeignPermissions extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_10_23_203857_add_foreign_server_variables.php b/database/migrations/2016_10_23_203857_add_foreign_server_variables.php index 0a975dc8..b4720495 100644 --- a/database/migrations/2016_10_23_203857_add_foreign_server_variables.php +++ b/database/migrations/2016_10_23_203857_add_foreign_server_variables.php @@ -8,8 +8,6 @@ class AddForeignServerVariables extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -26,8 +24,6 @@ class AddForeignServerVariables extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_10_23_204157_add_foreign_service_options.php b/database/migrations/2016_10_23_204157_add_foreign_service_options.php index ff6e3b35..cb8c0e2e 100644 --- a/database/migrations/2016_10_23_204157_add_foreign_service_options.php +++ b/database/migrations/2016_10_23_204157_add_foreign_service_options.php @@ -8,8 +8,6 @@ class AddForeignServiceOptions extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -22,8 +20,6 @@ class AddForeignServiceOptions extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_10_23_204321_add_foreign_service_variables.php b/database/migrations/2016_10_23_204321_add_foreign_service_variables.php index 5a543898..02bbc46f 100644 --- a/database/migrations/2016_10_23_204321_add_foreign_service_variables.php +++ b/database/migrations/2016_10_23_204321_add_foreign_service_variables.php @@ -8,8 +8,6 @@ class AddForeignServiceVariables extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -22,8 +20,6 @@ class AddForeignServiceVariables extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_10_23_204454_add_foreign_subusers.php b/database/migrations/2016_10_23_204454_add_foreign_subusers.php index 2f404566..b637c80a 100644 --- a/database/migrations/2016_10_23_204454_add_foreign_subusers.php +++ b/database/migrations/2016_10_23_204454_add_foreign_subusers.php @@ -8,8 +8,6 @@ class AddForeignSubusers extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -21,8 +19,6 @@ class AddForeignSubusers extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_10_23_204610_add_foreign_tasks.php b/database/migrations/2016_10_23_204610_add_foreign_tasks.php index 3821caff..18ea297e 100644 --- a/database/migrations/2016_10_23_204610_add_foreign_tasks.php +++ b/database/migrations/2016_10_23_204610_add_foreign_tasks.php @@ -8,8 +8,6 @@ class AddForeignTasks extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -20,8 +18,6 @@ class AddForeignTasks extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_11_04_000949_add_ark_service_option_fixed.php b/database/migrations/2016_11_04_000949_add_ark_service_option_fixed.php index 5a2dd6da..4383c11c 100644 --- a/database/migrations/2016_11_04_000949_add_ark_service_option_fixed.php +++ b/database/migrations/2016_11_04_000949_add_ark_service_option_fixed.php @@ -6,8 +6,6 @@ class AddArkServiceOptionFixed extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -74,8 +72,6 @@ class AddArkServiceOptionFixed extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_11_11_220649_add_pack_support.php b/database/migrations/2016_11_11_220649_add_pack_support.php index 7cb3eb10..b6fa0972 100644 --- a/database/migrations/2016_11_11_220649_add_pack_support.php +++ b/database/migrations/2016_11_11_220649_add_pack_support.php @@ -8,8 +8,6 @@ class AddPackSupport extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -30,8 +28,6 @@ class AddPackSupport extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_11_11_231731_set_service_name_unique.php b/database/migrations/2016_11_11_231731_set_service_name_unique.php index 4db76f8e..42b0f695 100644 --- a/database/migrations/2016_11_11_231731_set_service_name_unique.php +++ b/database/migrations/2016_11_11_231731_set_service_name_unique.php @@ -8,8 +8,6 @@ class SetServiceNameUnique extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -20,8 +18,6 @@ class SetServiceNameUnique extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_11_27_142519_add_pack_column.php b/database/migrations/2016_11_27_142519_add_pack_column.php index 4e3507c3..d520466a 100644 --- a/database/migrations/2016_11_27_142519_add_pack_column.php +++ b/database/migrations/2016_11_27_142519_add_pack_column.php @@ -8,8 +8,6 @@ class AddPackColumn extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -22,8 +20,6 @@ class AddPackColumn extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_12_01_173018_add_configurable_upload_limit.php b/database/migrations/2016_12_01_173018_add_configurable_upload_limit.php index 91ef1fbb..d2d14f4d 100644 --- a/database/migrations/2016_12_01_173018_add_configurable_upload_limit.php +++ b/database/migrations/2016_12_01_173018_add_configurable_upload_limit.php @@ -8,8 +8,6 @@ class AddConfigurableUploadLimit extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -20,8 +18,6 @@ class AddConfigurableUploadLimit extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2016_12_02_185206_correct_service_variables.php b/database/migrations/2016_12_02_185206_correct_service_variables.php index dd99c122..e9c87989 100644 --- a/database/migrations/2016_12_02_185206_correct_service_variables.php +++ b/database/migrations/2016_12_02_185206_correct_service_variables.php @@ -6,8 +6,6 @@ class CorrectServiceVariables extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -67,8 +65,6 @@ class CorrectServiceVariables extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_01_03_150436_fix_misnamed_option_tag.php b/database/migrations/2017_01_03_150436_fix_misnamed_option_tag.php index a03584ca..7cdf9680 100644 --- a/database/migrations/2017_01_03_150436_fix_misnamed_option_tag.php +++ b/database/migrations/2017_01_03_150436_fix_misnamed_option_tag.php @@ -6,8 +6,6 @@ class FixMisnamedOptionTag extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -24,8 +22,6 @@ class FixMisnamedOptionTag extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_01_07_154228_create_node_configuration_tokens_table.php b/database/migrations/2017_01_07_154228_create_node_configuration_tokens_table.php index 905d28a4..77693c26 100644 --- a/database/migrations/2017_01_07_154228_create_node_configuration_tokens_table.php +++ b/database/migrations/2017_01_07_154228_create_node_configuration_tokens_table.php @@ -8,8 +8,6 @@ class CreateNodeConfigurationTokensTable extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -25,8 +23,6 @@ class CreateNodeConfigurationTokensTable extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_01_12_135449_add_more_user_data.php b/database/migrations/2017_01_12_135449_add_more_user_data.php index 67bc3f59..0206040b 100644 --- a/database/migrations/2017_01_12_135449_add_more_user_data.php +++ b/database/migrations/2017_01_12_135449_add_more_user_data.php @@ -9,8 +9,6 @@ class AddMoreUserData extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -35,8 +33,6 @@ class AddMoreUserData extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_02_02_175548_UpdateColumnNames.php b/database/migrations/2017_02_02_175548_UpdateColumnNames.php index 233780bf..c88aa8de 100644 --- a/database/migrations/2017_02_02_175548_UpdateColumnNames.php +++ b/database/migrations/2017_02_02_175548_UpdateColumnNames.php @@ -8,8 +8,6 @@ class UpdateColumnNames extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -48,8 +46,6 @@ class UpdateColumnNames extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_02_03_140948_UpdateNodesTable.php b/database/migrations/2017_02_03_140948_UpdateNodesTable.php index 4408e612..58ec63ef 100644 --- a/database/migrations/2017_02_03_140948_UpdateNodesTable.php +++ b/database/migrations/2017_02_03_140948_UpdateNodesTable.php @@ -8,8 +8,6 @@ class UpdateNodesTable extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -24,8 +22,6 @@ class UpdateNodesTable extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_02_03_155554_RenameColumns.php b/database/migrations/2017_02_03_155554_RenameColumns.php index 519ccc82..5f617abe 100644 --- a/database/migrations/2017_02_03_155554_RenameColumns.php +++ b/database/migrations/2017_02_03_155554_RenameColumns.php @@ -8,8 +8,6 @@ class RenameColumns extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -28,8 +26,6 @@ class RenameColumns extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_02_05_164123_AdjustColumnNames.php b/database/migrations/2017_02_05_164123_AdjustColumnNames.php index ddb37b89..c7688f05 100644 --- a/database/migrations/2017_02_05_164123_AdjustColumnNames.php +++ b/database/migrations/2017_02_05_164123_AdjustColumnNames.php @@ -8,8 +8,6 @@ class AdjustColumnNames extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -24,8 +22,6 @@ class AdjustColumnNames extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_02_05_164516_AdjustColumnNamesForServicePacks.php b/database/migrations/2017_02_05_164516_AdjustColumnNamesForServicePacks.php index 5e57ffef..6f86b3b6 100644 --- a/database/migrations/2017_02_05_164516_AdjustColumnNamesForServicePacks.php +++ b/database/migrations/2017_02_05_164516_AdjustColumnNamesForServicePacks.php @@ -8,8 +8,6 @@ class AdjustColumnNamesForServicePacks extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -24,8 +22,6 @@ class AdjustColumnNamesForServicePacks extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_02_09_174834_SetupPermissionsPivotTable.php b/database/migrations/2017_02_09_174834_SetupPermissionsPivotTable.php index 7194bf07..45efce83 100644 --- a/database/migrations/2017_02_09_174834_SetupPermissionsPivotTable.php +++ b/database/migrations/2017_02_09_174834_SetupPermissionsPivotTable.php @@ -10,8 +10,6 @@ class SetupPermissionsPivotTable extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -43,8 +41,6 @@ class SetupPermissionsPivotTable extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_02_10_171858_UpdateAPIKeyColumnNames.php b/database/migrations/2017_02_10_171858_UpdateAPIKeyColumnNames.php index 358f9938..8b541d94 100644 --- a/database/migrations/2017_02_10_171858_UpdateAPIKeyColumnNames.php +++ b/database/migrations/2017_02_10_171858_UpdateAPIKeyColumnNames.php @@ -8,8 +8,6 @@ class UpdateAPIKeyColumnNames extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -23,8 +21,6 @@ class UpdateAPIKeyColumnNames extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_03_03_224254_UpdateNodeConfigTokensColumns.php b/database/migrations/2017_03_03_224254_UpdateNodeConfigTokensColumns.php index 5931518d..4f27346f 100644 --- a/database/migrations/2017_03_03_224254_UpdateNodeConfigTokensColumns.php +++ b/database/migrations/2017_03_03_224254_UpdateNodeConfigTokensColumns.php @@ -8,8 +8,6 @@ class UpdateNodeConfigTokensColumns extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -24,8 +22,6 @@ class UpdateNodeConfigTokensColumns extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_03_05_212803_DeleteServiceExecutableOption.php b/database/migrations/2017_03_05_212803_DeleteServiceExecutableOption.php index e4af2511..2d6413ed 100644 --- a/database/migrations/2017_03_05_212803_DeleteServiceExecutableOption.php +++ b/database/migrations/2017_03_05_212803_DeleteServiceExecutableOption.php @@ -9,8 +9,6 @@ class DeleteServiceExecutableOption extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -51,8 +49,6 @@ class DeleteServiceExecutableOption extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_03_10_162934_AddNewServiceOptionsColumns.php b/database/migrations/2017_03_10_162934_AddNewServiceOptionsColumns.php index 351327d3..385004fa 100644 --- a/database/migrations/2017_03_10_162934_AddNewServiceOptionsColumns.php +++ b/database/migrations/2017_03_10_162934_AddNewServiceOptionsColumns.php @@ -8,8 +8,6 @@ class AddNewServiceOptionsColumns extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -28,8 +26,6 @@ class AddNewServiceOptionsColumns extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_03_10_173607_MigrateToNewServiceSystem.php b/database/migrations/2017_03_10_173607_MigrateToNewServiceSystem.php index ee247ee9..17297930 100644 --- a/database/migrations/2017_03_10_173607_MigrateToNewServiceSystem.php +++ b/database/migrations/2017_03_10_173607_MigrateToNewServiceSystem.php @@ -29,8 +29,6 @@ class MigrateToNewServiceSystem extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -56,8 +54,6 @@ class MigrateToNewServiceSystem extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_03_11_215455_ChangeServiceVariablesValidationRules.php b/database/migrations/2017_03_11_215455_ChangeServiceVariablesValidationRules.php index 617c349f..7784083a 100644 --- a/database/migrations/2017_03_11_215455_ChangeServiceVariablesValidationRules.php +++ b/database/migrations/2017_03_11_215455_ChangeServiceVariablesValidationRules.php @@ -9,8 +9,6 @@ class ChangeServiceVariablesValidationRules extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -32,8 +30,6 @@ class ChangeServiceVariablesValidationRules extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_03_12_150648_MoveFunctionsFromFileToDatabase.php b/database/migrations/2017_03_12_150648_MoveFunctionsFromFileToDatabase.php index bbd5fda4..5d5a3d16 100644 --- a/database/migrations/2017_03_12_150648_MoveFunctionsFromFileToDatabase.php +++ b/database/migrations/2017_03_12_150648_MoveFunctionsFromFileToDatabase.php @@ -85,8 +85,6 @@ EOF; /** * Run the migrations. - * - * @return void */ public function up() { @@ -107,8 +105,6 @@ EOF; /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_03_14_175631_RenameServicePacksToSingluarPacks.php b/database/migrations/2017_03_14_175631_RenameServicePacksToSingluarPacks.php index 910fb79d..d01012e4 100644 --- a/database/migrations/2017_03_14_175631_RenameServicePacksToSingluarPacks.php +++ b/database/migrations/2017_03_14_175631_RenameServicePacksToSingluarPacks.php @@ -8,8 +8,6 @@ class RenameServicePacksToSingluarPacks extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -26,8 +24,6 @@ class RenameServicePacksToSingluarPacks extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_03_14_200326_AddLockedStatusToTable.php b/database/migrations/2017_03_14_200326_AddLockedStatusToTable.php index 4916cd02..b1a8ee3a 100644 --- a/database/migrations/2017_03_14_200326_AddLockedStatusToTable.php +++ b/database/migrations/2017_03_14_200326_AddLockedStatusToTable.php @@ -8,8 +8,6 @@ class AddLockedStatusToTable extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -20,8 +18,6 @@ class AddLockedStatusToTable extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_03_16_181109_ReOrganizeDatabaseServersToDatabaseHost.php b/database/migrations/2017_03_16_181109_ReOrganizeDatabaseServersToDatabaseHost.php index 50699a68..a7166df9 100644 --- a/database/migrations/2017_03_16_181109_ReOrganizeDatabaseServersToDatabaseHost.php +++ b/database/migrations/2017_03_16_181109_ReOrganizeDatabaseServersToDatabaseHost.php @@ -8,8 +8,6 @@ class ReOrganizeDatabaseServersToDatabaseHost extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -28,8 +26,6 @@ class ReOrganizeDatabaseServersToDatabaseHost extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_03_16_181515_CleanupDatabasesDatabase.php b/database/migrations/2017_03_16_181515_CleanupDatabasesDatabase.php index 07192f89..bc6fb45c 100644 --- a/database/migrations/2017_03_16_181515_CleanupDatabasesDatabase.php +++ b/database/migrations/2017_03_16_181515_CleanupDatabasesDatabase.php @@ -8,8 +8,6 @@ class CleanupDatabasesDatabase extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -24,8 +22,6 @@ class CleanupDatabasesDatabase extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_03_18_204953_AddForeignKeyToPacks.php b/database/migrations/2017_03_18_204953_AddForeignKeyToPacks.php index 0271616a..3f26a1e3 100644 --- a/database/migrations/2017_03_18_204953_AddForeignKeyToPacks.php +++ b/database/migrations/2017_03_18_204953_AddForeignKeyToPacks.php @@ -8,8 +8,6 @@ class AddForeignKeyToPacks extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -20,8 +18,6 @@ class AddForeignKeyToPacks extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_03_31_221948_AddServerDescriptionColumn.php b/database/migrations/2017_03_31_221948_AddServerDescriptionColumn.php index 1e5ce027..e8ebcb20 100644 --- a/database/migrations/2017_03_31_221948_AddServerDescriptionColumn.php +++ b/database/migrations/2017_03_31_221948_AddServerDescriptionColumn.php @@ -8,8 +8,6 @@ class AddServerDescriptionColumn extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -20,8 +18,6 @@ class AddServerDescriptionColumn extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_04_02_163232_DropDeletedAtColumnFromServers.php b/database/migrations/2017_04_02_163232_DropDeletedAtColumnFromServers.php index ccd31865..3cd08f1a 100644 --- a/database/migrations/2017_04_02_163232_DropDeletedAtColumnFromServers.php +++ b/database/migrations/2017_04_02_163232_DropDeletedAtColumnFromServers.php @@ -8,8 +8,6 @@ class DropDeletedAtColumnFromServers extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -20,8 +18,6 @@ class DropDeletedAtColumnFromServers extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_04_15_125021_UpgradeTaskSystem.php b/database/migrations/2017_04_15_125021_UpgradeTaskSystem.php index 5264122b..dc58df4d 100644 --- a/database/migrations/2017_04_15_125021_UpgradeTaskSystem.php +++ b/database/migrations/2017_04_15_125021_UpgradeTaskSystem.php @@ -9,8 +9,6 @@ class UpgradeTaskSystem extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -34,8 +32,6 @@ class UpgradeTaskSystem extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_04_20_171943_AddScriptsToServiceOptions.php b/database/migrations/2017_04_20_171943_AddScriptsToServiceOptions.php index 610f18e5..ba2f57c4 100644 --- a/database/migrations/2017_04_20_171943_AddScriptsToServiceOptions.php +++ b/database/migrations/2017_04_20_171943_AddScriptsToServiceOptions.php @@ -8,8 +8,6 @@ class AddScriptsToServiceOptions extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -23,8 +21,6 @@ class AddScriptsToServiceOptions extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_04_21_151432_AddServiceScriptTrackingToServers.php b/database/migrations/2017_04_21_151432_AddServiceScriptTrackingToServers.php index 07afdfee..2bc8f27b 100644 --- a/database/migrations/2017_04_21_151432_AddServiceScriptTrackingToServers.php +++ b/database/migrations/2017_04_21_151432_AddServiceScriptTrackingToServers.php @@ -8,8 +8,6 @@ class AddServiceScriptTrackingToServers extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -20,8 +18,6 @@ class AddServiceScriptTrackingToServers extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_04_27_145300_AddCopyScriptFromColumn.php b/database/migrations/2017_04_27_145300_AddCopyScriptFromColumn.php index 027d1964..514d17e1 100644 --- a/database/migrations/2017_04_27_145300_AddCopyScriptFromColumn.php +++ b/database/migrations/2017_04_27_145300_AddCopyScriptFromColumn.php @@ -8,8 +8,6 @@ class AddCopyScriptFromColumn extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -22,8 +20,6 @@ class AddCopyScriptFromColumn extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_04_27_223629_AddAbilityToDefineConnectionOverSSLWithDaemonBehindProxy.php b/database/migrations/2017_04_27_223629_AddAbilityToDefineConnectionOverSSLWithDaemonBehindProxy.php index f82d3925..aa5e0449 100644 --- a/database/migrations/2017_04_27_223629_AddAbilityToDefineConnectionOverSSLWithDaemonBehindProxy.php +++ b/database/migrations/2017_04_27_223629_AddAbilityToDefineConnectionOverSSLWithDaemonBehindProxy.php @@ -8,8 +8,6 @@ class AddAbilityToDefineConnectionOverSSLWithDaemonBehindProxy extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -20,8 +18,6 @@ class AddAbilityToDefineConnectionOverSSLWithDaemonBehindProxy extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_05_01_141528_DeleteDownloadTable.php b/database/migrations/2017_05_01_141528_DeleteDownloadTable.php index 90a7f7a6..7dcae3c6 100644 --- a/database/migrations/2017_05_01_141528_DeleteDownloadTable.php +++ b/database/migrations/2017_05_01_141528_DeleteDownloadTable.php @@ -8,8 +8,6 @@ class DeleteDownloadTable extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -18,8 +16,6 @@ class DeleteDownloadTable extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_05_01_141559_DeleteNodeConfigurationTable.php b/database/migrations/2017_05_01_141559_DeleteNodeConfigurationTable.php index 369c867b..90c8c4b1 100644 --- a/database/migrations/2017_05_01_141559_DeleteNodeConfigurationTable.php +++ b/database/migrations/2017_05_01_141559_DeleteNodeConfigurationTable.php @@ -8,8 +8,6 @@ class DeleteNodeConfigurationTable extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -18,8 +16,6 @@ class DeleteNodeConfigurationTable extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_06_10_152951_add_external_id_to_users.php b/database/migrations/2017_06_10_152951_add_external_id_to_users.php index 696f10f4..9ce5057e 100644 --- a/database/migrations/2017_06_10_152951_add_external_id_to_users.php +++ b/database/migrations/2017_06_10_152951_add_external_id_to_users.php @@ -8,8 +8,6 @@ class AddExternalIdToUsers extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -20,8 +18,6 @@ class AddExternalIdToUsers extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_06_25_133923_ChangeForeignKeyToBeOnCascadeDelete.php b/database/migrations/2017_06_25_133923_ChangeForeignKeyToBeOnCascadeDelete.php index 17dbe822..a089ab4d 100644 --- a/database/migrations/2017_06_25_133923_ChangeForeignKeyToBeOnCascadeDelete.php +++ b/database/migrations/2017_06_25_133923_ChangeForeignKeyToBeOnCascadeDelete.php @@ -8,8 +8,6 @@ class ChangeForeignKeyToBeOnCascadeDelete extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -22,8 +20,6 @@ class ChangeForeignKeyToBeOnCascadeDelete extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_07_08_152806_ChangeUserPermissionsToDeleteOnUserDeletion.php b/database/migrations/2017_07_08_152806_ChangeUserPermissionsToDeleteOnUserDeletion.php index eaa7a4bf..0bfc7d52 100644 --- a/database/migrations/2017_07_08_152806_ChangeUserPermissionsToDeleteOnUserDeletion.php +++ b/database/migrations/2017_07_08_152806_ChangeUserPermissionsToDeleteOnUserDeletion.php @@ -8,8 +8,6 @@ class ChangeUserPermissionsToDeleteOnUserDeletion extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -30,8 +28,6 @@ class ChangeUserPermissionsToDeleteOnUserDeletion extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_07_08_154416_SetAllocationToReferenceNullOnServerDelete.php b/database/migrations/2017_07_08_154416_SetAllocationToReferenceNullOnServerDelete.php index 76e475b0..fb156ba8 100644 --- a/database/migrations/2017_07_08_154416_SetAllocationToReferenceNullOnServerDelete.php +++ b/database/migrations/2017_07_08_154416_SetAllocationToReferenceNullOnServerDelete.php @@ -8,8 +8,6 @@ class SetAllocationToReferenceNullOnServerDelete extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -22,8 +20,6 @@ class SetAllocationToReferenceNullOnServerDelete extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_07_08_154650_CascadeDeletionWhenAServerOrVariableIsDeleted.php b/database/migrations/2017_07_08_154650_CascadeDeletionWhenAServerOrVariableIsDeleted.php index f599f02c..5ae9a29f 100644 --- a/database/migrations/2017_07_08_154650_CascadeDeletionWhenAServerOrVariableIsDeleted.php +++ b/database/migrations/2017_07_08_154650_CascadeDeletionWhenAServerOrVariableIsDeleted.php @@ -8,8 +8,6 @@ class CascadeDeletionWhenAServerOrVariableIsDeleted extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -24,8 +22,6 @@ class CascadeDeletionWhenAServerOrVariableIsDeleted extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_07_24_194433_DeleteTaskWhenParentServerIsDeleted.php b/database/migrations/2017_07_24_194433_DeleteTaskWhenParentServerIsDeleted.php index 8a3d7842..88e2e013 100644 --- a/database/migrations/2017_07_24_194433_DeleteTaskWhenParentServerIsDeleted.php +++ b/database/migrations/2017_07_24_194433_DeleteTaskWhenParentServerIsDeleted.php @@ -8,8 +8,6 @@ class DeleteTaskWhenParentServerIsDeleted extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -22,8 +20,6 @@ class DeleteTaskWhenParentServerIsDeleted extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_08_05_115800_CascadeNullValuesForDatabaseHostWhenNodeIsDeleted.php b/database/migrations/2017_08_05_115800_CascadeNullValuesForDatabaseHostWhenNodeIsDeleted.php index 137384a8..a33b78af 100644 --- a/database/migrations/2017_08_05_115800_CascadeNullValuesForDatabaseHostWhenNodeIsDeleted.php +++ b/database/migrations/2017_08_05_115800_CascadeNullValuesForDatabaseHostWhenNodeIsDeleted.php @@ -8,8 +8,6 @@ class CascadeNullValuesForDatabaseHostWhenNodeIsDeleted extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -21,8 +19,6 @@ class CascadeNullValuesForDatabaseHostWhenNodeIsDeleted extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_08_05_144104_AllowNegativeValuesForOverallocation.php b/database/migrations/2017_08_05_144104_AllowNegativeValuesForOverallocation.php index 60eadcaf..260af9a4 100644 --- a/database/migrations/2017_08_05_144104_AllowNegativeValuesForOverallocation.php +++ b/database/migrations/2017_08_05_144104_AllowNegativeValuesForOverallocation.php @@ -8,8 +8,6 @@ class AllowNegativeValuesForOverallocation extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -21,8 +19,6 @@ class AllowNegativeValuesForOverallocation extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_08_05_174811_SetAllocationUnqiueUsingMultipleFields.php b/database/migrations/2017_08_05_174811_SetAllocationUnqiueUsingMultipleFields.php index 56e149d4..ea1cb891 100644 --- a/database/migrations/2017_08_05_174811_SetAllocationUnqiueUsingMultipleFields.php +++ b/database/migrations/2017_08_05_174811_SetAllocationUnqiueUsingMultipleFields.php @@ -8,8 +8,6 @@ class SetAllocationUnqiueUsingMultipleFields extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -20,8 +18,6 @@ class SetAllocationUnqiueUsingMultipleFields extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_08_15_214555_CascadeDeletionWhenAParentServiceIsDeleted.php b/database/migrations/2017_08_15_214555_CascadeDeletionWhenAParentServiceIsDeleted.php index aef29902..074f872e 100644 --- a/database/migrations/2017_08_15_214555_CascadeDeletionWhenAParentServiceIsDeleted.php +++ b/database/migrations/2017_08_15_214555_CascadeDeletionWhenAParentServiceIsDeleted.php @@ -8,8 +8,6 @@ class CascadeDeletionWhenAParentServiceIsDeleted extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -22,8 +20,6 @@ class CascadeDeletionWhenAParentServiceIsDeleted extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/migrations/2017_08_18_215428_RemovePackWhenParentServiceOptionIsDeleted.php b/database/migrations/2017_08_18_215428_RemovePackWhenParentServiceOptionIsDeleted.php index 694b3993..1b8f1a56 100644 --- a/database/migrations/2017_08_18_215428_RemovePackWhenParentServiceOptionIsDeleted.php +++ b/database/migrations/2017_08_18_215428_RemovePackWhenParentServiceOptionIsDeleted.php @@ -8,8 +8,6 @@ class RemovePackWhenParentServiceOptionIsDeleted extends Migration { /** * Run the migrations. - * - * @return void */ public function up() { @@ -22,8 +20,6 @@ class RemovePackWhenParentServiceOptionIsDeleted extends Migration /** * Reverse the migrations. - * - * @return void */ public function down() { diff --git a/database/seeds/DatabaseSeeder.php b/database/seeds/DatabaseSeeder.php index ae48c7c8..6afdea04 100644 --- a/database/seeds/DatabaseSeeder.php +++ b/database/seeds/DatabaseSeeder.php @@ -7,8 +7,6 @@ class DatabaseSeeder extends Seeder { /** * Run the database seeds. - * - * @return void */ public function run() { diff --git a/database/seeds/MinecraftServiceTableSeeder.php b/database/seeds/MinecraftServiceTableSeeder.php index 74d777f4..7ff69b07 100644 --- a/database/seeds/MinecraftServiceTableSeeder.php +++ b/database/seeds/MinecraftServiceTableSeeder.php @@ -85,8 +85,6 @@ EOF; /** * Run the database seeds. - * - * @return void */ public function run() { diff --git a/database/seeds/RustServiceTableSeeder.php b/database/seeds/RustServiceTableSeeder.php index e6688ef8..6f47773b 100644 --- a/database/seeds/RustServiceTableSeeder.php +++ b/database/seeds/RustServiceTableSeeder.php @@ -47,8 +47,6 @@ class RustServiceTableSeeder extends Seeder /** * Run the database seeds. - * - * @return void */ public function run() { diff --git a/database/seeds/SourceServiceTableSeeder.php b/database/seeds/SourceServiceTableSeeder.php index a20ce255..fcb2a90e 100644 --- a/database/seeds/SourceServiceTableSeeder.php +++ b/database/seeds/SourceServiceTableSeeder.php @@ -47,8 +47,6 @@ class SourceServiceTableSeeder extends Seeder /** * Run the database seeds. - * - * @return void */ public function run() { diff --git a/database/seeds/TerrariaServiceTableSeeder.php b/database/seeds/TerrariaServiceTableSeeder.php index 72afdd86..6d19b9fa 100644 --- a/database/seeds/TerrariaServiceTableSeeder.php +++ b/database/seeds/TerrariaServiceTableSeeder.php @@ -47,8 +47,6 @@ class TerrariaServiceTableSeeder extends Seeder /** * Run the database seeds. - * - * @return void */ public function run() { diff --git a/database/seeds/VoiceServiceTableSeeder.php b/database/seeds/VoiceServiceTableSeeder.php index cd0ba033..3d273de5 100644 --- a/database/seeds/VoiceServiceTableSeeder.php +++ b/database/seeds/VoiceServiceTableSeeder.php @@ -47,8 +47,6 @@ class VoiceServiceTableSeeder extends Seeder /** * Run the database seeds. - * - * @return void */ public function run() { diff --git a/resources/lang/en/pagination.php b/resources/lang/en/pagination.php index fcab34b2..ecac3aa3 100644 --- a/resources/lang/en/pagination.php +++ b/resources/lang/en/pagination.php @@ -1,7 +1,6 @@ '« Previous', - 'next' => 'Next »', - + 'next' => 'Next »', ]; diff --git a/resources/lang/en/validation.php b/resources/lang/en/validation.php index 9608bc25..d6b78467 100644 --- a/resources/lang/en/validation.php +++ b/resources/lang/en/validation.php @@ -1,7 +1,6 @@ 'The :attribute must be accepted.', - 'active_url' => 'The :attribute is not a valid URL.', - 'after' => 'The :attribute must be a date after :date.', - 'after_or_equal' => 'The :attribute must be a date after or equal to :date.', - 'alpha' => 'The :attribute may only contain letters.', - 'alpha_dash' => 'The :attribute may only contain letters, numbers, and dashes.', - 'alpha_num' => 'The :attribute may only contain letters and numbers.', - 'array' => 'The :attribute must be an array.', - 'before' => 'The :attribute must be a date before :date.', - 'before_or_equal' => 'The :attribute must be a date before or equal to :date.', - 'between' => [ + 'accepted' => 'The :attribute must be accepted.', + 'active_url' => 'The :attribute is not a valid URL.', + 'after' => 'The :attribute must be a date after :date.', + 'after_or_equal' => 'The :attribute must be a date after or equal to :date.', + 'alpha' => 'The :attribute may only contain letters.', + 'alpha_dash' => 'The :attribute may only contain letters, numbers, and dashes.', + 'alpha_num' => 'The :attribute may only contain letters and numbers.', + 'array' => 'The :attribute must be an array.', + 'before' => 'The :attribute must be a date before :date.', + 'before_or_equal' => 'The :attribute must be a date before or equal to :date.', + 'between' => [ 'numeric' => 'The :attribute must be between :min and :max.', - 'file' => 'The :attribute must be between :min and :max kilobytes.', - 'string' => 'The :attribute must be between :min and :max characters.', - 'array' => 'The :attribute must have between :min and :max items.', + 'file' => 'The :attribute must be between :min and :max kilobytes.', + 'string' => 'The :attribute must be between :min and :max characters.', + 'array' => 'The :attribute must have between :min and :max items.', ], - 'boolean' => 'The :attribute field must be true or false.', - 'confirmed' => 'The :attribute confirmation does not match.', - 'date' => 'The :attribute is not a valid date.', - 'date_format' => 'The :attribute does not match the format :format.', - 'different' => 'The :attribute and :other must be different.', - 'digits' => 'The :attribute must be :digits digits.', - 'digits_between' => 'The :attribute must be between :min and :max digits.', - 'dimensions' => 'The :attribute has invalid image dimensions.', - 'distinct' => 'The :attribute field has a duplicate value.', - 'email' => 'The :attribute must be a valid email address.', - 'exists' => 'The selected :attribute is invalid.', - 'file' => 'The :attribute must be a file.', - 'filled' => 'The :attribute field is required.', - 'image' => 'The :attribute must be an image.', - 'in' => 'The selected :attribute is invalid.', - 'in_array' => 'The :attribute field does not exist in :other.', - 'integer' => 'The :attribute must be an integer.', - 'ip' => 'The :attribute must be a valid IP address.', - 'json' => 'The :attribute must be a valid JSON string.', - 'max' => [ + 'boolean' => 'The :attribute field must be true or false.', + 'confirmed' => 'The :attribute confirmation does not match.', + 'date' => 'The :attribute is not a valid date.', + 'date_format' => 'The :attribute does not match the format :format.', + 'different' => 'The :attribute and :other must be different.', + 'digits' => 'The :attribute must be :digits digits.', + 'digits_between' => 'The :attribute must be between :min and :max digits.', + 'dimensions' => 'The :attribute has invalid image dimensions.', + 'distinct' => 'The :attribute field has a duplicate value.', + 'email' => 'The :attribute must be a valid email address.', + 'exists' => 'The selected :attribute is invalid.', + 'file' => 'The :attribute must be a file.', + 'filled' => 'The :attribute field is required.', + 'image' => 'The :attribute must be an image.', + 'in' => 'The selected :attribute is invalid.', + 'in_array' => 'The :attribute field does not exist in :other.', + 'integer' => 'The :attribute must be an integer.', + 'ip' => 'The :attribute must be a valid IP address.', + 'json' => 'The :attribute must be a valid JSON string.', + 'max' => [ 'numeric' => 'The :attribute may not be greater than :max.', - 'file' => 'The :attribute may not be greater than :max kilobytes.', - 'string' => 'The :attribute may not be greater than :max characters.', - 'array' => 'The :attribute may not have more than :max items.', + 'file' => 'The :attribute may not be greater than :max kilobytes.', + 'string' => 'The :attribute may not be greater than :max characters.', + 'array' => 'The :attribute may not have more than :max items.', ], - 'mimes' => 'The :attribute must be a file of type: :values.', - 'mimetypes' => 'The :attribute must be a file of type: :values.', - 'min' => [ + 'mimes' => 'The :attribute must be a file of type: :values.', + 'mimetypes' => 'The :attribute must be a file of type: :values.', + 'min' => [ 'numeric' => 'The :attribute must be at least :min.', - 'file' => 'The :attribute must be at least :min kilobytes.', - 'string' => 'The :attribute must be at least :min characters.', - 'array' => 'The :attribute must have at least :min items.', + 'file' => 'The :attribute must be at least :min kilobytes.', + 'string' => 'The :attribute must be at least :min characters.', + 'array' => 'The :attribute must have at least :min items.', ], - 'not_in' => 'The selected :attribute is invalid.', - 'numeric' => 'The :attribute must be a number.', - 'present' => 'The :attribute field must be present.', - 'regex' => 'The :attribute format is invalid.', - 'required' => 'The :attribute field is required.', - 'required_if' => 'The :attribute field is required when :other is :value.', - 'required_unless' => 'The :attribute field is required unless :other is in :values.', - 'required_with' => 'The :attribute field is required when :values is present.', - 'required_with_all' => 'The :attribute field is required when :values is present.', - 'required_without' => 'The :attribute field is required when :values is not present.', + 'not_in' => 'The selected :attribute is invalid.', + 'numeric' => 'The :attribute must be a number.', + 'present' => 'The :attribute field must be present.', + 'regex' => 'The :attribute format is invalid.', + 'required' => 'The :attribute field is required.', + 'required_if' => 'The :attribute field is required when :other is :value.', + 'required_unless' => 'The :attribute field is required unless :other is in :values.', + 'required_with' => 'The :attribute field is required when :values is present.', + 'required_with_all' => 'The :attribute field is required when :values is present.', + 'required_without' => 'The :attribute field is required when :values is not present.', 'required_without_all' => 'The :attribute field is required when none of :values are present.', - 'same' => 'The :attribute and :other must match.', - 'size' => [ + 'same' => 'The :attribute and :other must match.', + 'size' => [ 'numeric' => 'The :attribute must be :size.', - 'file' => 'The :attribute must be :size kilobytes.', - 'string' => 'The :attribute must be :size characters.', - 'array' => 'The :attribute must contain :size items.', + 'file' => 'The :attribute must be :size kilobytes.', + 'string' => 'The :attribute must be :size characters.', + 'array' => 'The :attribute must contain :size items.', ], - 'string' => 'The :attribute must be a string.', - 'timezone' => 'The :attribute must be a valid zone.', - 'unique' => 'The :attribute has already been taken.', - 'uploaded' => 'The :attribute failed to upload.', - 'url' => 'The :attribute format is invalid.', + 'string' => 'The :attribute must be a string.', + 'timezone' => 'The :attribute must be a valid zone.', + 'unique' => 'The :attribute has already been taken.', + 'uploaded' => 'The :attribute failed to upload.', + 'url' => 'The :attribute format is invalid.', /* |-------------------------------------------------------------------------- @@ -115,5 +114,4 @@ return [ */ 'attributes' => [], - ]; diff --git a/tests/Unit/Services/Api/KeyServiceTest.php b/tests/Unit/Services/Api/KeyServiceTest.php index 33d01cb0..b4912fca 100644 --- a/tests/Unit/Services/Api/KeyServiceTest.php +++ b/tests/Unit/Services/Api/KeyServiceTest.php @@ -72,7 +72,10 @@ class KeyServiceTest extends TestCase $this->repository = m::mock(ApiKeyRepositoryInterface::class); $this->service = new KeyService( - $this->repository, $this->database, $this->encrypter, $this->permissions + $this->repository, + $this->database, + $this->encrypter, + $this->permissions ); } @@ -108,7 +111,9 @@ class KeyServiceTest extends TestCase $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); $response = $this->service->create( - ['test-data' => 'test'], ['invalid-node', 'server-list'], ['invalid-node', 'server-create'] + ['test-data' => 'test'], + ['invalid-node', 'server-list'], + ['invalid-node', 'server-create'] ); $this->assertNotEmpty($response); diff --git a/tests/Unit/Services/Database/DatabaseManagementServiceTest.php b/tests/Unit/Services/Database/DatabaseManagementServiceTest.php index ca1e29ce..280fbce8 100644 --- a/tests/Unit/Services/Database/DatabaseManagementServiceTest.php +++ b/tests/Unit/Services/Database/DatabaseManagementServiceTest.php @@ -112,16 +112,23 @@ class DatabaseManagementServiceTest extends TestCase ->once() ->andReturnNull(); $this->repository->shouldReceive('createDatabase')->with( - self::TEST_DATA['database'], 'dynamic' + self::TEST_DATA['database'], + 'dynamic' )->once()->andReturnNull(); $this->encrypter->shouldReceive('decrypt')->with('enc_password')->once()->andReturn('str_random'); $this->repository->shouldReceive('createUser')->with( - self::TEST_DATA['username'], self::TEST_DATA['remote'], 'str_random', 'dynamic' + self::TEST_DATA['username'], + self::TEST_DATA['remote'], + 'str_random', + 'dynamic' )->once()->andReturnNull(); $this->repository->shouldReceive('assignUserToDatabase')->with( - self::TEST_DATA['database'], self::TEST_DATA['username'], self::TEST_DATA['remote'], 'dynamic' + self::TEST_DATA['database'], + self::TEST_DATA['username'], + self::TEST_DATA['remote'], + 'dynamic' )->once()->andReturnNull(); $this->repository->shouldReceive('flush')->with('dynamic')->once()->andReturnNull(); @@ -184,7 +191,8 @@ class DatabaseManagementServiceTest extends TestCase ->once() ->andReturnNull(); $this->repository->shouldReceive('createDatabase')->with( - self::TEST_DATA['database'], 'dynamic' + self::TEST_DATA['database'], + 'dynamic' )->once()->andThrow(new Exception('Test Message')); $this->repository->shouldReceive('dropDatabase') @@ -192,7 +200,9 @@ class DatabaseManagementServiceTest extends TestCase ->once() ->andReturnNull(); $this->repository->shouldReceive('dropUser')->with( - self::TEST_DATA['username'], self::TEST_DATA['remote'], 'dynamic' + self::TEST_DATA['username'], + self::TEST_DATA['remote'], + 'dynamic' )->once()->andReturnNull(); $this->repository->shouldReceive('flush')->with('dynamic')->once()->andReturnNull(); @@ -221,7 +231,8 @@ class DatabaseManagementServiceTest extends TestCase ->once() ->andReturnNull(); $this->repository->shouldReceive('createDatabase')->with( - self::TEST_DATA['database'], 'dynamic' + self::TEST_DATA['database'], + 'dynamic' )->once()->andThrow(new Exception('Test One')); $this->repository->shouldReceive('dropDatabase')->with(self::TEST_DATA['database'], 'dynamic') @@ -260,15 +271,23 @@ class DatabaseManagementServiceTest extends TestCase ])->andReturn(true); $this->repository->shouldReceive('dropUser')->with( - self::TEST_DATA['username'], self::TEST_DATA['remote'], 'dynamic' + self::TEST_DATA['username'], + self::TEST_DATA['remote'], + 'dynamic' )->once()->andReturnNull(); $this->repository->shouldReceive('createUser')->with( - self::TEST_DATA['username'], self::TEST_DATA['remote'], 'new_password', 'dynamic' + self::TEST_DATA['username'], + self::TEST_DATA['remote'], + 'new_password', + 'dynamic' )->once()->andReturnNull(); $this->repository->shouldReceive('assignUserToDatabase')->with( - self::TEST_DATA['database'], self::TEST_DATA['username'], self::TEST_DATA['remote'], 'dynamic' + self::TEST_DATA['database'], + self::TEST_DATA['username'], + self::TEST_DATA['remote'], + 'dynamic' )->once()->andReturnNull(); $this->repository->shouldReceive('flush')->with('dynamic')->once()->andReturnNull(); @@ -300,7 +319,9 @@ class DatabaseManagementServiceTest extends TestCase ])->andReturn(true); $this->repository->shouldReceive('dropUser')->with( - self::TEST_DATA['username'], self::TEST_DATA['remote'], 'dynamic' + self::TEST_DATA['username'], + self::TEST_DATA['remote'], + 'dynamic' )->once()->andThrow(new Exception()); $this->database->shouldReceive('rollBack')->withNoArgs()->once()->andReturnNull(); @@ -324,7 +345,9 @@ class DatabaseManagementServiceTest extends TestCase ->once() ->andReturnNull(); $this->repository->shouldReceive('dropUser')->with( - self::TEST_DATA['username'], self::TEST_DATA['remote'], 'dynamic' + self::TEST_DATA['username'], + self::TEST_DATA['remote'], + 'dynamic' )->once()->andReturnNull(); $this->repository->shouldReceive('flush')->with('dynamic')->once()->andReturnNull(); $this->repository->shouldReceive('delete')->with(1)->once()->andReturn(1); diff --git a/tests/Unit/Services/Nodes/UpdateServiceTest.php b/tests/Unit/Services/Nodes/UpdateServiceTest.php index 74db802b..5b042edb 100644 --- a/tests/Unit/Services/Nodes/UpdateServiceTest.php +++ b/tests/Unit/Services/Nodes/UpdateServiceTest.php @@ -157,7 +157,8 @@ class UpdateServiceTest extends TestCase } catch (Exception $exception) { $this->assertInstanceOf(DisplayException::class, $exception); $this->assertEquals( - trans('admin/exceptions.node.daemon_off_config_updated', ['code' => 400]), $exception->getMessage() + trans('admin/exceptions.node.daemon_off_config_updated', ['code' => 400]), + $exception->getMessage() ); } } diff --git a/tests/Unit/Services/Packs/PackUpdateServiceTest.php b/tests/Unit/Services/Packs/PackUpdateServiceTest.php index 628979bd..dffdecb9 100644 --- a/tests/Unit/Services/Packs/PackUpdateServiceTest.php +++ b/tests/Unit/Services/Packs/PackUpdateServiceTest.php @@ -73,7 +73,7 @@ class PackUpdateServiceTest extends TestCase 'locked' => false, 'visible' => false, 'selectable' => false, - 'test-data' => 'value' + 'test-data' => 'value', ])->once()->andReturn(1); $this->assertEquals(1, $this->service->handle($model, ['test-data' => 'value'])); @@ -108,7 +108,7 @@ class PackUpdateServiceTest extends TestCase 'locked' => false, 'visible' => false, 'selectable' => false, - 'test-data' => 'value' + 'test-data' => 'value', ])->once()->andReturn(1); $this->assertEquals(1, $this->service->handle($model->id, ['test-data' => 'value'])); diff --git a/tests/Unit/Services/Packs/TemplateUploadServiceTest.php b/tests/Unit/Services/Packs/TemplateUploadServiceTest.php index 4f8a81d2..f16583c7 100644 --- a/tests/Unit/Services/Packs/TemplateUploadServiceTest.php +++ b/tests/Unit/Services/Packs/TemplateUploadServiceTest.php @@ -246,7 +246,7 @@ class TemplateUploadServiceTest extends TestCase } /** - * Return values for archive->locateName function, import.json and archive.tar.gz respectively + * Return values for archive->locateName function, import.json and archive.tar.gz respectively. * * @return array */ diff --git a/tests/Unit/Services/Servers/ContainerRebuildServiceTest.php b/tests/Unit/Services/Servers/ContainerRebuildServiceTest.php index ad18baa4..f36e262d 100644 --- a/tests/Unit/Services/Servers/ContainerRebuildServiceTest.php +++ b/tests/Unit/Services/Servers/ContainerRebuildServiceTest.php @@ -133,7 +133,8 @@ class ContainerRebuildServiceTest extends TestCase } catch (Exception $exception) { $this->assertInstanceOf(DisplayException::class, $exception); $this->assertEquals( - trans('admin/server.exceptions.daemon_exception', ['code' => 400]), $exception->getMessage() + trans('admin/server.exceptions.daemon_exception', ['code' => 400]), + $exception->getMessage() ); } } diff --git a/tests/Unit/Services/Servers/DetailsModificationServiceTest.php b/tests/Unit/Services/Servers/DetailsModificationServiceTest.php index a617fbaa..a56697ba 100644 --- a/tests/Unit/Services/Servers/DetailsModificationServiceTest.php +++ b/tests/Unit/Services/Servers/DetailsModificationServiceTest.php @@ -258,7 +258,8 @@ class DetailsModificationServiceTest extends TestCase } catch (Exception $exception) { $this->assertInstanceOf(DisplayException::class, $exception); $this->assertEquals( - trans('admin/server.exceptions.daemon_exception', ['code' => 400]), $exception->getMessage() + trans('admin/server.exceptions.daemon_exception', ['code' => 400]), + $exception->getMessage() ); } } @@ -371,7 +372,8 @@ class DetailsModificationServiceTest extends TestCase } catch (Exception $exception) { $this->assertInstanceOf(DisplayException::class, $exception); $this->assertEquals( - trans('admin/server.exceptions.daemon_exception', ['code' => 400]), $exception->getMessage() + trans('admin/server.exceptions.daemon_exception', ['code' => 400]), + $exception->getMessage() ); } } diff --git a/tests/Unit/Services/Servers/ReinstallServiceTest.php b/tests/Unit/Services/Servers/ReinstallServiceTest.php index a2fee951..d57a1b55 100644 --- a/tests/Unit/Services/Servers/ReinstallServiceTest.php +++ b/tests/Unit/Services/Servers/ReinstallServiceTest.php @@ -162,7 +162,8 @@ class ReinstallServiceTest extends TestCase } catch (Exception $exception) { $this->assertInstanceOf(DisplayException::class, $exception); $this->assertEquals( - trans('admin/server.exceptions.daemon_exception', ['code' => 400]), $exception->getMessage() + trans('admin/server.exceptions.daemon_exception', ['code' => 400]), + $exception->getMessage() ); } } diff --git a/tests/Unit/Services/Servers/SuspensionServiceTest.php b/tests/Unit/Services/Servers/SuspensionServiceTest.php index 3445e02b..1ef5b071 100644 --- a/tests/Unit/Services/Servers/SuspensionServiceTest.php +++ b/tests/Unit/Services/Servers/SuspensionServiceTest.php @@ -190,7 +190,8 @@ class SuspensionServiceTest extends TestCase } catch (Exception $exception) { $this->assertInstanceOf(DisplayException::class, $exception); $this->assertEquals( - trans('admin/server.exceptions.daemon_exception', ['code' => 400]), $exception->getMessage() + trans('admin/server.exceptions.daemon_exception', ['code' => 400]), + $exception->getMessage() ); } } diff --git a/tests/Unit/Services/Users/DeletionServiceTest.php b/tests/Unit/Services/Users/DeletionServiceTest.php index e98a375f..a4e3cd1c 100644 --- a/tests/Unit/Services/Users/DeletionServiceTest.php +++ b/tests/Unit/Services/Users/DeletionServiceTest.php @@ -72,7 +72,9 @@ class DeletionServiceTest extends TestCase $this->serverRepository = m::mock(ServerRepositoryInterface::class); $this->service = new DeletionService( - $this->serverRepository, $this->translator, $this->repository + $this->serverRepository, + $this->translator, + $this->repository ); } From a73e71dd81e18cf890a244db64947a89244191f6 Mon Sep 17 00:00:00 2001 From: Anand Capur Date: Wed, 23 Aug 2017 12:34:34 -0700 Subject: [PATCH 63/99] Fix DB migrations to allow rollbacks --- ...2017_08_05_144104_AllowNegativeValuesForOverallocation.php | 4 ++-- ...17_08_05_174811_SetAllocationUnqiueUsingMultipleFields.php | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/database/migrations/2017_08_05_144104_AllowNegativeValuesForOverallocation.php b/database/migrations/2017_08_05_144104_AllowNegativeValuesForOverallocation.php index 260af9a4..77b7f984 100644 --- a/database/migrations/2017_08_05_144104_AllowNegativeValuesForOverallocation.php +++ b/database/migrations/2017_08_05_144104_AllowNegativeValuesForOverallocation.php @@ -23,8 +23,8 @@ class AllowNegativeValuesForOverallocation extends Migration public function down() { Schema::table('nodes', function (Blueprint $table) { - $table->mediumInteger('disk_overallocate')->unsigned()->nullable()->change(); - $table->mediumInteger('memory_overallocate')->unsigned()->nullable()->change(); + DB::statement('ALTER TABLE nodes MODIFY disk_overallocate MEDIUMINT UNSIGNED NULL, + MODIFY memory_overallocate MEDIUMINT UNSIGNED NULL'); }); } } diff --git a/database/migrations/2017_08_05_174811_SetAllocationUnqiueUsingMultipleFields.php b/database/migrations/2017_08_05_174811_SetAllocationUnqiueUsingMultipleFields.php index ea1cb891..f7aab7c0 100644 --- a/database/migrations/2017_08_05_174811_SetAllocationUnqiueUsingMultipleFields.php +++ b/database/migrations/2017_08_05_174811_SetAllocationUnqiueUsingMultipleFields.php @@ -22,7 +22,9 @@ class SetAllocationUnqiueUsingMultipleFields extends Migration public function down() { Schema::table('allocations', function (Blueprint $table) { + $table->dropForeign(['node_id']); $table->dropUnique(['node_id', 'ip', 'port']); + $table->foreign('node_id')->references('id')->on('nodes'); }); } } From 74ea1aa0aafef7290203f12b71e65da7d9b25f87 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Wed, 23 Aug 2017 21:34:11 -0500 Subject: [PATCH 64/99] Push subuser creation service --- .../Daemon/ServerRepositoryInterface.php | 9 + .../PermissionRepositoryInterface.php | 29 ++ .../SubuserRepositoryInterface.php} | 17 +- .../Helper/CdnVersionFetchingException.php | 29 ++ .../Subuser/ServerSubuserExistsException.php | 31 +++ .../Subuser/UserIsServerOwnerException.php | 31 +++ app/Http/Controllers/Admin/BaseController.php | 23 +- app/Models/Permission.php | 14 +- app/Models/Subuser.php | 26 +- app/Repositories/Daemon/ServerRepository.php | 14 + .../Eloquent/PermissionRepository.php | 39 +++ .../Eloquent/SubuserRepository.php | 39 +++ .../Helpers/SoftwareVersionService.php | 149 ++++++++++ app/Services/Old/APILogService.php | 65 ----- app/Services/Old/VersionService.php | 133 --------- .../Subusers/SubuserCreationService.php | 188 +++++++++++++ config/cache.php | 2 +- config/pterodactyl.php | 2 +- database/factories/ModelFactory.php | 9 + resources/lang/en/admin/exceptions.php | 4 + .../themes/pterodactyl/admin/index.blade.php | 10 +- .../Helpers/SoftwareVersionServiceTest.php | 183 ++++++++++++ .../Subusers/SubuserCreationServiceTest.php | 260 ++++++++++++++++++ 23 files changed, 1077 insertions(+), 229 deletions(-) create mode 100644 app/Contracts/Repository/PermissionRepositoryInterface.php rename app/{Facades/Version.php => Contracts/Repository/SubuserRepositoryInterface.php} (79%) create mode 100644 app/Exceptions/Service/Helper/CdnVersionFetchingException.php create mode 100644 app/Exceptions/Service/Subuser/ServerSubuserExistsException.php create mode 100644 app/Exceptions/Service/Subuser/UserIsServerOwnerException.php create mode 100644 app/Repositories/Eloquent/PermissionRepository.php create mode 100644 app/Repositories/Eloquent/SubuserRepository.php create mode 100644 app/Services/Helpers/SoftwareVersionService.php delete mode 100644 app/Services/Old/APILogService.php delete mode 100644 app/Services/Old/VersionService.php create mode 100644 app/Services/Subusers/SubuserCreationService.php create mode 100644 tests/Unit/Services/Helpers/SoftwareVersionServiceTest.php create mode 100644 tests/Unit/Services/Subusers/SubuserCreationServiceTest.php diff --git a/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php b/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php index de9fe0c3..c6d9ff08 100644 --- a/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php +++ b/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php @@ -36,6 +36,15 @@ interface ServerRepositoryInterface extends BaseRepositoryInterface */ public function create($id, $overrides = [], $start = false); + /** + * Set an access token and associated permissions for a server. + * + * @param string $key + * @param array $permissions + * @return \Psr\Http\Message\ResponseInterface + */ + public function setSubuserKey($key, array $permissions); + /** * Update server details on the daemon. * diff --git a/app/Contracts/Repository/PermissionRepositoryInterface.php b/app/Contracts/Repository/PermissionRepositoryInterface.php new file mode 100644 index 00000000..f84c16f7 --- /dev/null +++ b/app/Contracts/Repository/PermissionRepositoryInterface.php @@ -0,0 +1,29 @@ +. + * + * 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\Contracts\Repository; + +interface PermissionRepositoryInterface extends RepositoryInterface +{ +} diff --git a/app/Facades/Version.php b/app/Contracts/Repository/SubuserRepositoryInterface.php similarity index 79% rename from app/Facades/Version.php rename to app/Contracts/Repository/SubuserRepositoryInterface.php index e9475d2a..766fc1b3 100644 --- a/app/Facades/Version.php +++ b/app/Contracts/Repository/SubuserRepositoryInterface.php @@ -1,5 +1,5 @@ . * @@ -22,19 +22,8 @@ * SOFTWARE. */ -namespace Pterodactyl\Facades; +namespace Pterodactyl\Contracts\Repository; -use Illuminate\Support\Facades\Facade; - -class Version extends Facade +interface SubuserRepositoryInterface extends RepositoryInterface { - /** - * Returns the facade accessor class. - * - * @return strig - */ - protected static function getFacadeAccessor() - { - return '\Pterodactyl\Services\VersionService'; - } } diff --git a/app/Exceptions/Service/Helper/CdnVersionFetchingException.php b/app/Exceptions/Service/Helper/CdnVersionFetchingException.php new file mode 100644 index 00000000..d96fe25d --- /dev/null +++ b/app/Exceptions/Service/Helper/CdnVersionFetchingException.php @@ -0,0 +1,29 @@ +. + * + * 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\Exceptions\Service\Helper; + +class CdnVersionFetchingException extends \Exception +{ +} diff --git a/app/Exceptions/Service/Subuser/ServerSubuserExistsException.php b/app/Exceptions/Service/Subuser/ServerSubuserExistsException.php new file mode 100644 index 00000000..0c14f803 --- /dev/null +++ b/app/Exceptions/Service/Subuser/ServerSubuserExistsException.php @@ -0,0 +1,31 @@ +. + * + * 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\Exceptions\Service\Subuser; + +use Pterodactyl\Exceptions\DisplayException; + +class ServerSubuserExistsException extends DisplayException +{ +} diff --git a/app/Exceptions/Service/Subuser/UserIsServerOwnerException.php b/app/Exceptions/Service/Subuser/UserIsServerOwnerException.php new file mode 100644 index 00000000..ad551b19 --- /dev/null +++ b/app/Exceptions/Service/Subuser/UserIsServerOwnerException.php @@ -0,0 +1,31 @@ +. + * + * 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\Exceptions\Service\Subuser; + +use Pterodactyl\Exceptions\DisplayException; + +class UserIsServerOwnerException extends DisplayException +{ +} diff --git a/app/Http/Controllers/Admin/BaseController.php b/app/Http/Controllers/Admin/BaseController.php index 14665c32..2858cdaf 100644 --- a/app/Http/Controllers/Admin/BaseController.php +++ b/app/Http/Controllers/Admin/BaseController.php @@ -28,6 +28,7 @@ use Krucas\Settings\Settings; use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Http\Requests\Admin\BaseFormRequest; +use Pterodactyl\Services\Helpers\SoftwareVersionService; class BaseController extends Controller { @@ -41,10 +42,26 @@ class BaseController extends Controller */ protected $settings; - public function __construct(AlertsMessageBag $alert, Settings $settings) - { + /** + * @var \Pterodactyl\Services\Helpers\SoftwareVersionService + */ + protected $version; + + /** + * BaseController constructor. + * + * @param \Prologue\Alerts\AlertsMessageBag $alert + * @param \Krucas\Settings\Settings $settings + * @param \Pterodactyl\Services\Helpers\SoftwareVersionService $version + */ + public function __construct( + AlertsMessageBag $alert, + Settings $settings, + SoftwareVersionService $version + ) { $this->alert = $alert; $this->settings = $settings; + $this->version = $version; } /** @@ -54,7 +71,7 @@ class BaseController extends Controller */ public function getIndex() { - return view('admin.index'); + return view('admin.index', ['version' => $this->version]); } /** diff --git a/app/Models/Permission.php b/app/Models/Permission.php index 086586cd..3587e7fc 100644 --- a/app/Models/Permission.php +++ b/app/Models/Permission.php @@ -25,9 +25,13 @@ namespace Pterodactyl\Models; use Illuminate\Database\Eloquent\Model; +use Sofa\Eloquence\Contracts\CleansAttributes; +use Sofa\Eloquence\Eloquence; -class Permission extends Model +class Permission extends Model implements CleansAttributes { + use Eloquence; + /** * Should timestamps be used on this model. * @@ -118,12 +122,12 @@ class Permission extends Model /** * Return a collection of permissions available. * - * @param array $single - * @return \Illuminate\Support\Collection|array + * @param bool $array + * @return array|\Illuminate\Support\Collection */ - public static function listPermissions($single = false) + public static function getPermissions($array = false) { - if ($single) { + if ($array) { return collect(self::$permissions)->mapWithKeys(function ($item) { return $item; })->all(); diff --git a/app/Models/Subuser.php b/app/Models/Subuser.php index a2eff9c9..276f97b9 100644 --- a/app/Models/Subuser.php +++ b/app/Models/Subuser.php @@ -26,10 +26,14 @@ namespace Pterodactyl\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Notifications\Notifiable; +use Sofa\Eloquence\Contracts\CleansAttributes; +use Sofa\Eloquence\Contracts\Validable as ValidableContract; +use Sofa\Eloquence\Eloquence; +use Sofa\Eloquence\Validable; -class Subuser extends Model +class Subuser extends Model implements CleansAttributes, ValidableContract { - use Notifiable; + use Eloquence, Notifiable, Validable; /** * The table associated with the model. @@ -62,6 +66,24 @@ class Subuser extends Model 'server_id' => 'integer', ]; + /** + * @var array + */ + protected static $applicationRules = [ + 'user_id' => 'required', + 'server_id' => 'required', + 'daemonSecret' => 'required', + ]; + + /** + * @var array + */ + protected static $dataIntegrityRules = [ + 'user_id' => 'numeric|exists:users,id', + 'server_id' => 'numeric|exists:servers,id', + 'daemonSecret' => 'string', + ]; + /** * Gets the server associated with a subuser. * diff --git a/app/Repositories/Daemon/ServerRepository.php b/app/Repositories/Daemon/ServerRepository.php index c1cbee1c..8de958b5 100644 --- a/app/Repositories/Daemon/ServerRepository.php +++ b/app/Repositories/Daemon/ServerRepository.php @@ -84,6 +84,20 @@ class ServerRepository extends BaseRepository implements ServerRepositoryInterfa ]); } + /** + * {@inheritdoc} + */ + public function setSubuserKey($key, array $permissions) + { + return $this->getHttpClient()->request('PATCH', '/server', [ + 'json' => [ + 'keys' => [ + $key => $permissions, + ], + ], + ]); + } + /** * {@inheritdoc} */ diff --git a/app/Repositories/Eloquent/PermissionRepository.php b/app/Repositories/Eloquent/PermissionRepository.php new file mode 100644 index 00000000..7fb7b56f --- /dev/null +++ b/app/Repositories/Eloquent/PermissionRepository.php @@ -0,0 +1,39 @@ +. + * + * 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\Repositories\Eloquent; + +use Pterodactyl\Models\Permission; +use Pterodactyl\Contracts\Repository\PermissionRepositoryInterface; + +class PermissionRepository extends EloquentRepository implements PermissionRepositoryInterface +{ + /** + * {@inheritdoc} + */ + public function model() + { + return Permission::class; + } +} diff --git a/app/Repositories/Eloquent/SubuserRepository.php b/app/Repositories/Eloquent/SubuserRepository.php new file mode 100644 index 00000000..cda8864e --- /dev/null +++ b/app/Repositories/Eloquent/SubuserRepository.php @@ -0,0 +1,39 @@ +. + * + * 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\Repositories\Eloquent; + +use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; +use Pterodactyl\Models\Subuser; + +class SubuserRepository extends EloquentRepository implements SubuserRepositoryInterface +{ + /** + * {@inheritdoc} + */ + public function model() + { + return Subuser::class; + } +} diff --git a/app/Services/Helpers/SoftwareVersionService.php b/app/Services/Helpers/SoftwareVersionService.php new file mode 100644 index 00000000..87f84a40 --- /dev/null +++ b/app/Services/Helpers/SoftwareVersionService.php @@ -0,0 +1,149 @@ +. + * + * 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\Helpers; + +use Exception; +use GuzzleHttp\Client; +use Illuminate\Contracts\Cache\Repository as CacheRepository; +use Illuminate\Contracts\Config\Repository as ConfigRepository; +use Pterodactyl\Exceptions\Service\Helper\CdnVersionFetchingException; + +class SoftwareVersionService +{ + const VERSION_CACHE_KEY = 'pterodactyl:versions'; + + /** + * @var \Illuminate\Contracts\Cache\Repository + */ + protected $cache; + + /** + * @var \GuzzleHttp\Client + */ + protected $client; + + /** + * @var \Illuminate\Contracts\Config\Repository + */ + protected $config; + + /** + * SoftwareVersionService constructor. + * + * @param \Illuminate\Contracts\Cache\Repository $cache + * @param \GuzzleHttp\Client $client + * @param \Illuminate\Contracts\Config\Repository $config + */ + public function __construct( + CacheRepository $cache, + Client $client, + ConfigRepository $config + ) { + $this->cache = $cache; + $this->client = $client; + $this->config = $config; + + $this->cacheVersionData(); + } + + /** + * Get the latest version of the panel from the CDN servers. + * + * @return string + */ + public function getPanel() + { + return object_get($this->cache->get(self::VERSION_CACHE_KEY), 'panel', 'error'); + } + + /** + * Get the latest version of the daemon from the CDN servers. + * + * @return string + */ + public function getDaemon() + { + return object_get($this->cache->get(self::VERSION_CACHE_KEY), 'daemon', 'error'); + } + + /** + * Get the URL to the discord server. + * + * @return string + */ + public function getDiscord() + { + return object_get($this->cache->get(self::VERSION_CACHE_KEY), 'discord', 'https://pterodactyl.io/discord'); + } + + /** + * Determine if the current version of the panel is the latest. + * + * @return bool + */ + public function isLatestPanel() + { + if ($this->config->get('app.version') === 'canary') { + return true; + } + + return version_compare($this->config->get('app.version'), $this->getPanel()) >= 0; + } + + /** + * Determine if a passed daemon version string is the latest. + * + * @param string $version + * @return bool + */ + public function isLatestDaemon($version) + { + if ($version === '0.0.0-canary') { + return true; + } + + return version_compare($version, $this->getDaemon()) >= 0; + } + + /** + * Keeps the versioning cache up-to-date with the latest results from the CDN. + */ + protected function cacheVersionData() + { + $this->cache->remember(self::VERSION_CACHE_KEY, $this->config->get('pterodactyl.cdn.cache_time'), function () { + try { + $response = $this->client->request('GET', $this->config->get('pterodactyl.cdn.url')); + + if ($response->getStatusCode() === 200) { + return json_decode($response->getBody()); + } + + throw new CdnVersionFetchingException; + } catch (Exception $exception) { + return (object) []; + } + }); + } +} diff --git a/app/Services/Old/APILogService.php b/app/Services/Old/APILogService.php deleted file mode 100644 index d4467041..00000000 --- a/app/Services/Old/APILogService.php +++ /dev/null @@ -1,65 +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; - -use Log; -use Illuminate\Http\Request; -use Pterodactyl\Models\APILog; - -class APILogService -{ - /** - * Log an API Request. - * - * @param \Illuminate\Http\Request $request - * @param null|string $error - * @param bool $authorized - */ - public static function log(Request $request, $error = null, $authorized = false) - { - if ($request->bearerToken() && ! empty($request->bearerToken())) { - list($public, $hashed) = explode('.', $request->bearerToken()); - } else { - $public = null; - } - - try { - $log = APILog::create([ - 'authorized' => $authorized, - 'error' => $error, - 'key' => $public, - 'method' => $request->method(), - 'route' => $request->fullUrl(), - 'content' => (empty($request->getContent())) ? null : $request->getContent(), - 'user_agent' => $request->header('User-Agent'), - 'request_ip' => $request->ip(), - ]); - $log->save(); - } catch (\Exception $ex) { - // Simply log it and move on. - Log::error($ex); - } - } -} diff --git a/app/Services/Old/VersionService.php b/app/Services/Old/VersionService.php deleted file mode 100644 index 7134b31f..00000000 --- a/app/Services/Old/VersionService.php +++ /dev/null @@ -1,133 +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; - -use Cache; -use GuzzleHttp\Client; - -class VersionService -{ - /** - * The cached CDN response. - * - * @var object - */ - protected static $versions; - - /** - * Version constructor. - */ - public function __construct() - { - self::$versions = Cache::remember('versions', config('pterodactyl.cdn.cache'), function () { - $client = new Client(); - - try { - $response = $client->request('GET', config('pterodactyl.cdn.url')); - - if ($response->getStatusCode() === 200) { - return json_decode($response->getBody()); - } else { - throw new \Exception('Invalid response code.'); - } - } catch (\Exception $ex) { - // Failed request, just return errored version. - return (object) [ - 'panel' => 'error', - 'daemon' => 'error', - 'discord' => 'https://pterodactyl.io/discord', - ]; - } - }); - } - - /** - * Return current panel version from CDN. - * - * @return string - */ - public static function getPanel() - { - return self::$versions->panel; - } - - /** - * Return current daemon version from CDN. - * - * @return string - */ - public static function getDaemon() - { - return self::$versions->daemon; - } - - /** - * Return Discord link from CDN. - * - * @return string - */ - public static function getDiscord() - { - return self::$versions->discord; - } - - /** - * Return current panel version. - * - * @return null|string - */ - public function getCurrentPanel() - { - return config('app.version'); - } - - /** - * Determine if panel is latest version. - * - * @return bool - */ - public static function isLatestPanel() - { - if (config('app.version') === 'canary') { - return true; - } - - return version_compare(config('app.version'), self::$versions->panel) >= 0; - } - - /** - * Determine if daemon is latest version. - * - * @return bool - */ - public static function isLatestDaemon($daemon) - { - if ($daemon === '0.0.0-canary') { - return true; - } - - return version_compare($daemon, self::$versions->daemon) >= 0; - } -} diff --git a/app/Services/Subusers/SubuserCreationService.php b/app/Services/Subusers/SubuserCreationService.php new file mode 100644 index 00000000..39c38f18 --- /dev/null +++ b/app/Services/Subusers/SubuserCreationService.php @@ -0,0 +1,188 @@ +. + * + * 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\Subusers; + +use GuzzleHttp\Exception\RequestException; +use Illuminate\Database\ConnectionInterface; +use Illuminate\Log\Writer; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\PermissionRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Exceptions\Service\Subuser\ServerSubuserExistsException; +use Pterodactyl\Exceptions\Service\Subuser\UserIsServerOwnerException; +use Pterodactyl\Models\Permission; +use Pterodactyl\Models\Server; +use Pterodactyl\Services\Users\CreationService; + +class SubuserCreationService +{ + const CORE_DAEMON_PERMISSIONS = [ + 's:get', + 's:console', + ]; + + const DAEMON_SECRET_BYTES = 18; + + /** + * @var \Illuminate\Database\ConnectionInterface + */ + protected $connection; + + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface + */ + protected $daemonRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\PermissionRepositoryInterface + */ + protected $permissionRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface + */ + protected $subuserRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $serverRepository; + + /** + * @var \Pterodactyl\Services\Users\CreationService + */ + protected $userCreationService; + + /** + * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface + */ + protected $userRepository; + + /** + * @var \Illuminate\Log\Writer + */ + protected $writer; + + public function __construct( + ConnectionInterface $connection, + CreationService $userCreationService, + DaemonServerRepositoryInterface $daemonRepository, + PermissionRepositoryInterface $permissionRepository, + ServerRepositoryInterface $serverRepository, + SubuserRepositoryInterface $subuserRepository, + UserRepositoryInterface $userRepository, + Writer $writer + ) { + $this->connection = $connection; + $this->daemonRepository = $daemonRepository; + $this->permissionRepository = $permissionRepository; + $this->subuserRepository = $subuserRepository; + $this->serverRepository = $serverRepository; + $this->userRepository = $userRepository; + $this->userCreationService = $userCreationService; + $this->writer = $writer; + } + + /** + * @param int|\Pterodactyl\Models\Server $server + * @param string $email + * @param array $permissions + * @return \Pterodactyl\Models\Subuser + * + * @throws \Exception + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Pterodactyl\Exceptions\Service\Subuser\ServerSubuserExistsException + * @throws \Pterodactyl\Exceptions\Service\Subuser\UserIsServerOwnerException + */ + public function handle($server, $email, array $permissions) + { + if (! $server instanceof Server) { + $server = $this->serverRepository->find($server); + } + + $user = $this->userRepository->findWhere([['email', '=', $email]]); + if (is_null($user)) { + $user = $this->userCreationService->handle([ + 'email' => $email, + 'username' => substr(strtok($email, '@'), 0, 8), + 'name_first' => 'Server', + 'name_last' => 'Subuser', + 'root_admin' => false, + ]); + } else { + if ($server->owner_id === $user->id) { + throw new UserIsServerOwnerException(trans('admin/exceptions.subusers.user_is_owner')); + } + + $subuserCount = $this->subuserRepository->findCountWhere([['user_id', '=', $user->id], ['server_id', '=', $server->id]]); + if ($subuserCount !== 0) { + throw new ServerSubuserExistsException(trans('admin/exceptions.subusers.subuser_exists')); + } + } + + $this->connection->beginTransaction(); + $subuser = $this->subuserRepository->create([ + 'user_id' => $user->id, + 'server_id' => $server->id, + 'daemonSecret' => bin2hex(random_bytes(self::DAEMON_SECRET_BYTES)), + ]); + + $permissionMappings = Permission::getPermissions(true); + $daemonPermissions = self::CORE_DAEMON_PERMISSIONS; + + foreach ($permissions as $permission) { + if (array_key_exists($permission, $permissionMappings)) { + if (! is_null($permissionMappings[$permission])) { + array_push($daemonPermissions, $permissionMappings[$permission]); + } + + $this->permissionRepository->create([ + 'subuser_id' => $subuser->id, + 'permission' => $permission, + ]); + } + } + + try { + $this->daemonRepository->setNode($server->node_id)->setAccessServer($server->uuid) + ->setSubuserKey($subuser->daemonSecret, $daemonPermissions); + $this->connection->commit(); + + return $subuser; + } catch (RequestException $exception) { + $response = $exception->getResponse(); + $this->writer->warning($exception); + + throw new DisplayException(trans('admin/exceptions.daemon_connection_failed', [ + 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), + ])); + } + } +} diff --git a/config/cache.php b/config/cache.php index fb619fb5..6109216c 100644 --- a/config/cache.php +++ b/config/cache.php @@ -80,5 +80,5 @@ return [ | */ - 'prefix' => 'laravel', + 'prefix' => 'pterodactyl', ]; diff --git a/config/pterodactyl.php b/config/pterodactyl.php index 32b239bd..a21283d1 100644 --- a/config/pterodactyl.php +++ b/config/pterodactyl.php @@ -119,7 +119,7 @@ return [ | if panel is up to date. */ 'cdn' => [ - 'cache' => 60, + 'cache_time' => 60, 'url' => 'https://cdn.pterodactyl.io/releases/latest.json', ], diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index a6fbc069..398915ac 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -145,3 +145,12 @@ $factory->define(Pterodactyl\Models\Pack::class, function (Faker\Generator $fake 'locked' => 0, ]; }); + +$factory->define(Pterodactyl\Models\Subuser::class, function (Faker\Generator $faker) { + return [ + 'id' => $faker->unique()->randomNumber(), + 'user_id' => $faker->randomNumber(), + 'server_id' => $faker->randomNumber(), + 'daemonSecret' => $faker->unique()->uuid, + ]; +}); diff --git a/resources/lang/en/admin/exceptions.php b/resources/lang/en/admin/exceptions.php index 4ff0c4b4..21bd812b 100644 --- a/resources/lang/en/admin/exceptions.php +++ b/resources/lang/en/admin/exceptions.php @@ -54,4 +54,8 @@ return [ 'zip_extraction' => 'An exception was encountered while attempting to extract the archive provided onto the server.', 'invalid_archive_exception' => 'The pack archive provided appears to be missing a required archive.tar.gz or import.json file in the base directory.', ], + 'subusers' => [ + 'user_is_owner' => 'You cannot add the server owner as a subuser for this server.', + 'subuser_exists' => 'A user with that email address is already assigned as a subuser for this server.', + ], ]; diff --git a/resources/themes/pterodactyl/admin/index.blade.php b/resources/themes/pterodactyl/admin/index.blade.php index 76f0ed3f..e2884ba0 100644 --- a/resources/themes/pterodactyl/admin/index.blade.php +++ b/resources/themes/pterodactyl/admin/index.blade.php @@ -35,7 +35,7 @@
    System Information
    - @if (Version::isLatestPanel()) - You are running Pterodactyl Panel version {{ Version::getCurrentPanel() }}. Your panel is up-to-date! + @if ($version->isLatestPanel()) + You are running Pterodactyl Panel version {{ config('app.version') }}. Your panel is up-to-date! @else - Your panel is not up-to-date! The latest version is {{ Version::getPanel() }} and you are currently running version {{ Version::getCurrentPanel() }}. + Your panel is not up-to-date! The latest version is {{ $version->getPanel() }} and you are currently running version {{ config('app.version') }}. @endif
    @@ -56,7 +56,7 @@
    diff --git a/tests/Unit/Services/Helpers/SoftwareVersionServiceTest.php b/tests/Unit/Services/Helpers/SoftwareVersionServiceTest.php new file mode 100644 index 00000000..a3039694 --- /dev/null +++ b/tests/Unit/Services/Helpers/SoftwareVersionServiceTest.php @@ -0,0 +1,183 @@ +. + * + * 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\Helpers; + +use Closure; +use GuzzleHttp\Client; +use Mockery as m; +use Pterodactyl\Services\Helpers\SoftwareVersionService; +use Tests\TestCase; +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() + { + 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 booklean 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/Subusers/SubuserCreationServiceTest.php b/tests/Unit/Services/Subusers/SubuserCreationServiceTest.php new file mode 100644 index 00000000..a7492cb3 --- /dev/null +++ b/tests/Unit/Services/Subusers/SubuserCreationServiceTest.php @@ -0,0 +1,260 @@ +. + * + * 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\Subusers; + +use Illuminate\Database\ConnectionInterface; +use Illuminate\Log\Writer; +use Mockery as m; +use phpmock\phpunit\PHPMock; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\PermissionRepositoryInterface; +use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Exceptions\Service\Subuser\ServerSubuserExistsException; +use Pterodactyl\Exceptions\Service\Subuser\UserIsServerOwnerException; +use Pterodactyl\Models\Server; +use Pterodactyl\Models\Subuser; +use Pterodactyl\Models\User; +use Pterodactyl\Services\Subusers\SubuserCreationService; +use Pterodactyl\Services\Users\CreationService; +use Tests\TestCase; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; + +class SubuserCreationServiceTest extends TestCase +{ + use PHPMock; + + /** + * @var \Illuminate\Database\ConnectionInterface + */ + protected $connection; + + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface + */ + protected $daemonRepository; + + /** + * @var \Pterodactyl\Models\Permission + */ + protected $permission; + + /** + * @var \Pterodactyl\Contracts\Repository\PermissionRepositoryInterface + */ + protected $permissionRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface + */ + protected $subuserRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $serverRepository; + + /** + * @var \Pterodactyl\Services\Subusers\SubuserCreationService + */ + protected $service; + + /** + * @var \Pterodactyl\Services\Users\CreationService + */ + protected $userCreationService; + + /** + * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface + */ + protected $userRepository; + + /** + * @var \Illuminate\Log\Writer + */ + protected $writer; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->getFunctionMock('\\Pterodactyl\\Services\\Subusers', 'bin2hex')->expects($this->any())->willReturn('bin2hex'); + + $this->connection = m::mock(ConnectionInterface::class); + $this->daemonRepository = m::mock(DaemonServerRepositoryInterface::class); + $this->permission = m::mock('overload:Pterodactyl\Models\Permission'); + $this->permissionRepository = m::mock(PermissionRepositoryInterface::class); + $this->subuserRepository = m::mock(SubuserRepositoryInterface::class); + $this->serverRepository = m::mock(ServerRepositoryInterface::class); + $this->userCreationService = m::mock(CreationService::class); + $this->userRepository = m::mock(UserRepositoryInterface::class); + $this->writer = m::mock(Writer::class); + + $this->service = new SubuserCreationService( + $this->connection, + $this->userCreationService, + $this->daemonRepository, + $this->permissionRepository, + $this->serverRepository, + $this->subuserRepository, + $this->userRepository, + $this->writer + ); + } + + /** + * Test that a user without an existing account can be added as a subuser. + */ + public function testAccountIsCreatedForNewUser() + { + $permissions = ['test-1' => 'test:1', 'test-2' => null]; + $server = factory(Server::class)->make(); + $user = factory(User::class)->make(); + $subuser = factory(Subuser::class)->make(['user_id' => $user->id, 'server_id' => $server->id]); + + $this->userRepository->shouldReceive('findWhere')->with([['email', '=', $user->email]])->once()->andReturnNull(); + $this->userCreationService->shouldReceive('handle')->with([ + 'email' => $user->email, + 'username' => substr(strtok($user->email, '@'), 0, 8), + 'name_first' => 'Server', + 'name_last' => 'Subuser', + 'root_admin' => false, + ])->once()->andReturn($user); + + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->subuserRepository->shouldReceive('create')->with([ + 'user_id' => $user->id, + 'server_id' => $server->id, + 'daemonSecret' => 'bin2hex', + ])->once()->andReturn($subuser); + + $this->permission->shouldReceive('getPermissions')->with(true)->once() + ->andReturn($permissions); + + foreach(array_keys($permissions) as $permission) { + $this->permissionRepository->shouldReceive('create') + ->with(['subuser_id' => $subuser->id, 'permission' => $permission]) + ->once()->andReturnNull(); + } + + $this->daemonRepository->shouldReceive('setNode')->with($server->node_id)->once()->andReturnSelf() + ->shouldReceive('setAccessServer')->with($server->uuid)->once()->andReturnSelf() + ->shouldReceive('setSubuserKey')->with($subuser->daemonSecret, ['s:get', 's:console', 'test:1'])->once()->andReturnSelf(); + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $response = $this->service->handle($server, $user->email, array_keys($permissions)); + + $this->assertInstanceOf(Subuser::class, $response); + $this->assertSame($subuser, $response); + } + + /** + * Test that an existing user can be added as a subuser. + */ + public function testExistingUserCanBeAddedAsASubuser() + { + $permissions = ['test-1' => 'test:1', 'test-2' => null]; + $server = factory(Server::class)->make(); + $user = factory(User::class)->make(); + $subuser = factory(Subuser::class)->make(['user_id' => $user->id, 'server_id' => $server->id]); + + $this->userRepository->shouldReceive('findWhere')->with([['email', '=', $user->email]])->once()->andReturn($user); + $this->subuserRepository->shouldReceive('findCountWhere')->with([ + ['user_id', '=', $user->id], + ['server_id', '=', $server->id], + ])->once()->andReturn(0); + + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->subuserRepository->shouldReceive('create')->with([ + 'user_id' => $user->id, + 'server_id' => $server->id, + 'daemonSecret' => 'bin2hex', + ])->once()->andReturn($subuser); + + $this->permission->shouldReceive('getPermissions')->with(true)->once() + ->andReturn($permissions); + + foreach(array_keys($permissions) as $permission) { + $this->permissionRepository->shouldReceive('create') + ->with(['subuser_id' => $subuser->id, 'permission' => $permission]) + ->once()->andReturnNull(); + } + + $this->daemonRepository->shouldReceive('setNode')->with($server->node_id)->once()->andReturnSelf() + ->shouldReceive('setAccessServer')->with($server->uuid)->once()->andReturnSelf() + ->shouldReceive('setSubuserKey')->with($subuser->daemonSecret, ['s:get', 's:console', 'test:1'])->once()->andReturnSelf(); + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $response = $this->service->handle($server, $user->email, array_keys($permissions)); + + $this->assertInstanceOf(Subuser::class, $response); + $this->assertSame($subuser, $response); + } + + /** + * Test that an exception gets thrown if the subuser is actually the server owner + */ + public function testExceptionIsThrownIfUserIsServerOwner() + { + $user = factory(User::class)->make(); + $server = factory(Server::class)->make(['owner_id' => $user->id]); + + $this->userRepository->shouldReceive('findWhere')->with([['email', '=', $user->email]])->once()->andReturn($user); + + try { + $this->service->handle($server, $user->email, []); + } catch (DisplayException $exception) { + $this->assertInstanceOf(UserIsServerOwnerException::class, $exception); + $this->assertEquals(trans('admin/exceptions.subusers.user_is_owner'), $exception->getMessage()); + } + } + + /** + * Test that an exception is thrown if the user is already added as a subuser. + */ + public function testExceptionIsThrownIfUserIsAlreadyASubuser() + { + $user = factory(User::class)->make(); + $server = factory(Server::class)->make(); + + $this->userRepository->shouldReceive('findWhere')->with([['email', '=', $user->email]])->once()->andReturn($user); + $this->subuserRepository->shouldReceive('findCountWhere')->with([ + ['user_id', '=', $user->id], + ['server_id', '=', $server->id], + ])->once()->andReturn(1); + + try { + $this->service->handle($server, $user->email, []); + } catch (DisplayException $exception) { + $this->assertInstanceOf(ServerSubuserExistsException::class, $exception); + $this->assertEquals(trans('admin/exceptions.subusers.subuser_exists'), $exception->getMessage()); + } + + } +} From 2cabb61b541158d5749b9161236e42e024cfbcab Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 26 Aug 2017 13:31:18 -0500 Subject: [PATCH 65/99] Add subuser deletion service --- .../Repository/SubuserRepositoryInterface.php | 9 ++ .../Eloquent/SubuserRepository.php | 17 +++ .../Subusers/SubuserCreationService.php | 1 + .../Subusers/SubuserDeletionService.php | 108 ++++++++++++++ database/factories/ModelFactory.php | 3 +- .../Subusers/SubuserDeletionServiceTest.php | 139 ++++++++++++++++++ 6 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 app/Services/Subusers/SubuserDeletionService.php create mode 100644 tests/Unit/Services/Subusers/SubuserDeletionServiceTest.php diff --git a/app/Contracts/Repository/SubuserRepositoryInterface.php b/app/Contracts/Repository/SubuserRepositoryInterface.php index 766fc1b3..a31023f6 100644 --- a/app/Contracts/Repository/SubuserRepositoryInterface.php +++ b/app/Contracts/Repository/SubuserRepositoryInterface.php @@ -26,4 +26,13 @@ namespace Pterodactyl\Contracts\Repository; interface SubuserRepositoryInterface extends RepositoryInterface { + /** + * Find a subuser and return with server and permissions relationships. + * + * @param int $id + * @return \Illuminate\Database\Eloquent\Collection + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function getWithServerAndPermissions($id); } diff --git a/app/Repositories/Eloquent/SubuserRepository.php b/app/Repositories/Eloquent/SubuserRepository.php index cda8864e..4909ecd1 100644 --- a/app/Repositories/Eloquent/SubuserRepository.php +++ b/app/Repositories/Eloquent/SubuserRepository.php @@ -25,7 +25,9 @@ namespace Pterodactyl\Repositories\Eloquent; use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; +use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Models\Subuser; +use Webmozart\Assert\Assert; class SubuserRepository extends EloquentRepository implements SubuserRepositoryInterface { @@ -36,4 +38,19 @@ class SubuserRepository extends EloquentRepository implements SubuserRepositoryI { return Subuser::class; } + + /** + * {@inheritdoc} + */ + public function getWithServerAndPermissions($id) + { + Assert::numeric($id, 'First argument passed to getWithServerAndPermissions must be numeric, received %s.'); + + $instance = $this->getBuilder()->with(['server', 'permission'])->find($id, $this->getColumns()); + if (! $instance) { + throw new RecordNotFoundException; + } + + return $instance; + } } diff --git a/app/Services/Subusers/SubuserCreationService.php b/app/Services/Subusers/SubuserCreationService.php index 39c38f18..8f6e8329 100644 --- a/app/Services/Subusers/SubuserCreationService.php +++ b/app/Services/Subusers/SubuserCreationService.php @@ -177,6 +177,7 @@ class SubuserCreationService return $subuser; } catch (RequestException $exception) { + $this->connection->rollBack(); $response = $exception->getResponse(); $this->writer->warning($exception); diff --git a/app/Services/Subusers/SubuserDeletionService.php b/app/Services/Subusers/SubuserDeletionService.php new file mode 100644 index 00000000..d06f57bd --- /dev/null +++ b/app/Services/Subusers/SubuserDeletionService.php @@ -0,0 +1,108 @@ +. + * + * 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\Subusers; + +use Illuminate\Log\Writer; +use GuzzleHttp\Exception\RequestException; +use Illuminate\Database\ConnectionInterface; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; + +class SubuserDeletionService +{ + /** + * @var \Illuminate\Database\ConnectionInterface + */ + protected $connection; + + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface + */ + protected $daemonRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface + */ + protected $repository; + + /** + * @var \Illuminate\Log\Writer + */ + protected $writer; + + /** + * SubuserDeletionService constructor. + * + * @param \Illuminate\Database\ConnectionInterface $connection + * @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonRepository + * @param \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface $repository + * @param \Illuminate\Log\Writer $writer + */ + public function __construct( + ConnectionInterface $connection, + DaemonServerRepositoryInterface $daemonRepository, + SubuserRepositoryInterface $repository, + Writer $writer + ) { + $this->connection = $connection; + $this->daemonRepository = $daemonRepository; + $this->repository = $repository; + $this->writer = $writer; + } + + /** + * Delete a subuser and their associated permissions from the Panel and Daemon. + * + * @param int $subuser + * @return int|null + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function handle($subuser) + { + $subuser = $this->repository->getWithServerAndPermissions($subuser); + + $this->connection->beginTransaction(); + $response = $this->repository->delete($subuser->id); + + try { + $this->daemonRepository->setNode($subuser->server->node_id)->setAccessServer($subuser->server->uuid) + ->setSubuserKey($subuser->daemonSecret, []); + $this->connection->commit(); + + return $response; + } catch (RequestException $exception) { + $this->connection->rollBack(); + $response = $exception->getResponse(); + $this->writer->warning($exception); + + throw new DisplayException(trans('admin/exceptions.daemon_connection_failed', [ + 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), + ])); + } + } +} diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index 398915ac..b8b2f123 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -16,7 +16,8 @@ $factory->define(Pterodactyl\Models\Server::class, function (Faker\Generator $faker) { return [ 'id' => $faker->unique()->randomNumber(), - 'uuid' => $faker->uuid, + 'node_id' => $faker->randomNumber(), + 'uuid' => $faker->unique()->uuid, 'uuidShort' => str_random(8), 'name' => $faker->firstName, 'description' => implode(' ', $faker->sentences()), diff --git a/tests/Unit/Services/Subusers/SubuserDeletionServiceTest.php b/tests/Unit/Services/Subusers/SubuserDeletionServiceTest.php new file mode 100644 index 00000000..e465eb16 --- /dev/null +++ b/tests/Unit/Services/Subusers/SubuserDeletionServiceTest.php @@ -0,0 +1,139 @@ +. + * + * 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\Subusers; + +use GuzzleHttp\Exception\RequestException; +use Illuminate\Database\ConnectionInterface; +use Illuminate\Log\Writer; +use Mockery as m; +use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Models\Server; +use Pterodactyl\Models\Subuser; +use Pterodactyl\Services\Subusers\SubuserDeletionService; +use Tests\TestCase; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; + +class SubuserDeletionServiceTest extends TestCase +{ + /** + * @var \Illuminate\Database\ConnectionInterface + */ + protected $connection; + + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface + */ + protected $daemonRepository; + + /** + * @var \GuzzleHttp\Exception\RequestException + */ + protected $exception; + + /** + * @var \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Subusers\SubuserDeletionService + */ + protected $service; + + /** + * @var \Illuminate\Log\Writer + */ + protected $writer; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->connection = m::mock(ConnectionInterface::class); + $this->daemonRepository = m::mock(DaemonServerRepositoryInterface::class); + $this->exception = m::mock(RequestException::class); + $this->repository = m::mock(SubuserRepositoryInterface::class); + $this->writer = m::mock(Writer::class); + + $this->service = new SubuserDeletionService( + $this->connection, + $this->daemonRepository, + $this->repository, + $this->writer + ); + } + + /** + * Test that a subuser is deleted correctly. + */ + public function testSubuserIsDeleted() + { + $subuser = factory(Subuser::class)->make(); + $subuser->server = factory(Server::class)->make(); + + $this->repository->shouldReceive('getWithServerAndPermissions')->with($subuser->id)->once()->andReturn($subuser); + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('delete')->with($subuser->id)->once()->andReturn(1); + + $this->daemonRepository->shouldReceive('setNode')->with($subuser->server->node_id)->once()->andReturnSelf() + ->shouldReceive('setAccessServer')->with($subuser->server->uuid)->once()->andReturnSelf() + ->shouldReceive('setSubuserKey')->with($subuser->daemonSecret, [])->once()->andReturnNull(); + + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $response = $this->service->handle($subuser->id); + $this->assertEquals(1, $response); + } + + /** + * Test that an exception caused by the daemon is properly handled. + */ + public function testExceptionIsThrownIfDaemonConnectionFails() + { + $subuser = factory(Subuser::class)->make(); + $subuser->server = factory(Server::class)->make(); + + $this->repository->shouldReceive('getWithServerAndPermissions')->with($subuser->id)->once()->andReturn($subuser); + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('delete')->with($subuser->id)->once()->andReturn(1); + + $this->daemonRepository->shouldReceive('setNode->setAccessServer->setSubuserKey')->once()->andThrow($this->exception); + + $this->connection->shouldReceive('rollBack')->withNoArgs()->once()->andReturnNull(); + $this->exception->shouldReceive('getResponse')->withNoArgs()->once()->andReturnNull(); + $this->writer->shouldReceive('warning')->with($this->exception)->once()->andReturnNull(); + + try { + $this->service->handle($subuser->id); + } catch (DisplayException $exception) { + $this->assertEquals(trans('admin/exceptions.daemon_connection_failed', ['code' => 'E_CONN_REFUSED']), $exception->getMessage()); + } + } + +} From 72735c24f7f441269ab189fb3feb92d41ec14345 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 26 Aug 2017 18:08:11 -0500 Subject: [PATCH 66/99] Complete move from old repository to new repository structure! --- .../Daemon/BaseRepositoryInterface.php | 4 +- .../Daemon/CommandRepositoryInterface.php | 36 +++ .../Daemon/FileRepositoryInterface.php | 61 ++++ .../Daemon/PowerRepositoryInterface.php | 43 +++ .../Daemon/ServerRepositoryInterface.php | 2 +- .../Repository/SubuserRepositoryInterface.php | 10 + .../Daemon/InvalidPowerSignalException.php | 29 ++ app/Repositories/Daemon/BaseRepository.php | 61 +++- app/Repositories/Daemon/CommandRepository.php | 45 +++ app/Repositories/Daemon/FileRepository.php | 126 +++++++++ app/Repositories/Daemon/PowerRepository.php | 55 ++++ app/Repositories/Daemon/ServerRepository.php | 10 +- .../Eloquent/SubuserRepository.php | 15 + app/Repositories/Old/SubuserRepository.php | 262 ------------------ .../old_Daemon/CommandRepository.php | 90 ------ .../old_Daemon/FileRepository.php | 192 ------------- .../old_Daemon/PowerRepository.php | 120 -------- .../Subusers/PermissionCreationService.php | 84 ++++++ .../Subusers/SubuserCreationService.php | 33 +-- .../Subusers/SubuserDeletionService.php | 4 +- .../Subusers/SubuserUpdateService.php | 125 +++++++++ app/helpers.php | 10 + config/pterodactyl.php | 1 + .../PermissionCreationServiceTest.php | 72 +++++ .../Subusers/SubuserCreationServiceTest.php | 42 +-- .../Subusers/SubuserDeletionServiceTest.php | 4 +- .../Subusers/SubuserUpdateServiceTest.php | 158 +++++++++++ 27 files changed, 964 insertions(+), 730 deletions(-) create mode 100644 app/Contracts/Repository/Daemon/CommandRepositoryInterface.php create mode 100644 app/Contracts/Repository/Daemon/FileRepositoryInterface.php create mode 100644 app/Contracts/Repository/Daemon/PowerRepositoryInterface.php create mode 100644 app/Exceptions/Repository/Daemon/InvalidPowerSignalException.php create mode 100644 app/Repositories/Daemon/CommandRepository.php create mode 100644 app/Repositories/Daemon/FileRepository.php create mode 100644 app/Repositories/Daemon/PowerRepository.php delete mode 100644 app/Repositories/Old/SubuserRepository.php delete mode 100644 app/Repositories/old_Daemon/CommandRepository.php delete mode 100644 app/Repositories/old_Daemon/FileRepository.php delete mode 100644 app/Repositories/old_Daemon/PowerRepository.php create mode 100644 app/Services/Subusers/PermissionCreationService.php create mode 100644 app/Services/Subusers/SubuserUpdateService.php create mode 100644 tests/Unit/Services/Subusers/PermissionCreationServiceTest.php create mode 100644 tests/Unit/Services/Subusers/SubuserUpdateServiceTest.php diff --git a/app/Contracts/Repository/Daemon/BaseRepositoryInterface.php b/app/Contracts/Repository/Daemon/BaseRepositoryInterface.php index a18924f9..234fe42b 100644 --- a/app/Contracts/Repository/Daemon/BaseRepositoryInterface.php +++ b/app/Contracts/Repository/Daemon/BaseRepositoryInterface.php @@ -31,6 +31,8 @@ interface BaseRepositoryInterface * * @param int $id * @return $this + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function setNode($id); @@ -77,5 +79,5 @@ interface BaseRepositoryInterface * @param array $headers * @return \GuzzleHttp\Client */ - public function getHttpClient($headers = []); + public function getHttpClient(array $headers = []); } diff --git a/app/Contracts/Repository/Daemon/CommandRepositoryInterface.php b/app/Contracts/Repository/Daemon/CommandRepositoryInterface.php new file mode 100644 index 00000000..f55113a9 --- /dev/null +++ b/app/Contracts/Repository/Daemon/CommandRepositoryInterface.php @@ -0,0 +1,36 @@ +. + * + * 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\Contracts\Repository\Daemon; + +interface CommandRepositoryInterface extends BaseRepositoryInterface +{ + /** + * Send a command to a server. + * + * @param string $command + * @return \Psr\Http\Message\ResponseInterface + */ + public function send($command); +} diff --git a/app/Contracts/Repository/Daemon/FileRepositoryInterface.php b/app/Contracts/Repository/Daemon/FileRepositoryInterface.php new file mode 100644 index 00000000..a013bc1a --- /dev/null +++ b/app/Contracts/Repository/Daemon/FileRepositoryInterface.php @@ -0,0 +1,61 @@ +. + * + * 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\Contracts\Repository\Daemon; + +interface FileRepositoryInterface extends BaseRepositoryInterface +{ + /** + * Return stat information for a given file. + * + * @param string $path + * @return object + */ + public function getFileStat($path); + + /** + * Return the contents of a given file if it can be edited in the Panel. + * + * @param string $path + * @return object + */ + public function getContent($path); + + /** + * Save new contents to a given file. + * + * @param string $path + * @param string $content + * @return \Psr\Http\Message\ResponseInterface + */ + public function putContent($path, $content); + + /** + * Return a directory listing for a given path. + * + * @param string $path + * @return array + */ + public function getDirectory($path); +} diff --git a/app/Contracts/Repository/Daemon/PowerRepositoryInterface.php b/app/Contracts/Repository/Daemon/PowerRepositoryInterface.php new file mode 100644 index 00000000..11c0c7e5 --- /dev/null +++ b/app/Contracts/Repository/Daemon/PowerRepositoryInterface.php @@ -0,0 +1,43 @@ +. + * + * 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\Contracts\Repository\Daemon; + +interface PowerRepositoryInterface extends BaseRepositoryInterface +{ + const SIGNAL_START = 'start'; + const SIGNAL_STOP = 'stop'; + const SIGNAL_RESTART = 'restart'; + const SIGNAL_KILL = 'kill'; + + /** + * Send a power signal to a server. + * + * @param string $signal + * @return \Psr\Http\Message\ResponseInterface + * + * @throws \Pterodactyl\Exceptions\Repository\Daemon\InvalidPowerSignalException + */ + public function sendSignal($signal); +} diff --git a/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php b/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php index c6d9ff08..42bfb975 100644 --- a/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php +++ b/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php @@ -34,7 +34,7 @@ interface ServerRepositoryInterface extends BaseRepositoryInterface * @param bool $start * @return \Psr\Http\Message\ResponseInterface */ - public function create($id, $overrides = [], $start = false); + public function create($id, array $overrides = [], $start = false); /** * Set an access token and associated permissions for a server. diff --git a/app/Contracts/Repository/SubuserRepositoryInterface.php b/app/Contracts/Repository/SubuserRepositoryInterface.php index a31023f6..93eb39b7 100644 --- a/app/Contracts/Repository/SubuserRepositoryInterface.php +++ b/app/Contracts/Repository/SubuserRepositoryInterface.php @@ -26,6 +26,16 @@ namespace Pterodactyl\Contracts\Repository; interface SubuserRepositoryInterface extends RepositoryInterface { + /** + * Return a subuser with the associated server relationship. + * + * @param int $id + * @return \Illuminate\Database\Eloquent\Collection + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function getWithServer($id); + /** * Find a subuser and return with server and permissions relationships. * diff --git a/app/Exceptions/Repository/Daemon/InvalidPowerSignalException.php b/app/Exceptions/Repository/Daemon/InvalidPowerSignalException.php new file mode 100644 index 00000000..21579d20 --- /dev/null +++ b/app/Exceptions/Repository/Daemon/InvalidPowerSignalException.php @@ -0,0 +1,29 @@ +. + * + * 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\Exceptions\Repository\Daemon; + +class InvalidPowerSignalException extends \Exception +{ +} diff --git a/app/Repositories/Daemon/BaseRepository.php b/app/Repositories/Daemon/BaseRepository.php index 43e2e229..fc62d73e 100644 --- a/app/Repositories/Daemon/BaseRepository.php +++ b/app/Repositories/Daemon/BaseRepository.php @@ -29,16 +29,47 @@ use Illuminate\Foundation\Application; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Illuminate\Contracts\Config\Repository as ConfigRepository; use Pterodactyl\Contracts\Repository\Daemon\BaseRepositoryInterface; +use Webmozart\Assert\Assert; class BaseRepository implements BaseRepositoryInterface { + /** + * @var \Illuminate\Foundation\Application + */ protected $app; + + /** + * @var + */ protected $accessServer; + + /** + * @var + */ protected $accessToken; + + /** + * @var + */ protected $node; + + /** + * @var \Illuminate\Contracts\Config\Repository + */ protected $config; + + /** + * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface + */ protected $nodeRepository; + /** + * BaseRepository constructor. + * + * @param \Illuminate\Foundation\Application $app + * @param \Illuminate\Contracts\Config\Repository $config + * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $nodeRepository + */ public function __construct( Application $app, ConfigRepository $config, @@ -49,44 +80,70 @@ class BaseRepository implements BaseRepositoryInterface $this->nodeRepository = $nodeRepository; } + /** + * {@inheritdoc} + */ public function setNode($id) { - // @todo accept a model + Assert::numeric($id, 'The first argument passed to setNode must be numeric, received %s.'); + $this->node = $this->nodeRepository->find($id); return $this; } + /** + * {@inheritdoc} + */ public function getNode() { return $this->node; } + /** + * {@inheritdoc} + */ public function setAccessServer($server = null) { + Assert::nullOrString($server, 'The first argument passed to setAccessServer must be null or a string, received %s.'); + $this->accessServer = $server; return $this; } + /** + * {@inheritdoc} + */ public function getAccessServer() { return $this->accessServer; } + /** + * {@inheritdoc} + */ public function setAccessToken($token = null) { + Assert::nullOrString($token, 'The first argument passed to setAccessToken must be null or a string, received %s.'); + $this->accessToken = $token; return $this; } + /** + * {@inheritdoc} + */ public function getAccessToken() { return $this->accessToken; } - public function getHttpClient($headers = []) + /** + * {@inheritdoc} + */ + public function getHttpClient(array $headers = []) { if (! is_null($this->accessServer)) { $headers['X-Access-Server'] = $this->getAccessServer(); diff --git a/app/Repositories/Daemon/CommandRepository.php b/app/Repositories/Daemon/CommandRepository.php new file mode 100644 index 00000000..4984b6e4 --- /dev/null +++ b/app/Repositories/Daemon/CommandRepository.php @@ -0,0 +1,45 @@ +. + * + * 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\Repositories\Daemon; + +use Webmozart\Assert\Assert; +use Pterodactyl\Contracts\Repository\Daemon\CommandRepositoryInterface; + +class CommandRepository extends BaseRepository implements CommandRepositoryInterface +{ + /** + * {@inheritdoc} + */ + public function send($command) + { + Assert::stringNotEmpty($command, 'First argument passed to send must be a non-empty string, received %s.'); + + return $this->getHttpClient()->request('POST', '/server/command', [ + 'json' => [ + 'command' => $command, + ], + ]); + } +} diff --git a/app/Repositories/Daemon/FileRepository.php b/app/Repositories/Daemon/FileRepository.php new file mode 100644 index 00000000..71182b11 --- /dev/null +++ b/app/Repositories/Daemon/FileRepository.php @@ -0,0 +1,126 @@ +. + * + * 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\Repositories\Daemon; + +use Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface; +use Webmozart\Assert\Assert; + +class FileRepository extends BaseRepository implements FileRepositoryInterface +{ + public function getFileStat($path) + { + Assert::stringNotEmpty($path, 'First argument passed to getStat must be a non-empty string, received %s.'); + + $file = pathinfo($path); + $file['dirname'] = in_array($file['dirname'], ['.', './', '/']) ? null : trim($file['dirname'], '/') . '/'; + + $response = $this->getHttpClient()->request('GET', sprintf( + '/server/file/stat/%s', + rawurlencode($file['dirname'] . $file['basename']) + )); + + return json_decode($response->getBody()); + } + + /** + * {@inheritdoc} + */ + public function getContent($path) + { + Assert::stringNotEmpty($path, 'First argument passed to getContent must be a non-empty string, received %s.'); + + $file = pathinfo($path); + $file['dirname'] = in_array($file['dirname'], ['.', './', '/']) ? null : trim($file['dirname'], '/') . '/'; + + $response = $this->getHttpClient()->request('GET', sprintf( + '/server/file/f/%s', + rawurlencode($file['dirname'] . $file['basename']) + )); + + return json_decode($response->getBody()); + } + + /** + * {@inheritdoc} + */ + public function putContent($path, $content) + { + Assert::stringNotEmpty($path, 'First argument passed to putContent must be a non-empty string, received %s.'); + Assert::string($content, 'Second argument passed to putContent must be a string, received %s.'); + + $file = pathinfo($path); + $file['dirname'] = in_array($file['dirname'], ['.', './', '/']) ? null : trim($file['dirname'], '/') . '/'; + + return $this->getHttpClient()->request('POST', '/server/file/save', [ + 'json' => [ + 'path' => rawurlencode($file['dirname'] . $file['basename']), + 'content' => $content, + ], + ]); + } + + /** + * {@inheritdoc} + */ + public function getDirectory($path) + { + Assert::string($path, 'First argument passed to getDirectory must be a string, received %s.'); + + $response = $this->getHttpClient()->request('GET', sprintf( + '/server/directory/%s', + rawurlencode($path) + )); + + $contents = json_decode($response->getBody()); + $files = []; + $folders = []; + + foreach ($contents as $value) { + if ($value->directory) { + array_push($folders, [ + 'entry' => $value->name, + 'directory' => trim($path, '/'), + 'size' => null, + 'date' => strtotime($value->modified), + 'mime' => $value->mime, + ]); + } elseif ($value->file) { + array_push($files, [ + 'entry' => $value->name, + 'directory' => trim($path, '/'), + 'extension' => pathinfo($value->name, PATHINFO_EXTENSION), + 'size' => human_readable($value->size), + 'date' => strtotime($value->modified), + 'mime' => $value->mime, + ]); + } + } + + return [ + 'files' => $files, + 'folders' => $folders, + ]; + } +} diff --git a/app/Repositories/Daemon/PowerRepository.php b/app/Repositories/Daemon/PowerRepository.php new file mode 100644 index 00000000..660db1e8 --- /dev/null +++ b/app/Repositories/Daemon/PowerRepository.php @@ -0,0 +1,55 @@ +. + * + * 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\Repositories\Daemon; + +use Webmozart\Assert\Assert; +use Pterodactyl\Contracts\Repository\Daemon\PowerRepositoryInterface; +use Pterodactyl\Exceptions\Repository\Daemon\InvalidPowerSignalException; + +class PowerRepository extends BaseRepository implements PowerRepositoryInterface +{ + /** + * {@inheritdoc} + */ + public function sendSignal($signal) + { + Assert::stringNotEmpty($signal, 'The first argument passed to sendSignal must be a non-empty string, received %s.'); + + switch ($signal) { + case self::SIGNAL_START: + case self::SIGNAL_STOP: + case self::SIGNAL_RESTART: + case self::SIGNAL_KILL: + return $this->getHttpClient()->request('PUT', '/server/power', [ + 'json' => [ + 'action' => $signal, + ], + ]); + break; + default: + throw new InvalidPowerSignalException('The signal ' . $signal . ' is not defined and could not be processed.'); + } + } +} diff --git a/app/Repositories/Daemon/ServerRepository.php b/app/Repositories/Daemon/ServerRepository.php index 8de958b5..a761510d 100644 --- a/app/Repositories/Daemon/ServerRepository.php +++ b/app/Repositories/Daemon/ServerRepository.php @@ -27,6 +27,7 @@ namespace Pterodactyl\Repositories\Daemon; use Pterodactyl\Services\Servers\EnvironmentService; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface as DatabaseServerRepositoryInterface; +use Webmozart\Assert\Assert; class ServerRepository extends BaseRepository implements ServerRepositoryInterface { @@ -35,8 +36,11 @@ class ServerRepository extends BaseRepository implements ServerRepositoryInterfa /** * {@inheritdoc} */ - public function create($id, $overrides = [], $start = false) + public function create($id, array $overrides = [], $start = false) { + Assert::numeric($id, 'First argument passed to create must be numeric, received %s.'); + Assert::boolean($start, 'Third argument passed to create must be boolean, received %s.'); + $repository = $this->app->make(DatabaseServerRepositoryInterface::class); $environment = $this->app->make(EnvironmentService::class); @@ -89,6 +93,8 @@ class ServerRepository extends BaseRepository implements ServerRepositoryInterfa */ public function setSubuserKey($key, array $permissions) { + Assert::stringNotEmpty($key, 'First argument passed to setSubuserKey must be a non-empty string, received %s.'); + return $this->getHttpClient()->request('PATCH', '/server', [ 'json' => [ 'keys' => [ @@ -113,6 +119,8 @@ class ServerRepository extends BaseRepository implements ServerRepositoryInterfa */ public function reinstall($data = null) { + Assert::nullOrIsArray($data, 'First argument passed to reinstall must be null or an array, received %s.'); + if (is_null($data)) { return $this->getHttpClient()->request('POST', '/server/reinstall'); } diff --git a/app/Repositories/Eloquent/SubuserRepository.php b/app/Repositories/Eloquent/SubuserRepository.php index 4909ecd1..92f4b186 100644 --- a/app/Repositories/Eloquent/SubuserRepository.php +++ b/app/Repositories/Eloquent/SubuserRepository.php @@ -39,6 +39,21 @@ class SubuserRepository extends EloquentRepository implements SubuserRepositoryI return Subuser::class; } + /** + * {@inheritdoc} + */ + public function getWithServer($id) + { + Assert::numeric($id, 'First argument passed to getWithServer must be numeric, received %s.'); + + $instance = $this->getBuilder()->with('server')->find($id, $this->getColumns()); + if (! $instance) { + throw new RecordNotFoundException; + } + + return $instance; + } + /** * {@inheritdoc} */ diff --git a/app/Repositories/Old/SubuserRepository.php b/app/Repositories/Old/SubuserRepository.php deleted file mode 100644 index 26c95107..00000000 --- a/app/Repositories/Old/SubuserRepository.php +++ /dev/null @@ -1,262 +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\Repositories; - -use DB; -use Validator; -use Pterodactyl\Models\User; -use Pterodactyl\Models\Server; -use Pterodactyl\Models\Subuser; -use Pterodactyl\Models\Permission; -use Pterodactyl\Services\UuidService; -use GuzzleHttp\Exception\TransferException; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Exceptions\DisplayValidationException; - -class SubuserRepository -{ - /** - * Core permissions required for every subuser on the daemon. - * Without this we cannot connect the websocket or get basic - * information about the server. - * - * @var array - */ - protected $coreDaemonPermissions = [ - 's:get', - 's:console', - ]; - - /** - * Creates a new subuser on the server. - * - * @param int $sid - * @param array $data - * @return \Pterodactyl\Models\Subuser - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function create($sid, array $data) - { - $server = Server::with('node')->findOrFail($sid); - - $validator = Validator::make($data, [ - 'permissions' => 'required|array', - 'email' => 'required|email', - ]); - - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->errors())); - } - - DB::beginTransaction(); - - try { - // Determine if this user exists or if we need to make them an account. - $user = User::where('email', $data['email'])->first(); - if (! $user) { - try { - $repo = new oldUserRepository; - $user = $repo->create([ - 'email' => $data['email'], - 'username' => str_random(8), - 'name_first' => 'Unassigned', - 'name_last' => 'Name', - 'root_admin' => false, - ]); - } catch (\Exception $ex) { - throw $ex; - } - } elseif ($server->owner_id === $user->id) { - throw new DisplayException('You cannot add the owner of a server as a subuser.'); - } elseif (Subuser::select('id')->where('user_id', $user->id)->where('server_id', $server->id)->first()) { - throw new DisplayException('A subuser with that email already exists for this server.'); - } - - $uuid = new UuidService; - $subuser = Subuser::create([ - 'user_id' => $user->id, - 'server_id' => $server->id, - 'daemonSecret' => (string) $uuid->generate('servers', 'uuid'), - ]); - - $perms = Permission::listPermissions(true); - $daemonPermissions = $this->coreDaemonPermissions; - - foreach ($data['permissions'] as $permission) { - if (array_key_exists($permission, $perms)) { - // Build the daemon permissions array for sending. - if (! is_null($perms[$permission])) { - array_push($daemonPermissions, $perms[$permission]); - } - - Permission::create([ - 'subuser_id' => $subuser->id, - 'permission' => $permission, - ]); - } - } - - // Contact Daemon - // We contact even if they don't have any daemon permissions to overwrite - // if they did have them previously. - - $server->node->guzzleClient([ - 'X-Access-Server' => $server->uuid, - 'X-Access-Token' => $server->node->daemonSecret, - ])->request('PATCH', '/server', [ - 'json' => [ - 'keys' => [ - $subuser->daemonSecret => $daemonPermissions, - ], - ], - ]); - - DB::commit(); - - return $subuser; - } catch (TransferException $ex) { - DB::rollBack(); - throw new DisplayException('There was an error attempting to connect to the daemon to add this user.', $ex); - } catch (\Exception $ex) { - DB::rollBack(); - throw $ex; - } - - return false; - } - - /** - * Revokes a users permissions on a server. - * - * @param int $id - * - * @throws \Pterodactyl\Exceptions\DisplayException - */ - public function delete($id) - { - $subuser = Subuser::with('server.node')->findOrFail($id); - $server = $subuser->server; - - DB::beginTransaction(); - - try { - $server->node->guzzleClient([ - 'X-Access-Server' => $server->uuid, - 'X-Access-Token' => $server->node->daemonSecret, - ])->request('PATCH', '/server', [ - 'json' => [ - 'keys' => [ - $subuser->daemonSecret => [], - ], - ], - ]); - - foreach ($subuser->permissions as &$permission) { - $permission->delete(); - } - $subuser->delete(); - DB::commit(); - } catch (TransferException $ex) { - DB::rollBack(); - throw new DisplayException('There was an error attempting to connect to the daemon to delete this subuser.', $ex); - } catch (\Exception $ex) { - DB::rollBack(); - throw $ex; - } - } - - /** - * Updates permissions for a given subuser. - * - * @param int $id - * @param array $data - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\DisplayValidationException - */ - public function update($id, array $data) - { - $validator = Validator::make($data, [ - 'permissions' => 'required|array', - 'user' => 'required|exists:users,id', - 'server' => 'required|exists:servers,id', - ]); - - if ($validator->fails()) { - throw new DisplayValidationException(json_encode($validator->all())); - } - - $subuser = Subuser::with('server.node')->findOrFail($id); - $server = $subuser->server; - - DB::beginTransaction(); - - try { - foreach ($subuser->permissions as &$permission) { - $permission->delete(); - } - - $perms = Permission::listPermissions(true); - $daemonPermissions = $this->coreDaemonPermissions; - - foreach ($data['permissions'] as $permission) { - if (array_key_exists($permission, $perms)) { - // Build the daemon permissions array for sending. - if (! is_null($perms[$permission])) { - array_push($daemonPermissions, $perms[$permission]); - } - Permission::create([ - 'subuser_id' => $subuser->id, - 'permission' => $permission, - ]); - } - } - - // Contact Daemon - // We contact even if they don't have any daemon permissions to overwrite - // if they did have them previously. - $server->node->guzzleClient([ - 'X-Access-Server' => $server->uuid, - 'X-Access-Token' => $server->node->daemonSecret, - ])->request('PATCH', '/server', [ - 'json' => [ - 'keys' => [ - $subuser->daemonSecret => $daemonPermissions, - ], - ], - ]); - - DB::commit(); - } catch (TransferException $ex) { - DB::rollBack(); - throw new DisplayException('There was an error attempting to connect to the daemon to update permissions.', $ex); - } catch (\Exception $ex) { - DB::rollBack(); - throw $ex; - } - } -} diff --git a/app/Repositories/old_Daemon/CommandRepository.php b/app/Repositories/old_Daemon/CommandRepository.php deleted file mode 100644 index ce12e12d..00000000 --- a/app/Repositories/old_Daemon/CommandRepository.php +++ /dev/null @@ -1,90 +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\Repositories\old_Daemon; - -use Pterodactyl\Models\User; -use Pterodactyl\Models\Server; -use GuzzleHttp\Exception\ConnectException; -use Pterodactyl\Exceptions\DisplayException; - -class CommandRepository -{ - /** - * The Eloquent Model associated with the requested server. - * - * @var \Pterodactyl\Models\Server - */ - protected $server; - - /** - * The Eloquent Model associated with the user to run the request as. - * - * @var \Pterodactyl\Models\User|null - */ - protected $user; - - /** - * Constuctor for repository. - * - * @param \Pterodactyl\Models\Server $server - * @param \Pterodactyl\Models\User|null $user - */ - public function __construct(Server $server, User $user = null) - { - $this->server = $server; - $this->user = $user; - } - - /** - * Sends a command to the daemon. - * - * @param string $command - * @return string - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \GuzzleHttp\Exception\RequestException - */ - public function send($command) - { - // We don't use the user's specific daemon secret here since we - // are assuming that a call to this function has been validated. - try { - $response = $this->server->guzzleClient($this->user)->request('POST', '/server/command', [ - 'http_errors' => false, - 'json' => [ - 'command' => $command, - ], - ]); - - if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) { - throw new DisplayException('Command sending responded with a non-200 error code (HTTP/' . $response->getStatusCode() . ').'); - } - - return $response->getBody(); - } catch (ConnectException $ex) { - throw $ex; - } - } -} diff --git a/app/Repositories/old_Daemon/FileRepository.php b/app/Repositories/old_Daemon/FileRepository.php deleted file mode 100644 index b4dc5f7f..00000000 --- a/app/Repositories/old_Daemon/FileRepository.php +++ /dev/null @@ -1,192 +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\Repositories\old_Daemon; - -use Exception; -use GuzzleHttp\Client; -use Pterodactyl\Models\Server; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Repositories\HelperRepository; - -class FileRepository -{ - /** - * The Eloquent Model associated with the requested server. - * - * @var \Pterodactyl\Models\Server - */ - protected $server; - - /** - * Constructor. - * - * @param string $uuid - */ - public function __construct($uuid) - { - $this->server = Server::byUuid($uuid); - } - - /** - * Get the contents of a requested file for the server. - * - * @param string $file - * @return array - * - * @throws \GuzzleHttp\Exception\RequestException - * @throws \Pterodactyl\Exceptions\DisplayException - */ - public function returnFileContents($file) - { - if (empty($file)) { - throw new Exception('Not all parameters were properly passed to the function.'); - } - - $file = (object) pathinfo($file); - $file->dirname = (in_array($file->dirname, ['.', './', '/'])) ? null : trim($file->dirname, '/') . '/'; - - $res = $this->server->guzzleClient()->request('GET', '/server/file/stat/' . rawurlencode($file->dirname . $file->basename)); - - $stat = json_decode($res->getBody()); - if ($res->getStatusCode() !== 200 || ! isset($stat->size)) { - throw new DisplayException('The daemon provided a non-200 error code on stat lookup: HTTP\\' . $res->getStatusCode()); - } - - if (! in_array($stat->mime, HelperRepository::editableFiles())) { - throw new DisplayException('You cannot edit that type of file (' . $stat->mime . ') through the panel.'); - } - - if ($stat->size > 5000000) { - throw new DisplayException('That file is too large to open in the browser, consider using a SFTP client.'); - } - - $res = $this->server->guzzleClient()->request('GET', '/server/file/f/' . rawurlencode($file->dirname . $file->basename)); - - $json = json_decode($res->getBody()); - if ($res->getStatusCode() !== 200 || ! isset($json->content)) { - throw new DisplayException('The daemon provided a non-200 error code: HTTP\\' . $res->getStatusCode()); - } - - return [ - 'file' => $json, - 'stat' => $stat, - ]; - } - - /** - * Save the contents of a requested file on the daemon. - * - * @param string $file - * @param string $content - * @return bool - * - * @throws \GuzzleHttp\Exception\RequestException - * @throws \Pterodactyl\Exceptions\DisplayException - */ - public function saveFileContents($file, $content) - { - if (empty($file)) { - throw new Exception('A valid file and path must be specified to save a file.'); - } - - $file = (object) pathinfo($file); - $file->dirname = (in_array($file->dirname, ['.', './', '/'])) ? null : trim($file->dirname, '/') . '/'; - - $res = $this->server->guzzleClient()->request('POST', '/server/file/save', [ - 'json' => [ - 'path' => rawurlencode($file->dirname . $file->basename), - 'content' => $content, - ], - ]); - - if ($res->getStatusCode() !== 204) { - throw new DisplayException('An error occured while attempting to save this file. ' . $res->getBody()); - } - - return true; - } - - /** - * Returns a listing of all files and folders within a specified directory on the daemon. - * - * @param string $directory - * @return object - * - * @throws \GuzzleHttp\Exception\RequestException - * @throws \Pterodactyl\Exceptions\DisplayException - */ - public function returnDirectoryListing($directory) - { - if (empty($directory)) { - throw new Exception('A valid directory must be specified in order to list its contents.'); - } - - try { - $res = $this->server->guzzleClient()->request('GET', '/server/directory/' . rawurlencode($directory)); - } catch (\GuzzleHttp\Exception\ClientException $ex) { - $json = json_decode($ex->getResponse()->getBody()); - - throw new DisplayException($json->error); - } catch (\GuzzleHttp\Exception\ServerException $ex) { - throw new DisplayException('A remote server error was encountered while attempting to display this directory.'); - } catch (\GuzzleHttp\Exception\ConnectException $ex) { - throw new DisplayException('A ConnectException was encountered: unable to contact daemon.'); - } catch (\Exception $ex) { - throw $ex; - } - - $json = json_decode($res->getBody()); - - // Iterate through results - $files = []; - $folders = []; - foreach ($json as &$value) { - if ($value->directory) { - // @TODO Handle Symlinks - $folders[] = [ - 'entry' => $value->name, - 'directory' => trim($directory, '/'), - 'size' => null, - 'date' => strtotime($value->modified), - 'mime' => $value->mime, - ]; - } elseif ($value->file) { - $files[] = [ - 'entry' => $value->name, - 'directory' => trim($directory, '/'), - 'extension' => pathinfo($value->name, PATHINFO_EXTENSION), - 'size' => HelperRepository::bytesToHuman($value->size), - 'date' => strtotime($value->modified), - 'mime' => $value->mime, - ]; - } - } - - return (object) [ - 'files' => $files, - 'folders' => $folders, - ]; - } -} diff --git a/app/Repositories/old_Daemon/PowerRepository.php b/app/Repositories/old_Daemon/PowerRepository.php deleted file mode 100644 index 7b941f12..00000000 --- a/app/Repositories/old_Daemon/PowerRepository.php +++ /dev/null @@ -1,120 +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\Repositories\old_Daemon; - -use Pterodactyl\Models\User; -use Pterodactyl\Models\Server; -use GuzzleHttp\Exception\ConnectException; -use Pterodactyl\Exceptions\DisplayException; - -class PowerRepository -{ - /** - * The Eloquent Model associated with the requested server. - * - * @var \Pterodactyl\Models\Server - */ - protected $server; - - /** - * The Eloquent Model associated with the user to run the request as. - * - * @var \Pterodactyl\Models\User|null - */ - protected $user; - - /** - * Constuctor for repository. - * - * @param \Pterodactyl\Models\Server $server - * @param \Pterodactyl\Models\User|null $user - */ - public function __construct(Server $server, User $user = null) - { - $this->server = $server; - $this->user = $user; - } - - /** - * Sends a power option to the daemon. - * - * @param string $action - * @return string - * - * @throws \GuzzleHttp\Exception\RequestException - * @throws \Pterodactyl\Exceptions\DisplayException - */ - public function do($action) - { - try { - $response = $this->server->guzzleClient($this->user)->request('PUT', '/server/power', [ - 'http_errors' => false, - 'json' => [ - 'action' => $action, - ], - ]); - - if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) { - throw new DisplayException('Power toggle endpoint responded with a non-200 error code (HTTP/' . $response->getStatusCode() . ').'); - } - - return $response->getBody(); - } catch (ConnectException $ex) { - throw $ex; - } - } - - /** - * Starts a server. - */ - public function start() - { - $this->do('start'); - } - - /** - * Stops a server. - */ - public function stop() - { - $this->do('stop'); - } - - /** - * Restarts a server. - */ - public function restart() - { - $this->do('restart'); - } - - /** - * Kills a server. - */ - public function kill() - { - $this->do('kill'); - } -} diff --git a/app/Services/Subusers/PermissionCreationService.php b/app/Services/Subusers/PermissionCreationService.php new file mode 100644 index 00000000..8414e6c7 --- /dev/null +++ b/app/Services/Subusers/PermissionCreationService.php @@ -0,0 +1,84 @@ +. + * + * 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\Subusers; + +use Pterodactyl\Models\Permission; +use Pterodactyl\Contracts\Repository\PermissionRepositoryInterface; + +class PermissionCreationService +{ + const CORE_DAEMON_PERMISSIONS = [ + 's:get', + 's:console', + ]; + + /** + * @var \Pterodactyl\Contracts\Repository\PermissionRepositoryInterface + */ + protected $repository; + + /** + * PermissionCreationService constructor. + * + * @param \Pterodactyl\Contracts\Repository\PermissionRepositoryInterface $repository + */ + public function __construct(PermissionRepositoryInterface $repository) + { + $this->repository = $repository; + } + + /** + * Assign permissions to a given subuser. + * + * @param int $subuser + * @param array $permissions + * @return array + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + */ + public function handle($subuser, array $permissions) + { + $permissionMappings = Permission::getPermissions(true); + $daemonPermissions = self::CORE_DAEMON_PERMISSIONS; + $insertPermissions = []; + + foreach ($permissions as $permission) { + if (array_key_exists($permission, $permissionMappings)) { + if (! is_null($permissionMappings[$permission])) { + array_push($daemonPermissions, $permissionMappings[$permission]); + } + + array_push($insertPermissions, [ + 'subuser_id' => $subuser, + 'permission' => $permission, + ]); + } + } + + $this->repository->insert($insertPermissions); + + return $daemonPermissions; + } +} diff --git a/app/Services/Subusers/SubuserCreationService.php b/app/Services/Subusers/SubuserCreationService.php index 8f6e8329..3de92e27 100644 --- a/app/Services/Subusers/SubuserCreationService.php +++ b/app/Services/Subusers/SubuserCreationService.php @@ -28,24 +28,17 @@ use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; use Illuminate\Log\Writer; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; -use Pterodactyl\Contracts\Repository\PermissionRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Exceptions\Service\Subuser\ServerSubuserExistsException; use Pterodactyl\Exceptions\Service\Subuser\UserIsServerOwnerException; -use Pterodactyl\Models\Permission; use Pterodactyl\Models\Server; use Pterodactyl\Services\Users\CreationService; class SubuserCreationService { - const CORE_DAEMON_PERMISSIONS = [ - 's:get', - 's:console', - ]; - const DAEMON_SECRET_BYTES = 18; /** @@ -59,9 +52,9 @@ class SubuserCreationService protected $daemonRepository; /** - * @var \Pterodactyl\Contracts\Repository\PermissionRepositoryInterface + * @var \Pterodactyl\Services\Subusers\PermissionCreationService */ - protected $permissionRepository; + protected $permissionService; /** * @var \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface @@ -92,7 +85,7 @@ class SubuserCreationService ConnectionInterface $connection, CreationService $userCreationService, DaemonServerRepositoryInterface $daemonRepository, - PermissionRepositoryInterface $permissionRepository, + PermissionCreationService $permissionService, ServerRepositoryInterface $serverRepository, SubuserRepositoryInterface $subuserRepository, UserRepositoryInterface $userRepository, @@ -100,7 +93,7 @@ class SubuserCreationService ) { $this->connection = $connection; $this->daemonRepository = $daemonRepository; - $this->permissionRepository = $permissionRepository; + $this->permissionService = $permissionService; $this->subuserRepository = $subuserRepository; $this->serverRepository = $serverRepository; $this->userRepository = $userRepository; @@ -154,21 +147,7 @@ class SubuserCreationService 'daemonSecret' => bin2hex(random_bytes(self::DAEMON_SECRET_BYTES)), ]); - $permissionMappings = Permission::getPermissions(true); - $daemonPermissions = self::CORE_DAEMON_PERMISSIONS; - - foreach ($permissions as $permission) { - if (array_key_exists($permission, $permissionMappings)) { - if (! is_null($permissionMappings[$permission])) { - array_push($daemonPermissions, $permissionMappings[$permission]); - } - - $this->permissionRepository->create([ - 'subuser_id' => $subuser->id, - 'permission' => $permission, - ]); - } - } + $daemonPermissions = $this->permissionService->handle($subuser->id, $permissions); try { $this->daemonRepository->setNode($server->node_id)->setAccessServer($server->uuid) @@ -178,9 +157,9 @@ class SubuserCreationService return $subuser; } catch (RequestException $exception) { $this->connection->rollBack(); - $response = $exception->getResponse(); $this->writer->warning($exception); + $response = $exception->getResponse(); throw new DisplayException(trans('admin/exceptions.daemon_connection_failed', [ 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), ])); diff --git a/app/Services/Subusers/SubuserDeletionService.php b/app/Services/Subusers/SubuserDeletionService.php index d06f57bd..2cbc168b 100644 --- a/app/Services/Subusers/SubuserDeletionService.php +++ b/app/Services/Subusers/SubuserDeletionService.php @@ -84,7 +84,7 @@ class SubuserDeletionService */ public function handle($subuser) { - $subuser = $this->repository->getWithServerAndPermissions($subuser); + $subuser = $this->repository->getWithServer($subuser); $this->connection->beginTransaction(); $response = $this->repository->delete($subuser->id); @@ -97,9 +97,9 @@ class SubuserDeletionService return $response; } catch (RequestException $exception) { $this->connection->rollBack(); - $response = $exception->getResponse(); $this->writer->warning($exception); + $response = $exception->getResponse(); throw new DisplayException(trans('admin/exceptions.daemon_connection_failed', [ 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), ])); diff --git a/app/Services/Subusers/SubuserUpdateService.php b/app/Services/Subusers/SubuserUpdateService.php new file mode 100644 index 00000000..f69870cd --- /dev/null +++ b/app/Services/Subusers/SubuserUpdateService.php @@ -0,0 +1,125 @@ +. + * + * 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\Subusers; + +use GuzzleHttp\Exception\RequestException; +use Illuminate\Database\ConnectionInterface; +use Illuminate\Log\Writer; +use Pterodactyl\Contracts\Repository\PermissionRepositoryInterface; +use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; +use Pterodactyl\Exceptions\DisplayException; + +class SubuserUpdateService +{ + /** + * @var \Illuminate\Database\ConnectionInterface + */ + protected $connection; + + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface + */ + protected $daemonRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\PermissionRepositoryInterface + */ + protected $permissionRepository; + + /** + * @var \Pterodactyl\Services\Subusers\PermissionCreationService + */ + protected $permissionService; + + /** + * @var \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface + */ + protected $repository; + + /** + * @var \Illuminate\Log\Writer + */ + protected $writer; + + /** + * SubuserUpdateService constructor. + * + * @param \Illuminate\Database\ConnectionInterface $connection + * @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonRepository + * @param \Pterodactyl\Services\Subusers\PermissionCreationService $permissionService + * @param \Pterodactyl\Contracts\Repository\PermissionRepositoryInterface $permissionRepository + * @param \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface $repository + * @param \Illuminate\Log\Writer $writer + */ + public function __construct( + ConnectionInterface $connection, + DaemonServerRepositoryInterface $daemonRepository, + PermissionCreationService $permissionService, + PermissionRepositoryInterface $permissionRepository, + SubuserRepositoryInterface $repository, + Writer $writer + ) { + $this->connection = $connection; + $this->daemonRepository = $daemonRepository; + $this->permissionRepository = $permissionRepository; + $this->permissionService = $permissionService; + $this->repository = $repository; + $this->writer = $writer; + } + + /** + * Update permissions for a given subuser. + * + * @param int $subuser + * @param array $permissions + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function handle($subuser, array $permissions) + { + $subuser = $this->repository->getWithServer($subuser); + + $this->connection->beginTransaction(); + $this->permissionRepository->deleteWhere([['subuser_id', '=', $subuser->id]]); + $daemonPermissions = $this->permissionService->handle($subuser->id, $permissions); + + try { + $this->daemonRepository->setNode($subuser->server->node_id)->setAccessServer($subuser->server->uuid) + ->setSubuserKey($subuser->daemonSecret, $daemonPermissions); + $this->connection->commit(); + } catch (RequestException $exception) { + $this->connection->rollBack(); + $this->writer->warning($exception); + + $response = $exception->getResponse(); + throw new DisplayException(trans('admin/exceptions.daemon_connection_failed', [ + 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), + ])); + } + } +} diff --git a/app/helpers.php b/app/helpers.php index 763886f0..0b51c7f0 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -32,6 +32,16 @@ if (! function_exists('human_readable')) { */ function human_readable($path, $precision = 2) { + if (is_numeric($path)) { + $i = 0; + while (($path / 1024) > 0.9) { + $path = $path / 1024; + ++$i; + } + + return round($path, $precision) . ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'][$i]; + } + return app('file')->humanReadableSize($path, $precision); } } diff --git a/config/pterodactyl.php b/config/pterodactyl.php index a21283d1..7c66f322 100644 --- a/config/pterodactyl.php +++ b/config/pterodactyl.php @@ -143,6 +143,7 @@ return [ | This array includes the MIME filetypes that can be edited via the web. */ 'files' => [ + 'max_edit_size' => env('PTERODACTYL_FILES_MAX_EDIT_SIZE', 50000), 'editable' => [ 'application/json', 'application/javascript', diff --git a/tests/Unit/Services/Subusers/PermissionCreationServiceTest.php b/tests/Unit/Services/Subusers/PermissionCreationServiceTest.php new file mode 100644 index 00000000..85171448 --- /dev/null +++ b/tests/Unit/Services/Subusers/PermissionCreationServiceTest.php @@ -0,0 +1,72 @@ +. + * + * 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\Subusers; + +use Mockery as m; +use Pterodactyl\Contracts\Repository\PermissionRepositoryInterface; +use Pterodactyl\Services\Subusers\PermissionCreationService; +use Tests\TestCase; + +class PermissionCreationServiceTest extends TestCase +{ + /** + * @var \Pterodactyl\Contracts\Repository\PermissionRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Subusers\PermissionCreationService + */ + protected $service; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->repository = m::mock(PermissionRepositoryInterface::class); + $this->service = new PermissionCreationService($this->repository); + } + + /** + * Test that permissions can be assigned correctly. + */ + public function testPermissionsAreAssignedCorrectly() + { + $permissions = ['reset-sftp', 'view-sftp']; + + $this->repository->shouldReceive('insert')->with([ + ['subuser_id' => 1, 'permission' => 'reset-sftp'], + ['subuser_id' => 1, 'permission' => 'view-sftp'], + ]); + + $response = $this->service->handle(1, $permissions); + + $this->assertNotEmpty($response); + $this->assertEquals(['s:get', 's:console', 's:set-password'], $response); + } +} diff --git a/tests/Unit/Services/Subusers/SubuserCreationServiceTest.php b/tests/Unit/Services/Subusers/SubuserCreationServiceTest.php index a7492cb3..bf242cab 100644 --- a/tests/Unit/Services/Subusers/SubuserCreationServiceTest.php +++ b/tests/Unit/Services/Subusers/SubuserCreationServiceTest.php @@ -29,7 +29,6 @@ use Illuminate\Log\Writer; use Mockery as m; use phpmock\phpunit\PHPMock; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; -use Pterodactyl\Contracts\Repository\PermissionRepositoryInterface; use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Exceptions\DisplayException; @@ -38,6 +37,7 @@ use Pterodactyl\Exceptions\Service\Subuser\UserIsServerOwnerException; use Pterodactyl\Models\Server; use Pterodactyl\Models\Subuser; use Pterodactyl\Models\User; +use Pterodactyl\Services\Subusers\PermissionCreationService; use Pterodactyl\Services\Subusers\SubuserCreationService; use Pterodactyl\Services\Users\CreationService; use Tests\TestCase; @@ -58,14 +58,9 @@ class SubuserCreationServiceTest extends TestCase protected $daemonRepository; /** - * @var \Pterodactyl\Models\Permission + * @var \Pterodactyl\Services\Subusers\PermissionCreationService */ - protected $permission; - - /** - * @var \Pterodactyl\Contracts\Repository\PermissionRepositoryInterface - */ - protected $permissionRepository; + protected $permissionService; /** * @var \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface @@ -108,8 +103,7 @@ class SubuserCreationServiceTest extends TestCase $this->connection = m::mock(ConnectionInterface::class); $this->daemonRepository = m::mock(DaemonServerRepositoryInterface::class); - $this->permission = m::mock('overload:Pterodactyl\Models\Permission'); - $this->permissionRepository = m::mock(PermissionRepositoryInterface::class); + $this->permissionService = m::mock(PermissionCreationService::class); $this->subuserRepository = m::mock(SubuserRepositoryInterface::class); $this->serverRepository = m::mock(ServerRepositoryInterface::class); $this->userCreationService = m::mock(CreationService::class); @@ -120,7 +114,7 @@ class SubuserCreationServiceTest extends TestCase $this->connection, $this->userCreationService, $this->daemonRepository, - $this->permissionRepository, + $this->permissionService, $this->serverRepository, $this->subuserRepository, $this->userRepository, @@ -154,14 +148,8 @@ class SubuserCreationServiceTest extends TestCase 'daemonSecret' => 'bin2hex', ])->once()->andReturn($subuser); - $this->permission->shouldReceive('getPermissions')->with(true)->once() - ->andReturn($permissions); - - foreach(array_keys($permissions) as $permission) { - $this->permissionRepository->shouldReceive('create') - ->with(['subuser_id' => $subuser->id, 'permission' => $permission]) - ->once()->andReturnNull(); - } + $this->permissionService->shouldReceive('handle')->with($subuser->id, array_keys($permissions))->once() + ->andReturn(['s:get', 's:console', 'test:1']); $this->daemonRepository->shouldReceive('setNode')->with($server->node_id)->once()->andReturnSelf() ->shouldReceive('setAccessServer')->with($server->uuid)->once()->andReturnSelf() @@ -179,7 +167,7 @@ class SubuserCreationServiceTest extends TestCase */ public function testExistingUserCanBeAddedAsASubuser() { - $permissions = ['test-1' => 'test:1', 'test-2' => null]; + $permissions = ['view-sftp', 'reset-sftp']; $server = factory(Server::class)->make(); $user = factory(User::class)->make(); $subuser = factory(Subuser::class)->make(['user_id' => $user->id, 'server_id' => $server->id]); @@ -197,21 +185,15 @@ class SubuserCreationServiceTest extends TestCase 'daemonSecret' => 'bin2hex', ])->once()->andReturn($subuser); - $this->permission->shouldReceive('getPermissions')->with(true)->once() - ->andReturn($permissions); - - foreach(array_keys($permissions) as $permission) { - $this->permissionRepository->shouldReceive('create') - ->with(['subuser_id' => $subuser->id, 'permission' => $permission]) - ->once()->andReturnNull(); - } + $this->permissionService->shouldReceive('handle')->with($subuser->id, $permissions)->once() + ->andReturn(['s:get', 's:console', 's:set-password']); $this->daemonRepository->shouldReceive('setNode')->with($server->node_id)->once()->andReturnSelf() ->shouldReceive('setAccessServer')->with($server->uuid)->once()->andReturnSelf() - ->shouldReceive('setSubuserKey')->with($subuser->daemonSecret, ['s:get', 's:console', 'test:1'])->once()->andReturnSelf(); + ->shouldReceive('setSubuserKey')->with($subuser->daemonSecret, ['s:get', 's:console', 's:set-password'])->once()->andReturnSelf(); $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); - $response = $this->service->handle($server, $user->email, array_keys($permissions)); + $response = $this->service->handle($server, $user->email, $permissions); $this->assertInstanceOf(Subuser::class, $response); $this->assertSame($subuser, $response); diff --git a/tests/Unit/Services/Subusers/SubuserDeletionServiceTest.php b/tests/Unit/Services/Subusers/SubuserDeletionServiceTest.php index e465eb16..23d0155a 100644 --- a/tests/Unit/Services/Subusers/SubuserDeletionServiceTest.php +++ b/tests/Unit/Services/Subusers/SubuserDeletionServiceTest.php @@ -97,7 +97,7 @@ class SubuserDeletionServiceTest extends TestCase $subuser = factory(Subuser::class)->make(); $subuser->server = factory(Server::class)->make(); - $this->repository->shouldReceive('getWithServerAndPermissions')->with($subuser->id)->once()->andReturn($subuser); + $this->repository->shouldReceive('getWithServer')->with($subuser->id)->once()->andReturn($subuser); $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); $this->repository->shouldReceive('delete')->with($subuser->id)->once()->andReturn(1); @@ -119,7 +119,7 @@ class SubuserDeletionServiceTest extends TestCase $subuser = factory(Subuser::class)->make(); $subuser->server = factory(Server::class)->make(); - $this->repository->shouldReceive('getWithServerAndPermissions')->with($subuser->id)->once()->andReturn($subuser); + $this->repository->shouldReceive('getWithServer')->with($subuser->id)->once()->andReturn($subuser); $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); $this->repository->shouldReceive('delete')->with($subuser->id)->once()->andReturn(1); diff --git a/tests/Unit/Services/Subusers/SubuserUpdateServiceTest.php b/tests/Unit/Services/Subusers/SubuserUpdateServiceTest.php new file mode 100644 index 00000000..f4742f9c --- /dev/null +++ b/tests/Unit/Services/Subusers/SubuserUpdateServiceTest.php @@ -0,0 +1,158 @@ +. + * + * 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\Subusers; + +use GuzzleHttp\Exception\RequestException; +use Illuminate\Database\ConnectionInterface; +use Illuminate\Log\Writer; +use Mockery as m; +use Pterodactyl\Contracts\Repository\PermissionRepositoryInterface; +use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Models\Server; +use Pterodactyl\Models\Subuser; +use Pterodactyl\Services\Subusers\PermissionCreationService; +use Pterodactyl\Services\Subusers\SubuserUpdateService; +use Tests\TestCase; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; + +class SubuserUpdateServiceTest extends TestCase +{ + /** + * @var \Illuminate\Database\ConnectionInterface + */ + protected $connection; + + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface + */ + protected $daemonRepository; + + /** + * @var \GuzzleHttp\Exception\RequestException + */ + protected $exception; + + /** + * @var \Pterodactyl\Contracts\Repository\PermissionRepositoryInterface + */ + protected $permissionRepository; + + /** + * @var \Pterodactyl\Services\Subusers\PermissionCreationService + */ + protected $permissionService; + + /** + * @var \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Subusers\SubuserUpdateService + */ + protected $service; + + /** + * @var \Illuminate\Log\Writer + */ + protected $writer; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->connection = m::mock(ConnectionInterface::class); + $this->daemonRepository = m::mock(DaemonServerRepositoryInterface::class); + $this->exception = m::mock(RequestException::class); + $this->permissionRepository = m::mock(PermissionRepositoryInterface::class); + $this->permissionService = m::mock(PermissionCreationService::class); + $this->repository = m::mock(SubuserRepositoryInterface::class); + $this->writer = m::mock(Writer::class); + + $this->service = new SubuserUpdateService( + $this->connection, + $this->daemonRepository, + $this->permissionService, + $this->permissionRepository, + $this->repository, + $this->writer + ); + } + + /** + * Test that permissions are updated in the database. + */ + public function testPermissionsAreUpdated() + { + $subuser = factory(Subuser::class)->make(); + $subuser->server = factory(Server::class)->make(); + + $this->repository->shouldReceive('getWithServer')->with($subuser->id)->once()->andReturn($subuser); + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->permissionRepository->shouldReceive('deleteWhere')->with([ + ['subuser_id', '=', $subuser->id], + ])->once()->andReturnNull(); + $this->permissionService->shouldReceive('handle')->with($subuser->id, ['some-permission'])->once()->andReturn(['test:1', 'test:2']); + + $this->daemonRepository->shouldReceive('setNode')->with($subuser->server->node_id)->once()->andReturnSelf() + ->shouldReceive('setAccessServer')->with($subuser->server->uuid)->once()->andReturnSelf() + ->shouldReceive('setSubuserKey')->with($subuser->daemonSecret, ['test:1', 'test:2'])->once()->andReturnNull(); + + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + + $this->service->handle($subuser->id, ['some-permission']); + } + + /** + * Test that an exception is thrown if the daemon connection fails. + */ + public function testExceptionIsThrownIfDaemonConnectionFails() + { + $subuser = factory(Subuser::class)->make(); + $subuser->server = factory(Server::class)->make(); + + $this->repository->shouldReceive('getWithServer')->with($subuser->id)->once()->andReturn($subuser); + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->permissionRepository->shouldReceive('deleteWhere')->with([ + ['subuser_id', '=', $subuser->id], + ])->once()->andReturnNull(); + $this->permissionService->shouldReceive('handle')->with($subuser->id, [])->once()->andReturn([]); + + $this->daemonRepository->shouldReceive('setNode')->once()->andThrow($this->exception); + $this->connection->shouldReceive('rollBack')->withNoArgs()->once()->andReturnNull(); + $this->writer->shouldReceive('warning')->with($this->exception)->once()->andReturnNull(); + $this->exception->shouldReceive('getResponse')->withNoArgs()->once()->andReturnNull(); + + try { + $this->service->handle($subuser->id, []); + } catch (DisplayException $exception) { + $this->assertEquals(trans('admin/exceptions.daemon_connection_failed', ['code' => 'E_CONN_REFUSED']), $exception->getMessage()); + } + } +} From f451e4dc47215a0356490b5aac2e7e3b3b270f65 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 26 Aug 2017 19:58:24 -0500 Subject: [PATCH 67/99] Begin unit tests for repositories --- .../DatabaseHostRepositoryInterface.php | 11 -- .../DuplicateDatabaseNameException.php | 31 ++++ .../Controllers/Admin/LocationController.php | 2 + .../Controllers/Admin/ServersController.php | 4 + app/Providers/RepositoryServiceProvider.php | 11 +- .../Eloquent/DatabaseHostRepository.php | 23 +-- .../Eloquent/DatabaseRepository.php | 10 +- .../Eloquent/LocationRepository.php | 1 + app/Services/Database/DatabaseHostService.php | 18 +- database/factories/ModelFactory.php | 22 +++ resources/lang/en/admin/exceptions.php | 3 + .../Eloquent/AllocationRepositoryTest.php | 87 +++++++++ .../Eloquent/ApiKeyRepositoryTest.php | 65 +++++++ .../Eloquent/ApiPermissionRepositoryTest.php | 65 +++++++ .../Eloquent/DatabaseHostRepositoryTest.php | 103 +++++++++++ .../Eloquent/DatabaseRepositoryTest.php | 172 ++++++++++++++++++ .../Eloquent/LocationRepositoryTest.php | 115 ++++++++++++ .../Database/DatabaseHostServiceTest.php | 34 +++- 18 files changed, 734 insertions(+), 43 deletions(-) create mode 100644 app/Exceptions/Repository/DuplicateDatabaseNameException.php create mode 100644 tests/Unit/Repositories/Eloquent/AllocationRepositoryTest.php create mode 100644 tests/Unit/Repositories/Eloquent/ApiKeyRepositoryTest.php create mode 100644 tests/Unit/Repositories/Eloquent/ApiPermissionRepositoryTest.php create mode 100644 tests/Unit/Repositories/Eloquent/DatabaseHostRepositoryTest.php create mode 100644 tests/Unit/Repositories/Eloquent/DatabaseRepositoryTest.php create mode 100644 tests/Unit/Repositories/Eloquent/LocationRepositoryTest.php diff --git a/app/Contracts/Repository/DatabaseHostRepositoryInterface.php b/app/Contracts/Repository/DatabaseHostRepositoryInterface.php index 59ff2405..3a677fcb 100644 --- a/app/Contracts/Repository/DatabaseHostRepositoryInterface.php +++ b/app/Contracts/Repository/DatabaseHostRepositoryInterface.php @@ -42,15 +42,4 @@ interface DatabaseHostRepositoryInterface extends RepositoryInterface * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function getWithServers($id); - - /** - * Delete a database host from the DB if there are no databases using it. - * - * @param int $id - * @return bool|null - * - * @throws \Pterodactyl\Exceptions\DisplayException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException - */ - public function deleteIfNoDatabases($id); } diff --git a/app/Exceptions/Repository/DuplicateDatabaseNameException.php b/app/Exceptions/Repository/DuplicateDatabaseNameException.php new file mode 100644 index 00000000..2308f407 --- /dev/null +++ b/app/Exceptions/Repository/DuplicateDatabaseNameException.php @@ -0,0 +1,31 @@ +. + * + * 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\Exceptions\Repository; + +use Pterodactyl\Exceptions\DisplayException; + +class DuplicateDatabaseNameException extends DisplayException +{ +} diff --git a/app/Http/Controllers/Admin/LocationController.php b/app/Http/Controllers/Admin/LocationController.php index f3e12196..c7a78baf 100644 --- a/app/Http/Controllers/Admin/LocationController.php +++ b/app/Http/Controllers/Admin/LocationController.php @@ -83,6 +83,8 @@ class LocationController extends Controller * * @param int $id * @return \Illuminate\View\View + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function view($id) { diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php index efd74b2e..c693bcb1 100644 --- a/app/Http/Controllers/Admin/ServersController.php +++ b/app/Http/Controllers/Admin/ServersController.php @@ -434,6 +434,7 @@ class ServersController extends Controller * * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function toggleInstall(Server $server) { @@ -493,6 +494,7 @@ class ServersController extends Controller * * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function manageSuspension(Request $request, Server $server) { @@ -533,6 +535,7 @@ class ServersController extends Controller * @return \Illuminate\Http\RedirectResponse * * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function delete(Request $request, Server $server) { @@ -551,6 +554,7 @@ class ServersController extends Controller * * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function saveStartup(Request $request, Server $server) { diff --git a/app/Providers/RepositoryServiceProvider.php b/app/Providers/RepositoryServiceProvider.php index b1be571e..178c9102 100644 --- a/app/Providers/RepositoryServiceProvider.php +++ b/app/Providers/RepositoryServiceProvider.php @@ -25,7 +25,13 @@ namespace Pterodactyl\Providers; use Illuminate\Support\ServiceProvider; +use Pterodactyl\Contracts\Repository\Daemon\CommandRepositoryInterface; +use Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface; +use Pterodactyl\Contracts\Repository\Daemon\PowerRepositoryInterface; use Pterodactyl\Contracts\Repository\PackRepositoryInterface; +use Pterodactyl\Repositories\Daemon\CommandRepository; +use Pterodactyl\Repositories\Daemon\FileRepository; +use Pterodactyl\Repositories\Daemon\PowerRepository; use Pterodactyl\Repositories\Eloquent\NodeRepository; use Pterodactyl\Repositories\Eloquent\PackRepository; use Pterodactyl\Repositories\Eloquent\UserRepository; @@ -71,8 +77,8 @@ class RepositoryServiceProvider extends ServiceProvider $this->app->bind(AllocationRepositoryInterface::class, AllocationRepository::class); $this->app->bind(ApiKeyRepositoryInterface::class, ApiKeyRepository::class); $this->app->bind(ApiPermissionRepositoryInterface::class, ApiPermissionRepository::class); - $this->app->bind(DatabaseHostRepositoryInterface::class, DatabaseHostRepository::class); $this->app->bind(DatabaseRepositoryInterface::class, DatabaseRepository::class); + $this->app->bind(DatabaseHostRepositoryInterface::class, DatabaseHostRepository::class); $this->app->bind(LocationRepositoryInterface::class, LocationRepository::class); $this->app->bind(NodeRepositoryInterface::class, NodeRepository::class); $this->app->bind(OptionVariableRepositoryInterface::class, OptionVariableRepository::class); @@ -86,6 +92,9 @@ class RepositoryServiceProvider extends ServiceProvider // Daemon Repositories $this->app->bind(ConfigurationRepositoryInterface::class, ConfigurationRepository::class); + $this->app->bind(CommandRepositoryInterface::class, CommandRepository::class); $this->app->bind(DaemonServerRepositoryInterface::class, DaemonServerRepository::class); + $this->app->bind(FileRepositoryInterface::class, FileRepository::class); + $this->app->bind(PowerRepositoryInterface::class, PowerRepository::class); } } diff --git a/app/Repositories/Eloquent/DatabaseHostRepository.php b/app/Repositories/Eloquent/DatabaseHostRepository.php index 5aff2674..3304c308 100644 --- a/app/Repositories/Eloquent/DatabaseHostRepository.php +++ b/app/Repositories/Eloquent/DatabaseHostRepository.php @@ -26,7 +26,6 @@ namespace Pterodactyl\Repositories\Eloquent; use Webmozart\Assert\Assert; use Pterodactyl\Models\DatabaseHost; -use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; @@ -48,6 +47,9 @@ class DatabaseHostRepository extends EloquentRepository implements DatabaseHostR return $this->getBuilder()->withCount('databases')->with('node')->get(); } + /** + * {@inheritdoc} + */ public function getWithServers($id) { Assert::numeric($id, 'First argument passed to getWithServers must be numeric, recieved %s.'); @@ -59,23 +61,4 @@ class DatabaseHostRepository extends EloquentRepository implements DatabaseHostR return $instance; } - - /** - * {@inheritdoc} - */ - public function deleteIfNoDatabases($id) - { - Assert::numeric($id, 'First argument passed to deleteIfNoDatabases must be numeric, recieved %s.'); - - $instance = $this->getBuilder()->withCount('databases')->find($id); - if (! $instance) { - throw new RecordNotFoundException(); - } - - if ($instance->databases_count > 0) { - throw new DisplayException('Cannot delete a database host that has active databases attached to it.'); - } - - return $instance->delete(); - } } diff --git a/app/Repositories/Eloquent/DatabaseRepository.php b/app/Repositories/Eloquent/DatabaseRepository.php index e274f193..12000680 100644 --- a/app/Repositories/Eloquent/DatabaseRepository.php +++ b/app/Repositories/Eloquent/DatabaseRepository.php @@ -24,10 +24,10 @@ namespace Pterodactyl\Repositories\Eloquent; +use Pterodactyl\Exceptions\Repository\DuplicateDatabaseNameException; use Pterodactyl\Models\Database; use Illuminate\Foundation\Application; use Illuminate\Database\DatabaseManager; -use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; class DatabaseRepository extends EloquentRepository implements DatabaseRepositoryInterface @@ -67,13 +67,13 @@ class DatabaseRepository extends EloquentRepository implements DatabaseRepositor public function createIfNotExists(array $data) { $instance = $this->getBuilder()->where([ - ['server_id', $data['server_id']], - ['database_host_id', $data['database_host_id']], - ['database', $data['database']], + ['server_id', '=', array_get($data, 'server_id')], + ['database_host_id', '=', array_get($data, 'database_host_id')], + ['database', '=', array_get($data, 'database')], ])->count(); if ($instance > 0) { - throw new DisplayException('A database with those details already exists for the specified server.'); + throw new DuplicateDatabaseNameException('A database with those details already exists for the specified server.'); } return $this->create($data); diff --git a/app/Repositories/Eloquent/LocationRepository.php b/app/Repositories/Eloquent/LocationRepository.php index 70bece64..229e1e11 100644 --- a/app/Repositories/Eloquent/LocationRepository.php +++ b/app/Repositories/Eloquent/LocationRepository.php @@ -49,6 +49,7 @@ class LocationRepository extends EloquentRepository implements LocationRepositor /** * {@inheritdoc} + * @todo remove this, do logic in service */ public function deleteIfNoNodes($id) { diff --git a/app/Services/Database/DatabaseHostService.php b/app/Services/Database/DatabaseHostService.php index 654ce612..2ad11ecc 100644 --- a/app/Services/Database/DatabaseHostService.php +++ b/app/Services/Database/DatabaseHostService.php @@ -26,6 +26,8 @@ namespace Pterodactyl\Services\Database; use Illuminate\Database\DatabaseManager; use Illuminate\Contracts\Encryption\Encrypter; +use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; +use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Extensions\DynamicDatabaseConnection; use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; @@ -36,6 +38,11 @@ class DatabaseHostService */ protected $database; + /** + * @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface + */ + protected $databaseRepository; + /** * @var \Pterodactyl\Extensions\DynamicDatabaseConnection */ @@ -55,17 +62,20 @@ class DatabaseHostService * DatabaseHostService constructor. * * @param \Illuminate\Database\DatabaseManager $database + * @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $databaseRepository * @param \Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface $repository * @param \Pterodactyl\Extensions\DynamicDatabaseConnection $dynamic * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter */ public function __construct( DatabaseManager $database, + DatabaseRepositoryInterface $databaseRepository, DatabaseHostRepositoryInterface $repository, DynamicDatabaseConnection $dynamic, Encrypter $encrypter ) { $this->database = $database; + $this->databaseRepository = $databaseRepository; $this->dynamic = $dynamic; $this->encrypter = $encrypter; $this->repository = $repository; @@ -111,6 +121,7 @@ class DatabaseHostService * @return mixed * * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function update($id, array $data) { @@ -142,6 +153,11 @@ class DatabaseHostService */ public function delete($id) { - return $this->repository->deleteIfNoDatabases($id); + $count = $this->databaseRepository->findCountWhere([['database_host_id', '=', $id]]); + if ($count > 0) { + throw new DisplayException(trans('admin/exceptions.databases.delete_has_databases')); + } + + return $this->repository->delete($id); } } diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index b8b2f123..8726b2f4 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -91,6 +91,7 @@ $factory->define(Pterodactyl\Models\Node::class, function (Faker\Generator $fake $factory->define(Pterodactyl\Models\Service::class, function (Faker\Generator $faker) { return [ + 'id' => $faker->unique()->randomNumber(), 'author' => $faker->unique()->uuid, 'name' => $faker->word, 'description' => null, @@ -155,3 +156,24 @@ $factory->define(Pterodactyl\Models\Subuser::class, function (Faker\Generator $f 'daemonSecret' => $faker->unique()->uuid, ]; }); + +$factory->define(Pterodactyl\Models\Allocation::class, function (Faker\Generator $faker) { + return [ + 'id' => $faker->unique()->randomNumber(), + 'node_id' => $faker->randomNumber(), + 'ip' => $faker->ipv4, + 'port' => $faker->randomNumber(5), + ]; +}); + +$factory->define(Pterodactyl\Models\DatabaseHost::class, function (Faker\Generator $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(), + ]; +}); diff --git a/resources/lang/en/admin/exceptions.php b/resources/lang/en/admin/exceptions.php index 21bd812b..97c53775 100644 --- a/resources/lang/en/admin/exceptions.php +++ b/resources/lang/en/admin/exceptions.php @@ -58,4 +58,7 @@ return [ 'user_is_owner' => 'You cannot add the server owner as a subuser for this server.', 'subuser_exists' => 'A user with that email address is already assigned as a subuser for this server.', ], + 'databases' => [ + 'delete_has_databases' => 'Cannot delete a database host server that has active databases linked to it.', + ], ]; diff --git a/tests/Unit/Repositories/Eloquent/AllocationRepositoryTest.php b/tests/Unit/Repositories/Eloquent/AllocationRepositoryTest.php new file mode 100644 index 00000000..0c7b6f4c --- /dev/null +++ b/tests/Unit/Repositories/Eloquent/AllocationRepositoryTest.php @@ -0,0 +1,87 @@ +. + * + * 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\Repositories\Eloquent; + +use Illuminate\Database\Eloquent\Builder; +use Mockery as m; +use Pterodactyl\Models\Allocation; +use Pterodactyl\Repositories\Eloquent\AllocationRepository; +use Tests\TestCase; + +class AllocationRepositoryTest extends TestCase +{ + /** + * @var \Illuminate\Database\Eloquent\Builder + */ + protected $builder; + + /** + * @var \Pterodactyl\Repositories\Eloquent\AllocationRepository + */ + protected $repository; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->builder = m::mock(Builder::class); + $this->repository = m::mock(AllocationRepository::class)->makePartial(); + + $this->repository->shouldReceive('getBuilder')->withNoArgs()->andReturn($this->builder); + } + + /** + * Test that we are returning the correct model. + */ + public function testCorrectModelIsAssigned() + { + $this->assertEquals(Allocation::class, $this->repository->model()); + } + + /** + * Test that allocations can be assigned to a server correctly. + */ + public function testAllocationsAreAssignedToAServer() + { + $this->builder->shouldReceive('whereIn')->with('id', [1, 2])->once()->andReturnSelf() + ->shouldReceive('update')->with(['server_id' => 10])->once()->andReturn(true); + + $this->assertTrue($this->repository->assignAllocationsToServer(10, [1, 2])); + } + + /** + * Test that allocations with a node relationship are returned. + */ + public function testAllocationsForANodeAreReturned() + { + $this->builder->shouldReceive('where')->with('node_id', 1)->once()->andReturnSelf() + ->shouldReceive('get')->once()->andReturn(factory(Allocation::class)->make()); + + $this->assertInstanceOf(Allocation::class, $this->repository->getAllocationsForNode(1)); + } +} diff --git a/tests/Unit/Repositories/Eloquent/ApiKeyRepositoryTest.php b/tests/Unit/Repositories/Eloquent/ApiKeyRepositoryTest.php new file mode 100644 index 00000000..eadfbde4 --- /dev/null +++ b/tests/Unit/Repositories/Eloquent/ApiKeyRepositoryTest.php @@ -0,0 +1,65 @@ +. + * + * 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\Repositories\Eloquent; + +use Illuminate\Database\Eloquent\Builder; +use Mockery as m; +use Pterodactyl\Models\APIKey; +use Pterodactyl\Repositories\Eloquent\ApiKeyRepository; +use Tests\TestCase; + +class ApiKeyRepositoryTest extends TestCase +{ + /** + * @var \Illuminate\Database\Eloquent\Builder + */ + protected $builder; + + /** + * @var \Pterodactyl\Repositories\Eloquent\ApiKeyRepository + */ + protected $repository; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->builder = m::mock(Builder::class); + $this->repository = m::mock(ApiKeyRepository::class)->makePartial(); + + $this->repository->shouldReceive('getBuilder')->withNoArgs()->andReturn($this->builder); + } + + /** + * Test that we are returning the correct model. + */ + public function testCorrectModelIsAssigned() + { + $this->assertEquals(APIKey::class, $this->repository->model()); + } +} diff --git a/tests/Unit/Repositories/Eloquent/ApiPermissionRepositoryTest.php b/tests/Unit/Repositories/Eloquent/ApiPermissionRepositoryTest.php new file mode 100644 index 00000000..945693c5 --- /dev/null +++ b/tests/Unit/Repositories/Eloquent/ApiPermissionRepositoryTest.php @@ -0,0 +1,65 @@ +. + * + * 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\Repositories\Eloquent; + +use Illuminate\Database\Eloquent\Builder; +use Mockery as m; +use Pterodactyl\Models\APIPermission; +use Pterodactyl\Repositories\Eloquent\ApiPermissionRepository; +use Tests\TestCase; + +class ApiPermissionRepositoryTest extends TestCase +{ + /** + * @var \Illuminate\Database\Eloquent\Builder + */ + protected $builder; + + /** + * @var \Pterodactyl\Repositories\Eloquent\ApiPermissionRepository + */ + protected $repository; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->builder = m::mock(Builder::class); + $this->repository = m::mock(ApiPermissionRepository::class)->makePartial(); + + $this->repository->shouldReceive('getBuilder')->withNoArgs()->andReturn($this->builder); + } + + /** + * Test that we are returning the correct model. + */ + public function testCorrectModelIsAssigned() + { + $this->assertEquals(APIPermission::class, $this->repository->model()); + } +} diff --git a/tests/Unit/Repositories/Eloquent/DatabaseHostRepositoryTest.php b/tests/Unit/Repositories/Eloquent/DatabaseHostRepositoryTest.php new file mode 100644 index 00000000..389961b6 --- /dev/null +++ b/tests/Unit/Repositories/Eloquent/DatabaseHostRepositoryTest.php @@ -0,0 +1,103 @@ +. + * + * 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\Repositories\Eloquent; + +use Illuminate\Database\Eloquent\Builder; +use Mockery as m; +use Pterodactyl\Models\DatabaseHost; +use Pterodactyl\Repositories\Eloquent\DatabaseHostRepository; +use Tests\TestCase; + +class DatabaseHostRepositoryTest extends TestCase +{ + /** + * @var \Illuminate\Database\Eloquent\Builder + */ + protected $builder; + + /** + * @var \Pterodactyl\Repositories\Eloquent\DatabaseHostRepository + */ + protected $repository; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->builder = m::mock(Builder::class); + $this->repository = m::mock(DatabaseHostRepository::class)->makePartial(); + + $this->repository->shouldReceive('getBuilder')->withNoArgs()->andReturn($this->builder); + } + + /** + * Test that we are returning the correct model. + */ + public function testCorrectModelIsAssigned() + { + $this->assertEquals(DatabaseHost::class, $this->repository->model()); + } + + /** + * Test query to reutrn all of the default view data. + */ + public function testHostWithDefaultViewDataIsReturned() + { + $this->builder->shouldReceive('withCount')->with('databases')->once()->andReturnSelf() + ->shouldReceive('with')->with('node')->once()->andReturnSelf() + ->shouldReceive('get')->withNoArgs()->once()->andReturnNull(); + + $this->assertNull($this->repository->getWithViewDetails()); + } + + /** + * Test query to return host and servers. + */ + public function testHostIsReturnedWithServers() + { + $model = factory(DatabaseHost::class)->make(); + + $this->builder->shouldReceive('with')->with('databases.server')->once()->andReturnSelf() + ->shouldReceive('find')->with(1, ['*'])->once()->andReturn($model); + + $this->assertEquals($model, $this->repository->getWithServers(1)); + } + + /** + * Test exception is found if no host is found when querying for servers. + * + * @expectedException \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function testExceptionIsThrownIfNoRecordIsFoundWithServers() + { + $this->builder->shouldReceive('with')->with('databases.server')->once()->andReturnSelf() + ->shouldReceive('find')->with(1, ['*'])->once()->andReturnNull(); + + $this->repository->getWithServers(1); + } +} diff --git a/tests/Unit/Repositories/Eloquent/DatabaseRepositoryTest.php b/tests/Unit/Repositories/Eloquent/DatabaseRepositoryTest.php new file mode 100644 index 00000000..e18802c3 --- /dev/null +++ b/tests/Unit/Repositories/Eloquent/DatabaseRepositoryTest.php @@ -0,0 +1,172 @@ +. + * + * 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\Repositories\Eloquent; + +use Illuminate\Database\Eloquent\Builder; +use Mockery as m; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Exceptions\Repository\DuplicateDatabaseNameException; +use Pterodactyl\Models\Database; +use Pterodactyl\Repositories\Eloquent\DatabaseRepository; +use Tests\TestCase; + +class DatabaseRepositoryTest extends TestCase +{ + /** + * @var \Illuminate\Database\Eloquent\Builder + */ + protected $builder; + + /** + * @var \Pterodactyl\Repositories\Eloquent\DatabaseRepository + */ + protected $repository; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->builder = m::mock(Builder::class); + $this->repository = m::mock(DatabaseRepository::class)->makePartial()->shouldAllowMockingProtectedMethods(); + + $this->repository->shouldReceive('getBuilder')->withNoArgs()->andReturn($this->builder); + $this->repository->shouldNotReceive('runStatement'); + } + + /** + * Test that we are returning the correct model. + */ + public function testCorrectModelIsAssigned() + { + $this->assertEquals(Database::class, $this->repository->model()); + } + + /** + * Test that a database can be created if it does not already exist. + */ + public function testDatabaseIsCreatedIfNotExists() + { + $data = [ + 'server_id' => 1, + 'database_host_id' => 100, + 'database' => 'somename', + ]; + + $this->builder->shouldReceive('where')->with([ + ['server_id', '=', array_get($data, 'server_id')], + ['database_host_id', '=', array_get($data, 'database_host_id')], + ['database', '=', array_get($data, 'database')], + ])->once()->andReturnSelf() + ->shouldReceive('count')->withNoArgs()->once()->andReturn(0); + + $this->repository->shouldReceive('create')->with($data)->once()->andReturn(true); + + $this->assertTrue($this->repository->createIfNotExists($data)); + } + + /** + * Test that an exception is thrown if a database already exists with the given name. + */ + public function testExceptionIsThrownIfDatabaseAlreadyExists() + { + $this->builder->shouldReceive('where->count')->once()->andReturn(1); + $this->repository->shouldNotReceive('create'); + + try { + $this->repository->createIfNotExists([]); + } catch (DisplayException $exception) { + $this->assertInstanceOf(DuplicateDatabaseNameException::class, $exception); + $this->assertEquals('A database with those details already exists for the specified server.', $exception->getMessage()); + } + } + + /** + * Test SQL used to create a database. + */ + public function testCreateDatabaseStatement() + { + $query = sprintf('CREATE DATABASE IF NOT EXISTS `%s`', 'test_database'); + $this->repository->shouldReceive('runStatement')->with($query, 'test')->once()->andReturn(true); + + $this->assertTrue($this->repository->createDatabase('test_database', 'test')); + } + + /** + * Test SQL used to create a user. + */ + public function testCreateUserStatement() + { + $query = sprintf('CREATE USER `%s`@`%s` IDENTIFIED BY \'%s\'', 'test', '%', 'password'); + $this->repository->shouldReceive('runStatement')->with($query, 'test')->once()->andReturn(true); + + $this->assertTrue($this->repository->createUser('test', '%', 'password', 'test')); + } + + /** + * Test that a user is assigned the correct permissions on a database. + */ + public function testUserAssignmentToDatabaseStatement() + { + $query = sprintf('GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, INDEX ON `%s`.* TO `%s`@`%s`', 'test_database', 'test', '%'); + $this->repository->shouldReceive('runStatement')->with($query, 'test')->once()->andReturn(true); + + $this->assertTrue($this->repository->assignUserToDatabase('test_database', 'test', '%', 'test')); + } + + /** + * Test SQL for flushing privileges. + */ + public function testFlushStatement() + { + $this->repository->shouldReceive('runStatement')->with('FLUSH PRIVILEGES', 'test')->once()->andReturn(true); + + $this->assertTrue($this->repository->flush('test')); + } + + /** + * Test SQL to drop a database. + */ + public function testDropDatabaseStatement() + { + $query = sprintf('DROP DATABASE IF EXISTS `%s`', 'test_database'); + $this->repository->shouldReceive('runStatement')->with($query, 'test')->once()->andReturn(true); + + $this->assertTrue($this->repository->dropDatabase('test_database', 'test')); + } + + /** + * Test SQL to drop a user. + */ + public function testDropUserStatement() + { + $query = sprintf('DROP USER IF EXISTS `%s`@`%s`', 'test', '%'); + $this->repository->shouldReceive('runStatement')->with($query, 'test')->once()->andReturn(true); + + $this->assertTrue($this->repository->dropUser('test', '%', 'test')); + } +} diff --git a/tests/Unit/Repositories/Eloquent/LocationRepositoryTest.php b/tests/Unit/Repositories/Eloquent/LocationRepositoryTest.php new file mode 100644 index 00000000..fbc224fe --- /dev/null +++ b/tests/Unit/Repositories/Eloquent/LocationRepositoryTest.php @@ -0,0 +1,115 @@ +. + * + * 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\Repositories\Eloquent; + +use Illuminate\Database\Eloquent\Builder; +use Mockery as m; +use Pterodactyl\Models\Location; +use Pterodactyl\Repositories\Eloquent\LocationRepository; +use Tests\TestCase; + +class LocationRepositoryTest extends TestCase +{ + /** + * @var \Illuminate\Database\Eloquent\Builder + */ + protected $builder; + + /** + * @var \Pterodactyl\Repositories\Eloquent\LocationRepository + */ + protected $repository; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->builder = m::mock(Builder::class); + $this->repository = m::mock(LocationRepository::class)->makePartial(); + + $this->repository->shouldReceive('getBuilder')->withNoArgs()->andReturn($this->builder); + } + + /** + * Test that we are returning the correct model. + */ + public function testCorrectModelIsAssigned() + { + $this->assertEquals(Location::class, $this->repository->model()); + } + + /** + * Test that all locations with associated node and server counts are returned. + */ + public function testAllLocationsWithDetailsAreReturned() + { + $this->builder->shouldReceive('withCount')->with('nodes', 'servers')->once()->andReturnSelf() + ->shouldReceive('get')->with(['*'])->once()->andReturnNull(); + + $this->assertNull($this->repository->getAllWithDetails()); + } + + /** + * Test that all locations with associated node are returned. + */ + public function testAllLocationsWithNodes() + { + $this->builder->shouldReceive('with')->with('nodes')->once()->andReturnSelf() + ->shouldReceive('get')->with(['*'])->once()->andReturnNull(); + + $this->assertNull($this->repository->getAllWithNodes()); + } + + /** + * Test that a single location with associated node is returned. + */ + public function testLocationWithNodeIsReturned() + { + $model = factory(Location::class)->make(); + + $this->builder->shouldReceive('with')->with('nodes.servers')->once()->andReturnSelf() + ->shouldReceive('find')->with(1, ['*'])->once()->andReturn($model); + + $response = $this->repository->getWithNodes(1); + $this->assertInstanceOf(Location::class, $response); + $this->assertEquals($model, $response); + } + + /** + * Test that an exception is thrown when getting location with nodes if no location is found. + * + * @expectedException \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function testExceptionIsThrownIfNoLocationIsFoundWithNodes() + { + $this->builder->shouldReceive('with')->with('nodes.servers')->once()->andReturnSelf() + ->shouldReceive('find')->with(1, ['*'])->once()->andReturnNull(); + + $this->repository->getWithNodes(1); + } +} diff --git a/tests/Unit/Services/Database/DatabaseHostServiceTest.php b/tests/Unit/Services/Database/DatabaseHostServiceTest.php index 84df7f02..464d80ea 100644 --- a/tests/Unit/Services/Database/DatabaseHostServiceTest.php +++ b/tests/Unit/Services/Database/DatabaseHostServiceTest.php @@ -25,6 +25,8 @@ namespace Tests\Unit\Services\Administrative; use Mockery as m; +use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; +use Pterodactyl\Exceptions\DisplayException; use Tests\TestCase; use Illuminate\Database\DatabaseManager; use Illuminate\Contracts\Encryption\Encrypter; @@ -39,6 +41,11 @@ class DatabaseHostServiceTest extends TestCase */ protected $database; + /** + * @var \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface + */ + protected $databaseRepository; + /** * @var \Pterodactyl\Extensions\DynamicDatabaseConnection */ @@ -67,12 +74,14 @@ class DatabaseHostServiceTest extends TestCase parent::setUp(); $this->database = m::mock(DatabaseManager::class); + $this->databaseRepository = m::mock(DatabaseRepositoryInterface::class); $this->dynamic = m::mock(DynamicDatabaseConnection::class); $this->encrypter = m::mock(Encrypter::class); $this->repository = m::mock(DatabaseHostRepositoryInterface::class); $this->service = new DatabaseHostService( $this->database, + $this->databaseRepository, $this->repository, $this->dynamic, $this->encrypter @@ -82,7 +91,7 @@ class DatabaseHostServiceTest extends TestCase /** * Test that creating a host returns the correct data. */ - public function test_create_host_function() + public function testHostIsCreated() { $data = [ 'password' => 'raw-password', @@ -130,7 +139,7 @@ class DatabaseHostServiceTest extends TestCase /** * Test that passing a password will store an encrypted version in the DB. */ - public function test_update_with_password() + public function testHostIsUpdatedWithPasswordProvided() { $finalData = (object) ['password' => 'enc-pass', 'host' => '123.456.78.9']; @@ -158,7 +167,7 @@ class DatabaseHostServiceTest extends TestCase /** * Test that passing no or empty password will skip storing it. */ - public function test_update_without_password() + public function testHostIsUpdatedWithoutPassword() { $finalData = (object) ['host' => '123.456.78.9']; @@ -182,12 +191,27 @@ class DatabaseHostServiceTest extends TestCase /** * Test that a database host can be deleted. */ - public function test_delete_function() + public function testHostIsDeleted() { - $this->repository->shouldReceive('deleteIfNoDatabases')->with(1)->once()->andReturn(true); + $this->databaseRepository->shouldReceive('findCountWhere')->with([['database_host_id', '=', 1]])->once()->andReturn(0); + $this->repository->shouldReceive('delete')->with(1)->once()->andReturn(true); $response = $this->service->delete(1); $this->assertTrue($response, 'Assert that response is true.'); } + + /** + * Test exception is thrown when there are databases attached to a host. + */ + public function testExceptionIsThrownIfHostHasDatabases() + { + $this->databaseRepository->shouldReceive('findCountWhere')->with([['database_host_id', '=', 1]])->once()->andReturn(2); + + try { + $this->service->delete(1); + } catch (DisplayException $exception) { + $this->assertEquals(trans('admin/exceptions.databases.delete_has_databases'), $exception->getMessage()); + } + } } From 1e1eac1b9c717db991d2be610620e5a6a46d1051 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 27 Aug 2017 14:55:25 -0500 Subject: [PATCH 68/99] Apply fixes from StyleCI (#607) --- app/Http/Controllers/Admin/PackController.php | 20 +++++++------- app/Models/Permission.php | 2 +- app/Models/Subuser.php | 4 +-- app/Providers/RepositoryServiceProvider.php | 10 +++---- app/Repositories/Daemon/BaseRepository.php | 2 +- app/Repositories/Daemon/FileRepository.php | 2 +- app/Repositories/Daemon/ServerRepository.php | 2 +- .../Eloquent/DatabaseRepository.php | 2 +- .../Eloquent/LocationRepository.php | 2 +- app/Repositories/Eloquent/NodeRepository.php | 2 +- app/Repositories/Eloquent/PackRepository.php | 8 +++--- .../Eloquent/SubuserRepository.php | 6 ++--- app/Services/Database/DatabaseHostService.php | 4 +-- app/Services/Nodes/DeletionService.php | 2 +- app/Services/Packs/ExportPackService.php | 8 +++--- app/Services/Packs/PackCreationService.php | 2 +- .../Subusers/SubuserCreationService.php | 14 +++++----- .../Subusers/SubuserUpdateService.php | 8 +++--- .../Eloquent/AllocationRepositoryTest.php | 6 ++--- .../Eloquent/ApiKeyRepositoryTest.php | 6 ++--- .../Eloquent/ApiPermissionRepositoryTest.php | 6 ++--- .../Eloquent/DatabaseHostRepositoryTest.php | 6 ++--- .../Eloquent/DatabaseRepositoryTest.php | 10 +++---- .../Eloquent/LocationRepositoryTest.php | 6 ++--- .../Database/DatabaseHostServiceTest.php | 4 +-- .../Helpers/SoftwareVersionServiceTest.php | 6 ++--- .../Services/Packs/ExportPackServiceTest.php | 14 +++++----- .../Packs/PackCreationServiceTest.php | 16 +++++------ .../Packs/PackDeletionServiceTest.php | 12 ++++----- .../Services/Packs/PackUpdateServiceTest.php | 8 +++--- .../Packs/TemplateUploadServiceTest.php | 16 +++++------ .../PermissionCreationServiceTest.php | 4 +-- .../Subusers/SubuserCreationServiceTest.php | 27 +++++++++---------- .../Subusers/SubuserDeletionServiceTest.php | 13 +++++---- .../Subusers/SubuserUpdateServiceTest.php | 16 +++++------ 35 files changed, 137 insertions(+), 139 deletions(-) diff --git a/app/Http/Controllers/Admin/PackController.php b/app/Http/Controllers/Admin/PackController.php index 604ba333..ae06aff0 100644 --- a/app/Http/Controllers/Admin/PackController.php +++ b/app/Http/Controllers/Admin/PackController.php @@ -24,19 +24,19 @@ namespace Pterodactyl\Http\Controllers\Admin; -use Illuminate\Contracts\Config\Repository as ConfigRepository; -use Prologue\Alerts\AlertsMessageBag; -use Pterodactyl\Contracts\Repository\PackRepositoryInterface; -use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; -use Pterodactyl\Http\Requests\Admin\PackFormRequest; -use Pterodactyl\Services\Packs\ExportPackService; -use Pterodactyl\Services\Packs\PackCreationService; -use Pterodactyl\Services\Packs\PackDeletionService; -use Pterodactyl\Services\Packs\PackUpdateService; -use Pterodactyl\Services\Packs\TemplateUploadService; use Illuminate\Http\Request; use Pterodactyl\Models\Pack; +use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Http\Controllers\Controller; +use Pterodactyl\Services\Packs\ExportPackService; +use Pterodactyl\Services\Packs\PackUpdateService; +use Pterodactyl\Services\Packs\PackCreationService; +use Pterodactyl\Services\Packs\PackDeletionService; +use Pterodactyl\Http\Requests\Admin\PackFormRequest; +use Pterodactyl\Services\Packs\TemplateUploadService; +use Pterodactyl\Contracts\Repository\PackRepositoryInterface; +use Illuminate\Contracts\Config\Repository as ConfigRepository; +use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; class PackController extends Controller { diff --git a/app/Models/Permission.php b/app/Models/Permission.php index 3587e7fc..7d4619c6 100644 --- a/app/Models/Permission.php +++ b/app/Models/Permission.php @@ -24,9 +24,9 @@ namespace Pterodactyl\Models; +use Sofa\Eloquence\Eloquence; use Illuminate\Database\Eloquent\Model; use Sofa\Eloquence\Contracts\CleansAttributes; -use Sofa\Eloquence\Eloquence; class Permission extends Model implements CleansAttributes { diff --git a/app/Models/Subuser.php b/app/Models/Subuser.php index 276f97b9..5326da3f 100644 --- a/app/Models/Subuser.php +++ b/app/Models/Subuser.php @@ -24,12 +24,12 @@ namespace Pterodactyl\Models; +use Sofa\Eloquence\Eloquence; +use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; use Illuminate\Notifications\Notifiable; use Sofa\Eloquence\Contracts\CleansAttributes; use Sofa\Eloquence\Contracts\Validable as ValidableContract; -use Sofa\Eloquence\Eloquence; -use Sofa\Eloquence\Validable; class Subuser extends Model implements CleansAttributes, ValidableContract { diff --git a/app/Providers/RepositoryServiceProvider.php b/app/Providers/RepositoryServiceProvider.php index 178c9102..a0a85fae 100644 --- a/app/Providers/RepositoryServiceProvider.php +++ b/app/Providers/RepositoryServiceProvider.php @@ -25,16 +25,12 @@ namespace Pterodactyl\Providers; use Illuminate\Support\ServiceProvider; -use Pterodactyl\Contracts\Repository\Daemon\CommandRepositoryInterface; -use Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface; -use Pterodactyl\Contracts\Repository\Daemon\PowerRepositoryInterface; -use Pterodactyl\Contracts\Repository\PackRepositoryInterface; -use Pterodactyl\Repositories\Daemon\CommandRepository; use Pterodactyl\Repositories\Daemon\FileRepository; use Pterodactyl\Repositories\Daemon\PowerRepository; use Pterodactyl\Repositories\Eloquent\NodeRepository; use Pterodactyl\Repositories\Eloquent\PackRepository; use Pterodactyl\Repositories\Eloquent\UserRepository; +use Pterodactyl\Repositories\Daemon\CommandRepository; use Pterodactyl\Repositories\Eloquent\ApiKeyRepository; use Pterodactyl\Repositories\Eloquent\ServerRepository; use Pterodactyl\Repositories\Eloquent\ServiceRepository; @@ -43,6 +39,7 @@ use Pterodactyl\Repositories\Eloquent\LocationRepository; use Pterodactyl\Repositories\Eloquent\AllocationRepository; use Pterodactyl\Repositories\Daemon\ConfigurationRepository; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Pterodactyl\Contracts\Repository\PackRepositoryInterface; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Repositories\Eloquent\DatabaseHostRepository; use Pterodactyl\Repositories\Eloquent\ApiPermissionRepository; @@ -56,9 +53,12 @@ use Pterodactyl\Repositories\Eloquent\ServiceVariableRepository; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; +use Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface; +use Pterodactyl\Contracts\Repository\Daemon\PowerRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; use Pterodactyl\Contracts\Repository\ApiPermissionRepositoryInterface; use Pterodactyl\Contracts\Repository\ServiceOptionRepositoryInterface; +use Pterodactyl\Contracts\Repository\Daemon\CommandRepositoryInterface; use Pterodactyl\Contracts\Repository\OptionVariableRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; use Pterodactyl\Contracts\Repository\ServiceVariableRepositoryInterface; diff --git a/app/Repositories/Daemon/BaseRepository.php b/app/Repositories/Daemon/BaseRepository.php index fc62d73e..26fb898c 100644 --- a/app/Repositories/Daemon/BaseRepository.php +++ b/app/Repositories/Daemon/BaseRepository.php @@ -25,11 +25,11 @@ namespace Pterodactyl\Repositories\Daemon; use GuzzleHttp\Client; +use Webmozart\Assert\Assert; use Illuminate\Foundation\Application; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Illuminate\Contracts\Config\Repository as ConfigRepository; use Pterodactyl\Contracts\Repository\Daemon\BaseRepositoryInterface; -use Webmozart\Assert\Assert; class BaseRepository implements BaseRepositoryInterface { diff --git a/app/Repositories/Daemon/FileRepository.php b/app/Repositories/Daemon/FileRepository.php index 71182b11..30e73c82 100644 --- a/app/Repositories/Daemon/FileRepository.php +++ b/app/Repositories/Daemon/FileRepository.php @@ -24,8 +24,8 @@ namespace Pterodactyl\Repositories\Daemon; -use Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface; use Webmozart\Assert\Assert; +use Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface; class FileRepository extends BaseRepository implements FileRepositoryInterface { diff --git a/app/Repositories/Daemon/ServerRepository.php b/app/Repositories/Daemon/ServerRepository.php index a761510d..594bb175 100644 --- a/app/Repositories/Daemon/ServerRepository.php +++ b/app/Repositories/Daemon/ServerRepository.php @@ -24,10 +24,10 @@ namespace Pterodactyl\Repositories\Daemon; +use Webmozart\Assert\Assert; use Pterodactyl\Services\Servers\EnvironmentService; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface as DatabaseServerRepositoryInterface; -use Webmozart\Assert\Assert; class ServerRepository extends BaseRepository implements ServerRepositoryInterface { diff --git a/app/Repositories/Eloquent/DatabaseRepository.php b/app/Repositories/Eloquent/DatabaseRepository.php index 12000680..995b7db2 100644 --- a/app/Repositories/Eloquent/DatabaseRepository.php +++ b/app/Repositories/Eloquent/DatabaseRepository.php @@ -24,11 +24,11 @@ namespace Pterodactyl\Repositories\Eloquent; -use Pterodactyl\Exceptions\Repository\DuplicateDatabaseNameException; use Pterodactyl\Models\Database; use Illuminate\Foundation\Application; use Illuminate\Database\DatabaseManager; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; +use Pterodactyl\Exceptions\Repository\DuplicateDatabaseNameException; class DatabaseRepository extends EloquentRepository implements DatabaseRepositoryInterface { diff --git a/app/Repositories/Eloquent/LocationRepository.php b/app/Repositories/Eloquent/LocationRepository.php index 229e1e11..318493a8 100644 --- a/app/Repositories/Eloquent/LocationRepository.php +++ b/app/Repositories/Eloquent/LocationRepository.php @@ -26,9 +26,9 @@ namespace Pterodactyl\Repositories\Eloquent; use Pterodactyl\Models\Location; use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Repositories\Concerns\Searchable; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; -use Pterodactyl\Repositories\Concerns\Searchable; class LocationRepository extends EloquentRepository implements LocationRepositoryInterface { diff --git a/app/Repositories/Eloquent/NodeRepository.php b/app/Repositories/Eloquent/NodeRepository.php index e421d778..240f9791 100644 --- a/app/Repositories/Eloquent/NodeRepository.php +++ b/app/Repositories/Eloquent/NodeRepository.php @@ -25,9 +25,9 @@ namespace Pterodactyl\Repositories\Eloquent; use Pterodactyl\Models\Node; +use Pterodactyl\Repositories\Concerns\Searchable; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; -use Pterodactyl\Repositories\Concerns\Searchable; class NodeRepository extends EloquentRepository implements NodeRepositoryInterface { diff --git a/app/Repositories/Eloquent/PackRepository.php b/app/Repositories/Eloquent/PackRepository.php index 5f1641f7..56b04d2e 100644 --- a/app/Repositories/Eloquent/PackRepository.php +++ b/app/Repositories/Eloquent/PackRepository.php @@ -24,12 +24,12 @@ namespace Pterodactyl\Repositories\Eloquent; -use Illuminate\Database\Eloquent\ModelNotFoundException; use Pterodactyl\Models\Pack; -use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory; -use Pterodactyl\Repositories\Concerns\Searchable; -use Pterodactyl\Contracts\Repository\PackRepositoryInterface; use Webmozart\Assert\Assert; +use Pterodactyl\Repositories\Concerns\Searchable; +use Illuminate\Database\Eloquent\ModelNotFoundException; +use Pterodactyl\Contracts\Repository\PackRepositoryInterface; +use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory; class PackRepository extends EloquentRepository implements PackRepositoryInterface { diff --git a/app/Repositories/Eloquent/SubuserRepository.php b/app/Repositories/Eloquent/SubuserRepository.php index 92f4b186..7b7eaa8a 100644 --- a/app/Repositories/Eloquent/SubuserRepository.php +++ b/app/Repositories/Eloquent/SubuserRepository.php @@ -24,10 +24,10 @@ namespace Pterodactyl\Repositories\Eloquent; -use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; -use Pterodactyl\Exceptions\Repository\RecordNotFoundException; -use Pterodactyl\Models\Subuser; use Webmozart\Assert\Assert; +use Pterodactyl\Models\Subuser; +use Pterodactyl\Exceptions\Repository\RecordNotFoundException; +use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; class SubuserRepository extends EloquentRepository implements SubuserRepositoryInterface { diff --git a/app/Services/Database/DatabaseHostService.php b/app/Services/Database/DatabaseHostService.php index 2ad11ecc..f64f3a94 100644 --- a/app/Services/Database/DatabaseHostService.php +++ b/app/Services/Database/DatabaseHostService.php @@ -25,10 +25,10 @@ namespace Pterodactyl\Services\Database; use Illuminate\Database\DatabaseManager; -use Illuminate\Contracts\Encryption\Encrypter; -use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Exceptions\DisplayException; +use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Extensions\DynamicDatabaseConnection; +use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; class DatabaseHostService diff --git a/app/Services/Nodes/DeletionService.php b/app/Services/Nodes/DeletionService.php index 6cbab264..f5180ce0 100644 --- a/app/Services/Nodes/DeletionService.php +++ b/app/Services/Nodes/DeletionService.php @@ -24,10 +24,10 @@ namespace Pterodactyl\Services\Nodes; -use Pterodactyl\Exceptions\Service\HasActiveServersException; use Pterodactyl\Models\Node; use Illuminate\Contracts\Translation\Translator; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; +use Pterodactyl\Exceptions\Service\HasActiveServersException; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; class DeletionService diff --git a/app/Services/Packs/ExportPackService.php b/app/Services/Packs/ExportPackService.php index 42f1cf30..1f5ac7b4 100644 --- a/app/Services/Packs/ExportPackService.php +++ b/app/Services/Packs/ExportPackService.php @@ -24,11 +24,11 @@ namespace Pterodactyl\Services\Packs; -use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory; -use Pterodactyl\Contracts\Repository\PackRepositoryInterface; -use Pterodactyl\Exceptions\Service\Pack\ZipArchiveCreationException; -use Pterodactyl\Models\Pack; use ZipArchive; +use Pterodactyl\Models\Pack; +use Pterodactyl\Contracts\Repository\PackRepositoryInterface; +use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory; +use Pterodactyl\Exceptions\Service\Pack\ZipArchiveCreationException; class ExportPackService { diff --git a/app/Services/Packs/PackCreationService.php b/app/Services/Packs/PackCreationService.php index 9890882f..b4274023 100644 --- a/app/Services/Packs/PackCreationService.php +++ b/app/Services/Packs/PackCreationService.php @@ -24,8 +24,8 @@ namespace Pterodactyl\Services\Packs; -use Illuminate\Http\UploadedFile; use Ramsey\Uuid\Uuid; +use Illuminate\Http\UploadedFile; use Illuminate\Database\ConnectionInterface; use Pterodactyl\Contracts\Repository\PackRepositoryInterface; use Illuminate\Contracts\Filesystem\Factory as FilesystemFactory; diff --git a/app/Services/Subusers/SubuserCreationService.php b/app/Services/Subusers/SubuserCreationService.php index 3de92e27..cdcd4c3a 100644 --- a/app/Services/Subusers/SubuserCreationService.php +++ b/app/Services/Subusers/SubuserCreationService.php @@ -24,18 +24,18 @@ namespace Pterodactyl\Services\Subusers; +use Illuminate\Log\Writer; +use Pterodactyl\Models\Server; use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; -use Illuminate\Log\Writer; -use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Services\Users\CreationService; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; -use Pterodactyl\Contracts\Repository\UserRepositoryInterface; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Exceptions\Service\Subuser\ServerSubuserExistsException; use Pterodactyl\Exceptions\Service\Subuser\UserIsServerOwnerException; -use Pterodactyl\Models\Server; -use Pterodactyl\Services\Users\CreationService; +use Pterodactyl\Exceptions\Service\Subuser\ServerSubuserExistsException; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; class SubuserCreationService { diff --git a/app/Services/Subusers/SubuserUpdateService.php b/app/Services/Subusers/SubuserUpdateService.php index f69870cd..11faf5bb 100644 --- a/app/Services/Subusers/SubuserUpdateService.php +++ b/app/Services/Subusers/SubuserUpdateService.php @@ -24,13 +24,13 @@ namespace Pterodactyl\Services\Subusers; +use Illuminate\Log\Writer; use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; -use Illuminate\Log\Writer; -use Pterodactyl\Contracts\Repository\PermissionRepositoryInterface; -use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; -use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; +use Pterodactyl\Contracts\Repository\PermissionRepositoryInterface; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; class SubuserUpdateService { diff --git a/tests/Unit/Repositories/Eloquent/AllocationRepositoryTest.php b/tests/Unit/Repositories/Eloquent/AllocationRepositoryTest.php index 0c7b6f4c..9d889118 100644 --- a/tests/Unit/Repositories/Eloquent/AllocationRepositoryTest.php +++ b/tests/Unit/Repositories/Eloquent/AllocationRepositoryTest.php @@ -24,11 +24,11 @@ namespace Tests\Unit\Repositories\Eloquent; -use Illuminate\Database\Eloquent\Builder; use Mockery as m; -use Pterodactyl\Models\Allocation; -use Pterodactyl\Repositories\Eloquent\AllocationRepository; use Tests\TestCase; +use Pterodactyl\Models\Allocation; +use Illuminate\Database\Eloquent\Builder; +use Pterodactyl\Repositories\Eloquent\AllocationRepository; class AllocationRepositoryTest extends TestCase { diff --git a/tests/Unit/Repositories/Eloquent/ApiKeyRepositoryTest.php b/tests/Unit/Repositories/Eloquent/ApiKeyRepositoryTest.php index eadfbde4..8d8574b0 100644 --- a/tests/Unit/Repositories/Eloquent/ApiKeyRepositoryTest.php +++ b/tests/Unit/Repositories/Eloquent/ApiKeyRepositoryTest.php @@ -24,11 +24,11 @@ namespace Tests\Unit\Repositories\Eloquent; -use Illuminate\Database\Eloquent\Builder; use Mockery as m; -use Pterodactyl\Models\APIKey; -use Pterodactyl\Repositories\Eloquent\ApiKeyRepository; use Tests\TestCase; +use Pterodactyl\Models\APIKey; +use Illuminate\Database\Eloquent\Builder; +use Pterodactyl\Repositories\Eloquent\ApiKeyRepository; class ApiKeyRepositoryTest extends TestCase { diff --git a/tests/Unit/Repositories/Eloquent/ApiPermissionRepositoryTest.php b/tests/Unit/Repositories/Eloquent/ApiPermissionRepositoryTest.php index 945693c5..8d601f80 100644 --- a/tests/Unit/Repositories/Eloquent/ApiPermissionRepositoryTest.php +++ b/tests/Unit/Repositories/Eloquent/ApiPermissionRepositoryTest.php @@ -24,11 +24,11 @@ namespace Tests\Unit\Repositories\Eloquent; -use Illuminate\Database\Eloquent\Builder; use Mockery as m; -use Pterodactyl\Models\APIPermission; -use Pterodactyl\Repositories\Eloquent\ApiPermissionRepository; use Tests\TestCase; +use Pterodactyl\Models\APIPermission; +use Illuminate\Database\Eloquent\Builder; +use Pterodactyl\Repositories\Eloquent\ApiPermissionRepository; class ApiPermissionRepositoryTest extends TestCase { diff --git a/tests/Unit/Repositories/Eloquent/DatabaseHostRepositoryTest.php b/tests/Unit/Repositories/Eloquent/DatabaseHostRepositoryTest.php index 389961b6..d74d8e06 100644 --- a/tests/Unit/Repositories/Eloquent/DatabaseHostRepositoryTest.php +++ b/tests/Unit/Repositories/Eloquent/DatabaseHostRepositoryTest.php @@ -24,11 +24,11 @@ namespace Tests\Unit\Repositories\Eloquent; -use Illuminate\Database\Eloquent\Builder; use Mockery as m; -use Pterodactyl\Models\DatabaseHost; -use Pterodactyl\Repositories\Eloquent\DatabaseHostRepository; use Tests\TestCase; +use Pterodactyl\Models\DatabaseHost; +use Illuminate\Database\Eloquent\Builder; +use Pterodactyl\Repositories\Eloquent\DatabaseHostRepository; class DatabaseHostRepositoryTest extends TestCase { diff --git a/tests/Unit/Repositories/Eloquent/DatabaseRepositoryTest.php b/tests/Unit/Repositories/Eloquent/DatabaseRepositoryTest.php index e18802c3..aaceac3f 100644 --- a/tests/Unit/Repositories/Eloquent/DatabaseRepositoryTest.php +++ b/tests/Unit/Repositories/Eloquent/DatabaseRepositoryTest.php @@ -24,13 +24,13 @@ namespace Tests\Unit\Repositories\Eloquent; -use Illuminate\Database\Eloquent\Builder; use Mockery as m; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Exceptions\Repository\DuplicateDatabaseNameException; -use Pterodactyl\Models\Database; -use Pterodactyl\Repositories\Eloquent\DatabaseRepository; use Tests\TestCase; +use Pterodactyl\Models\Database; +use Illuminate\Database\Eloquent\Builder; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Repositories\Eloquent\DatabaseRepository; +use Pterodactyl\Exceptions\Repository\DuplicateDatabaseNameException; class DatabaseRepositoryTest extends TestCase { diff --git a/tests/Unit/Repositories/Eloquent/LocationRepositoryTest.php b/tests/Unit/Repositories/Eloquent/LocationRepositoryTest.php index fbc224fe..75113d85 100644 --- a/tests/Unit/Repositories/Eloquent/LocationRepositoryTest.php +++ b/tests/Unit/Repositories/Eloquent/LocationRepositoryTest.php @@ -24,11 +24,11 @@ namespace Tests\Unit\Repositories\Eloquent; -use Illuminate\Database\Eloquent\Builder; use Mockery as m; -use Pterodactyl\Models\Location; -use Pterodactyl\Repositories\Eloquent\LocationRepository; use Tests\TestCase; +use Pterodactyl\Models\Location; +use Illuminate\Database\Eloquent\Builder; +use Pterodactyl\Repositories\Eloquent\LocationRepository; class LocationRepositoryTest extends TestCase { diff --git a/tests/Unit/Services/Database/DatabaseHostServiceTest.php b/tests/Unit/Services/Database/DatabaseHostServiceTest.php index 464d80ea..bbba1453 100644 --- a/tests/Unit/Services/Database/DatabaseHostServiceTest.php +++ b/tests/Unit/Services/Database/DatabaseHostServiceTest.php @@ -25,13 +25,13 @@ namespace Tests\Unit\Services\Administrative; use Mockery as m; -use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; -use Pterodactyl\Exceptions\DisplayException; use Tests\TestCase; use Illuminate\Database\DatabaseManager; +use Pterodactyl\Exceptions\DisplayException; use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Extensions\DynamicDatabaseConnection; use Pterodactyl\Services\Database\DatabaseHostService; +use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; class DatabaseHostServiceTest extends TestCase diff --git a/tests/Unit/Services/Helpers/SoftwareVersionServiceTest.php b/tests/Unit/Services/Helpers/SoftwareVersionServiceTest.php index a3039694..895530e6 100644 --- a/tests/Unit/Services/Helpers/SoftwareVersionServiceTest.php +++ b/tests/Unit/Services/Helpers/SoftwareVersionServiceTest.php @@ -25,10 +25,10 @@ namespace Tests\Unit\Services\Helpers; use Closure; -use GuzzleHttp\Client; use Mockery as m; -use Pterodactyl\Services\Helpers\SoftwareVersionService; 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; @@ -64,7 +64,7 @@ class SoftwareVersionServiceTest extends TestCase protected $service; /** - * Setup tests + * Setup tests. */ public function setUp() { diff --git a/tests/Unit/Services/Packs/ExportPackServiceTest.php b/tests/Unit/Services/Packs/ExportPackServiceTest.php index 06674c7c..416c27f4 100644 --- a/tests/Unit/Services/Packs/ExportPackServiceTest.php +++ b/tests/Unit/Services/Packs/ExportPackServiceTest.php @@ -24,14 +24,14 @@ namespace Tests\Unit\Services\Packs; -use Illuminate\Contracts\Filesystem\Factory; -use Mockery as m; -use phpmock\phpunit\PHPMock; -use Pterodactyl\Contracts\Repository\PackRepositoryInterface; -use Pterodactyl\Models\Pack; -use Pterodactyl\Services\Packs\ExportPackService; -use Tests\TestCase; use ZipArchive; +use Mockery as m; +use Tests\TestCase; +use phpmock\phpunit\PHPMock; +use Pterodactyl\Models\Pack; +use Illuminate\Contracts\Filesystem\Factory; +use Pterodactyl\Services\Packs\ExportPackService; +use Pterodactyl\Contracts\Repository\PackRepositoryInterface; class ExportPackServiceTest extends TestCase { diff --git a/tests/Unit/Services/Packs/PackCreationServiceTest.php b/tests/Unit/Services/Packs/PackCreationServiceTest.php index 7a21b7a9..9dc634ba 100644 --- a/tests/Unit/Services/Packs/PackCreationServiceTest.php +++ b/tests/Unit/Services/Packs/PackCreationServiceTest.php @@ -25,16 +25,16 @@ namespace Tests\Unit\Services\Packs; use Exception; -use Illuminate\Contracts\Filesystem\Factory; -use Illuminate\Http\UploadedFile; use Mockery as m; -use Illuminate\Database\ConnectionInterface; -use Pterodactyl\Contracts\Repository\PackRepositoryInterface; -use Pterodactyl\Exceptions\Service\Pack\InvalidFileMimeTypeException; -use Pterodactyl\Exceptions\Service\Pack\InvalidFileUploadException; -use Pterodactyl\Models\Pack; -use Pterodactyl\Services\Packs\PackCreationService; use Tests\TestCase; +use Pterodactyl\Models\Pack; +use Illuminate\Http\UploadedFile; +use Illuminate\Contracts\Filesystem\Factory; +use Illuminate\Database\ConnectionInterface; +use Pterodactyl\Services\Packs\PackCreationService; +use Pterodactyl\Contracts\Repository\PackRepositoryInterface; +use Pterodactyl\Exceptions\Service\Pack\InvalidFileUploadException; +use Pterodactyl\Exceptions\Service\Pack\InvalidFileMimeTypeException; class PackCreationServiceTest extends TestCase { diff --git a/tests/Unit/Services/Packs/PackDeletionServiceTest.php b/tests/Unit/Services/Packs/PackDeletionServiceTest.php index 74ec01d5..c6b2e4b3 100644 --- a/tests/Unit/Services/Packs/PackDeletionServiceTest.php +++ b/tests/Unit/Services/Packs/PackDeletionServiceTest.php @@ -25,15 +25,15 @@ namespace Tests\Unit\Services\Packs; use Exception; +use Mockery as m; +use Tests\TestCase; +use Pterodactyl\Models\Pack; use Illuminate\Contracts\Filesystem\Factory; use Illuminate\Database\ConnectionInterface; -use Mockery as m; -use Pterodactyl\Contracts\Repository\PackRepositoryInterface; -use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; -use Pterodactyl\Exceptions\Service\HasActiveServersException; -use Pterodactyl\Models\Pack; use Pterodactyl\Services\Packs\PackDeletionService; -use Tests\TestCase; +use Pterodactyl\Contracts\Repository\PackRepositoryInterface; +use Pterodactyl\Exceptions\Service\HasActiveServersException; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; class PackDeletionServiceTest extends TestCase { diff --git a/tests/Unit/Services/Packs/PackUpdateServiceTest.php b/tests/Unit/Services/Packs/PackUpdateServiceTest.php index dffdecb9..48ae8d77 100644 --- a/tests/Unit/Services/Packs/PackUpdateServiceTest.php +++ b/tests/Unit/Services/Packs/PackUpdateServiceTest.php @@ -25,12 +25,12 @@ namespace Tests\Unit\Services\Packs; use Mockery as m; -use Pterodactyl\Contracts\Repository\PackRepositoryInterface; -use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; -use Pterodactyl\Exceptions\Service\HasActiveServersException; +use Tests\TestCase; use Pterodactyl\Models\Pack; use Pterodactyl\Services\Packs\PackUpdateService; -use Tests\TestCase; +use Pterodactyl\Contracts\Repository\PackRepositoryInterface; +use Pterodactyl\Exceptions\Service\HasActiveServersException; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; class PackUpdateServiceTest extends TestCase { diff --git a/tests/Unit/Services/Packs/TemplateUploadServiceTest.php b/tests/Unit/Services/Packs/TemplateUploadServiceTest.php index f16583c7..bb1a564f 100644 --- a/tests/Unit/Services/Packs/TemplateUploadServiceTest.php +++ b/tests/Unit/Services/Packs/TemplateUploadServiceTest.php @@ -24,18 +24,18 @@ namespace Tests\Unit\Services\Packs; -use Illuminate\Http\UploadedFile; +use ZipArchive; use Mockery as m; -use Pterodactyl\Exceptions\Service\Pack\InvalidFileMimeTypeException; -use Pterodactyl\Exceptions\Service\Pack\InvalidFileUploadException; -use Pterodactyl\Exceptions\Service\Pack\InvalidPackArchiveFormatException; -use Pterodactyl\Exceptions\Service\Pack\UnreadableZipArchiveException; -use Pterodactyl\Exceptions\Service\Pack\ZipExtractionException; +use Tests\TestCase; use Pterodactyl\Models\Pack; +use Illuminate\Http\UploadedFile; use Pterodactyl\Services\Packs\PackCreationService; use Pterodactyl\Services\Packs\TemplateUploadService; -use Tests\TestCase; -use ZipArchive; +use Pterodactyl\Exceptions\Service\Pack\ZipExtractionException; +use Pterodactyl\Exceptions\Service\Pack\InvalidFileUploadException; +use Pterodactyl\Exceptions\Service\Pack\InvalidFileMimeTypeException; +use Pterodactyl\Exceptions\Service\Pack\UnreadableZipArchiveException; +use Pterodactyl\Exceptions\Service\Pack\InvalidPackArchiveFormatException; class TemplateUploadServiceTest extends TestCase { diff --git a/tests/Unit/Services/Subusers/PermissionCreationServiceTest.php b/tests/Unit/Services/Subusers/PermissionCreationServiceTest.php index 85171448..65407ce9 100644 --- a/tests/Unit/Services/Subusers/PermissionCreationServiceTest.php +++ b/tests/Unit/Services/Subusers/PermissionCreationServiceTest.php @@ -25,9 +25,9 @@ namespace Tests\Unit\Services\Subusers; use Mockery as m; -use Pterodactyl\Contracts\Repository\PermissionRepositoryInterface; -use Pterodactyl\Services\Subusers\PermissionCreationService; use Tests\TestCase; +use Pterodactyl\Services\Subusers\PermissionCreationService; +use Pterodactyl\Contracts\Repository\PermissionRepositoryInterface; class PermissionCreationServiceTest extends TestCase { diff --git a/tests/Unit/Services/Subusers/SubuserCreationServiceTest.php b/tests/Unit/Services/Subusers/SubuserCreationServiceTest.php index bf242cab..bba981e1 100644 --- a/tests/Unit/Services/Subusers/SubuserCreationServiceTest.php +++ b/tests/Unit/Services/Subusers/SubuserCreationServiceTest.php @@ -24,23 +24,23 @@ namespace Tests\Unit\Services\Subusers; -use Illuminate\Database\ConnectionInterface; -use Illuminate\Log\Writer; use Mockery as m; +use Tests\TestCase; +use Illuminate\Log\Writer; use phpmock\phpunit\PHPMock; -use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; -use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; -use Pterodactyl\Contracts\Repository\UserRepositoryInterface; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Exceptions\Service\Subuser\ServerSubuserExistsException; -use Pterodactyl\Exceptions\Service\Subuser\UserIsServerOwnerException; +use Pterodactyl\Models\User; use Pterodactyl\Models\Server; use Pterodactyl\Models\Subuser; -use Pterodactyl\Models\User; -use Pterodactyl\Services\Subusers\PermissionCreationService; -use Pterodactyl\Services\Subusers\SubuserCreationService; +use Illuminate\Database\ConnectionInterface; +use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Services\Users\CreationService; -use Tests\TestCase; +use Pterodactyl\Services\Subusers\SubuserCreationService; +use Pterodactyl\Services\Subusers\PermissionCreationService; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; +use Pterodactyl\Exceptions\Service\Subuser\UserIsServerOwnerException; +use Pterodactyl\Exceptions\Service\Subuser\ServerSubuserExistsException; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; class SubuserCreationServiceTest extends TestCase @@ -200,7 +200,7 @@ class SubuserCreationServiceTest extends TestCase } /** - * Test that an exception gets thrown if the subuser is actually the server owner + * Test that an exception gets thrown if the subuser is actually the server owner. */ public function testExceptionIsThrownIfUserIsServerOwner() { @@ -237,6 +237,5 @@ class SubuserCreationServiceTest extends TestCase $this->assertInstanceOf(ServerSubuserExistsException::class, $exception); $this->assertEquals(trans('admin/exceptions.subusers.subuser_exists'), $exception->getMessage()); } - } } diff --git a/tests/Unit/Services/Subusers/SubuserDeletionServiceTest.php b/tests/Unit/Services/Subusers/SubuserDeletionServiceTest.php index 23d0155a..25a976e4 100644 --- a/tests/Unit/Services/Subusers/SubuserDeletionServiceTest.php +++ b/tests/Unit/Services/Subusers/SubuserDeletionServiceTest.php @@ -24,16 +24,16 @@ namespace Tests\Unit\Services\Subusers; -use GuzzleHttp\Exception\RequestException; -use Illuminate\Database\ConnectionInterface; -use Illuminate\Log\Writer; use Mockery as m; -use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; -use Pterodactyl\Exceptions\DisplayException; +use Tests\TestCase; +use Illuminate\Log\Writer; use Pterodactyl\Models\Server; use Pterodactyl\Models\Subuser; +use GuzzleHttp\Exception\RequestException; +use Illuminate\Database\ConnectionInterface; +use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Services\Subusers\SubuserDeletionService; -use Tests\TestCase; +use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; class SubuserDeletionServiceTest extends TestCase @@ -135,5 +135,4 @@ class SubuserDeletionServiceTest extends TestCase $this->assertEquals(trans('admin/exceptions.daemon_connection_failed', ['code' => 'E_CONN_REFUSED']), $exception->getMessage()); } } - } diff --git a/tests/Unit/Services/Subusers/SubuserUpdateServiceTest.php b/tests/Unit/Services/Subusers/SubuserUpdateServiceTest.php index f4742f9c..c053d57e 100644 --- a/tests/Unit/Services/Subusers/SubuserUpdateServiceTest.php +++ b/tests/Unit/Services/Subusers/SubuserUpdateServiceTest.php @@ -24,18 +24,18 @@ namespace Tests\Unit\Services\Subusers; -use GuzzleHttp\Exception\RequestException; -use Illuminate\Database\ConnectionInterface; -use Illuminate\Log\Writer; use Mockery as m; -use Pterodactyl\Contracts\Repository\PermissionRepositoryInterface; -use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; -use Pterodactyl\Exceptions\DisplayException; +use Tests\TestCase; +use Illuminate\Log\Writer; use Pterodactyl\Models\Server; use Pterodactyl\Models\Subuser; -use Pterodactyl\Services\Subusers\PermissionCreationService; +use GuzzleHttp\Exception\RequestException; +use Illuminate\Database\ConnectionInterface; +use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Services\Subusers\SubuserUpdateService; -use Tests\TestCase; +use Pterodactyl\Services\Subusers\PermissionCreationService; +use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; +use Pterodactyl\Contracts\Repository\PermissionRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; class SubuserUpdateServiceTest extends TestCase From 67ac36f5ce21de16fca455c3d0471b2ad865b1a2 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 27 Aug 2017 15:10:51 -0500 Subject: [PATCH 69/99] Refactor obscure service names to be clearer --- .../Controllers/Admin/NodesController.php | 24 +- .../Controllers/Admin/ServersController.php | 24 +- app/Http/Controllers/Admin/UserController.php | 24 +- .../Controllers/Base/AccountController.php | 5 +- ...ionService.php => NodeCreationService.php} | 2 +- ...ionService.php => NodeDeletionService.php} | 2 +- ...pdateService.php => NodeUpdateService.php} | 5 +- app/Services/Old/DeploymentService.php | 249 ------------------ ...Service.php => ReinstallServerService.php} | 2 +- ...nService.php => ServerCreationService.php} | 2 +- ...nService.php => ServerDeletionService.php} | 2 +- .../Subusers/SubuserCreationService.php | 6 +- ...ionService.php => UserCreationService.php} | 2 +- ...ionService.php => UserDeletionService.php} | 2 +- ...pdateService.php => UserUpdateService.php} | 2 +- ...ceTest.php => NodeCreationServiceTest.php} | 8 +- ...ceTest.php => NodeDeletionServiceTest.php} | 8 +- ...viceTest.php => NodeUpdateServiceTest.php} | 12 +- ...est.php => ReinstallServerServiceTest.php} | 8 +- ...Test.php => ServerCreationServiceTest.php} | 8 +- ...Test.php => ServerDeletionServiceTest.php} | 10 +- .../Subusers/SubuserCreationServiceTest.php | 6 +- ...ceTest.php => UserCreationServiceTest.php} | 8 +- ...ceTest.php => UserDeletionServiceTest.php} | 8 +- ...viceTest.php => UserUpdateServiceTest.php} | 8 +- 25 files changed, 96 insertions(+), 341 deletions(-) rename app/Services/Nodes/{CreationService.php => NodeCreationService.php} (98%) rename app/Services/Nodes/{DeletionService.php => NodeDeletionService.php} (99%) rename app/Services/Nodes/{UpdateService.php => NodeUpdateService.php} (94%) delete mode 100644 app/Services/Old/DeploymentService.php rename app/Services/Servers/{ReinstallService.php => ReinstallServerService.php} (99%) rename app/Services/Servers/{CreationService.php => ServerCreationService.php} (99%) rename app/Services/Servers/{DeletionService.php => ServerDeletionService.php} (99%) rename app/Services/Users/{CreationService.php => UserCreationService.php} (99%) rename app/Services/Users/{DeletionService.php => UserDeletionService.php} (99%) rename app/Services/Users/{UpdateService.php => UserUpdateService.php} (99%) rename tests/Unit/Services/Nodes/{CreationServiceTest.php => NodeCreationServiceTest.php} (90%) rename tests/Unit/Services/Nodes/{DeletionServiceTest.php => NodeDeletionServiceTest.php} (95%) rename tests/Unit/Services/Nodes/{UpdateServiceTest.php => NodeUpdateServiceTest.php} (95%) rename tests/Unit/Services/Servers/{ReinstallServiceTest.php => ReinstallServerServiceTest.php} (96%) rename tests/Unit/Services/Servers/{CreationServiceTest.php => ServerCreationServiceTest.php} (97%) rename tests/Unit/Services/Servers/{DeletionServiceTest.php => ServerDeletionServiceTest.php} (96%) rename tests/Unit/Services/Users/{CreationServiceTest.php => UserCreationServiceTest.php} (96%) rename tests/Unit/Services/Users/{DeletionServiceTest.php => UserDeletionServiceTest.php} (95%) rename tests/Unit/Services/Users/{UpdateServiceTest.php => UserUpdateServiceTest.php} (91%) diff --git a/app/Http/Controllers/Admin/NodesController.php b/app/Http/Controllers/Admin/NodesController.php index 24e7ce10..76cabdfe 100644 --- a/app/Http/Controllers/Admin/NodesController.php +++ b/app/Http/Controllers/Admin/NodesController.php @@ -29,9 +29,9 @@ use Illuminate\Http\Request; use Pterodactyl\Models\Node; use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Services\Nodes\UpdateService; -use Pterodactyl\Services\Nodes\CreationService; -use Pterodactyl\Services\Nodes\DeletionService; +use Pterodactyl\Services\Nodes\NodeUpdateService; +use Pterodactyl\Services\Nodes\NodeCreationService; +use Pterodactyl\Services\Nodes\NodeDeletionService; use Illuminate\Cache\Repository as CacheRepository; use Pterodactyl\Services\Allocations\AssignmentService; use Pterodactyl\Http\Requests\Admin\Node\NodeFormRequest; @@ -64,12 +64,12 @@ class NodesController extends Controller protected $cache; /** - * @var \Pterodactyl\Services\Nodes\CreationService + * @var \Pterodactyl\Services\Nodes\NodeCreationService */ protected $creationService; /** - * @var \Pterodactyl\Services\Nodes\DeletionService + * @var \Pterodactyl\Services\Nodes\NodeDeletionService */ protected $deletionService; @@ -84,7 +84,7 @@ class NodesController extends Controller protected $repository; /** - * @var \Pterodactyl\Services\Nodes\UpdateService + * @var \Pterodactyl\Services\Nodes\NodeUpdateService */ protected $updateService; @@ -95,22 +95,22 @@ class NodesController extends Controller * @param \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface $allocationRepository * @param \Pterodactyl\Services\Allocations\AssignmentService $assignmentService * @param \Illuminate\Cache\Repository $cache - * @param \Pterodactyl\Services\Nodes\CreationService $creationService - * @param \Pterodactyl\Services\Nodes\DeletionService $deletionService + * @param \Pterodactyl\Services\Nodes\NodeCreationService $creationService + * @param \Pterodactyl\Services\Nodes\NodeDeletionService $deletionService * @param \Pterodactyl\Contracts\Repository\LocationRepositoryInterface $locationRepository * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository - * @param \Pterodactyl\Services\Nodes\UpdateService $updateService + * @param \Pterodactyl\Services\Nodes\NodeUpdateService $updateService */ public function __construct( AlertsMessageBag $alert, AllocationRepositoryInterface $allocationRepository, AssignmentService $assignmentService, CacheRepository $cache, - CreationService $creationService, - DeletionService $deletionService, + NodeCreationService $creationService, + NodeDeletionService $deletionService, LocationRepositoryInterface $locationRepository, NodeRepositoryInterface $repository, - UpdateService $updateService + NodeUpdateService $updateService ) { $this->alert = $alert; $this->allocationRepository = $allocationRepository; diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php index c693bcb1..7557afb4 100644 --- a/app/Http/Controllers/Admin/ServersController.php +++ b/app/Http/Controllers/Admin/ServersController.php @@ -30,9 +30,9 @@ use Pterodactyl\Models\Server; use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Services\Servers\CreationService; -use Pterodactyl\Services\Servers\DeletionService; -use Pterodactyl\Services\Servers\ReinstallService; +use Pterodactyl\Services\Servers\ServerCreationService; +use Pterodactyl\Services\Servers\ServerDeletionService; +use Pterodactyl\Services\Servers\ReinstallServerService; use Pterodactyl\Services\Servers\SuspensionService; use Pterodactyl\Http\Requests\Admin\ServerFormRequest; use Pterodactyl\Services\Servers\ContainerRebuildService; @@ -92,7 +92,7 @@ class ServersController extends Controller protected $databaseHostRepository; /** - * @var \Pterodactyl\Services\Servers\DeletionService + * @var \Pterodactyl\Services\Servers\ServerDeletionService */ protected $deletionService; @@ -112,7 +112,7 @@ class ServersController extends Controller protected $nodeRepository; /** - * @var \Pterodactyl\Services\Servers\ReinstallService + * @var \Pterodactyl\Services\Servers\ReinstallServerService */ protected $reinstallService; @@ -122,7 +122,7 @@ class ServersController extends Controller protected $repository; /** - * @var \Pterodactyl\Services\Servers\CreationService + * @var \Pterodactyl\Services\Servers\ServerCreationService */ protected $service; @@ -149,15 +149,15 @@ class ServersController extends Controller * @param \Pterodactyl\Services\Servers\BuildModificationService $buildModificationService * @param \Illuminate\Contracts\Config\Repository $config * @param \Pterodactyl\Services\Servers\ContainerRebuildService $containerRebuildService - * @param \Pterodactyl\Services\Servers\CreationService $service + * @param \Pterodactyl\Services\Servers\ServerCreationService $service * @param \Pterodactyl\Services\Database\DatabaseManagementService $databaseManagementService * @param \Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface $databaseRepository * @param \Pterodactyl\Repositories\Eloquent\DatabaseHostRepository $databaseHostRepository - * @param \Pterodactyl\Services\Servers\DeletionService $deletionService + * @param \Pterodactyl\Services\Servers\ServerDeletionService $deletionService * @param \Pterodactyl\Services\Servers\DetailsModificationService $detailsModificationService * @param \Pterodactyl\Contracts\Repository\LocationRepositoryInterface $locationRepository * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $nodeRepository - * @param \Pterodactyl\Services\Servers\ReinstallService $reinstallService + * @param \Pterodactyl\Services\Servers\ReinstallServerService $reinstallService * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository * @param \Pterodactyl\Contracts\Repository\ServiceRepositoryInterface $serviceRepository * @param \Pterodactyl\Services\Servers\StartupModificationService $startupModificationService @@ -169,15 +169,15 @@ class ServersController extends Controller BuildModificationService $buildModificationService, ConfigRepository $config, ContainerRebuildService $containerRebuildService, - CreationService $service, + ServerCreationService $service, DatabaseManagementService $databaseManagementService, DatabaseRepositoryInterface $databaseRepository, DatabaseHostRepository $databaseHostRepository, - DeletionService $deletionService, + ServerDeletionService $deletionService, DetailsModificationService $detailsModificationService, LocationRepositoryInterface $locationRepository, NodeRepositoryInterface $nodeRepository, - ReinstallService $reinstallService, + ReinstallServerService $reinstallService, ServerRepositoryInterface $repository, ServiceRepositoryInterface $serviceRepository, StartupModificationService $startupModificationService, diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index 2daa4015..3aed7c02 100644 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -29,9 +29,9 @@ use Pterodactyl\Models\User; use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Services\Users\UpdateService; -use Pterodactyl\Services\Users\CreationService; -use Pterodactyl\Services\Users\DeletionService; +use Pterodactyl\Services\Users\UserUpdateService; +use Pterodactyl\Services\Users\UserCreationService; +use Pterodactyl\Services\Users\UserDeletionService; use Illuminate\Contracts\Translation\Translator; use Pterodactyl\Http\Requests\Admin\UserFormRequest; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; @@ -44,12 +44,12 @@ class UserController extends Controller protected $alert; /** - * @var \Pterodactyl\Services\Users\CreationService + * @var \Pterodactyl\Services\Users\UserCreationService */ protected $creationService; /** - * @var \Pterodactyl\Services\Users\DeletionService + * @var \Pterodactyl\Services\Users\UserDeletionService */ protected $deletionService; @@ -64,7 +64,7 @@ class UserController extends Controller protected $translator; /** - * @var \Pterodactyl\Services\Users\UpdateService + * @var \Pterodactyl\Services\Users\UserUpdateService */ protected $updateService; @@ -72,18 +72,18 @@ class UserController extends Controller * UserController constructor. * * @param \Prologue\Alerts\AlertsMessageBag $alert - * @param \Pterodactyl\Services\Users\CreationService $creationService - * @param \Pterodactyl\Services\Users\DeletionService $deletionService + * @param \Pterodactyl\Services\Users\UserCreationService $creationService + * @param \Pterodactyl\Services\Users\UserDeletionService $deletionService * @param \Illuminate\Contracts\Translation\Translator $translator - * @param \Pterodactyl\Services\Users\UpdateService $updateService + * @param \Pterodactyl\Services\Users\UserUpdateService $updateService * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository */ public function __construct( AlertsMessageBag $alert, - CreationService $creationService, - DeletionService $deletionService, + UserCreationService $creationService, + UserDeletionService $deletionService, Translator $translator, - UpdateService $updateService, + UserUpdateService $updateService, UserRepositoryInterface $repository ) { $this->alert = $alert; diff --git a/app/Http/Controllers/Base/AccountController.php b/app/Http/Controllers/Base/AccountController.php index 332ceadd..0c00b92c 100644 --- a/app/Http/Controllers/Base/AccountController.php +++ b/app/Http/Controllers/Base/AccountController.php @@ -30,11 +30,14 @@ use Alert; use Illuminate\Http\Request; use Pterodactyl\Models\User; use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Repositories\oldUserRepository; use Pterodactyl\Exceptions\DisplayValidationException; class AccountController extends Controller { + public function __construct() + { + } + /** * Display base account information page. * diff --git a/app/Services/Nodes/CreationService.php b/app/Services/Nodes/NodeCreationService.php similarity index 98% rename from app/Services/Nodes/CreationService.php rename to app/Services/Nodes/NodeCreationService.php index e2327c55..30f31a29 100644 --- a/app/Services/Nodes/CreationService.php +++ b/app/Services/Nodes/NodeCreationService.php @@ -26,7 +26,7 @@ namespace Pterodactyl\Services\Nodes; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; -class CreationService +class NodeCreationService { const DAEMON_SECRET_LENGTH = 18; diff --git a/app/Services/Nodes/DeletionService.php b/app/Services/Nodes/NodeDeletionService.php similarity index 99% rename from app/Services/Nodes/DeletionService.php rename to app/Services/Nodes/NodeDeletionService.php index f5180ce0..1a786822 100644 --- a/app/Services/Nodes/DeletionService.php +++ b/app/Services/Nodes/NodeDeletionService.php @@ -30,7 +30,7 @@ use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Exceptions\Service\HasActiveServersException; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; -class DeletionService +class NodeDeletionService { /** * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface diff --git a/app/Services/Nodes/UpdateService.php b/app/Services/Nodes/NodeUpdateService.php similarity index 94% rename from app/Services/Nodes/UpdateService.php rename to app/Services/Nodes/NodeUpdateService.php index 0b7f7b12..199d72f3 100644 --- a/app/Services/Nodes/UpdateService.php +++ b/app/Services/Nodes/NodeUpdateService.php @@ -31,7 +31,7 @@ use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface; -class UpdateService +class NodeUpdateService { /** * @var \Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface @@ -74,6 +74,7 @@ class UpdateService * * @throws \Pterodactyl\Exceptions\DisplayException * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function handle($node, array $data) { @@ -82,7 +83,7 @@ class UpdateService } if (! is_null(array_get($data, 'reset_secret'))) { - $data['daemonSecret'] = bin2hex(random_bytes(CreationService::DAEMON_SECRET_LENGTH)); + $data['daemonSecret'] = bin2hex(random_bytes(NodeCreationService::DAEMON_SECRET_LENGTH)); unset($data['reset_secret']); } diff --git a/app/Services/Old/DeploymentService.php b/app/Services/Old/DeploymentService.php deleted file mode 100644 index eed48c58..00000000 --- a/app/Services/Old/DeploymentService.php +++ /dev/null @@ -1,249 +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; - -use DB; -use Pterodactyl\Models\Node; -use Pterodactyl\Models\Server; -use Pterodactyl\Models\Location; -use Pterodactyl\Exceptions\AutoDeploymentException; - -class DeploymentService -{ - /** - * Eloquent model representing the allocation to use. - * - * @var \Pterodactyl\Models\Allocation - */ - protected $allocation; - - /** - * Amount of disk to be used by the server. - * - * @var int - */ - protected $disk; - - /** - * Amount of memory to be used by the sever. - * - * @var int - */ - protected $memory; - - /** - * Eloquent model representing the location to use. - * - * @var \Pterodactyl\Models\Location - */ - protected $location; - - /** - * Eloquent model representing the node to use. - * - * @var \Pterodactyl\Models\Node - */ - protected $node; - - /** - * Set the location to use when auto-deploying. - * - * @param int|\Pterodactyl\Models\Location $location - */ - public function setLocation($location) - { - $this->location = ($location instanceof Location) ? $location : Location::with('nodes')->findOrFail($location); - if (! $this->location->relationLoaded('nodes')) { - $this->location->load('nodes'); - } - - if (count($this->location->nodes) < 1) { - throw new AutoDeploymentException('The location provided does not contain any nodes and cannot be used.'); - } - - return $this; - } - - /** - * Set the node to use when auto-deploying. - * - * @param int|\Pterodactyl\Models\Node $node - */ - public function setNode($node) - { - $this->node = ($node instanceof Node) ? $node : Node::findOrFail($node); - if (! $this->node->relationLoaded('allocations')) { - $this->node->load('allocations'); - } - - $this->setLocation($this->node->location); - - return $this; - } - - /** - * Set the amount of disk space to be used by the new server. - * - * @param int $disk - */ - public function setDisk(int $disk) - { - $this->disk = $disk; - - return $this; - } - - /** - * Set the amount of memory to be used by the new server. - * - * @param int $memory - */ - public function setMemory(int $memory) - { - $this->memory = $memory; - - return $this; - } - - /** - * Return a random location model. - * - * @param array $exclude - */ - protected function findLocation(array $exclude = []) - { - $location = Location::with('nodes')->whereNotIn('id', $exclude)->inRandomOrder()->first(); - - if (! $location) { - throw new AutoDeploymentException('Unable to locate a suitable location to select a node from.'); - } - - if (count($location->nodes) < 1) { - return $this->findLocation(array_merge($exclude, [$location->id])); - } - - $this->setLocation($location); - } - - /** - * Return a model instance of a random node. - */ - protected function findNode(array $exclude = []) - { - if (! $this->location) { - $this->setLocation($this->findLocation()); - } - - $select = $this->location->nodes->whereNotIn('id', $exclude); - if (count($select) < 1) { - throw new AutoDeploymentException('Unable to find a suitable node within the assigned location with enough space.'); - } - - // Check usage, select new node if necessary - $this->setNode($select->random()); - if (! $this->checkNodeUsage()) { - return $this->findNode(array_merge($exclude, [$this->node()->id])); - } - } - - /** - * Checks that a node's allocation limits will not be passed - * with the assigned limits. - * - * @return bool - */ - protected function checkNodeUsage() - { - if (! $this->disk && ! $this->memory) { - return true; - } - - $totals = Server::select(DB::raw('SUM(memory) as memory, SUM(disk) as disk'))->where('node_id', $this->node()->id)->first(); - - if ($this->memory) { - $limit = ($this->node()->memory * (1 + ($this->node()->memory_overallocate / 100))); - - if (($totals->memory + $this->memory) > $limit) { - return false; - } - } - - if ($this->disk) { - $limit = ($this->node()->disk * (1 + ($this->node()->disk_overallocate / 100))); - - if (($totals->disk + $this->disk) > $limit) { - return false; - } - } - - return true; - } - - /** - * Return the assigned node for this auto-deployment. - * - * @return \Pterodactyl\Models\Node - */ - public function node() - { - return $this->node; - } - - /** - * Return the assigned location for this auto-deployment. - * - * @return \Pterodactyl\Models\Location - */ - public function location() - { - return $this->location; - } - - /** - * Return the assigned location for this auto-deployment. - * - * @return \Pterodactyl\Models\Allocation - */ - public function allocation() - { - return $this->allocation; - } - - /** - * Select and return the node to be used by the auto-deployment system. - */ - public function select() - { - if (! $this->node) { - $this->findNode(); - } - - // Set the Allocation - $this->allocation = $this->node()->allocations->where('server_id', null)->random(); - if (! $this->allocation) { - throw new AutoDeploymentException('Unable to find a suitable allocation to assign to this server.'); - } - } -} diff --git a/app/Services/Servers/ReinstallService.php b/app/Services/Servers/ReinstallServerService.php similarity index 99% rename from app/Services/Servers/ReinstallService.php rename to app/Services/Servers/ReinstallServerService.php index 4dc5d125..1aec0c7b 100644 --- a/app/Services/Servers/ReinstallService.php +++ b/app/Services/Servers/ReinstallServerService.php @@ -32,7 +32,7 @@ use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; -class ReinstallService +class ReinstallServerService { /** * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface diff --git a/app/Services/Servers/CreationService.php b/app/Services/Servers/ServerCreationService.php similarity index 99% rename from app/Services/Servers/CreationService.php rename to app/Services/Servers/ServerCreationService.php index baad7307..a2863424 100644 --- a/app/Services/Servers/CreationService.php +++ b/app/Services/Servers/ServerCreationService.php @@ -36,7 +36,7 @@ use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; -class CreationService +class ServerCreationService { /** * @var \Pterodactyl\Contracts\Repository\AllocationRepositoryInterface diff --git a/app/Services/Servers/DeletionService.php b/app/Services/Servers/ServerDeletionService.php similarity index 99% rename from app/Services/Servers/DeletionService.php rename to app/Services/Servers/ServerDeletionService.php index 850ca9ff..3f510baa 100644 --- a/app/Services/Servers/DeletionService.php +++ b/app/Services/Servers/ServerDeletionService.php @@ -34,7 +34,7 @@ use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; -class DeletionService +class ServerDeletionService { /** * @var \Illuminate\Database\ConnectionInterface diff --git a/app/Services/Subusers/SubuserCreationService.php b/app/Services/Subusers/SubuserCreationService.php index cdcd4c3a..78d99afb 100644 --- a/app/Services/Subusers/SubuserCreationService.php +++ b/app/Services/Subusers/SubuserCreationService.php @@ -29,7 +29,7 @@ use Pterodactyl\Models\Server; use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Services\Users\CreationService; +use Pterodactyl\Services\Users\UserCreationService; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; @@ -67,7 +67,7 @@ class SubuserCreationService protected $serverRepository; /** - * @var \Pterodactyl\Services\Users\CreationService + * @var \Pterodactyl\Services\Users\UserCreationService */ protected $userCreationService; @@ -83,7 +83,7 @@ class SubuserCreationService public function __construct( ConnectionInterface $connection, - CreationService $userCreationService, + UserCreationService $userCreationService, DaemonServerRepositoryInterface $daemonRepository, PermissionCreationService $permissionService, ServerRepositoryInterface $serverRepository, diff --git a/app/Services/Users/CreationService.php b/app/Services/Users/UserCreationService.php similarity index 99% rename from app/Services/Users/CreationService.php rename to app/Services/Users/UserCreationService.php index a5e5a55b..f0eee69b 100644 --- a/app/Services/Users/CreationService.php +++ b/app/Services/Users/UserCreationService.php @@ -32,7 +32,7 @@ use Pterodactyl\Notifications\AccountCreated; use Pterodactyl\Services\Helpers\TemporaryPasswordService; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; -class CreationService +class UserCreationService { /** * @var \Illuminate\Foundation\Application diff --git a/app/Services/Users/DeletionService.php b/app/Services/Users/UserDeletionService.php similarity index 99% rename from app/Services/Users/DeletionService.php rename to app/Services/Users/UserDeletionService.php index ab88068f..6a4b38e7 100644 --- a/app/Services/Users/DeletionService.php +++ b/app/Services/Users/UserDeletionService.php @@ -30,7 +30,7 @@ use Illuminate\Contracts\Translation\Translator; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; -class DeletionService +class UserDeletionService { /** * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface diff --git a/app/Services/Users/UpdateService.php b/app/Services/Users/UserUpdateService.php similarity index 99% rename from app/Services/Users/UpdateService.php rename to app/Services/Users/UserUpdateService.php index 5ba352f6..646b1940 100644 --- a/app/Services/Users/UpdateService.php +++ b/app/Services/Users/UserUpdateService.php @@ -27,7 +27,7 @@ namespace Pterodactyl\Services\Users; use Illuminate\Contracts\Hashing\Hasher; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; -class UpdateService +class UserUpdateService { /** * @var \Illuminate\Contracts\Hashing\Hasher diff --git a/tests/Unit/Services/Nodes/CreationServiceTest.php b/tests/Unit/Services/Nodes/NodeCreationServiceTest.php similarity index 90% rename from tests/Unit/Services/Nodes/CreationServiceTest.php rename to tests/Unit/Services/Nodes/NodeCreationServiceTest.php index 84efcbde..998ebe05 100644 --- a/tests/Unit/Services/Nodes/CreationServiceTest.php +++ b/tests/Unit/Services/Nodes/NodeCreationServiceTest.php @@ -27,10 +27,10 @@ namespace Tests\Unit\Services\Nodes; use Mockery as m; use Tests\TestCase; use phpmock\phpunit\PHPMock; -use Pterodactyl\Services\Nodes\CreationService; +use Pterodactyl\Services\Nodes\NodeCreationService; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; -class CreationServiceTest extends TestCase +class NodeCreationServiceTest extends TestCase { use PHPMock; @@ -40,7 +40,7 @@ class CreationServiceTest extends TestCase protected $repository; /** - * @var \Pterodactyl\Services\Nodes\CreationService + * @var \Pterodactyl\Services\Nodes\NodeCreationService */ protected $service; @@ -53,7 +53,7 @@ class CreationServiceTest extends TestCase $this->repository = m::mock(NodeRepositoryInterface::class); - $this->service = new CreationService($this->repository); + $this->service = new NodeCreationService($this->repository); } /** diff --git a/tests/Unit/Services/Nodes/DeletionServiceTest.php b/tests/Unit/Services/Nodes/NodeDeletionServiceTest.php similarity index 95% rename from tests/Unit/Services/Nodes/DeletionServiceTest.php rename to tests/Unit/Services/Nodes/NodeDeletionServiceTest.php index 845117db..5a93d1d3 100644 --- a/tests/Unit/Services/Nodes/DeletionServiceTest.php +++ b/tests/Unit/Services/Nodes/NodeDeletionServiceTest.php @@ -27,12 +27,12 @@ namespace Tests\Unit\Services\Nodes; use Mockery as m; use Tests\TestCase; use Pterodactyl\Models\Node; -use Pterodactyl\Services\Nodes\DeletionService; +use Pterodactyl\Services\Nodes\NodeDeletionService; use Illuminate\Contracts\Translation\Translator; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; -class DeletionServiceTest extends TestCase +class NodeDeletionServiceTest extends TestCase { /** * @var \Pterodactyl\Contracts\Repository\NodeRepositoryInterface @@ -50,7 +50,7 @@ class DeletionServiceTest extends TestCase protected $translator; /** - * @var \Pterodactyl\Services\Nodes\DeletionService + * @var \Pterodactyl\Services\Nodes\NodeDeletionService */ protected $service; @@ -65,7 +65,7 @@ class DeletionServiceTest extends TestCase $this->serverRepository = m::mock(ServerRepositoryInterface::class); $this->translator = m::mock(Translator::class); - $this->service = new DeletionService( + $this->service = new NodeDeletionService( $this->repository, $this->serverRepository, $this->translator diff --git a/tests/Unit/Services/Nodes/UpdateServiceTest.php b/tests/Unit/Services/Nodes/NodeUpdateServiceTest.php similarity index 95% rename from tests/Unit/Services/Nodes/UpdateServiceTest.php rename to tests/Unit/Services/Nodes/NodeUpdateServiceTest.php index 5b042edb..386e06fa 100644 --- a/tests/Unit/Services/Nodes/UpdateServiceTest.php +++ b/tests/Unit/Services/Nodes/NodeUpdateServiceTest.php @@ -32,12 +32,12 @@ use phpmock\phpunit\PHPMock; use Pterodactyl\Models\Node; use GuzzleHttp\Exception\RequestException; use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Services\Nodes\UpdateService; -use Pterodactyl\Services\Nodes\CreationService; +use Pterodactyl\Services\Nodes\NodeUpdateService; +use Pterodactyl\Services\Nodes\NodeCreationService; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\ConfigurationRepositoryInterface; -class UpdateServiceTest extends TestCase +class NodeUpdateServiceTest extends TestCase { use PHPMock; @@ -62,7 +62,7 @@ class UpdateServiceTest extends TestCase protected $repository; /** - * @var \Pterodactyl\Services\Nodes\UpdateService + * @var \Pterodactyl\Services\Nodes\NodeUpdateService */ protected $service; @@ -85,7 +85,7 @@ class UpdateServiceTest extends TestCase $this->repository = m::mock(NodeRepositoryInterface::class); $this->writer = m::mock(Writer::class); - $this->service = new UpdateService( + $this->service = new NodeUpdateService( $this->configRepository, $this->repository, $this->writer @@ -99,7 +99,7 @@ class UpdateServiceTest extends TestCase { $this->getFunctionMock('\\Pterodactyl\\Services\\Nodes', 'random_bytes') ->expects($this->once())->willReturnCallback(function ($bytes) { - $this->assertEquals(CreationService::DAEMON_SECRET_LENGTH, $bytes); + $this->assertEquals(NodeCreationService::DAEMON_SECRET_LENGTH, $bytes); return '\00'; }); diff --git a/tests/Unit/Services/Servers/ReinstallServiceTest.php b/tests/Unit/Services/Servers/ReinstallServerServiceTest.php similarity index 96% rename from tests/Unit/Services/Servers/ReinstallServiceTest.php rename to tests/Unit/Services/Servers/ReinstallServerServiceTest.php index d57a1b55..f1dbaecc 100644 --- a/tests/Unit/Services/Servers/ReinstallServiceTest.php +++ b/tests/Unit/Services/Servers/ReinstallServerServiceTest.php @@ -32,11 +32,11 @@ use Pterodactyl\Models\Server; use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Services\Servers\ReinstallService; +use Pterodactyl\Services\Servers\ReinstallServerService; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; -class ReinstallServiceTest extends TestCase +class ReinstallServerServiceTest extends TestCase { /** * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface @@ -64,7 +64,7 @@ class ReinstallServiceTest extends TestCase protected $server; /** - * @var \Pterodactyl\Services\Servers\ReinstallService + * @var \Pterodactyl\Services\Servers\ReinstallServerService */ protected $service; @@ -88,7 +88,7 @@ class ReinstallServiceTest extends TestCase $this->server = factory(Server::class)->make(['node_id' => 1]); - $this->service = new ReinstallService( + $this->service = new ReinstallServerService( $this->database, $this->daemonServerRepository, $this->repository, diff --git a/tests/Unit/Services/Servers/CreationServiceTest.php b/tests/Unit/Services/Servers/ServerCreationServiceTest.php similarity index 97% rename from tests/Unit/Services/Servers/CreationServiceTest.php rename to tests/Unit/Services/Servers/ServerCreationServiceTest.php index d9a5c245..f083e385 100644 --- a/tests/Unit/Services/Servers/CreationServiceTest.php +++ b/tests/Unit/Services/Servers/ServerCreationServiceTest.php @@ -32,7 +32,7 @@ use phpmock\phpunit\PHPMock; use Illuminate\Database\DatabaseManager; use GuzzleHttp\Exception\RequestException; use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Services\Servers\CreationService; +use Pterodactyl\Services\Servers\ServerCreationService; use Pterodactyl\Services\Servers\VariableValidatorService; use Pterodactyl\Services\Servers\UsernameGenerationService; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; @@ -42,7 +42,7 @@ use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerVariableRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; -class CreationServiceTest extends TestCase +class ServerCreationServiceTest extends TestCase { use PHPMock; @@ -106,7 +106,7 @@ class CreationServiceTest extends TestCase protected $serverVariableRepository; /** - * @var \Pterodactyl\Services\Servers\CreationService + * @var \Pterodactyl\Services\Servers\ServerCreationService */ protected $service; @@ -161,7 +161,7 @@ class CreationServiceTest extends TestCase $this->getFunctionMock('\\Ramsey\\Uuid\\Uuid', 'uuid4') ->expects($this->any())->willReturn('s'); - $this->service = new CreationService( + $this->service = new ServerCreationService( $this->allocationRepository, $this->daemonServerRepository, $this->database, diff --git a/tests/Unit/Services/Servers/DeletionServiceTest.php b/tests/Unit/Services/Servers/ServerDeletionServiceTest.php similarity index 96% rename from tests/Unit/Services/Servers/DeletionServiceTest.php rename to tests/Unit/Services/Servers/ServerDeletionServiceTest.php index f61b53a2..3a3cd548 100644 --- a/tests/Unit/Services/Servers/DeletionServiceTest.php +++ b/tests/Unit/Services/Servers/ServerDeletionServiceTest.php @@ -32,13 +32,13 @@ use Pterodactyl\Models\Server; use GuzzleHttp\Exception\RequestException; use Illuminate\Database\ConnectionInterface; use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Services\Servers\DeletionService; +use Pterodactyl\Services\Servers\ServerDeletionService; use Pterodactyl\Services\Database\DatabaseManagementService; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; -class DeletionServiceTest extends TestCase +class ServerDeletionServiceTest extends TestCase { /** * @var \Illuminate\Database\ConnectionInterface @@ -76,7 +76,7 @@ class DeletionServiceTest extends TestCase protected $repository; /** - * @var \Pterodactyl\Services\Servers\DeletionService + * @var \Pterodactyl\Services\Servers\ServerDeletionService */ protected $service; @@ -101,7 +101,7 @@ class DeletionServiceTest extends TestCase $this->repository = m::mock(ServerRepositoryInterface::class); $this->writer = m::mock(Writer::class); - $this->service = new DeletionService( + $this->service = new ServerDeletionService( $this->connection, $this->daemonServerRepository, $this->databaseRepository, @@ -118,7 +118,7 @@ class DeletionServiceTest extends TestCase { $response = $this->service->withForce(true); - $this->assertInstanceOf(DeletionService::class, $response); + $this->assertInstanceOf(ServerDeletionService::class, $response); } /** diff --git a/tests/Unit/Services/Subusers/SubuserCreationServiceTest.php b/tests/Unit/Services/Subusers/SubuserCreationServiceTest.php index bba981e1..42bae698 100644 --- a/tests/Unit/Services/Subusers/SubuserCreationServiceTest.php +++ b/tests/Unit/Services/Subusers/SubuserCreationServiceTest.php @@ -33,7 +33,7 @@ use Pterodactyl\Models\Server; use Pterodactyl\Models\Subuser; use Illuminate\Database\ConnectionInterface; use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Services\Users\CreationService; +use Pterodactyl\Services\Users\UserCreationService; use Pterodactyl\Services\Subusers\SubuserCreationService; use Pterodactyl\Services\Subusers\PermissionCreationService; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; @@ -78,7 +78,7 @@ class SubuserCreationServiceTest extends TestCase protected $service; /** - * @var \Pterodactyl\Services\Users\CreationService + * @var \Pterodactyl\Services\Users\UserCreationService */ protected $userCreationService; @@ -106,7 +106,7 @@ class SubuserCreationServiceTest extends TestCase $this->permissionService = m::mock(PermissionCreationService::class); $this->subuserRepository = m::mock(SubuserRepositoryInterface::class); $this->serverRepository = m::mock(ServerRepositoryInterface::class); - $this->userCreationService = m::mock(CreationService::class); + $this->userCreationService = m::mock(UserCreationService::class); $this->userRepository = m::mock(UserRepositoryInterface::class); $this->writer = m::mock(Writer::class); diff --git a/tests/Unit/Services/Users/CreationServiceTest.php b/tests/Unit/Services/Users/UserCreationServiceTest.php similarity index 96% rename from tests/Unit/Services/Users/CreationServiceTest.php rename to tests/Unit/Services/Users/UserCreationServiceTest.php index ac18c2ed..f4a78a70 100644 --- a/tests/Unit/Services/Users/CreationServiceTest.php +++ b/tests/Unit/Services/Users/UserCreationServiceTest.php @@ -31,11 +31,11 @@ use Illuminate\Contracts\Hashing\Hasher; use Illuminate\Database\ConnectionInterface; use Illuminate\Notifications\ChannelManager; use Pterodactyl\Notifications\AccountCreated; -use Pterodactyl\Services\Users\CreationService; +use Pterodactyl\Services\Users\UserCreationService; use Pterodactyl\Services\Helpers\TemporaryPasswordService; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; -class CreationServiceTest extends TestCase +class UserCreationServiceTest extends TestCase { /** * @var \Illuminate\Foundation\Application @@ -68,7 +68,7 @@ class CreationServiceTest extends TestCase protected $repository; /** - * @var \Pterodactyl\Services\Users\CreationService + * @var \Pterodactyl\Services\Users\UserCreationService */ protected $service; @@ -86,7 +86,7 @@ class CreationServiceTest extends TestCase $this->passwordService = m::mock(TemporaryPasswordService::class); $this->repository = m::mock(UserRepositoryInterface::class); - $this->service = new CreationService( + $this->service = new UserCreationService( $this->appMock, $this->notification, $this->database, diff --git a/tests/Unit/Services/Users/DeletionServiceTest.php b/tests/Unit/Services/Users/UserDeletionServiceTest.php similarity index 95% rename from tests/Unit/Services/Users/DeletionServiceTest.php rename to tests/Unit/Services/Users/UserDeletionServiceTest.php index a4e3cd1c..cd955f34 100644 --- a/tests/Unit/Services/Users/DeletionServiceTest.php +++ b/tests/Unit/Services/Users/UserDeletionServiceTest.php @@ -27,12 +27,12 @@ namespace Tests\Unit\Services\Users; use Mockery as m; use Tests\TestCase; use Pterodactyl\Models\User; -use Pterodactyl\Services\Users\DeletionService; +use Pterodactyl\Services\Users\UserDeletionService; use Illuminate\Contracts\Translation\Translator; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; -class DeletionServiceTest extends TestCase +class UserDeletionServiceTest extends TestCase { /** * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface @@ -50,7 +50,7 @@ class DeletionServiceTest extends TestCase protected $serverRepository; /** - * @var \Pterodactyl\Services\Users\DeletionService + * @var \Pterodactyl\Services\Users\UserDeletionService */ protected $service; @@ -71,7 +71,7 @@ class DeletionServiceTest extends TestCase $this->translator = m::mock(Translator::class); $this->serverRepository = m::mock(ServerRepositoryInterface::class); - $this->service = new DeletionService( + $this->service = new UserDeletionService( $this->serverRepository, $this->translator, $this->repository diff --git a/tests/Unit/Services/Users/UpdateServiceTest.php b/tests/Unit/Services/Users/UserUpdateServiceTest.php similarity index 91% rename from tests/Unit/Services/Users/UpdateServiceTest.php rename to tests/Unit/Services/Users/UserUpdateServiceTest.php index 2a5bd295..5ebf4c63 100644 --- a/tests/Unit/Services/Users/UpdateServiceTest.php +++ b/tests/Unit/Services/Users/UserUpdateServiceTest.php @@ -27,10 +27,10 @@ namespace Tests\Unit\Services\Users; use Mockery as m; use Tests\TestCase; use Illuminate\Contracts\Hashing\Hasher; -use Pterodactyl\Services\Users\UpdateService; +use Pterodactyl\Services\Users\UserUpdateService; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; -class UpdateServiceTest extends TestCase +class UserUpdateServiceTest extends TestCase { /** * @var \Illuminate\Contracts\Hashing\Hasher @@ -43,7 +43,7 @@ class UpdateServiceTest extends TestCase protected $repository; /** - * @var \Pterodactyl\Services\Users\UpdateService + * @var \Pterodactyl\Services\Users\UserUpdateService */ protected $service; @@ -57,7 +57,7 @@ class UpdateServiceTest extends TestCase $this->hasher = m::mock(Hasher::class); $this->repository = m::mock(UserRepositoryInterface::class); - $this->service = new UpdateService($this->hasher, $this->repository); + $this->service = new UserUpdateService($this->hasher, $this->repository); } /** From e045ef443a1d9bb04ebdd1db9fa6aa9ae4e312e8 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Wed, 30 Aug 2017 21:11:14 -0500 Subject: [PATCH 70/99] Should wrap up the base landing page stuff for accounts, next step is server rendering --- .php_cs | 4 + .../Daemon/ServerRepositoryInterface.php | 7 + .../Repository/RepositoryInterface.php | 3 +- .../Repository/ServerRepositoryInterface.php | 29 ++++ .../SessionRepositoryInterface.php} | 50 ++----- .../Base/InvalidPasswordProvidedException.php | 31 ++++ .../TwoFactorAuthenticationTokenInvalid.php | 29 ++++ app/Http/Controllers/Base/APIController.php | 59 ++++---- .../Controllers/Base/AccountController.php | 82 +++++------ app/Http/Controllers/Base/IndexController.php | 95 +++++++------ .../Controllers/Base/SecurityController.php | 109 ++++++++++----- .../Requests/Base/AccountDataFormRequest.php | 85 +++++++++++ .../ApiKeyFormRequest.php} | 10 +- ...equest.php => FrontendUserFormRequest.php} | 4 +- app/Models/User.php | 39 ++---- app/Providers/RepositoryServiceProvider.php | 9 ++ app/Repositories/Daemon/ServerRepository.php | 8 ++ .../Eloquent/ServerRepository.php | 70 ++++++++++ .../Eloquent/SessionRepository.php | 55 ++++++++ ...{KeyService.php => KeyCreationService.php} | 27 ++-- .../Servers/ServerAccessHelperService.php | 71 ++++++++++ app/Services/Users/ToggleTwoFactorService.php | 84 +++++++++++ app/Services/Users/TwoFactorSetupService.php | 91 ++++++++++++ app/Services/Users/UserUpdateService.php | 1 + resources/lang/en/base.php | 7 +- .../pterodactyl/base/security.blade.php | 50 ++++--- routes/base.php | 4 - .../Assertions/ControllerAssertionsTrait.php | 9 ++ .../Base/AccountControllerTest.php | 132 ++++++++++++++++++ ...iceTest.php => KeyCreationServiceTest.php} | 46 +++--- .../Users/ToggleTwoFactorServiceTest.php | 132 ++++++++++++++++++ .../Users/TwoFactorSetupServiceTest.php | 108 ++++++++++++++ 32 files changed, 1223 insertions(+), 317 deletions(-) rename app/{Http/Controllers/Base/LanguageController.php => Contracts/Repository/SessionRepositoryInterface.php} (51%) create mode 100644 app/Exceptions/Http/Base/InvalidPasswordProvidedException.php create mode 100644 app/Exceptions/Service/User/TwoFactorAuthenticationTokenInvalid.php create mode 100644 app/Http/Requests/Base/AccountDataFormRequest.php rename app/Http/Requests/{ApiKeyRequest.php => Base/ApiKeyFormRequest.php} (91%) rename app/Http/Requests/{BaseFormRequest.php => FrontendUserFormRequest.php} (94%) create mode 100644 app/Repositories/Eloquent/SessionRepository.php rename app/Services/Api/{KeyService.php => KeyCreationService.php} (89%) create mode 100644 app/Services/Servers/ServerAccessHelperService.php create mode 100644 app/Services/Users/ToggleTwoFactorService.php create mode 100644 app/Services/Users/TwoFactorSetupService.php create mode 100644 tests/Unit/Http/Controllers/Base/AccountControllerTest.php rename tests/Unit/Services/Api/{KeyServiceTest.php => KeyCreationServiceTest.php} (72%) create mode 100644 tests/Unit/Services/Users/ToggleTwoFactorServiceTest.php create mode 100644 tests/Unit/Services/Users/TwoFactorSetupServiceTest.php diff --git a/.php_cs b/.php_cs index aca934c8..fe3dfb56 100644 --- a/.php_cs +++ b/.php_cs @@ -25,6 +25,10 @@ return PhpCsFixer\Config::create() 'declare_equal_normalize' => ['space' => 'single'], 'heredoc_to_nowdoc' => true, 'linebreak_after_opening_tag' => true, + 'method_argument_space' => [ + 'ensure_fully_multiline' => false, + 'keep_multiple_spaces_after_comma' => false, + ], 'new_with_braces' => false, 'no_alias_functions' => true, 'no_multiline_whitespace_before_semicolons' => true, diff --git a/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php b/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php index 42bfb975..70373654 100644 --- a/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php +++ b/app/Contracts/Repository/Daemon/ServerRepositoryInterface.php @@ -88,4 +88,11 @@ interface ServerRepositoryInterface extends BaseRepositoryInterface * @return \Psr\Http\Message\ResponseInterface */ public function delete(); + + /** + * Return detials on a specific server. + * + * @return \Psr\Http\Message\ResponseInterface + */ + public function details(); } diff --git a/app/Contracts/Repository/RepositoryInterface.php b/app/Contracts/Repository/RepositoryInterface.php index 15c3a416..44450ea4 100644 --- a/app/Contracts/Repository/RepositoryInterface.php +++ b/app/Contracts/Repository/RepositoryInterface.php @@ -74,11 +74,12 @@ interface RepositoryInterface * * @param array $fields * @param bool $validate + * @param bool $force * @return mixed * * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function create(array $fields, $validate = true); + public function create(array $fields, $validate = true, $force = false); /** * Delete a given record from the database. diff --git a/app/Contracts/Repository/ServerRepositoryInterface.php b/app/Contracts/Repository/ServerRepositoryInterface.php index d71a349a..9413b160 100644 --- a/app/Contracts/Repository/ServerRepositoryInterface.php +++ b/app/Contracts/Repository/ServerRepositoryInterface.php @@ -87,4 +87,33 @@ interface ServerRepositoryInterface extends RepositoryInterface, SearchableInter * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function getDaemonServiceData($id); + + /** + * Return an array of server IDs that a given user can access based on owner and subuser permissions. + * + * @param int $user + * @return array + */ + public function getUserAccessServers($user); + + /** + * Return a paginated list of servers that a user can access at a given level. + * + * @param int $user + * @param string $level + * @param bool $admin + * @param array $relations + * @return \Illuminate\Pagination\LengthAwarePaginator + */ + public function filterUserAccessServers($user, $admin = false, $level = 'all', array $relations = []); + + /** + * Return a server by UUID. + * + * @param string $uuid + * @return \Illuminate\Database\Eloquent\Collection + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function getByUuid($uuid); } diff --git a/app/Http/Controllers/Base/LanguageController.php b/app/Contracts/Repository/SessionRepositoryInterface.php similarity index 51% rename from app/Http/Controllers/Base/LanguageController.php rename to app/Contracts/Repository/SessionRepositoryInterface.php index 0addd218..a4f0f3e9 100644 --- a/app/Http/Controllers/Base/LanguageController.php +++ b/app/Contracts/Repository/SessionRepositoryInterface.php @@ -1,5 +1,5 @@ . * @@ -22,50 +22,24 @@ * SOFTWARE. */ -namespace Pterodactyl\Http\Controllers\Base; +namespace Pterodactyl\Contracts\Repository; -use Auth; -use Session; -use Illuminate\Http\Request; -use Pterodactyl\Models\User; -use Pterodactyl\Http\Controllers\Controller; - -class LanguageController extends Controller +interface SessionRepositoryInterface extends RepositoryInterface { /** - * A list of supported languages on the panel. + * Delete a session for a given user. * - * @var array + * @param int $user + * @param int $session + * @return null|int */ - protected $languages = [ - 'de' => 'German', - 'en' => 'English', - 'et' => 'Estonian', - 'nb' => 'Norwegian', - 'nl' => 'Dutch', - 'pt' => 'Portuguese', - 'ro' => 'Romanian', - 'ru' => 'Russian', - ]; + public function deleteUserSession($user, $session); /** - * Sets the language for a user. + * Return all of the active sessions for a user. * - * @param \Illuminate\Http\Request $request - * @param string $language - * @return \Illuminate\Http\RedirectResponse + * @param int $user + * @return \Illuminate\Support\Collection */ - public function setLanguage(Request $request, $language) - { - if (array_key_exists($language, $this->languages)) { - if (Auth::check()) { - $user = User::findOrFail(Auth::user()->id); - $user->language = $language; - $user->save(); - } - Session::put('applocale', $language); - } - - return redirect()->back(); - } + public function getUserSessions($user); } diff --git a/app/Exceptions/Http/Base/InvalidPasswordProvidedException.php b/app/Exceptions/Http/Base/InvalidPasswordProvidedException.php new file mode 100644 index 00000000..3b4fce10 --- /dev/null +++ b/app/Exceptions/Http/Base/InvalidPasswordProvidedException.php @@ -0,0 +1,31 @@ +. + * + * 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\Exceptions\Http\Base; + +use Pterodactyl\Exceptions\DisplayException; + +class InvalidPasswordProvidedException extends DisplayException +{ +} diff --git a/app/Exceptions/Service/User/TwoFactorAuthenticationTokenInvalid.php b/app/Exceptions/Service/User/TwoFactorAuthenticationTokenInvalid.php new file mode 100644 index 00000000..1e7c6483 --- /dev/null +++ b/app/Exceptions/Service/User/TwoFactorAuthenticationTokenInvalid.php @@ -0,0 +1,29 @@ +. + * + * 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\Exceptions\Service\User; + +class TwoFactorAuthenticationTokenInvalid extends \Exception +{ +} diff --git a/app/Http/Controllers/Base/APIController.php b/app/Http/Controllers/Base/APIController.php index 7d7e3952..72a4e7b6 100644 --- a/app/Http/Controllers/Base/APIController.php +++ b/app/Http/Controllers/Base/APIController.php @@ -27,12 +27,11 @@ namespace Pterodactyl\Http\Controllers\Base; use Illuminate\Http\Request; use Prologue\Alerts\AlertsMessageBag; +use Pterodactyl\Http\Requests\Base\ApiKeyFormRequest; use Pterodactyl\Models\APIPermission; -use Pterodactyl\Services\ApiKeyService; use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Http\Requests\ApiKeyRequest; -use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; +use Pterodactyl\Services\Api\KeyCreationService; class APIController extends Controller { @@ -41,31 +40,31 @@ class APIController extends Controller */ protected $alert; + /** + * @var \Pterodactyl\Services\Api\KeyCreationService + */ + protected $keyService; + /** * @var \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface */ protected $repository; - /** - * @var \Pterodactyl\Services\ApiKeyService - */ - protected $service; - /** * APIController constructor. * * @param \Prologue\Alerts\AlertsMessageBag $alert * @param \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface $repository - * @param \Pterodactyl\Services\ApiKeyService $service + * @param \Pterodactyl\Services\Api\KeyCreationService $keyService */ public function __construct( AlertsMessageBag $alert, ApiKeyRepositoryInterface $repository, - ApiKeyService $service + KeyCreationService $keyService ) { $this->alert = $alert; + $this->keyService = $keyService; $this->repository = $repository; - $this->service = $service; } /** @@ -73,6 +72,8 @@ class APIController extends Controller * * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function index(Request $request) { @@ -84,14 +85,15 @@ class APIController extends Controller /** * Display API key creation page. * + * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ - public function create() + public function create(Request $request) { return view('base.api.new', [ 'permissions' => [ 'user' => collect(APIPermission::CONST_PERMISSIONS)->pull('_user'), - 'admin' => collect(APIPermission::CONST_PERMISSIONS)->except('_user')->toArray(), + 'admin' => ! $request->user()->root_admin ?: collect(APIPermission::CONST_PERMISSIONS)->except('_user')->toArray(), ], ]); } @@ -99,30 +101,25 @@ class APIController extends Controller /** * Handle saving new API key. * - * @param \Pterodactyl\Http\Requests\ApiKeyRequest $request + * @param \Pterodactyl\Http\Requests\Base\ApiKeyFormRequest $request * @return \Illuminate\Http\RedirectResponse - * * @throws \Exception * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function store(ApiKeyRequest $request) + public function store(ApiKeyFormRequest $request) { $adminPermissions = []; - if ($request->user()->isRootAdmin()) { + if ($request->user()->root_admin) { $adminPermissions = $request->input('admin_permissions') ?? []; } - $secret = $this->service->create([ + $secret = $this->keyService->handle([ 'user_id' => $request->user()->id, 'allowed_ips' => $request->input('allowed_ips'), 'memo' => $request->input('memo'), - ], $request->input('permissions') ?? [], $adminPermissions); + ], $request->input('permissions', []), $adminPermissions); - $this->alert->success( - "An API Key-Pair has successfully been generated. The API secret - for this public key is shown below and will not be shown again. -

    {$secret}" - )->flash(); + $this->alert->success(trans('base.api.index.keypair_created', ['token' => $secret]))->flash(); return redirect()->route('account.api'); } @@ -136,16 +133,10 @@ class APIController extends Controller */ public function revoke(Request $request, $key) { - try { - $key = $this->repository->withColumns('id')->findFirstWhere([ - ['user_id', '=', $request->user()->id], - ['public', $key], - ]); - - $this->service->revoke($key->id); - } catch (RecordNotFoundException $ex) { - return abort(404); - } + $this->repository->deleteWhere([ + ['user_id', '=', $request->user()->id], + ['public', '=', $key], + ]); return response('', 204); } diff --git a/app/Http/Controllers/Base/AccountController.php b/app/Http/Controllers/Base/AccountController.php index 0c00b92c..102850ed 100644 --- a/app/Http/Controllers/Base/AccountController.php +++ b/app/Http/Controllers/Base/AccountController.php @@ -25,83 +25,69 @@ namespace Pterodactyl\Http\Controllers\Base; -use Log; -use Alert; -use Illuminate\Http\Request; -use Pterodactyl\Models\User; +use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Exceptions\DisplayValidationException; +use Pterodactyl\Http\Requests\Base\AccountDataFormRequest; +use Pterodactyl\Services\Users\UserUpdateService; class AccountController extends Controller { - public function __construct() - { + /** + * @var \Prologue\Alerts\AlertsMessageBag + */ + protected $alert; + + /** + * @var \Pterodactyl\Services\Users\UserUpdateService + */ + protected $updateService; + + /** + * AccountController constructor. + * + * @param \Prologue\Alerts\AlertsMessageBag $alert + * @param \Pterodactyl\Services\Users\UserUpdateService $updateService + */ + public function __construct( + AlertsMessageBag $alert, + UserUpdateService $updateService + ) { + $this->alert = $alert; + $this->updateService = $updateService; } /** * Display base account information page. * - * @param \Illuminate\Http\Request $request * @return \Illuminate\View\View */ - public function index(Request $request) + public function index() { return view('base.account'); } /** - * Update details for a users account. + * Update details for a user's account. * - * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Http\Requests\Base\AccountDataFormRequest $request * @return \Illuminate\Http\RedirectResponse - * @throws \Symfony\Component\HttpKernel\Exception\HttpException + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ - public function update(Request $request) + public function update(AccountDataFormRequest $request) { $data = []; - - // Request to update account Password if ($request->input('do_action') === 'password') { - $this->validate($request, [ - 'current_password' => 'required', - 'new_password' => 'required|confirmed|' . User::PASSWORD_RULES, - 'new_password_confirmation' => 'required', - ]); - $data['password'] = $request->input('new_password'); - - // Request to update account Email } elseif ($request->input('do_action') === 'email') { $data['email'] = $request->input('new_email'); - - // Request to update account Identity } elseif ($request->input('do_action') === 'identity') { $data = $request->only(['name_first', 'name_last', 'username']); - - // Unknown, hit em with a 404 - } else { - return abort(404); } - if ( - in_array($request->input('do_action'), ['email', 'password']) - && ! password_verify($request->input('current_password'), $request->user()->password) - ) { - Alert::danger(trans('base.account.invalid_pass'))->flash(); - - return redirect()->route('account'); - } - - try { - $repo = new oldUserRepository; - $repo->update($request->user()->id, $data); - Alert::success('Your account details were successfully updated.')->flash(); - } catch (DisplayValidationException $ex) { - return redirect()->route('account')->withErrors(json_decode($ex->getMessage())); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger(trans('base.account.exception'))->flash(); - } + $this->updateService->handle($request->user()->id, $data); + $this->alert->success(trans('base.account.details_updated'))->flash(); return redirect()->route('account'); } diff --git a/app/Http/Controllers/Base/IndexController.php b/app/Http/Controllers/Base/IndexController.php index e9d9e768..50416300 100644 --- a/app/Http/Controllers/Base/IndexController.php +++ b/app/Http/Controllers/Base/IndexController.php @@ -26,11 +26,45 @@ namespace Pterodactyl\Http\Controllers\Base; use Illuminate\Http\Request; -use Pterodactyl\Models\Server; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Http\Controllers\Controller; +use Pterodactyl\Services\Servers\ServerAccessHelperService; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; class IndexController extends Controller { + /** + * @var \Pterodactyl\Services\Servers\ServerAccessHelperService + */ + protected $access; + + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface + */ + protected $daemonRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $repository; + + /** + * IndexController constructor. + * + * @param \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface $daemonRepository + * @param \Pterodactyl\Services\Servers\ServerAccessHelperService $access + * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository + */ + public function __construct( + DaemonServerRepositoryInterface $daemonRepository, + ServerAccessHelperService $access, + ServerRepositoryInterface $repository + ) { + $this->access = $access; + $this->daemonRepository = $daemonRepository; + $this->repository = $repository; + } + /** * Returns listing of user's servers. * @@ -39,38 +73,11 @@ class IndexController extends Controller */ public function getIndex(Request $request) { - $servers = $request->user()->access()->with('user'); + $servers = $this->repository->search($request->input('query'))->filterUserAccessServers( + $request->user()->id, $request->user()->root_admin, 'all', ['user'] + ); - if (! is_null($request->input('query'))) { - $servers->search($request->input('query')); - } - - return view('base.index', [ - 'servers' => $servers->paginate(config('pterodactyl.paginate.frontend.servers')), - ]); - } - - /** - * Generate a random string. - * - * @param \Illuminate\Http\Request $request - * @param int $length - * @return string - * @deprecated - */ - public function getPassword(Request $request, $length = 16) - { - $length = ($length < 8) ? 8 : $length; - - $returnable = false; - while (! $returnable) { - $generated = str_random($length); - if (preg_match('/[A-Z]+[a-z]+[0-9]+/', $generated)) { - $returnable = true; - } - } - - return $generated; + return view('base.index', ['servers' => $servers]); } /** @@ -79,31 +86,23 @@ class IndexController extends Controller * @param \Illuminate\Http\Request $request * @param string $uuid * @return \Illuminate\Http\JsonResponse + * @throws \Exception */ public function status(Request $request, $uuid) { - $server = Server::byUuid($uuid); - - if (! $server) { - return response()->json([], 404); - } + $server = $this->access->handle($uuid, $request->user()); if (! $server->installed) { return response()->json(['status' => 20]); - } - - if ($server->suspended) { + } elseif ($server->suspended) { return response()->json(['status' => 30]); } - try { - $res = $server->guzzleClient()->request('GET', '/server'); - if ($res->getStatusCode() === 200) { - return response()->json(json_decode($res->getBody())); - } - } catch (\Exception $e) { - } + $response = $this->daemonRepository->setNode($server->node_id) + ->setAccessServer($server->uuid) + ->setAccessToken($server->daemonSecret) + ->details(); - return response()->json([]); + return response()->json(json_decode($response->getBody())); } } diff --git a/app/Http/Controllers/Base/SecurityController.php b/app/Http/Controllers/Base/SecurityController.php index 5a143e65..b44e6e0f 100644 --- a/app/Http/Controllers/Base/SecurityController.php +++ b/app/Http/Controllers/Base/SecurityController.php @@ -25,14 +25,64 @@ namespace Pterodactyl\Http\Controllers\Base; -use Alert; -use Google2FA; +use Illuminate\Contracts\Config\Repository as ConfigRepository; +use Illuminate\Contracts\Session\Session; use Illuminate\Http\Request; -use Pterodactyl\Models\Session; +use Prologue\Alerts\AlertsMessageBag; +use Pterodactyl\Contracts\Repository\SessionRepositoryInterface; +use Pterodactyl\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid; use Pterodactyl\Http\Controllers\Controller; +use Pterodactyl\Services\Users\ToggleTwoFactorService; +use Pterodactyl\Services\Users\TwoFactorSetupService; class SecurityController extends Controller { + /** + * @var \Prologue\Alerts\AlertsMessageBag + */ + protected $alert; + + /** + * @var \Illuminate\Contracts\Config\Repository + */ + protected $config; + + /** + * @var \Pterodactyl\Contracts\Repository\SessionRepositoryInterface + */ + protected $repository; + + /** + * @var \Illuminate\Contracts\Session\Session + */ + protected $session; + + /** + * @var \Pterodactyl\Services\Users\ToggleTwoFactorService + */ + protected $toggleTwoFactorService; + + /** + * @var \Pterodactyl\Services\Users\TwoFactorSetupService + */ + protected $twoFactorSetupService; + + public function __construct( + AlertsMessageBag $alert, + ConfigRepository $config, + Session $session, + SessionRepositoryInterface $repository, + ToggleTwoFactorService $toggleTwoFactorService, + TwoFactorSetupService $twoFactorSetupService + ) { + $this->alert = $alert; + $this->config = $config; + $this->repository = $repository; + $this->session = $session; + $this->toggleTwoFactorService = $toggleTwoFactorService; + $this->twoFactorSetupService = $twoFactorSetupService; + } + /** * Returns Security Management Page. * @@ -41,8 +91,12 @@ class SecurityController extends Controller */ public function index(Request $request) { + if ($this->config->get('session.driver') === 'database') { + $activeSessions = $this->repository->getUserSessions($request->user()->id); + } + return view('base.security', [ - 'sessions' => Session::where('user_id', $request->user()->id)->get(), + 'sessions' => $activeSessions ?? null, ]); } @@ -52,22 +106,13 @@ class SecurityController extends Controller * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\JsonResponse + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function generateTotp(Request $request) { - $user = $request->user(); - - $user->totp_secret = Google2FA::generateSecretKey(); - $user->save(); - - return response()->json([ - 'qrImage' => Google2FA::getQRCodeGoogleUrl( - 'Pterodactyl', - $user->email, - $user->totp_secret - ), - 'secret' => $user->totp_secret, - ]); + return response()->json($this->twoFactorSetupService->handle($request->user())); } /** @@ -78,18 +123,13 @@ class SecurityController extends Controller */ public function setTotp(Request $request) { - if (! $request->has('token')) { - return response()->json([ - 'error' => 'Request is missing token parameter.', - ], 500); - } + try { + $this->toggleTwoFactorService->handle($request->user(), $request->input('token')); - $user = $request->user(); - if ($user->toggleTotp($request->input('token'))) { return response('true'); + } catch (TwoFactorAuthenticationTokenInvalid $exception) { + return response('false'); } - - return response('false'); } /** @@ -100,19 +140,12 @@ class SecurityController extends Controller */ public function disableTotp(Request $request) { - if (! $request->has('token')) { - Alert::danger('Missing required `token` field in request.')->flash(); - - return redirect()->route('account.security'); + try { + $this->toggleTwoFactorService->handle($request->user(), $request->input('token'), false); + } catch (TwoFactorAuthenticationTokenInvalid $exception) { + $this->alert->danger(trans('base.security.2fa_disable_error'))->flash(); } - $user = $request->user(); - if ($user->toggleTotp($request->input('token'))) { - return redirect()->route('account.security'); - } - - Alert::danger('The TOTP token provided was invalid.')->flash(); - return redirect()->route('account.security'); } @@ -125,7 +158,7 @@ class SecurityController extends Controller */ public function revoke(Request $request, $id) { - Session::where('user_id', $request->user()->id)->findOrFail($id)->delete(); + $this->repository->deleteUserSession($request->user()->id, $id); return redirect()->route('account.security'); } diff --git a/app/Http/Requests/Base/AccountDataFormRequest.php b/app/Http/Requests/Base/AccountDataFormRequest.php new file mode 100644 index 00000000..a9573106 --- /dev/null +++ b/app/Http/Requests/Base/AccountDataFormRequest.php @@ -0,0 +1,85 @@ +. + * + * 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\Http\Requests\Base; + +use Pterodactyl\Exceptions\Http\Base\InvalidPasswordProvidedException; +use Pterodactyl\Models\User; +use Pterodactyl\Http\Requests\FrontendUserFormRequest; + +class AccountDataFormRequest extends FrontendUserFormRequest +{ + /** + * @return bool + * @throws \Pterodactyl\Exceptions\Http\Base\InvalidPasswordProvidedException + */ + public function authorize() + { + if (! parent::authorize()) { + return false; + } + + // Verify password matches when changing password or email. + if (in_array($this->input('do_action'), ['password', 'email'])) { + if (! password_verify($this->input('current_password'), $this->user()->password)) { + throw new InvalidPasswordProvidedException(trans('base.account.invalid_password')); + } + } + + return true; + } + + /** + * @return array + */ + public function rules() + { + $modelRules = User::getUpdateRulesForId($this->user()->id); + + switch ($this->input('do_action')) { + case 'email': + $rules = [ + 'new_email' => array_get($modelRules, 'email'), + ]; + break; + case 'password': + $rules = [ + 'new_password' => 'required|confirmed|string|min:8', + 'new_password_confirmation' => 'required', + ]; + break; + case 'identity': + $rules = [ + 'name_first' => array_get($modelRules, 'name_first'), + 'name_last' => array_get($modelRules, 'name_last'), + 'username' => array_get($modelRules, 'username'), + ]; + break; + default: + abort(422); + } + + return $rules; + } +} diff --git a/app/Http/Requests/ApiKeyRequest.php b/app/Http/Requests/Base/ApiKeyFormRequest.php similarity index 91% rename from app/Http/Requests/ApiKeyRequest.php rename to app/Http/Requests/Base/ApiKeyFormRequest.php index 52b8f90e..33b2541c 100644 --- a/app/Http/Requests/ApiKeyRequest.php +++ b/app/Http/Requests/Base/ApiKeyFormRequest.php @@ -22,11 +22,12 @@ * SOFTWARE. */ -namespace Pterodactyl\Http\Requests; +namespace Pterodactyl\Http\Requests\Base; use IPTools\Network; +use Pterodactyl\Http\Requests\FrontendUserFormRequest; -class ApiKeyRequest extends BaseFormRequest +class ApiKeyFormRequest extends FrontendUserFormRequest { /** * Rules applied to data passed in this request. @@ -58,7 +59,7 @@ class ApiKeyRequest extends BaseFormRequest } } - $this->merge(['allowed_ips' => $loop], $this->except('allowed_ips')); + $this->merge(['allowed_ips' => $loop]); } /** @@ -69,12 +70,11 @@ class ApiKeyRequest extends BaseFormRequest public function withValidator($validator) { $validator->after(function ($validator) { + /* @var \Illuminate\Validation\Validator $validator */ if (empty($this->input('permissions')) && empty($this->input('admin_permissions'))) { $validator->errors()->add('permissions', 'At least one permission must be selected.'); } - }); - $validator->after(function ($validator) { foreach ($this->input('allowed_ips') as $ip) { $ip = trim($ip); diff --git a/app/Http/Requests/BaseFormRequest.php b/app/Http/Requests/FrontendUserFormRequest.php similarity index 94% rename from app/Http/Requests/BaseFormRequest.php rename to app/Http/Requests/FrontendUserFormRequest.php index 7d5274bb..404003c3 100644 --- a/app/Http/Requests/BaseFormRequest.php +++ b/app/Http/Requests/FrontendUserFormRequest.php @@ -26,8 +26,10 @@ namespace Pterodactyl\Http\Requests; use Illuminate\Foundation\Http\FormRequest; -class BaseFormRequest extends FormRequest +abstract class FrontendUserFormRequest extends FormRequest { + abstract public function rules(); + /** * Determine if a user is authorized to access this endpoint. * diff --git a/app/Models/User.php b/app/Models/User.php index d9f99d01..a3493522 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -50,21 +50,6 @@ class User extends Model implements { use Authenticatable, Authorizable, CanResetPassword, Eloquence, Notifiable, Validable; - /** - * The rules for user passwords. - * - * @var string - * @deprecated - */ - const PASSWORD_RULES = 'regex:((?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,})'; - - /** - * The regex rules for usernames. - * - * @var string - */ - const USERNAME_RULES = 'regex:/^([\w\d\.\-]{1,255})$/'; - /** * Level of servers to display when using access() on a user. * @@ -92,9 +77,9 @@ class User extends Model implements * @var array */ protected $casts = [ - 'root_admin' => 'integer', - 'use_totp' => 'integer', - 'gravatar' => 'integer', + 'root_admin' => 'boolean', + 'use_totp' => 'boolean', + 'gravatar' => 'boolean', ]; /** @@ -135,11 +120,11 @@ class User extends Model implements * @var array */ protected static $applicationRules = [ - 'email' => 'required|email', - 'username' => 'required|alpha_dash', - 'name_first' => 'required|string', - 'name_last' => 'required|string', - 'password' => 'sometimes|regex:((?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,})', + 'email' => 'required', + 'username' => 'required', + 'name_first' => 'required', + 'name_last' => 'required', + 'password' => 'sometimes', ]; /** @@ -148,10 +133,10 @@ class User extends Model implements * @var array */ protected static $dataIntegrityRules = [ - 'email' => 'unique:users,email', - 'username' => 'between:1,255|unique:users,username', - 'name_first' => 'between:1,255', - 'name_last' => 'between:1,255', + 'email' => 'email|unique:users,email', + 'username' => 'alpha_dash|between:1,255|unique:users,username', + 'name_first' => 'string|between:1,255', + 'name_last' => 'string|between:1,255', 'password' => 'nullable|string', 'root_admin' => 'boolean', 'language' => 'string|between:2,5', diff --git a/app/Providers/RepositoryServiceProvider.php b/app/Providers/RepositoryServiceProvider.php index a0a85fae..d30ae322 100644 --- a/app/Providers/RepositoryServiceProvider.php +++ b/app/Providers/RepositoryServiceProvider.php @@ -25,10 +25,16 @@ namespace Pterodactyl\Providers; use Illuminate\Support\ServiceProvider; +use Pterodactyl\Contracts\Repository\PermissionRepositoryInterface; +use Pterodactyl\Contracts\Repository\SessionRepositoryInterface; +use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; use Pterodactyl\Repositories\Daemon\FileRepository; use Pterodactyl\Repositories\Daemon\PowerRepository; use Pterodactyl\Repositories\Eloquent\NodeRepository; use Pterodactyl\Repositories\Eloquent\PackRepository; +use Pterodactyl\Repositories\Eloquent\PermissionRepository; +use Pterodactyl\Repositories\Eloquent\SessionRepository; +use Pterodactyl\Repositories\Eloquent\SubuserRepository; use Pterodactyl\Repositories\Eloquent\UserRepository; use Pterodactyl\Repositories\Daemon\CommandRepository; use Pterodactyl\Repositories\Eloquent\ApiKeyRepository; @@ -83,11 +89,14 @@ class RepositoryServiceProvider extends ServiceProvider $this->app->bind(NodeRepositoryInterface::class, NodeRepository::class); $this->app->bind(OptionVariableRepositoryInterface::class, OptionVariableRepository::class); $this->app->bind(PackRepositoryInterface::class, PackRepository::class); + $this->app->bind(PermissionRepositoryInterface::class, PermissionRepository::class); $this->app->bind(ServerRepositoryInterface::class, ServerRepository::class); $this->app->bind(ServerVariableRepositoryInterface::class, ServerVariableRepository::class); $this->app->bind(ServiceRepositoryInterface::class, ServiceRepository::class); $this->app->bind(ServiceOptionRepositoryInterface::class, ServiceOptionRepository::class); $this->app->bind(ServiceVariableRepositoryInterface::class, ServiceVariableRepository::class); + $this->app->bind(SessionRepositoryInterface::class, SessionRepository::class); + $this->app->bind(SubuserRepositoryInterface::class, SubuserRepository::class); $this->app->bind(UserRepositoryInterface::class, UserRepository::class); // Daemon Repositories diff --git a/app/Repositories/Daemon/ServerRepository.php b/app/Repositories/Daemon/ServerRepository.php index 594bb175..db2f31e6 100644 --- a/app/Repositories/Daemon/ServerRepository.php +++ b/app/Repositories/Daemon/ServerRepository.php @@ -161,4 +161,12 @@ class ServerRepository extends BaseRepository implements ServerRepositoryInterfa { return $this->getHttpClient()->request('DELETE', '/servers'); } + + /** + * {@inheritdoc} + */ + public function details() + { + return $this->getHttpClient()->request('GET', '/servers'); + } } diff --git a/app/Repositories/Eloquent/ServerRepository.php b/app/Repositories/Eloquent/ServerRepository.php index 05ba8039..ec89052b 100644 --- a/app/Repositories/Eloquent/ServerRepository.php +++ b/app/Repositories/Eloquent/ServerRepository.php @@ -28,6 +28,7 @@ use Pterodactyl\Models\Server; use Pterodactyl\Repositories\Concerns\Searchable; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Webmozart\Assert\Assert; class ServerRepository extends EloquentRepository implements ServerRepositoryInterface { @@ -149,4 +150,73 @@ class ServerRepository extends EloquentRepository implements ServerRepositoryInt 'pack' => (! is_null($instance->pack_id)) ? $instance->pack->uuid : null, ]; } + + /** + * {@inheritdoc} + */ + public function getUserAccessServers($user) + { + Assert::numeric($user, 'First argument passed to getUserAccessServers must be numeric, received %s.'); + + $subuser = $this->app->make(SubuserRepository::class); + + return $this->getBuilder()->select('id')->where('owner_id', $user)->union( + $subuser->getBuilder()->select('server_id')->where('user_id', $user) + )->pluck('id')->all(); + } + + /** + * {@inheritdoc} + */ + public function filterUserAccessServers($user, $admin = false, $level = 'all', array $relations = []) + { + Assert::numeric($user, 'First argument passed to filterUserAccessServers must be numeric, received %s.'); + Assert::boolean($admin, 'Second argument passed to filterUserAccessServers must be boolean, received %s.'); + Assert::stringNotEmpty($level, 'Third argument passed to filterUserAccessServers must be a non-empty string, received %s.'); + + $instance = $this->getBuilder()->with($relations); + + // If access level is set to owner, only display servers + // that the user owns. + if ($level === 'owner') { + $instance->where('owner_id', $user); + } + + // If set to all, display all servers they can access, including + // those they access as an admin. + // + // If set to subuser, only return the servers they can access because + // they are owner, or marked as a subuser of the server. + if (($level === 'all' && ! $admin) || $level === 'subuser') { + $instance->whereIn('id', $this->getUserAccessServers($user)); + } + + // If set to admin, only display the servers a user can access + // as an administrator (leaves out owned and subuser of). + if ($level === 'admin' && $admin) { + $instance->whereIn('id', $this->getUserAccessServers($user)); + } + + return $instance->search($this->searchTerm)->paginate( + $this->app->make('config')->get('pterodactyl.paginate.frontend.servers') + ); + } + + /** + * {@inheritdoc} + */ + public function getByUuid($uuid) + { + Assert::stringNotEmpty($uuid, 'First argument passed to getByUuid must be a non-empty string, received %s.'); + + $instance = $this->getBuilder()->with('service', 'node')->where(function ($query) use ($uuid) { + $query->where('uuidShort', $uuid)->orWhere('uuid', $uuid); + })->first($this->getColumns()); + + if (! $instance) { + throw new RecordNotFoundException; + } + + return $instance; + } } diff --git a/app/Repositories/Eloquent/SessionRepository.php b/app/Repositories/Eloquent/SessionRepository.php new file mode 100644 index 00000000..928feb56 --- /dev/null +++ b/app/Repositories/Eloquent/SessionRepository.php @@ -0,0 +1,55 @@ +. + * + * 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\Repositories\Eloquent; + +use Pterodactyl\Models\Session; +use Pterodactyl\Contracts\Repository\SessionRepositoryInterface; + +class SessionRepository extends EloquentRepository implements SessionRepositoryInterface +{ + /** + * {@inheritdoc} + */ + public function model() + { + return Session::class; + } + + /** + * {@inheritdoc} + */ + public function getUserSessions($user) + { + return $this->getBuilder()->where('user_id', $user)->get($this->getColumns()); + } + + /** + * {@inheritdoc} + */ + public function deleteUserSession($user, $session) + { + return $this->getBuilder()->where('user_id', $user)->where('id', $session)->delete(); + } +} diff --git a/app/Services/Api/KeyService.php b/app/Services/Api/KeyCreationService.php similarity index 89% rename from app/Services/Api/KeyService.php rename to app/Services/Api/KeyCreationService.php index fc67b892..4beaf3a4 100644 --- a/app/Services/Api/KeyService.php +++ b/app/Services/Api/KeyCreationService.php @@ -28,7 +28,7 @@ use Illuminate\Database\ConnectionInterface; use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; -class KeyService +class KeyCreationService { const PUB_CRYPTO_BYTES = 8; const PRIV_CRYPTO_BYTES = 32; @@ -36,7 +36,7 @@ class KeyService /** * @var \Illuminate\Database\ConnectionInterface */ - protected $database; + protected $connection; /** * @var \Illuminate\Contracts\Encryption\Encrypter @@ -57,18 +57,18 @@ class KeyService * ApiKeyService constructor. * * @param \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface $repository - * @param \Illuminate\Database\ConnectionInterface $database + * @param \Illuminate\Database\ConnectionInterface $connection * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter * @param \Pterodactyl\Services\Api\PermissionService $permissionService */ public function __construct( ApiKeyRepositoryInterface $repository, - ConnectionInterface $database, + ConnectionInterface $connection, Encrypter $encrypter, PermissionService $permissionService ) { $this->repository = $repository; - $this->database = $database; + $this->connection = $connection; $this->encrypter = $encrypter; $this->permissionService = $permissionService; } @@ -84,13 +84,13 @@ class KeyService * @throws \Exception * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ - public function create(array $data, array $permissions, array $administrative = []) + public function handle(array $data, array $permissions, array $administrative = []) { $publicKey = bin2hex(random_bytes(self::PUB_CRYPTO_BYTES)); $secretKey = bin2hex(random_bytes(self::PRIV_CRYPTO_BYTES)); // Start a Transaction - $this->database->beginTransaction(); + $this->connection->beginTransaction(); $data = array_merge($data, [ 'public' => $publicKey, @@ -128,19 +128,8 @@ class KeyService $this->permissionService->create($instance->id, $permission); } - $this->database->commit(); + $this->connection->commit(); return $secretKey; } - - /** - * Delete the API key and associated permissions from the database. - * - * @param int $id - * @return bool|null - */ - public function revoke($id) - { - return $this->repository->delete($id); - } } diff --git a/app/Services/Servers/ServerAccessHelperService.php b/app/Services/Servers/ServerAccessHelperService.php new file mode 100644 index 00000000..4ee77012 --- /dev/null +++ b/app/Services/Servers/ServerAccessHelperService.php @@ -0,0 +1,71 @@ +. + * + * 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\Servers; + +use Illuminate\Cache\Repository as CacheRepository; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; +use Pterodactyl\Models\User; + +class ServerAccessHelperService +{ + public function __construct( + CacheRepository $cache, + ServerRepositoryInterface $repository, + SubuserRepositoryInterface $subuserRepository, + UserRepositoryInterface $userRepository + ) { + $this->cache = $cache; + $this->repository = $repository; + $this->subuserRepository = $subuserRepository; + $this->userRepository = $userRepository; + } + + public function handle($uuid, $user) + { + if (! $user instanceof User) { + $user = $this->userRepository->find($user); + } + + $server = $this->repository->getByUuid($uuid); + if (! $user->root_admin) { + if (! in_array($server->id, $this->repository->getUserAccessServers($user->id))) { + throw new \Exception('User does not have access.'); + } + + if ($server->owner_id !== $user->id) { + $subuser = $this->subuserRepository->withColumns('daemonSecret')->findWhere([ + ['user_id', '=', $user->id], + ['server_id', '=', $server->id], + ]); + + $server->daemonSecret = $subuser->daemonToken; + } + } + + return $server; + } +} diff --git a/app/Services/Users/ToggleTwoFactorService.php b/app/Services/Users/ToggleTwoFactorService.php new file mode 100644 index 00000000..f731c13b --- /dev/null +++ b/app/Services/Users/ToggleTwoFactorService.php @@ -0,0 +1,84 @@ +. + * + * 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\Users; + +use PragmaRX\Google2FA\Contracts\Google2FA; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; +use Pterodactyl\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid; +use Pterodactyl\Models\User; + +class ToggleTwoFactorService +{ + /** + * @var \PragmaRX\Google2FA\Contracts\Google2FA + */ + protected $google2FA; + + /** + * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface + */ + protected $repository; + + /** + * ToggleTwoFactorService constructor. + * + * @param \PragmaRX\Google2FA\Contracts\Google2FA $google2FA + * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository + */ + public function __construct( + Google2FA $google2FA, + UserRepositoryInterface $repository + ) { + $this->google2FA = $google2FA; + $this->repository = $repository; + } + + /** + * @param int|\Pterodactyl\Models\User $user + * @param string $token + * @param null|bool $toggleState + * @return bool + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Pterodactyl\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid + */ + public function handle($user, $token, $toggleState = null) + { + if (! $user instanceof User) { + $user = $this->repository->find($user); + } + + if (! $this->google2FA->verifyKey($user->totp_secret, $token, 2)) { + throw new TwoFactorAuthenticationTokenInvalid; + } + + $this->repository->withoutFresh()->update($user->id, [ + 'use_totp' => (is_null($toggleState) ? ! $user->use_totp : $toggleState), + ]); + + return true; + } +} diff --git a/app/Services/Users/TwoFactorSetupService.php b/app/Services/Users/TwoFactorSetupService.php new file mode 100644 index 00000000..d959ef6a --- /dev/null +++ b/app/Services/Users/TwoFactorSetupService.php @@ -0,0 +1,91 @@ +. + * + * 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\Users; + +use Illuminate\Contracts\Config\Repository as ConfigRepository; +use PragmaRX\Google2FA\Contracts\Google2FA; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; +use Pterodactyl\Models\User; + +class TwoFactorSetupService +{ + /** + * @var \Illuminate\Contracts\Config\Repository + */ + protected $config; + + /** + * @var \PragmaRX\Google2FA\Contracts\Google2FA + */ + protected $google2FA; + + /** + * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface + */ + protected $repository; + + /** + * TwoFactorSetupService constructor. + * + * @param \Illuminate\Contracts\Config\Repository $config + * @param \PragmaRX\Google2FA\Contracts\Google2FA $google2FA + * @param \Pterodactyl\Contracts\Repository\UserRepositoryInterface $repository + */ + public function __construct( + ConfigRepository $config, + Google2FA $google2FA, + UserRepositoryInterface $repository + ) { + $this->config = $config; + $this->google2FA = $google2FA; + $this->repository = $repository; + } + + /** + * Generate a 2FA token and store it in the database. + * + * @param int|\Pterodactyl\Models\User $user + * @return array + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function handle($user) + { + if (! $user instanceof User) { + $user = $this->repository->find($user); + } + + $secret = $this->google2FA->generateSecretKey(); + $image = $this->google2FA->getQRCodeGoogleUrl($this->config->get('app.name'), $user->email, $secret); + + $this->repository->withoutFresh()->update($user->id, ['totp_secret' => $secret]); + + return [ + 'qrImage' => $image, + 'secret' => $secret, + ]; + } +} diff --git a/app/Services/Users/UserUpdateService.php b/app/Services/Users/UserUpdateService.php index 646b1940..99ce6366 100644 --- a/app/Services/Users/UserUpdateService.php +++ b/app/Services/Users/UserUpdateService.php @@ -61,6 +61,7 @@ class UserUpdateService * @return mixed * * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function handle($id, array $data) { diff --git a/resources/lang/en/base.php b/resources/lang/en/base.php index 2c5242df..9c7bd9d0 100644 --- a/resources/lang/en/base.php +++ b/resources/lang/en/base.php @@ -33,6 +33,7 @@ return [ 'header_sub' => 'Manage your API access keys.', 'list' => 'API Keys', 'create_new' => 'Create New API key', + 'keypair_created' => 'An API Key-Pair has been generated. Your API secret token is :token. Please take note of this key as it will not be displayed again.', ], 'new' => [ 'header' => 'New API Key', @@ -207,6 +208,8 @@ return [ ], ], 'account' => [ + 'details_updated' => 'Your account details have been successfully updated.', + 'invalid_password' => 'The password provided for your account was not valid.', 'header' => 'Your Account', 'header_sub' => 'Manage your account details.', 'update_pass' => 'Update Password', @@ -219,10 +222,9 @@ return [ 'last_name' => 'Last Name', 'update_identitity' => 'Update Identity', 'username_help' => 'Your username must be unique to your account, and may only contain the following characters: :requirements.', - 'invalid_pass' => 'The password provided was not valid for this account.', - 'exception' => 'An error occurred while attempting to update your account.', ], 'security' => [ + 'session_mgmt_disabled' => 'Your host has not enabled the ability to manage account sessions via this interface.', 'header' => 'Account Security', 'header_sub' => 'Control active sessions and 2-Factor Authentication.', 'sessions' => 'Active Sessions', @@ -234,5 +236,6 @@ return [ 'enable_2fa' => 'Enable 2-Factor Authentication', '2fa_qr' => 'Confgure 2FA on Your Device', '2fa_checkpoint_help' => 'Use the 2FA application on your phone to take a picture of the QR code to the left, or manually enter the code under it. Once you have done so, generate a token and enter it below.', + '2fa_disable_error' => 'The 2FA token provided was not valid. Protection has not been disabled for this account.', ], ]; diff --git a/resources/themes/pterodactyl/base/security.blade.php b/resources/themes/pterodactyl/base/security.blade.php index 666a790b..8f3908db 100644 --- a/resources/themes/pterodactyl/base/security.blade.php +++ b/resources/themes/pterodactyl/base/security.blade.php @@ -39,30 +39,36 @@

    @lang('base.security.sessions')

    -
    - - - - - - - - - @foreach($sessions as $session) + @if(!is_null($sessions)) +
    +
    @lang('strings.id')@lang('strings.ip')@lang('strings.last_activity')
    + - - - - + + + + - @endforeach - -
    {{ substr($session->id, 0, 6) }}{{ $session->ip_address }}{{ Carbon::createFromTimestamp($session->last_activity)->diffForHumans() }} - - - - @lang('strings.id')@lang('strings.ip')@lang('strings.last_activity')
    -
    + @foreach($sessions as $session) + + {{ substr($session->id, 0, 6) }} + {{ $session->ip_address }} + {{ Carbon::createFromTimestamp($session->last_activity)->diffForHumans() }} + + + + + + + @endforeach + + +
    + @else +
    +

    @lang('base.security.session_mgmt_disabled')

    +
    + @endif
    diff --git a/routes/base.php b/routes/base.php index 4b06bb64..adb100fe 100644 --- a/routes/base.php +++ b/routes/base.php @@ -24,10 +24,6 @@ Route::get('/', 'IndexController@getIndex')->name('index'); Route::get('/status/{server}', 'IndexController@status')->name('index.status'); -Route::get('/index', function () { - redirect()->route('index'); -}); - /* |-------------------------------------------------------------------------- | Account Controller Routes diff --git a/tests/Assertions/ControllerAssertionsTrait.php b/tests/Assertions/ControllerAssertionsTrait.php index 5e04512d..1ed835c8 100644 --- a/tests/Assertions/ControllerAssertionsTrait.php +++ b/tests/Assertions/ControllerAssertionsTrait.php @@ -83,4 +83,13 @@ trait ControllerAssertionsTrait { PHPUnit_Framework_Assert::assertEquals($value, array_get($view->getData(), $attribute)); } + + /** + * @param string $route + * @param \Illuminate\Http\RedirectResponse $response + */ + public function assertRouteRedirectEquals($route, $response) + { + PHPUnit_Framework_Assert::assertEquals(route($route), $response->getTargetUrl()); + } } diff --git a/tests/Unit/Http/Controllers/Base/AccountControllerTest.php b/tests/Unit/Http/Controllers/Base/AccountControllerTest.php new file mode 100644 index 00000000..1f9b556f --- /dev/null +++ b/tests/Unit/Http/Controllers/Base/AccountControllerTest.php @@ -0,0 +1,132 @@ +. + * + * 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\Http\Controllers\Base; + +use Mockery as m; +use Prologue\Alerts\AlertsMessageBag; +use Pterodactyl\Http\Controllers\Base\AccountController; +use Pterodactyl\Http\Requests\Base\AccountDataFormRequest; +use Pterodactyl\Services\Users\UserUpdateService; +use Tests\Assertions\ControllerAssertionsTrait; +use Tests\TestCase; + +class AccountControllerTest extends TestCase +{ + use ControllerAssertionsTrait; + + /** + * @var \Prologue\Alerts\AlertsMessageBag + */ + protected $alert; + + /** + * @var \Pterodactyl\Http\Controllers\Base\AccountController + */ + protected $controller; + + /** + * @var \Pterodactyl\Http\Requests\Base\AccountDataFormRequest + */ + protected $request; + + /** + * @var \Pterodactyl\Services\Users\UserUpdateService + */ + protected $updateService; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->alert = m::mock(AlertsMessageBag::class); + $this->request = m::mock(AccountDataFormRequest::class); + $this->updateService = m::mock(UserUpdateService::class); + + $this->controller = new AccountController($this->alert, $this->updateService); + } + + /** + * Test the index controller. + */ + public function testIndexController() + { + $response = $this->controller->index(); + + $this->assertViewNameEquals('base.account', $response); + } + + /** + * Test controller when password is being updated. + */ + public function testUpdateControllerForPassword() + { + $this->request->shouldReceive('input')->with('do_action')->andReturn('password'); + $this->request->shouldReceive('input')->with('new_password')->once()->andReturn('test-password'); + + $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn((object) ['id' => 1]); + $this->updateService->shouldReceive('handle')->with(1, ['password' => 'test-password'])->once()->andReturnNull(); + $this->alert->shouldReceive('success->flash')->once()->andReturnNull(); + + $response = $this->controller->update($this->request); + $this->assertRouteRedirectEquals('account', $response); + } + + /** + * Test controller when email is being updated. + */ + public function testUpdateControllerForEmail() + { + $this->request->shouldReceive('input')->with('do_action')->andReturn('email'); + $this->request->shouldReceive('input')->with('new_email')->once()->andReturn('test@example.com'); + + $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn((object) ['id' => 1]); + $this->updateService->shouldReceive('handle')->with(1, ['email' => 'test@example.com'])->once()->andReturnNull(); + $this->alert->shouldReceive('success->flash')->once()->andReturnNull(); + + $response = $this->controller->update($this->request); + $this->assertRouteRedirectEquals('account', $response); + } + + /** + * Test controller when identity is being updated. + */ + public function testUpdateControllerForIdentity() + { + $this->request->shouldReceive('input')->with('do_action')->andReturn('identity'); + $this->request->shouldReceive('only')->with(['name_first', 'name_last', 'username'])->once()->andReturn([ + 'test_data' => 'value', + ]); + + $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn((object) ['id' => 1]); + $this->updateService->shouldReceive('handle')->with(1, ['test_data' => 'value'])->once()->andReturnNull(); + $this->alert->shouldReceive('success->flash')->once()->andReturnNull(); + + $response = $this->controller->update($this->request); + $this->assertRouteRedirectEquals('account', $response); + } +} diff --git a/tests/Unit/Services/Api/KeyServiceTest.php b/tests/Unit/Services/Api/KeyCreationServiceTest.php similarity index 72% rename from tests/Unit/Services/Api/KeyServiceTest.php rename to tests/Unit/Services/Api/KeyCreationServiceTest.php index b4912fca..fb9afd62 100644 --- a/tests/Unit/Services/Api/KeyServiceTest.php +++ b/tests/Unit/Services/Api/KeyCreationServiceTest.php @@ -27,20 +27,20 @@ namespace Tests\Unit\Services\Api; use Mockery as m; use Tests\TestCase; use phpmock\phpunit\PHPMock; -use Pterodactyl\Services\Api\KeyService; +use Pterodactyl\Services\Api\KeyCreationService; use Illuminate\Database\ConnectionInterface; use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Services\Api\PermissionService; use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; -class KeyServiceTest extends TestCase +class KeyCreationServiceTest extends TestCase { use PHPMock; /** * @var \Illuminate\Database\ConnectionInterface */ - protected $database; + protected $connection; /** * @var \Illuminate\Contracts\Encryption\Encrypter @@ -58,7 +58,7 @@ class KeyServiceTest extends TestCase protected $repository; /** - * @var \Pterodactyl\Services\Api\KeyService + * @var \Pterodactyl\Services\Api\KeyCreationService */ protected $service; @@ -66,14 +66,14 @@ class KeyServiceTest extends TestCase { parent::setUp(); - $this->database = m::mock(ConnectionInterface::class); + $this->connection = m::mock(ConnectionInterface::class); $this->encrypter = m::mock(Encrypter::class); $this->permissions = m::mock(PermissionService::class); $this->repository = m::mock(ApiKeyRepositoryInterface::class); - $this->service = new KeyService( + $this->service = new KeyCreationService( $this->repository, - $this->database, + $this->connection, $this->encrypter, $this->permissions ); @@ -82,21 +82,17 @@ class KeyServiceTest extends TestCase /** * Test that the service is able to create a keypair and assign the correct permissions. */ - public function test_create_function() + public function testKeyIsCreated() { - $this->getFunctionMock('\\Pterodactyl\\Services\\Api', 'random_bytes') - ->expects($this->exactly(2)) - ->willReturnCallback(function ($bytes) { - return hex2bin(str_pad('', $bytes * 2, '0')); - }); + $this->getFunctionMock('\\Pterodactyl\\Services\\Api', 'bin2hex') + ->expects($this->exactly(2))->willReturn('bin2hex'); - $this->database->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); - $this->encrypter->shouldReceive('encrypt')->with(str_pad('', 64, '0')) - ->once()->andReturn('encrypted-secret'); + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->encrypter->shouldReceive('encrypt')->with('bin2hex')->once()->andReturn('encrypted-secret'); $this->repository->shouldReceive('create')->with([ 'test-data' => 'test', - 'public' => str_pad('', 16, '0'), + 'public' => 'bin2hex', 'secret' => 'encrypted-secret', ], true, true)->once()->andReturn((object) ['id' => 1]); @@ -108,25 +104,15 @@ class KeyServiceTest extends TestCase $this->permissions->shouldReceive('create')->with(1, 'user.server-list')->once()->andReturnNull(); $this->permissions->shouldReceive('create')->with(1, 'server-create')->once()->andReturnNull(); - $this->database->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); + $this->connection->shouldReceive('commit')->withNoArgs()->once()->andReturnNull(); - $response = $this->service->create( + $response = $this->service->handle( ['test-data' => 'test'], ['invalid-node', 'server-list'], ['invalid-node', 'server-create'] ); $this->assertNotEmpty($response); - $this->assertEquals(str_pad('', 64, '0'), $response); - } - - /** - * Test that an API key can be revoked. - */ - public function test_revoke_function() - { - $this->repository->shouldReceive('delete')->with(1)->once()->andReturn(true); - - $this->assertTrue($this->service->revoke(1)); + $this->assertEquals('bin2hex', $response); } } diff --git a/tests/Unit/Services/Users/ToggleTwoFactorServiceTest.php b/tests/Unit/Services/Users/ToggleTwoFactorServiceTest.php new file mode 100644 index 00000000..8714f974 --- /dev/null +++ b/tests/Unit/Services/Users/ToggleTwoFactorServiceTest.php @@ -0,0 +1,132 @@ +. + * + * 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\Users; + +use Mockery as m; +use PragmaRX\Google2FA\Contracts\Google2FA; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; +use Pterodactyl\Models\User; +use Pterodactyl\Services\Users\ToggleTwoFactorService; +use Tests\TestCase; + +class ToggleTwoFactorServiceTest extends TestCase +{ + /** + * @var \PragmaRX\Google2FA\Contracts\Google2FA + */ + protected $google2FA; + + /** + * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Users\ToggleTwoFactorService + */ + protected $service; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->google2FA = m::mock(Google2FA::class); + $this->repository = m::mock(UserRepositoryInterface::class); + + $this->service = new ToggleTwoFactorService($this->google2FA, $this->repository); + } + + /** + * Test that 2FA can be enabled for a user. + */ + public function testTwoFactorIsEnabledForUser() + { + $model = factory(User::class)->make(['totp_secret' => 'secret', 'use_totp' => false]); + + $this->google2FA->shouldReceive('verifyKey')->with($model->totp_secret, 'test-token', 2)->once()->andReturn(true); + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($model->id, ['use_totp' => true])->once()->andReturnNull(); + + $this->assertTrue($this->service->handle($model, 'test-token')); + } + + /** + * Test that 2FA can be disabled for a user. + */ + public function testTwoFactorIsDisabled() + { + $model = factory(User::class)->make(['totp_secret' => 'secret', 'use_totp' => true]); + + $this->google2FA->shouldReceive('verifyKey')->with($model->totp_secret, 'test-token', 2)->once()->andReturn(true); + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($model->id, ['use_totp' => false])->once()->andReturnNull(); + + $this->assertTrue($this->service->handle($model, 'test-token')); + } + + /** + * Test that 2FA will remain disabled for a user. + */ + public function testTwoFactorRemainsDisabledForUser() + { + $model = factory(User::class)->make(['totp_secret' => 'secret', 'use_totp' => false]); + + $this->google2FA->shouldReceive('verifyKey')->with($model->totp_secret, 'test-token', 2)->once()->andReturn(true); + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($model->id, ['use_totp' => false])->once()->andReturnNull(); + + $this->assertTrue($this->service->handle($model, 'test-token', false)); + } + + /** + * Test that an exception is thrown if the token provided is invalid. + * + * @expectedException \Pterodactyl\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid + */ + public function testExceptionIsThrownIfTokenIsInvalid() + { + $model = factory(User::class)->make(); + $this->google2FA->shouldReceive('verifyKey')->once()->andReturn(false); + + $this->service->handle($model, 'test-token'); + } + + /** + * Test that an integer can be passed in place of a user model. + */ + public function testIntegerCanBePassedInPlaceOfUserModel() + { + $model = factory(User::class)->make(['totp_secret' => 'secret', 'use_totp' => false]); + + $this->repository->shouldReceive('find')->with($model->id)->once()->andReturn($model); + $this->google2FA->shouldReceive('verifyKey')->once()->andReturn(true); + $this->repository->shouldReceive('withoutFresh->update')->once()->andReturnNull(); + + $this->assertTrue($this->service->handle($model->id, 'test-token')); + } +} diff --git a/tests/Unit/Services/Users/TwoFactorSetupServiceTest.php b/tests/Unit/Services/Users/TwoFactorSetupServiceTest.php new file mode 100644 index 00000000..5d5b3ad9 --- /dev/null +++ b/tests/Unit/Services/Users/TwoFactorSetupServiceTest.php @@ -0,0 +1,108 @@ +. + * + * 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\Users; + +use Illuminate\Contracts\Config\Repository; +use Mockery as m; +use PragmaRX\Google2FA\Contracts\Google2FA; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; +use Pterodactyl\Models\User; +use Pterodactyl\Services\Users\TwoFactorSetupService; +use Tests\TestCase; + +class TwoFactorSetupServiceTest extends TestCase +{ + /** + * @var \Illuminate\Contracts\Config\Repository + */ + protected $config; + + /** + * @var \PragmaRX\Google2FA\Contracts\Google2FA + */ + protected $google2FA; + + /** + * @var \Pterodactyl\Contracts\Repository\UserRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Services\Users\TwoFactorSetupService + */ + protected $service; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->config = m::mock(Repository::class); + $this->google2FA = m::mock(Google2FA::class); + $this->repository = m::mock(UserRepositoryInterface::class); + + $this->service = new TwoFactorSetupService($this->config, $this->google2FA, $this->repository); + } + + /** + * Test that the correct data is returned. + */ + public function testSecretAndImageAreReturned() + { + $model = factory(User::class)->make(); + + $this->google2FA->shouldReceive('generateSecretKey')->withNoArgs()->once()->andReturn('secretKey'); + $this->config->shouldReceive('get')->with('app.name')->once()->andReturn('CompanyName'); + $this->google2FA->shouldReceive('getQRCodeGoogleUrl')->with('CompanyName', $model->email, 'secretKey') + ->once()->andReturn('http://url.com'); + $this->repository->shouldReceive('withoutFresh')->withNoArgs()->once()->andReturnSelf() + ->shouldReceive('update')->with($model->id, ['totp_secret' => 'secretKey'])->once()->andReturnNull(); + + $response = $this->service->handle($model); + $this->assertNotEmpty($response); + $this->assertArrayHasKey('qrImage', $response); + $this->assertArrayHasKey('secret', $response); + $this->assertEquals('http://url.com', $response['qrImage']); + $this->assertEquals('secretKey', $response['secret']); + } + + /** + * Test that an integer can be passed in place of the user model. + */ + public function testIntegerCanBePassedInPlaceOfUserModel() + { + $model = factory(User::class)->make(); + + $this->repository->shouldReceive('find')->with($model->id)->once()->andReturn($model); + $this->google2FA->shouldReceive('generateSecretKey')->withNoArgs()->once()->andReturnNull(); + $this->config->shouldReceive('get')->with('app.name')->once()->andReturnNull(); + $this->google2FA->shouldReceive('getQRCodeGoogleUrl')->once()->andReturnNull(); + $this->repository->shouldReceive('withoutFresh->update')->once()->andReturnNull(); + + $this->assertTrue(is_array($this->service->handle($model->id))); + } +} From cb62e6a96da6f02cd2e692108bed3c4601661229 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Wed, 30 Aug 2017 21:12:35 -0500 Subject: [PATCH 71/99] Hide from UI if not admin --- .../themes/pterodactyl/base/api/new.blade.php | 56 ++++++++++--------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/resources/themes/pterodactyl/base/api/new.blade.php b/resources/themes/pterodactyl/base/api/new.blade.php index a7a487c3..aeef9d24 100644 --- a/resources/themes/pterodactyl/base/api/new.blade.php +++ b/resources/themes/pterodactyl/base/api/new.blade.php @@ -110,35 +110,37 @@ @endif @endforeach
    -
    - @foreach($permissions['admin'] as $block => $perms) -
    -
    -
    -

    @lang('base.api.permissions.admin.' . $block . '_header')

    -
    -
    - @foreach($perms as $permission) -
    -
    - - + @if(Auth::user()->root_admin) +
    + @foreach($permissions['admin'] as $block => $perms) +
    +
    +
    +

    @lang('base.api.permissions.admin.' . $block . '_header')

    +
    +
    + @foreach($perms as $permission) +
    +
    + + +
    +

    @lang('base.api.permissions.admin.' . $block . '.' . $permission . '.desc')

    -

    @lang('base.api.permissions.admin.' . $block . '.' . $permission . '.desc')

    -
    - @endforeach + @endforeach +
    -
    - @if ($loop->iteration % 3 === 0) -
    - @endif - @if ($loop->iteration % 2 === 0) -
    - @endif - @endforeach -
    + @if ($loop->iteration % 3 === 0) +
    + @endif + @if ($loop->iteration % 2 === 0) +
    + @endif + @endforeach +
    + @endif @endsection From 30660cfac2b834ae31bdcc990e9b775818602234 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Wed, 30 Aug 2017 21:14:20 -0500 Subject: [PATCH 72/99] Apply fixes from StyleCI (#609) --- app/Http/Controllers/Admin/NodesController.php | 2 +- app/Http/Controllers/Admin/ServersController.php | 4 ++-- app/Http/Controllers/Admin/UserController.php | 2 +- app/Http/Controllers/Base/APIController.php | 4 ++-- app/Http/Controllers/Base/AccountController.php | 2 +- app/Http/Controllers/Base/IndexController.php | 2 +- app/Http/Controllers/Base/SecurityController.php | 10 +++++----- app/Http/Requests/Base/AccountDataFormRequest.php | 2 +- app/Providers/RepositoryServiceProvider.php | 12 ++++++------ app/Repositories/Eloquent/ServerRepository.php | 2 +- app/Services/Servers/ServerAccessHelperService.php | 4 ++-- app/Services/Users/ToggleTwoFactorService.php | 2 +- app/Services/Users/TwoFactorSetupService.php | 4 ++-- .../Http/Controllers/Base/AccountControllerTest.php | 6 +++--- tests/Unit/Services/Api/KeyCreationServiceTest.php | 2 +- .../Unit/Services/Nodes/NodeDeletionServiceTest.php | 2 +- .../Services/Users/ToggleTwoFactorServiceTest.php | 8 ++++---- .../Services/Users/TwoFactorSetupServiceTest.php | 10 +++++----- .../Unit/Services/Users/UserDeletionServiceTest.php | 2 +- 19 files changed, 41 insertions(+), 41 deletions(-) diff --git a/app/Http/Controllers/Admin/NodesController.php b/app/Http/Controllers/Admin/NodesController.php index 76cabdfe..c0dd8a6e 100644 --- a/app/Http/Controllers/Admin/NodesController.php +++ b/app/Http/Controllers/Admin/NodesController.php @@ -30,9 +30,9 @@ use Pterodactyl\Models\Node; use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Services\Nodes\NodeUpdateService; +use Illuminate\Cache\Repository as CacheRepository; use Pterodactyl\Services\Nodes\NodeCreationService; use Pterodactyl\Services\Nodes\NodeDeletionService; -use Illuminate\Cache\Repository as CacheRepository; use Pterodactyl\Services\Allocations\AssignmentService; use Pterodactyl\Http\Requests\Admin\Node\NodeFormRequest; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; diff --git a/app/Http/Controllers/Admin/ServersController.php b/app/Http/Controllers/Admin/ServersController.php index 7557afb4..db84291b 100644 --- a/app/Http/Controllers/Admin/ServersController.php +++ b/app/Http/Controllers/Admin/ServersController.php @@ -30,11 +30,11 @@ use Pterodactyl\Models\Server; use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Http\Controllers\Controller; +use Pterodactyl\Services\Servers\SuspensionService; +use Pterodactyl\Http\Requests\Admin\ServerFormRequest; use Pterodactyl\Services\Servers\ServerCreationService; use Pterodactyl\Services\Servers\ServerDeletionService; use Pterodactyl\Services\Servers\ReinstallServerService; -use Pterodactyl\Services\Servers\SuspensionService; -use Pterodactyl\Http\Requests\Admin\ServerFormRequest; use Pterodactyl\Services\Servers\ContainerRebuildService; use Pterodactyl\Services\Servers\BuildModificationService; use Pterodactyl\Services\Database\DatabaseManagementService; diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index 3aed7c02..6907eaaa 100644 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -29,10 +29,10 @@ use Pterodactyl\Models\User; use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Http\Controllers\Controller; +use Illuminate\Contracts\Translation\Translator; use Pterodactyl\Services\Users\UserUpdateService; use Pterodactyl\Services\Users\UserCreationService; use Pterodactyl\Services\Users\UserDeletionService; -use Illuminate\Contracts\Translation\Translator; use Pterodactyl\Http\Requests\Admin\UserFormRequest; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; diff --git a/app/Http/Controllers/Base/APIController.php b/app/Http/Controllers/Base/APIController.php index 72a4e7b6..4dedd30d 100644 --- a/app/Http/Controllers/Base/APIController.php +++ b/app/Http/Controllers/Base/APIController.php @@ -27,11 +27,11 @@ namespace Pterodactyl\Http\Controllers\Base; use Illuminate\Http\Request; use Prologue\Alerts\AlertsMessageBag; -use Pterodactyl\Http\Requests\Base\ApiKeyFormRequest; use Pterodactyl\Models\APIPermission; use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; use Pterodactyl\Services\Api\KeyCreationService; +use Pterodactyl\Http\Requests\Base\ApiKeyFormRequest; +use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; class APIController extends Controller { diff --git a/app/Http/Controllers/Base/AccountController.php b/app/Http/Controllers/Base/AccountController.php index 102850ed..fea7f09d 100644 --- a/app/Http/Controllers/Base/AccountController.php +++ b/app/Http/Controllers/Base/AccountController.php @@ -27,8 +27,8 @@ namespace Pterodactyl\Http\Controllers\Base; use Prologue\Alerts\AlertsMessageBag; use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Http\Requests\Base\AccountDataFormRequest; use Pterodactyl\Services\Users\UserUpdateService; +use Pterodactyl\Http\Requests\Base\AccountDataFormRequest; class AccountController extends Controller { diff --git a/app/Http/Controllers/Base/IndexController.php b/app/Http/Controllers/Base/IndexController.php index 50416300..3c52a84f 100644 --- a/app/Http/Controllers/Base/IndexController.php +++ b/app/Http/Controllers/Base/IndexController.php @@ -26,9 +26,9 @@ namespace Pterodactyl\Http\Controllers\Base; use Illuminate\Http\Request; -use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Services\Servers\ServerAccessHelperService; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; class IndexController extends Controller diff --git a/app/Http/Controllers/Base/SecurityController.php b/app/Http/Controllers/Base/SecurityController.php index b44e6e0f..b44ecc12 100644 --- a/app/Http/Controllers/Base/SecurityController.php +++ b/app/Http/Controllers/Base/SecurityController.php @@ -25,15 +25,15 @@ namespace Pterodactyl\Http\Controllers\Base; -use Illuminate\Contracts\Config\Repository as ConfigRepository; -use Illuminate\Contracts\Session\Session; use Illuminate\Http\Request; use Prologue\Alerts\AlertsMessageBag; +use Illuminate\Contracts\Session\Session; +use Pterodactyl\Http\Controllers\Controller; +use Pterodactyl\Services\Users\TwoFactorSetupService; +use Pterodactyl\Services\Users\ToggleTwoFactorService; +use Illuminate\Contracts\Config\Repository as ConfigRepository; use Pterodactyl\Contracts\Repository\SessionRepositoryInterface; use Pterodactyl\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid; -use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Services\Users\ToggleTwoFactorService; -use Pterodactyl\Services\Users\TwoFactorSetupService; class SecurityController extends Controller { diff --git a/app/Http/Requests/Base/AccountDataFormRequest.php b/app/Http/Requests/Base/AccountDataFormRequest.php index a9573106..63cacacf 100644 --- a/app/Http/Requests/Base/AccountDataFormRequest.php +++ b/app/Http/Requests/Base/AccountDataFormRequest.php @@ -24,9 +24,9 @@ namespace Pterodactyl\Http\Requests\Base; -use Pterodactyl\Exceptions\Http\Base\InvalidPasswordProvidedException; use Pterodactyl\Models\User; use Pterodactyl\Http\Requests\FrontendUserFormRequest; +use Pterodactyl\Exceptions\Http\Base\InvalidPasswordProvidedException; class AccountDataFormRequest extends FrontendUserFormRequest { diff --git a/app/Providers/RepositoryServiceProvider.php b/app/Providers/RepositoryServiceProvider.php index d30ae322..09c1c295 100644 --- a/app/Providers/RepositoryServiceProvider.php +++ b/app/Providers/RepositoryServiceProvider.php @@ -25,24 +25,21 @@ namespace Pterodactyl\Providers; use Illuminate\Support\ServiceProvider; -use Pterodactyl\Contracts\Repository\PermissionRepositoryInterface; -use Pterodactyl\Contracts\Repository\SessionRepositoryInterface; -use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; use Pterodactyl\Repositories\Daemon\FileRepository; use Pterodactyl\Repositories\Daemon\PowerRepository; use Pterodactyl\Repositories\Eloquent\NodeRepository; use Pterodactyl\Repositories\Eloquent\PackRepository; -use Pterodactyl\Repositories\Eloquent\PermissionRepository; -use Pterodactyl\Repositories\Eloquent\SessionRepository; -use Pterodactyl\Repositories\Eloquent\SubuserRepository; use Pterodactyl\Repositories\Eloquent\UserRepository; use Pterodactyl\Repositories\Daemon\CommandRepository; use Pterodactyl\Repositories\Eloquent\ApiKeyRepository; use Pterodactyl\Repositories\Eloquent\ServerRepository; use Pterodactyl\Repositories\Eloquent\ServiceRepository; +use Pterodactyl\Repositories\Eloquent\SessionRepository; +use Pterodactyl\Repositories\Eloquent\SubuserRepository; use Pterodactyl\Repositories\Eloquent\DatabaseRepository; use Pterodactyl\Repositories\Eloquent\LocationRepository; use Pterodactyl\Repositories\Eloquent\AllocationRepository; +use Pterodactyl\Repositories\Eloquent\PermissionRepository; use Pterodactyl\Repositories\Daemon\ConfigurationRepository; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Contracts\Repository\PackRepositoryInterface; @@ -55,10 +52,13 @@ use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Repositories\Eloquent\OptionVariableRepository; use Pterodactyl\Repositories\Eloquent\ServerVariableRepository; use Pterodactyl\Contracts\Repository\ServiceRepositoryInterface; +use Pterodactyl\Contracts\Repository\SessionRepositoryInterface; +use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; use Pterodactyl\Repositories\Eloquent\ServiceVariableRepository; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; +use Pterodactyl\Contracts\Repository\PermissionRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\PowerRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseHostRepositoryInterface; diff --git a/app/Repositories/Eloquent/ServerRepository.php b/app/Repositories/Eloquent/ServerRepository.php index ec89052b..227b7c22 100644 --- a/app/Repositories/Eloquent/ServerRepository.php +++ b/app/Repositories/Eloquent/ServerRepository.php @@ -24,11 +24,11 @@ namespace Pterodactyl\Repositories\Eloquent; +use Webmozart\Assert\Assert; use Pterodactyl\Models\Server; use Pterodactyl\Repositories\Concerns\Searchable; use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; -use Webmozart\Assert\Assert; class ServerRepository extends EloquentRepository implements ServerRepositoryInterface { diff --git a/app/Services/Servers/ServerAccessHelperService.php b/app/Services/Servers/ServerAccessHelperService.php index 4ee77012..2aef717f 100644 --- a/app/Services/Servers/ServerAccessHelperService.php +++ b/app/Services/Servers/ServerAccessHelperService.php @@ -24,11 +24,11 @@ namespace Pterodactyl\Services\Servers; +use Pterodactyl\Models\User; use Illuminate\Cache\Repository as CacheRepository; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; -use Pterodactyl\Contracts\Repository\UserRepositoryInterface; -use Pterodactyl\Models\User; class ServerAccessHelperService { diff --git a/app/Services/Users/ToggleTwoFactorService.php b/app/Services/Users/ToggleTwoFactorService.php index f731c13b..9de97c36 100644 --- a/app/Services/Users/ToggleTwoFactorService.php +++ b/app/Services/Users/ToggleTwoFactorService.php @@ -24,10 +24,10 @@ namespace Pterodactyl\Services\Users; +use Pterodactyl\Models\User; use PragmaRX\Google2FA\Contracts\Google2FA; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid; -use Pterodactyl\Models\User; class ToggleTwoFactorService { diff --git a/app/Services/Users/TwoFactorSetupService.php b/app/Services/Users/TwoFactorSetupService.php index d959ef6a..e9b26955 100644 --- a/app/Services/Users/TwoFactorSetupService.php +++ b/app/Services/Users/TwoFactorSetupService.php @@ -24,10 +24,10 @@ namespace Pterodactyl\Services\Users; -use Illuminate\Contracts\Config\Repository as ConfigRepository; +use Pterodactyl\Models\User; use PragmaRX\Google2FA\Contracts\Google2FA; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; -use Pterodactyl\Models\User; +use Illuminate\Contracts\Config\Repository as ConfigRepository; class TwoFactorSetupService { diff --git a/tests/Unit/Http/Controllers/Base/AccountControllerTest.php b/tests/Unit/Http/Controllers/Base/AccountControllerTest.php index 1f9b556f..88d749c2 100644 --- a/tests/Unit/Http/Controllers/Base/AccountControllerTest.php +++ b/tests/Unit/Http/Controllers/Base/AccountControllerTest.php @@ -25,12 +25,12 @@ namespace Tests\Unit\Http\Controllers\Base; use Mockery as m; +use Tests\TestCase; use Prologue\Alerts\AlertsMessageBag; +use Tests\Assertions\ControllerAssertionsTrait; +use Pterodactyl\Services\Users\UserUpdateService; use Pterodactyl\Http\Controllers\Base\AccountController; use Pterodactyl\Http\Requests\Base\AccountDataFormRequest; -use Pterodactyl\Services\Users\UserUpdateService; -use Tests\Assertions\ControllerAssertionsTrait; -use Tests\TestCase; class AccountControllerTest extends TestCase { diff --git a/tests/Unit/Services/Api/KeyCreationServiceTest.php b/tests/Unit/Services/Api/KeyCreationServiceTest.php index fb9afd62..87d148c1 100644 --- a/tests/Unit/Services/Api/KeyCreationServiceTest.php +++ b/tests/Unit/Services/Api/KeyCreationServiceTest.php @@ -27,10 +27,10 @@ namespace Tests\Unit\Services\Api; use Mockery as m; use Tests\TestCase; use phpmock\phpunit\PHPMock; -use Pterodactyl\Services\Api\KeyCreationService; use Illuminate\Database\ConnectionInterface; use Illuminate\Contracts\Encryption\Encrypter; use Pterodactyl\Services\Api\PermissionService; +use Pterodactyl\Services\Api\KeyCreationService; use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; class KeyCreationServiceTest extends TestCase diff --git a/tests/Unit/Services/Nodes/NodeDeletionServiceTest.php b/tests/Unit/Services/Nodes/NodeDeletionServiceTest.php index 5a93d1d3..5f92d40f 100644 --- a/tests/Unit/Services/Nodes/NodeDeletionServiceTest.php +++ b/tests/Unit/Services/Nodes/NodeDeletionServiceTest.php @@ -27,8 +27,8 @@ namespace Tests\Unit\Services\Nodes; use Mockery as m; use Tests\TestCase; use Pterodactyl\Models\Node; -use Pterodactyl\Services\Nodes\NodeDeletionService; use Illuminate\Contracts\Translation\Translator; +use Pterodactyl\Services\Nodes\NodeDeletionService; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; diff --git a/tests/Unit/Services/Users/ToggleTwoFactorServiceTest.php b/tests/Unit/Services/Users/ToggleTwoFactorServiceTest.php index 8714f974..28a98ccf 100644 --- a/tests/Unit/Services/Users/ToggleTwoFactorServiceTest.php +++ b/tests/Unit/Services/Users/ToggleTwoFactorServiceTest.php @@ -25,11 +25,11 @@ namespace Tests\Unit\Services\Users; use Mockery as m; -use PragmaRX\Google2FA\Contracts\Google2FA; -use Pterodactyl\Contracts\Repository\UserRepositoryInterface; -use Pterodactyl\Models\User; -use Pterodactyl\Services\Users\ToggleTwoFactorService; use Tests\TestCase; +use Pterodactyl\Models\User; +use PragmaRX\Google2FA\Contracts\Google2FA; +use Pterodactyl\Services\Users\ToggleTwoFactorService; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; class ToggleTwoFactorServiceTest extends TestCase { diff --git a/tests/Unit/Services/Users/TwoFactorSetupServiceTest.php b/tests/Unit/Services/Users/TwoFactorSetupServiceTest.php index 5d5b3ad9..d9dbc68f 100644 --- a/tests/Unit/Services/Users/TwoFactorSetupServiceTest.php +++ b/tests/Unit/Services/Users/TwoFactorSetupServiceTest.php @@ -24,13 +24,13 @@ namespace Tests\Unit\Services\Users; -use Illuminate\Contracts\Config\Repository; use Mockery as m; -use PragmaRX\Google2FA\Contracts\Google2FA; -use Pterodactyl\Contracts\Repository\UserRepositoryInterface; -use Pterodactyl\Models\User; -use Pterodactyl\Services\Users\TwoFactorSetupService; use Tests\TestCase; +use Pterodactyl\Models\User; +use Illuminate\Contracts\Config\Repository; +use PragmaRX\Google2FA\Contracts\Google2FA; +use Pterodactyl\Services\Users\TwoFactorSetupService; +use Pterodactyl\Contracts\Repository\UserRepositoryInterface; class TwoFactorSetupServiceTest extends TestCase { diff --git a/tests/Unit/Services/Users/UserDeletionServiceTest.php b/tests/Unit/Services/Users/UserDeletionServiceTest.php index cd955f34..a156573a 100644 --- a/tests/Unit/Services/Users/UserDeletionServiceTest.php +++ b/tests/Unit/Services/Users/UserDeletionServiceTest.php @@ -27,8 +27,8 @@ namespace Tests\Unit\Services\Users; use Mockery as m; use Tests\TestCase; use Pterodactyl\Models\User; -use Pterodactyl\Services\Users\UserDeletionService; use Illuminate\Contracts\Translation\Translator; +use Pterodactyl\Services\Users\UserDeletionService; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; From 4e667b620aa298589154ae6b71bf7cc566c9037e Mon Sep 17 00:00:00 2001 From: Jakob Date: Fri, 1 Sep 2017 02:30:10 +0200 Subject: [PATCH 73/99] travis: add discord notification using webhook --- .travis.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.travis.yml b/.travis.yml index 48ca49a0..275263e6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,5 +20,13 @@ script: - vendor/bin/phpunit --coverage-clover coverage.xml notifications: email: false + webhooks: + urls: + - https://misc.schrej.net/travistodiscord/pterodev.php + on_success: change + on_failure: always + on_error: always + on_cancel: always + on_start: never after_success: - bash <(curl -s https://codecov.io/bash) From 7feb8bcedc8355c69254e5723a0c9313c431b60f Mon Sep 17 00:00:00 2001 From: Georgiy Slobodenyuk Date: Fri, 1 Sep 2017 23:39:10 -0400 Subject: [PATCH 74/99] Fix typo --- app/Console/Commands/UpdateEmailSettings.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Console/Commands/UpdateEmailSettings.php b/app/Console/Commands/UpdateEmailSettings.php index 93d28dfa..960742b9 100644 --- a/app/Console/Commands/UpdateEmailSettings.php +++ b/app/Console/Commands/UpdateEmailSettings.php @@ -161,7 +161,7 @@ class UpdateEmailSettings extends Command file_put_contents($file, $envContents); $bar->finish(); - $this->line('Updating evironment configuration cache file.'); + $this->line('Updating environment configuration cache file.'); $this->call('config:cache'); echo "\n"; } From 53d1182645a1757d33d6cfec258503bef79d40fa Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 2 Sep 2017 00:21:15 -0500 Subject: [PATCH 75/99] Add unit tests for API key controller --- app/Http/Controllers/Base/APIController.php | 5 +- .../Assertions/ControllerAssertionsTrait.php | 45 ++++- .../Controllers/Base/APIControllerTest.php | 181 ++++++++++++++++++ 3 files changed, 225 insertions(+), 6 deletions(-) create mode 100644 tests/Unit/Http/Controllers/Base/APIControllerTest.php diff --git a/app/Http/Controllers/Base/APIController.php b/app/Http/Controllers/Base/APIController.php index 72a4e7b6..21a77cf9 100644 --- a/app/Http/Controllers/Base/APIController.php +++ b/app/Http/Controllers/Base/APIController.php @@ -93,7 +93,7 @@ class APIController extends Controller return view('base.api.new', [ 'permissions' => [ 'user' => collect(APIPermission::CONST_PERMISSIONS)->pull('_user'), - 'admin' => ! $request->user()->root_admin ?: collect(APIPermission::CONST_PERMISSIONS)->except('_user')->toArray(), + 'admin' => ! $request->user()->root_admin ? null : collect(APIPermission::CONST_PERMISSIONS)->except('_user')->toArray(), ], ]); } @@ -103,6 +103,7 @@ class APIController extends Controller * * @param \Pterodactyl\Http\Requests\Base\ApiKeyFormRequest $request * @return \Illuminate\Http\RedirectResponse + * * @throws \Exception * @throws \Pterodactyl\Exceptions\Model\DataValidationException */ @@ -110,7 +111,7 @@ class APIController extends Controller { $adminPermissions = []; if ($request->user()->root_admin) { - $adminPermissions = $request->input('admin_permissions') ?? []; + $adminPermissions = $request->input('admin_permissions', []); } $secret = $this->keyService->handle([ diff --git a/tests/Assertions/ControllerAssertionsTrait.php b/tests/Assertions/ControllerAssertionsTrait.php index 1ed835c8..de3076cc 100644 --- a/tests/Assertions/ControllerAssertionsTrait.php +++ b/tests/Assertions/ControllerAssertionsTrait.php @@ -24,7 +24,9 @@ namespace Tests\Assertions; +use Illuminate\View\View; use PHPUnit_Framework_Assert; +use Illuminate\Http\RedirectResponse; trait ControllerAssertionsTrait { @@ -36,6 +38,7 @@ trait ControllerAssertionsTrait */ public function assertViewNameEquals($name, $view) { + PHPUnit_Framework_Assert::assertInstanceOf(View::class, $view); PHPUnit_Framework_Assert::assertEquals($name, $view->getName()); } @@ -47,6 +50,7 @@ trait ControllerAssertionsTrait */ public function assertViewNameNotEquals($name, $view) { + PHPUnit_Framework_Assert::assertInstanceOf(View::class, $view); PHPUnit_Framework_Assert::assertNotEquals($name, $view->getName()); } @@ -58,7 +62,16 @@ trait ControllerAssertionsTrait */ public function assertViewHasKey($attribute, $view) { - PHPUnit_Framework_Assert::assertArrayHasKey($attribute, $view->getData()); + PHPUnit_Framework_Assert::assertInstanceOf(View::class, $view); + + if (str_contains($attribute, '.')) { + PHPUnit_Framework_Assert::assertNotEquals( + '__TEST__FAIL', + array_get($view->getData(), $attribute, '__TEST__FAIL') + ); + } else { + PHPUnit_Framework_Assert::assertArrayHasKey($attribute, $view->getData()); + } } /** @@ -69,7 +82,16 @@ trait ControllerAssertionsTrait */ public function assertViewNotHasKey($attribute, $view) { - PHPUnit_Framework_Assert::assertArrayNotHasKey($attribute, $view->getData()); + PHPUnit_Framework_Assert::assertInstanceOf(View::class, $view); + + if (str_contains($attribute, '.')) { + PHPUnit_Framework_Assert::assertEquals( + '__TEST__PASS', + array_get($view->getData(), $attribute, '__TEST__PASS') + ); + } else { + PHPUnit_Framework_Assert::assertArrayNotHasKey($attribute, $view->getData()); + } } /** @@ -81,15 +103,30 @@ trait ControllerAssertionsTrait */ public function assertViewKeyEquals($attribute, $value, $view) { - PHPUnit_Framework_Assert::assertEquals($value, array_get($view->getData(), $attribute)); + PHPUnit_Framework_Assert::assertInstanceOf(View::class, $view); + PHPUnit_Framework_Assert::assertEquals($value, array_get($view->getData(), $attribute, '__TEST__FAIL')); } /** - * @param string $route + * Assert that a view attribute does not equal a given parameter. + * + * @param string $attribute + * @param mixed $value + * @param \Illuminate\View\View $view + */ + public function assertViewKeyNotEquals($attribute, $value, $view) + { + PHPUnit_Framework_Assert::assertInstanceOf(View::class, $view); + PHPUnit_Framework_Assert::assertNotEquals($value, array_get($view->getData(), $attribute, '__TEST__FAIL')); + } + + /** + * @param string $route * @param \Illuminate\Http\RedirectResponse $response */ public function assertRouteRedirectEquals($route, $response) { + PHPUnit_Framework_Assert::assertInstanceOf(RedirectResponse::class, $response); PHPUnit_Framework_Assert::assertEquals(route($route), $response->getTargetUrl()); } } diff --git a/tests/Unit/Http/Controllers/Base/APIControllerTest.php b/tests/Unit/Http/Controllers/Base/APIControllerTest.php new file mode 100644 index 00000000..55a991c8 --- /dev/null +++ b/tests/Unit/Http/Controllers/Base/APIControllerTest.php @@ -0,0 +1,181 @@ +. + * + * 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\Http\Controllers\Base; + +use Mockery as m; +use Tests\TestCase; +use Illuminate\Http\Request; +use Pterodactyl\Models\User; +use Illuminate\Http\Response; +use Prologue\Alerts\AlertsMessageBag; +use Tests\Assertions\ControllerAssertionsTrait; +use Pterodactyl\Services\Api\KeyCreationService; +use Pterodactyl\Http\Controllers\Base\APIController; +use Pterodactyl\Http\Requests\Base\ApiKeyFormRequest; +use Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface; + +class APIControllerTest extends TestCase +{ + use ControllerAssertionsTrait; + + /** + * @var \Prologue\Alerts\AlertsMessageBag + */ + protected $alert; + + /** + * @var \Pterodactyl\Http\Controllers\Base\APIController + */ + protected $controller; + + /** + * @var \Pterodactyl\Services\Api\KeyCreationService + */ + protected $keyService; + + /** + * @var \Pterodactyl\Contracts\Repository\ApiKeyRepositoryInterface + */ + protected $repository; + + /** + * @var \Illuminate\Http\Request + */ + protected $request; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->alert = m::mock(AlertsMessageBag::class); + $this->keyService = m::mock(KeyCreationService::class); + $this->repository = m::mock(ApiKeyRepositoryInterface::class); + $this->request = m::mock(Request::class); + + $this->controller = new APIController($this->alert, $this->repository, $this->keyService); + } + + /** + * Test the index controller. + */ + public function testIndexController() + { + $model = factory(User::class)->make(); + + $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn($model); + $this->repository->shouldReceive('findWhere')->with([['user_id', '=', $model->id]])->once()->andReturn(['testkeys']); + + $response = $this->controller->index($this->request); + $this->assertViewNameEquals('base.api.index', $response); + $this->assertViewHasKey('keys', $response); + $this->assertViewKeyEquals('keys', ['testkeys'], $response); + } + + /** + * Test the create API view controller. + * + * @dataProvider rootAdminDataProvider + */ + public function testCreateController($admin) + { + $model = factory(User::class)->make(['root_admin' => $admin]); + $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn($model); + + $response = $this->controller->create($this->request); + $this->assertViewNameEquals('base.api.new', $response); + $this->assertViewHasKey('permissions.user', $response); + $this->assertViewHasKey('permissions.admin', $response); + + if ($admin) { + $this->assertViewKeyNotEquals('permissions.admin', null, $response); + } else { + $this->assertViewKeyEquals('permissions.admin', null, $response); + } + } + + /** + * Test the store functionality for a non-admin user. + * + * @dataProvider rootAdminDataProvider + */ + public function testStoreController($admin) + { + $this->request = m::mock(ApiKeyFormRequest::class); + $model = factory(User::class)->make(['root_admin' => $admin]); + + if ($admin) { + $this->request->shouldReceive('input')->with('admin_permissions', [])->once()->andReturn(['admin.permission']); + } + + $this->request->shouldReceive('user')->withNoArgs()->andReturn($model); + $this->request->shouldReceive('input')->with('allowed_ips')->once()->andReturnNull(); + $this->request->shouldReceive('input')->with('memo')->once()->andReturnNull(); + $this->request->shouldReceive('input')->with('permissions', [])->once()->andReturn(['test.permission']); + + $this->keyService->shouldReceive('handle')->with([ + 'user_id' => $model->id, + 'allowed_ips' => null, + 'memo' => null, + ], ['test.permission'], ($admin) ? ['admin.permission'] : [])->once()->andReturn('testToken'); + + $this->alert->shouldReceive('success')->with(trans('base.api.index.keypair_created', ['token' => 'testToken']))->once()->andReturnSelf() + ->shouldReceive('flash')->withNoArgs()->once()->andReturnNull(); + + $response = $this->controller->store($this->request); + $this->assertRouteRedirectEquals('account.api', $response); + } + + /** + * Test the API key revocation controller. + */ + public function testRevokeController() + { + $model = factory(User::class)->make(); + $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn($model); + + $this->repository->shouldReceive('deleteWhere')->with([ + ['user_id', '=', $model->id], + ['public', '=', 'testKey123'], + ])->once()->andReturnNull(); + + $response = $this->controller->revoke($this->request, 'testKey123'); + $this->assertInstanceOf(Response::class, $response); + $this->assertEmpty($response->getContent()); + $this->assertEquals(204, $response->getStatusCode()); + } + + /** + * Data provider to determine if a user is a root admin. + * + * @return array + */ + public function rootAdminDataProvider() + { + return [[0], [1]]; + } +} From 37508a370d6399e3f7cfab9bc1af8af91a44b86c Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 2 Sep 2017 18:56:15 -0500 Subject: [PATCH 76/99] Finish up unit tests for base controllers --- .../Controllers/Base/SecurityController.php | 16 ++ .../Assertions/ControllerAssertionsTrait.php | 128 ++++++++--- .../Admin/DatabaseControllerTest.php | 26 ++- .../Controllers/Base/APIControllerTest.php | 8 +- .../Base/AccountControllerTest.php | 4 + .../Controllers/Base/IndexControllerTest.php | 159 ++++++++++++++ .../Base/SecurityControllerTest.php | 206 ++++++++++++++++++ 7 files changed, 507 insertions(+), 40 deletions(-) create mode 100644 tests/Unit/Http/Controllers/Base/IndexControllerTest.php create mode 100644 tests/Unit/Http/Controllers/Base/SecurityControllerTest.php diff --git a/app/Http/Controllers/Base/SecurityController.php b/app/Http/Controllers/Base/SecurityController.php index b44ecc12..d22c0ddb 100644 --- a/app/Http/Controllers/Base/SecurityController.php +++ b/app/Http/Controllers/Base/SecurityController.php @@ -67,6 +67,16 @@ class SecurityController extends Controller */ protected $twoFactorSetupService; + /** + * SecurityController constructor. + * + * @param \Prologue\Alerts\AlertsMessageBag $alert + * @param \Illuminate\Contracts\Config\Repository $config + * @param \Illuminate\Contracts\Session\Session $session + * @param \Pterodactyl\Contracts\Repository\SessionRepositoryInterface $repository + * @param \Pterodactyl\Services\Users\ToggleTwoFactorService $toggleTwoFactorService + * @param \Pterodactyl\Services\Users\TwoFactorSetupService $twoFactorSetupService + */ public function __construct( AlertsMessageBag $alert, ConfigRepository $config, @@ -120,6 +130,9 @@ class SecurityController extends Controller * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function setTotp(Request $request) { @@ -137,6 +150,9 @@ class SecurityController extends Controller * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\RedirectResponse + * + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function disableTotp(Request $request) { diff --git a/tests/Assertions/ControllerAssertionsTrait.php b/tests/Assertions/ControllerAssertionsTrait.php index de3076cc..a3558838 100644 --- a/tests/Assertions/ControllerAssertionsTrait.php +++ b/tests/Assertions/ControllerAssertionsTrait.php @@ -24,46 +24,84 @@ namespace Tests\Assertions; +use Illuminate\Http\JsonResponse; +use Illuminate\Http\Response; use Illuminate\View\View; use PHPUnit_Framework_Assert; use Illuminate\Http\RedirectResponse; trait ControllerAssertionsTrait { + /** + * Assert that a response is an instance of Illuminate View. + * + * @param mixed $response + */ + public function assertIsViewResponse($response) + { + PHPUnit_Framework_Assert::assertInstanceOf(View::class, $response); + } + + /** + * Assert that a response is an instance of Illuminate Redirect Response. + * + * @param mixed $response + */ + public function assertIsRedirectResponse($response) + { + PHPUnit_Framework_Assert::assertInstanceOf(RedirectResponse::class, $response); + } + + /** + * Assert that a response is an instance of Illuminate Json Response. + * + * @param mixed $response + */ + public function assertIsJsonResponse($response) + { + PHPUnit_Framework_Assert::assertInstanceOf(JsonResponse::class, $response); + } + + /** + * Assert that a response is an instance of Illuminate Response. + * + * @param mixed $response + */ + public function assertIsResponse($response) + { + PHPUnit_Framework_Assert::assertInstanceOf(Response::class, $response); + } + /** * Assert that a view name equals the passed name. * - * @param string $name - * @param \Illuminate\View\View $view + * @param string $name + * @param mixed $view */ public function assertViewNameEquals($name, $view) { - PHPUnit_Framework_Assert::assertInstanceOf(View::class, $view); PHPUnit_Framework_Assert::assertEquals($name, $view->getName()); } /** * Assert that a view name does not equal a provided name. * - * @param string $name - * @param \Illuminate\View\View $view + * @param string $name + * @param mixed $view */ public function assertViewNameNotEquals($name, $view) { - PHPUnit_Framework_Assert::assertInstanceOf(View::class, $view); PHPUnit_Framework_Assert::assertNotEquals($name, $view->getName()); } /** * Assert that a view has an attribute passed into it. * - * @param string $attribute - * @param \Illuminate\View\View $view + * @param string $attribute + * @param mixed $view */ public function assertViewHasKey($attribute, $view) { - PHPUnit_Framework_Assert::assertInstanceOf(View::class, $view); - if (str_contains($attribute, '.')) { PHPUnit_Framework_Assert::assertNotEquals( '__TEST__FAIL', @@ -77,13 +115,11 @@ trait ControllerAssertionsTrait /** * Assert that a view does not have a specific attribute passed in. * - * @param string $attribute - * @param \Illuminate\View\View $view + * @param string $attribute + * @param mixed $view */ public function assertViewNotHasKey($attribute, $view) { - PHPUnit_Framework_Assert::assertInstanceOf(View::class, $view); - if (str_contains($attribute, '.')) { PHPUnit_Framework_Assert::assertEquals( '__TEST__PASS', @@ -97,36 +133,78 @@ trait ControllerAssertionsTrait /** * Assert that a view attribute equals a given parameter. * - * @param string $attribute - * @param mixed $value - * @param \Illuminate\View\View $view + * @param string $attribute + * @param mixed $value + * @param mixed $view */ public function assertViewKeyEquals($attribute, $value, $view) { - PHPUnit_Framework_Assert::assertInstanceOf(View::class, $view); PHPUnit_Framework_Assert::assertEquals($value, array_get($view->getData(), $attribute, '__TEST__FAIL')); } /** * Assert that a view attribute does not equal a given parameter. * - * @param string $attribute - * @param mixed $value - * @param \Illuminate\View\View $view + * @param string $attribute + * @param mixed $value + * @param mixed $view */ public function assertViewKeyNotEquals($attribute, $value, $view) { - PHPUnit_Framework_Assert::assertInstanceOf(View::class, $view); PHPUnit_Framework_Assert::assertNotEquals($value, array_get($view->getData(), $attribute, '__TEST__FAIL')); } /** - * @param string $route - * @param \Illuminate\Http\RedirectResponse $response + * Assert that a route redirect equals a given route name. + * + * @param string $route + * @param mixed $response */ public function assertRouteRedirectEquals($route, $response) { - PHPUnit_Framework_Assert::assertInstanceOf(RedirectResponse::class, $response); PHPUnit_Framework_Assert::assertEquals(route($route), $response->getTargetUrl()); } + + /** + * Assert that a response code equals a given code. + * + * @param int $code + * @param mixed $response + */ + public function assertResponseCodeEquals($code, $response) + { + PHPUnit_Framework_Assert::assertEquals($code, $response->getStatusCode()); + } + + /** + * Assert that a response code does not equal a given code. + * + * @param int $code + * @param mixed $response + */ + public function assertResponseCodeNotEquals($code, $response) + { + PHPUnit_Framework_Assert::assertNotEquals($code, $response->getStatusCode()); + } + + /** + * Assert that a response is in a JSON format. + * + * @param mixed $response + */ + public function assertResponseHasJsonHeaders($response) + { + PHPUnit_Framework_Assert::assertEquals('application/json', $response->headers->get('content-type')); + } + + /** + * Assert that response JSON matches a given JSON string. + * + * @param array|string $json + * @param mixed $response + */ + public function assertResponseJsonEquals($json, $response) + { + PHPUnit_Framework_Assert::assertEquals(is_array($json) ? json_encode($json) : $json, $response->getContent()); + } } diff --git a/tests/Unit/Http/Controllers/Admin/DatabaseControllerTest.php b/tests/Unit/Http/Controllers/Admin/DatabaseControllerTest.php index c79ddd71..8a7c9103 100644 --- a/tests/Unit/Http/Controllers/Admin/DatabaseControllerTest.php +++ b/tests/Unit/Http/Controllers/Admin/DatabaseControllerTest.php @@ -90,13 +90,14 @@ class DatabaseControllerTest extends TestCase $this->locationRepository->shouldReceive('getAllWithNodes')->withNoArgs()->once()->andReturn('getAllWithNodes'); $this->repository->shouldReceive('getWithViewDetails')->withNoArgs()->once()->andReturn('getWithViewDetails'); - $view = $this->controller->index(); + $response = $this->controller->index(); - $this->assertViewNameEquals('admin.databases.index', $view); - $this->assertViewHasKey('locations', $view); - $this->assertViewHasKey('hosts', $view); - $this->assertViewKeyEquals('locations', 'getAllWithNodes', $view); - $this->assertViewKeyEquals('hosts', 'getWithViewDetails', $view); + $this->assertIsViewResponse($response); + $this->assertViewNameEquals('admin.databases.index', $response); + $this->assertViewHasKey('locations', $response); + $this->assertViewHasKey('hosts', $response); + $this->assertViewKeyEquals('locations', 'getAllWithNodes', $response); + $this->assertViewKeyEquals('hosts', 'getWithViewDetails', $response); } /** @@ -107,12 +108,13 @@ class DatabaseControllerTest extends TestCase $this->locationRepository->shouldReceive('getAllWithNodes')->withNoArgs()->once()->andReturn('getAllWithNodes'); $this->repository->shouldReceive('getWithServers')->with(1)->once()->andReturn('getWithServers'); - $view = $this->controller->view(1); + $response = $this->controller->view(1); - $this->assertViewNameEquals('admin.databases.view', $view); - $this->assertViewHasKey('locations', $view); - $this->assertViewHasKey('host', $view); - $this->assertViewKeyEquals('locations', 'getAllWithNodes', $view); - $this->assertViewKeyEquals('host', 'getWithServers', $view); + $this->assertIsViewResponse($response); + $this->assertViewNameEquals('admin.databases.view', $response); + $this->assertViewHasKey('locations', $response); + $this->assertViewHasKey('host', $response); + $this->assertViewKeyEquals('locations', 'getAllWithNodes', $response); + $this->assertViewKeyEquals('host', 'getWithServers', $response); } } diff --git a/tests/Unit/Http/Controllers/Base/APIControllerTest.php b/tests/Unit/Http/Controllers/Base/APIControllerTest.php index 55a991c8..cc2c1302 100644 --- a/tests/Unit/Http/Controllers/Base/APIControllerTest.php +++ b/tests/Unit/Http/Controllers/Base/APIControllerTest.php @@ -28,7 +28,6 @@ use Mockery as m; use Tests\TestCase; use Illuminate\Http\Request; use Pterodactyl\Models\User; -use Illuminate\Http\Response; use Prologue\Alerts\AlertsMessageBag; use Tests\Assertions\ControllerAssertionsTrait; use Pterodactyl\Services\Api\KeyCreationService; @@ -91,6 +90,7 @@ class APIControllerTest extends TestCase $this->repository->shouldReceive('findWhere')->with([['user_id', '=', $model->id]])->once()->andReturn(['testkeys']); $response = $this->controller->index($this->request); + $this->assertIsViewResponse($response); $this->assertViewNameEquals('base.api.index', $response); $this->assertViewHasKey('keys', $response); $this->assertViewKeyEquals('keys', ['testkeys'], $response); @@ -107,6 +107,7 @@ class APIControllerTest extends TestCase $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn($model); $response = $this->controller->create($this->request); + $this->assertIsViewResponse($response); $this->assertViewNameEquals('base.api.new', $response); $this->assertViewHasKey('permissions.user', $response); $this->assertViewHasKey('permissions.admin', $response); @@ -147,6 +148,7 @@ class APIControllerTest extends TestCase ->shouldReceive('flash')->withNoArgs()->once()->andReturnNull(); $response = $this->controller->store($this->request); + $this->assertIsRedirectResponse($response); $this->assertRouteRedirectEquals('account.api', $response); } @@ -164,9 +166,9 @@ class APIControllerTest extends TestCase ])->once()->andReturnNull(); $response = $this->controller->revoke($this->request, 'testKey123'); - $this->assertInstanceOf(Response::class, $response); + $this->assertIsResponse($response); $this->assertEmpty($response->getContent()); - $this->assertEquals(204, $response->getStatusCode()); + $this->assertResponseCodeEquals(204, $response); } /** diff --git a/tests/Unit/Http/Controllers/Base/AccountControllerTest.php b/tests/Unit/Http/Controllers/Base/AccountControllerTest.php index 88d749c2..7d8258b0 100644 --- a/tests/Unit/Http/Controllers/Base/AccountControllerTest.php +++ b/tests/Unit/Http/Controllers/Base/AccountControllerTest.php @@ -77,6 +77,7 @@ class AccountControllerTest extends TestCase { $response = $this->controller->index(); + $this->assertIsViewResponse($response); $this->assertViewNameEquals('base.account', $response); } @@ -93,6 +94,7 @@ class AccountControllerTest extends TestCase $this->alert->shouldReceive('success->flash')->once()->andReturnNull(); $response = $this->controller->update($this->request); + $this->assertIsRedirectResponse($response); $this->assertRouteRedirectEquals('account', $response); } @@ -109,6 +111,7 @@ class AccountControllerTest extends TestCase $this->alert->shouldReceive('success->flash')->once()->andReturnNull(); $response = $this->controller->update($this->request); + $this->assertIsRedirectResponse($response); $this->assertRouteRedirectEquals('account', $response); } @@ -127,6 +130,7 @@ class AccountControllerTest extends TestCase $this->alert->shouldReceive('success->flash')->once()->andReturnNull(); $response = $this->controller->update($this->request); + $this->assertIsRedirectResponse($response); $this->assertRouteRedirectEquals('account', $response); } } diff --git a/tests/Unit/Http/Controllers/Base/IndexControllerTest.php b/tests/Unit/Http/Controllers/Base/IndexControllerTest.php new file mode 100644 index 00000000..529f7c69 --- /dev/null +++ b/tests/Unit/Http/Controllers/Base/IndexControllerTest.php @@ -0,0 +1,159 @@ +. + * + * 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\Http\Controllers\Base; + +use Illuminate\Http\Request; +use Mockery as m; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Http\Controllers\Base\IndexController; +use Pterodactyl\Models\Server; +use Pterodactyl\Models\User; +use Pterodactyl\Services\Servers\ServerAccessHelperService; +use Tests\Assertions\ControllerAssertionsTrait; +use Tests\TestCase; + +class IndexControllerTest extends TestCase +{ + use ControllerAssertionsTrait; + + /** + * @var \Pterodactyl\Services\Servers\ServerAccessHelperService + */ + protected $access; + + /** + * @var \Pterodactyl\Http\Controllers\Base\IndexController + */ + protected $controller; + + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface + */ + protected $daemonRepository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $repository; + + /** + * @var \Illuminate\Http\Request + */ + protected $request; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->access = m::mock(ServerAccessHelperService::class); + $this->daemonRepository = m::mock(DaemonServerRepositoryInterface::class); + $this->repository = m::mock(ServerRepositoryInterface::class); + $this->request = m::mock(Request::class); + + $this->controller = new IndexController($this->daemonRepository, $this->access, $this->repository); + } + + /** + * Test the index controller. + */ + public function testIndexController() + { + $model = factory(User::class)->make(); + + $this->request->shouldReceive('user')->withNoArgs()->andReturn($model); + $this->request->shouldReceive('input')->with('query')->once()->andReturn('searchTerm'); + $this->repository->shouldReceive('search')->with('searchTerm')->once()->andReturnSelf() + ->shouldReceive('filterUserAccessServers')->with( + $model->id, $model->root_admin, 'all', ['user'] + )->once()->andReturn(['test']); + + $response = $this->controller->getIndex($this->request); + $this->assertIsViewResponse($response); + $this->assertViewNameEquals('base.index', $response); + $this->assertViewHasKey('servers', $response); + $this->assertViewKeyEquals('servers', ['test'], $response); + } + + /** + * Test the status controller. + */ + public function testStatusController() + { + $user = factory(User::class)->make(); + $server = factory(Server::class)->make(['suspended' => 0, 'installed' => 1]); + + $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn($user); + $this->access->shouldReceive('handle')->with($server->uuid, $user)->once()->andReturn($server); + + $this->daemonRepository->shouldReceive('setNode')->with($server->node_id)->once()->andReturnSelf() + ->shouldReceive('setAccessServer')->with($server->uuid)->once()->andReturnSelf() + ->shouldReceive('setAccessToken')->with($server->daemonSecret)->once()->andReturnSelf() + ->shouldReceive('details')->withNoArgs()->once()->andReturnSelf(); + + $this->daemonRepository->shouldReceive('getBody')->withNoArgs()->once()->andReturn('["test"]'); + + $response = $this->controller->status($this->request, $server->uuid); + $this->assertIsJsonResponse($response); + $this->assertResponseJsonEquals(['test'], $response); + } + + /** + * Test the status controller if a server is not installed. + */ + public function testStatusControllerWhenServerNotInstalled() + { + $user = factory(User::class)->make(); + $server = factory(Server::class)->make(['suspended' => 0, 'installed' => 0]); + + $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn($user); + $this->access->shouldReceive('handle')->with($server->uuid, $user)->once()->andReturn($server); + + $response = $this->controller->status($this->request, $server->uuid); + $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->access->shouldReceive('handle')->with($server->uuid, $user)->once()->andReturn($server); + + $response = $this->controller->status($this->request, $server->uuid); + $this->assertIsJsonResponse($response); + $this->assertResponseCodeEquals(200, $response); + $this->assertResponseJsonEquals(['status' => 30], $response); + } +} diff --git a/tests/Unit/Http/Controllers/Base/SecurityControllerTest.php b/tests/Unit/Http/Controllers/Base/SecurityControllerTest.php new file mode 100644 index 00000000..04186b39 --- /dev/null +++ b/tests/Unit/Http/Controllers/Base/SecurityControllerTest.php @@ -0,0 +1,206 @@ +. + * + * 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\Http\Controllers\Base; + +use Illuminate\Contracts\Config\Repository; +use Illuminate\Contracts\Session\Session; +use Illuminate\Http\Request; +use Mockery as m; +use Prologue\Alerts\AlertsMessageBag; +use Pterodactyl\Contracts\Repository\SessionRepositoryInterface; +use Pterodactyl\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid; +use Pterodactyl\Http\Controllers\Base\SecurityController; +use Pterodactyl\Models\User; +use Pterodactyl\Services\Users\ToggleTwoFactorService; +use Pterodactyl\Services\Users\TwoFactorSetupService; +use Tests\Assertions\ControllerAssertionsTrait; +use Tests\TestCase; + +class SecurityControllerTest extends TestCase +{ + use ControllerAssertionsTrait; + + /** + * @var \Prologue\Alerts\AlertsMessageBag + */ + protected $alert; + + /** + * @var \Illuminate\Contracts\Config\Repository + */ + protected $config; + + /** + * @var \Pterodactyl\Http\Controllers\Base\SecurityController + */ + protected $controller; + + /** + * @var \Pterodactyl\Contracts\Repository\SessionRepositoryInterface + */ + protected $repository; + + /** + * @var \Illuminate\Http\Request + */ + protected $request; + + /** + * @var \Illuminate\Contracts\Session\Session + */ + protected $session; + + /** + * @var \Pterodactyl\Services\Users\ToggleTwoFactorService + */ + protected $toggleTwoFactorService; + + /** + * @var \Pterodactyl\Services\Users\TwoFactorSetupService + */ + protected $twoFactorSetupService; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->alert = m::mock(AlertsMessageBag::class); + $this->config = m::mock(Repository::class); + $this->repository = m::mock(SessionRepositoryInterface::class); + $this->request = m::mock(Request::class); + $this->session = m::mock(Session::class); + $this->toggleTwoFactorService = m::mock(ToggleTwoFactorService::class); + $this->twoFactorSetupService = m::mock(TwoFactorSetupService::class); + + $this->controller = new SecurityController( + $this->alert, + $this->config, + $this->session, + $this->repository, + $this->toggleTwoFactorService, + $this->twoFactorSetupService + ); + } + + /** + * Test the index controller when using a database driver. + */ + public function testIndexControllerWithDatabaseDriver() + { + $model = factory(User::class)->make(); + + $this->config->shouldReceive('get')->with('session.driver')->once()->andReturn('database'); + $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn($model); + $this->repository->shouldReceive('getUserSessions')->with($model->id)->once()->andReturn(['sessions']); + + $response = $this->controller->index($this->request); + $this->assertIsViewResponse($response); + $this->assertViewNameEquals('base.security', $response); + $this->assertViewHasKey('sessions', $response); + $this->assertViewKeyEquals('sessions', ['sessions'], $response); + } + + /** + * Test the index controller when not using the database driver. + */ + public function testIndexControllerWithoutDatabaseDriver() + { + $this->config->shouldReceive('get')->with('session.driver')->once()->andReturn('redis'); + + $response = $this->controller->index($this->request); + $this->assertIsViewResponse($response); + $this->assertViewNameEquals('base.security', $response); + $this->assertViewHasKey('sessions', $response); + $this->assertViewKeyEquals('sessions', null, $response); + } + + /** + * Test TOTP generation controller. + */ + public function testGenerateTotpController() + { + $model = factory(User::class)->make(); + + $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn($model); + $this->twoFactorSetupService->shouldReceive('handle')->with($model)->once()->andReturn(['string']); + + $response = $this->controller->generateTotp($this->request); + $this->assertIsJsonResponse($response); + $this->assertResponseJsonEquals(['string'], $response); + } + + /** + * Test the disable totp controller when no exception is thrown by the service. + */ + public function testDisableTotpControllerSuccess() + { + $model = factory(User::class)->make(); + + $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn($model); + $this->request->shouldReceive('input')->with('token')->once()->andReturn('testToken'); + $this->toggleTwoFactorService->shouldReceive('handle')->with($model, 'testToken', false)->once()->andReturnNull(); + + $response = $this->controller->disableTotp($this->request); + $this->assertIsRedirectResponse($response); + $this->assertRouteRedirectEquals('account.security', $response); + } + + /** + * Test the disable totp controller when an exception is thrown by the service. + */ + public function testDisableTotpControllerWhenExceptionIsThrown() + { + $model = factory(User::class)->make(); + + $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn($model); + $this->request->shouldReceive('input')->with('token')->once()->andReturn('testToken'); + $this->toggleTwoFactorService->shouldReceive('handle')->with($model, 'testToken', false)->once() + ->andThrow(new TwoFactorAuthenticationTokenInvalid); + $this->alert->shouldReceive('danger')->with(trans('base.security.2fa_disable_error'))->once()->andReturnSelf() + ->shouldReceive('flash')->withNoArgs()->once()->andReturnNull(); + + $response = $this->controller->disableTotp($this->request); + $this->assertIsRedirectResponse($response); + $this->assertRouteRedirectEquals('account.security', $response); + } + + /** + * Test the revoke controller. + */ + public function testRevokeController() + { + $model = factory(User::class)->make(); + + $this->request->shouldReceive('user')->withNoArgs()->once()->andReturn($model); + $this->repository->shouldReceive('deleteUserSession')->with($model->id, 123)->once()->andReturnNull(); + + $response = $this->controller->revoke($this->request, 123); + $this->assertIsRedirectResponse($response); + $this->assertRouteRedirectEquals('account.security', $response); + } +} From bae76c27683ecb10f6ae1661f6f12acb2e27ab3b Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 2 Sep 2017 19:39:49 -0500 Subject: [PATCH 77/99] Fix support for PHP 7.0 --- .travis.yml | 2 +- composer.json | 61 +-- composer.lock | 1387 ++++++++++++++++++++++--------------------------- 3 files changed, 640 insertions(+), 810 deletions(-) diff --git a/.travis.yml b/.travis.yml index 275263e6..b0590806 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ dist: trusty php: - '7.0' - '7.1' - - '7.2' +# - '7.2' sudo: false cache: directories: diff --git a/composer.json b/composer.json index 86d2c16c..94ccfeee 100644 --- a/composer.json +++ b/composer.json @@ -11,43 +11,43 @@ } ], "require": { - "php": ">=7.0.0", + "php": ">=7.0", "ext-mbstring": "*", "ext-pdo_mysql": "*", "ext-zip": "*", - "aws/aws-sdk-php": "3.29.7", - "barryvdh/laravel-debugbar": "2.4.0", - "daneeveritt/login-notifications": "1.0.0", - "doctrine/dbal": "2.5.13", - "edvinaskrucas/settings": "2.0.0", - "fideloper/proxy": "3.3.3", - "friendsofphp/php-cs-fixer": "2.4.0", - "guzzlehttp/guzzle": "6.2.3", - "igaster/laravel-theme": "1.16.0", - "laracasts/utilities": "2.1.0", + "aws/aws-sdk-php": "^3.29", + "daneeveritt/login-notifications": "^1.0", + "doctrine/dbal": "^2.5", + "edvinaskrucas/settings": "^2.0", + "fideloper/proxy": "^3.3", + "guzzlehttp/guzzle": "~6.3.0", + "igaster/laravel-theme": "^1.16", + "laracasts/utilities": "^3.0", "laravel/framework": "5.4.27", "laravel/tinker": "1.0.1", - "lord/laroute": "2.4.4", - "mtdowling/cron-expression": "1.2.0", - "nesbot/carbon": "1.22.1", - "nicolaslopezj/searchable": "1.9.5", - "pragmarx/google2fa": "1.0.1", - "predis/predis": "1.1.1", - "prologue/alerts": "0.4.1", - "ramsey/uuid": "3.6.1", - "s1lentium/iptools": "1.1.0", - "sofa/eloquence": "5.4.1", - "spatie/laravel-fractal": "4.0.1", - "watson/validating": "3.0.1", + "lord/laroute": "~2.4.5", + "mtdowling/cron-expression": "^1.2", + "nesbot/carbon": "^1.22", + "nicolaslopezj/searchable": "^1.9", + "pragmarx/google2fa": "^1.0", + "predis/predis": "^1.1", + "prologue/alerts": "^0.4", + "ramsey/uuid": "^3.7", + "s1lentium/iptools": "^1.1", + "sofa/eloquence": "~5.4.1", + "spatie/laravel-fractal": "^4.0", + "watson/validating": "^3.0", "webmozart/assert": "^1.2", - "webpatser/laravel-uuid": "2.0.1" + "webpatser/laravel-uuid": "^2.0" }, "require-dev": { - "barryvdh/laravel-ide-helper": "2.4.1", - "fzaninotto/faker": "1.6.0", - "mockery/mockery": "0.9.9", - "php-mock/php-mock-phpunit": "1.1.2", - "phpunit/phpunit": "5.7.21" + "barryvdh/laravel-debugbar": "^2.4", + "barryvdh/laravel-ide-helper": "^2.4", + "friendsofphp/php-cs-fixer": "~2.4.0", + "fzaninotto/faker": "^1.6", + "mockery/mockery": "^0.9", + "php-mock/php-mock-phpunit": "^1.1", + "phpunit/phpunit": "^5.7" }, "autoload": { "classmap": [ @@ -85,6 +85,9 @@ }, "prefer-stable": true, "config": { + "platform": { + "php": "7.0" + }, "preferred-install": "dist", "sort-packages": true, "optimize-autoloader": true diff --git a/composer.lock b/composer.lock index 7b299fd3..eecee6ea 100644 --- a/composer.lock +++ b/composer.lock @@ -4,23 +4,27 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "e641798f79e2865a130f8b2d4f31a91b", + "content-hash": "a0014dfc711e382fff7903d9aeaffc25", "packages": [ { "name": "aws/aws-sdk-php", - "version": "3.29.7", + "version": "3.36.0", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "76540001ff938c072db5367a7c945296984b999b" + "reference": "69321675769dd3e3d00a94bfdc747bd3a5b75b3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/76540001ff938c072db5367a7c945296984b999b", - "reference": "76540001ff938c072db5367a7c945296984b999b", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/69321675769dd3e3d00a94bfdc747bd3a5b75b3b", + "reference": "69321675769dd3e3d00a94bfdc747bd3a5b75b3b", "shasum": "" }, "require": { + "ext-json": "*", + "ext-pcre": "*", + "ext-simplexml": "*", + "ext-spl": "*", "guzzlehttp/guzzle": "^5.3.1|^6.2.1", "guzzlehttp/promises": "~1.0", "guzzlehttp/psr7": "^1.4.1", @@ -33,11 +37,7 @@ "behat/behat": "~3.0", "doctrine/cache": "~1.4", "ext-dom": "*", - "ext-json": "*", "ext-openssl": "*", - "ext-pcre": "*", - "ext-simplexml": "*", - "ext-spl": "*", "nette/neon": "^2.3", "phpunit/phpunit": "^4.8.35|^5.4.0", "psr/cache": "^1.0" @@ -84,69 +84,7 @@ "s3", "sdk" ], - "time": "2017-06-16T17:29:33+00:00" - }, - { - "name": "barryvdh/laravel-debugbar", - "version": "v2.4.0", - "source": { - "type": "git", - "url": "https://github.com/barryvdh/laravel-debugbar.git", - "reference": "de15d00a74696db62e1b4782474c27ed0c4fc763" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/de15d00a74696db62e1b4782474c27ed0c4fc763", - "reference": "de15d00a74696db62e1b4782474c27ed0c4fc763", - "shasum": "" - }, - "require": { - "illuminate/support": "5.1.*|5.2.*|5.3.*|5.4.*|5.5.*", - "maximebf/debugbar": "~1.13.0", - "php": ">=5.5.9", - "symfony/finder": "~2.7|~3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.4-dev" - }, - "laravel": { - "providers": [ - "Barryvdh\\Debugbar\\ServiceProvider" - ], - "aliases": { - "Debugbar": "Barryvdh\\Debugbar\\Facade" - } - } - }, - "autoload": { - "psr-4": { - "Barryvdh\\Debugbar\\": "src/" - }, - "files": [ - "src/helpers.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Barry vd. Heuvel", - "email": "barryvdh@gmail.com" - } - ], - "description": "PHP Debugbar integration for Laravel", - "keywords": [ - "debug", - "debugbar", - "laravel", - "profiler", - "webprofiler" - ], - "time": "2017-06-01T17:46:08+00:00" + "time": "2017-09-01T22:52:38+00:00" }, { "name": "christian-riesen/base32", @@ -282,21 +220,21 @@ }, { "name": "doctrine/annotations", - "version": "v1.5.0", + "version": "v1.4.0", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "5beebb01b025c94e93686b7a0ed3edae81fe3e7f" + "reference": "54cacc9b81758b14e3ce750f205a393d52339e97" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/5beebb01b025c94e93686b7a0ed3edae81fe3e7f", - "reference": "5beebb01b025c94e93686b7a0ed3edae81fe3e7f", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/54cacc9b81758b14e3ce750f205a393d52339e97", + "reference": "54cacc9b81758b14e3ce750f205a393d52339e97", "shasum": "" }, "require": { "doctrine/lexer": "1.*", - "php": "^7.1" + "php": "^5.6 || ^7.0" }, "require-dev": { "doctrine/cache": "1.*", @@ -305,7 +243,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.5.x-dev" + "dev-master": "1.4.x-dev" } }, "autoload": { @@ -346,41 +284,37 @@ "docblock", "parser" ], - "time": "2017-07-22T10:58:02+00:00" + "time": "2017-02-24T16:22:25+00:00" }, { "name": "doctrine/cache", - "version": "v1.7.0", + "version": "v1.6.2", "source": { "type": "git", "url": "https://github.com/doctrine/cache.git", - "reference": "53d9518ffeb019c51d542ff60cb578f076d3ff16" + "reference": "eb152c5100571c7a45470ff2a35095ab3f3b900b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/cache/zipball/53d9518ffeb019c51d542ff60cb578f076d3ff16", - "reference": "53d9518ffeb019c51d542ff60cb578f076d3ff16", + "url": "https://api.github.com/repos/doctrine/cache/zipball/eb152c5100571c7a45470ff2a35095ab3f3b900b", + "reference": "eb152c5100571c7a45470ff2a35095ab3f3b900b", "shasum": "" }, "require": { - "php": "~7.1" + "php": "~5.5|~7.0" }, "conflict": { "doctrine/common": ">2.2,<2.4" }, "require-dev": { - "alcaeus/mongo-php-adapter": "^1.1", - "mongodb/mongodb": "^1.1", - "phpunit/phpunit": "^5.7", - "predis/predis": "~1.0" - }, - "suggest": { - "alcaeus/mongo-php-adapter": "Required to use legacy MongoDB driver" + "phpunit/phpunit": "~4.8|~5.0", + "predis/predis": "~1.0", + "satooshi/php-coveralls": "~0.6" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.7.x-dev" + "dev-master": "1.6.x-dev" } }, "autoload": { @@ -420,24 +354,24 @@ "cache", "caching" ], - "time": "2017-07-22T13:00:15+00:00" + "time": "2017-07-22T12:49:21+00:00" }, { "name": "doctrine/collections", - "version": "v1.5.0", + "version": "v1.4.0", "source": { "type": "git", "url": "https://github.com/doctrine/collections.git", - "reference": "a01ee38fcd999f34d9bfbcee59dbda5105449cbf" + "reference": "1a4fb7e902202c33cce8c55989b945612943c2ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/collections/zipball/a01ee38fcd999f34d9bfbcee59dbda5105449cbf", - "reference": "a01ee38fcd999f34d9bfbcee59dbda5105449cbf", + "url": "https://api.github.com/repos/doctrine/collections/zipball/1a4fb7e902202c33cce8c55989b945612943c2ba", + "reference": "1a4fb7e902202c33cce8c55989b945612943c2ba", "shasum": "" }, "require": { - "php": "^7.1" + "php": "^5.6 || ^7.0" }, "require-dev": { "doctrine/coding-standard": "~0.1@dev", @@ -487,7 +421,7 @@ "collections", "iterator" ], - "time": "2017-07-22T10:37:32+00:00" + "time": "2017-01-03T10:49:41+00:00" }, { "name": "doctrine/common", @@ -849,16 +783,16 @@ }, { "name": "fideloper/proxy", - "version": "3.3.3", + "version": "3.3.4", "source": { "type": "git", "url": "https://github.com/fideloper/TrustedProxy.git", - "reference": "985eac8f966c03b4d9503cad9b5e5a51d41ce477" + "reference": "9cdf6f118af58d89764249bbcc7bb260c132924f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fideloper/TrustedProxy/zipball/985eac8f966c03b4d9503cad9b5e5a51d41ce477", - "reference": "985eac8f966c03b4d9503cad9b5e5a51d41ce477", + "url": "https://api.github.com/repos/fideloper/TrustedProxy/zipball/9cdf6f118af58d89764249bbcc7bb260c132924f", + "reference": "9cdf6f118af58d89764249bbcc7bb260c132924f", "shasum": "" }, "require": { @@ -874,6 +808,11 @@ "extra": { "branch-alias": { "dev-master": "3.3-dev" + }, + "laravel": { + "providers": [ + "Fideloper\\Proxy\\TrustedProxyServiceProvider" + ] } }, "autoload": { @@ -897,142 +836,20 @@ "proxy", "trusted proxy" ], - "time": "2017-05-31T12:50:41+00:00" - }, - { - "name": "friendsofphp/php-cs-fixer", - "version": "v2.4.0", - "source": { - "type": "git", - "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "63661f3add3609e90e4ab8115113e189ae547bb4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/63661f3add3609e90e4ab8115113e189ae547bb4", - "reference": "63661f3add3609e90e4ab8115113e189ae547bb4", - "shasum": "" - }, - "require": { - "doctrine/annotations": "^1.2", - "ext-json": "*", - "ext-tokenizer": "*", - "gecko-packages/gecko-php-unit": "^2.0", - "php": "^5.6 || >=7.0 <7.2", - "sebastian/diff": "^1.4", - "symfony/console": "^3.0", - "symfony/event-dispatcher": "^3.0", - "symfony/filesystem": "^3.0", - "symfony/finder": "^3.0", - "symfony/options-resolver": "^3.0", - "symfony/polyfill-php70": "^1.0", - "symfony/polyfill-php72": "^1.4", - "symfony/process": "^3.0", - "symfony/stopwatch": "^3.0" - }, - "conflict": { - "hhvm": "*" - }, - "require-dev": { - "johnkary/phpunit-speedtrap": "^1.1", - "justinrainbow/json-schema": "^5.0", - "phpunit/phpunit": "^4.8.35 || ^5.4.3", - "satooshi/php-coveralls": "^1.0", - "symfony/phpunit-bridge": "^3.2.2" - }, - "suggest": { - "ext-mbstring": "For handling non-UTF8 characters in cache signature.", - "symfony/polyfill-mbstring": "When enabling `ext-mbstring` is not possible." - }, - "bin": [ - "php-cs-fixer" - ], - "type": "application", - "extra": { - "branch-alias": { - "dev-master": "2.4-dev" - } - }, - "autoload": { - "psr-4": { - "PhpCsFixer\\": "src/" - }, - "classmap": [ - "tests/Test/Assert/AssertTokensTrait.php", - "tests/Test/AbstractFixerTestCase.php", - "tests/Test/AbstractIntegrationTestCase.php", - "tests/Test/IntegrationCase.php", - "tests/Test/IntegrationCaseFactory.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Dariusz Rumiński", - "email": "dariusz.ruminski@gmail.com" - }, - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - } - ], - "description": "A tool to automatically fix PHP code style", - "time": "2017-07-18T15:35:40+00:00" - }, - { - "name": "gecko-packages/gecko-php-unit", - "version": "v2.1", - "source": { - "type": "git", - "url": "https://github.com/GeckoPackages/GeckoPHPUnit.git", - "reference": "5b9e9622c7efd3b22655270b80c03f9e52878a6e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/GeckoPackages/GeckoPHPUnit/zipball/5b9e9622c7efd3b22655270b80c03f9e52878a6e", - "reference": "5b9e9622c7efd3b22655270b80c03f9e52878a6e", - "shasum": "" - }, - "require": { - "php": "^5.3.6 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.4.3" - }, - "type": "library", - "autoload": { - "psr-4": { - "GeckoPackages\\PHPUnit\\": "src\\PHPUnit" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Additional PHPUnit tests.", - "homepage": "https://github.com/GeckoPackages", - "keywords": [ - "extension", - "filesystem", - "phpunit" - ], - "time": "2017-06-20T11:22:48+00:00" + "time": "2017-06-15T17:19:42+00:00" }, { "name": "guzzlehttp/guzzle", - "version": "6.2.3", + "version": "6.3.0", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "8d6c6cc55186db87b7dc5009827429ba4e9dc006" + "reference": "f4db5a78a5ea468d4831de7f0bf9d9415e348699" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/8d6c6cc55186db87b7dc5009827429ba4e9dc006", - "reference": "8d6c6cc55186db87b7dc5009827429ba4e9dc006", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/f4db5a78a5ea468d4831de7f0bf9d9415e348699", + "reference": "f4db5a78a5ea468d4831de7f0bf9d9415e348699", "shasum": "" }, "require": { @@ -1042,9 +859,12 @@ }, "require-dev": { "ext-curl": "*", - "phpunit/phpunit": "^4.0", + "phpunit/phpunit": "^4.0 || ^5.0", "psr/log": "^1.0" }, + "suggest": { + "psr/log": "Required for using the Log middleware" + }, "type": "library", "extra": { "branch-alias": { @@ -1081,7 +901,7 @@ "rest", "web service" ], - "time": "2017-02-28T22:50:30+00:00" + "time": "2017-06-22T18:50:49+00:00" }, { "name": "guzzlehttp/promises", @@ -1344,16 +1164,16 @@ }, { "name": "laracasts/utilities", - "version": "2.1", + "version": "3.0", "source": { "type": "git", "url": "https://github.com/laracasts/PHP-Vars-To-Js-Transformer.git", - "reference": "4402a0ed774f8eb36ea7ba169341d9d5b6049378" + "reference": "298fb3c6f29901a4550c4f98b57c05f368341d04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laracasts/PHP-Vars-To-Js-Transformer/zipball/4402a0ed774f8eb36ea7ba169341d9d5b6049378", - "reference": "4402a0ed774f8eb36ea7ba169341d9d5b6049378", + "url": "https://api.github.com/repos/laracasts/PHP-Vars-To-Js-Transformer/zipball/298fb3c6f29901a4550c4f98b57c05f368341d04", + "reference": "298fb3c6f29901a4550c4f98b57c05f368341d04", "shasum": "" }, "require": { @@ -1364,7 +1184,20 @@ "phpspec/phpspec": "~2.0" }, "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laracasts\\Utilities\\JavaScript\\JavaScriptServiceProvider" + ], + "aliases": { + "JavaScript": "Laracasts\\Utilities\\JavaScript\\JavaScriptFacade" + } + } + }, "autoload": { + "files": [ + "src/helpers.php" + ], "psr-4": { "Laracasts\\Utilities\\JavaScript\\": "src/" } @@ -1384,7 +1217,7 @@ "javascript", "laravel" ], - "time": "2015-10-01T05:16:28+00:00" + "time": "2017-09-01T17:25:57+00:00" }, { "name": "laravel/framework", @@ -1580,16 +1413,16 @@ }, { "name": "league/flysystem", - "version": "1.0.40", + "version": "1.0.41", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "3828f0b24e2c1918bb362d57a53205d6dc8fde61" + "reference": "f400aa98912c561ba625ea4065031b7a41e5a155" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/3828f0b24e2c1918bb362d57a53205d6dc8fde61", - "reference": "3828f0b24e2c1918bb362d57a53205d6dc8fde61", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/f400aa98912c561ba625ea4065031b7a41e5a155", + "reference": "f400aa98912c561ba625ea4065031b7a41e5a155", "shasum": "" }, "require": { @@ -1610,13 +1443,13 @@ "league/flysystem-aws-s3-v3": "Allows you to use S3 storage with AWS SDK v3", "league/flysystem-azure": "Allows you to use Windows Azure Blob storage", "league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching", - "league/flysystem-copy": "Allows you to use Copy.com storage", "league/flysystem-eventable-filesystem": "Allows you to use EventableFilesystem", "league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files", "league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib", "league/flysystem-webdav": "Allows you to use WebDAV storage", "league/flysystem-ziparchive": "Allows you to use ZipArchive adapter", - "spatie/flysystem-dropbox": "Allows you to use Dropbox storage" + "spatie/flysystem-dropbox": "Allows you to use Dropbox storage", + "srmklive/flysystem-dropbox-v2": "Allows you to use Dropbox storage for PHP 5 applications" }, "type": "library", "extra": { @@ -1659,20 +1492,20 @@ "sftp", "storage" ], - "time": "2017-04-28T10:15:08+00:00" + "time": "2017-08-06T17:41:04+00:00" }, { "name": "league/fractal", - "version": "0.16.0", + "version": "0.17.0", "source": { "type": "git", "url": "https://github.com/thephpleague/fractal.git", - "reference": "d0445305e308d9207430680acfd580557b679ddc" + "reference": "a0b350824f22fc2fdde2500ce9d6851a3f275b0e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/fractal/zipball/d0445305e308d9207430680acfd580557b679ddc", - "reference": "d0445305e308d9207430680acfd580557b679ddc", + "url": "https://api.github.com/repos/thephpleague/fractal/zipball/a0b350824f22fc2fdde2500ce9d6851a3f275b0e", + "reference": "a0b350824f22fc2fdde2500ce9d6851a3f275b0e", "shasum": "" }, "require": { @@ -1723,28 +1556,28 @@ "league", "rest" ], - "time": "2017-03-12T01:28:43+00:00" + "time": "2017-06-12T11:04:56+00:00" }, { "name": "lord/laroute", - "version": "v2.4.4", + "version": "2.4.5", "source": { "type": "git", "url": "https://github.com/aaronlord/laroute.git", - "reference": "2adee9daa5491f1ad7b953fc01df36ebc7294c3e" + "reference": "342af22147de90e02d5104447b7c5aea056b5548" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aaronlord/laroute/zipball/2adee9daa5491f1ad7b953fc01df36ebc7294c3e", - "reference": "2adee9daa5491f1ad7b953fc01df36ebc7294c3e", + "url": "https://api.github.com/repos/aaronlord/laroute/zipball/342af22147de90e02d5104447b7c5aea056b5548", + "reference": "342af22147de90e02d5104447b7c5aea056b5548", "shasum": "" }, "require": { - "illuminate/config": "5.0.*|5.1.*|5.2.*|5.3.*|5.4.*", - "illuminate/console": "5.0.*|5.1.*|5.2.*|5.3.*|5.4.*", - "illuminate/filesystem": "5.0.*|5.1.*|5.2.*|5.3.*|5.4.*", - "illuminate/routing": "5.0.*|5.1.*|5.2.*|5.3.*|5.4.*", - "illuminate/support": "5.0.*|5.1.*|5.2.*|5.3.*|5.4.*", + "illuminate/config": "5.0.*|5.1.*|5.2.*|5.3.*|5.4.*|5.5.*", + "illuminate/console": "5.0.*|5.1.*|5.2.*|5.3.*|5.4.*|5.5.*", + "illuminate/filesystem": "5.0.*|5.1.*|5.2.*|5.3.*|5.4.*|5.5.*", + "illuminate/routing": "5.0.*|5.1.*|5.2.*|5.3.*|5.4.*|5.5.*", + "illuminate/support": "5.0.*|5.1.*|5.2.*|5.3.*|5.4.*|5.5.*", "php": ">=5.4.0" }, "require-dev": { @@ -1774,68 +1607,7 @@ "routes", "routing" ], - "time": "2017-02-08T11:05:52+00:00" - }, - { - "name": "maximebf/debugbar", - "version": "1.13.1", - "source": { - "type": "git", - "url": "https://github.com/maximebf/php-debugbar.git", - "reference": "afee79a236348e39a44cb837106b7c5b4897ac2a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/afee79a236348e39a44cb837106b7c5b4897ac2a", - "reference": "afee79a236348e39a44cb837106b7c5b4897ac2a", - "shasum": "" - }, - "require": { - "php": ">=5.3.0", - "psr/log": "^1.0", - "symfony/var-dumper": "^2.6|^3.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.0|^5.0" - }, - "suggest": { - "kriswallsmith/assetic": "The best way to manage assets", - "monolog/monolog": "Log using Monolog", - "predis/predis": "Redis storage" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.13-dev" - } - }, - "autoload": { - "psr-4": { - "DebugBar\\": "src/DebugBar/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Maxime Bouroumeau-Fuseau", - "email": "maxime.bouroumeau@gmail.com", - "homepage": "http://maximebf.com" - }, - { - "name": "Barry vd. Heuvel", - "email": "barryvdh@gmail.com" - } - ], - "description": "Debug bar in the browser for php application", - "homepage": "https://github.com/maximebf/php-debugbar", - "keywords": [ - "debug", - "debugbar" - ], - "time": "2017-01-05T08:46:19+00:00" + "time": "2017-06-29T09:34:21+00:00" }, { "name": "monolog/monolog", @@ -2069,16 +1841,16 @@ }, { "name": "nicolaslopezj/searchable", - "version": "1.9.5", + "version": "1.9.6", "source": { "type": "git", "url": "https://github.com/nicolaslopezj/searchable.git", - "reference": "1351b1b21ae9be9e0f49f375f56488df839723d4" + "reference": "c067f11a993c274c9da0d4a2e70ca07614bb92da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nicolaslopezj/searchable/zipball/1351b1b21ae9be9e0f49f375f56488df839723d4", - "reference": "1351b1b21ae9be9e0f49f375f56488df839723d4", + "url": "https://api.github.com/repos/nicolaslopezj/searchable/zipball/c067f11a993c274c9da0d4a2e70ca07614bb92da", + "reference": "c067f11a993c274c9da0d4a2e70ca07614bb92da", "shasum": "" }, "require": { @@ -2111,20 +1883,20 @@ "search", "searchable" ], - "time": "2016-12-16T21:23:34+00:00" + "time": "2017-08-09T04:37:57+00:00" }, { "name": "nikic/php-parser", - "version": "v3.1.0", + "version": "v3.1.1", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "4d4896e553f2094e657fe493506dc37c509d4e2b" + "reference": "a1e8e1a30e1352f118feff1a8481066ddc2f234a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4d4896e553f2094e657fe493506dc37c509d4e2b", - "reference": "4d4896e553f2094e657fe493506dc37c509d4e2b", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/a1e8e1a30e1352f118feff1a8481066ddc2f234a", + "reference": "a1e8e1a30e1352f118feff1a8481066ddc2f234a", "shasum": "" }, "require": { @@ -2162,7 +1934,7 @@ "parser", "php" ], - "time": "2017-07-28T14:45:09+00:00" + "time": "2017-09-02T17:10:46+00:00" }, { "name": "paragonie/random_compat", @@ -2545,16 +2317,16 @@ }, { "name": "ramsey/uuid", - "version": "3.6.1", + "version": "3.7.0", "source": { "type": "git", "url": "https://github.com/ramsey/uuid.git", - "reference": "4ae32dd9ab8860a4bbd750ad269cba7f06f7934e" + "reference": "0ef23d1b10cf1bc576e9d865a7e9c47982c5715e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ramsey/uuid/zipball/4ae32dd9ab8860a4bbd750ad269cba7f06f7934e", - "reference": "4ae32dd9ab8860a4bbd750ad269cba7f06f7934e", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/0ef23d1b10cf1bc576e9d865a7e9c47982c5715e", + "reference": "0ef23d1b10cf1bc576e9d865a7e9c47982c5715e", "shasum": "" }, "require": { @@ -2623,7 +2395,7 @@ "identifier", "uuid" ], - "time": "2017-03-26T20:37:53+00:00" + "time": "2017-08-04T13:39:04+00:00" }, { "name": "s1lentium/iptools", @@ -2675,58 +2447,6 @@ ], "time": "2016-08-21T15:57:09+00:00" }, - { - "name": "sebastian/diff", - "version": "1.4.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7f066a26a962dbe58ddea9f72a4e82874a3975a4", - "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4", - "shasum": "" - }, - "require": { - "php": "^5.3.3 || ^7.0" - }, - "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Kore Nordmann", - "email": "mail@kore-nordmann.de" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - } - ], - "description": "Diff implementation", - "homepage": "https://github.com/sebastianbergmann/diff", - "keywords": [ - "diff" - ], - "time": "2017-05-22T07:24:03+00:00" - }, { "name": "sofa/eloquence", "version": "5.4.1", @@ -2831,20 +2551,20 @@ }, { "name": "spatie/fractalistic", - "version": "2.3.2", + "version": "2.5.0", "source": { "type": "git", "url": "https://github.com/spatie/fractalistic.git", - "reference": "012c4182203ba9127bb0a31cec3c211ce68227d9" + "reference": "fdd536f8c1b172edfc3a742f6074680b80db2606" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/fractalistic/zipball/012c4182203ba9127bb0a31cec3c211ce68227d9", - "reference": "012c4182203ba9127bb0a31cec3c211ce68227d9", + "url": "https://api.github.com/repos/spatie/fractalistic/zipball/fdd536f8c1b172edfc3a742f6074680b80db2606", + "reference": "fdd536f8c1b172edfc3a742f6074680b80db2606", "shasum": "" }, "require": { - "league/fractal": "^0.16.0", + "league/fractal": "^0.17.0", "php": "^5.6|^7.0" }, "require-dev": { @@ -2878,33 +2598,43 @@ "spatie", "transform" ], - "time": "2017-07-24T08:06:12+00:00" + "time": "2017-08-18T23:58:17+00:00" }, { "name": "spatie/laravel-fractal", - "version": "4.0.1", + "version": "4.5.0", "source": { "type": "git", "url": "https://github.com/spatie/laravel-fractal.git", - "reference": "3b95780f5f3ca79e29d445a5df87eac9f7c7c053" + "reference": "445364122d4e6d6da6f599939962e689668c2b5f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-fractal/zipball/3b95780f5f3ca79e29d445a5df87eac9f7c7c053", - "reference": "3b95780f5f3ca79e29d445a5df87eac9f7c7c053", + "url": "https://api.github.com/repos/spatie/laravel-fractal/zipball/445364122d4e6d6da6f599939962e689668c2b5f", + "reference": "445364122d4e6d6da6f599939962e689668c2b5f", "shasum": "" }, "require": { "illuminate/contracts": "~5.1.0|~5.2.0|~5.3.0|~5.4.0", "illuminate/support": "~5.1.0|~5.2.0|~5.3.0|~5.4.0", "php": "^5.6|^7.0", - "spatie/fractalistic": "^2.0" + "spatie/fractalistic": "^2.5" }, "require-dev": { "orchestra/testbench": "~3.2.0|3.3.0|~3.4.0", "phpunit/phpunit": "^5.7.5" }, "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\Fractal\\FractalServiceProvider" + ], + "aliases": { + "Fractal": "Spatie\\Fractal\\FractalFacade" + } + } + }, "autoload": { "psr-4": { "Spatie\\Fractal\\": "src" @@ -2936,7 +2666,7 @@ "spatie", "transform" ], - "time": "2017-05-05T19:01:43+00:00" + "time": "2017-08-22T17:52:05+00:00" }, { "name": "swiftmailer/swiftmailer", @@ -3233,55 +2963,6 @@ "homepage": "https://symfony.com", "time": "2017-06-09T14:53:08+00:00" }, - { - "name": "symfony/filesystem", - "version": "v3.3.6", - "source": { - "type": "git", - "url": "https://github.com/symfony/filesystem.git", - "reference": "427987eb4eed764c3b6e38d52a0f87989e010676" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/427987eb4eed764c3b6e38d52a0f87989e010676", - "reference": "427987eb4eed764c3b6e38d52a0f87989e010676", - "shasum": "" - }, - "require": { - "php": ">=5.5.9" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.3-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Filesystem\\": "" - }, - "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 Filesystem Component", - "homepage": "https://symfony.com", - "time": "2017-07-11T07:17:58+00:00" - }, { "name": "symfony/finder", "version": "v3.3.6", @@ -3470,60 +3151,6 @@ "homepage": "https://symfony.com", "time": "2017-08-01T10:25:59+00:00" }, - { - "name": "symfony/options-resolver", - "version": "v3.3.6", - "source": { - "type": "git", - "url": "https://github.com/symfony/options-resolver.git", - "reference": "ff48982d295bcac1fd861f934f041ebc73ae40f0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/ff48982d295bcac1fd861f934f041ebc73ae40f0", - "reference": "ff48982d295bcac1fd861f934f041ebc73ae40f0", - "shasum": "" - }, - "require": { - "php": ">=5.5.9" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.3-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\OptionsResolver\\": "" - }, - "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 OptionsResolver Component", - "homepage": "https://symfony.com", - "keywords": [ - "config", - "configuration", - "options" - ], - "time": "2017-04-12T14:14:56+00:00" - }, { "name": "symfony/polyfill-mbstring", "version": "v1.5.0", @@ -3585,16 +3212,16 @@ }, { "name": "symfony/polyfill-php56", - "version": "v1.4.0", + "version": "v1.5.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php56.git", - "reference": "bc0b7d6cb36b10cfabb170a3e359944a95174929" + "reference": "e85ebdef569b84e8709864e1a290c40f156b30ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php56/zipball/bc0b7d6cb36b10cfabb170a3e359944a95174929", - "reference": "bc0b7d6cb36b10cfabb170a3e359944a95174929", + "url": "https://api.github.com/repos/symfony/polyfill-php56/zipball/e85ebdef569b84e8709864e1a290c40f156b30ca", + "reference": "e85ebdef569b84e8709864e1a290c40f156b30ca", "shasum": "" }, "require": { @@ -3604,7 +3231,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "1.5-dev" } }, "autoload": { @@ -3637,79 +3264,20 @@ "portable", "shim" ], - "time": "2017-06-09T08:25:21+00:00" - }, - { - "name": "symfony/polyfill-php70", - "version": "v1.5.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php70.git", - "reference": "b6482e68974486984f59449ecea1fbbb22ff840f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/b6482e68974486984f59449ecea1fbbb22ff840f", - "reference": "b6482e68974486984f59449ecea1fbbb22ff840f", - "shasum": "" - }, - "require": { - "paragonie/random_compat": "~1.0|~2.0", - "php": ">=5.3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.5-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php70\\": "" - }, - "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 backporting some PHP 7.0+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], "time": "2017-06-14T15:44:48+00:00" }, { - "name": "symfony/polyfill-php72", + "name": "symfony/polyfill-util", "version": "v1.5.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "8abc9097f5001d310f0edba727469c988acc6ea7" + "url": "https://github.com/symfony/polyfill-util.git", + "reference": "67925d1cf0b84bd234a83bebf26d4eb281744c6d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/8abc9097f5001d310f0edba727469c988acc6ea7", - "reference": "8abc9097f5001d310f0edba727469c988acc6ea7", + "url": "https://api.github.com/repos/symfony/polyfill-util/zipball/67925d1cf0b84bd234a83bebf26d4eb281744c6d", + "reference": "67925d1cf0b84bd234a83bebf26d4eb281744c6d", "shasum": "" }, "require": { @@ -3721,61 +3289,6 @@ "dev-master": "1.5-dev" } }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php72\\": "" - }, - "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 backporting some PHP 7.2+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "time": "2017-07-11T13:25:55+00:00" - }, - { - "name": "symfony/polyfill-util", - "version": "v1.4.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-util.git", - "reference": "ebccbde4aad410f6438d86d7d261c6b4d2b9a51d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-util/zipball/ebccbde4aad410f6438d86d7d261c6b4d2b9a51d", - "reference": "ebccbde4aad410f6438d86d7d261c6b4d2b9a51d", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Polyfill\\Util\\": "" @@ -3803,7 +3316,7 @@ "polyfill", "shim" ], - "time": "2017-06-09T08:25:21+00:00" + "time": "2017-07-05T15:09:33+00:00" }, { "name": "symfony/process", @@ -3932,55 +3445,6 @@ ], "time": "2017-07-21T17:43:13+00:00" }, - { - "name": "symfony/stopwatch", - "version": "v3.3.6", - "source": { - "type": "git", - "url": "https://github.com/symfony/stopwatch.git", - "reference": "602a15299dc01556013b07167d4f5d3a60e90d15" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/602a15299dc01556013b07167d4f5d3a60e90d15", - "reference": "602a15299dc01556013b07167d4f5d3a60e90d15", - "shasum": "" - }, - "require": { - "php": ">=5.5.9" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.3-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Stopwatch\\": "" - }, - "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 Stopwatch Component", - "homepage": "https://symfony.com", - "time": "2017-04-12T14:14:56+00:00" - }, { "name": "symfony/translation", "version": "v3.3.6", @@ -4213,16 +3677,16 @@ }, { "name": "watson/validating", - "version": "3.0.1", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/dwightwatson/validating.git", - "reference": "3cef5b4cd0af2dc26d2c7ca668bd12f4d4ab421b" + "reference": "c6e9194b034a24d3b6e447bbc7a94d88fad9a1fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dwightwatson/validating/zipball/3cef5b4cd0af2dc26d2c7ca668bd12f4d4ab421b", - "reference": "3cef5b4cd0af2dc26d2c7ca668bd12f4d4ab421b", + "url": "https://api.github.com/repos/dwightwatson/validating/zipball/c6e9194b034a24d3b6e447bbc7a94d88fad9a1fd", + "reference": "c6e9194b034a24d3b6e447bbc7a94d88fad9a1fd", "shasum": "" }, "require": { @@ -4259,7 +3723,7 @@ "laravel", "validation" ], - "time": "2016-10-31T21:53:17+00:00" + "time": "2017-08-25T02:12:38+00:00" }, { "name": "webmozart/assert", @@ -4313,16 +3777,16 @@ }, { "name": "webpatser/laravel-uuid", - "version": "2.0.1", + "version": "2.1.1", "source": { "type": "git", "url": "https://github.com/webpatser/laravel-uuid.git", - "reference": "6ed2705775e3edf066b90e1292f76f157ec00507" + "reference": "317b835cf492240187e00673582e056ec4dc076a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webpatser/laravel-uuid/zipball/6ed2705775e3edf066b90e1292f76f157ec00507", - "reference": "6ed2705775e3edf066b90e1292f76f157ec00507", + "url": "https://api.github.com/repos/webpatser/laravel-uuid/zipball/317b835cf492240187e00673582e056ec4dc076a", + "reference": "317b835cf492240187e00673582e056ec4dc076a", "shasum": "" }, "require": { @@ -4336,6 +3800,13 @@ "paragonie/random_compat": "A random_bytes Php 5.x polyfill." }, "type": "library", + "extra": { + "laravel": { + "aliases": { + "Uuid": "Webpatser\\Uuid\\Uuid" + } + } + }, "autoload": { "psr-0": { "Webpatser\\Uuid": "src/" @@ -4356,10 +3827,59 @@ "keywords": [ "UUID RFC4122" ], - "time": "2016-05-09T09:22:18+00:00" + "time": "2017-08-30T20:45:29+00:00" } ], "packages-dev": [ + { + "name": "barryvdh/laravel-debugbar", + "version": "v2.4.3", + "source": { + "type": "git", + "url": "https://github.com/barryvdh/laravel-debugbar.git", + "reference": "d7c88f08131f6404cb714f3f6cf0642f6afa3903" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/d7c88f08131f6404cb714f3f6cf0642f6afa3903", + "reference": "d7c88f08131f6404cb714f3f6cf0642f6afa3903", + "shasum": "" + }, + "require": { + "illuminate/support": "5.1.*|5.2.*|5.3.*|5.4.*|5.5.*", + "maximebf/debugbar": "~1.13.0", + "php": ">=5.5.9", + "symfony/finder": "~2.7|~3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Barryvdh\\Debugbar\\": "src/" + }, + "files": [ + "src/helpers.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" + } + ], + "description": "PHP Debugbar integration for Laravel", + "keywords": [ + "debug", + "debugbar", + "laravel", + "profiler", + "webprofiler" + ], + "time": "2017-07-21T11:56:48+00:00" + }, { "name": "barryvdh/laravel-ide-helper", "version": "v2.4.1", @@ -4484,32 +4004,32 @@ }, { "name": "doctrine/instantiator", - "version": "1.1.0", + "version": "1.0.5", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda" + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", - "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", "shasum": "" }, "require": { - "php": "^7.1" + "php": ">=5.3,<8.0-DEV" }, "require-dev": { "athletic/athletic": "~0.1.8", "ext-pdo": "*", "ext-phar": "*", - "phpunit/phpunit": "^6.2.3", - "squizlabs/php_codesniffer": "^3.0.2" + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "~2.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2.x-dev" + "dev-master": "1.0.x-dev" } }, "autoload": { @@ -4534,33 +4054,118 @@ "constructor", "instantiate" ], - "time": "2017-07-22T11:58:36+00:00" + "time": "2015-06-14T21:17:01+00:00" }, { - "name": "fzaninotto/faker", - "version": "v1.6.0", + "name": "friendsofphp/php-cs-fixer", + "version": "v2.4.1", "source": { "type": "git", - "url": "https://github.com/fzaninotto/Faker.git", - "reference": "44f9a286a04b80c76a4e5fb7aad8bb539b920123" + "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", + "reference": "b4983586c8e7b1f99ec05dd1e75c8b673315da70" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fzaninotto/Faker/zipball/44f9a286a04b80c76a4e5fb7aad8bb539b920123", - "reference": "44f9a286a04b80c76a4e5fb7aad8bb539b920123", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/b4983586c8e7b1f99ec05dd1e75c8b673315da70", + "reference": "b4983586c8e7b1f99ec05dd1e75c8b673315da70", "shasum": "" }, "require": { - "php": "^5.3.3|^7.0" + "doctrine/annotations": "^1.2", + "ext-json": "*", + "ext-tokenizer": "*", + "gecko-packages/gecko-php-unit": "^2.0", + "php": "^5.6 || >=7.0 <7.2", + "sebastian/diff": "^1.4", + "symfony/console": "^3.0", + "symfony/event-dispatcher": "^3.0", + "symfony/filesystem": "^3.0", + "symfony/finder": "^3.0", + "symfony/options-resolver": "^3.0", + "symfony/polyfill-php70": "^1.0", + "symfony/polyfill-php72": "^1.4", + "symfony/process": "^3.0", + "symfony/stopwatch": "^3.0" + }, + "conflict": { + "hhvm": "*" + }, + "require-dev": { + "johnkary/phpunit-speedtrap": "^1.1", + "justinrainbow/json-schema": "^5.0", + "phpunit/phpunit": "^4.8.35 || ^5.4.3", + "satooshi/php-coveralls": "^1.0", + "symfony/phpunit-bridge": "^3.2.2" + }, + "suggest": { + "ext-mbstring": "For handling non-UTF8 characters in cache signature.", + "symfony/polyfill-mbstring": "When enabling `ext-mbstring` is not possible." + }, + "bin": [ + "php-cs-fixer" + ], + "type": "application", + "extra": { + "branch-alias": { + "dev-master": "2.4-dev" + } + }, + "autoload": { + "psr-4": { + "PhpCsFixer\\": "src/" + }, + "classmap": [ + "tests/Test/Assert/AssertTokensTrait.php", + "tests/Test/AbstractFixerTestCase.php", + "tests/Test/AbstractIntegrationTestCase.php", + "tests/Test/IntegrationCase.php", + "tests/Test/IntegrationCaseFactory.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dariusz Rumiński", + "email": "dariusz.ruminski@gmail.com" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "A tool to automatically fix PHP code style", + "time": "2017-08-22T14:11:01+00:00" + }, + { + "name": "fzaninotto/faker", + "version": "v1.7.1", + "source": { + "type": "git", + "url": "https://github.com/fzaninotto/Faker.git", + "reference": "d3ed4cc37051c1ca52d22d76b437d14809fc7e0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fzaninotto/Faker/zipball/d3ed4cc37051c1ca52d22d76b437d14809fc7e0d", + "reference": "d3ed4cc37051c1ca52d22d76b437d14809fc7e0d", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" }, "require-dev": { "ext-intl": "*", - "phpunit/phpunit": "~4.0", - "squizlabs/php_codesniffer": "~1.5" + "phpunit/phpunit": "^4.0 || ^5.0", + "squizlabs/php_codesniffer": "^1.5" }, "type": "library", "extra": { - "branch-alias": [] + "branch-alias": { + "dev-master": "1.8-dev" + } }, "autoload": { "psr-4": { @@ -4582,7 +4187,51 @@ "faker", "fixtures" ], - "time": "2016-04-29T12:21:54+00:00" + "time": "2017-08-15T16:48:10+00:00" + }, + { + "name": "gecko-packages/gecko-php-unit", + "version": "v2.2", + "source": { + "type": "git", + "url": "https://github.com/GeckoPackages/GeckoPHPUnit.git", + "reference": "ab525fac9a9ffea219687f261b02008b18ebf2d1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GeckoPackages/GeckoPHPUnit/zipball/ab525fac9a9ffea219687f261b02008b18ebf2d1", + "reference": "ab525fac9a9ffea219687f261b02008b18ebf2d1", + "shasum": "" + }, + "require": { + "php": "^5.3.6 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.4.3" + }, + "suggest": { + "ext-dom": "When testing with xml.", + "ext-libxml": "When testing with xml.", + "phpunit/phpunit": "This is an extension for it so make sure you have it some way." + }, + "type": "library", + "autoload": { + "psr-4": { + "GeckoPackages\\PHPUnit\\": "src/PHPUnit" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Additional PHPUnit asserts and constraints.", + "homepage": "https://github.com/GeckoPackages", + "keywords": [ + "extension", + "filesystem", + "phpunit" + ], + "time": "2017-08-23T07:39:54+00:00" }, { "name": "hamcrest/hamcrest-php", @@ -4630,27 +4279,42 @@ "time": "2015-05-11T14:41:42+00:00" }, { - "name": "ircmaxell/password-compat", - "version": "v1.0.4", + "name": "maximebf/debugbar", + "version": "1.13.1", "source": { "type": "git", - "url": "https://github.com/ircmaxell/password_compat.git", - "reference": "5c5cde8822a69545767f7c7f3058cb15ff84614c" + "url": "https://github.com/maximebf/php-debugbar.git", + "reference": "afee79a236348e39a44cb837106b7c5b4897ac2a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/ircmaxell/password_compat/zipball/5c5cde8822a69545767f7c7f3058cb15ff84614c", - "reference": "5c5cde8822a69545767f7c7f3058cb15ff84614c", + "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/afee79a236348e39a44cb837106b7c5b4897ac2a", + "reference": "afee79a236348e39a44cb837106b7c5b4897ac2a", "shasum": "" }, + "require": { + "php": ">=5.3.0", + "psr/log": "^1.0", + "symfony/var-dumper": "^2.6|^3.0" + }, "require-dev": { - "phpunit/phpunit": "4.*" + "phpunit/phpunit": "^4.0|^5.0" + }, + "suggest": { + "kriswallsmith/assetic": "The best way to manage assets", + "monolog/monolog": "Log using Monolog", + "predis/predis": "Redis storage" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.13-dev" + } + }, "autoload": { - "files": [ - "lib/password.php" - ] + "psr-4": { + "DebugBar\\": "src/DebugBar/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -4658,18 +4322,22 @@ ], "authors": [ { - "name": "Anthony Ferrara", - "email": "ircmaxell@php.net", - "homepage": "http://blog.ircmaxell.com" + "name": "Maxime Bouroumeau-Fuseau", + "email": "maxime.bouroumeau@gmail.com", + "homepage": "http://maximebf.com" + }, + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" } ], - "description": "A compatibility library for the proposed simplified password hashing algorithm: https://wiki.php.net/rfc/password_hash", - "homepage": "https://github.com/ircmaxell/password_compat", + "description": "Debug bar in the browser for php application", + "homepage": "https://github.com/maximebf/php-debugbar", "keywords": [ - "hashing", - "password" + "debug", + "debugbar" ], - "time": "2014-11-20T16:49:30+00:00" + "time": "2017-01-05T08:46:19+00:00" }, { "name": "mockery/mockery", @@ -5003,22 +4671,22 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "3.2.1", + "version": "3.2.3", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "183824db76118b9dddffc7e522b91fa175f75119" + "reference": "86e24012a3139b42a7b71155cfaa325389f00f1f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/183824db76118b9dddffc7e522b91fa175f75119", - "reference": "183824db76118b9dddffc7e522b91fa175f75119", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/86e24012a3139b42a7b71155cfaa325389f00f1f", + "reference": "86e24012a3139b42a7b71155cfaa325389f00f1f", "shasum": "" }, "require": { - "php": ">=5.5", + "php": "^7.0", "phpdocumentor/reflection-common": "^1.0@dev", - "phpdocumentor/type-resolver": "^0.3.0", + "phpdocumentor/type-resolver": "^0.4.0", "webmozart/assert": "^1.0" }, "require-dev": { @@ -5044,20 +4712,20 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2017-08-04T20:55:59+00:00" + "time": "2017-08-29T19:37:41+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "0.3.0", + "version": "0.4.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "fb3933512008d8162b3cdf9e18dba9309b7c3773" + "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/fb3933512008d8162b3cdf9e18dba9309b7c3773", - "reference": "fb3933512008d8162b3cdf9e18dba9309b7c3773", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7", + "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7", "shasum": "" }, "require": { @@ -5091,7 +4759,7 @@ "email": "me@mikevanriel.com" } ], - "time": "2017-06-03T08:32:36+00:00" + "time": "2017-07-14T14:27:02+00:00" }, { "name": "phpspec/prophecy", @@ -5358,16 +5026,16 @@ }, { "name": "phpunit/php-token-stream", - "version": "2.0.0", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "ecb0b2cdaa0add708fe6f329ef65ae0c5225130b" + "reference": "9a02332089ac48e704c70f6cefed30c224e3c0b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/ecb0b2cdaa0add708fe6f329ef65ae0c5225130b", - "reference": "ecb0b2cdaa0add708fe6f329ef65ae0c5225130b", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/9a02332089ac48e704c70f6cefed30c224e3c0b0", + "reference": "9a02332089ac48e704c70f6cefed30c224e3c0b0", "shasum": "" }, "require": { @@ -5403,7 +5071,7 @@ "keywords": [ "tokenizer" ], - "time": "2017-08-03T14:17:41+00:00" + "time": "2017-08-20T05:47:52+00:00" }, { "name": "phpunit/phpunit", @@ -5655,6 +5323,58 @@ ], "time": "2017-01-29T09:50:25+00:00" }, + { + "name": "sebastian/diff", + "version": "1.4.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7f066a26a962dbe58ddea9f72a4e82874a3975a4", + "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff" + ], + "time": "2017-05-22T07:24:03+00:00" + }, { "name": "sebastian/environment", "version": "2.0.0", @@ -6064,20 +5784,124 @@ "time": "2017-06-02T09:51:43+00:00" }, { - "name": "symfony/polyfill-php54", - "version": "v1.5.0", + "name": "symfony/filesystem", + "version": "v3.3.6", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php54.git", - "reference": "b7763422a5334c914ef0298ed21b253d25913a6e" + "url": "https://github.com/symfony/filesystem.git", + "reference": "427987eb4eed764c3b6e38d52a0f87989e010676" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php54/zipball/b7763422a5334c914ef0298ed21b253d25913a6e", - "reference": "b7763422a5334c914ef0298ed21b253d25913a6e", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/427987eb4eed764c3b6e38d52a0f87989e010676", + "reference": "427987eb4eed764c3b6e38d52a0f87989e010676", "shasum": "" }, "require": { + "php": ">=5.5.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "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 Filesystem Component", + "homepage": "https://symfony.com", + "time": "2017-07-11T07:17:58+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v3.3.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "ff48982d295bcac1fd861f934f041ebc73ae40f0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/ff48982d295bcac1fd861f934f041ebc73ae40f0", + "reference": "ff48982d295bcac1fd861f934f041ebc73ae40f0", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "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 OptionsResolver Component", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "time": "2017-04-12T14:14:56+00:00" + }, + { + "name": "symfony/polyfill-php70", + "version": "v1.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php70.git", + "reference": "b6482e68974486984f59449ecea1fbbb22ff840f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/b6482e68974486984f59449ecea1fbbb22ff840f", + "reference": "b6482e68974486984f59449ecea1fbbb22ff840f", + "shasum": "" + }, + "require": { + "paragonie/random_compat": "~1.0|~2.0", "php": ">=5.3.3" }, "type": "library", @@ -6088,7 +5912,7 @@ }, "autoload": { "psr-4": { - "Symfony\\Polyfill\\Php54\\": "" + "Symfony\\Polyfill\\Php70\\": "" }, "files": [ "bootstrap.php" @@ -6111,7 +5935,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 5.4+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 7.0+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", @@ -6122,21 +5946,20 @@ "time": "2017-06-14T15:44:48+00:00" }, { - "name": "symfony/polyfill-php55", + "name": "symfony/polyfill-php72", "version": "v1.5.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php55.git", - "reference": "29b1381d66f16e0581aab0b9f678ccf073288f68" + "url": "https://github.com/symfony/polyfill-php72.git", + "reference": "8abc9097f5001d310f0edba727469c988acc6ea7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php55/zipball/29b1381d66f16e0581aab0b9f678ccf073288f68", - "reference": "29b1381d66f16e0581aab0b9f678ccf073288f68", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/8abc9097f5001d310f0edba727469c988acc6ea7", + "reference": "8abc9097f5001d310f0edba727469c988acc6ea7", "shasum": "" }, "require": { - "ircmaxell/password-compat": "~1.0", "php": ">=5.3.3" }, "type": "library", @@ -6147,7 +5970,7 @@ }, "autoload": { "psr-4": { - "Symfony\\Polyfill\\Php55\\": "" + "Symfony\\Polyfill\\Php72\\": "" }, "files": [ "bootstrap.php" @@ -6167,7 +5990,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 5.5+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", @@ -6175,55 +5998,56 @@ "portable", "shim" ], - "time": "2017-06-14T15:44:48+00:00" + "time": "2017-07-11T13:25:55+00:00" }, { - "name": "symfony/polyfill-xml", - "version": "v1.5.0", + "name": "symfony/stopwatch", + "version": "v3.3.6", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-xml.git", - "reference": "7d536462e554da7b05600a926303bf9b99153275" + "url": "https://github.com/symfony/stopwatch.git", + "reference": "602a15299dc01556013b07167d4f5d3a60e90d15" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-xml/zipball/7d536462e554da7b05600a926303bf9b99153275", - "reference": "7d536462e554da7b05600a926303bf9b99153275", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/602a15299dc01556013b07167d4f5d3a60e90d15", + "reference": "602a15299dc01556013b07167d4f5d3a60e90d15", "shasum": "" }, "require": { - "php": ">=5.3.3", - "symfony/polyfill-php72": "~1.4" + "php": ">=5.5.9" }, - "type": "metapackage", + "type": "library", "extra": { "branch-alias": { - "dev-master": "1.5-dev" + "dev-master": "3.3-dev" } }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Stopwatch\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Fabien Potencier", + "email": "fabien@symfony.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for xml's utf8_encode and utf8_decode functions", + "description": "Symfony Stopwatch Component", "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "time": "2017-06-14T15:44:48+00:00" + "time": "2017-04-12T14:14:56+00:00" }, { "name": "symfony/yaml", @@ -6287,10 +6111,13 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": ">=7.0.0", + "php": ">=7.0", "ext-mbstring": "*", "ext-pdo_mysql": "*", "ext-zip": "*" }, - "platform-dev": [] + "platform-dev": [], + "platform-overrides": { + "php": "7.0" + } } From 4532811fcd2cf392996bb8b3eaa140d0eea2db1e Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 2 Sep 2017 21:35:33 -0500 Subject: [PATCH 78/99] Improved middleware, console page now using new setup --- .../RequiredVariableMissingException.php | 4 +- .../Server/UserNotLinkedToServerException.php | 31 +++++ .../Controllers/Server/ConsoleController.php | 99 ++++++++++++++ .../Controllers/Server/ServerController.php | 55 -------- app/Http/Kernel.php | 3 +- ...CheckServer.php => ServerAuthenticate.php} | 124 +++++++++--------- .../Middleware/SubuserAccessAuthenticate.php | 81 ++++++++++++ app/Providers/RouteServiceProvider.php | 2 +- .../Servers/ServerAccessHelperService.php | 43 +++--- app/Traits/Controllers/ServerToJavascript.php | 64 +++++++++ .../pterodactyl/layouts/master.blade.php | 49 +++---- routes/server.php | 4 +- .../Server/ConsoleControllerTest.php | 105 +++++++++++++++ 13 files changed, 499 insertions(+), 165 deletions(-) create mode 100644 app/Exceptions/Service/Server/UserNotLinkedToServerException.php create mode 100644 app/Http/Controllers/Server/ConsoleController.php rename app/Http/Middleware/{CheckServer.php => ServerAuthenticate.php} (56%) create mode 100644 app/Http/Middleware/SubuserAccessAuthenticate.php create mode 100644 app/Traits/Controllers/ServerToJavascript.php create mode 100644 tests/Unit/Http/Controllers/Server/ConsoleControllerTest.php diff --git a/app/Exceptions/Service/Server/RequiredVariableMissingException.php b/app/Exceptions/Service/Server/RequiredVariableMissingException.php index f026ccce..ea22dd08 100644 --- a/app/Exceptions/Service/Server/RequiredVariableMissingException.php +++ b/app/Exceptions/Service/Server/RequiredVariableMissingException.php @@ -24,8 +24,8 @@ namespace Pterodactyl\Exceptions\Service\Server; -use Exception; +use Pterodactyl\Exceptions\PterodactylException; -class RequiredVariableMissingException extends Exception +class RequiredVariableMissingException extends PterodactylException { } diff --git a/app/Exceptions/Service/Server/UserNotLinkedToServerException.php b/app/Exceptions/Service/Server/UserNotLinkedToServerException.php new file mode 100644 index 00000000..346b41fe --- /dev/null +++ b/app/Exceptions/Service/Server/UserNotLinkedToServerException.php @@ -0,0 +1,31 @@ +. + * + * 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\Exceptions\Service\Server; + +use Pterodactyl\Exceptions\PterodactylException; + +class UserNotLinkedToServerException extends PterodactylException +{ +} diff --git a/app/Http/Controllers/Server/ConsoleController.php b/app/Http/Controllers/Server/ConsoleController.php new file mode 100644 index 00000000..8f43b1a2 --- /dev/null +++ b/app/Http/Controllers/Server/ConsoleController.php @@ -0,0 +1,99 @@ +. + * + * 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\Http\Controllers\Server; + +use Illuminate\Contracts\Session\Session; +use Pterodactyl\Http\Controllers\Controller; +use Pterodactyl\Traits\Controllers\ServerToJavascript; +use Illuminate\Contracts\Config\Repository as ConfigRepository; + +class ConsoleController extends Controller +{ + use ServerToJavascript; + + /** + * @var \Illuminate\Contracts\Config\Repository + */ + protected $config; + + /** + * @var \Illuminate\Contracts\Session\Session + */ + protected $session; + + /** + * ConsoleController constructor. + * + * @param \Illuminate\Contracts\Config\Repository $config + * @param \Illuminate\Contracts\Session\Session $session + */ + public function __construct( + ConfigRepository $config, + Session $session + ) { + $this->config = $config; + $this->session = $session; + } + + /** + * Render server index page with the console and power options. + * + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + */ + public function index() + { + $server = $this->session->get('server_data.model'); + + $this->injectJavascript([ + 'meta' => [ + 'saveFile' => route('server.files.save', $server->uuidShort), + 'csrfToken' => csrf_token(), + ], + 'config' => [ + 'console_count' => $this->config->get('pterodactyl.console.count'), + 'console_freq' => $this->config->get('pterodactyl.console.frequency'), + ], + ]); + + return view('server.index', ['server' => $server, 'node' => $server->node]); + } + + /** + * Render a stand-alone console in the browser. + * + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + */ + public function console() + { + $server = $this->session->get('server_data.model'); + + $this->injectJavascript(['config' => [ + 'console_count' => $this->config->get('pterodactyl.console.count'), + 'console_freq' => $this->config->get('pterodactyl.console.frequency'), + ]]); + + return view('server.console', ['server' => $server, 'node' => $server->node]); + } +} diff --git a/app/Http/Controllers/Server/ServerController.php b/app/Http/Controllers/Server/ServerController.php index 86382186..c677c1cd 100644 --- a/app/Http/Controllers/Server/ServerController.php +++ b/app/Http/Controllers/Server/ServerController.php @@ -31,65 +31,10 @@ use Pterodactyl\Models; use Illuminate\Http\Request; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Repositories\ServerRepository; use Pterodactyl\Exceptions\DisplayValidationException; -use Pterodactyl\Repositories\old_Daemon\FileRepository; class ServerController extends Controller { - /** - * Renders server index page for specified server. - * - * @param \Illuminate\Http\Request $request - * @param string $uuid - * @return \Illuminate\View\View - */ - public function getIndex(Request $request, $uuid) - { - $server = Models\Server::byUuid($uuid); - - $server->js([ - 'meta' => [ - 'saveFile' => route('server.files.save', $server->uuidShort), - 'csrfToken' => csrf_token(), - ], - 'config' => [ - 'console_count' => config('pterodactyl.console.count'), - 'console_freq' => config('pterodactyl.console.frequency'), - ], - ]); - - return view('server.index', [ - 'server' => $server, - 'node' => $server->node, - ]); - } - - /** - * Renders server console as an individual item. - * - * @param \Illuminate\Http\Request $request - * @param string $uuid - * @return \Illuminate\View\View - */ - public function getConsole(Request $request, $uuid) - { - \Debugbar::disable(); - $server = Models\Server::byUuid($uuid); - - $server->js([ - 'config' => [ - 'console_count' => config('pterodactyl.console.count'), - 'console_freq' => config('pterodactyl.console.frequency'), - ], - ]); - - return view('server.console', [ - 'server' => $server, - 'node' => $server->node, - ]); - } - /** * Renders file overview page. * diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index a70895a3..1e98b9cf 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -54,7 +54,8 @@ class Kernel extends HttpKernel 'auth' => \Illuminate\Auth\Middleware\Authenticate::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'guest' => \Pterodactyl\Http\Middleware\RedirectIfAuthenticated::class, - 'server' => \Pterodactyl\Http\Middleware\CheckServer::class, + 'server' => \Pterodactyl\Http\Middleware\ServerAuthenticate::class, + 'subuser' => \Pterodactyl\Http\Middleware\SubuserAccessAuthenticate::class, 'admin' => \Pterodactyl\Http\Middleware\AdminAuthenticate::class, 'daemon' => \Pterodactyl\Http\Middleware\DaemonAuthenticate::class, 'csrf' => \Pterodactyl\Http\Middleware\VerifyCsrfToken::class, diff --git a/app/Http/Middleware/CheckServer.php b/app/Http/Middleware/ServerAuthenticate.php similarity index 56% rename from app/Http/Middleware/CheckServer.php rename to app/Http/Middleware/ServerAuthenticate.php index c1da9ea1..83d35073 100644 --- a/app/Http/Middleware/CheckServer.php +++ b/app/Http/Middleware/ServerAuthenticate.php @@ -24,106 +24,102 @@ namespace Pterodactyl\Http\Middleware; -use Auth; use Closure; +use Illuminate\Contracts\Session\Session; use Illuminate\Http\Request; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Models\Server; +use Illuminate\Contracts\Config\Repository as ConfigRepository; use Illuminate\Auth\AuthenticationException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; -class CheckServer +class ServerAuthenticate { /** - * The elquent model for the server. - * + * @var \Illuminate\Contracts\Config\Repository + */ + protected $config; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $repository; + + /** * @var \Pterodactyl\Models\Server */ protected $server; /** - * The request object. - * - * @var \Illuminate\Http\Request + * @var \Illuminate\Contracts\Session\Session */ - protected $request; + protected $session; /** - * Handle an incoming request. + * ServerAuthenticate constructor. + * + * @param \Illuminate\Contracts\Config\Repository $config + * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $repository + * @param \Illuminate\Contracts\Session\Session $session + */ + public function __construct( + ConfigRepository $config, + ServerRepositoryInterface $repository, + Session $session + ) { + $this->config = $config; + $this->repository = $repository; + $this->session = $session; + } + + /** + * Determine if a given user has permission to access a server. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed + * + * @throws \Illuminate\Auth\AuthenticationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function handle(Request $request, Closure $next) { - if (! Auth::user()) { - throw new AuthenticationException(); + if (! $request->user()) { + throw new AuthenticationException; } - $this->request = $request; - $this->server = Server::byUuid($request->route()->server); + $attributes = $request->route()->parameter('server'); + $isApiRequest = $request->expectsJson() || $request->is(...$this->config->get('pterodactyl.json_routes', [])); + $server = $this->repository->getByUuid($attributes instanceof Server ? $attributes->uuid : $attributes); + + if (! $server) { + if ($isApiRequest) { + throw new NotFoundHttpException('The requested server was not found on the system.'); + } - if (! $this->exists()) { return response()->view('errors.404', [], 404); } - if ($this->suspended()) { + if ($server->suspended) { + if ($isApiRequest) { + throw new AccessDeniedHttpException('Server is suspended.'); + } + return response()->view('errors.suspended', [], 403); } - if (! $this->installed()) { + if ($server->installed !== 1) { + if ($isApiRequest) { + throw new AccessDeniedHttpException('Server is completing install process.'); + } + return response()->view('errors.installing', [], 403); } + // Store the server in the session. + $this->session->now('server_data.model', $server); + return $next($request); } - - /** - * Determine if the server was found on the system. - * - * @return bool - */ - protected function exists() - { - if (! $this->server) { - if ($this->request->expectsJson() || $this->request->is(...config('pterodactyl.json_routes'))) { - throw new NotFoundHttpException('The requested server was not found on the system.'); - } - } - - return (! $this->server) ? false : true; - } - - /** - * Determine if the server is suspended. - * - * @return bool - */ - protected function suspended() - { - if ($this->server->suspended) { - if ($this->request->expectsJson() || $this->request->is(...config('pterodactyl.json_routes'))) { - throw new AccessDeniedHttpException('Server is suspended.'); - } - } - - return $this->server->suspended; - } - - /** - * Determine if the server is installed. - * - * @return bool - */ - protected function installed() - { - if ($this->server->installed !== 1) { - if ($this->request->expectsJson() || $this->request->is(...config('pterodactyl.json_routes'))) { - throw new AccessDeniedHttpException('Server is completing install process.'); - } - } - - return $this->server->installed === 1; - } } diff --git a/app/Http/Middleware/SubuserAccessAuthenticate.php b/app/Http/Middleware/SubuserAccessAuthenticate.php new file mode 100644 index 00000000..ca52d7e2 --- /dev/null +++ b/app/Http/Middleware/SubuserAccessAuthenticate.php @@ -0,0 +1,81 @@ +. + * + * 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\Http\Middleware; + +use Closure; +use Illuminate\Auth\AuthenticationException; +use Illuminate\Contracts\Session\Session; +use Illuminate\Http\Request; +use Pterodactyl\Exceptions\Service\Server\UserNotLinkedToServerException; +use Pterodactyl\Services\Servers\ServerAccessHelperService; + +class SubuserAccessAuthenticate +{ + /** + * @var \Pterodactyl\Services\Servers\ServerAccessHelperService + */ + protected $accessHelperService; + + /** + * @var \Illuminate\Contracts\Session\Session + */ + protected $session; + + /** + * SubuserAccessAuthenticate constructor. + * + * @param \Pterodactyl\Services\Servers\ServerAccessHelperService $accessHelperService + * @param \Illuminate\Contracts\Session\Session $session + */ + public function __construct( + ServerAccessHelperService $accessHelperService, + Session $session + ) { + $this->accessHelperService = $accessHelperService; + $this->session = $session; + } + + /** + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * @return mixed + * + * @throws \Illuminate\Auth\AuthenticationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function handle(Request $request, Closure $next) + { + $server = $this->session->get('server_data.model'); + + try { + $token = $this->accessHelperService->handle($server, $request->user()); + $this->session->now('server_data.token', $token); + } catch (UserNotLinkedToServerException $exception) { + throw new AuthenticationException('This account does not have permission to access this server.'); + } + + return $next($request); + } +} diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 9876208c..64b747d0 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -50,7 +50,7 @@ class RouteServiceProvider extends ServiceProvider ->namespace($this->namespace . '\Auth') ->group(base_path('routes/auth.php')); - Route::middleware(['web', 'auth', 'server', 'csrf'])->prefix('/server/{server}') + Route::middleware(['web', 'csrf', 'auth', 'server', 'subuser'])->prefix('/server/{server}') ->namespace($this->namespace . '\Server') ->group(base_path('routes/server.php')); diff --git a/app/Services/Servers/ServerAccessHelperService.php b/app/Services/Servers/ServerAccessHelperService.php index 2aef717f..deabbc05 100644 --- a/app/Services/Servers/ServerAccessHelperService.php +++ b/app/Services/Servers/ServerAccessHelperService.php @@ -24,6 +24,8 @@ namespace Pterodactyl\Services\Servers; +use Pterodactyl\Exceptions\Service\Server\UserNotLinkedToServerException; +use Pterodactyl\Models\Server; use Pterodactyl\Models\User; use Illuminate\Cache\Repository as CacheRepository; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; @@ -44,28 +46,37 @@ class ServerAccessHelperService $this->userRepository = $userRepository; } - public function handle($uuid, $user) + /** + * @param int|\Pterodactyl\Models\Server $server + * @param int|\Pterodactyl\Models\User $user + * @return string + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Pterodactyl\Exceptions\Service\Server\UserNotLinkedToServerException + */ + public function handle($server, $user) { + if (! $server instanceof Server) { + $server = $this->repository->find($server); + } + if (! $user instanceof User) { $user = $this->userRepository->find($user); } - $server = $this->repository->getByUuid($uuid); - if (! $user->root_admin) { - if (! in_array($server->id, $this->repository->getUserAccessServers($user->id))) { - throw new \Exception('User does not have access.'); - } - - if ($server->owner_id !== $user->id) { - $subuser = $this->subuserRepository->withColumns('daemonSecret')->findWhere([ - ['user_id', '=', $user->id], - ['server_id', '=', $server->id], - ]); - - $server->daemonSecret = $subuser->daemonToken; - } + if ($user->root_admin || $server->owner_id === $user->id) { + return $server->daemonSecret; } - return $server; + if (! in_array($server->id, $this->repository->getUserAccessServers($user->id))) { + throw new UserNotLinkedToServerException; + } + + $subuser = $this->subuserRepository->withColumns('daemonSecret')->findWhere([ + ['user_id', '=', $user->id], + ['server_id', '=', $server->id], + ]); + + return $subuser->daemonSecret; } } diff --git a/app/Traits/Controllers/ServerToJavascript.php b/app/Traits/Controllers/ServerToJavascript.php new file mode 100644 index 00000000..6c550f58 --- /dev/null +++ b/app/Traits/Controllers/ServerToJavascript.php @@ -0,0 +1,64 @@ +. + * + * 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\Traits\Controllers; + +use Javascript; + +trait ServerToJavascript +{ + /** + * @var \Illuminate\Contracts\Session\Session + */ + protected $session; + + /** + * Injects server javascript into the page to be used by other services. + * + * @param array $args + * @param bool $overwrite + * @return mixed + */ + public function injectJavascript($args = [], $overwrite = false) + { + $server = $this->session->get('server_data.model'); + $token = $this->session->get('server_data.token'); + + $response = array_merge([ + 'server' => [ + 'uuid' => $server->uuid, + 'uuidShort' => $server->uuidShort, + 'daemonSecret' => $token, + 'username' => $server->username, + ], + 'node' => [ + 'fqdn' => $server->node->fqdn, + 'scheme' => $server->node->scheme, + 'daemonListen' => $server->node->daemonListen, + ], + ], $args); + + return Javascript::put($overwrite ? $args : $response); + } +} diff --git a/resources/themes/pterodactyl/layouts/master.blade.php b/resources/themes/pterodactyl/layouts/master.blade.php index 7a137d10..a0963fd9 100644 --- a/resources/themes/pterodactyl/layouts/master.blade.php +++ b/resources/themes/pterodactyl/layouts/master.blade.php @@ -74,9 +74,9 @@ -
  • - -
  • + {{--
  • --}} + {{----}} + {{--
  • --}} @if(Auth::user()->isRootAdmin())
  • @@ -241,27 +241,28 @@ diff --git a/routes/server.php b/routes/server.php index d446bf6a..f8e68ee6 100644 --- a/routes/server.php +++ b/routes/server.php @@ -21,8 +21,8 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -Route::get('/', 'ServerController@getIndex')->name('server.index'); -Route::get('/console', 'ServerController@getConsole')->name('server.console'); +Route::get('/', 'ConsoleController@index')->name('server.index'); +Route::get('/console', 'ConsoleController@console')->name('server.console'); /* |-------------------------------------------------------------------------- diff --git a/tests/Unit/Http/Controllers/Server/ConsoleControllerTest.php b/tests/Unit/Http/Controllers/Server/ConsoleControllerTest.php new file mode 100644 index 00000000..e56c4ea7 --- /dev/null +++ b/tests/Unit/Http/Controllers/Server/ConsoleControllerTest.php @@ -0,0 +1,105 @@ +. + * + * 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\Http\Controllers\Server; + +use Illuminate\Contracts\Session\Session; +use Mockery as m; +use Pterodactyl\Http\Controllers\Server\ConsoleController; +use Pterodactyl\Models\Node; +use Pterodactyl\Models\Server; +use Tests\Assertions\ControllerAssertionsTrait; +use Tests\TestCase; +use Illuminate\Contracts\Config\Repository; + +class ConsoleControllerTest extends TestCase +{ + use ControllerAssertionsTrait; + + /** + * @var \Illuminate\Contracts\Config\Repository + */ + protected $config; + + /** + * @var \Pterodactyl\Http\Controllers\Server\ConsoleController + */ + protected $controller; + + /** + * @var \Illuminate\Contracts\Session\Session + */ + protected $session; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->config = m::mock(Repository::class); + $this->session = m::mock(Session::class); + + $this->controller = m::mock(ConsoleController::class, [$this->config, $this->session])->makePartial(); + } + + /** + * Test both controllers as they do effectively the same thing. + * + * @dataProvider controllerDataProvider + */ + public function testAllControllers($function, $view) + { + $server = factory(Server::class)->make(); + $node = factory(Node::class)->make(); + $server->node = $node; + + $this->session->shouldReceive('get')->with('server_data.model')->once()->andReturn($server); + $this->config->shouldReceive('get')->with('pterodactyl.console.count')->once()->andReturn(100); + $this->config->shouldReceive('get')->with('pterodactyl.console.frequency')->once()->andReturn(10); + $this->controller->shouldReceive('injectJavascript')->once()->andReturnNull(); + + $response = $this->controller->$function(); + $this->assertIsViewResponse($response); + $this->assertViewNameEquals($view, $response); + $this->assertViewHasKey('server', $response); + $this->assertViewHasKey('node', $response); + $this->assertViewKeyEquals('server', $server, $response); + $this->assertViewKeyEquals('node', $node, $response); + } + + /** + * Provide data for the tests. + * + * @return array + */ + public function controllerDataProvider() + { + return [ + ['index', 'server.index'], + ['console', 'server.console'], + ]; + } +} From 54554465f2e45abed47ae5b405774ba8fe91dabb Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 3 Sep 2017 16:32:52 -0500 Subject: [PATCH 79/99] Add more front-end controllers, language file cleanup --- .../Daemon/FileRepositoryInterface.php | 8 + .../Http/Server/FileSizeTooLargeException.php | 31 +++ .../Server/FileTypeNotEditableException.php | 31 +++ .../Controllers/Server/AjaxController.php | 129 ----------- .../Controllers/Server/ConsoleController.php | 10 +- .../Server/Files/DownloadController.php | 76 ++++++ .../Server/Files/FileActionsController.php | 161 +++++++++++++ .../Server/Files/RemoteRequestController.php | 166 ++++++++++++++ .../Controllers/Server/ServerController.php | 121 ---------- .../Server/UpdateFileContentsFormRequest.php | 127 ++++++++++ .../Server/ServerDataComposer.php | 60 +++++ app/Providers/ViewComposerServiceProvider.php | 39 ++++ app/Repositories/Daemon/FileRepository.php | 2 +- .../Allocations/AssignmentService.php | 6 +- app/Services/Database/DatabaseHostService.php | 2 +- app/Services/Nodes/NodeDeletionService.php | 2 +- app/Services/Nodes/NodeUpdateService.php | 2 +- app/Services/Packs/PackCreationService.php | 4 +- app/Services/Packs/PackDeletionService.php | 2 +- app/Services/Packs/PackUpdateService.php | 2 +- app/Services/Packs/TemplateUploadService.php | 10 +- .../Options/InstallScriptUpdateService.php | 2 +- .../Options/OptionCreationService.php | 2 +- .../Options/OptionDeletionService.php | 2 +- .../Services/Options/OptionUpdateService.php | 2 +- .../Services/ServiceDeletionService.php | 2 +- .../Variables/VariableUpdateService.php | 4 +- .../Subusers/SubuserCreationService.php | 6 +- .../Subusers/SubuserDeletionService.php | 2 +- .../Subusers/SubuserUpdateService.php | 2 +- ...Javascript.php => JavascriptInjection.php} | 2 +- config/app.php | 1 + resources/lang/en/{admin => }/exceptions.php | 0 resources/lang/en/server.php | 5 + routes/server.php | 16 +- .../Assertions/ControllerAssertionsTrait.php | 13 +- .../Controllers/Base/APIControllerTest.php | 2 +- .../Base/AccountControllerTest.php | 6 +- .../Base/SecurityControllerTest.php | 6 +- .../Server/ConsoleControllerTest.php | 11 +- .../Server/Files/DownloadControllerTest.php | 92 ++++++++ .../Files/FileActionsControllerTest.php | 217 ++++++++++++++++++ .../Allocations/AssignmentServiceTest.php | 6 +- .../Database/DatabaseHostServiceTest.php | 2 +- .../Nodes/NodeDeletionServiceTest.php | 2 +- .../Services/Nodes/NodeUpdateServiceTest.php | 2 +- .../Packs/PackCreationServiceTest.php | 4 +- .../Packs/PackDeletionServiceTest.php | 2 +- .../Services/Packs/PackUpdateServiceTest.php | 2 +- .../Packs/TemplateUploadServiceTest.php | 10 +- .../InstallScriptUpdateServiceTest.php | 2 +- .../Options/OptionCreationServiceTest.php | 2 +- .../Options/OptionDeletionServiceTest.php | 2 +- .../Options/OptionUpdateServiceTest.php | 2 +- .../Services/ServiceDeletionServiceTest.php | 2 +- .../Variables/VariableUpdateServiceTest.php | 2 +- .../Subusers/SubuserCreationServiceTest.php | 4 +- .../Subusers/SubuserDeletionServiceTest.php | 2 +- .../Subusers/SubuserUpdateServiceTest.php | 2 +- 59 files changed, 1100 insertions(+), 336 deletions(-) create mode 100644 app/Exceptions/Http/Server/FileSizeTooLargeException.php create mode 100644 app/Exceptions/Http/Server/FileTypeNotEditableException.php create mode 100644 app/Http/Controllers/Server/Files/DownloadController.php create mode 100644 app/Http/Controllers/Server/Files/FileActionsController.php create mode 100644 app/Http/Controllers/Server/Files/RemoteRequestController.php create mode 100644 app/Http/Requests/Server/UpdateFileContentsFormRequest.php create mode 100644 app/Http/ViewComposers/Server/ServerDataComposer.php create mode 100644 app/Providers/ViewComposerServiceProvider.php rename app/Traits/Controllers/{ServerToJavascript.php => JavascriptInjection.php} (98%) rename resources/lang/en/{admin => }/exceptions.php (100%) create mode 100644 tests/Unit/Http/Controllers/Server/Files/DownloadControllerTest.php create mode 100644 tests/Unit/Http/Controllers/Server/Files/FileActionsControllerTest.php diff --git a/app/Contracts/Repository/Daemon/FileRepositoryInterface.php b/app/Contracts/Repository/Daemon/FileRepositoryInterface.php index a013bc1a..d395794f 100644 --- a/app/Contracts/Repository/Daemon/FileRepositoryInterface.php +++ b/app/Contracts/Repository/Daemon/FileRepositoryInterface.php @@ -31,6 +31,8 @@ interface FileRepositoryInterface extends BaseRepositoryInterface * * @param string $path * @return object + * + * @throws \GuzzleHttp\Exception\RequestException */ public function getFileStat($path); @@ -39,6 +41,8 @@ interface FileRepositoryInterface extends BaseRepositoryInterface * * @param string $path * @return object + * + * @throws \GuzzleHttp\Exception\RequestException */ public function getContent($path); @@ -48,6 +52,8 @@ interface FileRepositoryInterface extends BaseRepositoryInterface * @param string $path * @param string $content * @return \Psr\Http\Message\ResponseInterface + * + * @throws \GuzzleHttp\Exception\RequestException */ public function putContent($path, $content); @@ -56,6 +62,8 @@ interface FileRepositoryInterface extends BaseRepositoryInterface * * @param string $path * @return array + * + * @throws \GuzzleHttp\Exception\RequestException */ public function getDirectory($path); } diff --git a/app/Exceptions/Http/Server/FileSizeTooLargeException.php b/app/Exceptions/Http/Server/FileSizeTooLargeException.php new file mode 100644 index 00000000..b2da45b6 --- /dev/null +++ b/app/Exceptions/Http/Server/FileSizeTooLargeException.php @@ -0,0 +1,31 @@ +. + * + * 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\Exceptions\Http\Server; + +use Pterodactyl\Exceptions\DisplayException; + +class FileSizeTooLargeException extends DisplayException +{ +} diff --git a/app/Exceptions/Http/Server/FileTypeNotEditableException.php b/app/Exceptions/Http/Server/FileTypeNotEditableException.php new file mode 100644 index 00000000..96f9f4d4 --- /dev/null +++ b/app/Exceptions/Http/Server/FileTypeNotEditableException.php @@ -0,0 +1,31 @@ +. + * + * 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\Exceptions\Http\Server; + +use Pterodactyl\Exceptions\DisplayException; + +class FileTypeNotEditableException extends DisplayException +{ +} diff --git a/app/Http/Controllers/Server/AjaxController.php b/app/Http/Controllers/Server/AjaxController.php index 01adb250..f4b54ca8 100644 --- a/app/Http/Controllers/Server/AjaxController.php +++ b/app/Http/Controllers/Server/AjaxController.php @@ -30,7 +30,6 @@ use Illuminate\Http\Request; use Pterodactyl\Repositories; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Exceptions\DisplayValidationException; class AjaxController extends Controller { @@ -49,134 +48,6 @@ class AjaxController extends Controller */ protected $directory; - /** - * Returns a listing of files in a given directory for a server. - * - * @param \Illuminate\Http\Request $request - * @param string $uuid - * @return \Illuminate\View\View|\Illuminate\Http\Response - */ - public function postDirectoryList(Request $request, $uuid) - { - $server = Models\Server::byUuid($uuid); - $this->authorize('list-files', $server); - - $this->directory = '/' . trim(urldecode($request->input('directory', '/')), '/'); - $prevDir = [ - 'header' => ($this->directory !== '/') ? $this->directory : '', - ]; - if ($this->directory !== '/') { - $prevDir['first'] = true; - } - - // Determine if we should show back links in the file browser. - // This code is strange, and could probably be rewritten much better. - $goBack = explode('/', trim($this->directory, '/')); - if (! empty(array_filter($goBack)) && count($goBack) >= 2) { - $prevDir['show'] = true; - array_pop($goBack); - $prevDir['link'] = '/' . implode('/', $goBack); - $prevDir['link_show'] = implode('/', $goBack) . '/'; - } - - $controller = new Repositories\old_Daemon\FileRepository($uuid); - - try { - $directoryContents = $controller->returnDirectoryListing($this->directory); - } catch (DisplayException $ex) { - return response($ex->getMessage(), 500); - } catch (\Exception $ex) { - Log::error($ex); - - return response('An error occured while attempting to load the requested directory, please try again.', 500); - } - - return view('server.files.list', [ - 'server' => $server, - 'files' => $directoryContents->files, - 'folders' => $directoryContents->folders, - 'editableMime' => Repositories\HelperRepository::editableFiles(), - 'directory' => $prevDir, - ]); - } - - /** - * Handles a POST request to save a file. - * - * @param \Illuminate\Http\Request $request - * @param string $uuid - * @return \Illuminate\Http\Response - */ - public function postSaveFile(Request $request, $uuid) - { - $server = Models\Server::byUuid($uuid); - $this->authorize('save-files', $server); - - $controller = new Repositories\old_Daemon\FileRepository($uuid); - - try { - $controller->saveFileContents($request->input('file'), $request->input('contents')); - - return response(null, 204); - } catch (DisplayException $ex) { - return response($ex->getMessage(), 500); - } catch (\Exception $ex) { - Log::error($ex); - - return response('An error occured while attempting to save this file, please try again.', 500); - } - } - - /** - * Sets the primary allocation for a server. - * - * @param \Illuminate\Http\Request $request - * @param string $uuid - * @return \Illuminate\Http\JsonResponse - * @deprecated - */ - public function postSetPrimary(Request $request, $uuid) - { - $server = Models\Server::byUuid($uuid)->load('allocations'); - $this->authorize('set-connection', $server); - - if ((int) $request->input('allocation') === $server->allocation_id) { - return response()->json([ - 'error' => 'You are already using this as your default connection.', - ], 409); - } - - try { - $allocation = $server->allocations->where('id', $request->input('allocation'))->where('server_id', $server->id)->first(); - if (! $allocation) { - return response()->json([ - 'error' => 'No allocation matching your request was found in the system.', - ], 422); - } - - $repo = new Repositories\ServerRepository; - $repo->changeBuild($server->id, [ - 'default' => $allocation->ip . ':' . $allocation->port, - ]); - - return response('The default connection for this server has been updated. Please be aware that you will need to restart your server for this change to go into effect.'); - } catch (DisplayValidationException $ex) { - return response()->json([ - 'error' => json_decode($ex->getMessage(), true), - ], 422); - } catch (DisplayException $ex) { - return response()->json([ - 'error' => $ex->getMessage(), - ], 503); - } catch (\Exception $ex) { - Log::error($ex); - - return response()->json([ - 'error' => 'An unhandled exception occured while attemping to modify the default connection for this server.', - ], 503); - } - } - /** * Resets a database password for a server. * diff --git a/app/Http/Controllers/Server/ConsoleController.php b/app/Http/Controllers/Server/ConsoleController.php index 8f43b1a2..ca20852e 100644 --- a/app/Http/Controllers/Server/ConsoleController.php +++ b/app/Http/Controllers/Server/ConsoleController.php @@ -26,12 +26,12 @@ namespace Pterodactyl\Http\Controllers\Server; use Illuminate\Contracts\Session\Session; use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Traits\Controllers\ServerToJavascript; +use Pterodactyl\Traits\Controllers\JavascriptInjection; use Illuminate\Contracts\Config\Repository as ConfigRepository; class ConsoleController extends Controller { - use ServerToJavascript; + use JavascriptInjection; /** * @var \Illuminate\Contracts\Config\Repository @@ -77,7 +77,7 @@ class ConsoleController extends Controller ], ]); - return view('server.index', ['server' => $server, 'node' => $server->node]); + return view('server.index'); } /** @@ -87,13 +87,11 @@ class ConsoleController extends Controller */ public function console() { - $server = $this->session->get('server_data.model'); - $this->injectJavascript(['config' => [ 'console_count' => $this->config->get('pterodactyl.console.count'), 'console_freq' => $this->config->get('pterodactyl.console.frequency'), ]]); - return view('server.console', ['server' => $server, 'node' => $server->node]); + return view('server.console'); } } diff --git a/app/Http/Controllers/Server/Files/DownloadController.php b/app/Http/Controllers/Server/Files/DownloadController.php new file mode 100644 index 00000000..32d4cb5f --- /dev/null +++ b/app/Http/Controllers/Server/Files/DownloadController.php @@ -0,0 +1,76 @@ +. + * + * 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\Http\Controllers\Server\Files; + +use Illuminate\Cache\Repository; +use Illuminate\Contracts\Session\Session; +use Pterodactyl\Http\Controllers\Controller; + +class DownloadController extends Controller +{ + /** + * @var \Illuminate\Cache\Repository + */ + protected $cache; + + /** + * @var \Illuminate\Contracts\Session\Session + */ + protected $session; + + /** + * DownloadController constructor. + * + * @param \Illuminate\Cache\Repository $cache + * @param \Illuminate\Contracts\Session\Session $session + */ + public function __construct(Repository $cache, Session $session) + { + $this->cache = $cache; + $this->session = $session; + } + + /** + * Setup a unique download link for a user to download a file from. + * + * @param string $uuid + * @param string $file + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector + * + * @throws \Illuminate\Auth\Access\AuthorizationException + */ + public function index($uuid, $file) + { + $server = $this->session->get('server_data.model'); + $this->authorize('download-files', $server); + + $token = str_random(40); + $this->cache->tags(['Server:Downloads'])->put($token, ['server' => $server->uuid, 'path' => $file], 5); + + return redirect(sprintf( + '%s://%s:%s/server/file/download/%s', $server->node->scheme, $server->node->fqdn, $server->node->daemonListen, $token + )); + } +} diff --git a/app/Http/Controllers/Server/Files/FileActionsController.php b/app/Http/Controllers/Server/Files/FileActionsController.php new file mode 100644 index 00000000..902e713c --- /dev/null +++ b/app/Http/Controllers/Server/Files/FileActionsController.php @@ -0,0 +1,161 @@ +. + * + * 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\Http\Controllers\Server\Files; + +use GuzzleHttp\Exception\RequestException; +use Illuminate\Contracts\Session\Session; +use Illuminate\Http\Request; +use Illuminate\Log\Writer; +use Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Http\Controllers\Controller; +use Pterodactyl\Http\Requests\Server\UpdateFileContentsFormRequest; +use Pterodactyl\Traits\Controllers\JavascriptInjection; + +class FileActionsController extends Controller +{ + use JavascriptInjection; + + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface + */ + protected $fileRepository; + + /** + * @var \Illuminate\Contracts\Session\Session + */ + protected $session; + + /** + * @var \Illuminate\Log\Writer + */ + protected $writer; + + /** + * FileActionsController constructor. + * + * @param \Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface $fileRepository + * @param \Illuminate\Contracts\Session\Session $session + * @param \Illuminate\Log\Writer $writer + */ + public function __construct(FileRepositoryInterface $fileRepository, Session $session, Writer $writer) + { + $this->fileRepository = $fileRepository; + $this->session = $session; + $this->writer = $writer; + } + + /** + * Display server file index list. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\View\View + * + * @throws \Illuminate\Auth\Access\AuthorizationException + */ + public function index(Request $request) + { + $server = $this->session->get('server_data.model'); + $this->authorize('list-files', $server); + + $this->injectJavascript([ + 'meta' => [ + 'directoryList' => route('server.files.directory-list', $server->uuidShort), + 'csrftoken' => csrf_token(), + ], + 'permissions' => [ + 'moveFiles' => $request->user()->can('move-files', $server), + 'copyFiles' => $request->user()->can('copy-files', $server), + 'compressFiles' => $request->user()->can('compress-files', $server), + 'decompressFiles' => $request->user()->can('decompress-files', $server), + 'createFiles' => $request->user()->can('create-files', $server), + 'downloadFiles' => $request->user()->can('download-files', $server), + 'deleteFiles' => $request->user()->can('delete-files', $server), + ], + ]); + + return view('server.files.index'); + } + + /** + * Render page to manually create a file in the panel. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\View\View + * + * @throws \Illuminate\Auth\Access\AuthorizationException + */ + public function create(Request $request) + { + $this->authorize('create-files', $this->session->get('server_data.model')); + $this->injectJavascript(); + + return view('server.files.add', [ + 'directory' => (in_array($request->get('dir'), [null, '/', ''])) ? '' : trim($request->get('dir'), '/') . '/', + ]); + } + + /** + * Display a form to allow for editing of a file. + * + * @param \Pterodactyl\Http\Requests\Server\UpdateFileContentsFormRequest $request + * @param string $uuid + * @param string $file + * @return \Illuminate\View\View + * + * @throws \Illuminate\Auth\Access\AuthorizationException + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function update(UpdateFileContentsFormRequest $request, $uuid, $file) + { + $server = $this->session->get('server_data.model'); + $this->authorize('edit-files', $server); + + $dirname = pathinfo($file, PATHINFO_DIRNAME); + try { + $content = $this->fileRepository->setNode($server->node_id) + ->setAccessServer($server->uuid) + ->setAccessToken($this->session->get('server_data.token')) + ->getContent($file); + } catch (RequestException $exception) { + $response = $exception->getResponse(); + $this->writer->warning($exception); + + throw new DisplayException(trans('exceptions.daemon_connection_failed', [ + 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), + ])); + } + + $this->injectJavascript(['stat' => $request->getStats()]); + + return view('server.files.edit', [ + 'file' => $file, + 'stat' => $request->getStats(), + 'contents' => $content, + 'directory' => (in_array($dirname, ['.', './', '/'])) ? '/' : trim($dirname, '/') . '/', + ]); + } +} diff --git a/app/Http/Controllers/Server/Files/RemoteRequestController.php b/app/Http/Controllers/Server/Files/RemoteRequestController.php new file mode 100644 index 00000000..f5506fb1 --- /dev/null +++ b/app/Http/Controllers/Server/Files/RemoteRequestController.php @@ -0,0 +1,166 @@ +. + * + * 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\Http\Controllers\Server\Files; + +use GuzzleHttp\Exception\RequestException; +use Illuminate\Contracts\Config\Repository as ConfigRepository; +use Illuminate\Contracts\Session\Session; +use Illuminate\Http\Request; +use Illuminate\Log\Writer; +use Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface; +use Pterodactyl\Http\Controllers\Controller; + +class RemoteRequestController extends Controller +{ + /** + * @var \Illuminate\Contracts\Config\Repository + */ + protected $config; + + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface + */ + protected $fileRepository; + + /** + * @var \Illuminate\Contracts\Session\Session + */ + protected $session; + + /** + * @var \Illuminate\Log\Writer + */ + protected $writer; + + /** + * RemoteRequestController constructor. + * + * @param \Illuminate\Contracts\Config\Repository $config + * @param \Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface $fileRepository + * @param \Illuminate\Contracts\Session\Session $session + * @param \Illuminate\Log\Writer $writer + */ + public function __construct( + ConfigRepository $config, + FileRepositoryInterface $fileRepository, + Session $session, + Writer $writer + ) { + $this->config = $config; + $this->fileRepository = $fileRepository; + $this->session = $session; + $this->writer = $writer; + } + + /** + * Return a listing of a servers file directory. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse|\Illuminate\View\View + * + * @throws \Illuminate\Auth\Access\AuthorizationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function directory(Request $request) + { + $server = $this->session->get('server_data.model'); + $this->authorize('list-files', $server); + + $requestDirectory = '/' . trim(urldecode($request->input('directory', '/')), '/'); + $directory = [ + 'header' => $requestDirectory !== '/' ? $requestDirectory : '', + 'first' => $requestDirectory !== '/', + ]; + + $goBack = explode('/', trim($requestDirectory, '/')); + if (! empty(array_filter($goBack)) && count($goBack) >= 2) { + array_pop($goBack); + + $directory['show'] = true; + $directory['link'] = '/' . implode('/', $goBack); + $directory['link_show'] = implode('/', $goBack) . '/'; + } + + try { + $listing = $this->fileRepository->setNode($server->node_id) + ->setAccessServer($server->uuid) + ->setAccessToken($this->session->get('server_data.token')) + ->getDirectory($requestDirectory); + } catch (RequestException $exception) { + $this->writer->warning($exception); + + if (! is_null($exception->getResponse())) { + return response()->json( + ['error' => $exception->getResponse()->getBody()], $exception->getResponse()->getStatusCode() + ); + } else { + return response()->json(['error' => trans('server.files.exceptions.list_directory')], 500); + } + } + + return view('server.files.list', [ + 'files' => $listing['files'], + 'folders' => $listing['folders'], + 'editableMime' => $this->config->get('pterodactyl.files.editable'), + 'directory' => $directory, + ]); + } + + /** + * Put the contents of a file onto the daemon. + * + * @param \Illuminate\Http\Request $request + * @param string $uuid + * @return \Illuminate\Http\JsonResponse + * + * @throws \Illuminate\Auth\Access\AuthorizationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function store(Request $request, $uuid) + { + $server = $this->session->get('server_data.model'); + $this->authorize('save-files', $server); + + try { + $this->fileRepository->setNode($server->node_id) + ->setAccessServer($server->uuid) + ->setAccessToken($this->session->get('server_data.token')) + ->putContent($request->input('file'), $request->input('contents')); + + return response('', 204); + } catch (RequestException $exception) { + $response = $exception->getResponse(); + $this->writer->warning($exception); + + if (! is_null($response)) { + return response()->json(['error' => $response->getBody()], $response->getStatusCode()); + } else { + return response()->json(['error' => trans('exceptions.daemon_connection_failed', [ + 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), + ])], 500); + } + } + } +} diff --git a/app/Http/Controllers/Server/ServerController.php b/app/Http/Controllers/Server/ServerController.php index c677c1cd..e5bad57a 100644 --- a/app/Http/Controllers/Server/ServerController.php +++ b/app/Http/Controllers/Server/ServerController.php @@ -26,7 +26,6 @@ namespace Pterodactyl\Http\Controllers\Server; use Log; use Alert; -use Cache; use Pterodactyl\Models; use Illuminate\Http\Request; use Pterodactyl\Exceptions\DisplayException; @@ -35,126 +34,6 @@ use Pterodactyl\Exceptions\DisplayValidationException; class ServerController extends Controller { - /** - * Renders file overview page. - * - * @param \Illuminate\Http\Request $request - * @param string $uuid - * @return \Illuminate\View\View - */ - public function getFiles(Request $request, $uuid) - { - $server = Models\Server::byUuid($uuid); - $this->authorize('list-files', $server); - - $server->js([ - 'meta' => [ - 'directoryList' => route('server.files.directory-list', $server->uuidShort), - 'csrftoken' => csrf_token(), - ], - 'permissions' => [ - 'moveFiles' => $request->user()->can('move-files', $server), - 'copyFiles' => $request->user()->can('copy-files', $server), - 'compressFiles' => $request->user()->can('compress-files', $server), - 'decompressFiles' => $request->user()->can('decompress-files', $server), - 'createFiles' => $request->user()->can('create-files', $server), - 'downloadFiles' => $request->user()->can('download-files', $server), - 'deleteFiles' => $request->user()->can('delete-files', $server), - ], - ]); - - return view('server.files.index', [ - 'server' => $server, - 'node' => $server->node, - ]); - } - - /** - * Renders add file page. - * - * @param \Illuminate\Http\Request $request - * @param string $uuid - * @return \Illuminate\View\View - */ - public function getAddFile(Request $request, $uuid) - { - $server = Models\Server::byUuid($uuid); - $this->authorize('create-files', $server); - - $server->js(); - - return view('server.files.add', [ - 'server' => $server, - 'node' => $server->node, - 'directory' => (in_array($request->get('dir'), [null, '/', ''])) ? '' : trim($request->get('dir'), '/') . '/', - ]); - } - - /** - * Renders edit file page for a given file. - * - * @param \Illuminate\Http\Request $request - * @param string $uuid - * @param string $file - * @return \Illuminate\View\View - */ - public function getEditFile(Request $request, $uuid, $file) - { - $server = Models\Server::byUuid($uuid); - $this->authorize('edit-files', $server); - - $fileInfo = (object) pathinfo($file); - $controller = new FileRepository($uuid); - - try { - $fileContent = $controller->returnFileContents($file); - } catch (DisplayException $ex) { - Alert::danger($ex->getMessage())->flash(); - - return redirect()->route('server.files.index', $uuid); - } catch (\Exception $ex) { - Log::error($ex); - Alert::danger('An error occured while attempting to load the requested file for editing, please try again.')->flash(); - - return redirect()->route('server.files.index', $uuid); - } - - $server->js([ - 'stat' => $fileContent['stat'], - ]); - - return view('server.files.edit', [ - 'server' => $server, - 'node' => $server->node, - 'file' => $file, - 'stat' => $fileContent['stat'], - 'contents' => $fileContent['file']->content, - 'directory' => (in_array($fileInfo->dirname, ['.', './', '/'])) ? '/' : trim($fileInfo->dirname, '/') . '/', - ]); - } - - /** - * Handles downloading a file for the user. - * - * @param \Illuminate\Http\Request $request - * @param string $uuid - * @param string $file - * @return \Illuminate\View\View - */ - public function getDownloadFile(Request $request, $uuid, $file) - { - $server = Models\Server::byUuid($uuid); - $this->authorize('download-files', $server); - - $token = str_random(40); - Cache::tags(['Server:Downloads'])->put($token, [ - 'server' => $server->uuid, - 'path' => $file, - ], 5); - - return redirect($server->node->scheme . '://' . $server->node->fqdn . ':' . $server->node->daemonListen . '/server/file/download/' . $token); - } - /** * Returns the allocation overview for a server. * diff --git a/app/Http/Requests/Server/UpdateFileContentsFormRequest.php b/app/Http/Requests/Server/UpdateFileContentsFormRequest.php new file mode 100644 index 00000000..cde7d8a0 --- /dev/null +++ b/app/Http/Requests/Server/UpdateFileContentsFormRequest.php @@ -0,0 +1,127 @@ +. + * + * 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\Http\Requests\Server; + +use GuzzleHttp\Exception\RequestException; +use Illuminate\Contracts\Config\Repository; +use Illuminate\Contracts\Session\Session; +use Illuminate\Log\Writer; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Exceptions\Http\Server\FileSizeTooLargeException; +use Pterodactyl\Exceptions\Http\Server\FileTypeNotEditableException; +use Pterodactyl\Http\Requests\FrontendUserFormRequest; +use Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface; + +class UpdateFileContentsFormRequest extends FrontendUserFormRequest +{ + /** + * @var object + */ + protected $stats; + + /** + * Authorize a request to edit a file. + * + * @return bool + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Http\Server\FileSizeTooLargeException + * @throws \Pterodactyl\Exceptions\Http\Server\FileTypeNotEditableException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function authorize() + { + parent::authorize(); + + $session = app()->make(Session::class); + $server = $session->get('server_data.model'); + $token = $session->get('server_data.token'); + + $permission = $this->user()->can('edit-files', $server); + if (! $permission) { + return false; + } + + return $this->checkFileCanBeEdited($server, $token); + } + + /** + * @return array + */ + public function rules() + { + return []; + } + + /** + * Return the file stats from the Daemon. + * + * @return object + */ + public function getStats() + { + return $this->stats; + } + + /** + * @param \Pterodactyl\Models\Server $server + * @param string $token + * @return bool + * + * @throws \Pterodactyl\Exceptions\DisplayException + * @throws \Pterodactyl\Exceptions\Http\Server\FileSizeTooLargeException + * @throws \Pterodactyl\Exceptions\Http\Server\FileTypeNotEditableException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + protected function checkFileCanBeEdited($server, $token) + { + $config = app()->make(Repository::class); + $repository = app()->make(FileRepositoryInterface::class); + + try { + $this->stats = $repository->setNode($server->node_id) + ->setAccessServer($server->uuid) + ->setAccessToken($token) + ->getFileStat($this->route()->parameter('file')); + } catch (RequestException $exception) { + $response = $exception->getResponse(); + app()->make(Writer::class)->warning($exception); + + throw new DisplayException(trans('exceptions.daemon_connection_failed', [ + 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), + ])); + } + + if (! $this->stats->file || ! in_array($this->stats->mime, $config->get('pterodactyl.files.editable'))) { + throw new FileTypeNotEditableException(trans('server.files.exceptions.invalid_mime')); + } + + if ($this->stats->size > $config->get('pterodactyl.files.max_edit_size')) { + throw new FileSizeTooLargeException(trans('server.files.exceptions.max_size')); + } + + return true; + } +} diff --git a/app/Http/ViewComposers/Server/ServerDataComposer.php b/app/Http/ViewComposers/Server/ServerDataComposer.php new file mode 100644 index 00000000..93bdb1bf --- /dev/null +++ b/app/Http/ViewComposers/Server/ServerDataComposer.php @@ -0,0 +1,60 @@ +. + * + * 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\Http\ViewComposers\Server; + +use Illuminate\View\View; +use Illuminate\Contracts\Session\Session; + +class ServerDataComposer +{ + /** + * @var \Illuminate\Contracts\Session\Session + */ + protected $session; + + /** + * ServerDataComposer constructor. + * + * @param \Illuminate\Contracts\Session\Session $session + */ + public function __construct(Session $session) + { + $this->session = $session; + } + + /** + * Attach server data to a view automatically. + * + * @param \Illuminate\View\View $view + */ + public function compose(View $view) + { + $data = $this->session->get('server_data'); + + $view->with('server', array_get($data, 'model')); + $view->with('node', object_get($data['model'], 'node')); + $view->with('daemon_token', array_get($data, 'token')); + } +} diff --git a/app/Providers/ViewComposerServiceProvider.php b/app/Providers/ViewComposerServiceProvider.php new file mode 100644 index 00000000..df1648f1 --- /dev/null +++ b/app/Providers/ViewComposerServiceProvider.php @@ -0,0 +1,39 @@ +. + * + * 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\Providers; + +use Illuminate\Support\ServiceProvider; +use Pterodactyl\Http\ViewComposers\Server\ServerDataComposer; + +class ViewComposerServiceProvider extends ServiceProvider +{ + /** + * Register bindings in the container. + */ + public function boot() + { + $this->app->make('view')->composer('server.*', ServerDataComposer::class); + } +} diff --git a/app/Repositories/Daemon/FileRepository.php b/app/Repositories/Daemon/FileRepository.php index 30e73c82..ee173331 100644 --- a/app/Repositories/Daemon/FileRepository.php +++ b/app/Repositories/Daemon/FileRepository.php @@ -59,7 +59,7 @@ class FileRepository extends BaseRepository implements FileRepositoryInterface rawurlencode($file['dirname'] . $file['basename']) )); - return json_decode($response->getBody()); + return object_get(json_decode($response->getBody()), 'content'); } /** diff --git a/app/Services/Allocations/AssignmentService.php b/app/Services/Allocations/AssignmentService.php index abf31079..9ff713c8 100644 --- a/app/Services/Allocations/AssignmentService.php +++ b/app/Services/Allocations/AssignmentService.php @@ -78,7 +78,7 @@ class AssignmentService $explode = explode('/', $data['allocation_ip']); if (count($explode) !== 1) { if (! ctype_digit($explode[1]) || ($explode[1] > self::CIDR_MIN_BITS || $explode[1] < self::CIDR_MAX_BITS)) { - throw new DisplayException(trans('admin/exceptions.allocations.cidr_out_of_range')); + throw new DisplayException(trans('exceptions.allocations.cidr_out_of_range')); } } @@ -86,7 +86,7 @@ class AssignmentService foreach (Network::parse(gethostbyname($data['allocation_ip'])) as $ip) { foreach ($data['allocation_ports'] as $port) { if (! ctype_digit($port) && ! preg_match(self::PORT_RANGE_REGEX, $port)) { - throw new DisplayException(trans('admin/exceptions.allocations.invalid_mapping', ['port' => $port])); + throw new DisplayException(trans('exceptions.allocations.invalid_mapping', ['port' => $port])); } $insertData = []; @@ -94,7 +94,7 @@ class AssignmentService $block = range($matches[1], $matches[2]); if (count($block) > self::PORT_RANGE_LIMIT) { - throw new DisplayException(trans('admin/exceptions.allocations.too_many_ports')); + throw new DisplayException(trans('exceptions.allocations.too_many_ports')); } foreach ($block as $unit) { diff --git a/app/Services/Database/DatabaseHostService.php b/app/Services/Database/DatabaseHostService.php index f64f3a94..10e45e53 100644 --- a/app/Services/Database/DatabaseHostService.php +++ b/app/Services/Database/DatabaseHostService.php @@ -155,7 +155,7 @@ class DatabaseHostService { $count = $this->databaseRepository->findCountWhere([['database_host_id', '=', $id]]); if ($count > 0) { - throw new DisplayException(trans('admin/exceptions.databases.delete_has_databases')); + throw new DisplayException(trans('exceptions.databases.delete_has_databases')); } return $this->repository->delete($id); diff --git a/app/Services/Nodes/NodeDeletionService.php b/app/Services/Nodes/NodeDeletionService.php index 1a786822..26238339 100644 --- a/app/Services/Nodes/NodeDeletionService.php +++ b/app/Services/Nodes/NodeDeletionService.php @@ -80,7 +80,7 @@ class NodeDeletionService $servers = $this->serverRepository->withColumns('id')->findCountWhere([['node_id', '=', $node]]); if ($servers > 0) { - throw new HasActiveServersException($this->translator->trans('admin/exceptions.node.servers_attached')); + throw new HasActiveServersException($this->translator->trans('exceptions.node.servers_attached')); } return $this->repository->delete($node); diff --git a/app/Services/Nodes/NodeUpdateService.php b/app/Services/Nodes/NodeUpdateService.php index 199d72f3..9fd27332 100644 --- a/app/Services/Nodes/NodeUpdateService.php +++ b/app/Services/Nodes/NodeUpdateService.php @@ -95,7 +95,7 @@ class NodeUpdateService $response = $exception->getResponse(); $this->writer->warning($exception); - throw new DisplayException(trans('admin/exceptions.node.daemon_off_config_updated', [ + throw new DisplayException(trans('exceptions.node.daemon_off_config_updated', [ 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), ])); } diff --git a/app/Services/Packs/PackCreationService.php b/app/Services/Packs/PackCreationService.php index b4274023..3943d122 100644 --- a/app/Services/Packs/PackCreationService.php +++ b/app/Services/Packs/PackCreationService.php @@ -86,11 +86,11 @@ class PackCreationService { if (! is_null($file)) { if (! $file->isValid()) { - throw new InvalidFileUploadException(trans('admin/exceptions.packs.invalid_upload')); + throw new InvalidFileUploadException(trans('exceptions.packs.invalid_upload')); } if (! in_array($file->getMimeType(), self::VALID_UPLOAD_TYPES)) { - throw new InvalidFileMimeTypeException(trans('admin/exceptions.packs.invalid_mime', [ + throw new InvalidFileMimeTypeException(trans('exceptions.packs.invalid_mime', [ 'type' => implode(', ', self::VALID_UPLOAD_TYPES), ])); } diff --git a/app/Services/Packs/PackDeletionService.php b/app/Services/Packs/PackDeletionService.php index 590bdb4d..c288a3d0 100644 --- a/app/Services/Packs/PackDeletionService.php +++ b/app/Services/Packs/PackDeletionService.php @@ -89,7 +89,7 @@ class PackDeletionService $count = $this->serverRepository->findCountWhere([['pack_id', '=', $pack->id]]); if ($count !== 0) { - throw new HasActiveServersException(trans('admin/exceptions.packs.delete_has_servers')); + throw new HasActiveServersException(trans('exceptions.packs.delete_has_servers')); } $this->connection->beginTransaction(); diff --git a/app/Services/Packs/PackUpdateService.php b/app/Services/Packs/PackUpdateService.php index 5928b95a..f0390376 100644 --- a/app/Services/Packs/PackUpdateService.php +++ b/app/Services/Packs/PackUpdateService.php @@ -76,7 +76,7 @@ class PackUpdateService $count = $this->serverRepository->findCountWhere([['pack_id', '=', $pack->id]]); if ($count !== 0) { - throw new HasActiveServersException(trans('admin/exceptions.packs.update_has_servers')); + throw new HasActiveServersException(trans('exceptions.packs.update_has_servers')); } } diff --git a/app/Services/Packs/TemplateUploadService.php b/app/Services/Packs/TemplateUploadService.php index 248ccfcf..13175736 100644 --- a/app/Services/Packs/TemplateUploadService.php +++ b/app/Services/Packs/TemplateUploadService.php @@ -81,11 +81,11 @@ class TemplateUploadService public function handle($option, UploadedFile $file) { if (! $file->isValid()) { - throw new InvalidFileUploadException(trans('admin/exceptions.packs.invalid_upload')); + throw new InvalidFileUploadException(trans('exceptions.packs.invalid_upload')); } if (! in_array($file->getMimeType(), self::VALID_UPLOAD_TYPES)) { - throw new InvalidFileMimeTypeException(trans('admin/exceptions.packs.invalid_mime', [ + throw new InvalidFileMimeTypeException(trans('exceptions.packs.invalid_mime', [ 'type' => implode(', ', self::VALID_UPLOAD_TYPES), ])); } @@ -117,11 +117,11 @@ class TemplateUploadService protected function handleArchive($option, $file) { if (! $this->archive->open($file->getRealPath())) { - throw new UnreadableZipArchiveException(trans('admin/exceptions.packs.unreadable')); + throw new UnreadableZipArchiveException(trans('exceptions.packs.unreadable')); } if (! $this->archive->locateName('import.json') || ! $this->archive->locateName('archive.tar.gz')) { - throw new InvalidPackArchiveFormatException(trans('admin/exceptions.packs.invalid_archive_exception')); + throw new InvalidPackArchiveFormatException(trans('exceptions.packs.invalid_archive_exception')); } $json = json_decode($this->archive->getFromName('import.json'), true); @@ -130,7 +130,7 @@ class TemplateUploadService $pack = $this->creationService->handle($json); if (! $this->archive->extractTo(storage_path('app/packs/' . $pack->uuid), 'archive.tar.gz')) { // @todo delete the pack that was created. - throw new ZipExtractionException(trans('admin/exceptions.packs.zip_extraction')); + throw new ZipExtractionException(trans('exceptions.packs.zip_extraction')); } $this->archive->close(); diff --git a/app/Services/Services/Options/InstallScriptUpdateService.php b/app/Services/Services/Options/InstallScriptUpdateService.php index efdd08df..976c2144 100644 --- a/app/Services/Services/Options/InstallScriptUpdateService.php +++ b/app/Services/Services/Options/InstallScriptUpdateService.php @@ -63,7 +63,7 @@ class InstallScriptUpdateService if (! is_null(array_get($data, 'copy_script_from'))) { if (! $this->repository->isCopiableScript(array_get($data, 'copy_script_from'), $option->service_id)) { - throw new InvalidCopyFromException(trans('admin/exceptions.service.options.invalid_copy_id')); + throw new InvalidCopyFromException(trans('exceptions.service.options.invalid_copy_id')); } } diff --git a/app/Services/Services/Options/OptionCreationService.php b/app/Services/Services/Options/OptionCreationService.php index 8755f0e9..509f64c1 100644 --- a/app/Services/Services/Options/OptionCreationService.php +++ b/app/Services/Services/Options/OptionCreationService.php @@ -62,7 +62,7 @@ class OptionCreationService ]); if ($results !== 1) { - throw new NoParentConfigurationFoundException(trans('admin/exceptions.service.options.must_be_child')); + throw new NoParentConfigurationFoundException(trans('exceptions.service.options.must_be_child')); } } else { $data['config_from'] = null; diff --git a/app/Services/Services/Options/OptionDeletionService.php b/app/Services/Services/Options/OptionDeletionService.php index c614348f..02a1e734 100644 --- a/app/Services/Services/Options/OptionDeletionService.php +++ b/app/Services/Services/Options/OptionDeletionService.php @@ -69,7 +69,7 @@ class OptionDeletionService ]); if ($servers > 0) { - throw new HasActiveServersException(trans('admin/exceptions.service.options.delete_has_servers')); + throw new HasActiveServersException(trans('exceptions.service.options.delete_has_servers')); } return $this->repository->delete($option); diff --git a/app/Services/Services/Options/OptionUpdateService.php b/app/Services/Services/Options/OptionUpdateService.php index 6bfe634e..62bf5d39 100644 --- a/app/Services/Services/Options/OptionUpdateService.php +++ b/app/Services/Services/Options/OptionUpdateService.php @@ -68,7 +68,7 @@ class OptionUpdateService ]); if ($results !== 1) { - throw new NoParentConfigurationFoundException(trans('admin/exceptions.service.options.must_be_child')); + throw new NoParentConfigurationFoundException(trans('exceptions.service.options.must_be_child')); } } diff --git a/app/Services/Services/ServiceDeletionService.php b/app/Services/Services/ServiceDeletionService.php index 9a299bee..27e291ed 100644 --- a/app/Services/Services/ServiceDeletionService.php +++ b/app/Services/Services/ServiceDeletionService.php @@ -66,7 +66,7 @@ class ServiceDeletionService { $count = $this->serverRepository->findCountWhere([['service_id', '=', $service]]); if ($count > 0) { - throw new HasActiveServersException(trans('admin/exceptions.service.delete_has_servers')); + throw new HasActiveServersException(trans('exceptions.service.delete_has_servers')); } return $this->repository->delete($service); diff --git a/app/Services/Services/Variables/VariableUpdateService.php b/app/Services/Services/Variables/VariableUpdateService.php index 1806c11c..4792ad6b 100644 --- a/app/Services/Services/Variables/VariableUpdateService.php +++ b/app/Services/Services/Variables/VariableUpdateService.php @@ -66,7 +66,7 @@ class VariableUpdateService if (! is_null(array_get($data, 'env_variable'))) { if (in_array(strtoupper(array_get($data, 'env_variable')), explode(',', ServiceVariable::RESERVED_ENV_NAMES))) { - throw new ReservedVariableNameException(trans('admin/exceptions.service.variables.reserved_name', [ + throw new ReservedVariableNameException(trans('exceptions.service.variables.reserved_name', [ 'name' => array_get($data, 'env_variable'), ])); } @@ -78,7 +78,7 @@ class VariableUpdateService ]); if ($search > 0) { - throw new DisplayException(trans('admin/exceptions.service.variables.env_not_unique', [ + throw new DisplayException(trans('exceptions.service.variables.env_not_unique', [ 'name' => array_get($data, 'env_variable'), ])); } diff --git a/app/Services/Subusers/SubuserCreationService.php b/app/Services/Subusers/SubuserCreationService.php index 78d99afb..55cb6e90 100644 --- a/app/Services/Subusers/SubuserCreationService.php +++ b/app/Services/Subusers/SubuserCreationService.php @@ -131,12 +131,12 @@ class SubuserCreationService ]); } else { if ($server->owner_id === $user->id) { - throw new UserIsServerOwnerException(trans('admin/exceptions.subusers.user_is_owner')); + throw new UserIsServerOwnerException(trans('exceptions.subusers.user_is_owner')); } $subuserCount = $this->subuserRepository->findCountWhere([['user_id', '=', $user->id], ['server_id', '=', $server->id]]); if ($subuserCount !== 0) { - throw new ServerSubuserExistsException(trans('admin/exceptions.subusers.subuser_exists')); + throw new ServerSubuserExistsException(trans('exceptions.subusers.subuser_exists')); } } @@ -160,7 +160,7 @@ class SubuserCreationService $this->writer->warning($exception); $response = $exception->getResponse(); - throw new DisplayException(trans('admin/exceptions.daemon_connection_failed', [ + throw new DisplayException(trans('exceptions.daemon_connection_failed', [ 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), ])); } diff --git a/app/Services/Subusers/SubuserDeletionService.php b/app/Services/Subusers/SubuserDeletionService.php index 2cbc168b..97b723a5 100644 --- a/app/Services/Subusers/SubuserDeletionService.php +++ b/app/Services/Subusers/SubuserDeletionService.php @@ -100,7 +100,7 @@ class SubuserDeletionService $this->writer->warning($exception); $response = $exception->getResponse(); - throw new DisplayException(trans('admin/exceptions.daemon_connection_failed', [ + throw new DisplayException(trans('exceptions.daemon_connection_failed', [ 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), ])); } diff --git a/app/Services/Subusers/SubuserUpdateService.php b/app/Services/Subusers/SubuserUpdateService.php index 11faf5bb..c11c551e 100644 --- a/app/Services/Subusers/SubuserUpdateService.php +++ b/app/Services/Subusers/SubuserUpdateService.php @@ -117,7 +117,7 @@ class SubuserUpdateService $this->writer->warning($exception); $response = $exception->getResponse(); - throw new DisplayException(trans('admin/exceptions.daemon_connection_failed', [ + throw new DisplayException(trans('exceptions.daemon_connection_failed', [ 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), ])); } diff --git a/app/Traits/Controllers/ServerToJavascript.php b/app/Traits/Controllers/JavascriptInjection.php similarity index 98% rename from app/Traits/Controllers/ServerToJavascript.php rename to app/Traits/Controllers/JavascriptInjection.php index 6c550f58..5a8f0b33 100644 --- a/app/Traits/Controllers/ServerToJavascript.php +++ b/app/Traits/Controllers/JavascriptInjection.php @@ -26,7 +26,7 @@ namespace Pterodactyl\Traits\Controllers; use Javascript; -trait ServerToJavascript +trait JavascriptInjection { /** * @var \Illuminate\Contracts\Session\Session diff --git a/config/app.php b/config/app.php index af3cf02f..b0e36cbc 100644 --- a/config/app.php +++ b/config/app.php @@ -165,6 +165,7 @@ return [ Pterodactyl\Providers\MacroServiceProvider::class, Pterodactyl\Providers\PhraseAppTranslationProvider::class, Pterodactyl\Providers\RepositoryServiceProvider::class, + Pterodactyl\Providers\ViewComposerServiceProvider::class, /* * Additional Dependencies diff --git a/resources/lang/en/admin/exceptions.php b/resources/lang/en/exceptions.php similarity index 100% rename from resources/lang/en/admin/exceptions.php rename to resources/lang/en/exceptions.php diff --git a/resources/lang/en/server.php b/resources/lang/en/server.php index bb852965..07e8faa2 100644 --- a/resources/lang/en/server.php +++ b/resources/lang/en/server.php @@ -203,6 +203,11 @@ return [ ], ], 'files' => [ + 'exceptions' => [ + 'invalid_mime' => 'This type of file cannot be edited via the Panel\'s built-in editor.', + 'max_size' => 'This file is too large to edit via the Panel\'s built-in editor.', + 'list_directory' => 'An error was encountered while attempting to get the contents of this directory. Please try again.', + ], 'header' => 'File Manager', 'header_sub' => 'Manage all of your files directly from the web.', 'loading' => 'Loading initial file structure, this could take a few seconds.', diff --git a/routes/server.php b/routes/server.php index f8e68ee6..46724e52 100644 --- a/routes/server.php +++ b/routes/server.php @@ -51,17 +51,13 @@ Route::group(['prefix' => 'settings'], function () { | */ Route::group(['prefix' => 'files'], function () { - Route::get('/', 'ServerController@getFiles')->name('server.files.index'); - Route::get('/add', 'ServerController@getAddFile')->name('server.files.add'); - Route::get('/edit/{file}', 'ServerController@getEditFile') - ->name('server.files.edit') - ->where('file', '.*'); - Route::get('/download/{file}', 'ServerController@getDownloadFile') - ->name('server.files.edit') - ->where('file', '.*'); + Route::get('/', 'Files\FileActionsController@index')->name('server.files.index'); + Route::get('/add', 'Files\FileActionsController@create')->name('server.files.add'); + Route::get('/edit/{file}', 'Files\FileActionsController@update')->name('server.files.edit')->where('file', '.*'); + Route::get('/download/{file}', 'Files\DownloadController@index')->name('server.files.edit')->where('file', '.*'); - Route::post('/directory-list', 'AjaxController@postDirectoryList')->name('server.files.directory-list'); - Route::post('/save', 'AjaxController@postSaveFile')->name('server.files.save'); + Route::post('/directory-list', 'Files\RemoteRequestController@directory')->name('server.files.directory-list'); + Route::post('/save', 'Files\RemoteRequestController@store')->name('server.files.save'); }); /* diff --git a/tests/Assertions/ControllerAssertionsTrait.php b/tests/Assertions/ControllerAssertionsTrait.php index a3558838..420eba1b 100644 --- a/tests/Assertions/ControllerAssertionsTrait.php +++ b/tests/Assertions/ControllerAssertionsTrait.php @@ -160,11 +160,22 @@ trait ControllerAssertionsTrait * @param string $route * @param mixed $response */ - public function assertRouteRedirectEquals($route, $response) + public function assertRedirectRouteEquals($route, $response) { PHPUnit_Framework_Assert::assertEquals(route($route), $response->getTargetUrl()); } + /** + * Assert that a route redirect URL equals as passed URL. + * + * @param string $url + * @param mixed $response + */ + public function assertRedirectUrlEquals($url, $response) + { + PHPUnit_Framework_Assert::assertEquals($url, $response->getTargetUrl()); + } + /** * Assert that a response code equals a given code. * diff --git a/tests/Unit/Http/Controllers/Base/APIControllerTest.php b/tests/Unit/Http/Controllers/Base/APIControllerTest.php index cc2c1302..19f0e6dc 100644 --- a/tests/Unit/Http/Controllers/Base/APIControllerTest.php +++ b/tests/Unit/Http/Controllers/Base/APIControllerTest.php @@ -149,7 +149,7 @@ class APIControllerTest extends TestCase $response = $this->controller->store($this->request); $this->assertIsRedirectResponse($response); - $this->assertRouteRedirectEquals('account.api', $response); + $this->assertRedirectRouteEquals('account.api', $response); } /** diff --git a/tests/Unit/Http/Controllers/Base/AccountControllerTest.php b/tests/Unit/Http/Controllers/Base/AccountControllerTest.php index 7d8258b0..6f277ba2 100644 --- a/tests/Unit/Http/Controllers/Base/AccountControllerTest.php +++ b/tests/Unit/Http/Controllers/Base/AccountControllerTest.php @@ -95,7 +95,7 @@ class AccountControllerTest extends TestCase $response = $this->controller->update($this->request); $this->assertIsRedirectResponse($response); - $this->assertRouteRedirectEquals('account', $response); + $this->assertRedirectRouteEquals('account', $response); } /** @@ -112,7 +112,7 @@ class AccountControllerTest extends TestCase $response = $this->controller->update($this->request); $this->assertIsRedirectResponse($response); - $this->assertRouteRedirectEquals('account', $response); + $this->assertRedirectRouteEquals('account', $response); } /** @@ -131,6 +131,6 @@ class AccountControllerTest extends TestCase $response = $this->controller->update($this->request); $this->assertIsRedirectResponse($response); - $this->assertRouteRedirectEquals('account', $response); + $this->assertRedirectRouteEquals('account', $response); } } diff --git a/tests/Unit/Http/Controllers/Base/SecurityControllerTest.php b/tests/Unit/Http/Controllers/Base/SecurityControllerTest.php index 04186b39..359b626f 100644 --- a/tests/Unit/Http/Controllers/Base/SecurityControllerTest.php +++ b/tests/Unit/Http/Controllers/Base/SecurityControllerTest.php @@ -167,7 +167,7 @@ class SecurityControllerTest extends TestCase $response = $this->controller->disableTotp($this->request); $this->assertIsRedirectResponse($response); - $this->assertRouteRedirectEquals('account.security', $response); + $this->assertRedirectRouteEquals('account.security', $response); } /** @@ -186,7 +186,7 @@ class SecurityControllerTest extends TestCase $response = $this->controller->disableTotp($this->request); $this->assertIsRedirectResponse($response); - $this->assertRouteRedirectEquals('account.security', $response); + $this->assertRedirectRouteEquals('account.security', $response); } /** @@ -201,6 +201,6 @@ class SecurityControllerTest extends TestCase $response = $this->controller->revoke($this->request, 123); $this->assertIsRedirectResponse($response); - $this->assertRouteRedirectEquals('account.security', $response); + $this->assertRedirectRouteEquals('account.security', $response); } } diff --git a/tests/Unit/Http/Controllers/Server/ConsoleControllerTest.php b/tests/Unit/Http/Controllers/Server/ConsoleControllerTest.php index e56c4ea7..b89dbb5b 100644 --- a/tests/Unit/Http/Controllers/Server/ConsoleControllerTest.php +++ b/tests/Unit/Http/Controllers/Server/ConsoleControllerTest.php @@ -27,7 +27,6 @@ namespace Tests\Unit\Http\Controllers\Server; use Illuminate\Contracts\Session\Session; use Mockery as m; use Pterodactyl\Http\Controllers\Server\ConsoleController; -use Pterodactyl\Models\Node; use Pterodactyl\Models\Server; use Tests\Assertions\ControllerAssertionsTrait; use Tests\TestCase; @@ -73,10 +72,10 @@ class ConsoleControllerTest extends TestCase public function testAllControllers($function, $view) { $server = factory(Server::class)->make(); - $node = factory(Node::class)->make(); - $server->node = $node; - $this->session->shouldReceive('get')->with('server_data.model')->once()->andReturn($server); + if ($function === 'index') { + $this->session->shouldReceive('get')->with('server_data.model')->once()->andReturn($server); + } $this->config->shouldReceive('get')->with('pterodactyl.console.count')->once()->andReturn(100); $this->config->shouldReceive('get')->with('pterodactyl.console.frequency')->once()->andReturn(10); $this->controller->shouldReceive('injectJavascript')->once()->andReturnNull(); @@ -84,10 +83,6 @@ class ConsoleControllerTest extends TestCase $response = $this->controller->$function(); $this->assertIsViewResponse($response); $this->assertViewNameEquals($view, $response); - $this->assertViewHasKey('server', $response); - $this->assertViewHasKey('node', $response); - $this->assertViewKeyEquals('server', $server, $response); - $this->assertViewKeyEquals('node', $node, $response); } /** diff --git a/tests/Unit/Http/Controllers/Server/Files/DownloadControllerTest.php b/tests/Unit/Http/Controllers/Server/Files/DownloadControllerTest.php new file mode 100644 index 00000000..fcf066e1 --- /dev/null +++ b/tests/Unit/Http/Controllers/Server/Files/DownloadControllerTest.php @@ -0,0 +1,92 @@ +. + * + * 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\Http\Controllers\Server\Files; + +use Mockery as m; +use Illuminate\Cache\Repository; +use Illuminate\Contracts\Session\Session; +use phpmock\phpunit\PHPMock; +use Pterodactyl\Http\Controllers\Server\Files\DownloadController; +use Pterodactyl\Models\Node; +use Pterodactyl\Models\Server; +use Tests\Assertions\ControllerAssertionsTrait; +use Tests\TestCase; + +class DownloadControllerTest extends TestCase +{ + use ControllerAssertionsTrait, PHPMock; + + /** + * @var \Illuminate\Cache\Repository + */ + protected $cache; + + /** + * @var \Pterodactyl\Http\Controllers\Server\Files\DownloadController + */ + protected $controller; + + /** + * @var \Illuminate\Contracts\Session\Session + */ + protected $session; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->cache = m::mock(Repository::class); + $this->session = m::mock(Session::class); + + $this->controller = m::mock(DownloadController::class, [$this->cache, $this->session])->makePartial(); + } + + /** + * Test the download controller redirects correctly. + */ + public function testIndexController() + { + $server = factory(Server::class)->make(); + $node = factory(Node::class)->make(); + $server->node = $node; + + $this->session->shouldReceive('get')->with('server_data.model')->once()->andReturn($server); + $this->controller->shouldReceive('authorize')->with('download-files', $server)->once()->andReturnNull(); + $this->getFunctionMock('\\Pterodactyl\\Http\\Controllers\\Server\\Files', 'str_random') + ->expects($this->once())->willReturn('randomString'); + + $this->cache->shouldReceive('tags')->with(['Server:Downloads'])->once()->andReturnSelf() + ->shouldReceive('put')->with('randomString', ['server' => $server->uuid, 'path' => '/my/file.txt'], 5)->once()->andReturnNull(); + + $response = $this->controller->index('1234', '/my/file.txt'); + $this->assertIsRedirectResponse($response); + $this->assertRedirectUrlEquals(sprintf( + '%s://%s:%s/server/file/download/%s', $server->node->scheme, $server->node->fqdn, $server->node->daemonListen, 'randomString' + ), $response); + } +} diff --git a/tests/Unit/Http/Controllers/Server/Files/FileActionsControllerTest.php b/tests/Unit/Http/Controllers/Server/Files/FileActionsControllerTest.php new file mode 100644 index 00000000..edbc6e3f --- /dev/null +++ b/tests/Unit/Http/Controllers/Server/Files/FileActionsControllerTest.php @@ -0,0 +1,217 @@ +. + * + * 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\Http\Controllers\Server\Files; + +use GuzzleHttp\Exception\RequestException; +use Illuminate\Contracts\Session\Session; +use Illuminate\Http\Request; +use Illuminate\Log\Writer; +use Mockery as m; +use Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface; +use Pterodactyl\Exceptions\DisplayException; +use Pterodactyl\Http\Controllers\Server\Files\FileActionsController; +use Pterodactyl\Http\Requests\Server\UpdateFileContentsFormRequest; +use Pterodactyl\Models\Server; +use Tests\Assertions\ControllerAssertionsTrait; +use Tests\TestCase; + +class FileActionsControllerTest extends TestCase +{ + use ControllerAssertionsTrait; + + /** + * @var \Pterodactyl\Http\Controllers\Server\Files\FileActionsController + */ + protected $controller; + + /** + * @var \Pterodactyl\Http\Requests\Server\UpdateFileContentsFormRequest + */ + protected $fileContentsFormRequest; + + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface + */ + protected $fileRepository; + + /** + * @var \Illuminate\Http\Request + */ + protected $request; + + /** + * @var \Illuminate\Contracts\Session\Session + */ + protected $session; + + /** + * @var \Illuminate\Log\Writer + */ + protected $writer; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->fileContentsFormRequest = m::mock(UpdateFileContentsFormRequest::class); + $this->fileRepository = m::mock(FileRepositoryInterface::class); + $this->request = m::mock(Request::class); + $this->session = m::mock(Session::class); + $this->writer = m::mock(Writer::class); + + $this->controller = m::mock(FileActionsController::class, [ + $this->fileRepository, $this->session, $this->writer, + ])->makePartial(); + } + + /** + * Test the index view controller. + */ + public function testIndexController() + { + $server = factory(Server::class)->make(); + + $this->session->shouldReceive('get')->with('server_data.model')->once()->andReturn($server); + $this->controller->shouldReceive('authorize')->with('list-files', $server)->once()->andReturnNull(); + $this->request->shouldReceive('user->can')->andReturn(true); + $this->controller->shouldReceive('injectJavascript')->once()->andReturnNull(); + + $response = $this->controller->index($this->request); + $this->assertIsViewResponse($response); + $this->assertViewNameEquals('server.files.index', $response); + } + + /** + * Test the file creation view controller. + * + * @dataProvider directoryNameProvider + */ + public function testCreateController($directory, $expected) + { + $server = factory(Server::class)->make(); + + $this->session->shouldReceive('get')->with('server_data.model')->once()->andReturn($server); + $this->controller->shouldReceive('authorize')->with('create-files', $server)->once()->andReturnNull(); + $this->controller->shouldReceive('injectJavascript')->once()->andReturnNull(); + $this->request->shouldReceive('get')->with('dir')->andReturn($directory); + + $response = $this->controller->create($this->request); + $this->assertIsViewResponse($response); + $this->assertViewNameEquals('server.files.add', $response); + $this->assertViewHasKey('directory', $response); + $this->assertViewKeyEquals('directory', $expected, $response); + } + + /** + * Test the update controller. + * + * @dataProvider fileNameProvider + */ + public function testUpdateController($file, $expected) + { + $server = factory(Server::class)->make(); + + $this->session->shouldReceive('get')->with('server_data.model')->once()->andReturn($server); + $this->controller->shouldReceive('authorize')->with('edit-files', $server)->once()->andReturnNull(); + $this->session->shouldReceive('get')->with('server_data.token')->once()->andReturn($server->daemonSecret); + $this->fileRepository->shouldReceive('setNode')->with($server->node_id)->once()->andReturnSelf() + ->shouldReceive('setAccessServer')->with($server->uuid)->once()->andReturnSelf() + ->shouldReceive('setAccessToken')->with($server->daemonSecret)->once()->andReturnSelf() + ->shouldReceive('getContent')->with($file)->once()->andReturn('file contents'); + + $this->fileContentsFormRequest->shouldReceive('getStats')->withNoArgs()->twice()->andReturn(['stats']); + $this->controller->shouldReceive('injectJavascript')->with(['stat' => ['stats']])->once()->andReturnNull(); + + $response = $this->controller->update($this->fileContentsFormRequest, '1234', $file); + $this->assertIsViewResponse($response); + $this->assertViewNameEquals('server.files.edit', $response); + $this->assertViewHasKey('file', $response); + $this->assertViewHasKey('stat', $response); + $this->assertViewHasKey('contents', $response); + $this->assertViewHasKey('directory', $response); + $this->assertViewKeyEquals('file', $file, $response); + $this->assertViewKeyEquals('stat', ['stats'], $response); + $this->assertViewKeyEquals('contents', 'file contents', $response); + $this->assertViewKeyEquals('directory', $expected, $response); + } + + /** + * Test that an exception is handled correctly in the controller. + */ + public function testExceptionRenderedByUpdateController() + { + $server = factory(Server::class)->make(); + $exception = m::mock(RequestException::class); + + $this->session->shouldReceive('get')->with('server_data.model')->once()->andReturn($server); + $this->controller->shouldReceive('authorize')->with('edit-files', $server)->once()->andReturnNull(); + $this->fileRepository->shouldReceive('setNode')->with($server->node_id)->once()->andThrow($exception); + + $exception->shouldReceive('getResponse')->withNoArgs()->once()->andReturnNull(); + $this->writer->shouldReceive('warning')->with($exception)->once()->andReturnNull(); + + try { + $this->controller->update($this->fileContentsFormRequest, '1234', 'file.txt'); + } catch (DisplayException $exception) { + $this->assertEquals(trans('exceptions.daemon_connection_failed', ['code' => 'E_CONN_REFUSED']), $exception->getMessage()); + } + } + + /** + * Provides a list of directory names and the expected output from formatting. + * + * @return array + */ + public function directoryNameProvider() + { + return [ + [null, ''], + ['/', ''], + ['', ''], + ['my/directory', 'my/directory/'], + ['/my/directory/', 'my/directory/'], + ['/////my/directory////', 'my/directory/'], + ]; + } + + /** + * Provides a list of file names and the expected output from formatting. + * + * @return array + */ + public function fileNameProvider() + { + return [ + ['/my/file.txt', 'my/'], + ['my/file.txt', 'my/'], + ['file.txt', '/'], + ['/file.txt', '/'], + ['./file.txt', '/'], + ]; + } +} diff --git a/tests/Unit/Services/Allocations/AssignmentServiceTest.php b/tests/Unit/Services/Allocations/AssignmentServiceTest.php index f55fdc5f..25352e37 100644 --- a/tests/Unit/Services/Allocations/AssignmentServiceTest.php +++ b/tests/Unit/Services/Allocations/AssignmentServiceTest.php @@ -247,7 +247,7 @@ class AssignmentServiceTest extends TestCase $this->service->handle($this->node->id, $data); } catch (Exception $exception) { $this->assertInstanceOf(DisplayException::class, $exception); - $this->assertEquals(trans('admin/exceptions.allocations.cidr_out_of_range'), $exception->getMessage()); + $this->assertEquals(trans('exceptions.allocations.cidr_out_of_range'), $exception->getMessage()); } } @@ -271,7 +271,7 @@ class AssignmentServiceTest extends TestCase } $this->assertInstanceOf(DisplayException::class, $exception); - $this->assertEquals(trans('admin/exceptions.allocations.too_many_ports'), $exception->getMessage()); + $this->assertEquals(trans('exceptions.allocations.too_many_ports'), $exception->getMessage()); } } @@ -295,7 +295,7 @@ class AssignmentServiceTest extends TestCase } $this->assertInstanceOf(DisplayException::class, $exception); - $this->assertEquals(trans('admin/exceptions.allocations.invalid_mapping', ['port' => 'test123']), $exception->getMessage()); + $this->assertEquals(trans('exceptions.allocations.invalid_mapping', ['port' => 'test123']), $exception->getMessage()); } } diff --git a/tests/Unit/Services/Database/DatabaseHostServiceTest.php b/tests/Unit/Services/Database/DatabaseHostServiceTest.php index bbba1453..d65ab889 100644 --- a/tests/Unit/Services/Database/DatabaseHostServiceTest.php +++ b/tests/Unit/Services/Database/DatabaseHostServiceTest.php @@ -211,7 +211,7 @@ class DatabaseHostServiceTest extends TestCase try { $this->service->delete(1); } catch (DisplayException $exception) { - $this->assertEquals(trans('admin/exceptions.databases.delete_has_databases'), $exception->getMessage()); + $this->assertEquals(trans('exceptions.databases.delete_has_databases'), $exception->getMessage()); } } } diff --git a/tests/Unit/Services/Nodes/NodeDeletionServiceTest.php b/tests/Unit/Services/Nodes/NodeDeletionServiceTest.php index 5f92d40f..862bd3da 100644 --- a/tests/Unit/Services/Nodes/NodeDeletionServiceTest.php +++ b/tests/Unit/Services/Nodes/NodeDeletionServiceTest.php @@ -96,7 +96,7 @@ class NodeDeletionServiceTest extends TestCase { $this->serverRepository->shouldReceive('withColumns')->with('id')->once()->andReturnSelf() ->shouldReceive('findCountWhere')->with([['node_id', '=', 1]])->once()->andReturn(1); - $this->translator->shouldReceive('trans')->with('admin/exceptions.node.servers_attached')->once()->andReturnNull(); + $this->translator->shouldReceive('trans')->with('exceptions.node.servers_attached')->once()->andReturnNull(); $this->repository->shouldNotReceive('delete'); $this->service->handle(1); diff --git a/tests/Unit/Services/Nodes/NodeUpdateServiceTest.php b/tests/Unit/Services/Nodes/NodeUpdateServiceTest.php index 386e06fa..b5c1c486 100644 --- a/tests/Unit/Services/Nodes/NodeUpdateServiceTest.php +++ b/tests/Unit/Services/Nodes/NodeUpdateServiceTest.php @@ -157,7 +157,7 @@ class NodeUpdateServiceTest extends TestCase } catch (Exception $exception) { $this->assertInstanceOf(DisplayException::class, $exception); $this->assertEquals( - trans('admin/exceptions.node.daemon_off_config_updated', ['code' => 400]), + trans('exceptions.node.daemon_off_config_updated', ['code' => 400]), $exception->getMessage() ); } diff --git a/tests/Unit/Services/Packs/PackCreationServiceTest.php b/tests/Unit/Services/Packs/PackCreationServiceTest.php index 9dc634ba..46706d33 100644 --- a/tests/Unit/Services/Packs/PackCreationServiceTest.php +++ b/tests/Unit/Services/Packs/PackCreationServiceTest.php @@ -152,7 +152,7 @@ class PackCreationServiceTest extends TestCase $this->service->handle([], $this->file); } catch (Exception $exception) { $this->assertInstanceOf(InvalidFileUploadException::class, $exception); - $this->assertEquals(trans('admin/exceptions.packs.invalid_upload'), $exception->getMessage()); + $this->assertEquals(trans('exceptions.packs.invalid_upload'), $exception->getMessage()); } } @@ -169,7 +169,7 @@ class PackCreationServiceTest extends TestCase try { $this->service->handle([], $this->file); } catch (InvalidFileMimeTypeException $exception) { - $this->assertEquals(trans('admin/exceptions.packs.invalid_mime', [ + $this->assertEquals(trans('exceptions.packs.invalid_mime', [ 'type' => implode(', ', PackCreationService::VALID_UPLOAD_TYPES), ]), $exception->getMessage()); } diff --git a/tests/Unit/Services/Packs/PackDeletionServiceTest.php b/tests/Unit/Services/Packs/PackDeletionServiceTest.php index c6b2e4b3..b345f2ac 100644 --- a/tests/Unit/Services/Packs/PackDeletionServiceTest.php +++ b/tests/Unit/Services/Packs/PackDeletionServiceTest.php @@ -130,7 +130,7 @@ class PackDeletionServiceTest extends TestCase try { $this->service->handle($model); } catch (HasActiveServersException $exception) { - $this->assertEquals(trans('admin/exceptions.packs.delete_has_servers'), $exception->getMessage()); + $this->assertEquals(trans('exceptions.packs.delete_has_servers'), $exception->getMessage()); } } } diff --git a/tests/Unit/Services/Packs/PackUpdateServiceTest.php b/tests/Unit/Services/Packs/PackUpdateServiceTest.php index 48ae8d77..bd93d602 100644 --- a/tests/Unit/Services/Packs/PackUpdateServiceTest.php +++ b/tests/Unit/Services/Packs/PackUpdateServiceTest.php @@ -90,7 +90,7 @@ class PackUpdateServiceTest extends TestCase try { $this->service->handle($model, ['option_id' => 0]); } catch (HasActiveServersException $exception) { - $this->assertEquals(trans('admin/exceptions.packs.update_has_servers'), $exception->getMessage()); + $this->assertEquals(trans('exceptions.packs.update_has_servers'), $exception->getMessage()); } } diff --git a/tests/Unit/Services/Packs/TemplateUploadServiceTest.php b/tests/Unit/Services/Packs/TemplateUploadServiceTest.php index bb1a564f..b77818c4 100644 --- a/tests/Unit/Services/Packs/TemplateUploadServiceTest.php +++ b/tests/Unit/Services/Packs/TemplateUploadServiceTest.php @@ -128,7 +128,7 @@ class TemplateUploadServiceTest extends TestCase try { $this->service->handle(1, $this->file); } catch (InvalidFileUploadException $exception) { - $this->assertEquals(trans('admin/exceptions.packs.invalid_upload'), $exception->getMessage()); + $this->assertEquals(trans('exceptions.packs.invalid_upload'), $exception->getMessage()); } } @@ -145,7 +145,7 @@ class TemplateUploadServiceTest extends TestCase try { $this->service->handle(1, $this->file); } catch (InvalidFileMimeTypeException $exception) { - $this->assertEquals(trans('admin/exceptions.packs.invalid_mime', [ + $this->assertEquals(trans('exceptions.packs.invalid_mime', [ 'type' => implode(', ', TemplateUploadService::VALID_UPLOAD_TYPES), ]), $exception->getMessage()); } @@ -165,7 +165,7 @@ class TemplateUploadServiceTest extends TestCase try { $this->service->handle(1, $this->file); } catch (UnreadableZipArchiveException $exception) { - $this->assertEquals(trans('admin/exceptions.packs.unreadable'), $exception->getMessage()); + $this->assertEquals(trans('exceptions.packs.unreadable'), $exception->getMessage()); } } @@ -190,7 +190,7 @@ class TemplateUploadServiceTest extends TestCase try { $this->service->handle(1, $this->file); } catch (InvalidPackArchiveFormatException $exception) { - $this->assertEquals(trans('admin/exceptions.packs.invalid_archive_exception'), $exception->getMessage()); + $this->assertEquals(trans('exceptions.packs.invalid_archive_exception'), $exception->getMessage()); } } @@ -214,7 +214,7 @@ class TemplateUploadServiceTest extends TestCase try { $this->service->handle(1, $this->file); } catch (ZipExtractionException $exception) { - $this->assertEquals(trans('admin/exceptions.packs.zip_extraction'), $exception->getMessage()); + $this->assertEquals(trans('exceptions.packs.zip_extraction'), $exception->getMessage()); } } diff --git a/tests/Unit/Services/Services/Options/InstallScriptUpdateServiceTest.php b/tests/Unit/Services/Services/Options/InstallScriptUpdateServiceTest.php index 4bb12a46..1308ac4f 100644 --- a/tests/Unit/Services/Services/Options/InstallScriptUpdateServiceTest.php +++ b/tests/Unit/Services/Services/Options/InstallScriptUpdateServiceTest.php @@ -99,7 +99,7 @@ class InstallScriptUpdateServiceTest extends TestCase $this->service->handle($this->model, $this->data); } catch (Exception $exception) { $this->assertInstanceOf(InvalidCopyFromException::class, $exception); - $this->assertEquals(trans('admin/exceptions.service.options.invalid_copy_id'), $exception->getMessage()); + $this->assertEquals(trans('exceptions.service.options.invalid_copy_id'), $exception->getMessage()); } } diff --git a/tests/Unit/Services/Services/Options/OptionCreationServiceTest.php b/tests/Unit/Services/Services/Options/OptionCreationServiceTest.php index 1d864b57..02522312 100644 --- a/tests/Unit/Services/Services/Options/OptionCreationServiceTest.php +++ b/tests/Unit/Services/Services/Options/OptionCreationServiceTest.php @@ -116,7 +116,7 @@ class OptionCreationServiceTest extends TestCase $this->service->handle(['config_from' => 1]); } catch (Exception $exception) { $this->assertInstanceOf(NoParentConfigurationFoundException::class, $exception); - $this->assertEquals(trans('admin/exceptions.service.options.must_be_child'), $exception->getMessage()); + $this->assertEquals(trans('exceptions.service.options.must_be_child'), $exception->getMessage()); } } } diff --git a/tests/Unit/Services/Services/Options/OptionDeletionServiceTest.php b/tests/Unit/Services/Services/Options/OptionDeletionServiceTest.php index 381ac3a5..21b59efc 100644 --- a/tests/Unit/Services/Services/Options/OptionDeletionServiceTest.php +++ b/tests/Unit/Services/Services/Options/OptionDeletionServiceTest.php @@ -80,7 +80,7 @@ class OptionDeletionServiceTest extends TestCase $this->service->handle(1); } catch (\Exception $exception) { $this->assertInstanceOf(HasActiveServersException::class, $exception); - $this->assertEquals(trans('admin/exceptions.service.options.delete_has_servers'), $exception->getMessage()); + $this->assertEquals(trans('exceptions.service.options.delete_has_servers'), $exception->getMessage()); } } } diff --git a/tests/Unit/Services/Services/Options/OptionUpdateServiceTest.php b/tests/Unit/Services/Services/Options/OptionUpdateServiceTest.php index ecc5f76c..842075e9 100644 --- a/tests/Unit/Services/Services/Options/OptionUpdateServiceTest.php +++ b/tests/Unit/Services/Services/Options/OptionUpdateServiceTest.php @@ -103,7 +103,7 @@ class OptionUpdateServiceTest extends TestCase $this->service->handle($this->model, ['config_from' => 1]); } catch (Exception $exception) { $this->assertInstanceOf(NoParentConfigurationFoundException::class, $exception); - $this->assertEquals(trans('admin/exceptions.service.options.must_be_child'), $exception->getMessage()); + $this->assertEquals(trans('exceptions.service.options.must_be_child'), $exception->getMessage()); } } diff --git a/tests/Unit/Services/Services/ServiceDeletionServiceTest.php b/tests/Unit/Services/Services/ServiceDeletionServiceTest.php index f986de51..d875b949 100644 --- a/tests/Unit/Services/Services/ServiceDeletionServiceTest.php +++ b/tests/Unit/Services/Services/ServiceDeletionServiceTest.php @@ -86,7 +86,7 @@ class ServiceDeletionServiceTest extends TestCase $this->service->handle(1); } catch (Exception $exception) { $this->assertInstanceOf(HasActiveServersException::class, $exception); - $this->assertEquals(trans('admin/exceptions.service.delete_has_servers'), $exception->getMessage()); + $this->assertEquals(trans('exceptions.service.delete_has_servers'), $exception->getMessage()); } } diff --git a/tests/Unit/Services/Services/Variables/VariableUpdateServiceTest.php b/tests/Unit/Services/Services/Variables/VariableUpdateServiceTest.php index 905389ef..848ebdce 100644 --- a/tests/Unit/Services/Services/Variables/VariableUpdateServiceTest.php +++ b/tests/Unit/Services/Services/Variables/VariableUpdateServiceTest.php @@ -116,7 +116,7 @@ class VariableUpdateServiceTest extends TestCase $this->service->handle($this->model, ['env_variable' => 'TEST_VAR_123']); } catch (Exception $exception) { $this->assertInstanceOf(DisplayException::class, $exception); - $this->assertEquals(trans('admin/exceptions.service.variables.env_not_unique', [ + $this->assertEquals(trans('exceptions.service.variables.env_not_unique', [ 'name' => 'TEST_VAR_123', ]), $exception->getMessage()); } diff --git a/tests/Unit/Services/Subusers/SubuserCreationServiceTest.php b/tests/Unit/Services/Subusers/SubuserCreationServiceTest.php index 42bae698..ad29c866 100644 --- a/tests/Unit/Services/Subusers/SubuserCreationServiceTest.php +++ b/tests/Unit/Services/Subusers/SubuserCreationServiceTest.php @@ -213,7 +213,7 @@ class SubuserCreationServiceTest extends TestCase $this->service->handle($server, $user->email, []); } catch (DisplayException $exception) { $this->assertInstanceOf(UserIsServerOwnerException::class, $exception); - $this->assertEquals(trans('admin/exceptions.subusers.user_is_owner'), $exception->getMessage()); + $this->assertEquals(trans('exceptions.subusers.user_is_owner'), $exception->getMessage()); } } @@ -235,7 +235,7 @@ class SubuserCreationServiceTest extends TestCase $this->service->handle($server, $user->email, []); } catch (DisplayException $exception) { $this->assertInstanceOf(ServerSubuserExistsException::class, $exception); - $this->assertEquals(trans('admin/exceptions.subusers.subuser_exists'), $exception->getMessage()); + $this->assertEquals(trans('exceptions.subusers.subuser_exists'), $exception->getMessage()); } } } diff --git a/tests/Unit/Services/Subusers/SubuserDeletionServiceTest.php b/tests/Unit/Services/Subusers/SubuserDeletionServiceTest.php index 25a976e4..dc2d16ef 100644 --- a/tests/Unit/Services/Subusers/SubuserDeletionServiceTest.php +++ b/tests/Unit/Services/Subusers/SubuserDeletionServiceTest.php @@ -132,7 +132,7 @@ class SubuserDeletionServiceTest extends TestCase try { $this->service->handle($subuser->id); } catch (DisplayException $exception) { - $this->assertEquals(trans('admin/exceptions.daemon_connection_failed', ['code' => 'E_CONN_REFUSED']), $exception->getMessage()); + $this->assertEquals(trans('exceptions.daemon_connection_failed', ['code' => 'E_CONN_REFUSED']), $exception->getMessage()); } } } diff --git a/tests/Unit/Services/Subusers/SubuserUpdateServiceTest.php b/tests/Unit/Services/Subusers/SubuserUpdateServiceTest.php index c053d57e..521af109 100644 --- a/tests/Unit/Services/Subusers/SubuserUpdateServiceTest.php +++ b/tests/Unit/Services/Subusers/SubuserUpdateServiceTest.php @@ -152,7 +152,7 @@ class SubuserUpdateServiceTest extends TestCase try { $this->service->handle($subuser->id, []); } catch (DisplayException $exception) { - $this->assertEquals(trans('admin/exceptions.daemon_connection_failed', ['code' => 'E_CONN_REFUSED']), $exception->getMessage()); + $this->assertEquals(trans('exceptions.daemon_connection_failed', ['code' => 'E_CONN_REFUSED']), $exception->getMessage()); } } } From 8f14ee989dcbbaab28a200a2251b57d1f0f4bde8 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 3 Sep 2017 21:41:03 +0000 Subject: [PATCH 80/99] Apply fixes from StyleCI --- .../Server/Files/FileActionsController.php | 10 +++++----- .../Server/Files/RemoteRequestController.php | 10 +++++----- app/Http/Middleware/ServerAuthenticate.php | 6 +++--- .../Middleware/SubuserAccessAuthenticate.php | 6 +++--- .../Server/UpdateFileContentsFormRequest.php | 8 ++++---- .../Servers/ServerAccessHelperService.php | 4 ++-- .../Assertions/ControllerAssertionsTrait.php | 4 ++-- .../Controllers/Base/IndexControllerTest.php | 16 +++++++-------- .../Base/SecurityControllerTest.php | 18 ++++++++--------- .../Server/ConsoleControllerTest.php | 8 ++++---- .../Server/Files/DownloadControllerTest.php | 8 ++++---- .../Files/FileActionsControllerTest.php | 20 +++++++++---------- 12 files changed, 59 insertions(+), 59 deletions(-) diff --git a/app/Http/Controllers/Server/Files/FileActionsController.php b/app/Http/Controllers/Server/Files/FileActionsController.php index 902e713c..878332a1 100644 --- a/app/Http/Controllers/Server/Files/FileActionsController.php +++ b/app/Http/Controllers/Server/Files/FileActionsController.php @@ -24,15 +24,15 @@ namespace Pterodactyl\Http\Controllers\Server\Files; -use GuzzleHttp\Exception\RequestException; -use Illuminate\Contracts\Session\Session; -use Illuminate\Http\Request; use Illuminate\Log\Writer; -use Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface; +use Illuminate\Http\Request; +use Illuminate\Contracts\Session\Session; +use GuzzleHttp\Exception\RequestException; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Http\Controllers\Controller; -use Pterodactyl\Http\Requests\Server\UpdateFileContentsFormRequest; use Pterodactyl\Traits\Controllers\JavascriptInjection; +use Pterodactyl\Http\Requests\Server\UpdateFileContentsFormRequest; +use Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface; class FileActionsController extends Controller { diff --git a/app/Http/Controllers/Server/Files/RemoteRequestController.php b/app/Http/Controllers/Server/Files/RemoteRequestController.php index f5506fb1..d8e4a7ac 100644 --- a/app/Http/Controllers/Server/Files/RemoteRequestController.php +++ b/app/Http/Controllers/Server/Files/RemoteRequestController.php @@ -24,13 +24,13 @@ namespace Pterodactyl\Http\Controllers\Server\Files; -use GuzzleHttp\Exception\RequestException; -use Illuminate\Contracts\Config\Repository as ConfigRepository; -use Illuminate\Contracts\Session\Session; -use Illuminate\Http\Request; use Illuminate\Log\Writer; -use Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface; +use Illuminate\Http\Request; +use Illuminate\Contracts\Session\Session; +use GuzzleHttp\Exception\RequestException; use Pterodactyl\Http\Controllers\Controller; +use Illuminate\Contracts\Config\Repository as ConfigRepository; +use Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface; class RemoteRequestController extends Controller { diff --git a/app/Http/Middleware/ServerAuthenticate.php b/app/Http/Middleware/ServerAuthenticate.php index 83d35073..b5d3fd1c 100644 --- a/app/Http/Middleware/ServerAuthenticate.php +++ b/app/Http/Middleware/ServerAuthenticate.php @@ -25,12 +25,12 @@ namespace Pterodactyl\Http\Middleware; use Closure; -use Illuminate\Contracts\Session\Session; use Illuminate\Http\Request; -use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Models\Server; -use Illuminate\Contracts\Config\Repository as ConfigRepository; +use Illuminate\Contracts\Session\Session; use Illuminate\Auth\AuthenticationException; +use Illuminate\Contracts\Config\Repository as ConfigRepository; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; diff --git a/app/Http/Middleware/SubuserAccessAuthenticate.php b/app/Http/Middleware/SubuserAccessAuthenticate.php index ca52d7e2..b3b54fb0 100644 --- a/app/Http/Middleware/SubuserAccessAuthenticate.php +++ b/app/Http/Middleware/SubuserAccessAuthenticate.php @@ -25,11 +25,11 @@ namespace Pterodactyl\Http\Middleware; use Closure; -use Illuminate\Auth\AuthenticationException; -use Illuminate\Contracts\Session\Session; use Illuminate\Http\Request; -use Pterodactyl\Exceptions\Service\Server\UserNotLinkedToServerException; +use Illuminate\Contracts\Session\Session; +use Illuminate\Auth\AuthenticationException; use Pterodactyl\Services\Servers\ServerAccessHelperService; +use Pterodactyl\Exceptions\Service\Server\UserNotLinkedToServerException; class SubuserAccessAuthenticate { diff --git a/app/Http/Requests/Server/UpdateFileContentsFormRequest.php b/app/Http/Requests/Server/UpdateFileContentsFormRequest.php index cde7d8a0..1f469aa8 100644 --- a/app/Http/Requests/Server/UpdateFileContentsFormRequest.php +++ b/app/Http/Requests/Server/UpdateFileContentsFormRequest.php @@ -24,15 +24,15 @@ namespace Pterodactyl\Http\Requests\Server; +use Illuminate\Log\Writer; +use Illuminate\Contracts\Session\Session; use GuzzleHttp\Exception\RequestException; use Illuminate\Contracts\Config\Repository; -use Illuminate\Contracts\Session\Session; -use Illuminate\Log\Writer; use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Exceptions\Http\Server\FileSizeTooLargeException; -use Pterodactyl\Exceptions\Http\Server\FileTypeNotEditableException; use Pterodactyl\Http\Requests\FrontendUserFormRequest; +use Pterodactyl\Exceptions\Http\Server\FileSizeTooLargeException; use Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface; +use Pterodactyl\Exceptions\Http\Server\FileTypeNotEditableException; class UpdateFileContentsFormRequest extends FrontendUserFormRequest { diff --git a/app/Services/Servers/ServerAccessHelperService.php b/app/Services/Servers/ServerAccessHelperService.php index deabbc05..4618d323 100644 --- a/app/Services/Servers/ServerAccessHelperService.php +++ b/app/Services/Servers/ServerAccessHelperService.php @@ -24,13 +24,13 @@ namespace Pterodactyl\Services\Servers; -use Pterodactyl\Exceptions\Service\Server\UserNotLinkedToServerException; -use Pterodactyl\Models\Server; use Pterodactyl\Models\User; +use Pterodactyl\Models\Server; use Illuminate\Cache\Repository as CacheRepository; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; +use Pterodactyl\Exceptions\Service\Server\UserNotLinkedToServerException; class ServerAccessHelperService { diff --git a/tests/Assertions/ControllerAssertionsTrait.php b/tests/Assertions/ControllerAssertionsTrait.php index 420eba1b..cfd0a2fc 100644 --- a/tests/Assertions/ControllerAssertionsTrait.php +++ b/tests/Assertions/ControllerAssertionsTrait.php @@ -24,10 +24,10 @@ namespace Tests\Assertions; -use Illuminate\Http\JsonResponse; -use Illuminate\Http\Response; use Illuminate\View\View; +use Illuminate\Http\Response; use PHPUnit_Framework_Assert; +use Illuminate\Http\JsonResponse; use Illuminate\Http\RedirectResponse; trait ControllerAssertionsTrait diff --git a/tests/Unit/Http/Controllers/Base/IndexControllerTest.php b/tests/Unit/Http/Controllers/Base/IndexControllerTest.php index 529f7c69..65884d74 100644 --- a/tests/Unit/Http/Controllers/Base/IndexControllerTest.php +++ b/tests/Unit/Http/Controllers/Base/IndexControllerTest.php @@ -24,16 +24,16 @@ namespace Tests\Unit\Http\Controllers\Base; -use Illuminate\Http\Request; use Mockery as m; -use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; -use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; -use Pterodactyl\Http\Controllers\Base\IndexController; -use Pterodactyl\Models\Server; -use Pterodactyl\Models\User; -use Pterodactyl\Services\Servers\ServerAccessHelperService; -use Tests\Assertions\ControllerAssertionsTrait; use Tests\TestCase; +use Illuminate\Http\Request; +use Pterodactyl\Models\User; +use Pterodactyl\Models\Server; +use Tests\Assertions\ControllerAssertionsTrait; +use Pterodactyl\Http\Controllers\Base\IndexController; +use Pterodactyl\Services\Servers\ServerAccessHelperService; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; +use Pterodactyl\Contracts\Repository\Daemon\ServerRepositoryInterface as DaemonServerRepositoryInterface; class IndexControllerTest extends TestCase { diff --git a/tests/Unit/Http/Controllers/Base/SecurityControllerTest.php b/tests/Unit/Http/Controllers/Base/SecurityControllerTest.php index 359b626f..3c9c8b8a 100644 --- a/tests/Unit/Http/Controllers/Base/SecurityControllerTest.php +++ b/tests/Unit/Http/Controllers/Base/SecurityControllerTest.php @@ -24,19 +24,19 @@ namespace Tests\Unit\Http\Controllers\Base; -use Illuminate\Contracts\Config\Repository; -use Illuminate\Contracts\Session\Session; -use Illuminate\Http\Request; use Mockery as m; +use Tests\TestCase; +use Illuminate\Http\Request; +use Pterodactyl\Models\User; use Prologue\Alerts\AlertsMessageBag; +use Illuminate\Contracts\Session\Session; +use Illuminate\Contracts\Config\Repository; +use Tests\Assertions\ControllerAssertionsTrait; +use Pterodactyl\Services\Users\TwoFactorSetupService; +use Pterodactyl\Services\Users\ToggleTwoFactorService; +use Pterodactyl\Http\Controllers\Base\SecurityController; use Pterodactyl\Contracts\Repository\SessionRepositoryInterface; use Pterodactyl\Exceptions\Service\User\TwoFactorAuthenticationTokenInvalid; -use Pterodactyl\Http\Controllers\Base\SecurityController; -use Pterodactyl\Models\User; -use Pterodactyl\Services\Users\ToggleTwoFactorService; -use Pterodactyl\Services\Users\TwoFactorSetupService; -use Tests\Assertions\ControllerAssertionsTrait; -use Tests\TestCase; class SecurityControllerTest extends TestCase { diff --git a/tests/Unit/Http/Controllers/Server/ConsoleControllerTest.php b/tests/Unit/Http/Controllers/Server/ConsoleControllerTest.php index b89dbb5b..b4eaba39 100644 --- a/tests/Unit/Http/Controllers/Server/ConsoleControllerTest.php +++ b/tests/Unit/Http/Controllers/Server/ConsoleControllerTest.php @@ -24,13 +24,13 @@ namespace Tests\Unit\Http\Controllers\Server; -use Illuminate\Contracts\Session\Session; use Mockery as m; -use Pterodactyl\Http\Controllers\Server\ConsoleController; -use Pterodactyl\Models\Server; -use Tests\Assertions\ControllerAssertionsTrait; use Tests\TestCase; +use Pterodactyl\Models\Server; +use Illuminate\Contracts\Session\Session; use Illuminate\Contracts\Config\Repository; +use Tests\Assertions\ControllerAssertionsTrait; +use Pterodactyl\Http\Controllers\Server\ConsoleController; class ConsoleControllerTest extends TestCase { diff --git a/tests/Unit/Http/Controllers/Server/Files/DownloadControllerTest.php b/tests/Unit/Http/Controllers/Server/Files/DownloadControllerTest.php index fcf066e1..a0ecd941 100644 --- a/tests/Unit/Http/Controllers/Server/Files/DownloadControllerTest.php +++ b/tests/Unit/Http/Controllers/Server/Files/DownloadControllerTest.php @@ -25,14 +25,14 @@ namespace Tests\Unit\Http\Controllers\Server\Files; use Mockery as m; -use Illuminate\Cache\Repository; -use Illuminate\Contracts\Session\Session; +use Tests\TestCase; use phpmock\phpunit\PHPMock; -use Pterodactyl\Http\Controllers\Server\Files\DownloadController; use Pterodactyl\Models\Node; use Pterodactyl\Models\Server; +use Illuminate\Cache\Repository; +use Illuminate\Contracts\Session\Session; use Tests\Assertions\ControllerAssertionsTrait; -use Tests\TestCase; +use Pterodactyl\Http\Controllers\Server\Files\DownloadController; class DownloadControllerTest extends TestCase { diff --git a/tests/Unit/Http/Controllers/Server/Files/FileActionsControllerTest.php b/tests/Unit/Http/Controllers/Server/Files/FileActionsControllerTest.php index edbc6e3f..67bf357e 100644 --- a/tests/Unit/Http/Controllers/Server/Files/FileActionsControllerTest.php +++ b/tests/Unit/Http/Controllers/Server/Files/FileActionsControllerTest.php @@ -24,18 +24,18 @@ namespace Tests\Unit\Http\Controllers\Server\Files; -use GuzzleHttp\Exception\RequestException; -use Illuminate\Contracts\Session\Session; -use Illuminate\Http\Request; -use Illuminate\Log\Writer; use Mockery as m; -use Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface; -use Pterodactyl\Exceptions\DisplayException; -use Pterodactyl\Http\Controllers\Server\Files\FileActionsController; -use Pterodactyl\Http\Requests\Server\UpdateFileContentsFormRequest; -use Pterodactyl\Models\Server; -use Tests\Assertions\ControllerAssertionsTrait; use Tests\TestCase; +use Illuminate\Log\Writer; +use Illuminate\Http\Request; +use Pterodactyl\Models\Server; +use Illuminate\Contracts\Session\Session; +use GuzzleHttp\Exception\RequestException; +use Pterodactyl\Exceptions\DisplayException; +use Tests\Assertions\ControllerAssertionsTrait; +use Pterodactyl\Http\Requests\Server\UpdateFileContentsFormRequest; +use Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface; +use Pterodactyl\Http\Controllers\Server\Files\FileActionsController; class FileActionsControllerTest extends TestCase { From b12f6f11563dc8e00a6a568e1371bb3498e24062 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Mon, 4 Sep 2017 14:34:38 -0500 Subject: [PATCH 81/99] Tests for RemoteRequestController --- .../Server/Files/RemoteRequestController.php | 23 +-- resources/lang/en/server.php | 1 - .../Files/RemoteRequestControllerTest.php | 189 ++++++++++++++++++ 3 files changed, 197 insertions(+), 16 deletions(-) create mode 100644 tests/Unit/Http/Controllers/Server/Files/RemoteRequestControllerTest.php diff --git a/app/Http/Controllers/Server/Files/RemoteRequestController.php b/app/Http/Controllers/Server/Files/RemoteRequestController.php index f5506fb1..cdc29878 100644 --- a/app/Http/Controllers/Server/Files/RemoteRequestController.php +++ b/app/Http/Controllers/Server/Files/RemoteRequestController.php @@ -110,14 +110,11 @@ class RemoteRequestController extends Controller ->getDirectory($requestDirectory); } catch (RequestException $exception) { $this->writer->warning($exception); + $response = $exception->getResponse(); - if (! is_null($exception->getResponse())) { - return response()->json( - ['error' => $exception->getResponse()->getBody()], $exception->getResponse()->getStatusCode() - ); - } else { - return response()->json(['error' => trans('server.files.exceptions.list_directory')], 500); - } + return response()->json(['error' => trans('exceptions.daemon_connection_failed', [ + 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), + ])], 500); } return view('server.files.list', [ @@ -151,16 +148,12 @@ class RemoteRequestController extends Controller return response('', 204); } catch (RequestException $exception) { - $response = $exception->getResponse(); $this->writer->warning($exception); + $response = $exception->getResponse(); - if (! is_null($response)) { - return response()->json(['error' => $response->getBody()], $response->getStatusCode()); - } else { - return response()->json(['error' => trans('exceptions.daemon_connection_failed', [ - 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), - ])], 500); - } + return response()->json(['error' => trans('exceptions.daemon_connection_failed', [ + 'code' => is_null($response) ? 'E_CONN_REFUSED' : $response->getStatusCode(), + ])], 500); } } } diff --git a/resources/lang/en/server.php b/resources/lang/en/server.php index 07e8faa2..fb64d6f8 100644 --- a/resources/lang/en/server.php +++ b/resources/lang/en/server.php @@ -206,7 +206,6 @@ return [ 'exceptions' => [ 'invalid_mime' => 'This type of file cannot be edited via the Panel\'s built-in editor.', 'max_size' => 'This file is too large to edit via the Panel\'s built-in editor.', - 'list_directory' => 'An error was encountered while attempting to get the contents of this directory. Please try again.', ], 'header' => 'File Manager', 'header_sub' => 'Manage all of your files directly from the web.', diff --git a/tests/Unit/Http/Controllers/Server/Files/RemoteRequestControllerTest.php b/tests/Unit/Http/Controllers/Server/Files/RemoteRequestControllerTest.php new file mode 100644 index 00000000..29eecb31 --- /dev/null +++ b/tests/Unit/Http/Controllers/Server/Files/RemoteRequestControllerTest.php @@ -0,0 +1,189 @@ +. + * + * 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\Http\Controllers\Server\Files; + +use GuzzleHttp\Exception\RequestException; +use Illuminate\Contracts\Config\Repository; +use Illuminate\Contracts\Session\Session; +use Illuminate\Http\Request; +use Illuminate\Log\Writer; +use Mockery as m; +use Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface; +use Pterodactyl\Http\Controllers\Server\Files\RemoteRequestController; +use Pterodactyl\Models\Server; +use Tests\Assertions\ControllerAssertionsTrait; +use Tests\TestCase; + +class RemoteRequestControllerTest extends TestCase +{ + use ControllerAssertionsTrait; + + /** + * @var \Illuminate\Contracts\Config\Repository + */ + protected $config; + + /** + * @var \Pterodactyl\Http\Controllers\Server\Files\RemoteRequestController + */ + protected $controller; + + /** + * @var \Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface + */ + protected $fileRepository; + + /** + * @var \Illuminate\Http\Request + */ + protected $request; + + /** + * @var \Illuminate\Contracts\Session\Session + */ + protected $session; + + /** + * @var \Illuminate\Log\Writer + */ + protected $writer; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->config = m::mock(Repository::class); + $this->fileRepository = m::mock(FileRepositoryInterface::class); + $this->request = m::mock(Request::class); + $this->session = m::mock(Session::class); + $this->writer = m::mock(Writer::class); + + $this->controller = m::mock(RemoteRequestController::class, [ + $this->config, + $this->fileRepository, + $this->session, + $this->writer, + ])->makePartial(); + } + + /** + * Test the directory listing controller. + */ + public function testDirectoryController() + { + $server = factory(Server::class)->make(); + + $this->session->shouldReceive('get')->with('server_data.model')->once()->andReturn($server); + $this->controller->shouldReceive('authorize')->with('list-files', $server)->once()->andReturnNull(); + $this->request->shouldReceive('input')->with('directory', '/')->once()->andReturn('/'); + $this->session->shouldReceive('get')->with('server_data.token')->once()->andReturn($server->daemonSecret); + $this->fileRepository->shouldReceive('setNode')->with($server->node_id)->once()->andReturnSelf() + ->shouldReceive('setAccessServer')->with($server->uuid)->once()->andReturnSelf() + ->shouldReceive('setAccessToken')->with($server->daemonSecret)->once()->andReturnSelf() + ->shouldReceive('getDirectory')->with('/')->once()->andReturn(['folders' => 1, 'files' => 2]); + $this->config->shouldReceive('get')->with('pterodactyl.files.editable')->once()->andReturn([]); + + $response = $this->controller->directory($this->request); + $this->assertIsViewResponse($response); + $this->assertViewNameEquals('server.files.list', $response); + $this->assertViewHasKey('files', $response); + $this->assertViewHasKey('folders', $response); + $this->assertViewHasKey('editableMime', $response); + $this->assertViewHasKey('directory', $response); + $this->assertViewKeyEquals('files', 2, $response); + $this->assertViewKeyEquals('folders', 1, $response); + $this->assertViewKeyEquals('editableMime', [], $response); + $this->assertViewKeyEquals('directory.first', false, $response); + $this->assertViewKeyEquals('directory.header', '', $response); + } + + /** + * Test that the controller properly handles an exception thrown by the daemon conneciton. + */ + public function testExceptionThrownByDaemonConnectionIsHandledByDisplayController() + { + $server = factory(Server::class)->make(); + $exception = m::mock(RequestException::class); + + $this->session->shouldReceive('get')->with('server_data.model')->once()->andReturn($server); + $this->controller->shouldReceive('authorize')->with('list-files', $server)->once()->andReturnNull(); + $this->request->shouldReceive('input')->with('directory', '/')->once()->andReturn('/'); + $this->fileRepository->shouldReceive('setNode')->with($server->node_id)->once()->andThrow($exception); + + $this->writer->shouldReceive('warning')->with($exception)->once()->andReturnNull(); + $exception->shouldReceive('getResponse')->withNoArgs()->once()->andReturnNull(); + + $response = $this->controller->directory($this->request); + $this->assertIsJsonResponse($response); + $this->assertResponseJsonEquals(['error' => trans('exceptions.daemon_connection_failed', ['code' => 'E_CONN_REFUSED'])], $response); + $this->assertResponseCodeEquals(500, $response); + } + + /** + * Test the store controller. + */ + public function testStoreController() + { + $server = factory(Server::class)->make(); + + $this->session->shouldReceive('get')->with('server_data.model')->once()->andReturn($server); + $this->controller->shouldReceive('authorize')->with('save-files', $server)->once()->andReturnNull(); + $this->session->shouldReceive('get')->with('server_data.token')->once()->andReturn($server->daemonSecret); + $this->request->shouldReceive('input')->with('file')->once()->andReturn('file.txt'); + $this->request->shouldReceive('input')->with('contents')->once()->andReturn('file contents'); + $this->fileRepository->shouldReceive('setNode')->with($server->node_id)->once()->andReturnSelf() + ->shouldReceive('setAccessServer')->with($server->uuid)->once()->andReturnSelf() + ->shouldReceive('setAccessToken')->with($server->daemonSecret)->once()->andReturnSelf() + ->shouldReceive('putContent')->with('file.txt', 'file contents')->once()->andReturnNull(); + + $response = $this->controller->store($this->request, '1234'); + $this->assertIsResponse($response); + $this->assertResponseCodeEquals(204, $response); + } + + /** + * Test that the controller properly handles an exception thrown by the daemon conneciton. + */ + public function testExceptionThrownByDaemonConnectionIsHandledByStoreController() + { + $server = factory(Server::class)->make(); + $exception = m::mock(RequestException::class); + + $this->session->shouldReceive('get')->with('server_data.model')->once()->andReturn($server); + $this->controller->shouldReceive('authorize')->with('save-files', $server)->once()->andReturnNull(); + $this->fileRepository->shouldReceive('setNode')->with($server->node_id)->once()->andThrow($exception); + + $this->writer->shouldReceive('warning')->with($exception)->once()->andReturnNull(); + $exception->shouldReceive('getResponse')->withNoArgs()->once()->andReturnNull(); + + $response = $this->controller->store($this->request, '1234'); + $this->assertIsJsonResponse($response); + $this->assertResponseJsonEquals(['error' => trans('exceptions.daemon_connection_failed', ['code' => 'E_CONN_REFUSED'])], $response); + $this->assertResponseCodeEquals(500, $response); + } +} From 73d153cacb8ec8e2598341c40145acf0bae4e229 Mon Sep 17 00:00:00 2001 From: Jakob Schrettenbrunner Date: Mon, 4 Sep 2017 23:50:21 +0200 Subject: [PATCH 82/99] fix pterodactyl:user command --- app/Console/Commands/MakeUser.php | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/app/Console/Commands/MakeUser.php b/app/Console/Commands/MakeUser.php index a4c0d442..b7c7c78d 100644 --- a/app/Console/Commands/MakeUser.php +++ b/app/Console/Commands/MakeUser.php @@ -25,7 +25,8 @@ namespace Pterodactyl\Console\Commands; use Illuminate\Console\Command; -use Pterodactyl\Repositories\oldUserRepository; +use Pterodactyl\Repositories\UserRepository; +use Pterodactyl\Services\Users\UserCreationService; class MakeUser extends Command { @@ -49,12 +50,20 @@ class MakeUser extends Command */ protected $description = 'Create a user within the panel.'; + /** + * @var \Pterodactyl\Services\Users\UserCreationService + */ + protected $creationService; + /** * Create a new command instance. */ - public function __construct() + public function __construct( + UserCreationService $creationService + ) { parent::__construct(); + $this->creationService = $creationService; } /** @@ -78,8 +87,8 @@ class MakeUser extends Command $data['root_admin'] = is_null($this->option('admin')) ? $this->confirm('Is this user a root administrator?') : $this->option('admin'); try { - $user = new oldUserRepository; - $user->create($data); + + $this->creationService->handle($data); return $this->info('User successfully created.'); } catch (\Exception $ex) { From 56dbe0e4bff2d2bb48f83f47a424852d83508349 Mon Sep 17 00:00:00 2001 From: Jakob Schrettenbrunner Date: Mon, 4 Sep 2017 23:53:29 +0200 Subject: [PATCH 83/99] update adminlte to 2.4-rc change adminlte blue colors to material indigo 700 --- .../pterodactyl/vendor/adminlte/admin.min.css | 8 +++--- .../pterodactyl/vendor/adminlte/app.min.js | 25 ++++++++++--------- .../vendor/adminlte/colors/skin-blue.min.css | 2 +- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/public/themes/pterodactyl/vendor/adminlte/admin.min.css b/public/themes/pterodactyl/vendor/adminlte/admin.min.css index 20792ca5..c8ad6291 100755 --- a/public/themes/pterodactyl/vendor/adminlte/admin.min.css +++ b/public/themes/pterodactyl/vendor/adminlte/admin.min.css @@ -1,7 +1,7 @@ -@import url(https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700,300italic,400italic,600italic);/*! - * AdminLTE v2.3.8 +/*! + * AdminLTE v2.4.0 * Author: Almsaeed Studio - * Website: Almsaeed Studio + * Website: Almsaeed Studio * License: Open source - MIT * Please visit http://opensource.org/licenses/MIT for more information -!*/html,body{height:100%}.layout-boxed html,.layout-boxed body{height:100%}body{font-family:'Source Sans Pro','Helvetica Neue',Helvetica,Arial,sans-serif;font-weight:400;overflow-x:hidden;overflow-y:auto}.wrapper{height:100%;position:relative;overflow-x:hidden;overflow-y:auto}.wrapper:before,.wrapper:after{content:" ";display:table}.wrapper:after{clear:both}.layout-boxed .wrapper{max-width:1250px;margin:0 auto;min-height:100%;box-shadow:0 0 8px rgba(0,0,0,0.5);position:relative}.layout-boxed{background:url('../img/boxed-bg.jpg') repeat fixed}.content-wrapper,.right-side,.main-footer{-webkit-transition:-webkit-transform .3s ease-in-out,margin .3s ease-in-out;-moz-transition:-moz-transform .3s ease-in-out,margin .3s ease-in-out;-o-transition:-o-transform .3s ease-in-out,margin .3s ease-in-out;transition:transform .3s ease-in-out,margin .3s ease-in-out;margin-left:230px;z-index:820}.layout-top-nav .content-wrapper,.layout-top-nav .right-side,.layout-top-nav .main-footer{margin-left:0}@media (max-width:767px){.content-wrapper,.right-side,.main-footer{margin-left:0}}@media (min-width:768px){.sidebar-collapse .content-wrapper,.sidebar-collapse .right-side,.sidebar-collapse .main-footer{margin-left:0}}@media (max-width:767px){.sidebar-open .content-wrapper,.sidebar-open .right-side,.sidebar-open .main-footer{-webkit-transform:translate(230px, 0);-ms-transform:translate(230px, 0);-o-transform:translate(230px, 0);transform:translate(230px, 0)}}.content-wrapper,.right-side{min-height:100%;background-color:#ecf0f5;z-index:800}.main-footer{background:#fff;padding:15px;color:#444;border-top:1px solid #d2d6de}.fixed .main-header,.fixed .main-sidebar,.fixed .left-side{position:fixed}.fixed .main-header{top:0;right:0;left:0}.fixed .content-wrapper,.fixed .right-side{padding-top:50px}@media (max-width:767px){.fixed .content-wrapper,.fixed .right-side{padding-top:100px}}.fixed.layout-boxed .wrapper{max-width:100%}body.hold-transition .content-wrapper,body.hold-transition .right-side,body.hold-transition .main-footer,body.hold-transition .main-sidebar,body.hold-transition .left-side,body.hold-transition .main-header .navbar,body.hold-transition .main-header .logo{-webkit-transition:none;-o-transition:none;transition:none}.content{min-height:250px;padding:15px;margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:'Source Sans Pro',sans-serif}a{color:#3c8dbc}a:hover,a:active,a:focus{outline:none;text-decoration:none;color:#72afd2}.page-header{margin:10px 0 20px 0;font-size:22px}.page-header>small{color:#666;display:block;margin-top:5px}.main-header{position:relative;max-height:100px;z-index:1030}.main-header .navbar{-webkit-transition:margin-left .3s ease-in-out;-o-transition:margin-left .3s ease-in-out;transition:margin-left .3s ease-in-out;margin-bottom:0;margin-left:230px;border:none;min-height:50px;border-radius:0}.layout-top-nav .main-header .navbar{margin-left:0}.main-header #navbar-search-input.form-control{background:rgba(255,255,255,0.2);border-color:transparent}.main-header #navbar-search-input.form-control:focus,.main-header #navbar-search-input.form-control:active{border-color:rgba(0,0,0,0.1);background:rgba(255,255,255,0.9)}.main-header #navbar-search-input.form-control::-moz-placeholder{color:#ccc;opacity:1}.main-header #navbar-search-input.form-control:-ms-input-placeholder{color:#ccc}.main-header #navbar-search-input.form-control::-webkit-input-placeholder{color:#ccc}.main-header .navbar-custom-menu,.main-header .navbar-right{float:right}@media (max-width:991px){.main-header .navbar-custom-menu a,.main-header .navbar-right a{color:inherit;background:transparent}}@media (max-width:767px){.main-header .navbar-right{float:none}.navbar-collapse .main-header .navbar-right{margin:7.5px -15px}.main-header .navbar-right>li{color:inherit;border:0}}.main-header .sidebar-toggle{float:left;background-color:transparent;background-image:none;padding:15px 15px;font-family:fontAwesome}.main-header .sidebar-toggle:before{content:"\f0c9"}.main-header .sidebar-toggle:hover{color:#fff}.main-header .sidebar-toggle:focus,.main-header .sidebar-toggle:active{background:transparent}.main-header .sidebar-toggle .icon-bar{display:none}.main-header .navbar .nav>li.user>a>.fa,.main-header .navbar .nav>li.user>a>.glyphicon,.main-header .navbar .nav>li.user>a>.ion{margin-right:5px}.main-header .navbar .nav>li>a>.label{position:absolute;top:9px;right:7px;text-align:center;font-size:9px;padding:2px 3px;line-height:.9}.main-header .logo{-webkit-transition:width .3s ease-in-out;-o-transition:width .3s ease-in-out;transition:width .3s ease-in-out;display:block;float:left;height:50px;font-size:20px;line-height:50px;text-align:center;width:230px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;padding:0 15px;font-weight:300;overflow:hidden}.main-header .logo .logo-lg{display:block}.main-header .logo .logo-mini{display:none}.main-header .navbar-brand{color:#fff}.content-header{position:relative;padding:15px 15px 0 15px}.content-header>h1{margin:0;font-size:24px}.content-header>h1>small{font-size:15px;display:inline-block;padding-left:4px;font-weight:300}.content-header>.breadcrumb{float:right;background:transparent;margin-top:0;margin-bottom:0;font-size:12px;padding:7px 5px;position:absolute;top:15px;right:10px;border-radius:2px}.content-header>.breadcrumb>li>a{color:#444;text-decoration:none;display:inline-block}.content-header>.breadcrumb>li>a>.fa,.content-header>.breadcrumb>li>a>.glyphicon,.content-header>.breadcrumb>li>a>.ion{margin-right:5px}.content-header>.breadcrumb>li+li:before{content:'>\00a0'}@media (max-width:991px){.content-header>.breadcrumb{position:relative;margin-top:5px;top:0;right:0;float:none;background:#d2d6de;padding-left:10px}.content-header>.breadcrumb li:before{color:#97a0b3}}.navbar-toggle{color:#fff;border:0;margin:0;padding:15px 15px}@media (max-width:991px){.navbar-custom-menu .navbar-nav>li{float:left}.navbar-custom-menu .navbar-nav{margin:0;float:left}.navbar-custom-menu .navbar-nav>li>a{padding-top:15px;padding-bottom:15px;line-height:20px}}@media (max-width:767px){.main-header{position:relative}.main-header .logo,.main-header .navbar{width:100%;float:none}.main-header .navbar{margin:0}.main-header .navbar-custom-menu{float:right}}@media (max-width:991px){.navbar-collapse.pull-left{float:none !important}.navbar-collapse.pull-left+.navbar-custom-menu{display:block;position:absolute;top:0;right:40px}}.main-sidebar,.left-side{position:absolute;top:0;left:0;padding-top:50px;min-height:100%;width:230px;z-index:810;-webkit-transition:-webkit-transform .3s ease-in-out,width .3s ease-in-out;-moz-transition:-moz-transform .3s ease-in-out,width .3s ease-in-out;-o-transition:-o-transform .3s ease-in-out,width .3s ease-in-out;transition:transform .3s ease-in-out,width .3s ease-in-out}@media (max-width:767px){.main-sidebar,.left-side{padding-top:100px}}@media (max-width:767px){.main-sidebar,.left-side{-webkit-transform:translate(-230px, 0);-ms-transform:translate(-230px, 0);-o-transform:translate(-230px, 0);transform:translate(-230px, 0)}}@media (min-width:768px){.sidebar-collapse .main-sidebar,.sidebar-collapse .left-side{-webkit-transform:translate(-230px, 0);-ms-transform:translate(-230px, 0);-o-transform:translate(-230px, 0);transform:translate(-230px, 0)}}@media (max-width:767px){.sidebar-open .main-sidebar,.sidebar-open .left-side{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0)}}.sidebar{padding-bottom:10px}.sidebar-form input:focus{border-color:transparent}.user-panel{position:relative;width:100%;padding:10px;overflow:hidden}.user-panel:before,.user-panel:after{content:" ";display:table}.user-panel:after{clear:both}.user-panel>.image>img{width:100%;max-width:45px;height:auto}.user-panel>.info{padding:5px 5px 5px 15px;line-height:1;position:absolute;left:55px}.user-panel>.info>p{font-weight:600;margin-bottom:9px}.user-panel>.info>a{text-decoration:none;padding-right:5px;margin-top:3px;font-size:11px}.user-panel>.info>a>.fa,.user-panel>.info>a>.ion,.user-panel>.info>a>.glyphicon{margin-right:3px}.sidebar-menu{list-style:none;margin:0;padding:0}.sidebar-menu>li{position:relative;margin:0;padding:0}.sidebar-menu>li>a{padding:12px 5px 12px 15px;display:block}.sidebar-menu>li>a>.fa,.sidebar-menu>li>a>.glyphicon,.sidebar-menu>li>a>.ion{width:20px}.sidebar-menu>li .label,.sidebar-menu>li .badge{margin-right:5px}.sidebar-menu>li .badge{margin-top:3px}.sidebar-menu li.header{padding:10px 25px 10px 15px;font-size:12px}.sidebar-menu li>a>.fa-angle-left,.sidebar-menu li>a>.pull-right-container>.fa-angle-left{width:auto;height:auto;padding:0;margin-right:10px}.sidebar-menu li>a>.fa-angle-left{position:absolute;top:50%;right:10px;margin-top:-8px}.sidebar-menu li.active>a>.fa-angle-left,.sidebar-menu li.active>a>.pull-right-container>.fa-angle-left{-webkit-transform:rotate(-90deg);-ms-transform:rotate(-90deg);-o-transform:rotate(-90deg);transform:rotate(-90deg)}.sidebar-menu li.active>.treeview-menu{display:block}.sidebar-menu .treeview-menu{display:none;list-style:none;padding:0;margin:0;padding-left:5px}.sidebar-menu .treeview-menu .treeview-menu{padding-left:20px}.sidebar-menu .treeview-menu>li{margin:0}.sidebar-menu .treeview-menu>li>a{padding:5px 5px 5px 15px;display:block;font-size:14px}.sidebar-menu .treeview-menu>li>a>.fa,.sidebar-menu .treeview-menu>li>a>.glyphicon,.sidebar-menu .treeview-menu>li>a>.ion{width:20px}.sidebar-menu .treeview-menu>li>a>.pull-right-container>.fa-angle-left,.sidebar-menu .treeview-menu>li>a>.pull-right-container>.fa-angle-down,.sidebar-menu .treeview-menu>li>a>.fa-angle-left,.sidebar-menu .treeview-menu>li>a>.fa-angle-down{width:auto}@media (min-width:768px){.sidebar-mini.sidebar-collapse .content-wrapper,.sidebar-mini.sidebar-collapse .right-side,.sidebar-mini.sidebar-collapse .main-footer{margin-left:50px !important;z-index:840}.sidebar-mini.sidebar-collapse .main-sidebar{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0);width:50px !important;z-index:850}.sidebar-mini.sidebar-collapse .sidebar-menu>li{position:relative}.sidebar-mini.sidebar-collapse .sidebar-menu>li>a{margin-right:0}.sidebar-mini.sidebar-collapse .sidebar-menu>li>a>span{border-top-right-radius:4px}.sidebar-mini.sidebar-collapse .sidebar-menu>li:not(.treeview)>a>span{border-bottom-right-radius:4px}.sidebar-mini.sidebar-collapse .sidebar-menu>li>.treeview-menu{padding-top:5px;padding-bottom:5px;border-bottom-right-radius:4px}.sidebar-mini.sidebar-collapse .sidebar-menu>li:hover>a>span:not(.pull-right),.sidebar-mini.sidebar-collapse .sidebar-menu>li:hover>.treeview-menu{display:block !important;position:absolute;width:180px;left:50px}.sidebar-mini.sidebar-collapse .sidebar-menu>li:hover>a>span{top:0;margin-left:-3px;padding:12px 5px 12px 20px;background-color:inherit}.sidebar-mini.sidebar-collapse .sidebar-menu>li:hover>a>.pull-right-container{position:relative!important;float:right;width:auto!important;left:180px !important;top:-22px !important;z-index:900}.sidebar-mini.sidebar-collapse .sidebar-menu>li:hover>a>.pull-right-container>.label:not(:first-of-type){display:none}.sidebar-mini.sidebar-collapse .sidebar-menu>li:hover>.treeview-menu{top:44px;margin-left:0}.sidebar-mini.sidebar-collapse .main-sidebar .user-panel>.info,.sidebar-mini.sidebar-collapse .sidebar-form,.sidebar-mini.sidebar-collapse .sidebar-menu>li>a>span,.sidebar-mini.sidebar-collapse .sidebar-menu>li>.treeview-menu,.sidebar-mini.sidebar-collapse .sidebar-menu>li>a>.pull-right,.sidebar-mini.sidebar-collapse .sidebar-menu li.header{display:none !important;-webkit-transform:translateZ(0)}.sidebar-mini.sidebar-collapse .main-header .logo{width:50px}.sidebar-mini.sidebar-collapse .main-header .logo>.logo-mini{display:block;margin-left:-15px;margin-right:-15px;font-size:18px}.sidebar-mini.sidebar-collapse .main-header .logo>.logo-lg{display:none}.sidebar-mini.sidebar-collapse .main-header .navbar{margin-left:50px}}.sidebar-menu,.main-sidebar .user-panel,.sidebar-menu>li.header{white-space:nowrap;overflow:hidden}.sidebar-menu:hover{overflow:visible}.sidebar-form,.sidebar-menu>li.header{overflow:hidden;text-overflow:clip}.sidebar-menu li>a{position:relative}.sidebar-menu li>a>.pull-right-container{position:absolute;right:10px;top:50%;margin-top:-7px}.control-sidebar-bg{position:fixed;z-index:1000;bottom:0}.control-sidebar-bg,.control-sidebar{top:0;right:-230px;width:230px;-webkit-transition:right .3s ease-in-out;-o-transition:right .3s ease-in-out;transition:right .3s ease-in-out}.control-sidebar{position:absolute;padding-top:50px;z-index:1010}@media (max-width:768px){.control-sidebar{padding-top:100px}}.control-sidebar>.tab-content{padding:10px 15px}.control-sidebar.control-sidebar-open,.control-sidebar.control-sidebar-open+.control-sidebar-bg{right:0}.control-sidebar-open .control-sidebar-bg,.control-sidebar-open .control-sidebar{right:0}@media (min-width:768px){.control-sidebar-open .content-wrapper,.control-sidebar-open .right-side,.control-sidebar-open .main-footer{margin-right:230px}}.nav-tabs.control-sidebar-tabs>li:first-of-type>a,.nav-tabs.control-sidebar-tabs>li:first-of-type>a:hover,.nav-tabs.control-sidebar-tabs>li:first-of-type>a:focus{border-left-width:0}.nav-tabs.control-sidebar-tabs>li>a{border-radius:0}.nav-tabs.control-sidebar-tabs>li>a,.nav-tabs.control-sidebar-tabs>li>a:hover{border-top:none;border-right:none;border-left:1px solid transparent;border-bottom:1px solid transparent}.nav-tabs.control-sidebar-tabs>li>a .icon{font-size:16px}.nav-tabs.control-sidebar-tabs>li.active>a,.nav-tabs.control-sidebar-tabs>li.active>a:hover,.nav-tabs.control-sidebar-tabs>li.active>a:focus,.nav-tabs.control-sidebar-tabs>li.active>a:active{border-top:none;border-right:none;border-bottom:none}@media (max-width:768px){.nav-tabs.control-sidebar-tabs{display:table}.nav-tabs.control-sidebar-tabs>li{display:table-cell}}.control-sidebar-heading{font-weight:400;font-size:16px;padding:10px 0;margin-bottom:10px}.control-sidebar-subheading{display:block;font-weight:400;font-size:14px}.control-sidebar-menu{list-style:none;padding:0;margin:0 -15px}.control-sidebar-menu>li>a{display:block;padding:10px 15px}.control-sidebar-menu>li>a:before,.control-sidebar-menu>li>a:after{content:" ";display:table}.control-sidebar-menu>li>a:after{clear:both}.control-sidebar-menu>li>a>.control-sidebar-subheading{margin-top:0}.control-sidebar-menu .menu-icon{float:left;width:35px;height:35px;border-radius:50%;text-align:center;line-height:35px}.control-sidebar-menu .menu-info{margin-left:45px;margin-top:3px}.control-sidebar-menu .menu-info>.control-sidebar-subheading{margin:0}.control-sidebar-menu .menu-info>p{margin:0;font-size:11px}.control-sidebar-menu .progress{margin:0}.control-sidebar-dark{color:#b8c7ce}.control-sidebar-dark,.control-sidebar-dark+.control-sidebar-bg{background:#222d32}.control-sidebar-dark .nav-tabs.control-sidebar-tabs{border-bottom:#1c2529}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a{background:#181f23;color:#b8c7ce}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:hover,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:focus{border-left-color:#141a1d;border-bottom-color:#141a1d}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:hover,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:focus,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:active{background:#1c2529}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:hover{color:#fff}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a:hover,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a:focus,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a:active{background:#222d32;color:#fff}.control-sidebar-dark .control-sidebar-heading,.control-sidebar-dark .control-sidebar-subheading{color:#fff}.control-sidebar-dark .control-sidebar-menu>li>a:hover{background:#1e282c}.control-sidebar-dark .control-sidebar-menu>li>a .menu-info>p{color:#b8c7ce}.control-sidebar-light{color:#5e5e5e}.control-sidebar-light,.control-sidebar-light+.control-sidebar-bg{background:#f9fafc;border-left:1px solid #d2d6de}.control-sidebar-light .nav-tabs.control-sidebar-tabs{border-bottom:#d2d6de}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a{background:#e8ecf4;color:#444}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:hover,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:focus{border-left-color:#d2d6de;border-bottom-color:#d2d6de}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:hover,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:focus,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:active{background:#eff1f7}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a:hover,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a:focus,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a:active{background:#f9fafc;color:#111}.control-sidebar-light .control-sidebar-heading,.control-sidebar-light .control-sidebar-subheading{color:#111}.control-sidebar-light .control-sidebar-menu{margin-left:-14px}.control-sidebar-light .control-sidebar-menu>li>a:hover{background:#f4f4f5}.control-sidebar-light .control-sidebar-menu>li>a .menu-info>p{color:#5e5e5e}.dropdown-menu{box-shadow:none;border-color:#eee}.dropdown-menu>li>a{color:#777}.dropdown-menu>li>a>.glyphicon,.dropdown-menu>li>a>.fa,.dropdown-menu>li>a>.ion{margin-right:10px}.dropdown-menu>li>a:hover{background-color:#e1e3e9;color:#333}.dropdown-menu>.divider{background-color:#eee}.navbar-nav>.notifications-menu>.dropdown-menu,.navbar-nav>.messages-menu>.dropdown-menu,.navbar-nav>.tasks-menu>.dropdown-menu{width:280px;padding:0 0 0 0;margin:0;top:100%}.navbar-nav>.notifications-menu>.dropdown-menu>li,.navbar-nav>.messages-menu>.dropdown-menu>li,.navbar-nav>.tasks-menu>.dropdown-menu>li{position:relative}.navbar-nav>.notifications-menu>.dropdown-menu>li.header,.navbar-nav>.messages-menu>.dropdown-menu>li.header,.navbar-nav>.tasks-menu>.dropdown-menu>li.header{border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0;background-color:#ffffff;padding:7px 10px;border-bottom:1px solid #f4f4f4;color:#444444;font-size:14px}.navbar-nav>.notifications-menu>.dropdown-menu>li.footer>a,.navbar-nav>.messages-menu>.dropdown-menu>li.footer>a,.navbar-nav>.tasks-menu>.dropdown-menu>li.footer>a{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px;font-size:12px;background-color:#fff;padding:7px 10px;border-bottom:1px solid #eeeeee;color:#444 !important;text-align:center}@media (max-width:991px){.navbar-nav>.notifications-menu>.dropdown-menu>li.footer>a,.navbar-nav>.messages-menu>.dropdown-menu>li.footer>a,.navbar-nav>.tasks-menu>.dropdown-menu>li.footer>a{background:#fff !important;color:#444 !important}}.navbar-nav>.notifications-menu>.dropdown-menu>li.footer>a:hover,.navbar-nav>.messages-menu>.dropdown-menu>li.footer>a:hover,.navbar-nav>.tasks-menu>.dropdown-menu>li.footer>a:hover{text-decoration:none;font-weight:normal}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu,.navbar-nav>.messages-menu>.dropdown-menu>li .menu,.navbar-nav>.tasks-menu>.dropdown-menu>li .menu{max-height:200px;margin:0;padding:0;list-style:none;overflow-x:hidden}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a,.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a,.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a{display:block;white-space:nowrap;border-bottom:1px solid #f4f4f4}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a:hover,.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:hover,.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a:hover{background:#f4f4f4;text-decoration:none}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a{color:#444444;overflow:hidden;text-overflow:ellipsis;padding:10px}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a>.glyphicon,.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a>.fa,.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a>.ion{width:20px}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a{margin:0;padding:10px 10px}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>div>img{margin:auto 10px auto auto;width:40px;height:40px}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>h4{padding:0;margin:0 0 0 45px;color:#444444;font-size:15px;position:relative}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>h4>small{color:#999999;font-size:10px;position:absolute;top:0;right:0}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>p{margin:0 0 0 45px;font-size:12px;color:#888888}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:before,.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:after{content:" ";display:table}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:after{clear:both}.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a{padding:10px}.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a>h3{font-size:14px;padding:0;margin:0 0 10px 0;color:#666666}.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a>.progress{padding:0;margin:0}.navbar-nav>.user-menu>.dropdown-menu{border-top-right-radius:0;border-top-left-radius:0;padding:1px 0 0 0;border-top-width:0;width:280px}.navbar-nav>.user-menu>.dropdown-menu,.navbar-nav>.user-menu>.dropdown-menu>.user-body{border-bottom-right-radius:4px;border-bottom-left-radius:4px}.navbar-nav>.user-menu>.dropdown-menu>li.user-header{height:175px;padding:10px;text-align:center}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>img{z-index:5;height:90px;width:90px;border:3px solid;border-color:transparent;border-color:rgba(255,255,255,0.2)}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>p{z-index:5;color:#fff;color:rgba(255,255,255,0.8);font-size:17px;margin-top:10px}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>p>small{display:block;font-size:12px}.navbar-nav>.user-menu>.dropdown-menu>.user-body{padding:15px;border-bottom:1px solid #f4f4f4;border-top:1px solid #dddddd}.navbar-nav>.user-menu>.dropdown-menu>.user-body:before,.navbar-nav>.user-menu>.dropdown-menu>.user-body:after{content:" ";display:table}.navbar-nav>.user-menu>.dropdown-menu>.user-body:after{clear:both}.navbar-nav>.user-menu>.dropdown-menu>.user-body a{color:#444 !important}@media (max-width:991px){.navbar-nav>.user-menu>.dropdown-menu>.user-body a{background:#fff !important;color:#444 !important}}.navbar-nav>.user-menu>.dropdown-menu>.user-footer{background-color:#f9f9f9;padding:10px}.navbar-nav>.user-menu>.dropdown-menu>.user-footer:before,.navbar-nav>.user-menu>.dropdown-menu>.user-footer:after{content:" ";display:table}.navbar-nav>.user-menu>.dropdown-menu>.user-footer:after{clear:both}.navbar-nav>.user-menu>.dropdown-menu>.user-footer .btn-default{color:#666666}@media (max-width:991px){.navbar-nav>.user-menu>.dropdown-menu>.user-footer .btn-default:hover{background-color:#f9f9f9}}.navbar-nav>.user-menu .user-image{float:left;width:25px;height:25px;border-radius:50%;margin-right:10px;margin-top:-2px}@media (max-width:767px){.navbar-nav>.user-menu .user-image{float:none;margin-right:0;margin-top:-8px;line-height:10px}}.open:not(.dropup)>.animated-dropdown-menu{backface-visibility:visible !important;-webkit-animation:flipInX .7s both;-o-animation:flipInX .7s both;animation:flipInX .7s both}@keyframes flipInX{0%{transform:perspective(400px) rotate3d(1, 0, 0, 90deg);transition-timing-function:ease-in;opacity:0}40%{transform:perspective(400px) rotate3d(1, 0, 0, -20deg);transition-timing-function:ease-in}60%{transform:perspective(400px) rotate3d(1, 0, 0, 10deg);opacity:1}80%{transform:perspective(400px) rotate3d(1, 0, 0, -5deg)}100%{transform:perspective(400px)}}@-webkit-keyframes flipInX{0%{-webkit-transform:perspective(400px) rotate3d(1, 0, 0, 90deg);-webkit-transition-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotate3d(1, 0, 0, -20deg);-webkit-transition-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotate3d(1, 0, 0, 10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotate3d(1, 0, 0, -5deg)}100%{-webkit-transform:perspective(400px)}}.navbar-custom-menu>.navbar-nav>li{position:relative}.navbar-custom-menu>.navbar-nav>li>.dropdown-menu{position:absolute;right:0;left:auto}@media (max-width:991px){.navbar-custom-menu>.navbar-nav{float:right}.navbar-custom-menu>.navbar-nav>li{position:static}.navbar-custom-menu>.navbar-nav>li>.dropdown-menu{position:absolute;right:5%;left:auto;border:1px solid #ddd;background:#fff}}.form-control{border-radius:0;box-shadow:none;border-color:#d2d6de}.form-control:focus{border-color:#3c8dbc;box-shadow:none}.form-control::-moz-placeholder,.form-control:-ms-input-placeholder,.form-control::-webkit-input-placeholder{color:#bbb;opacity:1}.form-control:not(select){-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-group.has-success label{color:#00a65a}.form-group.has-success .form-control,.form-group.has-success .input-group-addon{border-color:#00a65a;box-shadow:none}.form-group.has-success .help-block{color:#00a65a}.form-group.has-warning label{color:#f39c12}.form-group.has-warning .form-control,.form-group.has-warning .input-group-addon{border-color:#f39c12;box-shadow:none}.form-group.has-warning .help-block{color:#f39c12}.form-group.has-error label{color:#dd4b39}.form-group.has-error .form-control,.form-group.has-error .input-group-addon{border-color:#dd4b39;box-shadow:none}.form-group.has-error .help-block{color:#dd4b39}.input-group .input-group-addon{border-radius:0;border-color:#d2d6de;background-color:#fff}.btn-group-vertical .btn.btn-flat:first-of-type,.btn-group-vertical .btn.btn-flat:last-of-type{border-radius:0}.icheck>label{padding-left:0}.form-control-feedback.fa{line-height:34px}.input-lg+.form-control-feedback.fa,.input-group-lg+.form-control-feedback.fa,.form-group-lg .form-control+.form-control-feedback.fa{line-height:46px}.input-sm+.form-control-feedback.fa,.input-group-sm+.form-control-feedback.fa,.form-group-sm .form-control+.form-control-feedback.fa{line-height:30px}.progress,.progress>.progress-bar{-webkit-box-shadow:none;box-shadow:none}.progress,.progress>.progress-bar,.progress .progress-bar,.progress>.progress-bar .progress-bar{border-radius:1px}.progress.sm,.progress-sm{height:10px}.progress.sm,.progress-sm,.progress.sm .progress-bar,.progress-sm .progress-bar{border-radius:1px}.progress.xs,.progress-xs{height:7px}.progress.xs,.progress-xs,.progress.xs .progress-bar,.progress-xs .progress-bar{border-radius:1px}.progress.xxs,.progress-xxs{height:3px}.progress.xxs,.progress-xxs,.progress.xxs .progress-bar,.progress-xxs .progress-bar{border-radius:1px}.progress.vertical{position:relative;width:30px;height:200px;display:inline-block;margin-right:10px}.progress.vertical>.progress-bar{width:100%;position:absolute;bottom:0}.progress.vertical.sm,.progress.vertical.progress-sm{width:20px}.progress.vertical.xs,.progress.vertical.progress-xs{width:10px}.progress.vertical.xxs,.progress.vertical.progress-xxs{width:3px}.progress-group .progress-text{font-weight:600}.progress-group .progress-number{float:right}.table tr>td .progress{margin:0}.progress-bar-light-blue,.progress-bar-primary{background-color:#3c8dbc}.progress-striped .progress-bar-light-blue,.progress-striped .progress-bar-primary{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-green,.progress-bar-success{background-color:#00a65a}.progress-striped .progress-bar-green,.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-aqua,.progress-bar-info{background-color:#00c0ef}.progress-striped .progress-bar-aqua,.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-yellow,.progress-bar-warning{background-color:#f39c12}.progress-striped .progress-bar-yellow,.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-red,.progress-bar-danger{background-color:#dd4b39}.progress-striped .progress-bar-red,.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.small-box{border-radius:2px;position:relative;display:block;margin-bottom:20px;box-shadow:0 1px 1px rgba(0,0,0,0.1)}.small-box>.inner{padding:10px}.small-box>.small-box-footer{position:relative;text-align:center;padding:3px 0;color:#fff;color:rgba(255,255,255,0.8);display:block;z-index:10;background:rgba(0,0,0,0.1);text-decoration:none}.small-box>.small-box-footer:hover{color:#fff;background:rgba(0,0,0,0.15)}.small-box h3{font-size:38px;font-weight:bold;margin:0 0 10px 0;white-space:nowrap;padding:0}.small-box p{font-size:15px}.small-box p>small{display:block;color:#f9f9f9;font-size:13px;margin-top:5px}.small-box h3,.small-box p{z-index:5}.small-box .icon{-webkit-transition:all .3s linear;-o-transition:all .3s linear;transition:all .3s linear;position:absolute;top:-10px;right:10px;z-index:0;font-size:90px;color:rgba(0,0,0,0.15)}.small-box:hover{text-decoration:none;color:#f9f9f9}.small-box:hover .icon{font-size:95px}@media (max-width:767px){.small-box{text-align:center}.small-box .icon{display:none}.small-box p{font-size:12px}}.box{position:relative;border-radius:3px;background:#ffffff;border-top:3px solid #d2d6de;margin-bottom:20px;width:100%;box-shadow:0 1px 1px rgba(0,0,0,0.1)}.box.box-primary{border-top-color:#3c8dbc}.box.box-info{border-top-color:#00c0ef}.box.box-danger{border-top-color:#dd4b39}.box.box-warning{border-top-color:#f39c12}.box.box-success{border-top-color:#00a65a}.box.box-default{border-top-color:#d2d6de}.box.collapsed-box .box-body,.box.collapsed-box .box-footer{display:none}.box .nav-stacked>li{border-bottom:1px solid #f4f4f4;margin:0}.box .nav-stacked>li:last-of-type{border-bottom:none}.box.height-control .box-body{max-height:300px;overflow:auto}.box .border-right{border-right:1px solid #f4f4f4}.box .border-left{border-left:1px solid #f4f4f4}.box.box-solid{border-top:0}.box.box-solid>.box-header .btn.btn-default{background:transparent}.box.box-solid>.box-header .btn:hover,.box.box-solid>.box-header a:hover{background:rgba(0,0,0,0.1)}.box.box-solid.box-default{border:1px solid #d2d6de}.box.box-solid.box-default>.box-header{color:#444;background:#d2d6de;background-color:#d2d6de}.box.box-solid.box-default>.box-header a,.box.box-solid.box-default>.box-header .btn{color:#444}.box.box-solid.box-primary{border:1px solid #3c8dbc}.box.box-solid.box-primary>.box-header{color:#fff;background:#3c8dbc;background-color:#3c8dbc}.box.box-solid.box-primary>.box-header a,.box.box-solid.box-primary>.box-header .btn{color:#fff}.box.box-solid.box-info{border:1px solid #00c0ef}.box.box-solid.box-info>.box-header{color:#fff;background:#00c0ef;background-color:#00c0ef}.box.box-solid.box-info>.box-header a,.box.box-solid.box-info>.box-header .btn{color:#fff}.box.box-solid.box-danger{border:1px solid #dd4b39}.box.box-solid.box-danger>.box-header{color:#fff;background:#dd4b39;background-color:#dd4b39}.box.box-solid.box-danger>.box-header a,.box.box-solid.box-danger>.box-header .btn{color:#fff}.box.box-solid.box-warning{border:1px solid #f39c12}.box.box-solid.box-warning>.box-header{color:#fff;background:#f39c12;background-color:#f39c12}.box.box-solid.box-warning>.box-header a,.box.box-solid.box-warning>.box-header .btn{color:#fff}.box.box-solid.box-success{border:1px solid #00a65a}.box.box-solid.box-success>.box-header{color:#fff;background:#00a65a;background-color:#00a65a}.box.box-solid.box-success>.box-header a,.box.box-solid.box-success>.box-header .btn{color:#fff}.box.box-solid>.box-header>.box-tools .btn{border:0;box-shadow:none}.box.box-solid[class*='bg']>.box-header{color:#fff}.box .box-group>.box{margin-bottom:5px}.box .knob-label{text-align:center;color:#333;font-weight:100;font-size:12px;margin-bottom:0.3em}.box>.overlay,.overlay-wrapper>.overlay,.box>.loading-img,.overlay-wrapper>.loading-img{position:absolute;top:0;left:0;width:100%;height:100%}.box .overlay,.overlay-wrapper .overlay{z-index:50;background:rgba(255,255,255,0.7);border-radius:3px}.box .overlay>.fa,.overlay-wrapper .overlay>.fa{position:absolute;top:50%;left:50%;margin-left:-15px;margin-top:-15px;color:#000;font-size:30px}.box .overlay.dark,.overlay-wrapper .overlay.dark{background:rgba(0,0,0,0.5)}.box-header:before,.box-body:before,.box-footer:before,.box-header:after,.box-body:after,.box-footer:after{content:" ";display:table}.box-header:after,.box-body:after,.box-footer:after{clear:both}.box-header{color:#444;display:block;padding:10px;position:relative}.box-header.with-border{border-bottom:1px solid #f4f4f4}.collapsed-box .box-header.with-border{border-bottom:none}.box-header>.fa,.box-header>.glyphicon,.box-header>.ion,.box-header .box-title{display:inline-block;font-size:18px;margin:0;line-height:1}.box-header>.fa,.box-header>.glyphicon,.box-header>.ion{margin-right:5px}.box-header>.box-tools{position:absolute;right:10px;top:5px}.box-header>.box-tools [data-toggle="tooltip"]{position:relative}.box-header>.box-tools.pull-right .dropdown-menu{right:0;left:auto}.box-header>.box-tools .dropdown-menu>li>a{color:#444!important}.btn-box-tool{padding:5px;font-size:12px;background:transparent;color:#97a0b3}.open .btn-box-tool,.btn-box-tool:hover{color:#606c84}.btn-box-tool.btn:active{box-shadow:none}.box-body{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px;padding:10px}.no-header .box-body{border-top-right-radius:3px;border-top-left-radius:3px}.box-body>.table{margin-bottom:0}.box-body .fc{margin-top:5px}.box-body .full-width-chart{margin:-19px}.box-body.no-padding .full-width-chart{margin:-9px}.box-body .box-pane{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:3px}.box-body .box-pane-right{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:3px;border-bottom-left-radius:0}.box-footer{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px;border-top:1px solid #f4f4f4;padding:10px;background-color:#fff}.chart-legend{margin:10px 0}@media (max-width:991px){.chart-legend>li{float:left;margin-right:10px}}.box-comments{background:#f7f7f7}.box-comments .box-comment{padding:8px 0;border-bottom:1px solid #eee}.box-comments .box-comment:before,.box-comments .box-comment:after{content:" ";display:table}.box-comments .box-comment:after{clear:both}.box-comments .box-comment:last-of-type{border-bottom:0}.box-comments .box-comment:first-of-type{padding-top:0}.box-comments .box-comment img{float:left}.box-comments .comment-text{margin-left:40px;color:#555}.box-comments .username{color:#444;display:block;font-weight:600}.box-comments .text-muted{font-weight:400;font-size:12px}.todo-list{margin:0;padding:0;list-style:none;overflow:auto}.todo-list>li{border-radius:2px;padding:10px;background:#f4f4f4;margin-bottom:2px;border-left:2px solid #e6e7e8;color:#444}.todo-list>li:last-of-type{margin-bottom:0}.todo-list>li>input[type='checkbox']{margin:0 10px 0 5px}.todo-list>li .text{display:inline-block;margin-left:5px;font-weight:600}.todo-list>li .label{margin-left:10px;font-size:9px}.todo-list>li .tools{display:none;float:right;color:#dd4b39}.todo-list>li .tools>.fa,.todo-list>li .tools>.glyphicon,.todo-list>li .tools>.ion{margin-right:5px;cursor:pointer}.todo-list>li:hover .tools{display:inline-block}.todo-list>li.done{color:#999}.todo-list>li.done .text{text-decoration:line-through;font-weight:500}.todo-list>li.done .label{background:#d2d6de !important}.todo-list .danger{border-left-color:#dd4b39}.todo-list .warning{border-left-color:#f39c12}.todo-list .info{border-left-color:#00c0ef}.todo-list .success{border-left-color:#00a65a}.todo-list .primary{border-left-color:#3c8dbc}.todo-list .handle{display:inline-block;cursor:move;margin:0 5px}.chat{padding:5px 20px 5px 10px}.chat .item{margin-bottom:10px}.chat .item:before,.chat .item:after{content:" ";display:table}.chat .item:after{clear:both}.chat .item>img{width:40px;height:40px;border:2px solid transparent;border-radius:50%}.chat .item>.online{border:2px solid #00a65a}.chat .item>.offline{border:2px solid #dd4b39}.chat .item>.message{margin-left:55px;margin-top:-40px}.chat .item>.message>.name{display:block;font-weight:600}.chat .item>.attachment{border-radius:3px;background:#f4f4f4;margin-left:65px;margin-right:15px;padding:10px}.chat .item>.attachment>h4{margin:0 0 5px 0;font-weight:600;font-size:14px}.chat .item>.attachment>p,.chat .item>.attachment>.filename{font-weight:600;font-size:13px;font-style:italic;margin:0}.chat .item>.attachment:before,.chat .item>.attachment:after{content:" ";display:table}.chat .item>.attachment:after{clear:both}.box-input{max-width:200px}.modal .panel-body{color:#444}.info-box{display:block;min-height:90px;background:#fff;width:100%;box-shadow:0 1px 1px rgba(0,0,0,0.1);border-radius:2px;margin-bottom:15px}.info-box small{font-size:14px}.info-box .progress{background:rgba(0,0,0,0.2);margin:5px -10px 5px -10px;height:2px}.info-box .progress,.info-box .progress .progress-bar{border-radius:0}.info-box .progress .progress-bar{background:#fff}.info-box-icon{border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px;display:block;float:left;height:90px;width:90px;text-align:center;font-size:45px;line-height:90px;background:rgba(0,0,0,0.2)}.info-box-icon>img{max-width:100%}.info-box-content{padding:5px 10px;margin-left:90px}.info-box-number{display:block;font-weight:bold;font-size:18px}.progress-description,.info-box-text{display:block;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.info-box-text{text-transform:uppercase}.info-box-more{display:block}.progress-description{margin:0}.timeline{position:relative;margin:0 0 30px 0;padding:0;list-style:none}.timeline:before{content:'';position:absolute;top:0;bottom:0;width:4px;background:#ddd;left:31px;margin:0;border-radius:2px}.timeline>li{position:relative;margin-right:10px;margin-bottom:15px}.timeline>li:before,.timeline>li:after{content:" ";display:table}.timeline>li:after{clear:both}.timeline>li>.timeline-item{-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.1);box-shadow:0 1px 1px rgba(0,0,0,0.1);border-radius:3px;margin-top:0;background:#fff;color:#444;margin-left:60px;margin-right:15px;padding:0;position:relative}.timeline>li>.timeline-item>.time{color:#999;float:right;padding:10px;font-size:12px}.timeline>li>.timeline-item>.timeline-header{margin:0;color:#555;border-bottom:1px solid #f4f4f4;padding:10px;font-size:16px;line-height:1.1}.timeline>li>.timeline-item>.timeline-header>a{font-weight:600}.timeline>li>.timeline-item>.timeline-body,.timeline>li>.timeline-item>.timeline-footer{padding:10px}.timeline>li>.fa,.timeline>li>.glyphicon,.timeline>li>.ion{width:30px;height:30px;font-size:15px;line-height:30px;position:absolute;color:#666;background:#d2d6de;border-radius:50%;text-align:center;left:18px;top:0}.timeline>.time-label>span{font-weight:600;padding:5px;display:inline-block;background-color:#fff;border-radius:4px}.timeline-inverse>li>.timeline-item{background:#f0f0f0;border:1px solid #ddd;-webkit-box-shadow:none;box-shadow:none}.timeline-inverse>li>.timeline-item>.timeline-header{border-bottom-color:#ddd}.btn{border-radius:3px;-webkit-box-shadow:none;box-shadow:none;border:1px solid transparent}.btn.uppercase{text-transform:uppercase}.btn.btn-flat{border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;border-width:1px}.btn:active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);-moz-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn:focus{outline:none}.btn.btn-file{position:relative;overflow:hidden}.btn.btn-file>input[type='file']{position:absolute;top:0;right:0;min-width:100%;min-height:100%;font-size:100px;text-align:right;opacity:0;filter:alpha(opacity=0);outline:none;background:white;cursor:inherit;display:block}.btn-default{background-color:#f4f4f4;color:#444;border-color:#ddd}.btn-default:hover,.btn-default:active,.btn-default.hover{background-color:#e7e7e7}.btn-primary{background-color:#3c8dbc;border-color:#367fa9}.btn-primary:hover,.btn-primary:active,.btn-primary.hover{background-color:#367fa9}.btn-success{background-color:#00a65a;border-color:#008d4c}.btn-success:hover,.btn-success:active,.btn-success.hover{background-color:#008d4c}.btn-info{background-color:#00c0ef;border-color:#00acd6}.btn-info:hover,.btn-info:active,.btn-info.hover{background-color:#00acd6}.btn-danger{background-color:#dd4b39;border-color:#d73925}.btn-danger:hover,.btn-danger:active,.btn-danger.hover{background-color:#d73925}.btn-warning{background-color:#f39c12;border-color:#e08e0b}.btn-warning:hover,.btn-warning:active,.btn-warning.hover{background-color:#e08e0b}.btn-outline{border:1px solid #fff;background:transparent;color:#fff}.btn-outline:hover,.btn-outline:focus,.btn-outline:active{color:rgba(255,255,255,0.7);border-color:rgba(255,255,255,0.7)}.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn[class*='bg-']:hover{-webkit-box-shadow:inset 0 0 100px rgba(0,0,0,0.2);box-shadow:inset 0 0 100px rgba(0,0,0,0.2)}.btn-app{border-radius:3px;position:relative;padding:15px 5px;margin:0 0 10px 10px;min-width:80px;height:60px;text-align:center;color:#666;border:1px solid #ddd;background-color:#f4f4f4;font-size:12px}.btn-app>.fa,.btn-app>.glyphicon,.btn-app>.ion{font-size:20px;display:block}.btn-app:hover{background:#f4f4f4;color:#444;border-color:#aaa}.btn-app:active,.btn-app:focus{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);-moz-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-app>.badge{position:absolute;top:-3px;right:-10px;font-size:10px;font-weight:400}.callout{border-radius:3px;margin:0 0 20px 0;padding:15px 30px 15px 15px;border-left:5px solid #eee}.callout a{color:#fff;text-decoration:underline}.callout a:hover{color:#eee}.callout h4{margin-top:0;font-weight:600}.callout p:last-child{margin-bottom:0}.callout code,.callout .highlight{background-color:#fff}.callout.callout-danger{border-color:#c23321}.callout.callout-warning{border-color:#c87f0a}.callout.callout-info{border-color:#0097bc}.callout.callout-success{border-color:#00733e}.alert{border-radius:3px}.alert h4{font-weight:600}.alert .icon{margin-right:10px}.alert .close{color:#000;opacity:.2;filter:alpha(opacity=20)}.alert .close:hover{opacity:.5;filter:alpha(opacity=50)}.alert a{color:#fff;text-decoration:underline}.alert-success{border-color:#008d4c}.alert-danger,.alert-error{border-color:#d73925}.alert-warning{border-color:#e08e0b}.alert-info{border-color:#00acd6}.nav>li>a:hover,.nav>li>a:active,.nav>li>a:focus{color:#444;background:#f7f7f7}.nav-pills>li>a{border-radius:0;border-top:3px solid transparent;color:#444}.nav-pills>li>a>.fa,.nav-pills>li>a>.glyphicon,.nav-pills>li>a>.ion{margin-right:5px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{border-top-color:#3c8dbc}.nav-pills>li.active>a{font-weight:600}.nav-stacked>li>a{border-radius:0;border-top:0;border-left:3px solid transparent;color:#444}.nav-stacked>li.active>a,.nav-stacked>li.active>a:hover{background:transparent;color:#444;border-top:0;border-left-color:#3c8dbc}.nav-stacked>li.header{border-bottom:1px solid #ddd;color:#777;margin-bottom:10px;padding:5px 10px;text-transform:uppercase}.nav-tabs-custom{margin-bottom:20px;background:#fff;box-shadow:0 1px 1px rgba(0,0,0,0.1);border-radius:3px}.nav-tabs-custom>.nav-tabs{margin:0;border-bottom-color:#f4f4f4;border-top-right-radius:3px;border-top-left-radius:3px}.nav-tabs-custom>.nav-tabs>li{border-top:3px solid transparent;margin-bottom:-2px;margin-right:5px}.nav-tabs-custom>.nav-tabs>li>a{color:#444;border-radius:0}.nav-tabs-custom>.nav-tabs>li>a.text-muted{color:#999}.nav-tabs-custom>.nav-tabs>li>a,.nav-tabs-custom>.nav-tabs>li>a:hover{background:transparent;margin:0}.nav-tabs-custom>.nav-tabs>li>a:hover{color:#999}.nav-tabs-custom>.nav-tabs>li:not(.active)>a:hover,.nav-tabs-custom>.nav-tabs>li:not(.active)>a:focus,.nav-tabs-custom>.nav-tabs>li:not(.active)>a:active{border-color:transparent}.nav-tabs-custom>.nav-tabs>li.active{border-top-color:#3c8dbc}.nav-tabs-custom>.nav-tabs>li.active>a,.nav-tabs-custom>.nav-tabs>li.active:hover>a{background-color:#fff;color:#444}.nav-tabs-custom>.nav-tabs>li.active>a{border-top-color:transparent;border-left-color:#f4f4f4;border-right-color:#f4f4f4}.nav-tabs-custom>.nav-tabs>li:first-of-type{margin-left:0}.nav-tabs-custom>.nav-tabs>li:first-of-type.active>a{border-left-color:transparent}.nav-tabs-custom>.nav-tabs.pull-right{float:none !important}.nav-tabs-custom>.nav-tabs.pull-right>li{float:right}.nav-tabs-custom>.nav-tabs.pull-right>li:first-of-type{margin-right:0}.nav-tabs-custom>.nav-tabs.pull-right>li:first-of-type>a{border-left-width:1px}.nav-tabs-custom>.nav-tabs.pull-right>li:first-of-type.active>a{border-left-color:#f4f4f4;border-right-color:transparent}.nav-tabs-custom>.nav-tabs>li.header{line-height:35px;padding:0 10px;font-size:20px;color:#444}.nav-tabs-custom>.nav-tabs>li.header>.fa,.nav-tabs-custom>.nav-tabs>li.header>.glyphicon,.nav-tabs-custom>.nav-tabs>li.header>.ion{margin-right:5px}.nav-tabs-custom>.tab-content{background:#fff;padding:10px;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.nav-tabs-custom .dropdown.open>a:active,.nav-tabs-custom .dropdown.open>a:focus{background:transparent;color:#999}.nav-tabs-custom.tab-primary>.nav-tabs>li.active{border-top-color:#3c8dbc}.nav-tabs-custom.tab-info>.nav-tabs>li.active{border-top-color:#00c0ef}.nav-tabs-custom.tab-danger>.nav-tabs>li.active{border-top-color:#dd4b39}.nav-tabs-custom.tab-warning>.nav-tabs>li.active{border-top-color:#f39c12}.nav-tabs-custom.tab-success>.nav-tabs>li.active{border-top-color:#00a65a}.nav-tabs-custom.tab-default>.nav-tabs>li.active{border-top-color:#d2d6de}.pagination>li>a{background:#fafafa;color:#666}.pagination.pagination-flat>li>a{border-radius:0 !important}.products-list{list-style:none;margin:0;padding:0}.products-list>.item{border-radius:3px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.1);box-shadow:0 1px 1px rgba(0,0,0,0.1);padding:10px 0;background:#fff}.products-list>.item:before,.products-list>.item:after{content:" ";display:table}.products-list>.item:after{clear:both}.products-list .product-img{float:left}.products-list .product-img img{width:50px;height:50px}.products-list .product-info{margin-left:60px}.products-list .product-title{font-weight:600}.products-list .product-description{display:block;color:#999;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.product-list-in-box>.item{-webkit-box-shadow:none;box-shadow:none;border-radius:0;border-bottom:1px solid #f4f4f4}.product-list-in-box>.item:last-of-type{border-bottom-width:0}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{border-top:1px solid #f4f4f4}.table>thead>tr>th{border-bottom:2px solid #f4f4f4}.table tr td .progress{margin-top:5px}.table-bordered{border:1px solid #f4f4f4}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #f4f4f4}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table.no-border,.table.no-border td,.table.no-border th{border:0}table.text-center,table.text-center td,table.text-center th{text-align:center}.table.align th{text-align:left}.table.align td{text-align:right}.label-default{background-color:#d2d6de;color:#444}.direct-chat .box-body{border-bottom-right-radius:0;border-bottom-left-radius:0;position:relative;overflow-x:hidden;padding:0}.direct-chat.chat-pane-open .direct-chat-contacts{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0)}.direct-chat-messages{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0);padding:10px;height:250px;overflow:auto}.direct-chat-msg,.direct-chat-text{display:block}.direct-chat-msg{margin-bottom:10px}.direct-chat-msg:before,.direct-chat-msg:after{content:" ";display:table}.direct-chat-msg:after{clear:both}.direct-chat-messages,.direct-chat-contacts{-webkit-transition:-webkit-transform .5s ease-in-out;-moz-transition:-moz-transform .5s ease-in-out;-o-transition:-o-transform .5s ease-in-out;transition:transform .5s ease-in-out}.direct-chat-text{border-radius:5px;position:relative;padding:5px 10px;background:#d2d6de;border:1px solid #d2d6de;margin:5px 0 0 50px;color:#444}.direct-chat-text:after,.direct-chat-text:before{position:absolute;right:100%;top:15px;border:solid transparent;border-right-color:#d2d6de;content:' ';height:0;width:0;pointer-events:none}.direct-chat-text:after{border-width:5px;margin-top:-5px}.direct-chat-text:before{border-width:6px;margin-top:-6px}.right .direct-chat-text{margin-right:50px;margin-left:0}.right .direct-chat-text:after,.right .direct-chat-text:before{right:auto;left:100%;border-right-color:transparent;border-left-color:#d2d6de}.direct-chat-img{border-radius:50%;float:left;width:40px;height:40px}.right .direct-chat-img{float:right}.direct-chat-info{display:block;margin-bottom:2px;font-size:12px}.direct-chat-name{font-weight:600}.direct-chat-timestamp{color:#999}.direct-chat-contacts-open .direct-chat-contacts{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0)}.direct-chat-contacts{-webkit-transform:translate(101%, 0);-ms-transform:translate(101%, 0);-o-transform:translate(101%, 0);transform:translate(101%, 0);position:absolute;top:0;bottom:0;height:250px;width:100%;background:#222d32;color:#fff;overflow:auto}.contacts-list>li{border-bottom:1px solid rgba(0,0,0,0.2);padding:10px;margin:0}.contacts-list>li:before,.contacts-list>li:after{content:" ";display:table}.contacts-list>li:after{clear:both}.contacts-list>li:last-of-type{border-bottom:none}.contacts-list-img{border-radius:50%;width:40px;float:left}.contacts-list-info{margin-left:45px;color:#fff}.contacts-list-name,.contacts-list-status{display:block}.contacts-list-name{font-weight:600}.contacts-list-status{font-size:12px}.contacts-list-date{color:#aaa;font-weight:normal}.contacts-list-msg{color:#999}.direct-chat-danger .right>.direct-chat-text{background:#dd4b39;border-color:#dd4b39;color:#fff}.direct-chat-danger .right>.direct-chat-text:after,.direct-chat-danger .right>.direct-chat-text:before{border-left-color:#dd4b39}.direct-chat-primary .right>.direct-chat-text{background:#3c8dbc;border-color:#3c8dbc;color:#fff}.direct-chat-primary .right>.direct-chat-text:after,.direct-chat-primary .right>.direct-chat-text:before{border-left-color:#3c8dbc}.direct-chat-warning .right>.direct-chat-text{background:#f39c12;border-color:#f39c12;color:#fff}.direct-chat-warning .right>.direct-chat-text:after,.direct-chat-warning .right>.direct-chat-text:before{border-left-color:#f39c12}.direct-chat-info .right>.direct-chat-text{background:#00c0ef;border-color:#00c0ef;color:#fff}.direct-chat-info .right>.direct-chat-text:after,.direct-chat-info .right>.direct-chat-text:before{border-left-color:#00c0ef}.direct-chat-success .right>.direct-chat-text{background:#00a65a;border-color:#00a65a;color:#fff}.direct-chat-success .right>.direct-chat-text:after,.direct-chat-success .right>.direct-chat-text:before{border-left-color:#00a65a}.users-list>li{width:25%;float:left;padding:10px;text-align:center}.users-list>li img{border-radius:50%;max-width:100%;height:auto}.users-list>li>a:hover,.users-list>li>a:hover .users-list-name{color:#999}.users-list-name,.users-list-date{display:block}.users-list-name{font-weight:600;color:#444;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.users-list-date{color:#999;font-size:12px}.carousel-control.left,.carousel-control.right{background-image:none}.carousel-control>.fa{font-size:40px;position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-20px}.modal{background:rgba(0,0,0,0.3)}.modal-content{border-radius:0;-webkit-box-shadow:0 2px 3px rgba(0,0,0,0.125);box-shadow:0 2px 3px rgba(0,0,0,0.125);border:0}@media (min-width:768px){.modal-content{-webkit-box-shadow:0 2px 3px rgba(0,0,0,0.125);box-shadow:0 2px 3px rgba(0,0,0,0.125)}}.modal-header{border-bottom-color:#f4f4f4}.modal-footer{border-top-color:#f4f4f4}.modal-primary .modal-header,.modal-primary .modal-footer{border-color:#307095}.modal-warning .modal-header,.modal-warning .modal-footer{border-color:#c87f0a}.modal-info .modal-header,.modal-info .modal-footer{border-color:#0097bc}.modal-success .modal-header,.modal-success .modal-footer{border-color:#00733e}.modal-danger .modal-header,.modal-danger .modal-footer{border-color:#c23321}.box-widget{border:none;position:relative}.widget-user .widget-user-header{padding:20px;height:120px;border-top-right-radius:3px;border-top-left-radius:3px}.widget-user .widget-user-username{margin-top:0;margin-bottom:5px;font-size:25px;font-weight:300;text-shadow:0 1px 1px rgba(0,0,0,0.2)}.widget-user .widget-user-desc{margin-top:0}.widget-user .widget-user-image{position:absolute;top:65px;left:50%;margin-left:-45px}.widget-user .widget-user-image>img{width:90px;height:auto;border:3px solid #fff}.widget-user .box-footer{padding-top:30px}.widget-user-2 .widget-user-header{padding:20px;border-top-right-radius:3px;border-top-left-radius:3px}.widget-user-2 .widget-user-username{margin-top:5px;margin-bottom:5px;font-size:25px;font-weight:300}.widget-user-2 .widget-user-desc{margin-top:0}.widget-user-2 .widget-user-username,.widget-user-2 .widget-user-desc{margin-left:75px}.widget-user-2 .widget-user-image>img{width:65px;height:auto;float:left}.mailbox-messages>.table{margin:0}.mailbox-controls{padding:5px}.mailbox-controls.with-border{border-bottom:1px solid #f4f4f4}.mailbox-read-info{border-bottom:1px solid #f4f4f4;padding:10px}.mailbox-read-info h3{font-size:20px;margin:0}.mailbox-read-info h5{margin:0;padding:5px 0 0 0}.mailbox-read-time{color:#999;font-size:13px}.mailbox-read-message{padding:10px}.mailbox-attachments li{float:left;width:200px;border:1px solid #eee;margin-bottom:10px;margin-right:10px}.mailbox-attachment-name{font-weight:bold;color:#666}.mailbox-attachment-icon,.mailbox-attachment-info,.mailbox-attachment-size{display:block}.mailbox-attachment-info{padding:10px;background:#f4f4f4}.mailbox-attachment-size{color:#999;font-size:12px}.mailbox-attachment-icon{text-align:center;font-size:65px;color:#666;padding:20px 10px}.mailbox-attachment-icon.has-img{padding:0}.mailbox-attachment-icon.has-img>img{max-width:100%;height:auto}.lockscreen{background:#d2d6de}.lockscreen-logo{font-size:35px;text-align:center;margin-bottom:25px;font-weight:300}.lockscreen-logo a{color:#444}.lockscreen-wrapper{max-width:400px;margin:0 auto;margin-top:10%}.lockscreen .lockscreen-name{text-align:center;font-weight:600}.lockscreen-item{border-radius:4px;padding:0;background:#fff;position:relative;margin:10px auto 30px auto;width:290px}.lockscreen-image{border-radius:50%;position:absolute;left:-10px;top:-25px;background:#fff;padding:5px;z-index:10}.lockscreen-image>img{border-radius:50%;width:70px;height:70px}.lockscreen-credentials{margin-left:70px}.lockscreen-credentials .form-control{border:0}.lockscreen-credentials .btn{background-color:#fff;border:0;padding:0 10px}.lockscreen-footer{margin-top:10px}.login-logo,.register-logo{font-size:35px;text-align:center;margin-bottom:25px;font-weight:300}.login-logo a,.register-logo a{color:#444}.login-page,.register-page{background:#d2d6de}.login-box,.register-box{width:360px;margin:7% auto}@media (max-width:768px){.login-box,.register-box{width:90%;margin-top:20px}}.login-box-body,.register-box-body{background:#fff;padding:20px;border-top:0;color:#666}.login-box-body .form-control-feedback,.register-box-body .form-control-feedback{color:#777}.login-box-msg,.register-box-msg{margin:0;text-align:center;padding:0 20px 20px 20px}.social-auth-links{margin:10px 0}.error-page{width:600px;margin:20px auto 0 auto}@media (max-width:991px){.error-page{width:100%}}.error-page>.headline{float:left;font-size:100px;font-weight:300}@media (max-width:991px){.error-page>.headline{float:none;text-align:center}}.error-page>.error-content{margin-left:190px;display:block}@media (max-width:991px){.error-page>.error-content{margin-left:0}}.error-page>.error-content>h3{font-weight:300;font-size:25px}@media (max-width:991px){.error-page>.error-content>h3{text-align:center}}.invoice{position:relative;background:#fff;border:1px solid #f4f4f4;padding:20px;margin:10px 25px}.invoice-title{margin-top:0}.profile-user-img{margin:0 auto;width:100px;padding:3px;border:3px solid #d2d6de}.profile-username{font-size:21px;margin-top:5px}.post{border-bottom:1px solid #d2d6de;margin-bottom:15px;padding-bottom:15px;color:#666}.post:last-of-type{border-bottom:0;margin-bottom:0;padding-bottom:0}.post .user-block{margin-bottom:15px}.btn-social{position:relative;padding-left:44px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.btn-social>:first-child{position:absolute;left:0;top:0;bottom:0;width:32px;line-height:34px;font-size:1.6em;text-align:center;border-right:1px solid rgba(0,0,0,0.2)}.btn-social.btn-lg{padding-left:61px}.btn-social.btn-lg>:first-child{line-height:45px;width:45px;font-size:1.8em}.btn-social.btn-sm{padding-left:38px}.btn-social.btn-sm>:first-child{line-height:28px;width:28px;font-size:1.4em}.btn-social.btn-xs{padding-left:30px}.btn-social.btn-xs>:first-child{line-height:20px;width:20px;font-size:1.2em}.btn-social-icon{position:relative;padding-left:44px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;height:34px;width:34px;padding:0}.btn-social-icon>:first-child{position:absolute;left:0;top:0;bottom:0;width:32px;line-height:34px;font-size:1.6em;text-align:center;border-right:1px solid rgba(0,0,0,0.2)}.btn-social-icon.btn-lg{padding-left:61px}.btn-social-icon.btn-lg>:first-child{line-height:45px;width:45px;font-size:1.8em}.btn-social-icon.btn-sm{padding-left:38px}.btn-social-icon.btn-sm>:first-child{line-height:28px;width:28px;font-size:1.4em}.btn-social-icon.btn-xs{padding-left:30px}.btn-social-icon.btn-xs>:first-child{line-height:20px;width:20px;font-size:1.2em}.btn-social-icon>:first-child{border:none;text-align:center;width:100%}.btn-social-icon.btn-lg{height:45px;width:45px;padding-left:0;padding-right:0}.btn-social-icon.btn-sm{height:30px;width:30px;padding-left:0;padding-right:0}.btn-social-icon.btn-xs{height:22px;width:22px;padding-left:0;padding-right:0}.btn-adn{color:#fff;background-color:#d87a68;border-color:rgba(0,0,0,0.2)}.btn-adn:focus,.btn-adn.focus{color:#fff;background-color:#ce563f;border-color:rgba(0,0,0,0.2)}.btn-adn:hover{color:#fff;background-color:#ce563f;border-color:rgba(0,0,0,0.2)}.btn-adn:active,.btn-adn.active,.open>.dropdown-toggle.btn-adn{color:#fff;background-color:#ce563f;border-color:rgba(0,0,0,0.2)}.btn-adn:active,.btn-adn.active,.open>.dropdown-toggle.btn-adn{background-image:none}.btn-adn .badge{color:#d87a68;background-color:#fff}.btn-bitbucket{color:#fff;background-color:#205081;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:focus,.btn-bitbucket.focus{color:#fff;background-color:#163758;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:hover{color:#fff;background-color:#163758;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:active,.btn-bitbucket.active,.open>.dropdown-toggle.btn-bitbucket{color:#fff;background-color:#163758;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:active,.btn-bitbucket.active,.open>.dropdown-toggle.btn-bitbucket{background-image:none}.btn-bitbucket .badge{color:#205081;background-color:#fff}.btn-dropbox{color:#fff;background-color:#1087dd;border-color:rgba(0,0,0,0.2)}.btn-dropbox:focus,.btn-dropbox.focus{color:#fff;background-color:#0d6aad;border-color:rgba(0,0,0,0.2)}.btn-dropbox:hover{color:#fff;background-color:#0d6aad;border-color:rgba(0,0,0,0.2)}.btn-dropbox:active,.btn-dropbox.active,.open>.dropdown-toggle.btn-dropbox{color:#fff;background-color:#0d6aad;border-color:rgba(0,0,0,0.2)}.btn-dropbox:active,.btn-dropbox.active,.open>.dropdown-toggle.btn-dropbox{background-image:none}.btn-dropbox .badge{color:#1087dd;background-color:#fff}.btn-facebook{color:#fff;background-color:#3b5998;border-color:rgba(0,0,0,0.2)}.btn-facebook:focus,.btn-facebook.focus{color:#fff;background-color:#2d4373;border-color:rgba(0,0,0,0.2)}.btn-facebook:hover{color:#fff;background-color:#2d4373;border-color:rgba(0,0,0,0.2)}.btn-facebook:active,.btn-facebook.active,.open>.dropdown-toggle.btn-facebook{color:#fff;background-color:#2d4373;border-color:rgba(0,0,0,0.2)}.btn-facebook:active,.btn-facebook.active,.open>.dropdown-toggle.btn-facebook{background-image:none}.btn-facebook .badge{color:#3b5998;background-color:#fff}.btn-flickr{color:#fff;background-color:#ff0084;border-color:rgba(0,0,0,0.2)}.btn-flickr:focus,.btn-flickr.focus{color:#fff;background-color:#cc006a;border-color:rgba(0,0,0,0.2)}.btn-flickr:hover{color:#fff;background-color:#cc006a;border-color:rgba(0,0,0,0.2)}.btn-flickr:active,.btn-flickr.active,.open>.dropdown-toggle.btn-flickr{color:#fff;background-color:#cc006a;border-color:rgba(0,0,0,0.2)}.btn-flickr:active,.btn-flickr.active,.open>.dropdown-toggle.btn-flickr{background-image:none}.btn-flickr .badge{color:#ff0084;background-color:#fff}.btn-foursquare{color:#fff;background-color:#f94877;border-color:rgba(0,0,0,0.2)}.btn-foursquare:focus,.btn-foursquare.focus{color:#fff;background-color:#f71752;border-color:rgba(0,0,0,0.2)}.btn-foursquare:hover{color:#fff;background-color:#f71752;border-color:rgba(0,0,0,0.2)}.btn-foursquare:active,.btn-foursquare.active,.open>.dropdown-toggle.btn-foursquare{color:#fff;background-color:#f71752;border-color:rgba(0,0,0,0.2)}.btn-foursquare:active,.btn-foursquare.active,.open>.dropdown-toggle.btn-foursquare{background-image:none}.btn-foursquare .badge{color:#f94877;background-color:#fff}.btn-github{color:#fff;background-color:#444;border-color:rgba(0,0,0,0.2)}.btn-github:focus,.btn-github.focus{color:#fff;background-color:#2b2b2b;border-color:rgba(0,0,0,0.2)}.btn-github:hover{color:#fff;background-color:#2b2b2b;border-color:rgba(0,0,0,0.2)}.btn-github:active,.btn-github.active,.open>.dropdown-toggle.btn-github{color:#fff;background-color:#2b2b2b;border-color:rgba(0,0,0,0.2)}.btn-github:active,.btn-github.active,.open>.dropdown-toggle.btn-github{background-image:none}.btn-github .badge{color:#444;background-color:#fff}.btn-google{color:#fff;background-color:#dd4b39;border-color:rgba(0,0,0,0.2)}.btn-google:focus,.btn-google.focus{color:#fff;background-color:#c23321;border-color:rgba(0,0,0,0.2)}.btn-google:hover{color:#fff;background-color:#c23321;border-color:rgba(0,0,0,0.2)}.btn-google:active,.btn-google.active,.open>.dropdown-toggle.btn-google{color:#fff;background-color:#c23321;border-color:rgba(0,0,0,0.2)}.btn-google:active,.btn-google.active,.open>.dropdown-toggle.btn-google{background-image:none}.btn-google .badge{color:#dd4b39;background-color:#fff}.btn-instagram{color:#fff;background-color:#3f729b;border-color:rgba(0,0,0,0.2)}.btn-instagram:focus,.btn-instagram.focus{color:#fff;background-color:#305777;border-color:rgba(0,0,0,0.2)}.btn-instagram:hover{color:#fff;background-color:#305777;border-color:rgba(0,0,0,0.2)}.btn-instagram:active,.btn-instagram.active,.open>.dropdown-toggle.btn-instagram{color:#fff;background-color:#305777;border-color:rgba(0,0,0,0.2)}.btn-instagram:active,.btn-instagram.active,.open>.dropdown-toggle.btn-instagram{background-image:none}.btn-instagram .badge{color:#3f729b;background-color:#fff}.btn-linkedin{color:#fff;background-color:#007bb6;border-color:rgba(0,0,0,0.2)}.btn-linkedin:focus,.btn-linkedin.focus{color:#fff;background-color:#005983;border-color:rgba(0,0,0,0.2)}.btn-linkedin:hover{color:#fff;background-color:#005983;border-color:rgba(0,0,0,0.2)}.btn-linkedin:active,.btn-linkedin.active,.open>.dropdown-toggle.btn-linkedin{color:#fff;background-color:#005983;border-color:rgba(0,0,0,0.2)}.btn-linkedin:active,.btn-linkedin.active,.open>.dropdown-toggle.btn-linkedin{background-image:none}.btn-linkedin .badge{color:#007bb6;background-color:#fff}.btn-microsoft{color:#fff;background-color:#2672ec;border-color:rgba(0,0,0,0.2)}.btn-microsoft:focus,.btn-microsoft.focus{color:#fff;background-color:#125acd;border-color:rgba(0,0,0,0.2)}.btn-microsoft:hover{color:#fff;background-color:#125acd;border-color:rgba(0,0,0,0.2)}.btn-microsoft:active,.btn-microsoft.active,.open>.dropdown-toggle.btn-microsoft{color:#fff;background-color:#125acd;border-color:rgba(0,0,0,0.2)}.btn-microsoft:active,.btn-microsoft.active,.open>.dropdown-toggle.btn-microsoft{background-image:none}.btn-microsoft .badge{color:#2672ec;background-color:#fff}.btn-openid{color:#fff;background-color:#f7931e;border-color:rgba(0,0,0,0.2)}.btn-openid:focus,.btn-openid.focus{color:#fff;background-color:#da7908;border-color:rgba(0,0,0,0.2)}.btn-openid:hover{color:#fff;background-color:#da7908;border-color:rgba(0,0,0,0.2)}.btn-openid:active,.btn-openid.active,.open>.dropdown-toggle.btn-openid{color:#fff;background-color:#da7908;border-color:rgba(0,0,0,0.2)}.btn-openid:active,.btn-openid.active,.open>.dropdown-toggle.btn-openid{background-image:none}.btn-openid .badge{color:#f7931e;background-color:#fff}.btn-pinterest{color:#fff;background-color:#cb2027;border-color:rgba(0,0,0,0.2)}.btn-pinterest:focus,.btn-pinterest.focus{color:#fff;background-color:#9f191f;border-color:rgba(0,0,0,0.2)}.btn-pinterest:hover{color:#fff;background-color:#9f191f;border-color:rgba(0,0,0,0.2)}.btn-pinterest:active,.btn-pinterest.active,.open>.dropdown-toggle.btn-pinterest{color:#fff;background-color:#9f191f;border-color:rgba(0,0,0,0.2)}.btn-pinterest:active,.btn-pinterest.active,.open>.dropdown-toggle.btn-pinterest{background-image:none}.btn-pinterest .badge{color:#cb2027;background-color:#fff}.btn-reddit{color:#000;background-color:#eff7ff;border-color:rgba(0,0,0,0.2)}.btn-reddit:focus,.btn-reddit.focus{color:#000;background-color:#bcddff;border-color:rgba(0,0,0,0.2)}.btn-reddit:hover{color:#000;background-color:#bcddff;border-color:rgba(0,0,0,0.2)}.btn-reddit:active,.btn-reddit.active,.open>.dropdown-toggle.btn-reddit{color:#000;background-color:#bcddff;border-color:rgba(0,0,0,0.2)}.btn-reddit:active,.btn-reddit.active,.open>.dropdown-toggle.btn-reddit{background-image:none}.btn-reddit .badge{color:#eff7ff;background-color:#000}.btn-soundcloud{color:#fff;background-color:#f50;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:focus,.btn-soundcloud.focus{color:#fff;background-color:#c40;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:hover{color:#fff;background-color:#c40;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:active,.btn-soundcloud.active,.open>.dropdown-toggle.btn-soundcloud{color:#fff;background-color:#c40;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:active,.btn-soundcloud.active,.open>.dropdown-toggle.btn-soundcloud{background-image:none}.btn-soundcloud .badge{color:#f50;background-color:#fff}.btn-tumblr{color:#fff;background-color:#2c4762;border-color:rgba(0,0,0,0.2)}.btn-tumblr:focus,.btn-tumblr.focus{color:#fff;background-color:#1c2d3f;border-color:rgba(0,0,0,0.2)}.btn-tumblr:hover{color:#fff;background-color:#1c2d3f;border-color:rgba(0,0,0,0.2)}.btn-tumblr:active,.btn-tumblr.active,.open>.dropdown-toggle.btn-tumblr{color:#fff;background-color:#1c2d3f;border-color:rgba(0,0,0,0.2)}.btn-tumblr:active,.btn-tumblr.active,.open>.dropdown-toggle.btn-tumblr{background-image:none}.btn-tumblr .badge{color:#2c4762;background-color:#fff}.btn-twitter{color:#fff;background-color:#55acee;border-color:rgba(0,0,0,0.2)}.btn-twitter:focus,.btn-twitter.focus{color:#fff;background-color:#2795e9;border-color:rgba(0,0,0,0.2)}.btn-twitter:hover{color:#fff;background-color:#2795e9;border-color:rgba(0,0,0,0.2)}.btn-twitter:active,.btn-twitter.active,.open>.dropdown-toggle.btn-twitter{color:#fff;background-color:#2795e9;border-color:rgba(0,0,0,0.2)}.btn-twitter:active,.btn-twitter.active,.open>.dropdown-toggle.btn-twitter{background-image:none}.btn-twitter .badge{color:#55acee;background-color:#fff}.btn-vimeo{color:#fff;background-color:#1ab7ea;border-color:rgba(0,0,0,0.2)}.btn-vimeo:focus,.btn-vimeo.focus{color:#fff;background-color:#1295bf;border-color:rgba(0,0,0,0.2)}.btn-vimeo:hover{color:#fff;background-color:#1295bf;border-color:rgba(0,0,0,0.2)}.btn-vimeo:active,.btn-vimeo.active,.open>.dropdown-toggle.btn-vimeo{color:#fff;background-color:#1295bf;border-color:rgba(0,0,0,0.2)}.btn-vimeo:active,.btn-vimeo.active,.open>.dropdown-toggle.btn-vimeo{background-image:none}.btn-vimeo .badge{color:#1ab7ea;background-color:#fff}.btn-vk{color:#fff;background-color:#587ea3;border-color:rgba(0,0,0,0.2)}.btn-vk:focus,.btn-vk.focus{color:#fff;background-color:#466482;border-color:rgba(0,0,0,0.2)}.btn-vk:hover{color:#fff;background-color:#466482;border-color:rgba(0,0,0,0.2)}.btn-vk:active,.btn-vk.active,.open>.dropdown-toggle.btn-vk{color:#fff;background-color:#466482;border-color:rgba(0,0,0,0.2)}.btn-vk:active,.btn-vk.active,.open>.dropdown-toggle.btn-vk{background-image:none}.btn-vk .badge{color:#587ea3;background-color:#fff}.btn-yahoo{color:#fff;background-color:#720e9e;border-color:rgba(0,0,0,0.2)}.btn-yahoo:focus,.btn-yahoo.focus{color:#fff;background-color:#500a6f;border-color:rgba(0,0,0,0.2)}.btn-yahoo:hover{color:#fff;background-color:#500a6f;border-color:rgba(0,0,0,0.2)}.btn-yahoo:active,.btn-yahoo.active,.open>.dropdown-toggle.btn-yahoo{color:#fff;background-color:#500a6f;border-color:rgba(0,0,0,0.2)}.btn-yahoo:active,.btn-yahoo.active,.open>.dropdown-toggle.btn-yahoo{background-image:none}.btn-yahoo .badge{color:#720e9e;background-color:#fff}.fc-button{background:#f4f4f4;background-image:none;color:#444;border-color:#ddd;border-bottom-color:#ddd}.fc-button:hover,.fc-button:active,.fc-button.hover{background-color:#e9e9e9}.fc-header-title h2{font-size:15px;line-height:1.6em;color:#666;margin-left:10px}.fc-header-right{padding-right:10px}.fc-header-left{padding-left:10px}.fc-widget-header{background:#fafafa}.fc-grid{width:100%;border:0}.fc-widget-header:first-of-type,.fc-widget-content:first-of-type{border-left:0;border-right:0}.fc-widget-header:last-of-type,.fc-widget-content:last-of-type{border-right:0}.fc-toolbar{padding:10px;margin:0}.fc-day-number{font-size:20px;font-weight:300;padding-right:10px}.fc-color-picker{list-style:none;margin:0;padding:0}.fc-color-picker>li{float:left;font-size:30px;margin-right:5px;line-height:30px}.fc-color-picker>li .fa{-webkit-transition:-webkit-transform linear .3s;-moz-transition:-moz-transform linear .3s;-o-transition:-o-transform linear .3s;transition:transform linear .3s}.fc-color-picker>li .fa:hover{-webkit-transform:rotate(30deg);-ms-transform:rotate(30deg);-o-transform:rotate(30deg);transform:rotate(30deg)}#add-new-event{-webkit-transition:all linear .3s;-o-transition:all linear .3s;transition:all linear .3s}.external-event{padding:5px 10px;font-weight:bold;margin-bottom:4px;box-shadow:0 1px 1px rgba(0,0,0,0.1);text-shadow:0 1px 1px rgba(0,0,0,0.1);border-radius:3px;cursor:move}.external-event:hover{box-shadow:inset 0 0 90px rgba(0,0,0,0.2)}.select2-container--default.select2-container--focus,.select2-selection.select2-container--focus,.select2-container--default:focus,.select2-selection:focus,.select2-container--default:active,.select2-selection:active{outline:none}.select2-container--default .select2-selection--single,.select2-selection .select2-selection--single{border:1px solid #d2d6de;border-radius:0;padding:6px 12px;height:34px}.select2-container--default.select2-container--open{border-color:#3c8dbc}.select2-dropdown{border:1px solid #d2d6de;border-radius:0}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#3c8dbc;color:white}.select2-results__option{padding:6px 12px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{padding-left:0;padding-right:0;height:auto;margin-top:-4px}.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered{padding-right:6px;padding-left:20px}.select2-container--default .select2-selection--single .select2-selection__arrow{height:28px;right:3px}.select2-container--default .select2-selection--single .select2-selection__arrow b{margin-top:0}.select2-dropdown .select2-search__field,.select2-search--inline .select2-search__field{border:1px solid #d2d6de}.select2-dropdown .select2-search__field:focus,.select2-search--inline .select2-search__field:focus{outline:none;border:1px solid #3c8dbc}.select2-container--default .select2-results__option[aria-disabled=true]{color:#999}.select2-container--default .select2-results__option[aria-selected=true]{background-color:#ddd}.select2-container--default .select2-results__option[aria-selected=true],.select2-container--default .select2-results__option[aria-selected=true]:hover{color:#444}.select2-container--default .select2-selection--multiple{border:1px solid #d2d6de;border-radius:0}.select2-container--default .select2-selection--multiple:focus{border-color:#3c8dbc}.select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#d2d6de}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#3c8dbc;border-color:#367fa9;padding:1px 10px;color:#fff}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{margin-right:5px;color:rgba(255,255,255,0.7)}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container .select2-selection--single .select2-selection__rendered{padding-right:10px}.pad{padding:10px}.margin{margin:10px}.margin-bottom{margin-bottom:20px}.margin-bottom-none{margin-bottom:0}.margin-r-5{margin-right:5px}.inline{display:inline}.description-block{display:block;margin:10px 0;text-align:center}.description-block.margin-bottom{margin-bottom:25px}.description-block>.description-header{margin:0;padding:0;font-weight:600;font-size:16px}.description-block>.description-text{text-transform:uppercase}.bg-red,.bg-yellow,.bg-aqua,.bg-blue,.bg-light-blue,.bg-green,.bg-navy,.bg-teal,.bg-olive,.bg-lime,.bg-orange,.bg-fuchsia,.bg-purple,.bg-maroon,.bg-black,.bg-red-active,.bg-yellow-active,.bg-aqua-active,.bg-blue-active,.bg-light-blue-active,.bg-green-active,.bg-navy-active,.bg-teal-active,.bg-olive-active,.bg-lime-active,.bg-orange-active,.bg-fuchsia-active,.bg-purple-active,.bg-maroon-active,.bg-black-active,.callout.callout-danger,.callout.callout-warning,.callout.callout-info,.callout.callout-success,.alert-success,.alert-danger,.alert-error,.alert-warning,.alert-info,.label-danger,.label-info,.label-warning,.label-primary,.label-success,.modal-primary .modal-body,.modal-primary .modal-header,.modal-primary .modal-footer,.modal-warning .modal-body,.modal-warning .modal-header,.modal-warning .modal-footer,.modal-info .modal-body,.modal-info .modal-header,.modal-info .modal-footer,.modal-success .modal-body,.modal-success .modal-header,.modal-success .modal-footer,.modal-danger .modal-body,.modal-danger .modal-header,.modal-danger .modal-footer{color:#fff !important}.bg-gray{color:#000;background-color:#d2d6de !important}.bg-gray-light{background-color:#f7f7f7}.bg-black{background-color:#111 !important}.bg-red,.callout.callout-danger,.alert-danger,.alert-error,.label-danger,.modal-danger .modal-body{background-color:#dd4b39 !important}.bg-yellow,.callout.callout-warning,.alert-warning,.label-warning,.modal-warning .modal-body{background-color:#f39c12 !important}.bg-aqua,.callout.callout-info,.alert-info,.label-info,.modal-info .modal-body{background-color:#00c0ef !important}.bg-blue{background-color:#0073b7 !important}.bg-light-blue,.label-primary,.modal-primary .modal-body{background-color:#3c8dbc !important}.bg-green,.callout.callout-success,.alert-success,.label-success,.modal-success .modal-body{background-color:#00a65a !important}.bg-navy{background-color:#001f3f !important}.bg-teal{background-color:#39cccc !important}.bg-olive{background-color:#3d9970 !important}.bg-lime{background-color:#01ff70 !important}.bg-orange{background-color:#ff851b !important}.bg-fuchsia{background-color:#f012be !important}.bg-purple{background-color:#605ca8 !important}.bg-maroon{background-color:#d81b60 !important}.bg-gray-active{color:#000;background-color:#b5bbc8 !important}.bg-black-active{background-color:#000 !important}.bg-red-active,.modal-danger .modal-header,.modal-danger .modal-footer{background-color:#d33724 !important}.bg-yellow-active,.modal-warning .modal-header,.modal-warning .modal-footer{background-color:#db8b0b !important}.bg-aqua-active,.modal-info .modal-header,.modal-info .modal-footer{background-color:#00a7d0 !important}.bg-blue-active{background-color:#005384 !important}.bg-light-blue-active,.modal-primary .modal-header,.modal-primary .modal-footer{background-color:#357ca5 !important}.bg-green-active,.modal-success .modal-header,.modal-success .modal-footer{background-color:#008d4c !important}.bg-navy-active{background-color:#001a35 !important}.bg-teal-active{background-color:#30bbbb !important}.bg-olive-active{background-color:#368763 !important}.bg-lime-active{background-color:#00e765 !important}.bg-orange-active{background-color:#ff7701 !important}.bg-fuchsia-active{background-color:#db0ead !important}.bg-purple-active{background-color:#555299 !important}.bg-maroon-active{background-color:#ca195a !important}[class^="bg-"].disabled{opacity:.65;filter:alpha(opacity=65)}.text-red{color:#dd4b39 !important}.text-yellow{color:#f39c12 !important}.text-aqua{color:#00c0ef !important}.text-blue{color:#0073b7 !important}.text-black{color:#111 !important}.text-light-blue{color:#3c8dbc !important}.text-green{color:#00a65a !important}.text-gray{color:#d2d6de !important}.text-navy{color:#001f3f !important}.text-teal{color:#39cccc !important}.text-olive{color:#3d9970 !important}.text-lime{color:#01ff70 !important}.text-orange{color:#ff851b !important}.text-fuchsia{color:#f012be !important}.text-purple{color:#605ca8 !important}.text-maroon{color:#d81b60 !important}.link-muted{color:#7a869d}.link-muted:hover,.link-muted:focus{color:#606c84}.link-black{color:#666}.link-black:hover,.link-black:focus{color:#999}.hide{display:none !important}.no-border{border:0 !important}.no-padding{padding:0 !important}.no-margin{margin:0 !important}.no-shadow{box-shadow:none !important}.list-unstyled,.chart-legend,.contacts-list,.users-list,.mailbox-attachments{list-style:none;margin:0;padding:0}.list-group-unbordered>.list-group-item{border-left:0;border-right:0;border-radius:0;padding-left:0;padding-right:0}.flat{border-radius:0 !important}.text-bold,.text-bold.table td,.text-bold.table th{font-weight:700}.text-sm{font-size:12px}.jqstooltip{padding:5px !important;width:auto !important;height:auto !important}.bg-teal-gradient{background:#39cccc !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #39cccc), color-stop(1, #7adddd)) !important;background:-ms-linear-gradient(bottom, #39cccc, #7adddd) !important;background:-moz-linear-gradient(center bottom, #39cccc 0, #7adddd 100%) !important;background:-o-linear-gradient(#7adddd, #39cccc) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#7adddd', endColorstr='#39cccc', GradientType=0) !important;color:#fff}.bg-light-blue-gradient{background:#3c8dbc !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #3c8dbc), color-stop(1, #67a8ce)) !important;background:-ms-linear-gradient(bottom, #3c8dbc, #67a8ce) !important;background:-moz-linear-gradient(center bottom, #3c8dbc 0, #67a8ce 100%) !important;background:-o-linear-gradient(#67a8ce, #3c8dbc) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#67a8ce', endColorstr='#3c8dbc', GradientType=0) !important;color:#fff}.bg-blue-gradient{background:#0073b7 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #0073b7), color-stop(1, #0089db)) !important;background:-ms-linear-gradient(bottom, #0073b7, #0089db) !important;background:-moz-linear-gradient(center bottom, #0073b7 0, #0089db 100%) !important;background:-o-linear-gradient(#0089db, #0073b7) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#0089db', endColorstr='#0073b7', GradientType=0) !important;color:#fff}.bg-aqua-gradient{background:#00c0ef !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #00c0ef), color-stop(1, #14d1ff)) !important;background:-ms-linear-gradient(bottom, #00c0ef, #14d1ff) !important;background:-moz-linear-gradient(center bottom, #00c0ef 0, #14d1ff 100%) !important;background:-o-linear-gradient(#14d1ff, #00c0ef) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#14d1ff', endColorstr='#00c0ef', GradientType=0) !important;color:#fff}.bg-yellow-gradient{background:#f39c12 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #f39c12), color-stop(1, #f7bc60)) !important;background:-ms-linear-gradient(bottom, #f39c12, #f7bc60) !important;background:-moz-linear-gradient(center bottom, #f39c12 0, #f7bc60 100%) !important;background:-o-linear-gradient(#f7bc60, #f39c12) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f7bc60', endColorstr='#f39c12', GradientType=0) !important;color:#fff}.bg-purple-gradient{background:#605ca8 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #605ca8), color-stop(1, #9491c4)) !important;background:-ms-linear-gradient(bottom, #605ca8, #9491c4) !important;background:-moz-linear-gradient(center bottom, #605ca8 0, #9491c4 100%) !important;background:-o-linear-gradient(#9491c4, #605ca8) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#9491c4', endColorstr='#605ca8', GradientType=0) !important;color:#fff}.bg-green-gradient{background:#00a65a !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #00a65a), color-stop(1, #00ca6d)) !important;background:-ms-linear-gradient(bottom, #00a65a, #00ca6d) !important;background:-moz-linear-gradient(center bottom, #00a65a 0, #00ca6d 100%) !important;background:-o-linear-gradient(#00ca6d, #00a65a) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ca6d', endColorstr='#00a65a', GradientType=0) !important;color:#fff}.bg-red-gradient{background:#dd4b39 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #dd4b39), color-stop(1, #e47365)) !important;background:-ms-linear-gradient(bottom, #dd4b39, #e47365) !important;background:-moz-linear-gradient(center bottom, #dd4b39 0, #e47365 100%) !important;background:-o-linear-gradient(#e47365, #dd4b39) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#e47365', endColorstr='#dd4b39', GradientType=0) !important;color:#fff}.bg-black-gradient{background:#111 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #111), color-stop(1, #2b2b2b)) !important;background:-ms-linear-gradient(bottom, #111, #2b2b2b) !important;background:-moz-linear-gradient(center bottom, #111 0, #2b2b2b 100%) !important;background:-o-linear-gradient(#2b2b2b, #111) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#2b2b2b', endColorstr='#111111', GradientType=0) !important;color:#fff}.bg-maroon-gradient{background:#d81b60 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #d81b60), color-stop(1, #e73f7c)) !important;background:-ms-linear-gradient(bottom, #d81b60, #e73f7c) !important;background:-moz-linear-gradient(center bottom, #d81b60 0, #e73f7c 100%) !important;background:-o-linear-gradient(#e73f7c, #d81b60) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#e73f7c', endColorstr='#d81b60', GradientType=0) !important;color:#fff}.description-block .description-icon{font-size:16px}.no-pad-top{padding-top:0}.position-static{position:static !important}.list-header{font-size:15px;padding:10px 4px;font-weight:bold;color:#666}.list-seperator{height:1px;background:#f4f4f4;margin:15px 0 9px 0}.list-link>a{padding:4px;color:#777}.list-link>a:hover{color:#222}.font-light{font-weight:300}.user-block:before,.user-block:after{content:" ";display:table}.user-block:after{clear:both}.user-block img{width:40px;height:40px;float:left}.user-block .username,.user-block .description,.user-block .comment{display:block;margin-left:50px}.user-block .username{font-size:16px;font-weight:600}.user-block .description{color:#999;font-size:13px}.user-block.user-block-sm .username,.user-block.user-block-sm .description,.user-block.user-block-sm .comment{margin-left:40px}.user-block.user-block-sm .username{font-size:14px}.img-sm,.img-md,.img-lg,.box-comments .box-comment img,.user-block.user-block-sm img{float:left}.img-sm,.box-comments .box-comment img,.user-block.user-block-sm img{width:30px !important;height:30px !important}.img-sm+.img-push{margin-left:40px}.img-md{width:60px;height:60px}.img-md+.img-push{margin-left:70px}.img-lg{width:100px;height:100px}.img-lg+.img-push{margin-left:110px}.img-bordered{border:3px solid #d2d6de;padding:3px}.img-bordered-sm{border:2px solid #d2d6de;padding:2px}.attachment-block{border:1px solid #f4f4f4;padding:5px;margin-bottom:10px;background:#f7f7f7}.attachment-block .attachment-img{max-width:100px;max-height:100px;height:auto;float:left}.attachment-block .attachment-pushed{margin-left:110px}.attachment-block .attachment-heading{margin:0}.attachment-block .attachment-text{color:#555}.connectedSortable{min-height:100px}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sort-highlight{background:#f4f4f4;border:1px dashed #ddd;margin-bottom:10px}.full-opacity-hover{opacity:.65;filter:alpha(opacity=65)}.full-opacity-hover:hover{opacity:1;filter:alpha(opacity=100)}.chart{position:relative;overflow:hidden;width:100%}.chart svg,.chart canvas{width:100% !important}@media print{.no-print,.main-sidebar,.left-side,.main-header,.content-header{display:none !important}.content-wrapper,.right-side,.main-footer{margin-left:0 !important;min-height:0 !important;-webkit-transform:translate(0, 0) !important;-ms-transform:translate(0, 0) !important;-o-transform:translate(0, 0) !important;transform:translate(0, 0) !important}.fixed .content-wrapper,.fixed .right-side{padding-top:0 !important}.invoice{width:100%;border:0;margin:0;padding:0}.invoice-col{float:left;width:33.3333333%}.table-responsive{overflow:auto}.table-responsive>.table tr th,.table-responsive>.table tr td{white-space:normal !important}} \ No newline at end of file + */html,body{height:100%}.layout-boxed html,.layout-boxed body{height:100%}body{font-family:'Source Sans Pro','Helvetica Neue',Helvetica,Arial,sans-serif;font-weight:400;overflow-x:hidden;overflow-y:auto}.wrapper{height:100%;position:relative;overflow-x:hidden;overflow-y:auto}.wrapper:before,.wrapper:after{content:" ";display:table}.wrapper:after{clear:both}.layout-boxed .wrapper{max-width:1250px;margin:0 auto;min-height:100%;box-shadow:0 0 8px rgba(0,0,0,0.5);position:relative}.layout-boxed{background:url('../img/boxed-bg.jpg') repeat fixed}.content-wrapper,.main-footer{-webkit-transition:-webkit-transform .3s ease-in-out,margin .3s ease-in-out;-moz-transition:-moz-transform .3s ease-in-out,margin .3s ease-in-out;-o-transition:-o-transform .3s ease-in-out,margin .3s ease-in-out;transition:transform .3s ease-in-out,margin .3s ease-in-out;margin-left:230px;z-index:820}.layout-top-nav .content-wrapper,.layout-top-nav .main-footer{margin-left:0}@media (max-width:767px){.content-wrapper,.main-footer{margin-left:0}}@media (min-width:768px){.sidebar-collapse .content-wrapper,.sidebar-collapse .main-footer{margin-left:0}}@media (max-width:767px){.sidebar-open .content-wrapper,.sidebar-open .main-footer{-webkit-transform:translate(230px, 0);-ms-transform:translate(230px, 0);-o-transform:translate(230px, 0);transform:translate(230px, 0)}}.content-wrapper{min-height:100%;background-color:#ecf0f5;z-index:800}.main-footer{background:#fff;padding:15px;color:#444;border-top:1px solid #d2d6de}.fixed .main-header,.fixed .main-sidebar,.fixed .left-side{position:fixed}.fixed .main-header{top:0;right:0;left:0}.fixed .content-wrapper,.fixed .right-side{padding-top:50px}@media (max-width:767px){.fixed .content-wrapper,.fixed .right-side{padding-top:100px}}.fixed.layout-boxed .wrapper{max-width:100%}.fixed .wrapper{overflow:hidden}.hold-transition .content-wrapper,.hold-transition .right-side,.hold-transition .main-footer,.hold-transition .main-sidebar,.hold-transition .left-side,.hold-transition .main-header .navbar,.hold-transition .main-header .logo,.hold-transition .menu-open .fa-angle-left{-webkit-transition:none;-o-transition:none;transition:none}.content{min-height:250px;padding:15px;margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:'Source Sans Pro',sans-serif}a{color:#303f9f}a:hover,a:active,a:focus{outline:none;text-decoration:none;color:#5161ca}.page-header{margin:10px 0 20px 0;font-size:22px}.page-header>small{color:#666;display:block;margin-top:5px}.main-header{position:relative;max-height:100px;z-index:1030}.main-header .navbar{-webkit-transition:margin-left .3s ease-in-out;-o-transition:margin-left .3s ease-in-out;transition:margin-left .3s ease-in-out;margin-bottom:0;margin-left:230px;border:none;min-height:50px;border-radius:0}.layout-top-nav .main-header .navbar{margin-left:0}.main-header #navbar-search-input.form-control{background:rgba(255,255,255,0.2);border-color:transparent}.main-header #navbar-search-input.form-control:focus,.main-header #navbar-search-input.form-control:active{border-color:rgba(0,0,0,0.1);background:rgba(255,255,255,0.9)}.main-header #navbar-search-input.form-control::-moz-placeholder{color:#ccc;opacity:1}.main-header #navbar-search-input.form-control:-ms-input-placeholder{color:#ccc}.main-header #navbar-search-input.form-control::-webkit-input-placeholder{color:#ccc}.main-header .navbar-custom-menu,.main-header .navbar-right{float:right}@media (max-width:991px){.main-header .navbar-custom-menu a,.main-header .navbar-right a{color:inherit;background:transparent}}@media (max-width:767px){.main-header .navbar-right{float:none}.navbar-collapse .main-header .navbar-right{margin:7.5px -15px}.main-header .navbar-right>li{color:inherit;border:0}}.main-header .sidebar-toggle{float:left;background-color:transparent;background-image:none;padding:15px 15px;font-family:fontAwesome}.main-header .sidebar-toggle:before{content:"\f0c9"}.main-header .sidebar-toggle:hover{color:#fff}.main-header .sidebar-toggle:focus,.main-header .sidebar-toggle:active{background:transparent}.main-header .sidebar-toggle .icon-bar{display:none}.main-header .navbar .nav>li.user>a>.fa,.main-header .navbar .nav>li.user>a>.glyphicon,.main-header .navbar .nav>li.user>a>.ion{margin-right:5px}.main-header .navbar .nav>li>a>.label{position:absolute;top:9px;right:7px;text-align:center;font-size:9px;padding:2px 3px;line-height:.9}.main-header .logo{-webkit-transition:width .3s ease-in-out;-o-transition:width .3s ease-in-out;transition:width .3s ease-in-out;display:block;float:left;height:50px;font-size:20px;line-height:50px;text-align:center;width:230px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;padding:0 15px;font-weight:300;overflow:hidden}.main-header .logo .logo-lg{display:block}.main-header .logo .logo-mini{display:none}.main-header .navbar-brand{color:#fff}.content-header{position:relative;padding:15px 15px 0 15px}.content-header>h1{margin:0;font-size:24px}.content-header>h1>small{font-size:15px;display:inline-block;padding-left:4px;font-weight:300}.content-header>.breadcrumb{float:right;background:transparent;margin-top:0;margin-bottom:0;font-size:12px;padding:7px 5px;position:absolute;top:15px;right:10px;border-radius:2px}.content-header>.breadcrumb>li>a{color:#444;text-decoration:none;display:inline-block}.content-header>.breadcrumb>li>a>.fa,.content-header>.breadcrumb>li>a>.glyphicon,.content-header>.breadcrumb>li>a>.ion{margin-right:5px}.content-header>.breadcrumb>li+li:before{content:'>\00a0'}@media (max-width:991px){.content-header>.breadcrumb{position:relative;margin-top:5px;top:0;right:0;float:none;background:#d2d6de;padding-left:10px}.content-header>.breadcrumb li:before{color:#97a0b3}}.navbar-toggle{color:#fff;border:0;margin:0;padding:15px 15px}@media (max-width:991px){.navbar-custom-menu .navbar-nav>li{float:left}.navbar-custom-menu .navbar-nav{margin:0;float:left}.navbar-custom-menu .navbar-nav>li>a{padding-top:15px;padding-bottom:15px;line-height:20px}}@media (max-width:767px){.main-header{position:relative}.main-header .logo,.main-header .navbar{width:100%;float:none}.main-header .navbar{margin:0}.main-header .navbar-custom-menu{float:right}}@media (max-width:991px){.navbar-collapse.pull-left{float:none !important}.navbar-collapse.pull-left+.navbar-custom-menu{display:block;position:absolute;top:0;right:40px}}.main-sidebar{position:absolute;top:0;left:0;padding-top:50px;min-height:100%;width:230px;z-index:810;-webkit-transition:-webkit-transform .3s ease-in-out,width .3s ease-in-out;-moz-transition:-moz-transform .3s ease-in-out,width .3s ease-in-out;-o-transition:-o-transform .3s ease-in-out,width .3s ease-in-out;transition:transform .3s ease-in-out,width .3s ease-in-out}@media (max-width:767px){.main-sidebar{padding-top:100px}}@media (max-width:767px){.main-sidebar{-webkit-transform:translate(-230px, 0);-ms-transform:translate(-230px, 0);-o-transform:translate(-230px, 0);transform:translate(-230px, 0)}}@media (min-width:768px){.sidebar-collapse .main-sidebar{-webkit-transform:translate(-230px, 0);-ms-transform:translate(-230px, 0);-o-transform:translate(-230px, 0);transform:translate(-230px, 0)}}@media (max-width:767px){.sidebar-open .main-sidebar{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0)}}.sidebar{padding-bottom:10px}.sidebar-form input:focus{border-color:transparent}.user-panel{position:relative;width:100%;padding:10px;overflow:hidden}.user-panel:before,.user-panel:after{content:" ";display:table}.user-panel:after{clear:both}.user-panel>.image>img{width:100%;max-width:45px;height:auto}.user-panel>.info{padding:5px 5px 5px 15px;line-height:1;position:absolute;left:55px}.user-panel>.info>p{font-weight:600;margin-bottom:9px}.user-panel>.info>a{text-decoration:none;padding-right:5px;margin-top:3px;font-size:11px}.user-panel>.info>a>.fa,.user-panel>.info>a>.ion,.user-panel>.info>a>.glyphicon{margin-right:3px}.sidebar-menu{list-style:none;margin:0;padding:0}.sidebar-menu>li{position:relative;margin:0;padding:0}.sidebar-menu>li>a{padding:12px 5px 12px 15px;display:block}.sidebar-menu>li>a>.fa,.sidebar-menu>li>a>.glyphicon,.sidebar-menu>li>a>.ion{width:20px}.sidebar-menu>li .label,.sidebar-menu>li .badge{margin-right:5px}.sidebar-menu>li .badge{margin-top:3px}.sidebar-menu li.header{padding:10px 25px 10px 15px;font-size:12px}.sidebar-menu li>a>.fa-angle-left,.sidebar-menu li>a>.pull-right-container>.fa-angle-left{width:auto;height:auto;padding:0;margin-right:10px;-webkit-transition:transform .5s ease;-o-transition:transform .5s ease;transition:transform .5s ease}.sidebar-menu li>a>.fa-angle-left{position:absolute;top:50%;right:10px;margin-top:-8px}.sidebar-menu .menu-open>a>.fa-angle-left,.sidebar-menu .menu-open>a>.pull-right-container>.fa-angle-left{-webkit-transform:rotate(-90deg);-ms-transform:rotate(-90deg);-o-transform:rotate(-90deg);transform:rotate(-90deg)}.sidebar-menu .active>.treeview-menu{display:block}@media (min-width:768px){.sidebar-mini.sidebar-collapse .content-wrapper,.sidebar-mini.sidebar-collapse .right-side,.sidebar-mini.sidebar-collapse .main-footer{margin-left:50px !important;z-index:840}.sidebar-mini.sidebar-collapse .main-sidebar{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0);width:50px !important;z-index:850}.sidebar-mini.sidebar-collapse .sidebar-menu>li{position:relative}.sidebar-mini.sidebar-collapse .sidebar-menu>li>a{margin-right:0}.sidebar-mini.sidebar-collapse .sidebar-menu>li>a>span{border-top-right-radius:4px}.sidebar-mini.sidebar-collapse .sidebar-menu>li:not(.treeview)>a>span{border-bottom-right-radius:4px}.sidebar-mini.sidebar-collapse .sidebar-menu>li>.treeview-menu{padding-top:5px;padding-bottom:5px;border-bottom-right-radius:4px}.sidebar-mini.sidebar-collapse .main-sidebar .user-panel>.info,.sidebar-mini.sidebar-collapse .sidebar-form,.sidebar-mini.sidebar-collapse .sidebar-menu>li>a>span,.sidebar-mini.sidebar-collapse .sidebar-menu>li>.treeview-menu,.sidebar-mini.sidebar-collapse .sidebar-menu>li>a>.pull-right,.sidebar-mini.sidebar-collapse .sidebar-menu li.header{display:none !important;-webkit-transform:translateZ(0)}.sidebar-mini.sidebar-collapse .main-header .logo{width:50px}.sidebar-mini.sidebar-collapse .main-header .logo>.logo-mini{display:block;margin-left:-15px;margin-right:-15px;font-size:18px}.sidebar-mini.sidebar-collapse .main-header .logo>.logo-lg{display:none}.sidebar-mini.sidebar-collapse .main-header .navbar{margin-left:50px}}.sidebar-mini:not(.sidebar-mini-expand-feature).sidebar-collapse .sidebar-menu>li:hover>a>span:not(.pull-right),.sidebar-mini:not(.sidebar-mini-expand-feature).sidebar-collapse .sidebar-menu>li:hover>.treeview-menu{display:block !important;position:absolute;width:180px;left:50px}.sidebar-mini:not(.sidebar-mini-expand-feature).sidebar-collapse .sidebar-menu>li:hover>a>span{top:0;margin-left:-3px;padding:12px 5px 12px 20px;background-color:inherit}.sidebar-mini:not(.sidebar-mini-expand-feature).sidebar-collapse .sidebar-menu>li:hover>a>.pull-right-container{position:relative !important;float:right;width:auto !important;left:180px !important;top:-22px !important;z-index:900}.sidebar-mini:not(.sidebar-mini-expand-feature).sidebar-collapse .sidebar-menu>li:hover>a>.pull-right-container>.label:not(:first-of-type){display:none}.sidebar-mini:not(.sidebar-mini-expand-feature).sidebar-collapse .sidebar-menu>li:hover>.treeview-menu{top:44px;margin-left:0}.sidebar-expanded-on-hover .main-footer,.sidebar-expanded-on-hover .content-wrapper{margin-left:50px}.sidebar-expanded-on-hover .main-sidebar{box-shadow:3px 0 8px rgba(0,0,0,0.125)}.sidebar-menu,.main-sidebar .user-panel,.sidebar-menu>li.header{white-space:nowrap;overflow:hidden}.sidebar-menu:hover{overflow:visible}.sidebar-form,.sidebar-menu>li.header{overflow:hidden;text-overflow:clip}.sidebar-menu li>a{position:relative}.sidebar-menu li>a>.pull-right-container{position:absolute;right:10px;top:50%;margin-top:-7px}.control-sidebar-bg{position:fixed;z-index:1000;bottom:0}.control-sidebar-bg,.control-sidebar{top:0;right:-230px;width:230px;-webkit-transition:right .3s ease-in-out;-o-transition:right .3s ease-in-out;transition:right .3s ease-in-out}.control-sidebar{position:absolute;padding-top:50px;z-index:1010}@media (max-width:768px){.control-sidebar{padding-top:100px}}.control-sidebar>.tab-content{padding:10px 15px}.control-sidebar.control-sidebar-open,.control-sidebar.control-sidebar-open+.control-sidebar-bg{right:0}.control-sidebar-open .control-sidebar-bg,.control-sidebar-open .control-sidebar{right:0}@media (min-width:768px){.control-sidebar-open .content-wrapper,.control-sidebar-open .right-side,.control-sidebar-open .main-footer{margin-right:230px}}.fixed .control-sidebar{position:fixed;height:100%;overflow-y:auto;padding-bottom:50px}.nav-tabs.control-sidebar-tabs>li:first-of-type>a,.nav-tabs.control-sidebar-tabs>li:first-of-type>a:hover,.nav-tabs.control-sidebar-tabs>li:first-of-type>a:focus{border-left-width:0}.nav-tabs.control-sidebar-tabs>li>a{border-radius:0}.nav-tabs.control-sidebar-tabs>li>a,.nav-tabs.control-sidebar-tabs>li>a:hover{border-top:none;border-right:none;border-left:1px solid transparent;border-bottom:1px solid transparent}.nav-tabs.control-sidebar-tabs>li>a .icon{font-size:16px}.nav-tabs.control-sidebar-tabs>li.active>a,.nav-tabs.control-sidebar-tabs>li.active>a:hover,.nav-tabs.control-sidebar-tabs>li.active>a:focus,.nav-tabs.control-sidebar-tabs>li.active>a:active{border-top:none;border-right:none;border-bottom:none}@media (max-width:768px){.nav-tabs.control-sidebar-tabs{display:table}.nav-tabs.control-sidebar-tabs>li{display:table-cell}}.control-sidebar-heading{font-weight:400;font-size:16px;padding:10px 0;margin-bottom:10px}.control-sidebar-subheading{display:block;font-weight:400;font-size:14px}.control-sidebar-menu{list-style:none;padding:0;margin:0 -15px}.control-sidebar-menu>li>a{display:block;padding:10px 15px}.control-sidebar-menu>li>a:before,.control-sidebar-menu>li>a:after{content:" ";display:table}.control-sidebar-menu>li>a:after{clear:both}.control-sidebar-menu>li>a>.control-sidebar-subheading{margin-top:0}.control-sidebar-menu .menu-icon{float:left;width:35px;height:35px;border-radius:50%;text-align:center;line-height:35px}.control-sidebar-menu .menu-info{margin-left:45px;margin-top:3px}.control-sidebar-menu .menu-info>.control-sidebar-subheading{margin:0}.control-sidebar-menu .menu-info>p{margin:0;font-size:11px}.control-sidebar-menu .progress{margin:0}.control-sidebar-dark{color:#b3b9cf}.control-sidebar-dark,.control-sidebar-dark+.control-sidebar-bg{background:#1f2331}.control-sidebar-dark .nav-tabs.control-sidebar-tabs{border-bottom:#191c28}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a{background:#151821;color:#b3b9cf}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:hover,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:focus{border-left-color:#11131b;border-bottom-color:#11131b}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:hover,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:focus,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:active{background:#191c28}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:hover{color:#fff}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a:hover,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a:focus,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a:active{background:#1f2331;color:#fff}.control-sidebar-dark .control-sidebar-heading,.control-sidebar-dark .control-sidebar-subheading{color:#fff}.control-sidebar-dark .control-sidebar-menu>li>a:hover{background:#1b1f2b}.control-sidebar-dark .control-sidebar-menu>li>a .menu-info>p{color:#b3b9cf}.control-sidebar-light{color:#5e5e5e}.control-sidebar-light,.control-sidebar-light+.control-sidebar-bg{background:#f9fafc;border-left:1px solid #d2d6de}.control-sidebar-light .nav-tabs.control-sidebar-tabs{border-bottom:#d2d6de}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a{background:#e8ecf4;color:#444}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:hover,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:focus{border-left-color:#d2d6de;border-bottom-color:#d2d6de}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:hover,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:focus,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:active{background:#eff1f7}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a:hover,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a:focus,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a:active{background:#f9fafc;color:#111}.control-sidebar-light .control-sidebar-heading,.control-sidebar-light .control-sidebar-subheading{color:#111}.control-sidebar-light .control-sidebar-menu{margin-left:-14px}.control-sidebar-light .control-sidebar-menu>li>a:hover{background:#f4f4f5}.control-sidebar-light .control-sidebar-menu>li>a .menu-info>p{color:#5e5e5e}.dropdown-menu{box-shadow:none;border-color:#eee}.dropdown-menu>li>a{color:#777}.dropdown-menu>li>a>.glyphicon,.dropdown-menu>li>a>.fa,.dropdown-menu>li>a>.ion{margin-right:10px}.dropdown-menu>li>a:hover{background-color:#e1e3e9;color:#333}.dropdown-menu>.divider{background-color:#eee}.navbar-nav>.notifications-menu>.dropdown-menu,.navbar-nav>.messages-menu>.dropdown-menu,.navbar-nav>.tasks-menu>.dropdown-menu{width:280px;padding:0 0 0 0;margin:0;top:100%}.navbar-nav>.notifications-menu>.dropdown-menu>li,.navbar-nav>.messages-menu>.dropdown-menu>li,.navbar-nav>.tasks-menu>.dropdown-menu>li{position:relative}.navbar-nav>.notifications-menu>.dropdown-menu>li.header,.navbar-nav>.messages-menu>.dropdown-menu>li.header,.navbar-nav>.tasks-menu>.dropdown-menu>li.header{border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0;background-color:#ffffff;padding:7px 10px;border-bottom:1px solid #f4f4f4;color:#444444;font-size:14px}.navbar-nav>.notifications-menu>.dropdown-menu>li.footer>a,.navbar-nav>.messages-menu>.dropdown-menu>li.footer>a,.navbar-nav>.tasks-menu>.dropdown-menu>li.footer>a{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px;font-size:12px;background-color:#fff;padding:7px 10px;border-bottom:1px solid #eeeeee;color:#444 !important;text-align:center}@media (max-width:991px){.navbar-nav>.notifications-menu>.dropdown-menu>li.footer>a,.navbar-nav>.messages-menu>.dropdown-menu>li.footer>a,.navbar-nav>.tasks-menu>.dropdown-menu>li.footer>a{background:#fff !important;color:#444 !important}}.navbar-nav>.notifications-menu>.dropdown-menu>li.footer>a:hover,.navbar-nav>.messages-menu>.dropdown-menu>li.footer>a:hover,.navbar-nav>.tasks-menu>.dropdown-menu>li.footer>a:hover{text-decoration:none;font-weight:normal}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu,.navbar-nav>.messages-menu>.dropdown-menu>li .menu,.navbar-nav>.tasks-menu>.dropdown-menu>li .menu{max-height:200px;margin:0;padding:0;list-style:none;overflow-x:hidden}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a,.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a,.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a{display:block;white-space:nowrap;border-bottom:1px solid #f4f4f4}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a:hover,.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:hover,.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a:hover{background:#f4f4f4;text-decoration:none}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a{color:#444444;overflow:hidden;text-overflow:ellipsis;padding:10px}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a>.glyphicon,.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a>.fa,.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a>.ion{width:20px}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a{margin:0;padding:10px 10px}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>div>img{margin:auto 10px auto auto;width:40px;height:40px}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>h4{padding:0;margin:0 0 0 45px;color:#444444;font-size:15px;position:relative}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>h4>small{color:#999999;font-size:10px;position:absolute;top:0;right:0}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>p{margin:0 0 0 45px;font-size:12px;color:#888888}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:before,.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:after{content:" ";display:table}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:after{clear:both}.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a{padding:10px}.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a>h3{font-size:14px;padding:0;margin:0 0 10px 0;color:#666666}.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a>.progress{padding:0;margin:0}.navbar-nav>.user-menu>.dropdown-menu{border-top-right-radius:0;border-top-left-radius:0;padding:1px 0 0 0;border-top-width:0;width:280px}.navbar-nav>.user-menu>.dropdown-menu,.navbar-nav>.user-menu>.dropdown-menu>.user-body{border-bottom-right-radius:4px;border-bottom-left-radius:4px}.navbar-nav>.user-menu>.dropdown-menu>li.user-header{height:175px;padding:10px;text-align:center}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>img{z-index:5;height:90px;width:90px;border:3px solid;border-color:transparent;border-color:rgba(255,255,255,0.2)}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>p{z-index:5;color:#fff;color:rgba(255,255,255,0.8);font-size:17px;margin-top:10px}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>p>small{display:block;font-size:12px}.navbar-nav>.user-menu>.dropdown-menu>.user-body{padding:15px;border-bottom:1px solid #f4f4f4;border-top:1px solid #dddddd}.navbar-nav>.user-menu>.dropdown-menu>.user-body:before,.navbar-nav>.user-menu>.dropdown-menu>.user-body:after{content:" ";display:table}.navbar-nav>.user-menu>.dropdown-menu>.user-body:after{clear:both}.navbar-nav>.user-menu>.dropdown-menu>.user-body a{color:#444 !important}@media (max-width:991px){.navbar-nav>.user-menu>.dropdown-menu>.user-body a{background:#fff !important;color:#444 !important}}.navbar-nav>.user-menu>.dropdown-menu>.user-footer{background-color:#f9f9f9;padding:10px}.navbar-nav>.user-menu>.dropdown-menu>.user-footer:before,.navbar-nav>.user-menu>.dropdown-menu>.user-footer:after{content:" ";display:table}.navbar-nav>.user-menu>.dropdown-menu>.user-footer:after{clear:both}.navbar-nav>.user-menu>.dropdown-menu>.user-footer .btn-default{color:#666666}@media (max-width:991px){.navbar-nav>.user-menu>.dropdown-menu>.user-footer .btn-default:hover{background-color:#f9f9f9}}.navbar-nav>.user-menu .user-image{float:left;width:25px;height:25px;border-radius:50%;margin-right:10px;margin-top:-2px}@media (max-width:767px){.navbar-nav>.user-menu .user-image{float:none;margin-right:0;margin-top:-8px;line-height:10px}}.open:not(.dropup)>.animated-dropdown-menu{backface-visibility:visible !important;-webkit-animation:flipInX .7s both;-o-animation:flipInX .7s both;animation:flipInX .7s both}@keyframes flipInX{0%{transform:perspective(400px) rotate3d(1, 0, 0, 90deg);transition-timing-function:ease-in;opacity:0}40%{transform:perspective(400px) rotate3d(1, 0, 0, -20deg);transition-timing-function:ease-in}60%{transform:perspective(400px) rotate3d(1, 0, 0, 10deg);opacity:1}80%{transform:perspective(400px) rotate3d(1, 0, 0, -5deg)}100%{transform:perspective(400px)}}@-webkit-keyframes flipInX{0%{-webkit-transform:perspective(400px) rotate3d(1, 0, 0, 90deg);-webkit-transition-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotate3d(1, 0, 0, -20deg);-webkit-transition-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotate3d(1, 0, 0, 10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotate3d(1, 0, 0, -5deg)}100%{-webkit-transform:perspective(400px)}}.navbar-custom-menu>.navbar-nav>li{position:relative}.navbar-custom-menu>.navbar-nav>li>.dropdown-menu{position:absolute;right:0;left:auto}@media (max-width:991px){.navbar-custom-menu>.navbar-nav{float:right}.navbar-custom-menu>.navbar-nav>li{position:static}.navbar-custom-menu>.navbar-nav>li>.dropdown-menu{position:absolute;right:5%;left:auto;border:1px solid #ddd;background:#fff}}.form-control{border-radius:0;box-shadow:none;border-color:#d2d6de}.form-control:focus{border-color:#303f9f;box-shadow:none}.form-control::-moz-placeholder,.form-control:-ms-input-placeholder,.form-control::-webkit-input-placeholder{color:#bbb;opacity:1}.form-control:not(select){-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-group.has-success label{color:#00a65a}.form-group.has-success .form-control,.form-group.has-success .input-group-addon{border-color:#00a65a;box-shadow:none}.form-group.has-success .help-block{color:#00a65a}.form-group.has-warning label{color:#f39c12}.form-group.has-warning .form-control,.form-group.has-warning .input-group-addon{border-color:#f39c12;box-shadow:none}.form-group.has-warning .help-block{color:#f39c12}.form-group.has-error label{color:#dd4b39}.form-group.has-error .form-control,.form-group.has-error .input-group-addon{border-color:#dd4b39;box-shadow:none}.form-group.has-error .help-block{color:#dd4b39}.input-group .input-group-addon{border-radius:0;border-color:#d2d6de;background-color:#fff}.btn-group-vertical .btn.btn-flat:first-of-type,.btn-group-vertical .btn.btn-flat:last-of-type{border-radius:0}.icheck>label{padding-left:0}.form-control-feedback.fa{line-height:34px}.input-lg+.form-control-feedback.fa,.input-group-lg+.form-control-feedback.fa,.form-group-lg .form-control+.form-control-feedback.fa{line-height:46px}.input-sm+.form-control-feedback.fa,.input-group-sm+.form-control-feedback.fa,.form-group-sm .form-control+.form-control-feedback.fa{line-height:30px}.progress,.progress>.progress-bar{-webkit-box-shadow:none;box-shadow:none}.progress,.progress>.progress-bar,.progress .progress-bar,.progress>.progress-bar .progress-bar{border-radius:1px}.progress.sm,.progress-sm{height:10px}.progress.sm,.progress-sm,.progress.sm .progress-bar,.progress-sm .progress-bar{border-radius:1px}.progress.xs,.progress-xs{height:7px}.progress.xs,.progress-xs,.progress.xs .progress-bar,.progress-xs .progress-bar{border-radius:1px}.progress.xxs,.progress-xxs{height:3px}.progress.xxs,.progress-xxs,.progress.xxs .progress-bar,.progress-xxs .progress-bar{border-radius:1px}.progress.vertical{position:relative;width:30px;height:200px;display:inline-block;margin-right:10px}.progress.vertical>.progress-bar{width:100%;position:absolute;bottom:0}.progress.vertical.sm,.progress.vertical.progress-sm{width:20px}.progress.vertical.xs,.progress.vertical.progress-xs{width:10px}.progress.vertical.xxs,.progress.vertical.progress-xxs{width:3px}.progress-group .progress-text{font-weight:600}.progress-group .progress-number{float:right}.table tr>td .progress{margin:0}.progress-bar-light-blue,.progress-bar-primary{background-color:#303f9f}.progress-striped .progress-bar-light-blue,.progress-striped .progress-bar-primary{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-green,.progress-bar-success{background-color:#00a65a}.progress-striped .progress-bar-green,.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-aqua,.progress-bar-info{background-color:#00c0ef}.progress-striped .progress-bar-aqua,.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-yellow,.progress-bar-warning{background-color:#f39c12}.progress-striped .progress-bar-yellow,.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-red,.progress-bar-danger{background-color:#dd4b39}.progress-striped .progress-bar-red,.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.small-box{border-radius:2px;position:relative;display:block;margin-bottom:20px;box-shadow:0 1px 1px rgba(0,0,0,0.1)}.small-box>.inner{padding:10px}.small-box>.small-box-footer{position:relative;text-align:center;padding:3px 0;color:#fff;color:rgba(255,255,255,0.8);display:block;z-index:10;background:rgba(0,0,0,0.1);text-decoration:none}.small-box>.small-box-footer:hover{color:#fff;background:rgba(0,0,0,0.15)}.small-box h3{font-size:38px;font-weight:bold;margin:0 0 10px 0;white-space:nowrap;padding:0}.small-box p{font-size:15px}.small-box p>small{display:block;color:#f9f9f9;font-size:13px;margin-top:5px}.small-box h3,.small-box p{z-index:5}.small-box .icon{-webkit-transition:all .3s linear;-o-transition:all .3s linear;transition:all .3s linear;position:absolute;top:-10px;right:10px;z-index:0;font-size:90px;color:rgba(0,0,0,0.15)}.small-box:hover{text-decoration:none;color:#f9f9f9}.small-box:hover .icon{font-size:95px}@media (max-width:767px){.small-box{text-align:center}.small-box .icon{display:none}.small-box p{font-size:12px}}.box{position:relative;border-radius:3px;background:#ffffff;border-top:3px solid #d2d6de;margin-bottom:20px;width:100%;box-shadow:0 1px 1px rgba(0,0,0,0.1)}.box.box-primary{border-top-color:#303f9f}.box.box-info{border-top-color:#00c0ef}.box.box-danger{border-top-color:#dd4b39}.box.box-warning{border-top-color:#f39c12}.box.box-success{border-top-color:#00a65a}.box.box-default{border-top-color:#d2d6de}.box.collapsed-box .box-body,.box.collapsed-box .box-footer{display:none}.box .nav-stacked>li{border-bottom:1px solid #f4f4f4;margin:0}.box .nav-stacked>li:last-of-type{border-bottom:none}.box.height-control .box-body{max-height:300px;overflow:auto}.box .border-right{border-right:1px solid #f4f4f4}.box .border-left{border-left:1px solid #f4f4f4}.box.box-solid{border-top:0}.box.box-solid>.box-header .btn.btn-default{background:transparent}.box.box-solid>.box-header .btn:hover,.box.box-solid>.box-header a:hover{background:rgba(0,0,0,0.1)}.box.box-solid.box-default{border:1px solid #d2d6de}.box.box-solid.box-default>.box-header{color:#444;background:#d2d6de;background-color:#d2d6de}.box.box-solid.box-default>.box-header a,.box.box-solid.box-default>.box-header .btn{color:#444}.box.box-solid.box-primary{border:1px solid #303f9f}.box.box-solid.box-primary>.box-header{color:#fff;background:#303f9f;background-color:#303f9f}.box.box-solid.box-primary>.box-header a,.box.box-solid.box-primary>.box-header .btn{color:#fff}.box.box-solid.box-info{border:1px solid #00c0ef}.box.box-solid.box-info>.box-header{color:#fff;background:#00c0ef;background-color:#00c0ef}.box.box-solid.box-info>.box-header a,.box.box-solid.box-info>.box-header .btn{color:#fff}.box.box-solid.box-danger{border:1px solid #dd4b39}.box.box-solid.box-danger>.box-header{color:#fff;background:#dd4b39;background-color:#dd4b39}.box.box-solid.box-danger>.box-header a,.box.box-solid.box-danger>.box-header .btn{color:#fff}.box.box-solid.box-warning{border:1px solid #f39c12}.box.box-solid.box-warning>.box-header{color:#fff;background:#f39c12;background-color:#f39c12}.box.box-solid.box-warning>.box-header a,.box.box-solid.box-warning>.box-header .btn{color:#fff}.box.box-solid.box-success{border:1px solid #00a65a}.box.box-solid.box-success>.box-header{color:#fff;background:#00a65a;background-color:#00a65a}.box.box-solid.box-success>.box-header a,.box.box-solid.box-success>.box-header .btn{color:#fff}.box.box-solid>.box-header>.box-tools .btn{border:0;box-shadow:none}.box.box-solid[class*='bg']>.box-header{color:#fff}.box .box-group>.box{margin-bottom:5px}.box .knob-label{text-align:center;color:#333;font-weight:100;font-size:12px;margin-bottom:0.3em}.box>.overlay,.overlay-wrapper>.overlay,.box>.loading-img,.overlay-wrapper>.loading-img{position:absolute;top:0;left:0;width:100%;height:100%}.box .overlay,.overlay-wrapper .overlay{z-index:50;background:rgba(255,255,255,0.7);border-radius:3px}.box .overlay>.fa,.overlay-wrapper .overlay>.fa{position:absolute;top:50%;left:50%;margin-left:-15px;margin-top:-15px;color:#000;font-size:30px}.box .overlay.dark,.overlay-wrapper .overlay.dark{background:rgba(0,0,0,0.5)}.box-header:before,.box-body:before,.box-footer:before,.box-header:after,.box-body:after,.box-footer:after{content:" ";display:table}.box-header:after,.box-body:after,.box-footer:after{clear:both}.box-header{color:#444;display:block;padding:10px;position:relative}.box-header.with-border{border-bottom:1px solid #f4f4f4}.collapsed-box .box-header.with-border{border-bottom:none}.box-header>.fa,.box-header>.glyphicon,.box-header>.ion,.box-header .box-title{display:inline-block;font-size:18px;margin:0;line-height:1}.box-header>.fa,.box-header>.glyphicon,.box-header>.ion{margin-right:5px}.box-header>.box-tools{position:absolute;right:10px;top:5px}.box-header>.box-tools [data-toggle="tooltip"]{position:relative}.box-header>.box-tools.pull-right .dropdown-menu{right:0;left:auto}.box-header>.box-tools .dropdown-menu>li>a{color:#444!important}.btn-box-tool{padding:5px;font-size:12px;background:transparent;color:#97a0b3}.open .btn-box-tool,.btn-box-tool:hover{color:#606c84}.btn-box-tool.btn:active{box-shadow:none}.box-body{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px;padding:10px}.no-header .box-body{border-top-right-radius:3px;border-top-left-radius:3px}.box-body>.table{margin-bottom:0}.box-body .fc{margin-top:5px}.box-body .full-width-chart{margin:-19px}.box-body.no-padding .full-width-chart{margin:-9px}.box-body .box-pane{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:3px}.box-body .box-pane-right{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:3px;border-bottom-left-radius:0}.box-footer{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px;border-top:1px solid #f4f4f4;padding:10px;background-color:#fff}.chart-legend{margin:10px 0}@media (max-width:991px){.chart-legend>li{float:left;margin-right:10px}}.box-comments{background:#f7f7f7}.box-comments .box-comment{padding:8px 0;border-bottom:1px solid #eee}.box-comments .box-comment:before,.box-comments .box-comment:after{content:" ";display:table}.box-comments .box-comment:after{clear:both}.box-comments .box-comment:last-of-type{border-bottom:0}.box-comments .box-comment:first-of-type{padding-top:0}.box-comments .box-comment img{float:left}.box-comments .comment-text{margin-left:40px;color:#555}.box-comments .username{color:#444;display:block;font-weight:600}.box-comments .text-muted{font-weight:400;font-size:12px}.todo-list{margin:0;padding:0;list-style:none;overflow:auto}.todo-list>li{border-radius:2px;padding:10px;background:#f4f4f4;margin-bottom:2px;border-left:2px solid #e6e7e8;color:#444}.todo-list>li:last-of-type{margin-bottom:0}.todo-list>li>input[type='checkbox']{margin:0 10px 0 5px}.todo-list>li .text{display:inline-block;margin-left:5px;font-weight:600}.todo-list>li .label{margin-left:10px;font-size:9px}.todo-list>li .tools{display:none;float:right;color:#dd4b39}.todo-list>li .tools>.fa,.todo-list>li .tools>.glyphicon,.todo-list>li .tools>.ion{margin-right:5px;cursor:pointer}.todo-list>li:hover .tools{display:inline-block}.todo-list>li.done{color:#999}.todo-list>li.done .text{text-decoration:line-through;font-weight:500}.todo-list>li.done .label{background:#d2d6de !important}.todo-list .danger{border-left-color:#dd4b39}.todo-list .warning{border-left-color:#f39c12}.todo-list .info{border-left-color:#00c0ef}.todo-list .success{border-left-color:#00a65a}.todo-list .primary{border-left-color:#303f9f}.todo-list .handle{display:inline-block;cursor:move;margin:0 5px}.chat{padding:5px 20px 5px 10px}.chat .item{margin-bottom:10px}.chat .item:before,.chat .item:after{content:" ";display:table}.chat .item:after{clear:both}.chat .item>img{width:40px;height:40px;border:2px solid transparent;border-radius:50%}.chat .item>.online{border:2px solid #00a65a}.chat .item>.offline{border:2px solid #dd4b39}.chat .item>.message{margin-left:55px;margin-top:-40px}.chat .item>.message>.name{display:block;font-weight:600}.chat .item>.attachment{border-radius:3px;background:#f4f4f4;margin-left:65px;margin-right:15px;padding:10px}.chat .item>.attachment>h4{margin:0 0 5px 0;font-weight:600;font-size:14px}.chat .item>.attachment>p,.chat .item>.attachment>.filename{font-weight:600;font-size:13px;font-style:italic;margin:0}.chat .item>.attachment:before,.chat .item>.attachment:after{content:" ";display:table}.chat .item>.attachment:after{clear:both}.box-input{max-width:200px}.modal .panel-body{color:#444}.info-box{display:block;min-height:90px;background:#fff;width:100%;box-shadow:0 1px 1px rgba(0,0,0,0.1);border-radius:2px;margin-bottom:15px}.info-box small{font-size:14px}.info-box .progress{background:rgba(0,0,0,0.2);margin:5px -10px 5px -10px;height:2px}.info-box .progress,.info-box .progress .progress-bar{border-radius:0}.info-box .progress .progress-bar{background:#fff}.info-box-icon{border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px;display:block;float:left;height:90px;width:90px;text-align:center;font-size:45px;line-height:90px;background:rgba(0,0,0,0.2)}.info-box-icon>img{max-width:100%}.info-box-content{padding:5px 10px;margin-left:90px}.info-box-number{display:block;font-weight:bold;font-size:18px}.progress-description,.info-box-text{display:block;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.info-box-text{text-transform:uppercase}.info-box-more{display:block}.progress-description{margin:0}.timeline{position:relative;margin:0 0 30px 0;padding:0;list-style:none}.timeline:before{content:'';position:absolute;top:0;bottom:0;width:4px;background:#ddd;left:31px;margin:0;border-radius:2px}.timeline>li{position:relative;margin-right:10px;margin-bottom:15px}.timeline>li:before,.timeline>li:after{content:" ";display:table}.timeline>li:after{clear:both}.timeline>li>.timeline-item{-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.1);box-shadow:0 1px 1px rgba(0,0,0,0.1);border-radius:3px;margin-top:0;background:#fff;color:#444;margin-left:60px;margin-right:15px;padding:0;position:relative}.timeline>li>.timeline-item>.time{color:#999;float:right;padding:10px;font-size:12px}.timeline>li>.timeline-item>.timeline-header{margin:0;color:#555;border-bottom:1px solid #f4f4f4;padding:10px;font-size:16px;line-height:1.1}.timeline>li>.timeline-item>.timeline-header>a{font-weight:600}.timeline>li>.timeline-item>.timeline-body,.timeline>li>.timeline-item>.timeline-footer{padding:10px}.timeline>li>.fa,.timeline>li>.glyphicon,.timeline>li>.ion{width:30px;height:30px;font-size:15px;line-height:30px;position:absolute;color:#666;background:#d2d6de;border-radius:50%;text-align:center;left:18px;top:0}.timeline>.time-label>span{font-weight:600;padding:5px;display:inline-block;background-color:#fff;border-radius:4px}.timeline-inverse>li>.timeline-item{background:#f0f0f0;border:1px solid #ddd;-webkit-box-shadow:none;box-shadow:none}.timeline-inverse>li>.timeline-item>.timeline-header{border-bottom-color:#ddd}.btn{border-radius:3px;-webkit-box-shadow:none;box-shadow:none;border:1px solid transparent}.btn.uppercase{text-transform:uppercase}.btn.btn-flat{border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;border-width:1px}.btn:active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);-moz-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn:focus{outline:none}.btn.btn-file{position:relative;overflow:hidden}.btn.btn-file>input[type='file']{position:absolute;top:0;right:0;min-width:100%;min-height:100%;font-size:100px;text-align:right;opacity:0;filter:alpha(opacity=0);outline:none;background:white;cursor:inherit;display:block}.btn-default{background-color:#f4f4f4;color:#444;border-color:#ddd}.btn-default:hover,.btn-default:active,.btn-default.hover{background-color:#e7e7e7}.btn-primary{background-color:#303f9f;border-color:#2a378b}.btn-primary:hover,.btn-primary:active,.btn-primary.hover{background-color:#2a378b}.btn-success{background-color:#00a65a;border-color:#008d4c}.btn-success:hover,.btn-success:active,.btn-success.hover{background-color:#008d4c}.btn-info{background-color:#00c0ef;border-color:#00acd6}.btn-info:hover,.btn-info:active,.btn-info.hover{background-color:#00acd6}.btn-danger{background-color:#dd4b39;border-color:#d73925}.btn-danger:hover,.btn-danger:active,.btn-danger.hover{background-color:#d73925}.btn-warning{background-color:#f39c12;border-color:#e08e0b}.btn-warning:hover,.btn-warning:active,.btn-warning.hover{background-color:#e08e0b}.btn-outline{border:1px solid #fff;background:transparent;color:#fff}.btn-outline:hover,.btn-outline:focus,.btn-outline:active{color:rgba(255,255,255,0.7);border-color:rgba(255,255,255,0.7)}.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn[class*='bg-']:hover{-webkit-box-shadow:inset 0 0 100px rgba(0,0,0,0.2);box-shadow:inset 0 0 100px rgba(0,0,0,0.2)}.btn-app{border-radius:3px;position:relative;padding:15px 5px;margin:0 0 10px 10px;min-width:80px;height:60px;text-align:center;color:#666;border:1px solid #ddd;background-color:#f4f4f4;font-size:12px}.btn-app>.fa,.btn-app>.glyphicon,.btn-app>.ion{font-size:20px;display:block}.btn-app:hover{background:#f4f4f4;color:#444;border-color:#aaa}.btn-app:active,.btn-app:focus{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);-moz-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-app>.badge{position:absolute;top:-3px;right:-10px;font-size:10px;font-weight:400}.callout{border-radius:3px;margin:0 0 20px 0;padding:15px 30px 15px 15px;border-left:5px solid #eee}.callout a{color:#fff;text-decoration:underline}.callout a:hover{color:#eee}.callout h4{margin-top:0;font-weight:600}.callout p:last-child{margin-bottom:0}.callout code,.callout .highlight{background-color:#fff}.callout.callout-danger{border-color:#c23321}.callout.callout-warning{border-color:#c87f0a}.callout.callout-info{border-color:#0097bc}.callout.callout-success{border-color:#00733e}.alert{border-radius:3px}.alert h4{font-weight:600}.alert .icon{margin-right:10px}.alert .close{color:#000;opacity:.2;filter:alpha(opacity=20)}.alert .close:hover{opacity:.5;filter:alpha(opacity=50)}.alert a{color:#fff;text-decoration:underline}.alert-success{border-color:#008d4c}.alert-danger,.alert-error{border-color:#d73925}.alert-warning{border-color:#e08e0b}.alert-info{border-color:#00acd6}.nav>li>a:hover,.nav>li>a:active,.nav>li>a:focus{color:#444;background:#f7f7f7}.nav-pills>li>a{border-radius:0;border-top:3px solid transparent;color:#444}.nav-pills>li>a>.fa,.nav-pills>li>a>.glyphicon,.nav-pills>li>a>.ion{margin-right:5px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{border-top-color:#303f9f}.nav-pills>li.active>a{font-weight:600}.nav-stacked>li>a{border-radius:0;border-top:0;border-left:3px solid transparent;color:#444}.nav-stacked>li.active>a,.nav-stacked>li.active>a:hover{background:transparent;color:#444;border-top:0;border-left-color:#303f9f}.nav-stacked>li.header{border-bottom:1px solid #ddd;color:#777;margin-bottom:10px;padding:5px 10px;text-transform:uppercase}.nav-tabs-custom{margin-bottom:20px;background:#fff;box-shadow:0 1px 1px rgba(0,0,0,0.1);border-radius:3px}.nav-tabs-custom>.nav-tabs{margin:0;border-bottom-color:#f4f4f4;border-top-right-radius:3px;border-top-left-radius:3px}.nav-tabs-custom>.nav-tabs>li{border-top:3px solid transparent;margin-bottom:-2px;margin-right:5px}.nav-tabs-custom>.nav-tabs>li.disabled>a{color:#777}.nav-tabs-custom>.nav-tabs>li>a{color:#444;border-radius:0}.nav-tabs-custom>.nav-tabs>li>a.text-muted{color:#999}.nav-tabs-custom>.nav-tabs>li>a,.nav-tabs-custom>.nav-tabs>li>a:hover{background:transparent;margin:0}.nav-tabs-custom>.nav-tabs>li>a:hover{color:#999}.nav-tabs-custom>.nav-tabs>li:not(.active)>a:hover,.nav-tabs-custom>.nav-tabs>li:not(.active)>a:focus,.nav-tabs-custom>.nav-tabs>li:not(.active)>a:active{border-color:transparent}.nav-tabs-custom>.nav-tabs>li.active{border-top-color:#303f9f}.nav-tabs-custom>.nav-tabs>li.active>a,.nav-tabs-custom>.nav-tabs>li.active:hover>a{background-color:#fff;color:#444}.nav-tabs-custom>.nav-tabs>li.active>a{border-top-color:transparent;border-left-color:#f4f4f4;border-right-color:#f4f4f4}.nav-tabs-custom>.nav-tabs>li:first-of-type{margin-left:0}.nav-tabs-custom>.nav-tabs>li:first-of-type.active>a{border-left-color:transparent}.nav-tabs-custom>.nav-tabs.pull-right{float:none !important}.nav-tabs-custom>.nav-tabs.pull-right>li{float:right}.nav-tabs-custom>.nav-tabs.pull-right>li:first-of-type{margin-right:0}.nav-tabs-custom>.nav-tabs.pull-right>li:first-of-type>a{border-left-width:1px}.nav-tabs-custom>.nav-tabs.pull-right>li:first-of-type.active>a{border-left-color:#f4f4f4;border-right-color:transparent}.nav-tabs-custom>.nav-tabs>li.header{line-height:35px;padding:0 10px;font-size:20px;color:#444}.nav-tabs-custom>.nav-tabs>li.header>.fa,.nav-tabs-custom>.nav-tabs>li.header>.glyphicon,.nav-tabs-custom>.nav-tabs>li.header>.ion{margin-right:5px}.nav-tabs-custom>.tab-content{background:#fff;padding:10px;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.nav-tabs-custom .dropdown.open>a:active,.nav-tabs-custom .dropdown.open>a:focus{background:transparent;color:#999}.nav-tabs-custom.tab-primary>.nav-tabs>li.active{border-top-color:#303f9f}.nav-tabs-custom.tab-info>.nav-tabs>li.active{border-top-color:#00c0ef}.nav-tabs-custom.tab-danger>.nav-tabs>li.active{border-top-color:#dd4b39}.nav-tabs-custom.tab-warning>.nav-tabs>li.active{border-top-color:#f39c12}.nav-tabs-custom.tab-success>.nav-tabs>li.active{border-top-color:#00a65a}.nav-tabs-custom.tab-default>.nav-tabs>li.active{border-top-color:#d2d6de}.pagination>li>a{background:#fafafa;color:#666}.pagination.pagination-flat>li>a{border-radius:0 !important}.products-list{list-style:none;margin:0;padding:0}.products-list>.item{border-radius:3px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.1);box-shadow:0 1px 1px rgba(0,0,0,0.1);padding:10px 0;background:#fff}.products-list>.item:before,.products-list>.item:after{content:" ";display:table}.products-list>.item:after{clear:both}.products-list .product-img{float:left}.products-list .product-img img{width:50px;height:50px}.products-list .product-info{margin-left:60px}.products-list .product-title{font-weight:600}.products-list .product-description{display:block;color:#999;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.product-list-in-box>.item{-webkit-box-shadow:none;box-shadow:none;border-radius:0;border-bottom:1px solid #f4f4f4}.product-list-in-box>.item:last-of-type{border-bottom-width:0}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{border-top:1px solid #f4f4f4}.table>thead>tr>th{border-bottom:2px solid #f4f4f4}.table tr td .progress{margin-top:5px}.table-bordered{border:1px solid #f4f4f4}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #f4f4f4}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table.no-border,.table.no-border td,.table.no-border th{border:0}table.text-center,table.text-center td,table.text-center th{text-align:center}.table.align th{text-align:left}.table.align td{text-align:right}.label-default{background-color:#d2d6de;color:#444}.direct-chat .box-body{border-bottom-right-radius:0;border-bottom-left-radius:0;position:relative;overflow-x:hidden;padding:0}.direct-chat.chat-pane-open .direct-chat-contacts{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0)}.direct-chat-messages{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0);padding:10px;height:250px;overflow:auto}.direct-chat-msg,.direct-chat-text{display:block}.direct-chat-msg{margin-bottom:10px}.direct-chat-msg:before,.direct-chat-msg:after{content:" ";display:table}.direct-chat-msg:after{clear:both}.direct-chat-messages,.direct-chat-contacts{-webkit-transition:-webkit-transform .5s ease-in-out;-moz-transition:-moz-transform .5s ease-in-out;-o-transition:-o-transform .5s ease-in-out;transition:transform .5s ease-in-out}.direct-chat-text{border-radius:5px;position:relative;padding:5px 10px;background:#d2d6de;border:1px solid #d2d6de;margin:5px 0 0 50px;color:#444}.direct-chat-text:after,.direct-chat-text:before{position:absolute;right:100%;top:15px;border:solid transparent;border-right-color:#d2d6de;content:' ';height:0;width:0;pointer-events:none}.direct-chat-text:after{border-width:5px;margin-top:-5px}.direct-chat-text:before{border-width:6px;margin-top:-6px}.right .direct-chat-text{margin-right:50px;margin-left:0}.right .direct-chat-text:after,.right .direct-chat-text:before{right:auto;left:100%;border-right-color:transparent;border-left-color:#d2d6de}.direct-chat-img{border-radius:50%;float:left;width:40px;height:40px}.right .direct-chat-img{float:right}.direct-chat-info{display:block;margin-bottom:2px;font-size:12px}.direct-chat-name{font-weight:600}.direct-chat-timestamp{color:#999}.direct-chat-contacts-open .direct-chat-contacts{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0)}.direct-chat-contacts{-webkit-transform:translate(101%, 0);-ms-transform:translate(101%, 0);-o-transform:translate(101%, 0);transform:translate(101%, 0);position:absolute;top:0;bottom:0;height:250px;width:100%;background:#222d32;color:#fff;overflow:auto}.contacts-list>li{border-bottom:1px solid rgba(0,0,0,0.2);padding:10px;margin:0}.contacts-list>li:before,.contacts-list>li:after{content:" ";display:table}.contacts-list>li:after{clear:both}.contacts-list>li:last-of-type{border-bottom:none}.contacts-list-img{border-radius:50%;width:40px;float:left}.contacts-list-info{margin-left:45px;color:#fff}.contacts-list-name,.contacts-list-status{display:block}.contacts-list-name{font-weight:600}.contacts-list-status{font-size:12px}.contacts-list-date{color:#aaa;font-weight:normal}.contacts-list-msg{color:#999}.direct-chat-danger .right>.direct-chat-text{background:#dd4b39;border-color:#dd4b39;color:#fff}.direct-chat-danger .right>.direct-chat-text:after,.direct-chat-danger .right>.direct-chat-text:before{border-left-color:#dd4b39}.direct-chat-primary .right>.direct-chat-text{background:#303f9f;border-color:#303f9f;color:#fff}.direct-chat-primary .right>.direct-chat-text:after,.direct-chat-primary .right>.direct-chat-text:before{border-left-color:#303f9f}.direct-chat-warning .right>.direct-chat-text{background:#f39c12;border-color:#f39c12;color:#fff}.direct-chat-warning .right>.direct-chat-text:after,.direct-chat-warning .right>.direct-chat-text:before{border-left-color:#f39c12}.direct-chat-info .right>.direct-chat-text{background:#00c0ef;border-color:#00c0ef;color:#fff}.direct-chat-info .right>.direct-chat-text:after,.direct-chat-info .right>.direct-chat-text:before{border-left-color:#00c0ef}.direct-chat-success .right>.direct-chat-text{background:#00a65a;border-color:#00a65a;color:#fff}.direct-chat-success .right>.direct-chat-text:after,.direct-chat-success .right>.direct-chat-text:before{border-left-color:#00a65a}.users-list>li{width:25%;float:left;padding:10px;text-align:center}.users-list>li img{border-radius:50%;max-width:100%;height:auto}.users-list>li>a:hover,.users-list>li>a:hover .users-list-name{color:#999}.users-list-name,.users-list-date{display:block}.users-list-name{font-weight:600;color:#444;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.users-list-date{color:#999;font-size:12px}.carousel-control.left,.carousel-control.right{background-image:none}.carousel-control>.fa{font-size:40px;position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-20px}.modal{background:rgba(0,0,0,0.3)}.modal-content{border-radius:0;-webkit-box-shadow:0 2px 3px rgba(0,0,0,0.125);box-shadow:0 2px 3px rgba(0,0,0,0.125);border:0}@media (min-width:768px){.modal-content{-webkit-box-shadow:0 2px 3px rgba(0,0,0,0.125);box-shadow:0 2px 3px rgba(0,0,0,0.125)}}.modal-header{border-bottom-color:#f4f4f4}.modal-footer{border-top-color:#f4f4f4}.modal-primary .modal-header,.modal-primary .modal-footer{border-color:#242f78}.modal-warning .modal-header,.modal-warning .modal-footer{border-color:#c87f0a}.modal-info .modal-header,.modal-info .modal-footer{border-color:#0097bc}.modal-success .modal-header,.modal-success .modal-footer{border-color:#00733e}.modal-danger .modal-header,.modal-danger .modal-footer{border-color:#c23321}.box-widget{border:none;position:relative}.widget-user .widget-user-header{padding:20px;height:120px;border-top-right-radius:3px;border-top-left-radius:3px}.widget-user .widget-user-username{margin-top:0;margin-bottom:5px;font-size:25px;font-weight:300;text-shadow:0 1px 1px rgba(0,0,0,0.2)}.widget-user .widget-user-desc{margin-top:0}.widget-user .widget-user-image{position:absolute;top:65px;left:50%;margin-left:-45px}.widget-user .widget-user-image>img{width:90px;height:auto;border:3px solid #fff}.widget-user .box-footer{padding-top:30px}.widget-user-2 .widget-user-header{padding:20px;border-top-right-radius:3px;border-top-left-radius:3px}.widget-user-2 .widget-user-username{margin-top:5px;margin-bottom:5px;font-size:25px;font-weight:300}.widget-user-2 .widget-user-desc{margin-top:0}.widget-user-2 .widget-user-username,.widget-user-2 .widget-user-desc{margin-left:75px}.widget-user-2 .widget-user-image>img{width:65px;height:auto;float:left}.treeview-menu{display:none;list-style:none;padding:0;margin:0;padding-left:5px}.treeview-menu .treeview-menu{padding-left:20px}.treeview-menu>li{margin:0}.treeview-menu>li>a{padding:5px 5px 5px 15px;display:block;font-size:14px}.treeview-menu>li>a>.fa,.treeview-menu>li>a>.glyphicon,.treeview-menu>li>a>.ion{width:20px}.treeview-menu>li>a>.pull-right-container>.fa-angle-left,.treeview-menu>li>a>.pull-right-container>.fa-angle-down,.treeview-menu>li>a>.fa-angle-left,.treeview-menu>li>a>.fa-angle-down{width:auto}.mailbox-messages>.table{margin:0}.mailbox-controls{padding:5px}.mailbox-controls.with-border{border-bottom:1px solid #f4f4f4}.mailbox-read-info{border-bottom:1px solid #f4f4f4;padding:10px}.mailbox-read-info h3{font-size:20px;margin:0}.mailbox-read-info h5{margin:0;padding:5px 0 0 0}.mailbox-read-time{color:#999;font-size:13px}.mailbox-read-message{padding:10px}.mailbox-attachments li{float:left;width:200px;border:1px solid #eee;margin-bottom:10px;margin-right:10px}.mailbox-attachment-name{font-weight:bold;color:#666}.mailbox-attachment-icon,.mailbox-attachment-info,.mailbox-attachment-size{display:block}.mailbox-attachment-info{padding:10px;background:#f4f4f4}.mailbox-attachment-size{color:#999;font-size:12px}.mailbox-attachment-icon{text-align:center;font-size:65px;color:#666;padding:20px 10px}.mailbox-attachment-icon.has-img{padding:0}.mailbox-attachment-icon.has-img>img{max-width:100%;height:auto}.lockscreen{background:#d2d6de}.lockscreen-logo{font-size:35px;text-align:center;margin-bottom:25px;font-weight:300}.lockscreen-logo a{color:#444}.lockscreen-wrapper{max-width:400px;margin:0 auto;margin-top:10%}.lockscreen .lockscreen-name{text-align:center;font-weight:600}.lockscreen-item{border-radius:4px;padding:0;background:#fff;position:relative;margin:10px auto 30px auto;width:290px}.lockscreen-image{border-radius:50%;position:absolute;left:-10px;top:-25px;background:#fff;padding:5px;z-index:10}.lockscreen-image>img{border-radius:50%;width:70px;height:70px}.lockscreen-credentials{margin-left:70px}.lockscreen-credentials .form-control{border:0}.lockscreen-credentials .btn{background-color:#fff;border:0;padding:0 10px}.lockscreen-footer{margin-top:10px}.login-logo,.register-logo{font-size:35px;text-align:center;margin-bottom:25px;font-weight:300}.login-logo a,.register-logo a{color:#444}.login-page,.register-page{background:#d2d6de}.login-box,.register-box{width:360px;margin:7% auto}@media (max-width:768px){.login-box,.register-box{width:90%;margin-top:20px}}.login-box-body,.register-box-body{background:#fff;padding:20px;border-top:0;color:#666}.login-box-body .form-control-feedback,.register-box-body .form-control-feedback{color:#777}.login-box-msg,.register-box-msg{margin:0;text-align:center;padding:0 20px 20px 20px}.social-auth-links{margin:10px 0}.error-page{width:600px;margin:20px auto 0 auto}@media (max-width:991px){.error-page{width:100%}}.error-page>.headline{float:left;font-size:100px;font-weight:300}@media (max-width:991px){.error-page>.headline{float:none;text-align:center}}.error-page>.error-content{margin-left:190px;display:block}@media (max-width:991px){.error-page>.error-content{margin-left:0}}.error-page>.error-content>h3{font-weight:300;font-size:25px}@media (max-width:991px){.error-page>.error-content>h3{text-align:center}}.invoice{position:relative;background:#fff;border:1px solid #f4f4f4;padding:20px;margin:10px 25px}.invoice-title{margin-top:0}.profile-user-img{margin:0 auto;width:100px;padding:3px;border:3px solid #d2d6de}.profile-username{font-size:21px;margin-top:5px}.post{border-bottom:1px solid #d2d6de;margin-bottom:15px;padding-bottom:15px;color:#666}.post:last-of-type{border-bottom:0;margin-bottom:0;padding-bottom:0}.post .user-block{margin-bottom:15px}.btn-social{position:relative;padding-left:44px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.btn-social>:first-child{position:absolute;left:0;top:0;bottom:0;width:32px;line-height:34px;font-size:1.6em;text-align:center;border-right:1px solid rgba(0,0,0,0.2)}.btn-social.btn-lg{padding-left:61px}.btn-social.btn-lg>:first-child{line-height:45px;width:45px;font-size:1.8em}.btn-social.btn-sm{padding-left:38px}.btn-social.btn-sm>:first-child{line-height:28px;width:28px;font-size:1.4em}.btn-social.btn-xs{padding-left:30px}.btn-social.btn-xs>:first-child{line-height:20px;width:20px;font-size:1.2em}.btn-social-icon{position:relative;padding-left:44px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;height:34px;width:34px;padding:0}.btn-social-icon>:first-child{position:absolute;left:0;top:0;bottom:0;width:32px;line-height:34px;font-size:1.6em;text-align:center;border-right:1px solid rgba(0,0,0,0.2)}.btn-social-icon.btn-lg{padding-left:61px}.btn-social-icon.btn-lg>:first-child{line-height:45px;width:45px;font-size:1.8em}.btn-social-icon.btn-sm{padding-left:38px}.btn-social-icon.btn-sm>:first-child{line-height:28px;width:28px;font-size:1.4em}.btn-social-icon.btn-xs{padding-left:30px}.btn-social-icon.btn-xs>:first-child{line-height:20px;width:20px;font-size:1.2em}.btn-social-icon>:first-child{border:none;text-align:center;width:100%}.btn-social-icon.btn-lg{height:45px;width:45px;padding-left:0;padding-right:0}.btn-social-icon.btn-sm{height:30px;width:30px;padding-left:0;padding-right:0}.btn-social-icon.btn-xs{height:22px;width:22px;padding-left:0;padding-right:0}.btn-adn{color:#fff;background-color:#d87a68;border-color:rgba(0,0,0,0.2)}.btn-adn:focus,.btn-adn.focus{color:#fff;background-color:#ce563f;border-color:rgba(0,0,0,0.2)}.btn-adn:hover{color:#fff;background-color:#ce563f;border-color:rgba(0,0,0,0.2)}.btn-adn:active,.btn-adn.active,.open>.dropdown-toggle.btn-adn{color:#fff;background-color:#ce563f;border-color:rgba(0,0,0,0.2)}.btn-adn:active,.btn-adn.active,.open>.dropdown-toggle.btn-adn{background-image:none}.btn-adn .badge{color:#d87a68;background-color:#fff}.btn-bitbucket{color:#fff;background-color:#205081;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:focus,.btn-bitbucket.focus{color:#fff;background-color:#163758;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:hover{color:#fff;background-color:#163758;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:active,.btn-bitbucket.active,.open>.dropdown-toggle.btn-bitbucket{color:#fff;background-color:#163758;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:active,.btn-bitbucket.active,.open>.dropdown-toggle.btn-bitbucket{background-image:none}.btn-bitbucket .badge{color:#205081;background-color:#fff}.btn-dropbox{color:#fff;background-color:#1087dd;border-color:rgba(0,0,0,0.2)}.btn-dropbox:focus,.btn-dropbox.focus{color:#fff;background-color:#0d6aad;border-color:rgba(0,0,0,0.2)}.btn-dropbox:hover{color:#fff;background-color:#0d6aad;border-color:rgba(0,0,0,0.2)}.btn-dropbox:active,.btn-dropbox.active,.open>.dropdown-toggle.btn-dropbox{color:#fff;background-color:#0d6aad;border-color:rgba(0,0,0,0.2)}.btn-dropbox:active,.btn-dropbox.active,.open>.dropdown-toggle.btn-dropbox{background-image:none}.btn-dropbox .badge{color:#1087dd;background-color:#fff}.btn-facebook{color:#fff;background-color:#3b5998;border-color:rgba(0,0,0,0.2)}.btn-facebook:focus,.btn-facebook.focus{color:#fff;background-color:#2d4373;border-color:rgba(0,0,0,0.2)}.btn-facebook:hover{color:#fff;background-color:#2d4373;border-color:rgba(0,0,0,0.2)}.btn-facebook:active,.btn-facebook.active,.open>.dropdown-toggle.btn-facebook{color:#fff;background-color:#2d4373;border-color:rgba(0,0,0,0.2)}.btn-facebook:active,.btn-facebook.active,.open>.dropdown-toggle.btn-facebook{background-image:none}.btn-facebook .badge{color:#3b5998;background-color:#fff}.btn-flickr{color:#fff;background-color:#ff0084;border-color:rgba(0,0,0,0.2)}.btn-flickr:focus,.btn-flickr.focus{color:#fff;background-color:#cc006a;border-color:rgba(0,0,0,0.2)}.btn-flickr:hover{color:#fff;background-color:#cc006a;border-color:rgba(0,0,0,0.2)}.btn-flickr:active,.btn-flickr.active,.open>.dropdown-toggle.btn-flickr{color:#fff;background-color:#cc006a;border-color:rgba(0,0,0,0.2)}.btn-flickr:active,.btn-flickr.active,.open>.dropdown-toggle.btn-flickr{background-image:none}.btn-flickr .badge{color:#ff0084;background-color:#fff}.btn-foursquare{color:#fff;background-color:#f94877;border-color:rgba(0,0,0,0.2)}.btn-foursquare:focus,.btn-foursquare.focus{color:#fff;background-color:#f71752;border-color:rgba(0,0,0,0.2)}.btn-foursquare:hover{color:#fff;background-color:#f71752;border-color:rgba(0,0,0,0.2)}.btn-foursquare:active,.btn-foursquare.active,.open>.dropdown-toggle.btn-foursquare{color:#fff;background-color:#f71752;border-color:rgba(0,0,0,0.2)}.btn-foursquare:active,.btn-foursquare.active,.open>.dropdown-toggle.btn-foursquare{background-image:none}.btn-foursquare .badge{color:#f94877;background-color:#fff}.btn-github{color:#fff;background-color:#444;border-color:rgba(0,0,0,0.2)}.btn-github:focus,.btn-github.focus{color:#fff;background-color:#2b2b2b;border-color:rgba(0,0,0,0.2)}.btn-github:hover{color:#fff;background-color:#2b2b2b;border-color:rgba(0,0,0,0.2)}.btn-github:active,.btn-github.active,.open>.dropdown-toggle.btn-github{color:#fff;background-color:#2b2b2b;border-color:rgba(0,0,0,0.2)}.btn-github:active,.btn-github.active,.open>.dropdown-toggle.btn-github{background-image:none}.btn-github .badge{color:#444;background-color:#fff}.btn-google{color:#fff;background-color:#dd4b39;border-color:rgba(0,0,0,0.2)}.btn-google:focus,.btn-google.focus{color:#fff;background-color:#c23321;border-color:rgba(0,0,0,0.2)}.btn-google:hover{color:#fff;background-color:#c23321;border-color:rgba(0,0,0,0.2)}.btn-google:active,.btn-google.active,.open>.dropdown-toggle.btn-google{color:#fff;background-color:#c23321;border-color:rgba(0,0,0,0.2)}.btn-google:active,.btn-google.active,.open>.dropdown-toggle.btn-google{background-image:none}.btn-google .badge{color:#dd4b39;background-color:#fff}.btn-instagram{color:#fff;background-color:#3f729b;border-color:rgba(0,0,0,0.2)}.btn-instagram:focus,.btn-instagram.focus{color:#fff;background-color:#305777;border-color:rgba(0,0,0,0.2)}.btn-instagram:hover{color:#fff;background-color:#305777;border-color:rgba(0,0,0,0.2)}.btn-instagram:active,.btn-instagram.active,.open>.dropdown-toggle.btn-instagram{color:#fff;background-color:#305777;border-color:rgba(0,0,0,0.2)}.btn-instagram:active,.btn-instagram.active,.open>.dropdown-toggle.btn-instagram{background-image:none}.btn-instagram .badge{color:#3f729b;background-color:#fff}.btn-linkedin{color:#fff;background-color:#007bb6;border-color:rgba(0,0,0,0.2)}.btn-linkedin:focus,.btn-linkedin.focus{color:#fff;background-color:#005983;border-color:rgba(0,0,0,0.2)}.btn-linkedin:hover{color:#fff;background-color:#005983;border-color:rgba(0,0,0,0.2)}.btn-linkedin:active,.btn-linkedin.active,.open>.dropdown-toggle.btn-linkedin{color:#fff;background-color:#005983;border-color:rgba(0,0,0,0.2)}.btn-linkedin:active,.btn-linkedin.active,.open>.dropdown-toggle.btn-linkedin{background-image:none}.btn-linkedin .badge{color:#007bb6;background-color:#fff}.btn-microsoft{color:#fff;background-color:#2672ec;border-color:rgba(0,0,0,0.2)}.btn-microsoft:focus,.btn-microsoft.focus{color:#fff;background-color:#125acd;border-color:rgba(0,0,0,0.2)}.btn-microsoft:hover{color:#fff;background-color:#125acd;border-color:rgba(0,0,0,0.2)}.btn-microsoft:active,.btn-microsoft.active,.open>.dropdown-toggle.btn-microsoft{color:#fff;background-color:#125acd;border-color:rgba(0,0,0,0.2)}.btn-microsoft:active,.btn-microsoft.active,.open>.dropdown-toggle.btn-microsoft{background-image:none}.btn-microsoft .badge{color:#2672ec;background-color:#fff}.btn-openid{color:#fff;background-color:#f7931e;border-color:rgba(0,0,0,0.2)}.btn-openid:focus,.btn-openid.focus{color:#fff;background-color:#da7908;border-color:rgba(0,0,0,0.2)}.btn-openid:hover{color:#fff;background-color:#da7908;border-color:rgba(0,0,0,0.2)}.btn-openid:active,.btn-openid.active,.open>.dropdown-toggle.btn-openid{color:#fff;background-color:#da7908;border-color:rgba(0,0,0,0.2)}.btn-openid:active,.btn-openid.active,.open>.dropdown-toggle.btn-openid{background-image:none}.btn-openid .badge{color:#f7931e;background-color:#fff}.btn-pinterest{color:#fff;background-color:#cb2027;border-color:rgba(0,0,0,0.2)}.btn-pinterest:focus,.btn-pinterest.focus{color:#fff;background-color:#9f191f;border-color:rgba(0,0,0,0.2)}.btn-pinterest:hover{color:#fff;background-color:#9f191f;border-color:rgba(0,0,0,0.2)}.btn-pinterest:active,.btn-pinterest.active,.open>.dropdown-toggle.btn-pinterest{color:#fff;background-color:#9f191f;border-color:rgba(0,0,0,0.2)}.btn-pinterest:active,.btn-pinterest.active,.open>.dropdown-toggle.btn-pinterest{background-image:none}.btn-pinterest .badge{color:#cb2027;background-color:#fff}.btn-reddit{color:#000;background-color:#eff7ff;border-color:rgba(0,0,0,0.2)}.btn-reddit:focus,.btn-reddit.focus{color:#000;background-color:#bcddff;border-color:rgba(0,0,0,0.2)}.btn-reddit:hover{color:#000;background-color:#bcddff;border-color:rgba(0,0,0,0.2)}.btn-reddit:active,.btn-reddit.active,.open>.dropdown-toggle.btn-reddit{color:#000;background-color:#bcddff;border-color:rgba(0,0,0,0.2)}.btn-reddit:active,.btn-reddit.active,.open>.dropdown-toggle.btn-reddit{background-image:none}.btn-reddit .badge{color:#eff7ff;background-color:#000}.btn-soundcloud{color:#fff;background-color:#f50;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:focus,.btn-soundcloud.focus{color:#fff;background-color:#c40;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:hover{color:#fff;background-color:#c40;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:active,.btn-soundcloud.active,.open>.dropdown-toggle.btn-soundcloud{color:#fff;background-color:#c40;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:active,.btn-soundcloud.active,.open>.dropdown-toggle.btn-soundcloud{background-image:none}.btn-soundcloud .badge{color:#f50;background-color:#fff}.btn-tumblr{color:#fff;background-color:#2c4762;border-color:rgba(0,0,0,0.2)}.btn-tumblr:focus,.btn-tumblr.focus{color:#fff;background-color:#1c2d3f;border-color:rgba(0,0,0,0.2)}.btn-tumblr:hover{color:#fff;background-color:#1c2d3f;border-color:rgba(0,0,0,0.2)}.btn-tumblr:active,.btn-tumblr.active,.open>.dropdown-toggle.btn-tumblr{color:#fff;background-color:#1c2d3f;border-color:rgba(0,0,0,0.2)}.btn-tumblr:active,.btn-tumblr.active,.open>.dropdown-toggle.btn-tumblr{background-image:none}.btn-tumblr .badge{color:#2c4762;background-color:#fff}.btn-twitter{color:#fff;background-color:#55acee;border-color:rgba(0,0,0,0.2)}.btn-twitter:focus,.btn-twitter.focus{color:#fff;background-color:#2795e9;border-color:rgba(0,0,0,0.2)}.btn-twitter:hover{color:#fff;background-color:#2795e9;border-color:rgba(0,0,0,0.2)}.btn-twitter:active,.btn-twitter.active,.open>.dropdown-toggle.btn-twitter{color:#fff;background-color:#2795e9;border-color:rgba(0,0,0,0.2)}.btn-twitter:active,.btn-twitter.active,.open>.dropdown-toggle.btn-twitter{background-image:none}.btn-twitter .badge{color:#55acee;background-color:#fff}.btn-vimeo{color:#fff;background-color:#1ab7ea;border-color:rgba(0,0,0,0.2)}.btn-vimeo:focus,.btn-vimeo.focus{color:#fff;background-color:#1295bf;border-color:rgba(0,0,0,0.2)}.btn-vimeo:hover{color:#fff;background-color:#1295bf;border-color:rgba(0,0,0,0.2)}.btn-vimeo:active,.btn-vimeo.active,.open>.dropdown-toggle.btn-vimeo{color:#fff;background-color:#1295bf;border-color:rgba(0,0,0,0.2)}.btn-vimeo:active,.btn-vimeo.active,.open>.dropdown-toggle.btn-vimeo{background-image:none}.btn-vimeo .badge{color:#1ab7ea;background-color:#fff}.btn-vk{color:#fff;background-color:#587ea3;border-color:rgba(0,0,0,0.2)}.btn-vk:focus,.btn-vk.focus{color:#fff;background-color:#466482;border-color:rgba(0,0,0,0.2)}.btn-vk:hover{color:#fff;background-color:#466482;border-color:rgba(0,0,0,0.2)}.btn-vk:active,.btn-vk.active,.open>.dropdown-toggle.btn-vk{color:#fff;background-color:#466482;border-color:rgba(0,0,0,0.2)}.btn-vk:active,.btn-vk.active,.open>.dropdown-toggle.btn-vk{background-image:none}.btn-vk .badge{color:#587ea3;background-color:#fff}.btn-yahoo{color:#fff;background-color:#720e9e;border-color:rgba(0,0,0,0.2)}.btn-yahoo:focus,.btn-yahoo.focus{color:#fff;background-color:#500a6f;border-color:rgba(0,0,0,0.2)}.btn-yahoo:hover{color:#fff;background-color:#500a6f;border-color:rgba(0,0,0,0.2)}.btn-yahoo:active,.btn-yahoo.active,.open>.dropdown-toggle.btn-yahoo{color:#fff;background-color:#500a6f;border-color:rgba(0,0,0,0.2)}.btn-yahoo:active,.btn-yahoo.active,.open>.dropdown-toggle.btn-yahoo{background-image:none}.btn-yahoo .badge{color:#720e9e;background-color:#fff}.fc-button{background:#f4f4f4;background-image:none;color:#444;border-color:#ddd;border-bottom-color:#ddd}.fc-button:hover,.fc-button:active,.fc-button.hover{background-color:#e9e9e9}.fc-header-title h2{font-size:15px;line-height:1.6em;color:#666;margin-left:10px}.fc-header-right{padding-right:10px}.fc-header-left{padding-left:10px}.fc-widget-header{background:#fafafa}.fc-grid{width:100%;border:0}.fc-widget-header:first-of-type,.fc-widget-content:first-of-type{border-left:0;border-right:0}.fc-widget-header:last-of-type,.fc-widget-content:last-of-type{border-right:0}.fc-toolbar{padding:10px;margin:0}.fc-day-number{font-size:20px;font-weight:300;padding-right:10px}.fc-color-picker{list-style:none;margin:0;padding:0}.fc-color-picker>li{float:left;font-size:30px;margin-right:5px;line-height:30px}.fc-color-picker>li .fa{-webkit-transition:-webkit-transform linear .3s;-moz-transition:-moz-transform linear .3s;-o-transition:-o-transform linear .3s;transition:transform linear .3s}.fc-color-picker>li .fa:hover{-webkit-transform:rotate(30deg);-ms-transform:rotate(30deg);-o-transform:rotate(30deg);transform:rotate(30deg)}#add-new-event{-webkit-transition:all linear .3s;-o-transition:all linear .3s;transition:all linear .3s}.external-event{padding:5px 10px;font-weight:bold;margin-bottom:4px;box-shadow:0 1px 1px rgba(0,0,0,0.1);text-shadow:0 1px 1px rgba(0,0,0,0.1);border-radius:3px;cursor:move}.external-event:hover{box-shadow:inset 0 0 90px rgba(0,0,0,0.2)}.select2-container--default.select2-container--focus,.select2-selection.select2-container--focus,.select2-container--default:focus,.select2-selection:focus,.select2-container--default:active,.select2-selection:active{outline:none}.select2-container--default .select2-selection--single,.select2-selection .select2-selection--single{border:1px solid #d2d6de;border-radius:0;padding:6px 12px;height:34px}.select2-container--default.select2-container--open{border-color:#303f9f}.select2-dropdown{border:1px solid #d2d6de;border-radius:0}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#303f9f;color:white}.select2-results__option{padding:6px 12px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{padding-left:0;padding-right:0;height:auto;margin-top:-4px}.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered{padding-right:6px;padding-left:20px}.select2-container--default .select2-selection--single .select2-selection__arrow{height:28px;right:3px}.select2-container--default .select2-selection--single .select2-selection__arrow b{margin-top:0}.select2-dropdown .select2-search__field,.select2-search--inline .select2-search__field{border:1px solid #d2d6de}.select2-dropdown .select2-search__field:focus,.select2-search--inline .select2-search__field:focus{outline:none}.select2-container--default.select2-container--focus .select2-selection--multiple,.select2-container--default .select2-search--dropdown .select2-search__field{border-color:#303f9f !important}.select2-container--default .select2-results__option[aria-disabled=true]{color:#999}.select2-container--default .select2-results__option[aria-selected=true]{background-color:#ddd}.select2-container--default .select2-results__option[aria-selected=true],.select2-container--default .select2-results__option[aria-selected=true]:hover{color:#444}.select2-container--default .select2-selection--multiple{border:1px solid #d2d6de;border-radius:0}.select2-container--default .select2-selection--multiple:focus{border-color:#303f9f}.select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#d2d6de}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#303f9f;border-color:#2a378b;padding:1px 10px;color:#fff}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{margin-right:5px;color:rgba(255,255,255,0.7)}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container .select2-selection--single .select2-selection__rendered{padding-right:10px}.box .datepicker-inline,.box .datepicker-inline .datepicker-days,.box .datepicker-inline>table,.box .datepicker-inline .datepicker-days>table{width:100%}.box .datepicker-inline td:hover,.box .datepicker-inline .datepicker-days td:hover,.box .datepicker-inline>table td:hover,.box .datepicker-inline .datepicker-days>table td:hover{background-color:rgba(255,255,255,0.3)}.box .datepicker-inline td.day.old,.box .datepicker-inline .datepicker-days td.day.old,.box .datepicker-inline>table td.day.old,.box .datepicker-inline .datepicker-days>table td.day.old,.box .datepicker-inline td.day.new,.box .datepicker-inline .datepicker-days td.day.new,.box .datepicker-inline>table td.day.new,.box .datepicker-inline .datepicker-days>table td.day.new{color:#777}.pad{padding:10px}.margin{margin:10px}.margin-bottom{margin-bottom:20px}.margin-bottom-none{margin-bottom:0}.margin-r-5{margin-right:5px}.inline{display:inline}.description-block{display:block;margin:10px 0;text-align:center}.description-block.margin-bottom{margin-bottom:25px}.description-block>.description-header{margin:0;padding:0;font-weight:600;font-size:16px}.description-block>.description-text{text-transform:uppercase}.bg-red,.bg-yellow,.bg-aqua,.bg-blue,.bg-light-blue,.bg-green,.bg-navy,.bg-teal,.bg-olive,.bg-lime,.bg-orange,.bg-fuchsia,.bg-purple,.bg-maroon,.bg-black,.bg-red-active,.bg-yellow-active,.bg-aqua-active,.bg-blue-active,.bg-light-blue-active,.bg-green-active,.bg-navy-active,.bg-teal-active,.bg-olive-active,.bg-lime-active,.bg-orange-active,.bg-fuchsia-active,.bg-purple-active,.bg-maroon-active,.bg-black-active,.callout.callout-danger,.callout.callout-warning,.callout.callout-info,.callout.callout-success,.alert-success,.alert-danger,.alert-error,.alert-warning,.alert-info,.label-danger,.label-info,.label-warning,.label-primary,.label-success,.modal-primary .modal-body,.modal-primary .modal-header,.modal-primary .modal-footer,.modal-warning .modal-body,.modal-warning .modal-header,.modal-warning .modal-footer,.modal-info .modal-body,.modal-info .modal-header,.modal-info .modal-footer,.modal-success .modal-body,.modal-success .modal-header,.modal-success .modal-footer,.modal-danger .modal-body,.modal-danger .modal-header,.modal-danger .modal-footer{color:#fff !important}.bg-gray{color:#000;background-color:#d2d6de !important}.bg-gray-light{background-color:#f7f7f7}.bg-black{background-color:#111 !important}.bg-red,.callout.callout-danger,.alert-danger,.alert-error,.label-danger,.modal-danger .modal-body{background-color:#dd4b39 !important}.bg-yellow,.callout.callout-warning,.alert-warning,.label-warning,.modal-warning .modal-body{background-color:#f39c12 !important}.bg-aqua,.callout.callout-info,.alert-info,.label-info,.modal-info .modal-body{background-color:#00c0ef !important}.bg-blue{background-color:#303f9f !important}.bg-light-blue,.label-primary,.modal-primary .modal-body{background-color:#303f9f !important}.bg-green,.callout.callout-success,.alert-success,.label-success,.modal-success .modal-body{background-color:#00a65a !important}.bg-navy{background-color:#001f3f !important}.bg-teal{background-color:#39cccc !important}.bg-olive{background-color:#3d9970 !important}.bg-lime{background-color:#01ff70 !important}.bg-orange{background-color:#ff851b !important}.bg-fuchsia{background-color:#f012be !important}.bg-purple{background-color:#605ca8 !important}.bg-maroon{background-color:#d81b60 !important}.bg-gray-active{color:#000;background-color:#b5bbc8 !important}.bg-black-active{background-color:#000 !important}.bg-red-active,.modal-danger .modal-header,.modal-danger .modal-footer{background-color:#d33724 !important}.bg-yellow-active,.modal-warning .modal-header,.modal-warning .modal-footer{background-color:#db8b0b !important}.bg-aqua-active,.modal-info .modal-header,.modal-info .modal-footer{background-color:#00a7d0 !important}.bg-blue-active{background-color:#242f78 !important}.bg-light-blue-active,.modal-primary .modal-header,.modal-primary .modal-footer{background-color:#293687 !important}.bg-green-active,.modal-success .modal-header,.modal-success .modal-footer{background-color:#008d4c !important}.bg-navy-active{background-color:#001a35 !important}.bg-teal-active{background-color:#30bbbb !important}.bg-olive-active{background-color:#368763 !important}.bg-lime-active{background-color:#00e765 !important}.bg-orange-active{background-color:#ff7701 !important}.bg-fuchsia-active{background-color:#db0ead !important}.bg-purple-active{background-color:#555299 !important}.bg-maroon-active{background-color:#ca195a !important}[class^="bg-"].disabled{opacity:.65;filter:alpha(opacity=65)}.text-red{color:#dd4b39 !important}.text-yellow{color:#f39c12 !important}.text-aqua{color:#00c0ef !important}.text-blue{color:#303f9f !important}.text-black{color:#111 !important}.text-light-blue{color:#303f9f !important}.text-green{color:#00a65a !important}.text-gray{color:#d2d6de !important}.text-navy{color:#001f3f !important}.text-teal{color:#39cccc !important}.text-olive{color:#3d9970 !important}.text-lime{color:#01ff70 !important}.text-orange{color:#ff851b !important}.text-fuchsia{color:#f012be !important}.text-purple{color:#605ca8 !important}.text-maroon{color:#d81b60 !important}.link-muted{color:#7a869d}.link-muted:hover,.link-muted:focus{color:#606c84}.link-black{color:#666}.link-black:hover,.link-black:focus{color:#999}.hide{display:none !important}.no-border{border:0 !important}.no-padding{padding:0 !important}.no-margin{margin:0 !important}.no-shadow{box-shadow:none !important}.list-unstyled,.chart-legend,.contacts-list,.users-list,.mailbox-attachments{list-style:none;margin:0;padding:0}.list-group-unbordered>.list-group-item{border-left:0;border-right:0;border-radius:0;padding-left:0;padding-right:0}.flat{border-radius:0 !important}.text-bold,.text-bold.table td,.text-bold.table th{font-weight:700}.text-sm{font-size:12px}.jqstooltip{padding:5px !important;width:auto !important;height:auto !important}.bg-teal-gradient{background:#39cccc !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #39cccc), color-stop(1, #7adddd)) !important;background:-ms-linear-gradient(bottom, #39cccc, #7adddd) !important;background:-moz-linear-gradient(center bottom, #39cccc 0, #7adddd 100%) !important;background:-o-linear-gradient(#7adddd, #39cccc) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#7adddd', endColorstr='#39cccc', GradientType=0) !important;color:#fff}.bg-light-blue-gradient{background:#303f9f !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #303f9f), color-stop(1, #4557c7)) !important;background:-ms-linear-gradient(bottom, #303f9f, #4557c7) !important;background:-moz-linear-gradient(center bottom, #303f9f 0, #4557c7 100%) !important;background:-o-linear-gradient(#4557c7, #303f9f) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#4557c7', endColorstr='#303f9f', GradientType=0) !important;color:#fff}.bg-blue-gradient{background:#303f9f !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #303f9f), color-stop(1, #384aba)) !important;background:-ms-linear-gradient(bottom, #303f9f, #384aba) !important;background:-moz-linear-gradient(center bottom, #303f9f 0, #384aba 100%) !important;background:-o-linear-gradient(#384aba, #303f9f) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#384aba', endColorstr='#303f9f', GradientType=0) !important;color:#fff}.bg-aqua-gradient{background:#00c0ef !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #00c0ef), color-stop(1, #14d1ff)) !important;background:-ms-linear-gradient(bottom, #00c0ef, #14d1ff) !important;background:-moz-linear-gradient(center bottom, #00c0ef 0, #14d1ff 100%) !important;background:-o-linear-gradient(#14d1ff, #00c0ef) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#14d1ff', endColorstr='#00c0ef', GradientType=0) !important;color:#fff}.bg-yellow-gradient{background:#f39c12 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #f39c12), color-stop(1, #f7bc60)) !important;background:-ms-linear-gradient(bottom, #f39c12, #f7bc60) !important;background:-moz-linear-gradient(center bottom, #f39c12 0, #f7bc60 100%) !important;background:-o-linear-gradient(#f7bc60, #f39c12) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f7bc60', endColorstr='#f39c12', GradientType=0) !important;color:#fff}.bg-purple-gradient{background:#605ca8 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #605ca8), color-stop(1, #9491c4)) !important;background:-ms-linear-gradient(bottom, #605ca8, #9491c4) !important;background:-moz-linear-gradient(center bottom, #605ca8 0, #9491c4 100%) !important;background:-o-linear-gradient(#9491c4, #605ca8) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#9491c4', endColorstr='#605ca8', GradientType=0) !important;color:#fff}.bg-green-gradient{background:#00a65a !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #00a65a), color-stop(1, #00ca6d)) !important;background:-ms-linear-gradient(bottom, #00a65a, #00ca6d) !important;background:-moz-linear-gradient(center bottom, #00a65a 0, #00ca6d 100%) !important;background:-o-linear-gradient(#00ca6d, #00a65a) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ca6d', endColorstr='#00a65a', GradientType=0) !important;color:#fff}.bg-red-gradient{background:#dd4b39 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #dd4b39), color-stop(1, #e47365)) !important;background:-ms-linear-gradient(bottom, #dd4b39, #e47365) !important;background:-moz-linear-gradient(center bottom, #dd4b39 0, #e47365 100%) !important;background:-o-linear-gradient(#e47365, #dd4b39) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#e47365', endColorstr='#dd4b39', GradientType=0) !important;color:#fff}.bg-black-gradient{background:#111 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #111), color-stop(1, #2b2b2b)) !important;background:-ms-linear-gradient(bottom, #111, #2b2b2b) !important;background:-moz-linear-gradient(center bottom, #111 0, #2b2b2b 100%) !important;background:-o-linear-gradient(#2b2b2b, #111) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#2b2b2b', endColorstr='#111111', GradientType=0) !important;color:#fff}.bg-maroon-gradient{background:#d81b60 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #d81b60), color-stop(1, #e73f7c)) !important;background:-ms-linear-gradient(bottom, #d81b60, #e73f7c) !important;background:-moz-linear-gradient(center bottom, #d81b60 0, #e73f7c 100%) !important;background:-o-linear-gradient(#e73f7c, #d81b60) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#e73f7c', endColorstr='#d81b60', GradientType=0) !important;color:#fff}.description-block .description-icon{font-size:16px}.no-pad-top{padding-top:0}.position-static{position:static !important}.list-header{font-size:15px;padding:10px 4px;font-weight:bold;color:#666}.list-seperator{height:1px;background:#f4f4f4;margin:15px 0 9px 0}.list-link>a{padding:4px;color:#777}.list-link>a:hover{color:#222}.font-light{font-weight:300}.user-block:before,.user-block:after{content:" ";display:table}.user-block:after{clear:both}.user-block img{width:40px;height:40px;float:left}.user-block .username,.user-block .description,.user-block .comment{display:block;margin-left:50px}.user-block .username{font-size:16px;font-weight:600}.user-block .description{color:#999;font-size:13px}.user-block.user-block-sm .username,.user-block.user-block-sm .description,.user-block.user-block-sm .comment{margin-left:40px}.user-block.user-block-sm .username{font-size:14px}.img-sm,.img-md,.img-lg,.box-comments .box-comment img,.user-block.user-block-sm img{float:left}.img-sm,.box-comments .box-comment img,.user-block.user-block-sm img{width:30px !important;height:30px !important}.img-sm+.img-push{margin-left:40px}.img-md{width:60px;height:60px}.img-md+.img-push{margin-left:70px}.img-lg{width:100px;height:100px}.img-lg+.img-push{margin-left:110px}.img-bordered{border:3px solid #d2d6de;padding:3px}.img-bordered-sm{border:2px solid #d2d6de;padding:2px}.attachment-block{border:1px solid #f4f4f4;padding:5px;margin-bottom:10px;background:#f7f7f7}.attachment-block .attachment-img{max-width:100px;max-height:100px;height:auto;float:left}.attachment-block .attachment-pushed{margin-left:110px}.attachment-block .attachment-heading{margin:0}.attachment-block .attachment-text{color:#555}.connectedSortable{min-height:100px}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sort-highlight{background:#f4f4f4;border:1px dashed #ddd;margin-bottom:10px}.full-opacity-hover{opacity:.65;filter:alpha(opacity=65)}.full-opacity-hover:hover{opacity:1;filter:alpha(opacity=100)}.chart{position:relative;overflow:hidden;width:100%}.chart svg,.chart canvas{width:100% !important}@media print{.no-print,.main-sidebar,.left-side,.main-header,.content-header{display:none !important}.content-wrapper,.right-side,.main-footer{margin-left:0 !important;min-height:0 !important;-webkit-transform:translate(0, 0) !important;-ms-transform:translate(0, 0) !important;-o-transform:translate(0, 0) !important;transform:translate(0, 0) !important}.fixed .content-wrapper,.fixed .right-side{padding-top:0 !important}.invoice{width:100%;border:0;margin:0;padding:0}.invoice-col{float:left;width:33.3333333%}.table-responsive{overflow:auto}.table-responsive>.table tr th,.table-responsive>.table tr td{white-space:normal !important}} diff --git a/public/themes/pterodactyl/vendor/adminlte/app.min.js b/public/themes/pterodactyl/vendor/adminlte/app.min.js index 7efb107e..c779c0c4 100755 --- a/public/themes/pterodactyl/vendor/adminlte/app.min.js +++ b/public/themes/pterodactyl/vendor/adminlte/app.min.js @@ -1,13 +1,14 @@ /*! AdminLTE app.js - * ================ - * Main JS application file for AdminLTE v2. This file - * should be included in all pages. It controls some layout - * options and implements exclusive AdminLTE plugins. - * - * @Author Almsaeed Studio - * @Support - * @Email - * @version 2.3.8 - * @license MIT - */ -function _init(){"use strict";$.AdminLTE.layout={activate:function(){var a=this;a.fix(),a.fixSidebar(),$("body, html, .wrapper").css("height","auto"),$(window,".wrapper").resize(function(){a.fix(),a.fixSidebar()})},fix:function(){$(".layout-boxed > .wrapper").css("overflow","hidden");var a=$(".main-footer").outerHeight()||0,b=$(".main-header").outerHeight()+a,c=$(window).height(),d=$(".sidebar").height()||0;if($("body").hasClass("fixed"))$(".content-wrapper, .right-side").css("min-height",c-a);else{var e;c>=d?($(".content-wrapper, .right-side").css("min-height",c-b),e=c-b):($(".content-wrapper, .right-side").css("min-height",d),e=d);var f=$($.AdminLTE.options.controlSidebarOptions.selector);"undefined"!=typeof f&&f.height()>e&&$(".content-wrapper, .right-side").css("min-height",f.height())}},fixSidebar:function(){return $("body").hasClass("fixed")?("undefined"==typeof $.fn.slimScroll&&window.console&&window.console.error("Error: the fixed layout requires the slimscroll plugin!"),void($.AdminLTE.options.sidebarSlimScroll&&"undefined"!=typeof $.fn.slimScroll&&($(".sidebar").slimScroll({destroy:!0}).height("auto"),$(".sidebar").slimScroll({height:$(window).height()-$(".main-header").height()+"px",color:"rgba(0,0,0,0.2)",size:"3px"})))):void("undefined"!=typeof $.fn.slimScroll&&$(".sidebar").slimScroll({destroy:!0}).height("auto"))}},$.AdminLTE.pushMenu={activate:function(a){var b=$.AdminLTE.options.screenSizes;$(document).on("click",a,function(a){a.preventDefault(),$(window).width()>b.sm-1?$("body").hasClass("sidebar-collapse")?$("body").removeClass("sidebar-collapse").trigger("expanded.pushMenu"):$("body").addClass("sidebar-collapse").trigger("collapsed.pushMenu"):$("body").hasClass("sidebar-open")?$("body").removeClass("sidebar-open").removeClass("sidebar-collapse").trigger("collapsed.pushMenu"):$("body").addClass("sidebar-open").trigger("expanded.pushMenu")}),$(".content-wrapper").click(function(){$(window).width()<=b.sm-1&&$("body").hasClass("sidebar-open")&&$("body").removeClass("sidebar-open")}),($.AdminLTE.options.sidebarExpandOnHover||$("body").hasClass("fixed")&&$("body").hasClass("sidebar-mini"))&&this.expandOnHover()},expandOnHover:function(){var a=this,b=$.AdminLTE.options.screenSizes.sm-1;$(".main-sidebar").hover(function(){$("body").hasClass("sidebar-mini")&&$("body").hasClass("sidebar-collapse")&&$(window).width()>b&&a.expand()},function(){$("body").hasClass("sidebar-mini")&&$("body").hasClass("sidebar-expanded-on-hover")&&$(window).width()>b&&a.collapse()})},expand:function(){$("body").removeClass("sidebar-collapse").addClass("sidebar-expanded-on-hover")},collapse:function(){$("body").hasClass("sidebar-expanded-on-hover")&&$("body").removeClass("sidebar-expanded-on-hover").addClass("sidebar-collapse")}},$.AdminLTE.tree=function(a){var b=this,c=$.AdminLTE.options.animationSpeed;$(document).off("click",a+" li a").on("click",a+" li a",function(a){var d=$(this),e=d.next();if(e.is(".treeview-menu")&&e.is(":visible")&&!$("body").hasClass("sidebar-collapse"))e.slideUp(c,function(){e.removeClass("menu-open")}),e.parent("li").removeClass("active");else if(e.is(".treeview-menu")&&!e.is(":visible")){var f=d.parents("ul").first(),g=f.find("ul:visible").slideUp(c);g.removeClass("menu-open");var h=d.parent("li");e.slideDown(c,function(){e.addClass("menu-open"),f.find("li.active").removeClass("active"),h.addClass("active"),b.layout.fix()})}e.is(".treeview-menu")&&a.preventDefault()})},$.AdminLTE.controlSidebar={activate:function(){var a=this,b=$.AdminLTE.options.controlSidebarOptions,c=$(b.selector),d=$(b.toggleBtnSelector);d.on("click",function(d){d.preventDefault(),c.hasClass("control-sidebar-open")||$("body").hasClass("control-sidebar-open")?a.close(c,b.slide):a.open(c,b.slide)});var e=$(".control-sidebar-bg");a._fix(e),$("body").hasClass("fixed")?a._fixForFixed(c):$(".content-wrapper, .right-side").height() .box-body, > .box-footer, > form >.box-body, > form > .box-footer");c.hasClass("collapsed-box")?(a.children(":first").removeClass(b.icons.open).addClass(b.icons.collapse),d.slideDown(b.animationSpeed,function(){c.removeClass("collapsed-box")})):(a.children(":first").removeClass(b.icons.collapse).addClass(b.icons.open),d.slideUp(b.animationSpeed,function(){c.addClass("collapsed-box")}))},remove:function(a){var b=a.parents(".box").first();b.slideUp(this.animationSpeed)}}}if("undefined"==typeof jQuery)throw new Error("AdminLTE requires jQuery");$.AdminLTE={},$.AdminLTE.options={navbarMenuSlimscroll:!0,navbarMenuSlimscrollWidth:"3px",navbarMenuHeight:"200px",animationSpeed:500,sidebarToggleSelector:"[data-toggle='offcanvas']",sidebarPushMenu:!0,sidebarSlimScroll:!0,sidebarExpandOnHover:!1,enableBoxRefresh:!0,enableBSToppltip:!0,BSTooltipSelector:"[data-toggle='tooltip']",enableFastclick:!1,enableControlTreeView:!0,enableControlSidebar:!0,controlSidebarOptions:{toggleBtnSelector:"[data-action='control-sidebar']",selector:".control-sidebar",slide:!0},enableBoxWidget:!0,boxWidgetOptions:{boxWidgetIcons:{collapse:"fa-minus",open:"fa-plus",remove:"fa-times"},boxWidgetSelectors:{remove:'[data-widget="remove"]',collapse:'[data-widget="collapse"]'}},directChat:{enable:!0,contactToggleSelector:'[data-widget="chat-pane-toggle"]'},colors:{lightBlue:"#3c8dbc",red:"#f56954",green:"#00a65a",aqua:"#00c0ef",yellow:"#f39c12",blue:"#0073b7",navy:"#001F3F",teal:"#39CCCC",olive:"#3D9970",lime:"#01FF70",orange:"#FF851B",fuchsia:"#F012BE",purple:"#8E24AA",maroon:"#D81B60",black:"#222222",gray:"#d2d6de"},screenSizes:{xs:480,sm:768,md:992,lg:1200}},$(function(){"use strict";$("body").removeClass("hold-transition"),"undefined"!=typeof AdminLTEOptions&&$.extend(!0,$.AdminLTE.options,AdminLTEOptions);var a=$.AdminLTE.options;_init(),$.AdminLTE.layout.activate(),a.enableControlTreeView&&$.AdminLTE.tree(".sidebar"),a.enableControlSidebar&&$.AdminLTE.controlSidebar.activate(),a.navbarMenuSlimscroll&&"undefined"!=typeof $.fn.slimscroll&&$(".navbar .menu").slimscroll({height:a.navbarMenuHeight,alwaysVisible:!1,size:a.navbarMenuSlimscrollWidth}).css("width","100%"),a.sidebarPushMenu&&$.AdminLTE.pushMenu.activate(a.sidebarToggleSelector),a.enableBSToppltip&&$("body").tooltip({selector:a.BSTooltipSelector,container:"body"}),a.enableBoxWidget&&$.AdminLTE.boxWidget.activate(),a.enableFastclick&&"undefined"!=typeof FastClick&&FastClick.attach(document.body),a.directChat.enable&&$(document).on("click",a.directChat.contactToggleSelector,function(){var a=$(this).parents(".direct-chat").first();a.toggleClass("direct-chat-contacts-open")}),$('.btn-group[data-toggle="btn-toggle"]').each(function(){var a=$(this);$(this).find(".btn").on("click",function(b){a.find(".btn.active").removeClass("active"),$(this).addClass("active"),b.preventDefault()})})}),function(a){"use strict";a.fn.boxRefresh=function(b){function c(a){a.append(f),e.onLoadStart.call(a)}function d(a){a.find(f).remove(),e.onLoadDone.call(a)}var e=a.extend({trigger:".refresh-btn",source:"",onLoadStart:function(a){return a},onLoadDone:function(a){return a}},b),f=a('
    ');return this.each(function(){if(""===e.source)return void(window.console&&window.console.log("Please specify a source first - boxRefresh()"));var b=a(this),f=b.find(e.trigger).first();f.on("click",function(a){a.preventDefault(),c(b),b.find(".box-body").load(e.source,function(){d(b)})})})}}(jQuery),function(a){"use strict";a.fn.activateBox=function(){a.AdminLTE.boxWidget.activate(this)},a.fn.toggleBox=function(){var b=a(a.AdminLTE.boxWidget.selectors.collapse,this);a.AdminLTE.boxWidget.collapse(b)},a.fn.removeBox=function(){var b=a(a.AdminLTE.boxWidget.selectors.remove,this);a.AdminLTE.boxWidget.remove(b)}}(jQuery),function(a){"use strict";a.fn.todolist=function(b){var c=a.extend({onCheck:function(a){return a},onUncheck:function(a){return a}},b);return this.each(function(){"undefined"!=typeof a.fn.iCheck?(a("input",this).on("ifChecked",function(){var b=a(this).parents("li").first();b.toggleClass("done"),c.onCheck.call(b)}),a("input",this).on("ifUnchecked",function(){var b=a(this).parents("li").first();b.toggleClass("done"),c.onUncheck.call(b)})):a("input",this).on("change",function(){var b=a(this).parents("li").first();b.toggleClass("done"),a("input",b).is(":checked")?c.onCheck.call(b):c.onUncheck.call(b)})})}}(jQuery); \ No newline at end of file +* ================ +* Main JS application file for AdminLTE v2. This file +* should be included in all pages. It controls some layout +* options and implements exclusive AdminLTE plugins. +* +* @Author Almsaeed Studio +* @Support +* @Email +* @version 2.4.0 +* @repository git://github.com/almasaeed2010/AdminLTE.git +* @license MIT +*/ +if("undefined"==typeof jQuery)throw new Error("AdminLTE requires jQuery");+function(a){"use strict";function b(b){return this.each(function(){var e=a(this),g=e.data(c);if(!g){var h=a.extend({},d,e.data(),"object"==typeof b&&b);e.data(c,g=new f(e,h))}if("string"==typeof g){if(void 0===g[b])throw new Error("No method named "+b);g[b]()}})}var c="lte.boxrefresh",d={source:"",params:{},trigger:".refresh-btn",content:".box-body",loadInContent:!0,responseType:"",overlayTemplate:'
    ',onLoadStart:function(){},onLoadDone:function(a){return a}},e={data:'[data-widget="box-refresh"]'},f=function(b,c){if(this.element=b,this.options=c,this.$overlay=a(c.overlay),""===c.source)throw new Error("Source url was not defined. Please specify a url in your BoxRefresh source option.");this._setUpListeners(),this.load()};f.prototype.load=function(){this._addOverlay(),this.options.onLoadStart.call(a(this)),a.get(this.options.source,this.options.params,function(b){this.options.loadInContent&&a(this.options.content).html(b),this.options.onLoadDone.call(a(this),b),this._removeOverlay()}.bind(this),""!==this.options.responseType&&this.options.responseType)},f.prototype._setUpListeners=function(){a(this.element).on("click",e.trigger,function(a){a&&a.preventDefault(),this.load()}.bind(this))},f.prototype._addOverlay=function(){a(this.element).append(this.$overlay)},f.prototype._removeOverlay=function(){a(this.element).remove(this.$overlay)};var g=a.fn.boxRefresh;a.fn.boxRefresh=b,a.fn.boxRefresh.Constructor=f,a.fn.boxRefresh.noConflict=function(){return a.fn.boxRefresh=g,this},a(window).on("load",function(){a(e.data).each(function(){b.call(a(this))})})}(jQuery),function(a){"use strict";function b(b){return this.each(function(){var e=a(this),f=e.data(c);if(!f){var g=a.extend({},d,e.data(),"object"==typeof b&&b);e.data(c,f=new h(e,g))}if("string"==typeof b){if(void 0===f[b])throw new Error("No method named "+b);f[b]()}})}var c="lte.boxwidget",d={animationSpeed:500,collapseTrigger:'[data-widget="collapse"]',removeTrigger:'[data-widget="remove"]',collapseIcon:"fa-minus",expandIcon:"fa-plus",removeIcon:"fa-times"},e={data:".box",collapsed:".collapsed-box",body:".box-body",footer:".box-footer",tools:".box-tools"},f={collapsed:"collapsed-box"},g={collapsed:"collapsed.boxwidget",expanded:"expanded.boxwidget",removed:"removed.boxwidget"},h=function(a,b){this.element=a,this.options=b,this._setUpListeners()};h.prototype.toggle=function(){a(this.element).is(e.collapsed)?this.expand():this.collapse()},h.prototype.expand=function(){var b=a.Event(g.expanded),c=this.options.collapseIcon,d=this.options.expandIcon;a(this.element).removeClass(f.collapsed),a(this.element).find(e.tools).find("."+d).removeClass(d).addClass(c),a(this.element).find(e.body+", "+e.footer).slideDown(this.options.animationSpeed,function(){a(this.element).trigger(b)}.bind(this))},h.prototype.collapse=function(){var b=a.Event(g.collapsed),c=this.options.collapseIcon,d=this.options.expandIcon;a(this.element).find(e.tools).find("."+c).removeClass(c).addClass(d),a(this.element).find(e.body+", "+e.footer).slideUp(this.options.animationSpeed,function(){a(this.element).addClass(f.collapsed),a(this.element).trigger(b)}.bind(this))},h.prototype.remove=function(){var b=a.Event(g.removed);a(this.element).slideUp(this.options.animationSpeed,function(){a(this.element).trigger(b),a(this.element).remove()}.bind(this))},h.prototype._setUpListeners=function(){var b=this;a(this.element).on("click",this.options.collapseTrigger,function(a){a&&a.preventDefault(),b.toggle()}),a(this.element).on("click",this.options.removeTrigger,function(a){a&&a.preventDefault(),b.remove()})};var i=a.fn.boxWidget;a.fn.boxWidget=b,a.fn.boxWidget.Constructor=h,a.fn.boxWidget.noConflict=function(){return a.fn.boxWidget=i,this},a(window).on("load",function(){a(e.data).each(function(){b.call(a(this))})})}(jQuery),function(a){"use strict";function b(b){return this.each(function(){var e=a(this),f=e.data(c);if(!f){var g=a.extend({},d,e.data(),"object"==typeof b&&b);e.data(c,f=new h(e,g))}"string"==typeof b&&f.toggle()})}var c="lte.controlsidebar",d={slide:!0},e={sidebar:".control-sidebar",data:'[data-toggle="control-sidebar"]',open:".control-sidebar-open",bg:".control-sidebar-bg",wrapper:".wrapper",content:".content-wrapper",boxed:".layout-boxed"},f={open:"control-sidebar-open",fixed:"fixed"},g={collapsed:"collapsed.controlsidebar",expanded:"expanded.controlsidebar"},h=function(a,b){this.element=a,this.options=b,this.hasBindedResize=!1,this.init()};h.prototype.init=function(){a(this.element).is(e.data)||a(this).on("click",this.toggle),this.fix(),a(window).resize(function(){this.fix()}.bind(this))},h.prototype.toggle=function(b){b&&b.preventDefault(),this.fix(),a(e.sidebar).is(e.open)||a("body").is(e.open)?this.collapse():this.expand()},h.prototype.expand=function(){this.options.slide?a(e.sidebar).addClass(f.open):a("body").addClass(f.open),a(this.element).trigger(a.Event(g.expanded))},h.prototype.collapse=function(){a("body, "+e.sidebar).removeClass(f.open),a(this.element).trigger(a.Event(g.collapsed))},h.prototype.fix=function(){a("body").is(e.boxed)&&this._fixForBoxed(a(e.bg))},h.prototype._fixForBoxed=function(b){b.css({position:"absolute",height:a(e.wrapper).height()})};var i=a.fn.controlSidebar;a.fn.controlSidebar=b,a.fn.controlSidebar.Constructor=h,a.fn.controlSidebar.noConflict=function(){return a.fn.controlSidebar=i,this},a(document).on("click",e.data,function(c){c&&c.preventDefault(),b.call(a(this),"toggle")})}(jQuery),function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data(c);e||d.data(c,e=new f(d)),"string"==typeof b&&e.toggle(d)})}var c="lte.directchat",d={data:'[data-widget="chat-pane-toggle"]',box:".direct-chat"},e={open:"direct-chat-contacts-open"},f=function(a){this.element=a};f.prototype.toggle=function(a){a.parents(d.box).first().toggleClass(e.open)};var g=a.fn.directChat;a.fn.directChat=b,a.fn.directChat.Constructor=f,a.fn.directChat.noConflict=function(){return a.fn.directChat=g,this},a(document).on("click",d.data,function(c){c&&c.preventDefault(),b.call(a(this),"toggle")})}(jQuery),function(a){"use strict";function b(b){return this.each(function(){var e=a(this),f=e.data(c);if(!f){var h=a.extend({},d,e.data(),"object"==typeof b&&b);e.data(c,f=new g(h))}if("string"==typeof b){if(void 0===f[b])throw new Error("No method named "+b);f[b]()}})}var c="lte.layout",d={slimscroll:!0,resetHeight:!0},e={wrapper:".wrapper",contentWrapper:".content-wrapper",layoutBoxed:".layout-boxed",mainFooter:".main-footer",mainHeader:".main-header",sidebar:".sidebar",controlSidebar:".control-sidebar",fixed:".fixed",sidebarMenu:".sidebar-menu",logo:".main-header .logo"},f={fixed:"fixed",holdTransition:"hold-transition"},g=function(a){this.options=a,this.bindedResize=!1,this.activate()};g.prototype.activate=function(){this.fix(),this.fixSidebar(),a("body").removeClass(f.holdTransition),this.options.resetHeight&&a("body, html, "+e.wrapper).css({height:"auto","min-height":"100%"}),this.bindedResize||(a(window).resize(function(){this.fix(),this.fixSidebar(),a(e.logo+", "+e.sidebar).one("webkitTransitionEnd otransitionend oTransitionEnd msTransitionEnd transitionend",function(){this.fix(),this.fixSidebar()}.bind(this))}.bind(this)),this.bindedResize=!0),a(e.sidebarMenu).on("expanded.tree",function(){this.fix(),this.fixSidebar()}.bind(this)),a(e.sidebarMenu).on("collapsed.tree",function(){this.fix(),this.fixSidebar()}.bind(this))},g.prototype.fix=function(){a(e.layoutBoxed+" > "+e.wrapper).css("overflow","hidden");var b=a(e.mainFooter).outerHeight()||0,c=a(e.mainHeader).outerHeight()+b,d=a(window).height(),g=a(e.sidebar).height()||0;if(a("body").hasClass(f.fixed))a(e.contentWrapper).css("min-height",d-b);else{var h;d>=g?(a(e.contentWrapper).css("min-height",d-c),h=d-c):(a(e.contentWrapper).css("min-height",g),h=g);var i=a(e.controlSidebar);void 0!==i&&i.height()>h&&a(e.contentWrapper).css("min-height",i.height())}},g.prototype.fixSidebar=function(){if(!a("body").hasClass(f.fixed))return void(void 0!==a.fn.slimScroll&&a(e.sidebar).slimScroll({destroy:!0}).height("auto"));this.options.slimscroll&&void 0!==a.fn.slimScroll&&(a(e.sidebar).slimScroll({destroy:!0}).height("auto"),a(e.sidebar).slimScroll({height:a(window).height()-a(e.mainHeader).height()+"px",color:"rgba(0,0,0,0.2)",size:"3px"}))};var h=a.fn.layout;a.fn.layout=b,a.fn.layout.Constuctor=g,a.fn.layout.noConflict=function(){return a.fn.layout=h,this},a(window).on("load",function(){b.call(a("body"))})}(jQuery),function(a){"use strict";function b(b){return this.each(function(){var e=a(this),f=e.data(c);if(!f){var g=a.extend({},d,e.data(),"object"==typeof b&&b);e.data(c,f=new h(g))}"toggle"==b&&f.toggle()})}var c="lte.pushmenu",d={collapseScreenSize:767,expandOnHover:!1,expandTransitionDelay:200},e={collapsed:".sidebar-collapse",open:".sidebar-open",mainSidebar:".main-sidebar",contentWrapper:".content-wrapper",searchInput:".sidebar-form .form-control",button:'[data-toggle="push-menu"]',mini:".sidebar-mini",expanded:".sidebar-expanded-on-hover",layoutFixed:".fixed"},f={collapsed:"sidebar-collapse",open:"sidebar-open",mini:"sidebar-mini",expanded:"sidebar-expanded-on-hover",expandFeature:"sidebar-mini-expand-feature",layoutFixed:"fixed"},g={expanded:"expanded.pushMenu",collapsed:"collapsed.pushMenu"},h=function(a){this.options=a,this.init()};h.prototype.init=function(){(this.options.expandOnHover||a("body").is(e.mini+e.layoutFixed))&&(this.expandOnHover(),a("body").addClass(f.expandFeature)),a(e.contentWrapper).click(function(){a(window).width()<=this.options.collapseScreenSize&&a("body").hasClass(f.open)&&this.close()}.bind(this)),a(e.searchInput).click(function(a){a.stopPropagation()})},h.prototype.toggle=function(){var b=a(window).width(),c=!a("body").hasClass(f.collapsed);b<=this.options.collapseScreenSize&&(c=a("body").hasClass(f.open)),c?this.close():this.open()},h.prototype.open=function(){a(window).width()>this.options.collapseScreenSize?a("body").removeClass(f.collapsed).trigger(a.Event(g.expanded)):a("body").addClass(f.open).trigger(a.Event(g.expanded))},h.prototype.close=function(){a(window).width()>this.options.collapseScreenSize?a("body").addClass(f.collapsed).trigger(a.Event(g.collapsed)):a("body").removeClass(f.open+" "+f.collapsed).trigger(a.Event(g.collapsed))},h.prototype.expandOnHover=function(){a(e.mainSidebar).hover(function(){a("body").is(e.mini+e.collapsed)&&a(window).width()>this.options.collapseScreenSize&&this.expand()}.bind(this),function(){a("body").is(e.expanded)&&this.collapse()}.bind(this))},h.prototype.expand=function(){setTimeout(function(){a("body").removeClass(f.collapsed).addClass(f.expanded)},this.options.expandTransitionDelay)},h.prototype.collapse=function(){setTimeout(function(){a("body").removeClass(f.expanded).addClass(f.collapsed)},this.options.expandTransitionDelay)};var i=a.fn.pushMenu;a.fn.pushMenu=b,a.fn.pushMenu.Constructor=h,a.fn.pushMenu.noConflict=function(){return a.fn.pushMenu=i,this},a(document).on("click",e.button,function(c){c.preventDefault(),b.call(a(this),"toggle")}),a(window).on("load",function(){b.call(a(e.button))})}(jQuery),function(a){"use strict";function b(b){return this.each(function(){var e=a(this),f=e.data(c);if(!f){var h=a.extend({},d,e.data(),"object"==typeof b&&b);e.data(c,f=new g(e,h))}if("string"==typeof f){if(void 0===f[b])throw new Error("No method named "+b);f[b]()}})}var c="lte.todolist",d={onCheck:function(a){return a},onUnCheck:function(a){return a}},e={data:'[data-widget="todo-list"]'},f={done:"done"},g=function(a,b){this.element=a,this.options=b,this._setUpListeners()};g.prototype.toggle=function(a){if(a.parents(e.li).first().toggleClass(f.done),!a.prop("checked"))return void this.unCheck(a);this.check(a)},g.prototype.check=function(a){this.options.onCheck.call(a)},g.prototype.unCheck=function(a){this.options.onUnCheck.call(a)},g.prototype._setUpListeners=function(){var b=this;a(this.element).on("change ifChanged","input:checkbox",function(){b.toggle(a(this))})};var h=a.fn.todoList;a.fn.todoList=b,a.fn.todoList.Constructor=g,a.fn.todoList.noConflict=function(){return a.fn.todoList=h,this},a(window).on("load",function(){a(e.data).each(function(){b.call(a(this))})})}(jQuery),function(a){"use strict";function b(b){return this.each(function(){var e=a(this);if(!e.data(c)){var f=a.extend({},d,e.data(),"object"==typeof b&&b);e.data(c,new h(e,f))}})}var c="lte.tree",d={animationSpeed:500,accordion:!0,followLink:!1,trigger:".treeview a"},e={tree:".tree",treeview:".treeview",treeviewMenu:".treeview-menu",open:".menu-open, .active",li:"li",data:'[data-widget="tree"]',active:".active"},f={open:"menu-open",tree:"tree"},g={collapsed:"collapsed.tree",expanded:"expanded.tree"},h=function(b,c){this.element=b,this.options=c,a(this.element).addClass(f.tree),a(e.treeview+e.active,this.element).addClass(f.open),this._setUpListeners()};h.prototype.toggle=function(a,b){var c=a.next(e.treeviewMenu),d=a.parent(),g=d.hasClass(f.open);d.is(e.treeview)&&(this.options.followLink&&"#"!=a.attr("href")||b.preventDefault(),g?this.collapse(c,d):this.expand(c,d))},h.prototype.expand=function(b,c){var d=a.Event(g.expanded);if(this.options.accordion){var h=c.siblings(e.open),i=h.children(e.treeviewMenu);this.collapse(i,h)}c.addClass(f.open),b.slideDown(this.options.animationSpeed,function(){a(this.element).trigger(d)}.bind(this))},h.prototype.collapse=function(b,c){var d=a.Event(g.collapsed);b.find(e.open).removeClass(f.open),c.removeClass(f.open),b.slideUp(this.options.animationSpeed,function(){b.find(e.open+" > "+e.treeview).slideUp(),a(this.element).trigger(d)}.bind(this))},h.prototype._setUpListeners=function(){var b=this;a(this.element).on("click",this.options.trigger,function(c){b.toggle(a(this),c)})};var i=a.fn.tree;a.fn.tree=b,a.fn.tree.Constructor=h,a.fn.tree.noConflict=function(){return a.fn.tree=i,this},a(window).on("load",function(){a(e.data).each(function(){b.call(a(this))})})}(jQuery); diff --git a/public/themes/pterodactyl/vendor/adminlte/colors/skin-blue.min.css b/public/themes/pterodactyl/vendor/adminlte/colors/skin-blue.min.css index 44524fe3..172d3dfb 100755 --- a/public/themes/pterodactyl/vendor/adminlte/colors/skin-blue.min.css +++ b/public/themes/pterodactyl/vendor/adminlte/colors/skin-blue.min.css @@ -1 +1 @@ -.skin-blue .main-header .navbar{background-color:#3c8dbc}.skin-blue .main-header .navbar .nav>li>a{color:#fff}.skin-blue .main-header .navbar .nav>li>a:hover,.skin-blue .main-header .navbar .nav>li>a:active,.skin-blue .main-header .navbar .nav>li>a:focus,.skin-blue .main-header .navbar .nav .open>a,.skin-blue .main-header .navbar .nav .open>a:hover,.skin-blue .main-header .navbar .nav .open>a:focus,.skin-blue .main-header .navbar .nav>.active>a{background:rgba(0,0,0,0.1);color:#f6f6f6}.skin-blue .main-header .navbar .sidebar-toggle{color:#fff}.skin-blue .main-header .navbar .sidebar-toggle:hover{color:#f6f6f6;background:rgba(0,0,0,0.1)}.skin-blue .main-header .navbar .sidebar-toggle{color:#fff}.skin-blue .main-header .navbar .sidebar-toggle:hover{background-color:#367fa9}@media (max-width:767px){.skin-blue .main-header .navbar .dropdown-menu li.divider{background-color:rgba(255,255,255,0.1)}.skin-blue .main-header .navbar .dropdown-menu li a{color:#fff}.skin-blue .main-header .navbar .dropdown-menu li a:hover{background:#367fa9}}.skin-blue .main-header .logo{background-color:#367fa9;color:#fff;border-bottom:0 solid transparent}.skin-blue .main-header .logo:hover{background-color:#357ca5}.skin-blue .main-header li.user-header{background-color:#3c8dbc}.skin-blue .content-header{background:transparent}.skin-blue .wrapper,.skin-blue .main-sidebar,.skin-blue .left-side{background-color:#222d32}.skin-blue .user-panel>.info,.skin-blue .user-panel>.info>a{color:#fff}.skin-blue .sidebar-menu>li.header{color:#4b646f;background:#1a2226}.skin-blue .sidebar-menu>li>a{border-left:3px solid transparent}.skin-blue .sidebar-menu>li:hover>a,.skin-blue .sidebar-menu>li.active>a{color:#fff;background:#1e282c;border-left-color:#3c8dbc}.skin-blue .sidebar-menu>li>.treeview-menu{margin:0 1px;background:#2c3b41}.skin-blue .sidebar a{color:#b8c7ce}.skin-blue .sidebar a:hover{text-decoration:none}.skin-blue .treeview-menu>li>a{color:#8aa4af}.skin-blue .treeview-menu>li.active>a,.skin-blue .treeview-menu>li>a:hover{color:#fff}.skin-blue .sidebar-form{border-radius:3px;border:1px solid #374850;margin:10px 10px}.skin-blue .sidebar-form input[type="text"],.skin-blue .sidebar-form .btn{box-shadow:none;background-color:#374850;border:1px solid transparent;height:35px}.skin-blue .sidebar-form input[type="text"]{color:#666;border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px}.skin-blue .sidebar-form input[type="text"]:focus,.skin-blue .sidebar-form input[type="text"]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.skin-blue .sidebar-form input[type="text"]:focus+.input-group-btn .btn{border-left-color:#fff}.skin-blue .sidebar-form .btn{color:#999;border-top-left-radius:0;border-top-right-radius:2px;border-bottom-right-radius:2px;border-bottom-left-radius:0}.skin-blue.layout-top-nav .main-header>.logo{background-color:#3c8dbc;color:#fff;border-bottom:0 solid transparent}.skin-blue.layout-top-nav .main-header>.logo:hover{background-color:#3b8ab8} \ No newline at end of file +.skin-blue .main-header .navbar{background-color:#303f9f}.skin-blue .main-header .navbar .nav>li>a{color:#fff}.skin-blue .main-header .navbar .nav>li>a:hover,.skin-blue .main-header .navbar .nav>li>a:active,.skin-blue .main-header .navbar .nav>li>a:focus,.skin-blue .main-header .navbar .nav .open>a,.skin-blue .main-header .navbar .nav .open>a:hover,.skin-blue .main-header .navbar .nav .open>a:focus,.skin-blue .main-header .navbar .nav>.active>a{background:rgba(0,0,0,0.1);color:#f6f6f6}.skin-blue .main-header .navbar .sidebar-toggle{color:#fff}.skin-blue .main-header .navbar .sidebar-toggle:hover{color:#f6f6f6;background:rgba(0,0,0,0.1)}.skin-blue .main-header .navbar .sidebar-toggle{color:#fff}.skin-blue .main-header .navbar .sidebar-toggle:hover{background-color:#2a378b}@media (max-width:767px){.skin-blue .main-header .navbar .dropdown-menu li.divider{background-color:rgba(255,255,255,0.1)}.skin-blue .main-header .navbar .dropdown-menu li a{color:#fff}.skin-blue .main-header .navbar .dropdown-menu li a:hover{background:#2a378b}}.skin-blue .main-header .logo{background-color:#2a378b;color:#fff;border-bottom:0 solid transparent}.skin-blue .main-header .logo:hover{background-color:#293687}.skin-blue .main-header li.user-header{background-color:#303f9f}.skin-blue .content-header{background:transparent}.skin-blue .wrapper,.skin-blue .main-sidebar,.skin-blue .left-side{background-color:#1f2331}.skin-blue .user-panel>.info,.skin-blue .user-panel>.info>a{color:#fff}.skin-blue .sidebar-menu>li.header{color:#47506f;background:#171a25}.skin-blue .sidebar-menu>li>a{border-left:3px solid transparent}.skin-blue .sidebar-menu>li:hover>a,.skin-blue .sidebar-menu>li.active>a,.skin-blue .sidebar-menu>li.menu-open>a{color:#fff;background:#1b1f2b}.skin-blue .sidebar-menu>li.active>a{border-left-color:#303f9f}.skin-blue .sidebar-menu>li>.treeview-menu{margin:0 1px;background:#292e41}.skin-blue .sidebar a{color:#b3b9cf}.skin-blue .sidebar a:hover{text-decoration:none}.skin-blue .sidebar-menu .treeview-menu>li>a{color:#848eb1}.skin-blue .sidebar-menu .treeview-menu>li.active>a,.skin-blue .sidebar-menu .treeview-menu>li>a:hover{color:#fff}.skin-blue .sidebar-form{border-radius:3px;border:1px solid #333950;margin:10px 10px}.skin-blue .sidebar-form input[type="text"],.skin-blue .sidebar-form .btn{box-shadow:none;background-color:#333950;border:1px solid transparent;height:35px}.skin-blue .sidebar-form input[type="text"]{color:#666;border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px}.skin-blue .sidebar-form input[type="text"]:focus,.skin-blue .sidebar-form input[type="text"]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.skin-blue .sidebar-form input[type="text"]:focus+.input-group-btn .btn{border-left-color:#fff}.skin-blue .sidebar-form .btn{color:#999;border-top-left-radius:0;border-top-right-radius:2px;border-bottom-right-radius:2px;border-bottom-left-radius:0}.skin-blue.layout-top-nav .main-header>.logo{background-color:#303f9f;color:#fff;border-bottom:0 solid transparent}.skin-blue.layout-top-nav .main-header>.logo:hover{background-color:#2f3d9b} From 6ce3aa969f73add3848d6ffb1142f5ccb742c422 Mon Sep 17 00:00:00 2001 From: Jakob Schrettenbrunner Date: Mon, 4 Sep 2017 23:53:46 +0200 Subject: [PATCH 84/99] redesign login page --- public/themes/pterodactyl/css/pterodactyl.css | 38 +++++- .../vendor/particlesjs/particles.json | 110 ++++++++++++++++++ .../vendor/particlesjs/particles.min.js | 9 ++ .../themes/pterodactyl/layouts/auth.blade.php | 20 +++- 4 files changed, 165 insertions(+), 12 deletions(-) create mode 100644 public/themes/pterodactyl/vendor/particlesjs/particles.json create mode 100644 public/themes/pterodactyl/vendor/particlesjs/particles.min.js diff --git a/public/themes/pterodactyl/css/pterodactyl.css b/public/themes/pterodactyl/css/pterodactyl.css index 19251e69..d6975863 100644 --- a/public/themes/pterodactyl/css/pterodactyl.css +++ b/public/themes/pterodactyl/css/pterodactyl.css @@ -23,19 +23,44 @@ @import 'checkbox.css'; .login-page { - height: auto; + background: #303f9f; +} + +.login-logo { + color: white; + font-weight: bold; +} + +.login-copyright { + color: white; +} + +.login-copyright a, .login-copyright a:hover { + color: white; + font-weight: bold; +} + +.particles-js-canvas-el { + position: absolute; } .login-box, .register-box { - width: 40%; - max-width: 500px; - margin: 7% auto; + position: absolute; + margin: -180px 0 0 -180px; + left: 50%; + top: 50%; + height: 360px; + width: 360px; + z-index: 100; } @media (max-width:768px) { - .login-box, .register-box { + .login-box { width: 90%; - margin-top: 20px + margin-top: 20px; + margin: 5%; + left: 0; + top: 0; } } @@ -282,6 +307,7 @@ tr:hover + tr.server-description { position: absolute; bottom: 5px; right: 10px; + color: white; } input.form-autocomplete-stop[readonly] { diff --git a/public/themes/pterodactyl/vendor/particlesjs/particles.json b/public/themes/pterodactyl/vendor/particlesjs/particles.json new file mode 100644 index 00000000..077ccae7 --- /dev/null +++ b/public/themes/pterodactyl/vendor/particlesjs/particles.json @@ -0,0 +1,110 @@ +{ + "particles": { + "number": { + "value": 80, + "density": { + "enable": true, + "value_area": 800 + } + }, + "color": { + "value": "#ffffff" + }, + "shape": { + "type": "circle", + "stroke": { + "width": 0, + "color": "#000000" + }, + "polygon": { + "nb_sides": 5 + }, + "image": { + "src": "img/github.svg", + "width": 100, + "height": 100 + } + }, + "opacity": { + "value": 0.40246529723245905, + "random": false, + "anim": { + "enable": false, + "speed": 1, + "opacity_min": 0.1, + "sync": false + } + }, + "size": { + "value": 1, + "random": true, + "anim": { + "enable": false, + "speed": 40, + "size_min": 0.1, + "sync": false + } + }, + "line_linked": { + "enable": true, + "distance": 150, + "color": "#ffffff", + "opacity": 0.4, + "width": 1 + }, + "move": { + "enable": true, + "speed": 5, + "direction": "none", + "random": false, + "straight": false, + "out_mode": "out", + "bounce": false, + "attract": { + "enable": false, + "rotateX": 600, + "rotateY": 1200 + } + } + }, + "interactivity": { + "detect_on": "canvas", + "events": { + "onhover": { + "enable": true, + "mode": "repulse" + }, + "onclick": { + "enable": false, + "mode": "repulse" + }, + "resize": true + }, + "modes": { + "grab": { + "distance": 400, + "line_linked": { + "opacity": 1 + } + }, + "bubble": { + "distance": 400, + "size": 40, + "duration": 2, + "opacity": 8, + "speed": 3 + }, + "repulse": { + "distance": 200, + "duration": 0.4 + }, + "push": { + "particles_nb": 4 + }, + "remove": { + "particles_nb": 2 + } + } + }, + "retina_detect": true +} diff --git a/public/themes/pterodactyl/vendor/particlesjs/particles.min.js b/public/themes/pterodactyl/vendor/particlesjs/particles.min.js new file mode 100644 index 00000000..1b204b7b --- /dev/null +++ b/public/themes/pterodactyl/vendor/particlesjs/particles.min.js @@ -0,0 +1,9 @@ +/* ----------------------------------------------- +/* Author : Vincent Garreau - vincentgarreau.com +/* MIT license: http://opensource.org/licenses/MIT +/* Demo / Generator : vincentgarreau.com/particles.js +/* GitHub : github.com/VincentGarreau/particles.js +/* How to use? : Check the GitHub README +/* v2.0.0 +/* ----------------------------------------------- */ +function hexToRgb(e){var a=/^#?([a-f\d])([a-f\d])([a-f\d])$/i;e=e.replace(a,function(e,a,t,i){return a+a+t+t+i+i});var t=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(e);return t?{r:parseInt(t[1],16),g:parseInt(t[2],16),b:parseInt(t[3],16)}:null}function clamp(e,a,t){return Math.min(Math.max(e,a),t)}function isInArray(e,a){return a.indexOf(e)>-1}var pJS=function(e,a){var t=document.querySelector("#"+e+" > .particles-js-canvas-el");this.pJS={canvas:{el:t,w:t.offsetWidth,h:t.offsetHeight},particles:{number:{value:400,density:{enable:!0,value_area:800}},color:{value:"#fff"},shape:{type:"circle",stroke:{width:0,color:"#ff0000"},polygon:{nb_sides:5},image:{src:"",width:100,height:100}},opacity:{value:1,random:!1,anim:{enable:!1,speed:2,opacity_min:0,sync:!1}},size:{value:20,random:!1,anim:{enable:!1,speed:20,size_min:0,sync:!1}},line_linked:{enable:!0,distance:100,color:"#fff",opacity:1,width:1},move:{enable:!0,speed:2,direction:"none",random:!1,straight:!1,out_mode:"out",bounce:!1,attract:{enable:!1,rotateX:3e3,rotateY:3e3}},array:[]},interactivity:{detect_on:"canvas",events:{onhover:{enable:!0,mode:"grab"},onclick:{enable:!0,mode:"push"},resize:!0},modes:{grab:{distance:100,line_linked:{opacity:1}},bubble:{distance:200,size:80,duration:.4},repulse:{distance:200,duration:.4},push:{particles_nb:4},remove:{particles_nb:2}},mouse:{}},retina_detect:!1,fn:{interact:{},modes:{},vendors:{}},tmp:{}};var i=this.pJS;a&&Object.deepExtend(i,a),i.tmp.obj={size_value:i.particles.size.value,size_anim_speed:i.particles.size.anim.speed,move_speed:i.particles.move.speed,line_linked_distance:i.particles.line_linked.distance,line_linked_width:i.particles.line_linked.width,mode_grab_distance:i.interactivity.modes.grab.distance,mode_bubble_distance:i.interactivity.modes.bubble.distance,mode_bubble_size:i.interactivity.modes.bubble.size,mode_repulse_distance:i.interactivity.modes.repulse.distance},i.fn.retinaInit=function(){i.retina_detect&&window.devicePixelRatio>1?(i.canvas.pxratio=window.devicePixelRatio,i.tmp.retina=!0):(i.canvas.pxratio=1,i.tmp.retina=!1),i.canvas.w=i.canvas.el.offsetWidth*i.canvas.pxratio,i.canvas.h=i.canvas.el.offsetHeight*i.canvas.pxratio,i.particles.size.value=i.tmp.obj.size_value*i.canvas.pxratio,i.particles.size.anim.speed=i.tmp.obj.size_anim_speed*i.canvas.pxratio,i.particles.move.speed=i.tmp.obj.move_speed*i.canvas.pxratio,i.particles.line_linked.distance=i.tmp.obj.line_linked_distance*i.canvas.pxratio,i.interactivity.modes.grab.distance=i.tmp.obj.mode_grab_distance*i.canvas.pxratio,i.interactivity.modes.bubble.distance=i.tmp.obj.mode_bubble_distance*i.canvas.pxratio,i.particles.line_linked.width=i.tmp.obj.line_linked_width*i.canvas.pxratio,i.interactivity.modes.bubble.size=i.tmp.obj.mode_bubble_size*i.canvas.pxratio,i.interactivity.modes.repulse.distance=i.tmp.obj.mode_repulse_distance*i.canvas.pxratio},i.fn.canvasInit=function(){i.canvas.ctx=i.canvas.el.getContext("2d")},i.fn.canvasSize=function(){i.canvas.el.width=i.canvas.w,i.canvas.el.height=i.canvas.h,i&&i.interactivity.events.resize&&window.addEventListener("resize",function(){i.canvas.w=i.canvas.el.offsetWidth,i.canvas.h=i.canvas.el.offsetHeight,i.tmp.retina&&(i.canvas.w*=i.canvas.pxratio,i.canvas.h*=i.canvas.pxratio),i.canvas.el.width=i.canvas.w,i.canvas.el.height=i.canvas.h,i.particles.move.enable||(i.fn.particlesEmpty(),i.fn.particlesCreate(),i.fn.particlesDraw(),i.fn.vendors.densityAutoParticles()),i.fn.vendors.densityAutoParticles()})},i.fn.canvasPaint=function(){i.canvas.ctx.fillRect(0,0,i.canvas.w,i.canvas.h)},i.fn.canvasClear=function(){i.canvas.ctx.clearRect(0,0,i.canvas.w,i.canvas.h)},i.fn.particle=function(e,a,t){if(this.radius=(i.particles.size.random?Math.random():1)*i.particles.size.value,i.particles.size.anim.enable&&(this.size_status=!1,this.vs=i.particles.size.anim.speed/100,i.particles.size.anim.sync||(this.vs=this.vs*Math.random())),this.x=t?t.x:Math.random()*i.canvas.w,this.y=t?t.y:Math.random()*i.canvas.h,this.x>i.canvas.w-2*this.radius?this.x=this.x-this.radius:this.x<2*this.radius&&(this.x=this.x+this.radius),this.y>i.canvas.h-2*this.radius?this.y=this.y-this.radius:this.y<2*this.radius&&(this.y=this.y+this.radius),i.particles.move.bounce&&i.fn.vendors.checkOverlap(this,t),this.color={},"object"==typeof e.value)if(e.value instanceof Array){var s=e.value[Math.floor(Math.random()*i.particles.color.value.length)];this.color.rgb=hexToRgb(s)}else void 0!=e.value.r&&void 0!=e.value.g&&void 0!=e.value.b&&(this.color.rgb={r:e.value.r,g:e.value.g,b:e.value.b}),void 0!=e.value.h&&void 0!=e.value.s&&void 0!=e.value.l&&(this.color.hsl={h:e.value.h,s:e.value.s,l:e.value.l});else"random"==e.value?this.color.rgb={r:Math.floor(256*Math.random())+0,g:Math.floor(256*Math.random())+0,b:Math.floor(256*Math.random())+0}:"string"==typeof e.value&&(this.color=e,this.color.rgb=hexToRgb(this.color.value));this.opacity=(i.particles.opacity.random?Math.random():1)*i.particles.opacity.value,i.particles.opacity.anim.enable&&(this.opacity_status=!1,this.vo=i.particles.opacity.anim.speed/100,i.particles.opacity.anim.sync||(this.vo=this.vo*Math.random()));var n={};switch(i.particles.move.direction){case"top":n={x:0,y:-1};break;case"top-right":n={x:.5,y:-.5};break;case"right":n={x:1,y:-0};break;case"bottom-right":n={x:.5,y:.5};break;case"bottom":n={x:0,y:1};break;case"bottom-left":n={x:-.5,y:1};break;case"left":n={x:-1,y:0};break;case"top-left":n={x:-.5,y:-.5};break;default:n={x:0,y:0}}i.particles.move.straight?(this.vx=n.x,this.vy=n.y,i.particles.move.random&&(this.vx=this.vx*Math.random(),this.vy=this.vy*Math.random())):(this.vx=n.x+Math.random()-.5,this.vy=n.y+Math.random()-.5),this.vx_i=this.vx,this.vy_i=this.vy;var r=i.particles.shape.type;if("object"==typeof r){if(r instanceof Array){var c=r[Math.floor(Math.random()*r.length)];this.shape=c}}else this.shape=r;if("image"==this.shape){var o=i.particles.shape;this.img={src:o.image.src,ratio:o.image.width/o.image.height},this.img.ratio||(this.img.ratio=1),"svg"==i.tmp.img_type&&void 0!=i.tmp.source_svg&&(i.fn.vendors.createSvgImg(this),i.tmp.pushing&&(this.img.loaded=!1))}},i.fn.particle.prototype.draw=function(){function e(){i.canvas.ctx.drawImage(r,a.x-t,a.y-t,2*t,2*t/a.img.ratio)}var a=this;if(void 0!=a.radius_bubble)var t=a.radius_bubble;else var t=a.radius;if(void 0!=a.opacity_bubble)var s=a.opacity_bubble;else var s=a.opacity;if(a.color.rgb)var n="rgba("+a.color.rgb.r+","+a.color.rgb.g+","+a.color.rgb.b+","+s+")";else var n="hsla("+a.color.hsl.h+","+a.color.hsl.s+"%,"+a.color.hsl.l+"%,"+s+")";switch(i.canvas.ctx.fillStyle=n,i.canvas.ctx.beginPath(),a.shape){case"circle":i.canvas.ctx.arc(a.x,a.y,t,0,2*Math.PI,!1);break;case"edge":i.canvas.ctx.rect(a.x-t,a.y-t,2*t,2*t);break;case"triangle":i.fn.vendors.drawShape(i.canvas.ctx,a.x-t,a.y+t/1.66,2*t,3,2);break;case"polygon":i.fn.vendors.drawShape(i.canvas.ctx,a.x-t/(i.particles.shape.polygon.nb_sides/3.5),a.y-t/.76,2.66*t/(i.particles.shape.polygon.nb_sides/3),i.particles.shape.polygon.nb_sides,1);break;case"star":i.fn.vendors.drawShape(i.canvas.ctx,a.x-2*t/(i.particles.shape.polygon.nb_sides/4),a.y-t/1.52,2*t*2.66/(i.particles.shape.polygon.nb_sides/3),i.particles.shape.polygon.nb_sides,2);break;case"image":if("svg"==i.tmp.img_type)var r=a.img.obj;else var r=i.tmp.img_obj;r&&e()}i.canvas.ctx.closePath(),i.particles.shape.stroke.width>0&&(i.canvas.ctx.strokeStyle=i.particles.shape.stroke.color,i.canvas.ctx.lineWidth=i.particles.shape.stroke.width,i.canvas.ctx.stroke()),i.canvas.ctx.fill()},i.fn.particlesCreate=function(){for(var e=0;e=i.particles.opacity.value&&(a.opacity_status=!1),a.opacity+=a.vo):(a.opacity<=i.particles.opacity.anim.opacity_min&&(a.opacity_status=!0),a.opacity-=a.vo),a.opacity<0&&(a.opacity=0)),i.particles.size.anim.enable&&(1==a.size_status?(a.radius>=i.particles.size.value&&(a.size_status=!1),a.radius+=a.vs):(a.radius<=i.particles.size.anim.size_min&&(a.size_status=!0),a.radius-=a.vs),a.radius<0&&(a.radius=0)),"bounce"==i.particles.move.out_mode)var s={x_left:a.radius,x_right:i.canvas.w,y_top:a.radius,y_bottom:i.canvas.h};else var s={x_left:-a.radius,x_right:i.canvas.w+a.radius,y_top:-a.radius,y_bottom:i.canvas.h+a.radius};switch(a.x-a.radius>i.canvas.w?(a.x=s.x_left,a.y=Math.random()*i.canvas.h):a.x+a.radius<0&&(a.x=s.x_right,a.y=Math.random()*i.canvas.h),a.y-a.radius>i.canvas.h?(a.y=s.y_top,a.x=Math.random()*i.canvas.w):a.y+a.radius<0&&(a.y=s.y_bottom,a.x=Math.random()*i.canvas.w),i.particles.move.out_mode){case"bounce":a.x+a.radius>i.canvas.w?a.vx=-a.vx:a.x-a.radius<0&&(a.vx=-a.vx),a.y+a.radius>i.canvas.h?a.vy=-a.vy:a.y-a.radius<0&&(a.vy=-a.vy)}if(isInArray("grab",i.interactivity.events.onhover.mode)&&i.fn.modes.grabParticle(a),(isInArray("bubble",i.interactivity.events.onhover.mode)||isInArray("bubble",i.interactivity.events.onclick.mode))&&i.fn.modes.bubbleParticle(a),(isInArray("repulse",i.interactivity.events.onhover.mode)||isInArray("repulse",i.interactivity.events.onclick.mode))&&i.fn.modes.repulseParticle(a),i.particles.line_linked.enable||i.particles.move.attract.enable)for(var n=e+1;n0){var c=i.particles.line_linked.color_rgb_line;i.canvas.ctx.strokeStyle="rgba("+c.r+","+c.g+","+c.b+","+r+")",i.canvas.ctx.lineWidth=i.particles.line_linked.width,i.canvas.ctx.beginPath(),i.canvas.ctx.moveTo(e.x,e.y),i.canvas.ctx.lineTo(a.x,a.y),i.canvas.ctx.stroke(),i.canvas.ctx.closePath()}}},i.fn.interact.attractParticles=function(e,a){var t=e.x-a.x,s=e.y-a.y,n=Math.sqrt(t*t+s*s);if(n<=i.particles.line_linked.distance){var r=t/(1e3*i.particles.move.attract.rotateX),c=s/(1e3*i.particles.move.attract.rotateY);e.vx-=r,e.vy-=c,a.vx+=r,a.vy+=c}},i.fn.interact.bounceParticles=function(e,a){var t=e.x-a.x,i=e.y-a.y,s=Math.sqrt(t*t+i*i),n=e.radius+a.radius;n>=s&&(e.vx=-e.vx,e.vy=-e.vy,a.vx=-a.vx,a.vy=-a.vy)},i.fn.modes.pushParticles=function(e,a){i.tmp.pushing=!0;for(var t=0;e>t;t++)i.particles.array.push(new i.fn.particle(i.particles.color,i.particles.opacity.value,{x:a?a.pos_x:Math.random()*i.canvas.w,y:a?a.pos_y:Math.random()*i.canvas.h})),t==e-1&&(i.particles.move.enable||i.fn.particlesDraw(),i.tmp.pushing=!1)},i.fn.modes.removeParticles=function(e){i.particles.array.splice(0,e),i.particles.move.enable||i.fn.particlesDraw()},i.fn.modes.bubbleParticle=function(e){function a(){e.opacity_bubble=e.opacity,e.radius_bubble=e.radius}function t(a,t,s,n,c){if(a!=t)if(i.tmp.bubble_duration_end){if(void 0!=s){var o=n-p*(n-a)/i.interactivity.modes.bubble.duration,l=a-o;d=a+l,"size"==c&&(e.radius_bubble=d),"opacity"==c&&(e.opacity_bubble=d)}}else if(r<=i.interactivity.modes.bubble.distance){if(void 0!=s)var v=s;else var v=n;if(v!=a){var d=n-p*(n-a)/i.interactivity.modes.bubble.duration;"size"==c&&(e.radius_bubble=d),"opacity"==c&&(e.opacity_bubble=d)}}else"size"==c&&(e.radius_bubble=void 0),"opacity"==c&&(e.opacity_bubble=void 0)}if(i.interactivity.events.onhover.enable&&isInArray("bubble",i.interactivity.events.onhover.mode)){var s=e.x-i.interactivity.mouse.pos_x,n=e.y-i.interactivity.mouse.pos_y,r=Math.sqrt(s*s+n*n),c=1-r/i.interactivity.modes.bubble.distance;if(r<=i.interactivity.modes.bubble.distance){if(c>=0&&"mousemove"==i.interactivity.status){if(i.interactivity.modes.bubble.size!=i.particles.size.value)if(i.interactivity.modes.bubble.size>i.particles.size.value){var o=e.radius+i.interactivity.modes.bubble.size*c;o>=0&&(e.radius_bubble=o)}else{var l=e.radius-i.interactivity.modes.bubble.size,o=e.radius-l*c;o>0?e.radius_bubble=o:e.radius_bubble=0}if(i.interactivity.modes.bubble.opacity!=i.particles.opacity.value)if(i.interactivity.modes.bubble.opacity>i.particles.opacity.value){var v=i.interactivity.modes.bubble.opacity*c;v>e.opacity&&v<=i.interactivity.modes.bubble.opacity&&(e.opacity_bubble=v)}else{var v=e.opacity-(i.particles.opacity.value-i.interactivity.modes.bubble.opacity)*c;v=i.interactivity.modes.bubble.opacity&&(e.opacity_bubble=v)}}}else a();"mouseleave"==i.interactivity.status&&a()}else if(i.interactivity.events.onclick.enable&&isInArray("bubble",i.interactivity.events.onclick.mode)){if(i.tmp.bubble_clicking){var s=e.x-i.interactivity.mouse.click_pos_x,n=e.y-i.interactivity.mouse.click_pos_y,r=Math.sqrt(s*s+n*n),p=((new Date).getTime()-i.interactivity.mouse.click_time)/1e3;p>i.interactivity.modes.bubble.duration&&(i.tmp.bubble_duration_end=!0),p>2*i.interactivity.modes.bubble.duration&&(i.tmp.bubble_clicking=!1,i.tmp.bubble_duration_end=!1)}i.tmp.bubble_clicking&&(t(i.interactivity.modes.bubble.size,i.particles.size.value,e.radius_bubble,e.radius,"size"),t(i.interactivity.modes.bubble.opacity,i.particles.opacity.value,e.opacity_bubble,e.opacity,"opacity"))}},i.fn.modes.repulseParticle=function(e){function a(){var a=Math.atan2(d,p);if(e.vx=u*Math.cos(a),e.vy=u*Math.sin(a),"bounce"==i.particles.move.out_mode){var t={x:e.x+e.vx,y:e.y+e.vy};t.x+e.radius>i.canvas.w?e.vx=-e.vx:t.x-e.radius<0&&(e.vx=-e.vx),t.y+e.radius>i.canvas.h?e.vy=-e.vy:t.y-e.radius<0&&(e.vy=-e.vy)}}if(i.interactivity.events.onhover.enable&&isInArray("repulse",i.interactivity.events.onhover.mode)&&"mousemove"==i.interactivity.status){var t=e.x-i.interactivity.mouse.pos_x,s=e.y-i.interactivity.mouse.pos_y,n=Math.sqrt(t*t+s*s),r={x:t/n,y:s/n},c=i.interactivity.modes.repulse.distance,o=100,l=clamp(1/c*(-1*Math.pow(n/c,2)+1)*c*o,0,50),v={x:e.x+r.x*l,y:e.y+r.y*l};"bounce"==i.particles.move.out_mode?(v.x-e.radius>0&&v.x+e.radius0&&v.y+e.radius=m&&a()}else 0==i.tmp.repulse_clicking&&(e.vx=e.vx_i,e.vy=e.vy_i)},i.fn.modes.grabParticle=function(e){if(i.interactivity.events.onhover.enable&&"mousemove"==i.interactivity.status){var a=e.x-i.interactivity.mouse.pos_x,t=e.y-i.interactivity.mouse.pos_y,s=Math.sqrt(a*a+t*t);if(s<=i.interactivity.modes.grab.distance){var n=i.interactivity.modes.grab.line_linked.opacity-s/(1/i.interactivity.modes.grab.line_linked.opacity)/i.interactivity.modes.grab.distance;if(n>0){var r=i.particles.line_linked.color_rgb_line;i.canvas.ctx.strokeStyle="rgba("+r.r+","+r.g+","+r.b+","+n+")",i.canvas.ctx.lineWidth=i.particles.line_linked.width,i.canvas.ctx.beginPath(),i.canvas.ctx.moveTo(e.x,e.y),i.canvas.ctx.lineTo(i.interactivity.mouse.pos_x,i.interactivity.mouse.pos_y),i.canvas.ctx.stroke(),i.canvas.ctx.closePath()}}}},i.fn.vendors.eventsListeners=function(){"window"==i.interactivity.detect_on?i.interactivity.el=window:i.interactivity.el=i.canvas.el,(i.interactivity.events.onhover.enable||i.interactivity.events.onclick.enable)&&(i.interactivity.el.addEventListener("mousemove",function(e){if(i.interactivity.el==window)var a=e.clientX,t=e.clientY;else var a=e.offsetX||e.clientX,t=e.offsetY||e.clientY;i.interactivity.mouse.pos_x=a,i.interactivity.mouse.pos_y=t,i.tmp.retina&&(i.interactivity.mouse.pos_x*=i.canvas.pxratio,i.interactivity.mouse.pos_y*=i.canvas.pxratio),i.interactivity.status="mousemove"}),i.interactivity.el.addEventListener("mouseleave",function(e){i.interactivity.mouse.pos_x=null,i.interactivity.mouse.pos_y=null,i.interactivity.status="mouseleave"})),i.interactivity.events.onclick.enable&&i.interactivity.el.addEventListener("click",function(){if(i.interactivity.mouse.click_pos_x=i.interactivity.mouse.pos_x,i.interactivity.mouse.click_pos_y=i.interactivity.mouse.pos_y,i.interactivity.mouse.click_time=(new Date).getTime(),i.interactivity.events.onclick.enable)switch(i.interactivity.events.onclick.mode){case"push":i.particles.move.enable?i.fn.modes.pushParticles(i.interactivity.modes.push.particles_nb,i.interactivity.mouse):1==i.interactivity.modes.push.particles_nb?i.fn.modes.pushParticles(i.interactivity.modes.push.particles_nb,i.interactivity.mouse):i.interactivity.modes.push.particles_nb>1&&i.fn.modes.pushParticles(i.interactivity.modes.push.particles_nb);break;case"remove":i.fn.modes.removeParticles(i.interactivity.modes.remove.particles_nb);break;case"bubble":i.tmp.bubble_clicking=!0;break;case"repulse":i.tmp.repulse_clicking=!0,i.tmp.repulse_count=0,i.tmp.repulse_finish=!1,setTimeout(function(){i.tmp.repulse_clicking=!1},1e3*i.interactivity.modes.repulse.duration)}})},i.fn.vendors.densityAutoParticles=function(){if(i.particles.number.density.enable){var e=i.canvas.el.width*i.canvas.el.height/1e3;i.tmp.retina&&(e/=2*i.canvas.pxratio);var a=e*i.particles.number.value/i.particles.number.density.value_area,t=i.particles.array.length-a;0>t?i.fn.modes.pushParticles(Math.abs(t)):i.fn.modes.removeParticles(t)}},i.fn.vendors.checkOverlap=function(e,a){for(var t=0;tv;v++)e.lineTo(i,0),e.translate(i,0),e.rotate(l);e.fill(),e.restore()},i.fn.vendors.exportImg=function(){window.open(i.canvas.el.toDataURL("image/png"),"_blank")},i.fn.vendors.loadImg=function(e){if(i.tmp.img_error=void 0,""!=i.particles.shape.image.src)if("svg"==e){var a=new XMLHttpRequest;a.open("GET",i.particles.shape.image.src),a.onreadystatechange=function(e){4==a.readyState&&(200==a.status?(i.tmp.source_svg=e.currentTarget.response,i.fn.vendors.checkBeforeDraw()):(console.log("Error pJS - Image not found"),i.tmp.img_error=!0))},a.send()}else{var t=new Image;t.addEventListener("load",function(){i.tmp.img_obj=t,i.fn.vendors.checkBeforeDraw()}),t.src=i.particles.shape.image.src}else console.log("Error pJS - No image.src"),i.tmp.img_error=!0},i.fn.vendors.draw=function(){"image"==i.particles.shape.type?"svg"==i.tmp.img_type?i.tmp.count_svg>=i.particles.number.value?(i.fn.particlesDraw(),i.particles.move.enable?i.fn.drawAnimFrame=requestAnimFrame(i.fn.vendors.draw):cancelRequestAnimFrame(i.fn.drawAnimFrame)):i.tmp.img_error||(i.fn.drawAnimFrame=requestAnimFrame(i.fn.vendors.draw)):void 0!=i.tmp.img_obj?(i.fn.particlesDraw(),i.particles.move.enable?i.fn.drawAnimFrame=requestAnimFrame(i.fn.vendors.draw):cancelRequestAnimFrame(i.fn.drawAnimFrame)):i.tmp.img_error||(i.fn.drawAnimFrame=requestAnimFrame(i.fn.vendors.draw)):(i.fn.particlesDraw(),i.particles.move.enable?i.fn.drawAnimFrame=requestAnimFrame(i.fn.vendors.draw):cancelRequestAnimFrame(i.fn.drawAnimFrame))},i.fn.vendors.checkBeforeDraw=function(){"image"==i.particles.shape.type?"svg"==i.tmp.img_type&&void 0==i.tmp.source_svg?i.tmp.checkAnimFrame=requestAnimFrame(check):(cancelRequestAnimFrame(i.tmp.checkAnimFrame),i.tmp.img_error||(i.fn.vendors.init(),i.fn.vendors.draw())):(i.fn.vendors.init(),i.fn.vendors.draw())},i.fn.vendors.init=function(){i.fn.retinaInit(),i.fn.canvasInit(),i.fn.canvasSize(),i.fn.canvasPaint(),i.fn.particlesCreate(),i.fn.vendors.densityAutoParticles(),i.particles.line_linked.color_rgb_line=hexToRgb(i.particles.line_linked.color)},i.fn.vendors.start=function(){isInArray("image",i.particles.shape.type)?(i.tmp.img_type=i.particles.shape.image.src.substr(i.particles.shape.image.src.length-3),i.fn.vendors.loadImg(i.tmp.img_type)):i.fn.vendors.checkBeforeDraw()},i.fn.vendors.eventsListeners(),i.fn.vendors.start()};Object.deepExtend=function(e,a){for(var t in a)a[t]&&a[t].constructor&&a[t].constructor===Object?(e[t]=e[t]||{},arguments.callee(e[t],a[t])):e[t]=a[t];return e},window.requestAnimFrame=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(e){window.setTimeout(e,1e3/60)}}(),window.cancelRequestAnimFrame=function(){return window.cancelAnimationFrame||window.webkitCancelRequestAnimationFrame||window.mozCancelRequestAnimationFrame||window.oCancelRequestAnimationFrame||window.msCancelRequestAnimationFrame||clearTimeout}(),window.pJSDom=[],window.particlesJS=function(e,a){"string"!=typeof e&&(a=e,e="particles-js"),e||(e="particles-js");var t=document.getElementById(e),i="particles-js-canvas-el",s=t.getElementsByClassName(i);if(s.length)for(;s.length>0;)t.removeChild(s[0]);var n=document.createElement("canvas");n.className=i,n.style.width="100%",n.style.height="100%";var r=document.getElementById(e).appendChild(n);null!=r&&pJSDom.push(new pJS(e,a))},window.particlesJS.load=function(e,a,t){var i=new XMLHttpRequest;i.open("GET",a),i.onreadystatechange=function(a){if(4==i.readyState)if(200==i.status){var s=JSON.parse(a.currentTarget.response);window.particlesJS(e,s),t&&t()}else console.log("Error pJS - XMLHttpRequest status: "+i.status),console.log("Error pJS - File config not found")},i.send()}; diff --git a/resources/themes/pterodactyl/layouts/auth.blade.php b/resources/themes/pterodactyl/layouts/auth.blade.php index 1d9631fd..9c035f00 100644 --- a/resources/themes/pterodactyl/layouts/auth.blade.php +++ b/resources/themes/pterodactyl/layouts/auth.blade.php @@ -47,23 +47,31 @@ @show - + + + {!! Theme::js('vendor/jquery/jquery.min.js') !!} {!! Theme::js('vendor/bootstrap/bootstrap.min.js') !!} {!! Theme::js('js/autocomplete.js') !!} + {!! Theme::js('vendor/particlesjs/particles.min.js') !!} + @if(config('pterodactyl.lang.in_context')) {!! Theme::js('vendor/phraseapp/phraseapp.js') !!} @endif From 8b978b95970be6e198143a572b60ac55ea1bab02 Mon Sep 17 00:00:00 2001 From: Jakob Schrettenbrunner Date: Tue, 5 Sep 2017 00:03:03 +0200 Subject: [PATCH 85/99] refine some adminlte colors --- public/themes/pterodactyl/vendor/adminlte/admin.min.css | 2 +- .../themes/pterodactyl/vendor/adminlte/colors/skin-blue.min.css | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/themes/pterodactyl/vendor/adminlte/admin.min.css b/public/themes/pterodactyl/vendor/adminlte/admin.min.css index c8ad6291..9e67e230 100755 --- a/public/themes/pterodactyl/vendor/adminlte/admin.min.css +++ b/public/themes/pterodactyl/vendor/adminlte/admin.min.css @@ -4,4 +4,4 @@ * Website: Almsaeed Studio * License: Open source - MIT * Please visit http://opensource.org/licenses/MIT for more information - */html,body{height:100%}.layout-boxed html,.layout-boxed body{height:100%}body{font-family:'Source Sans Pro','Helvetica Neue',Helvetica,Arial,sans-serif;font-weight:400;overflow-x:hidden;overflow-y:auto}.wrapper{height:100%;position:relative;overflow-x:hidden;overflow-y:auto}.wrapper:before,.wrapper:after{content:" ";display:table}.wrapper:after{clear:both}.layout-boxed .wrapper{max-width:1250px;margin:0 auto;min-height:100%;box-shadow:0 0 8px rgba(0,0,0,0.5);position:relative}.layout-boxed{background:url('../img/boxed-bg.jpg') repeat fixed}.content-wrapper,.main-footer{-webkit-transition:-webkit-transform .3s ease-in-out,margin .3s ease-in-out;-moz-transition:-moz-transform .3s ease-in-out,margin .3s ease-in-out;-o-transition:-o-transform .3s ease-in-out,margin .3s ease-in-out;transition:transform .3s ease-in-out,margin .3s ease-in-out;margin-left:230px;z-index:820}.layout-top-nav .content-wrapper,.layout-top-nav .main-footer{margin-left:0}@media (max-width:767px){.content-wrapper,.main-footer{margin-left:0}}@media (min-width:768px){.sidebar-collapse .content-wrapper,.sidebar-collapse .main-footer{margin-left:0}}@media (max-width:767px){.sidebar-open .content-wrapper,.sidebar-open .main-footer{-webkit-transform:translate(230px, 0);-ms-transform:translate(230px, 0);-o-transform:translate(230px, 0);transform:translate(230px, 0)}}.content-wrapper{min-height:100%;background-color:#ecf0f5;z-index:800}.main-footer{background:#fff;padding:15px;color:#444;border-top:1px solid #d2d6de}.fixed .main-header,.fixed .main-sidebar,.fixed .left-side{position:fixed}.fixed .main-header{top:0;right:0;left:0}.fixed .content-wrapper,.fixed .right-side{padding-top:50px}@media (max-width:767px){.fixed .content-wrapper,.fixed .right-side{padding-top:100px}}.fixed.layout-boxed .wrapper{max-width:100%}.fixed .wrapper{overflow:hidden}.hold-transition .content-wrapper,.hold-transition .right-side,.hold-transition .main-footer,.hold-transition .main-sidebar,.hold-transition .left-side,.hold-transition .main-header .navbar,.hold-transition .main-header .logo,.hold-transition .menu-open .fa-angle-left{-webkit-transition:none;-o-transition:none;transition:none}.content{min-height:250px;padding:15px;margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:'Source Sans Pro',sans-serif}a{color:#303f9f}a:hover,a:active,a:focus{outline:none;text-decoration:none;color:#5161ca}.page-header{margin:10px 0 20px 0;font-size:22px}.page-header>small{color:#666;display:block;margin-top:5px}.main-header{position:relative;max-height:100px;z-index:1030}.main-header .navbar{-webkit-transition:margin-left .3s ease-in-out;-o-transition:margin-left .3s ease-in-out;transition:margin-left .3s ease-in-out;margin-bottom:0;margin-left:230px;border:none;min-height:50px;border-radius:0}.layout-top-nav .main-header .navbar{margin-left:0}.main-header #navbar-search-input.form-control{background:rgba(255,255,255,0.2);border-color:transparent}.main-header #navbar-search-input.form-control:focus,.main-header #navbar-search-input.form-control:active{border-color:rgba(0,0,0,0.1);background:rgba(255,255,255,0.9)}.main-header #navbar-search-input.form-control::-moz-placeholder{color:#ccc;opacity:1}.main-header #navbar-search-input.form-control:-ms-input-placeholder{color:#ccc}.main-header #navbar-search-input.form-control::-webkit-input-placeholder{color:#ccc}.main-header .navbar-custom-menu,.main-header .navbar-right{float:right}@media (max-width:991px){.main-header .navbar-custom-menu a,.main-header .navbar-right a{color:inherit;background:transparent}}@media (max-width:767px){.main-header .navbar-right{float:none}.navbar-collapse .main-header .navbar-right{margin:7.5px -15px}.main-header .navbar-right>li{color:inherit;border:0}}.main-header .sidebar-toggle{float:left;background-color:transparent;background-image:none;padding:15px 15px;font-family:fontAwesome}.main-header .sidebar-toggle:before{content:"\f0c9"}.main-header .sidebar-toggle:hover{color:#fff}.main-header .sidebar-toggle:focus,.main-header .sidebar-toggle:active{background:transparent}.main-header .sidebar-toggle .icon-bar{display:none}.main-header .navbar .nav>li.user>a>.fa,.main-header .navbar .nav>li.user>a>.glyphicon,.main-header .navbar .nav>li.user>a>.ion{margin-right:5px}.main-header .navbar .nav>li>a>.label{position:absolute;top:9px;right:7px;text-align:center;font-size:9px;padding:2px 3px;line-height:.9}.main-header .logo{-webkit-transition:width .3s ease-in-out;-o-transition:width .3s ease-in-out;transition:width .3s ease-in-out;display:block;float:left;height:50px;font-size:20px;line-height:50px;text-align:center;width:230px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;padding:0 15px;font-weight:300;overflow:hidden}.main-header .logo .logo-lg{display:block}.main-header .logo .logo-mini{display:none}.main-header .navbar-brand{color:#fff}.content-header{position:relative;padding:15px 15px 0 15px}.content-header>h1{margin:0;font-size:24px}.content-header>h1>small{font-size:15px;display:inline-block;padding-left:4px;font-weight:300}.content-header>.breadcrumb{float:right;background:transparent;margin-top:0;margin-bottom:0;font-size:12px;padding:7px 5px;position:absolute;top:15px;right:10px;border-radius:2px}.content-header>.breadcrumb>li>a{color:#444;text-decoration:none;display:inline-block}.content-header>.breadcrumb>li>a>.fa,.content-header>.breadcrumb>li>a>.glyphicon,.content-header>.breadcrumb>li>a>.ion{margin-right:5px}.content-header>.breadcrumb>li+li:before{content:'>\00a0'}@media (max-width:991px){.content-header>.breadcrumb{position:relative;margin-top:5px;top:0;right:0;float:none;background:#d2d6de;padding-left:10px}.content-header>.breadcrumb li:before{color:#97a0b3}}.navbar-toggle{color:#fff;border:0;margin:0;padding:15px 15px}@media (max-width:991px){.navbar-custom-menu .navbar-nav>li{float:left}.navbar-custom-menu .navbar-nav{margin:0;float:left}.navbar-custom-menu .navbar-nav>li>a{padding-top:15px;padding-bottom:15px;line-height:20px}}@media (max-width:767px){.main-header{position:relative}.main-header .logo,.main-header .navbar{width:100%;float:none}.main-header .navbar{margin:0}.main-header .navbar-custom-menu{float:right}}@media (max-width:991px){.navbar-collapse.pull-left{float:none !important}.navbar-collapse.pull-left+.navbar-custom-menu{display:block;position:absolute;top:0;right:40px}}.main-sidebar{position:absolute;top:0;left:0;padding-top:50px;min-height:100%;width:230px;z-index:810;-webkit-transition:-webkit-transform .3s ease-in-out,width .3s ease-in-out;-moz-transition:-moz-transform .3s ease-in-out,width .3s ease-in-out;-o-transition:-o-transform .3s ease-in-out,width .3s ease-in-out;transition:transform .3s ease-in-out,width .3s ease-in-out}@media (max-width:767px){.main-sidebar{padding-top:100px}}@media (max-width:767px){.main-sidebar{-webkit-transform:translate(-230px, 0);-ms-transform:translate(-230px, 0);-o-transform:translate(-230px, 0);transform:translate(-230px, 0)}}@media (min-width:768px){.sidebar-collapse .main-sidebar{-webkit-transform:translate(-230px, 0);-ms-transform:translate(-230px, 0);-o-transform:translate(-230px, 0);transform:translate(-230px, 0)}}@media (max-width:767px){.sidebar-open .main-sidebar{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0)}}.sidebar{padding-bottom:10px}.sidebar-form input:focus{border-color:transparent}.user-panel{position:relative;width:100%;padding:10px;overflow:hidden}.user-panel:before,.user-panel:after{content:" ";display:table}.user-panel:after{clear:both}.user-panel>.image>img{width:100%;max-width:45px;height:auto}.user-panel>.info{padding:5px 5px 5px 15px;line-height:1;position:absolute;left:55px}.user-panel>.info>p{font-weight:600;margin-bottom:9px}.user-panel>.info>a{text-decoration:none;padding-right:5px;margin-top:3px;font-size:11px}.user-panel>.info>a>.fa,.user-panel>.info>a>.ion,.user-panel>.info>a>.glyphicon{margin-right:3px}.sidebar-menu{list-style:none;margin:0;padding:0}.sidebar-menu>li{position:relative;margin:0;padding:0}.sidebar-menu>li>a{padding:12px 5px 12px 15px;display:block}.sidebar-menu>li>a>.fa,.sidebar-menu>li>a>.glyphicon,.sidebar-menu>li>a>.ion{width:20px}.sidebar-menu>li .label,.sidebar-menu>li .badge{margin-right:5px}.sidebar-menu>li .badge{margin-top:3px}.sidebar-menu li.header{padding:10px 25px 10px 15px;font-size:12px}.sidebar-menu li>a>.fa-angle-left,.sidebar-menu li>a>.pull-right-container>.fa-angle-left{width:auto;height:auto;padding:0;margin-right:10px;-webkit-transition:transform .5s ease;-o-transition:transform .5s ease;transition:transform .5s ease}.sidebar-menu li>a>.fa-angle-left{position:absolute;top:50%;right:10px;margin-top:-8px}.sidebar-menu .menu-open>a>.fa-angle-left,.sidebar-menu .menu-open>a>.pull-right-container>.fa-angle-left{-webkit-transform:rotate(-90deg);-ms-transform:rotate(-90deg);-o-transform:rotate(-90deg);transform:rotate(-90deg)}.sidebar-menu .active>.treeview-menu{display:block}@media (min-width:768px){.sidebar-mini.sidebar-collapse .content-wrapper,.sidebar-mini.sidebar-collapse .right-side,.sidebar-mini.sidebar-collapse .main-footer{margin-left:50px !important;z-index:840}.sidebar-mini.sidebar-collapse .main-sidebar{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0);width:50px !important;z-index:850}.sidebar-mini.sidebar-collapse .sidebar-menu>li{position:relative}.sidebar-mini.sidebar-collapse .sidebar-menu>li>a{margin-right:0}.sidebar-mini.sidebar-collapse .sidebar-menu>li>a>span{border-top-right-radius:4px}.sidebar-mini.sidebar-collapse .sidebar-menu>li:not(.treeview)>a>span{border-bottom-right-radius:4px}.sidebar-mini.sidebar-collapse .sidebar-menu>li>.treeview-menu{padding-top:5px;padding-bottom:5px;border-bottom-right-radius:4px}.sidebar-mini.sidebar-collapse .main-sidebar .user-panel>.info,.sidebar-mini.sidebar-collapse .sidebar-form,.sidebar-mini.sidebar-collapse .sidebar-menu>li>a>span,.sidebar-mini.sidebar-collapse .sidebar-menu>li>.treeview-menu,.sidebar-mini.sidebar-collapse .sidebar-menu>li>a>.pull-right,.sidebar-mini.sidebar-collapse .sidebar-menu li.header{display:none !important;-webkit-transform:translateZ(0)}.sidebar-mini.sidebar-collapse .main-header .logo{width:50px}.sidebar-mini.sidebar-collapse .main-header .logo>.logo-mini{display:block;margin-left:-15px;margin-right:-15px;font-size:18px}.sidebar-mini.sidebar-collapse .main-header .logo>.logo-lg{display:none}.sidebar-mini.sidebar-collapse .main-header .navbar{margin-left:50px}}.sidebar-mini:not(.sidebar-mini-expand-feature).sidebar-collapse .sidebar-menu>li:hover>a>span:not(.pull-right),.sidebar-mini:not(.sidebar-mini-expand-feature).sidebar-collapse .sidebar-menu>li:hover>.treeview-menu{display:block !important;position:absolute;width:180px;left:50px}.sidebar-mini:not(.sidebar-mini-expand-feature).sidebar-collapse .sidebar-menu>li:hover>a>span{top:0;margin-left:-3px;padding:12px 5px 12px 20px;background-color:inherit}.sidebar-mini:not(.sidebar-mini-expand-feature).sidebar-collapse .sidebar-menu>li:hover>a>.pull-right-container{position:relative !important;float:right;width:auto !important;left:180px !important;top:-22px !important;z-index:900}.sidebar-mini:not(.sidebar-mini-expand-feature).sidebar-collapse .sidebar-menu>li:hover>a>.pull-right-container>.label:not(:first-of-type){display:none}.sidebar-mini:not(.sidebar-mini-expand-feature).sidebar-collapse .sidebar-menu>li:hover>.treeview-menu{top:44px;margin-left:0}.sidebar-expanded-on-hover .main-footer,.sidebar-expanded-on-hover .content-wrapper{margin-left:50px}.sidebar-expanded-on-hover .main-sidebar{box-shadow:3px 0 8px rgba(0,0,0,0.125)}.sidebar-menu,.main-sidebar .user-panel,.sidebar-menu>li.header{white-space:nowrap;overflow:hidden}.sidebar-menu:hover{overflow:visible}.sidebar-form,.sidebar-menu>li.header{overflow:hidden;text-overflow:clip}.sidebar-menu li>a{position:relative}.sidebar-menu li>a>.pull-right-container{position:absolute;right:10px;top:50%;margin-top:-7px}.control-sidebar-bg{position:fixed;z-index:1000;bottom:0}.control-sidebar-bg,.control-sidebar{top:0;right:-230px;width:230px;-webkit-transition:right .3s ease-in-out;-o-transition:right .3s ease-in-out;transition:right .3s ease-in-out}.control-sidebar{position:absolute;padding-top:50px;z-index:1010}@media (max-width:768px){.control-sidebar{padding-top:100px}}.control-sidebar>.tab-content{padding:10px 15px}.control-sidebar.control-sidebar-open,.control-sidebar.control-sidebar-open+.control-sidebar-bg{right:0}.control-sidebar-open .control-sidebar-bg,.control-sidebar-open .control-sidebar{right:0}@media (min-width:768px){.control-sidebar-open .content-wrapper,.control-sidebar-open .right-side,.control-sidebar-open .main-footer{margin-right:230px}}.fixed .control-sidebar{position:fixed;height:100%;overflow-y:auto;padding-bottom:50px}.nav-tabs.control-sidebar-tabs>li:first-of-type>a,.nav-tabs.control-sidebar-tabs>li:first-of-type>a:hover,.nav-tabs.control-sidebar-tabs>li:first-of-type>a:focus{border-left-width:0}.nav-tabs.control-sidebar-tabs>li>a{border-radius:0}.nav-tabs.control-sidebar-tabs>li>a,.nav-tabs.control-sidebar-tabs>li>a:hover{border-top:none;border-right:none;border-left:1px solid transparent;border-bottom:1px solid transparent}.nav-tabs.control-sidebar-tabs>li>a .icon{font-size:16px}.nav-tabs.control-sidebar-tabs>li.active>a,.nav-tabs.control-sidebar-tabs>li.active>a:hover,.nav-tabs.control-sidebar-tabs>li.active>a:focus,.nav-tabs.control-sidebar-tabs>li.active>a:active{border-top:none;border-right:none;border-bottom:none}@media (max-width:768px){.nav-tabs.control-sidebar-tabs{display:table}.nav-tabs.control-sidebar-tabs>li{display:table-cell}}.control-sidebar-heading{font-weight:400;font-size:16px;padding:10px 0;margin-bottom:10px}.control-sidebar-subheading{display:block;font-weight:400;font-size:14px}.control-sidebar-menu{list-style:none;padding:0;margin:0 -15px}.control-sidebar-menu>li>a{display:block;padding:10px 15px}.control-sidebar-menu>li>a:before,.control-sidebar-menu>li>a:after{content:" ";display:table}.control-sidebar-menu>li>a:after{clear:both}.control-sidebar-menu>li>a>.control-sidebar-subheading{margin-top:0}.control-sidebar-menu .menu-icon{float:left;width:35px;height:35px;border-radius:50%;text-align:center;line-height:35px}.control-sidebar-menu .menu-info{margin-left:45px;margin-top:3px}.control-sidebar-menu .menu-info>.control-sidebar-subheading{margin:0}.control-sidebar-menu .menu-info>p{margin:0;font-size:11px}.control-sidebar-menu .progress{margin:0}.control-sidebar-dark{color:#b3b9cf}.control-sidebar-dark,.control-sidebar-dark+.control-sidebar-bg{background:#1f2331}.control-sidebar-dark .nav-tabs.control-sidebar-tabs{border-bottom:#191c28}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a{background:#151821;color:#b3b9cf}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:hover,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:focus{border-left-color:#11131b;border-bottom-color:#11131b}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:hover,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:focus,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:active{background:#191c28}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:hover{color:#fff}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a:hover,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a:focus,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a:active{background:#1f2331;color:#fff}.control-sidebar-dark .control-sidebar-heading,.control-sidebar-dark .control-sidebar-subheading{color:#fff}.control-sidebar-dark .control-sidebar-menu>li>a:hover{background:#1b1f2b}.control-sidebar-dark .control-sidebar-menu>li>a .menu-info>p{color:#b3b9cf}.control-sidebar-light{color:#5e5e5e}.control-sidebar-light,.control-sidebar-light+.control-sidebar-bg{background:#f9fafc;border-left:1px solid #d2d6de}.control-sidebar-light .nav-tabs.control-sidebar-tabs{border-bottom:#d2d6de}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a{background:#e8ecf4;color:#444}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:hover,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:focus{border-left-color:#d2d6de;border-bottom-color:#d2d6de}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:hover,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:focus,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:active{background:#eff1f7}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a:hover,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a:focus,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a:active{background:#f9fafc;color:#111}.control-sidebar-light .control-sidebar-heading,.control-sidebar-light .control-sidebar-subheading{color:#111}.control-sidebar-light .control-sidebar-menu{margin-left:-14px}.control-sidebar-light .control-sidebar-menu>li>a:hover{background:#f4f4f5}.control-sidebar-light .control-sidebar-menu>li>a .menu-info>p{color:#5e5e5e}.dropdown-menu{box-shadow:none;border-color:#eee}.dropdown-menu>li>a{color:#777}.dropdown-menu>li>a>.glyphicon,.dropdown-menu>li>a>.fa,.dropdown-menu>li>a>.ion{margin-right:10px}.dropdown-menu>li>a:hover{background-color:#e1e3e9;color:#333}.dropdown-menu>.divider{background-color:#eee}.navbar-nav>.notifications-menu>.dropdown-menu,.navbar-nav>.messages-menu>.dropdown-menu,.navbar-nav>.tasks-menu>.dropdown-menu{width:280px;padding:0 0 0 0;margin:0;top:100%}.navbar-nav>.notifications-menu>.dropdown-menu>li,.navbar-nav>.messages-menu>.dropdown-menu>li,.navbar-nav>.tasks-menu>.dropdown-menu>li{position:relative}.navbar-nav>.notifications-menu>.dropdown-menu>li.header,.navbar-nav>.messages-menu>.dropdown-menu>li.header,.navbar-nav>.tasks-menu>.dropdown-menu>li.header{border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0;background-color:#ffffff;padding:7px 10px;border-bottom:1px solid #f4f4f4;color:#444444;font-size:14px}.navbar-nav>.notifications-menu>.dropdown-menu>li.footer>a,.navbar-nav>.messages-menu>.dropdown-menu>li.footer>a,.navbar-nav>.tasks-menu>.dropdown-menu>li.footer>a{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px;font-size:12px;background-color:#fff;padding:7px 10px;border-bottom:1px solid #eeeeee;color:#444 !important;text-align:center}@media (max-width:991px){.navbar-nav>.notifications-menu>.dropdown-menu>li.footer>a,.navbar-nav>.messages-menu>.dropdown-menu>li.footer>a,.navbar-nav>.tasks-menu>.dropdown-menu>li.footer>a{background:#fff !important;color:#444 !important}}.navbar-nav>.notifications-menu>.dropdown-menu>li.footer>a:hover,.navbar-nav>.messages-menu>.dropdown-menu>li.footer>a:hover,.navbar-nav>.tasks-menu>.dropdown-menu>li.footer>a:hover{text-decoration:none;font-weight:normal}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu,.navbar-nav>.messages-menu>.dropdown-menu>li .menu,.navbar-nav>.tasks-menu>.dropdown-menu>li .menu{max-height:200px;margin:0;padding:0;list-style:none;overflow-x:hidden}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a,.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a,.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a{display:block;white-space:nowrap;border-bottom:1px solid #f4f4f4}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a:hover,.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:hover,.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a:hover{background:#f4f4f4;text-decoration:none}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a{color:#444444;overflow:hidden;text-overflow:ellipsis;padding:10px}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a>.glyphicon,.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a>.fa,.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a>.ion{width:20px}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a{margin:0;padding:10px 10px}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>div>img{margin:auto 10px auto auto;width:40px;height:40px}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>h4{padding:0;margin:0 0 0 45px;color:#444444;font-size:15px;position:relative}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>h4>small{color:#999999;font-size:10px;position:absolute;top:0;right:0}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>p{margin:0 0 0 45px;font-size:12px;color:#888888}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:before,.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:after{content:" ";display:table}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:after{clear:both}.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a{padding:10px}.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a>h3{font-size:14px;padding:0;margin:0 0 10px 0;color:#666666}.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a>.progress{padding:0;margin:0}.navbar-nav>.user-menu>.dropdown-menu{border-top-right-radius:0;border-top-left-radius:0;padding:1px 0 0 0;border-top-width:0;width:280px}.navbar-nav>.user-menu>.dropdown-menu,.navbar-nav>.user-menu>.dropdown-menu>.user-body{border-bottom-right-radius:4px;border-bottom-left-radius:4px}.navbar-nav>.user-menu>.dropdown-menu>li.user-header{height:175px;padding:10px;text-align:center}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>img{z-index:5;height:90px;width:90px;border:3px solid;border-color:transparent;border-color:rgba(255,255,255,0.2)}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>p{z-index:5;color:#fff;color:rgba(255,255,255,0.8);font-size:17px;margin-top:10px}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>p>small{display:block;font-size:12px}.navbar-nav>.user-menu>.dropdown-menu>.user-body{padding:15px;border-bottom:1px solid #f4f4f4;border-top:1px solid #dddddd}.navbar-nav>.user-menu>.dropdown-menu>.user-body:before,.navbar-nav>.user-menu>.dropdown-menu>.user-body:after{content:" ";display:table}.navbar-nav>.user-menu>.dropdown-menu>.user-body:after{clear:both}.navbar-nav>.user-menu>.dropdown-menu>.user-body a{color:#444 !important}@media (max-width:991px){.navbar-nav>.user-menu>.dropdown-menu>.user-body a{background:#fff !important;color:#444 !important}}.navbar-nav>.user-menu>.dropdown-menu>.user-footer{background-color:#f9f9f9;padding:10px}.navbar-nav>.user-menu>.dropdown-menu>.user-footer:before,.navbar-nav>.user-menu>.dropdown-menu>.user-footer:after{content:" ";display:table}.navbar-nav>.user-menu>.dropdown-menu>.user-footer:after{clear:both}.navbar-nav>.user-menu>.dropdown-menu>.user-footer .btn-default{color:#666666}@media (max-width:991px){.navbar-nav>.user-menu>.dropdown-menu>.user-footer .btn-default:hover{background-color:#f9f9f9}}.navbar-nav>.user-menu .user-image{float:left;width:25px;height:25px;border-radius:50%;margin-right:10px;margin-top:-2px}@media (max-width:767px){.navbar-nav>.user-menu .user-image{float:none;margin-right:0;margin-top:-8px;line-height:10px}}.open:not(.dropup)>.animated-dropdown-menu{backface-visibility:visible !important;-webkit-animation:flipInX .7s both;-o-animation:flipInX .7s both;animation:flipInX .7s both}@keyframes flipInX{0%{transform:perspective(400px) rotate3d(1, 0, 0, 90deg);transition-timing-function:ease-in;opacity:0}40%{transform:perspective(400px) rotate3d(1, 0, 0, -20deg);transition-timing-function:ease-in}60%{transform:perspective(400px) rotate3d(1, 0, 0, 10deg);opacity:1}80%{transform:perspective(400px) rotate3d(1, 0, 0, -5deg)}100%{transform:perspective(400px)}}@-webkit-keyframes flipInX{0%{-webkit-transform:perspective(400px) rotate3d(1, 0, 0, 90deg);-webkit-transition-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotate3d(1, 0, 0, -20deg);-webkit-transition-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotate3d(1, 0, 0, 10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotate3d(1, 0, 0, -5deg)}100%{-webkit-transform:perspective(400px)}}.navbar-custom-menu>.navbar-nav>li{position:relative}.navbar-custom-menu>.navbar-nav>li>.dropdown-menu{position:absolute;right:0;left:auto}@media (max-width:991px){.navbar-custom-menu>.navbar-nav{float:right}.navbar-custom-menu>.navbar-nav>li{position:static}.navbar-custom-menu>.navbar-nav>li>.dropdown-menu{position:absolute;right:5%;left:auto;border:1px solid #ddd;background:#fff}}.form-control{border-radius:0;box-shadow:none;border-color:#d2d6de}.form-control:focus{border-color:#303f9f;box-shadow:none}.form-control::-moz-placeholder,.form-control:-ms-input-placeholder,.form-control::-webkit-input-placeholder{color:#bbb;opacity:1}.form-control:not(select){-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-group.has-success label{color:#00a65a}.form-group.has-success .form-control,.form-group.has-success .input-group-addon{border-color:#00a65a;box-shadow:none}.form-group.has-success .help-block{color:#00a65a}.form-group.has-warning label{color:#f39c12}.form-group.has-warning .form-control,.form-group.has-warning .input-group-addon{border-color:#f39c12;box-shadow:none}.form-group.has-warning .help-block{color:#f39c12}.form-group.has-error label{color:#dd4b39}.form-group.has-error .form-control,.form-group.has-error .input-group-addon{border-color:#dd4b39;box-shadow:none}.form-group.has-error .help-block{color:#dd4b39}.input-group .input-group-addon{border-radius:0;border-color:#d2d6de;background-color:#fff}.btn-group-vertical .btn.btn-flat:first-of-type,.btn-group-vertical .btn.btn-flat:last-of-type{border-radius:0}.icheck>label{padding-left:0}.form-control-feedback.fa{line-height:34px}.input-lg+.form-control-feedback.fa,.input-group-lg+.form-control-feedback.fa,.form-group-lg .form-control+.form-control-feedback.fa{line-height:46px}.input-sm+.form-control-feedback.fa,.input-group-sm+.form-control-feedback.fa,.form-group-sm .form-control+.form-control-feedback.fa{line-height:30px}.progress,.progress>.progress-bar{-webkit-box-shadow:none;box-shadow:none}.progress,.progress>.progress-bar,.progress .progress-bar,.progress>.progress-bar .progress-bar{border-radius:1px}.progress.sm,.progress-sm{height:10px}.progress.sm,.progress-sm,.progress.sm .progress-bar,.progress-sm .progress-bar{border-radius:1px}.progress.xs,.progress-xs{height:7px}.progress.xs,.progress-xs,.progress.xs .progress-bar,.progress-xs .progress-bar{border-radius:1px}.progress.xxs,.progress-xxs{height:3px}.progress.xxs,.progress-xxs,.progress.xxs .progress-bar,.progress-xxs .progress-bar{border-radius:1px}.progress.vertical{position:relative;width:30px;height:200px;display:inline-block;margin-right:10px}.progress.vertical>.progress-bar{width:100%;position:absolute;bottom:0}.progress.vertical.sm,.progress.vertical.progress-sm{width:20px}.progress.vertical.xs,.progress.vertical.progress-xs{width:10px}.progress.vertical.xxs,.progress.vertical.progress-xxs{width:3px}.progress-group .progress-text{font-weight:600}.progress-group .progress-number{float:right}.table tr>td .progress{margin:0}.progress-bar-light-blue,.progress-bar-primary{background-color:#303f9f}.progress-striped .progress-bar-light-blue,.progress-striped .progress-bar-primary{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-green,.progress-bar-success{background-color:#00a65a}.progress-striped .progress-bar-green,.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-aqua,.progress-bar-info{background-color:#00c0ef}.progress-striped .progress-bar-aqua,.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-yellow,.progress-bar-warning{background-color:#f39c12}.progress-striped .progress-bar-yellow,.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-red,.progress-bar-danger{background-color:#dd4b39}.progress-striped .progress-bar-red,.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.small-box{border-radius:2px;position:relative;display:block;margin-bottom:20px;box-shadow:0 1px 1px rgba(0,0,0,0.1)}.small-box>.inner{padding:10px}.small-box>.small-box-footer{position:relative;text-align:center;padding:3px 0;color:#fff;color:rgba(255,255,255,0.8);display:block;z-index:10;background:rgba(0,0,0,0.1);text-decoration:none}.small-box>.small-box-footer:hover{color:#fff;background:rgba(0,0,0,0.15)}.small-box h3{font-size:38px;font-weight:bold;margin:0 0 10px 0;white-space:nowrap;padding:0}.small-box p{font-size:15px}.small-box p>small{display:block;color:#f9f9f9;font-size:13px;margin-top:5px}.small-box h3,.small-box p{z-index:5}.small-box .icon{-webkit-transition:all .3s linear;-o-transition:all .3s linear;transition:all .3s linear;position:absolute;top:-10px;right:10px;z-index:0;font-size:90px;color:rgba(0,0,0,0.15)}.small-box:hover{text-decoration:none;color:#f9f9f9}.small-box:hover .icon{font-size:95px}@media (max-width:767px){.small-box{text-align:center}.small-box .icon{display:none}.small-box p{font-size:12px}}.box{position:relative;border-radius:3px;background:#ffffff;border-top:3px solid #d2d6de;margin-bottom:20px;width:100%;box-shadow:0 1px 1px rgba(0,0,0,0.1)}.box.box-primary{border-top-color:#303f9f}.box.box-info{border-top-color:#00c0ef}.box.box-danger{border-top-color:#dd4b39}.box.box-warning{border-top-color:#f39c12}.box.box-success{border-top-color:#00a65a}.box.box-default{border-top-color:#d2d6de}.box.collapsed-box .box-body,.box.collapsed-box .box-footer{display:none}.box .nav-stacked>li{border-bottom:1px solid #f4f4f4;margin:0}.box .nav-stacked>li:last-of-type{border-bottom:none}.box.height-control .box-body{max-height:300px;overflow:auto}.box .border-right{border-right:1px solid #f4f4f4}.box .border-left{border-left:1px solid #f4f4f4}.box.box-solid{border-top:0}.box.box-solid>.box-header .btn.btn-default{background:transparent}.box.box-solid>.box-header .btn:hover,.box.box-solid>.box-header a:hover{background:rgba(0,0,0,0.1)}.box.box-solid.box-default{border:1px solid #d2d6de}.box.box-solid.box-default>.box-header{color:#444;background:#d2d6de;background-color:#d2d6de}.box.box-solid.box-default>.box-header a,.box.box-solid.box-default>.box-header .btn{color:#444}.box.box-solid.box-primary{border:1px solid #303f9f}.box.box-solid.box-primary>.box-header{color:#fff;background:#303f9f;background-color:#303f9f}.box.box-solid.box-primary>.box-header a,.box.box-solid.box-primary>.box-header .btn{color:#fff}.box.box-solid.box-info{border:1px solid #00c0ef}.box.box-solid.box-info>.box-header{color:#fff;background:#00c0ef;background-color:#00c0ef}.box.box-solid.box-info>.box-header a,.box.box-solid.box-info>.box-header .btn{color:#fff}.box.box-solid.box-danger{border:1px solid #dd4b39}.box.box-solid.box-danger>.box-header{color:#fff;background:#dd4b39;background-color:#dd4b39}.box.box-solid.box-danger>.box-header a,.box.box-solid.box-danger>.box-header .btn{color:#fff}.box.box-solid.box-warning{border:1px solid #f39c12}.box.box-solid.box-warning>.box-header{color:#fff;background:#f39c12;background-color:#f39c12}.box.box-solid.box-warning>.box-header a,.box.box-solid.box-warning>.box-header .btn{color:#fff}.box.box-solid.box-success{border:1px solid #00a65a}.box.box-solid.box-success>.box-header{color:#fff;background:#00a65a;background-color:#00a65a}.box.box-solid.box-success>.box-header a,.box.box-solid.box-success>.box-header .btn{color:#fff}.box.box-solid>.box-header>.box-tools .btn{border:0;box-shadow:none}.box.box-solid[class*='bg']>.box-header{color:#fff}.box .box-group>.box{margin-bottom:5px}.box .knob-label{text-align:center;color:#333;font-weight:100;font-size:12px;margin-bottom:0.3em}.box>.overlay,.overlay-wrapper>.overlay,.box>.loading-img,.overlay-wrapper>.loading-img{position:absolute;top:0;left:0;width:100%;height:100%}.box .overlay,.overlay-wrapper .overlay{z-index:50;background:rgba(255,255,255,0.7);border-radius:3px}.box .overlay>.fa,.overlay-wrapper .overlay>.fa{position:absolute;top:50%;left:50%;margin-left:-15px;margin-top:-15px;color:#000;font-size:30px}.box .overlay.dark,.overlay-wrapper .overlay.dark{background:rgba(0,0,0,0.5)}.box-header:before,.box-body:before,.box-footer:before,.box-header:after,.box-body:after,.box-footer:after{content:" ";display:table}.box-header:after,.box-body:after,.box-footer:after{clear:both}.box-header{color:#444;display:block;padding:10px;position:relative}.box-header.with-border{border-bottom:1px solid #f4f4f4}.collapsed-box .box-header.with-border{border-bottom:none}.box-header>.fa,.box-header>.glyphicon,.box-header>.ion,.box-header .box-title{display:inline-block;font-size:18px;margin:0;line-height:1}.box-header>.fa,.box-header>.glyphicon,.box-header>.ion{margin-right:5px}.box-header>.box-tools{position:absolute;right:10px;top:5px}.box-header>.box-tools [data-toggle="tooltip"]{position:relative}.box-header>.box-tools.pull-right .dropdown-menu{right:0;left:auto}.box-header>.box-tools .dropdown-menu>li>a{color:#444!important}.btn-box-tool{padding:5px;font-size:12px;background:transparent;color:#97a0b3}.open .btn-box-tool,.btn-box-tool:hover{color:#606c84}.btn-box-tool.btn:active{box-shadow:none}.box-body{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px;padding:10px}.no-header .box-body{border-top-right-radius:3px;border-top-left-radius:3px}.box-body>.table{margin-bottom:0}.box-body .fc{margin-top:5px}.box-body .full-width-chart{margin:-19px}.box-body.no-padding .full-width-chart{margin:-9px}.box-body .box-pane{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:3px}.box-body .box-pane-right{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:3px;border-bottom-left-radius:0}.box-footer{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px;border-top:1px solid #f4f4f4;padding:10px;background-color:#fff}.chart-legend{margin:10px 0}@media (max-width:991px){.chart-legend>li{float:left;margin-right:10px}}.box-comments{background:#f7f7f7}.box-comments .box-comment{padding:8px 0;border-bottom:1px solid #eee}.box-comments .box-comment:before,.box-comments .box-comment:after{content:" ";display:table}.box-comments .box-comment:after{clear:both}.box-comments .box-comment:last-of-type{border-bottom:0}.box-comments .box-comment:first-of-type{padding-top:0}.box-comments .box-comment img{float:left}.box-comments .comment-text{margin-left:40px;color:#555}.box-comments .username{color:#444;display:block;font-weight:600}.box-comments .text-muted{font-weight:400;font-size:12px}.todo-list{margin:0;padding:0;list-style:none;overflow:auto}.todo-list>li{border-radius:2px;padding:10px;background:#f4f4f4;margin-bottom:2px;border-left:2px solid #e6e7e8;color:#444}.todo-list>li:last-of-type{margin-bottom:0}.todo-list>li>input[type='checkbox']{margin:0 10px 0 5px}.todo-list>li .text{display:inline-block;margin-left:5px;font-weight:600}.todo-list>li .label{margin-left:10px;font-size:9px}.todo-list>li .tools{display:none;float:right;color:#dd4b39}.todo-list>li .tools>.fa,.todo-list>li .tools>.glyphicon,.todo-list>li .tools>.ion{margin-right:5px;cursor:pointer}.todo-list>li:hover .tools{display:inline-block}.todo-list>li.done{color:#999}.todo-list>li.done .text{text-decoration:line-through;font-weight:500}.todo-list>li.done .label{background:#d2d6de !important}.todo-list .danger{border-left-color:#dd4b39}.todo-list .warning{border-left-color:#f39c12}.todo-list .info{border-left-color:#00c0ef}.todo-list .success{border-left-color:#00a65a}.todo-list .primary{border-left-color:#303f9f}.todo-list .handle{display:inline-block;cursor:move;margin:0 5px}.chat{padding:5px 20px 5px 10px}.chat .item{margin-bottom:10px}.chat .item:before,.chat .item:after{content:" ";display:table}.chat .item:after{clear:both}.chat .item>img{width:40px;height:40px;border:2px solid transparent;border-radius:50%}.chat .item>.online{border:2px solid #00a65a}.chat .item>.offline{border:2px solid #dd4b39}.chat .item>.message{margin-left:55px;margin-top:-40px}.chat .item>.message>.name{display:block;font-weight:600}.chat .item>.attachment{border-radius:3px;background:#f4f4f4;margin-left:65px;margin-right:15px;padding:10px}.chat .item>.attachment>h4{margin:0 0 5px 0;font-weight:600;font-size:14px}.chat .item>.attachment>p,.chat .item>.attachment>.filename{font-weight:600;font-size:13px;font-style:italic;margin:0}.chat .item>.attachment:before,.chat .item>.attachment:after{content:" ";display:table}.chat .item>.attachment:after{clear:both}.box-input{max-width:200px}.modal .panel-body{color:#444}.info-box{display:block;min-height:90px;background:#fff;width:100%;box-shadow:0 1px 1px rgba(0,0,0,0.1);border-radius:2px;margin-bottom:15px}.info-box small{font-size:14px}.info-box .progress{background:rgba(0,0,0,0.2);margin:5px -10px 5px -10px;height:2px}.info-box .progress,.info-box .progress .progress-bar{border-radius:0}.info-box .progress .progress-bar{background:#fff}.info-box-icon{border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px;display:block;float:left;height:90px;width:90px;text-align:center;font-size:45px;line-height:90px;background:rgba(0,0,0,0.2)}.info-box-icon>img{max-width:100%}.info-box-content{padding:5px 10px;margin-left:90px}.info-box-number{display:block;font-weight:bold;font-size:18px}.progress-description,.info-box-text{display:block;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.info-box-text{text-transform:uppercase}.info-box-more{display:block}.progress-description{margin:0}.timeline{position:relative;margin:0 0 30px 0;padding:0;list-style:none}.timeline:before{content:'';position:absolute;top:0;bottom:0;width:4px;background:#ddd;left:31px;margin:0;border-radius:2px}.timeline>li{position:relative;margin-right:10px;margin-bottom:15px}.timeline>li:before,.timeline>li:after{content:" ";display:table}.timeline>li:after{clear:both}.timeline>li>.timeline-item{-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.1);box-shadow:0 1px 1px rgba(0,0,0,0.1);border-radius:3px;margin-top:0;background:#fff;color:#444;margin-left:60px;margin-right:15px;padding:0;position:relative}.timeline>li>.timeline-item>.time{color:#999;float:right;padding:10px;font-size:12px}.timeline>li>.timeline-item>.timeline-header{margin:0;color:#555;border-bottom:1px solid #f4f4f4;padding:10px;font-size:16px;line-height:1.1}.timeline>li>.timeline-item>.timeline-header>a{font-weight:600}.timeline>li>.timeline-item>.timeline-body,.timeline>li>.timeline-item>.timeline-footer{padding:10px}.timeline>li>.fa,.timeline>li>.glyphicon,.timeline>li>.ion{width:30px;height:30px;font-size:15px;line-height:30px;position:absolute;color:#666;background:#d2d6de;border-radius:50%;text-align:center;left:18px;top:0}.timeline>.time-label>span{font-weight:600;padding:5px;display:inline-block;background-color:#fff;border-radius:4px}.timeline-inverse>li>.timeline-item{background:#f0f0f0;border:1px solid #ddd;-webkit-box-shadow:none;box-shadow:none}.timeline-inverse>li>.timeline-item>.timeline-header{border-bottom-color:#ddd}.btn{border-radius:3px;-webkit-box-shadow:none;box-shadow:none;border:1px solid transparent}.btn.uppercase{text-transform:uppercase}.btn.btn-flat{border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;border-width:1px}.btn:active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);-moz-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn:focus{outline:none}.btn.btn-file{position:relative;overflow:hidden}.btn.btn-file>input[type='file']{position:absolute;top:0;right:0;min-width:100%;min-height:100%;font-size:100px;text-align:right;opacity:0;filter:alpha(opacity=0);outline:none;background:white;cursor:inherit;display:block}.btn-default{background-color:#f4f4f4;color:#444;border-color:#ddd}.btn-default:hover,.btn-default:active,.btn-default.hover{background-color:#e7e7e7}.btn-primary{background-color:#303f9f;border-color:#2a378b}.btn-primary:hover,.btn-primary:active,.btn-primary.hover{background-color:#2a378b}.btn-success{background-color:#00a65a;border-color:#008d4c}.btn-success:hover,.btn-success:active,.btn-success.hover{background-color:#008d4c}.btn-info{background-color:#00c0ef;border-color:#00acd6}.btn-info:hover,.btn-info:active,.btn-info.hover{background-color:#00acd6}.btn-danger{background-color:#dd4b39;border-color:#d73925}.btn-danger:hover,.btn-danger:active,.btn-danger.hover{background-color:#d73925}.btn-warning{background-color:#f39c12;border-color:#e08e0b}.btn-warning:hover,.btn-warning:active,.btn-warning.hover{background-color:#e08e0b}.btn-outline{border:1px solid #fff;background:transparent;color:#fff}.btn-outline:hover,.btn-outline:focus,.btn-outline:active{color:rgba(255,255,255,0.7);border-color:rgba(255,255,255,0.7)}.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn[class*='bg-']:hover{-webkit-box-shadow:inset 0 0 100px rgba(0,0,0,0.2);box-shadow:inset 0 0 100px rgba(0,0,0,0.2)}.btn-app{border-radius:3px;position:relative;padding:15px 5px;margin:0 0 10px 10px;min-width:80px;height:60px;text-align:center;color:#666;border:1px solid #ddd;background-color:#f4f4f4;font-size:12px}.btn-app>.fa,.btn-app>.glyphicon,.btn-app>.ion{font-size:20px;display:block}.btn-app:hover{background:#f4f4f4;color:#444;border-color:#aaa}.btn-app:active,.btn-app:focus{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);-moz-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-app>.badge{position:absolute;top:-3px;right:-10px;font-size:10px;font-weight:400}.callout{border-radius:3px;margin:0 0 20px 0;padding:15px 30px 15px 15px;border-left:5px solid #eee}.callout a{color:#fff;text-decoration:underline}.callout a:hover{color:#eee}.callout h4{margin-top:0;font-weight:600}.callout p:last-child{margin-bottom:0}.callout code,.callout .highlight{background-color:#fff}.callout.callout-danger{border-color:#c23321}.callout.callout-warning{border-color:#c87f0a}.callout.callout-info{border-color:#0097bc}.callout.callout-success{border-color:#00733e}.alert{border-radius:3px}.alert h4{font-weight:600}.alert .icon{margin-right:10px}.alert .close{color:#000;opacity:.2;filter:alpha(opacity=20)}.alert .close:hover{opacity:.5;filter:alpha(opacity=50)}.alert a{color:#fff;text-decoration:underline}.alert-success{border-color:#008d4c}.alert-danger,.alert-error{border-color:#d73925}.alert-warning{border-color:#e08e0b}.alert-info{border-color:#00acd6}.nav>li>a:hover,.nav>li>a:active,.nav>li>a:focus{color:#444;background:#f7f7f7}.nav-pills>li>a{border-radius:0;border-top:3px solid transparent;color:#444}.nav-pills>li>a>.fa,.nav-pills>li>a>.glyphicon,.nav-pills>li>a>.ion{margin-right:5px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{border-top-color:#303f9f}.nav-pills>li.active>a{font-weight:600}.nav-stacked>li>a{border-radius:0;border-top:0;border-left:3px solid transparent;color:#444}.nav-stacked>li.active>a,.nav-stacked>li.active>a:hover{background:transparent;color:#444;border-top:0;border-left-color:#303f9f}.nav-stacked>li.header{border-bottom:1px solid #ddd;color:#777;margin-bottom:10px;padding:5px 10px;text-transform:uppercase}.nav-tabs-custom{margin-bottom:20px;background:#fff;box-shadow:0 1px 1px rgba(0,0,0,0.1);border-radius:3px}.nav-tabs-custom>.nav-tabs{margin:0;border-bottom-color:#f4f4f4;border-top-right-radius:3px;border-top-left-radius:3px}.nav-tabs-custom>.nav-tabs>li{border-top:3px solid transparent;margin-bottom:-2px;margin-right:5px}.nav-tabs-custom>.nav-tabs>li.disabled>a{color:#777}.nav-tabs-custom>.nav-tabs>li>a{color:#444;border-radius:0}.nav-tabs-custom>.nav-tabs>li>a.text-muted{color:#999}.nav-tabs-custom>.nav-tabs>li>a,.nav-tabs-custom>.nav-tabs>li>a:hover{background:transparent;margin:0}.nav-tabs-custom>.nav-tabs>li>a:hover{color:#999}.nav-tabs-custom>.nav-tabs>li:not(.active)>a:hover,.nav-tabs-custom>.nav-tabs>li:not(.active)>a:focus,.nav-tabs-custom>.nav-tabs>li:not(.active)>a:active{border-color:transparent}.nav-tabs-custom>.nav-tabs>li.active{border-top-color:#303f9f}.nav-tabs-custom>.nav-tabs>li.active>a,.nav-tabs-custom>.nav-tabs>li.active:hover>a{background-color:#fff;color:#444}.nav-tabs-custom>.nav-tabs>li.active>a{border-top-color:transparent;border-left-color:#f4f4f4;border-right-color:#f4f4f4}.nav-tabs-custom>.nav-tabs>li:first-of-type{margin-left:0}.nav-tabs-custom>.nav-tabs>li:first-of-type.active>a{border-left-color:transparent}.nav-tabs-custom>.nav-tabs.pull-right{float:none !important}.nav-tabs-custom>.nav-tabs.pull-right>li{float:right}.nav-tabs-custom>.nav-tabs.pull-right>li:first-of-type{margin-right:0}.nav-tabs-custom>.nav-tabs.pull-right>li:first-of-type>a{border-left-width:1px}.nav-tabs-custom>.nav-tabs.pull-right>li:first-of-type.active>a{border-left-color:#f4f4f4;border-right-color:transparent}.nav-tabs-custom>.nav-tabs>li.header{line-height:35px;padding:0 10px;font-size:20px;color:#444}.nav-tabs-custom>.nav-tabs>li.header>.fa,.nav-tabs-custom>.nav-tabs>li.header>.glyphicon,.nav-tabs-custom>.nav-tabs>li.header>.ion{margin-right:5px}.nav-tabs-custom>.tab-content{background:#fff;padding:10px;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.nav-tabs-custom .dropdown.open>a:active,.nav-tabs-custom .dropdown.open>a:focus{background:transparent;color:#999}.nav-tabs-custom.tab-primary>.nav-tabs>li.active{border-top-color:#303f9f}.nav-tabs-custom.tab-info>.nav-tabs>li.active{border-top-color:#00c0ef}.nav-tabs-custom.tab-danger>.nav-tabs>li.active{border-top-color:#dd4b39}.nav-tabs-custom.tab-warning>.nav-tabs>li.active{border-top-color:#f39c12}.nav-tabs-custom.tab-success>.nav-tabs>li.active{border-top-color:#00a65a}.nav-tabs-custom.tab-default>.nav-tabs>li.active{border-top-color:#d2d6de}.pagination>li>a{background:#fafafa;color:#666}.pagination.pagination-flat>li>a{border-radius:0 !important}.products-list{list-style:none;margin:0;padding:0}.products-list>.item{border-radius:3px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.1);box-shadow:0 1px 1px rgba(0,0,0,0.1);padding:10px 0;background:#fff}.products-list>.item:before,.products-list>.item:after{content:" ";display:table}.products-list>.item:after{clear:both}.products-list .product-img{float:left}.products-list .product-img img{width:50px;height:50px}.products-list .product-info{margin-left:60px}.products-list .product-title{font-weight:600}.products-list .product-description{display:block;color:#999;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.product-list-in-box>.item{-webkit-box-shadow:none;box-shadow:none;border-radius:0;border-bottom:1px solid #f4f4f4}.product-list-in-box>.item:last-of-type{border-bottom-width:0}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{border-top:1px solid #f4f4f4}.table>thead>tr>th{border-bottom:2px solid #f4f4f4}.table tr td .progress{margin-top:5px}.table-bordered{border:1px solid #f4f4f4}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #f4f4f4}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table.no-border,.table.no-border td,.table.no-border th{border:0}table.text-center,table.text-center td,table.text-center th{text-align:center}.table.align th{text-align:left}.table.align td{text-align:right}.label-default{background-color:#d2d6de;color:#444}.direct-chat .box-body{border-bottom-right-radius:0;border-bottom-left-radius:0;position:relative;overflow-x:hidden;padding:0}.direct-chat.chat-pane-open .direct-chat-contacts{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0)}.direct-chat-messages{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0);padding:10px;height:250px;overflow:auto}.direct-chat-msg,.direct-chat-text{display:block}.direct-chat-msg{margin-bottom:10px}.direct-chat-msg:before,.direct-chat-msg:after{content:" ";display:table}.direct-chat-msg:after{clear:both}.direct-chat-messages,.direct-chat-contacts{-webkit-transition:-webkit-transform .5s ease-in-out;-moz-transition:-moz-transform .5s ease-in-out;-o-transition:-o-transform .5s ease-in-out;transition:transform .5s ease-in-out}.direct-chat-text{border-radius:5px;position:relative;padding:5px 10px;background:#d2d6de;border:1px solid #d2d6de;margin:5px 0 0 50px;color:#444}.direct-chat-text:after,.direct-chat-text:before{position:absolute;right:100%;top:15px;border:solid transparent;border-right-color:#d2d6de;content:' ';height:0;width:0;pointer-events:none}.direct-chat-text:after{border-width:5px;margin-top:-5px}.direct-chat-text:before{border-width:6px;margin-top:-6px}.right .direct-chat-text{margin-right:50px;margin-left:0}.right .direct-chat-text:after,.right .direct-chat-text:before{right:auto;left:100%;border-right-color:transparent;border-left-color:#d2d6de}.direct-chat-img{border-radius:50%;float:left;width:40px;height:40px}.right .direct-chat-img{float:right}.direct-chat-info{display:block;margin-bottom:2px;font-size:12px}.direct-chat-name{font-weight:600}.direct-chat-timestamp{color:#999}.direct-chat-contacts-open .direct-chat-contacts{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0)}.direct-chat-contacts{-webkit-transform:translate(101%, 0);-ms-transform:translate(101%, 0);-o-transform:translate(101%, 0);transform:translate(101%, 0);position:absolute;top:0;bottom:0;height:250px;width:100%;background:#222d32;color:#fff;overflow:auto}.contacts-list>li{border-bottom:1px solid rgba(0,0,0,0.2);padding:10px;margin:0}.contacts-list>li:before,.contacts-list>li:after{content:" ";display:table}.contacts-list>li:after{clear:both}.contacts-list>li:last-of-type{border-bottom:none}.contacts-list-img{border-radius:50%;width:40px;float:left}.contacts-list-info{margin-left:45px;color:#fff}.contacts-list-name,.contacts-list-status{display:block}.contacts-list-name{font-weight:600}.contacts-list-status{font-size:12px}.contacts-list-date{color:#aaa;font-weight:normal}.contacts-list-msg{color:#999}.direct-chat-danger .right>.direct-chat-text{background:#dd4b39;border-color:#dd4b39;color:#fff}.direct-chat-danger .right>.direct-chat-text:after,.direct-chat-danger .right>.direct-chat-text:before{border-left-color:#dd4b39}.direct-chat-primary .right>.direct-chat-text{background:#303f9f;border-color:#303f9f;color:#fff}.direct-chat-primary .right>.direct-chat-text:after,.direct-chat-primary .right>.direct-chat-text:before{border-left-color:#303f9f}.direct-chat-warning .right>.direct-chat-text{background:#f39c12;border-color:#f39c12;color:#fff}.direct-chat-warning .right>.direct-chat-text:after,.direct-chat-warning .right>.direct-chat-text:before{border-left-color:#f39c12}.direct-chat-info .right>.direct-chat-text{background:#00c0ef;border-color:#00c0ef;color:#fff}.direct-chat-info .right>.direct-chat-text:after,.direct-chat-info .right>.direct-chat-text:before{border-left-color:#00c0ef}.direct-chat-success .right>.direct-chat-text{background:#00a65a;border-color:#00a65a;color:#fff}.direct-chat-success .right>.direct-chat-text:after,.direct-chat-success .right>.direct-chat-text:before{border-left-color:#00a65a}.users-list>li{width:25%;float:left;padding:10px;text-align:center}.users-list>li img{border-radius:50%;max-width:100%;height:auto}.users-list>li>a:hover,.users-list>li>a:hover .users-list-name{color:#999}.users-list-name,.users-list-date{display:block}.users-list-name{font-weight:600;color:#444;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.users-list-date{color:#999;font-size:12px}.carousel-control.left,.carousel-control.right{background-image:none}.carousel-control>.fa{font-size:40px;position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-20px}.modal{background:rgba(0,0,0,0.3)}.modal-content{border-radius:0;-webkit-box-shadow:0 2px 3px rgba(0,0,0,0.125);box-shadow:0 2px 3px rgba(0,0,0,0.125);border:0}@media (min-width:768px){.modal-content{-webkit-box-shadow:0 2px 3px rgba(0,0,0,0.125);box-shadow:0 2px 3px rgba(0,0,0,0.125)}}.modal-header{border-bottom-color:#f4f4f4}.modal-footer{border-top-color:#f4f4f4}.modal-primary .modal-header,.modal-primary .modal-footer{border-color:#242f78}.modal-warning .modal-header,.modal-warning .modal-footer{border-color:#c87f0a}.modal-info .modal-header,.modal-info .modal-footer{border-color:#0097bc}.modal-success .modal-header,.modal-success .modal-footer{border-color:#00733e}.modal-danger .modal-header,.modal-danger .modal-footer{border-color:#c23321}.box-widget{border:none;position:relative}.widget-user .widget-user-header{padding:20px;height:120px;border-top-right-radius:3px;border-top-left-radius:3px}.widget-user .widget-user-username{margin-top:0;margin-bottom:5px;font-size:25px;font-weight:300;text-shadow:0 1px 1px rgba(0,0,0,0.2)}.widget-user .widget-user-desc{margin-top:0}.widget-user .widget-user-image{position:absolute;top:65px;left:50%;margin-left:-45px}.widget-user .widget-user-image>img{width:90px;height:auto;border:3px solid #fff}.widget-user .box-footer{padding-top:30px}.widget-user-2 .widget-user-header{padding:20px;border-top-right-radius:3px;border-top-left-radius:3px}.widget-user-2 .widget-user-username{margin-top:5px;margin-bottom:5px;font-size:25px;font-weight:300}.widget-user-2 .widget-user-desc{margin-top:0}.widget-user-2 .widget-user-username,.widget-user-2 .widget-user-desc{margin-left:75px}.widget-user-2 .widget-user-image>img{width:65px;height:auto;float:left}.treeview-menu{display:none;list-style:none;padding:0;margin:0;padding-left:5px}.treeview-menu .treeview-menu{padding-left:20px}.treeview-menu>li{margin:0}.treeview-menu>li>a{padding:5px 5px 5px 15px;display:block;font-size:14px}.treeview-menu>li>a>.fa,.treeview-menu>li>a>.glyphicon,.treeview-menu>li>a>.ion{width:20px}.treeview-menu>li>a>.pull-right-container>.fa-angle-left,.treeview-menu>li>a>.pull-right-container>.fa-angle-down,.treeview-menu>li>a>.fa-angle-left,.treeview-menu>li>a>.fa-angle-down{width:auto}.mailbox-messages>.table{margin:0}.mailbox-controls{padding:5px}.mailbox-controls.with-border{border-bottom:1px solid #f4f4f4}.mailbox-read-info{border-bottom:1px solid #f4f4f4;padding:10px}.mailbox-read-info h3{font-size:20px;margin:0}.mailbox-read-info h5{margin:0;padding:5px 0 0 0}.mailbox-read-time{color:#999;font-size:13px}.mailbox-read-message{padding:10px}.mailbox-attachments li{float:left;width:200px;border:1px solid #eee;margin-bottom:10px;margin-right:10px}.mailbox-attachment-name{font-weight:bold;color:#666}.mailbox-attachment-icon,.mailbox-attachment-info,.mailbox-attachment-size{display:block}.mailbox-attachment-info{padding:10px;background:#f4f4f4}.mailbox-attachment-size{color:#999;font-size:12px}.mailbox-attachment-icon{text-align:center;font-size:65px;color:#666;padding:20px 10px}.mailbox-attachment-icon.has-img{padding:0}.mailbox-attachment-icon.has-img>img{max-width:100%;height:auto}.lockscreen{background:#d2d6de}.lockscreen-logo{font-size:35px;text-align:center;margin-bottom:25px;font-weight:300}.lockscreen-logo a{color:#444}.lockscreen-wrapper{max-width:400px;margin:0 auto;margin-top:10%}.lockscreen .lockscreen-name{text-align:center;font-weight:600}.lockscreen-item{border-radius:4px;padding:0;background:#fff;position:relative;margin:10px auto 30px auto;width:290px}.lockscreen-image{border-radius:50%;position:absolute;left:-10px;top:-25px;background:#fff;padding:5px;z-index:10}.lockscreen-image>img{border-radius:50%;width:70px;height:70px}.lockscreen-credentials{margin-left:70px}.lockscreen-credentials .form-control{border:0}.lockscreen-credentials .btn{background-color:#fff;border:0;padding:0 10px}.lockscreen-footer{margin-top:10px}.login-logo,.register-logo{font-size:35px;text-align:center;margin-bottom:25px;font-weight:300}.login-logo a,.register-logo a{color:#444}.login-page,.register-page{background:#d2d6de}.login-box,.register-box{width:360px;margin:7% auto}@media (max-width:768px){.login-box,.register-box{width:90%;margin-top:20px}}.login-box-body,.register-box-body{background:#fff;padding:20px;border-top:0;color:#666}.login-box-body .form-control-feedback,.register-box-body .form-control-feedback{color:#777}.login-box-msg,.register-box-msg{margin:0;text-align:center;padding:0 20px 20px 20px}.social-auth-links{margin:10px 0}.error-page{width:600px;margin:20px auto 0 auto}@media (max-width:991px){.error-page{width:100%}}.error-page>.headline{float:left;font-size:100px;font-weight:300}@media (max-width:991px){.error-page>.headline{float:none;text-align:center}}.error-page>.error-content{margin-left:190px;display:block}@media (max-width:991px){.error-page>.error-content{margin-left:0}}.error-page>.error-content>h3{font-weight:300;font-size:25px}@media (max-width:991px){.error-page>.error-content>h3{text-align:center}}.invoice{position:relative;background:#fff;border:1px solid #f4f4f4;padding:20px;margin:10px 25px}.invoice-title{margin-top:0}.profile-user-img{margin:0 auto;width:100px;padding:3px;border:3px solid #d2d6de}.profile-username{font-size:21px;margin-top:5px}.post{border-bottom:1px solid #d2d6de;margin-bottom:15px;padding-bottom:15px;color:#666}.post:last-of-type{border-bottom:0;margin-bottom:0;padding-bottom:0}.post .user-block{margin-bottom:15px}.btn-social{position:relative;padding-left:44px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.btn-social>:first-child{position:absolute;left:0;top:0;bottom:0;width:32px;line-height:34px;font-size:1.6em;text-align:center;border-right:1px solid rgba(0,0,0,0.2)}.btn-social.btn-lg{padding-left:61px}.btn-social.btn-lg>:first-child{line-height:45px;width:45px;font-size:1.8em}.btn-social.btn-sm{padding-left:38px}.btn-social.btn-sm>:first-child{line-height:28px;width:28px;font-size:1.4em}.btn-social.btn-xs{padding-left:30px}.btn-social.btn-xs>:first-child{line-height:20px;width:20px;font-size:1.2em}.btn-social-icon{position:relative;padding-left:44px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;height:34px;width:34px;padding:0}.btn-social-icon>:first-child{position:absolute;left:0;top:0;bottom:0;width:32px;line-height:34px;font-size:1.6em;text-align:center;border-right:1px solid rgba(0,0,0,0.2)}.btn-social-icon.btn-lg{padding-left:61px}.btn-social-icon.btn-lg>:first-child{line-height:45px;width:45px;font-size:1.8em}.btn-social-icon.btn-sm{padding-left:38px}.btn-social-icon.btn-sm>:first-child{line-height:28px;width:28px;font-size:1.4em}.btn-social-icon.btn-xs{padding-left:30px}.btn-social-icon.btn-xs>:first-child{line-height:20px;width:20px;font-size:1.2em}.btn-social-icon>:first-child{border:none;text-align:center;width:100%}.btn-social-icon.btn-lg{height:45px;width:45px;padding-left:0;padding-right:0}.btn-social-icon.btn-sm{height:30px;width:30px;padding-left:0;padding-right:0}.btn-social-icon.btn-xs{height:22px;width:22px;padding-left:0;padding-right:0}.btn-adn{color:#fff;background-color:#d87a68;border-color:rgba(0,0,0,0.2)}.btn-adn:focus,.btn-adn.focus{color:#fff;background-color:#ce563f;border-color:rgba(0,0,0,0.2)}.btn-adn:hover{color:#fff;background-color:#ce563f;border-color:rgba(0,0,0,0.2)}.btn-adn:active,.btn-adn.active,.open>.dropdown-toggle.btn-adn{color:#fff;background-color:#ce563f;border-color:rgba(0,0,0,0.2)}.btn-adn:active,.btn-adn.active,.open>.dropdown-toggle.btn-adn{background-image:none}.btn-adn .badge{color:#d87a68;background-color:#fff}.btn-bitbucket{color:#fff;background-color:#205081;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:focus,.btn-bitbucket.focus{color:#fff;background-color:#163758;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:hover{color:#fff;background-color:#163758;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:active,.btn-bitbucket.active,.open>.dropdown-toggle.btn-bitbucket{color:#fff;background-color:#163758;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:active,.btn-bitbucket.active,.open>.dropdown-toggle.btn-bitbucket{background-image:none}.btn-bitbucket .badge{color:#205081;background-color:#fff}.btn-dropbox{color:#fff;background-color:#1087dd;border-color:rgba(0,0,0,0.2)}.btn-dropbox:focus,.btn-dropbox.focus{color:#fff;background-color:#0d6aad;border-color:rgba(0,0,0,0.2)}.btn-dropbox:hover{color:#fff;background-color:#0d6aad;border-color:rgba(0,0,0,0.2)}.btn-dropbox:active,.btn-dropbox.active,.open>.dropdown-toggle.btn-dropbox{color:#fff;background-color:#0d6aad;border-color:rgba(0,0,0,0.2)}.btn-dropbox:active,.btn-dropbox.active,.open>.dropdown-toggle.btn-dropbox{background-image:none}.btn-dropbox .badge{color:#1087dd;background-color:#fff}.btn-facebook{color:#fff;background-color:#3b5998;border-color:rgba(0,0,0,0.2)}.btn-facebook:focus,.btn-facebook.focus{color:#fff;background-color:#2d4373;border-color:rgba(0,0,0,0.2)}.btn-facebook:hover{color:#fff;background-color:#2d4373;border-color:rgba(0,0,0,0.2)}.btn-facebook:active,.btn-facebook.active,.open>.dropdown-toggle.btn-facebook{color:#fff;background-color:#2d4373;border-color:rgba(0,0,0,0.2)}.btn-facebook:active,.btn-facebook.active,.open>.dropdown-toggle.btn-facebook{background-image:none}.btn-facebook .badge{color:#3b5998;background-color:#fff}.btn-flickr{color:#fff;background-color:#ff0084;border-color:rgba(0,0,0,0.2)}.btn-flickr:focus,.btn-flickr.focus{color:#fff;background-color:#cc006a;border-color:rgba(0,0,0,0.2)}.btn-flickr:hover{color:#fff;background-color:#cc006a;border-color:rgba(0,0,0,0.2)}.btn-flickr:active,.btn-flickr.active,.open>.dropdown-toggle.btn-flickr{color:#fff;background-color:#cc006a;border-color:rgba(0,0,0,0.2)}.btn-flickr:active,.btn-flickr.active,.open>.dropdown-toggle.btn-flickr{background-image:none}.btn-flickr .badge{color:#ff0084;background-color:#fff}.btn-foursquare{color:#fff;background-color:#f94877;border-color:rgba(0,0,0,0.2)}.btn-foursquare:focus,.btn-foursquare.focus{color:#fff;background-color:#f71752;border-color:rgba(0,0,0,0.2)}.btn-foursquare:hover{color:#fff;background-color:#f71752;border-color:rgba(0,0,0,0.2)}.btn-foursquare:active,.btn-foursquare.active,.open>.dropdown-toggle.btn-foursquare{color:#fff;background-color:#f71752;border-color:rgba(0,0,0,0.2)}.btn-foursquare:active,.btn-foursquare.active,.open>.dropdown-toggle.btn-foursquare{background-image:none}.btn-foursquare .badge{color:#f94877;background-color:#fff}.btn-github{color:#fff;background-color:#444;border-color:rgba(0,0,0,0.2)}.btn-github:focus,.btn-github.focus{color:#fff;background-color:#2b2b2b;border-color:rgba(0,0,0,0.2)}.btn-github:hover{color:#fff;background-color:#2b2b2b;border-color:rgba(0,0,0,0.2)}.btn-github:active,.btn-github.active,.open>.dropdown-toggle.btn-github{color:#fff;background-color:#2b2b2b;border-color:rgba(0,0,0,0.2)}.btn-github:active,.btn-github.active,.open>.dropdown-toggle.btn-github{background-image:none}.btn-github .badge{color:#444;background-color:#fff}.btn-google{color:#fff;background-color:#dd4b39;border-color:rgba(0,0,0,0.2)}.btn-google:focus,.btn-google.focus{color:#fff;background-color:#c23321;border-color:rgba(0,0,0,0.2)}.btn-google:hover{color:#fff;background-color:#c23321;border-color:rgba(0,0,0,0.2)}.btn-google:active,.btn-google.active,.open>.dropdown-toggle.btn-google{color:#fff;background-color:#c23321;border-color:rgba(0,0,0,0.2)}.btn-google:active,.btn-google.active,.open>.dropdown-toggle.btn-google{background-image:none}.btn-google .badge{color:#dd4b39;background-color:#fff}.btn-instagram{color:#fff;background-color:#3f729b;border-color:rgba(0,0,0,0.2)}.btn-instagram:focus,.btn-instagram.focus{color:#fff;background-color:#305777;border-color:rgba(0,0,0,0.2)}.btn-instagram:hover{color:#fff;background-color:#305777;border-color:rgba(0,0,0,0.2)}.btn-instagram:active,.btn-instagram.active,.open>.dropdown-toggle.btn-instagram{color:#fff;background-color:#305777;border-color:rgba(0,0,0,0.2)}.btn-instagram:active,.btn-instagram.active,.open>.dropdown-toggle.btn-instagram{background-image:none}.btn-instagram .badge{color:#3f729b;background-color:#fff}.btn-linkedin{color:#fff;background-color:#007bb6;border-color:rgba(0,0,0,0.2)}.btn-linkedin:focus,.btn-linkedin.focus{color:#fff;background-color:#005983;border-color:rgba(0,0,0,0.2)}.btn-linkedin:hover{color:#fff;background-color:#005983;border-color:rgba(0,0,0,0.2)}.btn-linkedin:active,.btn-linkedin.active,.open>.dropdown-toggle.btn-linkedin{color:#fff;background-color:#005983;border-color:rgba(0,0,0,0.2)}.btn-linkedin:active,.btn-linkedin.active,.open>.dropdown-toggle.btn-linkedin{background-image:none}.btn-linkedin .badge{color:#007bb6;background-color:#fff}.btn-microsoft{color:#fff;background-color:#2672ec;border-color:rgba(0,0,0,0.2)}.btn-microsoft:focus,.btn-microsoft.focus{color:#fff;background-color:#125acd;border-color:rgba(0,0,0,0.2)}.btn-microsoft:hover{color:#fff;background-color:#125acd;border-color:rgba(0,0,0,0.2)}.btn-microsoft:active,.btn-microsoft.active,.open>.dropdown-toggle.btn-microsoft{color:#fff;background-color:#125acd;border-color:rgba(0,0,0,0.2)}.btn-microsoft:active,.btn-microsoft.active,.open>.dropdown-toggle.btn-microsoft{background-image:none}.btn-microsoft .badge{color:#2672ec;background-color:#fff}.btn-openid{color:#fff;background-color:#f7931e;border-color:rgba(0,0,0,0.2)}.btn-openid:focus,.btn-openid.focus{color:#fff;background-color:#da7908;border-color:rgba(0,0,0,0.2)}.btn-openid:hover{color:#fff;background-color:#da7908;border-color:rgba(0,0,0,0.2)}.btn-openid:active,.btn-openid.active,.open>.dropdown-toggle.btn-openid{color:#fff;background-color:#da7908;border-color:rgba(0,0,0,0.2)}.btn-openid:active,.btn-openid.active,.open>.dropdown-toggle.btn-openid{background-image:none}.btn-openid .badge{color:#f7931e;background-color:#fff}.btn-pinterest{color:#fff;background-color:#cb2027;border-color:rgba(0,0,0,0.2)}.btn-pinterest:focus,.btn-pinterest.focus{color:#fff;background-color:#9f191f;border-color:rgba(0,0,0,0.2)}.btn-pinterest:hover{color:#fff;background-color:#9f191f;border-color:rgba(0,0,0,0.2)}.btn-pinterest:active,.btn-pinterest.active,.open>.dropdown-toggle.btn-pinterest{color:#fff;background-color:#9f191f;border-color:rgba(0,0,0,0.2)}.btn-pinterest:active,.btn-pinterest.active,.open>.dropdown-toggle.btn-pinterest{background-image:none}.btn-pinterest .badge{color:#cb2027;background-color:#fff}.btn-reddit{color:#000;background-color:#eff7ff;border-color:rgba(0,0,0,0.2)}.btn-reddit:focus,.btn-reddit.focus{color:#000;background-color:#bcddff;border-color:rgba(0,0,0,0.2)}.btn-reddit:hover{color:#000;background-color:#bcddff;border-color:rgba(0,0,0,0.2)}.btn-reddit:active,.btn-reddit.active,.open>.dropdown-toggle.btn-reddit{color:#000;background-color:#bcddff;border-color:rgba(0,0,0,0.2)}.btn-reddit:active,.btn-reddit.active,.open>.dropdown-toggle.btn-reddit{background-image:none}.btn-reddit .badge{color:#eff7ff;background-color:#000}.btn-soundcloud{color:#fff;background-color:#f50;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:focus,.btn-soundcloud.focus{color:#fff;background-color:#c40;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:hover{color:#fff;background-color:#c40;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:active,.btn-soundcloud.active,.open>.dropdown-toggle.btn-soundcloud{color:#fff;background-color:#c40;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:active,.btn-soundcloud.active,.open>.dropdown-toggle.btn-soundcloud{background-image:none}.btn-soundcloud .badge{color:#f50;background-color:#fff}.btn-tumblr{color:#fff;background-color:#2c4762;border-color:rgba(0,0,0,0.2)}.btn-tumblr:focus,.btn-tumblr.focus{color:#fff;background-color:#1c2d3f;border-color:rgba(0,0,0,0.2)}.btn-tumblr:hover{color:#fff;background-color:#1c2d3f;border-color:rgba(0,0,0,0.2)}.btn-tumblr:active,.btn-tumblr.active,.open>.dropdown-toggle.btn-tumblr{color:#fff;background-color:#1c2d3f;border-color:rgba(0,0,0,0.2)}.btn-tumblr:active,.btn-tumblr.active,.open>.dropdown-toggle.btn-tumblr{background-image:none}.btn-tumblr .badge{color:#2c4762;background-color:#fff}.btn-twitter{color:#fff;background-color:#55acee;border-color:rgba(0,0,0,0.2)}.btn-twitter:focus,.btn-twitter.focus{color:#fff;background-color:#2795e9;border-color:rgba(0,0,0,0.2)}.btn-twitter:hover{color:#fff;background-color:#2795e9;border-color:rgba(0,0,0,0.2)}.btn-twitter:active,.btn-twitter.active,.open>.dropdown-toggle.btn-twitter{color:#fff;background-color:#2795e9;border-color:rgba(0,0,0,0.2)}.btn-twitter:active,.btn-twitter.active,.open>.dropdown-toggle.btn-twitter{background-image:none}.btn-twitter .badge{color:#55acee;background-color:#fff}.btn-vimeo{color:#fff;background-color:#1ab7ea;border-color:rgba(0,0,0,0.2)}.btn-vimeo:focus,.btn-vimeo.focus{color:#fff;background-color:#1295bf;border-color:rgba(0,0,0,0.2)}.btn-vimeo:hover{color:#fff;background-color:#1295bf;border-color:rgba(0,0,0,0.2)}.btn-vimeo:active,.btn-vimeo.active,.open>.dropdown-toggle.btn-vimeo{color:#fff;background-color:#1295bf;border-color:rgba(0,0,0,0.2)}.btn-vimeo:active,.btn-vimeo.active,.open>.dropdown-toggle.btn-vimeo{background-image:none}.btn-vimeo .badge{color:#1ab7ea;background-color:#fff}.btn-vk{color:#fff;background-color:#587ea3;border-color:rgba(0,0,0,0.2)}.btn-vk:focus,.btn-vk.focus{color:#fff;background-color:#466482;border-color:rgba(0,0,0,0.2)}.btn-vk:hover{color:#fff;background-color:#466482;border-color:rgba(0,0,0,0.2)}.btn-vk:active,.btn-vk.active,.open>.dropdown-toggle.btn-vk{color:#fff;background-color:#466482;border-color:rgba(0,0,0,0.2)}.btn-vk:active,.btn-vk.active,.open>.dropdown-toggle.btn-vk{background-image:none}.btn-vk .badge{color:#587ea3;background-color:#fff}.btn-yahoo{color:#fff;background-color:#720e9e;border-color:rgba(0,0,0,0.2)}.btn-yahoo:focus,.btn-yahoo.focus{color:#fff;background-color:#500a6f;border-color:rgba(0,0,0,0.2)}.btn-yahoo:hover{color:#fff;background-color:#500a6f;border-color:rgba(0,0,0,0.2)}.btn-yahoo:active,.btn-yahoo.active,.open>.dropdown-toggle.btn-yahoo{color:#fff;background-color:#500a6f;border-color:rgba(0,0,0,0.2)}.btn-yahoo:active,.btn-yahoo.active,.open>.dropdown-toggle.btn-yahoo{background-image:none}.btn-yahoo .badge{color:#720e9e;background-color:#fff}.fc-button{background:#f4f4f4;background-image:none;color:#444;border-color:#ddd;border-bottom-color:#ddd}.fc-button:hover,.fc-button:active,.fc-button.hover{background-color:#e9e9e9}.fc-header-title h2{font-size:15px;line-height:1.6em;color:#666;margin-left:10px}.fc-header-right{padding-right:10px}.fc-header-left{padding-left:10px}.fc-widget-header{background:#fafafa}.fc-grid{width:100%;border:0}.fc-widget-header:first-of-type,.fc-widget-content:first-of-type{border-left:0;border-right:0}.fc-widget-header:last-of-type,.fc-widget-content:last-of-type{border-right:0}.fc-toolbar{padding:10px;margin:0}.fc-day-number{font-size:20px;font-weight:300;padding-right:10px}.fc-color-picker{list-style:none;margin:0;padding:0}.fc-color-picker>li{float:left;font-size:30px;margin-right:5px;line-height:30px}.fc-color-picker>li .fa{-webkit-transition:-webkit-transform linear .3s;-moz-transition:-moz-transform linear .3s;-o-transition:-o-transform linear .3s;transition:transform linear .3s}.fc-color-picker>li .fa:hover{-webkit-transform:rotate(30deg);-ms-transform:rotate(30deg);-o-transform:rotate(30deg);transform:rotate(30deg)}#add-new-event{-webkit-transition:all linear .3s;-o-transition:all linear .3s;transition:all linear .3s}.external-event{padding:5px 10px;font-weight:bold;margin-bottom:4px;box-shadow:0 1px 1px rgba(0,0,0,0.1);text-shadow:0 1px 1px rgba(0,0,0,0.1);border-radius:3px;cursor:move}.external-event:hover{box-shadow:inset 0 0 90px rgba(0,0,0,0.2)}.select2-container--default.select2-container--focus,.select2-selection.select2-container--focus,.select2-container--default:focus,.select2-selection:focus,.select2-container--default:active,.select2-selection:active{outline:none}.select2-container--default .select2-selection--single,.select2-selection .select2-selection--single{border:1px solid #d2d6de;border-radius:0;padding:6px 12px;height:34px}.select2-container--default.select2-container--open{border-color:#303f9f}.select2-dropdown{border:1px solid #d2d6de;border-radius:0}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#303f9f;color:white}.select2-results__option{padding:6px 12px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{padding-left:0;padding-right:0;height:auto;margin-top:-4px}.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered{padding-right:6px;padding-left:20px}.select2-container--default .select2-selection--single .select2-selection__arrow{height:28px;right:3px}.select2-container--default .select2-selection--single .select2-selection__arrow b{margin-top:0}.select2-dropdown .select2-search__field,.select2-search--inline .select2-search__field{border:1px solid #d2d6de}.select2-dropdown .select2-search__field:focus,.select2-search--inline .select2-search__field:focus{outline:none}.select2-container--default.select2-container--focus .select2-selection--multiple,.select2-container--default .select2-search--dropdown .select2-search__field{border-color:#303f9f !important}.select2-container--default .select2-results__option[aria-disabled=true]{color:#999}.select2-container--default .select2-results__option[aria-selected=true]{background-color:#ddd}.select2-container--default .select2-results__option[aria-selected=true],.select2-container--default .select2-results__option[aria-selected=true]:hover{color:#444}.select2-container--default .select2-selection--multiple{border:1px solid #d2d6de;border-radius:0}.select2-container--default .select2-selection--multiple:focus{border-color:#303f9f}.select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#d2d6de}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#303f9f;border-color:#2a378b;padding:1px 10px;color:#fff}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{margin-right:5px;color:rgba(255,255,255,0.7)}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container .select2-selection--single .select2-selection__rendered{padding-right:10px}.box .datepicker-inline,.box .datepicker-inline .datepicker-days,.box .datepicker-inline>table,.box .datepicker-inline .datepicker-days>table{width:100%}.box .datepicker-inline td:hover,.box .datepicker-inline .datepicker-days td:hover,.box .datepicker-inline>table td:hover,.box .datepicker-inline .datepicker-days>table td:hover{background-color:rgba(255,255,255,0.3)}.box .datepicker-inline td.day.old,.box .datepicker-inline .datepicker-days td.day.old,.box .datepicker-inline>table td.day.old,.box .datepicker-inline .datepicker-days>table td.day.old,.box .datepicker-inline td.day.new,.box .datepicker-inline .datepicker-days td.day.new,.box .datepicker-inline>table td.day.new,.box .datepicker-inline .datepicker-days>table td.day.new{color:#777}.pad{padding:10px}.margin{margin:10px}.margin-bottom{margin-bottom:20px}.margin-bottom-none{margin-bottom:0}.margin-r-5{margin-right:5px}.inline{display:inline}.description-block{display:block;margin:10px 0;text-align:center}.description-block.margin-bottom{margin-bottom:25px}.description-block>.description-header{margin:0;padding:0;font-weight:600;font-size:16px}.description-block>.description-text{text-transform:uppercase}.bg-red,.bg-yellow,.bg-aqua,.bg-blue,.bg-light-blue,.bg-green,.bg-navy,.bg-teal,.bg-olive,.bg-lime,.bg-orange,.bg-fuchsia,.bg-purple,.bg-maroon,.bg-black,.bg-red-active,.bg-yellow-active,.bg-aqua-active,.bg-blue-active,.bg-light-blue-active,.bg-green-active,.bg-navy-active,.bg-teal-active,.bg-olive-active,.bg-lime-active,.bg-orange-active,.bg-fuchsia-active,.bg-purple-active,.bg-maroon-active,.bg-black-active,.callout.callout-danger,.callout.callout-warning,.callout.callout-info,.callout.callout-success,.alert-success,.alert-danger,.alert-error,.alert-warning,.alert-info,.label-danger,.label-info,.label-warning,.label-primary,.label-success,.modal-primary .modal-body,.modal-primary .modal-header,.modal-primary .modal-footer,.modal-warning .modal-body,.modal-warning .modal-header,.modal-warning .modal-footer,.modal-info .modal-body,.modal-info .modal-header,.modal-info .modal-footer,.modal-success .modal-body,.modal-success .modal-header,.modal-success .modal-footer,.modal-danger .modal-body,.modal-danger .modal-header,.modal-danger .modal-footer{color:#fff !important}.bg-gray{color:#000;background-color:#d2d6de !important}.bg-gray-light{background-color:#f7f7f7}.bg-black{background-color:#111 !important}.bg-red,.callout.callout-danger,.alert-danger,.alert-error,.label-danger,.modal-danger .modal-body{background-color:#dd4b39 !important}.bg-yellow,.callout.callout-warning,.alert-warning,.label-warning,.modal-warning .modal-body{background-color:#f39c12 !important}.bg-aqua,.callout.callout-info,.alert-info,.label-info,.modal-info .modal-body{background-color:#00c0ef !important}.bg-blue{background-color:#303f9f !important}.bg-light-blue,.label-primary,.modal-primary .modal-body{background-color:#303f9f !important}.bg-green,.callout.callout-success,.alert-success,.label-success,.modal-success .modal-body{background-color:#00a65a !important}.bg-navy{background-color:#001f3f !important}.bg-teal{background-color:#39cccc !important}.bg-olive{background-color:#3d9970 !important}.bg-lime{background-color:#01ff70 !important}.bg-orange{background-color:#ff851b !important}.bg-fuchsia{background-color:#f012be !important}.bg-purple{background-color:#605ca8 !important}.bg-maroon{background-color:#d81b60 !important}.bg-gray-active{color:#000;background-color:#b5bbc8 !important}.bg-black-active{background-color:#000 !important}.bg-red-active,.modal-danger .modal-header,.modal-danger .modal-footer{background-color:#d33724 !important}.bg-yellow-active,.modal-warning .modal-header,.modal-warning .modal-footer{background-color:#db8b0b !important}.bg-aqua-active,.modal-info .modal-header,.modal-info .modal-footer{background-color:#00a7d0 !important}.bg-blue-active{background-color:#242f78 !important}.bg-light-blue-active,.modal-primary .modal-header,.modal-primary .modal-footer{background-color:#293687 !important}.bg-green-active,.modal-success .modal-header,.modal-success .modal-footer{background-color:#008d4c !important}.bg-navy-active{background-color:#001a35 !important}.bg-teal-active{background-color:#30bbbb !important}.bg-olive-active{background-color:#368763 !important}.bg-lime-active{background-color:#00e765 !important}.bg-orange-active{background-color:#ff7701 !important}.bg-fuchsia-active{background-color:#db0ead !important}.bg-purple-active{background-color:#555299 !important}.bg-maroon-active{background-color:#ca195a !important}[class^="bg-"].disabled{opacity:.65;filter:alpha(opacity=65)}.text-red{color:#dd4b39 !important}.text-yellow{color:#f39c12 !important}.text-aqua{color:#00c0ef !important}.text-blue{color:#303f9f !important}.text-black{color:#111 !important}.text-light-blue{color:#303f9f !important}.text-green{color:#00a65a !important}.text-gray{color:#d2d6de !important}.text-navy{color:#001f3f !important}.text-teal{color:#39cccc !important}.text-olive{color:#3d9970 !important}.text-lime{color:#01ff70 !important}.text-orange{color:#ff851b !important}.text-fuchsia{color:#f012be !important}.text-purple{color:#605ca8 !important}.text-maroon{color:#d81b60 !important}.link-muted{color:#7a869d}.link-muted:hover,.link-muted:focus{color:#606c84}.link-black{color:#666}.link-black:hover,.link-black:focus{color:#999}.hide{display:none !important}.no-border{border:0 !important}.no-padding{padding:0 !important}.no-margin{margin:0 !important}.no-shadow{box-shadow:none !important}.list-unstyled,.chart-legend,.contacts-list,.users-list,.mailbox-attachments{list-style:none;margin:0;padding:0}.list-group-unbordered>.list-group-item{border-left:0;border-right:0;border-radius:0;padding-left:0;padding-right:0}.flat{border-radius:0 !important}.text-bold,.text-bold.table td,.text-bold.table th{font-weight:700}.text-sm{font-size:12px}.jqstooltip{padding:5px !important;width:auto !important;height:auto !important}.bg-teal-gradient{background:#39cccc !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #39cccc), color-stop(1, #7adddd)) !important;background:-ms-linear-gradient(bottom, #39cccc, #7adddd) !important;background:-moz-linear-gradient(center bottom, #39cccc 0, #7adddd 100%) !important;background:-o-linear-gradient(#7adddd, #39cccc) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#7adddd', endColorstr='#39cccc', GradientType=0) !important;color:#fff}.bg-light-blue-gradient{background:#303f9f !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #303f9f), color-stop(1, #4557c7)) !important;background:-ms-linear-gradient(bottom, #303f9f, #4557c7) !important;background:-moz-linear-gradient(center bottom, #303f9f 0, #4557c7 100%) !important;background:-o-linear-gradient(#4557c7, #303f9f) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#4557c7', endColorstr='#303f9f', GradientType=0) !important;color:#fff}.bg-blue-gradient{background:#303f9f !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #303f9f), color-stop(1, #384aba)) !important;background:-ms-linear-gradient(bottom, #303f9f, #384aba) !important;background:-moz-linear-gradient(center bottom, #303f9f 0, #384aba 100%) !important;background:-o-linear-gradient(#384aba, #303f9f) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#384aba', endColorstr='#303f9f', GradientType=0) !important;color:#fff}.bg-aqua-gradient{background:#00c0ef !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #00c0ef), color-stop(1, #14d1ff)) !important;background:-ms-linear-gradient(bottom, #00c0ef, #14d1ff) !important;background:-moz-linear-gradient(center bottom, #00c0ef 0, #14d1ff 100%) !important;background:-o-linear-gradient(#14d1ff, #00c0ef) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#14d1ff', endColorstr='#00c0ef', GradientType=0) !important;color:#fff}.bg-yellow-gradient{background:#f39c12 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #f39c12), color-stop(1, #f7bc60)) !important;background:-ms-linear-gradient(bottom, #f39c12, #f7bc60) !important;background:-moz-linear-gradient(center bottom, #f39c12 0, #f7bc60 100%) !important;background:-o-linear-gradient(#f7bc60, #f39c12) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f7bc60', endColorstr='#f39c12', GradientType=0) !important;color:#fff}.bg-purple-gradient{background:#605ca8 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #605ca8), color-stop(1, #9491c4)) !important;background:-ms-linear-gradient(bottom, #605ca8, #9491c4) !important;background:-moz-linear-gradient(center bottom, #605ca8 0, #9491c4 100%) !important;background:-o-linear-gradient(#9491c4, #605ca8) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#9491c4', endColorstr='#605ca8', GradientType=0) !important;color:#fff}.bg-green-gradient{background:#00a65a !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #00a65a), color-stop(1, #00ca6d)) !important;background:-ms-linear-gradient(bottom, #00a65a, #00ca6d) !important;background:-moz-linear-gradient(center bottom, #00a65a 0, #00ca6d 100%) !important;background:-o-linear-gradient(#00ca6d, #00a65a) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ca6d', endColorstr='#00a65a', GradientType=0) !important;color:#fff}.bg-red-gradient{background:#dd4b39 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #dd4b39), color-stop(1, #e47365)) !important;background:-ms-linear-gradient(bottom, #dd4b39, #e47365) !important;background:-moz-linear-gradient(center bottom, #dd4b39 0, #e47365 100%) !important;background:-o-linear-gradient(#e47365, #dd4b39) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#e47365', endColorstr='#dd4b39', GradientType=0) !important;color:#fff}.bg-black-gradient{background:#111 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #111), color-stop(1, #2b2b2b)) !important;background:-ms-linear-gradient(bottom, #111, #2b2b2b) !important;background:-moz-linear-gradient(center bottom, #111 0, #2b2b2b 100%) !important;background:-o-linear-gradient(#2b2b2b, #111) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#2b2b2b', endColorstr='#111111', GradientType=0) !important;color:#fff}.bg-maroon-gradient{background:#d81b60 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #d81b60), color-stop(1, #e73f7c)) !important;background:-ms-linear-gradient(bottom, #d81b60, #e73f7c) !important;background:-moz-linear-gradient(center bottom, #d81b60 0, #e73f7c 100%) !important;background:-o-linear-gradient(#e73f7c, #d81b60) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#e73f7c', endColorstr='#d81b60', GradientType=0) !important;color:#fff}.description-block .description-icon{font-size:16px}.no-pad-top{padding-top:0}.position-static{position:static !important}.list-header{font-size:15px;padding:10px 4px;font-weight:bold;color:#666}.list-seperator{height:1px;background:#f4f4f4;margin:15px 0 9px 0}.list-link>a{padding:4px;color:#777}.list-link>a:hover{color:#222}.font-light{font-weight:300}.user-block:before,.user-block:after{content:" ";display:table}.user-block:after{clear:both}.user-block img{width:40px;height:40px;float:left}.user-block .username,.user-block .description,.user-block .comment{display:block;margin-left:50px}.user-block .username{font-size:16px;font-weight:600}.user-block .description{color:#999;font-size:13px}.user-block.user-block-sm .username,.user-block.user-block-sm .description,.user-block.user-block-sm .comment{margin-left:40px}.user-block.user-block-sm .username{font-size:14px}.img-sm,.img-md,.img-lg,.box-comments .box-comment img,.user-block.user-block-sm img{float:left}.img-sm,.box-comments .box-comment img,.user-block.user-block-sm img{width:30px !important;height:30px !important}.img-sm+.img-push{margin-left:40px}.img-md{width:60px;height:60px}.img-md+.img-push{margin-left:70px}.img-lg{width:100px;height:100px}.img-lg+.img-push{margin-left:110px}.img-bordered{border:3px solid #d2d6de;padding:3px}.img-bordered-sm{border:2px solid #d2d6de;padding:2px}.attachment-block{border:1px solid #f4f4f4;padding:5px;margin-bottom:10px;background:#f7f7f7}.attachment-block .attachment-img{max-width:100px;max-height:100px;height:auto;float:left}.attachment-block .attachment-pushed{margin-left:110px}.attachment-block .attachment-heading{margin:0}.attachment-block .attachment-text{color:#555}.connectedSortable{min-height:100px}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sort-highlight{background:#f4f4f4;border:1px dashed #ddd;margin-bottom:10px}.full-opacity-hover{opacity:.65;filter:alpha(opacity=65)}.full-opacity-hover:hover{opacity:1;filter:alpha(opacity=100)}.chart{position:relative;overflow:hidden;width:100%}.chart svg,.chart canvas{width:100% !important}@media print{.no-print,.main-sidebar,.left-side,.main-header,.content-header{display:none !important}.content-wrapper,.right-side,.main-footer{margin-left:0 !important;min-height:0 !important;-webkit-transform:translate(0, 0) !important;-ms-transform:translate(0, 0) !important;-o-transform:translate(0, 0) !important;transform:translate(0, 0) !important}.fixed .content-wrapper,.fixed .right-side{padding-top:0 !important}.invoice{width:100%;border:0;margin:0;padding:0}.invoice-col{float:left;width:33.3333333%}.table-responsive{overflow:auto}.table-responsive>.table tr th,.table-responsive>.table tr td{white-space:normal !important}} + */html,body{height:100%}.layout-boxed html,.layout-boxed body{height:100%}body{font-family:'Source Sans Pro','Helvetica Neue',Helvetica,Arial,sans-serif;font-weight:400;overflow-x:hidden;overflow-y:auto}.wrapper{height:100%;position:relative;overflow-x:hidden;overflow-y:auto}.wrapper:before,.wrapper:after{content:" ";display:table}.wrapper:after{clear:both}.layout-boxed .wrapper{max-width:1250px;margin:0 auto;min-height:100%;box-shadow:0 0 8px rgba(0,0,0,0.5);position:relative}.layout-boxed{background:url('../img/boxed-bg.jpg') repeat fixed}.content-wrapper,.main-footer{-webkit-transition:-webkit-transform .3s ease-in-out,margin .3s ease-in-out;-moz-transition:-moz-transform .3s ease-in-out,margin .3s ease-in-out;-o-transition:-o-transform .3s ease-in-out,margin .3s ease-in-out;transition:transform .3s ease-in-out,margin .3s ease-in-out;margin-left:230px;z-index:820}.layout-top-nav .content-wrapper,.layout-top-nav .main-footer{margin-left:0}@media (max-width:767px){.content-wrapper,.main-footer{margin-left:0}}@media (min-width:768px){.sidebar-collapse .content-wrapper,.sidebar-collapse .main-footer{margin-left:0}}@media (max-width:767px){.sidebar-open .content-wrapper,.sidebar-open .main-footer{-webkit-transform:translate(230px, 0);-ms-transform:translate(230px, 0);-o-transform:translate(230px, 0);transform:translate(230px, 0)}}.content-wrapper{min-height:100%;background-color:#ecf0f5;z-index:800}.main-footer{background:#fff;padding:15px;color:#444;border-top:1px solid #d2d6de}.fixed .main-header,.fixed .main-sidebar,.fixed .left-side{position:fixed}.fixed .main-header{top:0;right:0;left:0}.fixed .content-wrapper,.fixed .right-side{padding-top:50px}@media (max-width:767px){.fixed .content-wrapper,.fixed .right-side{padding-top:100px}}.fixed.layout-boxed .wrapper{max-width:100%}.fixed .wrapper{overflow:hidden}.hold-transition .content-wrapper,.hold-transition .right-side,.hold-transition .main-footer,.hold-transition .main-sidebar,.hold-transition .left-side,.hold-transition .main-header .navbar,.hold-transition .main-header .logo,.hold-transition .menu-open .fa-angle-left{-webkit-transition:none;-o-transition:none;transition:none}.content{min-height:250px;padding:15px;margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:'Source Sans Pro',sans-serif}a{color:#303f9f}a:hover,a:active,a:focus{outline:none;text-decoration:none;color:#5161ca}.page-header{margin:10px 0 20px 0;font-size:22px}.page-header>small{color:#666;display:block;margin-top:5px}.main-header{position:relative;max-height:100px;z-index:1030}.main-header .navbar{-webkit-transition:margin-left .3s ease-in-out;-o-transition:margin-left .3s ease-in-out;transition:margin-left .3s ease-in-out;margin-bottom:0;margin-left:230px;border:none;min-height:50px;border-radius:0}.layout-top-nav .main-header .navbar{margin-left:0}.main-header #navbar-search-input.form-control{background:rgba(255,255,255,0.2);border-color:transparent}.main-header #navbar-search-input.form-control:focus,.main-header #navbar-search-input.form-control:active{border-color:rgba(0,0,0,0.1);background:rgba(255,255,255,0.9)}.main-header #navbar-search-input.form-control::-moz-placeholder{color:#ccc;opacity:1}.main-header #navbar-search-input.form-control:-ms-input-placeholder{color:#ccc}.main-header #navbar-search-input.form-control::-webkit-input-placeholder{color:#ccc}.main-header .navbar-custom-menu,.main-header .navbar-right{float:right}@media (max-width:991px){.main-header .navbar-custom-menu a,.main-header .navbar-right a{color:inherit;background:transparent}}@media (max-width:767px){.main-header .navbar-right{float:none}.navbar-collapse .main-header .navbar-right{margin:7.5px -15px}.main-header .navbar-right>li{color:inherit;border:0}}.main-header .sidebar-toggle{float:left;background-color:transparent;background-image:none;padding:15px 15px;font-family:fontAwesome}.main-header .sidebar-toggle:before{content:"\f0c9"}.main-header .sidebar-toggle:hover{color:#fff}.main-header .sidebar-toggle:focus,.main-header .sidebar-toggle:active{background:transparent}.main-header .sidebar-toggle .icon-bar{display:none}.main-header .navbar .nav>li.user>a>.fa,.main-header .navbar .nav>li.user>a>.glyphicon,.main-header .navbar .nav>li.user>a>.ion{margin-right:5px}.main-header .navbar .nav>li>a>.label{position:absolute;top:9px;right:7px;text-align:center;font-size:9px;padding:2px 3px;line-height:.9}.main-header .logo{-webkit-transition:width .3s ease-in-out;-o-transition:width .3s ease-in-out;transition:width .3s ease-in-out;display:block;float:left;height:50px;font-size:20px;line-height:50px;text-align:center;width:230px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;padding:0 15px;font-weight:300;overflow:hidden}.main-header .logo .logo-lg{display:block}.main-header .logo .logo-mini{display:none}.main-header .navbar-brand{color:#fff}.content-header{position:relative;padding:15px 15px 0 15px}.content-header>h1{margin:0;font-size:24px}.content-header>h1>small{font-size:15px;display:inline-block;padding-left:4px;font-weight:300}.content-header>.breadcrumb{float:right;background:transparent;margin-top:0;margin-bottom:0;font-size:12px;padding:7px 5px;position:absolute;top:15px;right:10px;border-radius:2px}.content-header>.breadcrumb>li>a{color:#444;text-decoration:none;display:inline-block}.content-header>.breadcrumb>li>a>.fa,.content-header>.breadcrumb>li>a>.glyphicon,.content-header>.breadcrumb>li>a>.ion{margin-right:5px}.content-header>.breadcrumb>li+li:before{content:'>\00a0'}@media (max-width:991px){.content-header>.breadcrumb{position:relative;margin-top:5px;top:0;right:0;float:none;background:#d2d6de;padding-left:10px}.content-header>.breadcrumb li:before{color:#97a0b3}}.navbar-toggle{color:#fff;border:0;margin:0;padding:15px 15px}@media (max-width:991px){.navbar-custom-menu .navbar-nav>li{float:left}.navbar-custom-menu .navbar-nav{margin:0;float:left}.navbar-custom-menu .navbar-nav>li>a{padding-top:15px;padding-bottom:15px;line-height:20px}}@media (max-width:767px){.main-header{position:relative}.main-header .logo,.main-header .navbar{width:100%;float:none}.main-header .navbar{margin:0}.main-header .navbar-custom-menu{float:right}}@media (max-width:991px){.navbar-collapse.pull-left{float:none !important}.navbar-collapse.pull-left+.navbar-custom-menu{display:block;position:absolute;top:0;right:40px}}.main-sidebar{position:absolute;top:0;left:0;padding-top:50px;min-height:100%;width:230px;z-index:810;-webkit-transition:-webkit-transform .3s ease-in-out,width .3s ease-in-out;-moz-transition:-moz-transform .3s ease-in-out,width .3s ease-in-out;-o-transition:-o-transform .3s ease-in-out,width .3s ease-in-out;transition:transform .3s ease-in-out,width .3s ease-in-out}@media (max-width:767px){.main-sidebar{padding-top:100px}}@media (max-width:767px){.main-sidebar{-webkit-transform:translate(-230px, 0);-ms-transform:translate(-230px, 0);-o-transform:translate(-230px, 0);transform:translate(-230px, 0)}}@media (min-width:768px){.sidebar-collapse .main-sidebar{-webkit-transform:translate(-230px, 0);-ms-transform:translate(-230px, 0);-o-transform:translate(-230px, 0);transform:translate(-230px, 0)}}@media (max-width:767px){.sidebar-open .main-sidebar{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0)}}.sidebar{padding-bottom:10px}.sidebar-form input:focus{border-color:transparent}.user-panel{position:relative;width:100%;padding:10px;overflow:hidden}.user-panel:before,.user-panel:after{content:" ";display:table}.user-panel:after{clear:both}.user-panel>.image>img{width:100%;max-width:45px;height:auto}.user-panel>.info{padding:5px 5px 5px 15px;line-height:1;position:absolute;left:55px}.user-panel>.info>p{font-weight:600;margin-bottom:9px}.user-panel>.info>a{text-decoration:none;padding-right:5px;margin-top:3px;font-size:11px}.user-panel>.info>a>.fa,.user-panel>.info>a>.ion,.user-panel>.info>a>.glyphicon{margin-right:3px}.sidebar-menu{list-style:none;margin:0;padding:0}.sidebar-menu>li{position:relative;margin:0;padding:0}.sidebar-menu>li>a{padding:12px 5px 12px 15px;display:block}.sidebar-menu>li>a>.fa,.sidebar-menu>li>a>.glyphicon,.sidebar-menu>li>a>.ion{width:20px}.sidebar-menu>li .label,.sidebar-menu>li .badge{margin-right:5px}.sidebar-menu>li .badge{margin-top:3px}.sidebar-menu li.header{padding:10px 25px 10px 15px;font-size:12px}.sidebar-menu li>a>.fa-angle-left,.sidebar-menu li>a>.pull-right-container>.fa-angle-left{width:auto;height:auto;padding:0;margin-right:10px;-webkit-transition:transform .5s ease;-o-transition:transform .5s ease;transition:transform .5s ease}.sidebar-menu li>a>.fa-angle-left{position:absolute;top:50%;right:10px;margin-top:-8px}.sidebar-menu .menu-open>a>.fa-angle-left,.sidebar-menu .menu-open>a>.pull-right-container>.fa-angle-left{-webkit-transform:rotate(-90deg);-ms-transform:rotate(-90deg);-o-transform:rotate(-90deg);transform:rotate(-90deg)}.sidebar-menu .active>.treeview-menu{display:block}@media (min-width:768px){.sidebar-mini.sidebar-collapse .content-wrapper,.sidebar-mini.sidebar-collapse .right-side,.sidebar-mini.sidebar-collapse .main-footer{margin-left:50px !important;z-index:840}.sidebar-mini.sidebar-collapse .main-sidebar{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0);width:50px !important;z-index:850}.sidebar-mini.sidebar-collapse .sidebar-menu>li{position:relative}.sidebar-mini.sidebar-collapse .sidebar-menu>li>a{margin-right:0}.sidebar-mini.sidebar-collapse .sidebar-menu>li>a>span{border-top-right-radius:4px}.sidebar-mini.sidebar-collapse .sidebar-menu>li:not(.treeview)>a>span{border-bottom-right-radius:4px}.sidebar-mini.sidebar-collapse .sidebar-menu>li>.treeview-menu{padding-top:5px;padding-bottom:5px;border-bottom-right-radius:4px}.sidebar-mini.sidebar-collapse .main-sidebar .user-panel>.info,.sidebar-mini.sidebar-collapse .sidebar-form,.sidebar-mini.sidebar-collapse .sidebar-menu>li>a>span,.sidebar-mini.sidebar-collapse .sidebar-menu>li>.treeview-menu,.sidebar-mini.sidebar-collapse .sidebar-menu>li>a>.pull-right,.sidebar-mini.sidebar-collapse .sidebar-menu li.header{display:none !important;-webkit-transform:translateZ(0)}.sidebar-mini.sidebar-collapse .main-header .logo{width:50px}.sidebar-mini.sidebar-collapse .main-header .logo>.logo-mini{display:block;margin-left:-15px;margin-right:-15px;font-size:18px}.sidebar-mini.sidebar-collapse .main-header .logo>.logo-lg{display:none}.sidebar-mini.sidebar-collapse .main-header .navbar{margin-left:50px}}.sidebar-mini:not(.sidebar-mini-expand-feature).sidebar-collapse .sidebar-menu>li:hover>a>span:not(.pull-right),.sidebar-mini:not(.sidebar-mini-expand-feature).sidebar-collapse .sidebar-menu>li:hover>.treeview-menu{display:block !important;position:absolute;width:180px;left:50px}.sidebar-mini:not(.sidebar-mini-expand-feature).sidebar-collapse .sidebar-menu>li:hover>a>span{top:0;margin-left:-3px;padding:12px 5px 12px 20px;background-color:inherit}.sidebar-mini:not(.sidebar-mini-expand-feature).sidebar-collapse .sidebar-menu>li:hover>a>.pull-right-container{position:relative !important;float:right;width:auto !important;left:180px !important;top:-22px !important;z-index:900}.sidebar-mini:not(.sidebar-mini-expand-feature).sidebar-collapse .sidebar-menu>li:hover>a>.pull-right-container>.label:not(:first-of-type){display:none}.sidebar-mini:not(.sidebar-mini-expand-feature).sidebar-collapse .sidebar-menu>li:hover>.treeview-menu{top:44px;margin-left:0}.sidebar-expanded-on-hover .main-footer,.sidebar-expanded-on-hover .content-wrapper{margin-left:50px}.sidebar-expanded-on-hover .main-sidebar{box-shadow:3px 0 8px rgba(0,0,0,0.125)}.sidebar-menu,.main-sidebar .user-panel,.sidebar-menu>li.header{white-space:nowrap;overflow:hidden}.sidebar-menu:hover{overflow:visible}.sidebar-form,.sidebar-menu>li.header{overflow:hidden;text-overflow:clip}.sidebar-menu li>a{position:relative}.sidebar-menu li>a>.pull-right-container{position:absolute;right:10px;top:50%;margin-top:-7px}.control-sidebar-bg{position:fixed;z-index:1000;bottom:0}.control-sidebar-bg,.control-sidebar{top:0;right:-230px;width:230px;-webkit-transition:right .3s ease-in-out;-o-transition:right .3s ease-in-out;transition:right .3s ease-in-out}.control-sidebar{position:absolute;padding-top:50px;z-index:1010}@media (max-width:768px){.control-sidebar{padding-top:100px}}.control-sidebar>.tab-content{padding:10px 15px}.control-sidebar.control-sidebar-open,.control-sidebar.control-sidebar-open+.control-sidebar-bg{right:0}.control-sidebar-open .control-sidebar-bg,.control-sidebar-open .control-sidebar{right:0}@media (min-width:768px){.control-sidebar-open .content-wrapper,.control-sidebar-open .right-side,.control-sidebar-open .main-footer{margin-right:230px}}.fixed .control-sidebar{position:fixed;height:100%;overflow-y:auto;padding-bottom:50px}.nav-tabs.control-sidebar-tabs>li:first-of-type>a,.nav-tabs.control-sidebar-tabs>li:first-of-type>a:hover,.nav-tabs.control-sidebar-tabs>li:first-of-type>a:focus{border-left-width:0}.nav-tabs.control-sidebar-tabs>li>a{border-radius:0}.nav-tabs.control-sidebar-tabs>li>a,.nav-tabs.control-sidebar-tabs>li>a:hover{border-top:none;border-right:none;border-left:1px solid transparent;border-bottom:1px solid transparent}.nav-tabs.control-sidebar-tabs>li>a .icon{font-size:16px}.nav-tabs.control-sidebar-tabs>li.active>a,.nav-tabs.control-sidebar-tabs>li.active>a:hover,.nav-tabs.control-sidebar-tabs>li.active>a:focus,.nav-tabs.control-sidebar-tabs>li.active>a:active{border-top:none;border-right:none;border-bottom:none}@media (max-width:768px){.nav-tabs.control-sidebar-tabs{display:table}.nav-tabs.control-sidebar-tabs>li{display:table-cell}}.control-sidebar-heading{font-weight:400;font-size:16px;padding:10px 0;margin-bottom:10px}.control-sidebar-subheading{display:block;font-weight:400;font-size:14px}.control-sidebar-menu{list-style:none;padding:0;margin:0 -15px}.control-sidebar-menu>li>a{display:block;padding:10px 15px}.control-sidebar-menu>li>a:before,.control-sidebar-menu>li>a:after{content:" ";display:table}.control-sidebar-menu>li>a:after{clear:both}.control-sidebar-menu>li>a>.control-sidebar-subheading{margin-top:0}.control-sidebar-menu .menu-icon{float:left;width:35px;height:35px;border-radius:50%;text-align:center;line-height:35px}.control-sidebar-menu .menu-info{margin-left:45px;margin-top:3px}.control-sidebar-menu .menu-info>.control-sidebar-subheading{margin:0}.control-sidebar-menu .menu-info>p{margin:0;font-size:11px}.control-sidebar-menu .progress{margin:0}.control-sidebar-dark{color:#bdccd3}.control-sidebar-dark,.control-sidebar-dark+.control-sidebar-bg{background:#263238}.control-sidebar-dark .nav-tabs.control-sidebar-tabs{border-bottom:#202a2f}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a{background:#1c2429;color:#bdccd3}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:hover,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:focus{border-left-color:#181f23;border-bottom-color:#181f23}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:hover,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:focus,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:active{background:#202a2f}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:hover{color:#fff}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a:hover,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a:focus,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a:active{background:#263238;color:#fff}.control-sidebar-dark .control-sidebar-heading,.control-sidebar-dark .control-sidebar-subheading{color:#fff}.control-sidebar-dark .control-sidebar-menu>li>a:hover{background:#222d32}.control-sidebar-dark .control-sidebar-menu>li>a .menu-info>p{color:#bdccd3}.control-sidebar-light{color:#5e5e5e}.control-sidebar-light,.control-sidebar-light+.control-sidebar-bg{background:#f9fafc;border-left:1px solid #d2d6de}.control-sidebar-light .nav-tabs.control-sidebar-tabs{border-bottom:#d2d6de}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a{background:#e8ecf4;color:#444}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:hover,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:focus{border-left-color:#d2d6de;border-bottom-color:#d2d6de}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:hover,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:focus,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:active{background:#eff1f7}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a:hover,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a:focus,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a:active{background:#f9fafc;color:#111}.control-sidebar-light .control-sidebar-heading,.control-sidebar-light .control-sidebar-subheading{color:#111}.control-sidebar-light .control-sidebar-menu{margin-left:-14px}.control-sidebar-light .control-sidebar-menu>li>a:hover{background:#f4f4f5}.control-sidebar-light .control-sidebar-menu>li>a .menu-info>p{color:#5e5e5e}.dropdown-menu{box-shadow:none;border-color:#eee}.dropdown-menu>li>a{color:#777}.dropdown-menu>li>a>.glyphicon,.dropdown-menu>li>a>.fa,.dropdown-menu>li>a>.ion{margin-right:10px}.dropdown-menu>li>a:hover{background-color:#e1e3e9;color:#333}.dropdown-menu>.divider{background-color:#eee}.navbar-nav>.notifications-menu>.dropdown-menu,.navbar-nav>.messages-menu>.dropdown-menu,.navbar-nav>.tasks-menu>.dropdown-menu{width:280px;padding:0 0 0 0;margin:0;top:100%}.navbar-nav>.notifications-menu>.dropdown-menu>li,.navbar-nav>.messages-menu>.dropdown-menu>li,.navbar-nav>.tasks-menu>.dropdown-menu>li{position:relative}.navbar-nav>.notifications-menu>.dropdown-menu>li.header,.navbar-nav>.messages-menu>.dropdown-menu>li.header,.navbar-nav>.tasks-menu>.dropdown-menu>li.header{border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0;background-color:#ffffff;padding:7px 10px;border-bottom:1px solid #f4f4f4;color:#444444;font-size:14px}.navbar-nav>.notifications-menu>.dropdown-menu>li.footer>a,.navbar-nav>.messages-menu>.dropdown-menu>li.footer>a,.navbar-nav>.tasks-menu>.dropdown-menu>li.footer>a{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px;font-size:12px;background-color:#fff;padding:7px 10px;border-bottom:1px solid #eeeeee;color:#444 !important;text-align:center}@media (max-width:991px){.navbar-nav>.notifications-menu>.dropdown-menu>li.footer>a,.navbar-nav>.messages-menu>.dropdown-menu>li.footer>a,.navbar-nav>.tasks-menu>.dropdown-menu>li.footer>a{background:#fff !important;color:#444 !important}}.navbar-nav>.notifications-menu>.dropdown-menu>li.footer>a:hover,.navbar-nav>.messages-menu>.dropdown-menu>li.footer>a:hover,.navbar-nav>.tasks-menu>.dropdown-menu>li.footer>a:hover{text-decoration:none;font-weight:normal}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu,.navbar-nav>.messages-menu>.dropdown-menu>li .menu,.navbar-nav>.tasks-menu>.dropdown-menu>li .menu{max-height:200px;margin:0;padding:0;list-style:none;overflow-x:hidden}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a,.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a,.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a{display:block;white-space:nowrap;border-bottom:1px solid #f4f4f4}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a:hover,.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:hover,.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a:hover{background:#f4f4f4;text-decoration:none}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a{color:#444444;overflow:hidden;text-overflow:ellipsis;padding:10px}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a>.glyphicon,.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a>.fa,.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a>.ion{width:20px}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a{margin:0;padding:10px 10px}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>div>img{margin:auto 10px auto auto;width:40px;height:40px}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>h4{padding:0;margin:0 0 0 45px;color:#444444;font-size:15px;position:relative}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>h4>small{color:#999999;font-size:10px;position:absolute;top:0;right:0}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>p{margin:0 0 0 45px;font-size:12px;color:#888888}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:before,.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:after{content:" ";display:table}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:after{clear:both}.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a{padding:10px}.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a>h3{font-size:14px;padding:0;margin:0 0 10px 0;color:#666666}.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a>.progress{padding:0;margin:0}.navbar-nav>.user-menu>.dropdown-menu{border-top-right-radius:0;border-top-left-radius:0;padding:1px 0 0 0;border-top-width:0;width:280px}.navbar-nav>.user-menu>.dropdown-menu,.navbar-nav>.user-menu>.dropdown-menu>.user-body{border-bottom-right-radius:4px;border-bottom-left-radius:4px}.navbar-nav>.user-menu>.dropdown-menu>li.user-header{height:175px;padding:10px;text-align:center}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>img{z-index:5;height:90px;width:90px;border:3px solid;border-color:transparent;border-color:rgba(255,255,255,0.2)}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>p{z-index:5;color:#fff;color:rgba(255,255,255,0.8);font-size:17px;margin-top:10px}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>p>small{display:block;font-size:12px}.navbar-nav>.user-menu>.dropdown-menu>.user-body{padding:15px;border-bottom:1px solid #f4f4f4;border-top:1px solid #dddddd}.navbar-nav>.user-menu>.dropdown-menu>.user-body:before,.navbar-nav>.user-menu>.dropdown-menu>.user-body:after{content:" ";display:table}.navbar-nav>.user-menu>.dropdown-menu>.user-body:after{clear:both}.navbar-nav>.user-menu>.dropdown-menu>.user-body a{color:#444 !important}@media (max-width:991px){.navbar-nav>.user-menu>.dropdown-menu>.user-body a{background:#fff !important;color:#444 !important}}.navbar-nav>.user-menu>.dropdown-menu>.user-footer{background-color:#f9f9f9;padding:10px}.navbar-nav>.user-menu>.dropdown-menu>.user-footer:before,.navbar-nav>.user-menu>.dropdown-menu>.user-footer:after{content:" ";display:table}.navbar-nav>.user-menu>.dropdown-menu>.user-footer:after{clear:both}.navbar-nav>.user-menu>.dropdown-menu>.user-footer .btn-default{color:#666666}@media (max-width:991px){.navbar-nav>.user-menu>.dropdown-menu>.user-footer .btn-default:hover{background-color:#f9f9f9}}.navbar-nav>.user-menu .user-image{float:left;width:25px;height:25px;border-radius:50%;margin-right:10px;margin-top:-2px}@media (max-width:767px){.navbar-nav>.user-menu .user-image{float:none;margin-right:0;margin-top:-8px;line-height:10px}}.open:not(.dropup)>.animated-dropdown-menu{backface-visibility:visible !important;-webkit-animation:flipInX .7s both;-o-animation:flipInX .7s both;animation:flipInX .7s both}@keyframes flipInX{0%{transform:perspective(400px) rotate3d(1, 0, 0, 90deg);transition-timing-function:ease-in;opacity:0}40%{transform:perspective(400px) rotate3d(1, 0, 0, -20deg);transition-timing-function:ease-in}60%{transform:perspective(400px) rotate3d(1, 0, 0, 10deg);opacity:1}80%{transform:perspective(400px) rotate3d(1, 0, 0, -5deg)}100%{transform:perspective(400px)}}@-webkit-keyframes flipInX{0%{-webkit-transform:perspective(400px) rotate3d(1, 0, 0, 90deg);-webkit-transition-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotate3d(1, 0, 0, -20deg);-webkit-transition-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotate3d(1, 0, 0, 10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotate3d(1, 0, 0, -5deg)}100%{-webkit-transform:perspective(400px)}}.navbar-custom-menu>.navbar-nav>li{position:relative}.navbar-custom-menu>.navbar-nav>li>.dropdown-menu{position:absolute;right:0;left:auto}@media (max-width:991px){.navbar-custom-menu>.navbar-nav{float:right}.navbar-custom-menu>.navbar-nav>li{position:static}.navbar-custom-menu>.navbar-nav>li>.dropdown-menu{position:absolute;right:5%;left:auto;border:1px solid #ddd;background:#fff}}.form-control{border-radius:0;box-shadow:none;border-color:#d2d6de}.form-control:focus{border-color:#303f9f;box-shadow:none}.form-control::-moz-placeholder,.form-control:-ms-input-placeholder,.form-control::-webkit-input-placeholder{color:#bbb;opacity:1}.form-control:not(select){-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-group.has-success label{color:#00a65a}.form-group.has-success .form-control,.form-group.has-success .input-group-addon{border-color:#00a65a;box-shadow:none}.form-group.has-success .help-block{color:#00a65a}.form-group.has-warning label{color:#f39c12}.form-group.has-warning .form-control,.form-group.has-warning .input-group-addon{border-color:#f39c12;box-shadow:none}.form-group.has-warning .help-block{color:#f39c12}.form-group.has-error label{color:#dd4b39}.form-group.has-error .form-control,.form-group.has-error .input-group-addon{border-color:#dd4b39;box-shadow:none}.form-group.has-error .help-block{color:#dd4b39}.input-group .input-group-addon{border-radius:0;border-color:#d2d6de;background-color:#fff}.btn-group-vertical .btn.btn-flat:first-of-type,.btn-group-vertical .btn.btn-flat:last-of-type{border-radius:0}.icheck>label{padding-left:0}.form-control-feedback.fa{line-height:34px}.input-lg+.form-control-feedback.fa,.input-group-lg+.form-control-feedback.fa,.form-group-lg .form-control+.form-control-feedback.fa{line-height:46px}.input-sm+.form-control-feedback.fa,.input-group-sm+.form-control-feedback.fa,.form-group-sm .form-control+.form-control-feedback.fa{line-height:30px}.progress,.progress>.progress-bar{-webkit-box-shadow:none;box-shadow:none}.progress,.progress>.progress-bar,.progress .progress-bar,.progress>.progress-bar .progress-bar{border-radius:1px}.progress.sm,.progress-sm{height:10px}.progress.sm,.progress-sm,.progress.sm .progress-bar,.progress-sm .progress-bar{border-radius:1px}.progress.xs,.progress-xs{height:7px}.progress.xs,.progress-xs,.progress.xs .progress-bar,.progress-xs .progress-bar{border-radius:1px}.progress.xxs,.progress-xxs{height:3px}.progress.xxs,.progress-xxs,.progress.xxs .progress-bar,.progress-xxs .progress-bar{border-radius:1px}.progress.vertical{position:relative;width:30px;height:200px;display:inline-block;margin-right:10px}.progress.vertical>.progress-bar{width:100%;position:absolute;bottom:0}.progress.vertical.sm,.progress.vertical.progress-sm{width:20px}.progress.vertical.xs,.progress.vertical.progress-xs{width:10px}.progress.vertical.xxs,.progress.vertical.progress-xxs{width:3px}.progress-group .progress-text{font-weight:600}.progress-group .progress-number{float:right}.table tr>td .progress{margin:0}.progress-bar-light-blue,.progress-bar-primary{background-color:#303f9f}.progress-striped .progress-bar-light-blue,.progress-striped .progress-bar-primary{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-green,.progress-bar-success{background-color:#00a65a}.progress-striped .progress-bar-green,.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-aqua,.progress-bar-info{background-color:#00c0ef}.progress-striped .progress-bar-aqua,.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-yellow,.progress-bar-warning{background-color:#f39c12}.progress-striped .progress-bar-yellow,.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-red,.progress-bar-danger{background-color:#dd4b39}.progress-striped .progress-bar-red,.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.small-box{border-radius:2px;position:relative;display:block;margin-bottom:20px;box-shadow:0 1px 1px rgba(0,0,0,0.1)}.small-box>.inner{padding:10px}.small-box>.small-box-footer{position:relative;text-align:center;padding:3px 0;color:#fff;color:rgba(255,255,255,0.8);display:block;z-index:10;background:rgba(0,0,0,0.1);text-decoration:none}.small-box>.small-box-footer:hover{color:#fff;background:rgba(0,0,0,0.15)}.small-box h3{font-size:38px;font-weight:bold;margin:0 0 10px 0;white-space:nowrap;padding:0}.small-box p{font-size:15px}.small-box p>small{display:block;color:#f9f9f9;font-size:13px;margin-top:5px}.small-box h3,.small-box p{z-index:5}.small-box .icon{-webkit-transition:all .3s linear;-o-transition:all .3s linear;transition:all .3s linear;position:absolute;top:-10px;right:10px;z-index:0;font-size:90px;color:rgba(0,0,0,0.15)}.small-box:hover{text-decoration:none;color:#f9f9f9}.small-box:hover .icon{font-size:95px}@media (max-width:767px){.small-box{text-align:center}.small-box .icon{display:none}.small-box p{font-size:12px}}.box{position:relative;border-radius:3px;background:#ffffff;border-top:3px solid #d2d6de;margin-bottom:20px;width:100%;box-shadow:0 1px 1px rgba(0,0,0,0.1)}.box.box-primary{border-top-color:#303f9f}.box.box-info{border-top-color:#00c0ef}.box.box-danger{border-top-color:#dd4b39}.box.box-warning{border-top-color:#f39c12}.box.box-success{border-top-color:#00a65a}.box.box-default{border-top-color:#d2d6de}.box.collapsed-box .box-body,.box.collapsed-box .box-footer{display:none}.box .nav-stacked>li{border-bottom:1px solid #f4f4f4;margin:0}.box .nav-stacked>li:last-of-type{border-bottom:none}.box.height-control .box-body{max-height:300px;overflow:auto}.box .border-right{border-right:1px solid #f4f4f4}.box .border-left{border-left:1px solid #f4f4f4}.box.box-solid{border-top:0}.box.box-solid>.box-header .btn.btn-default{background:transparent}.box.box-solid>.box-header .btn:hover,.box.box-solid>.box-header a:hover{background:rgba(0,0,0,0.1)}.box.box-solid.box-default{border:1px solid #d2d6de}.box.box-solid.box-default>.box-header{color:#444;background:#d2d6de;background-color:#d2d6de}.box.box-solid.box-default>.box-header a,.box.box-solid.box-default>.box-header .btn{color:#444}.box.box-solid.box-primary{border:1px solid #303f9f}.box.box-solid.box-primary>.box-header{color:#fff;background:#303f9f;background-color:#303f9f}.box.box-solid.box-primary>.box-header a,.box.box-solid.box-primary>.box-header .btn{color:#fff}.box.box-solid.box-info{border:1px solid #00c0ef}.box.box-solid.box-info>.box-header{color:#fff;background:#00c0ef;background-color:#00c0ef}.box.box-solid.box-info>.box-header a,.box.box-solid.box-info>.box-header .btn{color:#fff}.box.box-solid.box-danger{border:1px solid #dd4b39}.box.box-solid.box-danger>.box-header{color:#fff;background:#dd4b39;background-color:#dd4b39}.box.box-solid.box-danger>.box-header a,.box.box-solid.box-danger>.box-header .btn{color:#fff}.box.box-solid.box-warning{border:1px solid #f39c12}.box.box-solid.box-warning>.box-header{color:#fff;background:#f39c12;background-color:#f39c12}.box.box-solid.box-warning>.box-header a,.box.box-solid.box-warning>.box-header .btn{color:#fff}.box.box-solid.box-success{border:1px solid #00a65a}.box.box-solid.box-success>.box-header{color:#fff;background:#00a65a;background-color:#00a65a}.box.box-solid.box-success>.box-header a,.box.box-solid.box-success>.box-header .btn{color:#fff}.box.box-solid>.box-header>.box-tools .btn{border:0;box-shadow:none}.box.box-solid[class*='bg']>.box-header{color:#fff}.box .box-group>.box{margin-bottom:5px}.box .knob-label{text-align:center;color:#333;font-weight:100;font-size:12px;margin-bottom:0.3em}.box>.overlay,.overlay-wrapper>.overlay,.box>.loading-img,.overlay-wrapper>.loading-img{position:absolute;top:0;left:0;width:100%;height:100%}.box .overlay,.overlay-wrapper .overlay{z-index:50;background:rgba(255,255,255,0.7);border-radius:3px}.box .overlay>.fa,.overlay-wrapper .overlay>.fa{position:absolute;top:50%;left:50%;margin-left:-15px;margin-top:-15px;color:#000;font-size:30px}.box .overlay.dark,.overlay-wrapper .overlay.dark{background:rgba(0,0,0,0.5)}.box-header:before,.box-body:before,.box-footer:before,.box-header:after,.box-body:after,.box-footer:after{content:" ";display:table}.box-header:after,.box-body:after,.box-footer:after{clear:both}.box-header{color:#444;display:block;padding:10px;position:relative}.box-header.with-border{border-bottom:1px solid #f4f4f4}.collapsed-box .box-header.with-border{border-bottom:none}.box-header>.fa,.box-header>.glyphicon,.box-header>.ion,.box-header .box-title{display:inline-block;font-size:18px;margin:0;line-height:1}.box-header>.fa,.box-header>.glyphicon,.box-header>.ion{margin-right:5px}.box-header>.box-tools{position:absolute;right:10px;top:5px}.box-header>.box-tools [data-toggle="tooltip"]{position:relative}.box-header>.box-tools.pull-right .dropdown-menu{right:0;left:auto}.box-header>.box-tools .dropdown-menu>li>a{color:#444!important}.btn-box-tool{padding:5px;font-size:12px;background:transparent;color:#97a0b3}.open .btn-box-tool,.btn-box-tool:hover{color:#606c84}.btn-box-tool.btn:active{box-shadow:none}.box-body{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px;padding:10px}.no-header .box-body{border-top-right-radius:3px;border-top-left-radius:3px}.box-body>.table{margin-bottom:0}.box-body .fc{margin-top:5px}.box-body .full-width-chart{margin:-19px}.box-body.no-padding .full-width-chart{margin:-9px}.box-body .box-pane{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:3px}.box-body .box-pane-right{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:3px;border-bottom-left-radius:0}.box-footer{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px;border-top:1px solid #f4f4f4;padding:10px;background-color:#fff}.chart-legend{margin:10px 0}@media (max-width:991px){.chart-legend>li{float:left;margin-right:10px}}.box-comments{background:#f7f7f7}.box-comments .box-comment{padding:8px 0;border-bottom:1px solid #eee}.box-comments .box-comment:before,.box-comments .box-comment:after{content:" ";display:table}.box-comments .box-comment:after{clear:both}.box-comments .box-comment:last-of-type{border-bottom:0}.box-comments .box-comment:first-of-type{padding-top:0}.box-comments .box-comment img{float:left}.box-comments .comment-text{margin-left:40px;color:#555}.box-comments .username{color:#444;display:block;font-weight:600}.box-comments .text-muted{font-weight:400;font-size:12px}.todo-list{margin:0;padding:0;list-style:none;overflow:auto}.todo-list>li{border-radius:2px;padding:10px;background:#f4f4f4;margin-bottom:2px;border-left:2px solid #e6e7e8;color:#444}.todo-list>li:last-of-type{margin-bottom:0}.todo-list>li>input[type='checkbox']{margin:0 10px 0 5px}.todo-list>li .text{display:inline-block;margin-left:5px;font-weight:600}.todo-list>li .label{margin-left:10px;font-size:9px}.todo-list>li .tools{display:none;float:right;color:#dd4b39}.todo-list>li .tools>.fa,.todo-list>li .tools>.glyphicon,.todo-list>li .tools>.ion{margin-right:5px;cursor:pointer}.todo-list>li:hover .tools{display:inline-block}.todo-list>li.done{color:#999}.todo-list>li.done .text{text-decoration:line-through;font-weight:500}.todo-list>li.done .label{background:#d2d6de !important}.todo-list .danger{border-left-color:#dd4b39}.todo-list .warning{border-left-color:#f39c12}.todo-list .info{border-left-color:#00c0ef}.todo-list .success{border-left-color:#00a65a}.todo-list .primary{border-left-color:#303f9f}.todo-list .handle{display:inline-block;cursor:move;margin:0 5px}.chat{padding:5px 20px 5px 10px}.chat .item{margin-bottom:10px}.chat .item:before,.chat .item:after{content:" ";display:table}.chat .item:after{clear:both}.chat .item>img{width:40px;height:40px;border:2px solid transparent;border-radius:50%}.chat .item>.online{border:2px solid #00a65a}.chat .item>.offline{border:2px solid #dd4b39}.chat .item>.message{margin-left:55px;margin-top:-40px}.chat .item>.message>.name{display:block;font-weight:600}.chat .item>.attachment{border-radius:3px;background:#f4f4f4;margin-left:65px;margin-right:15px;padding:10px}.chat .item>.attachment>h4{margin:0 0 5px 0;font-weight:600;font-size:14px}.chat .item>.attachment>p,.chat .item>.attachment>.filename{font-weight:600;font-size:13px;font-style:italic;margin:0}.chat .item>.attachment:before,.chat .item>.attachment:after{content:" ";display:table}.chat .item>.attachment:after{clear:both}.box-input{max-width:200px}.modal .panel-body{color:#444}.info-box{display:block;min-height:90px;background:#fff;width:100%;box-shadow:0 1px 1px rgba(0,0,0,0.1);border-radius:2px;margin-bottom:15px}.info-box small{font-size:14px}.info-box .progress{background:rgba(0,0,0,0.2);margin:5px -10px 5px -10px;height:2px}.info-box .progress,.info-box .progress .progress-bar{border-radius:0}.info-box .progress .progress-bar{background:#fff}.info-box-icon{border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px;display:block;float:left;height:90px;width:90px;text-align:center;font-size:45px;line-height:90px;background:rgba(0,0,0,0.2)}.info-box-icon>img{max-width:100%}.info-box-content{padding:5px 10px;margin-left:90px}.info-box-number{display:block;font-weight:bold;font-size:18px}.progress-description,.info-box-text{display:block;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.info-box-text{text-transform:uppercase}.info-box-more{display:block}.progress-description{margin:0}.timeline{position:relative;margin:0 0 30px 0;padding:0;list-style:none}.timeline:before{content:'';position:absolute;top:0;bottom:0;width:4px;background:#ddd;left:31px;margin:0;border-radius:2px}.timeline>li{position:relative;margin-right:10px;margin-bottom:15px}.timeline>li:before,.timeline>li:after{content:" ";display:table}.timeline>li:after{clear:both}.timeline>li>.timeline-item{-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.1);box-shadow:0 1px 1px rgba(0,0,0,0.1);border-radius:3px;margin-top:0;background:#fff;color:#444;margin-left:60px;margin-right:15px;padding:0;position:relative}.timeline>li>.timeline-item>.time{color:#999;float:right;padding:10px;font-size:12px}.timeline>li>.timeline-item>.timeline-header{margin:0;color:#555;border-bottom:1px solid #f4f4f4;padding:10px;font-size:16px;line-height:1.1}.timeline>li>.timeline-item>.timeline-header>a{font-weight:600}.timeline>li>.timeline-item>.timeline-body,.timeline>li>.timeline-item>.timeline-footer{padding:10px}.timeline>li>.fa,.timeline>li>.glyphicon,.timeline>li>.ion{width:30px;height:30px;font-size:15px;line-height:30px;position:absolute;color:#666;background:#d2d6de;border-radius:50%;text-align:center;left:18px;top:0}.timeline>.time-label>span{font-weight:600;padding:5px;display:inline-block;background-color:#fff;border-radius:4px}.timeline-inverse>li>.timeline-item{background:#f0f0f0;border:1px solid #ddd;-webkit-box-shadow:none;box-shadow:none}.timeline-inverse>li>.timeline-item>.timeline-header{border-bottom-color:#ddd}.btn{border-radius:3px;-webkit-box-shadow:none;box-shadow:none;border:1px solid transparent}.btn.uppercase{text-transform:uppercase}.btn.btn-flat{border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;border-width:1px}.btn:active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);-moz-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn:focus{outline:none}.btn.btn-file{position:relative;overflow:hidden}.btn.btn-file>input[type='file']{position:absolute;top:0;right:0;min-width:100%;min-height:100%;font-size:100px;text-align:right;opacity:0;filter:alpha(opacity=0);outline:none;background:white;cursor:inherit;display:block}.btn-default{background-color:#f4f4f4;color:#444;border-color:#ddd}.btn-default:hover,.btn-default:active,.btn-default.hover{background-color:#e7e7e7}.btn-primary{background-color:#303f9f;border-color:#2a378b}.btn-primary:hover,.btn-primary:active,.btn-primary.hover{background-color:#2a378b}.btn-success{background-color:#00a65a;border-color:#008d4c}.btn-success:hover,.btn-success:active,.btn-success.hover{background-color:#008d4c}.btn-info{background-color:#00c0ef;border-color:#00acd6}.btn-info:hover,.btn-info:active,.btn-info.hover{background-color:#00acd6}.btn-danger{background-color:#dd4b39;border-color:#d73925}.btn-danger:hover,.btn-danger:active,.btn-danger.hover{background-color:#d73925}.btn-warning{background-color:#f39c12;border-color:#e08e0b}.btn-warning:hover,.btn-warning:active,.btn-warning.hover{background-color:#e08e0b}.btn-outline{border:1px solid #fff;background:transparent;color:#fff}.btn-outline:hover,.btn-outline:focus,.btn-outline:active{color:rgba(255,255,255,0.7);border-color:rgba(255,255,255,0.7)}.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn[class*='bg-']:hover{-webkit-box-shadow:inset 0 0 100px rgba(0,0,0,0.2);box-shadow:inset 0 0 100px rgba(0,0,0,0.2)}.btn-app{border-radius:3px;position:relative;padding:15px 5px;margin:0 0 10px 10px;min-width:80px;height:60px;text-align:center;color:#666;border:1px solid #ddd;background-color:#f4f4f4;font-size:12px}.btn-app>.fa,.btn-app>.glyphicon,.btn-app>.ion{font-size:20px;display:block}.btn-app:hover{background:#f4f4f4;color:#444;border-color:#aaa}.btn-app:active,.btn-app:focus{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);-moz-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-app>.badge{position:absolute;top:-3px;right:-10px;font-size:10px;font-weight:400}.callout{border-radius:3px;margin:0 0 20px 0;padding:15px 30px 15px 15px;border-left:5px solid #eee}.callout a{color:#fff;text-decoration:underline}.callout a:hover{color:#eee}.callout h4{margin-top:0;font-weight:600}.callout p:last-child{margin-bottom:0}.callout code,.callout .highlight{background-color:#fff}.callout.callout-danger{border-color:#c23321}.callout.callout-warning{border-color:#c87f0a}.callout.callout-info{border-color:#0097bc}.callout.callout-success{border-color:#00733e}.alert{border-radius:3px}.alert h4{font-weight:600}.alert .icon{margin-right:10px}.alert .close{color:#000;opacity:.2;filter:alpha(opacity=20)}.alert .close:hover{opacity:.5;filter:alpha(opacity=50)}.alert a{color:#fff;text-decoration:underline}.alert-success{border-color:#008d4c}.alert-danger,.alert-error{border-color:#d73925}.alert-warning{border-color:#e08e0b}.alert-info{border-color:#00acd6}.nav>li>a:hover,.nav>li>a:active,.nav>li>a:focus{color:#444;background:#f7f7f7}.nav-pills>li>a{border-radius:0;border-top:3px solid transparent;color:#444}.nav-pills>li>a>.fa,.nav-pills>li>a>.glyphicon,.nav-pills>li>a>.ion{margin-right:5px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{border-top-color:#303f9f}.nav-pills>li.active>a{font-weight:600}.nav-stacked>li>a{border-radius:0;border-top:0;border-left:3px solid transparent;color:#444}.nav-stacked>li.active>a,.nav-stacked>li.active>a:hover{background:transparent;color:#444;border-top:0;border-left-color:#303f9f}.nav-stacked>li.header{border-bottom:1px solid #ddd;color:#777;margin-bottom:10px;padding:5px 10px;text-transform:uppercase}.nav-tabs-custom{margin-bottom:20px;background:#fff;box-shadow:0 1px 1px rgba(0,0,0,0.1);border-radius:3px}.nav-tabs-custom>.nav-tabs{margin:0;border-bottom-color:#f4f4f4;border-top-right-radius:3px;border-top-left-radius:3px}.nav-tabs-custom>.nav-tabs>li{border-top:3px solid transparent;margin-bottom:-2px;margin-right:5px}.nav-tabs-custom>.nav-tabs>li.disabled>a{color:#777}.nav-tabs-custom>.nav-tabs>li>a{color:#444;border-radius:0}.nav-tabs-custom>.nav-tabs>li>a.text-muted{color:#999}.nav-tabs-custom>.nav-tabs>li>a,.nav-tabs-custom>.nav-tabs>li>a:hover{background:transparent;margin:0}.nav-tabs-custom>.nav-tabs>li>a:hover{color:#999}.nav-tabs-custom>.nav-tabs>li:not(.active)>a:hover,.nav-tabs-custom>.nav-tabs>li:not(.active)>a:focus,.nav-tabs-custom>.nav-tabs>li:not(.active)>a:active{border-color:transparent}.nav-tabs-custom>.nav-tabs>li.active{border-top-color:#303f9f}.nav-tabs-custom>.nav-tabs>li.active>a,.nav-tabs-custom>.nav-tabs>li.active:hover>a{background-color:#fff;color:#444}.nav-tabs-custom>.nav-tabs>li.active>a{border-top-color:transparent;border-left-color:#f4f4f4;border-right-color:#f4f4f4}.nav-tabs-custom>.nav-tabs>li:first-of-type{margin-left:0}.nav-tabs-custom>.nav-tabs>li:first-of-type.active>a{border-left-color:transparent}.nav-tabs-custom>.nav-tabs.pull-right{float:none !important}.nav-tabs-custom>.nav-tabs.pull-right>li{float:right}.nav-tabs-custom>.nav-tabs.pull-right>li:first-of-type{margin-right:0}.nav-tabs-custom>.nav-tabs.pull-right>li:first-of-type>a{border-left-width:1px}.nav-tabs-custom>.nav-tabs.pull-right>li:first-of-type.active>a{border-left-color:#f4f4f4;border-right-color:transparent}.nav-tabs-custom>.nav-tabs>li.header{line-height:35px;padding:0 10px;font-size:20px;color:#444}.nav-tabs-custom>.nav-tabs>li.header>.fa,.nav-tabs-custom>.nav-tabs>li.header>.glyphicon,.nav-tabs-custom>.nav-tabs>li.header>.ion{margin-right:5px}.nav-tabs-custom>.tab-content{background:#fff;padding:10px;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.nav-tabs-custom .dropdown.open>a:active,.nav-tabs-custom .dropdown.open>a:focus{background:transparent;color:#999}.nav-tabs-custom.tab-primary>.nav-tabs>li.active{border-top-color:#303f9f}.nav-tabs-custom.tab-info>.nav-tabs>li.active{border-top-color:#00c0ef}.nav-tabs-custom.tab-danger>.nav-tabs>li.active{border-top-color:#dd4b39}.nav-tabs-custom.tab-warning>.nav-tabs>li.active{border-top-color:#f39c12}.nav-tabs-custom.tab-success>.nav-tabs>li.active{border-top-color:#00a65a}.nav-tabs-custom.tab-default>.nav-tabs>li.active{border-top-color:#d2d6de}.pagination>li>a{background:#fafafa;color:#666}.pagination.pagination-flat>li>a{border-radius:0 !important}.products-list{list-style:none;margin:0;padding:0}.products-list>.item{border-radius:3px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.1);box-shadow:0 1px 1px rgba(0,0,0,0.1);padding:10px 0;background:#fff}.products-list>.item:before,.products-list>.item:after{content:" ";display:table}.products-list>.item:after{clear:both}.products-list .product-img{float:left}.products-list .product-img img{width:50px;height:50px}.products-list .product-info{margin-left:60px}.products-list .product-title{font-weight:600}.products-list .product-description{display:block;color:#999;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.product-list-in-box>.item{-webkit-box-shadow:none;box-shadow:none;border-radius:0;border-bottom:1px solid #f4f4f4}.product-list-in-box>.item:last-of-type{border-bottom-width:0}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{border-top:1px solid #f4f4f4}.table>thead>tr>th{border-bottom:2px solid #f4f4f4}.table tr td .progress{margin-top:5px}.table-bordered{border:1px solid #f4f4f4}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #f4f4f4}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table.no-border,.table.no-border td,.table.no-border th{border:0}table.text-center,table.text-center td,table.text-center th{text-align:center}.table.align th{text-align:left}.table.align td{text-align:right}.label-default{background-color:#d2d6de;color:#444}.direct-chat .box-body{border-bottom-right-radius:0;border-bottom-left-radius:0;position:relative;overflow-x:hidden;padding:0}.direct-chat.chat-pane-open .direct-chat-contacts{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0)}.direct-chat-messages{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0);padding:10px;height:250px;overflow:auto}.direct-chat-msg,.direct-chat-text{display:block}.direct-chat-msg{margin-bottom:10px}.direct-chat-msg:before,.direct-chat-msg:after{content:" ";display:table}.direct-chat-msg:after{clear:both}.direct-chat-messages,.direct-chat-contacts{-webkit-transition:-webkit-transform .5s ease-in-out;-moz-transition:-moz-transform .5s ease-in-out;-o-transition:-o-transform .5s ease-in-out;transition:transform .5s ease-in-out}.direct-chat-text{border-radius:5px;position:relative;padding:5px 10px;background:#d2d6de;border:1px solid #d2d6de;margin:5px 0 0 50px;color:#444}.direct-chat-text:after,.direct-chat-text:before{position:absolute;right:100%;top:15px;border:solid transparent;border-right-color:#d2d6de;content:' ';height:0;width:0;pointer-events:none}.direct-chat-text:after{border-width:5px;margin-top:-5px}.direct-chat-text:before{border-width:6px;margin-top:-6px}.right .direct-chat-text{margin-right:50px;margin-left:0}.right .direct-chat-text:after,.right .direct-chat-text:before{right:auto;left:100%;border-right-color:transparent;border-left-color:#d2d6de}.direct-chat-img{border-radius:50%;float:left;width:40px;height:40px}.right .direct-chat-img{float:right}.direct-chat-info{display:block;margin-bottom:2px;font-size:12px}.direct-chat-name{font-weight:600}.direct-chat-timestamp{color:#999}.direct-chat-contacts-open .direct-chat-contacts{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0)}.direct-chat-contacts{-webkit-transform:translate(101%, 0);-ms-transform:translate(101%, 0);-o-transform:translate(101%, 0);transform:translate(101%, 0);position:absolute;top:0;bottom:0;height:250px;width:100%;background:#222d32;color:#fff;overflow:auto}.contacts-list>li{border-bottom:1px solid rgba(0,0,0,0.2);padding:10px;margin:0}.contacts-list>li:before,.contacts-list>li:after{content:" ";display:table}.contacts-list>li:after{clear:both}.contacts-list>li:last-of-type{border-bottom:none}.contacts-list-img{border-radius:50%;width:40px;float:left}.contacts-list-info{margin-left:45px;color:#fff}.contacts-list-name,.contacts-list-status{display:block}.contacts-list-name{font-weight:600}.contacts-list-status{font-size:12px}.contacts-list-date{color:#aaa;font-weight:normal}.contacts-list-msg{color:#999}.direct-chat-danger .right>.direct-chat-text{background:#dd4b39;border-color:#dd4b39;color:#fff}.direct-chat-danger .right>.direct-chat-text:after,.direct-chat-danger .right>.direct-chat-text:before{border-left-color:#dd4b39}.direct-chat-primary .right>.direct-chat-text{background:#303f9f;border-color:#303f9f;color:#fff}.direct-chat-primary .right>.direct-chat-text:after,.direct-chat-primary .right>.direct-chat-text:before{border-left-color:#303f9f}.direct-chat-warning .right>.direct-chat-text{background:#f39c12;border-color:#f39c12;color:#fff}.direct-chat-warning .right>.direct-chat-text:after,.direct-chat-warning .right>.direct-chat-text:before{border-left-color:#f39c12}.direct-chat-info .right>.direct-chat-text{background:#00c0ef;border-color:#00c0ef;color:#fff}.direct-chat-info .right>.direct-chat-text:after,.direct-chat-info .right>.direct-chat-text:before{border-left-color:#00c0ef}.direct-chat-success .right>.direct-chat-text{background:#00a65a;border-color:#00a65a;color:#fff}.direct-chat-success .right>.direct-chat-text:after,.direct-chat-success .right>.direct-chat-text:before{border-left-color:#00a65a}.users-list>li{width:25%;float:left;padding:10px;text-align:center}.users-list>li img{border-radius:50%;max-width:100%;height:auto}.users-list>li>a:hover,.users-list>li>a:hover .users-list-name{color:#999}.users-list-name,.users-list-date{display:block}.users-list-name{font-weight:600;color:#444;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.users-list-date{color:#999;font-size:12px}.carousel-control.left,.carousel-control.right{background-image:none}.carousel-control>.fa{font-size:40px;position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-20px}.modal{background:rgba(0,0,0,0.3)}.modal-content{border-radius:0;-webkit-box-shadow:0 2px 3px rgba(0,0,0,0.125);box-shadow:0 2px 3px rgba(0,0,0,0.125);border:0}@media (min-width:768px){.modal-content{-webkit-box-shadow:0 2px 3px rgba(0,0,0,0.125);box-shadow:0 2px 3px rgba(0,0,0,0.125)}}.modal-header{border-bottom-color:#f4f4f4}.modal-footer{border-top-color:#f4f4f4}.modal-primary .modal-header,.modal-primary .modal-footer{border-color:#242f78}.modal-warning .modal-header,.modal-warning .modal-footer{border-color:#c87f0a}.modal-info .modal-header,.modal-info .modal-footer{border-color:#0097bc}.modal-success .modal-header,.modal-success .modal-footer{border-color:#00733e}.modal-danger .modal-header,.modal-danger .modal-footer{border-color:#c23321}.box-widget{border:none;position:relative}.widget-user .widget-user-header{padding:20px;height:120px;border-top-right-radius:3px;border-top-left-radius:3px}.widget-user .widget-user-username{margin-top:0;margin-bottom:5px;font-size:25px;font-weight:300;text-shadow:0 1px 1px rgba(0,0,0,0.2)}.widget-user .widget-user-desc{margin-top:0}.widget-user .widget-user-image{position:absolute;top:65px;left:50%;margin-left:-45px}.widget-user .widget-user-image>img{width:90px;height:auto;border:3px solid #fff}.widget-user .box-footer{padding-top:30px}.widget-user-2 .widget-user-header{padding:20px;border-top-right-radius:3px;border-top-left-radius:3px}.widget-user-2 .widget-user-username{margin-top:5px;margin-bottom:5px;font-size:25px;font-weight:300}.widget-user-2 .widget-user-desc{margin-top:0}.widget-user-2 .widget-user-username,.widget-user-2 .widget-user-desc{margin-left:75px}.widget-user-2 .widget-user-image>img{width:65px;height:auto;float:left}.treeview-menu{display:none;list-style:none;padding:0;margin:0;padding-left:5px}.treeview-menu .treeview-menu{padding-left:20px}.treeview-menu>li{margin:0}.treeview-menu>li>a{padding:5px 5px 5px 15px;display:block;font-size:14px}.treeview-menu>li>a>.fa,.treeview-menu>li>a>.glyphicon,.treeview-menu>li>a>.ion{width:20px}.treeview-menu>li>a>.pull-right-container>.fa-angle-left,.treeview-menu>li>a>.pull-right-container>.fa-angle-down,.treeview-menu>li>a>.fa-angle-left,.treeview-menu>li>a>.fa-angle-down{width:auto}.mailbox-messages>.table{margin:0}.mailbox-controls{padding:5px}.mailbox-controls.with-border{border-bottom:1px solid #f4f4f4}.mailbox-read-info{border-bottom:1px solid #f4f4f4;padding:10px}.mailbox-read-info h3{font-size:20px;margin:0}.mailbox-read-info h5{margin:0;padding:5px 0 0 0}.mailbox-read-time{color:#999;font-size:13px}.mailbox-read-message{padding:10px}.mailbox-attachments li{float:left;width:200px;border:1px solid #eee;margin-bottom:10px;margin-right:10px}.mailbox-attachment-name{font-weight:bold;color:#666}.mailbox-attachment-icon,.mailbox-attachment-info,.mailbox-attachment-size{display:block}.mailbox-attachment-info{padding:10px;background:#f4f4f4}.mailbox-attachment-size{color:#999;font-size:12px}.mailbox-attachment-icon{text-align:center;font-size:65px;color:#666;padding:20px 10px}.mailbox-attachment-icon.has-img{padding:0}.mailbox-attachment-icon.has-img>img{max-width:100%;height:auto}.lockscreen{background:#d2d6de}.lockscreen-logo{font-size:35px;text-align:center;margin-bottom:25px;font-weight:300}.lockscreen-logo a{color:#444}.lockscreen-wrapper{max-width:400px;margin:0 auto;margin-top:10%}.lockscreen .lockscreen-name{text-align:center;font-weight:600}.lockscreen-item{border-radius:4px;padding:0;background:#fff;position:relative;margin:10px auto 30px auto;width:290px}.lockscreen-image{border-radius:50%;position:absolute;left:-10px;top:-25px;background:#fff;padding:5px;z-index:10}.lockscreen-image>img{border-radius:50%;width:70px;height:70px}.lockscreen-credentials{margin-left:70px}.lockscreen-credentials .form-control{border:0}.lockscreen-credentials .btn{background-color:#fff;border:0;padding:0 10px}.lockscreen-footer{margin-top:10px}.login-logo,.register-logo{font-size:35px;text-align:center;margin-bottom:25px;font-weight:300}.login-logo a,.register-logo a{color:#444}.login-page,.register-page{background:#d2d6de}.login-box,.register-box{width:360px;margin:7% auto}@media (max-width:768px){.login-box,.register-box{width:90%;margin-top:20px}}.login-box-body,.register-box-body{background:#fff;padding:20px;border-top:0;color:#666}.login-box-body .form-control-feedback,.register-box-body .form-control-feedback{color:#777}.login-box-msg,.register-box-msg{margin:0;text-align:center;padding:0 20px 20px 20px}.social-auth-links{margin:10px 0}.error-page{width:600px;margin:20px auto 0 auto}@media (max-width:991px){.error-page{width:100%}}.error-page>.headline{float:left;font-size:100px;font-weight:300}@media (max-width:991px){.error-page>.headline{float:none;text-align:center}}.error-page>.error-content{margin-left:190px;display:block}@media (max-width:991px){.error-page>.error-content{margin-left:0}}.error-page>.error-content>h3{font-weight:300;font-size:25px}@media (max-width:991px){.error-page>.error-content>h3{text-align:center}}.invoice{position:relative;background:#fff;border:1px solid #f4f4f4;padding:20px;margin:10px 25px}.invoice-title{margin-top:0}.profile-user-img{margin:0 auto;width:100px;padding:3px;border:3px solid #d2d6de}.profile-username{font-size:21px;margin-top:5px}.post{border-bottom:1px solid #d2d6de;margin-bottom:15px;padding-bottom:15px;color:#666}.post:last-of-type{border-bottom:0;margin-bottom:0;padding-bottom:0}.post .user-block{margin-bottom:15px}.btn-social{position:relative;padding-left:44px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.btn-social>:first-child{position:absolute;left:0;top:0;bottom:0;width:32px;line-height:34px;font-size:1.6em;text-align:center;border-right:1px solid rgba(0,0,0,0.2)}.btn-social.btn-lg{padding-left:61px}.btn-social.btn-lg>:first-child{line-height:45px;width:45px;font-size:1.8em}.btn-social.btn-sm{padding-left:38px}.btn-social.btn-sm>:first-child{line-height:28px;width:28px;font-size:1.4em}.btn-social.btn-xs{padding-left:30px}.btn-social.btn-xs>:first-child{line-height:20px;width:20px;font-size:1.2em}.btn-social-icon{position:relative;padding-left:44px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;height:34px;width:34px;padding:0}.btn-social-icon>:first-child{position:absolute;left:0;top:0;bottom:0;width:32px;line-height:34px;font-size:1.6em;text-align:center;border-right:1px solid rgba(0,0,0,0.2)}.btn-social-icon.btn-lg{padding-left:61px}.btn-social-icon.btn-lg>:first-child{line-height:45px;width:45px;font-size:1.8em}.btn-social-icon.btn-sm{padding-left:38px}.btn-social-icon.btn-sm>:first-child{line-height:28px;width:28px;font-size:1.4em}.btn-social-icon.btn-xs{padding-left:30px}.btn-social-icon.btn-xs>:first-child{line-height:20px;width:20px;font-size:1.2em}.btn-social-icon>:first-child{border:none;text-align:center;width:100%}.btn-social-icon.btn-lg{height:45px;width:45px;padding-left:0;padding-right:0}.btn-social-icon.btn-sm{height:30px;width:30px;padding-left:0;padding-right:0}.btn-social-icon.btn-xs{height:22px;width:22px;padding-left:0;padding-right:0}.btn-adn{color:#fff;background-color:#d87a68;border-color:rgba(0,0,0,0.2)}.btn-adn:focus,.btn-adn.focus{color:#fff;background-color:#ce563f;border-color:rgba(0,0,0,0.2)}.btn-adn:hover{color:#fff;background-color:#ce563f;border-color:rgba(0,0,0,0.2)}.btn-adn:active,.btn-adn.active,.open>.dropdown-toggle.btn-adn{color:#fff;background-color:#ce563f;border-color:rgba(0,0,0,0.2)}.btn-adn:active,.btn-adn.active,.open>.dropdown-toggle.btn-adn{background-image:none}.btn-adn .badge{color:#d87a68;background-color:#fff}.btn-bitbucket{color:#fff;background-color:#205081;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:focus,.btn-bitbucket.focus{color:#fff;background-color:#163758;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:hover{color:#fff;background-color:#163758;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:active,.btn-bitbucket.active,.open>.dropdown-toggle.btn-bitbucket{color:#fff;background-color:#163758;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:active,.btn-bitbucket.active,.open>.dropdown-toggle.btn-bitbucket{background-image:none}.btn-bitbucket .badge{color:#205081;background-color:#fff}.btn-dropbox{color:#fff;background-color:#1087dd;border-color:rgba(0,0,0,0.2)}.btn-dropbox:focus,.btn-dropbox.focus{color:#fff;background-color:#0d6aad;border-color:rgba(0,0,0,0.2)}.btn-dropbox:hover{color:#fff;background-color:#0d6aad;border-color:rgba(0,0,0,0.2)}.btn-dropbox:active,.btn-dropbox.active,.open>.dropdown-toggle.btn-dropbox{color:#fff;background-color:#0d6aad;border-color:rgba(0,0,0,0.2)}.btn-dropbox:active,.btn-dropbox.active,.open>.dropdown-toggle.btn-dropbox{background-image:none}.btn-dropbox .badge{color:#1087dd;background-color:#fff}.btn-facebook{color:#fff;background-color:#3b5998;border-color:rgba(0,0,0,0.2)}.btn-facebook:focus,.btn-facebook.focus{color:#fff;background-color:#2d4373;border-color:rgba(0,0,0,0.2)}.btn-facebook:hover{color:#fff;background-color:#2d4373;border-color:rgba(0,0,0,0.2)}.btn-facebook:active,.btn-facebook.active,.open>.dropdown-toggle.btn-facebook{color:#fff;background-color:#2d4373;border-color:rgba(0,0,0,0.2)}.btn-facebook:active,.btn-facebook.active,.open>.dropdown-toggle.btn-facebook{background-image:none}.btn-facebook .badge{color:#3b5998;background-color:#fff}.btn-flickr{color:#fff;background-color:#ff0084;border-color:rgba(0,0,0,0.2)}.btn-flickr:focus,.btn-flickr.focus{color:#fff;background-color:#cc006a;border-color:rgba(0,0,0,0.2)}.btn-flickr:hover{color:#fff;background-color:#cc006a;border-color:rgba(0,0,0,0.2)}.btn-flickr:active,.btn-flickr.active,.open>.dropdown-toggle.btn-flickr{color:#fff;background-color:#cc006a;border-color:rgba(0,0,0,0.2)}.btn-flickr:active,.btn-flickr.active,.open>.dropdown-toggle.btn-flickr{background-image:none}.btn-flickr .badge{color:#ff0084;background-color:#fff}.btn-foursquare{color:#fff;background-color:#f94877;border-color:rgba(0,0,0,0.2)}.btn-foursquare:focus,.btn-foursquare.focus{color:#fff;background-color:#f71752;border-color:rgba(0,0,0,0.2)}.btn-foursquare:hover{color:#fff;background-color:#f71752;border-color:rgba(0,0,0,0.2)}.btn-foursquare:active,.btn-foursquare.active,.open>.dropdown-toggle.btn-foursquare{color:#fff;background-color:#f71752;border-color:rgba(0,0,0,0.2)}.btn-foursquare:active,.btn-foursquare.active,.open>.dropdown-toggle.btn-foursquare{background-image:none}.btn-foursquare .badge{color:#f94877;background-color:#fff}.btn-github{color:#fff;background-color:#444;border-color:rgba(0,0,0,0.2)}.btn-github:focus,.btn-github.focus{color:#fff;background-color:#2b2b2b;border-color:rgba(0,0,0,0.2)}.btn-github:hover{color:#fff;background-color:#2b2b2b;border-color:rgba(0,0,0,0.2)}.btn-github:active,.btn-github.active,.open>.dropdown-toggle.btn-github{color:#fff;background-color:#2b2b2b;border-color:rgba(0,0,0,0.2)}.btn-github:active,.btn-github.active,.open>.dropdown-toggle.btn-github{background-image:none}.btn-github .badge{color:#444;background-color:#fff}.btn-google{color:#fff;background-color:#dd4b39;border-color:rgba(0,0,0,0.2)}.btn-google:focus,.btn-google.focus{color:#fff;background-color:#c23321;border-color:rgba(0,0,0,0.2)}.btn-google:hover{color:#fff;background-color:#c23321;border-color:rgba(0,0,0,0.2)}.btn-google:active,.btn-google.active,.open>.dropdown-toggle.btn-google{color:#fff;background-color:#c23321;border-color:rgba(0,0,0,0.2)}.btn-google:active,.btn-google.active,.open>.dropdown-toggle.btn-google{background-image:none}.btn-google .badge{color:#dd4b39;background-color:#fff}.btn-instagram{color:#fff;background-color:#3f729b;border-color:rgba(0,0,0,0.2)}.btn-instagram:focus,.btn-instagram.focus{color:#fff;background-color:#305777;border-color:rgba(0,0,0,0.2)}.btn-instagram:hover{color:#fff;background-color:#305777;border-color:rgba(0,0,0,0.2)}.btn-instagram:active,.btn-instagram.active,.open>.dropdown-toggle.btn-instagram{color:#fff;background-color:#305777;border-color:rgba(0,0,0,0.2)}.btn-instagram:active,.btn-instagram.active,.open>.dropdown-toggle.btn-instagram{background-image:none}.btn-instagram .badge{color:#3f729b;background-color:#fff}.btn-linkedin{color:#fff;background-color:#007bb6;border-color:rgba(0,0,0,0.2)}.btn-linkedin:focus,.btn-linkedin.focus{color:#fff;background-color:#005983;border-color:rgba(0,0,0,0.2)}.btn-linkedin:hover{color:#fff;background-color:#005983;border-color:rgba(0,0,0,0.2)}.btn-linkedin:active,.btn-linkedin.active,.open>.dropdown-toggle.btn-linkedin{color:#fff;background-color:#005983;border-color:rgba(0,0,0,0.2)}.btn-linkedin:active,.btn-linkedin.active,.open>.dropdown-toggle.btn-linkedin{background-image:none}.btn-linkedin .badge{color:#007bb6;background-color:#fff}.btn-microsoft{color:#fff;background-color:#2672ec;border-color:rgba(0,0,0,0.2)}.btn-microsoft:focus,.btn-microsoft.focus{color:#fff;background-color:#125acd;border-color:rgba(0,0,0,0.2)}.btn-microsoft:hover{color:#fff;background-color:#125acd;border-color:rgba(0,0,0,0.2)}.btn-microsoft:active,.btn-microsoft.active,.open>.dropdown-toggle.btn-microsoft{color:#fff;background-color:#125acd;border-color:rgba(0,0,0,0.2)}.btn-microsoft:active,.btn-microsoft.active,.open>.dropdown-toggle.btn-microsoft{background-image:none}.btn-microsoft .badge{color:#2672ec;background-color:#fff}.btn-openid{color:#fff;background-color:#f7931e;border-color:rgba(0,0,0,0.2)}.btn-openid:focus,.btn-openid.focus{color:#fff;background-color:#da7908;border-color:rgba(0,0,0,0.2)}.btn-openid:hover{color:#fff;background-color:#da7908;border-color:rgba(0,0,0,0.2)}.btn-openid:active,.btn-openid.active,.open>.dropdown-toggle.btn-openid{color:#fff;background-color:#da7908;border-color:rgba(0,0,0,0.2)}.btn-openid:active,.btn-openid.active,.open>.dropdown-toggle.btn-openid{background-image:none}.btn-openid .badge{color:#f7931e;background-color:#fff}.btn-pinterest{color:#fff;background-color:#cb2027;border-color:rgba(0,0,0,0.2)}.btn-pinterest:focus,.btn-pinterest.focus{color:#fff;background-color:#9f191f;border-color:rgba(0,0,0,0.2)}.btn-pinterest:hover{color:#fff;background-color:#9f191f;border-color:rgba(0,0,0,0.2)}.btn-pinterest:active,.btn-pinterest.active,.open>.dropdown-toggle.btn-pinterest{color:#fff;background-color:#9f191f;border-color:rgba(0,0,0,0.2)}.btn-pinterest:active,.btn-pinterest.active,.open>.dropdown-toggle.btn-pinterest{background-image:none}.btn-pinterest .badge{color:#cb2027;background-color:#fff}.btn-reddit{color:#000;background-color:#eff7ff;border-color:rgba(0,0,0,0.2)}.btn-reddit:focus,.btn-reddit.focus{color:#000;background-color:#bcddff;border-color:rgba(0,0,0,0.2)}.btn-reddit:hover{color:#000;background-color:#bcddff;border-color:rgba(0,0,0,0.2)}.btn-reddit:active,.btn-reddit.active,.open>.dropdown-toggle.btn-reddit{color:#000;background-color:#bcddff;border-color:rgba(0,0,0,0.2)}.btn-reddit:active,.btn-reddit.active,.open>.dropdown-toggle.btn-reddit{background-image:none}.btn-reddit .badge{color:#eff7ff;background-color:#000}.btn-soundcloud{color:#fff;background-color:#f50;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:focus,.btn-soundcloud.focus{color:#fff;background-color:#c40;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:hover{color:#fff;background-color:#c40;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:active,.btn-soundcloud.active,.open>.dropdown-toggle.btn-soundcloud{color:#fff;background-color:#c40;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:active,.btn-soundcloud.active,.open>.dropdown-toggle.btn-soundcloud{background-image:none}.btn-soundcloud .badge{color:#f50;background-color:#fff}.btn-tumblr{color:#fff;background-color:#2c4762;border-color:rgba(0,0,0,0.2)}.btn-tumblr:focus,.btn-tumblr.focus{color:#fff;background-color:#1c2d3f;border-color:rgba(0,0,0,0.2)}.btn-tumblr:hover{color:#fff;background-color:#1c2d3f;border-color:rgba(0,0,0,0.2)}.btn-tumblr:active,.btn-tumblr.active,.open>.dropdown-toggle.btn-tumblr{color:#fff;background-color:#1c2d3f;border-color:rgba(0,0,0,0.2)}.btn-tumblr:active,.btn-tumblr.active,.open>.dropdown-toggle.btn-tumblr{background-image:none}.btn-tumblr .badge{color:#2c4762;background-color:#fff}.btn-twitter{color:#fff;background-color:#55acee;border-color:rgba(0,0,0,0.2)}.btn-twitter:focus,.btn-twitter.focus{color:#fff;background-color:#2795e9;border-color:rgba(0,0,0,0.2)}.btn-twitter:hover{color:#fff;background-color:#2795e9;border-color:rgba(0,0,0,0.2)}.btn-twitter:active,.btn-twitter.active,.open>.dropdown-toggle.btn-twitter{color:#fff;background-color:#2795e9;border-color:rgba(0,0,0,0.2)}.btn-twitter:active,.btn-twitter.active,.open>.dropdown-toggle.btn-twitter{background-image:none}.btn-twitter .badge{color:#55acee;background-color:#fff}.btn-vimeo{color:#fff;background-color:#1ab7ea;border-color:rgba(0,0,0,0.2)}.btn-vimeo:focus,.btn-vimeo.focus{color:#fff;background-color:#1295bf;border-color:rgba(0,0,0,0.2)}.btn-vimeo:hover{color:#fff;background-color:#1295bf;border-color:rgba(0,0,0,0.2)}.btn-vimeo:active,.btn-vimeo.active,.open>.dropdown-toggle.btn-vimeo{color:#fff;background-color:#1295bf;border-color:rgba(0,0,0,0.2)}.btn-vimeo:active,.btn-vimeo.active,.open>.dropdown-toggle.btn-vimeo{background-image:none}.btn-vimeo .badge{color:#1ab7ea;background-color:#fff}.btn-vk{color:#fff;background-color:#587ea3;border-color:rgba(0,0,0,0.2)}.btn-vk:focus,.btn-vk.focus{color:#fff;background-color:#466482;border-color:rgba(0,0,0,0.2)}.btn-vk:hover{color:#fff;background-color:#466482;border-color:rgba(0,0,0,0.2)}.btn-vk:active,.btn-vk.active,.open>.dropdown-toggle.btn-vk{color:#fff;background-color:#466482;border-color:rgba(0,0,0,0.2)}.btn-vk:active,.btn-vk.active,.open>.dropdown-toggle.btn-vk{background-image:none}.btn-vk .badge{color:#587ea3;background-color:#fff}.btn-yahoo{color:#fff;background-color:#720e9e;border-color:rgba(0,0,0,0.2)}.btn-yahoo:focus,.btn-yahoo.focus{color:#fff;background-color:#500a6f;border-color:rgba(0,0,0,0.2)}.btn-yahoo:hover{color:#fff;background-color:#500a6f;border-color:rgba(0,0,0,0.2)}.btn-yahoo:active,.btn-yahoo.active,.open>.dropdown-toggle.btn-yahoo{color:#fff;background-color:#500a6f;border-color:rgba(0,0,0,0.2)}.btn-yahoo:active,.btn-yahoo.active,.open>.dropdown-toggle.btn-yahoo{background-image:none}.btn-yahoo .badge{color:#720e9e;background-color:#fff}.fc-button{background:#f4f4f4;background-image:none;color:#444;border-color:#ddd;border-bottom-color:#ddd}.fc-button:hover,.fc-button:active,.fc-button.hover{background-color:#e9e9e9}.fc-header-title h2{font-size:15px;line-height:1.6em;color:#666;margin-left:10px}.fc-header-right{padding-right:10px}.fc-header-left{padding-left:10px}.fc-widget-header{background:#fafafa}.fc-grid{width:100%;border:0}.fc-widget-header:first-of-type,.fc-widget-content:first-of-type{border-left:0;border-right:0}.fc-widget-header:last-of-type,.fc-widget-content:last-of-type{border-right:0}.fc-toolbar{padding:10px;margin:0}.fc-day-number{font-size:20px;font-weight:300;padding-right:10px}.fc-color-picker{list-style:none;margin:0;padding:0}.fc-color-picker>li{float:left;font-size:30px;margin-right:5px;line-height:30px}.fc-color-picker>li .fa{-webkit-transition:-webkit-transform linear .3s;-moz-transition:-moz-transform linear .3s;-o-transition:-o-transform linear .3s;transition:transform linear .3s}.fc-color-picker>li .fa:hover{-webkit-transform:rotate(30deg);-ms-transform:rotate(30deg);-o-transform:rotate(30deg);transform:rotate(30deg)}#add-new-event{-webkit-transition:all linear .3s;-o-transition:all linear .3s;transition:all linear .3s}.external-event{padding:5px 10px;font-weight:bold;margin-bottom:4px;box-shadow:0 1px 1px rgba(0,0,0,0.1);text-shadow:0 1px 1px rgba(0,0,0,0.1);border-radius:3px;cursor:move}.external-event:hover{box-shadow:inset 0 0 90px rgba(0,0,0,0.2)}.select2-container--default.select2-container--focus,.select2-selection.select2-container--focus,.select2-container--default:focus,.select2-selection:focus,.select2-container--default:active,.select2-selection:active{outline:none}.select2-container--default .select2-selection--single,.select2-selection .select2-selection--single{border:1px solid #d2d6de;border-radius:0;padding:6px 12px;height:34px}.select2-container--default.select2-container--open{border-color:#303f9f}.select2-dropdown{border:1px solid #d2d6de;border-radius:0}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#303f9f;color:white}.select2-results__option{padding:6px 12px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{padding-left:0;padding-right:0;height:auto;margin-top:-4px}.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered{padding-right:6px;padding-left:20px}.select2-container--default .select2-selection--single .select2-selection__arrow{height:28px;right:3px}.select2-container--default .select2-selection--single .select2-selection__arrow b{margin-top:0}.select2-dropdown .select2-search__field,.select2-search--inline .select2-search__field{border:1px solid #d2d6de}.select2-dropdown .select2-search__field:focus,.select2-search--inline .select2-search__field:focus{outline:none}.select2-container--default.select2-container--focus .select2-selection--multiple,.select2-container--default .select2-search--dropdown .select2-search__field{border-color:#303f9f !important}.select2-container--default .select2-results__option[aria-disabled=true]{color:#999}.select2-container--default .select2-results__option[aria-selected=true]{background-color:#ddd}.select2-container--default .select2-results__option[aria-selected=true],.select2-container--default .select2-results__option[aria-selected=true]:hover{color:#444}.select2-container--default .select2-selection--multiple{border:1px solid #d2d6de;border-radius:0}.select2-container--default .select2-selection--multiple:focus{border-color:#303f9f}.select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#d2d6de}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#303f9f;border-color:#2a378b;padding:1px 10px;color:#fff}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{margin-right:5px;color:rgba(255,255,255,0.7)}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container .select2-selection--single .select2-selection__rendered{padding-right:10px}.box .datepicker-inline,.box .datepicker-inline .datepicker-days,.box .datepicker-inline>table,.box .datepicker-inline .datepicker-days>table{width:100%}.box .datepicker-inline td:hover,.box .datepicker-inline .datepicker-days td:hover,.box .datepicker-inline>table td:hover,.box .datepicker-inline .datepicker-days>table td:hover{background-color:rgba(255,255,255,0.3)}.box .datepicker-inline td.day.old,.box .datepicker-inline .datepicker-days td.day.old,.box .datepicker-inline>table td.day.old,.box .datepicker-inline .datepicker-days>table td.day.old,.box .datepicker-inline td.day.new,.box .datepicker-inline .datepicker-days td.day.new,.box .datepicker-inline>table td.day.new,.box .datepicker-inline .datepicker-days>table td.day.new{color:#777}.pad{padding:10px}.margin{margin:10px}.margin-bottom{margin-bottom:20px}.margin-bottom-none{margin-bottom:0}.margin-r-5{margin-right:5px}.inline{display:inline}.description-block{display:block;margin:10px 0;text-align:center}.description-block.margin-bottom{margin-bottom:25px}.description-block>.description-header{margin:0;padding:0;font-weight:600;font-size:16px}.description-block>.description-text{text-transform:uppercase}.bg-red,.bg-yellow,.bg-aqua,.bg-blue,.bg-light-blue,.bg-green,.bg-navy,.bg-teal,.bg-olive,.bg-lime,.bg-orange,.bg-fuchsia,.bg-purple,.bg-maroon,.bg-black,.bg-red-active,.bg-yellow-active,.bg-aqua-active,.bg-blue-active,.bg-light-blue-active,.bg-green-active,.bg-navy-active,.bg-teal-active,.bg-olive-active,.bg-lime-active,.bg-orange-active,.bg-fuchsia-active,.bg-purple-active,.bg-maroon-active,.bg-black-active,.callout.callout-danger,.callout.callout-warning,.callout.callout-info,.callout.callout-success,.alert-success,.alert-danger,.alert-error,.alert-warning,.alert-info,.label-danger,.label-info,.label-warning,.label-primary,.label-success,.modal-primary .modal-body,.modal-primary .modal-header,.modal-primary .modal-footer,.modal-warning .modal-body,.modal-warning .modal-header,.modal-warning .modal-footer,.modal-info .modal-body,.modal-info .modal-header,.modal-info .modal-footer,.modal-success .modal-body,.modal-success .modal-header,.modal-success .modal-footer,.modal-danger .modal-body,.modal-danger .modal-header,.modal-danger .modal-footer{color:#fff !important}.bg-gray{color:#000;background-color:#d2d6de !important}.bg-gray-light{background-color:#f7f7f7}.bg-black{background-color:#111 !important}.bg-red,.callout.callout-danger,.alert-danger,.alert-error,.label-danger,.modal-danger .modal-body{background-color:#dd4b39 !important}.bg-yellow,.callout.callout-warning,.alert-warning,.label-warning,.modal-warning .modal-body{background-color:#f39c12 !important}.bg-aqua,.callout.callout-info,.alert-info,.label-info,.modal-info .modal-body{background-color:#00c0ef !important}.bg-blue{background-color:#303f9f !important}.bg-light-blue,.label-primary,.modal-primary .modal-body{background-color:#303f9f !important}.bg-green,.callout.callout-success,.alert-success,.label-success,.modal-success .modal-body{background-color:#00a65a !important}.bg-navy{background-color:#001f3f !important}.bg-teal{background-color:#39cccc !important}.bg-olive{background-color:#3d9970 !important}.bg-lime{background-color:#01ff70 !important}.bg-orange{background-color:#ff851b !important}.bg-fuchsia{background-color:#f012be !important}.bg-purple{background-color:#605ca8 !important}.bg-maroon{background-color:#d81b60 !important}.bg-gray-active{color:#000;background-color:#b5bbc8 !important}.bg-black-active{background-color:#000 !important}.bg-red-active,.modal-danger .modal-header,.modal-danger .modal-footer{background-color:#d33724 !important}.bg-yellow-active,.modal-warning .modal-header,.modal-warning .modal-footer{background-color:#db8b0b !important}.bg-aqua-active,.modal-info .modal-header,.modal-info .modal-footer{background-color:#00a7d0 !important}.bg-blue-active{background-color:#242f78 !important}.bg-light-blue-active,.modal-primary .modal-header,.modal-primary .modal-footer{background-color:#293687 !important}.bg-green-active,.modal-success .modal-header,.modal-success .modal-footer{background-color:#008d4c !important}.bg-navy-active{background-color:#001a35 !important}.bg-teal-active{background-color:#30bbbb !important}.bg-olive-active{background-color:#368763 !important}.bg-lime-active{background-color:#00e765 !important}.bg-orange-active{background-color:#ff7701 !important}.bg-fuchsia-active{background-color:#db0ead !important}.bg-purple-active{background-color:#555299 !important}.bg-maroon-active{background-color:#ca195a !important}[class^="bg-"].disabled{opacity:.65;filter:alpha(opacity=65)}.text-red{color:#dd4b39 !important}.text-yellow{color:#f39c12 !important}.text-aqua{color:#00c0ef !important}.text-blue{color:#303f9f !important}.text-black{color:#111 !important}.text-light-blue{color:#303f9f !important}.text-green{color:#00a65a !important}.text-gray{color:#d2d6de !important}.text-navy{color:#001f3f !important}.text-teal{color:#39cccc !important}.text-olive{color:#3d9970 !important}.text-lime{color:#01ff70 !important}.text-orange{color:#ff851b !important}.text-fuchsia{color:#f012be !important}.text-purple{color:#605ca8 !important}.text-maroon{color:#d81b60 !important}.link-muted{color:#7a869d}.link-muted:hover,.link-muted:focus{color:#606c84}.link-black{color:#666}.link-black:hover,.link-black:focus{color:#999}.hide{display:none !important}.no-border{border:0 !important}.no-padding{padding:0 !important}.no-margin{margin:0 !important}.no-shadow{box-shadow:none !important}.list-unstyled,.chart-legend,.contacts-list,.users-list,.mailbox-attachments{list-style:none;margin:0;padding:0}.list-group-unbordered>.list-group-item{border-left:0;border-right:0;border-radius:0;padding-left:0;padding-right:0}.flat{border-radius:0 !important}.text-bold,.text-bold.table td,.text-bold.table th{font-weight:700}.text-sm{font-size:12px}.jqstooltip{padding:5px !important;width:auto !important;height:auto !important}.bg-teal-gradient{background:#39cccc !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #39cccc), color-stop(1, #7adddd)) !important;background:-ms-linear-gradient(bottom, #39cccc, #7adddd) !important;background:-moz-linear-gradient(center bottom, #39cccc 0, #7adddd 100%) !important;background:-o-linear-gradient(#7adddd, #39cccc) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#7adddd', endColorstr='#39cccc', GradientType=0) !important;color:#fff}.bg-light-blue-gradient{background:#303f9f !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #303f9f), color-stop(1, #4557c7)) !important;background:-ms-linear-gradient(bottom, #303f9f, #4557c7) !important;background:-moz-linear-gradient(center bottom, #303f9f 0, #4557c7 100%) !important;background:-o-linear-gradient(#4557c7, #303f9f) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#4557c7', endColorstr='#303f9f', GradientType=0) !important;color:#fff}.bg-blue-gradient{background:#303f9f !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #303f9f), color-stop(1, #384aba)) !important;background:-ms-linear-gradient(bottom, #303f9f, #384aba) !important;background:-moz-linear-gradient(center bottom, #303f9f 0, #384aba 100%) !important;background:-o-linear-gradient(#384aba, #303f9f) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#384aba', endColorstr='#303f9f', GradientType=0) !important;color:#fff}.bg-aqua-gradient{background:#00c0ef !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #00c0ef), color-stop(1, #14d1ff)) !important;background:-ms-linear-gradient(bottom, #00c0ef, #14d1ff) !important;background:-moz-linear-gradient(center bottom, #00c0ef 0, #14d1ff 100%) !important;background:-o-linear-gradient(#14d1ff, #00c0ef) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#14d1ff', endColorstr='#00c0ef', GradientType=0) !important;color:#fff}.bg-yellow-gradient{background:#f39c12 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #f39c12), color-stop(1, #f7bc60)) !important;background:-ms-linear-gradient(bottom, #f39c12, #f7bc60) !important;background:-moz-linear-gradient(center bottom, #f39c12 0, #f7bc60 100%) !important;background:-o-linear-gradient(#f7bc60, #f39c12) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f7bc60', endColorstr='#f39c12', GradientType=0) !important;color:#fff}.bg-purple-gradient{background:#605ca8 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #605ca8), color-stop(1, #9491c4)) !important;background:-ms-linear-gradient(bottom, #605ca8, #9491c4) !important;background:-moz-linear-gradient(center bottom, #605ca8 0, #9491c4 100%) !important;background:-o-linear-gradient(#9491c4, #605ca8) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#9491c4', endColorstr='#605ca8', GradientType=0) !important;color:#fff}.bg-green-gradient{background:#00a65a !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #00a65a), color-stop(1, #00ca6d)) !important;background:-ms-linear-gradient(bottom, #00a65a, #00ca6d) !important;background:-moz-linear-gradient(center bottom, #00a65a 0, #00ca6d 100%) !important;background:-o-linear-gradient(#00ca6d, #00a65a) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ca6d', endColorstr='#00a65a', GradientType=0) !important;color:#fff}.bg-red-gradient{background:#dd4b39 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #dd4b39), color-stop(1, #e47365)) !important;background:-ms-linear-gradient(bottom, #dd4b39, #e47365) !important;background:-moz-linear-gradient(center bottom, #dd4b39 0, #e47365 100%) !important;background:-o-linear-gradient(#e47365, #dd4b39) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#e47365', endColorstr='#dd4b39', GradientType=0) !important;color:#fff}.bg-black-gradient{background:#111 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #111), color-stop(1, #2b2b2b)) !important;background:-ms-linear-gradient(bottom, #111, #2b2b2b) !important;background:-moz-linear-gradient(center bottom, #111 0, #2b2b2b 100%) !important;background:-o-linear-gradient(#2b2b2b, #111) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#2b2b2b', endColorstr='#111111', GradientType=0) !important;color:#fff}.bg-maroon-gradient{background:#d81b60 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #d81b60), color-stop(1, #e73f7c)) !important;background:-ms-linear-gradient(bottom, #d81b60, #e73f7c) !important;background:-moz-linear-gradient(center bottom, #d81b60 0, #e73f7c 100%) !important;background:-o-linear-gradient(#e73f7c, #d81b60) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#e73f7c', endColorstr='#d81b60', GradientType=0) !important;color:#fff}.description-block .description-icon{font-size:16px}.no-pad-top{padding-top:0}.position-static{position:static !important}.list-header{font-size:15px;padding:10px 4px;font-weight:bold;color:#666}.list-seperator{height:1px;background:#f4f4f4;margin:15px 0 9px 0}.list-link>a{padding:4px;color:#777}.list-link>a:hover{color:#222}.font-light{font-weight:300}.user-block:before,.user-block:after{content:" ";display:table}.user-block:after{clear:both}.user-block img{width:40px;height:40px;float:left}.user-block .username,.user-block .description,.user-block .comment{display:block;margin-left:50px}.user-block .username{font-size:16px;font-weight:600}.user-block .description{color:#999;font-size:13px}.user-block.user-block-sm .username,.user-block.user-block-sm .description,.user-block.user-block-sm .comment{margin-left:40px}.user-block.user-block-sm .username{font-size:14px}.img-sm,.img-md,.img-lg,.box-comments .box-comment img,.user-block.user-block-sm img{float:left}.img-sm,.box-comments .box-comment img,.user-block.user-block-sm img{width:30px !important;height:30px !important}.img-sm+.img-push{margin-left:40px}.img-md{width:60px;height:60px}.img-md+.img-push{margin-left:70px}.img-lg{width:100px;height:100px}.img-lg+.img-push{margin-left:110px}.img-bordered{border:3px solid #d2d6de;padding:3px}.img-bordered-sm{border:2px solid #d2d6de;padding:2px}.attachment-block{border:1px solid #f4f4f4;padding:5px;margin-bottom:10px;background:#f7f7f7}.attachment-block .attachment-img{max-width:100px;max-height:100px;height:auto;float:left}.attachment-block .attachment-pushed{margin-left:110px}.attachment-block .attachment-heading{margin:0}.attachment-block .attachment-text{color:#555}.connectedSortable{min-height:100px}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sort-highlight{background:#f4f4f4;border:1px dashed #ddd;margin-bottom:10px}.full-opacity-hover{opacity:.65;filter:alpha(opacity=65)}.full-opacity-hover:hover{opacity:1;filter:alpha(opacity=100)}.chart{position:relative;overflow:hidden;width:100%}.chart svg,.chart canvas{width:100% !important}@media print{.no-print,.main-sidebar,.left-side,.main-header,.content-header{display:none !important}.content-wrapper,.right-side,.main-footer{margin-left:0 !important;min-height:0 !important;-webkit-transform:translate(0, 0) !important;-ms-transform:translate(0, 0) !important;-o-transform:translate(0, 0) !important;transform:translate(0, 0) !important}.fixed .content-wrapper,.fixed .right-side{padding-top:0 !important}.invoice{width:100%;border:0;margin:0;padding:0}.invoice-col{float:left;width:33.3333333%}.table-responsive{overflow:auto}.table-responsive>.table tr th,.table-responsive>.table tr td{white-space:normal !important}} diff --git a/public/themes/pterodactyl/vendor/adminlte/colors/skin-blue.min.css b/public/themes/pterodactyl/vendor/adminlte/colors/skin-blue.min.css index 172d3dfb..2de8c05b 100755 --- a/public/themes/pterodactyl/vendor/adminlte/colors/skin-blue.min.css +++ b/public/themes/pterodactyl/vendor/adminlte/colors/skin-blue.min.css @@ -1 +1 @@ -.skin-blue .main-header .navbar{background-color:#303f9f}.skin-blue .main-header .navbar .nav>li>a{color:#fff}.skin-blue .main-header .navbar .nav>li>a:hover,.skin-blue .main-header .navbar .nav>li>a:active,.skin-blue .main-header .navbar .nav>li>a:focus,.skin-blue .main-header .navbar .nav .open>a,.skin-blue .main-header .navbar .nav .open>a:hover,.skin-blue .main-header .navbar .nav .open>a:focus,.skin-blue .main-header .navbar .nav>.active>a{background:rgba(0,0,0,0.1);color:#f6f6f6}.skin-blue .main-header .navbar .sidebar-toggle{color:#fff}.skin-blue .main-header .navbar .sidebar-toggle:hover{color:#f6f6f6;background:rgba(0,0,0,0.1)}.skin-blue .main-header .navbar .sidebar-toggle{color:#fff}.skin-blue .main-header .navbar .sidebar-toggle:hover{background-color:#2a378b}@media (max-width:767px){.skin-blue .main-header .navbar .dropdown-menu li.divider{background-color:rgba(255,255,255,0.1)}.skin-blue .main-header .navbar .dropdown-menu li a{color:#fff}.skin-blue .main-header .navbar .dropdown-menu li a:hover{background:#2a378b}}.skin-blue .main-header .logo{background-color:#2a378b;color:#fff;border-bottom:0 solid transparent}.skin-blue .main-header .logo:hover{background-color:#293687}.skin-blue .main-header li.user-header{background-color:#303f9f}.skin-blue .content-header{background:transparent}.skin-blue .wrapper,.skin-blue .main-sidebar,.skin-blue .left-side{background-color:#1f2331}.skin-blue .user-panel>.info,.skin-blue .user-panel>.info>a{color:#fff}.skin-blue .sidebar-menu>li.header{color:#47506f;background:#171a25}.skin-blue .sidebar-menu>li>a{border-left:3px solid transparent}.skin-blue .sidebar-menu>li:hover>a,.skin-blue .sidebar-menu>li.active>a,.skin-blue .sidebar-menu>li.menu-open>a{color:#fff;background:#1b1f2b}.skin-blue .sidebar-menu>li.active>a{border-left-color:#303f9f}.skin-blue .sidebar-menu>li>.treeview-menu{margin:0 1px;background:#292e41}.skin-blue .sidebar a{color:#b3b9cf}.skin-blue .sidebar a:hover{text-decoration:none}.skin-blue .sidebar-menu .treeview-menu>li>a{color:#848eb1}.skin-blue .sidebar-menu .treeview-menu>li.active>a,.skin-blue .sidebar-menu .treeview-menu>li>a:hover{color:#fff}.skin-blue .sidebar-form{border-radius:3px;border:1px solid #333950;margin:10px 10px}.skin-blue .sidebar-form input[type="text"],.skin-blue .sidebar-form .btn{box-shadow:none;background-color:#333950;border:1px solid transparent;height:35px}.skin-blue .sidebar-form input[type="text"]{color:#666;border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px}.skin-blue .sidebar-form input[type="text"]:focus,.skin-blue .sidebar-form input[type="text"]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.skin-blue .sidebar-form input[type="text"]:focus+.input-group-btn .btn{border-left-color:#fff}.skin-blue .sidebar-form .btn{color:#999;border-top-left-radius:0;border-top-right-radius:2px;border-bottom-right-radius:2px;border-bottom-left-radius:0}.skin-blue.layout-top-nav .main-header>.logo{background-color:#303f9f;color:#fff;border-bottom:0 solid transparent}.skin-blue.layout-top-nav .main-header>.logo:hover{background-color:#2f3d9b} +.skin-blue .main-header .navbar{background-color:#303f9f}.skin-blue .main-header .navbar .nav>li>a{color:#fff}.skin-blue .main-header .navbar .nav>li>a:hover,.skin-blue .main-header .navbar .nav>li>a:active,.skin-blue .main-header .navbar .nav>li>a:focus,.skin-blue .main-header .navbar .nav .open>a,.skin-blue .main-header .navbar .nav .open>a:hover,.skin-blue .main-header .navbar .nav .open>a:focus,.skin-blue .main-header .navbar .nav>.active>a{background:rgba(0,0,0,0.1);color:#f6f6f6}.skin-blue .main-header .navbar .sidebar-toggle{color:#fff}.skin-blue .main-header .navbar .sidebar-toggle:hover{color:#f6f6f6;background:rgba(0,0,0,0.1)}.skin-blue .main-header .navbar .sidebar-toggle{color:#fff}.skin-blue .main-header .navbar .sidebar-toggle:hover{background-color:#2a378b}@media (max-width:767px){.skin-blue .main-header .navbar .dropdown-menu li.divider{background-color:rgba(255,255,255,0.1)}.skin-blue .main-header .navbar .dropdown-menu li a{color:#fff}.skin-blue .main-header .navbar .dropdown-menu li a:hover{background:#2a378b}}.skin-blue .main-header .logo{background-color:#2a378b;color:#fff;border-bottom:0 solid transparent}.skin-blue .main-header .logo:hover{background-color:#293687}.skin-blue .main-header li.user-header{background-color:#303f9f}.skin-blue .content-header{background:transparent}.skin-blue .wrapper,.skin-blue .main-sidebar,.skin-blue .left-side{background-color:#263238}.skin-blue .user-panel>.info,.skin-blue .user-panel>.info>a{color:#fff}.skin-blue .sidebar-menu>li.header{color:#4f6875;background:#1e272c}.skin-blue .sidebar-menu>li>a{border-left:3px solid transparent}.skin-blue .sidebar-menu>li:hover>a,.skin-blue .sidebar-menu>li.active>a,.skin-blue .sidebar-menu>li.menu-open>a{color:#fff;background:#222d32}.skin-blue .sidebar-menu>li.active>a{border-left-color:#303f9f}.skin-blue .sidebar-menu>li>.treeview-menu{margin:0 1px;background:#304047}.skin-blue .sidebar a{color:#bdccd3}.skin-blue .sidebar a:hover{text-decoration:none}.skin-blue .sidebar-menu .treeview-menu>li>a{color:#90a8b4}.skin-blue .sidebar-menu .treeview-menu>li.active>a,.skin-blue .sidebar-menu .treeview-menu>li>a:hover{color:#fff}.skin-blue .sidebar-form{border-radius:3px;border:1px solid #3b4d56;margin:10px 10px}.skin-blue .sidebar-form input[type="text"],.skin-blue .sidebar-form .btn{box-shadow:none;background-color:#3b4d56;border:1px solid transparent;height:35px}.skin-blue .sidebar-form input[type="text"]{color:#666;border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px}.skin-blue .sidebar-form input[type="text"]:focus,.skin-blue .sidebar-form input[type="text"]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.skin-blue .sidebar-form input[type="text"]:focus+.input-group-btn .btn{border-left-color:#fff}.skin-blue .sidebar-form .btn{color:#999;border-top-left-radius:0;border-top-right-radius:2px;border-bottom-right-radius:2px;border-bottom-left-radius:0}.skin-blue.layout-top-nav .main-header>.logo{background-color:#303f9f;color:#fff;border-bottom:0 solid transparent}.skin-blue.layout-top-nav .main-header>.logo:hover{background-color:#2f3d9b} From 0438ad7a2128a7ca999bfdd486896150c68526d9 Mon Sep 17 00:00:00 2001 From: Jakob Schrettenbrunner Date: Tue, 5 Sep 2017 00:29:34 +0200 Subject: [PATCH 86/99] switch blue and refine some colors --- public/themes/pterodactyl/css/pterodactyl.css | 2 +- public/themes/pterodactyl/vendor/adminlte/admin.min.css | 2 +- .../themes/pterodactyl/vendor/adminlte/colors/skin-blue.min.css | 2 +- resources/themes/pterodactyl/layouts/auth.blade.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/public/themes/pterodactyl/css/pterodactyl.css b/public/themes/pterodactyl/css/pterodactyl.css index d6975863..1c60da7d 100644 --- a/public/themes/pterodactyl/css/pterodactyl.css +++ b/public/themes/pterodactyl/css/pterodactyl.css @@ -23,7 +23,7 @@ @import 'checkbox.css'; .login-page { - background: #303f9f; + background: #10529f; } .login-logo { diff --git a/public/themes/pterodactyl/vendor/adminlte/admin.min.css b/public/themes/pterodactyl/vendor/adminlte/admin.min.css index 9e67e230..4098d2a3 100755 --- a/public/themes/pterodactyl/vendor/adminlte/admin.min.css +++ b/public/themes/pterodactyl/vendor/adminlte/admin.min.css @@ -4,4 +4,4 @@ * Website: Almsaeed Studio * License: Open source - MIT * Please visit http://opensource.org/licenses/MIT for more information - */html,body{height:100%}.layout-boxed html,.layout-boxed body{height:100%}body{font-family:'Source Sans Pro','Helvetica Neue',Helvetica,Arial,sans-serif;font-weight:400;overflow-x:hidden;overflow-y:auto}.wrapper{height:100%;position:relative;overflow-x:hidden;overflow-y:auto}.wrapper:before,.wrapper:after{content:" ";display:table}.wrapper:after{clear:both}.layout-boxed .wrapper{max-width:1250px;margin:0 auto;min-height:100%;box-shadow:0 0 8px rgba(0,0,0,0.5);position:relative}.layout-boxed{background:url('../img/boxed-bg.jpg') repeat fixed}.content-wrapper,.main-footer{-webkit-transition:-webkit-transform .3s ease-in-out,margin .3s ease-in-out;-moz-transition:-moz-transform .3s ease-in-out,margin .3s ease-in-out;-o-transition:-o-transform .3s ease-in-out,margin .3s ease-in-out;transition:transform .3s ease-in-out,margin .3s ease-in-out;margin-left:230px;z-index:820}.layout-top-nav .content-wrapper,.layout-top-nav .main-footer{margin-left:0}@media (max-width:767px){.content-wrapper,.main-footer{margin-left:0}}@media (min-width:768px){.sidebar-collapse .content-wrapper,.sidebar-collapse .main-footer{margin-left:0}}@media (max-width:767px){.sidebar-open .content-wrapper,.sidebar-open .main-footer{-webkit-transform:translate(230px, 0);-ms-transform:translate(230px, 0);-o-transform:translate(230px, 0);transform:translate(230px, 0)}}.content-wrapper{min-height:100%;background-color:#ecf0f5;z-index:800}.main-footer{background:#fff;padding:15px;color:#444;border-top:1px solid #d2d6de}.fixed .main-header,.fixed .main-sidebar,.fixed .left-side{position:fixed}.fixed .main-header{top:0;right:0;left:0}.fixed .content-wrapper,.fixed .right-side{padding-top:50px}@media (max-width:767px){.fixed .content-wrapper,.fixed .right-side{padding-top:100px}}.fixed.layout-boxed .wrapper{max-width:100%}.fixed .wrapper{overflow:hidden}.hold-transition .content-wrapper,.hold-transition .right-side,.hold-transition .main-footer,.hold-transition .main-sidebar,.hold-transition .left-side,.hold-transition .main-header .navbar,.hold-transition .main-header .logo,.hold-transition .menu-open .fa-angle-left{-webkit-transition:none;-o-transition:none;transition:none}.content{min-height:250px;padding:15px;margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:'Source Sans Pro',sans-serif}a{color:#303f9f}a:hover,a:active,a:focus{outline:none;text-decoration:none;color:#5161ca}.page-header{margin:10px 0 20px 0;font-size:22px}.page-header>small{color:#666;display:block;margin-top:5px}.main-header{position:relative;max-height:100px;z-index:1030}.main-header .navbar{-webkit-transition:margin-left .3s ease-in-out;-o-transition:margin-left .3s ease-in-out;transition:margin-left .3s ease-in-out;margin-bottom:0;margin-left:230px;border:none;min-height:50px;border-radius:0}.layout-top-nav .main-header .navbar{margin-left:0}.main-header #navbar-search-input.form-control{background:rgba(255,255,255,0.2);border-color:transparent}.main-header #navbar-search-input.form-control:focus,.main-header #navbar-search-input.form-control:active{border-color:rgba(0,0,0,0.1);background:rgba(255,255,255,0.9)}.main-header #navbar-search-input.form-control::-moz-placeholder{color:#ccc;opacity:1}.main-header #navbar-search-input.form-control:-ms-input-placeholder{color:#ccc}.main-header #navbar-search-input.form-control::-webkit-input-placeholder{color:#ccc}.main-header .navbar-custom-menu,.main-header .navbar-right{float:right}@media (max-width:991px){.main-header .navbar-custom-menu a,.main-header .navbar-right a{color:inherit;background:transparent}}@media (max-width:767px){.main-header .navbar-right{float:none}.navbar-collapse .main-header .navbar-right{margin:7.5px -15px}.main-header .navbar-right>li{color:inherit;border:0}}.main-header .sidebar-toggle{float:left;background-color:transparent;background-image:none;padding:15px 15px;font-family:fontAwesome}.main-header .sidebar-toggle:before{content:"\f0c9"}.main-header .sidebar-toggle:hover{color:#fff}.main-header .sidebar-toggle:focus,.main-header .sidebar-toggle:active{background:transparent}.main-header .sidebar-toggle .icon-bar{display:none}.main-header .navbar .nav>li.user>a>.fa,.main-header .navbar .nav>li.user>a>.glyphicon,.main-header .navbar .nav>li.user>a>.ion{margin-right:5px}.main-header .navbar .nav>li>a>.label{position:absolute;top:9px;right:7px;text-align:center;font-size:9px;padding:2px 3px;line-height:.9}.main-header .logo{-webkit-transition:width .3s ease-in-out;-o-transition:width .3s ease-in-out;transition:width .3s ease-in-out;display:block;float:left;height:50px;font-size:20px;line-height:50px;text-align:center;width:230px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;padding:0 15px;font-weight:300;overflow:hidden}.main-header .logo .logo-lg{display:block}.main-header .logo .logo-mini{display:none}.main-header .navbar-brand{color:#fff}.content-header{position:relative;padding:15px 15px 0 15px}.content-header>h1{margin:0;font-size:24px}.content-header>h1>small{font-size:15px;display:inline-block;padding-left:4px;font-weight:300}.content-header>.breadcrumb{float:right;background:transparent;margin-top:0;margin-bottom:0;font-size:12px;padding:7px 5px;position:absolute;top:15px;right:10px;border-radius:2px}.content-header>.breadcrumb>li>a{color:#444;text-decoration:none;display:inline-block}.content-header>.breadcrumb>li>a>.fa,.content-header>.breadcrumb>li>a>.glyphicon,.content-header>.breadcrumb>li>a>.ion{margin-right:5px}.content-header>.breadcrumb>li+li:before{content:'>\00a0'}@media (max-width:991px){.content-header>.breadcrumb{position:relative;margin-top:5px;top:0;right:0;float:none;background:#d2d6de;padding-left:10px}.content-header>.breadcrumb li:before{color:#97a0b3}}.navbar-toggle{color:#fff;border:0;margin:0;padding:15px 15px}@media (max-width:991px){.navbar-custom-menu .navbar-nav>li{float:left}.navbar-custom-menu .navbar-nav{margin:0;float:left}.navbar-custom-menu .navbar-nav>li>a{padding-top:15px;padding-bottom:15px;line-height:20px}}@media (max-width:767px){.main-header{position:relative}.main-header .logo,.main-header .navbar{width:100%;float:none}.main-header .navbar{margin:0}.main-header .navbar-custom-menu{float:right}}@media (max-width:991px){.navbar-collapse.pull-left{float:none !important}.navbar-collapse.pull-left+.navbar-custom-menu{display:block;position:absolute;top:0;right:40px}}.main-sidebar{position:absolute;top:0;left:0;padding-top:50px;min-height:100%;width:230px;z-index:810;-webkit-transition:-webkit-transform .3s ease-in-out,width .3s ease-in-out;-moz-transition:-moz-transform .3s ease-in-out,width .3s ease-in-out;-o-transition:-o-transform .3s ease-in-out,width .3s ease-in-out;transition:transform .3s ease-in-out,width .3s ease-in-out}@media (max-width:767px){.main-sidebar{padding-top:100px}}@media (max-width:767px){.main-sidebar{-webkit-transform:translate(-230px, 0);-ms-transform:translate(-230px, 0);-o-transform:translate(-230px, 0);transform:translate(-230px, 0)}}@media (min-width:768px){.sidebar-collapse .main-sidebar{-webkit-transform:translate(-230px, 0);-ms-transform:translate(-230px, 0);-o-transform:translate(-230px, 0);transform:translate(-230px, 0)}}@media (max-width:767px){.sidebar-open .main-sidebar{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0)}}.sidebar{padding-bottom:10px}.sidebar-form input:focus{border-color:transparent}.user-panel{position:relative;width:100%;padding:10px;overflow:hidden}.user-panel:before,.user-panel:after{content:" ";display:table}.user-panel:after{clear:both}.user-panel>.image>img{width:100%;max-width:45px;height:auto}.user-panel>.info{padding:5px 5px 5px 15px;line-height:1;position:absolute;left:55px}.user-panel>.info>p{font-weight:600;margin-bottom:9px}.user-panel>.info>a{text-decoration:none;padding-right:5px;margin-top:3px;font-size:11px}.user-panel>.info>a>.fa,.user-panel>.info>a>.ion,.user-panel>.info>a>.glyphicon{margin-right:3px}.sidebar-menu{list-style:none;margin:0;padding:0}.sidebar-menu>li{position:relative;margin:0;padding:0}.sidebar-menu>li>a{padding:12px 5px 12px 15px;display:block}.sidebar-menu>li>a>.fa,.sidebar-menu>li>a>.glyphicon,.sidebar-menu>li>a>.ion{width:20px}.sidebar-menu>li .label,.sidebar-menu>li .badge{margin-right:5px}.sidebar-menu>li .badge{margin-top:3px}.sidebar-menu li.header{padding:10px 25px 10px 15px;font-size:12px}.sidebar-menu li>a>.fa-angle-left,.sidebar-menu li>a>.pull-right-container>.fa-angle-left{width:auto;height:auto;padding:0;margin-right:10px;-webkit-transition:transform .5s ease;-o-transition:transform .5s ease;transition:transform .5s ease}.sidebar-menu li>a>.fa-angle-left{position:absolute;top:50%;right:10px;margin-top:-8px}.sidebar-menu .menu-open>a>.fa-angle-left,.sidebar-menu .menu-open>a>.pull-right-container>.fa-angle-left{-webkit-transform:rotate(-90deg);-ms-transform:rotate(-90deg);-o-transform:rotate(-90deg);transform:rotate(-90deg)}.sidebar-menu .active>.treeview-menu{display:block}@media (min-width:768px){.sidebar-mini.sidebar-collapse .content-wrapper,.sidebar-mini.sidebar-collapse .right-side,.sidebar-mini.sidebar-collapse .main-footer{margin-left:50px !important;z-index:840}.sidebar-mini.sidebar-collapse .main-sidebar{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0);width:50px !important;z-index:850}.sidebar-mini.sidebar-collapse .sidebar-menu>li{position:relative}.sidebar-mini.sidebar-collapse .sidebar-menu>li>a{margin-right:0}.sidebar-mini.sidebar-collapse .sidebar-menu>li>a>span{border-top-right-radius:4px}.sidebar-mini.sidebar-collapse .sidebar-menu>li:not(.treeview)>a>span{border-bottom-right-radius:4px}.sidebar-mini.sidebar-collapse .sidebar-menu>li>.treeview-menu{padding-top:5px;padding-bottom:5px;border-bottom-right-radius:4px}.sidebar-mini.sidebar-collapse .main-sidebar .user-panel>.info,.sidebar-mini.sidebar-collapse .sidebar-form,.sidebar-mini.sidebar-collapse .sidebar-menu>li>a>span,.sidebar-mini.sidebar-collapse .sidebar-menu>li>.treeview-menu,.sidebar-mini.sidebar-collapse .sidebar-menu>li>a>.pull-right,.sidebar-mini.sidebar-collapse .sidebar-menu li.header{display:none !important;-webkit-transform:translateZ(0)}.sidebar-mini.sidebar-collapse .main-header .logo{width:50px}.sidebar-mini.sidebar-collapse .main-header .logo>.logo-mini{display:block;margin-left:-15px;margin-right:-15px;font-size:18px}.sidebar-mini.sidebar-collapse .main-header .logo>.logo-lg{display:none}.sidebar-mini.sidebar-collapse .main-header .navbar{margin-left:50px}}.sidebar-mini:not(.sidebar-mini-expand-feature).sidebar-collapse .sidebar-menu>li:hover>a>span:not(.pull-right),.sidebar-mini:not(.sidebar-mini-expand-feature).sidebar-collapse .sidebar-menu>li:hover>.treeview-menu{display:block !important;position:absolute;width:180px;left:50px}.sidebar-mini:not(.sidebar-mini-expand-feature).sidebar-collapse .sidebar-menu>li:hover>a>span{top:0;margin-left:-3px;padding:12px 5px 12px 20px;background-color:inherit}.sidebar-mini:not(.sidebar-mini-expand-feature).sidebar-collapse .sidebar-menu>li:hover>a>.pull-right-container{position:relative !important;float:right;width:auto !important;left:180px !important;top:-22px !important;z-index:900}.sidebar-mini:not(.sidebar-mini-expand-feature).sidebar-collapse .sidebar-menu>li:hover>a>.pull-right-container>.label:not(:first-of-type){display:none}.sidebar-mini:not(.sidebar-mini-expand-feature).sidebar-collapse .sidebar-menu>li:hover>.treeview-menu{top:44px;margin-left:0}.sidebar-expanded-on-hover .main-footer,.sidebar-expanded-on-hover .content-wrapper{margin-left:50px}.sidebar-expanded-on-hover .main-sidebar{box-shadow:3px 0 8px rgba(0,0,0,0.125)}.sidebar-menu,.main-sidebar .user-panel,.sidebar-menu>li.header{white-space:nowrap;overflow:hidden}.sidebar-menu:hover{overflow:visible}.sidebar-form,.sidebar-menu>li.header{overflow:hidden;text-overflow:clip}.sidebar-menu li>a{position:relative}.sidebar-menu li>a>.pull-right-container{position:absolute;right:10px;top:50%;margin-top:-7px}.control-sidebar-bg{position:fixed;z-index:1000;bottom:0}.control-sidebar-bg,.control-sidebar{top:0;right:-230px;width:230px;-webkit-transition:right .3s ease-in-out;-o-transition:right .3s ease-in-out;transition:right .3s ease-in-out}.control-sidebar{position:absolute;padding-top:50px;z-index:1010}@media (max-width:768px){.control-sidebar{padding-top:100px}}.control-sidebar>.tab-content{padding:10px 15px}.control-sidebar.control-sidebar-open,.control-sidebar.control-sidebar-open+.control-sidebar-bg{right:0}.control-sidebar-open .control-sidebar-bg,.control-sidebar-open .control-sidebar{right:0}@media (min-width:768px){.control-sidebar-open .content-wrapper,.control-sidebar-open .right-side,.control-sidebar-open .main-footer{margin-right:230px}}.fixed .control-sidebar{position:fixed;height:100%;overflow-y:auto;padding-bottom:50px}.nav-tabs.control-sidebar-tabs>li:first-of-type>a,.nav-tabs.control-sidebar-tabs>li:first-of-type>a:hover,.nav-tabs.control-sidebar-tabs>li:first-of-type>a:focus{border-left-width:0}.nav-tabs.control-sidebar-tabs>li>a{border-radius:0}.nav-tabs.control-sidebar-tabs>li>a,.nav-tabs.control-sidebar-tabs>li>a:hover{border-top:none;border-right:none;border-left:1px solid transparent;border-bottom:1px solid transparent}.nav-tabs.control-sidebar-tabs>li>a .icon{font-size:16px}.nav-tabs.control-sidebar-tabs>li.active>a,.nav-tabs.control-sidebar-tabs>li.active>a:hover,.nav-tabs.control-sidebar-tabs>li.active>a:focus,.nav-tabs.control-sidebar-tabs>li.active>a:active{border-top:none;border-right:none;border-bottom:none}@media (max-width:768px){.nav-tabs.control-sidebar-tabs{display:table}.nav-tabs.control-sidebar-tabs>li{display:table-cell}}.control-sidebar-heading{font-weight:400;font-size:16px;padding:10px 0;margin-bottom:10px}.control-sidebar-subheading{display:block;font-weight:400;font-size:14px}.control-sidebar-menu{list-style:none;padding:0;margin:0 -15px}.control-sidebar-menu>li>a{display:block;padding:10px 15px}.control-sidebar-menu>li>a:before,.control-sidebar-menu>li>a:after{content:" ";display:table}.control-sidebar-menu>li>a:after{clear:both}.control-sidebar-menu>li>a>.control-sidebar-subheading{margin-top:0}.control-sidebar-menu .menu-icon{float:left;width:35px;height:35px;border-radius:50%;text-align:center;line-height:35px}.control-sidebar-menu .menu-info{margin-left:45px;margin-top:3px}.control-sidebar-menu .menu-info>.control-sidebar-subheading{margin:0}.control-sidebar-menu .menu-info>p{margin:0;font-size:11px}.control-sidebar-menu .progress{margin:0}.control-sidebar-dark{color:#bdccd3}.control-sidebar-dark,.control-sidebar-dark+.control-sidebar-bg{background:#263238}.control-sidebar-dark .nav-tabs.control-sidebar-tabs{border-bottom:#202a2f}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a{background:#1c2429;color:#bdccd3}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:hover,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:focus{border-left-color:#181f23;border-bottom-color:#181f23}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:hover,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:focus,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:active{background:#202a2f}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:hover{color:#fff}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a:hover,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a:focus,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a:active{background:#263238;color:#fff}.control-sidebar-dark .control-sidebar-heading,.control-sidebar-dark .control-sidebar-subheading{color:#fff}.control-sidebar-dark .control-sidebar-menu>li>a:hover{background:#222d32}.control-sidebar-dark .control-sidebar-menu>li>a .menu-info>p{color:#bdccd3}.control-sidebar-light{color:#5e5e5e}.control-sidebar-light,.control-sidebar-light+.control-sidebar-bg{background:#f9fafc;border-left:1px solid #d2d6de}.control-sidebar-light .nav-tabs.control-sidebar-tabs{border-bottom:#d2d6de}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a{background:#e8ecf4;color:#444}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:hover,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:focus{border-left-color:#d2d6de;border-bottom-color:#d2d6de}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:hover,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:focus,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:active{background:#eff1f7}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a:hover,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a:focus,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a:active{background:#f9fafc;color:#111}.control-sidebar-light .control-sidebar-heading,.control-sidebar-light .control-sidebar-subheading{color:#111}.control-sidebar-light .control-sidebar-menu{margin-left:-14px}.control-sidebar-light .control-sidebar-menu>li>a:hover{background:#f4f4f5}.control-sidebar-light .control-sidebar-menu>li>a .menu-info>p{color:#5e5e5e}.dropdown-menu{box-shadow:none;border-color:#eee}.dropdown-menu>li>a{color:#777}.dropdown-menu>li>a>.glyphicon,.dropdown-menu>li>a>.fa,.dropdown-menu>li>a>.ion{margin-right:10px}.dropdown-menu>li>a:hover{background-color:#e1e3e9;color:#333}.dropdown-menu>.divider{background-color:#eee}.navbar-nav>.notifications-menu>.dropdown-menu,.navbar-nav>.messages-menu>.dropdown-menu,.navbar-nav>.tasks-menu>.dropdown-menu{width:280px;padding:0 0 0 0;margin:0;top:100%}.navbar-nav>.notifications-menu>.dropdown-menu>li,.navbar-nav>.messages-menu>.dropdown-menu>li,.navbar-nav>.tasks-menu>.dropdown-menu>li{position:relative}.navbar-nav>.notifications-menu>.dropdown-menu>li.header,.navbar-nav>.messages-menu>.dropdown-menu>li.header,.navbar-nav>.tasks-menu>.dropdown-menu>li.header{border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0;background-color:#ffffff;padding:7px 10px;border-bottom:1px solid #f4f4f4;color:#444444;font-size:14px}.navbar-nav>.notifications-menu>.dropdown-menu>li.footer>a,.navbar-nav>.messages-menu>.dropdown-menu>li.footer>a,.navbar-nav>.tasks-menu>.dropdown-menu>li.footer>a{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px;font-size:12px;background-color:#fff;padding:7px 10px;border-bottom:1px solid #eeeeee;color:#444 !important;text-align:center}@media (max-width:991px){.navbar-nav>.notifications-menu>.dropdown-menu>li.footer>a,.navbar-nav>.messages-menu>.dropdown-menu>li.footer>a,.navbar-nav>.tasks-menu>.dropdown-menu>li.footer>a{background:#fff !important;color:#444 !important}}.navbar-nav>.notifications-menu>.dropdown-menu>li.footer>a:hover,.navbar-nav>.messages-menu>.dropdown-menu>li.footer>a:hover,.navbar-nav>.tasks-menu>.dropdown-menu>li.footer>a:hover{text-decoration:none;font-weight:normal}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu,.navbar-nav>.messages-menu>.dropdown-menu>li .menu,.navbar-nav>.tasks-menu>.dropdown-menu>li .menu{max-height:200px;margin:0;padding:0;list-style:none;overflow-x:hidden}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a,.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a,.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a{display:block;white-space:nowrap;border-bottom:1px solid #f4f4f4}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a:hover,.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:hover,.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a:hover{background:#f4f4f4;text-decoration:none}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a{color:#444444;overflow:hidden;text-overflow:ellipsis;padding:10px}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a>.glyphicon,.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a>.fa,.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a>.ion{width:20px}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a{margin:0;padding:10px 10px}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>div>img{margin:auto 10px auto auto;width:40px;height:40px}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>h4{padding:0;margin:0 0 0 45px;color:#444444;font-size:15px;position:relative}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>h4>small{color:#999999;font-size:10px;position:absolute;top:0;right:0}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>p{margin:0 0 0 45px;font-size:12px;color:#888888}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:before,.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:after{content:" ";display:table}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:after{clear:both}.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a{padding:10px}.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a>h3{font-size:14px;padding:0;margin:0 0 10px 0;color:#666666}.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a>.progress{padding:0;margin:0}.navbar-nav>.user-menu>.dropdown-menu{border-top-right-radius:0;border-top-left-radius:0;padding:1px 0 0 0;border-top-width:0;width:280px}.navbar-nav>.user-menu>.dropdown-menu,.navbar-nav>.user-menu>.dropdown-menu>.user-body{border-bottom-right-radius:4px;border-bottom-left-radius:4px}.navbar-nav>.user-menu>.dropdown-menu>li.user-header{height:175px;padding:10px;text-align:center}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>img{z-index:5;height:90px;width:90px;border:3px solid;border-color:transparent;border-color:rgba(255,255,255,0.2)}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>p{z-index:5;color:#fff;color:rgba(255,255,255,0.8);font-size:17px;margin-top:10px}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>p>small{display:block;font-size:12px}.navbar-nav>.user-menu>.dropdown-menu>.user-body{padding:15px;border-bottom:1px solid #f4f4f4;border-top:1px solid #dddddd}.navbar-nav>.user-menu>.dropdown-menu>.user-body:before,.navbar-nav>.user-menu>.dropdown-menu>.user-body:after{content:" ";display:table}.navbar-nav>.user-menu>.dropdown-menu>.user-body:after{clear:both}.navbar-nav>.user-menu>.dropdown-menu>.user-body a{color:#444 !important}@media (max-width:991px){.navbar-nav>.user-menu>.dropdown-menu>.user-body a{background:#fff !important;color:#444 !important}}.navbar-nav>.user-menu>.dropdown-menu>.user-footer{background-color:#f9f9f9;padding:10px}.navbar-nav>.user-menu>.dropdown-menu>.user-footer:before,.navbar-nav>.user-menu>.dropdown-menu>.user-footer:after{content:" ";display:table}.navbar-nav>.user-menu>.dropdown-menu>.user-footer:after{clear:both}.navbar-nav>.user-menu>.dropdown-menu>.user-footer .btn-default{color:#666666}@media (max-width:991px){.navbar-nav>.user-menu>.dropdown-menu>.user-footer .btn-default:hover{background-color:#f9f9f9}}.navbar-nav>.user-menu .user-image{float:left;width:25px;height:25px;border-radius:50%;margin-right:10px;margin-top:-2px}@media (max-width:767px){.navbar-nav>.user-menu .user-image{float:none;margin-right:0;margin-top:-8px;line-height:10px}}.open:not(.dropup)>.animated-dropdown-menu{backface-visibility:visible !important;-webkit-animation:flipInX .7s both;-o-animation:flipInX .7s both;animation:flipInX .7s both}@keyframes flipInX{0%{transform:perspective(400px) rotate3d(1, 0, 0, 90deg);transition-timing-function:ease-in;opacity:0}40%{transform:perspective(400px) rotate3d(1, 0, 0, -20deg);transition-timing-function:ease-in}60%{transform:perspective(400px) rotate3d(1, 0, 0, 10deg);opacity:1}80%{transform:perspective(400px) rotate3d(1, 0, 0, -5deg)}100%{transform:perspective(400px)}}@-webkit-keyframes flipInX{0%{-webkit-transform:perspective(400px) rotate3d(1, 0, 0, 90deg);-webkit-transition-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotate3d(1, 0, 0, -20deg);-webkit-transition-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotate3d(1, 0, 0, 10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotate3d(1, 0, 0, -5deg)}100%{-webkit-transform:perspective(400px)}}.navbar-custom-menu>.navbar-nav>li{position:relative}.navbar-custom-menu>.navbar-nav>li>.dropdown-menu{position:absolute;right:0;left:auto}@media (max-width:991px){.navbar-custom-menu>.navbar-nav{float:right}.navbar-custom-menu>.navbar-nav>li{position:static}.navbar-custom-menu>.navbar-nav>li>.dropdown-menu{position:absolute;right:5%;left:auto;border:1px solid #ddd;background:#fff}}.form-control{border-radius:0;box-shadow:none;border-color:#d2d6de}.form-control:focus{border-color:#303f9f;box-shadow:none}.form-control::-moz-placeholder,.form-control:-ms-input-placeholder,.form-control::-webkit-input-placeholder{color:#bbb;opacity:1}.form-control:not(select){-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-group.has-success label{color:#00a65a}.form-group.has-success .form-control,.form-group.has-success .input-group-addon{border-color:#00a65a;box-shadow:none}.form-group.has-success .help-block{color:#00a65a}.form-group.has-warning label{color:#f39c12}.form-group.has-warning .form-control,.form-group.has-warning .input-group-addon{border-color:#f39c12;box-shadow:none}.form-group.has-warning .help-block{color:#f39c12}.form-group.has-error label{color:#dd4b39}.form-group.has-error .form-control,.form-group.has-error .input-group-addon{border-color:#dd4b39;box-shadow:none}.form-group.has-error .help-block{color:#dd4b39}.input-group .input-group-addon{border-radius:0;border-color:#d2d6de;background-color:#fff}.btn-group-vertical .btn.btn-flat:first-of-type,.btn-group-vertical .btn.btn-flat:last-of-type{border-radius:0}.icheck>label{padding-left:0}.form-control-feedback.fa{line-height:34px}.input-lg+.form-control-feedback.fa,.input-group-lg+.form-control-feedback.fa,.form-group-lg .form-control+.form-control-feedback.fa{line-height:46px}.input-sm+.form-control-feedback.fa,.input-group-sm+.form-control-feedback.fa,.form-group-sm .form-control+.form-control-feedback.fa{line-height:30px}.progress,.progress>.progress-bar{-webkit-box-shadow:none;box-shadow:none}.progress,.progress>.progress-bar,.progress .progress-bar,.progress>.progress-bar .progress-bar{border-radius:1px}.progress.sm,.progress-sm{height:10px}.progress.sm,.progress-sm,.progress.sm .progress-bar,.progress-sm .progress-bar{border-radius:1px}.progress.xs,.progress-xs{height:7px}.progress.xs,.progress-xs,.progress.xs .progress-bar,.progress-xs .progress-bar{border-radius:1px}.progress.xxs,.progress-xxs{height:3px}.progress.xxs,.progress-xxs,.progress.xxs .progress-bar,.progress-xxs .progress-bar{border-radius:1px}.progress.vertical{position:relative;width:30px;height:200px;display:inline-block;margin-right:10px}.progress.vertical>.progress-bar{width:100%;position:absolute;bottom:0}.progress.vertical.sm,.progress.vertical.progress-sm{width:20px}.progress.vertical.xs,.progress.vertical.progress-xs{width:10px}.progress.vertical.xxs,.progress.vertical.progress-xxs{width:3px}.progress-group .progress-text{font-weight:600}.progress-group .progress-number{float:right}.table tr>td .progress{margin:0}.progress-bar-light-blue,.progress-bar-primary{background-color:#303f9f}.progress-striped .progress-bar-light-blue,.progress-striped .progress-bar-primary{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-green,.progress-bar-success{background-color:#00a65a}.progress-striped .progress-bar-green,.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-aqua,.progress-bar-info{background-color:#00c0ef}.progress-striped .progress-bar-aqua,.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-yellow,.progress-bar-warning{background-color:#f39c12}.progress-striped .progress-bar-yellow,.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-red,.progress-bar-danger{background-color:#dd4b39}.progress-striped .progress-bar-red,.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.small-box{border-radius:2px;position:relative;display:block;margin-bottom:20px;box-shadow:0 1px 1px rgba(0,0,0,0.1)}.small-box>.inner{padding:10px}.small-box>.small-box-footer{position:relative;text-align:center;padding:3px 0;color:#fff;color:rgba(255,255,255,0.8);display:block;z-index:10;background:rgba(0,0,0,0.1);text-decoration:none}.small-box>.small-box-footer:hover{color:#fff;background:rgba(0,0,0,0.15)}.small-box h3{font-size:38px;font-weight:bold;margin:0 0 10px 0;white-space:nowrap;padding:0}.small-box p{font-size:15px}.small-box p>small{display:block;color:#f9f9f9;font-size:13px;margin-top:5px}.small-box h3,.small-box p{z-index:5}.small-box .icon{-webkit-transition:all .3s linear;-o-transition:all .3s linear;transition:all .3s linear;position:absolute;top:-10px;right:10px;z-index:0;font-size:90px;color:rgba(0,0,0,0.15)}.small-box:hover{text-decoration:none;color:#f9f9f9}.small-box:hover .icon{font-size:95px}@media (max-width:767px){.small-box{text-align:center}.small-box .icon{display:none}.small-box p{font-size:12px}}.box{position:relative;border-radius:3px;background:#ffffff;border-top:3px solid #d2d6de;margin-bottom:20px;width:100%;box-shadow:0 1px 1px rgba(0,0,0,0.1)}.box.box-primary{border-top-color:#303f9f}.box.box-info{border-top-color:#00c0ef}.box.box-danger{border-top-color:#dd4b39}.box.box-warning{border-top-color:#f39c12}.box.box-success{border-top-color:#00a65a}.box.box-default{border-top-color:#d2d6de}.box.collapsed-box .box-body,.box.collapsed-box .box-footer{display:none}.box .nav-stacked>li{border-bottom:1px solid #f4f4f4;margin:0}.box .nav-stacked>li:last-of-type{border-bottom:none}.box.height-control .box-body{max-height:300px;overflow:auto}.box .border-right{border-right:1px solid #f4f4f4}.box .border-left{border-left:1px solid #f4f4f4}.box.box-solid{border-top:0}.box.box-solid>.box-header .btn.btn-default{background:transparent}.box.box-solid>.box-header .btn:hover,.box.box-solid>.box-header a:hover{background:rgba(0,0,0,0.1)}.box.box-solid.box-default{border:1px solid #d2d6de}.box.box-solid.box-default>.box-header{color:#444;background:#d2d6de;background-color:#d2d6de}.box.box-solid.box-default>.box-header a,.box.box-solid.box-default>.box-header .btn{color:#444}.box.box-solid.box-primary{border:1px solid #303f9f}.box.box-solid.box-primary>.box-header{color:#fff;background:#303f9f;background-color:#303f9f}.box.box-solid.box-primary>.box-header a,.box.box-solid.box-primary>.box-header .btn{color:#fff}.box.box-solid.box-info{border:1px solid #00c0ef}.box.box-solid.box-info>.box-header{color:#fff;background:#00c0ef;background-color:#00c0ef}.box.box-solid.box-info>.box-header a,.box.box-solid.box-info>.box-header .btn{color:#fff}.box.box-solid.box-danger{border:1px solid #dd4b39}.box.box-solid.box-danger>.box-header{color:#fff;background:#dd4b39;background-color:#dd4b39}.box.box-solid.box-danger>.box-header a,.box.box-solid.box-danger>.box-header .btn{color:#fff}.box.box-solid.box-warning{border:1px solid #f39c12}.box.box-solid.box-warning>.box-header{color:#fff;background:#f39c12;background-color:#f39c12}.box.box-solid.box-warning>.box-header a,.box.box-solid.box-warning>.box-header .btn{color:#fff}.box.box-solid.box-success{border:1px solid #00a65a}.box.box-solid.box-success>.box-header{color:#fff;background:#00a65a;background-color:#00a65a}.box.box-solid.box-success>.box-header a,.box.box-solid.box-success>.box-header .btn{color:#fff}.box.box-solid>.box-header>.box-tools .btn{border:0;box-shadow:none}.box.box-solid[class*='bg']>.box-header{color:#fff}.box .box-group>.box{margin-bottom:5px}.box .knob-label{text-align:center;color:#333;font-weight:100;font-size:12px;margin-bottom:0.3em}.box>.overlay,.overlay-wrapper>.overlay,.box>.loading-img,.overlay-wrapper>.loading-img{position:absolute;top:0;left:0;width:100%;height:100%}.box .overlay,.overlay-wrapper .overlay{z-index:50;background:rgba(255,255,255,0.7);border-radius:3px}.box .overlay>.fa,.overlay-wrapper .overlay>.fa{position:absolute;top:50%;left:50%;margin-left:-15px;margin-top:-15px;color:#000;font-size:30px}.box .overlay.dark,.overlay-wrapper .overlay.dark{background:rgba(0,0,0,0.5)}.box-header:before,.box-body:before,.box-footer:before,.box-header:after,.box-body:after,.box-footer:after{content:" ";display:table}.box-header:after,.box-body:after,.box-footer:after{clear:both}.box-header{color:#444;display:block;padding:10px;position:relative}.box-header.with-border{border-bottom:1px solid #f4f4f4}.collapsed-box .box-header.with-border{border-bottom:none}.box-header>.fa,.box-header>.glyphicon,.box-header>.ion,.box-header .box-title{display:inline-block;font-size:18px;margin:0;line-height:1}.box-header>.fa,.box-header>.glyphicon,.box-header>.ion{margin-right:5px}.box-header>.box-tools{position:absolute;right:10px;top:5px}.box-header>.box-tools [data-toggle="tooltip"]{position:relative}.box-header>.box-tools.pull-right .dropdown-menu{right:0;left:auto}.box-header>.box-tools .dropdown-menu>li>a{color:#444!important}.btn-box-tool{padding:5px;font-size:12px;background:transparent;color:#97a0b3}.open .btn-box-tool,.btn-box-tool:hover{color:#606c84}.btn-box-tool.btn:active{box-shadow:none}.box-body{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px;padding:10px}.no-header .box-body{border-top-right-radius:3px;border-top-left-radius:3px}.box-body>.table{margin-bottom:0}.box-body .fc{margin-top:5px}.box-body .full-width-chart{margin:-19px}.box-body.no-padding .full-width-chart{margin:-9px}.box-body .box-pane{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:3px}.box-body .box-pane-right{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:3px;border-bottom-left-radius:0}.box-footer{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px;border-top:1px solid #f4f4f4;padding:10px;background-color:#fff}.chart-legend{margin:10px 0}@media (max-width:991px){.chart-legend>li{float:left;margin-right:10px}}.box-comments{background:#f7f7f7}.box-comments .box-comment{padding:8px 0;border-bottom:1px solid #eee}.box-comments .box-comment:before,.box-comments .box-comment:after{content:" ";display:table}.box-comments .box-comment:after{clear:both}.box-comments .box-comment:last-of-type{border-bottom:0}.box-comments .box-comment:first-of-type{padding-top:0}.box-comments .box-comment img{float:left}.box-comments .comment-text{margin-left:40px;color:#555}.box-comments .username{color:#444;display:block;font-weight:600}.box-comments .text-muted{font-weight:400;font-size:12px}.todo-list{margin:0;padding:0;list-style:none;overflow:auto}.todo-list>li{border-radius:2px;padding:10px;background:#f4f4f4;margin-bottom:2px;border-left:2px solid #e6e7e8;color:#444}.todo-list>li:last-of-type{margin-bottom:0}.todo-list>li>input[type='checkbox']{margin:0 10px 0 5px}.todo-list>li .text{display:inline-block;margin-left:5px;font-weight:600}.todo-list>li .label{margin-left:10px;font-size:9px}.todo-list>li .tools{display:none;float:right;color:#dd4b39}.todo-list>li .tools>.fa,.todo-list>li .tools>.glyphicon,.todo-list>li .tools>.ion{margin-right:5px;cursor:pointer}.todo-list>li:hover .tools{display:inline-block}.todo-list>li.done{color:#999}.todo-list>li.done .text{text-decoration:line-through;font-weight:500}.todo-list>li.done .label{background:#d2d6de !important}.todo-list .danger{border-left-color:#dd4b39}.todo-list .warning{border-left-color:#f39c12}.todo-list .info{border-left-color:#00c0ef}.todo-list .success{border-left-color:#00a65a}.todo-list .primary{border-left-color:#303f9f}.todo-list .handle{display:inline-block;cursor:move;margin:0 5px}.chat{padding:5px 20px 5px 10px}.chat .item{margin-bottom:10px}.chat .item:before,.chat .item:after{content:" ";display:table}.chat .item:after{clear:both}.chat .item>img{width:40px;height:40px;border:2px solid transparent;border-radius:50%}.chat .item>.online{border:2px solid #00a65a}.chat .item>.offline{border:2px solid #dd4b39}.chat .item>.message{margin-left:55px;margin-top:-40px}.chat .item>.message>.name{display:block;font-weight:600}.chat .item>.attachment{border-radius:3px;background:#f4f4f4;margin-left:65px;margin-right:15px;padding:10px}.chat .item>.attachment>h4{margin:0 0 5px 0;font-weight:600;font-size:14px}.chat .item>.attachment>p,.chat .item>.attachment>.filename{font-weight:600;font-size:13px;font-style:italic;margin:0}.chat .item>.attachment:before,.chat .item>.attachment:after{content:" ";display:table}.chat .item>.attachment:after{clear:both}.box-input{max-width:200px}.modal .panel-body{color:#444}.info-box{display:block;min-height:90px;background:#fff;width:100%;box-shadow:0 1px 1px rgba(0,0,0,0.1);border-radius:2px;margin-bottom:15px}.info-box small{font-size:14px}.info-box .progress{background:rgba(0,0,0,0.2);margin:5px -10px 5px -10px;height:2px}.info-box .progress,.info-box .progress .progress-bar{border-radius:0}.info-box .progress .progress-bar{background:#fff}.info-box-icon{border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px;display:block;float:left;height:90px;width:90px;text-align:center;font-size:45px;line-height:90px;background:rgba(0,0,0,0.2)}.info-box-icon>img{max-width:100%}.info-box-content{padding:5px 10px;margin-left:90px}.info-box-number{display:block;font-weight:bold;font-size:18px}.progress-description,.info-box-text{display:block;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.info-box-text{text-transform:uppercase}.info-box-more{display:block}.progress-description{margin:0}.timeline{position:relative;margin:0 0 30px 0;padding:0;list-style:none}.timeline:before{content:'';position:absolute;top:0;bottom:0;width:4px;background:#ddd;left:31px;margin:0;border-radius:2px}.timeline>li{position:relative;margin-right:10px;margin-bottom:15px}.timeline>li:before,.timeline>li:after{content:" ";display:table}.timeline>li:after{clear:both}.timeline>li>.timeline-item{-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.1);box-shadow:0 1px 1px rgba(0,0,0,0.1);border-radius:3px;margin-top:0;background:#fff;color:#444;margin-left:60px;margin-right:15px;padding:0;position:relative}.timeline>li>.timeline-item>.time{color:#999;float:right;padding:10px;font-size:12px}.timeline>li>.timeline-item>.timeline-header{margin:0;color:#555;border-bottom:1px solid #f4f4f4;padding:10px;font-size:16px;line-height:1.1}.timeline>li>.timeline-item>.timeline-header>a{font-weight:600}.timeline>li>.timeline-item>.timeline-body,.timeline>li>.timeline-item>.timeline-footer{padding:10px}.timeline>li>.fa,.timeline>li>.glyphicon,.timeline>li>.ion{width:30px;height:30px;font-size:15px;line-height:30px;position:absolute;color:#666;background:#d2d6de;border-radius:50%;text-align:center;left:18px;top:0}.timeline>.time-label>span{font-weight:600;padding:5px;display:inline-block;background-color:#fff;border-radius:4px}.timeline-inverse>li>.timeline-item{background:#f0f0f0;border:1px solid #ddd;-webkit-box-shadow:none;box-shadow:none}.timeline-inverse>li>.timeline-item>.timeline-header{border-bottom-color:#ddd}.btn{border-radius:3px;-webkit-box-shadow:none;box-shadow:none;border:1px solid transparent}.btn.uppercase{text-transform:uppercase}.btn.btn-flat{border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;border-width:1px}.btn:active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);-moz-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn:focus{outline:none}.btn.btn-file{position:relative;overflow:hidden}.btn.btn-file>input[type='file']{position:absolute;top:0;right:0;min-width:100%;min-height:100%;font-size:100px;text-align:right;opacity:0;filter:alpha(opacity=0);outline:none;background:white;cursor:inherit;display:block}.btn-default{background-color:#f4f4f4;color:#444;border-color:#ddd}.btn-default:hover,.btn-default:active,.btn-default.hover{background-color:#e7e7e7}.btn-primary{background-color:#303f9f;border-color:#2a378b}.btn-primary:hover,.btn-primary:active,.btn-primary.hover{background-color:#2a378b}.btn-success{background-color:#00a65a;border-color:#008d4c}.btn-success:hover,.btn-success:active,.btn-success.hover{background-color:#008d4c}.btn-info{background-color:#00c0ef;border-color:#00acd6}.btn-info:hover,.btn-info:active,.btn-info.hover{background-color:#00acd6}.btn-danger{background-color:#dd4b39;border-color:#d73925}.btn-danger:hover,.btn-danger:active,.btn-danger.hover{background-color:#d73925}.btn-warning{background-color:#f39c12;border-color:#e08e0b}.btn-warning:hover,.btn-warning:active,.btn-warning.hover{background-color:#e08e0b}.btn-outline{border:1px solid #fff;background:transparent;color:#fff}.btn-outline:hover,.btn-outline:focus,.btn-outline:active{color:rgba(255,255,255,0.7);border-color:rgba(255,255,255,0.7)}.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn[class*='bg-']:hover{-webkit-box-shadow:inset 0 0 100px rgba(0,0,0,0.2);box-shadow:inset 0 0 100px rgba(0,0,0,0.2)}.btn-app{border-radius:3px;position:relative;padding:15px 5px;margin:0 0 10px 10px;min-width:80px;height:60px;text-align:center;color:#666;border:1px solid #ddd;background-color:#f4f4f4;font-size:12px}.btn-app>.fa,.btn-app>.glyphicon,.btn-app>.ion{font-size:20px;display:block}.btn-app:hover{background:#f4f4f4;color:#444;border-color:#aaa}.btn-app:active,.btn-app:focus{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);-moz-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-app>.badge{position:absolute;top:-3px;right:-10px;font-size:10px;font-weight:400}.callout{border-radius:3px;margin:0 0 20px 0;padding:15px 30px 15px 15px;border-left:5px solid #eee}.callout a{color:#fff;text-decoration:underline}.callout a:hover{color:#eee}.callout h4{margin-top:0;font-weight:600}.callout p:last-child{margin-bottom:0}.callout code,.callout .highlight{background-color:#fff}.callout.callout-danger{border-color:#c23321}.callout.callout-warning{border-color:#c87f0a}.callout.callout-info{border-color:#0097bc}.callout.callout-success{border-color:#00733e}.alert{border-radius:3px}.alert h4{font-weight:600}.alert .icon{margin-right:10px}.alert .close{color:#000;opacity:.2;filter:alpha(opacity=20)}.alert .close:hover{opacity:.5;filter:alpha(opacity=50)}.alert a{color:#fff;text-decoration:underline}.alert-success{border-color:#008d4c}.alert-danger,.alert-error{border-color:#d73925}.alert-warning{border-color:#e08e0b}.alert-info{border-color:#00acd6}.nav>li>a:hover,.nav>li>a:active,.nav>li>a:focus{color:#444;background:#f7f7f7}.nav-pills>li>a{border-radius:0;border-top:3px solid transparent;color:#444}.nav-pills>li>a>.fa,.nav-pills>li>a>.glyphicon,.nav-pills>li>a>.ion{margin-right:5px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{border-top-color:#303f9f}.nav-pills>li.active>a{font-weight:600}.nav-stacked>li>a{border-radius:0;border-top:0;border-left:3px solid transparent;color:#444}.nav-stacked>li.active>a,.nav-stacked>li.active>a:hover{background:transparent;color:#444;border-top:0;border-left-color:#303f9f}.nav-stacked>li.header{border-bottom:1px solid #ddd;color:#777;margin-bottom:10px;padding:5px 10px;text-transform:uppercase}.nav-tabs-custom{margin-bottom:20px;background:#fff;box-shadow:0 1px 1px rgba(0,0,0,0.1);border-radius:3px}.nav-tabs-custom>.nav-tabs{margin:0;border-bottom-color:#f4f4f4;border-top-right-radius:3px;border-top-left-radius:3px}.nav-tabs-custom>.nav-tabs>li{border-top:3px solid transparent;margin-bottom:-2px;margin-right:5px}.nav-tabs-custom>.nav-tabs>li.disabled>a{color:#777}.nav-tabs-custom>.nav-tabs>li>a{color:#444;border-radius:0}.nav-tabs-custom>.nav-tabs>li>a.text-muted{color:#999}.nav-tabs-custom>.nav-tabs>li>a,.nav-tabs-custom>.nav-tabs>li>a:hover{background:transparent;margin:0}.nav-tabs-custom>.nav-tabs>li>a:hover{color:#999}.nav-tabs-custom>.nav-tabs>li:not(.active)>a:hover,.nav-tabs-custom>.nav-tabs>li:not(.active)>a:focus,.nav-tabs-custom>.nav-tabs>li:not(.active)>a:active{border-color:transparent}.nav-tabs-custom>.nav-tabs>li.active{border-top-color:#303f9f}.nav-tabs-custom>.nav-tabs>li.active>a,.nav-tabs-custom>.nav-tabs>li.active:hover>a{background-color:#fff;color:#444}.nav-tabs-custom>.nav-tabs>li.active>a{border-top-color:transparent;border-left-color:#f4f4f4;border-right-color:#f4f4f4}.nav-tabs-custom>.nav-tabs>li:first-of-type{margin-left:0}.nav-tabs-custom>.nav-tabs>li:first-of-type.active>a{border-left-color:transparent}.nav-tabs-custom>.nav-tabs.pull-right{float:none !important}.nav-tabs-custom>.nav-tabs.pull-right>li{float:right}.nav-tabs-custom>.nav-tabs.pull-right>li:first-of-type{margin-right:0}.nav-tabs-custom>.nav-tabs.pull-right>li:first-of-type>a{border-left-width:1px}.nav-tabs-custom>.nav-tabs.pull-right>li:first-of-type.active>a{border-left-color:#f4f4f4;border-right-color:transparent}.nav-tabs-custom>.nav-tabs>li.header{line-height:35px;padding:0 10px;font-size:20px;color:#444}.nav-tabs-custom>.nav-tabs>li.header>.fa,.nav-tabs-custom>.nav-tabs>li.header>.glyphicon,.nav-tabs-custom>.nav-tabs>li.header>.ion{margin-right:5px}.nav-tabs-custom>.tab-content{background:#fff;padding:10px;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.nav-tabs-custom .dropdown.open>a:active,.nav-tabs-custom .dropdown.open>a:focus{background:transparent;color:#999}.nav-tabs-custom.tab-primary>.nav-tabs>li.active{border-top-color:#303f9f}.nav-tabs-custom.tab-info>.nav-tabs>li.active{border-top-color:#00c0ef}.nav-tabs-custom.tab-danger>.nav-tabs>li.active{border-top-color:#dd4b39}.nav-tabs-custom.tab-warning>.nav-tabs>li.active{border-top-color:#f39c12}.nav-tabs-custom.tab-success>.nav-tabs>li.active{border-top-color:#00a65a}.nav-tabs-custom.tab-default>.nav-tabs>li.active{border-top-color:#d2d6de}.pagination>li>a{background:#fafafa;color:#666}.pagination.pagination-flat>li>a{border-radius:0 !important}.products-list{list-style:none;margin:0;padding:0}.products-list>.item{border-radius:3px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.1);box-shadow:0 1px 1px rgba(0,0,0,0.1);padding:10px 0;background:#fff}.products-list>.item:before,.products-list>.item:after{content:" ";display:table}.products-list>.item:after{clear:both}.products-list .product-img{float:left}.products-list .product-img img{width:50px;height:50px}.products-list .product-info{margin-left:60px}.products-list .product-title{font-weight:600}.products-list .product-description{display:block;color:#999;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.product-list-in-box>.item{-webkit-box-shadow:none;box-shadow:none;border-radius:0;border-bottom:1px solid #f4f4f4}.product-list-in-box>.item:last-of-type{border-bottom-width:0}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{border-top:1px solid #f4f4f4}.table>thead>tr>th{border-bottom:2px solid #f4f4f4}.table tr td .progress{margin-top:5px}.table-bordered{border:1px solid #f4f4f4}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #f4f4f4}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table.no-border,.table.no-border td,.table.no-border th{border:0}table.text-center,table.text-center td,table.text-center th{text-align:center}.table.align th{text-align:left}.table.align td{text-align:right}.label-default{background-color:#d2d6de;color:#444}.direct-chat .box-body{border-bottom-right-radius:0;border-bottom-left-radius:0;position:relative;overflow-x:hidden;padding:0}.direct-chat.chat-pane-open .direct-chat-contacts{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0)}.direct-chat-messages{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0);padding:10px;height:250px;overflow:auto}.direct-chat-msg,.direct-chat-text{display:block}.direct-chat-msg{margin-bottom:10px}.direct-chat-msg:before,.direct-chat-msg:after{content:" ";display:table}.direct-chat-msg:after{clear:both}.direct-chat-messages,.direct-chat-contacts{-webkit-transition:-webkit-transform .5s ease-in-out;-moz-transition:-moz-transform .5s ease-in-out;-o-transition:-o-transform .5s ease-in-out;transition:transform .5s ease-in-out}.direct-chat-text{border-radius:5px;position:relative;padding:5px 10px;background:#d2d6de;border:1px solid #d2d6de;margin:5px 0 0 50px;color:#444}.direct-chat-text:after,.direct-chat-text:before{position:absolute;right:100%;top:15px;border:solid transparent;border-right-color:#d2d6de;content:' ';height:0;width:0;pointer-events:none}.direct-chat-text:after{border-width:5px;margin-top:-5px}.direct-chat-text:before{border-width:6px;margin-top:-6px}.right .direct-chat-text{margin-right:50px;margin-left:0}.right .direct-chat-text:after,.right .direct-chat-text:before{right:auto;left:100%;border-right-color:transparent;border-left-color:#d2d6de}.direct-chat-img{border-radius:50%;float:left;width:40px;height:40px}.right .direct-chat-img{float:right}.direct-chat-info{display:block;margin-bottom:2px;font-size:12px}.direct-chat-name{font-weight:600}.direct-chat-timestamp{color:#999}.direct-chat-contacts-open .direct-chat-contacts{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0)}.direct-chat-contacts{-webkit-transform:translate(101%, 0);-ms-transform:translate(101%, 0);-o-transform:translate(101%, 0);transform:translate(101%, 0);position:absolute;top:0;bottom:0;height:250px;width:100%;background:#222d32;color:#fff;overflow:auto}.contacts-list>li{border-bottom:1px solid rgba(0,0,0,0.2);padding:10px;margin:0}.contacts-list>li:before,.contacts-list>li:after{content:" ";display:table}.contacts-list>li:after{clear:both}.contacts-list>li:last-of-type{border-bottom:none}.contacts-list-img{border-radius:50%;width:40px;float:left}.contacts-list-info{margin-left:45px;color:#fff}.contacts-list-name,.contacts-list-status{display:block}.contacts-list-name{font-weight:600}.contacts-list-status{font-size:12px}.contacts-list-date{color:#aaa;font-weight:normal}.contacts-list-msg{color:#999}.direct-chat-danger .right>.direct-chat-text{background:#dd4b39;border-color:#dd4b39;color:#fff}.direct-chat-danger .right>.direct-chat-text:after,.direct-chat-danger .right>.direct-chat-text:before{border-left-color:#dd4b39}.direct-chat-primary .right>.direct-chat-text{background:#303f9f;border-color:#303f9f;color:#fff}.direct-chat-primary .right>.direct-chat-text:after,.direct-chat-primary .right>.direct-chat-text:before{border-left-color:#303f9f}.direct-chat-warning .right>.direct-chat-text{background:#f39c12;border-color:#f39c12;color:#fff}.direct-chat-warning .right>.direct-chat-text:after,.direct-chat-warning .right>.direct-chat-text:before{border-left-color:#f39c12}.direct-chat-info .right>.direct-chat-text{background:#00c0ef;border-color:#00c0ef;color:#fff}.direct-chat-info .right>.direct-chat-text:after,.direct-chat-info .right>.direct-chat-text:before{border-left-color:#00c0ef}.direct-chat-success .right>.direct-chat-text{background:#00a65a;border-color:#00a65a;color:#fff}.direct-chat-success .right>.direct-chat-text:after,.direct-chat-success .right>.direct-chat-text:before{border-left-color:#00a65a}.users-list>li{width:25%;float:left;padding:10px;text-align:center}.users-list>li img{border-radius:50%;max-width:100%;height:auto}.users-list>li>a:hover,.users-list>li>a:hover .users-list-name{color:#999}.users-list-name,.users-list-date{display:block}.users-list-name{font-weight:600;color:#444;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.users-list-date{color:#999;font-size:12px}.carousel-control.left,.carousel-control.right{background-image:none}.carousel-control>.fa{font-size:40px;position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-20px}.modal{background:rgba(0,0,0,0.3)}.modal-content{border-radius:0;-webkit-box-shadow:0 2px 3px rgba(0,0,0,0.125);box-shadow:0 2px 3px rgba(0,0,0,0.125);border:0}@media (min-width:768px){.modal-content{-webkit-box-shadow:0 2px 3px rgba(0,0,0,0.125);box-shadow:0 2px 3px rgba(0,0,0,0.125)}}.modal-header{border-bottom-color:#f4f4f4}.modal-footer{border-top-color:#f4f4f4}.modal-primary .modal-header,.modal-primary .modal-footer{border-color:#242f78}.modal-warning .modal-header,.modal-warning .modal-footer{border-color:#c87f0a}.modal-info .modal-header,.modal-info .modal-footer{border-color:#0097bc}.modal-success .modal-header,.modal-success .modal-footer{border-color:#00733e}.modal-danger .modal-header,.modal-danger .modal-footer{border-color:#c23321}.box-widget{border:none;position:relative}.widget-user .widget-user-header{padding:20px;height:120px;border-top-right-radius:3px;border-top-left-radius:3px}.widget-user .widget-user-username{margin-top:0;margin-bottom:5px;font-size:25px;font-weight:300;text-shadow:0 1px 1px rgba(0,0,0,0.2)}.widget-user .widget-user-desc{margin-top:0}.widget-user .widget-user-image{position:absolute;top:65px;left:50%;margin-left:-45px}.widget-user .widget-user-image>img{width:90px;height:auto;border:3px solid #fff}.widget-user .box-footer{padding-top:30px}.widget-user-2 .widget-user-header{padding:20px;border-top-right-radius:3px;border-top-left-radius:3px}.widget-user-2 .widget-user-username{margin-top:5px;margin-bottom:5px;font-size:25px;font-weight:300}.widget-user-2 .widget-user-desc{margin-top:0}.widget-user-2 .widget-user-username,.widget-user-2 .widget-user-desc{margin-left:75px}.widget-user-2 .widget-user-image>img{width:65px;height:auto;float:left}.treeview-menu{display:none;list-style:none;padding:0;margin:0;padding-left:5px}.treeview-menu .treeview-menu{padding-left:20px}.treeview-menu>li{margin:0}.treeview-menu>li>a{padding:5px 5px 5px 15px;display:block;font-size:14px}.treeview-menu>li>a>.fa,.treeview-menu>li>a>.glyphicon,.treeview-menu>li>a>.ion{width:20px}.treeview-menu>li>a>.pull-right-container>.fa-angle-left,.treeview-menu>li>a>.pull-right-container>.fa-angle-down,.treeview-menu>li>a>.fa-angle-left,.treeview-menu>li>a>.fa-angle-down{width:auto}.mailbox-messages>.table{margin:0}.mailbox-controls{padding:5px}.mailbox-controls.with-border{border-bottom:1px solid #f4f4f4}.mailbox-read-info{border-bottom:1px solid #f4f4f4;padding:10px}.mailbox-read-info h3{font-size:20px;margin:0}.mailbox-read-info h5{margin:0;padding:5px 0 0 0}.mailbox-read-time{color:#999;font-size:13px}.mailbox-read-message{padding:10px}.mailbox-attachments li{float:left;width:200px;border:1px solid #eee;margin-bottom:10px;margin-right:10px}.mailbox-attachment-name{font-weight:bold;color:#666}.mailbox-attachment-icon,.mailbox-attachment-info,.mailbox-attachment-size{display:block}.mailbox-attachment-info{padding:10px;background:#f4f4f4}.mailbox-attachment-size{color:#999;font-size:12px}.mailbox-attachment-icon{text-align:center;font-size:65px;color:#666;padding:20px 10px}.mailbox-attachment-icon.has-img{padding:0}.mailbox-attachment-icon.has-img>img{max-width:100%;height:auto}.lockscreen{background:#d2d6de}.lockscreen-logo{font-size:35px;text-align:center;margin-bottom:25px;font-weight:300}.lockscreen-logo a{color:#444}.lockscreen-wrapper{max-width:400px;margin:0 auto;margin-top:10%}.lockscreen .lockscreen-name{text-align:center;font-weight:600}.lockscreen-item{border-radius:4px;padding:0;background:#fff;position:relative;margin:10px auto 30px auto;width:290px}.lockscreen-image{border-radius:50%;position:absolute;left:-10px;top:-25px;background:#fff;padding:5px;z-index:10}.lockscreen-image>img{border-radius:50%;width:70px;height:70px}.lockscreen-credentials{margin-left:70px}.lockscreen-credentials .form-control{border:0}.lockscreen-credentials .btn{background-color:#fff;border:0;padding:0 10px}.lockscreen-footer{margin-top:10px}.login-logo,.register-logo{font-size:35px;text-align:center;margin-bottom:25px;font-weight:300}.login-logo a,.register-logo a{color:#444}.login-page,.register-page{background:#d2d6de}.login-box,.register-box{width:360px;margin:7% auto}@media (max-width:768px){.login-box,.register-box{width:90%;margin-top:20px}}.login-box-body,.register-box-body{background:#fff;padding:20px;border-top:0;color:#666}.login-box-body .form-control-feedback,.register-box-body .form-control-feedback{color:#777}.login-box-msg,.register-box-msg{margin:0;text-align:center;padding:0 20px 20px 20px}.social-auth-links{margin:10px 0}.error-page{width:600px;margin:20px auto 0 auto}@media (max-width:991px){.error-page{width:100%}}.error-page>.headline{float:left;font-size:100px;font-weight:300}@media (max-width:991px){.error-page>.headline{float:none;text-align:center}}.error-page>.error-content{margin-left:190px;display:block}@media (max-width:991px){.error-page>.error-content{margin-left:0}}.error-page>.error-content>h3{font-weight:300;font-size:25px}@media (max-width:991px){.error-page>.error-content>h3{text-align:center}}.invoice{position:relative;background:#fff;border:1px solid #f4f4f4;padding:20px;margin:10px 25px}.invoice-title{margin-top:0}.profile-user-img{margin:0 auto;width:100px;padding:3px;border:3px solid #d2d6de}.profile-username{font-size:21px;margin-top:5px}.post{border-bottom:1px solid #d2d6de;margin-bottom:15px;padding-bottom:15px;color:#666}.post:last-of-type{border-bottom:0;margin-bottom:0;padding-bottom:0}.post .user-block{margin-bottom:15px}.btn-social{position:relative;padding-left:44px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.btn-social>:first-child{position:absolute;left:0;top:0;bottom:0;width:32px;line-height:34px;font-size:1.6em;text-align:center;border-right:1px solid rgba(0,0,0,0.2)}.btn-social.btn-lg{padding-left:61px}.btn-social.btn-lg>:first-child{line-height:45px;width:45px;font-size:1.8em}.btn-social.btn-sm{padding-left:38px}.btn-social.btn-sm>:first-child{line-height:28px;width:28px;font-size:1.4em}.btn-social.btn-xs{padding-left:30px}.btn-social.btn-xs>:first-child{line-height:20px;width:20px;font-size:1.2em}.btn-social-icon{position:relative;padding-left:44px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;height:34px;width:34px;padding:0}.btn-social-icon>:first-child{position:absolute;left:0;top:0;bottom:0;width:32px;line-height:34px;font-size:1.6em;text-align:center;border-right:1px solid rgba(0,0,0,0.2)}.btn-social-icon.btn-lg{padding-left:61px}.btn-social-icon.btn-lg>:first-child{line-height:45px;width:45px;font-size:1.8em}.btn-social-icon.btn-sm{padding-left:38px}.btn-social-icon.btn-sm>:first-child{line-height:28px;width:28px;font-size:1.4em}.btn-social-icon.btn-xs{padding-left:30px}.btn-social-icon.btn-xs>:first-child{line-height:20px;width:20px;font-size:1.2em}.btn-social-icon>:first-child{border:none;text-align:center;width:100%}.btn-social-icon.btn-lg{height:45px;width:45px;padding-left:0;padding-right:0}.btn-social-icon.btn-sm{height:30px;width:30px;padding-left:0;padding-right:0}.btn-social-icon.btn-xs{height:22px;width:22px;padding-left:0;padding-right:0}.btn-adn{color:#fff;background-color:#d87a68;border-color:rgba(0,0,0,0.2)}.btn-adn:focus,.btn-adn.focus{color:#fff;background-color:#ce563f;border-color:rgba(0,0,0,0.2)}.btn-adn:hover{color:#fff;background-color:#ce563f;border-color:rgba(0,0,0,0.2)}.btn-adn:active,.btn-adn.active,.open>.dropdown-toggle.btn-adn{color:#fff;background-color:#ce563f;border-color:rgba(0,0,0,0.2)}.btn-adn:active,.btn-adn.active,.open>.dropdown-toggle.btn-adn{background-image:none}.btn-adn .badge{color:#d87a68;background-color:#fff}.btn-bitbucket{color:#fff;background-color:#205081;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:focus,.btn-bitbucket.focus{color:#fff;background-color:#163758;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:hover{color:#fff;background-color:#163758;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:active,.btn-bitbucket.active,.open>.dropdown-toggle.btn-bitbucket{color:#fff;background-color:#163758;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:active,.btn-bitbucket.active,.open>.dropdown-toggle.btn-bitbucket{background-image:none}.btn-bitbucket .badge{color:#205081;background-color:#fff}.btn-dropbox{color:#fff;background-color:#1087dd;border-color:rgba(0,0,0,0.2)}.btn-dropbox:focus,.btn-dropbox.focus{color:#fff;background-color:#0d6aad;border-color:rgba(0,0,0,0.2)}.btn-dropbox:hover{color:#fff;background-color:#0d6aad;border-color:rgba(0,0,0,0.2)}.btn-dropbox:active,.btn-dropbox.active,.open>.dropdown-toggle.btn-dropbox{color:#fff;background-color:#0d6aad;border-color:rgba(0,0,0,0.2)}.btn-dropbox:active,.btn-dropbox.active,.open>.dropdown-toggle.btn-dropbox{background-image:none}.btn-dropbox .badge{color:#1087dd;background-color:#fff}.btn-facebook{color:#fff;background-color:#3b5998;border-color:rgba(0,0,0,0.2)}.btn-facebook:focus,.btn-facebook.focus{color:#fff;background-color:#2d4373;border-color:rgba(0,0,0,0.2)}.btn-facebook:hover{color:#fff;background-color:#2d4373;border-color:rgba(0,0,0,0.2)}.btn-facebook:active,.btn-facebook.active,.open>.dropdown-toggle.btn-facebook{color:#fff;background-color:#2d4373;border-color:rgba(0,0,0,0.2)}.btn-facebook:active,.btn-facebook.active,.open>.dropdown-toggle.btn-facebook{background-image:none}.btn-facebook .badge{color:#3b5998;background-color:#fff}.btn-flickr{color:#fff;background-color:#ff0084;border-color:rgba(0,0,0,0.2)}.btn-flickr:focus,.btn-flickr.focus{color:#fff;background-color:#cc006a;border-color:rgba(0,0,0,0.2)}.btn-flickr:hover{color:#fff;background-color:#cc006a;border-color:rgba(0,0,0,0.2)}.btn-flickr:active,.btn-flickr.active,.open>.dropdown-toggle.btn-flickr{color:#fff;background-color:#cc006a;border-color:rgba(0,0,0,0.2)}.btn-flickr:active,.btn-flickr.active,.open>.dropdown-toggle.btn-flickr{background-image:none}.btn-flickr .badge{color:#ff0084;background-color:#fff}.btn-foursquare{color:#fff;background-color:#f94877;border-color:rgba(0,0,0,0.2)}.btn-foursquare:focus,.btn-foursquare.focus{color:#fff;background-color:#f71752;border-color:rgba(0,0,0,0.2)}.btn-foursquare:hover{color:#fff;background-color:#f71752;border-color:rgba(0,0,0,0.2)}.btn-foursquare:active,.btn-foursquare.active,.open>.dropdown-toggle.btn-foursquare{color:#fff;background-color:#f71752;border-color:rgba(0,0,0,0.2)}.btn-foursquare:active,.btn-foursquare.active,.open>.dropdown-toggle.btn-foursquare{background-image:none}.btn-foursquare .badge{color:#f94877;background-color:#fff}.btn-github{color:#fff;background-color:#444;border-color:rgba(0,0,0,0.2)}.btn-github:focus,.btn-github.focus{color:#fff;background-color:#2b2b2b;border-color:rgba(0,0,0,0.2)}.btn-github:hover{color:#fff;background-color:#2b2b2b;border-color:rgba(0,0,0,0.2)}.btn-github:active,.btn-github.active,.open>.dropdown-toggle.btn-github{color:#fff;background-color:#2b2b2b;border-color:rgba(0,0,0,0.2)}.btn-github:active,.btn-github.active,.open>.dropdown-toggle.btn-github{background-image:none}.btn-github .badge{color:#444;background-color:#fff}.btn-google{color:#fff;background-color:#dd4b39;border-color:rgba(0,0,0,0.2)}.btn-google:focus,.btn-google.focus{color:#fff;background-color:#c23321;border-color:rgba(0,0,0,0.2)}.btn-google:hover{color:#fff;background-color:#c23321;border-color:rgba(0,0,0,0.2)}.btn-google:active,.btn-google.active,.open>.dropdown-toggle.btn-google{color:#fff;background-color:#c23321;border-color:rgba(0,0,0,0.2)}.btn-google:active,.btn-google.active,.open>.dropdown-toggle.btn-google{background-image:none}.btn-google .badge{color:#dd4b39;background-color:#fff}.btn-instagram{color:#fff;background-color:#3f729b;border-color:rgba(0,0,0,0.2)}.btn-instagram:focus,.btn-instagram.focus{color:#fff;background-color:#305777;border-color:rgba(0,0,0,0.2)}.btn-instagram:hover{color:#fff;background-color:#305777;border-color:rgba(0,0,0,0.2)}.btn-instagram:active,.btn-instagram.active,.open>.dropdown-toggle.btn-instagram{color:#fff;background-color:#305777;border-color:rgba(0,0,0,0.2)}.btn-instagram:active,.btn-instagram.active,.open>.dropdown-toggle.btn-instagram{background-image:none}.btn-instagram .badge{color:#3f729b;background-color:#fff}.btn-linkedin{color:#fff;background-color:#007bb6;border-color:rgba(0,0,0,0.2)}.btn-linkedin:focus,.btn-linkedin.focus{color:#fff;background-color:#005983;border-color:rgba(0,0,0,0.2)}.btn-linkedin:hover{color:#fff;background-color:#005983;border-color:rgba(0,0,0,0.2)}.btn-linkedin:active,.btn-linkedin.active,.open>.dropdown-toggle.btn-linkedin{color:#fff;background-color:#005983;border-color:rgba(0,0,0,0.2)}.btn-linkedin:active,.btn-linkedin.active,.open>.dropdown-toggle.btn-linkedin{background-image:none}.btn-linkedin .badge{color:#007bb6;background-color:#fff}.btn-microsoft{color:#fff;background-color:#2672ec;border-color:rgba(0,0,0,0.2)}.btn-microsoft:focus,.btn-microsoft.focus{color:#fff;background-color:#125acd;border-color:rgba(0,0,0,0.2)}.btn-microsoft:hover{color:#fff;background-color:#125acd;border-color:rgba(0,0,0,0.2)}.btn-microsoft:active,.btn-microsoft.active,.open>.dropdown-toggle.btn-microsoft{color:#fff;background-color:#125acd;border-color:rgba(0,0,0,0.2)}.btn-microsoft:active,.btn-microsoft.active,.open>.dropdown-toggle.btn-microsoft{background-image:none}.btn-microsoft .badge{color:#2672ec;background-color:#fff}.btn-openid{color:#fff;background-color:#f7931e;border-color:rgba(0,0,0,0.2)}.btn-openid:focus,.btn-openid.focus{color:#fff;background-color:#da7908;border-color:rgba(0,0,0,0.2)}.btn-openid:hover{color:#fff;background-color:#da7908;border-color:rgba(0,0,0,0.2)}.btn-openid:active,.btn-openid.active,.open>.dropdown-toggle.btn-openid{color:#fff;background-color:#da7908;border-color:rgba(0,0,0,0.2)}.btn-openid:active,.btn-openid.active,.open>.dropdown-toggle.btn-openid{background-image:none}.btn-openid .badge{color:#f7931e;background-color:#fff}.btn-pinterest{color:#fff;background-color:#cb2027;border-color:rgba(0,0,0,0.2)}.btn-pinterest:focus,.btn-pinterest.focus{color:#fff;background-color:#9f191f;border-color:rgba(0,0,0,0.2)}.btn-pinterest:hover{color:#fff;background-color:#9f191f;border-color:rgba(0,0,0,0.2)}.btn-pinterest:active,.btn-pinterest.active,.open>.dropdown-toggle.btn-pinterest{color:#fff;background-color:#9f191f;border-color:rgba(0,0,0,0.2)}.btn-pinterest:active,.btn-pinterest.active,.open>.dropdown-toggle.btn-pinterest{background-image:none}.btn-pinterest .badge{color:#cb2027;background-color:#fff}.btn-reddit{color:#000;background-color:#eff7ff;border-color:rgba(0,0,0,0.2)}.btn-reddit:focus,.btn-reddit.focus{color:#000;background-color:#bcddff;border-color:rgba(0,0,0,0.2)}.btn-reddit:hover{color:#000;background-color:#bcddff;border-color:rgba(0,0,0,0.2)}.btn-reddit:active,.btn-reddit.active,.open>.dropdown-toggle.btn-reddit{color:#000;background-color:#bcddff;border-color:rgba(0,0,0,0.2)}.btn-reddit:active,.btn-reddit.active,.open>.dropdown-toggle.btn-reddit{background-image:none}.btn-reddit .badge{color:#eff7ff;background-color:#000}.btn-soundcloud{color:#fff;background-color:#f50;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:focus,.btn-soundcloud.focus{color:#fff;background-color:#c40;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:hover{color:#fff;background-color:#c40;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:active,.btn-soundcloud.active,.open>.dropdown-toggle.btn-soundcloud{color:#fff;background-color:#c40;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:active,.btn-soundcloud.active,.open>.dropdown-toggle.btn-soundcloud{background-image:none}.btn-soundcloud .badge{color:#f50;background-color:#fff}.btn-tumblr{color:#fff;background-color:#2c4762;border-color:rgba(0,0,0,0.2)}.btn-tumblr:focus,.btn-tumblr.focus{color:#fff;background-color:#1c2d3f;border-color:rgba(0,0,0,0.2)}.btn-tumblr:hover{color:#fff;background-color:#1c2d3f;border-color:rgba(0,0,0,0.2)}.btn-tumblr:active,.btn-tumblr.active,.open>.dropdown-toggle.btn-tumblr{color:#fff;background-color:#1c2d3f;border-color:rgba(0,0,0,0.2)}.btn-tumblr:active,.btn-tumblr.active,.open>.dropdown-toggle.btn-tumblr{background-image:none}.btn-tumblr .badge{color:#2c4762;background-color:#fff}.btn-twitter{color:#fff;background-color:#55acee;border-color:rgba(0,0,0,0.2)}.btn-twitter:focus,.btn-twitter.focus{color:#fff;background-color:#2795e9;border-color:rgba(0,0,0,0.2)}.btn-twitter:hover{color:#fff;background-color:#2795e9;border-color:rgba(0,0,0,0.2)}.btn-twitter:active,.btn-twitter.active,.open>.dropdown-toggle.btn-twitter{color:#fff;background-color:#2795e9;border-color:rgba(0,0,0,0.2)}.btn-twitter:active,.btn-twitter.active,.open>.dropdown-toggle.btn-twitter{background-image:none}.btn-twitter .badge{color:#55acee;background-color:#fff}.btn-vimeo{color:#fff;background-color:#1ab7ea;border-color:rgba(0,0,0,0.2)}.btn-vimeo:focus,.btn-vimeo.focus{color:#fff;background-color:#1295bf;border-color:rgba(0,0,0,0.2)}.btn-vimeo:hover{color:#fff;background-color:#1295bf;border-color:rgba(0,0,0,0.2)}.btn-vimeo:active,.btn-vimeo.active,.open>.dropdown-toggle.btn-vimeo{color:#fff;background-color:#1295bf;border-color:rgba(0,0,0,0.2)}.btn-vimeo:active,.btn-vimeo.active,.open>.dropdown-toggle.btn-vimeo{background-image:none}.btn-vimeo .badge{color:#1ab7ea;background-color:#fff}.btn-vk{color:#fff;background-color:#587ea3;border-color:rgba(0,0,0,0.2)}.btn-vk:focus,.btn-vk.focus{color:#fff;background-color:#466482;border-color:rgba(0,0,0,0.2)}.btn-vk:hover{color:#fff;background-color:#466482;border-color:rgba(0,0,0,0.2)}.btn-vk:active,.btn-vk.active,.open>.dropdown-toggle.btn-vk{color:#fff;background-color:#466482;border-color:rgba(0,0,0,0.2)}.btn-vk:active,.btn-vk.active,.open>.dropdown-toggle.btn-vk{background-image:none}.btn-vk .badge{color:#587ea3;background-color:#fff}.btn-yahoo{color:#fff;background-color:#720e9e;border-color:rgba(0,0,0,0.2)}.btn-yahoo:focus,.btn-yahoo.focus{color:#fff;background-color:#500a6f;border-color:rgba(0,0,0,0.2)}.btn-yahoo:hover{color:#fff;background-color:#500a6f;border-color:rgba(0,0,0,0.2)}.btn-yahoo:active,.btn-yahoo.active,.open>.dropdown-toggle.btn-yahoo{color:#fff;background-color:#500a6f;border-color:rgba(0,0,0,0.2)}.btn-yahoo:active,.btn-yahoo.active,.open>.dropdown-toggle.btn-yahoo{background-image:none}.btn-yahoo .badge{color:#720e9e;background-color:#fff}.fc-button{background:#f4f4f4;background-image:none;color:#444;border-color:#ddd;border-bottom-color:#ddd}.fc-button:hover,.fc-button:active,.fc-button.hover{background-color:#e9e9e9}.fc-header-title h2{font-size:15px;line-height:1.6em;color:#666;margin-left:10px}.fc-header-right{padding-right:10px}.fc-header-left{padding-left:10px}.fc-widget-header{background:#fafafa}.fc-grid{width:100%;border:0}.fc-widget-header:first-of-type,.fc-widget-content:first-of-type{border-left:0;border-right:0}.fc-widget-header:last-of-type,.fc-widget-content:last-of-type{border-right:0}.fc-toolbar{padding:10px;margin:0}.fc-day-number{font-size:20px;font-weight:300;padding-right:10px}.fc-color-picker{list-style:none;margin:0;padding:0}.fc-color-picker>li{float:left;font-size:30px;margin-right:5px;line-height:30px}.fc-color-picker>li .fa{-webkit-transition:-webkit-transform linear .3s;-moz-transition:-moz-transform linear .3s;-o-transition:-o-transform linear .3s;transition:transform linear .3s}.fc-color-picker>li .fa:hover{-webkit-transform:rotate(30deg);-ms-transform:rotate(30deg);-o-transform:rotate(30deg);transform:rotate(30deg)}#add-new-event{-webkit-transition:all linear .3s;-o-transition:all linear .3s;transition:all linear .3s}.external-event{padding:5px 10px;font-weight:bold;margin-bottom:4px;box-shadow:0 1px 1px rgba(0,0,0,0.1);text-shadow:0 1px 1px rgba(0,0,0,0.1);border-radius:3px;cursor:move}.external-event:hover{box-shadow:inset 0 0 90px rgba(0,0,0,0.2)}.select2-container--default.select2-container--focus,.select2-selection.select2-container--focus,.select2-container--default:focus,.select2-selection:focus,.select2-container--default:active,.select2-selection:active{outline:none}.select2-container--default .select2-selection--single,.select2-selection .select2-selection--single{border:1px solid #d2d6de;border-radius:0;padding:6px 12px;height:34px}.select2-container--default.select2-container--open{border-color:#303f9f}.select2-dropdown{border:1px solid #d2d6de;border-radius:0}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#303f9f;color:white}.select2-results__option{padding:6px 12px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{padding-left:0;padding-right:0;height:auto;margin-top:-4px}.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered{padding-right:6px;padding-left:20px}.select2-container--default .select2-selection--single .select2-selection__arrow{height:28px;right:3px}.select2-container--default .select2-selection--single .select2-selection__arrow b{margin-top:0}.select2-dropdown .select2-search__field,.select2-search--inline .select2-search__field{border:1px solid #d2d6de}.select2-dropdown .select2-search__field:focus,.select2-search--inline .select2-search__field:focus{outline:none}.select2-container--default.select2-container--focus .select2-selection--multiple,.select2-container--default .select2-search--dropdown .select2-search__field{border-color:#303f9f !important}.select2-container--default .select2-results__option[aria-disabled=true]{color:#999}.select2-container--default .select2-results__option[aria-selected=true]{background-color:#ddd}.select2-container--default .select2-results__option[aria-selected=true],.select2-container--default .select2-results__option[aria-selected=true]:hover{color:#444}.select2-container--default .select2-selection--multiple{border:1px solid #d2d6de;border-radius:0}.select2-container--default .select2-selection--multiple:focus{border-color:#303f9f}.select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#d2d6de}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#303f9f;border-color:#2a378b;padding:1px 10px;color:#fff}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{margin-right:5px;color:rgba(255,255,255,0.7)}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container .select2-selection--single .select2-selection__rendered{padding-right:10px}.box .datepicker-inline,.box .datepicker-inline .datepicker-days,.box .datepicker-inline>table,.box .datepicker-inline .datepicker-days>table{width:100%}.box .datepicker-inline td:hover,.box .datepicker-inline .datepicker-days td:hover,.box .datepicker-inline>table td:hover,.box .datepicker-inline .datepicker-days>table td:hover{background-color:rgba(255,255,255,0.3)}.box .datepicker-inline td.day.old,.box .datepicker-inline .datepicker-days td.day.old,.box .datepicker-inline>table td.day.old,.box .datepicker-inline .datepicker-days>table td.day.old,.box .datepicker-inline td.day.new,.box .datepicker-inline .datepicker-days td.day.new,.box .datepicker-inline>table td.day.new,.box .datepicker-inline .datepicker-days>table td.day.new{color:#777}.pad{padding:10px}.margin{margin:10px}.margin-bottom{margin-bottom:20px}.margin-bottom-none{margin-bottom:0}.margin-r-5{margin-right:5px}.inline{display:inline}.description-block{display:block;margin:10px 0;text-align:center}.description-block.margin-bottom{margin-bottom:25px}.description-block>.description-header{margin:0;padding:0;font-weight:600;font-size:16px}.description-block>.description-text{text-transform:uppercase}.bg-red,.bg-yellow,.bg-aqua,.bg-blue,.bg-light-blue,.bg-green,.bg-navy,.bg-teal,.bg-olive,.bg-lime,.bg-orange,.bg-fuchsia,.bg-purple,.bg-maroon,.bg-black,.bg-red-active,.bg-yellow-active,.bg-aqua-active,.bg-blue-active,.bg-light-blue-active,.bg-green-active,.bg-navy-active,.bg-teal-active,.bg-olive-active,.bg-lime-active,.bg-orange-active,.bg-fuchsia-active,.bg-purple-active,.bg-maroon-active,.bg-black-active,.callout.callout-danger,.callout.callout-warning,.callout.callout-info,.callout.callout-success,.alert-success,.alert-danger,.alert-error,.alert-warning,.alert-info,.label-danger,.label-info,.label-warning,.label-primary,.label-success,.modal-primary .modal-body,.modal-primary .modal-header,.modal-primary .modal-footer,.modal-warning .modal-body,.modal-warning .modal-header,.modal-warning .modal-footer,.modal-info .modal-body,.modal-info .modal-header,.modal-info .modal-footer,.modal-success .modal-body,.modal-success .modal-header,.modal-success .modal-footer,.modal-danger .modal-body,.modal-danger .modal-header,.modal-danger .modal-footer{color:#fff !important}.bg-gray{color:#000;background-color:#d2d6de !important}.bg-gray-light{background-color:#f7f7f7}.bg-black{background-color:#111 !important}.bg-red,.callout.callout-danger,.alert-danger,.alert-error,.label-danger,.modal-danger .modal-body{background-color:#dd4b39 !important}.bg-yellow,.callout.callout-warning,.alert-warning,.label-warning,.modal-warning .modal-body{background-color:#f39c12 !important}.bg-aqua,.callout.callout-info,.alert-info,.label-info,.modal-info .modal-body{background-color:#00c0ef !important}.bg-blue{background-color:#303f9f !important}.bg-light-blue,.label-primary,.modal-primary .modal-body{background-color:#303f9f !important}.bg-green,.callout.callout-success,.alert-success,.label-success,.modal-success .modal-body{background-color:#00a65a !important}.bg-navy{background-color:#001f3f !important}.bg-teal{background-color:#39cccc !important}.bg-olive{background-color:#3d9970 !important}.bg-lime{background-color:#01ff70 !important}.bg-orange{background-color:#ff851b !important}.bg-fuchsia{background-color:#f012be !important}.bg-purple{background-color:#605ca8 !important}.bg-maroon{background-color:#d81b60 !important}.bg-gray-active{color:#000;background-color:#b5bbc8 !important}.bg-black-active{background-color:#000 !important}.bg-red-active,.modal-danger .modal-header,.modal-danger .modal-footer{background-color:#d33724 !important}.bg-yellow-active,.modal-warning .modal-header,.modal-warning .modal-footer{background-color:#db8b0b !important}.bg-aqua-active,.modal-info .modal-header,.modal-info .modal-footer{background-color:#00a7d0 !important}.bg-blue-active{background-color:#242f78 !important}.bg-light-blue-active,.modal-primary .modal-header,.modal-primary .modal-footer{background-color:#293687 !important}.bg-green-active,.modal-success .modal-header,.modal-success .modal-footer{background-color:#008d4c !important}.bg-navy-active{background-color:#001a35 !important}.bg-teal-active{background-color:#30bbbb !important}.bg-olive-active{background-color:#368763 !important}.bg-lime-active{background-color:#00e765 !important}.bg-orange-active{background-color:#ff7701 !important}.bg-fuchsia-active{background-color:#db0ead !important}.bg-purple-active{background-color:#555299 !important}.bg-maroon-active{background-color:#ca195a !important}[class^="bg-"].disabled{opacity:.65;filter:alpha(opacity=65)}.text-red{color:#dd4b39 !important}.text-yellow{color:#f39c12 !important}.text-aqua{color:#00c0ef !important}.text-blue{color:#303f9f !important}.text-black{color:#111 !important}.text-light-blue{color:#303f9f !important}.text-green{color:#00a65a !important}.text-gray{color:#d2d6de !important}.text-navy{color:#001f3f !important}.text-teal{color:#39cccc !important}.text-olive{color:#3d9970 !important}.text-lime{color:#01ff70 !important}.text-orange{color:#ff851b !important}.text-fuchsia{color:#f012be !important}.text-purple{color:#605ca8 !important}.text-maroon{color:#d81b60 !important}.link-muted{color:#7a869d}.link-muted:hover,.link-muted:focus{color:#606c84}.link-black{color:#666}.link-black:hover,.link-black:focus{color:#999}.hide{display:none !important}.no-border{border:0 !important}.no-padding{padding:0 !important}.no-margin{margin:0 !important}.no-shadow{box-shadow:none !important}.list-unstyled,.chart-legend,.contacts-list,.users-list,.mailbox-attachments{list-style:none;margin:0;padding:0}.list-group-unbordered>.list-group-item{border-left:0;border-right:0;border-radius:0;padding-left:0;padding-right:0}.flat{border-radius:0 !important}.text-bold,.text-bold.table td,.text-bold.table th{font-weight:700}.text-sm{font-size:12px}.jqstooltip{padding:5px !important;width:auto !important;height:auto !important}.bg-teal-gradient{background:#39cccc !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #39cccc), color-stop(1, #7adddd)) !important;background:-ms-linear-gradient(bottom, #39cccc, #7adddd) !important;background:-moz-linear-gradient(center bottom, #39cccc 0, #7adddd 100%) !important;background:-o-linear-gradient(#7adddd, #39cccc) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#7adddd', endColorstr='#39cccc', GradientType=0) !important;color:#fff}.bg-light-blue-gradient{background:#303f9f !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #303f9f), color-stop(1, #4557c7)) !important;background:-ms-linear-gradient(bottom, #303f9f, #4557c7) !important;background:-moz-linear-gradient(center bottom, #303f9f 0, #4557c7 100%) !important;background:-o-linear-gradient(#4557c7, #303f9f) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#4557c7', endColorstr='#303f9f', GradientType=0) !important;color:#fff}.bg-blue-gradient{background:#303f9f !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #303f9f), color-stop(1, #384aba)) !important;background:-ms-linear-gradient(bottom, #303f9f, #384aba) !important;background:-moz-linear-gradient(center bottom, #303f9f 0, #384aba 100%) !important;background:-o-linear-gradient(#384aba, #303f9f) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#384aba', endColorstr='#303f9f', GradientType=0) !important;color:#fff}.bg-aqua-gradient{background:#00c0ef !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #00c0ef), color-stop(1, #14d1ff)) !important;background:-ms-linear-gradient(bottom, #00c0ef, #14d1ff) !important;background:-moz-linear-gradient(center bottom, #00c0ef 0, #14d1ff 100%) !important;background:-o-linear-gradient(#14d1ff, #00c0ef) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#14d1ff', endColorstr='#00c0ef', GradientType=0) !important;color:#fff}.bg-yellow-gradient{background:#f39c12 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #f39c12), color-stop(1, #f7bc60)) !important;background:-ms-linear-gradient(bottom, #f39c12, #f7bc60) !important;background:-moz-linear-gradient(center bottom, #f39c12 0, #f7bc60 100%) !important;background:-o-linear-gradient(#f7bc60, #f39c12) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f7bc60', endColorstr='#f39c12', GradientType=0) !important;color:#fff}.bg-purple-gradient{background:#605ca8 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #605ca8), color-stop(1, #9491c4)) !important;background:-ms-linear-gradient(bottom, #605ca8, #9491c4) !important;background:-moz-linear-gradient(center bottom, #605ca8 0, #9491c4 100%) !important;background:-o-linear-gradient(#9491c4, #605ca8) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#9491c4', endColorstr='#605ca8', GradientType=0) !important;color:#fff}.bg-green-gradient{background:#00a65a !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #00a65a), color-stop(1, #00ca6d)) !important;background:-ms-linear-gradient(bottom, #00a65a, #00ca6d) !important;background:-moz-linear-gradient(center bottom, #00a65a 0, #00ca6d 100%) !important;background:-o-linear-gradient(#00ca6d, #00a65a) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ca6d', endColorstr='#00a65a', GradientType=0) !important;color:#fff}.bg-red-gradient{background:#dd4b39 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #dd4b39), color-stop(1, #e47365)) !important;background:-ms-linear-gradient(bottom, #dd4b39, #e47365) !important;background:-moz-linear-gradient(center bottom, #dd4b39 0, #e47365 100%) !important;background:-o-linear-gradient(#e47365, #dd4b39) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#e47365', endColorstr='#dd4b39', GradientType=0) !important;color:#fff}.bg-black-gradient{background:#111 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #111), color-stop(1, #2b2b2b)) !important;background:-ms-linear-gradient(bottom, #111, #2b2b2b) !important;background:-moz-linear-gradient(center bottom, #111 0, #2b2b2b 100%) !important;background:-o-linear-gradient(#2b2b2b, #111) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#2b2b2b', endColorstr='#111111', GradientType=0) !important;color:#fff}.bg-maroon-gradient{background:#d81b60 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #d81b60), color-stop(1, #e73f7c)) !important;background:-ms-linear-gradient(bottom, #d81b60, #e73f7c) !important;background:-moz-linear-gradient(center bottom, #d81b60 0, #e73f7c 100%) !important;background:-o-linear-gradient(#e73f7c, #d81b60) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#e73f7c', endColorstr='#d81b60', GradientType=0) !important;color:#fff}.description-block .description-icon{font-size:16px}.no-pad-top{padding-top:0}.position-static{position:static !important}.list-header{font-size:15px;padding:10px 4px;font-weight:bold;color:#666}.list-seperator{height:1px;background:#f4f4f4;margin:15px 0 9px 0}.list-link>a{padding:4px;color:#777}.list-link>a:hover{color:#222}.font-light{font-weight:300}.user-block:before,.user-block:after{content:" ";display:table}.user-block:after{clear:both}.user-block img{width:40px;height:40px;float:left}.user-block .username,.user-block .description,.user-block .comment{display:block;margin-left:50px}.user-block .username{font-size:16px;font-weight:600}.user-block .description{color:#999;font-size:13px}.user-block.user-block-sm .username,.user-block.user-block-sm .description,.user-block.user-block-sm .comment{margin-left:40px}.user-block.user-block-sm .username{font-size:14px}.img-sm,.img-md,.img-lg,.box-comments .box-comment img,.user-block.user-block-sm img{float:left}.img-sm,.box-comments .box-comment img,.user-block.user-block-sm img{width:30px !important;height:30px !important}.img-sm+.img-push{margin-left:40px}.img-md{width:60px;height:60px}.img-md+.img-push{margin-left:70px}.img-lg{width:100px;height:100px}.img-lg+.img-push{margin-left:110px}.img-bordered{border:3px solid #d2d6de;padding:3px}.img-bordered-sm{border:2px solid #d2d6de;padding:2px}.attachment-block{border:1px solid #f4f4f4;padding:5px;margin-bottom:10px;background:#f7f7f7}.attachment-block .attachment-img{max-width:100px;max-height:100px;height:auto;float:left}.attachment-block .attachment-pushed{margin-left:110px}.attachment-block .attachment-heading{margin:0}.attachment-block .attachment-text{color:#555}.connectedSortable{min-height:100px}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sort-highlight{background:#f4f4f4;border:1px dashed #ddd;margin-bottom:10px}.full-opacity-hover{opacity:.65;filter:alpha(opacity=65)}.full-opacity-hover:hover{opacity:1;filter:alpha(opacity=100)}.chart{position:relative;overflow:hidden;width:100%}.chart svg,.chart canvas{width:100% !important}@media print{.no-print,.main-sidebar,.left-side,.main-header,.content-header{display:none !important}.content-wrapper,.right-side,.main-footer{margin-left:0 !important;min-height:0 !important;-webkit-transform:translate(0, 0) !important;-ms-transform:translate(0, 0) !important;-o-transform:translate(0, 0) !important;transform:translate(0, 0) !important}.fixed .content-wrapper,.fixed .right-side{padding-top:0 !important}.invoice{width:100%;border:0;margin:0;padding:0}.invoice-col{float:left;width:33.3333333%}.table-responsive{overflow:auto}.table-responsive>.table tr th,.table-responsive>.table tr td{white-space:normal !important}} + */html,body{height:100%}.layout-boxed html,.layout-boxed body{height:100%}body{font-family:'Source Sans Pro','Helvetica Neue',Helvetica,Arial,sans-serif;font-weight:400;overflow-x:hidden;overflow-y:auto}.wrapper{height:100%;position:relative;overflow-x:hidden;overflow-y:auto}.wrapper:before,.wrapper:after{content:" ";display:table}.wrapper:after{clear:both}.layout-boxed .wrapper{max-width:1250px;margin:0 auto;min-height:100%;box-shadow:0 0 8px rgba(0,0,0,0.5);position:relative}.layout-boxed{background:url('../img/boxed-bg.jpg') repeat fixed}.content-wrapper,.main-footer{-webkit-transition:-webkit-transform .3s ease-in-out,margin .3s ease-in-out;-moz-transition:-moz-transform .3s ease-in-out,margin .3s ease-in-out;-o-transition:-o-transform .3s ease-in-out,margin .3s ease-in-out;transition:transform .3s ease-in-out,margin .3s ease-in-out;margin-left:230px;z-index:820}.layout-top-nav .content-wrapper,.layout-top-nav .main-footer{margin-left:0}@media (max-width:767px){.content-wrapper,.main-footer{margin-left:0}}@media (min-width:768px){.sidebar-collapse .content-wrapper,.sidebar-collapse .main-footer{margin-left:0}}@media (max-width:767px){.sidebar-open .content-wrapper,.sidebar-open .main-footer{-webkit-transform:translate(230px, 0);-ms-transform:translate(230px, 0);-o-transform:translate(230px, 0);transform:translate(230px, 0)}}.content-wrapper{min-height:100%;background-color:#ecf0f5;z-index:800}.main-footer{background:#fff;padding:15px;color:#444;border-top:1px solid #eaecf1}.fixed .main-header,.fixed .main-sidebar,.fixed .left-side{position:fixed}.fixed .main-header{top:0;right:0;left:0}.fixed .content-wrapper,.fixed .right-side{padding-top:50px}@media (max-width:767px){.fixed .content-wrapper,.fixed .right-side{padding-top:100px}}.fixed.layout-boxed .wrapper{max-width:100%}.fixed .wrapper{overflow:hidden}.hold-transition .content-wrapper,.hold-transition .right-side,.hold-transition .main-footer,.hold-transition .main-sidebar,.hold-transition .left-side,.hold-transition .main-header .navbar,.hold-transition .main-header .logo,.hold-transition .menu-open .fa-angle-left{-webkit-transition:none;-o-transition:none;transition:none}.content{min-height:250px;padding:15px;margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:'Source Sans Pro',sans-serif}a{color:#10529f}a:hover,a:active,a:focus{outline:none;text-decoration:none;color:#1776e5}.page-header{margin:10px 0 20px 0;font-size:22px}.page-header>small{color:#666;display:block;margin-top:5px}.main-header{position:relative;max-height:100px;z-index:1030}.main-header .navbar{-webkit-transition:margin-left .3s ease-in-out;-o-transition:margin-left .3s ease-in-out;transition:margin-left .3s ease-in-out;margin-bottom:0;margin-left:230px;border:none;min-height:50px;border-radius:0}.layout-top-nav .main-header .navbar{margin-left:0}.main-header #navbar-search-input.form-control{background:rgba(255,255,255,0.2);border-color:transparent}.main-header #navbar-search-input.form-control:focus,.main-header #navbar-search-input.form-control:active{border-color:rgba(0,0,0,0.1);background:rgba(255,255,255,0.9)}.main-header #navbar-search-input.form-control::-moz-placeholder{color:#ccc;opacity:1}.main-header #navbar-search-input.form-control:-ms-input-placeholder{color:#ccc}.main-header #navbar-search-input.form-control::-webkit-input-placeholder{color:#ccc}.main-header .navbar-custom-menu,.main-header .navbar-right{float:right}@media (max-width:991px){.main-header .navbar-custom-menu a,.main-header .navbar-right a{color:inherit;background:transparent}}@media (max-width:767px){.main-header .navbar-right{float:none}.navbar-collapse .main-header .navbar-right{margin:7.5px -15px}.main-header .navbar-right>li{color:inherit;border:0}}.main-header .sidebar-toggle{float:left;background-color:transparent;background-image:none;padding:15px 15px;font-family:fontAwesome}.main-header .sidebar-toggle:before{content:"\f0c9"}.main-header .sidebar-toggle:hover{color:#fff}.main-header .sidebar-toggle:focus,.main-header .sidebar-toggle:active{background:transparent}.main-header .sidebar-toggle .icon-bar{display:none}.main-header .navbar .nav>li.user>a>.fa,.main-header .navbar .nav>li.user>a>.glyphicon,.main-header .navbar .nav>li.user>a>.ion{margin-right:5px}.main-header .navbar .nav>li>a>.label{position:absolute;top:9px;right:7px;text-align:center;font-size:9px;padding:2px 3px;line-height:.9}.main-header .logo{-webkit-transition:width .3s ease-in-out;-o-transition:width .3s ease-in-out;transition:width .3s ease-in-out;display:block;float:left;height:50px;font-size:20px;line-height:50px;text-align:center;width:230px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;padding:0 15px;font-weight:300;overflow:hidden}.main-header .logo .logo-lg{display:block}.main-header .logo .logo-mini{display:none}.main-header .navbar-brand{color:#fff}.content-header{position:relative;padding:15px 15px 0 15px}.content-header>h1{margin:0;font-size:24px}.content-header>h1>small{font-size:15px;display:inline-block;padding-left:4px;font-weight:300}.content-header>.breadcrumb{float:right;background:transparent;margin-top:0;margin-bottom:0;font-size:12px;padding:7px 5px;position:absolute;top:15px;right:10px;border-radius:2px}.content-header>.breadcrumb>li>a{color:#444;text-decoration:none;display:inline-block}.content-header>.breadcrumb>li>a>.fa,.content-header>.breadcrumb>li>a>.glyphicon,.content-header>.breadcrumb>li>a>.ion{margin-right:5px}.content-header>.breadcrumb>li+li:before{content:'>\00a0'}@media (max-width:991px){.content-header>.breadcrumb{position:relative;margin-top:5px;top:0;right:0;float:none;background:#eaecf1;padding-left:10px}.content-header>.breadcrumb li:before{color:#adb5c8}}.navbar-toggle{color:#fff;border:0;margin:0;padding:15px 15px}@media (max-width:991px){.navbar-custom-menu .navbar-nav>li{float:left}.navbar-custom-menu .navbar-nav{margin:0;float:left}.navbar-custom-menu .navbar-nav>li>a{padding-top:15px;padding-bottom:15px;line-height:20px}}@media (max-width:767px){.main-header{position:relative}.main-header .logo,.main-header .navbar{width:100%;float:none}.main-header .navbar{margin:0}.main-header .navbar-custom-menu{float:right}}@media (max-width:991px){.navbar-collapse.pull-left{float:none !important}.navbar-collapse.pull-left+.navbar-custom-menu{display:block;position:absolute;top:0;right:40px}}.main-sidebar{position:absolute;top:0;left:0;padding-top:50px;min-height:100%;width:230px;z-index:810;-webkit-transition:-webkit-transform .3s ease-in-out,width .3s ease-in-out;-moz-transition:-moz-transform .3s ease-in-out,width .3s ease-in-out;-o-transition:-o-transform .3s ease-in-out,width .3s ease-in-out;transition:transform .3s ease-in-out,width .3s ease-in-out}@media (max-width:767px){.main-sidebar{padding-top:100px}}@media (max-width:767px){.main-sidebar{-webkit-transform:translate(-230px, 0);-ms-transform:translate(-230px, 0);-o-transform:translate(-230px, 0);transform:translate(-230px, 0)}}@media (min-width:768px){.sidebar-collapse .main-sidebar{-webkit-transform:translate(-230px, 0);-ms-transform:translate(-230px, 0);-o-transform:translate(-230px, 0);transform:translate(-230px, 0)}}@media (max-width:767px){.sidebar-open .main-sidebar{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0)}}.sidebar{padding-bottom:10px}.sidebar-form input:focus{border-color:transparent}.user-panel{position:relative;width:100%;padding:10px;overflow:hidden}.user-panel:before,.user-panel:after{content:" ";display:table}.user-panel:after{clear:both}.user-panel>.image>img{width:100%;max-width:45px;height:auto}.user-panel>.info{padding:5px 5px 5px 15px;line-height:1;position:absolute;left:55px}.user-panel>.info>p{font-weight:600;margin-bottom:9px}.user-panel>.info>a{text-decoration:none;padding-right:5px;margin-top:3px;font-size:11px}.user-panel>.info>a>.fa,.user-panel>.info>a>.ion,.user-panel>.info>a>.glyphicon{margin-right:3px}.sidebar-menu{list-style:none;margin:0;padding:0}.sidebar-menu>li{position:relative;margin:0;padding:0}.sidebar-menu>li>a{padding:12px 5px 12px 15px;display:block}.sidebar-menu>li>a>.fa,.sidebar-menu>li>a>.glyphicon,.sidebar-menu>li>a>.ion{width:20px}.sidebar-menu>li .label,.sidebar-menu>li .badge{margin-right:5px}.sidebar-menu>li .badge{margin-top:3px}.sidebar-menu li.header{padding:10px 25px 10px 15px;font-size:12px}.sidebar-menu li>a>.fa-angle-left,.sidebar-menu li>a>.pull-right-container>.fa-angle-left{width:auto;height:auto;padding:0;margin-right:10px;-webkit-transition:transform .5s ease;-o-transition:transform .5s ease;transition:transform .5s ease}.sidebar-menu li>a>.fa-angle-left{position:absolute;top:50%;right:10px;margin-top:-8px}.sidebar-menu .menu-open>a>.fa-angle-left,.sidebar-menu .menu-open>a>.pull-right-container>.fa-angle-left{-webkit-transform:rotate(-90deg);-ms-transform:rotate(-90deg);-o-transform:rotate(-90deg);transform:rotate(-90deg)}.sidebar-menu .active>.treeview-menu{display:block}@media (min-width:768px){.sidebar-mini.sidebar-collapse .content-wrapper,.sidebar-mini.sidebar-collapse .right-side,.sidebar-mini.sidebar-collapse .main-footer{margin-left:50px !important;z-index:840}.sidebar-mini.sidebar-collapse .main-sidebar{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0);width:50px !important;z-index:850}.sidebar-mini.sidebar-collapse .sidebar-menu>li{position:relative}.sidebar-mini.sidebar-collapse .sidebar-menu>li>a{margin-right:0}.sidebar-mini.sidebar-collapse .sidebar-menu>li>a>span{border-top-right-radius:4px}.sidebar-mini.sidebar-collapse .sidebar-menu>li:not(.treeview)>a>span{border-bottom-right-radius:4px}.sidebar-mini.sidebar-collapse .sidebar-menu>li>.treeview-menu{padding-top:5px;padding-bottom:5px;border-bottom-right-radius:4px}.sidebar-mini.sidebar-collapse .main-sidebar .user-panel>.info,.sidebar-mini.sidebar-collapse .sidebar-form,.sidebar-mini.sidebar-collapse .sidebar-menu>li>a>span,.sidebar-mini.sidebar-collapse .sidebar-menu>li>.treeview-menu,.sidebar-mini.sidebar-collapse .sidebar-menu>li>a>.pull-right,.sidebar-mini.sidebar-collapse .sidebar-menu li.header{display:none !important;-webkit-transform:translateZ(0)}.sidebar-mini.sidebar-collapse .main-header .logo{width:50px}.sidebar-mini.sidebar-collapse .main-header .logo>.logo-mini{display:block;margin-left:-15px;margin-right:-15px;font-size:18px}.sidebar-mini.sidebar-collapse .main-header .logo>.logo-lg{display:none}.sidebar-mini.sidebar-collapse .main-header .navbar{margin-left:50px}}.sidebar-mini:not(.sidebar-mini-expand-feature).sidebar-collapse .sidebar-menu>li:hover>a>span:not(.pull-right),.sidebar-mini:not(.sidebar-mini-expand-feature).sidebar-collapse .sidebar-menu>li:hover>.treeview-menu{display:block !important;position:absolute;width:180px;left:50px}.sidebar-mini:not(.sidebar-mini-expand-feature).sidebar-collapse .sidebar-menu>li:hover>a>span{top:0;margin-left:-3px;padding:12px 5px 12px 20px;background-color:inherit}.sidebar-mini:not(.sidebar-mini-expand-feature).sidebar-collapse .sidebar-menu>li:hover>a>.pull-right-container{position:relative !important;float:right;width:auto !important;left:180px !important;top:-22px !important;z-index:900}.sidebar-mini:not(.sidebar-mini-expand-feature).sidebar-collapse .sidebar-menu>li:hover>a>.pull-right-container>.label:not(:first-of-type){display:none}.sidebar-mini:not(.sidebar-mini-expand-feature).sidebar-collapse .sidebar-menu>li:hover>.treeview-menu{top:44px;margin-left:0}.sidebar-expanded-on-hover .main-footer,.sidebar-expanded-on-hover .content-wrapper{margin-left:50px}.sidebar-expanded-on-hover .main-sidebar{box-shadow:3px 0 8px rgba(0,0,0,0.125)}.sidebar-menu,.main-sidebar .user-panel,.sidebar-menu>li.header{white-space:nowrap;overflow:hidden}.sidebar-menu:hover{overflow:visible}.sidebar-form,.sidebar-menu>li.header{overflow:hidden;text-overflow:clip}.sidebar-menu li>a{position:relative}.sidebar-menu li>a>.pull-right-container{position:absolute;right:10px;top:50%;margin-top:-7px}.control-sidebar-bg{position:fixed;z-index:1000;bottom:0}.control-sidebar-bg,.control-sidebar{top:0;right:-230px;width:230px;-webkit-transition:right .3s ease-in-out;-o-transition:right .3s ease-in-out;transition:right .3s ease-in-out}.control-sidebar{position:absolute;padding-top:50px;z-index:1010}@media (max-width:768px){.control-sidebar{padding-top:100px}}.control-sidebar>.tab-content{padding:10px 15px}.control-sidebar.control-sidebar-open,.control-sidebar.control-sidebar-open+.control-sidebar-bg{right:0}.control-sidebar-open .control-sidebar-bg,.control-sidebar-open .control-sidebar{right:0}@media (min-width:768px){.control-sidebar-open .content-wrapper,.control-sidebar-open .right-side,.control-sidebar-open .main-footer{margin-right:230px}}.fixed .control-sidebar{position:fixed;height:100%;overflow-y:auto;padding-bottom:50px}.nav-tabs.control-sidebar-tabs>li:first-of-type>a,.nav-tabs.control-sidebar-tabs>li:first-of-type>a:hover,.nav-tabs.control-sidebar-tabs>li:first-of-type>a:focus{border-left-width:0}.nav-tabs.control-sidebar-tabs>li>a{border-radius:0}.nav-tabs.control-sidebar-tabs>li>a,.nav-tabs.control-sidebar-tabs>li>a:hover{border-top:none;border-right:none;border-left:1px solid transparent;border-bottom:1px solid transparent}.nav-tabs.control-sidebar-tabs>li>a .icon{font-size:16px}.nav-tabs.control-sidebar-tabs>li.active>a,.nav-tabs.control-sidebar-tabs>li.active>a:hover,.nav-tabs.control-sidebar-tabs>li.active>a:focus,.nav-tabs.control-sidebar-tabs>li.active>a:active{border-top:none;border-right:none;border-bottom:none}@media (max-width:768px){.nav-tabs.control-sidebar-tabs{display:table}.nav-tabs.control-sidebar-tabs>li{display:table-cell}}.control-sidebar-heading{font-weight:400;font-size:16px;padding:10px 0;margin-bottom:10px}.control-sidebar-subheading{display:block;font-weight:400;font-size:14px}.control-sidebar-menu{list-style:none;padding:0;margin:0 -15px}.control-sidebar-menu>li>a{display:block;padding:10px 15px}.control-sidebar-menu>li>a:before,.control-sidebar-menu>li>a:after{content:" ";display:table}.control-sidebar-menu>li>a:after{clear:both}.control-sidebar-menu>li>a>.control-sidebar-subheading{margin-top:0}.control-sidebar-menu .menu-icon{float:left;width:35px;height:35px;border-radius:50%;text-align:center;line-height:35px}.control-sidebar-menu .menu-info{margin-left:45px;margin-top:3px}.control-sidebar-menu .menu-info>.control-sidebar-subheading{margin:0}.control-sidebar-menu .menu-info>p{margin:0;font-size:11px}.control-sidebar-menu .progress{margin:0}.control-sidebar-dark{color:#abb0c2}.control-sidebar-dark,.control-sidebar-dark+.control-sidebar-bg{background:#191b22}.control-sidebar-dark .nav-tabs.control-sidebar-tabs{border-bottom:#131419}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a{background:#0e0f13;color:#abb0c2}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:hover,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:focus{border-left-color:#0a0b0d;border-bottom-color:#0a0b0d}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:hover,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:focus,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:active{background:#131419}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li>a:hover{color:#fff}.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a:hover,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a:focus,.control-sidebar-dark .nav-tabs.control-sidebar-tabs>li.active>a:active{background:#191b22;color:#fff}.control-sidebar-dark .control-sidebar-heading,.control-sidebar-dark .control-sidebar-subheading{color:#fff}.control-sidebar-dark .control-sidebar-menu>li>a:hover{background:#15161c}.control-sidebar-dark .control-sidebar-menu>li>a .menu-info>p{color:#abb0c2}.control-sidebar-light{color:#5e5e5e}.control-sidebar-light,.control-sidebar-light+.control-sidebar-bg{background:#f9fafc;border-left:1px solid #eaecf1}.control-sidebar-light .nav-tabs.control-sidebar-tabs{border-bottom:#eaecf1}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a{background:#e8ecf4;color:#444}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:hover,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:focus{border-left-color:#eaecf1;border-bottom-color:#eaecf1}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:hover,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:focus,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li>a:active{background:#eff1f7}.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a:hover,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a:focus,.control-sidebar-light .nav-tabs.control-sidebar-tabs>li.active>a:active{background:#f9fafc;color:#111}.control-sidebar-light .control-sidebar-heading,.control-sidebar-light .control-sidebar-subheading{color:#111}.control-sidebar-light .control-sidebar-menu{margin-left:-14px}.control-sidebar-light .control-sidebar-menu>li>a:hover{background:#f4f4f5}.control-sidebar-light .control-sidebar-menu>li>a .menu-info>p{color:#5e5e5e}.dropdown-menu{box-shadow:none;border-color:#eee}.dropdown-menu>li>a{color:#777}.dropdown-menu>li>a>.glyphicon,.dropdown-menu>li>a>.fa,.dropdown-menu>li>a>.ion{margin-right:10px}.dropdown-menu>li>a:hover{background-color:#f9fafb;color:#333}.dropdown-menu>.divider{background-color:#eee}.navbar-nav>.notifications-menu>.dropdown-menu,.navbar-nav>.messages-menu>.dropdown-menu,.navbar-nav>.tasks-menu>.dropdown-menu{width:280px;padding:0 0 0 0;margin:0;top:100%}.navbar-nav>.notifications-menu>.dropdown-menu>li,.navbar-nav>.messages-menu>.dropdown-menu>li,.navbar-nav>.tasks-menu>.dropdown-menu>li{position:relative}.navbar-nav>.notifications-menu>.dropdown-menu>li.header,.navbar-nav>.messages-menu>.dropdown-menu>li.header,.navbar-nav>.tasks-menu>.dropdown-menu>li.header{border-top-left-radius:4px;border-top-right-radius:4px;border-bottom-right-radius:0;border-bottom-left-radius:0;background-color:#ffffff;padding:7px 10px;border-bottom:1px solid #f4f4f4;color:#444444;font-size:14px}.navbar-nav>.notifications-menu>.dropdown-menu>li.footer>a,.navbar-nav>.messages-menu>.dropdown-menu>li.footer>a,.navbar-nav>.tasks-menu>.dropdown-menu>li.footer>a{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:4px;border-bottom-left-radius:4px;font-size:12px;background-color:#fff;padding:7px 10px;border-bottom:1px solid #eeeeee;color:#444 !important;text-align:center}@media (max-width:991px){.navbar-nav>.notifications-menu>.dropdown-menu>li.footer>a,.navbar-nav>.messages-menu>.dropdown-menu>li.footer>a,.navbar-nav>.tasks-menu>.dropdown-menu>li.footer>a{background:#fff !important;color:#444 !important}}.navbar-nav>.notifications-menu>.dropdown-menu>li.footer>a:hover,.navbar-nav>.messages-menu>.dropdown-menu>li.footer>a:hover,.navbar-nav>.tasks-menu>.dropdown-menu>li.footer>a:hover{text-decoration:none;font-weight:normal}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu,.navbar-nav>.messages-menu>.dropdown-menu>li .menu,.navbar-nav>.tasks-menu>.dropdown-menu>li .menu{max-height:200px;margin:0;padding:0;list-style:none;overflow-x:hidden}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a,.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a,.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a{display:block;white-space:nowrap;border-bottom:1px solid #f4f4f4}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a:hover,.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:hover,.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a:hover{background:#f4f4f4;text-decoration:none}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a{color:#444444;overflow:hidden;text-overflow:ellipsis;padding:10px}.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a>.glyphicon,.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a>.fa,.navbar-nav>.notifications-menu>.dropdown-menu>li .menu>li>a>.ion{width:20px}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a{margin:0;padding:10px 10px}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>div>img{margin:auto 10px auto auto;width:40px;height:40px}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>h4{padding:0;margin:0 0 0 45px;color:#444444;font-size:15px;position:relative}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>h4>small{color:#999999;font-size:10px;position:absolute;top:0;right:0}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a>p{margin:0 0 0 45px;font-size:12px;color:#888888}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:before,.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:after{content:" ";display:table}.navbar-nav>.messages-menu>.dropdown-menu>li .menu>li>a:after{clear:both}.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a{padding:10px}.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a>h3{font-size:14px;padding:0;margin:0 0 10px 0;color:#666666}.navbar-nav>.tasks-menu>.dropdown-menu>li .menu>li>a>.progress{padding:0;margin:0}.navbar-nav>.user-menu>.dropdown-menu{border-top-right-radius:0;border-top-left-radius:0;padding:1px 0 0 0;border-top-width:0;width:280px}.navbar-nav>.user-menu>.dropdown-menu,.navbar-nav>.user-menu>.dropdown-menu>.user-body{border-bottom-right-radius:4px;border-bottom-left-radius:4px}.navbar-nav>.user-menu>.dropdown-menu>li.user-header{height:175px;padding:10px;text-align:center}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>img{z-index:5;height:90px;width:90px;border:3px solid;border-color:transparent;border-color:rgba(255,255,255,0.2)}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>p{z-index:5;color:#fff;color:rgba(255,255,255,0.8);font-size:17px;margin-top:10px}.navbar-nav>.user-menu>.dropdown-menu>li.user-header>p>small{display:block;font-size:12px}.navbar-nav>.user-menu>.dropdown-menu>.user-body{padding:15px;border-bottom:1px solid #f4f4f4;border-top:1px solid #dddddd}.navbar-nav>.user-menu>.dropdown-menu>.user-body:before,.navbar-nav>.user-menu>.dropdown-menu>.user-body:after{content:" ";display:table}.navbar-nav>.user-menu>.dropdown-menu>.user-body:after{clear:both}.navbar-nav>.user-menu>.dropdown-menu>.user-body a{color:#444 !important}@media (max-width:991px){.navbar-nav>.user-menu>.dropdown-menu>.user-body a{background:#fff !important;color:#444 !important}}.navbar-nav>.user-menu>.dropdown-menu>.user-footer{background-color:#f9f9f9;padding:10px}.navbar-nav>.user-menu>.dropdown-menu>.user-footer:before,.navbar-nav>.user-menu>.dropdown-menu>.user-footer:after{content:" ";display:table}.navbar-nav>.user-menu>.dropdown-menu>.user-footer:after{clear:both}.navbar-nav>.user-menu>.dropdown-menu>.user-footer .btn-default{color:#666666}@media (max-width:991px){.navbar-nav>.user-menu>.dropdown-menu>.user-footer .btn-default:hover{background-color:#f9f9f9}}.navbar-nav>.user-menu .user-image{float:left;width:25px;height:25px;border-radius:50%;margin-right:10px;margin-top:-2px}@media (max-width:767px){.navbar-nav>.user-menu .user-image{float:none;margin-right:0;margin-top:-8px;line-height:10px}}.open:not(.dropup)>.animated-dropdown-menu{backface-visibility:visible !important;-webkit-animation:flipInX .7s both;-o-animation:flipInX .7s both;animation:flipInX .7s both}@keyframes flipInX{0%{transform:perspective(400px) rotate3d(1, 0, 0, 90deg);transition-timing-function:ease-in;opacity:0}40%{transform:perspective(400px) rotate3d(1, 0, 0, -20deg);transition-timing-function:ease-in}60%{transform:perspective(400px) rotate3d(1, 0, 0, 10deg);opacity:1}80%{transform:perspective(400px) rotate3d(1, 0, 0, -5deg)}100%{transform:perspective(400px)}}@-webkit-keyframes flipInX{0%{-webkit-transform:perspective(400px) rotate3d(1, 0, 0, 90deg);-webkit-transition-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotate3d(1, 0, 0, -20deg);-webkit-transition-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotate3d(1, 0, 0, 10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotate3d(1, 0, 0, -5deg)}100%{-webkit-transform:perspective(400px)}}.navbar-custom-menu>.navbar-nav>li{position:relative}.navbar-custom-menu>.navbar-nav>li>.dropdown-menu{position:absolute;right:0;left:auto}@media (max-width:991px){.navbar-custom-menu>.navbar-nav{float:right}.navbar-custom-menu>.navbar-nav>li{position:static}.navbar-custom-menu>.navbar-nav>li>.dropdown-menu{position:absolute;right:5%;left:auto;border:1px solid #ddd;background:#fff}}.form-control{border-radius:0;box-shadow:none;border-color:#eaecf1}.form-control:focus{border-color:#10529f;box-shadow:none}.form-control::-moz-placeholder,.form-control:-ms-input-placeholder,.form-control::-webkit-input-placeholder{color:#bbb;opacity:1}.form-control:not(select){-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-group.has-success label{color:#00a65a}.form-group.has-success .form-control,.form-group.has-success .input-group-addon{border-color:#00a65a;box-shadow:none}.form-group.has-success .help-block{color:#00a65a}.form-group.has-warning label{color:#f39c12}.form-group.has-warning .form-control,.form-group.has-warning .input-group-addon{border-color:#f39c12;box-shadow:none}.form-group.has-warning .help-block{color:#f39c12}.form-group.has-error label{color:#dd4b39}.form-group.has-error .form-control,.form-group.has-error .input-group-addon{border-color:#dd4b39;box-shadow:none}.form-group.has-error .help-block{color:#dd4b39}.input-group .input-group-addon{border-radius:0;border-color:#eaecf1;background-color:#fff}.btn-group-vertical .btn.btn-flat:first-of-type,.btn-group-vertical .btn.btn-flat:last-of-type{border-radius:0}.icheck>label{padding-left:0}.form-control-feedback.fa{line-height:34px}.input-lg+.form-control-feedback.fa,.input-group-lg+.form-control-feedback.fa,.form-group-lg .form-control+.form-control-feedback.fa{line-height:46px}.input-sm+.form-control-feedback.fa,.input-group-sm+.form-control-feedback.fa,.form-group-sm .form-control+.form-control-feedback.fa{line-height:30px}.progress,.progress>.progress-bar{-webkit-box-shadow:none;box-shadow:none}.progress,.progress>.progress-bar,.progress .progress-bar,.progress>.progress-bar .progress-bar{border-radius:1px}.progress.sm,.progress-sm{height:10px}.progress.sm,.progress-sm,.progress.sm .progress-bar,.progress-sm .progress-bar{border-radius:1px}.progress.xs,.progress-xs{height:7px}.progress.xs,.progress-xs,.progress.xs .progress-bar,.progress-xs .progress-bar{border-radius:1px}.progress.xxs,.progress-xxs{height:3px}.progress.xxs,.progress-xxs,.progress.xxs .progress-bar,.progress-xxs .progress-bar{border-radius:1px}.progress.vertical{position:relative;width:30px;height:200px;display:inline-block;margin-right:10px}.progress.vertical>.progress-bar{width:100%;position:absolute;bottom:0}.progress.vertical.sm,.progress.vertical.progress-sm{width:20px}.progress.vertical.xs,.progress.vertical.progress-xs{width:10px}.progress.vertical.xxs,.progress.vertical.progress-xxs{width:3px}.progress-group .progress-text{font-weight:600}.progress-group .progress-number{float:right}.table tr>td .progress{margin:0}.progress-bar-light-blue,.progress-bar-primary{background-color:#10529f}.progress-striped .progress-bar-light-blue,.progress-striped .progress-bar-primary{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-green,.progress-bar-success{background-color:#00a65a}.progress-striped .progress-bar-green,.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-aqua,.progress-bar-info{background-color:#00c0ef}.progress-striped .progress-bar-aqua,.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-yellow,.progress-bar-warning{background-color:#f39c12}.progress-striped .progress-bar-yellow,.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-red,.progress-bar-danger{background-color:#dd4b39}.progress-striped .progress-bar-red,.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:-o-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.small-box{border-radius:2px;position:relative;display:block;margin-bottom:20px;box-shadow:0 1px 1px rgba(0,0,0,0.1)}.small-box>.inner{padding:10px}.small-box>.small-box-footer{position:relative;text-align:center;padding:3px 0;color:#fff;color:rgba(255,255,255,0.8);display:block;z-index:10;background:rgba(0,0,0,0.1);text-decoration:none}.small-box>.small-box-footer:hover{color:#fff;background:rgba(0,0,0,0.15)}.small-box h3{font-size:38px;font-weight:bold;margin:0 0 10px 0;white-space:nowrap;padding:0}.small-box p{font-size:15px}.small-box p>small{display:block;color:#f9f9f9;font-size:13px;margin-top:5px}.small-box h3,.small-box p{z-index:5}.small-box .icon{-webkit-transition:all .3s linear;-o-transition:all .3s linear;transition:all .3s linear;position:absolute;top:-10px;right:10px;z-index:0;font-size:90px;color:rgba(0,0,0,0.15)}.small-box:hover{text-decoration:none;color:#f9f9f9}.small-box:hover .icon{font-size:95px}@media (max-width:767px){.small-box{text-align:center}.small-box .icon{display:none}.small-box p{font-size:12px}}.box{position:relative;border-radius:3px;background:#ffffff;border-top:3px solid #d2d6de;margin-bottom:20px;width:100%;box-shadow:0 1px 1px rgba(0,0,0,0.1)}.box.box-primary{border-top-color:#10529f}.box.box-info{border-top-color:#00c0ef}.box.box-danger{border-top-color:#dd4b39}.box.box-warning{border-top-color:#f39c12}.box.box-success{border-top-color:#00a65a}.box.box-default{border-top-color:#eaecf1}.box.collapsed-box .box-body,.box.collapsed-box .box-footer{display:none}.box .nav-stacked>li{border-bottom:1px solid #f4f4f4;margin:0}.box .nav-stacked>li:last-of-type{border-bottom:none}.box.height-control .box-body{max-height:300px;overflow:auto}.box .border-right{border-right:1px solid #f4f4f4}.box .border-left{border-left:1px solid #f4f4f4}.box.box-solid{border-top:0}.box.box-solid>.box-header .btn.btn-default{background:transparent}.box.box-solid>.box-header .btn:hover,.box.box-solid>.box-header a:hover{background:rgba(0,0,0,0.1)}.box.box-solid.box-default{border:1px solid #eaecf1}.box.box-solid.box-default>.box-header{color:#444;background:#eaecf1;background-color:#eaecf1}.box.box-solid.box-default>.box-header a,.box.box-solid.box-default>.box-header .btn{color:#444}.box.box-solid.box-primary{border:1px solid #10529f}.box.box-solid.box-primary>.box-header{color:#fff;background:#10529f;background-color:#10529f}.box.box-solid.box-primary>.box-header a,.box.box-solid.box-primary>.box-header .btn{color:#fff}.box.box-solid.box-info{border:1px solid #00c0ef}.box.box-solid.box-info>.box-header{color:#fff;background:#00c0ef;background-color:#00c0ef}.box.box-solid.box-info>.box-header a,.box.box-solid.box-info>.box-header .btn{color:#fff}.box.box-solid.box-danger{border:1px solid #dd4b39}.box.box-solid.box-danger>.box-header{color:#fff;background:#dd4b39;background-color:#dd4b39}.box.box-solid.box-danger>.box-header a,.box.box-solid.box-danger>.box-header .btn{color:#fff}.box.box-solid.box-warning{border:1px solid #f39c12}.box.box-solid.box-warning>.box-header{color:#fff;background:#f39c12;background-color:#f39c12}.box.box-solid.box-warning>.box-header a,.box.box-solid.box-warning>.box-header .btn{color:#fff}.box.box-solid.box-success{border:1px solid #00a65a}.box.box-solid.box-success>.box-header{color:#fff;background:#00a65a;background-color:#00a65a}.box.box-solid.box-success>.box-header a,.box.box-solid.box-success>.box-header .btn{color:#fff}.box.box-solid>.box-header>.box-tools .btn{border:0;box-shadow:none}.box.box-solid[class*='bg']>.box-header{color:#fff}.box .box-group>.box{margin-bottom:5px}.box .knob-label{text-align:center;color:#333;font-weight:100;font-size:12px;margin-bottom:0.3em}.box>.overlay,.overlay-wrapper>.overlay,.box>.loading-img,.overlay-wrapper>.loading-img{position:absolute;top:0;left:0;width:100%;height:100%}.box .overlay,.overlay-wrapper .overlay{z-index:50;background:rgba(255,255,255,0.7);border-radius:3px}.box .overlay>.fa,.overlay-wrapper .overlay>.fa{position:absolute;top:50%;left:50%;margin-left:-15px;margin-top:-15px;color:#000;font-size:30px}.box .overlay.dark,.overlay-wrapper .overlay.dark{background:rgba(0,0,0,0.5)}.box-header:before,.box-body:before,.box-footer:before,.box-header:after,.box-body:after,.box-footer:after{content:" ";display:table}.box-header:after,.box-body:after,.box-footer:after{clear:both}.box-header{color:#444;display:block;padding:10px;position:relative}.box-header.with-border{border-bottom:1px solid #f4f4f4}.collapsed-box .box-header.with-border{border-bottom:none}.box-header>.fa,.box-header>.glyphicon,.box-header>.ion,.box-header .box-title{display:inline-block;font-size:18px;margin:0;line-height:1}.box-header>.fa,.box-header>.glyphicon,.box-header>.ion{margin-right:5px}.box-header>.box-tools{position:absolute;right:10px;top:5px}.box-header>.box-tools [data-toggle="tooltip"]{position:relative}.box-header>.box-tools.pull-right .dropdown-menu{right:0;left:auto}.box-header>.box-tools .dropdown-menu>li>a{color:#444!important}.btn-box-tool{padding:5px;font-size:12px;background:transparent;color:#97a0b3}.open .btn-box-tool,.btn-box-tool:hover{color:#606c84}.btn-box-tool.btn:active{box-shadow:none}.box-body{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px;padding:10px}.no-header .box-body{border-top-right-radius:3px;border-top-left-radius:3px}.box-body>.table{margin-bottom:0}.box-body .fc{margin-top:5px}.box-body .full-width-chart{margin:-19px}.box-body.no-padding .full-width-chart{margin:-9px}.box-body .box-pane{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:3px}.box-body .box-pane-right{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:3px;border-bottom-left-radius:0}.box-footer{border-top-left-radius:0;border-top-right-radius:0;border-bottom-right-radius:3px;border-bottom-left-radius:3px;border-top:1px solid #f4f4f4;padding:10px;background-color:#fff}.chart-legend{margin:10px 0}@media (max-width:991px){.chart-legend>li{float:left;margin-right:10px}}.box-comments{background:#f7f7f7}.box-comments .box-comment{padding:8px 0;border-bottom:1px solid #eee}.box-comments .box-comment:before,.box-comments .box-comment:after{content:" ";display:table}.box-comments .box-comment:after{clear:both}.box-comments .box-comment:last-of-type{border-bottom:0}.box-comments .box-comment:first-of-type{padding-top:0}.box-comments .box-comment img{float:left}.box-comments .comment-text{margin-left:40px;color:#555}.box-comments .username{color:#444;display:block;font-weight:600}.box-comments .text-muted{font-weight:400;font-size:12px}.todo-list{margin:0;padding:0;list-style:none;overflow:auto}.todo-list>li{border-radius:2px;padding:10px;background:#f4f4f4;margin-bottom:2px;border-left:2px solid #e6e7e8;color:#444}.todo-list>li:last-of-type{margin-bottom:0}.todo-list>li>input[type='checkbox']{margin:0 10px 0 5px}.todo-list>li .text{display:inline-block;margin-left:5px;font-weight:600}.todo-list>li .label{margin-left:10px;font-size:9px}.todo-list>li .tools{display:none;float:right;color:#dd4b39}.todo-list>li .tools>.fa,.todo-list>li .tools>.glyphicon,.todo-list>li .tools>.ion{margin-right:5px;cursor:pointer}.todo-list>li:hover .tools{display:inline-block}.todo-list>li.done{color:#999}.todo-list>li.done .text{text-decoration:line-through;font-weight:500}.todo-list>li.done .label{background:#eaecf1 !important}.todo-list .danger{border-left-color:#dd4b39}.todo-list .warning{border-left-color:#f39c12}.todo-list .info{border-left-color:#00c0ef}.todo-list .success{border-left-color:#00a65a}.todo-list .primary{border-left-color:#10529f}.todo-list .handle{display:inline-block;cursor:move;margin:0 5px}.chat{padding:5px 20px 5px 10px}.chat .item{margin-bottom:10px}.chat .item:before,.chat .item:after{content:" ";display:table}.chat .item:after{clear:both}.chat .item>img{width:40px;height:40px;border:2px solid transparent;border-radius:50%}.chat .item>.online{border:2px solid #00a65a}.chat .item>.offline{border:2px solid #dd4b39}.chat .item>.message{margin-left:55px;margin-top:-40px}.chat .item>.message>.name{display:block;font-weight:600}.chat .item>.attachment{border-radius:3px;background:#f4f4f4;margin-left:65px;margin-right:15px;padding:10px}.chat .item>.attachment>h4{margin:0 0 5px 0;font-weight:600;font-size:14px}.chat .item>.attachment>p,.chat .item>.attachment>.filename{font-weight:600;font-size:13px;font-style:italic;margin:0}.chat .item>.attachment:before,.chat .item>.attachment:after{content:" ";display:table}.chat .item>.attachment:after{clear:both}.box-input{max-width:200px}.modal .panel-body{color:#444}.info-box{display:block;min-height:90px;background:#fff;width:100%;box-shadow:0 1px 1px rgba(0,0,0,0.1);border-radius:2px;margin-bottom:15px}.info-box small{font-size:14px}.info-box .progress{background:rgba(0,0,0,0.2);margin:5px -10px 5px -10px;height:2px}.info-box .progress,.info-box .progress .progress-bar{border-radius:0}.info-box .progress .progress-bar{background:#fff}.info-box-icon{border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px;display:block;float:left;height:90px;width:90px;text-align:center;font-size:45px;line-height:90px;background:rgba(0,0,0,0.2)}.info-box-icon>img{max-width:100%}.info-box-content{padding:5px 10px;margin-left:90px}.info-box-number{display:block;font-weight:bold;font-size:18px}.progress-description,.info-box-text{display:block;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.info-box-text{text-transform:uppercase}.info-box-more{display:block}.progress-description{margin:0}.timeline{position:relative;margin:0 0 30px 0;padding:0;list-style:none}.timeline:before{content:'';position:absolute;top:0;bottom:0;width:4px;background:#ddd;left:31px;margin:0;border-radius:2px}.timeline>li{position:relative;margin-right:10px;margin-bottom:15px}.timeline>li:before,.timeline>li:after{content:" ";display:table}.timeline>li:after{clear:both}.timeline>li>.timeline-item{-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.1);box-shadow:0 1px 1px rgba(0,0,0,0.1);border-radius:3px;margin-top:0;background:#fff;color:#444;margin-left:60px;margin-right:15px;padding:0;position:relative}.timeline>li>.timeline-item>.time{color:#999;float:right;padding:10px;font-size:12px}.timeline>li>.timeline-item>.timeline-header{margin:0;color:#555;border-bottom:1px solid #f4f4f4;padding:10px;font-size:16px;line-height:1.1}.timeline>li>.timeline-item>.timeline-header>a{font-weight:600}.timeline>li>.timeline-item>.timeline-body,.timeline>li>.timeline-item>.timeline-footer{padding:10px}.timeline>li>.fa,.timeline>li>.glyphicon,.timeline>li>.ion{width:30px;height:30px;font-size:15px;line-height:30px;position:absolute;color:#666;background:#eaecf1;border-radius:50%;text-align:center;left:18px;top:0}.timeline>.time-label>span{font-weight:600;padding:5px;display:inline-block;background-color:#fff;border-radius:4px}.timeline-inverse>li>.timeline-item{background:#f0f0f0;border:1px solid #ddd;-webkit-box-shadow:none;box-shadow:none}.timeline-inverse>li>.timeline-item>.timeline-header{border-bottom-color:#ddd}.btn{border-radius:3px;-webkit-box-shadow:none;box-shadow:none;border:1px solid transparent}.btn.uppercase{text-transform:uppercase}.btn.btn-flat{border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;border-width:1px}.btn:active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);-moz-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn:focus{outline:none}.btn.btn-file{position:relative;overflow:hidden}.btn.btn-file>input[type='file']{position:absolute;top:0;right:0;min-width:100%;min-height:100%;font-size:100px;text-align:right;opacity:0;filter:alpha(opacity=0);outline:none;background:white;cursor:inherit;display:block}.btn-default{background-color:#f4f4f4;color:#444;border-color:#ddd}.btn-default:hover,.btn-default:active,.btn-default.hover{background-color:#e7e7e7}.btn-primary{background-color:#10529f;border-color:#0e4688}.btn-primary:hover,.btn-primary:active,.btn-primary.hover{background-color:#0e4688}.btn-success{background-color:#00a65a;border-color:#008d4c}.btn-success:hover,.btn-success:active,.btn-success.hover{background-color:#008d4c}.btn-info{background-color:#00c0ef;border-color:#00acd6}.btn-info:hover,.btn-info:active,.btn-info.hover{background-color:#00acd6}.btn-danger{background-color:#dd4b39;border-color:#d73925}.btn-danger:hover,.btn-danger:active,.btn-danger.hover{background-color:#d73925}.btn-warning{background-color:#f39c12;border-color:#e08e0b}.btn-warning:hover,.btn-warning:active,.btn-warning.hover{background-color:#e08e0b}.btn-outline{border:1px solid #fff;background:transparent;color:#fff}.btn-outline:hover,.btn-outline:focus,.btn-outline:active{color:rgba(255,255,255,0.7);border-color:rgba(255,255,255,0.7)}.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn[class*='bg-']:hover{-webkit-box-shadow:inset 0 0 100px rgba(0,0,0,0.2);box-shadow:inset 0 0 100px rgba(0,0,0,0.2)}.btn-app{border-radius:3px;position:relative;padding:15px 5px;margin:0 0 10px 10px;min-width:80px;height:60px;text-align:center;color:#666;border:1px solid #ddd;background-color:#f4f4f4;font-size:12px}.btn-app>.fa,.btn-app>.glyphicon,.btn-app>.ion{font-size:20px;display:block}.btn-app:hover{background:#f4f4f4;color:#444;border-color:#aaa}.btn-app:active,.btn-app:focus{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);-moz-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-app>.badge{position:absolute;top:-3px;right:-10px;font-size:10px;font-weight:400}.callout{border-radius:3px;margin:0 0 20px 0;padding:15px 30px 15px 15px;border-left:5px solid #eee}.callout a{color:#fff;text-decoration:underline}.callout a:hover{color:#eee}.callout h4{margin-top:0;font-weight:600}.callout p:last-child{margin-bottom:0}.callout code,.callout .highlight{background-color:#fff}.callout.callout-danger{border-color:#c23321}.callout.callout-warning{border-color:#c87f0a}.callout.callout-info{border-color:#0097bc}.callout.callout-success{border-color:#00733e}.alert{border-radius:3px}.alert h4{font-weight:600}.alert .icon{margin-right:10px}.alert .close{color:#000;opacity:.2;filter:alpha(opacity=20)}.alert .close:hover{opacity:.5;filter:alpha(opacity=50)}.alert a{color:#fff;text-decoration:underline}.alert-success{border-color:#008d4c}.alert-danger,.alert-error{border-color:#d73925}.alert-warning{border-color:#e08e0b}.alert-info{border-color:#00acd6}.nav>li>a:hover,.nav>li>a:active,.nav>li>a:focus{color:#444;background:#f7f7f7}.nav-pills>li>a{border-radius:0;border-top:3px solid transparent;color:#444}.nav-pills>li>a>.fa,.nav-pills>li>a>.glyphicon,.nav-pills>li>a>.ion{margin-right:5px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{border-top-color:#10529f}.nav-pills>li.active>a{font-weight:600}.nav-stacked>li>a{border-radius:0;border-top:0;border-left:3px solid transparent;color:#444}.nav-stacked>li.active>a,.nav-stacked>li.active>a:hover{background:transparent;color:#444;border-top:0;border-left-color:#10529f}.nav-stacked>li.header{border-bottom:1px solid #ddd;color:#777;margin-bottom:10px;padding:5px 10px;text-transform:uppercase}.nav-tabs-custom{margin-bottom:20px;background:#fff;box-shadow:0 1px 1px rgba(0,0,0,0.1);border-radius:3px}.nav-tabs-custom>.nav-tabs{margin:0;border-bottom-color:#f4f4f4;border-top-right-radius:3px;border-top-left-radius:3px}.nav-tabs-custom>.nav-tabs>li{border-top:3px solid transparent;margin-bottom:-2px;margin-right:5px}.nav-tabs-custom>.nav-tabs>li.disabled>a{color:#777}.nav-tabs-custom>.nav-tabs>li>a{color:#444;border-radius:0}.nav-tabs-custom>.nav-tabs>li>a.text-muted{color:#999}.nav-tabs-custom>.nav-tabs>li>a,.nav-tabs-custom>.nav-tabs>li>a:hover{background:transparent;margin:0}.nav-tabs-custom>.nav-tabs>li>a:hover{color:#999}.nav-tabs-custom>.nav-tabs>li:not(.active)>a:hover,.nav-tabs-custom>.nav-tabs>li:not(.active)>a:focus,.nav-tabs-custom>.nav-tabs>li:not(.active)>a:active{border-color:transparent}.nav-tabs-custom>.nav-tabs>li.active{border-top-color:#10529f}.nav-tabs-custom>.nav-tabs>li.active>a,.nav-tabs-custom>.nav-tabs>li.active:hover>a{background-color:#fff;color:#444}.nav-tabs-custom>.nav-tabs>li.active>a{border-top-color:transparent;border-left-color:#f4f4f4;border-right-color:#f4f4f4}.nav-tabs-custom>.nav-tabs>li:first-of-type{margin-left:0}.nav-tabs-custom>.nav-tabs>li:first-of-type.active>a{border-left-color:transparent}.nav-tabs-custom>.nav-tabs.pull-right{float:none !important}.nav-tabs-custom>.nav-tabs.pull-right>li{float:right}.nav-tabs-custom>.nav-tabs.pull-right>li:first-of-type{margin-right:0}.nav-tabs-custom>.nav-tabs.pull-right>li:first-of-type>a{border-left-width:1px}.nav-tabs-custom>.nav-tabs.pull-right>li:first-of-type.active>a{border-left-color:#f4f4f4;border-right-color:transparent}.nav-tabs-custom>.nav-tabs>li.header{line-height:35px;padding:0 10px;font-size:20px;color:#444}.nav-tabs-custom>.nav-tabs>li.header>.fa,.nav-tabs-custom>.nav-tabs>li.header>.glyphicon,.nav-tabs-custom>.nav-tabs>li.header>.ion{margin-right:5px}.nav-tabs-custom>.tab-content{background:#fff;padding:10px;border-bottom-right-radius:3px;border-bottom-left-radius:3px}.nav-tabs-custom .dropdown.open>a:active,.nav-tabs-custom .dropdown.open>a:focus{background:transparent;color:#999}.nav-tabs-custom.tab-primary>.nav-tabs>li.active{border-top-color:#10529f}.nav-tabs-custom.tab-info>.nav-tabs>li.active{border-top-color:#00c0ef}.nav-tabs-custom.tab-danger>.nav-tabs>li.active{border-top-color:#dd4b39}.nav-tabs-custom.tab-warning>.nav-tabs>li.active{border-top-color:#f39c12}.nav-tabs-custom.tab-success>.nav-tabs>li.active{border-top-color:#00a65a}.nav-tabs-custom.tab-default>.nav-tabs>li.active{border-top-color:#eaecf1}.pagination>li>a{background:#fafafa;color:#666}.pagination.pagination-flat>li>a{border-radius:0 !important}.products-list{list-style:none;margin:0;padding:0}.products-list>.item{border-radius:3px;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.1);box-shadow:0 1px 1px rgba(0,0,0,0.1);padding:10px 0;background:#fff}.products-list>.item:before,.products-list>.item:after{content:" ";display:table}.products-list>.item:after{clear:both}.products-list .product-img{float:left}.products-list .product-img img{width:50px;height:50px}.products-list .product-info{margin-left:60px}.products-list .product-title{font-weight:600}.products-list .product-description{display:block;color:#999;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.product-list-in-box>.item{-webkit-box-shadow:none;box-shadow:none;border-radius:0;border-bottom:1px solid #f4f4f4}.product-list-in-box>.item:last-of-type{border-bottom-width:0}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{border-top:1px solid #f4f4f4}.table>thead>tr>th{border-bottom:2px solid #f4f4f4}.table tr td .progress{margin-top:5px}.table-bordered{border:1px solid #f4f4f4}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #f4f4f4}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table.no-border,.table.no-border td,.table.no-border th{border:0}table.text-center,table.text-center td,table.text-center th{text-align:center}.table.align th{text-align:left}.table.align td{text-align:right}.label-default{background-color:#eaecf1;color:#444}.direct-chat .box-body{border-bottom-right-radius:0;border-bottom-left-radius:0;position:relative;overflow-x:hidden;padding:0}.direct-chat.chat-pane-open .direct-chat-contacts{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0)}.direct-chat-messages{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0);padding:10px;height:250px;overflow:auto}.direct-chat-msg,.direct-chat-text{display:block}.direct-chat-msg{margin-bottom:10px}.direct-chat-msg:before,.direct-chat-msg:after{content:" ";display:table}.direct-chat-msg:after{clear:both}.direct-chat-messages,.direct-chat-contacts{-webkit-transition:-webkit-transform .5s ease-in-out;-moz-transition:-moz-transform .5s ease-in-out;-o-transition:-o-transform .5s ease-in-out;transition:transform .5s ease-in-out}.direct-chat-text{border-radius:5px;position:relative;padding:5px 10px;background:#eaecf1;border:1px solid #eaecf1;margin:5px 0 0 50px;color:#444}.direct-chat-text:after,.direct-chat-text:before{position:absolute;right:100%;top:15px;border:solid transparent;border-right-color:#eaecf1;content:' ';height:0;width:0;pointer-events:none}.direct-chat-text:after{border-width:5px;margin-top:-5px}.direct-chat-text:before{border-width:6px;margin-top:-6px}.right .direct-chat-text{margin-right:50px;margin-left:0}.right .direct-chat-text:after,.right .direct-chat-text:before{right:auto;left:100%;border-right-color:transparent;border-left-color:#eaecf1}.direct-chat-img{border-radius:50%;float:left;width:40px;height:40px}.right .direct-chat-img{float:right}.direct-chat-info{display:block;margin-bottom:2px;font-size:12px}.direct-chat-name{font-weight:600}.direct-chat-timestamp{color:#999}.direct-chat-contacts-open .direct-chat-contacts{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);-o-transform:translate(0, 0);transform:translate(0, 0)}.direct-chat-contacts{-webkit-transform:translate(101%, 0);-ms-transform:translate(101%, 0);-o-transform:translate(101%, 0);transform:translate(101%, 0);position:absolute;top:0;bottom:0;height:250px;width:100%;background:#222d32;color:#fff;overflow:auto}.contacts-list>li{border-bottom:1px solid rgba(0,0,0,0.2);padding:10px;margin:0}.contacts-list>li:before,.contacts-list>li:after{content:" ";display:table}.contacts-list>li:after{clear:both}.contacts-list>li:last-of-type{border-bottom:none}.contacts-list-img{border-radius:50%;width:40px;float:left}.contacts-list-info{margin-left:45px;color:#fff}.contacts-list-name,.contacts-list-status{display:block}.contacts-list-name{font-weight:600}.contacts-list-status{font-size:12px}.contacts-list-date{color:#aaa;font-weight:normal}.contacts-list-msg{color:#999}.direct-chat-danger .right>.direct-chat-text{background:#dd4b39;border-color:#dd4b39;color:#fff}.direct-chat-danger .right>.direct-chat-text:after,.direct-chat-danger .right>.direct-chat-text:before{border-left-color:#dd4b39}.direct-chat-primary .right>.direct-chat-text{background:#10529f;border-color:#10529f;color:#fff}.direct-chat-primary .right>.direct-chat-text:after,.direct-chat-primary .right>.direct-chat-text:before{border-left-color:#10529f}.direct-chat-warning .right>.direct-chat-text{background:#f39c12;border-color:#f39c12;color:#fff}.direct-chat-warning .right>.direct-chat-text:after,.direct-chat-warning .right>.direct-chat-text:before{border-left-color:#f39c12}.direct-chat-info .right>.direct-chat-text{background:#00c0ef;border-color:#00c0ef;color:#fff}.direct-chat-info .right>.direct-chat-text:after,.direct-chat-info .right>.direct-chat-text:before{border-left-color:#00c0ef}.direct-chat-success .right>.direct-chat-text{background:#00a65a;border-color:#00a65a;color:#fff}.direct-chat-success .right>.direct-chat-text:after,.direct-chat-success .right>.direct-chat-text:before{border-left-color:#00a65a}.users-list>li{width:25%;float:left;padding:10px;text-align:center}.users-list>li img{border-radius:50%;max-width:100%;height:auto}.users-list>li>a:hover,.users-list>li>a:hover .users-list-name{color:#999}.users-list-name,.users-list-date{display:block}.users-list-name{font-weight:600;color:#444;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.users-list-date{color:#999;font-size:12px}.carousel-control.left,.carousel-control.right{background-image:none}.carousel-control>.fa{font-size:40px;position:absolute;top:50%;z-index:5;display:inline-block;margin-top:-20px}.modal{background:rgba(0,0,0,0.3)}.modal-content{border-radius:0;-webkit-box-shadow:0 2px 3px rgba(0,0,0,0.125);box-shadow:0 2px 3px rgba(0,0,0,0.125);border:0}@media (min-width:768px){.modal-content{-webkit-box-shadow:0 2px 3px rgba(0,0,0,0.125);box-shadow:0 2px 3px rgba(0,0,0,0.125)}}.modal-header{border-bottom-color:#f4f4f4}.modal-footer{border-top-color:#f4f4f4}.modal-primary .modal-header,.modal-primary .modal-footer{border-color:#0b3a71}.modal-warning .modal-header,.modal-warning .modal-footer{border-color:#c87f0a}.modal-info .modal-header,.modal-info .modal-footer{border-color:#0097bc}.modal-success .modal-header,.modal-success .modal-footer{border-color:#00733e}.modal-danger .modal-header,.modal-danger .modal-footer{border-color:#c23321}.box-widget{border:none;position:relative}.widget-user .widget-user-header{padding:20px;height:120px;border-top-right-radius:3px;border-top-left-radius:3px}.widget-user .widget-user-username{margin-top:0;margin-bottom:5px;font-size:25px;font-weight:300;text-shadow:0 1px 1px rgba(0,0,0,0.2)}.widget-user .widget-user-desc{margin-top:0}.widget-user .widget-user-image{position:absolute;top:65px;left:50%;margin-left:-45px}.widget-user .widget-user-image>img{width:90px;height:auto;border:3px solid #fff}.widget-user .box-footer{padding-top:30px}.widget-user-2 .widget-user-header{padding:20px;border-top-right-radius:3px;border-top-left-radius:3px}.widget-user-2 .widget-user-username{margin-top:5px;margin-bottom:5px;font-size:25px;font-weight:300}.widget-user-2 .widget-user-desc{margin-top:0}.widget-user-2 .widget-user-username,.widget-user-2 .widget-user-desc{margin-left:75px}.widget-user-2 .widget-user-image>img{width:65px;height:auto;float:left}.treeview-menu{display:none;list-style:none;padding:0;margin:0;padding-left:5px}.treeview-menu .treeview-menu{padding-left:20px}.treeview-menu>li{margin:0}.treeview-menu>li>a{padding:5px 5px 5px 15px;display:block;font-size:14px}.treeview-menu>li>a>.fa,.treeview-menu>li>a>.glyphicon,.treeview-menu>li>a>.ion{width:20px}.treeview-menu>li>a>.pull-right-container>.fa-angle-left,.treeview-menu>li>a>.pull-right-container>.fa-angle-down,.treeview-menu>li>a>.fa-angle-left,.treeview-menu>li>a>.fa-angle-down{width:auto}.mailbox-messages>.table{margin:0}.mailbox-controls{padding:5px}.mailbox-controls.with-border{border-bottom:1px solid #f4f4f4}.mailbox-read-info{border-bottom:1px solid #f4f4f4;padding:10px}.mailbox-read-info h3{font-size:20px;margin:0}.mailbox-read-info h5{margin:0;padding:5px 0 0 0}.mailbox-read-time{color:#999;font-size:13px}.mailbox-read-message{padding:10px}.mailbox-attachments li{float:left;width:200px;border:1px solid #eee;margin-bottom:10px;margin-right:10px}.mailbox-attachment-name{font-weight:bold;color:#666}.mailbox-attachment-icon,.mailbox-attachment-info,.mailbox-attachment-size{display:block}.mailbox-attachment-info{padding:10px;background:#f4f4f4}.mailbox-attachment-size{color:#999;font-size:12px}.mailbox-attachment-icon{text-align:center;font-size:65px;color:#666;padding:20px 10px}.mailbox-attachment-icon.has-img{padding:0}.mailbox-attachment-icon.has-img>img{max-width:100%;height:auto}.lockscreen{background:#eaecf1}.lockscreen-logo{font-size:35px;text-align:center;margin-bottom:25px;font-weight:300}.lockscreen-logo a{color:#444}.lockscreen-wrapper{max-width:400px;margin:0 auto;margin-top:10%}.lockscreen .lockscreen-name{text-align:center;font-weight:600}.lockscreen-item{border-radius:4px;padding:0;background:#fff;position:relative;margin:10px auto 30px auto;width:290px}.lockscreen-image{border-radius:50%;position:absolute;left:-10px;top:-25px;background:#fff;padding:5px;z-index:10}.lockscreen-image>img{border-radius:50%;width:70px;height:70px}.lockscreen-credentials{margin-left:70px}.lockscreen-credentials .form-control{border:0}.lockscreen-credentials .btn{background-color:#fff;border:0;padding:0 10px}.lockscreen-footer{margin-top:10px}.login-logo,.register-logo{font-size:35px;text-align:center;margin-bottom:25px;font-weight:300}.login-logo a,.register-logo a{color:#444}.login-page,.register-page{background:#eaecf1}.login-box,.register-box{width:360px;margin:7% auto}@media (max-width:768px){.login-box,.register-box{width:90%;margin-top:20px}}.login-box-body,.register-box-body{background:#fff;padding:20px;border-top:0;color:#666}.login-box-body .form-control-feedback,.register-box-body .form-control-feedback{color:#777}.login-box-msg,.register-box-msg{margin:0;text-align:center;padding:0 20px 20px 20px}.social-auth-links{margin:10px 0}.error-page{width:600px;margin:20px auto 0 auto}@media (max-width:991px){.error-page{width:100%}}.error-page>.headline{float:left;font-size:100px;font-weight:300}@media (max-width:991px){.error-page>.headline{float:none;text-align:center}}.error-page>.error-content{margin-left:190px;display:block}@media (max-width:991px){.error-page>.error-content{margin-left:0}}.error-page>.error-content>h3{font-weight:300;font-size:25px}@media (max-width:991px){.error-page>.error-content>h3{text-align:center}}.invoice{position:relative;background:#fff;border:1px solid #f4f4f4;padding:20px;margin:10px 25px}.invoice-title{margin-top:0}.profile-user-img{margin:0 auto;width:100px;padding:3px;border:3px solid #eaecf1}.profile-username{font-size:21px;margin-top:5px}.post{border-bottom:1px solid #eaecf1;margin-bottom:15px;padding-bottom:15px;color:#666}.post:last-of-type{border-bottom:0;margin-bottom:0;padding-bottom:0}.post .user-block{margin-bottom:15px}.btn-social{position:relative;padding-left:44px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.btn-social>:first-child{position:absolute;left:0;top:0;bottom:0;width:32px;line-height:34px;font-size:1.6em;text-align:center;border-right:1px solid rgba(0,0,0,0.2)}.btn-social.btn-lg{padding-left:61px}.btn-social.btn-lg>:first-child{line-height:45px;width:45px;font-size:1.8em}.btn-social.btn-sm{padding-left:38px}.btn-social.btn-sm>:first-child{line-height:28px;width:28px;font-size:1.4em}.btn-social.btn-xs{padding-left:30px}.btn-social.btn-xs>:first-child{line-height:20px;width:20px;font-size:1.2em}.btn-social-icon{position:relative;padding-left:44px;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;height:34px;width:34px;padding:0}.btn-social-icon>:first-child{position:absolute;left:0;top:0;bottom:0;width:32px;line-height:34px;font-size:1.6em;text-align:center;border-right:1px solid rgba(0,0,0,0.2)}.btn-social-icon.btn-lg{padding-left:61px}.btn-social-icon.btn-lg>:first-child{line-height:45px;width:45px;font-size:1.8em}.btn-social-icon.btn-sm{padding-left:38px}.btn-social-icon.btn-sm>:first-child{line-height:28px;width:28px;font-size:1.4em}.btn-social-icon.btn-xs{padding-left:30px}.btn-social-icon.btn-xs>:first-child{line-height:20px;width:20px;font-size:1.2em}.btn-social-icon>:first-child{border:none;text-align:center;width:100%}.btn-social-icon.btn-lg{height:45px;width:45px;padding-left:0;padding-right:0}.btn-social-icon.btn-sm{height:30px;width:30px;padding-left:0;padding-right:0}.btn-social-icon.btn-xs{height:22px;width:22px;padding-left:0;padding-right:0}.btn-adn{color:#fff;background-color:#d87a68;border-color:rgba(0,0,0,0.2)}.btn-adn:focus,.btn-adn.focus{color:#fff;background-color:#ce563f;border-color:rgba(0,0,0,0.2)}.btn-adn:hover{color:#fff;background-color:#ce563f;border-color:rgba(0,0,0,0.2)}.btn-adn:active,.btn-adn.active,.open>.dropdown-toggle.btn-adn{color:#fff;background-color:#ce563f;border-color:rgba(0,0,0,0.2)}.btn-adn:active,.btn-adn.active,.open>.dropdown-toggle.btn-adn{background-image:none}.btn-adn .badge{color:#d87a68;background-color:#fff}.btn-bitbucket{color:#fff;background-color:#205081;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:focus,.btn-bitbucket.focus{color:#fff;background-color:#163758;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:hover{color:#fff;background-color:#163758;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:active,.btn-bitbucket.active,.open>.dropdown-toggle.btn-bitbucket{color:#fff;background-color:#163758;border-color:rgba(0,0,0,0.2)}.btn-bitbucket:active,.btn-bitbucket.active,.open>.dropdown-toggle.btn-bitbucket{background-image:none}.btn-bitbucket .badge{color:#205081;background-color:#fff}.btn-dropbox{color:#fff;background-color:#1087dd;border-color:rgba(0,0,0,0.2)}.btn-dropbox:focus,.btn-dropbox.focus{color:#fff;background-color:#0d6aad;border-color:rgba(0,0,0,0.2)}.btn-dropbox:hover{color:#fff;background-color:#0d6aad;border-color:rgba(0,0,0,0.2)}.btn-dropbox:active,.btn-dropbox.active,.open>.dropdown-toggle.btn-dropbox{color:#fff;background-color:#0d6aad;border-color:rgba(0,0,0,0.2)}.btn-dropbox:active,.btn-dropbox.active,.open>.dropdown-toggle.btn-dropbox{background-image:none}.btn-dropbox .badge{color:#1087dd;background-color:#fff}.btn-facebook{color:#fff;background-color:#3b5998;border-color:rgba(0,0,0,0.2)}.btn-facebook:focus,.btn-facebook.focus{color:#fff;background-color:#2d4373;border-color:rgba(0,0,0,0.2)}.btn-facebook:hover{color:#fff;background-color:#2d4373;border-color:rgba(0,0,0,0.2)}.btn-facebook:active,.btn-facebook.active,.open>.dropdown-toggle.btn-facebook{color:#fff;background-color:#2d4373;border-color:rgba(0,0,0,0.2)}.btn-facebook:active,.btn-facebook.active,.open>.dropdown-toggle.btn-facebook{background-image:none}.btn-facebook .badge{color:#3b5998;background-color:#fff}.btn-flickr{color:#fff;background-color:#ff0084;border-color:rgba(0,0,0,0.2)}.btn-flickr:focus,.btn-flickr.focus{color:#fff;background-color:#cc006a;border-color:rgba(0,0,0,0.2)}.btn-flickr:hover{color:#fff;background-color:#cc006a;border-color:rgba(0,0,0,0.2)}.btn-flickr:active,.btn-flickr.active,.open>.dropdown-toggle.btn-flickr{color:#fff;background-color:#cc006a;border-color:rgba(0,0,0,0.2)}.btn-flickr:active,.btn-flickr.active,.open>.dropdown-toggle.btn-flickr{background-image:none}.btn-flickr .badge{color:#ff0084;background-color:#fff}.btn-foursquare{color:#fff;background-color:#f94877;border-color:rgba(0,0,0,0.2)}.btn-foursquare:focus,.btn-foursquare.focus{color:#fff;background-color:#f71752;border-color:rgba(0,0,0,0.2)}.btn-foursquare:hover{color:#fff;background-color:#f71752;border-color:rgba(0,0,0,0.2)}.btn-foursquare:active,.btn-foursquare.active,.open>.dropdown-toggle.btn-foursquare{color:#fff;background-color:#f71752;border-color:rgba(0,0,0,0.2)}.btn-foursquare:active,.btn-foursquare.active,.open>.dropdown-toggle.btn-foursquare{background-image:none}.btn-foursquare .badge{color:#f94877;background-color:#fff}.btn-github{color:#fff;background-color:#444;border-color:rgba(0,0,0,0.2)}.btn-github:focus,.btn-github.focus{color:#fff;background-color:#2b2b2b;border-color:rgba(0,0,0,0.2)}.btn-github:hover{color:#fff;background-color:#2b2b2b;border-color:rgba(0,0,0,0.2)}.btn-github:active,.btn-github.active,.open>.dropdown-toggle.btn-github{color:#fff;background-color:#2b2b2b;border-color:rgba(0,0,0,0.2)}.btn-github:active,.btn-github.active,.open>.dropdown-toggle.btn-github{background-image:none}.btn-github .badge{color:#444;background-color:#fff}.btn-google{color:#fff;background-color:#dd4b39;border-color:rgba(0,0,0,0.2)}.btn-google:focus,.btn-google.focus{color:#fff;background-color:#c23321;border-color:rgba(0,0,0,0.2)}.btn-google:hover{color:#fff;background-color:#c23321;border-color:rgba(0,0,0,0.2)}.btn-google:active,.btn-google.active,.open>.dropdown-toggle.btn-google{color:#fff;background-color:#c23321;border-color:rgba(0,0,0,0.2)}.btn-google:active,.btn-google.active,.open>.dropdown-toggle.btn-google{background-image:none}.btn-google .badge{color:#dd4b39;background-color:#fff}.btn-instagram{color:#fff;background-color:#3f729b;border-color:rgba(0,0,0,0.2)}.btn-instagram:focus,.btn-instagram.focus{color:#fff;background-color:#305777;border-color:rgba(0,0,0,0.2)}.btn-instagram:hover{color:#fff;background-color:#305777;border-color:rgba(0,0,0,0.2)}.btn-instagram:active,.btn-instagram.active,.open>.dropdown-toggle.btn-instagram{color:#fff;background-color:#305777;border-color:rgba(0,0,0,0.2)}.btn-instagram:active,.btn-instagram.active,.open>.dropdown-toggle.btn-instagram{background-image:none}.btn-instagram .badge{color:#3f729b;background-color:#fff}.btn-linkedin{color:#fff;background-color:#007bb6;border-color:rgba(0,0,0,0.2)}.btn-linkedin:focus,.btn-linkedin.focus{color:#fff;background-color:#005983;border-color:rgba(0,0,0,0.2)}.btn-linkedin:hover{color:#fff;background-color:#005983;border-color:rgba(0,0,0,0.2)}.btn-linkedin:active,.btn-linkedin.active,.open>.dropdown-toggle.btn-linkedin{color:#fff;background-color:#005983;border-color:rgba(0,0,0,0.2)}.btn-linkedin:active,.btn-linkedin.active,.open>.dropdown-toggle.btn-linkedin{background-image:none}.btn-linkedin .badge{color:#007bb6;background-color:#fff}.btn-microsoft{color:#fff;background-color:#2672ec;border-color:rgba(0,0,0,0.2)}.btn-microsoft:focus,.btn-microsoft.focus{color:#fff;background-color:#125acd;border-color:rgba(0,0,0,0.2)}.btn-microsoft:hover{color:#fff;background-color:#125acd;border-color:rgba(0,0,0,0.2)}.btn-microsoft:active,.btn-microsoft.active,.open>.dropdown-toggle.btn-microsoft{color:#fff;background-color:#125acd;border-color:rgba(0,0,0,0.2)}.btn-microsoft:active,.btn-microsoft.active,.open>.dropdown-toggle.btn-microsoft{background-image:none}.btn-microsoft .badge{color:#2672ec;background-color:#fff}.btn-openid{color:#fff;background-color:#f7931e;border-color:rgba(0,0,0,0.2)}.btn-openid:focus,.btn-openid.focus{color:#fff;background-color:#da7908;border-color:rgba(0,0,0,0.2)}.btn-openid:hover{color:#fff;background-color:#da7908;border-color:rgba(0,0,0,0.2)}.btn-openid:active,.btn-openid.active,.open>.dropdown-toggle.btn-openid{color:#fff;background-color:#da7908;border-color:rgba(0,0,0,0.2)}.btn-openid:active,.btn-openid.active,.open>.dropdown-toggle.btn-openid{background-image:none}.btn-openid .badge{color:#f7931e;background-color:#fff}.btn-pinterest{color:#fff;background-color:#cb2027;border-color:rgba(0,0,0,0.2)}.btn-pinterest:focus,.btn-pinterest.focus{color:#fff;background-color:#9f191f;border-color:rgba(0,0,0,0.2)}.btn-pinterest:hover{color:#fff;background-color:#9f191f;border-color:rgba(0,0,0,0.2)}.btn-pinterest:active,.btn-pinterest.active,.open>.dropdown-toggle.btn-pinterest{color:#fff;background-color:#9f191f;border-color:rgba(0,0,0,0.2)}.btn-pinterest:active,.btn-pinterest.active,.open>.dropdown-toggle.btn-pinterest{background-image:none}.btn-pinterest .badge{color:#cb2027;background-color:#fff}.btn-reddit{color:#000;background-color:#eff7ff;border-color:rgba(0,0,0,0.2)}.btn-reddit:focus,.btn-reddit.focus{color:#000;background-color:#bcddff;border-color:rgba(0,0,0,0.2)}.btn-reddit:hover{color:#000;background-color:#bcddff;border-color:rgba(0,0,0,0.2)}.btn-reddit:active,.btn-reddit.active,.open>.dropdown-toggle.btn-reddit{color:#000;background-color:#bcddff;border-color:rgba(0,0,0,0.2)}.btn-reddit:active,.btn-reddit.active,.open>.dropdown-toggle.btn-reddit{background-image:none}.btn-reddit .badge{color:#eff7ff;background-color:#000}.btn-soundcloud{color:#fff;background-color:#f50;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:focus,.btn-soundcloud.focus{color:#fff;background-color:#c40;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:hover{color:#fff;background-color:#c40;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:active,.btn-soundcloud.active,.open>.dropdown-toggle.btn-soundcloud{color:#fff;background-color:#c40;border-color:rgba(0,0,0,0.2)}.btn-soundcloud:active,.btn-soundcloud.active,.open>.dropdown-toggle.btn-soundcloud{background-image:none}.btn-soundcloud .badge{color:#f50;background-color:#fff}.btn-tumblr{color:#fff;background-color:#2c4762;border-color:rgba(0,0,0,0.2)}.btn-tumblr:focus,.btn-tumblr.focus{color:#fff;background-color:#1c2d3f;border-color:rgba(0,0,0,0.2)}.btn-tumblr:hover{color:#fff;background-color:#1c2d3f;border-color:rgba(0,0,0,0.2)}.btn-tumblr:active,.btn-tumblr.active,.open>.dropdown-toggle.btn-tumblr{color:#fff;background-color:#1c2d3f;border-color:rgba(0,0,0,0.2)}.btn-tumblr:active,.btn-tumblr.active,.open>.dropdown-toggle.btn-tumblr{background-image:none}.btn-tumblr .badge{color:#2c4762;background-color:#fff}.btn-twitter{color:#fff;background-color:#55acee;border-color:rgba(0,0,0,0.2)}.btn-twitter:focus,.btn-twitter.focus{color:#fff;background-color:#2795e9;border-color:rgba(0,0,0,0.2)}.btn-twitter:hover{color:#fff;background-color:#2795e9;border-color:rgba(0,0,0,0.2)}.btn-twitter:active,.btn-twitter.active,.open>.dropdown-toggle.btn-twitter{color:#fff;background-color:#2795e9;border-color:rgba(0,0,0,0.2)}.btn-twitter:active,.btn-twitter.active,.open>.dropdown-toggle.btn-twitter{background-image:none}.btn-twitter .badge{color:#55acee;background-color:#fff}.btn-vimeo{color:#fff;background-color:#1ab7ea;border-color:rgba(0,0,0,0.2)}.btn-vimeo:focus,.btn-vimeo.focus{color:#fff;background-color:#1295bf;border-color:rgba(0,0,0,0.2)}.btn-vimeo:hover{color:#fff;background-color:#1295bf;border-color:rgba(0,0,0,0.2)}.btn-vimeo:active,.btn-vimeo.active,.open>.dropdown-toggle.btn-vimeo{color:#fff;background-color:#1295bf;border-color:rgba(0,0,0,0.2)}.btn-vimeo:active,.btn-vimeo.active,.open>.dropdown-toggle.btn-vimeo{background-image:none}.btn-vimeo .badge{color:#1ab7ea;background-color:#fff}.btn-vk{color:#fff;background-color:#587ea3;border-color:rgba(0,0,0,0.2)}.btn-vk:focus,.btn-vk.focus{color:#fff;background-color:#466482;border-color:rgba(0,0,0,0.2)}.btn-vk:hover{color:#fff;background-color:#466482;border-color:rgba(0,0,0,0.2)}.btn-vk:active,.btn-vk.active,.open>.dropdown-toggle.btn-vk{color:#fff;background-color:#466482;border-color:rgba(0,0,0,0.2)}.btn-vk:active,.btn-vk.active,.open>.dropdown-toggle.btn-vk{background-image:none}.btn-vk .badge{color:#587ea3;background-color:#fff}.btn-yahoo{color:#fff;background-color:#720e9e;border-color:rgba(0,0,0,0.2)}.btn-yahoo:focus,.btn-yahoo.focus{color:#fff;background-color:#500a6f;border-color:rgba(0,0,0,0.2)}.btn-yahoo:hover{color:#fff;background-color:#500a6f;border-color:rgba(0,0,0,0.2)}.btn-yahoo:active,.btn-yahoo.active,.open>.dropdown-toggle.btn-yahoo{color:#fff;background-color:#500a6f;border-color:rgba(0,0,0,0.2)}.btn-yahoo:active,.btn-yahoo.active,.open>.dropdown-toggle.btn-yahoo{background-image:none}.btn-yahoo .badge{color:#720e9e;background-color:#fff}.fc-button{background:#f4f4f4;background-image:none;color:#444;border-color:#ddd;border-bottom-color:#ddd}.fc-button:hover,.fc-button:active,.fc-button.hover{background-color:#e9e9e9}.fc-header-title h2{font-size:15px;line-height:1.6em;color:#666;margin-left:10px}.fc-header-right{padding-right:10px}.fc-header-left{padding-left:10px}.fc-widget-header{background:#fafafa}.fc-grid{width:100%;border:0}.fc-widget-header:first-of-type,.fc-widget-content:first-of-type{border-left:0;border-right:0}.fc-widget-header:last-of-type,.fc-widget-content:last-of-type{border-right:0}.fc-toolbar{padding:10px;margin:0}.fc-day-number{font-size:20px;font-weight:300;padding-right:10px}.fc-color-picker{list-style:none;margin:0;padding:0}.fc-color-picker>li{float:left;font-size:30px;margin-right:5px;line-height:30px}.fc-color-picker>li .fa{-webkit-transition:-webkit-transform linear .3s;-moz-transition:-moz-transform linear .3s;-o-transition:-o-transform linear .3s;transition:transform linear .3s}.fc-color-picker>li .fa:hover{-webkit-transform:rotate(30deg);-ms-transform:rotate(30deg);-o-transform:rotate(30deg);transform:rotate(30deg)}#add-new-event{-webkit-transition:all linear .3s;-o-transition:all linear .3s;transition:all linear .3s}.external-event{padding:5px 10px;font-weight:bold;margin-bottom:4px;box-shadow:0 1px 1px rgba(0,0,0,0.1);text-shadow:0 1px 1px rgba(0,0,0,0.1);border-radius:3px;cursor:move}.external-event:hover{box-shadow:inset 0 0 90px rgba(0,0,0,0.2)}.select2-container--default.select2-container--focus,.select2-selection.select2-container--focus,.select2-container--default:focus,.select2-selection:focus,.select2-container--default:active,.select2-selection:active{outline:none}.select2-container--default .select2-selection--single,.select2-selection .select2-selection--single{border:1px solid #eaecf1;border-radius:0;padding:6px 12px;height:34px}.select2-container--default.select2-container--open{border-color:#10529f}.select2-dropdown{border:1px solid #eaecf1;border-radius:0}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#10529f;color:white}.select2-results__option{padding:6px 12px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{padding-left:0;padding-right:0;height:auto;margin-top:-4px}.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered{padding-right:6px;padding-left:20px}.select2-container--default .select2-selection--single .select2-selection__arrow{height:28px;right:3px}.select2-container--default .select2-selection--single .select2-selection__arrow b{margin-top:0}.select2-dropdown .select2-search__field,.select2-search--inline .select2-search__field{border:1px solid #eaecf1}.select2-dropdown .select2-search__field:focus,.select2-search--inline .select2-search__field:focus{outline:none}.select2-container--default.select2-container--focus .select2-selection--multiple,.select2-container--default .select2-search--dropdown .select2-search__field{border-color:#10529f !important}.select2-container--default .select2-results__option[aria-disabled=true]{color:#999}.select2-container--default .select2-results__option[aria-selected=true]{background-color:#ddd}.select2-container--default .select2-results__option[aria-selected=true],.select2-container--default .select2-results__option[aria-selected=true]:hover{color:#444}.select2-container--default .select2-selection--multiple{border:1px solid #eaecf1;border-radius:0}.select2-container--default .select2-selection--multiple:focus{border-color:#10529f}.select2-container--default.select2-container--focus .select2-selection--multiple{border-color:#eaecf1}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#10529f;border-color:#0e4688;padding:1px 10px;color:#fff}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{margin-right:5px;color:rgba(255,255,255,0.7)}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#fff}.select2-container .select2-selection--single .select2-selection__rendered{padding-right:10px}.box .datepicker-inline,.box .datepicker-inline .datepicker-days,.box .datepicker-inline>table,.box .datepicker-inline .datepicker-days>table{width:100%}.box .datepicker-inline td:hover,.box .datepicker-inline .datepicker-days td:hover,.box .datepicker-inline>table td:hover,.box .datepicker-inline .datepicker-days>table td:hover{background-color:rgba(255,255,255,0.3)}.box .datepicker-inline td.day.old,.box .datepicker-inline .datepicker-days td.day.old,.box .datepicker-inline>table td.day.old,.box .datepicker-inline .datepicker-days>table td.day.old,.box .datepicker-inline td.day.new,.box .datepicker-inline .datepicker-days td.day.new,.box .datepicker-inline>table td.day.new,.box .datepicker-inline .datepicker-days>table td.day.new{color:#777}.pad{padding:10px}.margin{margin:10px}.margin-bottom{margin-bottom:20px}.margin-bottom-none{margin-bottom:0}.margin-r-5{margin-right:5px}.inline{display:inline}.description-block{display:block;margin:10px 0;text-align:center}.description-block.margin-bottom{margin-bottom:25px}.description-block>.description-header{margin:0;padding:0;font-weight:600;font-size:16px}.description-block>.description-text{text-transform:uppercase}.bg-red,.bg-yellow,.bg-aqua,.bg-blue,.bg-light-blue,.bg-green,.bg-navy,.bg-teal,.bg-olive,.bg-lime,.bg-orange,.bg-fuchsia,.bg-purple,.bg-maroon,.bg-black,.bg-red-active,.bg-yellow-active,.bg-aqua-active,.bg-blue-active,.bg-light-blue-active,.bg-green-active,.bg-navy-active,.bg-teal-active,.bg-olive-active,.bg-lime-active,.bg-orange-active,.bg-fuchsia-active,.bg-purple-active,.bg-maroon-active,.bg-black-active,.callout.callout-danger,.callout.callout-warning,.callout.callout-info,.callout.callout-success,.alert-success,.alert-danger,.alert-error,.alert-warning,.alert-info,.label-danger,.label-info,.label-warning,.label-primary,.label-success,.modal-primary .modal-body,.modal-primary .modal-header,.modal-primary .modal-footer,.modal-warning .modal-body,.modal-warning .modal-header,.modal-warning .modal-footer,.modal-info .modal-body,.modal-info .modal-header,.modal-info .modal-footer,.modal-success .modal-body,.modal-success .modal-header,.modal-success .modal-footer,.modal-danger .modal-body,.modal-danger .modal-header,.modal-danger .modal-footer{color:#fff !important}.bg-gray{color:#000;background-color:#eaecf1 !important}.bg-gray-light{background-color:#f7f7f7}.bg-black{background-color:#111 !important}.bg-red,.callout.callout-danger,.alert-danger,.alert-error,.label-danger,.modal-danger .modal-body{background-color:#dd4b39 !important}.bg-yellow,.callout.callout-warning,.alert-warning,.label-warning,.modal-warning .modal-body{background-color:#f39c12 !important}.bg-aqua,.callout.callout-info,.alert-info,.label-info,.modal-info .modal-body{background-color:#00c0ef !important}.bg-blue{background-color:#10529f !important}.bg-light-blue,.label-primary,.modal-primary .modal-body{background-color:#10529f !important}.bg-green,.callout.callout-success,.alert-success,.label-success,.modal-success .modal-body{background-color:#00a65a !important}.bg-navy{background-color:#001f3f !important}.bg-teal{background-color:#39cccc !important}.bg-olive{background-color:#3d9970 !important}.bg-lime{background-color:#01ff70 !important}.bg-orange{background-color:#ff851b !important}.bg-fuchsia{background-color:#f012be !important}.bg-purple{background-color:#605ca8 !important}.bg-maroon{background-color:#d81b60 !important}.bg-gray-active{color:#000;background-color:#cbd0dd !important}.bg-black-active{background-color:#000 !important}.bg-red-active,.modal-danger .modal-header,.modal-danger .modal-footer{background-color:#d33724 !important}.bg-yellow-active,.modal-warning .modal-header,.modal-warning .modal-footer{background-color:#db8b0b !important}.bg-aqua-active,.modal-info .modal-header,.modal-info .modal-footer{background-color:#00a7d0 !important}.bg-blue-active{background-color:#0b3a71 !important}.bg-light-blue-active,.modal-primary .modal-header,.modal-primary .modal-footer{background-color:#0d4483 !important}.bg-green-active,.modal-success .modal-header,.modal-success .modal-footer{background-color:#008d4c !important}.bg-navy-active{background-color:#001a35 !important}.bg-teal-active{background-color:#30bbbb !important}.bg-olive-active{background-color:#368763 !important}.bg-lime-active{background-color:#00e765 !important}.bg-orange-active{background-color:#ff7701 !important}.bg-fuchsia-active{background-color:#db0ead !important}.bg-purple-active{background-color:#555299 !important}.bg-maroon-active{background-color:#ca195a !important}[class^="bg-"].disabled{opacity:.65;filter:alpha(opacity=65)}.text-red{color:#dd4b39 !important}.text-yellow{color:#f39c12 !important}.text-aqua{color:#00c0ef !important}.text-blue{color:#10529f !important}.text-black{color:#111 !important}.text-light-blue{color:#10529f !important}.text-green{color:#00a65a !important}.text-gray{color:#eaecf1 !important}.text-navy{color:#001f3f !important}.text-teal{color:#39cccc !important}.text-olive{color:#3d9970 !important}.text-lime{color:#01ff70 !important}.text-orange{color:#ff851b !important}.text-fuchsia{color:#f012be !important}.text-purple{color:#605ca8 !important}.text-maroon{color:#d81b60 !important}.link-muted{color:#8e99b4}.link-muted:hover,.link-muted:focus{color:#707d9f}.link-black{color:#666}.link-black:hover,.link-black:focus{color:#999}.hide{display:none !important}.no-border{border:0 !important}.no-padding{padding:0 !important}.no-margin{margin:0 !important}.no-shadow{box-shadow:none !important}.list-unstyled,.chart-legend,.contacts-list,.users-list,.mailbox-attachments{list-style:none;margin:0;padding:0}.list-group-unbordered>.list-group-item{border-left:0;border-right:0;border-radius:0;padding-left:0;padding-right:0}.flat{border-radius:0 !important}.text-bold,.text-bold.table td,.text-bold.table th{font-weight:700}.text-sm{font-size:12px}.jqstooltip{padding:5px !important;width:auto !important;height:auto !important}.bg-teal-gradient{background:#39cccc !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #39cccc), color-stop(1, #7adddd)) !important;background:-ms-linear-gradient(bottom, #39cccc, #7adddd) !important;background:-moz-linear-gradient(center bottom, #39cccc 0, #7adddd 100%) !important;background:-o-linear-gradient(#7adddd, #39cccc) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#7adddd', endColorstr='#39cccc', GradientType=0) !important;color:#fff}.bg-light-blue-gradient{background:#10529f !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #10529f), color-stop(1, #166fd7)) !important;background:-ms-linear-gradient(bottom, #10529f, #166fd7) !important;background:-moz-linear-gradient(center bottom, #10529f 0, #166fd7 100%) !important;background:-o-linear-gradient(#166fd7, #10529f) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#166fd7', endColorstr='#10529f', GradientType=0) !important;color:#fff}.bg-blue-gradient{background:#10529f !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #10529f), color-stop(1, #1363bf)) !important;background:-ms-linear-gradient(bottom, #10529f, #1363bf) !important;background:-moz-linear-gradient(center bottom, #10529f 0, #1363bf 100%) !important;background:-o-linear-gradient(#1363bf, #10529f) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#1363bf', endColorstr='#10529f', GradientType=0) !important;color:#fff}.bg-aqua-gradient{background:#00c0ef !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #00c0ef), color-stop(1, #14d1ff)) !important;background:-ms-linear-gradient(bottom, #00c0ef, #14d1ff) !important;background:-moz-linear-gradient(center bottom, #00c0ef 0, #14d1ff 100%) !important;background:-o-linear-gradient(#14d1ff, #00c0ef) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#14d1ff', endColorstr='#00c0ef', GradientType=0) !important;color:#fff}.bg-yellow-gradient{background:#f39c12 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #f39c12), color-stop(1, #f7bc60)) !important;background:-ms-linear-gradient(bottom, #f39c12, #f7bc60) !important;background:-moz-linear-gradient(center bottom, #f39c12 0, #f7bc60 100%) !important;background:-o-linear-gradient(#f7bc60, #f39c12) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#f7bc60', endColorstr='#f39c12', GradientType=0) !important;color:#fff}.bg-purple-gradient{background:#605ca8 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #605ca8), color-stop(1, #9491c4)) !important;background:-ms-linear-gradient(bottom, #605ca8, #9491c4) !important;background:-moz-linear-gradient(center bottom, #605ca8 0, #9491c4 100%) !important;background:-o-linear-gradient(#9491c4, #605ca8) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#9491c4', endColorstr='#605ca8', GradientType=0) !important;color:#fff}.bg-green-gradient{background:#00a65a !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #00a65a), color-stop(1, #00ca6d)) !important;background:-ms-linear-gradient(bottom, #00a65a, #00ca6d) !important;background:-moz-linear-gradient(center bottom, #00a65a 0, #00ca6d 100%) !important;background:-o-linear-gradient(#00ca6d, #00a65a) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ca6d', endColorstr='#00a65a', GradientType=0) !important;color:#fff}.bg-red-gradient{background:#dd4b39 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #dd4b39), color-stop(1, #e47365)) !important;background:-ms-linear-gradient(bottom, #dd4b39, #e47365) !important;background:-moz-linear-gradient(center bottom, #dd4b39 0, #e47365 100%) !important;background:-o-linear-gradient(#e47365, #dd4b39) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#e47365', endColorstr='#dd4b39', GradientType=0) !important;color:#fff}.bg-black-gradient{background:#111 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #111), color-stop(1, #2b2b2b)) !important;background:-ms-linear-gradient(bottom, #111, #2b2b2b) !important;background:-moz-linear-gradient(center bottom, #111 0, #2b2b2b 100%) !important;background:-o-linear-gradient(#2b2b2b, #111) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#2b2b2b', endColorstr='#111111', GradientType=0) !important;color:#fff}.bg-maroon-gradient{background:#d81b60 !important;background:-webkit-gradient(linear, left bottom, left top, color-stop(0, #d81b60), color-stop(1, #e73f7c)) !important;background:-ms-linear-gradient(bottom, #d81b60, #e73f7c) !important;background:-moz-linear-gradient(center bottom, #d81b60 0, #e73f7c 100%) !important;background:-o-linear-gradient(#e73f7c, #d81b60) !important;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#e73f7c', endColorstr='#d81b60', GradientType=0) !important;color:#fff}.description-block .description-icon{font-size:16px}.no-pad-top{padding-top:0}.position-static{position:static !important}.list-header{font-size:15px;padding:10px 4px;font-weight:bold;color:#666}.list-seperator{height:1px;background:#f4f4f4;margin:15px 0 9px 0}.list-link>a{padding:4px;color:#777}.list-link>a:hover{color:#222}.font-light{font-weight:300}.user-block:before,.user-block:after{content:" ";display:table}.user-block:after{clear:both}.user-block img{width:40px;height:40px;float:left}.user-block .username,.user-block .description,.user-block .comment{display:block;margin-left:50px}.user-block .username{font-size:16px;font-weight:600}.user-block .description{color:#999;font-size:13px}.user-block.user-block-sm .username,.user-block.user-block-sm .description,.user-block.user-block-sm .comment{margin-left:40px}.user-block.user-block-sm .username{font-size:14px}.img-sm,.img-md,.img-lg,.box-comments .box-comment img,.user-block.user-block-sm img{float:left}.img-sm,.box-comments .box-comment img,.user-block.user-block-sm img{width:30px !important;height:30px !important}.img-sm+.img-push{margin-left:40px}.img-md{width:60px;height:60px}.img-md+.img-push{margin-left:70px}.img-lg{width:100px;height:100px}.img-lg+.img-push{margin-left:110px}.img-bordered{border:3px solid #eaecf1;padding:3px}.img-bordered-sm{border:2px solid #eaecf1;padding:2px}.attachment-block{border:1px solid #f4f4f4;padding:5px;margin-bottom:10px;background:#f7f7f7}.attachment-block .attachment-img{max-width:100px;max-height:100px;height:auto;float:left}.attachment-block .attachment-pushed{margin-left:110px}.attachment-block .attachment-heading{margin:0}.attachment-block .attachment-text{color:#555}.connectedSortable{min-height:100px}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sort-highlight{background:#f4f4f4;border:1px dashed #ddd;margin-bottom:10px}.full-opacity-hover{opacity:.65;filter:alpha(opacity=65)}.full-opacity-hover:hover{opacity:1;filter:alpha(opacity=100)}.chart{position:relative;overflow:hidden;width:100%}.chart svg,.chart canvas{width:100% !important}@media print{.no-print,.main-sidebar,.left-side,.main-header,.content-header{display:none !important}.content-wrapper,.right-side,.main-footer{margin-left:0 !important;min-height:0 !important;-webkit-transform:translate(0, 0) !important;-ms-transform:translate(0, 0) !important;-o-transform:translate(0, 0) !important;transform:translate(0, 0) !important}.fixed .content-wrapper,.fixed .right-side{padding-top:0 !important}.invoice{width:100%;border:0;margin:0;padding:0}.invoice-col{float:left;width:33.3333333%}.table-responsive{overflow:auto}.table-responsive>.table tr th,.table-responsive>.table tr td{white-space:normal !important}} diff --git a/public/themes/pterodactyl/vendor/adminlte/colors/skin-blue.min.css b/public/themes/pterodactyl/vendor/adminlte/colors/skin-blue.min.css index 2de8c05b..cdbef24b 100755 --- a/public/themes/pterodactyl/vendor/adminlte/colors/skin-blue.min.css +++ b/public/themes/pterodactyl/vendor/adminlte/colors/skin-blue.min.css @@ -1 +1 @@ -.skin-blue .main-header .navbar{background-color:#303f9f}.skin-blue .main-header .navbar .nav>li>a{color:#fff}.skin-blue .main-header .navbar .nav>li>a:hover,.skin-blue .main-header .navbar .nav>li>a:active,.skin-blue .main-header .navbar .nav>li>a:focus,.skin-blue .main-header .navbar .nav .open>a,.skin-blue .main-header .navbar .nav .open>a:hover,.skin-blue .main-header .navbar .nav .open>a:focus,.skin-blue .main-header .navbar .nav>.active>a{background:rgba(0,0,0,0.1);color:#f6f6f6}.skin-blue .main-header .navbar .sidebar-toggle{color:#fff}.skin-blue .main-header .navbar .sidebar-toggle:hover{color:#f6f6f6;background:rgba(0,0,0,0.1)}.skin-blue .main-header .navbar .sidebar-toggle{color:#fff}.skin-blue .main-header .navbar .sidebar-toggle:hover{background-color:#2a378b}@media (max-width:767px){.skin-blue .main-header .navbar .dropdown-menu li.divider{background-color:rgba(255,255,255,0.1)}.skin-blue .main-header .navbar .dropdown-menu li a{color:#fff}.skin-blue .main-header .navbar .dropdown-menu li a:hover{background:#2a378b}}.skin-blue .main-header .logo{background-color:#2a378b;color:#fff;border-bottom:0 solid transparent}.skin-blue .main-header .logo:hover{background-color:#293687}.skin-blue .main-header li.user-header{background-color:#303f9f}.skin-blue .content-header{background:transparent}.skin-blue .wrapper,.skin-blue .main-sidebar,.skin-blue .left-side{background-color:#263238}.skin-blue .user-panel>.info,.skin-blue .user-panel>.info>a{color:#fff}.skin-blue .sidebar-menu>li.header{color:#4f6875;background:#1e272c}.skin-blue .sidebar-menu>li>a{border-left:3px solid transparent}.skin-blue .sidebar-menu>li:hover>a,.skin-blue .sidebar-menu>li.active>a,.skin-blue .sidebar-menu>li.menu-open>a{color:#fff;background:#222d32}.skin-blue .sidebar-menu>li.active>a{border-left-color:#303f9f}.skin-blue .sidebar-menu>li>.treeview-menu{margin:0 1px;background:#304047}.skin-blue .sidebar a{color:#bdccd3}.skin-blue .sidebar a:hover{text-decoration:none}.skin-blue .sidebar-menu .treeview-menu>li>a{color:#90a8b4}.skin-blue .sidebar-menu .treeview-menu>li.active>a,.skin-blue .sidebar-menu .treeview-menu>li>a:hover{color:#fff}.skin-blue .sidebar-form{border-radius:3px;border:1px solid #3b4d56;margin:10px 10px}.skin-blue .sidebar-form input[type="text"],.skin-blue .sidebar-form .btn{box-shadow:none;background-color:#3b4d56;border:1px solid transparent;height:35px}.skin-blue .sidebar-form input[type="text"]{color:#666;border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px}.skin-blue .sidebar-form input[type="text"]:focus,.skin-blue .sidebar-form input[type="text"]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.skin-blue .sidebar-form input[type="text"]:focus+.input-group-btn .btn{border-left-color:#fff}.skin-blue .sidebar-form .btn{color:#999;border-top-left-radius:0;border-top-right-radius:2px;border-bottom-right-radius:2px;border-bottom-left-radius:0}.skin-blue.layout-top-nav .main-header>.logo{background-color:#303f9f;color:#fff;border-bottom:0 solid transparent}.skin-blue.layout-top-nav .main-header>.logo:hover{background-color:#2f3d9b} +.skin-blue .main-header .navbar{background-color:#10529f}.skin-blue .main-header .navbar .nav>li>a{color:#fff}.skin-blue .main-header .navbar .nav>li>a:hover,.skin-blue .main-header .navbar .nav>li>a:active,.skin-blue .main-header .navbar .nav>li>a:focus,.skin-blue .main-header .navbar .nav .open>a,.skin-blue .main-header .navbar .nav .open>a:hover,.skin-blue .main-header .navbar .nav .open>a:focus,.skin-blue .main-header .navbar .nav>.active>a{background:rgba(0,0,0,0.1);color:#f6f6f6}.skin-blue .main-header .navbar .sidebar-toggle{color:#fff}.skin-blue .main-header .navbar .sidebar-toggle:hover{color:#f6f6f6;background:rgba(0,0,0,0.1)}.skin-blue .main-header .navbar .sidebar-toggle{color:#fff}.skin-blue .main-header .navbar .sidebar-toggle:hover{background-color:#0e4688}@media (max-width:767px){.skin-blue .main-header .navbar .dropdown-menu li.divider{background-color:rgba(255,255,255,0.1)}.skin-blue .main-header .navbar .dropdown-menu li a{color:#fff}.skin-blue .main-header .navbar .dropdown-menu li a:hover{background:#0e4688}}.skin-blue .main-header .logo{background-color:#0e4688;color:#fff;border-bottom:0 solid transparent}.skin-blue .main-header .logo:hover{background-color:#0d4483}.skin-blue .main-header li.user-header{background-color:#10529f}.skin-blue .content-header{background:transparent}.skin-blue .wrapper,.skin-blue .main-sidebar,.skin-blue .left-side{background-color:#191b22}.skin-blue .user-panel>.info,.skin-blue .user-panel>.info>a{color:#fff}.skin-blue .sidebar-menu>li.header{color:#444a5d;background:#101216}.skin-blue .sidebar-menu>li>a{border-left:3px solid transparent}.skin-blue .sidebar-menu>li:hover>a,.skin-blue .sidebar-menu>li.active>a,.skin-blue .sidebar-menu>li.menu-open>a{color:#fff;background:#15161c}.skin-blue .sidebar-menu>li.active>a{border-left-color:#10529f}.skin-blue .sidebar-menu>li>.treeview-menu{margin:0 1px;background:#242731}.skin-blue .sidebar a{color:#abb0c2}.skin-blue .sidebar a:hover{text-decoration:none}.skin-blue .sidebar-menu .treeview-menu>li>a{color:#7f87a1}.skin-blue .sidebar-menu .treeview-menu>li.active>a,.skin-blue .sidebar-menu .treeview-menu>li>a:hover{color:#fff}.skin-blue .sidebar-form{border-radius:3px;border:1px solid #2f323f;margin:10px 10px}.skin-blue .sidebar-form input[type="text"],.skin-blue .sidebar-form .btn{box-shadow:none;background-color:#2f323f;border:1px solid transparent;height:35px}.skin-blue .sidebar-form input[type="text"]{color:#666;border-top-left-radius:2px;border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:2px}.skin-blue .sidebar-form input[type="text"]:focus,.skin-blue .sidebar-form input[type="text"]:focus+.input-group-btn .btn{background-color:#fff;color:#666}.skin-blue .sidebar-form input[type="text"]:focus+.input-group-btn .btn{border-left-color:#fff}.skin-blue .sidebar-form .btn{color:#999;border-top-left-radius:0;border-top-right-radius:2px;border-bottom-right-radius:2px;border-bottom-left-radius:0}.skin-blue.layout-top-nav .main-header>.logo{background-color:#10529f;color:#fff;border-bottom:0 solid transparent}.skin-blue.layout-top-nav .main-header>.logo:hover{background-color:#10509a} diff --git a/resources/themes/pterodactyl/layouts/auth.blade.php b/resources/themes/pterodactyl/layouts/auth.blade.php index 9c035f00..d49e68ee 100644 --- a/resources/themes/pterodactyl/layouts/auth.blade.php +++ b/resources/themes/pterodactyl/layouts/auth.blade.php @@ -47,7 +47,7 @@ @show - + + {!! method_field('PATCH') !!}
    @endcan diff --git a/routes/server.php b/routes/server.php index 46724e52..a79309c2 100644 --- a/routes/server.php +++ b/routes/server.php @@ -21,6 +21,8 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ +use Pterodactyl\Http\Middleware\Server\SubuserAccess; + Route::get('/', 'ConsoleController@index')->name('server.index'); Route::get('/console', 'ConsoleController@console')->name('server.console'); @@ -71,12 +73,13 @@ Route::group(['prefix' => 'files'], function () { Route::group(['prefix' => 'users'], function () { Route::get('/', 'SubuserController@index')->name('server.subusers'); Route::get('/new', 'SubuserController@create')->name('server.subusers.new'); - Route::get('/view/{id}', 'SubuserController@view')->name('server.subusers.view'); + Route::get('/view/{subuser}', 'SubuserController@view')->middleware(SubuserAccess::class)->name('server.subusers.view'); Route::post('/new', 'SubuserController@store'); - Route::post('/view/{id}', 'SubuserController@update'); - Route::delete('/delete/{id}', 'SubuserController@delete')->name('server.subusers.delete'); + Route::patch('/view/{subuser}', 'SubuserController@update')->middleware(SubuserAccess::class); + + Route::delete('/delete/{subuser}', 'SubuserController@delete')->middleware(SubuserAccess::class)->name('server.subusers.delete'); }); /* diff --git a/tests/Assertions/ControllerAssertionsTrait.php b/tests/Assertions/ControllerAssertionsTrait.php index cfd0a2fc..2211a080 100644 --- a/tests/Assertions/ControllerAssertionsTrait.php +++ b/tests/Assertions/ControllerAssertionsTrait.php @@ -159,10 +159,11 @@ trait ControllerAssertionsTrait * * @param string $route * @param mixed $response + * @param array $args */ - public function assertRedirectRouteEquals($route, $response) + public function assertRedirectRouteEquals($route, $response, array $args = []) { - PHPUnit_Framework_Assert::assertEquals(route($route), $response->getTargetUrl()); + PHPUnit_Framework_Assert::assertEquals(route($route, $args), $response->getTargetUrl()); } /** diff --git a/tests/Unit/Http/Controllers/Server/SubuserControllerTest.php b/tests/Unit/Http/Controllers/Server/SubuserControllerTest.php new file mode 100644 index 00000000..adfbdf34 --- /dev/null +++ b/tests/Unit/Http/Controllers/Server/SubuserControllerTest.php @@ -0,0 +1,234 @@ +. + * + * 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\Http\Controllers\Server; + +use Illuminate\Contracts\Session\Session; +use Illuminate\Http\Request; +use Mockery as m; +use Prologue\Alerts\AlertsMessageBag; +use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface; +use Pterodactyl\Http\Controllers\Server\SubuserController; +use Pterodactyl\Models\Permission; +use Pterodactyl\Models\Server; +use Pterodactyl\Models\Subuser; +use Pterodactyl\Services\Subusers\SubuserCreationService; +use Pterodactyl\Services\Subusers\SubuserDeletionService; +use Pterodactyl\Services\Subusers\SubuserUpdateService; +use Tests\Assertions\ControllerAssertionsTrait; +use Tests\TestCase; + +class SubuserControllerTest extends TestCase +{ + use ControllerAssertionsTrait; + + /** + * @var \Prologue\Alerts\AlertsMessageBag + */ + protected $alert; + + /** + * @var \Pterodactyl\Http\Controllers\Server\SubuserController + */ + protected $controller; + + /** + * @var \Pterodactyl\Contracts\Repository\SubuserRepositoryInterface + */ + protected $repository; + + /** + * @var \Illuminate\Http\Request + */ + protected $request; + + /** + * @var \Illuminate\Contracts\Session\Session + */ + protected $session; + + /** + * @var \Pterodactyl\Services\Subusers\SubuserCreationService + */ + protected $subuserCreationService; + + /** + * @var \Pterodactyl\Services\Subusers\SubuserDeletionService + */ + protected $subuserDeletionService; + + /** + * @var \Pterodactyl\Services\Subusers\SubuserUpdateService + */ + protected $subuserUpdateService; + + /** + * Setup tests. + */ + public function setUp() + { + parent::setUp(); + + $this->alert = m::mock(AlertsMessageBag::class); + $this->repository = m::mock(SubuserRepositoryInterface::class); + $this->request = m::mock(Request::class); + $this->session = m::mock(Session::class); + $this->subuserCreationService = m::mock(SubuserCreationService::class); + $this->subuserDeletionService = m::mock(SubuserDeletionService::class); + $this->subuserUpdateService = m::mock(SubuserUpdateService::class); + + $this->controller = m::mock(SubuserController::class, [ + $this->alert, + $this->session, + $this->subuserCreationService, + $this->subuserDeletionService, + $this->repository, + $this->subuserUpdateService, + ])->makePartial(); + } + + /* + * Test index controller. + */ + public function testIndexController() + { + $server = factory(Server::class)->make(); + + $this->session->shouldReceive('get')->with('server_data.model')->once()->andReturn($server); + $this->controller->shouldReceive('authorize')->with('list-subusers', $server)->once()->andReturnNull(); + $this->controller->shouldReceive('injectJavascript')->withNoArgs()->once()->andReturnNull(); + $this->repository->shouldReceive('findWhere')->with([['server_id', '=', $server->id]])->once()->andReturn([]); + + $response = $this->controller->index(); + $this->assertIsViewResponse($response); + $this->assertViewNameEquals('server.users.index', $response); + $this->assertViewHasKey('subusers', $response); + } + + /** + * Test view controller. + */ + public function testViewController() + { + $subuser = factory(Subuser::class)->make([ + 'permissions' => collect([ + (object) ['permission' => 'some.permission'], + (object) ['permission' => 'another.permission'], + ]), + ]); + $server = factory(Server::class)->make(); + + $this->session->shouldReceive('get')->with('server_data.model')->once()->andReturn($server); + $this->controller->shouldReceive('authorize')->with('view-subuser', $server)->once()->andReturnNull(); + $this->repository->shouldReceive('getWithPermissions')->with(1234)->once()->andReturn($subuser); + $this->controller->shouldReceive('injectJavascript')->withNoArgs()->once()->andReturnNull(); + + $response = $this->controller->view($server->uuid, 1234); + $this->assertIsViewResponse($response); + $this->assertViewNameEquals('server.users.view', $response); + $this->assertViewHasKey('subuser', $response); + $this->assertViewHasKey('permlist', $response); + $this->assertViewHasKey('permissions', $response); + $this->assertViewKeyEquals('subuser', $subuser, $response); + $this->assertViewKeyEquals('permlist', Permission::getPermissions(), $response); + $this->assertViewKeyEquals('permissions', collect([ + 'some.permission' => true, + 'another.permission' => true, + ]), $response); + } + + /** + * Test the update controller. + */ + public function testUpdateController() + { + $server = factory(Server::class)->make(); + + $this->session->shouldReceive('get')->with('server_data.model')->once()->andReturn($server); + $this->controller->shouldReceive('authorize')->with('edit-subuser', $server)->once()->andReturnNull(); + $this->request->shouldReceive('input')->with('permissions', [])->once()->andReturn(['some.permission']); + $this->subuserUpdateService->shouldReceive('handle')->with(1234, ['some.permission'])->once()->andReturnNull(); + $this->alert->shouldReceive('success')->with(trans('server.users.user_updated'))->once()->andReturnSelf() + ->shouldReceive('flash')->withNoArgs()->once()->andReturnNull(); + + $response = $this->controller->update($this->request, $server->uuid, 1234); + $this->assertIsRedirectResponse($response); + $this->assertRedirectRouteEquals('server.subusers.view', $response, ['uuid' => $server->uuid, 'id' => 1234]); + } + + /** + * Test the create controller. + */ + public function testCreateController() + { + $server = factory(Server::class)->make(); + + $this->session->shouldReceive('get')->with('server_data.model')->once()->andReturn($server); + $this->controller->shouldReceive('authorize')->with('create-subuser', $server)->once()->andReturnNull(); + $this->controller->shouldReceive('injectJavascript')->withNoArgs()->once()->andReturnNull(); + + $response = $this->controller->create(); + $this->assertIsViewResponse($response); + $this->assertViewNameEquals('server.users.new', $response); + $this->assertViewHasKey('permissions', $response); + $this->assertViewKeyEquals('permissions', Permission::getPermissions(), $response); + } + + /** + * Test the store controller. + */ + public function testStoreController() + { + $server = factory(Server::class)->make(); + $subuser = factory(Subuser::class)->make(); + + $this->session->shouldReceive('get')->with('server_data.model')->once()->andReturn($server); + $this->controller->shouldReceive('authorize')->with('create-subuser', $server)->once()->andReturnNull(); + $this->request->shouldReceive('input')->with('email')->once()->andReturn('user@test.com'); + $this->request->shouldReceive('input')->with('permissions', [])->once()->andReturn(['some.permission']); + $this->subuserCreationService->shouldReceive('handle')->with($server, 'user@test.com', ['some.permission'])->once()->andReturn($subuser); + $this->alert->shouldReceive('success')->with(trans('server.users.user_assigned'))->once()->andReturnSelf() + ->shouldReceive('flash')->withNoArgs()->once()->andReturnNull(); + + $response = $this->controller->store($this->request, $server->uuid); + $this->assertIsRedirectResponse($response); + $this->assertRedirectRouteEquals('server.subusers.view', $response, ['uuid' => $server->uuid, 'id' => $subuser->id]); + } + + /** + * Test the delete controller. + */ + public function testDeleteController() + { + $server = factory(Server::class)->make(); + + $this->session->shouldReceive('get')->with('server_data.model')->once()->andReturn($server); + $this->controller->shouldReceive('authorize')->with('delete-subuser', $server)->once()->andReturnNull(); + $this->subuserDeletionService->shouldReceive('handle')->with(1234)->once()->andReturnNull(); + + $response = $this->controller->delete($server->uuid, 1234); + $this->assertIsResponse($response); + $this->assertResponseCodeEquals(204, $response); + } +} diff --git a/tests/Unit/Services/Subusers/SubuserCreationServiceTest.php b/tests/Unit/Services/Subusers/SubuserCreationServiceTest.php index ad29c866..7dcd6792 100644 --- a/tests/Unit/Services/Subusers/SubuserCreationServiceTest.php +++ b/tests/Unit/Services/Subusers/SubuserCreationServiceTest.php @@ -25,6 +25,7 @@ namespace Tests\Unit\Services\Subusers; use Mockery as m; +use Pterodactyl\Exceptions\Repository\RecordNotFoundException; use Tests\TestCase; use Illuminate\Log\Writer; use phpmock\phpunit\PHPMock; @@ -100,6 +101,7 @@ class SubuserCreationServiceTest extends TestCase parent::setUp(); $this->getFunctionMock('\\Pterodactyl\\Services\\Subusers', 'bin2hex')->expects($this->any())->willReturn('bin2hex'); + $this->getFunctionMock('\\Pterodactyl\\Services\\Subusers', 'str_random')->expects($this->any())->willReturn('123456'); $this->connection = m::mock(ConnectionInterface::class); $this->daemonRepository = m::mock(DaemonServerRepositoryInterface::class); @@ -132,16 +134,16 @@ class SubuserCreationServiceTest extends TestCase $user = factory(User::class)->make(); $subuser = factory(Subuser::class)->make(['user_id' => $user->id, 'server_id' => $server->id]); - $this->userRepository->shouldReceive('findWhere')->with([['email', '=', $user->email]])->once()->andReturnNull(); + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->userRepository->shouldReceive('findFirstWhere')->with([['email', '=', $user->email]])->once()->andThrow(new RecordNotFoundException); $this->userCreationService->shouldReceive('handle')->with([ 'email' => $user->email, - 'username' => substr(strtok($user->email, '@'), 0, 8), + 'username' => substr(strtok($user->email, '@'), 0, 8) . '_' . '123456', 'name_first' => 'Server', 'name_last' => 'Subuser', 'root_admin' => false, ])->once()->andReturn($user); - $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); $this->subuserRepository->shouldReceive('create')->with([ 'user_id' => $user->id, 'server_id' => $server->id, @@ -172,13 +174,13 @@ class SubuserCreationServiceTest extends TestCase $user = factory(User::class)->make(); $subuser = factory(Subuser::class)->make(['user_id' => $user->id, 'server_id' => $server->id]); - $this->userRepository->shouldReceive('findWhere')->with([['email', '=', $user->email]])->once()->andReturn($user); + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->userRepository->shouldReceive('findFirstWhere')->with([['email', '=', $user->email]])->once()->andReturn($user); $this->subuserRepository->shouldReceive('findCountWhere')->with([ ['user_id', '=', $user->id], ['server_id', '=', $server->id], ])->once()->andReturn(0); - $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); $this->subuserRepository->shouldReceive('create')->with([ 'user_id' => $user->id, 'server_id' => $server->id, @@ -207,7 +209,8 @@ class SubuserCreationServiceTest extends TestCase $user = factory(User::class)->make(); $server = factory(Server::class)->make(['owner_id' => $user->id]); - $this->userRepository->shouldReceive('findWhere')->with([['email', '=', $user->email]])->once()->andReturn($user); + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->userRepository->shouldReceive('findFirstWhere')->with([['email', '=', $user->email]])->once()->andReturn($user); try { $this->service->handle($server, $user->email, []); @@ -225,7 +228,8 @@ class SubuserCreationServiceTest extends TestCase $user = factory(User::class)->make(); $server = factory(Server::class)->make(); - $this->userRepository->shouldReceive('findWhere')->with([['email', '=', $user->email]])->once()->andReturn($user); + $this->connection->shouldReceive('beginTransaction')->withNoArgs()->once()->andReturnNull(); + $this->userRepository->shouldReceive('findFirstWhere')->with([['email', '=', $user->email]])->once()->andReturn($user); $this->subuserRepository->shouldReceive('findCountWhere')->with([ ['user_id', '=', $user->id], ['server_id', '=', $server->id], From 855b7fa1e4655b69d7f4c3af80015c23f4a0e41f Mon Sep 17 00:00:00 2001 From: Jakob Schrettenbrunner Date: Tue, 5 Sep 2017 01:46:55 +0200 Subject: [PATCH 88/99] fix menu collapse with adminlte 2.4 --- resources/themes/pterodactyl/layouts/admin.blade.php | 2 +- resources/themes/pterodactyl/layouts/master.blade.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/themes/pterodactyl/layouts/admin.blade.php b/resources/themes/pterodactyl/layouts/admin.blade.php index 66dd63d4..b8689693 100644 --- a/resources/themes/pterodactyl/layouts/admin.blade.php +++ b/resources/themes/pterodactyl/layouts/admin.blade.php @@ -61,7 +61,7 @@ {{ Settings::get('company', 'Pterodactyl') }}
    @endsection From 1e94bd51289536f3fbdaedc2596802378e2789f9 Mon Sep 17 00:00:00 2001 From: Jakob Date: Thu, 7 Sep 2017 09:55:18 +0200 Subject: [PATCH 91/99] add particles.js to readme --- README.md | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 393dff90..35c26453 100644 --- a/README.md +++ b/README.md @@ -33,42 +33,44 @@ SOFTWARE. ![](http://static.s3.pterodactyl.io/PhraseApp-parrot.png) A huge thanks to [PhraseApp](https://phraseapp.com) who provide us the software to help translate this project. -Ace Editor - [license](https://github.com/ajaxorg/ace/blob/master/LICENSE) - [homepage](https://ace.c9.io) +Ace Editor — [license](https://github.com/ajaxorg/ace/blob/master/LICENSE) — [homepage](https://ace.c9.io) -AdminLTE - [license](https://github.com/almasaeed2010/AdminLTE/blob/master/LICENSE) - [homepage](https://almsaeedstudio.com) +AdminLTE — [license](https://github.com/almasaeed2010/AdminLTE/blob/master/LICENSE) — [homepage](https://almsaeedstudio.com) -Animate.css - [license](https://github.com/daneden/animate.css/blob/master/LICENSE) - [homepage](http://daneden.github.io/animate.css/) +Animate.css — [license](https://github.com/daneden/animate.css/blob/master/LICENSE) — [homepage](http://daneden.github.io/animate.css/) -AnsiUp - [license](https://github.com/drudru/ansi_up/blob/master/Readme.md#license) - [homepage](https://github.com/drudru/ansi_up) +AnsiUp — [license](https://github.com/drudru/ansi_up/blob/master/Readme.md#license) — [homepage](https://github.com/drudru/ansi_up) -Async.js - [license](https://github.com/caolan/async/blob/master/LICENSE) - [homepage](https://github.com/caolan/async/) +Async.js — [license](https://github.com/caolan/async/blob/master/LICENSE) — [homepage](https://github.com/caolan/async/) -Bootstrap - [license](https://github.com/twbs/bootstrap/blob/master/LICENSE) - [homepage](http://getbootstrap.com) +Bootstrap — [license](https://github.com/twbs/bootstrap/blob/master/LICENSE) — [homepage](http://getbootstrap.com) -BootStrap Notify - [license](https://github.com/mouse0270/bootstrap-notify/blob/master/LICENSE) - [homepage](http://bootstrap-notify.remabledesigns.com) +BootStrap Notify — [license](https://github.com/mouse0270/bootstrap-notify/blob/master/LICENSE) — [homepage](http://bootstrap-notify.remabledesigns.com) -Chart.js - [license](https://github.com/chartjs/Chart.js/blob/master/LICENSE.md) - [homepage](http://www.chartjs.org) +Chart.js — [license](https://github.com/chartjs/Chart.js/blob/master/LICENSE.md) — [homepage](http://www.chartjs.org) -FontAwesome - [license](http://fontawesome.io/license/) - [homepage](http://fontawesome.io) +FontAwesome — [license](http://fontawesome.io/license/) — [homepage](http://fontawesome.io) -FontAwesome Animations - [license](https://github.com/l-lin/font-awesome-animation#license) - [homepage](https://github.com/l-lin/font-awesome-animation) +FontAwesome Animations — [license](https://github.com/l-lin/font-awesome-animation#license) — [homepage](https://github.com/l-lin/font-awesome-animation) -jQuery - [license](https://github.com/jquery/jquery/blob/master/LICENSE.txt) - [homepage](http://jquery.com) +jQuery — [license](https://github.com/jquery/jquery/blob/master/LICENSE.txt) — [homepage](http://jquery.com) -Laravel Framework - [license](https://github.com/laravel/framework/blob/5.4/LICENSE.md) - [homepage](https://laravel.com) +Laravel Framework — [license](https://github.com/laravel/framework/blob/5.4/LICENSE.md) — [homepage](https://laravel.com) -Lodash - [license](https://github.com/lodash/lodash/blob/master/LICENSE) - [homepage](https://lodash.com/) +Lodash — [license](https://github.com/lodash/lodash/blob/master/LICENSE) — [homepage](https://lodash.com/) -Select2 - [license](https://github.com/select2/select2/blob/master/LICENSE.md) - [homepage](https://select2.github.io) +Select2 — [license](https://github.com/select2/select2/blob/master/LICENSE.md) — [homepage](https://select2.github.io) -Socket.io - [license](https://github.com/socketio/socket.io/blob/master/LICENSE) - [homepage](http://socket.io) +Socket.io — [license](https://github.com/socketio/socket.io/blob/master/LICENSE) — [homepage](http://socket.io) -Socket.io File Upload - [license](https://github.com/vote539/socketio-file-upload/blob/master/server.js#L1-L27) - [homepage](https://github.com/vote539/socketio-file-upload) +Socket.io File Upload — [license](https://github.com/vote539/socketio-file-upload/blob/master/server.js#L1-L27) — [homepage](https://github.com/vote539/socketio-file-upload) -SweetAlert - [license](https://github.com/t4t5/sweetalert/blob/master/LICENSE) - [homepage](http://t4t5.github.io/sweetalert/) +SweetAlert — [license](https://github.com/t4t5/sweetalert/blob/master/LICENSE) — [homepage](http://t4t5.github.io/sweetalert/) Typeahead — [license](https://github.com/bassjobsen/Bootstrap-3-Typeahead/blob/master/bootstrap3-typeahead.js) — [homepage](https://github.com/bassjobsen/Bootstrap-3-Typeahead) +particles.js — [license](https://github.com/VincentGarreau/particles.js/blob/master/LICENSE.md) — [homepage](http://vincentgarreau.com/particles.js/) + ### Additional License Information Some Javascript and CSS used within the panel is licensed under a `MIT` or `Apache 2.0`. Please check their respective header files for more information. From 3f380987c1398ab76508311fd3cf57d10e4668a7 Mon Sep 17 00:00:00 2001 From: Jakob Date: Fri, 8 Sep 2017 10:00:36 +0200 Subject: [PATCH 92/99] remove auto enabled query port from minecraft services fix #620 --- database/seeds/MinecraftServiceTableSeeder.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/database/seeds/MinecraftServiceTableSeeder.php b/database/seeds/MinecraftServiceTableSeeder.php index 7ff69b07..7b827a6a 100644 --- a/database/seeds/MinecraftServiceTableSeeder.php +++ b/database/seeds/MinecraftServiceTableSeeder.php @@ -138,7 +138,7 @@ EOF; 'docker_image' => 'quay.io/pterodactyl/core:java', 'config_startup' => '{"done": ")! For help, type ", "userInteraction": [ "Go to eula.txt for more info."]}', 'config_logs' => '{"custom": false, "location": "logs/latest.log"}', - 'config_files' => '{"server.properties":{"parser": "properties", "find":{"server-ip": "0.0.0.0", "enable-query": "true", "server-port": "{{server.build.default.port}}", "query.port": "{{server.build.default.port}}"}}}', + 'config_files' => '{"server.properties":{"parser": "properties", "find":{"server-ip": "0.0.0.0", "server-port": "{{server.build.default.port}}"}}}', 'config_stop' => 'stop', 'config_from' => null, 'startup' => null, @@ -234,7 +234,7 @@ EOF; 'description' => 'For a long time, Minecraft server owners have had a dream that encompasses a free, easy, and reliable way to connect multiple Minecraft servers together. BungeeCord is the answer to said dream. Whether you are a small server wishing to string multiple game-modes together, or the owner of the ShotBow Network, BungeeCord is the ideal solution for you. With the help of BungeeCord, you will be able to unlock your community\'s full potential.', 'docker_image' => 'quay.io/pterodactyl/core:java', 'config_startup' => '{"done": "Listening on ", "userInteraction": [ "Listening on /0.0.0.0:25577"]}', - 'config_files' => '{"config.yml":{"parser": "yaml", "find":{"listeners[0].query_enabled": true, "listeners[0].query_port": "{{server.build.default.port}}", "listeners[0].host": "0.0.0.0:{{server.build.default.port}}", "servers.*.address":{"127.0.0.1": "{{config.docker.interface}}", "localhost": "{{config.docker.interface}}"}}}}', + 'config_files' => '{"config.yml":{"parser": "yaml", "find":{"listeners[0].host": "0.0.0.0:{{server.build.default.port}}", "servers.*.address":{"127.0.0.1": "{{config.docker.interface}}", "localhost": "{{config.docker.interface}}"}}}}', 'config_logs' => '{"custom": false, "location": "proxy.log.0"}', 'config_stop' => 'end', 'config_from' => null, @@ -271,7 +271,7 @@ EOF; 'docker_image' => 'quay.io/pterodactyl/core:java', 'config_startup' => '{"done": ")! For help, type ", "userInteraction": [ "Go to eula.txt for more info."]}', 'config_logs' => '{"custom": false, "location": "logs/latest.log"}', - 'config_files' => '{"server.properties":{"parser": "properties", "find":{"server-ip": "0.0.0.0", "enable-query": "true", "server-port": "{{server.build.default.port}}", "query.port": "{{server.build.default.port}}"}}}', + 'config_files' => '{"server.properties":{"parser": "properties", "find":{"server-ip": "0.0.0.0", "server-port": "{{server.build.default.port}}"}}}', 'config_stop' => 'stop', 'config_from' => null, 'startup' => 'java -Xms128M -Xmx{{SERVER_MEMORY}}M -jar {{SERVER_JARFILE}}', From d3f2059a9c5c83df5d012d148074dae57490b0a5 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 9 Sep 2017 20:54:57 -0500 Subject: [PATCH 93/99] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 393dff90..0ebe001e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ [![Logo Image](https://cdn.pterodactyl.io/logos/Banner%20Logo%20Black@2x.png)](https://pterodactyl.io) +[![Build Status](https://travis-ci.org/Pterodactyl/Panel.svg?branch=develop)](https://travis-ci.org/Pterodactyl/Panel) [![StyleCI](https://styleci.io/repos/47508644/shield?branch=develop)](https://styleci.io/repos/47508644) [![codecov](https://codecov.io/gh/Pterodactyl/Panel/branch/develop/graph/badge.svg)](https://codecov.io/gh/Pterodactyl/Panel) + ## Pterodactyl Panel Pterodactyl Panel is the free, open-source, game agnostic, self-hosted control panel for users, networks, and game service providers. Pterodactyl supports games and servers such as Minecraft (including Spigot, Bungeecord, and Sponge), ARK: Evolution Evolved, CS:GO, Team Fortress 2, Insurgency, Teamspeak 3, Mumble, and many more. Control all of your games from one unified interface. From bab28dbc858951a8f8b942e750a2157a8a823aa7 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sat, 9 Sep 2017 23:55:21 -0500 Subject: [PATCH 94/99] Initial implementation of new task mgmt system :cop: --- .env.example | 3 + .env.travis | 2 + app/Contracts/Extensions/HashidsInterface.php | 41 ++++ .../Repository/TaskRepositoryInterface.php | 47 ++++ app/Extensions/DynamicDatabaseConnection.php | 2 + app/Extensions/Hashids.php | 44 ++++ .../Server/Tasks/TaskManagementController.php | 171 +++++++++++++ .../Server/TaskCreationFormRequest.php | 84 +++++++ app/Models/Task.php | 75 +++++- app/Providers/HashidsServiceProvider.php | 51 ++++ app/Providers/RepositoryServiceProvider.php | 3 + app/Repositories/Eloquent/TaskRepository.php | 74 ++++++ app/Services/Tasks/TaskCreationService.php | 108 ++++++++ app/Services/Tasks/TaskUpdateService.php | 47 ++++ composer.json | 1 + composer.lock | 65 ++++- config/app.php | 2 +- config/hashids.php | 15 ++ config/ide-helper.php | 175 +++++++++++++ ...17_09_09_125253_AddChainedTasksAbility.php | 49 ++++ ..._09_09_162204_AddNullableNextRunColumn.php | 31 +++ .../themes/pterodactyl/js/frontend/tasks.js | 199 ++++++++------- resources/lang/en/server.php | 8 + resources/lang/en/strings.php | 3 + .../pterodactyl/layouts/master.blade.php | 6 +- .../partials/tasks/chain-template.blade.php | 42 ++++ .../pterodactyl/server/tasks/index.blade.php | 26 +- .../pterodactyl/server/tasks/new.blade.php | 47 ++-- .../pterodactyl/server/tasks/view.blade.php | 230 ++++++++++++++++++ resources/themes/pterodactyl/vendor/.gitkeep | 0 routes/server.php | 16 +- 31 files changed, 1535 insertions(+), 132 deletions(-) create mode 100644 app/Contracts/Extensions/HashidsInterface.php create mode 100644 app/Contracts/Repository/TaskRepositoryInterface.php create mode 100644 app/Extensions/Hashids.php create mode 100644 app/Http/Controllers/Server/Tasks/TaskManagementController.php create mode 100644 app/Http/Requests/Server/TaskCreationFormRequest.php create mode 100644 app/Providers/HashidsServiceProvider.php create mode 100644 app/Repositories/Eloquent/TaskRepository.php create mode 100644 app/Services/Tasks/TaskCreationService.php create mode 100644 app/Services/Tasks/TaskUpdateService.php create mode 100644 config/hashids.php create mode 100644 config/ide-helper.php create mode 100644 database/migrations/2017_09_09_125253_AddChainedTasksAbility.php create mode 100644 database/migrations/2017_09_09_162204_AddNullableNextRunColumn.php create mode 100644 resources/themes/pterodactyl/partials/tasks/chain-template.blade.php create mode 100644 resources/themes/pterodactyl/server/tasks/view.blade.php delete mode 100644 resources/themes/pterodactyl/vendor/.gitkeep diff --git a/.env.example b/.env.example index c7a972be..45644374 100644 --- a/.env.example +++ b/.env.example @@ -16,6 +16,9 @@ DB_PASSWORD=secret CACHE_DRIVER=file SESSION_DRIVER=database +HASHIDS_SALT= +HASHIDS_LENGTH=8 + MAIL_DRIVER=smtp MAIL_HOST=mailtrap.io MAIL_PORT=2525 diff --git a/.env.travis b/.env.travis index 1b6ed1af..22a0c304 100644 --- a/.env.travis +++ b/.env.travis @@ -14,3 +14,5 @@ CACHE_DRIVER=array SESSION_DRIVER=array MAIL_DRIVER=array QUEUE_DRIVER=sync + +HASHIDS_SALT=test123 diff --git a/app/Contracts/Extensions/HashidsInterface.php b/app/Contracts/Extensions/HashidsInterface.php new file mode 100644 index 00000000..8630718f --- /dev/null +++ b/app/Contracts/Extensions/HashidsInterface.php @@ -0,0 +1,41 @@ +. + * + * 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\Contracts\Extensions; + +use Hashids\HashidsInterface as VendorHashidsInterface; + +interface HashidsInterface extends VendorHashidsInterface +{ + /** + * Decode an encoded hashid and return the first result. + * + * @param string $encoded + * @param null $default + * @return mixed + * + * @throws \InvalidArgumentException + */ + public function decodeFirst($encoded, $default = null); +} diff --git a/app/Contracts/Repository/TaskRepositoryInterface.php b/app/Contracts/Repository/TaskRepositoryInterface.php new file mode 100644 index 00000000..8d8b32c3 --- /dev/null +++ b/app/Contracts/Repository/TaskRepositoryInterface.php @@ -0,0 +1,47 @@ +. + * + * 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\Contracts\Repository; + +interface TaskRepositoryInterface extends RepositoryInterface +{ + /** + * Return the parent tasks and the count of children attached to that task. + * + * @param int $server + * @return mixed + */ + public function getParentTasksWithChainCount($server); + + /** + * Return a single task for a given server including all of the chained tasks. + * + * @param int $task + * @param int $server + * @return mixed + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function getTaskForServer($task, $server); +} diff --git a/app/Extensions/DynamicDatabaseConnection.php b/app/Extensions/DynamicDatabaseConnection.php index 1e13e231..15328d05 100644 --- a/app/Extensions/DynamicDatabaseConnection.php +++ b/app/Extensions/DynamicDatabaseConnection.php @@ -73,6 +73,8 @@ class DynamicDatabaseConnection * @param string $connection * @param \Pterodactyl\Models\DatabaseHost|int $host * @param string $database + * + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException */ public function set($connection, $host, $database = 'mysql') { diff --git a/app/Extensions/Hashids.php b/app/Extensions/Hashids.php new file mode 100644 index 00000000..49107a5e --- /dev/null +++ b/app/Extensions/Hashids.php @@ -0,0 +1,44 @@ +. + * + * 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\Extensions; + +use Hashids\Hashids as VendorHashids; +use Pterodactyl\Contracts\Extensions\HashidsInterface; + +class Hashids extends VendorHashids implements HashidsInterface +{ + /** + * {@inheritdoc} + */ + public function decodeFirst($encoded, $default = null) + { + $result = $this->decode($encoded); + if (! is_array($result)) { + return $default; + } + + return array_first($result, null, $default); + } +} diff --git a/app/Http/Controllers/Server/Tasks/TaskManagementController.php b/app/Http/Controllers/Server/Tasks/TaskManagementController.php new file mode 100644 index 00000000..80c2e667 --- /dev/null +++ b/app/Http/Controllers/Server/Tasks/TaskManagementController.php @@ -0,0 +1,171 @@ +. + * + * 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\Http\Controllers\Server\Tasks; + +use Illuminate\Contracts\Session\Session; +use Pterodactyl\Http\Controllers\Controller; +use Pterodactyl\Services\Tasks\TaskCreationService; +use Pterodactyl\Contracts\Extensions\HashidsInterface; +use Pterodactyl\Traits\Controllers\JavascriptInjection; +use Pterodactyl\Contracts\Repository\TaskRepositoryInterface; +use Pterodactyl\Http\Requests\Server\TaskCreationFormRequest; + +class TaskManagementController extends Controller +{ + use JavascriptInjection; + + /** + * @var \Pterodactyl\Services\Tasks\TaskCreationService + */ + protected $creationService; + + /** + * @var \Pterodactyl\Contracts\Extensions\HashidsInterface + */ + protected $hashids; + + /** + * @var \Pterodactyl\Contracts\Repository\TaskRepositoryInterface + */ + protected $repository; + + /** + * @var \Illuminate\Contracts\Session\Session + */ + protected $session; + + /** + * TaskManagementController constructor. + * + * @param \Pterodactyl\Contracts\Extensions\HashidsInterface $hashids + * @param \Illuminate\Contracts\Session\Session $session + * @param \Pterodactyl\Services\Tasks\TaskCreationService $creationService + * @param \Pterodactyl\Contracts\Repository\TaskRepositoryInterface $repository + */ + public function __construct( + HashidsInterface $hashids, + Session $session, + TaskCreationService $creationService, + TaskRepositoryInterface $repository + ) { + $this->creationService = $creationService; + $this->hashids = $hashids; + $this->repository = $repository; + $this->session = $session; + } + + /** + * Display the task page listing. + * + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + * + * @throws \Illuminate\Auth\Access\AuthorizationException + */ + public function index() + { + $server = $this->session->get('server_data.model'); + $this->authorize('list-tasks', $server); + $this->injectJavascript(); + + return view('server.tasks.index', [ + 'tasks' => $this->repository->getParentTasksWithChainCount($server->id), + 'actions' => [ + 'command' => trans('server.tasks.actions.command'), + 'power' => trans('server.tasks.actions.power'), + ], + ]); + } + + /** + * Display the task creation page. + * + * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View + * + * @throws \Illuminate\Auth\Access\AuthorizationException + */ + public function create() + { + $server = $this->session->get('server_data.model'); + $this->authorize('create-task', $server); + $this->injectJavascript(); + + return view('server.tasks.new'); + } + + /** + * @param \Pterodactyl\Http\Requests\Server\TaskCreationFormRequest $request + * + * @return \Illuminate\Http\RedirectResponse + * + * @throws \Exception + * @throws \Illuminate\Auth\Access\AuthorizationException + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function store(TaskCreationFormRequest $request) + { + $server = $this->session->get('server_data.model'); + $this->authorize('create-task', $server); + + $task = $this->creationService->handle($server, $request->normalize(), $request->getChainedTasks()); + + return redirect()->route('server.tasks.view', [ + 'server' => $server->uuidShort, + 'task' => $task->id, + ]); + } + + /** + * Return a view to modify task settings. + * + * @param string $uuid + * @param string $task + * @return \Illuminate\View\View + * + * @throws \Illuminate\Auth\Access\AuthorizationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function view($uuid, $task) + { + $server = $this->session->get('server_data.model'); + $this->authorize('edit-task', $server); + $task = $this->repository->getTaskForServer($this->hashids->decodeFirst($task, 0), $server->id); + + $this->injectJavascript([ + 'chained' => $task->chained->map(function ($chain) { + return collect($chain->toArray())->only('action', 'chain_delay', 'data')->all(); + }), + ]); + + return view('server.tasks.view', ['task' => $task]); + } + + public function update(TaskCreationFormRequest $request, $uuid, $task) + { + $server = $this->session->get('server_data.model'); + $this->authorize('edit-task', $server); + $task = $this->repository->getTaskForServer($this->hashids->decodeFirst($task, 0), $server->id); + } +} diff --git a/app/Http/Requests/Server/TaskCreationFormRequest.php b/app/Http/Requests/Server/TaskCreationFormRequest.php new file mode 100644 index 00000000..486e04b3 --- /dev/null +++ b/app/Http/Requests/Server/TaskCreationFormRequest.php @@ -0,0 +1,84 @@ +. + * + * 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\Http\Requests\Server; + +use Pterodactyl\Http\Requests\FrontendUserFormRequest; + +class TaskCreationFormRequest extends FrontendUserFormRequest +{ + /** + * Validation rules to apply to the request. + * + * @return array + */ + public function rules() + { + return [ + 'name' => 'string|max:255', + 'day_of_week' => 'required|string', + 'day_of_month' => 'required|string', + 'hour' => 'required|string', + 'minute' => 'required|string', + 'action' => 'required|string|in:power,command', + 'data' => 'required|string', + 'chain' => 'sometimes|array|size:4', + 'chain.time_value' => 'required_with:chain|max:5', + 'chain.time_interval' => 'required_with:chain|max:5', + 'chain.action' => 'required_with:chain|max:5', + 'chain.payload' => 'required_with:chain|max:5', + 'chain.time_value.*' => 'numeric|between:1,60', + 'chain.time_interval.*' => 'string|in:s,m', + 'chain.action.*' => 'string|in:power,command', + 'chain.payload.*' => 'string', + ]; + } + + /** + * Normalize the request into a format that can be used by the application. + * + * @return array + */ + public function normalize() + { + return $this->only('name', 'day_of_week', 'day_of_month', 'hour', 'minute', 'action', 'data'); + } + + /** + * Return the chained tasks provided in the request. + * + * @return array|null + */ + public function getChainedTasks() + { + $restructured = []; + foreach (array_get($this->all(), 'chain', []) as $key => $values) { + for ($i = 0; $i < count($values); ++$i) { + $restructured[$i][$key] = $values[$i]; + } + } + + return empty($restructured) ? null : $restructured; + } +} diff --git a/app/Models/Task.php b/app/Models/Task.php index 5e44a826..71a58251 100644 --- a/app/Models/Task.php +++ b/app/Models/Task.php @@ -24,10 +24,16 @@ namespace Pterodactyl\Models; +use Sofa\Eloquence\Eloquence; +use Sofa\Eloquence\Validable; use Illuminate\Database\Eloquent\Model; +use Sofa\Eloquence\Contracts\CleansAttributes; +use Sofa\Eloquence\Contracts\Validable as ValidableContract; -class Task extends Model +class Task extends Model implements CleansAttributes, ValidableContract { + use Eloquence, Validable; + /** * The table associated with the model. * @@ -55,6 +61,53 @@ class Task extends Model 'active' => 'boolean', ]; + /** + * Default attributes when creating a new model. + * + * @var array + */ + protected $attributes = [ + 'parent_task_id' => null, + 'chain_order' => null, + 'active' => true, + 'day_of_week' => '*', + 'day_of_month' => '*', + 'hour' => '*', + 'minute' => '*', + 'chain_delay' => null, + 'queued' => false, + ]; + + /** + * @var array + */ + protected static $applicationRules = [ + 'server_id' => 'required', + 'action' => 'required', + 'data' => 'required', + ]; + + /** + * @var array + */ + protected static $dataIntegrityRules = [ + 'name' => 'nullable|string|max:255', + 'parent_task_id' => 'nullable|numeric|exists:tasks,id', + 'chain_order' => 'nullable|numeric|min:1', + 'server_id' => 'numeric|exists:servers,id', + 'active' => 'boolean', + 'action' => 'string', + 'data' => 'string', + 'queued' => 'boolean', + 'day_of_month' => 'string', + 'day_of_week' => 'string', + 'hour' => 'string', + 'minute' => 'string', + 'chain_delay' => 'nullable|numeric|between:1,900', + 'last_run' => 'nullable|timestamp', + 'next_run' => 'nullable|timestamp', + ]; + /** * The attributes that should be mutated to dates. * @@ -62,6 +115,16 @@ class Task extends Model */ protected $dates = ['last_run', 'next_run', 'created_at', 'updated_at']; + /** + * Return a hashid encoded string to represent the ID of the task. + * + * @return string + */ + public function getHashidAttribute() + { + return app()->make('hashids')->encode($this->id); + } + /** * Gets the server associated with a task. * @@ -81,4 +144,14 @@ class Task extends Model { return $this->belongsTo(User::class); } + + /** + * Return chained tasks for a parent task. + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function chained() + { + return $this->hasMany(self::class, 'parent_task_id')->orderBy('chain_order', 'asc'); + } } diff --git a/app/Providers/HashidsServiceProvider.php b/app/Providers/HashidsServiceProvider.php new file mode 100644 index 00000000..ee28104e --- /dev/null +++ b/app/Providers/HashidsServiceProvider.php @@ -0,0 +1,51 @@ +. + * + * 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\Providers; + +use Pterodactyl\Extensions\Hashids; +use Illuminate\Support\ServiceProvider; +use Pterodactyl\Contracts\Extensions\HashidsInterface; + +class HashidsServiceProvider extends ServiceProvider +{ + /** + * Register the ability to use Hashids. + */ + public function register() + { + $this->app->singleton(HashidsInterface::class, function () { + /** @var \Illuminate\Contracts\Config\Repository $config */ + $config = $this->app['config']; + + return new Hashids( + $config->get('hashids.salt', ''), + $config->get('hashids.length', 0), + $config->get('hashids.alphabet', 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890') + ); + }); + + $this->app->alias(HashidsInterface::class, 'hashids'); + } +} diff --git a/app/Providers/RepositoryServiceProvider.php b/app/Providers/RepositoryServiceProvider.php index 09c1c295..f44715df 100644 --- a/app/Providers/RepositoryServiceProvider.php +++ b/app/Providers/RepositoryServiceProvider.php @@ -29,6 +29,7 @@ use Pterodactyl\Repositories\Daemon\FileRepository; use Pterodactyl\Repositories\Daemon\PowerRepository; use Pterodactyl\Repositories\Eloquent\NodeRepository; use Pterodactyl\Repositories\Eloquent\PackRepository; +use Pterodactyl\Repositories\Eloquent\TaskRepository; use Pterodactyl\Repositories\Eloquent\UserRepository; use Pterodactyl\Repositories\Daemon\CommandRepository; use Pterodactyl\Repositories\Eloquent\ApiKeyRepository; @@ -43,6 +44,7 @@ use Pterodactyl\Repositories\Eloquent\PermissionRepository; use Pterodactyl\Repositories\Daemon\ConfigurationRepository; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Contracts\Repository\PackRepositoryInterface; +use Pterodactyl\Contracts\Repository\TaskRepositoryInterface; use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Repositories\Eloquent\DatabaseHostRepository; use Pterodactyl\Repositories\Eloquent\ApiPermissionRepository; @@ -97,6 +99,7 @@ class RepositoryServiceProvider extends ServiceProvider $this->app->bind(ServiceVariableRepositoryInterface::class, ServiceVariableRepository::class); $this->app->bind(SessionRepositoryInterface::class, SessionRepository::class); $this->app->bind(SubuserRepositoryInterface::class, SubuserRepository::class); + $this->app->bind(TaskRepositoryInterface::class, TaskRepository::class); $this->app->bind(UserRepositoryInterface::class, UserRepository::class); // Daemon Repositories diff --git a/app/Repositories/Eloquent/TaskRepository.php b/app/Repositories/Eloquent/TaskRepository.php new file mode 100644 index 00000000..3cc5b10a --- /dev/null +++ b/app/Repositories/Eloquent/TaskRepository.php @@ -0,0 +1,74 @@ +. + * + * 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\Repositories\Eloquent; + +use Pterodactyl\Models\Task; +use Webmozart\Assert\Assert; +use Pterodactyl\Contracts\Repository\TaskRepositoryInterface; +use Pterodactyl\Exceptions\Repository\RecordNotFoundException; + +class TaskRepository extends EloquentRepository implements TaskRepositoryInterface +{ + /** + * {@inheritdoc} + */ + public function model() + { + return Task::class; + } + + /** + * {@inheritdoc} + */ + public function getParentTasksWithChainCount($server) + { + Assert::numeric($server, 'First argument passed to GetParentTasksWithChainCount must be numeric, received %s.'); + + return $this->getBuilder()->withCount('chained')->where([ + ['server_id', '=', $server], + ['parent_task_id', '=', null], + ])->get($this->getColumns()); + } + + /** + * {@inheritdoc} + */ + public function getTaskForServer($task, $server) + { + Assert::numeric($task, 'First argument passed to getTaskForServer must be numeric, received %s.'); + Assert::numeric($server, 'Second argument passed to getTaskForServer must be numeric, received %s.'); + + $instance = $this->getBuilder()->with('chained')->where([ + ['server_id', '=', $server], + ['parent_task_id', '=', null], + ])->find($task, $this->getColumns()); + + if (! $instance) { + throw new RecordNotFoundException; + } + + return $instance; + } +} diff --git a/app/Services/Tasks/TaskCreationService.php b/app/Services/Tasks/TaskCreationService.php new file mode 100644 index 00000000..d7549bf0 --- /dev/null +++ b/app/Services/Tasks/TaskCreationService.php @@ -0,0 +1,108 @@ +. + * + * 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\Tasks; + +use Pterodactyl\Models\Server; +use Illuminate\Database\ConnectionInterface; +use Pterodactyl\Contracts\Repository\TaskRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; + +class TaskCreationService +{ + /** + * @var \Illuminate\Database\ConnectionInterface + */ + protected $connection; + + /** + * @var \Pterodactyl\Contracts\Repository\TaskRepositoryInterface + */ + protected $repository; + + /** + * @var \Pterodactyl\Contracts\Repository\ServerRepositoryInterface + */ + protected $serverRepository; + + /** + * TaskCreationService constructor. + * + * @param \Illuminate\Database\ConnectionInterface $connection + * @param \Pterodactyl\Contracts\Repository\ServerRepositoryInterface $serverRepository + * @param \Pterodactyl\Contracts\Repository\TaskRepositoryInterface $repository + */ + public function __construct( + ConnectionInterface $connection, + ServerRepositoryInterface $serverRepository, + TaskRepositoryInterface $repository + ) { + $this->connection = $connection; + $this->repository = $repository; + $this->serverRepository = $serverRepository; + } + + /** + * @param int|\Pterodactyl\Models\Server $server + * @param array $data + * @param array|null $chain + * @return \Pterodactyl\Models\Task + * + * @throws \Exception + * @throws \Pterodactyl\Exceptions\Model\DataValidationException + * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + */ + public function handle($server, array $data, array $chain = null) + { + if (! $server instanceof Server) { + $server = $this->serverRepository->find($server); + } + + $this->connection->beginTransaction(); + + $data['server_id'] = $server->id; + $task = $this->repository->create($data); + + if (is_array($chain)) { + foreach ($chain as $index => $values) { + if ($values['time_interval'] === 'm' && $values['time_value'] > 15) { + throw new \Exception('I should fix this.'); + } + + $delay = $values['time_interval'] === 'm' ? $values['time_value'] * 60 : $values['time_value']; + $this->repository->withoutFresh()->create([ + 'parent_task_id' => $task->id, + 'chain_order' => $index + 1, + 'server_id' => $server->id, + 'action' => $values['action'], + 'data' => $values['payload'], + 'chain_delay' => $delay, + ]); + } + } + $this->connection->commit(); + + return $task; + } +} diff --git a/app/Services/Tasks/TaskUpdateService.php b/app/Services/Tasks/TaskUpdateService.php new file mode 100644 index 00000000..af227fdc --- /dev/null +++ b/app/Services/Tasks/TaskUpdateService.php @@ -0,0 +1,47 @@ +. + * + * 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\Tasks; + +use Pterodactyl\Contracts\Repository\TaskRepositoryInterface; +use Pterodactyl\Contracts\Repository\ServerRepositoryInterface; + +class TaskUpdateService +{ + protected $repository; + + protected $serverRepository; + + public function __construct( + ServerRepositoryInterface $serverRepository, + TaskRepositoryInterface $repository + ) { + $this->repository = $repository; + $this->serverRepository = $serverRepository; + } + + public function handle($server, array $data, array $chain = null) + { + } +} diff --git a/composer.json b/composer.json index 94ccfeee..a81baeec 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,7 @@ "edvinaskrucas/settings": "^2.0", "fideloper/proxy": "^3.3", "guzzlehttp/guzzle": "~6.3.0", + "hashids/hashids": "^2.0", "igaster/laravel-theme": "^1.16", "laracasts/utilities": "^3.0", "laravel/framework": "5.4.27", diff --git a/composer.lock b/composer.lock index eecee6ea..5b4809a0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "a0014dfc711e382fff7903d9aeaffc25", + "content-hash": "15a4dc6de122bc1e47d1d9ca3b1224d6", "packages": [ { "name": "aws/aws-sdk-php", @@ -1019,6 +1019,69 @@ ], "time": "2017-03-20T17:10:46+00:00" }, + { + "name": "hashids/hashids", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/ivanakimov/hashids.php.git", + "reference": "28889ed83cdc91f4a55637daff0fb5c799eb324e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ivanakimov/hashids.php/zipball/28889ed83cdc91f4a55637daff0fb5c799eb324e", + "reference": "28889ed83cdc91f4a55637daff0fb5c799eb324e", + "shasum": "" + }, + "require": { + "ext-bcmath": "*", + "php": "^5.6.4 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^5.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "psr-4": { + "Hashids\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ivan Akimov", + "email": "ivan@barreleye.com", + "homepage": "https://twitter.com/IvanAkimov" + }, + { + "name": "Vincent Klaiber", + "email": "hello@vinkla.com", + "homepage": "https://vinkla.com" + } + ], + "description": "Generate short, unique, non-sequential ids (like YouTube and Bitly) from numbers", + "homepage": "http://hashids.org/php", + "keywords": [ + "bitly", + "decode", + "encode", + "hash", + "hashid", + "hashids", + "ids", + "obfuscate", + "youtube" + ], + "time": "2017-01-01T13:33:33+00:00" + }, { "name": "igaster/laravel-theme", "version": "v1.16", diff --git a/config/app.php b/config/app.php index b0e36cbc..e3cb7170 100644 --- a/config/app.php +++ b/config/app.php @@ -161,6 +161,7 @@ return [ Pterodactyl\Providers\AppServiceProvider::class, Pterodactyl\Providers\AuthServiceProvider::class, Pterodactyl\Providers\EventServiceProvider::class, + Pterodactyl\Providers\HashidsServiceProvider::class, Pterodactyl\Providers\RouteServiceProvider::class, Pterodactyl\Providers\MacroServiceProvider::class, Pterodactyl\Providers\PhraseAppTranslationProvider::class, @@ -237,7 +238,6 @@ return [ 'URL' => Illuminate\Support\Facades\URL::class, 'Uuid' => Webpatser\Uuid\Uuid::class, 'Validator' => Illuminate\Support\Facades\Validator::class, - 'Version' => Pterodactyl\Facades\Version::class, 'View' => Illuminate\Support\Facades\View::class, ], ]; diff --git a/config/hashids.php b/config/hashids.php new file mode 100644 index 00000000..199de1a3 --- /dev/null +++ b/config/hashids.php @@ -0,0 +1,15 @@ + env('HASHIDS_SALT'), + 'length' => env('HASHIDS_LENGTH', 8), + 'alphabet' => env('HASHIDS_ALPHABET', 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'), +]; diff --git a/config/ide-helper.php b/config/ide-helper.php new file mode 100644 index 00000000..9f10873f --- /dev/null +++ b/config/ide-helper.php @@ -0,0 +1,175 @@ + '_ide_helper', + 'format' => 'php', + + /* + |-------------------------------------------------------------------------- + | Fluent helpers + |-------------------------------------------------------------------------- + | + | Set to true to generate commonly used Fluent methods + | + */ + + 'include_fluent' => true, + + /* + |-------------------------------------------------------------------------- + | Write Model Magic methods + |-------------------------------------------------------------------------- + | + | Set to false to disable write magic methods of model + | + */ + + 'write_model_magic_where' => true, + + /* + |-------------------------------------------------------------------------- + | Helper files to include + |-------------------------------------------------------------------------- + | + | Include helper files. By default not included, but can be toggled with the + | -- helpers (-H) option. Extra helper files can be included. + | + */ + + 'include_helpers' => false, + + 'helper_files' => [ + base_path() . '/vendor/laravel/framework/src/Illuminate/Support/helpers.php', + ], + + /* + |-------------------------------------------------------------------------- + | Model locations to include + |-------------------------------------------------------------------------- + | + | Define in which directories the ide-helper:models command should look + | for models. + | + */ + + 'model_locations' => [ + 'app/Models', + ], + + /* + |-------------------------------------------------------------------------- + | Extra classes + |-------------------------------------------------------------------------- + | + | These implementations are not really extended, but called with magic functions + | + */ + + 'extra' => [ + 'Eloquent' => ['Illuminate\Database\Eloquent\Builder', 'Illuminate\Database\Query\Builder'], + 'Session' => ['Illuminate\Session\Store'], + ], + + 'magic' => [ + 'Log' => [ + 'debug' => 'Monolog\Logger::addDebug', + 'info' => 'Monolog\Logger::addInfo', + 'notice' => 'Monolog\Logger::addNotice', + 'warning' => 'Monolog\Logger::addWarning', + 'error' => 'Monolog\Logger::addError', + 'critical' => 'Monolog\Logger::addCritical', + 'alert' => 'Monolog\Logger::addAlert', + 'emergency' => 'Monolog\Logger::addEmergency', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Interface implementations + |-------------------------------------------------------------------------- + | + | These interfaces will be replaced with the implementing class. Some interfaces + | are detected by the helpers, others can be listed below. + | + */ + + 'interfaces' => [ + ], + + /* + |-------------------------------------------------------------------------- + | Support for custom DB types + |-------------------------------------------------------------------------- + | + | This setting allow you to map any custom database type (that you may have + | created using CREATE TYPE statement or imported using database plugin + | / extension to a Doctrine type. + | + | Each key in this array is a name of the Doctrine2 DBAL Platform. Currently valid names are: + | 'postgresql', 'db2', 'drizzle', 'mysql', 'oracle', 'sqlanywhere', 'sqlite', 'mssql' + | + | This name is returned by getName() method of the specific Doctrine/DBAL/Platforms/AbstractPlatform descendant + | + | The value of the array is an array of type mappings. Key is the name of the custom type, + | (for example, "jsonb" from Postgres 9.4) and the value is the name of the corresponding Doctrine2 type (in + | our case it is 'json_array'. Doctrine types are listed here: + | http://doctrine-dbal.readthedocs.org/en/latest/reference/types.html + | + | So to support jsonb in your models when working with Postgres, just add the following entry to the array below: + | + | "postgresql" => array( + | "jsonb" => "json_array", + | ), + | + */ + 'custom_db_types' => [ + ], + + /* + |-------------------------------------------------------------------------- + | Support for camel cased models + |-------------------------------------------------------------------------- + | + | There are some Laravel packages (such as Eloquence) that allow for accessing + | Eloquent model properties via camel case, instead of snake case. + | + | Enabling this option will support these packages by saving all model + | properties as camel case, instead of snake case. + | + | For example, normally you would see this: + | + | * @property \Carbon\Carbon $created_at + | * @property \Carbon\Carbon $updated_at + | + | With this enabled, the properties will be this: + | + | * @property \Carbon\Carbon $createdAt + | * @property \Carbon\Carbon $updatedAt + | + | Note, it is currently an all-or-nothing option. + | + */ + 'model_camel_case_properties' => false, + + /* + |-------------------------------------------------------------------------- + | Property Casts + |-------------------------------------------------------------------------- + | + | Cast the given "real type" to the given "type". + | + */ + 'type_overrides' => [ + 'integer' => 'int', + 'boolean' => 'bool', + ], +]; diff --git a/database/migrations/2017_09_09_125253_AddChainedTasksAbility.php b/database/migrations/2017_09_09_125253_AddChainedTasksAbility.php new file mode 100644 index 00000000..8748e409 --- /dev/null +++ b/database/migrations/2017_09_09_125253_AddChainedTasksAbility.php @@ -0,0 +1,49 @@ +unsignedInteger('parent_task_id')->after('id')->nullable(); + $table->unsignedInteger('chain_order')->after('parent_task_id')->nullable(); + $table->unsignedInteger('chain_delay')->after('minute')->nullable(); + $table->string('name')->after('server_id')->nullable(); + + $table->foreign('parent_task_id')->references('id')->on('tasks')->onDelete('cascade'); + $table->index(['parent_task_id', 'chain_order']); + + $table->dropForeign(['user_id']); + $table->dropColumn('user_id'); + $table->dropColumn('year'); + $table->dropColumn('month'); + }); + } + + /** + * Reverse the migrations. + */ + public function down() + { + Schema::table('tasks', function (Blueprint $table) { + $table->dropForeign(['parent_task_id']); + $table->dropIndex(['parent_task_id', 'chain_order']); + $table->dropColumn('parent_task_id'); + $table->dropColumn('chain_order'); + $table->dropColumn('chain_delay'); + $table->dropColumn('name'); + + $table->unsignedInteger('user_id')->after('id')->nullable(); + $table->string('year')->after('queued')->default('*'); + $table->string('month')->after('year')->default('*'); + $table->foreign('user_id')->references('id')->on('users'); + }); + } +} diff --git a/database/migrations/2017_09_09_162204_AddNullableNextRunColumn.php b/database/migrations/2017_09_09_162204_AddNullableNextRunColumn.php new file mode 100644 index 00000000..40adb401 --- /dev/null +++ b/database/migrations/2017_09_09_162204_AddNullableNextRunColumn.php @@ -0,0 +1,31 @@ +wrapTable('tasks'); + DB::statement('ALTER TABLE ' . $table . ' CHANGE `next_run` `next_run` TIMESTAMP NULL;'); + }); + } + + /** + * Reverse the migrations. + */ + public function down() + { + Schema::table('tasks', function (Blueprint $table) { + $table = DB::getQueryGrammar()->wrapTable('tasks'); + DB::statement('ALTER TABLE ' . $table . ' CHANGE `next_run` `next_run` TIMESTAMP NOT NULL;'); + }); + } +} diff --git a/public/themes/pterodactyl/js/frontend/tasks.js b/public/themes/pterodactyl/js/frontend/tasks.js index b19e1955..8b600a26 100644 --- a/public/themes/pterodactyl/js/frontend/tasks.js +++ b/public/themes/pterodactyl/js/frontend/tasks.js @@ -18,98 +18,113 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -var Tasks = (function () { - - function initTaskFunctions() { - $('[data-action="delete-task"]').click(function (event) { - var self = $(this); - swal({ - type: 'error', - title: 'Delete Task?', - text: 'Are you sure you want to delete this task? There is no undo.', - showCancelButton: true, - allowOutsideClick: true, - closeOnConfirm: false, - confirmButtonText: 'Delete Task', - confirmButtonColor: '#d9534f', - showLoaderOnConfirm: true - }, function () { - $.ajax({ - method: 'DELETE', - url: Router.route('server.tasks.delete', { - server: Pterodactyl.server.uuidShort, - id: self.data('id'), - }), - headers: { - 'X-CSRF-TOKEN': $('meta[name="_token"]').attr('content'), - } - }).done(function (data) { - swal({ - type: 'success', - title: '', - text: 'Task has been deleted.' - }); - self.parent().parent().slideUp(); - }).fail(function (jqXHR) { - console.error(jqXHR); - swal({ - type: 'error', - title: 'Whoops!', - text: 'An error occured while attempting to delete this task.' - }); - }); - }); - }); - $('[data-action="toggle-task"]').click(function (event) { - var self = $(this); - swal({ - type: 'info', - title: 'Toggle Task', - text: 'This will toggle the selected task.', - showCancelButton: true, - allowOutsideClick: true, - closeOnConfirm: false, - confirmButtonText: 'Continue', - showLoaderOnConfirm: true - }, function () { - $.ajax({ - method: 'POST', - url: Router.route('server.tasks.toggle', { - server: Pterodactyl.server.uuidShort, - id: self.data('id'), - }), - headers: { - 'X-CSRF-TOKEN': $('meta[name="_token"]').attr('content'), - } - }).done(function (data) { - swal({ - type: 'success', - title: '', - text: 'Task has been toggled.' - }); - if (data.status !== 1) { - self.parent().parent().addClass('muted muted-hover'); - } else { - self.parent().parent().removeClass('muted muted-hover'); - } - }).fail(function (jqXHR) { - console.error(jqXHR); - swal({ - type: 'error', - title: 'Whoops!', - text: 'An error occured while attempting to toggle this task.' - }); - }); - }); - }); - } - - return { - init: function () { - initTaskFunctions(); +$(document).ready(function () { + $('select[name="action"]').select2(); + $('[data-action="update-field"]').on('change', function (event) { + event.preventDefault(); + var updateField = $(this).data('field'); + var selected = $(this).map(function (i, opt) { + return $(opt).val(); + }).toArray(); + if (selected.length === $(this).find('option').length) { + $('input[name=' + updateField + ']').val('*'); + } else { + $('input[name=' + updateField + ']').val(selected.join(',')); } - } + }); -})(); + $('button[data-action="add-chain"]').on('click', function () { + var clone = $('div[data-target="chain-clone"]').clone(); + clone.insertBefore('#chainLastSegment').removeAttr('data-target').removeClass('hidden'); + clone.find('select[name="chain[time_value][]"]').select2(); + clone.find('select[name="chain[time_interval][]"]').select2(); + clone.find('select[name="chain[action][]"]').select2(); + clone.find('button[data-action="remove-chain-element"]').on('click', function () { + clone.remove(); + }); + $(this).data('element', clone); + }); -Tasks.init(); + $('[data-action="delete-task"]').click(function () { + var self = $(this); + swal({ + type: 'error', + title: 'Delete Task?', + text: 'Are you sure you want to delete this task? There is no undo.', + showCancelButton: true, + allowOutsideClick: true, + closeOnConfirm: false, + confirmButtonText: 'Delete Task', + confirmButtonColor: '#d9534f', + showLoaderOnConfirm: true + }, function () { + $.ajax({ + method: 'DELETE', + url: Router.route('server.tasks.delete', { + server: Pterodactyl.server.uuidShort, + id: self.data('id'), + }), + headers: { + 'X-CSRF-TOKEN': $('meta[name="_token"]').attr('content'), + } + }).done(function (data) { + swal({ + type: 'success', + title: '', + text: 'Task has been deleted.' + }); + self.parent().parent().slideUp(); + }).fail(function (jqXHR) { + console.error(jqXHR); + swal({ + type: 'error', + title: 'Whoops!', + text: 'An error occured while attempting to delete this task.' + }); + }); + }); + }); + + $('[data-action="toggle-task"]').click(function (event) { + var self = $(this); + swal({ + type: 'info', + title: 'Toggle Task', + text: 'This will toggle the selected task.', + showCancelButton: true, + allowOutsideClick: true, + closeOnConfirm: false, + confirmButtonText: 'Continue', + showLoaderOnConfirm: true + }, function () { + $.ajax({ + method: 'POST', + url: Router.route('server.tasks.toggle', { + server: Pterodactyl.server.uuidShort, + id: self.data('id'), + }), + headers: { + 'X-CSRF-TOKEN': $('meta[name="_token"]').attr('content'), + } + }).done(function (data) { + swal({ + type: 'success', + title: '', + text: 'Task has been toggled.' + }); + if (data.status !== 1) { + self.parent().parent().addClass('muted muted-hover'); + } else { + self.parent().parent().removeClass('muted muted-hover'); + } + }).fail(function (jqXHR) { + console.error(jqXHR); + swal({ + type: 'error', + title: 'Whoops!', + text: 'An error occured while attempting to toggle this task.' + }); + }); + }); + }); +}); diff --git a/resources/lang/en/server.php b/resources/lang/en/server.php index e62b94d3..8e750324 100644 --- a/resources/lang/en/server.php +++ b/resources/lang/en/server.php @@ -19,6 +19,7 @@ return [ 'new' => [ 'header' => 'New Task', 'header_sub' => 'Create a new scheduled task for this server.', + 'task_name' => 'Task Name', 'day_of_week' => 'Day of Week', 'custom' => 'Custom Value', 'day_of_month' => 'Day of Month', @@ -33,9 +34,16 @@ return [ 'sat' => 'Saturday', 'submit' => 'Create Task', 'type' => 'Task Type', + 'chain_then' => 'Then, After', + 'chain_do' => 'Do', + 'chain_arguments' => 'With Arguments', 'payload' => 'Task Payload', 'payload_help' => 'For example, if you selected Send Command enter the command here. If you selected Send Power Option put the power action here (e.g. restart).', ], + 'edit' => [ + 'header' => 'Manage Task', + 'submit' => 'Update Task', + ], ], 'users' => [ 'header' => 'Manage Users', diff --git a/resources/lang/en/strings.php b/resources/lang/en/strings.php index 864cdf2e..8cdb400c 100644 --- a/resources/lang/en/strings.php +++ b/resources/lang/en/strings.php @@ -71,4 +71,7 @@ return [ 'admin' => 'Admin', 'subuser' => 'Subuser', 'captcha_invalid' => 'The provided captcha is invalid.', + 'child_tasks' => 'Child Tasks', + 'seconds' => 'Seconds', + 'minutes' => 'Minutes', ]; diff --git a/resources/themes/pterodactyl/layouts/master.blade.php b/resources/themes/pterodactyl/layouts/master.blade.php index 6199f3d8..df5b4284 100644 --- a/resources/themes/pterodactyl/layouts/master.blade.php +++ b/resources/themes/pterodactyl/layouts/master.blade.php @@ -146,7 +146,7 @@ @endcan @can('list-subusers', $server)
  • @@ -157,7 +157,7 @@ @endcan @can('list-tasks', $server)
  • @@ -171,7 +171,7 @@ @endcan @if(Gate::allows('view-startup', $server) || Gate::allows('view-sftp', $server) || Gate::allows('view-databases', $server) || Gate::allows('view-allocation', $server))
  • diff --git a/resources/themes/pterodactyl/partials/tasks/chain-template.blade.php b/resources/themes/pterodactyl/partials/tasks/chain-template.blade.php new file mode 100644 index 00000000..0ba47e2c --- /dev/null +++ b/resources/themes/pterodactyl/partials/tasks/chain-template.blade.php @@ -0,0 +1,42 @@ +@section('tasks::chain-template') + +@show diff --git a/resources/themes/pterodactyl/server/tasks/index.blade.php b/resources/themes/pterodactyl/server/tasks/index.blade.php index 4ed9654c..7c70f514 100644 --- a/resources/themes/pterodactyl/server/tasks/index.blade.php +++ b/resources/themes/pterodactyl/server/tasks/index.blade.php @@ -46,9 +46,9 @@ - - - + + + @@ -56,25 +56,35 @@ @foreach($tasks as $task) active)class="muted muted-hover"@endif> - - + +
    @lang('strings.action')@lang('strings.data')@lang('strings.queued')@lang('strings.name')@lang('strings.queued')@lang('strings.child_tasks') @lang('strings.last_run') @lang('strings.next_run')
    {{ $actions[$task->action] }}{{ $task->data }} + @can('edit-task', $server) + {{ $task->name }} + @else + {{ $task->name }} + @endcan + @if ($task->queued) @lang('strings.yes') @else @lang('strings.no') @endif {{ $task->chained_count }} @if($task->last_run) {{ Carbon::parse($task->last_run)->toDayDateTimeString() }}
    ({{ Carbon::parse($task->last_run)->diffForHumans() }}) - @else - @lang('strings.not_run_yet') + @else + @lang('strings.not_run_yet') @endif
    @if($task->active !== 0) - {{ Carbon::parse($task->next_run)->toDayDateTimeString() }}
    ({{ Carbon::parse($task->next_run)->diffForHumans() }}) + @if($task->last_run) + {{ Carbon::parse($task->next_run)->toDayDateTimeString() }}
    ({{ Carbon::parse($task->next_run)->diffForHumans() }}) + @else + @lang('strings.not_run_yet') + @endif @else n/a @endif diff --git a/resources/themes/pterodactyl/server/tasks/new.blade.php b/resources/themes/pterodactyl/server/tasks/new.blade.php index 045d2fe8..baa69b87 100644 --- a/resources/themes/pterodactyl/server/tasks/new.blade.php +++ b/resources/themes/pterodactyl/server/tasks/new.blade.php @@ -41,6 +41,22 @@ @section('content')
    +
    +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    +
    @@ -176,33 +192,26 @@
    - - +
    +@include('partials.tasks.chain-template') @endsection @section('footer-scripts') @parent {!! Theme::js('js/frontend/server.socket.js') !!} {!! Theme::js('vendor/select2/select2.full.min.js') !!} - + {!! Theme::js('js/frontend/tasks.js') !!} @endsection diff --git a/resources/themes/pterodactyl/server/tasks/view.blade.php b/resources/themes/pterodactyl/server/tasks/view.blade.php new file mode 100644 index 00000000..b58106be --- /dev/null +++ b/resources/themes/pterodactyl/server/tasks/view.blade.php @@ -0,0 +1,230 @@ +{{-- Copyright (c) 2015 - 2017 Dane Everitt --}} + +{{-- 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. --}} +@extends('layouts.master') + +@section('title') + @lang('server.tasks.edit.header') +@endsection + +@section('scripts') + {{-- This has to be loaded before the AdminLTE theme to avoid dropdown issues. --}} + {!! Theme::css('vendor/select2/select2.min.css') !!} + @parent +@endsection + +@section('content-header') +

    @lang('server.tasks.edit.header'){{ $task->name }}

    + +@endsection + +@section('content') +
    +
    +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    @lang('server.tasks.new.day_of_week')

    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    @lang('server.tasks.new.day_of_month')

    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    @lang('server.tasks.new.hour')

    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +

    @lang('server.tasks.new.minute')

    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    + +
    +
    +
    + +
    + + @lang('server.tasks.new.payload_help') +
    +
    +
    +
    + +
    +
    +
    +
    +@include('partials.tasks.chain-template') +@endsection + +@section('footer-scripts') + @parent + {!! Theme::js('js/frontend/server.socket.js') !!} + {!! Theme::js('vendor/select2/select2.full.min.js') !!} + {!! Theme::js('js/frontend/tasks.js') !!} + +@endsection diff --git a/resources/themes/pterodactyl/vendor/.gitkeep b/resources/themes/pterodactyl/vendor/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/routes/server.php b/routes/server.php index a79309c2..3d479d4c 100644 --- a/routes/server.php +++ b/routes/server.php @@ -79,7 +79,7 @@ Route::group(['prefix' => 'users'], function () { Route::patch('/view/{subuser}', 'SubuserController@update')->middleware(SubuserAccess::class); - Route::delete('/delete/{subuser}', 'SubuserController@delete')->middleware(SubuserAccess::class)->name('server.subusers.delete'); + Route::delete('/view/{subuser}/delete', 'SubuserController@delete')->middleware(SubuserAccess::class)->name('server.subusers.delete'); }); /* @@ -91,13 +91,16 @@ Route::group(['prefix' => 'users'], function () { | */ Route::group(['prefix' => 'tasks'], function () { - Route::get('/', 'TaskController@index')->name('server.tasks'); - Route::get('/new', 'TaskController@create')->name('server.tasks.new'); + Route::get('/', 'Tasks\TaskManagementController@index')->name('server.tasks'); + Route::get('/new', 'Tasks\TaskManagementController@create')->name('server.tasks.new'); + Route::get('/view/{task}', 'Tasks\TaskManagementController@view')->name('server.tasks.view'); - Route::post('/new', 'TaskController@store'); - Route::post('/toggle/{id}', 'TaskController@toggle')->name('server.tasks.toggle'); + Route::post('/new', 'Tasks\TaskManagementController@store'); - Route::delete('/delete/{id}', 'TaskController@delete')->name('server.tasks.delete'); + Route::patch('/view/{task}', 'Tasks\TaskManagementController@update'); + Route::patch('/view/{task}/toggle', 'Tasks\ToggleTaskController@index')->name('server.tasks.toggle'); + + Route::delete('/view/{task}/delete', 'Tasks\TaskManagementController@delete')->name('server.tasks.delete'); }); /* @@ -109,6 +112,5 @@ Route::group(['prefix' => 'tasks'], function () { | */ Route::group(['prefix' => 'ajax'], function () { - Route::post('/set-primary', 'AjaxController@postSetPrimary')->name('server.ajax.set-primary'); Route::post('/settings/reset-database-password', 'AjaxController@postResetDatabasePassword')->name('server.ajax.reset-database-password'); }); From 7b454980ab38a8c506ac4761a82cfe94526d0a12 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 10 Sep 2017 23:45:27 -0500 Subject: [PATCH 95/99] Fix version display in node list --- app/Http/ViewComposers/VersionComposer.php | 56 +++++++++++++++++++ app/Providers/ViewComposerServiceProvider.php | 2 + .../admin/nodes/view/index.blade.php | 2 +- 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 app/Http/ViewComposers/VersionComposer.php diff --git a/app/Http/ViewComposers/VersionComposer.php b/app/Http/ViewComposers/VersionComposer.php new file mode 100644 index 00000000..5c066b55 --- /dev/null +++ b/app/Http/ViewComposers/VersionComposer.php @@ -0,0 +1,56 @@ +. + * + * 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\Http\ViewComposers; + +use Illuminate\View\View; +use Pterodactyl\Services\Helpers\SoftwareVersionService; + +class VersionComposer +{ + /** + * @var \Pterodactyl\Services\Helpers\SoftwareVersionService + */ + protected $version; + + /** + * VersionComposer constructor. + * + * @param \Pterodactyl\Services\Helpers\SoftwareVersionService $version + */ + public function __construct(SoftwareVersionService $version) + { + $this->version = $version; + } + + /** + * Attach server data to a view automatically. + * + * @param \Illuminate\View\View $view + */ + public function compose(View $view) + { + $view->with('version', $this->version); + } +} diff --git a/app/Providers/ViewComposerServiceProvider.php b/app/Providers/ViewComposerServiceProvider.php index df1648f1..1e4b77f1 100644 --- a/app/Providers/ViewComposerServiceProvider.php +++ b/app/Providers/ViewComposerServiceProvider.php @@ -25,6 +25,7 @@ namespace Pterodactyl\Providers; use Illuminate\Support\ServiceProvider; +use Pterodactyl\Http\ViewComposers\VersionComposer; use Pterodactyl\Http\ViewComposers\Server\ServerDataComposer; class ViewComposerServiceProvider extends ServiceProvider @@ -35,5 +36,6 @@ class ViewComposerServiceProvider extends ServiceProvider public function boot() { $this->app->make('view')->composer('server.*', ServerDataComposer::class); + $this->app->make('view')->composer('*', VersionComposer::class); } } diff --git a/resources/themes/pterodactyl/admin/nodes/view/index.blade.php b/resources/themes/pterodactyl/admin/nodes/view/index.blade.php index 1dc80007..97864ea6 100644 --- a/resources/themes/pterodactyl/admin/nodes/view/index.blade.php +++ b/resources/themes/pterodactyl/admin/nodes/view/index.blade.php @@ -58,7 +58,7 @@ - + From 131159c246ce111fe2ebae930f0a9208db4bdc00 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Sun, 10 Sep 2017 23:57:18 -0500 Subject: [PATCH 96/99] Fix some forgotten logic checks temporarily --- app/Http/Requests/Admin/AdminFormRequest.php | 2 +- app/Models/User.php | 2 +- app/Policies/APIKeyPolicy.php | 2 +- app/Policies/ServerPolicy.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Http/Requests/Admin/AdminFormRequest.php b/app/Http/Requests/Admin/AdminFormRequest.php index 4399d56c..a3442a56 100644 --- a/app/Http/Requests/Admin/AdminFormRequest.php +++ b/app/Http/Requests/Admin/AdminFormRequest.php @@ -42,7 +42,7 @@ abstract class AdminFormRequest extends FormRequest return false; } - return $this->user()->isRootAdmin(); + return (bool) $this->user()->root_admin; } /** diff --git a/app/Models/User.php b/app/Models/User.php index a3493522..29941e09 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -202,7 +202,7 @@ class User extends Model implements */ public function isRootAdmin() { - return $this->root_admin === 1; + return $this->root_admin; } /** diff --git a/app/Policies/APIKeyPolicy.php b/app/Policies/APIKeyPolicy.php index 31b888a7..f397d486 100644 --- a/app/Policies/APIKeyPolicy.php +++ b/app/Policies/APIKeyPolicy.php @@ -43,7 +43,7 @@ class APIKeyPolicy protected function checkPermission(User $user, Key $key, $permission) { // Non-administrative users cannot use administrative routes. - if (! starts_with($key, 'user.') && ! $user->isRootAdmin()) { + if (! starts_with($key, 'user.') && ! $user->root_admin) { return false; } diff --git a/app/Policies/ServerPolicy.php b/app/Policies/ServerPolicy.php index 618deebf..0b9968f9 100644 --- a/app/Policies/ServerPolicy.php +++ b/app/Policies/ServerPolicy.php @@ -60,7 +60,7 @@ class ServerPolicy */ public function before(User $user, $ability, Server $server) { - if ($user->isRootAdmin() || $server->owner_id === $user->id) { + if ($user->root_admin || $server->owner_id === $user->id) { return true; } From f9bf8603b2ddfc75e178a1d256d7a18a1853894a Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Mon, 11 Sep 2017 00:15:48 -0500 Subject: [PATCH 97/99] wot :question: --- app/Services/Helpers/SoftwareVersionService.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Services/Helpers/SoftwareVersionService.php b/app/Services/Helpers/SoftwareVersionService.php index 87f84a40..15b4445f 100644 --- a/app/Services/Helpers/SoftwareVersionService.php +++ b/app/Services/Helpers/SoftwareVersionService.php @@ -24,6 +24,7 @@ namespace Pterodactyl\Services\Helpers; +use stdClass; use Exception; use GuzzleHttp\Client; use Illuminate\Contracts\Cache\Repository as CacheRepository; @@ -142,7 +143,7 @@ class SoftwareVersionService throw new CdnVersionFetchingException; } catch (Exception $exception) { - return (object) []; + return new stdClass(); } }); } From 1873c1e9b9f47077c4b7f051ea7e32237e512e02 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Mon, 11 Sep 2017 00:27:43 -0500 Subject: [PATCH 98/99] Who doesn't love a good mystery novel. :bread: Fix ide helper stubs? --- .../Controllers/Admin/NodesController.php | 12 +++- app/Http/ViewComposers/VersionComposer.php | 56 ------------------- app/Providers/ViewComposerServiceProvider.php | 2 - 3 files changed, 11 insertions(+), 59 deletions(-) delete mode 100644 app/Http/ViewComposers/VersionComposer.php diff --git a/app/Http/Controllers/Admin/NodesController.php b/app/Http/Controllers/Admin/NodesController.php index c0dd8a6e..0262f4e6 100644 --- a/app/Http/Controllers/Admin/NodesController.php +++ b/app/Http/Controllers/Admin/NodesController.php @@ -34,6 +34,7 @@ use Illuminate\Cache\Repository as CacheRepository; use Pterodactyl\Services\Nodes\NodeCreationService; use Pterodactyl\Services\Nodes\NodeDeletionService; use Pterodactyl\Services\Allocations\AssignmentService; +use Pterodactyl\Services\Helpers\SoftwareVersionService; use Pterodactyl\Http\Requests\Admin\Node\NodeFormRequest; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Http\Requests\Admin\Node\AllocationFormRequest; @@ -88,6 +89,11 @@ class NodesController extends Controller */ protected $updateService; + /** + * @var \Pterodactyl\Services\Helpers\SoftwareVersionService + */ + protected $versionService; + /** * NodesController constructor. * @@ -100,6 +106,7 @@ class NodesController extends Controller * @param \Pterodactyl\Contracts\Repository\LocationRepositoryInterface $locationRepository * @param \Pterodactyl\Contracts\Repository\NodeRepositoryInterface $repository * @param \Pterodactyl\Services\Nodes\NodeUpdateService $updateService + * @param \Pterodactyl\Services\Helpers\SoftwareVersionService $versionService */ public function __construct( AlertsMessageBag $alert, @@ -110,7 +117,8 @@ class NodesController extends Controller NodeDeletionService $deletionService, LocationRepositoryInterface $locationRepository, NodeRepositoryInterface $repository, - NodeUpdateService $updateService + NodeUpdateService $updateService, + SoftwareVersionService $versionService ) { $this->alert = $alert; $this->allocationRepository = $allocationRepository; @@ -121,6 +129,7 @@ class NodesController extends Controller $this->locationRepository = $locationRepository; $this->repository = $repository; $this->updateService = $updateService; + $this->versionService = $versionService; } /** @@ -182,6 +191,7 @@ class NodesController extends Controller return view('admin.nodes.view.index', [ 'node' => $this->repository->getSingleNode($node), 'stats' => $this->repository->getUsageStats($node), + 'version' => $this->versionService, ]); } diff --git a/app/Http/ViewComposers/VersionComposer.php b/app/Http/ViewComposers/VersionComposer.php deleted file mode 100644 index 5c066b55..00000000 --- a/app/Http/ViewComposers/VersionComposer.php +++ /dev/null @@ -1,56 +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\Http\ViewComposers; - -use Illuminate\View\View; -use Pterodactyl\Services\Helpers\SoftwareVersionService; - -class VersionComposer -{ - /** - * @var \Pterodactyl\Services\Helpers\SoftwareVersionService - */ - protected $version; - - /** - * VersionComposer constructor. - * - * @param \Pterodactyl\Services\Helpers\SoftwareVersionService $version - */ - public function __construct(SoftwareVersionService $version) - { - $this->version = $version; - } - - /** - * Attach server data to a view automatically. - * - * @param \Illuminate\View\View $view - */ - public function compose(View $view) - { - $view->with('version', $this->version); - } -} diff --git a/app/Providers/ViewComposerServiceProvider.php b/app/Providers/ViewComposerServiceProvider.php index 1e4b77f1..df1648f1 100644 --- a/app/Providers/ViewComposerServiceProvider.php +++ b/app/Providers/ViewComposerServiceProvider.php @@ -25,7 +25,6 @@ namespace Pterodactyl\Providers; use Illuminate\Support\ServiceProvider; -use Pterodactyl\Http\ViewComposers\VersionComposer; use Pterodactyl\Http\ViewComposers\Server\ServerDataComposer; class ViewComposerServiceProvider extends ServiceProvider @@ -36,6 +35,5 @@ class ViewComposerServiceProvider extends ServiceProvider public function boot() { $this->app->make('view')->composer('server.*', ServerDataComposer::class); - $this->app->make('view')->composer('*', VersionComposer::class); } } From 07965d0ce704b3a5cb1d9a274c6cb6d43b6760e8 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Mon, 11 Sep 2017 01:15:44 -0500 Subject: [PATCH 99/99] These migrations... work?? :whale2: --- ...17_09_09_125253_AddChainedTasksAbility.php | 49 ------------ ..._09_09_162204_AddNullableNextRunColumn.php | 31 -------- ...9_RenameTasksTableForStructureRefactor.php | 23 ++++++ ...2017_09_10_225941_CreateSchedulesTable.php | 39 ++++++++++ ...230309_CreateNewTasksTableForSchedules.php | 36 +++++++++ ..._002938_TransferOldTasksToNewScheduler.php | 75 +++++++++++++++++++ 6 files changed, 173 insertions(+), 80 deletions(-) delete mode 100644 database/migrations/2017_09_09_125253_AddChainedTasksAbility.php delete mode 100644 database/migrations/2017_09_09_162204_AddNullableNextRunColumn.php create mode 100644 database/migrations/2017_09_10_225749_RenameTasksTableForStructureRefactor.php create mode 100644 database/migrations/2017_09_10_225941_CreateSchedulesTable.php create mode 100644 database/migrations/2017_09_10_230309_CreateNewTasksTableForSchedules.php create mode 100644 database/migrations/2017_09_11_002938_TransferOldTasksToNewScheduler.php diff --git a/database/migrations/2017_09_09_125253_AddChainedTasksAbility.php b/database/migrations/2017_09_09_125253_AddChainedTasksAbility.php deleted file mode 100644 index 8748e409..00000000 --- a/database/migrations/2017_09_09_125253_AddChainedTasksAbility.php +++ /dev/null @@ -1,49 +0,0 @@ -unsignedInteger('parent_task_id')->after('id')->nullable(); - $table->unsignedInteger('chain_order')->after('parent_task_id')->nullable(); - $table->unsignedInteger('chain_delay')->after('minute')->nullable(); - $table->string('name')->after('server_id')->nullable(); - - $table->foreign('parent_task_id')->references('id')->on('tasks')->onDelete('cascade'); - $table->index(['parent_task_id', 'chain_order']); - - $table->dropForeign(['user_id']); - $table->dropColumn('user_id'); - $table->dropColumn('year'); - $table->dropColumn('month'); - }); - } - - /** - * Reverse the migrations. - */ - public function down() - { - Schema::table('tasks', function (Blueprint $table) { - $table->dropForeign(['parent_task_id']); - $table->dropIndex(['parent_task_id', 'chain_order']); - $table->dropColumn('parent_task_id'); - $table->dropColumn('chain_order'); - $table->dropColumn('chain_delay'); - $table->dropColumn('name'); - - $table->unsignedInteger('user_id')->after('id')->nullable(); - $table->string('year')->after('queued')->default('*'); - $table->string('month')->after('year')->default('*'); - $table->foreign('user_id')->references('id')->on('users'); - }); - } -} diff --git a/database/migrations/2017_09_09_162204_AddNullableNextRunColumn.php b/database/migrations/2017_09_09_162204_AddNullableNextRunColumn.php deleted file mode 100644 index 40adb401..00000000 --- a/database/migrations/2017_09_09_162204_AddNullableNextRunColumn.php +++ /dev/null @@ -1,31 +0,0 @@ -wrapTable('tasks'); - DB::statement('ALTER TABLE ' . $table . ' CHANGE `next_run` `next_run` TIMESTAMP NULL;'); - }); - } - - /** - * Reverse the migrations. - */ - public function down() - { - Schema::table('tasks', function (Blueprint $table) { - $table = DB::getQueryGrammar()->wrapTable('tasks'); - DB::statement('ALTER TABLE ' . $table . ' CHANGE `next_run` `next_run` TIMESTAMP NOT NULL;'); - }); - } -} diff --git a/database/migrations/2017_09_10_225749_RenameTasksTableForStructureRefactor.php b/database/migrations/2017_09_10_225749_RenameTasksTableForStructureRefactor.php new file mode 100644 index 00000000..12eada73 --- /dev/null +++ b/database/migrations/2017_09_10_225749_RenameTasksTableForStructureRefactor.php @@ -0,0 +1,23 @@ +increments('id'); + $table->unsignedInteger('server_id'); + $table->string('name')->nullable(); + $table->string('cron_day_of_week'); + $table->string('cron_day_of_month'); + $table->string('cron_hour'); + $table->string('cron_minute'); + $table->boolean('is_active'); + $table->boolean('is_processing'); + $table->timestamp('last_run_at'); + $table->timestamp('next_run_at'); + $table->timestamps(); + + $table->foreign('server_id')->references('id')->on('servers')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down() + { + Schema::dropIfExists('schedules'); + } +} diff --git a/database/migrations/2017_09_10_230309_CreateNewTasksTableForSchedules.php b/database/migrations/2017_09_10_230309_CreateNewTasksTableForSchedules.php new file mode 100644 index 00000000..9c225a83 --- /dev/null +++ b/database/migrations/2017_09_10_230309_CreateNewTasksTableForSchedules.php @@ -0,0 +1,36 @@ +increments('id'); + $table->unsignedInteger('schedule_id'); + $table->unsignedInteger('sequence_id'); + $table->string('action'); + $table->text('payload'); + $table->unsignedInteger('time_offset'); + $table->boolean('is_queued'); + $table->timestamps(); + + $table->index(['schedule_id', 'sequence_id']); + $table->foreign('schedule_id')->references('id')->on('schedules')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down() + { + Schema::dropIfExists('tasks'); + } +} diff --git a/database/migrations/2017_09_11_002938_TransferOldTasksToNewScheduler.php b/database/migrations/2017_09_11_002938_TransferOldTasksToNewScheduler.php new file mode 100644 index 00000000..2a20ef10 --- /dev/null +++ b/database/migrations/2017_09_11_002938_TransferOldTasksToNewScheduler.php @@ -0,0 +1,75 @@ +get(); + + DB::beginTransaction(); + $tasks->each(function ($task) { + $schedule = DB::table('schedules')->insertGetId([ + 'server_id' => $task->server_id, + 'name' => null, + 'cron_day_of_week' => $task->day_of_week, + 'cron_day_of_month' => $task->day_of_month, + 'cron_hour' => $task->hour, + 'cron_minute' => $task->minute, + 'is_active' => (bool) $task->active, + 'is_processing' => false, + 'last_run_at' => $task->last_run, + 'next_run_at' => $task->next_run, + 'created_at' => $task->created_at, + 'updated_at' => Carbon::now()->toDateTimeString(), + ]); + + DB::table('tasks')->insert([ + 'schedule_id' => $schedule, + 'sequence_id' => 1, + 'action' => $task->action, + 'payload' => $task->data, + 'time_offset' => 0, + 'is_queued' => false, + 'updated_at' => Carbon::now()->toDateTimeString(), + 'created_at' => Carbon::now()->toDateTimeString(), + ]); + + DB::table('tasks_old')->delete($task->id); + DB::commit(); + }); + + Schema::dropIfExists('tasks_old'); + } + + /** + * Reverse the migrations. + */ + public function down() + { + Schema::create('tasks_old', function (Blueprint $table) { + $table->increments('id'); + $table->unsignedInteger('user_id')->nullable(); + $table->unsignedInteger('server_id'); + $table->tinyInteger('active')->default(1); + $table->string('action'); + $table->text('data'); + $table->unsignedTinyInteger('queued')->default(0); + $table->string('year')->default('*'); + $table->string('month')->default('*'); + $table->string('day_of_week')->default('*'); + $table->string('day_of_month')->default('*'); + $table->string('minute')->default('*'); + $table->timestamp('last_run')->nullable(); + $table->timestamp('next_run'); + $table->timestamps(); + }); + } +}
    Daemon Version (Latest: {{ Version::getDaemon() }}) (Latest: {{ $version->getDaemon() }})
    System Information