<?php

/**
 * Part of the Joomla Framework Filter Package
 *
 * @copyright  Copyright (C) 2005 - 2021 Open Source Matters, Inc. All rights reserved.
 * @license    GNU General Public License version 2 or later; see LICENSE
 */

namespace Joomla\Filter;

use Joomla\String\StringHelper;

/**
 * InputFilter is a class for filtering input from any data source
 *
 * Forked from the php input filter library by: Daniel Morris <dan@rootcube.com>
 * Original Contributors: Gianpaolo Racca, Ghislain Picard, Marco Wandschneider, Chris Tobin and Andrew Eddie.
 *
 * @since  1.0
 */
class InputFilter
{
    /**
     * Defines the InputFilter instance should only allow the supplied list of HTML tags.
     *
     * @var    integer
     * @since  1.4.0
     */
    public const ONLY_ALLOW_DEFINED_TAGS = 0;

    /**
     * Defines the InputFilter instance should block the defined list of HTML tags and allow all others.
     *
     * @var    integer
     * @since  1.4.0
     */
    public const ONLY_BLOCK_DEFINED_TAGS = 1;

    /**
     * Defines the InputFilter instance should only allow the supplied list of attributes.
     *
     * @var    integer
     * @since  1.4.0
     */
    public const ONLY_ALLOW_DEFINED_ATTRIBUTES = 0;

    /**
     * Defines the InputFilter instance should block the defined list of attributes and allow all others.
     *
     * @var    integer
     * @since  1.4.0
     */
    public const ONLY_BLOCK_DEFINED_ATTRIBUTES = 1;

    /**
     * The array of permitted tags.
     *
     * @var    array
     * @since  1.0
     */
    public $tagsArray;

    /**
     * The array of permitted tag attributes.
     *
     * @var    array
     * @since  1.0
     */
    public $attrArray;

    /**
     * The method for sanitising tags
     *
     * @var    integer
     * @since  1.0
     */
    public $tagsMethod;

    /**
     * The method for sanitising attributes
     *
     * @var    integer
     * @since  1.0
     */
    public $attrMethod;

    /**
     * A flag for XSS checks. Only auto clean essentials = 0, Allow clean blocked tags/attr = 1
     *
     * @var    integer
     * @since  1.0
     */
    public $xssAuto;

    /**
     * The list the blocked tags for the instance.
     *
     * @var    string[]
     * @since  1.0
     */
    public $blockedTags = [
        'applet',
        'body',
        'bgsound',
        'base',
        'basefont',
        'canvas',
        'embed',
        'frame',
        'frameset',
        'head',
        'html',
        'id',
        'iframe',
        'ilayer',
        'layer',
        'link',
        'meta',
        'name',
        'object',
        'script',
        'style',
        'title',
        'xml',
    ];

    /**
     * The list of blocked tag attributes for the instance.
     *
     * @var    string[]
     * @since  1.0
     */
    public $blockedAttributes = [
        'action',
        'background',
        'codebase',
        'dynsrc',
        'formaction',
        'lowsrc',
    ];

    /**
     * A special list of blocked characters.
     *
     * @var    string[]
     * @since  1.3.3
     */
    private $blockedChars = [
        '&tab;',
        '&space;',
        '&colon;',
        '&column;',
    ];

    /**
     * Constructor for InputFilter class.
     *
     * @param   array    $tagsArray   List of permitted HTML tags
     * @param   array    $attrArray   List of permitted HTML tag attributes
     * @param   integer  $tagsMethod  Method for filtering tags, should be one of the `ONLY_*_DEFINED_TAGS` constants
     * @param   integer  $attrMethod  Method for filtering attributes, should be one of the `ONLY_*_DEFINED_ATTRIBUTES` constants
     * @param   integer  $xssAuto     Only auto clean essentials = 0, Allow clean blocked tags/attributes = 1
     *
     * @since   1.0
     */
    public function __construct(
        array $tagsArray = [],
        array $attrArray = [],
        $tagsMethod = self::ONLY_ALLOW_DEFINED_TAGS,
        $attrMethod = self::ONLY_ALLOW_DEFINED_ATTRIBUTES,
        $xssAuto = 1
    ) {
        // Make sure user defined arrays are in lowercase
        $tagsArray = array_map('strtolower', (array) $tagsArray);
        $attrArray = array_map('strtolower', (array) $attrArray);

        // Assign member variables
        $this->tagsArray  = $tagsArray;
        $this->attrArray  = $attrArray;
        $this->tagsMethod = $tagsMethod;
        $this->attrMethod = $attrMethod;
        $this->xssAuto    = $xssAuto;
    }

