Skip to content

Commit 1e0d630

Browse files
committed
Finish building out schedule management functionality
1 parent 4ac6507 commit 1e0d630

File tree

16 files changed

+510
-79
lines changed

16 files changed

+510
-79
lines changed

app/Helpers/Utilities.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
namespace Pterodactyl\Helpers;
44

55
use Exception;
6+
use Carbon\Carbon;
7+
use Cron\CronExpression;
68
use Illuminate\Support\Facades\Log;
79

810
class Utilities
@@ -32,4 +34,20 @@ public static function randomStringWithSpecialCharacters(int $length = 16): stri
3234

3335
return $string;
3436
}
37+
38+
/**
39+
* Converts schedule cron data into a carbon object.
40+
*
41+
* @param string $minute
42+
* @param string $hour
43+
* @param string $dayOfMonth
44+
* @param string $dayOfWeek
45+
* @return \Carbon\Carbon
46+
*/
47+
public static function getScheduleNextRunDate(string $minute, string $hour, string $dayOfMonth, string $dayOfWeek)
48+
{
49+
return Carbon::instance(CronExpression::factory(
50+
sprintf('%s %s %s * %s', $minute, $hour, $dayOfMonth, $dayOfWeek)
51+
)->getNextRunDate());
52+
}
3553
}

app/Http/Controllers/Api/Client/Servers/ScheduleController.php

Lines changed: 128 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,51 @@
22

33
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
44

5+
use Exception;
6+
use Carbon\Carbon;
57
use Illuminate\Http\Request;
8+
use Illuminate\Http\Response;
69
use Pterodactyl\Models\Server;
710
use Pterodactyl\Models\Schedule;
11+
use Illuminate\Http\JsonResponse;
12+
use Pterodactyl\Helpers\Utilities;
13+
use Pterodactyl\Exceptions\DisplayException;
14+
use Pterodactyl\Repositories\Eloquent\ScheduleRepository;
815
use Pterodactyl\Transformers\Api\Client\ScheduleTransformer;
916
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
1017
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
18+
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\ViewScheduleRequest;
19+
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\StoreScheduleRequest;
20+
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\DeleteScheduleRequest;
21+
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\UpdateScheduleRequest;
1122

