I'm banging my head against a wall with an issue involving a WordPress plugin I've been working on. It's supposed to restrict access to PDF files in a specific area of the site, only letting logged-in users see them. The PDFs are located in wp-content/custom-files/pdfs/ and their filenames all start with 's'.
The Problem:
So, the plugin seems to be doing its job. When a logged-in user tries to grab one of these restricted PDFs, I'm seeing in the WordPress debug log that the plugin fires, sees they're logged in, finds the PDF file just fine, and even says it's sending the file using readfile() (or I tried fpassthru() too, same result). It even logs the correct file size and says all the bytes were sent!
But instead of the PDF popping up in the browser, the user gets a big fat 404 Not Found error. What's really weird is, if I access a PDF in the same folder that doesn't start with 's' (so the plugin doesn't kick in), it works perfectly! This makes me think the problem is happening after my plugin does its thing, but before the browser gets the file.
Code Snippets:
Here's the code for my plugin (with the folder path sanitized):
<?php /**
Plugin Name: ...
*/
defined('ABSPATH') || exit;
add_action( 'template_redirect', 'restrict_s_pdfs' );
function restrict_s_pdfs() {
error_log('restrict_s_pdfs function called');
// Check if the user is trying to access a restricted PDF
if ( strpos( $_SERVER['REQUEST_URI'], '/wp-content/custom-files/pdfs/s' ) !== false &&
strtolower( pathinfo( $_SERVER['REQUEST_URI'], PATHINFO_EXTENSION ) ) === 'pdf' ) {
error_log('Potential restricted PDF access detected: ' . $_SERVER['REQUEST_URI']);
if ( is_user_logged_in() ) {
error_log('User is logged in.');
// Define the full path to the PDF file (remove the leading slash from REQUEST_URI)
$filepath = ABSPATH . ltrim(urldecode( $_SERVER['REQUEST_URI'] ), '/');
error_log('Filepath constructed: ' . $filepath);
// Security check: Ensure the file exists and is within the allowed directory
$file_exists = file_exists( $filepath );
error_log('file_exists: ' . ($file_exists ? 'true' : 'false'));
$is_within_allowed_dir = strpos( $filepath, ABSPATH . 'wp-content/custom-files/pdfs/' ) === 0;
error_log('is_within_allowed_dir: ' . ($is_within_allowed_dir ? 'true' : 'false'));
if ( $file_exists && $is_within_allowed_dir ) {
error_log('File exists and is within allowed directory. Serving PDF inline.');
ob_end_clean(); // Try clearing output buffer
// Set the appropriate headers for a PDF
header('Content-Type: application/pdf');
error_log('Content-Type header sent.');
header('Content-Disposition: inline; filename="' . basename( $_SERVER['REQUEST_URI'] ) . '"');
error_log('Content-Disposition header sent.');
$filesize = filesize($filepath);
header('Content-Length: ' . $filesize);
error_log('Content-Length header sent: ' . $filesize);
error_log('About to readfile for inline display: ' . $filepath);
$bytes_sent = readfile($filepath);
error_log('readfile completed for inline display. Bytes sent: ' . $bytes_sent);
if ($bytes_sent !== $filesize) {
error_log('ERROR: Incomplete file transfer for inline display!');
} else {
error_log('File transfer for inline display appears complete.');
}
exit;
} else {
error_log('File not found or not within allowed directory. Returning 403.');
wp_die( 'File not found or access denied.', 'Access Denied', array( 'response' => 403 ) );
}
} else {
error_log('User is not logged in. Redirecting to login with redirect URL.');
// Get the current URL to redirect back to after login
$redirect_url = $_SERVER['REQUEST_URI'];
$login_url = wp_login_url( $redirect_url );
// Redirect to the login page with the redirect URL
wp_redirect( $login_url, 302 );
exit;
}
} else {
error_log('Not a restricted PDF request: ' . $_SERVER['REQUEST_URI']);
}
}
And here my htaccess file:
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteRule ^en/wp-login.php /wp-login.php [QSA,L]
RewriteRule ^de/wp-login.php /wp-login.php [QSA,L]
# **Add your PDF restriction rules here, before the last RewriteRule**
RewriteCond %{REQUEST_URI} ^/wp-content/custom-files/pdfs/s.*\.pdf$ [NC]
RewriteCond %{REQUEST_FILENAME} -f
RewriteRule . /index.php [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
I'm at a complete loss here. It feels like PHP is doing its job, but something else in the server stack is causing these requests to fail with a 404. If you have any ideas what could be causing this error, i would appreciate your help. Thanks in advance!
What I've already tried:
Double-Checked the Plugin Logic: I've gone over the code again and again, and it looks like it's doing what it should. The logs don't show any errors in the plugin itself.
File Paths and Permisions: the file paths the plugin makes are spot on, and I've made sure the PDF files are actually there on the server and that the web server can read them. If i disable the htaccess logic the the files can be served no problem, but as they are served direclty and not through wordpress, the login check wont work.
Output Buffering: Tried throwing in an ob_end_clean() before sending headers, but no joy.
Different PHP Function (FPassthru): Thought maybe readfile() was choking, so I switched to fpassthru(), but the 404 is still there.
Nginx Config Adjustments: Our server uses Nginx in front of Apache, and I've been wrestling with the Nginx config like crazy. Tried all sorts of location blocks to handle these PDFs directly, checking for login cookies, messing with FastCGI settings, and what not.
Also asked several of our AI-Companions for help but they are of no use either.
when the execution arrive to the hook template_redirect
, wordpress has already send the 404 status code.
then when you send the PDF file, you need to set the status code again with status_header(200);
.