Allow empty parameter values
[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                     $arValues[$name], $arOption['min'], $arOption['max']
156                 );
157             } else if (gettype($arOption['type']) == 'array') {
158                 $this->values[$name] = $this->validateArray(
159                     $arValues[$name], $arOption['type']
160                 );
161             } else if ($arOption['type'] == 'age') {
162                 $this->values[$name] = $this->clamp(
163                     static::validateAge($arValues[$name]),
164                     $arOption['min'], null,
165                     true
166                 );
167             } else if ($arOption['type'] != 'skip') {
168                 throw new \InvalidArgumentException(
169                     'Unsupported option type: ' . $arOption['type']
170                 );
171             }
172             unset($arValues[$name]);
173         }
174
175         if (count($arValues) > 0) {
176             throw new \InvalidArgumentException(
177                 'Unsupported parameter: ' . implode(', ', array_keys($arValues))
178             );
179         }
180
181         $this->calcPageSize();
182     }
183
184     /**
185      * Calculate the browser size and screenshot size from the given options
186      *
187      * @return void
188      */
189     protected function calcPageSize()
190     {
191         if ($this->values['swidth'] === null) {
192             $this->values['swidth'] = $this->values['bwidth'];
193         }
194         if ($this->values['smode'] == 'page') {
195             return;
196         }
197
198         if ($this->values['sheight'] !== null) {
199             $this->values['bheight'] = intval(
200                 $this->values['bwidth'] / $this->values['swidth']
201                 * $this->values['sheight']
202             );
203         } else if ($this->values['bheight'] !== null) {
204             $this->values['sheight'] = intval(
205                 $this->values['swidth'] / $this->values['bwidth']
206                 * $this->values['bheight']
207             );
208         } else {
209             //no height set. use 4:3
210             $this->values['sheight'] = $this->values['swidth'] / 4 * 3;
211             $this->values['bheight'] = $this->values['bwidth'] / 4 * 3;
212         }
213     }
214
215     /**
216      * Makes sure a value is between $min and $max (inclusive)
217      *
218      * @param integer $value  Value to check
219      * @param integer $min    Minimum allowed value
220      * @param integer $max    Maximum allowed value
221      * @param boolean $silent When silent, invalid values are corrected.
222      *                        An exception is thrown otherwise.
223      *
224      * @return integer Corrected value
225      * @throws \InvalidArgumentException When not silent and value outside range
226      */
227     protected function clamp($value, $min, $max, $silent = false)
228     {
229         if ($min !== null && $value < $min) {
230             if ($silent) {
231                 $value = $min;
232             } else {
233                 throw new \InvalidArgumentException(
234                     'Value must be at least ' . $min
235                 );
236             }
237         }
238         if ($max !== null && $value > $max) {
239             if ($silent) {
240                 $value = $max;
241             } else {
242                 throw new \InvalidArgumentException(
243                     'Value may be up to ' . $min
244                 );
245             }
246         }
247         return $value;
248     }
249
250     /**
251      * Validates an age is numeric. If it is not numeric, it's interpreted as
252      * a ISO 8601 duration specification.
253      *
254      * @param string $value Age in seconds
255      *
256      * @return integer Age in seconds
257      * @throws \InvalidArgumentException
258      * @link   http://en.wikipedia.org/wiki/Iso8601#Durations
259      */
260     public static function validateAge($value)
261     {
262         if (!is_numeric($value)) {
263             //convert short notation to seconds
264             $value = 'P' . ltrim(strtoupper($value), 'P');
265             try {
266                 $interval = new \DateInterval($value);
267             } catch (\Exception $e) {
268                 throw new \InvalidArgumentException(
269                     'Invalid age: ' . $value
270                 );
271             }
272             $value = 86400 * (
273                 $interval->y * 365
274                 + $interval->m * 30
275                 + $interval->d
276             ) + $interval->h * 3600
277                 + $interval->m * 60
278                 + $interval->s;
279         }
280         return $value;
281     }
282
283     /**
284      * Check that a given value exists in an array
285      *
286      * @param string $value   Value to check
287      * @param array  $options Array of allowed values
288      *
289      * @return string Value
290      * @throws \InvalidArgumentException If the value does not exist in $options
291      */
292     protected function validateArray($value, $options)
293     {
294         if (array_search($value, $options) === false) {
295             throw new \InvalidArgumentException(
296                 'Invalid value ' . $value . '.'
297                 . ' Allowed: ' . implode(', ', $options)
298             );
299         }
300         return $value;
301     }
302
303     /**
304      * Validate that a value is numeric and between $min and $max (inclusive)
305      *
306      * @param string  $value Value to check
307      * @param integer $min   Minimum allowed value
308      * @param integer $max   Maximum allowed value
309      *
310      * @return integer Value as integer
311      * @throws \InvalidArgumentException When outside range or not numeric
312      */
313     protected function validateInt($value, $min, $max)
314     {
315         if (!is_numeric($value)) {
316             throw new \InvalidArgumentException(
317                 'Value must be a number'
318             );
319         }
320         $value = (int) $value;
321         return $this->clamp($value, $min, $max);
322     }
323
324     /**
325      * Validate (and fix) an URL
326      *
327      * @param string $url URL
328      *
329      * @return string Fixed URL
330      * @throws \InvalidArgumentException
331      */
332     protected function validateUrl($url)
333     {
334         $parts = parse_url($url);
335         if ($parts === false) {
336             throw new \InvalidArgumentException('Invalid URL');
337         }
338         if (!isset($parts['scheme'])) {
339             $url = 'http://' . $url;
340             $parts = parse_url($url);
341         } else if ($parts['scheme'] != 'http' && $parts['scheme'] != 'https') {
342             throw new \InvalidArgumentException('Unsupported protocol');
343         }
344         if (!isset($parts['host'])) {
345             throw new \InvalidArgumentException('URL host missing');
346         }
347         return $url;
348     }
349
350     /**
351      * Set phancap configuration
352      *
353      * @param Config $config Phancap configuration
354      *
355      * @return void
356      */
357     public function setConfig(Config $config)
358     {
359         $this->config = $config;
360         $this->options['smaxage']['default'] = $this->config->screenshotMaxAge;
361         $this->options['smaxage']['min']     = $this->config->screenshotMinAge;
362     }
363 }
364 ?>