When you need to deliver secure, seamless PDF viewing with activity statistics on WordPress, Biophysics Lab provides a polished, end‑to‑end solution.
Ron Fredericks, BiophysicsLab.com, 03/31/2026
DLM PDF.js System
Biophysics Lab’s DLM PDF.js System is a coordinated pair of WordPress proxies that deliver secure PDF viewing and protected file downloads with unified event tracking. Designed for sites using the popular Download Monitor and PDF.js Viewer plugins, this system ensures consistent file‑access control and accurate, consolidated activity reporting. It also demonstrates a practical, reusable developer pattern—leveraging trusted, rock‑solid plugins and extending them with lightweight “glue logic” implemented through Code Snippets.
The DLM PDF.js System solves three problems that Download Monitor and PDF.js Viewer cannot handle natively:
- Protected PDF files managed by Download Monitor cannot be viewed inline — clicking a download link prompts a file download rather than opening a viewer.
- The PDF.js Viewer plugin cannot access Download Monitor‘s protected file storage directly, making it impossible to use the two plugins together without the proxy layer presented with this system.
- Download Monitor‘s admin tool does not count PDF views (since the PDF viewer bypass the normal download flow). DLM PDF.js System offers its own admin tool to quickly see all file counts served by Download Monitor as well as the PDF.js views.
Table of Contents
- DLM PDF.js System
- Plugin Settings
- Demonstration
- System Architecture
- Code Snippets Source Code by Biophysics Lab
Plugin Settings
Download Monitor File Download Counter
The DLM PDF.js System provides shortcode tags to display total file download count/last date, and PDF view count/last date on user pages. Meanwhile Download Monitor automatically displays total download counts but not PDF viewer counts and not last date. Meanwhile Download Monitor does not show download statistics for Admin users, while DLM PDF.js System does, so the two counts presented to the user won’t be the same.
To avoid duplicate count display for file downloads, disable Download Monitor plugin’s download count in Dashboard > Download Monitor > Settings > General settings > Default Template. Set the template to “Title – Shows download title only”
Plugins Installed
The DLM PDF.js System needs three plugins for Biophysics Lab’s proxy logic to work.
- PDF.js plugin (free version): The version of PDFjs Viewer - Embed PDFs is 3.0.4
- Download Monitor (free version): The version of Download Monitor is 5.1.12
- Code Snippets (free version): The version of Code Snippets is 3.9.5
- DLM PDF.js Proxy Viewer [open source php]
- DLM PDF.js Proxy Counter [open source php]
Demonstration
This demo describes the features available with the DLM PDF.js System and offers a live demonstration:
- View a secure PDF document by PDF.js
- Count and time-stamp all files served by Download Monitor
- Fast tracking of file activity by count or date-stamp with a new admin page within Download Monitor’s Dashboard service
The PDF.js Viewer Shortcode plugin handles the front‑end experience, while Download Monitor keeps your files protected behind authenticated access. To tie everything together, Biophysics Lab adds two lightweight PHP proxies—delivered through the Code Snippets plugin—that enable controlled viewing and precise activity tracking. The monitor proxy also introduces a dedicated DLM PDF.js Stats dashboard page, giving you clear, real‑time insight into how your documents are being accessed across your site.
Admin Page
The DLM PDF.js Stats admin page is found in Download Monitor’s Dashboard tools called Downloads.
The page offers easy count reset, count update per file, and control of counts while logged in as an Administrator.

