<?php
defined('ABSPATH') or die("you do not have access to this page!");

require_once rsssl_le_path . 'vendor/autoload.php';
require_once rsssl_path . 'lib/admin/class-encryption.php';
use RSSSL\lib\admin\Encryption;
use LE_ACME2\Account;
use LE_ACME2\Authorizer\AbstractDNSWriter;
use LE_ACME2\Authorizer\DNS;
use LE_ACME2\Authorizer\HTTP;
use LE_ACME2\Connector\Connector;
use LE_ACME2\Order;
use LE_ACME2\Utilities\Certificate;
use LE_ACME2\Utilities\Logger;
use RSSSL\Security\RSSSL_Htaccess_File_Manager;

class rsssl_letsencrypt_handler {
	use Encryption;

	private static $_this;
	/**
	 * Account object
	 * @var bool|LE_ACME2\Account
	 */
	public $account = false;
	public $challenge_directory = false;
	public $key_directory = false;
	public $certs_directory = false;
    public $subjects = array();

	public $htaccess_file_manager;

	public function __construct()
    {
		if ( isset( self::$_this ) ) {
			wp_die();
		}
		$this->htaccess_file_manager = Rsssl_Htaccess_File_Manager::get_instance();
		add_action('admin_init', array($this, 'upgrade'));

		//loading of these hooks is stricter. The class can be used in the notices, which are needed on the generic dashboard
		//These functionality is not needed on the dashboard, so should only be loaded in strict circumstances
		if ( rsssl_letsencrypt_generation_allowed( true ) ) {
			add_action( 'rsssl_after_save_field', array( $this, 'after_save_field' ), 10, 4 );
			add_action( 'admin_init', array( $this, 'maybe_queue_root_htaccess_acme_exclusion_update' ) );
			add_action( 'admin_init', array( $this, 'maybe_queue_letsencrypt_directory_htaccess_files' ) );
			add_filter( 'rsssl_htaccess_security_rules', array( $this, 'add_root_htaccess_acme_exclusion_rule' ) );

			$this->key_directory = $this->key_directory();
			$this->challenge_directory = $this->challenge_directory();
			$this->certs_directory = $this->certs_directory();

			// Config the desired paths
			if ( $this->key_directory ) {
				Account::setCommonKeyDirectoryPath( $this->key_directory );
			}

			if ( $this->challenge_directory ) {
				HTTP::setDirectoryPath( $this->challenge_directory );
			}

			// General configs
			//$use_staging = defined('RSSSL_LE_DEBUG');
			Connector::getInstance()->useStagingServer( false );
			Logger::getInstance()->setDesiredLevel( Logger::LEVEL_DISABLED );

			if ( !rsssl_get_option( 'disable_ocsp' ) ) {
				Certificate::enableFeatureOCSPMustStaple();
			}

			Order::setPreferredChain('ISRG Root X1');
			$this->subjects = $this->get_subjects();
		}

		self::$_this = $this;
	}

	public static function this() {
		return self::$_this;
	}

		/**
		 * Queue a root `.htaccess` refresh when the ACME HTTP-01 bypass block is missing or stale.
		 *
		 * This method only schedules the centralized root writer. It does not modify the root file directly.
		 * The actual ACME block is emitted through the shared root rule filter and committed together with the
		 * rest of the desired root marker set in the single atomic root write.
		 */
		public function maybe_queue_root_htaccess_acme_exclusion_update(): void {

		if ( ! rsssl_user_can_manage() ) {
			return;
		}

		if ( ! function_exists( 'rsssl_schedule_htaccess_update' ) ) {
			return;
		}

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

		$htaccess_file = $this->htaccess_file_manager->determineExistingRootHtaccessFilePath();
		if ( ! $this->htaccess_file_manager->is_valid_htaccess_file_path( $htaccess_file ) ) {
			return;
		}

		// Normalize per line so indentation differences do not keep re-queueing the same marker block.
		$current_lines  = array_map( 'trim', $this->htaccess_file_manager->get_rule_lines_for_path( $htaccess_file, 'Really Simple Security LETS ENCRYPT' ) );
		$expected_lines = array_map( 'trim', $this->get_root_htaccess_acme_exclusion_rule_lines() );
		if ( $current_lines === $expected_lines ) {
			return;
		}

		// Queue the shared root writer so ACME exclusions use the same lock/commit path as other root updates.
		rsssl_schedule_htaccess_update();
	}

	/**
	 * Add the root-level ACME bypass block through the shared `.htaccess` rule filter.
	 * HTTP-01 validation must reach `/.well-known/` before the plugin's broader root rewrites or protections run.
	 */
	public function add_root_htaccess_acme_exclusion_rule( array $rules ): array
	{
		if ( ! $this->should_include_root_htaccess_acme_exclusion_rule() ) {
			return $rules;
		}

		$rules[] = [
			'identifier' => 'Really Simple Security LETS ENCRYPT',
			'rules'      => implode( "\n", $this->get_root_htaccess_acme_exclusion_rule_lines() ),
		];

		return $rules;
	}

	/**
	 * Only emit the ACME bypass block when the installation uses a real root `.htaccess` target.
	 */
	private function should_include_root_htaccess_acme_exclusion_rule(): bool
	{
		if ( ! RSSSL()->server->uses_htaccess() ) {
			return false;
		}

		return $this->htaccess_file_manager->determineExistingRootHtaccessFilePath() !== '';
	}

	/**
	 * Return the literal ACME bypass lines that the shared root writer should own.
	 */
	private function get_root_htaccess_acme_exclusion_rule_lines(): array
	{
		return [
			'RewriteRule ^.well-known/(.*)$ - [L]',
		];
	}

	/**
	 * Check if we have an installation failed state.
	 * @return bool
	 */
	public function installation_failed(){
		$installation_active = get_option("rsssl_le_start_installation");
		$installation_failed = get_option("rsssl_installation_error");

		return $installation_active && $installation_failed;
	}

	/**
	 * Cleanup. If user did not consent to storage, all password fields should be removed on activation, unless they're needed for renewals
	 *
	 * @return bool
	 */
	public function cleanup_on_ssl_activation(){
		if ( !current_user_can('manage_security') ) {
			return false;
		}
		$delete_credentials = !rsssl_get_option('store_credentials');
		$le_fields = rsssl_le_add_fields([]);
		if ( !$this->certificate_automatic_install_possible() || !$this->certificate_install_required() || $delete_credentials ) {
			$le_fields = array_filter($le_fields, function($i){
				return isset( $i['type'] ) && $i['type'] === 'password';
			});
			$options = get_option( 'rsssl_options' );
			foreach ($le_fields as $index => $field ) {
				unset($options[$field['id']]);
			}
			update_option( 'rsssl_options', $options, false );
		}
		return true;
	}

	/**
	 * some custom actions after a field has been saved
	 * @param string $fieldname
	 * @param mixed $fieldvalue
	 * @param mixed $prev_value
	 * @param string $type
	 *
	 * @return void
	 */
	public function after_save_field(
		$fieldname, $fieldvalue, $prev_value, $type
	) {
		rsssl_progress_add( 'domain' );
		//only run when changes have been made
		if ( $fieldvalue === $prev_value ) {
			return;
		}

		$options = [
			'directadmin_password',
			'cpanel_password',
			'cloudways_api_key',
			'plesk_password',
		];

		if ( in_array($fieldname, $options) && strpos( $fieldvalue, 'rsssl_' ) === false ) {
			rsssl_update_option($fieldname, $this->encrypt_with_prefix($fieldvalue) );
		}

		if ( $fieldname==='other_host_type' ){
			if ( !rsssl_do_local_lets_encrypt_generation() ) {
				rsssl_progress_add('directories');
				rsssl_progress_add('generation');
				rsssl_progress_add('dns-verification');
			}
		}

		if ( $fieldname==='email' ){
		    if ( !is_email($fieldvalue) ) {
		        rsssl_progress_remove('domain');
            }
        }
	}

	/**
     * Test for localhost or subfolder usage
	 * @return RSSSL_RESPONSE
	 */
    public function check_domain(){
	    $details = parse_url(site_url());
	    $path = isset($details['path']) ? $details['path'] : '';
        if ( strpos(site_url(), 'localhost')!==false ) {
	        rsssl_progress_remove( 'system-status' );
	        $action  = 'stop';
	        $status  = 'error';
	        $message = __( "It is not possible to install Let's Encrypt on a localhost environment.", "really-simple-ssl" );
        } else if (is_multisite() && get_current_blog_id() !== get_main_site_id() ) {
		    rsssl_progress_remove('system-status');
		    $action = 'stop';
		    $status = 'error';
		    $message = __("It is not possible to install Let's Encrypt on a subsite. Please go to the main site of your website.", "really-simple-ssl" );
	    } else if ( strlen($path)>0 ) {
		    rsssl_progress_remove('system-status');
		    $action = 'stop';
		    $status = 'error';
		    $message = __("It is not possible to install Let's Encrypt on a subfolder configuration.", "really-simple-ssl" ).rsssl_le_read_more(rsssl_link('install-ssl-on-subfolders') );
	    } elseif ( rsssl_caa_record_prevents_le() ) {
		    $action = 'stop';
		    $status = 'error';
		    $message = __("Please adjust the CAA records via your DNS provider to allow Let’s Encrypt SSL certificates", "really-simple-ssl" ).rsssl_le_read_more(rsssl_link('instructions/edit-dns-caa-records-to-allow-lets-encrypt-ssl-certificates/') );
	    } else {
		    $action = 'continue';
		    $status = 'success';
		    $message = __("Your domain meets the requirements for Let's Encrypt.", "really-simple-ssl" );
	    }
	    return new RSSSL_RESPONSE($status, $action, $message);
    }

