Completed
Pull Request — master (#2)
by René
04:42
created

Controller::callAction()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 46
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 4
Bugs 0 Features 0
Metric Value
c 4
b 0
f 0
dl 0
loc 46
ccs 0
cts 18
cp 0
rs 8.4751
cc 5
eloc 19
nc 4
nop 0
crap 30
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 array Headers for output
92
     *
93
     * @todo JSON content type: `Content-Type: application/javascript; charset=utf-8`
94
     */
95
    protected $headers = [
96
        'content-type' => 'Content-Type: text/html; charset=utf-8'
97
    ];
98
99
    /**
100
     * Controller constructor.
101
     *
102
     * @param \PDO          $pdo
103
     * @param Configuration $configuration
104
     * @param Request       $request
105
     * @param Entity|null   $user
106
     */
107 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...
108
    {
109 1
        $this->pdo           = $pdo;
110 1
        $this->configuration = $configuration;
111 1
        $this->request       = $request;
112 1
        $this->user          = $user;
113 1
    }
114
115
    /**
116
     * @return string Controller name without namespace
117
     */
118 1
    public function getShortName(): string
119
    {
120 1
        return str_replace('Controller', null, (new \ReflectionClass($this))->getShortName());
121
    }
122
123
    /**
124
     * @param string $action Controller action
125
     *
126
     * @throws ControllerActionNonexistentException
127
     * @throws ControllerActionPrivateInsufficientAuthenticationException
128
     * @throws ControllerActionProtectedInsufficientAuthenticationException
129
     */
130 4
    public function setAction(string $action)
131
    {
132
        /**
133
         * Check if method exists and that access has been defined
134
         */
135 4
        if (!method_exists($this, $action) || !isset($this->access[$action])) {
136 1
            throw new ControllerActionNonexistentException([get_class($this), $action]);
137
        }
138
139
        /**
140
         * Check controller action access level if user is not authenticated
141
         */
142 3
        if (!$this->user) {
143 3
            if ($this->access[$action] === self::ACTION_PRIVATE) {
144 1
                throw new ControllerActionPrivateInsufficientAuthenticationException([get_class($this), $action]);
145 2
            } elseif ($this->access[$action] === self::ACTION_PROTECTED) {
146 1
                throw new ControllerActionProtectedInsufficientAuthenticationException([get_class($this), $action]);
147
            }
148
        }
149
150
        /**
151
         * Set controller action
152
         */
153 1
        $this->action = $action;
154 1
    }
155
156
    /**
157
     * Call action
158
     *
159
     * @return Response
160
     *
161
     * @throws \LogicException If controller action is not set
162
     */
163
    public function callAction(): Response
164
    {
165
        if (!isset($this->action)) {
166
            throw new \LogicException('Controller action must be set before being called');
167
        }
168
169
        /**
170
         * Before controller action hook
171
         */
172
        $this->beforeAction();
173
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
         * Render view
188
         */
189
        if ($this->render) {
190
            if ($this->request->getCookie()->exists('Flash.Message') && $this->request->getCookie()->exists('Flash.Type')) {
191
                $this->set('_flash', [
192
                    'message' => $this->request->getCookie()->get('Flash.Message'),
193
                    'type'    => $this->request->getCookie()->get('Flash.Type')
194
                ]);
195
196
                $this->request->getCookie()->remove('Flash.Message');
197
                $this->request->getCookie()->remove('Flash.Type');
198
            }
199
200
            $render = new HtmlRender($this->variables);
201
202
            $output = $render->render(['_view' => $this->getViewTemplate(), '_layout' => $this->getLayoutTemplate()]);
203
        } else {
204
            $output = '';
205
        }
206
207
        return new Response($this->headers, $this->request->getCookie(), $output);
208
    }
209
210
    /**
211
     * Before controller action hook
212
     *
213
     * Called right before controller action is called
214
     */
215
    protected function beforeAction()
216
    {
217
    }
218
219
    /**
220
     * After controller action hook
221
     *
222
     * Called right after controller action is called, but before rendering of the view
223
     */
224
    protected function afterAction()
225
    {
226
    }
227
228
    /**
229
     * Set view variable
230
     *
231
     * @param string $variable
232
     * @param mixed  $value
233
     */
234
    protected function set(string $variable, $value)
235
    {
236
        $this->variables[$variable] = $value;
237
    }
238
239
    /**
240
     * Set flash message
241
     *
242
     * Recommended types: error, warning, success & info
243
     *
244
     * @param string $message Flash message
245
     * @param string $type    Flash type
246
     */
247
    protected function setFlash(string $message, string $type)
248
    {
249
        $cookie = $this->request->getCookie();
250
251
        $cookie->set('Flash.Message', $message);
252
        $cookie->set('Flash.Type', $type);
253
    }
254
255
    /**
256
     * Set a redirect header in the response
257
     *
258
     * @param string $url URL for redirect
259
     */
260
    protected function redirect(string $url)
261
    {
262
        $this->headers['locaction'] = "Location: $url";
263
264
        /**
265
         * Disable rendering if redirecting
266
         */
267
        $this->render = false;
268
    }
269
270
    /**
271
     * Get layout template
272
     *
273
     * @return string Layout template file path
274
     */
275 1
    protected function getLayoutTemplate(): string
276
    {
277 1
        $layout = $this->layout;
278
279 1
        if (empty($layout)) {
280
            $layout = 'View/Layout/default';
281
        }
282
283 1
        return "{$this->configuration->get('App.Path')}$layout.layout";
284
    }
285
286
    /**
287
     * Get view template
288
     *
289
     * @return string View template file path
290
     */
291 2
    protected function getViewTemplate(): string
292
    {
293 2
        $view = $this->view;
294
295 2
        if (empty($view)) {
296 1
            $view = sprintf('View/%s/%s', $this->getShortName(), $this->action);
297
        }
298
299 2
        return "{$this->configuration->get('App.Path')}$view.view";
300
    }
301
302
    /**
303
     * Set response code
304
     *
305
     * Supports 200 OK, 403 Forbidden, 404 Not Found & 500 Internal Server Error
306
     *
307
     * @param int $code HTTP response code
308
     *
309
     * @throws \InvalidArgumentException If unsupported code is provided
310
     */
311 5
    protected function setResponseCode(int $code)
312
    {
313
        switch ($code) {
314 5
            case 200:
315 1
                $text = 'OK';
316 1
                break;
317
318 4
            case 403:
319 1
                $text = 'Forbidden';
320 1
                break;
321
322 3
            case 404:
323 1
                $text = 'Not Found';
324 1
                break;
325
326 2
            case 500:
327 1
                $text = 'Internal Server Error';
328 1
                break;
329
330
            default:
331 1
                throw new \InvalidArgumentException("HTTP status '$code' is not implemented");
332
        }
333
334
        /**
335
         * Set header
336
         */
337
        // @todo test that running response code multiple times only results in one response code header
338 4
        $this->headers['response_code'] = "HTTP/1.1 $code $text";
339 4
    }
340
}
341