View file upload/library/XenForo/ProxyOutput.php

File size: 10.28Kb
<?php

/**
 * This class is entirely self sufficient. It handles parsing the input,
 * getting the data, rendering it, and manipulating HTTP headers.
 *
 * @package XenForo_ProxyOutput
 */
class XenForo_ProxyOutput
{
	/**
	 * @var Zend_Controller_Response_Http
	 */
	protected $_response = null;

	/**
	 * @var XenForo_Dependencies_Abstract
	 */
	protected $_dependencies;

	protected $_mode = null;

	protected $_url = null;

	protected $_hash = null;

	protected $_json = false;

	protected $_referrer = null;

	/**
	 * Constructor.
	 *
	 * @param array $input Array of input (intended to normally be $_REQUEST)
	 * @param XenForo_Dependencies_Abstract $dependencies
	 */
	public function __construct(array $input, XenForo_Dependencies_Abstract $dependencies)
	{
		$this->_dependencies = $dependencies;
		$this->_response = new Zend_Controller_Response_Http();

		if (!empty($input['image']) && is_scalar($input['image']))
		{
			$this->_mode = 'image';
			$this->_url = trim(strval($input['image']));
		}

		if (!empty($input['link']) && is_scalar($input['link']))
		{
			$this->_mode = 'link';
			$this->_url = trim(strval($input['link']));
		}

		if (!empty($input['hash']) && is_scalar($input['hash']))
		{
			$this->_hash = trim(strval($input['hash']));
		}

		$paths = XenForo_Application::get('requestPaths');
		$uriParts = explode('?', $paths['fullUri'], 2);
		$subMatchTest = $uriParts[0] . '?';

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

			if (strpos($this->_url, $subMatchTest) === 0)
			{
				$subMatchQs = substr($this->_url, strlen($subMatchTest));
				parse_str($subMatchQs, $subMatchParams);
				if (isset($subMatchParams[$this->_mode])
					&& is_scalar($subMatchParams[$this->_mode])
					&& isset($subMatchParams['hash'])
					&& is_scalar($subMatchParams['hash'])
				)
				{
					$subMatchUrl = trim(strval($subMatchParams[$this->_mode]));
					$subMatchHash = trim(strval($subMatchParams['hash']));
					if ($this->_validateHash($subMatchUrl, $subMatchHash))
					{
						$this->_url = $subMatchUrl;
						$this->_hash = $subMatchHash;
						$hasSubMatch = true;
					}
				}
			}
		}
		while ($hasSubMatch);

		$this->_json = (!empty($input['_xfResponseType']) && $input['_xfResponseType'] === 'json');

		if ($this->_json
			&& $this->_mode == 'link'
			&& !empty($input['_xfRequestUri'])
			&& is_scalar($input['_xfRequestUri'])
		)
		{
			// only use this if we're using JSON output (in a handler that supports it)
			$this->_referrer = XenForo_Link::convertUriToAbsoluteUri($input['_xfRequestUri'], true);
		}

		if (!$this->_referrer && !empty($_SERVER['HTTP_REFERER']))
		{
			$this->_referrer = $_SERVER['HTTP_REFERER'];
		}

