Browse Source

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).
Marien Fressinaud 11 years ago
parent
commit
86f69ca396

+ 22 - 20
app/FreshRSS.php

@@ -6,6 +6,9 @@ class FreshRSS extends Minz_FrontController {
 			Minz_Session::init('FreshRSS');
 			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
 		// Need to be called just after session init because it initializes
 		// current user.
 		// current user.
 		FreshRSS_Auth::init();
 		FreshRSS_Auth::init();
@@ -32,7 +35,6 @@ class FreshRSS extends Minz_FrontController {
 
 
 		$this->loadStylesAndScripts();
 		$this->loadStylesAndScripts();
 		$this->loadNotifications();
 		$this->loadNotifications();
-		$this->loadExtensions();
 	}
 	}
 
 
 	private function loadStylesAndScripts() {
 	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
+	// 			} 
+	// 		}
+	// 	}
+	// }
 }
 }

+ 2 - 1
constants.php

@@ -20,6 +20,7 @@ define('FRESHRSS_PATH', dirname(__FILE__));
 		define('CACHE_PATH', DATA_PATH . '/cache');
 		define('CACHE_PATH', DATA_PATH . '/cache');
 
 
 	define('LIB_PATH', FRESHRSS_PATH . '/lib');
 	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());
 define('TMP_PATH', sys_get_temp_dir());

+ 0 - 0
extensions/Read-me.txt → extensions/README.md


+ 10 - 0
lib/Minz/Configuration.php

@@ -69,6 +69,8 @@ class Minz_Configuration {
 		'max_categories' => Minz_Configuration::MAX_SMALL_INT,
 		'max_categories' => Minz_Configuration::MAX_SMALL_INT,
 	);
 	);
 
 
+	private static $extensions_enabled = array();
+
 	/*
 	/*
 	 * Getteurs
 	 * Getteurs
 	 */
 	 */
@@ -133,6 +135,9 @@ class Minz_Configuration {
 	public static function unsafeAutologinEnabled() {
 	public static function unsafeAutologinEnabled() {
 		return self::$unsafe_autologin_enabled;
 		return self::$unsafe_autologin_enabled;
 	}
 	}
+	public static function extensionsEnabled() {
+		return self::$extensions_enabled;
+	}
 
 
 	public static function _allowAnonymous($allow = false) {
 	public static function _allowAnonymous($allow = false) {
 		self::$allow_anonymous = ((bool)$allow) && self::canLogIn();
 		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
 		// Base de données
 		if (isset ($ini_array['db'])) {
 		if (isset ($ini_array['db'])) {
 			$db = $ini_array['db'];
 			$db = $ini_array['db'];

+ 96 - 0
lib/Minz/Extension.php

@@ -0,0 +1,96 @@
+<?php
+
+/**
+ * The extension base class.
+ */
+class Minz_Extension {
+	private $name;
+	private $entrypoint;
+	private $path;
+	private $author;
+	private $description;
+	private $version;
+	private $type;
+
+	public static $authorized_types = array(
+		'system',
+		'user',
+	);
+
+	/**
+	 * The constructor to assign specific information to the extension.
+	 *
+	 * Available fields are:
+	 * - name: the name of the extension (required).
+	 * - entrypoint: the extension class name (required).
+	 * - path: the pathname to the extension files (required).
+	 * - author: the name and / or email address of the extension author.
+	 * - description: a short description to describe the extension role.
+	 * - version: a version for the current extension.
+	 * - type: "system" or "user" (default).
+	 *
+	 * It must not be redefined by child classes.
+	 *
+	 * @param $meta_info contains information about the extension.
+	 */
+	public function __construct($meta_info) {
+		$this->name = $meta_info['name'];
+		$this->entrypoint = $meta_info['entrypoint'];
+		$this->path = $meta_info['path'];
+		$this->author = isset($meta_info['author']) ? $meta_info['author'] : '';
+		$this->description = isset($meta_info['description']) ? $meta_info['description'] : '';
+		$this->version = isset($meta_info['version']) ? $meta_info['version'] : '0.1';
+		$this->setType(isset($meta_info['type']) ? $meta_info['type'] : 'user');
+	}
+
+	/**
+	 * 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;
+	}
+}

+ 15 - 0
lib/Minz/ExtensionException.php

@@ -0,0 +1,15 @@
+<?php
+
+class Minz_ExtensionException extends Minz_Exception {
+	public function __construct ($message, $extension_name = false, $code = self::ERROR) {
+		if ($extension_name) {
+			$message = 'An error occured in `' . $extension_name
+			         . '` extension with the message: ' . $message;
+		} else {
+			$message = 'An error occured in an unnamed '
+			         . 'extension with the message: ' . $message;
+		}
+
+		parent::__construct($message, $code);
+	}
+}

+ 150 - 0
lib/Minz/ExtensionManager.php

@@ -0,0 +1,150 @@
+<?php
+
+/**
+ * An extension manager to load extensions present in EXTENSIONS_PATH.
+ */
+class Minz_ExtensionManager {
+	private static $ext_metaname = 'metadata.json';
+	private static $ext_entry_point = 'extension.php';
+	private static $ext_list = array();
+	private static $ext_list_enabled = array();
+
+	private static $ext_auto_enabled = array();
+
+	/**
+	 * Initialize the extension manager by loading extensions in EXTENSIONS_PATH.
+	 *
+	 * A valid extension is a directory containing metadata.json and
+	 * extension.php files.
+	 * metadata.json is a JSON structure where the only required fields are
+	 * `name` and `entry_point`.
+	 * extension.php should contain at least a class named <name>Extension where
+	 * <name> must match with the entry point in metadata.json. This class must
+	 * inherit from Minz_Extension class.
+	 */
+	public static function init() {
+		$list_potential_extensions = array_values(array_diff(
+			scandir(EXTENSIONS_PATH),
+			array('..', '.')
+		));
+
+		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();
+		}
+	}
+}