Dotclear

source: inc/admin/lib.moduleslist.php @ 3731:3770620079d4

Revision 3731:3770620079d4, 70.4 KB checked in by franck <carnet.franck.paul@…>, 8 years ago (diff)

Simplify licence block at the beginning of each file

RevLine 
[2147]1<?php
[3731]2/**
3 * @package Dotclear
4 * @subpackage Backend
5 *
6 * @copyright Olivier Meunier & Association Dotclear
7 * @copyright GPL-2.0-only
8 */
9
[3699]10if (!defined('DC_ADMIN_CONTEXT')) {return;}
[2147]11
[2227]12/**
13 * @brief Helper for admin list of modules.
14 * @since 2.6
15
16 * Provides an object to parse XML feed of modules from a repository.
17 */
[2147]18class adminModulesList
19{
[3699]20    public $core; /**< @var    object    dcCore instance */
21    public $modules; /**< @var    object    dcModules instance */
22    public $store; /**< @var    object    dcStore instance */
[2147]23
[3699]24    public static $allow_multi_install = false; /**< @var    boolean    Work with multiple root directories */
25    public static $distributed_modules = array(); /**< @var    array    List of modules distributed with Dotclear */
[2149]26
[3699]27    protected $list_id = 'unknow'; /**< @var    string    Current list ID */
28    protected $data    = array(); /**< @var    array    Current modules */
[2156]29
[3699]30    protected $config_module  = ''; /**< @var    string    Module ID to configure */
31    protected $config_file    = ''; /**< @var    string    Module path to configure */
32    protected $config_content = ''; /**< @var    string    Module configuration page content */
[2182]33
[3699]34    protected $path          = false; /**< @var    string    Modules root directory */
35    protected $path_writable = false; /**< @var    boolean    Indicate if modules root directory is writable */
36    protected $path_pattern  = false; /**< @var    string    Directory pattern to work on */
[2149]37
[3699]38    protected $page_url   = ''; /**< @var    string    Page URL */
39    protected $page_qs    = '?'; /**< @var    string    Page query string */
40    protected $page_tab   = ''; /**< @var    string    Page tab */
41    protected $page_redir = ''; /**< @var    string    Page redirection */
[2147]42
[3699]43    public static $nav_indexes = 'abcdefghijklmnopqrstuvwxyz0123456789'; /**< @var    string    Index list */
44    protected $nav_list        = array(); /**< @var    array    Index list with special index */
45    protected $nav_special     = 'other'; /**< @var    string    Text for other special index */
[2147]46
[3699]47    protected $sort_field = 'sname'; /**< @var    string    Field used to sort modules */
48    protected $sort_asc   = true; /**< @var    boolean    Sort order asc */
[2147]49
[3699]50    /**
51     * Constructor.
52     *
53     * Note that this creates dcStore instance.
54     *
55     * @param    object    $modules        dcModules instance
56     * @param    string    $modules_root    Modules root directories
57     * @param    string    $xml_url        URL of modules feed from repository
58     */
59    public function __construct(dcModules $modules, $modules_root, $xml_url)
60    {
61        $this->core    = $modules->core;
62        $this->modules = $modules;
63        $this->store   = new dcStore($modules, $xml_url);
[2215]64
[3699]65        $this->page_url = $this->core->adminurl->get('admin.plugins');
[2817]66
[3699]67        $this->setPath($modules_root);
68        $this->setIndex(__('other'));
69    }
[2147]70
[3699]71    /**
72     * Begin a new list.
73     *
74     * @param    string    $id        New list ID
75     * @return    adminModulesList self instance
76     */
77    public function setList($id)
78    {
79        $this->data     = array();
80        $this->page_tab = '';
81        $this->list_id  = $id;
[2156]82
[3699]83        return $this;
84    }
[2156]85
[3699]86    /**
87     * Get list ID.
88     *
89     * @return    List ID
90     */
91    public function getList()
92    {
93        return $this->list_id;
94    }
[2241]95
[3699]96    /// @name Modules root directory methods
97    //@{
98    /**
99     * Set path info.
100     *
101     * @param    string    $root        Modules root directories
102     * @return    adminModulesList self instance
103     */
104    protected function setPath($root)
105    {
106        $paths = explode(PATH_SEPARATOR, $root);
107        $path  = array_pop($paths);
108        unset($paths);
[2149]109
[3699]110        $this->path = $path;
111        if (is_dir($path) && is_writeable($path)) {
112            $this->path_writable = true;
113            $this->path_pattern  = preg_quote($path, '!');
114        }
[2149]115
[3699]116        return $this;
117    }
[2149]118
[3699]119    /**
120     * Get modules root directory.
121     *
122     * @return    Path to work on
123     */
124    public function getPath()
125    {
126        return $this->path;
127    }
[2149]128
[3699]129    /**
130     * Check if modules root directory is writable.
131     *
132     * @return    True if directory is writable
133     */
134    public function isWritablePath()
135    {
136        return $this->path_writable;
137    }
[2149]138
[3699]139    /**
140     * Check if root directory of a module is deletable.
141     *
142     * @param    string    $root        Module root directory
143     * @return    True if directory is delatable
144     */
145    public function isDeletablePath($root)
146    {
147        return $this->path_writable
148        && (preg_match('!^' . $this->path_pattern . '!', $root) || defined('DC_DEV') && DC_DEV)
149        && $this->core->auth->isSuperAdmin();
150    }
151    //@}
[2149]152
[3699]153    /// @name Page methods
154    //@{
155    /**
156     * Set page base URL.
157     *
158     * @param    string    $url        Page base URL
159     * @return    adminModulesList self instance
160     */
161    public function setURL($url)
162    {
163        $this->page_qs  = strpos('?', $url) ? '&amp;' : '?';
164        $this->page_url = $url;
[2147]165
[3699]166        return $this;
167    }
[2147]168
[3699]169    /**
170     * Get page URL.
171     *
172     * @param    string|array    $queries    Additionnal query string
173     * @param    booleany    $with_tab        Add current tab to URL end
174     * @return    Clean page URL
175     */
176    public function getURL($queries = '', $with_tab = true)
177    {
178        return $this->page_url .
179            (!empty($queries) ? $this->page_qs : '') .
180            (is_array($queries) ? http_build_query($queries) : $queries) .
181            ($with_tab && !empty($this->page_tab) ? '#' . $this->page_tab : '');
182    }
[2147]183
[3699]184    /**
185     * Set page tab.
186     *
187     * @param    string    $tab        Page tab
188     * @return    adminModulesList self instance
189     */
190    public function setTab($tab)
191    {
192        $this->page_tab = $tab;
[2147]193
[3699]194        return $this;
195    }
[2147]196
[3699]197    /**
198     * Get page tab.
199     *
200     * @return    Page tab
201     */
202    public function getTab()
203    {
204        return $this->page_tab;
205    }
[2259]206
[3699]207    /**
208     * Set page redirection.
209     *
210     * @param    string    $default        Default redirection
211     * @return    adminModulesList self instance
212     */
213    public function setRedir($default = '')
214    {
215        $this->page_redir = empty($_REQUEST['redir']) ? $default : $_REQUEST['redir'];
[2259]216
[3699]217        return $this;
218    }
[2259]219
[3699]220    /**
221     * Get page redirection.
222     *
223     * @return    Page redirection
224     */
225    public function getRedir()
226    {
227        return empty($this->page_redir) ? $this->getURL() : $this->page_redir;
228    }
229    //@}
[2147]230
[3699]231    /// @name Search methods
232    //@{
233    /**
234     * Get search query.
235     *
236     * @return    Search query
237     */
238    public function getSearch()
239    {
240        $query = !empty($_REQUEST['m_search']) ? trim($_REQUEST['m_search']) : null;
241        return strlen($query) > 2 ? $query : null;
242    }
[2147]243
[3699]244    /**
245     * Display searh form.
246     *
247     * @return    adminModulesList self instance
248     */
249    public function displaySearch()
250    {
251        $query = $this->getSearch();
[2163]252
[3699]253        if (empty($this->data) && $query === null) {
254            return $this;
255        }
[2147]256
[3699]257        echo
258        '<div class="modules-search">' .
259        '<form action="' . $this->getURL() . '" method="get">' .
260        '<p><label for="m_search" class="classic">' . __('Search in repository:') . '&nbsp;</label><br />' .
[3725]261        form::field('m_search', 30, 255, html::escapeHTML($query)) .
[3699]262        '<input type="submit" value="' . __('OK') . '" /> ';
[2287]263
[3699]264        if ($query) {
265            echo
266            ' <a href="' . $this->getURL() . '" class="button">' . __('Reset search') . '</a>';
267        }
[2287]268
[3699]269        echo
270        '</p>' .
271        '<p class="form-note">' .
272        __('Search is allowed on multiple terms longer than 2 chars, terms must be separated by space.') .
273            '</p>' .
274            '</form>';
[2147]275
[3699]276        if ($query) {
277            echo
278            '<p class="message">' . sprintf(
279                __('Found %d result for search "%s":', 'Found %d results for search "%s":', count($this->data)),
280                count($this->data), html::escapeHTML($query)
281            ) .
282                '</p>';
283        }
284        echo '</div>';
[2287]285
[3699]286        return $this;
287    }
288    //@}
[2147]289
[3699]290    /// @name Navigation menu methods
291    //@{
292    /**
293     * Set navigation special index.
294     *
295     * @return    adminModulesList self instance
296     */
297    public function setIndex($str)
298    {
299        $this->nav_special = (string) $str;
300        $this->nav_list    = array_merge(str_split(self::$nav_indexes), array($this->nav_special));
[2147]301
[3699]302        return $this;
303    }
[2147]304
[3699]305    /**
306     * Get index from query.
307     *
308     * @return    Query index or default one
309     */
310    public function getIndex()
311    {
312        return isset($_REQUEST['m_nav']) && in_array($_REQUEST['m_nav'], $this->nav_list) ? $_REQUEST['m_nav'] : $this->nav_list[0];
313    }
[2147]314
[3699]315    /**
316     * Display navigation by index menu.
317     *
318     * @return    adminModulesList self instance
319     */
320    public function displayIndex()
321    {
322        if (empty($this->data) || $this->getSearch() !== null) {
323            return $this;
324        }
[2147]325
[3699]326        # Fetch modules required field
327        $indexes = array();
328        foreach ($this->data as $id => $module) {
329            if (!isset($module[$this->sort_field])) {
330                continue;
331            }
332            $char = substr($module[$this->sort_field], 0, 1);
333            if (!in_array($char, $this->nav_list)) {
334                $char = $this->nav_special;
335            }
336            if (!isset($indexes[$char])) {
337                $indexes[$char] = 0;
338            }
339            $indexes[$char]++;
340        }
[2147]341
[3699]342        $buttons = array();
343        foreach ($this->nav_list as $char) {
344            # Selected letter
345            if ($this->getIndex() == $char) {
346                $buttons[] = '<li class="active" title="' . __('current selection') . '"><strong> ' . $char . ' </strong></li>';
347            }
348            # Letter having modules
349            elseif (!empty($indexes[$char])) {
350                $title     = sprintf(__('%d result', '%d results', $indexes[$char]), $indexes[$char]);
351                $buttons[] = '<li class="btn" title="' . $title . '"><a href="' . $this->getURL('m_nav=' . $char) . '" title="' . $title . '"> ' . $char . ' </a></li>';
352            }
353            # Letter without modules
354            else {
355                $buttons[] = '<li class="btn no-link" title="' . __('no results') . '"> ' . $char . ' </li>';
356            }
357        }
358        # Parse navigation menu
359        echo '<div class="pager">' . __('Browse index:') . ' <ul class="index">' . implode('', $buttons) . '</ul></div>';
[2147]360
[3699]361        return $this;
362    }
363    //@}
[2147]364
[3699]365    /// @name Sort methods
366    //@{
367    /**
368     * Set default sort field.
369     *
370     * @return    adminModulesList self instance
371     */
372    public function setSort($field, $asc = true)
373    {
374        $this->sort_field = $field;
375        $this->sort_asc   = (boolean) $asc;
[2147]376
[3699]377        return $this;
378    }
[2147]379
[3699]380    /**
381     * Get sort field from query.
382     *
383     * @return    Query sort field or default one
384     */
385    public function getSort()
386    {
387        return !empty($_REQUEST['m_sort']) ? $_REQUEST['m_sort'] : $this->sort_field;
388    }
[2147]389
[3699]390    /**
391     * Display sort field form.
392     *
393     * @note    This method is not implemented yet
394     * @return    adminModulesList self instance
395     */
396    public function displaySort()
397    {
398        //
[2227]399
[3699]400        return $this;
401    }
402    //@}
[2147]403
[3699]404    /// @name Modules methods
405    //@{
406    /**
407     * Set modules and sanitize them.
408     *
409     * @return    adminModulesList self instance
410     */
411    public function setModules($modules)
412    {
413        $this->data = array();
414        if (!empty($modules) && is_array($modules)) {
415            foreach ($modules as $id => $module) {
416                $this->data[$id] = self::sanitizeModule($id, $module);
417            }
418        }
419        return $this;
420    }
[2147]421
[3699]422    /**
423     * Get modules currently set.
424     *
425     * @return    Array of modules
426     */
427    public function getModules()
428    {
429        return $this->data;
430    }
[2147]431
[3699]432    /**
433     * Sanitize a module.
434     *
435     * This clean infos of a module by adding default keys
436     * and clean some of them, sanitize module can safely
437     * be used in lists.
438     *
439     * @return    Array of the module informations
440     */
441    public static function sanitizeModule($id, $module)
442    {
443        $label = empty($module['label']) ? $id : $module['label'];
444        $name  = __(empty($module['name']) ? $label : $module['name']);
[2431]445
[3699]446        return array_merge(
447            # Default values
448            array(
449                'desc'              => '',
450                'author'            => '',
451                'version'           => 0,
452                'current_version'   => 0,
453                'root'              => '',
454                'root_writable'     => false,
455                'permissions'       => null,
456                'parent'            => null,
457                'priority'          => 1000,
458                'standalone_config' => false,
459                'support'           => '',
460                'section'           => '',
461                'tags'              => '',
462                'details'           => '',
463                'sshot'             => '',
464                'score'             => 0,
465                'type'              => null,
466                'require'           => array(),
467                'settings'          => array()
468            ),
469            # Module's values
470            $module,
471            # Clean up values
472            array(
473                'id'    => $id,
474                'sid'   => self::sanitizeString($id),
475                'label' => $label,
476                'name'  => $name,
477                'sname' => self::sanitizeString($name)
478            )
479        );
480    }
[2147]481
[3699]482    /**
483     * Check if a module is part of the distribution.
484     *
485     * @param    string    $id        Module root directory
486     * @return    True if module is part of the distribution
487     */
488    public static function isDistributedModule($id)
489    {
490        $distributed_modules = self::$distributed_modules;
[2171]491
[3699]492        return is_array($distributed_modules) && in_array($id, $distributed_modules);
493    }
[2148]494
[3699]495    /**
496     * Sort modules list by specific field.
497     *
498     * @param    string    $module        Array of modules
499     * @param    string    $field        Field to sort from
500     * @param    bollean    $asc        Sort asc if true, else decs
501     * @return    Array of sorted modules
502     */
503    public static function sortModules($modules, $field, $asc = true)
504    {
505        $origin = $sorter = array();
[2631]506
[3699]507        foreach ($modules as $id => $module) {
508            $origin[] = $module;
509            $sorter[] = isset($module[$field]) ? $module[$field] : $field;
510        }
[2147]511
[3699]512        array_multisort($sorter, $asc ? SORT_ASC : SORT_DESC, $origin);
[2631]513
[3699]514        foreach ($origin as $module) {
515            $final[$module['id']] = $module;
516        }
[2631]517
[3699]518        return $final;
519    }
[2147]520
[3699]521    /**
522     * Display list of modules.
523     *
524     * @param    array    $cols        List of colones (module field) to display
525     * @param    array    $actions    List of predefined actions to show on form
526     * @param    boolean    $nav_limit    Limit list to previously selected index
527     * @return    adminModulesList self instance
528     */
529    public function displayModules($cols = array('name', 'version', 'desc'), $actions = array(), $nav_limit = false)
530    {
531        echo
532        '<form action="' . $this->getURL() . '" method="post" class="modules-form-actions">' .
533        '<div class="table-outer">' .
534        '<table id="' . html::escapeHTML($this->list_id) . '" class="modules' . (in_array('expander', $cols) ? ' expandable' : '') . '">' .
535        '<caption class="hidden">' . html::escapeHTML(__('Plugins list')) . '</caption><tr>';
[2163]536
[3699]537        if (in_array('name', $cols)) {
538            $colspan = 1;
539            if (in_array('checkbox', $cols)) {
540                $colspan++;
541            }
542            if (in_array('icon', $cols)) {
543                $colspan++;
544            }
545            echo
546            '<th class="first nowrap"' . ($colspan > 1 ? ' colspan="' . $colspan . '"' : '') . '>' . __('Name') . '</th>';
547        }
[2147]548
[3699]549        if (in_array('score', $cols) && $this->getSearch() !== null && defined('DC_DEBUG') && DC_DEBUG) {
550            echo
551            '<th class="nowrap">' . __('Score') . '</th>';
552        }
[2222]553
[3699]554        if (in_array('version', $cols)) {
555            echo
556            '<th class="nowrap count" scope="col">' . __('Version') . '</th>';
557        }
[2147]558
[3699]559        if (in_array('current_version', $cols)) {
560            echo
561            '<th class="nowrap count" scope="col">' . __('Current version') . '</th>';
562        }
[2147]563
[3699]564        if (in_array('desc', $cols)) {
565            echo
566            '<th class="nowrap" scope="col">' . __('Details') . '</th>';
567        }
[2147]568
[3699]569        if (in_array('distrib', $cols)) {
570            echo
571                '<th' . (in_array('desc', $cols) ? '' : ' class="maximal"') . '></th>';
572        }
[2149]573
[3699]574        if (!empty($actions) && $this->core->auth->isSuperAdmin()) {
575            echo
576            '<th class="minimal nowrap">' . __('Action') . '</th>';
577        }
[2147]578
[3699]579        echo
580            '</tr>';
[2147]581
[3699]582        $sort_field = $this->getSort();
[2147]583
[3699]584        # Sort modules by $sort_field (default sname)
585        $modules = $this->getSearch() === null ?
586        self::sortModules($this->data, $sort_field, $this->sort_asc) :
587        $this->data;
[2147]588
[3699]589        $count = 0;
590        foreach ($modules as $id => $module) {
591            # Show only requested modules
592            if ($nav_limit && $this->getSearch() === null) {
593                $char = substr($module[$sort_field], 0, 1);
594                if (!in_array($char, $this->nav_list)) {
595                    $char = $this->nav_special;
596                }
597                if ($this->getIndex() != $char) {
598                    continue;
599                }
600            }
[2147]601
[3699]602            echo
603            '<tr class="line" id="' . html::escapeHTML($this->list_id) . '_m_' . html::escapeHTML($id) . '">';
[2431]604
[3699]605            $tds = 0;
[2163]606
[3699]607            if (in_array('checkbox', $cols)) {
608                $tds++;
609                echo
610                '<td class="module-icon nowrap">' .
611                form::checkbox(array('modules[' . $count . ']', html::escapeHTML($this->list_id) . '_modules_' . html::escapeHTML($id)), html::escapeHTML($id)) .
612                    '</td>';
613            }
[2487]614
[3699]615            if (in_array('icon', $cols)) {
616                $tds++;
617                echo
618                '<td class="module-icon nowrap">' . sprintf(
619                    '<img alt="%1$s" title="%1$s" src="%2$s" />',
620                    html::escapeHTML($id), file_exists($module['root'] . '/icon.png') ?
621                    dcPage::getPF($id . '/icon.png') : 'images/module.png'
622                ) . '</td>';
623            }
[2156]624
[3699]625            $tds++;
626            echo
627                '<td class="module-name nowrap" scope="row">';
628            if (in_array('checkbox', $cols)) {
629                if (in_array('expander', $cols)) {
630                    echo
631                    html::escapeHTML($module['name']) . ($id != $module['name'] ? sprintf(__(' (%s)'), $id) : '');
632                } else {
633                    echo
634                    '<label for="' . html::escapeHTML($this->list_id) . '_modules_' . html::escapeHTML($id) . '">' .
635                    html::escapeHTML($module['name']) . ($id != $module['name'] ? sprintf(__(' (%s)'), $id) : '') .
636                        '</label>';
637                }
638            } else {
639                echo
640                html::escapeHTML($module['name']) . ($id != $module['name'] ? sprintf(__(' (%s)'), $id) : '') .
641                form::hidden(array('modules[' . $count . ']'), html::escapeHTML($id));
642            }
643            echo
644            $this->core->formNonce() .
645                '</td>';
[2147]646
[3699]647            # Display score only for debug purpose
648            if (in_array('score', $cols) && $this->getSearch() !== null && defined('DC_DEBUG') && DC_DEBUG) {
649                $tds++;
650                echo
651                    '<td class="module-version nowrap count"><span class="debug">' . $module['score'] . '</span></td>';
652            }
[2222]653
[3699]654            if (in_array('version', $cols)) {
655                $tds++;
656                echo
657                '<td class="module-version nowrap count">' . html::escapeHTML($module['version']) . '</td>';
658            }
[2147]659
[3699]660            if (in_array('current_version', $cols)) {
661                $tds++;
662                echo
663                '<td class="module-current-version nowrap count">' . html::escapeHTML($module['current_version']) . '</td>';
664            }
[2147]665
[3699]666            if (in_array('desc', $cols)) {
667                $tds++;
668                echo
669                '<td class="module-desc maximal">' . html::escapeHTML(__($module['desc']));
670                if (isset($module['cannot_disable']) && $module['enabled']) {
671                    echo
672                    '<br/><span class="info">' .
673                    sprintf(__('This module cannot be disabled nor deleted, since the following modules are also enabled : %s'),
674                        join(',', $module['cannot_disable'])) .
675                        '</span>';
676                }
677                if (isset($module['cannot_enable']) && !$module['enabled']) {
678                    echo
679                    '<br/><span class="info">' .
680                    __('This module cannot be enabled, because of the following reasons :') .
681                        '<ul>';
682                    foreach ($module['cannot_enable'] as $m => $reason) {
683                        echo '<li>' . $reason . '</li>';
684                    }
685                    echo '</ul>' .
686                        '</span>';
687                }
688                echo '</td>';
[2997]689
[3699]690            }
[2149]691
[3699]692            if (in_array('distrib', $cols)) {
693                $tds++;
694                echo
695                    '<td class="module-distrib">' . (self::isDistributedModule($id) ?
696                    '<img src="images/dotclear_pw.png" alt="' .
697                    __('Plugin from official distribution') . '" title="' .
698                    __('Plugin from official distribution') . '" />'
699                    : '') . '</td>';
700            }
[2147]701
[3699]702            if (!empty($actions) && $this->core->auth->isSuperAdmin()) {
703                $buttons = $this->getActions($id, $module, $actions);
[2215]704
[3699]705                $tds++;
706                echo
707                '<td class="module-actions nowrap">' .
[2147]708
[3699]709                '<div>' . implode(' ', $buttons) . '</div>' .
[2215]710
[3699]711                    '</td>';
712            }
[2147]713
[3699]714            echo
715                '</tr>';
[2147]716
[3699]717            # Other informations
718            if (in_array('expander', $cols)) {
719                echo
720                    '<tr class="module-more"><td colspan="' . $tds . '" class="expand">';
[2354]721
[3699]722                if (!empty($module['author']) || !empty($module['details']) || !empty($module['support'])) {
723                    echo
724                        '<div><ul class="mod-more">';
[2354]725
[3699]726                    if (!empty($module['author'])) {
727                        echo
728                        '<li class="module-author">' . __('Author:') . ' ' . html::escapeHTML($module['author']) . '</li>';
729                    }
[2354]730
[3699]731                    $more = array();
732                    if (!empty($module['details'])) {
733                        $more[] = '<a class="module-details" href="' . $module['details'] . '">' . __('Details') . '</a>';
734                    }
[2354]735
[3699]736                    if (!empty($module['support'])) {
737                        $more[] = '<a class="module-support" href="' . $module['support'] . '">' . __('Support') . '</a>';
738                    }
[2431]739
[3699]740                    if (!empty($more)) {
741                        echo
742                        '<li>' . implode(' - ', $more) . '</li>';
743                    }
[2354]744
[3699]745                    echo
746                        '</ul></div>';
747                }
[2354]748
[3699]749                $config = !empty($module['root']) && file_exists(path::real($module['root'] . '/_config.php'));
750                $index  = !empty($module['root']) && file_exists(path::real($module['root'] . '/index.php'));
[2354]751
[3699]752                if ($config || $index || !empty($module['section']) || !empty($module['tags']) || !empty($module['settings'])) {
753                    echo
754                        '<div><ul class="mod-more">';
[2354]755
[3699]756                    if ($index && $module['enabled']) {
757                        echo '<li><a href="' . $this->core->adminurl->get('admin.plugin.' . $id) . '">' . __('Manage plugin') . '</a></li>';
758                    }
[3335]759
[3699]760                    $settings = $this->getSettingsUrls($this->core, $id);
761                    if (!empty($settings) && $module['enabled']) {
762                        echo '<li>' . implode(' - ', $settings) . '</li>';
763                    }
[2354]764
[3699]765                    if (!empty($module['section'])) {
766                        echo
767                        '<li class="module-section">' . __('Section:') . ' ' . html::escapeHTML($module['section']) . '</li>';
768                    }
[2354]769
[3699]770                    if (!empty($module['tags'])) {
771                        echo
772                        '<li class="module-tags">' . __('Tags:') . ' ' . html::escapeHTML($module['tags']) . '</li>';
773                    }
[2354]774
[3699]775                    echo
776                        '</ul></div>';
777                }
[2354]778
[3699]779                echo
780                    '</td></tr>';
781            }
[2354]782
[3699]783            $count++;
784        }
785        echo
786            '</table></div>';
[2147]787
[3699]788        if (!$count && $this->getSearch() === null) {
789            echo
790            '<p class="message">' . __('No plugins matched your search.') . '</p>';
791        } elseif ((in_array('checkbox', $cols) || $count > 1) && !empty($actions) && $this->core->auth->isSuperAdmin()) {
792            $buttons = $this->getGlobalActions($actions, in_array('checkbox', $cols));
[2227]793
[3699]794            if (!empty($buttons)) {
795                if (in_array('checkbox', $cols)) {
796                    echo
797                        '<p class="checkboxes-helpers"></p>';
798                }
799                echo
800                '<div>' . implode(' ', $buttons) . '</div>';
801            }
802        }
803        echo
804            '</form>';
[2428]805
[3699]806        return $this;
807    }
[2428]808
[3699]809    /**
810     * Get settings URLs if any
811     *
812     * @param object $core
813     * @param string $id module ID
814     * @param boolean $check check permission
815     * @param boolean $self include self URL (→ plugin index.php URL)
816     * @return Array of settings URLs
817     */
818    public static function getSettingsUrls($core, $id, $check = false, $self = true)
819    {
820        $st = array();
[2147]821
[3699]822        $mr       = $core->plugins->moduleRoot($id);
823        $config   = !empty($mr) && file_exists(path::real($mr . '/_config.php'));
824        $settings = $core->plugins->moduleInfo($id, 'settings');
825        if ($config || !empty($settings)) {
826            if ($config) {
827                if (!$check ||
828                    $core->auth->isSuperAdmin() ||
829                    $core->auth->check($core->plugins->moduleInfo($id, 'permissions'), $core->blog->id)) {
830                    $params = array('module' => $id, 'conf' => '1');
831                    if (!$core->plugins->moduleInfo($id, 'standalone_config') && !$self) {
832                        $params['redir'] = $core->adminurl->get('admin.plugin.' . $id);
833                    }
834                    $st[] = '<a class="module-config" href="' .
835                    $core->adminurl->get('admin.plugins', $params) .
836                    '">' . __('Configure plugin') . '</a>';
837                }
838            }
839            if (is_array($settings)) {
840                foreach ($settings as $sk => $sv) {
841                    switch ($sk) {
842                        case 'blog':
843                            if (!$check ||
844                                $core->auth->isSuperAdmin() ||
845                                $core->auth->check('admin', $core->blog->id)) {
846                                $st[] = '<a class="module-config" href="' .
847                                $core->adminurl->get('admin.blog.pref') . $sv .
848                                '">' . __('Plugin settings (in blog parameters)') . '</a>';
849                            }
850                            break;
851                        case 'pref':
852                            if (!$check ||
853                                $core->auth->isSuperAdmin() ||
854                                $core->auth->check('usage,contentadmin', $core->blog->id)) {
855                                $st[] = '<a class="module-config" href="' .
856                                $core->adminurl->get('admin.user.preferences') . $sv .
857                                '">' . __('Plugin settings (in user preferences)') . '</a>';
858                            }
859                            break;
860                        case 'self':
861                            if ($self) {
862                                if (!$check ||
863                                    $core->auth->isSuperAdmin() ||
864                                    $core->auth->check($core->plugins->moduleInfo($id, 'permissions'), $core->blog->id)) {
865                                    $st[] = '<a class="module-config" href="' .
866                                    $core->adminurl->get('admin.plugin.' . $id) . $sv .
867                                    '">' . __('Plugin settings') . '</a>';
868                                }
869                            }
870                            break;
871                    }
872                }
873            }
874        }
[3333]875
[3699]876        return $st;
877    }
[3333]878
[3699]879    /**
880     * Get action buttons to add to modules list.
881     *
882     * @param    string    $id            Module ID
883     * @param    array    $module        Module info
884     * @param    array    $actions    Actions keys
885     * @return    Array of actions buttons
886     */
887    protected function getActions($id, $module, $actions)
888    {
889        $submits = array();
[3333]890
[3699]891        # Use loop to keep requested order
892        foreach ($actions as $action) {
893            switch ($action) {
[2147]894
[3699]895                # Deactivate
896                case 'activate':if ($this->core->auth->isSuperAdmin() && $module['root_writable'] && !isset($module['cannot_enable'])) {
897                        $submits[] =
898                        '<input type="submit" name="activate[' . html::escapeHTML($id) . ']" value="' . __('Activate') . '" />';
899                    }break;
[2215]900
[3699]901                # Activate
902                case 'deactivate':if ($this->core->auth->isSuperAdmin() && $module['root_writable'] && !isset($module['cannot_disable'])) {
903                        $submits[] =
904                        '<input type="submit" name="deactivate[' . html::escapeHTML($id) . ']" value="' . __('Deactivate') . '" class="reset" />';
905                    }break;
[2215]906
[3699]907                # Delete
908                case 'delete':if ($this->core->auth->isSuperAdmin() && $this->isDeletablePath($module['root']) && !isset($module['cannot_disable'])) {
909                        $dev       = !preg_match('!^' . $this->path_pattern . '!', $module['root']) && defined('DC_DEV') && DC_DEV ? ' debug' : '';
910                        $submits[] =
911                        '<input type="submit" class="delete ' . $dev . '" name="delete[' . html::escapeHTML($id) . ']" value="' . __('Delete') . '" />';
912                    }break;
[2215]913
[3699]914                # Install (from store)
915                case 'install':if ($this->core->auth->isSuperAdmin() && $this->path_writable) {
916                        $submits[] =
917                        '<input type="submit" name="install[' . html::escapeHTML($id) . ']" value="' . __('Install') . '" />';
918                    }break;
[2215]919
[3699]920                # Update (from store)
921                case 'update':if ($this->core->auth->isSuperAdmin() && $this->path_writable) {
922                        $submits[] =
923                        '<input type="submit" name="update[' . html::escapeHTML($id) . ']" value="' . __('Update') . '" />';
924                    }break;
[2215]925
[3699]926                # Behavior
927                case 'behavior':
[2241]928
[3699]929                    # --BEHAVIOR-- adminModulesListGetActions
930                    $tmp = $this->core->callBehavior('adminModulesListGetActions', $this, $id, $module);
[2241]931
[3699]932                    if (!empty($tmp)) {
933                        $submits[] = $tmp;
934                    }
935                    break;
936            }
937        }
[2241]938
[3699]939        return $submits;
940    }
[2192]941
[3699]942    /**
943     * Get global action buttons to add to modules list.
944     *
945     * @param    array    $actions          Actions keys
946     * @param boolean   $with_selection Limit action to selected modules
947     * @return    Array of actions buttons
948     */
949    protected function getGlobalActions($actions, $with_selection = false)
950    {
951        $submits = array();
[2428]952
[3699]953        # Use loop to keep requested order
954        foreach ($actions as $action) {
955            switch ($action) {
[2428]956
[3699]957                # Deactivate
958                case 'activate':if ($this->core->auth->isSuperAdmin() && $this->path_writable) {
959                        $submits[] =
960                            '<input type="submit" name="activate" value="' . ($with_selection ?
961                            __('Activate selected plugins') :
962                            __('Activate all plugins from this list')
963                        ) . '" />';
964                    }break;
[2428]965
[3699]966                # Activate
967                case 'deactivate':if ($this->core->auth->isSuperAdmin() && $this->path_writable) {
968                        $submits[] =
969                            '<input type="submit" name="deactivate" value="' . ($with_selection ?
970                            __('Deactivate selected plugins') :
971                            __('Deactivate all plugins from this list')
972                        ) . '" />';
973                    }break;
[2428]974
[3699]975                # Update (from store)
976                case 'update':if ($this->core->auth->isSuperAdmin() && $this->path_writable) {
977                        $submits[] =
978                            '<input type="submit" name="update" value="' . ($with_selection ?
979                            __('Update selected plugins') :
980                            __('Update all plugins from this list')
981                        ) . '" />';
982                    }break;
[2428]983
[3699]984                # Behavior
985                case 'behavior':
[2428]986
[3699]987                    # --BEHAVIOR-- adminModulesListGetGlobalActions
988                    $tmp = $this->core->callBehavior('adminModulesListGetGlobalActions', $this, $with_selection);
[2428]989
[3699]990                    if (!empty($tmp)) {
991                        $submits[] = $tmp;
992                    }
993                    break;
994            }
995        }
[2428]996
[3699]997        return $submits;
998    }
[2241]999
[3699]1000    /**
1001     * Execute POST action.
1002     *
1003     * @note    Set a notice on success through dcPage::addSuccessNotice
1004     * @throw    Exception    Module not find or command failed
1005     * @return    Null
1006     */
1007    public function doActions()
1008    {
1009        if (empty($_POST) || !empty($_REQUEST['conf'])
1010            || !$this->isWritablePath()) {
1011            return;
1012        }
[2147]1013
[3699]1014        $modules = !empty($_POST['modules']) && is_array($_POST['modules']) ? array_values($_POST['modules']) : array();
[2149]1015
[3699]1016        if ($this->core->auth->isSuperAdmin() && !empty($_POST['delete'])) {
[2149]1017
[3699]1018            if (is_array($_POST['delete'])) {
1019                $modules = array_keys($_POST['delete']);
1020            }
[2149]1021
[3699]1022            $list = $this->modules->getDisabledModules();
[2149]1023
[3699]1024            $failed = false;
1025            $count  = 0;
1026            foreach ($modules as $id) {
1027                if (!isset($list[$id])) {
[2149]1028
[3699]1029                    if (!$this->modules->moduleExists($id)) {
1030                        throw new Exception(__('No such plugin.'));
1031                    }
[2171]1032
[3699]1033                    $module       = $this->modules->getModules($id);
1034                    $module['id'] = $id;
[2171]1035
[3699]1036                    if (!$this->isDeletablePath($module['root'])) {
1037                        $failed = true;
1038                        continue;
1039                    }
[2171]1040
[3699]1041                    # --BEHAVIOR-- moduleBeforeDelete
1042                    $this->core->callBehavior('pluginBeforeDelete', $module);
[2171]1043
[3699]1044                    $this->modules->deleteModule($id);
[2171]1045
[3699]1046                    # --BEHAVIOR-- moduleAfterDelete
1047                    $this->core->callBehavior('pluginAfterDelete', $module);
1048                } else {
1049                    $this->modules->deleteModule($id, true);
1050                }
[2171]1051
[3699]1052                $count++;
1053            }
[2171]1054
[3699]1055            if (!$count && $failed) {
1056                throw new Exception(__("You don't have permissions to delete this plugin."));
1057            } elseif ($failed) {
1058                dcPage::addWarningNotice(__('Some plugins have not been delete.'));
1059            } else {
1060                dcPage::addSuccessNotice(
1061                    __('Plugin has been successfully deleted.', 'Plugins have been successuflly deleted.', $count)
1062                );
1063            }
1064            http::redirect($this->getURL());
1065        } elseif ($this->core->auth->isSuperAdmin() && !empty($_POST['install'])) {
[2215]1066
[3699]1067            if (is_array($_POST['install'])) {
1068                $modules = array_keys($_POST['install']);
1069            }
[2487]1070
[3699]1071            $list = $this->store->get();
[2487]1072
[3699]1073            if (empty($list)) {
1074                throw new Exception(__('No such plugin.'));
1075            }
[2487]1076
[3699]1077            $count = 0;
1078            foreach ($list as $id => $module) {
[2487]1079
[3699]1080                if (!in_array($id, $modules)) {
1081                    continue;
1082                }
[2487]1083
[3699]1084                $dest = $this->getPath() . '/' . basename($module['file']);
[2487]1085
[3699]1086                # --BEHAVIOR-- moduleBeforeAdd
1087                $this->core->callBehavior('pluginBeforeAdd', $module);
[2215]1088
[3699]1089                $this->store->process($module['file'], $dest);
[2215]1090
[3699]1091                # --BEHAVIOR-- moduleAfterAdd
1092                $this->core->callBehavior('pluginAfterAdd', $module);
[2215]1093
[3699]1094                $count++;
1095            }
[2215]1096
[3699]1097            dcPage::addSuccessNotice(
1098                __('Plugin has been successfully installed.', 'Plugins have been successuflly installed.', $count)
1099            );
1100            http::redirect($this->getURL());
1101        } elseif ($this->core->auth->isSuperAdmin() && !empty($_POST['activate'])) {
[2215]1102
[3699]1103            if (is_array($_POST['activate'])) {
1104                $modules = array_keys($_POST['activate']);
1105            }
[2215]1106
[3699]1107            $list = $this->modules->getDisabledModules();
1108            if (empty($list)) {
1109                throw new Exception(__('No such plugin.'));
1110            }
[2171]1111
[3699]1112            $count = 0;
1113            foreach ($list as $id => $module) {
[2487]1114
[3699]1115                if (!in_array($id, $modules)) {
1116                    continue;
1117                }
[2487]1118
[3699]1119                # --BEHAVIOR-- moduleBeforeActivate
1120                $this->core->callBehavior('pluginBeforeActivate', $id);
[2487]1121
[3699]1122                $this->modules->activateModule($id);
[2487]1123
[3699]1124                # --BEHAVIOR-- moduleAfterActivate
1125                $this->core->callBehavior('pluginAfterActivate', $id);
[2171]1126
[3699]1127                $count++;
1128            }
[2487]1129
[3699]1130            dcPage::addSuccessNotice(
1131                __('Plugin has been successfully activated.', 'Plugins have been successuflly activated.', $count)
1132            );
1133            http::redirect($this->getURL());
1134        } elseif ($this->core->auth->isSuperAdmin() && !empty($_POST['deactivate'])) {
[2487]1135
[3699]1136            if (is_array($_POST['deactivate'])) {
1137                $modules = array_keys($_POST['deactivate']);
1138            }
[2487]1139
[3699]1140            $list = $this->modules->getModules();
1141            if (empty($list)) {
1142                throw new Exception(__('No such plugin.'));
1143            }
[2487]1144
[3699]1145            $failed = false;
1146            $count  = 0;
1147            foreach ($list as $id => $module) {
[2487]1148
[3699]1149                if (!in_array($id, $modules)) {
1150                    continue;
1151                }
[2487]1152
[3699]1153                if (!$module['root_writable']) {
1154                    $failed = true;
1155                    continue;
1156                }
[2487]1157
[3699]1158                $module[$id] = $id;
[2487]1159
[3699]1160                # --BEHAVIOR-- moduleBeforeDeactivate
1161                $this->core->callBehavior('pluginBeforeDeactivate', $module);
[2487]1162
[3699]1163                $this->modules->deactivateModule($id);
[2171]1164
[3699]1165                # --BEHAVIOR-- moduleAfterDeactivate
1166                $this->core->callBehavior('pluginAfterDeactivate', $module);
[2270]1167
[3699]1168                $count++;
1169            }
[2487]1170
[3699]1171            if ($failed) {
1172                dcPage::addWarningNotice(__('Some plugins have not been deactivated.'));
1173            } else {
1174                dcPage::addSuccessNotice(
1175                    __('Plugin has been successfully deactivated.', 'Plugins have been successuflly deactivated.', $count)
1176                );
1177            }
1178            http::redirect($this->getURL());
1179        } elseif ($this->core->auth->isSuperAdmin() && !empty($_POST['update'])) {
[2487]1180
[3699]1181            if (is_array($_POST['update'])) {
1182                $modules = array_keys($_POST['update']);
1183            }
[2487]1184
[3699]1185            $list = $this->store->get(true);
1186            if (empty($list)) {
1187                throw new Exception(__('No such plugin.'));
1188            }
[2487]1189
[3699]1190            $count = 0;
1191            foreach ($list as $module) {
[2487]1192
[3699]1193                if (!in_array($module['id'], $modules)) {
1194                    continue;
1195                }
[2487]1196
[3699]1197                if (!self::$allow_multi_install) {
1198                    $dest = $module['root'] . '/../' . basename($module['file']);
1199                } else {
1200                    $dest = $this->getPath() . '/' . basename($module['file']);
1201                    if ($module['root'] != $dest) {
1202                        @file_put_contents($module['root'] . '/_disabled', '');
1203                    }
1204                }
[2487]1205
[3699]1206                # --BEHAVIOR-- moduleBeforeUpdate
1207                $this->core->callBehavior('pluginBeforeUpdate', $module);
[2487]1208
[3699]1209                $this->store->process($module['file'], $dest);
[2487]1210
[3699]1211                # --BEHAVIOR-- moduleAfterUpdate
1212                $this->core->callBehavior('pluginAfterUpdate', $module);
[2487]1213
[3699]1214                $count++;
1215            }
[2227]1216
[3699]1217            $tab = $count && $count == count($list) ? '#plugins' : '#update';
[2171]1218
[3699]1219            dcPage::addSuccessNotice(
1220                __('Plugin has been successfully updated.', 'Plugins have been successuflly updated.', $count)
1221            );
1222            http::redirect($this->getURL() . $tab);
1223        }
[2171]1224
[3699]1225        # Manual actions
1226        elseif (!empty($_POST['upload_pkg']) && !empty($_FILES['pkg_file'])
1227            || !empty($_POST['fetch_pkg']) && !empty($_POST['pkg_url'])) {
1228            if (empty($_POST['your_pwd']) || !$this->core->auth->checkPassword($_POST['your_pwd'])) {
1229                throw new Exception(__('Password verification failed'));
1230            }
[2171]1231
[3699]1232            if (!empty($_POST['upload_pkg'])) {
1233                files::uploadStatus($_FILES['pkg_file']);
[2171]1234
[3699]1235                $dest = $this->getPath() . '/' . $_FILES['pkg_file']['name'];
1236                if (!move_uploaded_file($_FILES['pkg_file']['tmp_name'], $dest)) {
1237                    throw new Exception(__('Unable to move uploaded file.'));
1238                }
1239            } else {
1240                $url  = urldecode($_POST['pkg_url']);
1241                $dest = $this->getPath() . '/' . basename($url);
1242                $this->store->download($url, $dest);
1243            }
[2428]1244
[3699]1245            # --BEHAVIOR-- moduleBeforeAdd
1246            $this->core->callBehavior('pluginBeforeAdd', null);
[2428]1247
[3699]1248            $ret_code = $this->store->install($dest);
[2428]1249
[3699]1250            # --BEHAVIOR-- moduleAfterAdd
1251            $this->core->callBehavior('pluginAfterAdd', null);
[2171]1252
[3699]1253            dcPage::addSuccessNotice($ret_code == 2 ?
1254                __('Plugin has been successfully updated.') :
1255                __('Plugin has been successfully installed.')
1256            );
1257            http::redirect($this->getURL() . '#plugins');
1258        } else {
[2431]1259
[3699]1260            # --BEHAVIOR-- adminModulesListDoActions
1261            $this->core->callBehavior('adminModulesListDoActions', $this, $modules, 'plugin');
[2149]1262
[3699]1263        }
[2171]1264
[3699]1265        return;
1266    }
[2171]1267
[3699]1268    /**
1269     * Display tab for manual installation.
1270     *
1271     * @return    adminModulesList self instance
1272     */
1273    public function displayManualForm()
1274    {
1275        if (!$this->core->auth->isSuperAdmin() || !$this->isWritablePath()) {
1276            return;
1277        }
[2171]1278
[3699]1279        # 'Upload module' form
1280        echo
1281        '<form method="post" action="' . $this->getURL() . '" id="uploadpkg" enctype="multipart/form-data" class="fieldset">' .
1282        '<h4>' . __('Upload a zip file') . '</h4>' .
1283        '<p class="field"><label for="pkg_file" class="classic required"><abbr title="' . __('Required field') . '">*</abbr> ' . __('Zip file path:') . '</label> ' .
1284        '<input type="file" name="pkg_file" id="pkg_file" required /></p>' .
1285        '<p class="field"><label for="your_pwd1" class="classic required"><abbr title="' . __('Required field') . '">*</abbr> ' . __('Your password:') . '</label> ' .
1286        form::password(array('your_pwd', 'your_pwd1'), 20, 255,
1287            array(
1288                'extra_html'   => 'required placeholder="' . __('Password') . '"',
1289                'autocomplete' => 'current-password'
1290            )
1291        ) . '</p>' .
1292        '<p><input type="submit" name="upload_pkg" value="' . __('Upload') . '" />' .
1293        $this->core->formNonce() . '</p>' .
1294            '</form>';
[2192]1295
[3699]1296        # 'Fetch module' form
1297        echo
1298        '<form method="post" action="' . $this->getURL() . '" id="fetchpkg" class="fieldset">' .
1299        '<h4>' . __('Download a zip file') . '</h4>' .
1300        '<p class="field"><label for="pkg_url" class="classic required"><abbr title="' . __('Required field') . '">*</abbr> ' . __('Zip file URL:') . '</label> ' .
[3725]1301        form::field('pkg_url', 40, 255, array(
1302            'extra_html' => 'required placeholder="' . __('URL') . '"'
1303        )) .
1304        '</p>' .
[3699]1305        '<p class="field"><label for="your_pwd2" class="classic required"><abbr title="' . __('Required field') . '">*</abbr> ' . __('Your password:') . '</label> ' .
1306        form::password(array('your_pwd', 'your_pwd2'), 20, 255,
1307            array(
1308                'extra_html'   => 'required placeholder="' . __('Password') . '"',
1309                'autocomplete' => 'current-password'
1310            )
1311        ) . '</p>' .
1312        '<p><input type="submit" name="fetch_pkg" value="' . __('Download') . '" />' .
1313        $this->core->formNonce() . '</p>' .
1314            '</form>';
[2487]1315
[3699]1316        return $this;
1317    }
1318    //@}
[2487]1319
[3699]1320    /// @name Module configuration methods
1321    //@{
1322    /**
1323     * Prepare module configuration.
1324     *
1325     * We need to get configuration content in three steps
1326     * and out of this class to keep backward compatibility.
1327     *
1328     * if ($xxx->setConfiguration()) {
1329     *    include $xxx->includeConfiguration();
1330     * }
1331     * $xxx->getConfiguration();
1332     * ... [put here page headers and other stuff]
1333     * $xxx->displayConfiguration();
1334     *
1335     * @param    string    $id        Module to work on or it gather through REQUEST
1336     * @return    True if config set
1337     */
1338    public function setConfiguration($id = null)
1339    {
1340        if (empty($_REQUEST['conf']) || empty($_REQUEST['module']) && !$id) {
1341            return false;
1342        }
[2487]1343
[3699]1344        if (!empty($_REQUEST['module']) && empty($id)) {
1345            $id = $_REQUEST['module'];
1346        }
[2171]1347
[3699]1348        if (!$this->modules->moduleExists($id)) {
1349            $this->core->error->add(__('Unknow plugin ID'));
1350            return false;
1351        }
[2149]1352
[3699]1353        $module = $this->modules->getModules($id);
1354        $module = self::sanitizeModule($id, $module);
1355        $file   = path::real($module['root'] . '/_config.php');
[2227]1356
[3699]1357        if (!file_exists($file)) {
1358            $this->core->error->add(__('This plugin has no configuration file.'));
1359            return false;
1360        }
[2227]1361
[3699]1362        $this->config_module  = $module;
1363        $this->config_file    = $file;
1364        $this->config_content = '';
[2149]1365
[3699]1366        if (!defined('DC_CONTEXT_MODULE')) {
1367            define('DC_CONTEXT_MODULE', true);
1368        }
[2431]1369
[3699]1370        return true;
1371    }
[2182]1372
[3699]1373    /**
1374     * Get path of module configuration file.
1375     *
1376     * @note Required previously set file info
1377     * @return Full path of config file or null
1378     */
1379    public function includeConfiguration()
1380    {
1381        if (!$this->config_file) {
1382            return;
1383        }
1384        $this->setRedir($this->getURL() . '#plugins');
[2182]1385
[3699]1386        ob_start();
[2182]1387
[3699]1388        return $this->config_file;
1389    }
[2182]1390
[3699]1391    /**
1392     * Gather module configuration file content.
1393     *
1394     * @note Required previously file inclusion
1395     * @return True if content has been captured
1396     */
1397    public function getConfiguration()
1398    {
1399        if ($this->config_file) {
1400            $this->config_content = ob_get_contents();
1401        }
[2182]1402
[3699]1403        ob_end_clean();
[2182]1404
[3699]1405        return !empty($this->file_content);
1406    }
[2182]1407
[3699]1408    /**
1409     * Display module configuration form.
1410     *
1411     * @note Required previously gathered content
1412     * @return    adminModulesList self instance
1413     */
1414    public function displayConfiguration()
1415    {
1416        if ($this->config_file) {
[2182]1417
[3699]1418            if (!$this->config_module['standalone_config']) {
1419                echo
1420                '<form id="module_config" action="' . $this->getURL('conf=1') . '" method="post" enctype="multipart/form-data">' .
1421                '<h3>' . sprintf(__('Configure "%s"'), html::escapeHTML($this->config_module['name'])) . '</h3>' .
1422                '<p><a class="back" href="' . $this->getRedir() . '">' . __('Back') . '</a></p>';
1423            }
[2182]1424
[3699]1425            echo $this->config_content;
[2182]1426
[3699]1427            if (!$this->config_module['standalone_config']) {
1428                echo
1429                '<p class="clear"><input type="submit" name="save" value="' . __('Save') . '" />' .
1430                form::hidden('module', $this->config_module['id']) .
1431                form::hidden('redir', $this->getRedir()) .
1432                $this->core->formNonce() . '</p>' .
1433                    '</form>';
1434            }
1435        }
[2182]1436
[3699]1437        return $this;
1438    }
1439    //@}
[2182]1440
[3699]1441    /**
1442     * Helper to sanitize a string.
1443     *
1444     * Used for search or id.
1445     *
1446     * @param    string    $str        String to sanitize
1447     * @return    Sanitized string
1448     */
1449    public static function sanitizeString($str)
1450    {
1451        return preg_replace('/[^A-Za-z0-9\@\#+_-]/', '', strtolower($str));
1452    }
[2147]1453}
1454
[2227]1455/**
1456 * @ingroup DC_CORE
1457 * @brief Helper to manage list of themes.
1458 * @since 2.6
1459 */
[2147]1460class adminThemesList extends adminModulesList
1461{
[3699]1462    /**
1463     * Constructor.
1464     *
1465     * Note that this creates dcStore instance.
1466     *
1467     * @param    object    $modules        dcModules instance
1468     * @param    string    $modules_root    Modules root directories
1469     * @param    string    $xml_url        URL of modules feed from repository
1470     */
1471    public function __construct(dcModules $modules, $modules_root, $xml_url)
1472    {
1473        parent::__construct($modules, $modules_root, $xml_url);
1474        $this->page_url = $this->core->adminurl->get('admin.blog.theme');
1475    }
[2147]1476
[3699]1477    public function displayModules($cols = array('name', 'config', 'version', 'desc'), $actions = array(), $nav_limit = false)
1478    {
1479        echo
1480        '<form action="' . $this->getURL() . '" method="post" class="modules-form-actions">' .
1481        '<div id="' . html::escapeHTML($this->list_id) . '" class="modules' . (in_array('expander', $cols) ? ' expandable' : '') . ' one-box">';
[2171]1482
[3699]1483        $sort_field = $this->getSort();
[2171]1484
[3699]1485        # Sort modules by id
1486        $modules = $this->getSearch() === null ?
1487        self::sortModules($this->data, $sort_field, $this->sort_asc) :
1488        $this->data;
[2171]1489
[3699]1490        $res   = '';
1491        $count = 0;
1492        foreach ($modules as $id => $module) {
1493            # Show only requested modules
1494            if ($nav_limit && $this->getSearch() === null) {
1495                $char = substr($module[$sort_field], 0, 1);
1496                if (!in_array($char, $this->nav_list)) {
1497                    $char = $this->nav_special;
1498                }
1499                if ($this->getIndex() != $char) {
1500                    continue;
1501                }
1502            }
[2171]1503
[3699]1504            $current = $this->core->blog->settings->system->theme == $id && $this->modules->moduleExists($id);
1505            $distrib = self::isDistributedModule($id) ? ' dc-box' : '';
[2171]1506
[3699]1507            $line =
1508                '<div class="box ' . ($current ? 'medium current-theme' : 'theme') . $distrib . '">';
[2171]1509
[3699]1510            if (in_array('name', $cols) && !$current) {
1511                $line .=
1512                    '<h4 class="module-name">';
[2487]1513
[3699]1514                if (in_array('checkbox', $cols)) {
1515                    $line .=
1516                    '<label for="' . html::escapeHTML($this->list_id) . '_modules_' . html::escapeHTML($id) . '">' .
1517                    form::checkbox(array('modules[' . $count . ']', html::escapeHTML($this->list_id) . '_modules_' . html::escapeHTML($id)), html::escapeHTML($id)) .
1518                    html::escapeHTML($module['name']) .
1519                        '</label>';
[2487]1520
[3699]1521                } else {
1522                    $line .=
1523                    form::hidden(array('modules[' . $count . ']'), html::escapeHTML($id)) .
1524                    html::escapeHTML($module['name']);
1525                }
[2487]1526
[3699]1527                $line .=
1528                $this->core->formNonce() .
1529                    '</h4>';
1530            }
[2171]1531
[3699]1532            # Display score only for debug purpose
1533            if (in_array('score', $cols) && $this->getSearch() !== null && defined('DC_DEBUG') && DC_DEBUG) {
1534                $line .=
1535                '<p class="module-score debug">' . sprintf(__('Score: %s'), $module['score']) . '</p>';
1536            }
[2222]1537
[3699]1538            if (in_array('sshot', $cols)) {
1539                # Screenshot from url
1540                if (preg_match('#^http(s)?://#', $module['sshot'])) {
1541                    $sshot = $module['sshot'];
1542                }
1543                # Screenshot from installed module
1544                elseif (file_exists($this->core->blog->themes_path . '/' . $id . '/screenshot.jpg')) {
1545                    $sshot = $this->getURL('shot=' . rawurlencode($id));
1546                }
1547                # Default screenshot
1548                else {
1549                    $sshot = 'images/noscreenshot.png';
1550                }
[2195]1551
[3699]1552                $line .=
1553                '<div class="module-sshot"><img src="' . $sshot . '" alt="' .
1554                sprintf(__('%s screenshot.'), html::escapeHTML($module['name'])) . '" /></div>';
1555            }
[2195]1556
[3699]1557            $line .=
1558                '<div class="module-infos toggle-bloc">';
[2347]1559
[3699]1560            if (in_array('name', $cols) && $current) {
1561                $line .=
1562                    '<h4 class="module-name">';
[2487]1563
[3699]1564                if (in_array('checkbox', $cols)) {
1565                    $line .=
1566                    '<label for="' . html::escapeHTML($this->list_id) . '_modules_' . html::escapeHTML($id) . '">' .
1567                    form::checkbox(array('modules[' . $count . ']', html::escapeHTML($this->list_id) . '_modules_' . html::escapeHTML($id)), html::escapeHTML($id)) .
1568                    html::escapeHTML($module['name']) .
1569                        '</label>';
1570                } else {
1571                    $line .=
1572                    form::hidden(array('modules[' . $count . ']'), html::escapeHTML($id)) .
1573                    html::escapeHTML($module['name']);
1574                }
[2487]1575
[3699]1576                $line .=
1577                    '</h4>';
1578            }
[2347]1579
[3699]1580            $line .=
1581                '<p>';
[2171]1582
[3699]1583            if (in_array('desc', $cols)) {
1584                $line .=
1585                '<span class="module-desc">' . html::escapeHTML(__($module['desc'])) . '</span> ';
1586            }
[2171]1587
[3699]1588            if (in_array('author', $cols)) {
1589                $line .=
1590                '<span class="module-author">' . sprintf(__('by %s'), html::escapeHTML($module['author'])) . '</span> ';
1591            }
[2171]1592
[3699]1593            if (in_array('version', $cols)) {
1594                $line .=
1595                '<span class="module-version">' . sprintf(__('version %s'), html::escapeHTML($module['version'])) . '</span> ';
1596            }
[2171]1597
[3699]1598            if (in_array('current_version', $cols)) {
1599                $line .=
1600                '<span class="module-current-version">' . sprintf(__('(current version %s)'), html::escapeHTML($module['current_version'])) . '</span> ';
1601            }
[2222]1602
[3699]1603            if (in_array('parent', $cols) && !empty($module['parent'])) {
1604                if ($this->modules->moduleExists($module['parent'])) {
1605                    $line .=
1606                    '<span class="module-parent-ok">' . sprintf(__('(built on "%s")'), html::escapeHTML($module['parent'])) . '</span> ';
1607                } else {
1608                    $line .=
1609                    '<span class="module-parent-missing">' . sprintf(__('(requires "%s")'), html::escapeHTML($module['parent'])) . '</span> ';
1610                }
1611            }
[2171]1612
[3699]1613            $has_details = in_array('details', $cols) && !empty($module['details']);
1614            $has_support = in_array('support', $cols) && !empty($module['support']);
1615            if ($has_details || $has_support) {
1616                $line .=
1617                    '<span class="mod-more">';
[2215]1618
[3699]1619                if ($has_details) {
1620                    $line .=
1621                    '<a class="module-details" href="' . $module['details'] . '">' . __('Details') . '</a>';
1622                }
[2215]1623
[3699]1624                if ($has_support) {
1625                    $line .=
1626                    ' - <a class="module-support" href="' . $module['support'] . '">' . __('Support') . '</a>';
1627                }
[2215]1628
[3699]1629                $line .=
1630                    '</span>';
1631            }
[2215]1632
[3699]1633            $line .=
1634                '</p>' .
1635                '</div>';
[2171]1636
[3699]1637            $line .=
1638                '<div class="module-actions toggle-bloc">';
[2431]1639
[3699]1640            # Plugins actions
1641            if ($current) {
[2227]1642
[3699]1643                # _GET actions
1644                if (file_exists(path::real($this->core->blog->themes_path . '/' . $id) . '/style.css')) {
1645                    $theme_url = preg_match('#^http(s)?://#', $this->core->blog->settings->system->themes_url) ?
1646                    http::concatURL($this->core->blog->settings->system->themes_url, '/' . $id) :
1647                    http::concatURL($this->core->blog->url, $this->core->blog->settings->system->themes_url . '/' . $id);
1648                    $line .=
1649                    '<p><a href="' . $theme_url . '/style.css">' . __('View stylesheet') . '</a></p>';
1650                }
[2227]1651
[3699]1652                $line .= '<div class="current-actions">';
[2260]1653
[3699]1654                if (file_exists(path::real($this->core->blog->themes_path . '/' . $id) . '/_config.php')) {
1655                    $line .=
1656                    '<p><a href="' . $this->getURL('module=' . $id . '&amp;conf=1', false) . '" class="button submit">' . __('Configure theme') . '</a></p>';
1657                }
[2227]1658
[3699]1659                # --BEHAVIOR-- adminCurrentThemeDetails
1660                $line .=
1661                $this->core->callBehavior('adminCurrentThemeDetails', $this->core, $id, $module);
[2260]1662
[3699]1663                $line .= '</div>';
1664            }
[2171]1665
[3699]1666            # _POST actions
1667            if (!empty($actions)) {
1668                $line .=
1669                '<p>' . implode(' ', $this->getActions($id, $module, $actions)) . '</p>';
1670            }
[2171]1671
[3699]1672            $line .=
1673                '</div>';
[2171]1674
[3699]1675            $line .=
1676                '</div>';
[2171]1677
[3699]1678            $count++;
[2171]1679
[3699]1680            $res = $current ? $line . $res : $res . $line;
1681        }
[2487]1682
[3699]1683        echo
1684            $res .
1685            '</div>';
[2171]1686
[3699]1687        if (!$count && $this->getSearch() === null) {
1688            echo
1689            '<p class="message">' . __('No themes matched your search.') . '</p>';
1690        } elseif ((in_array('checkbox', $cols) || $count > 1) && !empty($actions) && $this->core->auth->isSuperAdmin()) {
1691            $buttons = $this->getGlobalActions($actions, in_array('checkbox', $cols));
[2428]1692
[3699]1693            if (!empty($buttons)) {
1694                if (in_array('checkbox', $cols)) {
1695                    echo
1696                        '<p class="checkboxes-helpers"></p>';
1697                }
1698                echo '<div>' . implode(' ', $buttons) . '</div>';
1699            }
1700        }
[2428]1701
[3699]1702        echo
1703            '</form>';
[2428]1704
[3699]1705        return $this;
1706    }
[2428]1707
[3699]1708    protected function getActions($id, $module, $actions)
1709    {
1710        $submits = array();
[2192]1711
[3699]1712        $this->core->blog->settings->addNamespace('system');
1713        if ($id != $this->core->blog->settings->system->theme) {
[2192]1714
[3699]1715            # Select theme to use on curent blog
1716            if (in_array('select', $actions)) {
1717                $submits[] =
1718                '<input type="submit" name="select[' . html::escapeHTML($id) . ']" value="' . __('Use this one') . '" />';
1719            }
1720        }
[2215]1721
[3699]1722        return array_merge(
1723            $submits,
1724            parent::getActions($id, $module, $actions)
1725        );
1726    }
[2192]1727
[3699]1728    protected function getGlobalActions($actions, $with_selection = false)
1729    {
1730        $submits = array();
[2215]1731
[3699]1732        foreach ($actions as $action) {
1733            switch ($action) {
[2428]1734
[3699]1735                # Update (from store)
1736                case 'update':if ($this->core->auth->isSuperAdmin() && $this->path_writable) {
1737                        $submits[] =
1738                        '<input type="submit" name="update" value="' . ($with_selection ?
1739                            __('Update selected themes') :
1740                            __('Update all themes from this list')
1741                        ) . '" />' . $this->core->formNonce();
1742                    }break;
[2428]1743
[3699]1744                # Behavior
1745                case 'behavior':
[2428]1746
[3699]1747                    # --BEHAVIOR-- adminModulesListGetGlobalActions
1748                    $tmp = $this->core->callBehavior('adminModulesListGetGlobalActions', $this);
[2428]1749
[3699]1750                    if (!empty($tmp)) {
1751                        $submits[] = $tmp;
1752                    }
1753                    break;
1754            }
1755        }
[2428]1756
[3699]1757        return $submits;
1758    }
[2428]1759
[3699]1760    public function doActions()
1761    {
1762        if (empty($_POST) || !empty($_REQUEST['conf'])) {
1763            return;
1764        }
[2428]1765
[3699]1766        $modules = !empty($_POST['modules']) && is_array($_POST['modules']) ? array_values($_POST['modules']) : array();
[2215]1767
[3699]1768        if (!empty($_POST['select'])) {
[2377]1769
[3699]1770            # Can select only one theme at a time!
1771            if (is_array($_POST['select'])) {
1772                $modules = array_keys($_POST['select']);
1773                $id      = $modules[0];
[2377]1774
[3699]1775                if (!$this->modules->moduleExists($id)) {
1776                    throw new Exception(__('No such theme.'));
1777                }
[2215]1778
[3699]1779                $this->core->blog->settings->addNamespace('system');
1780                $this->core->blog->settings->system->put('theme', $id);
1781                $this->core->blog->triggerBlog();
[2215]1782
[3699]1783                dcPage::addSuccessNotice(__('Theme has been successfully selected.'));
1784                http::redirect($this->getURL() . '#themes');
1785            }
1786        } else {
1787            if (!$this->isWritablePath()) {
1788                return;
1789            }
[2215]1790
[3699]1791            if ($this->core->auth->isSuperAdmin() && !empty($_POST['activate'])) {
[2377]1792
[3699]1793                if (is_array($_POST['activate'])) {
1794                    $modules = array_keys($_POST['activate']);
1795                }
[2487]1796
[3699]1797                $list = $this->modules->getDisabledModules();
1798                if (empty($list)) {
1799                    throw new Exception(__('No such theme.'));
1800                }
[3221]1801
[3699]1802                $count = 0;
1803                foreach ($list as $id => $module) {
[3221]1804
[3699]1805                    if (!in_array($id, $modules)) {
1806                        continue;
1807                    }
[3221]1808
[3699]1809                    # --BEHAVIOR-- themeBeforeActivate
1810                    $this->core->callBehavior('themeBeforeActivate', $id);
[3221]1811
[3699]1812                    $this->modules->activateModule($id);
[3221]1813
[3699]1814                    # --BEHAVIOR-- themeAfterActivate
1815                    $this->core->callBehavior('themeAfterActivate', $id);
[3221]1816
[3699]1817                    $count++;
1818                }
[3221]1819
[3699]1820                dcPage::addSuccessNotice(
1821                    __('Theme has been successfully activated.', 'Themes have been successuflly activated.', $count)
1822                );
1823                http::redirect($this->getURL());
1824            } elseif ($this->core->auth->isSuperAdmin() && !empty($_POST['deactivate'])) {
[3221]1825
[3699]1826                if (is_array($_POST['deactivate'])) {
1827                    $modules = array_keys($_POST['deactivate']);
1828                }
[3221]1829
[3699]1830                $list = $this->modules->getModules();
1831                if (empty($list)) {
1832                    throw new Exception(__('No such theme.'));
1833                }
[2487]1834
[3699]1835                $failed = false;
1836                $count  = 0;
1837                foreach ($list as $id => $module) {
[2487]1838
[3699]1839                    if (!in_array($id, $modules)) {
1840                        continue;
1841                    }
[2377]1842
[3699]1843                    if (!$module['root_writable']) {
1844                        $failed = true;
1845                        continue;
1846                    }
[2377]1847
[3699]1848                    $module[$id] = $id;
[2377]1849
[3699]1850                    # --BEHAVIOR-- themeBeforeDeactivate
1851                    $this->core->callBehavior('themeBeforeDeactivate', $module);
[2377]1852
[3699]1853                    $this->modules->deactivateModule($id);
[2377]1854
[3699]1855                    # --BEHAVIOR-- themeAfterDeactivate
1856                    $this->core->callBehavior('themeAfterDeactivate', $module);
[2377]1857
[3699]1858                    $count++;
1859                }
[2377]1860
[3699]1861                if ($failed) {
1862                    dcPage::addWarningNotice(__('Some themes have not been deactivated.'));
1863                } else {
1864                    dcPage::addSuccessNotice(
1865                        __('Theme has been successfully deactivated.', 'Themes have been successuflly deactivated.', $count)
1866                    );
1867                }
1868                http::redirect($this->getURL());
1869            } elseif ($this->core->auth->isSuperAdmin() && !empty($_POST['delete'])) {
[3221]1870
[3699]1871                if (is_array($_POST['delete'])) {
1872                    $modules = array_keys($_POST['delete']);
1873                }
[3221]1874
[3699]1875                $list = $this->modules->getDisabledModules();
[3221]1876
[3699]1877                $failed = false;
1878                $count  = 0;
1879                foreach ($modules as $id) {
1880                    if (!isset($list[$id])) {
[3221]1881
[3699]1882                        if (!$this->modules->moduleExists($id)) {
1883                            throw new Exception(__('No such theme.'));
1884                        }
[3221]1885
[3699]1886                        $module       = $this->modules->getModules($id);
1887                        $module['id'] = $id;
[2377]1888
[3699]1889                        if (!$this->isDeletablePath($module['root'])) {
1890                            $failed = true;
1891                            continue;
1892                        }
[3221]1893
[3699]1894                        # --BEHAVIOR-- themeBeforeDelete
1895                        $this->core->callBehavior('themeBeforeDelete', $module);
[3221]1896
[3699]1897                        $this->modules->deleteModule($id);
[3221]1898
[3699]1899                        # --BEHAVIOR-- themeAfterDelete
1900                        $this->core->callBehavior('themeAfterDelete', $module);
1901                    } else {
1902                        $this->modules->deleteModule($id, true);
1903                    }
[3221]1904
[3699]1905                    $count++;
1906                }
[3221]1907
[3699]1908                if (!$count && $failed) {
1909                    throw new Exception(__("You don't have permissions to delete this theme."));
1910                } elseif ($failed) {
1911                    dcPage::addWarningNotice(__('Some themes have not been delete.'));
1912                } else {
1913                    dcPage::addSuccessNotice(
1914                        __('Theme has been successfully deleted.', 'Themes have been successuflly deleted.', $count)
1915                    );
1916                }
1917                http::redirect($this->getURL());
1918            } elseif ($this->core->auth->isSuperAdmin() && !empty($_POST['install'])) {
[3221]1919
[3699]1920                if (is_array($_POST['install'])) {
1921                    $modules = array_keys($_POST['install']);
1922                }
[3221]1923
[3699]1924                $list = $this->store->get();
[3221]1925
[3699]1926                if (empty($list)) {
1927                    throw new Exception(__('No such theme.'));
1928                }
[3221]1929
[3699]1930                $count = 0;
1931                foreach ($list as $id => $module) {
[2377]1932
[3699]1933                    if (!in_array($id, $modules)) {
1934                        continue;
1935                    }
[3221]1936
[3699]1937                    $dest = $this->getPath() . '/' . basename($module['file']);
[3221]1938
[3699]1939                    # --BEHAVIOR-- themeBeforeAdd
1940                    $this->core->callBehavior('themeBeforeAdd', $module);
[3221]1941
[3699]1942                    $this->store->process($module['file'], $dest);
[3221]1943
[3699]1944                    # --BEHAVIOR-- themeAfterAdd
1945                    $this->core->callBehavior('themeAfterAdd', $module);
[3221]1946
[3699]1947                    $count++;
1948                }
[3221]1949
[3699]1950                dcPage::addSuccessNotice(
1951                    __('Theme has been successfully installed.', 'Themes have been successuflly installed.', $count)
1952                );
1953                http::redirect($this->getURL());
1954            } elseif ($this->core->auth->isSuperAdmin() && !empty($_POST['update'])) {
[3221]1955
[3699]1956                if (is_array($_POST['update'])) {
1957                    $modules = array_keys($_POST['update']);
1958                }
[3221]1959
[3699]1960                $list = $this->store->get(true);
1961                if (empty($list)) {
1962                    throw new Exception(__('No such theme.'));
1963                }
[3221]1964
[3699]1965                $count = 0;
1966                foreach ($list as $module) {
[3221]1967
[3699]1968                    if (!in_array($module['id'], $modules)) {
1969                        continue;
1970                    }
[3221]1971
[3699]1972                    $dest = $module['root'] . '/../' . basename($module['file']);
[2487]1973
[3699]1974                    # --BEHAVIOR-- themeBeforeUpdate
1975                    $this->core->callBehavior('themeBeforeUpdate', $module);
[2487]1976
[3699]1977                    $this->store->process($module['file'], $dest);
[3221]1978
[3699]1979                    # --BEHAVIOR-- themeAfterUpdate
1980                    $this->core->callBehavior('themeAfterUpdate', $module);
[3221]1981
[3699]1982                    $count++;
1983                }
[3221]1984
[3699]1985                $tab = $count && $count == count($list) ? '#themes' : '#update';
[3221]1986
[3699]1987                dcPage::addSuccessNotice(
1988                    __('Theme has been successfully updated.', 'Themes have been successuflly updated.', $count)
1989                );
1990                http::redirect($this->getURL() . $tab);
1991            }
[3221]1992
[3699]1993            # Manual actions
1994            elseif (!empty($_POST['upload_pkg']) && !empty($_FILES['pkg_file'])
1995                || !empty($_POST['fetch_pkg']) && !empty($_POST['pkg_url'])) {
1996                if (empty($_POST['your_pwd']) || !$this->core->auth->checkPassword($_POST['your_pwd'])) {
1997                    throw new Exception(__('Password verification failed'));
1998                }
[3221]1999
[3699]2000                if (!empty($_POST['upload_pkg'])) {
2001                    files::uploadStatus($_FILES['pkg_file']);
[3221]2002
[3699]2003                    $dest = $this->getPath() . '/' . $_FILES['pkg_file']['name'];
2004                    if (!move_uploaded_file($_FILES['pkg_file']['tmp_name'], $dest)) {
2005                        throw new Exception(__('Unable to move uploaded file.'));
2006                    }
2007                } else {
2008                    $url  = urldecode($_POST['pkg_url']);
2009                    $dest = $this->getPath() . '/' . basename($url);
2010                    $this->store->download($url, $dest);
2011                }
[3221]2012
[3699]2013                # --BEHAVIOR-- themeBeforeAdd
2014                $this->core->callBehavior('themeBeforeAdd', null);
[3221]2015
[3699]2016                $ret_code = $this->store->install($dest);
[3221]2017
[3699]2018                # --BEHAVIOR-- themeAfterAdd
2019                $this->core->callBehavior('themeAfterAdd', null);
[2487]2020
[3699]2021                dcPage::addSuccessNotice($ret_code == 2 ?
2022                    __('Theme has been successfully updated.') :
2023                    __('Theme has been successfully installed.')
2024                );
2025                http::redirect($this->getURL() . '#themes');
2026            } else {
[2377]2027
[3699]2028                # --BEHAVIOR-- adminModulesListDoActions
2029                $this->core->callBehavior('adminModulesListDoActions', $this, $modules, 'theme');
[3221]2030
[3699]2031            }
2032        }
[2377]2033
[3699]2034        return;
2035    }
[2147]2036}
Note: See TracBrowser for help on using the repository browser.

Sites map