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