1223
class ScheduleController extends ClientApiController
1324
{
25+
/**
26+
* @var \Pterodactyl\Repositories\Eloquent\ScheduleRepository
27+
*/
28+
private $repository;
29+
30+
/**
31+
* ScheduleController constructor.
32+
*
33+
* @param \Pterodactyl\Repositories\Eloquent\ScheduleRepository $repository
34+
*/
35+
public function __construct(ScheduleRepository $repository)
36+
{
37+
parent::__construct();
38+
39+
$this->repository = $repository;
40+
}
41+
1442
/**
1543
* Returns all of the schedules belonging to a given server.
1644
*
17-
* @param \Illuminate\Http\Request $request
45+
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\ViewScheduleRequest $request
1846
* @param \Pterodactyl\Models\Server $server
1947
* @return array
2048
*/
21-
public function index(Request $request, Server $server)
49+
public function index(ViewScheduleRequest $request, Server $server)
2250
{
2351
$schedules = $server->schedule;
2452
$schedules->loadMissing('tasks');
@@ -28,15 +56,44 @@ public function index(Request $request, Server $server)
2856
->toArray();
2957
}
3058

59+
/**
60+
* Store a new schedule for a server.
61+
*
62+
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\StoreScheduleRequest $request
63+
* @param \Pterodactyl\Models\Server $server
64+
* @return array
65+
*
66+
* @throws \Pterodactyl\Exceptions\DisplayException
67+
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
68+
*/
69+
public function store(StoreScheduleRequest $request, Server $server)
70+
{
71+
/** @var \Pterodactyl\Models\Schedule $model */
72+
$model = $this->repository->create([
73+
'server_id' => $server->id,
74+
'name' => $request->input('name'),
75+
'cron_day_of_week' => $request->input('day_of_week'),
76+
'cron_day_of_month' => $request->input('day_of_month'),
77+
'cron_hour' => $request->input('hour'),
78+
'cron_minute' => $request->input('minute'),
79+
'is_active' => (bool) $request->input('is_active'),
80+
'next_run_at' => $this->getNextRunAt($request),
81+
]);
82+
83+
return $this->fractal->item($model)
84+
->transformWith($this->getTransformer(ScheduleTransformer::class))
85+
->toArray();
86+
}
87+
3188
/**
3289
* Returns a specific schedule for the server.
3390
*
34-
* @param \Illuminate\Http\Request $request
91+
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\ViewScheduleRequest $request
3592
* @param \Pterodactyl\Models\Server $server
3693
* @param \Pterodactyl\Models\Schedule $schedule
3794
* @return array
3895
*/
39-
public function view(Request $request, Server $server, Schedule $schedule)
96+
public function view(ViewScheduleRequest $request, Server $server, Schedule $schedule)
4097
{
4198
if ($schedule->server_id !== $server->id) {
4299
throw new NotFoundHttpException;
@@ -48,4 +105,71 @@ public function view(Request $request, Server $server, Schedule $schedule)
48105
->transformWith($this->getTransformer(ScheduleTransformer::class))
49106
->toArray();
50107
}
108+
109+
/**
110+
* Updates a given schedule with the new data provided.
111+
*
112+
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\UpdateScheduleRequest $request
113+
* @param \Pterodactyl\Models\Server $server
114+
* @param \Pterodactyl\Models\Schedule $schedule
115+
* @return array
116+
*
117+
* @throws \Pterodactyl\Exceptions\DisplayException
118+
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
119+
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
120+
*/
121+
public function update(UpdateScheduleRequest $request, Server $server, Schedule $schedule)
122+
{
123+
$this->repository->update($schedule->id, [
124+
'name' => $request->input('name'),
125+
'cron_day_of_week' => $request->input('day_of_week'),
126+
'cron_day_of_month' => $request->input('day_of_month'),
127+
'cron_hour' => $request->input('hour'),
128+
'cron_minute' => $request->input('minute'),
129+
'is_active' => (bool) $request->input('is_active'),
130+
'next_run_at' => $this->getNextRunAt($request),
131+
]);
132+
133+
return $this->fractal->item($schedule->refresh())
134+
->transformWith($this->getTransformer(ScheduleTransformer::class))
135+
->toArray();
136+
}
137+
138+
/**
139+
* Deletes a schedule and it's associated tasks.
140+
*
141+
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\DeleteScheduleRequest $request
142+
* @param \Pterodactyl\Models\Server $server
143+
* @param \Pterodactyl\Models\Schedule $schedule
144+
* @return \Illuminate\Http\JsonResponse
145+
*/
146+
public function delete(DeleteScheduleRequest $request, Server $server, Schedule $schedule)
147+
{
148+
$this->repository->delete($schedule->id);
149+
150+
return JsonResponse::create([], Response::HTTP_NO_CONTENT);
151+
}
152+
153+
/**
154+
* Get the next run timestamp based on the cron data provided.
155+
*
156+
* @param \Illuminate\Http\Request $request
157+
* @return \Carbon\Carbon
158+
* @throws \Pterodactyl\Exceptions\DisplayException
159+
*/
160+
protected function getNextRunAt(Request $request): Carbon
161+
{
162+
try {
163+
return Utilities::getScheduleNextRunDate(
164+
$request->input('minute'),
165+
$request->input('hour'),
166+
$request->input('day_of_month'),
167+
$request->input('day_of_week')
168+
);
169+
} catch (Exception $exception) {
170+
throw new DisplayException(
171+
'The cron data provided does not evaluate to a valid expression.'
172+
);
173+
}
174+
}
51175
}

app/Http/Requests/Api/Client/ClientApiRequest.php

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
namespace Pterodactyl\Http\Requests\Api\Client;
44

5-
use Pterodactyl\Models\User;
65
use Pterodactyl\Models\Server;
76
use Pterodactyl\Contracts\Http\ClientPermissionsRequest;
87
use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest;
@@ -20,7 +19,15 @@ class ClientApiRequest extends ApplicationApiRequest
2019
public function authorize(): bool
2120
{
2221
if ($this instanceof ClientPermissionsRequest || method_exists($this, 'permission')) {
23-
return $this->user()->can($this->permission(), $this->getModel(Server::class));
22+
$server = $this->route()->parameter('server');
23+
24+
if ($server instanceof Server) {
25+
return $this->user()->can($this->permission(), $server);
26+
}
27+
28+
// If there is no server available on the reqest, trigger a failure since
29+
// we expect there to be one at this point.
30+
return false;
2431
}
2532

2633
return true;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Schedules;
4+
5+
use Pterodactyl\Models\Permission;
6+
7+
class DeleteScheduleRequest extends ViewScheduleRequest
8+
{
9+
/**
10+
* @return string
11+
*/
12+
public function permission(): string
13+
{
14+
return Permission::ACTION_SCHEDULE_DELETE;
15+
}
16+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Schedules;
4+
5+
use Pterodactyl\Models\Permission;
6+
7+
class StoreScheduleRequest extends ViewScheduleRequest
8+
{
9+
/**
10+
* @return string
11+
*/
12+
public function permission(): string
13+
{
14+
return Permission::ACTION_SCHEDULE_CREATE;
15+
}
16+
17+
/**
18+
* @return array
19+
*/
20+
public function rules(): array
21+
{
22+
return [
23+
'name' => 'required|string|min:1',
24+
'is_active' => 'boolean',
25+
'minute' => 'required|string',
26+
'hour' => 'required|string',
27+
'day_of_month' => 'required|string',
28+
'day_of_week' => 'required|string',
29+
];
30+
}
31+
}

app/Http/Requests/Api/Client/Servers/Schedules/StoreTaskRequest.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@
33
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Schedules;
44

55
use Pterodactyl\Models\Permission;
6-
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
76

8-
class StoreTaskRequest extends ClientApiRequest
7+
class StoreTaskRequest extends ViewScheduleRequest
98
{
109
/**
1110
* Determine if the user is allowed to create a new task for this schedule. We simply
@@ -14,7 +13,7 @@ class StoreTaskRequest extends ClientApiRequest
1413
*
1514
* @return string
1615
*/
17-
public function permission()
16+
public function permission(): string
1817
{
1918
return Permission::ACTION_SCHEDULE_UPDATE;
2019
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Schedules;
4+
5+
use Pterodactyl\Models\Permission;
6+
7+
class UpdateScheduleRequest extends StoreScheduleRequest
8+
{
9+
/**
10+
* @return string
11+
*/
12+
public function permission(): string
13+
{
14+
return Permission::ACTION_SCHEDULE_UPDATE;
15+
}
16+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Schedules;
4+
5+
use Pterodactyl\Models\Task;
6+
use Pterodactyl\Models\Server;
7+
use Pterodactyl\Models\Schedule;
8+
use Pterodactyl\Models\Permission;
9+
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
10+
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
11+
12+
class ViewScheduleRequest extends ClientApiRequest
13+
{
14+
/**
15+
* Determine if this resource can be viewed.
16+
*
17+
* @return bool
18+
*/
19+
public function authorize(): bool
20+
{
21+
if (! parent::authorize()) {
22+
return false;
23+
}
24+
25+
$server = $this->route()->parameter('server');
26+
$schedule = $this->route()->parameter('schedule');
27+
28+
// If the schedule does not belong to this server throw a 404 error. Also throw an
29+
// error if the task being requested does not belong to the associated schedule.
30+
if ($server instanceof Server && $schedule instanceof Schedule) {
31+
$task = $this->route()->parameter('task');
32+
33+
if ($schedule->server_id !== $server->id || ($task instanceof Task && $task->schedule_id !== $schedule->id)) {
34+
throw new NotFoundHttpException(
35+
'The requested resource does not exist on the system.'
36+
);
37+
}
38+
}
39+
40+
return true;
41+
}
42+
43+
/**
44+
* @return string
45+
*/
46+
public function permission(): string
47+
{
48+
return Permission::ACTION_SCHEDULE_READ;
49+
}
50+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { rawDataToServerSchedule, Schedule } from '@/api/server/schedules/getServerSchedules';
2+
import http from '@/api/http';
3+
4+
type Data = Pick<Schedule, 'cron' | 'name' | 'isActive'> & { id?: number }
5+
6+
export default (uuid: string, schedule: Data): Promise<Schedule> => {
7+
return new Promise((resolve, reject) => {
8+
http.post(`/api/client/servers/${uuid}/schedules${schedule.id ? `/${schedule.id}` : ''}`, {
9+
is_active: schedule.isActive,
10+
name: schedule.name,
11+
minute: schedule.cron.minute,
12+
hour: schedule.cron.hour,
13+
day_of_month: schedule.cron.dayOfMonth,
14+
day_of_week: schedule.cron.dayOfWeek,
15+
})
16+
.then(({ data }) => resolve(rawDataToServerSchedule(data.attributes)))
17+
.catch(reject);
18+
});
19+
};
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import http from '@/api/http';
2+
3+
export default (uuid: string, schedule: number): Promise<void> => {
4+
return new Promise((resolve, reject) => {
5+
http.delete(`/api/client/servers/${uuid}/schedules/${schedule}`)
6+
.then(() => resolve())
7+
.catch(reject);
8+
});
9+
};

0 commit comments

Comments
 (0)