Skip to content

Commit 85c0fe6

Browse files
authored
Merge pull request #1443 from hydephp/realtime-compiler-dashboard-improvements
Realtime compiler dashboard improvements
2 parents a60c42d + ef474e7 commit 85c0fe6

File tree

2 files changed

+114
-152
lines changed

2 files changed

+114
-152
lines changed

packages/realtime-compiler/src/Http/DashboardController.php

Lines changed: 113 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use Illuminate\Support\Arr;
1212
use Hyde\Pages\MarkdownPage;
1313
use Hyde\Pages\MarkdownPost;
14+
use Desilva\Microserve\Response;
1415
use Hyde\Pages\Concerns\HydePage;
1516
use Hyde\Pages\DocumentationPage;
1617
use Hyde\Support\Models\RouteKey;
@@ -28,31 +29,6 @@
2829
use Hyde\Framework\Actions\CreatesNewMarkdownPostFile;
2930
use Symfony\Component\HttpKernel\Exception\HttpException;
3031

31-
use function e;
32-
use function str;
33-
use function time;
34-
use function trim;
35-
use function round;
36-
use function rtrim;
37-
use function strlen;
38-
use function substr;
39-
use function is_bool;
40-
use function basename;
41-
use function in_array;
42-
use function json_decode;
43-
use function json_encode;
44-
use function substr_count;
45-
use function array_combine;
46-
use function trigger_error;
47-
use function escapeshellarg;
48-
use function file_get_contents;
49-
use function str_starts_with;
50-
use function str_replace;
51-
use function array_merge;
52-
use function sprintf;
53-
use function config;
54-
use function app;
55-
5632
/**
5733
* @internal This class is not intended to be edited outside the Hyde Realtime Compiler.
5834
*/
@@ -73,6 +49,8 @@ class DashboardController
7349
'The dashboard update your project files. You can disable this by setting `server.dashboard.interactive` to `false` in `config/hyde.php`.',
7450
];
7551

52+
protected JsonResponse $response;
53+
7654
public function __construct()
7755
{
7856
$this->title = config('hyde.name').' - Dashboard';
@@ -82,64 +60,60 @@ public function __construct()
8260

8361
if ($this->request->method === 'POST') {
8462
$this->isAsync = (getallheaders()['X-RC-Handler'] ?? getallheaders()['x-rc-handler'] ?? null) === 'Async';
63+
}
64+
}
8565

66+
public function handle(): Response
67+
{
68+
if ($this->request->method === 'POST') {
8669
if (! $this->isInteractive()) {
87-
$this->abort(403, 'Enable `server.editor` in `config/hyde.php` to use interactive dashboard features.');
70+
return $this->sendJsonErrorResponse(403, 'Enable `server.editor` in `config/hyde.php` to use interactive dashboard features.');
71+
}
72+
73+
if ($this->shouldUnsafeRequestBeBlocked()) {
74+
return $this->sendJsonErrorResponse(403, "Refusing to serve request from address {$_SERVER['REMOTE_ADDR']} (must be on localhost)");
8875
}
8976

9077
try {
91-
$this->blockUnsafeRequests();
92-
$this->handlePostRequest();
78+
return $this->handlePostRequest();
9379
} catch (HttpException $exception) {
9480
if (! $this->isAsync) {
9581
throw $exception;
9682
}
9783

98-
$this->sendJsonErrorResponse($exception);
84+
return $this->sendJsonErrorResponse($exception->getStatusCode(), $exception->getMessage());
9985
}
10086
}
101-
}
102-
103-
protected function handlePostRequest(): void
104-
{
105-
$actions = array_combine($actions = [
106-
'openInExplorer',
107-
'openPageInEditor',
108-
'openMediaFileInEditor',
109-
'createPage',
110-
], $actions);
111-
112-
$action = $this->request->data['action'] ?? $this->abort(400, 'Must provide action');
113-
$action = $actions[$action] ?? $this->abort(403, "Invalid action '$action'");
114-
115-
if ($action === 'openInExplorer') {
116-
$this->openInExplorer();
117-
}
118-
119-
if ($action === 'openPageInEditor') {
120-
$routeKey = $this->request->data['routeKey'] ?? $this->abort(400, 'Must provide routeKey');
121-
$page = Routes::getOrFail($routeKey)->getPage();
122-
$this->openPageInEditor($page);
123-
}
12487

125-
if ($action === 'openMediaFileInEditor') {
126-
$identifier = $this->request->data['identifier'] ?? $this->abort(400, 'Must provide identifier');
127-
$asset = @MediaFile::all()[$identifier] ?? $this->abort(404, "Invalid media identifier '$identifier'");
128-
$this->openMediaFileInEditor($asset);
129-
}
130-
131-
if ($action === 'createPage') {
132-
$this->createPage();
133-
}
88+
return new HtmlResponse(200, 'OK', [
89+
'body' => $this->show(),
90+
]);
13491
}
13592

