<?php
require_once 'DevComponents.php';

use Firebase\JWT\JWT;
use Defuse\Crypto\Crypto;

class DevTools extends DevComponents {
    
    private $contentType;
    private $cacheControl;
    private $devToken;
    
    private $payload;
    
    private $decodedPayload;
    private $secretKey;
    
    public function __construct(){
        parent::__construct();
        
        $this -> initComponents();
        $this -> setParameters();
        $this -> validateParamValues();
    }
    
    private function initComponents(){
        header("Content-type: text/json");
    }
    
    private function setParameters(){
        $this -> contentType = isset($_SERVER["CONTENT_TYPE"]) 
            ? $_SERVER["CONTENT_TYPE"] : NO_STRING;
        $this -> cacheControl = isset($_SERVER["HTTP_CACHE_CONTROL"]) 
            ? $_SERVER["HTTP_CACHE_CONTROL"] : NO_STRING;
        $this -> devToken = isset($_SERVER["HTTP_DEV_TOKEN"])
            ? $_SERVER["HTTP_DEV_TOKEN"] : NO_STRING;
        
        $this -> payload = file_get_contents("php://input");
    }
    
    private function validateParamValues(){
        $stops = [];
        
        $isDevTokenNotPresent = empty($this -> devToken);
        $isPayloadNotJSONString = !$this -> checkJSONString($this -> payload);
        
        $conditions = [
            [
                "code" => "E01",
                "condition" => empty($this -> contentType),
                "message" => "No Content-Type found"
            ],
            [
                "code" => "E02",
                "condition" => empty($this -> cacheControl),
                "message" => "No Cache-Control found"
            ],
            [
                "code" => "E03",
                "condition" => $_SERVER["REQUEST_METHOD"] != "POST",
                "message" => "Invalid Request Method: {$_SERVER["REQUEST_METHOD"]}"
            ],
            [
                "code" => "E04",
                "condition" => $this -> contentType != "application/x-www-form-urlencoded",
                "message" => "Invalid Content Type: {$this -> contentType}"
            ],
            [
                "code" => "E05",
                "condition" => $isDevTokenNotPresent,
                "message" => "No Dev Token Found"
            ],
            [
                "code" => "E06",
                "condition" => $isPayloadNotJSONString,
                "message" => "Invalid Payload String"
            ]
        ];

        
        if(!$isDevTokenNotPresent && !$isPayloadNotJSONString){
            $this -> decodedPayload = json_decode($this -> payload);
            
            if(isset($this -> decodedPayload -> purpose)){
                switch($this -> decodedPayload -> purpose){
                    case "authentication":{
                        $decodedToken = base64_decode($this -> devToken);
                        
                        if($this -> checkJSONString($decodedToken)){
                            $decodedToken = json_decode($decodedToken);
                            $devCredential = $this -> getDevCred($decodedToken -> devKey);
                            
                            $this -> devCredValidation($conditions, $devCredential, $decodedToken);
                        }
                        break;
                    }
                    default:{
                        try{
                            $decodedToken = base64_decode($this -> devToken);
                            
                            if($this -> checkJSONString($decodedToken)){
                                $decodedToken = json_decode($decodedToken);
                                          
                                $jwtDecodedObject = JWT::decode($decodedToken -> payload, $decodedToken -> secretKey, ["HS256"]);                      
                                
                                $currentTimestamp = strtotime(date("Y-m-d H:i:s"));
                                $payloadTimestamp = strtotime($jwtDecodedObject -> payloadDate);
                               
                                $conditions[] = [
                                    "code" => "E11",
                                    "condition" => $payloadTimestamp < $currentTimestamp,
                                    "message" => "Payload is Expired **{$jwtDecodedObject -> payloadDate}**"
                                ];
                                
                                $devCredential = $this -> getDevCred($jwtDecodedObject -> devKey);
                                $this -> devCredValidation($conditions, $devCredential, $jwtDecodedObject);
                            }
                        }catch(Exception $ex){
                            $conditions[] = [
                                "code" => "E12",
                                "condition" => true,
                                "message" => "JWT Payload Problem Occurred. [Please check the secret key used on the request]"
                            ];
                        }
                        break;
                    }
                }
            }else{
                $conditions[] = [
                    "code" => "E07",
                    "condition" => true,
                    "message" => "'purpose' was not specified"
                ];
            }
        }
        
        foreach($conditions as $verification){
            if($verification["condition"]){
                unset($verification["condition"]);
                $stops[] = $verification;
            }
        }
        
        if(count($stops) != 0){
            die(json_encode($stops));
        }else{
            if(isset($jwtDecodedObject))
                $this -> updateDevCredExecution($jwtDecodedObject -> devKey);
        }
    }
    
