Failed Conditions
Branch newinternal (3df7fe)
by Simon
04:13
created

PageViewRequest::main()   B

Complexity

Conditions 7
Paths 25

Size

Total Lines 56
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 2
Bugs 1 Features 0
Metric Value
c 2
b 1
f 0
dl 0
loc 56
ccs 0
cts 38
cp 0
rs 7.7489
cc 7
eloc 30
nc 25
nop 0
crap 56

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Pages;
10
11
use Exception;
12
use Waca\DataObjects\Comment;
13
use Waca\DataObjects\EmailTemplate;
14
use Waca\DataObjects\Log;
15
use Waca\DataObjects\Request;
16
use Waca\DataObjects\User;
17
use Waca\Exceptions\ApplicationLogicException;
18
use Waca\Helpers\LogHelper;
19
use Waca\Helpers\SearchHelpers\RequestSearchHelper;
20
use Waca\PdoDatabase;
21
use Waca\Security\SecurityConfiguration;
22
use Waca\SiteConfiguration;
23
use Waca\Tasks\InternalPageBase;
24
use Waca\WebRequest;
25
26
class PageViewRequest extends InternalPageBase
27
{
28
	const PRIVATE_DATA_BARRIER = 'privateData';
29
	const STATUS_SYMBOL_OPEN = '&#x2610';
30
	const STATUS_SYMBOL_ACCEPTED = '&#x2611';
31
	const STATUS_SYMBOL_REJECTED = '&#x2612';
32
	/**
33
	 * @var array Array of IP address classed as 'private' by RFC1918.
34
	 */
35
	private static $rfc1918ips = array(
36
		"10.0.0.0"    => "10.255.255.255",
37
		"172.16.0.0"  => "172.31.255.255",
38
		"192.168.0.0" => "192.168.255.255",
39
		"169.254.0.0" => "169.254.255.255",
40
		"127.0.0.0"   => "127.255.255.255",
41
	);
42
43
	/**
44
	 * Main function for this page, when no specific actions are called.
45
	 * @throws ApplicationLogicException
46
	 */
47
	protected function main()
48
	{
49
		// set up csrf protection
50
		$this->assignCSRFToken();
51
52
		// get some useful objects
53
		$request = $this->getRequest();
54
		$config = $this->getSiteConfiguration();
55
		$database = $this->getDatabase();
56
		$currentUser = User::getCurrent($database);
57
58
		// Test we should be able to look at this request
59
		if ($config->getEmailConfirmationEnabled()) {
60
			if ($request->getEmailConfirm() !== 'Confirmed') {
61
				// Not allowed to look at this yet.
62
				throw new ApplicationLogicException('The email address has not yet been confirmed for this request.');
63
			}
64
		}
65
66
		$this->assign('requestId', $request->getId());
67
		$this->assign('updateVersion', $request->getUpdateVersion());
68
		$this->assign('requestName', $request->getName());
69
		$this->assign('requestDate', $request->getDate());
70
		$this->assign('requestStatus', $request->getStatus());
71
72
		$this->assign('requestIsClosed', !array_key_exists($request->getStatus(), $config->getRequestStates()));
73
74
		$this->setupUsernameData($request);
75
76
		$this->setupTitle($request);
77
78
		$this->setupReservationDetails($request->getReserved(), $database, $currentUser);
79
		$this->setupGeneralData($database);
80
81
		$this->assign('requestDataCleared', false);
82
		if ($request->getEmail() === $this->getSiteConfiguration()->getDataClearEmail()) {
83
			$this->assign('requestDataCleared', true);
84
		}
85
86
		$allowedPrivateData = true && $this->isAllowedPrivateData($request, $currentUser);
87
88
		$this->setupLogData($request, $database);
89
90
		if ($allowedPrivateData) {
91
			// todo: logging?
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...
92
93
			$this->setupPrivateData($request, $currentUser, $this->getSiteConfiguration());
94
95
			if ($currentUser->isCheckuser()) {
96
				$this->setupCheckUserData($request);
97
			}
98
		}
99
		else {
100
			$this->setTemplate('view-request/main.tpl');
101
		}
102
	}
103
104
	/**
105
	 * Gets a request object
106
	 *
107
	 * @return Request
108
	 * @throws ApplicationLogicException
109
	 */
110 View Code Duplication
	private function getRequest()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
111
	{
112
		$requestId = WebRequest::getInt('id');
113
		if ($requestId === null) {
114
			throw new ApplicationLogicException("No request specified");
115
		}
116
117
		$database = $this->getDatabase();
118
119
		$request = Request::getById($requestId, $database);
120
		if ($request === false || !is_a($request, Request::class)) {
121
			throw new ApplicationLogicException('Could not load the requested request!');
122
		}
123
124
		return $request;
125
	}
126
127
	/**
128
	 * @param Request $request
129
	 */
130
	protected function setupTitle(Request $request)
131
	{
132
		$statusSymbol = self::STATUS_SYMBOL_OPEN;
133
		if ($request->getStatus() === 'Closed') {
134
			if ($request->getWasCreated()) {
135
				$statusSymbol = self::STATUS_SYMBOL_ACCEPTED;
136
			}
137
			else {
138
				$statusSymbol = self::STATUS_SYMBOL_REJECTED;
139
			}
140
		}
141
142
		$this->setHtmlTitle($statusSymbol . ' #' . $request->getId());
143
	}
144
145
	/**
146
	 * @param int         $requestReservationId
147
	 * @param PdoDatabase $database
148
	 * @param User        $currentUser
149
	 */
150
	protected function setupReservationDetails($requestReservationId, PdoDatabase $database, User $currentUser)
151
	{
152
		$requestIsReserved = $requestReservationId != 0;
153
		$this->assign('requestIsReserved', $requestIsReserved);
154
		$this->assign('requestIsReservedByMe', false);
155
156 View Code Duplication
		if ($requestIsReserved) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
157
			$this->assign('requestReservedByName', User::getById($requestReservationId, $database)->getUsername());
158
			$this->assign('requestReservedById', $requestReservationId);
159
160
			if ($requestReservationId === $currentUser->getId()) {
161
				$this->assign('requestIsReservedByMe', true);
162
			}
163
		}
164
	}
