<?php

/**
 * @package     Joomla.Administrator
 * @subpackage  com_installer
 *
 * @copyright   (C) 2017 Open Source Matters, Inc. <https://www.joomla.org>
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

namespace Joomla\Component\Installer\Administrator\Helper;

use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Object\CMSObject;
use Joomla\Database\DatabaseInterface;
use Joomla\Database\ParameterType;

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

/**
 * Installer helper.
 *
 * @since  1.6
 */
class InstallerHelper
{
    /**
     * Get a list of filter options for the extension types.
     *
     * @return  array  An array of \stdClass objects.
     *
     * @since   3.0
     */
    public static function getExtensionTypes()
    {
        $db    = Factory::getDbo();
        $query = $db->getQuery(true)
            ->select('DISTINCT ' . $db->quoteName('type'))
            ->from($db->quoteName('#__extensions'));
        $db->setQuery($query);
        $types = $db->loadColumn();

        $options = [];

        foreach ($types as $type) {
            $options[] = HTMLHelper::_('select.option', $type, Text::_('COM_INSTALLER_TYPE_' . strtoupper($type)));
        }

        return $options;
    }

    /**
     * Get a list of filter options for the extension types.
     *
     * @return  array  An array of \stdClass objects.
     *
     * @since   3.0
     */
    public static function getExtensionGroups()
    {
        $nofolder = '';
        $db       = Factory::getDbo();
        $query    = $db->getQuery(true)
            ->select('DISTINCT ' . $db->quoteName('folder'))
            ->from($db->quoteName('#__extensions'))
            ->where($db->quoteName('folder') . ' != :folder')
            ->bind(':folder', $nofolder)
            ->order($db->quoteName('folder'));
        $db->setQuery($query);
        $folders = $db->loadColumn();

        $options = [];

        foreach ($folders as $folder) {
            $options[] = HTMLHelper::_('select.option', $folder, $folder);
        }

        return $options;
    }

    /**
     * Get a list of filter options for the application clients.
     *
     * @return  array  An array of \JHtmlOption elements.
     *
     * @since   3.5
     */
    public static function getClientOptions()
    {
        // Build the filter options.
        $options   = [];
        $options[] = HTMLHelper::_('select.option', '0', Text::_('JSITE'));
        $options[] = HTMLHelper::_('select.option', '1', Text::_('JADMINISTRATOR'));
        $options[] = HTMLHelper::_('select.option', '3', Text::_('JAPI'));

        return $options;
    }

    /**
     * Get a list of filter options for the application statuses.
     *
     * @return  array  An array of \JHtmlOption elements.
     *
     * @since   3.5
     */
    public static function getStateOptions()
    {
        // Build the filter options.
        $options   = [];
        $options[] = HTMLHelper::_('select.option', '0', Text::_('JDISABLED'));
        $options[] = HTMLHelper::_('select.option', '1', Text::_('JENABLED'));
        $options[] = HTMLHelper::_('select.option', '2', Text::_('JPROTECTED'));
        $options[] = HTMLHelper::_('select.option', '3', Text::_('JUNPROTECTED'));

        return $options;
    }

    /**
     * Get a list of filter options for extensions of the "package" type.
     *
     * @return  array
     * @since   4.2.0
     */
    public static function getPackageOptions(): array
    {
        $options = [];

        /** @var DatabaseInterface $db The application's database driver object */
        $db         = Factory::getContainer()->get(DatabaseInterface::class);
        $query      = $db->getQuery(true)
            ->select(
                $db->quoteName(
                    [
                        'extension_id',
                        'name',
                        'element',
                    ]
                )
            )
            ->from($db->quoteName('#__extensions'))
            ->where($db->quoteName('type') . ' = ' . $db->quote('package'));
        $extensions = $db->setQuery($query)->loadObjectList() ?: [];

        if (empty($extensions)) {
            return $options;
        }

        $language  = Factory::getApplication()->getLanguage();
        $arrayKeys = array_map(
            function (object $entry) use ($language): string {
                $language->load($entry->element, JPATH_ADMINISTRATOR);

                return Text::_($entry->name);
            },
            $extensions
        );
        $arrayValues = array_map(
            function (object $entry): int {
                return $entry->extension_id;
            },
            $extensions
        );

        $extensions = array_combine($arrayKeys, $arrayValues);
        ksort($extensions);

        foreach ($extensions as $label => $id) {
            $options[] = HTMLHelper::_('select.option', $id, $label);
        }

        return $options;
    }

