Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
74.87% covered (warning)
74.87%
143 / 191
44.44% covered (danger)
44.44%
4 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
module
74.87% covered (warning)
74.87%
143 / 191
44.44% covered (danger)
44.44%
4 / 9
154.28
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 get_name
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 exists
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
11
 add
70.33% covered (warning)
70.33%
64 / 91
0.00% covered (danger)
0.00%
0 / 1
56.10
 remove
62.50% covered (warning)
62.50%
20 / 32
0.00% covered (danger)
0.00%
0 / 1
19.59
 reverse
53.33% covered (warning)
53.33%
8 / 15
0.00% covered (danger)
0.00%
0 / 1
7.54
 get_module_info
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 get_categories_list
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 get_parent_module_id
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
7.02
1<?php
2/**
3*
4* This file is part of the phpBB Forum Software package.
5*
6* @copyright (c) phpBB Limited <https://www.phpbb.com>
7* @license GNU General Public License, version 2 (GPL-2.0)
8*
9* For full copyright and license information, please see
10* the docs/CREDITS.txt file.
11*
12*/
13
14namespace phpbb\db\migration\tool;
15
16use phpbb\module\exception\module_exception;
17
18/**
19* Migration module management tool
20*/
21class module implements \phpbb\db\migration\tool\tool_interface
22{
23    /** @var \phpbb\db\driver\driver_interface */
24    protected $db;
25
26    /** @var \phpbb\user */
27    protected $user;
28
29    /** @var \phpbb\module\module_manager */
30    protected $module_manager;
31
32    /** @var string */
33    protected $modules_table;
34
35    /** @var array */
36    protected $module_categories = array();
37
38    /**
39    * Constructor
40    *
41    * @param \phpbb\db\driver\driver_interface $db
42    * @param \phpbb\user $user
43    * @param \phpbb\module\module_manager    $module_manager
44    * @param string $modules_table
45    */
46    public function __construct(\phpbb\db\driver\driver_interface $db, \phpbb\user $user, \phpbb\module\module_manager $module_manager, $modules_table)
47    {
48        $this->db = $db;
49        $this->user = $user;
50        $this->module_manager = $module_manager;
51        $this->modules_table = $modules_table;
52    }
53
54    /**
55    * {@inheritdoc}
56    */
57    public function get_name()
58    {
59        return 'module';
60    }
61
62    /**
63    * Module Exists
64    *
65    * Check if a module exists
66    *
67    * @param string $class The module class(acp|mcp|ucp)
68    * @param int|string|bool $parent The parent module_id|module_langname (0 for no parent).
69    *        Use false to ignore the parent check and check class wide.
70    * @param int|string $module The module_id|module_langname you would like to
71    *         check for to see if it exists
72    * @param bool $lazy Checks lazily if the module exists. Returns true if it exists in at
73    *       least one given parent.
74    * @return bool true if module exists in *all* given parents, false if not in any given parent;
75     *      true if ignoring parent check and module exists class wide, false if not found at all.
76    */
77    public function exists($class, $parent, $module, $lazy = false)
78    {
79        // the main root directory should return true
80        if (!$module)
81        {
82            return true;
83        }
84
85        $parent_sqls = [];
86        if ($parent !== false)
87        {
88            $parents = $this->get_parent_module_id($parent, $module, false);
89            if ($parents === false)
90            {
91                return false;
92            }
93
94            foreach ((array) $parents as $parent_id)
95            {
96                $parent_sqls[] = 'AND parent_id = ' . (int) $parent_id;
97            }
98        }
99        else
100        {
101            $parent_sqls[] = '';
102        }
103
104        foreach ($parent_sqls as $parent_sql)
105        {
106            /** @psalm-suppress NoValue */
107            $sql = 'SELECT module_id
108                FROM ' . $this->modules_table . "
109                WHERE module_class = '" . $this->db->sql_escape($class) . "'
110                    $parent_sql
111                    AND " . ((is_numeric($module)) ? 'module_id = ' . (int) $module : "module_langname = '" . $this->db->sql_escape($module) . "'");
112            $result = $this->db->sql_query($sql);
113            $module_id = $this->db->sql_fetchfield('module_id');
114            $this->db->sql_freeresult($result);
115
116            if (!$lazy && !$module_id)
117            {
118                return false;
119            }
120            if ($lazy && $module_id)
121            {
122                return true;
123            }
124        }
125
126        // Returns true, if modules exist in all parents and false otherwise
127        return !$lazy;
128    }
129
130    /**
131    * Module Add
132    *
133    * Add a new module
134    *
135    * @param string $class The module class(acp|mcp|ucp)
136    * @param int|string $parent The parent module_id|module_langname (0 for no parent)
137    * @param array $data an array of the data on the new \module.
138    *     This can be setup in two different ways.
139    *    1. The "manual" way.  For inserting a category or one at a time.
140    *        It will be merged with the base array shown a bit below,
141    *            but at the least requires 'module_langname' to be sent, and,
142    *            if you want to create a module (instead of just a category) you must
143    *            send module_basename and module_mode.
144    *        array(
145    *            'module_enabled'    => 1,
146    *            'module_display'    => 1,
147    *               'module_basename'    => '',
148    *            'module_class'        => $class,
149    *               'parent_id'            => (int) $parent,
150    *            'module_langname'    => '',
151    *               'module_mode'        => '',
152    *               'module_auth'        => '',
153    *        )
154    *    2. The "automatic" way.  For inserting multiple at a time based on the
155    *            specs in the info file for the module(s).  For this to work the
156    *            modules must be correctly setup in the info file.
157    *        An example follows (this would insert the settings, log, and flag
158    *            modes from the includes/acp/info/acp_asacp.php file):
159    *         array(
160    *             'module_basename'    => 'asacp',
161    *             'modes'                => array('settings', 'log', 'flag'),
162    *         )
163    *         Optionally you may not send 'modes' and it will insert all of the
164    *             modules in that info file.
165    *     path, specify that here
166    * @return void
167    * @throws \phpbb\db\migration\exception
168    */
169    public function add($class, $parent = 0, $data = array())
170    {
171        global $user, $phpbb_log;
172
173        // allow sending the name as a string in $data to create a category
174        if (!is_array($data))
175        {
176            $data = array('module_langname' => $data);
177        }
178
179        $parents = (array) $this->get_parent_module_id($parent, $data);
180
181        if (!isset($data['module_langname']))
182        {
183            // The "automatic" way
184            $basename = (isset($data['module_basename'])) ? $data['module_basename'] : '';
185            $module = $this->get_module_info($class, $basename);
186
187            foreach ($module['modes'] as $mode => $module_info)
188            {
189                if (!isset($data['modes']) || in_array($mode, $data['modes']))
190                {
191                    $new_module = array(
192                        'module_basename'    => $basename,
193                        'module_langname'    => $module_info['title'],
194                        'module_mode'        => $mode,
195                        'module_auth'        => $module_info['auth'],
196                        'module_display'    => (isset($module_info['display'])) ? $module_info['display'] : true,
197                        'before'            => (isset($module_info['before'])) ? $module_info['before'] : false,
198                        'after'                => (isset($module_info['after'])) ? $module_info['after'] : false,
199                    );
200
201                    // Run the "manual" way with the data we've collected.
202                    foreach ($parents as $parent)
203                    {
204                        $this->add($class, $parent, $new_module);
205                    }
206                }
207            }
208
209            return;
210        }
211
212        foreach ($parents as $parent)
213        {
214            $data['parent_id'] = $parent;
215
216            // The "manual" way
217            if (!$this->exists($class, false, $parent))
218            {
219                throw new \phpbb\db\migration\exception('MODULE_NOT_EXIST', $parent);
220            }
221
222            if ($this->exists($class, $parent, $data['module_langname']))
223            {
224                throw new \phpbb\db\migration\exception('MODULE_EXISTS', $data['module_langname']);
225            }
226
227            $module_data = array(
228                'module_enabled'    => (isset($data['module_enabled'])) ? $data['module_enabled'] : 1,
229                'module_display'    => (isset($data['module_display'])) ? $data['module_display'] : 1,
230                'module_basename'    => (isset($data['module_basename'])) ? $data['module_basename'] : '',
231                'module_class'        => $class,
232                'parent_id'            => (int) $parent,
233                'module_langname'    => (isset($data['module_langname'])) ? $data['module_langname'] : '',
234                'module_mode'        => (isset($data['module_mode'])) ? $data['module_mode'] : '',
235                'module_auth'        => (isset($data['module_auth'])) ? $data['module_auth'] : '',
236            );
237
238            try
239            {
240                $this->module_manager->update_module_data($module_data);
241
242                // Success
243                $module_log_name = ((isset($this->user->lang[$data['module_langname']])) ? $this->user->lang[$data['module_langname']] : $data['module_langname']);
244                $phpbb_log->add('admin', (isset($user->data['user_id'])) ? $user->data['user_id'] : ANONYMOUS, $user->ip, 'LOG_MODULE_ADD', false, array($module_log_name));
245
246                // Move the module if requested above/below an existing one
247                if (isset($data['before']) && $data['before'])
248                {
249                    $before_mode = $before_langname = '';
250                    if (is_array($data['before']))
251                    {
252                        // Restore legacy-legacy behaviour from phpBB 3.0
253                        list($before_mode, $before_langname) = $data['before'];
254                    }
255                    else
256                    {
257                        // Legacy behaviour from phpBB 3.1+
258                        $before_langname = $data['before'];
259                    }
260
261                    $sql = 'SELECT left_id
262                    FROM ' . $this->modules_table . "
263                    WHERE module_class = '" . $this->db->sql_escape($class) . "'
264                        AND parent_id = " . (int) $parent . "
265                        AND module_langname = '" . $this->db->sql_escape($before_langname) . "'"
266                        . (($before_mode) ? " AND module_mode = '" . $this->db->sql_escape($before_mode) . "'" : '');
267                    $result = $this->db->sql_query($sql);
268                    $to_left = (int) $this->db->sql_fetchfield('left_id');
269                    $this->db->sql_freeresult($result);
270
271                    $sql = 'UPDATE ' . $this->modules_table . "
272                    SET left_id = left_id + 2, right_id = right_id + 2
273                    WHERE module_class = '" . $this->db->sql_escape($class) . "'
274                        AND left_id >= $to_left
275                        AND left_id < {$module_data['left_id']}";
276                    $this->db->sql_query($sql);
277
278                    $sql = 'UPDATE ' . $this->modules_table . "
279                    SET left_id = $to_left, right_id = " . ($to_left + 1) . "
280                    WHERE module_class = '" . $this->db->sql_escape($class) . "'
281                        AND module_id = {$module_data['module_id']}";
282                    $this->db->sql_query($sql);
283                }
284                else if (isset($data['after']) && $data['after'])
285                {
286                    $after_mode = $after_langname = '';
287                    if (is_array($data['after']))
288                    {
289                        // Restore legacy-legacy behaviour from phpBB 3.0
290                        list($after_mode, $after_langname) = $data['after'];
291                    }
292                    else
293                    {
294                        // Legacy behaviour from phpBB 3.1+
295                        $after_langname = $data['after'];
296                    }
297
298                    $sql = 'SELECT right_id
299                    FROM ' . $this->modules_table . "
300                    WHERE module_class = '" . $this->db->sql_escape($class) . "'
301                        AND parent_id = " . (int) $parent . "
302                        AND module_langname = '" . $this->db->sql_escape($after_langname) . "'"
303                        . (($after_mode) ? " AND module_mode = '" . $this->db->sql_escape($after_mode) . "'" : '');
304                    $result = $this->db->sql_query($sql);
305                    $to_right = (int) $this->db->sql_fetchfield('right_id');
306                    $this->db->sql_freeresult($result);
307
308                    $sql = 'UPDATE ' . $this->modules_table . "
309                    SET left_id = left_id + 2, right_id = right_id + 2
310                    WHERE module_class = '" . $this->db->sql_escape($class) . "'
311                        AND left_id >= $to_right
312                        AND left_id < {$module_data['left_id']}";
313                    $this->db->sql_query($sql);
314
315                    $sql = 'UPDATE ' . $this->modules_table . '
316                    SET left_id = ' . ($to_right + 1) . ', right_id = ' . ($to_right + 2) . "
317                    WHERE module_class = '" . $this->db->sql_escape($class) . "'
318                        AND module_id = {$module_data['module_id']}";
319                    $this->db->sql_query($sql);
320                }
321            }
322            catch (module_exception $e)
323            {
324                // Error
325                throw new \phpbb\db\migration\exception('MODULE_ERROR', $e->getMessage());
326            }
327        }
328
329        // Clear the Modules Cache
330        $this->module_manager->remove_cache_file($class);
331    }
332
333    /**
334    * Module Remove
335    *
336    * Remove a module
337    *
338    * @param string $class The module class(acp|mcp|ucp)
339    * @param int|string|bool $parent The parent module_id|module_langname(0 for no parent).
340    *     Use false to ignore the parent check and check class wide.
341    * @param int|string $module The module id|module_langname
342    *     specify that here
343    * @return void
344    * @throws \phpbb\db\migration\exception
345    */
346    public function remove($class, $parent = 0, $module = '')
347    {
348        // Imitation of module_add's "automatic" and "manual" method so the uninstaller works from the same set of instructions for umil_auto
349        if (is_array($module))
350        {
351            if (isset($module['module_langname']))
352            {
353                // Manual Method
354                $this->remove($class, $parent, $module['module_langname']);
355                return;
356            }
357
358            // Failed.
359            if (!isset($module['module_basename']))
360            {
361                throw new \phpbb\db\migration\exception('MODULE_NOT_EXIST');
362            }
363
364            // Automatic method
365            $basename = $module['module_basename'];
366            $module_info = $this->get_module_info($class, $basename);
367
368            foreach ($module_info['modes'] as $mode => $info)
369            {
370                if (!isset($module['modes']) || in_array($mode, $module['modes']))
371                {
372                    $this->remove($class, $parent, $info['title']);
373                }
374            }
375        }
376        else
377        {
378            if (!$this->exists($class, $parent, $module, true))
379            {
380                return;
381            }
382
383            $parent_sql = '';
384            if ($parent !== false)
385            {
386                $parents = (array) $this->get_parent_module_id($parent, $module);
387                $parent_sql = 'AND ' . $this->db->sql_in_set('parent_id', $parents);
388            }
389
390            $module_ids = array();
391            if (!is_numeric($module))
392            {
393                $sql = 'SELECT module_id
394                    FROM ' . $this->modules_table . "
395                    WHERE module_langname = '" . $this->db->sql_escape($module) . "'
396                        AND module_class = '" . $this->db->sql_escape($class) . "'
397                        $parent_sql";
398                $result = $this->db->sql_query($sql);
399                while ($module_id = $this->db->sql_fetchfield('module_id'))
400                {
401                    $module_ids[] = (int) $module_id;
402                }
403                $this->db->sql_freeresult($result);
404            }
405            else
406            {
407                $module_ids[] = (int) $module;
408            }
409
410            foreach ($module_ids as $module_id)
411            {
412                $this->module_manager->delete_module($module_id, $class);
413            }
414
415            $this->module_manager->remove_cache_file($class);
416        }
417    }
418
419    /**
420    * {@inheritdoc}
421    */
422    public function reverse()
423    {
424        $arguments = func_get_args();
425        $original_call = array_shift($arguments);
426
427        $call = false;
428        switch ($original_call)
429        {
430            case 'add':
431                $call = 'remove';
432            break;
433
434            case 'remove':
435                $call = 'add';
436            break;
437
438            case 'reverse':
439                // Reversing a reverse is just the call itself
440                $call = array_shift($arguments);
441            break;
442        }
443
444        if ($call)
445        {
446            return call_user_func_array(array(&$this, $call), $arguments);
447        }
448
449        return null;
450    }
451
452    /**
453    * Wrapper for \acp_modules::get_module_infos()
454    *
455    * @param string $class Module Class
456    * @param string $basename Module Basename
457    * @return array Module Information
458    * @throws \phpbb\db\migration\exception
459    */
460    protected function get_module_info($class, $basename)
461    {
462        $module = $this->module_manager->get_module_infos($class, $basename, true);
463
464        if (empty($module))
465        {
466            throw new \phpbb\db\migration\exception('MODULE_INFO_FILE_NOT_EXIST', $class, $basename);
467        }
468
469        return array_pop($module);
470    }
471
472    /**
473    * Get the list of installed module categories
474    *    key - module_id
475    *    value - module_langname
476    *
477    * @return void
478    */
479    protected function get_categories_list()
480    {
481        // Select the top level categories
482        // and 2nd level [sub]categories
483        $sql = 'SELECT m2.module_id, m2.module_langname
484            FROM ' . $this->modules_table . ' m1, ' . $this->modules_table . " m2
485            WHERE m1.parent_id = 0
486                AND (m1.module_id = m2.module_id OR m2.parent_id = m1.module_id)
487            ORDER BY m1.module_id, m2.module_id ASC";
488
489        $result = $this->db->sql_query($sql);
490        while ($row = $this->db->sql_fetchrow($result))
491        {
492            $this->module_categories[(int) $row['module_id']] = $row['module_langname'];
493        }
494        $this->db->sql_freeresult($result);
495    }
496
497    /**
498    * Get parent module id
499    *
500    * @param string|int $parent_id The parent module_id|module_langname
501    * @param int|string|array $data The module_id, module_langname for existence checking or module data array for adding
502    * @param bool $throw_exception The flag indicating if exception should be thrown on error
503    * @return mixed The int parent module_id, an array of int parent module_id values or false
504    * @throws \phpbb\db\migration\exception
505    */
506    public function get_parent_module_id($parent_id, $data = '', $throw_exception = true)
507    {
508        // Allow '' to be sent as 0
509        $parent_id = $parent_id ?: 0;
510
511        if (!is_numeric($parent_id))
512        {
513            // Refresh the $module_categories array
514            $this->get_categories_list();
515
516            // Search for the parent module_langname
517            $ids = array_keys($this->module_categories, $parent_id);
518
519            switch (count($ids))
520            {
521                // No parent with the given module_langname exist
522                case 0:
523                    if ($throw_exception)
524                    {
525                        throw new \phpbb\db\migration\exception('MODULE_NOT_EXIST', $parent_id);
526                    }
527
528                    return false;
529                break;
530
531                // Return the module id
532                case 1:
533                    return (int) $ids[0];
534                break;
535
536                default:
537                    // This represents the old behaviour of phpBB 3.0
538                    return $ids;
539                break;
540            }
541        }
542
543        return $parent_id;
544    }
545}