HEX
Server: LiteSpeed
System: Linux server315.web-hosting.com 4.18.0-553.54.1.lve.el8.x86_64 #1 SMP Wed Jun 4 13:01:13 UTC 2025 x86_64
User: globfdxw (6114)
PHP: 8.1.34
Disabled: NONE
Upload Files
File: //home/globfdxw/public_html/wp-content/plugins/wpforms-pdf/src/Access/PDF.php
<?php

namespace WPFormsPDF\Access;

use WPFormsPDF\Notifications\Process;
use WPFormsPDF\Storage;
use WPForms\Helpers\File;
use WP_User;

/**
 * PDF Access control and download handler.
 *
 * @since 1.0.0
 */
class PDF {
	/**
	 * Class constructor.
	 *
	 * @since 1.0.0
	 */
	public function __construct() {

		$this->hooks();
	}

	/**
	 * Register hooks.
	 *
	 * @since 1.0.0
	 */
	private function hooks(): void {

		add_action( 'template_redirect', [ $this, 'handle_pdf_download_request' ] );
	}

	/**
	 * Handle PDF download request via query param.
	 *
	 * @since 1.0.0
	 */
	public function handle_pdf_download_request(): void {

		$hash = $this->get_pdf_hash_from_request();

		if ( ! isset( $hash ) ) {
			return;
		}

		$pdf_info = $this->get_pdf_info_from_hash( $hash );

		[ $form_data, $entry, $entry_fields ] = $this->get_form_and_entry_data( $pdf_info );

		$pdf_settings = $this->get_pdf_settings( $form_data, $pdf_info['pdf_id'] );

		if ( ! $this->is_access_granted( $pdf_settings ) ) {
			$this->render_access_denied();
		}

		if ( $this->needs_password( $pdf_settings ) && ! $this->check_password( $pdf_settings ) ) {
			$error_message = $this->get_password_error_message();

			$this->render_password_form( $hash, $error_message );
		}

		$pdf_file = $this->get_pdf_file_path( $form_data, (array) $entry, $entry_fields, $pdf_settings, $pdf_info );

		$this->serve_pdf_file( $pdf_file );
	}

	/**
	 * Extract PDF hash from GET.
	 *
	 * @since 1.0.0
	 *
	 * @return string|null
	 */
	private function get_pdf_hash_from_request(): ?string {
		// phpcs:disable WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing
		if ( ! empty( $_GET['wpforms_pdf_download'] ) ) {
			return sanitize_text_field( wp_unslash( $_GET['wpforms_pdf_download'] ) );
		}
		// phpcs:enable WordPress.Security.NonceVerification.Recommended, WordPress.Security.NonceVerification.Missing

		return null;
	}

	/**
	 * Get an error message for incorrect password.
	 *
	 * @since 1.0.0
	 *
	 * @return string
	 */
	private function get_password_error_message(): string {

		return isset( $_POST['wpforms_pdf_password'] ) // phpcs:ignore WordPress.Security.NonceVerification.Missing
			? __( 'Sorry, the password you entered is incorrect.', 'wpforms-pdf' )
			: '';
	}

	/**
	 * Validate and decode hash from URL.
	 *
	 * @since 1.0.0
	 *
	 * @param string $hash Hash from URL.
	 *
	 * @return array
	 */
	private function get_pdf_info_from_hash( string $hash ): array {

		$hash     = sanitize_text_field( $hash );
		$pdf_info = $this->decode_hash( $hash );

		if ( ! $pdf_info ) {
			$this->render_access_denied();
		}

		$form_id  = (int) ( $pdf_info['form_id'] ?? 0 );
		$entry_id = (int) ( $pdf_info['entry_id'] ?? 0 );
		$pdf_id   = (int) ( $pdf_info['pdf_id'] ?? 0 );

		if ( ! $form_id || ! $entry_id || ! $pdf_id ) {
			$this->render_access_denied();
		}

		return $pdf_info;
	}

	/**
	 * Fetch and validate form and entry data.
	 *
	 * @since 1.0.0
	 *
	 * @param array $pdf_info PDF info.
	 *
	 * @return array (form_data, entry, entry_fields)
	 */
	private function get_form_and_entry_data( array $pdf_info ): array {

		$form_data = $this->get_form_data( $pdf_info );
		$entry     = $this->get_entry( $pdf_info );

		$entry_fields = $entry->fields ?? [];
		$entry_fields = is_array( $entry_fields ) ? $entry_fields : json_decode( $entry_fields, true );

		return [ $form_data, $entry, $entry_fields ];
	}

	/**
	 * Get form data.
	 *
	 * @since 1.0.0
	 *
	 * @param array $pdf_info PDF info.
	 *
	 * @return array
	 */
	private function get_form_data( array $pdf_info ): array {

		$form_id   = (int) $pdf_info['form_id'];
		$form_obj  = wpforms()->obj( 'form' );
		$form_data = $form_obj ? $form_obj->get( $form_id, [ 'content_only' => true ] ) : null;

		if ( empty( $form_data ) ) {
			$this->render_access_denied();
		}

		return is_array( $form_data ) ? $form_data : (array) json_decode( $form_data, true );
	}

