Skip to content

Instantly share code, notes, and snippets.

@aznoisib
Forked from ssut/attachment.php
Created August 2, 2019 09:07
Show Gist options
  • Save aznoisib/20c541c8e6b855cb7664cd60758fa80b to your computer and use it in GitHub Desktop.
Save aznoisib/20c541c8e6b855cb7664cd60758fa80b to your computer and use it in GitHub Desktop.
UTF-8 file download function, support remote file passing(proxy) with http range header
<?php
/**
* PHP File download function.
* Version 1.4
*
* Copyright (c) 2014 성기진 Kijin Sung
* Modified by ssut
*
* License: MIT License (a.k.a. X11 License)
* http://www.olis.or.kr/ossw/license/license/detail.do?lid=1006
*
* This function supports following features:
*
* 1. Do not break UTF-8 character. (RFC2231/5987 Standards and considering browsers)
* 2. Remove special character when include characters that cannot used by OS.
* 3. Add Cache-Control and Expires header when you want to enable cache.
* 4. Fix the download error when use cache and IE <= 8.
* 5. Resumable download. (Auto detect Range header and auto generate Accept-Ranges header)
* 6. Fix the memory leak when download large file.
* 7. Support speed limitation
*
* How to : send_attachment('filename that provide to client', 'file path or remote url', [period of cache], [speed limit]);
*
* below, download 'foo.jpg' file from server to client named '사진.jpg'.
* send_attachment('사진.jpg', '/srv/www/files/uploads/foo.jpg');
*
* below, download 'bar.mp3' file from server with 24-hour caching and 300KB/s limit.
* send_attachment('bar.mp3', '/srv/www/files/uploads/bar.mp3', 60 * 60 * 24, 300);
*
* Return : true when successfully sent else false.
*
* Caution : 1. please execute 'exit' when end the transfer. (file can be broken)
* 2. don't ensure that php version is very low(< 5.1) or not UTF-8 environment.
* 3. speed limitation is very dangerous when you using FastCGI/FPM.
* recommend to use web server's speed limitation.
* 4. some android versions does not support UTF-8 encoding.
*/
function send_attachment($filename, $server_filename, $expires = 0, $speed_limit = 0) {
$remote = false;
// check filename
if (strpos($server_filename, 'http') === false) {
if (!file_exists($server_filename) || !is_readable($server_filename)) {
return false;
}
if (($filesize = filesize($server_filename)) == 0) {
return false;
}
if (($fp = @fopen($server_filename, 'rb')) === false) {
return false;
}
} else {
$remote = true;
$handle = curl_init($server_filename);
curl_setopt($handle, CURLOPT_RETURNTRANSFER, true);
curl_setopt($handle, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($handle, CURLOPT_HEADER, true);
curl_setopt($handle, CURLOPT_NOBODY, true);
$response = curl_exec($handle);
$http_code = curl_getinfo($handle, CURLINFO_HTTP_CODE);
if ($http_code == 404) {
return false;
}
$filesize = curl_getinfo($handle, CURLINFO_CONTENT_LENGTH_DOWNLOAD);
curl_close($handle);
$ch = curl_init($server_filename);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
}
$pass_remote = function($ch, $chunk) {
echo $chunk; flush();
return strlen($chunk);
};
// replace special characters
$illegal = array('\\', '/', '<', '>', '{', '}', ':', ';', '|', '"', '~', '`', '@', '#', '$', '%', '^', '&', '*', '?');
$replace = array('', '', '(', ')', '(', ')', '_', ',', '_', '', '_', '\'', '_', '_', '_', '_', '_', '_', '', '');
$filename = str_replace($illegal, $replace, $filename);
$filename = preg_replace('/([\\x00-\\x1f\\x7f\\xff]+)/', '', $filename);
// replace special spaces to normal spaces(0x20).
$filename = trim(preg_replace('/[\\pZ\\pC]+/u', ' ', $filename));
// remove duplicates or dots.
$filename = trim($filename, ' .-_');
$filename = preg_replace('/__+/', '_', $filename);
if ($filename === '') {
return false;
}
// get User-Agent from browser
$ua = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
$old_ie = (bool)preg_match('#MSIE [3-8]\.#', $ua);
// add filename to header when filename only includes normal characters.
if (preg_match('/^[a-zA-Z0-9_.-]+$/', $filename)) {
$header = 'filename="' . $filename . '"';
}
// < IE 9 or < FF 5
elseif ($old_ie || preg_match('#Firefox/(\d+)\.#', $ua, $matches) && $matches[1] < 5) {
$header = 'filename="' . rawurlencode($filename) . '"';
}
// < Chrome 11
elseif (preg_match('#Chrome/(\d+)\.#', $ua, $matches) && $matches[1] < 11) {
$header = 'filename=' . $filename;
}
// < Safari 6
elseif (preg_match('#Safari/(\d+)\.#', $ua, $matches) && $matches[1] < 6) {
$header = 'filename=' . $filename;
}
// Android
elseif (preg_match('#Android #', $ua, $matches)) {
$header = 'filename="' . $filename . '"';
}
// other browsers assume that validate RFC/2231/5987 standards
// but, add old style filename information for special circumstances
else {
$header = "filename*=UTF-8''" . rawurlencode($filename) . '; filename="' . rawurlencode($filename) . '"';
}
// cache is disallowed by client
if (!$expires) {
// Cannot use no-cache and pragma header when use old IE versions(<= 8) and SSL.
if ($old_ie) {
header('Cache-Control: private, must-revalidate, post-check=0, pre-check=0');
header('Expires: Sat, 01 Jan 2000 00:00:00 GMT');
}
else {
header('Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0');
header('Expires: Sat, 01 Jan 2000 00:00:00 GMT');
}
}
// cache is allowed by client
else {
header('Cache-Control: max-age=' . (int)$expires);
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + (int)$expires) . ' GMT');
}
// process range header for resume download
if (isset($_SERVER['HTTP_RANGE']) && preg_match('/^bytes=(\d+)-/', $_SERVER['HTTP_RANGE'], $matches)) {
$range_start = $matches[1];
if ($range_start < 0 || $range_start > $filesize) {
header('HTTP/1.1 416 Requested Range Not Satisfiable');
return false;
}
header('HTTP/1.1 206 Partial Content');
header('Content-Range: bytes ' . $range_start . '-' . ($filesize - 1) . '/' . $filesize);
header('Content-Length: ' . ($filesize - $range_start));
if ($remote) {
curl_setopt($ch, CURLOPT_WRITEFUNCTION, $pass_remote);
curl_setopt($ch, CURLOPT_RANGE, $range_start . '-' . ($filesize - 1));
}
} else {
$range_start = 0;
header('Content-Length: ' . $filesize);
if ($remote) {
curl_setopt($ch, CURLOPT_WRITEFUNCTION, $pass_remote);
curl_setopt($ch, CURLOPT_RANGE, '0-' . $filesize);
}
}
// send other headers.
header('Accept-Ranges: bytes');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; ' . $header);
// clear output buffer.
// (blocks file broken and decrease memory usage)
while (ob_get_level()) {
ob_end_clean();
}
// send a file each 64KB and clear output buffer.
// sometimes occurs memory leak when use readfile() function.
$block_size = 16 * 1024;
$speed_sleep = $speed_limit > 0 ? round(($block_size / $speed_limit / 1024) * 1000000) : 0;
$buffer = '';
if ($range_start > 0 && !$remote) {
fseek($fp, $range_start);
$alignment = (ceil($range_start / $block_size) * $block_size) - $range_start;
if ($alignment > 0) {
$buffer = fread($fp, $alignment);
echo $buffer; unset($buffer); flush();
}
}
while (!feof($fp) && !$remote) {
$buffer = fread($fp, $block_size);
echo $buffer; unset($buffer); flush();
usleep($speed_sleep);
}
if ($remote && $ch) {
curl_exec($ch);
}
if (!$remote) {
fclose($fp);
} else {
curl_close($ch);
}
// true when successfully sent.
return true;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment