HEX
Server: LiteSpeed
System: Linux server318.web-hosting.com 4.18.0-513.18.1.lve.el8.x86_64 #1 SMP Thu Feb 22 12:55:50 UTC 2024 x86_64
User: sahadove (2893)
PHP: 8.2.29
Disabled: NONE
Upload Files
File: /home/sahadove/www/wp-content/plugins/protect-uploads/includes/class-protect-uploads-passwords.php
<?php
/**
 * Handles password protection functionality
 *
 * @package    Protect_Uploads
 * @subpackage Protect_Uploads/includes
 */

/**
 * The password protection functionality of the plugin.
 *
 * @package    Protect_Uploads
 * @subpackage Protect_Uploads/includes
 * @author     Your Name <[email protected]>
 */
class Alti_ProtectUploads_Passwords {

	/**
	 * The plugin settings
	 *
	 * @since    0.5.2
	 * @access   private
	 * @var      array    $settings    The plugin settings.
	 */
	private $settings;

	/**
	 * Initialize the class and set its properties.
	 *
	 * @since    0.5.2
	 */
	public function __construct() {
		$this->settings = get_option( 'protect_uploads_settings', array() );
	}

	/**
	 * Initialize hooks
	 *
	 * @since    0.5.2
	 */
	public function init() {
		if ( ! empty( $this->settings['enable_password_protection'] ) ) {
			// Add meta box to media edit screen
			add_action( 'add_meta_boxes', array( $this, 'add_password_meta_box' ) );
			
			// Handle AJAX actions
			add_action( 'wp_ajax_protect_uploads_add_password', array( $this, 'ajax_add_password' ) );
			add_action( 'wp_ajax_protect_uploads_delete_password', array( $this, 'ajax_delete_password' ) );
			add_action( 'wp_ajax_verify_attachment_password', array( $this, 'verify_attachment_password' ) );
			add_action( 'wp_ajax_nopriv_verify_attachment_password', array( $this, 'verify_attachment_password' ) );
			
			// Handle frontend URL modification
			add_filter( 'wp_get_attachment_url', array( $this, 'modify_attachment_url' ), 10, 2 );
		}
	}

	/**
	 * Add password fields to media modal and edit screen
	 *
	 * @since    0.5.2
	 * @param    array   $form_fields    Array of form fields.
	 * @param    WP_Post $post           Attachment post object.
	 * @return   array
	 */
	public function add_password_fields( $form_fields, $post ) {
		$passwords = $this->get_attachment_passwords( $post->ID );
		
		$html = '<div class="protect-uploads-passwords">';
		$html .= wp_nonce_field( 'protect_uploads_save_password_' . $post->ID, 'protect_uploads_password_nonce', false, false );
		$html .= '<div class="existing-passwords">';
		if ( ! empty( $passwords ) ) {
			$html .= '<h4>' . esc_html__( 'Existing Passwords', 'protect-uploads' ) . '</h4>';
			$html .= '<ul>';
			foreach ( $passwords as $password ) {
				$html .= '<li>';
				$html .= esc_html( $password->password_label );
				$html .= ' <a href="#" class="delete-password" data-id="' . esc_attr( $password->id ) . '">';
				$html .= esc_html__( 'Delete', 'protect-uploads' );
				$html .= '</a>';
				$html .= '</li>';
			}
			$html .= '</ul>';
		}
		$html .= '</div>';
		
		$html .= '<div class="add-password">';
		$html .= '<input type="text" name="protect_uploads_password_label" placeholder="' . esc_attr__( 'Password Label', 'protect-uploads' ) . '" />';
		$html .= '<input type="password" name="protect_uploads_password" placeholder="' . esc_attr__( 'Password', 'protect-uploads' ) . '" />';
		$html .= '<button type="button" class="button add-password-button">' . esc_html__( 'Add Password', 'protect-uploads' ) . '</button>';
		$html .= '</div>';
		$html .= '</div>';

		$form_fields['protect_uploads_passwords'] = array(
			'label' => __( 'Password Protection', 'protect-uploads' ),
			'input' => 'html',
			'html'  => $html,
		);

		return $form_fields;
	}

