<?php

namespace XF;

use Doctrine\Common\Cache\CacheProvider;
use League\Flysystem\MountManager;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\Mailer\Transport\NullTransport;
use XF\AddOn\DataManager;
use XF\AdminSearch\Searcher;
use XF\Captcha\HCaptcha;
use XF\CustomField\DefinitionSet;
use XF\CustomField\Set;
use XF\Db\AbstractAdapter;
use XF\Db\ForeignAdapter;
use XF\Db\Mysqli\Adapter;
use XF\Db\ReplicationAdapterInterface;
use XF\Db\SchemaManager;
use XF\Entity\User;
use XF\ForumType\AbstractHandler;
use XF\Giphy\Api;
use XF\Http\Request;
use XF\Http\Response;
use XF\Install\Data\MySql;
use XF\Job\AbstractJob;
use XF\Mail\Mailer;
use XF\Mail\Styler;
use XF\Mvc\Controller;
use XF\Mvc\Dispatcher;
use XF\Mvc\Entity\ArrayCollection;
use XF\Mvc\Entity\ArrayValidator;
use XF\Mvc\Entity\Manager;
use XF\Mvc\Entity\ValueFormatter;
use XF\Mvc\FormAction;
use XF\Mvc\Renderer\AbstractRenderer;
use XF\Mvc\Renderer\Html;
use XF\Mvc\Reply\AbstractReply;
use XF\Mvc\Reply\Message;
use XF\Mvc\Reply\Redirect;
use XF\Mvc\RouteMatch;
use XF\Mvc\Router;
use XF\Mvc\View;
use XF\Permission\Builder;
use XF\Repository\AdminNavigationRepository;
use XF\Repository\ApprovalQueueRepository;
use XF\Repository\BanningRepository;
use XF\Repository\CodeEventListenerRepository;
use XF\Repository\ConnectedAccountRepository;
use XF\Repository\ContentTypeFieldRepository;
use XF\Repository\CountersRepository;
use XF\Repository\ForumTypeRepository;
use XF\Repository\HelpPageRepository;
use XF\Repository\IconRepository;
use XF\Repository\LanguageRepository;
use XF\Repository\NodeTypeRepository;
use XF\Repository\NoticeRepository;
use XF\Repository\OAuthRepository;
use XF\Repository\OptionRepository;
use XF\Repository\PaymentRepository;
use XF\Repository\ReactionRepository;
use XF\Repository\ReportRepository;
use XF\Repository\RouteFilterRepository;
use XF\Repository\RouteRepository;
use XF\Repository\SitemapLogRepository;
use XF\Repository\SmilieRepository;
use XF\Repository\StyleRepository;
use XF\Repository\ThreadFieldRepository;
use XF\Repository\ThreadPrefixRepository;
use XF\Repository\ThreadTypeRepository;
use XF\Repository\UserFieldRepository;
use XF\Repository\UserGroupRepository;
use XF\Repository\UserRepository;
use XF\Repository\UserTitleLadderRepository;
use XF\Repository\UserUpgradeRepository;
use XF\Search\Search;
use XF\Search\Source\AbstractSource;
use XF\Search\Source\MySqlFt;
use XF\Session\CacheStorage;
use XF\Session\DbStorage;
use XF\Session\NullStorage;
use XF\Session\Session;
use XF\Session\StorageInterface;
use XF\Sitemap\Renderer;
use XF\Stats\Grouper\Daily;
use XF\Stats\Grouper\Monthly;
use XF\Stats\Grouper\Weekly;
use XF\Str\Formatter;
use XF\SubContainer\ApiDocs;
use XF\SubContainer\BbCode;
use XF\SubContainer\Bounce;
use XF\SubContainer\Import;
use XF\SubContainer\OAuth;
use XF\SubContainer\Oembed;
use XF\SubContainer\Proxy;
use XF\SubContainer\Spam;
use XF\SubContainer\Unsubscribe;
use XF\SubContainer\Widget;
use XF\Template\Compiler;
use XF\Template\Templater;
use XF\Util\File;
use XF\Util\Php;
use XF\Webhook\Criteria\AbstractCriteria;

use function func_get_args, get_class, intval, is_array, is_scalar, is_string, strlen, strval;

class App implements \ArrayAccess
{
	protected $container;

	protected $preLoadShared = [
		'addOns',
		'addOnsComposer',
		'autoJobRun',
		'bbCodeMedia',
		'classExtensions',
		'codeEventListeners',
		'connectedAccountCount',
		'contentTypes',
		'displayStyles',
		'helpPageCount',
		'iconSprite',
		'languages',
		'masterStyleProperties',
		'nodeTypes',
		'options',
		'paymentProvider',
		'reactions',
		'reportCounts',
		'simpleCache',
		'smilies',
		'unapprovedCounts',
		'userBanners',
		'userTitleLadder',
		'userUpgradeCount',
		'widgetCache',
		'widgetDefinition',
		'widgetPosition',
	];
	protected $preLoadLocal = [];

	public function __construct(Container $container)
	{
		$this->container = $container;

		$this->initialize();
	}

