Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
87.42% |
139 / 159 |
|
69.57% |
16 / 23 |
CRAP | |
0.00% |
0 / 1 |
manager | |
87.42% |
139 / 159 |
|
69.57% |
16 / 23 |
79.75 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
3 | |||
load_extensions | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
5 | |||
get_extension_path | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
get_extension | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
2.03 | |||
create_extension_metadata_manager | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
update_state | |
100.00% |
22 / 22 |
|
100.00% |
1 / 1 |
5 | |||
enable_step | |
94.12% |
16 / 17 |
|
0.00% |
0 / 1 |
6.01 | |||
enable | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
disable_step | |
91.67% |
11 / 12 |
|
0.00% |
0 / 1 |
2.00 | |||
disable | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
purge_step | |
84.62% |
11 / 13 |
|
0.00% |
0 / 1 |
4.06 | |||
purge | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
all_available | |
94.12% |
16 / 17 |
|
0.00% |
0 / 1 |
6.01 | |||
all_configured | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
all_enabled | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
all_disabled | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
20 | |||
is_available | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
is_enabled | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
is_disabled | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
is_configured | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
version_check | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
is_purged | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
get_finder | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 |
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\extension; |
15 | |
16 | use phpbb\exception\runtime_exception; |
17 | use phpbb\file_downloader; |
18 | use phpbb\finder\factory as finder_factory; |
19 | use Symfony\Component\DependencyInjection\ContainerInterface; |
20 | |
21 | /** |
22 | * The extension manager provides means to activate/deactivate extensions. |
23 | */ |
24 | class manager |
25 | { |
26 | /** @var ContainerInterface */ |
27 | protected $container; |
28 | |
29 | protected $db; |
30 | protected $config; |
31 | protected $finder_factory; |
32 | protected $cache; |
33 | protected $extensions; |
34 | protected $extension_table; |
35 | protected $phpbb_root_path; |
36 | protected $cache_name; |
37 | |
38 | /** |
39 | * Creates a manager and loads information from database |
40 | * |
41 | * @param ContainerInterface $container A container |
42 | * @param \phpbb\db\driver\driver_interface $db A database connection |
43 | * @param \phpbb\config\config $config Config object |
44 | * @param finder_factory $finder_factory Finder factory |
45 | * @param string $extension_table The name of the table holding extensions |
46 | * @param string $phpbb_root_path Path to the phpbb includes directory. |
47 | * @param \phpbb\cache\service|null $cache A cache instance or null |
48 | * @param string $cache_name The name of the cache variable, defaults to _ext |
49 | */ |
50 | public function __construct(ContainerInterface $container, \phpbb\db\driver\driver_interface $db, \phpbb\config\config $config, finder_factory $finder_factory, $extension_table, $phpbb_root_path, \phpbb\cache\service $cache = null, $cache_name = '_ext') |
51 | { |
52 | $this->cache = $cache; |
53 | $this->cache_name = $cache_name; |
54 | $this->config = $config; |
55 | $this->finder_factory = $finder_factory; |
56 | $this->container = $container; |
57 | $this->db = $db; |
58 | $this->extension_table = $extension_table; |
59 | $this->phpbb_root_path = $phpbb_root_path; |
60 | |
61 | $this->extensions = ($this->cache) ? $this->cache->get($this->cache_name) : false; |
62 | |
63 | if ($this->extensions === false) |
64 | { |
65 | $this->load_extensions(); |
66 | } |
67 | } |
68 | |
69 | /** |
70 | * Loads all extension information from the database |
71 | * |
72 | * @return void |
73 | */ |
74 | public function load_extensions() |
75 | { |
76 | $this->extensions = array(); |
77 | |
78 | // Do not try to load any extensions if the extension table |
79 | // does not exist or when installing or updating. |
80 | // Note: database updater invokes this code, and in 3.0 |
81 | // there is no extension table therefore the rest of this function |
82 | // fails |
83 | if (defined('IN_INSTALL') || version_compare($this->config['version'], '3.1.0-dev', '<')) |
84 | { |
85 | return; |
86 | } |
87 | |
88 | $sql = 'SELECT * |
89 | FROM ' . $this->extension_table; |
90 | |
91 | $result = $this->db->sql_query($sql); |
92 | $extensions = $this->db->sql_fetchrowset($result); |
93 | $this->db->sql_freeresult($result); |
94 | |
95 | foreach ($extensions as $extension) |
96 | { |
97 | $extension['ext_path'] = $this->get_extension_path($extension['ext_name']); |
98 | $this->extensions[$extension['ext_name']] = $extension; |
99 | } |
100 | |
101 | ksort($this->extensions); |
102 | |
103 | if ($this->cache) |
104 | { |
105 | $this->cache->put($this->cache_name, $this->extensions); |
106 | } |
107 | } |
108 | |
109 | /** |
110 | * Generates the path to an extension |
111 | * |
112 | * @param string $name The name of the extension |
113 | * @param bool $phpbb_relative Whether the path should be relative to phpbb root |
114 | * @return string Path to an extension |
115 | */ |
116 | public function get_extension_path($name, $phpbb_relative = false) |
117 | { |
118 | $name = str_replace('.', '', $name); |
119 | |
120 | return (($phpbb_relative) ? $this->phpbb_root_path : '') . 'ext/' . $name . '/'; |
121 | } |
122 | |
123 | /** |
124 | * Instantiates the extension meta class for the extension with the given name |
125 | * |
126 | * @param string $name The extension name |
127 | * @return \phpbb\extension\extension_interface Instance of the extension meta class or |
128 | * \phpbb\extension\base if the class does not exist |
129 | */ |
130 | public function get_extension($name) |
131 | { |
132 | $extension_class_name = str_replace('/', '\\', $name) . '\\ext'; |
133 | |
134 | $migrator = $this->container->get('migrator'); |
135 | |
136 | if (class_exists($extension_class_name)) |
137 | { |
138 | return new $extension_class_name($this->container, $this->get_finder(), $migrator, $name, $this->get_extension_path($name, true)); |
139 | } |
140 | else |
141 | { |
142 | return new \phpbb\extension\base($this->container, $this->get_finder(), $migrator, $name, $this->get_extension_path($name, true)); |
143 | } |
144 | } |
145 | |
146 | /** |
147 | * Instantiates the metadata manager for the extension with the given name |
148 | * |
149 | * @param string $name The extension name |
150 | * @return \phpbb\extension\metadata_manager Instance of the metadata manager |
151 | */ |
152 | public function create_extension_metadata_manager($name) |
153 | { |
154 | if (!isset($this->extensions[$name]['metadata'])) |
155 | { |
156 | $metadata = new \phpbb\extension\metadata_manager($name, $this->get_extension_path($name, true)); |
157 | $this->extensions[$name]['metadata'] = $metadata; |
158 | } |
159 | return $this->extensions[$name]['metadata']; |
160 | } |
161 | |
162 | /** |
163 | * Update the database entry for an extension |
164 | * |
165 | * @param string $name Extension name to update |
166 | * @param array $data Data to update in the database |
167 | * @param string $action Action to perform, by default 'update', may be also 'insert' or 'delete' |
168 | */ |
169 | protected function update_state($name, $data, $action = 'update') |
170 | { |
171 | switch ($action) |
172 | { |
173 | case 'insert': |
174 | $this->extensions[$name] = $data; |
175 | $this->extensions[$name]['ext_path'] = $this->get_extension_path($name); |
176 | ksort($this->extensions); |
177 | $sql = 'INSERT INTO ' . $this->extension_table . ' ' . $this->db->sql_build_array('INSERT', $data); |
178 | $this->db->sql_query($sql); |
179 | break; |
180 | |
181 | case 'update': |
182 | $this->extensions[$name] = array_merge($this->extensions[$name], $data); |
183 | $sql = 'UPDATE ' . $this->extension_table . ' |
184 | SET ' . $this->db->sql_build_array('UPDATE', $data) . " |
185 | WHERE ext_name = '" . $this->db->sql_escape($name) . "'"; |
186 | $this->db->sql_query($sql); |
187 | break; |
188 | |
189 | case 'delete': |
190 | unset($this->extensions[$name]); |
191 | $sql = 'DELETE FROM ' . $this->extension_table . " |
192 | WHERE ext_name = '" . $this->db->sql_escape($name) . "'"; |
193 | $this->db->sql_query($sql); |
194 | break; |
195 | } |
196 | |
197 | if ($this->cache) |
198 | { |
199 | $this->cache->deferred_purge(); |
200 | } |
201 | } |
202 | |
203 | /** |
204 | * Runs a step of the extension enabling process. |
205 | * |
206 | * Allows the exentension to enable in a long running script that works |
207 | * in multiple steps across requests. State is kept for the extension |
208 | * in the extensions table. |
209 | * |
210 | * @param string $name The extension's name |
211 | * @return bool False if enabling is finished, true otherwise |
212 | */ |
213 | public function enable_step($name) |
214 | { |
215 | // ignore extensions that are already enabled |
216 | if ($this->is_enabled($name)) |
217 | { |
218 | return false; |
219 | } |
220 | |
221 | $old_state = (isset($this->extensions[$name]['ext_state'])) ? unserialize($this->extensions[$name]['ext_state']) : false; |
222 | |
223 | $extension = $this->get_extension($name); |
224 | |
225 | if (!$extension->is_enableable()) |
226 | { |
227 | return false; |
228 | } |
229 | |
230 | $state = $extension->enable_step($old_state); |
231 | |
232 | $active = ($state === false); |
233 | |
234 | $extension_data = array( |
235 | 'ext_name' => $name, |
236 | 'ext_active' => $active, |
237 | 'ext_state' => serialize($state), |
238 | ); |
239 | |
240 | $this->update_state($name, $extension_data, $this->is_configured($name) ? 'update' : 'insert'); |
241 | |
242 | if ($active) |
243 | { |
244 | $this->config->increment('assets_version', 1); |
245 | } |
246 | |
247 | return !$active; |
248 | } |
249 | |
250 | /** |
251 | * Enables an extension |
252 | * |
253 | * This method completely enables an extension. But it could be long running |
254 | * so never call this in a script that has a max_execution time. |
255 | * |
256 | * @param string $name The extension's name |
257 | * @return void |
258 | */ |
259 | public function enable($name) |
260 | { |
261 | // @codingStandardsIgnoreStart |
262 | while ($this->enable_step($name)); |
263 | // @codingStandardsIgnoreEnd |
264 | } |
265 | |
266 | /** |
267 | * Disables an extension |
268 | * |
269 | * Calls the disable method on the extension's meta class to allow it to |
270 | * process the event. |
271 | * |
272 | * @param string $name The extension's name |
273 | * @return bool False if disabling is finished, true otherwise |
274 | */ |
275 | public function disable_step($name) |
276 | { |
277 | // ignore extensions that are not enabled |
278 | if (!$this->is_enabled($name)) |
279 | { |
280 | return false; |
281 | } |
282 | |
283 | $old_state = unserialize($this->extensions[$name]['ext_state']); |
284 | |
285 | $extension = $this->get_extension($name); |
286 | $state = $extension->disable_step($old_state); |
287 | $active = ($state !== false); |
288 | |
289 | $extension_data = array( |
290 | 'ext_active' => $active, |
291 | 'ext_state' => serialize($state), |
292 | ); |
293 | $this->update_state($name, $extension_data); |
294 | |
295 | return $active; |
296 | } |
297 | |
298 | /** |
299 | * Disables an extension |
300 | * |
301 | * Disables an extension completely at once. This process could run for a |
302 | * while so never call this in a script that has a max_execution time. |
303 | * |
304 | * @param string $name The extension's name |
305 | * @return void |
306 | */ |
307 | public function disable($name) |
308 | { |
309 | // @codingStandardsIgnoreStart |
310 | while ($this->disable_step($name)); |
311 | // @codingStandardsIgnoreEnd |
312 | } |
313 | |
314 | /** |
315 | * Purge an extension |
316 | * |
317 | * Disables the extension first if active, and then calls purge on the |
318 | * extension's meta class to delete the extension's database content. |
319 | * |
320 | * @param string $name The extension's name |
321 | * @return bool False if purging is finished, true otherwise |
322 | */ |
323 | public function purge_step($name) |
324 | { |
325 | // ignore extensions that are not configured |
326 | if (!$this->is_configured($name)) |
327 | { |
328 | return false; |
329 | } |
330 | |
331 | // disable first if necessary |
332 | if ($this->extensions[$name]['ext_active']) |
333 | { |
334 | $this->disable($name); |
335 | } |
336 | |
337 | $old_state = unserialize($this->extensions[$name]['ext_state']); |
338 | |
339 | $extension = $this->get_extension($name); |
340 | $state = $extension->purge_step($old_state); |
341 | $purged = ($state === false); |
342 | |
343 | $extension_data = array( |
344 | 'ext_state' => serialize($state), |
345 | ); |
346 | |
347 | $this->update_state($name, $extension_data, $purged ? 'delete' : 'update'); |
348 | |
349 | // continue until the state is false |
350 | return !$purged; |
351 | } |
352 | |
353 | /** |
354 | * Purge an extension |
355 | * |
356 | * Purges an extension completely at once. This process could run for a while |
357 | * so never call this in a script that has a max_execution time. |
358 | * |
359 | * @param string $name The extension's name |
360 | * @return void |
361 | */ |
362 | public function purge($name) |
363 | { |
364 | // @codingStandardsIgnoreStart |
365 | while ($this->purge_step($name)); |
366 | // @codingStandardsIgnoreEnd |
367 | } |
368 | |
369 | /** |
370 | * Retrieves a list of all available extensions on the filesystem |
371 | * |
372 | * @return array An array with extension names as keys and paths to the |
373 | * extension as values |
374 | */ |
375 | public function all_available() |
376 | { |
377 | $available = array(); |
378 | if (!is_dir($this->phpbb_root_path . 'ext/')) |
379 | { |
380 | return $available; |
381 | } |
382 | |
383 | $iterator = new \phpbb\finder\recursive_path_iterator( |
384 | $this->phpbb_root_path . 'ext/', |
385 | \RecursiveIteratorIterator::SELF_FIRST, |
386 | \FilesystemIterator::NEW_CURRENT_AND_KEY | \FilesystemIterator::FOLLOW_SYMLINKS |
387 | ); |
388 | $iterator->setMaxDepth(2); |
389 | |
390 | foreach ($iterator as $file_info) |
391 | { |
392 | if ($file_info->isFile() && $file_info->getFilename() == 'composer.json') |
393 | { |
394 | $ext_name = $iterator->getInnerIterator()->getSubPath(); |
395 | $ext_name = str_replace(DIRECTORY_SEPARATOR, '/', $ext_name); |
396 | if ($this->is_available($ext_name)) |
397 | { |
398 | $available[$ext_name] = $this->get_extension_path($ext_name, true); |
399 | } |
400 | } |
401 | } |
402 | ksort($available); |
403 | return $available; |
404 | } |
405 | |
406 | /** |
407 | * Retrieves all configured extensions. |
408 | * |
409 | * All enabled and disabled extensions are considered configured. A purged |
410 | * extension that is no longer in the database is not configured. |
411 | * |
412 | * @param bool $phpbb_relative Whether the path should be relative to phpbb root |
413 | * |
414 | * @return array An array with extension names as keys and and the |
415 | * database stored extension information as values |
416 | */ |
417 | public function all_configured($phpbb_relative = true) |
418 | { |
419 | $configured = array(); |
420 | foreach ($this->extensions as $name => $data) |
421 | { |
422 | if ($this->is_configured($name)) |
423 | { |
424 | unset($data['metadata']); |
425 | $data['ext_path'] = ($phpbb_relative ? $this->phpbb_root_path : '') . $data['ext_path']; |
426 | $configured[$name] = $data; |
427 | } |
428 | } |
429 | return $configured; |
430 | } |
431 | |
432 | /** |
433 | * Retrieves all enabled extensions. |
434 | * @param bool $phpbb_relative Whether the path should be relative to phpbb root |
435 | * |
436 | * @return array An array with extension names as keys and and the |
437 | * database stored extension information as values |
438 | */ |
439 | public function all_enabled($phpbb_relative = true) |
440 | { |
441 | $enabled = array(); |
442 | foreach ($this->extensions as $name => $data) |
443 | { |
444 | if ($this->is_enabled($name)) |
445 | { |
446 | $enabled[$name] = ($phpbb_relative ? $this->phpbb_root_path : '') . $data['ext_path']; |
447 | } |
448 | } |
449 | return $enabled; |
450 | } |
451 | |
452 | /** |
453 | * Retrieves all disabled extensions. |
454 | * |
455 | * @param bool $phpbb_relative Whether the path should be relative to phpbb root |
456 | * |
457 | * @return array An array with extension names as keys and and the |
458 | * database stored extension information as values |
459 | */ |
460 | public function all_disabled($phpbb_relative = true) |
461 | { |
462 | $disabled = array(); |
463 | foreach ($this->extensions as $name => $data) |
464 | { |
465 | if ($this->is_disabled($name)) |
466 | { |
467 | $disabled[$name] = ($phpbb_relative ? $this->phpbb_root_path : '') . $data['ext_path']; |
468 | } |
469 | } |
470 | return $disabled; |
471 | } |
472 | |
473 | /** |
474 | * Check to see if a given extension is available on the filesystem |
475 | * |
476 | * @param string $name Extension name to check NOTE: Can be user input |
477 | * @return bool Depending on whether or not the extension is available |
478 | */ |
479 | public function is_available($name) |
480 | { |
481 | $md_manager = $this->create_extension_metadata_manager($name); |
482 | try |
483 | { |
484 | return $md_manager->get_metadata('all') && $md_manager->validate_enable(); |
485 | } |
486 | catch (\phpbb\extension\exception $e) |
487 | { |
488 | return false; |
489 | } |
490 | } |
491 | |
492 | /** |
493 | * Check to see if a given extension is enabled |
494 | * |
495 | * @param string $name Extension name to check |
496 | * @return bool Depending on whether or not the extension is enabled |
497 | */ |
498 | public function is_enabled($name) |
499 | { |
500 | return isset($this->extensions[$name]['ext_active']) && $this->extensions[$name]['ext_active']; |
501 | } |
502 | |
503 | /** |
504 | * Check to see if a given extension is disabled |
505 | * |
506 | * @param string $name Extension name to check |
507 | * @return bool Depending on whether or not the extension is disabled |
508 | */ |
509 | public function is_disabled($name) |
510 | { |
511 | return isset($this->extensions[$name]['ext_active']) && !$this->extensions[$name]['ext_active']; |
512 | } |
513 | |
514 | /** |
515 | * Check to see if a given extension is configured |
516 | * |
517 | * All enabled and disabled extensions are considered configured. A purged |
518 | * extension that is no longer in the database is not configured. |
519 | * |
520 | * @param string $name Extension name to check |
521 | * @return bool Depending on whether or not the extension is configured |
522 | */ |
523 | public function is_configured($name) |
524 | { |
525 | return isset($this->extensions[$name]['ext_active']); |
526 | } |
527 | |
528 | /** |
529 | * Check the version and return the available updates (for an extension). |
530 | * |
531 | * @param \phpbb\extension\metadata_manager $md_manager The metadata manager for the version to check. |
532 | * @param bool $force_update Ignores cached data. Defaults to false. |
533 | * @param bool $force_cache Force the use of the cache. Override $force_update. |
534 | * @param string $stability Force the stability (null by default). |
535 | * @return array |
536 | * @throws runtime_exception |
537 | */ |
538 | public function version_check(\phpbb\extension\metadata_manager $md_manager, $force_update = false, $force_cache = false, $stability = null) |
539 | { |
540 | $meta = $md_manager->get_metadata('all'); |
541 | |
542 | if (!isset($meta['extra']['version-check'])) |
543 | { |
544 | throw new runtime_exception('NO_VERSIONCHECK'); |
545 | } |
546 | |
547 | $version_check = $meta['extra']['version-check']; |
548 | |
549 | $version_helper = new \phpbb\version_helper($this->cache, $this->config, new file_downloader()); |
550 | $version_helper->set_current_version($meta['version']); |
551 | $version_helper->set_file_location($version_check['host'], $version_check['directory'], $version_check['filename'], isset($version_check['ssl']) ? $version_check['ssl'] : false); |
552 | $version_helper->force_stability($stability); |
553 | |
554 | return $version_helper->get_ext_update_on_branch($force_update, $force_cache); |
555 | } |
556 | |
557 | /** |
558 | * Check to see if a given extension is purged |
559 | * |
560 | * An extension is purged if it is available, not enabled and not disabled. |
561 | * |
562 | * @param string $name Extension name to check |
563 | * @return bool Depending on whether or not the extension is purged |
564 | */ |
565 | public function is_purged($name) |
566 | { |
567 | return $this->is_available($name) && !$this->is_configured($name); |
568 | } |
569 | |
570 | /** |
571 | * Instantiates a \phpbb\finder\finder. |
572 | * |
573 | * @param bool $use_all_available Should we load all extensions, or just enabled ones |
574 | * @return \phpbb\finder\finder An extension finder instance |
575 | */ |
576 | public function get_finder($use_all_available = false) |
577 | { |
578 | $finder = $this->finder_factory->get($this->cache_name . '_finder'); |
579 | |
580 | if ($use_all_available) |
581 | { |
582 | $finder->set_extensions(array_keys($this->all_available())); |
583 | } |
584 | else |
585 | { |
586 | $finder->set_extensions(array_keys($this->all_enabled())); |
587 | } |
588 | |
589 | return $finder; |
590 | } |
591 | } |