0ad262a6a1c951a50e01b7402d7a73e525d81819
[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             } catch (Zend_Cache_Exception $zce) {
149                 if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
150                     . " Failed to create cache. Exception: \n". $zce);
151             }
152         }
153         
154         if (isset($cache) && $cache->test($cacheId)) {
155             $server = $cache->load($cacheId);
156             if ($server instanceof Zend_Json_Server) {
157                 return $server;
158             }
159         }
160         
161         $server = new Zend_Json_Server();
162         $server->setAutoEmitResponse(false);
163         $server->setAutoHandleExceptions(false);
164         
165         if (is_array($classes)) {
166             foreach ($classes as $class => $namespace) {
167                 try {
168                     $server->setClass($class, $namespace);
169                 } catch (Exception $e) {
170                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
171                         . " Failed to add JSON API for '$class' => '$namespace' Exception: \n". $e);
172                 }
173             }
174         }
175         
176         if (isset($cache)) {
177             $cache->save($server, $cacheId, array(), null);
178         }
179         
180         return $server;
181     }
182     
183     /**
184      * handler for JSON api requests
185      * @todo session expire handling
186      * 
187      * @param $request
188      * @return JSON
189      */
190     protected function _handle($request)
191     {
192         try {
193             $method = $request->getMethod();
194             Tinebase_Core::getLogger()->INFO(__METHOD__ . '::' . __LINE__ .' is JSON request. method: ' . $method);
195             
196             $jsonKey = (isset($_SERVER['HTTP_X_TINE20_JSONKEY'])) ? $_SERVER['HTTP_X_TINE20_JSONKEY'] : '';
197             $this->_checkJsonKey($method, $jsonKey);
198             
199             if (empty($method)) {
200                 // SMD request
201                 return self::getServiceMap();
202             }
203             
204             $this->_methods[] = $method;
205             
206             $classes = array();
207             
208             // add json apis which require no auth
209             $classes['Tinebase_Frontend_Json'] = 'Tinebase';
210             
211             // register additional Json apis only available for authorised users
212             if (Zend_Session::isStarted() && Zend_Auth::getInstance()->hasIdentity()) {
213                 
214                 $applicationParts = explode('.', $method);
215                 $applicationName = ucfirst($applicationParts[0]);
216                 
217                 switch($applicationName) {
218                     // additional Tinebase json apis
219                     case 'Tinebase_Container':
220                         $classes['Tinebase_Frontend_Json_Container'] = 'Tinebase_Container';
221                         break;
222                     case 'Tinebase_PersistentFilter':
223                         $classes['Tinebase_Frontend_Json_PersistentFilter'] = 'Tinebase_PersistentFilter';
224                         break;
225                         
226                     default;
227                         if(Tinebase_Core::getUser() && Tinebase_Core::getUser()->hasRight($applicationName, Tinebase_Acl_Rights_Abstract::RUN)) {
228                             $classes[$applicationName.'_Frontend_Json'] = $applicationName;
229                         }
230                         break;
231                 }
232             }
233             
234             $server = self::_getServer($classes);
235             
236             $response = $server->handle($request);
237             if ($response->isError()) {
238                 Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . ' Got response error: '
239                     . print_r($response->getError()->toArray(), true));
240             }
241             return $response;
242             
243         } catch (Exception $exception) {
244             return $this->_handleException($request, $exception);
245         }
246     }
247     
248     /**
249      * handle exceptions
250      * 
251      * @param Zend_Json_Server_Request_Http $request
252      * @param Exception $exception
253      * @return Zend_Json_Server_Response
254      */
255     protected function _handleException($request, $exception)
256     {
257         $server = self::_getServer();
258         
259         $exceptionData = method_exists($exception, 'toArray')? $exception->toArray() : array();
260         $exceptionData['message'] = htmlentities($exception->getMessage(), ENT_COMPAT, 'UTF-8');
261         $exceptionData['code']    = $exception->getCode();
262         
263         if ($exception instanceof Tinebase_Exception) {
264             $exceptionData['appName'] = $exception->getAppName();
265             $exceptionData['title'] = $exception->getTitle();
266         }
267         
268         Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' ' . get_class($exception) . ' -> ' . $exception->getMessage());
269         
270         $suppressTrace = Tinebase_Core::getConfig()->suppressExceptionTraces;
271         if ($suppressTrace !== TRUE) {
272             $exceptionData['trace'] = Tinebase_Exception::getTraceAsArray($exception);
273         }
274         
275         Tinebase_Exception::log($exception, $suppressTrace);
276         
277         $server->fault($exceptionData['message'], $exceptionData['code'], $exceptionData);
278         
279         $response = $server->getResponse();
280         if (null !== ($id = $request->getId())) {
281             $response->setId($id);
282         }
283         if (null !== ($version = $request->getVersion())) {
284             $response->setVersion($version);
285         }
286     
287         return $response;
288     }
289     
290     /**
291      * return service map
292      * 
293      * @return Zend_Json_Server_Smd
294      */
295     public static function getServiceMap()
296     {
297         $classes = array();
298         
299         $classes['Tinebase_Frontend_Json'] = 'Tinebase';
300         
301         if (Tinebase_Core::isRegistered(Tinebase_Core::USER)) {
302             $classes['Tinebase_Frontend_Json_Container'] = 'Tinebase_Container';
303             $classes['Tinebase_Frontend_Json_PersistentFilter'] = 'Tinebase_PersistentFilter';
304             
305             $userApplications = Tinebase_Core::getUser()->getApplications(TRUE);
306             foreach($userApplications as $application) {
307                 $jsonAppName = $application->name . '_Frontend_Json';
308                 $classes[$jsonAppName] = $application->name;
309             }
310         }
311         
312         $server = self::_getServer($classes);
313         
314         $server->setTarget('index.php')
315                ->setEnvelope(Zend_Json_Server_Smd::ENV_JSONRPC_2);
316             
317         $smd = $server->getServiceMap();
318         
319         return $smd;
320     }
321     
322     /**
323      * check json key
324      *
325      * @param string $method
326      * @param string $jsonKey
327      */
328     protected function _checkJsonKey($method, $jsonKey)
329     {
330         $anonymnousMethods = array(
331             '', //empty method
332             'Tinebase.getRegistryData',
333             'Tinebase.getAllRegistryData',
334             'Tinebase.authenticate',
335             'Tinebase.login',
336             'Tinebase.getAvailableTranslations',
337             'Tinebase.getTranslations',
338             'Tinebase.setLocale'
339         );
340         // check json key for all methods but some exceptions
341         if ( !(in_array($method, $anonymnousMethods))
342                 && $jsonKey != Tinebase_Core::get('jsonKey')) {
343         
344             if (! Tinebase_Core::isRegistered(Tinebase_Core::USER)) {
345                 Tinebase_Core::getLogger()->INFO(__METHOD__ . '::' . __LINE__ . ' Attempt to request a privileged Json-API method (' . $method . ') without authorisation from "' . $_SERVER['REMOTE_ADDR'] . '". (session timeout?)');
346                 
347                 throw new Tinebase_Exception_AccessDenied('Not Authorised', 401);
348             } else {
349                 Tinebase_Core::getLogger()->WARN(__METHOD__ . '::' . __LINE__ . ' Fatal: got wrong json key! (' . $jsonKey . ') Possible CSRF attempt!' .
350                     ' affected account: ' . print_r(Tinebase_Core::getUser()->toArray(), true) .
351                     ' request: ' . print_r($_REQUEST, true)
352                 );
353                 
354                 throw new Tinebase_Exception_AccessDenied('Not Authorised', 401);
355             }
356         }
357     }
358     
359     /**
360     * returns request method
361     *
362     * @return string|NULL
363     */
364     public function getRequestMethod()
365     {
366         return (! empty($this->_methods)) ? implode('|', $this->_methods) : NULL;
367     }
368 }