		if ($this->_referrer && !preg_match('#^https?://#i', $this->_referrer))
		{
			$this->_referrer = null;
		}
	}

	public function isValidRequest(&$error = null)
	{
		$error = false;

		if (!$this->_url || !preg_match('#^https?://#i', $this->_url))
		{
			$error = 'invalid_url';
			return false;
		}

		$urlParts = @parse_url($this->_url);
		if (!$urlParts || empty($urlParts['host']))
		{
			$error = 'invalid_url';
			return false;
		}

		if (!$this->_validateHash($this->_url, $this->_hash))
		{
			$error = 'invalid_hash';
			return false;
		}

		if (!$this->isValidReferrer())
		{
			$error = 'invalid_referrer';
			return false;
		}

		return true;
	}

	protected function _validateHash($url, $hash)
	{
		$testHash = hash_hmac('md5', $url,
			XenForo_Application::getConfig()->globalSalt . XenForo_Application::getOptions()->imageLinkProxyKey
		);
		return ($hash === $testHash);
	}

	/**
	 * Attempts to determine whether the request is referred from the expected host.
	 * By default, only supports the current host.
	 *
	 * @return boolean
	 */
	public function isValidReferrer()
	{
		if ($this->_referrer)
		{
			$refererParts = @parse_url($this->_referrer);
			if ($refererParts && !empty($refererParts['host']))
			{
				$paths = XenForo_Application::get('requestPaths');
				$requestParts = @parse_url($paths['fullUri']);

				if ($requestParts && !empty($requestParts['host']))
				{
					if ($refererParts['host'] != $requestParts['host'])
					{
						// referer is not the same as request host
						return false;
					}
				}
			}
		}

		// either we have the same host and referer, or we just don't know...
		return true;
	}

	public function output()
	{
		$isValidRequest = $this->isValidRequest($error);

		switch ($this->_mode)
		{
			case 'image':
				$this->_outputImage($error);
				break;

			case 'link':
				$this->_outputLinkRedirect($error);
				break;

			default:
				header('Content-Type: text/html; charset=utf-8', true, 400);
				die('Unknown proxy mode');
		}
	}

	/**
	 * Tells the browser that the data should be downloaded (rather than displayed)
	 * using the specified file name.
	 *
	 * @param string $fileName
	 * @param boolean $inline True if the attachment should be shown inline - use with caution!
	 */
	protected function _setDownloadFileName($fileName, $inline = false)
	{
		$type = ($inline ? 'inline' : 'attachment');

		$this->_response->setHeader('Content-Disposition',
			$type . '; filename="' . str_replace('"', '', $fileName) . '"',
			true
		);
	}

	protected function _isLocalHost($host)
	{
		return (
			preg_match('#^(127\.|192\.168\.|10\.|172\.(1[6789]|2|3[01])|169\.254\.|0\.0\.0\.0)#', $host)
			|| preg_match('#^localhost(\.localdomain)?$#i', $host)
		);
	}

	/**
	 * Intercept a request for a cached image from the proxy and output it
	 *
	 * @param string|bool $error If non-false, an error that occurred when validating the request
	 */
	protected function _outputImage($error)
	{
		if (empty(XenForo_Application::getOptions()->imageLinkProxy['images']))
		{
			$error = 'disabled';
		}

		/* @var $proxyModel XenForo_Model_ImageProxy */
		$proxyModel = XenForo_Model::create('XenForo_Model_ImageProxy');

		$image = false;

		if (!$error)
		{
			if (!XenForo_Helper_Http::isRequestableUntrustedUrl($this->_url))
			{
				$error = 'unrequestable_url';
			}
		}

		if (!$error)
		{
			$image = $proxyModel->getImage($this->_url);
			if ($image)
			{
				$image = $proxyModel->prepareImage($image);
				if ($image['use_file'])
				{
					$proxyModel->logImageView($image);
					if (!empty(XenForo_Application::getOptions()->imageLinkProxyReferrer['enabled'])
						&& $this->_referrer
					)
					{
						$proxyModel->logImageReferrer($image['image_id'], $this->_referrer);
					}

					$eTag = !empty($_SERVER['HTTP_IF_NONE_MATCH']) ? $_SERVER['HTTP_IF_NONE_MATCH'] : null;
					$eTagExpected =  '"' . $image['fetch_date'] . '"';
					if ($eTag && ($eTag == $eTagExpected || $eTag == "W/$eTagExpected"))
					{
						$this->_response->setHttpResponseCode(304);
						$this->_response->clearHeader('Last-Modified');
						$this->_response->sendHeaders();
						return;
					}
				}
				else
				{
					$image = false;
					$error = 'retrieve_failed';
				}
			}
		}

		if (!$image)
		{
			$image = $proxyModel->getPlaceHolderImage();
		}

		$imageTypes = array(
			'image/gif',
			'image/jpeg',
			'image/pjpeg',
			'image/png'
		);

		if (in_array($image['mime_type'], $imageTypes))
		{
			$this->_response->setHeader('Content-type', $image['mime_type'], true);
			$this->_setDownloadFileName($image['file_name'], true);
		}
		else
		{
			$this->_response->setHeader('Content-type', 'application/octet-stream', true);
			$this->_setDownloadFileName($image['file_name']);
		}

		if (!$error)
		{
			$this->_response->setHeader('ETag', '"' . $image['fetch_date'] . '"', true);
		}

		if ($image['file_size'])
		{
			$this->_response->setHeader('Content-Length', $image['file_size'], true);
		}
		$this->_response->setHeader('X-Content-Type-Options', 'nosniff');

		if ($error)
		{
			$this->_response->setHeader('X-Proxy-Error', $error);
		}

		$this->_response->sendHeaders();

		$imageData = new XenForo_FileOutput($image['file_path']);
		$imageData->output();
	}

	/**
	 * Intercept a request for a link redirect
	 *
	 * @param string|bool $error If non-false, an error that occurred when validating the request
	 */
	protected function _outputLinkRedirect($error)
	{
		if ($error === 'invalid_url')
		{
			header('Content-Type: text/html; utf-8', true, 400);
			die('Invalid URL');
		}

		if (empty(XenForo_Application::getOptions()->imageLinkProxy['links']))
		{
			$error = 'disabled';
		}

		if (!$error)
		{
			/* @var $proxyModel XenForo_Model_LinkProxy */
			$proxyModel = XenForo_Model::create('XenForo_Model_LinkProxy');
			$linkId = $proxyModel->logVisit($this->_url);
			if ($linkId
				&& !empty(XenForo_Application::getOptions()->imageLinkProxyReferrer['enabled'])
				&& $this->_referrer
			)
			{
				$proxyModel->logLinkReferrer($linkId, $this->_referrer);
			}

			if ($this->_json)
			{
				header('Content-type: application/json; charset=UTF-8');
				echo json_encode(array('logged' => true));
				exit;
			}

			// this used to redirect, but we don't take this approach any longer, so we display an intersitial
		}

		$request = new Zend_Controller_Request_Http();

		XenForo_Session::startPublicSession($request);

		$this->_dependencies->preRenderView();

		if (!preg_match('#^https?://#i', $this->_url))
		{
			throw new Exception('Unsafe proxy URL: ' . $this->_url);
		}

		$printable = urldecode($this->_url);
		if (!preg_match('/./u', $printable))
		{
			$printable = $this->_url;
		}

		$renderer = new XenForo_ViewRenderer_HtmlPublic($this->_dependencies, $this->_response, $request);
		$contents = $renderer->createTemplateObject('link_redirect', array(
			'url' => $this->_url,
			'printable' => $printable,
			'parts' => parse_url($this->_url)
		));
		$containerParams = $this->_dependencies->getEffectiveContainerParams(array(), $request);

		$output = $renderer->renderContainer($contents, $containerParams);

		$extraHeaders = XenForo_Application::gzipContentIfSupported($output);
		foreach ($extraHeaders AS $extraHeader)
		{
			$this->_response->setHeader($extraHeader[0], $extraHeader[1], $extraHeader[2]);
		}

		$this->_response->setHeader('X-Proxy-Error', $error);

		$this->_response->sendHeaders();
		echo $output;
	}

	/**
	 * Static helper to execute a full request for proxy output
	 */
	public static function run()
	{
		$dependencies = new XenForo_Dependencies_Public();
		$dependencies->preLoadData();

		$class = XenForo_Application::resolveDynamicClass(__CLASS__);

		$proxyOutput = new $class($_REQUEST, $dependencies);
		$proxyOutput->output();
	}
}