	/**
	 * Get certificate installation URL
	 * @return RSSSL_RESPONSE
	 */

    public function search_ssl_installation_url(){
    	//start with most generic, then more specific if possible.
	    $url = rsssl_link('install-ssl-certificate');
	    $host = 'enter-your-dashboard-url-here';

	    if (function_exists('wp_get_direct_update_https_url') && !empty(wp_get_direct_update_https_url())) {
		    $url = wp_get_direct_update_https_url();
	    }

	    if ( rsssl_is_cpanel() ) {
		    $cpanel = new rsssl_cPanel();
		    $host = $cpanel->host;
		    $url = $cpanel->ssl_installation_url;
	    } else if ( rsssl_is_plesk() ) {
		    $plesk = new rsssl_plesk();
		    $host = $plesk->host;
		    $url = $plesk->ssl_installation_url;
	    } else if ( rsssl_is_directadmin() ) {
		    $directadmin = new rsssl_directadmin();
		    $host = $directadmin->host;
		    $url = $directadmin->ssl_installation_url;
	    }

	    $hosting_company = rsssl_get_other_host();
	    if ( $hosting_company && $hosting_company !== 'none' ) {
		    $hosting_specific_link = RSSSL_LE()->hosts->getKnownHosts()[$hosting_company]['ssl_installation_link'];
		    if ($hosting_specific_link) {
			    $site = trailingslashit( str_replace(array('https://','http://', 'www.'),'', site_url()) );
			    if ( strpos($hosting_specific_link,'{host}') !==false && empty($host) ) {
			    	$url = '';
			    } else {
				    $url = str_replace(array('{host}', '{domain}'), array($host, $site), $hosting_specific_link);
			    }
		    }
	    }

	    $action = 'continue';
	    $status = 'warning';
	    $message = rsssl_get_manual_instructions_text($url);
		$output = $url;
	    return new RSSSL_RESPONSE($status, $action, $message, $output );
    }

    /**
     * Test for localhost usage
	 * @return RSSSL_RESPONSE
	 */
    public function certificate_status(){
	    delete_transient('rsssl_certinfo');
	    if ( RSSSL()->certificate->is_valid() ) {
	    	//we have now renewed the cert info transient
		    $certinfo = get_transient('rsssl_certinfo');
		    $end_date = isset($certinfo['validTo_time_t']) ? $certinfo['validTo_time_t'] : false;
		    $grace_period = strtotime('+'.rsssl_le_manual_generation_renewal_check.' days');
		    $expiry_date = !empty($end_date) ? date( get_option('date_format'), $end_date ) : __("(unknown)","really-simple-ssl");
		    //if the certificate expires within the grace period, allow renewal
		    //e.g. expiry date 30 may, now = 10 may => grace period 9 june.
		    if ( $grace_period > $end_date ) {
			    $action = 'continue';
			    $status = 'success';
			    $message = sprintf(__("Your certificate will expire on %s.", "really-simple-ssl" ).' '.__("Continue to renew.", "really-simple-ssl" ), $expiry_date);   ;
		    } else {
			    $action = 'continue';
			    $status = 'error';
			    $message = __("You already have a valid SSL certificate.", "really-simple-ssl" );
		    }

	    } else {
		    $action = 'continue';
		    $status = 'success';
		    $message = __("SSL certificate should be generated and installed.", "really-simple-ssl" );
	    }
	    return new RSSSL_RESPONSE($status, $action, $message);
    }

	/**
	 * Check if the certifiate is to expire in max rsssl_le_manual_generation_renewal_check days.
	 * Used in notices list
	 * @return bool
	 */

	public function certificate_about_to_expire() {
		$about_to_expire = RSSSL()->certificate->about_to_expire();
		if ( !$about_to_expire ) {
			//if the certificate is valid, stop any attempt to renew.
			delete_option('rsssl_le_start_renewal');
			delete_option('rsssl_le_start_installation');
			return false;
		} else {
			return true;
		}
	}

    /**
     * Test for server software
	 * @return RSSSL_RESPONSE
	 */

	public function server_software(){
	    $action = 'continue';
	    $status = 'warning';
	    $message = __("The Hosting Panel software was not recognized. Depending on your hosting provider, the generated certificate may need to be installed manually.", "really-simple-ssl" );

        if ( rsssl_is_cpanel() ) {
	        $status = 'success';
	        $message = __("CPanel recognized. Possibly the certificate can be installed automatically.", "really-simple-ssl" );
        } else if ( rsssl_is_plesk() ) {
	        $status = 'success';
	        $message = __("Plesk recognized. Possibly the certificate can be installed automatically.", "really-simple-ssl" );
        } else if ( rsssl_is_directadmin() ) {
			$status = 'success';
			$message = __("DirectAdmin recognized. Possibly the certificate can be installed automatically.", "really-simple-ssl" );
		}

		return new RSSSL_RESPONSE($status, $action, $message);
    }

	/**
	 * Check if CURL is available
	 *
	 * @return RSSSL_RESPONSE
	 */

    public function curl_exists() {
	    if( function_exists('curl_init') === false ){
		    $action = 'stop';
		    $status = 'error';
		    $message = __("The PHP function CURL is not available on your server, which is required. Please contact your hosting provider.", "really-simple-ssl" );
	    } else {
		    $action = 'continue';
		    $status = 'success';
		    $message = __("The PHP function CURL has successfully been detected.", "really-simple-ssl" );
	    }

	    return new RSSSL_RESPONSE($status, $action, $message);
    }

	/**
	 * Get or create an account
	 * @return RSSSL_RESPONSE
	 */
    public function get_account() {
	    $account_email = $this->account_email();
        if ( is_email($account_email) ) {
	        try {
		        $this->account
			        = ! Account::exists( $account_email ) ?
			        Account::create( $account_email ) :
			        Account::get( $account_email );
		        $status = 'success';
		        $action = 'continue';
		        $message = __("Successfully retrieved account", "really-simple-ssl");
	        } catch(Exception $e) {
		        $response = $this->get_error($e);
		        $status = 'error';
		        $action = 'retry';
		        if ( strpos($response, 'invalid contact domain')) {
		        	$action = 'stop';
		        	$response = __("The used domain for your email address is not allowed.","really-simple-ssl").'&nbsp;'.
			                    sprintf(__("Please change your email address %shere%s and try again.", "really-simple-ssl"),'<a href="'.rsssl_letsencrypt_wizard_url().'&step=2'.'">','</a>');
		        }

		        $message = $response;
	        }
        } else {
	        $status = 'error';
	        $action = 'stop';
	        $message = __("The email address was not set. Please set the email address",'really-simple-ssl');
        }
	    return new RSSSL_RESPONSE($status, $action, $message);
    }

