]> git.mxchange.org Git - friendica.git/blob - src/App.php
Improved protocol storing
[friendica.git] / src / App.php
1 <?php
2 /**
3  * @file src/App.php
4  */
5 namespace Friendica;
6
7 use Detection\MobileDetect;
8 use DOMDocument;
9 use DOMXPath;
10 use Exception;
11 use Friendica\Core\Config\Cache\IConfigCache;
12 use Friendica\Core\Config\Configuration;
13 use Friendica\Core\Hook;
14 use Friendica\Core\Theme;
15 use Friendica\Database\DBA;
16 use Friendica\Model\Profile;
17 use Friendica\Network\HTTPException\InternalServerErrorException;
18 use Friendica\Util\BaseURL;
19 use Friendica\Util\Config\ConfigFileLoader;
20 use Friendica\Util\HTTPSignature;
21 use Friendica\Util\Profiler;
22 use Friendica\Util\Strings;
23 use Psr\Log\LoggerInterface;
24
25 /**
26  *
27  * class: App
28  *
29  * @brief Our main application structure for the life of this page.
30  *
31  * Primarily deals with the URL that got us here
32  * and tries to make some sense of it, and
33  * stores our page contents and config storage
34  * and anything else that might need to be passed around
35  * before we spit the page out.
36  *
37  */
38 class App
39 {
40         public $module_class = null;
41         public $query_string = '';
42         public $page = [];
43         public $profile;
44         public $profile_uid;
45         public $user;
46         public $cid;
47         public $contact;
48         public $contacts;
49         public $page_contact;
50         public $content;
51         public $data = [];
52         public $error = false;
53         public $cmd = '';
54         public $argv;
55         public $argc;
56         public $module;
57         public $timezone;
58         public $interactive = true;
59         public $identities;
60         public $is_mobile = false;
61         public $is_tablet = false;
62         public $theme_info = [];
63         public $category;
64         // Allow themes to control internal parameters
65         // by changing App values in theme.php
66
67         public $sourcename = '';
68         public $videowidth = 425;
69         public $videoheight = 350;
70         public $force_max_items = 0;
71         public $theme_events_in_profile = true;
72
73         public $stylesheets = [];
74         public $footerScripts = [];
75
76         /**
77          * @var App\Mode The Mode of the Application
78          */
79         private $mode;
80
81         /**
82          * @var App\Router
83          */
84         private $router;
85
86         /**
87          * @var BaseURL
88          */
89         private $baseURL;
90
91         /**
92          * @var bool true, if the call is from the Friendica APP, otherwise false
93          */
94         private $isFriendicaApp;
95
96         /**
97          * @var bool true, if the call is from an backend node (f.e. worker)
98          */
99         private $isBackend;
100
101         /**
102          * @var string The name of the current theme
103          */
104         private $currentTheme;
105
106         /**
107          * @var bool check if request was an AJAX (xmlhttprequest) request
108          */
109         private $isAjax;
110
111         /**
112          * @var MobileDetect
113          */
114         public $mobileDetect;
115
116         /**
117          * @var Configuration The config
118          */
119         private $config;
120
121         /**
122          * @var LoggerInterface The logger
123          */
124         private $logger;
125
126         /**
127          * @var Profiler The profiler of this app
128          */
129         private $profiler;
130
131         /**
132          * Returns the current config cache of this node
133          *
134          * @return IConfigCache
135          */
136         public function getConfigCache()
137         {
138                 return $this->config->getCache();
139         }
140
141         /**
142          * Returns the current config of this node
143          *
144          * @return Configuration
145          */
146         public function getConfig()
147         {
148                 return $this->config;
149         }
150
151         /**
152          * The basepath of this app
153          *
154          * @return string
155          */
156         public function getBasePath()
157         {
158                 return $this->config->get('system', 'basepath');
159         }
160
161         /**
162          * The Logger of this app
163          *
164          * @return LoggerInterface
165          */
166         public function getLogger()
167         {
168                 return $this->logger;
169         }
170
171         /**
172          * The profiler of this app
173          *
174          * @return Profiler
175          */
176         public function getProfiler()
177         {
178                 return $this->profiler;
179         }
180
181         /**
182          * Returns the Mode of the Application
183          *
184          * @return App\Mode The Application Mode
185          */
186         public function getMode()
187         {
188                 return $this->mode;
189         }
190
191         /**
192          * Returns the router of the Application
193          *
194          * @return App\Router
195          */
196         public function getRouter()
197         {
198                 return $this->router;
199         }
200
201         /**
202          * Register a stylesheet file path to be included in the <head> tag of every page.
203          * Inclusion is done in App->initHead().
204          * The path can be absolute or relative to the Friendica installation base folder.
205          *
206          * @see initHead()
207          *
208          * @param string $path
209          */
210         public function registerStylesheet($path)
211         {
212                 if (mb_strpos($path, $this->getBasePath() . DIRECTORY_SEPARATOR) === 0) {
213                         $path = mb_substr($path, mb_strlen($this->getBasePath() . DIRECTORY_SEPARATOR));
214                 }
215
216                 $this->stylesheets[] = trim($path, '/');
217         }
218
219         /**
220          * Register a javascript file path to be included in the <footer> tag of every page.
221          * Inclusion is done in App->initFooter().
222          * The path can be absolute or relative to the Friendica installation base folder.
223          *
224          * @see initFooter()
225          *
226          * @param string $path
227          */
228         public function registerFooterScript($path)
229         {
230                 $url = str_replace($this->getBasePath() . DIRECTORY_SEPARATOR, '', $path);
231
232                 $this->footerScripts[] = trim($url, '/');
233         }
234
235         public $queue;
236
237         /**
238          * @brief App constructor.
239          *
240          * @param Configuration    $config    The Configuration
241          * @param App\Mode         $mode      The mode of this Friendica app
242          * @param App\Router       $router    The router of this Friendica app
243          * @param BaseURL          $baseURL   The full base URL of this Friendica app
244          * @param LoggerInterface  $logger    The current app logger
245          * @param Profiler         $profiler  The profiler of this application
246          * @param bool             $isBackend Whether it is used for backend or frontend (Default true=backend)
247          *
248          * @throws Exception if the Basepath is not usable
249          */
250         public function __construct(Configuration $config, App\Mode $mode, App\Router $router, BaseURL $baseURL, LoggerInterface $logger, Profiler $profiler, $isBackend = true)
251         {
252                 BaseObject::setApp($this);
253
254                 $this->config   = $config;
255                 $this->mode     = $mode;
256                 $this->router   = $router;
257                 $this->baseURL  = $baseURL;
258                 $this->profiler = $profiler;
259                 $this->logger   = $logger;
260
261                 $this->checkFriendicaApp();
262
263                 $this->profiler->reset();
264
265                 $this->reload();
266
267                 set_time_limit(0);
268
269                 // This has to be quite large to deal with embedded private photos
270                 ini_set('pcre.backtrack_limit', 500000);
271
272                 set_include_path(
273                         get_include_path() . PATH_SEPARATOR
274                         . $this->getBasePath() . DIRECTORY_SEPARATOR . 'include' . PATH_SEPARATOR
275                         . $this->getBasePath() . DIRECTORY_SEPARATOR . 'library' . PATH_SEPARATOR
276                         . $this->getBasePath());
277
278                 if (!empty($_SERVER['QUERY_STRING']) && strpos($_SERVER['QUERY_STRING'], 'pagename=') === 0) {
279                         $this->query_string = substr($_SERVER['QUERY_STRING'], 9);
280                 } elseif (!empty($_SERVER['QUERY_STRING']) && strpos($_SERVER['QUERY_STRING'], 'q=') === 0) {
281                         $this->query_string = substr($_SERVER['QUERY_STRING'], 2);
282                 }
283
284                 // removing trailing / - maybe a nginx problem
285                 $this->query_string = ltrim($this->query_string, '/');
286
287                 if (!empty($_GET['pagename'])) {
288                         $this->cmd = trim($_GET['pagename'], '/\\');
289                 } elseif (!empty($_GET['q'])) {
290                         $this->cmd = trim($_GET['q'], '/\\');
291                 }
292
293                 // fix query_string
294                 $this->query_string = str_replace($this->cmd . '&', $this->cmd . '?', $this->query_string);
295
296                 // unix style "homedir"
297                 if (substr($this->cmd, 0, 1) === '~') {
298                         $this->cmd = 'profile/' . substr($this->cmd, 1);
299                 }
300
301                 // Diaspora style profile url
302                 if (substr($this->cmd, 0, 2) === 'u/') {
303                         $this->cmd = 'profile/' . substr($this->cmd, 2);
304                 }
305
306                 /*
307                  * Break the URL path into C style argc/argv style arguments for our
308                  * modules. Given "http://example.com/module/arg1/arg2", $this->argc
309                  * will be 3 (integer) and $this->argv will contain:
310                  *   [0] => 'module'
311                  *   [1] => 'arg1'
312                  *   [2] => 'arg2'
313                  *
314                  *
315                  * There will always be one argument. If provided a naked domain
316                  * URL, $this->argv[0] is set to "home".
317                  */
318
319                 $this->argv = explode('/', $this->cmd);
320                 $this->argc = count($this->argv);
321                 if ((array_key_exists('0', $this->argv)) && strlen($this->argv[0])) {
322                         $this->module = str_replace('.', '_', $this->argv[0]);
323                         $this->module = str_replace('-', '_', $this->module);
324                 } else {
325                         $this->argc = 1;
326                         $this->argv = ['home'];
327                         $this->module = 'home';
328                 }
329
330                 $this->isBackend = $isBackend || $this->checkBackend($this->module);
331
332                 // Detect mobile devices
333                 $mobile_detect = new MobileDetect();
334
335                 $this->mobileDetect = $mobile_detect;
336
337                 $this->is_mobile = $mobile_detect->isMobile();
338                 $this->is_tablet = $mobile_detect->isTablet();
339
340                 $this->isAjax = strtolower(defaults($_SERVER, 'HTTP_X_REQUESTED_WITH', '')) == 'xmlhttprequest';
341
342                 // Register template engines
343                 Core\Renderer::registerTemplateEngine('Friendica\Render\FriendicaSmartyEngine');
344         }
345
346         /**
347          * Reloads the whole app instance
348          */
349         public function reload()
350         {
351                 $this->getMode()->determine($this->getBasePath());
352
353                 if ($this->getMode()->has(App\Mode::DBAVAILABLE)) {
354                         $loader = new ConfigFileLoader($this->getBasePath(), $this->getMode());
355                         $this->config->getCache()->load($loader->loadCoreConfig('addon'), true);
356
357                         $this->profiler->update(
358                                 $this->config->get('system', 'profiler', false),
359                                 $this->config->get('rendertime', 'callstack', false));
360
361                         Core\Hook::loadHooks();
362                         $loader = new ConfigFileLoader($this->getBasePath(), $this->mode);
363                         Core\Hook::callAll('load_config', $loader);
364                 }
365
366                 $this->loadDefaultTimezone();
367
368                 Core\L10n::init();
369         }
370
371         /**
372          * Loads the default timezone
373          *
374          * Include support for legacy $default_timezone
375          *
376          * @global string $default_timezone
377          */
378         private function loadDefaultTimezone()
379         {
380                 if ($this->config->get('system', 'default_timezone')) {
381                         $this->timezone = $this->config->get('system', 'default_timezone');
382                 } else {
383                         global $default_timezone;
384                         $this->timezone = !empty($default_timezone) ? $default_timezone : 'UTC';
385                 }
386
387                 if ($this->timezone) {
388                         date_default_timezone_set($this->timezone);
389                 }
390         }
391
392         /**
393          * Returns the scheme of the current call
394          * @return string
395          *
396          * @deprecated 2019.06 - use BaseURL->getScheme() instead
397          */
398         public function getScheme()
399         {
400                 return $this->baseURL->getScheme();
401         }
402
403         /**
404          * Retrieves the Friendica instance base URL
405          *
406          * @param bool $ssl Whether to append http or https under BaseURL::SSL_POLICY_SELFSIGN
407          *
408          * @return string Friendica server base URL
409          *
410          * @deprecated 2019.06 - use BaseURL->get($ssl) instead
411          */
412         public function getBaseURL($ssl = false)
413         {
414                 return $this->baseURL->get($ssl);
415         }
416
417         /**
418          * @brief Initializes the baseurl components
419          *
420          * Clears the baseurl cache to prevent inconsistencies
421          *
422          * @param string $url
423          *
424          * @deprecated 2019.06 - use BaseURL->saveByURL($url) instead
425          */
426         public function setBaseURL($url)
427         {
428                 $this->baseURL->saveByURL($url);
429         }
430
431         /**
432          * Returns the current hostname
433          *
434          * @return string
435          *
436          * @deprecated 2019.06 - use BaseURL->getHostname() instead
437          */
438         public function getHostName()
439         {
440                 return $this->baseURL->getHostname();
441         }
442
443         /**
444          * Returns the sub-path of the full URL
445          *
446          * @return string
447          *
448          * @deprecated 2019.06 - use BaseURL->getUrlPath() instead
449          */
450         public function getURLPath()
451         {
452                 return $this->baseURL->getUrlPath();
453         }
454
455         /**
456          * Initializes App->page['htmlhead'].
457          *
458          * Includes:
459          * - Page title
460          * - Favicons
461          * - Registered stylesheets (through App->registerStylesheet())
462          * - Infinite scroll data
463          * - head.tpl template
464          */
465         public function initHead()
466         {
467                 $interval = ((local_user()) ? Core\PConfig::get(local_user(), 'system', 'update_interval') : 40000);
468
469                 // If the update is 'deactivated' set it to the highest integer number (~24 days)
470                 if ($interval < 0) {
471                         $interval = 2147483647;
472                 }
473
474                 if ($interval < 10000) {
475                         $interval = 40000;
476                 }
477
478                 // Default title: current module called
479                 if (empty($this->page['title']) && $this->module) {
480                         $this->page['title'] = ucfirst($this->module);
481                 }
482
483                 // Prepend the sitename to the page title
484                 $this->page['title'] = $this->config->get('config', 'sitename', '') . (!empty($this->page['title']) ? ' | ' . $this->page['title'] : '');
485
486                 if (!empty(Core\Renderer::$theme['stylesheet'])) {
487                         $stylesheet = Core\Renderer::$theme['stylesheet'];
488                 } else {
489                         $stylesheet = $this->getCurrentThemeStylesheetPath();
490                 }
491
492                 $this->registerStylesheet($stylesheet);
493
494                 $shortcut_icon = $this->config->get('system', 'shortcut_icon');
495                 if ($shortcut_icon == '') {
496                         $shortcut_icon = 'images/friendica-32.png';
497                 }
498
499                 $touch_icon = $this->config->get('system', 'touch_icon');
500                 if ($touch_icon == '') {
501                         $touch_icon = 'images/friendica-128.png';
502                 }
503
504                 Core\Hook::callAll('head', $this->page['htmlhead']);
505
506                 $tpl = Core\Renderer::getMarkupTemplate('head.tpl');
507                 /* put the head template at the beginning of page['htmlhead']
508                  * since the code added by the modules frequently depends on it
509                  * being first
510                  */
511                 $this->page['htmlhead'] = Core\Renderer::replaceMacros($tpl, [
512                         '$baseurl'         => $this->getBaseURL(),
513                         '$local_user'      => local_user(),
514                         '$generator'       => 'Friendica' . ' ' . FRIENDICA_VERSION,
515                         '$delitem'         => Core\L10n::t('Delete this item?'),
516                         '$update_interval' => $interval,
517                         '$shortcut_icon'   => $shortcut_icon,
518                         '$touch_icon'      => $touch_icon,
519                         '$block_public'    => intval($this->config->get('system', 'block_public')),
520                         '$stylesheets'     => $this->stylesheets,
521                 ]) . $this->page['htmlhead'];
522         }
523
524         /**
525          * Initializes App->page['footer'].
526          *
527          * Includes:
528          * - Javascript homebase
529          * - Mobile toggle link
530          * - Registered footer scripts (through App->registerFooterScript())
531          * - footer.tpl template
532          */
533         public function initFooter()
534         {
535                 // If you're just visiting, let javascript take you home
536                 if (!empty($_SESSION['visitor_home'])) {
537                         $homebase = $_SESSION['visitor_home'];
538                 } elseif (local_user()) {
539                         $homebase = 'profile/' . $this->user['nickname'];
540                 }
541
542                 if (isset($homebase)) {
543                         $this->page['footer'] .= '<script>var homebase="' . $homebase . '";</script>' . "\n";
544                 }
545
546                 /*
547                  * Add a "toggle mobile" link if we're using a mobile device
548                  */
549                 if ($this->is_mobile || $this->is_tablet) {
550                         if (isset($_SESSION['show-mobile']) && !$_SESSION['show-mobile']) {
551                                 $link = 'toggle_mobile?address=' . urlencode(curPageURL());
552                         } else {
553                                 $link = 'toggle_mobile?off=1&address=' . urlencode(curPageURL());
554                         }
555                         $this->page['footer'] .= Core\Renderer::replaceMacros(Core\Renderer::getMarkupTemplate("toggle_mobile_footer.tpl"), [
556                                 '$toggle_link' => $link,
557                                 '$toggle_text' => Core\L10n::t('toggle mobile')
558                         ]);
559                 }
560
561                 Core\Hook::callAll('footer', $this->page['footer']);
562
563                 $tpl = Core\Renderer::getMarkupTemplate('footer.tpl');
564                 $this->page['footer'] = Core\Renderer::replaceMacros($tpl, [
565                         '$baseurl' => $this->getBaseURL(),
566                         '$footerScripts' => $this->footerScripts,
567                 ]) . $this->page['footer'];
568         }
569
570         /**
571          * @brief Removes the base url from an url. This avoids some mixed content problems.
572          *
573          * @param string $origURL
574          *
575          * @return string The cleaned url
576          * @throws InternalServerErrorException
577          */
578         public function removeBaseURL($origURL)
579         {
580                 // Remove the hostname from the url if it is an internal link
581                 $nurl = Util\Strings::normaliseLink($origURL);
582                 $base = Util\Strings::normaliseLink($this->getBaseURL());
583                 $url = str_replace($base . '/', '', $nurl);
584
585                 // if it is an external link return the orignal value
586                 if ($url == Util\Strings::normaliseLink($origURL)) {
587                         return $origURL;
588                 } else {
589                         return $url;
590                 }
591         }
592
593         /**
594          * Returns the current UserAgent as a String
595          *
596          * @return string the UserAgent as a String
597          * @throws InternalServerErrorException
598          */
599         public function getUserAgent()
600         {
601                 return
602                         FRIENDICA_PLATFORM . " '" .
603                         FRIENDICA_CODENAME . "' " .
604                         FRIENDICA_VERSION . '-' .
605                         DB_UPDATE_VERSION . '; ' .
606                         $this->getBaseURL();
607         }
608
609         /**
610          * Checks, if the call is from the Friendica App
611          *
612          * Reason:
613          * The friendica client has problems with the GUID in the notify. this is some workaround
614          */
615         private function checkFriendicaApp()
616         {
617                 // Friendica-Client
618                 $this->isFriendicaApp = isset($_SERVER['HTTP_USER_AGENT']) && $_SERVER['HTTP_USER_AGENT'] == 'Apache-HttpClient/UNAVAILABLE (java 1.4)';
619         }
620
621         /**
622          *      Is the call via the Friendica app? (not a "normale" call)
623          *
624          * @return bool true if it's from the Friendica app
625          */
626         public function isFriendicaApp()
627         {
628                 return $this->isFriendicaApp;
629         }
630
631         /**
632          * @brief Checks if the site is called via a backend process
633          *
634          * This isn't a perfect solution. But we need this check very early.
635          * So we cannot wait until the modules are loaded.
636          *
637          * @param string $module
638          * @return bool
639          */
640         private function checkBackend($module) {
641                 static $backends = [
642                         '_well_known',
643                         'api',
644                         'dfrn_notify',
645                         'feed',
646                         'fetch',
647                         'followers',
648                         'following',
649                         'hcard',
650                         'hostxrd',
651                         'inbox',
652                         'manifest',
653                         'nodeinfo',
654                         'noscrape',
655                         'objects',
656                         'outbox',
657                         'poco',
658                         'post',
659                         'proxy',
660                         'pubsub',
661                         'pubsubhubbub',
662                         'receive',
663                         'rsd_xml',
664                         'salmon',
665                         'statistics_json',
666                         'xrd',
667                 ];
668
669                 // Check if current module is in backend or backend flag is set
670                 return in_array($module, $backends);
671         }
672
673         /**
674          * Returns true, if the call is from a backend node (f.e. from a worker)
675          *
676          * @return bool Is it a known backend?
677          */
678         public function isBackend()
679         {
680                 return $this->isBackend;
681         }
682
683         /**
684          * @brief Checks if the maximum number of database processes is reached
685          *
686          * @return bool Is the limit reached?
687          */
688         public function isMaxProcessesReached()
689         {
690                 // Deactivated, needs more investigating if this check really makes sense
691                 return false;
692
693                 /*
694                  * Commented out to suppress static analyzer issues
695                  *
696                 if ($this->is_backend()) {
697                         $process = 'backend';
698                         $max_processes = $this->config->get('system', 'max_processes_backend');
699                         if (intval($max_processes) == 0) {
700                                 $max_processes = 5;
701                         }
702                 } else {
703                         $process = 'frontend';
704                         $max_processes = $this->config->get('system', 'max_processes_frontend');
705                         if (intval($max_processes) == 0) {
706                                 $max_processes = 20;
707                         }
708                 }
709
710                 $processlist = DBA::processlist();
711                 if ($processlist['list'] != '') {
712                         Core\Logger::log('Processcheck: Processes: ' . $processlist['amount'] . ' - Processlist: ' . $processlist['list'], Core\Logger::DEBUG);
713
714                         if ($processlist['amount'] > $max_processes) {
715                                 Core\Logger::log('Processcheck: Maximum number of processes for ' . $process . ' tasks (' . $max_processes . ') reached.', Core\Logger::DEBUG);
716                                 return true;
717                         }
718                 }
719                 return false;
720                  */
721         }
722
723         /**
724          * @brief Checks if the minimal memory is reached
725          *
726          * @return bool Is the memory limit reached?
727          * @throws InternalServerErrorException
728          */
729         public function isMinMemoryReached()
730         {
731                 $min_memory = $this->config->get('system', 'min_memory', 0);
732                 if ($min_memory == 0) {
733                         return false;
734                 }
735
736                 if (!is_readable('/proc/meminfo')) {
737                         return false;
738                 }
739
740                 $memdata = explode("\n", file_get_contents('/proc/meminfo'));
741
742                 $meminfo = [];
743                 foreach ($memdata as $line) {
744                         $data = explode(':', $line);
745                         if (count($data) != 2) {
746                                 continue;
747                         }
748                         list($key, $val) = $data;
749                         $meminfo[$key] = (int) trim(str_replace('kB', '', $val));
750                         $meminfo[$key] = (int) ($meminfo[$key] / 1024);
751                 }
752
753                 if (!isset($meminfo['MemFree'])) {
754                         return false;
755                 }
756
757                 $free = $meminfo['MemFree'];
758
759                 $reached = ($free < $min_memory);
760
761                 if ($reached) {
762                         Core\Logger::log('Minimal memory reached: ' . $free . '/' . $meminfo['MemTotal'] . ' - limit ' . $min_memory, Core\Logger::DEBUG);
763                 }
764
765                 return $reached;
766         }
767
768         /**
769          * @brief Checks if the maximum load is reached
770          *
771          * @return bool Is the load reached?
772          * @throws InternalServerErrorException
773          */
774         public function isMaxLoadReached()
775         {
776                 if ($this->isBackend()) {
777                         $process = 'backend';
778                         $maxsysload = intval($this->config->get('system', 'maxloadavg'));
779                         if ($maxsysload < 1) {
780                                 $maxsysload = 50;
781                         }
782                 } else {
783                         $process = 'frontend';
784                         $maxsysload = intval($this->config->get('system', 'maxloadavg_frontend'));
785                         if ($maxsysload < 1) {
786                                 $maxsysload = 50;
787                         }
788                 }
789
790                 $load = Core\System::currentLoad();
791                 if ($load) {
792                         if (intval($load) > $maxsysload) {
793                                 Core\Logger::log('system: load ' . $load . ' for ' . $process . ' tasks (' . $maxsysload . ') too high.');
794                                 return true;
795                         }
796                 }
797                 return false;
798         }
799
800         /**
801          * Executes a child process with 'proc_open'
802          *
803          * @param string $command The command to execute
804          * @param array  $args    Arguments to pass to the command ( [ 'key' => value, 'key2' => value2, ... ]
805          * @throws InternalServerErrorException
806          */
807         public function proc_run($command, $args)
808         {
809                 if (!function_exists('proc_open')) {
810                         return;
811                 }
812
813                 $cmdline = $this->config->get('config', 'php_path', 'php') . ' ' . escapeshellarg($command);
814
815                 foreach ($args as $key => $value) {
816                         if (!is_null($value) && is_bool($value) && !$value) {
817                                 continue;
818                         }
819
820                         $cmdline .= ' --' . $key;
821                         if (!is_null($value) && !is_bool($value)) {
822                                 $cmdline .= ' ' . $value;
823                         }
824                 }
825
826                 if ($this->isMinMemoryReached()) {
827                         return;
828                 }
829
830                 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
831                         $resource = proc_open('cmd /c start /b ' . $cmdline, [], $foo, $this->getBasePath());
832                 } else {
833                         $resource = proc_open($cmdline . ' &', [], $foo, $this->getBasePath());
834                 }
835                 if (!is_resource($resource)) {
836                         Core\Logger::log('We got no resource for command ' . $cmdline, Core\Logger::DEBUG);
837                         return;
838                 }
839                 proc_close($resource);
840         }
841
842         /**
843          * Generates the site's default sender email address
844          *
845          * @return string
846          * @throws InternalServerErrorException
847          */
848         public function getSenderEmailAddress()
849         {
850                 $sender_email = $this->config->get('config', 'sender_email');
851                 if (empty($sender_email)) {
852                         $hostname = $this->baseURL->getHostname();
853                         if (strpos($hostname, ':')) {
854                                 $hostname = substr($hostname, 0, strpos($hostname, ':'));
855                         }
856
857                         $sender_email = 'noreply@' . $hostname;
858                 }
859
860                 return $sender_email;
861         }
862
863         /**
864          * Returns the current theme name.
865          *
866          * @return string the name of the current theme
867          * @throws InternalServerErrorException
868          */
869         public function getCurrentTheme()
870         {
871                 if ($this->getMode()->isInstall()) {
872                         return '';
873                 }
874
875                 if (!$this->currentTheme) {
876                         $this->computeCurrentTheme();
877                 }
878
879                 return $this->currentTheme;
880         }
881
882         public function setCurrentTheme($theme)
883         {
884                 $this->currentTheme = $theme;
885         }
886
887         /**
888          * Computes the current theme name based on the node settings, the user settings and the device type
889          *
890          * @throws Exception
891          */
892         private function computeCurrentTheme()
893         {
894                 $system_theme = $this->config->get('system', 'theme');
895                 if (!$system_theme) {
896                         throw new Exception(Core\L10n::t('No system theme config value set.'));
897                 }
898
899                 // Sane default
900                 $this->currentTheme = $system_theme;
901
902                 $page_theme = null;
903                 // Find the theme that belongs to the user whose stuff we are looking at
904                 if ($this->profile_uid && ($this->profile_uid != local_user())) {
905                         // Allow folks to override user themes and always use their own on their own site.
906                         // This works only if the user is on the same server
907                         $user = DBA::selectFirst('user', ['theme'], ['uid' => $this->profile_uid]);
908                         if (DBA::isResult($user) && !Core\PConfig::get(local_user(), 'system', 'always_my_theme')) {
909                                 $page_theme = $user['theme'];
910                         }
911                 }
912
913                 $user_theme = Core\Session::get('theme', $system_theme);
914
915                 // Specific mobile theme override
916                 if (($this->is_mobile || $this->is_tablet) && Core\Session::get('show-mobile', true)) {
917                         $system_mobile_theme = $this->config->get('system', 'mobile-theme');
918                         $user_mobile_theme = Core\Session::get('mobile-theme', $system_mobile_theme);
919
920                         // --- means same mobile theme as desktop
921                         if (!empty($user_mobile_theme) && $user_mobile_theme !== '---') {
922                                 $user_theme = $user_mobile_theme;
923                         }
924                 }
925
926                 if ($page_theme) {
927                         $theme_name = $page_theme;
928                 } else {
929                         $theme_name = $user_theme;
930                 }
931
932                 $theme_name = Strings::sanitizeFilePathItem($theme_name);
933                 if ($theme_name
934                         && in_array($theme_name, Theme::getAllowedList())
935                         && (file_exists('view/theme/' . $theme_name . '/style.css')
936                         || file_exists('view/theme/' . $theme_name . '/style.php'))
937                 ) {
938                         $this->currentTheme = $theme_name;
939                 }
940         }
941
942         /**
943          * @brief Return full URL to theme which is currently in effect.
944          *
945          * Provide a sane default if nothing is chosen or the specified theme does not exist.
946          *
947          * @return string
948          * @throws InternalServerErrorException
949          */
950         public function getCurrentThemeStylesheetPath()
951         {
952                 return Core\Theme::getStylesheetPath($this->getCurrentTheme());
953         }
954
955         /**
956          * Check if request was an AJAX (xmlhttprequest) request.
957          *
958          * @return boolean true if it was an AJAX request
959          */
960         public function isAjax()
961         {
962                 return $this->isAjax;
963         }
964
965         /**
966          * Returns the value of a argv key
967          * TODO there are a lot of $a->argv usages in combination with defaults() which can be replaced with this method
968          *
969          * @param int $position the position of the argument
970          * @param mixed $default the default value if not found
971          *
972          * @return mixed returns the value of the argument
973          */
974         public function getArgumentValue($position, $default = '')
975         {
976                 if (array_key_exists($position, $this->argv)) {
977                         return $this->argv[$position];
978                 }
979
980                 return $default;
981         }
982
983         /**
984          * Sets the base url for use in cmdline programs which don't have
985          * $_SERVER variables
986          */
987         public function checkURL()
988         {
989                 $url = $this->config->get('system', 'url');
990
991                 // if the url isn't set or the stored url is radically different
992                 // than the currently visited url, store the current value accordingly.
993                 // "Radically different" ignores common variations such as http vs https
994                 // and www.example.com vs example.com.
995                 // We will only change the url to an ip address if there is no existing setting
996
997                 if (empty($url) || (!Util\Strings::compareLink($url, $this->getBaseURL())) && (!preg_match("/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/", $this->baseURL->getHostname()))) {
998                         $this->config->set('system', 'url', $this->getBaseURL());
999                 }
1000         }
1001
1002         /**
1003          * Frontend App script
1004          *
1005          * The App object behaves like a container and a dispatcher at the same time, including a representation of the
1006          * request and a representation of the response.
1007          *
1008          * This probably should change to limit the size of this monster method.
1009          */
1010         public function runFrontend()
1011         {
1012                 // Missing DB connection: ERROR
1013                 if ($this->getMode()->has(App\Mode::LOCALCONFIGPRESENT) && !$this->getMode()->has(App\Mode::DBAVAILABLE)) {
1014                         Core\System::httpExit(500, ['title' => 'Error 500 - Internal Server Error', 'description' => 'Apologies but the website is unavailable at the moment.']);
1015                 }
1016
1017                 // Max Load Average reached: ERROR
1018                 if ($this->isMaxProcessesReached() || $this->isMaxLoadReached()) {
1019                         header('Retry-After: 120');
1020                         header('Refresh: 120; url=' . $this->getBaseURL() . "/" . $this->query_string);
1021
1022                         Core\System::httpExit(503, ['title' => 'Error 503 - Service Temporarily Unavailable', 'description' => 'Core\System is currently overloaded. Please try again later.']);
1023                 }
1024
1025                 if (strstr($this->query_string, '.well-known/host-meta') && ($this->query_string != '.well-known/host-meta')) {
1026                         Core\System::httpExit(404);
1027                 }
1028
1029                 if (!$this->getMode()->isInstall()) {
1030                         // Force SSL redirection
1031                         if ($this->baseURL->checkRedirectHttps()) {
1032                                 header('HTTP/1.1 302 Moved Temporarily');
1033                                 header('Location: ' . $this->getBaseURL() . '/' . $this->query_string);
1034                                 exit();
1035                         }
1036
1037                         Core\Session::init();
1038                         Core\Hook::callAll('init_1');
1039                 }
1040
1041                 // Exclude the backend processes from the session management
1042                 if (!$this->isBackend()) {
1043                         $stamp1 = microtime(true);
1044                         session_start();
1045                         $this->profiler->saveTimestamp($stamp1, 'parser', Core\System::callstack());
1046                         Core\L10n::setSessionVariable();
1047                         Core\L10n::setLangFromSession();
1048                 } else {
1049                         $_SESSION = [];
1050                         Core\Worker::executeIfIdle();
1051                 }
1052
1053                 if ($this->getMode()->isNormal()) {
1054                         $requester = HTTPSignature::getSigner('', $_SERVER);
1055                         if (!empty($requester)) {
1056                                 Profile::addVisitorCookieForHandle($requester);
1057                         }
1058                 }
1059
1060                 // ZRL
1061                 if (!empty($_GET['zrl']) && $this->getMode()->isNormal()) {
1062                         $this->query_string = Model\Profile::stripZrls($this->query_string);
1063                         if (!local_user()) {
1064                                 // Only continue when the given profile link seems valid
1065                                 // Valid profile links contain a path with "/profile/" and no query parameters
1066                                 if ((parse_url($_GET['zrl'], PHP_URL_QUERY) == "") &&
1067                                         strstr(parse_url($_GET['zrl'], PHP_URL_PATH), "/profile/")) {
1068                                         if (defaults($_SESSION, "visitor_home", "") != $_GET["zrl"]) {
1069                                                 $_SESSION['my_url'] = $_GET['zrl'];
1070                                                 $_SESSION['authenticated'] = 0;
1071                                         }
1072                                         Model\Profile::zrlInit($this);
1073                                 } else {
1074                                         // Someone came with an invalid parameter, maybe as a DDoS attempt
1075                                         // We simply stop processing here
1076                                         Core\Logger::log("Invalid ZRL parameter " . $_GET['zrl'], Core\Logger::DEBUG);
1077                                         Core\System::httpExit(403, ['title' => '403 Forbidden']);
1078                                 }
1079                         }
1080                 }
1081
1082                 if (!empty($_GET['owt']) && $this->getMode()->isNormal()) {
1083                         $token = $_GET['owt'];
1084                         $this->query_string = Model\Profile::stripQueryParam($this->query_string, 'owt');
1085                         Model\Profile::openWebAuthInit($token);
1086                 }
1087
1088                 Module\Login::sessionAuth();
1089
1090                 if (empty($_SESSION['authenticated'])) {
1091                         header('X-Account-Management-Status: none');
1092                 }
1093
1094                 $_SESSION['sysmsg']       = defaults($_SESSION, 'sysmsg'      , []);
1095                 $_SESSION['sysmsg_info']  = defaults($_SESSION, 'sysmsg_info' , []);
1096                 $_SESSION['last_updated'] = defaults($_SESSION, 'last_updated', []);
1097
1098                 /*
1099                  * check_config() is responsible for running update scripts. These automatically
1100                  * update the DB schema whenever we push a new one out. It also checks to see if
1101                  * any addons have been added or removed and reacts accordingly.
1102                  */
1103
1104                 // in install mode, any url loads install module
1105                 // but we need "view" module for stylesheet
1106                 if ($this->getMode()->isInstall() && $this->module != 'view') {
1107                         $this->module = 'install';
1108                 } elseif (!$this->getMode()->has(App\Mode::MAINTENANCEDISABLED) && $this->module != 'view') {
1109                         $this->module = 'maintenance';
1110                 } else {
1111                         $this->checkURL();
1112                         Core\Update::check($this->getBasePath(), false, $this->getMode());
1113                         Core\Addon::loadAddons();
1114                         Core\Hook::loadHooks();
1115                 }
1116
1117                 $this->page = [
1118                         'aside' => '',
1119                         'bottom' => '',
1120                         'content' => '',
1121                         'footer' => '',
1122                         'htmlhead' => '',
1123                         'nav' => '',
1124                         'page_title' => '',
1125                         'right_aside' => '',
1126                         'template' => '',
1127                         'title' => ''
1128                 ];
1129
1130                 if (strlen($this->module)) {
1131                         // Compatibility with the Android Diaspora client
1132                         if ($this->module == 'stream') {
1133                                 $this->internalRedirect('network?f=&order=post');
1134                         }
1135
1136                         if ($this->module == 'conversations') {
1137                                 $this->internalRedirect('message');
1138                         }
1139
1140                         if ($this->module == 'commented') {
1141                                 $this->internalRedirect('network?f=&order=comment');
1142                         }
1143
1144                         if ($this->module == 'liked') {
1145                                 $this->internalRedirect('network?f=&order=comment');
1146                         }
1147
1148                         if ($this->module == 'activity') {
1149                                 $this->internalRedirect('network/?f=&conv=1');
1150                         }
1151
1152                         if (($this->module == 'status_messages') && ($this->cmd == 'status_messages/new')) {
1153                                 $this->internalRedirect('bookmarklet');
1154                         }
1155
1156                         if (($this->module == 'user') && ($this->cmd == 'user/edit')) {
1157                                 $this->internalRedirect('settings');
1158                         }
1159
1160                         if (($this->module == 'tag_followings') && ($this->cmd == 'tag_followings/manage')) {
1161                                 $this->internalRedirect('search');
1162                         }
1163
1164                         // Compatibility with the Firefox App
1165                         if (($this->module == "users") && ($this->cmd == "users/sign_in")) {
1166                                 $this->module = "login";
1167                         }
1168
1169                         /*
1170                          * ROUTING
1171                          *
1172                          * From the request URL, routing consists of obtaining the name of a BaseModule-extending class of which the
1173                          * post() and/or content() static methods can be respectively called to produce a data change or an output.
1174                          */
1175
1176                         // First we try explicit routes defined in App\Router
1177                         $this->router->collectRoutes();
1178
1179                         $data = $this->router->getRouteCollector();
1180                         Hook::callAll('route_collection', $data);
1181
1182                         $this->module_class = $this->router->getModuleClass($this->cmd);
1183
1184                         // Then we try addon-provided modules that we wrap in the LegacyModule class
1185                         if (!$this->module_class && Core\Addon::isEnabled($this->module) && file_exists("addon/{$this->module}/{$this->module}.php")) {
1186                                 //Check if module is an app and if public access to apps is allowed or not
1187                                 $privateapps = $this->config->get('config', 'private_addons', false);
1188                                 if ((!local_user()) && Core\Hook::isAddonApp($this->module) && $privateapps) {
1189                                         info(Core\L10n::t("You must be logged in to use addons. "));
1190                                 } else {
1191                                         include_once "addon/{$this->module}/{$this->module}.php";
1192                                         if (function_exists($this->module . '_module')) {
1193                                                 LegacyModule::setModuleFile("addon/{$this->module}/{$this->module}.php");
1194                                                 $this->module_class = 'Friendica\\LegacyModule';
1195                                         }
1196                                 }
1197                         }
1198
1199                         // Then we try name-matching a Friendica\Module class
1200                         if (!$this->module_class && class_exists('Friendica\\Module\\' . ucfirst($this->module))) {
1201                                 $this->module_class = 'Friendica\\Module\\' . ucfirst($this->module);
1202                         }
1203
1204                         /* Finally, we look for a 'standard' program module in the 'mod' directory
1205                          * We emulate a Module class through the LegacyModule class
1206                          */
1207                         if (!$this->module_class && file_exists("mod/{$this->module}.php")) {
1208                                 LegacyModule::setModuleFile("mod/{$this->module}.php");
1209                                 $this->module_class = 'Friendica\\LegacyModule';
1210                         }
1211
1212                         /* The URL provided does not resolve to a valid module.
1213                          *
1214                          * On Dreamhost sites, quite often things go wrong for no apparent reason and they send us to '/internal_error.html'.
1215                          * We don't like doing this, but as it occasionally accounts for 10-20% or more of all site traffic -
1216                          * we are going to trap this and redirect back to the requested page. As long as you don't have a critical error on your page
1217                          * this will often succeed and eventually do the right thing.
1218                          *
1219                          * Otherwise we are going to emit a 404 not found.
1220                          */
1221                         if (!$this->module_class) {
1222                                 // Stupid browser tried to pre-fetch our Javascript img template. Don't log the event or return anything - just quietly exit.
1223                                 if (!empty($_SERVER['QUERY_STRING']) && preg_match('/{[0-9]}/', $_SERVER['QUERY_STRING']) !== 0) {
1224                                         exit();
1225                                 }
1226
1227                                 if (!empty($_SERVER['QUERY_STRING']) && ($_SERVER['QUERY_STRING'] === 'q=internal_error.html') && isset($dreamhost_error_hack)) {
1228                                         Core\Logger::log('index.php: dreamhost_error_hack invoked. Original URI =' . $_SERVER['REQUEST_URI']);
1229                                         $this->internalRedirect($_SERVER['REQUEST_URI']);
1230                                 }
1231
1232                                 Core\Logger::log('index.php: page not found: ' . $_SERVER['REQUEST_URI'] . ' ADDRESS: ' . $_SERVER['REMOTE_ADDR'] . ' QUERY: ' . $_SERVER['QUERY_STRING'], Core\Logger::DEBUG);
1233
1234                                 header($_SERVER["SERVER_PROTOCOL"] . ' 404 ' . Core\L10n::t('Not Found'));
1235                                 $tpl = Core\Renderer::getMarkupTemplate("404.tpl");
1236                                 $this->page['content'] = Core\Renderer::replaceMacros($tpl, [
1237                                         '$message' =>  Core\L10n::t('Page not found.')
1238                                 ]);
1239                         }
1240                 }
1241
1242                 $content = '';
1243
1244                 // Initialize module that can set the current theme in the init() method, either directly or via App->profile_uid
1245                 if ($this->module_class) {
1246                         $this->page['page_title'] = $this->module;
1247                         $placeholder = '';
1248
1249                         Core\Hook::callAll($this->module . '_mod_init', $placeholder);
1250
1251                         call_user_func([$this->module_class, 'init']);
1252
1253                         // "rawContent" is especially meant for technical endpoints.
1254                         // This endpoint doesn't need any theme initialization or other comparable stuff.
1255                         if (!$this->error) {
1256                                 call_user_func([$this->module_class, 'rawContent']);
1257                         }
1258                 }
1259
1260                 // Load current theme info after module has been initialized as theme could have been set in module
1261                 $theme_info_file = 'view/theme/' . $this->getCurrentTheme() . '/theme.php';
1262                 if (file_exists($theme_info_file)) {
1263                         require_once $theme_info_file;
1264                 }
1265
1266                 if (function_exists(str_replace('-', '_', $this->getCurrentTheme()) . '_init')) {
1267                         $func = str_replace('-', '_', $this->getCurrentTheme()) . '_init';
1268                         $func($this);
1269                 }
1270
1271                 if ($this->module_class) {
1272                         if (! $this->error && $_SERVER['REQUEST_METHOD'] === 'POST') {
1273                                 Core\Hook::callAll($this->module . '_mod_post', $_POST);
1274                                 call_user_func([$this->module_class, 'post']);
1275                         }
1276
1277                         if (! $this->error) {
1278                                 Core\Hook::callAll($this->module . '_mod_afterpost', $placeholder);
1279                                 call_user_func([$this->module_class, 'afterpost']);
1280                         }
1281
1282                         if (! $this->error) {
1283                                 $arr = ['content' => $content];
1284                                 Core\Hook::callAll($this->module . '_mod_content', $arr);
1285                                 $content = $arr['content'];
1286                                 $arr = ['content' => call_user_func([$this->module_class, 'content'])];
1287                                 Core\Hook::callAll($this->module . '_mod_aftercontent', $arr);
1288                                 $content .= $arr['content'];
1289                         }
1290                 }
1291
1292                 // initialise content region
1293                 if ($this->getMode()->isNormal()) {
1294                         Core\Hook::callAll('page_content_top', $this->page['content']);
1295                 }
1296
1297                 $this->page['content'] .= $content;
1298
1299                 /* Create the page head after setting the language
1300                  * and getting any auth credentials.
1301                  *
1302                  * Moved initHead() and initFooter() to after
1303                  * all the module functions have executed so that all
1304                  * theme choices made by the modules can take effect.
1305                  */
1306                 $this->initHead();
1307
1308                 /* Build the page ending -- this is stuff that goes right before
1309                  * the closing </body> tag
1310                  */
1311                 $this->initFooter();
1312
1313                 /* now that we've been through the module content, see if the page reported
1314                  * a permission problem and if so, a 403 response would seem to be in order.
1315                  */
1316                 if (stristr(implode("", $_SESSION['sysmsg']), Core\L10n::t('Permission denied'))) {
1317                         header($_SERVER["SERVER_PROTOCOL"] . ' 403 ' . Core\L10n::t('Permission denied.'));
1318                 }
1319
1320                 if (!$this->isAjax()) {
1321                         Core\Hook::callAll('page_end', $this->page['content']);
1322                 }
1323
1324                 // Add the navigation (menu) template
1325                 if ($this->module != 'install' && $this->module != 'maintenance') {
1326                         $this->page['htmlhead'] .= Core\Renderer::replaceMacros(Core\Renderer::getMarkupTemplate('nav_head.tpl'), []);
1327                         $this->page['nav']       = Content\Nav::build($this);
1328                 }
1329
1330                 // Build the page - now that we have all the components
1331                 if (isset($_GET["mode"]) && (($_GET["mode"] == "raw") || ($_GET["mode"] == "minimal"))) {
1332                         $doc = new DOMDocument();
1333
1334                         $target = new DOMDocument();
1335                         $target->loadXML("<root></root>");
1336
1337                         $content = mb_convert_encoding($this->page["content"], 'HTML-ENTITIES', "UTF-8");
1338
1339                         /// @TODO one day, kill those error-surpressing @ stuff, or PHP should ban it
1340                         @$doc->loadHTML($content);
1341
1342                         $xpath = new DOMXPath($doc);
1343
1344                         $list = $xpath->query("//*[contains(@id,'tread-wrapper-')]");  /* */
1345
1346                         foreach ($list as $item) {
1347                                 $item = $target->importNode($item, true);
1348
1349                                 // And then append it to the target
1350                                 $target->documentElement->appendChild($item);
1351                         }
1352
1353                         if ($_GET["mode"] == "raw") {
1354                                 header("Content-type: text/html; charset=utf-8");
1355
1356                                 echo substr($target->saveHTML(), 6, -8);
1357
1358                                 exit();
1359                         }
1360                 }
1361
1362                 $page    = $this->page;
1363                 $profile = $this->profile;
1364
1365                 header("X-Friendica-Version: " . FRIENDICA_VERSION);
1366                 header("Content-type: text/html; charset=utf-8");
1367
1368                 if ($this->config->get('system', 'hsts') && ($this->baseURL->getSSLPolicy() == BaseUrl::SSL_POLICY_FULL)) {
1369                         header("Strict-Transport-Security: max-age=31536000");
1370                 }
1371
1372                 // Some security stuff
1373                 header('X-Content-Type-Options: nosniff');
1374                 header('X-XSS-Protection: 1; mode=block');
1375                 header('X-Permitted-Cross-Domain-Policies: none');
1376                 header('X-Frame-Options: sameorigin');
1377
1378                 // Things like embedded OSM maps don't work, when this is enabled
1379                 // header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self'; img-src 'self' https: data:; media-src 'self' https:; child-src 'self' https:; object-src 'none'");
1380
1381                 /* We use $_GET["mode"] for special page templates. So we will check if we have
1382                  * to load another page template than the default one.
1383                  * The page templates are located in /view/php/ or in the theme directory.
1384                  */
1385                 if (isset($_GET["mode"])) {
1386                         $template = Core\Theme::getPathForFile($_GET["mode"] . '.php');
1387                 }
1388
1389                 // If there is no page template use the default page template
1390                 if (empty($template)) {
1391                         $template = Core\Theme::getPathForFile("default.php");
1392                 }
1393
1394                 // Theme templates expect $a as an App instance
1395                 $a = $this;
1396
1397                 // Used as is in view/php/default.php
1398                 $lang = Core\L10n::getCurrentLang();
1399
1400                 /// @TODO Looks unsafe (remote-inclusion), is maybe not but Core\Theme::getPathForFile() uses file_exists() but does not escape anything
1401                 require_once $template;
1402         }
1403
1404         /**
1405          * Redirects to another module relative to the current Friendica base.
1406          * If you want to redirect to a external URL, use System::externalRedirectTo()
1407          *
1408          * @param string $toUrl The destination URL (Default is empty, which is the default page of the Friendica node)
1409          * @param bool $ssl if true, base URL will try to get called with https:// (works just for relative paths)
1410          *
1411          * @throws InternalServerErrorException In Case the given URL is not relative to the Friendica node
1412          */
1413         public function internalRedirect($toUrl = '', $ssl = false)
1414         {
1415                 if (!empty(parse_url($toUrl, PHP_URL_SCHEME))) {
1416                         throw new InternalServerErrorException("'$toUrl is not a relative path, please use System::externalRedirectTo");
1417                 }
1418
1419                 $redirectTo = $this->getBaseURL($ssl) . '/' . ltrim($toUrl, '/');
1420                 Core\System::externalRedirect($redirectTo);
1421         }
1422
1423         /**
1424          * Automatically redirects to relative or absolute URL
1425          * Should only be used if it isn't clear if the URL is either internal or external
1426          *
1427          * @param string $toUrl The target URL
1428          * @throws InternalServerErrorException
1429          */
1430         public function redirect($toUrl)
1431         {
1432                 if (!empty(parse_url($toUrl, PHP_URL_SCHEME))) {
1433                         Core\System::externalRedirect($toUrl);
1434                 } else {
1435                         $this->internalRedirect($toUrl);
1436                 }
1437         }
1438 }