	protected function initialize()
	{
		$time = !empty($_SERVER["REQUEST_TIME_FLOAT"]) ? $_SERVER["REQUEST_TIME_FLOAT"] : microtime(true);

		$container = $this->container;

		$container['time'] = intval($time);
		$container['time.granular'] = $time;

		$container['app.classType'] = '';
		$container['app.defaultType'] = '';

		$container['config.default'] = [
			'db' => [
				'adapterClass' => Adapter::class,
			],
			'fullUnicode' => false,
			'searchInnoDb' => false,
			'cache' => [
				'enabled' => false,
				'sessions' => false,
				'namespace' => 'xf',
				'provider' => 'Null',
				'config' => [],
				'context' => [],
			],
			'pageCache' => [
				'enabled' => false,
				'lifetime' => 300,
				'recordSessionActivity' => true,
				'routeMatches' => [],
				'onSetup' => null,
			],
			'debug' => false,
			'development' => [
				'enabled' => false,
				'defaultAddOn' => '',
				'skipAddOns' => null, // this has to be something other than an array to allow people to change it
				'unlistedAddOns' => [], // simply prevents the noted add-ons being listed as available
				'throwJobErrors' => false,
				'fullJs' => false,
				'fullEditorJs' => false, // internal use only, the necessary files are not distributed
				'fullJsAutoUpdate' => false, // bust js cache whenever a JS file is updated in fullJs mode
				'closureCompilerPath' => null,
			],
			'designer' => [
				'enabled' => false,
				'basePath' => 'src' . \XF::$DS . 'styles',
			],
			'cookie' => [
				'prefix' => 'xf_',
				'path' => '/',
				'domain' => '',
			],
			'http' => [
				'sslVerify' => null,
				'proxy' => null,
			],
			'globalSalt' => '1f3b26b3166179aa9cb77b402ec8f043',
			'superAdmins' => '', // keep this for upgrade purposes
			'internalDataPath' => 'internal_data',
			'codeCachePath' => '%s/code_cache',
			'tempDataPath' => '%s/temp',
			'fsAdapters' => [],
			'externalDataPath' => 'data',
			'externalDataUrl' => 'data',
			'localDataPath' => 'data/local',
			'localDataUrl' => 'data/local', // this must always be same-origin
			'javaScriptUrl' => 'js',
			'chmodWritableValue' => 0,
			'forceCliUser' => '',
			'jobMaxRunTime' => 8,
			'enableMail' => true,
			'enableMailQueue' => true,
			'enableListeners' => true,
			'enableTemplateModificationCallbacks' => true,
			'enableClickjackingProtection' => true,
			'enableReverseTabnabbingProtection' => true,
			'enableLoginCsrf' => true,
			'enableCssSplitting' => null, // attempts to auto-detect HTTP2+
			'enableGzip' => true,
			'enableContentLength' => true,
			'enableTfa' => true,
			'enableLivePayments' => true,
			'enableApi' => true,
			'enableAddOnArchiveInstaller' => false,
			'enableOneClickUpgrade' => true,
			'disableRocketLoader' => null,
			'maxImageResizeQuality' => 85,
			'maxImageResizePixelCount' => 48000000,
			'adminLogLength' => 60, // number of days to keep admin log entries
			'adminColorHueShift' => 0,
			'checkVersion' => true,
			'passwordIterations' => 10,
			'auth' => null,
			'proxyUrlFormat' => 'proxy.php?{type}={url}&hash={hash}',
			'sendmailPath' => null,
			'serviceUnavailableCode' => 503,
			'serviceWorkerPath' => null,
			'sessionActivityExpiration' => 3600,
			'sessionActivityOptimized' => true, // works around MySQL locking issues
			'unassociatedAttachmentLimit' => 100,
			'container' => [],
			'exists' => false,
		];

		$container['config.file'] = \XF::getSourceDirectory() . '/config.php';
		$container['config.legacyFile'] = \XF::getRootDirectory() . '/library/config.php';
		$container['config'] = function (Container $c)
		{
			static $configLock = false;
			if ($configLock)
			{
				throw new \Error(
					'Attempted to recursively load configuration file'
				);
			}

			$configLock = true;

			$default = $c['config.default'];
			$file = $c['config.file'];
			$legacyFile = $c['config.legacyFile'];

			if (file_exists($file))
			{
				$config = [];
				require $file;

				$config = array_replace_recursive($default, $config);
				$config['exists'] = true;
				$config['legacyExists'] = null;
			}
			else
			{
				if (file_exists($legacyFile))
				{
					$config = [];
					require $legacyFile;

					$config = array_replace_recursive($default, $config);
					$config['legacyExists'] = true;
				}
				else
				{
					$config = $default;
					$config['legacyExists'] = false;
				}
			}

			// if there's a specific session cache specified, force this to be enabled
			if (!empty($config['cache']['context']['sessions']))
			{
				$config['cache']['sessions'] = true;
			}

			foreach ($config['container'] AS $key => $value)
			{
				$c[$key] = $value;
			}

			$configLock = false;

			return $config;
		};

		$container['jQueryVersion'] = '3.7.1';

		$container['jsVersion'] = function (Container $c)
		{
			return substr(md5(\XF::$versionId . $c['options']->jsLastUpdate), 0, 8);
		};

		$container['avatarSizeMap'] = [
			'o' => 384,
			'h' => 384,
			'l' => 192,
			'm' => 96,
			's' => 48,
		];

		$container['editorToolbarSizes'] = [
			'SM' => 420,
			'MD' => 575,
			'LG' => 900,
		];

		$container['profileBannerSizeMap'] = [
			'l' => 1280,
			'm' => 640,
		];

		$container['request'] = function (Container $c)
		{
			$request = new Request($c['inputFilterer']);
			$request->setCookiePrefix($c['config']['cookie']['prefix']);
			return $request;
		};
		$container['request.paths'] = function (Container $c)
		{
			if (PHP_SAPI === 'cli')
			{
				$canonical = $c['options']->boardUrl;
				$urlPath = parse_url($canonical, PHP_URL_PATH) ?? '';

				$rootFullPath = $fullPath = $canonical;
				$rootBasePath = $basePath = $canonical ? $urlPath : '';
			}
			else
			{
				/** @var Request $request */
				$request = $c['request'];

				$fullPath = $request->getFullBasePath();
				$basePath = $request->getBasePath();

				// Protect against the request object being from an older release
				// that didn't have these methods.
				if (method_exists($request, 'getFullXfRootPath'))
				{
					$rootFullPath = $request->getFullXfRootPath();
					$rootBasePath = $request->getXfRootPath();
				}
				else
				{
					$rootFullPath = $fullPath;
					$rootBasePath = $basePath;
				}

				$canonical = $c['options']->boardUrl ?? $rootFullPath;
			}

			return [
				'full' => rtrim($fullPath, '/') . '/',
				'base' => rtrim($basePath, '/') . '/',
				'root-full' => rtrim($rootFullPath, '/') . '/',
				'root-base' => rtrim($rootBasePath, '/') . '/',
				'canonical' => rtrim($canonical, '/') . '/',
				'nopath' => '',
			];
		};
		$container['request.pather'] = function (Container $c)
		{
			$paths = $c['request.paths'];

			return function ($url, $modifier = 'base') use ($paths)
			{
				if (preg_match('#^(/|[a-z]+:)#i', $url))
				{
					return $url;
				}

				if (isset($paths[$modifier]))
				{
					$url = $paths[$modifier] . $url;
				}
				else
				{
					$url = $paths['base'] . $url;
				}

				return $url;
			};
		};

		$container['inlineImageTypes'] = function (Container $c)
		{
			return [
				'gif' => 'image/gif',
				'jpg' => 'image/jpeg',
				'jpeg' => 'image/jpeg',
				'jpe' => 'image/jpeg',
				'pjpeg' => 'image/pjpeg',
				'png' => 'image/png',
				'ico' => 'image/ico',
				'webp' => 'image/webp',
			];
		};

		$container['inlineVideoTypes'] = function (Container $c)
		{
			return [
				'm4v' => 'video/mp4',
				'mov' => 'video/quicktime',
				'mp4' => 'video/mp4',
				'mp4v' => 'video/mp4',
				'mpeg' => 'video/mpeg',
				'mpg' => 'video/mpeg',
				'ogv' => 'video/ogg',
				'webm' => 'video/webm',
			];
		};

		$container['inlineAudioTypes'] = function (Container $c)
		{
			return [
				'mp3' => 'audio/mpeg',
				'opus' => 'audio/ogg',
				'ogg' => 'audio/ogg',
				'wav' => 'audio/wav',
			];
		};

		$container['cookieConsent'] = function ($c)
		{
			$class = $this->extendClass(CookieConsent::class);
			$cookieConsent = new $class();

			$this->setupCookieConsent($cookieConsent);

			return $cookieConsent;
		};

		$container['response'] = function (Container $c)
		{
			/** @var Request $request */
			$request = $c['request'];

			$cookie = $c['config']['cookie'];
			$cookie['secure'] = $request->isSecure() ? true : false;

			$response = new Response();
			$response->setCookieConfig($cookie);

			$config = $c['config'];
			if ($config['enableClickjackingProtection'])
			{
				$response->header('X-Frame-Options', 'SAMEORIGIN');
			}
			if (!$config['enableGzip'])
			{
				$response->compressIfAble(false);
			}
			if (!$config['enableContentLength'])
			{
				$response->includeContentLength(false);
			}

			return $response;
		};

		$container['inputFilterer'] = function (Container $c)
		{
			$class = $this->extendClass(InputFilterer::class);
			return new $class($c['config']['fullUnicode']);
		};

		$container['dispatcher'] = function ()
		{
			$class = $this->extendClass(Dispatcher::class);
			return new $class($this);
		};

		$container['router'] = function (Container $c)
		{
			return $c['router.public'];
		};

		$container['router.public'] = function (Container $c)
		{
			$class = $this->extendClass(Router::class);

			/** @var Router $r */
			$r = new $class($c['router.public.formatter'], $c['router.public.routes']);
			$r->addRoutePreProcessor('filter', [$r, 'routePreProcessRouteFilter']);
			$r->addRoutePreProcessor('extension', [$r, 'routePreProcessExtension']);
			$r->addRoutePreProcessor('type', [$r, 'routePreProcessResponseType']);

			$routeFilters = $c['routeFilters'];
			$r->setRouteFilters($routeFilters['in'], $routeFilters['out']);

			$r->setPather($c['request.pather']);
			$r->setIndexRoute($c['options']->indexRoute ?: 'forums/');
			$r->setIncludeTitleInUrls($c['options']->includeTitleInUrls);
			$r->setRomanizeUrls($c['options']->romanizeUrls);

			$this->fire('router_public_setup', [$c, &$r]);

			return $r;
		};

		$container['router.public.formatter'] = function ($c)
		{
			if ($c['options']->useFriendlyUrls)
			{
				return function ($route, $queryString)
				{
					return $route . (strlen($queryString) ? '?' . $queryString : '');
				};
			}
			else
			{
				return function ($route, $queryString)
				{
					$suffix = $route . (strlen($queryString) ? (strlen($route) ? '&' : '') . $queryString : '');
					return strlen($suffix) ? 'index.php?' . $suffix : 'index.php';
				};
			}
		};
		$container['router.public.routes'] = $this->fromRegistry(
			'routesPublic',
			function (Container $c) { return $c['em']->getRepository(RouteRepository::class)->rebuildRouteCache('public'); }
		);

		$container['router.install'] = function (Container $c)
		{
			$class = $this->extendClass(Router::class);

			/** @var Router $r */
			$r = new $class($c['router.install.formatter'], $c['router.install.routes']);
			$r->addRoutePreProcessor('extension', [$r, 'routePreProcessExtension']);
			$r->addRoutePreProcessor('type', [$r, 'routePreProcessResponseType']);

			$r->setPather($c['request.pather']);

			return $r;
		};
		$container['router.install.formatter'] = $container->wrap(function ($route, $queryString)
		{
			$suffix = $route . (strlen($queryString) ? '&' . $queryString : '');
			return strlen($suffix) ? 'index.php?' . $suffix : 'index.php';
		});
		$container['router.install.routes'] = function (Container $c)
		{
			return [
				'install' => [
					'' => [
						'controller' => 'XF:Install',
						'format' => '',
					],
				],
				'upgrade' => [
					'' => [
						'controller' => 'XF:Upgrade',
						'format' => '',
					],
				],
				'index' => [
					'' => [
						'controller' => 'XF:Index',
						'format' => '',
					],
				],
			];
		};

		$container['router.admin'] = function (Container $c)
		{
			$class = $this->extendClass(Router::class);

			/** @var Router $r */
			$r = new $class($c['router.admin.formatter'], $c['router.admin.routes']);
			$r->addRoutePreProcessor('extension', [$r, 'routePreProcessExtension']);
			$r->addRoutePreProcessor('type', [$r, 'routePreProcessResponseType']);

			$r->setPather($c['request.pather']);
			$r->setRomanizeUrls($c['options']->romanizeUrls);

			return $r;
		};
		$container['router.admin.formatter'] = $container->wrap(function ($route, $queryString)
		{
			$suffix = $route . (strlen($queryString) ? '&' . $queryString : '');
			return strlen($suffix) ? 'admin.php?' . $suffix : 'admin.php';
		});
		$container['router.admin.routes'] = $this->fromRegistry(
			'routesAdmin',
			function (Container $c) { return $c['em']->getRepository(RouteRepository::class)->rebuildRouteCache('admin'); }
		);

		$container['router.api'] = function (Container $c)
		{
			$class = $this->extendClass(\XF\Api\Mvc\Router::class);

			/** @var \XF\Api\Mvc\Router $r */
			$r = new $class($c['router.api.formatter'], $c['router.api.routes']);
			$r->addRoutePreProcessor('apiPrefix', [$r, 'routePreProcessApiPrefix']);
			$r->addRoutePreProcessor('apiVersion', [$r, 'routePreProcessApiVersion']);
			$r->addRoutePreProcessor('apiType', [$r, 'routePreProcessApiResponseType']);

			$r->setPather($c['request.pather']);

			return $r;
		};
		$container['router.api.formatter'] = function ($c)
		{
			if ($c['options']->useFriendlyUrls)
			{
				return function ($route, $queryString)
				{
					return 'api/' . $route . (strlen($queryString) ? '?' . $queryString : '');
				};
			}
			else
			{
				return function ($route, $queryString)
				{
					$suffix = $route . (strlen($queryString) ? (strlen($route) ? '&' : '') . $queryString : '');
					return 'index.php?api/' . $suffix;
				};
			}
		};
		$container['router.api.routes'] = $this->fromRegistry(
			'routesApi',
			function (Container $c) { return $c['em']->getRepository(RouteRepository::class)->rebuildRouteCache('api'); }
		);

		$container['logger'] = function ($c)
		{
			$class = $this->extendClass(Logger::class);
			return new $class($this);
		};

		$container['debugger'] = function ($c)
		{
			$class = $this->extendClass(Debugger::class);
			return new $class($this);
		};

		$container['contactUrl'] = function ($c)
		{
			$options = $c['options'];
			$router = $c['router.public'];

			if (!isset($options->contactUrl['type']))
			{
				return '';
			}

			switch ($options->contactUrl['type'])
			{
				case 'default': $url = $router->buildLink('misc/contact'); break;
				case 'custom': $url = $options->contactUrl['custom']; break;
				default: $url = '';
			}
			return $url;
		};

		$container['privacyPolicyUrl'] = function ($c)
		{
			$options = $c['options'];
			$router = $c['router.public'];

			if (!isset($options->privacyPolicyUrl['type']))
			{
				return '';
			}

			switch ($options->privacyPolicyUrl['type'])
			{
				case 'default': return $router->buildLink('help/privacy-policy/');
				case 'custom': return $options->privacyPolicyUrl['custom'];
				default: return '';
			}
		};

		$container['tosUrl'] = function ($c)
		{
			$options = $c['options'];
			$router = $c['router.public'];

			if (!isset($options->tosUrl['type']))
			{
				return '';
			}

			switch ($options->tosUrl['type'])
			{
				case 'default': $url = $router->buildLink('help/terms/'); break;
				case 'custom': $url = $options->tosUrl['custom']; break;
				default: $url = '';
			}
			return $url;
		};

		$container['homePageUrl'] = function (Container $c)
		{
			$options = $c['options'];
			$router = $c['router.public'];

			$homePageUrl = $options->homePageUrl;

			$this->extension()->fire('home_page_url', [&$homePageUrl, $router]);

			if ($homePageUrl)
			{
				/** @var \Closure $pather */
				$pather = $c['request.pather'];
				$homePageUrl = $pather($homePageUrl, 'full');
			}

			return $homePageUrl;
		};

		$container['navigation.compiler'] = function (Container $c)
		{
			return new Navigation\Compiler($c['templateCompiler']);
		};
		$container['navigation.file'] = 'navigation_cache.php'; // will be written to code-cache/codeCachePath

		$container['navigation.admin'] = function ($c)
		{
			$class = \XF::extendClass(AdminNavigation::class);
			return new $class($c['navigation.adminEntries']);
		};
		$container['navigation.adminEntries'] = $this->fromRegistry(
			'adminNavigation',
			function (Container $c) { return $c['em']->getRepository(AdminNavigationRepository::class)->rebuildNavigationCache(); }
		);

		$container['db'] = function ($c)
		{
			$config = $c['config'];

			$dbConfig = $config['db'];
			$adapterClass = $dbConfig['adapterClass'];
			unset($dbConfig['adapterClass']);

			/** @var AbstractAdapter $db */
			$db = new $adapterClass($dbConfig, $config['fullUnicode']);

			if ($db instanceof ForeignAdapter)
			{
				throw new \LogicException('This database adapter cannot be used natively by XenForo.');
			}

			if (\XF::$debugMode)
			{
				$db->logQueries(true);
			}

			return $db;
		};

		$container->factory('cache', function ($context, array $params, Container $c)
		{
			$adapter = $c->create('cache.symfony', $context, $params);
			if (!$adapter)
			{
				return null;
			}

			return new CacheProvider($adapter);
		});

		$container->factory('cache.symfony', function ($context, array $params, Container $c): ?AdapterInterface
		{
			$cacheConfig = $c['config']['cache'];
			if (!$cacheConfig['enabled'])
			{
				return null;
			}

			$namespace = $cacheConfig['namespace'];

			if ($context)
			{
				if (empty($cacheConfig['context'][$context]))
				{
					return null;
				}

				$cacheConfig = $cacheConfig['context'][$context];
				if (!is_array($cacheConfig) || empty($cacheConfig['provider']))
				{
					return null;
				}

				if (!isset($cacheConfig['config']))
				{
					$cacheConfig['config'] = [];
				}
				if (isset($cacheConfig['namespace']))
				{
					$namespace = $cacheConfig['namespace'];
				}
			}

			/** @var CacheFactory $factory */
			$factory = $c['cache.factory'];
			$factory->setNamespace($namespace);

			try
			{
				$cache = $factory->create(
					$cacheConfig['provider'],
					$cacheConfig['config'],
					false
				);
			}
			catch (\Error $e)
			{
				return null;
			}

			if (!($cache instanceof AdapterInterface))
			{
				return null;
			}

			return $cache;
		});

		$container['cache'] = function (Container $c)
		{
			return $c->create('cache', '');
		};
		$container['cache.factory'] = function ($c)
		{
			// this cannot be dynamically extended because it's used by the registry
			// different types of cache providers can be configured via code in config.php
			return new CacheFactory();
		};

		$container['permission.cache'] = function ($c)
		{
			return new PermissionCache($c['db']);
		};
		$container['permission.builder'] = function ($c)
		{
			return new Builder(
				$c['db'],
				$c['em'],
				$this->getContentTypeField('permission_handler_class')
			);
		};

		$container['registry'] = function ($c)
		{
			return new DataRegistry($c['db'], $this->cache('registry', true, false));
		};

		$container['simpleCache'] = function ($c)
		{
			$class = $this->extendClass(SimpleCache::class);
			return new $class($c['simpleCache.data']);
		};
		$container['simpleCache.data'] = $this->fromRegistry('simpleCache', function ()
		{
			$this->registry()->set('simpleCache', []);
			return [];
		});

		$container['options'] = $this->fromRegistry(
			'options',
			function (Container $c) { return $c['em']->getRepository(OptionRepository::class)->rebuildOptionCache(); },
			function (array $options)
			{
				return new Options($options);
			}
		);

		$container['codeEventListeners'] = $this->fromRegistry(
			'codeEventListeners',
			function (Container $c) { return $c['em']->getRepository(CodeEventListenerRepository::class)->rebuildListenerCache(); }
		);

		$container['contentTypes'] = $this->fromRegistry(
			'contentTypes',
			function (Container $c) { return $c['em']->getRepository(ContentTypeFieldRepository::class)->rebuildContentTypeCache(); }
		);

		$container['customFields.threads'] = $this->fromRegistry(
			'threadFieldsInfo',
			function (Container $c) { return $c['em']->getRepository(ThreadFieldRepository::class)->rebuildFieldCache(); },
			function (array $threadFieldsInfo)
			{
				$class = DefinitionSet::class;
				$class = $this->extendClass($class);

				return new $class($threadFieldsInfo);
			}
		);

		$container['customFields.users'] = $this->fromRegistry(
			'userFieldsInfo',
			function (Container $c) { return $c['em']->getRepository(UserFieldRepository::class)->rebuildFieldCache(); },
			function (array $userFieldsInfo)
			{
				$class = DefinitionSet::class;
				$class = $this->extendClass($class);

				$definitionSet = new $class($userFieldsInfo);
				$definitionSet->addFilter('registration', function (array $field)
				{
					return (!empty($field['show_registration']) || !empty($field['required']));
				});
				$definitionSet->addFilter('profile', function (array $field)
				{
					return !empty($field['viewable_profile']);
				});
				$definitionSet->addFilter('message', function (array $field)
				{
					return !empty($field['viewable_message']);
				});
				return $definitionSet;
			}
		);

		$container['displayStyles'] = $this->fromRegistry(
			'displayStyles',
			function (Container $c) { return $c['em']->getRepository(UserGroupRepository::class)->rebuildDisplayStyleCache(); }
		);

		$container['iconSprite'] = $this->fromRegistry(
			'iconSprite',
			function (Container $c): array
			{
				return $c['em']->getRepository(IconRepository::class)->rebuildIconSpriteCache();
			}
		);

		$container['reportCounts'] = $this->fromRegistry(
			'reportCounts',
			function (Container $c) { return $c['em']->getRepository(ReportRepository::class)->rebuildReportCounts(); }
		);

		$container['unapprovedCounts'] = $this->fromRegistry(
			'unapprovedCounts',
			function (Container $c) { return $c['em']->getRepository(ApprovalQueueRepository::class)->rebuildUnapprovedCounts(); }
		);

		$container['nodeTypes'] = $this->fromRegistry(
			'nodeTypes',
			function (Container $c) { return $c['em']->getRepository(NodeTypeRepository::class)->rebuildNodeTypeCache(); }
		);

		$container['notices'] = $this->fromRegistry(
			'notices',
			function (Container $c) { return $c['em']->getRepository(NoticeRepository::class)->rebuildNoticeCache(); }
		);
		$container['notices.lastReset'] = $this->fromRegistry(
			'noticesLastReset',
			function (Container $c) { return $c['em']->getRepository(NoticeRepository::class)->rebuildNoticeLastResetCache(); }
		);

		$container->factory('criteria', function ($class, array $params, Container $c)
		{
			$class = \XF::stringToClass($class, '\%s\Criteria\%s');
			$class = $this->extendClass($class);

			array_unshift($params, $this);

			return $c->createObject($class, $params);
		}, false);

		$container->factory('webhookCriteria', function ($class, array $params, Container $c)
		{
			$class = \XF::stringToClass($class, '\%s\Webhook\Criteria\%s');
			$class = $this->extendClass($class);

			array_unshift($params, $this);

			return $c->createObject($class, $params);
		}, false);

		$container['routeFilters'] = $this->fromRegistry(
			'routeFilters',
			function (Container $c) { return $c['em']->getRepository(RouteFilterRepository::class)->rebuildRouteFilterCache(); }
		);

		$container['reactionDefault'] = function (Container $c)
		{
			return $c['reactions'][1];
		};

		$container['reactionColors'] = function (Container $c)
		{
			$output = [];
			foreach ($c['reactions'] AS $reactionId => $reaction)
			{
				$output[$reactionId] = $reaction['text_color'];
			}
			return $output;
		};

		$container['reactionSprites'] = $this->fromRegistry(
			'reactionSprites',
			function (Container $c) { return $c['em']->getRepository(ReactionRepository::class)->rebuildReactionSpriteCache(); }
		);

		$container['reactions'] = $this->fromRegistry(
			'reactions',
			function (Container $c) { return $c['em']->getRepository(ReactionRepository::class)->rebuildReactionCache(); }
		);

		$container['smilieSprites'] = $this->fromRegistry(
			'smilieSprites',
			function (Container $c) { return $c['em']->getRepository(SmilieRepository::class)->rebuildSmilieSpriteCache(); }
		);

		$container['smilies'] = $this->fromRegistry(
			'smilies',
			function (Container $c) { return $c['em']->getRepository(SmilieRepository::class)->rebuildSmilieCache(); }
		);

		$container['prefixes.thread'] = $this->fromRegistry(
			'threadPrefixes',
			function (Container $c) { return $c['em']->getRepository(ThreadPrefixRepository::class)->rebuildPrefixCache(); }
		);

		$container['userBanners'] = $this->fromRegistry(
			'userBanners',
			function (Container $c) { return $c['em']->getRepository(UserGroupRepository::class)->rebuildUserBannerCache(); }
		);

		$container['userTitleLadder'] = $this->fromRegistry(
			'userTitleLadder',
			function (Container $c) { return $c['em']->getRepository(UserTitleLadderRepository::class)->rebuildLadderCache(); }
		);

		$container['forumStatistics'] = $this->fromRegistry(
			'forumStatistics',
			function (Container $c) { return $c['em']->getRepository(CountersRepository::class)->rebuildForumStatisticsCache(); }
		);

		$container['connectedAccountCount'] = $this->fromRegistry(
			'connectedAccountCount',
			function (Container $c) { return $c['em']->getRepository(ConnectedAccountRepository::class)->rebuildProviderCount(); }
		);

		$container['helpPageCount'] = $this->fromRegistry(
			'helpPageCount',
			function (Container $c) { return $c['em']->getRepository(HelpPageRepository::class)->rebuildHelpPageCount(); }
		);

		$container['oAuthClientCount'] = $this->fromRegistry(
			'oAuthClientCount',
			function (Container $c) { return $c['em']->getRepository(OAuthRepository::class)->rebuildClientCount(); }
		);

		$container['userUpgradeCount'] = $this->fromRegistry(
			'userUpgradeCount',
			function (Container $c) { return $c['em']->getRepository(UserUpgradeRepository::class)->rebuildUpgradeCount(); }
		);

		$container['session'] = function ()
		{
			throw new \LogicException('The session key must be overridden.');
		};
		$container['session.public'] = function (Container $c)
		{
			$class = $this->extendClass(Session::class);

			/** @var Session $session */
			$session = new $class($c['session.public.storage'], [
				'cookie' => 'session',
			]);
			return $session->start($c['request']);
		};
		$container['session.public.storage'] = function (Container $c)
		{
			$storage = null;
			$cache = $c['cache'];

			$this->fire('session_public_storage_setup', [$c, $cache, &$storage]);
			if ($storage)
			{
				if (!($storage instanceof StorageInterface))
				{
					throw new \LogicException('Storage must be instance of XF\Session\StorageInterface. Received ' . get_class($storage));
				}
				return $storage;
			}

			if ($c['config']['cache']['sessions'] && $cache = $this->cache('sessions', true, false))
			{
				return new CacheStorage($cache, 'session_');
			}
			else
			{
				return new DbStorage($c['db'], 'xf_session');
			}
		};

		$container['session.admin'] = function (Container $c)
		{
			$class = $this->extendClass(Session::class);

			/** @var Session $session */
			$session = new $class($c['session.admin.storage'], [
				'cookie' => 'session_admin',
			]);
			return $session->start($c['request']);
		};
		$container['session.admin.storage'] = function (Container $c)
		{
			return new DbStorage($c['db'], 'xf_session_admin');
		};

		$container['session.install'] = function (Container $c)
		{
			$session = new Session($c['session.install.storage'], [
				'cookie' => 'session_install',
			]);
			return $session->start($c['request']);
		};
		$container['session.install.storage'] = function (Container $c)
		{
			/** @var SchemaManager $sm */
			$sm = $c['db']->getSchemaManager();

			try
			{
				if (!(bool) $sm->getTableStatus('xf_session_install'))
				{
					$mySql = new MySql();
					$tables = $mySql->getTables();
					$sm->createTable('xf_session_install', $tables['xf_session_install']);
				}
			}
			catch (\Exception $e)
			{
			}

			return new DbStorage($c['db'], 'xf_session_install');
		};

		$container['session.api'] = function (Container $c)
		{
			$session = new Session(new NullStorage());
			return $session->start($c['request']);
		};

		$container['session.embedResolver'] = function (Container $c)
		{
			$session = new Session(new NullStorage());
			return $session->start($c['request']);
		};

		$container['csrf.token'] = function (Container $c)
		{
			/** @var Request $request */
			$request = $c['request'];

			$token = $request->getCookie('csrf');
			if (!$token)
			{
				$token = \XF::generateRandomString(16);
				$this->updateCsrfCookie($token);
			}

			/** @var \Closure $validator */
			$validator = $c['csrf.validator'];
			return \XF::$time . ',' . $validator($token, \XF::$time);
		};
		$container['csrf.validator'] = $container->wrap(function ($value, $time)
		{
			return hash_hmac('md5', $value . $time, $this->config('globalSalt'));
		});

		$container['error'] = function ()
		{
			return new Error($this);
		};

		$container['em'] = function (Container $c)
		{
			return new Manager($c['db'], $c['em.valueFormatter'], $c['extension']);
		};
		$container['em.valueFormatter'] = function (Container $c)
		{
			return new ValueFormatter();
		};

		$container['mailer'] = function (Container $c)
		{
			/** @var Options $options */
			$options = $c['options'];
			$config = $c['config'];

			$mailerClass = $this->extendClass(Mailer::class);

			/** @var Mailer $mailer */
			$mailer = new $mailerClass($c['mailer.templater'], $c['mailer.transport'], $c['mailer.styler'], $config['enableMailQueue']);

			$mailer->setDefaultFrom(
				$options->defaultEmailAddress,
				$options->emailSenderName ?: $options->boardTitle
			);
			$mailer->setDefaultReturnPath(
				$options->bounceEmailAddress ?: $options->defaultEmailAddress
			);
			$mailer->setDefaultUseVerp($options->enableVerp);

			$mailClass = $this->extendClass($mailer->getMailClass());
			$mailer->setMailClass($mailClass);

			$this->fire('mailer_setup', [$c, &$mailer]);

			return $mailer;
		};
		$container['mailer.transport'] = function (Container $c)
		{
			$config = $c['config'];
			if (!$config['enableMail'])
			{
				return new NullTransport();
			}

			$transport = null;
			$this->fire('mailer_transport_setup', [$c, &$transport]);
			if ($transport)
			{
				return $transport;
			}

			/** @var Options $options */
			$options = $c['options'];

			$mailerClass = $this->extendClass(Mailer::class);

			if (is_array($options->emailTransport))
			{
				$method = $options->emailTransport['emailTransport'];
				$config = $options->emailTransport;

				if (!empty($config['oauth']))
				{
					/** @var OptionRepository $optionRepo */
					$optionRepo = $this->repository(OptionRepository::class);
					$config = $optionRepo->refreshEmailAccessTokenIfNeeded('emailTransport');
				}
			}
			else
			{
				$method = 'sendmail';
				$config = [];
			}

			return $mailerClass::getTransportFromOption($method, $config);
		};
		$container['mailer.styler'] = function ($c)
		{
			$rendererClass = $this->extendClass(CssRenderer::class);
			$stylerClass = $this->extendClass(Styler::class);

			return new $stylerClass(
				new $rendererClass($this, $c['mailer.templater'], $this->cache('css'))
			);
		};
		$container['mailer.templater'] = function (Container $c)
		{
			if ($c['app.classType'] != 'Pub')
			{
				// preload this in bulk as we will load them individually below
				$this->registry()->get(['routesPublic', 'routeFilters', 'styles']);
			}

			$templater = $this->setupTemplaterObject($c, Mail\Templater::class);
			$templater->setStyle($c->create('style', $c['options']->defaultEmailStyleId));

			return $templater;
		};

		$container['fs'] = function (Container $c)
		{
			$mountsClass = $this->extendClass(FsMounts::class);

			return $mountsClass::loadDefaultMounts($c['config']);
		};

		$container['spam'] = function ($c)
		{
			$class = $this->extendClass(Spam::class);
			return new $class($c, $this);
		};

		$container['http'] = function ($c)
		{
			$class = $this->extendClass(SubContainer\Http::class);
			return new $class($c, $this);
		};

		$container['oAuth'] = function ($c)
		{
			$class = $this->extendClass(OAuth::class);
			return new $class($c, $this);
		};

		$container['proxy'] = function ($c)
		{
			$class = $this->extendClass(Proxy::class);
			return new $class($c, $this);
		};

		$container['oembed'] = function ($c)
		{
			$class = $this->extendClass(Oembed::class);
			return new $class($c, $this);
		};

		$container['bounce'] = function ($c)
		{
			$class = $this->extendClass(Bounce::class);
			return new $class($c, $this);
		};

		$container['unsubscribe'] = function ($c)
		{
			$class = $this->extendClass(Unsubscribe::class);
			return new $class($c, $this);
		};

		$container['widget'] = function ($c)
		{
			$class = $this->extendClass(Widget::class);
			return new $class($c, $this);
		};

		$container['import'] = function ($c)
		{
			$class = $this->extendClass(Import::class);
			return new $class($c, $this);
		};

		$container['webManifestRenderer'] = function ($c)
		{
			$class = $this->extendClass(WebManifestRenderer::class);
			return new $class($this);
		};

		$container->set('sitemap.builder', function (Container $c)
		{
			$class = Sitemap\Builder::class;
			$class = $this->extendClass($class);

			$user = $c['em']->getRepository(UserRepository::class)->getGuestUser();
			$types = $c['em']->getRepository(SitemapLogRepository::class)->getSitemapContentTypes();

			return new $class($this, $user, $types);
		}, false);

		$container->set('sitemap.renderer', function (Container $c)
		{
			$sitemapRepo = $this->repository(SitemapLogRepository::class);

			$class = $this->extendClass(Renderer::class);
			return new $class($this, $sitemapRepo->getActiveSitemap());
		}, false);

		$container['imageManager'] = function ($c)
		{
			$manager = new Image\Manager($c['imageManager.defaultDriver'], $c['imageManager.extraDrivers']);
			$manager->setMaxResizePixels($c['config']['maxImageResizePixelCount']);

			return $manager;
		};

		$container['imageManager.defaultDriver'] = function ($c)
		{
			return $c['options']->imageLibrary;
		};
		$container['imageManager.extraDrivers'] = [];

		$container['adminSearcher'] = function ($c)
		{
			$class = $this->extendClass(Searcher::class);

			return new $class(
				$this,
				$this->getContentTypeField('admin_search_class')
			);
		};

		$container['search'] = function ($c)
		{
			$class = $this->extendClass(Search::class);

			return new $class($c['search.source'], $this->getContentTypeField('search_handler_class'));
		};
		$container['search.source'] = function ($c)
		{
			$source = null;
			$this->fire('search_source_setup_22', [$c, &$source]);
			if ($source)
			{
				if (!($source instanceof AbstractSource))
				{
					throw new \LogicException('Search source must be instance of XF\Search\Source\AbstractSource. Received ' . get_class($source));
				}
				return $source;
			}

			$mySqlFtClass = $this->extendClass(MySqlFt::class);

			return new $mySqlFtClass($c['db'], $c['options']->searchMinWordLength);
		};

		$container->factory('stats.grouper', function ($grouping, array $params, Container $c)
		{
			$groupings = $c['stats.groupings'];
			if (!isset($groupings[$grouping]))
			{
				throw new \InvalidArgumentException("Unknown grouping '$grouping'");
			}

			$grouperClass = \XF::stringToClass($groupings[$grouping], '%s\Stats\Grouper\%s');
			$grouperClass = \XF::extendClass($grouperClass);

			$language = $params[0] ?? \XF::language();
			return new $grouperClass($language);
		});
		$container['stats.groupings'] = [
			'daily' => Daily::class,
			'weekly' => Weekly::class,
			'monthly' => Monthly::class,
		];

		$container->factory('language', function ($id, array $params, Container $c)
		{
			$id = intval($id);

			$cache = $c['language.cache'];
			if (!$id || !isset($cache[$id]))
			{
				$id = $c['options']->defaultLanguageId ?? null;
			}

			if (isset($cache[$id]))
			{
				$groupPath = File::getCodeCachePath() . '/phrase_groups';

				$class = $this->extendClass(Language::class);
				return new $class($id, $cache[$id], $c['db'], $groupPath);
			}
			else
			{
				return $c['language.fallback'];
			}
		});

		$container['language.fallback'] = function (Container $c)
		{
			$groupPath = File::getCodeCachePath() . '/phrase_groups';

			$class = $this->extendClass(Language::class);
			return new $class(0, [], $c['db'], $groupPath);
		};
		$container['language.cache'] = $this->fromRegistry(
			'languages',
			function (Container $c) { return $c['em']->getRepository(LanguageRepository::class)->rebuildLanguageCache(); }
		);
		$container['language.all'] = function (Container $c)
		{
			$output = [];
			foreach (array_keys($c['language.cache']) AS $languageId)
			{
				$output[$languageId] = $c->create('language', $languageId);
			}

			return $output;
		};

		$container['paymentProvider'] = $this->fromRegistry(
			'paymentProvider',
			function (Container $c) { return $c['em']->getRepository(PaymentRepository::class)->rebuildPaymentProviderCache(); }
		);

		$container->factory('style', function ($id, array $params, Container $c)
		{
			$id = intval($id);

			$cache = $c['style.cache'];
			if (!$id || !isset($cache[$id]))
			{
				$id = $c['options']->defaultStyleId;
			}

			if (isset($cache[$id]))
			{
				$style = $cache[$id];
				$masterStyleProperties = $c['style.masterStyleProperties'];
				if (is_array($style['properties']))
				{
					$style['properties'] += $masterStyleProperties;
				}
				else
				{
					$style['properties'] = $masterStyleProperties;
				}

				$class = $this->extendClass(Style::class);
				return new $class($id, $style);
			}
			else
			{
				return $c['style.fallback'];
			}
		});

		$container['style.fallback'] = function (Container $c)
		{
			$lastModified = $c['style.masterModifiedDate'];
			$masterStyleProperties = $c['style.masterStyleProperties'];
			$class = $this->extendClass(Style::class);
			return new $class(0, $masterStyleProperties, $lastModified);
		};
		$container['style.masterModifiedDate'] = $this->fromRegistry(
			'masterStyleModifiedDate',
			function (Container $c) { return \XF::$time; }
		);
		$container['style.masterStyleProperties'] = $this->fromRegistry(
			'masterStyleProperties',
			function (Container $c) { return []; }
		);
		$container['style.cache'] = $this->fromRegistry(
			'styles',
			function (Container $c) { return $c['em']->getRepository(StyleRepository::class)->rebuildStyleCache(); }
		);
		$container['style.all'] = function (Container $c)
		{
			$output = [];
			foreach (array_keys($c['style.cache']) AS $styleId)
			{
				$output[$styleId] = $c->create('style', $styleId);
			}

			return $output;
		};

		$container['defaultNavigationId'] = function (Container $c)
		{
			return '_default';
		};

		$container['uploadMaxFilesize'] = function (Container $c)
		{
			return Php::getUploadMaxFilesize();
		};

		// We do not recommend trying to extend this class or the tags/functions it defines. Doing so may interfere
		// with the upgrade process. If you must do so, it should not be part of an add-on. It should be done
		// unconditionally via config.php. However, do so at your own peril as the worst case would potentially be
		// an un-upgradeable installation.
		$container['templateCompiler'] = function (Container $c)
		{
			return new Compiler();
		};

		$container['templater'] = function (Container $c)
		{
			return $this->setupTemplaterObject($c, Templater::class);
		};
		// the default config is in the templater
		$container['templater.config.filters'] = [];
		$container['templater.config.functions'] = [];
		$container['templater.config.tests'] = [];

		$container['cssWriter'] = function ($c)
		{
			$rendererClass = $this->extendClass(CssRenderer::class);
			$renderer = new $rendererClass($this, $c['templater'], $this->cache('css'));

			$class = $this->extendClass(CssWriter::class);

			/** @var CssWriter $writer */
			$writer = new $class($this, $renderer);
			$writer->setValidator($c['css.validator']);

			return $writer;
		};
		$container['css.validator'] = $container->wrap(function (array $templates)
		{
			return hash_hmac('sha1', implode(',', $templates), $this->config('globalSalt'));
		});

		$container['iconRenderer'] = $container->factory(
			'iconRenderer',
			function (int $styleId, array $params, Container $c): IconRenderer
			{
				$class = $this->extendClass(IconRenderer::class);

				$style = $c->create('style', $styleId);
				array_unshift($params, $this, $style);

				return $c->createObject($class, $params);
			}
		);

		$container['addon.manager'] = function ($c)
		{
			$class = $this->extendClass(AddOn\Manager::class);
			return new $class(\XF::getAddOnDirectory());
		};
		$container['addon.dataManager'] = function ($c)
		{
			$class = $this->extendClass(DataManager::class);
			return new $class($c['em']);
		};
		$container['addon.cache'] = $this->fromRegistry(
			'addOns',
			function (Container $c) { return $c['addon.dataManager']->rebuildActiveAddOnCache(); }
		);
		$container['addon.composer'] = $this->fromRegistry(
			'addOnsComposer',
			function (Container $c)
			{
				$c['addon.dataManager']->rebuildActiveAddOnCache();
				return $this->container['registry']['addOnsComposer'];
			}
		);

		$container['giphy.api'] = function ($c)
		{
			$giphyOption = $c['options']['giphy'];
			if (!$giphyOption['enabled'] || !$giphyOption['api_key'])
			{
				return null;
			}

			$class = $this->extendClass(Api::class);

			return new $class($giphyOption['api_key'], [
				'rating' => $giphyOption['rating'],
				'lang' => substr(\XF::language()->getLanguageCode(), 0, 2), // note: invalid values ignored
				'random_id' => null, // TODO: system to fetch and maintain per-user random ID (xf_user_connected_account?) optional though
			]);
		};

		$container['bannedEmails'] = $this->fromRegistry(
			'bannedEmails',
			function (Container $c) { return $c['em']->getRepository(BanningRepository::class)->rebuildBannedEmailCache(); }
		);

		$container['bannedIps'] = $this->fromRegistry(
			'bannedIps',
			function (Container $c) { return $c['em']->getRepository(BanningRepository::class)->rebuildBannedIpCache(); }
		);

		$container['discouragedIps'] = $this->fromRegistry(
			'discouragedIps',
			function (Container $c) { return $c['em']->getRepository(BanningRepository::class)->rebuildDiscouragedIpCache(); }
		);

		$container['forumTypes'] = $this->fromRegistry(
			'forumTypes',
			function (Container $c) { return $c['em']->getRepository(ForumTypeRepository::class)->rebuildForumTypeCache(); }
		);

		$container->factory('forumType', function ($type, array $params, Container $c)
		{
			$forumTypes = $c['forumTypes'];
			if (!isset($forumTypes[$type]))
			{
				return null;
			}

			$typeClass = \XF::stringToClass($forumTypes[$type], '%s\ForumType\%s');
			$typeClass = \XF::extendClass($typeClass);

			return new $typeClass($type);
		});

		$container['threadTypes'] = $this->fromRegistry(
			'threadTypes',
			function (Container $c) { return $c['em']->getRepository(ThreadTypeRepository::class)->rebuildThreadTypeCache(); }
		);

		$container->factory('threadType', function ($type, array $params, Container $c)
		{
			$threadTypes = $c['threadTypes'];
			if (!isset($threadTypes[$type]))
			{
				return null;
			}

			$typeClass = \XF::stringToClass($threadTypes[$type], '%s\ThreadType\%s');
			$typeClass = \XF::extendClass($typeClass);

			return new $typeClass($type);
		});

		$container['string.formatter'] = function (Container $c)
		{
			$class = $this->extendClass(Formatter::class);
			/** @var Formatter $formatter */
			$formatter = new $class();

			$options = $c['options'];
			$formatter->setCensorRules($options->censorWords ?: [], $options->censorCharacter);
			$formatter->addSmilies($c['smilies']);
			$formatter->setSmilieHtmlPather($c['request.pather']);
			$formatter->setProxyHandler(function ($type, $url, array $options = [])
			{
				return $this->proxy()->generateExtended($type, $url, $options);
			});

			$this->fire('string_formatter_setup', [$c, &$formatter]);

			return $formatter;
		};

		$container['bbCode'] = function ($c)
		{
			$class = $this->extendClass(BbCode::class);
			return new $class($c, $this);
		};

		$container['apiDocs'] = function ($c)
		{
			$class = $this->extendClass(ApiDocs::class);
			return new $class($c, $this);
		};

		$container['logSearcher'] = function ($c)
		{
			/** @var LogSearch\Searcher $class */
			$class = $this->extendClass(LogSearch\Searcher::class);

			return new $class(
				$this,
				$this->getContentTypeField('log_search_class')
			);
		};

		$container['job.manager'] = function ($c)
		{
			$class = $this->extendClass(Job\Manager::class);
			return new $class($this, $c['job.manual.allow'], $c['job.manual.force']);
		};
		$container['job.runTime'] = $this->fromRegistry(
			'autoJobRun',
			function (Container $c) { return $c['job.manager']->updateNextRunTime(); }
		);
		$container['job.manual.allow'] = false;
		$container['job.manual.force'] = false;

		$container['development.output'] = function (Container $c)
		{
			$config = $c['config'];

			$skip = $config['development']['skipAddOns'];
			if (!is_array($skip))
			{
				$skip = ['XF', 'XF*'];
			}

			$class = $this->extendClass(DevelopmentOutput::class);
			return new $class(
				$config['development']['enabled'],
				\XF::getAddOnDirectory(),
				$skip
			);
		};

		$container['development.jsResponse'] = function (Container $c)
		{
			$class = $this->extendClass(DevJsResponse::class);
			return new $class($this);
		};

		$container['designer.output'] = function (Container $c)
		{
			$config = $c['config'];

			$class = $this->extendClass(DesignerOutput::class);
			return new $class(
				$config['designer']['enabled'],
				$config['designer']['basePath']
			);
		};

		$container['extension'] = function (Container $c)
		{
			$config = $c['config'];
			if (!$config['enableListeners'])
			{
				// disable
				return new Extension();
			}

			try
			{
				$listeners = $c['extension.listeners'];
				$classExtensions = $c['extension.classExtensions'];
			}
			catch (Db\Exception $e)
			{
				$listeners = [];
				$classExtensions = [];
			}

			return new Extension($listeners, $classExtensions);
		};
		// note: these don't trigger normal rebuilds to prevent the possibility of an infinite loop
		$container['extension.listeners'] = $this->fromRegistry(
			'codeEventListeners',
			function (Container $c)
			{
				$c['registry']->set('codeEventListeners', []);
				return [];
			}
		);
		$container['extension.classExtensions'] = $this->fromRegistry(
			'classExtensions',
			function (Container $c)
			{
				$c['registry']->set('classExtensions', []);
				return [];
			}
		);

		$container->factory('controller', function ($class, array $params, Container $c)
		{
			$class = \XF::stringToClass($class, '%s\%s\Controller\%s', $c['app.classType']);
			$class = $this->extendClass($class);

			$passParams = [
				$this,
				$params['request'] ?? $c['request'],
			];
			return $c->createObject($class, $passParams, true);
		}, false);

		$container->factory('renderer', function ($type, array $params, Container $c)
		{
			$type = strtolower($type);
			switch ($type)
			{
				case 'html': $class = 'Html'; break;
				case 'json': $class = 'Json'; break;
				case 'xml': $class = 'Xml'; break;
				case 'raw': $class = 'Raw'; break;
				case 'rss': $class = 'Rss'; break;
				default:
					$unknownCallback = $c['renderer.unknown'];
					$class = $unknownCallback($type);
			}

			$params = [
				$c->getInvokableFactory('view'),
				$c['response'],
				$c['templater'],
			];
			if (strpos($class, '\\') === false)
			{
				$class = 'XF\Mvc\Renderer\\' . $class;
			}
			$class = $this->extendClass($class);

			return $c->createObject($class, $params);
		}, false);

		$container['renderer.unknown'] = function ()
		{
			return function ($rendererType)
			{
				return 'Html';
			};
		};

		$container['view.defaultClass'] = View::class;

		$container->factory('view', function ($class, array $params, Container $c): View
		{
			try
			{
				$class = \XF::stringToClass($class, '%s\%s\View\%s', $c['app.classType']);
			}
			catch (\InvalidArgumentException $e)
			{
				$this->logException($e);
			}

			$class = $this->extendClass($class, $c['view.defaultClass']);

			if (!$class || !class_exists($class))
			{
				$class = $c['view.defaultClass'];
			}

			return $c->createObject($class, $params);
		}, false);

		$container->factory('job', function ($class, array $params, Container $c)
		{
			$class = \XF::stringToClass($class, '\%s\Job\%s');
			$class = $this->extendClass($class);

			array_unshift($params, $this);

			return $c->createObject($class, $params, true);
		}, false);

		$container->factory('searcher', function ($class, array $params, Container $c)
		{
			$class = \XF::stringToClass($class, '\%s\Searcher\%s');
			$class = $this->extendClass($class);

			array_unshift($params, $c['em']);

			return $c->createObject($class, $params);
		}, false);

		$container->factory('filterer', function ($class, array $params, Container $c)
		{
			$class = \XF::stringToClass($class, '\%s\Filterer\%s');
			$class = $this->extendClass($class);

			return $c->createObject($class, $params);
		}, false);

		$container->factory('auth', function ($class, array $params, Container $c)
		{
			$class = \XF::stringToClass($class, '\%s\Authentication\%s');
			$class = $this->extendClass($class);

			return $c->createObject($class, $params);
		}, false);

		$container['auth.default'] = 'XF:Core12';

		$container->factory('service', function ($class, array $params, Container $c)
		{
			$class = \XF::stringToClass($class, '\%s\Service\%s');
			$class = $this->extendClass($class);

			array_unshift($params, $this);

			return $c->createObject($class, $params);
		}, false);

		$container->factory('helper', function ($class, array $params, Container $c)
		{
			$class = \XF::stringToClass($class, '\%s\Helper\%s');
			$class = $this->extendClass($class);

			return $c->createObject($class, $params);
		}, false);

		$container->factory('validator', function ($class, array $params, Container $c)
		{
			if (strpos($class, ':') === false && strpos($class, '\\') === false)
			{
				$class = "XF:$class";
			}

			$class = \XF::stringToClass($class, '\%s\Validator\%s');
			$class = $this->extendClass($class);

			array_unshift($params, $this);

			return $c->createObject($class, $params);
		}, false);

		$container->factory('data', function ($class, array $params, Container $c)
		{
			$class = \XF::stringToClass($class, '\%s\Data\%s');
			$class = $this->extendClass($class);

			array_unshift($params, $this);

			return $c->createObject($class, $params);
		}, true);

		$container->factory('captcha', function ($class, array $params, Container $c)
		{
			if (strpos($class, ':') === false && strpos($class, '\\') === false)
			{
				$class = "XF:$class";
			}
			$class = \XF::stringToClass($class, '\%s\Captcha\%s');
			if (!class_exists($class))
			{
				$this->error()->logError('CAPTCHA class ' . htmlspecialchars($class) . ' does not exist. Falling back to default provider \\' . htmlspecialchars($c['captcha.default']) . '.');

				$class = $c['captcha.default'];
			}
			$class = $this->extendClass($class);

			array_unshift($params, $this);

			return $c->createObject($class, $params);
		}, true);

		$container['captcha.default'] = HCaptcha::class;

		$container->factory('notifier', function ($class, array $params, Container $c)
		{
			$class = \XF::stringToClass($class, '\%s\Notifier\%s');
			$class = $this->extendClass($class);

			array_unshift($params, $this);

			return $c->createObject($class, $params);
		}, false);

		if (function_exists('xdebug_disable'))
		{
			xdebug_disable(); // use PHP's own stack trace in case of errors
		}

		$this->initializeExtra();
	}

