فهرست منبع

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 سال پیش
والد
کامیت
86f69ca396
7فایلهای تغییر یافته به همراه295 افزوده شده و 21 حذف شده
  1. 22 20
      app/FreshRSS.php
  2. 2 1
      constants.php
  3. 0 0
      extensions/README.md
  4. 10 0
      lib/Minz/Configuration.php
  5. 96 0
      lib/Minz/Extension.php
  6. 15 0
      lib/Minz/ExtensionException.php
  7. 150 0
      lib/Minz/ExtensionManager.php

+ 22 - 20
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
+	// 			} 
+	// 		}
+	// 	}
+	// }
 }

+ 2 - 1
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());

+ 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,
 	);
 
+	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'];

+ 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();
+		}
+	}
+}