<?php

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

namespace Joomla\CMS\Installer\Adapter;

use Joomla\CMS\Installer\Installer;
use Joomla\CMS\Installer\InstallerAdapter;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Log\Log;
use Joomla\CMS\Table\Update;
use Joomla\Database\ParameterType;
use Joomla\Filesystem\File;
use Joomla\Filesystem\Folder;
use Joomla\Filesystem\Path;

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

/**
 * File installer
 *
 * @since  3.1
 */
class FileAdapter extends InstallerAdapter
{
    /**
     * `<scriptfile>` element of the extension manifest
     *
     * @var    object
     * @since  3.1
     */
    protected $scriptElement = null;

    /**
     * Flag if the adapter supports discover installs
     *
     * Adapters should override this and set to false if discover install is unsupported
     *
     * @var    boolean
     * @since  3.4
     */
    protected $supportsDiscoverInstall = false;

    /**
     * List of processed folders
     *
     * @var    array
     * @since  3.4
     */
    protected $folderList;

    /**
     * List of processed files
     *
     * @var    array
     * @since  3.4
     */
    protected $fileList;

    /**
     * Method to copy the extension's base files from the `<files>` tag(s) and the manifest file
     *
     * @return  void
     *
     * @since   3.4
     * @throws  \RuntimeException
     */
    protected function copyBaseFiles()
    {
        // Populate File and Folder List to copy
        $this->populateFilesAndFolderList();

        // Now that we have folder list, lets start creating them
        foreach ($this->folderList as $folder) {
            if (!is_dir(Path::clean($folder))) {
                if (!$created = Folder::create($folder)) {
                    throw new \RuntimeException(
                        Text::sprintf('JLIB_INSTALLER_ABORT_FILE_INSTALL_FAIL_SOURCE_DIRECTORY', $folder)
                    );
                }

                // Since we created a directory and will want to remove it if we have to roll back.
                // The installation due to some errors, let's add it to the installation step stack.
                if ($created) {
                    $this->parent->pushStep(['type' => 'folder', 'path' => $folder]);
                }
            }
        }

        // Now that we have file list, let's start copying them
        $this->parent->copyFiles($this->fileList);
    }

    /**
     * Method to finalise the installation processing
     *
     * @return  void
     *
     * @since   3.4
     * @throws  \RuntimeException
     */
    protected function finaliseInstall()
    {
        // Clobber any possible pending updates
        $update = new Update($this->getDatabase());

        $uid = $update->find(
            [
                'element' => $this->element,
                'type'    => $this->type,
            ]
        );

        if ($uid) {
            $update->delete($uid);
        }

        // Lastly, we will copy the manifest file to its appropriate place.
        $manifest         = [];
        $manifest['src']  = $this->parent->getPath('manifest');
        $manifest['dest'] = JPATH_MANIFESTS . '/files/' . basename($this->parent->getPath('manifest'));

        if (!$this->parent->copyFiles([$manifest], true)) {
            // Install failed, rollback changes
            throw new \RuntimeException(
                Text::sprintf(
                    'JLIB_INSTALLER_ABORT_COPY_SETUP',
                    Text::_('JLIB_INSTALLER_' . strtoupper($this->route))
                )
            );
        }

        // If there is a manifest script, let's copy it.
        if ($this->manifest_script) {
            // First, we have to create a folder for the script if one isn't present
            if (!file_exists($this->parent->getPath('extension_root'))) {
                Folder::create($this->parent->getPath('extension_root'));
            }

            $path         = [];
            $path['src']  = $this->parent->getPath('source') . '/' . $this->manifest_script;
            $path['dest'] = $this->parent->getPath('extension_root') . '/' . $this->manifest_script;

            if ($this->parent->isOverwrite() || !file_exists($path['dest'])) {
                if (!$this->parent->copyFiles([$path])) {
                    // Install failed, rollback changes
                    throw new \RuntimeException(
                        Text::sprintf(
                            'JLIB_INSTALLER_ABORT_MANIFEST',
                            Text::_('JLIB_INSTALLER_' . strtoupper($this->route))
                        )
                    );
                }
            }
        }
    }

