<?php

/**
 * Joomla! Content Management System
 *
 * @copyright  (C) 2005 Open Source Matters, Inc. <https://www.joomla.org>
 * @license    GNU General Public License version 2 or later; see LICENSE.txt
 */

namespace Joomla\CMS;

use Joomla\CMS\Application\CMSApplicationInterface;
use Joomla\CMS\Cache\Cache;
use Joomla\CMS\Cache\CacheControllerFactoryInterface;
use Joomla\CMS\Client\ClientHelper;
use Joomla\CMS\Date\Date;
use Joomla\CMS\Document\Document;
use Joomla\CMS\Document\FactoryInterface;
use Joomla\CMS\Filesystem\Stream;
use Joomla\CMS\Language\Language;
use Joomla\CMS\Language\LanguageFactoryInterface;
use Joomla\CMS\Mail\Mail;
use Joomla\CMS\Mail\MailerFactoryInterface;
use Joomla\CMS\Session\Session;
use Joomla\CMS\User\User;
use Joomla\Database\DatabaseDriver;
use Joomla\Database\DatabaseInterface;
use Joomla\DI\Container;
use Joomla\Registry\Registry;

// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects

/**
 * Joomla Platform Factory class.
 *
 * @since  1.7.0
 */
abstract class Factory
{
    /**
     * Global application object
     *
     * @var    CMSApplicationInterface
     * @since  1.7.0
     */
    public static $application = null;

    /**
     * Global cache object
     *
     * @var    Cache
     * @since  1.7.0
     */
    public static $cache = null;

    /**
     * Global configuration object
     *
     * @var         \JConfig
     * @since       1.7.0
     *
     * @deprecated  4.3 will be removed in 6.0
     *              Use the configuration object within the application
     *              Example:
     *              Factory::getApplication()->getConfig();
     */
    public static $config = null;

    /**
     * Global container object
     *
     * @var    Container
     * @since  4.0.0
     */
    public static $container = null;

    /**
     * Container for Date instances
     *
     * @var    array
     * @since  1.7.3
     */
    public static $dates = [];

    /**
     * Global session object
     *
     * @var         Session
     * @since       1.7.0
     *
     * @deprecated  4.3 will be removed in 6.0
     *              Use the session service in the DI container or get from the application object
     *              Example:
     *              Factory::getApplication()->getSession();
     */
    public static $session = null;

    /**
     * Global language object
     *
     * @var         Language
     * @since       1.7.0
     *
     * @deprecated  4.3 will be removed in 6.0
     *              Use the language service in the DI container or get from the application object
     *              Example:
     *              Factory::getApplication()->getLanguage();
     */
    public static $language = null;

    /**
     * Global document object
     *
     * @var         Document
     * @since       1.7.0
     *
     * @deprecated  4.3 will be removed in 6.0
     *               Use the document service in the DI container or get from the application object
     *               Example:
     *               Factory::getApplication()->getDocument();
     */
    public static $document = null;

    /**
     * Global database object
     *
     * @var         DatabaseDriver
     * @since       1.7.0
     *
     * @deprecated  4.3 will be removed in 6.0
     *              Use the database service in the DI container
     *              Example:
     *              Factory::getContainer()->get(DatabaseInterface::class);
     */
    public static $database = null;

    /**
     * Global mailer object
     *
     * @var    Mail
     * @since  1.7.0
     */
    public static $mailer = null;

    /**
     * Get the global application object. When the global application doesn't exist, an exception is thrown.
     *
     * @return  CMSApplicationInterface object
     *
     * @since   1.7.0
     * @throws  \Exception
     */
    public static function getApplication()
    {
        if (!self::$application) {
            throw new \Exception('Failed to start application', 500);
        }

        return self::$application;
    }

    /**
     * Get a configuration object
     *
     * Returns the global {@link \JConfig} object, only creating it if it doesn't already exist.
     *
     * @param   string  $file       The path to the configuration file
     * @param   string  $type       The type of the configuration file
     * @param   string  $namespace  The namespace of the configuration file
     *
     * @return  Registry
     *
     * @see         Registry
     * @since       1.7.0
     *
     * @deprecated  4.3 will be removed in 6.0
     *              Use the configuration object within the application
     *              Example:
     *              Factory::getApplication()->getConfig();
     */
    public static function getConfig($file = null, $type = 'PHP', $namespace = '')
    {
        @trigger_error(
            \sprintf(
                '%s() is deprecated. The configuration object should be read from the application.',
                __METHOD__
            ),
            E_USER_DEPRECATED
        );

        /**
         * If there is an application object, fetch the configuration from there.
         * Check it's not null because LanguagesModel can make it null and if it's null
         * we would want to re-init it from configuration.php.
         */
        if (self::$application && self::$application->getConfig() !== null) {
            return self::$application->getConfig();
        }

        if (!self::$config) {
            if ($file === null) {
                $file = JPATH_CONFIGURATION . '/configuration.php';
            }

            self::$config = self::createConfig($file, $type, $namespace);
        }

        return self::$config;
    }

    /**
     * Get a container object
     *
     * Returns the global service container object, only creating it if it doesn't already exist.
     *
     * This method is only suggested for use in code whose responsibility is to create new services
     * and needs to be able to resolve the dependencies, and should therefore only be used when the
     * container is not accessible by other means.  Valid uses of this method include:
     *
     * - A static `getInstance()` method calling a factory service from the container,
     *   see `Joomla\CMS\Toolbar\Toolbar::getInstance()` as an example
     * - An application front controller loading and executing the Joomla application class,
     *   see the `cli/joomla.php` file as an example
     * - Retrieving optional constructor dependencies when not injected into a class during a transitional
     *   period to retain backward compatibility, in this case a deprecation notice should also be emitted to
     *   notify developers of changes needed in their code
     *
     * This method is not suggested for use as a one-for-one replacement of static calls, such as
     * replacing calls to `Factory::getDbo()` with calls to `Factory::getContainer()->get(DatabaseInterface::class)`,
     * code should be refactored to support dependency injection instead of making this change.
     *
     * @return  Container
     *
     * @since   4.0.0
     */
    public static function getContainer(): Container
    {
        if (!self::$container) {
            self::$container = self::createContainer();
        }

        return self::$container;
    }

    /**
     * Get a session object.
     *
     * Returns the global {@link Session} object, only creating it if it doesn't already exist.
     *
     * @param   array  $options  An array containing session options
     *
     * @return  Session object
     *
     * @see         Session
     * @since       1.7.0
     *
     * @deprecated  4.3 will be removed in 6.0
     *              Use the session service in the DI container or get from the application object
     *              Example:
     *              Factory::getApplication()->getSession();
     */
    public static function getSession(array $options = [])
    {
        @trigger_error(
            \sprintf(
                '%1$s() is deprecated. Load the session from the dependency injection container or via %2$s::getApplication()->getSession().',
                __METHOD__,
                __CLASS__
            ),
            E_USER_DEPRECATED
        );

        return self::getApplication()->getSession();
    }

    /**
     * Get a language object.
     *
     * Returns the global {@link Language} object, only creating it if it doesn't already exist.
     *
     * @return  Language object
     *
     * @see         Language
     * @since       1.7.0
     *
     * @deprecated  4.3 will be removed in 6.0
     *              Use the language service in the DI container or get from the application object
     *              Example:
     *              Factory::getApplication()->getLanguage();
     */
    public static function getLanguage()
    {
        @trigger_error(
            \sprintf(
                '%1$s() is deprecated. Load the language from the dependency injection container or via %2$s::getApplication()->getLanguage().',
                __METHOD__,
                __CLASS__
            ),
            E_USER_DEPRECATED
        );

        if (!self::$language) {
            self::$language = self::createLanguage();
        }

        return self::$language;
    }