	/**
	 * @return RSSSL_RESPONSE
	 */
    public function get_dns_token()
    {
	    if ( rsssl_is_ready_for('dns-verification') ) {
		    $use_dns        = rsssl_dns_verification_required();
		    $challenge_type = $use_dns ? Order::CHALLENGE_TYPE_DNS : Order::CHALLENGE_TYPE_HTTP;
		    if ( $use_dns ) {
			    try {
				    $this->get_account();
				    $dnsWriter = new class extends AbstractDNSWriter {
					    public function write( Order $order, string $identifier, string $digest ): bool {
						    $tokens                = get_option( 'rsssl_le_dns_tokens', [] );
						    $tokens[ $identifier ] = $digest;
						    update_option( "rsssl_le_dns_tokens", $tokens, false );
						    rsssl_progress_add( 'dns-verification' );

						    //return false, as we will continue later on.
						    return false;
					    }
				    };
				    DNS::setWriter( $dnsWriter );
				    $response = $this->get_order();
				    $order = $response->output;
				    $response->output = false;

				    if ( $order ) {
					    try {
						    if ( $order->authorize( $challenge_type ) ) {
							    $response = new RSSSL_RESPONSE(
								    'success',
								    'continue',
								    __( "Token successfully retrieved. Click the refresh button if it's not visible yet.", 'really-simple-ssl' ),
								    $this->get_dns_tokens()
							    );
						    } else {
							    if ( get_option( 'rsssl_le_dns_tokens' ) ) {
								    $response = new RSSSL_RESPONSE(
									    'success',
									    'continue',
									    __( "Token successfully retrieved. Click the refresh button if it's not visible yet.", 'really-simple-ssl' ),
									    $this->get_dns_tokens()
								    );
							    } else {
								    $response = new RSSSL_RESPONSE(
									    'error',
									    'retry',
									    __( "Token not received yet.", 'really-simple-ssl' )
								    );
							    }

						    }
					    } catch ( Exception $e ) {
						    $error = $this->get_error( $e );
						    if ( strpos( $error, 'No challenge found with given type')!==false ) {
							    //Maybe it was first set to HTTP challenge. retry after clearing the order.
							    $this->clear_order(true);
						    } else if (strpos($error, 'Keys exist already')!==false) {
							    $this->clear_order(true);
							    $error = __("DNS token not retrieved.", 'really-simple-ssl').' '.__("There are existing keys, the order had to be cleared first.","really-simple-ssl")." ".__("Please start at the previous step.","really-simple-ssl");
						    } else if (strpos($error, 'Order has status "invalid"')!==false) {
							    $this->clear_order();
							    $error = __("DNS token not retrieved.", 'really-simple-ssl').' '.__("The order is invalid, possibly due to too many failed authorization attempts. Please start at the previous step.","really-simple-ssl");
						    } else
						    //fixing a plesk bug
						    if ( strpos($error, 'No order for ID ') !== FALSE){
							    $error .= '&nbsp;'.__("Order ID mismatch, regenerate order.","really-simple-ssl");
							    $this->clear_order();
							    rsssl_progress_remove('dns-verification');
							    $error .= '&nbsp;'.__("If you entered your DNS records before, they need to be changed.","really-simple-ssl");
						    }
						    $response = new RSSSL_RESPONSE(
							    'error',
							    'retry',
							    $error
						    );
					    }
				    } else {
					    if (strpos($response->message, 'Keys exist already')!==false) {
						    $this->clear_order(true);
						    $response->message = __("DNS token not retrieved.", 'really-simple-ssl').' '.__("There are existing keys, the order had to be cleared first.","really-simple-ssl")." ".__("Please start at the previous step.","really-simple-ssl");
					    }
					    return $response;
				    }
			    } catch ( Exception $e ) {
				    rsssl_progress_remove( 'dns-verification' );
				    $response = $this->get_error( $e );
					$response = new RSSSL_RESPONSE(
						'error',
						'retry',
						$response
					);
			    }
		    } else {
			    $response = new RSSSL_RESPONSE(
				    'error',
				    'stop',
				    __( "Configured for HTTP challenge", 'really-simple-ssl' )
			    );
		    }
	    } else {
		    rsssl_progress_remove( 'dns-verification' );
		    $response = new RSSSL_RESPONSE(
			    'error',
			    'stop',
			    $this->not_completed_steps_message('dns-verification')
		    );
	    }
	    return $response;
    }

	/**
     * Get the DNS tokens
	 */
	public function get_dns_tokens(): array
    {
		$tokens = get_option( 'rsssl_le_dns_tokens', [] );
		$output = [];
		foreach ($tokens as $domain => $token ) {
			$output[] = [
				'domain' => $domain,
				'token' => $token,
			];
		}
		return $output;
	}

	/**
	 * Check DNS txt records.
	 */
	public function verify_dns(): RSSSL_RESPONSE
    {
		if ( rsssl_is_ready_for('generation') ) {
			// Reset state at the start of every verification run; we only
			// flip it back to true if every non-wildcard identifier matches.
			update_option('rsssl_le_dns_records_verified', false, false );

			$tokens = get_option('rsssl_le_dns_tokens');
			if ( !$tokens) {
				$status = 'error';
				$action = 'stop';
				$message = __('Token not generated. Please complete the previous step.',"really-simple-ssl");
				return new RSSSL_RESPONSE($status, $action, $message);
			}

			$skip_dns_check       = (bool) get_option('rsssl_skip_dns_check');
			$failure_action       = $skip_dns_check ? 'continue' : 'stop';
			$all_verified         = true;
			$last_failure         = null;
			$last_success_message = '';
			$checked_count        = 0;

			foreach ($tokens as $identifier => $token){
				if (strpos($identifier, '*') !== false) {
					continue;
				}

				$checked_count++;
				set_error_handler(array($this, 'custom_error_handling'));
				ini_set('dns_cache_expiry', 0);
				$dns_records = dns_get_record( "_acme-challenge.$identifier", DNS_TXT );
				restore_error_handler();

				if ( ! is_array( $dns_records ) ) {
					$dns_records = array();
				}

				$txt_values = array_values( array_filter(
					array_column( $dns_records, 'txt' ),
					static function ( $value ) { return is_string( $value ) && $value !== ''; }
				) );

				if ( in_array( $token, $txt_values, true ) ) {
					$last_success_message = sprintf(
						__('Successfully verified DNS records', "really-simple-ssl"),
						"_acme-challenge.$identifier"
					);
					continue;
				}

				$all_verified = false;

				if ( ! empty( $txt_values ) ) {
					// We found TXT records but none match our token. Surface
					// the first one so the user has a concrete hint.
					$ttl = $dns_records[0]['ttl'] ?? 0;
					$ttl = $this->format_duration( $ttl );
					$last_failure = new RSSSL_RESPONSE(
						'error',
						$failure_action,
						sprintf(
							__('The DNS response for %s was %s, while it should be %s.', "really-simple-ssl"),
							"_acme-challenge.$identifier",
							implode( ', ', $txt_values ),
							$token
						) . ' ' .
						sprintf(
							__("Please wait %s before trying again, as this is the expiration of the DNS record currently.", 'really-simple-ssl'),
							$ttl
						)
					);
				} else {
					$last_failure = new RSSSL_RESPONSE(
						'warning',
						$failure_action,
						sprintf(
							__('Could not verify TXT record for domain %s', "really-simple-ssl"),
							"_acme-challenge.$identifier"
						)
					);
				}
			}

			if ( $checked_count === 0 ) {
				// All identifiers were wildcards; nothing to verify locally.
				update_option('rsssl_le_dns_records_verified', true, false );
				return new RSSSL_RESPONSE(
					'success',
					'continue',
					__('Successfully verified DNS records', "really-simple-ssl")
				);
			}

			if ( $all_verified ) {
				update_option('rsssl_le_dns_records_verified', true, false );
				return new RSSSL_RESPONSE(
					'success',
					'continue',
					$last_success_message !== '' ? $last_success_message : __('Successfully verified DNS records', "really-simple-ssl")
				);
			}

			return $last_failure;
		}

		return new RSSSL_RESPONSE(
			'error',
			'stop',
			$this->not_completed_steps_message('dns-verification')
		);
	}

	private function format_duration($seconds)
    {
		$seconds = (int) $seconds;
		if ($seconds >= 3600) {
			$hours = floor($seconds / 3600);
			$minutes = floor(($seconds % 3600) / 60);
			$secs = $seconds % 3600 % 60;
			return sprintf(__("%d:%02d:%02d hours", 'really-simple-ssl'), $hours, $minutes, $secs);
		} elseif ($seconds >= 60) {
			$minutes = floor($seconds / 60);
			$secs = $seconds % 60;
			return sprintf(__("%d:%02d minutes", 'really-simple-ssl'), $minutes, $secs);
		} else {
			return sprintf(__("%d seconds", 'really-simple-ssl'), $seconds);
		}
	}

	/**
	 * Clear an existing order
	 */
	public function clear_order( $clear_keys = false )
    {
		if ( $clear_keys ) {
			$this->clear_keys_directory();
		}
		$this->get_account();
		if ( $this->account ) {
			$response = $this->get_order();
			$order    = $response->output;
			if ( $order ) {
				$order->clear();
			}
		}
	}