    /**
     * Cleans the given input source based on the instance configuration and specified data type
     *
     * @param   string|string[]|object  $source  Input string/array-of-string/object to be 'cleaned'
     * @param   string                  $type    The return type for the variable:
     *                                           INT:       An integer
     *                                           UINT:      An unsigned integer
     *                                           FLOAT:     A floating point number
     *                                           BOOLEAN:   A boolean value
     *                                           WORD:      A string containing A-Z or underscores only (not case sensitive)
     *                                           ALNUM:     A string containing A-Z or 0-9 only (not case sensitive)
     *                                           CMD:       A string containing A-Z, 0-9, underscores, periods or hyphens (not case
     *                                                      sensitive)
     *                                           BASE64:    A string containing A-Z, 0-9, forward slashes, plus or equals (not case
     *                                                      sensitive)
     *                                           STRING:    A fully decoded and sanitised string (default)
     *                                           HTML:      A sanitised string
     *                                           ARRAY:     An array
     *                                           PATH:      A sanitised file path
     *                                           TRIM:      A string trimmed from normal, non-breaking and multibyte spaces
     *                                           USERNAME:  Do not use (use an application specific filter)
     *                                           RAW:       The raw string is returned with no filtering
     *                                           unknown:   An unknown filter will act like STRING. If the input is an array it will
     *                                                      return an array of fully decoded and sanitised strings.
     *
     * @return  mixed  'Cleaned' version of the `$source` parameter
     *
     * @since   1.0
     */
    public function clean($source, $type = 'string')
    {
        $type = ucfirst(strtolower($type));

        if ($type === 'Array') {
            return (array) $source;
        }

        if ($type === 'Raw') {
            return $source;
        }

        if (\is_array($source)) {
            $result = [];

            foreach ($source as $key => $value) {
                $result[$key] = $this->clean($value, $type);
            }

            return $result;
        }

        if (\is_object($source)) {
            foreach (get_object_vars($source) as $key => $value) {
                $source->$key = $this->clean($value, $type);
            }

            return $source;
        }

        $method = 'clean' . $type;

        if (method_exists($this, $method)) {
            return $this->$method((string) $source);
        }

        // Unknown filter method
        if (\is_string($source) && !empty($source)) {
            // Filter source for XSS and other 'bad' code etc.
            return $this->cleanString($source);
        }

        // Not an array or string... return the passed parameter
        return $source;
    }

    /**
     * Function to determine if contents of an attribute are safe
     *
     * @param   array  $attrSubSet  A 2 element array for attribute's name, value
     *
     * @return  boolean  True if bad code is detected
     *
     * @since   1.0
     */
    public static function checkAttribute($attrSubSet)
    {
        $attrSubSet[0] = strtolower($attrSubSet[0]);
        $attrSubSet[1] = html_entity_decode(strtolower($attrSubSet[1]), ENT_QUOTES | ENT_HTML401, 'UTF-8');

        // Remove common XSS-evasion characters
        $attrSubSet[1] = str_replace(["\t", "\n", " ", "\0"], "", $attrSubSet[1]);

        return (strpos($attrSubSet[1], 'expression') !== false && $attrSubSet[0] === 'style')
            || preg_match('/(?:(?:java|vb|live)script|behaviour|mocha)(?::|&colon;|&column;)/', $attrSubSet[1]) !== 0;
    }

    /**
     * Internal method to iteratively remove all unwanted tags and attributes
     *
     * @param   string  $source  Input string to be 'cleaned'
     *
     * @return  string  'Cleaned' version of input parameter
     *
     * @since   1.0
     */
    protected function remove($source)
    {
        // Iteration provides nested tag protection
        do {
            $temp   = $source;
            $source = $this->cleanTags($source);
        } while ($temp !== $source);

        return $source;
    }