    /**
     * Method to finalise the uninstallation processing
     *
     * @return  boolean
     *
     * @since   4.0.0
     * @throws  \RuntimeException
     */
    protected function finaliseUninstall(): bool
    {
        $manifest = JPATH_MANIFESTS . '/files/' . $this->extension->element . '.xml';

        if (is_file($manifest)) {
            File::delete($manifest);
        }

        $extensionId = $this->extension->extension_id;

        $db = $this->getDatabase();

        // Remove the schema version
        $query = $db->createQuery()
            ->delete('#__schemas')
            ->where('extension_id = :extension_id')
            ->bind(':extension_id', $extensionId, ParameterType::INTEGER);
        $db->setQuery($query);
        $db->execute();

        // Clobber any possible pending updates
        $update = new Update($this->getDatabase());
        $uid    = $update->find(
            [
                'element' => $this->extension->element,
                'type'    => $this->type,
            ]
        );

        if ($uid) {
            $update->delete($uid);
        }

        $this->extension->delete();

        return true;
    }

    /**
     * Get the filtered extension element from the manifest
     *
     * @param   string  $element  Optional element name to be converted
     *
     * @return  string  The filtered element
     *
     * @since   3.4
     */
    public function getElement($element = null)
    {
        if (!$element) {
            $manifestPath = Path::clean($this->parent->getPath('manifest', ''));
            $element      = preg_replace('/\.xml/', '', basename($manifestPath));
        }

        return $element;
    }

    /**
     * Custom loadLanguage method
     *
     * @param   string  $path  The path on which to find language files.
     *
     * @return  void
     *
     * @since   3.1
     */
    public function loadLanguage($path)
    {
        $extension = 'files_' . strtolower(str_replace('files_', '', $this->getElement()));

        $this->doLoadLanguage($extension, $path, JPATH_SITE);
    }

    /**
     * Method to parse optional tags in the manifest
     *
     * @return  void
     *
     * @since   3.4
     */
    protected function parseOptionalTags()
    {
        // Parse optional tags
        $this->parent->parseLanguages($this->getManifest()->languages);
    }

    /**
     * Removes this extension's files
     *
     * @return  void
     *
     * @since   4.0.0
     * @throws  \RuntimeException
     */
    protected function removeExtensionFiles()
    {
        // Loop through all elements and get list of files and folders
        foreach ($this->getManifest()->fileset->files as $eFiles) {
            $target = (string) $eFiles->attributes()->target;

            // Create folder path
            if (empty($target)) {
                $targetFolder = JPATH_ROOT;
            } else {
                $targetFolder = JPATH_ROOT . '/' . $target;
            }

            $folderList = [];

            // Check if all children exists
            if (\count($eFiles->children()) > 0) {
                // Loop through all filenames elements
                foreach ($eFiles->children() as $eFileName) {
                    if ($eFileName->getName() === 'folder') {
                        $folderList[] = $targetFolder . '/' . $eFileName;
                    } else {
                        $fileName = $targetFolder . '/' . $eFileName;

                        if (is_file($fileName)) {
                            File::delete($fileName);
                        }
                    }
                }
            }

            // Delete any folders that don't have any content in them.
            foreach ($folderList as $folder) {
                $files = Folder::files($folder);

                if ($files !== false && !\count($files)) {
                    Folder::delete($folder);
                }
            }
        }

        // Lastly, remove the extension_root
        $folder = $this->parent->getPath('extension_root');

        if (is_dir(Path::clean($folder))) {
            Folder::delete($folder);
        }

        $this->parent->removeFiles($this->getManifest()->languages);
    }

    /**
     * Method to do any prechecks and setup the install paths for the extension
     *
     * @return  void
     *
     * @since   3.4
     */
    protected function setupInstallPaths()
    {
        // Set the file root path
        if ($this->name === 'files_joomla') {
            // If we are updating the Joomla core, set the root path to the root of Joomla
            $this->parent->setPath('extension_root', JPATH_ROOT);
        } else {
            $this->parent->setPath('extension_root', JPATH_MANIFESTS . '/files/' . $this->element);
        }
    }

