Seditio Source
Root |
./othercms/xenForo 2.2.8/src/XF/Proxy/Controller.php
<?php

namespace XF\Proxy;

use
XF\Http\Request;

use function
in_array, is_scalar, strlen, strval;

class
Controller
{
   
/**
     * @var \XF\App
     */
   
protected $app;

   
/**
     * @var Linker
     */
   
protected $linker;

   
/**
     * @var Request
     */
   
protected $request;

    protected
$requestUri;
    protected
$referrer;
    protected
$eTag;

   
/**
     * If true, image requests will return an error message (and HTTP code) rather than a placeholder image.
     *
     * @var bool
     */
   
protected $returnError = false;

    const
ERROR_INVALID_URL = 1;
    const
ERROR_INVALID_HASH = 2;
    const
ERROR_INVALID_REFERRER = 3;
    const
ERROR_DISABLED = 4;
    const
ERROR_FAILED = 5;

    public function
__construct(\XF\App $app, Linker $linker, Request $request = null)
    {
       
$this->app = $app;
       
$this->linker = $linker;

        if (!
$request)
        {
           
$request = $app->request();
        }
       
$this->request = $request;

       
$this->requestUri = $request->getFullRequestUri();
       
$this->referrer = $request->getReferrer();
       
$this->eTag = $request->getServer('HTTP_IF_NONE_MATCH');
    }

    public function
setReferrer($referrer)
    {
       
$this->referrer = $referrer;
    }

    public function
setReturnError($returnError)
    {
       
$this->returnError = (bool)$returnError;
    }

    public function
resolveImageProxyRecursion(Request $request, $url)
    {
       
$uriParts = explode('?', $request->getFullRequestUri(), 2);
       
$subMatchTest = $uriParts[0] . '?';

       
$subUrl = $url;
       
$subHash = null;

       
// Recursion can happen, mostly if people copy proxied image URLs.
        // Try to resolve that here.
       
do
        {
           
$hasSubMatch = false;

            if (
strpos($subUrl, $subMatchTest) === 0)
            {
               
$subMatchQs = substr($subUrl, strlen($subMatchTest));
               
parse_str($subMatchQs, $subMatchParams);
                if (isset(
$subMatchParams['image'])
                    &&
is_scalar($subMatchParams['image'])
                    && isset(
$subMatchParams['hash'])
                    &&
is_scalar($subMatchParams['hash'])
                )
                {
                   
$subMatchUrl = trim(strval($subMatchParams['image']));
                   
$subMatchHash = trim(strval($subMatchParams['hash']));
                    if (
$this->linker->verifyHash($subMatchUrl, $subMatchHash))
                    {
                       
$subUrl = $subMatchUrl;
                       
$subHash = $subMatchHash;
                       
$hasSubMatch = true;
                    }
                }
            }
        }
        while (
$hasSubMatch);

        if (
$subHash)
        {
            return [
$subUrl, $subHash];
        }
        else
        {
            return
null;
        }
    }

    public function
outputImage($url, $hash)
    {
        if (
$this->validateImageRequest($url, $hash, $error))
        {
           
/** @var \XF\Service\ImageProxy $imageProxy */
           
$imageProxy = $this->app->service('XF:ImageProxy');
           
$image = $imageProxy->getImage($url);
            if (!
$image || !$image->isValid())
            {
               
$image = null;
            }
        }
        else
        {
           
$image = null;
        }

        if (!
$image)
        {
            if (!
$error)
            {
               
$error = self::ERROR_FAILED;
            }

           
/** @var \XF\Repository\ImageProxy $proxyRepo */
           
$proxyRepo = $this->app->repository('XF:ImageProxy');
           
$image = $proxyRepo->getPlaceholderImage();
        }

        if (
$error && $this->returnError)
        {
            return
$this->outputImageErrorResponse($url, $error);
        }

        if (!
$error)
        {
           
/** @var \XF\Repository\ImageProxy $proxyRepo */
           
$proxyRepo = $this->app->repository('XF:ImageProxy');

           
$proxyRepo->logImageView($image);
            if (
$this->referrer && $this->app->options()->imageLinkProxyReferrer['enabled'])
            {
               
$proxyRepo->logImageReferrer($image, $this->referrer);
            }
        }

       
$response = $this->app->response();
       
$this->applyImageResponseHeaders($response, $image, $error);

        if (
$image->isPlaceholder())
        {
           
$body = $response->responseFile($image->getPlaceholderPath());
        }
        else
        {
            try
            {
               
$stream = $this->app->fs()->readStream($image->getAbstractedImagePath());
               
$body = $response->responseStream($stream, $image->file_size);
            }
            catch (\
League\Flysystem\FileNotFoundException $e)
            {
               
// the file was pruned mid-request
               
$proxyRepo = $this->app->repository('XF:ImageProxy');
               
$body = $response->responseFile(
                   
$proxyRepo->getPlaceholderImage()->getPlaceholderPath()
                );
            }
        }

       
$response->body($body);

        return
$response;
    }

    public function
applyImageResponseHeaders(\XF\Http\Response $response, \XF\Entity\ImageProxy $image, $error)
    {
        if (
$error)
        {
           
$response->header('Cache-Control', 'no-cache');
        }
        else
        {
           
$nextRefresh = $image->getNextPlannedRefreshDate();
            if (
$nextRefresh)
            {
               
$maxAge = max(0, $nextRefresh - \XF::$time);
            }
            else
            {
               
$maxAge = 86400; // allow daily revalidation if we don't know otherwise
           
}

           
$response->header('Cache-Control', 'public, max-age=' . $maxAge);
           
$response->header('Last-Modified', gmdate('D, d M Y H:i:s', $image->fetch_date) . ' GMT');

           
$expectedETag = $image->getETagValue();
            if (
$expectedETag)
            {
               
$response->header('ETag', '"' . $expectedETag . '"', true);

                if (
$this->eTag && $this->eTag === "\"$expectedETag\"")
                {
                   
$response->httpCode(304);
                   
$response->removeHeader('Last-Modified');
                    return;
                }
            }
        }

       
$imageTypes = array_unique(\XF::app()->inlineImageTypes);

        if (
in_array($image->mime_type, $imageTypes))
        {
           
$response->contentType($image->mime_type);
           
$response->setDownloadFileName($image->file_name, true);
        }
        else
        {
           
$response->contentType('application/octet-stream');
           
$response->setDownloadFileName($image->file_name);
        }

       
$response->header('X-Content-Type-Options', 'nosniff');

        if (
$error)
        {
           
$response->header('X-Proxy-Error', $error);
        }
    }

    protected function
outputImageErrorResponse($url, $error)
    {
       
$response = $this->app->response();
       
$response->httpCode(404);
       
$response->contentType('text/plain');
       
$response->header('Cache-Control', 'no-cache');
       
$response->header('X-Proxy-Error', $error);
       
$response->body('error');

        return
$response;
    }

    public function
validateImageRequest($url, $hash, &$error = null)
    {
        if (!
$this->linker->isTypeEnabled('image'))
        {
           
$error = self::ERROR_DISABLED;
            return
false;
        }

        if (!
$this->validateProxyRequestGeneric($url, $hash, $error))
        {
            return
false;
        }

        return
true;
    }

    public function
outputLink($url, $hash, $json = false)
    {
       
$response = $this->app->response();
       
$response->contentType('application/json', 'utf-8');

        if (
$this->validateLinkRequest($url, $hash, $error))
        {
           
/** @var \XF\Repository\LinkProxy $proxyRepo */
           
$proxyRepo = $this->app->repository('XF:LinkProxy');

           
$link = $proxyRepo->logLinkVisit($url);
            if (
$link && $this->referrer && $this->app->options()->imageLinkProxyReferrer['enabled'])
            {
               
$proxyRepo->logLinkReferrer($link, $this->referrer);
            }

           
$response->body(json_encode(['logged' => true]));
        }
        else
        {
           
$response->httpCode(400);

            if (\
XF::$debugMode)
            {
               
$response->body(json_encode(['invalid' => true, 'code' => $error]));
            }
            else
            {
               
$response->body(json_encode(['invalid' => true]));
            }
        }

        return
$response;
    }

    public function
validateLinkRequest($url, $hash, &$error = null)
    {
        if (!
$this->linker->isTypeEnabled('link'))
        {
           
$error = self::ERROR_DISABLED;
            return
false;
        }

        if (!
$this->validateProxyRequestGeneric($url, $hash, $error))
        {
            return
false;
        }

        return
true;
    }

    protected function
validateProxyRequestGeneric($url, $hash, &$error = null)
    {
       
$error = null;

        if (!
preg_match('#^https?://#i', $url))
        {
           
$error = self::ERROR_INVALID_URL;
            return
false;
        }

       
$urlParts = @parse_url($url);
        if (!
$urlParts || empty($urlParts['host']))
        {
           
$error = self::ERROR_INVALID_URL;
            return
false;
        }

        if (!
$this->linker->verifyHash($url, $hash))
        {
           
$error = self::ERROR_INVALID_HASH;
            return
false;
        }

        if (!
$this->isValidReferrer())
        {
           
$error = self::ERROR_INVALID_REFERRER;
            return
false;
        }

        return
true;
    }

    protected function
isValidReferrer()
    {
        if (!
$this->referrer)
        {
            return
true;
        }

       
$referrerParts = @parse_url($this->referrer);
        if (!
$referrerParts || empty($referrerParts['host']))
        {
            return
true;
        }

       
$requestParts = @parse_url($this->requestUri);
        if (!
$requestParts || empty($requestParts['host']))
        {
            return
true;
        }

        return (
$requestParts['host'] === $referrerParts['host']);
    }
}