Completed
Pull Request — master (#2)
by René
05:02 queued 02:37
created

Controller::setFlash()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 7
ccs 0
cts 5
cp 0
rs 9.4285
cc 1
eloc 4
nc 1
nop 2
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 array URL Arguments
67
     */
68
    protected $arguments;
69
70
    /**
71
     * @var string Controller action
72
     */
73
    protected $action;
74
75
    /**
76
     * @var array View variables
77
     */
78
    protected $variables = [];
79
80
    /**
81
     * @var bool Should render view for controller action
82
     */
83
    protected $render = true;
84
85
    /**
86
     * @var string File path for layout template file
87
     */
88
    protected $layout;
89
90
    /**
91
     * @var string File path for view template file
92
     */
93
    protected $view;
94
95
    /**
96
     * @var string Content type
97
     */
98
    protected $contentType = 'html';
99
100
    /**
101
     * @var array Headers for output
102
     */
103
    protected $headers = [];
104
105
    /**
106
     * Controller constructor.
107
     *
108
     * @param \PDO          $pdo
109
     * @param Configuration $configuration
110
     * @param Request       $request
111
     * @param Entity|null   $user
112
     */
113 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...
114
    {
115 1
        $this->pdo           = $pdo;
116 1
        $this->configuration = $configuration;
117 1
        $this->request       = $request;
118 1
        $this->user          = $user;
119 1
    }
120
121
    /**
122
     * Get short controller name
123
     *
124
     * @return string Controller name without namespace
125
     */
126 1
    public function getShortName(): string
127
    {
128 1
        return str_replace('Controller', null, (new \ReflectionClass($this))->getShortName());
129
    }
130
131
    /**
132
     * Set URL arguments
133
     *
134
     * @param array $arguments
135
     */
136 1
    public function setArguments(array $arguments)
137
    {
138 1
        $this->arguments = $arguments;
139 1
    }
140
141
    /**
142
     * Set controller action
143
     *
144
     * @param string $action Controller action
145
     *
146
     * @throws ControllerActionNonexistentException
147
     * @throws ControllerActionPrivateInsufficientAuthenticationException
148
     * @throws ControllerActionProtectedInsufficientAuthenticationException
149
     */
150 4
    public function setAction(string $action)
151
    {
152
        /**
153
         * Check if method exists and that access has been defined
154
         */
155 4
        if (!method_exists($this, $action) || !isset($this->access[$action])) {
156 1
            throw new ControllerActionNonexistentException([get_class($this), $action]);
157
        }
158
159
        /**
160
         * Check controller action access level if user is not authenticated
161
         */
162 3
        if (!$this->user) {
163 3
            if ($this->access[$action] === self::ACTION_PRIVATE) {
164 1
                throw new ControllerActionPrivateInsufficientAuthenticationException([get_class($this), $action]);
165 2
            } elseif ($this->access[$action] === self::ACTION_PROTECTED) {
166 1
                throw new ControllerActionProtectedInsufficientAuthenticationException([get_class($this), $action]);
167
            }
168
        }
169
170
        /**
171
         * Set controller action
172
         */
173 1
        $this->action = $action;
174 1
    }
175
176
    /**
177
     * Call action
178
     *
179
     * @return Response
180
     *
181
     * @throws \LogicException If controller action is not set
182
     */
183
    public function callAction(): Response
184
    {
185
        if (!isset($this->action)) {
186
            throw new \LogicException('Controller action must be set before being called');
187
        }
188
189
        /**
190
         * Before controller action hook
191
         */
192
        if ($this->beforeAction()) {
193
            /**
194
             * Call controller action
195
             */
196
            $action = $this->action;
197
198
            $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.

1 path for user data to reach this point

  1. Entered via action parameter $action
    in vendor/src/Controller/Controller.php on line 150
  2. Controller::$action is assigned
    in src/Controller/Controller.php on line 173
  3. Tainted property Controller::$action is read, and $action is assigned
    in src/Controller/Controller.php on line 196

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...
199
200
            /**
201
             * After controller action hook
202
             */
203
            $this->afterAction();
204
        }
205
206
        /**
207
         * Render view
208
         */
209
        if ($this->render && $this->contentType === 'html') {
210
            if ($this->request->getCookie()->exists('Flash.Message') && $this->request->getCookie()->exists('Flash.Type')) {
211
                $this->set('_flash', [
212
                    'message' => $this->request->getCookie()->get('Flash.Message'),
213
                    'type'    => $this->request->getCookie()->get('Flash.Type')
214
                ]);
215
216
                $this->request->getCookie()->remove('Flash.Message');
217
                $this->request->getCookie()->remove('Flash.Type');
218
            }
219
220
            /**
221
             * Set content type header
222
             */
223
            $this->headers['content-type'] = 'Content-Type: text/html; charset=utf-8';
224
225
            /**
226
             * Render output
227
             */
228
            $render = new HtmlRender($this->variables);
229
230
            $output = $render->render(['_view' => $this->getViewTemplate(), '_layout' => $this->getLayoutTemplate()]);
231
232
            /**
233
             * Return response
234
             */
235
            return new Response($this->headers, $this->request->getCookie(), $output);
236
        } elseif ($this->render && $this->contentType === 'json') {
237
            /**
238
             * Set content type header
239
             */
240
            $this->headers['content-type'] = 'Content-Type: application/javascript;';
241
242
            /**
243
             * Return response
244
             */
245
            return new Response($this->headers, null, json_encode($this->variables));
246
        }
247
248
        return new Response($this->headers, null, '');
249
    }
250
251
    /**
252
     * Before controller action hook
253
     *
254
     * Called right before controller action is called
255
     *
256
     * If FALSE is returned, the action will not be called
257
     *
258
     * @return bool
259
     */
260
    protected function beforeAction(): bool
261
    {
262
        return true;
263
    }
264
265
    /**
266
     * After controller action hook
267
     *
268
     * Called right after controller action is called, before rendering of the view
269
     *
270
     * Only called if action is called
271
     */
272
    protected function afterAction()
273
    {
274
    }
275
276
    /**
277
     * Set view variable
278
     *
279
     * @param string $variable
280
     * @param mixed  $value
281
     */
282 1
    protected function set(string $variable, $value)
283
    {
284 1
        $this->variables[$variable] = $value;
285 1
    }
286
287
    /**
288
     * Set flash message
289
     *
290
     * Recommended types: error, warning, success & info
291
     *
292
     * @param string $message Flash message
293
     * @param string $type    Flash type
294
     */
295
    protected function setFlash(string $message, string $type)
296
    {
297
        $cookie = $this->request->getCookie();
298
299
        $cookie->set('Flash.Message', $message);
300
        $cookie->set('Flash.Type', $type);
301
    }
302
303
    /**
304
     * Set a redirect header in the response
305
     *
306
     * @param string $url URL for redirect
307
     */
308 1
    protected function redirect(string $url)
309
    {
310 1
        $this->headers['location'] = "Location: $url";
311
312
        /**
313
         * Disable rendering if redirecting
314
         */
315 1
        $this->render = false;
316 1
    }
317
318
    /**
319
     * Get layout template
320
     *
321
     * @return string Layout template file path
322
     */
323 1
    protected function getLayoutTemplate(): string
324
    {
325 1
        $layout = $this->layout;
326
327 1
        if (empty($layout)) {
328
            $layout = 'View/Layout/default';
329
        }
330
331 1
        return "{$this->configuration->get('App.Path')}$layout.layout";
332
    }
333
334
    /**
335
     * Get view template
336
     *
337
     * @return string View template file path
338
     */
339 2
    protected function getViewTemplate(): string
340
    {
341 2
        $view = $this->view;
342
343 2
        if (empty($view)) {
344 1
            $view = sprintf('View/%s/%s', $this->getShortName(), $this->action);
345
        }
346
347 2
        return "{$this->configuration->get('App.Path')}$view.view";
348
    }
349
350
    /**
351
     * Set response code
352
     *
353
     * Supports 200 OK, 403 Forbidden, 404 Not Found & 500 Internal Server Error
354
     *
355
     * @param int $code HTTP response code
356
     *
357
     * @throws \InvalidArgumentException If unsupported code is provided
358
     */
359 9
    protected function setResponseCode(int $code)
360
    {
361
        switch ($code) {
362
            /**
363
             * Success
364
             */
365 9
            case 200:
366 1
                $text = 'OK';
367 1
                break;
368
369 8
            case 201:
370 1
                $text = 'Created';
371 1
                break;
372
373 7
            case 202:
374 1
                $text = 'Accepted';
375 1
                break;
376
377 6
            case 204:
378 1
                $text = 'No Content';
379 1
                break;
380
381
            /**
382
             * Client error
383
             */
384 5
            case 400:
385 1
                $text = 'Bad Request';
386 1
                break;
387
388 4
            case 403:
389 1
                $text = 'Forbidden';
390 1
                break;
391
392 3
            case 404:
393 1
                $text = 'Not Found';
394 1
                break;
395
396
            /**
397
             * Server error
398
             */
399 2
            case 500:
400 1
                $text = 'Internal Server Error';
401 1
                break;
402
403
            default:
404 1
                throw new \InvalidArgumentException("HTTP status '$code' is not implemented");
405
        }
406
407
        /**
408
         * Set header
409
         */
410
        // @todo test that running response code multiple times only results in one response code header
411 8
        $this->headers['response_code'] = "HTTP/1.1 $code $text";
412 8
    }
413
}
414