Completed
Branch newinternal (104de7)
by Simon
10:16
created

PageBase::sendResponseHeaders()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 11
rs 9.2
cc 4
eloc 5
nc 3
nop 0
1
<?php
2
3
namespace Waca\Tasks;
4
5
use Exception;
6
use Waca\DataObjects\SiteNotice;
7
use Waca\DataObjects\User;
8
use Waca\Exceptions\ApplicationLogicException;
9
use Waca\Exceptions\OptimisticLockFailedException;
10
use Waca\Fragments\TemplateOutput;
11
use Waca\Security\TokenManager;
12
use Waca\SessionAlert;
13
use Waca\WebRequest;
14
15
abstract class PageBase extends TaskBase implements IRoutedTask
16
{
17
	use TemplateOutput;
18
	/** @var string Smarty template to display */
19
	protected $template = "base.tpl";
20
	/** @var string HTML title. Currently unused. */
21
	protected $htmlTitle;
22
	/** @var bool Determines if the page is a redirect or not */
23
	protected $isRedirecting = false;
24
	/** @var array Queue of headers to be sent on successful completion */
25
	protected $headerQueue = array();
26
	/** @var string The name of the route to use, as determined by the request router. */
27
	private $routeName = null;
28
	/** @var TokenManager */
29
	protected $tokenManager;
30
31
	/**
32
	 * Sets the route the request will take. Only should be called from the request router.
33
	 *
34
	 * @param $routeName string
35
	 *
36
	 * @throws Exception
37
	 * @category Security-Critical
38
	 */
39
	final public function setRoute($routeName)
40
	{
41
		// Test the new route is callable before adopting it.
42
		if (!is_callable(array($this, $routeName))) {
43
			throw new Exception("Proposed route '$routeName' is not callable.");
44
		}
45
46
		// Adopt the new route
47
		$this->routeName = $routeName;
48
	}
49
50
	/**
51
	 * Gets the name of the route that has been passed from the request router.
52
	 * @return string
53
	 */
54
	final public function getRouteName()
55
	{
56
		return $this->routeName;
57
	}
58
59
	/**
60
	 * Performs generic page setup actions
61
	 */
62
	final protected function setupPage()
63
	{
64
		$this->setUpSmarty();
65
66
		$siteNoticeText = SiteNotice::get($this->getDatabase());
67
68
		$this->assign('siteNoticeText', $siteNoticeText);
69
70
		$currentUser = User::getCurrent($this->getDatabase());
71
		$this->assign('currentUser', $currentUser);
72
		$this->assign('loggedIn', (!$currentUser->isCommunityUser()));
73
	}
74
75
	/**
76
	 * Runs the page logic as routed by the RequestRouter
77
	 *
78
	 * Only should be called after a security barrier! That means only from execute().
79
	 */
80
	final protected function runPage()
81
	{
82
		$database = $this->getDatabase();
83
84
		// initialise a database transaction
85
		if (!$database->beginTransaction()) {
86
			throw new Exception('Failed to start transaction on primary database.');
87
		}
88
89
		try {
90
			// run the page code
91
			$this->{$this->getRouteName()}();
92
93
			$database->commit();
94
		}
95
		catch (ApplicationLogicException $ex) {
96
			// it's an application logic exception, so nothing went seriously wrong with the site. We can use the
97
			// standard templating system for this.
98
99
			// Firstly, let's undo anything that happened to the database.
100
			$database->rollBack();
101
102
			// Reset smarty
103
			$this->setUpSmarty();
104
105
			// Set the template
106
			$this->setTemplate('exception/application-logic.tpl');
107
			$this->assign('message', $ex->getMessage());
108
109
			// Force this back to false
110
			$this->isRedirecting = false;
111
			$this->headerQueue = array();
112
		}
113
		catch (OptimisticLockFailedException $ex) {
114
			// it's an optimistic lock failure 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->setUpSmarty();
122
123
			// Set the template
124
			$this->setTemplate('exception/optimistic-lock-failure.tpl');
125
			$this->assign('message', $ex->getMessage());
126
127
			// Force this back to false
128
			$this->isRedirecting = false;
129
			$this->headerQueue = array();
130
		}
131
		finally {
132
			// Catch any hanging on transactions
133
			if ($database->hasActiveTransaction()) {
134
				$database->rollBack();
135
			}
136
		}
137
138
		// run any finalisation code needed before we send the output to the browser.
139
		$this->finalisePage();
140
141
		// Send the headers
142
		$this->sendResponseHeaders();
143
144
		// Check we have a template to use!
145
		if ($this->template !== null) {
146
			$content = $this->fetchTemplate($this->template);
147
			ob_clean();
148
			print($content);
149
			ob_flush();
150
151
			return;
152
		}
153
	}
154
155
	/**
156
	 * Performs final tasks needed before rendering the page.
157
	 */
158
	protected function finalisePage()
159
	{
160
		if ($this->isRedirecting) {
161
			$this->template = null;
162
163
			return;
164
		}
165
166
		// as new users can actually stay logged in
167
		if (User::getCurrent($this->getDatabase())->isNew()) {
168
			$registeredSuccessfully = new SessionAlert(
169
				'Your request will be reviewed soon by a tool administrator, and you\'ll get an email informing you of the decision. You won\'t be able to access most of the tool until then.',
170
				'Account Requested!', 'alert-success', false);
171
			SessionAlert::append($registeredSuccessfully);
172
		}
173
174
		// If we're actually displaying content, we want to add the session alerts here!
175
		$this->assign('alerts', SessionAlert::getAlerts());
176
		SessionAlert::clearAlerts();
177
178
		$this->assign('htmlTitle', $this->htmlTitle);
179
	}
180
181
	/**
182
	 * @return TokenManager
183
	 */
184
	public function getTokenManager()
185
	{
186
		return $this->tokenManager;
187
	}
188
189
	/**
190
	 * @param TokenManager $tokenManager
191
	 */
192
	public function setTokenManager($tokenManager)
193
	{
194
		$this->tokenManager = $tokenManager;
195
	}
196
197
	/**
198
	 * Sends the redirect headers to perform a GET at the destination page.
199
	 *
200
	 * Also nullifies the set template so Smarty does not render it.
201
	 *
202
	 * @param string      $page   The page to redirect requests to (as used in the UR)
203
	 * @param null|string $action The action to use on the page.
204
	 * @param null|array  $parameters
205
	 */
206
	final protected function redirect($page = '', $action = null, $parameters = null)
207
	{
208
		$pathInfo = array(WebRequest::scriptName());
209
210
		$pathInfo[1] = $page;
211
212
		if ($action !== null) {
213
			$pathInfo[2] = $action;
214
		}
215
216
		$url = implode('/', $pathInfo);
217
218
		if (is_array($parameters) && count($parameters) > 0) {
219
			$url .= '?' . http_build_query($parameters);
220
		}
221
222
		$this->redirectUrl($url);
223
	}
224
225
	/**
226
	 * Sends the redirect headers to perform a GET at the new address.
227
	 *
228
	 * Also nullifies the set template so Smarty does not render it.
229
	 *
230
	 * @param string $path URL to redirect to
231
	 */
232
	final protected function redirectUrl($path)
233
	{
234
		// 303 See Other = re-request at new address with a GET.
235
		$this->headerQueue[] = 'HTTP/1.1 303 See Other';
236
		$this->headerQueue[] = "Location: $path";
237
238
		$this->setTemplate(null);
239
		$this->isRedirecting = true;
240
	}
241
242
	/**
243
	 * Sets the name of the template this page should display.
244
	 *
245
	 * @param string $name
246
	 *
247
	 * @throws Exception
248
	 */
249
	final protected function setTemplate($name)
250
	{
251
		if ($this->isRedirecting) {
252
			throw new Exception('This page has been set as a redirect, no template can be displayed!');
253
		}
254
255
		$this->template = $name;
256
	}
257
258
	/**
259
	 * Main function for this page, when no specific actions are called.
260
	 * @return void
261
	 */
262
	abstract protected function main();
263
264
	/**
265
	 * @param string $title
266
	 */
267
	final protected function setHtmlTitle($title)
268
	{
269
		$this->htmlTitle = $title;
270
	}
271
272
	public function execute()
273
	{
274
		if ($this->getRouteName() === null) {
275
			throw new Exception('Request is unrouted.');
276
		}
277
278
		if ($this->getSiteConfiguration() === null) {
279
			throw new Exception('Page has no configuration!');
280
		}
281
282
		$this->setupPage();
283
284
		$this->runPage();
285
	}
286
287
	public function assignCSRFToken()
288
	{
289
		$token = $this->tokenManager->getNewToken();
290
		$this->assign('csrfTokenData', $token->getTokenData());
291
	}
292
293
	public function validateCSRFToken()
294
	{
295
		if (!$this->tokenManager->validateToken(WebRequest::postString('csrfTokenData'))) {
296
			throw new ApplicationLogicException('Form token is not valid, please reload and try again');
297
		}
298
	}
299
300
	protected function sendResponseHeaders()
301
	{
302
		foreach ($this->headerQueue as $item) {
303
			if(mb_strpos($item, "\r") !== false || mb_strpos($item, "\n") !== false){
304
				// Oops. We're not allowed to do this.
305
				throw new Exception('Unable to split header');
306
			}
307
308
			header($item);
309
		}
310
	}
311
}