<?php
/**************************************************************************************************
| Ionicware
|
| VERSIONED ASSET HANDLER
|
| autoVersion() (include/functions/main.php) links /assets/css/styles.css as
| /assets/css/styles.<mtime>.css, and .htaccess sends that name here. The file is served from
| disk as before; what this adds is a check that the number in the URL is this node's mtime for
| the file.
|
| Why: production is three nodes behind Cloudflare. app01 deploys on merge and app02/app03 rsync
| the docroot from it once a minute, so for up to a minute app01 renders pages linking
| styles.<NEW>.css while the other two still hold the old styles.css. The rewrite this replaces
| mapped any 10-digit number to the plain file, so when Cloudflare's first request for the new
| URL reached app02/03 it cached the old bytes under the new name for a year (Cache Reserve and
| Tiered Cache keep it), and the Pro zone can only purge by URL. It happened on 2026-09-18 twice:
| styles.1789755483.css, and again at 19:06 for styles.1789758363.css.
|
| So only an exact match is cacheable. Any other number still gets this node's bytes, so the
| page stays styled, but with no-store, and the next request tries again - by then the rsync has
| landed and the URL is cached with the right content. That covers both directions: a lagging
| node asked for a newer version, and a caught-up node asked for the version a lagging node just
| linked. rsync -a keeps mtimes, so all three nodes agree once they are in sync.
|
| A content hash in the URL would not help on its own: the lagging node would still answer the
| new hash with the old bytes unless it checked, and checking is all this does.
|
| Deliberately standalone - no config.php - so no session, no cookies and no database: a
| response carrying Set-Cookie is never cached by Cloudflare, and a stylesheet needs none of it.
|
| 2026-09-18 JD
**************************************************************************************************/

$types = array('css' => 'text/css', 'js' => 'application/javascript');

$file    = isset($_GET['f']) ? (string) $_GET['f'] : '';
$version = isset($_GET['v']) ? (string) $_GET['v'] : '';
$ext     = strtolower(pathinfo($file, PATHINFO_EXTENSION));

/*
 * .htaccess only sends names under these directories, so anything else is a
 * direct request for this script. readfile() skips Apache's access rules, so
 * the check here is what keeps it to public stylesheets and scripts - include/
 * and temporary/ are Deny from all, and must stay unreadable through here.
 * Both sides go through realpath(), so a symlinked asset directory on a server
 * still works, while a link inside one that points elsewhere is refused.
 */
$path = false;
if (isset($types[$ext]) && ctype_digit($version) && strpos($file, "\0") === false)
{
	$real = realpath(__DIR__ . '/' . $file);
	foreach (array('assets', 'css', 'js', 'admincp/css', 'admincp/js', 'admincp/plugins') as $dir)
	{
		$root = realpath(__DIR__ . '/' . $dir);
		if ($real !== false && $root !== false && strpos($real, $root . DIRECTORY_SEPARATOR) === 0)
		{
			$path = $real;
			break;
		}
	}
}

// The same headers Apache sent for the static file: no ";charset" on text/css.
ini_set('default_charset', '');
header_remove('X-Powered-By');
header_remove('Set-Cookie');
header_remove('Pragma');
header_remove('Expires');

/*
 * One open handle for the check, the length and the body. rsync replaces a
 * file by renaming a new one over it, so separate lookups by name could check
 * version N and then send the bytes of N+1 under N's year-long URL.
 */
$fp   = $path !== false && is_file($path) ? fopen($path, 'rb') : false;
$stat = $fp !== false ? fstat($fp) : false;

if ($stat === false)
{
	http_response_code(404);
	header('Cache-Control: no-store');
	exit;
}

$mtime = $stat['mtime'];

header('Content-Type: ' . $types[$ext]);
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $mtime) . ' GMT');

if ((string) $mtime !== $version)
{
	header('Cache-Control: no-store');
}
else
{
	header('Cache-Control: public, max-age=31536000');

	$since = isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) : false;
	if ($since !== false && $since >= $mtime)
	{
		http_response_code(304);
		exit;
	}
}

header('Content-Length: ' . $stat['size']);
fpassthru($fp);