    private function devCredValidation(&$conditions, $devCredential, $decodedToken){
        if(!empty($devCredential)){
            $this -> secretKey = $devCredential["dev_secret_key"];
            
            if(isset($devCredential["dev_ip_addresses"])){
                $conditions[] = [
                    "code" => "E08",
                    "condition" => !in_array($_SERVER["REMOTE_ADDR"], explode(",", $devCredential["dev_ip_addresses"])),
                    "message" => "Invalid IP Address: [{$_SERVER["REMOTE_ADDR"]}]"
                ];
            }
            
            if(isset($devCredential["dev_passcode"])){
                $conditions[] = [
                    "code" => "E09",
                    "condition" => !password_verify($decodedToken -> passcode.VAL_STR_AUTH_HASH_SECRET_KEY, $devCredential["dev_passcode"]),
                    "message" => "Incorrect Passcode"
                ];
            }
            
            if(isset($devCredential["dev_expiration"])){
                $credExpiration = strtotime($devCredential["dev_expiration"]);
                $currentDate = strtotime(date("Y-m-d"));
                
                $credExpStr = date("Y-m-d", $credExpiration);
                
                $conditions[] = [
                    "code" => "E13",
                    "condition" => $credExpiration < $currentDate,
                    "message" => "Dev Credential Expired **{$credExpStr}**"
                ];
            }
        }else{
            $conditions[] = [
                "code" => "E10",
                "condition" => true,
                "message" => "Dev Credential [{$decodedToken -> devKey}] doesn't exist"
            ];
        }
    }
    
    private function myPurpose(){
        if(isset($this -> decodedPayload -> cstring) && !empty($this -> decodedPayload -> cstring)){
            try{
                $starttime = microtime(true);
                
                $result = $this -> getResults($this -> decodedPayload -> cstring);
                $rowCount = count($result);
                
                $endtime = microtime(true);
                                
                echo json_encode([
                    "totalNumRows" => $rowCount,
                    "elapsedTime" => $endtime - $starttime,
                    "result" => $result
                ]);
            }catch(Exception $ex){
                echo json_encode([
                    "code" => "MYERR01",
                    "message" => "My-Exception found: {$ex->getMessage()}"
                ]);
            }
        }else{
            echo json_encode([
                "code" => "MYERR02",
                "message" => "cstring is missing or not having a value."
            ]);
        }
    }
    
    private function authPurpose(){
        $decodedToken = json_decode(base64_decode($this -> devToken));
        
        $time = new DateTime(date("Y-m-d H:i:s"));
        // adds 1 minutes to the current time that will be included in the payload
        $time->add(new DateInterval("PT1M")); 
                
        echo json_encode([
            "payload" => JWT::encode([
                "devKey" => $decodedToken -> devKey,
                "passcode" => $decodedToken -> passcode,
                "payloadDate" => $time -> format('Y-m-d H:i:s')
            ], $this -> secretKey),
            "secretKey" => "<put-your-secret-key-right-here>"
        ]);
    }
    
