e107inc/e107

View on GitHub
e107_handlers/e_thumbnail_class.php

Summary

Maintainability
A
0 mins
Test Coverage
F
40%
<?php
    /**
     * e107 website system
     *
     * Copyright (C) 2008-2020 e107 Inc (e107.org)
     * Released under the terms and conditions of the
     * GNU General Public License (http://www.gnu.org/licenses/gpl.txt)
     *
     */



use Intervention\Image\ImageManagerStatic as Image;

/**
 *
 */
class e_thumbnail
{
    private $_debug = false;

    private $_cache = true;

    /**
     * Page request
     * @var array
     */
    protected $_request = array();

    /**
     * @var string image source path (e107 path shortcode)
     */
    protected $_src = null;

    /**
     * @var string source path modified/sanitized
     */
    protected $_src_path = null;


    /** Stores watermark prefs
     */
    protected $_watermark = array('activate'=>null);

    protected $_placeholder = false;

    protected $_thumbQuality = 65;

    protected $_upsize = true;

    protected $_forceWebP = false;

    /**
     * Constructor - init paths
     *
     *
     * @return void
     */
    public function __construct()
    {

    }

    /**
     * Initialize the class with e107 core prefs.
     * @param array $pref
     * @return null
     */
    public function init($pref)
    {
        $this->parseRequest();

        if(!empty($this->_request['noinit']))
        {
            return null;
        }

        $this->_watermark = array(
            'activate'        => vartrue($pref['watermark_activate'], false),
            'text'            => vartrue($pref['watermark_text']),
            'size'            => vartrue($pref['watermark_size'], 20),
            'pos'            => vartrue($pref['watermark_pos'],"BR"),
            'color'            => vartrue($pref['watermark_color'],'fff'),
            'font'            => vartrue($pref['watermark_font']),
            'margin'        => vartrue($pref['watermark_margin'],30),
            'shadowcolor'    => vartrue($pref['watermark_shadowcolor'], '000000'),
            'opacity'        => vartrue($pref['watermark_opacity'], 20)
        );

        $this->_thumbQuality = vartrue($pref['thumbnail_quality'],65);

        $this->_upsize = ((isset($this->_request['w']) && $this->_request['w'] > 110) || (isset($this->_request['aw']) && ($this->_request['aw'] > 110))); // don't resizeUp the icon images.

        $this->_forceWebP = empty($this->_request['type']) && !empty($pref['thumb_to_webp']) && (strpos( $_SERVER['HTTP_ACCEPT'], 'image/webp' ) !== false) ? true : false;
    //    var_dump($this);
    //    exit;
        return null;
    }

    /**
     * @param array $array keys: activate, text, size, pos, color, font, margin, shadowcolor, opacity. @see above.
     */
    public function setWatermark($array)
    {
        $this->_watermark = (array) $array;

    }

    /**
     * Enabled/Disable debugging/testing.
     * @param $val
     */
    public function setDebug($val)
    {
        $this->_debug = (bool) $val;
    }


    /**
     * Enable/disable image caching.
     * @param bool $val
     */
    public function setCache($val)
    {
        $this->_cache = (bool) $val;
    }

