Dotclear

source: inc/core/class.dc.modules.php @ 3984:360a96edeef5

Revision 3984:360a96edeef5, 24.6 KB checked in by franck <carnet.franck.paul@…>, 6 years ago (diff)

Cope with various git/hg directories to ignore in zip archive

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

Sites map