165
166
	/**
167
	 * Sets up data unrelated to the request, such as the email template information
168
	 *
169
	 * @param PdoDatabase $database
170
	 */
171
	protected function setupGeneralData(PdoDatabase $database)
172
	{
173
		$config = $this->getSiteConfiguration();
174
175
		$this->assign('createAccountReason', 'Requested account at [[WP:ACC]], request #');
176
177
		$this->assign('defaultRequestState', $config->getDefaultRequestStateKey());
178
179
		$this->assign('requestStates', $config->getRequestStates());
180
181
		/** @var EmailTemplate $createdTemplate */
182
		$createdTemplate = EmailTemplate::getById($config->getDefaultCreatedTemplateId(), $database);
183
184
		$this->assign('createdHasJsQuestion', $createdTemplate->getJsquestion() != '');
185
		$this->assign('createdJsQuestion', $createdTemplate->getJsquestion());
186
		$this->assign('createdId', $createdTemplate->getId());
187
		$this->assign('createdName', $createdTemplate->getName());
188
189
		$createReasons = EmailTemplate::getActiveTemplates(EmailTemplate::CREATED, $database);
190
		$this->assign("createReasons", $createReasons);
191
		$declineReasons = EmailTemplate::getActiveTemplates(EmailTemplate::NOT_CREATED, $database);
192
		$this->assign("declineReasons", $declineReasons);
193
194
		$allCreateReasons = EmailTemplate::getAllActiveTemplates(EmailTemplate::CREATED, $database);
195
		$this->assign("allCreateReasons", $allCreateReasons);
196
		$allDeclineReasons = EmailTemplate::getAllActiveTemplates(EmailTemplate::NOT_CREATED, $database);
197
		$this->assign("allDeclineReasons", $allDeclineReasons);
198
		$allOtherReasons = EmailTemplate::getAllActiveTemplates(false, $database);
199
		$this->assign("allOtherReasons", $allOtherReasons);
200
201
		$this->getTypeAheadHelper()->defineTypeAheadSource('username-typeahead', function() use ($database) {
202
			return User::getAllUsernames($database, true);
203
		});
204
	}
