]> git.mxchange.org Git - friendica.git/blob - src/App.php
Fix formatting in Model\Group
[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\ConfigCache;
12 use Friendica\Core\Config\Configuration;
13 use Friendica\Core\Hook;
14 use Friendica\Core\L10n\L10n;
15 use Friendica\Core\System;
16 use Friendica\Core\Theme;
17 use Friendica\Database\Database;
18 use Friendica\Database\DBA;
19 use Friendica\Model\Profile;
20 use Friendica\Module\Login;
21 use Friendica\Network\HTTPException;
22 use Friendica\Util\BaseURL;
23 use Friendica\Util\ConfigFileLoader;
24 use Friendica\Util\HTTPSignature;
25 use Friendica\Util\Profiler;
26 use Friendica\Util\Strings;
27 use Psr\Log\LoggerInterface;
28
29 /**
30  *
31  * class: App
32  *
33  * @brief Our main application structure for the life of this page.
34  *
35  * Primarily deals with the URL that got us here
36  * and tries to make some sense of it, and
37  * stores our page contents and config storage
38  * and anything else that might need to be passed around
39  * before we spit the page out.
40  *
41  */
42 class App
43 {
44         public $module_class = null;
45         public $query_string = '';
46         public $page = [];
47         public $profile;
48         public $profile_uid;
49         public $user;
50         public $cid;
51         public $contact;
52         public $contacts;
53         public $page_contact;
54         public $content;
55         public $data = [];
56         public $cmd = '';
57         public $argv;
58         public $argc;
59         public $module;
60         public $timezone;
61         public $interactive = true;
62         public $identities;
63         public $is_mobile = false;
64         public $is_tablet = false;
65         public $theme_info = [];
66         public $category;
67         // Allow themes to control internal parameters
68         // by changing App values in theme.php
69
70         public $sourcename = '';
71         public $videowidth = 425;
72         public $videoheight = 350;
73         public $force_max_items = 0;
74         public $theme_events_in_profile = true;
75
76         public $stylesheets = [];
77         public $footerScripts = [];
78
79         /**
80          * @var App\Mode The Mode of the Application
81          */
82         private $mode;
83
84         /**
85          * @var App\Router
86          */
87         private $router;
88
89         /**
90          * @var BaseURL
91          */
92         private $baseURL;
93
94         /**
95          * @var bool true, if the call is from an backend node (f.e. worker)
96          */
97         private $isBackend;
98
99         /**
100          * @var string The name of the current theme
101          */
102         private $currentTheme;
103
104         /**
105          * @var bool check if request was an AJAX (xmlhttprequest) request
106          */
107         private $isAjax;
108
109         /**
110          * @var MobileDetect
111          */
112         public $mobileDetect;
113
114         /**
115          * @var Configuration The config
116          */
117         private $config;
118
119         /**
120          * @var LoggerInterface The logger
121          */
122         private $logger;
123
124         /**
125          * @var Profiler The profiler of this app
126          */
127         private $profiler;
128
129         /**
130          * @var Database The Friendica database connection
131          */
132         private $database;
133
134         /**
135          * @var L10n The translator
136          */
137         private $l10n;
138
139         /**
140          * Returns the current config cache of this node
141          *
142          * @return ConfigCache
143          */
144         public function getConfigCache()
145         {
146                 return $this->config->getCache();
147         }
148
149         /**
150          * Returns the current config of this node
151          *
152          * @return Configuration
153          */
154         public function getConfig()
155         {
156                 return $this->config;
157         }
158
159         /**
160          * The basepath of this app
161          *
162          * @return string
163          */
164         public function getBasePath()
165         {
166                 // Don't use the basepath of the config table for basepath (it should always be the config-file one)
167                 return $this->config->getCache()->get('system', 'basepath');
168         }
169
170         /**
171          * The Logger of this app
172          *
173          * @return LoggerInterface
174          */
175         public function getLogger()
176         {
177                 return $this->logger;
178         }
179
180         /**
181          * The profiler of this app
182          *
183          * @return Profiler
184          */
185         public function getProfiler()
186         {
187                 return $this->profiler;
188         }
189
190         /**
191          * Returns the Mode of the Application
192          *
193          * @return App\Mode The Application Mode
194          */
195         public function getMode()
196         {
197                 return $this->mode;
198         }
199
200         /**
201          * Register a stylesheet file path to be included in the <head> tag of every page.
202          * Inclusion is done in App->initHead().
203          * The path can be absolute or relative to the Friendica installation base folder.
204          *
205          * @see initHead()
206          *
207          * @param string $path
208          */
209         public function registerStylesheet($path)
210         {
211                 if (mb_strpos($path, $this->getBasePath() . DIRECTORY_SEPARATOR) === 0) {
212                         $path = mb_substr($path, mb_strlen($this->getBasePath() . DIRECTORY_SEPARATOR));
213                 }
214
215                 $this->stylesheets[] = trim($path, '/');
216         }
217
218         /**
219          * Register a javascript file path to be included in the <footer> tag of every page.
220          * Inclusion is done in App->initFooter().
221          * The path can be absolute or relative to the Friendica installation base folder.
222          *
223          * @see initFooter()
224          *
225          * @param string $path
226          */
227         public function registerFooterScript($path)
228         {
229                 $url = str_replace($this->getBasePath() . DIRECTORY_SEPARATOR, '', $path);
230
231                 $this->footerScripts[] = trim($url, '/');
232         }
233
234         public $queue;
235
236         /**
237          * @brief App constructor.
238          *
239          * @param Database $database The Friendica Database
240          * @param Configuration    $config    The Configuration
241          * @param App\Mode         $mode      The mode of this Friendica app
242          * @param App\Router       $router    The router of this Friendica app
243          * @param BaseURL          $baseURL   The full base URL of this Friendica app
244          * @param LoggerInterface  $logger    The current app logger
245          * @param Profiler         $profiler  The profiler of this application
246          * @param L10n             $l10n      The translator instance
247          *
248          * @throws Exception if the Basepath is not usable
249          */
250         public function __construct(Database $database, Configuration $config, App\Mode $mode, App\Router $router, BaseURL $baseURL, LoggerInterface $logger, Profiler $profiler, L10n $l10n)
251         {
252                 $this->database = $database;
253                 $this->config   = $config;
254                 $this->mode     = $mode;
255                 $this->router   = $router;
256                 $this->baseURL  = $baseURL;
257                 $this->profiler = $profiler;
258                 $this->logger   = $logger;
259                 $this->l10n     = $l10n;
260
261                 $this->profiler->reset();
262
263                 $this->reload();
264
265                 set_time_limit(0);
266
267                 // This has to be quite large to deal with embedded private photos
268                 ini_set('pcre.backtrack_limit', 500000);
269
270                 set_include_path(
271                         get_include_path() . PATH_SEPARATOR
272                         . $this->getBasePath() . DIRECTORY_SEPARATOR . 'include' . PATH_SEPARATOR
273                         . $this->getBasePath() . DIRECTORY_SEPARATOR . 'library' . PATH_SEPARATOR
274                         . $this->getBasePath());
275
276                 if (!empty($_SERVER['QUERY_STRING']) && strpos($_SERVER['QUERY_STRING'], 'pagename=') === 0) {
277                         $this->query_string = substr($_SERVER['QUERY_STRING'], 9);
278                 } elseif (!empty($_SERVER['QUERY_STRING']) && strpos($_SERVER['QUERY_STRING'], 'q=') === 0) {
279                         $this->query_string = substr($_SERVER['QUERY_STRING'], 2);
280                 }
281
282                 // removing trailing / - maybe a nginx problem
283                 $this->query_string = ltrim($this->query_string, '/');
284
285                 if (!empty($_GET['pagename'])) {
286                         $this->cmd = trim($_GET['pagename'], '/\\');
287                 } elseif (!empty($_GET['q'])) {
288                         $this->cmd = trim($_GET['q'], '/\\');
289                 }
290
291                 // fix query_string
292                 $this->query_string = str_replace($this->cmd . '&', $this->cmd . '?', $this->query_string);
293
294                 // unix style "homedir"
295                 if (substr($this->cmd, 0, 1) === '~') {
296                         $this->cmd = 'profile/' . substr($this->cmd, 1);
297                 }
298
299                 // Diaspora style profile url
300                 if (substr($this->cmd, 0, 2) === 'u/') {
301                         $this->cmd = 'profile/' . substr($this->cmd, 2);
302                 }
303
304                 /*
305                  * Break the URL path into C style argc/argv style arguments for our
306                  * modules. Given "http://example.com/module/arg1/arg2", $this->argc
307                  * will be 3 (integer) and $this->argv will contain:
308                  *   [0] => 'module'
309                  *   [1] => 'arg1'
310                  *   [2] => 'arg2'
311                  *
312                  *
313                  * There will always be one argument. If provided a naked domain
314                  * URL, $this->argv[0] is set to "home".
315                  */
316
317                 $this->argv = explode('/', $this->cmd);
318                 $this->argc = count($this->argv);
319                 if ((array_key_exists('0', $this->argv)) && strlen($this->argv[0])) {
320                         $this->module = str_replace('.', '_', $this->argv[0]);
321                         $this->module = str_replace('-', '_', $this->module);
322                 } else {
323                         $this->argc = 1;
324                         $this->argv = ['home'];
325                         $this->module = 'home';
326                 }
327
328                 $this->isBackend = $this->isBackend || $this->checkBackend($this->module);
329
330                 // Detect mobile devices
331                 $mobile_detect = new MobileDetect();
332
333                 $this->mobileDetect = $mobile_detect;
334
335                 $this->is_mobile = $mobile_detect->isMobile();
336                 $this->is_tablet = $mobile_detect->isTablet();
337
338                 $this->isAjax = strtolower(defaults($_SERVER, 'HTTP_X_REQUESTED_WITH', '')) == 'xmlhttprequest';
339
340                 // Register template engines
341                 Core\Renderer::registerTemplateEngine('Friendica\Render\FriendicaSmartyEngine');
342         }
343
344         /**
345          * Reloads the whole app instance
346          */
347         public function reload()
348         {
349                 $this->isBackend = basename($_SERVER['PHP_SELF'], '.php') !== 'index';
350
351                 $this->getMode()->determine($this->getBasePath());
352
353                 if ($this->getMode()->has(App\Mode::DBAVAILABLE)) {
354                         $this->profiler->update($this->config);
355
356                         Core\Hook::loadHooks();
357                         $loader = new ConfigFileLoader($this->getBasePath());
358                         Core\Hook::callAll('load_config', $loader);
359                 }
360
361                 $this->loadDefaultTimezone();
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'         => $this->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' => $this->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($this->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                 try {
980                         // Missing DB connection: ERROR
981                         if ($this->getMode()->has(App\Mode::LOCALCONFIGPRESENT) && !$this->getMode()->has(App\Mode::DBAVAILABLE)) {
982                                 throw new HTTPException\InternalServerErrorException('Apologies but the website is unavailable at the moment.');
983                         }
984
985                         // Max Load Average reached: ERROR
986                         if ($this->isMaxProcessesReached() || $this->isMaxLoadReached()) {
987                                 header('Retry-After: 120');
988                                 header('Refresh: 120; url=' . $this->getBaseURL() . "/" . $this->query_string);
989
990                                 throw new HTTPException\ServiceUnavailableException('The node is currently overloaded. Please try again later.');
991                         }
992
993                         if (!$this->getMode()->isInstall()) {
994                                 // Force SSL redirection
995                                 if ($this->baseURL->checkRedirectHttps()) {
996                                         System::externalRedirect($this->getBaseURL() . '/' . $this->query_string);
997                                 }
998
999                                 Core\Session::init();
1000                                 Core\Hook::callAll('init_1');
1001                         }
1002
1003                         // Exclude the backend processes from the session management
1004                         if (!$this->isBackend()) {
1005                                 $stamp1 = microtime(true);
1006                                 session_start();
1007                                 $this->profiler->saveTimestamp($stamp1, 'parser', Core\System::callstack());
1008                                 $this->l10n->setSessionVariable();
1009                                 $this->l10n->setLangFromSession();
1010                         } else {
1011                                 $_SESSION = [];
1012                                 Core\Worker::executeIfIdle();
1013                         }
1014
1015                         if ($this->getMode()->isNormal()) {
1016                                 $requester = HTTPSignature::getSigner('', $_SERVER);
1017                                 if (!empty($requester)) {
1018                                         Profile::addVisitorCookieForHandle($requester);
1019                                 }
1020                         }
1021
1022                         // ZRL
1023                         if (!empty($_GET['zrl']) && $this->getMode()->isNormal()) {
1024                                 $this->query_string = Model\Profile::stripZrls($this->query_string);
1025                                 if (!local_user()) {
1026                                         // Only continue when the given profile link seems valid
1027                                         // Valid profile links contain a path with "/profile/" and no query parameters
1028                                         if ((parse_url($_GET['zrl'], PHP_URL_QUERY) == "") &&
1029                                                 strstr(parse_url($_GET['zrl'], PHP_URL_PATH), "/profile/")) {
1030                                                 if (Core\Session::get('visitor_home') != $_GET["zrl"]) {
1031                                                         Core\Session::set('my_url', $_GET['zrl']);
1032                                                         Core\Session::set('authenticated', 0);
1033                                                 }
1034
1035                                                 Model\Profile::zrlInit($this);
1036                                         } else {
1037                                                 // Someone came with an invalid parameter, maybe as a DDoS attempt
1038                                                 // We simply stop processing here
1039                                                 Core\Logger::log("Invalid ZRL parameter " . $_GET['zrl'], Core\Logger::DEBUG);
1040                                                 throw new HTTPException\ForbiddenException();
1041                                         }
1042                                 }
1043                         }
1044
1045                         if (!empty($_GET['owt']) && $this->getMode()->isNormal()) {
1046                                 $token = $_GET['owt'];
1047                                 $this->query_string = Model\Profile::stripQueryParam($this->query_string, 'owt');
1048                                 Model\Profile::openWebAuthInit($token);
1049                         }
1050
1051                         Module\Login::sessionAuth();
1052
1053                         if (empty($_SESSION['authenticated'])) {
1054                                 header('X-Account-Management-Status: none');
1055                         }
1056
1057                         $_SESSION['sysmsg']       = Core\Session::get('sysmsg', []);
1058                         $_SESSION['sysmsg_info']  = Core\Session::get('sysmsg_info', []);
1059                         $_SESSION['last_updated'] = Core\Session::get('last_updated', []);
1060
1061                         /*
1062                          * check_config() is responsible for running update scripts. These automatically
1063                          * update the DB schema whenever we push a new one out. It also checks to see if
1064                          * any addons have been added or removed and reacts accordingly.
1065                          */
1066
1067                         // in install mode, any url loads install module
1068                         // but we need "view" module for stylesheet
1069                         if ($this->getMode()->isInstall() && $this->module !== 'install') {
1070                                 $this->internalRedirect('install');
1071                         } elseif (!$this->getMode()->isInstall() && !$this->getMode()->has(App\Mode::MAINTENANCEDISABLED) && $this->module !== 'maintenance') {
1072                                 $this->internalRedirect('maintenance');
1073                         } else {
1074                                 $this->checkURL();
1075                                 Core\Update::check($this->getBasePath(), false, $this->getMode());
1076                                 Core\Addon::loadAddons();
1077                                 Core\Hook::loadHooks();
1078                         }
1079
1080                         $this->page = [
1081                                 'aside' => '',
1082                                 'bottom' => '',
1083                                 'content' => '',
1084                                 'footer' => '',
1085                                 'htmlhead' => '',
1086                                 'nav' => '',
1087                                 'page_title' => '',
1088                                 'right_aside' => '',
1089                                 'template' => '',
1090                                 'title' => ''
1091                         ];
1092
1093                         // Compatibility with the Android Diaspora client
1094                         if ($this->module == 'stream') {
1095                                 $this->internalRedirect('network?order=post');
1096                         }
1097
1098                         if ($this->module == 'conversations') {
1099                                 $this->internalRedirect('message');
1100                         }
1101
1102                         if ($this->module == 'commented') {
1103                                 $this->internalRedirect('network?order=comment');
1104                         }
1105
1106                         if ($this->module == 'liked') {
1107                                 $this->internalRedirect('network?order=comment');
1108                         }
1109
1110                         if ($this->module == 'activity') {
1111                                 $this->internalRedirect('network?conv=1');
1112                         }
1113
1114                         if (($this->module == 'status_messages') && ($this->cmd == 'status_messages/new')) {
1115                                 $this->internalRedirect('bookmarklet');
1116                         }
1117
1118                         if (($this->module == 'user') && ($this->cmd == 'user/edit')) {
1119                                 $this->internalRedirect('settings');
1120                         }
1121
1122                         if (($this->module == 'tag_followings') && ($this->cmd == 'tag_followings/manage')) {
1123                                 $this->internalRedirect('search');
1124                         }
1125
1126                         // Compatibility with the Firefox App
1127                         if (($this->module == "users") && ($this->cmd == "users/sign_in")) {
1128                                 $this->module = "login";
1129                         }
1130
1131                         /*
1132                          * ROUTING
1133                          *
1134                          * From the request URL, routing consists of obtaining the name of a BaseModule-extending class of which the
1135                          * post() and/or content() static methods can be respectively called to produce a data change or an output.
1136                          */
1137
1138                         // First we try explicit routes defined in App\Router
1139                         $this->router->collectRoutes();
1140
1141                         $data = $this->router->getRouteCollector();
1142                         Hook::callAll('route_collection', $data);
1143
1144                         $this->module_class = $this->router->getModuleClass($this->cmd);
1145
1146                         // Then we try addon-provided modules that we wrap in the LegacyModule class
1147                         if (!$this->module_class && Core\Addon::isEnabled($this->module) && file_exists("addon/{$this->module}/{$this->module}.php")) {
1148                                 //Check if module is an app and if public access to apps is allowed or not
1149                                 $privateapps = $this->config->get('config', 'private_addons', false);
1150                                 if ((!local_user()) && Core\Hook::isAddonApp($this->module) && $privateapps) {
1151                                         info($this->l10n->t("You must be logged in to use addons. "));
1152                                 } else {
1153                                         include_once "addon/{$this->module}/{$this->module}.php";
1154                                         if (function_exists($this->module . '_module')) {
1155                                                 LegacyModule::setModuleFile("addon/{$this->module}/{$this->module}.php");
1156                                                 $this->module_class = LegacyModule::class;
1157                                         }
1158                                 }
1159                         }
1160
1161                         /* Finally, we look for a 'standard' program module in the 'mod' directory
1162                          * We emulate a Module class through the LegacyModule class
1163                          */
1164                         if (!$this->module_class && file_exists("mod/{$this->module}.php")) {
1165                                 LegacyModule::setModuleFile("mod/{$this->module}.php");
1166                                 $this->module_class = LegacyModule::class;
1167                         }
1168
1169                         /* The URL provided does not resolve to a valid module.
1170                          *
1171                          * On Dreamhost sites, quite often things go wrong for no apparent reason and they send us to '/internal_error.html'.
1172                          * We don't like doing this, but as it occasionally accounts for 10-20% or more of all site traffic -
1173                          * 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
1174                          * this will often succeed and eventually do the right thing.
1175                          *
1176                          * Otherwise we are going to emit a 404 not found.
1177                          */
1178                         if (!$this->module_class) {
1179                                 // Stupid browser tried to pre-fetch our Javascript img template. Don't log the event or return anything - just quietly exit.
1180                                 if (!empty($_SERVER['QUERY_STRING']) && preg_match('/{[0-9]}/', $_SERVER['QUERY_STRING']) !== 0) {
1181                                         exit();
1182                                 }
1183
1184                                 if (!empty($_SERVER['QUERY_STRING']) && ($_SERVER['QUERY_STRING'] === 'q=internal_error.html') && isset($dreamhost_error_hack)) {
1185                                         Core\Logger::log('index.php: dreamhost_error_hack invoked. Original URI =' . $_SERVER['REQUEST_URI']);
1186                                         $this->internalRedirect($_SERVER['REQUEST_URI']);
1187                                 }
1188
1189                                 Core\Logger::log('index.php: page not found: ' . $_SERVER['REQUEST_URI'] . ' ADDRESS: ' . $_SERVER['REMOTE_ADDR'] . ' QUERY: ' . $_SERVER['QUERY_STRING'], Core\Logger::DEBUG);
1190
1191                                 $this->module_class = Module\PageNotFound::class;
1192                         }
1193
1194                         // Initialize module that can set the current theme in the init() method, either directly or via App->profile_uid
1195                         $this->page['page_title'] = $this->module;
1196
1197                         $placeholder = '';
1198
1199                         Core\Hook::callAll($this->module . '_mod_init', $placeholder);
1200
1201                         call_user_func([$this->module_class, 'init']);
1202
1203                         // "rawContent" is especially meant for technical endpoints.
1204                         // This endpoint doesn't need any theme initialization or other comparable stuff.
1205                         call_user_func([$this->module_class, 'rawContent']);
1206
1207                         // Load current theme info after module has been initialized as theme could have been set in module
1208                         $theme_info_file = 'view/theme/' . $this->getCurrentTheme() . '/theme.php';
1209                         if (file_exists($theme_info_file)) {
1210                                 require_once $theme_info_file;
1211                         }
1212
1213                         if (function_exists(str_replace('-', '_', $this->getCurrentTheme()) . '_init')) {
1214                                 $func = str_replace('-', '_', $this->getCurrentTheme()) . '_init';
1215                                 $func($this);
1216                         }
1217
1218                         if ($_SERVER['REQUEST_METHOD'] === 'POST') {
1219                                 Core\Hook::callAll($this->module . '_mod_post', $_POST);
1220                                 call_user_func([$this->module_class, 'post']);
1221                         }
1222
1223                         Core\Hook::callAll($this->module . '_mod_afterpost', $placeholder);
1224                         call_user_func([$this->module_class, 'afterpost']);
1225                 } catch(HTTPException $e) {
1226                         Module\Special\HTTPException::rawContent($e);
1227                 }
1228
1229                 $content = '';
1230
1231                 try {
1232                         $arr = ['content' => $content];
1233                         Core\Hook::callAll($this->module . '_mod_content', $arr);
1234                         $content = $arr['content'];
1235                         $arr = ['content' => call_user_func([$this->module_class, 'content'])];
1236                         Core\Hook::callAll($this->module . '_mod_aftercontent', $arr);
1237                         $content .= $arr['content'];
1238                 } catch(HTTPException $e) {
1239                         $content = Module\Special\HTTPException::content($e);
1240                 }
1241
1242                 // initialise content region
1243                 if ($this->getMode()->isNormal()) {
1244                         Core\Hook::callAll('page_content_top', $this->page['content']);
1245                 }
1246
1247                 $this->page['content'] .= $content;
1248
1249                 /* Create the page head after setting the language
1250                  * and getting any auth credentials.
1251                  *
1252                  * Moved initHead() and initFooter() to after
1253                  * all the module functions have executed so that all
1254                  * theme choices made by the modules can take effect.
1255                  */
1256                 $this->initHead();
1257
1258                 /* Build the page ending -- this is stuff that goes right before
1259                  * the closing </body> tag
1260                  */
1261                 $this->initFooter();
1262
1263                 if (!$this->isAjax()) {
1264                         Core\Hook::callAll('page_end', $this->page['content']);
1265                 }
1266
1267                 // Add the navigation (menu) template
1268                 if ($this->module != 'install' && $this->module != 'maintenance') {
1269                         $this->page['htmlhead'] .= Core\Renderer::replaceMacros(Core\Renderer::getMarkupTemplate('nav_head.tpl'), []);
1270                         $this->page['nav']       = Content\Nav::build($this);
1271                 }
1272
1273                 // Build the page - now that we have all the components
1274                 if (isset($_GET["mode"]) && (($_GET["mode"] == "raw") || ($_GET["mode"] == "minimal"))) {
1275                         $doc = new DOMDocument();
1276
1277                         $target = new DOMDocument();
1278                         $target->loadXML("<root></root>");
1279
1280                         $content = mb_convert_encoding($this->page["content"], 'HTML-ENTITIES', "UTF-8");
1281
1282                         /// @TODO one day, kill those error-surpressing @ stuff, or PHP should ban it
1283                         @$doc->loadHTML($content);
1284
1285                         $xpath = new DOMXPath($doc);
1286
1287                         $list = $xpath->query("//*[contains(@id,'tread-wrapper-')]");  /* */
1288
1289                         foreach ($list as $item) {
1290                                 $item = $target->importNode($item, true);
1291
1292                                 // And then append it to the target
1293                                 $target->documentElement->appendChild($item);
1294                         }
1295
1296                         if ($_GET["mode"] == "raw") {
1297                                 header("Content-type: text/html; charset=utf-8");
1298
1299                                 echo substr($target->saveHTML(), 6, -8);
1300
1301                                 exit();
1302                         }
1303                 }
1304
1305                 $page    = $this->page;
1306                 $profile = $this->profile;
1307
1308                 header("X-Friendica-Version: " . FRIENDICA_VERSION);
1309                 header("Content-type: text/html; charset=utf-8");
1310
1311                 if ($this->config->get('system', 'hsts') && ($this->baseURL->getSSLPolicy() == BaseUrl::SSL_POLICY_FULL)) {
1312                         header("Strict-Transport-Security: max-age=31536000");
1313                 }
1314
1315                 // Some security stuff
1316                 header('X-Content-Type-Options: nosniff');
1317                 header('X-XSS-Protection: 1; mode=block');
1318                 header('X-Permitted-Cross-Domain-Policies: none');
1319                 header('X-Frame-Options: sameorigin');
1320
1321                 // Things like embedded OSM maps don't work, when this is enabled
1322                 // 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'");
1323
1324                 /* We use $_GET["mode"] for special page templates. So we will check if we have
1325                  * to load another page template than the default one.
1326                  * The page templates are located in /view/php/ or in the theme directory.
1327                  */
1328                 if (isset($_GET["mode"])) {
1329                         $template = Core\Theme::getPathForFile($_GET["mode"] . '.php');
1330                 }
1331
1332                 // If there is no page template use the default page template
1333                 if (empty($template)) {
1334                         $template = Core\Theme::getPathForFile("default.php");
1335                 }
1336
1337                 // Theme templates expect $a as an App instance
1338                 $a = $this;
1339
1340                 // Used as is in view/php/default.php
1341                 $lang = $this->l10n->getCurrentLang();
1342
1343                 /// @TODO Looks unsafe (remote-inclusion), is maybe not but Core\Theme::getPathForFile() uses file_exists() but does not escape anything
1344                 require_once $template;
1345         }
1346
1347         /**
1348          * Redirects to another module relative to the current Friendica base.
1349          * If you want to redirect to a external URL, use System::externalRedirectTo()
1350          *
1351          * @param string $toUrl The destination URL (Default is empty, which is the default page of the Friendica node)
1352          * @param bool $ssl if true, base URL will try to get called with https:// (works just for relative paths)
1353          *
1354          * @throws HTTPException\InternalServerErrorException In Case the given URL is not relative to the Friendica node
1355          */
1356         public function internalRedirect($toUrl = '', $ssl = false)
1357         {
1358                 if (!empty(parse_url($toUrl, PHP_URL_SCHEME))) {
1359                         throw new HTTPException\InternalServerErrorException("'$toUrl is not a relative path, please use System::externalRedirectTo");
1360                 }
1361
1362                 $redirectTo = $this->getBaseURL($ssl) . '/' . ltrim($toUrl, '/');
1363                 Core\System::externalRedirect($redirectTo);
1364         }
1365
1366         /**
1367          * Automatically redirects to relative or absolute URL
1368          * Should only be used if it isn't clear if the URL is either internal or external
1369          *
1370          * @param string $toUrl The target URL
1371          * @throws HTTPException\InternalServerErrorException
1372          */
1373         public function redirect($toUrl)
1374         {
1375                 if (!empty(parse_url($toUrl, PHP_URL_SCHEME))) {
1376                         Core\System::externalRedirect($toUrl);
1377                 } else {
1378                         $this->internalRedirect($toUrl);
1379                 }
1380         }
1381 }