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