Verzeichnisstruktur phpBB-3.3.15
- Veröffentlicht
- 28.08.2024
So funktioniert es
|
|
Auf das letzte Element klicken. Dies geht jeweils ein Schritt zurück |
Auf das Icon klicken, dies öffnet das Verzeichnis. Nochmal klicken schließt das Verzeichnis. |
|
|
(Beispiel Datei-Icons)
|
Auf das Icon klicken um den Quellcode anzuzeigen |
functions_download.php
001 <?php
002 /**
003 *
004 * This file is part of the phpBB Forum Software package.
005 *
006 * @copyright (c) phpBB Limited <https://www.phpbb.com>
007 * @license GNU General Public License, version 2 (GPL-2.0)
008 *
009 * For full copyright and license information, please see
010 * the docs/CREDITS.txt file.
011 *
012 */
013
014 /**
015 * @ignore
016 */
017 if (!defined('IN_PHPBB'))
018 {
019 exit;
020 }
021
022 /**
023 * A simplified function to deliver avatars
024 * The argument needs to be checked before calling this function.
025 */
026 function send_avatar_to_browser($file, $browser)
027 {
028 global $config, $phpbb_root_path;
029
030 $prefix = $config['avatar_salt'] . '_';
031 $image_dir = $config['avatar_path'];
032
033 // Adjust image_dir path (no trailing slash)
034 if (substr($image_dir, -1, 1) == '/' || substr($image_dir, -1, 1) == '\\')
035 {
036 $image_dir = substr($image_dir, 0, -1) . '/';
037 }
038 $image_dir = str_replace(array('../', '..\\', './', '.\\'), '', $image_dir);
039
040 if ($image_dir && ($image_dir[0] == '/' || $image_dir[0] == '\\'))
041 {
042 $image_dir = '';
043 }
044 $file_path = $phpbb_root_path . $image_dir . '/' . $prefix . $file;
045
046 if ((@file_exists($file_path) && @is_readable($file_path)) && !headers_sent())
047 {
048 header('Cache-Control: public');
049
050 $image_data = @getimagesize($file_path);
051 header('Content-Type: ' . image_type_to_mime_type($image_data[2]));
052
053 if ((strpos(strtolower($browser), 'msie') !== false) && !phpbb_is_greater_ie_version($browser, 7))
054 {
055 header('Content-Disposition: attachment; ' . header_filename($file));
056
057 if (strpos(strtolower($browser), 'msie 6.0') !== false)
058 {
059 header('Expires: ' . gmdate('D, d M Y H:i:s', time()) . ' GMT');
060 }
061 else
062 {
063 header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 31536000) . ' GMT');
064 }
065 }
066 else
067 {
068 header('Content-Disposition: inline; ' . header_filename($file));
069 header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 31536000) . ' GMT');
070 }
071
072 $size = @filesize($file_path);
073 if ($size)
074 {
075 header("Content-Length: $size");
076 }
077
078 if (@readfile($file_path) == false)
079 {
080 $fp = @fopen($file_path, 'rb');
081
082 if ($fp !== false)
083 {
084 while (!feof($fp))
085 {
086 echo fread($fp, 8192);
087 }
088 fclose($fp);
089 }
090 }
091
092 flush();
093 }
094 else
095 {
096 header('HTTP/1.0 404 Not Found');
097 }
098 }
099
100 /**
101 * Wraps an url into a simple html page. Used to display attachments in IE.
102 * this is a workaround for now; might be moved to template system later
103 * direct any complaints to 1 Microsoft Way, Redmond
104 */
105 function wrap_img_in_html($src, $title)
106 {
107 echo '<!DOCTYPE html>';
108 echo '<html>';
109 echo '<head>';
110 echo '<meta charset="utf-8">';
111 echo '<meta http-equiv="X-UA-Compatible" content="IE=edge">';
112 echo '<title>' . $title . '</title>';
113 echo '</head>';
114 echo '<body>';
115 echo '<div>';
116 echo '<img src="' . $src . '" alt="' . $title . '" />';
117 echo '</div>';
118 echo '</body>';
119 echo '</html>';
120 }
121
122 /**
123 * Send file to browser
124 */
125 function send_file_to_browser($attachment, $upload_dir, $category)
126 {
127 global $user, $db, $phpbb_dispatcher, $phpbb_root_path, $request;
128
129 $filename = $phpbb_root_path . $upload_dir . '/' . $attachment['physical_filename'];
130
131 if (!@file_exists($filename))
132 {
133 send_status_line(404, 'Not Found');
134 trigger_error('ERROR_NO_ATTACHMENT');
135 }
136
137 // Correct the mime type - we force application/octetstream for all files, except images
138 // Please do not change this, it is a security precaution
139 if ($category != ATTACHMENT_CATEGORY_IMAGE || strpos($attachment['mimetype'], 'image') !== 0)
140 {
141 $attachment['mimetype'] = (strpos(strtolower($user->browser), 'msie') !== false || strpos(strtolower($user->browser), 'opera') !== false) ? 'application/octetstream' : 'application/octet-stream';
142 }
143
144 if (@ob_get_length())
145 {
146 @ob_end_clean();
147 }
148
149 // Now send the File Contents to the Browser
150 $size = @filesize($filename);
151
152 /**
153 * Event to alter attachment before it is sent to browser.
154 *
155 * @event core.send_file_to_browser_before
156 * @var array attachment Attachment data
157 * @var string upload_dir Relative path of upload directory
158 * @var int category Attachment category
159 * @var string filename Path to file, including filename
160 * @var int size File size
161 * @since 3.1.11-RC1
162 */
163 $vars = array(
164 'attachment',
165 'upload_dir',
166 'category',
167 'filename',
168 'size',
169 );
170 extract($phpbb_dispatcher->trigger_event('core.send_file_to_browser_before', compact($vars)));
171
172 // To correctly display further errors we need to make sure we are using the correct headers for both (unsetting content-length may not work)
173
174 // Check if headers already sent or not able to get the file contents.
175 if (headers_sent() || !@file_exists($filename) || !@is_readable($filename))
176 {
177 // PHP track_errors setting On?
178 if (!empty($php_errormsg))
179 {
180 send_status_line(500, 'Internal Server Error');
181 trigger_error($user->lang['UNABLE_TO_DELIVER_FILE'] . '<br />' . sprintf($user->lang['TRACKED_PHP_ERROR'], $php_errormsg));
182 }
183
184 send_status_line(500, 'Internal Server Error');
185 trigger_error('UNABLE_TO_DELIVER_FILE');
186 }
187
188 // Make sure the database record for the filesize is correct
189 if ($size > 0 && $size != $attachment['filesize'] && strpos($attachment['physical_filename'], 'thumb_') === false)
190 {
191 // Update database record
192 $sql = 'UPDATE ' . ATTACHMENTS_TABLE . '
193 SET filesize = ' . (int) $size . '
194 WHERE attach_id = ' . (int) $attachment['attach_id'];
195 $db->sql_query($sql);
196 }
197
198 // Now the tricky part... let's dance
199 header('Cache-Control: private');
200
201 // Send out the Headers. Do not set Content-Disposition to inline please, it is a security measure for users using the Internet Explorer.
202 header('Content-Type: ' . $attachment['mimetype']);
203
204 if (phpbb_is_greater_ie_version($user->browser, 7))
205 {
206 header('X-Content-Type-Options: nosniff');
207 }
208
209 if (empty($user->browser) || ((strpos(strtolower($user->browser), 'msie') !== false) && !phpbb_is_greater_ie_version($user->browser, 7)))
210 {
211 header('Content-Disposition: attachment; ' . header_filename(html_entity_decode($attachment['real_filename'], ENT_COMPAT)));
212 if (empty($user->browser) || (strpos(strtolower($user->browser), 'msie 6.0') !== false))
213 {
214 header('Expires: ' . gmdate('D, d M Y H:i:s', time()) . ' GMT');
215 }
216 }
217 else
218 {
219 header('Content-Disposition: ' . ((strpos($attachment['mimetype'], 'image') === 0) ? 'inline' : 'attachment') . '; ' . header_filename(html_entity_decode($attachment['real_filename'], ENT_COMPAT)));
220 if (phpbb_is_greater_ie_version($user->browser, 7) && (strpos($attachment['mimetype'], 'image') !== 0))
221 {
222 header('X-Download-Options: noopen');
223 }
224 }
225
226 // Close the db connection before sending the file etc.
227 file_gc(false);
228
229 if (!set_modified_headers($attachment['filetime'], $user->browser))
230 {
231 // We make sure those have to be enabled manually by defining a constant
232 // because of the potential disclosure of full attachment path
233 // in case support for features is absent in the webserver software.
234 if (defined('PHPBB_ENABLE_X_ACCEL_REDIRECT') && PHPBB_ENABLE_X_ACCEL_REDIRECT)
235 {
236 // X-Accel-Redirect - http://wiki.nginx.org/XSendfile
237 header('X-Accel-Redirect: ' . $user->page['root_script_path'] . $upload_dir . '/' . $attachment['physical_filename']);
238 exit;
239 }
240 else if (defined('PHPBB_ENABLE_X_SENDFILE') && PHPBB_ENABLE_X_SENDFILE && !phpbb_http_byte_range($size))
241 {
242 // X-Sendfile - http://blog.lighttpd.net/articles/2006/07/02/x-sendfile
243 // Lighttpd's X-Sendfile does not support range requests as of 1.4.26
244 // and always requires an absolute path.
245 header('X-Sendfile: ' . __DIR__ . "/../$upload_dir/{$attachment['physical_filename']}");
246 exit;
247 }
248
249 if ($size)
250 {
251 header("Content-Length: $size");
252 }
253
254 // Try to deliver in chunks
255 @set_time_limit(0);
256
257 $fp = @fopen($filename, 'rb');
258
259 if ($fp !== false)
260 {
261 // Deliver file partially if requested
262 if ($range = phpbb_http_byte_range($size))
263 {
264 fseek($fp, $range['byte_pos_start']);
265
266 send_status_line(206, 'Partial Content');
267 header('Content-Range: bytes ' . $range['byte_pos_start'] . '-' . $range['byte_pos_end'] . '/' . $range['bytes_total']);
268 header('Content-Length: ' . $range['bytes_requested']);
269
270 // First read chunks
271 while (!feof($fp) && ftell($fp) < $range['byte_pos_end'] - 8192)
272 {
273 echo fread($fp, 8192);
274 }
275 // Then, read the remainder
276 echo fread($fp, $range['bytes_requested'] % 8192);
277 }
278 else
279 {
280 while (!feof($fp))
281 {
282 echo fread($fp, 8192);
283 }
284 }
285 fclose($fp);
286 }
287 else
288 {
289 @readfile($filename);
290 }
291
292 flush();
293 }
294
295 exit;
296 }
297
298 /**
299 * Get a browser friendly UTF-8 encoded filename
300 */
301 function header_filename($file)
302 {
303 global $request;
304
305 $user_agent = $request->header('User-Agent');
306
307 // There be dragons here.
308 // Not many follows the RFC...
309 if (strpos($user_agent, 'MSIE') !== false || strpos($user_agent, 'Konqueror') !== false)
310 {
311 return "filename=" . rawurlencode($file);
312 }
313
314 // follow the RFC for extended filename for the rest
315 return "filename*=UTF-8''" . rawurlencode($file);
316 }
317
318 /**
319 * Check if downloading item is allowed
320 */
321 function download_allowed()
322 {
323 global $config, $user, $db, $request;
324
325 if (!$config['secure_downloads'])
326 {
327 return true;
328 }
329
330 $url = html_entity_decode($request->header('Referer'), ENT_COMPAT);
331
332 if (!$url)
333 {
334 return ($config['secure_allow_empty_referer']) ? true : false;
335 }
336
337 // Split URL into domain and script part
338 $url = @parse_url($url);
339
340 if ($url === false)
341 {
342 return ($config['secure_allow_empty_referer']) ? true : false;
343 }
344
345 $hostname = $url['host'];
346 unset($url);
347
348 $allowed = ($config['secure_allow_deny']) ? false : true;
349 $iplist = array();
350
351 if (($ip_ary = @gethostbynamel($hostname)) !== false)
352 {
353 foreach ($ip_ary as $ip)
354 {
355 if ($ip)
356 {
357 $iplist[] = $ip;
358 }
359 }
360 }
361
362 // Check for own server...
363 $server_name = $user->host;
364
365 // Forcing server vars is the only way to specify/override the protocol
366 if ($config['force_server_vars'] || !$server_name)
367 {
368 $server_name = $config['server_name'];
369 }
370
371 if (preg_match('#^.*?' . preg_quote($server_name, '#') . '.*?$#i', $hostname))
372 {
373 $allowed = true;
374 }
375
376 // Get IP's and Hostnames
377 if (!$allowed)
378 {
379 $sql = 'SELECT site_ip, site_hostname, ip_exclude
380 FROM ' . SITELIST_TABLE;
381 $result = $db->sql_query($sql);
382
383 while ($row = $db->sql_fetchrow($result))
384 {
385 $site_ip = trim($row['site_ip']);
386 $site_hostname = trim($row['site_hostname']);
387
388 if ($site_ip)
389 {
390 foreach ($iplist as $ip)
391 {
392 if (preg_match('#^' . str_replace('\*', '.*?', preg_quote($site_ip, '#')) . '$#i', $ip))
393 {
394 if ($row['ip_exclude'])
395 {
396 $allowed = ($config['secure_allow_deny']) ? false : true;
397 break 2;
398 }
399 else
400 {
401 $allowed = ($config['secure_allow_deny']) ? true : false;
402 }
403 }
404 }
405 }
406
407 if ($site_hostname)
408 {
409 if (preg_match('#^' . str_replace('\*', '.*?', preg_quote($site_hostname, '#')) . '$#i', $hostname))
410 {
411 if ($row['ip_exclude'])
412 {
413 $allowed = ($config['secure_allow_deny']) ? false : true;
414 break;
415 }
416 else
417 {
418 $allowed = ($config['secure_allow_deny']) ? true : false;
419 }
420 }
421 }
422 }
423 $db->sql_freeresult($result);
424 }
425
426 return $allowed;
427 }
428
429 /**
430 * Check if the browser has the file already and set the appropriate headers-
431 * @returns false if a resend is in order.
432 */
433 function set_modified_headers($stamp, $browser)
434 {
435 global $request;
436
437 // let's see if we have to send the file at all
438 $last_load = $request->header('If-Modified-Since') ? strtotime(trim($request->header('If-Modified-Since'))) : false;
439
440 if (strpos(strtolower($browser), 'msie 6.0') === false && !phpbb_is_greater_ie_version($browser, 7))
441 {
442 if ($last_load !== false && $last_load >= $stamp)
443 {
444 send_status_line(304, 'Not Modified');
445 // seems that we need those too ... browsers
446 header('Cache-Control: private');
447 header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 31536000) . ' GMT');
448 return true;
449 }
450 else
451 {
452 header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $stamp) . ' GMT');
453 }
454 }
455 return false;
456 }
457
458 /**
459 * Garbage Collection
460 *
461 * @param bool $exit Whether to die or not.
462 *
463 * @return null
464 */
465 function file_gc($exit = true)
466 {
467 global $cache, $db;
468
469 if (!empty($cache))
470 {
471 $cache->unload();
472 }
473
474 $db->sql_close();
475
476 if ($exit)
477 {
478 exit;
479 }
480 }
481
482 /**
483 * HTTP range support (RFC 2616 Section 14.35)
484 *
485 * Allows browsers to request partial file content
486 * in case a download has been interrupted.
487 *
488 * @param int $filesize the size of the file in bytes we are about to deliver
489 *
490 * @return mixed false if the whole file has to be delivered
491 * associative array on success
492 */
493 function phpbb_http_byte_range($filesize)
494 {
495 // Only call find_range_request() once.
496 static $request_array;
497
498 if (!$filesize)
499 {
500 return false;
501 }
502
503 if (!isset($request_array))
504 {
505 $request_array = phpbb_find_range_request();
506 }
507
508 return (empty($request_array)) ? false : phpbb_parse_range_request($request_array, $filesize);
509 }
510
511 /**
512 * Searches for HTTP range request in request headers.
513 *
514 * @return mixed false if no request found
515 * array of strings containing the requested ranges otherwise
516 * e.g. array(0 => '0-0', 1 => '123-125')
517 */
518 function phpbb_find_range_request()
519 {
520 global $request;
521
522 $value = $request->header('Range');
523
524 // Make sure range request starts with "bytes="
525 if (strpos($value, 'bytes=') === 0)
526 {
527 // Strip leading 'bytes='
528 // Multiple ranges can be separated by a comma
529 return explode(',', substr($value, 6));
530 }
531
532 return false;
533 }
534
535 /**
536 * Analyses a range request array.
537 *
538 * A range request can contain multiple ranges,
539 * we however only handle the first request and
540 * only support requests from a given byte to the end of the file.
541 *
542 * @param array $request_array array of strings containing the requested ranges
543 * @param int $filesize the full size of the file in bytes that has been requested
544 *
545 * @return mixed false if the whole file has to be delivered
546 * associative array on success
547 * byte_pos_start the first byte position, can be passed to fseek()
548 * byte_pos_end the last byte position
549 * bytes_requested the number of bytes requested
550 * bytes_total the full size of the file
551 */
552 function phpbb_parse_range_request($request_array, $filesize)
553 {
554 $first_byte_pos = -1;
555 $last_byte_pos = -1;
556
557 // Go through all ranges
558 foreach ($request_array as $range_string)
559 {
560 $range = explode('-', trim($range_string));
561
562 // "-" is invalid, "0-0" however is valid and means the very first byte.
563 if (count($range) != 2 || $range[0] === '' && $range[1] === '')
564 {
565 continue;
566 }
567
568 // Substitute defaults
569 if ($range[0] === '')
570 {
571 $range[0] = 0;
572 }
573
574 if ($range[1] === '')
575 {
576 $range[1] = $filesize - 1;
577 }
578
579 if ($last_byte_pos >= 0 && $last_byte_pos + 1 != $range[0])
580 {
581 // We only support contiguous ranges, no multipart stuff :(
582 return false;
583 }
584
585 if ($range[1] && $range[1] < $range[0])
586 {
587 // The requested range contains 0 bytes.
588 continue;
589 }
590
591 // Return bytes from $range[0] to $range[1]
592 if ($first_byte_pos < 0)
593 {
594 $first_byte_pos = (int) $range[0];
595 }
596
597 $last_byte_pos = (int) $range[1];
598
599 if ($first_byte_pos >= $filesize)
600 {
601 // Requested range not satisfiable
602 return false;
603 }
604
605 // Adjust last-byte-pos if it is absent or greater than the content.
606 if ($range[1] === '' || $last_byte_pos >= $filesize)
607 {
608 $last_byte_pos = $filesize - 1;
609 }
610 }
611
612 if ($first_byte_pos < 0 || $last_byte_pos < 0)
613 {
614 return false;
615 }
616
617 return array(
618 'byte_pos_start' => $first_byte_pos,
619 'byte_pos_end' => $last_byte_pos,
620 'bytes_requested' => $last_byte_pos - $first_byte_pos + 1,
621 'bytes_total' => $filesize,
622 );
623 }
624
625 /**
626 * Increments the download count of all provided attachments
627 *
628 * @param \phpbb\db\driver\driver_interface $db The database object
629 * @param array|int $ids The attach_id of each attachment
630 *
631 * @return null
632 */
633 function phpbb_increment_downloads($db, $ids)
634 {
635 if (!is_array($ids))
636 {
637 $ids = array($ids);
638 }
639
640 $sql = 'UPDATE ' . ATTACHMENTS_TABLE . '
641 SET download_count = download_count + 1
642 WHERE ' . $db->sql_in_set('attach_id', $ids);
643 $db->sql_query($sql);
644 }
645
646 /**
647 * Handles authentication when downloading attachments from a post or topic
648 *
649 * @param \phpbb\db\driver\driver_interface $db The database object
650 * @param \phpbb\auth\auth $auth The authentication object
651 * @param int $topic_id The id of the topic that we are downloading from
652 *
653 * @return null
654 */
655 function phpbb_download_handle_forum_auth($db, $auth, $topic_id)
656 {
657 global $phpbb_container;
658
659 $sql_array = [
660 'SELECT' => 't.forum_id, t.topic_poster, t.topic_visibility, f.forum_name, f.forum_password, f.parent_id',
661 'FROM' => [
662 TOPICS_TABLE => 't',
663 FORUMS_TABLE => 'f',
664 ],
665 'WHERE' => 't.topic_id = ' . (int) $topic_id . '
666 AND t.forum_id = f.forum_id',
667 ];
668
669 $sql = $db->sql_build_query('SELECT', $sql_array);
670 $result = $db->sql_query($sql);
671 $row = $db->sql_fetchrow($result);
672 $db->sql_freeresult($result);
673
674 $phpbb_content_visibility = $phpbb_container->get('content.visibility');
675
676 if ($row && !$phpbb_content_visibility->is_visible('topic', $row['forum_id'], $row))
677 {
678 send_status_line(404, 'Not Found');
679 trigger_error('ERROR_NO_ATTACHMENT');
680 }
681 else if ($row && $auth->acl_get('u_download') && $auth->acl_get('f_download', $row['forum_id']))
682 {
683 if ($row['forum_password'])
684 {
685 // Do something else ... ?
686 login_forum_box($row);
687 }
688 }
689 else
690 {
691 send_status_line(403, 'Forbidden');
692 trigger_error('SORRY_AUTH_VIEW_ATTACH');
693 }
694 }
695
696 /**
697 * Handles authentication when downloading attachments from PMs
698 *
699 * @param \phpbb\db\driver\driver_interface $db The database object
700 * @param \phpbb\auth\auth $auth The authentication object
701 * @param int $user_id The user id
702 * @param int $msg_id The id of the PM that we are downloading from
703 *
704 * @return null
705 */
706 function phpbb_download_handle_pm_auth($db, $auth, $user_id, $msg_id)
707 {
708 global $phpbb_dispatcher;
709
710 if (!$auth->acl_get('u_pm_download'))
711 {
712 send_status_line(403, 'Forbidden');
713 trigger_error('SORRY_AUTH_VIEW_ATTACH');
714 }
715
716 $allowed = phpbb_download_check_pm_auth($db, $user_id, $msg_id);
717
718 /**
719 * Event to modify PM attachments download auth
720 *
721 * @event core.modify_pm_attach_download_auth
722 * @var bool allowed Whether the user is allowed to download from that PM or not
723 * @var int msg_id The id of the PM to download from
724 * @var int user_id The user id for auth check
725 * @since 3.1.11-RC1
726 */
727 $vars = array('allowed', 'msg_id', 'user_id');
728 extract($phpbb_dispatcher->trigger_event('core.modify_pm_attach_download_auth', compact($vars)));
729
730 if (!$allowed)
731 {
732 send_status_line(403, 'Forbidden');
733 trigger_error('ERROR_NO_ATTACHMENT');
734 }
735 }
736
737 /**
738 * Checks whether a user can download from a particular PM
739 *
740 * @param \phpbb\db\driver\driver_interface $db The database object
741 * @param int $user_id The user id
742 * @param int $msg_id The id of the PM that we are downloading from
743 *
744 * @return bool Whether the user is allowed to download from that PM or not
745 */
746 function phpbb_download_check_pm_auth($db, $user_id, $msg_id)
747 {
748 // Check if the attachment is within the users scope...
749 $sql = 'SELECT msg_id
750 FROM ' . PRIVMSGS_TO_TABLE . '
751 WHERE msg_id = ' . (int) $msg_id . '
752 AND (
753 user_id = ' . (int) $user_id . '
754 OR author_id = ' . (int) $user_id . '
755 )';
756 $result = $db->sql_query_limit($sql, 1);
757 $allowed = (bool) $db->sql_fetchfield('msg_id');
758 $db->sql_freeresult($result);
759
760 return $allowed;
761 }
762
763 /**
764 * Check if the browser is internet explorer version 7+
765 *
766 * @param string $user_agent User agent HTTP header
767 * @param int $version IE version to check against
768 *
769 * @return bool true if internet explorer version is greater than $version
770 */
771 function phpbb_is_greater_ie_version($user_agent, $version)
772 {
773 if (preg_match('/msie (\d+)/', strtolower($user_agent), $matches))
774 {
775 $ie_version = (int) $matches[1];
776 return ($ie_version > $version);
777 }
778 else
779 {
780 return false;
781 }
782 }
783