	/**
	 * Save password fields
	 *
	 * @since    0.5.2
	 * @param    array   $post       Attachment post array.
	 * @param    array   $attachment Attachment fields array.
	 * @return   array
	 */
	public function save_password_fields( $post, $attachment ) {
		// Verify nonce first
		$nonce = isset( $_POST['protect_uploads_password_nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['protect_uploads_password_nonce'] ) ) : '';
		if ( ! wp_verify_nonce( $nonce, 'protect_uploads_save_password_' . $post['ID'] ) ) {
			// Nonce is invalid, maybe log this or return the post array without changes
			return $post; 
		}
		
		// Check if both password and label are set and password is not empty
		if ( isset( $_POST['protect_uploads_password'], $_POST['protect_uploads_password_label'] ) && ! empty( $_POST['protect_uploads_password'] ) ) {
			$this->add_attachment_password(
				$post['ID'],
				sanitize_text_field( wp_unslash( $_POST['protect_uploads_password'] ) ),
				sanitize_text_field( wp_unslash( $_POST['protect_uploads_password_label'] ) )
			);
		}
		return $post;
	}

	/**
	 * Add a password to an attachment
	 *
	 * @since    0.5.2
	 * @param    int    $attachment_id    Attachment ID.
	 * @param    string $password         Password.
	 * @param    string $label            Password label.
	 * @return   bool|int
	 */
	public function add_attachment_password( $attachment_id, $password, $label ) {
		global $wpdb;

		// Validate attachment exists
		if ( ! get_post( $attachment_id ) ) {
			return false;
		}

		// Validate input
		$attachment_id = absint( $attachment_id );
		$label = sanitize_text_field( $label );
		
		if ( empty( $password ) || empty( $label ) ) {
			return false;
		}

		// Use WordPress password hashing
		$hash = wp_hash_password( $password );

		// Prepare data with wpdb->prepare
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query
		$result = $wpdb->insert(
			$wpdb->prefix . 'protect_uploads_passwords',
			array(
				'attachment_id'  => $attachment_id,
				'password_hash'  => $hash,
				'password_label' => $label,
				'created_by'     => get_current_user_id(),
			),
			array( '%d', '%s', '%s', '%d' )
		);

		if ( false === $result ) {
			return false;
		}

		// Invalidate caches for this attachment
		wp_cache_delete( 'protect_uploads_passwords_' . $attachment_id, 'protect_uploads' );
		wp_cache_delete( 'protect_uploads_has_passwords_' . $attachment_id, 'protect_uploads' );
		wp_cache_delete( 'protect_uploads_password_hashes_' . $attachment_id, 'protect_uploads' );

		return $wpdb->insert_id;
	}

	/**
	 * Get passwords for an attachment
	 *
	 * @since    0.5.2
	 * @param    int $attachment_id    Attachment ID.
	 * @return   array
	 */
	public function get_attachment_passwords( $attachment_id ) {
		global $wpdb;

		$cache_key = 'protect_uploads_passwords_' . $attachment_id;
		$cached = wp_cache_get( $cache_key, 'protect_uploads' );

		if ( false !== $cached ) {
			return $cached;
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query
		$results = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT id, password_label, created_at FROM {$wpdb->prefix}protect_uploads_passwords WHERE attachment_id = %d",
				$attachment_id
			)
		);

		wp_cache_set( $cache_key, $results, 'protect_uploads', HOUR_IN_SECONDS );

		return $results;
	}

	/**
	 * Modify attachment URL to go through our password check
	 *
	 * @since    0.5.2
	 * @param    string $url            Original URL.
	 * @param    int    $attachment_id  Attachment ID.
	 * @return   string
	 */
	public function modify_attachment_url( $url, $attachment_id ) {
		if ( $this->has_passwords( $attachment_id ) ) {
			return add_query_arg(
				array(
					'protect_uploads_file' => $attachment_id,
					'_wpnonce'            => wp_create_nonce( 'protect_uploads_' . $attachment_id ),
				),
				home_url( 'index.php' )
			);
		}
		return $url;
	}

	/**
	 * Check if attachment has passwords
	 *
	 * @since    0.5.2
	 * @param    int $attachment_id    Attachment ID.
	 * @return   bool
	 */
	public function has_passwords( $attachment_id ) {
		global $wpdb;

		$cache_key = 'protect_uploads_has_passwords_' . $attachment_id;
		$cached = wp_cache_get( $cache_key, 'protect_uploads' );

		if ( false !== $cached ) {
			return $cached;
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query
		$count = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT(*) FROM {$wpdb->prefix}protect_uploads_passwords WHERE attachment_id = %d",
				$attachment_id
			)
		);

		$result = $count > 0;
		wp_cache_set( $cache_key, $result, 'protect_uploads', HOUR_IN_SECONDS );

		return $result;
	}

