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