Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
40.00% covered (danger)
40.00%
32 / 80
12.50% covered (danger)
12.50%
1 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Page
40.00% covered (danger)
40.00%
32 / 80
12.50% covered (danger)
12.50%
1 / 8
160.00
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
72.22% covered (warning)
72.22%
13 / 18
0.00% covered (danger)
0.00%
0 / 1
8.05
 preview
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 dispatch
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
 renderPage
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
2.00
 jsonRead
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
6
 handleFormPost
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 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 Celemas\Container\Container;
8use Celemas\Core\Exception\HttpBadRequest;
9use Celemas\Core\Exception\HttpNotFound;
10use Celemas\Core\Factory\Factory;
11use Celemas\Core\Response;
12use Cosray\Cms;
13use Cosray\Context;
14use Cosray\Exception\RuntimeException;
15use Cosray\Middleware\Permission;
16use Cosray\Node\Contract\HandlesFormPost;
17use Cosray\Node\Factory as NodeFactory;
18use Cosray\Node\Node;
19use Cosray\Node\Serializer;
20use Cosray\Node\Types;
21use Cosray\Node\ViewRenderer;
22use Cosray\Util\Path;
23use ReflectionMethod;
24
25class Page
26{
27    public function __construct(
28        protected readonly Factory $factory,
29        protected readonly Container $container,
30        protected readonly Types $types,
31    ) {}
32
33    public function catchall(Context $context, Cms $cms): Response
34    {
35        $request = $context->request;
36        $config = $context->config;
37        $path = $request->uri()->getPath();
38        $prefix = $config->path->prefix;
39
40        if ($prefix) {
41            $path = preg_replace('/^' . preg_quote($prefix, '/') . '/', '', $path);
42        }
43
44        $page = $cms->node->byPath($path === '' ? '/' : $path);
45
46        if (!$page) {
47            try {
48                $path = Path::inside($config->path->public, $path);
49
50                return Response::create($this->factory)->file($path);
51            } catch (RuntimeException $e) {
52                $this->redirectIfExists($context, $path);
53
54                throw new HttpNotFound($request, previous: $e);
55            }
56        }
57
58        if ($request->get('isXhr', false)) {
59            if ($request->method() === 'GET') {
60                return $this->jsonRead($page, $cms);
61            }
62
63            throw new HttpBadRequest();
64        }
65
66        return $this->dispatch($page, $context, $cms, $request->method(), $request->form());
67    }
68
69    #[Permission('panel')]
70    public function preview(Context $context, Cms $cms, string $slug): Response
71    {
72        $page = $cms->node->byPath('/' . $slug);
73
74        return $this->renderPage($page, $context, $cms);
75    }
76
77    private function dispatch(
78        object $page,
79        Context $context,
80        Cms $cms,
81        string $method,
82        ?array $formBody,
83    ): Response {
84        return match ($method) {
85            'GET' => $this->renderPage($page, $context, $cms),
86            'POST' => $this->handleFormPost($page, $formBody),
87            default => throw new HttpBadRequest(),
88        };
89    }
90
91    private function renderPage(object $page, Context $context, Cms $cms): Response
92    {
93        $node = Node::unwrap($page);
94
95        if (is_callable([$node, 'render'])) {
96            return $node->render();
97        }
98
99        $hydrator = $cms->nodeFactory()->hydrator();
100        $renderer = new ViewRenderer($this->container, $this->factory, $hydrator, $this->types);
101
102        return $renderer->renderPage(
103            $node,
104            NodeFactory::fieldNamesFor($node),
105            $cms,
106            $context->request,
107            $context->config,
108        );
109    }
110
111    private function jsonRead(object $node, Cms $cms): Response
112    {
113        $inner = Node::unwrap($node);
114
115        if (method_exists($inner, 'read')) {
116            $data = $inner->read();
117        } else {
118            $nodeFactory = $cms->nodeFactory();
119            $hydrator = $nodeFactory->hydrator();
120            $serializer = new Serializer($hydrator, $this->types, $nodeFactory->uid());
121            $data = $serializer->read(
122                $inner,
123                NodeFactory::dataFor($inner),
124                NodeFactory::fieldNamesFor($inner),
125            );
126        }
127
128        $content = json_encode(
129            $data,
130            JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR,
131        );
132
133        return new Response(
134            $this->factory
135                ->response()
136                ->withHeader('Content-Type', 'application/json'),
137        )->body($content);
138    }
139
140    private function handleFormPost(object $node, ?array $formBody): Response
141    {
142        $inner = Node::unwrap($node);
143
144        if ($inner instanceof HandlesFormPost) {
145            return $inner->formPost($formBody);
146        }
147
148        if (method_exists($inner, 'formPost')) {
149            $method = new ReflectionMethod($inner, 'formPost');
150
151            return $method->invoke($inner, $formBody);
152        }
153
154        throw new HttpBadRequest();
155    }
156
157    protected function redirectIfExists(Context $context, string $path): void
158    {
159        $db = $context->db;
160        $path = $db->paths->byPath(['path' => $path])->first();
161
162        if ($path && !($path['inactive'] === null)) {
163            $paths = $db->paths->activeByNode(['node' => $path['node']])->all();
164
165            $pathsByLocale = array_combine(
166                array_map(static fn($p) => $p['locale'], $paths),
167                array_map(static fn($p) => $p['path'], $paths),
168            );
169
170            $locale = $context->request->get('locale');
171
172            while ($locale) {
173                $path = $pathsByLocale[$locale->id] ?? null;
174
175                if ($path) {
176                    header('Location: ' . $path, true, 301);
177                    exit();
178                }
179
180                $locale = $locale->fallback();
181            }
182        }
183    }
184}