    /**
     * Get a document object.
     *
     * Returns the global {@link \Joomla\CMS\Document\Document} object, only creating it if it doesn't already exist.
     *
     * @return  Document object
     *
     * @see         Document
     * @since       1.7.0
     *
     * @deprecated  4.3 will be removed in 6.0
     *              Use the document service in the DI container or get from the application object
     *              Example:
     *              Factory::getApplication()->getDocument();
     */
    public static function getDocument()
    {
        @trigger_error(
            \sprintf(
                '%1$s() is deprecated. Load the document from the dependency injection container or via %2$s::getApplication()->getDocument().',
                __METHOD__,
                __CLASS__
            ),
            E_USER_DEPRECATED
        );

        if (!self::$document) {
            self::$document = self::createDocument();
        }

        return self::$document;
    }

    /**
     * Get a user object.
     *
     * Returns the global {@link User} object, only creating it if it doesn't already exist.
     *
     * @param   integer  $id  The user to load - Can be an integer or string - If string, it is converted to ID automatically.
     *
     * @return  User object
     *
     * @see         User
     * @since       1.7.0
     *
     * @deprecated  4.3 will be removed in 6.0
     *              Load the user service from the dependency injection container or get from the application object
     *              Example:
     *              Factory::getApplication()->getIdentity();
     */
    public static function getUser($id = null)
    {
        @trigger_error(
            \sprintf(
                '%1$s() is deprecated. Load the user from the dependency injection container or via %2$s::getApplication()->getIdentity().',
                __METHOD__,
                __CLASS__
            ),
            E_USER_DEPRECATED
        );

        $instance = self::getApplication()->getSession()->get('user');

        if (\is_null($id)) {
            if (!($instance instanceof User)) {
                $instance = User::getInstance();
            }
        } elseif (!($instance instanceof User) || \is_string($id) || $instance->id !== $id) {
            // Check if we have a string as the id or if the numeric id is the current instance
            $instance = User::getInstance($id);
        }

        return $instance;
    }

    /**
     * Get a cache object
     *
     * Returns the global {@link CacheController} object
     *
     * @param   string  $group    The cache group name
     * @param   string  $handler  The handler to use
     * @param   string  $storage  The storage method
     *
     * @return  \Joomla\CMS\Cache\CacheController object
     *
     * @see         Cache
     * @since       1.7.0
     *
     * @deprecated  4.3 will be removed in 6.0
     *              Use the cache controller factory instead
     *              Example:
     *              Factory::getContainer()->get(CacheControllerFactoryInterface::class)->createCacheController($handler, $options);
     */
    public static function getCache($group = '', $handler = 'callback', $storage = null)
    {
        @trigger_error(
            \sprintf(
                '%s() is deprecated. The cache controller should be fetched from the factory.',
                __METHOD__
            ),
            E_USER_DEPRECATED
        );

        $hash = md5($group . $handler . $storage);

        if (isset(self::$cache[$hash])) {
            return self::$cache[$hash];
        }

        $handler = ($handler === 'function') ? 'callback' : $handler;

        $options = ['defaultgroup' => $group];

        if (isset($storage)) {
            $options['storage'] = $storage;
        }

        $cache = self::getContainer()->get(CacheControllerFactoryInterface::class)->createCacheController($handler, $options);

        self::$cache[$hash] = $cache;

        return self::$cache[$hash];
    }

    /**
     * Get a database object.
     *
     * Returns the global {@link DatabaseDriver} object, only creating it if it doesn't already exist.
     *
     * @return  DatabaseDriver
     *
     * @see         DatabaseDriver
     * @since       1.7.0
     *
     * @deprecated  4.3 will be removed in 6.0
     *              Use the database service in the DI container
     *              Example:
     *              Factory::getContainer()->get(DatabaseInterface::class);
     */
    public static function getDbo()
    {
        @trigger_error(
            \sprintf(
                '%1$s() is deprecated. Load the database from the dependency injection container.',
                __METHOD__
            ),
            E_USER_DEPRECATED
        );

        if (!self::$database) {
            if (self::getContainer()->has(DatabaseInterface::class)) {
                self::$database = self::getContainer()->get(DatabaseInterface::class);
            } else {
                self::$database = self::createDbo();
            }
        }

        return self::$database;
    }

