From 5e9c60752c9a40e1540de50d02e19ba1d06e5491 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sat, 1 Feb 2014 12:23:12 +0100 Subject: Actualize : Tolère PHP lancé en CGI plutôt que CLI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Corrige https://github.com/marienfressinaud/FreshRSS/issues/405 + Message d'erreur en cas de problème de droits dans ./data/ (par exemple si le cron utilise un mauvais utilisateur) --- lib/lib_rss.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/lib_rss.php') diff --git a/lib/lib_rss.php b/lib/lib_rss.php index 1bb117ab6..a13d9e951 100644 --- a/lib/lib_rss.php +++ b/lib/lib_rss.php @@ -214,8 +214,8 @@ function uSecString() { } function invalidateHttpCache() { - touch(LOG_PATH . '/' . Minz_Session::param('currentUser', '_') . '.log'); Minz_Session::_param('touch', uTimeString()); + return touch(LOG_PATH . '/' . Minz_Session::param('currentUser', '_') . '.log'); } function usernameFromPath($userPath) { -- cgit v1.2.3 From db120b4ed41584c7e33bfe63015f948e6230f1ab Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sun, 23 Feb 2014 12:16:23 +0100 Subject: Test si crypt() fonctionne pour PHP 5.3.3 https://github.com/marienfressinaud/FreshRSS/issues/432 --- README.md | 2 +- app/views/configure/users.phtml | 2 +- lib/lib_rss.php | 11 +++++++++++ p/i/install.php | 2 +- 4 files changed, 14 insertions(+), 3 deletions(-) (limited to 'lib/lib_rss.php') diff --git a/README.md b/README.md index 519db7e76..2b54ff9d5 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Privilégiez pour cela des demandes sur GitHub # Contrôle d’accès Il est requis pour le mode multi-utilisateur, et recommandé dans tous les cas, de limiter l’accès à votre FreshRSS. Au choix : -* En utilisant l’identification par formulaire (requiert PHP 5.3.4+ et JavaScript) +* En utilisant l’identification par formulaire (requiert JavaScript, et PHP 5.3.7+ recommandé – fonctionne avec certaines versions de PHP5.3.3+) * En utilisant l’identification par [Mozilla Persona](https://login.persona.org/about) incluse dans FreshRSS * En utilisant un contrôle d’accès HTTP défini par votre serveur Web * Voir par exemple la [documentation d’Apache sur l’authentification](http://httpd.apache.org/docs/trunk/howto/auth.html) diff --git a/app/views/configure/users.phtml b/app/views/configure/users.phtml index 18e4dd091..0677db881 100644 --- a/app/views/configure/users.phtml +++ b/app/views/configure/users.phtml @@ -52,7 +52,7 @@ - + diff --git a/lib/lib_rss.php b/lib/lib_rss.php index a13d9e951..8c6fb5543 100644 --- a/lib/lib_rss.php +++ b/lib/lib_rss.php @@ -233,3 +233,14 @@ function listUsers() { 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) { + } + } + return false; +} diff --git a/p/i/install.php b/p/i/install.php index dd38fab1f..720813323 100644 --- a/p/i/install.php +++ b/p/i/install.php @@ -837,7 +837,7 @@ function printStep2 () { - + -- cgit v1.2.3 From 11b807e4c3ee63172fecae9953d92a8b937a768a Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sun, 2 Mar 2014 00:08:45 +0100 Subject: Bug usernames with uppercase https://github.com/marienfressinaud/FreshRSS/issues/439 --- lib/lib_rss.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/lib_rss.php') diff --git a/lib/lib_rss.php b/lib/lib_rss.php index 8c6fb5543..83edbf015 100644 --- a/lib/lib_rss.php +++ b/lib/lib_rss.php @@ -219,7 +219,7 @@ function invalidateHttpCache() { } function usernameFromPath($userPath) { - if (preg_match('%/([a-z0-9]{1,16})_user\.php$%', $userPath, $matches)) { + if (preg_match('%/([A-Za-z0-9]{1,16})_user\.php$%', $userPath, $matches)) { return $matches[1]; } else { return ''; -- cgit v1.2.3 From 5081ffaf39699398f83be97e47b72444e5bcd5d1 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sat, 22 Mar 2014 17:56:07 +0100 Subject: Minz: remove one layer of ob_ (experimental) https://github.com/marienfressinaud/FreshRSS/issues/303#issuecomment-38351311 https://github.com/marienfressinaud/FreshRSS/issues/163 * Remove Minz_Response (not needed anymore) * Move Minz_Request::reseted to Minz_Dispatcher::reset() --- app/Controllers/importExportController.php | 14 ++---- app/actualize_script.php | 1 - lib/Minz/Dispatcher.php | 73 +++++++++++++----------------- lib/Minz/Error.php | 32 +++++++++---- lib/Minz/FrontController.php | 16 +------ lib/Minz/Request.php | 5 +- lib/Minz/Response.php | 60 ------------------------ lib/Minz/View.php | 10 ++++ lib/lib_rss.php | 2 +- 9 files changed, 73 insertions(+), 140 deletions(-) delete mode 100644 lib/Minz/Response.php (limited to 'lib/lib_rss.php') diff --git a/app/Controllers/importExportController.php b/app/Controllers/importExportController.php index 458814676..f697f4c9e 100644 --- a/app/Controllers/importExportController.php +++ b/app/Controllers/importExportController.php @@ -65,7 +65,7 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { $export_all = Minz_Request::param('export_all', false); // code from https://stackoverflow.com/questions/1061710/php-zip-files-on-the-fly - $file = tempnam("tmp", "zip"); + $file = tempnam('tmp', 'zip'); $zip = new ZipArchive(); $zip->open($file, ZipArchive::OVERWRITE); @@ -101,17 +101,11 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { } $this->view->categories = $list; - - // TODO: add a parameter to renderHelper in order to get a variable - ob_start(); - $this->view->renderHelper('export/opml'); - return ob_get_clean(); + return $this->view->helperToString('export/opml'); } private function generate_articles($type) { - // TODO: same here + we should get articles according to $type - ob_start(); - $this->view->renderHelper('export/articles'); - return ob_get_clean(); + // TODO: we should get articles according to $type + return $this->view->helperToString('export/articles'); } } diff --git a/app/actualize_script.php b/app/actualize_script.php index 8d81e0189..4c306b8da 100755 --- a/app/actualize_script.php +++ b/app/actualize_script.php @@ -28,7 +28,6 @@ foreach ($users as $myUser) { $_SERVER['HTTP_HOST'] = ''; $freshRSS = new FreshRSS(); - $freshRSS->_useOb(false); Minz_Configuration::_authType('none'); diff --git a/lib/Minz/Dispatcher.php b/lib/Minz/Dispatcher.php index 819f4cd5c..ca1fd1f5c 100644 --- a/lib/Minz/Dispatcher.php +++ b/lib/Minz/Dispatcher.php @@ -14,6 +14,7 @@ class Minz_Dispatcher { /* singleton */ private static $instance = null; + private static $needsReset; private $router; private $controller; @@ -40,44 +41,36 @@ class Minz_Dispatcher { * Remplit le body de Response à partir de la Vue * @exception Minz_Exception */ - public function run ($ob = true) { - // Le ob_start est dupliqué : sans ça il y a un bug sous Firefox - // ici on l'appelle avec 'ob_gzhandler', après sans. - // Vraisemblablement la compression fonctionne mais c'est sale - // J'ignore les effets de bord :( - if ($ob) { - ob_start ('ob_gzhandler'); - } - - $text = ''; //TODO: Clean this code - while (Minz_Request::$reseted) { - Minz_Request::$reseted = false; + public function run () { + do { + self::$needsReset = false; try { $this->createController ('FreshRSS_' . Minz_Request::controllerName () . '_Controller'); $this->controller->init (); $this->controller->firstAction (); - $this->launchAction ( - Minz_Request::actionName () - . 'Action' - ); + if (!self::$needsReset) { + $this->launchAction ( + Minz_Request::actionName () + . 'Action' + ); + } $this->controller->lastAction (); - if (!Minz_Request::$reseted) { - if ($ob) { - ob_start (); - } - $this->controller->view ()->build (); - if ($ob) { - $text = ob_get_clean(); - } + if (!self::$needsReset) { + echo $this->controller->view ()->build (); } } catch (Minz_Exception $e) { throw $e; } - } + } while (self::$needsReset); + } - Minz_Response::setBody ($text); + /** + * Informe le contrôleur qu'il doit recommancer car la requête a été modifiée + */ + public static function reset() { + self::$needsReset = true; } /** @@ -114,21 +107,19 @@ class Minz_Dispatcher { * le controller */ private function launchAction ($action_name) { - if (!Minz_Request::$reseted) { - if (!is_callable (array ( - $this->controller, - $action_name - ))) { - throw new Minz_ActionException ( - get_class ($this->controller), - $action_name, - Minz_Exception::ERROR - ); - } - call_user_func (array ( - $this->controller, - $action_name - )); + if (!is_callable (array ( + $this->controller, + $action_name + ))) { + throw new Minz_ActionException ( + get_class ($this->controller), + $action_name, + Minz_Exception::ERROR + ); } + call_user_func (array ( + $this->controller, + $action_name + )); } } diff --git a/lib/Minz/Error.php b/lib/Minz/Error.php index 337ab6c0a..c8222a430 100644 --- a/lib/Minz/Error.php +++ b/lib/Minz/Error.php @@ -23,13 +23,32 @@ class Minz_Error { $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_Response::setHeader ($code); if ($redirect) { Minz_Request::forward (array ( 'c' => 'error' @@ -41,19 +60,16 @@ class Minz_Error { ), false); } } else { - $text = '

