<?php
/*
Plugin Name: avalex
Description: Das Plugin ermöglicht es, Ihre Website mit der avalex API zu verbinden. Einen API Key erhalten Sie auf www.avalex.de.
Author: avalex GmbH
Author URI: https://avalex.de/
Version: 2.0.7
Text Domain: Avalex
Domain Path: /languages
*/

class Avalex {
    protected $apiUrl = 'https://avalex.de';
    protected $fallbackApiUrl = 'https://proxy.avalex.de';
    protected $types = [
        'dse' => '/avx-datenschutzerklaerung',
        'imprint' => '/avx-impressum',
        'agb' => '/avx-bedingungen',
        'widerruf' => '/avx-widerruf',
    ];

    public function __construct() {
        // Call the functions that handle the stuff we need to do on plugin activation.
        register_activation_hook(__FILE__, array($this, 'activatePlugin'));

        // And call the functions, when the user deactivates the plugin.
        register_deactivation_hook(__FILE__, array($this, 'deactivatePlugin'));

        // And finally call the functions, when the user deletes the plugin.
        register_uninstall_hook(__FILE__, 'Avalex::uninstallPlugin');

        // We also want to load the language files (To make WordPress think it is translated).
        $this->loadLanguageFiles();

        // Set our variables.
        global $wpdb;
        $this->pluginPath = dirname(__FILE__);
        $this->tableName = $wpdb->prefix . 'avalex';
        $this->apiKey = get_option('avalex_api_key', false);
        $this->isKeyValid = get_option('avalex_valid_api_key', false);
        $this->isDomainValid = false;
        $this->response = false;
        $this->recursiveCalls = 0;
        $this->notice = false;
        $this->cachePath = WP_CONTENT_DIR . '/cache';
        $this->avalexCronAction();
        $this->maybeUpdateDatabase();

        // Register a new cron schedule.
        add_filter('cron_schedules', array($this, 'addQuarterlyCronSchedule'));

		// We also want to check if our cronjob is still active, because it is crucial for the functionality of this plugin.
		$this->registerCronJob();

        // Call our methods that we need as early as possible.
        $this->init();

        // Add our admin menu page.
        add_action('admin_menu', array($this, 'addAdminMenu'));

        // Add our DSE shortcode.
        add_shortcode('avalex', array($this, 'renderAvalexShortcode'));
        add_shortcode('avalex_de_datenschutz', array($this, 'renderAvalexShortcode'));
        add_shortcode('avalex_de_impressum', array($this, 'renderAvalexShortcode'));
        add_shortcode('avalex_de_agb', array($this, 'renderAvalexShortcode'));
        add_shortcode('avalex_de_widerrufsbelehrung', array($this, 'renderAvalexShortcode'));

        // Add update functionality.
        add_action('pre_set_site_transient_update_plugins', array($this, 'pluginUpdateNotification'));

        // We need an action to make it possible to force a DSE update from outside for important updates that can't wait.
        add_action('init', array($this, 'forceUpdate'));

        // Add settings link to plugin page.
        add_action('plugin_action_links_' . plugin_basename(__FILE__), array($this, 'addSettingsLink'));
    }

    public function activatePlugin() {
        $this->createTable();
        $this->maybeDeleteOldCronJob();
        $this->registerCronJob();
    }

    public function checkPhpVersion() {
        if (version_compare(PHP_VERSION, '5.6', '>')) {
            return;
        }

        wp_die('PHP Version zu alt. Bitte nutzen Sie mindestens PHP 5.6.<br>Sie nutzen aktuell Version: ' . PHP_VERSION);
    }

    public function createTable() {
        global $wpdb;
        $charsetCollate = $wpdb->get_charset_collate();

        // First we want to drop any old tables of avalex, if there are some.
        $this->deleteAvalexTable();

        // Now create our new table.
        $sql = "CREATE TABLE $this->tableName (
        id mediumint(9) NOT NULL AUTO_INCREMENT,
        time datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
        type text NOT NULL,
        data longtext NOT NULL,
        PRIMARY KEY  (id)
    ) $charsetCollate;";

