PageBase   B
last analyzed

Complexity

Total Complexity 43

Size/Duplication

Total Lines 372
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 43
eloc 115
c 0
b 0
f 0
dl 0
loc 372
rs 8.96

19 Methods

Rating   Name   Duplication   Size   Complexity  
A setupPage() 0 8 1
A setTemplate() 0 7 2
A setHtmlTitle() 0 3 1
A setRoute() 0 9 3
A finalisePage() 0 17 3
B runPage() 0 89 7
A setTokenManager() 0 3 1
A setCspManager() 0 3 1
A addJs() 0 8 2
A validateCSRFToken() 0 4 2
A redirectUrl() 0 8 1
A redirect() 0 27 6
A assignCSRFToken() 0 4 1
A sendResponseHeaders() 0 16 5
A getRouteName() 0 3 1
A skipAlerts() 0 3 1
A getTokenManager() 0 3 1
A getCspManager() 0 3 1
A execute() 0 13 3

How to fix   Complexity   

Complex Class

Complex classes like PageBase often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PageBase, and based on these observations, apply Extract Interface, too.

1
<?php
2
/******************************************************************************
3
 * Wikipedia Account Creation Assistance tool                                 *
4
 *                                                                            *
5
 * All code in this file is released into the public domain by the ACC        *
6
 * Development Team. Please see team.json for a list of contributors.         *
7
 ******************************************************************************/
