Begin refactoring Tasks to be apart of the Scheduler system

This commit is contained in:
Dane Everitt 2017-09-12 23:45:19 -05:00
parent 07965d0ce7
commit 2ac90b50f2
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
28 changed files with 902 additions and 468 deletions

View file

@ -25,7 +25,6 @@
namespace Pterodactyl\Console\Commands; namespace Pterodactyl\Console\Commands;
use Illuminate\Console\Command; use Illuminate\Console\Command;
use Pterodactyl\Repositories\UserRepository;
use Pterodactyl\Services\Users\UserCreationService; use Pterodactyl\Services\Users\UserCreationService;
class MakeUser extends Command class MakeUser extends Command
@ -60,8 +59,7 @@ class MakeUser extends Command
*/ */
public function __construct( public function __construct(
UserCreationService $creationService UserCreationService $creationService
) ) {
{
parent::__construct(); parent::__construct();
$this->creationService = $creationService; $this->creationService = $creationService;
} }
@ -87,7 +85,6 @@ class MakeUser extends Command
$data['root_admin'] = is_null($this->option('admin')) ? $this->confirm('Is this user a root administrator?') : $this->option('admin'); $data['root_admin'] = is_null($this->option('admin')) ? $this->confirm('Is this user a root administrator?') : $this->option('admin');
try { try {
$this->creationService->handle($data); $this->creationService->handle($data);
return $this->info('User successfully created.'); return $this->info('User successfully created.');

View file

@ -22,26 +22,23 @@
* SOFTWARE. * SOFTWARE.
*/ */
namespace Pterodactyl\Services\Tasks; namespace Pterodactyl\Contracts\Repository;
use Pterodactyl\Contracts\Repository\TaskRepositoryInterface; interface ScheduleRepositoryInterface extends RepositoryInterface
use Pterodactyl\Contracts\Repository\ServerRepositoryInterface;
class TaskUpdateService
{ {
protected $repository; /**
* Return all of the schedules for a given server.
*
* @param int $server
* @return \Illuminate\Database\Eloquent\Collection
*/
public function getServerSchedules($server);
protected $serverRepository; /**
* Return a schedule model with all of the associated tasks as a relationship.
public function __construct( *
ServerRepositoryInterface $serverRepository, * @param int $schedule
TaskRepositoryInterface $repository * @return \Illuminate\Support\Collection
) { */
$this->repository = $repository; public function getScheduleWithTasks($schedule);
$this->serverRepository = $serverRepository;
}
public function handle($server, array $data, array $chain = null)
{
}
} }

View file

@ -26,22 +26,4 @@ namespace Pterodactyl\Contracts\Repository;
interface TaskRepositoryInterface extends RepositoryInterface 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

@ -0,0 +1,31 @@
<?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\Exceptions\Service\Schedule\Task;
use Pterodactyl\Exceptions\DisplayException;
class TaskIntervalTooLongException extends DisplayException
{
}

View file

@ -24,20 +24,27 @@
namespace Pterodactyl\Http\Controllers\Server\Tasks; namespace Pterodactyl\Http\Controllers\Server\Tasks;
use Prologue\Alerts\AlertsMessageBag;
use Pterodactyl\Http\Requests\Request;
use Illuminate\Contracts\Session\Session; use Illuminate\Contracts\Session\Session;
use Pterodactyl\Http\Controllers\Controller; use Pterodactyl\Http\Controllers\Controller;
use Pterodactyl\Services\Tasks\TaskCreationService;
use Pterodactyl\Contracts\Extensions\HashidsInterface; use Pterodactyl\Contracts\Extensions\HashidsInterface;
use Pterodactyl\Traits\Controllers\JavascriptInjection; use Pterodactyl\Traits\Controllers\JavascriptInjection;
use Pterodactyl\Contracts\Repository\TaskRepositoryInterface; use Pterodactyl\Services\Schedules\ScheduleCreationService;
use Pterodactyl\Http\Requests\Server\TaskCreationFormRequest; use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface;
use Pterodactyl\Http\Requests\Server\ScheduleCreationFormRequest;
class TaskManagementController extends Controller class TaskManagementController extends Controller
{ {
use JavascriptInjection; use JavascriptInjection;
/** /**
* @var \Pterodactyl\Services\Tasks\TaskCreationService * @var \Prologue\Alerts\AlertsMessageBag
*/
protected $alert;
/**
* @var \Pterodactyl\Services\Schedules\ScheduleCreationService
*/ */
protected $creationService; protected $creationService;
@ -47,7 +54,7 @@ class TaskManagementController extends Controller
protected $hashids; protected $hashids;
/** /**
* @var \Pterodactyl\Contracts\Repository\TaskRepositoryInterface * @var \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface
*/ */
protected $repository; protected $repository;
@ -59,17 +66,20 @@ class TaskManagementController extends Controller
/** /**
* TaskManagementController constructor. * TaskManagementController constructor.
* *
* @param \Prologue\Alerts\AlertsMessageBag $alert
* @param \Pterodactyl\Contracts\Extensions\HashidsInterface $hashids * @param \Pterodactyl\Contracts\Extensions\HashidsInterface $hashids
* @param \Illuminate\Contracts\Session\Session $session * @param \Illuminate\Contracts\Session\Session $session
* @param \Pterodactyl\Services\Tasks\TaskCreationService $creationService * @param \Pterodactyl\Services\Schedules\ScheduleCreationService $creationService
* @param \Pterodactyl\Contracts\Repository\TaskRepositoryInterface $repository * @param \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface $repository
*/ */
public function __construct( public function __construct(
AlertsMessageBag $alert,
HashidsInterface $hashids, HashidsInterface $hashids,
Session $session, Session $session,
TaskCreationService $creationService, ScheduleCreationService $creationService,
TaskRepositoryInterface $repository ScheduleRepositoryInterface $repository
) { ) {
$this->alert = $alert;
$this->creationService = $creationService; $this->creationService = $creationService;
$this->hashids = $hashids; $this->hashids = $hashids;
$this->repository = $repository; $this->repository = $repository;
@ -86,11 +96,11 @@ class TaskManagementController extends Controller
public function index() public function index()
{ {
$server = $this->session->get('server_data.model'); $server = $this->session->get('server_data.model');
$this->authorize('list-tasks', $server); $this->authorize('list-schedules', $server);
$this->injectJavascript(); $this->injectJavascript();
return view('server.tasks.index', [ return view('server.tasks.index', [
'tasks' => $this->repository->getParentTasksWithChainCount($server->id), 'schedules' => $this->repository->getServerSchedules($server->id),
'actions' => [ 'actions' => [
'command' => trans('server.tasks.actions.command'), 'command' => trans('server.tasks.actions.command'),
'power' => trans('server.tasks.actions.power'), 'power' => trans('server.tasks.actions.power'),
@ -108,64 +118,95 @@ class TaskManagementController extends Controller
public function create() public function create()
{ {
$server = $this->session->get('server_data.model'); $server = $this->session->get('server_data.model');
$this->authorize('create-task', $server); $this->authorize('create-schedule', $server);
$this->injectJavascript(); $this->injectJavascript();
return view('server.tasks.new'); return view('server.tasks.new');
} }
/** /**
* @param \Pterodactyl\Http\Requests\Server\TaskCreationFormRequest $request * @param \Pterodactyl\Http\Requests\Server\ScheduleCreationFormRequest $request
*
* @return \Illuminate\Http\RedirectResponse * @return \Illuminate\Http\RedirectResponse
* *
* @throws \Exception
* @throws \Illuminate\Auth\Access\AuthorizationException * @throws \Illuminate\Auth\Access\AuthorizationException
* @throws \Pterodactyl\Exceptions\Model\DataValidationException * @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException * @throws \Pterodactyl\Exceptions\Service\Schedule\Task\TaskIntervalTooLongException
*/ */
public function store(TaskCreationFormRequest $request) public function store(ScheduleCreationFormRequest $request)
{ {
$server = $this->session->get('server_data.model'); $server = $this->session->get('server_data.model');
$this->authorize('create-task', $server); $this->authorize('create-schedule', $server);
$task = $this->creationService->handle($server, $request->normalize(), $request->getChainedTasks()); $schedule = $this->creationService->handle($server, $request->normalize(), $request->getTasks());
$this->alert->success(trans('server.tasks.task_created'))->flash();
return redirect()->route('server.tasks.view', [ return redirect()->route('server.tasks.view', [
'server' => $server->uuidShort, 'server' => $server->uuidShort,
'task' => $task->id, 'task' => $schedule->hashid,
]); ]);
} }
/** /**
* Return a view to modify task settings. * Return a view to modify a schedule.
* *
* @param string $uuid * @param \Pterodactyl\Http\Requests\Request $request
* @param string $task
* @return \Illuminate\View\View * @return \Illuminate\View\View
*
* @throws \Illuminate\Auth\Access\AuthorizationException * @throws \Illuminate\Auth\Access\AuthorizationException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/ */
public function view($uuid, $task) public function view(Request $request)
{ {
$server = $this->session->get('server_data.model'); $server = $this->session->get('server_data.model');
$this->authorize('edit-task', $server); $schedule = $request->attributes->get('schedule');
$task = $this->repository->getTaskForServer($this->hashids->decodeFirst($task, 0), $server->id); $this->authorize('view-schedule', $server);
$this->injectJavascript([ $this->injectJavascript([
'chained' => $task->chained->map(function ($chain) { 'tasks' => $schedule->tasks->map(function ($schedule) {
return collect($chain->toArray())->only('action', 'chain_delay', 'data')->all(); return collect($schedule->toArray())->only('action', 'time_offset', 'payload')->all();
}), }),
]); ]);
return view('server.tasks.view', ['task' => $task]); return view('server.tasks.view', ['schedule' => $schedule]);
} }
public function update(TaskCreationFormRequest $request, $uuid, $task) /**
* Update a specific parent task on the system.
*
* @param \Pterodactyl\Http\Requests\Server\ScheduleCreationFormRequest $request
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function update(ScheduleCreationFormRequest $request)
{ {
$server = $this->session->get('server_data.model'); $server = $this->session->get('server_data.model');
$this->authorize('edit-task', $server); $schedule = $request->attributes->get('schedule');
$task = $this->repository->getTaskForServer($this->hashids->decodeFirst($task, 0), $server->id); $this->authorize('edit-schedule', $server);
// $this->updateService->handle($task, $request->normalize(), $request->getChainedTasks());
$this->alert->success(trans('server.tasks.task_updated'))->flash();
return redirect()->route('server.tasks.view', [
'server' => $server->uuidShort,
'task' => $schedule->hashid,
]);
}
/**
* Delete a parent task from the Panel.
*
* @param \Pterodactyl\Http\Requests\Request $request
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function delete(Request $request)
{
$server = $this->session->get('server_data.model');
$schedule = $request->attributes->get('schedule');
$this->authorize('delete-schedule', $server);
$this->repository->delete($task->id);
return response('', 204);
} }
} }

View file

@ -0,0 +1,91 @@
<?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\Middleware\Server;
use Closure;
use Illuminate\Contracts\Session\Session;
use Pterodactyl\Contracts\Extensions\HashidsInterface;
use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface;
class ScheduleAccess
{
/**
* @var \Pterodactyl\Contracts\Extensions\HashidsInterface
*/
protected $hashids;
/**
* @var \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface
*/
protected $repository;
/**
* @var \Illuminate\Contracts\Session\Session
*/
protected $session;
/**
* TaskAccess constructor.
*
* @param \Pterodactyl\Contracts\Extensions\HashidsInterface $hashids
* @param \Illuminate\Contracts\Session\Session $session
* @param \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface $repository
*/
public function __construct(
HashidsInterface $hashids,
Session $session,
ScheduleRepositoryInterface $repository
) {
$this->hashids = $hashids;
$this->repository = $repository;
$this->session = $session;
}
/**
* Determine if a task is assigned to the active server.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*
* @throws \Pterodactyl\Exceptions\DisplayException
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
*/
public function handle($request, Closure $next)
{
$server = $this->session->get('server_data.model');
$scheduleId = $this->hashids->decodeFirst($request->route()->parameter('task'), 0);
$schedule = $this->repository->getScheduleWithTasks($scheduleId);
if ($schedule->server_id !== $server->id) {
abort(404);
}
$request->attributes->set('schedule', $schedule);
return $next($request);
}
}

View file

@ -26,7 +26,7 @@ namespace Pterodactyl\Http\Requests\Server;
use Pterodactyl\Http\Requests\FrontendUserFormRequest; use Pterodactyl\Http\Requests\FrontendUserFormRequest;
class TaskCreationFormRequest extends FrontendUserFormRequest class ScheduleCreationFormRequest extends FrontendUserFormRequest
{ {
/** /**
* Validation rules to apply to the request. * Validation rules to apply to the request.
@ -37,21 +37,19 @@ class TaskCreationFormRequest extends FrontendUserFormRequest
{ {
return [ return [
'name' => 'string|max:255', 'name' => 'string|max:255',
'day_of_week' => 'required|string', 'cron_day_of_week' => 'required|string',
'day_of_month' => 'required|string', 'cron_day_of_month' => 'required|string',
'hour' => 'required|string', 'cron_hour' => 'required|string',
'minute' => 'required|string', 'cron_minute' => 'required|string',
'action' => 'required|string|in:power,command', 'tasks' => 'sometimes|array|size:4',
'data' => 'required|string', 'tasks.time_value' => 'required_with:chain|max:5',
'chain' => 'sometimes|array|size:4', 'tasks.time_interval' => 'required_with:chain|max:5',
'chain.time_value' => 'required_with:chain|max:5', 'tasks.action' => 'required_with:chain|max:5',
'chain.time_interval' => 'required_with:chain|max:5', 'tasks.payload' => 'required_with:chain|max:5',
'chain.action' => 'required_with:chain|max:5', 'tasks.time_value.*' => 'numeric|between:1,60',
'chain.payload' => 'required_with:chain|max:5', 'tasks.time_interval.*' => 'string|in:s,m',
'chain.time_value.*' => 'numeric|between:1,60', 'tasks.action.*' => 'string|in:power,command',
'chain.time_interval.*' => 'string|in:s,m', 'tasks.payload.*' => 'string',
'chain.action.*' => 'string|in:power,command',
'chain.payload.*' => 'string',
]; ];
} }
@ -62,18 +60,18 @@ class TaskCreationFormRequest extends FrontendUserFormRequest
*/ */
public function normalize() public function normalize()
{ {
return $this->only('name', 'day_of_week', 'day_of_month', 'hour', 'minute', 'action', 'data'); return $this->only('name', 'cron_day_of_week', 'cron_day_of_month', 'cron_hour', 'cron_minute');
} }
/** /**
* Return the chained tasks provided in the request. * Return the tasks provided in the request that are associated with this schedule.
* *
* @return array|null * @return array|null
*/ */
public function getChainedTasks() public function getTasks()
{ {
$restructured = []; $restructured = [];
foreach (array_get($this->all(), 'chain', []) as $key => $values) { foreach (array_get($this->all(), 'tasks', []) as $key => $values) {
for ($i = 0; $i < count($values); ++$i) { for ($i = 0; $i < count($values); ++$i) {
$restructured[$i][$key] = $values[$i]; $restructured[$i][$key] = $values[$i];
} }

153
app/Models/Schedule.php Normal file
View file

@ -0,0 +1,153 @@
<?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\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 Schedule extends Model implements CleansAttributes, ValidableContract
{
use Eloquence, Validable;
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'schedules';
/**
* Mass assignable attributes on this model.
*
* @var array
*/
protected $fillable = [
'server_id',
'name',
'cron_day_of_week',
'cron_day_of_month',
'cron_hour',
'cron_minute',
'is_active',
'is_processing',
'last_run_at',
'next_run_at',
];
/**
* @var array
*/
protected $casts = [
'id' => 'integer',
'server_id' => 'integer',
'is_active' => 'boolean',
'is_processing' => 'boolean',
];
/**
* Columns to mutate to a date.
*
* @var array
*/
protected $dates = [
self::CREATED_AT,
self::UPDATED_AT,
'last_run_at',
'next_run_at',
];
/**
* @var array
*/
protected $attributes = [
'name' => null,
'cron_day_of_week' => '*',
'cron_day_of_month' => '*',
'cron_hour' => '*',
'cron_minute' => '*',
'is_active' => true,
'is_processing' => false,
];
/**
* @var array
*/
protected static $applicationRules = [
'server_id' => 'required',
'cron_day_of_week' => 'required',
'cron_day_of_month' => 'required',
'cron_hour' => 'required',
'cron_minute' => 'required',
];
/**
* @var array
*/
protected static $dataIntegrityRules = [
'server_id' => 'exists:servers,id',
'name' => 'nullable|string|max:255',
'cron_day_of_week' => 'string',
'cron_day_of_month' => 'string',
'cron_hour' => 'string',
'cron_minute' => 'string',
'is_active' => 'boolean',
'is_processing' => 'boolean',
'last_run_at' => 'nullable|timestamp',
'next_run_at' => 'nullable|timestamp',
];
/**
* Return a hashid encoded string to represent the ID of the schedule.
*
* @return string
*/
public function getHashidAttribute()
{
return app()->make('hashids')->encode($this->id);
}
/**
* Return tasks belonging to a schedule.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function tasks()
{
return $this->hasMany(Task::class);
}
/**
* Return the server model that a schedule belongs to.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function server()
{
return $this->belongsTo(Server::class);
}
}

View file

@ -42,11 +42,25 @@ class Task extends Model implements CleansAttributes, ValidableContract
protected $table = 'tasks'; protected $table = 'tasks';
/** /**
* Fields that are not mass assignable. * Relationships to be updated when this model is updated.
* *
* @var array * @var array
*/ */
protected $guarded = ['id', 'created_at', 'updated_at']; protected $touches = ['schedule'];
/**
* Fields that are mass assignable.
*
* @var array
*/
protected $fillable = [
'schedule_id',
'squence_id',
'action',
'payload',
'time_offset',
'is_queued',
];
/** /**
* Cast values to correct type. * Cast values to correct type.
@ -55,10 +69,10 @@ class Task extends Model implements CleansAttributes, ValidableContract
*/ */
protected $casts = [ protected $casts = [
'id' => 'integer', 'id' => 'integer',
'user_id' => 'integer', 'schedule_id' => 'integer',
'server_id' => 'integer', 'squence_id' => 'integer',
'queued' => 'boolean', 'time_offset' => 'integer',
'active' => 'boolean', 'is_queued' => 'boolean',
]; ];
/** /**
@ -67,54 +81,32 @@ class Task extends Model implements CleansAttributes, ValidableContract
* @var array * @var array
*/ */
protected $attributes = [ protected $attributes = [
'parent_task_id' => null, 'is_queued' => false,
'chain_order' => null,
'active' => true,
'day_of_week' => '*',
'day_of_month' => '*',
'hour' => '*',
'minute' => '*',
'chain_delay' => null,
'queued' => false,
]; ];
/** /**
* @var array * @var array
*/ */
protected static $applicationRules = [ protected static $applicationRules = [
'server_id' => 'required', 'schedule_id' => 'required',
'squence_id' => 'required',
'action' => 'required', 'action' => 'required',
'data' => 'required', 'payload' => 'required',
'time_offset' => 'required',
]; ];
/** /**
* @var array * @var array
*/ */
protected static $dataIntegrityRules = [ protected static $dataIntegrityRules = [
'name' => 'nullable|string|max:255', 'schedule_id' => 'numeric|exists:schedules,id',
'parent_task_id' => 'nullable|numeric|exists:tasks,id', 'squence_id' => 'numeric|min:1',
'chain_order' => 'nullable|numeric|min:1',
'server_id' => 'numeric|exists:servers,id',
'active' => 'boolean',
'action' => 'string', 'action' => 'string',
'data' => 'string', 'payload' => 'string',
'queued' => 'boolean', 'time_offset' => 'numeric|between:0,900',
'day_of_month' => 'string', 'is_queued' => 'boolean',
'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.
*
* @var array
*/
protected $dates = ['last_run', 'next_run', 'created_at', 'updated_at'];
/** /**
* Return a hashid encoded string to represent the ID of the task. * Return a hashid encoded string to represent the ID of the task.
* *
@ -126,32 +118,26 @@ class Task extends Model implements CleansAttributes, ValidableContract
} }
/** /**
* Gets the server associated with a task. * Return the schedule that a task belongs to.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function schedule()
{
return $this->belongsTo(Schedule::class);
}
/**
* Return the server a task is assigned to, acts as a belongsToThrough.
* *
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/ */
public function server() public function server()
{ {
return $this->belongsTo(Server::class); if ($schedule = $this->schedule) {
return $schedule->server();
} else {
throw new \InvalidArgumentException('Instance of Task must have an associated Schedule in the database.');
} }
/**
* Gets the user associated with a task.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
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

@ -39,6 +39,7 @@ use Pterodactyl\Repositories\Eloquent\SessionRepository;
use Pterodactyl\Repositories\Eloquent\SubuserRepository; use Pterodactyl\Repositories\Eloquent\SubuserRepository;
use Pterodactyl\Repositories\Eloquent\DatabaseRepository; use Pterodactyl\Repositories\Eloquent\DatabaseRepository;
use Pterodactyl\Repositories\Eloquent\LocationRepository; use Pterodactyl\Repositories\Eloquent\LocationRepository;
use Pterodactyl\Repositories\Eloquent\ScheduleRepository;
use Pterodactyl\Repositories\Eloquent\AllocationRepository; use Pterodactyl\Repositories\Eloquent\AllocationRepository;
use Pterodactyl\Repositories\Eloquent\PermissionRepository; use Pterodactyl\Repositories\Eloquent\PermissionRepository;
use Pterodactyl\Repositories\Daemon\ConfigurationRepository; use Pterodactyl\Repositories\Daemon\ConfigurationRepository;
@ -59,6 +60,7 @@ use Pterodactyl\Contracts\Repository\SubuserRepositoryInterface;
use Pterodactyl\Repositories\Eloquent\ServiceVariableRepository; use Pterodactyl\Repositories\Eloquent\ServiceVariableRepository;
use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface; use Pterodactyl\Contracts\Repository\DatabaseRepositoryInterface;
use Pterodactyl\Contracts\Repository\LocationRepositoryInterface; use Pterodactyl\Contracts\Repository\LocationRepositoryInterface;
use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface;
use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface; use Pterodactyl\Contracts\Repository\AllocationRepositoryInterface;
use Pterodactyl\Contracts\Repository\PermissionRepositoryInterface; use Pterodactyl\Contracts\Repository\PermissionRepositoryInterface;
use Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface; use Pterodactyl\Contracts\Repository\Daemon\FileRepositoryInterface;
@ -92,6 +94,7 @@ class RepositoryServiceProvider extends ServiceProvider
$this->app->bind(OptionVariableRepositoryInterface::class, OptionVariableRepository::class); $this->app->bind(OptionVariableRepositoryInterface::class, OptionVariableRepository::class);
$this->app->bind(PackRepositoryInterface::class, PackRepository::class); $this->app->bind(PackRepositoryInterface::class, PackRepository::class);
$this->app->bind(PermissionRepositoryInterface::class, PermissionRepository::class); $this->app->bind(PermissionRepositoryInterface::class, PermissionRepository::class);
$this->app->bind(ScheduleRepositoryInterface::class, ScheduleRepository::class);
$this->app->bind(ServerRepositoryInterface::class, ServerRepository::class); $this->app->bind(ServerRepositoryInterface::class, ServerRepository::class);
$this->app->bind(ServerVariableRepositoryInterface::class, ServerVariableRepository::class); $this->app->bind(ServerVariableRepositoryInterface::class, ServerVariableRepository::class);
$this->app->bind(ServiceRepositoryInterface::class, ServiceRepository::class); $this->app->bind(ServiceRepositoryInterface::class, ServiceRepository::class);

View file

@ -0,0 +1,55 @@
<?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\Schedule;
use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface;
class ScheduleRepository extends EloquentRepository implements ScheduleRepositoryInterface
{
/**
* {@inheritdoc}
*/
public function model()
{
return Schedule::class;
}
/**
* {@inheritdoc}
*/
public function getServerSchedules($server)
{
return $this->getBuilder()->withCount('tasks')->where('server_id', '=', $server)->get($this->getColumns());
}
/**
* {@inheritdoc}
*/
public function getScheduleWithTasks($schedule)
{
return $this->getBuilder()->with('tasks')->find($schedule, $this->getColumns());
}
}

View file

@ -0,0 +1,106 @@
<?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\Schedules;
use Webmozart\Assert\Assert;
use Pterodactyl\Models\Server;
use Illuminate\Database\ConnectionInterface;
use Pterodactyl\Services\Schedules\Tasks\TaskCreationService;
use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface;
class ScheduleCreationService
{
/**
* @var \Illuminate\Database\ConnectionInterface
*/
protected $connection;
/**
* @var \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface
*/
protected $repository;
/**
* @var \Pterodactyl\Services\Schedules\Tasks\TaskCreationService
*/
protected $taskCreationService;
/**
* ScheduleCreationService constructor.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface $repository
* @param \Pterodactyl\Services\Schedules\Tasks\TaskCreationService $taskCreationService
*/
public function __construct(
ConnectionInterface $connection,
ScheduleRepositoryInterface $repository,
TaskCreationService $taskCreationService
) {
$this->connection = $connection;
$this->repository = $repository;
$this->taskCreationService = $taskCreationService;
}
/**
* Create a new schedule for a specific server.
*
* @param int|\Pterodactyl\Models\Server $server
* @param array $data
* @param array $tasks
* @return \Pterodactyl\Models\Schedule
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Service\Schedule\Task\TaskIntervalTooLongException
*/
public function handle($server, array $data, array $tasks = [])
{
Assert::true(($server instanceof Server || is_numeric($server)),
'First argument passed to handle must be numeric or instance of \Pterodactyl\Models\Server, received %s.'
);
$server = ($server instanceof Server) ? $server->id : $server;
$data['server_id'] = $server;
$this->connection->beginTransaction();
$schedule = $this->repository->create($data);
if (! empty($tasks)) {
foreach ($tasks as $index => $task) {
$this->taskCreationService->handle($schedule, [
'time_interval' => array_get($task, 'time_interval'),
'time_value' => array_get($task, 'time_value'),
'sequence_id' => $index + 1,
'action' => array_get($task, 'action'),
'payload' => array_get($task, 'payload'),
], false);
}
}
$this->connection->commit();
return $schedule;
}
}

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\Services\Schedules\Tasks;
use Webmozart\Assert\Assert;
use Pterodactyl\Models\Schedule;
use Pterodactyl\Contracts\Repository\TaskRepositoryInterface;
use Pterodactyl\Exceptions\Service\Schedule\Task\TaskIntervalTooLongException;
class TaskCreationService
{
/**
* @var \Pterodactyl\Contracts\Repository\TaskRepositoryInterface
*/
protected $repository;
/**
* TaskCreationService constructor.
*
* @param \Pterodactyl\Contracts\Repository\TaskRepositoryInterface $repository
*/
public function __construct(TaskRepositoryInterface $repository)
{
$this->repository = $repository;
}
/**
* Create a new task that is assigned to a schedule.
*
* @param int|\Pterodactyl\Models\Schedule $schedule
* @param array $data
* @param bool $returnModel
* @return bool|\Pterodactyl\Models\Task
*
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
* @throws \Pterodactyl\Exceptions\Service\Schedule\Task\TaskIntervalTooLongException
*/
public function handle($schedule, array $data, $returnModel = true)
{
Assert::true(($schedule instanceof Schedule || is_numeric($schedule)),
'First argument passed to handle must be numeric or instance of \Pterodactyl\Models\Schedule, received %s.'
);
$schedule = ($schedule instanceof Schedule) ? $schedule->id : $schedule;
if ($data['time_interval'] === 'm' && $data['time_value'] > 15) {
throw new TaskIntervalTooLongException(trans('exceptions.tasks.chain_interval_too_long'));
}
$delay = $data['time_interval'] === 'm' ? $data['time_value'] * 60 : $data['time_value'];
$repository = ($returnModel) ? $this->repository : $this->repository->withoutFresh();
$task = $repository->create([
'schedule_id' => $schedule,
'sequence_id' => $data['sequence_id'],
'action' => $data['action'],
'payload' => $data['payload'],
'time_offset' => $delay,
]);
return $task;
}
}

View file

@ -1,108 +0,0 @@
<?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

@ -21,8 +21,8 @@ class CreateSchedulesTable extends Migration
$table->string('cron_minute'); $table->string('cron_minute');
$table->boolean('is_active'); $table->boolean('is_active');
$table->boolean('is_processing'); $table->boolean('is_processing');
$table->timestamp('last_run_at'); $table->timestamp('last_run_at')->nullable();
$table->timestamp('next_run_at'); $table->timestamp('next_run_at')->nullable();
$table->timestamps(); $table->timestamps();
$table->foreign('server_id')->references('id')->on('servers')->onDelete('cascade'); $table->foreign('server_id')->references('id')->on('servers')->onDelete('cascade');

File diff suppressed because one or more lines are too long

View file

@ -343,3 +343,20 @@ input.form-autocomplete-stop[readonly] {
bottom: 1px; bottom: 1px;
margin-right: 5px !important; margin-right: 5px !important;
} }
label.control-label > span {
font-size: 80%;
font-weight: 400;
font-style: italic;
color: #dd4b39;
}
label.control-label > span.field-required:before {
content: "required";
color: #dd4b39;
}
label.control-label > span.field-optional:before {
content: "optional";
color: #bbbbbb;
}

View file

@ -19,32 +19,6 @@
// SOFTWARE. // SOFTWARE.
$(document).ready(function () { $(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);
});
$('[data-action="delete-task"]').click(function () { $('[data-action="delete-task"]').click(function () {
var self = $(this); var self = $(this);
swal({ swal({
@ -62,7 +36,7 @@ $(document).ready(function () {
method: 'DELETE', method: 'DELETE',
url: Router.route('server.tasks.delete', { url: Router.route('server.tasks.delete', {
server: Pterodactyl.server.uuidShort, server: Pterodactyl.server.uuidShort,
id: self.data('id'), task: self.data('taskid'),
}), }),
headers: { headers: {
'X-CSRF-TOKEN': $('meta[name="_token"]').attr('content'), 'X-CSRF-TOKEN': $('meta[name="_token"]').attr('content'),
@ -101,7 +75,7 @@ $(document).ready(function () {
method: 'POST', method: 'POST',
url: Router.route('server.tasks.toggle', { url: Router.route('server.tasks.toggle', {
server: Pterodactyl.server.uuidShort, server: Pterodactyl.server.uuidShort,
id: self.data('id'), task: self.data('taskid'),
}), }),
headers: { headers: {
'X-CSRF-TOKEN': $('meta[name="_token"]').attr('content'), 'X-CSRF-TOKEN': $('meta[name="_token"]').attr('content'),

View file

@ -0,0 +1,61 @@
// 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.
$(document).ready(function () {
function setupSelect2() {
$('select[name="chain[time_value][]"]').select2();
$('select[name="chain[time_interval][]"]').select2();
$('select[name="chain[action][]"]').select2();
}
setupSelect2();
$('[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-new-task"]').on('click', function () {
if ($('#containsTaskList').find('.task-list-item').length >= 5) {
swal('Task Limit Reached', 'You may only assign a maximum of 5 tasks to one schedule.');
return;
}
var clone = $('div[data-target="task-clone"]').clone();
clone.insertBefore('#taskAppendBefore').removeAttr('data-target');
clone.find('select:first').attr('selected');
clone.find('input').val('');
clone.find('span.select2-container').remove();
clone.find('div[data-attribute="remove-task-element"]').addClass('input-group').find('div.input-group-btn').removeClass('hidden');
clone.find('button[data-action="remove-task"]').on('click', function () {
clone.remove();
});
setupSelect2();
$(this).data('element', clone);
});
});

View file

@ -62,4 +62,7 @@ return [
'databases' => [ 'databases' => [
'delete_has_databases' => 'Cannot delete a database host server that has active databases linked to it.', 'delete_has_databases' => 'Cannot delete a database host server that has active databases linked to it.',
], ],
'tasks' => [
'chain_interval_too_long' => 'The maximum interval time for a chained task is 15 minutes.',
],
]; ];

View file

@ -6,7 +6,29 @@ return [
'header' => 'Server Console', 'header' => 'Server Console',
'header_sub' => 'Control your server in real time.', 'header_sub' => 'Control your server in real time.',
], ],
'schedule' => [
'new' => [
'header' => 'Create New Schedule',
'header_sub' => 'Create a new set of scheduled tasks for this server.',
'submit' => 'Create Schedule',
],
'task' => [
'time' => 'After',
'action' => 'Perform Action',
'payload' => 'With Payload',
'add_more' => 'Add Another Task',
],
'setup' => 'Schedule Setup',
'day_of_week' => 'Day of Week',
'day_of_month' => 'Day of Month',
'hour' => 'Hour of Day',
'minute' => 'Minute of Hour',
'time_help' => 'The schedule system supports the use of Cronjob syntax when defining when tasks should begin running. Use the fields above to specify when these tasks should begin running or select options from the multiple select menus.',
'task_help' => 'Times for tasks are relative to the previously defined task. Each schedule may have no more than 5 tasks assigned to it and tasks may not be scheduled more than 15 minutes apart.',
],
'tasks' => [ 'tasks' => [
'task_created' => 'Successfully created a new task on the Panel.',
'task_updated' => 'Task has successfully been updated. Any currently queued task actions will be cancelled and run again at the next defined time.',
'header' => 'Scheduled Tasks', 'header' => 'Scheduled Tasks',
'header_sub' => 'Automate your server.', 'header_sub' => 'Automate your server.',
'current' => 'Current Scheduled Tasks', 'current' => 'Current Scheduled Tasks',

View file

@ -71,7 +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', 'tasks' => 'Tasks',
'seconds' => 'Seconds', 'seconds' => 'Seconds',
'minutes' => 'Minutes', 'minutes' => 'Minutes',
]; ];

View file

@ -164,7 +164,7 @@
<a href="{{ route('server.tasks', $server->uuidShort)}}"> <a href="{{ route('server.tasks', $server->uuidShort)}}">
<i class="fa fa-clock-o"></i> <span>@lang('navigation.server.task_management')</span> <i class="fa fa-clock-o"></i> <span>@lang('navigation.server.task_management')</span>
<span class="pull-right-container"> <span class="pull-right-container">
<span class="label label-primary pull-right">{{ \Pterodactyl\Models\Task::select('id')->where('server_id', $server->id)->where('active', 1)->count() }}</span> <span class="label label-primary pull-right">{{ \Pterodactyl\Models\Schedule::select('id')->where('server_id', $server->id)->where('is_active', 1)->count() }}</span>
</span> </span>
</a> </a>
</li> </li>

View file

@ -1,12 +1,12 @@
@section('tasks::chain-template') @section('tasks::chain-template')
<div class="box-footer with-border hidden" data-target="chain-clone"> <div class="box-footer with-border task-list-item" data-target="task-clone">
<div class="row"> <div class="row">
<div class="form-group col-md-3"> <div class="form-group col-md-3">
<label class="control-label">@lang('server.tasks.new.chain_then'):</label> <label class="control-label">@lang('server.schedule.task.time')</label>
<div class="row"> <div class="row">
<div class="col-xs-4"> <div class="col-xs-4">
<select name="chain[time_value][]" class="form-control"> <select name="chain[time_value][]" class="form-control">
@foreach(range(1, 60) as $number) @foreach(range(0, 59) as $number)
<option value="{{ $number }}">{{ $number }}</option> <option value="{{ $number }}">{{ $number }}</option>
@endforeach @endforeach
</select> </select>
@ -20,7 +20,7 @@
</div> </div>
</div> </div>
<div class="form-group col-md-3"> <div class="form-group col-md-3">
<label class="control-label">@lang('server.tasks.new.chain_do'):</label> <label class="control-label">@lang('server.schedule.task.action')</label>
<div> <div>
<select name="chain[action][]" class="form-control"> <select name="chain[action][]" class="form-control">
<option value="command">@lang('server.tasks.actions.command')</option> <option value="command">@lang('server.tasks.actions.command')</option>
@ -29,11 +29,11 @@
</div> </div>
</div> </div>
<div class="form-group col-md-6"> <div class="form-group col-md-6">
<label class="control-label">@lang('server.tasks.new.chain_arguments'):</label> <label class="control-label">@lang('server.schedule.task.payload')</label>
<div class="input-group"> <div data-attribute="remove-task-element">
<input type="text" name="chain[payload][]" class="form-control"> <input type="text" name="chain[payload][]" class="form-control">
<div class="input-group-btn"> <div class="input-group-btn hidden">
<button type="button" class="btn btn-danger" data-action="remove-chain-element"><i class="fa fa-close"></i></button> <button type="button" class="btn btn-danger" data-action="remove-task"><i class="fa fa-close"></i></button>
</div> </div>
</div> </div>
</div> </div>

View file

@ -48,40 +48,40 @@
<tr> <tr>
<th>@lang('strings.name')</th> <th>@lang('strings.name')</th>
<th class="text-center">@lang('strings.queued')</th> <th class="text-center">@lang('strings.queued')</th>
<th class="text-center">@lang('strings.child_tasks')</th> <th class="text-center">@lang('strings.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>
<th></th> <th></th>
</tr> </tr>
@foreach($tasks as $task) @foreach($schedules as $schedule)
<tr @if(! $task->active)class="muted muted-hover"@endif> <tr @if(! $schedule->is_active)class="muted muted-hover"@endif>
<td class="middle"> <td class="middle">
@can('edit-task', $server) @can('edit-schedule', $server)
<a href="{{ route('server.tasks.view', ['server' => $server->uuidShort, 'task' => $task->hashid]) }}">{{ $task->name }}</a> <a href="{{ route('server.tasks.view', ['server' => $server->uuidShort, '$schedule' => $schedule->hashid]) }}">{{ $schedule->name }}</a>
@else @else
{{ $task->name }} {{ $schedule->name }}
@endcan @endcan
</td> </td>
<td class="middle text-center"> <td class="middle text-center">
@if ($task->queued) @if ($schedule->is_processing)
<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 text-center"><span class="label label-primary">{{ $schedule->tasks_count }}</span></td>
<td class="middle"> <td class="middle">
@if($task->last_run) @if($schedule->last_run_at)
{{ Carbon::parse($task->last_run)->toDayDateTimeString() }}<br /><span class="text-muted small">({{ Carbon::parse($task->last_run)->diffForHumans() }})</span> {{ Carbon::parse($schedule->last_run_at)->toDayDateTimeString() }}<br /><span class="text-muted small">({{ Carbon::parse($schedule->last_run_at)->diffForHumans() }})</span>
@else @else
<em class="text-muted">@lang('strings.not_run_yet')</em> <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($schedule->is_active)
@if($task->last_run) @if($schedule->last_run_at)
{{ Carbon::parse($task->next_run)->toDayDateTimeString() }}<br /><span class="text-muted small">({{ Carbon::parse($task->next_run)->diffForHumans() }})</span> {{ Carbon::parse($schedule->next_run_at)->toDayDateTimeString() }}<br /><span class="text-muted small">({{ Carbon::parse($schedule->next_run_at)->diffForHumans() }})</span>
@else @else
<em class="text-muted">@lang('strings.not_run_yet')</em> <em class="text-muted">@lang('strings.not_run_yet')</em>
@endif @endif
@ -89,15 +89,14 @@
<em>n/a</em> <em>n/a</em>
@endif @endif
</td> </td>
@can('delete-task', $server) @can('delete-schedule', $server)
<td class="text-center middle"><a href="#" data-action="delete-task" data-id="{{ $task->id }}"><i class="fa fa-fw fa-trash-o text-danger" data-toggle="tooltip" data-placement="top" title="@lang('strings.delete')"></i></a></td> <td class="text-center middle"><a href="#" data-action="delete-task" data-taskid="{{ $schedule->hashid }}"><i class="fa fa-fw fa-trash-o text-danger" data-toggle="tooltip" data-placement="top" title="@lang('strings.delete')"></i></a></td>
@endcan @endcan
@can('toggle-task', $server) @can('toggle-schedule', $server)
<td class="text-center middle"><a href="#" data-action="toggle-task" data-active="{{ $task->active }}" data-id="{{ $task->id }}"><i class="fa fa-fw fa-eye-slash text-primary" data-toggle="tooltip" data-placement="top" title="@lang('server.tasks.toggle')"></i></a></td> <td class="text-center middle"><a href="#" data-action="toggle-task" data-active="{{ $schedule->active }}" data-taskid="{{ $schedule->hashid }}"><i class="fa fa-fw fa-eye-slash text-primary" data-toggle="tooltip" data-placement="top" title="@lang('server.tasks.toggle')"></i></a></td>
@endcan @endcan
</tr> </tr>
@endforeach @endforeach
</tbody> </tbody>
</table> </table>
</div> </div>
@ -109,5 +108,5 @@
@section('footer-scripts') @section('footer-scripts')
@parent @parent
{!! Theme::js('js/frontend/server.socket.js') !!} {!! Theme::js('js/frontend/server.socket.js') !!}
{!! Theme::js('js/frontend/tasks.js') !!} {!! Theme::js('js/frontend/tasks/management-actions.js') !!}
@endsection @endsection

View file

@ -30,12 +30,12 @@
@endsection @endsection
@section('content-header') @section('content-header')
<h1>@lang('server.tasks.new.header')<small>@lang('server.tasks.new.header_sub')</small></h1> <h1>@lang('server.schedule.new.header')<small>@lang('server.schedule.new.header_sub')</small></h1>
<ol class="breadcrumb"> <ol class="breadcrumb">
<li><a href="{{ route('index') }}">@lang('strings.home')</a></li> <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.index', $server->uuidShort) }}">{{ $server->name }}</a></li>
<li><a href="{{ route('server.tasks', $server->uuidShort) }}">@lang('navigation.server.task_management')</a></li> <li><a href="{{ route('server.tasks', $server->uuidShort) }}">@lang('navigation.server.task_management')</a></li>
<li class="active">@lang('server.tasks.new_task')</li> <li class="active">@lang('server.schedule.new.header')</li>
</ol> </ol>
@endsection @endsection
@ -44,28 +44,22 @@
<div class="row"> <div class="row">
<div class="col-xs-12"> <div class="col-xs-12">
<div class="box box-primary"> <div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">@lang('server.schedule.setup')</h3>
</div>
<div class="box-body"> <div class="box-body">
<div class="row"> <div class="row">
<div class="form-group col-xs-12"> <div class="form-group col-xs-12">
<label class="control-label">@lang('server.tasks.new.task_name'):</label> <label class="control-label" for="scheduleName">@lang('strings.name') <span class="field-optional"></span></label>
<div> <div>
<input type="text" name="name" class="form-control" value="{{ old('name') }}" /> <input type="text" name="name" id="scheduleName" class="form-control" value="{{ old('name') }}" />
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-3 col-sm-6"> <div class="col-xs-6 col-md-3">
<div class="box"> <div class="form-group">
<div class="box-header with-border"> <label for="scheduleDayOfWeek" class="control-label">@lang('server.schedule.day_of_week')</label>
<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> <div>
<select data-action="update-field" data-field="day_of_week" class="form-control" multiple> <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="0">@lang('server.tasks.new.sun')</option>
@ -78,24 +72,13 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group col-md-12"> <div class="form-group">
<label class="control-label">@lang('server.tasks.new.custom')</label> <input type="text" id="scheduleDayOfWeek" class="form-control" name="day_of_week" value="{{ old('day_of_week') }}" />
<div>
<input type="text" class="form-control" name="day_of_week" />
</div> </div>
</div> </div>
</div> <div class="col-xs-6 col-md-3">
</div> <div class="form-group">
</div> <label for="scheduleDayOfMonth" class="control-label">@lang('server.schedule.day_of_month')</label>
</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> <div>
<select data-action="update-field" data-field="day_of_month" class="form-control" multiple> <select data-action="update-field" data-field="day_of_month" class="form-control" multiple>
@foreach(range(1, 31) as $i) @foreach(range(1, 31) as $i)
@ -104,24 +87,13 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group">
<input type="text" id="scheduleDayOfMonth" class="form-control" name="day_of_month" value="{{ old('day_of_month') }}" />
</div>
</div>
<div class="col-xs-6 col-md-3">
<div class="form-group col-md-12"> <div class="form-group col-md-12">
<label class="control-label">@lang('server.tasks.new.custom')</label> <label for="scheduleHour" class="control-label">@lang('server.schedule.hour')</label>
<div>
<input type="text" class="form-control" name="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> <div>
<select data-action="update-field" data-field="hour" class="form-control" multiple> <select data-action="update-field" data-field="hour" class="form-control" multiple>
@foreach(range(0, 23) as $i) @foreach(range(0, 23) as $i)
@ -131,23 +103,12 @@
</div> </div>
</div> </div>
<div class="form-group col-md-12"> <div class="form-group col-md-12">
<label class="control-label">@lang('server.tasks.new.custom')</label> <input type="text" id="scheduleHour" class="form-control" name="hour" value="{{ old('hour') }}" />
<div>
<input type="text" class="form-control" name="hour" />
</div> </div>
</div> </div>
</div> <div class="col-xs-6 col-md-3">
</div> <div class="form-group">
</div> <label for="scheduleMinute" class="control-label">@lang('server.schedule.minute')</label>
</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> <div>
<select data-action="update-field" data-field="minute" class="form-control" multiple> <select data-action="update-field" data-field="minute" class="form-control" multiple>
@foreach(range(0, 55) as $i) @foreach(range(0, 55) as $i)
@ -158,60 +119,41 @@
</select> </select>
</div> </div>
</div> </div>
<div class="form-group col-md-12"> <div class="form-group">
<label class="control-label">@lang('server.tasks.new.custom')</label> <input type="text" id="scheduleMinute" class="form-control" name="minute" value="{{ old('minute') }}" />
<div>
<input type="text" class="form-control" name="minute" />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="box-footer with-border">
<p class="small text-muted no-margin-bottom">@lang('server.schedule.time_help')</p>
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-xs-12"> <div class="col-xs-12">
<div class="box box-primary"> <div class="box box-primary" id="containsTaskList">
<div class="box-body"> @include('partials.schedules.task-template')
<div class="row"> <div class="box-footer with-border" id="taskAppendBefore">
<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">@lang('server.tasks.actions.command')</option>
<option value="power">@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') }}">
<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"> <div class="pull-left">
<p class="text-muted small">Times for chain arguments are relative to the previous argument.</p> <p class="text-muted small">@lang('server.schedule.task_help')</p>
</div> </div>
<div class="pull-right"> <div class="pull-right">
{!! csrf_field() !!} {!! 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="button" class="btn btn-sm btn-default" data-action="add-new-task"><i class="fa fa-plus"></i> @lang('server.schedule.task.add_more')</button>
<button type="submit" class="btn btn-sm btn-success">@lang('server.tasks.new.submit')</button> <button type="submit" class="btn btn-sm btn-success">@lang('server.schedule.new.submit')</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</form> </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') !!}
{!! Theme::js('js/frontend/tasks.js') !!} {!! Theme::js('js/frontend/tasks/view-actions.js') !!}
@endsection @endsection

View file

@ -213,7 +213,7 @@
@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') !!}
{!! Theme::js('js/frontend/tasks.js') !!} {!! Theme::js('js/frontend/tasks/view-actions.js') !!}
<script> <script>
$(document).ready(function () { $(document).ready(function () {
$.each(Pterodactyl.chained, function (index, value) { $.each(Pterodactyl.chained, function (index, value) {

View file

@ -21,7 +21,7 @@
* 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.
*/ */
use Pterodactyl\Http\Middleware\Server\SubuserAccess; use Pterodactyl\Http\Middleware\Server\ScheduleAccess;
Route::get('/', 'ConsoleController@index')->name('server.index'); Route::get('/', 'ConsoleController@index')->name('server.index');
Route::get('/console', 'ConsoleController@console')->name('server.console'); Route::get('/console', 'ConsoleController@console')->name('server.console');
@ -90,17 +90,17 @@ Route::group(['prefix' => 'users'], function () {
| Endpoint: /server/{server}/tasks | Endpoint: /server/{server}/tasks
| |
*/ */
Route::group(['prefix' => 'tasks'], function () { Route::group(['prefix' => 'schedules'], function () {
Route::get('/', 'Tasks\TaskManagementController@index')->name('server.tasks'); Route::get('/', 'Tasks\TaskManagementController@index')->name('server.tasks');
Route::get('/new', 'Tasks\TaskManagementController@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::get('/view/{schedule}', 'Tasks\TaskManagementController@view')->middleware(ScheduleAccess::class)->name('server.tasks.view');
Route::post('/new', 'Tasks\TaskManagementController@store'); Route::post('/new', 'Tasks\TaskManagementController@store');
Route::patch('/view/{task}', 'Tasks\TaskManagementController@update'); Route::patch('/view/{schedule}', 'Tasks\TaskManagementController@update')->middleware(ScheduleAccess::class);
Route::patch('/view/{task}/toggle', 'Tasks\ToggleTaskController@index')->name('server.tasks.toggle'); Route::patch('/view/{schedule}/toggle', 'Tasks\TaskToggleController@index')->middleware(ScheduleAccess::class)->name('server.tasks.toggle');
Route::delete('/view/{task}/delete', 'Tasks\TaskManagementController@delete')->name('server.tasks.delete'); Route::delete('/view/{schedule}/delete', 'Tasks\TaskManagementController@delete')->middleware(ScheduleAccess::class)->name('server.tasks.delete');
}); });
/* /*