Issues (186)

Branch: master

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 8
     * @param Request $request
71
     * @param User    $currentUser
72
     *
73 8
     * @return bool
74
     * @category Security-Critical
75 1
     */
76
    protected function isAllowedPrivateData(Request $request, User $currentUser)
77
    {
78
        // Test the main security barrier for private data access using SecurityManager
79 7
        if ($this->barrierTest('alwaysSeePrivateData', $currentUser, 'RequestData')) {
80 7
            // Tool admins/check-users can always see private data
81 7
            return true;
82
        }
83 1
84
        // reserving user is allowed to see the data
85
        if ($currentUser->getId() === $request->getReserved()
86
            && $request->getReserved() !== null
87 6
            && $this->barrierTest('seePrivateDataWhenReserved', $currentUser, 'RequestData')
88 6
        ) {
89
            return true;
90 1
        }
91
92
        // user has the reveal hash
93
        if (WebRequest::getString('hash') === $request->getRevealHash()
94 5
            && $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('requestRealIp', $request->getIp());
194
        $this->assign('requestForwardedIp', $request->getForwardedIp());
195
196
        $trustedIpLocation = $this->getLocationProvider()->getIpLocation($trustedIp);
197
        $this->assign('requestTrustedIpLocation', $trustedIpLocation);
198
199
        $this->assign('requestHasForwardedIp', $request->getForwardedIp() !== null);
200
201
        $this->setupForwardedIpData($request);
202
    }
203
204
    /**
205
     * Adds related request data to Smarty. DO NOT USE WITHOUT FIRST CHECKING THAT THE USER IS AUTHORISED!
206
     *
207
     * @param Request           $request
208
     * @param SiteConfiguration $configuration
209
     * @param PdoDatabase       $database
210
     */
211
    protected function setupRelatedRequests(
212
        Request $request,
213
        SiteConfiguration $configuration,
214
        PdoDatabase $database)
215
    {
216
        $this->assign('canSeeRelatedRequests', true);
217
218
        // TODO: Do we want to return results from other domains?
219
        $relatedEmailRequests = RequestSearchHelper::get($database, null)
220
            ->byEmailAddress($request->getEmail())
221
            ->withConfirmedEmail()
222
            ->excludingPurgedData($configuration)
223
            ->excludingRequest($request->getId())
224
            ->fetch();
225
226
        $this->assign('requestRelatedEmailRequestsCount', count($relatedEmailRequests));
227
        $this->assign('requestRelatedEmailRequests', $relatedEmailRequests);
228
229
        $trustedIp = $this->getXffTrustProvider()->getTrustedClientIp($request->getIp(), $request->getForwardedIp());
230
231
        // TODO: Do we want to return results from other domains?
232
        $relatedIpRequests = RequestSearchHelper::get($database, null)
233
            ->byIp($trustedIp)
234
            ->withConfirmedEmail()
235
            ->excludingPurgedData($configuration)
236
            ->excludingRequest($request->getId())
237
            ->fetch();
238
239
        $this->assign('requestRelatedIpRequestsCount', count($relatedIpRequests));
240
        $this->assign('requestRelatedIpRequests', $relatedIpRequests);
241
    }
242
243
    /**
244
     * Adds checkuser request data to Smarty. DO NOT USE WITHOUT FIRST CHECKING THAT THE USER IS AUTHORISED!
245
     *
246
     * @param Request $request
247
     */
248
    protected function setupCheckUserData(Request $request)
249
    {
250
        $this->assign('requestUserAgent', $request->getUserAgent());
251
252
        $data = \Waca\DataObjects\RequestData::getForRequest($request->getId(), $request->getDatabase(), \Waca\DataObjects\RequestData::TYPE_CLIENTHINT);
253
        $this->assign('requestClientHints', $data);
254
    }
255
256
    /**
257
     * Sets up the basic data for this request, and adds it to Smarty
258
     *
259
     * @param Request           $request
260
     * @param SiteConfiguration $config
261
     */
262
    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

262
    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...
263
    {
264
        $this->assign('requestId', $request->getId());
265 6
        $this->assign('updateVersion', $request->getUpdateVersion());
266
        $this->assign('requestName', $request->getName());
267 6
        $this->assign('requestDate', $request->getDate());
268 5
        $this->assign('requestStatus', $request->getStatus());
269 5
270
        $this->assign('requestQueue', null);
271
        if ($request->getQueue() !== null) {
272
            /** @var RequestQueue $queue */
273 5
            $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

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