Failed Conditions
Branch newinternal (104de7)
by Simon
09:33
created

WebStart::getExceptionData()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
c 4
b 0
f 0
dl 0
loc 13
rs 9.4285
cc 2
eloc 8
nc 2
nop 1
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;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
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)
0 ignored issues
show
Coding Style Naming introduced by
The parameter $err_severity is not named in camelCase.

This check marks parameter names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
Coding Style Naming introduced by
The parameter $err_msg is not named in camelCase.

This check marks parameter names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
Coding Style Naming introduced by
The parameter $err_file is not named in camelCase.

This check marks parameter names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
Coding Style Naming introduced by
The parameter $err_line is not named in camelCase.

This check marks parameter names that have not been written in camelCase.

In camelCase names are written without any punctuation, the start of each new word being marked by a capital letter. Thus the name database connection string becomes databaseConnectionString.

Loading history...
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?
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
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. */
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
261
		global $oauthHelper;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
262
		$oauthHelper = $page->getOAuthHelper();
263
264
		/* @todo Remove this global statement! It's here for Request.php, which does far more than it should. */
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
265
		global $globalXffTrustProvider;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
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