1<?php
2/**
3 * Zend Framework (http://framework.zend.com/)
4 *
5 * @link      http://github.com/zendframework/zf2 for the canonical source repository
6 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
7 * @license   http://framework.zend.com/license/new-bsd New BSD License
8 */
9
10namespace Zend\Validator;
11
12use Traversable;
13
14class Ip extends AbstractValidator
15{
16    const INVALID        = 'ipInvalid';
17    const NOT_IP_ADDRESS = 'notIpAddress';
18
19    /**
20     * @var array
21     */
22    protected $messageTemplates = array(
23        self::INVALID        => 'Invalid type given. String expected',
24        self::NOT_IP_ADDRESS => "The input does not appear to be a valid IP address",
25    );
26
27    /**
28     * Internal options
29     *
30     * @var array
31     */
32    protected $options = array(
33        'allowipv4'      => true, // Enable IPv4 Validation
34        'allowipv6'      => true, // Enable IPv6 Validation
35        'allowipvfuture' => false, // Enable IPvFuture Validation
36        'allowliteral'   => true, // Enable IPs in literal format (only IPv6 and IPvFuture)
37    );
38
39    /**
40     * Sets the options for this validator
41     *
42     * @param array|Traversable $options
43     * @throws Exception\InvalidArgumentException If there is any kind of IP allowed or $options is not an array or Traversable.
44     * @return AbstractValidator
45     */
46    public function setOptions($options = array())
47    {
48        parent::setOptions($options);
49
50        if (!$this->options['allowipv4'] && !$this->options['allowipv6'] && !$this->options['allowipvfuture']) {
51            throw new Exception\InvalidArgumentException('Nothing to validate. Check your options');
52        }
53
54        return $this;
55    }
56
57    /**
58     * Returns true if and only if $value is a valid IP address
59     *
60     * @param  mixed $value
61     * @return bool
62     */
63    public function isValid($value)
64    {
65        if (!is_string($value)) {
66            $this->error(self::INVALID);
67            return false;
68        }
69
70        $this->setValue($value);
71
72        if ($this->options['allowipv4'] && $this->validateIPv4($value)) {
73            return true;
74        } else {
75            if ((bool) $this->options['allowliteral']) {
76                static $regex = '/^\[(.*)\]$/';
77                if ((bool) preg_match($regex, $value, $matches)) {
78                    $value = $matches[1];
79                }
80            }
81
82            if (($this->options['allowipv6'] && $this->validateIPv6($value)) ||
83                ($this->options['allowipvfuture'] && $this->validateIPvFuture($value))
84            ) {
85                return true;
86            }
87        }
88        $this->error(self::NOT_IP_ADDRESS);
89        return false;
90    }
91
92    /**
93     * Validates an IPv4 address
94     *
95     * @param string $value
96     * @return bool
97     */
98    protected function validateIPv4($value)
99    {
100        if (preg_match('/^([01]{8}.){3}[01]{8}\z/i', $value)) {
101            // binary format  00000000.00000000.00000000.00000000
102            $value = bindec(substr($value, 0, 8)) . '.' . bindec(substr($value, 9, 8)) . '.'
103                   . bindec(substr($value, 18, 8)) . '.' . bindec(substr($value, 27, 8));
104        } elseif (preg_match('/^([0-9]{3}.){3}[0-9]{3}\z/i', $value)) {
105            // octet format 777.777.777.777
106            $value = (int) substr($value, 0, 3) . '.' . (int) substr($value, 4, 3) . '.'
107                   . (int) substr($value, 8, 3) . '.' . (int) substr($value, 12, 3);
108        } elseif (preg_match('/^([0-9a-f]{2}.){3}[0-9a-f]{2}\z/i', $value)) {
109            // hex format ff.ff.ff.ff
110            $value = hexdec(substr($value, 0, 2)) . '.' . hexdec(substr($value, 3, 2)) . '.'
111                   . hexdec(substr($value, 6, 2)) . '.' . hexdec(substr($value, 9, 2));
112        }
113
114        $ip2long = ip2long($value);
115        if ($ip2long === false) {
116            return false;
117        }
118
119        return ($value == long2ip($ip2long));
120    }
121
122    /**
123     * Validates an IPv6 address
124     *
125     * @param  string $value Value to check against
126     * @return bool True when $value is a valid ipv6 address
127     *                 False otherwise
128     */
129    protected function validateIPv6($value)
130    {
131        if (strlen($value) < 3) {
132            return $value == '::';
133        }
134
135        if (strpos($value, '.')) {
136            $lastcolon = strrpos($value, ':');
137            if (!($lastcolon && $this->validateIPv4(substr($value, $lastcolon + 1)))) {
138                return false;
139            }
140
141            $value = substr($value, 0, $lastcolon) . ':0:0';
142        }
143
144        if (strpos($value, '::') === false) {
145            return preg_match('/\A(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}\z/i', $value);
146        }
147
148        $colonCount = substr_count($value, ':');
149        if ($colonCount < 8) {
150            return preg_match('/\A(?::|(?:[a-f0-9]{1,4}:)+):(?:(?:[a-f0-9]{1,4}:)*[a-f0-9]{1,4})?\z/i', $value);
151        }
152
153        // special case with ending or starting double colon
154        if ($colonCount == 8) {
155            return preg_match('/\A(?:::)?(?:[a-f0-9]{1,4}:){6}[a-f0-9]{1,4}(?:::)?\z/i', $value);
156        }
157
158        return false;
159    }
160
161    /**
162     * Validates an IPvFuture address.
163     *
164     * IPvFuture is loosely defined in the Section 3.2.2 of RFC 3986
165     *
166     * @param  string $value Value to check against
167     * @return bool True when $value is a valid IPvFuture address
168     *                 False otherwise
169     */
170    protected function validateIPvFuture($value)
171    {
172        /*
173         * ABNF:
174         * IPvFuture  = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" )
175         * unreserved    = ALPHA / DIGIT / "-" / "." / "_" / "~"
176         * sub-delims    = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / ","
177         *               / ";" / "="
178         */
179        static $regex = '/^v([[:xdigit:]]+)\.[[:alnum:]\-\._~!\$&\'\(\)\*\+,;=:]+$/';
180
181        $result = (bool) preg_match($regex, $value, $matches);
182
183        /*
184         * "As such, implementations must not provide the version flag for the
185         *  existing IPv4 and IPv6 literal address forms described below."
186         */
187        return ($result && $matches[1] != 4 && $matches[1] != 6);
188    }
189}
190