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