<?php
/**
 * @version     backend/classes/checkfiles.php 2015-08-03 14:37:00 UTC Ch
 * @authorUrl   https://watchful.li
 * @copyright   Copyright (c) 2012-2015 watchful.li
 * @license     GNU/GPL v3 or later
 * @copyright   Copyright (c) 2012-2015 watchful.li
 * @license     GNU/GPL v3 or later
 */
defined('_JEXEC') or die('Restricted access');
jimport('joomla.filesystem.file');
class WatchfulliFilesScanner extends WatchfulliAuditProcess
{
    const MINIMUMFOLDERSPERMISSION = 755;
    const MINIMUMFILESPERMISSION = 655;
    /**
     *
     * @var \stdClass
     */
    private $structure;
    /**
     *
     * @var array
     */
    private $hashes;
    /**
     *
     * @var array
     */
    private $nonCoreFiles;
    
    public function __construct()
    {
        parent::__construct();
        $this->structure = $this->cache->call(array('WatchfulliRecursiveListing', 'getStructure'), JPATH_SITE);
        $this->hashes = $this->cache->call(array('WatchfulliConnection', 'getHash'));
        $this->nonCoreFiles = $this->cache->call(array('WatchfulliRecursiveListing', 'getNonCoreFiles'), $this->structure, $this->hashes);
    }

    /**
     * Compare the hashes of the core files
     * 
     * @param int $start
     * @return \stdClass
     */
    public function auditFolderPermissions($start)
    {
        $folders = $this->structure->dirs;

        $result = new stdClass();
        $result->wrong = array(); // files with wrong permission
        $result->size = count($folders);
        $result->start = $start;

        $current = $start;
        while ($this->haveTime() && $current < count($folders) - 1)
        {
            $permission = $this->checkPermissions($folders[$current]);
            if ($permission > self::MINIMUMFOLDERSPERMISSION)
            {
                $result->wrong[] = array($folders[$current], $permission);
            }
            $current++;
        }
        $result->lastFolderChecked = $folders[$current];
        $result->end = $current;
        return $result;
    }

    /**
     *
     * @param int $start
     * @return \stdClass
     */
    public function auditMalwareScanner($start = 0)
    {
        $files = $this->nonCoreFiles;
        $result = new stdClass();
        $result->wrong = array(); // files with problems
        $result->size = count($files);
        $result->start = $start;

        $current = $start;

        while ($this->haveTime() && $current < count($files) - 1)
        {
            $check = $this->checkSignatures($files[$current]);
            if ($check)
            {
                $result->wrong[] = $check;
            }
            $current++;
        }

        $result->lastFileChecked = $files[$current];
        $result->end = $current;

        return $result;
    }

    /**
     *
     * @param int $start
     * @return \stdClass
     */
    public function auditFilesPermissions($start = 0)
    {
        $files = $this->structure->files;
        $result = new stdClass();
        $result->wrong = array(); // files with wrong permission
        $result->size = count($files);
        $result->start = $start;

        $current = $start;
        while ($this->haveTime() && $current < $result->size - 1)
        {
            $permission = $this->checkPermissions($files[$current]);
            if ($permission >= self::MINIMUMFILESPERMISSION)
            {
                $pathFromRoot = str_replace(JPATH_BASE, '', $files[$current]);
                $result->wrong[] = array($pathFromRoot, $permission);
            }
            $current++;
        }


        $result->lastFileChecked = $files[$current];
        $result->end = $current;

        return $result;
    }

    /**
     * 
     * @param string $file
     * @return boolean
     */
    private function checkSignatures($file)
    {
        $signatures = $this->cache->call(array('WatchfulliConnection', 'getSignatures'));

        if (!$this->needToCheckThisFile($file))
        {
            return false;
        }

        $contents = JFile::read($file);
        $fileExtension = JFile::getExt($file);
        $pathFromRoot = str_replace(JPATH_BASE, '', $file);

        foreach ($signatures as $signature)
        {
            if ($signature->type == 'regex' && $fileExtension == 'php')
            {
                if (preg_match_all('#(\{|\(|\s|\/\*.*\*\/|@|^)' . $signature->signature . '#i', $contents, $matches))
                {
                    return array('path' => $pathFromRoot, 'match' => $matches[0], 'reason' => $signature->reason, 'signature_id' => $signature->id);
                }
            }
            elseif ($signature->type == 'regex-htaccess' && $fileExtension == 'htaccess')
            {
                if (preg_match_all('#(\{|\(|\s|\/\*.*\*\/|@|^)' . $signature->signature . '#i', $contents, $matches))
                {
                    return array('path' => $pathFromRoot, 'match' => $matches[0], 'reason' => $signature->reason, 'signature_id' => $signature->id);
                }
            }
            elseif ($signature->type == 'filename')
            {
                if (preg_match('#' . $signature->signature . '#i', basename($file), $match))
                {
                    return array('path' => $pathFromRoot, 'match' => $match[0], 'reason' => $signature->reason, 'signature_id' => $signature->id);
                }
            }
        }

        return false;
    }

    /**
     * 
     * @param string $path
     * @return boolean
     */
    private function checkPermissions($path)
    {
        if (!is_readable($path))
        {
            return false;
        }

        return substr(decoct(@fileperms($path)), -3);
    }

    /**
     * 
     * @param string $path
     * @return boolean
     */
    private function needToCheckThisFile($path)
    {

        $safeExtensions = array(
            'DS_Store', 'zip', 'gzip', 'gz', 'doc',
            'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'pdf', 'rtf', 'mno', 'ashx', 'png',
            'psd', 'wott', 'ttf', 'css', 'swf', 'flv', 'po', 'mo', 'mov', 'qt', 'pot',
            'eot', 'ini', 'svg', 'mpeg', 'mvk', 'mp3', 'less', 'sql', 'wsdl', 'woff',
            'xml', 'php_expire', 'jpa');

        $excludePaths = array(
            '%/akeeba_json.php',
            '%/akeeba_backend.id%.php',
            '%/akeeba_backend.php',
            '%/akeeba_backend.id%.log',
            '%/akeeba_backend.log',
            '%/akeeba_lazy.php',
            '%/akeeba_frontend.php',
            '%/stats/webalizer.current',
            '%/stats/usage_%.html',
            '%/components/libraries/cmslib/cache/cache__%',
            '%/cache/com_watchfulli%',
            '%/plugins/system/akgeoip/lib/vendor/guzzle/guzzle/%');

        $path_parts = pathinfo($path);

        //If file deleted after caching
        if (!file_exists($path))
        {
            return false;
        }

        foreach ($excludePaths as $excludePath)
        {
            $excludeRegex = $this->generateRegex($excludePath);

            if (preg_match('#' . $excludeRegex . '#', $path_parts['dirname']))
            {
                return false;
            }
        }

        //Not check files > 2Mo
        if (filesize($path) > 2097152)
        {
            return false;
        }

        if (isset($path_parts['extension']) && in_array($path_parts['extension'], $safeExtensions))
        {
            return false;
        }

        return true;
    }

    /**
     * Generate Regex from path
     * Input %/akeeba_json.php
     * Output .*\/akeeba_json\.php
     *
     * @param string Path to be converted
     * @return string Regex
     */
    private function generateRegex($path)
    {
        $patterns = array(
            '#\.#',
            '#/#',
            '#%#'
        );

        $replacements = array(
            '\.',
            '\/',
            '.*'
        );

        return preg_replace($patterns, $replacements, $path);
    }

}