    /**
     * Get a mailer object.
     *
     * Returns the global {@link Mail} object, only creating it if it doesn't already exist.
     *
     * @return  Mail object
     *
     * @see     Mail
     * @since   1.7.0
     *
     * @deprecated  4.4.0 will be removed in 6.0
     *              Use the mailer service in the DI container and create a mailer from there
     *              Example:
     *              Factory::getContainer()->get(MailerFactoryInterface::class)->createMailer();
     */
    public static function getMailer()
    {
        if (!self::$mailer) {
            self::$mailer = self::createMailer();
        }

        $copy = clone self::$mailer;

        return $copy;
    }

    /**
     * Return the {@link Date} object
     *
     * @param   mixed  $time      The initial time for the Date object
     * @param   mixed  $tzOffset  The timezone offset.
     *
     * @return  Date object
     *
     * @see     Date
     * @since   1.7.0
     */
    public static function getDate($time = 'now', $tzOffset = null)
    {
        static $classname;
        static $mainLocale;

        $language = self::getLanguage();
        $locale   = $language->getTag();

        if (!isset($classname) || $locale != $mainLocale) {
            // Store the locale for future reference
            $mainLocale = $locale;

            if ($mainLocale !== false) {
                $classname = str_replace('-', '_', $mainLocale) . 'Date';

                if (!class_exists($classname)) {
                    // The class does not exist, default to Date
                    $classname = 'Joomla\\CMS\\Date\\Date';
                }
            } else {
                // No tag, so default to Date
                $classname = 'Joomla\\CMS\\Date\\Date';
            }
        }

        $key = $time . '-' . ($tzOffset instanceof \DateTimeZone ? $tzOffset->getName() : (string) $tzOffset);

        if (!isset(self::$dates[$classname][$key])) {
            self::$dates[$classname][$key] = new $classname($time, $tzOffset);
        }

        $date = clone self::$dates[$classname][$key];

        return $date;
    }

    /**
     * Create a configuration object
     *
     * @param   string  $file       The path to the configuration file.
     * @param   string  $type       The type of the configuration file.
     * @param   string  $namespace  The namespace of the configuration file.
     *
     * @return  Registry
     *
     * @see         Registry
     * @since       1.7.0
     *
     * @deprecated  4.0 will be removed in 6.0
     *              Use the configuration object within the application.
     *              Example: Factory::getApplication()->getConfig();
     */
    protected static function createConfig($file, $type = 'PHP', $namespace = '')
    {
        @trigger_error(
            \sprintf(
                '%s() is deprecated. The configuration object should be read from the application.',
                __METHOD__
            ),
            E_USER_DEPRECATED
        );

        if (is_file($file)) {
            include_once $file;
        }

        // Create the registry with a default namespace of config
        $registry = new Registry();

        // Sanitize the namespace.
        $namespace = ucfirst((string) preg_replace('/[^A-Z_]/i', '', $namespace));

        // Build the config name.
        $name = 'JConfig' . $namespace;

        // Handle the PHP configuration type.
        if ($type === 'PHP' && class_exists($name)) {
            // Create the JConfig object
            $config = new $name();

            // Load the configuration values into the registry
            $registry->loadObject($config);
        }

        return $registry;
    }

