]> git.mxchange.org Git - friendica.git/blob - src/App.php
Move delimiter props and functions
[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\Database\DBA;
12 use Friendica\Network\HTTPException\InternalServerErrorException;
13
14 require_once 'boot.php';
15 require_once 'include/text.php';
16
17 /**
18  *
19  * class: App
20  *
21  * @brief Our main application structure for the life of this page.
22  *
23  * Primarily deals with the URL that got us here
24  * and tries to make some sense of it, and
25  * stores our page contents and config storage
26  * and anything else that might need to be passed around
27  * before we spit the page out.
28  *
29  */
30 class App
31 {
32         public $module_loaded = false;
33         public $module_class = null;
34         public $query_string = '';
35         public $config = [];
36         public $page = [];
37         public $profile;
38         public $profile_uid;
39         public $user;
40         public $cid;
41         public $contact;
42         public $contacts;
43         public $page_contact;
44         public $content;
45         public $data = [];
46         public $error = false;
47         public $cmd = '';
48         public $argv;
49         public $argc;
50         public $module;
51         public $timezone;
52         public $interactive = true;
53         public $identities;
54         public $is_mobile = false;
55         public $is_tablet = false;
56         public $performance = [];
57         public $callstack = [];
58         public $theme_info = [];
59         public $category;
60         // Allow themes to control internal parameters
61         // by changing App values in theme.php
62
63         public $sourcename = '';
64         public $videowidth = 425;
65         public $videoheight = 350;
66         public $force_max_items = 0;
67         public $theme_events_in_profile = true;
68
69         public $stylesheets = [];
70         public $footerScripts = [];
71
72         /**
73          * @var App\Mode The Mode of the Application
74          */
75         private $mode;
76
77         /**
78          * @var string The App base path
79          */
80         private $basePath;
81
82         /**
83          * @var string The App URL path
84          */
85         private $urlPath;
86
87         /**
88          * @var bool true, if the call is from the Friendica APP, otherwise false
89          */
90         private $isFriendicaApp;
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          * Register a stylesheet file path to be included in the <head> tag of every page.
109          * Inclusion is done in App->initHead().
110          * The path can be absolute or relative to the Friendica installation base folder.
111          *
112          * @see initHead()
113          *
114          * @param string $path
115          */
116         public function registerStylesheet($path)
117         {
118                 $url = str_replace($this->getBasePath() . DIRECTORY_SEPARATOR, '', $path);
119
120                 $this->stylesheets[] = trim($url, '/');
121         }
122
123         /**
124          * Register a javascript file path to be included in the <footer> tag of every page.
125          * Inclusion is done in App->initFooter().
126          * The path can be absolute or relative to the Friendica installation base folder.
127          *
128          * @see initFooter()
129          *
130          * @param string $path
131          */
132         public function registerFooterScript($path)
133         {
134                 $url = str_replace($this->getBasePath() . DIRECTORY_SEPARATOR, '', $path);
135
136                 $this->footerScripts[] = trim($url, '/');
137         }
138
139         /**
140          * @brief An array for all theme-controllable parameters
141          *
142          * Mostly unimplemented yet. Only options 'template_engine' and
143          * beyond are used.
144          */
145         public $theme = [
146                 'sourcename' => '',
147                 'videowidth' => 425,
148                 'videoheight' => 350,
149                 'force_max_items' => 0,
150                 'stylesheet' => '',
151                 'template_engine' => 'smarty3',
152         ];
153
154         /**
155          * @brief An array of registered template engines ('name'=>'class name')
156          */
157         public $template_engines = [];
158
159         /**
160          * @brief An array of instanced template engines ('name'=>'instance')
161          */
162         public $template_engine_instance = [];
163         public $process_id;
164         public $queue;
165         private $scheme;
166         private $hostname;
167
168         /**
169          * @brief App constructor.
170          *
171          * @param string $basePath  Path to the app base folder
172          * @param bool   $isBackend Whether it is used for backend or frontend (Default true=backend)
173          *
174          * @throws Exception if the Basepath is not usable
175          */
176         public function __construct($basePath, $isBackend = true)
177         {
178                 if (!static::isDirectoryUsable($basePath, false)) {
179                         throw new Exception('Basepath ' . $basePath . ' isn\'t usable.');
180                 }
181
182                 BaseObject::setApp($this);
183
184                 $this->basePath = rtrim($basePath, DIRECTORY_SEPARATOR);
185                 $this->checkBackend($isBackend);
186                 $this->checkFriendicaApp();
187
188                 $this->performance['start'] = microtime(true);
189                 $this->performance['database'] = 0;
190                 $this->performance['database_write'] = 0;
191                 $this->performance['cache'] = 0;
192                 $this->performance['cache_write'] = 0;
193                 $this->performance['network'] = 0;
194                 $this->performance['file'] = 0;
195                 $this->performance['rendering'] = 0;
196                 $this->performance['parser'] = 0;
197                 $this->performance['marktime'] = 0;
198                 $this->performance['markstart'] = microtime(true);
199
200                 $this->callstack['database'] = [];
201                 $this->callstack['database_write'] = [];
202                 $this->callstack['cache'] = [];
203                 $this->callstack['cache_write'] = [];
204                 $this->callstack['network'] = [];
205                 $this->callstack['file'] = [];
206                 $this->callstack['rendering'] = [];
207                 $this->callstack['parser'] = [];
208
209                 $this->mode = new App\Mode($basePath);
210
211                 $this->reload();
212
213                 set_time_limit(0);
214
215                 // This has to be quite large to deal with embedded private photos
216                 ini_set('pcre.backtrack_limit', 500000);
217
218                 $this->scheme = 'http';
219
220                 if (!empty($_SERVER['HTTPS']) ||
221                         !empty($_SERVER['HTTP_FORWARDED']) && preg_match('/proto=https/', $_SERVER['HTTP_FORWARDED']) ||
222                         !empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https' ||
223                         !empty($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] == 'on' ||
224                         !empty($_SERVER['FRONT_END_HTTPS']) && $_SERVER['FRONT_END_HTTPS'] == 'on' ||
225                         !empty($_SERVER['SERVER_PORT']) && (intval($_SERVER['SERVER_PORT']) == 443) // XXX: reasonable assumption, but isn't this hardcoding too much?
226                 ) {
227                         $this->scheme = 'https';
228                 }
229
230                 if (!empty($_SERVER['SERVER_NAME'])) {
231                         $this->hostname = $_SERVER['SERVER_NAME'];
232
233                         if (!empty($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] != 80 && $_SERVER['SERVER_PORT'] != 443) {
234                                 $this->hostname .= ':' . $_SERVER['SERVER_PORT'];
235                         }
236                 }
237
238                 set_include_path(
239                         get_include_path() . PATH_SEPARATOR
240                         . $this->getBasePath() . DIRECTORY_SEPARATOR . 'include' . PATH_SEPARATOR
241                         . $this->getBasePath(). DIRECTORY_SEPARATOR . 'library' . PATH_SEPARATOR
242                         . $this->getBasePath());
243
244                 if (!empty($_SERVER['QUERY_STRING']) && strpos($_SERVER['QUERY_STRING'], 'pagename=') === 0) {
245                         $this->query_string = substr($_SERVER['QUERY_STRING'], 9);
246                 } elseif (!empty($_SERVER['QUERY_STRING']) && strpos($_SERVER['QUERY_STRING'], 'q=') === 0) {
247                         $this->query_string = substr($_SERVER['QUERY_STRING'], 2);
248                 }
249
250                 // removing trailing / - maybe a nginx problem
251                 $this->query_string = ltrim($this->query_string, '/');
252
253                 if (!empty($_GET['pagename'])) {
254                         $this->cmd = trim($_GET['pagename'], '/\\');
255                 } elseif (!empty($_GET['q'])) {
256                         $this->cmd = trim($_GET['q'], '/\\');
257                 }
258
259                 // fix query_string
260                 $this->query_string = str_replace($this->cmd . '&', $this->cmd . '?', $this->query_string);
261
262                 // unix style "homedir"
263                 if (substr($this->cmd, 0, 1) === '~') {
264                         $this->cmd = 'profile/' . substr($this->cmd, 1);
265                 }
266
267                 // Diaspora style profile url
268                 if (substr($this->cmd, 0, 2) === 'u/') {
269                         $this->cmd = 'profile/' . substr($this->cmd, 2);
270                 }
271
272                 /*
273                  * Break the URL path into C style argc/argv style arguments for our
274                  * modules. Given "http://example.com/module/arg1/arg2", $this->argc
275                  * will be 3 (integer) and $this->argv will contain:
276                  *   [0] => 'module'
277                  *   [1] => 'arg1'
278                  *   [2] => 'arg2'
279                  *
280                  *
281                  * There will always be one argument. If provided a naked domain
282                  * URL, $this->argv[0] is set to "home".
283                  */
284
285                 $this->argv = explode('/', $this->cmd);
286                 $this->argc = count($this->argv);
287                 if ((array_key_exists('0', $this->argv)) && strlen($this->argv[0])) {
288                         $this->module = str_replace('.', '_', $this->argv[0]);
289                         $this->module = str_replace('-', '_', $this->module);
290                 } else {
291                         $this->argc = 1;
292                         $this->argv = ['home'];
293                         $this->module = 'home';
294                 }
295
296                 // Detect mobile devices
297                 $mobile_detect = new MobileDetect();
298                 $this->is_mobile = $mobile_detect->isMobile();
299                 $this->is_tablet = $mobile_detect->isTablet();
300
301                 $this->isAjax = strtolower(defaults($_SERVER, 'HTTP_X_REQUESTED_WITH', '')) == 'xmlhttprequest';
302
303                 // Register template engines
304                 $this->registerTemplateEngine('Friendica\Render\FriendicaSmartyEngine');
305         }
306
307         /**
308          * Returns the Mode of the Application
309          *
310          * @return App\Mode The Application Mode
311          *
312          * @throws InternalServerErrorException when the mode isn't created
313          */
314         public function getMode()
315         {
316                 if (empty($this->mode)) {
317                         throw new InternalServerErrorException('Mode of the Application is not defined');
318                 }
319
320                 return $this->mode;
321         }
322
323         /**
324          * Reloads the whole app instance
325          */
326         public function reload()
327         {
328                 // The order of the following calls is important to ensure proper initialization
329                 $this->loadConfigFiles();
330
331                 $this->loadDatabase();
332
333                 $this->getMode()->determine($this->getBasePath());
334
335                 $this->determineURLPath();
336
337                 Core\Config::load();
338
339                 if ($this->getMode()->has(App\Mode::DBAVAILABLE)) {
340                         Core\Hook::loadHooks();
341
342                         $this->loadAddonConfig();
343                 }
344
345                 $this->loadDefaultTimezone();
346
347                 Core\L10n::init();
348
349                 $this->process_id = Core\System::processID('log');
350         }
351
352         /**
353          * Load the configuration files
354          *
355          * First loads the default value for all the configuration keys, then the legacy configuration files, then the
356          * expected local.ini.php
357          */
358         private function loadConfigFiles()
359         {
360                 $this->loadConfigFile($this->getBasePath() . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'config.ini.php');
361                 $this->loadConfigFile($this->getBasePath() . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'settings.ini.php');
362
363                 // Legacy .htconfig.php support
364                 if (file_exists($this->getBasePath() . DIRECTORY_SEPARATOR . '.htpreconfig.php')) {
365                         $a = $this;
366                         include $this->getBasePath() . DIRECTORY_SEPARATOR . '.htpreconfig.php';
367                 }
368
369                 // Legacy .htconfig.php support
370                 if (file_exists($this->getBasePath() . DIRECTORY_SEPARATOR . '.htconfig.php')) {
371                         $a = $this;
372
373                         include $this->getBasePath() . DIRECTORY_SEPARATOR . '.htconfig.php';
374
375                         $this->setConfigValue('database', 'hostname', $db_host);
376                         $this->setConfigValue('database', 'username', $db_user);
377                         $this->setConfigValue('database', 'password', $db_pass);
378                         $this->setConfigValue('database', 'database', $db_data);
379                         if (isset($a->config['system']['db_charset'])) {
380                                 $this->setConfigValue('database', 'charset', $a->config['system']['db_charset']);
381                         }
382
383                         unset($db_host, $db_user, $db_pass, $db_data);
384
385                         if (isset($default_timezone)) {
386                                 $this->setConfigValue('system', 'default_timezone', $default_timezone);
387                                 unset($default_timezone);
388                         }
389
390                         if (isset($pidfile)) {
391                                 $this->setConfigValue('system', 'pidfile', $pidfile);
392                                 unset($pidfile);
393                         }
394
395                         if (isset($lang)) {
396                                 $this->setConfigValue('system', 'language', $lang);
397                                 unset($lang);
398                         }
399                 }
400
401                 if (file_exists($this->getBasePath() . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'local.ini.php')) {
402                         $this->loadConfigFile($this->getBasePath() . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'local.ini.php', true);
403                 }
404         }
405
406         /**
407          * Tries to load the specified configuration file into the App->config array.
408          * Doesn't overwrite previously set values by default to prevent default config files to supersede DB Config.
409          *
410          * The config format is INI and the template for configuration files is the following:
411          *
412          * <?php return <<<INI
413          *
414          * [section]
415          * key = value
416          *
417          * INI;
418          * // Keep this line
419          *
420          * @param string $filepath
421          * @param bool $overwrite Force value overwrite if the config key already exists
422          * @throws Exception
423          */
424         public function loadConfigFile($filepath, $overwrite = false)
425         {
426                 if (!file_exists($filepath)) {
427                         throw new Exception('Error parsing non-existent config file ' . $filepath);
428                 }
429
430                 $contents = include($filepath);
431
432                 $config = parse_ini_string($contents, true, INI_SCANNER_TYPED);
433
434                 if ($config === false) {
435                         throw new Exception('Error parsing config file ' . $filepath);
436                 }
437
438                 foreach ($config as $category => $values) {
439                         foreach ($values as $key => $value) {
440                                 if ($overwrite) {
441                                         $this->setConfigValue($category, $key, $value);
442                                 } else {
443                                         $this->setDefaultConfigValue($category, $key, $value);
444                                 }
445                         }
446                 }
447         }
448
449         /**
450          * Loads addons configuration files
451          *
452          * First loads all activated addons default configuration throught the load_config hook, then load the local.ini.php
453          * again to overwrite potential local addon configuration.
454          */
455         private function loadAddonConfig()
456         {
457                 // Loads addons default config
458                 Core\Addon::callHooks('load_config');
459
460                 // Load the local addon config file to overwritten default addon config values
461                 if (file_exists($this->getBasePath() . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'addon.ini.php')) {
462                         $this->loadConfigFile($this->getBasePath() . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'addon.ini.php', true);
463                 }
464         }
465
466         /**
467          * Loads the default timezone
468          *
469          * Include support for legacy $default_timezone
470          *
471          * @global string $default_timezone
472          */
473         private function loadDefaultTimezone()
474         {
475                 if ($this->getConfigValue('system', 'default_timezone')) {
476                         $this->timezone = $this->getConfigValue('system', 'default_timezone');
477                 } else {
478                         global $default_timezone;
479                         $this->timezone = !empty($default_timezone) ? $default_timezone : 'UTC';
480                 }
481
482                 if ($this->timezone) {
483                         date_default_timezone_set($this->timezone);
484                 }
485         }
486
487         /**
488          * Figure out if we are running at the top of a domain or in a sub-directory and adjust accordingly
489          */
490         private function determineURLPath()
491         {
492                 /* Relative script path to the web server root
493                  * Not all of those $_SERVER properties can be present, so we do by inverse priority order
494                  */
495                 $relative_script_path = '';
496                 $relative_script_path = defaults($_SERVER, 'REDIRECT_URL'       , $relative_script_path);
497                 $relative_script_path = defaults($_SERVER, 'REDIRECT_URI'       , $relative_script_path);
498                 $relative_script_path = defaults($_SERVER, 'REDIRECT_SCRIPT_URL', $relative_script_path);
499                 $relative_script_path = defaults($_SERVER, 'SCRIPT_URL'         , $relative_script_path);
500                 $relative_script_path = defaults($_SERVER, 'REQUEST_URI'        , $relative_script_path);
501
502                 $this->urlPath = $this->getConfigValue('system', 'urlpath');
503
504                 /* $relative_script_path gives /relative/path/to/friendica/module/parameter
505                  * QUERY_STRING gives pagename=module/parameter
506                  *
507                  * To get /relative/path/to/friendica we perform dirname() for as many levels as there are slashes in the QUERY_STRING
508                  */
509                 if (!empty($relative_script_path)) {
510                         // Module
511                         if (!empty($_SERVER['QUERY_STRING'])) {
512                                 $path = trim(dirname($relative_script_path, substr_count(trim($_SERVER['QUERY_STRING'], '/'), '/') + 1), '/');
513                         } else {
514                                 // Root page
515                                 $path = trim($relative_script_path, '/');
516                         }
517
518                         if ($path && $path != $this->urlPath) {
519                                 $this->urlPath = $path;
520                         }
521                 }
522         }
523
524         public function loadDatabase()
525         {
526                 if (DBA::connected()) {
527                         return;
528                 }
529
530                 $db_host = $this->getConfigValue('database', 'hostname');
531                 $db_user = $this->getConfigValue('database', 'username');
532                 $db_pass = $this->getConfigValue('database', 'password');
533                 $db_data = $this->getConfigValue('database', 'database');
534                 $charset = $this->getConfigValue('database', 'charset');
535
536                 // Use environment variables for mysql if they are set beforehand
537                 if (!empty(getenv('MYSQL_HOST'))
538                         && (!empty(getenv('MYSQL_USERNAME')) || !empty(getenv('MYSQL_USER')))
539                         && getenv('MYSQL_PASSWORD') !== false
540                         && !empty(getenv('MYSQL_DATABASE')))
541                 {
542                         $db_host = getenv('MYSQL_HOST');
543                         if (!empty(getenv('MYSQL_PORT'))) {
544                                 $db_host .= ':' . getenv('MYSQL_PORT');
545                         }
546                         if (!empty(getenv('MYSQL_USERNAME'))) {
547                                 $db_user = getenv('MYSQL_USERNAME');
548                         } else {
549                                 $db_user = getenv('MYSQL_USER');
550                         }
551                         $db_pass = (string) getenv('MYSQL_PASSWORD');
552                         $db_data = getenv('MYSQL_DATABASE');
553                 }
554
555                 $stamp1 = microtime(true);
556
557                 if (DBA::connect($db_host, $db_user, $db_pass, $db_data, $charset)) {
558                         // Loads DB_UPDATE_VERSION constant
559                         Database\DBStructure::definition(false);
560                 }
561
562                 unset($db_host, $db_user, $db_pass, $db_data, $charset);
563
564                 $this->saveTimestamp($stamp1, 'network');
565         }
566
567         /**
568          * @brief Returns the base filesystem path of the App
569          *
570          * It first checks for the internal variable, then for DOCUMENT_ROOT and
571          * finally for PWD
572          *
573          * @return string
574          */
575         public function getBasePath()
576         {
577                 $basepath = $this->basePath;
578
579                 if (!$basepath) {
580                         $basepath = Core\Config::get('system', 'basepath');
581                 }
582
583                 if (!$basepath && !empty($_SERVER['DOCUMENT_ROOT'])) {
584                         $basepath = $_SERVER['DOCUMENT_ROOT'];
585                 }
586
587                 if (!$basepath && !empty($_SERVER['PWD'])) {
588                         $basepath = $_SERVER['PWD'];
589                 }
590
591                 return self::getRealPath($basepath);
592         }
593
594         /**
595          * @brief Returns a normalized file path
596          *
597          * This is a wrapper for the "realpath" function.
598          * That function cannot detect the real path when some folders aren't readable.
599          * Since this could happen with some hosters we need to handle this.
600          *
601          * @param string $path The path that is about to be normalized
602          * @return string normalized path - when possible
603          */
604         public static function getRealPath($path)
605         {
606                 $normalized = realpath($path);
607
608                 if (!is_bool($normalized)) {
609                         return $normalized;
610                 } else {
611                         return $path;
612                 }
613         }
614
615         public function getScheme()
616         {
617                 return $this->scheme;
618         }
619
620         /**
621          * @brief Retrieves the Friendica instance base URL
622          *
623          * This function assembles the base URL from multiple parts:
624          * - Protocol is determined either by the request or a combination of
625          * system.ssl_policy and the $ssl parameter.
626          * - Host name is determined either by system.hostname or inferred from request
627          * - Path is inferred from SCRIPT_NAME
628          *
629          * Note: $ssl parameter value doesn't directly correlate with the resulting protocol
630          *
631          * @param bool $ssl Whether to append http or https under SSL_POLICY_SELFSIGN
632          * @return string Friendica server base URL
633          */
634         public function getBaseURL($ssl = false)
635         {
636                 $scheme = $this->scheme;
637
638                 if (Core\Config::get('system', 'ssl_policy') == SSL_POLICY_FULL) {
639                         $scheme = 'https';
640                 }
641
642                 //      Basically, we have $ssl = true on any links which can only be seen by a logged in user
643                 //      (and also the login link). Anything seen by an outsider will have it turned off.
644
645                 if (Core\Config::get('system', 'ssl_policy') == SSL_POLICY_SELFSIGN) {
646                         if ($ssl) {
647                                 $scheme = 'https';
648                         } else {
649                                 $scheme = 'http';
650                         }
651                 }
652
653                 if (Core\Config::get('config', 'hostname') != '') {
654                         $this->hostname = Core\Config::get('config', 'hostname');
655                 }
656
657                 return $scheme . '://' . $this->hostname . (!empty($this->getURLPath()) ? '/' . $this->getURLPath() : '' );
658         }
659
660         /**
661          * @brief Initializes the baseurl components
662          *
663          * Clears the baseurl cache to prevent inconsistencies
664          *
665          * @param string $url
666          */
667         public function setBaseURL($url)
668         {
669                 $parsed = @parse_url($url);
670                 $hostname = '';
671
672                 if (!empty($parsed)) {
673                         if (!empty($parsed['scheme'])) {
674                                 $this->scheme = $parsed['scheme'];
675                         }
676
677                         if (!empty($parsed['host'])) {
678                                 $hostname = $parsed['host'];
679                         }
680
681                         if (!empty($parsed['port'])) {
682                                 $hostname .= ':' . $parsed['port'];
683                         }
684                         if (!empty($parsed['path'])) {
685                                 $this->urlPath = trim($parsed['path'], '\\/');
686                         }
687
688                         if (file_exists($this->getBasePath() . DIRECTORY_SEPARATOR . '.htpreconfig.php')) {
689                                 include $this->getBasePath() . DIRECTORY_SEPARATOR . '.htpreconfig.php';
690                         }
691
692                         if (Core\Config::get('config', 'hostname') != '') {
693                                 $this->hostname = Core\Config::get('config', 'hostname');
694                         }
695
696                         if (!isset($this->hostname) || ($this->hostname == '')) {
697                                 $this->hostname = $hostname;
698                         }
699                 }
700         }
701
702         public function getHostName()
703         {
704                 if (Core\Config::get('config', 'hostname') != '') {
705                         $this->hostname = Core\Config::get('config', 'hostname');
706                 }
707
708                 return $this->hostname;
709         }
710
711         public function getURLPath()
712         {
713                 return $this->urlPath;
714         }
715
716         /**
717          * Initializes App->page['htmlhead'].
718          *
719          * Includes:
720          * - Page title
721          * - Favicons
722          * - Registered stylesheets (through App->registerStylesheet())
723          * - Infinite scroll data
724          * - head.tpl template
725          */
726         public function initHead()
727         {
728                 $interval = ((local_user()) ? Core\PConfig::get(local_user(), 'system', 'update_interval') : 40000);
729
730                 // If the update is 'deactivated' set it to the highest integer number (~24 days)
731                 if ($interval < 0) {
732                         $interval = 2147483647;
733                 }
734
735                 if ($interval < 10000) {
736                         $interval = 40000;
737                 }
738
739                 // compose the page title from the sitename and the
740                 // current module called
741                 if (!$this->module == '') {
742                         $this->page['title'] = $this->config['sitename'] . ' (' . $this->module . ')';
743                 } else {
744                         $this->page['title'] = $this->config['sitename'];
745                 }
746
747                 if (!empty($this->theme['stylesheet'])) {
748                         $stylesheet = $this->theme['stylesheet'];
749                 } else {
750                         $stylesheet = $this->getCurrentThemeStylesheetPath();
751                 }
752
753                 $this->registerStylesheet($stylesheet);
754
755                 $shortcut_icon = Core\Config::get('system', 'shortcut_icon');
756                 if ($shortcut_icon == '') {
757                         $shortcut_icon = 'images/friendica-32.png';
758                 }
759
760                 $touch_icon = Core\Config::get('system', 'touch_icon');
761                 if ($touch_icon == '') {
762                         $touch_icon = 'images/friendica-128.png';
763                 }
764
765                 Core\Addon::callHooks('head', $this->page['htmlhead']);
766
767                 $tpl = Core\Renderer::getMarkupTemplate('head.tpl');
768                 /* put the head template at the beginning of page['htmlhead']
769                  * since the code added by the modules frequently depends on it
770                  * being first
771                  */
772                 $this->page['htmlhead'] = Core\Renderer::replaceMacros($tpl, [
773                         '$baseurl'         => $this->getBaseURL(),
774                         '$local_user'      => local_user(),
775                         '$generator'       => 'Friendica' . ' ' . FRIENDICA_VERSION,
776                         '$delitem'         => Core\L10n::t('Delete this item?'),
777                         '$showmore'        => Core\L10n::t('show more'),
778                         '$showfewer'       => Core\L10n::t('show fewer'),
779                         '$update_interval' => $interval,
780                         '$shortcut_icon'   => $shortcut_icon,
781                         '$touch_icon'      => $touch_icon,
782                         '$block_public'    => intval(Core\Config::get('system', 'block_public')),
783                         '$stylesheets'     => $this->stylesheets,
784                 ]) . $this->page['htmlhead'];
785         }
786
787         /**
788          * Initializes App->page['footer'].
789          *
790          * Includes:
791          * - Javascript homebase
792          * - Mobile toggle link
793          * - Registered footer scripts (through App->registerFooterScript())
794          * - footer.tpl template
795          */
796         public function initFooter()
797         {
798                 // If you're just visiting, let javascript take you home
799                 if (!empty($_SESSION['visitor_home'])) {
800                         $homebase = $_SESSION['visitor_home'];
801                 } elseif (local_user()) {
802                         $homebase = 'profile/' . $this->user['nickname'];
803                 }
804
805                 if (isset($homebase)) {
806                         $this->page['footer'] .= '<script>var homebase="' . $homebase . '";</script>' . "\n";
807                 }
808
809                 /*
810                  * Add a "toggle mobile" link if we're using a mobile device
811                  */
812                 if ($this->is_mobile || $this->is_tablet) {
813                         if (isset($_SESSION['show-mobile']) && !$_SESSION['show-mobile']) {
814                                 $link = 'toggle_mobile?address=' . curPageURL();
815                         } else {
816                                 $link = 'toggle_mobile?off=1&address=' . curPageURL();
817                         }
818                         $this->page['footer'] .= Core\Renderer::replaceMacros(Core\Renderer::getMarkupTemplate("toggle_mobile_footer.tpl"), [
819                                 '$toggle_link' => $link,
820                                 '$toggle_text' => Core\L10n::t('toggle mobile')
821                         ]);
822                 }
823
824                 Core\Addon::callHooks('footer', $this->page['footer']);
825
826                 $tpl = Core\Renderer::getMarkupTemplate('footer.tpl');
827                 $this->page['footer'] = Core\Renderer::replaceMacros($tpl, [
828                         '$baseurl' => $this->getBaseURL(),
829                         '$footerScripts' => $this->footerScripts,
830                 ]) . $this->page['footer'];
831         }
832
833         /**
834          * @brief Removes the base url from an url. This avoids some mixed content problems.
835          *
836          * @param string $origURL
837          *
838          * @return string The cleaned url
839          */
840         public function removeBaseURL($origURL)
841         {
842                 // Remove the hostname from the url if it is an internal link
843                 $nurl = normalise_link($origURL);
844                 $base = normalise_link($this->getBaseURL());
845                 $url = str_replace($base . '/', '', $nurl);
846
847                 // if it is an external link return the orignal value
848                 if ($url == normalise_link($origURL)) {
849                         return $origURL;
850                 } else {
851                         return $url;
852                 }
853         }
854
855         /**
856          * @brief Register template engine class
857          *
858          * @param string $class
859          */
860         private function registerTemplateEngine($class)
861         {
862                 $v = get_class_vars($class);
863                 if (!empty($v['name'])) {
864                         $name = $v['name'];
865                         $this->template_engines[$name] = $class;
866                 } else {
867                         echo "template engine <tt>$class</tt> cannot be registered without a name.\n";
868                         die();
869                 }
870         }
871
872         /**
873          * @brief Return template engine instance.
874          *
875          * If $name is not defined, return engine defined by theme,
876          * or default
877          *
878          * @return object Template Engine instance
879          */
880         public function getTemplateEngine()
881         {
882                 $template_engine = defaults($this->theme, 'template_engine', 'smarty3');
883
884                 if (isset($this->template_engines[$template_engine])) {
885                         if (isset($this->template_engine_instance[$template_engine])) {
886                                 return $this->template_engine_instance[$template_engine];
887                         } else {
888                                 $class = $this->template_engines[$template_engine];
889                                 $obj = new $class;
890                                 $this->template_engine_instance[$template_engine] = $obj;
891                                 return $obj;
892                         }
893                 }
894
895                 echo "template engine <tt>$template_engine</tt> is not registered!\n";
896                 exit();
897         }
898
899         /**
900          * @brief Returns the active template engine.
901          *
902          * @return string the active template engine
903          */
904         public function getActiveTemplateEngine()
905         {
906                 return $this->theme['template_engine'];
907         }
908
909         /**
910          * sets the active template engine
911          *
912          * @param string $engine the template engine (default is Smarty3)
913          */
914         public function setActiveTemplateEngine($engine = 'smarty3')
915         {
916                 $this->theme['template_engine'] = $engine;
917         }
918
919         /**
920          * Saves a timestamp for a value - f.e. a call
921          * Necessary for profiling Friendica
922          *
923          * @param int $timestamp the Timestamp
924          * @param string $value A value to profile
925          */
926         public function saveTimestamp($timestamp, $value)
927         {
928                 if (!isset($this->config['system']['profiler']) || !$this->config['system']['profiler']) {
929                         return;
930                 }
931
932                 $duration = (float) (microtime(true) - $timestamp);
933
934                 if (!isset($this->performance[$value])) {
935                         // Prevent ugly E_NOTICE
936                         $this->performance[$value] = 0;
937                 }
938
939                 $this->performance[$value] += (float) $duration;
940                 $this->performance['marktime'] += (float) $duration;
941
942                 $callstack = Core\System::callstack();
943
944                 if (!isset($this->callstack[$value][$callstack])) {
945                         // Prevent ugly E_NOTICE
946                         $this->callstack[$value][$callstack] = 0;
947                 }
948
949                 $this->callstack[$value][$callstack] += (float) $duration;
950         }
951
952         /**
953          * Returns the current UserAgent as a String
954          *
955          * @return string the UserAgent as a String
956          */
957         public function getUserAgent()
958         {
959                 return
960                         FRIENDICA_PLATFORM . " '" .
961                         FRIENDICA_CODENAME . "' " .
962                         FRIENDICA_VERSION . '-' .
963                         DB_UPDATE_VERSION . '; ' .
964                         $this->getBaseURL();
965         }
966
967         /**
968          * Checks, if the call is from the Friendica App
969          *
970          * Reason:
971          * The friendica client has problems with the GUID in the notify. this is some workaround
972          */
973         private function checkFriendicaApp()
974         {
975                 // Friendica-Client
976                 $this->isFriendicaApp = isset($_SERVER['HTTP_USER_AGENT']) && $_SERVER['HTTP_USER_AGENT'] == 'Apache-HttpClient/UNAVAILABLE (java 1.4)';
977         }
978
979         /**
980          *      Is the call via the Friendica app? (not a "normale" call)
981          *
982          * @return bool true if it's from the Friendica app
983          */
984         public function isFriendicaApp()
985         {
986                 return $this->isFriendicaApp;
987         }
988
989         /**
990          * @brief Checks if the site is called via a backend process
991          *
992          * This isn't a perfect solution. But we need this check very early.
993          * So we cannot wait until the modules are loaded.
994          *
995          * @param string $backend true, if the backend flag was set during App initialization
996          *
997          */
998         private function checkBackend($backend) {
999                 static $backends = [
1000                         '_well_known',
1001                         'api',
1002                         'dfrn_notify',
1003                         'fetch',
1004                         'hcard',
1005                         'hostxrd',
1006                         'nodeinfo',
1007                         'noscrape',
1008                         'p',
1009                         'poco',
1010                         'post',
1011                         'proxy',
1012                         'pubsub',
1013                         'pubsubhubbub',
1014                         'receive',
1015                         'rsd_xml',
1016                         'salmon',
1017                         'statistics_json',
1018                         'xrd',
1019                 ];
1020
1021                 // Check if current module is in backend or backend flag is set
1022                 $this->isBackend = (in_array($this->module, $backends) || $backend || $this->isBackend);
1023         }
1024
1025         /**
1026          * Returns true, if the call is from a backend node (f.e. from a worker)
1027          *
1028          * @return bool Is it a known backend?
1029          */
1030         public function isBackend()
1031         {
1032                 return $this->isBackend;
1033         }
1034
1035         /**
1036          * @brief Checks if the maximum number of database processes is reached
1037          *
1038          * @return bool Is the limit reached?
1039          */
1040         public function isMaxProcessesReached()
1041         {
1042                 // Deactivated, needs more investigating if this check really makes sense
1043                 return false;
1044
1045                 /*
1046                  * Commented out to suppress static analyzer issues
1047                  *
1048                 if ($this->is_backend()) {
1049                         $process = 'backend';
1050                         $max_processes = Core\Config::get('system', 'max_processes_backend');
1051                         if (intval($max_processes) == 0) {
1052                                 $max_processes = 5;
1053                         }
1054                 } else {
1055                         $process = 'frontend';
1056                         $max_processes = Core\Config::get('system', 'max_processes_frontend');
1057                         if (intval($max_processes) == 0) {
1058                                 $max_processes = 20;
1059                         }
1060                 }
1061
1062                 $processlist = DBA::processlist();
1063                 if ($processlist['list'] != '') {
1064                         Core\Logger::log('Processcheck: Processes: ' . $processlist['amount'] . ' - Processlist: ' . $processlist['list'], Core\Logger::DEBUG);
1065
1066                         if ($processlist['amount'] > $max_processes) {
1067                                 Core\Logger::log('Processcheck: Maximum number of processes for ' . $process . ' tasks (' . $max_processes . ') reached.', Core\Logger::DEBUG);
1068                                 return true;
1069                         }
1070                 }
1071                 return false;
1072                  */
1073         }
1074
1075         /**
1076          * @brief Checks if the minimal memory is reached
1077          *
1078          * @return bool Is the memory limit reached?
1079          */
1080         public function isMinMemoryReached()
1081         {
1082                 $min_memory = Core\Config::get('system', 'min_memory', 0);
1083                 if ($min_memory == 0) {
1084                         return false;
1085                 }
1086
1087                 if (!is_readable('/proc/meminfo')) {
1088                         return false;
1089                 }
1090
1091                 $memdata = explode("\n", file_get_contents('/proc/meminfo'));
1092
1093                 $meminfo = [];
1094                 foreach ($memdata as $line) {
1095                         $data = explode(':', $line);
1096                         if (count($data) != 2) {
1097                                 continue;
1098                         }
1099                         list($key, $val) = $data;
1100                         $meminfo[$key] = (int) trim(str_replace('kB', '', $val));
1101                         $meminfo[$key] = (int) ($meminfo[$key] / 1024);
1102                 }
1103
1104                 if (!isset($meminfo['MemAvailable']) || !isset($meminfo['MemFree'])) {
1105                         return false;
1106                 }
1107
1108                 $free = $meminfo['MemAvailable'] + $meminfo['MemFree'];
1109
1110                 $reached = ($free < $min_memory);
1111
1112                 if ($reached) {
1113                         Core\Logger::log('Minimal memory reached: ' . $free . '/' . $meminfo['MemTotal'] . ' - limit ' . $min_memory, Core\Logger::DEBUG);
1114                 }
1115
1116                 return $reached;
1117         }
1118
1119         /**
1120          * @brief Checks if the maximum load is reached
1121          *
1122          * @return bool Is the load reached?
1123          */
1124         public function isMaxLoadReached()
1125         {
1126                 if ($this->isBackend()) {
1127                         $process = 'backend';
1128                         $maxsysload = intval(Core\Config::get('system', 'maxloadavg'));
1129                         if ($maxsysload < 1) {
1130                                 $maxsysload = 50;
1131                         }
1132                 } else {
1133                         $process = 'frontend';
1134                         $maxsysload = intval(Core\Config::get('system', 'maxloadavg_frontend'));
1135                         if ($maxsysload < 1) {
1136                                 $maxsysload = 50;
1137                         }
1138                 }
1139
1140                 $load = Core\System::currentLoad();
1141                 if ($load) {
1142                         if (intval($load) > $maxsysload) {
1143                                 Core\Logger::log('system: load ' . $load . ' for ' . $process . ' tasks (' . $maxsysload . ') too high.');
1144                                 return true;
1145                         }
1146                 }
1147                 return false;
1148         }
1149
1150         /**
1151          * Executes a child process with 'proc_open'
1152          *
1153          * @param string $command The command to execute
1154          * @param array  $args    Arguments to pass to the command ( [ 'key' => value, 'key2' => value2, ... ]
1155          */
1156         public function proc_run($command, $args)
1157         {
1158                 if (!function_exists('proc_open')) {
1159                         return;
1160                 }
1161
1162                 $cmdline = $this->getConfigValue('config', 'php_path', 'php') . ' ' . escapeshellarg($command);
1163
1164                 foreach ($args as $key => $value) {
1165                         if (!is_null($value) && is_bool($value) && !$value) {
1166                                 continue;
1167                         }
1168
1169                         $cmdline .= ' --' . $key;
1170                         if (!is_null($value) && !is_bool($value)) {
1171                                 $cmdline .= ' ' . $value;
1172                         }
1173                 }
1174
1175                 if ($this->isMinMemoryReached()) {
1176                         return;
1177                 }
1178
1179                 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
1180                         $resource = proc_open('cmd /c start /b ' . $cmdline, [], $foo, $this->getBasePath());
1181                 } else {
1182                         $resource = proc_open($cmdline . ' &', [], $foo, $this->getBasePath());
1183                 }
1184                 if (!is_resource($resource)) {
1185                         Core\Logger::log('We got no resource for command ' . $cmdline, Core\Logger::DEBUG);
1186                         return;
1187                 }
1188                 proc_close($resource);
1189         }
1190
1191         /**
1192          * @brief Returns the system user that is executing the script
1193          *
1194          * This mostly returns something like "www-data".
1195          *
1196          * @return string system username
1197          */
1198         private static function getSystemUser()
1199         {
1200                 if (!function_exists('posix_getpwuid') || !function_exists('posix_geteuid')) {
1201                         return '';
1202                 }
1203
1204                 $processUser = posix_getpwuid(posix_geteuid());
1205                 return $processUser['name'];
1206         }
1207
1208         /**
1209          * @brief Checks if a given directory is usable for the system
1210          *
1211          * @return boolean the directory is usable
1212          */
1213         public static function isDirectoryUsable($directory, $check_writable = true)
1214         {
1215                 if ($directory == '') {
1216                         Core\Logger::log('Directory is empty. This shouldn\'t happen.', Core\Logger::DEBUG);
1217                         return false;
1218                 }
1219
1220                 if (!file_exists($directory)) {
1221                         Core\Logger::log('Path "' . $directory . '" does not exist for user ' . self::getSystemUser(), Core\Logger::DEBUG);
1222                         return false;
1223                 }
1224
1225                 if (is_file($directory)) {
1226                         Core\Logger::log('Path "' . $directory . '" is a file for user ' . self::getSystemUser(), Core\Logger::DEBUG);
1227                         return false;
1228                 }
1229
1230                 if (!is_dir($directory)) {
1231                         Core\Logger::log('Path "' . $directory . '" is not a directory for user ' . self::getSystemUser(), Core\Logger::DEBUG);
1232                         return false;
1233                 }
1234
1235                 if ($check_writable && !is_writable($directory)) {
1236                         Core\Logger::log('Path "' . $directory . '" is not writable for user ' . self::getSystemUser(), Core\Logger::DEBUG);
1237                         return false;
1238                 }
1239
1240                 return true;
1241         }
1242
1243         /**
1244          * @param string $cat     Config category
1245          * @param string $k       Config key
1246          * @param mixed  $default Default value if it isn't set
1247          *
1248          * @return string Returns the value of the Config entry
1249          */
1250         public function getConfigValue($cat, $k, $default = null)
1251         {
1252                 $return = $default;
1253
1254                 if ($cat === 'config') {
1255                         if (isset($this->config[$k])) {
1256                                 $return = $this->config[$k];
1257                         }
1258                 } else {
1259                         if (isset($this->config[$cat][$k])) {
1260                                 $return = $this->config[$cat][$k];
1261                         }
1262                 }
1263
1264                 return $return;
1265         }
1266
1267         /**
1268          * Sets a default value in the config cache. Ignores already existing keys.
1269          *
1270          * @param string $cat Config category
1271          * @param string $k   Config key
1272          * @param mixed  $v   Default value to set
1273          */
1274         private function setDefaultConfigValue($cat, $k, $v)
1275         {
1276                 if (!isset($this->config[$cat][$k])) {
1277                         $this->setConfigValue($cat, $k, $v);
1278                 }
1279         }
1280
1281         /**
1282          * Sets a value in the config cache. Accepts raw output from the config table
1283          *
1284          * @param string $cat Config category
1285          * @param string $k   Config key
1286          * @param mixed  $v   Value to set
1287          */
1288         public function setConfigValue($cat, $k, $v)
1289         {
1290                 // Only arrays are serialized in database, so we have to unserialize sparingly
1291                 $value = is_string($v) && preg_match("|^a:[0-9]+:{.*}$|s", $v) ? unserialize($v) : $v;
1292
1293                 if ($cat === 'config') {
1294                         $this->config[$k] = $value;
1295                 } else {
1296                         if (!isset($this->config[$cat])) {
1297                                 $this->config[$cat] = [];
1298                         }
1299
1300                         $this->config[$cat][$k] = $value;
1301                 }
1302         }
1303
1304         /**
1305          * Deletes a value from the config cache
1306          *
1307          * @param string $cat Config category
1308          * @param string $k   Config key
1309          */
1310         public function deleteConfigValue($cat, $k)
1311         {
1312                 if ($cat === 'config') {
1313                         if (isset($this->config[$k])) {
1314                                 unset($this->config[$k]);
1315                         }
1316                 } else {
1317                         if (isset($this->config[$cat][$k])) {
1318                                 unset($this->config[$cat][$k]);
1319                         }
1320                 }
1321         }
1322
1323
1324         /**
1325          * Retrieves a value from the user config cache
1326          *
1327          * @param int    $uid     User Id
1328          * @param string $cat     Config category
1329          * @param string $k       Config key
1330          * @param mixed  $default Default value if key isn't set
1331          *
1332          * @return string The value of the config entry
1333          */
1334         public function getPConfigValue($uid, $cat, $k, $default = null)
1335         {
1336                 $return = $default;
1337
1338                 if (isset($this->config[$uid][$cat][$k])) {
1339                         $return = $this->config[$uid][$cat][$k];
1340                 }
1341
1342                 return $return;
1343         }
1344
1345         /**
1346          * Sets a value in the user config cache
1347          *
1348          * Accepts raw output from the pconfig table
1349          *
1350          * @param int    $uid User Id
1351          * @param string $cat Config category
1352          * @param string $k   Config key
1353          * @param mixed  $v   Value to set
1354          */
1355         public function setPConfigValue($uid, $cat, $k, $v)
1356         {
1357                 // Only arrays are serialized in database, so we have to unserialize sparingly
1358                 $value = is_string($v) && preg_match("|^a:[0-9]+:{.*}$|s", $v) ? unserialize($v) : $v;
1359
1360                 if (!isset($this->config[$uid]) || !is_array($this->config[$uid])) {
1361                         $this->config[$uid] = [];
1362                 }
1363
1364                 if (!isset($this->config[$uid][$cat]) || !is_array($this->config[$uid][$cat])) {
1365                         $this->config[$uid][$cat] = [];
1366                 }
1367
1368                 $this->config[$uid][$cat][$k] = $value;
1369         }
1370
1371         /**
1372          * Deletes a value from the user config cache
1373          *
1374          * @param int    $uid User Id
1375          * @param string $cat Config category
1376          * @param string $k   Config key
1377          */
1378         public function deletePConfigValue($uid, $cat, $k)
1379         {
1380                 if (isset($this->config[$uid][$cat][$k])) {
1381                         unset($this->config[$uid][$cat][$k]);
1382                 }
1383         }
1384
1385         /**
1386          * Generates the site's default sender email address
1387          *
1388          * @return string
1389          */
1390         public function getSenderEmailAddress()
1391         {
1392                 $sender_email = Core\Config::get('config', 'sender_email');
1393                 if (empty($sender_email)) {
1394                         $hostname = $this->getHostName();
1395                         if (strpos($hostname, ':')) {
1396                                 $hostname = substr($hostname, 0, strpos($hostname, ':'));
1397                         }
1398
1399                         $sender_email = 'noreply@' . $hostname;
1400                 }
1401
1402                 return $sender_email;
1403         }
1404
1405         /**
1406          * Returns the current theme name.
1407          *
1408          * @return string the name of the current theme
1409          */
1410         public function getCurrentTheme()
1411         {
1412                 if ($this->getMode()->isInstall()) {
1413                         return '';
1414                 }
1415
1416                 //// @TODO Compute the current theme only once (this behavior has
1417                 /// already been implemented, but it didn't work well -
1418                 /// https://github.com/friendica/friendica/issues/5092)
1419                 $this->computeCurrentTheme();
1420
1421                 return $this->currentTheme;
1422         }
1423
1424         /**
1425          * Computes the current theme name based on the node settings, the user settings and the device type
1426          *
1427          * @throws Exception
1428          */
1429         private function computeCurrentTheme()
1430         {
1431                 $system_theme = Core\Config::get('system', 'theme');
1432                 if (!$system_theme) {
1433                         throw new Exception(Core\L10n::t('No system theme config value set.'));
1434                 }
1435
1436                 // Sane default
1437                 $this->currentTheme = $system_theme;
1438
1439                 $allowed_themes = explode(',', Core\Config::get('system', 'allowed_themes', $system_theme));
1440
1441                 $page_theme = null;
1442                 // Find the theme that belongs to the user whose stuff we are looking at
1443                 if ($this->profile_uid && ($this->profile_uid != local_user())) {
1444                         // Allow folks to override user themes and always use their own on their own site.
1445                         // This works only if the user is on the same server
1446                         $user = DBA::selectFirst('user', ['theme'], ['uid' => $this->profile_uid]);
1447                         if (DBA::isResult($user) && !Core\PConfig::get(local_user(), 'system', 'always_my_theme')) {
1448                                 $page_theme = $user['theme'];
1449                         }
1450                 }
1451
1452                 $user_theme = Core\Session::get('theme', $system_theme);
1453
1454                 // Specific mobile theme override
1455                 if (($this->is_mobile || $this->is_tablet) && Core\Session::get('show-mobile', true)) {
1456                         $system_mobile_theme = Core\Config::get('system', 'mobile-theme');
1457                         $user_mobile_theme = Core\Session::get('mobile-theme', $system_mobile_theme);
1458
1459                         // --- means same mobile theme as desktop
1460                         if (!empty($user_mobile_theme) && $user_mobile_theme !== '---') {
1461                                 $user_theme = $user_mobile_theme;
1462                         }
1463                 }
1464
1465                 if ($page_theme) {
1466                         $theme_name = $page_theme;
1467                 } else {
1468                         $theme_name = $user_theme;
1469                 }
1470
1471                 if ($theme_name
1472                         && in_array($theme_name, $allowed_themes)
1473                         && (file_exists('view/theme/' . $theme_name . '/style.css')
1474                         || file_exists('view/theme/' . $theme_name . '/style.php'))
1475                 ) {
1476                         $this->currentTheme = $theme_name;
1477                 }
1478         }
1479
1480         /**
1481          * @brief Return full URL to theme which is currently in effect.
1482          *
1483          * Provide a sane default if nothing is chosen or the specified theme does not exist.
1484          *
1485          * @return string
1486          */
1487         public function getCurrentThemeStylesheetPath()
1488         {
1489                 return Core\Theme::getStylesheetPath($this->getCurrentTheme());
1490         }
1491
1492         /**
1493          * Check if request was an AJAX (xmlhttprequest) request.
1494          *
1495          * @return boolean true if it was an AJAX request
1496          */
1497         public function isAjax()
1498         {
1499                 return $this->isAjax;
1500         }
1501
1502         /**
1503          * Returns the value of a argv key
1504          * TODO there are a lot of $a->argv usages in combination with defaults() which can be replaced with this method
1505          *
1506          * @param int $position the position of the argument
1507          * @param mixed $default the default value if not found
1508          *
1509          * @return mixed returns the value of the argument
1510          */
1511         public function getArgumentValue($position, $default = '')
1512         {
1513                 if (array_key_exists($position, $this->argv)) {
1514                         return $this->argv[$position];
1515                 }
1516
1517                 return $default;
1518         }
1519
1520         /**
1521          * Sets the base url for use in cmdline programs which don't have
1522          * $_SERVER variables
1523          */
1524         public function checkURL()
1525         {
1526                 $url = Core\Config::get('system', 'url');
1527
1528                 // if the url isn't set or the stored url is radically different
1529                 // than the currently visited url, store the current value accordingly.
1530                 // "Radically different" ignores common variations such as http vs https
1531                 // and www.example.com vs example.com.
1532                 // We will only change the url to an ip address if there is no existing setting
1533
1534                 if (empty($url) || (!link_compare($url, $this->getBaseURL())) && (!preg_match("/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/", $this->getHostName()))) {
1535                         Core\Config::set('system', 'url', $this->getBaseURL());
1536                 }
1537         }
1538
1539         /**
1540          * Frontend App script
1541          *
1542          * The App object behaves like a container and a dispatcher at the same time, including a representation of the
1543          * request and a representation of the response.
1544          *
1545          * This probably should change to limit the size of this monster method.
1546          */
1547         public function runFrontend()
1548         {
1549                 // Missing DB connection: ERROR
1550                 if ($this->getMode()->has(App\Mode::LOCALCONFIGPRESENT) && !$this->getMode()->has(App\Mode::DBAVAILABLE)) {
1551                         Core\System::httpExit(500, ['title' => 'Error 500 - Internal Server Error', 'description' => 'Apologies but the website is unavailable at the moment.']);
1552                 }
1553
1554                 // Max Load Average reached: ERROR
1555                 if ($this->isMaxProcessesReached() || $this->isMaxLoadReached()) {
1556                         header('Retry-After: 120');
1557                         header('Refresh: 120; url=' . $this->getBaseURL() . "/" . $this->query_string);
1558
1559                         Core\System::httpExit(503, ['title' => 'Error 503 - Service Temporarily Unavailable', 'description' => 'Core\System is currently overloaded. Please try again later.']);
1560                 }
1561
1562                 if (strstr($this->query_string, '.well-known/host-meta') && ($this->query_string != '.well-known/host-meta')) {
1563                         Core\System::httpExit(404);
1564                 }
1565
1566                 if (!$this->getMode()->isInstall()) {
1567                         // Force SSL redirection
1568                         if (Core\Config::get('system', 'force_ssl') && ($this->getScheme() == "http")
1569                                 && intval(Core\Config::get('system', 'ssl_policy')) == SSL_POLICY_FULL
1570                                 && strpos($this->getBaseURL(), 'https://') === 0
1571                                 && $_SERVER['REQUEST_METHOD'] == 'GET') {
1572                                 header('HTTP/1.1 302 Moved Temporarily');
1573                                 header('Location: ' . $this->getBaseURL() . '/' . $this->query_string);
1574                                 exit();
1575                         }
1576
1577                         Core\Session::init();
1578                         Core\Addon::callHooks('init_1');
1579                 }
1580
1581                 // Exclude the backend processes from the session management
1582                 if (!$this->isBackend()) {
1583                         $stamp1 = microtime(true);
1584                         session_start();
1585                         $this->saveTimestamp($stamp1, 'parser');
1586                         Core\L10n::setSessionVariable();
1587                         Core\L10n::setLangFromSession();
1588                 } else {
1589                         $_SESSION = [];
1590                         Core\Worker::executeIfIdle();
1591                 }
1592
1593                 // ZRL
1594                 if (!empty($_GET['zrl']) && $this->getMode()->isNormal()) {
1595                         $this->query_string = Model\Profile::stripZrls($this->query_string);
1596                         if (!local_user()) {
1597                                 // Only continue when the given profile link seems valid
1598                                 // Valid profile links contain a path with "/profile/" and no query parameters
1599                                 if ((parse_url($_GET['zrl'], PHP_URL_QUERY) == "") &&
1600                                         strstr(parse_url($_GET['zrl'], PHP_URL_PATH), "/profile/")) {
1601                                         if (defaults($_SESSION, "visitor_home", "") != $_GET["zrl"]) {
1602                                                 $_SESSION['my_url'] = $_GET['zrl'];
1603                                                 $_SESSION['authenticated'] = 0;
1604                                         }
1605                                         Model\Profile::zrlInit($this);
1606                                 } else {
1607                                         // Someone came with an invalid parameter, maybe as a DDoS attempt
1608                                         // We simply stop processing here
1609                                         Core\Logger::log("Invalid ZRL parameter " . $_GET['zrl'], Core\Logger::DEBUG);
1610                                         Core\System::httpExit(403, ['title' => '403 Forbidden']);
1611                                 }
1612                         }
1613                 }
1614
1615                 if (!empty($_GET['owt']) && $this->getMode()->isNormal()) {
1616                         $token = $_GET['owt'];
1617                         $this->query_string = Model\Profile::stripQueryParam($this->query_string, 'owt');
1618                         Model\Profile::openWebAuthInit($token);
1619                 }
1620
1621                 Module\Login::sessionAuth();
1622
1623                 if (empty($_SESSION['authenticated'])) {
1624                         header('X-Account-Management-Status: none');
1625                 }
1626
1627                 $_SESSION['sysmsg']       = defaults($_SESSION, 'sysmsg'      , []);
1628                 $_SESSION['sysmsg_info']  = defaults($_SESSION, 'sysmsg_info' , []);
1629                 $_SESSION['last_updated'] = defaults($_SESSION, 'last_updated', []);
1630
1631                 /*
1632                  * check_config() is responsible for running update scripts. These automatically
1633                  * update the DB schema whenever we push a new one out. It also checks to see if
1634                  * any addons have been added or removed and reacts accordingly.
1635                  */
1636
1637                 // in install mode, any url loads install module
1638                 // but we need "view" module for stylesheet
1639                 if ($this->getMode()->isInstall() && $this->module != 'view') {
1640                         $this->module = 'install';
1641                 } elseif (!$this->getMode()->has(App\Mode::MAINTENANCEDISABLED) && $this->module != 'view') {
1642                         $this->module = 'maintenance';
1643                 } else {
1644                         $this->checkURL();
1645                         check_db(false);
1646                         Core\Addon::loadAddons();
1647                         Core\Hook::loadHooks();
1648                 }
1649
1650                 $this->page = [
1651                         'aside' => '',
1652                         'bottom' => '',
1653                         'content' => '',
1654                         'footer' => '',
1655                         'htmlhead' => '',
1656                         'nav' => '',
1657                         'page_title' => '',
1658                         'right_aside' => '',
1659                         'template' => '',
1660                         'title' => ''
1661                 ];
1662
1663                 if (strlen($this->module)) {
1664                         // Compatibility with the Android Diaspora client
1665                         if ($this->module == 'stream') {
1666                                 $this->internalRedirect('network?f=&order=post');
1667                         }
1668
1669                         if ($this->module == 'conversations') {
1670                                 $this->internalRedirect('message');
1671                         }
1672
1673                         if ($this->module == 'commented') {
1674                                 $this->internalRedirect('network?f=&order=comment');
1675                         }
1676
1677                         if ($this->module == 'liked') {
1678                                 $this->internalRedirect('network?f=&order=comment');
1679                         }
1680
1681                         if ($this->module == 'activity') {
1682                                 $this->internalRedirect('network/?f=&conv=1');
1683                         }
1684
1685                         if (($this->module == 'status_messages') && ($this->cmd == 'status_messages/new')) {
1686                                 $this->internalRedirect('bookmarklet');
1687                         }
1688
1689                         if (($this->module == 'user') && ($this->cmd == 'user/edit')) {
1690                                 $this->internalRedirect('settings');
1691                         }
1692
1693                         if (($this->module == 'tag_followings') && ($this->cmd == 'tag_followings/manage')) {
1694                                 $this->internalRedirect('search');
1695                         }
1696
1697                         // Compatibility with the Firefox App
1698                         if (($this->module == "users") && ($this->cmd == "users/sign_in")) {
1699                                 $this->module = "login";
1700                         }
1701
1702                         $privateapps = Core\Config::get('config', 'private_addons', false);
1703                         if (Core\Addon::isEnabled($this->module) && file_exists("addon/{$this->module}/{$this->module}.php")) {
1704                                 //Check if module is an app and if public access to apps is allowed or not
1705                                 if ((!local_user()) && Core\Hook::isAddonApp($this->module) && $privateapps) {
1706                                         info(Core\L10n::t("You must be logged in to use addons. "));
1707                                 } else {
1708                                         include_once "addon/{$this->module}/{$this->module}.php";
1709                                         if (function_exists($this->module . '_module')) {
1710                                                 LegacyModule::setModuleFile("addon/{$this->module}/{$this->module}.php");
1711                                                 $this->module_class = 'Friendica\\LegacyModule';
1712                                                 $this->module_loaded = true;
1713                                         }
1714                                 }
1715                         }
1716
1717                         // Controller class routing
1718                         if (! $this->module_loaded && class_exists('Friendica\\Module\\' . ucfirst($this->module))) {
1719                                 $this->module_class = 'Friendica\\Module\\' . ucfirst($this->module);
1720                                 $this->module_loaded = true;
1721                         }
1722
1723                         /* If not, next look for a 'standard' program module in the 'mod' directory
1724                          * We emulate a Module class through the LegacyModule class
1725                          */
1726                         if (! $this->module_loaded && file_exists("mod/{$this->module}.php")) {
1727                                 LegacyModule::setModuleFile("mod/{$this->module}.php");
1728                                 $this->module_class = 'Friendica\\LegacyModule';
1729                                 $this->module_loaded = true;
1730                         }
1731
1732                         /* The URL provided does not resolve to a valid module.
1733                          *
1734                          * On Dreamhost sites, quite often things go wrong for no apparent reason and they send us to '/internal_error.html'.
1735                          * We don't like doing this, but as it occasionally accounts for 10-20% or more of all site traffic -
1736                          * 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
1737                          * this will often succeed and eventually do the right thing.
1738                          *
1739                          * Otherwise we are going to emit a 404 not found.
1740                          */
1741                         if (! $this->module_loaded) {
1742                                 // Stupid browser tried to pre-fetch our Javascript img template. Don't log the event or return anything - just quietly exit.
1743                                 if (!empty($_SERVER['QUERY_STRING']) && preg_match('/{[0-9]}/', $_SERVER['QUERY_STRING']) !== 0) {
1744                                         exit();
1745                                 }
1746
1747                                 if (!empty($_SERVER['QUERY_STRING']) && ($_SERVER['QUERY_STRING'] === 'q=internal_error.html') && isset($dreamhost_error_hack)) {
1748                                         Core\Logger::log('index.php: dreamhost_error_hack invoked. Original URI =' . $_SERVER['REQUEST_URI']);
1749                                         $this->internalRedirect($_SERVER['REQUEST_URI']);
1750                                 }
1751
1752                                 Core\Logger::log('index.php: page not found: ' . $_SERVER['REQUEST_URI'] . ' ADDRESS: ' . $_SERVER['REMOTE_ADDR'] . ' QUERY: ' . $_SERVER['QUERY_STRING'], Core\Logger::DEBUG);
1753
1754                                 header($_SERVER["SERVER_PROTOCOL"] . ' 404 ' . Core\L10n::t('Not Found'));
1755                                 $tpl = Core\Renderer::getMarkupTemplate("404.tpl");
1756                                 $this->page['content'] = Core\Renderer::replaceMacros($tpl, [
1757                                         '$message' =>  Core\L10n::t('Page not found.')
1758                                 ]);
1759                         }
1760                 }
1761
1762                 // Load current theme info
1763                 $theme_info_file = 'view/theme/' . $this->getCurrentTheme() . '/theme.php';
1764                 if (file_exists($theme_info_file)) {
1765                         require_once $theme_info_file;
1766                 }
1767
1768                 // initialise content region
1769                 if ($this->getMode()->isNormal()) {
1770                         Core\Addon::callHooks('page_content_top', $this->page['content']);
1771                 }
1772
1773                 // Call module functions
1774                 if ($this->module_loaded) {
1775                         $this->page['page_title'] = $this->module;
1776                         $placeholder = '';
1777
1778                         Core\Addon::callHooks($this->module . '_mod_init', $placeholder);
1779
1780                         call_user_func([$this->module_class, 'init']);
1781
1782                         // "rawContent" is especially meant for technical endpoints.
1783                         // This endpoint doesn't need any theme initialization or other comparable stuff.
1784                         if (!$this->error) {
1785                                 call_user_func([$this->module_class, 'rawContent']);
1786                         }
1787
1788                         if (function_exists(str_replace('-', '_', $this->getCurrentTheme()) . '_init')) {
1789                                 $func = str_replace('-', '_', $this->getCurrentTheme()) . '_init';
1790                                 $func($this);
1791                         }
1792
1793                         if (! $this->error && $_SERVER['REQUEST_METHOD'] === 'POST') {
1794                                 Core\Addon::callHooks($this->module . '_mod_post', $_POST);
1795                                 call_user_func([$this->module_class, 'post']);
1796                         }
1797
1798                         if (! $this->error) {
1799                                 Core\Addon::callHooks($this->module . '_mod_afterpost', $placeholder);
1800                                 call_user_func([$this->module_class, 'afterpost']);
1801                         }
1802
1803                         if (! $this->error) {
1804                                 $arr = ['content' => $this->page['content']];
1805                                 Core\Addon::callHooks($this->module . '_mod_content', $arr);
1806                                 $this->page['content'] = $arr['content'];
1807                                 $arr = ['content' => call_user_func([$this->module_class, 'content'])];
1808                                 Core\Addon::callHooks($this->module . '_mod_aftercontent', $arr);
1809                                 $this->page['content'] .= $arr['content'];
1810                         }
1811
1812                         if (function_exists(str_replace('-', '_', $this->getCurrentTheme()) . '_content_loaded')) {
1813                                 $func = str_replace('-', '_', $this->getCurrentTheme()) . '_content_loaded';
1814                                 $func($this);
1815                         }
1816                 }
1817
1818                 /* Create the page head after setting the language
1819                  * and getting any auth credentials.
1820                  *
1821                  * Moved initHead() and initFooter() to after
1822                  * all the module functions have executed so that all
1823                  * theme choices made by the modules can take effect.
1824                  */
1825                 $this->initHead();
1826
1827                 /* Build the page ending -- this is stuff that goes right before
1828                  * the closing </body> tag
1829                  */
1830                 $this->initFooter();
1831
1832                 /* now that we've been through the module content, see if the page reported
1833                  * a permission problem and if so, a 403 response would seem to be in order.
1834                  */
1835                 if (stristr(implode("", $_SESSION['sysmsg']), Core\L10n::t('Permission denied'))) {
1836                         header($_SERVER["SERVER_PROTOCOL"] . ' 403 ' . Core\L10n::t('Permission denied.'));
1837                 }
1838
1839                 // Report anything which needs to be communicated in the notification area (before the main body)
1840                 Core\Addon::callHooks('page_end', $this->page['content']);
1841
1842                 // Add the navigation (menu) template
1843                 if ($this->module != 'install' && $this->module != 'maintenance') {
1844                         $this->page['htmlhead'] .= Core\Renderer::replaceMacros(Core\Renderer::getMarkupTemplate('nav_head.tpl'), []);
1845                         $this->page['nav']       = Content\Nav::build($this);
1846                 }
1847
1848                 // Build the page - now that we have all the components
1849                 if (isset($_GET["mode"]) && (($_GET["mode"] == "raw") || ($_GET["mode"] == "minimal"))) {
1850                         $doc = new DOMDocument();
1851
1852                         $target = new DOMDocument();
1853                         $target->loadXML("<root></root>");
1854
1855                         $content = mb_convert_encoding($this->page["content"], 'HTML-ENTITIES', "UTF-8");
1856
1857                         /// @TODO one day, kill those error-surpressing @ stuff, or PHP should ban it
1858                         @$doc->loadHTML($content);
1859
1860                         $xpath = new DOMXPath($doc);
1861
1862                         $list = $xpath->query("//*[contains(@id,'tread-wrapper-')]");  /* */
1863
1864                         foreach ($list as $item) {
1865                                 $item = $target->importNode($item, true);
1866
1867                                 // And then append it to the target
1868                                 $target->documentElement->appendChild($item);
1869                         }
1870                 }
1871
1872                 if (isset($_GET["mode"]) && ($_GET["mode"] == "raw")) {
1873                         header("Content-type: text/html; charset=utf-8");
1874
1875                         echo substr($target->saveHTML(), 6, -8);
1876
1877                         exit();
1878                 }
1879
1880                 $page    = $this->page;
1881                 $profile = $this->profile;
1882
1883                 header("X-Friendica-Version: " . FRIENDICA_VERSION);
1884                 header("Content-type: text/html; charset=utf-8");
1885
1886                 if (Core\Config::get('system', 'hsts') && (Core\Config::get('system', 'ssl_policy') == SSL_POLICY_FULL)) {
1887                         header("Strict-Transport-Security: max-age=31536000");
1888                 }
1889
1890                 // Some security stuff
1891                 header('X-Content-Type-Options: nosniff');
1892                 header('X-XSS-Protection: 1; mode=block');
1893                 header('X-Permitted-Cross-Domain-Policies: none');
1894                 header('X-Frame-Options: sameorigin');
1895
1896                 // Things like embedded OSM maps don't work, when this is enabled
1897                 // 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'");
1898
1899                 /* We use $_GET["mode"] for special page templates. So we will check if we have
1900                  * to load another page template than the default one.
1901                  * The page templates are located in /view/php/ or in the theme directory.
1902                  */
1903                 if (isset($_GET["mode"])) {
1904                         $template = Core\Theme::getPathForFile($_GET["mode"] . '.php');
1905                 }
1906
1907                 // If there is no page template use the default page template
1908                 if (empty($template)) {
1909                         $template = Core\Theme::getPathForFile("default.php");
1910                 }
1911
1912                 // Theme templates expect $a as an App instance
1913                 $a = $this;
1914
1915                 // Used as is in view/php/default.php
1916                 $lang = Core\L10n::getCurrentLang();
1917
1918                 /// @TODO Looks unsafe (remote-inclusion), is maybe not but Core\Theme::getPathForFile() uses file_exists() but does not escape anything
1919                 require_once $template;
1920         }
1921
1922         /**
1923          * Redirects to another module relative to the current Friendica base.
1924          * If you want to redirect to a external URL, use System::externalRedirectTo()
1925          *
1926          * @param string $toUrl The destination URL (Default is empty, which is the default page of the Friendica node)
1927          * @param bool $ssl if true, base URL will try to get called with https:// (works just for relative paths)
1928          *
1929          * @throws InternalServerErrorException In Case the given URL is not relative to the Friendica node
1930          */
1931         public function internalRedirect($toUrl = '', $ssl = false)
1932         {
1933                 if (filter_var($toUrl, FILTER_VALIDATE_URL)) {
1934                         throw new InternalServerErrorException("'$toUrl is not a relative path, please use System::externalRedirectTo");
1935                 }
1936
1937                 $redirectTo = $this->getBaseURL($ssl) . '/' . ltrim($toUrl, '/');
1938                 Core\System::externalRedirect($redirectTo);
1939         }
1940
1941         /**
1942          * Automatically redirects to relative or absolute URL
1943          * Should only be used if it isn't clear if the URL is either internal or external
1944          *
1945          * @param string $toUrl The target URL
1946          *
1947          */
1948         public function redirect($toUrl)
1949         {
1950                 if (filter_var($toUrl, FILTER_VALIDATE_URL)) {
1951                         Core\System::externalRedirect($toUrl);
1952                 } else {
1953                         $this->internalRedirect($toUrl);
1954                 }
1955         }
1956 }