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