Skip to content

Instantly share code, notes, and snippets.

@colorwebdesigner
Created June 8, 2025 19:36
Show Gist options
  • Save colorwebdesigner/8282e85d70d7ed81e2bce0b1d3f2994a to your computer and use it in GitHub Desktop.
Save colorwebdesigner/8282e85d70d7ed81e2bce0b1d3f2994a to your computer and use it in GitHub Desktop.
SecureDownload - Protected File Download with Counter for MODX Revolution
<?php
/**
* SecureDownload - Protected File Download with Counter for MODX Revolution
*
* Features:
* 1. Generates secure time-limited download links
* 2. Tracks download counts in a Template Variable
* 3. Automatically resolves file paths from Media Source
* 4. Prevents direct access to files
* 5. Detailed logging for debugging
*
* @author Colorwebdesigner
* @version 2.0
*
* ===================================================================================
* HOW TO USE:
* ===================================================================================
*
* 1. CONFIGURATION:
* - Set the TV names in the configuration section:
* $tvFilePath = 'your_file_tv'; // TV containing the file reference
* $tvDownloadCount = 'download_count'; // TV to store download counter
* - Optional: Adjust cache time (default 24 hours)
*
* 2. SNIPPET CALL:
* Use in templates or MIGx fields:
* [[!SecureDownload?
* &file=`[[*your_file_tv]]` // TV with file path
* &resource=`[[*id]]` // Resource ID for counter
* ]]
*
* 3. MEDIA SOURCE SETUP:
* - Ensure your file TV is connected to a Media Source
* - The Media Source must have a valid basePath
*
* 4. DEBUGGING:
* - Set $debugMode = true to enable detailed logs
* - Logs appear in MODX system log (filter by "SecureDownload")
*
*
* ===================================================================================
* SECURITY NOTES:
* ===================================================================================
*
* - Tokens expire after 24 hours by default
* - File paths are never exposed to users
* - Direct file access is prevented
* - Counter updates are atomic
*/
// ===== CONFIGURATION =====
$debugMode = false; // Enable detailed logging for debugging
$cacheTime = 86400; // Token lifetime in seconds (24 hours)
$tvFilePath = 'package-offers-item_kp'; // TV name containing file path
$tvDownloadCount = 'download_count'; // TV name for download counter
// ===== END CONFIG =====
// Initialize logging
$requestId = uniqid();
$log = function($message, $level = modX::LOG_LEVEL_INFO) use ($modx, $requestId, $debugMode) {
if ($debugMode) {
$modx->log($level, "[$requestId] $message", '', 'SecureDownload');
}
};
// Override settings from snippet parameters
$cacheTime = (int)$modx->getOption('cacheTime', $scriptProperties, $cacheTime);
$customTvName = $modx->getOption('tvDownloadCount', $scriptProperties, '');
if (!empty($customTvName)) {
$tvDownloadCount = $customTvName;
$log("Using custom counter TV: $tvDownloadCount");
}
/**
* Get absolute file path from Media Source
* @param string $fileValue - File path from TV
* @return string|bool Absolute path or false on error
*/
$getFilePath = function($fileValue) use ($modx, $tvFilePath, $log) {
// Get the TV object
$tv = $modx->getObject('modTemplateVar', ['name' => $tvFilePath]);
if (!$tv) {
$log("ERROR: TV '$tvFilePath' not found", modX::LOG_LEVEL_ERROR);
return false;
}
// Get Media Source ID
$sourceId = $tv->get('source');
$log("Media Source ID: $sourceId");
if (empty($sourceId)) {
$log("WARNING: No Media Source assigned to TV", modX::LOG_LEVEL_WARN);
return false;
}
// Load Media Source
$source = $modx->getObject('sources.modMediaSource', $sourceId);
if (!$source) {
$log("ERROR: Media Source #$sourceId not found", modX::LOG_LEVEL_ERROR);
return false;
}
$source->initialize();
$properties = $source->getPropertyList();
// Get base path with trailing slash
$basePath = isset($properties['basePath'])
? rtrim($properties['basePath'], '/') . '/'
: '';
$log("Media Source basePath: $basePath");
$log("File value from TV: $fileValue");
// Build absolute path
return MODX_BASE_PATH . ltrim($basePath, '/') . ltrim($fileValue, '/');
};
// Download handler mode
if (isset($_GET['download_token'])) {
$token = $_GET['download_token'];
$log("DOWNLOAD HANDLER: Token received - $token");
// Retrieve cached data
$cacheData = $modx->cacheManager->get($token);
if (!$cacheData || !isset($cacheData['file']) || !isset($cacheData['resource'])) {
$log("ERROR: Invalid or expired token", modX::LOG_LEVEL_ERROR);
header('HTTP/1.1 404 Not Found');
exit('Invalid download token');
}
$fileName = $cacheData['file'];
$resourceId = (int)$cacheData['resource'];
$log("File: $fileName | Resource ID: $resourceId");
// Build absolute file path from Media Source
$absolutePath = $getFilePath($fileName);
if (!$absolutePath) {
$log("ERROR: Could not resolve file path", modX::LOG_LEVEL_ERROR);
header('HTTP/1.1 500 Internal Server Error');
exit('Configuration error');
}
$log("Absolute path: $absolutePath");
// Validate file
if (!file_exists($absolutePath) || !is_readable($absolutePath)) {
$log("ERROR: File not found or inaccessible", modX::LOG_LEVEL_ERROR);
header('HTTP/1.1 404 Not Found');
exit('File not found: ' . $fileName);
}
// Update download counter
$log("Updating download counter for resource $resourceId");
$tv = $modx->getObject('modTemplateVar', ['name' => $tvDownloadCount]);
if ($tv) {
$tvr = $modx->getObject('modTemplateVarResource', [
'tmplvarid' => $tv->get('id'),
'contentid' => $resourceId
]);
if ($tvr) {
$count = (int)$tvr->get('value');
$tvr->set('value', $count + 1);
$tvr->save();
$log("Counter updated: $count → " . ($count + 1));
} else {
$tvr = $modx->newObject('modTemplateVarResource');
$tvr->set('tmplvarid', $tv->get('id'));
$tvr->set('contentid', $resourceId);
$tvr->set('value', 1);
$tvr->save();
$log("New counter created: 1");
}
} else {
$log("WARNING: TV '$tvDownloadCount' not found", modX::LOG_LEVEL_WARN);
}
// Send file to browser
$log("Sending file: $fileName");
// Detect MIME type
$mimeType = mime_content_type($absolutePath) ?: 'application/octet-stream';
header('Content-Description: File Transfer');
header('Content-Type: ' . $mimeType);
header('Content-Disposition: attachment; filename="' . $fileName . '"');
header('Content-Length: ' . filesize($absolutePath));
header('Expires: 0');
header('Cache-Control: must-revalidate');
header('Pragma: public');
readfile($absolutePath);
exit;
}
// Link generator mode
$fileInput = $modx->getOption('file', $scriptProperties, '');
if (empty($fileInput)) {
$log("WARNING: Empty file parameter", modX::LOG_LEVEL_WARN);
return '';
}
// Extract clean filename
$fileName = basename($fileInput);
$log("LINK GENERATOR: File input: '$fileInput' | Clean name: '$fileName'");
// Get resource ID (current resource by default)
$resourceId = (int)$modx->getOption('resource', $scriptProperties, $modx->resource->get('id'));
$log("Resource ID: $resourceId");
// Create unique token
$token = md5($fileName . microtime() . uniqid());
$log("Token generated: $token");
// Store data in cache
$cacheData = [
'file' => $fileName,
'resource' => $resourceId
];
$modx->cacheManager->set($token, $cacheData, $cacheTime);
$log("Cache data stored: " . print_r($cacheData, true));
// Generate secure download URL
$url = $modx->makeUrl(
$modx->resource->get('id'),
'',
['download_token' => $token],
'full'
);
$log("Secure URL generated: $url");
return $url;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment