Improve parameter validation error messages
[phancap.git] / src / phancap / Options.php
1 <?php
2 /**
3  * Part of phancap
4  *
5  * PHP version 5
6  *
7  * @category  Tools
8  * @package   Options
9  * @author    Christian Weiske <cweiske@cweiske.de>
10  * @copyright 2014 Christian Weiske
11  * @license   http://www.gnu.org/licenses/agpl.html GNU AGPL v3
12  * @link      http://cweiske.de/phancap.htm
13  */
14 namespace phancap;
15
16 /**
17  * Options a user can give to the API
18  *
19  * @category  Tools
20  * @package   Options
21  * @author    Christian Weiske <cweiske@cweiske.de>
22  * @copyright 2014 Christian Weiske
23  * @license   http://www.gnu.org/licenses/agpl.html GNU AGPL v3
24  * @version   Release: @package_version@
25  * @link      http://cweiske.de/phancap.htm
26  */
27 class Options
28 {
29     /**
30      * Available options and their configuration
31      *
32      * @var array
33      */
34     public $options = array(
35         /**
36          * Browser settings
37          */
38         'url' => array(
39             'title'     => 'Website URL',
40             'default'   => null,
41             'type'      => 'url',
42             'required'  => true,
43         ),
44         'bwidth' => array(
45             'title'   => 'Browser width',
46             'default' => 1024,
47             'type'    => 'int',
48             'min'     => 16,
49             'max'     => 2048,
50         ),
51         'bheight' => array(
52             'title'   => 'Browser height',
53             'default' => null,
54             'type'    => 'int',
55             'min'     => 16,
56             'max'     => 8192,
57         ),
58         /**
59          * Screenshot settings
60          */
61         'swidth' => array(
62             'title'   => 'Screenshot width',
63             'default' => null,
64             'type'    => 'int',
65             'min'     => 16,
66             'max'     => 8192,
67         ),
68         'sheight' => array(
69             'title'   => 'Screenshot height',
70             'default' => null,
71             'type'    => 'int',
72             'min'     => 16,
73             'max'     => 8192,
74         ),
75         'sformat' => array(
76             'title'   => 'Screenshot format',
77             'default' => 'png',
78             'type'    => array('png', 'jpg', 'pdf'),
79         ),
80         'smode' => array(
81             'title'   => 'Screenshot mode',
82             'default' => 'screen',
83             'type'    => array('screen', 'page'),
84         ),
85         'smaxage' => array(
86             'title'   => 'Maximum age for a screenshot',
87             'default' => null,
88             'type'    => 'age',
89             'min'     => null,
90         ),
91         /**
92          * Authentication
93          */
94         'atimestamp' => array(
95             'title'   => 'Timestamp the request has been generated',
96             'default' => null,
97             'type'    => 'skip',
98         ),
99         'atoken' => array(
100             'title'   => 'Access token (user name)',
101             'default' => null,
102             'type'    => 'skip',
103         ),
104         'asignature' => array(
105             'title'   => 'Access signature',
106             'default' => null,
107             'type'    => 'skip',
108         ),
109     );
110
111     /**
112      * Actual values we use after parsing the GET parameters
113      *
114      * @var array
115      */
116     public $values = array();
117
118     /**
119      * @var Config
120      */
121     protected $config;
122
123
124     /**
125      * Parses an array of options, validates them and writes them into
126      * $this->values.
127      *
128      * @param array $arValues Array of options, e.g. $_GET
129      *
130      * @return void
131      * @throws \InvalidArgumentException When required parameters are missing
132      *         or parameter values are invalid.
133      */
134     public function parse($arValues)
135     {
136         foreach ($this->options as $name => $arOption) {
137             $this->values[$name] = $arOption['default'];
138             if (!isset($arValues[$name])) {
139                 if (isset($arOption['required'])) {
140                     throw new \InvalidArgumentException(
141                         $name . ' parameter missing'
142                     );
143                 }
144                 continue;
145             }
146
147             if ($arValues[$name] === ''
148                 && !isset($arOption['required'])
149             ) {
150                 //allow empty value; default value will be used
151             } else if ($arOption['type'] == 'url') {
152                 $this->values[$name] = $this->validateUrl($arValues[$name]);
153             } else if ($arOption['type'] == 'int') {
154                 $this->values[$name] = $this->validateInt(
155                     $name, $arValues[$name],
156                     $arOption['min'], $arOption['max']
157                 );
158             } else if (gettype($arOption['type']) == 'array') {
159                 $this->values[$name] = $this->validateArray(
160                     $name, $arValues[$name], $arOption['type']
161                 );
162             } else if ($arOption['type'] == 'age') {
163                 $this->values[$name] = $this->clamp(
164                     static::validateAge($arValues[$name]),
165                     $arOption['min'], null,
166                     true
167                 );
168             } else if ($arOption['type'] != 'skip') {
169                 throw new \InvalidArgumentException(
170                     'Unsupported option type: ' . $arOption['type']
171                 );
172             }
173             unset($arValues[$name]);
174         }
175
176         if (count($arValues) > 0) {
177             throw new \InvalidArgumentException(
178                 'Unsupported parameter: ' . implode(', ', array_keys($arValues))
179             );
180         }
181
182         $this->calcPageSize();
183     }
184
185     /**
186      * Calculate the browser size and screenshot size from the given options
187      *
188      * @return void
189      */
190     protected function calcPageSize()
191     {
192         if ($this->values['swidth'] === null) {
193             $this->values['swidth'] = $this->values['bwidth'];
194         }
195         if ($this->values['smode'] == 'page') {
196             return;
197         }
198
199         if ($this->values['sheight'] !== null) {
200             $this->values['bheight'] = intval(
201                 $this->values['bwidth'] / $this->values['swidth']
202                 * $this->values['sheight']
203             );
204         } else if ($this->values['bheight'] !== null) {
205             $this->values['sheight'] = intval(
206                 $this->values['swidth'] / $this->values['bwidth']
207                 * $this->values['bheight']
208             );
209         } else {
210             //no height set. use 4:3
211             $this->values['sheight'] = $this->values['swidth'] / 4 * 3;
212             $this->values['bheight'] = $this->values['bwidth'] / 4 * 3;
213         }
214     }
215
216     /**
217      * Makes sure a value is between $min and $max (inclusive)
218      *
219      * @param integer $value  Value to check
220      * @param integer $min    Minimum allowed value
221      * @param integer $max    Maximum allowed value
222      * @param boolean $silent When silent, invalid values are corrected.
223      *                        An exception is thrown otherwise.
224      *
225      * @return integer Corrected value
226      * @throws \InvalidArgumentException When not silent and value outside range
227      */
228     protected function clamp($value, $min, $max, $silent = false)
229     {
230         if ($min !== null && $value < $min) {
231             if ($silent) {
232                 $value = $min;
233             } else {
234                 throw new \InvalidArgumentException(
235                     'Value must be at least ' . $min
236                 );
237             }
238         }
239         if ($max !== null && $value > $max) {
240             if ($silent) {
241                 $value = $max;
242             } else {
243                 throw new \InvalidArgumentException(
244                     'Value may be up to ' . $min
245                 );
246             }
247         }
248         return $value;
249     }
250
251     /**
252      * Validates an age is numeric. If it is not numeric, it's interpreted as
253      * a ISO 8601 duration specification.
254      *
255      * @param string $value Age in seconds
256      *
257      * @return integer Age in seconds
258      * @throws \InvalidArgumentException
259      * @link   http://en.wikipedia.org/wiki/Iso8601#Durations
260      */
261     public static function validateAge($value)
262     {
263         if (!is_numeric($value)) {
264             //convert short notation to seconds
265             $value = 'P' . ltrim(strtoupper($value), 'P');
266             try {
267                 $interval = new \DateInterval($value);
268             } catch (\Exception $e) {
269                 throw new \InvalidArgumentException(
270                     'Invalid age: ' . $value
271                 );
272             }
273             $value = 86400 * (
274                 $interval->y * 365
275                 + $interval->m * 30
276                 + $interval->d
277             ) + $interval->h * 3600
278                 + $interval->m * 60
279                 + $interval->s;
280         }
281         return $value;
282     }
283
284     /**
285      * Check that a given value exists in an array
286      *
287      * @param string $name    Variable name
288      * @param string $value   Value to check
289      * @param array  $options Array of allowed values
290      *
291      * @return string Value
292      * @throws \InvalidArgumentException If the value does not exist in $options
293      */
294     protected function validateArray($name, $value, $options)
295     {
296         if (array_search($value, $options) === false) {
297             throw new \InvalidArgumentException(
298                 'Invalid value ' . $value . ' for ' . $name . '.'
299                 . ' Allowed: ' . implode(', ', $options)
300             );
301         }
302         return $value;
303     }
304
305     /**
306      * Validate that a value is numeric and between $min and $max (inclusive)
307      *
308      * @param string  $name  Variable name
309      * @param string  $value Value to check
310      * @param integer $min   Minimum allowed value
311      * @param integer $max   Maximum allowed value
312      *
313      * @return integer Value as integer
314      * @throws \InvalidArgumentException When outside range or not numeric
315      */
316     protected function validateInt($name, $value, $min, $max)
317     {
318         if (!is_numeric($value)) {
319             throw new \InvalidArgumentException(
320                 $name . ' value must be a number'
321             );
322         }
323         $value = (int) $value;
324         return $this->clamp($value, $min, $max);
325     }
326
327     /**
328      * Validate (and fix) an URL
329      *
330      * @param string $url URL
331      *
332      * @return string Fixed URL
333      * @throws \InvalidArgumentException
334      */
335     protected function validateUrl($url)
336     {
337         if ($url === '') {
338             throw new \InvalidArgumentException('URL is empty');
339         }
340         $parts = parse_url($url);
341         if ($parts === false) {
342             throw new \InvalidArgumentException('Invalid URL');
343         }
344         if (!isset($parts['scheme'])) {
345             $url = 'http://' . $url;
346             $parts = parse_url($url);
347         } else if ($parts['scheme'] != 'http' && $parts['scheme'] != 'https') {
348             throw new \InvalidArgumentException('Unsupported protocol');
349         }
350         if (!isset($parts['host'])) {
351             throw new \InvalidArgumentException('URL host missing');
352         }
353         return $url;
354     }
355
356     /**
357      * Set phancap configuration
358      *
359      * @param Config $config Phancap configuration
360      *
361      * @return void
362      */
363     public function setConfig(Config $config)
364     {
365         $this->config = $config;
366         $this->options['smaxage']['default'] = $this->config->screenshotMaxAge;
367         $this->options['smaxage']['min']     = $this->config->screenshotMinAge;
368     }
369 }
370 ?>