	/**
	 * Verify attachment password
	 *
	 * @since    0.5.2
	 */
	public function verify_attachment_password() {
		check_ajax_referer( 'protect_uploads_verify_password', 'nonce' );

		$attachment_id = isset( $_POST['attachment_id'] ) ? intval( $_POST['attachment_id'] ) : 0;
		$password = isset( $_POST['password'] ) ? sanitize_text_field( wp_unslash( $_POST['password'] ) ) : '';

		if ( ! $attachment_id || ! $password ) {
			wp_send_json_error( array( 'message' => __( 'Invalid request', 'protect-uploads' ) ) );
		}

		$verified = $this->verify_password( $attachment_id, $password );
		if ( $verified ) {
			$this->log_access( $attachment_id, $verified, 'view' );
			wp_send_json_success( array(
				'url' => wp_get_attachment_url( $attachment_id ),
			) );
		}

		wp_send_json_error( array( 'message' => __( 'Invalid password', 'protect-uploads' ) ) );
	}

	/**
	 * Verify a password for an attachment
	 *
	 * @since    0.5.2
	 * @param    int    $attachment_id    Attachment ID.
	 * @param    string $password         Password to verify.
	 * @return   bool|int
	 */
	public function verify_password( $attachment_id, $password ) {
		global $wpdb;

		// Basic validation
		$attachment_id = absint( $attachment_id );
		if ( empty( $password ) || empty( $attachment_id ) ) {
			return false;
		}

		// Get passwords with caching
		$cache_key = 'protect_uploads_password_hashes_' . $attachment_id;
		$passwords = wp_cache_get( $cache_key, 'protect_uploads' );

		if ( false === $passwords ) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query
			$passwords = $wpdb->get_results( $wpdb->prepare(
				"SELECT id, password_hash FROM {$wpdb->prefix}protect_uploads_passwords WHERE attachment_id = %d",
				$attachment_id
			) );
			wp_cache_set( $cache_key, $passwords, 'protect_uploads', HOUR_IN_SECONDS );
		}

		if ( empty( $passwords ) ) {
			return false;
		}

		// Check against all passwords
		foreach ( $passwords as $pwd ) {
			if ( wp_check_password( $password, $pwd->password_hash ) ) {
				return $pwd->id;
			}
		}

		// Log failed attempt
		$this->log_failed_attempt( $attachment_id );

		return false;
	}

	/**
	 * Log failed password attempt
	 *
	 * @since    0.5.2
	 * @param    int $attachment_id    Attachment ID.
	 */
	private function log_failed_attempt( $attachment_id ) {
		global $wpdb;

		// Get number of recent failed attempts (no caching - needs real-time count for rate limiting)
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table, real-time rate limiting
		$attempts = $wpdb->get_var( $wpdb->prepare(
			"SELECT COUNT(*) FROM {$wpdb->prefix}protect_uploads_access_logs
			WHERE attachment_id = %d
			AND access_type = 'failed'
			AND access_time > DATE_SUB(NOW(), INTERVAL 1 HOUR)",
			$attachment_id
		) );

		// Log the attempt
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query
		$wpdb->insert(
			$wpdb->prefix . 'protect_uploads_access_logs',
			array(
				'attachment_id' => $attachment_id,
				'password_id'   => 0,
				'ip_address'    => $this->get_client_ip(),
				'user_agent'    => substr( sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ?? '' ) ), 0, 255 ),
				'access_type'   => 'failed'
			),
			array( '%d', '%d', '%s', '%s', '%s' )
		);

		// If too many attempts, maybe implement temporary lockout
		if ( $attempts > 5 ) {
			/* // Removed error log
			error_log( sprintf( 
				'Protect Uploads: Multiple failed password attempts for attachment %d from IP %s',
				$attachment_id,
				$this->get_client_ip()
			) );
			*/
		}
	}

	/**
	 * Get client IP address with proxy support
	 *
	 * @since    0.5.2
	 * @return   string
	 */
	private function get_client_ip() {
		$headers = array(
			'HTTP_CF_CONNECTING_IP',
			'HTTP_X_REAL_IP',
			'HTTP_CLIENT_IP',
			'HTTP_X_FORWARDED_FOR',
			'REMOTE_ADDR',
		);

		foreach ( $headers as $header ) {
			if ( empty( $_SERVER[ $header ] ) ) {
				continue;
			}

			$raw_value = sanitize_text_field( wp_unslash( $_SERVER[ $header ] ) );
			$candidates = ( 'HTTP_X_FORWARDED_FOR' === $header )
				? explode( ',', $raw_value )
				: array( $raw_value );

			foreach ( $candidates as $candidate ) {
				$candidate = trim( $candidate );
				if ( filter_var( $candidate, FILTER_VALIDATE_IP ) ) {
					return $candidate;
				}
			}
		}

		return '0.0.0.0';
	}

	/**
	 * Add password meta box to media edit screen
	 *
	 * @since    0.5.2
	 */
	public function add_password_meta_box() {
		add_meta_box(
			'protect-uploads-passwords',
			__( 'Password Protection', 'protect-uploads' ),
			array( $this, 'render_password_meta_box' ),
			'attachment',
			'side',
			'high'
		);
	}

	/**
	 * Get access logs for an attachment
	 *
	 * @since    0.5.2
	 * @param    int $attachment_id    Attachment ID.
	 * @return   array
	 */
	public function get_attachment_access_logs( $attachment_id ) {
		global $wpdb;

		$cache_key = 'protect_uploads_access_logs_' . $attachment_id;
		$cached = wp_cache_get( $cache_key, 'protect_uploads' );

		if ( false !== $cached ) {
			return $cached;
		}

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query
		$results = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT l.*, p.password_label
				FROM {$wpdb->prefix}protect_uploads_access_logs l
				LEFT JOIN {$wpdb->prefix}protect_uploads_passwords p ON l.password_id = p.id
				WHERE l.attachment_id = %d
				ORDER BY l.access_time DESC
				LIMIT 10",
				$attachment_id
			)
		);

		wp_cache_set( $cache_key, $results, 'protect_uploads', 5 * MINUTE_IN_SECONDS );

		return $results;
	}

	/**
	 * Render password meta box content
	 *
	 * @since    0.5.2
	 * @param    WP_Post $post    Post object.
	 */
	public function render_password_meta_box( $post ) {
		$passwords = $this->get_attachment_passwords( $post->ID );
		$access_logs = $this->get_attachment_access_logs( $post->ID );
		?>
		<div class="protect-uploads-passwords" data-attachment-id="<?php echo esc_attr( $post->ID ); ?>">
			<div class="existing-passwords">
				<?php if ( ! empty( $passwords ) ) : ?>
					<h4><?php esc_html_e( 'Existing Passwords', 'protect-uploads' ); ?></h4>
					<ul>
						<?php foreach ( $passwords as $password ) : ?>
							<li>
								<?php echo esc_html( $password->password_label ); ?>
								<a href="#" class="delete-password" data-id="<?php echo esc_attr( $password->id ); ?>">
									<?php esc_html_e( 'Delete', 'protect-uploads' ); ?>
								</a>
							</li>
						<?php endforeach; ?>
					</ul>
				<?php endif; ?>
			</div>
			
			<div class="add-password">
				<p>
					<label>
						<?php esc_html_e( 'Password Label:', 'protect-uploads' ); ?><br>
						<input type="text" name="protect_uploads_password_label" class="widefat">
					</label>
				</p>
				<p>
					<label>
						<?php esc_html_e( 'Password:', 'protect-uploads' ); ?><br>
						<input type="password" name="protect_uploads_password" class="widefat">
					</label>
				</p>
				<p>
					<button type="button" class="button add-password-button">
						<?php esc_html_e( 'Add Password', 'protect-uploads' ); ?>
					</button>
				</p>
			</div>

			<?php if ( ! empty( $access_logs ) ) : ?>
				<div class="access-logs">
					<h4><?php esc_html_e( 'Recent Access Logs', 'protect-uploads' ); ?></h4>
					<table class="widefat striped">
						<thead>
							<tr>
								<th><?php esc_html_e( 'Date', 'protect-uploads' ); ?></th>
								<th><?php esc_html_e( 'Password', 'protect-uploads' ); ?></th>
								<th><?php esc_html_e( 'IP', 'protect-uploads' ); ?></th>
							</tr>
						</thead>
						<tbody>
							<?php foreach ( $access_logs as $log ) : ?>
								<tr>
									<td><?php echo esc_html( wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( $log->access_time ) ) ); ?></td>
									<td><?php echo esc_html( $log->password_label ); ?></td>
									<td><?php echo esc_html( $log->ip_address ); ?></td>
								</tr>
							<?php endforeach; ?>
						</tbody>
					</table>
				</div>
			<?php endif; ?>
		</div>
		<?php
	}

	/**
	 * Handle AJAX request to add password
	 *
	 * @since    0.5.2
	 */
	public function ajax_add_password() {
		check_ajax_referer( 'protect_uploads_password_action', 'nonce' );

		// Sanitize attachment_id first
		$attachment_id = isset( $_POST['attachment_id'] ) ? absint( $_POST['attachment_id'] ) : 0;

		if ( ! $attachment_id || ! current_user_can( 'edit_post', $attachment_id ) ) {
			wp_send_json_error( array( 'message' => __( 'Permission denied or invalid attachment ID.', 'protect-uploads' ) ) );
		}

		// $attachment_id is already sanitized
		$label = sanitize_text_field( wp_unslash( $_POST['label'] ?? '' ) ); // Added ?? '' for safety
		$password = sanitize_text_field( wp_unslash( $_POST['password'] ?? '' ) ); // Added ?? '' for safety

		if ( ! $label || ! $password ) { // Check label and password specifically
			wp_send_json_error( array( 'message' => __( 'Missing required fields (label or password).', 'protect-uploads' ) ) );
		}

		$result = $this->add_attachment_password( $attachment_id, $password, $label );
		if ( $result ) {
			wp_send_json_success( array(
				'message' => __( 'Password added successfully.', 'protect-uploads' ),
				'passwords' => $this->get_attachment_passwords( $attachment_id ),
			) );
		}

		wp_send_json_error( array( 'message' => __( 'Failed to add password.', 'protect-uploads' ) ) );
	}

	/**
	 * Handle AJAX request to delete password
	 *
	 * @since    0.5.2
	 */
	public function ajax_delete_password() {
		check_ajax_referer( 'protect_uploads_password_action', 'nonce' );

		// Sanitize attachment_id first
		$attachment_id = isset( $_POST['attachment_id'] ) ? absint( $_POST['attachment_id'] ) : 0;

		if ( ! $attachment_id || ! current_user_can( 'edit_post', $attachment_id ) ) {
			wp_send_json_error( array( 'message' => __( 'Permission denied or invalid attachment ID.', 'protect-uploads' ) ) );
		}

		// $attachment_id is already sanitized
		$password_id = isset( $_POST['password_id'] ) ? absint( $_POST['password_id'] ) : 0;

		if ( ! $password_id ) { // Check password_id specifically
			wp_send_json_error( array( 'message' => __( 'Invalid request (missing password ID).', 'protect-uploads' ) ) );
		}

		$result = $this->delete_attachment_password( $attachment_id, $password_id );
		if ( $result ) {
			wp_send_json_success( array(
				'message' => __( 'Password deleted successfully.', 'protect-uploads' ),
				'passwords' => $this->get_attachment_passwords( $attachment_id ),
			) );
		}

		wp_send_json_error( array( 'message' => __( 'Failed to delete password.', 'protect-uploads' ) ) );
	}

	/**
	 * Delete a password
	 *
	 * @since    0.5.2
	 * @param    int $attachment_id    Attachment ID.
	 * @param    int $password_id      Password ID.
	 * @return   bool
	 */
	public function delete_attachment_password( $attachment_id, $password_id ) {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query
		$result = $wpdb->delete(
			$wpdb->prefix . 'protect_uploads_passwords',
			array(
				'id' => $password_id,
				'attachment_id' => $attachment_id,
			),
			array( '%d', '%d' )
		);

		// Invalidate caches for this attachment
		wp_cache_delete( 'protect_uploads_passwords_' . $attachment_id, 'protect_uploads' );
		wp_cache_delete( 'protect_uploads_has_passwords_' . $attachment_id, 'protect_uploads' );
		wp_cache_delete( 'protect_uploads_password_hashes_' . $attachment_id, 'protect_uploads' );

		return $result;
	}

	/**
	 * Log an access attempt
	 *
	 * @since    0.5.2
	 * @param    int    $attachment_id    Attachment ID.
	 * @param    int    $password_id      Password ID.
	 * @param    string $access_type      Type of access (view/download).
	 * @return   bool|int
	 */
	public function log_access( $attachment_id, $password_id, $access_type ) {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table requires direct query
		return $wpdb->insert(
			$wpdb->prefix . 'protect_uploads_access_logs',
			array(
				'attachment_id' => $attachment_id,
				'password_id'   => $password_id,
				'ip_address'    => $this->get_client_ip(),
				'user_agent'    => sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ?? '' ) ),
				'access_type'   => $access_type,
			),
			array( '%d', '%d', '%s', '%s', '%s' )
		);
	}
}