    /**
     * Method to do any prechecks and setup the uninstall job
     *
     * @return  void
     *
     * @since   4.0.0
     */
    protected function setupUninstall()
    {
        $manifestFile = JPATH_MANIFESTS . '/files/' . $this->extension->element . '.xml';
        $this->parent->setPath('manifest', $manifestFile);

        // Because libraries may not have their own folders we cannot use the standard method of finding an installation manifest
        if (!file_exists($manifestFile)) {
            // Remove this row entry since its invalid
            $this->extension->delete($this->extension->extension_id);

            throw new \RuntimeException(Text::_('JLIB_INSTALLER_ERROR_FILE_UNINSTALL_INVALID_NOTFOUND_MANIFEST'));
        }

        // Set the files root path
        $this->parent->setPath('extension_root', JPATH_MANIFESTS . '/files/' . $this->extension->element);

        // Set the source path for compatibility with the API
        $this->parent->setPath('source', $this->parent->getPath('extension_root'));

        $xml = simplexml_load_file($manifestFile);

        // If we cannot load the XML file return null
        if (!$xml) {
            throw new \RuntimeException(Text::_('JLIB_INSTALLER_ERROR_FILE_UNINSTALL_LOAD_MANIFEST'));
        }

        // Check for a valid XML root tag.
        if ($xml->getName() !== 'extension') {
            throw new \RuntimeException(Text::_('JLIB_INSTALLER_ERROR_FILE_UNINSTALL_INVALID_MANIFEST'));
        }

        $this->setManifest($xml);

        // Attempt to load the language file; might have uninstall strings
        $this->loadLanguage(JPATH_MANIFESTS . '/files');
    }

    /**
     * Method to store the extension to the database
     *
     * @return  void
     *
     * @since   3.4
     * @throws  \RuntimeException
     */
    protected function storeExtension()
    {
        if ($this->currentExtensionId) {
            // Load the entry and update the manifest_cache
            $this->extension->load($this->currentExtensionId);

            // Update name
            $this->extension->name = $this->name;

            // Update manifest
            $this->extension->manifest_cache = $this->parent->generateManifestCache();

            if (!$this->extension->store()) {
                // Install failed, roll back changes
                throw new \RuntimeException(
                    Text::sprintf(
                        'JLIB_INSTALLER_ABORT_ROLLBACK',
                        Text::_('JLIB_INSTALLER_' . strtoupper($this->route)),
                        $this->extension->getError()
                    )
                );
            }
        } else {
            // Add an entry to the extension table with a whole heap of defaults
            $this->extension->name         = $this->name;
            $this->extension->type         = 'file';
            $this->extension->element      = $this->element;
            $this->extension->changelogurl = $this->changelogurl;

            // There is no folder for files so leave it blank
            $this->extension->folder    = '';
            $this->extension->enabled   = 1;
            $this->extension->protected = 0;
            $this->extension->access    = 0;
            $this->extension->client_id = 0;
            $this->extension->params    = '';

            // Update the manifest cache for the entry
            $this->extension->manifest_cache = $this->parent->generateManifestCache();

            if (!$this->extension->store()) {
                // Install failed, roll back changes
                throw new \RuntimeException(
                    Text::sprintf(
                        'JLIB_INSTALLER_ABORT_ROLLBACK',
                        Text::_('JLIB_INSTALLER_' . strtoupper($this->route)),
                        $this->extension->getError()
                    )
                );
            }

            // Since we have created a module item, we add it to the installation step stack
            // so that if we have to rollback the changes we can undo it.
            $this->parent->pushStep(['type' => 'extension', 'extension_id' => $this->extension->extension_id]);
        }
    }

    /**
     * Function used to check if extension is already installed
     *
     * @param   string  $extension  The element name of the extension to install
     *
     * @return  boolean  True if extension exists
     *
     * @since   3.1
     */
    protected function extensionExistsInSystem($extension = null)
    {
        // Get a database connector object
        $db = $this->getDatabase();

        $query = $db->createQuery()
            ->select($db->quoteName('extension_id'))
            ->from($db->quoteName('#__extensions'))
            ->where($db->quoteName('type') . ' = ' . $db->quote('file'))
            ->where($db->quoteName('element') . ' = :extension')
            ->bind(':extension', $extension);
        $db->setQuery($query);

        try {
            $db->execute();
        } catch (\RuntimeException) {
            // Install failed, rollback changes - error logged by the installer
            return false;
        }

        $id = $db->loadResult();

        if (empty($id)) {
            return false;
        }

        return true;
    }

