├── composer.json ├── LICENSE ├── .github └── workflows │ └── tests.yml ├── README.md └── src └── File.php /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mikehaertl/php-tmpfile", 3 | "description": "A convenience class for temporary files", 4 | "keywords": ["files"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Michael Härtl", 9 | "email": "haertl.mike@gmail.com" 10 | } 11 | ], 12 | "require-dev": { 13 | "php": ">=5.3.0", 14 | "phpunit/phpunit": ">4.0 <=9.4" 15 | }, 16 | "autoload": { 17 | "psr-4": { 18 | "mikehaertl\\tmp\\": "src/" 19 | } 20 | }, 21 | "autoload-dev": { 22 | "psr-4": { 23 | "tests\\": "tests" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Michael Härtl 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: pull_request 3 | jobs: 4 | phpunit: 5 | name: PHP ${{ matrix.php }} 6 | runs-on: ubuntu-latest 7 | strategy: 8 | matrix: 9 | php: 10 | - "5.3" 11 | - "5.4" 12 | - "5.5" 13 | - "5.6" 14 | - "7.0" 15 | - "7.1" 16 | - "7.2" 17 | - "7.3" 18 | - "7.4" 19 | - "8.0" 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v2 23 | 24 | - name: Install PHP 25 | uses: shivammathur/setup-php@v2 26 | with: 27 | php-version: ${{ matrix.php }} 28 | tools: composer:v2 29 | 30 | - name: Update composer 31 | run: composer self-update 32 | 33 | - name: Get composer cache directory 34 | id: composer-cache 35 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 36 | 37 | - name: Cache dependencies 38 | uses: actions/cache@v2 39 | with: 40 | path: ${{ steps.composer-cache.outputs.dir }} 41 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} 42 | restore-keys: ${{ runner.os }}-composer- 43 | 44 | - name: Install composer packages 45 | run: composer update --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi 46 | 47 | - name: Run phpunit 48 | run: vendor/bin/phpunit --color=always 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | php-tmpfile 2 | =========== 3 | 4 | [![GitHub Tests](https://github.com/mikehaertl/php-tmpfile/workflows/Tests/badge.svg)](https://github.com/mikehaertl/php-tmpfile/actions) 5 | [![Packagist Version](https://img.shields.io/packagist/v/mikehaertl/php-tmpfile?label=version)](https://packagist.org/packages/mikehaertl/php-tmpfile) 6 | [![Packagist Downloads](https://img.shields.io/packagist/dt/mikehaertl/php-tmpfile)](https://packagist.org/packages/mikehaertl/php-tmpfile) 7 | [![GitHub license](https://img.shields.io/github/license/mikehaertl/php-tmpfile)](https://github.com/mikehaertl/php-tmpfile/blob/master/LICENSE) 8 | 9 | A convenience class for temporary files. 10 | 11 | ## Features 12 | 13 | * Create temporary file with arbitrary content 14 | * Delete file after use (can be disabled) 15 | * Send file to client, either inline or with save dialog, optionally with custom HTTP headers 16 | * Save file locally 17 | 18 | ## Examples 19 | 20 | ```php 21 | send('home.html'); 28 | // ... with custom content type (autodetected otherwhise) 29 | $file->send('home.html', 'application/pdf'); 30 | // ... for inline display (download dialog otherwhise) 31 | $file->send('home.html', 'application/pdf', true); 32 | // ... with custom headers 33 | $file->send('home.html', 'application/pdf', true, [ 34 | 'X-Header' => 'Example', 35 | ]); 36 | 37 | // save to disk 38 | $file->saveAs('/dir/test.html'); 39 | 40 | // Access file name and directory 41 | echo $file->getFileName(); 42 | echo $file->getTempDir(); 43 | ``` 44 | 45 | If you want to keep the temporary file, e.g. for debugging, you can set the `$delete` property to false: 46 | 47 | ```php 48 | delete = false; 53 | ``` 54 | 55 | Default HTTP headers can also be added: 56 | ```php 57 | send('home.html'); 64 | ``` 65 | 66 | The `$ignoreUserAbort` option (on by default) mitigates an issue where the file 67 | was not deleted if the user closes the connection during a download. Try setting 68 | it to `false` if you experience unexpected behavior. 69 | -------------------------------------------------------------------------------- /src/File.php: -------------------------------------------------------------------------------- 1 | 10 | * @license http://www.opensource.org/licenses/MIT 11 | */ 12 | class File 13 | { 14 | const DEFAULT_CONTENT_TYPE = 'application/octet-stream'; 15 | 16 | /** 17 | * @var bool whether to delete the tmp file when it's no longer referenced 18 | * or when the request ends. Default is `true`. 19 | */ 20 | public $delete = true; 21 | 22 | /** 23 | * @var bool whether to ignore if a user closed the connection so that the 24 | * temporary file can still be cleaned up in that case. Default is `true`. 25 | * @see https://www.php.net/manual/en/function.ignore-user-abort.php 26 | */ 27 | public $ignoreUserAbort = true; 28 | 29 | /** 30 | * @var array the list of static default headers to send when `send()` is 31 | * called as key/value pairs. 32 | */ 33 | public static $defaultHeaders = array( 34 | 'Pragma' => 'public', 35 | 'Expires' => 0, 36 | 'Cache-Control' => 'must-revalidate, post-check=0, pre-check=0', 37 | 'Content-Transfer-Encoding' => 'binary', 38 | ); 39 | 40 | /** 41 | * @var string the name of this file 42 | */ 43 | protected $_fileName; 44 | 45 | /** 46 | * Constructor 47 | * 48 | * @param string $content the tmp file content 49 | * @param string|null $suffix the optional suffix for the tmp file 50 | * @param string|null $prefix the optional prefix for the tmp file. If null 51 | * 'php_tmpfile_' is used. 52 | * @param string|null $directory directory where the file should be 53 | * created. Autodetected if not provided. 54 | */ 55 | public function __construct($content, $suffix = null, $prefix = null, $directory = null) 56 | { 57 | if ($directory === null) { 58 | $directory = self::getTempDir(); 59 | } 60 | 61 | if ($prefix === null) { 62 | $prefix = 'php_tmpfile_'; 63 | } 64 | 65 | $this->_fileName = tempnam($directory,$prefix); 66 | if ($suffix !== null) { 67 | $newName = $this->_fileName . $suffix; 68 | rename($this->_fileName, $newName); 69 | $this->_fileName = $newName; 70 | } 71 | file_put_contents($this->_fileName, $content); 72 | } 73 | 74 | /** 75 | * Delete tmp file on shutdown if `$delete` is `true` 76 | */ 77 | public function __destruct() 78 | { 79 | if ($this->delete && file_exists($this->_fileName)) { 80 | unlink($this->_fileName); 81 | } 82 | } 83 | 84 | /** 85 | * Send tmp file to client, either inline or as download 86 | * 87 | * @param string|null $filename the filename to send. If empty, the file is 88 | * streamed inline. 89 | * @param string|null $contentType the Content-Type header to send. If 90 | * `null` the type is auto-detected and if that fails 91 | * 'application/octet-stream' is used. 92 | * @param bool $inline whether to force inline display of the file, even if 93 | * filename is present. 94 | * @param array $headers a list of additional HTTP headers to send in the 95 | * response as an array. The array keys are the header names like 96 | * 'Cache-Control' and the array values the header value strings to send. 97 | * Each array value can also be another array of strings if the same header 98 | * should be sent multiple times. This can also be used to override 99 | * automatically created headers like 'Expires' or 'Content-Length'. To suppress 100 | * automatically created headers, `false` can also be used as header value. 101 | */ 102 | public function send($filename = null, $contentType = null, $inline = false, $headers = array()) 103 | { 104 | $headers = array_merge(self::$defaultHeaders, $headers); 105 | 106 | if ($contentType !== null) { 107 | $headers['Content-Type'] = $contentType; 108 | } elseif (!isset($headers['Content-Type'])) { 109 | $contentType = @mime_content_type($this->_filename); 110 | if ($contentType === false) { 111 | $contentType = self::DEFAULT_CONTENT_TYPE; 112 | } 113 | $headers['Content-Type'] = $contentType; 114 | } 115 | 116 | if (!isset($headers['Content-Length'])) { 117 | // #11 Undefined index: HTTP_USER_AGENT 118 | $userAgent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''; 119 | 120 | // #84: Content-Length leads to "network connection was lost" on iOS 121 | $isIOS = preg_match('/i(phone|pad|pod)/i', $userAgent); 122 | if (!$isIOS) { 123 | $headers['Content-Length'] = filesize($this->_fileName); 124 | } 125 | } 126 | 127 | if (($filename !== null || $inline) && !isset($headers['Content-Disposition'])) { 128 | $disposition = $inline ? 'inline' : 'attachment'; 129 | $encodedFilename = rawurlencode($filename); 130 | $headers['Content-Disposition'] = "$disposition; " . 131 | "filename=\"$filename\"; " . 132 | "filename*=UTF-8''$encodedFilename"; 133 | } 134 | 135 | $this->sendHeaders($headers); 136 | 137 | // #28: File not cleaned up if user aborts connection during download 138 | if ($this->ignoreUserAbort) { 139 | ignore_user_abort(true); 140 | } 141 | 142 | readfile($this->_fileName); 143 | } 144 | 145 | /** 146 | * @param string $name the name to save the file as 147 | * @return bool whether the file could be saved 148 | */ 149 | public function saveAs($name) 150 | { 151 | return copy($this->_fileName, $name); 152 | } 153 | 154 | /** 155 | * @return string the full file name 156 | */ 157 | public function getFileName() 158 | { 159 | return $this->_fileName; 160 | } 161 | 162 | /** 163 | * @return string the path to the temp directory 164 | */ 165 | public static function getTempDir() 166 | { 167 | if (function_exists('sys_get_temp_dir')) { 168 | return sys_get_temp_dir(); 169 | } elseif ( 170 | ($tmp = getenv('TMP')) || 171 | ($tmp = getenv('TEMP')) || 172 | ($tmp = getenv('TMPDIR')) 173 | ) { 174 | return realpath($tmp); 175 | } else { 176 | return '/tmp'; 177 | } 178 | } 179 | 180 | /** 181 | * @return string the full file name 182 | */ 183 | public function __toString() 184 | { 185 | return $this->_fileName; 186 | } 187 | 188 | /** 189 | * Send the given list of headers 190 | * 191 | * @param array $headers the list of headers to send as key/value pairs. 192 | * Value can either be a string or an array of strings to send the same 193 | * header multiple times. 194 | */ 195 | protected function sendHeaders($headers) 196 | { 197 | foreach ($headers as $name => $value) { 198 | if ($value === false) { 199 | continue; 200 | } 201 | if (is_array($value)) { 202 | foreach ($value as $v) { 203 | header("$name: $v"); 204 | } 205 | } else { 206 | header("$name: $value"); 207 | } 208 | } 209 | } 210 | } 211 | --------------------------------------------------------------------------------