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

use Joomla\CMS\Factory;
use Joomla\CMS\Filesystem\Folder;

defined('_JEXEC') || die;

class AtsystemFeatureCriticalfiles extends AtsystemFeatureAbstract
{
	protected $loadOrder = 999;

	/**
	 * Is this feature enabled?
	 *
	 * @return bool
	 */
	public function isEnabled()
	{
		return ($this->cparams->getValue('criticalfiles', 0) == 1);
	}

	public function onAfterRender()
	{
		$mustSaveData = false;

		$criticalFiles = $this->getCriticalFiles();
		$loadedFiles   = $this->load();
		$alteredFiles  = [];
		$filesToSave   = [];

		foreach ($criticalFiles as $relPath)
		{
			$curInfo = $this->getFileInfo($relPath);

			if ($curInfo == false)
			{
				// Did that file exist? If so, we need to save the critical files list.
				if (is_array($loadedFiles) && array_key_exists($relPath, $loadedFiles))
				{
					$mustSaveData = true;
				}

				continue;
			}

			$filesToSave[$relPath] = $curInfo;

			// Did the file change?
			$oldInfo = null;

			if (isset($loadedFiles[$relPath]))
			{
				$oldInfo = $loadedFiles[$relPath];
			}

			if ($oldInfo !== $curInfo)
			{
				$mustSaveData = true;

				$alteredFiles[$relPath] = [$oldInfo, $curInfo];
			}
		}

		if ($mustSaveData)
		{
			$this->save($filesToSave);
		}

		// Send out the email only if we have some altered files AND we have some previous data (otherwise we will scary
		// the user as soon as they enable the feature)
		if (!empty($alteredFiles) && $loadedFiles)
		{
			$this->sendEmail($alteredFiles);
		}
	}

	/**
	 * Get the critical files, i.e. the files which get most commonly hacked in Joomla: configuration.php, the index.php
	 * files in front- and backend and the index.php, error.php and component.php files of the installed templates.
	 *
	 * @return  array  The list of critical files (relative paths)
	 */
	protected function getCriticalFiles()
	{
		$criticalFiles = [
			'configuration.php',
			'index.php',
			'administrator/index.php',
		];

		$templateFiles = ['index.php', 'error.php', 'component.php'];
		$templates     = Folder::folders(JPATH_SITE . '/templates');

		if (is_array($templates) && !empty($templates))
		{
			foreach ($templates as $template)
			{
				foreach ($templateFiles as $templateFile)
				{
					$relPath = 'templates/' . $template . '/' . $templateFile;

					if (file_exists(JPATH_SITE . '/' . $relPath))
					{
						$criticalFiles[] = $relPath;
					}
				}
			}
		}

		$templates = Folder::folders(JPATH_ADMINISTRATOR . '/templates');

		if (is_array($templates) && !empty($templates))
		{
			foreach ($templates as $template)
			{
				foreach ($templateFiles as $templateFile)
				{
					$relPath = 'templates/' . $template . '/' . $templateFile;

					if (file_exists(JPATH_ADMINISTRATOR . '/' . $relPath))
					{
						$criticalFiles[] = $relPath;
					}
				}
			}
		}

		return $criticalFiles;
	}

	/**
	 * Returns information about a file
	 *
	 * @param   string  $relPath  The path to the file relative to the site's root
	 *
	 * @return  null|array  Null if the file is not there, object with information otherwise
	 */
	protected function getFileInfo($relPath)
	{
		$absolutePath = JPATH_SITE . '/' . $relPath;

		if (!file_exists($absolutePath))
		{
			return null;
		}

		return [
			'size'      => @filesize($absolutePath),
			'timestamp' => filemtime($absolutePath),
			'md5'       => @md5_file($absolutePath),
			'sha1'      => @sha1_file($absolutePath),
		];
	}

