Failed Conditions
Branch newinternal (286d66)
by Simon
03:46
created

PageViewRequest   F

Complexity

Total Complexity 58

Size/Duplication

Total Lines 481
Duplicated Lines 4.99 %

Coupling/Cohesion

Components 1
Dependencies 17

Test Coverage

Coverage 0%

Importance

Changes 13
Bugs 3 Features 2
Metric Value
wmc 58
c 13
b 3
f 2
lcom 1
cbo 17
dl 24
loc 481
ccs 0
cts 291
cp 0
rs 3.5483

12 Methods

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

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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