diff -r 000000000000 -r 7f95f8617b0b vendor/symfony/src/Symfony/Component/HttpFoundation/Response.php --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/vendor/symfony/src/Symfony/Component/HttpFoundation/Response.php Sat Sep 24 15:40:41 2011 +0200 @@ -0,0 +1,891 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation; + +/** + * Response represents an HTTP response. + * + * @author Fabien Potencier + * + * @api + */ +class Response +{ + /** + * @var \Symfony\Component\HttpFoundation\ResponseHeaderBag + */ + public $headers; + + protected $content; + protected $version; + protected $statusCode; + protected $statusText; + protected $charset; + + static public $statusTexts = array( + 100 => 'Continue', + 101 => 'Switching Protocols', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 307 => 'Temporary Redirect', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Requested Range Not Satisfiable', + 417 => 'Expectation Failed', + 418 => 'I\'m a teapot', + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported', + ); + + /** + * Constructor. + * + * @param string $content The response content + * @param integer $status The response status code + * @param array $headers An array of response headers + * + * @api + */ + public function __construct($content = '', $status = 200, $headers = array()) + { + $this->headers = new ResponseHeaderBag($headers); + $this->setContent($content); + $this->setStatusCode($status); + $this->setProtocolVersion('1.0'); + if (!$this->headers->has('Date')) { + $this->setDate(new \DateTime(null, new \DateTimeZone('UTC'))); + } + } + + /** + * Returns the response content as it will be sent (with the headers). + * + * @return string The response content + */ + public function __toString() + { + $this->prepare(); + + return + sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText)."\r\n". + $this->headers."\r\n". + $this->getContent(); + } + + /** + * Clones the current Response instance. + */ + public function __clone() + { + $this->headers = clone $this->headers; + } + + /** + * Prepares the Response before it is sent to the client. + * + * This method tweaks the Response to ensure that it is + * compliant with RFC 2616. + */ + public function prepare() + { + if ($this->isInformational() || in_array($this->statusCode, array(204, 304))) { + $this->setContent(''); + } + + // Fix Content-Type + $charset = $this->charset ?: 'UTF-8'; + if (!$this->headers->has('Content-Type')) { + $this->headers->set('Content-Type', 'text/html; charset='.$charset); + } elseif ('text/' === substr($this->headers->get('Content-Type'), 0, 5) && false === strpos($this->headers->get('Content-Type'), 'charset')) { + // add the charset + $this->headers->set('Content-Type', $this->headers->get('Content-Type').'; charset='.$charset); + } + + // Fix Content-Length + if ($this->headers->has('Transfer-Encoding')) { + $this->headers->remove('Content-Length'); + } + } + + /** + * Sends HTTP headers. + */ + public function sendHeaders() + { + // headers have already been sent by the developer + if (headers_sent()) { + return; + } + + $this->prepare(); + + // status + header(sprintf('HTTP/%s %s %s', $this->version, $this->statusCode, $this->statusText)); + + // headers + foreach ($this->headers->all() as $name => $values) { + foreach ($values as $value) { + header($name.': '.$value, false); + } + } + + // cookies + foreach ($this->headers->getCookies() as $cookie) { + setcookie($cookie->getName(), $cookie->getValue(), $cookie->getExpiresTime(), $cookie->getPath(), $cookie->getDomain(), $cookie->isSecure(), $cookie->isHttpOnly()); + } + } + + /** + * Sends content for the current web response. + */ + public function sendContent() + { + echo $this->content; + } + + /** + * Sends HTTP headers and content. + * + * @api + */ + public function send() + { + $this->sendHeaders(); + $this->sendContent(); + + if (function_exists('fastcgi_finish_request')) { + fastcgi_finish_request(); + } + } + + /** + * Sets the response content + * + * Valid types are strings, numbers, and objects that implement a __toString() method. + * + * @param mixed $content + * + * @api + */ + public function setContent($content) + { + if (null !== $content && !is_string($content) && !is_numeric($content) && !is_callable(array($content, '__toString'))) { + throw new \UnexpectedValueException('The Response content must be a string or object implementing __toString(), "'.gettype($content).'" given.'); + } + + $this->content = (string) $content; + } + + /** + * Gets the current response content + * + * @return string Content + * + * @api + */ + public function getContent() + { + return $this->content; + } + + /** + * Sets the HTTP protocol version (1.0 or 1.1). + * + * @param string $version The HTTP protocol version + * + * @api + */ + public function setProtocolVersion($version) + { + $this->version = $version; + } + + /** + * Gets the HTTP protocol version. + * + * @return string The HTTP protocol version + * + * @api + */ + public function getProtocolVersion() + { + return $this->version; + } + + /** + * Sets response status code. + * + * @param integer $code HTTP status code + * @param string $text HTTP status text + * + * @throws \InvalidArgumentException When the HTTP status code is not valid + * + * @api + */ + public function setStatusCode($code, $text = null) + { + $this->statusCode = (int) $code; + if ($this->isInvalid()) { + throw new \InvalidArgumentException(sprintf('The HTTP status code "%s" is not valid.', $code)); + } + + $this->statusText = false === $text ? '' : (null === $text ? self::$statusTexts[$this->statusCode] : $text); + } + + /** + * Retrieves status code for the current web response. + * + * @return string Status code + * + * @api + */ + public function getStatusCode() + { + return $this->statusCode; + } + + /** + * Sets response charset. + * + * @param string $charset Character set + * + * @api + */ + public function setCharset($charset) + { + $this->charset = $charset; + } + + /** + * Retrieves the response charset. + * + * @return string Character set + * + * @api + */ + public function getCharset() + { + return $this->charset; + } + + /** + * Returns true if the response is worth caching under any circumstance. + * + * Responses marked "private" with an explicit Cache-Control directive are + * considered uncacheable. + * + * Responses with neither a freshness lifetime (Expires, max-age) nor cache + * validator (Last-Modified, ETag) are considered uncacheable. + * + * @return Boolean true if the response is worth caching, false otherwise + * + * @api + */ + public function isCacheable() + { + if (!in_array($this->statusCode, array(200, 203, 300, 301, 302, 404, 410))) { + return false; + } + + if ($this->headers->hasCacheControlDirective('no-store') || $this->headers->getCacheControlDirective('private')) { + return false; + } + + return $this->isValidateable() || $this->isFresh(); + } + + /** + * Returns true if the response is "fresh". + * + * Fresh responses may be served from cache without any interaction with the + * origin. A response is considered fresh when it includes a Cache-Control/max-age + * indicator or Expiration header and the calculated age is less than the freshness lifetime. + * + * @return Boolean true if the response is fresh, false otherwise + * + * @api + */ + public function isFresh() + { + return $this->getTtl() > 0; + } + + /** + * Returns true if the response includes headers that can be used to validate + * the response with the origin server using a conditional GET request. + * + * @return Boolean true if the response is validateable, false otherwise + * + * @api + */ + public function isValidateable() + { + return $this->headers->has('Last-Modified') || $this->headers->has('ETag'); + } + + /** + * Marks the response as "private". + * + * It makes the response ineligible for serving other clients. + * + * @api + */ + public function setPrivate() + { + $this->headers->removeCacheControlDirective('public'); + $this->headers->addCacheControlDirective('private'); + } + + /** + * Marks the response as "public". + * + * It makes the response eligible for serving other clients. + * + * @api + */ + public function setPublic() + { + $this->headers->addCacheControlDirective('public'); + $this->headers->removeCacheControlDirective('private'); + } + + /** + * Returns true if the response must be revalidated by caches. + * + * This method indicates that the response must not be served stale by a + * cache in any circumstance without first revalidating with the origin. + * When present, the TTL of the response should not be overridden to be + * greater than the value provided by the origin. + * + * @return Boolean true if the response must be revalidated by a cache, false otherwise + * + * @api + */ + public function mustRevalidate() + { + return $this->headers->hasCacheControlDirective('must-revalidate') || $this->headers->has('must-proxy-revalidate'); + } + + /** + * Returns the Date header as a DateTime instance. + * + * @return \DateTime A \DateTime instance + * + * @throws \RuntimeException when the header is not parseable + * + * @api + */ + public function getDate() + { + return $this->headers->getDate('Date'); + } + + /** + * Sets the Date header. + * + * @param \DateTime $date A \DateTime instance + * + * @api + */ + public function setDate(\DateTime $date) + { + $date->setTimezone(new \DateTimeZone('UTC')); + $this->headers->set('Date', $date->format('D, d M Y H:i:s').' GMT'); + } + + /** + * Returns the age of the response. + * + * @return integer The age of the response in seconds + */ + public function getAge() + { + if ($age = $this->headers->get('Age')) { + return $age; + } + + return max(time() - $this->getDate()->format('U'), 0); + } + + /** + * Marks the response stale by setting the Age header to be equal to the maximum age of the response. + * + * @api + */ + public function expire() + { + if ($this->isFresh()) { + $this->headers->set('Age', $this->getMaxAge()); + } + } + + /** + * Returns the value of the Expires header as a DateTime instance. + * + * @return \DateTime A DateTime instance + * + * @api + */ + public function getExpires() + { + return $this->headers->getDate('Expires'); + } + + /** + * Sets the Expires HTTP header with a \DateTime instance. + * + * If passed a null value, it removes the header. + * + * @param \DateTime $date A \DateTime instance + * + * @api + */ + public function setExpires(\DateTime $date = null) + { + if (null === $date) { + $this->headers->remove('Expires'); + } else { + $date = clone $date; + $date->setTimezone(new \DateTimeZone('UTC')); + $this->headers->set('Expires', $date->format('D, d M Y H:i:s').' GMT'); + } + } + + /** + * Sets the number of seconds after the time specified in the response's Date + * header when the the response should no longer be considered fresh. + * + * First, it checks for a s-maxage directive, then a max-age directive, and then it falls + * back on an expires header. It returns null when no maximum age can be established. + * + * @return integer|null Number of seconds + * + * @api + */ + public function getMaxAge() + { + if ($age = $this->headers->getCacheControlDirective('s-maxage')) { + return $age; + } + + if ($age = $this->headers->getCacheControlDirective('max-age')) { + return $age; + } + + if (null !== $this->getExpires()) { + return $this->getExpires()->format('U') - $this->getDate()->format('U'); + } + + return null; + } + + /** + * Sets the number of seconds after which the response should no longer be considered fresh. + * + * This methods sets the Cache-Control max-age directive. + * + * @param integer $value A number of seconds + * + * @api + */ + public function setMaxAge($value) + { + $this->headers->addCacheControlDirective('max-age', $value); + } + + /** + * Sets the number of seconds after which the response should no longer be considered fresh by shared caches. + * + * This methods sets the Cache-Control s-maxage directive. + * + * @param integer $value A number of seconds + * + * @api + */ + public function setSharedMaxAge($value) + { + $this->setPublic(); + $this->headers->addCacheControlDirective('s-maxage', $value); + } + + /** + * Returns the response's time-to-live in seconds. + * + * It returns null when no freshness information is present in the response. + * + * When the responses TTL is <= 0, the response may not be served from cache without first + * revalidating with the origin. + * + * @return integer The TTL in seconds + * + * @api + */ + public function getTtl() + { + if ($maxAge = $this->getMaxAge()) { + return $maxAge - $this->getAge(); + } + + return null; + } + + /** + * Sets the response's time-to-live for shared caches. + * + * This method adjusts the Cache-Control/s-maxage directive. + * + * @param integer $seconds The number of seconds + * + * @api + */ + public function setTtl($seconds) + { + $this->setSharedMaxAge($this->getAge() + $seconds); + } + + /** + * Sets the response's time-to-live for private/client caches. + * + * This method adjusts the Cache-Control/max-age directive. + * + * @param integer $seconds The number of seconds + * + * @api + */ + public function setClientTtl($seconds) + { + $this->setMaxAge($this->getAge() + $seconds); + } + + /** + * Returns the Last-Modified HTTP header as a DateTime instance. + * + * @return \DateTime A DateTime instance + * + * @api + */ + public function getLastModified() + { + return $this->headers->getDate('Last-Modified'); + } + + /** + * Sets the Last-Modified HTTP header with a \DateTime instance. + * + * If passed a null value, it removes the header. + * + * @param \DateTime $date A \DateTime instance + * + * @api + */ + public function setLastModified(\DateTime $date = null) + { + if (null === $date) { + $this->headers->remove('Last-Modified'); + } else { + $date = clone $date; + $date->setTimezone(new \DateTimeZone('UTC')); + $this->headers->set('Last-Modified', $date->format('D, d M Y H:i:s').' GMT'); + } + } + + /** + * Returns the literal value of ETag HTTP header. + * + * @return string The ETag HTTP header + * + * @api + */ + public function getEtag() + { + return $this->headers->get('ETag'); + } + + /** + * Sets the ETag value. + * + * @param string $etag The ETag unique identifier + * @param Boolean $weak Whether you want a weak ETag or not + * + * @api + */ + public function setEtag($etag = null, $weak = false) + { + if (null === $etag) { + $this->headers->remove('Etag'); + } else { + if (0 !== strpos($etag, '"')) { + $etag = '"'.$etag.'"'; + } + + $this->headers->set('ETag', (true === $weak ? 'W/' : '').$etag); + } + } + + /** + * Sets Response cache headers (validation and/or expiration). + * + * Available options are: etag, last_modified, max_age, s_maxage, private, and public. + * + * @param array $options An array of cache options + * + * @api + */ + public function setCache(array $options) + { + if ($diff = array_diff(array_keys($options), array('etag', 'last_modified', 'max_age', 's_maxage', 'private', 'public'))) { + throw new \InvalidArgumentException(sprintf('Response does not support the following options: "%s".', implode('", "', array_keys($diff)))); + } + + if (isset($options['etag'])) { + $this->setEtag($options['etag']); + } + + if (isset($options['last_modified'])) { + $this->setLastModified($options['last_modified']); + } + + if (isset($options['max_age'])) { + $this->setMaxAge($options['max_age']); + } + + if (isset($options['s_maxage'])) { + $this->setSharedMaxAge($options['s_maxage']); + } + + if (isset($options['public'])) { + if ($options['public']) { + $this->setPublic(); + } else { + $this->setPrivate(); + } + } + + if (isset($options['private'])) { + if ($options['private']) { + $this->setPrivate(); + } else { + $this->setPublic(); + } + } + } + + /** + * Modifies the response so that it conforms to the rules defined for a 304 status code. + * + * This sets the status, removes the body, and discards any headers + * that MUST NOT be included in 304 responses. + * + * @see http://tools.ietf.org/html/rfc2616#section-10.3.5 + * + * @api + */ + public function setNotModified() + { + $this->setStatusCode(304); + $this->setContent(null); + + // remove headers that MUST NOT be included with 304 Not Modified responses + foreach (array('Allow', 'Content-Encoding', 'Content-Language', 'Content-Length', 'Content-MD5', 'Content-Type', 'Last-Modified') as $header) { + $this->headers->remove($header); + } + } + + /** + * Returns true if the response includes a Vary header. + * + * @return true if the response includes a Vary header, false otherwise + * + * @api + */ + public function hasVary() + { + return (Boolean) $this->headers->get('Vary'); + } + + /** + * Returns an array of header names given in the Vary header. + * + * @return array An array of Vary names + * + * @api + */ + public function getVary() + { + if (!$vary = $this->headers->get('Vary')) { + return array(); + } + + return is_array($vary) ? $vary : preg_split('/[\s,]+/', $vary); + } + + /** + * Sets the Vary header. + * + * @param string|array $headers + * @param Boolean $replace Whether to replace the actual value of not (true by default) + * + * @api + */ + public function setVary($headers, $replace = true) + { + $this->headers->set('Vary', $headers, $replace); + } + + /** + * Determines if the Response validators (ETag, Last-Modified) matches + * a conditional value specified in the Request. + * + * If the Response is not modified, it sets the status code to 304 and + * remove the actual content by calling the setNotModified() method. + * + * @param Request $request A Request instance + * + * @return Boolean true if the Response validators matches the Request, false otherwise + * + * @api + */ + public function isNotModified(Request $request) + { + $lastModified = $request->headers->get('If-Modified-Since'); + $notModified = false; + if ($etags = $request->getEtags()) { + $notModified = (in_array($this->getEtag(), $etags) || in_array('*', $etags)) && (!$lastModified || $this->headers->get('Last-Modified') == $lastModified); + } elseif ($lastModified) { + $notModified = $lastModified == $this->headers->get('Last-Modified'); + } + + if ($notModified) { + $this->setNotModified(); + } + + return $notModified; + } + + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html + /** + * @api + */ + public function isInvalid() + { + return $this->statusCode < 100 || $this->statusCode >= 600; + } + + /** + * @api + */ + public function isInformational() + { + return $this->statusCode >= 100 && $this->statusCode < 200; + } + + /** + * @api + */ + public function isSuccessful() + { + return $this->statusCode >= 200 && $this->statusCode < 300; + } + + /** + * @api + */ + public function isRedirection() + { + return $this->statusCode >= 300 && $this->statusCode < 400; + } + + /** + * @api + */ + public function isClientError() + { + return $this->statusCode >= 400 && $this->statusCode < 500; + } + + /** + * @api + */ + public function isServerError() + { + return $this->statusCode >= 500 && $this->statusCode < 600; + } + + /** + * @api + */ + public function isOk() + { + return 200 === $this->statusCode; + } + + /** + * @api + */ + public function isForbidden() + { + return 403 === $this->statusCode; + } + + /** + * @api + */ + public function isNotFound() + { + return 404 === $this->statusCode; + } + + /** + * @api + */ + public function isRedirect($location = null) + { + return in_array($this->statusCode, array(201, 301, 302, 303, 307)) && (null === $location ?: $location == $this->headers->get('Location')); + } + + /** + * @api + */ + public function isEmpty() + { + return in_array($this->statusCode, array(201, 204, 304)); + } +}