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

WebStart::setupHelpers()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 31
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 7
Bugs 1 Features 0
Metric Value
c 7
b 1
f 0
dl 0
loc 31
rs 8.5806
cc 4
eloc 20
nc 4
nop 4
1
<?php
2
namespace Waca;
3
4
use ErrorException;
5
use Exception;
6
use Waca\DataObjects\User;
7
use Waca\Exceptions\EnvironmentException;
8
use Waca\Exceptions\ReadableException;
9
use Waca\Helpers\BlacklistHelper;
10
use Waca\Helpers\FakeBlacklistHelper;
11
use Waca\Helpers\TypeAheadHelper;
12
use Waca\Providers\GlobalStateProvider;
13
use Waca\Router\IRequestRouter;
14
use Waca\Security\SecurityManager;
15
use Waca\Security\TokenManager;
16
use Waca\Tasks\ITask;
17
use Waca\Tasks\InternalPageBase;
18
use Waca\Tasks\PageBase;
19
20
/**
21
 * Internal application entry point.
22
 *
23
 * @package Waca
24
 */
25
class WebStart extends ApplicationBase
26
{
27
	/**
28
	 * @var IRequestRouter
29
	 */
30
	private $requestRouter;
31
	/** @var bool */
32
	private $isPublic;
33
34
	/**
35
	 * WebStart constructor.
36
	 *
37
	 * @param SiteConfiguration $configuration The site configuration
38
	 * @param IRequestRouter    $router        The request router to use
39
	 */
40
	public function __construct(SiteConfiguration $configuration, IRequestRouter $router)
41
	{
42
		parent::__construct($configuration);
43
44
		$this->requestRouter = $router;
45
	}
46
47
	/**
48
	 * @param ITask             $page
49
	 * @param SiteConfiguration $siteConfiguration
50
	 * @param PdoDatabase       $database
51
	 * @param PdoDatabase       $notificationsDatabase
52
	 *
53
	 * @return void
54
	 */
55
	protected function setupHelpers(
56
		ITask $page,
57
		SiteConfiguration $siteConfiguration,
58
		PdoDatabase $database,
59
		PdoDatabase $notificationsDatabase
60
	) {
61
		parent::setupHelpers($page, $siteConfiguration, $database, $notificationsDatabase);
62
63
		if ($page instanceof PageBase) {
64
			$page->setTokenManager(new TokenManager());
65
66
			if ($page instanceof InternalPageBase) {
67
				$page->setTypeAheadHelper(new TypeAheadHelper());
68
69
				$identificationVerifier = new IdentificationVerifier($page->getHttpHelper(), $siteConfiguration,
70
					$database);
71
				$page->setIdentificationVerifier($identificationVerifier);
72
73
				$page->setSecurityManager(new SecurityManager($identificationVerifier,
74
					$siteConfiguration->getForceIdentification()));
75
76
				if ($siteConfiguration->getTitleBlacklistEnabled()) {
77
					$page->setBlacklistHelper(new FakeBlacklistHelper());
78
				}
79
				else {
80
					$page->setBlacklistHelper(new BlacklistHelper($page->getHttpHelper(),
81
						$siteConfiguration->getMediawikiWebServiceEndpoint()));
82
				}
83
			}
84
		}
85
	}
86
87
	/**
88
	 * Application entry point.
89
	 *
90
	 * Sets up the environment and runs the application, performing any global cleanup operations when done.
91
	 */
92
	public function run()
93
	{
94
		try {
95
			if ($this->setupEnvironment()) {
96
				$this->main();
97
			}
98
		}
99
		catch (EnvironmentException $ex) {
100
			ob_end_clean();
101
			print Offline::getOfflineMessage(false, $ex->getMessage());
102
		}
103
		catch (ReadableException $ex) {
104
			ob_end_clean();
105
			print $ex->getReadableError();
106
		}
107
		finally {
108
			$this->cleanupEnvironment();
109
		}
110
	}
111
112
	/**
113
	 * Global exception handler
114
	 *
115
	 * Smarty would be nice to use, but it COULD BE smarty that throws the errors.
116
	 * Let's build something ourselves, and hope it works.
117
	 *
118
	 * @param $exception
119
	 *
120
	 * @category Security-Critical - has the potential to leak data when exception is thrown.
121
	 */
122
	public static function exceptionHandler(Exception $exception)
0 ignored issues
show
Coding Style introduced by
exceptionHandler uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
Coding Style introduced by
exceptionHandler uses the super-global variable $_GET which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
Coding Style introduced by
exceptionHandler uses the super-global variable $_POST which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
123
	{
124
		/** @global $siteConfiguration SiteConfiguration */
125
		global $siteConfiguration;
126
127
		$errorDocument = <<<HTML
128
<!DOCTYPE html>
129
<html lang="en"><head>
130
<meta charset="utf-8">
131
<title>Oops! Something went wrong!</title>
132
<meta name="viewport" content="width=device-width, initial-scale=1.0">
133
<link href="{$siteConfiguration->getBaseUrl()}/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet">
134
<style>
135
  body {
136
    padding-top: 60px;
137
  }
138
</style>
139
<link href="{$siteConfiguration->getBaseUrl()}/lib/bootstrap/css/bootstrap-responsive.min.css" rel="stylesheet">
140
</head><body><div class="container">
141
<h1>Oops! Something went wrong!</h1>
142
<p>We'll work on fixing this for you, so why not come back later?</p>
143
<p class="muted">If our trained monkeys ask, tell them this error ID: <code>$1$</code></p>
144
$2$
145
</div></body></html>
146
HTML;
147
148
		$errorData = self::getExceptionData($exception);
149
		$errorData['server'] = $_SERVER;
150
		$errorData['get'] = $_GET;
151
		$errorData['post'] = $_POST;
152
153
		$state = serialize($errorData);
154
		$errorId = sha1($state);
155
156
		// Save the error for later analysis
157
		file_put_contents($siteConfiguration->getErrorLog() . '/' . $errorId . '.log', $state);
158
159
		// clear and discard any content that's been saved to the output buffer
160
		if (ob_get_level() > 0) {
161
			ob_end_clean();
162
		}
163
164
		// push error ID into the document.
165
		$message = str_replace('$1$', $errorId, $errorDocument);
166
167
		if ($siteConfiguration->getDebuggingTraceEnabled()) {
168
			ob_start();
169
			var_dump($errorData);
1 ignored issue
show
Security Debugging Code introduced by
var_dump($errorData); looks like debug code. Are you sure you do not want to remove it? This might expose sensitive data.
Loading history...
170
			$textErrorData = ob_get_contents();
171
			ob_end_clean();
172
173
			$message = str_replace('$2$', $textErrorData, $message);
174
		}
175
		else {
176
			$message = str_replace('$2$', "", $message);
177
		}
178
179
		header('HTTP/1.1 500 Internal Server Error');
180
181
		// output the document
182
		print $message;
183
	}
184
185
	public static function errorHandler($err_severity, $err_msg, $err_file, $err_line)
186
	{
187
		// call into the main exception handler above
188
		throw new ErrorException($err_msg, 0, $err_severity, $err_file, $err_line);
189
	}
190
191
	/**
192
	 * Environment setup
193
	 *
194
	 * This method initialises the tool environment. If the tool cannot be initialised correctly, it will return false
195
	 * and shut down prematurely.
196
	 *
197
	 * @return bool
198
	 * @throws EnvironmentException
199
	 */
200
	protected function setupEnvironment()
201
	{
202
		// initialise global exception handler
203
		set_exception_handler(array(self::class, 'exceptionHandler'));
204
		set_error_handler(array(self::class, 'errorHandler'), E_RECOVERABLE_ERROR);
205
206
		// start output buffering if necessary
207
		if (ob_get_level() === 0) {
208
			ob_start();
209
		}
210
211
		// initialise super-global providers
212
		WebRequest::setGlobalStateProvider(new GlobalStateProvider());
213
214
		if (Offline::isOffline()) {
215
			print Offline::getOfflineMessage($this->isPublic());
216
			ob_end_flush();
217
218
			return false;
219
		}
220
221
		// Call parent setup
222
		if (!parent::setupEnvironment()) {
223
			return false;
224
		}
225
226
		// Start up sessions
227
		Session::start();
228
229
		// Check the user is allowed to be logged in still. This must be before we call any user-loading functions and
230
		// get the current user cached.
231
		// I'm not sure if this function call being here is particularly a good thing, but it's part of starting up a
232
		// session I suppose.
233
		$this->checkForceLogout();
234
235
		// environment initialised!
236
		return true;
237
	}
238
239
	/**
240
	 * Main application logic
241
	 */
242
	protected function main()
243
	{
244
		// Get the right route for the request
245
		$page = $this->requestRouter->route();
246
247
		$siteConfiguration = $this->getConfiguration();
248
		$database = PdoDatabase::getDatabaseConnection('acc');
249
250
		if ($siteConfiguration->getIrcNotificationsEnabled()) {
251
			$notificationsDatabase = PdoDatabase::getDatabaseConnection('notifications');
252
		}
253
		else {
254
			// @todo federated table here?
255
			$notificationsDatabase = $database;
256
		}
257
258
		$this->setupHelpers($page, $siteConfiguration, $database, $notificationsDatabase);
259
260
		/* @todo Remove this global statement! It's here for User.php, which does far more than it should. */
261
		global $oauthHelper;
262
		$oauthHelper = $page->getOAuthHelper();
263
264
		/* @todo Remove this global statement! It's here for Request.php, which does far more than it should. */
265
		global $globalXffTrustProvider;
266
		$globalXffTrustProvider = $page->getXffTrustProvider();
267
268
		// run the route code for the request.
269
		$page->execute();
270
	}
271
272
	/**
273
	 * Any cleanup tasks should go here
274
	 *
275
	 * Note that we need to be very careful here, as exceptions may have been thrown and handled.
276
	 * This should *only* be for cleaning up, no logic should go here.
277
	 */
278
	protected function cleanupEnvironment()
279
	{
280
		// Clean up anything we splurged after sending the page.
281
		if (ob_get_level() > 0) {
282
			for ($i = ob_get_level(); $i > 0; $i--) {
283
				ob_end_clean();
284
			}
285
		}
286
	}
287
288
	/**
289
	 * @param Exception $exception
290
	 *
291
	 * @return null|array
292
	 */
293
	private static function getExceptionData($exception)
294
	{
295
		if ($exception == null) {
296
			return null;
297
		}
298
299
		return array(
300
			'exception' => get_class($exception),
301
			'message'   => $exception->getMessage(),
302
			'stack'     => $exception->getTraceAsString(),
303
			'previous'  => self::getExceptionData($exception->getPrevious()),
304
		);
305
	}
306
307
	private function checkForceLogout()
308
	{
309
		$database = PdoDatabase::getDatabaseConnection('acc');
310
311
		$sessionUserId = WebRequest::getSessionUserId();
312
		iF ($sessionUserId === null) {
313
			return;
314
		}
315
316
		// Note, User::getCurrent() caches it's result, which we *really* don't want to trigger.
317
		$currentUser = User::getById($sessionUserId, $database);
318
319
		if ($currentUser === false) {
320
			// Umm... this user has a session cookie with a userId set, but no user exists...
321
			Session::restart();
322
		}
323
324
		if ($currentUser->getForceLogout()) {
325
			Session::restart();
326
327
			$currentUser->setForceLogout(false);
328
			$currentUser->save();
329
		}
330
	}
331
332
	public function isPublic()
333
	{
334
		return $this->isPublic;
335
	}
336
337
	public function setPublic($isPublic)
338
	{
339
		$this->isPublic = $isPublic;
340
	}
341
}
342