Initial implementation of new task mgmt system 👮

This commit is contained in:
Dane Everitt 2017-09-09 23:55:21 -05:00
parent f157c06d04
commit bab28dbc85
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
31 changed files with 1535 additions and 132 deletions

View file

@ -16,6 +16,9 @@ DB_PASSWORD=secret
CACHE_DRIVER=file CACHE_DRIVER=file
SESSION_DRIVER=database SESSION_DRIVER=database
HASHIDS_SALT=
HASHIDS_LENGTH=8
MAIL_DRIVER=smtp MAIL_DRIVER=smtp
MAIL_HOST=mailtrap.io MAIL_HOST=mailtrap.io
MAIL_PORT=2525 MAIL_PORT=2525

View file

@ -14,3 +14,5 @@ CACHE_DRIVER=array
SESSION_DRIVER=array SESSION_DRIVER=array
MAIL_DRIVER=array MAIL_DRIVER=array
QUEUE_DRIVER=sync QUEUE_DRIVER=sync
HASHIDS_SALT=test123

View file

@ -0,0 +1,41 @@
<?php
/*
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF 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);
}

View file

@ -0,0 +1,47 @@
<?php
/*
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF 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);
}

View file

@ -73,6 +73,8 @@ class DynamicDatabaseConnection
* @param string $connection * @param string $connection
* @param \Pterodactyl\Models\DatabaseHost|int $host * @param \Pterodactyl\Models\DatabaseHost|int $host
* @param string $database * @param string $database
*
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/ */
public function set($connection, $host, $database = 'mysql') public function set($connection, $host, $database = 'mysql')
{ {

View file

@ -0,0 +1,44 @@
<?php
/*
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF 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);
}
}

View file

@ -0,0 +1,171 @@
<?php
/*
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF 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);
}
}

View file

@ -0,0 +1,84 @@
<?php
/*
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF 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;
}
}

View file

@ -24,10 +24,16 @@
namespace Pterodactyl\Models; namespace Pterodactyl\Models;
use Sofa\Eloquence\Eloquence;
use Sofa\Eloquence\Validable;
use Illuminate\Database\Eloquent\Model; 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. * The table associated with the model.
* *
@ -55,6 +61,53 @@ class Task extends Model
'active' => 'boolean', '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. * 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']; 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. * Gets the server associated with a task.
* *
@ -81,4 +144,14 @@ class Task extends Model
{ {
return $this->belongsTo(User::class); 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');
}
} }

View file

@ -0,0 +1,51 @@
<?php
/*
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF 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');
}
}

View file

@ -29,6 +29,7 @@ use Pterodactyl\Repositories\Daemon\FileRepository;
use Pterodactyl\Repositories\Daemon\PowerRepository; use Pterodactyl\Repositories\Daemon\PowerRepository;
use Pterodactyl\Repositories\Eloquent\NodeRepository; use Pterodactyl\Repositories\Eloquent\NodeRepository;
use Pterodactyl\Repositories\Eloquent\PackRepository; use Pterodactyl\Repositories\Eloquent\PackRepository;
use Pterodactyl\Repositories\Eloquent\TaskRepository;
use Pterodactyl\Repositories\Eloquent\UserRepository; use Pterodactyl\Repositories\Eloquent\UserRepository;
use Pterodactyl\Repositories\Daemon\CommandRepository; use Pterodactyl\Repositories\Daemon\CommandRepository;
use Pterodactyl\Repositories\Eloquent\ApiKeyRepository; use Pterodactyl\Repositories\Eloquent\ApiKeyRepository;
@ -43,6 +44,7 @@ use Pterodactyl\Repositories\Eloquent\PermissionRepository;
use Pterodactyl\Repositories\Daemon\ConfigurationRepository; use Pterodactyl\Repositories\Daemon\ConfigurationRepository;
use Pterodactyl\Contracts\Repository\NodeRepositoryInterface; use Pterodactyl\Contracts\Repository\NodeRepositoryInterface;
use Pterodactyl\Contracts\Repository\PackRepositoryInterface; use Pterodactyl\Contracts\Repository\PackRepositoryInterface;
use Pterodactyl\Contracts\Repository\TaskRepositoryInterface;
use Pterodactyl\Contracts\Repository\UserRepositoryInterface; use Pterodactyl\Contracts\Repository\UserRepositoryInterface;
use Pterodactyl\Repositories\Eloquent\DatabaseHostRepository; use Pterodactyl\Repositories\Eloquent\DatabaseHostRepository;
use Pterodactyl\Repositories\Eloquent\ApiPermissionRepository; use Pterodactyl\Repositories\Eloquent\ApiPermissionRepository;
@ -97,6 +99,7 @@ class RepositoryServiceProvider extends ServiceProvider
$this->app->bind(ServiceVariableRepositoryInterface::class, ServiceVariableRepository::class); $this->app->bind(ServiceVariableRepositoryInterface::class, ServiceVariableRepository::class);
$this->app->bind(SessionRepositoryInterface::class, SessionRepository::class); $this->app->bind(SessionRepositoryInterface::class, SessionRepository::class);
$this->app->bind(SubuserRepositoryInterface::class, SubuserRepository::class); $this->app->bind(SubuserRepositoryInterface::class, SubuserRepository::class);
$this->app->bind(TaskRepositoryInterface::class, TaskRepository::class);
$this->app->bind(UserRepositoryInterface::class, UserRepository::class); $this->app->bind(UserRepositoryInterface::class, UserRepository::class);
// Daemon Repositories // Daemon Repositories

View file

@ -0,0 +1,74 @@
<?php
/*
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF 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;
}
}

View file

@ -0,0 +1,108 @@
<?php
/*
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF 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;
}
}

View file

@ -0,0 +1,47 @@
<?php
/*
* Pterodactyl - Panel
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF 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)
{
}
}

View file

@ -21,6 +21,7 @@
"edvinaskrucas/settings": "^2.0", "edvinaskrucas/settings": "^2.0",
"fideloper/proxy": "^3.3", "fideloper/proxy": "^3.3",
"guzzlehttp/guzzle": "~6.3.0", "guzzlehttp/guzzle": "~6.3.0",
"hashids/hashids": "^2.0",
"igaster/laravel-theme": "^1.16", "igaster/laravel-theme": "^1.16",
"laracasts/utilities": "^3.0", "laracasts/utilities": "^3.0",
"laravel/framework": "5.4.27", "laravel/framework": "5.4.27",

65
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "a0014dfc711e382fff7903d9aeaffc25", "content-hash": "15a4dc6de122bc1e47d1d9ca3b1224d6",
"packages": [ "packages": [
{ {
"name": "aws/aws-sdk-php", "name": "aws/aws-sdk-php",
@ -1019,6 +1019,69 @@
], ],
"time": "2017-03-20T17:10:46+00:00" "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", "name": "igaster/laravel-theme",
"version": "v1.16", "version": "v1.16",

View file

@ -161,6 +161,7 @@ return [
Pterodactyl\Providers\AppServiceProvider::class, Pterodactyl\Providers\AppServiceProvider::class,
Pterodactyl\Providers\AuthServiceProvider::class, Pterodactyl\Providers\AuthServiceProvider::class,
Pterodactyl\Providers\EventServiceProvider::class, Pterodactyl\Providers\EventServiceProvider::class,
Pterodactyl\Providers\HashidsServiceProvider::class,
Pterodactyl\Providers\RouteServiceProvider::class, Pterodactyl\Providers\RouteServiceProvider::class,
Pterodactyl\Providers\MacroServiceProvider::class, Pterodactyl\Providers\MacroServiceProvider::class,
Pterodactyl\Providers\PhraseAppTranslationProvider::class, Pterodactyl\Providers\PhraseAppTranslationProvider::class,
@ -237,7 +238,6 @@ return [
'URL' => Illuminate\Support\Facades\URL::class, 'URL' => Illuminate\Support\Facades\URL::class,
'Uuid' => Webpatser\Uuid\Uuid::class, 'Uuid' => Webpatser\Uuid\Uuid::class,
'Validator' => Illuminate\Support\Facades\Validator::class, 'Validator' => Illuminate\Support\Facades\Validator::class,
'Version' => Pterodactyl\Facades\Version::class,
'View' => Illuminate\Support\Facades\View::class, 'View' => Illuminate\Support\Facades\View::class,
], ],
]; ];

15
config/hashids.php Normal file
View file

@ -0,0 +1,15 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Hashids Configuration
|--------------------------------------------------------------------------
|
| Here are the settings that control the Hashids setup and usage in the panel.
|
*/
'salt' => env('HASHIDS_SALT'),
'length' => env('HASHIDS_LENGTH', 8),
'alphabet' => env('HASHIDS_ALPHABET', 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'),
];

175
config/ide-helper.php Normal file
View file

@ -0,0 +1,175 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Filename & Format
|--------------------------------------------------------------------------
|
| The default filename (without extension) and the format (php or json)
|
*/
'filename' => '_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',
],
];

