Dave Jarvis' Repositories

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

Adds nonce to script

Author Dave Jarvis <email>
Date 2026-03-03 12:18:37 GMT-0800
Commit 2f1f12d0e9118a6429e98406112b13cf72713a3a
Parent b40b02b
downloads/counter.php
set_time_limit( 0 );
- ignore_user_abort( true );
-
- $level = ob_get_level();
-
- while( $level > 0 ) {
- ob_end_clean();
-
- $newLevel = ob_get_level();
-
- if( $newLevel === $level ) {
- break;
- }
- $level = $newLevel;
+ while( ob_get_level() > 0 ) {
+ ob_end_flush();
}
if( session_id() === "" ) {
session_start();
}
-
- class DownloadManager {
- private $filename;
- private $expiry;
-
- public function __construct( $filename, $expiry ) {
- $this->filename = $filename;
- $this->expiry = $expiry;
- }
- public function process() {
- $result = false;
- $valid = $this->filename !== '';
+ ignore_user_abort( true );
- if( $valid ) {
- $expired = $this->tokenExpired();
+ $filename = get_sanitized_filename();
+ $valid = !empty( $filename );
+ $expiry = 24 * 60 * 60;
- if( $expired ) {
- $this->incrementCount();
- }
+ if( $valid && download( $filename ) && token_expired( $expiry ) ) {
+ increment_count( "$filename-count.txt" );
+ }
- $downloaded = $this->download();
+ /**
+ * Retrieve the file name being downloaded from the HTTP GET request.
+ *
+ * @return string The sanitized file name (without path information).
+ */
+ function get_sanitized_filename() {
+ $filepath = isset( $_GET[ 'filename' ] ) ? $_GET[ 'filename' ] : '';
+ $fileinfo = pathinfo( $filepath );
+ $basename = $fileinfo[ 'basename' ];
- if( $downloaded ) {
- $result = true;
- }
- }
+ if( isset( $_SERVER[ 'HTTP_USER_AGENT' ] ) ) {
+ $periods = substr_count( $basename, '.' );
- return $result;
+ $basename = strstr( $_SERVER[ 'HTTP_USER_AGENT' ], 'MSIE' )
+ ? mb_ereg_replace( '/\./', '%2e', $basename, $periods - 1 )
+ : $basename;
}
-
- private function download() {
- $result = false;
- $method = isset( $_SERVER[ 'REQUEST_METHOD' ] ) ? $_SERVER[ 'REQUEST_METHOD' ] : 'GET';
- $isHead = $method === 'HEAD';
- $contentType = mime_content_type( $this->filename );
-
- clearstatcache();
- $fileSize = @filesize( $this->filename );
- $size = $fileSize === false || empty( $fileSize ) ? 0 : $fileSize;
+ $basename = mb_ereg_replace( '/\s+/', '', $basename );
+ $basename = mb_ereg_replace( '([^\w\d\-_~,;\[\]\(\).])', '', $basename );
- if( !$isHead ) {
- $rangeData = $this->parseRange( $size );
- $start = $rangeData[ 0 ];
- $length = $rangeData[ 1 ];
+ return $basename;
+ }
- if( $length > 0 ) {
- header_remove( 'x-powered-by' );
- header( 'Expires: 0' );
- header( 'Cache-Control: public, must-revalidate, post-check=0, pre-check=0' );
- header( 'Cache-Control: private', false );
- header( 'Content-Disposition: attachment; filename="' . $this->filename . '"' );
- header( 'Accept-Ranges: bytes' );
- header( "Content-Length: $length" );
- header( "Content-Type: $contentType" );
+ /**
+ * Answers whether the user's download token has expired.
+ *
+ * @param int $lifetime Number of seconds before expiring the token.
+ *
+ * @return bool True indicates the token has expired (or was not set).
+ */
+ function token_expired( $lifetime ) {
+ $TOKEN_NAME = 'LAST_DOWNLOAD';
+ $now = time();
+ $expired = !isset( $_SESSION[ $TOKEN_NAME ] );
- $result = $this->transmit( $start, $size );
- }
- }
+ if( !$expired && ( $now - $_SESSION[ $TOKEN_NAME ] > $lifetime ) ) {
+ $expired = true;
+ $_SESSION = array();
- return $result;
+ session_destroy();
}
-
- private function parseRange( $size ) {
- $start = 0;
- $length = $size;
- if( isset( $_SERVER[ 'HTTP_RANGE' ] ) ) {
- $format = '/^bytes=(\d*)-(\d*)(?:,\d*-\d*)*$/';
- $range = $_SERVER[ 'HTTP_RANGE' ];
- $match = preg_match( $format, $range, $matches );
+ $_SESSION[ $TOKEN_NAME ] = $now;
- if( $match ) {
- $start = isset( $matches[ 1 ] ) ? (int)$matches[ 1 ] : 0;
- $end = !empty( $matches[ 2 ] ) ? (int)$matches[ 2 ] : $size - 1;
- $length = $end - $start + 1;
+ $TOKEN_CREATE = 'CREATED';
- header( 'HTTP/1.1 206 Partial Content' );
- header( "Content-Range: bytes $start-$end/$size" );
- }
- else {
- header( 'HTTP/1.1 416 Requested Range Not Satisfiable' );
- header( "Content-Range: bytes */$size" );
+ if( !isset( $_SESSION[ $TOKEN_CREATE ] ) ) {
+ $_SESSION[ $TOKEN_CREATE ] = $now;
+ }
+ else if( $now - $_SESSION[ $TOKEN_CREATE ] > $lifetime ) {
+ session_regenerate_id( true );
+ $_SESSION[ $TOKEN_CREATE ] = $now;
+ }
- $length = 0;
- }
- }
+ return $expired;
+ }
- return array(
- $start,
- $length
- );
- }
+ /**
+ * Downloads a file, allowing for resuming partial downloads.
+ *
+ * @param string $filename File to download, must be in script directory.
+ *
+ * @return bool True if the file was transferred.
+ */
+ function download( $filename ) {
+ clearstatcache();
- private function transmit( $seekStart, $size ) {
- $result = false;
- $bytesSent = -1;
- $fp = @fopen( $this->filename, 'rb' );
+ $size = @filesize( $filename );
+ $size = $size === false || empty( $size ) ? 0 : $size;
+ $content_type = mime_content_type( $filename );
+ list( $seek_start, $content_length ) = parse_range( $size );
- if( $fp !== false ) {
- $aborted = false;
- $bytesSent = $seekStart;
- $chunkSize = 1024 * 16;
+ header_remove( 'x-powered-by' );
+ header( 'Expires: 0' );
+ header( 'Cache-Control: public, must-revalidate, post-check=0, pre-check=0' );
+ header( 'Cache-Control: private', false );
+ header( "Content-Disposition: attachment; filename=\"$filename\"" );
+ header( 'Accept-Ranges: bytes' );
+ header( "Content-Length: $content_length" );
+ header( "Content-Type: $content_type" );
- @fseek( $fp, $seekStart );
+ $method = isset( $_SERVER[ 'REQUEST_METHOD' ] )
+ ? $_SERVER[ 'REQUEST_METHOD' ]
+ : 'GET';
- while( !feof( $fp ) && !$aborted ) {
- print( @fread( $fp, $chunkSize ) );
+ return $method === 'HEAD'
+ ? false
+ : transmit( $filename, $seek_start, $size );
+ }
- $bytesSent += $chunkSize;
- $aborted = connection_aborted() || connection_status() !== 0;
+ /**
+ * Parses the HTTP range request header, provided one was sent by the
+ * client. This provides download resume functionality.
+ *
+ * @param int $size The total file size (as stored on disk).
+ *
+ * @return array The starting offset for resuming the download, or 0 to
+ * download the entire file (i.e., no offset could be parsed); also the
+ * number of bytes to be transferred.
+ */
+ function parse_range( $size ) {
+ $seek_start = 0;
+ $content_length = $size;
- flush();
- }
+ if( isset( $_SERVER[ 'HTTP_RANGE' ] ) ) {
+ $range_format = '/^bytes=(\d*)-(\d*)(?:,\d*-\d*)*$/';
+ $request_range = $_SERVER[ 'HTTP_RANGE' ];
- fclose( $fp );
+ if( !preg_match( $range_format, $request_range, $matches ) ) {
+ header( 'HTTP/1.1 416 Requested Range Not Satisfiable' );
+ header( "Content-Range: bytes */$size" );
- $result = $bytesSent >= $size;
+ exit;
}
- return $result;
- }
+ $seek_start = isset( $matches[ 1 ] ) ? $matches[ 1 ] + 0 : 0;
+ $seek_end = !empty( $matches[ 2 ] ) ? $matches[ 2 ] + 0 : $size - 1;
+ $range_bytes = $seek_start . '-' . $seek_end . '/' . $size;
+ $content_length = $seek_end - $seek_start + 1;
- private function tokenExpired() {
- $tokenName = 'LAST_DOWNLOAD';
- $tokenCreate = 'CREATED';
- $now = time();
- $expired = !isset( $_SESSION[ $tokenName ] );
+ header( 'HTTP/1.1 206 Partial Content' );
+ header( "Content-Range: bytes $range_bytes" );
+ }
- if( !$expired && $now - $_SESSION[ $tokenName ] > $this->expiry ) {
- $expired = true;
- $_SESSION = array();
+ return array( $seek_start, $content_length );
+ }
- session_destroy();
- }
+ /**
+ * Transmits a file from the server to the client.
+ *
+ * @param string $filename File to download, must be this script directory.
+ * @param int $seek_start Offset into file to start downloading.
+ * @param int $size Total size of the file.
+ *
+ * @return bool True if the file was transferred.
+ */
+ function transmit( $filename, $seek_start, $size ) {
+ if( ob_get_level() == 0 ) {
+ ob_start();
+ }
- $_SESSION[ $tokenName ] = $now;
+ $bytes_sent = -1;
+ $fp = @fopen( $filename, 'rb' );
- if( !isset( $_SESSION[ $tokenCreate ] ) ) {
- $_SESSION[ $tokenCreate ] = $now;
- }
- else {
- $elapsed = $now - $_SESSION[ $tokenCreate ];
+ if( $fp !== false ) {
+ @fseek( $fp, $seek_start );
- if( $elapsed > $this->expiry ) {
- session_regenerate_id( true );
+ $aborted = false;
+ $bytes_sent = $seek_start;
+ $chunk_size = 1024 * 16;
- $_SESSION[ $tokenCreate ] = $now;
- }
- }
+ while( !feof( $fp ) && !$aborted ) {
+ print( @fread( $fp, $chunk_size ) );
- return $expired;
- }
+ $bytes_sent += $chunk_size;
- private function incrementCount() {
- $result = false;
- $filename = $this->filename . '-count.txt';
- $lockDir = $filename . '.lock';
- $locked = $this->lockOpen( $lockDir );
+ if( ob_get_level() > 0 ) {
+ ob_flush();
+ }
- if( $locked ) {
- $count = (int)@file_get_contents( $filename ) + 1;
+ flush();
- @file_put_contents( $filename, $count );
- @rmdir( $lockDir );
+ $aborted = connection_aborted() || connection_status() != 0;
+ }
- $result = true;
+ if( ob_get_level() > 0 ) {
+ ob_end_flush();
}
- return $result;
+ fclose( $fp );
}
- private function lockOpen( $lockDir ) {
- $result = false;
- $iterations = 0;
+ return $bytes_sent >= $size;
+ }
- while( $iterations < 10 ) {
- $made = @mkdir( $lockDir, 0777 );
+ /**
+ * Increments the number in a file using an exclusive lock. The file
+ * is set to an initial value set to 0 if it doesn't exist.
+ *
+ * @param string $filename The file containing a number to increment.
+ */
+ function increment_count( $filename ) {
+ try {
+ lock_open( $filename );
- if( $made ) {
- $result = true;
- break;
- }
+ $count = @file_get_contents( $filename ) + 1;
+
+ file_put_contents( $filename, $count );
+ }
+ finally {
+ lock_close( $filename );
+ }
+ }
+
+ /**
+ * Acquires a lock for a particular file. Callers would be prudent to
+ * 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 ) {
+ $lockdir = create_lock_filename( $filename );
+ $iterations = 0;
+ do {
+ if( @mkdir( $lockdir, 0777 ) ) {
+ $iterations = 0;
+ }
+ else {
$iterations++;
- $lifetime = time() - (int)@filemtime( $lockDir );
+ $lifetime = time() - filemtime( $lockdir );
if( $lifetime > 10 ) {
- @rmdir( $lockDir );
+ @rmdir( $lockdir );
}
else {
usleep( rand( 1000, 10000 ) );
}
}
-
- return $result;
}
+ while( $iterations > 0 && $iterations < 10 );
+
+ return $iterations == 0;
}
- $filepath = isset( $_GET[ 'filename' ] ) ? $_GET[ 'filename' ] : '';
- $fileinfo = pathinfo( $filepath );
- $basename = $fileinfo[ 'basename' ];
- $noSpaces = preg_replace( '/\s+/', '', $basename );
- $filename = preg_replace( '/[^\w\d\-_~,;\[\]\(\).]/', '', $noSpaces );
- $expiry = 24 * 60 * 60;
- $manager = new DownloadManager( $filename, $expiry );
+ /**
+ * 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 ) );
+ }
- $manager->process();
+ /**
+ * Creates a uniquely named lock directory name.
+ *
+ * @param string $filename The name of the file under contention.
+ *
+ * @return string A unique lock file reference for the given file name.
+ */
+ function create_lock_filename( $filename ) {
+ return $filename . '.lock';
+ }
?>
index.html
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
- script-src 'self' https://liberapay.com;
+ script-src 'self' https://liberapay.com 'nonce-x9F3pQ7Lm2';
connect-src 'self' https://liberapay.com;
img-src 'self' https://liberapay.com https://liberapay.com/assets/widgets;
class="download"></a>
<div class="liberapay">
- <script src="https://liberapay.com/Dave.Jarvis/widgets/receiving.js"></script>
+ <script nonce="x9F3pQ7Lm2" src="https://liberapay.com/Dave.Jarvis/widgets/receiving.js"></script>
<noscript>
<a href="https://liberapay.com/Dave.Jarvis/donate">
Delta 206 lines added, 165 lines removed, 41-line increase