Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git

Add hit counter for downloads

AuthorDaveJarvis <email>
Date2023-11-08 19:04:18 GMT-0800
Commita9835e9a8430fb8f53c65a4152944076d7d2a000
Parent6e30610
Delta79 lines added, 31 lines removed, 48-line increase
www/index.shtml
<strong>Version <!--#include file="downloads/version.txt" --></strong>
<br><!--#flastmod virtual="downloads/version.txt" -->
+ <br><!--#exec cmd="awk '{s+=$1} END {print s}' *-count.txt 2> /dev/null || echo 0" -->
+ downloads
</p>
</main>
www/downloads/counter.php
<?php
- // Log all errors to a file.
+ // Log all errors to a temporary file.
ini_set( "log_errors", 1 );
ini_set( "error_log", "/tmp/php-errors.log" );
// Keep running upon client disconnect (helps catch file transfer failures).
+ // This setting requires checking whether the connection has been aborted at
+ // a regular interval to prevent bogging the server with abandoned requests.
ignore_user_abort( true );
-
- // Allow the download to complete.
- set_time_limit( 0 );
// Allow setting session variables (cookies).
- session_start();
+ if( session_id() === PHP_SESSION_NONE ) {
+ session_start();
+ }
/**
* Answers whether the user's session has expired.
*
* @param int $lifetime Number of seconds the session lasts before expiring.
*
* @return bool True indicates the session has expired (or was not set).
*/
function session_expired( $lifetime ) {
+ // Session cookie, not used for user tracking, tracks last download date.
+ $COOKIE_NAME = 'LAST_DOWNLOAD';
$now = time();
- $expired = !isset( $_SESSION[ 'LAST_ACTIVITY' ] );
+ $expired = !isset( $_SESSION[ $COOKIE_NAME ] );
- if( !$expired && ($now - $_SESSION[ 'LAST_ACTIVITY' ]) > $lifetime ) {
+ if( !$expired && ($now - $_SESSION[ $COOKIE_NAME ]) > $lifetime ) {
$_SESSION = array();
session_destroy();
$expired = true;
}
// Update last activity timestamp.
- $_SESSION[ 'LAST_ACTIVITY' ] = $now;
+ $_SESSION[ $COOKIE_NAME ] = $now;
return $expired;
/**
* Acquires a lock for a particular file. Callers would be prudent to
- * call this function from within a try/finally handler and close the lock
- * in the finally block. The amount of time between opening and closing
- * the lock must be quick because parallel processes will be waiting on
+ * call this function from within a try/finally block and close the lock
+ * in the finally section. The amount of time between opening and closing
+ * the lock must be minimal because parallel processes will be waiting on
* the lock's release.
*
* @param string $filename The name of file to lock.
*
* @return bool True if the lock was obtained, false upon excessive attempts.
*/
function lock_open( $filename ) {
- $lockfile = create_lock_filename( $filename );
+ $lockdir = create_lock_filename( $filename );
// Track the number of times a lock attempt is made.
$iterations = 0;
do {
- // Create and test lock file existence atomically.
- if( @mkdir( $lockfile, 0777 ) ) {
+ // Creates and tests lock file existence atomically.
+ if( @mkdir( $lockdir, 0777 ) ) {
// Exit the loop.
$iterations = 0;
}
else {
$iterations++;
- $lifetime = time() - filemtime( $lockfile );
+ $lifetime = time() - filemtime( $lockdir );
if( $lifetime > 10 ) {
// If the lock has gone stale, delete it.
- @rmdir( $lockfile );
+ @rmdir( $lockdir );
}
else {
}
+ /**
+ * Releases the lock on a particular file.
+ *
+ * @param string $filename The name of file that was locked.
+ */
function lock_close( $filename ) {
@rmdir( create_lock_filename( $filename ) );
}
+ /**
+ * Isolate the file name being downloaded.
+ *
+ * @param array $fileinfo The result from calling pathinfo.
+ *
+ * @return string The normalized file name.
+ */
function normalize_filename( $fileinfo ) {
$basename = $fileinfo[ 'basename' ];
: $basename;
}
+
+ $basename = preg_replace( '/\s+/', '', $basename );
+ $basename = mb_ereg_replace( '([^\w\d\-_~,;\[\]\(\).])', '', $basename );
+ $basename = mb_ereg_replace( '([\.]{2,})', '', $basename );
return $basename;
}
+ /**
+ * Determine the content type based on the file name extension, rather
+ * than the file contents. This could be inaccurate, but we'll trust that
+ * the website administrator is posting files whose content reflects the
+ * file name extension.
+ * <p>
+ * If the file name extension is not known, the content type will force
+ * the download (to prevent the browser from trying to play the content
+ * directly).
+ *
+ * @param array $fileinfo The result from calling pathinfo.
+ *
+ * @return string The IANA-defined Media Type for the file name extension.
+ */
function get_content_type( $fileinfo ) {
$extension = strtolower( $fileinfo[ 'extension' ] );
/**
- * Downloads a file transfer, allowing for resuming partial downloads.
+ * Downloads a file, allowing for resuming partial downloads.
*
- * @param string $path Fully qualified path of file to download.
+ * @param string $path Fully qualified path of a file to download.
*
* @return bool True if the download succeeded.
*/
function download( $path ) {
+ // Don't cache the result of the file stats.
+ clearstatcache();
+
$size = @filesize( $path );
+ $size = $size === false || empty( $size ) ? 0 : $size;
$fileinfo = pathinfo( $path );
$filename = normalize_filename( $fileinfo );
$content_type = get_content_type( $fileinfo );
- $range = '0-0';
+ $range = "0-$size";
- // Check if http_range is sent by browser or download manager.
+ // Check if a range is sent by browser or download manager.
if( isset( $_SERVER[ 'HTTP_RANGE' ] ) ) {
- list( $units, $range_orig ) = explode( '=', $_SERVER['HTTP_RANGE'], 2 );
+ list( $units, $range_orig ) = explode( '=', $_SERVER[ 'HTTP_RANGE' ], 2 );
if( $units == 'bytes' ) {
// Set start and end based on range, otherwise use defaults.
$seek_end = empty( $seek_end )
- ? $size - 1
+ ? max( $size - 1, 0 )
: min( abs( $seek_end + 0 ), $size - 1 );
$seek_start = empty( $seek_start || $seek_end < abs( $seek_start + 0 ) )
$range_bytes = $seek_start . '-' . $seek_end . '/' . $size;
- if( ob_get_level() == 0 ) {
- ob_start();
+ if( ob_get_level() > 0 ) {
+ ob_end_clean();
}
header( 'Accept-Ranges: bytes' );
header( 'Content-Range: bytes ' . $range_bytes );
header( 'Content-Type: ' . $content_type );
header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
header( 'Content-Length: ' . ($seek_end - $seek_start + 1) );
- $total_bytes = 0;
+ if( ob_get_level() == 0 ) {
+ ob_start();
+ }
+
+ // If the file doesn't exist, don't count it as a download.
+ $bytes_sent = -1;
+
+ // Open the file to be downloaded.
$fp = @fopen( $path, 'rb' );
if( $fp !== false ) {
@fseek( $fp, $seek_start );
$aborted = false;
- $total_bytes = $seek_start;
+ $bytes_sent = $seek_start;
$chunk_size = 1024 * 8;
while( !feof( $fp ) && !$aborted ) {
set_time_limit( 0 );
print( fread( $fp, $chunk_size ) );
+ $bytes_sent += $chunk_size;
if( ob_get_level() > 0 ) {
ob_flush();
}
flush();
- $total_bytes += $chunk_size;
$aborted = connection_aborted();
}
}
- // Download succeeded if the total bytes sent exceeds the file size.
- return $total_bytes >= $size;
+ // Download succeeded if the total bytes matches or exceeds the file size.
+ return $bytes_sent >= $size;
}
- if( download( 'f.txt' ) ) {
- hit_count( 'f.txt' );
+ $filename = isset( $_GET[ 'filename' ] ) ? $_GET[ 'filename' ] : '';
+
+ if( !empty( $filename ) && download( $filename ) ) {
+ hit_count( "$filename-count.txt" );
}
?>