	protected function setupCookieConsent(CookieConsent $cookieConsent)
	{
		$cookies = [
			'dbWriteForced' => CookieConsent::GROUP_REQUIRED,
			'consent' => CookieConsent::GROUP_REQUIRED,
			'csrf' => CookieConsent::GROUP_REQUIRED,
			'inline_mod_*' => CookieConsent::GROUP_REQUIRED,
			'language_id' => CookieConsent::GROUP_REQUIRED,
			'ls' => CookieConsent::GROUP_REQUIRED,
			'notice_dismiss' => CookieConsent::GROUP_REQUIRED,
			'push_notice_dismiss' => CookieConsent::GROUP_REQUIRED,
			'push_subscription_updated' => CookieConsent::GROUP_REQUIRED,
			'session' => CookieConsent::GROUP_REQUIRED,
			'style_id' => CookieConsent::GROUP_REQUIRED,
			'toggle' => CookieConsent::GROUP_REQUIRED,
			'tfa_trust' => CookieConsent::GROUP_REQUIRED,
			'user' => CookieConsent::GROUP_REQUIRED,

			'from_search' => CookieConsent::GROUP_OPTIONAL,
			'emoji_usage' => CookieConsent::GROUP_OPTIONAL,
		];
		$cookieConsent->addCookies($cookies);

		$localStorages = [
			'__crossTab' => CookieConsent::GROUP_REQUIRED,
			'cacheKey' => CookieConsent::GROUP_REQUIRED,
			'editorDisabled' => CookieConsent::GROUP_REQUIRED,
			'fr-copied-html' => [
				'group' => CookieConsent::GROUP_REQUIRED,
				'prefix' => false,
			],
			'fr-copied-text' => [
				'group' => CookieConsent::GROUP_REQUIRED,
				'prefix' => false,
			],
			'guestUsername' => CookieConsent::GROUP_REQUIRED,
			'lbSidebarDisabled' => CookieConsent::GROUP_REQUIRED,
			'multiQuote*' => CookieConsent::GROUP_REQUIRED,
			'push_history_user_ids' => CookieConsent::GROUP_REQUIRED,
			'visitorCounts' => CookieConsent::GROUP_REQUIRED,
		];
		foreach ($localStorages AS $localStorage => $config)
		{
			if (is_string($config))
			{
				$config = ['group' => $config];
			}

			$config = array_merge(['localStorage' => true], $config);

			$localStorages[$localStorage] = $config;
		}

		$cookieConsent->addCookies($localStorages);

		$thirdParties = [];
		if ($this->options()->googleAnalyticsWebPropertyId)
		{
			$thirdParties[] = 'google_analytics';
		}

		$captcha = $this->captcha();
		if ($captcha)
		{
			$captchaThirdParties = $captcha->getCookieThirdParties();
			if ($captchaThirdParties)
			{
				$thirdParties = array_merge($thirdParties, $captchaThirdParties);
			}
		}

		$bbCodeMediaSites = $this->bbCode()->get('media');
		foreach ($bbCodeMediaSites AS $bbCodeMediaSite)
		{
			$bbCodeThirdParties = array_filter(preg_split(
				'/\r?\n/',
				$bbCodeMediaSite['cookie_third_parties'] ?? ''
			));
			if (!$bbCodeThirdParties)
			{
				continue;
			}

			$thirdParties = array_merge($thirdParties, $bbCodeThirdParties);
		}

		$paymentProviders = $this->get('paymentProvider');
		$paymentRepo = $this->repository(PaymentRepository::class);
		foreach ($paymentProviders AS $paymentProvider)
		{
			$paymentHandler = $paymentRepo->getPaymentProviderHandler(
				$paymentProvider['provider_id'],
				$paymentProvider['provider_class']
			);
			if (!$paymentHandler)
			{
				continue;
			}
			$paymentThirdParties = $paymentHandler->getCookieThirdParties();
			if (!$paymentThirdParties)
			{
				continue;
			}

			$thirdParties = array_merge($thirdParties, $paymentThirdParties);
		}

		$cookieConsent->addThirdParties($thirdParties);

		$consentedGroups = @json_decode(
			$this->request()->getCookie('consent', '[]'),
			true
		);
		if (!is_array($consentedGroups))
		{
			$consentedGroups = [];
		}
		$cookieConsent->addConsentedGroups($consentedGroups);

		$this->fire('cookie_consent_setup', [$cookieConsent]);
	}

