<?php
namespace App\Controller;
use App\Model\Account;
use App\Security\User;
use App\Util\AccountTools;
use App\Util\Api;
use App\Util\AppLogger;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class IndexController extends AbstractController
{
private $params;
private $security;
private $session;
private $logger;
private $translator;
private $requestStack;
public function __construct(ParameterBagInterface $params, Security $security, AppLogger $logger,
TranslatorInterface $translator, RequestStack $requestStack)
{
$this->params = $params;
$this->security = $security;
$this->logger = $logger;
$this->translator = $translator;
$this->requestStack = $requestStack;
$this->session = $this->requestStack->getSession();
}
/**
* @Route("/", name="index")
*/
public function index(Request $request)
{
return $this->redirectToRoute('qu_main');
}
/**
* for session refreshing (AJAX calls)
* @Route("/refresh", name="refresh")
*/
public function refresh(Request $request)
{
$account = $this->security->getUser()->getAccount();
$sessionTime = $account->findSOption('cfgSessionTime') == null ? $this->params->get('session_max_idle_time') : $account->findSOption('cfgSessionTime');
$sessionEndTime = time() + $sessionTime;
return new Response($sessionEndTime);
}
/**
* @Route("/remind-password", name="remind-password")
*/
public function passwordRemind(Request $request)
{
return $this->render('security/password_remind.html.twig');
}
/**
* @Route("/change_lang/{lang}", name="change_lang", requirements={"lang"="en|pl|es"})
*/
public function changeLanguage($lang, Request $request)
{
$referer = $request->headers->get('referer');
if($referer == null || $referer == "") {
$referer = "/";
}
$this->requestStack->getSession()->set('sessionlocale', $lang);
$cookie = Cookie::create(User::LOCALE_COOKIE_NAME, $lang, time() + 24*7*3600, '/');
$response = $this->redirect($referer);
$response->headers->setCookie($cookie);
return $response;
}
/**
* @Route("/app_init.js", name="js_init")
*/
public function jsAppInit(Request $request)
{
$sessionTime = $this->params->get('session_max_idle_time');
$paginationItems = Account::NUMBER_OF_ITEMS;
$userPaginationItems = Account::NUMBER_OF_ITEMS;
$historyPaginationItems = Account::NUMBER_OF_ITEMS;
$passComplexity = '';
$options = [];
$totalMem = [];
if($this->security->getUser() != null) {
$account = $this->security->getUser()->getAccount();
$passComplexity = $this->security->getUser()->getPassComplexity();
$paginationItems = $account->findSOption('cfgPagination') == null ?
(isset($_SERVER['APP_DEFAULT_PER_PAGE']) ? $_SERVER['APP_DEFAULT_PER_PAGE'] : Account::NUMBER_OF_ITEMS)
: $account->findSOption('cfgPagination');
$userPaginationItems = $account->findSOption('cfgUserPagination') == null ?
(isset($_SERVER['APP_DEFAULT_PER_PAGE']) ? $_SERVER['APP_DEFAULT_PER_PAGE'] : Account::NUMBER_OF_ITEMS)
: $account->findSOption('cfgUserPagination');
$historyPaginationItems = $account->findSOption('cfgHistoryPagination') == null ?
(isset($_SERVER['APP_DEFAULT_PER_PAGE']) ? $_SERVER['APP_DEFAULT_PER_PAGE'] : Account::NUMBER_OF_ITEMS)
: $account->findSOption('cfgHistoryPagination');
$sessionTime = $account->findSOption('cfgSessionTime') == null ? $this->params->get('session_max_idle_time') : $account->findSOption('cfgSessionTime');
$options = $account->getSOptions();
$totalMem = [];
$totalMem['mem'] = $totalMem['swap'] = 0;
$userRoles = $this->security->getUser()->getRoles();
// total mem only for admin's chars
if(array_key_exists('HTTP_REFERER', $_SERVER) && str_contains($_SERVER['HTTP_REFERER'], 'charts')) {
foreach($userRoles as $role) {
if($role == 'ROLE_ADMIN') {
$mem = Api::getMemoryStats();
$totalMem['mem'] = $mem['mem']['total'];
$totalMem['swap'] = $mem['swap']['total'];
break;
}
}
}
}
$clearPasswords = 'false';
if(isset($_SERVER['APP_ALLOW_CLEAR_PASS'])) {
if($_SERVER['APP_ALLOW_CLEAR_PASS'] == 'true') {
$clearPasswords = 'true';
}
}
$catchallAccounts = [];
if($this->session->has('catchallAccounts')) {
$catchallAccounts = $this->session->get('catchallAccounts');
}
else {
try {
$cas = Api::getCatchalls();
foreach($cas as $ca) {
$catchallAccounts[$ca->param->svalue] = $ca->user_name . '@' . $ca->user_domain;
}
$this->session->set('catchallAccounts', $catchallAccounts);
}
catch(\Exception $e) {}
}
$sessionEndTime = time() + $sessionTime;
$systemAliases = [];
if(isset($_SERVER['APP_SYSTEM_ALIASES'])) {
$systemAliases = AccountTools::envToArray($_SERVER['APP_SYSTEM_ALIASES']);
}
$passGenReq = ['lower', 'upper', 'digit', 'special'];
if(isset($_SERVER['APP_PASS_GEN_REQ'])) {
$passGenReq = AccountTools::envToArray($_SERVER['APP_PASS_GEN_REQ']);
}
$passGenLength = 8;
if(isset($_SERVER['APP_PASS_GEN_LENGTH'])) {
$passGenLength = $_SERVER['APP_PASS_GEN_LENGTH'];
}
return $this->render('app_init.js.twig', [
'apiEndpoint' => Api::getEndpoint(),
'sessionEndTime' => $sessionEndTime,
'passComplexity' => $passComplexity,
'paginationItems' => $paginationItems,
'userPaginationItems' => $userPaginationItems,
'historyPaginationItems' => $historyPaginationItems,
'locale' => $this->requestStack->getSession()->get('sessionlocale'),
'userOptions' => json_encode($options),
'clearPasswords' => $clearPasswords,
'catchallAccounts' => $catchallAccounts,
'systemAliases' => $systemAliases,
'passGenLength' => $passGenLength,
'passGenReq' => $passGenReq,
'totalMem' => $totalMem,
]);
}
/**
* @Route("/relog-webmail/{forceUserPanel}/{forceLang}/{msg}", defaults={"forceLang"="", "forceUserPanel"=0, "msg"=""}, name="relog-webmail", requirements={"forceLang"="[a-z-]{2}"})
*
* @param $forceLang string - force language in webmail (pl / en / es) or '--' if no forcing
* @param $forceUserPanel - force return URL to point to panel start page (1). If 0 than return URL is referrer
* @param $msg - if not null show message before relog
*/
public function relogWebmail(Request $request, $forceLang, $forceUserPanel, $msg)
{
$systemHealth = Api::checkSystemHealth();
// allow login while API is not responding
if($systemHealth == 1) {
$this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY');
}
$account = $this->security->getUser()->getAccount();
$allowedServices = $account->getAllowedServices();
if(!in_array('Webmail', $allowedServices)) {
$this->addFlash('error', $this->translator->trans('Webmail access is not allowed.'));
return $this->redirectToRoute("qu_main");
}
$referer = $request->headers->get('referer');
if(!$this->requestStack->getSession()->has('storedPassword')) {
if(strlen($referer)) {
return $this->redirect($referer);
}
return $this->redirectToRoute("app_login");
}
if(!isset($_SERVER['ROUNDCUBE_URL'])) {
die('ROUNDCUBE_URL not defined');
}
$imapUser = AccountTools::decryptString($this->requestStack->getSession()->get('storedUsername'));
$imapPass = urlencode(AccountTools::decryptString($this->requestStack->getSession()->get('storedPassword')));
if($systemHealth == 1) {
// impersonated login?
if($this->isGranted('IS_IMPERSONATOR')) {
$token = $this->security->getToken();
if ($token instanceof SwitchUserToken) {
$impersonatorUser = $token->getOriginalToken()->getUser();
}
$imapUser = $this->security->getUser()->getUsername() . '*' . $impersonatorUser->getUsername();
}
}
// check if IMAP is working
if($systemHealth == 1) {
$systemStatus = Api::getSystemStatus();
if(($systemStatus['imap_143_status'] != 'OK') && ($systemStatus['imap_993_status'] != 'OK')) {
$this->addFlash('error', $this->translator->trans('IMAP server not responding, relog to webmail not possible.'));
return $this->redirectToRoute("qu_main");
}
}
$ch = curl_init();
$cmd = $_SERVER['ROUNDCUBE_INTERNAL_URL'] . '?_task=login&_qmailux=true';
$data = '_task=login&_action=login&_timezone=_default_&_user=' . $imapUser;
$data .= '&_pass=' . $imapPass . '&_qmailux=true';
$data .= '&submit';
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type:application/x-www-form-urlencoded'));
curl_setopt($ch, CURLOPT_HEADER, true); // return header, too
curl_setopt($ch, CURLOPT_URL, $cmd);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_POST, 1);
if(strstr($_SERVER['ROUNDCUBE_INTERNAL_URL'], 'local_internal') === false) {
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
}
else {
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
}
if(($output = curl_exec($ch)) === false) {
$this->addFlash('error', $this->translator->trans('Relog to webmail not possible, contact administrator.'));
$this->logger->log('IndexController relogWebmail curl error: ', $request, ['error_code' => curl_errno($ch), 'error_message' => curl_error($ch)], 'critical');
return $this->redirectToRoute("qu_main");
}
curl_close($ch);
$outputArr = explode("\n", $output);
$location = '';
foreach($outputArr as $line) {
if(preg_match('/^Set-Cookie:/i', $line)) {
//echo "line = $line<br>";
$cookieArr = explode('; ', str_ireplace('Set-Cookie: ', '', $line));
$cookieNameVal = explode('=', $cookieArr[0]);
if($cookieNameVal[1] == '-del-') {
setcookie($cookieNameVal[0], $cookieNameVal[1], time() - 3600, '/');
}
else {
setcookie($cookieNameVal[0], $cookieNameVal[1], null, '/');
}
}
if(preg_match('/^Location:/i', $line)) {
$location = str_ireplace('Location: ', '', $line);
$location = trim($location);
}
}
if((!array_key_exists('DONT_FORCE_LANG_ROUNDCUBE', $_SERVER)) || ($_SERVER['DONT_FORCE_LANG_ROUNDCUBE'] == false)) {
if($forceLang != '' && $forceLang != '--') {
$forceLang = $this->_localeToLang($forceLang);
setcookie('forcelang', $forceLang, null, '/');
}
}
// set cookie if this is impersonated session
if($this->isGranted('IS_IMPERSONATOR')) {
setcookie('impersonated', 1, 0, '/');
}
else {
setcookie('impersonated', '', time() - 3600, '/');
}
if($forceUserPanel) {
setcookie('panelreturnurl', $this->generateUrl('qu_main'), null, '/');
}
else {
if($referer != '') {
// if referer is login page set main panel page intead
if(strstr($referer, $this->generateUrl('app_login')) || strstr($referer, $this->generateUrl('2fa_login'))) {
if($this->security->isGranted('ROLE_POSTMASTER')) {
setcookie('panelreturnurl', $this->generateUrl('qp_main'), null, '/');
}
else {
setcookie('panelreturnurl', $this->generateUrl('qu_main'), null, '/');
}
}
else {
setcookie('panelreturnurl', $referer, 0, '/');
}
}
else {
setcookie('panelreturnurl', '', time() - 3600, '/');
}
}
if((strlen($msg) > 0) && array_key_exists('APP_LOGIN_TARGET_MSG', $_SERVER) && $_SERVER['APP_LOGIN_TARGET_MSG'] == 'true') {
return $this->forward('\App\Controller\IndexController::relogWebmailShowMsg', [
'retLocation' => $location,
'msg' => urlencode($msg),
]);
}
else {
return $this->redirect($_SERVER['ROUNDCUBE_URL'] . $location);
}
}
/**
* @Route("/relog-webmail-show-msg/{retLocation}/{msg}", name="relog-webmail-show-msg")
*
* @param $retLocation - where to redirect inside webmail
* @param $msg - message to show before relog
*/
public function relogWebmailShowMsg($retLocation, $msg)
{
return $this->render('index/relog_msg.html.twig', [
'retLocation' => $_SERVER['ROUNDCUBE_URL'] . $retLocation,
'msg' => urldecode($msg),
]);
}
/**
* @Route("/general-error/", name="general-error")
*/
public function generalError(Request $request)
{
$account = $this->security->getUser()->getAccount();
$skin = $account->findSOption('cfgSkin') == null ? 'standard' : $account->findSOption('cfgSkin');
return $this->render('skins/' . $skin . '/general_error.html.twig');
}
/**
* catch-all routing in routes.yaml
*/
public function apiPassThrough(Request $request)
{
$uri = $request->getRequestUri();
$cmd = preg_replace('/.*?\/api\//', '', $uri);
$cmdData = [];
$cmdData['command'] = $cmd;
if(($data = $request->request->all())) {
$cmdData['data'] = $data;
}
$this->denyAccessUnlessGranted('API_PASS_THROUGH', $cmdData);
if($data) {
$ret = Api::passThrough($cmd, json_encode($data));
}
else {
$ret = Api::passThrough($cmd);
}
return new Response($ret);
}
/**
* @Route("/check2fa/{token}", name="check-two-factor", requirements={"token"="^[a-z0-9]{48}$"})
*
* checks if the token is valid when user adds new alternative email address for 2FA
*
* @param $token - token to check
*/
public function checkTwoFactor(Request $request, string $token)
{
$skin = 'raw';
if(Api::checkTwoFactorToken($token)) {
$userEmail = Api::getEmailForTwoFactorToken($token);
$userArr = explode('@', $userEmail);
if(count($userArr) != 2) {
$this->addFlash('error', $this->translator->trans('Email for two factor authentication NOT linked - contact administrator'));
$this->logger->log('Error while linking email for two factor authentication - email has no correct format', $request, ['userEmail' => $userEmail], 'error');
}
$account = Api::getAccount($userArr[0], $userArr[1]);
if(!is_null($account)) {
$account->setSOption('emailForAuthConfirmed', '1');
$account->setSOption('cfgTwoFactorEnabled', '1');
if(Api::saveAccountParameters($account)) {
$this->addFlash('notice', $this->translator->trans('Email for two factor authentication successfuly linked. Two factor authentication is now enabled.'));
$this->logger->log('Email for two factor authentication linked', $request, ['userEmail' => $userEmail]);
}
else {
$this->addFlash('error', $this->translator->trans('Email for two factor authentication NOT linked, error saving configuration. Contact administrator.'));
$this->logger->log('Error while linking email for two factor authentication', $request, ['userEmail' => $userEmail], 'error');
}
$skin = $account->findSOption('cfgSkin') == null ? 'standard' : $account->findSOption('cfgSkin');
// if already logged in - reload config
if($this->isGranted('ROLE_USER')) {
$this->getUser()->loadAccount();
$this->getUser()->setForceCodeNextTime();
}
}
}
else {
$this->addFlash('error', $this->translator->trans('Two factor authentication token not valid or expired'));
}
return $this->render('skins/' . $skin . '/check2fa_token.html.twig');
}
private function _localeToLang($locale)
{
if($locale == 'pl') {
return 'pl_PL';
}
if($locale == 'en') {
return 'en_US';
}
if($locale == 'es') {
return 'es_ES';
}
return $locale;
}
}