Issues (186)

Branch: master

includes/Tasks/PageBase.php (3 issues)

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

266
        if ($script === null || substr(/** @scrutinizer ignore-type */ $currentScriptName, -1 * count($script)) === $script) {
Loading history...
$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

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