    /**
     * Get a list of filter options for the application statuses.
     *
     * @param   string    $element   element of an extension
     * @param   string    $type      type of an extension
     * @param   integer   $clientId  client_id of an extension
     * @param   ?string   $folder    folder of an extension
     *
     * @return  \SimpleXMLElement
     *
     * @since   4.0.0
     */
    public static function getInstallationXML(
        string $element,
        string $type,
        int $clientId = 1,
        ?string $folder = null
    ): ?\SimpleXMLElement {
        $path = [0 => JPATH_SITE, 1 => JPATH_ADMINISTRATOR, 3 => JPATH_API][$clientId] ?? JPATH_SITE;

        switch ($type) {
            case 'component':
                $path .= '/components/' . $element . '/' . substr($element, 4) . '.xml';
                break;
            case 'plugin':
                $path .= '/plugins/' . $folder . '/' . $element . '/' . $element . '.xml';
                break;
            case 'module':
                $path .= '/modules/' . $element . '/' . $element . '.xml';
                break;
            case 'template':
                $path .= '/templates/' . $element . '/templateDetails.xml';
                break;
            case 'library':
                $path = JPATH_ADMINISTRATOR . '/manifests/libraries/' . $element . '.xml';
                break;
            case 'file':
                $path = JPATH_ADMINISTRATOR . '/manifests/files/' . $element . '.xml';
                break;
            case 'package':
                $path = JPATH_ADMINISTRATOR . '/manifests/packages/' . $element . '.xml';
                break;
            case 'language':
                $path .= '/language/' . $element . '/install.xml';
        }

        if (file_exists($path) === false) {
            return null;
        }

        $xmlElement = simplexml_load_file($path);

        return ($xmlElement !== false) ? $xmlElement : null;
    }

    /**
     * Get the download key of an extension going through their installation xml
     *
     * @param   CMSObject  $extension  element of an extension
     *
     * @return  array  An array with the prefix, suffix and value of the download key
     *
     * @since   4.0.0
     */
    public static function getDownloadKey(CMSObject $extension): array
    {
        $installXmlFile = self::getInstallationXML(
            $extension->get('element'),
            $extension->get('type'),
            $extension->get('client_id'),
            $extension->get('folder')
        );

        if (!$installXmlFile) {
            return [
                'supported' => false,
                'valid'     => false,
            ];
        }

        if (!isset($installXmlFile->dlid)) {
            return [
                'supported' => false,
                'valid'     => false,
            ];
        }

        $prefix = (string) $installXmlFile->dlid['prefix'];
        $suffix = (string) $installXmlFile->dlid['suffix'];
        $value  = substr($extension->get('extra_query'), \strlen($prefix));

        if ($suffix) {
            $value = substr($value, 0, -\strlen($suffix));
        }

        $downloadKey = [
            'supported' => true,
            'valid'     => (bool) $value,
            'prefix'    => $prefix,
            'suffix'    => $suffix,
            'value'     => $value,
        ];

        return $downloadKey;
    }

    /**
     * Get the download key of an extension given enough information to locate it in the #__extensions table
     *
     * @param   string       $element   Name of the extension, e.g. com_foo
     * @param   string       $type      The type of the extension, e.g. component
     * @param   int          $clientId  [optional] Joomla client for the extension, see the #__extensions table
     * @param   string|null  $folder    Extension folder, only applies for 'plugin' type
     *
     * @return  array
     *
     * @since   4.0.0
     */
    public static function getExtensionDownloadKey(
        string $element,
        string $type,
        int $clientId = 1,
        ?string $folder = null
    ): array {
        // Get the database driver. If it fails we cannot report whether the extension supports download keys.
        try {
            $db = Factory::getDbo();
        } catch (\Exception $e) {
            return [
                'supported' => false,
                'valid'     => false,
            ];
        }

        // Try to retrieve the extension information as a CMSObject
        $query = $db->getQuery(true)
            ->select($db->quoteName('extension_id'))
            ->from($db->quoteName('#__extensions'))
            ->where($db->quoteName('type') . ' = :type')
            ->where($db->quoteName('element') . ' = :element')
            ->where($db->quoteName('folder') . ' = :folder')
            ->where($db->quoteName('client_id') . ' = :client_id');
        $query->bind(':type', $type, ParameterType::STRING);
        $query->bind(':element', $element, ParameterType::STRING);
        $query->bind(':client_id', $clientId, ParameterType::INTEGER);
        $query->bind(':folder', $folder, ParameterType::STRING);

        try {
            $extension = new CMSObject($db->setQuery($query)->loadAssoc());
        } catch (\Exception) {
            return [
                'supported' => false,
                'valid'     => false,
            ];
        }

        // Use the getDownloadKey() method to return the download key information
        return self::getDownloadKey($extension);
    }

