e16806f09f0f9d07c164141d44bd8dc882ca6f5d
[tine20] / tine20 / Tinebase / Frontend / Http.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-2014 Metaways Infosystems GmbH (http://www.metaways.de)
9  * @author      Lars Kneschke <l.kneschke@metaways.de>
10  */
11
12 /**
13  * HTTP interface to Tine
14  *
15  * @package     Tinebase
16  * @subpackage  Server
17  */
18 class Tinebase_Frontend_Http extends Tinebase_Frontend_Http_Abstract
19 {
20     const REQUEST_TYPE = 'HttpPost';
21     
22     /**
23      * get json-api service map
24      * 
25      * @return string
26      */
27     public static function getServiceMap()
28     {
29         $smd = Tinebase_Server_Json::getServiceMap();
30         
31         $smdArray = $smd->toArray();
32         unset($smdArray['methods']);
33         
34         if (! isset($_REQUEST['method']) || $_REQUEST['method'] != 'Tinebase.getServiceMap') {
35             return $smdArray;
36         }
37         
38         header('Content-type: application/json');
39         echo '_smd = ' . json_encode($smdArray);
40         die();
41     }
42     
43     /**
44      * return xrds file
45      * used to autodiscover openId servers
46      * 
47      * @return void
48      */
49     public function getXRDS()
50     {
51         // selfUrl == http://servername/pathtotine20/users/loginname
52         $url = dirname(dirname(Zend_OpenId::selfUrl())) . '/index.php?method=Tinebase.openId';
53         
54         header('Content-type: application/xrds+xml');
55         
56         echo '<?xml version="1.0"?>
57             <xrds:XRDS xmlns="xri://$xrd*($v*2.0)" xmlns:xrds="xri://$xrds">
58               <XRD>
59                 <Service priority="0">
60                   <Type>http://specs.openid.net/auth/2.0/signon</Type>
61                   <URI>' . $url . '</URI>
62                 </Service>
63                 <Service priority="1">
64                   <Type>http://openid.net/signon/1.1</Type>
65                   <URI>' . $url . '</URI>
66                 </Service>
67                 <Service priority="2">
68                   <Type>http://openid.net/signon/1.0</Type>
69                   <URI>' . $url . '</URI>
70                 </Service>
71               </XRD>
72             </xrds:XRDS>';
73     }
74     
75     /**
76      * display user info page
77      * 
78      * in the future we can display public informations about the user here too
79      * currently it is only used as entry point for openId
80      * 
81      * @param string $username the username
82      * @return void
83      */
84     public function userInfoPage($username)
85     {
86         // selfUrl == http://servername/pathtotine20/users/loginname
87         $openIdUrl = dirname(dirname(Zend_OpenId::selfUrl())) . '/index.php?method=Tinebase.openId';
88         
89         $view = new Zend_View();
90         $view->setScriptPath('Tinebase/views');
91         
92         $view->openIdUrl = $openIdUrl;
93         $view->username = $username;
94
95         header('Content-Type: text/html; charset=utf-8');
96         echo $view->render('userInfoPage.php');
97     }
98     
99     /**
100      * handle all kinds of openId requests
101      * 
102      * @return void
103      */
104     public function openId()
105     {
106         Tinebase_Core::startCoreSession();
107         $server = new Tinebase_OpenId_Provider(
108             null,
109             null,
110             new Tinebase_OpenId_Provider_User_Tine20(),
111             new Tinebase_OpenId_Provider_Storage()
112         );
113         $server->setOpEndpoint(dirname(Zend_OpenId::selfUrl()) . '/index.php?method=Tinebase.openId');
114         
115         // handle openId login form
116         if (isset($_POST['openid_action']) && $_POST['openid_action'] === 'login') {
117             $server->login($_POST['openid_identifier'], $_POST['password'], $_POST['username']);
118             unset($_GET['openid_action']);
119             $this->_setJsonKeyCookie();
120             Zend_OpenId::redirect(dirname(Zend_OpenId::selfUrl()) . '/index.php', $_GET);
121
122         // display openId login form
123         } else if (isset($_GET['openid_action']) && $_GET['openid_action'] === 'login') {
124             $view = new Zend_View();
125             $view->setScriptPath('Tinebase/views');
126             
127             $view->openIdIdentity = $_GET['openid_identity'];
128             $view->loginName = $_GET['openid_identity'];
129     
130             header('Content-Type: text/html; charset=utf-8');
131             echo $view->render('openidLogin.php');
132
133         // handle openId trust form
134         } else if (isset($_POST['openid_action']) && $_POST['openid_action'] === 'trust') {
135             if (isset($_POST['allow'])) {
136                 if (isset($_POST['forever'])) {
137                     $server->allowSite($server->getSiteRoot($_GET));
138                 }
139                 $server->respondToConsumer($_GET);
140             } else if (isset($_POST['deny'])) {
141                 if (isset($_POST['forever'])) {
142                     $server->denySite($server->getSiteRoot($_GET));
143                 }
144                 Zend_OpenId::redirect($_GET['openid_return_to'],
145                                       array('openid.mode'=>'cancel'));
146             }
147
148         // display openId trust form
149         } else if (isset($_GET['openid_action']) && $_GET['openid_action'] === 'trust') {
150             if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
151                 . " Display openId trust screen");
152             $view = new Zend_View();
153             $view->setScriptPath('Tinebase/views');
154             
155             $view->openIdConsumer = $server->getSiteRoot($_GET);
156             $view->openIdIdentity = $server->getLoggedInUser();
157             
158             header('Content-Type: text/html; charset=utf-8');
159             echo $view->render('openidTrust.php');
160
161         // handle all other openId requests
162         } else {
163             $result = $server->handle();
164
165             if (is_string($result)) {
166                 echo $result;
167             } elseif ($result !== true) {
168                 header('HTTP/1.0 403 Forbidden');
169                 return;
170             }
171         }
172     }
173     
174     /**
175      * checks if a user is logged in. If not we redirect to login
176      */
177     protected function checkAuth()
178     {
179         try {
180             Tinebase_Core::getUser();
181         } catch (Exception $e) {
182             header('HTTP/1.0 403 Forbidden');
183             exit;
184         }
185     }
186     
187     /**
188      * renders the login dialog
189      *
190      * @todo perhaps we could add a config option to display the update dialog if it is set
191      */
192     public function login()
193     {
194         // redirect to REDIRECTURL if set
195         $redirectUrl = Tinebase_Config::getInstance()->get(Tinebase_Config::REDIRECTURL, '');
196
197         if ($redirectUrl !== '' && Tinebase_Config::getInstance()->get(Tinebase_Config::REDIRECTALWAYS, FALSE)) {
198             header('Location: ' . $redirectUrl);
199             return;
200         }
201         
202         // check if setup/update required
203         $setupController = Setup_Controller::getInstance();
204         $applications = Tinebase_Application::getInstance()->getApplicationsByState(Tinebase_Application::ENABLED);
205         foreach ($applications as $application) {
206             if ($setupController->updateNeeded($application)) {
207                 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
208                     . " " . $application->name . ' needs an update');
209                 $this->setupRequired();
210             }
211         }
212         
213         $this->_renderMainScreen();
214         
215         /**
216          * old code used to display user registration
217          * @todo must be reworked
218          * 
219         
220         $view = new Zend_View();
221         $view->setScriptPath('Tinebase/views');
222
223         header('Content-Type: text/html; charset=utf-8');
224         
225         // check if registration is active
226         if(isset(Tinebase_Core::getConfig()->login)) {
227             $registrationConfig = Tinebase_Core::getConfig()->registration;
228             $view->userRegistration = (isset($registrationConfig->active)) ? $registrationConfig->active : '';
229         } else {
230             $view->userRegistration = 0;
231         }        
232         
233         echo $view->render('jsclient.php');
234         */
235     }
236     
237     /**
238      * renders the main screen
239      * 
240      * @return void
241      */
242     protected function _renderMainScreen()
243     {
244         $view = new Zend_View();
245         $baseDir = dirname(dirname(dirname(__FILE__)));
246         $view->setScriptPath("$baseDir/Tinebase/views");
247
248         if (TINE20_BUILDTYPE =='DEVELOPMENT') {
249             $jsFilestoInclude = $this->_getFilesToWatch('js');
250             $view->devIncludes = $jsFilestoInclude;
251         }
252
253         $view->registryData = array();
254         
255         $this->_setMainscreenHeaders();
256         
257         echo $view->render('jsclient.php');
258     }
259     
260     /**
261      * set headers for mainscreen
262      */
263     protected function _setMainscreenHeaders()
264     {
265         if (headers_sent()) {
266             return;
267         }
268         
269         header('Content-Type: text/html; charset=utf-8');
270         
271         // obsoleted by CSP see https://www.w3.org/TR/CSP2/#directive-frame-ancestors
272         //header('X-Frame-Options: SAMEORIGIN');
273
274         $frameAncestors = implode(' ' ,array_merge(
275             (array) Tinebase_Core::getConfig()->get(Tinebase_Config::ALLOWEDJSONORIGINS, array()),
276             array("'self'")
277         ));
278
279         // set Content-Security-Policy header against clickjacking and XSS
280         // @see https://developer.mozilla.org/en/Security/CSP/CSP_policy_directives
281         $scriptSrcs = array("'self'", "'unsafe-eval'", 'https://versioncheck.tine20.net');
282         if (TINE20_BUILDTYPE == 'DEVELOPMENT') {
283             $scriptSrcs[] = Tinebase_Core::getUrl('protocol') . '://' . Tinebase_Core::getUrl('host') . ":10443";
284         }
285         $scriptSrc = implode(' ', $scriptSrcs);
286         header("Content-Security-Policy: default-src 'self'");
287         header("Content-Security-Policy: script-src $scriptSrc");
288         header("Content-Security-Policy: frame-ancestors $frameAncestors");
289
290         // headers for IE 10+11
291         header("X-Content-Security-Policy: default-src 'self'");
292         header("X-Content-Security-Policy: script-src $scriptSrc");
293         header("X-Content-Security-Policy: frame-ancestors $frameAncestors");
294
295         // set Strict-Transport-Security; used only when served over HTTPS
296         header('Strict-Transport-Security: max-age=16070400');
297
298         // cache mainscreen for one day in production
299         $maxAge = ! defined('TINE20_BUILDTYPE') || TINE20_BUILDTYPE != 'DEVELOPMENT' ? 86400 : -10000;
300         header('Cache-Control: private, max-age=' . $maxAge);
301         header("Expires: " . gmdate('D, d M Y H:i:s', Tinebase_DateTime::now()->addSecond($maxAge)->getTimestamp()) . " GMT");
302         header_remove('Pragma');
303     }
304     
305     /**
306      * renders the setup/update required dialog
307      */
308     public function setupRequired()
309     {
310         $view = new Zend_View();
311         $view->setScriptPath('Tinebase/views');
312
313         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->DEBUG(__CLASS__ . '::' . __METHOD__ . ' (' . __LINE__ .') Update/Setup required!');
314
315         header('Content-Type: text/html; charset=utf-8');
316         echo $view->render('update.php');
317         exit();
318     }
319
320     /**
321      * login from HTTP post 
322      * 
323      * redirects the tine main screen if authentication is successful
324      * otherwise redirects back to login url 
325      */
326     public function loginFromPost($username, $password)
327     {
328         Tinebase_Core::startCoreSession();
329         
330         if (!empty($username)) {
331             // try to login user
332             $success = (Tinebase_Controller::getInstance()->login(
333                 $username,
334                 $password,
335                 Tinebase_Core::get(Tinebase_Core::REQUEST),
336                 self::REQUEST_TYPE
337             ) === TRUE);
338         } else {
339             $success = FALSE;
340         }
341         
342         if ($success === TRUE) {
343             $this->_setJsonKeyCookie();
344
345             $ccAdapter = Tinebase_Auth_CredentialCache::getInstance()->getCacheAdapter();
346             if (Tinebase_Core::isRegistered(Tinebase_Core::USERCREDENTIALCACHE)) {
347                 $ccAdapter->setCache(Tinebase_Core::getUserCredentialCache());
348             } else {
349                 Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' Something went wrong with the CredentialCache / no CC registered.');
350                 $success = FALSE;
351                 $ccAdapter->resetCache();
352             }
353         
354         }
355
356         $request = new Sabre\HTTP\Request();
357         $redirectUrl = str_replace('index.php', '', $request->getAbsoluteUri());
358
359         // authentication failed
360         if ($success !== TRUE) {
361             $_SESSION = array();
362             Tinebase_Session::destroyAndRemoveCookie();
363             
364             // redirect back to loginurl if needed
365             $redirectUrl = Tinebase_Config::getInstance()->get(Tinebase_Config::REDIRECTURL, $redirectUrl);
366         }
367
368         // load the client with GET
369         header('Location: ' . $redirectUrl);
370     }
371
372     /**
373      * put jsonKey into separate cookie
374      *
375      * this is needed if login is not done by the client itself
376      */
377     protected function _setJsonKeyCookie()
378     {
379         // SSO Login
380         $cookie_params = session_get_cookie_params();
381
382         // don't issue errors in unit tests
383         @setcookie(
384             'TINE20JSONKEY',
385             Tinebase_Core::get('jsonKey'),
386             0,
387             $cookie_params['path'],
388             $cookie_params['domain'],
389             $cookie_params['secure']
390         );
391     }
392
393     /**
394      * checks authentication and display Tine 2.0 main screen 
395      */
396     public function mainScreen()
397     {
398         $this->checkAuth();
399         $this->_renderMainScreen();
400     }
401     
402     /**
403      * handle session exception for http requests
404      * 
405      * we force the client to delete session cookie, but we don't destroy
406      * the session on server side. This way we prevent session DOS from thrid party
407      */
408     public function sessionException()
409     {
410         Tinebase_Session::expireSessionCookie();
411         echo "
412             <script type='text/javascript'>
413                 window.location.href = window.location.href;
414             </script>
415         ";
416         /*
417         ob_start();
418         $html = $this->login();
419         $html = ob_get_clean();
420         
421         $script = "
422             <script type='text/javascript'>
423                 exception = {code: 401};
424                 Ext.onReady(function() {
425                     Ext.MessageBox.show({
426                         title: _('Authorisation Required'), 
427                         msg: _('Your session is not valid. You need to login again.'),
428                         buttons: Ext.Msg.OK,
429                         icon: Ext.MessageBox.WARNING
430                     });
431                 });
432             </script>";
433         
434         echo preg_replace('/<\/head.*>/', $script . '</head>', $html);
435         */
436     }
437     
438     /**
439      * generic http exception occurred
440      */
441     public function exception()
442     {
443         ob_start();
444         $this->_renderMainScreen();
445         $html = ob_get_clean();
446         
447         $script = "
448         <script type='text/javascript'>
449             exception = {code: 400};
450             Ext.onReady(function() {
451                 Ext.MessageBox.show({
452                     title: _('Abnormal End'), 
453                     msg: _('An error occurred, the program ended abnormal.'),
454                     buttons: Ext.Msg.OK,
455                     icon: Ext.MessageBox.WARNING
456                 });
457             });
458         </script>";
459         
460         echo preg_replace('/<\/head.*>/', $script . '</head>', $html);
461     }
462     
463     /**
464      * returns javascript of translations for the currently configured locale
465      * 
466      * @return string (javascript)
467      */
468     public function getJsTranslations()
469     {
470         if (! in_array(TINE20_BUILDTYPE, array('DEBUG', 'RELEASE'))) {
471             $locale = Tinebase_Core::get('locale');
472             $translations = Tinebase_Translation::getJsTranslations($locale);
473             header('Content-Type: application/javascript');
474             die($translations);
475         }
476
477         $this->_deliverChangedFiles('lang');
478     }
479     
480     /**
481      * return javascript files if changed
482      * 
483      */
484     public function getJsFiles()
485     {
486         $this->_deliverChangedFiles('js');
487     }
488
489     /**
490      * check if js files have changed and return all js as one big file or return "HTTP/1.0 304 Not Modified" if files don't have changed
491      * 
492      * @param string $_fileType
493      * @param array $filesToWatch
494      */
495     protected function _deliverChangedFiles($_fileType, $filesToWatch=null)
496     {
497         // close session to allow other requests
498         Tinebase_Session::writeClose(true);
499
500         $filesToWatch = $filesToWatch ? $filesToWatch : $this->_getFilesToWatch($_fileType);
501         if ($_fileType == 'js' && TINE20_BUILDTYPE != 'DEVELOPMENT') {
502             $customJSFiles = Tinebase_Config::getInstance()->get(Tinebase_Config::FAT_CLIENT_CUSTOM_JS);
503             if (! empty($customJSFiles)) {
504                 $filesToWatch = array_merge($filesToWatch, (array)$customJSFiles);
505             }
506         }
507
508         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__CLASS__ . '::' . __METHOD__
509             . ' (' . __LINE__ .') Got files to watch: ' . print_r($filesToWatch, true));
510
511         // cache for one day
512         $maxAge = 86400;
513         header('Cache-Control: private, max-age=' . $maxAge);
514         header("Expires: " . gmdate('D, d M Y H:i:s', Tinebase_DateTime::now()->addSecond($maxAge)->getTimestamp()) . " GMT");
515         
516         // remove Pragma header from session
517         header_remove('Pragma');
518
519         $clientETag = isset($_SERVER['If_None_Match'])
520             ? $_SERVER['If_None_Match']
521             : (isset($_SERVER['HTTP_IF_NONE_MATCH']) ? $_SERVER['HTTP_IF_NONE_MATCH'] : '');
522
523         if (preg_match('/[a-f0-9]{40}/', $clientETag, $matches)) {
524             $clientETag = $matches[0];
525         }
526
527         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__CLASS__ . '::' . __METHOD__
528             . ' (' . __LINE__ .') $clientETag: ' . $clientETag);
529
530         $serverETag = $this->getAssetHash();
531
532         if ($clientETag == $serverETag) {
533             header("HTTP/1.0 304 Not Modified");
534         } else {
535             header('Content-Type: application/javascript');
536             header('Etag: "' . $serverETag . '"');
537
538             // send files to client
539             foreach ($filesToWatch as $file) {
540                 readfile($file);
541             }
542             if ($_fileType != 'lang') {
543                 // adds new server version etag for client version check
544                 echo "Tine.clientVersion.filesHash = '$serverETag';";
545             }
546         }
547     }
548
549     /**
550      * get map of asset files
551      *
552      * @return array
553      */
554     public static function getAssetsMap($asJson=false)
555     {
556         $jsonFile = 'Tinebase/js/webpack-assets-FAT.json';
557
558         if (TINE20_BUILDTYPE =='DEVELOPMENT') {
559             $devServerURL = Tinebase_Config::getInstance()->get('webpackDevServerURL', 'http://localhost:10443');
560             $jsonFileUri = $devServerURL . '/' . $jsonFile;
561             $json = Tinebase_Helper::getFileOrUriContents($jsonFileUri);
562             if (! $json) {
563                 Tinebase_Core::getLogger()->ERR(__CLASS__ . '::' . __METHOD__ . ' (' . __LINE__ .') Could not get json file: ' . $jsonFile);
564                 throw new Exception('You need to run webpack-dev-server in dev mode! See https://wiki.tine20.org/Developers/Getting_Started/Working_with_GIT#Install_webpack');
565             }
566         } else {
567             $json = file_get_contents(dirname(dirname(__DIR__)) . '/' . $jsonFile);
568         }
569
570         return $asJson ? $json : json_decode($json, true);
571     }
572
573     /**
574      * @return string
575      * @throws Exception
576      * @throws Tinebase_Exception_InvalidArgument
577      */
578     public static function getAssetHash()
579     {
580         $enabledApplications = Tinebase_Application::getInstance()->getApplicationsByState(Tinebase_Application::ENABLED)->name;
581         $map = self::getAssetsMap();
582         foreach($map as $asset => $ressources) {
583             if (! $enabledApplications->filter('name', basename($asset))->count()) {
584                 unset($map[$asset]);
585             }
586         }
587
588         return sha1(json_encode($map) . TINE20_BUILDTYPE);
589     }
590
591     /**
592      * @param string $_fileType
593      * @return array
594      * @throws Exception
595      * @throws Tinebase_Exception_InvalidArgument
596      */
597     protected function _getFilesToWatch($_fileType)
598     {
599         $requiredApplications = array('Tinebase', 'Admin', 'Addressbook');
600         $installedApplications = Tinebase_Application::getInstance()->getApplications(null, /* sort = */ 'order')->name;
601         $orderedApplications = array_merge($requiredApplications, array_diff($installedApplications, $requiredApplications));
602         $filesToWatch = array();
603         $fileMap = $this->getAssetsMap();
604
605         foreach ($orderedApplications as $application) {
606             switch($_fileType) {
607                 case 'js':
608                     if (isset($fileMap["{$application}/js/{$application}"])) {
609                         $jsFile = $fileMap["{$application}/js/{$application}"]['js'];
610                     } else {
611                         break;
612                     }
613
614                     if (TINE20_BUILDTYPE === 'DEBUG') {
615                         $jsFile = preg_replace('/\.js$/', '.debug.js', $jsFile);
616                     }
617
618                     $filesToWatch[] = $jsFile;
619                     break;
620                 case 'lang':
621                     $fileName = "{$application}/js/{$application}-lang-" . Tinebase_Core::getLocale() . (TINE20_BUILDTYPE == 'DEBUG' ? '-debug' : null) . '.js';
622                     $lang = Tinebase_Core::getLocale();
623                     $customPath = Tinebase_Config::getInstance()->translations;
624                     $basePath = is_readable("$customPath/$lang/$fileName") ?  "$customPath/$lang" : '.';
625
626                     $langFile = "{$basePath}/{$application}/js/{$application}-lang-" . Tinebase_Core::getLocale() . (TINE20_BUILDTYPE == 'DEBUG' ? '-debug' : null) . '.js';
627                     $filesToWatch[] = $langFile;
628                     break;
629                 default:
630                     throw new Exception('no such fileType');
631                     break;
632             }
633         }
634
635         return $filesToWatch;
636     }
637
638     /**
639      * dev mode custom js delivery
640      */
641     public function getCustomJsFiles()
642     {
643         try {
644             $customJSFiles = Tinebase_Config::getInstance()->get(Tinebase_Config::FAT_CLIENT_CUSTOM_JS);
645             if (! empty($customJSFiles)) {
646                 $this->_deliverChangedFiles('js', $customJSFiles);
647             }
648         } catch (Exception $exception) {
649             Tinebase_Core::getLogger()->WARN(__METHOD__ . '::' . __LINE__ . " can't deliver custom js: \n" . $exception);
650
651         }
652     }
653
654     /**
655      * return last modified timestamp formated in gmt
656      * 
657      * @param  array  $_files
658      * @return array
659      */
660     protected function _getLastModified(array $_files)
661     {
662         $timeStamp = null;
663         
664         foreach ($_files as $file) {
665             $mtime = filemtime($file);
666             if ($mtime > $timeStamp) {
667                 $timeStamp = $mtime;
668             }
669         }
670
671         return gmdate("D, d M Y H:i:s", $timeStamp) . " GMT";
672     }
673
674     /**
675      * receives file uploads and stores it in the file_uploads db
676      * 
677      * @throws Tinebase_Exception_UnexpectedValue
678      * @throws Tinebase_Exception_NotFound
679      */
680     public function uploadTempFile()
681     {
682         try {
683             $this->checkAuth();
684             
685             // close session to allow other requests
686             Tinebase_Session::writeClose(true);
687         
688             $tempFile = Tinebase_TempFile::getInstance()->uploadTempFile();
689             
690             die(Zend_Json::encode(array(
691                'status'   => 'success',
692                'tempFile' => $tempFile->toArray(),
693             )));
694         } catch (Tinebase_Exception $exception) {
695             Tinebase_Core::getLogger()->WARN(__METHOD__ . '::' . __LINE__ . " File upload could not be done, due to the following exception: \n" . $exception);
696             
697             if (! headers_sent()) {
698                header("HTTP/1.0 500 Internal Server Error");
699             }
700             die(Zend_Json::encode(array(
701                'status'   => 'failed',
702             )));
703         }
704     }
705     
706     /**
707      * downloads an image/thumbnail at a given size
708      *
709      * @param unknown_type $application
710      * @param string $id
711      * @param string $location
712      * @param int $width
713      * @param int $height
714      * @param int $ratiomode
715      */
716     public function getImage($application, $id, $location, $width, $height, $ratiomode)
717     {
718         $this->checkAuth();
719
720         // close session to allow other requests
721         Tinebase_Session::writeClose(true);
722         
723         $clientETag      = null;
724         $ifModifiedSince = null;
725         
726         if (isset($_SERVER['If_None_Match'])) {
727             $clientETag     = trim($_SERVER['If_None_Match'], '"');
728             $ifModifiedSince = trim($_SERVER['If_Modified_Since'], '"');
729         } elseif (isset($_SERVER['HTTP_IF_NONE_MATCH']) && isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
730             $clientETag     = trim($_SERVER['HTTP_IF_NONE_MATCH'], '"');
731             $ifModifiedSince = trim($_SERVER['HTTP_IF_MODIFIED_SINCE'], '"');
732         }
733         
734         $image = Tinebase_Controller::getInstance()->getImage($application, $id, $location);
735
736         if (is_array($image)) {
737         }
738         
739         $serverETag = sha1($image->blob . $width . $height . $ratiomode);
740         
741         // cache for 3600 seconds
742         $maxAge = 3600;
743         header('Cache-Control: private, max-age=' . $maxAge);
744         header("Expires: " . gmdate('D, d M Y H:i:s', Tinebase_DateTime::now()->addSecond($maxAge)->getTimestamp()) . " GMT");
745         
746         // overwrite Pragma header from session
747         header("Pragma: cache");
748         
749         // if the cache id is still valid
750         if ($clientETag == $serverETag) {
751             header("Last-Modified: " . $ifModifiedSince);
752             header("HTTP/1.0 304 Not Modified");
753             header('Content-Length: 0');
754         } else {
755             #$cache = Tinebase_Core::getCache();
756             
757             #if ($cache->test($serverETag) === true) {
758             #    $image = $cache->load($serverETag);
759             #} else {
760                 if ($width != -1 && $height != -1) {
761                     Tinebase_ImageHelper::resize($image, $width, $height, $ratiomode);
762                 }
763             #    $cache->save($image, $serverETag);
764             #}
765         
766             header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
767             header('Content-Type: '. $image->mime);
768             header('Etag: "' . $serverETag . '"');
769             
770             flush();
771             
772             die($image->blob);
773         }
774     }
775     
776     /**
777      * crops a image identified by an imgageURL and returns a new tempFileImage
778      * 
779      * @param  string $imageurl imageURL of the image to be croped
780      * @param  int    $left     left position of crop window
781      * @param  int    $top      top  position of crop window
782      * @param  int    $widht    widht  of crop window
783      * @param  int    $height   heidht of crop window
784      * @return string imageURL of new temp image
785      * 
786      */
787     public function cropImage($imageurl, $left, $top, $widht, $height)
788     {
789         $this->checkAuth();
790         
791         $image = Tinebase_Model_Image::getImageFromImageURL($imageurl);
792         Tinebase_ImageHelper::crop($image, $left, $top, $widht, $height);
793         
794     }
795     
796     /**
797      * download file attachment
798      * 
799      * @param string $nodeId
800      * @param string $recordId
801      * @param string $modelName
802      */
803     public function downloadRecordAttachment($nodeId, $recordId, $modelName)
804     {
805         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
806             . ' Downloading attachment of ' . $modelName . ' record with id ' . $recordId);
807         
808         $recordController = Tinebase_Core::getApplicationInstance($modelName);
809         $record = $recordController->get($recordId);
810         
811         $node = Tinebase_FileSystem::getInstance()->get($nodeId);
812         $node->grants =
813         $path = Tinebase_Model_Tree_Node_Path::STREAMWRAPPERPREFIX
814             . Tinebase_FileSystem_RecordAttachments::getInstance()->getRecordAttachmentPath($record)
815             . '/' . $node->name;
816         
817         $this->_downloadFileNode($node, $path, /* revision */ null, /* $ignoreAcl */ true);
818         exit;
819     }
820
821     /**
822      * Download temp file to review
823      *
824      * @param $tmpfileId
825      */
826     public function downloadTempfile($tmpfileId)
827     {
828         $tmpFile = Tinebase_TempFile::getInstance()->getTempFile($tmpfileId);
829
830         // some grids can house tempfiles and filemanager nodes, therefor first try tmpfile and if no tmpfile try filemanager
831         if (!$tmpFile && Tinebase_Application::getInstance()->isInstalled('Filemanager')) {
832             $filemanagerNodeController = Filemanager_Controller_Node::getInstance();
833             $file = $filemanagerNodeController->get($tmpfileId);
834
835             $filemanagerHttpFrontend = new Filemanager_Frontend_Http();
836             $filemanagerHttpFrontend->downloadFile($file->path, null);
837         }
838
839         $this->_downloadFileNode($tmpFile, $tmpFile->path);
840         exit;
841     }
842
843     public function getPostalXWindow()
844     {
845         $view = new Zend_View();
846         $view->setScriptPath('Tinebase/views');
847
848         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
849             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' getPostalXWindow');
850
851         $this->_setMainscreenHeaders();
852
853         echo $view->render('postal.xwindow.php');
854         exit();
855     }
856
857     /**
858      * download file
859      *
860      * @param string $_path
861      * @param string $_appId
862      * @param string $_type
863      * @param int $_num
864      * @param string $_revision
865      * @throws Tinebase_Exception_InvalidArgument
866      * @throws Tinebase_Exception_NotFound
867      */
868     public function downloadPreview($_path, $_appId, $_type, $_num = 0, $_revision = null)
869     {
870         $_revision = $_revision ?: null;
871
872         if ($_path) {
873             $path = ltrim($_path, '/');
874
875             if (strpos($path, 'records/') === 0) {
876                 $pathParts = explode('/', $path);
877                 $controller = Tinebase_Core::getApplicationInstance($pathParts[1]);
878
879                 // ACL Check
880                 $controller->get($pathParts[2]);
881
882                 //$pathRecord = Tinebase_Model_Tree_Node_Path::createFromPath();
883                 $node = Tinebase_FileSystem::getInstance()->stat('/' . $_appId . '/folders/' . $path, $_revision);
884             } else {
885                 $pathRecord = Tinebase_Model_Tree_Node_Path::createFromPath('/' . $_appId . '/folders/' . $path);
886                 $node = Filemanager_Controller_Node::getInstance()->getFileNode($pathRecord, $_revision);
887             }
888         } else {
889             throw new Tinebase_Exception_InvalidArgument('A path is needed to download a preview file.');
890         }
891
892         $this->_downloadPreview($node, $_type, $_num);
893
894         exit;
895     }
896 }