    /**
     * Create a container object
     *
     * @return  Container
     *
     * @since   4.0.0
     */
    protected static function createContainer(): Container
    {
        $container = (new Container())
            ->registerServiceProvider(new \Joomla\CMS\Service\Provider\Application())
            ->registerServiceProvider(new \Joomla\CMS\Service\Provider\Authentication())
            ->registerServiceProvider(new \Joomla\CMS\Service\Provider\CacheController())
            ->registerServiceProvider(new \Joomla\CMS\Service\Provider\CaptchaRegistry())
            ->registerServiceProvider(new \Joomla\CMS\Service\Provider\Config())
            ->registerServiceProvider(new \Joomla\CMS\Service\Provider\Console())
            ->registerServiceProvider(new \Joomla\CMS\Service\Provider\EditorsRegistry())
            ->registerServiceProvider(new \Joomla\CMS\Service\Provider\Database())
            ->registerServiceProvider(new \Joomla\CMS\Service\Provider\Dispatcher())
            ->registerServiceProvider(new \Joomla\CMS\Service\Provider\Document())
            ->registerServiceProvider(new \Joomla\CMS\Service\Provider\Form())
            ->registerServiceProvider(new \Joomla\CMS\Service\Provider\Input())
            ->registerServiceProvider(new \Joomla\CMS\Service\Provider\Logger())
            ->registerServiceProvider(new \Joomla\CMS\Service\Provider\Language())
            ->registerServiceProvider(new \Joomla\CMS\Service\Provider\Mailer())
            ->registerServiceProvider(new \Joomla\CMS\Service\Provider\Menu())
            ->registerServiceProvider(new \Joomla\CMS\Service\Provider\Pathway())
            ->registerServiceProvider(new \Joomla\CMS\Service\Provider\HTMLRegistry())
            ->registerServiceProvider(new \Joomla\CMS\Service\Provider\Session())
            ->registerServiceProvider(new \Joomla\CMS\Service\Provider\Toolbar())
            ->registerServiceProvider(new \Joomla\CMS\Service\Provider\WebAssetRegistry())
            ->registerServiceProvider(new \Joomla\CMS\Service\Provider\Router())
            ->registerServiceProvider(new \Joomla\CMS\Service\Provider\User());

        return $container;
    }

    /**
     * Create a database object
     *
     * @return  DatabaseDriver
     *
     * @see         DatabaseDriver
     * @since       1.7.0
     *
     * @deprecated  4.3 will be removed in 6.0
     *              Use the database service in the DI container
     *              Example:
     *              Factory::getContainer()->get(DatabaseInterface::class);
     */
    protected static function createDbo()
    {
        @trigger_error(
            \sprintf(
                '%1$s() is deprecated, register a service provider to create a %2$s instance instead.',
                __METHOD__,
                DatabaseInterface::class
            ),
            E_USER_DEPRECATED
        );

        $conf = self::getConfig();

        $host     = $conf->get('host');
        $user     = $conf->get('user');
        $password = $conf->get('password');
        $database = $conf->get('db');
        $prefix   = $conf->get('dbprefix');
        $driver   = $conf->get('dbtype');

        $options = ['driver' => $driver, 'host' => $host, 'user' => $user, 'password' => $password, 'database' => $database, 'prefix' => $prefix];

        if ((int) $conf->get('dbencryption') !== 0) {
            $options['ssl'] = [
                'enable'             => true,
                'verify_server_cert' => (bool) $conf->get('dbsslverifyservercert'),
            ];

            foreach (['cipher', 'ca', 'key', 'cert'] as $value) {
                $confVal = trim($conf->get('dbssl' . $value, ''));

                if ($confVal !== '') {
                    $options['ssl'][$value] = $confVal;
                }
            }
        }

        try {
            $db = DatabaseDriver::getInstance($options);
        } catch (\RuntimeException $e) {
            if (!headers_sent()) {
                header('HTTP/1.1 500 Internal Server Error');
            }

            jexit('Database Error: ' . $e->getMessage());
        }

        return $db;
    }

    /**
     * Create a mailer object
     *
     * @return  Mail object
     *
     * @see     Mail
     * @since   1.7.0
     *
     * @deprecated  4.4.0 will be removed in 6.0
     *              Use the mailer service in the DI container and create a mailer from there
     *              Example:
     *              Factory::getContainer()->get(MailerFactoryInterface::class)->createMailer();
     */
    protected static function createMailer()
    {
        $mailer = self::getContainer()->get(MailerFactoryInterface::class)->createMailer(self::getConfig());

        // This needs to be set here for backwards compatibility
        Mail::$instances['Joomla'] = $mailer;

        return $mailer;
    }