View file

@ -0,0 +1,49 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddChainedTasksAbility extends Migration
{
/**
* Run the migrations.
*/
public function up()
{
Schema::table('tasks', function (Blueprint $table) {
$table->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');
});
}
}

View file

@ -0,0 +1,31 @@
<?php
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AddNullableNextRunColumn extends Migration
{
/**
* Run the migrations.
*/
public function up()
{
Schema::table('tasks', function (Blueprint $table) {
$table = DB::getQueryGrammar()->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;');
});
}
}

View file

@ -18,98 +18,113 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE. // SOFTWARE.
var Tasks = (function () { $(document).ready(function () {
$('select[name="action"]').select2();
function initTaskFunctions() { $('[data-action="update-field"]').on('change', function (event) {
$('[data-action="delete-task"]').click(function (event) { event.preventDefault();
var self = $(this); var updateField = $(this).data('field');
swal({ var selected = $(this).map(function (i, opt) {
type: 'error', return $(opt).val();
title: 'Delete Task?', }).toArray();
text: 'Are you sure you want to delete this task? There is no undo.', if (selected.length === $(this).find('option').length) {
showCancelButton: true, $('input[name=' + updateField + ']').val('*');
allowOutsideClick: true, } else {
closeOnConfirm: false, $('input[name=' + updateField + ']').val(selected.join(','));
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();
} }
} });
})(); $('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.'
});
});
});
});
});

