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