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