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