<?php /** * EGroupware API: JSON - Contains functions and classes for doing JSON requests. * * @link http://www.egroupware.org * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @package api * @subpackage ajax * @author Andreas Stoeckel <as@stylite.de> * @version $Id$ */ /** * Class handling JSON requests to the server */ class egw_json_request { private static $_hadJSONRequest = false; /** * Check if JSON request running or (re)set JSON request flag * * Can be used to: * - detect regular JSON request: * egw_json_request::isJSONRequest() * - switch regular JSON response handling off, which would send arbitrary output via response method "html". * Neccessary if json.php is used to send arbitrary JSON data eg. nodes for foldertree! * egw_json_request::isJSONRequest(false) * * @param boolean $set =null * @return boolean */ public static function isJSONRequest($set=null) { $ret = self::$_hadJSONRequest; if (isset($set)) self::$_hadJSONRequest = $set; return $ret; } /** * Parses the raw input data supplied with the input_data parameter and calls the menuaction * passing all parameters supplied in the request to it. * * Also handle queued requests (menuaction == 'home.queue') containing multiple requests * * @param string menuaction to call * @param string $input_data is the RAW input data as it was received from the client */ public function parseRequest($menuaction, $input_data) { // Remember that we currently are in a JSON request - e.g. used in the redirect code self::$_hadJSONRequest = true; if (get_magic_quotes_gpc()) $input_data = stripslashes($input_data); $json_data = json_decode($input_data,true); if (is_array($json_data) && isset($json_data['request']) && isset($json_data['request']['parameters']) && is_array($json_data['request']['parameters'])) { //error_log(__METHOD__.__LINE__.array2string($json_data['request']).function_backtrace()); $parameters =& $json_data['request']['parameters']; } else { $parameters = array(); } // do we have a single request or an array of queued requests if ($menuaction == 'home.queue') { $responses = array(); $response = egw_json_response::get(); foreach($parameters as $uid => $data) { //error_log("$uid: menuaction=$data[menuaction], parameters=".array2string($data['parameters'])); $this->handleRequest($data['menuaction'], (array)$data['parameters']); $responses[$uid] = $response->initResponseArray(); //error_log("responses[$uid]=".array2string($responses[$uid])); } $response->data($responses); // send all responses as data } else { $this->handleRequest($menuaction, $parameters); } } /** * Request handler * * @param string $menuaction * @param array $parameters */ public function handleRequest($menuaction, array $parameters) { if (strpos($menuaction,'::') !== false && strpos($menuaction,'.') === false) // static method name app_something::method { @list($className,$functionName,$handler) = explode('::',$menuaction); list($appName) = explode('_',$className); // Check for a real static method, avoid instanciation if it is $m = new ReflectionMethod($menuaction); if($m->isStatic()) { $ajaxClass = $className; } } else { @list($appName, $className, $functionName, $handler) = explode('.',$menuaction); } //error_log("json.php: appName=$appName, className=$className, functionName=$functionName, handler=$handler"); switch($handler) { case '/etemplate/process_exec': $_GET['menuaction'] = $appName.'.'.$className.'.'.$functionName; $appName = $className = 'etemplate'; $functionName = 'process_exec'; $menuaction = 'etemplate.etemplate.process_exec'; $parameters = array( $parameters[0]['etemplate_exec_id'], $parameters[0]['submit_button'], $parameters[0], 'xajaxResponse', ); //error_log("xajax_doXMLHTTP() /etemplate/process_exec handler: arg0='$menuaction', menuaction='$_GET[menuaction]'"); break; case 'etemplate': // eg. ajax code in an eTemplate widget $menuaction = ($appName = 'etemplate').'.'.$className.'.'.$functionName; break; case 'template': // calling current template / framework object $menuaction = $appName.'.'.$className.'.'.$functionName; $className = get_class($GLOBALS['egw']->framework); list($template) = explode('_', $className); break; } if(substr($className,0,4) != 'ajax' && substr($className,-4) != 'ajax' && $menuaction != 'etemplate.etemplate.process_exec' && substr($functionName,0,4) != 'ajax' || !preg_match('/^[A-Za-z0-9_-]+(\.[A-Za-z0-9_]+\.|::)[A-Za-z0-9_]+$/',$menuaction)) { // stopped for security reasons error_log($_SERVER['PHP_SELF']. ' stopped for security reason. '.$menuaction.' is not valid. class- or function-name must start with ajax!!!'); // send message also to the user throw new Exception($_SERVER['PHP_SELF']. ' stopped for security reason. '.$menuaction.' is not valid. class- or function-name must start with ajax!!!'); } if (isset($template)) { $ajaxClass = $GLOBALS['egw']->framework; } else if (!$ajaxClass) { $ajaxClass = CreateObject($appName.'.'.$className); } // for Ajax: no need to load the "standard" javascript files, // they are already loaded, in fact jquery has a problem if loaded twice egw_framework::js_files(array()); $parameters = translation::convert($parameters, 'utf-8'); call_user_func_array(array($ajaxClass, $functionName), $parameters); // check if we have push notifications, if notifications app available if (class_exists('notifications_push')) notifications_push::get(); } } /** * Abstract class implementing different type of JSON messages understood by client-side */ abstract class egw_json_msg { /** * Adds an "alert" to the response which can be handeled on the client side. * * The default implementation simply displays the text supplied here with the JavaScript function "alert". * * @param string $message contains the actual message being sent to the client. * @param string $details (optional) can be used to inform the user on the client side about additional details about the error. This might be information how the error can be resolved/why it was raised or simply some debug data. */ public function alert($message, $details = '') { if (is_string($message) && is_string($details)) { $this->addGeneric('alert', array( "message" => $message, "details" => $details)); } else { throw new Exception("Invalid parameters supplied."); } } /** * Allows to add a generic java script to the response which will be executed upon the request gets received. * * @deprecated * @param string $script the script code which should be executed upon receiving */ public function script($script) { if (is_string($script)) { $this->addGeneric('script', $script); } else { throw new Exception("Invalid parameters supplied."); } } /** * Allows to call a global javascript function with given parameters: window[$func].apply(window, $parameters) * * @param string $function name of the global (window) javascript function to call * @param array $parameters =array() */ public function apply($function,array $parameters=array()) { if (is_string($function)) { $this->addGeneric('apply', array( 'func' => $function, 'parms' => $parameters, )); } else { throw new Exception("Invalid parameters supplied."); } } /** * Allows to call a global javascript function with given parameters: window[$func].call(window[, $param1[, ...]]) * * @param string $func name of the global (window) javascript function to call * @param mixed $parameters variable number of parameters */ public function call($function) { $parameters = func_get_args(); array_shift($parameters); // shift off $function if (is_string($function)) { $this->addGeneric('apply', array( 'func' => $function, 'parms' => $parameters, )); } else { throw new Exception("Invalid parameters supplied."); } } /** * Allows to call a jquery function on a selector with given parameters: $j($selector).$func($parmeters) * * @param string $selector jquery selector * @param string $method name of the jquery to call * @param array $parameters =array() */ public function jquery($selector,$method,array $parameters=array()) { if (is_string($selector) && is_string($method)) { $this->addGeneric('jquery', array( 'select' => $selector, 'func' => $method, 'parms' => $parameters, )); } else { throw new Exception("Invalid parameters supplied."); } } public function generic($type, array $parameters = array()) { if (is_string($type)) { $this->addGeneric($type, $parameters); } else { throw new Exception("Invalid parameters supplied."); } } /** * Adds an html assign to the response, which is excecuted upon the request is received. * * @param string $id id of dom element to modify * @param string $key attribute name of dom element which should be modified * @param string $value the value which should be assigned to the given attribute */ public function assign($id, $key, $value) { if (is_string($id) && is_string($key) && (is_string($value) || is_numeric($value) || is_null($value))) { $this->addGeneric('assign', array( 'id' => $id, 'key' => $key, 'value' => $value, )); } else { throw new Exception("Invalid parameters supplied"); } } /** * Redirect to given url * * @param string $url * @param boolean $global specifies whether to redirect the whole framework * @param string $app =null default current app from flags * or only the current application */ public function redirect($url, $global = false, $app=null) { if (is_string($url) && is_bool($global)) { //self::script("location.href = '$url';"); $this->addGeneric('redirect', array( 'url' => $url, 'global' => $global, 'app' => $app ? $app : $GLOBALS['egw_info']['flags']['currentapp'], )); } } /** * Displays an error message on the client */ public function error($msg) { if (is_string($msg)) { $this->addGeneric('error', $msg); } } /** * Includes the given CSS file. Every url can only be included once. * * @param string $url specifies the url to the css file to include */ public function includeCSS($url) { if (is_string($url)) { $this->addGeneric('css', $url); } } /** * Includes the given JS file. Every url can only be included once. * * @param string $url specifies the url to the css file to include */ public function includeScript($url) { if (is_string($url)) { $this->addGeneric('js', $url); } } /** * Adds any type of data to the message * * @param string $key * @param mixed $data */ abstract protected function addGeneric($key, $data); } /** * Class used to send ajax responses */ class egw_json_response extends egw_json_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 egw_json_response::get() * * @var egw_json_response */ private static $response = null; /** * Force use of singleton: $response = egw_json_response::get(); */ protected function __construct() { } /** * Singelton for class * * @return egw_json_response */ public static function get() { if (!isset(self::$response)) { self::$response = new egw_json_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 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='.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 */ 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) { 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); 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; } /** * Set everything in $var to null, 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 --> setting it to null"); $var = null; } else { foreach($var as $name => &$value) { $value = self::fix_content($value, $prefix ? $prefix.'['.$name.']' : $name); } } return $var; } /** * 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 ); } } /** * Deprecated legacy xajax wrapper functions for the new egw_json interface */ class xajaxResponse { public function __call($name, $args) { if (substr($name, 0, 3) == 'add') { $name = substr($name, 3); $name[0] = strtolower($name[0]); } return call_user_func_array(array(egw_json_response::get(), $name), $args); } public function addScriptCall($func) { $args = func_get_args(); $func = array_shift($args); return call_user_func(array(egw_json_response::get(), 'apply'), $func, $args); } public function getXML() { return ''; } }