<?php
/**
 * @package   admintools
 * @copyright Copyright (c)2010-2026 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 3, or later
 */

namespace Akeeba\Plugin\System\AdminTools\Feature;

defined('_JEXEC') || die;

use Exception;
use Joomla\CMS\Factory;
use Joomla\Database\Exception\ConnectionFailureException;
use Joomla\Database\Exception\ExecutionFailureException;
use Joomla\Database\Pdo\PdoDriver;
use mysqli;
use function mysqli_fetch_object;

/**
 * @deprecated 8.0  Use the Joomla Scheduled Tasks instead
 */
class SessionOptimiser extends Base
{
	/**
	 * Is this feature enabled?
	 *
	 * @return bool
	 */
	public function isEnabled()
	{
		return ($this->params->get('sesoptimizer', 0) == 1);
	}

	public function onAfterInitialise(): void
	{
		$minutes = (int) $this->params->get('sesopt_freq', 0);

		if ($minutes <= 0)
		{
			return;
		}

		$lastJob = $this->getTimestamp('session_optimize');
		$nextJob = $lastJob + $minutes * 60;

		$now = clone Factory::getDate();

		if ($now->toUnix() >= $nextJob)
		{
			$this->setTimestamp('session_optimize');
			$this->sessionOptimize();
		}
	}

	/**
	 * Optimizes the session table. The idea is that as users log in and out,
	 * vast amounts of records are created and deleted, slowly fragmenting the
	 * underlying database file and slowing down user session operations. At
	 * some point, your site might even crash. By doing a periodic optimization
	 * of the sessions table this is prevented. An optimization per hour should
	 * be adequate, even for huge sites.
	 *
	 * Note: this is not necessary if you're not using the database to save
	 * session data. Using disk files, memcache, APC or other alternative caches
	 * has no impact on your database performance. In this case you should not
	 * enable this option, as you have nothing to gain.
	 */
	private function sessionOptimize()
	{
		$db     = $this->db;
		$dbName = $db->getName();

		// Make sure this is MySQL
		if (!in_array(strtolower($dbName), ['mysql', 'mysqli', 'pdomysql']))
		{
			return;
		}

		try
		{
			$result = $this->executeUnpreparedGetObjectList('CHECK TABLE ' . $db->quoteName('#__session'));
		}
		catch (Exception $e)
		{
			return;
		}

		$isOK = false;

		if (!empty($result))
		{
			foreach ($result as $row)
			{
				if (($row->Msg_type == 'status')
				    && (
					    ($row->Msg_text == 'OK')
					    || ($row->Msg_text == 'Table is already up to date')
				    )
				)
				{
					$isOK = true;
				}
			}
		}

		// Run a repair only if it is required
		if (!$isOK)
		{
			try
			{
				$this->executeUnpreparedQuery('REPAIR TABLE ' . $db->quoteName('#__session'));
			}
			catch (Exception $e)
			{
				return;
			}
		}

		// Finally, optimize
		try
		{
			$this->executeUnpreparedQuery('OPTIMIZE TABLE ' . $db->quoteName('#__session'));
		}
		catch (Exception $e)
		{
			return;
		}
	}

	/**
	 * Executes an unprepared SQL statement.
	 *
	 * The PDO driver doesn't distinguish between prepared and unprepared statements. Therefore we can just run anything
	 * we please. The MySQLi driver, however, has a distinction between prepared and unprepared statements. We cannot
	 * run certain SQL comments (such as OPTIMIZE and REPAIR) over a prepared statement. The MySQLi driver has a handy
	 * method called executeUnpreparedStatement which is protected and which runs this kind of statements.
	 *
	 * This here method tries to figure out if the database driver object has that method and use it instead of the
	 * prepared statement.
	 *
	 * @param $sql
	 *
	 * @return bool|mixed
	 */
	private function executeUnpreparedGetObjectList($sql)
	{
		$sql        = $this->db->replacePrefix($sql);
		$connection = $this->db->getConnection();

		if (($this->db instanceof PdoDriver) || !($connection instanceof mysqli))
		{
			return $this->db->setQuery($sql)->loadObjectList();
		}

		$cursor = $connection->query($sql);

		// If an error occurred handle it.
		if (!$cursor)
		{
			$errorNum = (int) $connection->errno;
			$errorMsg = (string) $connection->error;

			// Check if the server was disconnected.
			if (!$this->db->connected())
			{
				try
				{
					// Attempt to reconnect.
					$connection = null;
					$this->db->connect();
				}
				catch (ConnectionFailureException $e)
				{
					throw new ExecutionFailureException($sql, $errorMsg, $errorNum);
				}

				// Since we were able to reconnect, run the query again.
				return $this->executeUnpreparedGetObjectList($sql);
			}

			// The server was not disconnected.
			throw new ExecutionFailureException($sql, $errorMsg, $errorNum);
		}

		$result = [];

		while ($row = mysqli_fetch_object($cursor))
		{
			$result[] = $row;
		}

		$cursor->free_result();

		return $result;
	}

	/**
	 * Executes an unprepared SQL statement.
	 *
	 * The PDO driver doesn't distinguish between prepared and unprepared statements. Therefore we can just run anything
	 * we please. The MySQLi driver, however, has a distinction between prepared and unprepared statements. We cannot
	 * run certain SQL comments (such as OPTIMIZE and REPAIR) over a prepared statement. The MySQLi driver has a handy
	 * method called executeUnpreparedStatement which is protected and which runs this kind of statements.
	 *
	 * This here method tries to figure out if the database driver object has that method and use it instead of the
	 * prepared statement.
	 *
	 * @param $sql
	 *
	 * @return bool|mixed
	 */
	private function executeUnpreparedQuery($sql)
	{
		$sql    = $this->db->replacePrefix($sql);
		$db     = $this->db;
		$refObj = new \ReflectionObject($db);

		try
		{
			$method = $refObj->getMethod('executeUnpreparedQuery');

			if (version_compare(PHP_VERSION, '8.1.0', 'lt'))
			{
				$method->setAccessible(true);
			}

			return $method->invoke($db, $sql);
		}
		catch (\ReflectionException $e)
		{
			return $db->setQuery($sql)->execute();
		}
	}

}
