Completed
Push — feature/controller ( c1cf64...243833 )
by René
04:19
created

Controller::getViewTemplate()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 5
Bugs 0 Features 0
Metric Value
c 5
b 0
f 0
dl 0
loc 10
ccs 5
cts 5
cp 1
rs 9.4285
cc 2
eloc 5
nc 2
nop 0
crap 2
1
<?php
2
declare(strict_types = 1);
3
4
namespace Zortje\MVC\Controller;
5
6
use Zortje\MVC\Configuration\Configuration;
7
use Zortje\MVC\Controller\Exception\ControllerActionNonexistentException;
8
use Zortje\MVC\Controller\Exception\ControllerActionPrivateInsufficientAuthenticationException;
9
use Zortje\MVC\Controller\Exception\ControllerActionProtectedInsufficientAuthenticationException;
10
use Zortje\MVC\Model\Table\Entity\Entity;
11
use Zortje\MVC\Network\Request;
12
use Zortje\MVC\Network\Response;
13
use Zortje\MVC\View\Render\HtmlRender;
14
15
/**
16
 * Class Controller
17
 *
18
 * @package Zortje\MVC\Controller
19
 */
20
class Controller
21
{
22
23
    /**
24
     * Controller action is publicly accessible
25
     */
26
    const ACTION_PUBLIC = 0;
27
28
    /**
29
     * Controller action requires authentication
30
     * Will redirect to sign in page if not authenticated
31
     */
32
    const ACTION_PROTECTED = 1;
33
34
    /**
35
     * Controller action requires authentication
36
     * Will result in an 404 if not authenticated
37
     */
38
    const ACTION_PRIVATE = 2;
39
40
    /**
41
     * @var array Controller action access rules
42
     */
43
    protected $access = [];
44
45
    /**
46
     * @var \PDO PDO
47
     */
48
    protected $pdo;
49
50
    /**
51
     * @var Configuration
52
     */
53
    protected $configuration;
54
55
    /**
56
     * @var Request
57
     */
58
    protected $request;
59
60
    /**
61
     * @var Entity|null User
62
     */
63
    protected $user;
64
65
    /**
66
     * @var string Controller action
67
     */
68
    protected $action;
69
70
    /**
71
     * @var array View variables
72
     */
73
    protected $variables = [];
74
75
    /**
76
     * @var bool Should render view for controller action
77
     */
78
    protected $render = true;
79
80
    /**
81
     * @var string File path for layout template file
82
     */
83
    protected $layout;
84
85
    /**
86
     * @var string File path for view template file
87
     */
88
    protected $view;
89
90
    /**
91
     * @var string Content type
92
     */
93
    protected $contentType = 'html';
94
95
    /**
96
     * @var array Headers for output
97
     */
98
    protected $headers = [];
99
100
    /**
101
     * Controller constructor.
102
     *
103
     * @param \PDO          $pdo
104
     * @param Configuration $configuration
105
     * @param Request       $request
106
     * @param Entity|null   $user
107
     */
108 1 View Code Duplication
    public function __construct(\PDO $pdo, Configuration $configuration, Request $request, Entity $user = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
109
    {
110 1
        $this->pdo           = $pdo;
111 1
        $this->configuration = $configuration;
112 1
        $this->request       = $request;
113 1
        $this->user          = $user;
114 1
    }
115
116
    /**
117
     * @return string Controller name without namespace
118
     */
119 1
    public function getShortName(): string
120
    {
121 1
        return str_replace('Controller', null, (new \ReflectionClass($this))->getShortName());
122
    }
123
124
    /**
125
     * @param string $action Controller action
126
     *
127
     * @throws ControllerActionNonexistentException
128
     * @throws ControllerActionPrivateInsufficientAuthenticationException
129
     * @throws ControllerActionProtectedInsufficientAuthenticationException
130
     */
131 4
    public function setAction(string $action)
132
    {
133
        /**
134
         * Check if method exists and that access has been defined
135
         */
136 4
        if (!method_exists($this, $action) || !isset($this->access[$action])) {
137 1
            throw new ControllerActionNonexistentException([get_class($this), $action]);
138
        }
139
140
        /**
141
         * Check controller action access level if user is not authenticated
142
         */
143 3
        if (!$this->user) {
144 3
            if ($this->access[$action] === self::ACTION_PRIVATE) {
145 1
                throw new ControllerActionPrivateInsufficientAuthenticationException([get_class($this), $action]);
146 2
            } elseif ($this->access[$action] === self::ACTION_PROTECTED) {
147 1
                throw new ControllerActionProtectedInsufficientAuthenticationException([get_class($this), $action]);
148
            }
149
        }
150
151
        /**
152
         * Set controller action
153
         */
154 1
        $this->action = $action;
155 1
    }
156
157
    /**
158
     * Call action
159
     *
160
     * @return Response
161
     *
162
     * @throws \LogicException If controller action is not set
163
     */
164
    public function callAction(): Response
165
    {
166
        if (!isset($this->action)) {
167
            throw new \LogicException('Controller action must be set before being called');
168
        }
169
170
        /**
171
         * Before controller action hook
172
         */
173
        if ($this->beforeAction()) {
174
            /**
175
             * Call controller action
176
             */
177
            $action = $this->action;
178
179
            $this->$action();
0 ignored issues
show
Security Code Execution introduced by
$action can contain request data and is used in code execution context(s) leading to a potential security vulnerability.

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
180
181
            /**
182
             * After controller action hook
183
             */
184
            $this->afterAction();
185
        }
186
187
        /**
188
         * Render view
189
         */
190
        if ($this->render && $this->contentType === 'html') {
191
            if ($this->request->getCookie()->exists('Flash.Message') && $this->request->getCookie()->exists('Flash.Type')) {
192
                $this->set('_flash', [
193
                    'message' => $this->request->getCookie()->get('Flash.Message'),
194
                    'type'    => $this->request->getCookie()->get('Flash.Type')
195
                ]);
196
197
                $this->request->getCookie()->remove('Flash.Message');
198
                $this->request->getCookie()->remove('Flash.Type');
199
            }
200
201
            /**
202
             * Set content type header
203
             */
204
            $this->headers['content-type'] = 'Content-Type: text/html; charset=utf-8';
205
206
            /**
207
             * Render output
208
             */
209
            $render = new HtmlRender($this->variables);
210
211
            $output = $render->render(['_view' => $this->getViewTemplate(), '_layout' => $this->getLayoutTemplate()]);
212
        } elseif ($this->render && $this->contentType === 'json') {
213
            /**
214
             * Set content type header
215
             */
216
            $this->headers['content-type'] = 'Content-Type: application/javascript;';
217
218
            /**
219
             * Render output
220
             */
221
            $output = json_encode([$this->variables]);
222
223
        } else {
224
            $output = '';
225
        }
226
227
        return new Response($this->headers, $this->request->getCookie(), $output);
228
    }
229
230
    /**
231
     * Before controller action hook
232
     *
233
     * Called right before controller action is called
234
     *
235
     * If FALSE is returned, the action will not be called
236
     *
237
     * @return bool
238
     */
239
    protected function beforeAction(): bool
240
    {
241
        return true;
242
    }
243
244
    /**
245
     * After controller action hook
246
     *
247
     * Called right after controller action is called, before rendering of the view
248
     *
249
     * Only called if action is called
250
     */
251
    protected function afterAction()
252
    {
253
    }
254
255
    /**
256
     * Set view variable
257
     *
258
     * @param string $variable
259
     * @param mixed  $value
260
     */
261
    protected function set(string $variable, $value)
262
    {
263
        $this->variables[$variable] = $value;
264
    }
265
266
    /**
267
     * Set flash message
268
     *
269
     * Recommended types: error, warning, success & info
270
     *
271
     * @param string $message Flash message
272
     * @param string $type    Flash type
273
     */
274
    protected function setFlash(string $message, string $type)
275
    {
276
        $cookie = $this->request->getCookie();
277
278
        $cookie->set('Flash.Message', $message);
279
        $cookie->set('Flash.Type', $type);
280
    }
281
282
    /**
283
     * Set a redirect header in the response
284
     *
285
     * @param string $url URL for redirect
286
     */
287
    protected function redirect(string $url)
288
    {
289
        $this->headers['locaction'] = "Location: $url";
290
291
        /**
292
         * Disable rendering if redirecting
293
         */
294
        $this->render = false;
295
    }
296
297
    /**
298
     * Get layout template
299
     *
300
     * @return string Layout template file path
301
     */
302 1
    protected function getLayoutTemplate(): string
303
    {
304 1
        $layout = $this->layout;
305
306 1
        if (empty($layout)) {
307
            $layout = 'View/Layout/default';
308
        }
309
310 1
        return "{$this->configuration->get('App.Path')}$layout.layout";
311
    }
312
313
    /**
314
     * Get view template
315
     *
316
     * @return string View template file path
317
     */
318 2
    protected function getViewTemplate(): string
319
    {
320 2
        $view = $this->view;
321
322 2
        if (empty($view)) {
323 1
            $view = sprintf('View/%s/%s', $this->getShortName(), $this->action);
324
        }
325
326 2
        return "{$this->configuration->get('App.Path')}$view.view";
327
    }
328
329
    /**
330
     * Set response code
331
     *
332
     * Supports 200 OK, 403 Forbidden, 404 Not Found & 500 Internal Server Error
333
     *
334
     * @param int $code HTTP response code
335
     *
336
     * @throws \InvalidArgumentException If unsupported code is provided
337
     */
338 5
    protected function setResponseCode(int $code)
339
    {
340
        switch ($code) {
341 5
            case 200:
342 1
                $text = 'OK';
343 1
                break;
344
345 4
            case 403:
346 1
                $text = 'Forbidden';
347 1
                break;
348
349 3
            case 404:
350 1
                $text = 'Not Found';
351 1
                break;
352
353 2
            case 500:
354 1
                $text = 'Internal Server Error';
355 1
                break;
356
357
            default:
358 1
                throw new \InvalidArgumentException("HTTP status '$code' is not implemented");
359
        }
360
361
        /**
362
         * Set header
363
         */
364
        // @todo test that running response code multiple times only results in one response code header
365 4
        $this->headers['response_code'] = "HTTP/1.1 $code $text";
366 4
    }
367
}
368