    private function settingsPurpose(){
        if(isset($this -> decodedPayload -> cstring) && !empty($this -> decodedPayload -> cstring)){
            $settingsCmd = $this -> decodedPayload -> cstring;
            
            if(is_object($settingsCmd)){
                $devSettings = $this -> getDevSetting($settingsCmd -> control);
                
                if($this -> isLoopData($devSettings)){            
                    if($devSettings["dev_setting_enabled"]){
                        $remarks = [
                            "status" => "UPDATED",
                            "settings" => $devSettings["dev_setting_id"]
                        ];
                        
                        switch($settingsCmd -> control){
                            case "maintenance_mode_site":
                            case "maintenance_mode_system":{
                                $nval = json_encode([
                                    "enabled" => $settingsCmd -> enabled,
                                    "exceptIpAddress" => $settingsCmd -> exceptIpAddress
                                ]);
                                $remarks["update"] = ($settingsCmd -> enabled ? "enabled" : "disabled");
                                
                                if($settingsCmd -> enabled && isset($settingsCmd -> exceptIpAddress))
                                    $remarks["exceptions"] = "IP Address(es): {$settingsCmd -> exceptIpAddress}";
                                break;
                            }
                            case "grant_deposit_solution":{
                                $nval = $settingsCmd -> grantPayload;
                                break;
                            }
                        }
                        
                        $this -> updateDevSetting($devSettings["dev_setting_id"], $nval);
                      
                        echo json_encode($remarks);
                    }else{
                        echo json_encode([
                            "code" => "STWRN01",
                            "message" => "[{$devSettings["dev_setting_id"]}]  can't be modified"
                        ]);
                    }
                }else{
                    echo json_encode([
                        "code" => "STWRN02",
                        "message" => "[{$settingsCmd -> control}]  doesn't exist"
                    ]);
                }
            }else{
                echo json_encode([
                    "code" => "STWRN03",
                    "message" => "The inputted settings command doesn't make an object out of it."
                ]);
            }
            
        }else{
            echo json_encode([
                "code" => "STERR01",
                "message" => "cstring is missing or not having a value."
            ]);
        }
    }
    