        require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        dbDelta($sql);
    }

    public function maybeUpdateDatabase() {
        global $wpdb;
        $charsetCollate = $wpdb->get_charset_collate();

         // Now create our new table.
         $sql = "CREATE TABLE $this->tableName (
            id mediumint(9) NOT NULL AUTO_INCREMENT,
            time datetime DEFAULT '0000-00-00 00:00:00' NOT NULL,
            data longtext NOT NULL,
            type text NOT NULL,
            PRIMARY KEY  (id)
        ) $charsetCollate;";
    
            require_once ABSPATH . 'wp-admin/includes/upgrade.php';
            dbDelta($sql);
    }

    public function maybeDeleteOldCronJob() {
        if (wp_next_scheduled('avalex_cron_event')) {
            wp_clear_scheduled_hook('avalex_cron_event');
        }

        if(wp_next_scheduled('avalex_update_dse_cron_event')) {
            wp_clear_scheduled_hook('avalex_update_dse_cron_event');
        }
    }

    public function registerCronJob() {
        if (!wp_next_scheduled('avalex_update_dse_cron_event')) {
            wp_schedule_event(time(), 'avalex_interval', 'avalex_update_dse_cron_event');
        }
    }

    public function deactivatePlugin() {
        $this->maybeDeleteOldCronJob();
    }

    public function uninstallPlugin() {
        global $wpdb;
        $tableName = $wpdb->prefix . 'avalex';
        $wpdb->query("DROP TABLE IF EXISTS $tableName");

        self::deleteOptions();
    }

    public function deleteAvalexTable() {
        global $wpdb;
        $wpdb->query("DROP TABLE IF EXISTS $this->tableName");
    }

    public function deleteOptions() {
        delete_option('avalex_api_key');
        delete_option('avalex_valid_api_key');
    }

    public function avalexCronAction() {
        add_action('avalex_update_dse_cron_event', array($this, 'fetchAvalexTexts'));
    }

    public function addQuarterlyCronSchedule($schedules) {
        $schedules['avalex_interval'] = array(
                'interval' => 21600,
                'display' => __('4 times a day'));
        return $schedules;
    }

    public function loadLanguageFiles() {
        load_plugin_textdomain( 'Avalex', FALSE, basename( dirname( __FILE__ ) ) . '/languages/' );
    }

    public function init() {
        // If the user hits the submit button, we want to store the new apikey.
        if(!isset($_POST['save_avalex'])) {
            return;
        }

        if (!$this->saveApiKey()) {
            return;
        }

        if (!$this->validateApiKey()) {
            return;
        }

        $this->fetchAvalexTexts();
    }

    public function addAdminMenu() {
        add_options_page('avalex', 'avalex', 'manage_options', 'avalex', array($this, 'avalexAdminPage'));
    }

    public function avalexAdminPage() {
        include $this->pluginPath . '/templates/admin_page.php';
    }

    public function addNotice() {
        if (!$this->notice) {
            return false;
        }

        echo '<div class="notice notice-' . $this->noticeType . '"><p>' . $this->noticeMessage . '</p></div>';
        return true;
    }

    private function saveApiKey() {
        // Check if api key was entered.
        $apiKey = sanitize_text_field($_POST['avalex_api_key']);
        update_option('avalex_api_key', $apiKey);

        if (!$apiKey) {
            $this->notice = true;
            $this->noticeType = 'error';
            $this->noticeMessage = __('Please enter an API key.', 'Avalex');
            return false;
        }

        if ($apiKey) {
            $this->apiKey = $apiKey;
            return true;
        }
    }

    private function showApiKey() {
        if ($this->apiKey) {
            echo $this->apiKey;
            return;
        }

        return false;
    }

    private function validateApiKey() {
        
        // Build the api url.
        $apiUrlDomain = $this->apiUrl . '/avx-datenschutzerklaerung';
        if ($this->recursiveCalls == 1) {
            // $apiUrlDomain = $this->fallbackApiUrl . 'avx-datenschutzerklaerung';
        }

        $serverDomain = $this->trimDomain(home_url());

        $apiUrlDomain = add_query_arg('apikey', $this->apiKey, $apiUrlDomain);
        $apiUrlDomain = add_query_arg('domain', $serverDomain, $apiUrlDomain);

        // Call the api.
        $response = wp_remote_get($apiUrlDomain);
        $responseCode = wp_remote_retrieve_response_code($response);

        // If we have a 401, the key is not valid.
        if ($responseCode == 401) {
            $this->notice = true;
            $this->noticeType = 'error';
            $this->noticeMessage = 'Der API-Key ist ungültig.';
            $this->isKeyValid = false;
            update_option('avalex_valid_api_key', false);
            return false;
        }

        // 200 means api key is valid, so we can grab the domain and proceed.
        if ($responseCode == 200) {
            update_option('avalex_valid_api_key', true);
            $this->isKeyValid = 1;
            $this->response = json_decode(wp_remote_retrieve_body($response), true);
            $this->recursiveCalls = 0;
            return true;
        }

        if (is_wp_error($response)) {
            // When this is the first time the error happens, we want to try the fallback url.
            if ($this->recursiveCalls == 0) {
                $this->recursiveCalls = 1;
                return $this->validateApiKey();
            }

            $errorMessage = $response->get_error_message();
            $this->notice = true;
            $this->noticeType = 'error';
            $this->noticeMessage = 'Beim Datenabgleich  mit dem Avalex Server ist etwas schiefgelaufen. Bitte wenden Sie sich an den Support mit den folgenden Informationen:<br>' . $errorMessage;
            return false;
        }

        if ($responseCode == 400) {
            // Website not configured.
            $this->notice = true;
            $this->noticeType = 'error';
            $this->noticeMessage = 'Webseite noch nicht fertig konfiguriert oder die hinterlegte Domain stimmt nicht mit ihrem Server überein.';
            $this->isKeyValid = false;
            update_option('avalex_valid_api_key', false);
            return false;
        }

     
        update_option('avalex_valid_api_key', false);
        return false;
    }

    public function trimDomain($domain) {
        // Remove protocoll and www.
        $domain = str_replace('http://', '', $domain);
        $domain = str_replace('https://', '', $domain);
        $domain = str_replace('www.', '', $domain);
        $domain = rtrim($domain, '/');
        return $domain;
    }

    public function fetchAvalexTexts() {
        
        // We loop trough every type and save everything in its own row.
        foreach($this->types as $type => $endpoint) {
            $this->fetchAvalexDse($endpoint, $type);
        }

    }

    public function fetchAvalexDse($endpoint, $type) {
        $apiUrl = $this->apiUrl . $endpoint;


        if ($this->recursiveCalls == 1) {
            $apiUrl = $this->fallbackApiUrl . $endpoint;
        }

        $serverDomain = $this->trimDomain(home_url());

        $apiUrl = add_query_arg('apikey', $this->apiKey, $apiUrl);
        $apiUrl = add_query_arg('domain', $serverDomain, $apiUrl);
        $response = wp_remote_get($apiUrl);
        $responseCode = wp_remote_retrieve_response_code($response);

        if ($responseCode == 401) {
            // API Key not authorized, shouldn't happen at this point, but you never know.
            return;
        }

        if ($responseCode == 200) {
            // We got something back, let's see if the body has some data.
            $data = wp_remote_retrieve_body($response);

            // If the data is empty, we do nothing to avoid overwriting the DSE with empty content.
            if (empty($data)) {
                return false;
            }

            // Alright, we should be safe to actually save the dse in the database. But to be sure, we sanitize the data.
            $sanitizedData = sanitize_post_field('post_content', trim($data), false, 'display');
            $trimmedData = preg_replace("/\r|\n/", '', $sanitizedData);
            $this->dseHtml = $trimmedData;
            $this->writeDseIntoDatabase($type);
            $this->recursiveCalls = 0;

            // Set the data for the successfull update notice.
            $this->notice = true;
            $this->noticeType = 'success';
            $this->noticeMessage = 'Der API Key und die avalex Rechtstexte wurden aktualisiert.';

            // Now we delete the whole cache of WordPress to make sure the update actually shows.
            $this->emptyCache();

            return true;
        }

        if (is_wp_error($response)) {
            if ($this->recursiveCalls == 0) {
                $this->recursiveCalls = 1;
                return $this->fetchAvalexDse($endpoint);
            }

            $errorMessage = $response->get_error_message();
            $this->notice = true;
            $this->noticeType = 'error';
            $this->noticeMessage = 'Beim Datenabgleich  mit dem Avalex Server ist etwas schiefgelaufen. Bitte wenden Sie sich an den Support mit den folgenden Informationen:<br>' . $errorMessage;
            return false;
        }
    }

    public function writeDseIntoDatabase($type) {
        if (!$this->dseHtml) {
            return;
        }

        // Write our new data into the database. And delete the old row beforehand.
        global $wpdb;

        $wpdb->query("DELETE FROM $this->tableName WHERE type = '$type'");


        $wpdb->insert(
            $this->tableName,
            array(
                'time' => current_time('mysql'),
                'data' => $this->dseHtml,
                'type' => $type,
            ),
            array(
                '%s',
                '%s',
                '%s',
            )
        );
    }

    public function getDseFromDatabase($type) {
        // Write our new data into the database.
        global $wpdb;
        $dseRow = $wpdb->get_results("SELECT * FROM $this->tableName WHERE type = '$type'");

        if (!$dseRow) {
            return;
        }

        return $dseRow[0]->data;
    }

    public function renderAvalexShortcode($atts, $content, $shortcode) {
        if (!$this->isKeyValid) {
            
            // Try to revalidate.
            if($this->validateApiKey()) {
                $this->renderAvalexShortcode(false, false, $shortcode);
            }

            return 'Avalex ist noch nicht fertig eingerichtet.';
        }

        switch ($shortcode) {
            case 'avalex_de_impressum':
                $type = 'imprint';
                break;

            case 'avalex_de_agb':
                $type = 'agb';
                break;

            case 'avalex_de_widerrufsbelehrung':
                $type = 'widerruf';
                break;
            
            case 'avalex':
            case 'avalex_de_datenschutz':
                $type = 'dse';
                break;

            default:
                return;
        }

        // First we check if by whatever reason the dse is empty, if it is, we try to fetch the new data.
        if (!$this->getDseFromDatabase($type) || empty($this->getDseFromDatabase($type))) {
            if (!$this->validateApiKey()) {
                if($this->addNotice()) {
                    return;
                }
                return 'API Key ungültig.';
            }

            if (!$this->fetchAvalexTexts() && $this->recursiveCalls < 2) {
                $this->recursiveCalls++;
                $this->renderAvalexShortcode(false, false, $shortcode);
                return false;
            }
        }

        // We have a DSE and we will use it!
        return $this->getDseFromDatabase($type);
    }

    public function getDseTime() {
        global $wpdb;
        $result = $wpdb->get_row("SELECT time FROM $this->tableName");

        if (!$result) {
            echo 'Noch keine DSE vorhanden.';
        }

		// We want to show the date in a nice format.
		echo date_i18n('d.m.Y H:i', strtotime($result->time)) . ' Uhr';
    }

    public function pluginUpdateNotification($transient) {
        if (empty($transient->checked)) {
            return $transient;
        }

        $url = $this->apiUrl . '/files/wordpress/package.json';

        if ($this->recursiveCalls == 1) {
            $url = $this->fallbackApiUrl . '/files/wordpress/package.json';
        }

        $response = wp_remote_get($url);

        if (is_wp_error($response)) {
            if ($this->recursiveCalls == 0) {
                $this->recursiveCalls = 1;
                return $this->pluginUpdateNotification($transient);
            }

            $errorMessage = $response->get_error_message();
            $this->notice = true;
            $this->noticeType = 'error';
            $this->noticeMessage = 'Beim Datenabgleich  mit dem Avalex Server ist etwas schiefgelaufen. Bitte wenden Sie sich an den Support mit den folgenden Informationen:<br>' . $errorMessage;
            return false;
        }

        $body = json_decode(wp_remote_retrieve_body($response));
        $version = $body->version;

        $pluginData = get_plugin_data(__FILE__, false, false);

        if (version_compare($version, $pluginData['Version'], '<=')) {
            return $transient;
        }

        if ($this->recursiveCalls == 0) {
            $updateInfo = array(
                'plugin' => plugin_basename(__FILE__),
                'slug' => plugin_basename(__FILE__),
                'new_version' => $version,
                'url' => 'https://avalex.de',
                'package' => 'https://avalex.de/files/wordpress/avalex_wordpress.zip',
            );
        }

        if ($this->recursiveCalls == 1) {
            $updateInfo = array(
                'plugin' => plugin_basename(__FILE__),
                'slug' => plugin_basename(__FILE__),
                'new_version' => $version,
                'url' => 'https://avalex.de',
                'package' => 'https://proxy.avalex.de/files/wordpress/avalex_wordpress.zip',
            );
        }

        $this->recursiveCalls = 0;

        $transient->response[plugin_basename(__FILE__)] = (object) $updateInfo;
        return $transient;
    }

    public function forceUpdate() {
        if (!isset($_GET['force_dse_update']) || $_GET['force_dse_update'] != true) {
            return;
        }

        $this->fetchAvalexTexts();
        $this->emptyCache();
    }

    public function emptyCache() {
        // We want to get all folders in the cache path, not files. Atleast not in the root of the cache folder.
        $objects = glob($this->cachePath . '/*');
        foreach ($objects as $object) {
            if (!is_dir($object)) {
                continue;
            }


            $foldersToExclude = [
                'borlabs-cookie',
                'autoptimize',
                'tmp',
            ];
            // We also want to exclude some cache paths.
            
            $skip = false;
            foreach($foldersToExclude as $folderName) {
                if(strpos($object, $folderName) !== false) {
                    $skip = true;
                    break;
                }
            }

            if($skip) {
                continue;
            }

            $this->emptyFolder($object);
        }

    }

    public function emptyFolder($dir) {
        $objects = glob($dir . '/*');

        foreach ($objects as $object) {
            if (!is_dir($object)) {
                $this->deleteFile($object);
                continue;
            }

            // Now we know, this is a folder, so restart the process.
            $this->emptyFolder($object);
        }
    }

    public function deleteFile($file) {

        // We only want to delete html or gzip files, no css or js.
        $extension = pathinfo($file)['extension'];
        $allowedExtensionsToDelete = [
            'html', 'html_gzip', 'gz',
        ];

        if(!in_array($extension, $allowedExtensionsToDelete)) {
            return;
        }

        unlink($file);
    }

    public function addSettingsLink($links) {
        $links = array_merge(array(
            '<a href="' . esc_url(admin_url('/options-general.php?page=avalex')) . '">Einstellungen</a>',
        ), $links);
        return $links;
    }
}

// Call our class.
new Avalex();