205
206
	/**
207
	 * Returns a value stating whether the user is allowed to see private data or not
208
	 *
209
	 * @param Request $request
210
	 * @param User    $currentUser
211
	 *
212
	 * @return bool
213
	 * @category Security-Critical
214
	 */
215
	private function isAllowedPrivateData(Request $request, User $currentUser)
216
	{
217
		// Test the main security barrier for private data access using SecurityManager
218
		if ($this->barrierTest(self::PRIVATE_DATA_BARRIER)) {
219
			// Tool admins/check-users can always see private data
220
			return true;
221
		}
222
223
		// reserving user is allowed to see the data
224
		if ($currentUser->getId() === $request->getReserved() && $request->getReserved() !== 0) {
225
			return true;
226
		}
227
228
		// user has the reveal hash
229
		if (WebRequest::getString('hash') === $request->getRevealHash()) {
0 ignored issues
show
Unused Code introduced by
This if statement, and the following return statement can be replaced with return \Waca\WebRequest:...quest->getRevealHash();.
Loading history...
230
			return true;
231
		}
232
233
		// nope. Not allowed.
234
		return false;
235
	}
236
237
	private function setupLogData(Request $request, PdoDatabase $database)
238
	{
239
		$currentUser = User::getCurrent($database);
240
241
		$logs = LogHelper::getRequestLogsWithComments($request->getId(), $database);
242
		$requestLogs = array();
243
244
		if (trim($request->getComment()) !== "") {
245
			$requestLogs[] = array(
246
				'type'     => 'comment',
247
				'security' => 'user',
248
				'userid'   => null,
249
				'user'     => $request->getName(),
250
				'entry'    => null,
251
				'time'     => $request->getDate(),
252
				'canedit'  => false,
253
				'id'       => $request->getId(),
254
				'comment'  => $request->getComment(),
255
			);
256
		}
257
258
		/** @var User[] $nameCache */
259
		$nameCache = array();
260
261
		$editableComments = false;
262
		if ($currentUser->isAdmin() || $currentUser->isCheckuser()) {
263
			$editableComments = true;
264
		}
265
266
		/** @var Log|Comment $entry */
267
		foreach ($logs as $entry) {
268
			if (!($entry instanceof User) && !($entry instanceof Log)) {
269
				// Something weird has happened here.
270
				continue;
271
			}
272
273
			// both log and comment have a 'user' field
274
			if (!array_key_exists($entry->getUser(), $nameCache)) {
275
				$entryUser = User::getById($entry->getUser(), $database);
276
				$nameCache[$entry->getUser()] = $entryUser;
277
			}
278
279
			if ($entry instanceof Comment) {
280
				$requestLogs[] = array(
281
					'type'     => 'comment',
282
					'security' => $entry->getVisibility(),
283
					'user'     => $nameCache[$entry->getUser()]->getUsername(),
284
					'userid'   => $entry->getUser() == -1 ? null : $entry->getUser(),
285
					'entry'    => null,
286
					'time'     => $entry->getTime(),
287
					'canedit'  => ($editableComments || $entry->getUser() == $currentUser->getId()),
288
					'id'       => $entry->getId(),
289
					'comment'  => $entry->getComment(),
290
				);
291
			}
292
293
			if ($entry instanceof Log) {
294
				$requestLogs[] = array(
295
					'type'     => 'log',
296
					'security' => 'user',
297
					'userid'   => $entry->getUser() == -1 ? null : $entry->getUser(),
298
					'user'     => $nameCache[$entry->getUser()]->getUsername(),
299
					'entry'    => LogHelper::getLogDescription($entry),
300
					'time'     => $entry->getTimestamp(),
301
					'canedit'  => false,
302
					'id'       => $entry->getId(),
303
					'comment'  => $entry->getComment(),
304
				);
305
			}
306
		}
307
308
		$this->assign("requestLogs", $requestLogs);
309
	}
