Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
79.71% covered (warning)
79.71%
55 / 69
50.00% covered (danger)
50.00%
3 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
Node
79.71% covered (warning)
79.71%
55 / 69
50.00% covered (danger)
50.00%
3 / 6
23.34
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 catchall
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
5.01
 preview
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 dispatch
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
5
 jsonRead
95.83% covered (success)
95.83%
23 / 24
0.00% covered (danger)
0.00%
0 / 1
2
 redirectIfExists
20.00% covered (danger)
20.00%
3 / 15
0.00% covered (danger)
0.00%
0 / 1
17.80
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\Controller;
6
7use Celema\Core\Exception\HttpBadRequest;
8use Celema\Core\Exception\HttpNotFound;
9use Celema\Core\Factory\Factory;
10use Celema\Core\Response;
11use Cosray\Cms;
12use Cosray\Context;
13use Cosray\Contract\HttpDelete;
14use Cosray\Contract\HttpGet;
15use Cosray\Contract\HttpPost;
16use Cosray\Contract\HttpPut;
17use Cosray\Exception\RuntimeException;
18use Cosray\Middleware\Permission;
19use Cosray\Node\Factory as NodeFactory;
20use Cosray\Node\Serializer;
21use Cosray\Node\Types;
22use Cosray\Node\View;
23use Cosray\Node\Wrapper;
24use Cosray\Util\Path;
25
26class Node
27{
28    /**
29     * Request method to the node interface that answers it, and the method
30     * to call. A node implementing one takes over that request completely.
31     *
32     * @var array<string, array{class-string, string}>
33     */
34    private const array HANDLERS = [
35        'GET' => [HttpGet::class, 'httpGet'],
36        'POST' => [HttpPost::class, 'httpPost'],
37        'PUT' => [HttpPut::class, 'httpPut'],
38        'DELETE' => [HttpDelete::class, 'httpDelete'],
39    ];
40
41    public function __construct(
42        protected readonly Factory $factory,
43        protected readonly Types $types,
44    ) {}
45
46    public function catchall(Context $context, Cms $cms): Response
47    {
48        $request = $context->httpRequest();
49        $config = $context->config;
50        $path = $request->uri()->getPath();
51        $prefix = $config->path->prefix;
52
53        if ($prefix) {
54            $path = preg_replace('/^' . preg_quote($prefix, '/') . '/', '', $path);
55        }
56
57        $node = $cms->node->byPath($path === '' ? '/' : $path);
58
59        if (!$node) {
60            try {
61                $path = Path::inside($config->path->public, $path, checkIsFile: true);
62
63                return Response::create($this->factory)->file($path);
64            } catch (RuntimeException $e) {
65                $this->redirectIfExists($context, $path);
66
67                throw new HttpNotFound($request, previous: $e);
68            }
69        }
70
71        return $this->dispatch($node, $context, $cms);
72    }
73
74    #[Permission('panel')]
75    public function preview(Context $context, Cms $cms, string $slug): Response
76    {
77        $node = $cms->node->byPath('/' . $slug);
78
79        if (!$node) {
80            throw new HttpNotFound($context->httpRequest());
81        }
82
83        // Preview goes through the same dispatch as the public path, so a
84        // node that answers GET itself is previewed the way it is served.
85        return $this->dispatch($node, $context, $cms);
86    }
87
88    private function dispatch(object $node, Context $context, Cms $cms): Response
89    {
90        $request = $context->httpRequest();
91        $method = $request->method();
92        $handler = self::HANDLERS[$method] ?? null;
93        $inner = Wrapper::unwrap($node);
94
95        if ($handler !== null && $inner instanceof $handler[0]) {
96            return $inner->{$handler[1]}();
97        }
98
99        if ($method !== 'GET') {
100            throw new HttpBadRequest();
101        }
102
103        if ($request->get('isXhr', false)) {
104            return $this->jsonRead($node, $context, $cms);
105        }
106
107        return new View($inner, $cms, $context, $this->types)->render();
108    }
109
110    private function jsonRead(object $node, Context $context, Cms $cms): Response
111    {
112        $inner = Wrapper::unwrap($node);
113
114        if (method_exists($inner, 'read')) {
115            $data = $inner->read();
116        } else {
117            $nodeFactory = $cms->nodeFactory();
118            $serializer = new Serializer(
119                $this->types,
120                $nodeFactory->uid(),
121                $context->assets(),
122                $context->paths(),
123            );
124            $data = $serializer->read(
125                $inner,
126                NodeFactory::dataFor($inner),
127                NodeFactory::fieldNamesFor($inner),
128            );
129        }
130
131        $content = json_encode(
132            $data,
133            JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR,
134        );
135
136        return new Response(
137            $this->factory
138                ->response()
139                ->withHeader('Content-Type', 'application/json'),
140        )->body($content);
141    }
142
143    protected function redirectIfExists(Context $context, string $path): void
144    {
145        $db = $context->db;
146        $path = $db->paths->byPath(['path' => $path])->first();
147
148        if ($path && !($path['inactive'] === null)) {
149            $paths = $db->paths->activeByNode(['node' => $path['node']])->all();
150
151            $pathsByLocale = array_combine(
152                array_map(static fn($p) => $p['locale'], $paths),
153                array_map(static fn($p) => $p['path'], $paths),
154            );
155
156            $locale = $context->locale();
157
158            while ($locale) {
159                $path = $pathsByLocale[$locale->id] ?? null;
160
161                if ($path) {
162                    header('Location: ' . $path, true, 301);
163                    exit();
164                }
165
166                $locale = $locale->fallback();
167            }
168        }
169    }
170}