[ Index ] |
PHP Cross Reference of phpBB-3.2.11-deutsch |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * 4 * This file is part of the phpBB Forum Software package. 5 * 6 * @copyright (c) phpBB Limited <https://www.phpbb.com> 7 * @license GNU General Public License, version 2 (GPL-2.0) 8 * 9 * For full copyright and license information, please see 10 * the docs/CREDITS.txt file. 11 * 12 */ 13 14 /** 15 * @ignore 16 */ 17 if (!defined('IN_PHPBB')) 18 { 19 exit; 20 } 21 22 /** 23 * A simplified function to deliver avatars 24 * The argument needs to be checked before calling this function. 25 */ 26 function send_avatar_to_browser($file, $browser) 27 { 28 global $config, $phpbb_root_path; 29 30 $prefix = $config['avatar_salt'] . '_'; 31 $image_dir = $config['avatar_path']; 32 33 // Adjust image_dir path (no trailing slash) 34 if (substr($image_dir, -1, 1) == '/' || substr($image_dir, -1, 1) == '\\') 35 { 36 $image_dir = substr($image_dir, 0, -1) . '/'; 37 } 38 $image_dir = str_replace(array('../', '..\\', './', '.\\'), '', $image_dir); 39 40 if ($image_dir && ($image_dir[0] == '/' || $image_dir[0] == '\\')) 41 { 42 $image_dir = ''; 43 } 44 $file_path = $phpbb_root_path . $image_dir . '/' . $prefix . $file; 45 46 if ((@file_exists($file_path) && @is_readable($file_path)) && !headers_sent()) 47 { 48 header('Cache-Control: public'); 49 50 $image_data = @getimagesize($file_path); 51 header('Content-Type: ' . image_type_to_mime_type($image_data[2])); 52 53 if ((strpos(strtolower($browser), 'msie') !== false) && !phpbb_is_greater_ie_version($browser, 7)) 54 { 55 header('Content-Disposition: attachment; ' . header_filename($file)); 56 57 if (strpos(strtolower($browser), 'msie 6.0') !== false) 58 { 59 header('Expires: ' . gmdate('D, d M Y H:i:s', time()) . ' GMT'); 60 } 61 else 62 { 63 header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 31536000) . ' GMT'); 64 } 65 } 66 else 67 { 68 header('Content-Disposition: inline; ' . header_filename($file)); 69 header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 31536000) . ' GMT'); 70 } 71 72 $size = @filesize($file_path); 73 if ($size) 74 { 75 header("Content-Length: $size"); 76 } 77 78 if (@readfile($file_path) == false) 79 { 80 $fp = @fopen($file_path, 'rb'); 81 82 if ($fp !== false) 83 { 84 while (!feof($fp)) 85 { 86 echo fread($fp, 8192); 87 } 88 fclose($fp); 89 } 90 } 91 92 flush(); 93 } 94 else 95 { 96 header('HTTP/1.0 404 Not Found'); 97 } 98 } 99 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 ($category == ATTACHMENT_CATEGORY_FLASH && $request->variable('view', 0) === 1) 210 { 211 // We use content-disposition: inline for flash files and view=1 to let it correctly play with flash player 10 - any other disposition will fail to play inline 212 header('Content-Disposition: inline'); 213 } 214 else 215 { 216 if (empty($user->browser) || ((strpos(strtolower($user->browser), 'msie') !== false) && !phpbb_is_greater_ie_version($user->browser, 7))) 217 { 218 header('Content-Disposition: attachment; ' . header_filename(htmlspecialchars_decode($attachment['real_filename']))); 219 if (empty($user->browser) || (strpos(strtolower($user->browser), 'msie 6.0') !== false)) 220 { 221 header('Expires: ' . gmdate('D, d M Y H:i:s', time()) . ' GMT'); 222 } 223 } 224 else 225 { 226 header('Content-Disposition: ' . ((strpos($attachment['mimetype'], 'image') === 0) ? 'inline' : 'attachment') . '; ' . header_filename(htmlspecialchars_decode($attachment['real_filename']))); 227 if (phpbb_is_greater_ie_version($user->browser, 7) && (strpos($attachment['mimetype'], 'image') !== 0)) 228 { 229 header('X-Download-Options: noopen'); 230 } 231 } 232 } 233 234 // Close the db connection before sending the file etc. 235 file_gc(false); 236 237 if (!set_modified_headers($attachment['filetime'], $user->browser)) 238 { 239 // We make sure those have to be enabled manually by defining a constant 240 // because of the potential disclosure of full attachment path 241 // in case support for features is absent in the webserver software. 242 if (defined('PHPBB_ENABLE_X_ACCEL_REDIRECT') && PHPBB_ENABLE_X_ACCEL_REDIRECT) 243 { 244 // X-Accel-Redirect - http://wiki.nginx.org/XSendfile 245 header('X-Accel-Redirect: ' . $user->page['root_script_path'] . $upload_dir . '/' . $attachment['physical_filename']); 246 exit; 247 } 248 else if (defined('PHPBB_ENABLE_X_SENDFILE') && PHPBB_ENABLE_X_SENDFILE && !phpbb_http_byte_range($size)) 249 { 250 // X-Sendfile - http://blog.lighttpd.net/articles/2006/07/02/x-sendfile 251 // Lighttpd's X-Sendfile does not support range requests as of 1.4.26 252 // and always requires an absolute path. 253 header('X-Sendfile: ' . dirname(__FILE__) . "/../$upload_dir/{$attachment['physical_filename']}"); 254 exit; 255 } 256 257 if ($size) 258 { 259 header("Content-Length: $size"); 260 } 261 262 // Try to deliver in chunks 263 @set_time_limit(0); 264 265 $fp = @fopen($filename, 'rb'); 266 267 if ($fp !== false) 268 { 269 // Deliver file partially if requested 270 if ($range = phpbb_http_byte_range($size)) 271 { 272 fseek($fp, $range['byte_pos_start']); 273 274 send_status_line(206, 'Partial Content'); 275 header('Content-Range: bytes ' . $range['byte_pos_start'] . '-' . $range['byte_pos_end'] . '/' . $range['bytes_total']); 276 header('Content-Length: ' . $range['bytes_requested']); 277 278 // First read chunks 279 while (!feof($fp) && ftell($fp) < $range['byte_pos_end'] - 8192) 280 { 281 echo fread($fp, 8192); 282 } 283 // Then, read the remainder 284 echo fread($fp, $range['bytes_requested'] % 8192); 285 } 286 else 287 { 288 while (!feof($fp)) 289 { 290 echo fread($fp, 8192); 291 } 292 } 293 fclose($fp); 294 } 295 else 296 { 297 @readfile($filename); 298 } 299 300 flush(); 301 } 302 303 exit; 304 } 305 306 /** 307 * Get a browser friendly UTF-8 encoded filename 308 */ 309 function header_filename($file) 310 { 311 global $request; 312 313 $user_agent = $request->header('User-Agent'); 314 315 // There be dragons here. 316 // Not many follows the RFC... 317 if (strpos($user_agent, 'MSIE') !== false || strpos($user_agent, 'Konqueror') !== false) 318 { 319 return "filename=" . rawurlencode($file); 320 } 321 322 // follow the RFC for extended filename for the rest 323 return "filename*=UTF-8''" . rawurlencode($file); 324 } 325 326 /** 327 * Check if downloading item is allowed 328 */ 329 function download_allowed() 330 { 331 global $config, $user, $db, $request; 332 333 if (!$config['secure_downloads']) 334 { 335 return true; 336 } 337 338 $url = htmlspecialchars_decode($request->header('Referer')); 339 340 if (!$url) 341 { 342 return ($config['secure_allow_empty_referer']) ? true : false; 343 } 344 345 // Split URL into domain and script part 346 $url = @parse_url($url); 347 348 if ($url === false) 349 { 350 return ($config['secure_allow_empty_referer']) ? true : false; 351 } 352 353 $hostname = $url['host']; 354 unset($url); 355 356 $allowed = ($config['secure_allow_deny']) ? false : true; 357 $iplist = array(); 358 359 if (($ip_ary = @gethostbynamel($hostname)) !== false) 360 { 361 foreach ($ip_ary as $ip) 362 { 363 if ($ip) 364 { 365 $iplist[] = $ip; 366 } 367 } 368 } 369 370 // Check for own server... 371 $server_name = $user->host; 372 373 // Forcing server vars is the only way to specify/override the protocol 374 if ($config['force_server_vars'] || !$server_name) 375 { 376 $server_name = $config['server_name']; 377 } 378 379 if (preg_match('#^.*?' . preg_quote($server_name, '#') . '.*?$#i', $hostname)) 380 { 381 $allowed = true; 382 } 383 384 // Get IP's and Hostnames 385 if (!$allowed) 386 { 387 $sql = 'SELECT site_ip, site_hostname, ip_exclude 388 FROM ' . SITELIST_TABLE; 389 $result = $db->sql_query($sql); 390 391 while ($row = $db->sql_fetchrow($result)) 392 { 393 $site_ip = trim($row['site_ip']); 394 $site_hostname = trim($row['site_hostname']); 395 396 if ($site_ip) 397 { 398 foreach ($iplist as $ip) 399 { 400 if (preg_match('#^' . str_replace('\*', '.*?', preg_quote($site_ip, '#')) . '$#i', $ip)) 401 { 402 if ($row['ip_exclude']) 403 { 404 $allowed = ($config['secure_allow_deny']) ? false : true; 405 break 2; 406 } 407 else 408 { 409 $allowed = ($config['secure_allow_deny']) ? true : false; 410 } 411 } 412 } 413 } 414 415 if ($site_hostname) 416 { 417 if (preg_match('#^' . str_replace('\*', '.*?', preg_quote($site_hostname, '#')) . '$#i', $hostname)) 418 { 419 if ($row['ip_exclude']) 420 { 421 $allowed = ($config['secure_allow_deny']) ? false : true; 422 break; 423 } 424 else 425 { 426 $allowed = ($config['secure_allow_deny']) ? true : false; 427 } 428 } 429 } 430 } 431 $db->sql_freeresult($result); 432 } 433 434 return $allowed; 435 } 436 437 /** 438 * Check if the browser has the file already and set the appropriate headers- 439 * @returns false if a resend is in order. 440 */ 441 function set_modified_headers($stamp, $browser) 442 { 443 global $request; 444 445 // let's see if we have to send the file at all 446 $last_load = $request->header('If-Modified-Since') ? strtotime(trim($request->header('If-Modified-Since'))) : false; 447 448 if (strpos(strtolower($browser), 'msie 6.0') === false && !phpbb_is_greater_ie_version($browser, 7)) 449 { 450 if ($last_load !== false && $last_load >= $stamp) 451 { 452 send_status_line(304, 'Not Modified'); 453 // seems that we need those too ... browsers 454 header('Cache-Control: private'); 455 header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 31536000) . ' GMT'); 456 return true; 457 } 458 else 459 { 460 header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $stamp) . ' GMT'); 461 } 462 } 463 return false; 464 } 465 466 /** 467 * Garbage Collection 468 * 469 * @param bool $exit Whether to die or not. 470 * 471 * @return null 472 */ 473 function file_gc($exit = true) 474 { 475 global $cache, $db; 476 477 if (!empty($cache)) 478 { 479 $cache->unload(); 480 } 481 482 $db->sql_close(); 483 484 if ($exit) 485 { 486 exit; 487 } 488 } 489 490 /** 491 * HTTP range support (RFC 2616 Section 14.35) 492 * 493 * Allows browsers to request partial file content 494 * in case a download has been interrupted. 495 * 496 * @param int $filesize the size of the file in bytes we are about to deliver 497 * 498 * @return mixed false if the whole file has to be delivered 499 * associative array on success 500 */ 501 function phpbb_http_byte_range($filesize) 502 { 503 // Only call find_range_request() once. 504 static $request_array; 505 506 if (!$filesize) 507 { 508 return false; 509 } 510 511 if (!isset($request_array)) 512 { 513 $request_array = phpbb_find_range_request(); 514 } 515 516 return (empty($request_array)) ? false : phpbb_parse_range_request($request_array, $filesize); 517 } 518 519 /** 520 * Searches for HTTP range request in request headers. 521 * 522 * @return mixed false if no request found 523 * array of strings containing the requested ranges otherwise 524 * e.g. array(0 => '0-0', 1 => '123-125') 525 */ 526 function phpbb_find_range_request() 527 { 528 global $request; 529 530 $value = $request->header('Range'); 531 532 // Make sure range request starts with "bytes=" 533 if (strpos($value, 'bytes=') === 0) 534 { 535 // Strip leading 'bytes=' 536 // Multiple ranges can be separated by a comma 537 return explode(',', substr($value, 6)); 538 } 539 540 return false; 541 } 542 543 /** 544 * Analyses a range request array. 545 * 546 * A range request can contain multiple ranges, 547 * we however only handle the first request and 548 * only support requests from a given byte to the end of the file. 549 * 550 * @param array $request_array array of strings containing the requested ranges 551 * @param int $filesize the full size of the file in bytes that has been requested 552 * 553 * @return mixed false if the whole file has to be delivered 554 * associative array on success 555 * byte_pos_start the first byte position, can be passed to fseek() 556 * byte_pos_end the last byte position 557 * bytes_requested the number of bytes requested 558 * bytes_total the full size of the file 559 */ 560 function phpbb_parse_range_request($request_array, $filesize) 561 { 562 $first_byte_pos = -1; 563 $last_byte_pos = -1; 564 565 // Go through all ranges 566 foreach ($request_array as $range_string) 567 { 568 $range = explode('-', trim($range_string)); 569 570 // "-" is invalid, "0-0" however is valid and means the very first byte. 571 if (count($range) != 2 || $range[0] === '' && $range[1] === '') 572 { 573 continue; 574 } 575 576 // Substitute defaults 577 if ($range[0] === '') 578 { 579 $range[0] = 0; 580 } 581 582 if ($range[1] === '') 583 { 584 $range[1] = $filesize - 1; 585 } 586 587 if ($last_byte_pos >= 0 && $last_byte_pos + 1 != $range[0]) 588 { 589 // We only support contiguous ranges, no multipart stuff :( 590 return false; 591 } 592 593 if ($range[1] && $range[1] < $range[0]) 594 { 595 // The requested range contains 0 bytes. 596 continue; 597 } 598 599 // Return bytes from $range[0] to $range[1] 600 if ($first_byte_pos < 0) 601 { 602 $first_byte_pos = (int) $range[0]; 603 } 604 605 $last_byte_pos = (int) $range[1]; 606 607 if ($first_byte_pos >= $filesize) 608 { 609 // Requested range not satisfiable 610 return false; 611 } 612 613 // Adjust last-byte-pos if it is absent or greater than the content. 614 if ($range[1] === '' || $last_byte_pos >= $filesize) 615 { 616 $last_byte_pos = $filesize - 1; 617 } 618 } 619 620 if ($first_byte_pos < 0 || $last_byte_pos < 0) 621 { 622 return false; 623 } 624 625 return array( 626 'byte_pos_start' => $first_byte_pos, 627 'byte_pos_end' => $last_byte_pos, 628 'bytes_requested' => $last_byte_pos - $first_byte_pos + 1, 629 'bytes_total' => $filesize, 630 ); 631 } 632 633 /** 634 * Increments the download count of all provided attachments 635 * 636 * @param \phpbb\db\driver\driver_interface $db The database object 637 * @param array|int $ids The attach_id of each attachment 638 * 639 * @return null 640 */ 641 function phpbb_increment_downloads($db, $ids) 642 { 643 if (!is_array($ids)) 644 { 645 $ids = array($ids); 646 } 647 648 $sql = 'UPDATE ' . ATTACHMENTS_TABLE . ' 649 SET download_count = download_count + 1 650 WHERE ' . $db->sql_in_set('attach_id', $ids); 651 $db->sql_query($sql); 652 } 653 654 /** 655 * Handles authentication when downloading attachments from a post or topic 656 * 657 * @param \phpbb\db\driver\driver_interface $db The database object 658 * @param \phpbb\auth\auth $auth The authentication object 659 * @param int $topic_id The id of the topic that we are downloading from 660 * 661 * @return null 662 */ 663 function phpbb_download_handle_forum_auth($db, $auth, $topic_id) 664 { 665 global $phpbb_container; 666 667 $sql_array = array( 668 'SELECT' => 't.topic_visibility, t.forum_id, f.forum_name, f.forum_password, f.parent_id', 669 'FROM' => array( 670 TOPICS_TABLE => 't', 671 FORUMS_TABLE => 'f', 672 ), 673 'WHERE' => 't.topic_id = ' . (int) $topic_id . ' 674 AND t.forum_id = f.forum_id', 675 ); 676 677 $sql = $db->sql_build_query('SELECT', $sql_array); 678 $result = $db->sql_query($sql); 679 $row = $db->sql_fetchrow($result); 680 $db->sql_freeresult($result); 681 682 $phpbb_content_visibility = $phpbb_container->get('content.visibility'); 683 684 if ($row && !$phpbb_content_visibility->is_visible('topic', $row['forum_id'], $row)) 685 { 686 send_status_line(404, 'Not Found'); 687 trigger_error('ERROR_NO_ATTACHMENT'); 688 } 689 else if ($row && $auth->acl_get('u_download') && $auth->acl_get('f_download', $row['forum_id'])) 690 { 691 if ($row['forum_password']) 692 { 693 // Do something else ... ? 694 login_forum_box($row); 695 } 696 } 697 else 698 { 699 send_status_line(403, 'Forbidden'); 700 trigger_error('SORRY_AUTH_VIEW_ATTACH'); 701 } 702 } 703 704 /** 705 * Handles authentication when downloading attachments from PMs 706 * 707 * @param \phpbb\db\driver\driver_interface $db The database object 708 * @param \phpbb\auth\auth $auth The authentication object 709 * @param int $user_id The user id 710 * @param int $msg_id The id of the PM that we are downloading from 711 * 712 * @return null 713 */ 714 function phpbb_download_handle_pm_auth($db, $auth, $user_id, $msg_id) 715 { 716 global $phpbb_dispatcher; 717 718 if (!$auth->acl_get('u_pm_download')) 719 { 720 send_status_line(403, 'Forbidden'); 721 trigger_error('SORRY_AUTH_VIEW_ATTACH'); 722 } 723 724 $allowed = phpbb_download_check_pm_auth($db, $user_id, $msg_id); 725 726 /** 727 * Event to modify PM attachments download auth 728 * 729 * @event core.modify_pm_attach_download_auth 730 * @var bool allowed Whether the user is allowed to download from that PM or not 731 * @var int msg_id The id of the PM to download from 732 * @var int user_id The user id for auth check 733 * @since 3.1.11-RC1 734 */ 735 $vars = array('allowed', 'msg_id', 'user_id'); 736 extract($phpbb_dispatcher->trigger_event('core.modify_pm_attach_download_auth', compact($vars))); 737 738 if (!$allowed) 739 { 740 send_status_line(403, 'Forbidden'); 741 trigger_error('ERROR_NO_ATTACHMENT'); 742 } 743 } 744 745 /** 746 * Checks whether a user can download from a particular PM 747 * 748 * @param \phpbb\db\driver\driver_interface $db The database object 749 * @param int $user_id The user id 750 * @param int $msg_id The id of the PM that we are downloading from 751 * 752 * @return bool Whether the user is allowed to download from that PM or not 753 */ 754 function phpbb_download_check_pm_auth($db, $user_id, $msg_id) 755 { 756 // Check if the attachment is within the users scope... 757 $sql = 'SELECT msg_id 758 FROM ' . PRIVMSGS_TO_TABLE . ' 759 WHERE msg_id = ' . (int) $msg_id . ' 760 AND ( 761 user_id = ' . (int) $user_id . ' 762 OR author_id = ' . (int) $user_id . ' 763 )'; 764 $result = $db->sql_query_limit($sql, 1); 765 $allowed = (bool) $db->sql_fetchfield('msg_id'); 766 $db->sql_freeresult($result); 767 768 return $allowed; 769 } 770 771 /** 772 * Check if the browser is internet explorer version 7+ 773 * 774 * @param string $user_agent User agent HTTP header 775 * @param int $version IE version to check against 776 * 777 * @return bool true if internet explorer version is greater than $version 778 */ 779 function phpbb_is_greater_ie_version($user_agent, $version) 780 { 781 if (preg_match('/msie (\d+)/', strtolower($user_agent), $matches)) 782 { 783 $ie_version = (int) $matches[1]; 784 return ($ie_version > $version); 785 } 786 else 787 { 788 return false; 789 } 790 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Nov 11 20:33:01 2020 | Cross-referenced by PHPXref 0.7.1 |