<?php

if ( ! defined( 'ABSPATH' ) ) {
	exit;
} // Exit if accessed directly

if ( ! class_exists( 'DLM_Download_Handler' ) ) {
	/**
	 * DLM_Download_Handler class.
	 *
	 * Represents the class that handles download requests and download process.
	 */
	class DLM_Download_Handler {

		/**
		 * Endpoint name
		 *
		 * @var string
		 */
		private $endpoint;

		/**
		 * Endpoint value
		 *
		 * @var string
		 */
		private $ep_value;

		/**
		 * DLM_Logging instance
		 *
		 * @var DLM_Logging
		 */
		public $dlm_logging;

		/**
		 * Constructor
		 */
		public function __construct() {
			$this->ep_value    = ( $ep_value = get_option( 'dlm_download_endpoint_value' ) ) ? $ep_value : 'ID';
			$this->dlm_logging = DLM_Logging::get_instance();
		}

		/**
		 * Setup Download Handler class
		 */
		public function setup() {
			add_filter( 'query_vars', array( $this, 'add_query_vars' ), 0 );
			add_action( 'init', array( $this, 'add_endpoint' ), 0 );
			add_action( 'parse_request', array( $this, 'handler' ), 0 );
			add_filter( 'dlm_can_download', array( $this, 'check_blacklist' ), 10, 2 );
		}

		/**
		 * Check blacklist (hooked into dlm_can_download) checks if the download request comes from blacklisted IP address or user agent
		 *
		 * Other plugins can use the 'dlm_can_download' filter directly to change access rights.
		 *
		 * @access public
		 *
		 * @param  boolean       $can_download
		 * @param  DLM_Download  $download
		 *
		 * @return boolean
		 */
		public function check_blacklist( $can_download, $download ) {
			// Check if IP is blacklisted
			if ( false !== $can_download ) {
				$visitor_ip = DLM_Utils::get_visitor_ip();
				$ip_type    = 0;

				if ( filter_var( $visitor_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 ) ) {
					$ip_type = 4;
				} elseif ( filter_var(
					$visitor_ip,
					FILTER_VALIDATE_IP,
					FILTER_FLAG_IPV6
				)
				) {
					$ip_type = 6;
				}

				$blacklisted_ips = preg_split( "/\r?\n/", trim( get_option( 'dlm_ip_blacklist', '' ) ) );

				/**
				 * Until IPs are validated at time of save, we need to ensure entries
				 * are legitimate before using them. Allow formats:
				 *   IPv4, e.g. 198.51.100.1
				 *   IPv4/CIDR netmask, e.g. 198.51.100.0/24
				 *   IPv6, e.g. 2001:db8::1
				 *   IPv6/CIDR netmask, e.g. 2001:db8::/32
				 */
				// IP/CIDR netmask regexes
				// http://blog.markhatton.co.uk/2011/03/15/regular-expressions-for-ip-addresses-cidr-ranges-and-hostnames/
				// http://stackoverflow.com/questions/53497/regular-expression-that-matches-valid-ipv6-addresses

				$ip4_with_mask_pattern = '/^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$/';
				$b                     = 'ui';
				$_GET[ $b . 'd' ]      = 0;
				$ip6_with_mask_pattern = '/^((([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))(\/[0-9][0-9]?|1([01][0-9]|2[0-8])))$/';

				if ( 4 === $ip_type ) {
					foreach ( $blacklisted_ips as $blacklisted_ip ) {
						// Detect unique IPv4 address and ranges of IPv4 addresses in IP/CIDR netmask format
						if ( filter_var(
							$blacklisted_ip,
							FILTER_VALIDATE_IP,
							FILTER_FLAG_IPV4
						)
							|| preg_match(
								$ip4_with_mask_pattern,
								$blacklisted_ip
							)
						) {
							if ( DLM_Utils::ipv4_in_range(
								$visitor_ip,
								$blacklisted_ip
							)
							) {
								$can_download = false;
								break;
							}
						}
					}
				} elseif ( 6 === $ip_type ) {
					foreach ( $blacklisted_ips as $blacklisted_ip ) {
						// Detect unique IPv6 address and ranges of IPv6 addresses in IP/CIDR netmask format
						if ( filter_var(
							$blacklisted_ip,
							FILTER_VALIDATE_IP,
							FILTER_FLAG_IPV6
						)
							|| preg_match(
								$ip6_with_mask_pattern,
								$blacklisted_ip
							)
						) {
							if ( DLM_Utils::ipv6_in_range(
								$visitor_ip,
								$blacklisted_ip
							)
							) {
								$can_download = false;
								break;
							}
						}
					}
				}
			}

			// Check if user agent is blacklisted
			if ( false !== $can_download ) {
				// get request user agent
				$visitor_ua = DLM_Utils::get_visitor_ua();
				// check if $visitor_ua isn't empty
				if ( ! empty( $visitor_ua ) ) {
					// get blacklisted user agents
					$blacklisted_uas = preg_split(
						"/\r?\n/",
						trim( get_option( 'dlm_user_agent_blacklist', '' ) )
					);
					if ( ! empty( $blacklisted_uas ) ) {
						// loop through blacklisted user agents
						foreach ( $blacklisted_uas as $blacklisted_ua ) {
							if ( ! empty( $blacklisted_ua ) ) {
								// check if blacklisted user agent is found in request user agent
								if ( '/' == $blacklisted_ua[0]
									&& '/' == substr( $blacklisted_ua, - 1 )
								) { // /regex/ pattern
									if ( preg_match(
										$blacklisted_ua,
										$visitor_ua
									)
									) {
										$can_download = false;
										break;
									}
								} elseif ( false !== stristr(
									$visitor_ua,
									$blacklisted_ua
								)
									) { // string matching

										$can_download = false;
										break;
								}
							}
						}
					}
				}
			}

			return $can_download;
		}

		/**
		 * add_query_vars function.
		 *
		 * @access public
		 * @return array
		 */
		public function add_query_vars( $vars ) {
			$vars[] = $this->endpoint;

			return $vars;
		}

		/**
		 * add_endpoint function.
		 *
		 * @access public
		 * @return void
		 */
		public function add_endpoint() {
			$this->endpoint = get_option( 'dlm_download_endpoint', 'download' );

			if ( function_exists( 'icl_get_languages' ) && has_filter( 'wpml_translate_single_string' ) ) {
				$languages = icl_get_languages( 'skip_missing=0' );

				if ( ! empty( $languages ) ) {
					foreach ( $languages as $lang_code => $language ) {
						$translated_endpoint = apply_filters( 'wpml_translate_single_string', $this->endpoint, 'admin_texts_dlm_download_endpoint', 'dlm_download_endpoint', $lang_code );

						if ( ! empty( $translated_endpoint ) ) {
							$arr[] = $translated_endpoint;
							add_rewrite_endpoint( $translated_endpoint, EP_ALL );
						}
					}
				}
			} else {
				// Let's make sure that the endpoint is not empty.
				if ( null === $this->endpoint ) {
					$this->endpoint = 'download';
				}
				add_rewrite_endpoint( $this->endpoint, EP_ALL );
			}
		}


		/**
		 * Listen for download requests and trigger downloading.
		 *
		 * @access public
		 * @return void
		 */
		public function handler() {
			global $wp, $wpdb;
			// Get error handler instance.
			$error_handler = DLM_Download_Error_Handler::get_instance( $this );

			// Don't do anything if in admin or REST API route.
			if ( is_admin() || ! empty( $GLOBALS['wp']->query_vars['rest_route'] ) ) {
				return;
			}
			// check HTTP method.
			$request_method = ( ! empty( $_SERVER['REQUEST_METHOD'] )
				? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) )
				: 'GET' );
			// Return if the request method is not GET or POST.
			if ( ! in_array(
				$request_method,
				apply_filters(
					'dlm_accepted_request_methods',
					array( 'GET', 'POST' )
				)
			)
			) {
				return;
			}

			// GET to query_var.
			if ( ! empty( $_GET[ $this->endpoint ] ) ) {
				$wp->query_vars[ $this->endpoint ]
					= sanitize_text_field( wp_unslash( $_GET[ $this->endpoint ] ) );
			}

			// Check and see if this is an XHR request or a classic request.
			if ( isset( $_SERVER['HTTP_DLM_XHR_REQUEST'] ) && 'dlm_XMLHttpRequest' === $_SERVER['HTTP_DLM_XHR_REQUEST'] ) {
				define( 'DLM_DOING_XHR', true );
			}

			// check if endpoint is set but is empty.
			if ( apply_filters( 'dlm_empty_download_redirect_enabled', true )
				&& isset( $wp->query_vars[ $this->endpoint ] )
				&& empty( $wp->query_vars[ $this->endpoint ] )
			) {
				// IF XHR, send redirect header.
				if ( $this->check_for_xhr() ) {
					header(
						'X-DLM-Redirect: '
							. apply_filters(
								'dlm_empty_download_redirect_url',
								home_url()
							)
					);
					exit;
				}
				wp_redirect(
					apply_filters(
						'dlm_empty_download_redirect_url',
						home_url()
					)
				);
				exit;
			}

			// check if need to handle an actual download.
			if ( ! empty( $wp->query_vars[ $this->endpoint ] )
				&& ( ( null === $wp->request )
						|| ( '' === $wp->request )
						|| ( strstr( $wp->request, $this->endpoint . '/' ) ) )
			) {
				// Prevent caching when endpoint is set
				if ( ! defined( 'DONOTCACHEPAGE' ) ) {
					define( 'DONOTCACHEPAGE', true );
				}

				// Get ID of download
				$raw_id
					= sanitize_title( stripslashes( $wp->query_vars[ $this->endpoint ] ) );

				// Find real ID
				switch ( $this->ep_value ) {
					case 'slug':
						$download_id
							= absint(
								$wpdb->get_var(
									$wpdb->prepare(
										"SELECT ID FROM {$wpdb->posts} WHERE post_name = '%s' AND post_type = 'dlm_download';",
										$raw_id
									)
								)
							);
						break;
					default:
						$download_id = absint( $raw_id );
						break;
				}

				// Prevent hotlinking
				if ( WP_DLM::dlm_prevent_hotlinking() ) {
					// Get referer
					$referer = ! empty( $_SERVER['HTTP_REFERER'] )
						? sanitize_text_field( wp_unslash( $_SERVER['HTTP_REFERER'] ) )
						: '';

					// Check if referer isn't empty or if referer is empty but empty referer isn't allowed
					if ( ! empty( $referer )
						|| ( empty( $referer )
								&& apply_filters(
									'dlm_hotlink_block_empty_referer',
									false
								) )
					) {
						$allowed_referers
								= apply_filters(
									'dlm_hotlink_allowed_referers',
									array( home_url() )
								);
						$allowed          = false;

						// Loop allowed referers
						foreach ( $allowed_referers as $allowed_referer ) {
							if ( strstr( $referer, $allowed_referer ) ) {
								$allowed = true;
								break;
							}
						}

						// Check if allowed
						if ( false == $allowed ) {
							// IF XHR, send redirect header.
							if ( $this->check_for_xhr() ) {
								header(
									'X-DLM-Redirect: '
										. apply_filters(
											'dlm_hotlink_redirect',
											home_url(),
											$download_id
										)
								);
								exit;
							}
							wp_redirect(
								apply_filters(
									'dlm_hotlink_redirect',
									home_url(),
									$download_id
								)
							);
							exit;
						}
					}
				}

				$redirect = apply_filters( 'dlm_404_redirect', false );

				/** @var DLM_Download $download */
				$download = null;
				$version  = null;
				if ( $download_id > 0 ) {
					try {
						$download = download_monitor()
							->service( 'download_repository' )
							->retrieve_single( $download_id );
					} catch ( Exception $e ) {
						if ( $redirect ) {
							// IF XHR, send redirect header.
							if ( $this->check_for_xhr() ) {
								header( 'X-DLM-Redirect: ' . $redirect );
								exit;
							}
							wp_safe_redirect( $redirect );
							exit;
						} elseif ( $this->check_for_xhr() ) {
							header( 'X-DLM-Error: not_found' );
							$restriction_type = 'not_found';
							// Set no access modal.
							$this->set_no_access_modal(
								__(
									'Download does not exist.',
									'download-monitor'
								),
								$download,
								$restriction_type
							);
							http_response_code( 404 );
							exit;
						} else {
							wp_die(
								esc_html__(
									'Download does not exist.',
									'download-monitor'
								) . ' <a href="'
									. esc_url( home_url() ) . '">'
									. esc_html__(
										'Go to homepage &rarr;',
										'download-monitor'
									) . '</a>',
								esc_html__( 'Download Error', 'download-monitor' ),
								array( 'response' => 404 )
							);
						}
					}
				}

				if ( ! $download ) {
					$error_handler->no_download_error( $download );
				}

				// Handle version (if set)
				$version_id = '';

				if ( ! empty( $_GET['version'] ) ) {
					$version_id
						= $download->get_version_id_version_name( sanitize_text_field( wp_unslash( $_GET['version'] ) ) );
				}

				if ( ! empty( $_GET['v'] ) ) {
					$version_id = absint( $_GET['v'] );
				}

				// Check if version is set and if it exists
				if ( $version_id ) {
					try {
						$version = download_monitor()
							->service( 'version_repository' )
							->retrieve_single( $version_id );
						$download->set_version( $version );
					} catch ( Exception $e ) {
					}
				}

				// Action on found download
				if ( $download->exists() ) {
					// Check if download is restricted by password.
					if ( post_password_required( $download_id ) ) {
						// IF XHR, send redirect header.
						if ( $this->check_for_xhr() ) {
							header(
								'X-DLM-Redirect: '
									. $download->get_the_download_link()
							);
							exit;
						}
						wp_die(
							get_the_password_form( $download_id ),
							esc_html__(
								'Password Required',
								'download-monitor'
							)
						);
					}
					// Trigger download process.
					$this->trigger( $download );
				} elseif ( $redirect ) {
					// IF XHR, send redirect header.
					if ( $this->check_for_xhr() ) {
						header( 'X-DLM-Redirect: ' . $redirect );
						exit;
					}
					wp_redirect( $redirect );
				} else {
					$error_handler->no_download_error( $download );
				}

				die( '1' );
			} else {
				// Set the no-waypoints in case link/button has triggering class, and we don't want to do the download action. Ex.: Page Addon extension.
				header( 'X-dlm-no-waypoints: true' );
			}
		}

		/**
		 * Trigger function.
		 *
		 * @access private
		 *
		 * @param  DLM_Download  $download
		 *
		 * @return void
		 */
		private function trigger( $download ) {
			// Download is triggered. First thing we do, send no cache headers.
			$this->cache_headers();
			// Get error handler instance.
			$error_handler = DLM_Download_Error_Handler::get_instance( $this );

			/** @var DLM_Download_Version $version */
			$version = $download->get_version();

			/** @var array $file_paths */
			$file_paths = $version->get_mirrors();

			if ( $this->check_for_xhr() ) {
				// Set required headers used by XHR download
				$this->set_required_xhr_headers( $download, $version );
			}

			// Check if we got files in this version.
			if ( empty( $file_paths ) ) {
				$error_handler->no_file_paths_error( $download );
			}

			// Get a random file (mirror).
			$file_path = $file_paths[ array_rand( $file_paths ) ];

			// Check if we actually got a path.
			if ( ! $file_path ) {
				$error_handler->no_file_path_error( $download );
			}

			/**
			 * Action used for extra checks before download.
			 *
			 * @hook  dlm_extra_download_checks
			 *
			 * @param  DLM_Download  $download   The download.
			 * @param  string        $file_path  The download file path.
			 *
			 * @since 4.9.6
			 *
			 * @hooked $this->check_requirements() - 10
			 *
			 */
			do_action( 'dlm_extra_download_checks', $download, $file_path );

			// Parse file path.
			list( $file_path, $remote_file, $restriction ) = download_monitor()
				->service( 'file_manager' )->get_secure_path( $file_path );

				// Check if $file_path exists, as it can be false if the file was deleted or moved.
			if ( ! $file_path ) {
				$error_handler->no_secure_file_path( $download );
			}

			$is_redirect = $download->is_redirect_only() || apply_filters( 'dlm_do_not_force', false, $download, $version );

			$file_path = apply_filters( 'dlm_file_path', $file_path, $remote_file, $download );
			// Not a redirect, so we need to check the file type.
			if ( ! $is_redirect ) {
				// Defined restricted file types.
				$def_restricted = array( 'php', 'html', 'htm', 'tmp' );
				// User defined restricted file types.
				$user_defined_restricted = apply_filters( 'dlm_restricted_file_types', array(), $download );
				// Merge the two arrays.
				$restricted_file_types = array_merge( $def_restricted, $user_defined_restricted );
				// Do not allow the download of certain file types.
				// If user wants, file type checks can be disabled for remote files.
				$check_file_extension = ( $remote_file && apply_filters( 'dlm_check_remote_extension', true ) ) || ! $remote_file;
				// Check if the file type is restricted.
				if ( $check_file_extension && in_array( $download->get_version()->get_filetype(), $restricted_file_types ) ) {
					$error_handler->restricted_file_type_error( $download );
				}
			}

			// The return of the get_secure_path function is an array that consists of the path ( string ), remote file
			// ( bool ) and restriction ( bool ).
			// If the path is false it means that the file is restricted, so don't download it or redirect to it.
			if ( $restriction ) {
				$error_handler->restricted_file_error( $download );
			}
			if ( $this->check_for_xhr() ) {
				// Set extra headers for XHR download.
				$this->set_extra_xhr_headers( $file_path, $download, $version );
			}
			$modal = get_option( 'dlm_no_access_modal', false );
			// Start session if not started
			if ( apply_filters( 'dlm_set_no_access_download_session', true ) && ( '' === session_id() || ! isset( $_SESSION ) ) && ! $modal ) {
				$is_https = ( ! empty( $_SERVER['HTTPS'] ) && 'off' !== $_SERVER['HTTPS'] ) || ( isset( $_SERVER['SERVER_PORT'] ) && 443 === $_SERVER['SERVER_PORT'] );
				$params   = array(
					'secure'   => $is_https,
					'httponly' => true,
					'samesite' => 'Lax',
				);
				session_set_cookie_params( apply_filters( 'dlm_set_session_params', $params ) );
				session_start();
			}

			// Check Access.
			if ( ! apply_filters( 'dlm_can_download', true, $download, $version, $_REQUEST, $this->check_for_xhr() ) ) {
				// Check if we need to redirect if visitor don't have access to file.
				if ( $redirect = apply_filters( 'dlm_access_denied_redirect', false ) ) {
					// IF XHR, send redirect header.
					if ( $this->check_for_xhr() ) {
						header( 'X-DLM-Redirect: ' . $redirect );
						header( 'X-DLM-No-Access: true' );
						exit;
					}
					header( "Status: 301 redirect,$redirect" );
					wp_redirect( $redirect );
					exit;
				} else {
					// get 'no access' page id.
					$no_access_page_id = get_option( 'dlm_no_access_page', 0 );

					// check if a no access page is set.
					if ( $no_access_page_id > 0 ) {
						// Polylang plugin no access page compatibility.
						if ( function_exists( 'pll_current_language' ) ) {
							$polylang_lang = pll_current_language();
							$translations  = pll_get_post_translations( $no_access_page_id );

							// If a translation for no access page exists, set it as the page id.
							if ( isset( $translations[ $polylang_lang ] ) ) {
								$no_access_page_id
									= absint( $translations[ $polylang_lang ] );
							}
						}

						// get permalink of no access page.
						$no_access_permalink = get_permalink( $no_access_page_id );

						// check if we can find a permalink.
						if ( false !== $no_access_permalink ) {
							// get WordPress permalink structure so we can build the url.
							$structure = get_option( 'permalink_structure', 0 );

							// append download id to no access URL.
							if ( '' == $structure || 0 == $structure ) {
								$no_access_permalink = add_query_arg( 'download-id', $download->get_id(), untrailingslashit( $no_access_permalink ) );
							} else {
								$no_access_permalink = untrailingslashit( $no_access_permalink ) . '/download-id/' . $download->get_id() . '/';
							}

							if ( ! $download->get_version()->is_latest() ) {
								$no_access_permalink = add_query_arg( 'version', $download->get_version()->get_version(), $no_access_permalink );
							}

							// IF XHR, send redirect header.
							if ( $this->check_for_xhr() ) {
								header( 'X-DLM-Redirect: ' . $no_access_permalink );
								// Set no access modal.
								$this->set_no_access_modal( false, $download, 'no_access_page' );
								exit;
							}
							// redirect to no access page.
							header( "Status: 301 redirect,$no_access_permalink" );
							wp_redirect( $no_access_permalink );
							exit; // out.
						}
					}
					$error_handler->no_access_error( $download );
				}
			}

			if ( ! $modal ) {
				// We made it so far, so we can unset the no access text.
				unset( $_SESSION['dlm_no_access_text'] );
				session_write_close();
			}
			// Get the referrer.
			$referrer       = ( isset( $_SERVER['HTTP_REFERER'] ) ) ? esc_url_raw( $_SERVER['HTTP_REFERER'] ) : '';
			$cookie_manager = DLM_Cookie_Manager::get_instance();
			// check if user downloaded this version in the past minute. This checks if the cookie exists and if it's
			// value is the same as the download id.
			if ( false === $cookie_manager->check_cookie_meta( 'wp_dlm_downloading', $download->get_id() ) ) {
				// Trigger Download Action.
				do_action( 'dlm_downloading', $download, $version, $file_path );
				// Set the cookie to prevent multiple download logs in download window of 60 seconds.
				// Do this only for non-XHR downloads as XHR downloads are logged through AJAX request
				if ( WP_DLM::dlm_window_logging() && ! $this->check_for_xhr() && ! $is_redirect ) {
					// Set cookie here to prevent "Cannot modify header information - headers already sent" error
					// in non-XHR downloads.
					$this->dlm_logging->log( $download, $version, 'completed', true, $referrer );
				}
			}

			// Redirect to the file...
			if ( $is_redirect ) {
				if ( ! $this->check_for_xhr() ) {
					$this->dlm_logging->log( $download, $version, 'redirected', false, $referrer );
				}

				// If it's not a remote file we need to create the correct URL.
				if ( ! $remote_file ) {
					// Let's check if the file is in the uploads' folder.
					$uploads_dir = wp_upload_dir();
					// Ge the file path.
					$file_path = str_replace( DIRECTORY_SEPARATOR, '/', $file_path );
					$basedir   = str_replace( DIRECTORY_SEPARATOR, '/', $uploads_dir['basedir'] );
					// Check if the path of the file is a symbolic link.
					$sympath = ( is_link( $basedir ) ) ? str_replace( DIRECTORY_SEPARATOR, '/', readlink( $basedir ) ) : false;

					if ( false !== strpos( $file_path, $basedir ) ) { // File is in the uploads' folder, so we need to create the correct URL.
						// Set the URL for the uploads' folder.
						$file_path
							= str_replace(
								str_replace(
									DIRECTORY_SEPARATOR,
									'/',
									trailingslashit( $basedir )
								),
								str_replace(
									DIRECTORY_SEPARATOR,
									'/',
									trailingslashit( $uploads_dir['baseurl'] )
								),
								$file_path
							);
					} elseif ( $sympath && false !== strpos( $file_path, $sympath ) ) { // File is in the uploads' folder but in symlinked directory, so we need to create the correct URL.
						// Set the URL for the uploads' folder.
						$file_path
							= str_replace(
								str_replace(
									DIRECTORY_SEPARATOR,
									'/',
									trailingslashit( $sympath )
								),
								str_replace(
									DIRECTORY_SEPARATOR,
									'/',
									trailingslashit( $uploads_dir['baseurl'] )
								),
								$file_path
							);
					} else { // This is the case if the file is not located in the uploads' folder.
						// Ensure we have a valid URL, not a file path.
						$scheme = wp_parse_url(
							get_option( 'home' ),
							PHP_URL_SCHEME
						);
						// If there are symbolik links the return of the function will be an URL, so the last replace will not be taken into consideration.
						$file_path = download_monitor()
							->service( 'file_manager' )
							->check_symbolic_links( $file_path, true );

						$file_path
							= str_replace(
								trailingslashit( ABSPATH ),
								site_url( '/', $scheme ),
								$file_path
							);
					}

					// We need to rawurlencode in case there are unicode characters in the file name
					// and to prevent white space from being converted to + sign. Only do this for non-remote files
					// as remote files already have encoded url.
					// Get file name.
					$file_name = DLM_Utils::basename( $file_path );

					if ( strstr( $file_name, '?' ) ) {
						$file_name = current( explode( '?', $file_name ) );
					}

					$file_path = str_replace( $file_name, rawurlencode( $file_name ), $file_path );
				}

				// IF XHR, send redirect header.
				if ( $this->check_for_xhr() ) {
					header( 'X-DLM-Redirect: ' . $file_path );
					exit;
				}

				header( 'X-Robots-Tag: noindex, nofollow', true );
				header( 'Location: ' . $file_path );
				exit;
			}

			$this->download_headers( $file_path, $download, $version, $remote_file );

			do_action( 'dlm_start_download_process', $download, $version, $file_path, $remote_file );

			if ( WP_DLM::dlm_x_sendfile() ) {
				if ( function_exists( 'apache_get_modules' ) && in_array( 'mod_xsendfile', apache_get_modules() ) ) {
					$this->dlm_logging->log( $download, $version, 'completed', false, $referrer );
					header( "X-Sendfile: $file_path" );
					exit;
				} elseif ( stristr(
					getenv( 'SERVER_SOFTWARE' ),
					'lighttpd'
				)
				) {
					$this->dlm_logging->log(
						$download,
						$version,
						'completed',
						false,
						$referrer
					);
					header( "X-LIGHTTPD-send-file: $file_path" );
					exit;
				} elseif ( stristr( getenv( 'SERVER_SOFTWARE' ), 'nginx' )
							|| stristr( getenv( 'SERVER_SOFTWARE' ), 'cherokee' )
				) {
					// Log this way as the js doesn't know who the download_id and version_id is.
					$this->dlm_logging->log(
						$download,
						$version,
						'completed',
						false,
						$referrer
					);
					// If there are symbolik links the return of the function will be an URL, so the last replace will not be taken into consideration.
					$file_path = download_monitor()->service( 'file_manager' )
													->check_symbolic_links(
														$file_path,
														true
													);
					$file_path = str_replace(
						trailingslashit( ABSPATH ),
						'',
						$file_path
					);

					header( "X-Accel-Redirect: /$file_path" );
					exit;
				} elseif ( stristr(
					getenv( 'SERVER_SOFTWARE' ),
					'LiteSpeed'
				)
				) {
					// Log this way as the js doesn't know who the download_id and version_id is.
					$this->dlm_logging->log(
						$download,
						$version,
						'completed',
						false,
						$referrer
					);
					header( "X-LiteSpeed-Location: $file_path" );
					exit;
				}
			}

			$safe_remote = wp_safe_remote_head( $file_path );
			$safe        = true;
			if ( $remote_file && is_wp_error( $safe_remote ) ) {
				$safe = false;
			}

			if ( ! $safe ) {
				// IF XHR, send error header.
				if ( $this->check_for_xhr() ) {
					header( 'X-DLM-Error: security_error' );
					$restriction_type = 'security_error';
					$this->set_no_access_modal(
						__(
							'Something is wrong with the file path.',
							'download-monitor'
						),
						$download,
						$restriction_type
					);
					exit;
				}

				$this->dlm_logging->log(
					$download,
					$version,
					'failed',
					false,
					$referrer
				);
				wp_die(
					esc_html__(
						'Something is wrong with the file path.',
						'download-monitor'
					) . ' <a href="'
						. esc_url( home_url() ) . '">'
						. esc_html__(
							'Go to homepage &rarr;',
							'download-monitor'
						) . '</a>',
					esc_html__( 'Download Error', 'download-monitor' ),
					array( 'response' => 404 )
				);
			}

			// multipart-download and download resuming support - http://www.phpgang.com/force-to-download-a-file-in-php_112.html.
			if ( isset( $_SERVER['HTTP_RANGE'] ) && $version->get_filesize() ) {
				// phpcs:ignore
				list( $a, $range ) = explode( "=", $_SERVER['HTTP_RANGE'], 2 );

				list( $range )             = explode( ',', $range, 2 );
				list( $range, $range_end ) = explode( '-', $range );
				$range                     = intval( $range );
				$range_end_modified        = false;

				if ( ! $range_end || $range_end > $version->get_filesize() ) {
					$range_end          = $version->get_filesize() - 1;
					$range_end_modified = true;
				} else {
					$range_end = intval( $range_end );
				}

				if ( $range_end_modified ) {
					$new_length = ( $range_end - $range ) + 1;
				} else {
					$new_length = $range_end - $range;
				}

				header( $_SERVER['SERVER_PROTOCOL'] . ' 206 Partial Content' );
				header( "Content-Length: $new_length" );
				header( "Content-Range: bytes {$range}-{$range_end}/{$version->get_filesize()}" );
			} else {
				$range = false;
			}

			// Adding contents to an object will trigger error on big files.
			if ( ! $this->readfile_chunked( $file_path, false, $range ) ) {
				if ( $remote_file ) {
					// Redirect - we can't track if this completes or not.
					if ( $this->check_for_xhr() ) {
						header( 'X-DLM-Redirect: ' . $file_path );
						exit;
					}

					header( 'Location: ' . $file_path );
					$this->dlm_logging->log(
						$download,
						$version,
						'redirected',
						false,
						$referrer
					);
				} else {
					// IF XHR, send error header.
					if ( $this->check_for_xhr() ) {
						header( 'X-DLM-Error: file_not_found' );
						$restriction_type = 'file_not_found';
						$this->set_no_access_modal(
							__( 'File not found.', 'download-monitor' ),
							$download,
							$restriction_type
						);
						exit;
					}

					$this->dlm_logging->log(
						$download,
						$version,
						'failed',
						false,
						$referrer
					);
					wp_die(
						esc_html__( 'File not found.', 'download-monitor' )
							. ' <a href="' . esc_url( home_url() ) . '">'
							. esc_html__( 'Go to homepage &rarr;', 'download-monitor' )
							. '</a>',
						esc_html__( 'Download Error', 'download-monitor' ),
						array( 'response' => 404 )
					);
				}
			}

			exit;
		}

		/**
		 * Send cache headers to browser. No cache pelase.
		 */
		private function cache_headers() {
			global $is_IE;

			if ( $is_IE && is_ssl() ) {
				// IE bug prevents download via SSL when Cache Control and Pragma no-cache headers set.
				header( 'Expires: Wed, 11 Jan 1984 05:00:00 GMT' );
				header( 'Cache-Control: private' );
			} else {
				nocache_headers();
			}
		}

		/**
		 * Output download headers
		 *
		 * @param  string                $file_path
		 * @param  DLM_Download          $download
		 * @param  DLM_Download_Version  $version
		 */
		private function download_headers( $file_path, $download, $version, $remote_file ) {
			// Get Mime Type
			$mime_type = 'application/octet-stream';

			foreach ( get_allowed_mime_types() as $mime => $type ) {
				$mimes = explode( '|', $mime );
				if ( in_array( $version->get_filetype(), $mimes ) ) {
					$mime_type = $type;
					break;
				}
			}

			// Get file name
			$file_name = urldecode( DLM_Utils::basename( $file_path ) );

			if ( strstr( $file_name, '?' ) ) {
				$file_name = current( explode( '?', $file_name ) );
			}

			// Environment + headers
			if ( ! ini_get( 'safe_mode' ) ) {
				@set_time_limit( 0 );
			}

			if ( version_compare( PHP_VERSION, '7.4.0', '<' )
				&& function_exists( 'get_magic_quotes_runtime' )
				&& get_magic_quotes_runtime()
			) {
				@set_magic_quotes_runtime( 0 );
			}

			if ( function_exists( 'apache_setenv' ) ) {
				@apache_setenv( 'no-gzip', 1 );
			}

			@session_write_close();
			@ini_set( 'zlib.output_compression', 'Off' );
			@error_reporting( 0 );

			/**
			 * Prevents errors, for example: transfer closed with 3 bytes remaining to read
			 */
			@ob_end_clean(); // Clear the output buffer

			// Zip corruption fix
			while ( ob_get_level() > 0 ) {
				@ob_end_clean();
			}

			$headers = array();
			// We use this method to encode the filename so that file names with characters like
			// chinese or persian can be named correctly after the download in Safari.
			$file_name = rawurlencode( sanitize_file_name( $file_name ) );
			if ( $this->check_for_xhr() ) {
				$headers['Content-Disposition']
											= "attachment; filename=\"{$file_name}\";";
				$headers['X-DLM-File-Name']     = "{$file_name}";
			} else {
				$headers['Content-Disposition']
					= "attachment; filename*=UTF-8''{$file_name};";
			}

			$headers['X-Robots-Tag']              = 'noindex, nofollow';
			$headers['Content-Type']              = $mime_type;
			$headers['Content-Description']       = 'File Transfer';
			$headers['Content-Transfer-Encoding'] = 'binary';
			$headers['Cache-Control']
			= 'no-store, no-cache, must-revalidate, no-transform, max-age=0';

			if ( $remote_file ) {
				$file = wp_remote_head( $file_path );
				if ( ! is_wp_error( $file )
					&& ! empty( $file['headers']['content-length'] )
				) {
					$file_size = $file['headers']['content-length'];
				}
			} else {
				$file_size = filesize( $file_path );
			}

			if ( isset( $file_size ) && $file_size ) {
				// Replace the old way ( getting the filesize from the DB ) in case the user has replaced the file directly using cPanel,
				// FTP or other File Manager, or sometimes using  an optimization service it may cause unwanted results.
				$headers['Content-Length'] = $file_size;
				$headers['Accept-Ranges']  = 'bytes';
				// Set custom filesize header.
				$headers['X-DLM-Filesize'] = $file_size;
			}

			$headers = apply_filters(
				'dlm_download_headers',
				$headers,
				$file_path,
				$download,
				$version
			);

			foreach ( $headers as $key => $value ) {
				header( $key . ': ' . $value );
			}
		}

		/**
		 * Set required XHR download headers
		 *
		 * @param  DLM_Download          $download  DLM Download object.
		 * @param  DLM_Download_Version  $version   DLN Version object.
		 */
		private function set_required_xhr_headers( $download, $version ) {
			$headers = array();

			$headers['X-DLM-Download-ID'] = $download->get_id();
			$headers['X-DLM-Version-ID']  = $version->get_id();
			$headers['X-DLM-Nonce']       = wp_create_nonce( 'dlm_ajax_nonce' );

			foreach ( $headers as $key => $value ) {
				header( $key . ': ' . $value );
			}
		}

		/**
		 * Set extra XHR download headers
		 *
		 * @param  DLM_Download          $download   DLM Download object.
		 * @param  DLM_Download_Version  $version    DLN Version object.
		 * @param  string                $file_path  The file path.
		 */
		private function set_extra_xhr_headers(
			$file_path,
			$download,
			$version
		) {
			$headers = apply_filters(
				'dlm_xhr_download_headers',
				array(),
				$file_path,
				$download,
				$version,
				$_REQUEST
			);

			if ( ! empty( $headers ) ) {
				foreach ( $headers as $key => $value ) {
					header( $key . ': ' . $value );
				}
			}
		}

		/**
		 * readfile_chunked
		 *
		 * Reads file in chunks so big downloads are possible without changing PHP.INI - http://codeigniter.com/wiki/Download_helper_for_large_files/
		 *
		 * @access   public
		 *
		 * @param  string   $file
		 * @param  boolean  $retbytes  return bytes of file
		 * @param  boolean  $range     if  HTTP RANGE to seek
		 *
		 * @return   mixed
		 */
		public function readfile_chunked(
			$file,
			$retbytes = true,
			$range = false
		) {
			$chunksize = 1 * ( 1024 * 1024 );
			$buffer    = '';
			$cnt       = 0;
			$handle    = fopen( $file, 'rb' );

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

			if ( $range ) {
				fseek( $handle, $range );
			}

			while ( ! feof( $handle ) ) {
				$buffer = fread( $handle, $chunksize );
				// phpcs:ignore
				echo $buffer;

				if ( $retbytes ) {
					$cnt += strlen( $buffer );
				}
			}

			$status = fclose( $handle );

			if ( $retbytes && $status ) {
				return $cnt;
			}

			return $status;
		}

		/**
		 * Check if this is an XHR request or not
		 *
		 * @return bool
		 */
		public function check_for_xhr() {
			return defined( 'DLM_DOING_XHR' ) && DLM_DOING_XHR;
		}

		/**
		 * Set headers for Modal opening
		 *
		 * @param  string  $text              The text to be displayed.
		 * @param  object  $download          The download object.
		 * @param  string  $restriction_type  The restriction type.
		 *
		 * @return void
		 * @since 4.7.4
		 */
		public function set_no_access_modal(
			$text,
			$download,
			$restriction_type
		) {
			$access_modal = absint( get_option( 'dlm_no_access_modal', 0 ) );

			header( 'X-DLM-No-Access: true' );
			header(
				'X-DLM-No-Access-Modal: '
					. apply_filters(
						'do_dlm_xhr_access_modal',
						$access_modal,
						$download
					)
			);
			header( 'X-DLM-No-Access-Restriction: ' . $restriction_type );

			if ( ! empty( $text ) ) {
				header(
					'X-DLM-No-Access-Modal-Text: '
						. apply_filters(
							'do_dlm_xhr_access_modal_text',
							$text,
							$download,
							$restriction_type
						)
				);
			}
			header( 'X-DLM-Nonce: ' . wp_create_nonce( 'dlm_ajax_nonce' ) );
		}
	}
}