    /**
     * Function used to populate files and folder list
     *
     * @return  boolean  none
     *
     * @since   3.1
     */
    protected function populateFilesAndFolderList()
    {
        // Initialise variable
        $this->folderList = [];
        $this->fileList   = [];

        // Set root folder names
        $packagePath = $this->parent->getPath('source');
        $jRootPath   = Path::clean(JPATH_ROOT);

        // Loop through all elements and get list of files and folders
        foreach ($this->getManifest()->fileset->files as $eFiles) {
            // Check if the element is files element
            $folder = (string) $eFiles->attributes()->folder;
            $target = (string) $eFiles->attributes()->target;

            // Split folder names into array to get folder names. This will help in creating folders
            $arrList = preg_split("#/|\\/#", $target);

            $folderName = $jRootPath;

            foreach ($arrList as $dir) {
                if (empty($dir)) {
                    continue;
                }

                $folderName .= '/' . $dir;

                // Check if folder exists, if not then add to the array for folder creation
                if (!is_dir(Path::clean($folderName))) {
                    $this->folderList[] = $folderName;
                }
            }

            // Create folder path
            $sourceFolder = empty($folder) ? $packagePath : $packagePath . '/' . $folder;
            $targetFolder = empty($target) ? $jRootPath : $jRootPath . '/' . $target;

            // Check if source folder exists
            if (!is_dir(Path::clean($sourceFolder))) {
                Log::add(Text::sprintf('JLIB_INSTALLER_ABORT_FILE_INSTALL_FAIL_SOURCE_DIRECTORY', $sourceFolder), Log::WARNING, 'jerror');

                // If installation fails, rollback
                $this->parent->abort();

                return false;
            }

            // Check if all children exists
            if (\count($eFiles->children())) {
                // Loop through all filenames elements
                foreach ($eFiles->children() as $eFileName) {
                    $path         = [];
                    $path['src']  = $sourceFolder . '/' . $eFileName;
                    $path['dest'] = $targetFolder . '/' . $eFileName;
                    $path['type'] = 'file';

                    if ($eFileName->getName() === 'folder') {
                        $folderName         = $targetFolder . '/' . $eFileName;
                        $this->folderList[] = $folderName;
                        $path['type']       = 'folder';
                    }

                    $this->fileList[] = $path;
                }
            } else {
                $files = Folder::files($sourceFolder);

                foreach ($files as $file) {
                    $path         = [];
                    $path['src']  = $sourceFolder . '/' . $file;
                    $path['dest'] = $targetFolder . '/' . $file;

                    $this->fileList[] = $path;
                }
            }
        }

        return true;
    }

    /**
     * Refreshes the extension table cache
     *
     * @return  boolean result of operation, true if updated, false on failure
     *
     * @since   3.1
     */
    public function refreshManifestCache()
    {
        // Need to find to find where the XML file is since we don't store this normally
        $manifestPath           = JPATH_MANIFESTS . '/files/' . $this->parent->extension->element . '.xml';
        $this->parent->manifest = $this->parent->isManifest($manifestPath);
        $this->parent->setPath('manifest', $manifestPath);

        $manifest_details                        = Installer::parseXMLInstallFile($this->parent->getPath('manifest'));
        $this->parent->extension->manifest_cache = json_encode($manifest_details);
        $this->parent->extension->name           = $manifest_details['name'];
        $this->parent->extension->changelogurl   = $manifest_details['changelogurl'];

        try {
            return $this->parent->extension->store();
        } catch (\RuntimeException) {
            Log::add(Text::_('JLIB_INSTALLER_ERROR_PACK_REFRESH_MANIFEST_CACHE'), Log::WARNING, 'jerror');

            return false;
        }
    }
}