310
311
	/**
312
	 * @param Request           $request
313
	 * @param User              $currentUser
314
	 * @param SiteConfiguration $configuration
315
	 *
316
	 * @throws Exception
317
	 */
318
	protected function setupPrivateData($request, User $currentUser, SiteConfiguration $configuration)
319
	{
320
		$xffProvider = $this->getXffTrustProvider();
321
		$this->setTemplate('view-request/main-with-data.tpl');
322
323
		$relatedEmailRequests = RequestSearchHelper::get($this->getDatabase())
324
			->byEmailAddress($request->getEmail())
325
			->withConfirmedEmail()
326
			->excludingPurgedData($configuration)
327
			->excludingRequest($request->getId())
328
			->fetch();
329
330
		$this->assign('requestEmail', $request->getEmail());
331
		$emailDomain = explode("@", $request->getEmail())[1];
332
		$this->assign("emailurl", $emailDomain);
333
		$this->assign('requestRelatedEmailRequestsCount', count($relatedEmailRequests));
334
		$this->assign('requestRelatedEmailRequests', $relatedEmailRequests);
335
336
		$trustedIp = $xffProvider->getTrustedClientIp($request->getIp(), $request->getForwardedIp());
337
		$this->assign('requestTrustedIp', $trustedIp);
338
		$this->assign('requestRealIp', $request->getIp());
339
		$this->assign('requestForwardedIp', $request->getForwardedIp());
340
341
		$trustedIpLocation = $this->getLocationProvider()->getIpLocation($trustedIp);
342
		$this->assign('requestTrustedIpLocation', $trustedIpLocation);
343
344
		$this->assign('requestHasForwardedIp', $request->getForwardedIp() !== null);
345
346
		$relatedIpRequests = RequestSearchHelper::get($this->getDatabase())
347
			->byIp($trustedIp)
348
			->withConfirmedEmail()
349
			->excludingPurgedData($configuration)
350
			->excludingRequest($request->getId())
351
			->fetch();
352
353
		$this->assign('requestRelatedIpRequestsCount', count($relatedIpRequests));
354
		$this->assign('requestRelatedIpRequests', $relatedIpRequests);
355
356
		$this->assign('showRevealLink', false);
357
		if ($request->getReserved() === $currentUser->getId() ||
358
			$currentUser->isAdmin() ||
359
			$currentUser->isCheckuser()
360
		) {
361
			$this->assign('showRevealLink', true);
362
363
			$this->assign('revealHash', $request->getRevealHash());
364
		}
365
366
		$this->setupForwardedIpData($request);
367
	}
368
369
	private function setupForwardedIpData(Request $request)
