Failed Conditions
Push — newinternal ( b66232...216d62 )
by Simon
16:33 queued 06:35
created

includes/Tasks/PageBase.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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