    /**
     * Internal method to strip a string of disallowed tags
     *
     * @param   string  $source  Input string to be 'cleaned'
     *
     * @return  string  'Cleaned' version of input parameter
     *
     * @since   1.0
     */
    protected function cleanTags($source)
    {
        // First, pre-process this for illegal characters inside attribute values
        $source = $this->escapeAttributeValues($source);

        // In the beginning we don't really have a tag, so everything is postTag
        $preTag       = null;
        $postTag      = $source;
        $currentSpace = false;

        // Setting to null to deal with undefined variables
        $attr = '';

        // Is there a tag? If so it will certainly start with a '<'.
        $tagOpenStart = strpos($source, '<');

        while ($tagOpenStart !== false) {
            // Get some information about the tag we are processing
            $preTag .= substr($postTag, 0, $tagOpenStart);
            $postTag     = substr($postTag, $tagOpenStart);
            $fromTagOpen = substr($postTag, 1);
            $tagOpenEnd  = strpos($fromTagOpen, '>');

            // Check for mal-formed tag where we have a second '<' before the first '>'
            $nextOpenTag = (strlen($postTag) > $tagOpenStart) ? strpos($postTag, '<', $tagOpenStart + 1) : false;

            if (($nextOpenTag !== false) && ($nextOpenTag < $tagOpenEnd)) {
                // At this point we have a mal-formed tag -- remove the offending open
                $postTag      = substr($postTag, 0, $tagOpenStart) . substr($postTag, $tagOpenStart + 1);
                $tagOpenStart = strpos($postTag, '<');

                continue;
            }

            // Let's catch any non-terminated tags and skip over them
            if ($tagOpenEnd === false) {
                $postTag      = substr($postTag, $tagOpenStart + 1);
                $tagOpenStart = strpos($postTag, '<');

                continue;
            }

            // Do we have a nested tag?
            $tagOpenNested = strpos($fromTagOpen, '<');

            if (($tagOpenNested !== false) && ($tagOpenNested < $tagOpenEnd)) {
                $preTag .= substr($postTag, 1, $tagOpenNested);
                $postTag      = substr($postTag, ($tagOpenNested + 1));
                $tagOpenStart = strpos($postTag, '<');

                continue;
            }

            // Let's get some information about our tag and setup attribute pairs
            $tagOpenNested = (strpos($fromTagOpen, '<') + $tagOpenStart + 1);
            $currentTag    = substr($fromTagOpen, 0, $tagOpenEnd);
            $tagLength     = strlen($currentTag);
            $tagLeft       = $currentTag;
            $attrSet       = [];
            $currentSpace  = strpos($tagLeft, ' ');

            // Are we an open tag or a close tag?
            if (substr($currentTag, 0, 1) === '/') {
                // Close Tag
                $isCloseTag    = true;
                list($tagName) = explode(' ', $currentTag);
                $tagName       = substr($tagName, 1);
            } else {
                // Open Tag
                $isCloseTag    = false;
                list($tagName) = explode(' ', $currentTag);
            }

            /*
             * Exclude all "non-regular" tagnames
             * OR no tagname
             * OR remove if xssauto is on and tag is blocked
             */
            if (
                (!preg_match('/^[a-z][a-z0-9]*$/i', $tagName))
                || (!$tagName)
                || ((\in_array(strtolower($tagName), $this->blockedTags)) && $this->xssAuto)
            ) {
                $postTag      = substr($postTag, ($tagLength + 2));
                $tagOpenStart = strpos($postTag, '<');

                // Strip tag
                continue;
            }

            /*
             * Time to grab any attributes from the tag... need this section in
             * case attributes have spaces in the values.
             */
            while ($currentSpace !== false) {
                $attr        = '';
                $fromSpace   = substr($tagLeft, ($currentSpace + 1));
                $nextEqual   = strpos($fromSpace, '=');
                $nextSpace   = strpos($fromSpace, ' ');
                $openQuotes  = strpos($fromSpace, '"');
                $closeQuotes = strpos(substr($fromSpace, ($openQuotes + 1)), '"') + $openQuotes + 1;

                $startAtt         = '';
                $startAttPosition = 0;

                // Find position of equal and open quotes ignoring
                if (preg_match('#\s*=\s*\"#', $fromSpace, $matches, \PREG_OFFSET_CAPTURE)) {
                    $stringBeforeAttr = substr($fromSpace, 0, $matches[0][1]);
                    $startAttPosition = strlen($stringBeforeAttr);
                    $startAtt         = $matches[0][0];
                    $closeQuotePos    = strpos(
                        substr($fromSpace, ($startAttPosition + strlen($startAtt))),
                        '"'
                    );
                    $closeQuotes = $closeQuotePos + $startAttPosition + strlen($startAtt);
                    $nextEqual   = $startAttPosition + strpos($startAtt, '=');
                    $openQuotes  = $startAttPosition + strpos($startAtt, '"');
                    $nextSpace   = strpos(substr($fromSpace, $closeQuotes), ' ') + $closeQuotes;
                }

                // Do we have an attribute to process? [check for equal sign]
                if ($fromSpace !== '/' && (($nextEqual && $nextSpace && $nextSpace < $nextEqual) || !$nextEqual)) {
                    if (!$nextEqual) {
                        $attribEnd = strpos($fromSpace, '/') - 1;
                    } else {
                        $attribEnd = $nextSpace - 1;
                    }

                    // If there is an ending, use this, if not, do not worry.
                    if ($attribEnd > 0) {
                        $fromSpace = substr($fromSpace, $attribEnd + 1);
                    }
                }

                if (strpos($fromSpace, '=') !== false) {
                    /*
                     * If the attribute value is wrapped in quotes we need to grab the substring from the closing quote,
                     * otherwise grab until the next space.
                     */
                    if (
                        ($openQuotes !== false)
                        && (strpos(substr($fromSpace, ($openQuotes + 1)), '"') !== false)
                    ) {
                        $attr = substr($fromSpace, 0, ($closeQuotes + 1));
                    } else {
                        $attr = substr($fromSpace, 0, $nextSpace);
                    }
                } else {
                    // No more equal signs so add any extra text in the tag into the attribute array [eg. checked]
                    if ($fromSpace !== '/') {
                        $attr = substr($fromSpace, 0, $nextSpace);
                    }
                }

                // Last Attribute Pair
                if (!$attr && $fromSpace !== '/') {
                    $attr = $fromSpace;
                }

                // Add attribute pair to the attribute array
                $attrSet[] = $attr;

                // Move search point and continue iteration
                $tagLeft      = substr($fromSpace, strlen($attr));
                $currentSpace = strpos($tagLeft, ' ');
            }

            // Is our tag in the user input array?
            $tagFound = \in_array(strtolower($tagName), $this->tagsArray);

            // If the tag is allowed let's append it to the output string.
            if ((!$tagFound && $this->tagsMethod) || ($tagFound && !$this->tagsMethod)) {
                // Reconstruct tag with allowed attributes
                if (!$isCloseTag) {
                    // Open or single tag
                    $attrSet = $this->cleanAttributes($attrSet);
                    $preTag .= '<' . $tagName;

                    for ($i = 0, $count = \count($attrSet); $i < $count; $i++) {
                        $preTag .= ' ' . $attrSet[$i];
                    }

                    // Reformat single tags to XHTML
                    if (strpos($fromTagOpen, '</' . $tagName)) {
                        $preTag .= '>';
                    } else {
                        $preTag .= ' />';
                    }
                } else {
                    // Closing tag
                    $preTag .= '</' . $tagName . '>';
                }
            }

            // Find next tag's start and continue iteration
            $postTag      = substr($postTag, ($tagLength + 2));
            $tagOpenStart = strpos($postTag, '<');
        }

        // Append any code after the end of tags and return
        if ($postTag !== '<') {
            $preTag .= $postTag;
        }

        return $preTag;
    }

