Rev 32 | Only display areas with differences | Regard whitespace | Details | Blame | Last modification | View Log | RSS feed
Rev 32 | Rev 52 | ||
---|---|---|---|
1 | <?php |
1 | <?php |
2 | 2 | ||
3 | /* |
3 | /* |
4 | * VtsBrowserDownload.class.php |
4 | * VtsBrowserDownload.class.php |
5 | * Copyright 2021 Daniel Marschall, ViaThinkSoft |
5 | * Copyright 2021 Daniel Marschall, ViaThinkSoft |
6 | * Revision: 2021-05-21 |
6 | * Revision: 2021-05-21 |
7 | * |
7 | * |
8 | * Licensed under the Apache License, Version 2.0 (the "License"); |
8 | * Licensed under the Apache License, Version 2.0 (the "License"); |
9 | * you may not use this file except in compliance with the License. |
9 | * you may not use this file except in compliance with the License. |
10 | * You may obtain a copy of the License at |
10 | * You may obtain a copy of the License at |
11 | * |
11 | * |
12 | * http://www.apache.org/licenses/LICENSE-2.0 |
12 | * http://www.apache.org/licenses/LICENSE-2.0 |
13 | * |
13 | * |
14 | * Unless required by applicable law or agreed to in writing, software |
14 | * Unless required by applicable law or agreed to in writing, software |
15 | * distributed under the License is distributed on an "AS IS" BASIS, |
15 | * distributed under the License is distributed on an "AS IS" BASIS, |
16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
17 | * See the License for the specific language governing permissions and |
17 | * See the License for the specific language governing permissions and |
18 | * limitations under the License. |
18 | * limitations under the License. |
19 | */ |
19 | */ |
20 | 20 | ||
21 | class VtsBrowserDownload { |
21 | class VtsBrowserDownload { |
22 | 22 | ||
23 | private static function wellKnownInlineFile($file_extension) { |
23 | private static function wellKnownInlineFile($file_extension) { |
24 | // Windows Firefox: Browser decided wheater to display or download by looking at the mime type (inline disposition with explicit filename works) |
24 | // Windows Firefox: Browser decided wheater to display or download by looking at the mime type (inline disposition with explicit filename works) |
25 | // Windows Chrome: Browser decided wheater to display or download by looking at the mime type (inline disposition with explicit filename DOES NOT WORK) |
25 | // Windows Chrome: Browser decided wheater to display or download by looking at the mime type (inline disposition with explicit filename DOES NOT WORK) |
26 | 26 | ||
27 | //$file_extension = strtolower($file_extension); |
27 | //$file_extension = strtolower($file_extension); |
28 | //$array_listen = array('txt', 'mp3', 'wav', 'mid', 'ogg', 'pdf', 'avi', 'mov', 'mp4', 'mpeg', 'mpg', 'swf', 'gif', 'jpg', 'jpeg', 'png'); |
28 | //$array_listen = array('txt', 'mp3', 'wav', 'mid', 'ogg', 'pdf', 'avi', 'mov', 'mp4', 'mpeg', 'mpg', 'swf', 'gif', 'jpg', 'jpeg', 'png'); |
29 | //return in_array($file_extension, $array_listen); |
29 | //return in_array($file_extension, $array_listen); |
30 | 30 | ||
31 | return true; |
31 | return true; |
32 | } |
32 | } |
33 | 33 | ||
34 | private static function getMimeType($file_extension) { |
34 | private static function getMimeType($file_extension) { |
35 | $file_extension = strtolower($file_extension); |
35 | $file_extension = strtolower($file_extension); |
36 | if (!class_exists('VtsFileTypeDetect')) { |
36 | if (!class_exists('VtsFileTypeDetect')) { |
37 | // https://github.com/danielmarschall/fileformats |
37 | // https://github.com/danielmarschall/fileformats |
38 | throw new Exception("Require 'fileformats' package"); |
38 | throw new Exception("Require 'fileformats' package"); |
39 | } |
39 | } |
40 | return VtsFileTypeDetect::getMimeType('dummy.'.$file_extension); |
40 | return VtsFileTypeDetect::getMimeType('dummy.'.$file_extension); |
41 | } |
41 | } |
42 | 42 | ||
43 | public static function output_file($file, $mime_type='', $inline_mode=2/*2=auto*/) { |
43 | public static function output_file($file, $mime_type='', $inline_mode=2/*2=auto*/) { |
44 | // Partitally taken from: |
44 | // Partitally taken from: |
45 | // - https://stackoverflow.com/a/13821992/488539 |
45 | // - https://stackoverflow.com/a/13821992/488539 |
46 | // - https://stackoverflow.com/a/32885706/488539 |
46 | // - https://stackoverflow.com/a/32885706/488539 |
47 | 47 | ||
48 | if (connection_status() != 0) return false; |
48 | if (connection_status() != 0) return false; |
49 | 49 | ||
50 | $file_extension = pathinfo($file, PATHINFO_EXTENSION); |
50 | $file_extension = pathinfo($file, PATHINFO_EXTENSION); |
51 | 51 | ||
52 | if(!is_readable($file)) throw new Exception('File not found or inaccessible!'); |
52 | if(!is_readable($file)) throw new Exception('File not found or inaccessible!'); |
53 | $size = filesize($file); |
53 | $size = filesize($file); |
54 | $name = rawurldecode(basename($file)); |
54 | $name = rawurldecode(basename($file)); |
55 | 55 | ||
56 | if ($mime_type == '') { |
56 | if ($mime_type == '') { |
57 | $mime_type = self::getMimeType($file_extension); |
57 | $mime_type = self::getMimeType($file_extension); |
58 | if (!$mime_type) $mime_type='application/force-download'; |
58 | if (!$mime_type) $mime_type='application/force-download'; |
59 | } |
59 | } |
60 | 60 | ||
61 | 61 | ||
62 | 62 | ||
63 | while (ob_get_level() > 0) @ob_end_clean(); |
63 | while (ob_get_level() > 0) @ob_end_clean(); |
64 | 64 | ||
65 | switch ($inline_mode) { |
65 | switch ($inline_mode) { |
66 | 66 | ||
67 | case 0: |
67 | case 0: |
68 | $disposition = 'attachment'; |
68 | $disposition = 'attachment'; |
69 | break; |
69 | break; |
70 | 70 | ||
71 | case 1: |
71 | case 1: |
72 | $disposition = 'inline'; |
72 | $disposition = 'inline'; |
73 | break; |
73 | break; |
74 | 74 | ||
75 | case 2: |
75 | case 2: |
76 | $disposition = self::wellKnownInlineFile($file_extension) ? 'inline' : 'attachment'; |
76 | $disposition = self::wellKnownInlineFile($file_extension) ? 'inline' : 'attachment'; |
77 | break; |
77 | break; |
78 | 78 | ||
79 | default: |
79 | default: |
80 | throw new Exception('Invalid value for inline_mode'); |
80 | throw new Exception('Invalid value for inline_mode'); |
81 | } |
81 | } |
82 | 82 | ||
83 | if(ini_get('zlib.output_compression')){ |
83 | if(ini_get('zlib.output_compression')){ |
84 | ini_set('zlib.output_compression', 'Off'); |
84 | ini_set('zlib.output_compression', 'Off'); |
85 | } |
85 | } |
86 | header('Content-Type: ' . $mime_type); |
86 | header('Content-Type: ' . $mime_type); |
87 | 87 | ||
88 | $ua = isset($_SERVER['HTTP_USER_AGENT']) ? strtoupper($_SERVER['HTTP_USER_AGENT']) : ''; |
88 | $ua = isset($_SERVER['HTTP_USER_AGENT']) ? strtoupper($_SERVER['HTTP_USER_AGENT']) : ''; |
89 | if (strstr($ua, 'MSIE')) { |
89 | if (strstr($ua, 'MSIE')) { |
90 | $name_msie = preg_replace('/\./', '%2e', $name, substr_count($name, '.') - 1); |
90 | $name_msie = preg_replace('/\./', '%2e', $name, substr_count($name, '.') - 1); |
91 | header('Content-Disposition: '.$disposition.';filename="'.$name_msie.'"'); |
91 | header('Content-Disposition: '.$disposition.';filename="'.$name_msie.'"'); |
92 | } else if (strstr($ua, 'FIREFOX')) { |
92 | } else if (strstr($ua, 'FIREFOX')) { |
93 | // TODO: Implement "encodeRFC5987ValueChars" described at https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent ? |
93 | // TODO: Implement "encodeRFC5987ValueChars" described at https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent ? |
- | 94 | $name_utf8 = mb_convert_encoding($name, 'UTF-8'); |
|
94 | header('Content-Disposition: '.$disposition.';filename*="UTF-8\'\''.utf8_encode($name).'"'); |
95 | header('Content-Disposition: '.$disposition.';filename*="UTF-8\'\''.$name_utf8.'"'); |
95 | } else { |
96 | } else { |
96 | // Note: There is possibly a bug in Google Chrome: https://stackoverflow.com/questions/61866508/chrome-ignores-content-disposition-filename |
97 | // Note: There is possibly a bug in Google Chrome: https://stackoverflow.com/questions/61866508/chrome-ignores-content-disposition-filename |
97 | header('Content-Disposition: '.$disposition.';filename="'.$name.'"'); |
98 | header('Content-Disposition: '.$disposition.';filename="'.$name.'"'); |
98 | } |
99 | } |
99 | 100 | ||
100 | header('Content-Transfer-Encoding: binary'); |
101 | header('Content-Transfer-Encoding: binary'); |
101 | header('Accept-Ranges: bytes'); |
102 | header('Accept-Ranges: bytes'); |
102 | header('Cache-Control: public'); |
103 | header('Cache-Control: public'); |
103 | 104 | ||
104 | if (isset($_SERVER['HTTP_RANGE'])) { |
105 | if (isset($_SERVER['HTTP_RANGE'])) { |
105 | list($a, $range) = explode("=",$_SERVER['HTTP_RANGE'],2); |
106 | list($a, $range) = explode("=",$_SERVER['HTTP_RANGE'],2); |
106 | list($range) = explode(",",$range,2); |
107 | list($range) = explode(",",$range,2); |
107 | list($range, $range_end) = explode("-", $range); |
108 | list($range, $range_end) = explode("-", $range); |
108 | $range=intval($range); |
109 | $range=intval($range); |
109 | if(!$range_end) { |
110 | if(!$range_end) { |
110 | $range_end=$size-1; |
111 | $range_end=$size-1; |
111 | } else { |
112 | } else { |
112 | $range_end=intval($range_end); |
113 | $range_end=intval($range_end); |
113 | } |
114 | } |
114 | 115 | ||
115 | $new_length = $range_end-$range+1; |
116 | $new_length = $range_end-$range+1; |
116 | http_response_code(206); // 206 Partial Content |
117 | http_response_code(206); // 206 Partial Content |
117 | header("Content-Length: $new_length"); |
118 | header("Content-Length: $new_length"); |
118 | header("Content-Range: bytes $range-$range_end/$size"); |
119 | header("Content-Range: bytes $range-$range_end/$size"); |
119 | } else { |
120 | } else { |
120 | $range = 0; |
121 | $range = 0; |
121 | $etag = md5_file($file); |
122 | $etag = md5_file($file); |
122 | header("Etag: $etag"); |
123 | header("Etag: $etag"); |
123 | if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && (trim($_SERVER['HTTP_IF_NONE_MATCH']) == $etag)) { |
124 | if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && (trim($_SERVER['HTTP_IF_NONE_MATCH']) == $etag)) { |
124 | http_response_code(304); // 304 Not Modified |
125 | http_response_code(304); // 304 Not Modified |
125 | return true; |
126 | return true; |
126 | } |
127 | } |
127 | 128 | ||
128 | $new_length=$size; |
129 | $new_length=$size; |
129 | header("Content-Length: ".$size); |
130 | header("Content-Length: ".$size); |
130 | header('Content-MD5: '.$etag); // RFC 2616 clause 14.15 |
131 | header('Content-MD5: '.$etag); // RFC 2616 clause 14.15 |
131 | } |
132 | } |
132 | 133 | ||
133 | set_time_limit(0); |
134 | set_time_limit(0); |
134 | 135 | ||
135 | $chunksize = 1*(1024*1024); |
136 | $chunksize = 1*(1024*1024); |
136 | $bytes_send = 0; |
137 | $bytes_send = 0; |
137 | if ($file = fopen($file, 'r')) { |
138 | if ($file = fopen($file, 'r')) { |
138 | if(isset($_SERVER['HTTP_RANGE'])) |
139 | if(isset($_SERVER['HTTP_RANGE'])) |
139 | fseek($file, $range); |
140 | fseek($file, $range); |
140 | 141 | ||
141 | while(!feof($file) && |
142 | while(!feof($file) && |
142 | (!connection_aborted()) && // connection_status() == 0 |
143 | (!connection_aborted()) && // connection_status() == 0 |
143 | ($bytes_send<$new_length)) |
144 | ($bytes_send<$new_length)) |
144 | { |
145 | { |
145 | $buffer = fread($file, $chunksize); |
146 | $buffer = fread($file, $chunksize); |
146 | echo($buffer); |
147 | echo($buffer); |
147 | flush(); |
148 | flush(); |
148 | $bytes_send += strlen($buffer); |
149 | $bytes_send += strlen($buffer); |
149 | } |
150 | } |
150 | fclose($file); |
151 | fclose($file); |
151 | } else { |
152 | } else { |
152 | throw new Exception("Cannot open file $file"); |
153 | throw new Exception("Cannot open file $file"); |
153 | } |
154 | } |
154 | return((connection_status() == 0) and !connection_aborted()); |
155 | return((connection_status() == 0) and !connection_aborted()); |
155 | } |
156 | } |
156 | 157 | ||
157 | } |
158 | } |
158 | 159 |