	/**
	 * Save the critical file information to the database
	 *
	 * @param   array  $fileList  The list of critical file information
	 *
	 * @return  void
	 */
	protected function save(array $fileList)
	{
		$db   = $this->container->db;
		$data = json_encode($fileList);

		$query = $db->getQuery(true)
			->delete($db->quoteName('#__admintools_storage'))
			->where($db->quoteName('key') . ' = ' . $db->quote('criticalfiles'));
		$db->setQuery($query);
		$db->execute();

		$object = (object) [
			'key'   => 'criticalfiles',
			'value' => $data,
		];

		$db->insertObject('#__admintools_storage', $object);
	}

	/**
	 * Load the critical file information from the database
	 *
	 * @return  array
	 */
	protected function load()
	{
		$db    = $this->container->db;
		$query = $db->getQuery(true)
			->select($db->quoteName('value'))
			->from($db->quoteName('#__admintools_storage'))
			->where($db->quoteName('key') . ' = ' . $db->quote('criticalfiles'));
		$db->setQuery($query);

		$error = 0;

		try
		{
			$jsonData = $db->loadResult();
		}
		catch (Exception $e)
		{
			$error = $e->getCode();
		}

		if (method_exists($db, 'getErrorNum') && $db->getErrorNum())
		{
			$error = $db->getErrorNum();
		}

		if ($error)
		{
			$jsonData = null;
		}

		if (empty($jsonData))
		{
			return [];
		}

		return json_decode($jsonData, true);
	}

	/**
	 * Sends a warning email to the addresses set up to receive security exception emails
	 *
	 * @param   array  $alteredFiles  The files which were modified
	 *
	 * @return  void
	 */
	private function sendEmail($alteredFiles)
	{
		if (empty($alteredFiles))
		{
			// What are you doing here? There are no altered files.
			return;
		}

		// Load the component's administrator translation files
		$jlang = Factory::getLanguage();
		$jlang->load('com_admintools', JPATH_ADMINISTRATOR, 'en-GB', true);
		$jlang->load('com_admintools', JPATH_ADMINISTRATOR, $jlang->getDefault(), true);
		$jlang->load('com_admintools', JPATH_ADMINISTRATOR, null, true);

		// Convert the list of modified files to HTML
		$htmlAlteredFiles = <<< HTML
<ul>
HTML;

		foreach ($alteredFiles as $fileName => $fileSet)
		{
			if (empty($fileName))
			{
				continue;
			}

			$htmlAlteredFiles .= <<< HTML
	<li>
		$fileName
	</li>
HTML;

		}

		$htmlAlteredFiles .= <<< HTML
</ul>

HTML;

		// Construct the replacement table
		$substitutions = $this->exceptionsHandler->getEmailVariables('', [
			'[INFO]' => $htmlAlteredFiles,
		]);

		// Let's get the most suitable email template
		$template = $this->exceptionsHandler->getEmailTemplate('criticalfiles', true);

		// Got no template, the user didn't published any email template, or the template doesn't want us to
		// send a notification email. Anyway, let's stop here.
		if (!$template)
		{
			return;
		}

		$subject = $template[0];
		$body    = $template[1];

		foreach ($substitutions as $k => $v)
		{
			$subject = str_replace($k, $v, $subject);
			$body    = str_replace($k, $v, $body);
		}

		try
		{
			$config = $this->container->platform->getConfig();
			$mailer = Factory::getMailer();

			$mailfrom = $config->get('mailfrom');
			$fromname = $config->get('fromname');

			$recipients = explode(',', $this->cparams->getValue('emailbreaches', ''));
			$recipients = array_map('trim', $recipients);

			foreach ($recipients as $recipient)
			{
				if (empty($recipient))
				{
					continue;
				}

				// This line is required because SpamAssassin is BROKEN
				$mailer->Priority = 3;

				$mailer->isHtml(true);
				$mailer->setSender([$mailfrom, $fromname]);

				// Resets the recipients, otherwise they will pile up
				$mailer->clearAllRecipients();

				if ($mailer->addRecipient($recipient) === false)
				{
					// Failed to add a recipient?
					continue;
				}

				$mailer->setSubject($subject);
				$mailer->setBody($body);
				$mailer->Send();
			}
		}
		catch (Exception $e)
		{
			// Joomla! 3.5 and later throw an exception when crap happens instead of suppressing it and returning false
		}
	}

}

© 2025 Cubjrnet7