    /**
     * Internal method to strip a tag of disallowed attributes
     *
     * @param   array  $attrSet  Array of attribute pairs to filter
     *
     * @return  array  Filtered array of attribute pairs
     *
     * @since   1.0
     */
    protected function cleanAttributes(array $attrSet)
    {
        $newSet = [];

        $count = \count($attrSet);

        // Iterate through attribute pairs
        for ($i = 0; $i < $count; $i++) {
            // Skip blank spaces
            if (!$attrSet[$i]) {
                continue;
            }

            // Split into name/value pairs
            $attrSubSet = explode('=', trim($attrSet[$i]), 2);

            // Take the last attribute in case there is an attribute with no value
            $attrSubSet0   = explode(' ', trim($attrSubSet[0]));
            $attrSubSet[0] = array_pop($attrSubSet0);

            $attrSubSet[0] = strtolower($attrSubSet[0]);
            $quoteStyle    = \ENT_QUOTES | \ENT_HTML401;

            // Remove all spaces as valid attributes does not have spaces.
            $attrSubSet[0] = html_entity_decode($attrSubSet[0], $quoteStyle, 'UTF-8');
            $attrSubSet[0] = preg_replace('/^[\pZ\pC]+|[\pZ\pC]+$/u', '', $attrSubSet[0]);
            $attrSubSet[0] = preg_replace('/\s+/u', '', $attrSubSet[0]);

            // Remove blocked chars from the attribute name
            foreach ($this->blockedChars as $blockedChar) {
                $attrSubSet[0] = str_ireplace($blockedChar, '', $attrSubSet[0]);
            }

            // Remove all symbols
            $attrSubSet[0] = preg_replace('/[^\p{L}\p{N}\-\s]/u', '', $attrSubSet[0]);

            // Remove all "non-regular" attribute names
            // AND blocked attributes
            if (
                (!preg_match('/[a-z]*$/i', $attrSubSet[0]))
                || ($this->xssAuto && ((\in_array(strtolower($attrSubSet[0]), $this->blockedAttributes))
                || substr($attrSubSet[0], 0, 2) == 'on'))
            ) {
                continue;
            }

            // XSS attribute value filtering
            if (!isset($attrSubSet[1])) {
                continue;
            }

            // Remove blocked chars from the attribute value
            foreach ($this->blockedChars as $blockedChar) {
                $attrSubSet[1] = str_ireplace($blockedChar, '', $attrSubSet[1]);
            }

            // Trim leading and trailing spaces
            $attrSubSet[1] = trim($attrSubSet[1]);

            // Strips unicode, hex, etc
            $attrSubSet[1] = str_replace('&#', '', $attrSubSet[1]);

            // Strip normal newline within attr value
            $attrSubSet[1] = preg_replace('/[\n\r]/', '', $attrSubSet[1]);

            // Strip double quotes
            $attrSubSet[1] = str_replace('"', '', $attrSubSet[1]);

            // Convert single quotes from either side to doubles (Single quotes shouldn't be used to pad attr values)
            if ((substr($attrSubSet[1], 0, 1) == "'") && (substr($attrSubSet[1], (\strlen($attrSubSet[1]) - 1), 1) == "'")) {
                $attrSubSet[1] = substr($attrSubSet[1], 1, (\strlen($attrSubSet[1]) - 2));
            }

            // Strip slashes
            $attrSubSet[1] = stripslashes($attrSubSet[1]);

            // Autostrip script tags
            if (static::checkAttribute($attrSubSet)) {
                continue;
            }

            // Is our attribute in the user input array?
            $attrFound = \in_array(strtolower($attrSubSet[0]), $this->attrArray);

            // If the tag is allowed lets keep it
            if ((!$attrFound && $this->attrMethod) || ($attrFound && !$this->attrMethod)) {
                // Does the attribute have a value?
                if (empty($attrSubSet[1]) === false) {
                    $newSet[] = $attrSubSet[0] . '="' . $attrSubSet[1] . '"';
                } elseif ($attrSubSet[1] === '0') {
                    // Special Case
                    // Is the value 0?
                    $newSet[] = $attrSubSet[0] . '="0"';
                } else {
                    // Leave empty attributes alone
                    $newSet[] = $attrSubSet[0] . '=""';
                }
            }
        }

        return $newSet;
    }

