Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
74.87% |
143 / 191 |
|
44.44% |
4 / 9 |
CRAP | |
0.00% |
0 / 1 |
module | |
74.87% |
143 / 191 |
|
44.44% |
4 / 9 |
154.28 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
get_name | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
exists | |
100.00% |
24 / 24 |
|
100.00% |
1 / 1 |
11 | |||
add | |
70.33% |
64 / 91 |
|
0.00% |
0 / 1 |
56.10 | |||
remove | |
62.50% |
20 / 32 |
|
0.00% |
0 / 1 |
19.59 | |||
reverse | |
53.33% |
8 / 15 |
|
0.00% |
0 / 1 |
7.54 | |||
get_module_info | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
get_categories_list | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
get_parent_module_id | |
92.31% |
12 / 13 |
|
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 | |
14 | namespace phpbb\db\migration\tool; |
15 | |
16 | use phpbb\module\exception\module_exception; |
17 | |
18 | /** |
19 | * Migration module management tool |
20 | */ |
21 | class 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 | } |