	/**
	 * Get entry data.
	 *
	 * @since 1.0.0
	 *
	 * @param array $pdf_info PDF info.
	 *
	 * @return object
	 */
	private function get_entry( array $pdf_info ): object {

		$entry_id  = (int) $pdf_info['entry_id'];
		$entry_obj = wpforms()->obj( 'entry' );
		$entry     = $entry_obj ? $entry_obj->get( $entry_id ) : null;

		if ( empty( $entry ) ) {
			$this->render_access_denied();
		}

		return $entry;
	}

	/**
	 * Fetch and validate PDF settings.
	 *
	 * @since 1.0.0
	 *
	 * @param array $form_data Form data.
	 * @param int   $pdf_id    PDF ID.
	 *
	 * @return array
	 */
	private function get_pdf_settings( array $form_data, int $pdf_id ): array {

		$pdf_settings = $form_data['settings']['pdfs'][ $pdf_id ] ?? [];

		if ( empty( $pdf_settings ) ) {
			$this->render_access_denied();
		}

		return $pdf_settings;
	}

	/**
	 * Check if PDF needs password protection.
	 *
	 * @since 1.0.0
	 *
	 * @param array $pdf_settings PDF settings.
	 *
	 * @return bool
	 */
	private function needs_password( array $pdf_settings ): bool {

		return ! empty( $pdf_settings['is_protected'] ) && ! empty( $pdf_settings['protection_password'] );
	}

	/**
	 * Get the full path to the PDF file.
	 *
	 * @since 1.0.0
	 *
	 * @param array $form_data    Form data.
	 * @param array $entry        Entry.
	 * @param array $entry_fields Entry fields.
	 * @param array $pdf_settings PDF settings.
	 * @param array $pdf_info     PDF info.
	 *
	 * @return string
	 * @noinspection PhpUnusedParameterInspection
	 */
	private function get_pdf_file_path( array $form_data, array $entry, array $entry_fields, array $pdf_settings, array $pdf_info ): string {

		$form_id  = (int) $pdf_info['form_id'];
		$entry_id = (int) $pdf_info['entry_id'];
		$pdf_dir  = Storage::get_dir( $form_id, $entry_id );
		$pdf_file = $pdf_dir . Process::get_pdf_file_name( $pdf_settings, $form_data, $entry_fields, $entry_id );

		if ( ! File::exists( $pdf_file ) ) {
			$this->render_file_not_found();
		}

		return $pdf_file;
	}

	/**
	 * Check if a user has access to PDF based on restrictions.
	 *
	 * @since 1.0.0
	 *
	 * @param array $pdf_settings PDF settings.
	 *
	 * @return bool
	 */
	private function is_access_granted( array $pdf_settings ): bool {

		$user_restrictions = $pdf_settings['user_restrictions'] ?? 'none';

		if ( $user_restrictions === 'none' ) {
			return true;
		}
		if ( $user_restrictions === 'loggedin' && ! is_user_logged_in() ) {
			return false;
		}

		$current_user = wp_get_current_user();

		// Use inclusive OR logic: access is granted if the user satisfies EITHER
		// the role restriction OR the user ID restriction.
		return $this->has_role_access( $current_user, $pdf_settings )
			|| $this->has_user_id_access( $current_user, $pdf_settings );
	}

	/**
	 * Check if a user has access by role.
	 *
	 * @since 1.0.0
	 *
	 * @param WP_User|null $current_user Current user object.
	 * @param array        $pdf_settings PDF settings.
	 *
	 * @return bool
	 */
	private function has_role_access( ?WP_User $current_user, array $pdf_settings ): bool {

		// No role restriction configured or no user — deny role-based access.
		if ( empty( $pdf_settings['user_roles_restrictions'] ) || ! $current_user ) {
			return false;
		}

		$roles = is_string( $pdf_settings['user_roles_restrictions'] )
			? json_decode( $pdf_settings['user_roles_restrictions'], true )
			: $pdf_settings['user_roles_restrictions'];

		return empty( $roles ) || array_intersect( $current_user->roles, $roles );
	}

	/**
	 * Check if a user has access via the user ID.
	 *
	 * @since 1.0.0
	 *
	 * @param WP_User|null $current_user Current user object.
	 * @param array        $pdf_settings PDF settings.
	 *
	 * @return bool
	 */
	private function has_user_id_access( ?WP_User $current_user, array $pdf_settings ): bool {

		// No user ID restriction configured or no user — deny user-based access.
		if ( empty( $pdf_settings['user_names_restrictions'] ) || ! $current_user ) {
			return false;
		}

		$user_ids = is_string( $pdf_settings['user_names_restrictions'] )
			? json_decode( $pdf_settings['user_names_restrictions'], true )
			: $pdf_settings['user_names_restrictions'];

		// Convert user IDs to integers.
		$user_ids = array_map( 'intval', $user_ids );

		return empty( $user_ids ) || in_array( $current_user->ID, $user_ids, true );
	}