	/**
     * Authorize the order
	 */
    public function create_bundle_or_renew(): RSSSL_RESPONSE
    {
	    $bundle_completed = false;
    	$use_dns = rsssl_dns_verification_required();
	    $attempt_count = (int) get_transient( 'rsssl_le_generate_attempt_count' );
	    if ( $attempt_count>10 ){
		    delete_option("rsssl_le_start_renewal");
		    $message = __("The certificate generation was rate limited for 5 minutes because the authorization failed.",'really-simple-ssl');
		    if ($use_dns){
			    $message .= '&nbsp;'.__("Please double check your DNS txt record.",'really-simple-ssl');
		    }
		    return new RSSSL_RESPONSE(
			    'error',
			    'stop',
			    $message
		    );
	    }

	    if ( !get_option('rsssl_skip_dns_check') ) {
		    if ( $use_dns && ! get_option( 'rsssl_le_dns_records_verified' ) ) {
			    return new RSSSL_RESPONSE(
				    'error',
				    'stop',
				    __( "DNS records were not verified yet. Please complete the previous step.", 'really-simple-ssl' )
			    );
	        }
	    }

	    if ( rsssl_is_ready_for('generation') ) {

		    $this->get_account();
			if ( $use_dns ) {
				if ( defined('WP_DEBUG') && WP_DEBUG ) {
					error_log( "DNS verified: " . get_option( 'rsssl_le_dns_records_verified' ) );
					error_log( "Skip DNS check: " . get_option( 'rsssl_skip_dns_check' ) );
				}
				$dnsWriter = new class extends AbstractDNSWriter {
					public function write( Order $order, string $identifier, string $digest): bool {
						$status = false;
						if ( get_option('rsssl_le_dns_tokens') ) {
							$status = true;
						}
						return $status;
					}
				};
				DNS::setWriter($dnsWriter);
			}

		    $response = $this->get_order();
			$order = $response->output;
		    $response->output = false;

		    if ( $order ) {
			    if ( $order->isCertificateBundleAvailable() ) {

				    try {
					    $order->enableAutoRenewal();
					    $bundle_completed = $this->update_certificate_paths($order);
					    if ( $bundle_completed ) {
						    $response = new RSSSL_RESPONSE(
							    'success',
							    'continue',
							    __("Certificate already generated. It was renewed if required.",'really-simple-ssl')
						    );
					    } else {
						    $response = new RSSSL_RESPONSE(
							    'error',
							    'retry',
							    __("Files not created yet...",'really-simple-ssl')
						    );
					    }
				    } catch ( Exception $e ) {
					    $response = new RSSSL_RESPONSE(
						    'error',
						    'retry',
						    $this->get_error( $e )
					    );
					    $bundle_completed = false;
				    }
			    } else {
				    $finalized = false;
			    	$challenge_type = $use_dns ? Order::CHALLENGE_TYPE_DNS : Order::CHALLENGE_TYPE_HTTP;
				    try {
					    if ( $order->authorize( $challenge_type ) ) {
						    $order->finalize();
						    $this->reset_attempt();
						    $finalized = true;
					    } else {
							$this->count_attempt();
						    $response = new RSSSL_RESPONSE(
							    'error',
							    'retry',
							    __('Authorization not completed yet.',"really-simple-ssl")
						    );
						    $bundle_completed = false;
					    }
				    } catch ( Exception $e ) {
					    $this->count_attempt();
					    $message = $this->get_error( $e );
						if ( defined('WP_DEBUG') && WP_DEBUG ) {
							error_log("Really Simple Security: ".$message);
						}
					    $response = new RSSSL_RESPONSE(
						    'error',
						    'stop',
						    $message
					    );
					    if ( strpos( $message, 'No challenge found with given type')!==false ) {
						    //Maybe it was first set to HTTP challenge. retry after clearing the order.
						    $response->message = __("Due to a change in challenge type, the order had to be reset. Please start at the previous step.","really-simple-ssl");
						    $this->clear_order(true);
					    } else if ( strpos($message, 'Order has status "invalid"')!==false) {
						    $this->clear_order();
						    $response->message = __("Certificate not created.", 'really-simple-ssl').' '.__("The order is invalid, possibly due to too many failed authorization attempts. Please start at the previous step.","really-simple-ssl");
					        if ($use_dns) {
					        	rsssl_progress_remove('dns-verification');
						        $response->message .= '&nbsp;'.__("As your order will be regenerated, you'll need to update your DNS text records.","really-simple-ssl");
					        }
					    } else {
					    	//if OCSP is not disabled yet, and the order status is not invalid, we disable ocsp, and try again.
					    	if ( !rsssl_get_option( 'disable_ocsp' ) ) {
							    rsssl_update_option( 'disable_ocsp', true );
							    $response->action = 'retry';
							    $response->status = 'warning';
							    $response->message = __("OCSP not supported, the certificate will be generated without OCSP.","really-simple-ssl");
						    }
					    }
				    }

					if ( $finalized ) {
					    try {
						    if ( $order->isCertificateBundleAvailable() ) {
							    $bundle_completed  = $this->update_certificate_paths($order);

							    if ( $bundle_completed ) {
								    $response = new RSSSL_RESPONSE(
									    'success',
									    'continue',
									    __("Successfully generated certificate.",'really-simple-ssl')
								    );
							    } else {
								    $response = new RSSSL_RESPONSE(
									    'error',
									    'retry',
									    __("Files not created yet...",'really-simple-ssl')
								    );
							    }

						    } else {
							    $response = new RSSSL_RESPONSE(
								    'error',
								    'retry',
								    __("Bundle not available yet...",'really-simple-ssl')
							    );
						    }
					    } catch ( Exception $e ) {
						    $response = new RSSSL_RESPONSE(
							    'error',
							    'retry',
							    $this->get_error( $e )
						    );
				        }
					}
			    }
		    }
	    } else {
		    $response = new RSSSL_RESPONSE(
		    	'error',
			    'stop',
			    $this->not_completed_steps_message('generation')
		    );
	    }

	    if ( $bundle_completed ){
		    rsssl_progress_add('generation');
		    update_option('rsssl_le_certificate_generated_by_rsssl', true, false);
		    delete_option("rsssl_le_start_renewal");
	    } else {
		    rsssl_progress_remove('generation');
	    }

	    return $response;
    }

	/**
	 * For each file, check if the path exists, update the path options, and
     * return success or false accordingly
	 */
	private function update_certificate_paths($order): bool
    {
		$bundle             = $order->getCertificateBundle();
		$pathToPrivateKey   = $bundle->path . $bundle->private;
		$pathToCertificate  = $bundle->path . $bundle->certificate;
		$pathToIntermediate = $bundle->path . $bundle->intermediate;
		$success_private = $success_cert = $success_intermediate = false;

		if ( file_exists( $pathToPrivateKey ) ) {
			$success_private = true;
			update_option( 'rsssl_private_key_path', $pathToPrivateKey, false );
		}
		if ( file_exists( $pathToCertificate ) ) {
			$success_cert = true;
			update_option( 'rsssl_certificate_path', $pathToCertificate, false );
		}

		if ( file_exists( $pathToIntermediate ) ) {
			$success_intermediate = true;
			update_option( 'rsssl_intermediate_path', $pathToIntermediate, false );
		}

		$bundle_completed = true;
		if ( ! $success_cert || ! $success_private || ! $success_intermediate ) {
			$bundle_completed = false;
		}

		return $bundle_completed;
	}

	/**
	 * Get the order object
	 */
    public function get_order(): RSSSL_RESPONSE
    {
		// if we don't have an account, try to retrieve it
		if ( !$this->account ) {
			$this->get_account();
		}

		// still no account, then exit
		if ( !$this->account ) {
			return new RSSSL_RESPONSE(
				'error',
				'retry',
				__( "Failed retrieving account.", 'really-simple-ssl' )
			);
		}

	    if ( ! Order::exists( $this->account, $this->subjects ) ) {
		    try {
			    $response = new RSSSL_RESPONSE(
				    'success',
				    'continue',
				    __("Order successfully created.",'really-simple-ssl')
			    );
			    $response->output = Order::create( $this->account, $this->subjects );

		    } catch(Exception $e) {
			    $response = new RSSSL_RESPONSE(
				    'error',
				    'retry',
				    $this->get_error($e)
			    );
		    }
	    } else {
		    //order exists already
		    $response = new RSSSL_RESPONSE(
			    'success',
			    'continue',
			    __( "Order successfully retrieved.", 'really-simple-ssl' )
		    );
		    $response->output = Order::get( $this->account, $this->subjects );
	    }

	    return $response;
    }

	/**
	 * Keep track of certain request counts, to prevent rate limiting by LE
	 */
    public function count_attempt()
    {
	    $attempt_count = (int) get_transient( 'rsssl_le_generate_attempt_count' );
	    $attempt_count++;
	    set_transient('rsssl_le_generate_attempt_count', $attempt_count, 5 * MINUTE_IN_SECONDS);
    }

	public function reset_attempt(){
		delete_transient('rsssl_le_generate_attempt_count');
	}

	/**
	 * Check if SSL generation renewal can be handled automatically
	 */
    public function ssl_generation_can_auto_renew(): bool
    {
	    if ( rsssl_get_option('verification_type')==='dns' && !get_option('rsssl_le_dns_configured_by_rsssl') ) {
		    return false;
	    }

        return true;
    }

	/**
	 * Check if it's possible to autorenew
	 */
	public function certificate_automatic_install_possible(): bool
    {

		$install_method = get_option('rsssl_le_certificate_installed_by_rsssl');

		//if it was never auto installed, we probably can't autorenew.
		if ($install_method === false ) {
			return false;
		}

        return true;
    }

	/**
	 * Check if the manual renewal should start.
	 */
    public function should_start_manual_installation_renewal(): bool
    {
	    if ( !$this->should_start_manual_ssl_generation() && get_option( "rsssl_le_start_installation" ) ) {
			return true;
	    }
	    return false;
    }

	public function should_start_manual_ssl_generation()
    {
		return get_option( "rsssl_le_start_renewal" );
	}

	/**
	 * Only used if
	 * - SSL generated by RSSSL
	 * - certificate is about to expire
	 */
	public function certificate_renewal_status_notice(): string
    {
    	if ( !RSSSL_LE()->letsencrypt_handler->ssl_generation_can_auto_renew()){
		    return 'manual-generation';
	    }

    	if ( $this->certificate_install_required() &&
	         $this->certificate_automatic_install_possible() &&
	         $this->installation_failed()
	    ){
    		return 'automatic-installation-failed';
	    }

    	if ( $this->certificate_install_required() && !$this->certificate_automatic_install_possible() ) {
    		return 'manual-installation';
	    }

    	return 'automatic';
	}

	/**
	 * Check if the certificate has to be installed on each renewal defaults to
     * true.
	 */
    public function certificate_install_required(): bool
    {
	    $install_method = get_option('rsssl_le_certificate_installed_by_rsssl');
	    $hosting_company = rsssl_get_other_host();
	    $hosts = RSSSL_LE()->hosts;
	    if ( in_array($install_method, $hosts->no_installation_renewal_needed) || in_array($hosting_company, $hosts->no_installation_renewal_needed)) {
		    return false;
	    }

        return true;
    }

