<?php

namespace SovereignStack\SecuritySafe;

if ( ! defined( 'ABSPATH' ) ) exit; // Exit if accessed directly

use WP_Site;

/**
 * Class Janitor - Cleans up after the plugin
 *
 * @package SecuritySafe
 * @since   2.0.0
 */
class Janitor {

	/**
	 * Janitor constructor.
	 */
	function __construct() {

		// Register enable plugin process
		add_action( "activated_plugin", [
			self::class,
			'enable_plugin',
		], 10, 2 );

		add_action( 'upgrader_process_complete', [
			self::class,
			'upgrade_complete',
		], 10, 2 );

		// Cleanup Settings, Database, & Crons on Plugin Disable
		register_deactivation_hook( SECSAFE_FILE, [
			self::class,
			'disable_plugin',
		] );

		// Add Cleanup Action
		add_action( 'secsafe_cleanup_tables_daily', [
			self::class,
			'cleanup_tables',
		] );

		// Schedule Cleanup Services
		if( ! wp_next_scheduled( 'secsafe_cleanup_tables_daily' ) ) {

			wp_schedule_event( time(), 'daily', 'secsafe_cleanup_tables_daily' );
		}

		// Create Tables For New Blog
		add_action( 'wp_insert_site', [
			self::class,
			'wp_insert_site',
		] );
	}

	/**
	 * Writes to debug.log for troubleshooting
	 *
	 * @since 0.1.0
	 *
	 * @param string $file    Location of the file where the error occured
	 * @param string $line    Line number of where the error occured
	 *
	 * @param string $message Message entered into the log
	 */
	public static function log( string $message, string $file = '', string $line = '' ): void {

		// Debug must be on
		if( SECSAFE_DEBUG ) {

			$message = ( $message ) ? $message : 'Error: Log Message not defined!';
			$message .= ( $file && $line ) ? ' - ' . 'Occurred on line ' . $line . ' in file ' . $file : '';

			error_log( date( 'Y-M-j h:m:s' ) . " - " . $message . "\n", 3, SECSAFE_DIR . '/debug.log' );
		}
	}

	/**
	 * Run functions after upgrade
	 *
	 * @since  2.0.1
	 *
	 * @param array  $options
	 *
	 * @param object $upgrader_object
	 */
	public static function upgrade_complete( object $upgrader_object, array $options ): void {

		if( isset( $options['action'] ) && $options['action'] == 'update' && isset( $options['type'] ) && $options['type'] == 'plugin' ) {

			if( isset( $options['plugins'] ) ) {

				if( is_array( $options['plugins'] ) ) {

					foreach( $options['plugins'] as $plugin ) {
						self::enable_plugin( $plugin );
					}
				} elseif( is_string( $options['plugins'] ) ) {
					self::enable_plugin( $options['plugins'] );
				}
			}
		}
	}

	/**
	 * Creates database tables
	 *
	 * @since  2.5.0
	 *
	 * @param string $plugin
	 * @param bool   $network_wide
	 *
	 * @return void
	 */
	public static function enable_plugin( string $plugin, bool $network_wide = false ): void {

		if( SECSAFE_PLUGIN === $plugin ) {

			global $SecuritySafe;

			if( empty( $SecuritySafe ) ) {
				Plugin::init();
			}

			$sites = Yoda::get_sites();

			foreach( $sites as $site ) {

				if( is_multisite() ) {
					switch_to_blog( $site->blog_id );
				}

				if( is_plugin_active( SECSAFE_PLUGIN ) ) {

					// Create Logs Table
					Janitor::create_table_logs();

					// Create Stats Table
					Janitor::create_table_stats();

					$args = [];
					$args['type'] = '404s';
					$args['threats'] = 0;
					$args['status'] = 'test';
					$args['details'] = ( isset( $args['details'] ) ) ? $args['details'] : sprintf( __( '%s plugin enabled.', 'security-safe' ), SECSAFE_NAME );

					// Log Test 404
					Janitor::add_entry( $args );

					// Log Actual Activity
					Janitor::log_activity( $args );
				}

				if( is_multisite() ) {
					restore_current_blog();
				}
			}
		}
	}

	/**
	 * Runs when a new blog is added in a multisite setup
	 *
	 * @since WP 5.1
	 * @since 1.0.0
	 *
	 * @param \WP_Site $new_site
	 *
	 * @return void
	 */
	public static function wp_insert_site( WP_Site $new_site ): void {

		self::enable_plugin( SECSAFE_PLUGIN );
	}

