Dotclear

source: inc/core/class.dc.modules.php @ 3730:5c45a5df9a59

Revision 3730:5c45a5df9a59, 24.9 KB checked in by franck <carnet.franck.paul@…>, 7 years ago (diff)

Code formatting (PSR-2)

Line 
1<?php
2# -- BEGIN LICENSE BLOCK ---------------------------------------
3#
4# This file is part of Dotclear 2.
5#
6# Copyright (c) 2003-2013 Olivier Meunier & Association Dotclear
7# Licensed under the GPL version 2.0 license.
8# See LICENSE file or
9# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
10#
11# -- END LICENSE BLOCK -----------------------------------------
12if (!defined('DC_RC_PATH')) {return;}
13
14/**
15@ingroup DC_CORE
16@brief Modules handler
17
18Provides an object to handle modules (themes or plugins).
19 */
20class dcModules
21{
22    protected $path;
23    protected $ns;
24    protected $modules       = array();
25    protected $disabled      = array();
26    protected $errors        = array();
27    protected $modules_names = array();
28    protected $all_modules   = array();
29    protected $disabled_mode = false;
30    protected $disabled_meta = array();
31    protected $to_disable    = array();
32
33    protected $id;
34    protected $mroot;
35
36    # Inclusion variables
37    protected static $superglobals = array('GLOBALS', '_SERVER', '_GET', '_POST', '_COOKIE', '_FILES', '_ENV', '_REQUEST', '_SESSION');
38    protected static $_k;
39    protected static $_n;
40
41    protected static $type = null;
42
43    public $core; ///< <b>dcCore</b>    dcCore instance
44
45    /**
46    Object constructor.
47
48    @param    core        <b>dcCore</b>    dcCore instance
49     */
50    public function __construct($core)
51    {
52        $this->core = &$core;
53    }
54
55    /**
56     * Checks all modules dependencies
57     *     Fills in the following information in module :
58     *       * cannot_enable : list reasons why module cannot be enabled. Not set if module can be enabled
59     *       * cannot_disable : list reasons why module cannot be disabled. Not set if module can be disabled
60     *       * implies : reverse dependencies
61     * @return array list of enabled modules with unmet dependencies, and that must be disabled.
62     */
63    public function checkDependencies()
64    {
65        $dc_version       = preg_replace('/\-dev$/', '', DC_VERSION);
66        $this->to_disable = array();
67        foreach ($this->all_modules as $k => &$m) {
68            if (isset($m['requires'])) {
69                $missing = array();
70                foreach ($m['requires'] as &$dep) {
71                    if (!is_array($dep)) {
72                        $dep = array($dep);
73                    }
74                    // grab missing dependencies
75                    if (!isset($this->all_modules[$dep[0]]) && ($dep[0] != 'core')) {
76                        // module not present
77                        $missing[$dep[0]] = sprintf(__("Requires %s module which is not installed"), $dep[0]);
78                    } elseif ((count($dep) > 1) &&
79                        version_compare(($dep[0] == 'core' ? $dc_version : $this->all_modules[$dep[0]]['version']), $dep[1]) == -1) {
80                        // module present, but version missing
81                        if ($dep[0] == 'core') {
82                            $missing[$dep[0]] = sprintf(__("Requires Dotclear version %s, but version %s is installed"),
83                                $dep[1], $dc_version);
84                        } else {
85                            $missing[$dep[0]] = sprintf(__("Requires %s module version %s, but version %s is installed"),
86                                $dep[0], $dep[1], $this->all_modules[$dep[0]]['version']);
87                        }
88                    } elseif (($dep[0] != 'core') && !$this->all_modules[$dep[0]]['enabled']) {
89                        // module disabled
90                        $missing[$dep[0]] = sprintf(__("Requires %s module which is disabled"), $dep[0]);
91                    }
92                    $this->all_modules[$dep[0]]['implies'][] = $k;
93                }
94                if (count($missing)) {
95                    $m['cannot_enable'] = $missing;
96                    if ($m['enabled']) {
97                        $this->to_disable[] = array('name' => $k, 'reason' => $missing);
98                    }
99                }
100            }
101        }
102        // Check modules that cannot be disabled
103        foreach ($this->modules as $k => &$m) {
104            if (isset($m['implies']) && $m['enabled']) {
105                foreach ($m['implies'] as $im) {
106                    if (isset($this->all_modules[$im]) && $this->all_modules[$im]['enabled']) {
107                        $m['cannot_disable'][] = $im;
108                    }
109                }
110            }
111        }
112    }
113
114    /**
115     * Checks all modules dependencies, and disable unmet dependencies
116     * @param  string $redir_url URL to redirect if modules are to disable
117     * @return boolean, true if a redirection has been performed
118     */
119    public function disableDepModules($redir_url)
120    {
121        if (isset($_GET['dep'])) {
122            // Avoid infinite redirects
123            return false;
124        }
125        $reason = array();
126        foreach ($this->to_disable as $module) {
127            try {
128                $this->deactivateModule($module['name']);
129                $reason[] = sprintf("<li>%s : %s</li>", $module['name'], join(',', $module['reason']));
130            } catch (Exception $e) {
131            }
132        }
133        if (count($reason)) {
134            $message = sprintf("<p>%s</p><ul>%s</ul>",
135                __('The following extensions have been disabled :'),
136                join('', $reason)
137            );
138            dcPage::addWarningNotice($message, array('divtag' => true, 'with_ts' => false));
139            $url = $redir_url . (strpos($redir_url, "?") ? '&' : '?') . 'dep=1';
140            http::redirect($url);
141            return true;
142        }
143        return false;
144    }
145
146    /**
147    Loads modules. <var>$path</var> could be a separated list of paths
148    (path separator depends on your OS).
149
150    <var>$ns</var> indicates if an additionnal file needs to be loaded on plugin
151    load, value could be:
152    - admin (loads module's _admin.php)
153    - public (loads module's _public.php)
154    - xmlrpc (loads module's _xmlrpc.php)
155
156    <var>$lang</var> indicates if we need to load a lang file on plugin
157    loading.
158     */
159    public function loadModules($path, $ns = null, $lang = null)
160    {
161        $this->path = explode(PATH_SEPARATOR, $path);
162        $this->ns   = $ns;
163
164        $disabled = isset($_SESSION['sess_safe_mode']) && $_SESSION['sess_safe_mode'];
165        $disabled = $disabled && !get_parent_class($this) ? true : false;
166
167        $ignored = array();
168
169        foreach ($this->path as $root) {
170            if (!is_dir($root) || !is_readable($root)) {
171                continue;
172            }
173
174            if (substr($root, -1) != '/') {
175                $root .= '/';
176            }
177
178            if (($d = @dir($root)) === false) {
179                continue;
180            }
181
182            while (($entry = $d->read()) !== false) {
183                $full_entry = $root . $entry;
184
185                if ($entry != '.' && $entry != '..' && is_dir($full_entry)
186                    && file_exists($full_entry . '/_define.php')) {
187                    if (!file_exists($full_entry . '/_disabled') && !$disabled) {
188                        $this->id    = $entry;
189                        $this->mroot = $full_entry;
190                        ob_start();
191                        require $full_entry . '/_define.php';
192                        ob_end_clean();
193                        $this->all_modules[$entry] = &$this->modules[$entry];
194                        $this->id                  = null;
195                        $this->mroot               = null;
196                    } else {
197                        if (file_exists($full_entry . '/_define.php')) {
198                            $this->id            = $entry;
199                            $this->mroot         = $full_entry;
200                            $this->disabled_mode = true;
201                            ob_start();
202                            require $full_entry . '/_define.php';
203                            ob_end_clean();
204                            $this->disabled_mode       = false;
205                            $this->disabled[$entry]    = $this->disabled_meta;
206                            $this->all_modules[$entry] = &$this->disabled[$entry];
207                            $this->id                  = null;
208                            $this->mroot               = null;
209                        }
210                    }
211                }
212            }
213            $d->close();
214        }
215        $this->checkDependencies();
216        # Sort plugins
217        uasort($this->modules, array($this, 'sortModules'));
218
219        foreach ($this->modules as $id => $m) {
220            # Load translation and _prepend
221            if (file_exists($m['root'] . '/_prepend.php')) {
222                $r = $this->loadModuleFile($m['root'] . '/_prepend.php');
223
224                # If _prepend.php file returns null (ie. it has a void return statement)
225                if (is_null($r)) {
226                    $ignored[] = $id;
227                    continue;
228                }
229                unset($r);
230            }
231
232            $this->loadModuleL10N($id, $lang, 'main');
233            if ($ns == 'admin') {
234                $this->loadModuleL10Nresources($id, $lang);
235                $this->core->adminurl->register('admin.plugin.' . $id, 'plugin.php', array('p' => $id));
236            }
237        }
238
239        // Give opportunity to do something before loading context (admin,public,xmlrpc) files
240        $this->core->callBehavior('coreBeforeLoadingNsFiles', $this->core, $this, $lang);
241
242        foreach ($this->modules as $id => $m) {
243            # If _prepend.php file returns null (ie. it has a void return statement)
244            if (in_array($id, $ignored)) {
245                continue;
246            }
247            # Load ns_file
248            $this->loadNsFile($id, $ns);
249        }
250    }
251
252    public function requireDefine($dir, $id)
253    {
254        if (file_exists($dir . '/_define.php')) {
255            $this->id = $id;
256            ob_start();
257            require $dir . '/_define.php';
258            ob_end_clean();
259            $this->id = null;
260        }
261    }
262
263    /**
264    This method registers a module in modules list. You should use this to
265    register a new module.
266
267    <var>$permissions</var> is a comma separated list of permissions for your
268    module. If <var>$permissions</var> is null, only super admin has access to
269    this module.
270
271    <var>$priority</var> is an integer. Modules are sorted by priority and name.
272    Lowest priority comes first.
273
274    @param    name            <b>string</b>        Module name
275    @param    desc            <b>string</b>        Module description
276    @param    author        <b>string</b>        Module author name
277    @param    version        <b>string</b>        Module version
278    @param    properties    <b>array</b>        extra properties
279    (currently available keys : permissions, priority, type)
280     */
281    public function registerModule($name, $desc, $author, $version, $properties = array())
282    {
283        if ($this->disabled_mode) {
284            $this->disabled_meta = array_merge(
285                $properties,
286                array(
287                    'root'          => $this->mroot,
288                    'name'          => $name,
289                    'desc'          => $desc,
290                    'author'        => $author,
291                    'version'       => $version,
292                    'enabled'       => false,
293                    'root_writable' => is_writable($this->mroot)
294                )
295            );
296            return;
297        }
298        # Fallback to legacy registerModule parameters
299        if (!is_array($properties)) {
300            $args       = func_get_args();
301            $properties = array();
302            if (isset($args[4])) {
303                $properties['permissions'] = $args[4];
304            }
305            if (isset($args[5])) {
306                $properties['priority'] = (integer) $args[5];
307            }
308        }
309
310        # Default module properties
311        $properties = array_merge(
312            array(
313                'permissions'       => null,
314                'priority'          => 1000,
315                'standalone_config' => false,
316                'type'              => null,
317                'enabled'           => true,
318                'requires'          => array(),
319                'settings'          => array()
320            ), $properties
321        );
322
323        # Check module type
324        if (self::$type !== null && $properties['type'] !== null && $properties['type'] != self::$type) {
325            $this->errors[] = sprintf(
326                __('Module "%s" has type "%s" that mismatch required module type "%s".'),
327                '<strong>' . html::escapeHTML($name) . '</strong>',
328                '<em>' . html::escapeHTML($properties['type']) . '</em>',
329                '<em>' . html::escapeHTML(self::$type) . '</em>'
330            );
331            return;
332        }
333
334        # Check module perms on admin side
335        $permissions = $properties['permissions'];
336        if ($this->ns == 'admin') {
337            if ($permissions == '' && !$this->core->auth->isSuperAdmin()) {
338                return;
339            } elseif (!$this->core->auth->check($permissions, $this->core->blog->id)) {
340                return;
341            }
342        }
343
344        # Check module install on multiple path
345        if ($this->id) {
346            $module_exists    = array_key_exists($name, $this->modules_names);
347            $module_overwrite = $module_exists ? version_compare($this->modules_names[$name], $version, '<') : false;
348            if (!$module_exists || ($module_exists && $module_overwrite)) {
349                $this->modules_names[$name] = $version;
350                $this->modules[$this->id]   = array_merge(
351                    $properties,
352                    array(
353                        'root'          => $this->mroot,
354                        'name'          => $name,
355                        'desc'          => $desc,
356                        'author'        => $author,
357                        'version'       => $version,
358                        'root_writable' => is_writable($this->mroot)
359                    )
360                );
361            } else {
362                $path1          = path::real($this->moduleInfo($name, 'root'));
363                $path2          = path::real($this->mroot);
364                $this->errors[] = sprintf(
365                    __('Module "%s" is installed twice in "%s" and "%s".'),
366                    '<strong>' . $name . '</strong>',
367                    '<em>' . $path1 . '</em>',
368                    '<em>' . $path2 . '</em>'
369                );
370            }
371        }
372    }
373
374    public function resetModulesList()
375    {
376        $this->modules       = array();
377        $this->modules_names = array();
378        $this->errors        = array();
379    }
380
381    public static function installPackage($zip_file, dcModules &$modules)
382    {
383        $zip = new fileUnzip($zip_file);
384        $zip->getList(false, '#(^|/)(__MACOSX|\.svn|\.hg|\.git|\.DS_Store|\.directory|Thumbs\.db)(/|$)#');
385
386        $zip_root_dir = $zip->getRootDir();
387        $define       = '';
388        if ($zip_root_dir != false) {
389            $target      = dirname($zip_file);
390            $destination = $target . '/' . $zip_root_dir;
391            $define      = $zip_root_dir . '/_define.php';
392            $has_define  = $zip->hasFile($define);
393        } else {
394            $target      = dirname($zip_file) . '/' . preg_replace('/\.([^.]+)$/', '', basename($zip_file));
395            $destination = $target;
396            $define      = '_define.php';
397            $has_define  = $zip->hasFile($define);
398        }
399
400        if ($zip->isEmpty()) {
401            $zip->close();
402            unlink($zip_file);
403            throw new Exception(__('Empty module zip file.'));
404        }
405
406        if (!$has_define) {
407            $zip->close();
408            unlink($zip_file);
409            throw new Exception(__('The zip file does not appear to be a valid Dotclear module.'));
410        }
411
412        $ret_code = 1;
413
414        if (!is_dir($destination)) {
415            try {
416                files::makeDir($destination, true);
417
418                $sandbox = clone $modules;
419                $zip->unzip($define, $target . '/_define.php');
420
421                $sandbox->resetModulesList();
422                $sandbox->requireDefine($target, basename($destination));
423                unlink($target . '/_define.php');
424
425                $new_errors = $sandbox->getErrors();
426                if (!empty($new_errors)) {
427                    $new_errors = is_array($new_errors) ? implode(" \n", $new_errors) : $new_errors;
428                    throw new Exception($new_errors);
429                }
430
431                files::deltree($destination);
432            } catch (Exception $e) {
433                $zip->close();
434                unlink($zip_file);
435                files::deltree($destination);
436                throw new Exception($e->getMessage());
437            }
438        } else {
439            # test for update
440            $sandbox = clone $modules;
441            $zip->unzip($define, $target . '/_define.php');
442
443            $sandbox->resetModulesList();
444            $sandbox->requireDefine($target, basename($destination));
445            unlink($target . '/_define.php');
446            $new_modules = $sandbox->getModules();
447
448            if (!empty($new_modules)) {
449                $tmp        = array_keys($new_modules);
450                $id         = $tmp[0];
451                $cur_module = $modules->getModules($id);
452                if (!empty($cur_module) && (defined('DC_DEV') && DC_DEV === true || dcUtils::versionsCompare($new_modules[$id]['version'], $cur_module['version'], '>', true))) {
453                    # delete old module
454                    if (!files::deltree($destination)) {
455                        throw new Exception(__('An error occurred during module deletion.'));
456                    }
457                    $ret_code = 2;
458                } else {
459                    $zip->close();
460                    unlink($zip_file);
461                    throw new Exception(sprintf(__('Unable to upgrade "%s". (older or same version)'), basename($destination)));
462                }
463            } else {
464                $zip->close();
465                unlink($zip_file);
466                throw new Exception(sprintf(__('Unable to read new _define.php file')));
467            }
468        }
469        $zip->unzipAll($target);
470        $zip->close();
471        unlink($zip_file);
472        return $ret_code;
473    }
474
475    /**
476    This method installs all modules having a _install file.
477
478    @see dcModules::installModule
479     */
480    public function installModules()
481    {
482        $res = array('success' => array(), 'failure' => array());
483        foreach ($this->modules as $id => &$m) {
484            $i = $this->installModule($id, $msg);
485            if ($i === true) {
486                $res['success'][$id] = true;
487            } elseif ($i === false) {
488                $res['failure'][$id] = $msg;
489            }
490        }
491
492        return $res;
493    }
494
495    /**
496    This method installs module with ID <var>$id</var> and having a _install
497    file. This file should throw exception on failure or true if it installs
498    successfully.
499
500    <var>$msg</var> is an out parameter that handle installer message.
501
502    @param    id        <b>string</b>        Module ID
503    @param    msg        <b>string</b>        Module installer message
504    @return    <b>boolean</b>
505     */
506    public function installModule($id, &$msg)
507    {
508        try {
509            $i = $this->loadModuleFile($this->modules[$id]['root'] . '/_install.php');
510            if ($i === true) {
511                return true;
512            }
513        } catch (Exception $e) {
514            $msg = $e->getMessage();
515            return false;
516        }
517
518        return;
519    }
520
521    public function deleteModule($id, $disabled = false)
522    {
523        if ($disabled) {
524            $p = &$this->disabled;
525        } else {
526            $p = &$this->modules;
527        }
528
529        if (!isset($p[$id])) {
530            throw new Exception(__('No such module.'));
531        }
532
533        if (!files::deltree($p[$id]['root'])) {
534            throw new Exception(__('Cannot remove module files'));
535        }
536    }
537
538    public function deactivateModule($id)
539    {
540        if (!isset($this->modules[$id])) {
541            throw new Exception(__('No such module.'));
542        }
543
544        if (!$this->modules[$id]['root_writable']) {
545            throw new Exception(__('Cannot deactivate plugin.'));
546        }
547
548        if (@file_put_contents($this->modules[$id]['root'] . '/_disabled', '')) {
549            throw new Exception(__('Cannot deactivate plugin.'));
550        }
551    }
552
553    public function activateModule($id)
554    {
555        if (!isset($this->disabled[$id])) {
556            throw new Exception(__('No such module.'));
557        }
558
559        if (!$this->disabled[$id]['root_writable']) {
560            throw new Exception(__('Cannot activate plugin.'));
561        }
562
563        if (@unlink($this->disabled[$id]['root'] . '/_disabled') === false) {
564            throw new Exception(__('Cannot activate plugin.'));
565        }
566    }
567
568    /**
569    This method will search for file <var>$file</var> in language
570    <var>$lang</var> for module <var>$id</var>.
571
572    <var>$file</var> should not have any extension.
573
574    @param    id        <b>string</b>        Module ID
575    @param    lang        <b>string</b>        Language code
576    @param    file        <b>string</b>        File name (without extension)
577     */
578    public function loadModuleL10N($id, $lang, $file)
579    {
580        if (!$lang || !isset($this->modules[$id])) {
581            return;
582        }
583
584        $lfile = $this->modules[$id]['root'] . '/locales/%s/%s';
585        if (l10n::set(sprintf($lfile, $lang, $file)) === false && $lang != 'en') {
586            l10n::set(sprintf($lfile, 'en', $file));
587        }
588    }
589
590    public function loadModuleL10Nresources($id, $lang)
591    {
592        if (!$lang || !isset($this->modules[$id])) {
593            return;
594        }
595
596        $f = l10n::getFilePath($this->modules[$id]['root'] . '/locales', 'resources.php', $lang);
597        if ($f) {
598            $this->loadModuleFile($f);
599        }
600    }
601
602    /**
603    Returns all modules associative array or only one module if <var>$id</var>
604    is present.
605
606    @param    id        <b>string</b>        Optionnal module ID
607    @return    <b>array</b>
608     */
609    public function getModules($id = null)
610    {
611        if ($id && isset($this->modules[$id])) {
612            return $this->modules[$id];
613        }
614        return $this->modules;
615    }
616
617    /**
618    Returns true if the module with ID <var>$id</var> exists.
619
620    @param    id        <b>string</b>        Module ID
621    @return    <b>boolean</b>
622     */
623    public function moduleExists($id)
624    {
625        return isset($this->modules[$id]);
626    }
627
628    /**
629    Returns all disabled modules in an array
630
631    @return    <b>array</b>
632     */
633    public function getDisabledModules()
634    {
635        return $this->disabled;
636    }
637
638    /**
639    Returns root path for module with ID <var>$id</var>.
640
641    @param    id        <b>string</b>        Module ID
642    @return    <b>string</b>
643     */
644    public function moduleRoot($id)
645    {
646        return $this->moduleInfo($id, 'root');
647    }
648
649    /**
650    Returns a module information that could be:
651    - root
652    - name
653    - desc
654    - author
655    - version
656    - permissions
657    - priority
658
659    @param    id        <b>string</b>        Module ID
660    @param    info        <b>string</b>        Information to retrieve
661    @return    <b>string</b>
662     */
663    public function moduleInfo($id, $info)
664    {
665        return isset($this->modules[$id][$info]) ? $this->modules[$id][$info] : null;
666    }
667
668    /**
669    Loads namespace <var>$ns</var> specific files for all modules.
670
671    @param    ns        <b>string</b>        Namespace name
672     */
673    public function loadNsFiles($ns = null)
674    {
675        foreach ($this->modules as $k => $v) {
676            $this->loadNsFile($k, $ns);
677        }
678    }
679
680    /**
681    Loads namespace <var>$ns</var> specific file for module with ID
682    <var>$id</var>
683
684    @param    id        <b>string</b>        Module ID
685    @param    ns        <b>string</b>        Namespace name
686     */
687    public function loadNsFile($id, $ns = null)
688    {
689        switch ($ns) {
690            case 'admin':
691                $this->loadModuleFile($this->modules[$id]['root'] . '/_admin.php');
692                break;
693            case 'public':
694                $this->loadModuleFile($this->modules[$id]['root'] . '/_public.php');
695                break;
696            case 'xmlrpc':
697                $this->loadModuleFile($this->modules[$id]['root'] . '/_xmlrpc.php');
698                break;
699        }
700    }
701
702    public function getErrors()
703    {
704        return $this->errors;
705    }
706
707    protected function loadModuleFile($________, $catch = true)
708    {
709        if (!file_exists($________)) {
710            return;
711        }
712
713        self::$_k = array_keys($GLOBALS);
714
715        foreach (self::$_k as self::$_n) {
716            if (!in_array(self::$_n, self::$superglobals)) {
717                global ${self::$_n};
718            }
719        }
720
721        if ($catch) {
722            // Catch ouput to prevents hacked or corrupted modules
723            ob_start();
724            $ret = require $________;
725            ob_end_clean();
726            return $ret;
727        }
728
729        return require $________;
730    }
731
732    private function sortModules($a, $b)
733    {
734        if ($a['priority'] == $b['priority']) {
735            return strcasecmp($a['name'], $b['name']);
736        }
737
738        return ($a['priority'] < $b['priority']) ? -1 : 1;
739    }
740}
Note: See TracBrowser for help on using the repository browser.

Sites map