    /**
     * @return $this
     */
    private function parseRequest()
    {

        $e_QUERY = !empty($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : e_QUERY;

        if(isset($_GET['id'])) // very-basic url-tampering prevention and path cloaking
        {
            $e_QUERY = base64_decode($_GET['id']);
        }

        $e_QUERY= str_replace('e_AVATAR', 'e_AVATAR/', $e_QUERY); // FIXME  Quick and dirty fix.

        parse_str(str_replace('&amp;', '&', $e_QUERY), $this->_request);

        if(isset($this->_request['w']))
        {
            $this->_request['w'] = (int) $this->_request['w'];
        }
        if(isset($this->_request['h']))
        {
            $this->_request['h'] = (int) $this->_request['h'];
        }

        return $this;
    }

    /**
     * Manually Set the Request parameters
     * @param array $array keys: w, h, ah, aw, c(rop)
     */
    public function setRequest($array)
    {
        $this->_request = (array) $array;
    }

    /**
     * @return array|string|string[]
     */
    private function getImageInfo()
    {
        $thumbnfo = pathinfo($this->_src_path);

        if($this->_forceWebP === true || (!empty($this->_request['type']) && $this->_request['type'] == 'webp'))
        {
            $thumbnfo['extension'] = 'webp';
        }

        return $thumbnfo;
    }

    /**
     * Validate and Sanitize the Request.
     * @return bool true when request is okay.
     */
    public function checkSrc()
    {
        if(!vartrue($this->_request['src'])) // display placeholder when src is missing.
        {
            $this->_placeholder = true;
            return true;
        }

        $tp = e107::getParser();

        // convert raw to SC path
        $this->_request['src'] = str_replace($tp->getUrlConstants('raw'), $tp->getUrlConstants(), $this->_request['src']);

        // convert absolute and full url to SC URL
        $this->_src = $tp->createConstants($this->_request['src'], 'mix');

        if(preg_match('#^(https?|ftps?|file)://#i', $this->_request['src']))
        {
            return false;
        }

        if(!is_writable(e_CACHE_IMAGE))
        {
            echo 'Cache folder not writeable! ';
            return false;
        }

        // convert to relative server path
        $path = $tp->replaceConstants(str_replace('..', '', $this->_src)); //should be safe enough

        if(is_file($path) && is_readable($path))
        {
            $this->_src_path = $path;
            return true;
        }

        if($this->_debug === true || defined("e_DEBUG_THUMBNAIL"))
        {
            echo "File Not Found: ".$path;
        }

        $this->_placeholder = true;
        return true;

    }

    /**
     * @return $this|false|string|void
     */
    public function sendImage()
    {
        if($this->_placeholder == true)
        {
            $width = ($this->_request['aw']) ? $this->_request['aw'] : $this->_request['w'];
            $height = ($this->_request['ah']) ? $this->_request['ah'] : $this->_request['h'];

            $parm = array('size' => $width."x".$height);

            $this->placeholder($parm);
            return false;
        }

        if(!$this->_src_path) // would only happen during testing.
        {
            echo "no source";
            return $this;
        }

        $thumbnfo = $this->getImageInfo();
        $options = $this->getRequestOptions();

        $fname = e107::getParser()->thumbCacheFile($this->_src_path, $options);
        $cache_filename = e_CACHE_IMAGE . $fname;

        $this->sendCachedImage($cache_filename, $thumbnfo);

        // No Cached file found - proceed with image creation.

        $img = Image::make($this->_src_path);


        if(!empty($options['c'])) // Cropping:  $quadrant T(op), B(ottom), C(enter), L(eft), R(right)
        {
            if(!empty($this->_request['ah']))
            {
                $this->_request['h'] = $this->_request['ah'];
            }

            if(!empty($this->_request['aw']))
            {
                $this->_request['w'] = $this->_request['aw'];
            }

            $key = $options['c'];
            $fit = array(
                'T' => 'top',
                'C' => 'center',
                'B' => 'bottom',
                'L' => 'left',
                'R' => 'right'
            );

            $position = varset($fit[$key],'top');

            $img->fit(vartrue($this->_request['w'], null), vartrue($this->_request['h'], null), null, $position);
        }
        elseif(!empty($this->_request['w']) || !empty($this->_request['h']))
        {
            $img->resize(vartrue($this->_request['w'], null), vartrue($this->_request['h'], null), function ($constraint)
            {
                $constraint->aspectRatio();

                if(!$this->_upsize)
                {
                    $constraint->upsize();
                }

            });
        }
        elseif(!empty($this->_request['ah']) || !empty($this->_request['aw']))
        {
            $img->fit(vartrue($this->_request['aw'], null), vartrue($this->_request['ah'], null), null, 'top');
        }

        /*
            todo watermark support. @see http://image.intervention.io/api/text
            todo also @see: http://image.intervention.io/api/insert
        */


        $img->save($cache_filename, $this->_thumbQuality, $thumbnfo['extension']);

        $this->_request = array(); // reset the request.

        if($this->_debug === true) // return the cache file path for testing.
        {
            return $cache_filename;
        }


        $imageData = $img->encode($thumbnfo['extension'], $this->_thumbQuality);
        $thumbnfo['fsize'] = strlen($imageData);

        $this->sendHeaders($thumbnfo);
        echo $imageData;

        exit;
    }



/*
    protected function sendImageOld() // for reference. - can be removed at a later date.
    {

        if($this->_placeholder == true)
        {
            $width = ($this->_request['aw']) ? $this->_request['aw'] : $this->_request['w'];
            $height = ($this->_request['ah']) ? $this->_request['ah'] : $this->_request['h'];

            $parm = array('size' => $width."x".$height);

            $this->placeholder($parm);
            return false;
        }

        if(!$this->_src_path)
        {
            echo "no source";
            return $this;
        }

        $thumbnfo = pathinfo($this->_src_path);
        $options = $this->getRequestOptions();
        $fname = e107::getParser()->thumbCacheFile($this->_src_path, $options);
        $cache_filename = e_CACHE_IMAGE . $fname;

        $this->sendCachedImage($cache_filename,$thumbnfo);

        require_once(e_HANDLER.'phpthumb/ThumbLib.inc.php');

        if(!$thumb = PhpThumbFactory::create($this->_src_path))
        {
            if(getperms('0'))
            {
                echo "Couldn't load thumb factory";
            }
            return null;
        }

        $sizeUp = ((isset($this->_request['w']) && $this->_request['w'] > 110) || (isset($this->_request['aw']) && ($this->_request['aw'] > 110))); // don't resizeUp the icon images.
           $thumb->setOptions(array(
                   'correctPermissions'    => true,
                   'resizeUp'              => $sizeUp,
                   'jpegQuality'           => $this->_thumbQuality,
                'interlace'             => true // improves performance
            ));


        // Image Cropping by Quadrant.
        if(!empty($options['c'])) // $quadrant T(op), B(ottom), C(enter), L(eft), R(right)
        {
            if(!empty($this->_request['ah']))
            {
                $this->_request['h'] = $this->_request['ah'];
            }

            if(!empty($this->_request['aw']))
            {
                $this->_request['w'] = $this->_request['aw'];
            }



            $thumb->adaptiveResizeQuadrant((integer) vartrue($this->_request['w'], 0), (integer) vartrue($this->_request['h'], 0), $options['c']);
        }
        if(isset($this->_request['w']) || isset($this->_request['h']))
        {
            $thumb->resize((integer) vartrue($this->_request['w'], 0), (integer) vartrue($this->_request['h'], 0));
        }
        elseif(!empty($this->_request['ah']))
        {
            //Typically gives a better result with images of people than adaptiveResize().
            $thumb->adaptiveResizeQuadrant((integer) vartrue($this->_request['aw'], 0), (integer) vartrue($this->_request['ah'], 0), 'T');
        }
        else
        {
            $thumb->adaptiveResize((integer) vartrue($this->_request['aw'], 0), (integer) vartrue($this->_request['ah'], 0));
        }


        if($this->_debug === true)
        {
            // echo "time: ".round((microtime(true) - $start),4);

            var_dump($thumb);
            return false;
        }

        // Watermark Option - See admin->MediaManager->prefs for details.

    /    if(($this->_watermark['activate'] < $options['w']
        || $this->_watermark['activate'] < $options['aw']
        || $this->_watermark['activate'] < $options['h']
        || $this->_watermark['activate'] < $options['ah']
        ) && $this->_watermark['activate'] > 0 && $this->_watermark['font'] !='')
        {
            $tp = e107::getParser();
            $this->_watermark['font'] = $tp->createConstants($this->_watermark['font'], 'mix');
            $this->_watermark['font'] =  realpath($tp->replaceConstants($this->_watermark['font'],'rel'));

        //    $thumb->WatermarkText($this->_watermark); // failing due to phpThumb::
        }
        //    echo "hello";


        // set cache
        $thumb->save($cache_filename);

        $this->_request = array(); // reset the request.

        if($this->_debug === true) // return the cache file path for testing.
        {
            return $cache_filename;
        }

        // show thumb
        $thumb->show();

        return $this;
    }
*/

    /**
     * When caching is enabled, send the cached image with headers and then exit the script.
     * @param string $cache_filename
     * @param array $thumbnfo
     * @return null
     */
    private function sendCachedImage($cache_filename, $thumbnfo)
    {
        if(!$this->_cache || !is_file($cache_filename) || !is_readable($cache_filename) || $this->_debug)
        {
            return null;
        }

        $thumbnfo['lmodified'] = filemtime($cache_filename);
        $thumbnfo['md5s'] = md5_file($cache_filename);
        $thumbnfo['fsize'] = filesize($cache_filename);

            // Send required headers
        if($this->_debug !== true)
        {
            $this->sendHeaders($thumbnfo);
        }

        // check browser cache
        if (@$_SERVER['HTTP_IF_MODIFIED_SINCE'] && ($thumbnfo['lmodified'] <= strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])) && (isset($_SERVER['HTTP_IF_NONE_MATCH']) && trim($_SERVER['HTTP_IF_NONE_MATCH']) == $thumbnfo['md5s']))
        {
            header('HTTP/1.1 304 Not Modified');
            //$bench->end()->logResult('thumb.php', $_GET['src'].' - 304 not modified');
            exit;
        }

