PDF Viewer

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

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″]

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.

DLM PDF.js system — component overview WordPress page shortcodes placed by site author [dlm_pdfjs id="x"] opens PDF in new tab [download id="x"] ZIP / other downloads [file_event_count_live] live count + date display Required plugins must be installed and active in WordPress PDF.js Viewer viewer.php endpoint Download Monitor protected file storage Code Snippets runs the PHP snippets Required snippets installed via Code Snippets plugin DLM PDF.js Proxy Viewer Rev 2 — proxy + [dlm_pdfjs] DLM PDF.js Proxy Counter Rev 2 — counting + admin panel
[dlm_pdfjs] — PDF view request flow User clicks [dlm_pdfjs id="x"] link link opens in new browser tab viewer.php loads in new tab PDF.js viewer renders in browser PDF.js requests proxy URL /?dlm_pdf_proxy=x Proxy Viewer snippet handles request verifies ID, resolves path, checks security Counter incremented ron_increment_file_event() PDF streamed to browser readfile() with correct headers
The [dlm_pdfjs] shortcode renders a plain HTML link. Clicking opens viewer.php in a new tab. PDF.js then requests the proxy URL, which verifies the download ID, resolves the file path from Download Monitor metadata, increments the counter, and streams the PDF. The real file path in dlm_uploads is never exposed to the visitor.
[download id="x"] — ZIP download flow User clicks [download id="x"] link standard Download Monitor link Download Monitor serves file handles request natively dlm_downloading hook fires WordPress action after file served Counter snippet catches hook ron_unified_count_dlm_download() → ron_increment_file_event()
For ZIP files and any other non-PDF file type, the existing [download id=”x”] shortcode works unchanged. Download Monitor handles delivery natively and fires the dlm_downloading hook when done. The Proxy Counter snippet listens for this hook and increments the event counter automatically — no changes needed for any file type Download Monitor serves.
[file_event_count_live] — live counter flow [file_event_count_live id="x"] shortcode on page PHP renders span with initial count <span class="ron-file-event-count" data-id="x">47</span> Global flag set true CSS + JS this page only Count shown immediately no blank flash on load JS polls AJAX every 5 seconds admin-ajax.php?action=ron_get_file_event&id=x DOM updated only if value changed prevents unnecessary page reflows every 5s
The live counter renders the current count server-side on first load so there is never a blank value while JavaScript initializes. A global PHP flag ensures the polling JavaScript is only loaded on pages that use a live shortcode — other pages get no extra overhead. The polling loop only updates the DOM when the value has actually changed, preventing unnecessary reflows.
PDF.js Viewer plugin — role in the system Plugin by TwisterMc pdfjs-viewer-shortcode v3.0.4 Bundles Mozilla PDF.js open source PDF renderer viewer.php endpoint /wp-content/plugins/pdfjs-viewer-shortcode/pdfjs/web/ Accepts file= parameter renders PDF from proxy URL in browser
The PDF.js Viewer Shortcode plugin provides the viewer.php endpoint the system relies on. viewer.php is used instead of viewer.html because newer versions of PDF.js bundle ES modules (.mjs files) which require correct MIME types — viewer.php handles this internally. No plugin configuration changes are needed; the system simply calls its viewer endpoint with the proxy URL as the file= parameter.
Download Monitor — role in the system File storage dlm_uploads/YYYY/MM/ Download IDs unique ID per managed file dlm_downloading hook fires on every download Used by Proxy Viewer snippet resolves file path from DLM metadata [download id="x"] for ZIPs unchanged DLM handles delivery, hook triggers counter
Download Monitor stores protected files in a non-public uploads directory, assigns a unique download ID to each file, and fires the dlm_downloading hook when a file is served. The Proxy Viewer snippet uses DLM metadata to resolve the real file path server-side. DLM’s native display counter is disabled since the Proxy Counter provides unified tracking across both PDFs and ZIPs.
Code Snippets plugin — role in the system Code Snippets plugin manages and executes PHP snippets DLM PDF.js Proxy Viewer Rev 2 — snippet 1 DLM PDF.js Proxy Counter Rev 2 — snippet 2 Both snippets active and working together
Code Snippets executes the two PHP snippets within the WordPress environment. An important technical note: because Code Snippets runs snippets at an early load stage, WordPress transient functions are not yet available. This is why the PDF proxy counter uses a static PHP array combined with wp_cache (Redis) for duplicate-count prevention. ZIP and other Download Monitor file downloads are counted via JavaScript click interception — a cache-independent approach that works with any cache system without exclusion rules.
DLM PDF.js Proxy Viewer — what it contains DLM PDF.js Proxy Viewer — Rev 2 [dlm_pdfjs] shortcode renders link, opens viewer in new tab manual fallback map override auto-detect per download ID PDF proxy handler template_redirect, streams PDF path resolver 7-step DLM metadata lookup chain path cache _ron_pdfjs_relative_path post meta file_exists() guard graceful fallback if plugin missing
The Proxy Viewer snippet owns the complete PDF delivery pipeline. The [dlm_pdfjs] shortcode renders a link to viewer.php with the proxy URL as the file parameter. The proxy intercepts requests via template_redirect, runs security checks, resolves the file path through a 6-step metadata lookup, caches the result, increments the counter, and streams the PDF. The real file path in dlm_uploads is never exposed to the visitor.
DLM PDF.js Proxy Counter — what it contains DLM PDF.js Proxy Counter — Rev 2 ron_increment_file_event() static array + Redis two-layer lock dlm_downloading hook counts ZIP and all other downloads display shortcodes [file_event_count_live] + date AJAX endpoint ron_get_file_event — JSON response DLM PDF.js Stats panel Downloads → admin panel conditional JS enqueue only loads on pages that need it
The Proxy Counter is the shared counting engine for the entire system. It provides the core increment function called by both the PDF proxy and the ZIP download hook. The two-layer lock (static PHP array for within-request duplicates, Redis wp_cache for cross-request duplicates) prevents PDF.js byte-range requests from inflating counts. The admin panel under Downloads → DLM PDF.js Stats shows sortable stats with editable counts for restoring historical data.

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
}

Leave a Reply

Your email address will not be published. Required fields are marked *