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\di;
15
16use phpbb\filesystem\filesystem;
17use Symfony\Bridge\ProxyManager\LazyProxy\PhpDumper\ProxyDumper;
18use Symfony\Component\Config\ConfigCache;
19use Symfony\Component\Config\FileLocator;
20use Symfony\Component\DependencyInjection\ContainerBuilder;
21use Symfony\Component\DependencyInjection\Dumper\PhpDumper;
22use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
23use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
24use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass;
25use Symfony\Component\Filesystem\Exception\IOException;
26use Symfony\Component\Finder\Finder;
27use Symfony\Component\HttpKernel\DependencyInjection\MergeExtensionConfigurationPass;
28
29class container_builder
30{
31	/**
32	 * @var string The environment to use.
33	 */
34	protected $environment;
35
36	/**
37	 * @var string phpBB Root Path
38	 */
39	protected $phpbb_root_path;
40
41	/**
42	 * @var string php file extension
43	 */
44	protected $php_ext;
45
46	/**
47	 * The container under construction
48	 *
49	 * @var ContainerBuilder
50	 */
51	protected $container;
52
53	/**
54	 * @var \phpbb\db\driver\driver_interface
55	 */
56	protected $dbal_connection = null;
57
58	/**
59	 * Indicates whether extensions should be used (default to true).
60	 *
61	 * @var bool
62	 */
63	protected $use_extensions = true;
64
65	/**
66	 * Defines a custom path to find the configuration of the container (default to $this->phpbb_root_path . 'config')
67	 *
68	 * @var string
69	 */
70	protected $config_path = null;
71
72	/**
73	 * Indicates whether the container should be dumped to the filesystem (default to true).
74	 *
75	 * If DEBUG_CONTAINER is set this option is ignored and a new container is build.
76	 *
77	 * @var bool
78	 */
79	protected $use_cache = true;
80
81	/**
82	 * Indicates if the container should be compiled automatically (default to true).
83	 *
84	 * @var bool
85	 */
86	protected $compile_container = true;
87
88	/**
89	 * Custom parameters to inject into the container.
90	 *
91	 * Default to:
92	 * 	array(
93	 * 		'core.root_path', $this->phpbb_root_path,
94	 * 		'core.php_ext', $this->php_ext,
95	 * );
96	 *
97	 * @var array
98	 */
99	protected $custom_parameters = [];
100
101	/**
102	 * @var \phpbb\config_php_file
103	 */
104	protected $config_php_file;
105
106	/**
107	 * @var string
108	 */
109	protected $cache_dir;
110
111	/**
112	 * @var array
113	 */
114	private $container_extensions;
115
116	/** @var \Exception */
117	private $build_exception;
118
119	/**
120	 * @var array
121	 */
122	private $env_parameters = [];
123
124	/**
125	 * Constructor
126	 *
127	 * @param string $phpbb_root_path Path to the phpbb includes directory.
128	 * @param string $php_ext php file extension
129	 */
130	public function __construct($phpbb_root_path, $php_ext)
131	{
132		$this->phpbb_root_path	= $phpbb_root_path;
133		$this->php_ext			= $php_ext;
134		$this->env_parameters	= $this->get_env_parameters();
135
136		if (isset($this->env_parameters['core.cache_dir']))
137		{
138			$this->with_cache_dir($this->env_parameters['core.cache_dir']);
139		}
140	}
141
142	/**
143	 * Build and return a new Container respecting the current configuration
144	 *
145	 * @return \phpbb_cache_container|ContainerBuilder
146	 */
147	public function get_container()
148	{
149		try
150		{
151			$build_container = true;
152
153			if ($this->use_cache)
154			{
155				if ($this->use_extensions)
156				{
157					$autoload_cache = new ConfigCache($this->get_autoload_filename(), defined('DEBUG'));
158
159					if (!$autoload_cache->isFresh())
160					{
161						// autoload cache should be refreshed
162						$this->load_extensions();
163					}
164
165					require($this->get_autoload_filename());
166				}
167
168				$container_filename = $this->get_container_filename();
169				$config_cache = new ConfigCache($container_filename, defined('DEBUG'));
170
171				if ($config_cache->isFresh())
172				{
173					require($config_cache->getPath());
174					$this->container = new \phpbb_cache_container();
175					$build_container = false;
176				}
177			}
178
179			if ($build_container)
180			{
181				$this->container_extensions = [
182					new extension\core($this->get_config_path()),
183				];
184
185				if ($this->use_extensions)
186				{
187					$this->load_extensions();
188				}
189
190				// Add tables extension after all extensions
191				$this->container_extensions[] = new extension\tables();
192
193				// Inject the config
194				if ($this->config_php_file)
195				{
196					$this->container_extensions[] = new extension\config($this->config_php_file);
197				}
198
199				$this->container = $this->create_container($this->container_extensions);
200
201				// Easy collections through tags
202				$this->container->addCompilerPass(new pass\collection_pass());
203
204				// Event listeners "phpBB style"
205				$this->container->addCompilerPass(new RegisterListenersPass('dispatcher', 'event.listener_listener', 'event.listener'));
206
207				// Event listeners "Symfony style"
208				$this->container->addCompilerPass(new RegisterListenersPass('dispatcher'));
209
210				if ($this->use_extensions)
211				{
212					$this->register_ext_compiler_pass();
213				}
214
215				$filesystem = new filesystem();
216				$loader     = new YamlFileLoader($this->container, new FileLocator($filesystem->realpath($this->get_config_path())));
217				$loader->load($this->container->getParameter('core.environment') . '/config.yml');
218
219				$this->inject_custom_parameters();
220
221				if ($this->compile_container)
222				{
223					$this->container->compile();
224
225					if ($this->use_cache)
226					{
227						$this->dump_container($config_cache);
228					}
229				}
230			}
231
232			if ($this->compile_container && $this->config_php_file)
233			{
234				$this->container->set('config.php', $this->config_php_file);
235			}
236
237			$this->inject_dbal_driver();
238
239			return $this->container;
240		}
241		catch (\Exception $e)
242		{
243			// Don't try to recover if we are in the development environment
244			if ($this->get_environment() === 'development')
245			{
246				throw $e;
247			}
248
249			if ($this->build_exception === null)
250			{
251				$this->build_exception = $e;
252
253				return $this
254					->without_extensions()
255					->without_cache()
256					->with_custom_parameters(array_merge($this->custom_parameters, [
257						'container_exception' => $e,
258					]))
259					->get_container();
260			}
261			else
262			{
263				// Rethrow the original exception if it's still failing
264				throw $this->build_exception;
265			}
266		}
267	}
268
269	/**
270	 * Enable the extensions.
271	 *
272	 * @param string $environment The environment to use
273	 * @return $this
274	 */
275	public function with_environment($environment)
276	{
277		$this->environment = $environment;
278
279		return $this;
280	}
281
282	/**
283	 * Enable the extensions.
284	 *
285	 * @return $this
286	 */
287	public function with_extensions()
288	{
289		$this->use_extensions = true;
290
291		return $this;
292	}
293
294	/**
295	 * Disable the extensions.
296	 *
297	 * @return $this
298	 */
299	public function without_extensions()
300	{
301		$this->use_extensions = false;
302
303		return $this;
304	}
305
306	/**
307	 * Enable the caching of the container.
308	 *
309	 * If DEBUG_CONTAINER is set this option is ignored and a new container is build.
310	 *
311	 * @return $this
312	 */
313	public function with_cache()
314	{
315		$this->use_cache = true;
316
317		return $this;
318	}
319
320	/**
321	 * Disable the caching of the container.
322	 *
323	 * @return $this
324	 */
325	public function without_cache()
326	{
327		$this->use_cache = false;
328
329		return $this;
330	}
331
332	/**
333	 * Set the cache directory.
334	 *
335	 * @param string $cache_dir The cache directory.
336	 * @return $this
337	 */
338	public function with_cache_dir($cache_dir)
339	{
340		$this->cache_dir = $cache_dir;
341
342		return $this;
343	}
344
345	/**
346	 * Enable the compilation of the container.
347	 *
348	 * @return $this
349	 */
350	public function with_compiled_container()
351	{
352		$this->compile_container = true;
353
354		return $this;
355	}
356
357	/**
358	 * Disable the compilation of the container.
359	 *
360	 * @return $this
361	 */
362	public function without_compiled_container()
363	{
364		$this->compile_container = false;
365
366		return $this;
367	}
368
369	/**
370	 * Set a custom path to find the configuration of the container.
371	 *
372	 * @param string $config_path
373	 * @return $this
374	 */
375	public function with_config_path($config_path)
376	{
377		$this->config_path = $config_path;
378
379		return $this;
380	}
381
382	/**
383	 * Set custom parameters to inject into the container.
384	 *
385	 * @param array $custom_parameters
386	 * @return $this
387	 */
388	public function with_custom_parameters($custom_parameters)
389	{
390		$this->custom_parameters = $custom_parameters;
391
392		return $this;
393	}
394
395	/**
396	 * Set custom parameters to inject into the container.
397	 *
398	 * @param \phpbb\config_php_file $config_php_file
399	 * @return $this
400	 */
401	public function with_config(\phpbb\config_php_file $config_php_file)
402	{
403		$this->config_php_file = $config_php_file;
404
405		return $this;
406	}
407
408	/**
409	 * Returns the path to the container configuration (default: root_path/config)
410	 *
411	 * @return string
412	 */
413	protected function get_config_path()
414	{
415		return $this->config_path ?: $this->phpbb_root_path . 'config';
416	}
417
418	/**
419	 * Returns the path to the cache directory (default: root_path/cache/environment).
420	 *
421	 * @return string Path to the cache directory.
422	 */
423	protected function get_cache_dir()
424	{
425		return $this->cache_dir ?: $this->phpbb_root_path . 'cache/' . $this->get_environment() . '/';
426	}
427
428	/**
429	 * Load the enabled extensions.
430	 */
431	protected function load_extensions()
432	{
433		if ($this->config_php_file !== null)
434		{
435			// Build an intermediate container to load the ext list from the database
436			$container_builder = new container_builder($this->phpbb_root_path, $this->php_ext);
437			$ext_container = $container_builder
438				->without_cache()
439				->without_extensions()
440				->with_config($this->config_php_file)
441				->with_config_path($this->get_config_path())
442				->with_environment('production')
443				->without_compiled_container()
444				->get_container()
445			;
446
447			$ext_container->register('cache.driver', '\\phpbb\\cache\\driver\\dummy');
448			$ext_container->compile();
449
450			$extensions = $ext_container->get('ext.manager')->all_enabled();
451
452			// Load each extension found
453			$autoloaders = '<?php
454/**
455 * Loads all extensions custom auto-loaders.
456 *
457 * This file has been auto-generated
458 * by phpBB while loading the extensions.
459 */
460
461';
462			foreach ($extensions as $ext_name => $path)
463			{
464				$extension_class = '\\' . str_replace('/', '\\', $ext_name) . '\\di\\extension';
465
466				if (!class_exists($extension_class))
467				{
468					$extension_class = '\\phpbb\\extension\\di\\extension_base';
469				}
470
471				$this->container_extensions[] = new $extension_class($ext_name, $path);
472
473				// Load extension autoloader
474				$filename = $path . 'vendor/autoload.php';
475				if (file_exists($filename))
476				{
477					$autoloaders .= "require('{$filename}');\n";
478				}
479			}
480
481			$configCache = new ConfigCache($this->get_autoload_filename(), false);
482			$configCache->write($autoloaders);
483
484			require($this->get_autoload_filename());
485		}
486		else
487		{
488			// To load the extensions we need the database credentials.
489			// Automatically disable the extensions if we don't have them.
490			$this->use_extensions = false;
491		}
492	}
493
494	/**
495	 * Dump the container to the disk.
496	 *
497	 * @param ConfigCache $cache The config cache
498	 */
499	protected function dump_container($cache)
500	{
501		try
502		{
503			$dumper = new PhpDumper($this->container);
504			$proxy_dumper = new ProxyDumper();
505			$dumper->setProxyDumper($proxy_dumper);
506
507			$cached_container_dump = $dumper->dump(array(
508				'class'      => 'phpbb_cache_container',
509				'base_class' => 'Symfony\\Component\\DependencyInjection\\Container',
510			));
511
512			$cache->write($cached_container_dump, $this->container->getResources());
513		}
514		catch (IOException $e)
515		{
516			// Don't fail if the cache isn't writeable
517		}
518	}
519
520	/**
521	 * Create the ContainerBuilder object
522	 *
523	 * @param array $extensions Array of Container extension objects
524	 * @return ContainerBuilder object
525	 */
526	protected function create_container(array $extensions)
527	{
528		$container = new ContainerBuilder(new ParameterBag($this->get_core_parameters()));
529		$container->setProxyInstantiator(new proxy_instantiator($this->get_cache_dir()));
530
531		$extensions_alias = array();
532
533		foreach ($extensions as $extension)
534		{
535			$container->registerExtension($extension);
536			$extensions_alias[] = $extension->getAlias();
537		}
538
539		$container->getCompilerPassConfig()->setMergePass(new MergeExtensionConfigurationPass($extensions_alias));
540
541		return $container;
542	}
543
544	/**
545	 * Inject the customs parameters into the container
546	 */
547	protected function inject_custom_parameters()
548	{
549		foreach ($this->custom_parameters as $key => $value)
550		{
551			$this->container->setParameter($key, $value);
552		}
553	}
554
555	/**
556	 * Inject the dbal connection driver into container
557	 */
558	protected function inject_dbal_driver()
559	{
560		if (empty($this->config_php_file))
561		{
562			return;
563		}
564
565		$config_data = $this->config_php_file->get_all();
566		if (!empty($config_data))
567		{
568			if ($this->dbal_connection === null)
569			{
570				$dbal_driver_class = $this->config_php_file->convert_30_dbms_to_31($this->config_php_file->get('dbms'));
571				/** @var \phpbb\db\driver\driver_interface $dbal_connection */
572				$this->dbal_connection = new $dbal_driver_class();
573				$this->dbal_connection->sql_connect(
574					$this->config_php_file->get('dbhost'),
575					$this->config_php_file->get('dbuser'),
576					$this->config_php_file->get('dbpasswd'),
577					$this->config_php_file->get('dbname'),
578					$this->config_php_file->get('dbport'),
579					false,
580					defined('PHPBB_DB_NEW_LINK') && PHPBB_DB_NEW_LINK
581				);
582			}
583			$this->container->set('dbal.conn.driver', $this->dbal_connection);
584		}
585	}
586
587	/**
588	 * Returns the core parameters.
589	 *
590	 * @return array An array of core parameters
591	 */
592	protected function get_core_parameters()
593	{
594		return array_merge(
595			[
596				'core.root_path'     => $this->phpbb_root_path,
597				'core.php_ext'       => $this->php_ext,
598				'core.environment'   => $this->get_environment(),
599				'core.debug'         => defined('DEBUG') ? DEBUG : false,
600				'core.cache_dir'     => $this->get_cache_dir(),
601			],
602			$this->env_parameters
603		);
604	}
605
606	/**
607	 * Gets the environment parameters.
608	 *
609	 * Only the parameters starting with "PHPBB__" are considered.
610	 *
611	 * @return array An array of parameters
612	 */
613	protected function get_env_parameters()
614	{
615		$parameters = array();
616		foreach ($_SERVER as $key => $value)
617		{
618			if (0 === strpos($key, 'PHPBB__'))
619			{
620				$parameters[strtolower(str_replace('__', '.', substr($key, 9)))] = $value;
621			}
622		}
623
624		return $parameters;
625	}
626
627	/**
628	 * Get the filename under which the dumped container will be stored.
629	 *
630	 * @return string Path for dumped container
631	 */
632	protected function get_container_filename()
633	{
634		$container_params = [
635			'phpbb_root_path' => $this->phpbb_root_path,
636			'use_extensions' => $this->use_extensions,
637			'config_path' => $this->config_path,
638		];
639
640		return $this->get_cache_dir() . 'container_' . md5(implode(',', $container_params)) . '.' . $this->php_ext;
641	}
642
643	/**
644	 * Get the filename under which the dumped extensions autoloader will be stored.
645	 *
646	 * @return string Path for dumped extensions autoloader
647	 */
648	protected function get_autoload_filename()
649	{
650		$container_params = [
651			'phpbb_root_path' => $this->phpbb_root_path,
652			'use_extensions' => $this->use_extensions,
653			'config_path' => $this->config_path,
654		];
655
656		return $this->get_cache_dir() . 'autoload_' . md5(implode(',', $container_params)) . '.' . $this->php_ext;
657	}
658
659	/**
660	 * Return the name of the current environment.
661	 *
662	 * @return string
663	 */
664	protected function get_environment()
665	{
666		return $this->environment ?: PHPBB_ENVIRONMENT;
667	}
668
669	private function register_ext_compiler_pass()
670	{
671		$finder = new Finder();
672		$finder
673			->name('*_pass.php')
674			->path('di/pass')
675			->files()
676			->ignoreDotFiles(true)
677			->ignoreUnreadableDirs(true)
678			->ignoreVCS(true)
679			->followLinks()
680			->in($this->phpbb_root_path . 'ext')
681		;
682
683		/** @var \SplFileInfo $pass */
684		foreach ($finder as $pass)
685		{
686			$filename = $pass->getPathname();
687			$filename = substr($filename, 0, -strlen('.' . $pass->getExtension()));
688			$filename = str_replace(DIRECTORY_SEPARATOR, '/', $filename);
689			$className = preg_replace('#^.*ext/#', '', $filename);
690			$className = '\\' . str_replace('/', '\\', $className);
691
692			if (class_exists($className) && in_array('Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface', class_implements($className), true))
693			{
694				$this->container->addCompilerPass(new $className());
695			}
696		}
697	}
698}
699