	/**
	 * @param               $key
	 * @param \Closure      $rebuildFunction
	 * @param \Closure|null $decoratorFunction
	 *
	 * @return \Closure
	 * @throws Db\Exception
	 */
	public function fromRegistry($key, \Closure $rebuildFunction, ?\Closure $decoratorFunction = null)
	{
		return function (Container $c) use ($key, $rebuildFunction, $decoratorFunction)
		{
			$data = $this->container['registry'][$key];

			if ($data === null)
			{
				$data = $rebuildFunction($c, $key);
			}

			return $decoratorFunction ? $decoratorFunction($data, $c, $key) : $data;
		};
	}

	/**
	 * @param Container $c
	 * @param string    $class
	 *
	 * @return Templater
	 * @throws \Exception
	 */
	public function setupTemplaterObject(Container $c, $class)
	{
		$config = $c['config'];

		$class = $this->extendClass($class);

		/** @var Templater $templater */
		$templater = new $class(
			$this,
			\XF::language(),
			File::getCodeCachePath() . '/templates'
		);
		$templater->addDefaultHandlers();
		$templater->addFilters($c['templater.config.filters']);
		$templater->addFunctions($c['templater.config.functions']);
		$templater->addTests($c['templater.config.tests']);
		if ($config['development']['enabled'])
		{
			$templater->addTemplateWatcher($c['development.output']->getHandler('XF:Template'));
		}
		if ($config['designer']['enabled'])
		{
			$templater->addTemplateWatcher($c['designer.output']->getHandler('XF:Template'));
		}

		$templater->setCssValidator($c['css.validator']);

		$options = $c['options'];

		$templater->setJsVersion($c['jsVersion']);
		$templater->setJsBaseUrl($config['javaScriptUrl']);

		$templater->setDynamicDefaultAvatars($options->dynamicAvatarEnable);

		$templater->setMediaSites($c['bbCode.media']);

		$templater->setUserTitleLadder($c['userTitleLadder'], $options->userTitleLadderField);
		$templater->setUserBanners($c['userBanners'], $options->userBanners ?: []);
		$templater->setGroupStyles($c['displayStyles']);

		$templater->setWidgetPositions($c['widget.widgetPosition'] ?: []);

		$this->fire('templater_setup', [$c, &$templater]);

		return $templater;
	}

	public function initializeExtra()
	{
	}

	public function setup(array $options = [])
	{
		$config = $this->container('config');

		if (!$config['exists'])
		{
			if ($config['legacyExists'])
			{
				echo 'The site is currently being upgraded. Please check back later.';
				exit;
			}
			else if (File::installLockExists())
			{
				echo "Couldn't load src/config.php file.";
				exit;
			}
			else
			{
				header('Location: install/index.php');
				exit;
			}
		}

		$this->checkDebugMode();
		$this->checkDbWriteForced();

		$preLoadType = $options['preLoad'] ?? [];
		$this->preLoadData($preLoadType);

		$this->setupAddOnComposerAutoload();

		$preLoadExtraType = $options['preLoadExtra'] ?? [];
		$this->preLoadExtraData($preLoadExtraType);

		$this->fire('app_setup', [$this]);
	}

	protected function preLoadData(array $typeSpecific = [])
	{
		try
		{
			$keys = array_merge($this->preLoadShared, $this->preLoadLocal, $typeSpecific);
			$this->registry()->get($keys);
		}
		catch (\Exception $e)
		{
		}
	}

	/**
	 * @param list<string> $typeSpecific
	 */
	protected function preloadExtraData(array $typeSpecific = []): void
	{
		try
		{
			$keys = array_merge($this->getPreloadExtraKeys(), $typeSpecific);
			$this->registry()->get($keys);
		}
		catch (\Exception $e)
		{
		}
	}