	/**
     * Check if the certificate needs renewal.
	 */
    public function cron_certificate_needs_renewal(): bool
    {
	    $cert_file = get_option('rsssl_certificate_path');
	    if ( empty($cert_file) ) {
	    	return false;
	    }

	    $certificate = file_get_contents($cert_file);
	    $certificateInfo = openssl_x509_parse($certificate);
	    $valid_to = $certificateInfo['validTo_time_t'];
	    $in_expiry_days = strtotime( "+".rsssl_le_cron_generation_renewal_check." days" );
	    if ( $in_expiry_days > $valid_to ) {
	        return true;
	    }
        return false;
    }


	/**
     * Get account email
     * @return string
	 */
	public function account_email()
    {
	    //don't use the default value: we want users to explicitly enter a value
	    return rsssl_get_option('email_address');
    }

	/**
     * Get terms accepted
	 */
	public function terms_accepted(): RSSSL_RESPONSE
    {
	    //don't use the default value: we want users to explicitly enter a value
	    $accepted =  rsssl_get_option('accept_le_terms');
		if ( $accepted ) {
			$status = 'success';
			$action = 'continue';
			$message = __("Terms & Conditions are accepted.",'really-simple-ssl');
		} else {
			$status = 'error';
			$action = 'stop';
			$message = __("The Terms & Conditions were not accepted. Please accept in the general settings.",'really-simple-ssl');
		}

		return new RSSSL_RESPONSE($status, $action, $message);
    }



	/**
     * Change the email address in an account
	 * @param $new_email
	 */
    public function update_account( $new_email )
    {
	    if (!$this->account) return;

	    try {
	        $this->account->update($new_email);
        } catch (Exception $e) {
        }
    }

	/**
     * Get list of common names on the certificate
	 */
	public function get_subjects(): array
    {
		$subjects = array();
		$domain = rsssl_get_domain();
		$root = str_replace( 'www.', '', $domain );;
		$subjects[] = $domain;
		//don't offer aliasses for subdomains
		if ( !rsssl_is_subdomain() ) {
			if (rsssl_get_option( 'include_alias' )) {
				//main is www.
				if ( strpos( $domain, 'www.' ) !== false ) {
					$alias_domain = $root;
				} else {
					$alias_domain = 'www.'.$root;
				}
				$subjects[] = $alias_domain;
			}
		}

		if ( rsssl_wildcard_certificate_required() ) {
			$domain = rsssl_get_domain();
			//in theory, the main site of a subdomain setup can be a www. domain. But we have to request a certificate without the www.
			$domain   = str_replace( 'www.', '', $domain );
			$subjects = array(
				$domain,
				'*.' . $domain,
			);
		}

	    return apply_filters('rsssl_le_subjects', $subjects);
	}

	/**
     * Check if we're ready for the next step.
	 * @param string $item
	 *
	 * @return array | bool
	 */
	public function is_ready_for($item)
    {
		if ( !rsssl_do_local_lets_encrypt_generation() ) {
			rsssl_progress_add('directories');
			rsssl_progress_add('generation');
			rsssl_progress_add('dns-verification');
		}

		if ( !rsssl_dns_verification_required() ) {
			rsssl_progress_add('dns-verification');
		}

		if (empty(rsssl_get_not_completed_steps($item))){
            return true;
        }

        return false;
	}

	/**
	 * Catch errors
	 *
	 * @since 3.0
	 *
	 * @access public
	 * @param       $errno
	 * @param       $errstr
	 * @param       $errfile
	 * @param       $errline
	 * @param array $errcontext
	 *
	 * @return bool
	 */
	public function custom_error_handling( $errno, $errstr, $errfile, $errline, $errcontext = array() ) {
		return true;
	}

	public function not_completed_steps_message($step)
    {
		$not_completed_steps = rsssl_get_not_completed_steps($step);
		$nice_names = array();
		$steps = rsssl_le_steps();
		foreach ($not_completed_steps as $not_completed_step ) {
			$index = array_search($not_completed_step, array_column( $steps, 'id'));
			$nice_names[] = $steps[$index]['title'];
		}
		return sprintf(__('Please complete the following step(s) first: %s', "really-simple-ssl"), implode(", ", $nice_names) );
	}

	/**
	 * Test for writing permissions
	 * @return RSSSL_RESPONSE
	 */
	public function check_writing_permissions(): RSSSL_RESPONSE
    {
		$directories_without_permissions = $this->directories_without_writing_permissions();
		$has_missing_permissions = count($directories_without_permissions)>0;

		if ( $has_missing_permissions ) {
			rsssl_progress_remove('directories');
			$action = 'stop';
			$status = 'error';
			$message = __("The following directories do not have the necessary writing permissions.", "really-simple-ssl" )."&nbsp;".__("Set permissions to 644 to enable SSL generation.", "really-simple-ssl" );
			foreach ($directories_without_permissions as $directories_without_permission) {
				$message .= "<br> - ".$directories_without_permission;
			}
		} else {
			$action = 'continue';
			$status = 'success';
			$message = __("The required directories have the necessary writing permissions.", "really-simple-ssl" );
		}

		return new RSSSL_RESPONSE($status, $action, $message);
	}

	/**
	 * Verify if a host has been selected, and if so, if this host supports LE, or if it's already active
	 */
	public function check_host(): RSSSL_RESPONSE
    {
		$action = 'continue';
		$status = 'success';
		$message = __("We have not detected any known hosting limitations.", "really-simple-ssl" );
		$host = rsssl_get_other_host();
		if ( $host === 'none' ) $host = false;
	$known_hosts = RSSSL_LE()->hosts->getKnownHosts();
		if ( isset($known_hosts[$host]) ){
			if ( $known_hosts[$host]['free_ssl_available'] === 'paid_only' ) {
				$action = 'stop';
				$status = 'error';
				$message = sprintf(__("According to our information, your hosting provider does not allow any kind of SSL installation, other than their own paid certificate. For an alternative hosting provider with SSL, see this %sarticle%s.","really-simple-ssl"), '<a target="_blank" href="https://really-simple-ssl.com/hosting-providers-with-free-ssl">', '</a>');
			}

			if ( $known_hosts[$host]['free_ssl_available'] === 'activated_by_default' ) {
				$url = $known_hosts[$host]['ssl_installation_link'];
				$action = 'continue';
				$status = 'error';
				$message = sprintf(__("According to our information, your hosting provider supplies your account with an SSL certificate by default. Please contact your %shosting support%s if this is not the case.","really-simple-ssl"), '<a target="_blank" href="'.$url.'">', '</a>').'&nbsp'.
				       __("After completing the installation, you can let Really Simple Security automatically configure your site for SSL by using the 'Activate SSL' button.","really-simple-ssl");
			}
		}
		return new RSSSL_RESPONSE($status, $action, $message);
	}

	/**
	 * Test for directory
	 */
	public function check_challenge_directory(): RSSSL_RESPONSE
    {
		if ( !$this->challenge_directory() ) {
			rsssl_progress_remove('directories');
			$action = 'stop';
			$status = 'error';
			$message = __("The challenge directory is not created yet.", "really-simple-ssl" );
		} else {
			$action = 'continue';
			$status = 'success';
			$message = __("The challenge directory was successfully created.", "really-simple-ssl" );
		}

		return new RSSSL_RESPONSE($status, $action, $message);
	}

	/**
	 * Test for directory
	 */
	public function check_key_directory(): RSSSL_RESPONSE
    {
		$action = 'stop';
		$status = 'error';
		$message = __("The key directory is not created yet.", "really-simple-ssl" );
		//this option is set in the key_dir function, so we need to check it now.
		if ( !get_option('rsssl_create_folders_in_root')) {
			$action = 'retry';
			$message = __("Trying to create directory in root of website.", "really-simple-ssl" );
		}

		if ( !$this->key_directory() ) {
			rsssl_progress_remove('directories');
		} else {
			$action = 'continue';
			$status = 'success';
			$message = __("The key directory was successfully created.", "really-simple-ssl" );
		}
		return new RSSSL_RESPONSE($status, $action, $message);
	}

	/**
	 * Test for directory
	 */
	public function check_certs_directory(): RSSSL_RESPONSE
    {
		if ( !$this->certs_directory() ) {
			rsssl_progress_remove('directories');
			$action = 'stop';
			$status = 'error';
			$message = __("The certs directory is not created yet.", "really-simple-ssl" );
		} else {
			$action = 'continue';
			$status = 'success';
			$message = __("The certs directory was successfully created.", "really-simple-ssl" );
		}
		return new RSSSL_RESPONSE($status, $action, $message);
	}

	/**
	 * Check if our created directories have the necessary writing permissions
	 */

	public function directories_without_writing_permissions( ){
		$required_folders = array(
			$this->key_directory,
			$this->certs_directory,
		);

		if ( !rsssl_dns_verification_required() ) {
			$required_folders[] = $this->challenge_directory;
		}

		$no_writing_permissions = array();
		foreach ($required_folders as $required_folder){
			if (!$this->directory_has_writing_permissions( $required_folder )) {
				$no_writing_permissions[] = $required_folder;
			}
		}

		return $no_writing_permissions;
	}

