. */ namespace Fisharebest\Webtrees; /** * Defined in session.php * * @global Tree $WT_TREE */ global $WT_TREE; use Fisharebest\Webtrees\Functions\FunctionsMedia; define('WT_SCRIPT_NAME', 'mediafirewall.php'); require './includes/session.php'; $mid = Filter::get('mid', WT_REGEX_XREF); $thumb = Filter::getBool('thumb'); $media = Media::getInstance($mid, $WT_TREE); /** * Send a “Not found” error as an image */ function send404AsImage() { $error = I18N::translate('The media file was not found in this family tree.'); $width = mb_strlen($error) * 6.5 + 50; $height = 60; $im = imagecreatetruecolor($width, $height); /* Create a black image */ $bgc = imagecolorallocate($im, 255, 255, 255); /* set background color */ imagefilledrectangle($im, 2, 2, $width - 4, $height - 4, $bgc); /* create a rectangle, leaving 2 px border */ embedText($im, $error, 100, '255, 0, 0', WT_ROOT . Config::FONT_DEJAVU_SANS_TTF, 'top', 'left'); http_response_code(404); header('Content-Type: image/png'); imagepng($im); imagedestroy($im); } /** * The media firewall passes in an image * this function can manipulate the image however it wants * before returning it back to the media firewall * * @param resource $im * @param Tree $tree * * @return resource */ function applyWatermark($im, Tree $tree) { // text to watermark with $word1_text = $tree->getTitle(); // maximum font size for “word1” ; will be automaticaly reduced to fit in the image $word1_maxsize = 100; // rgb color codes for text $word1_color = '0,0,0'; // ttf font file to use $word1_font = WT_ROOT . Config::FONT_DEJAVU_SANS_TTF; // vertical position for the text to past; possible values are: top, middle or bottom, across $word1_vpos = 'across'; // horizontal position for the text to past in media file; possible values are: left, right, top2bottom, bottom2top // this value is used only if $word1_vpos=across $word1_hpos = 'left'; $word2_text = $_SERVER['HTTP_HOST']; $word2_maxsize = 20; $word2_color = '0,0,0'; $word2_font = WT_ROOT . Config::FONT_DEJAVU_SANS_TTF; $word2_vpos = 'top'; $word2_hpos = 'top2bottom'; embedText($im, $word1_text, $word1_maxsize, $word1_color, $word1_font, $word1_vpos, $word1_hpos); embedText($im, $word2_text, $word2_maxsize, $word2_color, $word2_font, $word2_vpos, $word2_hpos); return $im; } /** * Embed text into an image. * * @param resource $im * @param string $text * @param int $maxsize * @param string $color * @param string $font * @param string $vpos * @param string $hpos */ function embedText($im, $text, $maxsize, $color, $font, $vpos, $hpos) { global $useTTF; // there are two ways to embed text with PHP // (preferred) using GD and FreeType you can embed text using any True Type font // (fall back) if that is not available, you can insert basic monospaced text $col = explode(',', $color); $textcolor = imagecolorallocate($im, $col[0], $col[1], $col[2]); // make adjustments to settings that imagestring and imagestringup can’t handle if (!$useTTF) { // imagestringup only writes up, can’t use top2bottom if ($hpos === 'top2bottom') { $hpos = 'bottom2top'; } } $text = I18N::reverseText($text); $height = imagesy($im); $width = imagesx($im); $calc_angle = rad2deg(atan($height / $width)); $hypoth = $height / sin(deg2rad($calc_angle)); // vertical and horizontal position of the text switch ($vpos) { default: case 'top': $taille = textlength($maxsize, $width, $text); $pos_y = $height * 0.15 + $taille; $pos_x = $width * 0.15; $rotation = 0; break; case 'middle': $taille = textlength($maxsize, $width, $text); $pos_y = ($height + $taille) / 2; $pos_x = $width * 0.15; $rotation = 0; break; case 'bottom': $taille = textlength($maxsize, $width, $text); $pos_y = ($height * .85 - $taille); $pos_x = $width * 0.15; $rotation = 0; break; case 'across': switch ($hpos) { default: case 'left': $taille = textlength($maxsize, $hypoth, $text); $pos_y = ($height * .85 - $taille); $pos_x = $width * 0.15; $rotation = $calc_angle; break; case 'right': $taille = textlength($maxsize, $hypoth, $text); $pos_y = ($height * .15 - $taille); $pos_x = $width * 0.85; $rotation = $calc_angle + 180; break; case 'top2bottom': $taille = textlength($maxsize, $height, $text); $pos_y = ($height * .15 - $taille); $pos_x = ($width * .90 - $taille); $rotation = -90; break; case 'bottom2top': $taille = textlength($maxsize, $height, $text); $pos_y = $height * 0.85; $pos_x = $width * 0.15; $rotation = 90; break; } break; } // apply the text if ($useTTF) { // if imagettftext throws errors, catch them with a custom error handler set_error_handler('\Fisharebest\Webtrees\\imagettftextErrorHandler'); imagettftext($im, $taille, $rotation, $pos_x, $pos_y, $textcolor, $font, $text); restore_error_handler(); } // Don’t use an ‘else’ here since imagettftextErrorHandler may have changed the value of $useTTF from true to false if (!$useTTF) { if ($rotation !== 90) { imagestring($im, 5, $pos_x, $pos_y, $text, $textcolor); } else { imagestringup($im, 5, $pos_x, $pos_y, $text, $textcolor); } } } /** * Generate an approximate length of text, in pixels. * * @param int $t * @param int $mxl * @param string $text * * @return int */ function textlength($t, $mxl, $text) { $taille_c = $t; $len = mb_strlen($text); while (($taille_c - 2) * $len > $mxl) { $taille_c--; if ($taille_c == 2) { break; } } return $taille_c; } /** * imagettftext is the function that is most likely to throw an error * use this custom error handler to catch and log it * * @param int $errno * @param string $errstr * * @return bool */ function imagettftextErrorHandler($errno, $errstr) { global $useTTF, $serverFilename; // log the error Log::addErrorLog('Media Firewall error: >' . $errno . '/' . $errstr . '< while processing file >' . $serverFilename . '<'); // change value of useTTF to false so the fallback watermarking can be used. $useTTF = false; return true; } /** * Determine if the system supports editing of that image type * * @param string $reqtype * * @return string|false */ function isImageTypeSupported($reqtype) { $supportByGD = array('jpg' => 'jpeg', 'jpeg' => 'jpeg', 'gif' => 'gif', 'png' => 'png'); $reqtype = strtolower($reqtype); if (empty($supportByGD[$reqtype])) { return false; } $type = $supportByGD[$reqtype]; if (function_exists('imagecreatefrom' . $type) && function_exists('image' . $type)) { return $type; } return false; } // this needs to be a global variable so imagettftextErrorHandler can set it $useTTF = function_exists('imagettftext'); // Media object missing/private? if (!$media || !$media->canShow()) { send404AsImage(); return; } // media file somewhere else? if ($media->isExternal()) { header('Location: ' . $media->getFilename()); return; } $which = $thumb ? 'thumb' : 'main'; $serverFilename = $media->getServerFilename($which); if (!file_exists($serverFilename)) { send404AsImage(); return; } $mimetype = $media->mimeType(); $imgsize = $media->getImageAttributes($which); $filetime = $media->getFiletime($which); $filetimeHeader = gmdate('D, d M Y H:i:s', $filetime) . ' GMT'; $expireOffset = 3600 * 24; // tell browser to cache this image for 24 hours if (Filter::get('cb')) { $expireOffset = $expireOffset * 7; } // if cb parameter was sent, cache for 7 days $expireHeader = gmdate('D, d M Y H:i:s', WT_TIMESTAMP + $expireOffset) . ' GMT'; $type = isImageTypeSupported($imgsize['ext']); $usewatermark = false; // if this image supports watermarks and the watermark module is intalled... if ($type) { // if this is not a thumbnail, or WATERMARK_THUMB is true if (($which === 'main') || $WT_TREE->getPreference('WATERMARK_THUMB')) { // if the user’s priv’s justify it... if (Auth::accessLevel($WT_TREE) > $WT_TREE->getPreference('SHOW_NO_WATERMARK')) { // add a watermark $usewatermark = true; } } } // determine whether we have enough memory to watermark this image if ($usewatermark) { if (!FunctionsMedia::hasMemoryForImage($serverFilename)) { // not enough memory to watermark this file $usewatermark = false; } } $watermarkfile = ''; $generatewatermark = false; if ($usewatermark) { if ($which === 'thumb') { $watermarkfile = WT_DATA_DIR . $WT_TREE->getPreference('MEDIA_DIRECTORY') . 'watermark/' . $WT_TREE->getName() . '/thumb/' . $media->getFilename(); } else { $watermarkfile = WT_DATA_DIR . $WT_TREE->getPreference('MEDIA_DIRECTORY') . 'watermark/' . $WT_TREE->getName() . '/' . $media->getFilename(); } if (!file_exists($watermarkfile)) { // no saved watermark file exists // generate the watermark file $generatewatermark = true; } else { $watermarktime = filemtime($watermarkfile); if ($filetime > $watermarktime) { // if the original image was updated after the saved file was created // generate the watermark file $generatewatermark = true; } } } $etag = $media->getEtag($which); // parse IF_MODIFIED_SINCE header from client $if_modified_since = 'x'; if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { $if_modified_since = preg_replace('/;.*$/', '', $_SERVER['HTTP_IF_MODIFIED_SINCE']); } // parse IF_NONE_MATCH header from client $if_none_match = 'x'; if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) { $if_none_match = str_replace('"', '', $_SERVER['HTTP_IF_NONE_MATCH']); } // add caching headers. allow browser to cache file, but not proxy header('Last-Modified: ' . $filetimeHeader); header('ETag: "' . $etag . '"'); header('Expires: ' . $expireHeader); header('Cache-Control: max-age=' . $expireOffset . ', s-maxage=0, proxy-revalidate'); // if this file is already in the user’s cache, don’t resend it // first check if the if_modified_since param matches if ($if_modified_since === $filetimeHeader) { // then check if the etag matches if ($if_none_match === $etag) { http_response_code(304); return; } } // send headers for the image header('Content-Type: ' . $mimetype); header('Content-Disposition: filename="' . addslashes(basename($media->getFilename())) . '"'); if ($generatewatermark) { // generate the watermarked image $imCreateFunc = 'imagecreatefrom' . $type; $imSendFunc = 'image' . $type; if (function_exists($imCreateFunc) && function_exists($imSendFunc)) { $im = $imCreateFunc($serverFilename); $im = applyWatermark($im, $WT_TREE); // save the image, if preferences allow if ($which === 'thumb' && $WT_TREE->getPreference('SAVE_WATERMARK_THUMB') || $which === 'main' && $WT_TREE->getPreference('SAVE_WATERMARK_IMAGE')) { // make sure the folder exists File::mkdir(dirname($watermarkfile)); // save the image $imSendFunc($im, $watermarkfile); } // send the image $imSendFunc($im); imagedestroy($im); return; } else { // this image is defective. log it Log::addMediaLog('Media Firewall error: >' . I18N::translate('This media file is broken and cannot be watermarked.') . '< in file >' . $serverFilename . '< memory used: ' . memory_get_usage()); // set usewatermark to false so image will simply be passed through below $usewatermark = false; } } // pass the image through without manipulating it if ($usewatermark) { // the stored watermarked image is good, lets use it $serverFilename = $watermarkfile; } // determine filesize of image (could be original or watermarked version) $filesize = filesize($serverFilename); // set content-length header, send file header('Content-Length: ' . $filesize); // Some servers disable fpassthru() and readfile() if (function_exists('readfile')) { readfile($serverFilename); } else { $fp = fopen($serverFilename, 'rb'); if (function_exists('fpassthru')) { fpassthru($fp); } else { while (!feof($fp)) { echo fread($fp, 65536); } } fclose($fp); }