Completed
Branch newinternal (e32466)
by Simon
03:39
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 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 on private data access
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()
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 !== null;
154
		$this->assign('requestIsReserved', $requestIsReserved);
155
		$this->assign('requestIsReservedByMe', false);
156
157 View Code Duplication
		if ($requestIsReserved) {
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() !== null) {
226
			return true;
227
		}
228
229
		// user has the reveal hash
230
		if (WebRequest::getString('hash') === $request->getRevealHash()) {
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
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
}