Admidio / admidio

Admidio is a free open source user management system for websites of organizations and groups. The system has a flexible role model so that it’s possible to reflect the structure and permissions of your organization.
https://www.admidio.org
GNU General Public License v2.0
337 stars 131 forks source link

Plugin Interface #541

Open ximex opened 7 years ago

ximex commented 7 years ago

For a better handling we should define how a plugin should look like. I have coded some first ideas. But i think we shouldn't make all static. And we need a better config handling

PluginManager.php

<?php

namespace Admidio\Plugins;

/**
 * Class PluginsManager
 */
class PluginsManager
{
    private static $instance;

    protected $pluginsPath;

    /**
     *
     */
    protected function __construct()
    {
        $this->pluginsPath = \realpath(ADMIDIO_PATH . FOLDER_PLUGINS);
    }

    /**
     * Singleton Class! Stop cloning this class!
     */
    private function __clone()
    {

    }

    /**
     * Singleton Class! Stop unserializing this class!
     */
    private function __wakeup()
    {

    }

    /**
     * @return PluginsManager
     */
    public static function getInstance()
    {
        if (self::$instance === null)
        {
            self::$instance = new self();
        }

        return self::$instance;
    }

    /**
     * @return string[]
     */
    public function getAvailablePlugins()
    {
        $plugins = array();
        foreach (\scandir($this->pluginsPath) as $entry)
        {
            $pluginFolder = $this->pluginsPath . DIRECTORY_SEPARATOR . $entry;
            if (\is_dir($pluginFolder))
            {
                $pluginFile = $pluginFolder . DIRECTORY_SEPARATOR . 'Plugin.php';
                if (\is_file($pluginFile))
                {
                    $plugins[$entry] = $pluginFile;
                }
            }
        }

        return $plugins;
    }

    /**
     *
     */
    public function getInstalledPlugins()
    {

    }

    /**
     *
     */
    public function getActivePlugins()
    {

    }
}

iPlugin.php

<?php

namespace Admidio\Plugins;

/**
 * Interface iPlugin
 */
interface iPlugin
{
    /**
     * @return iPlugin
     */
    public static function getInstance();

    /**
     * @return string
     */
    public static function getName();

    /**
     * @return string
     */
    public static function getVersion();

    /**
     * @return array
     */
    public static function getMetadata();

    /**
     * @return array
     */
    public static function getDependencies();

    /**
     * @return array
     */
    public static function getSupportedLanguages();

    /**
     * @param string $type
     * @throws \InvalidArgumentException
     * @throws \Exception
     * @return array
     */
    public static function getStaticFiles($type = null);

    /**
     * @throws \Exception
     * @return bool
     */
    public static function isInstalled();

    /**
     * @throws \Exception
     * @return bool
     */
    public static function isActivated();

    /**
     * @throws \Exception
     * @return bool
     */
    public static function doClassAutoload();

    /**
     * @throws \Exception
     * @return bool
     */
    public static function doInstall();

    /**
     * @param array $options
     * @throws \InvalidArgumentException
     * @throws \Exception
     * @return bool
     */
    public static function doUninstall(array $options = array());

    /**
     * @throws \Exception
     * @return bool
     */
    public static function doUpdate();

    /**
     * @param array $config
     * @throws \InvalidArgumentException
     * @throws \Exception
     * @return bool
     */
    public static function doRender(array $config = array());
}

AbstractPlugin.php

<?php

namespace Admidio\Plugins;

/**
 * Class AbstractPlugin
 */
abstract class AbstractPlugin implements iPlugin
{
    private static $instances = array();

    protected static $name;
    protected static $version;
    protected static $dependencies = array();
    protected static $metadata = array();

    /**
     *
     */
    protected function __construct()
    {

    }

    /**
     * Singleton Class! Stop cloning this class!
     */
    private function __clone()
    {

    }

    /**
     * Singleton Class! Stop unserializing this class!
     */
    private function __wakeup()
    {

    }

    /**
     * @return AbstractPlugin
     */
    public static function getInstance()
    {
        $class = \get_called_class();
        if (!\array_key_exists($class, self::$instances))
        {
            self::$instances[$class] = new $class();
        }

        return self::$instances[$class];
    }

    /**
     * @return string
     */
    public static function getName()
    {
        return static::$name;
    }

    /**
     * @return string
     */
    public static function getVersion()
    {
        return static::$version;
    }

    /**
     * @return array
     */
    public static function getMetadata()
    {
        return static::$metadata;
    }

    /**
     * @return array
     */
    public static function getDependencies()
    {
        return static::$dependencies;
    }

    /**
     * @return array
     */
    public static function getSupportedLanguages()
    {
        $dir = __DIR__ . DIRECTORY_SEPARATOR . 'languages';

        $langFiles = array();
        foreach (\scandir($dir) as $entry)
        {
            $entryPath = $dir . DIRECTORY_SEPARATOR . $entry;
            $entryInfo = \pathinfo($entryPath);
            if (\is_file($entryPath) && $entryInfo['extension'] === 'xml')
            {
                $langFiles[] = $entryInfo['filename'];
            }
        }

        return $langFiles;
    }

    /**
     * @param string $type
     * @throws \InvalidArgumentException
     * @throws \Exception
     * @return array
     */
    public static function getStaticFiles($type = null)
    {
        if ($type !== null && !\is_string($type))
        {
            throw new \InvalidArgumentException('Type must be "null" or a "string".');
        }

        $files = array();
        foreach (\scandir(__DIR__) as $entry)
        {
            $entryPath = __DIR__ . DIRECTORY_SEPARATOR . $entry;
            if (\is_file($entryPath))
            {
                $entryInfo = \pathinfo($entryPath);

                if (!\array_key_exists($entryInfo['extension'], $files))
                {
                    $files[$entryInfo['extension']] = array();
                }

                $files[$entryInfo['extension']][] = $entryPath;
            }
        }

        if ($type === null)
        {
            return $files;
        }
        else
        {
            return $files[$type];
        }
    }