370
	{
371
		if ($request->getForwardedIp() !== null) {
372
			$requestProxyData = array(); // Initialize array to store data to be output in Smarty template.
373
			$proxyIndex = 0;
374
375
			// Assuming [client] <=> [proxy1] <=> [proxy2] <=> [proxy3] <=> [us], we will see an XFF header of [client],
376
			// [proxy1], [proxy2], and our actual IP will be [proxy3]
377
			$proxies = explode(",", $request->getForwardedIp());
378
			$proxies[] = $request->getIp();
379
380
			// Origin is the supposed "client" IP.
381
			$origin = $proxies[0];
382
			$this->assign("forwardedOrigin", $origin);
383
384
			// We step through the servers in reverse order, from closest to furthest
385
			$proxies = array_reverse($proxies);
386
387
			// By default, we have trust, because the first in the chain is now REMOTE_ADDR, which is hardest to spoof.
388
			$trust = true;
389
390
			/**
391
			 * @var int    $index     The zero-based index of the proxy.
392
			 * @var string $proxyData The proxy IP address (although possibly not!)
393
			 */
394
			foreach ($proxies as $index => $proxyData) {
395
				$proxyAddress = trim($proxyData);
396
				$requestProxyData[$proxyIndex]['ip'] = $proxyAddress;
397
398
				// get data on this IP.
399
				$thisProxyIsTrusted = $this->getXffTrustProvider()->isTrusted($proxyAddress);
400
401
				$proxyIsInPrivateRange = $this->getXffTrustProvider()->ipInRange(self::$rfc1918ips, $proxyAddress);
402
403
				if (!$proxyIsInPrivateRange) {
404
					$proxyReverseDns = $this->getRdnsProvider()->getReverseDNS($proxyAddress);
405
					$proxyLocation = $this->getLocationProvider()->getIpLocation($proxyAddress);
406
				}
407
				else {
408
					// this is going to fail, so why bother trying?
409
					$proxyReverseDns = false;
410
					$proxyLocation = false;
411
				}
412
413
				// current trust chain status BEFORE this link
414
				$preLinkTrust = $trust;
415
416
				// is *this* link trusted? Note, this will be true even if there is an untrusted link before this!
417
				$requestProxyData[$proxyIndex]['trustedlink'] = $thisProxyIsTrusted;
418
419
				// set the trust status of the chain to this point
420
				$trust = $trust & $thisProxyIsTrusted;
421
422
				// If this is the origin address, and the chain was trusted before this point, then we can trust
423
				// the origin.
424
				if ($preLinkTrust && $proxyAddress == $origin) {
425
					// if this is the origin, then we are at the last point in the chain.
426
					// @todo: this is probably the cause of some bugs when an IP appears twice - we're missing a check
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...
427
					// to see if this is *really* the last in the chain, rather than just the same IP as it.
428
					$trust = true;
429
				}
430
431
				$requestProxyData[$proxyIndex]['trust'] = $trust;
432
433
				$requestProxyData[$proxyIndex]['rdnsfailed'] = $proxyReverseDns === false;
434
				$requestProxyData[$proxyIndex]['rdns'] = $proxyReverseDns;
435
				$requestProxyData[$proxyIndex]['routable'] = !$proxyIsInPrivateRange;
436
437
				$requestProxyData[$proxyIndex]['location'] = $proxyLocation;
438
439
				if ($proxyReverseDns === $proxyAddress && $proxyIsInPrivateRange === false) {
440
					$requestProxyData[$proxyIndex]['rdns'] = null;
441
				}
442
443
				$showLinks = (!$trust || $proxyAddress == $origin) && !$proxyIsInPrivateRange;
444
				$requestProxyData[$proxyIndex]['showlinks'] = $showLinks;
445
446
				$proxyIndex++;
447
			}
448
449
			$this->assign("requestProxyData", $requestProxyData);
450
		}
451
	}
452
453
	/**
454
	 * @param Request $request
455
	 */
456
	protected function setupCheckUserData(Request $request)
457
	{
458
		$this->setTemplate('view-request/main-with-checkuser-data.tpl');
459
		$this->assign('requestUserAgent', $request->getUserAgent());
460
	}
461
462
	/**
463
	 * Sets up the security for this page. If certain actions have different permissions, this should be reflected in
464
	 * the return value from this function.
465
	 *
466
	 * If this page even supports actions, you will need to check the route
467
	 *
468
	 * @return SecurityConfiguration
469
	 * @category Security-Critical
470
	 */
471
	protected function getSecurityConfiguration()
472
	{
473
		switch ($this->getRouteName()) {
474
			case self::PRIVATE_DATA_BARRIER:
475
				return $this->getSecurityManager()->configure()->asGeneralPrivateDataAccess();
476
			default:
477
				return $this->getSecurityManager()->configure()->asInternalPage();
478
		}
479
	}
480
481
	/**
482
	 * @param Request $request
483
	 */
484
	protected function setupUsernameData(Request $request)
485
	{
486
		$blacklistData = $this->getBlacklistHelper()->isBlacklisted($request->getName());
487
488
		$this->assign('requestIsBlacklisted', $blacklistData !== false);
489
		$this->assign('requestBlacklist', $blacklistData);
490
491
		try {
492
			$spoofs = $this->getAntiSpoofProvider()->getSpoofs($request->getName());
493
		}
494
		catch (Exception $ex) {
495
			$spoofs = $ex->getMessage();
496
		}
497
498
		$this->assign("spoofs", $spoofs);
499
	}
500
}