Download/View
The files presented here are uploaded to your WordPress site using the Download Monitor plugin. This process produces a reference ID to access files in the Download Monitor secure directory as is the default.
Download any file: Use the standard shortcode offered by Download Monitor to show a clickable link to begin download. Optionally include the DLM PDF.js file event tags to log download count and event date. For example:
- [download id=”3972″]
- [file_event_count_live id=”3972″]
- [file_event_date_live id=”3972″]
View PDF file: Use the shortcode offered by DLM PDF.js to show a clickable link to launch PDF in new tab. Optionally include the DLM PDF.js file event tags to log download count and event date. For example:
- [dlm_pdfjs id=”4146″]
- [file_event_count_live id=”4146″]
- [file_event_date_live id=”4146″]
DLM PDF.js System: How to Use – View PDF
5
2026-04-05 02:27:32 GMT
DLM PDF.js System Release: Code and Docs
3
2026-04-05 02:27:40 GMT
System Architecture
A clickable architecture drawing where each feature is mapped to a more detailed view along with a description.
Click any component box to explore its detail diagram.
Code Snippets Source Code by Biophysics Lab
DLM PDF.js Proxy Viewer
/*
* DLM PDF.js Proxy Viewer
* Contributor: Ron Fredericks, BiophysicsLab.com
* Snippet: DLM PDF.js Proxy Viewer
* Rev: 2
* Last updated: 3/31/2026
*
* Provides a protected PDF proxy and viewer shortcode for files managed
* by the Download Monitor plugin.
*
* Features:
* - Shortcode [dlm_pdfjs id="123"] renders a link to open the PDF in a
* new tab using the PDF.js viewer with full viewer controls
* - Link text defaults to the Download Monitor title, or can be overridden
* with the name= attribute: [dlm_pdfjs id="123" name="My PDF Title"]
* - Proxy streams protected DLM PDF files securely from uploads
* - Auto-detects PDF path from Download Monitor metadata
* - Caches resolved path in post meta for performance
* - New downloads auto-resolve on first proxy request via 7-step resolver
* - Allows manual fallback map for stubborn files
* - Counting is handled by the companion snippet:
* DLM PDF.js Proxy Counter
*
* Shortcode usage:
* [dlm_pdfjs id="123"]
* [dlm_pdfjs id="123" name="My PDF Title"]
*
* Typical page usage:
* [dlm_pdfjs id="123"]
* [file_event_count_live id="123"] [file_event_date_live id="123"]
*
* IMPORTANT:
* This snippet calls ron_increment_file_event($download_id, 'proxy') from the
* companion snippet for unified counting of PDF views.
* ZIP file counting is handled via JavaScript click interception in the
* companion snippet - no server-side hook is used for ZIPs, making counting
* fully cache-independent.
*/
/*
* OPTIONAL MANUAL FALLBACKS
*
* The auto-detect system resolves PDF paths automatically from Download Monitor
* metadata and caches the result in post meta (_ron_pdfjs_relative_path).
* In most cases this array should remain empty.
*
* How to override auto-detect system:
*
* Add an entry here only if auto-detect fails for a specific download ID.
* What follows is a description of the example presented here.
*
* The download_id for each file managed by the Download Monitor plugin can be
* found in the WordPress Dashboard → Downloads → All Downloads: Shortcode column.
*
* Path format: relative to /wp-content/uploads/
* For Download Monitor files this is typically: dlm_uploads/YYYY/MM/filename.pdf
*
* Example:
* 3873 => 'dlm_uploads/2026/03/The_Transistor_1951_v4.pdf',
*/
function ron_dlm_pdfjs_manual_map() {
return array(
// Example:
// 3873 => 'dlm_uploads/2026/03/The_Transistor_1951_v4.pdf',
);
}
/*
* Shortcode: [dlm_pdfjs id="123"]
* Renders a link that opens the protected PDF in a new tab using PDF.js viewer.
* Link text defaults to the Download Monitor title for the download.
* Optional name= attribute overrides the link text.
*/
add_shortcode('dlm_pdfjs', 'ron_dlm_pdfjs_link_shortcode');
function ron_dlm_pdfjs_link_shortcode($atts) {
$atts = shortcode_atts(
array(
'id' => 0,
'name' => '',
),
$atts,
'dlm_pdfjs'
);
$download_id = absint($atts['id']);
if (!$download_id) {
return '';
}
$viewer_file = 'viewer.php';
$viewer_disk_path = WP_CONTENT_DIR . '/plugins/pdfjs-viewer-shortcode/pdfjs/web/' . $viewer_file;
if ( ! file_exists( $viewer_disk_path ) ) {
return '';
}
$proxy_url = add_query_arg(
array('dlm_pdf_proxy' => $download_id),
home_url('/')
);
$viewer_url = home_url('/wp-content/plugins/pdfjs-viewer-shortcode/pdfjs/web/' . $viewer_file)
. '?file=' . rawurlencode($proxy_url);
$label = !empty($atts['name'])
? esc_html($atts['name'])
: esc_html(get_the_title($download_id));
return '<a href="' . esc_url($viewer_url) . '" target="_blank">'
. $label
. '</a>';
}
/*
* Handle the proxy request.
*/
add_action('template_redirect', 'ron_dlm_handle_pdf_proxy', 1);
function ron_dlm_handle_pdf_proxy() {
if (!isset($_GET['dlm_pdf_proxy'])) {
return;
}
$download_id = absint($_GET['dlm_pdf_proxy']);
if (!$download_id) {
ron_dlm_proxy_die(400, 'Missing download ID.');
}
if (!function_exists('wp_get_upload_dir')) {
ron_dlm_proxy_die(500, 'WordPress upload functions unavailable.');
}
$download_post = get_post($download_id);
if (!$download_post) {
ron_dlm_proxy_die(404, 'Download not found.');
}
$relative_path = ron_dlm_pdfjs_get_cached_relative_pdf_path($download_id);
if (!$relative_path) {
$manual = ron_dlm_pdfjs_manual_map();
if (isset($manual[$download_id])) {
$relative_path = ltrim($manual[$download_id], '/');
}
}
/*
* If cache and manual map both failed, run the full resolver.
* This handles new downloads that have never been proxied before
* and therefore have no cached path yet. The resolver will also
* write the cache so subsequent requests use the fast path.
*/
if (!$relative_path) {
$relative_path = ron_dlm_pdfjs_resolve_relative_pdf_path($download_id, null, $download_post);
}
if (!$relative_path) {
ron_dlm_proxy_die(404, 'No cached PDF path found for this download.');
}
$file_path = ron_dlm_pdfjs_relative_to_absolute_path($relative_path);
if (!$file_path) {
ron_dlm_proxy_die(404, 'Could not resolve a readable PDF file for this download.');
}
$real = realpath($file_path);
if (!$real || !is_file($real) || !is_readable($real)) {
ron_dlm_proxy_die(404, 'Protected PDF file not found or not readable.');
}
$uploads = wp_get_upload_dir();
$uploads_real = !empty($uploads['basedir']) ? realpath($uploads['basedir']) : false;
if (!$uploads_real || strpos($real, $uploads_real) !== 0) {
ron_dlm_proxy_die(403, 'Resolved file is outside uploads directory.');
}
if (strtolower(pathinfo($real, PATHINFO_EXTENSION)) !== 'pdf') {
ron_dlm_proxy_die(403, 'This proxy only serves PDF files.');
}
/*
* Unified counting:
* Pass 'proxy' as source so the companion snippet applies the
* byte-range duplicate lock appropriate for PDF.js requests.
*/
if (function_exists('ron_increment_file_event')) {
ron_increment_file_event($download_id, 'proxy');
}
nocache_headers();
header('Content-Type: application/pdf');
header('Content-Length: ' . filesize($real));
header('Content-Disposition: inline; filename="' . basename($real) . '"');
header('Accept-Ranges: bytes');
header('X-Content-Type-Options: nosniff');
readfile($real);
exit;
}
/*
* Main resolver.
* Returns uploads-relative path like:
* dlm_uploads/2026/03/file.pdf
*
* 7-step resolution chain:
* 1) Manual fallback map
* 2) Cached post meta (_ron_pdfjs_relative_path)
* 3) Version object getters (get_path, get_url, get_file_url, etc.)
* 4) Nested file object getters
* 5) Download object getters + version children object getters
* + _files meta on version child post (key lookup for new DLM uploads)
* 6) Common post meta keys on download post
* 7) Returns false if all steps fail
*/
function ron_dlm_pdfjs_resolve_relative_pdf_path($download_id, $version = null, $download = null) {
// 1) manual fallback wins
$manual = ron_dlm_pdfjs_manual_map();
if (isset($manual[$download_id])) {
$relative = ron_dlm_pdfjs_normalize_relative_path($manual[$download_id]);
if ($relative && ron_dlm_pdfjs_relative_to_absolute_path($relative)) {
ron_dlm_pdfjs_cache_relative_pdf_path($download_id, $relative);
return $relative;
}
}
// 2) cached path
$cached = ron_dlm_pdfjs_get_cached_relative_pdf_path($download_id);
if ($cached && ron_dlm_pdfjs_relative_to_absolute_path($cached)) {
return $cached;
}
$candidates = array();
// 3) version object from filter call
if (is_object($version)) {
$candidates = array_merge($candidates, ron_dlm_pdfjs_extract_candidates_from_object($version));
}
// 4) nested file object if available
if (is_object($version) && method_exists($version, 'get_file')) {
$file = $version->get_file();
if (is_object($file)) {
$candidates = array_merge($candidates, ron_dlm_pdfjs_extract_candidates_from_object($file));
}
}
// 5) download object from filter call
if (is_object($download)) {
$candidates = array_merge($candidates, ron_dlm_pdfjs_extract_candidates_from_object($download));
if (method_exists($download, 'get_versions')) {
$versions = $download->get_versions();
if (is_array($versions)) {
foreach ($versions as $v) {
if (is_object($v)) {
$candidates = array_merge($candidates, ron_dlm_pdfjs_extract_candidates_from_object($v));
if (method_exists($v, 'get_file')) {
$file = $v->get_file();
if (is_object($file)) {
$candidates = array_merge($candidates, ron_dlm_pdfjs_extract_candidates_from_object($file));
}
}
}
}
}
}
/*
* Also check _files meta on the version child post directly.
* Download Monitor stores file URLs as a JSON-encoded array in
* this meta key. This runs once after the version loop, not inside it.
* This is the key lookup for new downloads added via the DLM UI.
*/
$version_posts = get_posts(array(
'post_type' => 'dlm_download_version',
'post_parent' => $download_id,
'posts_per_page' => 1,
'post_status' => 'any',
));
if (!empty($version_posts)) {
$files_raw = get_post_meta($version_posts[0]->ID, '_files', true);
if (is_string($files_raw) && $files_raw !== '') {
$files_decoded = json_decode($files_raw, true);
if (is_array($files_decoded) && !empty($files_decoded)) {
foreach ($files_decoded as $file_url) {
if (is_string($file_url) && $file_url !== '') {
$candidates[] = $file_url;
}
}
} else {
$candidates[] = $files_raw;
}
}
}
}
// 6) common post meta fallbacks
$meta_keys = array(
'dlm_download_version',
'dlm_version',
'downloadable_file',
'file',
);
foreach ($meta_keys as $key) {
$value = get_post_meta($download_id, $key, true);
if (is_string($value) && $value !== '') {
$candidates[] = $value;
}
}
foreach ($candidates as $candidate) {
$relative = ron_dlm_pdfjs_candidate_to_relative_pdf_path($candidate);
if ($relative) {
ron_dlm_pdfjs_cache_relative_pdf_path($download_id, $relative);
return $relative;
}
}
return false;
}
/*
* Pull likely path/url strings from an object using common getters.
*/
function ron_dlm_pdfjs_extract_candidates_from_object($object) {
$out = array();
$methods = array(
'get_path',
'get_file_path',
'get_url',
'get_file_url',
'get_filename',
);
foreach ($methods as $method) {
if (method_exists($object, $method)) {
try {
$value = $object->$method();
if (is_string($value) && $value !== '') {
$out[] = $value;
}
} catch (Throwable $e) {
}
}
}
return $out;
}
/*
* Convert any candidate string to uploads-relative PDF path if possible.
*/
function ron_dlm_pdfjs_candidate_to_relative_pdf_path($candidate) {
if (!is_string($candidate) || $candidate === '') {
return false;
}
$candidate = trim($candidate);
// URL inside uploads
if (filter_var($candidate, FILTER_VALIDATE_URL)) {
$relative = ron_dlm_pdfjs_convert_uploads_url_to_relative($candidate);
if ($relative && strtolower(pathinfo($relative, PATHINFO_EXTENSION)) === 'pdf') {
return $relative;
}
return false;
}
// Absolute path
if ($candidate[0] === '/' || strpos($candidate, ':') !== false) {
$relative = ron_dlm_pdfjs_convert_absolute_path_to_relative($candidate);
if ($relative && strtolower(pathinfo($relative, PATHINFO_EXTENSION)) === 'pdf') {
return $relative;
}
return false;
}
// Relative path
$relative = ron_dlm_pdfjs_normalize_relative_path($candidate);
if ($relative && strtolower(pathinfo($relative, PATHINFO_EXTENSION)) === 'pdf') {
$absolute = ron_dlm_pdfjs_relative_to_absolute_path($relative);
if ($absolute) {
return $relative;
}
}
return false;
}
/*
* Convert uploads URL to uploads-relative path.
*/
function ron_dlm_pdfjs_convert_uploads_url_to_relative($url) {
if (!function_exists('wp_get_upload_dir')) {
return false;
}
$uploads = wp_get_upload_dir();
if (empty($uploads['baseurl'])) {
return false;
}
if (strpos($url, $uploads['baseurl']) !== 0) {
return false;
}
$relative = substr($url, strlen($uploads['baseurl']));
return ron_dlm_pdfjs_normalize_relative_path($relative);
}
/*
* Convert absolute uploads path to uploads-relative path.
*/
function ron_dlm_pdfjs_convert_absolute_path_to_relative($path) {
if (!function_exists('wp_get_upload_dir')) {
return false;
}
$uploads = wp_get_upload_dir();
if (empty($uploads['basedir'])) {
return false;
}
$uploads_real = realpath($uploads['basedir']);
$path_real = realpath($path);
if (!$uploads_real || !$path_real) {
return false;
}
if (strpos($path_real, $uploads_real) !== 0) {
return false;
}
$relative = substr($path_real, strlen($uploads_real));
return ron_dlm_pdfjs_normalize_relative_path($relative);
}
/*
* Normalize relative path.
*/
function ron_dlm_pdfjs_normalize_relative_path($relative) {
if (!is_string($relative) || $relative === '') {
return false;
}
$relative = str_replace('\\', '/', $relative);
$relative = ltrim($relative, '/');
if ($relative === '') {
return false;
}
return $relative;
}
/*
* Convert uploads-relative path to absolute path if readable.
*/
function ron_dlm_pdfjs_relative_to_absolute_path($relative) {
if (!function_exists('wp_get_upload_dir')) {
return false;
}
$uploads = wp_get_upload_dir();
if (empty($uploads['basedir'])) {
return false;
}
$full = trailingslashit($uploads['basedir']) . ltrim($relative, '/');
$real = realpath($full);
if (!$real || !is_file($real) || !is_readable($real)) {
return false;
}
return $real;
}
/*
* Cache resolved relative path.
*/
function ron_dlm_pdfjs_cache_relative_pdf_path($download_id, $relative_path) {
update_post_meta($download_id, '_ron_pdfjs_relative_path', $relative_path);
}
/*
* Get cached relative path.
*/
function ron_dlm_pdfjs_get_cached_relative_pdf_path($download_id) {
$value = get_post_meta($download_id, '_ron_pdfjs_relative_path', true);
if (!is_string($value) || $value === '') {
return false;
}
return ron_dlm_pdfjs_normalize_relative_path($value);
}
/*
* Friendly proxy errors for testing.
*/
function ron_dlm_proxy_die($status, $message) {
status_header((int) $status);
nocache_headers();
wp_die(
esc_html($message),
'PDF Proxy',
array(
'response' => (int) $status,
)
);
}
DLM PDF.js Proxy Counter
/*
* DLM PDF.js Proxy Counter
* Contributor: Ron Fredericks, BiophysicsLab.com
* Snippet: DLM PDF.js Proxy Counter
* Rev: 2
* Last updated: 3/30/2026
*
* Tracks:
* - PDF proxy opens via shared function call from Proxy Viewer snippet
* - ZIP/other Download Monitor downloads via JavaScript click interception
*
* Counting architecture:
* - PDF views: counted server-side via ron_increment_file_event('proxy')
* called from the Proxy Viewer snippet during the proxy request.
* - ZIP downloads: counted client-side via JavaScript click handler that
* calls the ron_count_dlm_download AJAX endpoint on every link click.
* This approach is fully cache-independent — works with LiteSpeed,
* WP Rocket, Cloudflare, or any other cache system without exclusion rules.
*
* Shared meta keys:
* - _ron_file_event_count
* - _ron_file_event_last_gmt
*
* Shortcodes:
* - [file_event_count id="123"]
* - [file_event_date id="123"]
* - [file_event_count_live id="123"]
* - [file_event_date_live id="123"]
*
* Admin panel:
* - WordPress Admin → Downloads → DLM PDF.js Stats
*/
/*
* Core increment function.
* Called from:
* - Proxy Viewer snippet for PDF views (source = 'proxy')
* - AJAX endpoint for ZIP/DLM downloads (source = 'dlm')
*
* Source-aware locking:
* - 'proxy': shared Redis lock per download_id prevents PDF.js byte-range
* requests from double-counting a single PDF view across simultaneous
* PHP processes within a 10-second window.
* - 'dlm': unique lock key per click so every legitimate ZIP download
* always counts regardless of timing.
*/
function ron_increment_file_event($download_id, $source = 'proxy') {
$download_id = absint($download_id);
if (!$download_id) {
return false;
}
if ($source === 'proxy') {
$lock_key = 'ron_file_event_lock_' . $download_id;
} else {
$lock_key = 'ron_file_event_lock_' . $download_id . '_' . uniqid();
}
/*
* Layer 1: Static PHP array catches duplicate calls within
* the same PHP process for both sources.
*/
static $processed = array();
if ( isset($processed[$lock_key]) ) {
return false;
}
$processed[$lock_key] = true;
/*
* Layer 2: Redis wp_cache lock for PDF proxy only.
* Prevents double counts from PDF.js simultaneous byte-range requests
* arriving as separate PHP processes within the 10-second window.
* Not applied to ZIP/DLM downloads — each click must always count.
*/
if ( $source === 'proxy' ) {
if ( wp_cache_get($lock_key, 'ron_file_event') ) {
return false;
}
wp_cache_set($lock_key, 1, 'ron_file_event', 10);
}
$count = (int) get_post_meta($download_id, '_ron_file_event_count', true);
update_post_meta($download_id, '_ron_file_event_count', $count + 1);
update_post_meta($download_id, '_ron_file_event_last_gmt', current_time('mysql', true));
return true;
}
/*
* AJAX endpoint for ZIP/DLM download counting.
* Called by JavaScript click handler when user clicks a Download Monitor link.
* This approach is cache-independent — admin-ajax.php is never cached.
* The dlm_downloading server-side hook is NOT used for ZIP counting.
*/
add_action('wp_ajax_ron_count_dlm_download', 'ron_ajax_count_dlm_download');
add_action('wp_ajax_nopriv_ron_count_dlm_download', 'ron_ajax_count_dlm_download');
function ron_ajax_count_dlm_download() {
nocache_headers();
$download_id = isset($_POST['download_id']) ? absint($_POST['download_id']) : 0;
if (!$download_id) {
wp_send_json_error('Missing download ID.');
}
/*
* Verify the download exists in Download Monitor.
*/
$post = get_post($download_id);
if (!$post || $post->post_type !== 'dlm_download') {
wp_send_json_error('Invalid download ID.');
}
ron_increment_file_event($download_id, 'dlm');
wp_send_json_success();
}
/*
* Public helper to read unified count.
*/
function ron_get_file_event_count($download_id) {
return (int) get_post_meta((int) $download_id, '_ron_file_event_count', true);
}
/*
* Public helper to read unified last GMT date.
*/
function ron_get_file_event_date($download_id) {
$value = get_post_meta((int) $download_id, '_ron_file_event_last_gmt', true);
return is_string($value) ? $value : '';
}
/*
* Shortcode: [file_event_count id="123"]
*/
add_shortcode('file_event_count', 'ron_file_event_count_shortcode');
function ron_file_event_count_shortcode($atts) {
$atts = shortcode_atts(
array(
'id' => 0,
),
$atts,
'file_event_count'
);
$download_id = absint($atts['id']);
if (!$download_id) {
return '';
}
return (string) ron_get_file_event_count($download_id);
}
/*
* Shortcode: [file_event_date id="123"]
*/
add_shortcode('file_event_date', 'ron_file_event_date_shortcode');
function ron_file_event_date_shortcode($atts) {
$atts = shortcode_atts(
array(
'id' => 0,
),
$atts,
'file_event_date'
);
$download_id = absint($atts['id']);
if (!$download_id) {
return '';
}
$value = ron_get_file_event_date($download_id);
if (!$value) {
return '';
}
return esc_html($value . ' GMT');
}
/*
* Live shortcode: [file_event_count_live id="123"]
*/
add_shortcode('file_event_count_live', 'ron_file_event_count_live_shortcode');
function ron_file_event_count_live_shortcode($atts) {
global $ron_file_event_live_used;
$ron_file_event_live_used = true;
$atts = shortcode_atts(
array(
'id' => 0,
),
$atts,
'file_event_count_live'
);
$download_id = absint($atts['id']);
if (!$download_id) {
return '';
}
$count = ron_get_file_event_count($download_id);
return '<span class="ron-file-event-count" data-id="' . esc_attr($download_id) . '">' . esc_html($count) . '</span>';
}
/*
* Live shortcode: [file_event_date_live id="123"]
*/
add_shortcode('file_event_date_live', 'ron_file_event_date_live_shortcode');
function ron_file_event_date_live_shortcode($atts) {
global $ron_file_event_live_used;
$ron_file_event_live_used = true;
$atts = shortcode_atts(
array(
'id' => 0,
),
$atts,
'file_event_date_live'
);
$download_id = absint($atts['id']);
if (!$download_id) {
return '';
}
$date = ron_get_file_event_date($download_id);
$text = $date ? $date . ' GMT' : '';
return '<span class="ron-file-event-date" data-id="' . esc_attr($download_id) . '">' . esc_html($text) . '</span>';
}
/*
* AJAX endpoint for live count/date display refresh.
*/
add_action('wp_ajax_ron_get_file_event', 'ron_ajax_get_file_event');
add_action('wp_ajax_nopriv_ron_get_file_event', 'ron_ajax_get_file_event');
function ron_ajax_get_file_event() {
nocache_headers();
$download_id = isset($_GET['id']) ? absint($_GET['id']) : 0;
if (!$download_id) {
wp_send_json_error();
}
$count = ron_get_file_event_count($download_id);
$date = ron_get_file_event_date($download_id);
wp_send_json_success(array(
'count' => $count,
'date' => $date ? $date . ' GMT' : '',
));
}
/*
* Built-in CSS to prevent layout jitter.
* Only outputs on pages that use a live shortcode.
*/
add_action('wp_head', 'ron_file_event_live_css');
function ron_file_event_live_css() {
global $ron_file_event_live_used;
if ( ! $ron_file_event_live_used ) {
return;
}
?>
<style>
.ron-file-event-count,
.ron-file-event-date {
display: inline-block;
vertical-align: baseline;
}
.ron-file-event-count {
min-width: 4ch;
}
.ron-file-event-date {
min-width: 22ch;
}
</style>
<?php
}
/*
* Built-in JS for:
* 1. ZIP/DLM download click counting — intercepts all Download Monitor
* links (URL pattern /download/) and fires an AJAX count request.
* Cache-independent: works with any cache system without exclusion rules.
* 2. Live counter display refresh — polls every 5 seconds and updates
* count/date spans when values change.
*
* Always loaded on every page because ZIP download links can appear anywhere.
* The live counter polling only runs if live shortcode spans are present.
*/
add_action('wp_footer', 'ron_file_event_live_script');
function ron_file_event_live_script() {
?>
<script>
(function() {
/*
* ZIP/DLM download click counting.
* Intercepts clicks on any Download Monitor link (href contains /download/)
* and fires an AJAX POST to increment the count for that download ID.
* The download ID is extracted from the URL pattern /download/NNNN/.
* The file download proceeds normally — the AJAX call does not block it.
*/
function ronAttachDownloadCounters() {
document.querySelectorAll('a[href*="/download/"]').forEach(function(link) {
if (link.dataset.ronCounted) return;
link.dataset.ronCounted = '1';
link.addEventListener('click', function() {
var href = this.href || '';
var match = href.match(/\/download\/(\d+)\//);
if (!match) return;
var downloadId = match[1];
var data = new FormData();
data.append('action', 'ron_count_dlm_download');
data.append('download_id', downloadId);
fetch('/wp-admin/admin-ajax.php', {
method: 'POST',
body: data
}).catch(function() {
// Ignore errors — download proceeds regardless
});
});
});
}
/*
* Live counter display refresh.
* Only runs if live shortcode spans are present on the page.
* Polls every 5 seconds and updates DOM only when value has changed.
*/
function ronUpdateFileEvents() {
var countEls = document.querySelectorAll('.ron-file-event-count');
var dateEls = document.querySelectorAll('.ron-file-event-date');
var ids = new Set();
countEls.forEach(function(el) { if (el.dataset.id) ids.add(el.dataset.id); });
dateEls.forEach(function(el) { if (el.dataset.id) ids.add(el.dataset.id); });
if (ids.size === 0) return;
ids.forEach(function(id) {
fetch('/wp-admin/admin-ajax.php?action=ron_get_file_event&id='
+ encodeURIComponent(id) + '&_=' + Date.now())
.then(function(res) { return res.json(); })
.then(function(data) {
if (!data.success) return;
document.querySelectorAll(
'.ron-file-event-count[data-id="' + id + '"]'
).forEach(function(el) {
var newText = String(data.data.count);
if (el.textContent !== newText) el.textContent = newText;
});
document.querySelectorAll(
'.ron-file-event-date[data-id="' + id + '"]'
).forEach(function(el) {
var newText = data.data.date || '';
if (el.textContent !== newText) el.textContent = newText;
});
})
.catch(function() {
// Ignore transient fetch errors
});
});
}
document.addEventListener('DOMContentLoaded', function() {
ronAttachDownloadCounters();
ronUpdateFileEvents();
setInterval(ronUpdateFileEvents, 5000);
});
})();
</script>
<?php
}
/*
* Admin panel: DLM PDF.js Stats
* Shows all Download Monitor files with their event counts and last event date.
* Accessible at: WordPress Admin → Downloads → DLM PDF.js Stats
*/
add_action('admin_menu', 'ron_file_event_stats_menu');
function ron_file_event_stats_menu() {
add_submenu_page(
'edit.php?post_type=dlm_download',
'DLM PDF.js Stats',
'DLM PDF.js Stats',
'manage_options',
'ron-file-event-stats',
'ron_file_event_stats_page'
);
}
/*
* Admin panel page renderer.
*/
function ron_file_event_stats_page() {
if (!current_user_can('manage_options')) {
wp_die('Access denied.');
}
// Get WordPress timezone label
$tz_string = get_option('timezone_string');
if (!$tz_string) {
$offset = get_option('gmt_offset');
$tz_string = timezone_name_from_abbr('', $offset * 3600, false);
}
$tz_label = $tz_string ? $tz_string : 'UTC';
// Short timezone abbreviation for display (e.g. PST, EST)
try {
$tz_obj = new DateTimeZone($tz_label);
$dt = new DateTime('now', $tz_obj);
$tz_abbr = $dt->format('T');
} catch (Exception $e) {
$tz_abbr = $tz_label;
}
// Handle Reset All
if (
isset($_POST['ron_reset_all']) &&
check_admin_referer('ron_file_event_reset_all', 'ron_nonce')
) {
$all = get_posts(array(
'post_type' => 'dlm_download',
'posts_per_page' => -1,
'post_status' => 'any',
'fields' => 'ids',
));
foreach ($all as $id) {
update_post_meta($id, '_ron_file_event_count', 0);
update_post_meta($id, '_ron_file_event_last_gmt', '');
}
echo '<div class="notice notice-success"><p>All counts have been reset to zero.</p></div>';
}
// Handle individual count update
if (
isset($_POST['ron_update_count']) &&
check_admin_referer('ron_file_event_update', 'ron_nonce')
) {
$update_id = absint($_POST['ron_update_id']);
$update_count = absint($_POST['ron_update_count_value']);
if ($update_id) {
update_post_meta($update_id, '_ron_file_event_count', $update_count);
echo '<div class="notice notice-success"><p>Count updated successfully.</p></div>';
}
}
// Sorting
$valid_orderby = array('title', 'count', 'date');
$orderby = isset($_GET['orderby']) && in_array($_GET['orderby'], $valid_orderby)
? $_GET['orderby'] : 'count';
$order = isset($_GET['order']) && $_GET['order'] === 'asc' ? 'asc' : 'desc';
$new_order = $order === 'asc' ? 'desc' : 'asc';
// Pagination
$per_page = 50;
$current_page = isset($_GET['paged']) ? max(1, absint($_GET['paged'])) : 1;
// Fetch all downloads
$all_downloads = get_posts(array(
'post_type' => 'dlm_download',
'posts_per_page' => -1,
'post_status' => 'any',
));
// Build rows with meta data
$rows = array();
foreach ($all_downloads as $dl) {
$count = (int) get_post_meta($dl->ID, '_ron_file_event_count', true);
$last_gmt = get_post_meta($dl->ID, '_ron_file_event_last_gmt', true);
// Convert GMT to local time
$last_local = '';
if ($last_gmt) {
try {
$dt = new DateTime($last_gmt, new DateTimeZone('UTC'));
$dt->setTimezone(new DateTimeZone($tz_label));
$last_local = $dt->format('M j, Y g:i A') . ' ' . $tz_abbr;
} catch (Exception $e) {
$last_local = $last_gmt . ' GMT';
}
}
// Detect file type - first try PDF proxy cache, then DLM version metadata
$relative = get_post_meta($dl->ID, '_ron_pdfjs_relative_path', true);
if ($relative) {
$ext = strtoupper(pathinfo($relative, PATHINFO_EXTENSION));
} else {
$versions = get_posts(array(
'post_type' => 'dlm_download_version',
'post_parent' => $dl->ID,
'posts_per_page' => 1,
'post_status' => 'any',
));
if (!empty($versions)) {
$files = get_post_meta($versions[0]->ID, '_files', true);
if (is_string($files) && $files !== '') {
$decoded = json_decode($files, true);
if (is_array($decoded) && !empty($decoded)) {
$first_file = reset($decoded);
$ext = strtoupper(pathinfo(parse_url($first_file, PHP_URL_PATH), PATHINFO_EXTENSION));
} else {
$ext = strtoupper(pathinfo(parse_url($files, PHP_URL_PATH), PATHINFO_EXTENSION));
}
} elseif (is_array($files) && !empty($files)) {
$first_file = reset($files);
$ext = strtoupper(pathinfo(parse_url($first_file, PHP_URL_PATH), PATHINFO_EXTENSION));
} else {
$ext = '—';
}
} else {
$ext = '—';
}
}
$rows[] = array(
'id' => $dl->ID,
'title' => $dl->post_title,
'type' => $ext,
'count' => $count,
'last_gmt' => $last_gmt,
'last_local' => $last_local,
'edit_url' => get_edit_post_link($dl->ID),
);
}
// Sort rows
usort($rows, function($a, $b) use ($orderby, $order) {
if ($orderby === 'title') {
$cmp = strcasecmp($a['title'], $b['title']);
} elseif ($orderby === 'date') {
$cmp = strcmp($a['last_gmt'], $b['last_gmt']);
} else {
$cmp = $a['count'] - $b['count'];
}
return $order === 'asc' ? $cmp : -$cmp;
});
// Paginate
$total = count($rows);
$total_pages = max(1, ceil($total / $per_page));
$offset = ($current_page - 1) * $per_page;
$rows = array_slice($rows, $offset, $per_page);
// Base URL for sorting/pagination links
$base_url = admin_url('edit.php?post_type=dlm_download&page=ron-file-event-stats');
?>
<div class="wrap">
<h1>DLM PDF.js Stats</h1>
<p>All times shown in <?php echo esc_html($tz_abbr); ?>
(<?php echo esc_html($tz_label); ?>).
Showing <?php echo $total; ?> download(s).
</p>
<p style="color:#999; font-size:12px;">
DLM PDF.js Stats — developed by
<a href="https://www.biophysicslab.com" target="_blank">
Ron Fredericks, BiophysicsLab.com
</a>
</p>
<?php /* Reset All Form */ ?>
<form method="post" id="ron-reset-form" style="margin-bottom:20px;">
<?php wp_nonce_field('ron_file_event_reset_all', 'ron_nonce'); ?>
<button type="submit" name="ron_reset_all" value="1"
class="button button-secondary"
onclick="return confirm('⚠️ Are you sure you want to reset ALL counts to zero?\n\nThis cannot be undone.');">
Reset All Counts
</button>
</form>
<?php /* Stats Table */ ?>
<table class="wp-list-table widefat fixed striped">
<colgroup>
<col style="width:40px;">
<col>
<col style="width:60px;">
<col style="width:160px;">
<col style="width:220px;">
<col style="width:120px;">
</colgroup>
<thead>
<tr>
<th>#</th>
<th>
<a href="<?php echo esc_url(add_query_arg(array(
'orderby' => 'title',
'order' => $orderby === 'title' ? $new_order : 'asc',
), $base_url)); ?>">
Title
<?php if ($orderby === 'title'): ?>
<?php echo $order === 'asc' ? '▲' : '▼'; ?>
<?php else: ?>
<span style="color:#ccc;">▲▼</span>
<?php endif; ?>
</a>
</th>
<th>Type</th>
<th>
<a href="<?php echo esc_url(add_query_arg(array(
'orderby' => 'count',
'order' => $orderby === 'count' ? $new_order : 'desc',
), $base_url)); ?>">
Count
<?php if ($orderby === 'count'): ?>
<?php echo $order === 'asc' ? '▲' : '▼'; ?>
<?php else: ?>
<span style="color:#ccc;">▲▼</span>
<?php endif; ?>
</a>
</th>
<th style="padding-left:15px;">
<a href="<?php echo esc_url(add_query_arg(array(
'orderby' => 'date',
'order' => $orderby === 'date' ? $new_order : 'desc',
), $base_url)); ?>">
Last Event
<?php if ($orderby === 'date'): ?>
<?php echo $order === 'asc' ? '▲' : '▼'; ?>
<?php else: ?>
<span style="color:#ccc;">▲▼</span>
<?php endif; ?>
</a>
</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<?php foreach ($rows as $i => $row): ?>
<tr>
<td><?php echo $offset + $i + 1; ?></td>
<td><?php echo esc_html($row['title']); ?></td>
<td><?php echo esc_html($row['type']); ?></td>
<td>
<form method="post" style="display:flex;gap:6px;align-items:center;">
<?php wp_nonce_field('ron_file_event_update', 'ron_nonce'); ?>
<input type="hidden" name="ron_update_id"
value="<?php echo esc_attr($row['id']); ?>"/>
<input type="number" name="ron_update_count_value"
value="<?php echo esc_attr($row['count']); ?>"
style="width:70px;" min="0"/>
<input type="submit" name="ron_update_count"
value="Save" class="button button-small"/>
</form>
</td>
<td style="padding-left:15px;"><?php echo esc_html($row['last_local'] ?: '—'); ?></td>
<td>
<a href="<?php echo esc_url($row['edit_url']); ?>"
target="_blank" class="button button-small">
Edit in DLM
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php /* Pagination */ ?>
<?php if ($total_pages > 1): ?>
<div style="margin-top:15px;">
<?php for ($p = 1; $p <= $total_pages; $p++): ?>
<?php if ($p === $current_page): ?>
<strong style="margin-right:5px;">[<?php echo $p; ?>]</strong>
<?php else: ?>
<a style="margin-right:5px;"
href="<?php echo esc_url(add_query_arg(array(
'paged' => $p,
'orderby' => $orderby,
'order' => $order,
), $base_url)); ?>">
<?php echo $p; ?>
</a>
<?php endif; ?>
<?php endfor; ?>
</div>
<?php endif; ?>
</div>
<?php
}