    /**
     * @throws \Exception
     * @return bool
     */
    public static function isInstalled()
    {
        // TODO
        return true;
    }

    /**
     * @throws \Exception
     * @return bool
     */
    public static function isActivated()
    {
        // TODO
        return true;
    }

    /**
     * @throws \Exception
     * @return bool
     */
    public static function doClassAutoload()
    {
        $autoloadPath = __DIR__ . '/autoload.php';

        if (\is_file($autoloadPath))
        {
            require_once($autoloadPath);

            return true;
        }

        return false;
    }

    /**
     * @throws \Exception
     * @return bool
     */
    public static function doInstall()
    {
        return false;
    }

    /**
     * @param array $options
     * @throws \InvalidArgumentException
     * @throws \Exception
     * @return bool
     */
    public static function doUninstall(array $options = array())
    {
        if (!\is_array($options))
        {
            throw new \InvalidArgumentException('Options must be an "array".');
        }

        return false;
    }

    /**
     * @throws \Exception
     * @return bool
     */
    public static function doUpdate()
    {
        return false;
    }
}

LoginFormPlugin.php

<?php

namespace Admidio\Plugins;

/**
 * Class LoginFormPlugin
 */
class LoginFormPlugin extends AbstractPlugin
{
    protected static $name = 'LoginForm';
    protected static $version = '1.0.0';
    protected static $dependencies = array(
        'Admidio' => '^3.3.0'
    );
    protected static $metadata = array(
        'author'      => 'Admidio Team',
        'releaseDate' => '2017-02-01'
    );

    /**
     * @param array $config
     * @throws \InvalidArgumentException
     * @throws \Exception
     * @return bool
     */
    public static function doRender(array $config = array())
    {
        if (!\is_array($config))
        {
            throw new \InvalidArgumentException('Config must be an "array".');
        }

        require(__DIR__ . '/login_form.php');

        return true;
    }
}
thomas-rcv commented 7 years ago

Also thought about this longer time ago. The question was for me how to realize a plugin manager. It should be easy for people with less knowledge. Just copy the plugin to folder an Admidio detects the new Plugin and handles install/uninstall in the system settings. The plugin will be added to the component table and store plugins config variables also .... therefor the plugins must follow a strict structure for its variables to be taken to the prederences. E.g. it must provide an config.xml wirh all parameters. \<Author>, \<Pluginname>, \<Version> ... Also all config parameters: \<param> , \<type>, \<defaultValue> ... The parameters table should be stored by our backup script .... Just as idea ... Sounds great for me

ximex commented 7 years ago

Updated my implementation above.

Possible "default config" with overriding this: (This is my idea for a future Admidio config (v4.0); not a plugin; but it shows my idea)

$defaultConfig = array(
    'database' => array(
        'type'        => 'mysql', // "mysql", "pgsql"
        'host'        => 'localhost',
        'port'        => null,
        'username'    => null,
        'password'    => null,
        'database'    => 'admidio',
        'tablePrefix' => 'adm'
    ),
    'passwordOptions' => array(
        'hashAlgorithm'  => 'DEFAULT', // "DEFAULT", "BCRYPT", "SHA512"
        'minLength'      => 8,
        'minComplexity'  => 1, // 0, 1, 2, 3, 4 => zxcvbn
        'generateLength' => 16,
        'generateChars'  => '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
    ),
    'security' => array(
        'authForUpdate' => true,
        'forceHTTPS'    => false,
        'secureProxy'   => null
    ),
    'timezone'     => 'Europe/Berlin', // TODO: null?
    'organization' => null, // TODO: necessary?
    'debug'        => false,
    'logging'      => array(
        'level'    => 'WARNING',
        'maxFiles' => 0,
        'path'     => null
    )
);

$config = array_replace($defaultConfig, $newConfig);

Other idea for saving a custom config in the database is to use "namespaced keys" like:

key value type
database.type mysql string
database.host localhost string
plugins.login_form.password.minlength 8 int
plugins.xyz.list 1,3,6,9 int[]
... ....

So it is possible to map this entries to an config array for above like:

$newConfig = array(
    'database' => array(
        'type' => 'mysql',
        'host' => 'localhost'
    ),
    'plugins' => array(
        'login_form' => array(
            'password' => array(
                'minlength' => 8
            )
        ),
        'xyz' => array(
            'list' => array(1, 3, 6, 9)
        )
    )
);

The problem with the value type could be solved with another type column in the database with: bool, int, float, string, bool[], int[], float[], string[]

Please give feedback

thomas-rcv commented 7 years ago

Personally I would prefer the second way, but it definitly depends on our requirements how the Plugin Manager should look like. The first way with overwriting the default config is still ok, if the plugins are customized handish in the config files. In my point of view we should drop this and make it more easier handling in the admin preferencens GUI. The modules are already customizable there. Also installed plugins should be moved to this tool. Therefor the plugins must follow a strict architectural guideline and must provide an idoc file with all elemantary data and config of parameters. Doing so, the paramaeters must be handled in the database ( 2. way). This would be managed by an installer/uninstaller. The GUI makes it costumizable on Admidio surface. No needs to use editors the config files anymore. This would be a proper way in my point of view. Maybe we should create a new Project on Git and discuss and work step by step? May be it is a good idea to create an UML of the planned Software architecture of the planned Manager ...