    private function composing(){
        $composerSettings = $this -> getDevSetting("composer_conf");
        
        $settingValue = $this -> getColumnData($composerSettings, "dev_setting_value");
        
        if($this -> checkJSONString($settingValue))
            $composerSettings = json_decode($settingValue);
        
        $extDir = SITE_ROOT."system/lib/ext/";
        $pharCreation = false;
        $pharInstallationLog = [];
        
        chdir($extDir);
        
        if(@file_exists($extDir)){    
            if(!@file_exists("composer.phar")){
                copy("https://getcomposer.org/installer", "composer-setup.php");
                if(hash_file("sha384", "composer-setup.php") == $composerSettings -> hashed_file_key){
                    exec("php composer-setup.php", $pharInstallationLog);
                    unlink("composer-setup.php");
                    
                    if(@file_exists("composer.phar"))
                        $pharCreation = "Installed.";
                }else{
                    $pharCreation = "Corrupted";
                    unlink("composer-setup.php");
                }
            }
            
            $output = [];
            
            $returnCode = -VAL_INT_1;
            exec("php composer.phar {$this -> decodedPayload -> cstring} 2>&1", $output, $returnCode);
            
            $commandOutput = [
                "pharOut" => $pharCreation,
                "pharInstallationLog" => $pharInstallationLog,
                "code" => $returnCode,
                "output" => $output
            ];
            
            echo json_encode($commandOutput);
        }
    }
    
    
    private function adminDebugCommands($debugCommand){
        $action = explode(DELIMIT_SPACE, $debugCommand -> action);
        
        if($this -> isLoopData($action)){
            $debugSettings = $this -> getDevSetting("sys_debug_conf");
            $debugContainer = LIBRARIES."debug-container.txt";
            
            switch($action[NO_COUNT]){
                case "create-debugger":                    
                    if(!@file_exists($debugContainer)){
                        if(isset($action[VAL_INT_1]) && $action[VAL_INT_1] == $debugCommand -> confirm_passphrase){
                            $debugPayloads = json_encode([
                                "ip_address" => [],
                                "my_commands" => [],
                                "enabled" => true,
                                "create_time" => date("Y-m-d H:i:s"),
                                "update_time" => ""
                            ]);
                            
                            $debugSettings -> debugger_passphrase = $debugCommand -> confirm_passphrase;
                            $debugPayloads = Crypto::encryptWithPassword($debugPayloads, $debugSettings -> debugger_passphrase);
                            
                            $this -> updateDevSetting("sys_debug_conf", json_encode($debugSettings));                                                        
                            file_put_contents($debugContainer, $debugPayloads);
                            
                            echo "OK";
                        }else
                            echo "Confirm Passphrase doesn't match";
                    }else
                        echo "Debug Container already exists";
                    
                    break;
                case "remove-debugger":
                    if(@file_exists($debugContainer)){
                        if(isset($action[VAL_INT_1]) && $action[VAL_INT_1] == $debugSettings -> debugger_passphrase){
                            $debugSettings -> debugger_passphrase = NO_STRING;
                            
                            $this -> updateDevSetting("sys_debug_conf", json_encode($debugSettings));
                            unlink($debugContainer);
                            
                            echo "OK";
                        }else
                            echo "Incorrect Passphrase.";
                    }else
                        echo "Debug Container already deleted.";
                    break;
                case "change-passphrase":
                    if(@file_exists($debugContainer)){
                        if(isset($action[VAL_INT_1]) && $action[VAL_INT_1] == $debugSettings -> debugger_passphrase){
                            if(isset($action[VAL_INT_2])){
                                if($action[VAL_INT_2] == $debugCommand -> confirm_passphrase){
                                    $containerContent = Crypto::decryptWithPassword(file_get_contents($debugContainer), $action[VAL_INT_1]);
                                    $containerContent = json_decode($containerContent);
                                    $containerContent -> update_time = date("Y-m-d H:i:s");
                                    
                                    $debugSettings -> debugger_passphrase = $action[VAL_INT_2];
                                    
                                    $debugPayloads = Crypto::encryptWithPassword(json_encode($containerContent), $debugSettings -> debugger_passphrase);
                                    
                                    $this -> updateDevSetting("sys_debug_conf", json_encode($debugSettings));
                                    file_put_contents($debugContainer, $debugPayloads);
                                    
                                    echo "OK";
                                }else
                                    echo "Confirm Passphrase doesn't match";
                            }else
                                echo "New Passphrase wasn't entered.";
                        }else
                            echo "Incorrect old passphrase.";
                    }
                    break;
                default:
                    echo "Unknown admin command: {$action[NO_COUNT]}";
                    break;
            }
        }
    }
    
