|
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
|
|
|
final public function setRoute($routeName, $skipCallableTest = false) |
|
|
|
|
|
|
51
|
|
|
{ |
|
52
|
|
|
// Test the new route is callable before adopting it. |
|
53
|
|
|
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
|
|
|
$this->routeName = $routeName; |
|
59
|
|
|
} |
|
|
|
|
|
|
60
|
|
|
|
|
61
|
|
|
/** |
|
62
|
|
|
* Gets the name of the route that has been passed from the request router. |
|
63
|
|
|
* @return string |
|
64
|
|
|
*/ |
|
65
|
|
|
final public function getRouteName() |
|
66
|
|
|
{ |
|
67
|
|
|
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.'); |
|
|
|
|
|
|
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
|
|
|
|
PHP provides two ways to mark string literals. Either with single quotes
'literal'or with double quotes"literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (
\') and the backslash (\\). Every other character is displayed as is.Double quoted string literals may contain other variables or more complex escape sequences.
will print an indented:
Single is ValueIf your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.
For more information on PHP string literals and available escape sequences see the PHP core documentation.