8
9
namespace Waca\Tasks;
10
11
use Exception;
12
use SmartyException;
13
use Waca\DataObjects\Domain;
14
use Waca\DataObjects\User;
15
use Waca\ExceptionHandler;
16
use Waca\Exceptions\ApplicationLogicException;
17
use Waca\Exceptions\OptimisticLockFailedException;
18
use Waca\Fragments\TemplateOutput;
19
use Waca\Security\ContentSecurityPolicyManager;
20
use Waca\Security\TokenManager;
21
use Waca\SessionAlert;
22
use Waca\WebRequest;
23
24
abstract class PageBase extends TaskBase implements IRoutedTask
25
{
26
    use TemplateOutput;
27
    /** @var string Smarty template to display */
28
    protected $template = "base.tpl";
29
    /** @var string HTML title. Currently unused. */
30
    protected $htmlTitle;
31
    /** @var bool Determines if the page is a redirect or not */
32
    protected $isRedirecting = false;
33
    /** @var array Queue of headers to be sent on successful completion */
34
    protected $headerQueue = array();
35
    /** @var string The name of the route to use, as determined by the request router. */
36
    private $routeName = null;
37
    /** @var TokenManager */
38
    protected $tokenManager;
39
    /** @var ContentSecurityPolicyManager */
40
    private $cspManager;
41
    /** @var string[] Extra JS files to include */
42
    private $extraJs = array();
43
    /** @var bool Don't show (and hence clear) session alerts when this page is displayed  */
44
    private $hideAlerts = false;
45
46
    /**
47
     * Sets the route the request will take. Only should be called from the request router or barrier test.
48
     *
49
     * @param string $routeName        The name of the route
50
     * @param bool   $skipCallableTest Don't use this unless you know what you're doing, and what the implications are.
51
     *
52
     * @throws Exception
53
     * @category Security-Critical
54
     */
55
    final public function setRoute($routeName, $skipCallableTest = false)
56
    {
57
        // Test the new route is callable before adopting it.
58
        if (!$skipCallableTest && !is_callable(array($this, $routeName))) {
59
            throw new Exception("Proposed route '$routeName' is not callable.");
60
        }
61
62
        // Adopt the new route
63
        $this->routeName = $routeName;
64
    }
65
66
    /**
67
     * Gets the name of the route that has been passed from the request router.
68
     * @return string
69
     */
70
    final public function getRouteName()
71
    {
72
        return $this->routeName;
73
    }
74
75
    /**
76
     * Performs generic page setup actions
77
     */
78
    final protected function setupPage()
79
    {
80
        $this->setUpSmarty();
81
82
        $currentUser = User::getCurrent($this->getDatabase());
83
        $this->assign('currentUser', $currentUser);
84
        $this->assign('currentDomain', Domain::getCurrent($this->getDatabase()));
85
        $this->assign('loggedIn', (!$currentUser->isCommunityUser()));
86
    }
87
88
    /**
89
     * Runs the page logic as routed by the RequestRouter
90
     *
91
     * Only should be called after a security barrier! That means only from execute().
92
     */
93
    final protected function runPage()
94
    {
95
        $database = $this->getDatabase();
96
97
        // initialise a database transaction
98
        if (!$database->beginTransaction()) {
99
            throw new Exception('Failed to start transaction on primary database.');
100
        }
101
102
        try {
103
            // run the page code
104
            $this->{$this->getRouteName()}();
105
106
            $database->commit();
107
        }
108
        /** @noinspection PhpRedundantCatchClauseInspection */
109
        catch (ApplicationLogicException $ex) {
110
            // it's an application logic exception, so nothing went seriously wrong with the site. We can use the
111
            // standard templating system for this.
112
113
            // Firstly, let's undo anything that happened to the database.
114
            $database->rollBack();
115
116
            // Reset smarty
117
            $this->setupPage();
118
119
            $this->skipAlerts();
120
121
            // Set the template
122
            $this->setTemplate('exception/application-logic.tpl');
123
            $this->assign('message', $ex->getMessage());
124
125
            // Force this back to false
126
            $this->isRedirecting = false;
127
            $this->headerQueue = array();
128
        }
129
        /** @noinspection PhpRedundantCatchClauseInspection */
130
        catch (OptimisticLockFailedException $ex) {
131
            // it's an optimistic lock failure exception, so nothing went seriously wrong with the site. We can use the
132
            // standard templating system for this.
133
134
            // Firstly, let's undo anything that happened to the database.
135
            $database->rollBack();
136
137
            // Reset smarty
138
            $this->setupPage();
139
140
            // Set the template
141
            $this->skipAlerts();
142
            $this->setTemplate('exception/optimistic-lock-failure.tpl');
143
            $this->assign('message', $ex->getMessage());
144
145
            $this->assign('debugTrace', false);
146
147
            if ($this->getSiteConfiguration()->getDebuggingTraceEnabled()) {
148
                ob_start();
149
                var_dump(ExceptionHandler::getExceptionData($ex));
1 ignored issue
show
Security Debugging Code introduced by
var_dump(Waca\ExceptionH...:getExceptionData($ex)) looks like debug code. Are you sure you do not want to remove it?
Loading history...
150
                $textErrorData = ob_get_contents();
151
                ob_end_clean();
152
153
                $this->assign('exceptionData', $textErrorData);
154
                $this->assign('debugTrace', true);
155
            }
156
157
            // Force this back to false
158
            $this->isRedirecting = false;
159
            $this->headerQueue = array();
160
        }
161
        finally {
162
            // Catch any hanging on transactions
163
            if ($database->hasActiveTransaction()) {
164
                $database->rollBack();
165
            }
166
        }
167
168
        // run any finalisation code needed before we send the output to the browser.
169
        $this->finalisePage();
170
171
        // Send the headers
172
        $this->sendResponseHeaders();
173
174
        // Check we have a template to use!
175
        if ($this->template !== null) {
176
            $content = $this->fetchTemplate($this->template);
177
            ob_clean();
178
            print($content);
179
            ob_flush();
180
181
            return;
182
        }
183
    }
184
185
    /**
186
     * Performs final tasks needed before rendering the page.
187
     */
188
    protected function finalisePage()
189
    {
190
        if ($this->isRedirecting) {
191
            $this->template = null;
192
193
            return;
194
        }
195
196
        $this->assign('extraJs', $this->extraJs);
197
198
        if (!$this->hideAlerts) {
199
            // If we're actually displaying content, we want to add the session alerts here!
200
            $this->assign('alerts', SessionAlert::getAlerts());
201
            SessionAlert::clearAlerts();
202
        }
203
204
        $this->assign('htmlTitle', $this->htmlTitle);
205
    }
206
207
    /**
208
     * @return TokenManager
209
     */
210
    public function getTokenManager()
211
    {
212
        return $this->tokenManager;
213
    }
214
215
    /**
216
     * @param TokenManager $tokenManager
217
     */
218
    public function setTokenManager($tokenManager)
219
    {
220
        $this->tokenManager = $tokenManager;
221
    }
222
223
    /**
224
     * @return ContentSecurityPolicyManager
225
     */
226
    public function getCspManager(): ContentSecurityPolicyManager
227
    {
228
        return $this->cspManager;
229
    }
230
231
    /**
232
     * @param ContentSecurityPolicyManager $cspManager
233
     */
234
    public function setCspManager(ContentSecurityPolicyManager $cspManager): void
235
    {
236
        $this->cspManager = $cspManager;
237
    }
238
239
    /**
240
     * Skip the display of session alerts in this page
241
     */
242
    public function skipAlerts(): void
243
    {
244
        $this->hideAlerts = true;
245
    }
246
247
    /**
248
     * Sends the redirect headers to perform a GET at the destination page.
249
     *
250
     * Also nullifies the set template so Smarty does not render it.
251
     *
252
     * @param string      $page   The page to redirect requests to (as used in the UR)
253
     * @param null|string $action The action to use on the page.
254
     * @param null|array  $parameters
255
     * @param null|string $script The script (relative to index.php) to redirect to
256
     */
257
    final protected function redirect($page = '', $action = null, $parameters = null, $script = null)
258
    {
259
        $currentScriptName = WebRequest::scriptName();
260
261
        // Are we changing script?
262
        if ($script === null || substr($currentScriptName, -1 * count($script)) === $script) {
0 ignored issues
show
Bug introduced by
$script of type string is incompatible with the type Countable|array expected by parameter $value of count(). ( Ignorable by Annotation )

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

262
        if ($script === null || substr($currentScriptName, -1 * count(/** @scrutinizer ignore-type */ $script)) === $script) {
Loading history...
Bug introduced by
It seems like $currentScriptName can also be of type null; however, parameter $string of substr() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

262
        if ($script === null || substr(/** @scrutinizer ignore-type */ $currentScriptName, -1 * count($script)) === $script) {
Loading history...
263
            $targetScriptName = $currentScriptName;
264
        }
265
        else {
266
            $targetScriptName = $this->getSiteConfiguration()->getBaseUrl() . '/' . $script;
267
        }
268
269
        $pathInfo = array($targetScriptName);
270
271
        $pathInfo[1] = $page;
272
273
        if ($action !== null) {
274
            $pathInfo[2] = $action;
275
        }
276
277
        $url = implode('/', $pathInfo);
278
279
        if (is_array($parameters) && count($parameters) > 0) {
280
            $url .= '?' . http_build_query($parameters);
281
        }
282
283
        $this->redirectUrl($url);
284
    }
285
286
    /**
287
     * Sends the redirect headers to perform a GET at the new address.
288
     *
289
     * Also nullifies the set template so Smarty does not render it.
290
     *
291
     * @param string $path URL to redirect to
292
     */
293
    final protected function redirectUrl($path)
294
    {
295
        // 303 See Other = re-request at new address with a GET.
296
        $this->headerQueue[] = 'HTTP/1.1 303 See Other';
297
        $this->headerQueue[] = "Location: $path";
298
299
        $this->setTemplate(null);
300
        $this->isRedirecting = true;
301
    }
302
303
    /**
304
     * Sets the name of the template this page should display.
305
     *
306
     * @param string $name
307
     *
308
     * @throws Exception
309
     */
310
    final protected function setTemplate($name)
311
    {
312
        if ($this->isRedirecting) {
313
            throw new Exception('This page has been set as a redirect, no template can be displayed!');
314
        }
315
316
        $this->template = $name;
317
    }
318
319
    /**
320
     * Adds an extra JS file to to the page
321
     *
322
     * @param string $path The path (relative to the application root) of the file
323
     */
324
    final protected function addJs($path)
325
    {
326
        if (in_array($path, $this->extraJs)) {
327
            // nothing to do
328
            return;
329
        }
330
331
        $this->extraJs[] = $path;
332
    }
333
334
    /**
335
     * Main function for this page, when no specific actions are called.
336
     * @return void
337
     */
338
    abstract protected function main();
339
340
    /**
341
     * Takes a smarty template string and sets the HTML title to that value
342
     *
343
     * @param string $title
344
     *
345
     * @throws SmartyException
346
     */
347
    final protected function setHtmlTitle($title)
348
    {
349
        $this->htmlTitle = $this->smarty->fetch('string:' . $title);
350
    }
351
352
    public function execute()
353
    {
354
        if ($this->getRouteName() === null) {
0 ignored issues
show
introduced by
The condition $this->getRouteName() === null is always false.
Loading history...
355
            throw new Exception('Request is unrouted.');
356
        }
357
358
        if ($this->getSiteConfiguration() === null) {
359
            throw new Exception('Page has no configuration!');
360
        }
361
362
        $this->setupPage();
363
364
        $this->runPage();
365
    }
366
367
    public function assignCSRFToken()
368
    {
369
        $token = $this->tokenManager->getNewToken();
370
        $this->assign('csrfTokenData', $token->getTokenData());
371
    }
372
373
    public function validateCSRFToken()
374
    {
375
        if (!$this->tokenManager->validateToken(WebRequest::postString('csrfTokenData'))) {
376
            throw new ApplicationLogicException('Form token is not valid, please reload and try again');
377
        }
378
    }
379
380
    protected function sendResponseHeaders()
381
    {
382
        if (headers_sent()) {
383
            throw new ApplicationLogicException('Headers have already been sent! This is likely a bug in the application.');
384
        }
385
386
        // send the CSP headers now
387
        header($this->getCspManager()->getHeader());
388
389
        foreach ($this->headerQueue as $item) {
390
            if (mb_strpos($item, "\r") !== false || mb_strpos($item, "\n") !== false) {
391
                // Oops. We're not allowed to do this.
392
                throw new Exception('Unable to split header');
393
            }
394
395
            header($item);
396
        }
397
    }
398
}
399