View file

@ -19,6 +19,7 @@ return [
'new' => [ 'new' => [
'header' => 'New Task', 'header' => 'New Task',
'header_sub' => 'Create a new scheduled task for this server.', 'header_sub' => 'Create a new scheduled task for this server.',
'task_name' => 'Task Name',
'day_of_week' => 'Day of Week', 'day_of_week' => 'Day of Week',
'custom' => 'Custom Value', 'custom' => 'Custom Value',
'day_of_month' => 'Day of Month', 'day_of_month' => 'Day of Month',
@ -33,9 +34,16 @@ return [
'sat' => 'Saturday', 'sat' => 'Saturday',
'submit' => 'Create Task', 'submit' => 'Create Task',
'type' => 'Task Type', 'type' => 'Task Type',
'chain_then' => 'Then, After',
'chain_do' => 'Do',
'chain_arguments' => 'With Arguments',
'payload' => 'Task Payload', 'payload' => 'Task Payload',
'payload_help' => 'For example, if you selected <code>Send Command</code> enter the command here. If you selected <code>Send Power Option</code> put the power action here (e.g. <code>restart</code>).', 'payload_help' => 'For example, if you selected <code>Send Command</code> enter the command here. If you selected <code>Send Power Option</code> put the power action here (e.g. <code>restart</code>).',
], ],
'edit' => [
'header' => 'Manage Task',
'submit' => 'Update Task',
],
], ],
'users' => [ 'users' => [
'header' => 'Manage Users', 'header' => 'Manage Users',

View file

@ -71,4 +71,7 @@ return [
'admin' => 'Admin', 'admin' => 'Admin',
'subuser' => 'Subuser', 'subuser' => 'Subuser',
'captcha_invalid' => 'The provided captcha is invalid.', 'captcha_invalid' => 'The provided captcha is invalid.',
'child_tasks' => 'Child Tasks',
'seconds' => 'Seconds',
'minutes' => 'Minutes',
]; ];

View file

@ -146,7 +146,7 @@
@endcan @endcan
@can('list-subusers', $server) @can('list-subusers', $server)
<li <li
@if(in_array(Route::currentRouteName(), ['server.subusers', 'server.subusers.new', 'server.subusers.view'])) @if(starts_with(Route::currentRouteName(), 'server.subusers'))
class="active" class="active"
@endif @endif
> >
@ -157,7 +157,7 @@
@endcan @endcan
@can('list-tasks', $server) @can('list-tasks', $server)
<li <li
@if(in_array(Route::currentRouteName(), ['server.tasks', 'server.tasks.new'])) @if(starts_with(Route::currentRouteName(), 'server.tasks'))
class="active" class="active"
@endif @endif
> >
@ -171,7 +171,7 @@
@endcan @endcan
@if(Gate::allows('view-startup', $server) || Gate::allows('view-sftp', $server) || Gate::allows('view-databases', $server) || Gate::allows('view-allocation', $server)) @if(Gate::allows('view-startup', $server) || Gate::allows('view-sftp', $server) || Gate::allows('view-databases', $server) || Gate::allows('view-allocation', $server))
<li class="treeview <li class="treeview
@if(in_array(Route::currentRouteName(), ['server.settings.sftp', 'server.settings.databases', 'server.settings.startup', 'server.settings.allocation'])) @if(starts_with(Route::currentRouteName(), 'server.settings'))
active active
@endif @endif
"> ">

View file

@ -0,0 +1,42 @@
@section('tasks::chain-template')
<div class="box-footer with-border hidden" data-target="chain-clone">
<div class="row">
<div class="form-group col-md-3">
<label class="control-label">@lang('server.tasks.new.chain_then'):</label>
<div class="row">
<div class="col-xs-4">
<select name="chain[time_value][]" class="form-control">
@foreach(range(1, 60) as $number)
<option value="{{ $number }}">{{ $number }}</option>
@endforeach
</select>
</div>
<div class="col-xs-8">
<select name="chain[time_interval][]" class="form-control">
<option value="s">@lang('strings.seconds')</option>
<option value="m">@lang('strings.minutes')</option>
</select>
</div>
</div>
</div>
<div class="form-group col-md-3">
<label class="control-label">@lang('server.tasks.new.chain_do'):</label>
<div>
<select name="chain[action][]" class="form-control">
<option value="command">@lang('server.tasks.actions.command')</option>
<option value="power">@lang('server.tasks.actions.power')</option>
</select>
</div>
</div>
<div class="form-group col-md-6">
<label class="control-label">@lang('server.tasks.new.chain_arguments'):</label>
<div class="input-group">
<input type="text" name="chain[payload][]" class="form-control">
<div class="input-group-btn">
<button type="button" class="btn btn-danger" data-action="remove-chain-element"><i class="fa fa-close"></i></button>
</div>
</div>
</div>
</div>
</div>
@show

View file

@ -46,9 +46,9 @@
<table class="table table-hover"> <table class="table table-hover">
<tbody> <tbody>
<tr> <tr>
<th>@lang('strings.action')</th> <th>@lang('strings.name')</th>
<th>@lang('strings.data')</th> <th class="text-center">@lang('strings.queued')</th>
<th>@lang('strings.queued')</th> <th class="text-center">@lang('strings.child_tasks')</th>
<th>@lang('strings.last_run')</th> <th>@lang('strings.last_run')</th>
<th>@lang('strings.next_run')</th> <th>@lang('strings.next_run')</th>
<th></th> <th></th>
@ -56,25 +56,35 @@
</tr> </tr>
@foreach($tasks as $task) @foreach($tasks as $task)
<tr @if(! $task->active)class="muted muted-hover"@endif> <tr @if(! $task->active)class="muted muted-hover"@endif>
<td class="middle">{{ $actions[$task->action] }}</td>
<td class="middle"><code>{{ $task->data }}</code></td>
<td class="middle"> <td class="middle">
@can('edit-task', $server)
<a href="{{ route('server.tasks.view', ['server' => $server->uuidShort, 'task' => $task->hashid]) }}">{{ $task->name }}</a>
@else
{{ $task->name }}
@endcan
</td>
<td class="middle text-center">
@if ($task->queued) @if ($task->queued)
<span class="label label-success">@lang('strings.yes')</span> <span class="label label-success">@lang('strings.yes')</span>
@else @else
<span class="label label-default">@lang('strings.no')</span> <span class="label label-default">@lang('strings.no')</span>
@endif @endif
</td> </td>
<td class="middle text-center"><span class="label label-primary">{{ $task->chained_count }}</span></td>
<td class="middle"> <td class="middle">
@if($task->last_run) @if($task->last_run)
{{ Carbon::parse($task->last_run)->toDayDateTimeString() }}<br /><span class="text-muted small">({{ Carbon::parse($task->last_run)->diffForHumans() }})</span> {{ Carbon::parse($task->last_run)->toDayDateTimeString() }}<br /><span class="text-muted small">({{ Carbon::parse($task->last_run)->diffForHumans() }})</span>
@else @else
@lang('strings.not_run_yet') <em class="text-muted">@lang('strings.not_run_yet')</em>
@endif @endif
</td> </td>
<td class="middle"> <td class="middle">
@if($task->active !== 0) @if($task->active !== 0)
{{ Carbon::parse($task->next_run)->toDayDateTimeString() }}<br /><span class="text-muted small">({{ Carbon::parse($task->next_run)->diffForHumans() }})</span> @if($task->last_run)
{{ Carbon::parse($task->next_run)->toDayDateTimeString() }}<br /><span class="text-muted small">({{ Carbon::parse($task->next_run)->diffForHumans() }})</span>
@else
<em class="text-muted">@lang('strings.not_run_yet')</em>
@endif
@else @else
<em>n/a</em> <em>n/a</em>
@endif @endif

View file

@ -41,6 +41,22 @@
@section('content') @section('content')
<form action="{{ route('server.tasks.new', $server->uuidShort) }}" method="POST"> <form action="{{ route('server.tasks.new', $server->uuidShort) }}" method="POST">
<div class="row">
<div class="col-xs-12">
<div class="box box-primary">
<div class="box-body">
<div class="row">
<div class="form-group col-xs-12">
<label class="control-label">@lang('server.tasks.new.task_name'):</label>
<div>
<input type="text" name="name" class="form-control" value="{{ old('name') }}" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row"> <div class="row">
<div class="col-md-3 col-sm-6"> <div class="col-md-3 col-sm-6">
<div class="box"> <div class="box">
@ -176,33 +192,26 @@
</div> </div>
</div> </div>
</div> </div>
<div class="box-footer with-border"> <div class="box-footer with-border" id="chainLastSegment">
{!! csrf_field() !!} <div class="pull-left">
<button type="submit" class="btn btn-sm btn-success">@lang('server.tasks.new.submit')</button> <p class="text-muted small">Times for chain arguments are relative to the previous argument.</p>
</div>
<div class="pull-right">
{!! csrf_field() !!}
<button type="button" class="btn btn-sm btn-default" data-action="add-chain"><i class="fa fa-plus"></i> New Chain Argument</button>
<button type="submit" class="btn btn-sm btn-success">@lang('server.tasks.new.submit')</button>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </form>
@include('partials.tasks.chain-template')
@endsection @endsection
@section('footer-scripts') @section('footer-scripts')
@parent @parent
{!! Theme::js('js/frontend/server.socket.js') !!} {!! Theme::js('js/frontend/server.socket.js') !!}
{!! Theme::js('vendor/select2/select2.full.min.js') !!} {!! Theme::js('vendor/select2/select2.full.min.js') !!}
<script> {!! Theme::js('js/frontend/tasks.js') !!}
$('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(','));
}
});
</script>
@endsection @endsection

