<?php

/**
 * @package         Smile Pack
 * @version         2.1.5-RC4 Free
 *
 * @author          Tassos Marinos <info@tassos.gr>
 * @link            http://www.tassos.gr
 * @copyright       Copyright © 2026 Tassos Marinos All Rights Reserved
 * @license         GNU GPLv3 <http://www.gnu.org/licenses/gpl.html> or later
*/

namespace SmilePack\Helpers;

defined('_JEXEC') or die('Restricted access');

use Joomla\CMS\Factory;
use Joomla\Database\ParameterType;
use Joomla\Filesystem\Folder;
use Joomla\Filesystem\Path;
use Joomla\Registry\Registry;
use NRFramework\File;
use NRFramework\Helpers\Widgets\GalleryManager2;
use NRFramework\Security\SafePath;

/**
 * Handles the files of the Gallery module when the module is copied or deleted.
 *
 * Joomla copies and deletes the module row without knowing anything about the files the
 * module references, which leaves copied modules sharing the original's images and
 * deleted modules leaving their images behind.
 */
class GalleryFiles
{
    /**
     * The item keys holding a file path.
     *
     * @var  array
     */
    private static $file_keys = ['source', 'image', 'thumbnail', 'slideshow'];

    /**
     * Returns whether the module is being copied, through the "Duplicate" button, the
     * "Save as Copy" button or the "Batch > Copy" action.
     *
     * @return  bool
     */
    public static function isCopying()
    {
        $input = Factory::getApplication()->input;

        /**
         * Joomla strips the controller while routing, so "modules.duplicate" reaches us
         * as "duplicate". Match on the task itself to cover both forms.
         */
        $task = $input->getCmd('task', '');
        $task = strpos($task, '.') === false ? $task : substr(strrchr($task, '.'), 1);

        if (in_array($task, ['duplicate', 'save2copy']))
        {
            return true;
        }

        $batch = $input->post->get('batch', [], 'array');

        return $task === 'batch' && isset($batch['move_copy']) && $batch['move_copy'] === 'c';
    }

    /**
     * Copies every file the module references into the module's own upload folder and
     * rewrites the paths in the given params.
     *
     * @param   int    $module_id  The module the copies should belong to.
     * @param   array  $params     The decoded module params, by reference.
     *
     * @return  bool  True when at least one path changed.
     */
    public static function duplicateFiles($module_id, &$params)
    {
        $module_id = (int) $module_id;

        if (!$module_id || !$items = self::getItems($params))
        {
            return false;
        }

        $destination_folder = rtrim(self::getDestinationFolder($module_id, $params), DIRECTORY_SEPARATOR);

        $changed = false;

        foreach ($items as &$item)
        {
            if (!is_array($item))
            {
                continue;
            }

            foreach (self::$file_keys as $key)
            {
                if (empty($item[$key]) || !is_string($item[$key]))
                {
                    continue;
                }

                if (!$new_path = self::copyFile($item[$key], $destination_folder))
                {
                    // Keep the original reference. A shared file beats a broken image.
                    continue;
                }

                $item[$key] = $new_path;
                $changed = true;
            }
        }

        unset($item);

        if (!$changed)
        {
            return false;
        }

        $params['value']['items'] = $items;

        return true;
    }

    /**
     * Deletes every file the module references, its upload folder and its temp folder.
     *
     * @param   int    $module_id
     * @param   array  $params     The decoded module params.
     *
     * @return  void
     */
    public static function deleteFiles($module_id, $params)
    {
        $module_id = (int) $module_id;

        if (!$module_id)
        {
            return;
        }

        foreach (self::getItems($params) as $item)
        {
            if (!is_array($item))
            {
                continue;
            }

            foreach (self::$file_keys as $key)
            {
                if (empty($item[$key]) || !is_string($item[$key]))
                {
                    continue;
                }

                if (!$file = self::toAbsolute($item[$key]))
                {
                    continue;
                }

                try
                {
                    // File::delete() confines the path to the site root and the temp folder
                    File::delete($file);
                }
                catch (\Throwable $e)
                {
                }
            }
        }

        self::deleteFolders($module_id, $params);
    }

