1: <?php
2: /*****************************************************************************************
3: * X2Engine Open Source Edition is a customer relationship management program developed by
4: * X2Engine, Inc. Copyright (C) 2011-2016 X2Engine Inc.
5: *
6: * This program is free software; you can redistribute it and/or modify it under
7: * the terms of the GNU Affero General Public License version 3 as published by the
8: * Free Software Foundation with the addition of the following permission added
9: * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
10: * IN WHICH THE COPYRIGHT IS OWNED BY X2ENGINE, X2ENGINE DISCLAIMS THE WARRANTY
11: * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
12: *
13: * This program is distributed in the hope that it will be useful, but WITHOUT
14: * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
15: * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
16: * details.
17: *
18: * You should have received a copy of the GNU Affero General Public License along with
19: * this program; if not, see http://www.gnu.org/licenses or write to the Free
20: * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
21: * 02110-1301 USA.
22: *
23: * You can contact X2Engine, Inc. P.O. Box 66752, Scotts Valley,
24: * California 95067, USA. or at email address contact@x2engine.com.
25: *
26: * The interactive user interfaces in modified source and object code versions
27: * of this program must display Appropriate Legal Notices, as required under
28: * Section 5 of the GNU Affero General Public License version 3.
29: *
30: * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
31: * these Appropriate Legal Notices must retain the display of the "Powered by
32: * X2Engine" logo. If the display of the logo is not reasonably feasible for
33: * technical reasons, the Appropriate Legal Notices must display the words
34: * "Powered by X2Engine".
35: *****************************************************************************************/
36:
37: /**
38: * Standalone environmentally-agnostic content/message feedback utility.
39: *
40: * In the scope of a web request, it will respond via JSON (i.e. for use in an
41: * API or AJAX response action). When run in a command line interface, it will
42: * echo messages without exiting.
43: *
44: * Setting elements of an object of this class (using the {@link ArrayAccess}
45: * implementation) will control the properties of the JSON that is returned when
46: * using it in a web request.
47: *
48: * @author Demitri Morgan <demitri@x2engine.com>
49: */
50: class ResponseUtil implements ArrayAccess {
51:
52: /**
53: * Exit on non-fatal PHP errors.
54: *
55: * If set to true, the error handler {@link respondWithError} will force a
56: * premature response for any PHP error, even if it's not of type E_ERROR.
57: *
58: * @var bool
59: */
60: public static $exitNonFatal = false;
61:
62: /**
63: * The default HTTP status code to use when handling an internal error.
64: *
65: * This can be set to 200 when dealing with client-side code that cannot
66: * retrieve response data if the response code is not 200. This would
67: * thus allow user-friendly error reporting.
68: *
69: * @var integer
70: */
71: public static $errorCode = 500;
72:
73: /**
74: * Whether to include or ignore any unintentional output
75: *
76: * If false, any extra output generated within the scope of the response (i.e.
77: * error messages) will be excluded from the response altogether.
78: * @var boolean
79: */
80: public static $includeExtraneousOutput = false;
81:
82: /**
83: * Shutdown method.
84: *
85: * Can be set to, for instance, "Yii::app()->end();" for a graceful Yii
86: * shutdown that saves/rotates logs and performs other useful/appropriate
87: * operations before terminating the PHP thread.
88: *
89: * @var string|array|closure
90: */
91: public static $shutdown = 'die();';
92:
93: /**
94: * Produce extended error traces in responses triggered by error handlers.
95: * @var type
96: */
97: public static $longErrorTrace = false;
98:
99: /**
100: * Override body.
101: *
102: * If left unset, the content type header will be set to JSON, and the
103: * response body will be {@link _properties}, encoded in JSON. Otherwise,
104: * any content type can be used, andthis property will be returned instead.
105: * @var string
106: */
107: public $body;
108:
109: /**
110: * HTTP header fields.
111: *
112: * The default of the "Content-type" field is JSON for ease of use, since
113: * it's expected that this class will be used mostly to compose responses
114: * in JSON format.
115: *
116: * @var array
117: */
118: public $httpHeader = array(
119: 'Content-Type' => 'application/json'
120: );
121:
122: /**
123: * Specifies, if true, that a response is already in progress.
124: *
125: * This is used to avoid double-responding when using
126: * {@link respondFatalErrorMessage} as a shutdown function for handling
127: * fatal errors.
128: *
129: * @var bool
130: */
131: private static $_responding = false;
132:
133: /**
134: * Response singleton (there can only be one response at a time)
135: * @var ResponseUtil
136: */
137: private static $_response = null;
138:
139: private static $_statusMessages = array(
140: 100 => 'Continue',
141: 101 => 'Switching Protocols',
142: 200 => 'OK',
143: 201 => 'Created',
144: 202 => 'Accepted',
145: 203 => 'Non-Authoritative Information',
146: 204 => 'No content',
147: 205 => 'Reset Content',
148: 206 => 'Partial Content',
149: 300 => 'Multiple Choices',
150: 301 => 'Moved Permanently',
151: 302 => 'Found',
152: 303 => 'See Other',
153: 304 => 'Not Modified',
154: 305 => 'Use Proxy',
155: 307 => 'Temporary Redirect',
156: 308 => 'Permanent Redirect',
157: 400 => 'Bad Request',
158: 401 => 'Unauthorized',
159: 402 => 'Payment Required',
160: 403 => 'Forbidden',
161: 404 => 'Not Found',
162: 405 => 'Method Not Allowed',
163: 406 => 'Not Acceptable',
164: 407 => 'Proxy Authentication Required',
165: 408 => 'Request Timeout',
166: 409 => 'Conflict',
167: 410 => 'Gone',
168: 411 => 'Length Required',
169: 412 => 'Precondition Failed',
170: 413 => 'Request Entity Too Large',
171: 414 => 'Request-URI Too Long',
172: 415 => 'Unsupported Media Type', // Incorrect content type in request
173: 416 => 'Requested Range Not Satisfiable',
174: 417 => 'Expectation Failed',
175: 418 => 'I\'m a teapot',
176: 422 => 'Unprocessable Entity', // Validation errors
177: 423 => 'Locked',
178: 424 => 'Failed Dependency',
179: 425 => 'Unordered Collection',
180: 426 => 'Upgrade Required',
181: 429 => 'Too Many Requests',
182: 431 => 'Request Header Fields Too Large',
183: 444 => 'No Response',
184: 494 => 'Request Header Too Large',
185: 495 => 'Cert Error',
186: 497 => 'HTTP to HTTPS',
187: 499 => 'Client Closed Request',
188: 500 => 'Internal Server Error',
189: 501 => 'Not Implemented',
190: 503 => 'Service Unavailable',
191: 504 => 'Gateway Timeout',
192: 505 => 'HTTP Version Not Supported',
193: 506 => 'Variant Also Negotiates',
194: 507 => 'Insufficient Storage',
195: 508 => 'Loop Detected',
196: 509 => 'Bandwidth Limit Exceeded',
197: 510 => 'Not Extended',
198: 511 => 'Network Authentication Required'
199: );
200:
201: /**
202: * Properties of the response.
203: *
204: * In the case of responding to a web request, the response will be this
205: * array encoded in JSON. When setting "indexes" of this class using the
206: * array access method, this is the array where the values are stored.
207: * @var array
208: */
209: private $_properties = array();
210:
211: /**
212: * HTTP status code when applicable.
213: *
214: * The default is 200, meaning no error.
215: *
216: * @var type
217: */
218: private $_status = 200;
219:
220: /**
221: * Performs the shutdown code.
222: */
223: public static function end() {
224: switch(gettype(self::$shutdown)) {
225: case 'string':
226: // Interpret as a snippet of PHP code
227: eval(self::$shutdown);
228: break;
229: case 'closure':
230: case 'object':
231: // Interpret as a function
232: $shutdown = self::$shutdown;
233: $shutdown();
234: break;
235: default:
236: die();
237: }
238: }
239:
240: /**
241: * Returns the current response singleton object.
242: */
243: public static function getObject() {
244: if(self::$_response instanceof ResponseUtil) {
245: return self::$_response;
246: } else {
247: return false;
248: }
249: }
250:
251: /**
252: * Returns the static array of status messages, i.e. for reference
253: * @return array
254: */
255: public static function getStatusMessages() {
256: return self::$_statusMessages;
257: }
258:
259: /**
260: * Returns true or false based on whether or not the current thread of PHP
261: * is being run from the command line.
262: * @return bool
263: */
264: public static function isCli(){
265: return (empty($_SERVER['SERVER_NAME']) || php_sapi_name()==='cli');
266: }
267: /**
268: * Universal, web-agnostic response function.
269: *
270: * Responds with a JSON and closes the connection if used in a web request;
271: * merely echoes the response message (but optionally exits) otherwise.
272: *
273: * @param type $message The message to respond with.
274: * @param bool $error Indicates that an error has occurred
275: * @param bool $fatal Shut down PHP thread after printing the message
276: * @param string $shutdown Optional shutdown expression to be evaluated; it must halt the current PHP process.
277: */
278: public static function respond($message, $error = false, $fatal = false){
279: if(self::isCli()){ // Command line interface message
280: self::$_responding = true;
281: echo trim($message)."\n";
282: if($error && $fatal)
283: self::end();
284: } else { // One-off JSON response to HTTP client
285: if(!isset(self::$_response)) {
286: self::$_response = new ResponseUtil(array());
287: }
288: // Default error code if there's an error; default/previously-set
289: // response code otherwise.
290: self::$_response->sendHttp($error?self::$errorCode:null,$message,$error);
291: }
292: }
293:
294: /**
295: * Error handler method that uses the web-agnostic response method.
296: *
297: * This is disabled by default; fatal errors should ordinarily be caught by
298: * {@link respondFatalErrorMessage()}. This can be enabled for debugging
299: * purposes via setting {@link exitNonFatal} to true.
300: *
301: * @param type $no
302: * @param type $st
303: * @param type $fi
304: * @param type $ln
305: */
306: public static function respondWithError($no, $st, $fi = Null, $ln = Null){
307: if(self::$exitNonFatal){
308: $message = "Error [$no]: $st $fi L$ln";
309: if(self::$longErrorTrace){
310: ob_start();
311: debug_print_backtrace();
312: $message .= "; Trace:\n".ob_get_contents();
313: ob_end_clean();
314: }
315: self::respond($message, true);
316: }
317: }
318:
319: /**
320: * Shutdown function for handling fatal errors not caught by
321: * {@link respondWithError()}.
322: */
323: public static function respondFatalErrorMessage(){
324: $error = error_get_last();
325: if($error != null && !self::$_responding){
326: $errno = $error["type"];
327: $errfile = $error["file"];
328: $errline = $error["line"];
329: $errstr = $error["message"];
330: self::respond("PHP ".($errno == E_PARSE ? 'parse' : 'fatal')." error [$errno]: $errstr in $errfile L$errline",true);
331: }
332: }
333:
334: /**
335: * @param Exception $e The uncaught exception
336: */
337: public static function respondWithException($e){
338: $message = 'Exception: "'.$e->getMessage().'" in '.$e->getFile().' L'.$e->getLine()."\n";
339: if(self::$longErrorTrace){
340: $message .= "; Trace:\n";
341: foreach($e->getTrace() as $stackLevel){
342: if(!empty($stackLevel['file']) && !empty($stackLevel['line'])){
343: $message .= $stackLevel['file'].' L'.$stackLevel['line'].' ';
344: }
345: if(!empty($stackLevel['class'])){
346: $message .= $stackLevel['class'];
347: $message .= '->';
348: }
349: if(!empty($stackLevel['function'])){
350: $message .= $stackLevel['function'];
351: $message .= "();";
352: }
353: $message .= "\n";
354: }
355: }
356: self::respond($message, true);
357: }
358:
359: /**
360: * Obtain an appropriate message for a given HTTP status code.
361: *
362: * @param integer $status
363: * @return string
364: */
365: public static function statusMessage($code){
366: $codes = self::$_statusMessages;
367: return isset($codes[$code]) ? $codes[$code] : '';
368: }
369:
370: //////////////////////
371: // Instance Methods //
372: //////////////////////
373:
374: /**
375: * Constructor.
376: * If one tries to instantiate two {@link ResponseUtil} objects, an
377: * exception will be thrown. The idea is that there should only ever be one
378: * response happening at a time.
379: *
380: * @param array $properties Initial response properties
381: */
382: public function __construct(){
383: if(self::$_response instanceof ResponseUtil){
384: throw new Exception('A response has already been declared.');
385: }
386: self::$_response = $this;
387: if(!self::isCli()){
388: // Collect any extraneous output so that it doesn't get sent before
389: // the intended HTTP header gets sent:
390: ob_start();
391: }
392: }
393:
394:
395: /////////////////////////////
396: // Array Interface Methods //
397: /////////////////////////////
398:
399: /**
400: * Array interface method from {@link ArrayAccess}
401: * @param type $offset
402: * @return type
403: */
404: public function offsetExists($offset){
405: return array_key_exists($offset, $this->_properties);
406: }
407:
408: /**
409: * Array interface method from {@link ArrayAccess}
410: * @param type $offset
411: * @return type
412: */
413: public function offsetGet($offset){
414: return $this->_properties[$offset];
415: }
416:
417: /**
418: * Array interface method from {@link ArrayAccess}
419: * @param type $offset
420: * @param type $value
421: */
422: public function offsetSet($offset, $value){
423: $this->_properties[$offset] = $value;
424: }
425:
426: /**
427: * Array interface method from {@link ArrayAccess}
428: * @param type $offset
429: */
430: public function offsetUnset($offset){
431: unset($this->_properties[$offset]);
432: }
433:
434: /**
435: * Sends a HTTP response back to the client.
436: *
437: * @param integer $status The status code to use
438: * @param type $message
439: * @param type $error
440: * @throws Exception
441: */
442: public function sendHttp($status=null, $message = '', $error = null){
443: self::$_responding = true;
444: // Close the output buffer; it's now safe to do so, since the header
445: // will soon be sent.
446: $output = ob_get_clean();
447: ob_end_clean();
448: $extraOutput = self::$includeExtraneousOutput && !empty($output);
449: $status = $status === null ? ((bool)$error ? self::$errorCode : 200) : $status;
450:
451: // Set the response content
452: if($status !== null && !array_key_exists((integer) $status,self::$_statusMessages)){
453: // Invalid call to this method. Fail noisily.
454: $this->_status = self::$errorCode;
455: $body = '{"error":true,"message":"Internal server error: invalid or '
456: . 'non-numeric HTTP response status code specifed.","status":500}';
457: } else if(!extension_loaded('json') || isset($this->body)) {
458: // We might be doing something other than responding in JSON
459: if(!isset($this->body)){
460: if(strpos($this->httpHeader['Content-Type'],'application/json')===0){
461: // JSON-format responding in use but not available
462: $this->_status = self::$errorCode;
463: $body = '{"error":true,"message":"The JSON PHP extension is required,'
464: . ' but this server lacks it.","status":'.$this->_status.'}';
465: } else {
466: // Simply echo the message if JSON isn't available.
467: $this->_status = $status;
468: $body = ($extraOutput?($output.' '):'').$message;
469: }
470: } else {
471: // The "body" property is in use, which overrides the standard
472: // way of responding with JSON-encoded properties
473: $this->_status = $status;
474: $body = ($extraOutput?($output.' '):'').$this->body;
475: }
476: } else {
477: if($status != null) {
478: // Override status. Loose comparison is in use because zero is
479: // an invalid HTTP response code and expected only of certain
480: // cURL libraries when the connection could not be established.
481: $this->_status = $status;
482: }
483: $response = $this->_properties;
484:
485: // Set universal response properties:
486: if(empty($message) && !empty($response['message']))
487: $message = $response['message'];
488: $response['message'] = $message.($extraOutput
489: ? " Note, extraneous output was generated in the scope of this response: $output"
490: : '');
491: $response['error'] = $error === null
492: ? $this->_status >= 400
493: : (bool) $error;
494: // Include the status code in the envelope for clients that can't
495: // read HTTP headers:
496: $response['status'] = $this->_status;
497: // Compose the body of the response as a JSON-encoded object:
498: $body = json_encode($response);
499: }
500:
501: // Send the response
502: $this->sendHttpHeader();
503: echo $body;
504:
505: // Shut down
506: self::$_response = null;
507: self::end();
508: }
509:
510: /**
511: * Sends HTTP headers. This method should be called before any content is sent.
512: *
513: * @param bool $replace The argument sent to header as the replacement flag.
514: */
515: protected function sendHttpHeader($replace = true){
516: header(sprintf("HTTP/1.1 %d %s", $this->_status, self::statusMessage($this->_status)), $replace, $this->_status);
517: foreach($this->httpHeader as $field => $value){
518: header("$field: $value", $replace, $this->_status);
519: }
520: }
521:
522: /**
523: * Sets {@link _properties}
524: * @param array $properties
525: */
526: public function setProperties(array $properties) {
527: $this->_properties = $properties;
528: }
529: }
530: