JezK
Edit File: class-letsencrypt-handler.php
<?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; } /** * 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 $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); } /** * 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").' '. 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 .= ' '.__("Order ID mismatch, regenerate order.","really-simple-ssl"); $this->clear_order(); rsssl_progress_remove('dns-verification'); $error .= ' '.__("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 .= ' '.__("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 .= ' '.__("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; } 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); } /** * 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" )." ".__("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>').' '. __("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(array_keys($e->getRawResponse()->body['subproblems']) as $index) { $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); } }
Paket Wisata Karimunjawa Terlengkap & Harga Terbaik 2026
https://validator.w3.org/feed/docs/rss2.html
Promo Paket Wisata Karimunjawa
Info Harga dan Paket Liburan ke Karimunjawa
Pulau Karimun Jawa
Paket Mancing Karimunjawa
Paket Trip Karimunjawa
Pelabuhan Karimunjawa
Kuliner Khas Karimunjawa
Pantai Bobby Karimunjawa
Whales: Giants of the Ocean
Whales: Giants of the Ocean
Taka Sendok Karimunjawa
Contact US
Informasi Perjalanan Paket Travel Wisata Karimunjawa
Rental Mobil Karimunjawa
Paket Open Trip Karimunjawa
Paket Snorkeling Karimunjawa
Sama Sama Homestay Karimunjawa
Paket Tour Karimunjawa Jepara
Informasi Biaya Liburan ke Karimunjawa
Paket Wisata Karimunjawa 4 Hari 3 Malam
Oleh Oleh Khas Karimunjawa
Promo Open Trip Karimun Jawa
Tentang Kami
Sunset Karimunjawa
Tiket Kapal Karimunjawa
Informasi Reservasi Hotel Resort Java Paradise Karimunjawa
Taman Nasional Karimunjawa
Informasi Harga Paket Wisata Karimunjawa Murah
Informasi Jadwal Kapal Pelni Semarang ke Karimunjawa
Informasi Jadwal Kapal Karimunjawa Ferry Siginjai
Paket Wisata Karimunjawa Rombongan
Tiket Kapal
Rincian Biaya Wisata Karimunjawa
Paket Wisata Karimunjawa 2 Hari 1 Malam
Booking
Informasi Jadwal Kapal Express Karimunjawa
Paket Diving Karimunjawa
Informasi Harga Paket Wisata Karimunjawa 3 Hari 2 Malam
Harga Paket Travel Karimunjawa
Private Trip Karimunjawa
Paket Wisata Karimunjawa dari Solo
Paket Homestay RGJS Karimunjawa
Cara ke Karimunjawa
Pantai Sunset Beach Karimunjawa
Sitemap
Paket Regular Karimunjawa 3 Hari 2 Malam
Cuaca Karimun Jawa
Karimunjawa Dimana
Narayana Resort Karimunjawa
Pantai Laendra Karimunjawa