	/**
	 * @return list<string>
	 */
	protected function getPreloadExtraKeys(): array
	{
		$keys = [];

		$this->fire('app_registry_preload', [$this, &$keys]);

		return $keys;
	}

	public function start($allowShortCircuit = false)
	{
		return null;
	}

	protected function getVisitorFromSession(Session $session, array $extraWith = [])
	{
		$userRepo = $this->repository(UserRepository::class);
		$sessionUserId = $session->userId;
		$user = $userRepo->getVisitor($sessionUserId, $extraWith);

		if ($user->user_id && $user->user_id == $sessionUserId)
		{
			$userPasswordDate = $user->Profile ? $user->Profile->password_date : 0;
			if ($session->passwordDate != $userPasswordDate)
			{
				$session->logoutUser();
				$user = $userRepo->getVisitor(0);
			}
		}

		return $user;
	}

	public function preDispatch(RouteMatch $match)
	{

	}

	public function postDispatch(AbstractReply $reply, RouteMatch $finalMatch, RouteMatch $originalMatch)
	{

	}

	public function preRender(AbstractReply $reply, $responseType)
	{
		$this->templater()->addDefaultParam('xf', $this->getGlobalTemplateData($reply));
	}

	public function getCustomFields($type, $group = null, ?array $onlyInclude = null, array $additionalFilters = [])
	{
		/** @var DefinitionSet $definitionSet */
		$definitionSet = $this->container["customFields.$type"];

		if (!$definitionSet)
		{
			return null;
		}

		if ($group !== null)
		{
			$definitionSet = $definitionSet->filterGroup($group);
		}

		if (is_array($onlyInclude))
		{
			$definitionSet = $definitionSet->filterOnly($onlyInclude);
		}

		if ($additionalFilters)
		{
			$definitionSet = $definitionSet->filter($additionalFilters);
		}

		return $definitionSet;
	}

	public function getCustomFieldsForEdit(
		$type,
		Set $set,
		$editMode = 'user',
		$group = null,
		?array $onlyInclude = null,
		array $additionalFilters = []
	)
	{
		$definitionSet = $this->getCustomFields($type, $group, $onlyInclude, $additionalFilters);
		if (!$definitionSet)
		{
			return null;
		}

		return $definitionSet->filterEditable($set, $editMode);
	}

	public function getGlobalTemplateData(?AbstractReply $reply = null)
	{
		$request = $this->request();

		$jobRunTime = null;
		if (!empty($this->options()->jobRunTrigger) && $this->options()->jobRunTrigger == 'activity')
		{
			// if activity based it is triggered by inclusion in the template - run time compared below
			$jobRunTime = $this['job.runTime'];
		}

		$language = \XF::language();
		$config = $this->config();

		$cookieConfig = $config['cookie'];
		$cookieConfig['secure'] = $request->isSecure() ? true : false;

		$searchAutoComplete = $this->search()->isAutoCompleteSupported()
			&& $this->options()->searchSuggestions['enabled'] ?? false;

		$data = [
			'versionVisible' => preg_replace('/^(\d+)\.(\d+)\..+$/', '$1.$2', \XF::$version),
			'versionId' => \XF::$versionId,
			'version' => \XF::$version,
			'app' => $this,
			'appType' => $this['app.defaultType'],
			'request' => $request,
			'uri' => $request->getRequestUri(),
			'fullUri' => $request->getFullRequestUri(),
			'time' => \XF::$time,
			'timeDetails' => $language->getDayStartTimestamps(),
			'debug' => \XF::$debugMode,
			'development' => \XF::$developmentMode,
			'designer' => $config['designer']['enabled'],
			'visitor' => \XF::visitor(),
			'session' => $this->session(),
			'cookie' => $cookieConfig,
			'cookieConsent' => $this->cookieConsent(),
			'enableRtnProtect' => $config['enableReverseTabnabbingProtection'],
			'serviceWorkerPath' => $config['serviceWorkerPath'],
			'language' => $language,
			'style' => $this->templater()->getStyle(),
			'isRtl' => $language->isRtl(),
			'options' => $this->options(),
			'reactions' => $this->get('reactions'),
			'reactionsActive' => array_filter($this->get('reactions'), function (array $reaction)
			{
				return ($reaction['active'] === true);
			}),
			'addOns' => $this->container['addon.cache'],
			'runJobs' => ($jobRunTime && $jobRunTime <= \XF::$time),
			'simpleCache' => $this->simpleCache(),
			'livePayments' => $config['enableLivePayments'],
			'fullJs' => $config['development']['fullJs'],
			'contactUrl' => $this->container['contactUrl'],
			'privacyPolicyUrl' => $this->container['privacyPolicyUrl'],
			'tosUrl' => $this->container['tosUrl'],
			'homePageUrl' => $this->container['homePageUrl'],
			'helpPageCount' => $this->container['helpPageCount'],
			'uploadMaxFilesize' => $this->container['uploadMaxFilesize'],
			'allowedVideoExtensions' => array_keys($this->container['inlineVideoTypes']),
			'allowedAudioExtensions' => array_keys($this->container['inlineAudioTypes']),
			'searchAutoComplete' => $searchAutoComplete,
		];

		if ($reply)
		{
			$replyData = [
				'controller' => $reply->getControllerClass(),
				'action' => $reply->getAction(),
				'section' => $reply->getSectionContext(),
				'containerKey' => $reply->getContainerKey(),
				'contentKey' => $reply->getContentKey(),
			];

			if ($reply instanceof Mvc\Reply\View)
			{
				$replyData['view'] = $reply->getViewClass();
				$replyData['template'] = $reply->getTemplateName();
			}
			else if ($reply instanceof Mvc\Reply\Error || $reply->getResponseCode() >= 400)
			{
				$replyData['template'] = 'error';
			}
			else if ($reply instanceof Message)
			{
				$replyData['template'] = 'message_page';
			}

			$data['reply'] = $replyData;
		}

		$this->fire('templater_global_data', [$this, &$data, $reply]);

		return $data;
	}

	protected $updateCsrfCookie = false;

	public function updateCsrfCookie($newValue)
	{
		$this->updateCsrfCookie = $newValue;
	}

	public function complete(Response $response)
	{
		if (!$response->headerExists('Expires'))
		{
			$response->header('Expires', 'Thu, 19 Nov 1981 08:52:00 GMT');
		}
		if (!$response->headerExists('Cache-control'))
		{
			$response->header('Cache-control', 'private, no-cache, max-age=0');
		}

		if ($this->updateCsrfCookie)
		{
			$response->setCookie('csrf', $this->updateCsrfCookie, 0, null, false);
		}

		if ($this->container->isCached('db'))
		{
			$db = $this->db();
			if ($db instanceof ReplicationAdapterInterface && $db->isForcedToWriteServerExplicit())
			{
				$forceTime = $db->getForceToWriteServerLength();
				if ($forceTime > 0)
				{
					$response->setCookie('dbWriteForced', time(), $forceTime * 2);
				}
			}
		}

		$this->fire('app_complete', [$this, &$response]);
	}

	public function finalOutputFilter(Response $response)
	{
		if (\XF::$debugMode && $this->request()->get('_debug'))
		{
			$response->contentType('text/html', 'utf-8');
			$response->body($this->debugger()->getDebugPageHtml());
		}

		$this->fire('app_final_output', [$this, &$response]);

		return $response;
	}

	public function getErrorRoute($action, array $params = [], $responseType = 'html')
	{
		return $this->router()->getNewRouteMatch('XF:Error', $action, $params, $responseType);
	}

	public function renderPage($content, AbstractReply $reply, AbstractRenderer $renderer)
	{
		if ($reply instanceof Redirect)
		{
			return $content;
		}

		if ($renderer instanceof Html)
		{
			$content = strval($content);
			$pageParams = $renderer->getTemplater()->pageParams;
			return $this->renderPageHtml($content, $pageParams, $reply, $renderer);
		}
		else
		{
			return $content;
		}
	}

	protected function renderPageHtml($content, array $params, AbstractReply $reply, AbstractRenderer $renderer)
	{
		return $content;
	}

	/**
	 * @param string $hash
	 *
	 * Generates the hash used for redirects, such as foo/bar/#redirect-anchor
	 *
	 * @return string
	 */
	public function getRedirectHash($hash)
	{
		return '__' . $hash;
	}

	/**
	 * @param string|null $fallbackUrl
	 * @param bool $useReferrer
	 *
	 * @return string
	 */
	public function getDynamicRedirect($fallbackUrl = null, $useReferrer = true)
	{
		if ($fallbackUrl === null)
		{
			$fallbackUrl = $this->router()->buildLink('index');
		}

		$request = $this->request();
		$fallbackUrl = $request->convertToAbsoluteUri($fallbackUrl);

		$redirect = $request->filter('_xfRedirect', 'str');
		if (!$redirect && $useReferrer)
		{
			$redirect = $request->getServer('HTTP_X_AJAX_REFERER')
				?: $request->getReferrer();
		}

		if (!$redirect || !preg_match('/./su', $redirect))
		{
			// no redirect provided
			return $fallbackUrl;
		}

		if (
			strpos($redirect, "\n") !== false ||
			strpos($redirect, "\r") !== false ||
			strpos($redirect, '@') !== false
		)
		{
			// redirect contained newlines or user/pass
			return $fallbackUrl;
		}

		$fullRedirect = $request->convertToAbsoluteUri($redirect);
		$redirectParts = @parse_url($fullRedirect);
		$redirectHost = $redirectParts['host'] ?? null;
		if (!$redirectHost)
		{
			// no redirect host
			return $fallbackUrl;
		}

		$requestParts = @parse_url($request->getFullBasePath());
		$requestHost = $requestParts['host'] ?? null;
		if ($redirectHost !== $requestHost)
		{
			// redirect host did not match request host
			return $fallbackUrl;
		}

		return $fullRedirect;
	}