    /**
     * Try to convert to plaintext
     *
     * @param   string  $source  The source string.
     *
     * @return  string  Plaintext string
     *
     * @since   1.0
     * @deprecated  This method will be removed once support for PHP 5.3 is discontinued.
     */
    protected function decode($source)
    {
        return html_entity_decode($source, \ENT_QUOTES, 'UTF-8');
    }

    /**
     * Escape < > and " inside attribute values
     *
     * @param   string  $source  The source string.
     *
     * @return  string  Filtered string
     *
     * @since   1.0
     */
    protected function escapeAttributeValues($source)
    {
        $alreadyFiltered = '';
        $remainder       = $source;
        $badChars        = ['<', '"', '>'];
        $escapedChars    = ['&lt;', '&quot;', '&gt;'];

        // Process each portion based on presence of =" and "<space>, "/>, or ">
        // See if there are any more attributes to process
        while (preg_match('#<[^>]*?=\s*?(\"|\')#s', $remainder, $matches, \PREG_OFFSET_CAPTURE)) {
            $stringBeforeTag = substr($remainder, 0, $matches[0][1]);
            $tagPosition     = strlen($stringBeforeTag);

            // Get the character length before the attribute value
            $nextBefore = $tagPosition + strlen($matches[0][0]);

            // Figure out if we have a single or double quote and look for the matching closing quote
            // Closing quote should be "/>, ">, "<space>, or " at the end of the string
            $quote     = substr($matches[0][0], -1);
            $pregMatch = ($quote == '"') ? '#(\"\s*/\s*>|\"\s*>|\"\s+|\"$)#' : "#(\'\s*/\s*>|\'\s*>|\'\s+|\'$)#";

            // Get the portion after attribute value
            $attributeValueRemainder = substr($remainder, $nextBefore);

            if (preg_match($pregMatch, $attributeValueRemainder, $matches, \PREG_OFFSET_CAPTURE)) {
                $stringBeforeQuote = substr($attributeValueRemainder, 0, $matches[0][1]);
                $closeQuoteChars   = strlen($stringBeforeQuote);
                $nextAfter         = $nextBefore + $closeQuoteChars;
            } else {
                // No closing quote
                $nextAfter = strlen($remainder);
            }

            // Get the actual attribute value
            $attributeValue = substr($remainder, $nextBefore, $nextAfter - $nextBefore);

            // Escape bad chars
            $attributeValue = str_replace($badChars, $escapedChars, $attributeValue);
            $attributeValue = $this->stripCssExpressions($attributeValue);
            $alreadyFiltered .= substr($remainder, 0, $nextBefore) . $attributeValue . $quote;
            $remainder = substr($remainder, $nextAfter + 1);
        }

        // At this point, we just have to return the $alreadyFiltered and the $remainder
        return $alreadyFiltered . $remainder;
    }

