Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
1 / 1
UserIpReader
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
2 / 2
10
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getUserIp
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
9
1<?php
2
3/**
4 * Service to retrieve user IP address.
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Villanova University 2020.
9 *
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License version 2,
12 * as published by the Free Software Foundation.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program; if not, write to the Free Software
21 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
22 *
23 * @category VuFind
24 * @package  Net
25 * @author   Demian Katz <demian.katz@villanova.edu>
26 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
27 * @link     https://vufind.org Main Page
28 */
29
30namespace VuFind\Net;
31
32use Laminas\Stdlib\Parameters;
33
34use function count;
35
36/**
37 * Service to retrieve user IP address.
38 *
39 * @category VuFind
40 * @package  Net
41 * @author   Demian Katz <demian.katz@villanova.edu>
42 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
43 * @link     https://vufind.org Main Page
44 */
45class UserIpReader
46{
47    /**
48     * Server parameters
49     *
50     * @var Parameters
51     */
52    protected $server;
53
54    /**
55     * Configuration specifying allowed HTTP headers containing IPs (false for none).
56     * See [Proxy] allow_forwarded_ips setting in config.ini for more details.
57     *
58     * @var string|bool
59     */
60    protected $allowForwardedIps;
61
62    /**
63     * IP addresses to exclude from consideration
64     *
65     * @var array
66     */
67    protected $ipFilter;
68
69    /**
70     * Constructor
71     *
72     * @param Parameters  $server            Server parameters
73     * @param string|bool $allowForwardedIps Forwarded header configuration string
74     * (false to disable checking IP-related X- headers)
75     * @param array       $ipFilter          IP addresses to exclude from
76     * consideration
77     */
78    public function __construct(
79        Parameters $server,
80        $allowForwardedIps = false,
81        array $ipFilter = []
82    ) {
83        $this->server = $server;
84        $this->allowForwardedIps = $allowForwardedIps;
85        $this->ipFilter = array_map('trim', $ipFilter);
86    }
87
88    /**
89     * Get the active user's IP address. Returns null if no address can be found.
90     *
91     * @return string
92     */
93    public function getUserIp()
94    {
95        if ($this->allowForwardedIps) {
96            foreach (explode(',', $this->allowForwardedIps) as $chunk) {
97                // Extract field and behavior from chunk:
98                [$field, $behavior] = explode(':', $chunk . ':', 2);
99
100                // Look up field value; skip if empty:
101                $fieldValue = $this->server->get($field);
102                if (empty($fieldValue)) {
103                    continue;
104                }
105
106                // Split up the field value, if it is delimited, then filter it:
107                $parts = array_diff(
108                    array_map('trim', explode(',', $fieldValue)),
109                    $this->ipFilter
110                );
111
112                // Apply the appropriate behavior (note that we trim any trailing
113                // colon off the behavior, since we may have added one above to
114                // prevent warnings in the explode operation):
115                //
116                // Also note that we need to use array_shift/array_pop/current here
117                // in place of specific indexes, because the filtering above may have
118                // left non-consecutive keys in place.
119                $finalBehavior = strtolower(rtrim($behavior, ':'));
120                $partCount = count($parts);
121                if ($finalBehavior === 'first' && $partCount > 0) {
122                    return array_shift($parts);
123                } elseif ($finalBehavior === 'last' && $partCount > 0) {
124                    return array_pop($parts);
125                } elseif ($partCount === 1) {
126                    return current($parts);
127                }
128            }
129        }
130        // Default case: use REMOTE_ADDR directly.
131        return $this->server->get('REMOTE_ADDR');
132    }
133}