	/**
	 * @param string $notUrl
	 * @param string|null $fallbackUrl
	 * @param bool $useReferrer
	 *
	 * @return string
	 */
	public function getDynamicRedirectIfNot(
		$notUrl,
		$fallbackUrl = null,
		$useReferrer = true
	)
	{
		if ($fallbackUrl === false)
		{
			$fallbackUrl = $this->router()->buildLink('index');
		}

		$request = $this->request();
		$fallbackUrl = $request->convertToAbsoluteUri($fallbackUrl);

		$redirect = $this->getDynamicRedirect($fallbackUrl, $useReferrer);
		$notUrl = $request->convertToAbsoluteUri($notUrl);

		if (strpos($redirect, $notUrl) === 0)
		{
			// the URL we can't redirect to is at the start
			return $fallbackUrl;
		}

		return $redirect;
	}

	public function applyExternalDataUrl($externalPath, $canonical = false)
	{
		$pathType = ($canonical ? 'canonical' : 'root-base');
		return $this->applyExternalDataUrlPathed($externalPath, $pathType);
	}

	public function applyExternalDataUrlPathed($externalPath, $pathType)
	{
		$externalDataUrl = $this->config('externalDataUrl');
		if ($externalDataUrl instanceof \Closure)
		{
			$url = $externalDataUrl($externalPath, $pathType);
		}
		else
		{
			$url = "$externalDataUrl/$externalPath";
		}

		/** @var \Closure $pather */
		$pather = $this->container('request.pather');

		return $pather($url, $pathType);
	}

	public function applyLocalDataUrl(string $localPath, bool $canonical = false): string
	{
		$pathType = ($canonical ? 'canonical' : 'root-base');
		return $this->applyLocalDataUrlPathed($localPath, $pathType);
	}

	public function applyLocalDataUrlPathed(string $localPath, string $pathType): string
	{
		$externalDataUrl = $this->config('localDataUrl');
		if ($externalDataUrl instanceof \Closure)
		{
			$url = $externalDataUrl($localPath, $pathType);
		}
		else
		{
			$url = "$externalDataUrl/$localPath";
		}

		/** @var \Closure $pather */
		$pather = $this->container('request.pather');
		return $pather($url, $pathType);
	}

	public function assertConfigExists()
	{
		if (!$this->container['config']['exists'])
		{
			echo 'Config.php does not exist.';
			exit;
		}
	}

	public function checkDebugMode()
	{
		$config = $this->container['config'];
		if ($config['development']['enabled'])
		{
			$config['debug'] = true;
			\XF::$developmentMode = true;
		}

		if ($config['debug'])
		{
			\XF::$debugMode = true;
			@ini_set('display_errors', true);
		}
	}

	public function checkDbWriteForced()
	{
		// reading this cookie directly as we do this before the DB is initialized and therefore
		// we haven't actually fetched things from the registry
		$cookieName = $this->container['config']['cookie']['prefix'] . 'dbWriteForced';
		if (isset($_COOKIE[$cookieName]) && is_scalar($_COOKIE[$cookieName]))
		{
			$writeForced = intval($_COOKIE[$cookieName]);
		}
		else
		{
			$writeForced = 0;
		}

		if ($writeForced)
		{
			$db = $this->db();
			if ($db instanceof ReplicationAdapterInterface)
			{
				$forceTime = $db->getForceToWriteServerLength();
				if ($forceTime > 0 && $writeForced + $forceTime >= time())
				{
					$db->forceToWriteServer('implicit');
				}
			}
		}
	}

	public function setupAddOnComposerAutoload()
	{
		if (!$this->config('enableListeners'))
		{
			return;
		}

		/** @var AddOn\Manager $addOnManager */
		$addOnManager = $this->container['addon.manager'];
		$addOns = $this->container['addon.composer'];

		foreach ($addOns AS $addOnId => $composerData)
		{
			if ($addOnId == 'XF' || !$composerData)
			{
				continue;
			}

			$addOnPath = $addOnManager->getAddOnPath($addOnId);
			if (is_array($composerData))
			{
				\XF::registerComposerAutoloadData($addOnPath . \XF::$DS . $composerData['autoload_path'], $composerData);
			}
			else
			{
				\XF::registerComposerAutoloadDir($addOnPath . \XF::$DS . $composerData);
			}
		}
	}

	public function run()
	{
		$response = $this->start(true);
		if (!($response instanceof Response))
		{
			$dispatcher = $this->dispatcher();
			$response = $dispatcher->run();
		}

		$this->complete($response);
		$response = $this->finalOutputFilter($response);

		return $response;
	}

	public function logException($e, $rollback = false, $messagePrefix = '')
	{
		$this->error()->logException($e, $rollback, $messagePrefix);
	}

	public function displayFatalExceptionMessage($e)
	{
		$this->error()->displayFatalExceptionMessage($e);
	}

	public function get($key)
	{
		return $this->container->offsetGet($key);
	}

	public function create($type, $key, array $params = [])
	{
		return $this->container->create($type, $key, $params);
	}

	/**
	 * @param mixed $key
	 *
	 * @return mixed
	 */
	#[\ReturnTypeWillChange]
	public function offsetGet($key)
	{
		return $this->container->offsetGet($key);
	}

	/**
	 * @param mixed $key
	 * @param mixed $value
	 */
	public function offsetSet($key, $value): void
	{
		$this->container->offsetSet($key, $value);
	}

	/**
	 * @param mixed $key
	 *
	 * @return bool
	 */
	public function offsetExists($key): bool
	{
		return $this->container->offsetExists($key);
	}

	/**
	 * @param mixed $key
	 */
	public function offsetUnset($key): void
	{
		$this->container->offsetUnset($key);
	}

	/**
	 * @param mixed $key
	 *
	 * @return mixed
	 */
	public function __get($key)
	{
		return $this->container->offsetGet($key);
	}

	/**
	 * @param mixed $key
	 * @param mixed $value
	 */
	public function __set($key, $value)
	{
		$this->container->offsetSet($key, $value);
	}

	/**
	 * @param string|null
	 *
	 * @return mixed
	 */
	public function config($key = null)
	{
		$config = $this->container['config'];

		if ($key)
		{
			return $config[$key] ?? null;
		}
		else
		{
			return $config;
		}
	}

	/**
	 * @return Request
	 */
	public function request()
	{
		return $this->container['request'];
	}

	/**
	 * @return InputFilterer
	 */
	public function inputFilterer()
	{
		return $this->container['inputFilterer'];
	}

	public function cookieConsent(): CookieConsent
	{
		return $this->container['cookieConsent'];
	}

	/**
	 * @return Response
	 */
	public function response()
	{
		return $this->container['response'];
	}

	/**
	 * @return Dispatcher
	 */
	public function dispatcher()
	{
		return $this->container['dispatcher'];
	}

	/**
	 * @param string|null $type
	 *
	 * @return Router
	 */
	public function router($type = null)
	{
		return $type ? $this->container['router.' . $type] : $this->container['router'];
	}

	/**
	 * @return AbstractAdapter
	 */
	public function db()
	{
		return $this->container['db'];
	}

	/**
	 * @param string $context Context of cache to load from. Empty represents the global cache.
	 * @param bool $fallbackToGlobal If true and no specific cache is available, fallback to the global cache
	 * @param bool $doctrineCompatible If false, return a new Symfony Cache adapter instead of the Doctrine Cache shim.
	 *
	 * @return CacheProvider|\Symfony\Component\Cache\Adapter\AbstractAdapter|null
	 */
	public function cache(
		$context = '',
		$fallbackToGlobal = true,
		bool $doctrineCompatible = true
	)
	{
		$cache = $doctrineCompatible
			? $this->container->create('cache', $context)
			: $this->container->create('cache.symfony', $context);
		if (!$cache && $fallbackToGlobal && strlen($context))
		{
			$cache = $doctrineCompatible
				? $this->container->create('cache', '')
				: $this->container->create('cache.symfony', '');
		}

		return $cache;
	}

	/**
	 * @return PermissionCache
	 */
	public function permissionCache()
	{
		return $this->container['permission.cache'];
	}

	/**
	 * @return Builder
	 */
	public function permissionBuilder()
	{
		return $this->container['permission.builder'];
	}

	/**
	 * @return DataRegistry
	 */
	public function registry()
	{
		return $this->container['registry'];
	}

	/**
	 * @return SimpleCache
	 */
	public function simpleCache()
	{
		return $this->container['simpleCache'];
	}

	/**
	 * @return Options
	 */
	public function options()
	{
		return $this->container['options'];
	}

	/**
	 * @return Mailer
	 */
	public function mailer()
	{
		return $this->container['mailer'];
	}

	/**
	 * @return Mail\Templater
	 */
	public function mailTemplater()
	{
		return $this->container['mailer.templater'];
	}

	/**
	 * @return MountManager
	 */
	public function fs()
	{
		return $this->container['fs'];
	}

	public function getContentTypeField($field)
	{
		$output = [];
		foreach ($this->container['contentTypes'] AS $type => $fields)
		{
			if (isset($fields[$field]))
			{
				$output[$type] = $fields[$field];
			}
		}

		return $output;
	}

	public function getFieldsForContentType(string $type): array
	{
		return $this->container['contentTypes'][$type] ?? [];
	}

	public function getContentTypeFieldValue($type, $field)
	{
		$types = $this->container['contentTypes'];
		return $types[$type][$field] ?? null;
	}

	public function getContentTypeIdFromString(string $contentTypeIdString, bool $validateContentType = true): array
	{
		$contentType = null;
		$contentId = null;

		if (preg_match('/^(?P<content_type>[A-Za-z0-9_]{1,25})-(?P<content_id>\d+)$/', $contentTypeIdString, $matches))
		{
			$contentType = $matches['content_type'] ?? null;
			$contentId = $matches['content_id'] ?? null;
		}

		if ($validateContentType && !isset($this->container['contentTypes'][$contentType]))
		{
			$contentType = null;
		}

		return [$contentType, $contentId];
	}

	public function getContentTypePhraseName($type, $plural = false)
	{
		$types = $this->container['contentTypes'];
		if (!isset($types[$type]))
		{
			return '';
		}
		$fields = $types[$type];

		if ($plural)
		{
			if (isset($fields['phrase_plural']))
			{
				return $fields['phrase_plural'];
			}
			else if (isset($fields['phrase']))
			{
				return $fields['phrase'] . 's';
			}
			else
			{
				return "{$type}s";
			}
		}
		else
		{
			return $fields['phrase'] ?? $type;
		}
	}

	public function getContentTypePhrase($type, $plural = false)
	{
		$phraseName = $this->getContentTypePhraseName($type, $plural);
		return strlen($phraseName) > 0 ? \XF::phrase($phraseName) : '';
	}

	public function getContentTypePhrases($plural = false, $withField = null)
	{
		$output = [];
		foreach ($this->container['contentTypes'] AS $type => $fields)
		{
			if (!$withField || isset($fields[$withField]))
			{
				if ($plural)
				{
					if (isset($fields['phrase_plural']))
					{
						$phrase = $fields['phrase_plural'];
					}
					else if (isset($fields['phrase']))
					{
						$phrase = $fields['phrase'] . 's';
					}
					else
					{
						$phrase = "{$type}s";
					}
				}
				else
				{
					$phrase = $fields['phrase'] ?? $type;
				}
				$output[$type] = \XF::phrase($phrase);
			}
		}

		return $output;
	}

	/**
	 * @return Session
	 */
	public function session()
	{
		return $this->container['session'];
	}

	/**
	 * @return Error
	 */
	public function error()
	{
		return $this->container['error'];
	}

	/**
	 * @return Manager
	 */
	public function em()
	{
		return $this->container['em'];
	}

	/**
	 * @template T of Mvc\Entity\Entity
	 *
	 * @param class-string<T> $entity
	 * @param mixed $id
	 * @param array|string|null $with
	 *
	 * @return T|null
	 */
	public function find($entity, $id, $with = [])
	{
		return $this->em()->find($entity, $id, $with);
	}

	/**
	 * @param string $contentType
	 * @param int|array $contentId
	 * @param string|array $with
	 *
	 * @return null|ArrayCollection|Mvc\Entity\Entity
	 */
	public function findByContentType($contentType, $contentId, $with = [])
	{
		$entity = $this->getContentTypeEntity($contentType);

		if (is_array($contentId))
		{
			return $this->em()->findByIds($entity, $contentId, $with);
		}
		else
		{
			return $this->em()->find($entity, $contentId, $with);
		}
	}

