Support IDNA - internationalized domain names
[phancap.git] / src / phancap / Options.php
index ee129e8ee9e15fc827712c19c3c79d991a818e60..2cfec7e23cd276021befc035dd08e8fa2f4e4a2e 100644 (file)
@@ -1,9 +1,37 @@
 <?php
+/**
+ * Part of phancap
+ *
+ * PHP version 5
+ *
+ * @category  Tools
+ * @package   Options
+ * @author    Christian Weiske <cweiske@cweiske.de>
+ * @copyright 2014 Christian Weiske
+ * @license   http://www.gnu.org/licenses/agpl.html GNU AGPL v3
+ * @link      http://cweiske.de/phancap.htm
+ */
 namespace phancap;
 
+/**
+ * Options a user can give to the API
+ *
+ * @category  Tools
+ * @package   Options
+ * @author    Christian Weiske <cweiske@cweiske.de>
+ * @copyright 2014 Christian Weiske
+ * @license   http://www.gnu.org/licenses/agpl.html GNU AGPL v3
+ * @version   Release: @package_version@
+ * @link      http://cweiske.de/phancap.htm
+ */
 class Options
 {
-    public static $options = array(
+    /**
+     * Available options and their configuration
+     *
+     * @var array
+     */
+    public $options = array(
         /**
          * Browser settings
          */
@@ -54,20 +82,60 @@ class Options
             'default' => 'screen',
             'type'    => array('screen', 'page'),
         ),
+        'smaxage' => array(
+            'title'   => 'Maximum age for a screenshot',
+            'default' => null,
+            'type'    => 'age',
+            'min'     => null,
+        ),
+        /**
+         * Authentication
+         */
+        'atimestamp' => array(
+            'title'   => 'Timestamp the request has been generated',
+            'default' => null,
+            'type'    => 'skip',
+        ),
+        'atoken' => array(
+            'title'   => 'Access token (user name)',
+            'default' => null,
+            'type'    => 'skip',
+        ),
+        'asignature' => array(
+            'title'   => 'Access signature',
+            'default' => null,
+            'type'    => 'skip',
+        ),
     );
 
+    /**
+     * Actual values we use after parsing the GET parameters
+     *
+     * @var array
+     */
     public $values = array();
 
+    /**
+     * Configuration object
+     *
+     * @var Config
+     */
+    protected $config;
+
 
     /**
      * Parses an array of options, validates them and writes them into
      * $this->values.
      *
      * @param array $arValues Array of options, e.g. $_GET
+     *
+     * @return void
+     * @throws \InvalidArgumentException When required parameters are missing
+     *         or parameter values are invalid.
      */
     public function parse($arValues)
     {
-        foreach (static::$options as $name => $arOption) {
+        foreach ($this->options as $name => $arOption) {
             $this->values[$name] = $arOption['default'];
             if (!isset($arValues[$name])) {
                 if (isset($arOption['required'])) {
@@ -78,17 +146,28 @@ class Options
                 continue;
             }
 
-            if ($arOption['type'] == 'url') {
+            if ($arValues[$name] === ''
+                && !isset($arOption['required'])
+            ) {
+                //allow empty value; default value will be used
+            } else if ($arOption['type'] == 'url') {
                 $this->values[$name] = $this->validateUrl($arValues[$name]);
             } else if ($arOption['type'] == 'int') {
                 $this->values[$name] = $this->validateInt(
-                    $arValues[$name], $arOption['min'], $arOption['max']
+                    $name, $arValues[$name],
+                    $arOption['min'], $arOption['max']
                 );
             } else if (gettype($arOption['type']) == 'array') {
                 $this->values[$name] = $this->validateArray(
-                    $arValues[$name], $arOption['type']
+                    $name, $arValues[$name], $arOption['type']
                 );
-            } else {
+            } else if ($arOption['type'] == 'age') {
+                $this->values[$name] = $this->clamp(
+                    static::validateAge($arValues[$name]),
+                    $arOption['min'], null,
+                    true
+                );
+            } else if ($arOption['type'] != 'skip') {
                 throw new \InvalidArgumentException(
                     'Unsupported option type: ' . $arOption['type']
                 );
@@ -105,6 +184,11 @@ class Options
         $this->calcPageSize();
     }
 
+    /**
+     * Calculate the browser size and screenshot size from the given options
+     *
+     * @return void
+     */
     protected function calcPageSize()
     {
         if ($this->values['swidth'] === null) {
@@ -131,51 +215,216 @@ class Options
         }
     }
 
-    protected function validateArray($value, $options)
+    /**
+     * Makes sure a value is between $min and $max (inclusive)
+     *
+     * @param integer $value  Value to check
+     * @param integer $min    Minimum allowed value
+     * @param integer $max    Maximum allowed value
+     * @param boolean $silent When silent, invalid values are corrected.
+     *                        An exception is thrown otherwise.
+     *
+     * @return integer Corrected value
+     * @throws \InvalidArgumentException When not silent and value outside range
+     */
+    protected function clamp($value, $min, $max, $silent = false)
+    {
+        if ($min !== null && $value < $min) {
+            if ($silent) {
+                $value = $min;
+            } else {
+                throw new \InvalidArgumentException(
+                    'Value must be at least ' . $min
+                );
+            }
+        }
+        if ($max !== null && $value > $max) {
+            if ($silent) {
+                $value = $max;
+            } else {
+                throw new \InvalidArgumentException(
+                    'Value may be up to ' . $min
+                );
+            }
+        }
+        return $value;
+    }
+
+    /**
+     * Validates an age is numeric. If it is not numeric, it's interpreted as
+     * a ISO 8601 duration specification.
+     *
+     * @param string $value Age in seconds
+     *
+     * @return integer Age in seconds
+     * @throws \InvalidArgumentException
+     * @link   http://en.wikipedia.org/wiki/Iso8601#Durations
+     */
+    public static function validateAge($value)
+    {
+        if (!is_numeric($value)) {
+            //convert short notation to seconds
+            $value = 'P' . ltrim(strtoupper($value), 'P');
+            try {
+                $interval = new \DateInterval($value);
+            } catch (\Exception $e) {
+                throw new \InvalidArgumentException(
+                    'Invalid age: ' . $value
+                );
+            }
+            $value = 86400 * (
+                $interval->y * 365
+                + $interval->m * 30
+                + $interval->d
+            ) + $interval->h * 3600
+                + $interval->m * 60
+                + $interval->s;
+        }
+        return $value;
+    }
+
+    /**
+     * Check that a given value exists in an array
+     *
+     * @param string $name    Variable name
+     * @param string $value   Value to check
+     * @param array  $options Array of allowed values
+     *
+     * @return string Value
+     * @throws \InvalidArgumentException If the value does not exist in $options
+     */
+    protected function validateArray($name, $value, $options)
     {
         if (array_search($value, $options) === false) {
             throw new \InvalidArgumentException(
-                'Invalid value ' . $value . '.'
+                'Invalid value ' . $value . ' for ' . $name . '.'
                 . ' Allowed: ' . implode(', ', $options)
             );
         }
         return $value;
     }
 
-    protected function validateInt($value, $min, $max)
+    /**
+     * Validate that a value is numeric and between $min and $max (inclusive)
+     *
+     * @param string  $name  Variable name
+     * @param string  $value Value to check
+     * @param integer $min   Minimum allowed value
+     * @param integer $max   Maximum allowed value
+     *
+     * @return integer Value as integer
+     * @throws \InvalidArgumentException When outside range or not numeric
+     */
+    protected function validateInt($name, $value, $min, $max)
     {
         if (!is_numeric($value)) {
             throw new \InvalidArgumentException(
-                'Value must be a number'
+                $name . ' value must be a number'
             );
         }
         $value = (int) $value;
-        if ($value < $min) {
-            throw new \InvalidArgumentException(
-                'Value must be at least ' . $min
-            );
-        }
-        if ($value > $max) {
-            throw new \InvalidArgumentException(
-                'Value may be up to ' . $min
-            );
-        }
-        return $value;
+        return $this->clamp($value, $min, $max);
     }
 
+    /**
+     * Validate (and fix) an URL
+     *
+     * @param string $url URL
+     *
+     * @return string Fixed URL
+     * @throws \InvalidArgumentException
+     */
     protected function validateUrl($url)
     {
+        if ($url === '') {
+            throw new \InvalidArgumentException('URL is empty');
+        }
         $parts = parse_url($url);
         if ($parts === false) {
             throw new \InvalidArgumentException('Invalid URL');
         }
         if (!isset($parts['scheme'])) {
-            throw new \InvalidArgumentException('URL scheme missing');            
+            $url = 'http://' . $url;
+            $parts = parse_url($url);
+        } else if ($parts['scheme'] != 'http' && $parts['scheme'] != 'https') {
+            throw new \InvalidArgumentException('Unsupported protocol');
         }
         if (!isset($parts['host'])) {
-            throw new \InvalidArgumentException('URL host missing');            
+            throw new \InvalidArgumentException('URL host missing');
         }
+
+        $rebuild = false;
+        if (strlen(preg_replace('#[[:ascii:]]#', '', $parts['host']))) {
+            //non-ascii characters in the host name
+            $host = idn_to_ascii($parts['host']);
+            if ($host === false) {
+                //incoming URL was not UTF-8 but some ISO dialect
+                $host = idn_to_ascii(utf8_encode($parts['host']));
+                if ($host === false) {
+                    throw new \InvalidArgumentException(
+                        'Strange characters in host name'
+                    );
+                }
+            }
+            $parts['host'] = $host;
+            $rebuild = true;
+        }
+        if (strlen(preg_replace('#[[:ascii:]]#', '', $parts['path']))) {
+            //non-ascii characters in the path
+            $parts['path'] = str_replace('%2F', '/', urlencode($parts['path']));
+            $rebuild = true;
+        }
+
+        if ($rebuild) {
+            $url = static::http_build_url($parts);
+        }
+
         return $url;
     }
+
+    /**
+     * Set phancap configuration
+     *
+     * @param Config $config Phancap configuration
+     *
+     * @return void
+     */
+    public function setConfig(Config $config)
+    {
+        $this->config = $config;
+        $this->options['smaxage']['default'] = $this->config->screenshotMaxAge;
+        $this->options['smaxage']['min']     = $this->config->screenshotMinAge;
+    }
+
+    /**
+     * Re-build an URL parts array generated by parse_url()
+     *
+     * @param string $parts Array of URL parts
+     *
+     * @return string URL
+     */
+    protected static function http_build_url($parts)
+    {
+        $scheme   = isset($parts['scheme'])
+            ? $parts['scheme'] . '://' : '';
+        $host     = isset($parts['host'])
+            ? $parts['host'] : '';
+        $port     = isset($parts['port'])
+            ? ':' . $parts['port'] : '';
+        $user     = isset($parts['user'])
+            ? $parts['user'] : '';
+        $pass     = isset($parts['pass'])
+
+            ? ':' . $parts['pass']  : '';
+        $pass     = ($user || $pass)
+            ? "$pass@" : '';
+        $path     = isset($parts['path'])
+            ? $parts['path'] : '';
+        $query    = isset($parts['query'])
+            ? '?' . $parts['query'] : '';
+        $fragment = isset($parts['fragment'])
+            ? '#' . $parts['fragment'] : '';
+        return "$scheme$user$pass$host$port$path$query$fragment";
+    }
 }
 ?>