config = $config; $this->client_factory = $client_factory; $this->logger = $logger; $this->rate_limiter = $rate_limiter; $this->plugin_file = $plugin_file; } /** * Register hooks for the admin page. * * @since 0.3.0 * * @return void */ public function register(): void { add_action( 'admin_menu', array( $this, 'register_settings_page' ), 20 ); add_action( 'admin_init', array( $this, 'register_settings' ) ); add_action( 'admin_init', array( $this, 'handle_test_actions' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ), 20 ); } /** * Register the iDrivee2 settings page under the Settings menu. * * Adds a submenu page to the Settings section of the admin where users * can configure and test their iDrivee2 setup. * * @since 0.3.0 * * @return void */ public function register_settings_page(): void { add_options_page( /* translators: Admin menu title. */ __( 'iDrivee2 Media Upload Settings', 'idrivee2-media-upload' ), /* translators: Admin menu label. */ __( 'iDrivee2', 'idrivee2-media-upload' ), 'manage_options', 'idrivee2-media-upload', array( $this, 'render_settings_page' ) ); } /** * Register settings with WordPress Settings API. * * @since 0.3.0 * * @return void */ public function register_settings(): void { register_setting( 'idrivee2_media_settings_group', 'idrivee2_media_settings', array( 'sanitize_callback' => array( $this, 'sanitize_settings' ), ) ); } /** * Handle test action POST requests. * * Processes test connection, upload test file, and delete test file actions. * * @since 0.3.0 * * @return void */ public function handle_test_actions(): void { // Only process on settings page. $current_page = isset( $_GET['page'] ) && is_string( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; if ( 'idrivee2-media-upload' !== $current_page ) { return; } // Handle test connection. if ( isset( $_POST['idrivee2_test_connection'] ) ) { check_admin_referer( 'idrivee2_test_connection' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'idrivee2-media-upload' ) ); } $this->handle_test_connection(); return; } // Handle upload test file. if ( isset( $_POST['idrivee2_upload_test'] ) ) { check_admin_referer( 'idrivee2_upload_test' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'idrivee2-media-upload' ) ); } $this->handle_upload_test(); return; } // Handle delete test file. if ( isset( $_POST['idrivee2_delete_test'] ) && isset( $_POST['test_file'] ) ) { check_admin_referer( 'idrivee2_delete_test' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'idrivee2-media-upload' ) ); } $raw_test_file = is_string( $_POST['test_file'] ) ? wp_unslash( $_POST['test_file'] ) : ''; $this->handle_delete_test( sanitize_file_name( $raw_test_file ) ); return; } } /** * Handle test connection action. * * @since 0.3.0 * * @return void */ private function handle_test_connection(): void { // Check rate limit. if ( $this->rate_limiter->is_rate_limited( 'test_connection', 60 ) ) { $remaining = $this->rate_limiter->get_remaining_time( 'test_connection', 60 ); set_transient( 'idrivee2_test_result', array( 'type' => 'error', 'message' => sprintf( /* translators: %d is the number of seconds to wait. */ __( 'Please wait %d seconds before testing again.', 'idrivee2-media-upload' ), $remaining ), ), 30 ); wp_safe_redirect( admin_url( 'options-general.php?page=idrivee2-media-upload' ) ); exit; } if ( ! $this->config->is_configured() ) { $this->logger->warning( 'Test connection attempted with incomplete configuration' ); set_transient( 'idrivee2_test_result', array( 'type' => 'error', 'message' => __( 'Configuration is incomplete. Please check your settings.', 'idrivee2-media-upload' ), ), 30 ); wp_safe_redirect( admin_url( 'options-general.php?page=idrivee2-media-upload' ) ); exit; } // Record this action for rate limiting. $this->rate_limiter->record_action( 'test_connection', 60 ); try { $client = $this->client_factory->create(); $client->headBucket( array( 'Bucket' => $this->config->get_bucket() ) ); $this->logger->s3_operation( 'headBucket', true ); set_transient( 'idrivee2_test_result', array( 'type' => 'success', 'message' => __( 'Connection successful! Your S3 configuration is working correctly.', 'idrivee2-media-upload' ), ), 30 ); } catch ( \Aws\Exception\AwsException $e ) { $this->logger->s3_operation( 'headBucket', false, '', $e->getAwsErrorMessage() ?? '' ); set_transient( 'idrivee2_test_result', array( 'type' => 'error', 'message' => sprintf( /* translators: %s is the error message from AWS. */ __( 'AWS Error: %s', 'idrivee2-media-upload' ), $e->getAwsErrorMessage() ?? __( 'Unknown error', 'idrivee2-media-upload' ) ), ), 30 ); } catch ( \Exception $e ) { $this->logger->error( 'Test connection failed: ' . $e->getMessage() ); set_transient( 'idrivee2_test_result', array( 'type' => 'error', 'message' => sprintf( /* translators: %s is the error message. */ __( 'Error: %s', 'idrivee2-media-upload' ), $e->getMessage() ), ), 30 ); } wp_safe_redirect( admin_url( 'options-general.php?page=idrivee2-media-upload' ) ); exit; } /** * Handle upload test file action. * * @since 0.3.0 * * @return void */ private function handle_upload_test(): void { // Check rate limit. if ( $this->rate_limiter->is_rate_limited( 'upload_test', 60 ) ) { $remaining = $this->rate_limiter->get_remaining_time( 'upload_test', 60 ); set_transient( 'idrivee2_test_result', array( 'type' => 'error', 'message' => sprintf( /* translators: %d is the number of seconds to wait. */ __( 'Please wait %d seconds before uploading again.', 'idrivee2-media-upload' ), $remaining ), ), 30 ); wp_safe_redirect( admin_url( 'options-general.php?page=idrivee2-media-upload' ) ); exit; } // Record this action for rate limiting. $this->rate_limiter->record_action( 'upload_test', 60 ); try { // Generate timestamp for filename (YYYYMMDDHHMMSS). $timestamp = gmdate( 'YmdHis' ); $file_name = 'test-' . $timestamp . '.txt'; // Create file content. $content = 'Test file uploaded at ' . gmdate( 'Y-m-d H:i:s' ) . ' UTC'; // Initialize the S3 client. $client = $this->client_factory->create(); // Upload to S3. $result = $client->putObject( array( 'Bucket' => $this->config->get_bucket(), 'Key' => $file_name, 'Body' => $content, // ACL matches the media uploader: files are served publicly via CDN/S3. 'ACL' => 'public-read', ) ); // Log successful upload. $this->logger->s3_operation( 'putObject', true, $file_name ); // Get the public URL - use CDN domain if configured, otherwise S3 ObjectURL. $s3_object_url = isset( $result['ObjectURL'] ) && is_string( $result['ObjectURL'] ) ? $result['ObjectURL'] : ''; $object_url = $this->get_public_url( $file_name, $s3_object_url ); set_transient( 'idrivee2_test_result', array( 'type' => 'success', 'message' => __( 'File uploaded successfully!', 'idrivee2-media-upload' ), 'file_name' => $file_name, 'object_url' => $object_url, ), 30 ); } catch ( \Aws\Exception\AwsException $e ) { $this->logger->s3_operation( 'putObject', false, $file_name, $e->getAwsErrorMessage() ?? '' ); set_transient( 'idrivee2_test_result', array( 'type' => 'error', 'message' => sprintf( /* translators: %s is the error message from AWS. */ __( 'AWS Error: %s', 'idrivee2-media-upload' ), $e->getAwsErrorMessage() ?? __( 'Unknown error', 'idrivee2-media-upload' ) ), ), 30 ); } catch ( \Exception $e ) { $this->logger->error( 'Test upload failed: ' . $e->getMessage() ); set_transient( 'idrivee2_test_result', array( 'type' => 'error', 'message' => sprintf( /* translators: %s is the error message. */ __( 'Error: %s', 'idrivee2-media-upload' ), $e->getMessage() ), ), 30 ); } wp_safe_redirect( admin_url( 'options-general.php?page=idrivee2-media-upload' ) ); exit; } /** * Get the public URL for an uploaded file. * * Uses CDN domain if configured, otherwise falls back to S3 ObjectURL. * * @since 0.3.0 * * @param string $file_name The file name/key in S3. * @param string $object_url The S3 ObjectURL returned by AWS SDK. * @return string The public URL to access the file. */ private function get_public_url( string $file_name, string $object_url ): string { // If custom domain is configured, use it. if ( $this->config->has_domain() ) { return trailingslashit( $this->config->get_domain() ) . $file_name; } // Otherwise, use the S3 ObjectURL. return $object_url; } /** * Handle delete test file action. * * @since 0.3.0 * * @param string $file_name The test file name to delete. * @return void */ private function handle_delete_test( string $file_name ): void { // Validate file name pattern (must be test-YYYYMMDDHHMMSS.txt). if ( ! preg_match( '/^test-\d{14}\.txt$/', $file_name ) ) { $this->logger->warning( 'Invalid test file name format attempted: ' . $file_name ); set_transient( 'idrivee2_test_result', array( 'type' => 'error', 'message' => __( 'Invalid test file name format.', 'idrivee2-media-upload' ), ), 30 ); wp_safe_redirect( admin_url( 'options-general.php?page=idrivee2-media-upload' ) ); exit; } try { // Initialize the S3 client. $client = $this->client_factory->create(); // Delete object from S3. $client->deleteObject( array( 'Bucket' => $this->config->get_bucket(), 'Key' => $file_name, ) ); // Log successful deletion. $this->logger->s3_operation( 'deleteObject', true, $file_name ); set_transient( 'idrivee2_test_result', array( 'type' => 'info', 'message' => sprintf( /* translators: %s is the file name. */ __( 'File %s deleted successfully.', 'idrivee2-media-upload' ), '' . esc_html( $file_name ) . '' ), ), 30 ); } catch ( \Aws\Exception\AwsException $e ) { $this->logger->s3_operation( 'deleteObject', false, $file_name, $e->getAwsErrorMessage() ?? '' ); set_transient( 'idrivee2_test_result', array( 'type' => 'error', 'message' => sprintf( /* translators: %s is the error message from AWS. */ __( 'AWS Error: %s', 'idrivee2-media-upload' ), $e->getAwsErrorMessage() ?? __( 'Unknown error', 'idrivee2-media-upload' ) ), ), 30 ); } catch ( \Exception $e ) { $this->logger->error( 'Test file deletion failed: ' . $e->getMessage() ); set_transient( 'idrivee2_test_result', array( 'type' => 'error', 'message' => sprintf( /* translators: %s is the error message. */ __( 'Error: %s', 'idrivee2-media-upload' ), $e->getMessage() ), ), 30 ); } wp_safe_redirect( admin_url( 'options-general.php?page=idrivee2-media-upload' ) ); exit; } /** * Sanitize settings before saving. * * @since 0.3.0 * * @param array $input Input settings. * @return array Sanitized settings. */ public function sanitize_settings( array $input ): array { $sanitized = array(); // Sanitize host. if ( isset( $input['host'] ) ) { $sanitized['host'] = sanitize_text_field( $input['host'] ); // Validate that host starts with https://. if ( ! empty( $sanitized['host'] ) && ! str_starts_with( $sanitized['host'], 'https://' ) ) { add_settings_error( 'idrivee2_media_settings', 'invalid_host', __( 'Host URL must start with "https://".', 'idrivee2-media-upload' ) ); } } // Sanitize other fields. $sanitized['key'] = isset( $input['key'] ) ? sanitize_text_field( $input['key'] ) : ''; $sanitized['secret'] = isset( $input['secret'] ) ? sanitize_text_field( $input['secret'] ) : ''; $sanitized['bucket'] = isset( $input['bucket'] ) ? sanitize_text_field( $input['bucket'] ) : ''; $sanitized['region'] = isset( $input['region'] ) ? sanitize_text_field( $input['region'] ) : ''; $sanitized['domain'] = isset( $input['domain'] ) ? sanitize_text_field( $input['domain'] ) : ''; // Sub-sizes mode: strict whitelist, default 'all'. $raw_mode = isset( $input['subsizes_mode'] ) ? strtolower( (string) sanitize_text_field( $input['subsizes_mode'] ) ) : ''; $allowed_modes = array( 'all', 'thumbnail', 'none' ); $sanitized['subsizes_mode'] = in_array( $raw_mode, $allowed_modes, true ) ? $raw_mode : 'all'; // Data cleanup on uninstall: checkbox, default off (preserve data). $sanitized['delete_on_uninstall'] = ! empty( $input['delete_on_uninstall'] ) ? '1' : ''; return $sanitized; } /** * Enqueue the admin JavaScript for enhanced UX. * * Loads the iDrivee2 admin script on the Settings → iDrivee2 page. * * @since 0.3.0 * * @param string $hook The current admin page hook suffix. * @return void */ public function enqueue_admin_scripts( string $hook ): void { // Only load script on our settings page. if ( 'settings_page_idrivee2-media-upload' !== $hook ) { return; } wp_enqueue_script( 'idrivee2-media-admin', plugin_dir_url( $this->plugin_file ) . 'assets/js/admin.js', array( 'jquery' ), defined( 'IDRIVEE2_MEDIA_VERSION' ) ? IDRIVEE2_MEDIA_VERSION : '1.0.0', true ); } /** * Render the iDrivee2 Media Upload settings page. * * Displays configuration form with fields for S3 settings. Fields are * read-only if values are defined in wp-config.php. * * @since 0.3.0 * * @return void */ public function render_settings_page(): void { $host = $this->config->get_host(); $access_key = $this->config->get_key(); $secret = $this->config->get_secret(); $bucket = $this->config->get_bucket(); $region = $this->config->get_region(); $domain = $this->config->get_domain(); $subsizes_mode = $this->config->get_subsizes_mode(); $delete_on_uninstall = $this->config->is_delete_on_uninstall(); $is_configured = $this->config->is_configured(); // Check which fields are defined in wp-config.php. $host_in_config = $this->config->is_defined_in_wp_config( 'host' ); $key_in_config = $this->config->is_defined_in_wp_config( 'key' ); $secret_in_config = $this->config->is_defined_in_wp_config( 'secret' ); $bucket_in_config = $this->config->is_defined_in_wp_config( 'bucket' ); $region_in_config = $this->config->is_defined_in_wp_config( 'region' ); $domain_in_config = $this->config->is_defined_in_wp_config( 'domain' ); $subsizes_mode_in_config = $this->config->is_defined_in_wp_config( 'subsizes_mode' ); $any_in_config = $host_in_config || $key_in_config || $secret_in_config || $bucket_in_config || $region_in_config || $domain_in_config || $subsizes_mode_in_config; ?>


' . esc_html( $test_result['file_name'] ) . '' ); ?>