    /**
     * Persists the module params without going through Table::store().
     *
     * @param   int    $module_id
     * @param   array  $params
     *
     * @return  string  The encoded params that were stored.
     */
    public static function updateModuleParams($module_id, $params)
    {
        $encoded = json_encode($params);
        $module_id = (int) $module_id;

        $db = Factory::getDbo();
        $query = $db->getQuery(true)
            ->update($db->quoteName('#__modules'))
            ->set($db->quoteName('params') . ' = :params')
            ->where($db->quoteName('id') . ' = :id')
            ->bind(':params', $encoded)
            ->bind(':id', $module_id, ParameterType::INTEGER);

        $db->setQuery($query);
        $db->execute();

        return $encoded;
    }

    /**
     * Returns the module's upload folder.
     *
     * @param   int    $module_id
     * @param   mixed  $params     The module params as an array or a Registry.
     *
     * @return  string  Absolute path with a trailing directory separator.
     */
    public static function getDestinationFolder($module_id, $params)
    {
        $ds = DIRECTORY_SEPARATOR;
        $destination_folder = ['media', 'spgallery', (int) $module_id];

        

        return Path::clean(implode($ds, array_merge([JPATH_ROOT], $destination_folder)) . $ds);
    }

    /**
     * Copies a single file into the module's upload folder.
     *
     * The Gallery Manager keeps sources under a md5('source') folder, full images under
     * "full" and slideshow thumbnails under "thumb", so that sub folder is preserved.
     * Thumbnails live in the folder root and have none.
     *
     * @param   string  $relative_path       The stored file path.
     * @param   string  $destination_folder  Absolute path, no trailing separator.
     *
     * @return  string  The new site relative path, or an empty string on failure.
     */
    private static function copyFile($relative_path, $destination_folder)
    {
        if (!$file = self::toAbsolute($relative_path))
        {
            return '';
        }

        if (!is_file($file))
        {
            return '';
        }

        // Files still in the temp folder are moved into place by the save routine
        if (SafePath::isWithinLexical($file, rtrim(File::getTempFolder(), DIRECTORY_SEPARATOR)))
        {
            return '';
        }

        $ds = DIRECTORY_SEPARATOR;

        $sub_folder = basename(dirname($file));

        if (!in_array($sub_folder, [md5('source'), 'full', 'thumb'], true))
        {
            $sub_folder = '';
        }

        $target = implode($ds, array_filter([$destination_folder, $sub_folder, basename($file)]));

        try
        {
            // Not replacing existing files, so File::uniquefy() appends _copy_X on collision
            $new_file = File::copy($file, $target);
        }
        catch (\Throwable $e)
        {
            return '';
        }

        return $new_file ? ltrim(str_replace(JPATH_ROOT, '', $new_file), $ds) : '';
    }

    /**
     * Removes the module's upload folder and its temp folder.
     *
     * The upload folder is removed only when it is the automatic, per module one. A
     * custom folder is set by the user and may hold unrelated files.
     *
     * @param   int    $module_id
     * @param   array  $params
     *
     * @return  void
     */
    private static function deleteFolders($module_id, $params)
    {
        $ds = DIRECTORY_SEPARATOR;

        $folders = [
            GalleryManager2::getFullTempFolder('module', '', (int) $module_id)
        ];

        $automatic_folder = Path::clean(implode($ds, [JPATH_ROOT, 'media', 'spgallery', (int) $module_id]));

        if (rtrim(self::getDestinationFolder($module_id, $params), $ds) === $automatic_folder)
        {
            $folders[] = $automatic_folder;
        }

        foreach ($folders as $folder)
        {
            if (!$folder || !is_dir($folder))
            {
                continue;
            }

            Folder::delete($folder);
        }
    }

    /**
     * Resolves a stored path to an absolute one, rejecting traversal sequences, absolute
     * paths and anything landing outside the site root.
     *
     * @param   string  $relative_path
     *
     * @return  string  The absolute path, or an empty string when it cannot be trusted.
     */
    private static function toAbsolute($relative_path)
    {
        if (!is_string($relative_path) || trim($relative_path) === '')
        {
            return '';
        }

        if (SafePath::hasTraversal($relative_path) || SafePath::isAbsolute($relative_path))
        {
            return '';
        }

        $file = SafePath::toAbsolute($relative_path);

        return SafePath::isWithinLexical($file, JPATH_ROOT) ? $file : '';
    }

    /**
     * Returns the gallery items held in the module params.
     *
     * @param   mixed  $params
     *
     * @return  array
     */
    private static function getItems($params)
    {
        if (!is_array($params) || !isset($params['value']['items']) || !is_array($params['value']['items']))
        {
            return [];
        }

        return $params['value']['items'];
    }
}
