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