sfWebResponse.class.php 20 KB

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