Issues (121)

src/Controllers/BaseController.php (1 issue)

Labels
Severity
1
<?php
2
3
namespace Jidaikobo\Kontiki\Controllers;
4
5
use Aura\Session\Session;
6
use Jidaikobo\Kontiki\Managers\CsrfManager;
7
use Jidaikobo\Kontiki\Managers\FlashManager;
8
use Jidaikobo\Kontiki\Middleware\AuthMiddleware;
9
use Jidaikobo\Kontiki\Services\RoutesService;
10
use Psr\Http\Message\ResponseInterface as Response;
11
use Psr\Http\Message\ServerRequestInterface as Request;
12
use Slim\App;
13
use Slim\Routing\RouteContext;
14
use Slim\Views\PhpRenderer;
15
16
abstract class BaseController
17
{
18
    protected array $routes = [];
19
    protected string $adminDirName = '';
20
    protected string $label = '';
21
22
    protected App $app;
23
    protected CsrfManager $csrfManager;
24
    protected FlashManager $flashManager;
25
    protected PhpRenderer $view;
26
    protected ?PhpRenderer $previewRenderer = null;
27
28
    /**
29
     * Constructor
30
     *
31
     * Initializes the BaseController with its dependencies.
32
     *
33
     * @param CsrfManager   $csrfManager csrfManager
34
     * @param FlashManager  $flashManager flashManager
35
     * @param PhpRenderer   $view view
36
     * @param RoutesService $routesService routesService
37
     */
38
    public function __construct(
39
        CsrfManager $csrfManager,
40
        FlashManager $flashManager,
41
        PhpRenderer $view,
42
        RoutesService $routesService
43
    ) {
44
        $this->csrfManager = $csrfManager;
45
        $this->flashManager = $flashManager;
46
        $this->view = $view;
47
        $this->setModel();
48
        $this->setRoutes($routesService);
49
        $this->setViewAttributes($routesService);
50
    }
51
52
    protected function setModel(): void
53
    {
54
    }
55
56
    protected function setViewAttributes($routesService): void
57
    {
58
        $this->view->setAttributes([
59
                'lang' => env('APPLANG', 'en'),
60
                'viewUrl' => env('POST_VIEW_URL', ''),
61
                'buttonPosition' => 'main',
62
                'sidebarItems' => $routesService->getRoutesByType('sidebar'),
63
                'is_previewable' => method_exists($this, 'renderPreview')
64
            ]);
65
    }
66
67
    protected function setRoutes($routesService): void
68
    {
69
        $this->routes = $routesService->getRoutesByController($this->adminDirName);
70
    }
71
72
    public function getRoutes(): array
73
    {
74
        return $this->routes;
75
    }
76
77
    public function getLabel(): string
78
    {
79
        return $this->label;
80
    }
81
82
    /**
83
     * Register routes for the controller.
84
     *
85
     * Defines the routing for this controller, based on traits.
86
     *
87
     * @param App    $app      The Slim application instance.
88
     * @param string $basePath The base path for the routes.
89
     *
90
     * @return void
91
     */
92
    public static function registerRoutes(App $app, string $basePath = ''): void
93
    {
94
        $controllerClass = static::class;
95
        $traits = class_uses($controllerClass);
96
        foreach ($traits as $trait) {
97
            $routeClass = self::resolveRouteClass($trait);
98
            if (class_exists($routeClass) && method_exists($routeClass, 'register')) {
99
                $routeClass::register($app, $basePath, $controllerClass);
100
            }
101
        }
102
    }
103
104
    /**
105
     * Resolve route class name from a trait name.
106
     *
107
     * Converts a trait name into the corresponding route class name.
108
     *
109
     * @param string $trait The fully qualified name of the trait.
110
     *
111
     * @return string The fully qualified name of the corresponding route class.
112
     */
113
    private static function resolveRouteClass(string $trait): string
114
    {
115
        $traitName = (new \ReflectionClass($trait))->getShortName();
116
        return "Jidaikobo\\Kontiki\\Controllers\\Routes\\" . str_replace('Trait', 'Routes', $traitName);
117
    }
118
119
    /**
120
     * Validate the CSRF token and handle errors if invalid.
121
     *
122
     * @param array    $data            The request data (e.g., POST body).
123
     * @param Request  $request         The current request instance.
124
     * @param Response $response        The current response instance.
125
     * @param string   $redirectTarget  The URL or route to redirect if validation fails.
126
     *
127
     * @return Response|null Returns a redirect response if invalid, or null if valid.
128
     */
129
    protected function validateCsrfToken(
130
        ?array $data,
131
        Request $request,
132
        Response $response,
133
        string $redirectTarget
134
    ): ?Response {
135
        $data = $data ?? [];
136
137
        if (!$this->isCsrfTokenValid($data)) {
138
            $this->flashManager->addErrors([
139
                ['messages' => [__("csrf_invalid", 'Invalid CSRF token.')]],
140
            ]);
141
            return $this->redirectResponse($request, $response, $redirectTarget);
142
        }
143
144
        $this->csrfManager->regenerate();
145
146
        return null;
147
    }
148
149
    protected function validateCsrfForJson(?array $data, Response $response): ?Response
150
    {
151
        $data = $data ?? [];
152
        if (!$this->isCsrfTokenValid($data)) {
153
            $this->flashManager->addErrors([
154
                ['messages' => [__("csrf_invalid", 'Invalid CSRF token.')]],
155
            ]);
156
            return $this->jsonResponse($response, $data, 403);
157
        }
158
159
        $this->csrfManager->regenerate();
160
161
        return null;
162
    }
163
164
    private function isCsrfTokenValid(array $data): bool
165
    {
166
        return !empty($data['_csrf_value']) && $this->csrfManager->isValid($data['_csrf_value']);
167
    }
168
169
    /**
170
     * Create a redirect response.
171
     *
172
     * @param  Request  $request
173
     * @param  Response $response
174
     * @param  string   $target      Route name or URL.
175
     * @param  array    $routeData   Route parameters (for named routes).
176
     * @param  int      $status      HTTP status code for the redirect (default: 302).
177
     *
178
     * @return Response
179
     */
180
    protected function redirectResponse(
181
        Request $request,
182
        Response $response,
183
        string $target,
184
        array $routeData = [],
185
        int $status = 302
186
    ): Response {
187
        if (strpos($target, '/') === 0 || filter_var($target, FILTER_VALIDATE_URL)) {
188
            $redirectUrl = env('BASEPATH', '') . $target;
189
        } else {
190
            $routeParser = RouteContext::fromRequest($request)->getRouteParser();
191
            $redirectUrl = $routeParser->urlFor($target, $routeData);
192
        }
193
194
        return $response
195
            ->withHeader('Location', $redirectUrl)
196
            ->withStatus($status);
0 ignored issues
show
The method withStatus() does not exist on Psr\Http\Message\MessageInterface. It seems like you code against a sub-type of Psr\Http\Message\MessageInterface such as Psr\Http\Message\ResponseInterface or Slim\Psr7\Response. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

196
            ->/** @scrutinizer ignore-call */ withStatus($status);
Loading history...
197
    }
198
199
    /**
200
     * Render a response with the given content and template.
201
     *
202
     * @param Response $response       The Slim response object.
203
     * @param string   $pageTitle      The page title for the rendered view.
204
     * @param string   $content        The main content of the page.
205
     * @param string   $template       The template to use for rendering.
206
     * @param array    $additionalData Additional data to pass to the view.
207
     *
208
     * @return Response The rendered response.
209
     */
210
    protected function renderResponse(
211
        Response $response,
212
        string $pageTitle,
213
        string $content,
214
        string $template = 'layout.php',
215
        array $additionalData = []
216
    ): Response {
217
        // Derive title/h1 with sane defaults (BC-friendly)
218
        $title = $additionalData['title'] ?? $pageTitle;
219
        $h1    = $additionalData['h1']    ?? $pageTitle;
220
221
        // Combine standard and additional data for the view
222
        $data = array_merge(
223
            [
224
                'pageTitle' => $pageTitle,
225
                'title'     => $title,
226
                'h1'        => $h1,
227
                'content' => $content,
228
            ],
229
            $additionalData
230
        );
231
232
        $cacheControl = 'no-store, no-cache, must-revalidate, max-age=0';
233
        $response = $response->withHeader('Cache-Control', $cacheControl)
234
                             ->withHeader('Pragma', 'no-cache')
235
                             ->withHeader('Expires', '0');
236
237
        // Output Buffering with Exception Handling
238
        ob_start();
239
        try {
240
            $response = $this->view->render($response, $template, $data);
241
            $output = ob_get_clean();
242
        } catch (\Throwable $e) {
243
            ob_end_clean(); // Ensure buffer is cleared on error
244
            throw $e;
245
        }
246
247
        $response->getBody()->write($output);
248
        return $response;
249
    }
250
251
    /**
252
     * Create a JSON response.
253
     *
254
     * @param Response $response The original response object.
255
     * @param array $data The data to be included in the JSON response.
256
     * @param int $status The HTTP status code.
257
     *
258
     * @return Response The modified response object with JSON content.
259
     */
260
    public static function jsonResponse(
261
        Response $response,
262
        array $data,
263
        int $status = 200
264
    ): Response {
265
        $response->getBody()->write(json_encode($data));
266
        return $response->withHeader('Content-Type', 'application/json')->withStatus($status);
267
    }
268
}
269