<?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 Joomla\CMS\Application\SiteApplication;
use Joomla\CMS\Event\ErrorEvent;
use Joomla\CMS\Factory;
use Joomla\CMS\Uri\Uri;
use Joomla\Database\DatabaseDriver;
use Joomla\Database\DatabaseInterface;
use Joomla\Event\Event;
use Throwable;

class Shield404 extends Base
{
	private static $blockedUrls;

	/**
	 * Is this feature enabled?
	 *
	 * @return bool
	 */
	public function isEnabled()
	{
		// Assign those values to our static variables, so we can reference to them in the static context
		static::$blockedUrls = $this->wafParams->getValue('404shield', [
			"wp-admin.php", "wp-login.php", "wp-content/*", "wp-admin/*",
		]);

		// Double check we truly have an array
		static::$blockedUrls = is_array(static::$blockedUrls) ? static::$blockedUrls : explode("\n", static::$blockedUrls);

		// Remove descriptions
		static::$blockedUrls = array_map(function ($x) {
			return is_array($x) ? $x[0] : $x;
		}, static::$blockedUrls);

		return ($this->wafParams->getValue('404shield_enable', 1));
	}

	public function onError(Event $event)
	{
		// Make sure we are handling the correct event here
		if (!($event instanceof ErrorEvent))
		{
			return;
		}

		// Get the event arguments
		$error = $event->getError();
		$app   = $event->getApplication();

		if (!($error instanceof Throwable))
		{
			return;
		}

		if ($app->isClient('administrator') || ((int) $error->getCode() !== 404))
		{
			return;
		}

		$blockedURLs = array_map('trim', static::$blockedUrls);
		$root        = Uri::root();
		$currentURL  = Uri::getInstance();
		$currentPath = $currentURL->toString(['scheme', 'host', 'port', 'path']);

		// Remove the root from the current path so we can work with relative URLs
		$currentPath = str_replace($root, '', $currentPath);
		$currentPath = $this->removeLanguageTag($currentPath);
		$currentPath = trim($currentPath, '/');

		$block = false;

		foreach ($blockedURLs as $blockPattern)
		{
			$shouldNegate = false;

			// If the pattern starts with a !, we're going to negate the assumption
			if (substr($blockPattern, 0, 1) == '!')
			{
				$blockPattern = substr($blockPattern, 1);
				$shouldNegate = true;
			}

			$blockPattern = trim($blockPattern, '/');

			$match = fnmatch($blockPattern, $currentPath);

			// Should I invert the result?
			if ($shouldNegate)
			{
				$match = !$match;
			}

			$block = $match;

			// No need to continue if we have to block the request
			if ($block)
			{
				break;
			}
		}

		if ($block)
		{
			$this->exceptionsHandler->logRequest('404shield');
		}
	}

	/**
	 * Removes the language tag from the URLs generated by multilanguage sites.
	 *
	 * @param $pathURL
	 *
	 * @return string
	 */
	private function removeLanguageTag($pathURL)
	{
		/** @var SiteApplication $app */
		$app               = $this->app;
		$hasLanguageFilter = false;

		if (method_exists($app, 'getLanguageFilter'))
		{
			$hasLanguageFilter = $app->getLanguageFilter();
		}

		if (!$hasLanguageFilter)
		{
			return $pathURL;
		}

		/** @var DatabaseDriver $db */
		$db = $this->db;

		// Let's get the list of SEF code used in the URLs of the published languages
		$query     = (method_exists($db, 'createQuery') ? $db->createQuery() : $db->getQuery(true))
			->select($db->qn('sef'))
			->from($db->qn('#__languages'))
			->where($db->qn('published') . ' = ' . $db->q('1'));
		$languages = $db->setQuery($query)->loadColumn();

		foreach ($languages as $lang)
		{
			$lang .= '/';

			// Replace only the starting string
			if (strpos($pathURL, $lang) !== 0)
			{
				continue;
			}

			$pathURL = substr($pathURL, strlen($lang));

			// There can be only one language tag in the URL, so if we get here it means that there's nothing left to do
			break;
		}

		return $pathURL;
	}
}
