]> git.mxchange.org Git - friendica.git/blob - src/App.php
Remove duplicate $baseurl template variable
[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         public function getBaseURL($ssl = false)
411         {
412                 return $this->baseURL->get($ssl);
413         }
414
415         /**
416          * @brief Initializes the baseurl components
417          *
418          * Clears the baseurl cache to prevent inconsistencies
419          *
420          * @param string $url
421          *
422          * @deprecated 2019.06 - use BaseURL->saveByURL($url) instead
423          */
424         public function setBaseURL($url)
425         {
426                 $this->baseURL->saveByURL($url);
427         }
428
429         /**
430          * Returns the current hostname
431          *
432          * @return string
433          *
434          * @deprecated 2019.06 - use BaseURL->getHostname() instead
435          */
436         public function getHostName()
437         {
438                 return $this->baseURL->getHostname();
439         }
440
441         /**
442          * Returns the sub-path of the full URL
443          *
444          * @return string
445          *
446          * @deprecated 2019.06 - use BaseURL->getUrlPath() instead
447          */
448         public function getURLPath()
449         {
450                 return $this->baseURL->getUrlPath();
451         }
452
453         /**
454          * Initializes App->page['htmlhead'].
455          *
456          * Includes:
457          * - Page title
458          * - Favicons
459          * - Registered stylesheets (through App->registerStylesheet())
460          * - Infinite scroll data
461          * - head.tpl template
462          */
463         public function initHead()
464         {
465                 $interval = ((local_user()) ? Core\PConfig::get(local_user(), 'system', 'update_interval') : 40000);
466
467                 // If the update is 'deactivated' set it to the highest integer number (~24 days)
468                 if ($interval < 0) {
469                         $interval = 2147483647;
470                 }
471
472                 if ($interval < 10000) {
473                         $interval = 40000;
474                 }
475
476                 // Default title: current module called
477                 if (empty($this->page['title']) && $this->module) {
478                         $this->page['title'] = ucfirst($this->module);
479                 }
480
481                 // Prepend the sitename to the page title
482                 $this->page['title'] = $this->config->get('config', 'sitename', '') . (!empty($this->page['title']) ? ' | ' . $this->page['title'] : '');
483
484                 if (!empty(Core\Renderer::$theme['stylesheet'])) {
485                         $stylesheet = Core\Renderer::$theme['stylesheet'];
486                 } else {
487                         $stylesheet = $this->getCurrentThemeStylesheetPath();
488                 }
489
490                 $this->registerStylesheet($stylesheet);
491
492                 $shortcut_icon = $this->config->get('system', 'shortcut_icon');
493                 if ($shortcut_icon == '') {
494                         $shortcut_icon = 'images/friendica-32.png';
495                 }
496
497                 $touch_icon = $this->config->get('system', 'touch_icon');
498                 if ($touch_icon == '') {
499                         $touch_icon = 'images/friendica-128.png';
500                 }
501
502                 Core\Hook::callAll('head', $this->page['htmlhead']);
503
504                 $tpl = Core\Renderer::getMarkupTemplate('head.tpl');
505                 /* put the head template at the beginning of page['htmlhead']
506                  * since the code added by the modules frequently depends on it
507                  * being first
508                  */
509                 $this->page['htmlhead'] = Core\Renderer::replaceMacros($tpl, [
510                         '$local_user'      => local_user(),
511                         '$generator'       => 'Friendica' . ' ' . FRIENDICA_VERSION,
512                         '$delitem'         => Core\L10n::t('Delete this item?'),
513                         '$update_interval' => $interval,
514                         '$shortcut_icon'   => $shortcut_icon,
515                         '$touch_icon'      => $touch_icon,
516                         '$block_public'    => intval($this->config->get('system', 'block_public')),
517                         '$stylesheets'     => $this->stylesheets,
518                 ]) . $this->page['htmlhead'];
519         }
520
521         /**
522          * Initializes App->page['footer'].
523          *
524          * Includes:
525          * - Javascript homebase
526          * - Mobile toggle link
527          * - Registered footer scripts (through App->registerFooterScript())
528          * - footer.tpl template
529          */
530         public function initFooter()
531         {
532                 // If you're just visiting, let javascript take you home
533                 if (!empty($_SESSION['visitor_home'])) {
534                         $homebase = $_SESSION['visitor_home'];
535                 } elseif (local_user()) {
536                         $homebase = 'profile/' . $this->user['nickname'];
537                 }
538
539                 if (isset($homebase)) {
540                         $this->page['footer'] .= '<script>var homebase="' . $homebase . '";</script>' . "\n";
541                 }
542
543                 /*
544                  * Add a "toggle mobile" link if we're using a mobile device
545                  */
546                 if ($this->is_mobile || $this->is_tablet) {
547                         if (isset($_SESSION['show-mobile']) && !$_SESSION['show-mobile']) {
548                                 $link = 'toggle_mobile?address=' . urlencode(curPageURL());
549                         } else {
550                                 $link = 'toggle_mobile?off=1&address=' . urlencode(curPageURL());
551                         }
552                         $this->page['footer'] .= Core\Renderer::replaceMacros(Core\Renderer::getMarkupTemplate("toggle_mobile_footer.tpl"), [
553                                 '$toggle_link' => $link,
554                                 '$toggle_text' => Core\L10n::t('toggle mobile')
555                         ]);
556                 }
557
558                 Core\Hook::callAll('footer', $this->page['footer']);
559
560                 $tpl = Core\Renderer::getMarkupTemplate('footer.tpl');
561                 $this->page['footer'] = Core\Renderer::replaceMacros($tpl, [
562                         '$footerScripts' => $this->footerScripts,
563                 ]) . $this->page['footer'];
564         }
565
566         /**
567          * @brief Removes the base url from an url. This avoids some mixed content problems.
568          *
569          * @param string $origURL
570          *
571          * @return string The cleaned url
572          * @throws InternalServerErrorException
573          */
574         public function removeBaseURL($origURL)
575         {
576                 // Remove the hostname from the url if it is an internal link
577                 $nurl = Util\Strings::normaliseLink($origURL);
578                 $base = Util\Strings::normaliseLink($this->getBaseURL());
579                 $url = str_replace($base . '/', '', $nurl);
580
581                 // if it is an external link return the orignal value
582                 if ($url == Util\Strings::normaliseLink($origURL)) {
583                         return $origURL;
584                 } else {
585                         return $url;
586                 }
587         }
588
589         /**
590          * Returns the current UserAgent as a String
591          *
592          * @return string the UserAgent as a String
593          * @throws InternalServerErrorException
594          */
595         public function getUserAgent()
596         {
597                 return
598                         FRIENDICA_PLATFORM . " '" .
599                         FRIENDICA_CODENAME . "' " .
600                         FRIENDICA_VERSION . '-' .
601                         DB_UPDATE_VERSION . '; ' .
602                         $this->getBaseURL();
603         }
604
605         /**
606          * Checks, if the call is from the Friendica App
607          *
608          * Reason:
609          * The friendica client has problems with the GUID in the notify. this is some workaround
610          */
611         private function checkFriendicaApp()
612         {
613                 // Friendica-Client
614                 $this->isFriendicaApp = isset($_SERVER['HTTP_USER_AGENT']) && $_SERVER['HTTP_USER_AGENT'] == 'Apache-HttpClient/UNAVAILABLE (java 1.4)';
615         }
616
617         /**
618          *      Is the call via the Friendica app? (not a "normale" call)
619          *
620          * @return bool true if it's from the Friendica app
621          */
622         public function isFriendicaApp()
623         {
624                 return $this->isFriendicaApp;
625         }
626
627         /**
628          * @brief Checks if the site is called via a backend process
629          *
630          * This isn't a perfect solution. But we need this check very early.
631          * So we cannot wait until the modules are loaded.
632          *
633          * @param string $module
634          * @return bool
635          */
636         private function checkBackend($module) {
637                 static $backends = [
638                         '_well_known',
639                         'api',
640                         'dfrn_notify',
641                         'feed',
642                         'fetch',
643                         'followers',
644                         'following',
645                         'hcard',
646                         'hostxrd',
647                         'inbox',
648                         'manifest',
649                         'nodeinfo',
650                         'noscrape',
651                         'objects',
652                         'outbox',
653                         'poco',
654                         'post',
655                         'proxy',
656                         'pubsub',
657                         'pubsubhubbub',
658                         'receive',
659                         'rsd_xml',
660                         'salmon',
661                         'statistics_json',
662                         'xrd',
663                 ];
664
665                 // Check if current module is in backend or backend flag is set
666                 return in_array($module, $backends);
667         }
668
669         /**
670          * Returns true, if the call is from a backend node (f.e. from a worker)
671          *
672          * @return bool Is it a known backend?
673          */
674         public function isBackend()
675         {
676                 return $this->isBackend;
677         }
678
679         /**
680          * @brief Checks if the maximum number of database processes is reached
681          *
682          * @return bool Is the limit reached?
683          */
684         public function isMaxProcessesReached()
685         {
686                 // Deactivated, needs more investigating if this check really makes sense
687                 return false;
688
689                 /*
690                  * Commented out to suppress static analyzer issues
691                  *
692                 if ($this->is_backend()) {
693                         $process = 'backend';
694                         $max_processes = $this->config->get('system', 'max_processes_backend');
695                         if (intval($max_processes) == 0) {
696                                 $max_processes = 5;
697                         }
698                 } else {
699                         $process = 'frontend';
700                         $max_processes = $this->config->get('system', 'max_processes_frontend');
701                         if (intval($max_processes) == 0) {
702                                 $max_processes = 20;
703                         }
704                 }
705
706                 $processlist = DBA::processlist();
707                 if ($processlist['list'] != '') {
708                         Core\Logger::log('Processcheck: Processes: ' . $processlist['amount'] . ' - Processlist: ' . $processlist['list'], Core\Logger::DEBUG);
709
710                         if ($processlist['amount'] > $max_processes) {
711                                 Core\Logger::log('Processcheck: Maximum number of processes for ' . $process . ' tasks (' . $max_processes . ') reached.', Core\Logger::DEBUG);
712                                 return true;
713                         }
714                 }
715                 return false;
716                  */
717         }
718
719         /**
720          * @brief Checks if the minimal memory is reached
721          *
722          * @return bool Is the memory limit reached?
723          * @throws InternalServerErrorException
724          */
725         public function isMinMemoryReached()
726         {
727                 $min_memory = $this->config->get('system', 'min_memory', 0);
728                 if ($min_memory == 0) {
729                         return false;
730                 }
731
732                 if (!is_readable('/proc/meminfo')) {
733                         return false;
734                 }
735
736                 $memdata = explode("\n", file_get_contents('/proc/meminfo'));
737
738                 $meminfo = [];
739                 foreach ($memdata as $line) {
740                         $data = explode(':', $line);
741                         if (count($data) != 2) {
742                                 continue;
743                         }
744                         list($key, $val) = $data;
745                         $meminfo[$key] = (int) trim(str_replace('kB', '', $val));
746                         $meminfo[$key] = (int) ($meminfo[$key] / 1024);
747                 }
748
749                 if (!isset($meminfo['MemFree'])) {
750                         return false;
751                 }
752
753                 $free = $meminfo['MemFree'];
754
755                 $reached = ($free < $min_memory);
756
757                 if ($reached) {
758                         Core\Logger::log('Minimal memory reached: ' . $free . '/' . $meminfo['MemTotal'] . ' - limit ' . $min_memory, Core\Logger::DEBUG);
759                 }
760
761                 return $reached;
762         }
763
764         /**
765          * @brief Checks if the maximum load is reached
766          *
767          * @return bool Is the load reached?
768          * @throws InternalServerErrorException
769          */
770         public function isMaxLoadReached()
771         {
772                 if ($this->isBackend()) {
773                         $process = 'backend';
774                         $maxsysload = intval($this->config->get('system', 'maxloadavg'));
775                         if ($maxsysload < 1) {
776                                 $maxsysload = 50;
777                         }
778                 } else {
779                         $process = 'frontend';
780                         $maxsysload = intval($this->config->get('system', 'maxloadavg_frontend'));
781                         if ($maxsysload < 1) {
782                                 $maxsysload = 50;
783                         }
784                 }
785
786                 $load = Core\System::currentLoad();
787                 if ($load) {
788                         if (intval($load) > $maxsysload) {
789                                 Core\Logger::log('system: load ' . $load . ' for ' . $process . ' tasks (' . $maxsysload . ') too high.');
790                                 return true;
791                         }
792                 }
793                 return false;
794         }
795
796         /**
797          * Executes a child process with 'proc_open'
798          *
799          * @param string $command The command to execute
800          * @param array  $args    Arguments to pass to the command ( [ 'key' => value, 'key2' => value2, ... ]
801          * @throws InternalServerErrorException
802          */
803         public function proc_run($command, $args)
804         {
805                 if (!function_exists('proc_open')) {
806                         return;
807                 }
808
809                 $cmdline = $this->config->get('config', 'php_path', 'php') . ' ' . escapeshellarg($command);
810
811                 foreach ($args as $key => $value) {
812                         if (!is_null($value) && is_bool($value) && !$value) {
813                                 continue;
814                         }
815
816                         $cmdline .= ' --' . $key;
817                         if (!is_null($value) && !is_bool($value)) {
818                                 $cmdline .= ' ' . $value;
819                         }
820                 }
821
822                 if ($this->isMinMemoryReached()) {
823                         return;
824                 }
825
826                 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
827                         $resource = proc_open('cmd /c start /b ' . $cmdline, [], $foo, $this->getBasePath());
828                 } else {
829                         $resource = proc_open($cmdline . ' &', [], $foo, $this->getBasePath());
830                 }
831                 if (!is_resource($resource)) {
832                         Core\Logger::log('We got no resource for command ' . $cmdline, Core\Logger::DEBUG);
833                         return;
834                 }
835                 proc_close($resource);
836         }
837
838         /**
839          * Generates the site's default sender email address
840          *
841          * @return string
842          * @throws InternalServerErrorException
843          */
844         public function getSenderEmailAddress()
845         {
846                 $sender_email = $this->config->get('config', 'sender_email');
847                 if (empty($sender_email)) {
848                         $hostname = $this->baseURL->getHostname();
849                         if (strpos($hostname, ':')) {
850                                 $hostname = substr($hostname, 0, strpos($hostname, ':'));
851                         }
852
853                         $sender_email = 'noreply@' . $hostname;
854                 }
855
856                 return $sender_email;
857         }
858
859         /**
860          * Returns the current theme name.
861          *
862          * @return string the name of the current theme
863          * @throws InternalServerErrorException
864          */
865         public function getCurrentTheme()
866         {
867                 if ($this->getMode()->isInstall()) {
868                         return '';
869                 }
870
871                 if (!$this->currentTheme) {
872                         $this->computeCurrentTheme();
873                 }
874
875                 return $this->currentTheme;
876         }
877
878         public function setCurrentTheme($theme)
879         {
880                 $this->currentTheme = $theme;
881         }
882
883         /**
884          * Computes the current theme name based on the node settings, the user settings and the device type
885          *
886          * @throws Exception
887          */
888         private function computeCurrentTheme()
889         {
890                 $system_theme = $this->config->get('system', 'theme');
891                 if (!$system_theme) {
892                         throw new Exception(Core\L10n::t('No system theme config value set.'));
893                 }
894
895                 // Sane default
896                 $this->currentTheme = $system_theme;
897
898                 $page_theme = null;
899                 // Find the theme that belongs to the user whose stuff we are looking at
900                 if ($this->profile_uid && ($this->profile_uid != local_user())) {
901                         // Allow folks to override user themes and always use their own on their own site.
902                         // This works only if the user is on the same server
903                         $user = DBA::selectFirst('user', ['theme'], ['uid' => $this->profile_uid]);
904                         if (DBA::isResult($user) && !Core\PConfig::get(local_user(), 'system', 'always_my_theme')) {
905                                 $page_theme = $user['theme'];
906                         }
907                 }
908
909                 $user_theme = Core\Session::get('theme', $system_theme);
910
911                 // Specific mobile theme override
912                 if (($this->is_mobile || $this->is_tablet) && Core\Session::get('show-mobile', true)) {
913                         $system_mobile_theme = $this->config->get('system', 'mobile-theme');
914                         $user_mobile_theme = Core\Session::get('mobile-theme', $system_mobile_theme);
915
916                         // --- means same mobile theme as desktop
917                         if (!empty($user_mobile_theme) && $user_mobile_theme !== '---') {
918                                 $user_theme = $user_mobile_theme;
919                         }
920                 }
921
922                 if ($page_theme) {
923                         $theme_name = $page_theme;
924                 } else {
925                         $theme_name = $user_theme;
926                 }
927
928                 $theme_name = Strings::sanitizeFilePathItem($theme_name);
929                 if ($theme_name
930                         && in_array($theme_name, Theme::getAllowedList())
931                         && (file_exists('view/theme/' . $theme_name . '/style.css')
932                         || file_exists('view/theme/' . $theme_name . '/style.php'))
933                 ) {
934                         $this->currentTheme = $theme_name;
935                 }
936         }
937
938         /**
939          * @brief Return full URL to theme which is currently in effect.
940          *
941          * Provide a sane default if nothing is chosen or the specified theme does not exist.
942          *
943          * @return string
944          * @throws InternalServerErrorException
945          */
946         public function getCurrentThemeStylesheetPath()
947         {
948                 return Core\Theme::getStylesheetPath($this->getCurrentTheme());
949         }
950
951         /**
952          * Check if request was an AJAX (xmlhttprequest) request.
953          *
954          * @return boolean true if it was an AJAX request
955          */
956         public function isAjax()
957         {
958                 return $this->isAjax;
959         }
960
961         /**
962          * Returns the value of a argv key
963          * TODO there are a lot of $a->argv usages in combination with defaults() which can be replaced with this method
964          *
965          * @param int $position the position of the argument
966          * @param mixed $default the default value if not found
967          *
968          * @return mixed returns the value of the argument
969          */
970         public function getArgumentValue($position, $default = '')
971         {
972                 if (array_key_exists($position, $this->argv)) {
973                         return $this->argv[$position];
974                 }
975
976                 return $default;
977         }
978
979         /**
980          * Sets the base url for use in cmdline programs which don't have
981          * $_SERVER variables
982          */
983         public function checkURL()
984         {
985                 $url = $this->config->get('system', 'url');
986
987                 // if the url isn't set or the stored url is radically different
988                 // than the currently visited url, store the current value accordingly.
989                 // "Radically different" ignores common variations such as http vs https
990                 // and www.example.com vs example.com.
991                 // We will only change the url to an ip address if there is no existing setting
992
993                 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()))) {
994                         $this->config->set('system', 'url', $this->getBaseURL());
995                 }
996         }
997
998         /**
999          * Frontend App script
1000          *
1001          * The App object behaves like a container and a dispatcher at the same time, including a representation of the
1002          * request and a representation of the response.
1003          *
1004          * This probably should change to limit the size of this monster method.
1005          */
1006         public function runFrontend()
1007         {
1008                 // Missing DB connection: ERROR
1009                 if ($this->getMode()->has(App\Mode::LOCALCONFIGPRESENT) && !$this->getMode()->has(App\Mode::DBAVAILABLE)) {
1010                         Core\System::httpExit(500, ['title' => 'Error 500 - Internal Server Error', 'description' => 'Apologies but the website is unavailable at the moment.']);
1011                 }
1012
1013                 // Max Load Average reached: ERROR
1014                 if ($this->isMaxProcessesReached() || $this->isMaxLoadReached()) {
1015                         header('Retry-After: 120');
1016                         header('Refresh: 120; url=' . $this->getBaseURL() . "/" . $this->query_string);
1017
1018                         Core\System::httpExit(503, ['title' => 'Error 503 - Service Temporarily Unavailable', 'description' => 'Core\System is currently overloaded. Please try again later.']);
1019                 }
1020
1021                 if (strstr($this->query_string, '.well-known/host-meta') && ($this->query_string != '.well-known/host-meta')) {
1022                         Core\System::httpExit(404);
1023                 }
1024
1025                 if (!$this->getMode()->isInstall()) {
1026                         // Force SSL redirection
1027                         if ($this->baseURL->checkRedirectHttps()) {
1028                                 header('HTTP/1.1 302 Moved Temporarily');
1029                                 header('Location: ' . $this->getBaseURL() . '/' . $this->query_string);
1030                                 exit();
1031                         }
1032
1033                         Core\Session::init();
1034                         Core\Hook::callAll('init_1');
1035                 }
1036
1037                 // Exclude the backend processes from the session management
1038                 if (!$this->isBackend()) {
1039                         $stamp1 = microtime(true);
1040                         session_start();
1041                         $this->profiler->saveTimestamp($stamp1, 'parser', Core\System::callstack());
1042                         Core\L10n::setSessionVariable();
1043                         Core\L10n::setLangFromSession();
1044                 } else {
1045                         $_SESSION = [];
1046                         Core\Worker::executeIfIdle();
1047                 }
1048
1049                 if ($this->getMode()->isNormal()) {
1050                         $requester = HTTPSignature::getSigner('', $_SERVER);
1051                         if (!empty($requester)) {
1052                                 Profile::addVisitorCookieForHandle($requester);
1053                         }
1054                 }
1055
1056                 // ZRL
1057                 if (!empty($_GET['zrl']) && $this->getMode()->isNormal()) {
1058                         $this->query_string = Model\Profile::stripZrls($this->query_string);
1059                         if (!local_user()) {
1060                                 // Only continue when the given profile link seems valid
1061                                 // Valid profile links contain a path with "/profile/" and no query parameters
1062                                 if ((parse_url($_GET['zrl'], PHP_URL_QUERY) == "") &&
1063                                         strstr(parse_url($_GET['zrl'], PHP_URL_PATH), "/profile/")) {
1064                                         if (defaults($_SESSION, "visitor_home", "") != $_GET["zrl"]) {
1065                                                 $_SESSION['my_url'] = $_GET['zrl'];
1066                                                 $_SESSION['authenticated'] = 0;
1067                                         }
1068                                         Model\Profile::zrlInit($this);
1069                                 } else {
1070                                         // Someone came with an invalid parameter, maybe as a DDoS attempt
1071                                         // We simply stop processing here
1072                                         Core\Logger::log("Invalid ZRL parameter " . $_GET['zrl'], Core\Logger::DEBUG);
1073                                         Core\System::httpExit(403, ['title' => '403 Forbidden']);
1074                                 }
1075                         }
1076                 }
1077
1078                 if (!empty($_GET['owt']) && $this->getMode()->isNormal()) {
1079                         $token = $_GET['owt'];
1080                         $this->query_string = Model\Profile::stripQueryParam($this->query_string, 'owt');
1081                         Model\Profile::openWebAuthInit($token);
1082                 }
1083
1084                 Module\Login::sessionAuth();
1085
1086                 if (empty($_SESSION['authenticated'])) {
1087                         header('X-Account-Management-Status: none');
1088                 }
1089
1090                 $_SESSION['sysmsg']       = defaults($_SESSION, 'sysmsg'      , []);
1091                 $_SESSION['sysmsg_info']  = defaults($_SESSION, 'sysmsg_info' , []);
1092                 $_SESSION['last_updated'] = defaults($_SESSION, 'last_updated', []);
1093
1094                 /*
1095                  * check_config() is responsible for running update scripts. These automatically
1096                  * update the DB schema whenever we push a new one out. It also checks to see if
1097                  * any addons have been added or removed and reacts accordingly.
1098                  */
1099
1100                 // in install mode, any url loads install module
1101                 // but we need "view" module for stylesheet
1102                 if ($this->getMode()->isInstall() && $this->module != 'view') {
1103                         $this->module = 'install';
1104                 } elseif (!$this->getMode()->has(App\Mode::MAINTENANCEDISABLED) && $this->module != 'view') {
1105                         $this->module = 'maintenance';
1106                 } else {
1107                         $this->checkURL();
1108                         Core\Update::check($this->getBasePath(), false, $this->getMode());
1109                         Core\Addon::loadAddons();
1110                         Core\Hook::loadHooks();
1111                 }
1112
1113                 $this->page = [
1114                         'aside' => '',
1115                         'bottom' => '',
1116                         'content' => '',
1117                         'footer' => '',
1118                         'htmlhead' => '',
1119                         'nav' => '',
1120                         'page_title' => '',
1121                         'right_aside' => '',
1122                         'template' => '',
1123                         'title' => ''
1124                 ];
1125
1126                 if (strlen($this->module)) {
1127                         // Compatibility with the Android Diaspora client
1128                         if ($this->module == 'stream') {
1129                                 $this->internalRedirect('network?f=&order=post');
1130                         }
1131
1132                         if ($this->module == 'conversations') {
1133                                 $this->internalRedirect('message');
1134                         }
1135
1136                         if ($this->module == 'commented') {
1137                                 $this->internalRedirect('network?f=&order=comment');
1138                         }
1139
1140                         if ($this->module == 'liked') {
1141                                 $this->internalRedirect('network?f=&order=comment');
1142                         }
1143
1144                         if ($this->module == 'activity') {
1145                                 $this->internalRedirect('network/?f=&conv=1');
1146                         }
1147
1148                         if (($this->module == 'status_messages') && ($this->cmd == 'status_messages/new')) {
1149                                 $this->internalRedirect('bookmarklet');
1150                         }
1151
1152                         if (($this->module == 'user') && ($this->cmd == 'user/edit')) {
1153                                 $this->internalRedirect('settings');
1154                         }
1155
1156                         if (($this->module == 'tag_followings') && ($this->cmd == 'tag_followings/manage')) {
1157                                 $this->internalRedirect('search');
1158                         }
1159
1160                         // Compatibility with the Firefox App
1161                         if (($this->module == "users") && ($this->cmd == "users/sign_in")) {
1162                                 $this->module = "login";
1163                         }
1164
1165                         /*
1166                          * ROUTING
1167                          *
1168                          * From the request URL, routing consists of obtaining the name of a BaseModule-extending class of which the
1169                          * post() and/or content() static methods can be respectively called to produce a data change or an output.
1170                          */
1171
1172                         // First we try explicit routes defined in App\Router
1173                         $this->router->collectRoutes();
1174
1175                         $data = $this->router->getRouteCollector();
1176                         Hook::callAll('route_collection', $data);
1177
1178                         $this->module_class = $this->router->getModuleClass($this->cmd);
1179
1180                         // Then we try addon-provided modules that we wrap in the LegacyModule class
1181                         if (!$this->module_class && Core\Addon::isEnabled($this->module) && file_exists("addon/{$this->module}/{$this->module}.php")) {
1182                                 //Check if module is an app and if public access to apps is allowed or not
1183                                 $privateapps = $this->config->get('config', 'private_addons', false);
1184                                 if ((!local_user()) && Core\Hook::isAddonApp($this->module) && $privateapps) {
1185                                         info(Core\L10n::t("You must be logged in to use addons. "));
1186                                 } else {
1187                                         include_once "addon/{$this->module}/{$this->module}.php";
1188                                         if (function_exists($this->module . '_module')) {
1189                                                 LegacyModule::setModuleFile("addon/{$this->module}/{$this->module}.php");
1190                                                 $this->module_class = 'Friendica\\LegacyModule';
1191                                         }
1192                                 }
1193                         }
1194
1195                         // Then we try name-matching a Friendica\Module class
1196                         if (!$this->module_class && class_exists('Friendica\\Module\\' . ucfirst($this->module))) {
1197                                 $this->module_class = 'Friendica\\Module\\' . ucfirst($this->module);
1198                         }
1199
1200                         /* Finally, we look for a 'standard' program module in the 'mod' directory
1201                          * We emulate a Module class through the LegacyModule class
1202                          */
1203                         if (!$this->module_class && file_exists("mod/{$this->module}.php")) {
1204                                 LegacyModule::setModuleFile("mod/{$this->module}.php");
1205                                 $this->module_class = 'Friendica\\LegacyModule';
1206                         }
1207
1208                         /* The URL provided does not resolve to a valid module.
1209                          *
1210                          * On Dreamhost sites, quite often things go wrong for no apparent reason and they send us to '/internal_error.html'.
1211                          * We don't like doing this, but as it occasionally accounts for 10-20% or more of all site traffic -
1212                          * 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
1213                          * this will often succeed and eventually do the right thing.
1214                          *
1215                          * Otherwise we are going to emit a 404 not found.
1216                          */
1217                         if (!$this->module_class) {
1218                                 // Stupid browser tried to pre-fetch our Javascript img template. Don't log the event or return anything - just quietly exit.
1219                                 if (!empty($_SERVER['QUERY_STRING']) && preg_match('/{[0-9]}/', $_SERVER['QUERY_STRING']) !== 0) {
1220                                         exit();
1221                                 }
1222
1223                                 if (!empty($_SERVER['QUERY_STRING']) && ($_SERVER['QUERY_STRING'] === 'q=internal_error.html') && isset($dreamhost_error_hack)) {
1224                                         Core\Logger::log('index.php: dreamhost_error_hack invoked. Original URI =' . $_SERVER['REQUEST_URI']);
1225                                         $this->internalRedirect($_SERVER['REQUEST_URI']);
1226                                 }
1227
1228                                 Core\Logger::log('index.php: page not found: ' . $_SERVER['REQUEST_URI'] . ' ADDRESS: ' . $_SERVER['REMOTE_ADDR'] . ' QUERY: ' . $_SERVER['QUERY_STRING'], Core\Logger::DEBUG);
1229
1230                                 header($_SERVER["SERVER_PROTOCOL"] . ' 404 ' . Core\L10n::t('Not Found'));
1231                                 $tpl = Core\Renderer::getMarkupTemplate("404.tpl");
1232                                 $this->page['content'] = Core\Renderer::replaceMacros($tpl, [
1233                                         '$message' =>  Core\L10n::t('Page not found.')
1234                                 ]);
1235                         }
1236                 }
1237
1238                 $content = '';
1239
1240                 // Initialize module that can set the current theme in the init() method, either directly or via App->profile_uid
1241                 if ($this->module_class) {
1242                         $this->page['page_title'] = $this->module;
1243                         $placeholder = '';
1244
1245                         Core\Hook::callAll($this->module . '_mod_init', $placeholder);
1246
1247                         call_user_func([$this->module_class, 'init']);
1248
1249                         // "rawContent" is especially meant for technical endpoints.
1250                         // This endpoint doesn't need any theme initialization or other comparable stuff.
1251                         if (!$this->error) {
1252                                 call_user_func([$this->module_class, 'rawContent']);
1253                         }
1254                 }
1255
1256                 // Load current theme info after module has been initialized as theme could have been set in module
1257                 $theme_info_file = 'view/theme/' . $this->getCurrentTheme() . '/theme.php';
1258                 if (file_exists($theme_info_file)) {
1259                         require_once $theme_info_file;
1260                 }
1261
1262                 if (function_exists(str_replace('-', '_', $this->getCurrentTheme()) . '_init')) {
1263                         $func = str_replace('-', '_', $this->getCurrentTheme()) . '_init';
1264                         $func($this);
1265                 }
1266
1267                 if ($this->module_class) {
1268                         if (! $this->error && $_SERVER['REQUEST_METHOD'] === 'POST') {
1269                                 Core\Hook::callAll($this->module . '_mod_post', $_POST);
1270                                 call_user_func([$this->module_class, 'post']);
1271                         }
1272
1273                         if (! $this->error) {
1274                                 Core\Hook::callAll($this->module . '_mod_afterpost', $placeholder);
1275                                 call_user_func([$this->module_class, 'afterpost']);
1276                         }
1277
1278                         if (! $this->error) {
1279                                 $arr = ['content' => $content];
1280                                 Core\Hook::callAll($this->module . '_mod_content', $arr);
1281                                 $content = $arr['content'];
1282                                 $arr = ['content' => call_user_func([$this->module_class, 'content'])];
1283                                 Core\Hook::callAll($this->module . '_mod_aftercontent', $arr);
1284                                 $content .= $arr['content'];
1285                         }
1286                 }
1287
1288                 // initialise content region
1289                 if ($this->getMode()->isNormal()) {
1290                         Core\Hook::callAll('page_content_top', $this->page['content']);
1291                 }
1292
1293                 $this->page['content'] .= $content;
1294
1295                 /* Create the page head after setting the language
1296                  * and getting any auth credentials.
1297                  *
1298                  * Moved initHead() and initFooter() to after
1299                  * all the module functions have executed so that all
1300                  * theme choices made by the modules can take effect.
1301                  */
1302                 $this->initHead();
1303
1304                 /* Build the page ending -- this is stuff that goes right before
1305                  * the closing </body> tag
1306                  */
1307                 $this->initFooter();
1308
1309                 /* now that we've been through the module content, see if the page reported
1310                  * a permission problem and if so, a 403 response would seem to be in order.
1311                  */
1312                 if (stristr(implode("", $_SESSION['sysmsg']), Core\L10n::t('Permission denied'))) {
1313                         header($_SERVER["SERVER_PROTOCOL"] . ' 403 ' . Core\L10n::t('Permission denied.'));
1314                 }
1315
1316                 if (!$this->isAjax()) {
1317                         Core\Hook::callAll('page_end', $this->page['content']);
1318                 }
1319
1320                 // Add the navigation (menu) template
1321                 if ($this->module != 'install' && $this->module != 'maintenance') {
1322                         $this->page['htmlhead'] .= Core\Renderer::replaceMacros(Core\Renderer::getMarkupTemplate('nav_head.tpl'), []);
1323                         $this->page['nav']       = Content\Nav::build($this);
1324                 }
1325
1326                 // Build the page - now that we have all the components
1327                 if (isset($_GET["mode"]) && (($_GET["mode"] == "raw") || ($_GET["mode"] == "minimal"))) {
1328                         $doc = new DOMDocument();
1329
1330                         $target = new DOMDocument();
1331                         $target->loadXML("<root></root>");
1332
1333                         $content = mb_convert_encoding($this->page["content"], 'HTML-ENTITIES', "UTF-8");
1334
1335                         /// @TODO one day, kill those error-surpressing @ stuff, or PHP should ban it
1336                         @$doc->loadHTML($content);
1337
1338                         $xpath = new DOMXPath($doc);
1339
1340                         $list = $xpath->query("//*[contains(@id,'tread-wrapper-')]");  /* */
1341
1342                         foreach ($list as $item) {
1343                                 $item = $target->importNode($item, true);
1344
1345                                 // And then append it to the target
1346                                 $target->documentElement->appendChild($item);
1347                         }
1348
1349                         if ($_GET["mode"] == "raw") {
1350                                 header("Content-type: text/html; charset=utf-8");
1351
1352                                 echo substr($target->saveHTML(), 6, -8);
1353
1354                                 exit();
1355                         }
1356                 }
1357
1358                 $page    = $this->page;
1359                 $profile = $this->profile;
1360
1361                 header("X-Friendica-Version: " . FRIENDICA_VERSION);
1362                 header("Content-type: text/html; charset=utf-8");
1363
1364                 if ($this->config->get('system', 'hsts') && ($this->baseURL->getSSLPolicy() == BaseUrl::SSL_POLICY_FULL)) {
1365                         header("Strict-Transport-Security: max-age=31536000");
1366                 }
1367
1368                 // Some security stuff
1369                 header('X-Content-Type-Options: nosniff');
1370                 header('X-XSS-Protection: 1; mode=block');
1371                 header('X-Permitted-Cross-Domain-Policies: none');
1372                 header('X-Frame-Options: sameorigin');
1373
1374                 // Things like embedded OSM maps don't work, when this is enabled
1375                 // 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'");
1376
1377                 /* We use $_GET["mode"] for special page templates. So we will check if we have
1378                  * to load another page template than the default one.
1379                  * The page templates are located in /view/php/ or in the theme directory.
1380                  */
1381                 if (isset($_GET["mode"])) {
1382                         $template = Core\Theme::getPathForFile($_GET["mode"] . '.php');
1383                 }
1384
1385                 // If there is no page template use the default page template
1386                 if (empty($template)) {
1387                         $template = Core\Theme::getPathForFile("default.php");
1388                 }
1389
1390                 // Theme templates expect $a as an App instance
1391                 $a = $this;
1392
1393                 // Used as is in view/php/default.php
1394                 $lang = Core\L10n::getCurrentLang();
1395
1396                 /// @TODO Looks unsafe (remote-inclusion), is maybe not but Core\Theme::getPathForFile() uses file_exists() but does not escape anything
1397                 require_once $template;
1398         }
1399
1400         /**
1401          * Redirects to another module relative to the current Friendica base.
1402          * If you want to redirect to a external URL, use System::externalRedirectTo()
1403          *
1404          * @param string $toUrl The destination URL (Default is empty, which is the default page of the Friendica node)
1405          * @param bool $ssl if true, base URL will try to get called with https:// (works just for relative paths)
1406          *
1407          * @throws InternalServerErrorException In Case the given URL is not relative to the Friendica node
1408          */
1409         public function internalRedirect($toUrl = '', $ssl = false)
1410         {
1411                 if (!empty(parse_url($toUrl, PHP_URL_SCHEME))) {
1412                         throw new InternalServerErrorException("'$toUrl is not a relative path, please use System::externalRedirectTo");
1413                 }
1414
1415                 $redirectTo = $this->getBaseURL($ssl) . '/' . ltrim($toUrl, '/');
1416                 Core\System::externalRedirect($redirectTo);
1417         }
1418
1419         /**
1420          * Automatically redirects to relative or absolute URL
1421          * Should only be used if it isn't clear if the URL is either internal or external
1422          *
1423          * @param string $toUrl The target URL
1424          * @throws InternalServerErrorException
1425          */
1426         public function redirect($toUrl)
1427         {
1428                 if (!empty(parse_url($toUrl, PHP_URL_SCHEME))) {
1429                         Core\System::externalRedirect($toUrl);
1430                 } else {
1431                         $this->internalRedirect($toUrl);
1432                 }
1433         }
1434 }