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