From 86f69ca396572ca4d7668a33e84cb4f3b523fc4e Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Thu, 4 Dec 2014 19:33:29 +0100 Subject: First draft for the new extension feature - Only system extensions can be loaded for the moment by adding them in the config.php file. - Remove previous system (it will be added properly in the new system in the next step). --- app/FreshRSS.php | 42 +++++------ constants.php | 3 +- extensions/README.md | 15 ++++ extensions/Read-me.txt | 15 ---- lib/Minz/Configuration.php | 10 +++ lib/Minz/Extension.php | 96 +++++++++++++++++++++++++ lib/Minz/ExtensionException.php | 15 ++++ lib/Minz/ExtensionManager.php | 150 ++++++++++++++++++++++++++++++++++++++++ 8 files changed, 310 insertions(+), 36 deletions(-) create mode 100644 extensions/README.md delete mode 100644 extensions/Read-me.txt create mode 100644 lib/Minz/Extension.php create mode 100644 lib/Minz/ExtensionException.php create mode 100644 lib/Minz/ExtensionManager.php diff --git a/app/FreshRSS.php b/app/FreshRSS.php index 6114a5d1a..2db811a71 100644 --- a/app/FreshRSS.php +++ b/app/FreshRSS.php @@ -6,6 +6,9 @@ class FreshRSS extends Minz_FrontController { Minz_Session::init('FreshRSS'); } + // Load list of extensions and initialize the "system" ones. + Minz_ExtensionManager::init(); + // Need to be called just after session init because it initializes // current user. FreshRSS_Auth::init(); @@ -32,7 +35,6 @@ class FreshRSS extends Minz_FrontController { $this->loadStylesAndScripts(); $this->loadNotifications(); - $this->loadExtensions(); } private function loadStylesAndScripts() { @@ -74,23 +76,23 @@ class FreshRSS extends Minz_FrontController { } } - 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 - } - } - } - } + // 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/constants.php b/constants.php index f66a012b0..999e703ba 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/extensions/README.md b/extensions/README.md new file mode 100644 index 000000000..e7b66d5bc --- /dev/null +++ b/extensions/README.md @@ -0,0 +1,15 @@ +== FreshRSS extensions == + +You may place in this directory some custom extensions for FreshRSS. + +The structure must be: + +./FreshRSS/extensions/ + ./NameOfExtensionAlphanumeric/ + ./style.css + ./script.js + ./module.php + +Each file is optional. + +The name of non-official extensions should start by an 'x'. diff --git a/extensions/Read-me.txt b/extensions/Read-me.txt deleted file mode 100644 index e7b66d5bc..000000000 --- a/extensions/Read-me.txt +++ /dev/null @@ -1,15 +0,0 @@ -== FreshRSS extensions == - -You may place in this directory some custom extensions for FreshRSS. - -The structure must be: - -./FreshRSS/extensions/ - ./NameOfExtensionAlphanumeric/ - ./style.css - ./script.js - ./module.php - -Each file is optional. - -The name of non-official extensions should start by an 'x'. diff --git a/lib/Minz/Configuration.php b/lib/Minz/Configuration.php index 6cbc9fc0b..4d3ab0964 100644 --- a/lib/Minz/Configuration.php +++ b/lib/Minz/Configuration.php @@ -69,6 +69,8 @@ class Minz_Configuration { 'max_categories' => Minz_Configuration::MAX_SMALL_INT, ); + private static $extensions_enabled = array(); + /* * Getteurs */ @@ -133,6 +135,9 @@ class Minz_Configuration { public static function unsafeAutologinEnabled() { return self::$unsafe_autologin_enabled; } + public static function extensionsEnabled() { + return self::$extensions_enabled; + } public static function _allowAnonymous($allow = false) { self::$allow_anonymous = ((bool)$allow) && self::canLogIn(); @@ -338,6 +343,11 @@ class Minz_Configuration { } } + // Extensions + if (isset($ini_array['extensions']) && is_array($ini_array['extensions'])) { + self::$extensions_enabled = $ini_array['extensions']; + } + // Base de données if (isset ($ini_array['db'])) { $db = $ini_array['db']; diff --git a/lib/Minz/Extension.php b/lib/Minz/Extension.php new file mode 100644 index 000000000..f442344a3 --- /dev/null +++ b/lib/Minz/Extension.php @@ -0,0 +1,96 @@ +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'); + } + + /** + * 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() {} + + /** + * Getters and setters. + */ + public function getName() { + return $this->name; + } + public function getEntrypoint() { + return $this->entrypoint; + } + 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; + } +} 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 @@ +Extension where + * 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('..', '.') + )); + + self::$ext_auto_enabled = Minz_Configuration::extensionsEnabled(); + + 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()); + } + } + + /** + * 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->init(); + } + } +} -- cgit v1.2.3 From 1086ba4a2bbe43a0101105624f831516b59ba9e9 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Thu, 4 Dec 2014 19:47:43 +0100 Subject: Enable extensions for users --- app/FreshRSS.php | 6 +++++- app/Models/Configuration.php | 8 ++++++++ lib/Minz/ExtensionManager.php | 11 +++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/app/FreshRSS.php b/app/FreshRSS.php index 2db811a71..166ee1709 100644 --- a/app/FreshRSS.php +++ b/app/FreshRSS.php @@ -6,7 +6,7 @@ class FreshRSS extends Minz_FrontController { Minz_Session::init('FreshRSS'); } - // Load list of extensions and initialize the "system" ones. + // Load list of extensions and enable the "system" ones. Minz_ExtensionManager::init(); // Need to be called just after session init because it initializes @@ -29,6 +29,10 @@ class FreshRSS extends Minz_FrontController { // Load context and configuration. FreshRSS_Context::init(); + // Enable extensions for the current user. + $ext_list = FreshRSS_Context::$conf->extensions_enabled; + Minz_ExtensionManager::enable_by_list($ext_list); + // Init i18n. Minz_Session::_param('language', FreshRSS_Context::$conf->language); Minz_Translate::init(); diff --git a/app/Models/Configuration.php b/app/Models/Configuration.php index 8668470b0..13ce43990 100644 --- a/app/Models/Configuration.php +++ b/app/Models/Configuration.php @@ -64,6 +64,7 @@ class FreshRSS_Configuration { 'sharing' => array(), 'queries' => array(), 'html5_notif_timeout' => 0, + 'extensions_enabled' => array(), ); private $available_languages = array( @@ -342,4 +343,11 @@ class FreshRSS_Configuration { public function _bottomline_link($value) { $this->data['bottomline_link'] = ((bool)$value) && $value !== 'no'; } + + public function _extensions_enabled($value) { + if (!is_array($value)) { + $value = array($value); + } + $this->data['extensions_enabled'] = $value; + } } diff --git a/lib/Minz/ExtensionManager.php b/lib/Minz/ExtensionManager.php index ae648fe0d..789557b9e 100644 --- a/lib/Minz/ExtensionManager.php +++ b/lib/Minz/ExtensionManager.php @@ -147,4 +147,15 @@ class Minz_ExtensionManager { $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); + } + } } -- cgit v1.2.3 From 0316badf649ef285f068847ef094ace80dd51290 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Thu, 4 Dec 2014 19:52:07 +0100 Subject: Update gitignore for extensions --- extensions/.gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/.gitignore b/extensions/.gitignore index d93e5e396..cd5592022 100644 --- a/extensions/.gitignore +++ b/extensions/.gitignore @@ -1 +1 @@ -/[xX] +[xX]* -- cgit v1.2.3 From f9b037742a0aeb49cab86782d1a59913c2de47bf Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Thu, 4 Dec 2014 20:41:01 +0100 Subject: Update ext.php to serve any file from extensions Add an extension->getFileUrl() method to facilitate url generation --- lib/Minz/Extension.php | 23 +++++++++++++++++++++++ p/ext.php | 37 +++++++++++++++++++------------------ 2 files changed, 42 insertions(+), 18 deletions(-) diff --git a/lib/Minz/Extension.php b/lib/Minz/Extension.php index f442344a3..72a375a6d 100644 --- a/lib/Minz/Extension.php +++ b/lib/Minz/Extension.php @@ -75,6 +75,9 @@ class Minz_Extension { public function getEntrypoint() { return $this->entrypoint; } + public function getPath() { + return $this->path; + } public function getAuthor() { return $this->author; } @@ -93,4 +96,24 @@ class Minz_Extension { } $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 . '/' . $filename); + + $absolute_path = $this->path . '/' . $filename; + $mtime = @filemtime($absolute_path); + + $url = '/ext.php?f=' . $file_name_url . + '&t=' . $type . + '&' . $mtime; + return Minz_Url::display($url); + } } diff --git a/p/ext.php b/p/ext.php index a1dde2f93..39c224a84 100644 --- a/p/ext.php +++ b/p/ext.php @@ -1,32 +1,33 @@ Date: Thu, 4 Dec 2014 20:43:05 +0100 Subject: Remove old code for extensions --- app/FreshRSS.php | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/app/FreshRSS.php b/app/FreshRSS.php index 166ee1709..dc7d0b375 100644 --- a/app/FreshRSS.php +++ b/app/FreshRSS.php @@ -79,24 +79,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 - // } - // } - // } - // } } -- cgit v1.2.3 From a2da70fd119cc43438f8dd88de54a7d19fafbe1a Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Fri, 5 Dec 2014 10:51:34 +0100 Subject: Fix security hole from ext.php script. Now, ext.php can only serve file under a EXTENSIONS_PATH/ext_dir/static/ directory. A 400 Bad Request error will be returned for other files. See https://github.com/FreshRSS/FreshRSS/issues/252 And https://github.com/FreshRSS/FreshRSS/commit/f9b037742a0aeb49cab86782d1a59913c2de47b --- lib/Minz/Extension.php | 4 ++-- p/ext.php | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/lib/Minz/Extension.php b/lib/Minz/Extension.php index 72a375a6d..ecf510ea2 100644 --- a/lib/Minz/Extension.php +++ b/lib/Minz/Extension.php @@ -106,9 +106,9 @@ class Minz_Extension { */ public function getFileUrl($filename, $type) { $dir = end(explode('/', $this->path)); - $file_name_url = urlencode($dir . '/' . $filename); + $file_name_url = urlencode($dir . '/static/' . $filename); - $absolute_path = $this->path . '/' . $filename; + $absolute_path = $this->path . '/static/' . $filename; $mtime = @filemtime($absolute_path); $url = '/ext.php?f=' . $file_name_url . diff --git a/p/ext.php b/p/ext.php index 39c224a84..5c9f9125f 100644 --- a/p/ext.php +++ b/p/ext.php @@ -7,10 +7,42 @@ if (!isset($_GET['f']) || 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 = EXTENSIONS_PATH . '/' . $file_name; +$absolute_filename = realpath(EXTENSIONS_PATH . '/' . $file_name); + +if (!is_valid_path($absolute_filename)) { + header('HTTP/1.1 400 Bad Request'); + die(); +} switch ($file_type) { case 'css': -- cgit v1.2.3 From 9fc60317eecba785b66011f04b9a5150296f2df6 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Fri, 5 Dec 2014 14:17:02 +0100 Subject: First draft for listing and manipulate extensions See https://github.com/FreshRSS/FreshRSS/issues/252 --- app/Controllers/extensionController.php | 43 +++++++++++++++++++++++++++++++++ app/layout/aside_configure.phtml | 4 +++ app/layout/header.phtml | 1 + app/views/extension/index.phtml | 35 +++++++++++++++++++++++++++ lib/Minz/Extension.php | 20 +++++++++++++++ lib/Minz/ExtensionManager.php | 17 +++++++++++++ 6 files changed, 120 insertions(+) create mode 100644 app/Controllers/extensionController.php create mode 100644 app/views/extension/index.phtml diff --git a/app/Controllers/extensionController.php b/app/Controllers/extensionController.php new file mode 100644 index 000000000..504be56d3 --- /dev/null +++ b/app/Controllers/extensionController.php @@ -0,0 +1,43 @@ +view->extension_list = Minz_ExtensionManager::list_extensions(); + } + + public function configureAction() { + if (Minz_Request::param('ajax')) { + $this->view->_useLayout(false); + } + } + + public function enableAction() { + + } + + public function disableAction() { + + } + + public function removeAction() { + + } +} diff --git a/app/layout/aside_configure.phtml b/app/layout/aside_configure.phtml index 53c52d3e3..f7f3617c4 100644 --- a/app/layout/aside_configure.phtml +++ b/app/layout/aside_configure.phtml @@ -22,6 +22,10 @@ Minz_Request::actionName() === 'profile'? ' active' : ''; ?>"> +
  • + +
  • +
  • diff --git a/app/views/extension/index.phtml b/app/views/extension/index.phtml new file mode 100644 index 000000000..c6b7c84a1 --- /dev/null +++ b/app/views/extension/index.phtml @@ -0,0 +1,35 @@ +partial('aside_configure'); ?> + +
    + + +

    + + extension_list)) { ?> + + extension_list as $ext) { ?> +
      +
    • + getName()); ?> +
      + + is_enabled()) { ?> + + + + + + + +
      +
    • +
    • getName(); ?>
    • +
    + + +

    + +
    + + +
    diff --git a/lib/Minz/Extension.php b/lib/Minz/Extension.php index ecf510ea2..a1fdd659b 100644 --- a/lib/Minz/Extension.php +++ b/lib/Minz/Extension.php @@ -17,6 +17,8 @@ class Minz_Extension { 'user', ); + private $is_enabled; + /** * The constructor to assign specific information to the extension. * @@ -41,6 +43,8 @@ class Minz_Extension { $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; } /** @@ -66,6 +70,22 @@ class Minz_Extension { */ 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; + } + /** * Getters and setters. */ diff --git a/lib/Minz/ExtensionManager.php b/lib/Minz/ExtensionManager.php index 789557b9e..6c32ccf19 100644 --- a/lib/Minz/ExtensionManager.php +++ b/lib/Minz/ExtensionManager.php @@ -144,6 +144,7 @@ class Minz_ExtensionManager { if (isset(self::$ext_list[$ext_name])) { $ext = self::$ext_list[$ext_name]; self::$ext_list_enabled[$ext_name] = $ext; + $ext->enable(); $ext->init(); } } @@ -158,4 +159,20 @@ class Minz_ExtensionManager { self::enable($ext_name); } } + + + + /** + * Returns 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; + } + } } -- cgit v1.2.3 From f8aa66152fcab24ae7cd9663dab0c0c96a45ca24 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Fri, 5 Dec 2014 14:48:09 +0100 Subject: Give possibility to register a new Controller. - Add a Extension->registerController(name) method - Controllers must be written in extension_dir/controllers/nameController.php - Controllers must be named as FreshExtension_name_Controller - Controllers must extend Minz_ActionController See https://github.com/FreshRSS/FreshRSS/issues/252 --- lib/Minz/Dispatcher.php | 47 ++++++++++++++++++++++++++++++++++++++++++++--- lib/Minz/Extension.php | 13 +++++++++++++ 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/lib/Minz/Dispatcher.php b/lib/Minz/Dispatcher.php index f62a92911..66789a3d3 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) { @@ -73,8 +74,11 @@ class Minz_Dispatcher { * > pas une instance de ActionController */ private function createController ($controller_name) { - $filename = APP_PATH . self::CONTROLLERS_PATH_NAME . '/' - . $controller_name . '.php'; + if (self::isRegistered($controller_name)) { + $controller_name = self::loadController($controller_name); + } else { + $controller_name = 'FreshRSS_' . $controller_name . '_Controller'; + } if (!class_exists ($controller_name)) { throw new Minz_ControllerNotExistException ( @@ -114,4 +118,41 @@ class Minz_Dispatcher { $action_name )); } + + /** + * Register a controller file. + * + * @param $base_name the base name of the controller (i.e. ./?c=) + * @param $controller_name the name of the controller (e.g. HelloWorldController). + * @param $filename the file which contains the controller. + */ + public static function registerController($base_name, $controller_name, $filename) { + if (file_exists($filename)) { + self::$registrations[$base_name] = array( + $controller_name, + $filename, + ); + } + } + + /** + * 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) and return its name. + * + * @param $base_name the base name of the controller. + */ + private static function loadController($base_name) { + list($controller_name, $filename) = self::$registrations[$base_name]; + include($filename); + return $controller_name; + } } diff --git a/lib/Minz/Extension.php b/lib/Minz/Extension.php index a1fdd659b..ad3465640 100644 --- a/lib/Minz/Extension.php +++ b/lib/Minz/Extension.php @@ -136,4 +136,17 @@ class Minz_Extension { '&' . $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__Controller. + */ + public function registerController($base_name) { + $controller_name = 'FreshExtension_' . $base_name . '_Controller'; + $filename = $this->path . '/controllers/' . $base_name . 'Controller.php'; + + Minz_Dispatcher::registerController($base_name, $controller_name, $filename); + } } -- cgit v1.2.3 From c6a682deb94111c1e14cf10e565da3f4214f02dc Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Fri, 5 Dec 2014 15:27:56 +0100 Subject: Extensions can define new views - View base pathname is set to the extension directory - An extension can now override an existing controller / view See https://github.com/FreshRSS/FreshRSS/issues/252 --- lib/Minz/Dispatcher.php | 40 +++++++++++++++++++++++----------------- lib/Minz/Extension.php | 5 +---- lib/Minz/View.php | 10 +++++++--- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/lib/Minz/Dispatcher.php b/lib/Minz/Dispatcher.php index 66789a3d3..edd59c7cc 100644 --- a/lib/Minz/Dispatcher.php +++ b/lib/Minz/Dispatcher.php @@ -68,16 +68,17 @@ 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) { - if (self::isRegistered($controller_name)) { - $controller_name = self::loadController($controller_name); + private function createController ($base_name) { + if (self::isRegistered($base_name)) { + self::loadController($base_name); + $controller_name = 'FreshExtension_' . $base_name . '_Controller'; } else { - $controller_name = 'FreshRSS_' . $controller_name . '_Controller'; + $controller_name = 'FreshRSS_' . $base_name . '_Controller'; } if (!class_exists ($controller_name)) { @@ -94,6 +95,10 @@ class Minz_Dispatcher { Minz_Exception::ERROR ); } + + if (self::isRegistered($base_name)) { + $this->setViewPath($this->controller, $base_name); + } } /** @@ -123,15 +128,11 @@ class Minz_Dispatcher { * Register a controller file. * * @param $base_name the base name of the controller (i.e. ./?c=) - * @param $controller_name the name of the controller (e.g. HelloWorldController). - * @param $filename the file which contains the controller. + * @param $base_path the base path where we should look into to find info. */ - public static function registerController($base_name, $controller_name, $filename) { - if (file_exists($filename)) { - self::$registrations[$base_name] = array( - $controller_name, - $filename, - ); + public static function registerController($base_name, $base_path) { + if (!self::isRegistered($base_name)) { + self::$registrations[$base_name] = $base_path; } } @@ -146,13 +147,18 @@ class Minz_Dispatcher { } /** - * Load a controller file (include) and return its name. + * Load a controller file (include). * * @param $base_name the base name of the controller. */ private static function loadController($base_name) { - list($controller_name, $filename) = self::$registrations[$base_name]; - include($filename); - return $controller_name; + $base_path = self::$registrations[$base_name]; + $controller_filename = $base_path . '/controllers/' . $base_name . 'Controller.php'; + include($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 index ad3465640..5a61ba2e0 100644 --- a/lib/Minz/Extension.php +++ b/lib/Minz/Extension.php @@ -144,9 +144,6 @@ class Minz_Extension { * FreshExtension__Controller. */ public function registerController($base_name) { - $controller_name = 'FreshExtension_' . $base_name . '_Controller'; - $filename = $this->path . '/controllers/' . $base_name . 'Controller.php'; - - Minz_Dispatcher::registerController($base_name, $controller_name, $filename); + Minz_Dispatcher::registerController($base_name, $this->path); } } diff --git a/lib/Minz/View.php b/lib/Minz/View.php index b40448491..bdfbbe63c 100644 --- a/lib/Minz/View.php +++ b/lib/Minz/View.php @@ -12,6 +12,7 @@ class Minz_View { const LAYOUT_PATH_NAME = '/layout'; const LAYOUT_FILENAME = '/layout.phtml'; + private $base_pathname = APP_PATH; private $view_filename = ''; private $use_layout = null; @@ -35,12 +36,15 @@ 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'; } + public function setBasePathname($base_pathname) { + $this->base_pathname = $base_pathname; + } + /** * Construit la vue */ @@ -70,7 +74,7 @@ class Minz_View { * Affiche la Vue en elle-même */ public function render () { - if ((include($this->view_filename)) === false) { + if ((include($this->base_pathname . $this->view_filename)) === false) { Minz_Log::notice('File not found: `' . $this->view_filename . '`'); } } -- cgit v1.2.3 From a08c382e0651f22a7db06feba225f3d49289763d Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Sat, 6 Dec 2014 15:20:20 +0100 Subject: Separate views registration from controllers one. - Add an Extension->registerViews() method. - Views are first searched in extension paths, then in APP_PATH. - It gives a way to override easily existing controllers / views. - Change include into an include_once in Dispatcher for new controllers. See https://github.com/FreshRSS/FreshRSS/issues/252 --- lib/Minz/Dispatcher.php | 6 +----- lib/Minz/Extension.php | 7 +++++++ lib/Minz/View.php | 28 ++++++++++++++++++++++++---- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/lib/Minz/Dispatcher.php b/lib/Minz/Dispatcher.php index edd59c7cc..125ce5757 100644 --- a/lib/Minz/Dispatcher.php +++ b/lib/Minz/Dispatcher.php @@ -95,10 +95,6 @@ class Minz_Dispatcher { Minz_Exception::ERROR ); } - - if (self::isRegistered($base_name)) { - $this->setViewPath($this->controller, $base_name); - } } /** @@ -154,7 +150,7 @@ class Minz_Dispatcher { private static function loadController($base_name) { $base_path = self::$registrations[$base_name]; $controller_filename = $base_path . '/controllers/' . $base_name . 'Controller.php'; - include($controller_filename); + include_once $controller_filename; } private static function setViewPath($controller, $base_name) { diff --git a/lib/Minz/Extension.php b/lib/Minz/Extension.php index 5a61ba2e0..490a5c5cb 100644 --- a/lib/Minz/Extension.php +++ b/lib/Minz/Extension.php @@ -146,4 +146,11 @@ class Minz_Extension { 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); + } } diff --git a/lib/Minz/View.php b/lib/Minz/View.php index bdfbbe63c..1bc2e862d 100644 --- a/lib/Minz/View.php +++ b/lib/Minz/View.php @@ -12,10 +12,10 @@ class Minz_View { const LAYOUT_PATH_NAME = '/layout'; const LAYOUT_FILENAME = '/layout.phtml'; - private $base_pathname = APP_PATH; private $view_filename = ''; private $use_layout = null; + private static $base_pathnames = array(APP_PATH); private static $title = ''; private static $styles = array (); private static $scripts = array (); @@ -41,8 +41,15 @@ class Minz_View { . $action_name . '.phtml'; } - public function setBasePathname($base_pathname) { - $this->base_pathname = $base_pathname; + /** + * 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); } /** @@ -74,7 +81,20 @@ class Minz_View { * Affiche la Vue en elle-même */ public function render () { - if ((include($this->base_pathname . $this->view_filename)) === false) { + $view_found = false; + + // We search the view in the list of base pathnames. Only the first view + // found is considered. + foreach (self::$base_pathnames as $base) { + $filename = $base . $this->view_filename; + if (file_exists($filename)) { + include $filename; + $view_found = true; + break; + } + } + + if (!$view_found) { Minz_Log::notice('File not found: `' . $this->view_filename . '`'); } } -- cgit v1.2.3 From 2e4682ebd451f8dd291e11141553add9164cbbef Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Sat, 6 Dec 2014 16:17:11 +0100 Subject: Add enable / disable extension features See https://github.com/FreshRSS/FreshRSS/issues/252 --- app/Controllers/extensionController.php | 72 ++++++++++++++++++++++++++++++++- app/Models/Configuration.php | 12 ++++++ lib/Minz/ExtensionManager.php | 18 +++++++-- 3 files changed, 97 insertions(+), 5 deletions(-) diff --git a/app/Controllers/extensionController.php b/app/Controllers/extensionController.php index 504be56d3..415f489a6 100644 --- a/app/Controllers/extensionController.php +++ b/app/Controllers/extensionController.php @@ -29,12 +29,80 @@ class FreshRSS_extension_Controller extends Minz_ActionController { } } + /** + * This action enables a disabled extension for the current user. + * + * System extensions can only be enabled by an administrator. + * + * 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('feedback.extension.not_found', $url_redirect); + } + + if ($ext->is_enabled()) { + Minz_Request::bad('feedback.extension.already_enabled', $url_redirect); + } + + if ($ext->getType() === 'system' && !FreshRSS_Auth::hasAccess('admin')) { + Minz_Request::bad('feedback.extension.no_access', $url_redirect); + } + + $ext->install(); + + FreshRSS_Context::$conf->addExtension($ext_name); + FreshRSS_Context::$conf->save(); + + Minz_Request::good('feedback.extension.enabled', $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. + * + * 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('feedback.extension.not_found', $url_redirect); + } + + if (!$ext->is_enabled()) { + Minz_Request::bad('feedback.extension.not_enabled', $url_redirect); + } + + if ($ext->getType() === 'system' && !FreshRSS_Auth::hasAccess('admin')) { + Minz_Request::bad('feedback.extension.no_access', $url_redirect); + } + + $ext->uninstall(); + + FreshRSS_Context::$conf->removeExtension($ext_name); + FreshRSS_Context::$conf->save(); + + Minz_Request::good('feedback.extension.disabled', $url_redirect); + } + + Minz_Request::forward($url_redirect, true); } public function removeAction() { diff --git a/app/Models/Configuration.php b/app/Models/Configuration.php index 13ce43990..83a00d4bb 100644 --- a/app/Models/Configuration.php +++ b/app/Models/Configuration.php @@ -350,4 +350,16 @@ class FreshRSS_Configuration { } $this->data['extensions_enabled'] = $value; } + public function removeExtension($ext_name) { + $this->data['extensions_enabled'] = array_diff( + $this->data['extensions_enabled'], + array($ext_name) + ); + } + public function addExtension($ext_name) { + $found = array_search($ext_name, $this->data['extensions_enabled']) !== false; + if (!$found) { + $this->data['extensions_enabled'][] = $ext_name; + } + } } diff --git a/lib/Minz/ExtensionManager.php b/lib/Minz/ExtensionManager.php index 6c32ccf19..46e421bac 100644 --- a/lib/Minz/ExtensionManager.php +++ b/lib/Minz/ExtensionManager.php @@ -160,10 +160,8 @@ class Minz_ExtensionManager { } } - - /** - * Returns a list of extensions. + * Return a list of extensions. * * @param $only_enabled if true returns only the enabled extensions (false by default). * @return an array of extensions. @@ -175,4 +173,18 @@ class Minz_ExtensionManager { 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]; + } } -- cgit v1.2.3 From 4c888590e6f0fd89fc1dccebb5e815883eeaa54c Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Sat, 6 Dec 2014 16:39:10 +0100 Subject: Improve system/user types for extensions - system extensions can only be managed by an administrator - system extensions are loaded for all users (even if not logged) - user extensions are loaded for logged users only - system extensions loading is saved in global config.php file See https://github.com/FreshRSS/FreshRSS/issues/252 --- app/Controllers/extensionController.php | 42 ++++++++++++++++++++++----------- app/FreshRSS.php | 8 ++++--- app/views/extension/index.phtml | 4 ++++ lib/Minz/Configuration.php | 19 +++++++++++++-- 4 files changed, 54 insertions(+), 19 deletions(-) diff --git a/app/Controllers/extensionController.php b/app/Controllers/extensionController.php index 415f489a6..e348d9f31 100644 --- a/app/Controllers/extensionController.php +++ b/app/Controllers/extensionController.php @@ -52,16 +52,23 @@ class FreshRSS_extension_Controller extends Minz_ActionController { Minz_Request::bad('feedback.extension.already_enabled', $url_redirect); } - if ($ext->getType() === 'system' && !FreshRSS_Auth::hasAccess('admin')) { - Minz_Request::bad('feedback.extension.no_access', $url_redirect); - } + if ($ext->getType() === 'system' && FreshRSS_Auth::hasAccess('admin')) { + $ext->install(); + + Minz_Configuration::addExtension($ext_name); + Minz_Configuration::writeFile(); - $ext->install(); + Minz_Request::good('feedback.extension.enabled', $url_redirect); + } elseif ($ext->getType() === 'user') { + $ext->install(); - FreshRSS_Context::$conf->addExtension($ext_name); - FreshRSS_Context::$conf->save(); + FreshRSS_Context::$conf->addExtension($ext_name); + FreshRSS_Context::$conf->save(); - Minz_Request::good('feedback.extension.enabled', $url_redirect); + Minz_Request::good('feedback.extension.enabled', $url_redirect); + } else { + Minz_Request::bad('feedback.extension.no_access', $url_redirect); + } } Minz_Request::forward($url_redirect, true); @@ -90,16 +97,23 @@ class FreshRSS_extension_Controller extends Minz_ActionController { Minz_Request::bad('feedback.extension.not_enabled', $url_redirect); } - if ($ext->getType() === 'system' && !FreshRSS_Auth::hasAccess('admin')) { - Minz_Request::bad('feedback.extension.no_access', $url_redirect); - } + if ($ext->getType() === 'system' && FreshRSS_Auth::hasAccess('admin')) { + $ext->uninstall(); + + Minz_Configuration::removeExtension($ext_name); + Minz_Configuration::writeFile(); - $ext->uninstall(); + Minz_Request::good('feedback.extension.disabled', $url_redirect); + } elseif ($ext->getType() === 'user') { + $ext->uninstall(); - FreshRSS_Context::$conf->removeExtension($ext_name); - FreshRSS_Context::$conf->save(); + FreshRSS_Context::$conf->removeExtension($ext_name); + FreshRSS_Context::$conf->save(); - Minz_Request::good('feedback.extension.disabled', $url_redirect); + Minz_Request::good('feedback.extension.disabled', $url_redirect); + } else { + Minz_Request::bad('feedback.extension.no_access', $url_redirect); + } } Minz_Request::forward($url_redirect, true); diff --git a/app/FreshRSS.php b/app/FreshRSS.php index dc7d0b375..b91dfcc46 100644 --- a/app/FreshRSS.php +++ b/app/FreshRSS.php @@ -29,9 +29,11 @@ class FreshRSS extends Minz_FrontController { // Load context and configuration. FreshRSS_Context::init(); - // Enable extensions for the current user. - $ext_list = FreshRSS_Context::$conf->extensions_enabled; - Minz_ExtensionManager::enable_by_list($ext_list); + // Enable extensions for the current (logged) user. + if (FreshRSS_Auth::hasAccess()) { + $ext_list = FreshRSS_Context::$conf->extensions_enabled; + Minz_ExtensionManager::enable_by_list($ext_list); + } // Init i18n. Minz_Session::_param('language', FreshRSS_Context::$conf->language); diff --git a/app/views/extension/index.phtml b/app/views/extension/index.phtml index c6b7c84a1..0be03d7b5 100644 --- a/app/views/extension/index.phtml +++ b/app/views/extension/index.phtml @@ -10,6 +10,7 @@ extension_list as $ext) { ?>
    • + getType() === 'user' || FreshRSS_Auth::hasAccess('admin')) { ?> getName()); ?>
      @@ -22,6 +23,9 @@
      + + +
    • getName(); ?>
    diff --git a/lib/Minz/Configuration.php b/lib/Minz/Configuration.php index 4d3ab0964..4a3221ef5 100644 --- a/lib/Minz/Configuration.php +++ b/lib/Minz/Configuration.php @@ -165,6 +165,19 @@ class Minz_Configuration { self::$unsafe_autologin_enabled = (bool)$value; } + 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; + } + } + /** * Initialise les variables de configuration * @exception Minz_FileNotExistException si le CONF_PATH_NAME n'existe pas @@ -197,6 +210,7 @@ class Minz_Configuration { ), 'limits' => self::$limits, 'db' => self::$db, + 'extensions_enabled' => self::$extensions_enabled, ); @rename(DATA_PATH . self::CONF_PATH_NAME, DATA_PATH . self::CONF_PATH_NAME . '.bak.php'); $result = file_put_contents(DATA_PATH . self::CONF_PATH_NAME, " Date: Sat, 6 Dec 2014 16:48:13 +0100 Subject: Fix typo (extensions) - change feedback.extension into feedback.extensions - disable button is pushed by default See https://github.com/FreshRSS/FreshRSS/issues/252 --- app/Controllers/extensionController.php | 20 ++++++++++---------- app/views/extension/index.phtml | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/Controllers/extensionController.php b/app/Controllers/extensionController.php index e348d9f31..543398b05 100644 --- a/app/Controllers/extensionController.php +++ b/app/Controllers/extensionController.php @@ -45,11 +45,11 @@ class FreshRSS_extension_Controller extends Minz_ActionController { $ext = Minz_ExtensionManager::find_extension($ext_name); if (is_null($ext)) { - Minz_Request::bad('feedback.extension.not_found', $url_redirect); + Minz_Request::bad('feedback.extensions.not_found', $url_redirect); } if ($ext->is_enabled()) { - Minz_Request::bad('feedback.extension.already_enabled', $url_redirect); + Minz_Request::bad('feedback.extensions.already_enabled', $url_redirect); } if ($ext->getType() === 'system' && FreshRSS_Auth::hasAccess('admin')) { @@ -58,16 +58,16 @@ class FreshRSS_extension_Controller extends Minz_ActionController { Minz_Configuration::addExtension($ext_name); Minz_Configuration::writeFile(); - Minz_Request::good('feedback.extension.enabled', $url_redirect); + Minz_Request::good('feedback.extensions.enabled', $url_redirect); } elseif ($ext->getType() === 'user') { $ext->install(); FreshRSS_Context::$conf->addExtension($ext_name); FreshRSS_Context::$conf->save(); - Minz_Request::good('feedback.extension.enabled', $url_redirect); + Minz_Request::good('feedback.extensions.enabled', $url_redirect); } else { - Minz_Request::bad('feedback.extension.no_access', $url_redirect); + Minz_Request::bad('feedback.extensions.no_access', $url_redirect); } } @@ -90,11 +90,11 @@ class FreshRSS_extension_Controller extends Minz_ActionController { $ext = Minz_ExtensionManager::find_extension($ext_name); if (is_null($ext)) { - Minz_Request::bad('feedback.extension.not_found', $url_redirect); + Minz_Request::bad('feedback.extensions.not_found', $url_redirect); } if (!$ext->is_enabled()) { - Minz_Request::bad('feedback.extension.not_enabled', $url_redirect); + Minz_Request::bad('feedback.extensions.not_enabled', $url_redirect); } if ($ext->getType() === 'system' && FreshRSS_Auth::hasAccess('admin')) { @@ -103,16 +103,16 @@ class FreshRSS_extension_Controller extends Minz_ActionController { Minz_Configuration::removeExtension($ext_name); Minz_Configuration::writeFile(); - Minz_Request::good('feedback.extension.disabled', $url_redirect); + Minz_Request::good('feedback.extensions.disabled', $url_redirect); } elseif ($ext->getType() === 'user') { $ext->uninstall(); FreshRSS_Context::$conf->removeExtension($ext_name); FreshRSS_Context::$conf->save(); - Minz_Request::good('feedback.extension.disabled', $url_redirect); + Minz_Request::good('feedback.extensions.disabled', $url_redirect); } else { - Minz_Request::bad('feedback.extension.no_access', $url_redirect); + Minz_Request::bad('feedback.extensions.no_access', $url_redirect); } } diff --git a/app/views/extension/index.phtml b/app/views/extension/index.phtml index 0be03d7b5..142ee4bc2 100644 --- a/app/views/extension/index.phtml +++ b/app/views/extension/index.phtml @@ -15,7 +15,7 @@
    is_enabled()) { ?> - + -- cgit v1.2.3 From 2da7c05fa6768b95a5cd0bd1c8f9934bbff05a03 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Sat, 6 Dec 2014 17:15:20 +0100 Subject: Update i18n (extensions) See https://github.com/FreshRSS/FreshRSS/issues/252 --- app/Controllers/extensionController.php | 30 ++++++++++++++++++++---------- app/i18n/en/admin.php | 5 +++++ app/i18n/en/feedback.php | 8 ++++++++ app/i18n/en/gen.php | 7 +++++++ app/i18n/fr/admin.php | 5 +++++ app/i18n/fr/feedback.php | 8 ++++++++ app/i18n/fr/gen.php | 7 +++++++ app/views/extension/index.phtml | 8 ++++---- 8 files changed, 64 insertions(+), 14 deletions(-) diff --git a/app/Controllers/extensionController.php b/app/Controllers/extensionController.php index 543398b05..35b001094 100644 --- a/app/Controllers/extensionController.php +++ b/app/Controllers/extensionController.php @@ -45,11 +45,13 @@ class FreshRSS_extension_Controller extends Minz_ActionController { $ext = Minz_ExtensionManager::find_extension($ext_name); if (is_null($ext)) { - Minz_Request::bad('feedback.extensions.not_found', $url_redirect); + Minz_Request::bad(_t('feedback.extensions.not_found', $ext_name), + $url_redirect); } if ($ext->is_enabled()) { - Minz_Request::bad('feedback.extensions.already_enabled', $url_redirect); + Minz_Request::bad(_t('feedback.extensions.already_enabled', $ext_name), + $url_redirect); } if ($ext->getType() === 'system' && FreshRSS_Auth::hasAccess('admin')) { @@ -58,16 +60,19 @@ class FreshRSS_extension_Controller extends Minz_ActionController { Minz_Configuration::addExtension($ext_name); Minz_Configuration::writeFile(); - Minz_Request::good('feedback.extensions.enabled', $url_redirect); + Minz_Request::good(_t('feedback.extensions.enabled', $ext_name), + $url_redirect); } elseif ($ext->getType() === 'user') { $ext->install(); FreshRSS_Context::$conf->addExtension($ext_name); FreshRSS_Context::$conf->save(); - Minz_Request::good('feedback.extensions.enabled', $url_redirect); + Minz_Request::good(_t('feedback.extensions.enabled', $ext_name), + $url_redirect); } else { - Minz_Request::bad('feedback.extensions.no_access', $url_redirect); + Minz_Request::bad(_t('feedback.extensions.no_access', $ext_name), + $url_redirect); } } @@ -90,11 +95,13 @@ class FreshRSS_extension_Controller extends Minz_ActionController { $ext = Minz_ExtensionManager::find_extension($ext_name); if (is_null($ext)) { - Minz_Request::bad('feedback.extensions.not_found', $url_redirect); + Minz_Request::bad(_t('feedback.extensions.not_found', $ext_name), + $url_redirect); } if (!$ext->is_enabled()) { - Minz_Request::bad('feedback.extensions.not_enabled', $url_redirect); + Minz_Request::bad(_t('feedback.extensions.not_enabled', $ext_name), + $url_redirect); } if ($ext->getType() === 'system' && FreshRSS_Auth::hasAccess('admin')) { @@ -103,16 +110,19 @@ class FreshRSS_extension_Controller extends Minz_ActionController { Minz_Configuration::removeExtension($ext_name); Minz_Configuration::writeFile(); - Minz_Request::good('feedback.extensions.disabled', $url_redirect); + Minz_Request::good(_t('feedback.extensions.disabled', $ext_name), + $url_redirect); } elseif ($ext->getType() === 'user') { $ext->uninstall(); FreshRSS_Context::$conf->removeExtension($ext_name); FreshRSS_Context::$conf->save(); - Minz_Request::good('feedback.extensions.disabled', $url_redirect); + Minz_Request::good(_t('feedback.extensions.disabled', $ext_name), + $url_redirect); } else { - Minz_Request::bad('feedback.extensions.no_access', $url_redirect); + Minz_Request::bad(_t('feedback.extensions.no_access', $ext_name), + $url_redirect); } } diff --git a/app/i18n/en/admin.php b/app/i18n/en/admin.php index 74f01ae06..d73775d96 100644 --- a/app/i18n/en/admin.php +++ b/app/i18n/en/admin.php @@ -86,6 +86,11 @@ return array( 'ok' => 'You have ZIP extension.', ), ), + 'extensions' => array( + 'empty_list' => 'There is no installed extension', + 'system' => 'System extension (you have no rights on it)', + 'title' => 'Extensions', + ), 'users' => array( 'articles_and_size' => '%s articles (%s)', ), diff --git a/app/i18n/en/feedback.php b/app/i18n/en/feedback.php index b3866f1dc..df1dc5725 100644 --- a/app/i18n/en/feedback.php +++ b/app/i18n/en/feedback.php @@ -1,6 +1,14 @@ 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', + ), 'login' => array( 'error' => 'Login is invalid', 'success' => 'You are connected', diff --git a/app/i18n/en/gen.php b/app/i18n/en/gen.php index 9e06357bc..ba5f0c86d 100644 --- a/app/i18n/en/gen.php +++ b/app/i18n/en/gen.php @@ -190,10 +190,17 @@ return array( 'freshrss_installation' => 'Installation · FreshRSS', 'fri' => 'Fri', 'g+' => 'Google+', + 'actions' => array( + 'disable' => 'Disable', + 'enable' => 'Enable', + 'manage' => 'Manage', + 'remove' => 'Remove', + ), 'menu' => array( 'admin' => 'Administration', 'authentication' => 'Authentication', 'check_install' => 'Installation checking', + 'extensions' => 'Extensions', 'user_management' => 'Manage users', 'user_profile' => 'Profile', ), diff --git a/app/i18n/fr/admin.php b/app/i18n/fr/admin.php index ad1fae6c0..e46a5a7b0 100644 --- a/app/i18n/fr/admin.php +++ b/app/i18n/fr/admin.php @@ -86,6 +86,11 @@ return array( 'ok' => 'Vous disposez de l\'extension ZIP.', ), ), + 'extensions' => array( + 'empty_list' => 'Il n’y a aucune extension installée.', + 'system' => 'Extension système (vous n’avez aucun droit dessus)', + 'title' => 'Extensions', + ), 'users' => array( 'articles_and_size' => '%s articles (%s)', ), diff --git a/app/i18n/fr/feedback.php b/app/i18n/fr/feedback.php index f4bb7cccf..7d3ab9db7 100644 --- a/app/i18n/fr/feedback.php +++ b/app/i18n/fr/feedback.php @@ -1,6 +1,14 @@ 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', + ), 'login' => array( 'error' => 'L’identifiant est invalide !', 'success' => 'Vous êtes désormais connecté', diff --git a/app/i18n/fr/gen.php b/app/i18n/fr/gen.php index 0b2395bd0..44d8fb837 100644 --- a/app/i18n/fr/gen.php +++ b/app/i18n/fr/gen.php @@ -191,10 +191,17 @@ return array( 'freshrss_installation' => 'Installation · FreshRSS', 'fri' => 'ven.', 'g+' => 'Google+', + 'actions' => array( + 'disable' => 'Désactiver', + 'enable' => 'Activer', + 'manage' => 'Gérer', + 'remove' => 'Supprimer', + ), 'menu' => array( 'admin' => 'Administration', 'authentication' => 'Authentification', 'check_install' => 'Vérification de l’installation', + 'extensions' => 'Extensions', 'user_management' => 'Gestion des utilisateurs', 'user_profile' => 'Profil', ), diff --git a/app/views/extension/index.phtml b/app/views/extension/index.phtml index 142ee4bc2..d34a84452 100644 --- a/app/views/extension/index.phtml +++ b/app/views/extension/index.phtml @@ -13,14 +13,14 @@ getType() === 'user' || FreshRSS_Auth::hasAccess('admin')) { ?> getName()); ?>
    - + is_enabled()) { ?> - + - + - +
    -- cgit v1.2.3 From bc81979a6b25554c4832d5ccb41b427023096463 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Sat, 6 Dec 2014 17:25:01 +0100 Subject: Add default behaviour for configure / remove ext See https://github.com/FreshRSS/FreshRSS/issues/252 --- app/Controllers/extensionController.php | 7 ++++++- app/views/extension/configure.phtml | 12 ++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 app/views/extension/configure.phtml diff --git a/app/Controllers/extensionController.php b/app/Controllers/extensionController.php index 35b001094..a1e39af37 100644 --- a/app/Controllers/extensionController.php +++ b/app/Controllers/extensionController.php @@ -130,6 +130,11 @@ class FreshRSS_extension_Controller extends Minz_ActionController { } public function removeAction() { - + if (!FreshRSS_Auth::hasAccess('admin')) { + Minz_Error::error(403); + } + + $url_redirect = array('c' => 'extension', 'a' => 'index'); + Minz_Request::bad('not implemented yet!', $url_redirect); } } diff --git a/app/views/extension/configure.phtml b/app/views/extension/configure.phtml new file mode 100644 index 000000000..a79e9baac --- /dev/null +++ b/app/views/extension/configure.phtml @@ -0,0 +1,12 @@ +partial('aside_configure'); +} + +?> + +
    +

    Extension name

    + Not implemented yet! +
    \ No newline at end of file -- cgit v1.2.3 From 08546af75ff9a25eac3409649ea4660fe070720c Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Sat, 6 Dec 2014 18:48:00 +0100 Subject: Add a first draft for hooks - New Extension->registerHook($hook_name, $hook_function) method to register a new hook - Only one hook works for the moment: entry_before_insert - ExtensionManager::callHook will need to evolve based on future hooks See https://github.com/FreshRSS/FreshRSS/issues/252 --- app/Controllers/feedController.php | 11 ++++++-- lib/Minz/Extension.php | 10 +++++++ lib/Minz/ExtensionManager.php | 55 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index 9990a852c..dce79c57a 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -329,9 +329,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/lib/Minz/Extension.php b/lib/Minz/Extension.php index 490a5c5cb..c93ba2520 100644 --- a/lib/Minz/Extension.php +++ b/lib/Minz/Extension.php @@ -153,4 +153,14 @@ class Minz_Extension { public function registerViews() { Minz_View::addBasePathname($this->path); } + + /** + * 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/ExtensionManager.php b/lib/Minz/ExtensionManager.php index 46e421bac..7557e178f 100644 --- a/lib/Minz/ExtensionManager.php +++ b/lib/Minz/ExtensionManager.php @@ -2,6 +2,8 @@ /** * 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'; @@ -11,6 +13,11 @@ class Minz_ExtensionManager { private static $ext_auto_enabled = array(); + private static $hook_list = array( + 'entry_before_insert' => array(), // function($entry) + ); + private static $ext_to_hooks = array(); + /** * Initialize the extension manager by loading extensions in EXTENSIONS_PATH. * @@ -131,6 +138,8 @@ class Minz_ExtensionManager { in_array($name, self::$ext_auto_enabled)) { self::enable($ext->getName()); } + + self::$ext_to_hooks[$name] = array(); } /** @@ -187,4 +196,50 @@ class Minz_ExtensionManager { 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; + foreach (self::$hook_list[$hook_name] as $function) { + $result = call_user_func_array($function, $args); + + if (is_null($result)) { + break; + } + + $args = $result; + } + return $result; + } } -- cgit v1.2.3 From 5932c3427b060d4f0aeab92d7ed17c8e8d4fd1d7 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Sun, 7 Dec 2014 14:31:50 +0100 Subject: Add entry_before_display hook See https://github.com/FreshRSS/FreshRSS/issues/252 --- app/views/index/normal.phtml | 5 +++++ app/views/index/reader.phtml | 11 +++++++---- lib/Minz/ExtensionManager.php | 6 ++++-- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/app/views/index/normal.phtml b/app/views/index/normal.phtml index 02d621bd0..9cbd367d0 100644 --- a/app/views/index/normal.phtml +++ b/app/views/index/normal.phtml @@ -35,6 +35,11 @@ if (!empty($this->entries)) {
    entries as $item) { + $item = Minz_ExtensionManager::callHook('entry_before_display', $item); + if (is_null($item)) { + continue; + } + if ($display_today && $item->isDay(FreshRSS_Days::TODAY, $today)) { ?>
    entries)) { $content_width = FreshRSS_Context::$conf->content_width; ?> -
    - entries as $item) { ?> - -
    +
    entries as $item) { + $item = Minz_ExtensionManager::callHook('entry_before_display', $item); + if (is_null($item)) { + continue; + } + ?>
    array(), // function($entry) + 'entry_before_display' => array(), // function($entry) -> Entry | null + 'entry_before_insert' => array(), // function($entry) -> Entry | null ); private static $ext_to_hooks = array(); @@ -230,7 +232,7 @@ class Minz_ExtensionManager { $args = func_get_args(); unset($args[0]); - $result = $args; + $result = $args[1]; foreach (self::$hook_list[$hook_name] as $function) { $result = call_user_func_array($function, $args); -- cgit v1.2.3 From 7ef4d6c033d6d12a644b6cf39940591901fdcb3b Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Sun, 7 Dec 2014 14:39:02 +0100 Subject: Fix entry_before_insert hook The hook must be called also in: - feedController->addAction() - importExportController->importJson() See https://github.com/FreshRSS/FreshRSS/issues/252 --- app/Controllers/feedController.php | 13 ++++++++++--- app/Controllers/importExportController.php | 6 ++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index dce79c57a..0a7edbee3 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -174,10 +174,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()); diff --git a/app/Controllers/importExportController.php b/app/Controllers/importExportController.php index 4e2dbd157..c67b30431 100644 --- a/app/Controllers/importExportController.php +++ b/app/Controllers/importExportController.php @@ -385,6 +385,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); -- cgit v1.2.3 From ea849d7c68cc3de33825c1daafd06b9f8bbf747c Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Sun, 7 Dec 2014 15:03:42 +0100 Subject: Prepare better organization of view files for exts View files must be well-splitted to simplify work for extensions. See https://github.com/FreshRSS/FreshRSS/issues/252 --- app/views/helpers/index/normal/entry_bottom.phtml | 84 ++++++++++++ app/views/helpers/index/normal/entry_header.phtml | 40 ++++++ app/views/index/index.phtml | 23 ---- app/views/index/normal.phtml | 157 +++------------------- 4 files changed, 144 insertions(+), 160 deletions(-) create mode 100644 app/views/helpers/index/normal/entry_bottom.phtml create mode 100644 app/views/helpers/index/normal/entry_header.phtml 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..704644f99 --- /dev/null +++ b/app/views/helpers/index/normal/entry_bottom.phtml @@ -0,0 +1,84 @@ +sharing; + } + + $bottomline_read = FreshRSS_Context::$conf->bottomline_read; + $bottomline_favorite = FreshRSS_Context::$conf->bottomline_favorite; + $bottomline_sharing = FreshRSS_Context::$conf->bottomline_sharing && (count($sharing) > 0); + $bottomline_tags = FreshRSS_Context::$conf->bottomline_tags; + $bottomline_date = FreshRSS_Context::$conf->bottomline_date; + $bottomline_link = FreshRSS_Context::$conf->bottomline_link; +?>
    • 'entry', 'a' => 'read', 'params' => array('id' => $this->entry->id())); + if ($this->entry->isRead()) { + $arUrl['params']['is_read'] = 0; + } + ?>entry->isRead() ? 'read' : 'unread'); ?>
    • 'entry', 'a' => 'bookmark', 'params' => array('id' => $this->entry->id())); + if ($this->entry->isFavorite()) { + $arUrl['params']['is_favorite'] = 0; + } + ?>entry->isFavorite() ? 'starred' : 'non-starred'); ?>
    • +
    • entry->link()); + $title = urlencode($this->entry->title() . ' · ' . $feed->name()); + ?> + +
    • entry->tags() : null; + if (!empty($tags)) { + ?>
    • + +
    • entry->date(); ?>
    • +
    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..b751b884f --- /dev/null +++ b/app/views/helpers/index/normal/entry_header.phtml @@ -0,0 +1,40 @@ +topline_read; + $topline_favorite = FreshRSS_Context::$conf->topline_favorite; + $topline_date = FreshRSS_Context::$conf->topline_date; + $topline_link = FreshRSS_Context::$conf->topline_link; +?>
    • 'entry', 'a' => 'read', 'params' => array('id' => $this->entry->id())); + if ($this->entry->isRead()) { + $arUrl['params']['is_read'] = 0; + } + ?>entry->isRead() ? 'read' : 'unread'); ?>
    • 'entry', 'a' => 'bookmark', 'params' => array('id' => $this->entry->id())); + if ($this->entry->isFavorite()) { + $arUrl['params']['is_favorite'] = 0; + } + ?>entry->isFavorite() ? 'starred' : 'non-starred'); ?>
    • categories, $this->entry->feed()); //We most likely already have the feed object in cache + if ($feed == null) { + $feed = $this->entry->feed(true); + if ($feed == null) { + $feed = FreshRSS_Feed::example(); + } + } + ?>
    • ✇ name(); ?>
    • +
    • entry->title(); ?>
    • +
    • entry->date(); ?> 
    • + +
    diff --git a/app/views/index/index.phtml b/app/views/index/index.phtml index 8b93461dd..e69de29bb 100644 --- a/app/views/index/index.phtml +++ b/app/views/index/index.phtml @@ -1,23 +0,0 @@ -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 9cbd367d0..fd2289365 100644 --- a/app/views/index/normal.phtml +++ b/app/views/index/normal.phtml @@ -7,23 +7,8 @@ if (!empty($this->entries)) { $display_today = true; $display_yesterday = true; $display_others = true; - if (FreshRSS_Auth::hasAccess()) { - $sharing = FreshRSS_Context::$conf->sharing; - } else { - $sharing = array(); - } $hidePosts = !FreshRSS_Context::$conf->display_posts; $lazyload = FreshRSS_Context::$conf->lazyload; - $topline_read = FreshRSS_Context::$conf->topline_read; - $topline_favorite = FreshRSS_Context::$conf->topline_favorite; - $topline_date = FreshRSS_Context::$conf->topline_date; - $topline_link = FreshRSS_Context::$conf->topline_link; - $bottomline_read = FreshRSS_Context::$conf->bottomline_read; - $bottomline_favorite = FreshRSS_Context::$conf->bottomline_favorite; - $bottomline_sharing = FreshRSS_Context::$conf->bottomline_sharing && (count($sharing)); - $bottomline_tags = FreshRSS_Context::$conf->bottomline_tags; - $bottomline_date = FreshRSS_Context::$conf->bottomline_date; - $bottomline_link = FreshRSS_Context::$conf->bottomline_link; $content_width = FreshRSS_Context::$conf->content_width; @@ -35,12 +20,12 @@ if (!empty($this->entries)) {
    entries as $item) { - $item = Minz_ExtensionManager::callHook('entry_before_display', $item); - if (is_null($item)) { + $this->entry = Minz_ExtensionManager::callHook('entry_before_display', $item); + if (is_null($this->entry)) { continue; } - if ($display_today && $item->isDay(FreshRSS_Days::TODAY, $today)) { + if ($display_today && $this->entry->isDay(FreshRSS_Days::TODAY, $today)) { ?>
    entries)) { ?>
    isDay(FreshRSS_Days::YESTERDAY, $today)) { + if ($display_yesterday && $this->entry->isDay(FreshRSS_Days::YESTERDAY, $today)) { ?>
    entries)) { ?>
    isDay(FreshRSS_Days::BEFORE_YESTERDAY, $today)) { + if ($display_others && $this->entry->isDay(FreshRSS_Days::BEFORE_YESTERDAY, $today)) { ?>
    -
    • 'entry', 'a' => 'read', 'params' => array('id' => $item->id())); - if ($item->isRead()) { - $arUrl['params']['is_read'] = 0; - } - ?>isRead() ? 'read' : 'unread'); ?>
    • 'entry', 'a' => 'bookmark', 'params' => array('id' => $item->id())); - if ($item->isFavorite()) { - $arUrl['params']['is_favorite'] = 0; - } - ?>isFavorite() ? 'starred' : 'non-starred'); ?>
    • 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(); - } - } - ?>
    • ✇ name(); ?>
    • -
    • title(); ?>
    • -
    • date(); ?> 
    • - -
    + ?>
    renderHelper('index/normal/entry_header'); -
    + ?>
    -

    title(); ?>

    +

    entry->title(); ?>

    author(); + $author = $this->entry->author(); echo $author != '' ? '
    ' . _t('by_author', $author) . '
    ' : '', - $lazyload && $hidePosts ? lazyimg($item->content()) : $item->content(); + $lazyload && $hidePosts ? lazyimg($this->entry->content()) : $this->entry->content(); ?> -
    -
    • 'entry', 'a' => 'read', 'params' => array('id' => $item->id())); - if ($item->isRead()) { - $arUrl['params']['is_read'] = 0; - } - ?>isRead() ? 'read' : 'unread'); ?>
    • 'entry', 'a' => 'bookmark', 'params' => array('id' => $item->id())); - if ($item->isFavorite()) { - $arUrl['params']['is_favorite'] = 0; - } - ?>isFavorite() ? 'starred' : 'non-starred'); ?>
    • -
    • link()); - $title = urlencode($item->title() . ' · ' . $feed->name()); - ?> -
    • - - - -
    -
    - - tags() : null; - if (!empty($tags)) { - ?>
  • - -
  • date(); ?>
  • - -
    -
    - + $this->renderHelper('index/normal/entry_bottom'); - renderHelper('pagination'); ?> -
    + ?>
    +
    renderHelper('pagination'); +?>
    partial('nav_entries'); ?> -- cgit v1.2.3 From 198b154064a12cfbeefd494afaa69378e77ffce9 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Sun, 7 Dec 2014 15:30:24 +0100 Subject: Fix View files inclusion. Now, each part of the view (layout, partials, helpers, views) is included based on the $base_pathnames attribute. Extensions can now override all of these files. See https://github.com/FreshRSS/FreshRSS/issues/252 --- lib/Minz/View.php | 62 ++++++++++++++++++++++++++----------------------------- 1 file changed, 29 insertions(+), 33 deletions(-) diff --git a/lib/Minz/View.php b/lib/Minz/View.php index 1bc2e862d..44034ae9a 100644 --- a/lib/Minz/View.php +++ b/lib/Minz/View.php @@ -13,7 +13,7 @@ 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 = ''; @@ -56,9 +56,6 @@ class Minz_View { * 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 { @@ -66,35 +63,40 @@ class Minz_View { } } + /** + * Include a view file. + * + * The file is searched inside list of $base_pathnames. + * + * @param $filename the name of the file to include. + * @return true if the file has been included, false else. + */ + private function includeFile($filename) { + // We search the filename in the list of base pathnames. Only the first view + // found is considered. + foreach (self::$base_pathnames as $base) { + $absolute_filename = $base . $filename; + if (file_exists($absolute_filename)) { + include $absolute_filename; + return true; + } + } + + return false; + } + /** * Construit le layout */ public function buildLayout () { - include ( - APP_PATH - . self::LAYOUT_PATH_NAME - . self::LAYOUT_FILENAME - ); + $this->includeFile(self::LAYOUT_PATH_NAME . self::LAYOUT_FILENAME); } /** * Affiche la Vue en elle-même */ public function render () { - $view_found = false; - - // We search the view in the list of base pathnames. Only the first view - // found is considered. - foreach (self::$base_pathnames as $base) { - $filename = $base . $this->view_filename; - if (file_exists($filename)) { - include $filename; - $view_found = true; - break; - } - } - - if (!$view_found) { + if (!$this->includeFile($this->view_filename)) { Minz_Log::notice('File not found: `' . $this->view_filename . '`'); } } @@ -104,11 +106,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 . '`'); } } @@ -118,11 +117,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 . '`'); } } -- cgit v1.2.3 From 2232d1e02a4bc9dbaa99cdbd22efad116ec01403 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Sun, 7 Dec 2014 15:45:34 +0100 Subject: Load user extensions after all the global inits See https://github.com/FreshRSS/FreshRSS/issues/252 --- app/FreshRSS.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/FreshRSS.php b/app/FreshRSS.php index b91dfcc46..88fe60850 100644 --- a/app/FreshRSS.php +++ b/app/FreshRSS.php @@ -29,18 +29,18 @@ class FreshRSS extends Minz_FrontController { // Load context and configuration. FreshRSS_Context::init(); - // Enable extensions for the current (logged) user. - if (FreshRSS_Auth::hasAccess()) { - $ext_list = FreshRSS_Context::$conf->extensions_enabled; - Minz_ExtensionManager::enable_by_list($ext_list); - } - // Init i18n. Minz_Session::_param('language', FreshRSS_Context::$conf->language); Minz_Translate::init(); $this->loadStylesAndScripts(); $this->loadNotifications(); + + // Enable extensions for the current (logged) user. + if (FreshRSS_Auth::hasAccess()) { + $ext_list = FreshRSS_Context::$conf->extensions_enabled; + Minz_ExtensionManager::enable_by_list($ext_list); + } } private function loadStylesAndScripts() { -- cgit v1.2.3 From c6dfec3ad351ee3b828c6a2c0a273bad5d9ac0df Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Mon, 8 Dec 2014 12:01:47 +0100 Subject: Add behaviour to configure action (extensions) - Put extension configure view in dir_ext/configure.phtml - Handle POST action in Extension->handleConfigureAction() method See https://github.com/FreshRSS/FreshRSS/issues/252 --- app/Controllers/extensionController.php | 16 ++++++++++++++++ app/views/extension/configure.phtml | 17 ++++++++++++++--- lib/Minz/Extension.php | 21 +++++++++++++++++++++ 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/app/Controllers/extensionController.php b/app/Controllers/extensionController.php index a1e39af37..73b8070cb 100644 --- a/app/Controllers/extensionController.php +++ b/app/Controllers/extensionController.php @@ -27,6 +27,22 @@ class FreshRSS_extension_Controller extends Minz_ActionController { 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; + + if (Minz_Request::isPost()) { + $this->view->extension->handleConfigureAction(); + } } /** diff --git a/app/views/extension/configure.phtml b/app/views/extension/configure.phtml index a79e9baac..295080d5e 100644 --- a/app/views/extension/configure.phtml +++ b/app/views/extension/configure.phtml @@ -7,6 +7,17 @@ if (!Minz_Request::param('ajax')) { ?>
    -

    Extension name

    - Not implemented yet! -
    \ No newline at end of file +

    extension->getName(); ?> (extension->getVersion(); ?>) — extension->getType(); ?>

    + +

    extension->getDescription(); ?> — extension->getAuthor(); ?>

    + +

    + extension->getConfigureView(); + if ($configure_view !== false) { + echo $configure_view; + } else { + ?> +

    + +
    diff --git a/lib/Minz/Extension.php b/lib/Minz/Extension.php index c93ba2520..1d706ed80 100644 --- a/lib/Minz/Extension.php +++ b/lib/Minz/Extension.php @@ -86,6 +86,27 @@ class Minz_Extension { 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; + } + return @file_get_contents($filename); + } + + /** + * Handle the configure POST action. + * + * It must be redefined by child classes. + */ + public function handleConfigureAction() {} + /** * Getters and setters. */ -- cgit v1.2.3 From 188b517daa174ce494f31dec02ae2cff122488ff Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Mon, 8 Dec 2014 13:05:56 +0100 Subject: Add a feed_before_insert hook See https://github.com/FreshRSS/FreshRSS/issues/252 --- app/Controllers/feedController.php | 6 ++++++ app/Controllers/importExportController.php | 30 ++++++++++++++++++++---------- lib/Minz/ExtensionManager.php | 1 + 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index 0a7edbee3..7dda3840e 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -138,6 +138,12 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $feed->_category($cat); $feed->_httpAuth($http_auth); + // Call the extension hook + $feed = Minz_ExtensionManager::callHook('feed_before_insert', $feed); + if (is_null($feed)) { + Minz_Request::bad(_t('feed_not_added', $feed->name()), $url_redirect); + } + $values = array( 'url' => $feed->url(), 'category' => $feed->category(), diff --git a/app/Controllers/importExportController.php b/app/Controllers/importExportController.php index c67b30431..52df5bf8b 100644 --- a/app/Controllers/importExportController.php +++ b/app/Controllers/importExportController.php @@ -259,10 +259,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; @@ -427,13 +433,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/lib/Minz/ExtensionManager.php b/lib/Minz/ExtensionManager.php index 5491c7cf6..9e6a3155a 100644 --- a/lib/Minz/ExtensionManager.php +++ b/lib/Minz/ExtensionManager.php @@ -17,6 +17,7 @@ class Minz_ExtensionManager { 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(); -- cgit v1.2.3 From 28c77f22900a10ac43c6d61b2b2d1a146feac42a Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Mon, 8 Dec 2014 13:12:12 +0100 Subject: Fix a bug with feed_before_insert hook $feed->name() was called on a null value. See https://github.com/FreshRSS/FreshRSS/issues/252 --- app/Controllers/feedController.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/Controllers/feedController.php b/app/Controllers/feedController.php index 7dda3840e..379b8d8da 100755 --- a/app/Controllers/feedController.php +++ b/app/Controllers/feedController.php @@ -139,9 +139,10 @@ class FreshRSS_feed_Controller extends Minz_ActionController { $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', $feed->name()), $url_redirect); + Minz_Request::bad(_t('feed_not_added', $name), $url_redirect); } $values = array( -- cgit v1.2.3 From 0543f96a97fa860fdec38d61b117e6b5addf94b6 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Mon, 8 Dec 2014 13:22:11 +0100 Subject: Update comments for ExtensionController See https://github.com/FreshRSS/FreshRSS/issues/252 --- app/Controllers/extensionController.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/app/Controllers/extensionController.php b/app/Controllers/extensionController.php index 73b8070cb..cd56de9eb 100644 --- a/app/Controllers/extensionController.php +++ b/app/Controllers/extensionController.php @@ -23,6 +23,16 @@ class FreshRSS_extension_Controller extends Minz_ActionController { $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); @@ -49,6 +59,7 @@ class FreshRSS_extension_Controller extends Minz_ActionController { * 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). @@ -99,6 +110,7 @@ class FreshRSS_extension_Controller extends Minz_ActionController { * 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). @@ -145,6 +157,15 @@ class FreshRSS_extension_Controller extends Minz_ActionController { 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); -- cgit v1.2.3 From 76358846abe2eba95668d66d3847cbdfe3f8bcdc Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Mon, 8 Dec 2014 13:36:08 +0100 Subject: Implement extension deletion See https://github.com/FreshRSS/FreshRSS/issues/252 --- app/Controllers/extensionController.php | 22 +++++++++++++++++++++- lib/lib_rss.php | 26 ++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/app/Controllers/extensionController.php b/app/Controllers/extensionController.php index cd56de9eb..adb3e1864 100644 --- a/app/Controllers/extensionController.php +++ b/app/Controllers/extensionController.php @@ -172,6 +172,26 @@ class FreshRSS_extension_Controller extends Minz_ActionController { } $url_redirect = array('c' => 'extension', 'a' => 'index'); - Minz_Request::bad('not implemented yet!', $url_redirect); + + 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/lib/lib_rss.php b/lib/lib_rss.php index 8170c7fd9..e466bcb15 100644 --- a/lib/lib_rss.php +++ b/lib/lib_rss.php @@ -319,3 +319,29 @@ function check_install_database() { return $status; } + +/** + * Remove a directory recursively. + * + * From http://php.net/rmdir#110489 + * + * @param $dir the directory to remove + */ +function recursive_unlink($dir) { + if (!is_dir($dir)) { + return true; + } + + $files = array_diff(scandir($dir), array('.', '..')); + foreach ($files as $filename) { + $filename = $dir . '/' . $filename; + if (is_dir($filename)) { + @chmod($filename, 0777); + recursive_unlink($filename); + } else { + unlink($filename); + } + } + + return rmdir($dir); +} -- cgit v1.2.3 From 930026df2159b027bc035f4ed15c39a43eb0e9fe Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Wed, 17 Dec 2014 21:24:49 +0100 Subject: Fix bug with new file organisation entry_bottom.phtml didn't find $feed variable. It's fixed now. --- app/views/helpers/index/normal/entry_bottom.phtml | 4 ++-- app/views/helpers/index/normal/entry_header.phtml | 9 +-------- app/views/index/normal.phtml | 9 +++++++++ 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/app/views/helpers/index/normal/entry_bottom.phtml b/app/views/helpers/index/normal/entry_bottom.phtml index 704644f99..e1961e2aa 100644 --- a/app/views/helpers/index/normal/entry_bottom.phtml +++ b/app/views/helpers/index/normal/entry_bottom.phtml @@ -36,7 +36,7 @@
  • entry->link()); - $title = urlencode($this->entry->title() . ' · ' . $feed->name()); + $title = urlencode($this->entry->title() . ' · ' . $this->feed->name()); ?>
  • diff --git a/app/views/helpers/index/normal/entry_header.phtml b/app/views/helpers/index/normal/entry_header.phtml index b751b884f..3d8b089b9 100644 --- a/app/views/helpers/index/normal/entry_header.phtml +++ b/app/views/helpers/index/normal/entry_header.phtml @@ -26,14 +26,7 @@ ?>categories, $this->entry->feed()); //We most likely already have the feed object in cache - if ($feed == null) { - $feed = $this->entry->feed(true); - if ($feed == null) { - $feed = FreshRSS_Feed::example(); - } - } - ?>
  • ✇ name(); ?>
  • + ?>
  • ✇ feed->name(); ?>
  • entry->title(); ?>
  • entry->date(); ?> 
  • diff --git a/app/views/index/normal.phtml b/app/views/index/normal.phtml index fd2289365..f2e1c5012 100644 --- a/app/views/index/normal.phtml +++ b/app/views/index/normal.phtml @@ -25,6 +25,15 @@ if (!empty($this->entries)) { 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)) { ?>
    Date: Wed, 17 Dec 2014 21:44:00 +0100 Subject: Fix i18n for extension system --- app/views/extension/configure.phtml | 4 ++-- app/views/extension/index.phtml | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/views/extension/configure.phtml b/app/views/extension/configure.phtml index 295080d5e..c03439a75 100644 --- a/app/views/extension/configure.phtml +++ b/app/views/extension/configure.phtml @@ -9,9 +9,9 @@ if (!Minz_Request::param('ajax')) {

    extension->getName(); ?> (extension->getVersion(); ?>) — extension->getType(); ?>

    -

    extension->getDescription(); ?> — extension->getAuthor(); ?>

    +

    extension->getDescription(); ?> — extension->getAuthor()); ?>

    -

    +

    extension->getConfigureView(); if ($configure_view !== false) { diff --git a/app/views/extension/index.phtml b/app/views/extension/index.phtml index d34a84452..fd97c5e81 100644 --- a/app/views/extension/index.phtml +++ b/app/views/extension/index.phtml @@ -1,7 +1,7 @@ partial('aside_configure'); ?>
    - +

    @@ -13,14 +13,14 @@ getType() === 'user' || FreshRSS_Auth::hasAccess('admin')) { ?> getName()); ?>
    - + is_enabled()) { ?> - + - + - +
    -- cgit v1.2.3 From 251d5a78ce893676c7e32e2a85c9ba45775a72bf Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Thu, 18 Dec 2014 13:57:25 +0100 Subject: Fix php interpretation in configure.phtml file See https://github.com/FreshRSS/FreshRSS/issues/252 --- lib/Minz/Extension.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/Minz/Extension.php b/lib/Minz/Extension.php index 1d706ed80..e7b38448f 100644 --- a/lib/Minz/Extension.php +++ b/lib/Minz/Extension.php @@ -97,7 +97,10 @@ class Minz_Extension { if (!file_exists($filename)) { return false; } - return @file_get_contents($filename); + + ob_start(); + include($filename); + return ob_get_clean(); } /** -- cgit v1.2.3 From 26da4aa448906f857a252507b34d369a386043c6 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Thu, 18 Dec 2014 18:59:13 +0100 Subject: Update Minz_Translation - Give possibility to register new i18n files - Add a extension->registerTranslates() method - extensions can define new strings or override previous ones Fix https://github.com/FreshRSS/FreshRSS/issues/731 --- lib/Minz/Extension.php | 8 +++++ lib/Minz/Translate.php | 79 +++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 76 insertions(+), 11 deletions(-) diff --git a/lib/Minz/Extension.php b/lib/Minz/Extension.php index e7b38448f..a24c718c3 100644 --- a/lib/Minz/Extension.php +++ b/lib/Minz/Extension.php @@ -178,6 +178,14 @@ class Minz_Extension { 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. * diff --git a/lib/Minz/Translate.php b/lib/Minz/Translate.php index e7efb8665..aa96728f2 100644 --- a/lib/Minz/Translate.php +++ b/lib/Minz/Translate.php @@ -15,9 +15,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. @@ -30,7 +30,10 @@ class Minz_Translate { public static function init() { $l = Minz_Configuration::language(); self::$lang_name = Minz_Session::param('language', $l); - self::$lang_path = APP_PATH . '/i18n/' . self::$lang_name . '/'; + self::$lang_files = array(); + self::$translates = array(); + + self::registerPath(APP_PATH . '/i18n'); } /** @@ -40,6 +43,63 @@ class Minz_Translate { self::init(); } + /** + * Register a new path and load i18n files inside. + * + * @param $path a path containing i18n directories (e.g. ./en/, ./fr/). + */ + public static function registerPath($path) { + // We load first i18n files for the current language. + $lang_path = $path . '/' . self::$lang_name; + $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; + } + /** * Translate a key into its corresponding value based on selected language. * @param $key the key to translate. @@ -57,16 +117,13 @@ 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 (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. -- cgit v1.2.3 From e86a6097c886ac8b0ed9278c2cafc543ad4c0227 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Thu, 8 Jan 2015 14:22:20 +0100 Subject: Add a setter for extensions_enabled config value --- app/Models/ConfigurationSetter.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/Models/ConfigurationSetter.php b/app/Models/ConfigurationSetter.php index 9830fed28..e5ea6ff6f 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; -- cgit v1.2.3 From 5bd7997d41d052420e2a4c164a4f60fe982760e2 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Thu, 8 Jan 2015 14:30:05 +0100 Subject: Call handleConfigureAction() even for GET requests See https://github.com/FreshRSS/FreshRSS/issues/252 --- app/Controllers/extensionController.php | 5 +---- lib/Minz/Extension.php | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/app/Controllers/extensionController.php b/app/Controllers/extensionController.php index 3eedcd949..e013bb369 100644 --- a/app/Controllers/extensionController.php +++ b/app/Controllers/extensionController.php @@ -49,10 +49,7 @@ class FreshRSS_extension_Controller extends Minz_ActionController { } $this->view->extension = $ext; - - if (Minz_Request::isPost()) { - $this->view->extension->handleConfigureAction(); - } + $this->view->extension->handleConfigureAction(); } /** diff --git a/lib/Minz/Extension.php b/lib/Minz/Extension.php index a24c718c3..b3991c129 100644 --- a/lib/Minz/Extension.php +++ b/lib/Minz/Extension.php @@ -104,7 +104,7 @@ class Minz_Extension { } /** - * Handle the configure POST action. + * Handle the configure action. * * It must be redefined by child classes. */ -- cgit v1.2.3 From a3f667e586e73e996175477939aa7beb10630901 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Thu, 8 Jan 2015 14:50:21 +0100 Subject: Fix Minz_Translate::availableLanguages() method --- app/Models/ConfigurationSetter.php | 2 +- app/i18n/en/gen.php | 4 ++++ app/i18n/fr/gen.php | 4 ++++ app/views/configure/display.phtml | 4 ++-- lib/Minz/Translate.php | 28 +++++++++++++++++++++++++--- 5 files changed, 36 insertions(+), 6 deletions(-) diff --git a/app/Models/ConfigurationSetter.php b/app/Models/ConfigurationSetter.php index e5ea6ff6f..f1c465c7c 100644 --- a/app/Models/ConfigurationSetter.php +++ b/app/Models/ConfigurationSetter.php @@ -89,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/gen.php b/app/i18n/en/gen.php index b3b8d8647..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', diff --git a/app/i18n/fr/gen.php b/app/i18n/fr/gen.php index 996c81cf5..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', 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 @@
    diff --git a/lib/Minz/Translate.php b/lib/Minz/Translate.php index b05c9c695..d870b6bb2 100644 --- a/lib/Minz/Translate.php +++ b/lib/Minz/Translate.php @@ -9,6 +9,11 @@ * It uses files in `./app/i18n/` */ class Minz_Translate { + /** + * $path_list is the list of registered base path to search translations. + */ + private static $path_list = array(); + /** * $lang_name is the name of the current language to use. */ @@ -30,6 +35,7 @@ class Minz_Translate { */ public static function init($lang_name) { self::$lang_name = $lang_name; + self::$path_list = array(); self::$lang_files = array(); self::$translates = array(); self::registerPath(APP_PATH . '/i18n'); @@ -45,11 +51,21 @@ class Minz_Translate { /** * Return the list of available languages. - * @return an array. - * @todo fix this method. + * @return an array containing langs found in different registered paths. */ public static function availableLanguages() { - return array(); + $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); } /** @@ -58,6 +74,12 @@ class Minz_Translate { * @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; + // We load first i18n files for the current language. $lang_path = $path . '/' . self::$lang_name; $list_i18n_files = array_values(array_diff( -- cgit v1.2.3 From 8f04cb9d9db90cce71a0de81bcfbc68b0873ea23 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Thu, 8 Jan 2015 15:05:25 +0100 Subject: Change Minz_Translate::reset() behaviour - Don't reset the path list (use init() instead) - init() accept a null lang_name. To use i18n, you'll have to use reset() later. It is helpful to load the list of available language before choosing one of them. --- lib/Minz/Translate.php | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/lib/Minz/Translate.php b/lib/Minz/Translate.php index d870b6bb2..a90af659e 100644 --- a/lib/Minz/Translate.php +++ b/lib/Minz/Translate.php @@ -33,7 +33,7 @@ class Minz_Translate { * Init the translation object. * @param $lang_name the lang to show. */ - public static function init($lang_name) { + public static function init($lang_name = null) { self::$lang_name = $lang_name; self::$path_list = array(); self::$lang_files = array(); @@ -46,7 +46,12 @@ class Minz_Translate { * @param $lang_name the new language to use */ public static function reset($lang_name) { - self::init($lang_name); + self::$lang_name = $lang_name; + self::$lang_files = array(); + self::$translates = array(); + foreach ($path_list as $path) { + self::loadLang($path); + } } /** @@ -70,7 +75,6 @@ class Minz_Translate { /** * Register a new path and load i18n files inside. - * * @param $path a path containing i18n directories (e.g. ./en/, ./fr/). */ public static function registerPath($path) { @@ -79,9 +83,20 @@ class Minz_Translate { } self::$path_list[] = $path; + self::loadLang($path); + } - // We load first i18n files for the current language. + /** + * 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('..', '.') @@ -102,7 +117,6 @@ class Minz_Translate { /** * Load the files associated to $key into $translates. - * * @param $key the top level i18n key we want to load. */ private static function loadKey($key) { -- cgit v1.2.3 From 85ea5e548ac1057feeb8dfff99b1b433e4ecfd6b Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Thu, 8 Jan 2015 15:16:32 +0100 Subject: Fix install.php script (choice of lang) Fix a bug in Minz_Translate too --- app/install.php | 25 ++++++++++++------------- lib/Minz/Translate.php | 2 +- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/app/install.php b/app/install.php index 55817ec68..a79b0d318 100644 --- a/app/install.php +++ b/app/install.php @@ -43,21 +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($_SESSION['language']); + Minz_Translate::reset($_SESSION['language']); } function get_best_language() { @@ -254,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', @@ -429,8 +426,10 @@ function printStep0() {
    diff --git a/lib/Minz/Translate.php b/lib/Minz/Translate.php index a90af659e..24497a193 100644 --- a/lib/Minz/Translate.php +++ b/lib/Minz/Translate.php @@ -49,7 +49,7 @@ class Minz_Translate { self::$lang_name = $lang_name; self::$lang_files = array(); self::$translates = array(); - foreach ($path_list as $path) { + foreach (self::$path_list as $path) { self::loadLang($path); } } -- cgit v1.2.3 From 19dfef8b49f78e898ea5841869ff80cc351724bc Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Thu, 8 Jan 2015 16:32:13 +0100 Subject: Fix bug with Minz_Translate When a path was registered before initialization, it was not considered unless by calling reset() method. This is fixed now. --- app/views/user/manage.phtml | 4 ++-- lib/Minz/Translate.php | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) 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 @@
    diff --git a/lib/Minz/Translate.php b/lib/Minz/Translate.php index 24497a193..d8ce2a0f7 100644 --- a/lib/Minz/Translate.php +++ b/lib/Minz/Translate.php @@ -35,10 +35,12 @@ class Minz_Translate { */ public static function init($lang_name = null) { self::$lang_name = $lang_name; - self::$path_list = array(); self::$lang_files = array(); self::$translates = array(); self::registerPath(APP_PATH . '/i18n'); + foreach (self::$path_list as $path) { + self::loadLang($path); + } } /** @@ -74,7 +76,7 @@ class Minz_Translate { } /** - * Register a new path and load i18n files inside. + * Register a new path. * @param $path a path containing i18n directories (e.g. ./en/, ./fr/). */ public static function registerPath($path) { @@ -83,7 +85,6 @@ class Minz_Translate { } self::$path_list[] = $path; - self::loadLang($path); } /** @@ -164,7 +165,8 @@ class Minz_Translate { // If $translates[$top_level] is null it means we have to load the // corresponding files. - if (is_null(self::$translates[$top_level])) { + if (!isset(self::$translates[$top_level]) || + is_null(self::$translates[$top_level])) { $res = self::loadKey($top_level); if (!$res) { return $key; -- cgit v1.2.3 From b23fc3187cb90800aad6417badf7822a8d280b74 Mon Sep 17 00:00:00 2001 From: Marien Fressinaud Date: Thu, 8 Jan 2015 21:32:21 +0100 Subject: Fix translation bug A path registered after initialization must be loaded. --- app/i18n/en/admin.php | 1 + app/i18n/fr/admin.php | 1 + lib/Minz/Translate.php | 1 + 3 files changed, 3 insertions(+) diff --git a/app/i18n/en/admin.php b/app/i18n/en/admin.php index 83c06bfb4..6a89d4a31 100644 --- a/app/i18n/en/admin.php +++ b/app/i18n/en/admin.php @@ -104,6 +104,7 @@ return array( ), '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', ), diff --git a/app/i18n/fr/admin.php b/app/i18n/fr/admin.php index b16e2be54..2e879da61 100644 --- a/app/i18n/fr/admin.php +++ b/app/i18n/fr/admin.php @@ -104,6 +104,7 @@ return array( ), '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', ), diff --git a/lib/Minz/Translate.php b/lib/Minz/Translate.php index d8ce2a0f7..baddcb424 100644 --- a/lib/Minz/Translate.php +++ b/lib/Minz/Translate.php @@ -85,6 +85,7 @@ class Minz_Translate { } self::$path_list[] = $path; + self::loadLang($path); } /** -- cgit v1.2.3