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

PageViewRequest   D

Complexity

Total Complexity 48

Size/Duplication

Total Lines 443
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 19

Importance

Changes 7
Bugs 1 Features 0
Metric Value
wmc 48
c 7
b 1
f 0
lcom 1
cbo 19
dl 0
loc 443
rs 4.8854

12 Methods

Rating   Name   Duplication   Size   Complexity  
B main() 0 56 7
A getRequest() 0 16 4
A setupTitle() 0 14 3
A setupReservationDetails() 0 15 3
B setupGeneralData() 0 34 1
A isAllowedPrivateData() 0 4 1
C setupLogData() 0 68 11
B setupPrivateData() 0 47 4
C setupForwardedIpData() 0 83 10
A setupCheckUserData() 0 5 1
A getSecurityConfiguration() 0 5 1
A setupUsernameData() 0 16 2

How to fix   Complexity   

Complex Class

Complex classes like PageViewRequest often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PageViewRequest, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Waca\Pages;
4
5
use Exception;
6
use Waca\DataObjects\Comment;
7
use Waca\DataObjects\EmailTemplate;
8
use Waca\DataObjects\Log;
9
use Waca\DataObjects\Request;
10
use Waca\DataObjects\User;
11
use Waca\Exceptions\ApplicationLogicException;
12
use Waca\Helpers\LogHelper;
13
use Waca\Helpers\RequestSearchHelper;
14
use Waca\PdoDatabase;
15
use Waca\Security\SecurityConfiguration;
16
use Waca\SiteConfiguration;
17
use Waca\Tasks\InternalPageBase;
18
use Waca\WebRequest;
19
20
class PageViewRequest extends InternalPageBase
21
{
22
	const STATUS_SYMBOL_OPEN = '&#x2610';
23
	const STATUS_SYMBOL_ACCEPTED = '&#x2611';
24
	const STATUS_SYMBOL_REJECTED = '&#x2612';
25
	/**
26
	 * @var array Array of IP address classed as 'private' by RFC1918.
27
	 */
28
	private static $rfc1918ips = array(
29
		"10.0.0.0"    => "10.255.255.255",
30
		"172.16.0.0"  => "172.31.255.255",
31
		"192.168.0.0" => "192.168.255.255",
32
		"169.254.0.0" => "169.254.255.255",
33
		"127.0.0.0"   => "127.255.255.255",
34
	);
35
36
	/**
37
	 * Main function for this page, when no specific actions are called.
38
	 * @throws ApplicationLogicException
39
	 */
40
	protected function main()
41
	{
42
		// set up csrf protection
43
		$this->assignCSRFToken();
44
45
		// get some useful objects
46
		$request = $this->getRequest();
47
		$config = $this->getSiteConfiguration();
48
		$database = $this->getDatabase();
49
		$currentUser = User::getCurrent($database);
50
51
		// Test we should be able to look at this request
52
		if ($config->getEmailConfirmationEnabled()) {
53
			if ($request->getEmailConfirm() !== 'Confirmed') {
54
				// Not allowed to look at this yet.
55
				throw new ApplicationLogicException('The email address has not yet been confirmed for this request.');
56
			}
57
		}
58
59
		$this->assign('requestId', $request->getId());
60
		$this->assign('updateVersion', $request->getUpdateVersion());
61
		$this->assign('requestName', $request->getName());
62
		$this->assign('requestDate', $request->getDate());
63
		$this->assign('requestStatus', $request->getStatus());
64
65
		$this->assign('requestIsClosed', !array_key_exists($request->getStatus(), $config->getRequestStates()));
66
67
		$this->setupUsernameData($request);
68
69
		$this->setupTitle($request);
70
71
		$this->setupReservationDetails($request->getReserved(), $database, $currentUser);
72
		$this->setupGeneralData($database);
73
74
		$this->assign('requestDataCleared', false);
75
		if ($request->getEmail() === $this->getSiteConfiguration()->getDataClearEmail()) {
76
			$this->assign('requestDataCleared', true);
77
		}
78
79
		$allowedPrivateData = true && $this->isAllowedPrivateData();
80
81
		$this->setupLogData($request, $database);
82
83
		if ($allowedPrivateData) {
84
			// todo: logging?
85
86
			$this->setupPrivateData($request, $currentUser, $this->getSiteConfiguration());
87
88
			if ($currentUser->isCheckuser()) {
89
				$this->setupCheckUserData($request);
90
			}
91
		}
92
		else {
93
			$this->setTemplate('view-request/main.tpl');
94
		}
95
	}
96
97
	/**
98
	 * Gets a request object
99
	 *
100
	 * @return Request
101
	 * @throws ApplicationLogicException
102
	 */
103
	private function getRequest()
104
	{
105
		$requestId = WebRequest::getInt('id');
106
		if ($requestId === null) {
107
			throw new ApplicationLogicException("No request specified");
108
		}
109
110
		$database = $this->getDatabase();
111
112
		$request = Request::getById($requestId, $database);
113
		if ($request === false || !is_a($request, Request::class)) {
114
			throw new ApplicationLogicException('Could not load the requested request!');
115
		}
116
117
		return $request;
118
	}
119
120
	/**
121
	 * @param Request $request
122
	 */
123
	protected function setupTitle(Request $request)
124
	{
125
		$statusSymbol = self::STATUS_SYMBOL_OPEN;
126
		if ($request->getStatus() === 'Closed') {
127
			if ($request->getWasCreated()) {
128
				$statusSymbol = self::STATUS_SYMBOL_ACCEPTED;
129
			}
130
			else {
131
				$statusSymbol = self::STATUS_SYMBOL_REJECTED;
132
			}
133
		}
134
135
		$this->setHtmlTitle($statusSymbol . ' #' . $request->getId());
136
	}
137
138
	/**
139
	 * @param int         $requestReservationId
140
	 * @param PdoDatabase $database
141
	 * @param User        $currentUser
142
	 */
143
	protected function setupReservationDetails($requestReservationId, PdoDatabase $database, User $currentUser)
144
	{
145
		$requestIsReserved = $requestReservationId != 0;
146
		$this->assign('requestIsReserved', $requestIsReserved);
147
		$this->assign('requestIsReservedByMe', false);
148
149
		if ($requestIsReserved) {
150
			$this->assign('requestReservedByName', User::getById($requestReservationId, $database)->getUsername());
151
			$this->assign('requestReservedById', $requestReservationId);
152
153
			if ($requestReservationId === $currentUser->getId()) {
154
				$this->assign('requestIsReservedByMe', true);
155
			}
156
		}
157
	}
158
159
	/**
160
	 * Sets up data unrelated to the request, such as the email template information
161
	 *
162
	 * @param PdoDatabase $database
163
	 */
164
	protected function setupGeneralData(PdoDatabase $database)
165
	{
166
		$config = $this->getSiteConfiguration();
167
168
		$this->assign('createAccountReason', 'Requested account at [[WP:ACC]], request #');
169
170
		$this->assign('defaultRequestState', $config->getDefaultRequestStateKey());
171
172
		$this->assign('requestStates', $config->getRequestStates());
173
174
		/** @var EmailTemplate $createdTemplate */
175
		$createdTemplate = EmailTemplate::getById($config->getDefaultCreatedTemplateId(), $database);
176
177
		$this->assign('createdHasJsQuestion', $createdTemplate->getJsquestion() != '');
178
		$this->assign('createdJsQuestion', $createdTemplate->getJsquestion());
179
		$this->assign('createdId', $createdTemplate->getId());
180
		$this->assign('createdName', $createdTemplate->getName());
181
182
		$createReasons = EmailTemplate::getActiveTemplates(EmailTemplate::CREATED, $database);
183
		$this->assign("createReasons", $createReasons);
184
		$declineReasons = EmailTemplate::getActiveTemplates(EmailTemplate::NOT_CREATED, $database);
185
		$this->assign("declineReasons", $declineReasons);
186
187
		$allCreateReasons = EmailTemplate::getAllActiveTemplates(EmailTemplate::CREATED, $database);
188
		$this->assign("allCreateReasons", $allCreateReasons);
189
		$allDeclineReasons = EmailTemplate::getAllActiveTemplates(EmailTemplate::NOT_CREATED, $database);
190
		$this->assign("allDeclineReasons", $allDeclineReasons);
191
		$allOtherReasons = EmailTemplate::getAllActiveTemplates(false, $database);
192
		$this->assign("allOtherReasons", $allOtherReasons);
193
194
		$this->getTypeAheadHelper()->defineTypeAheadSource('username-typeahead', function() use ($database) {
195
			return User::getAllUsernames($database, true);
196
		});
197
	}
198
199
	/**
200
	 * Returns a value stating whether the user is allowed to see private data or not
201
	 *
202
	 * @return bool
203
	 * @category Security-Critical
204
	 * @todo     Implement me!
205
	 */
206
	private function isAllowedPrivateData()
207
	{
208
		return true;
209
	}
210
211
	private function setupLogData(Request $request, PdoDatabase $database)
212
	{
213
		$currentUser = User::getCurrent($database);
214
215
		$logs = LogHelper::getRequestLogsWithComments($request->getId(), $database);
216
		$requestLogs = array();
217
218
		if (trim($request->getComment()) !== "") {
219
			$requestLogs[] = array(
220
				'type'     => 'comment',
221
				'security' => 'user',
222
				'userid'   => null,
223
				'user'     => $request->getName(),
224
				'entry'    => null,
225
				'time'     => $request->getDate(),
226
				'canedit'  => false,
227
				'id'       => $request->getId(),
228
				'comment'  => $request->getComment(),
229
			);
230
		}
231
232
		/** @var User[] $nameCache */
233
		$nameCache = array();
234
235
		$editableComments = false;
236
		if ($currentUser->isAdmin() || $currentUser->isCheckuser()) {
237
			$editableComments = true;
238
		}
239
240
		/** @var Log|Comment $entry */
241
		foreach ($logs as $entry) {
242
			// both log and comment have a 'user' field
243
			if (!array_key_exists($entry->getUser(), $nameCache)) {
244
				$entryUser = User::getById($entry->getUser(), $database);
245
				$nameCache[$entry->getUser()] = $entryUser;
246
			}
247
248
			if ($entry instanceof Comment) {
249
				$requestLogs[] = array(
250
					'type'     => 'comment',
251
					'security' => $entry->getVisibility(),
252
					'user'     => $nameCache[$entry->getUser()]->getUsername(),
253
					'userid'   => $entry->getUser() == -1 ? null : $entry->getUser(),
254
					'entry'    => null,
255
					'time'     => $entry->getTime(),
256
					'canedit'  => ($editableComments || $entry->getUser() == $currentUser->getId()),
257
					'id'       => $entry->getId(),
258
					'comment'  => $entry->getComment(),
259
				);
260
			}
261
262
			if ($entry instanceof Log) {
263
				$requestLogs[] = array(
264
					'type'     => 'log',
265
					'security' => 'user',
266
					'userid'   => $entry->getUser() == -1 ? null : $entry->getUser(),
267
					'user'     => $nameCache[$entry->getUser()]->getUsername(),
268
					'entry'    => LogHelper::getLogDescription($entry),
269
					'time'     => $entry->getTimestamp(),
270
					'canedit'  => false,
271
					'id'       => $entry->getId(),
272
					'comment'  => $entry->getComment(),
273
				);
274
			}
275
		}
276
277
		$this->assign("requestLogs", $requestLogs);
278
	}
279
280
	/**
281
	 * @param Request           $request
282
	 * @param User              $currentUser
283
	 * @param SiteConfiguration $configuration
284
	 *
285
	 * @throws Exception
286
	 */
287
	protected function setupPrivateData($request, User $currentUser, SiteConfiguration $configuration)
288
	{
289
		$xffProvider = $this->getXffTrustProvider();
290
		$this->setTemplate('view-request/main-with-data.tpl');
291
292
		$relatedEmailRequests = RequestSearchHelper::get($this->getDatabase())
293
			->byEmailAddress($request->getEmail())
294
			->withConfirmedEmail()
295
			->excludingPurgedData($configuration)
296
			->excludingRequest($request->getId())
297
			->fetch();
298
299
		$this->assign('requestEmail', $request->getEmail());
300
		$this->assign('requestRelatedEmailRequestsCount', count($relatedEmailRequests));
301
		$this->assign('requestRelatedEmailRequests', $relatedEmailRequests);
302
303
		$trustedIp = $xffProvider->getTrustedClientIp($request->getIp(), $request->getForwardedIp());
304
		$this->assign('requestTrustedIp', $trustedIp);
305
		$this->assign('requestRealIp', $request->getIp());
306
		$this->assign('requestForwardedIp', $request->getForwardedIp());
307
308
		$trustedIpLocation = $this->getLocationProvider()->getIpLocation($trustedIp);
309
		$this->assign('requestTrustedIpLocation', $trustedIpLocation);
310
311
		$this->assign('requestHasForwardedIp', $request->getForwardedIp() !== null);
312
313
		$relatedIpRequests = RequestSearchHelper::get($this->getDatabase())
314
			->byIp($trustedIp)
315
			->withConfirmedEmail()
316
			->excludingPurgedData($configuration)
317
			->excludingRequest($request->getId())
318
			->fetch();
319
320
		$this->assign('requestRelatedIpRequestsCount', count($relatedIpRequests));
321
		$this->assign('requestRelatedIpRequests', $relatedIpRequests);
322
323
		$this->assign('showRevealLink', false);
324
		if ($request->getReserved() === $currentUser->getId() ||
325
			$currentUser->isAdmin() ||
326
			$currentUser->isCheckuser()
327
		) {
328
			$this->assign('showRevealLink', true);
329
			$this->assign('revealHash', false); // @todo
330
		}
331
332
		$this->setupForwardedIpData($request);
333
	}
334
335
	private function setupForwardedIpData(Request $request)
336
	{
337
		if ($request->getForwardedIp() !== null) {
338
			$requestProxyData = array(); // Initialize array to store data to be output in Smarty template.
339
			$proxyIndex = 0;
340
341
			// Assuming [client] <=> [proxy1] <=> [proxy2] <=> [proxy3] <=> [us], we will see an XFF header of [client],
342
			// [proxy1], [proxy2], and our actual IP will be [proxy3]
343
			$proxies = explode(",", $request->getForwardedIp());
344
			$proxies[] = $request->getIp();
345
346
			// Origin is the supposed "client" IP.
347
			$origin = $proxies[0];
348
			$this->assign("forwardedOrigin", $origin);
349
350
			// We step through the servers in reverse order, from closest to furthest
351
			$proxies = array_reverse($proxies);
352
353
			// By default, we have trust, because the first in the chain is now REMOTE_ADDR, which is hardest to spoof.
354
			$trust = true;
355
356
			/**
357
			 * @var int    $index     The zero-based index of the proxy.
358
			 * @var string $proxyData The proxy IP address (although possibly not!)
359
			 */
360
			foreach ($proxies as $index => $proxyData) {
361
				$proxyAddress = trim($proxyData);
362
				$requestProxyData[$proxyIndex]['ip'] = $proxyAddress;
363
364
				// get data on this IP.
365
				$thisProxyIsTrusted = $this->getXffTrustProvider()->isTrusted($proxyAddress);
366
367
				$proxyIsInPrivateRange = $this->getXffTrustProvider()->ipInRange(self::$rfc1918ips, $proxyAddress);
368
369
				if (!$proxyIsInPrivateRange) {
370
					$proxyReverseDns = $this->getRdnsProvider()->getReverseDNS($proxyAddress);
371
					$proxyLocation = $this->getLocationProvider()->getIpLocation($proxyAddress);
372
				}
373
				else {
374
					// this is going to fail, so why bother trying?
375
					$proxyReverseDns = false;
376
					$proxyLocation = false;
377
				}
378
379
				// current trust chain status BEFORE this link
380
				$preLinkTrust = $trust;
381
382
				// is *this* link trusted? Note, this will be true even if there is an untrusted link before this!
383
				$requestProxyData[$proxyIndex]['trustedlink'] = $thisProxyIsTrusted;
384
385
				// set the trust status of the chain to this point
386
				$trust = $trust & $thisProxyIsTrusted;
387
388
				// If this is the origin address, and the chain was trusted before this point, then we can trust
389
				// the origin.
390
				if ($preLinkTrust && $proxyAddress == $origin) {
391
					// if this is the origin, then we are at the last point in the chain.
392
					// @todo: this is probably the cause of some bugs when an IP appears twice - we're missing a check
393
					// to see if this is *really* the last in the chain, rather than just the same IP as it.
394
					$trust = true;
395
				}
396
397
				$requestProxyData[$proxyIndex]['trust'] = $trust;
398
399
				$requestProxyData[$proxyIndex]['rdnsfailed'] = $proxyReverseDns === false;
400
				$requestProxyData[$proxyIndex]['rdns'] = $proxyReverseDns;
401
				$requestProxyData[$proxyIndex]['routable'] = !$proxyIsInPrivateRange;
402
403
				$requestProxyData[$proxyIndex]['location'] = $proxyLocation;
404
405
				if ($proxyReverseDns === $proxyAddress && $proxyIsInPrivateRange === false) {
406
					$requestProxyData[$proxyIndex]['rdns'] = null;
407
				}
408
409
				$showLinks = (!$trust || $proxyAddress == $origin) && !$proxyIsInPrivateRange;
410
				$requestProxyData[$proxyIndex]['showlinks'] = $showLinks;
411
412
				$proxyIndex++;
413
			}
414
415
			$this->assign("requestProxyData", $requestProxyData);
416
		}
417
	}
418
419
	/**
420
	 * @param Request $request
421
	 */
422
	protected function setupCheckUserData(Request $request)
423
	{
424
		$this->setTemplate('view-request/main-with-checkuser-data.tpl');
425
		$this->assign('requestUserAgent', $request->getUserAgent());
426
	}
427
428
	/**
429
	 * Sets up the security for this page. If certain actions have different permissions, this should be reflected in
430
	 * the return value from this function.
431
	 *
432
	 * If this page even supports actions, you will need to check the route
433
	 *
434
	 * @return SecurityConfiguration
435
	 * @category Security-Critical
436
	 */
437
	protected function getSecurityConfiguration()
438
	{
439
		// @todo review me!
440
		return $this->getSecurityManager()->configure()->asInternalPage();
441
	}
442
443
	/**
444
	 * @param Request $request
445
	 */
446
	protected function setupUsernameData(Request $request)
447
	{
448
		$blacklistData = $this->getBlacklistHelper()->isBlacklisted($request->getName());
449
450
		$this->assign('requestIsBlacklisted', $blacklistData !== false);
451
		$this->assign('requestBlacklist', $blacklistData);
452
453
		try {
454
			$spoofs = $this->getAntiSpoofProvider()->getSpoofs($request->getName());
455
		}
456
		catch (Exception $ex) {
457
			$spoofs = $ex->getMessage();
458
		}
459
460
		$this->assign("spoofs", $spoofs);
461
	}
462
}