]> git.mxchange.org Git - friendica.git/blob - src/App.php
50e4364fda379cfeb6e21c21932f3a24e4bb9150
[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                         echo Module\Special\HTTPException::rawContent(
981                                 new HTTPException\InternalServerErrorException('Apologies but the website is unavailable at the moment.')
982                         );
983                         exit;
984                 }
985
986                 // Max Load Average reached: ERROR
987                 if ($this->isMaxProcessesReached() || $this->isMaxLoadReached()) {
988                         header('Retry-After: 120');
989                         header('Refresh: 120; url=' . $this->getBaseURL() . "/" . $this->query_string);
990
991                         echo Module\Special\HTTPException::rawContent(
992                                 new HTTPException\ServiceUnavaiableException('The node is currently overloaded. Please try again later.')
993                         );
994                         exit;
995                 }
996
997                 if (strstr($this->query_string, '.well-known/host-meta') && ($this->query_string != '.well-known/host-meta')) {
998                         echo Module\Special\HTTPException::rawContent(
999                                 new HTTPException\NotFoundException()
1000                         );
1001                         exit;
1002                 }
1003
1004                 if (!$this->getMode()->isInstall()) {
1005                         // Force SSL redirection
1006                         if ($this->baseURL->checkRedirectHttps()) {
1007                                 header('HTTP/1.1 302 Moved Temporarily');
1008                                 header('Location: ' . $this->getBaseURL() . '/' . $this->query_string);
1009                                 exit();
1010                         }
1011
1012                         Core\Session::init();
1013                         Core\Hook::callAll('init_1');
1014                 }
1015
1016                 // Exclude the backend processes from the session management
1017                 if (!$this->isBackend()) {
1018                         $stamp1 = microtime(true);
1019                         session_start();
1020                         $this->profiler->saveTimestamp($stamp1, 'parser', Core\System::callstack());
1021                         Core\L10n::setSessionVariable();
1022                         Core\L10n::setLangFromSession();
1023                 } else {
1024                         $_SESSION = [];
1025                         Core\Worker::executeIfIdle();
1026                 }
1027
1028                 if ($this->getMode()->isNormal()) {
1029                         $requester = HTTPSignature::getSigner('', $_SERVER);
1030                         if (!empty($requester)) {
1031                                 Profile::addVisitorCookieForHandle($requester);
1032                         }
1033                 }
1034
1035                 // ZRL
1036                 if (!empty($_GET['zrl']) && $this->getMode()->isNormal()) {
1037                         $this->query_string = Model\Profile::stripZrls($this->query_string);
1038                         if (!local_user()) {
1039                                 // Only continue when the given profile link seems valid
1040                                 // Valid profile links contain a path with "/profile/" and no query parameters
1041                                 if ((parse_url($_GET['zrl'], PHP_URL_QUERY) == "") &&
1042                                         strstr(parse_url($_GET['zrl'], PHP_URL_PATH), "/profile/")) {
1043                                         if (defaults($_SESSION, "visitor_home", "") != $_GET["zrl"]) {
1044                                                 $_SESSION['my_url'] = $_GET['zrl'];
1045                                                 $_SESSION['authenticated'] = 0;
1046                                         }
1047                                         Model\Profile::zrlInit($this);
1048                                 } else {
1049                                         // Someone came with an invalid parameter, maybe as a DDoS attempt
1050                                         // We simply stop processing here
1051                                         Core\Logger::log("Invalid ZRL parameter " . $_GET['zrl'], Core\Logger::DEBUG);
1052                                         echo Module\Special\HTTPException::rawContent(
1053                                                 new HTTPException\ForbiddenException()
1054                                         );
1055                                         exit;
1056                                 }
1057                         }
1058                 }
1059
1060                 if (!empty($_GET['owt']) && $this->getMode()->isNormal()) {
1061                         $token = $_GET['owt'];
1062                         $this->query_string = Model\Profile::stripQueryParam($this->query_string, 'owt');
1063                         Model\Profile::openWebAuthInit($token);
1064                 }
1065
1066                 Module\Login::sessionAuth();
1067
1068                 if (empty($_SESSION['authenticated'])) {
1069                         header('X-Account-Management-Status: none');
1070                 }
1071
1072                 $_SESSION['sysmsg']       = defaults($_SESSION, 'sysmsg'      , []);
1073                 $_SESSION['sysmsg_info']  = defaults($_SESSION, 'sysmsg_info' , []);
1074                 $_SESSION['last_updated'] = defaults($_SESSION, 'last_updated', []);
1075
1076                 /*
1077                  * check_config() is responsible for running update scripts. These automatically
1078                  * update the DB schema whenever we push a new one out. It also checks to see if
1079                  * any addons have been added or removed and reacts accordingly.
1080                  */
1081
1082                 // in install mode, any url loads install module
1083                 // but we need "view" module for stylesheet
1084                 if ($this->getMode()->isInstall() && $this->module != 'view') {
1085                         $this->module = 'install';
1086                 } elseif (!$this->getMode()->has(App\Mode::MAINTENANCEDISABLED) && $this->module != 'view') {
1087                         $this->module = 'maintenance';
1088                 } else {
1089                         $this->checkURL();
1090                         Core\Update::check($this->getBasePath(), false, $this->getMode());
1091                         Core\Addon::loadAddons();
1092                         Core\Hook::loadHooks();
1093                 }
1094
1095                 $this->page = [
1096                         'aside' => '',
1097                         'bottom' => '',
1098                         'content' => '',
1099                         'footer' => '',
1100                         'htmlhead' => '',
1101                         'nav' => '',
1102                         'page_title' => '',
1103                         'right_aside' => '',
1104                         'template' => '',
1105                         'title' => ''
1106                 ];
1107
1108                 // Compatibility with the Android Diaspora client
1109                 if ($this->module == 'stream') {
1110                         $this->internalRedirect('network?f=&order=post');
1111                 }
1112
1113                 if ($this->module == 'conversations') {
1114                         $this->internalRedirect('message');
1115                 }
1116
1117                 if ($this->module == 'commented') {
1118                         $this->internalRedirect('network?f=&order=comment');
1119                 }
1120
1121                 if ($this->module == 'liked') {
1122                         $this->internalRedirect('network?f=&order=comment');
1123                 }
1124
1125                 if ($this->module == 'activity') {
1126                         $this->internalRedirect('network/?f=&conv=1');
1127                 }
1128
1129                 if (($this->module == 'status_messages') && ($this->cmd == 'status_messages/new')) {
1130                         $this->internalRedirect('bookmarklet');
1131                 }
1132
1133                 if (($this->module == 'user') && ($this->cmd == 'user/edit')) {
1134                         $this->internalRedirect('settings');
1135                 }
1136
1137                 if (($this->module == 'tag_followings') && ($this->cmd == 'tag_followings/manage')) {
1138                         $this->internalRedirect('search');
1139                 }
1140
1141                 // Compatibility with the Firefox App
1142                 if (($this->module == "users") && ($this->cmd == "users/sign_in")) {
1143                         $this->module = "login";
1144                 }
1145
1146                 /*
1147                  * ROUTING
1148                  *
1149                  * From the request URL, routing consists of obtaining the name of a BaseModule-extending class of which the
1150                  * post() and/or content() static methods can be respectively called to produce a data change or an output.
1151                  */
1152
1153                 // First we try explicit routes defined in App\Router
1154                 $this->router->collectRoutes();
1155
1156                 $data = $this->router->getRouteCollector();
1157                 Hook::callAll('route_collection', $data);
1158
1159                 $this->module_class = $this->router->getModuleClass($this->cmd);
1160
1161                 // Then we try addon-provided modules that we wrap in the LegacyModule class
1162                 if (!$this->module_class && Core\Addon::isEnabled($this->module) && file_exists("addon/{$this->module}/{$this->module}.php")) {
1163                         //Check if module is an app and if public access to apps is allowed or not
1164                         $privateapps = $this->config->get('config', 'private_addons', false);
1165                         if ((!local_user()) && Core\Hook::isAddonApp($this->module) && $privateapps) {
1166                                 info(Core\L10n::t("You must be logged in to use addons. "));
1167                         } else {
1168                                 include_once "addon/{$this->module}/{$this->module}.php";
1169                                 if (function_exists($this->module . '_module')) {
1170                                         LegacyModule::setModuleFile("addon/{$this->module}/{$this->module}.php");
1171                                         $this->module_class = LegacyModule::class;
1172                                 }
1173                         }
1174                 }
1175
1176                 /* Finally, we look for a 'standard' program module in the 'mod' directory
1177                  * We emulate a Module class through the LegacyModule class
1178                  */
1179                 if (!$this->module_class && file_exists("mod/{$this->module}.php")) {
1180                         LegacyModule::setModuleFile("mod/{$this->module}.php");
1181                         $this->module_class = LegacyModule::class;
1182                 }
1183
1184                 /* The URL provided does not resolve to a valid module.
1185                  *
1186                  * On Dreamhost sites, quite often things go wrong for no apparent reason and they send us to '/internal_error.html'.
1187                  * We don't like doing this, but as it occasionally accounts for 10-20% or more of all site traffic -
1188                  * 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
1189                  * this will often succeed and eventually do the right thing.
1190                  *
1191                  * Otherwise we are going to emit a 404 not found.
1192                  */
1193                 if (!$this->module_class) {
1194                         // Stupid browser tried to pre-fetch our Javascript img template. Don't log the event or return anything - just quietly exit.
1195                         if (!empty($_SERVER['QUERY_STRING']) && preg_match('/{[0-9]}/', $_SERVER['QUERY_STRING']) !== 0) {
1196                                 exit();
1197                         }
1198
1199                         if (!empty($_SERVER['QUERY_STRING']) && ($_SERVER['QUERY_STRING'] === 'q=internal_error.html') && isset($dreamhost_error_hack)) {
1200                                 Core\Logger::log('index.php: dreamhost_error_hack invoked. Original URI =' . $_SERVER['REQUEST_URI']);
1201                                 $this->internalRedirect($_SERVER['REQUEST_URI']);
1202                         }
1203
1204                         Core\Logger::log('index.php: page not found: ' . $_SERVER['REQUEST_URI'] . ' ADDRESS: ' . $_SERVER['REMOTE_ADDR'] . ' QUERY: ' . $_SERVER['QUERY_STRING'], Core\Logger::DEBUG);
1205
1206                         $this->module_class = Module\PageNotFound::class;
1207                 }
1208
1209                 // Initialize module that can set the current theme in the init() method, either directly or via App->profile_uid
1210                 $this->page['page_title'] = $this->module;
1211                 try {
1212                         $placeholder = '';
1213
1214                         Core\Hook::callAll($this->module . '_mod_init', $placeholder);
1215
1216                         call_user_func([$this->module_class, 'init']);
1217
1218                         // "rawContent" is especially meant for technical endpoints.
1219                         // This endpoint doesn't need any theme initialization or other comparable stuff.
1220                         call_user_func([$this->module_class, 'rawContent']);
1221
1222                         // Load current theme info after module has been initialized as theme could have been set in module
1223                         $theme_info_file = 'view/theme/' . $this->getCurrentTheme() . '/theme.php';
1224                         if (file_exists($theme_info_file)) {
1225                                 require_once $theme_info_file;
1226                         }
1227
1228                         if (function_exists(str_replace('-', '_', $this->getCurrentTheme()) . '_init')) {
1229                                 $func = str_replace('-', '_', $this->getCurrentTheme()) . '_init';
1230                                 $func($this);
1231                         }
1232
1233                         if ($_SERVER['REQUEST_METHOD'] === 'POST') {
1234                                 Core\Hook::callAll($this->module . '_mod_post', $_POST);
1235                                 call_user_func([$this->module_class, 'post']);
1236                         }
1237
1238                         Core\Hook::callAll($this->module . '_mod_afterpost', $placeholder);
1239                         call_user_func([$this->module_class, 'afterpost']);
1240                 } catch(HTTPException $e) {
1241                         echo Module\Special\HTTPException::rawContent($e);
1242                         exit;
1243                 }
1244
1245                 $content = '';
1246
1247                 try {
1248                         $arr = ['content' => $content];
1249                         Core\Hook::callAll($this->module . '_mod_content', $arr);
1250                         $content = $arr['content'];
1251                         $arr = ['content' => call_user_func([$this->module_class, 'content'])];
1252                         Core\Hook::callAll($this->module . '_mod_aftercontent', $arr);
1253                         $content .= $arr['content'];
1254                 } catch(HTTPException $e) {
1255                         $content = Module\Special\HTTPException::content($e);
1256                 }
1257
1258                 // initialise content region
1259                 if ($this->getMode()->isNormal()) {
1260                         Core\Hook::callAll('page_content_top', $this->page['content']);
1261                 }
1262
1263                 $this->page['content'] .= $content;
1264
1265                 /* Create the page head after setting the language
1266                  * and getting any auth credentials.
1267                  *
1268                  * Moved initHead() and initFooter() to after
1269                  * all the module functions have executed so that all
1270                  * theme choices made by the modules can take effect.
1271                  */
1272                 $this->initHead();
1273
1274                 /* Build the page ending -- this is stuff that goes right before
1275                  * the closing </body> tag
1276                  */
1277                 $this->initFooter();
1278
1279                 if (!$this->isAjax()) {
1280                         Core\Hook::callAll('page_end', $this->page['content']);
1281                 }
1282
1283                 // Add the navigation (menu) template
1284                 if ($this->module != 'install' && $this->module != 'maintenance') {
1285                         $this->page['htmlhead'] .= Core\Renderer::replaceMacros(Core\Renderer::getMarkupTemplate('nav_head.tpl'), []);
1286                         $this->page['nav']       = Content\Nav::build($this);
1287                 }
1288
1289                 // Build the page - now that we have all the components
1290                 if (isset($_GET["mode"]) && (($_GET["mode"] == "raw") || ($_GET["mode"] == "minimal"))) {
1291                         $doc = new DOMDocument();
1292
1293                         $target = new DOMDocument();
1294                         $target->loadXML("<root></root>");
1295
1296                         $content = mb_convert_encoding($this->page["content"], 'HTML-ENTITIES', "UTF-8");
1297
1298                         /// @TODO one day, kill those error-surpressing @ stuff, or PHP should ban it
1299                         @$doc->loadHTML($content);
1300
1301                         $xpath = new DOMXPath($doc);
1302
1303                         $list = $xpath->query("//*[contains(@id,'tread-wrapper-')]");  /* */
1304
1305                         foreach ($list as $item) {
1306                                 $item = $target->importNode($item, true);
1307
1308                                 // And then append it to the target
1309                                 $target->documentElement->appendChild($item);
1310                         }
1311
1312                         if ($_GET["mode"] == "raw") {
1313                                 header("Content-type: text/html; charset=utf-8");
1314
1315                                 echo substr($target->saveHTML(), 6, -8);
1316
1317                                 exit();
1318                         }
1319                 }
1320
1321                 $page    = $this->page;
1322                 $profile = $this->profile;
1323
1324                 header("X-Friendica-Version: " . FRIENDICA_VERSION);
1325                 header("Content-type: text/html; charset=utf-8");
1326
1327                 if ($this->config->get('system', 'hsts') && ($this->baseURL->getSSLPolicy() == BaseUrl::SSL_POLICY_FULL)) {
1328                         header("Strict-Transport-Security: max-age=31536000");
1329                 }
1330
1331                 // Some security stuff
1332                 header('X-Content-Type-Options: nosniff');
1333                 header('X-XSS-Protection: 1; mode=block');
1334                 header('X-Permitted-Cross-Domain-Policies: none');
1335                 header('X-Frame-Options: sameorigin');
1336
1337                 // Things like embedded OSM maps don't work, when this is enabled
1338                 // 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'");
1339
1340                 /* We use $_GET["mode"] for special page templates. So we will check if we have
1341                  * to load another page template than the default one.
1342                  * The page templates are located in /view/php/ or in the theme directory.
1343                  */
1344                 if (isset($_GET["mode"])) {
1345                         $template = Core\Theme::getPathForFile($_GET["mode"] . '.php');
1346                 }
1347
1348                 // If there is no page template use the default page template
1349                 if (empty($template)) {
1350                         $template = Core\Theme::getPathForFile("default.php");
1351                 }
1352
1353                 // Theme templates expect $a as an App instance
1354                 $a = $this;
1355
1356                 // Used as is in view/php/default.php
1357                 $lang = Core\L10n::getCurrentLang();
1358
1359                 /// @TODO Looks unsafe (remote-inclusion), is maybe not but Core\Theme::getPathForFile() uses file_exists() but does not escape anything
1360                 require_once $template;
1361         }
1362
1363         /**
1364          * Redirects to another module relative to the current Friendica base.
1365          * If you want to redirect to a external URL, use System::externalRedirectTo()
1366          *
1367          * @param string $toUrl The destination URL (Default is empty, which is the default page of the Friendica node)
1368          * @param bool $ssl if true, base URL will try to get called with https:// (works just for relative paths)
1369          *
1370          * @throws HTTPException\InternalServerErrorException In Case the given URL is not relative to the Friendica node
1371          */
1372         public function internalRedirect($toUrl = '', $ssl = false)
1373         {
1374                 if (!empty(parse_url($toUrl, PHP_URL_SCHEME))) {
1375                         throw new HTTPException\InternalServerErrorException("'$toUrl is not a relative path, please use System::externalRedirectTo");
1376                 }
1377
1378                 $redirectTo = $this->getBaseURL($ssl) . '/' . ltrim($toUrl, '/');
1379                 Core\System::externalRedirect($redirectTo);
1380         }
1381
1382         /**
1383          * Automatically redirects to relative or absolute URL
1384          * Should only be used if it isn't clear if the URL is either internal or external
1385          *
1386          * @param string $toUrl The target URL
1387          * @throws HTTPException\InternalServerErrorException
1388          */
1389         public function redirect($toUrl)
1390         {
1391                 if (!empty(parse_url($toUrl, PHP_URL_SCHEME))) {
1392                         Core\System::externalRedirect($toUrl);
1393                 } else {
1394                         $this->internalRedirect($toUrl);
1395                 }
1396         }
1397 }