]> git.mxchange.org Git - friendica.git/blob - src/App.php
5e7ad36d80be1c4db826b75271d7153acf47cc26
[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\App\Arguments;
12 use Friendica\Core\Config\Cache\ConfigCache;
13 use Friendica\Core\Config\Configuration;
14 use Friendica\Core\Config\PConfiguration;
15 use Friendica\Core\L10n\L10n;
16 use Friendica\Core\System;
17 use Friendica\Core\Theme;
18 use Friendica\Database\Database;
19 use Friendica\Database\DBA;
20 use Friendica\Model\Profile;
21 use Friendica\Module\Login;
22 use Friendica\Module\Special\HTTPException as ModuleHTTPException;
23 use Friendica\Network\HTTPException;
24 use Friendica\Util\BaseURL;
25 use Friendica\Util\ConfigFileLoader;
26 use Friendica\Util\HTTPSignature;
27 use Friendica\Util\Profiler;
28 use Friendica\Util\Strings;
29 use Psr\Log\LoggerInterface;
30
31 /**
32  *
33  * class: App
34  *
35  * @brief Our main application structure for the life of this page.
36  *
37  * Primarily deals with the URL that got us here
38  * and tries to make some sense of it, and
39  * stores our page contents and config storage
40  * and anything else that might need to be passed around
41  * before we spit the page out.
42  *
43  */
44 class App
45 {
46         /** @deprecated 2019.09 - use App\Arguments->getQueryString() */
47         public $query_string = '';
48         public $page = [];
49         public $profile;
50         public $profile_uid;
51         public $user;
52         public $cid;
53         public $contact;
54         public $contacts;
55         public $page_contact;
56         public $content;
57         public $data = [];
58         /** @deprecated 2019.09 - use App\Arguments->getCommand() */
59         public $cmd = '';
60         /** @deprecated 2019.09 - use App\Arguments->getArgv() or Arguments->get() */
61         public $argv;
62         /** @deprecated 2019.09 - use App\Arguments->getArgc() */
63         public $argc;
64         /** @deprecated 2019.09 - Use App\Module->getName() instead */
65         public $module;
66         public $timezone;
67         public $interactive = true;
68         public $identities;
69         public $is_mobile = false;
70         public $is_tablet = false;
71         public $theme_info = [];
72         public $category;
73         // Allow themes to control internal parameters
74         // by changing App values in theme.php
75
76         public $sourcename = '';
77         public $videowidth = 425;
78         public $videoheight = 350;
79         public $force_max_items = 0;
80         public $theme_events_in_profile = true;
81
82         public $stylesheets = [];
83         public $footerScripts = [];
84
85         /**
86          * @var App\Mode The Mode of the Application
87          */
88         private $mode;
89
90         /**
91          * @var App\Router
92          */
93         private $router;
94
95         /**
96          * @var BaseURL
97          */
98         private $baseURL;
99
100         /**
101          * @var string The name of the current theme
102          */
103         private $currentTheme;
104
105         /**
106          * @var bool check if request was an AJAX (xmlhttprequest) request
107          */
108         private $isAjax;
109
110         /**
111          * @var MobileDetect
112          */
113         public $mobileDetect;
114
115         /**
116          * @var Configuration The config
117          */
118         private $config;
119
120         /**
121          * @var LoggerInterface The logger
122          */
123         private $logger;
124
125         /**
126          * @var Profiler The profiler of this app
127          */
128         private $profiler;
129
130         /**
131          * @var Database The Friendica database connection
132          */
133         private $database;
134
135         /**
136          * @var L10n The translator
137          */
138         private $l10n;
139
140         /**
141          * @var App\Arguments
142          */
143         private $args;
144
145         /**
146          * Returns the current config cache of this node
147          *
148          * @return ConfigCache
149          */
150         public function getConfigCache()
151         {
152                 return $this->config->getCache();
153         }
154
155         /**
156          * Returns the current config of this node
157          *
158          * @return Configuration
159          */
160         public function getConfig()
161         {
162                 return $this->config;
163         }
164
165         /**
166          * The basepath of this app
167          *
168          * @return string
169          */
170         public function getBasePath()
171         {
172                 // Don't use the basepath of the config table for basepath (it should always be the config-file one)
173                 return $this->config->getCache()->get('system', 'basepath');
174         }
175
176         /**
177          * The Logger of this app
178          *
179          * @return LoggerInterface
180          */
181         public function getLogger()
182         {
183                 return $this->logger;
184         }
185
186         /**
187          * The profiler of this app
188          *
189          * @return Profiler
190          */
191         public function getProfiler()
192         {
193                 return $this->profiler;
194         }
195
196         /**
197          * Returns the Mode of the Application
198          *
199          * @return App\Mode The Application Mode
200          */
201         public function getMode()
202         {
203                 return $this->mode;
204         }
205
206         /**
207          * Returns the Database of the Application
208          *
209          * @return Database
210          */
211         public function getDBA()
212         {
213                 return $this->database;
214         }
215
216         /**
217          * Register a stylesheet file path to be included in the <head> tag of every page.
218          * Inclusion is done in App->initHead().
219          * The path can be absolute or relative to the Friendica installation base folder.
220          *
221          * @see initHead()
222          *
223          * @param string $path
224          */
225         public function registerStylesheet($path)
226         {
227                 if (mb_strpos($path, $this->getBasePath() . DIRECTORY_SEPARATOR) === 0) {
228                         $path = mb_substr($path, mb_strlen($this->getBasePath() . DIRECTORY_SEPARATOR));
229                 }
230
231                 $this->stylesheets[] = trim($path, '/');
232         }
233
234         /**
235          * Register a javascript file path to be included in the <footer> tag of every page.
236          * Inclusion is done in App->initFooter().
237          * The path can be absolute or relative to the Friendica installation base folder.
238          *
239          * @see initFooter()
240          *
241          * @param string $path
242          */
243         public function registerFooterScript($path)
244         {
245                 $url = str_replace($this->getBasePath() . DIRECTORY_SEPARATOR, '', $path);
246
247                 $this->footerScripts[] = trim($url, '/');
248         }
249
250         public $queue;
251
252         /**
253          * @brief App constructor.
254          *
255          * @param Database $database The Friendica Database
256          * @param Configuration    $config    The Configuration
257          * @param App\Mode         $mode      The mode of this Friendica app
258          * @param App\Router       $router    The router of this Friendica app
259          * @param BaseURL          $baseURL   The full base URL of this Friendica app
260          * @param LoggerInterface  $logger    The current app logger
261          * @param Profiler         $profiler  The profiler of this application
262          * @param L10n             $l10n      The translator instance
263          *
264          * @throws Exception if the Basepath is not usable
265          */
266         public function __construct(Database $database, Configuration $config, App\Mode $mode, App\Router $router, BaseURL $baseURL, LoggerInterface $logger, Profiler $profiler, L10n $l10n, Arguments $args)
267         {
268                 $this->database = $database;
269                 $this->config   = $config;
270                 $this->mode     = $mode;
271                 $this->router   = $router;
272                 $this->baseURL  = $baseURL;
273                 $this->profiler = $profiler;
274                 $this->logger   = $logger;
275                 $this->l10n     = $l10n;
276                 $this->args = $args;
277
278                 $this->profiler->reset();
279
280                 $this->reload();
281
282                 set_time_limit(0);
283
284                 // This has to be quite large to deal with embedded private photos
285                 ini_set('pcre.backtrack_limit', 500000);
286
287                 set_include_path(
288                         get_include_path() . PATH_SEPARATOR
289                         . $this->getBasePath() . DIRECTORY_SEPARATOR . 'include' . PATH_SEPARATOR
290                         . $this->getBasePath() . DIRECTORY_SEPARATOR . 'library' . PATH_SEPARATOR
291                         . $this->getBasePath());
292
293                 $this->cmd = $args->getCommand();
294                 $this->argv = $args->getArgv();
295                 $this->argc = $args->getArgc();
296                 $this->query_string = $args->getQueryString();
297
298                 // Detect mobile devices
299                 $mobile_detect = new MobileDetect();
300
301                 $this->mobileDetect = $mobile_detect;
302
303                 $this->is_mobile = $mobile_detect->isMobile();
304                 $this->is_tablet = $mobile_detect->isTablet();
305
306                 $this->isAjax = strtolower(defaults($_SERVER, 'HTTP_X_REQUESTED_WITH', '')) == 'xmlhttprequest';
307
308                 // Register template engines
309                 Core\Renderer::registerTemplateEngine('Friendica\Render\FriendicaSmartyEngine');
310         }
311
312         /**
313          * Reloads the whole app instance
314          */
315         public function reload()
316         {
317                 if ($this->getMode()->has(App\Mode::DBAVAILABLE)) {
318                         $this->profiler->update($this->config);
319
320                         Core\Hook::loadHooks();
321                         $loader = new ConfigFileLoader($this->getBasePath());
322                         Core\Hook::callAll('load_config', $loader);
323                 }
324
325                 $this->loadDefaultTimezone();
326         }
327
328         /**
329          * Loads the default timezone
330          *
331          * Include support for legacy $default_timezone
332          *
333          * @global string $default_timezone
334          */
335         private function loadDefaultTimezone()
336         {
337                 if ($this->config->get('system', 'default_timezone')) {
338                         $this->timezone = $this->config->get('system', 'default_timezone');
339                 } else {
340                         global $default_timezone;
341                         $this->timezone = !empty($default_timezone) ? $default_timezone : 'UTC';
342                 }
343
344                 if ($this->timezone) {
345                         date_default_timezone_set($this->timezone);
346                 }
347         }
348
349         /**
350          * Returns the scheme of the current call
351          * @return string
352          *
353          * @deprecated 2019.06 - use BaseURL->getScheme() instead
354          */
355         public function getScheme()
356         {
357                 return $this->baseURL->getScheme();
358         }
359
360         /**
361          * Retrieves the Friendica instance base URL
362          *
363          * @param bool $ssl Whether to append http or https under BaseURL::SSL_POLICY_SELFSIGN
364          *
365          * @return string Friendica server base URL
366          *
367          * @deprecated 2019.09 - use BaseUrl->get($ssl) instead
368          */
369         public function getBaseURL($ssl = false)
370         {
371                 return $this->baseURL->get($ssl);
372         }
373
374         /**
375          * @brief Initializes the baseurl components
376          *
377          * Clears the baseurl cache to prevent inconsistencies
378          *
379          * @param string $url
380          *
381          * @deprecated 2019.06 - use BaseURL->saveByURL($url) instead
382          */
383         public function setBaseURL($url)
384         {
385                 $this->baseURL->saveByURL($url);
386         }
387
388         /**
389          * Returns the current hostname
390          *
391          * @return string
392          *
393          * @deprecated 2019.06 - use BaseURL->getHostname() instead
394          */
395         public function getHostName()
396         {
397                 return $this->baseURL->getHostname();
398         }
399
400         /**
401          * Returns the sub-path of the full URL
402          *
403          * @return string
404          *
405          * @deprecated 2019.06 - use BaseURL->getUrlPath() instead
406          */
407         public function getURLPath()
408         {
409                 return $this->baseURL->getUrlPath();
410         }
411
412         /**
413          * Initializes App->page['htmlhead'].
414          *
415          * Includes:
416          * - Page title
417          * - Favicons
418          * - Registered stylesheets (through App->registerStylesheet())
419          * - Infinite scroll data
420          * - head.tpl template
421          */
422         private function initHead(App\Module $module, PConfiguration $pconfig)
423         {
424                 $interval = ((local_user()) ? $pconfig->get(local_user(), 'system', 'update_interval') : 40000);
425
426                 // If the update is 'deactivated' set it to the highest integer number (~24 days)
427                 if ($interval < 0) {
428                         $interval = 2147483647;
429                 }
430
431                 if ($interval < 10000) {
432                         $interval = 40000;
433                 }
434
435                 // Default title: current module called
436                 if (empty($this->page['title']) && $module->getName()) {
437                         $this->page['title'] = ucfirst($module->getName());
438                 }
439
440                 // Prepend the sitename to the page title
441                 $this->page['title'] = $this->config->get('config', 'sitename', '') . (!empty($this->page['title']) ? ' | ' . $this->page['title'] : '');
442
443                 if (!empty(Core\Renderer::$theme['stylesheet'])) {
444                         $stylesheet = Core\Renderer::$theme['stylesheet'];
445                 } else {
446                         $stylesheet = $this->getCurrentThemeStylesheetPath();
447                 }
448
449                 $this->registerStylesheet($stylesheet);
450
451                 $shortcut_icon = $this->config->get('system', 'shortcut_icon');
452                 if ($shortcut_icon == '') {
453                         $shortcut_icon = 'images/friendica-32.png';
454                 }
455
456                 $touch_icon = $this->config->get('system', 'touch_icon');
457                 if ($touch_icon == '') {
458                         $touch_icon = 'images/friendica-128.png';
459                 }
460
461                 Core\Hook::callAll('head', $this->page['htmlhead']);
462
463                 $tpl = Core\Renderer::getMarkupTemplate('head.tpl');
464                 /* put the head template at the beginning of page['htmlhead']
465                  * since the code added by the modules frequently depends on it
466                  * being first
467                  */
468                 $this->page['htmlhead'] = Core\Renderer::replaceMacros($tpl, [
469                         '$local_user'      => local_user(),
470                         '$generator'       => 'Friendica' . ' ' . FRIENDICA_VERSION,
471                         '$delitem'         => $this->l10n->t('Delete this item?'),
472                         '$update_interval' => $interval,
473                         '$shortcut_icon'   => $shortcut_icon,
474                         '$touch_icon'      => $touch_icon,
475                         '$block_public'    => intval($this->config->get('system', 'block_public')),
476                         '$stylesheets'     => $this->stylesheets,
477                 ]) . $this->page['htmlhead'];
478         }
479
480         /**
481          * Initializes App->page['footer'].
482          *
483          * Includes:
484          * - Javascript homebase
485          * - Mobile toggle link
486          * - Registered footer scripts (through App->registerFooterScript())
487          * - footer.tpl template
488          */
489         private function initFooter()
490         {
491                 // If you're just visiting, let javascript take you home
492                 if (!empty($_SESSION['visitor_home'])) {
493                         $homebase = $_SESSION['visitor_home'];
494                 } elseif (local_user()) {
495                         $homebase = 'profile/' . $this->user['nickname'];
496                 }
497
498                 if (isset($homebase)) {
499                         $this->page['footer'] .= '<script>var homebase="' . $homebase . '";</script>' . "\n";
500                 }
501
502                 /*
503                  * Add a "toggle mobile" link if we're using a mobile device
504                  */
505                 if ($this->is_mobile || $this->is_tablet) {
506                         if (isset($_SESSION['show-mobile']) && !$_SESSION['show-mobile']) {
507                                 $link = 'toggle_mobile?address=' . urlencode(curPageURL());
508                         } else {
509                                 $link = 'toggle_mobile?off=1&address=' . urlencode(curPageURL());
510                         }
511                         $this->page['footer'] .= Core\Renderer::replaceMacros(Core\Renderer::getMarkupTemplate("toggle_mobile_footer.tpl"), [
512                                 '$toggle_link' => $link,
513                                 '$toggle_text' => $this->l10n->t('toggle mobile')
514                         ]);
515                 }
516
517                 Core\Hook::callAll('footer', $this->page['footer']);
518
519                 $tpl = Core\Renderer::getMarkupTemplate('footer.tpl');
520                 $this->page['footer'] = Core\Renderer::replaceMacros($tpl, [
521                         '$footerScripts' => $this->footerScripts,
522                 ]) . $this->page['footer'];
523         }
524
525         /**
526          * @brief Removes the base url from an url. This avoids some mixed content problems.
527          *
528          * @param string $origURL
529          *
530          * @return string The cleaned url
531          * @throws HTTPException\InternalServerErrorException
532          */
533         public function removeBaseURL($origURL)
534         {
535                 // Remove the hostname from the url if it is an internal link
536                 $nurl = Util\Strings::normaliseLink($origURL);
537                 $base = Util\Strings::normaliseLink($this->getBaseURL());
538                 $url = str_replace($base . '/', '', $nurl);
539
540                 // if it is an external link return the orignal value
541                 if ($url == Util\Strings::normaliseLink($origURL)) {
542                         return $origURL;
543                 } else {
544                         return $url;
545                 }
546         }
547
548         /**
549          * Returns the current UserAgent as a String
550          *
551          * @return string the UserAgent as a String
552          * @throws HTTPException\InternalServerErrorException
553          */
554         public function getUserAgent()
555         {
556                 return
557                         FRIENDICA_PLATFORM . " '" .
558                         FRIENDICA_CODENAME . "' " .
559                         FRIENDICA_VERSION . '-' .
560                         DB_UPDATE_VERSION . '; ' .
561                         $this->getBaseURL();
562         }
563
564         /**
565          * Returns true, if the call is from a backend node (f.e. from a worker)
566          *
567          * @return bool Is it a known backend?
568          *
569          * @deprecated 2019.09 - use App\Mode->isBackend() instead
570          * @see App\Mode::isBackend()
571          * Use BaseObject::getClass(App\Mode::class) to get these class directly per global registry
572          */
573         public function isBackend()
574         {
575                 return $this->mode->isBackend();
576         }
577
578         /**
579          * @brief Checks if the maximum number of database processes is reached
580          *
581          * @return bool Is the limit reached?
582          */
583         public function isMaxProcessesReached()
584         {
585                 // Deactivated, needs more investigating if this check really makes sense
586                 return false;
587
588                 /*
589                  * Commented out to suppress static analyzer issues
590                  *
591                 if ($this->is_backend()) {
592                         $process = 'backend';
593                         $max_processes = $this->config->get('system', 'max_processes_backend');
594                         if (intval($max_processes) == 0) {
595                                 $max_processes = 5;
596                         }
597                 } else {
598                         $process = 'frontend';
599                         $max_processes = $this->config->get('system', 'max_processes_frontend');
600                         if (intval($max_processes) == 0) {
601                                 $max_processes = 20;
602                         }
603                 }
604
605                 $processlist = DBA::processlist();
606                 if ($processlist['list'] != '') {
607                         Core\Logger::log('Processcheck: Processes: ' . $processlist['amount'] . ' - Processlist: ' . $processlist['list'], Core\Logger::DEBUG);
608
609                         if ($processlist['amount'] > $max_processes) {
610                                 Core\Logger::log('Processcheck: Maximum number of processes for ' . $process . ' tasks (' . $max_processes . ') reached.', Core\Logger::DEBUG);
611                                 return true;
612                         }
613                 }
614                 return false;
615                  */
616         }
617
618         /**
619          * @brief Checks if the minimal memory is reached
620          *
621          * @return bool Is the memory limit reached?
622          * @throws HTTPException\InternalServerErrorException
623          */
624         public function isMinMemoryReached()
625         {
626                 $min_memory = $this->config->get('system', 'min_memory', 0);
627                 if ($min_memory == 0) {
628                         return false;
629                 }
630
631                 if (!is_readable('/proc/meminfo')) {
632                         return false;
633                 }
634
635                 $memdata = explode("\n", file_get_contents('/proc/meminfo'));
636
637                 $meminfo = [];
638                 foreach ($memdata as $line) {
639                         $data = explode(':', $line);
640                         if (count($data) != 2) {
641                                 continue;
642                         }
643                         list($key, $val) = $data;
644                         $meminfo[$key] = (int) trim(str_replace('kB', '', $val));
645                         $meminfo[$key] = (int) ($meminfo[$key] / 1024);
646                 }
647
648                 if (!isset($meminfo['MemFree'])) {
649                         return false;
650                 }
651
652                 $free = $meminfo['MemFree'];
653
654                 $reached = ($free < $min_memory);
655
656                 if ($reached) {
657                         Core\Logger::log('Minimal memory reached: ' . $free . '/' . $meminfo['MemTotal'] . ' - limit ' . $min_memory, Core\Logger::DEBUG);
658                 }
659
660                 return $reached;
661         }
662
663         /**
664          * @brief Checks if the maximum load is reached
665          *
666          * @return bool Is the load reached?
667          * @throws HTTPException\InternalServerErrorException
668          */
669         public function isMaxLoadReached()
670         {
671                 if ($this->isBackend()) {
672                         $process = 'backend';
673                         $maxsysload = intval($this->config->get('system', 'maxloadavg'));
674                         if ($maxsysload < 1) {
675                                 $maxsysload = 50;
676                         }
677                 } else {
678                         $process = 'frontend';
679                         $maxsysload = intval($this->config->get('system', 'maxloadavg_frontend'));
680                         if ($maxsysload < 1) {
681                                 $maxsysload = 50;
682                         }
683                 }
684
685                 $load = Core\System::currentLoad();
686                 if ($load) {
687                         if (intval($load) > $maxsysload) {
688                                 Core\Logger::log('system: load ' . $load . ' for ' . $process . ' tasks (' . $maxsysload . ') too high.');
689                                 return true;
690                         }
691                 }
692                 return false;
693         }
694
695         /**
696          * Executes a child process with 'proc_open'
697          *
698          * @param string $command The command to execute
699          * @param array  $args    Arguments to pass to the command ( [ 'key' => value, 'key2' => value2, ... ]
700          * @throws HTTPException\InternalServerErrorException
701          */
702         public function proc_run($command, $args)
703         {
704                 if (!function_exists('proc_open')) {
705                         return;
706                 }
707
708                 $cmdline = $this->config->get('config', 'php_path', 'php') . ' ' . escapeshellarg($command);
709
710                 foreach ($args as $key => $value) {
711                         if (!is_null($value) && is_bool($value) && !$value) {
712                                 continue;
713                         }
714
715                         $cmdline .= ' --' . $key;
716                         if (!is_null($value) && !is_bool($value)) {
717                                 $cmdline .= ' ' . $value;
718                         }
719                 }
720
721                 if ($this->isMinMemoryReached()) {
722                         return;
723                 }
724
725                 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
726                         $resource = proc_open('cmd /c start /b ' . $cmdline, [], $foo, $this->getBasePath());
727                 } else {
728                         $resource = proc_open($cmdline . ' &', [], $foo, $this->getBasePath());
729                 }
730                 if (!is_resource($resource)) {
731                         Core\Logger::log('We got no resource for command ' . $cmdline, Core\Logger::DEBUG);
732                         return;
733                 }
734                 proc_close($resource);
735         }
736
737         /**
738          * Generates the site's default sender email address
739          *
740          * @return string
741          * @throws HTTPException\InternalServerErrorException
742          */
743         public function getSenderEmailAddress()
744         {
745                 $sender_email = $this->config->get('config', 'sender_email');
746                 if (empty($sender_email)) {
747                         $hostname = $this->baseURL->getHostname();
748                         if (strpos($hostname, ':')) {
749                                 $hostname = substr($hostname, 0, strpos($hostname, ':'));
750                         }
751
752                         $sender_email = 'noreply@' . $hostname;
753                 }
754
755                 return $sender_email;
756         }
757
758         /**
759          * Returns the current theme name.
760          *
761          * @return string the name of the current theme
762          * @throws HTTPException\InternalServerErrorException
763          */
764         public function getCurrentTheme()
765         {
766                 if ($this->getMode()->isInstall()) {
767                         return '';
768                 }
769
770                 if (!$this->currentTheme) {
771                         $this->computeCurrentTheme();
772                 }
773
774                 return $this->currentTheme;
775         }
776
777         public function setCurrentTheme($theme)
778         {
779                 $this->currentTheme = $theme;
780         }
781
782         /**
783          * Computes the current theme name based on the node settings, the user settings and the device type
784          *
785          * @throws Exception
786          */
787         private function computeCurrentTheme()
788         {
789                 $system_theme = $this->config->get('system', 'theme');
790                 if (!$system_theme) {
791                         throw new Exception($this->l10n->t('No system theme config value set.'));
792                 }
793
794                 // Sane default
795                 $this->currentTheme = $system_theme;
796
797                 $page_theme = null;
798                 // Find the theme that belongs to the user whose stuff we are looking at
799                 if ($this->profile_uid && ($this->profile_uid != local_user())) {
800                         // Allow folks to override user themes and always use their own on their own site.
801                         // This works only if the user is on the same server
802                         $user = DBA::selectFirst('user', ['theme'], ['uid' => $this->profile_uid]);
803                         if (DBA::isResult($user) && !Core\PConfig::get(local_user(), 'system', 'always_my_theme')) {
804                                 $page_theme = $user['theme'];
805                         }
806                 }
807
808                 $user_theme = Core\Session::get('theme', $system_theme);
809
810                 // Specific mobile theme override
811                 if (($this->is_mobile || $this->is_tablet) && Core\Session::get('show-mobile', true)) {
812                         $system_mobile_theme = $this->config->get('system', 'mobile-theme');
813                         $user_mobile_theme = Core\Session::get('mobile-theme', $system_mobile_theme);
814
815                         // --- means same mobile theme as desktop
816                         if (!empty($user_mobile_theme) && $user_mobile_theme !== '---') {
817                                 $user_theme = $user_mobile_theme;
818                         }
819                 }
820
821                 if ($page_theme) {
822                         $theme_name = $page_theme;
823                 } else {
824                         $theme_name = $user_theme;
825                 }
826
827                 $theme_name = Strings::sanitizeFilePathItem($theme_name);
828                 if ($theme_name
829                         && in_array($theme_name, Theme::getAllowedList())
830                         && (file_exists('view/theme/' . $theme_name . '/style.css')
831                         || file_exists('view/theme/' . $theme_name . '/style.php'))
832                 ) {
833                         $this->currentTheme = $theme_name;
834                 }
835         }
836
837         /**
838          * @brief Return full URL to theme which is currently in effect.
839          *
840          * Provide a sane default if nothing is chosen or the specified theme does not exist.
841          *
842          * @return string
843          * @throws HTTPException\InternalServerErrorException
844          */
845         public function getCurrentThemeStylesheetPath()
846         {
847                 return Core\Theme::getStylesheetPath($this->getCurrentTheme());
848         }
849
850         /**
851          * Check if request was an AJAX (xmlhttprequest) request.
852          *
853          * @return boolean true if it was an AJAX request
854          */
855         public function isAjax()
856         {
857                 return $this->isAjax;
858         }
859
860         /**
861          * @deprecated use Arguments->get() instead
862          *
863          * @see App\Arguments
864          */
865         public function getArgumentValue($position, $default = '')
866         {
867                 return $this->args->get($position, $default);
868         }
869
870         /**
871          * Sets the base url for use in cmdline programs which don't have
872          * $_SERVER variables
873          */
874         public function checkURL()
875         {
876                 $url = $this->config->get('system', 'url');
877
878                 // if the url isn't set or the stored url is radically different
879                 // than the currently visited url, store the current value accordingly.
880                 // "Radically different" ignores common variations such as http vs https
881                 // and www.example.com vs example.com.
882                 // We will only change the url to an ip address if there is no existing setting
883
884                 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()))) {
885                         $this->config->set('system', 'url', $this->getBaseURL());
886                 }
887         }
888
889         /**
890          * Frontend App script
891          *
892          * The App object behaves like a container and a dispatcher at the same time, including a representation of the
893          * request and a representation of the response.
894          *
895          * This probably should change to limit the size of this monster method.
896          *
897          * @param App\Module $module The determined module
898          */
899         public function runFrontend(App\Module $module, App\Router $router, PConfiguration $pconfig)
900         {
901                 $moduleName = $module->getName();
902
903                 try {
904                         // Missing DB connection: ERROR
905                         if ($this->getMode()->has(App\Mode::LOCALCONFIGPRESENT) && !$this->getMode()->has(App\Mode::DBAVAILABLE)) {
906                                 throw new HTTPException\InternalServerErrorException('Apologies but the website is unavailable at the moment.');
907                         }
908
909                         // Max Load Average reached: ERROR
910                         if ($this->isMaxProcessesReached() || $this->isMaxLoadReached()) {
911                                 header('Retry-After: 120');
912                                 header('Refresh: 120; url=' . $this->baseURL->get() . "/" . $this->args->getQueryString());
913
914                                 throw new HTTPException\ServiceUnavailableException('The node is currently overloaded. Please try again later.');
915                         }
916
917                         if (!$this->getMode()->isInstall()) {
918                                 // Force SSL redirection
919                                 if ($this->baseURL->checkRedirectHttps()) {
920                                         System::externalRedirect($this->baseURL->get() . '/' . $this->args->getQueryString());
921                                 }
922
923                                 Core\Session::init();
924                                 Core\Hook::callAll('init_1');
925                         }
926
927                         // Exclude the backend processes from the session management
928                         if (!$this->isBackend()) {
929                                 $stamp1 = microtime(true);
930                                 session_start();
931                                 $this->profiler->saveTimestamp($stamp1, 'parser', Core\System::callstack());
932                                 $this->l10n->setSessionVariable();
933                                 $this->l10n->setLangFromSession();
934                         } else {
935                                 $_SESSION = [];
936                                 Core\Worker::executeIfIdle();
937                         }
938
939                         if ($this->getMode()->isNormal()) {
940                                 $requester = HTTPSignature::getSigner('', $_SERVER);
941                                 if (!empty($requester)) {
942                                         Profile::addVisitorCookieForHandle($requester);
943                                 }
944                         }
945
946                         // ZRL
947                         if (!empty($_GET['zrl']) && $this->getMode()->isNormal()) {
948                                 if (!local_user()) {
949                                         // Only continue when the given profile link seems valid
950                                         // Valid profile links contain a path with "/profile/" and no query parameters
951                                         if ((parse_url($_GET['zrl'], PHP_URL_QUERY) == "") &&
952                                                 strstr(parse_url($_GET['zrl'], PHP_URL_PATH), "/profile/")) {
953                                                 if (Core\Session::get('visitor_home') != $_GET["zrl"]) {
954                                                         Core\Session::set('my_url', $_GET['zrl']);
955                                                         Core\Session::set('authenticated', 0);
956                                                 }
957
958                                                 Model\Profile::zrlInit($this);
959                                         } else {
960                                                 // Someone came with an invalid parameter, maybe as a DDoS attempt
961                                                 // We simply stop processing here
962                                                 Core\Logger::log("Invalid ZRL parameter " . $_GET['zrl'], Core\Logger::DEBUG);
963                                                 throw new HTTPException\ForbiddenException();
964                                         }
965                                 }
966                         }
967
968                         if (!empty($_GET['owt']) && $this->getMode()->isNormal()) {
969                                 $token = $_GET['owt'];
970                                 Model\Profile::openWebAuthInit($token);
971                         }
972
973                         Login::sessionAuth();
974
975                         if (empty($_SESSION['authenticated'])) {
976                                 header('X-Account-Management-Status: none');
977                         }
978
979                         $_SESSION['sysmsg']       = Core\Session::get('sysmsg', []);
980                         $_SESSION['sysmsg_info']  = Core\Session::get('sysmsg_info', []);
981                         $_SESSION['last_updated'] = Core\Session::get('last_updated', []);
982
983                         /*
984                          * check_config() is responsible for running update scripts. These automatically
985                          * update the DB schema whenever we push a new one out. It also checks to see if
986                          * any addons have been added or removed and reacts accordingly.
987                          */
988
989                         // in install mode, any url loads install module
990                         // but we need "view" module for stylesheet
991                         if ($this->getMode()->isInstall() && $moduleName !== 'install') {
992                                 $this->internalRedirect('install');
993                         } elseif (!$this->getMode()->isInstall() && !$this->getMode()->has(App\Mode::MAINTENANCEDISABLED) && $moduleName !== 'maintenance') {
994                                 $this->internalRedirect('maintenance');
995                         } else {
996                                 $this->checkURL();
997                                 Core\Update::check($this->getBasePath(), false, $this->getMode());
998                                 Core\Addon::loadAddons();
999                                 Core\Hook::loadHooks();
1000                         }
1001
1002                         $this->page = [
1003                                 'aside' => '',
1004                                 'bottom' => '',
1005                                 'content' => '',
1006                                 'footer' => '',
1007                                 'htmlhead' => '',
1008                                 'nav' => '',
1009                                 'page_title' => '',
1010                                 'right_aside' => '',
1011                                 'template' => '',
1012                                 'title' => ''
1013                         ];
1014
1015                         // Compatibility with the Android Diaspora client
1016                         if ($moduleName == 'stream') {
1017                                 $this->internalRedirect('network?order=post');
1018                         }
1019
1020                         if ($moduleName == 'conversations') {
1021                                 $this->internalRedirect('message');
1022                         }
1023
1024                         if ($moduleName == 'commented') {
1025                                 $this->internalRedirect('network?order=comment');
1026                         }
1027
1028                         if ($moduleName == 'liked') {
1029                                 $this->internalRedirect('network?order=comment');
1030                         }
1031
1032                         if ($moduleName == 'activity') {
1033                                 $this->internalRedirect('network?conv=1');
1034                         }
1035
1036                         if (($moduleName == 'status_messages') && ($this->args->getCommand() == 'status_messages/new')) {
1037                                 $this->internalRedirect('bookmarklet');
1038                         }
1039
1040                         if (($moduleName == 'user') && ($this->args->getCommand() == 'user/edit')) {
1041                                 $this->internalRedirect('settings');
1042                         }
1043
1044                         if (($moduleName == 'tag_followings') && ($this->args->getCommand() == 'tag_followings/manage')) {
1045                                 $this->internalRedirect('search');
1046                         }
1047
1048                         // Initialize module that can set the current theme in the init() method, either directly or via App->profile_uid
1049                         $this->page['page_title'] = $moduleName;
1050
1051                         // determine the module class and save it to the module instance
1052                         // @todo there's an implicit dependency due SESSION::start(), so it has to be called here (yet)
1053                         $module = $module->determineClass($this->args, $router, $this->config);
1054
1055                         // Let the module run it's internal process (init, get, post, ...)
1056                         $module->run($this->l10n, $this, $this->logger, $this->getCurrentTheme(), $_SERVER, $_POST);
1057
1058                 } catch(HTTPException $e) {
1059                         ModuleHTTPException::rawContent($e);
1060                 }
1061
1062                 $content = '';
1063
1064                 try {
1065                         $moduleClass = $module->getClassName();
1066
1067                         $arr = ['content' => $content];
1068                         Core\Hook::callAll($moduleClass . '_mod_content', $arr);
1069                         $content = $arr['content'];
1070                         $arr = ['content' => call_user_func([$moduleClass, 'content'])];
1071                         Core\Hook::callAll($moduleClass . '_mod_aftercontent', $arr);
1072                         $content .= $arr['content'];
1073                 } catch(HTTPException $e) {
1074                         $content = ModuleHTTPException::content($e);
1075                 }
1076
1077                 // initialise content region
1078                 if ($this->getMode()->isNormal()) {
1079                         Core\Hook::callAll('page_content_top', $this->page['content']);
1080                 }
1081
1082                 $this->page['content'] .= $content;
1083
1084                 /* Create the page head after setting the language
1085                  * and getting any auth credentials.
1086                  *
1087                  * Moved initHead() and initFooter() to after
1088                  * all the module functions have executed so that all
1089                  * theme choices made by the modules can take effect.
1090                  */
1091                 $this->initHead($module, $pconfig);
1092
1093                 /* Build the page ending -- this is stuff that goes right before
1094                  * the closing </body> tag
1095                  */
1096                 $this->initFooter();
1097
1098                 if (!$this->isAjax()) {
1099                         Core\Hook::callAll('page_end', $this->page['content']);
1100                 }
1101
1102                 // Add the navigation (menu) template
1103                 if ($moduleName != 'install' && $moduleName != 'maintenance') {
1104                         $this->page['htmlhead'] .= Core\Renderer::replaceMacros(Core\Renderer::getMarkupTemplate('nav_head.tpl'), []);
1105                         $this->page['nav']       = Content\Nav::build($this);
1106                 }
1107
1108                 // Build the page - now that we have all the components
1109                 if (isset($_GET["mode"]) && (($_GET["mode"] == "raw") || ($_GET["mode"] == "minimal"))) {
1110                         $doc = new DOMDocument();
1111
1112                         $target = new DOMDocument();
1113                         $target->loadXML("<root></root>");
1114
1115                         $content = mb_convert_encoding($this->page["content"], 'HTML-ENTITIES', "UTF-8");
1116
1117                         /// @TODO one day, kill those error-surpressing @ stuff, or PHP should ban it
1118                         @$doc->loadHTML($content);
1119
1120                         $xpath = new DOMXPath($doc);
1121
1122                         $list = $xpath->query("//*[contains(@id,'tread-wrapper-')]");  /* */
1123
1124                         foreach ($list as $item) {
1125                                 $item = $target->importNode($item, true);
1126
1127                                 // And then append it to the target
1128                                 $target->documentElement->appendChild($item);
1129                         }
1130
1131                         if ($_GET["mode"] == "raw") {
1132                                 header("Content-type: text/html; charset=utf-8");
1133
1134                                 echo substr($target->saveHTML(), 6, -8);
1135
1136                                 exit();
1137                         }
1138                 }
1139
1140                 $page    = $this->page;
1141                 $profile = $this->profile;
1142
1143                 header("X-Friendica-Version: " . FRIENDICA_VERSION);
1144                 header("Content-type: text/html; charset=utf-8");
1145
1146                 if ($this->config->get('system', 'hsts') && ($this->baseURL->getSSLPolicy() == BaseUrl::SSL_POLICY_FULL)) {
1147                         header("Strict-Transport-Security: max-age=31536000");
1148                 }
1149
1150                 // Some security stuff
1151                 header('X-Content-Type-Options: nosniff');
1152                 header('X-XSS-Protection: 1; mode=block');
1153                 header('X-Permitted-Cross-Domain-Policies: none');
1154                 header('X-Frame-Options: sameorigin');
1155
1156                 // Things like embedded OSM maps don't work, when this is enabled
1157                 // 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'");
1158
1159                 /* We use $_GET["mode"] for special page templates. So we will check if we have
1160                  * to load another page template than the default one.
1161                  * The page templates are located in /view/php/ or in the theme directory.
1162                  */
1163                 if (isset($_GET["mode"])) {
1164                         $template = Core\Theme::getPathForFile($_GET["mode"] . '.php');
1165                 }
1166
1167                 // If there is no page template use the default page template
1168                 if (empty($template)) {
1169                         $template = Core\Theme::getPathForFile("default.php");
1170                 }
1171
1172                 // Theme templates expect $a as an App instance
1173                 $a = $this;
1174
1175                 // Used as is in view/php/default.php
1176                 $lang = $this->l10n->getCurrentLang();
1177
1178                 /// @TODO Looks unsafe (remote-inclusion), is maybe not but Core\Theme::getPathForFile() uses file_exists() but does not escape anything
1179                 require_once $template;
1180         }
1181
1182         /**
1183          * Redirects to another module relative to the current Friendica base.
1184          * If you want to redirect to a external URL, use System::externalRedirectTo()
1185          *
1186          * @param string $toUrl The destination URL (Default is empty, which is the default page of the Friendica node)
1187          * @param bool $ssl if true, base URL will try to get called with https:// (works just for relative paths)
1188          *
1189          * @throws HTTPException\InternalServerErrorException In Case the given URL is not relative to the Friendica node
1190          */
1191         public function internalRedirect($toUrl = '', $ssl = false)
1192         {
1193                 if (!empty(parse_url($toUrl, PHP_URL_SCHEME))) {
1194                         throw new HTTPException\InternalServerErrorException("'$toUrl is not a relative path, please use System::externalRedirectTo");
1195                 }
1196
1197                 $redirectTo = $this->getBaseURL($ssl) . '/' . ltrim($toUrl, '/');
1198                 Core\System::externalRedirect($redirectTo);
1199         }
1200
1201         /**
1202          * Automatically redirects to relative or absolute URL
1203          * Should only be used if it isn't clear if the URL is either internal or external
1204          *
1205          * @param string $toUrl The target URL
1206          * @throws HTTPException\InternalServerErrorException
1207          */
1208         public function redirect($toUrl)
1209         {
1210                 if (!empty(parse_url($toUrl, PHP_URL_SCHEME))) {
1211                         Core\System::externalRedirect($toUrl);
1212                 } else {
1213                         $this->internalRedirect($toUrl);
1214                 }
1215         }
1216 }