*/ /** * HTTP interface to Tine * * @package Tinebase * @subpackage Server */ class Tinebase_Frontend_Http extends Tinebase_Frontend_Http_Abstract { const REQUEST_TYPE = 'HttpPost'; /** * get json-api service map * * @return string */ public static function getServiceMap() { $smd = Tinebase_Server_Json::getServiceMap(); $smdArray = $smd->toArray(); unset($smdArray['methods']); if (! isset($_REQUEST['method']) || $_REQUEST['method'] != 'Tinebase.getServiceMap') { return $smdArray; } header('Content-type: application/json'); echo '_smd = ' . json_encode($smdArray); die(); } /** * return xrds file * used to autodiscover openId servers * * @return void */ public function getXRDS() { // selfUrl == http://servername/pathtotine20/users/loginname $url = dirname(dirname(Zend_OpenId::selfUrl())) . '/index.php?method=Tinebase.openId'; header('Content-type: application/xrds+xml'); echo ' http://specs.openid.net/auth/2.0/signon ' . $url . ' http://openid.net/signon/1.1 ' . $url . ' http://openid.net/signon/1.0 ' . $url . ' '; } /** * display user info page * * in the future we can display public informations about the user here too * currently it is only used as entry point for openId * * @param string $username the username * @return void */ public function userInfoPage($username) { // selfUrl == http://servername/pathtotine20/users/loginname $openIdUrl = dirname(dirname(Zend_OpenId::selfUrl())) . '/index.php?method=Tinebase.openId'; $view = new Zend_View(); $view->setScriptPath('Tinebase/views'); $view->openIdUrl = $openIdUrl; $view->username = $username; header('Content-Type: text/html; charset=utf-8'); echo $view->render('userInfoPage.php'); } /** * handle all kinds of openId requests * * @return void */ public function openId() { Tinebase_Core::startCoreSession(); $server = new Tinebase_OpenId_Provider( null, null, new Tinebase_OpenId_Provider_User_Tine20(), new Tinebase_OpenId_Provider_Storage() ); $server->setOpEndpoint(dirname(Zend_OpenId::selfUrl()) . '/index.php?method=Tinebase.openId'); // handle openId login form if (isset($_POST['openid_action']) && $_POST['openid_action'] === 'login') { $server->login($_POST['openid_identifier'], $_POST['password'], $_POST['username']); unset($_GET['openid_action']); $this->_setJsonKeyCookie(); Zend_OpenId::redirect(dirname(Zend_OpenId::selfUrl()) . '/index.php', $_GET); // display openId login form } else if (isset($_GET['openid_action']) && $_GET['openid_action'] === 'login') { $view = new Zend_View(); $view->setScriptPath('Tinebase/views'); $view->openIdIdentity = $_GET['openid_identity']; $view->loginName = $_GET['openid_identity']; header('Content-Type: text/html; charset=utf-8'); echo $view->render('openidLogin.php'); // handle openId trust form } else if (isset($_POST['openid_action']) && $_POST['openid_action'] === 'trust') { if (isset($_POST['allow'])) { if (isset($_POST['forever'])) { $server->allowSite($server->getSiteRoot($_GET)); } $server->respondToConsumer($_GET); } else if (isset($_POST['deny'])) { if (isset($_POST['forever'])) { $server->denySite($server->getSiteRoot($_GET)); } Zend_OpenId::redirect($_GET['openid_return_to'], array('openid.mode'=>'cancel')); } // display openId trust form } else if (isset($_GET['openid_action']) && $_GET['openid_action'] === 'trust') { if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . " Display openId trust screen"); $view = new Zend_View(); $view->setScriptPath('Tinebase/views'); $view->openIdConsumer = $server->getSiteRoot($_GET); $view->openIdIdentity = $server->getLoggedInUser(); header('Content-Type: text/html; charset=utf-8'); echo $view->render('openidTrust.php'); // handle all other openId requests } else { $result = $server->handle(); if (is_string($result)) { echo $result; } elseif ($result !== true) { header('HTTP/1.0 403 Forbidden'); return; } } } /** * checks if a user is logged in. If not we redirect to login */ protected function checkAuth() { try { Tinebase_Core::getUser(); } catch (Exception $e) { header('HTTP/1.0 403 Forbidden'); exit; } } /** * renders the login dialog * * @todo perhaps we could add a config option to display the update dialog if it is set */ public function login() { // redirect to REDIRECTURL if set $redirectUrl = Tinebase_Config::getInstance()->get(Tinebase_Config::REDIRECTURL, ''); if ($redirectUrl !== '' && Tinebase_Config::getInstance()->get(Tinebase_Config::REDIRECTALWAYS, FALSE)) { header('Location: ' . $redirectUrl); return; } // check if setup/update required $setupController = Setup_Controller::getInstance(); $applications = Tinebase_Application::getInstance()->getApplicationsByState(Tinebase_Application::ENABLED); foreach ($applications as $application) { if ($setupController->updateNeeded($application)) { if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " " . $application->name . ' needs an update'); $this->setupRequired(); } } $this->_renderMainScreen(); /** * old code used to display user registration * @todo must be reworked * $view = new Zend_View(); $view->setScriptPath('Tinebase/views'); header('Content-Type: text/html; charset=utf-8'); // check if registration is active if(isset(Tinebase_Core::getConfig()->login)) { $registrationConfig = Tinebase_Core::getConfig()->registration; $view->userRegistration = (isset($registrationConfig->active)) ? $registrationConfig->active : ''; } else { $view->userRegistration = 0; } echo $view->render('jsclient.php'); */ } /** * renders the main screen * * @return void */ protected function _renderMainScreen() { $view = new Zend_View(); $baseDir = dirname(dirname(dirname(__FILE__))); $view->setScriptPath("$baseDir/Tinebase/views"); if (TINE20_BUILDTYPE =='DEVELOPMENT') { $jsFilestoInclude = $this->_getFilesToWatch('js'); $view->devIncludes = $jsFilestoInclude; } $view->registryData = array(); $this->_setMainscreenHeaders(); echo $view->render('jsclient.php'); } /** * set headers for mainscreen */ protected function _setMainscreenHeaders() { if (headers_sent()) { return; } header('Content-Type: text/html; charset=utf-8'); // obsoleted by CSP see https://www.w3.org/TR/CSP2/#directive-frame-ancestors //header('X-Frame-Options: SAMEORIGIN'); $frameAncestors = implode(' ' ,array_merge( (array) Tinebase_Core::getConfig()->get(Tinebase_Config::ALLOWEDJSONORIGINS, array()), array("'self'") )); // set Content-Security-Policy header against clickjacking and XSS // @see https://developer.mozilla.org/en/Security/CSP/CSP_policy_directives $scriptSrcs = array("'self'", "'unsafe-eval'", 'https://versioncheck.tine20.net'); if (TINE20_BUILDTYPE == 'DEVELOPMENT') { $scriptSrcs[] = Tinebase_Core::getUrl('protocol') . '://' . Tinebase_Core::getUrl('host') . ":10443"; } $scriptSrc = implode(' ', $scriptSrcs); header("Content-Security-Policy: default-src 'self'"); header("Content-Security-Policy: script-src $scriptSrc"); header("Content-Security-Policy: frame-ancestors $frameAncestors"); // headers for IE 10+11 header("X-Content-Security-Policy: default-src 'self'"); header("X-Content-Security-Policy: script-src $scriptSrc"); header("X-Content-Security-Policy: frame-ancestors $frameAncestors"); // set Strict-Transport-Security; used only when served over HTTPS header('Strict-Transport-Security: max-age=16070400'); // cache mainscreen for one day in production $maxAge = ! defined('TINE20_BUILDTYPE') || TINE20_BUILDTYPE != 'DEVELOPMENT' ? 86400 : -10000; header('Cache-Control: private, max-age=' . $maxAge); header("Expires: " . gmdate('D, d M Y H:i:s', Tinebase_DateTime::now()->addSecond($maxAge)->getTimestamp()) . " GMT"); header_remove('Pragma'); } /** * renders the setup/update required dialog */ public function setupRequired() { $view = new Zend_View(); $view->setScriptPath('Tinebase/views'); if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->DEBUG(__CLASS__ . '::' . __METHOD__ . ' (' . __LINE__ .') Update/Setup required!'); header('Content-Type: text/html; charset=utf-8'); echo $view->render('update.php'); exit(); } /** * login from HTTP post * * redirects the tine main screen if authentication is successful * otherwise redirects back to login url */ public function loginFromPost($username, $password) { Tinebase_Core::startCoreSession(); if (!empty($username)) { // try to login user $success = (Tinebase_Controller::getInstance()->login( $username, $password, Tinebase_Core::get(Tinebase_Core::REQUEST), self::REQUEST_TYPE ) === TRUE); } else { $success = FALSE; } if ($success === TRUE) { $this->_setJsonKeyCookie(); $ccAdapter = Tinebase_Auth_CredentialCache::getInstance()->getCacheAdapter(); if (Tinebase_Core::isRegistered(Tinebase_Core::USERCREDENTIALCACHE)) { $ccAdapter->setCache(Tinebase_Core::getUserCredentialCache()); } else { Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' Something went wrong with the CredentialCache / no CC registered.'); $success = FALSE; $ccAdapter->resetCache(); } } $request = new Sabre\HTTP\Request(); $redirectUrl = str_replace('index.php', '', $request->getAbsoluteUri()); // authentication failed if ($success !== TRUE) { $_SESSION = array(); Tinebase_Session::destroyAndRemoveCookie(); // redirect back to loginurl if needed $redirectUrl = Tinebase_Config::getInstance()->get(Tinebase_Config::REDIRECTURL, $redirectUrl); } // load the client with GET header('Location: ' . $redirectUrl); } /** * put jsonKey into separate cookie * * this is needed if login is not done by the client itself */ protected function _setJsonKeyCookie() { // SSO Login $cookie_params = session_get_cookie_params(); // don't issue errors in unit tests @setcookie( 'TINE20JSONKEY', Tinebase_Core::get('jsonKey'), 0, $cookie_params['path'], $cookie_params['domain'], $cookie_params['secure'] ); } /** * checks authentication and display Tine 2.0 main screen */ public function mainScreen() { $this->checkAuth(); $this->_renderMainScreen(); } /** * handle session exception for http requests * * we force the client to delete session cookie, but we don't destroy * the session on server side. This way we prevent session DOS from thrid party */ public function sessionException() { Tinebase_Session::expireSessionCookie(); echo " "; /* ob_start(); $html = $this->login(); $html = ob_get_clean(); $script = " "; echo preg_replace('/<\/head.*>/', $script . '', $html); */ } /** * generic http exception occurred */ public function exception() { ob_start(); $this->_renderMainScreen(); $html = ob_get_clean(); $script = " "; echo preg_replace('/<\/head.*>/', $script . '', $html); } /** * returns javascript of translations for the currently configured locale * * @return string (javascript) */ public function getJsTranslations() { if (! in_array(TINE20_BUILDTYPE, array('DEBUG', 'RELEASE'))) { $locale = Tinebase_Core::get('locale'); $translations = Tinebase_Translation::getJsTranslations($locale); header('Content-Type: application/javascript'); die($translations); } $this->_deliverChangedFiles('lang'); } /** * return javascript files if changed * */ public function getJsFiles() { $this->_deliverChangedFiles('js'); } /** * 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 * * @param string $_fileType * @param array $filesToWatch */ protected function _deliverChangedFiles($_fileType, $filesToWatch=null) { // close session to allow other requests Tinebase_Session::writeClose(true); $filesToWatch = $filesToWatch ? $filesToWatch : $this->_getFilesToWatch($_fileType); if ($_fileType == 'js' && TINE20_BUILDTYPE != 'DEVELOPMENT') { $customJSFiles = Tinebase_Config::getInstance()->get(Tinebase_Config::FAT_CLIENT_CUSTOM_JS); if (! empty($customJSFiles)) { $filesToWatch = array_merge($filesToWatch, (array)$customJSFiles); } } if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__CLASS__ . '::' . __METHOD__ . ' (' . __LINE__ .') Got files to watch: ' . print_r($filesToWatch, true)); // cache for one day $maxAge = 86400; header('Cache-Control: private, max-age=' . $maxAge); header("Expires: " . gmdate('D, d M Y H:i:s', Tinebase_DateTime::now()->addSecond($maxAge)->getTimestamp()) . " GMT"); // remove Pragma header from session header_remove('Pragma'); $clientETag = isset($_SERVER['If_None_Match']) ? $_SERVER['If_None_Match'] : (isset($_SERVER['HTTP_IF_NONE_MATCH']) ? $_SERVER['HTTP_IF_NONE_MATCH'] : ''); if (preg_match('/[a-f0-9]{40}/', $clientETag, $matches)) { $clientETag = $matches[0]; } if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__CLASS__ . '::' . __METHOD__ . ' (' . __LINE__ .') $clientETag: ' . $clientETag); $serverETag = $this->getAssetHash(); if ($clientETag == $serverETag) { header("HTTP/1.0 304 Not Modified"); } else { header('Content-Type: application/javascript'); header('Etag: "' . $serverETag . '"'); // send files to client foreach ($filesToWatch as $file) { readfile($file); } if ($_fileType != 'lang') { // adds new server version etag for client version check echo "Tine.clientVersion.filesHash = '$serverETag';"; } } } /** * get map of asset files * * @return array */ public static function getAssetsMap($asJson=false) { $jsonFile = 'Tinebase/js/webpack-assets-FAT.json'; if (TINE20_BUILDTYPE =='DEVELOPMENT') { $devServerURL = Tinebase_Config::getInstance()->get('webpackDevServerURL', 'http://localhost:10443'); $jsonFileUri = $devServerURL . '/' . $jsonFile; $json = Tinebase_Helper::getFileOrUriContents($jsonFileUri); if (! $json) { Tinebase_Core::getLogger()->ERR(__CLASS__ . '::' . __METHOD__ . ' (' . __LINE__ .') Could not get json file: ' . $jsonFile); 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'); } } else { $json = file_get_contents(dirname(dirname(__DIR__)) . '/' . $jsonFile); } return $asJson ? $json : json_decode($json, true); } /** * @return string * @throws Exception * @throws Tinebase_Exception_InvalidArgument */ public static function getAssetHash() { $enabledApplications = Tinebase_Application::getInstance()->getApplicationsByState(Tinebase_Application::ENABLED)->name; $map = self::getAssetsMap(); foreach($map as $asset => $ressources) { if (! $enabledApplications->filter('name', basename($asset))->count()) { unset($map[$asset]); } } return sha1(json_encode($map) . TINE20_BUILDTYPE); } /** * @param string $_fileType * @return array * @throws Exception * @throws Tinebase_Exception_InvalidArgument */ protected function _getFilesToWatch($_fileType) { $requiredApplications = array('Tinebase', 'Admin', 'Addressbook'); $installedApplications = Tinebase_Application::getInstance()->getApplications(null, /* sort = */ 'order')->name; $orderedApplications = array_merge($requiredApplications, array_diff($installedApplications, $requiredApplications)); $filesToWatch = array(); $fileMap = $this->getAssetsMap(); foreach ($orderedApplications as $application) { switch($_fileType) { case 'js': if (isset($fileMap["{$application}/js/{$application}"])) { $jsFile = $fileMap["{$application}/js/{$application}"]['js']; } else { break; } if (TINE20_BUILDTYPE === 'DEBUG') { $jsFile = preg_replace('/\.js$/', '.debug.js', $jsFile); } $filesToWatch[] = $jsFile; break; case 'lang': $fileName = "{$application}/js/{$application}-lang-" . Tinebase_Core::getLocale() . (TINE20_BUILDTYPE == 'DEBUG' ? '-debug' : null) . '.js'; $lang = Tinebase_Core::getLocale(); $customPath = Tinebase_Config::getInstance()->translations; $basePath = is_readable("$customPath/$lang/$fileName") ? "$customPath/$lang" : '.'; $langFile = "{$basePath}/{$application}/js/{$application}-lang-" . Tinebase_Core::getLocale() . (TINE20_BUILDTYPE == 'DEBUG' ? '-debug' : null) . '.js'; $filesToWatch[] = $langFile; break; default: throw new Exception('no such fileType'); break; } } return $filesToWatch; } /** * dev mode custom js delivery */ public function getCustomJsFiles() { try { $customJSFiles = Tinebase_Config::getInstance()->get(Tinebase_Config::FAT_CLIENT_CUSTOM_JS); if (! empty($customJSFiles)) { $this->_deliverChangedFiles('js', $customJSFiles); } } catch (Exception $exception) { Tinebase_Core::getLogger()->WARN(__METHOD__ . '::' . __LINE__ . " can't deliver custom js: \n" . $exception); } } /** * return last modified timestamp formated in gmt * * @param array $_files * @return array */ protected function _getLastModified(array $_files) { $timeStamp = null; foreach ($_files as $file) { $mtime = filemtime($file); if ($mtime > $timeStamp) { $timeStamp = $mtime; } } return gmdate("D, d M Y H:i:s", $timeStamp) . " GMT"; } /** * receives file uploads and stores it in the file_uploads db * * @throws Tinebase_Exception_UnexpectedValue * @throws Tinebase_Exception_NotFound */ public function uploadTempFile() { try { $this->checkAuth(); // close session to allow other requests Tinebase_Session::writeClose(true); $tempFile = Tinebase_TempFile::getInstance()->uploadTempFile(); die(Zend_Json::encode(array( 'status' => 'success', 'tempFile' => $tempFile->toArray(), ))); } catch (Tinebase_Exception $exception) { Tinebase_Core::getLogger()->WARN(__METHOD__ . '::' . __LINE__ . " File upload could not be done, due to the following exception: \n" . $exception); if (! headers_sent()) { header("HTTP/1.0 500 Internal Server Error"); } die(Zend_Json::encode(array( 'status' => 'failed', ))); } } /** * downloads an image/thumbnail at a given size * * @param unknown_type $application * @param string $id * @param string $location * @param int $width * @param int $height * @param int $ratiomode */ public function getImage($application, $id, $location, $width, $height, $ratiomode) { $this->checkAuth(); // close session to allow other requests Tinebase_Session::writeClose(true); $clientETag = null; $ifModifiedSince = null; if (isset($_SERVER['If_None_Match'])) { $clientETag = trim($_SERVER['If_None_Match'], '"'); $ifModifiedSince = trim($_SERVER['If_Modified_Since'], '"'); } elseif (isset($_SERVER['HTTP_IF_NONE_MATCH']) && isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) { $clientETag = trim($_SERVER['HTTP_IF_NONE_MATCH'], '"'); $ifModifiedSince = trim($_SERVER['HTTP_IF_MODIFIED_SINCE'], '"'); } $image = Tinebase_Controller::getInstance()->getImage($application, $id, $location); if (is_array($image)) { } $serverETag = sha1($image->blob . $width . $height . $ratiomode); // cache for 3600 seconds $maxAge = 3600; header('Cache-Control: private, max-age=' . $maxAge); header("Expires: " . gmdate('D, d M Y H:i:s', Tinebase_DateTime::now()->addSecond($maxAge)->getTimestamp()) . " GMT"); // overwrite Pragma header from session header("Pragma: cache"); // if the cache id is still valid if ($clientETag == $serverETag) { header("Last-Modified: " . $ifModifiedSince); header("HTTP/1.0 304 Not Modified"); header('Content-Length: 0'); } else { #$cache = Tinebase_Core::getCache(); #if ($cache->test($serverETag) === true) { # $image = $cache->load($serverETag); #} else { if ($width != -1 && $height != -1) { Tinebase_ImageHelper::resize($image, $width, $height, $ratiomode); } # $cache->save($image, $serverETag); #} header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT"); header('Content-Type: '. $image->mime); header('Etag: "' . $serverETag . '"'); flush(); die($image->blob); } } /** * crops a image identified by an imgageURL and returns a new tempFileImage * * @param string $imageurl imageURL of the image to be croped * @param int $left left position of crop window * @param int $top top position of crop window * @param int $widht widht of crop window * @param int $height heidht of crop window * @return string imageURL of new temp image * */ public function cropImage($imageurl, $left, $top, $widht, $height) { $this->checkAuth(); $image = Tinebase_Model_Image::getImageFromImageURL($imageurl); Tinebase_ImageHelper::crop($image, $left, $top, $widht, $height); } /** * download file attachment * * @param string $nodeId * @param string $recordId * @param string $modelName */ public function downloadRecordAttachment($nodeId, $recordId, $modelName) { if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Downloading attachment of ' . $modelName . ' record with id ' . $recordId); $recordController = Tinebase_Core::getApplicationInstance($modelName); $record = $recordController->get($recordId); $node = Tinebase_FileSystem::getInstance()->get($nodeId); $node->grants = $path = Tinebase_Model_Tree_Node_Path::STREAMWRAPPERPREFIX . Tinebase_FileSystem_RecordAttachments::getInstance()->getRecordAttachmentPath($record) . '/' . $node->name; $this->_downloadFileNode($node, $path, /* revision */ null, /* $ignoreAcl */ true); exit; } /** * Download temp file to review * * @param $tmpfileId */ public function downloadTempfile($tmpfileId) { $tmpFile = Tinebase_TempFile::getInstance()->getTempFile($tmpfileId); // some grids can house tempfiles and filemanager nodes, therefor first try tmpfile and if no tmpfile try filemanager if (!$tmpFile && Tinebase_Application::getInstance()->isInstalled('Filemanager')) { $filemanagerNodeController = Filemanager_Controller_Node::getInstance(); $file = $filemanagerNodeController->get($tmpfileId); $filemanagerHttpFrontend = new Filemanager_Frontend_Http(); $filemanagerHttpFrontend->downloadFile($file->path, null); } $this->_downloadFileNode($tmpFile, $tmpFile->path); exit; } public function getPostalXWindow() { $view = new Zend_View(); $view->setScriptPath('Tinebase/views'); if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' getPostalXWindow'); $this->_setMainscreenHeaders(); echo $view->render('postal.xwindow.php'); exit(); } /** * download file * * @param string $_path * @param string $_appId * @param string $_type * @param int $_num * @param string $_revision * @throws Tinebase_Exception_InvalidArgument * @throws Tinebase_Exception_NotFound */ public function downloadPreview($_path, $_appId, $_type, $_num = 0, $_revision = null) { $_revision = $_revision ?: null; if ($_path) { $path = ltrim($_path, '/'); if (strpos($path, 'records/') === 0) { $pathParts = explode('/', $path); $controller = Tinebase_Core::getApplicationInstance($pathParts[1]); // ACL Check $controller->get($pathParts[2]); //$pathRecord = Tinebase_Model_Tree_Node_Path::createFromPath(); $node = Tinebase_FileSystem::getInstance()->stat('/' . $_appId . '/folders/' . $path, $_revision); } else { $pathRecord = Tinebase_Model_Tree_Node_Path::createFromPath('/' . $_appId . '/folders/' . $path); $node = Filemanager_Controller_Node::getInstance()->getFileNode($pathRecord, $_revision); } } else { throw new Tinebase_Exception_InvalidArgument('A path is needed to download a preview file.'); } $this->_downloadPreview($node, $_type, $_num); exit; } }