1<?php
2
3namespace RainLoop;
4
5class Service
6{
7	/**
8	 * @var \MailSo\Base\Http
9	 */
10	private $oHttp;
11
12	/**
13	 * @var \RainLoop\Actions
14	 */
15	private $oActions;
16
17	/**
18	 * @var \RainLoop\ServiceActions
19	 */
20	private $oServiceActions;
21
22	/**
23	 * @return void
24	 */
25	private function __construct()
26	{
27		$this->oHttp = \MailSo\Base\Http::SingletonInstance();
28		$this->oActions = \RainLoop\Api::Actions();
29
30		$this->oServiceActions = new \RainLoop\ServiceActions($this->oHttp, $this->oActions);
31
32		if ($this->oActions->Config()->Get('debug', 'enable', false))
33		{
34			\error_reporting(E_ALL);
35			\ini_set('display_errors', 1);
36		}
37
38		$sServer = \trim($this->oActions->Config()->Get('security', 'custom_server_signature', ''));
39		if (0 < \strlen($sServer))
40		{
41			@\header('Server: '.$sServer, true);
42		}
43
44		$sXFrameOptionsHeader = \trim($this->oActions->Config()->Get('security', 'x_frame_options_header', ''));
45		if (0 < \strlen($sXFrameOptionsHeader))
46		{
47			@\header('X-Frame-Options: '.$sXFrameOptionsHeader, true);
48		}
49
50		$sXssProtectionOptionsHeader = \trim($this->oActions->Config()->Get('security', 'x_xss_protection_header', ''));
51		if (0 < \strlen($sXssProtectionOptionsHeader))
52		{
53			@\header('X-XSS-Protection: '.$sXssProtectionOptionsHeader, true);
54		}
55
56		if ($this->oActions->Config()->Get('labs', 'force_https', false) && !$this->oHttp->IsSecure())
57		{
58			@\header('Location: https://'.$this->oHttp->GetHost(false, false).$this->oHttp->GetUrl(), true);
59			exit(0);
60		}
61
62		$this->localHandle();
63	}
64
65	/**
66	 * @return bool
67	 */
68	public function RunResult()
69	{
70		return true;
71	}
72
73	/**
74	 * @staticvar bool $bOne
75	 * @return bool
76	 */
77	public static function Handle()
78	{
79		static $bOne = null;
80		if (null === $bOne)
81		{
82			$oService = null;
83			if (\class_exists('MailSo\Version'))
84			{
85				$oService = new self();
86			}
87
88			$bOne = $oService->RunResult();
89		}
90
91		return $bOne;
92	}
93
94	/**
95	 * @return \RainLoop\Service
96	 */
97	private function localHandle()
98	{
99		if (!\class_exists('MailSo\Version'))
100		{
101			return $this;
102		}
103
104		$sResult = '';
105		$bCached = false;
106
107		$sQuery = $this->oActions->ParseQueryAuthString();
108
109		$this->oActions->Plugins()->RunHook('filter.http-query', array(&$sQuery));
110		$aPaths = \explode('/', $sQuery);
111		$this->oActions->Plugins()->RunHook('filter.http-paths', array(&$aPaths));
112
113		$bAdmin = false;
114		$sAdminPanelHost = $this->oActions->Config()->Get('security', 'admin_panel_host', '');
115		if (empty($sAdminPanelHost))
116		{
117			$sAdminPanelKey = \strtolower($this->oActions->Config()->Get('security', 'admin_panel_key', 'admin'));
118			$bAdmin = !empty($aPaths[0]) && \strtolower($aPaths[0]) === $sAdminPanelKey;
119		}
120		else if (empty($aPaths[0]) &&
121			\MailSo\Base\Utils::StrToLowerIfAscii($sAdminPanelHost) === \MailSo\Base\Utils::StrToLowerIfAscii($this->oHttp->GetHost()))
122		{
123			$bAdmin = true;
124		}
125
126		if ($this->oHttp->IsPost())
127		{
128			$this->oHttp->ServerNoCache();
129		}
130
131		if ($bAdmin && !$this->oActions->Config()->Get('security', 'allow_admin_panel', true))
132		{
133			$this->oHttp->StatusHeader(403);
134			echo $this->oServiceActions->ErrorTemplates('Access Denied.',
135				'Access to the RainLoop Webmail Admin Panel is not allowed!', true);
136
137			return $this;
138		}
139
140		$bIndex = true;
141		if (0 < \count($aPaths) && !empty($aPaths[0]) && !$bAdmin && 'index' !== \strtolower($aPaths[0]))
142		{
143			$bIndex = false;
144			$sMethodName = 'Service'.\preg_replace('/@.+$/', '', $aPaths[0]);
145			$sMethodExtra = 0 < \strpos($aPaths[0], '@') ? \preg_replace('/^[^@]+@/', '', $aPaths[0]) : '';
146
147			if (\method_exists($this->oServiceActions, $sMethodName) &&
148				\is_callable(array($this->oServiceActions, $sMethodName)))
149			{
150				$this->oServiceActions->SetQuery($sQuery)->SetPaths($aPaths);
151				$sResult = \call_user_func(array($this->oServiceActions, $sMethodName), $sMethodExtra);
152			}
153			else if (!$this->oActions->Plugins()->RunAdditionalPart($aPaths[0], $aPaths))
154			{
155				$bIndex = true;
156			}
157		}
158
159		$bMobile = false;
160		$bMobileDevice = false;
161
162		if ($this->oActions->Config()->Get('labs', 'allow_mobile_version', false))
163		{
164			$bUseMobileVersionForTablets = $this->oActions->Config()->Get('labs', 'use_mobile_version_for_tablets', false);
165
166			$oMobileDetect = new \Detection\MobileDetect();
167			$bMobileDevice = $oMobileDetect->isMobile() &&
168				($bUseMobileVersionForTablets ? true : !$oMobileDetect->isTablet());
169
170			if ($bIndex)
171			{
172				$sMobileType = (string) \RainLoop\Utils::GetCookie(\RainLoop\Actions::RL_MOBILE_TYPE, '');
173				switch ($sMobileType) {
174					default:
175						$sMobileType = '';
176						$bMobile = $bMobileDevice;
177						break;
178					case 'mobile':
179						$bMobile = true;
180						break;
181					case 'desktop':
182						$bMobile = false;
183						break;
184				}
185			}
186		}
187
188		if ($bIndex)
189		{
190			@\header('Content-Security-Policy:');
191			@\header_remove('Content-Security-Policy');
192
193			@header('Content-Type: text/html; charset=utf-8');
194			$this->oHttp->ServerNoCache();
195
196			if (!@\is_dir(APP_DATA_FOLDER_PATH) || !@\is_writable(APP_DATA_FOLDER_PATH))
197			{
198				echo $this->oServiceActions->ErrorTemplates(
199					'Permission denied!',
200					'RainLoop Webmail cannot access to the data folder "'.APP_DATA_FOLDER_PATH.'"'
201				);
202
203				return $this;
204			}
205
206			$aTemplateParameters = $this->indexTemplateParameters($bAdmin, $bMobile, $bMobileDevice);
207
208			$sCacheFileName = '';
209			if ($this->oActions->Config()->Get('labs', 'cache_system_data', true) && !empty($aTemplateParameters['{{BaseHash}}']))
210			{
211				$sCacheFileName = 'TMPL:'.$aTemplateParameters['{{BaseHash}}'];
212				$sResult = $this->oActions->Cacher()->Get($sCacheFileName);
213			}
214
215			if (0 === \strlen($sResult))
216			{
217				$aTemplateParameters['{{BaseTemplates}}'] = $this->oServiceActions->compileTemplates($bAdmin, false);
218				$sResult = \strtr(\file_get_contents(APP_VERSION_ROOT_PATH.'app/templates/Index.html'), $aTemplateParameters);
219
220				$sResult = \RainLoop\Utils::ClearHtmlOutput($sResult);
221				if (0 < \strlen($sCacheFileName))
222				{
223					$this->oActions->Cacher()->Set($sCacheFileName, $sResult);
224				}
225			}
226			else
227			{
228				$bCached = true;
229			}
230
231			$sResult .= '<!--';
232			$sResult .= '[time:'.\substr(\microtime(true) - APP_START, 0, 6);
233
234//			$sResult .= '][version:'.APP_VERSION;
235			if ($this->oActions->IsOpen())
236			{
237				$sResult .= '][AGPLv3';
238			}
239
240			$sResult .= '][cached:'.($bCached ? 'true' : 'false');
241//			$sResult .= '][hash:'.$aTemplateParameters['{{BaseHash}}'];
242//			$sResult .= '][session:'.\md5(\RainLoop\Utils::GetShortToken());
243
244			if ($bMobile)
245			{
246				$sResult .= '][mobile:true';
247			}
248
249			if (\RainLoop\Utils::IsOwnCloud())
250			{
251				$sResult .= '][cloud:true';
252			}
253
254			$sResult .= ']-->';
255		}
256		else
257		{
258			@\header('X-XSS-Protection: 1; mode=block');
259		}
260
261		// Output result
262		echo $sResult;
263		unset($sResult);
264
265		$this->oActions->BootEnd();
266		return $this;
267	}
268
269	/**
270	 * @param string $sPath
271	 *
272	 * @return string
273	 */
274	private function staticPath($sPath)
275	{
276		return $this->oActions->StaticPath($sPath);
277	}
278
279	/**
280	 * @param bool $bAdmin = false
281	 * @param bool $bMobile = false
282	 * @param bool $bMobileDevice = false
283	 *
284	 * @return array
285	 */
286	private function indexTemplateParameters($bAdmin = false, $bMobile = false, $bMobileDevice = false)
287	{
288		$sLanguage = 'en';
289		$sTheme = 'Default';
290
291		list($sLanguage, $sTheme) = $this->oActions->GetLanguageAndTheme($bAdmin, $bMobile);
292
293		$bAppJsDebug = !!$this->oActions->Config()->Get('labs', 'use_app_debug_js', false);
294		$bAppCssDebug = !!$this->oActions->Config()->Get('labs', 'use_app_debug_css', false);
295
296		$sFaviconUrl = (string) $this->oActions->Config()->Get('webmail', 'favicon_url', '');
297
298		$sFaviconPngLink = $sFaviconUrl ? $sFaviconUrl : $this->staticPath('apple-touch-icon.png');
299		$sAppleTouchLink = $sFaviconUrl ? '' : $this->staticPath('apple-touch-icon.png');
300
301		$sContentSecurityPolicy = $this->oActions->Config()->Get('security', 'content_security_policy', '');
302		$sSentryDsn = $this->oActions->Config()->Get('logs', 'sentry_dsn', '');
303
304		$aTemplateParameters = array(
305			'{{BaseAppHeadScriptLink}}' => $sSentryDsn ?
306				'<script type="text/javascript" data-cfasync="false" src="https://browser.sentry-cdn.com/5.4.3/bundle.min.js" crossorigin="anonymous"></script>' : '',
307			'{{BaseAppBodyScript}}' => $sSentryDsn ?
308				'<script type="text/javascript" data-cfasync="false">window && window.Sentry && window.Sentry.init({dsn:\''.$sSentryDsn.'\',ignoreErrors:[\'Document not active\']});</script>' : '',
309			'{{BaseAppFaviconPngLinkTag}}' => $sFaviconPngLink ? '<link type="image/png" rel="shortcut icon" href="'.$sFaviconPngLink.'" />' : '',
310			'{{BaseAppFaviconTouchLinkTag}}' => $sAppleTouchLink ? '<link type="image/png" rel="apple-touch-icon" href="'.$sAppleTouchLink.'" />' : '',
311			'{{BaseAppMainCssLink}}' => $this->staticPath('css/app'.($bAppCssDebug ? '' : '.min').'.css'),
312			'{{BaseAppThemeCssLink}}' => $this->oActions->ThemeLink($sTheme, $bAdmin),
313			'{{BaseAppPolyfillsScriptLink}}' => $this->staticPath('js/'.($bAppJsDebug ? '' : 'min/').'polyfills'.($bAppJsDebug ? '' : '.min').'.js'),
314			'{{BaseAppBootScriptLink}}' => $this->staticPath('js/'.($bAppJsDebug ? '' : 'min/').'boot'.($bAppJsDebug ? '' : '.min').'.js'),
315			'{{BaseViewport}}' => $bMobile ? 'width=device-width,initial-scale=1,user-scalable=no' : 'width=950,maximum-scale=2',
316			'{{BaseContentSecurityPolicy}}' => $sContentSecurityPolicy ?
317				'<meta http-equiv="Content-Security-Policy" content="'.$sContentSecurityPolicy.'" />' : '',
318			'{{BaseDir}}' => false && \in_array($sLanguage, array('ar', 'he', 'ur')) ? 'rtl' : 'ltr',
319			'{{BaseAppManifestLink}}' => $this->staticPath('manifest.json')
320		);
321
322		$aTemplateParameters['{{RainloopBootData}}'] = \json_encode(array(
323			'admin' => $bAdmin,
324			'language' => $sLanguage,
325			'theme' => $sTheme,
326			'mobile' => $bMobile,
327			'mobileDevice' => $bMobileDevice
328		));
329
330		$aTemplateParameters['{{BaseHash}}'] = \md5(
331			\implode('~', array(
332				$bAdmin ? '1' : '0',
333				\md5($this->oActions->Config()->Get('cache', 'index', '')),
334				$this->oActions->Plugins()->Hash(),
335				\RainLoop\Utils::WebVersionPath(),
336				APP_VERSION,
337			)).
338			\implode('~', $aTemplateParameters)
339		);
340
341		return $aTemplateParameters;
342	}
343}
344