Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
52.63% covered (warning)
52.63%
120 / 228
33.33% covered (danger)
33.33%
3 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
module_manager
52.63% covered (warning)
52.63%
120 / 228
33.33% covered (danger)
33.33%
3 / 9
229.48
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 get_module_row
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
2.00
 get_module_infos
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
1 / 1
11
 get_module_branch
79.17% covered (warning)
79.17%
19 / 24
0.00% covered (danger)
0.00%
0 / 1
7.44
 remove_cache_file
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 update_module_data
68.75% covered (warning)
68.75%
33 / 48
0.00% covered (danger)
0.00%
0 / 1
9.95
 move_module
0.00% covered (danger)
0.00%
0 / 50
0.00% covered (danger)
0.00%
0 / 1
30
 delete_module
95.45% covered (success)
95.45%
21 / 22
0.00% covered (danger)
0.00%
0 / 1
2
 move_module_by
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
30
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\module;
15
16use phpbb\module\exception\module_exception;
17use phpbb\module\exception\module_not_found_exception;
18
19class module_manager
20{
21    /**
22     * @var \phpbb\cache\driver\driver_interface
23     */
24    protected $cache;
25
26    /**
27     * @var \phpbb\db\driver\driver_interface
28     */
29    protected $db;
30
31    /**
32     * @var \phpbb\extension\manager
33     */
34    protected $extension_manager;
35
36    /**
37     * @var string
38     */
39    protected $modules_table;
40
41    /**
42     * @var string
43     */
44    protected $phpbb_root_path;
45
46    /**
47     * @var string
48     */
49    protected $php_ext;
50
51    /**
52     * Constructor
53     *
54     * @param \phpbb\cache\driver\driver_interface    $cache                Cache driver
55     * @param \phpbb\db\driver\driver_interface        $db                    Database driver
56     * @param \phpbb\extension\manager                $ext_manager        Extension manager
57     * @param string                                $modules_table        Module database table's name
58     * @param string                                $phpbb_root_path    Path to phpBB's root
59     * @param string                                $php_ext            Extension of PHP files
60     */
61    public function __construct(\phpbb\cache\driver\driver_interface $cache, \phpbb\db\driver\driver_interface $db, \phpbb\extension\manager $ext_manager, $modules_table, $phpbb_root_path, $php_ext)
62    {
63        $this->cache                = $cache;
64        $this->db                    = $db;
65        $this->extension_manager    = $ext_manager;
66        $this->modules_table        = $modules_table;
67        $this->phpbb_root_path        = $phpbb_root_path;
68        $this->php_ext                = $php_ext;
69    }
70
71    /**
72     * Get row for specified module
73     *
74     * @param int        $module_id        ID of the module
75     * @param string    $module_class    Class of the module (acp, ucp, mcp etc...)
76     *
77     * @return array    Array of data fetched from the database
78     *
79     * @throws module_not_found_exception    When there is no module with $module_id
80     */
81    public function get_module_row($module_id, $module_class)
82    {
83        $module_id = (int) $module_id;
84
85        $sql = 'SELECT *
86            FROM ' . $this->modules_table . "
87            WHERE module_class = '" . $this->db->sql_escape($module_class) . "'
88                AND module_id = $module_id";
89        $result = $this->db->sql_query($sql);
90        $row = $this->db->sql_fetchrow($result);
91        $this->db->sql_freeresult($result);
92
93        if (!$row)
94        {
95            throw new module_not_found_exception('NO_MODULE');
96        }
97
98        return $row;
99    }
100
101    /**
102     * Get available module information from module files
103     *
104     * @param string    $module_class        Class of the module (acp, ucp, mcp etc...)
105     * @param string    $module                ID of module
106     * @param bool        $use_all_available    Use all available instead of just all
107     *                                        enabled extensions
108     *
109     * @return array    Array with module information gathered from module info files.
110     */
111    public function get_module_infos($module_class, $module = '', $use_all_available = false)
112    {
113        $directory = $this->phpbb_root_path . 'includes/' . $module_class . '/info/';
114        $fileinfo = array();
115
116        $finder = $this->extension_manager->get_finder($use_all_available);
117
118        $modules = $finder
119            ->extension_suffix('_module')
120            ->extension_directory("/$module_class")
121            ->core_path("includes/$module_class/info/")
122            ->core_prefix($module_class . '_')
123            ->get_classes(true);
124
125        foreach ($modules as $cur_module)
126        {
127            // Skip entries we do not need if we know the module we are
128            // looking for
129            if ($module && strpos(str_replace('\\', '_', $cur_module), $module) === false && $module !== $cur_module)
130            {
131                continue;
132            }
133
134            $info_class = preg_replace('/_module$/', '_info', $cur_module);
135
136            // If the class does not exist it might be following the old
137            // format. phpbb_acp_info_acp_foo needs to be turned into
138            // acp_foo_info and the respective file has to be included
139            // manually because it does not support auto loading
140            $old_info_class_file = str_replace("phpbb_{$module_class}_info_", '', $cur_module);
141            $old_info_class = $old_info_class_file . '_info';
142
143            if (class_exists($old_info_class))
144            {
145                $info_class = $old_info_class;
146            }
147            else if (!class_exists($info_class))
148            {
149                $info_class = $old_info_class;
150
151                // need to check class exists again because previous checks triggered autoloading
152                if (!class_exists($info_class) && file_exists($directory . $old_info_class_file . '.' . $this->php_ext))
153                {
154                    include($directory . $old_info_class_file . '.' . $this->php_ext);
155                }
156            }
157
158            if (class_exists($info_class))
159            {
160                $info = new $info_class();
161                $module_info = $info->module();
162
163                $main_class = (isset($module_info['filename'])) ? $module_info['filename'] : $cur_module;
164
165                $fileinfo[$main_class] = $module_info;
166            }
167        }
168
169        ksort($fileinfo);
170
171        return $fileinfo;
172    }
173
174    /**
175     * Get module branch
176     *
177     * @param int        $module_id        ID of the module
178     * @param string    $module_class    Class of the module (acp, ucp, mcp etc...)
179     * @param string    $type            Type of branch (Expected values: all, parents or children)
180     * @param bool        $include_module    Whether or not to include the specified module with $module_id
181     *
182     * @return array    Returns an array containing the modules in the specified branch type.
183     */
184    public function get_module_branch($module_id, $module_class, $type = 'all', $include_module = true)
185    {
186        $module_id = (int) $module_id;
187
188        switch ($type)
189        {
190            case 'parents':
191                $condition = 'm1.left_id BETWEEN m2.left_id AND m2.right_id';
192            break;
193
194            case 'children':
195                $condition = 'm2.left_id BETWEEN m1.left_id AND m1.right_id';
196            break;
197
198            default:
199                $condition = 'm2.left_id BETWEEN m1.left_id AND m1.right_id OR m1.left_id BETWEEN m2.left_id AND m2.right_id';
200            break;
201        }
202
203        $rows = array();
204
205        $sql = 'SELECT m2.*
206            FROM ' . $this->modules_table . ' m1
207            LEFT JOIN ' . $this->modules_table . " m2 ON ($condition)
208            WHERE m1.module_class = '" . $this->db->sql_escape($module_class) . "'
209                AND m2.module_class = '" . $this->db->sql_escape($module_class) . "'
210                AND m1.module_id = $module_id
211            ORDER BY m2.left_id";
212        $result = $this->db->sql_query($sql);
213
214        while ($row = $this->db->sql_fetchrow($result))
215        {
216            if (!$include_module && $row['module_id'] == $module_id)
217            {
218                continue;
219            }
220
221            $rows[] = $row;
222        }
223        $this->db->sql_freeresult($result);
224
225        return $rows;
226    }
227
228    /**
229     * Remove modules cache file
230     *
231     * @param string    $module_class    Class of the module (acp, ucp, mcp etc...)
232     */
233    public function remove_cache_file($module_class)
234    {
235        // Sanitise for future path use, it's escaped as appropriate for queries
236        $cache_class = str_replace(array('.', '/', '\\'), '', basename($module_class));
237        $this->cache->destroy('_modules_' . $cache_class);
238        $this->cache->destroy('sql', $this->modules_table);
239    }
240
241    /**
242     * Update/Add module
243     *
244     * @param array    &$module_data    The module data
245     *
246     * @throws module_not_found_exception    When parent module or the category is not exist
247     */
248    public function update_module_data(&$module_data)
249    {
250        if (!isset($module_data['module_id']))
251        {
252            // no module_id means we're creating a new category/module
253            if ($module_data['parent_id'])
254            {
255                $sql = 'SELECT left_id, right_id
256                    FROM ' . $this->modules_table . "
257                    WHERE module_class = '" . $this->db->sql_escape($module_data['module_class']) . "'
258                        AND module_id = " . (int) $module_data['parent_id'];
259                $result = $this->db->sql_query($sql);
260                $row = $this->db->sql_fetchrow($result);
261                $this->db->sql_freeresult($result);
262
263                if (!$row)
264                {
265                    throw new module_not_found_exception('PARENT_NOT_EXIST');
266                }
267
268                // Workaround
269                $row['left_id'] = (int) $row['left_id'];
270                $row['right_id'] = (int) $row['right_id'];
271
272                $sql = 'UPDATE ' . $this->modules_table . "
273                    SET left_id = left_id + 2, right_id = right_id + 2
274                    WHERE module_class = '" . $this->db->sql_escape($module_data['module_class']) . "'
275                        AND left_id > {$row['right_id']}";
276                $this->db->sql_query($sql);
277
278                $sql = 'UPDATE ' . $this->modules_table . "
279                    SET right_id = right_id + 2
280                    WHERE module_class = '" . $this->db->sql_escape($module_data['module_class']) . "'
281                        AND {$row['left_id']} BETWEEN left_id AND right_id";
282                $this->db->sql_query($sql);
283
284                $module_data['left_id'] = (int) $row['right_id'];
285                $module_data['right_id'] = (int) $row['right_id'] + 1;
286            }
287            else
288            {
289                $sql = 'SELECT MAX(right_id) AS right_id
290                    FROM ' . $this->modules_table . "
291                    WHERE module_class = '" . $this->db->sql_escape($module_data['module_class']) . "'";
292                $result = $this->db->sql_query($sql);
293                $row = $this->db->sql_fetchrow($result);
294                $this->db->sql_freeresult($result);
295
296                $module_data['left_id'] = (int) $row['right_id'] + 1;
297                $module_data['right_id'] = (int) $row['right_id'] + 2;
298            }
299
300            $sql = 'INSERT INTO ' . $this->modules_table . ' ' . $this->db->sql_build_array('INSERT', $module_data);
301            $this->db->sql_query($sql);
302
303            $module_data['module_id'] = $this->db->sql_nextid();
304        }
305        else
306        {
307            $row = $this->get_module_row($module_data['module_id'], $module_data['module_class']);
308
309            if ($module_data['module_basename'] && !$row['module_basename'])
310            {
311                // we're turning a category into a module
312                $branch = $this->get_module_branch($module_data['module_id'], $module_data['module_class'], 'children', false);
313
314                if (count($branch))
315                {
316                    throw new module_not_found_exception('NO_CATEGORY_TO_MODULE');
317                }
318            }
319
320            if ($row['parent_id'] != $module_data['parent_id'])
321            {
322                $this->move_module($module_data['module_id'], $module_data['parent_id'], $module_data['module_class']);
323            }
324
325            $update_ary = $module_data;
326            unset($update_ary['module_id']);
327
328            $sql = 'UPDATE ' . $this->modules_table . '
329                SET ' . $this->db->sql_build_array('UPDATE', $update_ary) . "
330                WHERE module_class = '" . $this->db->sql_escape($module_data['module_class']) . "'
331                    AND module_id = " . (int) $module_data['module_id'];
332            $this->db->sql_query($sql);
333        }
334    }
335
336    /**
337     * Move module around the tree
338     *
339     * @param int        $from_module_id    ID of the current parent module
340     * @param int        $to_parent_id    ID of the target parent module
341     * @param string    $module_class    Class of the module (acp, ucp, mcp etc...)
342     *
343     * @throws module_not_found_exception    If the module specified to move modules from does not
344     *                                         have any children.
345     */
346    public function move_module($from_module_id, $to_parent_id, $module_class)
347    {
348        $moved_modules = $this->get_module_branch($from_module_id, $module_class, 'children');
349
350        if (empty($moved_modules))
351        {
352            throw new module_not_found_exception();
353        }
354
355        $from_data = $moved_modules[0];
356        $diff = count($moved_modules) * 2;
357
358        $moved_ids = array();
359        for ($i = 0, $size = count($moved_modules); $i < $size; ++$i)
360        {
361            $moved_ids[] = $moved_modules[$i]['module_id'];
362        }
363
364        // Resync parents
365        $sql = 'UPDATE ' . $this->modules_table . "
366            SET right_id = right_id - $diff
367            WHERE module_class = '" . $this->db->sql_escape($module_class) . "'
368                AND left_id < " . (int) $from_data['right_id'] . '
369                AND right_id > ' . (int) $from_data['right_id'];
370        $this->db->sql_query($sql);
371
372        // Resync righthand side of tree
373        $sql = 'UPDATE ' . $this->modules_table . "
374            SET left_id = left_id - $diff, right_id = right_id - $diff
375            WHERE module_class = '" . $this->db->sql_escape($module_class) . "'
376                AND left_id > " . (int) $from_data['right_id'];
377        $this->db->sql_query($sql);
378
379        if ($to_parent_id > 0)
380        {
381            $to_data = $this->get_module_row($to_parent_id, $module_class);
382
383            // Resync new parents
384            $sql = 'UPDATE ' . $this->modules_table . "
385                SET right_id = right_id + $diff
386                WHERE module_class = '" . $this->db->sql_escape($module_class) . "'
387                    AND " . (int) $to_data['right_id'] . ' BETWEEN left_id AND right_id
388                    AND ' . $this->db->sql_in_set('module_id', $moved_ids, true);
389            $this->db->sql_query($sql);
390
391            // Resync the righthand side of the tree
392            $sql = 'UPDATE ' . $this->modules_table . "
393                SET left_id = left_id + $diff, right_id = right_id + $diff
394                WHERE module_class = '" . $this->db->sql_escape($module_class) . "'
395                    AND left_id > " . (int) $to_data['right_id'] . '
396                    AND ' . $this->db->sql_in_set('module_id', $moved_ids, true);
397            $this->db->sql_query($sql);
398
399            // Resync moved branch
400            $to_data['right_id'] += $diff;
401            if ($to_data['right_id'] > $from_data['right_id'])
402            {
403                $diff = '+ ' . ($to_data['right_id'] - $from_data['right_id'] - 1);
404            }
405            else
406            {
407                $diff = '- ' . abs($to_data['right_id'] - $from_data['right_id'] - 1);
408            }
409        }
410        else
411        {
412            $sql = 'SELECT MAX(right_id) AS right_id
413                FROM ' . $this->modules_table . "
414                WHERE module_class = '" . $this->db->sql_escape($module_class) . "'
415                    AND " . $this->db->sql_in_set('module_id', $moved_ids, true);
416            $result = $this->db->sql_query($sql);
417            $row = $this->db->sql_fetchrow($result);
418            $this->db->sql_freeresult($result);
419
420            $diff = '+ ' . (int) ($row['right_id'] - $from_data['left_id'] + 1);
421        }
422
423        $sql = 'UPDATE ' . $this->modules_table . "
424            SET left_id = left_id $diff, right_id = right_id $diff
425            WHERE module_class = '" . $this->db->sql_escape($module_class) . "'
426                AND " . $this->db->sql_in_set('module_id', $moved_ids);
427        $this->db->sql_query($sql);
428    }
429
430    /**
431     * Remove module from tree
432     *
433     * @param int        $module_id        ID of the module to delete
434     * @param string    $module_class    Class of the module (acp, ucp, mcp etc...)
435     *
436     * @throws module_exception    When the specified module cannot be removed
437     */
438    public function delete_module($module_id, $module_class)
439    {
440        $module_id = (int) $module_id;
441
442        $row = $this->get_module_row($module_id, $module_class);
443
444        $branch = $this->get_module_branch($module_id, $module_class, 'children', false);
445
446        if (count($branch))
447        {
448            throw new module_exception('CANNOT_REMOVE_MODULE');
449        }
450
451        // If not move
452        $diff = 2;
453        $sql = 'DELETE FROM ' . $this->modules_table . "
454            WHERE module_class = '" . $this->db->sql_escape($module_class) . "'
455                AND module_id = $module_id";
456        $this->db->sql_query($sql);
457
458        $row['right_id'] = (int) $row['right_id'];
459        $row['left_id'] = (int) $row['left_id'];
460
461        // Resync tree
462        $sql = 'UPDATE ' . $this->modules_table . "
463            SET right_id = right_id - $diff
464            WHERE module_class = '" . $this->db->sql_escape($module_class) . "'
465                AND left_id < {$row['right_id']} AND right_id > {$row['right_id']}";
466        $this->db->sql_query($sql);
467
468        $sql = 'UPDATE ' . $this->modules_table . "
469            SET left_id = left_id - $diff, right_id = right_id - $diff
470            WHERE module_class = '" . $this->db->sql_escape($module_class) . "'
471                AND left_id > {$row['right_id']}";
472        $this->db->sql_query($sql);
473    }
474
475    /**
476     * Move module position by $steps up/down
477     *
478     * @param array        $module_row        Array of module data
479     * @param string    $module_class    Class of the module (acp, ucp, mcp etc...)
480     * @param string    $action            Direction of moving (valid values: move_up or move_down)
481     * @param int        $steps            Number of steps to move module
482     *
483     * @return string    Returns the language name of the module
484     *
485     * @throws module_not_found_exception    When the specified module does not exists
486     */
487    public function move_module_by($module_row, $module_class, $action = 'move_up', $steps = 1)
488    {
489        /**
490         * Fetch all the siblings between the module's current spot
491         * and where we want to move it to. If there are less than $steps
492         * siblings between the current spot and the target then the
493         * module will move as far as possible
494         */
495        $sql = 'SELECT module_id, left_id, right_id, module_langname
496            FROM ' . $this->modules_table . "
497            WHERE module_class = '" . $this->db->sql_escape($module_class) . "'
498                AND parent_id = " . (int) $module_row['parent_id'] . '
499                AND ' . (($action == 'move_up') ? 'right_id < ' . (int) $module_row['right_id'] . ' ORDER BY right_id DESC' : 'left_id > ' . (int) $module_row['left_id'] . ' ORDER BY left_id ASC');
500        $result = $this->db->sql_query_limit($sql, $steps);
501
502        $target = array();
503        while ($row = $this->db->sql_fetchrow($result))
504        {
505            $target = $row;
506        }
507        $this->db->sql_freeresult($result);
508
509        if (!count($target))
510        {
511            // The module is already on top or bottom
512            throw new module_not_found_exception();
513        }
514
515        /**
516         * $left_id and $right_id define the scope of the nodes that are affected by the move.
517         * $diff_up and $diff_down are the values to substract or add to each node's left_id
518         * and right_id in order to move them up or down.
519         * $move_up_left and $move_up_right define the scope of the nodes that are moving
520         * up. Other nodes in the scope of ($left_id, $right_id) are considered to move down.
521         */
522        if ($action == 'move_up')
523        {
524            $left_id = (int) $target['left_id'];
525            $right_id = (int) $module_row['right_id'];
526
527            $diff_up = (int) ($module_row['left_id'] - $target['left_id']);
528            $diff_down = (int) ($module_row['right_id'] + 1 - $module_row['left_id']);
529
530            $move_up_left = (int) $module_row['left_id'];
531            $move_up_right = (int) $module_row['right_id'];
532        }
533        else
534        {
535            $left_id = (int) $module_row['left_id'];
536            $right_id = (int) $target['right_id'];
537
538            $diff_up = (int) ($module_row['right_id'] + 1 - $module_row['left_id']);
539            $diff_down = (int) ($target['right_id'] - $module_row['right_id']);
540
541            $move_up_left = (int) ($module_row['right_id'] + 1);
542            $move_up_right = (int) $target['right_id'];
543        }
544
545        // Now do the dirty job
546        $sql = 'UPDATE ' . $this->modules_table . "
547            SET left_id = left_id + CASE
548                WHEN left_id BETWEEN {$move_up_left} AND {$move_up_right} THEN -{$diff_up}
549                ELSE {$diff_down}
550            END,
551            right_id = right_id + CASE
552                WHEN right_id BETWEEN {$move_up_left} AND {$move_up_right} THEN -{$diff_up}
553                ELSE {$diff_down}
554            END
555            WHERE module_class = '" . $this->db->sql_escape($module_class) . "'
556                AND left_id BETWEEN {$left_id} AND {$right_id}
557                AND right_id BETWEEN {$left_id} AND {$right_id}";
558        $this->db->sql_query($sql);
559
560        $this->remove_cache_file($module_class);
561
562        return $target['module_langname'];
563    }
564}