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
12class Isbn extends AbstractValidator
13{
14    const AUTO    = 'auto';
15    const ISBN10  = '10';
16    const ISBN13  = '13';
17    const INVALID = 'isbnInvalid';
18    const NO_ISBN = 'isbnNoIsbn';
19
20    /**
21     * Validation failure message template definitions.
22     *
23     * @var array
24     */
25    protected $messageTemplates = array(
26        self::INVALID => "Invalid type given. String or integer expected",
27        self::NO_ISBN => "The input is not a valid ISBN number",
28    );
29
30    protected $options = array(
31        'type'      => self::AUTO, // Allowed type
32        'separator' => '',         // Separator character
33    );
34
35    /**
36     * Detect input format.
37     *
38     * @return string
39     */
40    protected function detectFormat()
41    {
42        // prepare separator and pattern list
43        $sep      = quotemeta($this->getSeparator());
44        $patterns = array();
45        $lengths  = array();
46        $type     = $this->getType();
47
48        // check for ISBN-10
49        if ($type == self::ISBN10 || $type == self::AUTO) {
50            if (empty($sep)) {
51                $pattern = '/^[0-9]{9}[0-9X]{1}$/';
52                $length  = 10;
53            } else {
54                $pattern = "/^[0-9]{1,7}[{$sep}]{1}[0-9]{1,7}[{$sep}]{1}[0-9]{1,7}[{$sep}]{1}[0-9X]{1}$/";
55                $length  = 13;
56            }
57
58            $patterns[$pattern] = self::ISBN10;
59            $lengths[$pattern]  = $length;
60        }
61
62        // check for ISBN-13
63        if ($type == self::ISBN13 || $type == self::AUTO) {
64            if (empty($sep)) {
65                $pattern = '/^[0-9]{13}$/';
66                $length  = 13;
67            } else {
68                $pattern = "/^[0-9]{1,9}[{$sep}]{1}[0-9]{1,5}[{$sep}]{1}[0-9]{1,9}[{$sep}]{1}[0-9]{1,9}[{$sep}]{1}[0-9]{1}$/";
69                $length  = 17;
70            }
71
72            $patterns[$pattern] = self::ISBN13;
73            $lengths[$pattern]  = $length;
74        }
75
76        // check pattern list
77        foreach ($patterns as $pattern => $type) {
78            if ((strlen($this->getValue()) == $lengths[$pattern]) && preg_match($pattern, $this->getValue())) {
79                return $type;
80            }
81        }
82
83        return;
84    }
85
86    /**
87     * Returns true if and only if $value is a valid ISBN.
88     *
89     * @param  string $value
90     * @return bool
91     */
92    public function isValid($value)
93    {
94        if (!is_string($value) && !is_int($value)) {
95            $this->error(self::INVALID);
96            return false;
97        }
98
99        $value = (string) $value;
100        $this->setValue($value);
101
102        switch ($this->detectFormat()) {
103            case self::ISBN10:
104                // sum
105                $isbn10 = str_replace($this->getSeparator(), '', $value);
106                $sum    = 0;
107                for ($i = 0; $i < 9; $i++) {
108                    $sum += (10 - $i) * $isbn10{$i};
109                }
110
111                // checksum
112                $checksum = 11 - ($sum % 11);
113                if ($checksum == 11) {
114                    $checksum = '0';
115                } elseif ($checksum == 10) {
116                    $checksum = 'X';
117                }
118                break;
119
120            case self::ISBN13:
121                // sum
122                $isbn13 = str_replace($this->getSeparator(), '', $value);
123                $sum    = 0;
124                for ($i = 0; $i < 12; $i++) {
125                    if ($i % 2 == 0) {
126                        $sum += $isbn13{$i};
127                    } else {
128                        $sum += 3 * $isbn13{$i};
129                    }
130                }
131                // checksum
132                $checksum = 10 - ($sum % 10);
133                if ($checksum == 10) {
134                    $checksum = '0';
135                }
136                break;
137
138            default:
139                $this->error(self::NO_ISBN);
140                return false;
141        }
142
143        // validate
144        if (substr($this->getValue(), -1) != $checksum) {
145            $this->error(self::NO_ISBN);
146            return false;
147        }
148        return true;
149    }
150
151    /**
152     * Set separator characters.
153     *
154     * It is allowed only empty string, hyphen and space.
155     *
156     * @param  string $separator
157     * @throws Exception\InvalidArgumentException When $separator is not valid
158     * @return Isbn Provides a fluent interface
159     */
160    public function setSeparator($separator)
161    {
162        // check separator
163        if (!in_array($separator, array('-', ' ', ''))) {
164            throw new Exception\InvalidArgumentException('Invalid ISBN separator.');
165        }
166
167        $this->options['separator'] = $separator;
168        return $this;
169    }
170
171    /**
172     * Get separator characters.
173     *
174     * @return string
175     */
176    public function getSeparator()
177    {
178        return $this->options['separator'];
179    }
180
181    /**
182     * Set allowed ISBN type.
183     *
184     * @param  string $type
185     * @throws Exception\InvalidArgumentException When $type is not valid
186     * @return Isbn Provides a fluent interface
187     */
188    public function setType($type)
189    {
190        // check type
191        if (!in_array($type, array(self::AUTO, self::ISBN10, self::ISBN13))) {
192            throw new Exception\InvalidArgumentException('Invalid ISBN type');
193        }
194
195        $this->options['type'] = $type;
196        return $this;
197    }
198
199    /**
200     * Get allowed ISBN type.
201     *
202     * @return string
203     */
204    public function getType()
205    {
206        return $this->options['type'];
207    }
208}
209