Completed
Pull Request — newinternal (#285)
by Simon
06:16 queued 03:05
created

PageBase::redirect()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 28
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

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

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
349
        }
350
351
        foreach ($this->headerQueue as $item) {
352
            if (mb_strpos($item, "\r") !== false || mb_strpos($item, "\n") !== false) {
353
                // Oops. We're not allowed to do this.
354
                throw new Exception('Unable to split header');
355
            }
356
357
            header($item);
358
        }
359
    }
360
}
361