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