	/**
	 * Check the password for PDF.
	 *
	 * @since 1.0.0
	 *
	 * @param array $pdf_settings PDF settings.
	 *
	 * @return bool
	 */
	private function check_password( array $pdf_settings ): bool {

		if ( ! isset( $_POST['wpforms_pdf_password'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
			return false;
		}

		$pw_submitted = sanitize_text_field( wp_unslash( $_POST['wpforms_pdf_password'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
		$hash_pw      = $pdf_settings['protection_password'];

		if ( hash_equals( $hash_pw, $pw_submitted ) ) {
			return true;
		}

		return false;
	}

	/**
	 * Render access denied page.
	 *
	 * @since 1.0.0
	 */
	private function render_access_denied(): void {

		status_header( 403 );
		include wp_normalize_path( WPFORMS_PDF_PATH . 'templates/access/file-protected.php' );
		exit;
	}

	/**
	 * Render file not found page.
	 *
	 * @since 1.0.0
	 */
	private function render_file_not_found(): void {

		status_header( 404 );
		include wp_normalize_path( WPFORMS_PDF_PATH . 'templates/access/file-not-found.php' );
		exit;
	}

	/**
	 * Render password form.
	 *
	 * @since 1.0.0
	 *
	 * @param string $hash          Hash from URL.
	 * @param string $error_message Error message (optional).
	 *
	 * @noinspection PhpUnusedParameterInspection
	 */
	private function render_password_form( string $hash, string $error_message ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed

		status_header( 200 );

		include wp_normalize_path( WPFORMS_PDF_PATH . 'templates/access/file-password-required.php' );
		exit;
	}

	/**
	 * Serve the PDF file for download.
	 *
	 * @since 1.0.0
	 *
	 * @param string $pdf_file PDF file path.
	 */
	private function serve_pdf_file( string $pdf_file ): void {

		if ( ! file_exists( $pdf_file ) ) {
			$this->render_file_not_found();
		}

		header( 'Content-Description: File Transfer' );
		header( 'Content-Type: application/pdf' );
		header( 'Content-Disposition: attachment; filename="' . basename( $pdf_file ) . '"' );
		header( 'Content-Transfer-Encoding: binary' );
		header( 'Expires: 0' );
		header( 'Cache-Control: must-revalidate' );
		header( 'Pragma: public' );
		header( 'Content-Length: ' . filesize( $pdf_file ) );

		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		echo File::get_contents( $pdf_file );

		exit;
	}

	/**
	 * Generate hash for a download link.
	 *
	 * @since 1.0.0
	 *
	 * @param int $form_id  Form ID.
	 * @param int $entry_id Entry ID.
	 * @param int $pdf_id   PDF ID.
	 *
	 * @return string
	 */
	public static function generate_hash( int $form_id, int $entry_id, int $pdf_id ): string {

		$data  = $form_id . '|' . $entry_id . '|' . $pdf_id;
		$token = wp_hash( $data . self::nonce_salt() );

		return base64_encode( // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
			wp_json_encode(
				[
					'form_id'  => $form_id,
					'entry_id' => $entry_id,
					'pdf_id'   => $pdf_id,
					'token'    => $token,
				]
			)
		);
	}

	/**
	 * Get nonce salt.
	 *
	 * @since 1.0.0
	 *
	 * @return string
	 */
	private static function nonce_salt(): string {

		return defined( 'NONCE_SALT' ) ? NONCE_SALT : 'wpforms-pdf-nonce-salt';
	}

	/**
	 * Decode hash from URL.
	 *
	 * @since 1.0.0
	 *
	 * @param string $hash Hash from URL.
	 *
	 * @return array|false
	 */
	private function decode_hash( string $hash ) {

		$data = json_decode( base64_decode( $hash ), true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode

		if ( ! is_array( $data ) ) {
			return false;
		}

		$expected = wp_hash( $data['form_id'] . '|' . $data['entry_id'] . '|' . $data['pdf_id'] . self::nonce_salt() );

		if ( empty( $data['token'] ) || $data['token'] !== $expected ) {
			return false;
		}

		return $data;
	}

	/**
	 * Generate the full download URL for a protected PDF.
	 *
	 * @since 1.0.0
	 *
	 * @param int $form_id  Form ID.
	 * @param int $entry_id Entry ID.
	 * @param int $pdf_id   PDF ID.
	 *
	 * @return string Full download URL.
	 */
	public static function generate_pdf_download_url( int $form_id, int $entry_id, int $pdf_id ): string {

		$hash = self::generate_hash( $form_id, $entry_id, $pdf_id );

		return add_query_arg( 'wpforms_pdf_download', rawurlencode( $hash ), home_url( '/' ) );
	}

	/**
	 * Check if a PDF file is protected.
	 *
	 * @since 1.0.0
	 *
	 * @param array $pdf_settings PDF settings.
	 *
	 * @return bool
	 */
	public static function is_file_protected( array $pdf_settings ): bool {

		// Access protection is not toggled on.
		if ( empty( $pdf_settings['access'] ) ) {
			return false;
		}

		// It is not either user restriction enabled or password protection enabled.
		if ( $pdf_settings['user_restrictions'] === 'none' && empty( $pdf_settings['is_protected'] ) ) {
			return false;
		}

		return true;
	}
}