	/**
	 * Check if a directory has writing permissions
	 * @param string $directory
	 *
	 * @return bool
	 */
	public function directory_has_writing_permissions( $directory ){
		set_error_handler(array($this, 'custom_error_handling'));
		$test_file = fopen( $directory . "/really-simple-ssl-permissions-check.txt", "w" );
		if ( !$test_file ) {
			return false;
		}

		fwrite($test_file, 'file to test writing permissions for Really Simple Security');
		fclose( $test_file );
		restore_error_handler();
		if (!file_exists($directory . "/really-simple-ssl-permissions-check.txt")) {
			return false;
		}
        return true;
	}

	/**
	 * Check if the challenge directory is reachable over the http protocol
	 * @return RSSSL_RESPONSE
	 */
	public function challenge_directory_reachable()
    {
		$file_content = false;
		$status_code = __('no response','really-simple-ssl');
		//make sure we request over http, otherwise the request might fail if the url is already https.
		$url = str_replace('https://', 'http://', site_url('.well-known/acme-challenge/really-simple-ssl-permissions-check.txt'));

		$error_message = sprintf(__( "Could not reach challenge directory over %s.", "really-simple-ssl"), '<a target="_blank" href="'.$url.'">'.$url.'</a>');
		$test_string = 'Really Simple Security';
		$folders = $this->directories_without_writing_permissions();
		if ( !$this->challenge_directory() || count($folders) !==0 ) {
			$status  = 'error';
			$action  = 'stop';
			$message = __( "Challenge directory not writable.", "really-simple-ssl");
			return new RSSSL_RESPONSE($status, $action, $message);
		}

		$response       = wp_remote_get( $url );
		if ( is_array( $response ) ) {
			$status_code       = wp_remote_retrieve_response_code( $response );
			$file_content = wp_remote_retrieve_body( $response );
		}

		if ( $status_code !== 200 ) {
			if (get_option('rsssl_skip_challenge_directory_request')) {
				$status  = 'warning';
				$action = 'continue';
				$message = $error_message.' '.sprintf( __( "Error code %s", "really-simple-ssl" ), $status_code );
			} else {
				$status  = 'error';
				$action = 'stop';
				$message = $error_message.' '.sprintf( __( "Error code %s", "really-simple-ssl" ), $status_code );
				rsssl_progress_remove('directories');
			}
		} else {
			if ( ! is_wp_error( $response ) && ( strpos( $file_content, $test_string ) !== false ) ) {
				$status  = 'success';
				$action  = 'continue';
				$message = __( "Successfully verified alias domain.", "really-simple-ssl" );
				set_transient('rsssl_alias_domain_available', 'available', 30 * MINUTE_IN_SECONDS );
			} else {
				$status  = 'error';
				$action  = 'stop';
				$message = $error_message;
				rsssl_progress_remove('directories');
			}
		}

		return new RSSSL_RESPONSE($status, $action, $message);
	}

	/**
	 * Check if exists, create .well-known/acme-challenge directory if not
     * existing
	 * @return bool|string
	 */
	public function challenge_directory()
    {
		$root_directory = trailingslashit(ABSPATH);
		if ( ! file_exists( $root_directory . '.well-known' ) ) {
			mkdir( $root_directory . '.well-known', 0755 );
		}

		if ( ! file_exists( $root_directory . '.well-known/acme-challenge' ) ) {
			mkdir( $root_directory . '.well-known/acme-challenge', 0755 );
		}

		if ( file_exists( $root_directory . '.well-known/acme-challenge' ) ){
			return $root_directory . '.well-known/acme-challenge';
		}

        return false;
	}

	/**
	 * Get path to location where to create the directories.
	 * @uses apply_filters 'rsssl_le_directory_path'
	 */
	public function get_directory_path(): string
    {
		$root_directory = trailingslashit(ABSPATH);
        $directoryPath = trailingslashit(dirname($root_directory));

		if ( get_option('rsssl_create_folders_in_root') ) {
			if ( !get_option('rsssl_ssl_dirname') ) {
				$token = str_shuffle ( time() );
				update_option('rsssl_ssl_dirname', $token, false );
			}
			if ( ! file_exists( $root_directory . get_option('rsssl_ssl_dirname') ) ) {
				mkdir( $root_directory . get_option('rsssl_ssl_dirname'), 0755 );
			}
            $directoryPath = ($root_directory . trailingslashit( get_option('rsssl_ssl_dirname') ));
		}

        /**
         * Filter: 'rsssl_le_directory_path'
         * Can be used to change the directory path where the ssl/keys directory
         * is created.
         *
         * @param string $directoryPath
         * @return string
         */
        return apply_filters('rsssl_le_directory_path', $directoryPath);
	}

    /**
     * Get path to ssl directory. Note that here is NO trailing slash.
     * @uses apply_filters 'rsssl_le_ssl_path'
     */
    public function get_ssl_path(): string
    {
        $directoryPath = $this->get_directory_path();
        $sslPath = ($directoryPath . 'ssl');

        /**
         * Filter: 'rsssl_le_ssl_path'
         * Can be used to change the ssl path where the ssl/keys directory
         * is created.
         *
         * @param string $sslPath
         * @param string $directoryPath
         * @return string
         */
        return apply_filters('rsssl_le_ssl_path', $sslPath, $directoryPath);
    }

    /**
     * Check if exists, create ssl/certs directory above the wp root if not existing
     * @return bool|string
     */
    public function certs_directory()
    {
        $sslPath = $this->get_ssl_path();

        if (!file_exists($sslPath)) {
            mkdir($sslPath, 0755 );
        }

        if (!file_exists( $sslPath . '/certs')) {
            mkdir( $sslPath . '/certs', 0755 );
        }

        if (file_exists($sslPath . '/certs')) {
            return $sslPath . '/certs';
        }

        return false;
    }

	/**
     * Check if exists, create ssl/keys directory above the wp root if not
	 * @return bool|string
	 */
	public function key_directory()
    {
		$directory = $this->get_directory_path();
        $sslPath = $this->get_ssl_path();

		try {
			$openbasedirHasRestrictions = $this->openbasedir_restriction($directory);

			if ($openbasedirHasRestrictions === false) {
				if (!file_exists($sslPath) && is_writable($directory)) {
					mkdir($sslPath, 0755);
				}

				if (!file_exists($sslPath . '/keys') && is_writable($sslPath)) {
					mkdir($sslPath . '/keys', 0755);
				}

                if (file_exists($sslPath . '/keys')) {
                    return $sslPath . '/keys';
                }
			}

            // If creating the folder has failed, we're on apache, and can write
            // to these folders, we create a root directory.
            $has_writing_permissions = $this->directory_has_writing_permissions($this->challenge_directory);

            // We're guessing that if the challenge dir has writing permissions,
            // the new dir will also have it.
            if (RSSSL()->server->uses_htaccess() && $has_writing_permissions) {
                update_option('rsssl_create_folders_in_root', true, false);
            }
		} catch ( Exception $e ) {
			return false;
		}

        return false;
	}

	/**
	 * Check for openbasedir restrictions
	 */
	private function openbasedir_restriction(string $path): bool
    {

		// Default error handler is required
		set_error_handler(null);

		// Clean last error info.
		error_clear_last();

		// Testing...
		@file_exists($path);

		// Restore previous error handler
		restore_error_handler();

		// Return `true` if error has occurred
		return ($error = error_get_last()) && $error['message'] !== '__clean_error_info';
	}


	/**
	 * Clear the keys directory, used in reset function
	 * @since 5.0
	 */
	public function clear_keys_directory()
    {
		if (!rsssl_user_can_manage()) {
			return;
		}

		$dir = $this->key_directory();
		$this->delete_files_directories_recursively( $dir );
	}

	/**
	 * @param $dir
	 * Delete files and directories recursively. Used to clear the order from keys directory
	 * @since 5.0.11
	 */
	private function delete_files_directories_recursively( $dir ) {
		if ( strpos( $dir, 'ssl/keys' ) !== false ) {
			foreach ( glob( $dir . '/*' ) as $file ) {
				if ( is_dir( $file ) ) {
					$this->delete_files_directories_recursively( $file );
				} else {
					unlink( $file );
				}
			}
			rmdir( $dir );
		}
	}

		/**
		 * Queue the shared managed-htaccess executor when a plugin-owned LE directory `.htaccess` file is missing or stale.
		 *
		 * These files are auxiliary managed targets. They are not folded into the root `.htaccess` write itself,
		 * but they do run through the same centralized executor and atomic file manager.
		 */
		public function maybe_queue_letsencrypt_directory_htaccess_files(): void {
		if ( ! rsssl_user_can_manage() ) {
			return;
		}

		if ( ! RSSSL()->server->uses_htaccess() ) {
			return;
		}

		if ( ! get_option( 'rsssl_create_folders_in_root' ) ) {
			return;
		}

		if ( ! function_exists( 'rsssl_schedule_htaccess_update' ) ) {
			return;
		}

		if ( ! empty( $this->get_directory_path() ) && $this->should_sync_letsencrypt_directory_htaccess_file( $this->get_directory_path() . 'ssl/.htaccess' ) ) {
			rsssl_schedule_htaccess_update();
			return;
		}

		if ( ! empty( $this->key_directory() ) && $this->should_sync_letsencrypt_directory_htaccess_file( trailingslashit( $this->key_directory() ) . '.htaccess' ) ) {
			rsssl_schedule_htaccess_update();
			return;
		}

		if ( ! empty( $this->certs_directory() ) && $this->should_sync_letsencrypt_directory_htaccess_file( trailingslashit( $this->certs_directory() ) . '.htaccess' ) ) {
			rsssl_schedule_htaccess_update();
		}
	}

