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