* @package WB */ WBClass::load( 'WBLog' , 'WBClock' , 'WBRequest' ); /** * Generic response class * * * * @version 1.0.0 * @package WB */ abstract class WBResponse extends WBStdClass { /** * response header * @var array */ protected $_header = array(); /** * response data * @var array */ protected $_data = array(); /** * the most recent checksum * @var string */ protected $_checksum = ''; /** * filters, that be applied on output * @var array */ protected $_filters = array(); /** * whether caching features should be used * @var bool */ protected $_useCache = true; /** * HTTP-a-like status code for this response object * * Default value is 200 - HTTP-OK * @var int $responsestatus */ protected $_responseStatus = 200; /** * maximum time to cache response * @var int */ protected $expireAge = 3600; /** * cache control header directive */ const CACHECONTROL_PRIVATE = 'private'; const CACHECONTROL_PUBLIC = 'public'; const CACHECONTROL_MUSTREVALIDATE = 'must-revalidate'; /** * cache control * @var string */ protected $cacheControl = self::CACHECONTROL_PRIVATE; /** * Response objects use HTTP style status codes * * Default value is 200 - OK * @var array $_statusCodes */ protected $_statusCodes = array( 100 => array( 'body' => true, 'msg' => 'Continue' ), 101 => array( 'body' => true, 'msg' => 'Switching Protocols' ), 200 => array( 'body' => true, 'msg' => 'OK' ), 201 => array( 'body' => true, 'msg' => 'Created' ), 202 => array( 'body' => true, 'msg' => 'Accepted' ), 203 => array( 'body' => true, 'msg' => 'Non-Authoritative Information' ), 204 => array( 'body' => true, 'msg' => 'No Content' ), 205 => array( 'body' => true, 'msg' => 'Reset Content' ), 206 => array( 'body' => true, 'msg' => 'Partial Content' ), 300 => array( 'body' => true, 'msg' => 'Multiple Choices' ), 301 => array( 'body' => false, 'msg' => 'Moved Permanently' ), 302 => array( 'body' => false, 'msg' => 'Found' ), 303 => array( 'body' => true, 'msg' => 'See Other' ), 304 => array( 'body' => false, 'msg' => 'Not Modified' ), 305 => array( 'body' => true, 'msg' => 'Use Proxy' ), 306 => array( 'body' => true, 'msg' => '(Unused)' ), 307 => array( 'body' => true, 'msg' => 'Temporary Redirect' ), 400 => array( 'body' => true, 'msg' => 'Bad Request' ), 401 => array( 'body' => true, 'msg' => 'Unauthorized' ), 402 => array( 'body' => true, 'msg' => 'Payment Required' ), 403 => array( 'body' => true, 'msg' => 'Forbidden' ), 404 => array( 'body' => true, 'msg' => 'Not Found' ), 405 => array( 'body' => true, 'msg' => 'Method Not Allowed' ), 406 => array( 'body' => true, 'msg' => 'Not Acceptable' ), 407 => array( 'body' => true, 'msg' => 'Proxy Authentication Required' ), 408 => array( 'body' => true, 'msg' => 'Request Timeout' ), 409 => array( 'body' => true, 'msg' => 'Conflict' ), 410 => array( 'body' => true, 'msg' => 'Gone' ), 411 => array( 'body' => true, 'msg' => 'Length Required' ), 412 => array( 'body' => true, 'msg' => 'Precondition Failed' ), 413 => array( 'body' => true, 'msg' => 'Request Entity Too Large' ), 414 => array( 'body' => true, 'msg' => 'Request-URI Too Long' ), 415 => array( 'body' => true, 'msg' => 'Unsupported Media Type' ), 416 => array( 'body' => true, 'msg' => 'Requested Range Not Satisfiable' ), 417 => array( 'body' => true, 'msg' => 'Expectation Failed' ), 500 => array( 'body' => true, 'msg' => 'Internal Server Error' ), 501 => array( 'body' => true, 'msg' => 'Not Implemented' ), 502 => array( 'body' => true, 'msg' => 'Bad Gateway' ), 503 => array( 'body' => true, 'msg' => 'Service Unavailable' ), 504 => array( 'body' => true, 'msg' => 'Gateway Timeout' ), 505 => array( 'body' => true, 'msg' => 'HTTP Version Not Supported' ) ); /** * set response status code HTTP-a-like * * @param int status * @returns bool - true if given status is acceptable, false otherwise */ public function setStatus( $status ) { if( !isset( $this->_statusCodes[$status] ) ) { return false; } $this->_responseStatus = $status; return true; } /** * set age before cache expires * * Tell browser cache how many seconds the response is acceptable * * @param int $age seconds */ public function setMaxAge($age = 3600) { $this->expireAge = $age; } /** * Use cache control * * Set either to CACHECONTROL_MUSTREVALIDATE, CACHECONTROL_PRIVATE or CACHECONTROL_PUBLIC * * @see CACHECONTROL_MUSTREVALIDATE * @see CACHECONTROL_PRIVATE * @see CACHECONTROL_PUBLIC * @param string $control */ public function setCacheControl($control = self::CACHECONTROL_MUSTREVALIDATE) { $this->cacheControl = $control; } /** * tell whether to use caching features * * Allow to switch off caching regardless of WBParam: wb/cache/use * In case $use is true, WBParam: wb/cache/use decides if caching is * enabled. * * @param bool $use */ public function useCache($use = true) { $this->_useCache = $use; } /** * append string to response data * * @param string $name of the property * @param bool $clear wipe out all collected data * @return bool true on success */ public function add( $string, $clear = false ) { if( $clear ) { $this->_data = array(); } if( empty( $string ) ) { return false; } if( empty( $this->_data ) ) { $this->_checksum = md5( $string ); } $this->_data[] = array( 'type' => 'string', 'data' => $string, 'checksum' => $this->_checksum ); return true; } /** * append stream to output * * @param stream $stream * @param string $checksum checksum of stream (md5sum or something) * @return bool true on success */ public function addStream( $stream, $checksum ) { if( empty( $this->_data ) ) { $this->_checksum = $checksum; } $this->_data[] = array( 'type' => 'stream', 'data' => $stream, 'checksum' => $this->_checksum ); return true; } /** * insert response header * * @param string name of the property * @param mixed $value * @return bool true on success */ public function addHeader( $name, $value = null ) { $this->_header[$name] = $value; return true; } /** * append string to response data * * @param callbacl * @return bool true on success */ public function addOutputFilter( $callback ) { if( !is_callable( $callback ) ) { return false; } $this->_filters[] = $callback; return true; } /** * send * * send response to client * * @param WBRequest $request * @return bool true un success */ public function send( WBRequest $request ) { // use cache? if ($this->_useCache) { $this->_useCache = true; if (1 > WBParam::get('wb/cache/use', 1)) { $this->_useCache = false;; } } $start = WBClock::now(); $logger = WBLog::start(__CLASS__); $this->onSend($request); $range = $this->checkHeaderRange($request); $log = array( 'action' => 'send', 'self' => $_SERVER['PHP_SELF'], 'status' => $this->_responseStatus, 'size' => 0, 'memory' => round(memory_get_usage() / 1024, 0) . 'k', 'time-send' => 0, 'time-total' => 0, 'cache' => intval($this->_useCache), 'range' => implode('-', $range) ); // do not send a data in body if( !$this->_statusCodes[$this->_responseStatus]['body'] ) { $this->sendHeader(); $log['time-send'] = WBClock::stop( $start, 1000, 1 ) . 'ms'; $log['time-total'] = WBClock::stop( null, 1000, 1 ) . 'ms'; $logger->notice( $log ); return true; } $size = $this->sendData($range); $log['size'] = $size; $log['time-send'] = WBClock::stop( $start, 1000, 1 ) . 'ms'; $log['time-total'] = WBClock::stop( null, 1000, 1 ) . 'ms'; $logger->notice( $log ); return true; } /** * send * * @return bool true un success */ abstract protected function onSend( WBRequest $request ); /** * send header * * @return bool true on success */ protected function sendHeader() { return true; } /** * Check range header * * * * @param WBRequest $request * @return array */ private function checkHeaderRange($request) { // range header make sense for status 200, only $status = array( 200, 201, 202, 205, 206 ); if (!in_array($this->_responseStatus, $status)) { $range = array(0, -1); return $range; } $range = $request->getRange(); // set proper headers if (isset($this->_header['Content-Length'])) { $orgLength = $this->_header['Content-Length']; if (-1 == $range[1]) { $this->addHeader('Content-Length', ($orgLength - $range[0])); $this->addHeader('Content-Range', sprintf('bytes %s-%s/%s', $range[0], $orgLength - 1, $orgLength)); } else { $this->addHeader('Content-Length', $range[1]); $this->addHeader('Content-Range', sprintf('bytes %s-%s/%s', $range[0], ($range[1] - 1 + $range[0]), $orgLength)); } } // set status partial content if (0 != $range[0] || -1 != $range[1]) { $this->setStatus(206); } return $range; } /** * Send data * * Send all or range of data * * @todo Range support only works for single streams * @param array $range * @return int $size send bytes */ protected function sendData($range) { $logger = WBLog::start(__CLASS__); $size = 0; $useFilter = true; if (empty($this->_filters)) { $useFilter = false; $this->sendHeader(); } $buffer = ''; for ($i = 0; $i < count( $this->_data); ++$i) { switch ($this->_data[$i]['type']) { case 'stream': if (!$useFilter) { echo $buffer; $size += strlen( $buffer ); $buffer = ''; if (0 < $range[1] && $size >= $range[1]) { break; } } if (0 < $range[0]) { fseek($this->_data[$i]['data'], $range[0], SEEK_SET); $range[0] = 0; } while (!feof($this->_data[$i]['data'])) { $buffer .= fgets($this->_data[$i]['data'], 8192); if (!$useFilter) { $oldSize = $size; $size += strlen($buffer); // reached range if (0 < $range[1] && $size >= $range[1]) { $size = $range[1]; $buffer = substr($buffer, 0, ($size - $oldSize)); echo $buffer; $buffer = ''; break; } echo $buffer; $buffer = ''; } } fclose($this->_data[$i]['data']); break; default: case 'string': $buffer .= $this->_data[$i]['data']; if (!$useFilter){ echo $buffer; $size += strlen( $buffer ); $buffer = ''; } break; } } // apply filter if ($useFilter) { foreach ($this->_filters as $filter) { $buffer = call_user_func( $filter, $buffer ); } $size = strlen($buffer); $this->sendHeader(); echo $buffer; } return $size; } }