Failed Conditions
Push — dependabot/composer/smarty/sma... ( 7af6bd )
by
unknown
21:22 queued 16:09
created

includes/Tasks/PageBase.php (1 issue)

Labels
Severity
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;
0 ignored issues
show
The type SmartyException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
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\Helpers\PreferenceManager;
20
use Waca\Security\ContentSecurityPolicyManager;
21
use Waca\Security\TokenManager;
22
use Waca\SessionAlert;
23
use Waca\WebRequest;
24
25
abstract class PageBase extends TaskBase implements IRoutedTask
26
{
27
    use TemplateOutput;
28
    /** @var string Smarty template to display */
29
    protected $template = "base.tpl";
30
    /** @var string HTML title. Currently unused. */
31
    protected $htmlTitle;
32
    /** @var bool Determines if the page is a redirect or not */
33
    protected $isRedirecting = false;
34
    /** @var array Queue of headers to be sent on successful completion */
35
    protected $headerQueue = array();
36
    /** @var string The name of the route to use, as determined by the request router. */
37
    private $routeName = null;
38
    /** @var TokenManager */
39
    protected $tokenManager;
40
    /** @var ContentSecurityPolicyManager */
41
    private $cspManager;
42
    /** @var string[] Extra JS files to include */
43
    private $extraJs = array();
44
    /** @var bool Don't show (and hence clear) session alerts when this page is displayed  */
45
    private $hideAlerts = false;
46
47
    /**
48
     * Sets the route the request will take. Only should be called from the request router or barrier test.
49
     *
50
     * @param string $routeName        The name of the route
51
     * @param bool   $skipCallableTest Don't use this unless you know what you're doing, and what the implications are.
52
     *
53
     * @throws Exception
54
     * @category Security-Critical
55
     */
56
    final public function setRoute($routeName, $skipCallableTest = false)
57
    {
58
        // Test the new route is callable before adopting it.
59
        if (!$skipCallableTest && !is_callable(array($this, $routeName))) {
60
            throw new Exception("Proposed route '$routeName' is not callable.");
61
        }
62
63
        // Adopt the new route
64
        $this->routeName = $routeName;
65
    }
66
67
    /**
68
     * Gets the name of the route that has been passed from the request router.
69
     * @return string
70
     */
71
    final public function getRouteName()
72
    {
73
        return $this->routeName;
74
    }
75
76
    /**
77
     * Performs generic page setup actions
78
     */
79
    final protected function setupPage()
80
    {
81
        $this->setUpSmarty();
82
83
        $database = $this->getDatabase();
84
        $currentUser = User::getCurrent($database);
85
        $this->assign('currentUser', $currentUser);
86
        $this->assign('skin', PreferenceManager::getForCurrent($database)->getPreference(PreferenceManager::PREF_SKIN));
87
        $this->assign('currentDomain', Domain::getCurrent($database));
88
        $this->assign('loggedIn', (!$currentUser->isCommunityUser()));
89
    }
90
91
    /**
92
     * Runs the page logic as routed by the RequestRouter
93
     *
94
     * Only should be called after a security barrier! That means only from execute().
95
     */
96
    final protected function runPage()
97
    {
98
        $database = $this->getDatabase();
99
100
        // initialise a database transaction
101
        if (!$database->beginTransaction()) {
102
            throw new Exception('Failed to start transaction on primary database.');
103
        }
104
105
        try {
106
            // run the page code
107
            $this->{$this->getRouteName()}();
108
109
            $database->commit();
110
        }
111
        /** @noinspection PhpRedundantCatchClauseInspection */
112
        catch (ApplicationLogicException $ex) {
113
            // it's an application logic exception, so nothing went seriously wrong with the site. We can use the
114
            // standard templating system for this.
115
116
            // Firstly, let's undo anything that happened to the database.
117
            $database->rollBack();
118
119
            // Reset smarty
120
            $this->setupPage();
121
122
            $this->skipAlerts();
123
124
            // Set the template
125
            $this->setTemplate('exception/application-logic.tpl');
126
            $this->assign('message', $ex->getMessage());
127
128
            // Force this back to false
129
            $this->isRedirecting = false;
130
            $this->headerQueue = array();
131
        }
132
        /** @noinspection PhpRedundantCatchClauseInspection */
133
        catch (OptimisticLockFailedException $ex) {
134
            // it's an optimistic lock failure exception, so nothing went seriously wrong with the site. We can use the
135
            // standard templating system for this.
136
137
            // Firstly, let's undo anything that happened to the database.
138
            $database->rollBack();
139
140
            // Reset smarty
141
            $this->setupPage();
142
143
            // Set the template
144
            $this->skipAlerts();
145
            $this->setTemplate('exception/optimistic-lock-failure.tpl');
146
            $this->assign('message', $ex->getMessage());
147
148
            $this->assign('debugTrace', false);
149
150
            if ($this->getSiteConfiguration()->getDebuggingTraceEnabled()) {
151
                ob_start();
152
                var_dump(ExceptionHandler::getExceptionData($ex));
153
                $textErrorData = ob_get_contents();
154
                ob_end_clean();
155
156
                $this->assign('exceptionData', $textErrorData);
157
                $this->assign('debugTrace', true);
158
            }
159
160
            // Force this back to false
161
            $this->isRedirecting = false;
162
            $this->headerQueue = array();
163
        }
164
        finally {
165
            // Catch any hanging on transactions
166
            if ($database->hasActiveTransaction()) {
167
                $database->rollBack();
168
            }
169
        }
170
171
        // run any finalisation code needed before we send the output to the browser.
172
        $this->finalisePage();
173
174
        // Send the headers
175
        $this->sendResponseHeaders();
176
177
        // Check we have a template to use!
178
        if ($this->template !== null) {
179
            $content = $this->fetchTemplate($this->template);
180
            ob_clean();
181
            print($content);
182
            ob_flush();
183
184
            return;
185
        }
186
    }
187
188
    /**
189
     * Performs final tasks needed before rendering the page.
190
     */
191
    protected function finalisePage()
192
    {
193
        if ($this->isRedirecting) {
194
            $this->template = null;
195
196
            return;
197
        }
198
199
        $this->assign('extraJs', $this->extraJs);
200
201
        if (!$this->hideAlerts) {
202
            // If we're actually displaying content, we want to add the session alerts here!
203
            $this->assign('alerts', SessionAlert::getAlerts());
204
            SessionAlert::clearAlerts();
205
        }
206
207
        $this->assign('htmlTitle', $this->htmlTitle);
208
    }
209
210
    /**
211
     * @return TokenManager
212
     */
213
    public function getTokenManager()
214
    {
215
        return $this->tokenManager;
216
    }
217
218
    /**
219
     * @param TokenManager $tokenManager
220
     */
221
    public function setTokenManager($tokenManager)
222
    {
223
        $this->tokenManager = $tokenManager;
224
    }
225
226
    /**
227
     * @return ContentSecurityPolicyManager
228
     */
229
    public function getCspManager(): ContentSecurityPolicyManager
230
    {
231
        return $this->cspManager;
232
    }
233
234
    /**
235
     * @param ContentSecurityPolicyManager $cspManager
236
     */
237
    public function setCspManager(ContentSecurityPolicyManager $cspManager): void
238
    {
239
        $this->cspManager = $cspManager;
240
    }
241
242
    /**
243
     * Skip the display of session alerts in this page
244
     */
245
    public function skipAlerts(): void
246
    {
247
        $this->hideAlerts = true;
248
    }
249
250
    /**
251
     * Sends the redirect headers to perform a GET at the destination page.
252
     *
253
     * Also nullifies the set template so Smarty does not render it.
254
     *
255
     * @param string      $page   The page to redirect requests to (as used in the UR)
256
     * @param null|string $action The action to use on the page.
257
     * @param null|array  $parameters
258
     * @param null|string $script The script (relative to index.php) to redirect to
259
     */
260
    final protected function redirect($page = '', $action = null, $parameters = null, $script = null)
261
    {
262
        $currentScriptName = WebRequest::scriptName();
263
264
        // Are we changing script?
265
        if ($script === null || substr($currentScriptName, -1 * count($script)) === $script) {
266
            $targetScriptName = $currentScriptName;
267
        }
268
        else {
269
            $targetScriptName = $this->getSiteConfiguration()->getBaseUrl() . '/' . $script;
270
        }
271
272
        $pathInfo = array($targetScriptName);
273
274
        $pathInfo[1] = $page;
275
276
        if ($action !== null) {
277
            $pathInfo[2] = $action;
278
        }
279
280
        $url = implode('/', $pathInfo);
281
282
        if (is_array($parameters) && count($parameters) > 0) {
283
            $url .= '?' . http_build_query($parameters);
284
        }
285
286
        $this->redirectUrl($url);
287
    }
288
289
    /**
290
     * Sends the redirect headers to perform a GET at the new address.
291
     *
292
     * Also nullifies the set template so Smarty does not render it.
293
     *
294
     * @param string $path URL to redirect to
295
     */
296
    final protected function redirectUrl($path)
297
    {
298
        // 303 See Other = re-request at new address with a GET.
299
        $this->headerQueue[] = 'HTTP/1.1 303 See Other';
300
        $this->headerQueue[] = "Location: $path";
301
302
        $this->setTemplate(null);
303
        $this->isRedirecting = true;
304
    }
305
306
    /**
307
     * Sets the name of the template this page should display.
308
     *
309
     * @param string $name
310
     *
311
     * @throws Exception
312
     */
313
    final protected function setTemplate($name)
314
    {
315
        if ($this->isRedirecting) {
316
            throw new Exception('This page has been set as a redirect, no template can be displayed!');
317
        }
318
319
        $this->template = $name;
320
    }
321
322
    /**
323
     * Adds an extra JS file to to the page
324
     *
325
     * @param string $path The path (relative to the application root) of the file
326
     */
327
    final protected function addJs($path)
328
    {
329
        if (in_array($path, $this->extraJs)) {
330
            // nothing to do
331
            return;
332
        }
333
334
        $this->extraJs[] = $path;
335
    }
336
337
    /**
338
     * Main function for this page, when no specific actions are called.
339
     * @return void
340
     */
341
    abstract protected function main();
342
343
    /**
344
     * Takes a smarty template string and sets the HTML title to that value
345
     *
346
     * @param string $title
347
     *
348
     * @throws SmartyException
349
     */
350
    final protected function setHtmlTitle($title)
351
    {
352
        $this->htmlTitle = $this->smarty->fetch('string:' . $title);
353
    }
354
355
    public function execute()
356
    {
357
        if ($this->getRouteName() === null) {
358
            throw new Exception('Request is unrouted.');
359
        }
360
361
        if ($this->getSiteConfiguration() === null) {
362
            throw new Exception('Page has no configuration!');
363
        }
364
365
        $this->setupPage();
366
367
        $this->runPage();
368
    }
369
370
    public function assignCSRFToken()
371
    {
372
        $token = $this->tokenManager->getNewToken();
373
        $this->assign('csrfTokenData', $token->getTokenData());
374
    }
375
376
    public function validateCSRFToken()
377
    {
378
        if (!$this->tokenManager->validateToken(WebRequest::postString('csrfTokenData'))) {
379
            throw new ApplicationLogicException('Form token is not valid, please reload and try again');
380
        }
381
    }
382
383
    protected function sendResponseHeaders()
384
    {
385
        if (headers_sent()) {
386
            throw new ApplicationLogicException('Headers have already been sent! This is likely a bug in the application.');
387
        }
388
389
        // send the CSP headers now
390
        header($this->getCspManager()->getHeader());
391
392
        foreach ($this->headerQueue as $item) {
393
            if (mb_strpos($item, "\r") !== false || mb_strpos($item, "\n") !== false) {
394
                // Oops. We're not allowed to do this.
395
                throw new Exception('Unable to split header');
396
            }
397
398
            header($item);
399
        }
400
    }
401
}
402