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'); });