<?php

namespace WebPConvert\Helpers;

use WebPConvert\Helpers\Sanitize;
use WebPConvert\Exceptions\SanityException;

class SanityCheck
{

    /**
     *
     *  @param  string  $input  string to test for NUL char
     */
    public static function mustBeString($input, $errorMsg = 'String expected')
    {
        if (gettype($input) !== 'string') {
            throw new SanityException($errorMsg);
        }
        return $input;
    }

    /**
     *  The NUL character is a demon, because it can be used to bypass other tests
     *  See https://st-g.de/2011/04/doing-filename-checks-securely-in-PHP.
     *
     *  @param  string  $input  string to test for NUL char
     */
    public static function noNUL($input, $errorMsg = 'NUL character is not allowed')
    {
        self::mustBeString($input);
        if (strpos($input, chr(0)) !== false) {
            throw new SanityException($errorMsg);
        }
        return $input;
    }

    /**
     *  Prevent control chararters (#00 - #20).
     *
     *  This prevents line feed, new line, tab, charater return, tab, ets.
     *  https://www.rapidtables.com/code/text/ascii-table.html
     *
     *  @param  string  $input  string to test for control characters
     */
    public static function noControlChars($input)
    {
        self::mustBeString($input);
        self::noNUL($input);
        if (preg_match('#[\x{0}-\x{1f}]#', $input)) {
            throw new SanityException('Control characters are not allowed');
        }
        return $input;
    }


    /**
     *
     *  @param  mixed  $input  something that may not be empty
     */
    public static function notEmpty($input, $errorMsg = 'Must be non-empty')
    {
        if (empty($input)) {
            throw new SanityException($input);
        }
        return $input;
    }



    public static function noDirectoryTraversal($input, $errorMsg = 'Directory traversal is not allowed')
    {
        self::mustBeString($input);
        self::noControlChars($input);
        if (preg_match('#\.\.\/#', $input)) {
            throw new SanityException($errorMsg);
        }
        return $input;
    }

    public static function noStreamWrappers($input, $errorMsg = 'Stream wrappers are not allowed')
    {
        self::mustBeString($input);
        self::noControlChars($input);

        // Prevent stream wrappers ("phar://", "php://" and the like)
        // https://www.php.net/manual/en/wrappers.phar.php
        if (preg_match('#^\\w+://#', Sanitize::removeNUL($input))) {
            throw new SanityException($errorMsg);
        }
        return $input;
    }

    public static function path($input)
    {
        self::notEmpty($input);
        self::mustBeString($input);
        self::noControlChars($input);
        self::noDirectoryTraversal($input);
        self::noStreamWrappers($input);
        return $input;
    }

    public static function pathWithoutDirectoryTraversal($input)
    {
        return self::path($input);
    }

    public static function absPathMicrosoftStyle($input, $errorMsg = 'Not an fully qualified Windows path')
    {
        // On microsoft we allow [drive letter]:\
        if (!preg_match("#^[A-Z]:\\\\|/#", $input)) {
            throw new SanityException($errorMsg . ':' . $input);
        }
        return $input;
    }

    public static function absPath($input, $errorMsg = 'Not an absolute path')
    {
        if ((strpos($input, '/') !== 0)) {

            // Check if we are on Microsoft
            $onMicrosoft = false;
            if (isset($_SERVER['SERVER_SOFTWARE'])) {
                if (strpos(strtolower($_SERVER['SERVER_SOFTWARE']), 'microsoft') !== false) {
                    $onMicrosoft = true;
                }
            }
            switch (PHP_OS) {
                case "WINNT":
                case "WIN32":
                case "INTERIX":
                case "UWIN":
                case "UWIN-W7":
                    $onMicrosoft = true;
                    break;
            }

            if (!$onMicrosoft) {
                throw new SanityException($errorMsg . ':' . $input);
            }
            self::absPathMicrosoftStyle($input);

        }
        return self::path($input);
    }

    public static function pathBeginsWith($input, $beginsWith, $errorMsg = 'Path is outside allowed path')
    {
        self::path($input);
        if (!(strpos($input, $beginsWith) === 0)) {
            throw new SanityException($errorMsg);
        }
        return $input;
    }

    public static function findClosestExistingFolderSymLinksExpanded($input) {
        $levelsUp = 1;
        //echo 'input:' . $input;
        while (true) {
            $dir = dirname($input, $levelsUp);
            //echo 'dir:' . $dir . '<br>';
            $realPathResult = realpath($dir);
            if ($realPathResult !== false) {
                return $realPathResult;
            }
            if (($dir == '/') || (strlen($dir) < 4)) {
                return $dir;
            }
            $levelsUp++;
        }
        return '/';
    }

    public static function pathBeginsWithSymLinksExpanded($input, $beginsWith, $errorMsg = 'Path is outside allowed path') {
        $closestExistingFolder = self::findClosestExistingFolderSymLinksExpanded($input);
        //throw new SanityException('hm.' . $input . ' : <br>' . $closestExistingFolder);
        self::pathBeginsWith($closestExistingFolder, $beginsWith, $errorMsg);
    }



    public static function absPathExists($input, $errorMsg = 'Path does not exist')
    {
        self::absPath($input);
        if (@!file_exists($input)) {
            throw new SanityException($errorMsg);
        }
        return $input;
    }

    public static function absPathExistsAndIsDir(
        $input,
        $errorMsg = 'Path points to a file (it should point to a directory)'
    ) {
        self::absPathExists($input);
        if (!is_dir($input)) {
            throw new SanityException($errorMsg);
        }
        return $input;
    }

    public static function absPathExistsAndIsFile(
        $input,
        $errorMsg = 'Path points to a directory (it should not do that)'
    ) {
        self::absPathExists($input, 'File does not exist');
        if (@is_dir($input)) {
            throw new SanityException($errorMsg);
        }
        return $input;
    }

    public static function absPathExistsAndIsNotDir(
        $input,
        $errorMsg = 'Path points to a directory (it should point to a file)'
    ) {
        self::absPathExistsAndIsFile($input, $errorMsg);
        return $input;
    }


    public static function pregMatch($pattern, $input, $errorMsg = 'Does not match expected pattern')
    {
        self::noNUL($input);
        self::mustBeString($input);
        if (!preg_match($pattern, $input)) {
            throw new SanityException($errorMsg);
        }
        return $input;
    }

    public static function isJSONArray($input, $errorMsg = 'Not a JSON array')
    {
        self::noNUL($input);
        self::mustBeString($input);
        self::notEmpty($input);
        if ((strpos($input, '[') !== 0) || (!is_array(json_decode($input)))) {
            throw new SanityException($errorMsg);
        }
        return $input;
    }

    public static function isJSONObject($input, $errorMsg = 'Not a JSON object')
    {
        self::noNUL($input);
        self::mustBeString($input);
        self::notEmpty($input);
        if ((strpos($input, '{') !== 0) || (!is_object(json_decode($input)))) {
            throw new SanityException($errorMsg);
        }
        return $input;
    }

}