		/**
		 * Ensure the plugin-owned LE directories keep their deny-all `.htaccess` files in sync.
		 *
		 * The executor calls this after uploads and before root so the overall target order stays deterministic.
		 * Each auxiliary file still gets its own atomic commit; there is no cross-file transaction.
		 */
		public function sync_letsencrypt_directory_htaccess_files(): bool {
		if ( ! RSSSL()->server->uses_htaccess() ) {
			return true;
		}

		if ( ! get_option( 'rsssl_create_folders_in_root' ) ) {
			return true;
		}

		$success = true;

		if ( ! empty( $this->get_directory_path() ) ) {
			$success = $this->sync_letsencrypt_directory_htaccess_file( $this->get_directory_path() . 'ssl/.htaccess', 'ssl' ) && $success;
		}

		if ( ! empty( $this->key_directory() ) ) {
			$success = $this->sync_letsencrypt_directory_htaccess_file( trailingslashit( $this->key_directory() ) . '.htaccess', 'key' ) && $success;
		}

		if ( ! empty( $this->certs_directory() ) ) {
			$success = $this->sync_letsencrypt_directory_htaccess_file( trailingslashit( $this->certs_directory() ) . '.htaccess', 'certs' ) && $success;
		}

		return $success;
	}

	/**
	 * Check whether a plugin-owned LE directory `.htaccess` file is missing or diverges from the managed rule block.
	 */
	private function should_sync_letsencrypt_directory_htaccess_file( string $path ): bool {
		if ( $path === '' ) {
			return false;
		}

		if ( ! is_file( $path ) ) {
			return true;
		}

		return ! $this->has_expected_letsencrypt_directory_htaccess_rules( $path );
	}

	/**
	 * Validate that the managed LE marker block exactly matches the expected deny-all lines.
	 */
	private function has_expected_letsencrypt_directory_htaccess_rules( string $path ): bool {
		return $this->htaccess_file_manager->get_rule_lines_for_path( $path, 'Really Simple Security LETS ENCRYPT' ) === $this->get_letsencrypt_directory_htaccess_lines();
	}

    /**
     * Write one plugin-owned LE directory `.htaccess` file through the shared atomic manager path.
     *
     * This is intentionally path-scoped. LE directory files reuse the same lock hierarchy and atomic writer as
     * root/uploads, but create permission is granted only for the current auxiliary target.
     */
    public function sync_letsencrypt_directory_htaccess_file( string $path, string $type ): bool {
		if ( $path === '' ) {
			return true;
		}

		// Keep preconditions local so LE directory writes do not rely on global root .htaccess state.
		if ( ! $this->ensure_htaccess_directory_is_ready( $path ) ) {
			update_option( 'rsssl_htaccess_file_set_' . $type, false, false );
			return false;
		}

		// Use the manager atomic write path to avoid partial/competing writes on concurrent requests.
		if ( ! $this->write_letsencrypt_htaccess_rules_atomic( $path ) ) {
			update_option( 'rsssl_htaccess_file_set_' . $type, false, false );
			return false;
		}

		if ( ! $this->has_expected_letsencrypt_directory_htaccess_rules( $path ) ) {
			update_option( 'rsssl_htaccess_file_set_' . $type, false, false );
			return false;
		}

		update_option( 'rsssl_htaccess_file_set_' . $type, true, false );
		return true;
	}

	/**
	 * Remove empty plugin-owned LE placeholders left behind by an interrupted guarded create.
	 * Empty files are never a valid end state here, and removing them lets the next write recreate the file cleanly.
	 */
	private function ensure_htaccess_directory_is_ready( string $path ): bool {
		if ( is_link( $path ) ) {
			return false;
		}

		$directory = dirname( $path );
		if ( ! is_dir( $directory ) && ! wp_mkdir_p( $directory ) ) {
			return false;
		}

		if ( ! is_file( $path ) ) {
			return true;
		}

		$content = @file_get_contents( $path );
		if ( $content === false ) {
			return true;
		}

		if ( trim( $content ) !== '' ) {
			return true;
		}

		// These per-directory files are plugin-owned. An interrupted guarded create can leave an empty file behind,
		// and clearing that placeholder lets the next write use the same guarded create path again.
		if ( ! $this->is_plugin_owned_letsencrypt_htaccess_path( $path ) ) {
			return false;
		}

		return @unlink( $path );
	}

	/**
	 * Only allow placeholder cleanup for the exact plugin-owned LE `.htaccess` targets.
	 */
	private function is_plugin_owned_letsencrypt_htaccess_path( string $path ): bool {
		if ( basename( $path ) !== '.htaccess' ) {
			return false;
		}

		if ( ! get_option( 'rsssl_create_folders_in_root' ) ) {
			return false;
		}

		$ssl_dirname = get_option( 'rsssl_ssl_dirname' );
		if ( ! is_string( $ssl_dirname ) || $ssl_dirname === '' ) {
			return false;
		}

		$ssl_dirname = trim( $ssl_dirname, "/\\" );
		if ( $ssl_dirname === '' || strpos( $ssl_dirname, '/' ) !== false || strpos( $ssl_dirname, '\\' ) !== false || strpos( $ssl_dirname, '..' ) !== false ) {
			return false;
		}

		$normalized_path = $this->normalize_htaccess_target_path( $path );
		// Use the canonical root-owned LE directory here, not the filtered runtime paths, so cleanup never
		// expands the unlink scope beyond the plugin-managed targets created under ABSPATH.
		$ssl_root = trailingslashit( trailingslashit( ABSPATH ) . $ssl_dirname ) . 'ssl';
		$allowed_paths = [
			$this->normalize_htaccess_target_path( $ssl_root . '/.htaccess' ),
			$this->normalize_htaccess_target_path( $ssl_root . '/keys/.htaccess' ),
			$this->normalize_htaccess_target_path( $ssl_root . '/certs/.htaccess' ),
		];

		return in_array( $normalized_path, $allowed_paths, true );
	}

	/**
	 * Normalize `.htaccess` target paths before comparing scoped create permissions.
	 */
	private function normalize_htaccess_target_path( string $path ): string {
		$normalized_path = function_exists( 'wp_normalize_path' ) ? wp_normalize_path( $path ) : str_replace( '\\', '/', $path );
		return rtrim( $normalized_path, '/' );
	}

	/**
	 * Scope create permission to the current LE directory target without leaking that allowance to root/uploads writes.
	 *
	 * `write_rule_for_path()` still reaches the shared atomic manager path, so LE writes keep the same advisory
	 * locking, temp-swap commit, and fallback semantics as the rest of the `.htaccess` system.
	 */
	private function write_letsencrypt_htaccess_rules_atomic( string $path ): bool {
		$manager = $this->htaccess_file_manager;
		$normalized_path = $this->normalize_htaccess_target_path( $path );
		// Temporarily allow creation only for this LE directory file. The shared manager also writes root
		// and uploads rules, so this filter must stay scoped to the current target and be removed again.
		$allow_create_filter = function( bool $allow, string $candidate_path ) use ( $normalized_path ): bool {
			if ( $this->normalize_htaccess_target_path( $candidate_path ) !== $normalized_path ) {
				return $allow;
			}

			return true;
		};
		add_filter( 'rsssl_allow_create_htaccess', $allow_create_filter, 9999, 2 );
		try {
			return $manager->write_rule_for_path(
				$path,
				[
					'marker' => 'Really Simple Security LETS ENCRYPT',
					'lines'  => $this->get_letsencrypt_directory_htaccess_lines(),
				]
			);
		} finally {
			remove_filter( 'rsssl_allow_create_htaccess', $allow_create_filter, 9999 );
		}
	}

	/**
	 * Return the deny-all block that protects plugin-owned LE directories from direct web access.
	 */
	private function get_letsencrypt_directory_htaccess_lines(): array {
		return [
			'<IfModule mod_authz_core.c>',
			'    Require all denied',
			'</IfModule>',
			'<IfModule !mod_authz_core.c>',
			'    Deny from all',
			'</IfModule>',
		];
	}

	/**
	 * Check if it's a subdomain multisite
	 * @return RSSSL_RESPONSE
	 */
	public function is_subdomain_setup(){
		if ( !is_multisite() ) {
			$is_subdomain = false;
		} else {
			if ( defined('SUBDOMAIN_INSTALL') && SUBDOMAIN_INSTALL ) {
				$is_subdomain = true;
			} else {
				$is_subdomain = false;
			}
		}

		if ($is_subdomain) {
			$status  = 'error';
			$action  = 'stop';
			$message = sprintf(__("This is a multisite configuration with subdomains. You should generate a wildcard certificate on the root domain.",'really-simple-ssl'), '<a href="'.rsssl_link('pro','error', 'letsencrypt').'" target="_blank">','</a>');
			rsssl_progress_remove('system-status');
		} else {
			$status  = 'success';
			$action  = 'continue';
			$message = __("No subdomain setup detected.","really-simple-ssl");
		}

		return new RSSSL_RESPONSE($status, $action, $message);
	}

