diff options
| author | 2017-02-15 14:12:25 +0100 | |
|---|---|---|
| committer | 2017-02-15 14:12:25 +0100 | |
| commit | 2d097bc855dbd1ad06c7c306c05e78a198209084 (patch) | |
| tree | 67028e45792c575c25c92616633f64cc7a4a13eb /lib | |
| parent | fe293900061263a1917fc1cf18ca369c8e07cb99 (diff) | |
| parent | 5f637bd816b7323885bfe1751a1724ee59a822f6 (diff) | |
Merge remote-tracking branch 'FreshRSS/master' into dev
Diffstat (limited to 'lib')
36 files changed, 2405 insertions, 736 deletions
diff --git a/lib/Favicon/DataAccess.php b/lib/Favicon/DataAccess.php new file mode 100644 index 000000000..ae7509881 --- /dev/null +++ b/lib/Favicon/DataAccess.php @@ -0,0 +1,41 @@ +<?php + +namespace Favicon; + +/** + * DataAccess is a wrapper used to read/write data locally or remotly + * Aside from SOLID principles, this wrapper is also useful to mock remote resources in unit tests + * Note: remote access warning are silenced because we don't care if a website is unreachable + **/ +class DataAccess { + public function retrieveUrl($url) { + $this->set_context(); + return @file_get_contents($url); + } + + public function retrieveHeader($url) { + $this->set_context(); + $headers = @get_headers($url, 1); + return $headers ? array_change_key_case($headers) : array(); + } + + public function saveCache($file, $data) { + file_put_contents($file, $data); + } + + public function readCache($file) { + return file_get_contents($file); + } + + private function set_context() { + stream_context_set_default( + array( + 'http' => array( + 'method' => 'GET', + 'timeout' => 10, + 'header' => "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:20.0; Favicon; +https://github.com/ArthurHoaro/favicon) Gecko/20100101 Firefox/32.0\r\n", + ) + ) + ); + } +}
\ No newline at end of file diff --git a/lib/Favicon/Favicon.php b/lib/Favicon/Favicon.php new file mode 100644 index 000000000..1912050d6 --- /dev/null +++ b/lib/Favicon/Favicon.php @@ -0,0 +1,293 @@ +<?php + +namespace Favicon; + +class Favicon +{ + protected $url = ''; + protected $cacheDir; + protected $cacheTimeout; + protected $dataAccess; + + public function __construct($args = array()) + { + if (isset($args['url'])) { + $this->url = $args['url']; + } + + $this->cacheDir = __DIR__ . '/../../resources/cache'; + $this->dataAccess = new DataAccess(); + } + + public function cache($args = array()) { + if (isset($args['dir'])) { + $this->cacheDir = $args['dir']; + } + + if (!empty($args['timeout'])) { + $this->cacheTimeout = $args['timeout']; + } else { + $this->cacheTimeout = 0; + } + } + + public static function baseUrl($url, $path = false) + { + $return = ''; + + if (!$url = parse_url($url)) { + return FALSE; + } + + // Scheme + $scheme = isset($url['scheme']) ? strtolower($url['scheme']) : null; + if ($scheme != 'http' && $scheme != 'https') { + + return FALSE; + } + $return .= "{$scheme}://"; + + // Username and password + if (isset($url['user'])) { + $return .= $url['user']; + if (isset($url['pass'])) { + $return .= ":{$url['pass']}"; + } + $return .= '@'; + } + + // Hostname + if( !isset($url['host']) ) { + return FALSE; + } + + $return .= $url['host']; + + // Port + if (isset($url['port'])) { + $return .= ":{$url['port']}"; + } + + // Path + if( $path && isset($url['path']) ) { + $return .= $url['path']; + } + $return .= '/'; + + return $return; + } + + public function info($url) + { + if(empty($url) || $url === false) { + return false; + } + + $max_loop = 5; + + // Discover real status by following redirects. + $loop = TRUE; + while ($loop && $max_loop-- > 0) { + $headers = $this->dataAccess->retrieveHeader($url); + $exploded = explode(' ', $headers[0]); + + if( !isset($exploded[1]) ) { + return false; + } + list(,$status) = $exploded; + + switch ($status) { + case '301': + case '302': + $url = isset($headers['location']) ? $headers['location'] : ''; + break; + default: + $loop = FALSE; + break; + } + } + + return array('status' => $status, 'url' => $url); + } + + public function endRedirect($url) { + $out = $this->info($url); + return !empty($out['url']) ? $out['url'] : false; + } + + /** + * Find remote (or cached) favicon + * @return favicon URL, false if nothing was found + **/ + public function get($url = '') + { + // URLs passed to this method take precedence. + if (!empty($url)) { + $this->url = $url; + } + + // Get the base URL without the path for clearer concatenations. + $original = rtrim($this->baseUrl($this->url, true), '/'); + $url = rtrim($this->endRedirect($this->baseUrl($this->url, false)), '/'); + + if(($favicon = $this->checkCache($url)) || ($favicon = $this->getFavicon($url))) { + $base = true; + } + elseif(($favicon = $this->checkCache($original)) || ($favicon = $this->getFavicon($original, false))) { + $base = false; + } + else + return false; + + // Save cache if necessary + $cache = $this->cacheDir . '/' . md5($base ? $url : $original); + if ($this->cacheTimeout && !file_exists($cache) || (is_writable($cache) && time() - filemtime($cache) > $this->cacheTimeout)) { + $this->dataAccess->saveCache($cache, $favicon); + } + + return $favicon; + } + + private function getFavicon($url, $checkDefault = true) { + $favicon = false; + + if(empty($url)) { + return false; + } + + // Try /favicon.ico first. + if( $checkDefault ) { + $info = $this->info("{$url}/favicon.ico"); + if ($info['status'] == '200') { + $favicon = $info['url']; + } + } + + // See if it's specified in a link tag in domain url. + if (!$favicon) { + $favicon = $this->getInPage($url); + } + + // Make sure the favicon is an absolute URL. + if( $favicon && filter_var($favicon, FILTER_VALIDATE_URL) === false ) { + $favicon = $url . '/' . $favicon; + } + + // Sometimes people lie, so check the status. + // And sometimes, it's not even an image. Sneaky bastards! + // If cacheDir isn't writable, that's not our problem + if ($favicon && is_writable($this->cacheDir) && !$this->checkImageMType($favicon)) { + $favicon = false; + } + + return $favicon; + } + + private function getInPage($url) { + $html = $this->dataAccess->retrieveUrl("{$url}/"); + preg_match('!<head.*?>.*</head>!ims', $html, $match); + + if(empty($match) || count($match) == 0) { + return false; + } + + $head = $match[0]; + + $dom = new \DOMDocument(); + // Use error supression, because the HTML might be too malformed. + if (@$dom->loadHTML($head)) { + $links = $dom->getElementsByTagName('link'); + foreach ($links as $link) { + if ($link->hasAttribute('rel') && strtolower($link->getAttribute('rel')) == 'shortcut icon') { + return $link->getAttribute('href'); + } elseif ($link->hasAttribute('rel') && strtolower($link->getAttribute('rel')) == 'icon') { + return $link->getAttribute('href'); + } elseif ($link->hasAttribute('href') && strpos($link->getAttribute('href'), 'favicon') !== FALSE) { + return $link->getAttribute('href'); + } + } + } + return false; + } + + private function checkCache($url) { + if ($this->cacheTimeout) { + $cache = $this->cacheDir . '/' . md5($url); + if (file_exists($cache) && is_readable($cache) && (time() - filemtime($cache) < $this->cacheTimeout)) { + return $this->dataAccess->readCache($cache); + } + } + return false; + } + + private function checkImageMType($url) { + $tmpFile = $this->cacheDir . '/tmp.ico'; + + $fileContent = $this->dataAccess->retrieveUrl($url); + $this->dataAccess->saveCache($tmpFile, $fileContent); + + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $isImage = strpos(finfo_file($finfo, $tmpFile), 'image') !== false; + finfo_close($finfo); + + unlink($tmpFile); + + return $isImage; + } + + /** + * @return mixed + */ + public function getCacheDir() + { + return $this->cacheDir; + } + + /** + * @param mixed $cacheDir + */ + public function setCacheDir($cacheDir) + { + $this->cacheDir = $cacheDir; + } + + /** + * @return mixed + */ + public function getCacheTimeout() + { + return $this->cacheTimeout; + } + + /** + * @param mixed $cacheTimeout + */ + public function setCacheTimeout($cacheTimeout) + { + $this->cacheTimeout = $cacheTimeout; + } + + /** + * @return string + */ + public function getUrl() + { + return $this->url; + } + + /** + * @param string $url + */ + public function setUrl($url) + { + $this->url = $url; + } + + /** + * @param DataAccess $dataAccess + */ + public function setDataAccess($dataAccess) + { + $this->dataAccess = $dataAccess; + } +} diff --git a/lib/Minz/BadConfigurationException.php b/lib/Minz/BadConfigurationException.php deleted file mode 100644 index a7b77d687..000000000 --- a/lib/Minz/BadConfigurationException.php +++ /dev/null @@ -1,9 +0,0 @@ -<?php -class Minz_BadConfigurationException extends Minz_Exception { - public function __construct ($part_missing, $code = self::ERROR) { - $message = '`' . $part_missing - . '` in the configuration file is missing or is misconfigured'; - - parent::__construct ($message, $code); - } -} diff --git a/lib/Minz/Configuration.php b/lib/Minz/Configuration.php index 554bc8c96..d695d4a53 100644 --- a/lib/Minz/Configuration.php +++ b/lib/Minz/Configuration.php @@ -1,372 +1,215 @@ <?php -/** - * MINZ - Copyright 2011 Marien Fressinaud - * Sous licence AGPL3 <http://www.gnu.org/licenses/> -*/ /** - * La classe Configuration permet de gérer la configuration de l'application + * Manage configuration for the application. */ class Minz_Configuration { - const CONF_PATH_NAME = '/config.php'; - /** - * VERSION est la version actuelle de MINZ + * The list of configurations. */ - const VERSION = '1.3.1.freshrss'; // version spéciale FreshRSS + private static $config_list = array(); /** - * valeurs possibles pour l'"environment" - * SILENT rend l'application muette (pas de log) - * PRODUCTION est recommandée pour une appli en production - * (log les erreurs critiques) - * DEVELOPMENT log toutes les erreurs + * Add a new configuration to the list of configuration. + * + * @param $namespace the name of the current configuration + * @param $config_filename the filename of the configuration + * @param $default_filename a filename containing default values for the configuration + * @param $configuration_setter an optional helper to set values in configuration */ - const SILENT = 0; - const PRODUCTION = 1; - const DEVELOPMENT = 2; + public static function register($namespace, $config_filename, $default_filename = null, + $configuration_setter = null) { + self::$config_list[$namespace] = new Minz_Configuration( + $namespace, $config_filename, $default_filename, $configuration_setter + ); + } /** - * définition des variables de configuration - * $salt une chaîne de caractères aléatoires (obligatoire) - * $environment gère le niveau d'affichage pour log et erreurs - * $base_url le chemin de base pour accéder à l'application - * $title le nom de l'application - * $language la langue par défaut de l'application - * $db paramètres pour la base de données (tableau) - * - host le serveur de la base - * - user nom d'utilisateur - * - password mot de passe de l'utilisateur - * - base le nom de la base de données + * Parse a file and return its data. + * + * If the file does not contain a valid PHP code returning an array, an + * empty array is returned anyway. + * + * @param $filename the name of the file to parse. + * @return an array of values + * @throws Minz_FileNotExistException if the file does not exist. */ - private static $salt = ''; - private static $environment = Minz_Configuration::PRODUCTION; - private static $base_url = ''; - private static $title = ''; - private static $language = 'en'; - private static $default_user = ''; - private static $allow_anonymous = false; - private static $allow_anonymous_refresh = false; - private static $auth_type = 'none'; - private static $api_enabled = false; - private static $unsafe_autologin_enabled = false; - - private static $db = array ( - 'type' => 'mysql', - 'host' => '', - 'user' => '', - 'password' => '', - 'base' => '', - 'prefix' => '', - ); + public static function load($filename) { + if (!file_exists($filename)) { + throw new Minz_FileNotExistException($filename); + } - /* - * Getteurs - */ - public static function salt () { - return self::$salt; + $data = include($filename); + if (is_array($data)) { + return $data; + } else { + return array(); + } } - public static function environment ($str = false) { - $env = self::$environment; - if ($str) { - switch (self::$environment) { - case self::SILENT: - $env = 'silent'; - break; - case self::DEVELOPMENT: - $env = 'development'; - break; - case self::PRODUCTION: - default: - $env = 'production'; - } + /** + * Return the configuration related to a given namespace. + * + * @param $namespace the name of the configuration to get. + * @return a Minz_Configuration object + * @throws Minz_ConfigurationNamespaceException if the namespace does not exist. + */ + public static function get($namespace) { + if (!isset(self::$config_list[$namespace])) { + throw new Minz_ConfigurationNamespaceException( + $namespace . ' namespace does not exist' + ); } - return $env; - } - public static function baseUrl () { - return self::$base_url; - } - public static function title () { - return self::$title; - } - public static function language () { - return self::$language; - } - public static function dataBase () { - return self::$db; - } - public static function defaultUser () { - return self::$default_user; - } - public static function allowAnonymous() { - return self::$allow_anonymous; - } - public static function allowAnonymousRefresh() { - return self::$allow_anonymous_refresh; - } - public static function authType() { - return self::$auth_type; - } - public static function needsLogin() { - return self::$auth_type !== 'none'; - } - public static function canLogIn() { - return self::$auth_type === 'form' || self::$auth_type === 'persona'; - } - public static function apiEnabled() { - return self::$api_enabled; - } - public static function unsafeAutologinEnabled() { - return self::$unsafe_autologin_enabled; + return self::$config_list[$namespace]; } - public static function _allowAnonymous($allow = false) { - self::$allow_anonymous = ((bool)$allow) && self::canLogIn(); - } - public static function _allowAnonymousRefresh($allow = false) { - self::$allow_anonymous_refresh = ((bool)$allow) && self::allowAnonymous(); - } - public static function _authType($value) { - $value = strtolower($value); - switch ($value) { - case 'form': - case 'http_auth': - case 'persona': - case 'none': - self::$auth_type = $value; - break; - } - self::_allowAnonymous(self::$allow_anonymous); - } + /** + * The namespace of the current configuration. + */ + private $namespace = ''; + + /** + * The filename for the current configuration. + */ + private $config_filename = ''; + + /** + * The filename for the current default values, null by default. + */ + private $default_filename = null; + + /** + * The configuration values, an empty array by default. + */ + private $data = array(); - public static function _enableApi($value = false) { - self::$api_enabled = (bool)$value; + /** + * An object which help to set good values in configuration. + */ + private $configuration_setter = null; + + public function removeExtension($ext_name) { + self::$extensions_enabled = array_diff( + self::$extensions_enabled, + array($ext_name) + ); } - public static function _enableAutologin($value = false) { - self::$unsafe_autologin_enabled = (bool)$value; + public function addExtension($ext_name) { + $found = array_search($ext_name, self::$extensions_enabled) !== false; + if (!$found) { + self::$extensions_enabled[] = $ext_name; + } } /** - * Initialise les variables de configuration - * @exception Minz_FileNotExistException si le CONF_PATH_NAME n'existe pas - * @exception Minz_BadConfigurationException si CONF_PATH_NAME mal formaté + * Create a new Minz_Configuration object. + * + * @param $namespace the name of the current configuration. + * @param $config_filename the file containing configuration values. + * @param $default_filename the file containing default values, null by default. + * @param $configuration_setter an optional helper to set values in configuration */ - public static function init () { + private function __construct($namespace, $config_filename, $default_filename = null, + $configuration_setter = null) { + $this->namespace = $namespace; + $this->config_filename = $config_filename; + $this->default_filename = $default_filename; + $this->_configurationSetter($configuration_setter); + + if (!is_null($this->default_filename)) { + $this->data = self::load($this->default_filename); + } + try { - self::parseFile (); - self::setReporting (); + $this->data = array_replace_recursive( + $this->data, self::load($this->config_filename) + ); } catch (Minz_FileNotExistException $e) { - throw $e; - } catch (Minz_BadConfigurationException $e) { - throw $e; + if (is_null($this->default_filename)) { + throw $e; + } } } - public static function writeFile() { - $ini_array = array( - 'general' => array( - 'environment' => self::environment(true), - 'salt' => self::$salt, - 'base_url' => self::$base_url, - 'title' => self::$title, - 'default_user' => self::$default_user, - 'allow_anonymous' => self::$allow_anonymous, - 'allow_anonymous_refresh' => self::$allow_anonymous_refresh, - 'auth_type' => self::$auth_type, - 'api_enabled' => self::$api_enabled, - 'unsafe_autologin_enabled' => self::$unsafe_autologin_enabled, - ), - 'db' => self::$db, - ); - @rename(DATA_PATH . self::CONF_PATH_NAME, DATA_PATH . self::CONF_PATH_NAME . '.bak.php'); - $result = file_put_contents(DATA_PATH . self::CONF_PATH_NAME, "<?php\n return " . var_export($ini_array, true) . ';'); - if (function_exists('opcache_invalidate')) { - opcache_invalidate(DATA_PATH . self::CONF_PATH_NAME); //Clear PHP 5.5+ cache for include + /** + * Set a configuration setter for the current configuration. + * @param $configuration_setter the setter to call when modifying data. It + * must implement an handle($key, $value) method. + */ + public function _configurationSetter($configuration_setter) { + if (is_callable(array($configuration_setter, 'handle'))) { + $this->configuration_setter = $configuration_setter; } - return (bool)$result; } /** - * Parse un fichier de configuration - * @exception Minz_PermissionDeniedException si le CONF_PATH_NAME n'est pas accessible - * @exception Minz_BadConfigurationException si CONF_PATH_NAME mal formaté + * Return the value of the given param. + * + * @param $key the name of the param. + * @param $default default value to return if key does not exist. + * @return the value corresponding to the key. + * @throws Minz_ConfigurationParamException if the param does not exist */ - private static function parseFile () { - $ini_array = include(DATA_PATH . self::CONF_PATH_NAME); - - if (!is_array($ini_array)) { - throw new Minz_PermissionDeniedException ( - DATA_PATH . self::CONF_PATH_NAME, - Minz_Exception::ERROR - ); + public function param($key, $default = null) { + if (isset($this->data[$key])) { + return $this->data[$key]; + } elseif (!is_null($default)) { + return $default; + } else { + Minz_Log::warning($key . ' does not exist in configuration'); + return null; } + } - // [general] est obligatoire - if (!isset ($ini_array['general'])) { - throw new Minz_BadConfigurationException ( - '[general]', - Minz_Exception::ERROR - ); - } - $general = $ini_array['general']; + /** + * A wrapper for param(). + */ + public function __get($key) { + return $this->param($key); + } - // salt est obligatoire - if (!isset ($general['salt'])) { - if (isset($general['sel_application'])) { //v0.6 - $general['salt'] = $general['sel_application']; - } else { - throw new Minz_BadConfigurationException ( - 'salt', - Minz_Exception::ERROR - ); - } + /** + * Set or remove a param. + * + * @param $key the param name to set. + * @param $value the value to set. If null, the key is removed from the configuration. + */ + public function _param($key, $value = null) { + if (!is_null($this->configuration_setter) && $this->configuration_setter->support($key)) { + $this->configuration_setter->handle($this->data, $key, $value); + } elseif (isset($this->data[$key]) && is_null($value)) { + unset($this->data[$key]); + } elseif (!is_null($value)) { + $this->data[$key] = $value; } - self::$salt = $general['salt']; + } - if (isset ($general['environment'])) { - switch ($general['environment']) { - case 'silent': - self::$environment = Minz_Configuration::SILENT; - break; - case 'development': - self::$environment = Minz_Configuration::DEVELOPMENT; - break; - case 'production': - self::$environment = Minz_Configuration::PRODUCTION; - break; - default: - if ($general['environment'] >= 0 && - $general['environment'] <= 2) { - // fallback 0.7-beta - self::$environment = $general['environment']; - } else { - throw new Minz_BadConfigurationException ( - 'environment', - Minz_Exception::ERROR - ); - } - } + /** + * A wrapper for _param(). + */ + public function __set($key, $value) { + $this->_param($key, $value); + } - } - if (isset ($general['base_url'])) { - self::$base_url = $general['base_url']; - } + /** + * Save the current configuration in the configuration file. + */ + public function save() { + $back_filename = $this->config_filename . '.bak.php'; + @rename($this->config_filename, $back_filename); - if (isset ($general['title'])) { - self::$title = $general['title']; - } - if (isset ($general['language'])) { - self::$language = $general['language']; - } - if (isset ($general['default_user'])) { - self::$default_user = $general['default_user']; - } - if (isset ($general['auth_type'])) { - self::_authType($general['auth_type']); - } - if (isset ($general['allow_anonymous'])) { - self::$allow_anonymous = ( - ((bool)($general['allow_anonymous'])) && - ($general['allow_anonymous'] !== 'no') - ); - } - if (isset ($general['allow_anonymous_refresh'])) { - self::$allow_anonymous_refresh = ( - ((bool)($general['allow_anonymous_refresh'])) && - ($general['allow_anonymous_refresh'] !== 'no') - ); - } - if (isset ($general['api_enabled'])) { - self::$api_enabled = ( - ((bool)($general['api_enabled'])) && - ($general['api_enabled'] !== 'no') - ); - } - if (isset ($general['unsafe_autologin_enabled'])) { - self::$unsafe_autologin_enabled = ( - ((bool)($general['unsafe_autologin_enabled'])) && - ($general['unsafe_autologin_enabled'] !== 'no') - ); + if (file_put_contents($this->config_filename, + "<?php\nreturn " . var_export($this->data, true) . ';', + LOCK_EX) === false) { + return false; } - // Base de données - if (isset ($ini_array['db'])) { - $db = $ini_array['db']; - if (empty($db['type'])) { - throw new Minz_BadConfigurationException ( - 'type', - Minz_Exception::ERROR - ); - } - switch ($db['type']) { - case 'mysql': - if (empty($db['host'])) { - throw new Minz_BadConfigurationException ( - 'host', - Minz_Exception::ERROR - ); - } - if (empty($db['user'])) { - throw new Minz_BadConfigurationException ( - 'user', - Minz_Exception::ERROR - ); - } - if (!isset($db['password'])) { - throw new Minz_BadConfigurationException ( - 'password', - Minz_Exception::ERROR - ); - } - if (empty($db['base'])) { - throw new Minz_BadConfigurationException ( - 'base', - Minz_Exception::ERROR - ); - } - self::$db['host'] = $db['host']; - self::$db['user'] = $db['user']; - self::$db['password'] = $db['password']; - self::$db['base'] = $db['base']; - if (isset($db['prefix'])) { - self::$db['prefix'] = $db['prefix']; - } - break; - case 'sqlite': - self::$db['host'] = ''; - self::$db['user'] = ''; - self::$db['password'] = ''; - self::$db['base'] = ''; - self::$db['prefix'] = ''; - break; - default: - throw new Minz_BadConfigurationException ( - 'type', - Minz_Exception::ERROR - ); - break; - } - self::$db['type'] = $db['type']; + // Clear PHP 5.5+ cache for include + if (function_exists('opcache_invalidate')) { + opcache_invalidate($this->config_filename); } - } - private static function setReporting() { - switch (self::$environment) { - case self::PRODUCTION: - error_reporting(E_ALL); - ini_set('display_errors','Off'); - ini_set('log_errors', 'On'); - break; - case self::DEVELOPMENT: - error_reporting(E_ALL); - ini_set('display_errors','On'); - ini_set('log_errors', 'On'); - break; - case self::SILENT: - error_reporting(0); - break; - } + return true; } } diff --git a/lib/Minz/ConfigurationException.php b/lib/Minz/ConfigurationException.php new file mode 100644 index 000000000..f294c3341 --- /dev/null +++ b/lib/Minz/ConfigurationException.php @@ -0,0 +1,8 @@ +<?php + +class Minz_ConfigurationException extends Minz_Exception { + public function __construct($error, $code = self::ERROR) { + $message = 'Configuration error: ' . $error; + parent::__construct($message, $code); + } +} diff --git a/lib/Minz/ConfigurationNamespaceException.php b/lib/Minz/ConfigurationNamespaceException.php new file mode 100644 index 000000000..f4278c5d6 --- /dev/null +++ b/lib/Minz/ConfigurationNamespaceException.php @@ -0,0 +1,4 @@ +<?php + +class Minz_ConfigurationNamespaceException extends Minz_ConfigurationException { +} diff --git a/lib/Minz/ConfigurationParamException.php b/lib/Minz/ConfigurationParamException.php new file mode 100644 index 000000000..eac977935 --- /dev/null +++ b/lib/Minz/ConfigurationParamException.php @@ -0,0 +1,4 @@ +<?php + +class Minz_ConfigurationParamException extends Minz_ConfigurationException { +} diff --git a/lib/Minz/Dispatcher.php b/lib/Minz/Dispatcher.php index f62a92911..125ce5757 100644 --- a/lib/Minz/Dispatcher.php +++ b/lib/Minz/Dispatcher.php @@ -15,6 +15,7 @@ class Minz_Dispatcher { /* singleton */ private static $instance = null; private static $needsReset; + private static $registrations = array(); private $controller; @@ -38,7 +39,7 @@ class Minz_Dispatcher { self::$needsReset = false; try { - $this->createController ('FreshRSS_' . Minz_Request::controllerName () . '_Controller'); + $this->createController (Minz_Request::controllerName ()); $this->controller->init (); $this->controller->firstAction (); if (!self::$needsReset) { @@ -67,14 +68,18 @@ class Minz_Dispatcher { /** * Instancie le Controller - * @param $controller_name le nom du controller à instancier + * @param $base_name le nom du controller à instancier * @exception ControllerNotExistException le controller n'existe pas * @exception ControllerNotActionControllerException controller n'est * > pas une instance de ActionController */ - private function createController ($controller_name) { - $filename = APP_PATH . self::CONTROLLERS_PATH_NAME . '/' - . $controller_name . '.php'; + private function createController ($base_name) { + if (self::isRegistered($base_name)) { + self::loadController($base_name); + $controller_name = 'FreshExtension_' . $base_name . '_Controller'; + } else { + $controller_name = 'FreshRSS_' . $base_name . '_Controller'; + } if (!class_exists ($controller_name)) { throw new Minz_ControllerNotExistException ( @@ -114,4 +119,42 @@ class Minz_Dispatcher { $action_name )); } + + /** + * Register a controller file. + * + * @param $base_name the base name of the controller (i.e. ./?c=<base_name>) + * @param $base_path the base path where we should look into to find info. + */ + public static function registerController($base_name, $base_path) { + if (!self::isRegistered($base_name)) { + self::$registrations[$base_name] = $base_path; + } + } + + /** + * Return if a controller is registered. + * + * @param $base_name the base name of the controller. + * @return true if the controller has been registered, false else. + */ + public static function isRegistered($base_name) { + return isset(self::$registrations[$base_name]); + } + + /** + * Load a controller file (include). + * + * @param $base_name the base name of the controller. + */ + private static function loadController($base_name) { + $base_path = self::$registrations[$base_name]; + $controller_filename = $base_path . '/controllers/' . $base_name . 'Controller.php'; + include_once $controller_filename; + } + + private static function setViewPath($controller, $base_name) { + $base_path = self::$registrations[$base_name]; + $controller->view()->setBasePathname($base_path); + } } diff --git a/lib/Minz/Error.php b/lib/Minz/Error.php index c8222a430..3e4a3e8f3 100644 --- a/lib/Minz/Error.php +++ b/lib/Minz/Error.php @@ -19,46 +19,17 @@ class Minz_Error { * > $logs['notice'] * @param $redirect indique s'il faut forcer la redirection (les logs ne seront pas transmis) */ - public static function error ($code = 404, $logs = array (), $redirect = false) { + public static function error ($code = 404, $logs = array (), $redirect = true) { $logs = self::processLogs ($logs); $error_filename = APP_PATH . '/Controllers/errorController.php'; - switch ($code) { - case 200 : - header('HTTP/1.1 200 OK'); - break; - case 403 : - header('HTTP/1.1 403 Forbidden'); - break; - case 404 : - header('HTTP/1.1 404 Not Found'); - break; - case 500 : - header('HTTP/1.1 500 Internal Server Error'); - break; - case 503 : - header('HTTP/1.1 503 Service Unavailable'); - break; - default : - header('HTTP/1.1 500 Internal Server Error'); - } - if (file_exists ($error_filename)) { - $params = array ( - 'code' => $code, - 'logs' => $logs - ); + Minz_Session::_param('error_code', $code); + Minz_Session::_param('error_logs', $logs); - if ($redirect) { - Minz_Request::forward (array ( - 'c' => 'error' - ), true); - } else { - Minz_Request::forward (array ( - 'c' => 'error', - 'params' => $params - ), false); - } + Minz_Request::forward (array ( + 'c' => 'error' + ), $redirect); } else { echo '<h1>An error occured</h1>' . "\n"; @@ -82,7 +53,8 @@ class Minz_Error { * > en fonction de l'environment */ private static function processLogs ($logs) { - $env = Minz_Configuration::environment (); + $conf = Minz_Configuration::get('system'); + $env = $conf->environment; $logs_ok = array (); $error = array (); $warning = array (); @@ -98,10 +70,10 @@ class Minz_Error { $notice = $logs['notice']; } - if ($env == Minz_Configuration::PRODUCTION) { + if ($env == 'production') { $logs_ok = $error; } - if ($env == Minz_Configuration::DEVELOPMENT) { + if ($env == 'development') { $logs_ok = array_merge ($error, $warning, $notice); } diff --git a/lib/Minz/Extension.php b/lib/Minz/Extension.php new file mode 100644 index 000000000..78b8a2725 --- /dev/null +++ b/lib/Minz/Extension.php @@ -0,0 +1,208 @@ +<?php + +/** + * The extension base class. + */ +class Minz_Extension { + private $name; + private $entrypoint; + private $path; + private $author; + private $description; + private $version; + private $type; + + public static $authorized_types = array( + 'system', + 'user', + ); + + private $is_enabled; + + /** + * The constructor to assign specific information to the extension. + * + * Available fields are: + * - name: the name of the extension (required). + * - entrypoint: the extension class name (required). + * - path: the pathname to the extension files (required). + * - author: the name and / or email address of the extension author. + * - description: a short description to describe the extension role. + * - version: a version for the current extension. + * - type: "system" or "user" (default). + * + * It must not be redefined by child classes. + * + * @param $meta_info contains information about the extension. + */ + public function __construct($meta_info) { + $this->name = $meta_info['name']; + $this->entrypoint = $meta_info['entrypoint']; + $this->path = $meta_info['path']; + $this->author = isset($meta_info['author']) ? $meta_info['author'] : ''; + $this->description = isset($meta_info['description']) ? $meta_info['description'] : ''; + $this->version = isset($meta_info['version']) ? $meta_info['version'] : '0.1'; + $this->setType(isset($meta_info['type']) ? $meta_info['type'] : 'user'); + + $this->is_enabled = false; + } + + /** + * Used when installing an extension (e.g. update the database scheme). + * + * It must be redefined by child classes. + * + * @return true if the extension has been installed or a string explaining + * the problem. + */ + public function install() { + return true; + } + + /** + * Used when uninstalling an extension (e.g. revert the database scheme to + * cancel changes from install). + * + * It must be redefined by child classes. + * + * @return true if the extension has been uninstalled or a string explaining + * the problem. + */ + public function uninstall() { + return true; + } + + /** + * Call at the initialization of the extension (i.e. when the extension is + * enabled by the extension manager). + * + * It must be redefined by child classes. + */ + public function init() {} + + /** + * Set the current extension to enable. + */ + public function enable() { + $this->is_enabled = true; + } + + /** + * Return if the extension is currently enabled. + * + * @return true if extension is enabled, false else. + */ + public function isEnabled() { + return $this->is_enabled; + } + + /** + * Return the content of the configure view for the current extension. + * + * @return the html content from ext_dir/configure.phtml, false if it does + * not exist. + */ + public function getConfigureView() { + $filename = $this->path . '/configure.phtml'; + if (!file_exists($filename)) { + return false; + } + + ob_start(); + include($filename); + return ob_get_clean(); + } + + /** + * Handle the configure action. + * + * It must be redefined by child classes. + */ + public function handleConfigureAction() {} + + /** + * Getters and setters. + */ + public function getName() { + return $this->name; + } + public function getEntrypoint() { + return $this->entrypoint; + } + public function getPath() { + return $this->path; + } + public function getAuthor() { + return $this->author; + } + public function getDescription() { + return $this->description; + } + public function getVersion() { + return $this->version; + } + public function getType() { + return $this->type; + } + private function setType($type) { + if (!in_array($type, self::$authorized_types)) { + throw new Minz_ExtensionException('invalid `type` info', $this->name); + } + $this->type = $type; + } + + /** + * Return the url for a given file. + * + * @param $filename name of the file to serve. + * @param $type the type (js or css) of the file to serve. + * @return the url corresponding to the file. + */ + public function getFileUrl($filename, $type) { + $dir = substr(strrchr($this->path, '/'), 1); + $file_name_url = urlencode($dir . '/static/' . $filename); + + $absolute_path = $this->path . '/static/' . $filename; + $mtime = @filemtime($absolute_path); + + $url = '/ext.php?f=' . $file_name_url . + '&t=' . $type . + '&' . $mtime; + return Minz_Url::display($url, 'php'); + } + + /** + * Register a controller in the Dispatcher. + * + * @param @base_name the base name of the controller. Final name will be: + * FreshExtension_<base_name>_Controller. + */ + public function registerController($base_name) { + Minz_Dispatcher::registerController($base_name, $this->path); + } + + /** + * Register the views in order to be accessible by the application. + */ + public function registerViews() { + Minz_View::addBasePathname($this->path); + } + + /** + * Register i18n files from ext_dir/i18n/ + */ + public function registerTranslates() { + $i18n_dir = $this->path . '/i18n'; + Minz_Translate::registerPath($i18n_dir); + } + + /** + * Register a new hook. + * + * @param $hook_name the hook name (must exist). + * @param $hook_function the function name to call (must be callable). + */ + public function registerHook($hook_name, $hook_function) { + Minz_ExtensionManager::addHook($hook_name, $hook_function, $this); + } +} diff --git a/lib/Minz/ExtensionException.php b/lib/Minz/ExtensionException.php new file mode 100644 index 000000000..647f1a9b9 --- /dev/null +++ b/lib/Minz/ExtensionException.php @@ -0,0 +1,15 @@ +<?php + +class Minz_ExtensionException extends Minz_Exception { + public function __construct ($message, $extension_name = false, $code = self::ERROR) { + if ($extension_name) { + $message = 'An error occured in `' . $extension_name + . '` extension with the message: ' . $message; + } else { + $message = 'An error occured in an unnamed ' + . 'extension with the message: ' . $message; + } + + parent::__construct($message, $code); + } +} diff --git a/lib/Minz/ExtensionManager.php b/lib/Minz/ExtensionManager.php new file mode 100644 index 000000000..c5c68a8d4 --- /dev/null +++ b/lib/Minz/ExtensionManager.php @@ -0,0 +1,301 @@ +<?php + +/** + * An extension manager to load extensions present in EXTENSIONS_PATH. + * + * @todo see coding style for methods!! + */ +class Minz_ExtensionManager { + private static $ext_metaname = 'metadata.json'; + private static $ext_entry_point = 'extension.php'; + private static $ext_list = array(); + private static $ext_list_enabled = array(); + + private static $ext_auto_enabled = array(); + + // List of available hooks. Please keep this list sorted. + private static $hook_list = array( + 'entry_before_display' => array( // function($entry) -> Entry | null + 'list' => array(), + 'signature' => 'OneToOne', + ), + 'entry_before_insert' => array( // function($entry) -> Entry | null + 'list' => array(), + 'signature' => 'OneToOne', + ), + 'feed_before_insert' => array( // function($feed) -> Feed | null + 'list' => array(), + 'signature' => 'OneToOne', + ), + 'post_update' => array( // function(none) -> none + 'list' => array(), + 'signature' => 'NoneToNone', + ), + ); + private static $ext_to_hooks = array(); + + /** + * Initialize the extension manager by loading extensions in EXTENSIONS_PATH. + * + * A valid extension is a directory containing metadata.json and + * extension.php files. + * metadata.json is a JSON structure where the only required fields are + * `name` and `entry_point`. + * extension.php should contain at least a class named <name>Extension where + * <name> must match with the entry point in metadata.json. This class must + * inherit from Minz_Extension class. + */ + public static function init() { + $list_potential_extensions = array_values(array_diff( + scandir(EXTENSIONS_PATH), + array('..', '.') + )); + + $system_conf = Minz_Configuration::get('system'); + self::$ext_auto_enabled = $system_conf->extensions_enabled; + + foreach ($list_potential_extensions as $ext_dir) { + $ext_pathname = EXTENSIONS_PATH . '/' . $ext_dir; + if (!is_dir($ext_pathname)) { + continue; + } + $metadata_filename = $ext_pathname . '/' . self::$ext_metaname; + + // Try to load metadata file. + if (!file_exists($metadata_filename)) { + // No metadata file? Invalid! + continue; + } + $meta_raw_content = file_get_contents($metadata_filename); + $meta_json = json_decode($meta_raw_content, true); + if (!$meta_json || !self::isValidMetadata($meta_json)) { + // metadata.json is not a json file? Invalid! + // or metadata.json is invalid (no required information), invalid! + Minz_Log::warning('`' . $metadata_filename . '` is not a valid metadata file'); + continue; + } + + $meta_json['path'] = $ext_pathname; + + // Try to load extension itself + $extension = self::load($meta_json); + if (!is_null($extension)) { + self::register($extension); + } + } + } + + /** + * Indicates if the given parameter is a valid metadata array. + * + * Required fields are: + * - `name`: the name of the extension + * - `entry_point`: a class name to load the extension source code + * If the extension class name is `TestExtension`, entry point will be `Test`. + * `entry_point` must be composed of alphanumeric characters. + * + * @param $meta is an array of values. + * @return true if the array is valid, false else. + */ + public static function isValidMetadata($meta) { + $valid_chars = array('_'); + return !(empty($meta['name']) || + empty($meta['entrypoint']) || + !ctype_alnum(str_replace($valid_chars, '', $meta['entrypoint']))); + } + + /** + * Load the extension source code based on info metadata. + * + * @param $info an array containing information about extension. + * @return an extension inheriting from Minz_Extension. + */ + public static function load($info) { + $entry_point_filename = $info['path'] . '/' . self::$ext_entry_point; + $ext_class_name = $info['entrypoint'] . 'Extension'; + + include_once($entry_point_filename); + + // Test if the given extension class exists. + if (!class_exists($ext_class_name)) { + Minz_Log::warning('`' . $ext_class_name . + '` cannot be found in `' . $entry_point_filename . '`'); + return null; + } + + // Try to load the class. + $extension = null; + try { + $extension = new $ext_class_name($info); + } catch (Minz_ExtensionException $e) { + // We cannot load the extension? Invalid! + Minz_Log::warning('In `' . $metadata_filename . '`: ' . $e->getMessage()); + return null; + } + + // Test if class is correct. + if (!($extension instanceof Minz_Extension)) { + Minz_Log::warning('`' . $ext_class_name . + '` is not an instance of `Minz_Extension`'); + return null; + } + + return $extension; + } + + /** + * Add the extension to the list of the known extensions ($ext_list). + * + * If the extension is present in $ext_auto_enabled and if its type is "system", + * it will be enabled in the same time. + * + * @param $ext a valid extension. + */ + public static function register($ext) { + $name = $ext->getName(); + self::$ext_list[$name] = $ext; + + if ($ext->getType() === 'system' && + in_array($name, self::$ext_auto_enabled)) { + self::enable($ext->getName()); + } + + self::$ext_to_hooks[$name] = array(); + } + + /** + * Enable an extension so it will be called when necessary. + * + * The extension init() method will be called. + * + * @param $ext_name is the name of a valid extension present in $ext_list. + */ + public static function enable($ext_name) { + if (isset(self::$ext_list[$ext_name])) { + $ext = self::$ext_list[$ext_name]; + self::$ext_list_enabled[$ext_name] = $ext; + $ext->enable(); + $ext->init(); + } + } + + /** + * Enable a list of extensions. + * + * @param $ext_list the names of extensions we want to load. + */ + public static function enableByList($ext_list) { + foreach ($ext_list as $ext_name) { + self::enable($ext_name); + } + } + + /** + * Return a list of extensions. + * + * @param $only_enabled if true returns only the enabled extensions (false by default). + * @return an array of extensions. + */ + public static function listExtensions($only_enabled = false) { + if ($only_enabled) { + return self::$ext_list_enabled; + } else { + return self::$ext_list; + } + } + + /** + * Return an extension by its name. + * + * @param $ext_name the name of the extension. + * @return the corresponding extension or null if it doesn't exist. + */ + public static function findExtension($ext_name) { + if (!isset(self::$ext_list[$ext_name])) { + return null; + } + + return self::$ext_list[$ext_name]; + } + + /** + * Add a hook function to a given hook. + * + * The hook name must be a valid one. For the valid list, see self::$hook_list + * array keys. + * + * @param $hook_name the hook name (must exist). + * @param $hook_function the function name to call (must be callable). + * @param $ext the extension which register the hook. + */ + public static function addHook($hook_name, $hook_function, $ext) { + if (isset(self::$hook_list[$hook_name]) && is_callable($hook_function)) { + self::$hook_list[$hook_name]['list'][] = $hook_function; + self::$ext_to_hooks[$ext->getName()][] = $hook_name; + } + } + + /** + * Call functions related to a given hook. + * + * The hook name must be a valid one. For the valid list, see self::$hook_list + * array keys. + * + * @param $hook_name the hook to call. + * @param additionnal parameters (for signature, please see self::$hook_list). + * @return the final result of the called hook. + */ + public static function callHook($hook_name) { + if (!isset(self::$hook_list[$hook_name])) { + return; + } + + $signature = self::$hook_list[$hook_name]['signature']; + $signature = 'self::call' . $signature; + $args = func_get_args(); + + return call_user_func_array($signature, $args); + } + + /** + * Call a hook which takes one argument and return a result. + * + * The result is chained between the extension, for instance, first extension + * hook will receive the initial argument and return a result which will be + * passed as an argument to the next extension hook and so on. + * + * If a hook return a null value, the method is stopped and return null. + * + * @param $hook_name is the hook to call. + * @param $arg is the argument to pass to the first extension hook. + * @return the final chained result of the hooks. If nothing is changed, + * the initial argument is returned. + */ + private static function callOneToOne($hook_name, $arg) { + $result = $arg; + foreach (self::$hook_list[$hook_name]['list'] as $function) { + $result = call_user_func($function, $arg); + + if (is_null($result)) { + break; + } + + $arg = $result; + } + return $result; + } + + /** + * Call a hook which takes no argument and returns nothing. + * + * This case is simpler than callOneToOne because hooks are called one by + * one, without any consideration of argument nor result. + * + * @param $hook_name is the hook to call. + */ + private static function callNoneToNone($hook_name) { + foreach (self::$hook_list[$hook_name]['list'] as $function) { + call_user_func($function); + } + } +} diff --git a/lib/Minz/FrontController.php b/lib/Minz/FrontController.php index e95c56bf3..f9eff3db6 100644 --- a/lib/Minz/FrontController.php +++ b/lib/Minz/FrontController.php @@ -30,14 +30,13 @@ class Minz_FrontController { * Initialise le dispatcher, met à jour la Request */ public function __construct () { - if (LOG_PATH === false) { - $this->killApp ('Path not found: LOG_PATH'); - } - try { - Minz_Configuration::init (); + Minz_Configuration::register('system', + DATA_PATH . '/config.php', + DATA_PATH . '/config.default.php'); + $this->setReporting(); - Minz_Request::init (); + Minz_Request::init(); $url = $this->buildUrl(); $url['params'] = array_merge ( @@ -114,4 +113,23 @@ class Minz_FrontController { } exit ('### Application problem ###<br />'."\n".$txt); } + + private function setReporting() { + $conf = Minz_Configuration::get('system'); + switch($conf->environment) { + case 'production': + error_reporting(E_ALL); + ini_set('display_errors','Off'); + ini_set('log_errors', 'On'); + break; + case 'development': + error_reporting(E_ALL); + ini_set('display_errors','On'); + ini_set('log_errors', 'On'); + break; + case 'silent': + error_reporting(0); + break; + } + } } diff --git a/lib/Minz/Log.php b/lib/Minz/Log.php index d3eaec2ae..9559a0bd4 100644 --- a/lib/Minz/Log.php +++ b/lib/Minz/Log.php @@ -28,16 +28,25 @@ class Minz_Log { * - level = NOTICE et environment = PRODUCTION * @param $information message d'erreur / information à enregistrer * @param $level niveau d'erreur - * @param $file_name fichier de log, par défaut LOG_PATH/application.log + * @param $file_name fichier de log */ public static function record ($information, $level, $file_name = null) { - $env = Minz_Configuration::environment (); + try { + $conf = Minz_Configuration::get('system'); + $env = $conf->environment; + } catch (Minz_ConfigurationException $e) { + $env = 'production'; + } - if (! ($env === Minz_Configuration::SILENT - || ($env === Minz_Configuration::PRODUCTION + if (! ($env === 'silent' + || ($env === 'production' && ($level >= Minz_Log::NOTICE)))) { if ($file_name === null) { - $file_name = LOG_PATH . '/' . Minz_Session::param('currentUser', '_') . '.log'; + $username = Minz_Session::param('currentUser', ''); + if ($username == '') { + $username = '_'; + } + $file_name = join_path(USERS_PATH, $username, 'log.txt'); } switch ($level) { @@ -71,7 +80,7 @@ class Minz_Log { * Automatise le log des variables globales $_GET et $_POST * Fait appel à la fonction record(...) * Ne fonctionne qu'en environnement "development" - * @param $file_name fichier de log, par défaut LOG_PATH/application.log + * @param $file_name fichier de log */ public static function recordRequest($file_name = null) { $msg_get = str_replace("\n", '', '$_GET content : ' . print_r($_GET, true)); diff --git a/lib/Minz/ModelPdo.php b/lib/Minz/ModelPdo.php index 827c89c69..caab1d114 100644 --- a/lib/Minz/ModelPdo.php +++ b/lib/Minz/ModelPdo.php @@ -16,7 +16,7 @@ class Minz_ModelPdo { public static $useSharedBd = true; private static $sharedBd = null; private static $sharedPrefix; - private static $has_transaction = false; + private static $sharedCurrentUser; protected static $sharedDbType; /** @@ -36,54 +36,61 @@ class Minz_ModelPdo { * HOST, BASE, USER et PASS définies dans le fichier de configuration */ public function __construct($currentUser = null) { - if (self::$useSharedBd && self::$sharedBd != null && $currentUser === null) { + if ($currentUser === null) { + $currentUser = Minz_Session::param('currentUser'); + } + if (self::$useSharedBd && self::$sharedBd != null && + ($currentUser == null || $currentUser === self::$sharedCurrentUser)) { $this->bd = self::$sharedBd; $this->prefix = self::$sharedPrefix; + $this->current_user = self::$sharedCurrentUser; return; } + $this->current_user = $currentUser; + self::$sharedCurrentUser = $currentUser; - $db = Minz_Configuration::dataBase(); + $conf = Minz_Configuration::get('system'); + $db = $conf->db; - if ($currentUser === null) { - $currentUser = Minz_Session::param('currentUser', '_'); - } - $this->current_user = $currentUser; + $driver_options = isset($conf->db['pdo_options']) && is_array($conf->db['pdo_options']) ? $conf->db['pdo_options'] : array(); + $dbServer = parse_url('db://' . $db['host']); try { - $type = $db['type']; - if ($type === 'mysql') { - $string = 'mysql:host=' . $db['host'] - . ';dbname=' . $db['base'] - . ';charset=utf8'; - $driver_options = array( - PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8', - ); - $this->prefix = $db['prefix'] . $currentUser . '_'; - } elseif ($type === 'sqlite') { - $string = 'sqlite:' . DATA_PATH . '/' . $currentUser . '.sqlite'; - $driver_options = array( - //PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, - ); - $this->prefix = ''; - } else { - throw new Minz_PDOConnectionException( - 'Invalid database type!', - $db['user'], Minz_Exception::ERROR - ); - } - self::$sharedDbType = $type; - self::$sharedPrefix = $this->prefix; - - $this->bd = new MinzPDO( - $string, - $db['user'], - $db['password'], - $driver_options - ); - if ($type === 'sqlite') { - $this->bd->exec('PRAGMA foreign_keys = ON;'); + switch ($db['type']) { + case 'mysql': + $string = 'mysql:host=' . $dbServer['host'] . ';dbname=' . $db['base'] . ';charset=utf8mb4'; + if (!empty($dbServer['port'])) { + $string .= ';port=' . $dbServer['port']; + } + $driver_options[PDO::MYSQL_ATTR_INIT_COMMAND] = 'SET NAMES utf8mb4'; + $this->prefix = $db['prefix'] . $currentUser . '_'; + $this->bd = new MinzPDOMySql($string, $db['user'], $db['password'], $driver_options); + break; + case 'sqlite': + $string = 'sqlite:' . join_path(DATA_PATH, 'users', $currentUser, 'db.sqlite'); + $this->prefix = ''; + $this->bd = new MinzPDOMSQLite($string, $db['user'], $db['password'], $driver_options); + $this->bd->exec('PRAGMA foreign_keys = ON;'); + break; + case 'pgsql': + $string = 'pgsql:host=' . $dbServer['host'] . ';dbname=' . $db['base']; + if (!empty($dbServer['port'])) { + $string .= ';port=' . $dbServer['port']; + } + $this->prefix = $db['prefix'] . $currentUser . '_'; + $this->bd = new MinzPDOPGSQL($string, $db['user'], $db['password'], $driver_options); + $this->bd->exec("SET NAMES 'UTF8';"); + break; + default: + throw new Minz_PDOConnectionException( + 'Invalid database type!', + $db['user'], Minz_Exception::ERROR + ); + break; } self::$sharedBd = $this->bd; + self::$sharedDbType = $db['type']; + self::$sharedPrefix = $this->prefix; } catch (Exception $e) { throw new Minz_PDOConnectionException( $string, @@ -94,24 +101,27 @@ class Minz_ModelPdo { public function beginTransaction() { $this->bd->beginTransaction(); - self::$has_transaction = true; } - public function hasTransaction() { - return self::$has_transaction; + public function inTransaction() { + return $this->bd->inTransaction(); //requires PHP >= 5.3.3 } public function commit() { $this->bd->commit(); - self::$has_transaction = false; } public function rollBack() { $this->bd->rollBack(); - self::$has_transaction = false; } public static function clean() { self::$sharedBd = null; self::$sharedPrefix = ''; } + + public function disableBuffering() { + if ((self::$sharedDbType === 'mysql') && defined('PDO::MYSQL_ATTR_USE_BUFFERED_QUERY')) { + $this->bd->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false); + } + } } class MinzPDO extends PDO { @@ -121,13 +131,43 @@ class MinzPDO extends PDO { } } + protected function compatibility($statement) { + return $statement; + } + public function prepare($statement, $driver_options = array()) { MinzPDO::check($statement); + $statement = $this->compatibility($statement); return parent::prepare($statement, $driver_options); } public function exec($statement) { MinzPDO::check($statement); + $statement = $this->compatibility($statement); return parent::exec($statement); } + + public function query($statement) { + MinzPDO::check($statement); + $statement = $this->compatibility($statement); + return parent::query($statement); + } +} + +class MinzPDOMySql extends MinzPDO { + public function lastInsertId($name = null) { + return parent::lastInsertId(); //We discard the name, only used by PostgreSQL + } +} + +class MinzPDOMSQLite extends MinzPDO { + public function lastInsertId($name = null) { + return parent::lastInsertId(); //We discard the name, only used by PostgreSQL + } +} + +class MinzPDOPGSQL extends MinzPDO { + protected function compatibility($statement) { + return str_replace(array('`', ' LIKE '), array('"', ' ILIKE '), $statement); + } } diff --git a/lib/Minz/Request.php b/lib/Minz/Request.php index f7a24c026..f80b707d6 100644 --- a/lib/Minz/Request.php +++ b/lib/Minz/Request.php @@ -45,6 +45,13 @@ class Minz_Request { public static function defaultActionName() { return self::$default_action_name; } + public static function currentRequest() { + return array( + 'c' => self::$controller_name, + 'a' => self::$action_name, + 'params' => self::$params, + ); + } /** * Setteurs @@ -78,43 +85,64 @@ class Minz_Request { } /** - * Retourn le nom de domaine du site + * Return true if the request is over HTTPS, false otherwise (HTTP) */ - public static function getDomainName() { - return $_SERVER['HTTP_HOST']; + public static function isHttps() { + if (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])) { + return strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) === 'https'; + } else { + return isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on'; + } } /** - * Détermine la base de l'url - * @return la base de l'url + * Try to guess the base URL from $_SERVER information + * + * @return the base url (e.g. http://example.com/) */ - public static function getBaseUrl() { - $defaultBaseUrl = Minz_Configuration::baseUrl(); - if (!empty($defaultBaseUrl)) { - return $defaultBaseUrl; - } elseif (isset($_SERVER['REQUEST_URI'])) { - return dirname($_SERVER['REQUEST_URI']) . '/'; + public static function guessBaseUrl() { + $url = 'http'; + + $https = self::isHttps(); + + if (!empty($_SERVER['HTTP_HOST'])) { + $host = $_SERVER['HTTP_HOST']; + } elseif (!empty($_SERVER['SERVER_NAME'])) { + $host = $_SERVER['SERVER_NAME']; } else { - return '/'; + $host = 'localhost'; } - } - /** - * Récupère l'URI de la requête - * @return l'URI - */ - public static function getURI() { - if (isset($_SERVER['REQUEST_URI'])) { - $base_url = self::getBaseUrl(); - $uri = $_SERVER['REQUEST_URI']; + if (!empty($_SERVER['HTTP_X_FORWARDED_PORT'])) { + $port = intval($_SERVER['HTTP_X_FORWARDED_PORT']); + } elseif (!empty($_SERVER['SERVER_PORT'])) { + $port = intval($_SERVER['SERVER_PORT']); + } else { + $port = $https ? 443 : 80; + } - $len_base_url = strlen($base_url); - $real_uri = substr($uri, $len_base_url); + if ($https) { + $url .= 's://' . $host . ($port == 443 ? '' : ':' . $port); } else { - $real_uri = ''; + $url .= '://' . $host . ($port == 80 ? '' : ':' . $port); + } + if (isset($_SERVER['REQUEST_URI'])) { + $path = $_SERVER['REQUEST_URI']; + $url .= substr($path, -1) === '/' ? substr($path, 0, -1) : dirname($path); } - return $real_uri; + return filter_var($url, FILTER_SANITIZE_URL); + } + + /** + * Return the base_url from configuration and add a suffix if given. + * + * @return the base_url with a suffix. + */ + public static function getBaseUrl() { + $conf = Minz_Configuration::get('system'); + $url = rtrim($conf->base_url, '/\\'); + return filter_var($url, FILTER_SANITIZE_URL); } /** diff --git a/lib/Minz/Session.php b/lib/Minz/Session.php index af4de75bb..c94f2b646 100644 --- a/lib/Minz/Session.php +++ b/lib/Minz/Session.php @@ -55,18 +55,25 @@ class Minz_Session { if (!$force) { self::_param('language', $language); - Minz_Translate::reset(); + Minz_Translate::reset($language); } } + public static function getCookieDir() { + // Get the script_name (e.g. /p/i/index.php) and keep only the path. + $cookie_dir = empty($_SERVER['REQUEST_URI']) ? '/' : $_SERVER['REQUEST_URI']; + if (substr($cookie_dir, -1) !== '/') { + $cookie_dir = dirname($cookie_dir) . '/'; + } + return $cookie_dir; + } /** * Spécifie la durée de vie des cookies * @param $l la durée de vie */ public static function keepCookie($l) { - $cookie_dir = empty($_SERVER['REQUEST_URI']) ? '' : $_SERVER['REQUEST_URI']; - session_set_cookie_params($l, $cookie_dir, '', false, true); + session_set_cookie_params($l, self::getCookieDir(), '', Minz_Request::isHttps(), true); } @@ -79,11 +86,11 @@ class Minz_Session { } public static function deleteLongTermCookie($name) { - setcookie($name, '', 1, '', '', false, true); + setcookie($name, '', 1, '', '', Minz_Request::isHttps(), true); } public static function setLongTermCookie($name, $value, $expire) { - setcookie($name, $value, $expire, '', '', false, true); + setcookie($name, $value, $expire, '', '', Minz_Request::isHttps(), true); } public static function getLongTermCookie($name) { diff --git a/lib/Minz/Translate.php b/lib/Minz/Translate.php index 8c2f90041..baddcb424 100644 --- a/lib/Minz/Translate.php +++ b/lib/Minz/Translate.php @@ -5,71 +5,222 @@ */ /** - * La classe Translate se charge de la traduction - * Utilise les fichiers du répertoire /app/i18n/ + * This class is used for the internationalization. + * It uses files in `./app/i18n/` */ class Minz_Translate { /** - * $language est la langue à afficher + * $path_list is the list of registered base path to search translations. */ - private static $language; - + private static $path_list = array(); + + /** + * $lang_name is the name of the current language to use. + */ + private static $lang_name; + + /** + * $lang_files is a list of registered i18n files. + */ + private static $lang_files = array(); + /** - * $translates est le tableau de correspondance - * $key => $traduction + * $translates is a cache for i18n translation. */ private static $translates = array(); - + + /** + * Init the translation object. + * @param $lang_name the lang to show. + */ + public static function init($lang_name = null) { + self::$lang_name = $lang_name; + self::$lang_files = array(); + self::$translates = array(); + self::registerPath(APP_PATH . '/i18n'); + foreach (self::$path_list as $path) { + self::loadLang($path); + } + } + + /** + * Reset the translation object with a new language. + * @param $lang_name the new language to use + */ + public static function reset($lang_name) { + self::$lang_name = $lang_name; + self::$lang_files = array(); + self::$translates = array(); + foreach (self::$path_list as $path) { + self::loadLang($path); + } + } + + /** + * Return the list of available languages. + * @return an array containing langs found in different registered paths. + */ + public static function availableLanguages() { + $list_langs = array(); + + foreach (self::$path_list as $path) { + $path_langs = array_values(array_diff( + scandir($path), + array('..', '.') + )); + + $list_langs = array_merge($list_langs, $path_langs); + } + + return array_unique($list_langs); + } + + /** + * Register a new path. + * @param $path a path containing i18n directories (e.g. ./en/, ./fr/). + */ + public static function registerPath($path) { + if (in_array($path, self::$path_list)) { + return; + } + + self::$path_list[] = $path; + self::loadLang($path); + } + /** - * Inclus le fichier de langue qui va bien - * l'enregistre dans $translates + * Load translations of the current language from the given path. + * @param $path the path containing i18n directories. */ - public static function init() { - $l = Minz_Configuration::language(); - self::$language = Minz_Session::param('language', $l); - - $l_path = APP_PATH . '/i18n/' . self::$language . '.php'; - - if (file_exists($l_path)) { - self::$translates = include($l_path); + private static function loadLang($path) { + $lang_path = $path . '/' . self::$lang_name; + if (!file_exists($lang_path) || is_null(self::$lang_name)) { + // The lang path does not exist, nothing more to do. + return; + } + + $list_i18n_files = array_values(array_diff( + scandir($lang_path), + array('..', '.') + )); + + // Each file basename correspond to a top-level i18n key. For each of + // these keys we store the file pathname and mark translations must be + // reloaded (by setting $translates[$i18n_key] to null). + foreach ($list_i18n_files as $i18n_filename) { + $i18n_key = basename($i18n_filename, '.php'); + if (!isset(self::$lang_files[$i18n_key])) { + self::$lang_files[$i18n_key] = array(); + } + self::$lang_files[$i18n_key][] = $lang_path . '/' . $i18n_filename; + self::$translates[$i18n_key] = null; } } - + /** - * Alias de init + * Load the files associated to $key into $translates. + * @param $key the top level i18n key we want to load. */ - public static function reset() { - self::init(); + private static function loadKey($key) { + // The top level key is not in $lang_files, it means it does not exist! + if (!isset(self::$lang_files[$key])) { + Minz_Log::debug($key . ' is not a valid top level key'); + return false; + } + + self::$translates[$key] = array(); + + foreach (self::$lang_files[$key] as $lang_pathname) { + $i18n_array = include($lang_pathname); + if (!is_array($i18n_array)) { + Minz_Log::warning('`' . $lang_pathname . '` does not contain a PHP array'); + continue; + } + + // We must avoid to erase previous data so we just override them if + // needed. + self::$translates[$key] = array_replace_recursive( + self::$translates[$key], $i18n_array + ); + } + + return true; } - + /** - * Traduit une clé en sa valeur du tableau $translates - * @param $key la clé à traduire - * @return la valeur correspondante à la clé - * > si non présente dans le tableau, on retourne la clé elle-même + * Translate a key into its corresponding value based on selected language. + * @param $key the key to translate. + * @param additional parameters for variable keys. + * @return the value corresponding to the key. + * If no value is found, return the key itself. */ public static function t($key) { - $translate = $key; - - if (isset(self::$translates[$key])) { - $translate = self::$translates[$key]; + $group = explode('.', $key); + + if (count($group) < 2) { + Minz_Log::debug($key . ' is not in a valid format'); + $top_level = 'gen'; + } else { + $top_level = array_shift($group); + } + + // If $translates[$top_level] is null it means we have to load the + // corresponding files. + if (!isset(self::$translates[$top_level]) || + is_null(self::$translates[$top_level])) { + $res = self::loadKey($top_level); + if (!$res) { + return $key; + } + } + + // Go through the i18n keys to get the correct translation value. + $translates = self::$translates[$top_level]; + $size_group = count($group); + $level_processed = 0; + $translation_value = $key; + foreach ($group as $i18n_level) { + $level_processed++; + if (!isset($translates[$i18n_level])) { + Minz_Log::debug($key . ' is not a valid key'); + return $key; + } + + if ($level_processed < $size_group) { + $translates = $translates[$i18n_level]; + } else { + $translation_value = $translates[$i18n_level]; + } } + if (is_array($translation_value)) { + if (isset($translation_value['_'])) { + $translation_value = $translation_value['_']; + } else { + Minz_Log::debug($key . ' is not a valid key'); + return $key; + } + } + + // Get the facultative arguments to replace i18n variables. $args = func_get_args(); unset($args[0]); - - return vsprintf($translate, $args); + + return vsprintf($translation_value, $args); } - + /** - * Retourne la langue utilisée actuellement - * @return la langue + * Return the current language. */ public static function language() { - return self::$language; + return self::$lang_name; } } + +/** + * Alias for Minz_Translate::t() + */ function _t($key) { $args = func_get_args(); unset($args[0]); diff --git a/lib/Minz/Url.php b/lib/Minz/Url.php index e9f9a69ba..99c0443c1 100644 --- a/lib/Minz/Url.php +++ b/lib/Minz/Url.php @@ -10,7 +10,6 @@ class Minz_Url { * $url['c'] = controller * $url['a'] = action * $url['params'] = tableau des paramètres supplémentaires - * $url['protocol'] = protocole à utiliser (http par défaut) * ou comme une chaîne de caractère * @param $encodage pour indiquer comment encoder les & (& ou & pour html) * @return l'url formatée @@ -19,71 +18,77 @@ class Minz_Url { $isArray = is_array($url); if ($isArray) { - $url = self::checkUrl ($url); + $url = self::checkUrl($url); } $url_string = ''; if ($absolute) { - if ($isArray && isset ($url['protocol'])) { - $protocol = $url['protocol']; - } elseif (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') { - $protocol = 'https:'; - } else { - $protocol = 'http:'; + $url_string = Minz_Request::getBaseUrl(); + if ($url_string == '') { + $url_string = Minz_Request::guessBaseUrl(); + } + if ($isArray) { + $url_string .= PUBLIC_TO_INDEX_PATH; + } + if ($absolute === 'root') { + $url_string = parse_url($url_string, PHP_URL_PATH); } - $url_string = $protocol . '//' . Minz_Request::getDomainName () . Minz_Request::getBaseUrl (); } else { $url_string = $isArray ? '.' : PUBLIC_RELATIVE; } if ($isArray) { - $url_string .= self::printUri ($url, $encodage); + $url_string .= self::printUri($url, $encodage); + } elseif ($encodage === 'html') { + $url_string = Minz_Helper::htmlspecialchars_utf8($url_string . $url); } else { $url_string .= $url; } return $url_string; } - + /** * Construit l'URI d'une URL * @param l'url sous forme de tableau * @param $encodage pour indiquer comment encoder les & (& ou & pour html) * @return l'uri sous la forme ?key=value&key2=value2 */ - private static function printUri ($url, $encodage) { + private static function printUri($url, $encodage) { $uri = ''; $separator = '?'; - - if($encodage == 'html') { + + if ($encodage === 'html') { $and = '&'; } else { $and = '&'; } - - if (isset ($url['c']) - && $url['c'] != Minz_Request::defaultControllerName ()) { + + if (isset($url['c']) + && $url['c'] != Minz_Request::defaultControllerName()) { $uri .= $separator . 'c=' . $url['c']; $separator = $and; } - - if (isset ($url['a']) - && $url['a'] != Minz_Request::defaultActionName ()) { + + if (isset($url['a']) + && $url['a'] != Minz_Request::defaultActionName()) { $uri .= $separator . 'a=' . $url['a']; $separator = $and; } - - if (isset ($url['params'])) { + + if (isset($url['params'])) { + unset($url['params']['c']); + unset($url['params']['a']); foreach ($url['params'] as $key => $param) { - $uri .= $separator . $key . '=' . $param; + $uri .= $separator . urlencode($key) . '=' . urlencode($param); $separator = $and; } } - + return $uri; } - + /** * Vérifie que les éléments du tableau représentant une url soit ok * @param l'url sous forme de tableau (sinon renverra directement $url) @@ -91,7 +96,7 @@ class Minz_Url { */ public static function checkUrl ($url) { $url_checked = $url; - + if (is_array ($url)) { if (!isset ($url['c'])) { $url_checked['c'] = Minz_Request::defaultControllerName (); @@ -103,7 +108,7 @@ class Minz_Url { $url_checked['params'] = array (); } } - + return $url_checked; } } diff --git a/lib/Minz/View.php b/lib/Minz/View.php index b40448491..8c5230ab6 100644 --- a/lib/Minz/View.php +++ b/lib/Minz/View.php @@ -13,8 +13,9 @@ class Minz_View { const LAYOUT_FILENAME = '/layout.phtml'; private $view_filename = ''; - private $use_layout = null; + private $use_layout = true; + private static $base_pathnames = array(APP_PATH); private static $title = ''; private static $styles = array (); private static $scripts = array (); @@ -28,26 +29,35 @@ class Minz_View { public function __construct () { $this->change_view(Minz_Request::controllerName(), Minz_Request::actionName()); - self::$title = Minz_Configuration::title (); + + $conf = Minz_Configuration::get('system'); + self::$title = $conf->title; } /** * Change le fichier de vue en fonction d'un controller / action */ public function change_view($controller_name, $action_name) { - $this->view_filename = APP_PATH - . self::VIEWS_PATH_NAME . '/' + $this->view_filename = self::VIEWS_PATH_NAME . '/' . $controller_name . '/' . $action_name . '.phtml'; } /** + * Add a base pathname to search views. + * + * New pathnames will be added at the beginning of the list. + * + * @param $base_pathname the new base pathname. + */ + public static function addBasePathname($base_pathname) { + array_unshift(self::$base_pathnames, $base_pathname); + } + + /** * Construit la vue */ public function build () { - if ($this->use_layout === null) { //TODO: avoid file_exists and require views to be explicit - $this->use_layout = file_exists (APP_PATH . self::LAYOUT_PATH_NAME . self::LAYOUT_FILENAME); - } if ($this->use_layout) { $this->buildLayout (); } else { @@ -56,21 +66,40 @@ class Minz_View { } /** + * Include a view file. + * + * The file is searched inside list of $base_pathnames. + * + * @param $filename the name of the file to include. + * @return true if the file has been included, false else. + */ + private function includeFile($filename) { + // We search the filename in the list of base pathnames. Only the first view + // found is considered. + foreach (self::$base_pathnames as $base) { + $absolute_filename = $base . $filename; + if (file_exists($absolute_filename)) { + include $absolute_filename; + return true; + } + } + + return false; + } + + /** * Construit le layout */ public function buildLayout () { - include ( - APP_PATH - . self::LAYOUT_PATH_NAME - . self::LAYOUT_FILENAME - ); + header('Content-Type: text/html; charset=UTF-8'); + $this->includeFile(self::LAYOUT_PATH_NAME . self::LAYOUT_FILENAME); } /** * Affiche la Vue en elle-même */ public function render () { - if ((include($this->view_filename)) === false) { + if (!$this->includeFile($this->view_filename)) { Minz_Log::notice('File not found: `' . $this->view_filename . '`'); } } @@ -80,11 +109,8 @@ class Minz_View { * @param $part l'élément partial à ajouter */ public function partial ($part) { - $fic_partial = APP_PATH - . self::LAYOUT_PATH_NAME . '/' - . $part . '.phtml'; - - if ((include($fic_partial)) === false) { + $fic_partial = self::LAYOUT_PATH_NAME . '/' . $part . '.phtml'; + if (!$this->includeFile($fic_partial)) { Minz_Log::warning('File not found: `' . $fic_partial . '`'); } } @@ -94,11 +120,8 @@ class Minz_View { * @param $helper l'élément à afficher */ public function renderHelper ($helper) { - $fic_helper = APP_PATH - . '/views/helpers/' - . $helper . '.phtml'; - - if ((include($fic_helper)) === false) {; + $fic_helper = '/views/helpers/' . $helper . '.phtml'; + if (!$this->includeFile($fic_helper)) { Minz_Log::warning('File not found: `' . $fic_helper . '`'); } } diff --git a/lib/SimplePie/SimplePie.php b/lib/SimplePie/SimplePie.php index 06c100f59..0f2fdbb87 100644 --- a/lib/SimplePie/SimplePie.php +++ b/lib/SimplePie/SimplePie.php @@ -75,6 +75,12 @@ define('SIMPLEPIE_USERAGENT', SIMPLEPIE_NAME . '/' . SIMPLEPIE_VERSION . ' (Feed define('SIMPLEPIE_LINKBACK', '<a href="' . SIMPLEPIE_URL . '" title="' . SIMPLEPIE_NAME . ' ' . SIMPLEPIE_VERSION . '">' . SIMPLEPIE_NAME . '</a>'); /** + * Use syslog to report HTTP requests done by SimplePie. + * @see SimplePie::set_syslog() + */ +define('SIMPLEPIE_SYSLOG', true); //FreshRSS + +/** * No Autodiscovery * @see SimplePie::set_autodiscovery_level() */ @@ -450,7 +456,7 @@ class SimplePie * @see SimplePie::subscribe_url() * @access private */ - public $permanent_url = null; //FreshRSS + public $permanent_url = null; /** * @var object Instance of SimplePie_File to use as a feed @@ -474,6 +480,13 @@ class SimplePie public $timeout = 10; /** + * @var array Custom curl options + * @see SimplePie::set_curl_options() + * @access private + */ + public $curl_options = array(); + + /** * @var bool Forces fsockopen() to be used for remote files instead * of cURL, even if a new enough version is installed * @see SimplePie::force_fsockopen() @@ -623,6 +636,12 @@ class SimplePie public $strip_htmltags = array('base', 'blink', 'body', 'doctype', 'embed', 'font', 'form', 'frame', 'frameset', 'html', 'iframe', 'input', 'marquee', 'meta', 'noscript', 'object', 'param', 'script', 'style'); /** + * Use syslog to report HTTP requests done by SimplePie. + * @see SimplePie::set_syslog() + */ + public $syslog_enabled = SIMPLEPIE_SYSLOG; + + /** * The SimplePie class contains feed level data and options * * To use SimplePie, create the SimplePie object with no parameters. You can @@ -742,7 +761,7 @@ class SimplePie else { $this->feed_url = $this->registry->call('Misc', 'fix_protocol', array($url, 1)); - $this->permanent_url = $this->feed_url; //FreshRSS + $this->permanent_url = $this->feed_url; } } @@ -757,7 +776,7 @@ class SimplePie if ($file instanceof SimplePie_File) { $this->feed_url = $file->url; - $this->permanent_url = $this->feed_url; //FreshRSS + $this->permanent_url = $this->feed_url; $this->file =& $file; return true; } @@ -795,6 +814,19 @@ class SimplePie { $this->timeout = (int) $timeout; } + + /** + * Set custom curl options + * + * This allows you to change default curl options + * + * @since 1.0 Beta 3 + * @param array $curl_options Curl options to add to default settings + */ + public function set_curl_options(array $curl_options = array()) + { + $this->curl_options = $curl_options; + } /** * Force SimplePie to use fsockopen() instead of cURL @@ -1091,6 +1123,7 @@ class SimplePie $this->strip_attributes(false); $this->add_attributes(false); $this->set_image_handler(false); + $this->set_https_domains(array()); } } @@ -1136,7 +1169,7 @@ class SimplePie $this->sanitize->strip_attributes($attribs); } - public function add_attributes($attribs = '') + public function add_attributes($attribs = '') //FreshRSS { if ($attribs === '') { @@ -1146,6 +1179,14 @@ class SimplePie } /** + * Use syslog to report HTTP requests done by SimplePie. + */ + public function set_syslog($value = SIMPLEPIE_SYSLOG) //FreshRSS + { + $this->syslog_enabled = $value == true; + } + + /** * Set the output encoding * * Allows you to override SimplePie's output to match that of your webpage. @@ -1194,6 +1235,19 @@ class SimplePie } /** + * Set the list of domains for which force HTTPS. + * @see SimplePie_Sanitize::set_https_domains() + * FreshRSS + */ + public function set_https_domains($domains = array()) + { + if (is_array($domains)) + { + $this->sanitize->set_https_domains($domains); + } + } + + /** * Set the handler to enable the display of cached images. * * @param str $page Web-accessible path to the handler_image.php file. @@ -1231,7 +1285,8 @@ class SimplePie $this->enable_exceptions = $enable; } - function cleanMd5($rss) { //FreshRSS + function cleanMd5($rss) + { return md5(preg_replace(array('#<(lastBuildDate|pubDate|updated|feedDate|dc:date|slash:comments)>[^<]+</\\1>#', '#<!--.+?-->#s'), '', $rss)); } @@ -1249,6 +1304,7 @@ class SimplePie // Check absolute bare minimum requirements. if (!extension_loaded('xml') || !extension_loaded('pcre')) { + $this->error = 'XML or PCRE extensions not loaded!'; return false; } // Then check the xml extension is sane (i.e., libxml 2.7.x issue on PHP < 5.2.9 and libxml 2.7.0 to 2.7.2 on any version) if we don't have xmlreader. @@ -1276,7 +1332,7 @@ class SimplePie // Pass whatever was set with config options over to the sanitizer. // Pass the classes in for legacy support; new classes should use the registry instead $this->sanitize->pass_cache_data($this->cache, $this->cache_location, $this->cache_name_function, $this->registry->get_class('Cache')); - $this->sanitize->pass_file_data($this->registry->get_class('File'), $this->timeout, $this->useragent, $this->force_fsockopen); + $this->sanitize->pass_file_data($this->registry->get_class('File'), $this->timeout, $this->useragent, $this->force_fsockopen, $this->curl_options); if (!empty($this->multifeed_url)) { @@ -1321,7 +1377,7 @@ class SimplePie // Fetch the data via SimplePie_File into $this->raw_data if (($fetched = $this->fetch_data($cache)) === true) { - return $this->data['mtime']; //FreshRSS + return $this->data['mtime']; } elseif ($fetched === false) { return false; @@ -1329,7 +1385,8 @@ class SimplePie list($headers, $sniffed) = $fetched; - if (isset($this->data['md5'])) { //FreshRSS + if (isset($this->data['md5'])) + { $md5 = $this->data['md5']; } } @@ -1413,8 +1470,8 @@ class SimplePie $this->data['headers'] = $headers; } $this->data['build'] = SIMPLEPIE_BUILD; - $this->data['mtime'] = time(); //FreshRSS - $this->data['md5'] = empty($md5) ? $this->cleanMd5($this->raw_data) : $md5; //FreshRSS + $this->data['mtime'] = time(); + $this->data['md5'] = empty($md5) ? $this->cleanMd5($this->raw_data) : $md5; // Cache the file if caching is enabled if ($cache && !$cache->save($this)) @@ -1429,7 +1486,7 @@ class SimplePie if (isset($parser)) { // We have an error, just set SimplePie_Misc::error to it and quit - $this->error = sprintf('This XML document is invalid, likely due to invalid characters. XML error: %s at line %d, column %d', $parser->get_error_string(), $parser->get_current_line(), $parser->get_current_column()); + $this->error = sprintf('This XML document is invalid, likely due to invalid characters. XML error: %s at line %d, column %d, encoding %s, URL: %s', $parser->get_error_string(), $parser->get_current_line(), $parser->get_current_column(), $encoding, $this->feed_url); } else { @@ -1455,7 +1512,12 @@ class SimplePie { // Load the Cache $this->data = $cache->load(); - if (!empty($this->data)) + if ($cache->mtime() + $this->cache_duration > time()) + { + $this->raw_data = false; + return true; // If the cache is still valid, just return true + } + elseif (!empty($this->data)) { // If the cache is for an outdated build of SimplePie if (!isset($this->data['build']) || $this->data['build'] !== SIMPLEPIE_BUILD) @@ -1487,63 +1549,58 @@ class SimplePie } } // Check if the cache has been updated - elseif ($cache->mtime() + $this->cache_duration < time()) + else { - // If we have last-modified and/or etag set - //if (isset($this->data['headers']['last-modified']) || isset($this->data['headers']['etag'])) //FreshRSS removed + $headers = array( + 'Accept' => 'application/atom+xml, application/rss+xml, application/rdf+xml;q=0.9, application/xml;q=0.8, text/xml;q=0.8, text/html;q=0.7, unknown/unknown;q=0.1, application/unknown;q=0.1, */*;q=0.1', + ); + if (isset($this->data['headers']['last-modified'])) { - $headers = array( - 'Accept' => 'application/atom+xml, application/rss+xml, application/rdf+xml;q=0.9, application/xml;q=0.8, text/xml;q=0.8, text/html;q=0.7, unknown/unknown;q=0.1, application/unknown;q=0.1, */*;q=0.1', - ); - if (isset($this->data['headers']['last-modified'])) - { - $headers['if-modified-since'] = $this->data['headers']['last-modified']; - } - if (isset($this->data['headers']['etag'])) - { - $headers['if-none-match'] = $this->data['headers']['etag']; - } + $headers['if-modified-since'] = $this->data['headers']['last-modified']; + } + if (isset($this->data['headers']['etag'])) + { + $headers['if-none-match'] = $this->data['headers']['etag']; + } - $file = $this->registry->create('File', array($this->feed_url, $this->timeout, 5, $headers, $this->useragent, $this->force_fsockopen)); //FreshRSS + $file = $this->registry->create('File', array($this->feed_url, $this->timeout, 5, $headers, $this->useragent, $this->force_fsockopen, $this->curl_options)); - if ($file->success) + if ($file->success) + { + if ($file->status_code === 304) { - if ($file->status_code === 304) - { - $cache->touch(); - return true; - } + $cache->touch(); + return true; } - else + } + else + { + $cache->touch(); + $this->error = $file->error; + return !empty($this->data); + } + + $md5 = $this->cleanMd5($file->body); + if ($this->data['md5'] === $md5) { + if ($this->syslog_enabled) { - $this->error = $file->error; //FreshRSS - return !empty($this->data); //FreshRSS - //unset($file); //FreshRSS removed + syslog(LOG_DEBUG, 'SimplePie MD5 cache match for ' . SimplePie_Misc::url_remove_credentials($this->feed_url)); } - } - { //FreshRSS - $md5 = $this->cleanMd5($file->body); - if ($this->data['md5'] === $md5) { - syslog(LOG_DEBUG, 'SimplePie MD5 cache match for ' . $this->feed_url); - $cache->touch(); - return true; //Content unchanged even though server did not send a 304 - } else { - syslog(LOG_DEBUG, 'SimplePie MD5 cache no match for ' . $this->feed_url); - $this->data['md5'] = $md5; + $cache->touch(); + return true; //Content unchanged even though server did not send a 304 + } else { + if ($this->syslog_enabled) + { + syslog(LOG_DEBUG, 'SimplePie MD5 cache no match for ' . SimplePie_Misc::url_remove_credentials($this->feed_url)); } + $this->data['md5'] = $md5; } } - // If the cache is still valid, just return true - else - { - $this->raw_data = false; - return true; - } } - // If the cache is empty, delete it + // If the cache is empty else { - $cache->unlink(); + $cache->touch(); //To keep the date/time of the last tentative update $this->data = array(); } } @@ -1559,7 +1616,7 @@ class SimplePie $headers = array( 'Accept' => 'application/atom+xml, application/rss+xml, application/rdf+xml;q=0.9, application/xml;q=0.8, text/xml;q=0.8, text/html;q=0.7, unknown/unknown;q=0.1, application/unknown;q=0.1, */*;q=0.1', ); - $file = $this->registry->create('File', array($this->feed_url, $this->timeout, 5, $headers, $this->useragent, $this->force_fsockopen)); + $file = $this->registry->create('File', array($this->feed_url, $this->timeout, 5, $headers, $this->useragent, $this->force_fsockopen, $this->curl_options)); } } // If the file connection has an error, set SimplePie::error to that and quit @@ -1576,13 +1633,15 @@ class SimplePie if (!$locate->is_feed($file)) { + $copyStatusCode = $file->status_code; + $copyContentType = $file->headers['content-type']; // We need to unset this so that if SimplePie::set_file() has been called that object is untouched unset($file); try { if (!($file = $locate->find($this->autodiscovery, $this->all_discovered_feeds))) { - $this->error = "A feed could not be found at $this->feed_url. A feed with an invalid mime type may fall victim to this error, or " . SIMPLEPIE_NAME . " was unable to auto-discover it.. Use force_feed() if you are certain this URL is a real feed."; + $this->error = "A feed could not be found at `$this->feed_url`; the status code is `$copyStatusCode` and content-type is `$copyContentType`"; $this->registry->call('Misc', 'error', array($this->error, E_USER_NOTICE, __FILE__, __LINE__)); return false; } @@ -1597,8 +1656,8 @@ class SimplePie if ($cache) { $this->data = array('url' => $this->feed_url, 'feed_url' => $file->url, 'build' => SIMPLEPIE_BUILD); - $this->data['mtime'] = time(); //FreshRSS - $this->data['md5'] = empty($md5) ? $this->cleanMd5($file->body) : $md5; //FreshRSS + $this->data['mtime'] = time(); + $this->data['md5'] = empty($md5) ? $this->cleanMd5($file->body) : $md5; if (!$cache->save($this)) { trigger_error("$this->cache_location is not writeable. Make sure you've set the correct relative or absolute path, and that the location is server-writable.", E_USER_WARNING); @@ -1610,8 +1669,9 @@ class SimplePie $locate = null; } + $file->body = trim($file->body); $this->raw_data = $file->body; - $this->permanent_url = $file->permanent_url; //FreshRSS + $this->permanent_url = $file->permanent_url; $headers = $file->headers; $sniffer = $this->registry->create('Content_Type_Sniffer', array(&$file)); $sniffed = $sniffer->get_type(); @@ -1815,7 +1875,7 @@ class SimplePie */ public function subscribe_url($permanent = false) { - if ($permanent) //FreshRSS + if ($permanent) { if ($this->permanent_url !== null) { diff --git a/lib/SimplePie/SimplePie/Cache/File.php b/lib/SimplePie/SimplePie/Cache/File.php index 3b163545b..72e75a4b6 100644 --- a/lib/SimplePie/SimplePie/Cache/File.php +++ b/lib/SimplePie/SimplePie/Cache/File.php @@ -136,11 +136,7 @@ class SimplePie_Cache_File implements SimplePie_Cache_Base */ public function mtime() { - if (file_exists($this->name)) - { - return filemtime($this->name); - } - return false; + return @filemtime($this->name); } /** @@ -150,11 +146,7 @@ class SimplePie_Cache_File implements SimplePie_Cache_Base */ public function touch() { - if (file_exists($this->name)) - { - return touch($this->name); - } - return false; + return @touch($this->name); } /** diff --git a/lib/SimplePie/SimplePie/Content/Type/Sniffer.php b/lib/SimplePie/SimplePie/Content/Type/Sniffer.php index a32f47f59..ec0bf0952 100644 --- a/lib/SimplePie/SimplePie/Content/Type/Sniffer.php +++ b/lib/SimplePie/SimplePie/Content/Type/Sniffer.php @@ -109,9 +109,7 @@ class SimplePie_Content_Type_Sniffer { return $this->unknown(); } - elseif (substr($official, -4) === '+xml' - || $official === 'text/xml' - || $official === 'application/xml') + elseif (substr($official, -4) === '+xml') { return $official; } @@ -126,7 +124,9 @@ class SimplePie_Content_Type_Sniffer return $official; } } - elseif ($official === 'text/html') + elseif ($official === 'text/html' + || $official === 'text/xml' + || $official === 'application/xml') { return $this->feed_or_html(); } @@ -256,7 +256,12 @@ class SimplePie_Content_Type_Sniffer public function feed_or_html() { $len = strlen($this->file->body); - $pos = strspn($this->file->body, "\x09\x0A\x0D\x20"); + $pos = 0; + if (isset($this->file->body[2]) && $this->file->body[0] === "\xEF" && + $this->file->body[1] === "\xBB" && $this->file->body[2] === "\xBF") { + $pos += 3; //UTF-8 BOM + } + $pos += strspn($this->file->body, "\x09\x0A\x0D\x20", $pos); while ($pos < $len) { diff --git a/lib/SimplePie/SimplePie/Decode/HTML/Entities.php b/lib/SimplePie/SimplePie/Decode/HTML/Entities.php index cde06c884..46b3a1dff 100644 --- a/lib/SimplePie/SimplePie/Decode/HTML/Entities.php +++ b/lib/SimplePie/SimplePie/Decode/HTML/Entities.php @@ -169,7 +169,6 @@ class SimplePie_Decode_HTML_Entities case "\x09": case "\x0A": case "\x0B": - case "\x0B": case "\x0C": case "\x20": case "\x3C": diff --git a/lib/SimplePie/SimplePie/File.php b/lib/SimplePie/SimplePie/File.php index b1bbe4420..45994d102 100644 --- a/lib/SimplePie/SimplePie/File.php +++ b/lib/SimplePie/SimplePie/File.php @@ -66,7 +66,7 @@ class SimplePie_File var $method = SIMPLEPIE_FILE_SOURCE_NONE; var $permanent_url; //FreshRSS - public function __construct($url, $timeout = 10, $redirects = 5, $headers = null, $useragent = null, $force_fsockopen = false) + public function __construct($url, $timeout = 10, $redirects = 5, $headers = null, $useragent = null, $force_fsockopen = false, $curl_options = array(), $syslog_enabled = SIMPLEPIE_SYSLOG) { if (class_exists('idna_convert')) { @@ -75,11 +75,14 @@ class SimplePie_File $url = SimplePie_Misc::compress_parse_url($parsed['scheme'], $idn->encode($parsed['authority']), $parsed['path'], $parsed['query'], $parsed['fragment']); } $this->url = $url; - $this->permanent_url = $url; //FreshRSS + $this->permanent_url = $url; $this->useragent = $useragent; if (preg_match('/^http(s)?:\/\//i', $url)) { - syslog(LOG_INFO, 'SimplePie GET ' . $url); //FreshRSS + if ($syslog_enabled) + { + syslog(LOG_INFO, 'SimplePie GET ' . SimplePie_Misc::url_remove_credentials($url)); //FreshRSS + } if ($useragent === null) { $useragent = ini_get('user_agent'); @@ -110,12 +113,15 @@ class SimplePie_File curl_setopt($fp, CURLOPT_REFERER, $url); curl_setopt($fp, CURLOPT_USERAGENT, $useragent); curl_setopt($fp, CURLOPT_HTTPHEADER, $headers2); - curl_setopt($fp, CURLOPT_SSL_VERIFYPEER, false); //FreshRSS if (!ini_get('open_basedir') && !ini_get('safe_mode') && version_compare(SimplePie_Misc::get_curl_version(), '7.15.2', '>=')) { curl_setopt($fp, CURLOPT_FOLLOWLOCATION, 1); curl_setopt($fp, CURLOPT_MAXREDIRS, $redirects); } + foreach ($curl_options as $curl_param => $curl_value) + { + curl_setopt($fp, $curl_param, $curl_value); + } $this->headers = curl_exec($fp); if (curl_errno($fp) === 23 || curl_errno($fp) === 61) @@ -146,7 +152,7 @@ class SimplePie_File $location = SimplePie_Misc::absolutize_url($this->headers['location'], $url); $previousStatusCode = $this->status_code; $this->__construct($location, $timeout, $redirects, $headers, $useragent, $force_fsockopen); - $this->permanent_url = ($previousStatusCode == 301) ? $location : $url; //FreshRSS + $this->permanent_url = ($previousStatusCode == 301) ? $location : $url; return; } } diff --git a/lib/SimplePie/SimplePie/Item.php b/lib/SimplePie/SimplePie/Item.php index 7bd96c15f..19ba7c8f4 100644 --- a/lib/SimplePie/SimplePie/Item.php +++ b/lib/SimplePie/SimplePie/Item.php @@ -406,6 +406,30 @@ class SimplePie_Item return null; } } + + /** + * Get the media:thumbnail of the item + * + * Uses `<media:thumbnail>` + * + * + * @return array|null + */ + public function get_thumbnail() + { + if (!isset($this->data['thumbnail'])) + { + if ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_MEDIARSS, 'thumbnail')) + { + $this->data['thumbnail'] = $return[0]['attribs']['']; + } + else + { + $this->data['thumbnail'] = null; + } + } + return $this->data['thumbnail']; + } /** * Get a category for the item @@ -738,31 +762,31 @@ class SimplePie_Item { $this->data['date']['raw'] = $return[0]['data']; } - elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'updated')) + elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'pubDate')) { $this->data['date']['raw'] = $return[0]['data']; } - elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'issued')) + elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_DC_11, 'date')) { $this->data['date']['raw'] = $return[0]['data']; } - elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'created')) + elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_DC_10, 'date')) { $this->data['date']['raw'] = $return[0]['data']; } - elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'modified')) + elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_10, 'updated')) { $this->data['date']['raw'] = $return[0]['data']; } - elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_RSS_20, 'pubDate')) + elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'issued')) { $this->data['date']['raw'] = $return[0]['data']; } - elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_DC_11, 'date')) + elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'created')) { $this->data['date']['raw'] = $return[0]['data']; } - elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_DC_10, 'date')) + elseif ($return = $this->get_item_tags(SIMPLEPIE_NAMESPACE_ATOM_03, 'modified')) { $this->data['date']['raw'] = $return[0]['data']; } @@ -2733,7 +2757,9 @@ class SimplePie_Item { foreach ($content['child'][SIMPLEPIE_NAMESPACE_MEDIARSS]['thumbnail'] as $thumbnail) { - $thumbnails[] = $this->sanitize($thumbnail['attribs']['']['url'], SIMPLEPIE_CONSTRUCT_IRI); + if (isset($thumbnail['attribs']['']['url'])) { + $thumbnails[] = $this->sanitize($thumbnail['attribs']['']['url'], SIMPLEPIE_CONSTRUCT_IRI); + } } if (is_array($thumbnails)) { @@ -2851,6 +2877,7 @@ class SimplePie_Item $width = null; $url = $this->sanitize($enclosure[0]['attribs']['']['url'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($enclosure[0])); + $url = $this->feed->sanitize->https_url($url); //FreshRSS if (isset($enclosure[0]['attribs']['']['type'])) { $type = $this->sanitize($enclosure[0]['attribs']['']['type'], SIMPLEPIE_CONSTRUCT_TEXT); diff --git a/lib/SimplePie/SimplePie/Locator.php b/lib/SimplePie/SimplePie/Locator.php index 90ee7a302..ba4a843b0 100644 --- a/lib/SimplePie/SimplePie/Locator.php +++ b/lib/SimplePie/SimplePie/Locator.php @@ -148,7 +148,7 @@ class SimplePie_Locator { $sniffer = $this->registry->create('Content_Type_Sniffer', array($file)); $sniffed = $sniffer->get_type(); - if (in_array($sniffed, array('application/rss+xml', 'application/rdf+xml', 'text/rdf', 'application/atom+xml', 'text/xml', 'application/xml'))) + if (in_array($sniffed, array('application/rss+xml', 'application/rdf+xml', 'text/rdf', 'application/atom+xml', 'text/xml', 'application/xml', 'application/x-rss+xml'))) { return true; } diff --git a/lib/SimplePie/SimplePie/Misc.php b/lib/SimplePie/SimplePie/Misc.php index 5a263a2e5..2d154cbcb 100644 --- a/lib/SimplePie/SimplePie/Misc.php +++ b/lib/SimplePie/SimplePie/Misc.php @@ -79,9 +79,9 @@ class SimplePie_Misc public static function absolutize_url($relative, $base) { - if (substr($relative, 0, 2) === '//') //FreshRSS: disable absolutize_url for "//www.example.net" which will pick HTTP or HTTPS automatically - { - return $relative; + if (substr($relative, 0, 2) === '//') + {//Protocol-relative URLs "//www.example.net" + return 'https:' . $relative; } $iri = SimplePie_IRI::absolutize(new SimplePie_IRI($base), $relative); if ($iri === false) @@ -128,7 +128,7 @@ class SimplePie_Misc { $attribs[$j][2] = $attribs[$j][1]; } - $return[$i]['attribs'][strtolower($attribs[$j][1])]['data'] = SimplePie_Misc::entities_decode(end($attribs[$j]), 'UTF-8'); //FreshRSS + $return[$i]['attribs'][strtolower($attribs[$j][1])]['data'] = SimplePie_Misc::entities_decode(end($attribs[$j]), 'UTF-8'); } } } @@ -142,7 +142,7 @@ class SimplePie_Misc foreach ($element['attribs'] as $key => $value) { $key = strtolower($key); - $full .= " $key=\"" . htmlspecialchars($value['data'], ENT_COMPAT, 'UTF-8') . '"'; //FreshRSS + $full .= " $key=\"" . htmlspecialchars($value['data'], ENT_COMPAT, 'UTF-8') . '"'; } if ($element['self_closing']) { @@ -2240,5 +2240,15 @@ function embed_wmedia(width, height, link) { { // No-op } + + /** + * Sanitize a URL by removing HTTP credentials. + * @param $url the URL to sanitize. + * @return the same URL without HTTP credentials. + */ + public static function url_remove_credentials($url) //FreshRSS + { + return preg_replace('#^(https?://)[^/:@]+:[^/:@]+@#i', '$1', $url); + } } diff --git a/lib/SimplePie/SimplePie/Parse/Date.php b/lib/SimplePie/SimplePie/Parse/Date.php index ba7c0703e..50bb5cffa 100644 --- a/lib/SimplePie/SimplePie/Parse/Date.php +++ b/lib/SimplePie/SimplePie/Parse/Date.php @@ -173,7 +173,7 @@ class SimplePie_Parse_Date 'aug' => 8, 'august' => 8, 'sep' => 9, - 'september' => 8, + 'september' => 9, 'oct' => 10, 'october' => 10, 'nov' => 11, @@ -331,8 +331,8 @@ class SimplePie_Parse_Date 'CCT' => 23400, 'CDT' => -18000, 'CEDT' => 7200, - 'CEST' => 7200, //FreshRSS 'CET' => 3600, + 'CEST' => 7200, 'CGST' => -7200, 'CGT' => -10800, 'CHADT' => 49500, @@ -721,7 +721,7 @@ class SimplePie_Parse_Date { $output .= substr($string, $position, $pos - $position); $position = $pos + 1; - if ($string[$pos - 1] !== '\\') + if ($pos === 0 || $string[$pos - 1] !== '\\') { $depth++; while ($depth && $position < $length) diff --git a/lib/SimplePie/SimplePie/Registry.php b/lib/SimplePie/SimplePie/Registry.php index bd9c1f535..dac55e34e 100755 --- a/lib/SimplePie/SimplePie/Registry.php +++ b/lib/SimplePie/SimplePie/Registry.php @@ -113,7 +113,7 @@ class SimplePie_Registry */ public function register($type, $class, $legacy = false) { - if (!is_subclass_of($class, $this->default[$type])) + if (!@is_subclass_of($class, $this->default[$type])) { return false; } @@ -222,4 +222,4 @@ class SimplePie_Registry $result = call_user_func_array(array($class, $method), $parameters); return $result; } -}
\ No newline at end of file +} diff --git a/lib/SimplePie/SimplePie/Sanitize.php b/lib/SimplePie/SimplePie/Sanitize.php index 168a5e2e8..bdc601100 100644 --- a/lib/SimplePie/SimplePie/Sanitize.php +++ b/lib/SimplePie/SimplePie/Sanitize.php @@ -73,6 +73,15 @@ class SimplePie_Sanitize var $force_fsockopen = false; var $replace_url_attributes = null; + /** + * List of domains for which force HTTPS. + * @see SimplePie_Sanitize::set_https_domains() + * Array is tree split at DNS levels. Example: + * array('biz' => true, 'com' => array('example' => true), 'net' => array('example') => array('www' => true)) + * FreshRSS + */ + var $https_domains = array('com' => array('dailymotion' => true, 'youtube' => true)); + public function __construct() { // Set defaults @@ -242,6 +251,71 @@ class SimplePie_Sanitize $this->replace_url_attributes = (array) $element_attribute; } + /** + * Set the list of domains for which force HTTPS. + * @see SimplePie_Misc::https_url() + * Example array('biz', 'example.com', 'example.org', 'www.example.net'); + * FreshRSS + */ + public function set_https_domains($domains) + { + $this->https_domains = array(); + foreach ($domains as $domain) + { + $domain = trim($domain, ". \t\n\r\0\x0B"); + $segments = array_reverse(explode('.', $domain)); + $node =& $this->https_domains; + foreach ($segments as $segment) + {//Build a tree + if ($node === true) + { + break; + } + if (!isset($node[$segment])) + { + $node[$segment] = array(); + } + $node =& $node[$segment]; + } + $node = true; + } + } + + /** + * Check if the domain is in the list of forced HTTPS + * FreshRSS + */ + protected function is_https_domain($domain) + { + $domain = trim($domain, '. '); + $segments = array_reverse(explode('.', $domain)); + $node =& $this->https_domains; + foreach ($segments as $segment) + {//Explore the tree + if (isset($node[$segment])) + { + $node =& $node[$segment]; + } + else + { + break; + } + } + return $node === true; + } + + /** + * Force HTTPS for selected Web sites + * FreshRSS + */ + public function https_url($url) + { + return (strtolower(substr($url, 0, 7)) === 'http://') && + $this->is_https_domain(parse_url($url, PHP_URL_HOST)) ? + substr_replace($url, 's', 4, 0) : //Add the 's' to HTTPS + $url; + } + public function sanitize($data, $type, $base = '') { $data = trim($data); @@ -249,6 +323,7 @@ class SimplePie_Sanitize { if ($type & SIMPLEPIE_CONSTRUCT_MAYBE_HTML) { + $data = htmlspecialchars_decode($data, ENT_QUOTES); if (preg_match('/(&(#(x[0-9a-fA-F]+|[0-9]+)|[a-zA-Z0-9]+)|<\/[A-Za-z][^\x09\x0A\x0B\x0C\x0D\x20\x2F\x3E]*' . SIMPLEPIE_PCRE_HTML_ATTRIBUTE . '>)/', $data)) { $type |= SIMPLEPIE_CONSTRUCT_HTML; @@ -279,7 +354,7 @@ class SimplePie_Sanitize $document->loadHTML($data); restore_error_handler(); - $xpath = new DOMXPath($document); //FreshRSS + $xpath = new DOMXPath($document); // Strip comments if ($this->strip_comments) @@ -450,7 +525,8 @@ class SimplePie_Sanitize if ($element->hasAttribute($attribute)) { $value = $this->registry->call('Misc', 'absolutize_url', array($element->getAttribute($attribute), $this->base)); - if ($value !== false) + $value = $this->https_url($value); //FreshRSS + if ($value) { $element->setAttribute($attribute, $value); } diff --git a/lib/favicons.php b/lib/favicons.php new file mode 100644 index 000000000..d8c97964e --- /dev/null +++ b/lib/favicons.php @@ -0,0 +1,43 @@ +<?php + +include(LIB_PATH . '/Favicon/Favicon.php'); +include(LIB_PATH . '/Favicon/DataAccess.php'); + +$favicons_dir = DATA_PATH . '/favicons/'; +$default_favicon = PUBLIC_PATH . '/themes/icons/default_favicon.ico'; + +function download_favicon($website, $dest) { + global $favicons_dir, $default_favicon; + + syslog(LOG_INFO, 'FreshRSS Favicon discovery GET ' . $website); + $favicon_getter = new \Favicon\Favicon(); + $favicon_getter->setCacheDir($favicons_dir); + $favicon_url = $favicon_getter->get($website); + + if ($favicon_url === false) { + return @copy($default_favicon, $dest); + } + + syslog(LOG_INFO, 'FreshRSS Favicon GET ' . $favicon_url); + $c = curl_init($favicon_url); + curl_setopt($c, CURLOPT_HEADER, false); + curl_setopt($c, CURLOPT_RETURNTRANSFER, true); + curl_setopt($c, CURLOPT_BINARYTRANSFER, true); + curl_setopt($c, CURLOPT_USERAGENT, 'FreshRSS/' . FRESHRSS_VERSION . ' (' . PHP_OS . '; ' . FRESHRSS_WEBSITE . ')'); + $img_raw = curl_exec($c); + $status_code = curl_getinfo($c, CURLINFO_HTTP_CODE); + curl_close($c); + + if ($status_code === 200) { + $file = fopen($dest, 'w'); + if ($file !== false) { + fwrite($file, $img_raw); + fclose($file); + return true; + } + } else { + syslog(LOG_WARNING, 'FreshRSS Favicon GET ' . $favicon_url . ' error ' . $status_code); + } + + return false; +} diff --git a/lib/http-conditional.php b/lib/http-conditional.php index 59fbef41f..6d3a0a97f 100644 --- a/lib/http-conditional.php +++ b/lib/http-conditional.php @@ -35,12 +35,12 @@ ... //Rest of the script, just as you would do normally. ?> - Version 1.7 beta, 2013-12-02, http://alexandre.alapetite.fr/doc-alex/php-http-304/ + Version 1.8 beta, 2016-08-07, http://alexandre.alapetite.fr/doc-alex/php-http-304/ ------------------------------------------------------------------ Written by Alexandre Alapetite, http://alexandre.alapetite.fr/cv/ - Copyright 2004-2013, Licence: Creative Commons "Attribution-ShareAlike 2.0 France" BY-SA (FR), + Copyright 2004-2016, Licence: Creative Commons "Attribution-ShareAlike 2.0 France" BY-SA (FR), http://creativecommons.org/licenses/by-sa/2.0/fr/ http://alexandre.alapetite.fr/divers/apropos/#by-sa - Attribution. You must give the original author credit @@ -96,7 +96,8 @@ function httpConditional($UnixTimeStamp,$cacheSeconds=0,$cachePrivacy=0,$feedMod if ((!$is412)&&isset($_SERVER['HTTP_IF_MATCH'])) {//rfc2616-sec14.html#sec14.24 $etagsClient=stripslashes($_SERVER['HTTP_IF_MATCH']); - $is412=(($etagClient!=='*')&&(strpos($etagsClient,$etagServer)===false)); + $etagsClient=str_ireplace('-gzip','',$etagsClient); + $is412=(($etagsClient!=='*')&&(strpos($etagsClient,$etagServer)===false)); } if ($is304&&isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {//rfc2616-sec14.html#sec14.25 //rfc1945.txt @@ -111,6 +112,7 @@ function httpConditional($UnixTimeStamp,$cacheSeconds=0,$cachePrivacy=0,$feedMod {//rfc2616-sec14.html#sec14.26 $nbCond++; $etagClient=stripslashes($_SERVER['HTTP_IF_NONE_MATCH']); + $etagClient=str_ireplace('-gzip','',$etagClient); $is304=(($etagClient===$etagServer)||($etagClient==='*')); } if ((!$is412)&&isset($_SERVER['HTTP_IF_UNMODIFIED_SINCE'])) diff --git a/lib/lib_install.php b/lib/lib_install.php new file mode 100644 index 000000000..c5cae4293 --- /dev/null +++ b/lib/lib_install.php @@ -0,0 +1,121 @@ +<?php + +define('BCRYPT_COST', 9); + +Minz_Configuration::register('default_system', join_path(DATA_PATH, 'config.default.php')); +Minz_Configuration::register('default_user', join_path(USERS_PATH, '_', 'config.default.php')); + +function checkRequirements() { + $php = version_compare(PHP_VERSION, '5.3.3') >= 0; + $minz = file_exists(join_path(LIB_PATH, 'Minz')); + $curl = extension_loaded('curl'); + $pdo_mysql = extension_loaded('pdo_mysql'); + $pdo_sqlite = extension_loaded('pdo_sqlite'); + $pdo_pgsql = extension_loaded('pdo_pgsql'); + $pdo = $pdo_mysql || $pdo_sqlite || $pdo_pgsql; + $pcre = extension_loaded('pcre'); + $ctype = extension_loaded('ctype'); + $fileinfo = extension_loaded('fileinfo'); + $dom = class_exists('DOMDocument'); + $xml = function_exists('xml_parser_create'); + $json = function_exists('json_encode'); + $data = DATA_PATH && is_writable(DATA_PATH); + $cache = CACHE_PATH && is_writable(CACHE_PATH); + $users = USERS_PATH && is_writable(USERS_PATH); + $favicons = is_writable(join_path(DATA_PATH, 'favicons')); + $http_referer = is_referer_from_same_domain(); + + return array( + 'php' => $php ? 'ok' : 'ko', + 'minz' => $minz ? 'ok' : 'ko', + 'curl' => $curl ? 'ok' : 'ko', + 'pdo-mysql' => $pdo_mysql ? 'ok' : 'ko', + 'pdo-sqlite' => $pdo_sqlite ? 'ok' : 'ko', + 'pdo-pgsql' => $pdo_pgsql ? 'ok' : 'ko', + 'pdo' => $pdo ? 'ok' : 'ko', + 'pcre' => $pcre ? 'ok' : 'ko', + 'ctype' => $ctype ? 'ok' : 'ko', + 'fileinfo' => $fileinfo ? 'ok' : 'ko', + 'dom' => $dom ? 'ok' : 'ko', + 'xml' => $xml ? 'ok' : 'ko', + 'json' => $json ? 'ok' : 'ko', + 'data' => $data ? 'ok' : 'ko', + 'cache' => $cache ? 'ok' : 'ko', + 'users' => $users ? 'ok' : 'ko', + 'favicons' => $favicons ? 'ok' : 'ko', + 'http_referer' => $http_referer ? 'ok' : 'ko', + 'all' => $php && $minz && $curl && $pdo && $pcre && $ctype && $fileinfo && $dom && $xml && + $data && $cache && $users && $favicons && $http_referer ? + 'ok' : 'ko' + ); +} + +function generateSalt() { + return sha1(uniqid(mt_rand(), true).implode('', stat(__FILE__))); +} + +function checkDb(&$dbOptions) { + $dsn = ''; + $driver_options = null; + try { + switch ($dbOptions['type']) { + case 'mysql': + include_once(APP_PATH . '/SQL/install.sql.mysql.php'); + $driver_options = array( + PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4' + ); + try { // on ouvre une connexion juste pour créer la base si elle n'existe pas + $dsn = 'mysql:host=' . $dbOptions['host'] . ';'; + $c = new PDO($dsn, $dbOptions['user'], $dbOptions['password'], $driver_options); + $sql = sprintf(SQL_CREATE_DB, $dbOptions['base']); + $res = $c->query($sql); + } catch (PDOException $e) { + syslog(LOG_DEBUG, 'FreshRSS MySQL warning: ' . $e->getMessage()); + } + // on écrase la précédente connexion en sélectionnant la nouvelle BDD + $dsn = 'mysql:host=' . $dbOptions['host'] . ';dbname=' . $dbOptions['base']; + break; + case 'sqlite': + include_once(APP_PATH . '/SQL/install.sql.sqlite.php'); + $dsn = 'sqlite:' . join_path(USERS_PATH, $dbOptions['default_user'], 'db.sqlite'); + $driver_options = array( + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + ); + break; + case 'pgsql': + include_once(APP_PATH . '/SQL/install.sql.pgsql.php'); + $driver_options = array( + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + ); + try { // on ouvre une connexion juste pour créer la base si elle n'existe pas + $dsn = 'pgsql:host=' . $dbOptions['host'] . ';dbname=postgres'; + $c = new PDO($dsn, $dbOptions['user'], $dbOptions['password'], $driver_options); + $sql = sprintf(SQL_CREATE_DB, $dbOptions['base']); + $res = $c->query($sql); + } catch (PDOException $e) { + syslog(LOG_DEBUG, 'FreshRSS PostgreSQL warning: ' . $e->getMessage()); + } + // on écrase la précédente connexion en sélectionnant la nouvelle BDD + $dsn = 'pgsql:host=' . $dbOptions['host'] . ';dbname=' . $dbOptions['base']; + break; + default: + return false; + } + + $c = new PDO($dsn, $dbOptions['user'], $dbOptions['password'], $driver_options); + $res = $c->query('SELECT 1'); + } catch (PDOException $e) { + $dsn = ''; + syslog(LOG_DEBUG, 'FreshRSS SQL warning: ' . $e->getMessage()); + $dbOptions['error'] = $e->getMessage(); + } + $dbOptions['dsn'] = $dsn; + $dbOptions['options'] = $driver_options; + return $dsn != ''; +} + +function deleteInstall() { + $path = join_path(DATA_PATH, 'do-install.txt'); + @unlink($path); + return !file_exists($path); +} diff --git a/lib/lib_opml.php b/lib/lib_opml.php index f320335bb..b89e92977 100644 --- a/lib/lib_opml.php +++ b/lib/lib_opml.php @@ -1,11 +1,19 @@ <?php -/* * +/** * lib_opml is a free library to manage OPML format in PHP. - * It takes in consideration only version 2.0 (http://dev.opml.org/spec2.html). - * Basically it means "text" attribute for outline elements is required. * - * lib_opml requires SimpleXML (http://php.net/manual/en/book.simplexml.php) + * By default, it takes in consideration version 2.0 but can be compatible with + * OPML 1.0. More information on http://dev.opml.org. + * Difference is "text" attribute is optional in version 1.0. It is highly + * recommended to use this attribute. + * + * lib_opml requires SimpleXML (php.net/simplexml) and DOMDocument (php.net/domdocument) + * + * @author Marien Fressinaud <dev@marienfressinaud.fr> + * @link https://github.com/marienfressinaud/lib_opml + * @version 0.2-FreshRSS~1.5.1 + * @license public domain * * Usages: * > include('lib_opml.php'); @@ -23,21 +31,44 @@ * > echo $opml_string; * > print_r($opml_object); * + * You can set $strict argument to false if you want to bypass "text" attribute + * requirement. + * * If parsing fails for any reason (e.g. not an XML string, does not match with * the specifications), a LibOPML_Exception is raised. * - * Author: Marien Fressinaud <dev@marienfressinaud.fr> - * Url: https://github.com/marienfressinaud/lib_opml - * Version: 0.1 - * Date: 2014-03-29 - * License: public domain + * lib_opml array format is described here: + * $array = array( + * 'head' => array( // 'head' element is optional (but recommended) + * 'key' => 'value', // key must be a part of available OPML head elements + * ), + * 'body' => array( // body is required + * array( // this array represents an outline (at least one) + * 'text' => 'value', // 'text' element is required if $strict is true + * 'key' => 'value', // key and value are what you want (optional) + * '@outlines' = array( // @outlines is a special value and represents sub-outlines + * array( + * [...] // where [...] is a valid outline definition + * ), + * ), + * ), + * array( // other outline definitions + * [...] + * ), + * [...], + * ) + * ) * - * */ + */ +/** + * A simple Exception class which represents any kind of OPML problem. + * Message should precise the current problem. + */ class LibOPML_Exception extends Exception {} -// These elements are optional +// Define the list of available head attributes. All of them are optional. define('HEAD_ELEMENTS', serialize(array( 'title', 'dateCreated', 'dateModified', 'ownerName', 'ownerEmail', 'ownerId', 'docs', 'expansionState', 'vertScrollState', 'windowTop', @@ -45,7 +76,16 @@ define('HEAD_ELEMENTS', serialize(array( ))); -function libopml_parse_outline($outline_xml) { +/** + * Parse an XML object as an outline object and return corresponding array + * + * @param SimpleXMLElement $outline_xml the XML object we want to parse + * @param bool $strict true if "text" attribute is required, false else + * @return array corresponding to an outline and following format described above + * @throws LibOPML_Exception + * @access private + */ +function libopml_parse_outline($outline_xml, $strict = true) { $outline = array(); // An outline may contain any kind of attributes but "text" attribute is @@ -59,16 +99,20 @@ function libopml_parse_outline($outline_xml) { } } - if (!$text_is_present) { + if (!$text_is_present && $strict) { throw new LibOPML_Exception( 'Outline does not contain any text attribute' ); } + if (empty($outline['text']) && isset($outline['title'])) { + $outline['text'] = $outline['title']; + } + foreach ($outline_xml->children() as $key => $value) { // An outline may contain any number of outline children if ($key === 'outline') { - $outline['@outlines'][] = libopml_parse_outline($value); + $outline['@outlines'][] = libopml_parse_outline($value, $strict); } else { throw new LibOPML_Exception( 'Body can contain only outline elements' @@ -79,14 +123,52 @@ function libopml_parse_outline($outline_xml) { return $outline; } +/** + * Reformat the XML document as a hierarchy when + * the OPML 2.0 category attribute is used + */ +function preprocessing_categories($doc) { + $outline_categories = array(); + $body = $doc->getElementsByTagName('body')->item(0); + $xpath = new DOMXpath($doc); + $outlines = $xpath->query('/opml/body/outline[@category]'); + foreach ($outlines as $outline) { + $category = trim($outline->getAttribute('category')); + if ($category != '') { + $outline_categorie = null; + if (!isset($outline_categories[$category])) { + $outline_categorie = $doc->createElement('outline'); + $outline_categorie->setAttribute('text', $category); + $body->insertBefore($outline_categorie, $body->firstChild); + $outline_categories[$category] = $outline_categorie; + } else { + $outline_categorie = $outline_categories[$category]; + } + $outline->parentNode->removeChild($outline); + $outline_categorie->appendChild($outline); + } + } +} -function libopml_parse_string($xml) { +/** + * Parse a string as a XML one and returns the corresponding array + * + * @param string $xml is the string we want to parse + * @param bool $strict true if "text" attribute is required, false else + * @return array corresponding to the XML string and following format described above + * @throws LibOPML_Exception + * @access public + */ +function libopml_parse_string($xml, $strict = true) { $dom = new DOMDocument(); $dom->recover = true; $dom->strictErrorChecking = false; $dom->loadXML($xml); $dom->encoding = 'UTF-8'; + //Partial compatibility with the category attribute of OPML 2.0 + preprocessing_categories($dom); + $opml = simplexml_import_dom($dom); if (!$opml) { @@ -101,7 +183,6 @@ function libopml_parse_string($xml) { // First, we get all "head" elements. Head is required but its sub-elements // are optional. - // TODO: test head exists! foreach ($opml->head->children() as $key => $value) { if (in_array($key, unserialize(HEAD_ELEMENTS), true)) { $array['head'][$key] = (string)$value; @@ -115,11 +196,10 @@ function libopml_parse_string($xml) { // Then, we get body oulines. Body must contain at least one outline // element. $at_least_one_outline = false; - // TODO: test body exists! foreach ($opml->body->children() as $key => $value) { if ($key === 'outline') { $at_least_one_outline = true; - $array['body'][] = libopml_parse_outline($value); + $array['body'][] = libopml_parse_outline($value, $strict); } else { throw new LibOPML_Exception( 'Body can contain only outline elements' @@ -137,7 +217,16 @@ function libopml_parse_string($xml) { } -function libopml_parse_file($filename) { +/** + * Parse a string contained into a file as a XML string and returns the corresponding array + * + * @param string $filename should indicates a valid XML file + * @param bool $strict true if "text" attribute is required, false else + * @return array corresponding to the file content and following format described above + * @throws LibOPML_Exception + * @access public + */ +function libopml_parse_file($filename, $strict = true) { $file_content = file_get_contents($filename); if ($file_content === false) { @@ -146,11 +235,20 @@ function libopml_parse_file($filename) { ); } - return libopml_parse_string($file_content); + return libopml_parse_string($file_content, $strict); } -function libopml_render_outline($parent_elt, $outline) { +/** + * Create a XML outline object in a parent object. + * + * @param SimpleXMLElement $parent_elt is the parent object of current outline + * @param array $outline array representing an outline object + * @param bool $strict true if "text" attribute is required, false else + * @throws LibOPML_Exception + * @access private + */ +function libopml_render_outline($parent_elt, $outline, $strict) { // Outline MUST be an array! if (!is_array($outline)) { throw new LibOPML_Exception( @@ -165,7 +263,7 @@ function libopml_render_outline($parent_elt, $outline) { // outline elements. if ($key === '@outlines' && is_array($value)) { foreach ($value as $outline_child) { - libopml_render_outline($outline_elt, $outline_child); + libopml_render_outline($outline_elt, $outline_child, $strict); } } elseif (is_array($value)) { throw new LibOPML_Exception( @@ -181,7 +279,7 @@ function libopml_render_outline($parent_elt, $outline) { } } - if (!$text_is_present) { + if (!$text_is_present && $strict) { throw new LibOPML_Exception( 'You must define at least a text element for all outlines' ); @@ -189,8 +287,19 @@ function libopml_render_outline($parent_elt, $outline) { } -function libopml_render($array, $as_xml_object = false) { - $opml = new SimpleXMLElement('<opml version="2.0"></opml>'); +/** + * Render an array as an OPML string or a XML object. + * + * @param array $array is the array we want to render and must follow structure defined above + * @param bool $as_xml_object false if function must return a string, true for a XML object + * @param bool $strict true if "text" attribute is required, false else + * @return string|SimpleXMLElement XML string corresponding to $array or XML object + * @throws LibOPML_Exception + * @access public + */ +function libopml_render($array, $as_xml_object = false, $strict = true) { + $opml = new SimpleXMLElement('<opml></opml>'); + $opml->addAttribute('version', $strict ? '2.0' : '1.0'); // Create head element. $array['head'] is optional but head element will // exist in the final XML object. @@ -218,7 +327,7 @@ function libopml_render($array, $as_xml_object = false) { // Create outline elements $body = $opml->addChild('body'); foreach ($array['body'] as $outline) { - libopml_render_outline($body, $outline); + libopml_render_outline($body, $outline, $strict); } // And return the final result diff --git a/lib/lib_rss.php b/lib/lib_rss.php index 9abdf18ce..560e5b256 100644 --- a/lib/lib_rss.php +++ b/lib/lib_rss.php @@ -1,20 +1,33 @@ <?php if (!function_exists('json_decode')) { require_once('JSON.php'); - function json_decode($var) { - $JSON = new Services_JSON; - return (array)($JSON->decode($var)); + function json_decode($var, $assoc = false) { + $JSON = new Services_JSON($assoc ? SERVICES_JSON_LOOSE_TYPE : 0); + return $JSON->decode($var); } } if (!function_exists('json_encode')) { require_once('JSON.php'); function json_encode($var) { - $JSON = new Services_JSON; + $JSON = new Services_JSON(); return $JSON->encodeUnsafe($var); } } +defined('JSON_UNESCAPED_UNICODE') or define('JSON_UNESCAPED_UNICODE', 256); //PHP 5.3 + +/** + * Build a directory path by concatenating a list of directory names. + * + * @param $path_parts a list of directory names + * @return a string corresponding to the final pathname + */ +function join_path() { + $path_parts = func_get_args(); + return join(DIRECTORY_SEPARATOR, $path_parts); +} + //<Auto-loading> function classAutoloader($class) { if (strpos($class, 'FreshRSS') === 0) { @@ -27,7 +40,7 @@ function classAutoloader($class) { include(APP_PATH . '/Models/' . $components[1] . '.php'); return; case 3: //Controllers, Exceptions - @include(APP_PATH . '/' . $components[2] . 's/' . $components[1] . $components[2] . '.php'); + include(APP_PATH . '/' . $components[2] . 's/' . $components[1] . $components[2] . '.php'); return; } } elseif (strpos($class, 'Minz') === 0) { @@ -40,6 +53,21 @@ function classAutoloader($class) { spl_autoload_register('classAutoloader'); //</Auto-loading> +function idn_to_puny($url) { + if (function_exists('idn_to_ascii')) { + $parts = parse_url($url); + if (!empty($parts['host'])) { + $idn = $parts['host']; + $puny = idn_to_ascii($idn); + $pos = strpos($url, $idn); + if ($pos !== false) { + return substr_replace($url, $puny, $pos, strlen($idn)); + } + } + } + return $url; +} + function checkUrl($url) { if (empty ($url)) { return ''; @@ -47,41 +75,74 @@ function checkUrl($url) { if (!preg_match ('#^https?://#i', $url)) { $url = 'http://' . $url; } - if (filter_var($url, FILTER_VALIDATE_URL) || - (version_compare(PHP_VERSION, '5.3.3', '<') && (strpos($url, '-') > 0) && //PHP bug #51192 - ($url === filter_var($url, FILTER_SANITIZE_URL)))) { + $url = idn_to_puny($url); //PHP bug #53474 IDN + if (filter_var($url, FILTER_VALIDATE_URL)) { return $url; } else { return false; } } -function formatNumber($n, $precision = 0) { - return str_replace(' ', ' ', //Espace insécable //TODO: remplacer par une espace _fine_ insécable - number_format($n, $precision, '.', ' ')); //number_format does not seem to be Unicode-compatible +function safe_ascii($text) { + return filter_var($text, FILTER_DEFAULT, FILTER_FLAG_STRIP_LOW | FILTER_FLAG_STRIP_HIGH); +} + +/** + * Test if a given server address is publicly accessible. + * + * Note: for the moment it tests only if address is corresponding to a + * localhost address. + * + * @param $address the address to test, can be an IP or a URL. + * @return true if server is accessible, false else. + * @todo improve test with a more valid technique (e.g. test with an external server?) + */ +function server_is_public($address) { + $host = parse_url($address, PHP_URL_HOST); + + $is_public = !in_array($host, array( + '127.0.0.1', + 'localhost', + 'localhost.localdomain', + '[::1]', + 'localhost6', + 'localhost6.localdomain6', + )); + + return $is_public; } -function formatBytes($bytes, $precision = 2, $system = 'IEC') { + +function format_number($n, $precision = 0) { + // number_format does not seem to be Unicode-compatible + return str_replace(' ', ' ', //Espace fine insécable + number_format($n, $precision, '.', ' ') + ); +} + +function format_bytes($bytes, $precision = 2, $system = 'IEC') { if ($system === 'IEC') { $base = 1024; $units = array('B', 'KiB', 'MiB', 'GiB', 'TiB'); } elseif ($system === 'SI') { $base = 1000; $units = array('B', 'KB', 'MB', 'GB', 'TB'); + } else { + return format_number($bytes, $precision); } $bytes = max(intval($bytes), 0); $pow = $bytes === 0 ? 0 : floor(log($bytes) / log($base)); $pow = min($pow, count($units) - 1); $bytes /= pow($base, $pow); - return formatNumber($bytes, $precision) . ' ' . $units[$pow]; + return format_number($bytes, $precision) . ' ' . $units[$pow]; } function timestamptodate ($t, $hour = true) { - $month = _t(date('M', $t)); + $month = _t('gen.date.' . date('M', $t)); if ($hour) { - $date = _t('format_date_hour', $month); + $date = _t('gen.date.format_date_hour', $month); } else { - $date = _t('format_date', $month); + $date = _t('gen.date.format_date', $month); } return @date ($date, $t); @@ -106,11 +167,15 @@ function html_only_entity_decode($text) { } function customSimplePie() { + $system_conf = Minz_Configuration::get('system'); + $limits = $system_conf->limits; $simplePie = new SimplePie(); - $simplePie->set_useragent(_t('freshrss') . '/' . FRESHRSS_VERSION . ' (' . PHP_OS . '; ' . FRESHRSS_WEBSITE . ') ' . SIMPLEPIE_NAME . '/' . SIMPLEPIE_VERSION); + $simplePie->set_useragent('FreshRSS/' . FRESHRSS_VERSION . ' (' . PHP_OS . '; ' . FRESHRSS_WEBSITE . ') ' . SIMPLEPIE_NAME . '/' . SIMPLEPIE_VERSION); + $simplePie->set_syslog($system_conf->simplepie_syslog_enabled); $simplePie->set_cache_location(CACHE_PATH); - $simplePie->set_cache_duration(800); - $simplePie->set_timeout(10); //TODO: Make a user setting + $simplePie->set_cache_duration($limits['cache_duration']); + $simplePie->set_timeout($limits['timeout']); + $simplePie->set_curl_options($system_conf->curl_options); $simplePie->strip_htmltags(array( 'base', 'blink', 'body', 'doctype', 'embed', 'font', 'form', 'frame', 'frameset', 'html', @@ -120,12 +185,11 @@ function customSimplePie() { $simplePie->strip_attributes(array_merge($simplePie->strip_attributes, array( 'autoplay', 'onload', 'onunload', 'onclick', 'ondblclick', 'onmousedown', 'onmouseup', 'onmouseover', 'onmousemove', 'onmouseout', 'onfocus', 'onblur', - 'onkeypress', 'onkeydown', 'onkeyup', 'onselect', 'onchange', 'seamless'))); + 'onkeypress', 'onkeydown', 'onkeyup', 'onselect', 'onchange', 'seamless', 'sizes', 'srcset'))); $simplePie->add_attributes(array( - 'img' => array('lazyload' => '', 'postpone' => ''), //http://www.w3.org/TR/resource-priorities/ - 'audio' => array('lazyload' => '', 'postpone' => '', 'preload' => 'none'), - 'iframe' => array('lazyload' => '', 'postpone' => '', 'sandbox' => 'allow-scripts allow-same-origin'), - 'video' => array('lazyload' => '', 'postpone' => '', 'preload' => 'none'), + 'audio' => array('preload' => 'none'), + 'iframe' => array('sandbox' => 'allow-scripts allow-same-origin'), + 'video' => array('preload' => 'none'), )); $simplePie->set_url_replacements(array( 'a' => 'href', @@ -149,6 +213,16 @@ function customSimplePie() { 'src', ), )); + $https_domains = array(); + $force = @file(DATA_PATH . '/force-https.default.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + if (is_array($force)) { + $https_domains = array_merge($https_domains, $force); + } + $force = @file(DATA_PATH . '/force-https.txt', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + if (is_array($force)) { + $https_domains = array_merge($https_domains, $force); + } + $simplePie->set_https_domains($https_domains); return $simplePie; } @@ -163,17 +237,27 @@ function sanitizeHTML($data, $base = '') { /* permet de récupérer le contenu d'un article pour un flux qui n'est pas complet */ function get_content_by_parsing ($url, $path) { - require_once (LIB_PATH . '/lib_phpQuery.php'); + require_once(LIB_PATH . '/lib_phpQuery.php'); - syslog(LOG_INFO, 'FreshRSS GET ' . $url); - $html = file_get_contents ($url); + Minz_Log::notice('FreshRSS GET ' . SimplePie_Misc::url_remove_credentials($url)); + $html = file_get_contents($url); if ($html) { - $doc = phpQuery::newDocument ($html); - $content = $doc->find ($path); + $doc = phpQuery::newDocument($html); + $content = $doc->find($path); + + foreach (pq('img[data-src]') as $img) { + $imgP = pq($img); + $dataSrc = $imgP->attr('data-src'); + if (strlen($dataSrc) > 4) { + $imgP->attr('src', $dataSrc); + $imgP->removeAttr('data-src'); + } + } + return sanitizeHTML($content->__toString(), $url); } else { - throw new Exception (); + throw new Exception(); } } @@ -200,50 +284,102 @@ function uSecString() { return str_pad($t['usec'], 6, '0'); } -function invalidateHttpCache() { - Minz_Session::_param('touch', uTimeString()); - return touch(LOG_PATH . '/' . Minz_Session::param('currentUser', '_') . '.log'); +function invalidateHttpCache($username = '') { + if (($username == '') || (!ctype_alnum($username))) { + Minz_Session::_param('touch', uTimeString()); + $username = Minz_Session::param('currentUser', '_'); + } + return touch(join_path(DATA_PATH, 'users', $username, 'log.txt')); } -function usernameFromPath($userPath) { - if (preg_match('%/([A-Za-z0-9]{1,16})_user\.php$%', $userPath, $matches)) { - return $matches[1]; - } else { - return ''; +function listUsers() { + $final_list = array(); + $base_path = join_path(DATA_PATH, 'users'); + $dir_list = array_values(array_diff( + scandir($base_path), + array('..', '.', '_') + )); + + foreach ($dir_list as $file) { + if (is_dir(join_path($base_path, $file))) { + $final_list[] = $file; + } } + + return $final_list; } -function listUsers() { - return array_map('usernameFromPath', glob(DATA_PATH . '/*_user.php')); + +/** + * Return if the maximum number of registrations has been reached. + * + * Note a max_regstrations of 0 means there is no limit. + * + * @return true if number of users >= max registrations, false else. + */ +function max_registrations_reached() { + $system_conf = Minz_Configuration::get('system'); + $limit_registrations = $system_conf->limits['max_registrations']; + $number_accounts = count(listUsers()); + + return $limit_registrations > 0 && $number_accounts >= $limit_registrations; } + +/** + * Register and return the configuration for a given user. + * + * Note this function has been created to generate temporary configuration + * objects. If you need a long-time configuration, please don't use this function. + * + * @param $username the name of the user of which we want the configuration. + * @return a Minz_Configuration object, null if the configuration cannot be loaded. + */ +function get_user_configuration($username) { + $namespace = 'user_' . $username; + try { + Minz_Configuration::register($namespace, + join_path(USERS_PATH, $username, 'config.php'), + join_path(USERS_PATH, '_', 'config.default.php')); + } catch (Minz_ConfigurationNamespaceException $e) { + // namespace already exists, do nothing. + } catch (Minz_FileNotExistException $e) { + Minz_Log::warning($e->getMessage()); + return null; + } + + return Minz_Configuration::get($namespace); +} + + function httpAuthUser() { return isset($_SERVER['REMOTE_USER']) ? $_SERVER['REMOTE_USER'] : ''; } function cryptAvailable() { - if (version_compare(PHP_VERSION, '5.3.3', '>=')) { - try { - $hash = '$2y$04$usesomesillystringfore7hnbRJHxXVLeakoG8K30oukPsA.ztMG'; - return $hash === @crypt('password', $hash); - } catch (Exception $e) { - } + try { + $hash = '$2y$04$usesomesillystringfore7hnbRJHxXVLeakoG8K30oukPsA.ztMG'; + return $hash === @crypt('password', $hash); + } catch (Exception $e) { } return false; } function is_referer_from_same_domain() { if (empty($_SERVER['HTTP_REFERER'])) { - return false; + return true; //Accept empty referer while waiting for good support of meta referrer same-origin policy in browsers } $host = parse_url(((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') ? 'https://' : 'http://') . (empty($_SERVER['HTTP_HOST']) ? $_SERVER['SERVER_NAME'] : $_SERVER['HTTP_HOST'])); $referer = parse_url($_SERVER['HTTP_REFERER']); - if (empty($host['scheme']) || empty($referer['scheme']) || $host['scheme'] !== $referer['scheme'] || - empty($host['host']) || empty($referer['host']) || $host['host'] !== $referer['host']) { + if (empty($host['host']) || empty($referer['host']) || $host['host'] !== $referer['host']) { return false; } - return (isset($host['port']) ? $host['port'] : 0) === (isset($referer['port']) ? $referer['port'] : 0); + //TODO: check 'scheme', taking into account the case of a proxy + if ((isset($host['port']) ? $host['port'] : 0) !== (isset($referer['port']) ? $referer['port'] : 0)) { + return false; + } + return true; } @@ -253,15 +389,16 @@ function is_referer_from_same_domain() { * @return array of tested values. */ function check_install_php() { + $pdo_mysql = extension_loaded('pdo_mysql'); + $pdo_sqlite = extension_loaded('pdo_sqlite'); return array( - 'php' => version_compare(PHP_VERSION, '5.2.1') >= 0, + 'php' => version_compare(PHP_VERSION, '5.3.3') >= 0, 'minz' => file_exists(LIB_PATH . '/Minz'), 'curl' => extension_loaded('curl'), - 'pdo_mysql' => extension_loaded('pdo_mysql'), - 'pdo_sqlite' => extension_loaded('pdo_sqlite'), - 'pdo' => extension_loaded('pdo_mysql') || extension_loaded('pdo_sqlite'), + 'pdo' => $pdo_mysql || $pdo_sqlite, 'pcre' => extension_loaded('pcre'), 'ctype' => extension_loaded('ctype'), + 'fileinfo' => extension_loaded('fileinfo'), 'dom' => class_exists('DOMDocument'), 'json' => extension_loaded('json'), 'zip' => extension_loaded('zip'), @@ -278,9 +415,8 @@ function check_install_files() { return array( 'data' => DATA_PATH && is_writable(DATA_PATH), 'cache' => CACHE_PATH && is_writable(CACHE_PATH), - 'logs' => LOG_PATH && is_writable(LOG_PATH), + 'users' => USERS_PATH && is_writable(USERS_PATH), 'favicons' => is_writable(DATA_PATH . '/favicons'), - 'persona' => is_writable(DATA_PATH . '/persona'), 'tokens' => is_writable(DATA_PATH . '/tokens'), ); } @@ -313,3 +449,82 @@ function check_install_database() { return $status; } + +/** + * Remove a directory recursively. + * + * From http://php.net/rmdir#110489 + * + * @param $dir the directory to remove + */ +function recursive_unlink($dir) { + if (!is_dir($dir)) { + return true; + } + + $files = array_diff(scandir($dir), array('.', '..')); + foreach ($files as $filename) { + $filename = $dir . '/' . $filename; + if (is_dir($filename)) { + @chmod($filename, 0777); + recursive_unlink($filename); + } else { + unlink($filename); + } + } + + return rmdir($dir); +} + + +/** + * Remove queries where $get is appearing. + * @param $get the get attribute which should be removed. + * @param $queries an array of queries. + * @return the same array whithout those where $get is appearing. + */ +function remove_query_by_get($get, $queries) { + $final_queries = array(); + foreach ($queries as $key => $query) { + if (empty($query['get']) || $query['get'] !== $get) { + $final_queries[$key] = $query; + } + } + return $final_queries; +} + + +/** + * Add a value in an array and take care it is unique. + * @param $array the array in which we add the value. + * @param $value the value to add. + */ +function array_push_unique(&$array, $value) { + $found = array_search($value, $array) !== false; + if (!$found) { + $array[] = $value; + } +} + + +/** + * Remove a value from an array. + * @param $array the array from wich value is removed. + * @param $value the value to remove. + */ +function array_remove(&$array, $value) { + $array = array_diff($array, array($value)); +} + +//RFC 4648 +function base64url_encode($data) { + return strtr(rtrim(base64_encode($data), '='), '+/', '-_'); +} +//RFC 4648 +function base64url_decode($data) { + return base64_decode(strtr($data, '-_', '+/')); +} + +function _i($icon, $url_only = false) { + return FreshRSS_Themes::icon($icon, $url_only); +} |
