]> git.mxchange.org Git - friendica.git/blob - src/App.php
2b229bdf421f9696eade92dc6eef50b53906e0d1
[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->load();
289         }
290
291         /**
292          * Load the whole app instance
293          */
294         public function load()
295         {
296                 set_time_limit(0);
297
298                 // This has to be quite large to deal with embedded private photos
299                 ini_set('pcre.backtrack_limit', 500000);
300
301                 set_include_path(
302                         get_include_path() . PATH_SEPARATOR
303                         . $this->getBasePath() . DIRECTORY_SEPARATOR . 'include' . PATH_SEPARATOR
304                         . $this->getBasePath() . DIRECTORY_SEPARATOR . 'library' . PATH_SEPARATOR
305                         . $this->getBasePath());
306
307                 $this->profiler->reset();
308
309                 if ($this->mode->has(App\Mode::DBAVAILABLE)) {
310                         $this->profiler->update($this->config);
311
312                         Core\Hook::loadHooks();
313                         $loader = new ConfigFileLoader($this->getBasePath());
314                         Core\Hook::callAll('load_config', $loader);
315                 }
316
317                 $this->loadDefaultTimezone();
318                 // Register template engines
319                 Core\Renderer::registerTemplateEngine('Friendica\Render\FriendicaSmartyEngine');
320         }
321
322         /**
323          * Loads the default timezone
324          *
325          * Include support for legacy $default_timezone
326          *
327          * @global string $default_timezone
328          */
329         private function loadDefaultTimezone()
330         {
331                 if ($this->config->get('system', 'default_timezone')) {
332                         $this->timezone = $this->config->get('system', 'default_timezone');
333                 } else {
334                         global $default_timezone;
335                         $this->timezone = !empty($default_timezone) ? $default_timezone : 'UTC';
336                 }
337
338                 if ($this->timezone) {
339                         date_default_timezone_set($this->timezone);
340                 }
341         }
342
343         /**
344          * Returns the scheme of the current call
345          *
346          * @return string
347          *
348          * @deprecated 2019.06 - use BaseURL->getScheme() instead
349          */
350         public function getScheme()
351         {
352                 return $this->baseURL->getScheme();
353         }
354
355         /**
356          * Retrieves the Friendica instance base URL
357          *
358          * @param bool $ssl Whether to append http or https under BaseURL::SSL_POLICY_SELFSIGN
359          *
360          * @return string Friendica server base URL
361          *
362          * @deprecated 2019.09 - use BaseUrl->get($ssl) instead
363          */
364         public function getBaseURL($ssl = false)
365         {
366                 return $this->baseURL->get($ssl);
367         }
368
369         /**
370          * @brief      Initializes the baseurl components
371          *
372          * Clears the baseurl cache to prevent inconsistencies
373          *
374          * @param string $url
375          *
376          * @deprecated 2019.06 - use BaseURL->saveByURL($url) instead
377          */
378         public function setBaseURL($url)
379         {
380                 $this->baseURL->saveByURL($url);
381         }
382
383         /**
384          * Returns the current hostname
385          *
386          * @return string
387          *
388          * @deprecated 2019.06 - use BaseURL->getHostname() instead
389          */
390         public function getHostName()
391         {
392                 return $this->baseURL->getHostname();
393         }
394
395         /**
396          * Returns the sub-path of the full URL
397          *
398          * @return string
399          *
400          * @deprecated 2019.06 - use BaseURL->getUrlPath() instead
401          */
402         public function getURLPath()
403         {
404                 return $this->baseURL->getUrlPath();
405         }
406
407         /**
408          * Initializes App->page['htmlhead'].
409          *
410          * Includes:
411          * - Page title
412          * - Favicons
413          * - Registered stylesheets (through App->registerStylesheet())
414          * - Infinite scroll data
415          * - head.tpl template
416          */
417         private function initHead(App\Module $module, PConfiguration $pconfig)
418         {
419                 $interval = ((local_user()) ? $pconfig->get(local_user(), 'system', 'update_interval') : 40000);
420
421                 // If the update is 'deactivated' set it to the highest integer number (~24 days)
422                 if ($interval < 0) {
423                         $interval = 2147483647;
424                 }
425
426                 if ($interval < 10000) {
427                         $interval = 40000;
428                 }
429
430                 // Default title: current module called
431                 if (empty($this->page['title']) && $module->getName()) {
432                         $this->page['title'] = ucfirst($module->getName());
433                 }
434
435                 // Prepend the sitename to the page title
436                 $this->page['title'] = $this->config->get('config', 'sitename', '') . (!empty($this->page['title']) ? ' | ' . $this->page['title'] : '');
437
438                 if (!empty(Core\Renderer::$theme['stylesheet'])) {
439                         $stylesheet = Core\Renderer::$theme['stylesheet'];
440                 } else {
441                         $stylesheet = $this->getCurrentThemeStylesheetPath();
442                 }
443
444                 $this->registerStylesheet($stylesheet);
445
446                 $shortcut_icon = $this->config->get('system', 'shortcut_icon');
447                 if ($shortcut_icon == '') {
448                         $shortcut_icon = 'images/friendica-32.png';
449                 }
450
451                 $touch_icon = $this->config->get('system', 'touch_icon');
452                 if ($touch_icon == '') {
453                         $touch_icon = 'images/friendica-128.png';
454                 }
455
456                 Core\Hook::callAll('head', $this->page['htmlhead']);
457
458                 $tpl = Core\Renderer::getMarkupTemplate('head.tpl');
459                 /* put the head template at the beginning of page['htmlhead']
460                  * since the code added by the modules frequently depends on it
461                  * being first
462                  */
463                 $this->page['htmlhead'] = Core\Renderer::replaceMacros($tpl, [
464                                 '$local_user'      => local_user(),
465                                 '$generator'       => 'Friendica' . ' ' . FRIENDICA_VERSION,
466                                 '$delitem'         => $this->l10n->t('Delete this item?'),
467                                 '$update_interval' => $interval,
468                                 '$shortcut_icon'   => $shortcut_icon,
469                                 '$touch_icon'      => $touch_icon,
470                                 '$block_public'    => intval($this->config->get('system', 'block_public')),
471                                 '$stylesheets'     => $this->stylesheets,
472                         ]) . $this->page['htmlhead'];
473         }
474
475         /**
476          * Initializes App->page['footer'].
477          *
478          * Includes:
479          * - Javascript homebase
480          * - Mobile toggle link
481          * - Registered footer scripts (through App->registerFooterScript())
482          * - footer.tpl template
483          */
484         private function initFooter()
485         {
486                 // If you're just visiting, let javascript take you home
487                 if (!empty($_SESSION['visitor_home'])) {
488                         $homebase = $_SESSION['visitor_home'];
489                 } elseif (local_user()) {
490                         $homebase = 'profile/' . $this->user['nickname'];
491                 }
492
493                 if (isset($homebase)) {
494                         $this->page['footer'] .= '<script>var homebase="' . $homebase . '";</script>' . "\n";
495                 }
496
497                 /*
498                  * Add a "toggle mobile" link if we're using a mobile device
499                  */
500                 if ($this->is_mobile || $this->is_tablet) {
501                         if (isset($_SESSION['show-mobile']) && !$_SESSION['show-mobile']) {
502                                 $link = 'toggle_mobile?address=' . urlencode(curPageURL());
503                         } else {
504                                 $link = 'toggle_mobile?off=1&address=' . urlencode(curPageURL());
505                         }
506                         $this->page['footer'] .= Core\Renderer::replaceMacros(Core\Renderer::getMarkupTemplate("toggle_mobile_footer.tpl"), [
507                                 '$toggle_link' => $link,
508                                 '$toggle_text' => $this->l10n->t('toggle mobile')
509                         ]);
510                 }
511
512                 Core\Hook::callAll('footer', $this->page['footer']);
513
514                 $tpl                  = Core\Renderer::getMarkupTemplate('footer.tpl');
515                 $this->page['footer'] = Core\Renderer::replaceMacros($tpl, [
516                                 '$footerScripts' => $this->footerScripts,
517                         ]) . $this->page['footer'];
518         }
519
520         /**
521          * @brief Removes the base url from an url. This avoids some mixed content problems.
522          *
523          * @param string $origURL
524          *
525          * @return string The cleaned url
526          * @throws HTTPException\InternalServerErrorException
527          */
528         public function removeBaseURL($origURL)
529         {
530                 // Remove the hostname from the url if it is an internal link
531                 $nurl = Util\Strings::normaliseLink($origURL);
532                 $base = Util\Strings::normaliseLink($this->getBaseURL());
533                 $url  = str_replace($base . '/', '', $nurl);
534
535                 // if it is an external link return the orignal value
536                 if ($url == Util\Strings::normaliseLink($origURL)) {
537                         return $origURL;
538                 } else {
539                         return $url;
540                 }
541         }
542
543         /**
544          * Returns the current UserAgent as a String
545          *
546          * @return string the UserAgent as a String
547          * @throws HTTPException\InternalServerErrorException
548          */
549         public function getUserAgent()
550         {
551                 return
552                         FRIENDICA_PLATFORM . " '" .
553                         FRIENDICA_CODENAME . "' " .
554                         FRIENDICA_VERSION . '-' .
555                         DB_UPDATE_VERSION . '; ' .
556                         $this->getBaseURL();
557         }
558
559         /**
560          * Returns true, if the call is from a backend node (f.e. from a worker)
561          *
562          * @return bool Is it a known backend?
563          *
564          * @deprecated 2019.09 - use App\Mode->isBackend() instead
565          * @see App\Mode::isBackend()
566          * Use BaseObject::getClass(App\Mode::class) to get the global instance of Mode
567          */
568         public function isBackend()
569         {
570                 return $this->mode->isBackend();
571         }
572
573         /**
574          * @brief Checks if the maximum number of database processes is reached
575          *
576          * @return bool Is the limit reached?
577          */
578         public function isMaxProcessesReached()
579         {
580                 // Deactivated, needs more investigating if this check really makes sense
581                 return false;
582
583                 /*
584                  * Commented out to suppress static analyzer issues
585                  *
586                 if ($this->is_backend()) {
587                         $process = 'backend';
588                         $max_processes = $this->config->get('system', 'max_processes_backend');
589                         if (intval($max_processes) == 0) {
590                                 $max_processes = 5;
591                         }
592                 } else {
593                         $process = 'frontend';
594                         $max_processes = $this->config->get('system', 'max_processes_frontend');
595                         if (intval($max_processes) == 0) {
596                                 $max_processes = 20;
597                         }
598                 }
599
600                 $processlist = DBA::processlist();
601                 if ($processlist['list'] != '') {
602                         Core\Logger::log('Processcheck: Processes: ' . $processlist['amount'] . ' - Processlist: ' . $processlist['list'], Core\Logger::DEBUG);
603
604                         if ($processlist['amount'] > $max_processes) {
605                                 Core\Logger::log('Processcheck: Maximum number of processes for ' . $process . ' tasks (' . $max_processes . ') reached.', Core\Logger::DEBUG);
606                                 return true;
607                         }
608                 }
609                 return false;
610                  */
611         }
612
613         /**
614          * @brief Checks if the minimal memory is reached
615          *
616          * @return bool Is the memory limit reached?
617          * @throws HTTPException\InternalServerErrorException
618          */
619         public function isMinMemoryReached()
620         {
621                 $min_memory = $this->config->get('system', 'min_memory', 0);
622                 if ($min_memory == 0) {
623                         return false;
624                 }
625
626                 if (!is_readable('/proc/meminfo')) {
627                         return false;
628                 }
629
630                 $memdata = explode("\n", file_get_contents('/proc/meminfo'));
631
632                 $meminfo = [];
633                 foreach ($memdata as $line) {
634                         $data = explode(':', $line);
635                         if (count($data) != 2) {
636                                 continue;
637                         }
638                         list($key, $val) = $data;
639                         $meminfo[$key] = (int)trim(str_replace('kB', '', $val));
640                         $meminfo[$key] = (int)($meminfo[$key] / 1024);
641                 }
642
643                 if (!isset($meminfo['MemFree'])) {
644                         return false;
645                 }
646
647                 $free = $meminfo['MemFree'];
648
649                 $reached = ($free < $min_memory);
650
651                 if ($reached) {
652                         Core\Logger::log('Minimal memory reached: ' . $free . '/' . $meminfo['MemTotal'] . ' - limit ' . $min_memory, Core\Logger::DEBUG);
653                 }
654
655                 return $reached;
656         }
657
658         /**
659          * @brief Checks if the maximum load is reached
660          *
661          * @return bool Is the load reached?
662          * @throws HTTPException\InternalServerErrorException
663          */
664         public function isMaxLoadReached()
665         {
666                 if ($this->mode->isBackend()) {
667                         $process    = 'backend';
668                         $maxsysload = intval($this->config->get('system', 'maxloadavg'));
669                         if ($maxsysload < 1) {
670                                 $maxsysload = 50;
671                         }
672                 } else {
673                         $process    = 'frontend';
674                         $maxsysload = intval($this->config->get('system', 'maxloadavg_frontend'));
675                         if ($maxsysload < 1) {
676                                 $maxsysload = 50;
677                         }
678                 }
679
680                 $load = Core\System::currentLoad();
681                 if ($load) {
682                         if (intval($load) > $maxsysload) {
683                                 $this->logger->info('system load for process too high.', ['load' => $load, 'process' => $process, 'maxsysload' => $maxsysload]);
684                                 return true;
685                         }
686                 }
687                 return false;
688         }
689
690         /**
691          * Executes a child process with 'proc_open'
692          *
693          * @param string $command The command to execute
694          * @param array  $args    Arguments to pass to the command ( [ 'key' => value, 'key2' => value2, ... ]
695          *
696          * @throws HTTPException\InternalServerErrorException
697          */
698         public function proc_run($command, $args)
699         {
700                 if (!function_exists('proc_open')) {
701                         return;
702                 }
703
704                 $cmdline = $this->config->get('config', 'php_path', 'php') . ' ' . escapeshellarg($command);
705
706                 foreach ($args as $key => $value) {
707                         if (!is_null($value) && is_bool($value) && !$value) {
708                                 continue;
709                         }
710
711                         $cmdline .= ' --' . $key;
712                         if (!is_null($value) && !is_bool($value)) {
713                                 $cmdline .= ' ' . $value;
714                         }
715                 }
716
717                 if ($this->isMinMemoryReached()) {
718                         return;
719                 }
720
721                 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
722                         $resource = proc_open('cmd /c start /b ' . $cmdline, [], $foo, $this->getBasePath());
723                 } else {
724                         $resource = proc_open($cmdline . ' &', [], $foo, $this->getBasePath());
725                 }
726                 if (!is_resource($resource)) {
727                         $this->logger->debug('We got no resource for command.', ['cmd' => $cmdline]);
728                         return;
729                 }
730                 proc_close($resource);
731         }
732
733         /**
734          * Generates the site's default sender email address
735          *
736          * @return string
737          * @throws HTTPException\InternalServerErrorException
738          */
739         public function getSenderEmailAddress()
740         {
741                 $sender_email = $this->config->get('config', 'sender_email');
742                 if (empty($sender_email)) {
743                         $hostname = $this->baseURL->getHostname();
744                         if (strpos($hostname, ':')) {
745                                 $hostname = substr($hostname, 0, strpos($hostname, ':'));
746                         }
747
748                         $sender_email = 'noreply@' . $hostname;
749                 }
750
751                 return $sender_email;
752         }
753
754         /**
755          * Returns the current theme name.
756          *
757          * @return string the name of the current theme
758          * @throws HTTPException\InternalServerErrorException
759          */
760         public function getCurrentTheme()
761         {
762                 if ($this->mode->isInstall()) {
763                         return '';
764                 }
765
766                 if (!$this->currentTheme) {
767                         $this->computeCurrentTheme();
768                 }
769
770                 return $this->currentTheme;
771         }
772
773         public function setCurrentTheme($theme)
774         {
775                 $this->currentTheme = $theme;
776         }
777
778         /**
779          * Computes the current theme name based on the node settings, the user settings and the device type
780          *
781          * @throws Exception
782          */
783         private function computeCurrentTheme()
784         {
785                 $system_theme = $this->config->get('system', 'theme');
786                 if (!$system_theme) {
787                         throw new Exception($this->l10n->t('No system theme config value set.'));
788                 }
789
790                 // Sane default
791                 $this->currentTheme = $system_theme;
792
793                 $page_theme = null;
794                 // Find the theme that belongs to the user whose stuff we are looking at
795                 if ($this->profile_uid && ($this->profile_uid != local_user())) {
796                         // Allow folks to override user themes and always use their own on their own site.
797                         // This works only if the user is on the same server
798                         $user = $this->database->selectFirst('user', ['theme'], ['uid' => $this->profile_uid]);
799                         if ($this->database->isResult($user) && !Core\PConfig::get(local_user(), 'system', 'always_my_theme')) {
800                                 $page_theme = $user['theme'];
801                         }
802                 }
803
804                 $user_theme = Core\Session::get('theme', $system_theme);
805
806                 // Specific mobile theme override
807                 if (($this->is_mobile || $this->is_tablet) && Core\Session::get('show-mobile', true)) {
808                         $system_mobile_theme = $this->config->get('system', 'mobile-theme');
809                         $user_mobile_theme   = Core\Session::get('mobile-theme', $system_mobile_theme);
810
811                         // --- means same mobile theme as desktop
812                         if (!empty($user_mobile_theme) && $user_mobile_theme !== '---') {
813                                 $user_theme = $user_mobile_theme;
814                         }
815                 }
816
817                 if ($page_theme) {
818                         $theme_name = $page_theme;
819                 } else {
820                         $theme_name = $user_theme;
821                 }
822
823                 $theme_name = Strings::sanitizeFilePathItem($theme_name);
824                 if ($theme_name
825                     && in_array($theme_name, Theme::getAllowedList())
826                     && (file_exists('view/theme/' . $theme_name . '/style.css')
827                         || file_exists('view/theme/' . $theme_name . '/style.php'))
828                 ) {
829                         $this->currentTheme = $theme_name;
830                 }
831         }
832
833         /**
834          * @brief Return full URL to theme which is currently in effect.
835          *
836          * Provide a sane default if nothing is chosen or the specified theme does not exist.
837          *
838          * @return string
839          * @throws HTTPException\InternalServerErrorException
840          */
841         public function getCurrentThemeStylesheetPath()
842         {
843                 return Core\Theme::getStylesheetPath($this->getCurrentTheme());
844         }
845
846         /**
847          * Check if request was an AJAX (xmlhttprequest) request.
848          *
849          * @return boolean true if it was an AJAX request
850          */
851         public function isAjax()
852         {
853                 return $this->isAjax;
854         }
855
856         /**
857          * @deprecated use Arguments->get() instead
858          *
859          * @see        App\Arguments
860          */
861         public function getArgumentValue($position, $default = '')
862         {
863                 return $this->args->get($position, $default);
864         }
865
866         /**
867          * Sets the base url for use in cmdline programs which don't have
868          * $_SERVER variables
869          */
870         public function checkURL()
871         {
872                 $url = $this->config->get('system', 'url');
873
874                 // if the url isn't set or the stored url is radically different
875                 // than the currently visited url, store the current value accordingly.
876                 // "Radically different" ignores common variations such as http vs https
877                 // and www.example.com vs example.com.
878                 // We will only change the url to an ip address if there is no existing setting
879
880                 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()))) {
881                         $this->config->set('system', 'url', $this->getBaseURL());
882                 }
883         }
884
885         /**
886          * Frontend App script
887          *
888          * The App object behaves like a container and a dispatcher at the same time, including a representation of the
889          * request and a representation of the response.
890          *
891          * This probably should change to limit the size of this monster method.
892          *
893          * @param App\Module $module The determined module
894          */
895         public function runFrontend(App\Module $module, App\Router $router, PConfiguration $pconfig)
896         {
897                 $moduleName = $module->getName();
898
899                 try {
900                         // Missing DB connection: ERROR
901                         if ($this->mode->has(App\Mode::LOCALCONFIGPRESENT) && !$this->mode->has(App\Mode::DBAVAILABLE)) {
902                                 throw new HTTPException\InternalServerErrorException('Apologies but the website is unavailable at the moment.');
903                         }
904
905                         // Max Load Average reached: ERROR
906                         if ($this->isMaxProcessesReached() || $this->isMaxLoadReached()) {
907                                 header('Retry-After: 120');
908                                 header('Refresh: 120; url=' . $this->baseURL->get() . "/" . $this->args->getQueryString());
909
910                                 throw new HTTPException\ServiceUnavailableException('The node is currently overloaded. Please try again later.');
911                         }
912
913                         if (!$this->mode->isInstall()) {
914                                 // Force SSL redirection
915                                 if ($this->baseURL->checkRedirectHttps()) {
916                                         System::externalRedirect($this->baseURL->get() . '/' . $this->args->getQueryString());
917                                 }
918
919                                 Core\Session::init();
920                                 Core\Hook::callAll('init_1');
921                         }
922
923                         // Exclude the backend processes from the session management
924                         if (!$this->mode->isBackend()) {
925                                 $stamp1 = microtime(true);
926                                 session_start();
927                                 $this->profiler->saveTimestamp($stamp1, 'parser', Core\System::callstack());
928                                 $this->l10n->setSessionVariable();
929                                 $this->l10n->setLangFromSession();
930                         } else {
931                                 $_SESSION = [];
932                                 Core\Worker::executeIfIdle();
933                         }
934
935                         if ($this->mode->isNormal()) {
936                                 $requester = HTTPSignature::getSigner('', $_SERVER);
937                                 if (!empty($requester)) {
938                                         Profile::addVisitorCookieForHandle($requester);
939                                 }
940                         }
941
942                         // ZRL
943                         if (!empty($_GET['zrl']) && $this->mode->isNormal()) {
944                                 if (!local_user()) {
945                                         // Only continue when the given profile link seems valid
946                                         // Valid profile links contain a path with "/profile/" and no query parameters
947                                         if ((parse_url($_GET['zrl'], PHP_URL_QUERY) == "") &&
948                                             strstr(parse_url($_GET['zrl'], PHP_URL_PATH), "/profile/")) {
949                                                 if (Core\Session::get('visitor_home') != $_GET["zrl"]) {
950                                                         Core\Session::set('my_url', $_GET['zrl']);
951                                                         Core\Session::set('authenticated', 0);
952                                                 }
953
954                                                 Model\Profile::zrlInit($this);
955                                         } else {
956                                                 // Someone came with an invalid parameter, maybe as a DDoS attempt
957                                                 // We simply stop processing here
958                                                 $this->logger->debug('Invalid ZRL parameter.', ['zrl' => $_GET['zrl']]);
959                                                 throw new HTTPException\ForbiddenException();
960                                         }
961                                 }
962                         }
963
964                         if (!empty($_GET['owt']) && $this->mode->isNormal()) {
965                                 $token = $_GET['owt'];
966                                 Model\Profile::openWebAuthInit($token);
967                         }
968
969                         Login::sessionAuth();
970
971                         if (empty($_SESSION['authenticated'])) {
972                                 header('X-Account-Management-Status: none');
973                         }
974
975                         $_SESSION['sysmsg']       = Core\Session::get('sysmsg', []);
976                         $_SESSION['sysmsg_info']  = Core\Session::get('sysmsg_info', []);
977                         $_SESSION['last_updated'] = Core\Session::get('last_updated', []);
978
979                         /*
980                          * check_config() is responsible for running update scripts. These automatically
981                          * update the DB schema whenever we push a new one out. It also checks to see if
982                          * any addons have been added or removed and reacts accordingly.
983                          */
984
985                         // in install mode, any url loads install module
986                         // but we need "view" module for stylesheet
987                         if ($this->mode->isInstall() && $moduleName !== 'install') {
988                                 $this->internalRedirect('install');
989                         } elseif (!$this->mode->isInstall() && !$this->mode->has(App\Mode::MAINTENANCEDISABLED) && $moduleName !== 'maintenance') {
990                                 $this->internalRedirect('maintenance');
991                         } else {
992                                 $this->checkURL();
993                                 Core\Update::check($this->getBasePath(), false, $this->mode);
994                                 Core\Addon::loadAddons();
995                                 Core\Hook::loadHooks();
996                         }
997
998                         $this->page = [
999                                 'aside'       => '',
1000                                 'bottom'      => '',
1001                                 'content'     => '',
1002                                 'footer'      => '',
1003                                 'htmlhead'    => '',
1004                                 'nav'         => '',
1005                                 'page_title'  => '',
1006                                 'right_aside' => '',
1007                                 'template'    => '',
1008                                 'title'       => ''
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 }