a08e773cf41e12ca797e9bf5fa54fe765c44ac62
[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     public static function getAssetHash()
574     {
575         $installedApps = Tinebase_Application::getInstance()->getApplications();
576         $map = self::getAssetsMap();
577         foreach($map as $asset => $ressources) {
578             if (! $installedApps->filter('name', basename($asset))->count()) {
579                 unset($map[$asset]);
580             }
581         }
582
583         return sha1(json_encode($map) . TINE20_BUILDTYPE);
584     }
585
586     /**
587      * @param string $_fileType
588      * @return array
589      * @throws Exception
590      * @throws Tinebase_Exception_InvalidArgument
591      */
592     protected function _getFilesToWatch($_fileType)
593     {
594         $requiredApplications = array('Tinebase', 'Admin', 'Addressbook');
595         $installedApplications = Tinebase_Application::getInstance()->getApplications(null, /* sort = */ 'order')->name;
596         $orderedApplications = array_merge($requiredApplications, array_diff($installedApplications, $requiredApplications));
597         $filesToWatch = array();
598         $fileMap = $this->getAssetsMap();
599
600         foreach ($orderedApplications as $application) {
601             switch($_fileType) {
602                 case 'js':
603                     if (isset($fileMap["{$application}/js/{$application}"])) {
604                         $jsFile = $fileMap["{$application}/js/{$application}"]['js'];
605                     } else {
606                         break;
607                     }
608
609                     if (TINE20_BUILDTYPE === 'DEBUG') {
610                         $jsFile = preg_replace('/\.js$/', '.debug.js', $jsFile);
611                     }
612
613                     $filesToWatch[] = $jsFile;
614                     break;
615                 case 'lang':
616                     $fileName = "{$application}/js/{$application}-lang-" . Tinebase_Core::getLocale() . (TINE20_BUILDTYPE == 'DEBUG' ? '-debug' : null) . '.js';
617                     $lang = Tinebase_Core::getLocale();
618                     $customPath = Tinebase_Config::getInstance()->translations;
619                     $basePath = is_readable("$customPath/$lang/$fileName") ?  "$customPath/$lang" : '.';
620
621                     $langFile = "{$basePath}/{$application}/js/{$application}-lang-" . Tinebase_Core::getLocale() . (TINE20_BUILDTYPE == 'DEBUG' ? '-debug' : null) . '.js';
622                     $filesToWatch[] = $langFile;
623                     break;
624                 default:
625                     throw new Exception('no such fileType');
626                     break;
627             }
628         }
629
630         return $filesToWatch;
631     }
632
633     /**
634      * dev mode custom js delivery
635      */
636     public function getCustomJsFiles()
637     {
638         try {
639             $customJSFiles = Tinebase_Config::getInstance()->get(Tinebase_Config::FAT_CLIENT_CUSTOM_JS);
640             if (! empty($customJSFiles)) {
641                 $this->_deliverChangedFiles('js', $customJSFiles);
642             }
643         } catch (Exception $exception) {
644             Tinebase_Core::getLogger()->WARN(__METHOD__ . '::' . __LINE__ . " can't deliver custom js: \n" . $exception);
645
646         }
647     }
648
649     /**
650      * return last modified timestamp formated in gmt
651      * 
652      * @param  array  $_files
653      * @return array
654      */
655     protected function _getLastModified(array $_files)
656     {
657         $timeStamp = null;
658         
659         foreach ($_files as $file) {
660             $mtime = filemtime($file);
661             if ($mtime > $timeStamp) {
662                 $timeStamp = $mtime;
663             }
664         }
665
666         return gmdate("D, d M Y H:i:s", $timeStamp) . " GMT";
667     }
668
669     /**
670      * receives file uploads and stores it in the file_uploads db
671      * 
672      * @throws Tinebase_Exception_UnexpectedValue
673      * @throws Tinebase_Exception_NotFound
674      */
675     public function uploadTempFile()
676     {
677         try {
678             $this->checkAuth();
679             
680             // close session to allow other requests
681             Tinebase_Session::writeClose(true);
682         
683             $tempFile = Tinebase_TempFile::getInstance()->uploadTempFile();
684             
685             die(Zend_Json::encode(array(
686                'status'   => 'success',
687                'tempFile' => $tempFile->toArray(),
688             )));
689         } catch (Tinebase_Exception $exception) {
690             Tinebase_Core::getLogger()->WARN(__METHOD__ . '::' . __LINE__ . " File upload could not be done, due to the following exception: \n" . $exception);
691             
692             if (! headers_sent()) {
693                header("HTTP/1.0 500 Internal Server Error");
694             }
695             die(Zend_Json::encode(array(
696                'status'   => 'failed',
697             )));
698         }
699     }
700     
701     /**
702      * downloads an image/thumbnail at a given size
703      *
704      * @param unknown_type $application
705      * @param string $id
706      * @param string $location
707      * @param int $width
708      * @param int $height
709      * @param int $ratiomode
710      */
711     public function getImage($application, $id, $location, $width, $height, $ratiomode)
712     {
713         $this->checkAuth();
714
715         // close session to allow other requests
716         Tinebase_Session::writeClose(true);
717         
718         $clientETag      = null;
719         $ifModifiedSince = null;
720         
721         if (isset($_SERVER['If_None_Match'])) {
722             $clientETag     = trim($_SERVER['If_None_Match'], '"');
723             $ifModifiedSince = trim($_SERVER['If_Modified_Since'], '"');
724         } elseif (isset($_SERVER['HTTP_IF_NONE_MATCH']) && isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
725             $clientETag     = trim($_SERVER['HTTP_IF_NONE_MATCH'], '"');
726             $ifModifiedSince = trim($_SERVER['HTTP_IF_MODIFIED_SINCE'], '"');
727         }
728         
729         $image = Tinebase_Controller::getInstance()->getImage($application, $id, $location);
730
731         if (is_array($image)) {
732         }
733         
734         $serverETag = sha1($image->blob . $width . $height . $ratiomode);
735         
736         // cache for 3600 seconds
737         $maxAge = 3600;
738         header('Cache-Control: private, max-age=' . $maxAge);
739         header("Expires: " . gmdate('D, d M Y H:i:s', Tinebase_DateTime::now()->addSecond($maxAge)->getTimestamp()) . " GMT");
740         
741         // overwrite Pragma header from session
742         header("Pragma: cache");
743         
744         // if the cache id is still valid
745         if ($clientETag == $serverETag) {
746             header("Last-Modified: " . $ifModifiedSince);
747             header("HTTP/1.0 304 Not Modified");
748             header('Content-Length: 0');
749         } else {
750             #$cache = Tinebase_Core::getCache();
751             
752             #if ($cache->test($serverETag) === true) {
753             #    $image = $cache->load($serverETag);
754             #} else {
755                 if ($width != -1 && $height != -1) {
756                     Tinebase_ImageHelper::resize($image, $width, $height, $ratiomode);
757                 }
758             #    $cache->save($image, $serverETag);
759             #}
760         
761             header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
762             header('Content-Type: '. $image->mime);
763             header('Etag: "' . $serverETag . '"');
764             
765             flush();
766             
767             die($image->blob);
768         }
769     }
770     
771     /**
772      * crops a image identified by an imgageURL and returns a new tempFileImage
773      * 
774      * @param  string $imageurl imageURL of the image to be croped
775      * @param  int    $left     left position of crop window
776      * @param  int    $top      top  position of crop window
777      * @param  int    $widht    widht  of crop window
778      * @param  int    $height   heidht of crop window
779      * @return string imageURL of new temp image
780      * 
781      */
782     public function cropImage($imageurl, $left, $top, $widht, $height)
783     {
784         $this->checkAuth();
785         
786         $image = Tinebase_Model_Image::getImageFromImageURL($imageurl);
787         Tinebase_ImageHelper::crop($image, $left, $top, $widht, $height);
788         
789     }
790     
791     /**
792      * download file attachment
793      * 
794      * @param string $nodeId
795      * @param string $recordId
796      * @param string $modelName
797      */
798     public function downloadRecordAttachment($nodeId, $recordId, $modelName)
799     {
800         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
801             . ' Downloading attachment of ' . $modelName . ' record with id ' . $recordId);
802         
803         $recordController = Tinebase_Core::getApplicationInstance($modelName);
804         $record = $recordController->get($recordId);
805         
806         $node = Tinebase_FileSystem::getInstance()->get($nodeId);
807         $node->grants =
808         $path = Tinebase_Model_Tree_Node_Path::STREAMWRAPPERPREFIX
809             . Tinebase_FileSystem_RecordAttachments::getInstance()->getRecordAttachmentPath($record)
810             . '/' . $node->name;
811         
812         $this->_downloadFileNode($node, $path, /* revision */ null, /* $ignoreAcl */ true);
813         exit;
814     }
815
816     /**
817      * Download temp file to review
818      *
819      * @param $tmpfileId
820      */
821     public function downloadTempfile($tmpfileId)
822     {
823         $tmpFile = Tinebase_TempFile::getInstance()->getTempFile($tmpfileId);
824
825         // some grids can house tempfiles and filemanager nodes, therefor first try tmpfile and if no tmpfile try filemanager
826         if (!$tmpFile && Tinebase_Application::getInstance()->isInstalled('Filemanager')) {
827             $filemanagerNodeController = Filemanager_Controller_Node::getInstance();
828             $file = $filemanagerNodeController->get($tmpfileId);
829
830             $filemanagerHttpFrontend = new Filemanager_Frontend_Http();
831             $filemanagerHttpFrontend->downloadFile($file->path, null);
832         }
833
834         $this->_downloadFileNode($tmpFile, $tmpFile->path);
835         exit;
836     }
837
838     public function getPostalXWindow()
839     {
840         $view = new Zend_View();
841         $view->setScriptPath('Tinebase/views');
842
843         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG))
844             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' getPostalXWindow');
845
846         $this->_setMainscreenHeaders();
847
848         echo $view->render('postal.xwindow.php');
849         exit();
850     }
851
852     /**
853      * download file
854      *
855      * @param string $_path
856      * @param string $_appId
857      * @param string $_type
858      * @param int $_num
859      * @param string $_revision
860      * @throws Tinebase_Exception_InvalidArgument
861      * @throws Tinebase_Exception_NotFound
862      */
863     public function downloadPreview($_path, $_appId, $_type, $_num = 0, $_revision = null)
864     {
865         $_revision = $_revision ?: null;
866
867         if ($_path) {
868             $path = ltrim($_path, '/');
869
870             if (strpos($path, 'records/') === 0) {
871                 $pathParts = explode('/', $path);
872                 $controller = Tinebase_Core::getApplicationInstance($pathParts[1]);
873
874                 // ACL Check
875                 $controller->get($pathParts[2]);
876
877                 //$pathRecord = Tinebase_Model_Tree_Node_Path::createFromPath();
878                 $node = Tinebase_FileSystem::getInstance()->stat('/' . $_appId . '/folders/' . $path, $_revision);
879             } else {
880                 $pathRecord = Tinebase_Model_Tree_Node_Path::createFromPath('/' . $_appId . '/folders/' . $path);
881                 $node = Filemanager_Controller_Node::getInstance()->getFileNode($pathRecord, $_revision);
882             }
883         } else {
884             throw new Tinebase_Exception_InvalidArgument('A path is needed to download a preview file.');
885         }
886
887         $this->_downloadPreview($node, $_type, $_num);
888
889         exit;
890     }
891 }