Failed Conditions
Push — newinternal ( 45827b...a30d14 )
by Simon
24:54 queued 14:54
created

PageBase   A

Complexity

Total Complexity 41

Size/Duplication

Total Lines 353
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
wmc 41
eloc 109
c 5
b 0
f 0
dl 0
loc 353
ccs 0
cts 165
cp 0
rs 9.1199

18 Methods

Rating   Name   Duplication   Size   Complexity  
A setupPage() 0 7 1
A setTemplate() 0 7 2
A setHtmlTitle() 0 3 1
A setRoute() 0 9 3
A finalisePage() 0 15 2
B runPage() 0 84 7
A setTokenManager() 0 3 1
A setCspManager() 0 3 1
A addJs() 0 7 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 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\SiteNotice;
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
44
    /**
45
     * Sets the route the request will take. Only should be called from the request router or barrier test.
46
     *
47
     * @param string $routeName        The name of the route
48
     * @param bool   $skipCallableTest Don't use this unless you know what you're doing, and what the implications are.
49
     *
50
     * @throws Exception
51
     * @category Security-Critical
52
     */
53
    final public function setRoute($routeName, $skipCallableTest = false)
54
    {
55
        // Test the new route is callable before adopting it.
56
        if (!$skipCallableTest && !is_callable(array($this, $routeName))) {
57
            throw new Exception("Proposed route '$routeName' is not callable.");
58
        }
59
60
        // Adopt the new route
61
        $this->routeName = $routeName;
62
    }
63
64
    /**
65
     * Gets the name of the route that has been passed from the request router.
66
     * @return string
67
     */
68
    final public function getRouteName()
69
    {
70
        return $this->routeName;
71
    }
72
73
    /**
74
     * Performs generic page setup actions
75
     */
76
    final protected function setupPage()
77
    {
78
        $this->setUpSmarty();
79
80
        $currentUser = User::getCurrent($this->getDatabase());
81
        $this->assign('currentUser', $currentUser);
82
        $this->assign('loggedIn', (!$currentUser->isCommunityUser()));
83
    }
84
85
    /**
86
     * Runs the page logic as routed by the RequestRouter
87
     *
88
     * Only should be called after a security barrier! That means only from execute().
89
     */
90
    final protected function runPage()
91
    {
92
        $database = $this->getDatabase();
93
94
        // initialise a database transaction
95
        if (!$database->beginTransaction()) {
96
            throw new Exception('Failed to start transaction on primary database.');
97
        }
98
99
        try {
100
            // run the page code
101
            $this->{$this->getRouteName()}();
102
103
            $database->commit();
104
        }
105
        catch (ApplicationLogicException $ex) {
106
            // it's an application logic exception, so nothing went seriously wrong with the site. We can use the
107
            // standard templating system for this.
108
109
            // Firstly, let's undo anything that happened to the database.
110
            $database->rollBack();
111
112
            // Reset smarty
113
            $this->setupPage();
114
115
            // Set the template
116
            $this->setTemplate('exception/application-logic.tpl');
117
            $this->assign('message', $ex->getMessage());
118
119
            // Force this back to false
120
            $this->isRedirecting = false;
121
            $this->headerQueue = array();
122
        }
123
        catch (OptimisticLockFailedException $ex) {
124
            // it's an optimistic lock failure exception, so nothing went seriously wrong with the site. We can use the
125
            // standard templating system for this.
126
127
            // Firstly, let's undo anything that happened to the database.
128
            $database->rollBack();
129
130
            // Reset smarty
131
            $this->setupPage();
132
133
            // Set the template
134
            $this->setTemplate('exception/optimistic-lock-failure.tpl');
135
            $this->assign('message', $ex->getMessage());
136
137
            $this->assign('debugTrace', false);
138
139
            if ($this->getSiteConfiguration()->getDebuggingTraceEnabled()) {
140
                ob_start();
141
                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...
142
                $textErrorData = ob_get_contents();
143
                ob_end_clean();
144
145
                $this->assign('exceptionData', $textErrorData);
146
                $this->assign('debugTrace', true);
147
            }
148
149
            // Force this back to false
150
            $this->isRedirecting = false;
151
            $this->headerQueue = array();
152
        }
153
        finally {
154
            // Catch any hanging on transactions
155
            if ($database->hasActiveTransaction()) {
156
                $database->rollBack();
157
            }
158
        }
159
160
        // run any finalisation code needed before we send the output to the browser.
161
        $this->finalisePage();
162
163
        // Send the headers
164
        $this->sendResponseHeaders();
165
166
        // Check we have a template to use!
167
        if ($this->template !== null) {
168
            $content = $this->fetchTemplate($this->template);
169
            ob_clean();
170
            print($content);
171
            ob_flush();
172
173
            return;
174
        }
175
    }
176
177
    /**
178
     * Performs final tasks needed before rendering the page.
179
     */
180
    protected function finalisePage()
181
    {
182
        if ($this->isRedirecting) {
183
            $this->template = null;
184
185
            return;
186
        }
187
188
        $this->assign('extraJs', $this->extraJs);
189
190
        // If we're actually displaying content, we want to add the session alerts here!
191
        $this->assign('alerts', SessionAlert::getAlerts());
192
        SessionAlert::clearAlerts();
193
194
        $this->assign('htmlTitle', $this->htmlTitle);
195
    }
196
197
    /**
198
     * @return TokenManager
199
     */
200
    public function getTokenManager()
201
    {
202
        return $this->tokenManager;
203
    }
204
205
    /**
206
     * @param TokenManager $tokenManager
207
     */
208
    public function setTokenManager($tokenManager)
209
    {
210
        $this->tokenManager = $tokenManager;
211
    }
212
213
    /**
214
     * @return ContentSecurityPolicyManager
215
     */
216
    public function getCspManager(): ContentSecurityPolicyManager
217
    {
218
        return $this->cspManager;
219
    }
220
221
    /**
222
     * @param ContentSecurityPolicyManager $cspManager
223
     */
224
    public function setCspManager(ContentSecurityPolicyManager $cspManager): void
225
    {
226
        $this->cspManager = $cspManager;
227
    }
228
229
    /**
230
     * Sends the redirect headers to perform a GET at the destination page.
231
     *
232
     * Also nullifies the set template so Smarty does not render it.
233
     *
234
     * @param string      $page   The page to redirect requests to (as used in the UR)
235
     * @param null|string $action The action to use on the page.
236
     * @param null|array  $parameters
237
     * @param null|string $script The script (relative to index.php) to redirect to
238
     */
239
    final protected function redirect($page = '', $action = null, $parameters = null, $script = null)
240
    {
241
        $currentScriptName = WebRequest::scriptName();
242
243
        // Are we changing script?
244
        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 $var 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

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