Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
52.63% |
120 / 228 |
|
33.33% |
3 / 9 |
CRAP | |
0.00% |
0 / 1 |
module_manager | |
52.63% |
120 / 228 |
|
33.33% |
3 / 9 |
229.48 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
get_module_row | |
90.91% |
10 / 11 |
|
0.00% |
0 / 1 |
2.00 | |||
get_module_infos | |
100.00% |
28 / 28 |
|
100.00% |
1 / 1 |
11 | |||
get_module_branch | |
79.17% |
19 / 24 |
|
0.00% |
0 / 1 |
7.44 | |||
remove_cache_file | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
update_module_data | |
68.75% |
33 / 48 |
|
0.00% |
0 / 1 |
9.95 | |||
move_module | |
0.00% |
0 / 50 |
|
0.00% |
0 / 1 |
30 | |||
delete_module | |
95.45% |
21 / 22 |
|
0.00% |
0 / 1 |
2 | |||
move_module_by | |
0.00% |
0 / 36 |
|
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 | |
14 | namespace phpbb\module; |
15 | |
16 | use phpbb\module\exception\module_exception; |
17 | use phpbb\module\exception\module_not_found_exception; |
18 | |
19 | class 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 | } |