1<?php
2/**
3 * Zend Framework
4 *
5 * LICENSE
6 *
7 * This source file is subject to the new BSD license that is bundled
8 * with this package in the file LICENSE.txt.
9 * It is also available through the world-wide-web at this URL:
10 * http://framework.zend.com/license/new-bsd
11 * If you did not receive a copy of the license and are unable to
12 * obtain it through the world-wide-web, please send an email
13 * to license@zend.com so we can send you a copy immediately.
14 *
15 * @category   Zend
16 * @package    Zend_Controller
17 * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
18 * @license    http://framework.zend.com/license/new-bsd     New BSD License
19 * @version    $Id$
20 */
21
22/**
23 * Zend_Controller_Response_Abstract
24 *
25 * Base class for Zend_Controller responses
26 *
27 * @package Zend_Controller
28 * @subpackage Response
29 * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
30 * @license    http://framework.zend.com/license/new-bsd     New BSD License
31 */
32abstract class Zend_Controller_Response_Abstract
33{
34    /**
35     * Body content
36     * @var array
37     */
38    protected $_body = array();
39
40    /**
41     * Exception stack
42     * @var Exception
43     */
44    protected $_exceptions = array();
45
46    /**
47     * Array of headers. Each header is an array with keys 'name' and 'value'
48     * @var array
49     */
50    protected $_headers = array();
51
52    /**
53     * Array of raw headers. Each header is a single string, the entire header to emit
54     * @var array
55     */
56    protected $_headersRaw = array();
57
58    /**
59     * HTTP response code to use in headers
60     * @var int
61     */
62    protected $_httpResponseCode = 200;
63
64    /**
65     * Flag; is this response a redirect?
66     * @var boolean
67     */
68    protected $_isRedirect = false;
69
70    /**
71     * Whether or not to render exceptions; off by default
72     * @var boolean
73     */
74    protected $_renderExceptions = false;
75
76    /**
77     * Flag; if true, when header operations are called after headers have been
78     * sent, an exception will be raised; otherwise, processing will continue
79     * as normal. Defaults to true.
80     *
81     * @see canSendHeaders()
82     * @var boolean
83     */
84    public $headersSentThrowsException = true;
85
86    /**
87     * Normalize a header name
88     *
89     * Normalizes a header name to X-Capitalized-Names
90     *
91     * @param  string $name
92     * @return string
93     */
94    protected function _normalizeHeader($name)
95    {
96        $filtered = str_replace(array('-', '_'), ' ', (string) $name);
97        $filtered = ucwords(strtolower($filtered));
98        $filtered = str_replace(' ', '-', $filtered);
99        return $filtered;
100    }
101
102    /**
103     * Set a header
104     *
105     * If $replace is true, replaces any headers already defined with that
106     * $name.
107     *
108     * @param string $name
109     * @param string $value
110     * @param boolean $replace
111     * @return Zend_Controller_Response_Abstract
112     */
113    public function setHeader($name, $value, $replace = false)
114    {
115        $this->canSendHeaders(true);
116        $name  = $this->_normalizeHeader($name);
117        $value = (string) $value;
118
119        if ($replace) {
120            foreach ($this->_headers as $key => $header) {
121                if ($name == $header['name']) {
122                    unset($this->_headers[$key]);
123                }
124            }
125        }
126
127        $this->_headers[] = array(
128            'name'    => $name,
129            'value'   => $value,
130            'replace' => $replace
131        );
132
133        return $this;
134    }
135
136    /**
137     * Set redirect URL
138     *
139     * Sets Location header and response code. Forces replacement of any prior
140     * redirects.
141     *
142     * @param string $url
143     * @param int $code
144     * @return Zend_Controller_Response_Abstract
145     */
146    public function setRedirect($url, $code = 302)
147    {
148        $this->canSendHeaders(true);
149        $this->setHeader('Location', $url, true)
150             ->setHttpResponseCode($code);
151
152        return $this;
153    }
154
155    /**
156     * Is this a redirect?
157     *
158     * @return boolean
159     */
160    public function isRedirect()
161    {
162        return $this->_isRedirect;
163    }
164
165    /**
166     * Return array of headers; see {@link $_headers} for format
167     *
168     * @return array
169     */
170    public function getHeaders()
171    {
172        return $this->_headers;
173    }
174
175    /**
176     * Clear headers
177     *
178     * @return Zend_Controller_Response_Abstract
179     */
180    public function clearHeaders()
181    {
182        $this->_headers = array();
183
184        return $this;
185    }
186
187    /**
188     * Clears the specified HTTP header
189     *
190     * @param  string $name
191     * @return Zend_Controller_Response_Abstract
192     */
193    public function clearHeader($name)
194    {
195        if (! count($this->_headers)) {
196            return $this;
197        }
198
199        foreach ($this->_headers as $index => $header) {
200            if ($name == $header['name']) {
201                unset($this->_headers[$index]);
202            }
203        }
204
205        return $this;
206    }
207
208    /**
209     * Set raw HTTP header
210     *
211     * Allows setting non key => value headers, such as status codes
212     *
213     * @param string $value
214     * @return Zend_Controller_Response_Abstract
215     */
216    public function setRawHeader($value)
217    {
218        $this->canSendHeaders(true);
219        if ('Location' == substr($value, 0, 8)) {
220            $this->_isRedirect = true;
221        }
222        $this->_headersRaw[] = (string) $value;
223        return $this;
224    }
225
226    /**
227     * Retrieve all {@link setRawHeader() raw HTTP headers}
228     *
229     * @return array
230     */
231    public function getRawHeaders()
232    {
233        return $this->_headersRaw;
234    }
235
236    /**
237     * Clear all {@link setRawHeader() raw HTTP headers}
238     *
239     * @return Zend_Controller_Response_Abstract
240     */
241    public function clearRawHeaders()
242    {
243        $this->_headersRaw = array();
244        return $this;
245    }
246
247    /**
248     * Clears the specified raw HTTP header
249     *
250     * @param  string $headerRaw
251     * @return Zend_Controller_Response_Abstract
252     */
253    public function clearRawHeader($headerRaw)
254    {
255        if (! count($this->_headersRaw)) {
256            return $this;
257        }
258
259        $key = array_search($headerRaw, $this->_headersRaw);
260        if ($key !== false) {
261            unset($this->_headersRaw[$key]);
262        }
263
264        return $this;
265    }
266
267    /**
268     * Clear all headers, normal and raw
269     *
270     * @return Zend_Controller_Response_Abstract
271     */
272    public function clearAllHeaders()
273    {
274        return $this->clearHeaders()
275                    ->clearRawHeaders();
276    }
277
278    /**
279     * Set HTTP response code to use with headers
280     *
281     * @param int $code
282     * @return Zend_Controller_Response_Abstract
283     */
284    public function setHttpResponseCode($code)
285    {
286        if (!is_int($code) || (100 > $code) || (599 < $code)) {
287            throw new Zend_Controller_Response_Exception('Invalid HTTP response code');
288        }
289
290        if ((300 <= $code) && (307 >= $code)) {
291            $this->_isRedirect = true;
292        } else {
293            $this->_isRedirect = false;
294        }
295
296        $this->_httpResponseCode = $code;
297        return $this;
298    }
299
300    /**
301     * Retrieve HTTP response code
302     *
303     * @return int
304     */
305    public function getHttpResponseCode()
306    {
307        return $this->_httpResponseCode;
308    }
309
310    /**
311     * Can we send headers?
312     *
313     * @param boolean $throw Whether or not to throw an exception if headers have been sent; defaults to false
314     * @return boolean
315     * @throws Zend_Controller_Response_Exception
316     */
317    public function canSendHeaders($throw = false)
318    {
319        $ok = headers_sent($file, $line);
320        if ($ok && $throw && $this->headersSentThrowsException) {
321            throw new Zend_Controller_Response_Exception('Cannot send headers; headers already sent in ' . $file . ', line ' . $line);
322        }
323
324        return !$ok;
325    }
326
327    /**
328     * Send all headers
329     *
330     * Sends any headers specified. If an {@link setHttpResponseCode() HTTP response code}
331     * has been specified, it is sent with the first header.
332     *
333     * @return Zend_Controller_Response_Abstract
334     */
335    public function sendHeaders()
336    {
337        // Only check if we can send headers if we have headers to send
338        if (count($this->_headersRaw) || count($this->_headers) || (200 != $this->_httpResponseCode)) {
339            $this->canSendHeaders(true);
340        } elseif (200 == $this->_httpResponseCode) {
341            // Haven't changed the response code, and we have no headers
342            return $this;
343        }
344
345        $httpCodeSent = false;
346
347        foreach ($this->_headersRaw as $header) {
348            if (!$httpCodeSent && $this->_httpResponseCode) {
349                header($header, true, $this->_httpResponseCode);
350                $httpCodeSent = true;
351            } else {
352                header($header);
353            }
354        }
355
356        foreach ($this->_headers as $header) {
357            if (!$httpCodeSent && $this->_httpResponseCode) {
358                header($header['name'] . ': ' . $header['value'], $header['replace'], $this->_httpResponseCode);
359                $httpCodeSent = true;
360            } else {
361                header($header['name'] . ': ' . $header['value'], $header['replace']);
362            }
363        }
364
365        if (!$httpCodeSent) {
366            header('HTTP/1.1 ' . $this->_httpResponseCode);
367            $httpCodeSent = true;
368        }
369
370        return $this;
371    }
372
373    /**
374     * Set body content
375     *
376     * If $name is not passed, or is not a string, resets the entire body and
377     * sets the 'default' key to $content.
378     *
379     * If $name is a string, sets the named segment in the body array to
380     * $content.
381     *
382     * @param string $content
383     * @param null|string $name
384     * @return Zend_Controller_Response_Abstract
385     */
386    public function setBody($content, $name = null)
387    {
388        if ((null === $name) || !is_string($name)) {
389            $this->_body = array('default' => (string) $content);
390        } else {
391            $this->_body[$name] = (string) $content;
392        }
393
394        return $this;
395    }
396
397    /**
398     * Append content to the body content
399     *
400     * @param string $content
401     * @param null|string $name
402     * @return Zend_Controller_Response_Abstract
403     */
404    public function appendBody($content, $name = null)
405    {
406        if ((null === $name) || !is_string($name)) {
407            if (isset($this->_body['default'])) {
408                $this->_body['default'] .= (string) $content;
409            } else {
410                return $this->append('default', $content);
411            }
412        } elseif (isset($this->_body[$name])) {
413            $this->_body[$name] .= (string) $content;
414        } else {
415            return $this->append($name, $content);
416        }
417
418        return $this;
419    }
420
421    /**
422     * Clear body array
423     *
424     * With no arguments, clears the entire body array. Given a $name, clears
425     * just that named segment; if no segment matching $name exists, returns
426     * false to indicate an error.
427     *
428     * @param  string $name Named segment to clear
429     * @return boolean
430     */
431    public function clearBody($name = null)
432    {
433        if (null !== $name) {
434            $name = (string) $name;
435            if (isset($this->_body[$name])) {
436                unset($this->_body[$name]);
437                return true;
438            }
439
440            return false;
441        }
442
443        $this->_body = array();
444        return true;
445    }
446
447    /**
448     * Return the body content
449     *
450     * If $spec is false, returns the concatenated values of the body content
451     * array. If $spec is boolean true, returns the body content array. If
452     * $spec is a string and matches a named segment, returns the contents of
453     * that segment; otherwise, returns null.
454     *
455     * @param boolean $spec
456     * @return string|array|null
457     */
458    public function getBody($spec = false)
459    {
460        if (false === $spec) {
461            ob_start();
462            $this->outputBody();
463            return ob_get_clean();
464        } elseif (true === $spec) {
465            return $this->_body;
466        } elseif (is_string($spec) && isset($this->_body[$spec])) {
467            return $this->_body[$spec];
468        }
469
470        return null;
471    }
472
473    /**
474     * Append a named body segment to the body content array
475     *
476     * If segment already exists, replaces with $content and places at end of
477     * array.
478     *
479     * @param string $name
480     * @param string $content
481     * @return Zend_Controller_Response_Abstract
482     */
483    public function append($name, $content)
484    {
485        if (!is_string($name)) {
486            throw new Zend_Controller_Response_Exception('Invalid body segment key ("' . gettype($name) . '")');
487        }
488
489        if (isset($this->_body[$name])) {
490            unset($this->_body[$name]);
491        }
492        $this->_body[$name] = (string) $content;
493        return $this;
494    }
495
496    /**
497     * Prepend a named body segment to the body content array
498     *
499     * If segment already exists, replaces with $content and places at top of
500     * array.
501     *
502     * @param string $name
503     * @param string $content
504     * @return void
505     */
506    public function prepend($name, $content)
507    {
508        if (!is_string($name)) {
509            throw new Zend_Controller_Response_Exception('Invalid body segment key ("' . gettype($name) . '")');
510        }
511
512        if (isset($this->_body[$name])) {
513            unset($this->_body[$name]);
514        }
515
516        $new = array($name => (string) $content);
517        $this->_body = $new + $this->_body;
518
519        return $this;
520    }
521
522    /**
523     * Insert a named segment into the body content array
524     *
525     * @param  string $name
526     * @param  string $content
527     * @param  string $parent
528     * @param  boolean $before Whether to insert the new segment before or
529     * after the parent. Defaults to false (after)
530     * @return Zend_Controller_Response_Abstract
531     */
532    public function insert($name, $content, $parent = null, $before = false)
533    {
534        if (!is_string($name)) {
535            throw new Zend_Controller_Response_Exception('Invalid body segment key ("' . gettype($name) . '")');
536        }
537
538        if ((null !== $parent) && !is_string($parent)) {
539            throw new Zend_Controller_Response_Exception('Invalid body segment parent key ("' . gettype($parent) . '")');
540        }
541
542        if (isset($this->_body[$name])) {
543            unset($this->_body[$name]);
544        }
545
546        if ((null === $parent) || !isset($this->_body[$parent])) {
547            return $this->append($name, $content);
548        }
549
550        $ins  = array($name => (string) $content);
551        $keys = array_keys($this->_body);
552        $loc  = array_search($parent, $keys);
553        if (!$before) {
554            // Increment location if not inserting before
555            ++$loc;
556        }
557
558        if (0 === $loc) {
559            // If location of key is 0, we're prepending
560            $this->_body = $ins + $this->_body;
561        } elseif ($loc >= (count($this->_body))) {
562            // If location of key is maximal, we're appending
563            $this->_body = $this->_body + $ins;
564        } else {
565            // Otherwise, insert at location specified
566            $pre  = array_slice($this->_body, 0, $loc);
567            $post = array_slice($this->_body, $loc);
568            $this->_body = $pre + $ins + $post;
569        }
570
571        return $this;
572    }
573
574    /**
575     * Echo the body segments
576     *
577     * @return void
578     */
579    public function outputBody()
580    {
581        $body = implode('', $this->_body);
582        echo $body;
583    }
584
585    /**
586     * Register an exception with the response
587     *
588     * @param Exception $e
589     * @return Zend_Controller_Response_Abstract
590     */
591    public function setException(Exception $e)
592    {
593        $this->_exceptions[] = $e;
594        return $this;
595    }
596
597    /**
598     * Retrieve the exception stack
599     *
600     * @return array
601     */
602    public function getException()
603    {
604        return $this->_exceptions;
605    }
606
607    /**
608     * Has an exception been registered with the response?
609     *
610     * @return boolean
611     */
612    public function isException()
613    {
614        return !empty($this->_exceptions);
615    }
616
617    /**
618     * Does the response object contain an exception of a given type?
619     *
620     * @param  string $type
621     * @return boolean
622     */
623    public function hasExceptionOfType($type)
624    {
625        foreach ($this->_exceptions as $e) {
626            if ($e instanceof $type) {
627                return true;
628            }
629        }
630
631        return false;
632    }
633
634    /**
635     * Does the response object contain an exception with a given message?
636     *
637     * @param  string $message
638     * @return boolean
639     */
640    public function hasExceptionOfMessage($message)
641    {
642        foreach ($this->_exceptions as $e) {
643            if ($message == $e->getMessage()) {
644                return true;
645            }
646        }
647
648        return false;
649    }
650
651    /**
652     * Does the response object contain an exception with a given code?
653     *
654     * @param  int $code
655     * @return boolean
656     */
657    public function hasExceptionOfCode($code)
658    {
659        $code = (int) $code;
660        foreach ($this->_exceptions as $e) {
661            if ($code == $e->getCode()) {
662                return true;
663            }
664        }
665
666        return false;
667    }
668
669    /**
670     * Retrieve all exceptions of a given type
671     *
672     * @param  string $type
673     * @return false|array
674     */
675    public function getExceptionByType($type)
676    {
677        $exceptions = array();
678        foreach ($this->_exceptions as $e) {
679            if ($e instanceof $type) {
680                $exceptions[] = $e;
681            }
682        }
683
684        if (empty($exceptions)) {
685            $exceptions = false;
686        }
687
688        return $exceptions;
689    }
690
691    /**
692     * Retrieve all exceptions of a given message
693     *
694     * @param  string $message
695     * @return false|array
696     */
697    public function getExceptionByMessage($message)
698    {
699        $exceptions = array();
700        foreach ($this->_exceptions as $e) {
701            if ($message == $e->getMessage()) {
702                $exceptions[] = $e;
703            }
704        }
705
706        if (empty($exceptions)) {
707            $exceptions = false;
708        }
709
710        return $exceptions;
711    }
712
713    /**
714     * Retrieve all exceptions of a given code
715     *
716     * @param mixed $code
717     * @return void
718     */
719    public function getExceptionByCode($code)
720    {
721        $code       = (int) $code;
722        $exceptions = array();
723        foreach ($this->_exceptions as $e) {
724            if ($code == $e->getCode()) {
725                $exceptions[] = $e;
726            }
727        }
728
729        if (empty($exceptions)) {
730            $exceptions = false;
731        }
732
733        return $exceptions;
734    }
735
736    /**
737     * Whether or not to render exceptions (off by default)
738     *
739     * If called with no arguments or a null argument, returns the value of the
740     * flag; otherwise, sets it and returns the current value.
741     *
742     * @param boolean $flag Optional
743     * @return boolean
744     */
745    public function renderExceptions($flag = null)
746    {
747        if (null !== $flag) {
748            $this->_renderExceptions = $flag ? true : false;
749        }
750
751        return $this->_renderExceptions;
752    }
753
754    /**
755     * Send the response, including all headers, rendering exceptions if so
756     * requested.
757     *
758     * @return void
759     */
760    public function sendResponse()
761    {
762        $this->sendHeaders();
763
764        if ($this->isException() && $this->renderExceptions()) {
765            $exceptions = '';
766            foreach ($this->getException() as $e) {
767                $exceptions .= $e->__toString() . "\n";
768            }
769            echo $exceptions;
770            return;
771        }
772
773        $this->outputBody();
774    }
775
776    /**
777     * Magic __toString functionality
778     *
779     * Proxies to {@link sendResponse()} and returns response value as string
780     * using output buffering.
781     *
782     * @return string
783     */
784    public function __toString()
785    {
786        ob_start();
787        $this->sendResponse();
788        return ob_get_clean();
789    }
790}
791