Seditio Source
Root |
./othercms/dotclear-2.22/inc/core/class.dc.modules.php
<?php
/**
 * @brief Modules handler
 *
 * Provides an object to handle modules (themes or plugins).
 *
 * @package Dotclear
 * @subpackage Core
 *
 * @copyright Olivier Meunier & Association Dotclear
 * @copyright GPL-2.0-only
 */
if (!defined('DC_RC_PATH')) {
    return;
}

class
dcModules
{
    protected
$path;
    protected
$ns;
    protected
$modules       = [];
    protected
$disabled      = [];
    protected
$errors        = [];
    protected
$modules_names = [];
    protected
$all_modules   = [];
    protected
$disabled_mode = false;
    protected
$disabled_meta = [];
    protected
$to_disable    = [];

    protected
$id;
    protected
$mroot;

   
# Inclusion variables
   
protected static $superglobals = ['GLOBALS', '_SERVER', '_GET', '_POST', '_COOKIE', '_FILES', '_ENV', '_REQUEST', '_SESSION'];
    protected static
$_k;
    protected static
$_n;

    protected static
$type = null;

    public
$core; ///< <b>dcCore</b>    dcCore instance

    /**
     * Constructs a new instance.
     *
     * @param      dcCore  $core   The core
     */
   
public function __construct(dcCore $core)
    {
       
$this->core = &$core;
    }

   
/**
     * Checks all modules dependencies
     *
     *     Fills in the following information in module :
     *       * cannot_enable : list reasons why module cannot be enabled. Not set if module can be enabled
     *       * cannot_disable : list reasons why module cannot be disabled. Not set if module can be disabled
     *       * implies : reverse dependencies
     */
   
public function checkDependencies()
    {
       
$dc_version       = preg_replace('/\-dev.*$/', '', DC_VERSION);
       
$this->to_disable = [];
        foreach (
$this->all_modules as $k => &$m) {
            if (isset(
$m['requires'])) {
               
$missing = [];
                foreach (
$m['requires'] as &$dep) {
                    if (!
is_array($dep)) {
                       
$dep = [$dep];
                    }
                   
// grab missing dependencies
                   
if (!isset($this->all_modules[$dep[0]]) && ($dep[0] != 'core')) {
                       
// module not present
                       
$missing[$dep[0]] = sprintf(__('Requires %s module which is not installed'), $dep[0]);
                    } elseif ((
count($dep) > 1) && version_compare(($dep[0] == 'core' ? $dc_version : $this->all_modules[$dep[0]]['version']), $dep[1]) == -1) {
                       
// module present, but version missing
                       
if ($dep[0] == 'core') {
                           
$missing[$dep[0]] = sprintf(
                               
__('Requires Dotclear version %s, but version %s is installed'),
                               
$dep[1],
                               
$dc_version
                           
);
                        } else {
                           
$missing[$dep[0]] = sprintf(
                               
__('Requires %s module version %s, but version %s is installed'),
                               
$dep[0],
                               
$dep[1],
                               
$this->all_modules[$dep[0]]['version']
                            );
                        }
                    } elseif ((
$dep[0] != 'core') && !$this->all_modules[$dep[0]]['enabled']) {
                       
// module disabled
                       
$missing[$dep[0]] = sprintf(__('Requires %s module which is disabled'), $dep[0]);
                    }
                   
$this->all_modules[$dep[0]]['implies'][] = $k;
                }
                if (
count($missing)) {
                   
$m['cannot_enable'] = $missing;
                    if (
$m['enabled']) {
                       
$this->to_disable[] = ['name' => $k, 'reason' => $missing];
                    }
                }
            }
        }
       
// Check modules that cannot be disabled
       
foreach ($this->modules as $k => &$m) {
            if (isset(
$m['implies']) && $m['enabled']) {
                foreach (
$m['implies'] as $im) {
                    if (isset(
$this->all_modules[$im]) && $this->all_modules[$im]['enabled']) {
                       
$m['cannot_disable'][] = $im;
                    }
                }
            }
        }
    }

   
/**
     * Checks all modules dependencies, and disable unmet dependencies
     */
    /**
     * Disables the dep modules.
     *
     * @param  string $redir_url URL to redirect if modules are to disable
     *
     * @return bool  true if a redirection has been performed
     */
   
public function disableDepModules($redir_url)
    {
        if (isset(
$_GET['dep'])) {
           
// Avoid infinite redirects
           
return false;
        }
       
$reason = [];
        foreach (
$this->to_disable as $module) {
            try {
               
$this->deactivateModule($module['name']);
               
$reason[] = sprintf('<li>%s : %s</li>', $module['name'], join(',', $module['reason']));
            } catch (
Exception $e) {
            }
        }
        if (
count($reason)) {
           
$message = sprintf(
               
'<p>%s</p><ul>%s</ul>',
               
__('The following extensions have been disabled :'),
               
join('', $reason)
            );
           
dcPage::addWarningNotice($message, ['divtag' => true, 'with_ts' => false]);
           
$url = $redir_url . (strpos($redir_url, '?') ? '&' : '?') . 'dep=1';
           
http::redirect($url);

            return
true;
        }

        return
false;
    }

   
/**
     * Loads modules. <var>$path</var> could be a separated list of paths
     * (path separator depends on your OS).
     *
     * <var>$ns</var> indicates if an additionnal file needs to be loaded on plugin
     * load, value could be:
     * - admin (loads module's _admin.php)
     * - public (loads module's _public.php)
     * - xmlrpc (loads module's _xmlrpc.php)
     *
     * <var>$lang</var> indicates if we need to load a lang file on plugin
     * loading.
     *
     * @param      string  $path   The path
     * @param      mixed   $ns
     * @param      mixed   $lang   The language
     */
   
public function loadModules($path, $ns = null, $lang = null)
    {
       
$this->path = explode(PATH_SEPARATOR, $path);
       
$this->ns   = $ns;

       
$disabled = isset($_SESSION['sess_safe_mode']) && $_SESSION['sess_safe_mode'];
       
$disabled = $disabled                          && !get_parent_class($this) ? true : false;  // @phpstan-ignore-line

       
$ignored = [];

        foreach (
$this->path as $root) {
            if (!
is_dir($root) || !is_readable($root)) {
                continue;
            }

            if (
substr($root, -1) != '/') {
               
$root .= '/';
            }

            if ((
$d = @dir($root)) === false) {
                continue;
            }

            while ((
$entry = $d->read()) !== false) {
               
$full_entry = $root . $entry;

                if (
$entry != '.' && $entry != '..' && is_dir($full_entry) && file_exists($full_entry . '/_define.php')) {
                   
$this->id       = $entry;
                   
$this->mroot    = $full_entry;
                   
$module_enabled = !file_exists($full_entry . '/_disabled') && !$disabled;
                    if (!
$module_enabled) {
                       
$this->disabled_mode = true;
                    }
                   
ob_start();
                    require
$full_entry . '/_define.php';
                   
ob_end_clean();
                    if (
$module_enabled) {
                       
$this->all_modules[$entry] = &$this->modules[$entry];
                    } else {
                       
$this->disabled_mode       = false;
                       
$this->disabled[$entry]    = $this->disabled_meta;
                       
$this->all_modules[$entry] = &$this->disabled[$entry];
                    }
                   
$this->id    = null;
                   
$this->mroot = null;
                }
            }
           
$d->close();
        }
       
$this->checkDependencies();
       
# Sort plugins
       
uasort($this->modules, [$this, 'sortModules']);

        foreach (
$this->modules as $id => $m) {
           
# Load translation and _prepend
           
if (isset($m['root']) && file_exists($m['root'] . '/_prepend.php')) {
               
$r = $this->loadModuleFile($m['root'] . '/_prepend.php');

               
# If _prepend.php file returns null (ie. it has a void return statement)
               
if (is_null($r)) {
                   
$ignored[] = $id;

                    continue;
                }
                unset(
$r);
            }

           
$this->loadModuleL10N($id, $lang, 'main');
            if (
$ns == 'admin') {
               
$this->loadModuleL10Nresources($id, $lang);
               
$this->core->adminurl->register('admin.plugin.' . $id, 'plugin.php', ['p' => $id]);
            }
        }

       
// Give opportunity to do something before loading context (admin,public,xmlrpc) files
       
$this->core->callBehavior('coreBeforeLoadingNsFiles', $this->core, $this, $lang);

        foreach (
$this->modules as $id => $m) {
           
# If _prepend.php file returns null (ie. it has a void return statement)
           
if (in_array($id, $ignored)) {
                continue;
            }
           
# Load ns_file
           
$this->loadNsFile($id, $ns);
        }
    }

    public function
requireDefine($dir, $id)
    {
        if (
file_exists($dir . '/_define.php')) {
           
$this->id = $id;
           
ob_start();
            require
$dir . '/_define.php';
           
ob_end_clean();
           
$this->id = null;
        }
    }

   
/**
     * This method registers a module in modules list. You should use this to
     * register a new module.
     *
     * <var>$permissions</var> is a comma separated list of permissions for your
     * module. If <var>$permissions</var> is null, only super admin has access to
     * this module.
     *
     * <var>$priority</var> is an integer. Modules are sorted by priority and name.
     * Lowest priority comes first.
     *
     * @param      string  $name        The module name
     * @param      string  $desc        The module description
     * @param      string  $author      The module author
     * @param      string  $version     The module version
     * @param      mixed   $properties  The properties
     */
   
public function registerModule($name, $desc, $author, $version, $properties = [])
    {
        if (
$this->disabled_mode) {
           
$this->disabled_meta = array_merge(
               
$properties,
                [
                   
'root'          => $this->mroot,
                   
'name'          => $name,
                   
'desc'          => $desc,
                   
'author'        => $author,
                   
'version'       => $version,
                   
'enabled'       => false,
                   
'root_writable' => is_writable($this->mroot),
                ]
            );

            return;
        }
       
# Fallback to legacy registerModule parameters
       
if (!is_array($properties)) {
           
$args       = func_get_args();
           
$properties = [];
            if (isset(
$args[4])) {
               
$properties['permissions'] = $args[4];
            }
            if (isset(
$args[5])) {
               
$properties['priority'] = (int) $args[5];
            }
        }

       
# Default module properties
       
$properties = array_merge(
            [
               
'permissions'       => null,
               
'priority'          => 1000,
               
'standalone_config' => false,
               
'type'              => null,
               
'enabled'           => true,
               
'requires'          => [],
               
'settings'          => [],
               
'repository'        => '',
            ],
           
$properties
       
);

       
# Check module type
       
if (self::$type !== null && $properties['type'] !== null && $properties['type'] != self::$type) {
           
$this->errors[] = sprintf(
               
__('Module "%s" has type "%s" that mismatch required module type "%s".'),
               
'<strong>' . html::escapeHTML($name) . '</strong>',
               
'<em>' . html::escapeHTML($properties['type']) . '</em>',
               
'<em>' . html::escapeHTML(self::$type) . '</em>'
           
);

            return;
        }

       
# Check module perms on admin side
       
$permissions = $properties['permissions'];
        if (
$this->ns == 'admin') {
            if (
$permissions == '' && !$this->core->auth->isSuperAdmin()) {
                return;
            } elseif (!
$this->core->auth->check($permissions, $this->core->blog->id)) {
                return;
            }
        }

       
# Check module install on multiple path
       
if ($this->id) {
           
$module_exists    = array_key_exists($name, $this->modules_names);
           
$module_overwrite = $module_exists ? version_compare($this->modules_names[$name], $version, '<') : false;
            if (!
$module_exists || $module_overwrite) {
               
$this->modules_names[$name] = $version;
               
$this->modules[$this->id]   = array_merge(
                   
$properties,
                    [
                       
'root'          => $this->mroot,
                       
'name'          => $name,
                       
'desc'          => $desc,
                       
'author'        => $author,
                       
'version'       => $version,
                       
'root_writable' => is_writable($this->mroot ?? ''),
                    ]
                );
            } else {
               
$path1          = path::real($this->moduleInfo($name, 'root') ?? '');
               
$path2          = path::real($this->mroot ?? '');
               
$this->errors[] = sprintf(
                   
__('Module "%s" is installed twice in "%s" and "%s".'),
                   
'<strong>' . $name . '</strong>',
                   
'<em>' . $path1 . '</em>',
                   
'<em>' . $path2 . '</em>'
               
);
            }
        }
    }

   
/**
     * Reset modules list
     */
   
public function resetModulesList()
    {
       
$this->modules       = [];
       
$this->modules_names = [];
       
$this->errors        = [];
    }

   
/**
     * Install a Package
     *
     * @param      string     $zip_file  The zip file
     * @param      dcModules  $modules   The modules
     *
     * @throws     Exception
     *
     * @return     int
     */
   
public static function installPackage($zip_file, dcModules &$modules)
    {
       
$zip = new fileUnzip($zip_file);
       
$zip->getList(false, '#(^|/)(__MACOSX|\.svn|\.hg.*|\.git.*|\.DS_Store|\.directory|Thumbs\.db)(/|$)#');

       
$zip_root_dir = $zip->getRootDir();
       
$define       = '';
        if (
$zip_root_dir != false) {
           
$target      = dirname($zip_file);
           
$destination = $target . '/' . $zip_root_dir;
           
$define      = $zip_root_dir . '/_define.php';
           
$has_define  = $zip->hasFile($define);
        } else {
           
$target      = dirname($zip_file) . '/' . preg_replace('/\.([^.]+)$/', '', basename($zip_file));
           
$destination = $target;
           
$define      = '_define.php';
           
$has_define  = $zip->hasFile($define);
        }

        if (
$zip->isEmpty()) {
           
$zip->close();
           
unlink($zip_file);

            throw new
Exception(__('Empty module zip file.'));
        }

        if (!
$has_define) {
           
$zip->close();
           
unlink($zip_file);

            throw new
Exception(__('The zip file does not appear to be a valid Dotclear module.'));
        }

       
$ret_code = 1;

        if (!
is_dir($destination)) {
            try {
               
files::makeDir($destination, true);

               
$sandbox = clone $modules;
               
$zip->unzip($define, $target . '/_define.php');

               
$sandbox->resetModulesList();
               
$sandbox->requireDefine($target, basename($destination));
               
unlink($target . '/_define.php');

               
$new_errors = $sandbox->getErrors();
                if (!empty(
$new_errors)) {
                   
$new_errors = implode(" \n", $new_errors);

                    throw new
Exception($new_errors);
                }

               
files::deltree($destination);
            } catch (
Exception $e) {
               
$zip->close();
               
unlink($zip_file);
               
files::deltree($destination);

                throw new
Exception($e->getMessage());
            }
        } else {
           
# test for update
           
$sandbox = clone $modules;
           
$zip->unzip($define, $target . '/_define.php');

           
$sandbox->resetModulesList();
           
$sandbox->requireDefine($target, basename($destination));
           
unlink($target . '/_define.php');
           
$new_modules = $sandbox->getModules();

            if (!empty(
$new_modules)) {
               
$tmp        = array_keys($new_modules);
               
$id         = $tmp[0];
               
$cur_module = $modules->getModules($id);
                if (!empty(
$cur_module) && (defined('DC_DEV') && DC_DEV === true || dcUtils::versionsCompare($new_modules[$id]['version'], $cur_module['version'], '>', true))) {
                   
# delete old module
                   
if (!files::deltree($destination)) {
                        throw new
Exception(__('An error occurred during module deletion.'));
                    }
                   
$ret_code = 2;
                } else {
                   
$zip->close();
                   
unlink($zip_file);

                    throw new
Exception(sprintf(__('Unable to upgrade "%s". (older or same version)'), basename($destination)));
                }
            } else {
               
$zip->close();
               
unlink($zip_file);

                throw new
Exception(sprintf(__('Unable to read new _define.php file')));
            }
        }
       
$zip->unzipAll($target);
       
$zip->close();
       
unlink($zip_file);

        return
$ret_code;
    }

   
/**

     */
    /**
     * This method installs all modules having a _install file.
     *
     * @see dcModules::installModule
     *
     * @return     array
     */
   
public function installModules()
    {
       
$res = ['success' => [], 'failure' => []];
        foreach (
$this->modules as $id => &$m) {
           
$i = $this->installModule($id, $msg);
            if (
$i === true) {
               
$res['success'][$id] = true;
            } elseif (
$i === false) {
               
$res['failure'][$id] = $msg;
            }
        }

        return
$res;
    }

   
/**
     * This method installs module with ID <var>$id</var> and having a _install
     * file. This file should throw exception on failure or true if it installs
     * successfully.
     *
     * <var>$msg</var> is an out parameter that handle installer message.
     *
     * @param      string  $id     The identifier
     * @param      string  $msg    The message
     *
     * @return     mixed
     */
   
public function installModule($id, &$msg)
    {
        if (!isset(
$this->modules[$id])) {
            return;
        }

        try {
           
$i = $this->loadModuleFile($this->modules[$id]['root'] . '/_install.php');
            if (
$i === true) {
                return
true;
            }
        } catch (
Exception $e) {
           
$msg = $e->getMessage();

            return
false;
        }
    }

   
/**
     * Delete a module
     *
     * @param      string     $id        The module identifier
     * @param      bool       $disabled  Is module disabled
     *
     * @throws     Exception  (description)
     */
   
public function deleteModule($id, $disabled = false)
    {
        if (
$disabled) {
           
$p = &$this->disabled;
        } else {
           
$p = &$this->modules;
        }

        if (!isset(
$p[$id])) {
            throw new
Exception(__('No such module.'));
        }

        if (!
files::deltree($p[$id]['root'])) {
            throw new
Exception(__('Cannot remove module files'));
        }
    }

   
/**
     * Deactivate a module
     *
     * @param      string     $id     The identifier
     *
     * @throws     Exception
     */
   
public function deactivateModule($id)
    {
        if (!isset(
$this->modules[$id])) {
            throw new
Exception(__('No such module.'));
        }

        if (!
$this->modules[$id]['root_writable']) {
            throw new
Exception(__('Cannot deactivate plugin.'));
        }

        if (@
file_put_contents($this->modules[$id]['root'] . '/_disabled', '')) {
            throw new
Exception(__('Cannot deactivate plugin.'));
        }
    }

   
/**
     * Activate a module
     *
     * @param      string     $id     The identifier
     *
     * @throws     Exception
     */
   
public function activateModule($id)
    {
        if (!isset(
$this->disabled[$id])) {
            throw new
Exception(__('No such module.'));
        }

        if (!
$this->disabled[$id]['root_writable']) {
            throw new
Exception(__('Cannot activate plugin.'));
        }

        if (@
unlink($this->disabled[$id]['root'] . '/_disabled') === false) {
            throw new
Exception(__('Cannot activate plugin.'));
        }
    }

   
/**
     * Clone a module
     *
     * @param      string  $id     The module identifier
     */
   
public function cloneModule($id)
    {
    }

   
/**
     * This method will search for file <var>$file</var> in language
     * <var>$lang</var> for module <var>$id</var>.
     *
     * <var>$file</var> should not have any extension.
     *
     * @param      string  $id     The module identifier
     * @param      string  $lang   The language code
     * @param      string  $file   The filename (without extension)
     */
   
public function loadModuleL10N($id, $lang, $file)
    {
        if (!
$lang || !isset($this->modules[$id])) {
            return;
        }

       
$lfile = $this->modules[$id]['root'] . '/locales/%s/%s';
        if (
l10n::set(sprintf($lfile, $lang, $file)) === false && $lang != 'en') {
           
l10n::set(sprintf($lfile, 'en', $file));
        }
    }

   
/**
     * Loads module l10n resources.
     *
     * @param      string  $id     The module identifier
     * @param      string  $lang   The language code
     */
   
public function loadModuleL10Nresources($id, $lang)
    {
        if (!
$lang || !isset($this->modules[$id])) {
            return;
        }

       
$f = l10n::getFilePath($this->modules[$id]['root'] . '/locales', 'resources.php', $lang);
        if (
$f) {
           
$this->loadModuleFile($f);
        }
    }

   
/**
     * Returns all modules associative array or only one module if <var>$id</var>
     * is present.
     *
     * @param      mixed  $id     The optionnal module identifier
     *
     * @return     mixed  The modules.
     */
   
public function getModules($id = null)
    {
        if (
$id && isset($this->modules[$id])) {
            return
$this->modules[$id];
        }

        return
$this->modules;
    }

   
/**
     * Determines if module exists.
     *
     * @param      string  $id     The module identifier
     *
     * @return     bool  True if module exists, False otherwise.
     */
   
public function moduleExists($id)
    {
        return isset(
$this->modules[$id]);
    }

   
/**
     * Gets the disabled modules.
     *
     * @return     array  The disabled modules.
     */
   
public function getDisabledModules()
    {
        return
$this->disabled;
    }

   
/**
     * Returns root path for module with ID <var>$id</var>.
     *
     * @param      string  $id     The module identifier
     *
     * @return     string
     */
   
public function moduleRoot($id)
    {
        return
$this->moduleInfo($id, 'root');
    }

   
/**
     * Returns a module information that could be:
     * - root
     * - name
     * - desc
     * - author
     * - version
     * - permissions
     * - priority
     * - …
     *
     * @param      string  $id     The module identifier
     * @param      string  $info   The information
     *
     * @return     mixed
     */
   
public function moduleInfo($id, $info)
    {
        return
$this->modules[$id][$info] ?? null;
    }

   
/**
     * Loads namespace <var>$ns</var> specific files for all modules.
     *
     * @param      mixed  $ns
     */
   
public function loadNsFiles($ns = null)
    {
        foreach (
$this->modules as $k => $v) {
           
$this->loadNsFile($k, $ns);
        }
    }

   
/**
     * Loads namespace <var>$ns</var> specific file for module with ID
     * <var>$id</var>
     *
     * @param      string  $id     The module identifier
     * @param      mixed   $ns     Namespace name
     */
   
public function loadNsFile($id, $ns = null)
    {
        if (!isset(
$this->modules[$id])) {
            return;
        }
        switch (
$ns) {
            case
'admin':
               
$this->loadModuleFile($this->modules[$id]['root'] . '/_admin.php');

                break;
            case
'public':
               
$this->loadModuleFile($this->modules[$id]['root'] . '/_public.php');

                break;
            case
'xmlrpc':
               
$this->loadModuleFile($this->modules[$id]['root'] . '/_xmlrpc.php');

                break;
        }
    }

   
/**
     * Gets the errors.
     *
     * @return     array  The errors.
     */
   
public function getErrors()
    {
        return
$this->errors;
    }

    protected function
loadModuleFile($________, $catch = true)
    {
        if (!
file_exists($________)) {
            return;
        }

       
self::$_k = array_keys($GLOBALS);

        foreach (
self::$_k as self::$_n) {
            if (!
in_array(self::$_n, self::$superglobals)) {
                global ${
self::$_n};
            }
        }

        if (
$catch) {
           
// Catch ouput to prevents hacked or corrupted modules
           
ob_start();
           
$ret = require $________;
           
ob_end_clean();

            return
$ret;
        }

        return require
$________;
    }

    private function
sortModules($a, $b)
    {
        if (!isset(
$a['priority']) || !isset($b['priority'])) {
            return
1;
        }
        if (
$a['priority'] == $b['priority']) {
            return
strcasecmp($a['name'], $b['name']);
        }

        return (
$a['priority'] < $b['priority']) ? -1 : 1;
    }
}