<?php
/**
 * Customizer Font Upload Ajax Extension.
 * 
 * This file handles the customizers font upload on the server side.
 * It got a security overhaul with @version 1.2.6 and @version 1.2.7
 */
namespace Greyd;

if ( !defined( 'ABSPATH' ) ) exit;

new Customizer_Font_Upload();

class Customizer_Font_Upload {

    const AJAX_ACTION = "greyd_custom_font_upload";
    
    public function __construct() {

        // if ( !empty($_POST) ) {
        //     echo "\$_POST is not empty: ";
        //     debug( $_POST );
        // }

        // if ( !empty($_GET) ) {
        //     echo "\$_GET is not empty: ";
        //     debug( $_GET );
        // }

        // if ( !empty($_FILES) ) {
        //     echo "\$_FILES is not empty: ";
        //     debug( $_FILES );
        // }

        // $actual_link = "http://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]";
        // echo $actual_link;

        // register ajax request
        // add_action( 'wp_ajax_nopriv_'.self::AJAX_ACTION, array( $this, 'handle_font_upload' ) );
        add_action( 'wp_ajax_'.self::AJAX_ACTION, array( $this, 'handle_font_upload' ) );
    }

    /**
     * Handle the ajax font upload.
     */
    public function handle_font_upload() {

        if ( empty($_POST) ) die("err::no_data");

        // verify the nonce
        $nonce = isset($_POST["_wpnonce"]) ? $_POST["_wpnonce"] : null;
        if ( !$nonce || !wp_verify_nonce($nonce, self::AJAX_ACTION)) {
            $this->return_error("invalid_nonce", "The form could not be validated, validation of the security nonce failed.");
        }

        /**
         * Check capability
         * 
         * @since 1.2.7
         */
        if ( !current_user_can('edit_theme_options') ) {
            $this->return_error("access_denied", "The current user does not have the permission to 'edit_themes'.");
        }

        $data = wp_parse_args( $_POST, array(
            "mode"          => "",
            "uploadpath"    => "",
            "name_full"     => ""
        ) );

        if (
            empty($data["mode"]) ||
            empty($data["uploadpath"]) ||
            empty($data["name_full"])
        ) {
            $this->return_error("no_data", "Data was incomplete. Either 'mode', 'uploadpath' or 'name_full' was not set.");
        }

        // upload file exists?
        if ( !file_exists($data["uploadpath"]) ) {
            mkdir($data["uploadpath"], 0755, true);
        }

        if ($data["mode"] == "upload") {
            $this->upload_font( $data );
        }
        else if ($data["mode"] == "delete") {
            $this->delete_font( $data );
        }
        else if ($data["mode"] == "mod") {
            $this->edit_font( $data );
        }

        die("err::no_data");
    }

    /**
     * Upload the font-file and respond.
     */
    public function upload_font( $data ) {

        // check file
        if (empty($_FILES)) {
            $this->return_error("no_data", "No uploaded file found.");
        }
        if (count($_FILES) > 1) {
            $this->return_error("too_many", "Only 1 uploaded file allowed.");
        }
        if ( $_FILES['file']['size'] > wp_max_upload_size() ) {
            $this->return_error("too_big", "Uploaded file exceeds the max file size.");
        }
        if ($_FILES['file']['type'] != 'application/zip' && $_FILES['file']['type'] != 'application/x-zip-compressed') {
            $this->return_error("no_zip", "Uploaded file is not a ZIP archive.");
        }
        if ( !class_exists('ZipArchive') ) {
            $this->return_error("ZipArchive", "The PHP Class 'ZipArchive' is not installed. Please contact your server administrator to enable the <a href='https://www.php.net/manual/de/book.zip.php' target='_blank'>PHP Zip Extension</a>.");
        }

        /**
         * Check ZIP filetype
         * 
         * @since 1.2.7
         */
        $filetype = wp_check_filetype( $_FILES['file']['name'], array(
            "zip"   => "application/zip",
            "gzip"  => "application/x-gzip",
            "rar"   => "application/rar"
        ) );
        if ( !$filetype['type'] ) {
            $this->return_error("no_zip", "Uploaded file is not a valid ZIP archive.");
        }

        echo "\n\rFile is a valid ZIP archive.\n\r";

        // open file
        $zip = new \ZipArchive;
        if ($zip->open($_FILES['file']['tmp_name']) === false) {
            $this->return_error("no_zip", "Uploaded ZIP archive could not be opened.");
        }

        /**
         * Loop through files.
         */
        $found = false;
        for ($i=0; $i<$zip->numFiles; $i++) {

            $innerfile = $zip->statIndex($i);

            /**
             * Remove invalid files.
             * 
             * @since 1.2.7
             */
            if ( $innerfile['size'] > 0 ) {
                $check_filetype = wp_check_filetype( $innerfile['name'], array(
                    "css"   => "text/css",
                    "svg"   => "image/svg+xml",
                    "ttf"   => "application/x-font-ttf",
                    "ttf2"  => "application/x-font-truetype",
                    "otf"   => "application/x-font-opentype",
                    "woff"  => "application/font-woff",
                    "woff2" => "application/font-woff2",
                    "eot"   => "application/vnd.ms-fontobject",
                    "sfnt"  => "application/font-sfnt"
                ) );
                if ( !$check_filetype['type'] ) {
                    echo "\n\rFile '{$innerfile['name']}' removed from ZIP (invalid filetype).\n\r";
                    $zip->deleteName( $innerfile['name'] );
                    continue;
                }
            }

            // find the font css file
            if ( !$found && strpos($innerfile['name'], '.css') > -1 ) {
                $found = $innerfile;
            }
        }

        $zip->close();

        if ( !$found ) {
            $this->return_error("no_font::".$_FILES['file']['name']);
        }
    
        // get data
        $path = $data["uploadpath"];
        $zip_path = $path;
        
        $name_full = stripslashes( $data["name_full"] );
        $name = str_replace(".zip", "", $_FILES['file']['name']);
        $file = $found['name'];
        if (strpos($found['name'], $name.'/') === false) {
            $zip_path .= "/".$name;
            $file = $name."/".$found['name'];
        }
    
        // make data
        $newfont = (object) array(
            "name_full" => $name_full,
            "name" => $name,
            "file" => $file
        );
    
        // make json
        $indexfile  = $data["uploadpath"]."/custom_fonts_index.json";
        $fonts      = $this->openJSON($indexfile);

        foreach ( $fonts->fonts as $i => $font ) {
            if (
                $font == $newfont
                || $font->name_full == $name_full
            ) {
                $this->return_error("duplicate::".$name_full);
            }
        }
    
        // remove existing folder
        if (file_exists($zip_path."/".$name)) {
            $this->deleteDir($zip_path."/".$name);
        }

        // move zip to new location
        if ( $zip->open($_FILES['file']['tmp_name']) === true ) {
            $zip->extractTo( $zip_path );
        }
        // unlink($name.".zip");

        // update fonts file
        $fonts->fonts[] = $newfont;
        $this->writeJSON($indexfile, $fonts);
    
        // return full name
        exit( "success::".$name_full );
    }

    /**
     * Delete the font-file and respond.
     */
    public function delete_font( $data ) {
        
        $success    = false;
        $name_full  = stripslashes( $data["name_full"] );
        $indexfile  = $data["uploadpath"]."/custom_fonts_index.json";
        $fonts      = $this->openJSON($indexfile);

        debug($fonts);

        foreach ( $fonts->fonts as $i => $font ) {
            if ( $font->name_full == $name_full ) {
                
                $this->deleteDir( trailingslashit($data["uploadpath"]) . $font->name );
                unset($fonts->fonts[$i]);
                $success = true;
                break;
            }
        }

        if ( ! $success ) {
            exit( "err::font '{$name_full}' not found." );
        }

        $this->writeJSON($indexfile, $fonts);

        debug($fonts);
        
        exit( "success::".$name_full );
    }

    /**
     * Modify the font-file and respond.
     */
    public function edit_font( $data ) {
        
        $name_full       = stripslashes( $data["name_full"] );
        $name_full_new   = stripslashes( $data["name_full_new"] );
        $indexfile      = $data["uploadpath"]."/custom_fonts_index.json";
        $fonts          = $this->openJSON($indexfile);

        debug($fonts);

        foreach ( $fonts->fonts as $i => $font ) {

            if ( $font->name_full == $name_full ) {
                $fonts->fonts[$i]->name_full = $name_full_new;
                break;
            }
        }
        $this->writeJSON($indexfile, $fonts);

        debug($fonts);

        exit( "success::".$name_full_new );
    }
    
    /**
     * Open the JSON file.
     * 
     * @return object
     */
    public function openJSON($indexfile) {

        if ( !file_exists($indexfile) ) {
            $fonts = (object) array( "fonts" => array() );
        }
        else {
            $fonts = json_decode($this->get_file_contents($indexfile));
            if ( !$fonts->fonts ) {
                $fonts->fonts = array();
            } else {
                $fonts->fonts = (array) $fonts->fonts;
            }
    
        }
        return $fonts;
    }

    /**
     * Write the JSON file.
     */
    public function writeJSON($indexfile, $fonts) {
        // write json
        $fo = fopen($indexfile, 'w');
        fwrite($fo, json_encode( $fonts, JSON_PRETTY_PRINT));
        fclose($fo);
    }
    
    /**
     * Delete a directory.
     * 
     * @return bool
     */
    public function deleteDir($dirPath) {
        
        $files = glob(trailingslashit( $dirPath ) . '*', GLOB_MARK);
        foreach ($files as $file) {
            if (is_dir($file)) {
                $this->deleteDir($file);
            } else {
                unlink($file);
            }
        }
        return rmdir( untrailingslashit($dirPath) );
    }
    
    /**
     * Open file contents.
     * @see \vc\helper::get_file_contents()
     */
    public function get_file_contents($file) {
        // with the prefix '@' the function doesn't throw a warning, as error handling is done below
        $contents = @file_get_contents($file);
    
        if ( $contents ) return $contents;
    
        // request failed
        echo "<pre>HTTP request failed. Error was: ".( function_exists('error_get_last') ? error_get_last()['message'] : "unidentified error" )."</pre>";
    
        // check if 'allow_url_fopen' is true
        if ( ini_get('allow_url_fopen') == false )
            echo "<pre>Your server's PHP settings are not compatible with the GREYD.SUITE. The variable 'allow_url_fopen' is deactivated, which leads to the website being displayed incorrectly. Please contact your server administrator to resolve the problem.</pre>";
        /**
         *  Check if HTTP Authentication is enabled and pass it as $context
         *  otherwise files may not be found, due to the athentiaction not being passed
         */
        else if ( isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW']) ) {
            $auth = base64_encode( $_SERVER['PHP_AUTH_USER'].":".$_SERVER['PHP_AUTH_PW'] );
            $context = stream_context_create([
                "http" => [
                    "header" => "Authorization: Basic $auth"
                ]
            ]);
            $contents = file_get_contents($file, false, $context);
        }
        return $contents;
    }
    
    /**
     * Exit with error message.
     */
    public function return_error($slug, $msg="") {
        if ( !empty($msg) ) echo "Error: $msg<br>";
        die("err::$slug");
    }
}