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