config = $config; } /** * Register WordPress hooks. * * @return void */ public function register_hooks(): void { add_action( self::CRON_HOOK, array( $this, 'run_cron_update' ) ); add_action( 'init', array( $this, 'maybe_schedule_update' ) ); add_action( 'admin_post_robotstxt_2fa_geoip_update_now', array( $this, 'handle_update_now' ) ); } /** * Resolve the absolute path where the auto-downloaded database is stored. * * @return string Empty string when the uploads directory is unavailable. */ public static function get_database_path(): string { $uploads = wp_upload_dir(); if ( empty( $uploads['basedir'] ) ) { return ''; } return trailingslashit( $uploads['basedir'] ) . 'robotstxt-2fa/country.mmdb'; } /** * Schedule or clear the daily update cron based on the auto-download setting. * * @return void */ public function maybe_schedule_update(): void { if ( $this->config->get_geoip_auto_download() ) { if ( ! wp_next_scheduled( self::CRON_HOOK ) ) { wp_schedule_event( time(), 'daily', self::CRON_HOOK ); } } elseif ( wp_next_scheduled( self::CRON_HOOK ) ) { wp_clear_scheduled_hook( self::CRON_HOOK ); } } /** * Cron callback: refresh the database. * * @return void */ public function run_cron_update(): void { $this->download_database(); } /** * Download the country database, validate it, and store it atomically. * * @return bool True on success, false on failure (status stored for UI). */ public function download_database(): bool { $path = self::get_database_path(); if ( '' === $path ) { $this->store_status( false, __( 'The uploads directory is not writable.', 'robotstxt-2fa' ) ); return false; } $directory = dirname( $path ); if ( ! wp_mkdir_p( $directory ) ) { $this->store_status( false, __( 'Could not create the storage directory for the GeoIP database.', 'robotstxt-2fa' ) ); return false; } $response = wp_remote_get( self::DOWNLOAD_URL, array( 'timeout' => 60, 'redirection' => 5, ) ); if ( is_wp_error( $response ) ) { $this->store_status( false, $response->get_error_message() ); return false; } $code = (int) wp_remote_retrieve_response_code( $response ); $body = wp_remote_retrieve_body( $response ); if ( 200 !== $code || '' === $body ) { /* translators: %d: HTTP response code. */ $this->store_status( false, sprintf( __( 'Download failed (HTTP %d).', 'robotstxt-2fa' ), $code ) ); return false; } if ( false === strpos( $body, self::MAXMIND_MARKER ) ) { $this->store_status( false, __( 'The downloaded file is not a valid MaxMind database.', 'robotstxt-2fa' ) ); return false; } $temp = $path . '.tmp'; $written = file_put_contents( $temp, $body ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- Cron download; WP_Filesystem cannot run unattended. if ( false === $written || 0 === $written ) { $this->store_status( false, __( 'Could not write the GeoIP database file.', 'robotstxt-2fa' ) ); return false; } if ( ! rename( $temp, $path ) ) { // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename -- Atomic replace within the uploads directory. unlink( $temp ); // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink -- Cleanup of the temp file on failure. $this->store_status( false, __( 'Could not finalize the GeoIP database file.', 'robotstxt-2fa' ) ); return false; } $this->store_status( true, '' ); return true; } /** * Persist the outcome of the latest download attempt. * * @param bool $success Whether the download succeeded. * @param string $message Error or status message. * * @return void */ private function store_status( bool $success, string $message ): void { update_option( self::STATUS_OPTION, array( 'success' => $success, 'message' => $message, 'timestamp' => time(), ), false ); } /** * Retrieve the last download status. * * @return array{success: bool, message: string, timestamp: int} */ public static function get_status(): array { $raw = get_option( self::STATUS_OPTION, array() ); if ( ! is_array( $raw ) ) { return array( 'success' => false, 'message' => '', 'timestamp' => 0, ); } return array( 'success' => isset( $raw['success'] ) ? (bool) $raw['success'] : false, 'message' => isset( $raw['message'] ) && is_string( $raw['message'] ) ? $raw['message'] : '', 'timestamp' => isset( $raw['timestamp'] ) && is_numeric( $raw['timestamp'] ) ? (int) $raw['timestamp'] : 0, ); } /** * Build the URL for the manual "Update now" button. * * @return string */ public static function get_update_now_url(): string { return wp_nonce_url( admin_url( 'admin-post.php?action=robotstxt_2fa_geoip_update_now' ), self::UPDATE_NONCE_ACTION, self::UPDATE_NONCE_FIELD ); } /** * Handle the manual "Update now" request from the settings page. * * @return void */ public function handle_update_now(): void { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Sorry, you are not allowed to do this.', 'robotstxt-2fa' ), 403 ); } check_admin_referer( self::UPDATE_NONCE_ACTION, self::UPDATE_NONCE_FIELD ); $this->download_database(); $redirect = add_query_arg( 'geoip_updated', '1', admin_url( 'admin.php?page=robotstxt-2fa-settings' ) ); wp_safe_redirect( $redirect ); exit; } /** * Remove the cron event and the downloaded database file. * * Called from uninstall.php. * * @return void */ public static function cleanup(): void { $timestamp = wp_next_scheduled( self::CRON_HOOK ); if ( $timestamp ) { wp_clear_scheduled_hook( self::CRON_HOOK ); } delete_option( self::STATUS_OPTION ); $path = self::get_database_path(); if ( '' !== $path && file_exists( $path ) && is_file( $path ) ) { unlink( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink -- Removing our own artifact on uninstall. } } }