2
2
3
3
namespace Coderstm \Http \Controllers ;
4
4
5
- use Coderstm \Models \AppSetting ;
6
5
use Illuminate \Support \Str ;
7
6
use Coderstm \Services \Theme ;
8
7
use Illuminate \Http \Request ;
9
- use Spatie \Browsershot \Browsershot ;
8
+ use Coderstm \Jobs \BuildTheme ;
9
+ use Coderstm \Services \Helpers ;
10
+ use Coderstm \Models \AppSetting ;
10
11
use Illuminate \Support \Facades \File ;
12
+ use Coderstm \Services \Theme \FileMeta ;
11
13
use Illuminate \Support \Facades \Blade ;
12
- use Illuminate \Support \Facades \Cache ;
13
- use Illuminate \Support \Facades \Response ;
14
14
15
15
class ThemeController extends Controller
16
16
{
17
+ protected $ basePath ;
18
+
19
+ public function __construct ()
20
+ {
21
+ try {
22
+ Helpers::checkNpmInstallation ();
23
+ $ this ->basePath = null ;
24
+ } catch (\Exception $ e ) {
25
+ $ this ->basePath = '/views ' ;
26
+ }
27
+ }
28
+
17
29
// List all themes
18
30
public function index ()
19
31
{
@@ -73,6 +85,9 @@ public function destroy($theme)
73
85
// Clone a theme
74
86
public function clone ($ theme )
75
87
{
88
+ // Check if npm is installed and the test command can be run
89
+ Helpers::checkNpmInstallation ();
90
+
76
91
$ config = Theme::config ($ theme );
77
92
$ newThemeName = $ config ['name ' ] . ' (Copy) ' ;
78
93
$ newThemeKey = Str::slug ($ newThemeName );
@@ -91,7 +106,10 @@ public function clone($theme)
91
106
92
107
File::put (Theme::basePath ('config.json ' , $ newThemeKey ), json_encode ($ config , JSON_PRETTY_PRINT ));
93
108
94
- return response ()->json (['message ' => 'Theme cloned successfully ' ], 200 );
109
+ // Dispatch the theme build job to the queue
110
+ BuildTheme::dispatch ($ newThemeKey );
111
+
112
+ return response ()->json (['message ' => 'Theme cloned successfully, theme build queued. ' ], 200 );
95
113
}
96
114
97
115
return response ()->json (['message ' => 'Theme not found or new theme already exists ' ], 404 );
@@ -100,13 +118,13 @@ public function clone($theme)
100
118
// Get the list of files and directories in a theme with `basepath` for the editor
101
119
public function getFiles ($ theme )
102
120
{
103
- $ themePath = Theme::basePath (' views ' , $ theme );
121
+ $ themePath = Theme::basePath ($ this -> basePath , $ theme );
104
122
105
123
if (!File::exists ($ themePath )) {
106
124
return response ()->json (['message ' => 'Theme not found ' ], 404 );
107
125
}
108
126
109
- $ fileTree = $ this ->getDirectoryStructure ($ themePath, $ themePath );
127
+ $ fileTree = $ this ->getDirectoryStructure ($ themePath );
110
128
$ themeInfo = $ this ->info ($ theme );
111
129
112
130
return response ()->json ([
@@ -116,9 +134,10 @@ public function getFiles($theme)
116
134
}
117
135
118
136
// Recursive function to build file and folder structure
119
- private function getDirectoryStructure ($ directory , $ basepath )
137
+ private function getDirectoryStructure ($ directory , $ basepath = null )
120
138
{
121
139
$ items = [];
140
+ $ basepath = $ basepath ?? $ directory ;
122
141
123
142
// Get all directories and files in the current directory
124
143
$ directories = File::directories ($ directory );
@@ -127,16 +146,14 @@ private function getDirectoryStructure($directory, $basepath)
127
146
// Add directories to the structure
128
147
foreach ($ directories as $ dir ) {
129
148
$ dirName = basename ($ dir );
149
+ $ relativePath = str_replace ($ basepath . '/ ' , '' , $ dir ); // Get relative path
130
150
131
- // Exclude the public directory
132
151
if (!in_array ($ dirName , ['public ' ])) {
133
- // Recursively get directory structure while skipping 'public' and its children
134
- $ singular = Str::singular ($ dirName );
152
+ $ singular = Helpers::singularizeDirectoryName ($ dirName );
135
153
$ items [] = [
136
154
'name ' => $ dirName ,
137
- 'ext ' => '.blade.php ' ,
138
155
'addLabel ' => "Add a new $ singular " ,
139
- 'basepath ' => str_replace ( $ basepath . ' / ' , '' , $ dir ) ,
156
+ 'basepath ' => $ relativePath ,
140
157
'header ' => 'directory ' ,
141
158
'modified_at ' => date ('Y-m-d H:i:s ' , filemtime ($ dir )),
142
159
'children ' => $ this ->getDirectoryStructure ($ dir , $ basepath )
@@ -146,20 +163,7 @@ private function getDirectoryStructure($directory, $basepath)
146
163
147
164
// Add files to the structure
148
165
foreach ($ files as $ file ) {
149
- $ fileName = basename ($ file ->getPathname ());
150
-
151
- // Exclude the 'preview.png' file
152
- if ($ fileName === 'preview.png ' ) {
153
- continue ; // Skip 'preview.png'
154
- }
155
-
156
- $ items [] = [
157
- 'name ' => $ fileName ,
158
- 'basepath ' => str_replace ($ basepath . '/ ' , '' , $ file ->getPathname ()),
159
- 'icon ' => 'fas fa-code ' ,
160
- 'modified_at ' => date ('Y-m-d H:i:s ' , filemtime ($ file )),
161
- 'header ' => 'file '
162
- ];
166
+ $ items [] = (new FileMeta ($ file , $ basepath ))->toArray ();
163
167
}
164
168
165
169
return $ items ;
@@ -168,7 +172,7 @@ private function getDirectoryStructure($directory, $basepath)
168
172
public function getFileContent ($ theme , Request $ request )
169
173
{
170
174
$ filePath = $ request ->input ('key ' ); // The relative path of the selected file
171
- $ fullPath = realpath (Theme::basePath ("views /$ filePath " , $ theme ));
175
+ $ fullPath = realpath (Theme::basePath ("{ $ this -> basePath } / $ filePath " , $ theme ));
172
176
173
177
if (!$ fullPath || !File::exists ($ fullPath )) {
174
178
return response ()->json (['message ' => 'File not found or invalid path ' ], 404 );
@@ -188,7 +192,7 @@ public function saveFile(Request $request, $theme)
188
192
{
189
193
$ filePath = $ request ->input ('key ' );
190
194
$ content = $ request ->input ('content ' );
191
- $ themePath = Theme::basePath ("views /$ filePath " , $ theme );
195
+ $ themePath = Theme::basePath ("{ $ this -> basePath } / $ filePath " , $ theme );
192
196
193
197
// Validate Blade syntax (if it's a .blade.php file)
194
198
if (File::extension ($ filePath ) === 'php ' ) {
@@ -202,6 +206,10 @@ public function saveFile(Request $request, $theme)
202
206
// Save file content
203
207
File::put ($ themePath , $ content );
204
208
209
+ if (Str::startsWith ($ filePath , 'assets ' )) {
210
+ BuildTheme::dispatch ($ theme );
211
+ }
212
+
205
213
return response ()->json (['message ' => 'File saved successfully ' ], 200 );
206
214
}
207
215
@@ -216,14 +224,14 @@ public function createFile(Request $request, $theme)
216
224
217
225
$ fileName = $ request ->input ('name ' ) . $ request ->ext ;
218
226
$ basepath = rtrim ($ request ->input ('basepath ' ), '/ ' );
219
- $ themePath = Theme::basePath ("views /$ basepath " , $ theme );
227
+ $ themePath = Theme::basePath ("{ $ this -> basePath } / $ basepath " , $ theme );
220
228
221
229
222
230
if (!File::exists ($ themePath )) {
223
231
return response ()->json (['message ' => 'Theme not found ' ], 404 );
224
232
}
225
233
226
- $ filePath = Theme::basePath ("views /$ basepath/ $ fileName " , $ theme );
234
+ $ filePath = Theme::basePath ("{ $ this -> basePath } / $ basepath/ $ fileName " , $ theme );
227
235
228
236
// Check if the file already exists
229
237
if (File::exists ($ filePath )) {
@@ -251,7 +259,7 @@ public function createFile(Request $request, $theme)
251
259
'message ' => 'File created successfully ' ,
252
260
'file ' => [
253
261
'name ' => $ fileName ,
254
- 'basepath ' => str_replace (Theme::basePath (' views ' , $ theme ) . '/ ' , '' , $ filePath ),
262
+ 'basepath ' => str_replace (Theme::basePath ($ this -> basePath , $ theme ) . '/ ' , '' , $ filePath ),
255
263
'icon ' => 'fas fa-code ' ,
256
264
'header ' => 'file '
257
265
]
@@ -265,15 +273,15 @@ public function destroyThemeFile(Request $request, $theme)
265
273
]);
266
274
267
275
$ filePath = $ request ->input ('key ' ); // The relative path of the selected file
268
- $ fullPath = realpath (Theme::basePath (' views / ' . $ filePath , $ theme ));
276
+ $ fullPath = realpath (Theme::basePath ($ this -> basePath . ' / ' . $ filePath , $ theme ));
269
277
270
278
// Check if the file exists
271
279
if (!$ fullPath || !File::exists ($ fullPath )) {
272
280
return response ()->json (['message ' => 'File not found or invalid path ' ], 404 );
273
281
}
274
282
275
283
// Prevent deletion of specific directories or files (like 'public' or 'preview.png')
276
- if (str_contains ($ filePath , 'public ' ) || str_contains ($ filePath , 'preview.png ' )) {
284
+ if (str_contains ($ filePath , 'public ' ) || str_contains ($ filePath , 'preview.png ' ) || str_contains ( $ filePath , ' config.json ' ) ) {
277
285
return response ()->json (['message ' => 'This file or directory cannot be deleted ' ], 403 );
278
286
}
279
287
@@ -286,6 +294,65 @@ public function destroyThemeFile(Request $request, $theme)
286
294
}
287
295
}
288
296
297
+ public function assetsUpload (Request $ request , $ theme )
298
+ {
299
+ $ request ->validate ([
300
+ 'media ' => [
301
+ 'required ' ,
302
+ 'mimetypes:image/jpeg,image/png,image/gif ' ,
303
+ 'max:300 ' , // 300 KB size limit
304
+ ],
305
+ ], [
306
+ 'media.required ' => 'Please select an image to upload. ' ,
307
+ 'media.mimetypes ' => 'The file must be an image (JPEG, PNG, GIF). ' ,
308
+ 'media.max ' => 'The file must not be larger than 300 KB. ' ,
309
+ ]);
310
+
311
+ // Define the directory path for the theme assets
312
+ $ fileName = $ request ->file ('media ' )->getClientOriginalName ();
313
+ $ filePath = Theme::basePath ("{$ this ->basePath }assets/img/ $ fileName " , $ theme );
314
+
315
+ // Check if the file already exists
316
+ if (File::exists ($ filePath )) {
317
+ return response ()->json (['message ' => 'File already exists ' ], 422 );
318
+ }
319
+
320
+ // Move the uploaded file to the designated path
321
+ $ request ->file ('media ' )->move (dirname ($ filePath ), $ fileName );
322
+
323
+ return response ()->json ([
324
+ 'name ' => $ fileName ,
325
+ 'basepath ' => str_replace (Theme::basePath ($ this ->basePath , $ theme ) . '/ ' , '' , $ filePath ),
326
+ 'icon ' => 'fas fa-image ' ,
327
+ 'header ' => 'file '
328
+ ], 201 );
329
+ }
330
+
331
+ public function assets (Request $ request , $ theme )
332
+ {
333
+ // Sanitize the path to prevent directory traversal
334
+ $ path = str_replace (['.. ' , './ ' , '\\' ], '' , $ request ->input ('path ' )); // Remove directory traversal sequences
335
+
336
+ if (Str::endsWith ($ path , '.php ' )) {
337
+ abort (404 );
338
+ }
339
+
340
+ // Generate the full file path for the theme
341
+ $ filePath = Theme::basePath ($ path , $ theme );
342
+
343
+ // Use realpath to get the absolute path and ensure it's within the allowed directories
344
+ $ realFilePath = realpath ($ filePath );
345
+
346
+ // Check if the real path is valid and within the intended theme directories
347
+ if ($ realFilePath && File::exists ($ realFilePath )) {
348
+ // Return the file with headers
349
+ return response ()->file ($ realFilePath );
350
+ }
351
+
352
+ // Abort with a 404 if the file is not found or invalid
353
+ abort (404 );
354
+ }
355
+
289
356
// Show selected theme details
290
357
protected function info ($ theme )
291
358
{
@@ -304,33 +371,4 @@ protected function info($theme)
304
371
305
372
return null ;
306
373
}
307
-
308
- public function preview ($ theme )
309
- {
310
- // Check if the preview image is cached
311
- $ cacheKey = 'theme_preview_ ' . $ theme ;
312
- if (Cache::has ($ cacheKey )) {
313
- $ cachedImagePath = Cache::get ($ cacheKey );
314
- return Response::file ($ cachedImagePath );
315
- }
316
-
317
- // Generate the preview image
318
- $ imagePath = Theme::basePath ('preview.png ' , $ theme );
319
-
320
- try {
321
- // Capture homepage as a PNG using Browsershot
322
- Browsershot::url (route ('home ' , ['theme ' => $ theme ]))
323
- ->setScreenshotType ('jpeg ' , 50 )
324
- ->windowSize (1440 , 792 )
325
- ->save ($ imagePath );
326
-
327
- // Cache the image path for 12 hours
328
- Cache::put ($ cacheKey , $ imagePath , 180 * 60 * 4 ); // Cache for 12 hours
329
- } catch (\Exception $ e ) {
330
- return response ('Error generating preview ' , 404 );
331
- }
332
-
333
- // Return the generated image
334
- return Response::file ($ imagePath );
335
- }
336
374
}
0 commit comments