Development

/branches/1.4/lib/response/sfWebResponse.class.php

You must first sign up to be able to contribute.

root/branches/1.4/lib/response/sfWebResponse.class.php

Revision 31399, 22.1 kB (checked in by fabien, 3 years ago)

[1.3, 1.4] fixed sfViewCacheManager returns incorrect cached http_protocol (closes #9254)

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