Failed Conditions
Push — newinternal-releasecandidate ( 2e1778...b14046 )
by Simon
15:26 queued 05:35
created

PageBase::skipAlerts()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

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