	/**
	 * Check if we're about to create a wilcard certificate
	 * @return bool
	 */

	public function is_wildcard(){
		$subjects = $this->get_subjects();
		$is_wildcard = false;
		foreach ($subjects as $domain ) {
			if ( strpos($domain, '*') !== false ) {
				$is_wildcard = true;
			}
		}

		return $is_wildcard;
	}

	/**
	 * Check if the alias domain is available
	 *
	 * @return RSSSL_RESPONSE
	 */
	public function alias_domain_available(){
		if ( rsssl_is_subdomain() ) {
			return new RSSSL_RESPONSE('success', 'continue',__("Alias domain check is not relevant for a subdomain","really-simple-ssl"));
		}
		//write a test file to the uploads directory
		$uploads    = wp_upload_dir();
		$upload_dir = trailingslashit($uploads['basedir']);
		$upload_url = trailingslashit($uploads['baseurl']);
		$file_content = false;
		$status_code = __('no response','really-simple-ssl');
		$domain = rsssl_get_domain();

		if ( strpos( $domain, 'www.' ) !== false ) {
			$is_www = true;
			$alias_domain = str_replace( 'www.', '', $domain );
		} else {
			$is_www = false;
			$alias_domain = 'www.'.$domain;
		}

		if ( $is_www ) {
			$message =  __("Please check if the non www version of your site also points to this website.", "really-simple-ssl" );
		} else {
			$message = __("Please check if the www version of your site also points to this website.", "really-simple-ssl" );
		}
		$error_message = __( "Could not verify alias domain.", "really-simple-ssl") .' '. $message.' '. __( "If this is not the case, don't add this alias to your certificate.", "really-simple-ssl");
		//get cached status first.
		$cached_status = get_transient('rsssl_alias_domain_available');
		if ( $cached_status ) {
			if ( $cached_status === 'available' ) {
				$status  = 'success';
				$action  = 'continue';
				$message = __( "Successfully verified alias domain.", "really-simple-ssl" );
			} else {
				$status  = 'warning';
				$action  = 'continue';
				$message = $error_message;
			}
			return new RSSSL_RESPONSE($status, $action, $message);
		}

		if ( ! file_exists( $upload_dir . 'rsssl' ) ) {
			mkdir( $upload_dir . 'rsssl', 0755 );
		}

		$test_string = 'file to test alias domain existence';
		$test_file = $upload_dir . 'rsssl/test.txt';
		file_put_contents($test_file, $test_string );
		$test_url = $upload_url . 'rsssl/test.txt';


		if ( ! file_exists( $test_file ) ) {
			$status = 'error';
			$action = 'stop';
			$message = __("Could not create test folder and file.", "really-simple-ssl").' '.
			           __("Please create a folder 'rsssl' in the uploads directory, with 644 permissions.", "really-simple-ssl");
		} else {
			set_transient('rsssl_alias_domain_available', 'not-available', 30 * MINUTE_IN_SECONDS );
			$alias_test_url = str_replace( $domain, $alias_domain, $test_url );
			//always over http:
			$alias_test_url = str_replace('https://','http://', $alias_test_url);
			$response       = wp_remote_get( $alias_test_url );
			if ( is_array( $response ) ) {
				$status_code       = wp_remote_retrieve_response_code( $response );
				$file_content = wp_remote_retrieve_body( $response );
			}

			if ( $status_code !== 200 ) {
				$status  = 'warning';
				$action  = 'continue';
				$message = $error_message;
				if (intval($status_code) != 0 ) {
					$message .= ' '.sprintf( __( "Error code %s", "really-simple-ssl" ), $status_code );
				}
			} else {
				if ( ! is_wp_error( $response ) && ( strpos( $file_content, $test_string ) !== false ) ) {
					//make sure we only set this value once, during first setup.
					if ( !get_option('rsssl_initial_alias_domain_value_set') ) {
						rsssl_update_option('include_alias', true);
						update_option('rsssl_initial_alias_domain_value_set', true, false);
					}
					$status  = 'success';
					$action  = 'continue';
					$message = __( "Successfully verified alias domain.", "really-simple-ssl" );
					set_transient('rsssl_alias_domain_available', 'available', 30 * MINUTE_IN_SECONDS );
				} else {
					$status  = 'warning';
					$action  = 'continue';
					$message = $error_message;
				}
			}
		}

		return new RSSSL_RESPONSE($status, $action, $message);
	}

	/**
     * Get string error from error message.
	 * @param mixed|LE_ACME2\Exception\InvalidResponse $e
	 *
	 * @return string
	 */
	private function get_error($e){
		$is_raw_response = false;
		if (method_exists($e, 'getRawResponse') && isset($e->getRawResponse()->body['detail'])) {
	    	$is_raw_response = true;
		    $error = $e->getRawResponse()->body['detail'];
		    //check for subproblems
		    if (isset($e->getRawResponse()->body['subproblems'])){
			    $error .= '<ul>';
		        foreach($e->getRawResponse()->body['subproblems'] as $index => $problem) {
			        $error .= '<li>'. $this->cleanup_error_message($e->getRawResponse()->body['subproblems'][$index]['detail']).'</li>';
		        }
			    $error .= '</ul>';
		    }

	    } else {
	        $error = $e->getMessage();
	    }


	    $max = strpos($error, 'CURL response');
	    if ($max===false) {
	    	$max = 200;
	    }
	    if (!$is_raw_response){
		    $error = substr( $error, 0, $max);
	    }
	    return $error;

	}

	/**
	 * Generic SSL cert installation function
	 *
	 * @return RSSSL_RESPONSE
	 */
	public function cron_renew_installation() {
		$install_method = get_option('rsssl_le_certificate_installed_by_rsssl');
		$data = explode(':', $install_method );

		$server = isset($data[0]) ? $data[0] : false;
		$type = isset($data[1]) ? $data[1] : false;

		$attempt_count = (int) get_transient( 'rsssl_le_install_attempt_count' );
		$attempt_count++;
		set_transient('rsssl_le_install_attempt_count', $attempt_count, DAY_IN_SECONDS);
		if ( $attempt_count>10 ){
			delete_option("rsssl_le_start_installation");
			$status = 'error';
			$action = 'stop';
			$message = __("The certificate installation was rate limited. Please try again later.",'really-simple-ssl');
			return new RSSSL_RESPONSE($status, $action, $message);
		}

		if (rsssl_is_ready_for('installation')) {
			try {
				if ( $server === 'cpanel' ) {
					if ($type==='default') {
						$response = rsssl_install_cpanel_default();
					} else if ( function_exists('rsssl_shell_installSSL') ) {
						$response = rsssl_shell_installSSL();
					} else {
						//in case of auto ssl.
						$response = new RSSSL_RESPONSE('error', 'stop', '');
						delete_option( "rsssl_le_start_installation" );
					}

					if ( $response->status === 'success' ) {
						delete_option( "rsssl_le_start_installation" );
					}
					return $response;
				} else if ( $server === 'plesk') {
					$response = rsssl_plesk_install();
					if ( $response->status === 'success' ) {
						delete_option( "rsssl_le_start_installation" );
					}
					return $response;
				} else {
					$status = 'error';
					$action = 'stop';
					$message = __("Not recognized server.", "really-simple-ssl");
				}
			} catch (Exception $e) {
				$status = 'error';
				$action = 'stop';
				$message = __("Installation failed.", "really-simple-ssl");
			}
		} else {
			$status = 'error';
			$action = 'stop';
			$message = __("The system is not ready for the installation yet. Please run the wizard again.", "really-simple-ssl");
		}

		return new RSSSL_RESPONSE($status, $action, $message);
	}

	public function upgrade(){
		if ( get_option('rsssl_upgrade_le_key') ) {
			// Check if the encryption key is not empty before upgrading. On slow servers, the write to wp-config.php can be
			// incomplete before the plugin gets here
			$key = $this->get_encryption_key();
			if ( empty( $key ) ) {
				return;
			}
			delete_option('rsssl_upgrade_le_key');
			$site_key = get_site_option( 'rsssl_le_key');
			if ( $site_key ) {
				$options = [
					'directadmin_password',
					'cpanel_password',
					'cloudways_api_key',
					'plesk_password',
				];
				foreach ( $options as $option ) {
					$option_value = rsssl_get_option( $option );
					if ( $option_value ) {
						$decrypted = $this->decrypt_if_prefixed( $option_value, 'rsssl_', $site_key);
						$encrypted = $this->encrypt_with_prefix($decrypted);
						rsssl_update_option($option, $encrypted);
					}
				}
				delete_site_option( 'rsssl_le_key');
			}
		}
	}

	/**
	 * Cleanup the default message a bit
	 *
	 * @param $msg
	 *
	 * @return string|string[]
	 */
	private function cleanup_error_message($msg){
		return str_replace(array(
			'Refer to sub-problems for more information.',
			'Error creating new order ::',
		), '', $msg);
    }
}