	/**
	 * @param string $contentType
	 * @param bool $throw
	 *
	 * @return string|null
	 */
	public function getContentTypeEntity($contentType, $throw = true)
	{
		$entity = $this->getContentTypeFieldValue($contentType, 'entity');
		if (!$entity && $throw)
		{
			throw new \LogicException("Content type $contentType must define an 'entity' value");
		}

		return $entity;
	}

	/**
	 * @template T of Mvc\Entity\Finder
	 *
	 * @param class-string<T> $identifier
	 *
	 * @return T
	 */
	public function finder($identifier)
	{
		return $this->em()->getFinder($identifier);
	}

	/**
	 * @template T of Mvc\Entity\Repository
	 *
	 * @param class-string<T> $identifier
	 *
	 * @return T
	 */
	public function repository($identifier)
	{
		return $this->em()->getRepository($identifier);
	}

	/**
	 * @param integer $id
	 *
	 * @return Language
	 */
	public function language($id = 0)
	{
		return $this->container->create('language', $id);
	}

	/**
	 * Gets the effective language for the specified user. If the language is not usable by this person,
	 * it falls back to the default.
	 *
	 * @param User $user
	 *
	 * @return Language
	 */
	public function userLanguage(User $user)
	{
		$language = $this->language($user->language_id);
		if (!$language->isUsable($user))
		{
			$language = $this->language(0);
		}

		return $language;
	}

	/**
	 * @param integer $id
	 *
	 * @return Style
	 */
	public function style($id = 0)
	{
		return $this->container->create('style', $id);
	}

	/**
	 * @param string $class
	 * @param Request $request
	 *
	 * @return Controller|null
	 */
	public function controller($class, Request $request)
	{
		return $this->container->create('controller', $class, [
			'request' => $request,
		]);
	}

	/**
	 * @return Job\Manager
	 */
	public function jobManager()
	{
		return $this->container['job.manager'];
	}

	/**
	 * @return Extension
	 */
	public function extension()
	{
		return $this->container['extension'];
	}

	/**
	 * Fires a code event for an extension point
	 *
	 * @param string $event
	 * @param array $args
	 * @param null|string $hint
	 *
	 * @return bool
	 */
	public function fire($event, array $args, $hint = null)
	{
		return $this->extension()->fire($event, $args, $hint);
	}

	/**
	 * Gets the callable class name for a dynamically extended class.
	 *
	 * @template TBase
	 * @template TFakeBase
	 * @template TSubclass of TBase
	 *
	 * @param class-string<TBase>          $class
	 * @param class-string<TFakeBase>|null $fakeBaseClass
	 *
	 * @return class-string<TSubclass>
	 */
	public function extendClass($class, $fakeBaseClass = null)
	{
		return $this->extension()->extendClass($class, $fakeBaseClass);
	}

	/**
	 * @return Search
	 */
	public function search()
	{
		return $this->container['search'];
	}

	/**
	 * @return Formatter
	 */
	public function stringFormatter()
	{
		return $this->container['string.formatter'];
	}

	/**
	 * @return BbCode
	 */
	public function bbCode()
	{
		return $this->container['bbCode'];
	}

	/**
	 * @return ApiDocs
	 */
	public function apiDocs()
	{
		return $this->container['apiDocs'];
	}

	/**
	 * @return Image\Manager
	 */
	public function imageManager()
	{
		return $this->container['imageManager'];
	}

	/**
	 * @return Logger
	 */
	public function logger()
	{
		return $this->container['logger'];
	}

	/**
	 * @return Debugger
	 */
	public function debugger()
	{
		return $this->container['debugger'];
	}

	/**
	 * @template T of \XF\Job\AbstractJob
	 *
	 * @param class-string<T> $class
	 * @param int $jobId
	 * @param array<string, mixed> $params
	 * @param int $previousAttempts
	 *
	 * @return T|null
	 */
	public function job($class, $jobId, array $params = [], int $previousAttempts = 0): ?AbstractJob
	{
		$arguments = [$jobId, $params, $previousAttempts];

		return $this->container->create('job', $class, $arguments);
	}

	/**
	 * @template T of \XF\Searcher\AbstractSearcher
	 *
	 * @param class-string<T> $class
	 * @param array|null $criteria
	 *
	 * @return T
	 */
	public function searcher($class, ?array $criteria = null)
	{
		return $this->container->create('searcher', $class, [$criteria]);
	}

	/**
	 * @template T of \XF\Filterer\AbstractFilterer
	 *
	 * @param class-string<T> $class
	 * @param array $setupData
	 *
	 * @return T
	 */
	public function filterer($class, array $setupData = [])
	{
		return $this->container->create('filterer', $class, [$setupData]);
	}


	/**
	 * @template T of \XF\Authentication\AbstractAuth
	 *
	 * @param class-string<T> $class
	 * @param array $data
	 *
	 * @return T
	 */
	public function auth($class, array $data = [])
	{
		if (!$class)
		{
			$class = $this->container['auth.default'];
		}

		return $this->container->create('auth', $class, [$data]);
	}

	/**
	 * @param string $type
	 * @param bool $throw Throw an exception on invalid type; if false, returns null instead
	 *
	 * @return AbstractHandler|null
	 */
	public function forumType($type, $throw = true)
	{
		$forumType = $this->container->create('forumType', $type);
		if (!$forumType && $throw)
		{
			throw new \InvalidArgumentException("Invalid forum type '$type'");
		}

		return $forumType;
	}

	/**
	 * @param string $type
	 * @param bool $throw Throw an exception on invalid type; if false, returns null instead
	 *
	 * @return ThreadType\AbstractHandler|null
	 */
	public function threadType($type, $throw = true)
	{
		$threadType = $this->container->create('threadType', $type);
		if (!$threadType && $throw)
		{
			throw new \InvalidArgumentException("Invalid thread type '$type'");
		}

		return $threadType;
	}

	/**
	 * @template T of \XF\Service\AbstractService
	 *
	 * @param class-string<T> $class
	 * @param mixed ...$arguments
	 *
	 * @return T
	 */
	public function service($class, ...$arguments)
	{
		return $this->container->create('service', $class, $arguments);
	}

	/**
	 * @template T
	 *
	 * @param class-string<T> $class
	 * @param mixed ...$arguments
	 *
	 * @return T
	 */
	public function helper($class, ...$arguments)
	{
		return $this->container->create('helper', $class, $arguments);
	}

	/**
	 * @template T
	 *
	 * @param class-string<T> $class
	 *
	 * @return T
	 */
	public function data($class)
	{
		return $this->container->create('data', $class);
	}

	/**
	 * @template T of \XF\Validator\AbstractValidator
	 *
	 * @param class-string<T> $type
	 *
	 * @return T
	 */
	public function validator($type)
	{
		return $this->container->create('validator', $type);
	}

	/**
	 * @param array $columns
	 * @param array $existingValues
	 * @param bool $isUpdating
	 *
	 * @return ArrayValidator
	 */
	public function arrayValidator(
		array $columns,
		array $existingValues = [],
		bool $isUpdating = false
	): ArrayValidator
	{
		$valueFormatter = $this->get('em.valueFormatter');

		$class = $this->extendClass(ArrayValidator::class);
		return new $class($columns, $valueFormatter, $existingValues, $isUpdating);
	}

	/**
	 * @template T of \XF\Captcha\AbstractCaptcha
	 *
	 * @param class-string<T>|null $class
	 * @param mixed ...$arguments
	 *
	 * @return T|false
	 */
	public function captcha($class = null)
	{
		if ($class === null)
		{
			$class = $this->options()->captcha;
			if (!$class)
			{
				return false;
			}
		}
		$arguments = func_get_args();
		unset($arguments[0]);
		return $this->container->create('captcha', $class, $arguments);
	}

	/**
	 * @template T of \XF\Criteria\AbstractCriteria
	 *
	 * @param class-string<T> $class
	 * @param array $criteria
	 * @param mixed ...$arguments
	 *
	 * @return T
	 */
	public function criteria($class, array $criteria)
	{
		$arguments = func_get_args();
		unset($arguments[0]);
		return $this->container->create('criteria', $class, $arguments);
	}

	/**
	 * @template T of \XF\Webhook\Criteria\AbstractCriteria
	 *
	 * @param string $class
	 * @param array $criteria
	 * @param mixed ...$arguments
	 *
	 * @return ($class is class-string<T> ? T : AbstractCriteria)
	 */
	public function webhookCriteria(string $class, array $criteria)
	{
		$arguments = func_get_args();
		unset($arguments[0]);
		return $this->container->create('webhookCriteria', $class, $arguments);
	}

	/**
	 * @template T of \XF\Notifier\AbstractNotifier
	 *
	 * @param class-string<T> $class
	 * @param mixed ...$arguments
	 *
	 * @return T
	 */
	public function notifier($class)
	{
		$arguments = func_get_args();
		unset($arguments[0]);

		return $this->container->create('notifier', $class, $arguments);
	}

	/**
	 * @return Spam
	 */
	public function spam()
	{
		return $this->container['spam'];
	}

	/**
	 * @return SubContainer\Http
	 */
	public function http()
	{
		return $this->container['http'];
	}

	/**
	 * @return OAuth
	 */
	public function oAuth()
	{
		return $this->container['oAuth'];
	}

	/**
	 * @return Proxy
	 */
	public function proxy()
	{
		return $this->container['proxy'];
	}

	/**
	 * @return Oembed
	 */
	public function oembed()
	{
		return $this->container['oembed'];
	}

	/**
	 * @return Bounce
	 */
	public function bounce()
	{
		return $this->container['bounce'];
	}

	/**
	 * @return Unsubscribe
	 */
	public function unsubscribe()
	{
		return $this->container['unsubscribe'];
	}

	/**
	 * @return Widget
	 */
	public function widget()
	{
		return $this->container['widget'];
	}

	/**
	 * @return Import
	 */
	public function import()
	{
		return $this->container['import'];
	}

	/**
	 * @return Sitemap\Builder
	 */
	public function sitemapBuilder()
	{
		return $this->container['sitemap.builder'];
	}

	/**
	 * @param string $type
	 * @param mixed $value
	 * @param null $errorKey Returned error type identifier
	 * @param array $options
	 *
	 * @return bool
	 */
	public function isValid($type, $value, &$errorKey = null, array $options = [])
	{
		$validator = $this->validator($type);
		$validator->setOptions($options);
		return $validator->isValid($value, $errorKey);
	}

	/**
	 * @param bool $inTransaction
	 *
	 * @return FormAction
	 */
	public function formAction($inTransaction = true)
	{
		$formAction = new FormAction();
		if ($inTransaction)
		{
			$formAction->applyInTransaction($this->db());
		}
		return $formAction;
	}

	/**
	 * @param string $type
	 *
	 * @return AbstractRenderer
	 */
	public function renderer($type)
	{
		return $this->container->create('renderer', $type);
	}

	/**
	 * @return Templater
	 */
	public function templater()
	{
		return $this->container['templater'];
	}

	/**
	 * @return Compiler
	 */
	public function templateCompiler()
	{
		return $this->container['templateCompiler'];
	}

	/**
	 * @return CssWriter
	 */
	public function cssWriter()
	{
		return $this->container['cssWriter'];
	}

	public function iconRenderer(int $styleId = 0): IconRenderer
	{
		return $this->container->create('iconRenderer', $styleId);
	}

	/**
	 * @return DevJsResponse
	 */
	public function developmentJsResponse()
	{
		return $this->container['development.jsResponse'];
	}

	/**
	 * @return DevelopmentOutput
	 */
	public function developmentOutput()
	{
		return $this->container['development.output'];
	}

	/**
	 * @return DesignerOutput
	 */
	public function designerOutput()
	{
		return $this->container['designer.output'];
	}

	/**
	 * @return AddOn\Manager
	 */
	public function addOnManager()
	{
		return $this->container('addon.manager');
	}

	/**
	 * @return DataManager
	 */
	public function addOnDataManager()
	{
		return $this->container('addon.dataManager');
	}

	/**
	 * @return Api|null
	 */
	public function giphyApi()
	{
		return $this->container('giphy.api');
	}

	/**
	 * @param string|null $key
	 *
	 * @return Container|mixed
	 */
	public function container($key = null)
	{
		return $key === null ? $this->container : $this->container[$key];
	}

	public function __sleep()
	{
		throw new \LogicException('Instances of ' . self::class . ' cannot be serialized or unserialized');
	}

	public function __wakeup()
	{
		throw new \LogicException('Instances of ' . self::class . ' cannot be serialized or unserialized');
	}
}