An error occured

'."\n"; + echo '

An error occured

' . "\n"; if (!empty ($logs)) { - $text .= '
    '."\n"; + echo '
      ' . "\n"; foreach ($logs as $log) { - $text .= '
    • ' . $log . '
    • '."\n"; + echo '
    • ' . $log . '
    • ' . "\n"; } - $text .= '
    '."\n"; + echo '
' . "\n"; } - Minz_Response::setHeader ($code); - Minz_Response::setBody ($text); - Minz_Response::send (); exit (); } } diff --git a/lib/Minz/FrontController.php b/lib/Minz/FrontController.php index 80eda8877..3e50db1cf 100644 --- a/lib/Minz/FrontController.php +++ b/lib/Minz/FrontController.php @@ -26,8 +26,6 @@ class Minz_FrontController { protected $dispatcher; protected $router; - private $useOb = true; - /** * Constructeur * Initialise le router et le dispatcher @@ -63,8 +61,7 @@ class Minz_FrontController { */ public function run () { try { - $this->dispatcher->run ($this->useOb); - Minz_Response::send (); + $this->dispatcher->run(); } catch (Minz_Exception $e) { try { Minz_Log::record ($e->getMessage (), Minz_Log::ERROR); @@ -96,15 +93,4 @@ class Minz_FrontController { } exit ('### Application problem ###
'."\n".$txt); } - - public function useOb() { - return $this->useOb; - } - - /** - * Use ob_start('ob_gzhandler') or not. - */ - public function _useOb($ob) { - return $this->useOb = (bool)$ob; - } } diff --git a/lib/Minz/Request.php b/lib/Minz/Request.php index 282d47a77..7e3c59990 100644 --- a/lib/Minz/Request.php +++ b/lib/Minz/Request.php @@ -15,8 +15,6 @@ class Minz_Request { private static $default_controller_name = 'index'; private static $default_action_name = 'index'; - public static $reseted = true; - /** * Getteurs */ @@ -137,14 +135,13 @@ class Minz_Request { header ('Location: ' . Minz_Url::display ($url, 'php')); exit (); } else { - self::$reseted = true; - self::_controllerName ($url['c']); self::_actionName ($url['a']); self::_params (array_merge ( self::$params, $url['params'] )); + Minz_Dispatcher::reset(); } } diff --git a/lib/Minz/Response.php b/lib/Minz/Response.php deleted file mode 100644 index f8ea3d946..000000000 --- a/lib/Minz/Response.php +++ /dev/null @@ -1,60 +0,0 @@ - -*/ - -/** - * Response représente la requête http renvoyée à l'utilisateur - */ -class Minz_Response { - private static $header = 'HTTP/1.0 200 OK'; - private static $body = ''; - - /** - * Mets à jour le body de la Response - * @param $text le texte à incorporer dans le body - */ - public static function setBody ($text) { - self::$body = $text; - } - - /** - * Mets à jour le header de la Response - * @param $code le code HTTP, valeurs possibles - * - 200 (OK) - * - 403 (Forbidden) - * - 404 (Forbidden) - * - 500 (Forbidden) -> par défaut si $code erroné - * - 503 (Forbidden) - */ - public static function setHeader ($code) { - switch ($code) { - case 200 : - self::$header = 'HTTP/1.0 200 OK'; - break; - case 403 : - self::$header = 'HTTP/1.0 403 Forbidden'; - break; - case 404 : - self::$header = 'HTTP/1.0 404 Not Found'; - break; - case 500 : - self::$header = 'HTTP/1.0 500 Internal Server Error'; - break; - case 503 : - self::$header = 'HTTP/1.0 503 Service Unavailable'; - break; - default : - self::$header = 'HTTP/1.0 500 Internal Server Error'; - } - } - - /** - * Envoie la Response à l'utilisateur - */ - public static function send () { - header (self::$header); - echo self::$body; - } -} diff --git a/lib/Minz/View.php b/lib/Minz/View.php index e170bd406..00d9a1a6d 100644 --- a/lib/Minz/View.php +++ b/lib/Minz/View.php @@ -102,6 +102,16 @@ class Minz_View { } } + /** + * Retourne renderHelper() dans une chaîne + * @param $helper l'élément à traîter + */ + public function helperToString($helper) { + ob_start(); + renderHelper($helper); + return ob_get_clean(); + } + /** * Permet de choisir si on souhaite utiliser le layout * @param $use true si on souhaite utiliser le layout, false sinon diff --git a/lib/lib_rss.php b/lib/lib_rss.php index 83edbf015..2077fe63f 100644 --- a/lib/lib_rss.php +++ b/lib/lib_rss.php @@ -27,7 +27,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) { -- cgit v1.2.3 From 9ea3819402746d8425d4a608f2d5f3c0f5bc29fb Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Sat, 29 Mar 2014 20:18:57 +0100 Subject: Better OPML import / export - use a new OPML library (https://github.com/marienfressinaud/lib_opml) - import has been completely rewritten (far better!) - introduce addFeedObject and addCategoryObject (in DAO for the moment). Permit to add easily feeds and categories (check if they already exist in DB) - introduce html_chars_utf8 (wrap htmlspecialchars for UTF-8) --- app/Controllers/importExportController.php | 124 ++++++++----- app/Exceptions/OpmlException.php | 6 - app/Models/CategoryDAO.php | 12 ++ app/Models/FeedDAO.php | 29 +++ app/views/helpers/export/opml.phtml | 43 +++-- lib/lib_opml.php | 277 ++++++++++++++++++++--------- lib/lib_rss.php | 4 + 7 files changed, 345 insertions(+), 150 deletions(-) delete mode 100644 app/Exceptions/OpmlException.php (limited to 'lib/lib_rss.php') diff --git a/app/Controllers/importExportController.php b/app/Controllers/importExportController.php index cbadeb6ca..b6b4d0fed 100644 --- a/app/Controllers/importExportController.php +++ b/app/Controllers/importExportController.php @@ -129,71 +129,101 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { } private function import_opml($opml_file) { - $categories = array(); - $feeds = array(); + $opml_array = array(); try { - list($categories, $feeds) = opml_import($opml_file); - } catch (FreshRSS_Opml_Exception $e) { + $opml_array = libopml_parse_string($opml_file); + } catch (LibOPML_Exception $e) { Minz_Log::warning($e->getMessage()); return true; } $this->catDAO->checkDefault(); - // on ajoute les catégories en masse dans une fonction à part - $this->addCategories($categories); - - // on calcule la date des articles les plus anciens qu'on accepte - $nb_month_old = $this->view->conf->old_entries; - $date_min = time() - (3600 * 24 * 30 * $nb_month_old); + return $this->addOpmlElements($opml_array['body']); + } - // la variable $error permet de savoir si une erreur est survenue - // Le but est de ne pas arrêter l'import même en cas d'erreur - // L'utilisateur sera mis au courant s'il y a eu des erreurs, mais - // ne connaîtra pas les détails. Ceux-ci seront toutefois logguées + private function addOpmlElements($opml_elements, $parent_cat = null) { $error = false; - foreach ($feeds as $feed) { - try { - $values = array( - 'id' => $feed->id(), - 'url' => $feed->url(), - 'category' => $feed->category(), - 'name' => $feed->name(), - 'website' => $feed->website(), - 'description' => $feed->description(), - 'lastUpdate' => 0, - 'httpAuth' => $feed->httpAuth() - ); + foreach ($opml_elements as $elt) { + $res = false; + if (isset($elt['xmlUrl'])) { + $res = $this->addFeedOpml($elt, $parent_cat); + } else { + $res = $this->addCategoryOpml($elt, $parent_cat); + } - // ajout du flux que s'il n'est pas déjà en BDD - if (!$this->feedDAO->searchByUrl($values['url'])) { - $id = $this->feedDAO->addFeed($values); - if ($id) { - $feed->_id($id); - $feed->faviconPrepare(); - } else { - $error = true; - } - } - } catch (FreshRSS_Feed_Exception $e) { - $error = true; - Minz_Log::record($e->getMessage(), Minz_Log::WARNING); + if (!$error && $res) { + // oops: there is at least one error! + $error = $res; } } return $error; } - private function addCategories($categories) { - foreach ($categories as $cat) { - if (!$this->catDAO->searchByName($cat->name())) { - $values = array( - 'id' => $cat->id(), - 'name' => $cat->name(), - ); - $this->catDAO->addCategory($values); + private function addFeedOpml($feed_elt, $parent_cat) { + if (is_null($parent_cat)) { + // This feed has no parent category so we get the default one + $parent_cat = $catDAO->getDefault()->name(); + } + + $cat = $this->catDAO->searchByName($parent_cat); + + if (!$cat) { + return true; + } + + // We get different useful information + $url = html_chars_utf8($feed_elt['xmlUrl']); + $name = html_chars_utf8($feed_elt['text']); + $website = ''; + if (isset($feed_elt['htmlUrl'])) { + $website = html_chars_utf8($feed_elt['htmlUrl']); + } + $description = ''; + if (isset($feed_elt['description'])) { + $description = html_chars_utf8($feed_elt['description']); + } + + $error = false; + try { + // Create a Feed object and add it in DB + $feed = new FreshRSS_Feed($url); + $feed->_category($cat->id()); + $feed->_name($name); + $feed->_website($website); + $feed->_description($description); + + // addFeedObject checks if feed is already in DB so nothing else to + // check here + $id = $this->feedDAO->addFeedObject($feed); + $error = ($id === false); + } catch (FreshRSS_Feed_Exception $e) { + Minz_Log::record($e->getMessage(), Minz_Log::WARNING); + $error = true; + } + + return $error; + } + + private function addCategoryOpml($cat_elt, $parent_cat) { + // Create a new Category object + $cat = new FreshRSS_Category(html_chars_utf8($cat_elt['text'])); + + $id = $this->catDAO->addCategoryObject($cat); + $error = ($id === false); + + if (isset($cat_elt['@outlines'])) { + // Our cat_elt contains more categories or more feeds, so we + // add them recursively. + // Note: FreshRSS does not support yet category arborescence + $res = $this->addOpmlElements($cat_elt['@outlines'], $cat->name()); + if (!$error && $res) { + $error = true; } } + + return $error; } private function import_articles($article_file, $starred = false) { diff --git a/app/Exceptions/OpmlException.php b/app/Exceptions/OpmlException.php deleted file mode 100644 index e0ea3e493..000000000 --- a/app/Exceptions/OpmlException.php +++ /dev/null @@ -1,6 +0,0 @@ -searchByName($category->name())) { + // Category does not exist yet in DB so we add it before continue + $values = array( + 'name' => $category->name(), + ); + return $this->addCategory($values); + } + + return false; + } + public function updateCategory ($id, $valuesTmp) { $sql = 'UPDATE `' . $this->prefix . 'category` SET name=? WHERE id=?'; $stm = $this->bd->prepare ($sql); diff --git a/app/Models/FeedDAO.php b/app/Models/FeedDAO.php index ca25c3aeb..eac21df7e 100644 --- a/app/Models/FeedDAO.php +++ b/app/Models/FeedDAO.php @@ -24,6 +24,35 @@ class FreshRSS_FeedDAO extends Minz_ModelPdo { } } + public function addFeedObject($feed) { + // TODO: not sure if we should write this method in DAO since DAO + // should not be aware about feed class + + // Add feed only if we don't find it in DB + if (!$this->searchByUrl($feed->url())) { + $values = array( + 'id' => $feed->id(), + 'url' => $feed->url(), + 'category' => $feed->category(), + 'name' => $feed->name(), + 'website' => $feed->website(), + 'description' => $feed->description(), + 'lastUpdate' => 0, + 'httpAuth' => $feed->httpAuth() + ); + + $id = $this->addFeed($values); + if ($id) { + $feed->_id($id); + $feed->faviconPrepare(); + } + + return $id; + } + + return false; + } + public function updateFeed ($id, $valuesTmp) { $set = ''; foreach ($valuesTmp as $key => $v) { diff --git a/app/views/helpers/export/opml.phtml b/app/views/helpers/export/opml.phtml index 2e66e5054..adbac904d 100644 --- a/app/views/helpers/export/opml.phtml +++ b/app/views/helpers/export/opml.phtml @@ -1,15 +1,30 @@ '; -?> - - - - <?php echo Minz_Configuration::title (); ?> OPML Feed - - - -categories); ?> - - + +$opml_array = array( + 'head' => array( + 'title' => Minz_Configuration::title(), + 'dateCreated' => date('D, d M Y H:i:s') + ), + 'body' => array() +); + +foreach ($this->categories as $key => $cat) { + $opml_array['body'][$key] = array( + 'text' => $cat['name'], + '@outlines' => array() + ); + + foreach ($cat['feeds'] as $feed) { + $opml_array['body'][$key]['@outlines'][] = array( + 'text' => $feed->name(), + 'type' => 'rss', + 'xmlUrl' => $feed->url(), + 'htmlUrl' => $feed->website(), + 'description' => htmlspecialchars( + $feed->description(), ENT_COMPAT, 'UTF-8' + ) + ); + } +} + +echo libopml_render($opml_array); diff --git a/lib/lib_opml.php b/lib/lib_opml.php index 05e54d85e..16a9921ea 100644 --- a/lib/lib_opml.php +++ b/lib/lib_opml.php @@ -1,23 +1,86 @@ ' . "\n"; - - foreach ($cat['feeds'] as $feed) { - $txt .= "\t" . '' . "\n"; +/* * + * 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) + * + * Usages: + * > include('lib_opml.php'); + * > $filename = 'my_opml_file.xml'; + * > $opml_array = libopml_parse_file($filename); + * > print_r($opml_array); + * + * > $opml_string = [...]; + * > $opml_array = libopml_parse_string($opml_string); + * > print_r($opml_array); + * + * > $opml_array = [...]; + * > $opml_string = libopml_render($opml_array); + * > $opml_object = libopml_render($opml_array, true); + * > echo $opml_string; + * > print_r($opml_object); + * + * 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 + * Url: https://github.com/marienfressinaud/lib_opml + * Version: 0.1 + * Date: 2014-03-29 + * License: public domain + * + * */ + +class LibOPML_Exception extends Exception {} + + +// These elements are optional +define('HEAD_ELEMENTS', serialize(array( + 'title', 'dateCreated', 'dateModified', 'ownerName', 'ownerEmail', + 'ownerId', 'docs', 'expansionState', 'vertScrollState', 'windowTop', + 'windowLeft', 'windowBottom', 'windowRight' +))); + + +function libopml_parse_outline($outline_xml) { + $outline = array(); + + // An outline may contain any kind of attributes but "text" attribute is + // required ! + $text_is_present = false; + foreach ($outline_xml->attributes() as $key => $value) { + $outline[$key] = (string)$value; + + if ($key === 'text') { + $text_is_present = true; } + } - $txt .= '' . "\n"; + if (!$text_is_present) { + throw new LibOPML_Exception( + 'Outline does not contain any text attribute' + ); } - return $txt; + 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); + } else { + throw new LibOPML_Exception( + 'Body can contain only outline elements' + ); + } + } + + return $outline; } -function opml_import ($xml) { - $xml = html_only_entity_decode($xml); //!\ Assume UTF-8 +function libopml_parse_string($xml) { $dom = new DOMDocument(); $dom->recover = true; $dom->strictErrorChecking = false; @@ -27,94 +90,142 @@ function opml_import ($xml) { $opml = simplexml_import_dom($dom); if (!$opml) { - throw new FreshRSS_Opml_Exception (); + throw new LibOPML_Exception(); } - $catDAO = new FreshRSS_CategoryDAO(); - $catDAO->checkDefault(); - $defCat = $catDAO->getDefault(); + $array = array( + 'version' => (string)$opml['version'], + 'head' => array(), + 'body' => array() + ); + + // First, we get all "head" elements. Head is required but its sub-elements + // are optional. + foreach ($opml->head->children() as $key => $value) { + if (in_array($key, unserialize(HEAD_ELEMENTS), true)) { + $array['head'][$key] = (string)$value; + } else { + throw new LibOPML_Exception( + $key . 'is not part of OPML format' + ); + } + } - $categories = array (); - $feeds = array (); + // Then, we get body oulines. Body must contain at least one outline + // element. + $at_least_one_outline = false; + foreach ($opml->body->children() as $key => $value) { + if ($key === 'outline') { + $at_least_one_outline = true; + $array['body'][] = libopml_parse_outline($value); + } else { + throw new LibOPML_Exception( + 'Body can contain only outline elements' + ); + } + } + + if (!$at_least_one_outline) { + throw new LibOPML_Exception( + 'Body must contain at least one outline element' + ); + } - foreach ($opml->body->outline as $outline) { - if (!isset ($outline['xmlUrl'])) { - // Catégorie - $title = ''; + return $array; +} - if (isset ($outline['text'])) { - $title = (string) $outline['text']; - } elseif (isset ($outline['title'])) { - $title = (string) $outline['title']; - } - if ($title) { - // Permet d'éviter les soucis au niveau des id : - // ceux-ci sont générés en fonction de la date, - // un flux pourrait être dans une catégorie X avec l'id Y - // alors qu'il existe déjà la catégorie X mais avec l'id Z - // Y ne sera pas ajouté et le flux non plus vu que l'id - // de sa catégorie n'exisera pas - $title = htmlspecialchars($title, ENT_COMPAT, 'UTF-8'); - $catDAO = new FreshRSS_CategoryDAO (); - $cat = $catDAO->searchByName ($title); - if ($cat == null) { - $cat = new FreshRSS_Category ($title); - $values = array ( - 'name' => $cat->name () - ); - $cat->_id ($catDAO->addCategory ($values)); - } - - $feeds = array_merge ($feeds, getFeedsOutline ($outline, $cat->id ())); +function libopml_parse_file($filename) { + $file_content = file_get_contents($filename); + + if ($file_content === false) { + throw new LibOPML_Exception( + $filename . ' cannot be found' + ); + } + + return libopml_parse_string($file_content); +} + + +function libopml_render_outline($parent_elt, $outline) { + // Outline MUST be an array! + if (!is_array($outline)) { + throw new LibOPML_Exception( + 'Outline element must be defined as array' + ); + } + + $outline_elt = $parent_elt->addChild('outline'); + $text_is_present = false; + foreach ($outline as $key => $value) { + // Only outlines can be an array and so we consider children are also + // outline elements. + if ($key === '@outlines' && is_array($value)) { + foreach ($value as $outline_child) { + libopml_render_outline($outline_elt, $outline_child); } + } elseif (is_array($value)) { + throw new LibOPML_Exception( + 'Type of outline elements cannot be array: ' . $key + ); } else { - // Flux rss sans catégorie, on récupère l'ajoute dans la catégorie par défaut - $feeds[] = getFeed ($outline, $defCat->id()); + // Detect text attribute is present, that's good :) + if ($key === 'text') { + $text_is_present = true; + } + + $outline_elt->addAttribute($key, $value); } } - return array ($categories, $feeds); + if (!$text_is_present) { + throw new LibOPML_Exception( + 'You must define at least a text element for all outlines' + ); + } } -/** - * import all feeds of a given outline tag - */ -function getFeedsOutline ($outline, $cat_id) { - $feeds = array (); - foreach ($outline->children () as $child) { - if (isset ($child['xmlUrl'])) { - $feeds[] = getFeed ($child, $cat_id); - } else { - $feeds = array_merge( - $feeds, - getFeedsOutline ($child, $cat_id) - ); +function libopml_render($array, $as_xml_object = false) { + $opml = new SimpleXMLElement(''); + + // Create head element. $array['head'] is optional but head element will + // exist in the final XML object. + $head = $opml->addChild('head'); + if (isset($array['head'])) { + foreach ($array['head'] as $key => $value) { + if (in_array($key, unserialize(HEAD_ELEMENTS), true)) { + $head->addChild($key, $value); + } } } - return $feeds; -} + // Check body is set and contains at least one element + if (!isset($array['body'])) { + throw new LibOPML_Exception( + '$array must contain a body element' + ); + } + if (count($array['body']) <= 0) { + throw new LibOPML_Exception( + 'Body element must contain at least one element (array)' + ); + } -function getFeed ($outline, $cat_id) { - $url = (string) $outline['xmlUrl']; - $url = htmlspecialchars($url, ENT_COMPAT, 'UTF-8'); - $title = ''; - if (isset ($outline['text'])) { - $title = (string) $outline['text']; - } elseif (isset ($outline['title'])) { - $title = (string) $outline['title']; - } - $title = htmlspecialchars($title, ENT_COMPAT, 'UTF-8'); - $feed = new FreshRSS_Feed ($url); - $feed->_category ($cat_id); - $feed->_name ($title); - if (isset($outline['htmlUrl'])) { - $feed->_website(htmlspecialchars((string)$outline['htmlUrl'], ENT_COMPAT, 'UTF-8')); - } - if (isset($outline['description'])) { - $feed->_description(sanitizeHTML((string)$outline['description'])); - } - return $feed; + // Create outline elements + $body = $opml->addChild('body'); + foreach ($array['body'] as $outline) { + libopml_render_outline($body, $outline); + } + + // And return the final result + if ($as_xml_object) { + return $opml; + } else { + $dom = dom_import_simplexml($opml)->ownerDocument; + $dom->formatOutput = true; + $dom->encoding = 'UTF-8'; + return $dom->saveXML(); + } } diff --git a/lib/lib_rss.php b/lib/lib_rss.php index 2077fe63f..0f8161129 100644 --- a/lib/lib_rss.php +++ b/lib/lib_rss.php @@ -244,3 +244,7 @@ function cryptAvailable() { } return false; } + +function html_chars_utf8($str) { + return htmlspecialchars($str, ENT_COMPAT, 'UTF-8'); +} -- cgit v1.2.3