403 ) );
}
$payload = json_decode( $payload_json, true );
if ( ! $payload || empty( $payload['user'] ) || empty( $payload['file'] ) ) {
wp_die( 'Invalid token', 'Forbidden', array( 'response' => 403 ) );
}
// Permission check.
if ( get_current_user_id() !== intval( $payload['user'] ) ) {
wp_die( 'Unauthorized', 'Forbidden', array( 'response' => 403 ) );
}
// Expiry check - links older than AIMSLMS_FILE_TOKEN_TTL are rejected.
if ( empty( $payload['time'] ) || ( time() - (int) $payload['time'] ) > AIMSLMS_FILE_TOKEN_TTL ) {
wp_die( 'Link expired', 'Forbidden', array( 'response' => 403 ) );
}
// File path - resolve relative to the uploads directory (not ABSPATH) so the
// result can be verified to stay inside it, regardless of home_url()/uploads setup.
$upload_dir = wp_upload_dir();
$file_path = str_replace( $upload_dir['baseurl'], $upload_dir['basedir'], $payload['file'] );
$real_path = realpath( $file_path );
$real_base = realpath( $upload_dir['basedir'] );
if ( ! $real_path || ! $real_base || 0 !== strpos( $real_path, $real_base ) ) {
wp_die( 'File not found', 'Not Found', array( 'response' => 404 ) );
}
$file_path = $real_path;
$mime = mime_content_type( $file_path );
$size = filesize( $file_path );
$start = 0;
$end = $size - 1;
// Clean output buffer.
if ( ob_get_level() ) {
ob_end_clean();
}
header( 'Content-Type: ' . $mime );
header( 'Accept-Ranges: bytes' );
/*
* ======================================
* HANDLE VIDEO RANGE REQUEST.
* ======================================
*/
if ( isset( $_SERVER['HTTP_RANGE'] ) ) {
$http_range = sanitize_text_field(
wp_unslash( $_SERVER['HTTP_RANGE'] )
);
preg_match( '/bytes=(\d+)-(\d*)/', $http_range, $matches );
$start = intval( $matches[1] );
if ( ! empty( $matches[2] ) ) {
$end = intval( $matches[2] );
}
header( 'HTTP/1.1 206 Partial Content' );
header( "Content-Range: bytes $start-$end/$size" );
header( 'Content-Length: ' . ( $end - $start + 1 ) );
} else {
header( 'Content-Length: ' . $size );
}
header( 'Content-Disposition: inline; filename="' . basename( $file_path ) . '"' );
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
$fp = fopen( $file_path, 'rb' );
fseek( $fp, $start );
$buffer = 8192;
while ( ! feof( $fp ) && ftell( $fp ) <= $end ) {
if ( 0 !== connection_status() ) {
break;
}
$bytes_left = $end - ftell( $fp ) + 1;
$read_size = min( $buffer, $bytes_left );
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread
$chunk = fread( $fp, $read_size );
if ( false !== $chunk ) {
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- streaming raw binary file bytes (video/PDF) for playback/download, not HTML output; running this through an HTML sanitizer like wp_kses_post() corrupts and truncates the binary stream.
echo $chunk;
}
flush();
}
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
fclose( $fp );
exit;
}
add_action( 'template_redirect', 'aimslms_handle_file_request' );
// PDF Viewer.
add_action(
'init',
function () {
add_rewrite_rule( '^aimslms-secure-pdf/?', 'index.php?aimslms_secure_pdf=1', 'top' );
}
);
add_filter(
'query_vars',
function ( $vars ) {
$vars[] = 'aimslms_secure_pdf';
return $vars;
}
);
add_action(
'template_redirect',
function () {
if ( get_query_var( 'aimslms_secure_pdf' ) ) {
if ( ! session_id() ) {
session_start();
}
$token = '';
if (
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Access is controlled by session token validation and user ownership.
isset( $_GET['token'] )
) {
$token = sanitize_text_field(
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Access is controlled by session token validation and user ownership.
wp_unslash( $_GET['token'] )
);
}
if ( empty( $token ) || empty( $_SESSION['aimslms_pdf_tokens'][ $token ] ) ) {
wp_die( esc_html__( 'Invalid access.', 'aims-infosoft-lms' ), '', array( 'response' => 403 ) );
}
$data = array();
if ( isset( $_SESSION['aimslms_pdf_tokens'][ $token ] ) ) {
$data = map_deep(
wp_unslash( $_SESSION['aimslms_pdf_tokens'][ $token ] ),
'sanitize_text_field'
);
}
if ( time() > $data['expires'] ) {
unset( $_SESSION['aimslms_pdf_tokens'][ $token ] );
wp_die( esc_html__( 'Link expired.', 'aims-infosoft-lms' ), '', array( 'response' => 403 ) );
}
if ( get_current_user_id() !== (int) $data['user_id'] ) {
wp_die( esc_html__( 'Unauthorized.', 'aims-infosoft-lms' ), '', array( 'response' => 403 ) );
}
$pdf_url = esc_url( $data['file'] );
$lesson_title = ! empty( $data['lesson'] ) ? $data['lesson'] . ' PDF Viewer' : 'Secure PDF Viewer';
// Standalone document (no wp_head()/wp_footer()) - register/enqueue our
// assets and print only these handles instead of hardcoding /