diff options
| author | 2015-01-08 21:33:13 +0100 | |
|---|---|---|
| committer | 2015-01-08 21:33:13 +0100 | |
| commit | 250cd79251f5474915ad2230e786db70643b0ef3 (patch) | |
| tree | 3b757a544dfb0239bd119b953c316d27860f70cf | |
| parent | 0e4e16ac55097aa173c7c439367294ebd7645562 (diff) | |
| parent | b23fc3187cb90800aad6417badf7822a8d280b74 (diff) | |
Merge branch '252-extensions' into dev
37 files changed, 1293 insertions, 292 deletions
diff --git a/app/Controllers/extensionController.php b/app/Controllers/extensionController.php new file mode 100644 index 000000000..e013bb369 --- /dev/null +++ b/app/Controllers/extensionController.php @@ -0,0 +1,192 @@ +<?php + +/** + * The controller to manage extensions. + */ +class FreshRSS_extension_Controller extends Minz_ActionController { + /** + * This action is called before every other action in that class. It is + * the common boiler plate for every action. It is triggered by the + * underlying framework. + */ + public function firstAction() { + if (!FreshRSS_Auth::hasAccess()) { + Minz_Error::error(403); + } + } + + /** + * This action lists all the extensions available to the current user. + */ + public function indexAction() { + Minz_View::prependTitle(_t('admin.extensions.title') . ' · '); + $this->view->extension_list = Minz_ExtensionManager::list_extensions(); + } + + /** + * This action handles configuration of a given extension. + * + * Only administrator can configure a system extension. + * + * Parameters are: + * - e: the extension name (urlencoded) + * - additional parameters which should be handle by the extension + * handleConfigureAction() method (POST request). + */ + public function configureAction() { + if (Minz_Request::param('ajax')) { + $this->view->_useLayout(false); + } + + $ext_name = urldecode(Minz_Request::param('e')); + $ext = Minz_ExtensionManager::find_extension($ext_name); + + if (is_null($ext)) { + Minz_Error::error(404); + } + if ($ext->getType() === 'system' && !FreshRSS_Auth::hasAccess('admin')) { + Minz_Error::error(403); + } + + $this->view->extension = $ext; + $this->view->extension->handleConfigureAction(); + } + + /** + * This action enables a disabled extension for the current user. + * + * System extensions can only be enabled by an administrator. + * This action must be reached by a POST request. + * + * Parameter is: + * - e: the extension name (urlencoded). + */ + public function enableAction() { + $url_redirect = array('c' => 'extension', 'a' => 'index'); + + if (Minz_Request::isPost()) { + $ext_name = urldecode(Minz_Request::param('e')); + $ext = Minz_ExtensionManager::find_extension($ext_name); + + if (is_null($ext)) { + Minz_Request::bad(_t('feedback.extensions.not_found', $ext_name), + $url_redirect); + } + + if ($ext->is_enabled()) { + Minz_Request::bad(_t('feedback.extensions.already_enabled', $ext_name), + $url_redirect); + } + + $conf = null; + if ($ext->getType() === 'system' && FreshRSS_Auth::hasAccess('admin')) { + $conf = FreshRSS_Context::$system_conf; + } elseif ($ext->getType() === 'user') { + $conf = FreshRSS_Context::$user_conf; + } else { + Minz_Request::bad(_t('feedback.extensions.no_access', $ext_name), + $url_redirect); + } + + $ext->install(); + + $ext_list = $conf->extensions_enabled; + array_push_unique($ext_list, $ext_name); + $conf->extensions_enabled = $ext_list; + $conf->save(); + + Minz_Request::good(_t('feedback.extensions.enabled', $ext_name), + $url_redirect); + } + + Minz_Request::forward($url_redirect, true); + } + + /** + * This action disables an enabled extension for the current user. + * + * System extensions can only be disabled by an administrator. + * This action must be reached by a POST request. + * + * Parameter is: + * - e: the extension name (urlencoded). + */ + public function disableAction() { + $url_redirect = array('c' => 'extension', 'a' => 'index'); + + if (Minz_Request::isPost()) { + $ext_name = urldecode(Minz_Request::param('e')); + $ext = Minz_ExtensionManager::find_extension($ext_name); + + if (is_null($ext)) { + Minz_Request::bad(_t('feedback.extensions.not_found', $ext_name), + $url_redirect); + } + + if (!$ext->is_enabled()) { + Minz_Request::bad(_t('feedback.extensions.not_enabled', $ext_name), + $url_redirect); + } + + $conf = null; + if ($ext->getType() === 'system' && FreshRSS_Auth::hasAccess('admin')) { + $conf = FreshRSS_Context::$system_conf; + } elseif ($ext->getType() === 'user') { + $conf = FreshRSS_Context::$user_conf; + } else { + Minz_Request::bad(_t('feedback.extensions.no_access', $ext_name), + $url_redirect); + } + + $ext->uninstall(); + + $ext_list = $conf->extensions_enabled; + array_remove($ext_list, $ext_name); + $conf->extensions_enabled = $ext_list; + $conf->save(); + + Minz_Request::good(_t('feedback.extensions.disabled', $ext_name), + $url_redirect); + } + + Minz_Request::forward($url_redirect, true); + } + + /** + * This action handles deletion of an extension. + * + * Only administrator can remove an extension. + * This action must be reached by a POST request. + * + * Parameter is: + * -e: extension name (urlencoded) + */ + public function removeAction() { + if (!FreshRSS_Auth::hasAccess('admin')) { + Minz_Error::error(403); + } + + $url_redirect = array('c' => 'extension', 'a' => 'index'); + + if (Minz_Request::isPost()) { + $ext_name = urldecode(Minz_Request::param('e')); + $ext = Minz_ExtensionManager::find_extension($ext_name); + + if (is_null($ext)) { + Minz_Request::bad(_t('feedback.extensions.not_found', $ext_name), + $url_redirect); + } + + $res = recursive_unlink($ext->getPath()); + if ($res) { + Minz_Request::good(_t('feedback.extensions.removed', $ext_name), + $url_redirect); + } else { + Minz_Request::bad(_t('feedback.extensions.cannot_delete', $ext_name), + $url_redirect); + } + } + + Minz_Request::forward($url_redirect, true); + } +} diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index c110fda4e..6f544d834 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -142,6 +142,13 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $feed->_category($cat); $feed->_httpAuth($http_auth); + // Call the extension hook + $name = $feed->name(); + $feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed); + if (is_null($feed)) { + Minz_Request::bad(_t('feed_not_added', $name), $url_redirect); + } + $values = array( 'url' => $feed->url(), 'category' => $feed->category(), @@ -178,10 +185,17 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $feedDAO->beginTransaction(); foreach ($entries as $entry) { // Entries are added without any verification. + $entry->_feed($feed->id()); + $entry->_id(min(time(), $entry->date(true)) . uSecString()); + $entry->_isRead($is_read); + + $entry = Minz_ExtensionManager::callHook('entry_before_insert', $entry); + if (is_null($entry)) { + // An extension has returned a null value, there is nothing to insert. + continue; + } + $values = $entry->toArray(); - $values['id_feed'] = $feed->id(); - $values['id'] = min(time(), $entry->date(true)) . uSecString(); - $values['is_read'] = $is_read; $entryDAO->addEntry($values, $prepared_statement); } $feedDAO->updateLastUpdate($feed->id()); @@ -333,9 +347,16 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $id = min(time(), $entry_date) . uSecString(); } + $entry->_id($id); + $entry->_isRead($is_read); + + $entry = Minz_ExtensionManager::callHook('entry_before_insert', $entry); + if (is_null($entry)) { + // An extension has returned a null value, there is nothing to insert. + continue; + } + $values = $entry->toArray(); - $values['id'] = $id; - $values['is_read'] = $is_read; $entryDAO->addEntry($values, $prepared_statement); } } diff --git a/app/Controllers/importExportController.php b/app/Controllers/importExportController.php index 4ce24719e..db9db66a7 100644 --- a/app/Controllers/importExportController.php +++ b/app/Controllers/importExportController.php @@ -257,10 +257,16 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { $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); + // Call the extension hook + $feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed); + if (!is_null($feed)) { + // addFeedObject checks if feed is already in DB so nothing else to + // check here + $id = $this->feedDAO->addFeedObject($feed); + $error = ($id === false); + } else { + $error = true; + } } catch (FreshRSS_Feed_Exception $e) { Minz_Log::warning($e->getMessage()); $error = true; @@ -383,6 +389,12 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { $entry->_id(min(time(), $entry->date(true)) . uSecString()); $entry->_tags($tags); + $entry = Minz_ExtensionManager::callHook('entry_before_insert', $entry); + if (is_null($entry)) { + // An extension has returned a null value, there is nothing to insert. + continue; + } + $values = $entry->toArray(); $id = $this->entryDAO->addEntry($values, $prepared_statement); @@ -419,13 +431,17 @@ class FreshRSS_importExport_Controller extends Minz_ActionController { $feed->_name($name); $feed->_website($website); - // addFeedObject checks if feed is already in DB so nothing else to - // check here. - $id = $this->feedDAO->addFeedObject($feed); + // Call the extension hook + $feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed); + if (!is_null($feed)) { + // addFeedObject checks if feed is already in DB so nothing else to + // check here. + $id = $this->feedDAO->addFeedObject($feed); - if ($id !== false) { - $feed->_id($id); - $return = $feed; + if ($id !== false) { + $feed->_id($id); + $return = $feed; + } } } catch (FreshRSS_Feed_Exception $e) { Minz_Log::warning($e->getMessage()); diff --git a/app/FreshRSS.php b/app/FreshRSS.php index 002a70af5..453cc2d69 100644 --- a/app/FreshRSS.php +++ b/app/FreshRSS.php @@ -6,6 +6,8 @@ class FreshRSS extends Minz_FrontController { Minz_Session::init('FreshRSS'); } + // Load list of extensions and enable the "system" ones. + Minz_ExtensionManager::init(); $this->initConfiguration(); $this->initAuth(); FreshRSS_Context::init(); @@ -13,7 +15,11 @@ class FreshRSS extends Minz_FrontController { FreshRSS_Share::load(join_path(DATA_PATH, 'shares.php')); $this->loadStylesAndScripts(); $this->loadNotifications(); - $this->loadExtensions(); + // Enable extensions for the current (logged) user. + if (FreshRSS_Auth::hasAccess()) { + $ext_list = FreshRSS_Context::$user_conf->extensions_enabled; + Minz_ExtensionManager::enable_by_list($ext_list); + } } private function initConfiguration() { @@ -46,11 +52,7 @@ class FreshRSS extends Minz_FrontController { private function initI18n() { Minz_Session::_param('language', FreshRSS_Context::$user_conf->language); - - Minz_Translate::init(array( - 'en' => 'English', - 'fr' => 'Français', - ), FreshRSS_Context::$user_conf->language); + Minz_Translate::init(FreshRSS_Context::$user_conf->language); } private function loadStylesAndScripts() { @@ -91,24 +93,4 @@ class FreshRSS extends Minz_FrontController { Minz_Session::_param('notification'); } } - - private function loadExtensions() { - $extensionPath = FRESHRSS_PATH . '/extensions/'; - //TODO: Add a preference to load only user-selected extensions - foreach (scandir($extensionPath) as $key => $extension) { - if (ctype_alpha($extension)) { - $mtime = @filemtime($extensionPath . $extension . '/style.css'); - if ($mtime !== false) { - Minz_View::appendStyle(Minz_Url::display('/ext.php?c&e=' . $extension . '&' . $mtime)); - } - $mtime = @filemtime($extensionPath . $extension . '/script.js'); - if ($mtime !== false) { - Minz_View::appendScript(Minz_Url::display('/ext.php?j&e=' . $extension . '&' . $mtime)); - } - if (file_exists($extensionPath . $extension . '/module.php')) { - //TODO: include - } - } - } - } } diff --git a/app/Models/ConfigurationSetter.php b/app/Models/ConfigurationSetter.php index 9830fed28..f1c465c7c 100644 --- a/app/Models/ConfigurationSetter.php +++ b/app/Models/ConfigurationSetter.php @@ -67,6 +67,14 @@ class FreshRSS_ConfigurationSetter { } } + // It works for system config too! + private function _extensions_enabled(&$data, $value) { + if (!is_array($value)) { + $value = array($value); + } + $data['extensions_enabled'] = $value; + } + private function _html5_notif_timeout(&$data, $value) { $value = intval($value); $data['html5_notif_timeout'] = $value >= 0 ? $value : 0; @@ -81,7 +89,7 @@ class FreshRSS_ConfigurationSetter { private function _language(&$data, $value) { $value = strtolower($value); $languages = Minz_Translate::availableLanguages(); - if (!isset($languages[$value])) { + if (!in_array($value, $languages)) { $value = 'en'; } $data['language'] = $value; diff --git a/app/i18n/en/admin.php b/app/i18n/en/admin.php index 5653453df..6a89d4a31 100644 --- a/app/i18n/en/admin.php +++ b/app/i18n/en/admin.php @@ -102,6 +102,12 @@ return array( 'ok' => 'You have ZIP extension.', ), ), + 'extensions' => array( + 'empty_list' => 'There is no installed extension', + 'no_configure_view' => 'This extension cannot be configured.', + 'system' => 'System extension (you have no rights on it)', + 'title' => 'Extensions', + ), 'stats' => array( '_' => 'Statistics', 'all_feeds' => 'All feeds', diff --git a/app/i18n/en/feedback.php b/app/i18n/en/feedback.php index 8df83ac0a..5f7183da3 100644 --- a/app/i18n/en/feedback.php +++ b/app/i18n/en/feedback.php @@ -29,6 +29,14 @@ return array( 'shortcuts_updated' => 'Shortcuts have been updated', 'updated' => 'Configuration has been updated', ), + 'extensions' => array( + 'already_enabled' => '%s is already enabled', + 'disabled' => '%s is now disabled', + 'enabled' => '%s is now enabled', + 'no_access' => 'You have no access on %s', + 'not_enabled' => '%s is not enabled yet', + 'not_found' => '%s does not exist', + ), 'import_export' => array( 'export_no_zip_extension' => 'Zip extension is not present on your server. Please try to export files one by one.', 'feeds_imported' => 'Your feeds have been imported and will now be updated', diff --git a/app/i18n/en/gen.php b/app/i18n/en/gen.php index 4a6f8a71b..6271717d4 100644 --- a/app/i18n/en/gen.php +++ b/app/i18n/en/gen.php @@ -100,6 +100,10 @@ return array( 'notif_title_new_articles' => 'FreshRSS: new articles!', 'should_be_activated' => 'JavaScript must be enabled', ), + 'lang' => array( + 'en' => 'English', + 'fr' => 'Français', + ), 'menu' => array( 'about' => 'About', 'admin' => 'Administration', @@ -108,6 +112,7 @@ return array( 'check_install' => 'Installation checking', 'configuration' => 'Configuration', 'display' => 'Display', + 'extensions' => 'Extensions', 'logs' => 'Logs', 'queries' => 'User queries', 'reading' => 'Reading', diff --git a/app/i18n/fr/admin.php b/app/i18n/fr/admin.php index 7a4215722..2e879da61 100644 --- a/app/i18n/fr/admin.php +++ b/app/i18n/fr/admin.php @@ -102,6 +102,12 @@ return array( 'ok' => 'Vous disposez de l\'extension ZIP.', ), ), + 'extensions' => array( + 'empty_list' => 'Il n’y a aucune extension installée.', + 'no_configure_view' => 'Cette extension ne peut pas être configurée.', + 'system' => 'Extension système (vous n’avez aucun droit dessus)', + 'title' => 'Extensions', + ), 'stats' => array( '_' => 'Statistiques', 'all_feeds' => 'Tous les flux', diff --git a/app/i18n/fr/feedback.php b/app/i18n/fr/feedback.php index b9d1f6c99..5c71bbae1 100644 --- a/app/i18n/fr/feedback.php +++ b/app/i18n/fr/feedback.php @@ -29,6 +29,14 @@ return array( 'shortcuts_updated' => 'Les raccourcis ont été mis à jour.', 'updated' => 'La configuration a été mise à jour', ), + 'extensions' => array( + 'already_enabled' => '%s est déjà activée', + 'disabled' => '%s est désormais désactivée', + 'enabled' => '%s est désormais activée', + 'no_access' => 'Vous n’avez aucun accès sur %s', + 'not_enabled' => '%s n’est pas encore activée', + 'not_found' => '%s n’existe pas', + ), 'import_export' => array( 'export_no_zip_extension' => 'L’extension Zip n’est pas présente sur votre serveur. Veuillez essayer d’exporter les fichiers un par un.', 'feeds_imported' => 'Vos flux ont été importés et vont maintenant être actualisés.', diff --git a/app/i18n/fr/gen.php b/app/i18n/fr/gen.php index d990dad4a..6b449484f 100644 --- a/app/i18n/fr/gen.php +++ b/app/i18n/fr/gen.php @@ -100,6 +100,10 @@ return array( 'notif_title_new_articles' => 'FreshRSS : nouveaux articles !', 'should_be_activated' => 'Le JavaScript doit être activé.', ), + 'lang' => array( + 'en' => 'English', + 'fr' => 'Français', + ), 'menu' => array( 'about' => 'À propos', 'admin' => 'Administration', @@ -108,6 +112,7 @@ return array( 'check_install' => 'Vérification de l’installation', 'configuration' => 'Configuration', 'display' => 'Affichage', + 'extensions' => 'Extensions', 'logs' => 'Logs', 'queries' => 'Filtres utilisateurs', 'reading' => 'Lecture', diff --git a/app/install.php b/app/install.php index a9faa764b..a79b0d318 100644 --- a/app/install.php +++ b/app/install.php @@ -43,24 +43,18 @@ function param($key, $default = false) { // gestion internationalisation function initTranslate() { - $available_languages = array( - 'en' => 'English', - 'fr' => 'Français' - ); + Minz_Translate::init(); + $available_languages = Minz_Translate::availableLanguages(); if (!isset($_SESSION['language'])) { - $best = get_best_language(); - if (!isset($available_languages[$best])) { - $best = 'en'; - } + $_SESSION['language'] = get_best_language(); + } - $_SESSION['language'] = $best; + if (!in_array($_SESSION['language'], $available_languages)) { + $_SESSION['language'] = 'en'; } - Minz_Translate::init(array( - 'en' => 'English', - 'fr' => 'Français', - ), $_SESSION['language']); + Minz_Translate::reset($_SESSION['language']); } function get_best_language() { @@ -257,7 +251,7 @@ function checkStep() { function checkStep0() { $languages = Minz_Translate::availableLanguages(); $language = isset($_SESSION['language']) && - isset($languages[$_SESSION['language']]); + in_array($_SESSION['language'], $languages); return array( 'language' => $language ? 'ok' : 'ko', @@ -432,8 +426,10 @@ function printStep0() { <label class="group-name" for="language"><?php echo _t('install.language'); ?></label> <div class="group-controls"> <select name="language" id="language"> - <?php foreach ($languages as $short => $lib) { ?> - <option value="<?php echo $short; ?>"<?php echo $actual == $short ? ' selected="selected"' : ''; ?>><?php echo $lib; ?></option> + <?php foreach ($languages as $lang) { ?> + <option value="<?php echo $lang; ?>"<?php echo $actual == $lang ? ' selected="selected"' : ''; ?>> + <?php echo _t('gen.lang.' . $lang); ?> + </option> <?php } ?> </select> </div> diff --git a/app/layout/aside_configure.phtml b/app/layout/aside_configure.phtml index 25b8037e6..2d4cb9cad 100644 --- a/app/layout/aside_configure.phtml +++ b/app/layout/aside_configure.phtml @@ -22,6 +22,10 @@ Minz_Request::actionName() === 'profile'? ' active' : ''; ?>"> <a href="<?php echo _url('user', 'profile'); ?>"><?php echo _t('gen.menu.user_profile'); ?></a> </li> + <li class="item<?php echo Minz_Request::controllerName() === 'extension' && + Minz_Request::actionName() === 'index'? ' active' : ''; ?>"> + <a href="<?php echo _url('extension', 'index'); ?>"><?php echo _t('gen.menu.extensions'); ?></a> + </li> <?php if (FreshRSS_Auth::hasAccess('admin')) { ?> <li class="nav-header"><?php echo _t('gen.menu.admin'); ?></li> <li class="item<?php echo Minz_Request::controllerName() === 'user' && diff --git a/app/layout/header.phtml b/app/layout/header.phtml index 0c2e171dd..2b968252b 100644 --- a/app/layout/header.phtml +++ b/app/layout/header.phtml @@ -64,6 +64,7 @@ if (FreshRSS_Auth::accessNeedsAction()) { <li class="item"><a href="<?php echo _url('configure', 'shortcut'); ?>"><?php echo _t('gen.menu.shortcuts'); ?></a></li> <li class="item"><a href="<?php echo _url('configure', 'queries'); ?>"><?php echo _t('gen.menu.queries'); ?></a></li> <li class="item"><a href="<?php echo _url('user', 'profile'); ?>"><?php echo _t('gen.menu.user_profile'); ?></a></li> + <li class="item"><a href="<?php echo _url('extension', 'index'); ?>"><?php echo _t('gen.menu.extensions'); ?></a></li> <?php if (FreshRSS_Auth::hasAccess('admin')) { ?> <li class="separator"></li> <li class="dropdown-header"><?php echo _t('gen.menu.admin'); ?></li> diff --git a/app/views/configure/display.phtml b/app/views/configure/display.phtml index 36a075ea7..02249bc55 100644 --- a/app/views/configure/display.phtml +++ b/app/views/configure/display.phtml @@ -11,8 +11,8 @@ <div class="group-controls"> <select name="language" id="language"> <?php $languages = Minz_Translate::availableLanguages(); ?> - <?php foreach ($languages as $short => $lib) { ?> - <option value="<?php echo $short; ?>"<?php echo FreshRSS_Context::$user_conf->language === $short ? ' selected="selected"' : ''; ?>><?php echo $lib; ?></option> + <?php foreach ($languages as $lang) { ?> + <option value="<?php echo $lang; ?>"<?php echo FreshRSS_Context::$user_conf->language === $lang ? ' selected="selected"' : ''; ?>><?php echo _t('gen.lang.' . $lang); ?></option> <?php } ?> </select> </div> diff --git a/app/views/extension/configure.phtml b/app/views/extension/configure.phtml new file mode 100644 index 000000000..c03439a75 --- /dev/null +++ b/app/views/extension/configure.phtml @@ -0,0 +1,23 @@ +<?php + +if (!Minz_Request::param('ajax')) { + $this->partial('aside_configure'); +} + +?> + +<div class="post"> + <h1><?php echo $this->extension->getName(); ?> (<?php echo $this->extension->getVersion(); ?>) — <?php echo $this->extension->getType(); ?></h1> + + <p class="alert alert-warn"><?php echo $this->extension->getDescription(); ?> — <?php echo _t('gen.short.by_author', $this->extension->getAuthor()); ?></p> + + <h2><?php echo _t('gen.action.manage'); ?></h2> + <?php + $configure_view = $this->extension->getConfigureView(); + if ($configure_view !== false) { + echo $configure_view; + } else { + ?> + <p class="alert alert-warn"><?php echo _t('admin.extensions.no_configure_view'); ?></p> + <?php } ?> +</div> diff --git a/app/views/extension/index.phtml b/app/views/extension/index.phtml new file mode 100644 index 000000000..fd97c5e81 --- /dev/null +++ b/app/views/extension/index.phtml @@ -0,0 +1,39 @@ +<?php $this->partial('aside_configure'); ?> + +<div class="post"> + <a href="<?php echo _url('index', 'index'); ?>"><?php echo _t('gen.action.back_to_rss_feeds'); ?></a> + + <h1><?php echo _t('admin.extensions.title'); ?></h1> + + <?php if (!empty($this->extension_list)) { ?> + <form id="form-extension" method="post" style="display: none"></form> + <?php foreach ($this->extension_list as $ext) { ?> + <ul class="horizontal-list"> + <li class="item"> + <?php if ($ext->getType() === 'user' || FreshRSS_Auth::hasAccess('admin')) { ?> + <?php $name_encoded = urlencode($ext->getName()); ?> + <div class="stick"> + <a class="btn open-slider" href="<?php echo _url('extension', 'configure', 'e', $name_encoded); ?>"><?php echo _i('configure'); ?> <?php echo _t('gen.action.manage'); ?></a> + <?php if ($ext->is_enabled()) { ?> + <button class="btn active" form="form-extension" formaction="<?php echo _url('extension', 'disable', 'e', $name_encoded); ?>"><?php echo _t('gen.action.disable'); ?></button> + <?php } else { ?> + <button class="btn" form="form-extension" formaction="<?php echo _url('extension', 'enable', 'e', $name_encoded); ?>"><?php echo _t('gen.action.enable'); ?></button> + <?php } ?> + <?php if (FreshRSS_Auth::hasAccess('admin')) { ?> + <button class="btn btn-attention confirm" form="form-extension" formaction="<?php echo _url('extension', 'remove', 'e', $name_encoded); ?>"><?php echo _t('gen.action.remove'); ?></button> + <?php } ?> + </div> + <?php } else { ?> + <?php echo _t('admin.extensions.system'); ?> + <?php } ?> + </li> + <li class="item"><?php echo $ext->getName(); ?></li> + </ul> + <?php } ?> + <?php } else { ?> + <p class="alert alert-warn"><?php echo _t('admin.extensions.empty_list'); ?></p> + <?php } ?> +</div> + +<a href="#" id="close-slider"></a> +<div id="slider"></div> diff --git a/app/views/helpers/index/normal/entry_bottom.phtml b/app/views/helpers/index/normal/entry_bottom.phtml new file mode 100644 index 000000000..32317d027 --- /dev/null +++ b/app/views/helpers/index/normal/entry_bottom.phtml @@ -0,0 +1,86 @@ +<?php + $sharing = array(); + if (FreshRSS_Auth::hasAccess()) { + $sharing = FreshRSS_Context::$user_conf->sharing; + } + + $bottomline_read = FreshRSS_Context::$user_conf->bottomline_read; + $bottomline_favorite = FreshRSS_Context::$user_conf->bottomline_favorite; + $bottomline_sharing = FreshRSS_Context::$user_conf->bottomline_sharing && (count($sharing) > 0); + $bottomline_tags = FreshRSS_Context::$user_conf->bottomline_tags; + $bottomline_date = FreshRSS_Context::$user_conf->bottomline_date; + $bottomline_link = FreshRSS_Context::$user_conf->bottomline_link; +?><ul class="horizontal-list bottom"><?php + if (FreshRSS_Auth::hasAccess()) { + if ($bottomline_read) { + ?><li class="item manage"><?php + $arUrl = array('c' => 'entry', 'a' => 'read', 'params' => array('id' => $this->entry->id())); + if ($this->entry->isRead()) { + $arUrl['params']['is_read'] = 0; + } + ?><a class="read" href="<?php echo Minz_Url::display($arUrl); ?>"><?php + echo _i($this->entry->isRead() ? 'read' : 'unread'); ?></a><?php + ?></li><?php + } + if ($bottomline_favorite) { + ?><li class="item manage"><?php + $arUrl = array('c' => 'entry', 'a' => 'bookmark', 'params' => array('id' => $this->entry->id())); + if ($this->entry->isFavorite()) { + $arUrl['params']['is_favorite'] = 0; + } + ?><a class="bookmark" href="<?php echo Minz_Url::display($arUrl); ?>"><?php + echo _i($this->entry->isFavorite() ? 'starred' : 'non-starred'); ?></a><?php + ?></li><?php + } + } ?> + <li class="item"><?php + if ($bottomline_sharing) { + ?><div class="dropdown"> + <div id="dropdown-share-<?php echo $item->id();?>" class="dropdown-target"></div> + <a class="dropdown-toggle" href="#dropdown-share-<?php echo $item->id();?>"> + <?php echo _i('share'); ?> + <?php echo _t('index.share'); ?> + </a> + + <ul class="dropdown-menu"> + <li class="dropdown-close"><a href="#close">❌</a></li><?php + $link = $item->link(); + $title = $item->title() . ' · ' . $feed->name(); + foreach (FreshRSS_Context::$user_conf->sharing as $share_options) { + $share = FreshRSS_Share::get($share_options['type']); + $share_options['link'] = $link; + $share_options['title'] = $title; + $share->update($share_options); + ?><li class="item share"> + <a target="_blank" href="<?php echo $share->url(); ?>"><?php echo $share->name(); ?></a> + </li><?php + } + ?></ul> + </div> + <?php } ?> + </li><?php + $tags = $bottomline_tags ? $this->entry->tags() : null; + if (!empty($tags)) { + ?><li class="item"> + <div class="dropdown"> + <div id="dropdown-tags-<?php echo $this->entry->id();?>" class="dropdown-target"></div> + <?php echo _i('tag'); ?> + <a class="dropdown-toggle" href="#dropdown-tags-<?php echo $this->entry->id();?>"><?php + echo _t('index.tag.related'); + ?></a> + <ul class="dropdown-menu"> + <li class="dropdown-close"><a href="#close">❌</a></li><?php + foreach($tags as $tag) { + ?><li class="item"><a href="<?php echo _url('index', 'index', 'search', urlencode('#' . $tag)); ?>"><?php echo $tag; ?></a></li><?php + } ?> + </ul> + </div> + </li><?php + } + if ($bottomline_date) { + ?><li class="item date"><?php echo $this->entry->date(); ?></li><?php + } + if ($bottomline_link) { + ?><li class="item link"><a target="_blank" href="<?php echo $this->entry->link(); ?>"><?php echo _i('link'); ?></a></li><?php + } ?> +</ul> diff --git a/app/views/helpers/index/normal/entry_header.phtml b/app/views/helpers/index/normal/entry_header.phtml new file mode 100644 index 000000000..dc544298f --- /dev/null +++ b/app/views/helpers/index/normal/entry_header.phtml @@ -0,0 +1,33 @@ +<?php + $topline_read = FreshRSS_Context::$user_conf->topline_read; + $topline_favorite = FreshRSS_Context::$user_conf->topline_favorite; + $topline_date = FreshRSS_Context::$user_conf->topline_date; + $topline_link = FreshRSS_Context::$user_conf->topline_link; +?><ul class="horizontal-list flux_header"><?php + if (FreshRSS_Auth::hasAccess()) { + if ($topline_read) { + ?><li class="item manage"><?php + $arUrl = array('c' => 'entry', 'a' => 'read', 'params' => array('id' => $this->entry->id())); + if ($this->entry->isRead()) { + $arUrl['params']['is_read'] = 0; + } + ?><a class="read" href="<?php echo Minz_Url::display($arUrl); ?>"><?php + echo _i($this->entry->isRead() ? 'read' : 'unread'); ?></a><?php + ?></li><?php + } + if ($topline_favorite) { + ?><li class="item manage"><?php + $arUrl = array('c' => 'entry', 'a' => 'bookmark', 'params' => array('id' => $this->entry->id())); + if ($this->entry->isFavorite()) { + $arUrl['params']['is_favorite'] = 0; + } + ?><a class="bookmark" href="<?php echo Minz_Url::display($arUrl); ?>"><?php + echo _i($this->entry->isFavorite() ? 'starred' : 'non-starred'); ?></a><?php + ?></li><?php + } + } + ?><li class="item website"><a href="<?php echo _url('index', 'index', 'get', 'f_' . $this->feed->id()); ?>"><img class="favicon" src="<?php echo $this->feed->favicon(); ?>" alt="✇" /> <span><?php echo $this->feed->name(); ?></span></a></li> + <li class="item title"><a target="_blank" href="<?php echo $this->entry->link(); ?>"><?php echo $this->entry->title(); ?></a></li> + <?php if ($topline_date) { ?><li class="item date"><?php echo $this->entry->date(); ?> </li><?php } ?> + <?php if ($topline_link) { ?><li class="item link"><a target="_blank" href="<?php echo $this->entry->link(); ?>"><?php echo _i('link'); ?></a></li><?php } ?> +</ul> diff --git a/app/views/index/index.phtml b/app/views/index/index.phtml index 8b94c7f7d..e69de29bb 100644 --- a/app/views/index/index.phtml +++ b/app/views/index/index.phtml @@ -1,23 +0,0 @@ -<?php - -$output = Minz_Request::param('output', 'normal'); - -if (FreshRSS_Auth::hasAccess() || FreshRSS_Context::$system_conf->allow_anonymous) { - if ($output === 'normal') { - $this->renderHelper('view/normal_view'); - } elseif ($output === 'reader') { - $this->renderHelper('view/reader_view'); - } elseif ($output === 'rss') { - $this->renderHelper('view/rss_view'); - } else { - Minz_Request::_param('output', 'normal'); - $output = 'normal'; - $this->renderHelper('view/normal_view'); - } -} elseif ($output === 'rss') { - // token has already been checked in the controller so we can show the view - $this->renderHelper('view/rss_view'); -} else { - // Normally, it should not happen, but log it anyway - Minz_Log::error('Something is wrong in ' . __FILE__ . ' line ' . __LINE__); -} diff --git a/app/views/index/normal.phtml b/app/views/index/normal.phtml index 8c6ddd1ee..f71abf158 100644 --- a/app/views/index/normal.phtml +++ b/app/views/index/normal.phtml @@ -7,24 +7,8 @@ if (!empty($this->entries)) { $display_today = true; $display_yesterday = true; $display_others = true; - if (FreshRSS_Auth::hasAccess()) { - $sharing = FreshRSS_Context::$user_conf->sharing; - } else { - $sharing = array(); - } $hidePosts = !FreshRSS_Context::$user_conf->display_posts; $lazyload = FreshRSS_Context::$user_conf->lazyload; - $topline_read = FreshRSS_Context::$user_conf->topline_read; - $topline_favorite = FreshRSS_Context::$user_conf->topline_favorite; - $topline_date = FreshRSS_Context::$user_conf->topline_date; - $topline_link = FreshRSS_Context::$user_conf->topline_link; - $bottomline_read = FreshRSS_Context::$user_conf->bottomline_read; - $bottomline_favorite = FreshRSS_Context::$user_conf->bottomline_favorite; - $bottomline_sharing = FreshRSS_Context::$user_conf->bottomline_sharing && (count($sharing)); - $bottomline_tags = FreshRSS_Context::$user_conf->bottomline_tags; - $bottomline_date = FreshRSS_Context::$user_conf->bottomline_date; - $bottomline_link = FreshRSS_Context::$user_conf->bottomline_link; - $content_width = FreshRSS_Context::$user_conf->content_width; $today = @strtotime('today'); @@ -35,7 +19,21 @@ if (!empty($this->entries)) { <a href="<?php echo Minz_Url::display(Minz_Request::currentRequest()); ?>"><?php echo _t('gen.js.new_article'); /* TODO: move string in JS*/ ?></a> </div><?php foreach ($this->entries as $item) { - if ($display_today && $item->isDay(FreshRSS_Days::TODAY, $today)) { + $this->entry = Minz_ExtensionManager::callHook('entry_before_display', $item); + if (is_null($this->entry)) { + continue; + } + + // We most likely already have the feed object in cache + $this->feed = FreshRSS_CategoryDAO::findFeed($this->categories, $this->entry->feed()); + if ($this->feed == null) { + $this->feed = $this->entry->feed(true); + if ($this->feed == null) { + $this->feed = FreshRSS_Feed::example(); + } + } + + if ($display_today && $this->entry->isDay(FreshRSS_Days::TODAY, $today)) { ?><div class="day" id="day_today"><?php echo _t('gen.date.today'); ?><span class="date"> — <?php echo timestamptodate(time(), false); ?></span><?php @@ -43,7 +41,7 @@ if (!empty($this->entries)) { ?></div><?php $display_today = false; } - if ($display_yesterday && $item->isDay(FreshRSS_Days::YESTERDAY, $today)) { + if ($display_yesterday && $this->entry->isDay(FreshRSS_Days::YESTERDAY, $today)) { ?><div class="day" id="day_yesterday"><?php echo _t('gen.date.yesterday'); ?><span class="date"> — <?php echo timestamptodate(time() - 86400, false); ?></span><?php @@ -51,139 +49,35 @@ if (!empty($this->entries)) { ?></div><?php $display_yesterday = false; } - if ($display_others && $item->isDay(FreshRSS_Days::BEFORE_YESTERDAY, $today)) { + if ($display_others && $this->entry->isDay(FreshRSS_Days::BEFORE_YESTERDAY, $today)) { ?><div class="day" id="day_before_yesterday"><?php echo _t('gen.date.before_yesterday'); ?><span class="name"><?php echo FreshRSS_Context::$name; ?></span><?php ?></div><?php $display_others = false; } - ?><div class="flux<?php echo !$item->isRead() ? ' not_read' : ''; ?><?php echo $item->isFavorite() ? ' favorite' : ''; ?>" id="flux_<?php echo $item->id(); ?>"> - <ul class="horizontal-list flux_header"><?php - if (FreshRSS_Auth::hasAccess()) { - if ($topline_read) { - ?><li class="item manage"><?php - $arUrl = array('c' => 'entry', 'a' => 'read', 'params' => array('id' => $item->id())); - if ($item->isRead()) { - $arUrl['params']['is_read'] = 0; - } - ?><a class="read" href="<?php echo Minz_Url::display($arUrl); ?>"><?php - echo _i($item->isRead() ? 'read' : 'unread'); ?></a><?php - ?></li><?php - } - if ($topline_favorite) { - ?><li class="item manage"><?php - $arUrl = array('c' => 'entry', 'a' => 'bookmark', 'params' => array('id' => $item->id())); - if ($item->isFavorite()) { - $arUrl['params']['is_favorite'] = 0; - } - ?><a class="bookmark" href="<?php echo Minz_Url::display($arUrl); ?>"><?php - echo _i($item->isFavorite() ? 'starred' : 'non-starred'); ?></a><?php - ?></li><?php - } - } - $feed = FreshRSS_CategoryDAO::findFeed($this->categories, $item->feed()); //We most likely already have the feed object in cache - if ($feed == null) { - $feed = $item->feed(true); - if ($feed == null) { - $feed = FreshRSS_Feed::example(); - } - } - ?><li class="item website"><a href="<?php echo _url('index', 'index', 'get', 'f_' . $feed->id()); ?>"><img class="favicon" src="<?php echo $feed->favicon(); ?>" alt="✇" /> <span><?php echo $feed->name(); ?></span></a></li> - <li class="item title"><a target="_blank" href="<?php echo $item->link(); ?>"><?php echo $item->title(); ?></a></li> - <?php if ($topline_date) { ?><li class="item date"><?php echo $item->date(); ?> </li><?php } ?> - <?php if ($topline_link) { ?><li class="item link"><a target="_blank" href="<?php echo $item->link(); ?>"><?php echo _i('link'); ?></a></li><?php } ?> - </ul> + ?><div class="flux<?php echo !$this->entry->isRead() ? ' not_read' : ''; ?><?php echo $this->entry->isFavorite() ? ' favorite' : ''; ?>" id="flux_<?php echo $this->entry->id(); ?>"><?php - <div class="flux_content"> + $this->renderHelper('index/normal/entry_header'); + + ?><div class="flux_content"> <div class="content <?php echo $content_width; ?>"> - <h1 class="title"><a target="_blank" href="<?php echo $item->link(); ?>"><?php echo $item->title(); ?></a></h1> + <h1 class="title"><a target="_blank" href="<?php echo $this->entry->link(); ?>"><?php echo $this->entry->title(); ?></a></h1> <?php - $author = $item->author(); + $author = $this->entry->author(); echo $author != '' ? '<div class="author">' . _t('gen.short.by_author', $author) . '</div>' : '', - $lazyload && $hidePosts ? lazyimg($item->content()) : $item->content(); + $lazyload && $hidePosts ? lazyimg($this->entry->content()) : $this->entry->content(); ?> - </div> - <ul class="horizontal-list bottom"><?php - if (FreshRSS_Auth::hasAccess()) { - if ($bottomline_read) { - ?><li class="item manage"><?php - $arUrl = array('c' => 'entry', 'a' => 'read', 'params' => array('id' => $item->id())); - if ($item->isRead()) { - $arUrl['params']['is_read'] = 0; - } - ?><a class="read" href="<?php echo Minz_Url::display($arUrl); ?>"><?php - echo _i($item->isRead() ? 'read' : 'unread'); ?></a><?php - ?></li><?php - } - if ($bottomline_favorite) { - ?><li class="item manage"><?php - $arUrl = array('c' => 'entry', 'a' => 'bookmark', 'params' => array('id' => $item->id())); - if ($item->isFavorite()) { - $arUrl['params']['is_favorite'] = 0; - } - ?><a class="bookmark" href="<?php echo Minz_Url::display($arUrl); ?>"><?php - echo _i($item->isFavorite() ? 'starred' : 'non-starred'); ?></a><?php - ?></li><?php - } - } ?> - <li class="item"><?php - if ($bottomline_sharing) { - ?><div class="dropdown"> - <div id="dropdown-share-<?php echo $item->id();?>" class="dropdown-target"></div> - <a class="dropdown-toggle" href="#dropdown-share-<?php echo $item->id();?>"> - <?php echo _i('share'); ?> - <?php echo _t('index.share'); ?> - </a> + </div><?php - <ul class="dropdown-menu"> - <li class="dropdown-close"><a href="#close">❌</a></li><?php - $link = $item->link(); - $title = $item->title() . ' · ' . $feed->name(); - foreach (FreshRSS_Context::$user_conf->sharing as $share_options) { - $share = FreshRSS_Share::get($share_options['type']); - $share_options['link'] = $link; - $share_options['title'] = $title; - $share->update($share_options); - ?><li class="item share"> - <a target="_blank" href="<?php echo $share->url(); ?>"><?php echo $share->name(); ?></a> - </li><?php - } - ?></ul> - </div> - <?php } ?> - </li><?php - $tags = $bottomline_tags ? $item->tags() : null; - if (!empty($tags)) { - ?><li class="item"> - <div class="dropdown"> - <div id="dropdown-tags-<?php echo $item->id();?>" class="dropdown-target"></div> - <?php echo _i('tag'); ?> - <a class="dropdown-toggle" href="#dropdown-tags-<?php echo $item->id();?>"><?php - echo _t('index.tag.related'); - ?></a> - <ul class="dropdown-menu"> - <li class="dropdown-close"><a href="#close">❌</a></li><?php - foreach($tags as $tag) { - ?><li class="item"><a href="<?php echo _url('index', 'index', 'search', urlencode('#' . $tag)); ?>"><?php echo $tag; ?></a></li><?php - } ?> - </ul> - </div> - </li><?php - } - if ($bottomline_date) { - ?><li class="item date"><?php echo $item->date(); ?></li><?php - } - if ($bottomline_link) { - ?><li class="item link"><a target="_blank" href="<?php echo $item->link(); ?>"><?php echo _i('link'); ?></a></li><?php - } ?> - </ul> - </div> - </div> - <?php } ?> + $this->renderHelper('index/normal/entry_bottom'); - <?php $this->renderHelper('pagination'); ?> -</div> + ?></div> + </div><?php + } + + $this->renderHelper('pagination'); +?></div> <?php $this->partial('nav_entries'); ?> diff --git a/app/views/index/reader.phtml b/app/views/index/reader.phtml index 017297710..a19ee322e 100644 --- a/app/views/index/reader.phtml +++ b/app/views/index/reader.phtml @@ -6,10 +6,13 @@ if (!empty($this->entries)) { $content_width = FreshRSS_Context::$user_conf->content_width; ?> -<div id="stream" class="reader"> - <?php foreach ($this->entries as $item) { ?> - - <div class="flux<?php echo !$item->isRead() ? ' not_read' : ''; ?><?php echo $item->isFavorite() ? ' favorite' : ''; ?>" id="flux_<?php echo $item->id(); ?>"> +<div id="stream" class="reader"><?php + foreach ($this->entries as $item) { + $item = Minz_ExtensionManager::callHook('entry_before_display', $item); + if (is_null($item)) { + continue; + } + ?><div class="flux<?php echo !$item->isRead() ? ' not_read' : ''; ?><?php echo $item->isFavorite() ? ' favorite' : ''; ?>" id="flux_<?php echo $item->id(); ?>"> <div class="flux_content"> <div class="content <?php echo $content_width; ?>"> <?php diff --git a/app/views/user/manage.phtml b/app/views/user/manage.phtml index 11c42a857..466446f2f 100644 --- a/app/views/user/manage.phtml +++ b/app/views/user/manage.phtml @@ -11,8 +11,8 @@ <div class="group-controls"> <select name="new_user_language" id="new_user_language"> <?php $languages = Minz_Translate::availableLanguages(); ?> - <?php foreach ($languages as $short => $lib) { ?> - <option value="<?php echo $short; ?>"<?php echo FreshRSS_Context::$user_conf->language === $short ? ' selected="selected"' : ''; ?>><?php echo $lib; ?></option> + <?php foreach ($languages as $lang) { ?> + <option value="<?php echo $lang; ?>"<?php echo FreshRSS_Context::$user_conf->language === $lang ? ' selected="selected"' : ''; ?>><?php echo _t('gen.lang.' . $lang); ?></option> <?php } ?> </select> </div> diff --git a/constants.php b/constants.php index 07dd6f18e..8136afc85 100644 --- a/constants.php +++ b/constants.php @@ -20,6 +20,7 @@ define('FRESHRSS_PATH', dirname(__FILE__)); define('CACHE_PATH', DATA_PATH . '/cache'); define('LIB_PATH', FRESHRSS_PATH . '/lib'); - define('APP_PATH', FRESHRSS_PATH . '/app'); + define('APP_PATH', FRESHRSS_PATH . '/app'); + define('EXTENSIONS_PATH', FRESHRSS_PATH . '/extensions'); define('TMP_PATH', sys_get_temp_dir()); diff --git a/data/config.default.php b/data/config.default.php index 193ac5726..97df3a299 100644 --- a/data/config.default.php +++ b/data/config.default.php @@ -27,4 +27,5 @@ return array( 'base' => '', 'prefix' => '', ), + 'extensions_enabled' => array(), ); diff --git a/data/users/_/config.default.php b/data/users/_/config.default.php index 56d54b293..6d3f73a13 100644 --- a/data/users/_/config.default.php +++ b/data/users/_/config.default.php @@ -63,4 +63,5 @@ return array ( 'queries' => array ( ), 'html5_notif_timeout' => 0, + 'extensions_enabled' => array(), ); diff --git a/extensions/.gitignore b/extensions/.gitignore index d93e5e396..cd5592022 100644 --- a/extensions/.gitignore +++ b/extensions/.gitignore @@ -1 +1 @@ -/[xX] +[xX]* diff --git a/extensions/Read-me.txt b/extensions/README.md index e7b66d5bc..e7b66d5bc 100644 --- a/extensions/Read-me.txt +++ b/extensions/README.md diff --git a/lib/Minz/Configuration.php b/lib/Minz/Configuration.php index 6044fc269..fe415e22b 100644 --- a/lib/Minz/Configuration.php +++ b/lib/Minz/Configuration.php @@ -101,6 +101,19 @@ class Minz_Configuration { */ private $configuration_setter = null; + public function removeExtension($ext_name) { + self::$extensions_enabled = array_diff( + self::$extensions_enabled, + array($ext_name) + ); + } + public function addExtension($ext_name) { + $found = array_search($ext_name, self::$extensions_enabled) !== false; + if (!$found) { + self::$extensions_enabled[] = $ext_name; + } + } + /** * Create a new Minz_Configuration object. * 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/Extension.php b/lib/Minz/Extension.php new file mode 100644 index 000000000..b3991c129 --- /dev/null +++ b/lib/Minz/Extension.php @@ -0,0 +1,198 @@ +<?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. + */ + public function install() {} + + /** + * Used when uninstalling an extension (e.g. revert the database scheme to + * cancel changes from install). + * + * It must be redefined by child classes. + */ + public function uninstall() {} + + /** + * 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 is_enabled() { + 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 = end(explode('/', $this->path)); + $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); + } + + /** + * 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..5880e80f6 --- /dev/null +++ b/lib/Minz/ExtensionManager.php @@ -0,0 +1,249 @@ +<?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 + 'entry_before_insert' => array(), // function($entry) -> Entry | null + 'feed_before_insert' => array(), // function($feed) -> Feed | null + ); + 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; + $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::is_valid_metadata($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 is_valid_metadata($meta) { + return !(empty($meta['name']) || + empty($meta['entrypoint']) || + !ctype_alnum($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($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 enable_by_list($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 list_extensions($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 find_extension($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][] = $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 comments) + * @todo hook functions will have different signatures. So the $res = func($args); + * $args = $res; will not work for all of them in the future. We must + * find a better way to call hooks. + */ + public static function callHook($hook_name) { + $args = func_get_args(); + unset($args[0]); + + $result = $args[1]; + foreach (self::$hook_list[$hook_name] as $function) { + $result = call_user_func_array($function, $args); + + if (is_null($result)) { + break; + } + + $args = $result; + } + return $result; + } +} diff --git a/lib/Minz/Translate.php b/lib/Minz/Translate.php index 1b4102ca9..baddcb424 100644 --- a/lib/Minz/Translate.php +++ b/lib/Minz/Translate.php @@ -10,9 +10,9 @@ */ class Minz_Translate { /** - * $lang_list is the list of available languages. + * $path_list is the list of registered base path to search translations. */ - private static $lang_list = array(); + private static $path_list = array(); /** * $lang_name is the name of the current language to use. @@ -20,9 +20,9 @@ class Minz_Translate { private static $lang_name; /** - * $lang_path is the pathname of i18n files (e.g. ./app/i18n/en/). + * $lang_files is a list of registered i18n files. */ - private static $lang_path; + private static $lang_files = array(); /** * $translates is a cache for i18n translation. @@ -31,13 +31,16 @@ class Minz_Translate { /** * Init the translation object. - * @param $lang_list the list of available languages. * @param $lang_name the lang to show. */ - public static function init($lang_list, $lang_name) { - self::$lang_list = $lang_list; + public static function init($lang_name = null) { self::$lang_name = $lang_name; - self::$lang_path = APP_PATH . '/i18n/' . self::$lang_name . '/'; + self::$lang_files = array(); + self::$translates = array(); + self::registerPath(APP_PATH . '/i18n'); + foreach (self::$path_list as $path) { + self::loadLang($path); + } } /** @@ -45,15 +48,103 @@ class Minz_Translate { * @param $lang_name the new language to use */ public static function reset($lang_name) { - self::init(self::$lang_list, $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. + * @return an array containing langs found in different registered paths. */ public static function availableLanguages() { - return self::$lang_list; + $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); + } + + /** + * Load translations of the current language from the given path. + * @param $path the path containing i18n directories. + */ + 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; + } + } + + /** + * Load the files associated to $key into $translates. + * @param $key the top level i18n key we want to load. + */ + 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; } /** @@ -73,16 +164,14 @@ class Minz_Translate { $top_level = array_shift($group); } - $filename = self::$lang_path . $top_level . '.php'; - - // Try to load the i18n file if it's not done yet. - if (!isset(self::$translates[$top_level])) { - if (!file_exists($filename)) { - Minz_Log::debug($top_level . ' is not a valid top level key'); + // 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; } - - self::$translates[$top_level] = include($filename); } // Go through the i18n keys to get the correct translation value. diff --git a/lib/Minz/View.php b/lib/Minz/View.php index 481b0376d..ff5cce4a5 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 (); @@ -37,19 +38,26 @@ class Minz_View { * 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 { @@ -58,21 +66,39 @@ 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 - ); + $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 . '`'); } } @@ -82,11 +108,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 . '`'); } } @@ -96,11 +119,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/lib_rss.php b/lib/lib_rss.php index ffd56eae4..083e87745 100644 --- a/lib/lib_rss.php +++ b/lib/lib_rss.php @@ -375,6 +375,7 @@ function recursive_unlink($dir) { if (!is_dir($dir)) { return true; } + $files = array_diff(scandir($dir), array('.', '..')); foreach ($files as $filename) { $filename = $dir . '/' . $filename; @@ -385,6 +386,7 @@ function recursive_unlink($dir) { unlink($filename); } } + return rmdir($dir); } @@ -404,3 +406,26 @@ function remove_query_by_get($get, $queries) { } 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)); +} @@ -1,32 +1,65 @@ <?php -if (!isset($_GET['e'])) { +if (!isset($_GET['f']) || + !isset($_GET['t'])) { header('HTTP/1.1 400 Bad Request'); die(); } -$extension = substr($_GET['e'], 0, 64); -if (!ctype_alpha($extension)) { + +require('../constants.php'); + +/** + * Check if a file can be served by ext.php. A valid file is under a + * EXTENSIONS_PATH/extension_name/static/ directory. + * + * You should sanitize path by using the realpath() function. + * + * @param $path the path to the file we want to serve. + * @return true if it can be served, false else. + * + */ +function is_valid_path($path) { + // It must be under the extension path. + $in_ext_path = (substr($path, 0, strlen(EXTENSIONS_PATH)) === EXTENSIONS_PATH); + if (!$in_ext_path) { + return false; + } + + // File to serve must be under a `ext_dir/static/` directory. + $path_relative_to_ext = substr($path, strlen(EXTENSIONS_PATH) + 1); + $path_splitted = explode('/', $path_relative_to_ext); + if (count($path_splitted) < 3 || $path_splitted[1] !== 'static') { + return false; + } + + return true; +} + +$file_name = urldecode($_GET['f']); +$file_type = $_GET['t']; + +$absolute_filename = realpath(EXTENSIONS_PATH . '/' . $file_name); + +if (!is_valid_path($absolute_filename)) { header('HTTP/1.1 400 Bad Request'); die(); } -require('../constants.php'); -$filename = FRESHRSS_PATH . '/extensions/' . $extension . '/'; - -if (isset($_GET['j'])) { - header('Content-Type: application/javascript; charset=UTF-8'); - header('Content-Disposition: inline; filename="script.js"'); - $filename .= 'script.js'; -} elseif (isset($_GET['c'])) { +switch ($file_type) { +case 'css': header('Content-Type: text/css; charset=UTF-8'); - header('Content-Disposition: inline; filename="style.css"'); - $filename .= 'style.css'; -} else { + header('Content-Disposition: inline; filename="' . $file_name . '"'); + break; +case 'js': + header('Content-Type: application/javascript; charset=UTF-8'); + header('Content-Disposition: inline; filename="' . $file_name . '"'); + break; +default: header('HTTP/1.1 400 Bad Request'); die(); } -$mtime = @filemtime($filename); -if ($mtime == false) { +$mtime = @filemtime($absolute_filename); +if ($mtime === false) { header('HTTP/1.1 404 Not Found'); die(); } @@ -34,5 +67,5 @@ if ($mtime == false) { require(LIB_PATH . '/http-conditional.php'); if (!httpConditional($mtime, 604800, 2)) { - readfile($filename); + readfile($absolute_filename); } |
