Issues (186)

includes/Fragments/RequestData.php (6 issues)

1
<?php
2
/******************************************************************************
3
 * Wikipedia Account Creation Assistance tool                                 *
4
 * ACC Development Team. Please see team.json for a list of contributors.     *
5
 *                                                                            *
6
 * This is free and unencumbered software released into the public domain.    *
7
 * Please see LICENSE.md for the full licencing statement.                    *
8
 ******************************************************************************/
9
10
namespace Waca\Fragments;
11
12
use Waca\DataObjects\Request;
13
use Waca\DataObjects\RequestQueue;
14
use Waca\DataObjects\User;
15
use Waca\Exceptions\ApplicationLogicException;
16
use Waca\Helpers\SearchHelpers\RequestSearchHelper;
17
use Waca\Pages\PageRequestFormManagement;
18
use Waca\Pages\RequestAction\PageBreakReservation;
19
use Waca\PdoDatabase;
20
use Waca\Providers\Interfaces\ILocationProvider;
21
use Waca\Providers\Interfaces\IRDnsProvider;
22
use Waca\Providers\Interfaces\IXffTrustProvider;
23
use Waca\RequestStatus;
24
use Waca\Security\ISecurityManager;
25
use Waca\SiteConfiguration;
26
use Waca\WebRequest;
27
28
trait RequestData
29
{
30
    /** @return SiteConfiguration */
31
    protected abstract function getSiteConfiguration();
32
33
    /**
34
     * @var array Array of IP address classed as 'private' by RFC1918.
35
     */
36
    protected 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
     * Gets a request object
46
     *
47
     * @param PdoDatabase $database  The database connection
48
     * @param int|null    $requestId The ID of the request to retrieve
49
     *
50
     * @return Request
51
     * @throws ApplicationLogicException
52
     */
53
    protected function getRequest(PdoDatabase $database, $requestId)
54
    {
55
        if ($requestId === null) {
56
            throw new ApplicationLogicException("No request specified");
57
        }
58
59
        $request = Request::getById($requestId, $database);
60
        if ($request === false || !is_a($request, Request::class)) {
61
            throw new ApplicationLogicException('Could not load the requested request!');
62
        }
63
64
        return $request;
65
    }
66
67
    /**
68
     * Returns a value stating whether the user is allowed to see private data or not
69
     *
70
     * @param Request $request
71
     * @param User    $currentUser
72
     *
73
     * @return bool
74
     * @category Security-Critical
75
     */
76
    protected function isAllowedPrivateData(Request $request, User $currentUser)
77
    {
78
        // Test the main security barrier for private data access using SecurityManager
79
        if ($this->barrierTest('alwaysSeePrivateData', $currentUser, 'RequestData')) {
80
            // Tool admins/check-users can always see private data
81
            return true;
82
        }
83
84
        // reserving user is allowed to see the data
85
        if ($currentUser->getId() === $request->getReserved()
86
            && $request->getReserved() !== null
87
            && $this->barrierTest('seePrivateDataWhenReserved', $currentUser, 'RequestData')
88
        ) {
89
            return true;
90
        }
91
92
        // user has the reveal hash
93
        if (WebRequest::getString('hash') === $request->getRevealHash()
94
            && $this->barrierTest('seePrivateDataWithHash', $currentUser, 'RequestData')
95
        ) {
96
            return true;
97
        }
98
99
        // nope. Not allowed.
100
        return false;
101
    }
102
103
    /**
104
     * Tests the security barrier for a specified action.
105
     *
106
     * Don't use within templates
107
     *
108
     * @param string      $action
109
     *
110
     * @param User        $user
111
     * @param null|string $pageName
112
     *
113
     * @return bool
114
     * @category Security-Critical
115
     */
116
    abstract protected function barrierTest($action, User $user, $pageName = null);
117
118
    /**
119
     * Gets the name of the route that has been passed from the request router.
120
     * @return string
121
     */
122
    abstract protected function getRouteName();
123
124
    abstract protected function getSecurityManager(): ISecurityManager;
125
126
    /**
127
     * Sets the name of the template this page should display.
128
     *
129
     * @param string $name
130
     */
131
    abstract protected function setTemplate($name);
132
133
    /** @return IXffTrustProvider */
134
    abstract protected function getXffTrustProvider();
135
136
    /** @return ILocationProvider */
137
    abstract protected function getLocationProvider();
138
139
    /** @return IRDnsProvider */
140
    abstract protected function getRdnsProvider();
141
142
    /**
143
     * Assigns a Smarty variable
144
     *
145
     * @param  array|string $name  the template variable name(s)
146
     * @param  mixed        $value the value to assign
147
     */
148
    abstract protected function assign($name, $value);
149
150
    /**
151
     * @param int|null    $requestReservationId
152
     * @param PdoDatabase $database
153
     * @param User        $currentUser
154
     */
155
    protected function setupReservationDetails($requestReservationId, PdoDatabase $database, User $currentUser)
156
    {
157
        $requestIsReserved = $requestReservationId !== null;
158
        $this->assign('requestIsReserved', $requestIsReserved);
159
        $this->assign('requestIsReservedByMe', false);
160
161
        if ($requestIsReserved) {
162
            $this->assign('requestReservedByName', User::getById($requestReservationId, $database)->getUsername());
163
            $this->assign('requestReservedById', $requestReservationId);
164
165
            if ($requestReservationId === $currentUser->getId()) {
166
                $this->assign('requestIsReservedByMe', true);
167
            }
168
        }
169
170
        $this->assign('canBreakReservation', $this->barrierTest('force', $currentUser, PageBreakReservation::class));
171
    }
172
173
    /**
174
     * Adds private request data to Smarty. DO NOT USE WITHOUT FIRST CHECKING THAT THE USER IS AUTHORISED!
175
     *
176
     * @param Request           $request
177
     * @param SiteConfiguration $configuration
178
     */
179
    protected function setupPrivateData(
180
        $request,
181
        SiteConfiguration $configuration
182
    ) {
183
        $xffProvider = $this->getXffTrustProvider();
184
185
        $this->assign('requestEmail', $request->getEmail());
186
        $emailDomain = explode("@", $request->getEmail())[1];
0 ignored issues
show
It seems like $request->getEmail() can also be of type null; however, parameter $string of explode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

186
        $emailDomain = explode("@", /** @scrutinizer ignore-type */ $request->getEmail())[1];
Loading history...
187
        $this->assign("emailurl", $emailDomain);
188
        $this->assign('commonEmailDomain', in_array(strtolower($emailDomain), $configuration->getCommonEmailDomains())
189
            || $request->getEmail() === $this->getSiteConfiguration()->getDataClearEmail());
190
191
        $trustedIp = $xffProvider->getTrustedClientIp($request->getIp(), $request->getForwardedIp());
192
        $this->assign('requestTrustedIp', $trustedIp);
193
        $this->assign('requestTrustedIpProtocol', $this->getIpProtocol($trustedIp));
194
        $this->assign('requestRealIp', $request->getIp());
195
        $this->assign('requestForwardedIp', $request->getForwardedIp());
196
197
        $trustedIpLocation = $this->getLocationProvider()->getIpLocation($trustedIp);
198
        $this->assign('requestTrustedIpLocation', $trustedIpLocation);
199
200
        $this->assign('requestHasForwardedIp', $request->getForwardedIp() !== null);
201
202
        $this->setupForwardedIpData($request);
203
    }
204
205
    /**
206
     * Adds related request data to Smarty. DO NOT USE WITHOUT FIRST CHECKING THAT THE USER IS AUTHORISED!
207
     *
208
     * @param Request           $request
209
     * @param SiteConfiguration $configuration
210
     * @param PdoDatabase       $database
211
     */
212
    protected function setupRelatedRequests(
213
        Request $request,
214
        SiteConfiguration $configuration,
215
        PdoDatabase $database)
216
    {
217
        $this->assign('canSeeRelatedRequests', true);
218
219
        // TODO: Do we want to return results from other domains?
220
        $relatedEmailRequests = RequestSearchHelper::get($database, null)
221
            ->byEmailAddress($request->getEmail())
222
            ->withConfirmedEmail()
223
            ->excludingPurgedData($configuration)
224
            ->excludingRequest($request->getId())
225
            ->fetch();
226
227
        $this->assign('requestRelatedEmailRequestsCount', count($relatedEmailRequests));
228
        $this->assign('requestRelatedEmailRequests', $relatedEmailRequests);
229
230
        $trustedIp = $this->getXffTrustProvider()->getTrustedClientIp($request->getIp(), $request->getForwardedIp());
231
232
        // TODO: Do we want to return results from other domains?
233
        $relatedIpRequests = RequestSearchHelper::get($database, null)
234
            ->byIp($trustedIp)
235
            ->withConfirmedEmail()
236
            ->excludingPurgedData($configuration)
237
            ->excludingRequest($request->getId())
238
            ->fetch();
239
240
        $this->assign('requestRelatedIpRequestsCount', count($relatedIpRequests));
241
        $this->assign('requestRelatedIpRequests', $relatedIpRequests);
242
    }
243
244
    /**
245
     * Adds checkuser request data to Smarty. DO NOT USE WITHOUT FIRST CHECKING THAT THE USER IS AUTHORISED!
246
     *
247
     * @param Request $request
248
     */
249
    protected function setupCheckUserData(Request $request)
250
    {
251
        $this->assign('requestUserAgent', $request->getUserAgent());
252
253
        $data = \Waca\DataObjects\RequestData::getForRequest($request->getId(), $request->getDatabase(), \Waca\DataObjects\RequestData::TYPE_CLIENTHINT);
254
        $this->assign('requestClientHints', $data);
255
    }
256
257
    /**
258
     * Sets up the basic data for this request, and adds it to Smarty
259
     *
260
     * @param Request           $request
261
     * @param SiteConfiguration $config
262
     */
263
    protected function setupBasicData(Request $request, SiteConfiguration $config)
0 ignored issues
show
The parameter $config is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

263
    protected function setupBasicData(Request $request, /** @scrutinizer ignore-unused */ SiteConfiguration $config)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
264
    {
265
        $this->assign('requestId', $request->getId());
266
        $this->assign('updateVersion', $request->getUpdateVersion());
267
        $this->assign('requestName', $request->getName());
268
        $this->assign('requestDate', $request->getDate());
269
        $this->assign('requestStatus', $request->getStatus());
270
271
        $this->assign('requestQueue', null);
272
        if ($request->getQueue() !== null) {
273
            /** @var RequestQueue $queue */
274
            $queue = RequestQueue::getById($request->getQueue(), $this->getDatabase());
0 ignored issues
show
It seems like getDatabase() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

274
            $queue = RequestQueue::getById($request->getQueue(), $this->/** @scrutinizer ignore-call */ getDatabase());
Loading history...
275
            $this->assign('requestQueue', $queue->getHeader());
276
            $this->assign('requestQueueApiName', $queue->getApiName());
277
        }
278
279
        $this->assign('canPreviewForm', $this->barrierTest('view', User::getCurrent($this->getDatabase()), PageRequestFormManagement::class));
280
        $this->assign('originForm', $request->getOriginFormObject());
281
282
        $isClosed = $request->getStatus() === RequestStatus::CLOSED || $request->getStatus() === RequestStatus::JOBQUEUE;
283
        $this->assign('requestIsClosed', $isClosed);
284
		$isHospital = $request->getStatus() === RequestStatus::HOSPITAL;
285
		$this->assign('requestIsHospital', $isHospital);
286
    }
287
288
    /**
289
     * Sets up the forwarded IP data for this request and adds it to Smarty
290
     *
291
     * @param Request $request
292
     */
293
    protected function setupForwardedIpData(Request $request)
294
    {
295
        if ($request->getForwardedIp() !== null) {
296
            $requestProxyData = array(); // Initialize array to store data to be output in Smarty template.
297
            $proxyIndex = 0;
298
299
            // Assuming [client] <=> [proxy1] <=> [proxy2] <=> [proxy3] <=> [us], we will see an XFF header of [client],
300
            // [proxy1], [proxy2], and our actual IP will be [proxy3]
301
            $proxies = explode(",", $request->getForwardedIp());
302
            $proxies[] = $request->getIp();
303
304
            // Origin is the supposed "client" IP.
305
            $origin = $proxies[0];
306
            $this->assign("forwardedOrigin", $origin);
307
308
            // We step through the servers in reverse order, from closest to furthest
309
            $proxies = array_reverse($proxies);
310
311
            // By default, we have trust, because the first in the chain is now REMOTE_ADDR, which is hardest to spoof.
312
            $trust = true;
313
314
            /**
315
             * @var int    $index     The zero-based index of the proxy.
316
             * @var string $proxyData The proxy IP address (although possibly not!)
317
             */
318
            foreach ($proxies as $index => $proxyData) {
319
                $proxyAddress = trim($proxyData);
320
                $requestProxyData[$proxyIndex]['ip'] = $proxyAddress;
321
                $requestProxyData[$proxyIndex]['protocol'] = $this->getIpProtocol($proxyAddress);
322
323
                // get data on this IP.
324
                $thisProxyIsTrusted = $this->getXffTrustProvider()->isTrusted($proxyAddress);
325
326
                $proxyIsInPrivateRange = $this->getXffTrustProvider()
327
                    ->ipInRange(self::$rfc1918ips, $proxyAddress);
328
329
                if (!$proxyIsInPrivateRange) {
330
                    $proxyReverseDns = $this->getRdnsProvider()->getReverseDNS($proxyAddress);
331
                    $proxyLocation = $this->getLocationProvider()->getIpLocation($proxyAddress);
332
                }
333
                else {
334
                    // this is going to fail, so why bother trying?
335
                    $proxyReverseDns = false;
336
                    $proxyLocation = false;
337
                }
338
339
                // current trust chain status BEFORE this link
340
                $preLinkTrust = $trust;
341
342
                // is *this* link trusted? Note, this will be true even if there is an untrusted link before this!
343
                $requestProxyData[$proxyIndex]['trustedlink'] = $thisProxyIsTrusted;
344
345
                // set the trust status of the chain to this point
346
                $trust = $trust & $thisProxyIsTrusted;
0 ignored issues
show
Are you sure you want to use the bitwise & or did you mean &&?
Loading history...
347
348
                // If this is the origin address, and the chain was trusted before this point, then we can trust
349
                // the origin.
350
                if ($preLinkTrust && $proxyAddress == $origin) {
351
                    // if this is the origin, then we are at the last point in the chain.
352
                    // @todo: this is probably the cause of some bugs when an IP appears twice - we're missing a check
353
                    // to see if this is *really* the last in the chain, rather than just the same IP as it.
354
                    $trust = true;
355
                }
356
357
                $requestProxyData[$proxyIndex]['trust'] = $trust;
358
359
                $requestProxyData[$proxyIndex]['rdnsfailed'] = $proxyReverseDns === false;
360
                $requestProxyData[$proxyIndex]['rdns'] = $proxyReverseDns;
361
                $requestProxyData[$proxyIndex]['routable'] = !$proxyIsInPrivateRange;
0 ignored issues
show
The condition $proxyIsInPrivateRange is always false.
Loading history...
362
363
                $requestProxyData[$proxyIndex]['location'] = $proxyLocation;
364
365
                if ($proxyReverseDns === $proxyAddress && $proxyIsInPrivateRange === false) {
366
                    $requestProxyData[$proxyIndex]['rdns'] = null;
367
                }
368
369
                $showLinks = (!$trust || $proxyAddress == $origin) && !$proxyIsInPrivateRange;
0 ignored issues
show
The condition $trust is always true.
Loading history...
370
                $requestProxyData[$proxyIndex]['showlinks'] = $showLinks;
371
372
                $proxyIndex++;
373
            }
374
375
            $this->assign("requestProxyData", $requestProxyData);
376
        }
377
    }
378
379
    private function getIpProtocol(string $ip): ?int
380
    {
381
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
382
            return 4;
383
        }
384
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
385
            return 6;
386
        }
387
388
        return null;
389
    }
390
}
391