View file

@ -0,0 +1,230 @@
{{-- Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com> --}}
{{-- Permission is hereby granted, free of charge, to any person obtaining a copy --}}
{{-- of this software and associated documentation files (the "Software"), to deal --}}
{{-- in the Software without restriction, including without limitation the rights --}}
{{-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell --}}
{{-- copies of the Software, and to permit persons to whom the Software is --}}
{{-- furnished to do so, subject to the following conditions: --}}
{{-- The above copyright notice and this permission notice shall be included in all --}}
{{-- copies or substantial portions of the Software. --}}
{{-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR --}}
{{-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, --}}
{{-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE --}}
{{-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER --}}
{{-- LIABILITY, WHETHER IN AN ACTION 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')
<h1>@lang('server.tasks.edit.header')<small>{{ $task->name }}</small></h1>
<ol class="breadcrumb">
<li><a href="{{ route('index') }}">@lang('strings.home')</a></li>
<li><a href="{{ route('server.index', $server->uuidShort) }}">{{ $server->name }}</a></li>
<li><a href="{{ route('server.tasks', $server->uuidShort) }}">@lang('navigation.server.task_management')</a></li>
<li class="active">@lang('server.users.update')</li>
</ol>
@endsection
@section('content')
<form action="{{ route('server.tasks.view', ['server' => $server->uuidShort, 'task' => $task->hashid]) }}" method="POST">
<div class="row">
<div class="col-xs-12">
<div class="box box-primary">
<div class="box-body">
<div class="row">
<div class="form-group col-xs-12">
<label class="control-label">@lang('server.tasks.new.task_name'):</label>
<div>
<input type="text" name="name" class="form-control" value="{{ old('name', $task->name) }}" />
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-3 col-sm-6">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">@lang('server.tasks.new.day_of_week')</h3>
</div>
<div class="box-body">
<div class="row">
<div class="form-group col-md-12">
<div>
<select data-action="update-field" data-field="day_of_week" class="form-control" multiple>
<option value="0">@lang('server.tasks.new.sun')</option>
<option value="1">@lang('server.tasks.new.mon')</option>
<option value="2">@lang('server.tasks.new.tues')</option>
<option value="3">@lang('server.tasks.new.wed')</option>
<option value="4">@lang('server.tasks.new.thurs')</option>
<option value="5">@lang('server.tasks.new.fri')</option>
<option value="6">@lang('server.tasks.new.sat')</option>
</select>
</div>
</div>
<div class="form-group col-md-12">
<label class="control-label">@lang('server.tasks.new.custom')</label>
<div>
<input type="text" class="form-control" name="day_of_week" value="{{ old('day_of_week', $task->day_of_week) }}"/>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">@lang('server.tasks.new.day_of_month')</h3>
</div>
<div class="box-body">
<div class="row">
<div class="form-group col-md-12">
<div>
<select data-action="update-field" data-field="day_of_month" class="form-control" multiple>
@foreach(range(1, 31) as $i)
<option value="{{ $i }}">{{ $i }}</option>
@endforeach
</select>
</div>
</div>
<div class="form-group col-md-12">
<label class="control-label">@lang('server.tasks.new.custom')</label>
<div>
<input type="text" class="form-control" name="day_of_month" value="{{ old('day_of_month', $task->day_of_month) }}"/>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">@lang('server.tasks.new.hour')</h3>
</div>
<div class="box-body">
<div class="row">
<div class="form-group col-md-12">
<div>
<select data-action="update-field" data-field="hour" class="form-control" multiple>
@foreach(range(0, 23) as $i)
<option value="{{ $i }}">{{ str_pad($i, 2, '0', STR_PAD_LEFT) }}:00</option>
@endforeach
</select>
</div>
</div>
<div class="form-group col-md-12">
<label class="control-label">@lang('server.tasks.new.custom')</label>
<div>
<input type="text" class="form-control" name="hour" value="{{ old('hour', $task->hour) }}"/>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">@lang('server.tasks.new.minute')</h3>
</div>
<div class="box-body">
<div class="row">
<div class="form-group col-md-12">
<div>
<select data-action="update-field" data-field="minute" class="form-control" multiple>
@foreach(range(0, 55) as $i)
@if($i % 5 === 0)
<option value="{{ $i }}">_ _:{{ str_pad($i, 2, '0', STR_PAD_LEFT) }}</option>
@endif
@endforeach
</select>
</div>
</div>
<div class="form-group col-md-12">
<label class="control-label">@lang('server.tasks.new.custom')</label>
<div>
<input type="text" class="form-control" name="minute" value="{{ old('minute', $task->minute) }}"/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12">
<div class="box box-primary">
<div class="box-body">
<div class="row">
<div class="form-group col-md-4">
<label class="control-label">@lang('server.tasks.new.type'):</label>
<div>
<select name="action" class="form-control">
<option value="command" @if($task->action === 'command')selected @endif>@lang('server.tasks.actions.command')</option>
<option value="power" @if($task->action === 'power')selected @endif>@lang('server.tasks.actions.power')</option>
</select>
</div>
</div>
<div class="form-group col-md-8">
<label class="control-label">@lang('server.tasks.new.payload'):</label>
<div>
<input type="text" name="data" class="form-control" value="{{ old('data', $task->data) }}">
<span class="text-muted small">@lang('server.tasks.new.payload_help')</span>
</div>
</div>
</div>
</div>
<div class="box-footer with-border" id="chainLastSegment">
<div class="pull-left">
<p class="text-muted small">Times for chain arguments are relative to the previous argument.</p>
</div>
<div class="pull-right">
{!! csrf_field() !!}
<button type="button" class="btn btn-sm btn-default" data-action="add-chain"><i class="fa fa-plus"></i> New Chain Argument</button>
<button type="submit" name="_method" value="PATCH" class="btn btn-sm btn-success">@lang('server.tasks.edit.submit')</button>
</div>
</div>
</div>
</div>
</div>
</form>
@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') !!}
<script>
$(document).ready(function () {
$.each(Pterodactyl.chained, function (index, value) {
var element = $('button[data-action="add-chain"]').trigger('click').data('element');
var timeValue = (value.chain_delay > 60) ? value.chain_delay / 60 : value.chain_delay;
var timeInterval = (value.chain_delay > 60) ? 'm' : 's';
element.find('select[name="chain[time_value][]"]').val(timeValue).trigger('change');
element.find('select[name="chain[time_interval][]"]').val(timeInterval).trigger('change');
element.find('select[name="chain[action][]"]').val(value.action).trigger('change');
element.find('input[name="chain[payload][]"]').val(value.data);
});
});
</script>
@endsection

View file

@ -79,7 +79,7 @@ Route::group(['prefix' => 'users'], function () {
Route::patch('/view/{subuser}', 'SubuserController@update')->middleware(SubuserAccess::class); 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::group(['prefix' => 'tasks'], function () {
Route::get('/', 'TaskController@index')->name('server.tasks'); Route::get('/', 'Tasks\TaskManagementController@index')->name('server.tasks');
Route::get('/new', 'TaskController@create')->name('server.tasks.new'); 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('/new', 'Tasks\TaskManagementController@store');
Route::post('/toggle/{id}', 'TaskController@toggle')->name('server.tasks.toggle');
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::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'); Route::post('/settings/reset-database-password', 'AjaxController@postResetDatabasePassword')->name('server.ajax.reset-database-password');
}); });