<?php
/**
 * @version     backend/classes/joomlaaudit.php 2015-08-26 07:25:00 UTC Ch
 * @authorUrl   https://watchful.li
 * @copyright   Copyright (c) 2012-2015 watchful.li
 * @license     GNU/GPL v3 or later
 */
defined('_JEXEC') or die('Restricted access');
class WatchfulliJoomlaAudit extends WatchfulliAuditProcess
{
    /**
     *
     * @var \stdClass
     */
    private $structure;

    public function __construct()
    {
        parent::__construct();
        $this->loadPasswords();
        $this->db = JFactory::getDBO();
        $this->app = JFactory::getApplication();
        $this->response = new WatchfulliScannerResponse();
        $this->config = new JConfig();
        $this->structure = $this->cache->call(array('WatchfulliRecursiveListing', 'getStructure'), JPATH_SITE);
    }

    /**
     * Check a value in the configurations of Joomla
     * @param type $key
     * @param type $expectedValue
     * @param string $comparaison
     * @return class
     */
    public function checkConfigValue($key, $expectedValue, $comparaison = '==')
    {
        return $this->checkValue($this->config->$key, $expectedValue, $comparaison);
    }

    /**
     * Compare two values and return the correct status
     * @param type $value
     * @param type $expectedValue
     * @param enum $comparaison ==,<,>,<=,>=
     * @return type
     */
    public function checkValue($value, $expectedValue, $comparaison = '==')
    {
        $map = array(
            ">=" => $value >= $expectedValue,
            ">" => $value > $expectedValue,
            "<=" => $value <= $expectedValue,
            "<" => $value < $expectedValue,
            "==" => $value == $expectedValue,
            "!=" => $value != $expectedValue,
        );

        if ($map[$comparaison])
        {
            return $this->response->sendOk();
        }
        return $this->response->sendKo();
    }

    /**
     * Check if the sql passorow is weak
     * @return type
     */
    public function checkSQLPassword()
    {
        $password = $this->checkWeakPassword($this->config->password);
        if ($password)
        {
            return $this->response->sendKo($password);
        }
        return $this->response->sendOk();
    }

    /**
     * Check if a user have 'admin' as username
     * @return type
     */
    public function hasAdminUser()
    {
        $db = $this->db;
        $query = 'SELECT id'
                . ' FROM #__users '
                . ' WHERE username =' . $db->quote('admin')
                . ' AND block =' . $db->quote('0');

        $db->setQuery($query);

        if ($db->loadResult())
        {
            return $this->response->sendKo($db->loadResult());
        }
        return $this->response->sendOk();
    }

    /**
     * Check the integrity of configuration.php 
     * @return type
     */
    public function isConfigurationModified()
    {
        jimport('joomla.filesystem.file');
        $contents = JFile::read(JPATH_CONFIGURATION . '/configuration.php');
        $configuration = $this->buildConfiguration();

        if ($contents != $configuration)
        {
            $contents = explode("\n", $contents);
            $configuration = explode("\n", $configuration);
            $diff = array_diff($contents, $configuration);
            return $this->response->sendKo($diff);
        }

        return $this->response->sendOk();
    }

    /**
     * Check if a user with admin right use a weak password
     * @return type
     */
    public function checkAdminPasswords()
    {
        $users = $this->getAdminUsers();
        $return = array();
        foreach ($users as $user)
        {
            foreach ($this->passwords as $password)
            {
                if ($this->IsPasswordDecoded($user->password, $password))
                {
                    $return[] = array($user->username, $password);
                }
            }
        }

        if (count($return))
        {
            return $this->response->sendKo($return);
        }
        return $this->response->sendOk();
    }

    /**
     * Check if an .htaccess or web.config file exist
     * @return type
     */
    public function hasHtaccess()
    {
        $file = '.htaccess';
        if (preg_match('#IIS/([\d.]*)#', $_SERVER['SERVER_SOFTWARE']))
        {
            $file = 'web.config'; // IIS
        }

        if (!file_exists(JPATH_SITE . '/' . $file))
        {
            return $this->response->sendKo();
        }
        return $this->response->sendOk();
    }

    /**
     * Check if an other Joomla is located in a subdirectory.
     * @return type
     */
    public function checkJoomlaInSubdirectory()
    {
        $files = $this->structure->files;
        $paths = array();

        $escapedBasePath = preg_replace(array('#\/#', '#\.#'), array('\/', '\.'), JPATH_SITE);
        $pattern = '#^' . $escapedBasePath . '\/([a-z0-9_\-\.\s]*\/){1,2}configuration\.php$#i';

        foreach ($files as $file)
        {
            if (preg_match($pattern, $file) && $this->isAJoomlaConfigFile($file))
            {
                $relativePath = str_replace(JPATH_BASE, '', $file);
                $paths[] = preg_replace('#configuration.php$#', '', $relativePath);
            }
        }

        if (count($paths))
        {
            return $this->response->sendKo($paths);
        }
        return $this->response->sendOk();
    }