    private function modDebugCommands($debugCommand){
        $action         = explode(DELIMIT_SPACE, $debugCommand -> action);
        $debugSettings  = $this -> getDevSetting("sys_debug_conf");
        $debugContainer = LIBRARIES."debug-container.txt";
        
        $toUpdateDebugContainer = false;
                
        if($this -> isLoopData($action)){
            if(!@file_exists($debugContainer)){
                echo "Debug container isn't established yet.";
                return;
            }
            
            if($debugCommand -> passphrase != $debugSettings -> debugger_passphrase){
                echo "Incorrect Passphrase";
                return;
            }
            
            $containerContent = Crypto::decryptWithPassword(file_get_contents($debugContainer), $debugCommand -> passphrase);
            $containerContent = json_decode($containerContent);
            
            switch($action[NO_COUNT]){
                case "show.debug.settings":
                    echo json_encode($containerContent);        
                    break;
                case "add.ip":
                    if(isset($action[VAL_INT_1])){                                                
                        if(!in_array($action[VAL_INT_1], $containerContent -> ip_address)){
                            $containerContent -> ip_address[] = $action[VAL_INT_1];
                            $toUpdateDebugContainer = true;
                        }else
                            echo "IP Address already whitelisted!";
                    }
                    break;
                case "rmv.ip":
                    if(isset($action[VAL_INT_1])){                        
                        if(in_array($action[VAL_INT_1], $containerContent -> ip_address)){
                            foreach($containerContent -> ip_address as $key => $ipAddr){
                                if($ipAddr == $action[VAL_INT_1])
                                    unset($containerContent -> ip_address[$key]);
                            }
                            $toUpdateDebugContainer = true;
                        }else
                            echo "IP Address not found";
                    }
                    break;
                case "add.mcmd":
                    if(isset($action[VAL_INT_1])){
                        $displayFunc    = isset($action[VAL_INT_2]) ? $action[VAL_INT_2] : "print";
                        $preTagFormat   = isset($action[VAL_INT_3]) ? $action[VAL_INT_3] : false;
                        
                        $queryMap = array_map(function($element){
                            return $element -> query_name;
                        }, $containerContent -> my_commands);
                        
                        if(!in_array($action[VAL_INT_1], $queryMap)){
                           $containerContent -> my_commands[] = [
                               "query_name" => $action[VAL_INT_1],
                               "display_func" => $displayFunc,
                               "dp_pre_tag" => $preTagFormat
                           ];
                           $toUpdateDebugContainer = true;
                        }else
                            echo "Query name already listed.";
                    }
                    break;
                case "rmv.mcmd":
                    if(isset($action[VAL_INT_1])){
                        $queryMap = array_map(function($element){
                            return $element -> query_name;
                        }, $containerContent -> my_commands);
                        
                        if(in_array($action[VAL_INT_1], $queryMap)){
                            foreach($containerContent -> my_commands as $key => $nested){                                
                                if($nested -> query_name == $action[VAL_INT_1])
                                    unset($containerContent -> my_commands[$key]);
                            }
                            $toUpdateDebugContainer = true;
                        }else
                            echo "Query name not found";
                    }
                    break;
                case "enable":
                    if(isset($action[VAL_INT_1])){
                        switch(strtolower($action[VAL_INT_1])){
                            case "true":
                            case "false":
                                $toUpdateDebugContainer = true;
                                break;
                            default:
                                echo "Invalid boolean value. (true|false) only";
                                break;
                        }
                        
                        $containerContent -> enabled = filter_var($action[VAL_INT_1], FILTER_VALIDATE_BOOLEAN);
                    }
                    break;
                default:
                    echo "Unknown mod command: {$action[NO_COUNT]}";
                    break;
            }
            
            if($toUpdateDebugContainer){
                $containerContent -> update_time = date("Y-m-d H:i:s");
                $debugPayloads = Crypto::encryptWithPassword(json_encode($containerContent), $debugSettings -> debugger_passphrase);
                file_put_contents($debugContainer, $debugPayloads);
                
                echo "OK";
            }
        }
    }
    
    private function sysDebug(){
        $debugCommand = $this -> decodedPayload -> cstring;
        
        switch($debugCommand -> mode){
            case "admin":{
                $this -> adminDebugCommands($debugCommand);
                break;
            }
            case "mod":{
                $this -> modDebugCommands($debugCommand);
                break;
            }
            default:
                echo "Unknown Debug Mode: {$debugCommand -> mode}";
                break;
        }
    }
    
    public function listen(){
        switch($this -> decodedPayload -> purpose){
            case "authentication":{
                $this -> authPurpose();     
                break;
            }
            case "my":{
                $this -> myPurpose();
                break;
            }
            case "settings":{
                $this -> settingsPurpose();
                break;
            }
            case "composer":{
                $this -> composing();
                break;
            }
            case "sysDebug":{
                $this -> sysDebug();
                break;
            }
            default:{
                echo json_encode([
                    "code" => "W01",
                    "message" => "Unknown Purpose: {$this -> decodedPayload -> purpose}"
                ]);
                break;
            }
        }
    }
}

$devTools = new DevTools();
$devTools -> listen();