1111use Illuminate \Support \Arr ;
1212use Hyde \Pages \MarkdownPage ;
1313use Hyde \Pages \MarkdownPost ;
14+ use Desilva \Microserve \Response ;
1415use Hyde \Pages \Concerns \HydePage ;
1516use Hyde \Pages \DocumentationPage ;
1617use Hyde \Support \Models \RouteKey ;
2829use Hyde \Framework \Actions \CreatesNewMarkdownPostFile ;
2930use 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}
0 commit comments