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

includes/Pages/PageViewRequest.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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?
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()
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) {
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
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
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
}