Completed
Pull Request — master (#430)
by Anton
04:21
created

IpRule::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
1
<?php
2
/**
3
 * Bluz Framework Component
4
 *
5
 * @copyright Bluz PHP Team
6
 * @link      https://github.com/bluzphp/framework
7
 */
8
9
namespace Bluz\Validator\Rule;
10
11
use Bluz\Validator\Exception\ComponentException;
12
13
/**
14
 * Check for IP
15
 *
16
 * Strict mode disabled for this file, because function long2ip() was changed in PHP 7.1
17
 *
18
 * @package Bluz\Validator\Rule
19
 */
20
class IpRule extends AbstractRule
21
{
22
    /**
23
     * @var integer setup options
24
     */
25
    protected $options;
26
27
    /**
28
     * @var array network range
29
     */
30
    protected $networkRange;
31
32
    /**
33
     * Setup validation rule
34
     *
35
     * @param mixed $options
36
     *
37
     * @throws \Bluz\Validator\Exception\ComponentException
38
     */
39
    public function __construct($options = null)
40
    {
41
        if (is_int($options)) {
42
            $this->options = $options;
43
            return;
44
        }
45
46
        $this->networkRange = $this->parseRange($options);
47
    }
48
49
    /**
50
     * Parse IP range
51
     *
52
     * @param  string $input
53
     *
54
     * @return array
55
     * @throws \Bluz\Validator\Exception\ComponentException
56
     */
57
    protected function parseRange($input)
58
    {
59
        if ($input === null || $input === '*' || $input === '*.*.*.*'
60
            || $input === '0.0.0.0-255.255.255.255'
61
        ) {
62
            return null;
63
        }
64
65
        $range = ['min' => null, 'max' => null, 'mask' => null];
66
67
        if (strpos($input, '-') !== false) {
68
            list($range['min'], $range['max']) = explode('-', $input);
69
        } elseif (strpos($input, '*') !== false) {
70
            $this->parseRangeUsingWildcards($input, $range);
71
        } elseif (strpos($input, '/') !== false) {
72
            $this->parseRangeUsingCidr($input, $range);
73
        } else {
74
            throw new ComponentException('Invalid network range');
75
        }
76
77
        if (!$this->verifyAddress($range['min'])) {
78
            throw new ComponentException('Invalid network range');
79
        }
80
81
        if (isset($range['max']) && !$this->verifyAddress($range['max'])) {
82
            throw new ComponentException('Invalid network range');
83
        }
84
85
        return $range;
86
    }
87
88
    /**
89
     * Fill address
90
     *
91
     * @param string $input
92
     * @param string $char
93
     */
94
    protected function fillAddress(&$input, $char = '*')
95
    {
96
        while (substr_count($input, '.') < 3) {
97
            $input .= '.' . $char;
98
        }
99
    }
100
101
    /**
102
     * Parse range using wildcards
103
     *
104
     * @param string $input
105
     * @param array  $range
106
     */
107
    protected function parseRangeUsingWildcards($input, &$range)
108
    {
109
        $this->fillAddress($input);
110
111
        $range['min'] = strtr($input, '*', '0');
112
        $range['max'] = str_replace('*', '255', $input);
113
    }
114
115
    /**
116
     * Parse range using Classless Inter-Domain Routing (CIDR)
117
     *
118
     * @param  string $input
119
     * @param  array  $range
120
     *
121
     * @throws \Bluz\Validator\Exception\ComponentException
122
     * @link http://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing
123
     */
124
    protected function parseRangeUsingCidr($input, &$range)
125
    {
126
        $input = explode('/', $input);
127
        $this->fillAddress($input[0], '0');
128
129
        $range['min'] = $input[0];
130
        $isAddressMask = strpos($input[1], '.') !== false;
131
132
        if ($isAddressMask && $this->verifyAddress($input[1])) {
133
            $range['mask'] = sprintf('%032b', ip2long($input[1]));
134
            return;
135
        }
136
137
        if ($isAddressMask || $input[1] < 8 || $input[1] > 30) {
138
            throw new ComponentException('Invalid network mask');
139
        }
140
141
        $range['mask'] = sprintf('%032b', ip2long(long2ip(~(pow(2, (32 - $input[1])) - 1))));
142
    }
143
144
    /**
145
     * Check input data
146
     *
147
     * @param  string $input
148
     *
149
     * @return bool
150
     */
151
    public function validate($input): bool
152
    {
153
        return $this->verifyAddress($input) && $this->verifyNetwork($input);
154
    }
155
156
    /**
157
     * Verify IP address
158
     *
159
     * @param  string $address
160
     *
161
     * @return bool
162
     */
163
    protected function verifyAddress($address)
164
    {
165
        return (boolean)filter_var(
166
            $address,
167
            FILTER_VALIDATE_IP,
168
            [
169
                'flags' => $this->options
170
            ]
171
        );
172
    }
173
174
    /**
175
     * Verify Network by mask
176
     *
177
     * @param  string $input
178
     *
179
     * @return bool
180
     */
181
    protected function verifyNetwork($input)
182
    {
183
        if ($this->networkRange === null) {
184
            return true;
185
        }
186
187
        if (isset($this->networkRange['mask'])) {
188
            return $this->belongsToSubnet($input);
189
        }
190
191
        $input = sprintf('%u', ip2long($input));
192
193
        $min = sprintf('%u', ip2long($this->networkRange['min']));
194
        $max = sprintf('%u', ip2long($this->networkRange['max']));
195
196
        return ($input >= $min) && ($input <= $max);
197
    }
198
199
    /**
200
     * Check subnet
201
     *
202
     * @param  string $input
203
     *
204
     * @return bool
205
     */
206
    protected function belongsToSubnet($input)
207
    {
208
        $range = $this->networkRange;
209
        $min = sprintf('%032b', ip2long($range['min']));
210
        $input = sprintf('%032b', ip2long($input));
211
212
        return ($input & $range['mask']) === ($min & $range['mask']);
213
    }
214
215
    /**
216
     * Get error template
217
     *
218
     * @return string
219
     */
220
    public function getDescription() : string
221
    {
222
        if (!empty($this->networkRange)) {
223
            $message = $this->networkRange['min'];
224
            if (isset($this->networkRange['max'])) {
225
                $message .= '-' . $this->networkRange['max'];
226
            } else {
227
                $message .= '/' . long2ip((string)bindec($this->networkRange['mask']));
228
            }
229
            return __('must be an IP address in the "%s" range', $message);
230
        }
231
        return __('must be an IP address');
232
    }
233
}
234