Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
300 / 300
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
Routes
100.00% covered (success)
100.00%
300 / 300
100.00% covered (success)
100.00%
4 / 4
10
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 add
100.00% covered (success)
100.00%
31 / 31
100.00% covered (success)
100.00%
1 / 1
4
 catchallRoute
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 addPanel
100.00% covered (success)
100.00%
256 / 256
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray;
6
7use Celema\Core\App;
8use Celema\Core\Factory\Factory;
9use Celema\Quma\Database;
10use Celema\Router\Group;
11use Celema\Router\Route;
12use Closure;
13use Cosray\Controller\Media;
14use Cosray\Controller\Node;
15use Cosray\Controller\Panel;
16use Cosray\Middleware\InitRequest;
17use Cosray\Middleware\PanelAuth;
18use Cosray\Middleware\PanelLocale;
19use Cosray\Middleware\Session;
20
21class Routes
22{
23    protected string $panelPath;
24    protected InitRequest $initRequestMiddlware;
25    protected Session $session;
26    protected bool $frontendSession;
27
28    /**
29     * @param list<Closure(App): void> $pluginRoutes
30     * @param list<array{pattern: string, endpoint: mixed, template: string, name: string}> $panelPages
31     */
32    public function __construct(
33        protected Config $config,
34        protected Database $db,
35        protected Factory $factory,
36        protected array $pluginRoutes = [],
37        protected array $panelPages = [],
38    ) {
39        $this->panelPath = $config->panel->path;
40        $this->frontendSession = $config->session->enabled;
41        $this->initRequestMiddlware = new InitRequest($config);
42        $this->session = new Session($this->config, $this->db);
43    }
44
45    public function add(App $app): void
46    {
47        $sessionIfEnabled = [
48            $app->get('/', [Node::class, 'catchall'], 'cms.index.get'),
49            $app->post('/', [Node::class, 'catchall'], 'cms.index.post'),
50            $app->put('/', [Node::class, 'catchall'], 'cms.index.put'),
51            $app->delete('/', [Node::class, 'catchall'], 'cms.index.delete'),
52            $app->get('/preview/...slug', [Node::class, 'preview'], 'cms.preview.catchall'),
53        ];
54
55        // Rendition fallback: the web server serves existing cache files
56        // natively; only misses reach PHP and get generated once.
57        $app->get(
58            '/' . trim($this->config->path->cache, '/') . '/...slug',
59            [Media::class, 'cache'],
60            'cms.media.cache',
61        );
62
63        $app->post(
64            '/media/{mediatype:(image|file|video)}',
65            [Media::class, 'upload'],
66            'cms.media.upload',
67        )->middleware($this->session);
68
69        $app->get('/media/library', [Media::class, 'library'], 'cms.media.library')
70            ->middleware($this->session);
71
72        $app->get('/media/{uid}', [Media::class, 'detail'], 'cms.media.detail')
73            ->middleware($this->session);
74
75        $app->put('/media/{uid}', [Media::class, 'updateMeta'], 'cms.media.meta')
76            ->middleware($this->session);
77
78        $app->delete('/media/{uid}', [Media::class, 'delete'], 'cms.media.delete')
79            ->middleware($this->session);
80
81        $this->addPanel($app);
82
83        foreach ($this->pluginRoutes as $addRoutes) {
84            $addRoutes($app);
85        }
86
87        if ($this->frontendSession) {
88            foreach ($sessionIfEnabled as $route) {
89                $route->middleware($this->session);
90            }
91        }
92    }
93
94    public function catchallRoute(): Route
95    {
96        $catchallRoute = Route::map(
97            ['GET', 'POST', 'PUT', 'DELETE'],
98            '/...slug',
99            [Node::class, 'catchall'],
100            'cms.catchall',
101        )->middleware($this->initRequestMiddlware);
102
103        if ($this->frontendSession) {
104            $catchallRoute->middleware($this->session);
105        }
106
107        return $catchallRoute;
108    }
109
110    protected function addPanel(App $app): void
111    {
112        $app->group(
113            $this->panelPath,
114            function (Group $panel) use ($app) {
115                $renderers = new PanelRenderers($app);
116                $panelAuth = new PanelAuth(
117                    $this->config,
118                    new Users($this->db),
119                    $this->factory,
120                );
121                // PanelLocale runs after Session so the user attribute (and
122                // with it the stored panel language preference) is available.
123                $panel->middleware($this->session, new PanelLocale($this->config));
124
125                $panel
126                    ->get('/login', [Panel\Login::class, 'login'], 'login')
127                    ->after($renderers->get('login'));
128                $panel
129                    ->post('/login', [Panel\Login::class, 'authenticate'], 'login.authenticate')
130                    ->after($renderers->get('login'));
131                $panel
132                    ->post('/logout', [Panel\Login::class, 'logout'], 'logout')
133                    ->middleware($panelAuth);
134                $panel
135                    ->post('/locale', [Panel\Preferences::class, 'locale'], 'locale')
136                    ->middleware($panelAuth);
137                $panel
138                    ->get(
139                        '',
140                        [Panel\Index::class, 'index'],
141                        'index',
142                    )
143                    ->middleware($panelAuth)
144                    ->after($renderers->get('index'));
145
146                if ($this->config->debug()) {
147                    $panel
148                        ->get(
149                            '/styleguide',
150                            [Panel\Styleguide::class, 'index'],
151                            'styleguide',
152                        )
153                        ->middleware($panelAuth)
154                        ->after($renderers->get('styleguide'));
155                }
156
157                $panel
158                    ->get(
159                        '/media',
160                        [Panel\Media::class, 'index'],
161                        'media',
162                    )
163                    ->middleware($panelAuth)
164                    ->after($renderers->get('media'));
165                $panel
166                    ->get(
167                        '/menus',
168                        [Panel\Menus::class, 'index'],
169                        'menus',
170                    )
171                    ->middleware($panelAuth)
172                    ->after($renderers->get('menus'));
173                // Before the {menu} routes: the literal segment wins over a
174                // menu handle of the same name, and the handle validation
175                // reserves it.
176                $panel
177                    ->get(
178                        '/menus/create',
179                        [Panel\Menus::class, 'create'],
180                        'menus.create',
181                    )
182                    ->middleware($panelAuth)
183                    ->after($renderers->get('menu-form'));
184                $panel
185                    ->post(
186                        '/menus/create',
187                        [Panel\Menus::class, 'store'],
188                        'menus.store',
189                    )
190                    ->middleware($panelAuth)
191                    ->after($renderers->get('menu-form'));
192                // The menu's own fields live on its tree screen, so this only
193                // takes the write and answers with that screen.
194                $panel
195                    ->post(
196                        '/menus/{menu:[a-z0-9-]{1,32}}/edit',
197                        [Panel\Menus::class, 'update'],
198                        'menu.update',
199                    )
200                    ->middleware($panelAuth)
201                    ->after($renderers->get('menu'));
202                $panel
203                    ->post(
204                        '/menus/{menu:[a-z0-9-]{1,32}}/delete',
205                        [Panel\Menus::class, 'delete'],
206                        'menu.delete',
207                    )
208                    ->middleware($panelAuth);
209                $panel
210                    ->get(
211                        '/menus/{menu:[a-z0-9-]{1,32}}',
212                        [Panel\Menus::class, 'menu'],
213                        'menu',
214                    )
215                    ->middleware($panelAuth)
216                    ->after($renderers->get('menu'));
217                // Before the {item} route: the literal segment wins, and item
218                // ids are generated uids that never collide with it.
219                $panel
220                    ->post(
221                        '/menus/{menu:[a-z0-9-]{1,32}}/item/create',
222                        [Panel\Menus::class, 'storeItem'],
223                        'menu.item.store',
224                    )
225                    ->middleware($panelAuth)
226                    ->after($renderers->get('menu'));
227                $panel
228                    ->post(
229                        '/menus/{menu:[a-z0-9-]{1,32}}/item/{item:[A-Za-z0-9_-]{1,64}}/move',
230                        [Panel\Menus::class, 'moveItem'],
231                        'menu.item.move',
232                    )
233                    ->middleware($panelAuth);
234                $panel
235                    ->post(
236                        '/menus/{menu:[a-z0-9-]{1,32}}/item/{item:[A-Za-z0-9_-]{1,64}}/delete',
237                        [Panel\Menus::class, 'deleteItem'],
238                        'menu.item.delete',
239                    )
240                    ->middleware($panelAuth);
241                $panel
242                    ->post(
243                        '/menus/{menu:[a-z0-9-]{1,32}}/item/{item:[A-Za-z0-9_-]{1,64}}',
244                        [Panel\Menus::class, 'updateItem'],
245                        'menu.item.update',
246                    )
247                    ->middleware($panelAuth)
248                    ->after($renderers->get('menu'));
249                $panel
250                    ->get(
251                        '/reference/search',
252                        [Panel\Reference::class, 'search'],
253                        'reference.search',
254                    )
255                    ->middleware($panelAuth);
256                $panel
257                    ->get(
258                        '/reference/nodes',
259                        [Panel\Reference::class, 'nodes'],
260                        'reference.nodes',
261                    )
262                    ->middleware($panelAuth);
263                $panel
264                    ->get(
265                        '/reference/labels',
266                        [Panel\Reference::class, 'labels'],
267                        'reference.labels',
268                    )
269                    ->middleware($panelAuth);
270                $panel
271                    ->get(
272                        '/assets/...slug',
273                        [Panel\Assets::class, 'asset'],
274                        'asset',
275                    );
276                $panel
277                    ->get(
278                        '/static/...slug',
279                        [Panel\Assets::class, 'staticAsset'],
280                        'static.asset',
281                    );
282                $panel
283                    ->get(
284                        '/vendor/{plugin:[a-z0-9-]{1,64}}/...slug',
285                        [Panel\Assets::class, 'vendor'],
286                        'vendor.asset',
287                    );
288                $panel
289                    ->get(
290                        '/collection/{collection}',
291                        [Panel\Collection::class, 'collection'],
292                        'collection',
293                    )
294                    ->middleware($panelAuth)
295                    ->after($renderers->get('collection'));
296                $panel
297                    ->get(
298                        '/collection/{collection}/create/{type:[A-Za-z0-9-_.]{1,64}}',
299                        [Panel\Editor::class, 'create'],
300                        'editor.create',
301                    )
302                    ->middleware($panelAuth)
303                    ->after($renderers->get('editor'));
304                $panel
305                    ->post(
306                        '/collection/{collection}/create/{type:[A-Za-z0-9-_.]{1,64}}',
307                        [Panel\Editor::class, 'store'],
308                        'editor.store',
309                    )
310                    ->middleware($panelAuth)
311                    ->after($renderers->get('editor-save'));
312                $panel
313                    ->post(
314                        '/collection/{collection}/create/{type:[A-Za-z0-9-_.]{1,64}}/paths',
315                        [Panel\Editor::class, 'createPaths'],
316                        'editor.create.paths',
317                    )
318                    ->middleware($panelAuth)
319                    ->after($renderers->get('editor-paths'));
320                // Before the {node} routes: like `create`, the `bulk` segment
321                // wins over a node uid of the same name.
322                $panel
323                    ->post(
324                        '/collection/{collection}/bulk/publish',
325                        [Panel\Bulk::class, 'publish'],
326                        'collection.bulk.publish',
327                    )
328                    ->middleware($panelAuth);
329                $panel
330                    ->post(
331                        '/collection/{collection}/bulk/delete',
332                        [Panel\Bulk::class, 'delete'],
333                        'collection.bulk.delete',
334                    )
335                    ->middleware($panelAuth);
336                $panel
337                    ->post(
338                        '/collection/{collection}/bulk/duplicate',
339                        [Panel\Bulk::class, 'duplicate'],
340                        'collection.bulk.duplicate',
341                    )
342                    ->middleware($panelAuth);
343                $panel
344                    ->post(
345                        '/collection/{collection}/{node:[A-Za-z0-9-_.]{1,64}}/delete',
346                        [Panel\Editor::class, 'delete'],
347                        'editor.delete',
348                    )
349                    ->middleware($panelAuth)
350                    ->after($renderers->get('editor-save'));
351                $panel
352                    ->post(
353                        '/collection/{collection}/{node:[A-Za-z0-9-_.]{1,64}}/paths',
354                        [Panel\Editor::class, 'paths'],
355                        'editor.paths',
356                    )
357                    ->middleware($panelAuth)
358                    ->after($renderers->get('editor-paths'));
359                $panel
360                    ->get(
361                        '/collection/{collection}/{node:[A-Za-z0-9-_.]{1,64}}',
362                        [Panel\Editor::class, 'edit'],
363                        'editor',
364                    )
365                    ->middleware($panelAuth)
366                    ->after($renderers->get('editor'));
367                $panel
368                    ->post(
369                        '/collection/{collection}/{node:[A-Za-z0-9-_.]{1,64}}',
370                        [Panel\Editor::class, 'save'],
371                        'editor.save',
372                    )
373                    ->middleware($panelAuth)
374                    ->after($renderers->get('editor-save'));
375
376                foreach ($this->panelPages as $page) {
377                    $panel
378                        ->get($page['pattern'], $page['endpoint'], $page['name'])
379                        ->middleware($panelAuth)
380                        ->after($renderers->get($page['template']));
381                }
382            },
383            'cms.panel.',
384        );
385    }
386}