        eShims::readfile($cache_filename);

        exit;

    }

    /**
     * @return array
     */
    private function getRequestOptions()
    {
        $ret = array();
        $ret['w'] = isset($this->_request['w']) ? intval($this->_request['w']) : false;
        $ret['h'] = isset($this->_request['h']) ? intval($this->_request['h']) : $ret['w'];
        $ret['aw'] = isset($this->_request['aw']) ? intval($this->_request['aw']) : false;
        $ret['ah'] = isset($this->_request['ah']) ? intval($this->_request['ah']) : $ret['aw'];
        $ret['c'] = isset($this->_request['c']) ? strtoupper(substr(e107::getParser()->filter($this->_request['c'], 'str'),0,1)) : false;
    //    $ret['wm'] = isset($this->_request['wm']) ? intval($this->_request['wm']) : $ret['wm'];

        if($ret['c'] == 'A') // auto
        {
            $ret['c'] = 'T'; // default is 'Top';
        }

        if(!empty($this->_request['type']))
        {
            $ret['type'] = $this->_request['type'];
        }
        elseif($this->_forceWebP)
        {
            $ret['type'] = 'webp'; 
        }

        return $ret;
    }

    /**
     * @param $thumbnfo
     * @return void
     */
    private function sendHeaders($thumbnfo)
    {

        if(headers_sent($filename, $linenum))
        {
            echo 'Headers already sent in '.$filename.' on line '.$linenum;
            exit;
        }

        if (function_exists('date_default_timezone_set'))
        {
            date_default_timezone_set('UTC');
        }

        header('Cache-Control: must-revalidate');

        if(isset($thumbnfo['lmodified']))
        {
            header('Last-Modified: '.gmdate('D, d M Y H:i:s', $thumbnfo['lmodified']).' GMT');
        }

        if(isset($thumbnfo['fsize'])) // extra check, if empty image will fail.
        {
            header('Content-Length: '.$thumbnfo['fsize']);
        }

        header('Content-Disposition: filename='.$thumbnfo['basename']); // important for right-click save-as.

        $ctype = $this->ctype($thumbnfo['extension']);
        if(null !== $ctype)
        {
            header('Content-Type: '.$ctype);
        }

        // Expire header - 1 year
        $time = time() + 365 * 86400;
        header('Expires: '.gmdate("D, d M Y H:i:s", $time).' GMT');

        if(isset($thumbnfo['md5s']))
        {
            header("Etag: ".$thumbnfo['md5s']);
        }

    }


    /**
     * @param $ftype
     * @return mixed|string|null
     */
    private function ctype($ftype)
    {
        static $known_types = array(
            'gif'  => 'image/gif',
            'jpg'  => 'image/jpeg',
            'jpeg' => 'image/jpeg',
            'png'  => 'image/png',
            'webp'  => 'image/webp',
            //'bmp'  => 'image/bmp',
        );

        $ftype = strtolower($ftype);
        if(isset($known_types[$ftype]))
        {
            return $known_types[$ftype];
        }
        return null;
    }


    // Display a placeholder image.

    /**
     * @param $parm
     * @return void|null
     */
    private function placeholder($parm)
    {
        if($this->_debug === true)
        {
            echo "Placeholder activated";
            return null;
        }

        $getsize = $parm['size'] ?? '100x100';

        header('location: https://placehold.co/'.$getsize);
        header('Content-Length: 0');
        exit();
    }


}