    /**
     * Create a language object
     *
     * @return  Language object
     *
     * @see         Language
     * @since       1.7.0
     *
     * @deprecated  4.0 will be removed in 6.0
     *              Load the language service from the dependency injection container or via $app->getLanguage()
     *              Example: Factory::getContainer()->get(LanguageFactoryInterface::class)->createLanguage($locale, $debug)
     */
    protected static function createLanguage()
    {
        @trigger_error(
            \sprintf(
                '%1$s() is deprecated. Load the language from the dependency injection container or via %2$s::getApplication()->getLanguage().',
                __METHOD__,
                __CLASS__
            ),
            E_USER_DEPRECATED
        );

        $conf   = self::getConfig();
        $locale = $conf->get('language');
        $debug  = $conf->get('debug_lang');
        $lang   = self::getContainer()->get(LanguageFactoryInterface::class)->createLanguage($locale, $debug);

        return $lang;
    }

    /**
     * Create a document object
     *
     * @return  Document object
     *
     * @see         Document
     * @since       1.7.0
     *
     * @deprecated  4.0 will be removed in 6.0
     *              Load the document service from the dependency injection container or via $app->getDocument()
     *              Example: Factory::getContainer()->get(FactoryInterface::class)->createDocument($type, $attributes);
     */
    protected static function createDocument()
    {
        @trigger_error(
            \sprintf(
                '%1$s() is deprecated. Load the document from the dependency injection container or via %2$s::getApplication()->getDocument().',
                __METHOD__,
                __CLASS__
            ),
            E_USER_DEPRECATED
        );

        $lang = self::getLanguage();

        $input = self::getApplication()->getInput();
        $type  = $input->get('format', 'html', 'cmd');

        $version = new Version();

        $attributes = [
            'charset'      => 'utf-8',
            'lineend'      => 'unix',
            'tab'          => "\t",
            'language'     => $lang->getTag(),
            'direction'    => $lang->isRtl() ? 'rtl' : 'ltr',
            'mediaversion' => $version->getMediaVersion(),
        ];

        return self::getContainer()->get(FactoryInterface::class)->createDocument($type, $attributes);
    }

    /**
     * Creates a new stream object with appropriate prefix
     *
     * @param   boolean  $usePrefix        Prefix the connections for writing
     * @param   boolean  $useNetwork       Use network if available for writing; use false to disable (e.g. FTP, SCP)
     * @param   string   $userAgentSuffix  String to append to user agent
     * @param   boolean  $maskUserAgent    User agent masking (prefix Mozilla)
     *
     * @return  Stream
     *
     * @see     Stream
     * @since   1.7.0
     */
    public static function getStream($usePrefix = true, $useNetwork = true, $userAgentSuffix = 'Joomla', $maskUserAgent = false)
    {
        // Setup the context; Joomla! UA and overwrite
        $context = [];
        $version = new Version();

        // Set the UA for HTTP and overwrite for FTP
        $context['http']['user_agent'] = $version->getUserAgent($userAgentSuffix, $maskUserAgent);
        $context['ftp']['overwrite']   = true;

        if ($usePrefix) {
            $FTPOptions = ClientHelper::getCredentials('ftp');
            $SCPOptions = ClientHelper::getCredentials('scp');

            if ($FTPOptions['enabled'] == 1 && $useNetwork) {
                $prefix = 'ftp://' . $FTPOptions['user'] . ':' . $FTPOptions['pass'] . '@' . $FTPOptions['host'];
                $prefix .= $FTPOptions['port'] ? ':' . $FTPOptions['port'] : '';
                $prefix .= $FTPOptions['root'];
            } elseif ($SCPOptions['enabled'] == 1 && $useNetwork) {
                $prefix = 'ssh2.sftp://' . $SCPOptions['user'] . ':' . $SCPOptions['pass'] . '@' . $SCPOptions['host'];
                $prefix .= $SCPOptions['port'] ? ':' . $SCPOptions['port'] : '';
                $prefix .= $SCPOptions['root'];
            } else {
                $prefix = JPATH_ROOT . '/';
            }

            $retval = new Stream($prefix, JPATH_ROOT, $context);
        } else {
            $retval = new Stream('', '', $context);
        }

        return $retval;
    }
}