    /**
     * Returns a list of update site IDs which support download keys. By default this returns all qualifying update
     * sites, even if they are not enabled.
     *
     *
     * @param   bool  $onlyEnabled  [optional] Set true to only returned enabled update sites.
     *
     * @return  int[]
     * @since   4.0.0
     */
    public static function getDownloadKeySupportedSites($onlyEnabled = false): array
    {
        /**
         * NOTE: The closures are not inlined because in this case the Joomla Code Style standard produces two mutually
         * exclusive errors, making the file impossible to commit. Using closures in variables makes the code less
         * readable but works around that issue.
         */

        $extensions = self::getUpdateSitesInformation($onlyEnabled);

        $filterClosure = function (CMSObject $extension) {
            $dlidInfo = self::getDownloadKey($extension);

            return $dlidInfo['supported'];
        };
        $extensions = array_filter($extensions, $filterClosure);

        $mapClosure = function (CMSObject $extension) {
            return $extension->get('update_site_id');
        };

        return array_map($mapClosure, $extensions);
    }

    /**
     * Returns a list of update site IDs which are missing download keys. By default this returns all qualifying update
     * sites, even if they are not enabled.
     *
     * @param   bool  $exists       [optional] If true, returns update sites with a valid download key. When false,
     *                              returns update sites with an invalid / missing download key.
     * @param   bool  $onlyEnabled  [optional] Set true to only returned enabled update sites.
     *
     * @return  int[]
     * @since   4.0.0
     */
    public static function getDownloadKeyExistsSites(bool $exists = true, $onlyEnabled = false): array
    {
        /**
         * NOTE: The closures are not inlined because in this case the Joomla Code Style standard produces two mutually
         * exclusive errors, making the file impossible to commit. Using closures in variables makes the code less
         * readable but works around that issue.
         */

        $extensions = self::getUpdateSitesInformation($onlyEnabled);

        // Filter the extensions by what supports Download Keys
        $filterClosure = function (CMSObject $extension) use ($exists) {
            $dlidInfo = self::getDownloadKey($extension);

            if (!$dlidInfo['supported']) {
                return false;
            }

            return $exists ? $dlidInfo['valid'] : !$dlidInfo['valid'];
        };
        $extensions = array_filter($extensions, $filterClosure);

        // Return only the update site IDs
        $mapClosure = function (CMSObject $extension) {
            return $extension->get('update_site_id');
        };

        return array_map($mapClosure, $extensions);
    }


    /**
     * Get information about the update sites
     *
     * @param   bool  $onlyEnabled  Only return enabled update sites
     *
     * @return  CMSObject[]  List of update site and linked extension information
     * @since   4.0.0
     */
    protected static function getUpdateSitesInformation(bool $onlyEnabled): array
    {
        try {
            $db = Factory::getDbo();
        } catch (\Exception $e) {
            return [];
        }

        $query = $db->getQuery(true)
            ->select(
                $db->quoteName(
                    [
                        's.update_site_id',
                        's.enabled',
                        's.extra_query',
                        'e.extension_id',
                        'e.type',
                        'e.element',
                        'e.folder',
                        'e.client_id',
                        'e.manifest_cache',
                    ],
                    [
                        'update_site_id',
                        'enabled',
                        'extra_query',
                        'extension_id',
                        'type',
                        'element',
                        'folder',
                        'client_id',
                        'manifest_cache',
                    ]
                )
            )
            ->from($db->quoteName('#__update_sites', 's'))
            ->innerJoin(
                $db->quoteName('#__update_sites_extensions', 'se'),
                $db->quoteName('se.update_site_id') . ' = ' . $db->quoteName('s.update_site_id')
            )
            ->innerJoin(
                $db->quoteName('#__extensions', 'e'),
                $db->quoteName('e.extension_id') . ' = ' . $db->quoteName('se.extension_id')
            )
            ->where($db->quoteName('state') . ' = 0');

        if ($onlyEnabled) {
            $enabled = 1;
            $query->where($db->quoteName('s.enabled') . ' = :enabled')
                ->bind(':enabled', $enabled, ParameterType::INTEGER);
        }

        // Try to get all of the update sites, including related extension information
        try {
            $items = [];
            $db->setQuery($query);

            foreach ($db->getIterator() as $item) {
                $items[] = new CMSObject($item);
            }

            return $items;
        } catch (\Exception) {
            return [];
        }
    }
}
