update changelog and release version 0.4.0
[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      * Configuration object
120      *
121      * @var Config
122      */
123     protected $config;
124
125
126     /**
127      * Parses an array of options, validates them and writes them into
128      * $this->values.
129      *
130      * @param array $arValues Array of options, e.g. $_GET
131      *
132      * @return void
133      * @throws \InvalidArgumentException When required parameters are missing
134      *         or parameter values are invalid.
135      */
136     public function parse($arValues)
137     {
138         foreach ($this->options as $name => $arOption) {
139             $this->values[$name] = $arOption['default'];
140             if (!isset($arValues[$name])) {
141                 if (isset($arOption['required'])) {
142                     throw new \InvalidArgumentException(
143                         $name . ' parameter missing'
144                     );
145                 }
146                 continue;
147             }
148
149             if ($arValues[$name] === ''
150                 && !isset($arOption['required'])
151             ) {
152                 //allow empty value; default value will be used
153             } else if ($arOption['type'] == 'url') {
154                 $this->values[$name] = $this->validateUrl($arValues[$name]);
155             } else if ($arOption['type'] == 'int') {
156                 $this->values[$name] = $this->validateInt(
157                     $name, $arValues[$name],
158                     $arOption['min'], $arOption['max']
159                 );
160             } else if (gettype($arOption['type']) == 'array') {
161                 $this->values[$name] = $this->validateArray(
162                     $name, $arValues[$name], $arOption['type']
163                 );
164             } else if ($arOption['type'] == 'age') {
165                 $this->values[$name] = $this->clamp(
166                     static::validateAge($arValues[$name]),
167                     $arOption['min'], null,
168                     true
169                 );
170             } else if ($arOption['type'] != 'skip') {
171                 throw new \InvalidArgumentException(
172                     'Unsupported option type: ' . $arOption['type']
173                 );
174             }
175             unset($arValues[$name]);
176         }
177
178         if (count($arValues) > 0) {
179             throw new \InvalidArgumentException(
180                 'Unsupported parameter: ' . implode(', ', array_keys($arValues))
181             );
182         }
183
184         $this->calcPageSize();
185     }
186
187     /**
188      * Calculate the browser size and screenshot size from the given options
189      *
190      * @return void
191      */
192     protected function calcPageSize()
193     {
194         if ($this->values['swidth'] === null) {
195             $this->values['swidth'] = $this->values['bwidth'];
196         }
197         if ($this->values['smode'] == 'page') {
198             return;
199         }
200
201         if ($this->values['sheight'] !== null) {
202             $this->values['bheight'] = intval(
203                 $this->values['bwidth'] / $this->values['swidth']
204                 * $this->values['sheight']
205             );
206         } else if ($this->values['bheight'] !== null) {
207             $this->values['sheight'] = intval(
208                 $this->values['swidth'] / $this->values['bwidth']
209                 * $this->values['bheight']
210             );
211         } else {
212             //no height set. use 4:3
213             $this->values['sheight'] = $this->values['swidth'] / 4 * 3;
214             $this->values['bheight'] = $this->values['bwidth'] / 4 * 3;
215         }
216     }
217
218     /**
219      * Makes sure a value is between $min and $max (inclusive)
220      *
221      * @param integer $value  Value to check
222      * @param integer $min    Minimum allowed value
223      * @param integer $max    Maximum allowed value
224      * @param boolean $silent When silent, invalid values are corrected.
225      *                        An exception is thrown otherwise.
226      *
227      * @return integer Corrected value
228      * @throws \InvalidArgumentException When not silent and value outside range
229      */
230     protected function clamp($value, $min, $max, $silent = false)
231     {
232         if ($min !== null && $value < $min) {
233             if ($silent) {
234                 $value = $min;
235             } else {
236                 throw new \InvalidArgumentException(
237                     'Value must be at least ' . $min
238                 );
239             }
240         }
241         if ($max !== null && $value > $max) {
242             if ($silent) {
243                 $value = $max;
244             } else {
245                 throw new \InvalidArgumentException(
246                     'Value may be up to ' . $min
247                 );
248             }
249         }
250         return $value;
251     }
252
253     /**
254      * Validates an age is numeric. If it is not numeric, it's interpreted as
255      * a ISO 8601 duration specification.
256      *
257      * @param string $value Age in seconds
258      *
259      * @return integer Age in seconds
260      * @throws \InvalidArgumentException
261      * @link   http://en.wikipedia.org/wiki/Iso8601#Durations
262      */
263     public static function validateAge($value)
264     {
265         if (!is_numeric($value)) {
266             //convert short notation to seconds
267             $value = 'P' . ltrim(strtoupper($value), 'P');
268             try {
269                 $interval = new \DateInterval($value);
270             } catch (\Exception $e) {
271                 throw new \InvalidArgumentException(
272                     'Invalid age: ' . $value
273                 );
274             }
275             $value = 86400 * (
276                 $interval->y * 365
277                 + $interval->m * 30
278                 + $interval->d
279             ) + $interval->h * 3600
280                 + $interval->m * 60
281                 + $interval->s;
282         }
283         return $value;
284     }
285
286     /**
287      * Check that a given value exists in an array
288      *
289      * @param string $name    Variable name
290      * @param string $value   Value to check
291      * @param array  $options Array of allowed values
292      *
293      * @return string Value
294      * @throws \InvalidArgumentException If the value does not exist in $options
295      */
296     protected function validateArray($name, $value, $options)
297     {
298         if (array_search($value, $options) === false) {
299             throw new \InvalidArgumentException(
300                 'Invalid value ' . $value . ' for ' . $name . '.'
301                 . ' Allowed: ' . implode(', ', $options)
302             );
303         }
304         return $value;
305     }
306
307     /**
308      * Validate that a value is numeric and between $min and $max (inclusive)
309      *
310      * @param string  $name  Variable name
311      * @param string  $value Value to check
312      * @param integer $min   Minimum allowed value
313      * @param integer $max   Maximum allowed value
314      *
315      * @return integer Value as integer
316      * @throws \InvalidArgumentException When outside range or not numeric
317      */
318     protected function validateInt($name, $value, $min, $max)
319     {
320         if (!is_numeric($value)) {
321             throw new \InvalidArgumentException(
322                 $name . ' value must be a number'
323             );
324         }
325         $value = (int) $value;
326         return $this->clamp($value, $min, $max);
327     }
328
329     /**
330      * Validate (and fix) an URL
331      *
332      * @param string $url URL
333      *
334      * @return string Fixed URL
335      * @throws \InvalidArgumentException
336      */
337     protected function validateUrl($url)
338     {
339         if ($url === '') {
340             throw new \InvalidArgumentException('URL is empty');
341         }
342         $parts = parse_url($url);
343         if ($parts === false) {
344             throw new \InvalidArgumentException('Invalid URL');
345         }
346         if (!isset($parts['scheme'])) {
347             $url = 'http://' . $url;
348             $parts = parse_url($url);
349         } else if ($parts['scheme'] != 'http' && $parts['scheme'] != 'https') {
350             throw new \InvalidArgumentException('Unsupported protocol');
351         }
352         if (!isset($parts['host'])) {
353             throw new \InvalidArgumentException('URL host missing');
354         }
355
356         $rebuild = false;
357         if (strlen(preg_replace('#[[:ascii:]]#', '', $parts['host']))) {
358             //non-ascii characters in the host name
359             $host = idn_to_ascii($parts['host']);
360             if ($host === false) {
361                 //incoming URL was not UTF-8 but some ISO dialect
362                 $host = idn_to_ascii(utf8_encode($parts['host']));
363                 if ($host === false) {
364                     throw new \InvalidArgumentException(
365                         'Strange characters in host name'
366                     );
367                 }
368             }
369             $parts['host'] = $host;
370             $rebuild = true;
371         }
372         if (strlen(preg_replace('#[[:ascii:]]#', '', $parts['path']))) {
373             //non-ascii characters in the path
374             $parts['path'] = str_replace('%2F', '/', urlencode($parts['path']));
375             $rebuild = true;
376         }
377
378         if ($rebuild) {
379             $url = static::http_build_url($parts);
380         }
381
382         return $url;
383     }
384
385     /**
386      * Set phancap configuration
387      *
388      * @param Config $config Phancap configuration
389      *
390      * @return void
391      */
392     public function setConfig(Config $config)
393     {
394         $this->config = $config;
395         $this->options['smaxage']['default'] = $this->config->screenshotMaxAge;
396         $this->options['smaxage']['min']     = $this->config->screenshotMinAge;
397     }
398
399     /**
400      * Re-build an URL parts array generated by parse_url()
401      *
402      * @param string $parts Array of URL parts
403      *
404      * @return string URL
405      */
406     protected static function http_build_url($parts)
407     {
408         $scheme   = isset($parts['scheme'])
409             ? $parts['scheme'] . '://' : '';
410         $host     = isset($parts['host'])
411             ? $parts['host'] : '';
412         $port     = isset($parts['port'])
413             ? ':' . $parts['port'] : '';
414         $user     = isset($parts['user'])
415             ? $parts['user'] : '';
416         $pass     = isset($parts['pass'])
417
418             ? ':' . $parts['pass']  : '';
419         $pass     = ($user || $pass)
420             ? "$pass@" : '';
421         $path     = isset($parts['path'])
422             ? $parts['path'] : '';
423         $query    = isset($parts['query'])
424             ? '?' . $parts['query'] : '';
425         $fragment = isset($parts['fragment'])
426             ? '#' . $parts['fragment'] : '';
427         return "$scheme$user$pass$host$port$path$query$fragment";
428     }
429 }
430 ?>