Verzeichnisstruktur phpBB-3.3.16


Veröffentlicht
27.04.2026

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.
Auf den Verzeichnisnamen klicken, dies zeigt nur das Verzeichnis mit Inhalt an

(Beispiel Datei-Icons)

Auf das Icon klicken um den Quellcode anzuzeigen

functions_download.php

Zuletzt modifiziert: 01.05.2026, 11:25 - Dateigröße: 20.77 KiB


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      // Send restrictive CSP for file served to browser
205      header("Content-Security-Policy: default-src 'none'; style-src 'self' 'unsafe-inline'; img-src 'self'; script-src 'none'; object-src 'none'; frame-src 'none';");
206   
207      if (phpbb_is_greater_ie_version($user->browser, 7))
208      {
209          header('X-Content-Type-Options: nosniff');
210      }
211   
212      if (empty($user->browser) || ((strpos(strtolower($user->browser), 'msie') !== false) && !phpbb_is_greater_ie_version($user->browser, 7)))
213      {
214          header('Content-Disposition: attachment; ' . header_filename(html_entity_decode($attachment['real_filename'], ENT_COMPAT)));
215          if (empty($user->browser) || (strpos(strtolower($user->browser), 'msie 6.0') !== false))
216          {
217              header('Expires: ' . gmdate('D, d M Y H:i:s', time()) . ' GMT');
218          }
219      }
220      else
221      {
222          $sec_fetch_dest = $request->header('Sec-Fetch-Dest');
223   
224          // Only set inline if category is set to image, mimetype says it's an image, and browser either sends no Sec-Fetch-Dest header or explicitly marks the request as an image
225          if ($category == ATTACHMENT_CATEGORY_IMAGE && phpbb_allow_serve_inline($attachment['mimetype'], $sec_fetch_dest))
226          {
227              $disposition = 'inline';
228          }
229          else
230          {
231              $disposition = 'attachment';
232          }
233          header('Content-Disposition: ' . $disposition . '; ' . header_filename(html_entity_decode($attachment['real_filename'], ENT_COMPAT)));
234          if (phpbb_is_greater_ie_version($user->browser, 7) && (strpos($attachment['mimetype'], 'image') !== 0))
235          {
236              header('X-Download-Options: noopen');
237          }
238      }
239   
240      // Close the db connection before sending the file etc.
241      file_gc(false);
242   
243      if (!set_modified_headers($attachment['filetime'], $user->browser))
244      {
245          // We make sure those have to be enabled manually by defining a constant
246          // because of the potential disclosure of full attachment path
247          // in case support for features is absent in the webserver software.
248          if (defined('PHPBB_ENABLE_X_ACCEL_REDIRECT') && PHPBB_ENABLE_X_ACCEL_REDIRECT)
249          {
250              // X-Accel-Redirect - http://wiki.nginx.org/XSendfile
251              header('X-Accel-Redirect: ' . $user->page['root_script_path'] . $upload_dir . '/' . $attachment['physical_filename']);
252              exit;
253          }
254          else if (defined('PHPBB_ENABLE_X_SENDFILE') && PHPBB_ENABLE_X_SENDFILE && !phpbb_http_byte_range($size))
255          {
256              // X-Sendfile - http://blog.lighttpd.net/articles/2006/07/02/x-sendfile
257              // Lighttpd's X-Sendfile does not support range requests as of 1.4.26
258              // and always requires an absolute path.
259              header('X-Sendfile: ' . __DIR__ . "/../$upload_dir/{$attachment['physical_filename']}");
260              exit;
261          }
262   
263          if ($size)
264          {
265              header("Content-Length: $size");
266          }
267   
268          // Try to deliver in chunks
269          @set_time_limit(0);
270   
271          $fp = @fopen($filename, 'rb');
272   
273          if ($fp !== false)
274          {
275              // Deliver file partially if requested
276              if ($range = phpbb_http_byte_range($size))
277              {
278                  fseek($fp, $range['byte_pos_start']);
279   
280                  send_status_line(206, 'Partial Content');
281                  header('Content-Range: bytes ' . $range['byte_pos_start'] . '-' . $range['byte_pos_end'] . '/' . $range['bytes_total']);
282                  header('Content-Length: ' . $range['bytes_requested']);
283   
284                  // Read until we reach the end of the requested range
285                  $bytes_to_read = $range['bytes_requested'];
286                  while ($bytes_to_read > 0 && !feof($fp))
287                  {
288                      $chunk = ($bytes_to_read > 8192) ? 8192 : $bytes_to_read;
289                      echo fread($fp, $chunk);
290                      $bytes_to_read -= $chunk;
291                  }
292              }
293              else
294              {
295                  while (!feof($fp))
296                  {
297                      echo fread($fp, 8192);
298                  }
299              }
300              fclose($fp);
301          }
302          else
303          {
304              @readfile($filename);
305          }
306   
307          flush();
308      }
309   
310      exit;
311  }
312   
313  /**
314  * Get a browser friendly UTF-8 encoded filename
315  */
316  function header_filename($file)
317  {
318      global $request;
319   
320      $user_agent = $request->header('User-Agent');
321   
322      // There be dragons here.
323      // Not many follows the RFC...
324      if (strpos($user_agent, 'MSIE') !== false || strpos($user_agent, 'Konqueror') !== false)
325      {
326          return "filename=" . rawurlencode($file);
327      }
328   
329      // follow the RFC for extended filename for the rest
330      return "filename*=UTF-8''" . rawurlencode($file);
331  }
332   
333  /**
334  * Check if downloading item is allowed
335  */
336  function download_allowed()
337  {
338      global $config, $user, $db, $request;
339   
340      if (!$config['secure_downloads'])
341      {
342          return true;
343      }
344   
345      $url = html_entity_decode($request->header('Referer'), ENT_COMPAT);
346   
347      if (!$url)
348      {
349          return ($config['secure_allow_empty_referer']) ? true : false;
350      }
351   
352      // Split URL into domain and script part
353      $url = @parse_url($url);
354   
355      if ($url === false)
356      {
357          return ($config['secure_allow_empty_referer']) ? true : false;
358      }
359   
360      $hostname = $url['host'];
361      unset($url);
362   
363      $allowed = ($config['secure_allow_deny']) ? false : true;
364      $iplist = array();
365   
366      if (($ip_ary = @gethostbynamel($hostname)) !== false)
367      {
368          foreach ($ip_ary as $ip)
369          {
370              if ($ip)
371              {
372                  $iplist[] = $ip;
373              }
374          }
375      }
376   
377      // Check for own server...
378      $server_name = $user->host;
379   
380      // Forcing server vars is the only way to specify/override the protocol
381      if ($config['force_server_vars'] || !$server_name)
382      {
383          $server_name = $config['server_name'];
384      }
385   
386      if (preg_match('#^.*?' . preg_quote($server_name, '#') . '.*?$#i', $hostname))
387      {
388          $allowed = true;
389      }
390   
391      // Get IP's and Hostnames
392      if (!$allowed)
393      {
394          $sql = 'SELECT site_ip, site_hostname, ip_exclude
395              FROM ' . SITELIST_TABLE;
396          $result = $db->sql_query($sql);
397   
398          while ($row = $db->sql_fetchrow($result))
399          {
400              $site_ip = trim($row['site_ip']);
401              $site_hostname = trim($row['site_hostname']);
402   
403              if ($site_ip)
404              {
405                  foreach ($iplist as $ip)
406                  {
407                      if (preg_match('#^' . str_replace('\*', '.*?', preg_quote($site_ip, '#')) . '$#i', $ip))
408                      {
409                          if ($row['ip_exclude'])
410                          {
411                              $allowed = ($config['secure_allow_deny']) ? false : true;
412                              break 2;
413                          }
414                          else
415                          {
416                              $allowed = ($config['secure_allow_deny']) ? true : false;
417                          }
418                      }
419                  }
420              }
421   
422              if ($site_hostname)
423              {
424                  if (preg_match('#^' . str_replace('\*', '.*?', preg_quote($site_hostname, '#')) . '$#i', $hostname))
425                  {
426                      if ($row['ip_exclude'])
427                      {
428                          $allowed = ($config['secure_allow_deny']) ? false : true;
429                          break;
430                      }
431                      else
432                      {
433                          $allowed = ($config['secure_allow_deny']) ? true : false;
434                      }
435                  }
436              }
437          }
438          $db->sql_freeresult($result);
439      }
440   
441      return $allowed;
442  }
443   
444  /**
445  * Check if the browser has the file already and set the appropriate headers-
446  * @returns false if a resend is in order.
447  */
448  function set_modified_headers($stamp, $browser)
449  {
450      global $request;
451   
452      // let's see if we have to send the file at all
453      $last_load     =  $request->header('If-Modified-Since') ? strtotime(trim($request->header('If-Modified-Since'))) : false;
454   
455      if (strpos(strtolower($browser), 'msie 6.0') === false && !phpbb_is_greater_ie_version($browser, 7))
456      {
457          if ($last_load !== false && $last_load >= $stamp)
458          {
459              send_status_line(304, 'Not Modified');
460              // seems that we need those too ... browsers
461              header('Cache-Control: private');
462              header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 31536000) . ' GMT');
463              return true;
464          }
465          else
466          {
467              header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $stamp) . ' GMT');
468          }
469      }
470      return false;
471  }
472   
473  /**
474  * Garbage Collection
475  *
476  * @param bool $exit        Whether to die or not.
477  *
478  * @return null
479  */
480  function file_gc($exit = true)
481  {
482      global $cache, $db;
483   
484      if (!empty($cache))
485      {
486          $cache->unload();
487      }
488   
489      $db->sql_close();
490   
491      if ($exit)
492      {
493          exit;
494      }
495  }
496   
497  /**
498  * HTTP range support (RFC 2616 Section 14.35)
499  *
500  * Allows browsers to request partial file content
501  * in case a download has been interrupted.
502  *
503  * @param int $filesize        the size of the file in bytes we are about to deliver
504  *
505  * @return mixed        false if the whole file has to be delivered
506  *                    associative array on success
507  */
508  function phpbb_http_byte_range($filesize)
509  {
510      // Only call find_range_request() once.
511      static $request_array;
512   
513      if (!$filesize)
514      {
515          return false;
516      }
517   
518      if (!isset($request_array))
519      {
520          $request_array = phpbb_find_range_request();
521      }
522   
523      return (empty($request_array)) ? false : phpbb_parse_range_request($request_array, $filesize);
524  }
525   
526  /**
527  * Searches for HTTP range request in request headers.
528  *
529  * @return mixed        false if no request found
530  *                    array of strings containing the requested ranges otherwise
531  *                    e.g. array(0 => '0-0', 1 => '123-125')
532  */
533  function phpbb_find_range_request()
534  {
535      global $request;
536   
537      $value = $request->header('Range');
538   
539      // Make sure range request starts with "bytes="
540      if (strpos($value, 'bytes=') === 0)
541      {
542          // Strip leading 'bytes='
543          // Multiple ranges can be separated by a comma
544          return explode(',', substr($value, 6));
545      }
546   
547      return false;
548  }
549   
550  /**
551  * Analyses a range request array.
552  *
553  * A range request can contain multiple ranges,
554  * we however only handle the first request and
555  * only support requests from a given byte to the end of the file.
556  *
557  * @param array    $request_array    array of strings containing the requested ranges
558  * @param int    $filesize        the full size of the file in bytes that has been requested
559  *
560  * @return mixed        false if the whole file has to be delivered
561  *                    associative array on success
562  *                        byte_pos_start        the first byte position, can be passed to fseek()
563  *                        byte_pos_end        the last byte position
564  *                        bytes_requested        the number of bytes requested
565  *                        bytes_total            the full size of the file
566  */
567  function phpbb_parse_range_request($request_array, $filesize)
568  {
569      $first_byte_pos    = -1;
570      $last_byte_pos    = -1;
571   
572      // Go through all ranges
573      foreach ($request_array as $range_string)
574      {
575          $range = explode('-', trim($range_string));
576   
577          // "-" is invalid, "0-0" however is valid and means the very first byte.
578          if (count($range) != 2 || $range[0] === '' && $range[1] === '')
579          {
580              continue;
581          }
582   
583          // Substitute defaults
584          if ($range[0] === '')
585          {
586              $range[0] = 0;
587          }
588   
589          if ($range[1] === '')
590          {
591              $range[1] = $filesize - 1;
592          }
593   
594          if ($last_byte_pos >= 0 && $last_byte_pos + 1 != $range[0])
595          {
596              // We only support contiguous ranges, no multipart stuff :(
597              return false;
598          }
599   
600          if ($range[1] && $range[1] < $range[0])
601          {
602              // The requested range contains 0 bytes.
603              continue;
604          }
605   
606          // Return bytes from $range[0] to $range[1]
607          if ($first_byte_pos < 0)
608          {
609              $first_byte_pos    = (int) $range[0];
610          }
611   
612          $last_byte_pos    = (int) $range[1];
613   
614          if ($first_byte_pos >= $filesize)
615          {
616              // Requested range not satisfiable
617              return false;
618          }
619   
620          // Adjust last-byte-pos if it is absent or greater than the content.
621          if ($range[1] === '' || $last_byte_pos >= $filesize)
622          {
623              $last_byte_pos = $filesize - 1;
624          }
625      }
626   
627      if ($first_byte_pos < 0 || $last_byte_pos < 0)
628      {
629          return false;
630      }
631   
632      return array(
633          'byte_pos_start'    => $first_byte_pos,
634          'byte_pos_end'        => $last_byte_pos,
635          'bytes_requested'    => $last_byte_pos - $first_byte_pos + 1,
636          'bytes_total'        => $filesize,
637      );
638  }
639   
640  /**
641  * Increments the download count of all provided attachments
642  *
643  * @param \phpbb\db\driver\driver_interface $db The database object
644  * @param array|int $ids The attach_id of each attachment
645  *
646  * @return null
647  */
648  function phpbb_increment_downloads($db, $ids)
649  {
650      if (!is_array($ids))
651      {
652          $ids = array($ids);
653      }
654   
655      $sql = 'UPDATE ' . ATTACHMENTS_TABLE . '
656          SET download_count = download_count + 1
657          WHERE ' . $db->sql_in_set('attach_id', $ids);
658      $db->sql_query($sql);
659  }
660   
661  /**
662  * Handles authentication when downloading attachments from a post or topic
663  *
664  * @param \phpbb\db\driver\driver_interface $db The database object
665  * @param \phpbb\auth\auth $auth The authentication object
666  * @param int $topic_id The id of the topic that we are downloading from
667  *
668  * @return null
669  */
670  function phpbb_download_handle_forum_auth($db, $auth, $topic_id)
671  {
672      global $phpbb_container;
673   
674      $sql_array = [
675          'SELECT'    => 't.forum_id, t.topic_poster, t.topic_visibility, f.forum_name, f.forum_password, f.parent_id',
676          'FROM'        => [
677              TOPICS_TABLE => 't',
678              FORUMS_TABLE => 'f',
679          ],
680          'WHERE'        => 't.topic_id = ' . (int) $topic_id . '
681              AND t.forum_id = f.forum_id',
682      ];
683   
684      $sql = $db->sql_build_query('SELECT', $sql_array);
685      $result = $db->sql_query($sql);
686      $row = $db->sql_fetchrow($result);
687      $db->sql_freeresult($result);
688   
689      $phpbb_content_visibility = $phpbb_container->get('content.visibility');
690   
691      if ($row && !$phpbb_content_visibility->is_visible('topic', $row['forum_id'], $row))
692      {
693          send_status_line(404, 'Not Found');
694          trigger_error('ERROR_NO_ATTACHMENT');
695      }
696      else if ($row && $auth->acl_get('u_download') && $auth->acl_get('f_download', $row['forum_id']))
697      {
698          if ($row['forum_password'])
699          {
700              // Do something else ... ?
701              login_forum_box($row);
702          }
703      }
704      else
705      {
706          send_status_line(403, 'Forbidden');
707          trigger_error('SORRY_AUTH_VIEW_ATTACH');
708      }
709  }
710   
711  /**
712  * Handles authentication when downloading attachments from PMs
713  *
714  * @param \phpbb\db\driver\driver_interface $db The database object
715  * @param \phpbb\auth\auth $auth The authentication object
716  * @param int $user_id The user id
717  * @param int $msg_id The id of the PM that we are downloading from
718  *
719  * @return null
720  */
721  function phpbb_download_handle_pm_auth($db, $auth, $user_id, $msg_id)
722  {
723      global $phpbb_dispatcher;
724   
725      if (!$auth->acl_get('u_pm_download'))
726      {
727          send_status_line(403, 'Forbidden');
728          trigger_error('SORRY_AUTH_VIEW_ATTACH');
729      }
730   
731      $allowed = phpbb_download_check_pm_auth($db, $user_id, $msg_id);
732   
733      /**
734      * Event to modify PM attachments download auth
735      *
736      * @event core.modify_pm_attach_download_auth
737      * @var    bool    allowed        Whether the user is allowed to download from that PM or not
738      * @var    int        msg_id        The id of the PM to download from
739      * @var    int        user_id        The user id for auth check
740      * @since 3.1.11-RC1
741      */
742      $vars = array('allowed', 'msg_id', 'user_id');
743      extract($phpbb_dispatcher->trigger_event('core.modify_pm_attach_download_auth', compact($vars)));
744   
745      if (!$allowed)
746      {
747          send_status_line(403, 'Forbidden');
748          trigger_error('ERROR_NO_ATTACHMENT');
749      }
750  }
751   
752  /**
753  * Checks whether a user can download from a particular PM
754  *
755  * @param \phpbb\db\driver\driver_interface $db The database object
756  * @param int $user_id The user id
757  * @param int $msg_id The id of the PM that we are downloading from
758  *
759  * @return bool Whether the user is allowed to download from that PM or not
760  */
761  function phpbb_download_check_pm_auth($db, $user_id, $msg_id)
762  {
763      // Check if the attachment is within the users scope...
764      $sql = 'SELECT msg_id
765          FROM ' . PRIVMSGS_TO_TABLE . '
766          WHERE msg_id = ' . (int) $msg_id . '
767              AND (
768                  user_id = ' . (int) $user_id . '
769                  OR author_id = ' . (int) $user_id . '
770              )';
771      $result = $db->sql_query_limit($sql, 1);
772      $allowed = (bool) $db->sql_fetchfield('msg_id');
773      $db->sql_freeresult($result);
774   
775      return $allowed;
776  }
777   
778  /**
779  * Check if the browser is internet explorer version 7+
780  *
781  * @param string $user_agent    User agent HTTP header
782  * @param int $version IE version to check against
783  *
784  * @return bool true if internet explorer version is greater than $version
785  */
786  function phpbb_is_greater_ie_version($user_agent, $version)
787  {
788      if (preg_match('/msie (\d+)/', strtolower($user_agent), $matches))
789      {
790          $ie_version = (int) $matches[1];
791          return ($ie_version > $version);
792      }
793      else
794      {
795          return false;
796      }
797  }
798   
799  /**
800   * Return whether image type should be allowed to be displayed inline based on the mimetype and potentially Sec-Fetch-Dest header
801   *
802   * @param string $mimetype Image mime type
803   * @param string $sec_fetch_dest Sec-Fetch-Dest header field content
804   * @return bool
805   */
806  function phpbb_allow_serve_inline(string $mimetype, string $sec_fetch_dest): bool
807  {
808      // Allow image types that are known to be supported by all major browsers, and that are known to be safe when rendered inline
809      $allowed_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/avif', 'image/bmp', 'image/tiff'];
810      if (in_array($mimetype, $allowed_types, true))
811      {
812          return true;
813      }
814   
815      return strpos($mimetype, 'image') === 0 && (empty($sec_fetch_dest) || $sec_fetch_dest === 'image');
816  }
817