	/**
	 * Creates Firewall Table
	 *
	 * @since  2.0.0
	 */
	public static function create_table_logs(): void {

		global $wpdb;

		$table_main = Yoda::get_table_main();

		// Must have two spaces after PRIMARY KEY, UNIQUE and INDEX
		$sql = $wpdb->prepare( "CREATE TABLE IF NOT EXISTS %i (
	            ID BIGINT NOT NULL AUTO_INCREMENT,
	            `type` VARCHAR(10) NOT NULL default '',
	            `date` DATETIME NOT NULL,
	            `date_expire` DATETIME NOT NULL,
	            `ip` VARCHAR(50) NOT NULL default '',
	            `username` VARCHAR(50) NOT NULL default '',
	            `uri` VARCHAR(512) NOT NULL,
	            `referer` VARCHAR(512) NOT NULL default '',
	            `user_agent` VARCHAR(512) NOT NULL default '',
	            `threats` TINYINT(1) NOT NULL default '0',
	            `status` VARCHAR(10) NOT NULL default '',
	            `details` VARCHAR(512) NOT NULL default '',
	            `score` TINYINT(3) NOT NULL default '0',
	            PRIMARY KEY  (ID),
	            UNIQUE  (ID),
	            INDEX  (type, status)
	        ) " . $wpdb->get_charset_collate() . ";", $table_main );
		$wpdb->query( $sql );

		// In case the table already existed, we are going to ensure it has the correct charset and collate
		$sql = $wpdb->prepare( "ALTER TABLE %i CONVERT TO CHARACTER SET $wpdb->charset COLLATE $wpdb->collate;", $table_main );
		$wpdb->query( $sql );
	}

	/**
	 * Creates Stats Table
	 *
	 * @since  2.0.0
	 */
	public static function create_table_stats(): void {

		global $wpdb;

		$table_stats = Yoda::get_table_stats();

		// Must have two spaces after PRIMARY KEY and UNIQUE
		$sql = $wpdb->prepare( "CREATE TABLE IF NOT EXISTS %i (
	            `date` DATETIME NOT NULL,
	            `404s` BIGINT NOT NULL default '0',
	            `404s_threats` BIGINT NOT NULL default '0',
	            `blocked` BIGINT NOT NULL default '0',
	            `threats` BIGINT NOT NULL default '0',
	            `logins` BIGINT NOT NULL default '0',
	            `logins_failed` BIGINT NOT NULL default '0',
	            `logins_success` BIGINT NOT NULL default '0',
	            `logins_threats` BIGINT NOT NULL default '0',
	            `logins_blocked` BIGINT NOT NULL default '0',
	            PRIMARY KEY  (date),
	            UNIQUE  (date)
	        ) " . $wpdb->get_charset_collate() . ";", $table_stats );
		$wpdb->query( $sql );

		// In case the table already existed, we are going to ensure it has the correct charset and collate
		$sql = $wpdb->prepare( "ALTER TABLE %i CONVERT TO CHARACTER SET $wpdb->charset COLLATE $wpdb->collate;", $table_stats );
		$wpdb->query( $sql );
	}

	/**
	 * Add entry to database
	 *
	 * @since  2.0.0
	 *
	 * @param array $args
	 *
	 * @return bool
	 *
	 */
	public static function add_entry( array $args ): bool {

		global $wpdb;

		// Prevent Caching
		Janitor::prevent_caching();

		$args = ( isset( $args['type'] ) ) ? $args : [];
		$type = ( isset( $args['type'] ) ) ? $args['type'] : false;
		$types = Yoda::get_types();
		$result = false; // Default

		// Require Valid Type
		if( isset( $types[ $type ] ) ) {

			/**
			 * Statically set for now
			 *
			 * @todo  log all wp cron activity not just Security Safe's
			 */
			$args['date'] = date( 'Y-m-d H:i:s' );

			if( $args['type'] != 'activity' && $args['type'] != 'allow_deny' ) {

				$args['uri'] = ( isset( $_SERVER['REQUEST_URI'] ) ) ? filter_var( $_SERVER['REQUEST_URI'], FILTER_SANITIZE_URL ) : '';
				$args['referer'] = ( isset( $_SERVER['HTTP_REFERER'] ) ) ? filter_var( $_SERVER['HTTP_REFERER'], FILTER_SANITIZE_URL ) : '';
				$args['user_agent'] = Yoda::get_user_agent();
				$args['ip'] = Yoda::get_ip();

				$args['threats'] = ( isset( $args['threats'] ) && $args['threats'] ) ? 1 : 0;
				$args['score'] = ( isset( $args['score'] ) ) ? $args['score'] : 0;

				// Record Stats
				Janitor::add_stats( $args );
			}

			// Trim Data
			$targs = [];

			foreach( $args as $key => $value ) {

				// Limit to 512 Characters
				$targs[ $key ] = substr( $value, 0, 512 );
			}

			// Add data to DB and insert() is sanitized by WP
			$result = $wpdb->insert( Yoda::get_table_main(), $targs );

			if( ! $result ) {

				// Create Logs Table
				Janitor::create_table_logs();

				// Try Again now that a table exist
				$result = $wpdb->insert( Yoda::get_table_main(), $targs );
			}
		}

		return (bool) $result;
	}

