X-Git-Url: https://git.cweiske.de/phancap.git/blobdiff_plain/62875bdbecc633496c712bc2fc5e0ef8f54733e3..df5d413091c6481259fbadda6afa5895c17d9988:/src/phancap/Options.php diff --git a/src/phancap/Options.php b/src/phancap/Options.php index b5c96d9..2cfec7e 100644 --- a/src/phancap/Options.php +++ b/src/phancap/Options.php @@ -1,9 +1,37 @@ + * @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 + * @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 */ @@ -11,6 +39,7 @@ class Options 'title' => 'Website URL', 'default' => null, 'type' => 'url', + 'required' => true, ), 'bwidth' => array( 'title' => 'Browser width', @@ -31,7 +60,7 @@ class Options */ 'swidth' => array( 'title' => 'Screenshot width', - 'default' => 300, + 'default' => null, 'type' => 'int', 'min' => 16, 'max' => 8192, @@ -53,35 +82,92 @@ 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'])) { + throw new \InvalidArgumentException( + $name . ' parameter missing' + ); + } 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'] ); @@ -98,8 +184,16 @@ 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) { + $this->values['swidth'] = $this->values['bwidth']; + } if ($this->values['smode'] == 'page') { return; } @@ -121,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"; + } } ?>