    /**
     * Remove CSS Expressions in the form of <property>:expression(...)
     *
     * @param   string  $source  The source string.
     *
     * @return  string  Filtered string
     *
     * @since   1.0
     */
    protected function stripCssExpressions($source)
    {
        // Strip any comments out (in the form of /*...*/)
        $test = preg_replace('#\/\*.*\*\/#U', '', $source);

        // Test for :expression
        if (!stripos($test, ':expression')) {
            // Not found, so we are done
            return $source;
        }

        // At this point, we have stripped out the comments and have found :expression
        // Test stripped string for :expression followed by a '('
        if (preg_match_all('#:expression\s*\(#', $test, $matches)) {
            // If found, remove :expression
            return str_ireplace(':expression', '', $test);
        }

        return $source;
    }

    /**
     * Integer filter
     *
     * @param   string  $source  The string to be filtered
     *
     * @return  integer  The filtered value
     */
    private function cleanInt($source)
    {
        $pattern = '/[-+]?[0-9]+/';

        preg_match($pattern, $source, $matches);

        return isset($matches[0]) ? (int) $matches[0] : 0;
    }

    /**
     * Alias for cleanInt()
     *
     * @param   string  $source  The string to be filtered
     *
     * @return  integer  The filtered value
     */
    private function cleanInteger($source)
    {
        return $this->cleanInt($source);
    }

    /**
     * Unsigned integer filter
     *
     * @param   string  $source  The string to be filtered
     *
     * @return  integer  The filtered value
     */
    private function cleanUint($source)
    {
        $pattern = '/[-+]?[0-9]+/';

        preg_match($pattern, $source, $matches);

        return isset($matches[0]) ? abs((int) $matches[0]) : 0;
    }

