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