	/**
	 * Prevent plugins like WP Super Cache and W3TC from caching any data on this page.
	 *
	 * @since  2.2.3
	 */
	public static function prevent_caching(): void {

		( defined( 'DONOTCACHEOBJECT' ) ) || define( 'DONOTCACHEOBJECT', true );
		( defined( 'DONOTCACHEDB' ) ) || define( 'DONOTCACHEDB', true );
		( defined( 'DONOTCACHEPAGE' ) ) || define( 'DONOTCACHEPAGE', true );
	}

	/**
	 * Add stats into the db
	 *
	 * @since  2.0.0
	 *
	 * @param array $args
	 *
	 */
	public static function add_stats( array $args ): void {

		global $wpdb;

		$date = date( 'Y-m-d 00:00:00', strtotime( $args['date'] ) );

		$stats['blocked'] = $blocked = ( isset( $args['status'] ) && $args['status'] == 'blocked' ) ? 1 : 0;
		$stats['threats'] = $threats = ( isset( $args['threats'] ) && $args['threats'] ) ? 1 : 0;

		$stats['404s'] = $e404s = ( $args['type'] == '404s' ) ? 1 : 0;
		$stats['404s_threats'] = $e404s_threats = ( $threats && $args['type'] == '404s' ) ? 1 : 0;

		$stats['logins'] = $logins = ( isset( $args['type'] ) && $args['type'] == 'logins' ) ? 1 : 0;
		$stats['logins_failed'] = $logins_failed = ( $logins && isset( $args['status'] ) && $args['status'] == 'failed' ) ? 1 : 0;
		$stats['logins_success'] = $logins_success = ( $logins && isset( $args['status'] ) && $args['status'] == 'success' ) ? 1 : 0;
		$stats['logins_threats'] = $logins_threats = ( $logins && $threats ) ? 1 : 0;
		$stats['logins_blocked'] = $logins_blocked = ( $logins && $blocked ) ? 1 : 0;

		// Get the current day's stats
		$table = Yoda::get_table_stats();

		$values = [
			$table,
			$e404s,
			$e404s_threats,
			$blocked,
			$threats,
			$logins,
			$logins_failed,
			$logins_success,
			$logins_threats,
			$logins_blocked,
			$date
		];

		// Update
		$sql = $wpdb->prepare("UPDATE %i 
            SET 404s = 404s + %d,
            404s_threats = 404s_threats + %d,
            blocked = blocked + %d,
            threats = threats + %d,
            logins = logins + %d,
            logins_failed = logins_failed + %d,
            logins_success = logins_success + %d,
            logins_threats = logins_threats + %d,
            logins_blocked = logins_blocked + %d
            WHERE date like %s", $values);
		$exist = $wpdb->query( $sql );

		if( ! $exist ) {

			$stats['date'] = $date;

			// Add
			$result = $wpdb->insert( Yoda::get_table_stats(), $stats );

			if( ! $result ) {

				// Create Stats Table
				Janitor::create_table_stats();

				// Try Again now that a table exist
				$wpdb->insert( Yoda::get_table_stats(), $stats );
			}
		}
	}

	/**
	 * Logs activity into database
	 *
	 * @since 2.1.1
	 *
	 * @param array $args
	 *
	 */
	public static function log_activity( array $args ): void {

		$user = wp_get_current_user();

		// Log Actual Activity
		$args = ( is_array( $args ) ) ? $args : [];
		$args['type'] = 'activity';
		$args['threats'] = '0';
		$args['score'] = '0';
		$args['user_agent'] = Yoda::get_user_agent();
		$args['username'] = ( isset( $user->user_login ) ) ? $user->user_login : 'unknown';
		$args['ip'] = Yoda::get_ip();
		$args['status'] = ( defined( 'DOING_CRON' ) ) ? 'automatic' : 'unknown';
		$args['status'] = ( $args['status'] == 'unknown' && isset( $user->user_login ) ) ? 'manual' : $args['status'];

		Janitor::add_entry( $args );
	}

	/**
	 * Removes the settings from the database on plugin deactivation
	 *
	 * @since  0.3.5
	 */
	public static function disable_plugin(): void {

		// Remove Cron
		wp_clear_scheduled_hook( 'secsafe_cleanup_tables_daily' );

		$site_settings = get_option( SECSAFE_OPTIONS );

		// Check to see if the user wants us to cleanup the data
		if( isset( $site_settings['general']['cleanup'] ) && $site_settings['general']['cleanup'] == '1' ) {

			// Delete Settings
			delete_option( SECSAFE_OPTIONS );

			// Delete Tables
			Janitor::drop_table( SECSAFE_DB_FIREWALL );
			Janitor::drop_table( SECSAFE_DB_STATS );
		} else {

			// Log Activity
			$args = [];
			$args['details'] = sprintf( __( '%s plugin disabled.', 'security-safe' ), SECSAFE_NAME );
			Janitor::log_activity( $args );
		}
	}

	/**
	 * Drop table in the database
	 *
	 * @since  2.0.0
	 *
	 * @param string $table
	 *
	 * @return bool
	 *
	 */
	private static function drop_table( string $table ): bool {

		global $wpdb;

		$dropped = false;

		if( in_array( $table, [
			SECSAFE_DB_FIREWALL,
			SECSAFE_DB_STATS,
		] ) ) {

			$table = $wpdb->prefix . $table;

			$sql = $wpdb->prepare( "DROP TABLE IF EXISTS %i", $table );
			$dropped = $wpdb->query( $sql );
		}

		return $dropped;
	}

	/**
	 * This cleans up the database when the daily cron runs
	 *
	 * @since 2.0.0
	 */
	public static function cleanup_tables(): void {

		Janitor::cleanup_type( '404s' );
		Janitor::cleanup_type( 'logins' );
		Janitor::cleanup_type( 'activity' );
		Janitor::expire_type( 'allow_deny' );
	}

	/**
	 * Deletes all rows in excess of limit for a specific type
	 *
	 * @since 2.0.0
	 *
	 * @param string $type
	 *
	 */
	private static function cleanup_type( string $type ): void {

		global $wpdb;

		$types = Yoda::get_types();

		// Require Valid Type
		if( isset( $types[ $type ] ) ) {

			$args = [];

			$limit = Yoda::get_display_limits( $type, true );
			$table_main = Yoda::get_table_main();

			$values = [
				$table_main,
				$type
			];

			// Count how many exist
			$sql = $wpdb->prepare("SELECT COUNT(type) FROM %i WHERE type = %s", $values);
			$exists = (int) $wpdb->get_var( $sql );

			$args['details'] = '[';

			// If more than limit
			if( $exists > $limit ) {

				// Calculate amount to delete
				$delete = $exists - $limit;

				$values = [
					$table_main,
					$type,
					$delete
				];

				$sql = $wpdb->prepare( "DELETE FROM %i WHERE type = %s ORDER BY date ASC LIMIT %d", $values);
				$result = $wpdb->query( $sql );

				$args['details'] .= (int) $result . '-' . $exists . '-' . $limit;
			} else {

				$args['details'] = '0-' . $exists . '-' . $limit;
			}

			$args['details'] .= '] ' . $type . ' ' . __( 'database maintenance', 'security-safe' ) . '.';

			// Log Activity
			Janitor::log_activity( $args );
		}
	}

	/**
	 * Deletes all rows for a specific type that have an expire date that is older than now.
	 *
	 * @since 2.0.0
	 *
	 * @param string $type
	 *
	 */
	private static function expire_type( string $type ): void {

		global $wpdb;

		$args = [];

		// Cleanup Valid Types
		$types = Yoda::get_types();

		if( isset( $types[ $type ] ) ) {

			$table_main = Yoda::get_table_main();

			$ago = date( 'Y-m-d H:i:s', strtotime( '-3 days' ) );

			$values = [
				$table_main,
				$type,
				$ago
			];

			$sql = $wpdb->prepare("DELETE FROM %i WHERE type = %s AND ( status = 'allow' OR status = 'deny') AND date_expire < %s AND date_expire != '0000-00-00 00:00:00'", $values);
			$result = $wpdb->query( $sql );

			$args['details'] = '[' . (int) $result . '] ' . $type . ' ' . __( 'database maintenance', 'security-safe' ) . '.';
		} else {

			$args['details'] = sprintf( __( 'Error: %s is not a valid type.', 'security-safe' ), $type );
		}

		// Log Activity
		Janitor::log_activity( $args );
	}
}