136-
public function show(): string
93+
protected function show(): string
13794
{
13895
return AnonymousViewCompiler::handle(__DIR__.'/../../resources/dashboard.blade.php', array_merge(
13996
(array) $this, ['dashboard' => $this, 'request' => $this->request],
14097
));
14198
}
14299

100+
protected function handlePostRequest(): JsonResponse
101+
{
102+
$action = $this->request->data['action'] ?? $this->abort(400, 'Must provide action');
103+
104+
match ($action) {
105+
'openInExplorer' => $this->openInExplorer(),
106+
'openPageInEditor' => $this->openPageInEditor(),
107+
'openMediaFileInEditor' => $this->openMediaFileInEditor(),
108+
'createPage' => $this->createPage(),
109+
default => $this->abort(403, "Invalid action '$action'"),
110+
};
111+
112+
return $this->response ?? new JsonResponse(200, 'OK', [
113+
'message' => 'Action completed successfully',
114+
]);
115+
}
116+
143117
public function getVersion(): string
144118
{
145119
$version = InstalledVersions::getPrettyVersion('hyde/realtime-compiler');
@@ -186,7 +160,7 @@ public static function highlightMediaLibraryCode(string $contents): HtmlString
186160
$contents = str_replace([''', '"'], ['%SQT%', '%DQT%'], $contents); // Temporarily replace escaped quotes
187161

188162
if (static::isMediaFileProbablyMinified($contents)) {
189-
return new HtmlString(substr($contents, 0, 800));
163+
return new HtmlString(substr($contents, 0, count(MediaFile::files()) === 1 ? 2000 : 800));
190164
}
191165

192166
$highlighted = str($contents)->explode("\n")->slice(0, 25)->map(function (string $line): string {
@@ -240,7 +214,7 @@ public function getTip(): HtmlString
240214

241215
public static function enabled(): bool
242216
{
243-
// Previously, the setting was hyde.server.dashboard, so for backwards compatability we need this
217+
/** @deprecated Previously, the setting was hyde.server.dashboard, so for backwards compatability we need this */
244218
if (is_bool($oldConfig = config('hyde.server.dashboard'))) {
245219
trigger_error('Using `hyde.server.dashboard` as boolean is deprecated. Please use `hyde.server.dashboard.enabled` instead.', E_USER_DEPRECATED);
246220

@@ -311,79 +285,76 @@ protected function loadFlashData(): void
311285

312286
protected function openInExplorer(): void
313287
{
314-
if ($this->isInteractive()) {
315-
$binary = $this->findGeneralOpenBinary();
316-
$path = Hyde::path();
288+
$binary = $this->findGeneralOpenBinary();
289+
$path = Hyde::path();
317290

318-
Process::run(sprintf('%s %s', $binary, escapeshellarg($path)))->throw();
319-
}
291+
Process::run(sprintf('%s %s', $binary, escapeshellarg($path)))->throw();
320292
}
321293

322-
protected function openPageInEditor(HydePage $page): void
294+
protected function openPageInEditor(): void
323295
{
324-
if ($this->isInteractive()) {
325-
$binary = $this->findGeneralOpenBinary();
326-
$path = Hyde::path($page->getSourcePath());
296+
$routeKey = $this->request->data['routeKey'] ?? $this->abort(400, 'Must provide routeKey');
297+
$page = Routes::getOrFail($routeKey)->getPage();
327298

328-
if (! (str_ends_with($path, '.md') || str_ends_with($path, '.blade.php'))) {
329-
$this->abort(403, sprintf("Refusing to open unsafe file '%s'", basename($path)));
330-
}
299+
$binary = $this->findGeneralOpenBinary();
300+
$path = Hyde::path($page->getSourcePath());
331301

332-
Process::run(sprintf('%s %s', $binary, escapeshellarg($path)))->throw();
302+
if (! (str_ends_with($path, '.md') || str_ends_with($path, '.blade.php'))) {
303+
$this->abort(403, sprintf("Refusing to open unsafe file '%s'", basename($path)));
333304
}
305+
306+
Process::run(sprintf('%s %s', $binary, escapeshellarg($path)))->throw();
334307
}
335308

336-
protected function openMediaFileInEditor(MediaFile $file): void
309+
protected function openMediaFileInEditor(): void
337310
{
338-
if ($this->isInteractive()) {
339-
$binary = $this->findGeneralOpenBinary();
340-
$path = $file->getAbsolutePath();
311+
$identifier = $this->request->data['identifier'] ?? $this->abort(400, 'Must provide identifier');
312+
$file = @MediaFile::all()[$identifier] ?? $this->abort(404, "Invalid media identifier '$identifier'");
341313

342-
if (! in_array($file->getExtension(), ['png', 'svg', 'jpg', 'jpeg', 'gif', 'ico', 'css', 'js'])) {
343-
$this->abort(403, sprintf("Refusing to open unsafe file '%s'", basename($path)));
344-
}
314+
$binary = $this->findGeneralOpenBinary();
315+
$path = $file->getAbsolutePath();
345316

346-
Process::run(sprintf('%s %s', $binary, escapeshellarg($path)))->throw();
317+
if (! in_array($file->getExtension(), ['png', 'svg', 'jpg', 'jpeg', 'gif', 'ico', 'css', 'js'])) {
318+
$this->abort(403, sprintf("Refusing to open unsafe file '%s'", basename($path)));
347319
}
320+
321+
Process::run(sprintf('%s %s', $binary, escapeshellarg($path)))->throw();
348322
}
349323

350324
protected function createPage(): void
351325
{
352-
if ($this->isInteractive()) {
353-
// Required data
354-
$title = $this->request->data['titleInput'] ?? $this->abort(400, 'Must provide title');
355-
$content = $this->request->data['contentInput'] ?? $this->abort(400, 'Must provide content');
356-
$pageType = $this->request->data['pageTypeSelection'] ?? $this->abort(400, 'Must provide page type');
357-
358-
// Optional data
359-
$postDescription = $this->request->data['postDescription'] ?? null;
360-
$postCategory = $this->request->data['postCategory'] ?? null;
361-
$postAuthor = $this->request->data['postAuthor'] ?? null;
362-
$postDate = $this->request->data['postDate'] ?? null;
363-
364-
// Match page class
365-
$pageClass = match ($pageType) {
366-
'blade-page' => BladePage::class,
367-
'markdown-page' => MarkdownPage::class,
368-
'markdown-post' => MarkdownPost::class,
369-
'documentation-page' => DocumentationPage::class,
370-
default => throw new HttpException(400, "Invalid page type '$pageType'"),
371-
};
372-
373-
if ($pageClass === MarkdownPost::class) {
374-
$creator = new CreatesNewMarkdownPostFile($title, $postDescription, $postCategory, $postAuthor, $postDate, $content);
375-
} else {
376-
$creator = new CreatesNewPageSourceFile($title, $pageClass, false, $content);
377-
}
378-
try {
379-
$path = $creator->save();
380-
} catch (FileConflictException $exception) {
381-
$this->abort($exception->getCode(), $exception->getMessage());
382-
}
326+
// Required data
327+
$title = $this->request->data['titleInput'] ?? $this->abort(400, 'Must provide title');
328+
$content = $this->request->data['contentInput'] ?? $this->abort(400, 'Must provide content');
329+
$pageType = $this->request->data['pageTypeSelection'] ?? $this->abort(400, 'Must provide page type');
330+
331+
// Optional data
332+
$postDescription = $this->request->data['postDescription'] ?? null;
333+
$postCategory = $this->request->data['postCategory'] ?? null;
334+
$postAuthor = $this->request->data['postAuthor'] ?? null;
335+
$postDate = $this->request->data['postDate'] ?? null;
336+
337+
// Match page class
338+
$pageClass = match ($pageType) {
339+
'blade-page' => BladePage::class,
340+
'markdown-page' => MarkdownPage::class,
341+
'markdown-post' => MarkdownPost::class,
342+
'documentation-page' => DocumentationPage::class,
343+
default => $this->abort(400, "Unsupported page type '$pageType'"),
344+
};
345+
346+
$creator = $pageClass === MarkdownPost::class
347+
? new CreatesNewMarkdownPostFile($title, $postDescription, $postCategory, $postAuthor, $postDate, $content)
348+
: new CreatesNewPageSourceFile($title, $pageClass, false, $content);
383349

384-
$this->flash('justCreatedPage', RouteKey::fromPage($pageClass, $pageClass::pathToIdentifier($path))->get());
385-
$this->sendJsonResponse(201, "Created file '$path'!");
350+
try {
351+
$path = $creator->save();
352+
} catch (FileConflictException $exception) {
353+
$this->abort($exception->getCode(), $exception->getMessage());
386354
}
355+
356+
$this->flash('justCreatedPage', RouteKey::fromPage($pageClass, $pageClass::pathToIdentifier($path))->get());
357+
$this->setJsonResponse(201, "Created file '$path'!");
387358
}
388359

389360
protected static function injectDashboardButton(string $contents): string
@@ -473,7 +444,7 @@ protected static function getPackageVersion(string $packageName): string
473444
return $prettyVersion ?? 'unreleased';
474445
}
475446

476-
protected function blockUnsafeRequests(): void
447+
protected function shouldUnsafeRequestBeBlocked(): bool
477448
{
478449
// As the dashboard is not password-protected, and it can make changes to the file system,
479450
// we block any requests that are not coming from the host machine. While we are clear
@@ -483,41 +454,21 @@ protected function blockUnsafeRequests(): void
483454
$requestIp = $_SERVER['REMOTE_ADDR'];
484455
$allowedIps = ['::1', '127.0.0.1', 'localhost'];
485456

486-
if (! in_array($requestIp, $allowedIps, true)) {
487-
$this->abort(403, "Refusing to serve request from address '$requestIp' (must be on localhost)");
488-
}
457+
return ! in_array($requestIp, $allowedIps, true);
489458
}
490459

491-
protected function sendJsonResponse(int $statusCode, string $body): never
460+
protected function setJsonResponse(int $statusCode, string $body): void
492461
{
493-
$statusMessage = match ($statusCode) {
494-
200 => 'OK',
495-
201 => 'Created',
496-
default => 'Internal Server Error',
497-
};
498-
499-
(new JsonResponse($statusCode, $statusMessage, [
462+
$this->response = new JsonResponse($statusCode, $this->matchStatusCode($statusCode), [
500463
'body' => $body,
501-
]))->send();
502-
503-
exit;
464+
]);
504465
}
505466

506-
protected function sendJsonErrorResponse(HttpException $exception): never
467+
protected function sendJsonErrorResponse(int $statusCode, string $message): JsonResponse
507468
{
508-
$statusMessage = match ($exception->getStatusCode()) {
509-
400 => 'Bad Request',
510-
403 => 'Forbidden',
511-
404 => 'Not Found',
512-
409 => 'Conflict',
513-
default => 'Internal Server Error',
514-
};
515-
516-
(new JsonResponse($exception->getStatusCode(), $statusMessage, [
517-
'error' => $exception->getMessage(),
518-
]))->send();
519-
520-
exit;
469+
return new JsonResponse($statusCode, $this->matchStatusCode($statusCode), [
470+
'error' => $message,
471+
]);
521472
}
522473

523474
protected function abort(int $code, string $message): never
@@ -532,9 +483,22 @@ protected function findGeneralOpenBinary(): string
532483
'Windows' => 'powershell Start-Process',
533484
'Darwin' => 'open',
534485
'Linux' => 'xdg-open',
535-
default => throw new HttpException(500,
536-
sprintf("Unable to find a matching binary for OS family '%s'", PHP_OS_FAMILY)
486+
default => $this->abort(500,
487+
sprintf("Unable to find a matching 'open' binary for OS family '%s'", PHP_OS_FAMILY)
537488
)
538489
};
539490
}
491+
492+
protected function matchStatusCode(int $statusCode): string
493+
{
494+
return match ($statusCode) {
495+
200 => 'OK',
496+
201 => 'Created',
497+
400 => 'Bad Request',
498+
403 => 'Forbidden',
499+
404 => 'Not Found',
500+
409 => 'Conflict',
501+
default => 'Internal Server Error',
502+
};
503+
}
540504
}

packages/realtime-compiler/src/Routing/PageRouter.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,7 @@ public function __construct(Request $request)
3333
protected function handlePageRequest(): Response
3434
{
3535
if ($this->request->path === '/dashboard' && DashboardController::enabled()) {
36-
return new HtmlResponse(200, 'OK', [
37-
'body' => (new DashboardController())->show(),
38-
]);
36+
return (new DashboardController())->handle();
3937
}
4038

4139
return new HtmlResponse(200, 'OK', [

0 commit comments

Comments
 (0)