* @author Ralf Becker * @version $Id$ */ namespace EGroupware\Api\Json; use EGroupware\Api; /** * Class used to send ajax responses */ class Response extends Msg { /** * A response can only contain one generic data part. * This variable is used to store, whether a data part had already been added to the response. * * @var boolean */ private $hasData = false; /** * Array containing all beforeSendData callbacks */ protected $beforeSendDataProcs = array(); /** * Holds the actual response data which is then encoded to JSON * once the "getJSON" function is called * * @var array */ protected $responseArray = array(); /** * Holding instance of class for singelton Response::get() * * @var Response */ private static $response = null; /** * Force use of singleton: $response = Response::get(); */ protected function __construct() { } /** * Singelton for class * * @return Response */ public static function get() { if (!isset(self::$response)) { self::$response = new Response(); self::sendHeader(); } return self::$response; } public static function isJSONResponse() { return isset(self::$response); } /** * Do we have a JSON response to send back * * @return boolean */ public function haveJSONResponse() { return $this->responseArray || $this->beforeSendDataProcs; } /** * Private function used to send the HTTP header of the JSON response */ private static function sendHeader() { $file = $line = null; if (headers_sent($file, $line)) { error_log(__METHOD__."() header already sent by $file line $line: ".function_backtrace()); } else { //Send the character encoding header header('content-type: application/json; charset='.Api\Translation::charset()); } } /** * Private function which is used to send the result via HTTP */ public static function sendResult() { $inst = self::get(); //Call each attached before send data proc foreach ($inst->beforeSendDataProcs as $proc) { call_user_func_array($proc['proc'], $proc['params']); } // check if application made some direct output if (($output = ob_get_clean())) { if (!$inst->haveJSONResponse()) { error_log(__METHOD__."() adding output with inst->addGeneric('html', '$output')"); $inst->addGeneric('html', $output); } else { $inst->alert('Application echoed something', $output); } } echo $inst->getJSON(); $inst->initResponseArray(); } /** * Return json response data, after running beforeSendDataProcs * * Used to send json response with etemplate data in GET request * * @return array responseArray */ public static function returnResult() { $inst = self::get(); //Call each attached before send data proc foreach ($inst->beforeSendDataProcs as $proc) { call_user_func_array($proc['proc'], $proc['params']); } return $inst->initResponseArray(); } /** * xAjax compatibility function * * @deprecated output is send by egw::__destruct() */ public function printOutput() { // do nothing, as output is triggered by egw::__destruct() } /** * Adds any type of data to the message * * @param string $key * @param mixed $data */ protected function addGeneric($key, $data) { /* send testwise all message responses via push server if ($key === 'message' || $key === 'apply' && $data['func'] === 'egw.message') { return (new Push())->addGeneric($key, $data); }*/ self::get()->responseArray[] = array( 'type' => $key, 'data' => $data, ); } /** * Init responseArray * * @param array $arr * @return array previous content */ public function initResponseArray() { $return = $this->responseArray; $this->responseArray = $this->beforeSendDataProcs = array(); $this->hasData = false; return $return; } /** * Adds a "data" response to the json response. * * This function may only be called once for a single JSON response object. * * @param object|array|string $data can be of any data type and will be added JSON Encoded to your response. */ public function data($data) { /* Only allow adding the data response once */ $inst = self::get(); if (!$inst->hasData) { $inst->addGeneric('data', $data); $inst->hasData = true; } else { throw new Exception("Adding more than one data response to a JSON response is not allowed."); } } /** * Returns the actual JSON code generated by calling the above "add" function. * * @return string */ public function getJSON() { $inst = self::get(); // Wrap the result array into a parent "response" Object $res = array( 'response' => $inst->responseArray, )+Api\Framework::get_page_generation_time(); return self::json_encode($res); //PHP5.3+, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); } /** * More fault-tollerant version of json_encode removing everything that does not json_encode eg. because not utf-8 * * @param mixed $var * @return string */ public static function json_encode($var) { $ret = json_encode($var); if ($ret === false && ($err = json_last_error())) { static $json_err2str = array( JSON_ERROR_NONE => 'No errors', JSON_ERROR_DEPTH => 'Maximum stack depth exceeded', JSON_ERROR_STATE_MISMATCH => 'Underflow or the modes mismatch', JSON_ERROR_CTRL_CHAR => 'Unexpected control character found', JSON_ERROR_SYNTAX => 'Syntax error, malformed JSON', JSON_ERROR_UTF8 => 'Malformed UTF-8 characters, possibly incorrectly encoded', ); error_log(__METHOD__.'('.array2string($var).') json_last_error()='.$err.'='.$json_err2str[$err]); if (($var = self::fix_content($var))) { return self::json_encode($var); } } return $ret; } /** * Replace everything in $var which is not utf-8, that does not json_encode, eg. because no valid utf-8 * * @param midex $var * @param string $prefix ='' * @return mixed */ public static function fix_content($var, $prefix='') { if (json_encode($var) !== false) return $var; if (is_scalar($var)) { error_log(__METHOD__."() json_encode($prefix='$var') === false --> replacing it"); $var = self::cleanUtf8($var); } else { foreach($var as $name => &$value) { $value = self::fix_content($value, $prefix ? $prefix.'['.$name.']' : $name); } } return $var; } const UTF8_REPLACEMENT_CHAR = "\xEF\xBF\xBD"; /** * Replace non-utf8 chars in a string with a valid replacement char * * @param string $_str * @return string */ public static function cleanUtf8($_str) { //reject overly long 2 byte sequences, as well as characters above U+10000 and replace with ? $string = preg_replace('/[\x00-\x08\x10\x0B\x0C\x0E-\x19\x7F]'. '|[\x00-\x7F][\x80-\xBF]+'. '|([\xC0\xC1]|[\xF0-\xFF])[\x80-\xBF]*'. '|[\xC2-\xDF]((?![\x80-\xBF])|[\x80-\xBF]{2,})'. '|[\xE0-\xEF](([\x80-\xBF](?![\x80-\xBF]))|(?![\x80-\xBF]{2})|[\x80-\xBF]{3,})/S', self::UTF8_REPLACEMENT_CHAR, $_str); //reject overly long 3 byte sequences and UTF-16 surrogates and replace with ? return preg_replace('/\xE0[\x80-\x9F][\x80-\xBF]'. '|\xED[\xA0-\xBF][\x80-\xBF]/S', self::UTF8_REPLACEMENT_CHAR, $string ); } /** * Function which can be used to add an event listener callback function to * the "beforeSendData" callback. This callback might be used to add a response * which always has to be added after all other responses. * @param callback Callback function or method which should be called before the response gets sent * @param mixed n Optional parameters which get passed to the callback function. */ public function addBeforeSendDataCallback($proc) { //Get the current instance $inst = self::get(); //Get all parameters passed to the function and delete the first one $params = func_get_args(); array_shift($params); $inst->beforeSendDataProcs[] = array( 'proc' => $proc, 'params' => $params ); } }