Merge branch 'pu/2013.03/modelconfig-hr'
[tine20] / tine20 / Tinebase / Server / Json.php
1 <?php
2 /**
3  * Tine 2.0
4  * 
5  * @package     Tinebase
6  * @subpackage  Server
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @copyright   Copyright (c) 2007-2013 Metaways Infosystems GmbH (http://www.metaways.de)
9  * @author      Philipp Schüle <p.schuele@metaways.de>
10  * 
11  */
12
13 /**
14  * JSON Server class with handle() function
15  * 
16  * @package     Tinebase
17  * @subpackage  Server
18  */
19 class Tinebase_Server_Json implements Tinebase_Server_Interface
20 {
21     /**
22      * handled request methods
23      * 
24      * @var array
25      */
26     protected $_methods = array();
27     
28     /**
29      * handle request
30      * 
31      * @return void
32      */
33     public function handle()
34     {
35         // handle CORS requests
36         if (isset($_SERVER['HTTP_ORIGIN'])) {
37             $allowedOrigins = array_merge(
38                 (array) Tinebase_Core::getConfig()->get(Tinebase_Config::ALLOWEDJSONORIGINS, array()),
39                 array($_SERVER['SERVER_NAME'])
40             );
41             $origin = parse_url($_SERVER['HTTP_ORIGIN'], PHP_URL_HOST);
42             
43             if (in_array($origin, $allowedOrigins)) {
44                 header('Access-Control-Allow-Origin: ' . $_SERVER['HTTP_ORIGIN']);
45                 
46                 if ($_SERVER['REQUEST_METHOD'] == "OPTIONS" && isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_METHOD'])) {
47                     header('Access-Control-Allow-Methods: POST, OPTIONS');
48                     header('Access-Control-Allow-Headers: x-requested-with, x-tine20-request-type, content-type');
49                     exit;
50                 }
51             } else {
52                 Tinebase_Core::getLogger()->INFO(__METHOD__ . '::' . __LINE__ . " forbidden CORS request from {$_SERVER['HTTP_ORIGIN']}");
53                 Tinebase_Core::getLogger()->DEBUG(__METHOD__ . '::' . __LINE__ . " allowed origins: " . print_r($allowedOrigins, TRUE));
54                 header("HTTP/1.1 403 Access Forbidden");
55                 exit;
56             }
57         }
58         
59         try {
60             Tinebase_Core::initFramework();
61             $exception = FALSE;
62         } catch (Exception $exception) {
63             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .' initFramework exception: ' . $exception);
64             
65             // handle all kind of session exceptions as 'Not Authorised'
66             if ($exception instanceof Zend_Session_Exception) {
67                 $exception = new Tinebase_Exception_AccessDenied('Not Authorised', 401);
68                 
69                 // expire session cookie for client
70                 Zend_Session::expireSessionCookie();
71             }
72         }
73         
74         $json = file_get_contents('php://input');
75         if (substr($json, 0, 1) == '[') {
76             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' batched request');
77             $isBatchedRequest = true;
78             $requests = Zend_Json::decode($json);
79         } else {
80             $isBatchedRequest = false;
81             $requests = array(Zend_Json::decode($json));
82         }
83         
84         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) {
85             $_requests = $requests;
86             foreach (array('password', 'oldPassword', 'newPassword') as $field) {
87                 if (isset($requests[0]["params"][$field])) {
88                     $_requests[0]["params"][$field] = "*******";
89                 }
90             }
91             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .' is JSON request. rawdata: ' . print_r($_requests, true));
92         } 
93         
94         $response = array();
95         foreach ($requests as $requestOptions) {
96             if ($requestOptions !== NULL) {
97                 $request = new Zend_Json_Server_Request();
98                 $request->setOptions($requestOptions);
99                 
100                 $response[] = $exception ? 
101                    $this->_handleException($request, $exception) :
102                    $this->_handle($request);
103             } else {
104                 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' Got empty request options: skip request.');
105                 $response[] = NULL;
106             }
107         }
108         
109         header('Content-type: application/json');
110         echo $isBatchedRequest ? '['. implode(',', $response) .']' : $response[0];
111     }
112     
113     /**
114      * get JSON from cache or new instance
115      * 
116      * @param array $classes for Zend_Cache_Frontend_File
117      * @return Zend_Json_Server
118      */
119     protected static function _getServer($classes = null)
120     {
121         // setup cache if available
122         if (is_array($classes) && Tinebase_Core::getCache()) {
123             $masterFiles = array();
124         
125             $dirname = dirname(__FILE__) . '/../../';
126             foreach ($classes as $class => $namespace) {
127                 $masterFiles[] = $dirname . str_replace('_', '/', $class) . '.php';
128             }
129         
130             try {
131                 $cache = new Zend_Cache_Frontend_File(array(
132                     'master_files'              => $masterFiles,
133                     'lifetime'                  => null,
134                     'automatic_serialization'   => true, // turn that off for more speed
135                     'automatic_cleaning_factor' => 0,    // no garbage collection as this is done by a scheduler task
136                     'write_control'             => false, // don't read cache entry after it got written
137                     'logging'                   => (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)),
138                     'logger'                    => Tinebase_Core::getLogger(),
139                 ));
140                 $cache->setBackend(Tinebase_Core::getCache()->getBackend());
141                 $cacheId = '_handle_' . sha1(Zend_Json_Encoder::encode($classes));
142             } catch (Zend_Cache_Exception $zce) {
143                 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
144                     . " Failed to create cache. Exception: \n". $zce);
145             }
146         }
147         
148         if (isset($cache) && $cache->test($cacheId)) {
149             $server = $cache->load($cacheId);
150             if ($server instanceof Zend_Json_Server) {
151                 return $server;
152             }
153         }
154         
155         $server = new Zend_Json_Server();
156         $server->setAutoEmitResponse(false);
157         $server->setAutoHandleExceptions(false);
158         
159         if (is_array($classes)) {
160             foreach ($classes as $class => $namespace) {
161                 try {
162                     $server->setClass($class, $namespace);
163                 } catch (Exception $e) {
164                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
165                         . " Failed to add JSON API for '$class' => '$namespace' Exception: \n". $e);
166                 }
167             }
168         }
169         
170         if (isset($cache)) {
171             $cache->save($server, $cacheId, array(), null);
172         }
173         
174         return $server;
175     }
176     
177     /**
178      * handler for JSON api requests
179      * @todo session expire handling
180      * 
181      * @param $request
182      * @return JSON
183      */
184     protected function _handle($request)
185     {
186         try {
187             $method = $request->getMethod();
188             Tinebase_Core::getLogger()->INFO(__METHOD__ . '::' . __LINE__ .' is JSON request. method: ' . $method);
189             
190             $jsonKey = (isset($_SERVER['HTTP_X_TINE20_JSONKEY'])) ? $_SERVER['HTTP_X_TINE20_JSONKEY'] : '';
191             $this->_checkJsonKey($method, $jsonKey);
192             
193             if (empty($method)) {
194                 // SMD request
195                 return self::getServiceMap();
196             }
197             
198             $this->_methods[] = $method;
199             
200             $classes = array();
201             
202             // add json apis which require no auth
203             $classes['Tinebase_Frontend_Json'] = 'Tinebase';
204             $classes['Tinebase_Frontend_Json_UserRegistration'] = 'Tinebase_UserRegistration';
205             
206             // register additional Json apis only available for authorised users
207             if (Zend_Session::isStarted() && Zend_Auth::getInstance()->hasIdentity()) {
208                 
209                 $applicationParts = explode('.', $method);
210                 $applicationName = ucfirst($applicationParts[0]);
211                 
212                 switch($applicationName) {
213                     // additional Tinebase json apis
214                     case 'Tinebase_Container':
215                         $classes['Tinebase_Frontend_Json_Container'] = 'Tinebase_Container';
216                         break;
217                     case 'Tinebase_PersistentFilter':
218                         $classes['Tinebase_Frontend_Json_PersistentFilter'] = 'Tinebase_PersistentFilter';
219                         break;
220                         
221                     default;
222                         if(Tinebase_Core::getUser() && Tinebase_Core::getUser()->hasRight($applicationName, Tinebase_Acl_Rights_Abstract::RUN)) {
223                             $classes[$applicationName.'_Frontend_Json'] = $applicationName;
224                         }
225                         break;
226                 }
227             }
228             
229             $server = self::_getServer($classes);
230             
231             // handle response
232             return $server->handle($request);
233             
234         } catch (Exception $exception) {
235             return $this->_handleException($request, $exception);
236         }
237     }
238     
239     /**
240      * handle exceptions
241      * 
242      * @param Zend_Json_Server_Request_Http $request
243      * @param Exception $exception
244      * @return Zend_Json_Server_Response
245      */
246     protected function _handleException($request, $exception)
247     {
248         $server = self::_getServer();
249         
250         $exceptionData = method_exists($exception, 'toArray')? $exception->toArray() : array();
251         $exceptionData['message'] = htmlentities($exception->getMessage(), ENT_COMPAT, 'UTF-8');
252         $exceptionData['code']    = $exception->getCode();
253         $suppressTrace = Tinebase_Core::getConfig()->suppressExceptionTraces;
254         if ($suppressTrace !== TRUE) {
255             $exceptionData['trace'] = Tinebase_Exception::getTraceAsArray($exception);
256         }
257         if ($exception instanceof Tinebase_Exception) {
258             $exceptionData['appName'] = $exception->getAppName();
259         }
260         
261         Tinebase_Exception::log($exception, $suppressTrace);
262         
263         $server->fault($exceptionData['message'], $exceptionData['code'], $exceptionData);
264         
265         $response = $server->getResponse();
266         if (null !== ($id = $request->getId())) {
267             $response->setId($id);
268         }
269         if (null !== ($version = $request->getVersion())) {
270             $response->setVersion($version);
271         }
272     
273         return $response;
274     }
275     
276     /**
277      * return service map
278      * 
279      * @return Zend_Json_Server_Smd
280      */
281     public static function getServiceMap()
282     {
283         $classes = array();
284         
285         $classes['Tinebase_Frontend_Json'] = 'Tinebase';
286         $classes['Tinebase_Frontend_Json_UserRegistration'] = 'Tinebase_UserRegistration';
287         
288         if (Tinebase_Core::isRegistered(Tinebase_Core::USER)) {
289             $classes['Tinebase_Frontend_Json_Container'] = 'Tinebase_Container';
290             $classes['Tinebase_Frontend_Json_PersistentFilter'] = 'Tinebase_PersistentFilter';
291             
292             $userApplications = Tinebase_Core::getUser()->getApplications(TRUE);
293             foreach($userApplications as $application) {
294                 $jsonAppName = $application->name . '_Frontend_Json';
295                 $classes[$jsonAppName] = $application->name;
296             }
297         }
298         
299         $server = self::_getServer($classes);
300         
301         $server->setTarget('index.php')
302                ->setEnvelope(Zend_Json_Server_Smd::ENV_JSONRPC_2);
303             
304         $smd = $server->getServiceMap();
305         
306         return $smd;
307     }
308     
309     /**
310      * check json key
311      *
312      * @param string $method
313      * @param string $jsonKey
314      */
315     protected function _checkJsonKey($method, $jsonKey)
316     {
317         $anonymnousMethods = array(
318             '', //empty method
319             'Tinebase.getRegistryData',
320             'Tinebase.getAllRegistryData',
321             'Tinebase.authenticate',
322             'Tinebase.login',
323             'Tinebase.getAvailableTranslations',
324             'Tinebase.getTranslations',
325             'Tinebase.setLocale'
326         );
327         // check json key for all methods but some exceptions
328         if ( !(in_array($method, $anonymnousMethods) || preg_match('/Tinebase_UserRegistration/', $method))  
329                 && $jsonKey != Tinebase_Core::get('jsonKey')) {
330         
331             if (! Tinebase_Core::isRegistered(Tinebase_Core::USER)) {
332                 Tinebase_Core::getLogger()->INFO(__METHOD__ . '::' . __LINE__ . ' Attempt to request a privileged Json-API method (' . $method . ') without authorisation from "' . $_SERVER['REMOTE_ADDR'] . '". (session timeout?)');
333                 
334                 throw new Tinebase_Exception_AccessDenied('Not Authorised', 401);
335             } else {
336                 Tinebase_Core::getLogger()->WARN(__METHOD__ . '::' . __LINE__ . ' Fatal: got wrong json key! (' . $jsonKey . ') Possible CSRF attempt!' .
337                     ' affected account: ' . print_r(Tinebase_Core::getUser()->toArray(), true) .
338                     ' request: ' . print_r($_REQUEST, true)
339                 );
340                 
341                 throw new Tinebase_Exception_AccessDenied('Not Authorised', 401);
342             }
343         }
344     }
345     
346     /**
347     * returns request method
348     *
349     * @return string|NULL
350     */
351     public function getRequestMethod()
352     {
353         return (! empty($this->_methods)) ? implode('|', $this->_methods) : NULL;
354     }
355 }