    /**
     * Check if the robots.txt file authorise access to :
     *  /
     *  /templates
     *  /media
     * 
     * @return type
     */
    public function checkRobotsTxt()
    {
        $filePath = JPATH_BASE . '/robots.txt';

        if (!file_exists($filePath))
        {
            return $this->response->sendOk();
        }

        $matches = array();
        $tempMatches = array();
        $signatures = $this->cache->call(array('WatchfulliConnection', 'getSignatures'));
        $content = JFile::read($filePath);

        foreach ($signatures as $signature)
        {
            if ($signature->type != 'regex-robotstxt')
            {
                continue;
            }

            if (preg_match_all($signature->signature, $content, $tempMatches))
            {
                $matches = array_merge($matches, $tempMatches[0]);
            }
        }

        if (empty($matches))
        {
            return $this->response->sendOk();
        }

        return $this->response->sendKo($matches);
    }

    /**
     * Get all users with admin right
     * @return array of objects
     */
    private function getAdminUsers()
    {
        $admin_groups = $this->getAdminGroups();

        $query = 'SELECT u.username, u.password '
                . 'FROM #__user_usergroup_map m '
                . 'RIGHT JOIN #__users u ON (u.id=m.user_id) '
                . 'WHERE m.group_id IN (' . implode(',', $admin_groups) . ') '
                . 'GROUP BY u.id '
                . 'ORDER BY u.username ASC';
        $this->db->setQuery($query);
        $results = $this->db->loadObjectList();

        return $results;
    }

    /**
     * Get all usergroups with admin right
     * @return type
     */
    private function getAdminGroups()
    {
        if (Watchfulli::joomla()->RELEASE == '1.5')
        {
            $this->db->setQuery('SELECT id FROM #__groups');
            $groups = $this->db->loadResultArray();
        }
        else
        {
            $query = $this->db->getQuery(true);
            $query->select($this->db->quoteName('id'))
                    ->from($this->db->quoteName('#__usergroups'));
            $this->db->setQuery($query);
            $groups = $this->db->loadColumn();
        }

        $admin_groups = array();
        foreach ($groups as $group_id)
        {
            if (JAccess::checkGroup($group_id, 'core.login.admin'))
            {
                $admin_groups[] = $group_id;
            }
            elseif (JAccess::checkGroup($group_id, 'core.admin'))
            {
                $admin_groups[] = $group_id;
            }
        }

        return array_unique($admin_groups);
    }

    /**
     * Build a string representation of the configuration.php file
     * @return type
     */
    private function buildConfiguration()
    {
        $data = JArrayHelper::fromObject(new JConfig());
        $config = new JRegistry('config');
        $config->loadArray($data);

        if (Watchfulli::joomla()->RELEASE == '1.5')
        {
            return $config->toString('PHP', null, array('class' => 'JConfig'));
        }

        return $config->toString('PHP', array('class' => 'JConfig', 'closingtag' => false));
    }

    /**
     * Compare an encrypted password with a reference and try to decrypt
     * @param type $encryptedPassword
     * @param type $reference
     * @return boolean
     */
    private function IsPasswordDecoded($encryptedPassword, $reference)
    {
        if (substr($encryptedPassword, 0, 4) == '$2y$')
        {
            return false; // Cracking these passwor§ is extremely CPU intensive, skip.
        }

        $salt = '';
        $parts = explode(':', $encryptedPassword);
        $crypt = $parts[0];
        if (array_key_exists(1, $parts))
        {
            $salt = $parts[1];
        }
        if (substr($encryptedPassword, 0, 8) == '{SHA256}')
        {
            $testcrypt = JUserHelper::getCryptedPassword($reference, $salt, 'sha256', false);
            return ($encryptedPassword == $testcrypt);
        }

        $testcrypt = JUserHelper::getCryptedPassword($reference, $salt, 'md5-hex', false);
        return ($crypt == $testcrypt);
    }

    /**
     * Load a list of password, with cache
     */
    private function loadPasswords()
    {
        $cache = JFactory::getCache();
        $cache->setCaching(6 * 3600);
        $contentRaw = $cache->call(array('WatchfulliConnection', 'getPasswords'));
        $contents = str_replace(array("\r\n", "\r"), "\n", $contentRaw->data);
        $passwords = explode("\n", $contents);
        $this->passwords = $passwords;
    }

    /**
     * Check if a password is in the list of week passport
     * @param type $original
     * @return boolean
     */
    private function checkWeakPassword($original)
    {
        foreach ($this->passwords as $password)
        {
            if ($original == $password)
            {
                return $password;
            }
        }
        return false;
    }

    /**
     * Check if the file are the Joomla config file
     * @param string $filePath File
     * @return boolean
     */
    private function isAJoomlaConfigFile($filePath)
    {
        $content = JFile::read($filePath);
        $pattern = '#^<\?php[.\s]*class[.\s]*JConfig#';
        return preg_match($pattern, $content);
    }

}