    /**
     * Float filter
     *
     * @param   string  $source  The string to be filtered
     *
     * @return  float  The filtered value
     */
    private function cleanFloat($source)
    {
        $pattern = '/[-+]?[0-9]+(\.[0-9]+)?([eE][-+]?[0-9]+)?/';

        preg_match($pattern, $source, $matches);

        return isset($matches[0]) ? (float) $matches[0] : 0.0;
    }

    /**
     * Alias for cleanFloat()
     *
     * @param   string  $source  The string to be filtered
     *
     * @return  float  The filtered value
     */
    private function cleanDouble($source)
    {
        return $this->cleanFloat($source);
    }

    /**
     * Boolean filter
     *
     * @param   string  $source  The string to be filtered
     *
     * @return  boolean  The filtered value
     */
    private function cleanBool($source)
    {
        return (bool) $source;
    }

    /**
     * Alias for cleanBool()
     *
     * @param   string  $source  The string to be filtered
     *
     * @return  boolean  The filtered value
     */
    private function cleanBoolean($source)
    {
        return $this->cleanBool($source);
    }

    /**
     * Word filter
     *
     * @param   string  $source  The string to be filtered
     *
     * @return  string  The filtered string
     */
    private function cleanWord($source)
    {
        $pattern = '/[^A-Z_]/i';

        return preg_replace($pattern, '', $source);
    }

    /**
     * Alphanumerical filter
     *
     * @param   string  $source  The string to be filtered
     *
     * @return  string  The filtered string
     */
    private function cleanAlnum($source)
    {
        $pattern = '/[^A-Z0-9]/i';

        return preg_replace($pattern, '', $source);
    }

    /**
     * Command filter
     *
     * @param   string  $source  The string to be filtered
     *
     * @return  string  The filtered string
     */
    private function cleanCmd($source)
    {
        $pattern = '/[^A-Z0-9_\.-]/i';

        $result = preg_replace($pattern, '', $source);
        $result = ltrim($result, '.');

        return $result;
    }

    /**
     * Base64 filter
     *
     * @param   string  $source  The string to be filtered
     *
     * @return  string  The filtered string
     */
    private function cleanBase64($source)
    {
        $pattern = '/[^A-Z0-9\/+=]/i';

        return preg_replace($pattern, '', $source);
    }

    /**
     * String filter
     *
     * @param   string  $source  The string to be filtered
     *
     * @return  string  The filtered string
     */
    private function cleanString($source)
    {
        return $this->remove($this->decode($source));
    }

    /**
     * HTML filter
     *
     * @param   string  $source  The string to be filtered
     *
     * @return  string  The filtered string
     */
    private function cleanHtml($source)
    {
        return $this->remove($source);
    }

    /**
     * Path filter
     *
     * @param   string  $source  The string to be filtered
     *
     * @return  string  The filtered string
     */
    private function cleanPath($source)
    {
        $linuxPattern = '/^[A-Za-z0-9_\/-]+[A-Za-z0-9_\.-]*([\\\\\/]+[A-Za-z0-9_-]+[A-Za-z0-9_\.-]*)*$/';

        if (preg_match($linuxPattern, $source)) {
            return preg_replace('~/+~', '/', $source);
        }

        $windowsPattern = '/^([A-Za-z]:(\\\\|\/))?[A-Za-z0-9_-]+[A-Za-z0-9_\.-]*((\\\\|\/)+[A-Za-z0-9_-]+[A-Za-z0-9_\.-]*)*$/';

        if (preg_match($windowsPattern, $source)) {
            return preg_replace('~(\\\\|\/)+~', '\\', $source);
        }

        return '';
    }

    /**
     * Trim filter
     *
     * @param   string  $source  The string to be filtered
     *
     * @return  string  The filtered string
     */
    private function cleanTrim($source)
    {
        $result = trim($source);
        $result = StringHelper::trim($result, \chr(0xE3) . \chr(0x80) . \chr(0x80));
        $result = StringHelper::trim($result, \chr(0xC2) . \chr(0xA0));

        return $result;
    }

    /**
     * Username filter
     *
     * @param   string  $source  The string to be filtered
     *
     * @return  string  The filtered string
     */
    private function cleanUsername($source)
    {
        $pattern = '/[\x00-\x1F\x7F<>"\'%&]/';

        return preg_replace($pattern, '', $source);
    }
}
