1. sfWebResponse.class.php
  2. /** * sfWebResponse class. * * This class manages web reponses. It supports cookies and headers management. * * @package symfony * @subpackage response * @author Fabien Potencier * @version SVN: $Id: sfWebResponse.class.php 24619 2009-11-30 23:14:18Z FabianLange $ */
  3. class sfWebResponse extends sfResponse
  4. {
  5. const
  6. FIRST = 'first',
  7. MIDDLE = '',
  8. LAST = 'last',
  9. ALL = 'ALL',
  10. RAW = 'RAW';
  11. protected
  12. $cookies = array(),
  13. $statusCode = 200,
  14. $statusText = 'OK',
  15. $headerOnly = false,
  16. $headers = array(),
  17. $metas = array(),
  18. $httpMetas = array(),
  19. $positions = array('first', '', 'last'),
  20. $stylesheets = array(),
  21. $javascripts = array(),
  22. $slots = array();
  23. static protected $statusTexts = array(
  24. '100' => 'Continue',
  25. '101' => 'Switching Protocols',
  26. '200' => 'OK',
  27. '201' => 'Created',
  28. '202' => 'Accepted',
  29. '203' => 'Non-Authoritative Information',
  30. '204' => 'No Content',
  31. '205' => 'Reset Content',
  32. '206' => 'Partial Content',
  33. '300' => 'Multiple Choices',
  34. '301' => 'Moved Permanently',
  35. '302' => 'Found',
  36. '303' => 'See Other',
  37. '304' => 'Not Modified',
  38. '305' => 'Use Proxy',
  39. '306' => '(Unused)',
  40. '307' => 'Temporary Redirect',
  41. '400' => 'Bad Request',
  42. '401' => 'Unauthorized',
  43. '402' => 'Payment Required',
  44. '403' => 'Forbidden',
  45. '404' => 'Not Found',
  46. '405' => 'Method Not Allowed',
  47. '406' => 'Not Acceptable',
  48. '407' => 'Proxy Authentication Required',
  49. '408' => 'Request Timeout',
  50. '409' => 'Conflict',
  51. '410' => 'Gone',
  52. '411' => 'Length Required',
  53. '412' => 'Precondition Failed',
  54. '413' => 'Request Entity Too Large',
  55. '414' => 'Request-URI Too Long',
  56. '415' => 'Unsupported Media Type',
  57. '416' => 'Requested Range Not Satisfiable',
  58. '417' => 'Expectation Failed',
  59. '500' => 'Internal Server Error',
  60. '501' => 'Not Implemented',
  61. '502' => 'Bad Gateway',
  62. '503' => 'Service Unavailable',
  63. '504' => 'Gateway Timeout',
  64. '505' => 'HTTP Version Not Supported',
  65. );
  66. /**
  67. * Initializes this sfWebResponse.
  68. *
  69. * Available options:
  70. *
  71. * * charset: The charset to use (utf-8 by default)
  72. * * content_type: The content type (text/html by default)
  73. * * send_http_headers: Whether to send HTTP headers or not (true by default)
  74. * * http_protocol: The HTTP protocol to use for the response (HTTP/1.0 by default)
  75. *
  76. * @param sfEventDispatcher $dispatcher An sfEventDispatcher instance
  77. * @param array $options An array of options
  78. *
  79. * @return bool true, if initialization completes successfully, otherwise false
  80. *
  81. * @throws <b>sfInitializationException</b> If an error occurs while initializing this sfResponse
  82. *
  83. * @see sfResponse
  84. */
  85. public function initialize(sfEventDispatcher $dispatcher, $options = array())
  86. {
  87. parent::initialize($dispatcher, $options);
  88. $this->javascripts = array_combine($this->positions, array_fill(0, count($this->positions), array()));
  89. $this->stylesheets = array_combine($this->positions, array_fill(0, count($this->positions), array()));
  90. if (!isset($this->options['charset']))
  91. {
  92. $this->options['charset'] = 'utf-8';
  93. }
  94. if (!isset($this->options['send_http_headers']))
  95. {
  96. $this->options['send_http_headers'] = true;
  97. }
  98. if (!isset($this->options['http_protocol']))
  99. {
  100. $this->options['http_protocol'] = 'HTTP/1.0';
  101. }
  102. $this->options['content_type'] = $this->fixContentType(isset($this->options['content_type']) ? $this->options['content_type'] : 'text/html');
  103. }
  104. /**
  105. * Sets if the response consist of just HTTP headers.
  106. *
  107. * @param bool $value
  108. */
  109. public function setHeaderOnly($value = true)
  110. {
  111. $this->headerOnly = (boolean) $value;
  112. }
  113. /**
  114. * Returns if the response must only consist of HTTP headers.
  115. *
  116. * @return bool returns true if, false otherwise
  117. */
  118. public function isHeaderOnly()
  119. {
  120. return $this->headerOnly;
  121. }
  122. /**
  123. * Sets a cookie.
  124. *
  125. * @param string $name HTTP header name
  126. * @param string $value Value for the cookie
  127. * @param string $expire Cookie expiration period
  128. * @param string $path Path
  129. * @param string $domain Domain name
  130. * @param bool $secure If secure
  131. * @param bool $httpOnly If uses only HTTP
  132. *
  133. * @throws <b>sfException</b> If fails to set the cookie
  134. */
  135. public function setCookie($name, $value, $expire = null, $path = '/', $domain = '', $secure = false, $httpOnly = false)
  136. {
  137. if ($expire !== null)
  138. {
  139. if (is_numeric($expire))
  140. {
  141. $expire = (int) $expire;
  142. }
  143. else
  144. {
  145. $expire = strtotime($expire);
  146. if ($expire === false || $expire == -1)
  147. {
  148. throw new sfException('Your expire parameter is not valid.');
  149. }
  150. }
  151. }
  152. $this->cookies[$name] = array(
  153. 'name' => $name,
  154. 'value' => $value,
  155. 'expire' => $expire,
  156. 'path' => $path,
  157. 'domain' => $domain,
  158. 'secure' => $secure ? true : false,
  159. 'httpOnly' => $httpOnly,
  160. );
  161. }
  162. /**
  163. * Sets response status code.
  164. *
  165. * @param string $code HTTP status code
  166. * @param string $name HTTP status text
  167. *
  168. */
  169. public function setStatusCode($code, $name = null)
  170. {
  171. $this->statusCode = $code;
  172. $this->statusText = null !== $name ? $name : self::$statusTexts[$code];
  173. }
  174. /**
  175. * Retrieves status text for the current web response.
  176. *
  177. * @return string Status text
  178. */
  179. public function getStatusText()
  180. {
  181. return $this->statusText;
  182. }
  183. /**
  184. * Retrieves status code for the current web response.
  185. *
  186. * @return integer Status code
  187. */
  188. public function getStatusCode()
  189. {
  190. return $this->statusCode;
  191. }
  192. /**
  193. * Sets a HTTP header.
  194. *
  195. * @param string $name HTTP header name
  196. * @param string $value Value (if null, remove the HTTP header)
  197. * @param bool $replace Replace for the value
  198. *
  199. */
  200. public function setHttpHeader($name, $value, $replace = true)
  201. {
  202. $name = $this->normalizeHeaderName($name);
  203. if (null === $value)
  204. {
  205. unset($this->headers[$name]);
  206. return;
  207. }
  208. if ('Content-Type' == $name)
  209. {
  210. if ($replace || !$this->getHttpHeader('Content-Type', null))
  211. {
  212. $this->setContentType($value);
  213. }
  214. return;
  215. }
  216. if (!$replace)
  217. {
  218. $current = isset($this->headers[$name]) ? $this->headers[$name] : '';
  219. $value = ($current ? $current.', ' : '').$value;
  220. }
  221. $this->headers[$name] = $value;
  222. }
  223. /**
  224. * Gets HTTP header current value.
  225. *
  226. * @param string $name HTTP header name
  227. * @param string $default Default value returned if named HTTP header is not found
  228. *
  229. * @return string
  230. */
  231. public function getHttpHeader($name, $default = null)
  232. {
  233. $name = $this->normalizeHeaderName($name);
  234. return isset($this->headers[$name]) ? $this->headers[$name] : $default;
  235. }
  236. /**
  237. * Checks if response has given HTTP header.
  238. *
  239. * @param string $name HTTP header name
  240. *
  241. * @return bool
  242. */
  243. public function hasHttpHeader($name)
  244. {
  245. return array_key_exists($this->normalizeHeaderName($name), $this->headers);
  246. }
  247. /**
  248. * Sets response content type.
  249. *
  250. * @param string $value Content type
  251. *
  252. */
  253. public function setContentType($value)
  254. {
  255. $this->headers['Content-Type'] = $this->fixContentType($value);
  256. }
  257. /**
  258. * Gets the current charset as defined by the content type.
  259. *
  260. * @return string The current charset
  261. */
  262. public function getCharset()
  263. {
  264. return $this->options['charset'];
  265. }
  266. /**
  267. * Gets response content type.
  268. *
  269. * @return array
  270. */
  271. public function getContentType()
  272. {
  273. return $this->getHttpHeader('Content-Type', $this->options['content_type']);
  274. }
  275. /**
  276. * Sends HTTP headers and cookies. Only the first invocation of this method will send the headers.
  277. * Subsequent invocations will silently do nothing. This allows certain actions to send headers early,
  278. * while still using the standard controller.
  279. */
  280. public function sendHttpHeaders()
  281. {
  282. if (!$this->options['send_http_headers'])
  283. {
  284. return;
  285. }
  286. // status
  287. $status = $this->options['http_protocol'].' '.$this->statusCode.' '.$this->statusText;
  288. header($status);
  289. if (substr(php_sapi_name(), 0, 3) == 'cgi')
  290. {
  291. // fastcgi servers cannot send this status information because it was sent by them already due to the HTT/1.0 line
  292. // so we can safely unset them. see ticket #3191
  293. unset($this->headers['Status']);
  294. }
  295. if ($this->options['logging'])
  296. {
  297. $this->dispatcher->notify(new sfEvent($this, 'application.log', array(sprintf('Send status "%s"', $status))));
  298. }
  299. // headers
  300. if (!$this->getHttpHeader('Content-Type'))
  301. {
  302. $this->setContentType($this->options['content_type']);
  303. }
  304. foreach ($this->headers as $name => $value)
  305. {
  306. header($name.': '.$value);
  307. if ($value != '' && $this->options['logging'])
  308. {
  309. $this->dispatcher->notify(new sfEvent($this, 'application.log', array(sprintf('Send header "%s: %s"', $name, $value))));
  310. }
  311. }
  312. // cookies
  313. foreach ($this->cookies as $cookie)
  314. {
  315. setrawcookie($cookie['name'], $cookie['value'], $cookie['expire'], $cookie['path'], $cookie['domain'], $cookie['secure'], $cookie['httpOnly']);
  316. if ($this->options['logging'])
  317. {
  318. $this->dispatcher->notify(new sfEvent($this, 'application.log', array(sprintf('Send cookie "%s": "%s"', $cookie['name'], $cookie['value']))));
  319. }
  320. }
  321. // prevent resending the headers
  322. $this->options['send_http_headers'] = false;
  323. }
  324. /**
  325. * Send content for the current web response.
  326. *
  327. */
  328. public function sendContent()
  329. {
  330. if (!$this->headerOnly)
  331. {
  332. parent::sendContent();
  333. }
  334. }
  335. /**
  336. * Sends the HTTP headers and the content.
  337. */
  338. public function send()
  339. {
  340. $this->sendHttpHeaders();
  341. $this->sendContent();
  342. }
  343. /**
  344. * Retrieves a normalized Header.
  345. *
  346. * @param string $name Header name
  347. *
  348. * @return string Normalized header
  349. */
  350. protected function normalizeHeaderName($name)
  351. {
  352. return preg_replace('/\-(.)/e', "'-'.strtoupper('\\1')", strtr(ucfirst(strtolower($name)), '_', '-'));
  353. }
  354. /**
  355. * Retrieves a formated date.
  356. *
  357. * @param string $timestamp Timestamp
  358. * @param string $type Format type
  359. *
  360. * @return string Formatted date
  361. */
  362. static public function getDate($timestamp, $type = 'rfc1123')
  363. {
  364. $type = strtolower($type);
  365. if ($type == 'rfc1123')
  366. {
  367. return substr(gmdate('r', $timestamp), 0, -5).'GMT';
  368. }
  369. else if ($type == 'rfc1036')
  370. {
  371. return gmdate('l, d-M-y H:i:s ', $timestamp).'GMT';
  372. }
  373. else if ($type == 'asctime')
  374. {
  375. return gmdate('D M j H:i:s', $timestamp);
  376. }
  377. else
  378. {
  379. throw new InvalidArgumentException('The second getDate() method parameter must be one of: rfc1123, rfc1036 or asctime.');
  380. }
  381. }
  382. /**
  383. * Adds vary to a http header.
  384. *
  385. * @param string $header HTTP header
  386. */
  387. public function addVaryHttpHeader($header)
  388. {
  389. $vary = $this->getHttpHeader('Vary');
  390. $currentHeaders = array();
  391. if ($vary)
  392. {
  393. $currentHeaders = preg_split('/\s*,\s*/', $vary);
  394. }
  395. $header = $this->normalizeHeaderName($header);
  396. if (!in_array($header, $currentHeaders))
  397. {
  398. $currentHeaders[] = $header;
  399. $this->setHttpHeader('Vary', implode(', ', $currentHeaders));
  400. }
  401. }
  402. /**
  403. * Adds an control cache http header.
  404. *
  405. * @param string $name HTTP header
  406. * @param string $value Value for the http header
  407. */
  408. public function addCacheControlHttpHeader($name, $value = null)
  409. {
  410. $cacheControl = $this->getHttpHeader('Cache-Control');
  411. $currentHeaders = array();
  412. if ($cacheControl)
  413. {
  414. foreach (preg_split('/\s*,\s*/', $cacheControl) as $tmp)
  415. {
  416. $tmp = explode('=', $tmp);
  417. $currentHeaders[$tmp[0]] = isset($tmp[1]) ? $tmp[1] : null;
  418. }
  419. }
  420. $currentHeaders[strtr(strtolower($name), '_', '-')] = $value;
  421. $headers = array();
  422. foreach ($currentHeaders as $key => $value)
  423. {
  424. $headers[] = $key.(null !== $value ? '='.$value : '');
  425. }
  426. $this->setHttpHeader('Cache-Control', implode(', ', $headers));
  427. }
  428. /**
  429. * Retrieves meta headers for the current web response.
  430. *
  431. * @return string Meta headers
  432. */
  433. public function getHttpMetas()
  434. {
  435. return $this->httpMetas;
  436. }
  437. /**
  438. * Adds a HTTP meta header.
  439. *
  440. * @param string $key Key to replace
  441. * @param string $value HTTP meta header value (if null, remove the HTTP meta)
  442. * @param bool $replace Replace or not
  443. */
  444. public function addHttpMeta($key, $value, $replace = true)
  445. {
  446. $key = $this->normalizeHeaderName($key);
  447. // set HTTP header
  448. $this->setHttpHeader($key, $value, $replace);
  449. if (null === $value)
  450. {
  451. unset($this->httpMetas[$key]);
  452. return;
  453. }
  454. if ('Content-Type' == $key)
  455. {
  456. $value = $this->getContentType();
  457. }
  458. elseif (!$replace)
  459. {
  460. $current = isset($this->httpMetas[$key]) ? $this->httpMetas[$key] : '';
  461. $value = ($current ? $current.', ' : '').$value;
  462. }
  463. $this->httpMetas[$key] = $value;
  464. }
  465. /**
  466. * Retrieves all meta headers.
  467. *
  468. * @return array List of meta headers
  469. */
  470. public function getMetas()
  471. {
  472. return $this->metas;
  473. }
  474. /**
  475. * Adds a meta header.
  476. *
  477. * @param string $key Name of the header
  478. * @param string $value Meta header value (if null, remove the meta)
  479. * @param bool $replace true if it's replaceable
  480. * @param bool $escape true for escaping the header
  481. */
  482. public function addMeta($key, $value, $replace = true, $escape = true)
  483. {
  484. $key = strtolower($key);
  485. if (null === $value)
  486. {
  487. unset($this->metas[$key]);
  488. return;
  489. }
  490. // FIXME: If you use the i18n layer and escape the data here, it won't work
  491. // see include_metas() in AssetHelper
  492. if ($escape)
  493. {
  494. $value = htmlspecialchars($value, ENT_QUOTES, $this->options['charset']);
  495. }
  496. $current = isset($this->metas[$key]) ? $this->metas[$key] : null;
  497. if ($replace || !$current)
  498. {
  499. $this->metas[$key] = $value;
  500. }
  501. }
  502. /**
  503. * Retrieves title for the current web response.
  504. *
  505. * @return string Title
  506. */
  507. public function getTitle()
  508. {
  509. return isset($this->metas['title']) ? $this->metas['title'] : '';
  510. }
  511. /**
  512. * Sets title for the current web response.
  513. *
  514. * @param string $title Title name
  515. * @param bool $escape true, for escaping the title
  516. */
  517. public function setTitle($title, $escape = true)
  518. {
  519. $this->addMeta('title', $title, true, $escape);
  520. }
  521. /**
  522. * Returns the available position names for stylesheets and javascripts in order.
  523. *
  524. * @return array An array of position names
  525. */
  526. public function getPositions()
  527. {
  528. return $this->positions;
  529. }
  530. /**
  531. * Retrieves stylesheets for the current web response.
  532. *
  533. * By default, the position is sfWebResponse::ALL,
  534. * and the method returns all stylesheets ordered by position.
  535. *
  536. * @param string $position The position
  537. *
  538. * @return array An associative array of stylesheet files as keys and options as values
  539. */
  540. public function getStylesheets($position = self::ALL)
  541. {
  542. if (self::ALL === $position)
  543. {
  544. $stylesheets = array();
  545. foreach ($this->getPositions() as $position)
  546. {
  547. foreach ($this->stylesheets[$position] as $file => $options)
  548. {
  549. $stylesheets[$file] = $options;
  550. }
  551. }
  552. return $stylesheets;
  553. }
  554. else if (self::RAW === $position)
  555. {
  556. return $this->stylesheets;
  557. }
  558. $this->validatePosition($position);
  559. return $this->stylesheets[$position];
  560. }
  561. /**
  562. * Adds a stylesheet to the current web response.
  563. *
  564. * @param string $file The stylesheet file
  565. * @param string $position Position
  566. * @param string $options Stylesheet options
  567. */
  568. public function addStylesheet($file, $position = '', $options = array())
  569. {
  570. $this->validatePosition($position);
  571. $this->stylesheets[$position][$file] = $options;
  572. }
  573. /**
  574. * Removes a stylesheet from the current web response.
  575. *
  576. * @param string $file The stylesheet file to remove
  577. */
  578. public function removeStylesheet($file)
  579. {
  580. foreach ($this->getPositions() as $position)
  581. {
  582. unset($this->stylesheets[$position][$file]);
  583. }
  584. }
  585. /**
  586. * Retrieves javascript files from the current web response.
  587. *
  588. * By default, the position is sfWebResponse::ALL,
  589. * and the method returns all javascripts ordered by position.
  590. *
  591. * @param string $position The position
  592. *
  593. * @return array An associative array of javascript files as keys and options as values
  594. */
  595. public function getJavascripts($position = self::ALL)
  596. {
  597. if (self::ALL === $position)
  598. {
  599. $javascripts = array();
  600. foreach ($this->getPositions() as $position)
  601. {
  602. foreach ($this->javascripts[$position] as $file => $options)
  603. {
  604. $javascripts[$file] = $options;
  605. }
  606. }
  607. return $javascripts;
  608. }
  609. else if (self::RAW === $position)
  610. {
  611. return $this->javascripts;
  612. }
  613. $this->validatePosition($position);
  614. return $this->javascripts[$position];
  615. }
  616. /**
  617. * Adds javascript code to the current web response.
  618. *
  619. * @param string $file The JavaScript file
  620. * @param string $position Position
  621. * @param string $options Javascript options
  622. */
  623. public function addJavascript($file, $position = '', $options = array())
  624. {
  625. $this->validatePosition($position);
  626. $this->javascripts[$position][$file] = $options;
  627. }
  628. /**
  629. * Removes a JavaScript file from the current web response.
  630. *
  631. * @param string $file The Javascript file to remove
  632. */
  633. public function removeJavascript($file)
  634. {
  635. foreach ($this->getPositions() as $position)
  636. {
  637. unset($this->javascripts[$position][$file]);
  638. }
  639. }
  640. /**
  641. * Retrieves slots from the current web response.
  642. *
  643. * @return string Javascript code
  644. */
  645. public function getSlots()
  646. {
  647. return $this->slots;
  648. }
  649. /**
  650. * Sets a slot content.
  651. *
  652. * @param string $name Slot name
  653. * @param string $content Content
  654. */
  655. public function setSlot($name, $content)
  656. {
  657. $this->slots[$name] = $content;
  658. }
  659. /**
  660. * Retrieves cookies from the current web response.
  661. *
  662. * @return array Cookies
  663. */
  664. public function getCookies()
  665. {
  666. return $this->cookies;
  667. }
  668. /**
  669. * Retrieves HTTP headers from the current web response.
  670. *
  671. * @return string HTTP headers
  672. */
  673. public function getHttpHeaders()
  674. {
  675. return $this->headers;
  676. }
  677. /**
  678. * Cleans HTTP headers from the current web response.
  679. */
  680. public function clearHttpHeaders()
  681. {
  682. $this->headers = array();
  683. }
  684. /**
  685. * Copies all properties from a given sfWebResponse object to the current one.
  686. *
  687. * @param sfWebResponse $response An sfWebResponse instance
  688. */
  689. public function copyProperties(sfWebResponse $response)
  690. {
  691. $this->options = $response->getOptions();
  692. $this->headers = $response->getHttpHeaders();
  693. $this->metas = $response->getMetas();
  694. $this->httpMetas = $response->getHttpMetas();
  695. $this->stylesheets = $response->getStylesheets(self::RAW);
  696. $this->javascripts = $response->getJavascripts(self::RAW);
  697. $this->slots = $response->getSlots();
  698. }
  699. /**
  700. * Merges all properties from a given sfWebResponse object to the current one.
  701. *
  702. * @param sfWebResponse $response An sfWebResponse instance
  703. */
  704. public function merge(sfWebResponse $response)
  705. {
  706. foreach ($this->getPositions() as $position)
  707. {
  708. $this->javascripts[$position] = array_merge($this->getJavascripts($position), $response->getJavascripts($position));
  709. $this->stylesheets[$position] = array_merge($this->getStylesheets($position), $response->getStylesheets($position));
  710. }
  711. $this->slots = array_merge($this->getSlots(), $response->getSlots());
  712. }
  713. /**
  714. * @see sfResponse
  715. */
  716. public function serialize()
  717. {
  718. return serialize(array($this->content, $this->statusCode, $this->statusText, $this->options, $this->cookies, $this->headerOnly, $this->headers, $this->metas, $this->httpMetas, $this->stylesheets, $this->javascripts, $this->slots));
  719. }
  720. /**
  721. * @see sfResponse
  722. */
  723. public function unserialize($serialized)
  724. {
  725. list($this->content, $this->statusCode, $this->statusText, $this->options, $this->cookies, $this->headerOnly, $this->headers, $this->metas, $this->httpMetas, $this->stylesheets, $this->javascripts, $this->slots) = unserialize($serialized);
  726. }
  727. /**
  728. * Validate a position name.
  729. *
  730. * @param string $position
  731. *
  732. * @throws InvalidArgumentException if the position is not available
  733. */
  734. protected function validatePosition($position)
  735. {
  736. if (!in_array($position, $this->positions, true))
  737. {
  738. throw new InvalidArgumentException(sprintf('The position "%s" does not exist (available positions: %s).', $position, implode(', ', $this->positions)));
  739. }
  740. }
  741. /**
  742. * Fixes the content type by adding the charset for text content types.
  743. *
  744. * @param string $contentType The content type
  745. *
  746. * @return string The content type with the charset if needed
  747. */
  748. protected function fixContentType($contentType)
  749. {
  750. // add charset if needed (only on text content)
  751. if (false === stripos($contentType, 'charset') && (0 === stripos($contentType, 'text/') || strlen($contentType) - 3 === strripos($contentType, 'xml')))
  752. {
  753. $contentType .= '; charset='.$this->options['charset'];
  754. }
  755. // change the charset for the response
  756. if (preg_match('/charset\s*=\s*(.+)\s*$/', $contentType, $match))
  757. {
  758. $this->options['charset'] = $match[1];
  759. }
  760. return $contentType;
  761. }
  762. }

Debug toolbar