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