]> git.mxchange.org Git - friendica.git/blob - src/App.php
Merge pull request #5773 from MrPetovan/task/rewrite-js-hooks
[friendica.git] / src / App.php
1 <?php
2 /**
3  * @file src/App.php
4  */
5 namespace Friendica;
6
7 use Detection\MobileDetect;
8 use Exception;
9 use Friendica\Core\Config;
10 use Friendica\Core\L10n;
11 use Friendica\Core\PConfig;
12 use Friendica\Core\System;
13 use Friendica\Database\DBA;
14
15 require_once 'boot.php';
16 require_once 'include/dba.php';
17 require_once 'include/text.php';
18
19 /**
20  *
21  * class: App
22  *
23  * @brief Our main application structure for the life of this page.
24  *
25  * Primarily deals with the URL that got us here
26  * and tries to make some sense of it, and
27  * stores our page contents and config storage
28  * and anything else that might need to be passed around
29  * before we spit the page out.
30  *
31  */
32 class App
33 {
34         const MODE_LOCALCONFIGPRESENT = 1;
35         const MODE_DBAVAILABLE = 2;
36         const MODE_DBCONFIGAVAILABLE = 4;
37         const MODE_MAINTENANCEDISABLED = 8;
38
39         /**
40          * @deprecated since version 2008.08 Use App->isInstallMode() instead to check for install mode.
41          */
42         const MODE_INSTALL = 0;
43
44         /**
45          * @deprecated since version 2008.08 Use the precise mode constant to check for a specific capability instead.
46          */
47         const MODE_NORMAL = App::MODE_LOCALCONFIGPRESENT | App::MODE_DBAVAILABLE | App::MODE_DBCONFIGAVAILABLE | App::MODE_MAINTENANCEDISABLED;
48
49         public $module_loaded = false;
50         public $module_class = null;
51         public $query_string = '';
52         public $config = [];
53         public $page = [];
54         public $pager = [];
55         public $page_offset;
56         public $profile;
57         public $profile_uid;
58         public $user;
59         public $cid;
60         public $contact;
61         public $contacts;
62         public $page_contact;
63         public $content;
64         public $data = [];
65         public $error = false;
66         public $cmd = '';
67         public $argv;
68         public $argc;
69         public $module;
70         public $mode = App::MODE_INSTALL;
71         public $strings;
72         public $basepath;
73         public $urlpath;
74         public $hooks = [];
75         public $timezone;
76         public $interactive = true;
77         public $addons;
78         public $addons_admin = [];
79         public $apps = [];
80         public $identities;
81         public $is_mobile = false;
82         public $is_tablet = false;
83         public $is_friendica_app;
84         public $performance = [];
85         public $callstack = [];
86         public $theme_info = [];
87         public $backend = true;
88         public $nav_sel;
89         public $category;
90         // Allow themes to control internal parameters
91         // by changing App values in theme.php
92
93         public $sourcename = '';
94         public $videowidth = 425;
95         public $videoheight = 350;
96         public $force_max_items = 0;
97         public $theme_events_in_profile = true;
98
99         public $stylesheets = [];
100         public $footerScripts = [];
101
102         /**
103          * Register a stylesheet file path to be included in the <head> tag of every page.
104          * Inclusion is done in App->initHead().
105          * The path can be absolute or relative to the Friendica installation base folder.
106          *
107          * @see App->initHead()
108          *
109          * @param string $path
110          */
111         public function registerStylesheet($path)
112         {
113                 $url = str_replace($this->get_basepath() . DIRECTORY_SEPARATOR, '', $path);
114
115                 $this->stylesheets[] = trim($url, '/');
116         }
117
118         /**
119          * Register a javascript file path to be included in the <footer> tag of every page.
120          * Inclusion is done in App->initFooter().
121          * The path can be absolute or relative to the Friendica installation base folder.
122          *
123          * @see App->initFooter()
124          *
125          * @param string $path
126          */
127         public function registerFooterScript($path)
128         {
129                 $url = str_replace($this->get_basepath() . DIRECTORY_SEPARATOR, '', $path);
130
131                 $this->footerScripts[] = trim($url, '/');
132         }
133
134         /**
135          * @brief An array for all theme-controllable parameters
136          *
137          * Mostly unimplemented yet. Only options 'template_engine' and
138          * beyond are used.
139          */
140         public $theme = [
141                 'sourcename' => '',
142                 'videowidth' => 425,
143                 'videoheight' => 350,
144                 'force_max_items' => 0,
145                 'stylesheet' => '',
146                 'template_engine' => 'smarty3',
147         ];
148
149         /**
150          * @brief An array of registered template engines ('name'=>'class name')
151          */
152         public $template_engines = [];
153
154         /**
155          * @brief An array of instanced template engines ('name'=>'instance')
156          */
157         public $template_engine_instance = [];
158         public $process_id;
159         public $queue;
160         private $ldelim = [
161                 'internal' => '',
162                 'smarty3' => '{{'
163         ];
164         private $rdelim = [
165                 'internal' => '',
166                 'smarty3' => '}}'
167         ];
168         private $scheme;
169         private $hostname;
170         private $curl_code;
171         private $curl_content_type;
172         private $curl_headers;
173
174         /**
175          * @brief App constructor.
176          *
177          * @param string $basepath Path to the app base folder
178          *
179          * @throws Exception if the Basepath is not usable
180          */
181         public function __construct($basepath)
182         {
183                 if (!static::directory_usable($basepath, false)) {
184                         throw new Exception('Basepath ' . $basepath . ' isn\'t usable.');
185                 }
186
187                 BaseObject::setApp($this);
188
189                 $this->basepath = rtrim($basepath, DIRECTORY_SEPARATOR);
190
191                 $this->performance['start'] = microtime(true);
192                 $this->performance['database'] = 0;
193                 $this->performance['database_write'] = 0;
194                 $this->performance['cache'] = 0;
195                 $this->performance['cache_write'] = 0;
196                 $this->performance['network'] = 0;
197                 $this->performance['file'] = 0;
198                 $this->performance['rendering'] = 0;
199                 $this->performance['parser'] = 0;
200                 $this->performance['marktime'] = 0;
201                 $this->performance['markstart'] = microtime(true);
202
203                 $this->callstack['database'] = [];
204                 $this->callstack['database_write'] = [];
205                 $this->callstack['cache'] = [];
206                 $this->callstack['cache_write'] = [];
207                 $this->callstack['network'] = [];
208                 $this->callstack['file'] = [];
209                 $this->callstack['rendering'] = [];
210                 $this->callstack['parser'] = [];
211
212                 $this->reload();
213
214                 set_time_limit(0);
215
216                 // This has to be quite large to deal with embedded private photos
217                 ini_set('pcre.backtrack_limit', 500000);
218
219                 $this->scheme = 'http';
220
221                 if ((x($_SERVER, 'HTTPS') && $_SERVER['HTTPS']) ||
222                         (x($_SERVER, 'HTTP_FORWARDED') && preg_match('/proto=https/', $_SERVER['HTTP_FORWARDED'])) ||
223                         (x($_SERVER, 'HTTP_X_FORWARDED_PROTO') && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https') ||
224                         (x($_SERVER, 'HTTP_X_FORWARDED_SSL') && $_SERVER['HTTP_X_FORWARDED_SSL'] == 'on') ||
225                         (x($_SERVER, 'FRONT_END_HTTPS') && $_SERVER['FRONT_END_HTTPS'] == 'on') ||
226                         (x($_SERVER, 'SERVER_PORT') && (intval($_SERVER['SERVER_PORT']) == 443)) // XXX: reasonable assumption, but isn't this hardcoding too much?
227                 ) {
228                         $this->scheme = 'https';
229                 }
230
231                 if (x($_SERVER, 'SERVER_NAME')) {
232                         $this->hostname = $_SERVER['SERVER_NAME'];
233
234                         if (x($_SERVER, 'SERVER_PORT') && $_SERVER['SERVER_PORT'] != 80 && $_SERVER['SERVER_PORT'] != 443) {
235                                 $this->hostname .= ':' . $_SERVER['SERVER_PORT'];
236                         }
237                 }
238
239                 set_include_path(
240                         get_include_path() . PATH_SEPARATOR
241                         . $this->basepath . DIRECTORY_SEPARATOR . 'include' . PATH_SEPARATOR
242                         . $this->basepath . DIRECTORY_SEPARATOR . 'library' . PATH_SEPARATOR
243                         . $this->basepath);
244
245                 if ((x($_SERVER, 'QUERY_STRING')) && substr($_SERVER['QUERY_STRING'], 0, 9) === 'pagename=') {
246                         $this->query_string = substr($_SERVER['QUERY_STRING'], 9);
247                 } elseif ((x($_SERVER, 'QUERY_STRING')) && substr($_SERVER['QUERY_STRING'], 0, 2) === 'q=') {
248                         $this->query_string = substr($_SERVER['QUERY_STRING'], 2);
249                 }
250
251                 // removing trailing / - maybe a nginx problem
252                 $this->query_string = ltrim($this->query_string, '/');
253
254                 if (!empty($_GET['pagename'])) {
255                         $this->cmd = trim($_GET['pagename'], '/\\');
256                 } elseif (!empty($_GET['q'])) {
257                         $this->cmd = trim($_GET['q'], '/\\');
258                 }
259
260                 // fix query_string
261                 $this->query_string = str_replace($this->cmd . '&', $this->cmd . '?', $this->query_string);
262
263                 // unix style "homedir"
264                 if (substr($this->cmd, 0, 1) === '~') {
265                         $this->cmd = 'profile/' . substr($this->cmd, 1);
266                 }
267
268                 // Diaspora style profile url
269                 if (substr($this->cmd, 0, 2) === 'u/') {
270                         $this->cmd = 'profile/' . substr($this->cmd, 2);
271                 }
272
273                 /*
274                  * Break the URL path into C style argc/argv style arguments for our
275                  * modules. Given "http://example.com/module/arg1/arg2", $this->argc
276                  * will be 3 (integer) and $this->argv will contain:
277                  *   [0] => 'module'
278                  *   [1] => 'arg1'
279                  *   [2] => 'arg2'
280                  *
281                  *
282                  * There will always be one argument. If provided a naked domain
283                  * URL, $this->argv[0] is set to "home".
284                  */
285
286                 $this->argv = explode('/', $this->cmd);
287                 $this->argc = count($this->argv);
288                 if ((array_key_exists('0', $this->argv)) && strlen($this->argv[0])) {
289                         $this->module = str_replace('.', '_', $this->argv[0]);
290                         $this->module = str_replace('-', '_', $this->module);
291                 } else {
292                         $this->argc = 1;
293                         $this->argv = ['home'];
294                         $this->module = 'home';
295                 }
296
297                 // See if there is any page number information, and initialise pagination
298                 $this->pager['page'] = ((x($_GET, 'page') && intval($_GET['page']) > 0) ? intval($_GET['page']) : 1);
299                 $this->pager['itemspage'] = 50;
300                 $this->pager['start'] = ($this->pager['page'] * $this->pager['itemspage']) - $this->pager['itemspage'];
301
302                 if ($this->pager['start'] < 0) {
303                         $this->pager['start'] = 0;
304                 }
305                 $this->pager['total'] = 0;
306
307                 // Detect mobile devices
308                 $mobile_detect = new MobileDetect();
309                 $this->is_mobile = $mobile_detect->isMobile();
310                 $this->is_tablet = $mobile_detect->isTablet();
311
312                 // Friendica-Client
313                 $this->is_friendica_app = isset($_SERVER['HTTP_USER_AGENT']) && $_SERVER['HTTP_USER_AGENT'] == 'Apache-HttpClient/UNAVAILABLE (java 1.4)';
314
315                 // Register template engines
316                 $this->register_template_engine('Friendica\Render\FriendicaSmartyEngine');
317         }
318
319         /**
320          * Reloads the whole app instance
321          */
322         public function reload()
323         {
324                 // The order of the following calls is important to ensure proper initialization
325                 $this->loadConfigFiles();
326
327                 $this->loadDatabase();
328
329                 $this->determineMode();
330
331                 $this->determineUrlPath();
332
333                 Config::load();
334
335                 if ($this->mode & self::MODE_DBAVAILABLE) {
336                         Core\Addon::loadHooks();
337
338                         $this->loadAddonConfig();
339                 }
340
341                 $this->loadDefaultTimezone();
342
343                 $this->page = [
344                         'aside' => '',
345                         'bottom' => '',
346                         'content' => '',
347                         'footer' => '',
348                         'htmlhead' => '',
349                         'nav' => '',
350                         'page_title' => '',
351                         'right_aside' => '',
352                         'template' => '',
353                         'title' => ''
354                 ];
355
356                 $this->process_id = System::processID('log');
357         }
358
359         /**
360          * Load the configuration files
361          *
362          * First loads the default value for all the configuration keys, then the legacy configuration files, then the
363          * expected local.ini.php
364          */
365         private function loadConfigFiles()
366         {
367                 $this->loadConfigFile($this->basepath . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'config.ini.php');
368                 $this->loadConfigFile($this->basepath . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'settings.ini.php');
369
370                 // Legacy .htconfig.php support
371                 if (file_exists($this->basepath . DIRECTORY_SEPARATOR . '.htpreconfig.php')) {
372                         $a = $this;
373                         include $this->basepath . DIRECTORY_SEPARATOR . '.htpreconfig.php';
374                 }
375
376                 // Legacy .htconfig.php support
377                 if (file_exists($this->basepath . DIRECTORY_SEPARATOR . '.htconfig.php')) {
378                         $a = $this;
379
380                         include $this->basepath . DIRECTORY_SEPARATOR . '.htconfig.php';
381
382                         $this->setConfigValue('database', 'hostname', $db_host);
383                         $this->setConfigValue('database', 'username', $db_user);
384                         $this->setConfigValue('database', 'password', $db_pass);
385                         $this->setConfigValue('database', 'database', $db_data);
386                         if (isset($a->config['system']['db_charset'])) {
387                                 $this->setConfigValue('database', 'charset', $a->config['system']['db_charset']);
388                         }
389
390                         unset($db_host, $db_user, $db_pass, $db_data);
391
392                         if (isset($default_timezone)) {
393                                 $this->setConfigValue('system', 'default_timezone', $default_timezone);
394                                 unset($default_timezone);
395                         }
396
397                         if (isset($pidfile)) {
398                                 $this->setConfigValue('system', 'pidfile', $pidfile);
399                                 unset($pidfile);
400                         }
401
402                         if (isset($lang)) {
403                                 $this->setConfigValue('system', 'language', $lang);
404                                 unset($lang);
405                         }
406                 }
407
408                 if (file_exists($this->basepath . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'local.ini.php')) {
409                         $this->loadConfigFile($this->basepath . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'local.ini.php', true);
410                 }
411         }
412
413         /**
414          * Tries to load the specified configuration file into the App->config array.
415          * Doesn't overwrite previously set values by default to prevent default config files to supersede DB Config.
416          *
417          * The config format is INI and the template for configuration files is the following:
418          *
419          * <?php return <<<INI
420          *
421          * [section]
422          * key = value
423          *
424          * INI;
425          * // Keep this line
426          *
427          * @param type $filepath
428          * @param bool $overwrite Force value overwrite if the config key already exists
429          * @throws Exception
430          */
431         public function loadConfigFile($filepath, $overwrite = false)
432         {
433                 if (!file_exists($filepath)) {
434                         throw new Exception('Error parsing non-existent config file ' . $filepath);
435                 }
436
437                 $contents = include($filepath);
438
439                 $config = parse_ini_string($contents, true, INI_SCANNER_TYPED);
440
441                 if ($config === false) {
442                         throw new Exception('Error parsing config file ' . $filepath);
443                 }
444
445                 foreach ($config as $category => $values) {
446                         foreach ($values as $key => $value) {
447                                 if ($overwrite) {
448                                         $this->setConfigValue($category, $key, $value);
449                                 } else {
450                                         $this->setDefaultConfigValue($category, $key, $value);
451                                 }
452                         }
453                 }
454         }
455
456         /**
457          * Loads addons configuration files
458          *
459          * First loads all activated addons default configuration throught the load_config hook, then load the local.ini.php
460          * again to overwrite potential local addon configuration.
461          */
462         private function loadAddonConfig()
463         {
464                 // Loads addons default config
465                 Core\Addon::callHooks('load_config');
466
467                 // Load the local addon config file to overwritten default addon config values
468                 if (file_exists($this->basepath . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'addon.ini.php')) {
469                         $this->loadConfigFile($this->basepath . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'addon.ini.php', true);
470                 }
471         }
472
473         /**
474          * Loads the default timezone
475          *
476          * Include support for legacy $default_timezone
477          *
478          * @global string $default_timezone
479          */
480         private function loadDefaultTimezone()
481         {
482                 if ($this->getConfigValue('system', 'default_timezone')) {
483                         $this->timezone = $this->getConfigValue('system', 'default_timezone');
484                 } else {
485                         global $default_timezone;
486                         $this->timezone = !empty($default_timezone) ? $default_timezone : 'UTC';
487                 }
488
489                 if ($this->timezone) {
490                         date_default_timezone_set($this->timezone);
491                 }
492         }
493
494         /**
495          * Figure out if we are running at the top of a domain or in a sub-directory and adjust accordingly
496          */
497         private function determineUrlPath()
498         {
499                 $this->urlpath = $this->getConfigValue('system', 'urlpath');
500
501                 /* SCRIPT_URL gives /path/to/friendica/module/parameter
502                  * QUERY_STRING gives pagename=module/parameter
503                  *
504                  * To get /path/to/friendica we perform dirname() for as many levels as there are slashes in the QUERY_STRING
505                  */
506                 if (!empty($_SERVER['SCRIPT_URL'])) {
507                         // Module
508                         if (!empty($_SERVER['QUERY_STRING'])) {
509                                 $path = trim(dirname($_SERVER['SCRIPT_URL'], substr_count(trim($_SERVER['QUERY_STRING'], '/'), '/') + 1), '/');
510                         } else {
511                                 // Root page
512                                 $path = trim($_SERVER['SCRIPT_URL'], '/');
513                         }
514
515                         if ($path && $path != $this->urlpath) {
516                                 $this->urlpath = $path;
517                         }
518                 }
519         }
520
521         /**
522          * Sets the App mode
523          *
524          * - App::MODE_INSTALL    : Either the database connection can't be established or the config table doesn't exist
525          * - App::MODE_MAINTENANCE: The maintenance mode has been set
526          * - App::MODE_NORMAL     : Normal run with all features enabled
527          *
528          * @return type
529          */
530         private function determineMode()
531         {
532                 $this->mode = 0;
533
534                 if (!file_exists($this->basepath . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'local.ini.php')
535                         && !file_exists($this->basepath . DIRECTORY_SEPARATOR . '.htconfig.php')) {
536                         return;
537                 }
538
539                 $this->mode |= App::MODE_LOCALCONFIGPRESENT;
540
541                 if (!DBA::connected()) {
542                         return;
543                 }
544
545                 $this->mode |= App::MODE_DBAVAILABLE;
546
547                 if (DBA::fetchFirst("SHOW TABLES LIKE 'config'") === false) {
548                         return;
549                 }
550
551                 $this->mode |= App::MODE_DBCONFIGAVAILABLE;
552
553                 if (Config::get('system', 'maintenance')) {
554                         return;
555                 }
556
557                 $this->mode |= App::MODE_MAINTENANCEDISABLED;
558         }
559
560         public function loadDatabase()
561         {
562                 if (DBA::connected()) {
563                         return;
564                 }
565
566                 $db_host = $this->getConfigValue('database', 'hostname');
567                 $db_user = $this->getConfigValue('database', 'username');
568                 $db_pass = $this->getConfigValue('database', 'password');
569                 $db_data = $this->getConfigValue('database', 'database');
570                 $charset = $this->getConfigValue('database', 'charset');
571
572                 // Use environment variables for mysql if they are set beforehand
573                 if (!empty(getenv('MYSQL_HOST'))
574                         && (!empty(getenv('MYSQL_USERNAME')) || !empty(getenv('MYSQL_USER')))
575                         && getenv('MYSQL_PASSWORD') !== false
576                         && !empty(getenv('MYSQL_DATABASE')))
577                 {
578                         $db_host = getenv('MYSQL_HOST');
579                         if (!empty(getenv('MYSQL_PORT'))) {
580                                 $db_host .= ':' . getenv('MYSQL_PORT');
581                         }
582                         if (!empty(getenv('MYSQL_USERNAME'))) {
583                                 $db_user = getenv('MYSQL_USERNAME');
584                         } else {
585                                 $db_user = getenv('MYSQL_USER');
586                         }
587                         $db_pass = (string) getenv('MYSQL_PASSWORD');
588                         $db_data = getenv('MYSQL_DATABASE');
589                 }
590
591                 $stamp1 = microtime(true);
592
593                 DBA::connect($db_host, $db_user, $db_pass, $db_data, $charset);
594                 unset($db_host, $db_user, $db_pass, $db_data, $charset);
595
596                 $this->save_timestamp($stamp1, 'network');
597         }
598
599         /**
600          * Install mode is when the local config file is missing or the DB schema hasn't been installed yet.
601          *
602          * @return bool
603          */
604         public function isInstallMode()
605         {
606                 return !($this->mode & App::MODE_LOCALCONFIGPRESENT) || !($this->mode & App::MODE_DBCONFIGAVAILABLE);
607         }
608
609         /**
610          * @brief Returns the base filesystem path of the App
611          *
612          * It first checks for the internal variable, then for DOCUMENT_ROOT and
613          * finally for PWD
614          *
615          * @return string
616          */
617         public function get_basepath()
618         {
619                 $basepath = $this->basepath;
620
621                 if (!$basepath) {
622                         $basepath = Config::get('system', 'basepath');
623                 }
624
625                 if (!$basepath && x($_SERVER, 'DOCUMENT_ROOT')) {
626                         $basepath = $_SERVER['DOCUMENT_ROOT'];
627                 }
628
629                 if (!$basepath && x($_SERVER, 'PWD')) {
630                         $basepath = $_SERVER['PWD'];
631                 }
632
633                 return self::realpath($basepath);
634         }
635
636         /**
637          * @brief Returns a normalized file path
638          *
639          * This is a wrapper for the "realpath" function.
640          * That function cannot detect the real path when some folders aren't readable.
641          * Since this could happen with some hosters we need to handle this.
642          *
643          * @param string $path The path that is about to be normalized
644          * @return string normalized path - when possible
645          */
646         public static function realpath($path)
647         {
648                 $normalized = realpath($path);
649
650                 if (!is_bool($normalized)) {
651                         return $normalized;
652                 } else {
653                         return $path;
654                 }
655         }
656
657         public function get_scheme()
658         {
659                 return $this->scheme;
660         }
661
662         /**
663          * @brief Retrieves the Friendica instance base URL
664          *
665          * This function assembles the base URL from multiple parts:
666          * - Protocol is determined either by the request or a combination of
667          * system.ssl_policy and the $ssl parameter.
668          * - Host name is determined either by system.hostname or inferred from request
669          * - Path is inferred from SCRIPT_NAME
670          *
671          * Note: $ssl parameter value doesn't directly correlate with the resulting protocol
672          *
673          * @param bool $ssl Whether to append http or https under SSL_POLICY_SELFSIGN
674          * @return string Friendica server base URL
675          */
676         public function get_baseurl($ssl = false)
677         {
678                 $scheme = $this->scheme;
679
680                 if (Config::get('system', 'ssl_policy') == SSL_POLICY_FULL) {
681                         $scheme = 'https';
682                 }
683
684                 //      Basically, we have $ssl = true on any links which can only be seen by a logged in user
685                 //      (and also the login link). Anything seen by an outsider will have it turned off.
686
687                 if (Config::get('system', 'ssl_policy') == SSL_POLICY_SELFSIGN) {
688                         if ($ssl) {
689                                 $scheme = 'https';
690                         } else {
691                                 $scheme = 'http';
692                         }
693                 }
694
695                 if (Config::get('config', 'hostname') != '') {
696                         $this->hostname = Config::get('config', 'hostname');
697                 }
698
699                 return $scheme . '://' . $this->hostname . (!empty($this->urlpath) ? '/' . $this->urlpath : '' );
700         }
701
702         /**
703          * @brief Initializes the baseurl components
704          *
705          * Clears the baseurl cache to prevent inconsistencies
706          *
707          * @param string $url
708          */
709         public function set_baseurl($url)
710         {
711                 $parsed = @parse_url($url);
712                 $hostname = '';
713
714                 if (x($parsed)) {
715                         if (!empty($parsed['scheme'])) {
716                                 $this->scheme = $parsed['scheme'];
717                         }
718
719                         if (!empty($parsed['host'])) {
720                                 $hostname = $parsed['host'];
721                         }
722
723                         if (x($parsed, 'port')) {
724                                 $hostname .= ':' . $parsed['port'];
725                         }
726                         if (x($parsed, 'path')) {
727                                 $this->urlpath = trim($parsed['path'], '\\/');
728                         }
729
730                         if (file_exists($this->basepath . DIRECTORY_SEPARATOR . '.htpreconfig.php')) {
731                                 include $this->basepath . DIRECTORY_SEPARATOR . '.htpreconfig.php';
732                         }
733
734                         if (Config::get('config', 'hostname') != '') {
735                                 $this->hostname = Config::get('config', 'hostname');
736                         }
737
738                         if (!isset($this->hostname) || ($this->hostname == '')) {
739                                 $this->hostname = $hostname;
740                         }
741                 }
742         }
743
744         public function get_hostname()
745         {
746                 if (Config::get('config', 'hostname') != '') {
747                         $this->hostname = Config::get('config', 'hostname');
748                 }
749
750                 return $this->hostname;
751         }
752
753         public function get_path()
754         {
755                 return $this->urlpath;
756         }
757
758         public function set_pager_total($n)
759         {
760                 $this->pager['total'] = intval($n);
761         }
762
763         public function set_pager_itemspage($n)
764         {
765                 $this->pager['itemspage'] = ((intval($n) > 0) ? intval($n) : 0);
766                 $this->pager['start'] = ($this->pager['page'] * $this->pager['itemspage']) - $this->pager['itemspage'];
767         }
768
769         public function set_pager_page($n)
770         {
771                 $this->pager['page'] = $n;
772                 $this->pager['start'] = ($this->pager['page'] * $this->pager['itemspage']) - $this->pager['itemspage'];
773         }
774
775         /**
776          * Initializes App->page['htmlhead'].
777          *
778          * Includes:
779          * - Page title
780          * - Favicons
781          * - Registered stylesheets (through App->registerStylesheet())
782          * - Infinite scroll data
783          * - head.tpl template
784          */
785         public function initHead()
786         {
787                 $interval = ((local_user()) ? PConfig::get(local_user(), 'system', 'update_interval') : 40000);
788
789                 // If the update is 'deactivated' set it to the highest integer number (~24 days)
790                 if ($interval < 0) {
791                         $interval = 2147483647;
792                 }
793
794                 if ($interval < 10000) {
795                         $interval = 40000;
796                 }
797
798                 // compose the page title from the sitename and the
799                 // current module called
800                 if (!$this->module == '') {
801                         $this->page['title'] = $this->config['sitename'] . ' (' . $this->module . ')';
802                 } else {
803                         $this->page['title'] = $this->config['sitename'];
804                 }
805
806                 if (!empty($this->theme['stylesheet'])) {
807                         $stylesheet = $this->theme['stylesheet'];
808                 } else {
809                         $stylesheet = $this->getCurrentThemeStylesheetPath();
810                 }
811
812                 $this->registerStylesheet($stylesheet);
813
814                 $shortcut_icon = Config::get('system', 'shortcut_icon');
815                 if ($shortcut_icon == '') {
816                         $shortcut_icon = 'images/friendica-32.png';
817                 }
818
819                 $touch_icon = Config::get('system', 'touch_icon');
820                 if ($touch_icon == '') {
821                         $touch_icon = 'images/friendica-128.png';
822                 }
823
824                 // get data wich is needed for infinite scroll on the network page
825                 $infinite_scroll = infinite_scroll_data($this->module);
826
827                 Core\Addon::callHooks('head', $this->page['htmlhead']);
828
829                 $tpl = get_markup_template('head.tpl');
830                 /* put the head template at the beginning of page['htmlhead']
831                  * since the code added by the modules frequently depends on it
832                  * being first
833                  */
834                 $this->page['htmlhead'] = replace_macros($tpl, [
835                         '$baseurl'         => $this->get_baseurl(),
836                         '$local_user'      => local_user(),
837                         '$generator'       => 'Friendica' . ' ' . FRIENDICA_VERSION,
838                         '$delitem'         => L10n::t('Delete this item?'),
839                         '$showmore'        => L10n::t('show more'),
840                         '$showfewer'       => L10n::t('show fewer'),
841                         '$update_interval' => $interval,
842                         '$shortcut_icon'   => $shortcut_icon,
843                         '$touch_icon'      => $touch_icon,
844                         '$infinite_scroll' => $infinite_scroll,
845                         '$block_public'    => intval(Config::get('system', 'block_public')),
846                         '$stylesheets'     => $this->stylesheets,
847                 ]) . $this->page['htmlhead'];
848         }
849
850         /**
851          * Initializes App->page['footer'].
852          *
853          * Includes:
854          * - Javascript homebase
855          * - Mobile toggle link
856          * - Registered footer scripts (through App->registerFooterScript())
857          * - footer.tpl template
858          */
859         public function initFooter()
860         {
861                 // If you're just visiting, let javascript take you home
862                 if (!empty($_SESSION['visitor_home'])) {
863                         $homebase = $_SESSION['visitor_home'];
864                 } elseif (local_user()) {
865                         $homebase = 'profile/' . $this->user['nickname'];
866                 }
867
868                 if (isset($homebase)) {
869                         $this->page['footer'] .= '<script>var homebase="' . $homebase . '";</script>' . "\n";
870                 }
871
872                 /*
873                  * Add a "toggle mobile" link if we're using a mobile device
874                  */
875                 if ($this->is_mobile || $this->is_tablet) {
876                         if (isset($_SESSION['show-mobile']) && !$_SESSION['show-mobile']) {
877                                 $link = 'toggle_mobile?address=' . curPageURL();
878                         } else {
879                                 $link = 'toggle_mobile?off=1&address=' . curPageURL();
880                         }
881                         $this->page['footer'] .= replace_macros(get_markup_template("toggle_mobile_footer.tpl"), [
882                                 '$toggle_link' => $link,
883                                 '$toggle_text' => Core\L10n::t('toggle mobile')
884                         ]);
885                 }
886
887                 Core\Addon::callHooks('footer', $this->page['footer']);
888
889                 $tpl = get_markup_template('footer.tpl');
890                 $this->page['footer'] = replace_macros($tpl, [
891                         '$baseurl' => $this->get_baseurl(),
892                         '$footerScripts' => $this->footerScripts,
893                 ]) . $this->page['footer'];
894         }
895
896         public function set_curl_code($code)
897         {
898                 $this->curl_code = $code;
899         }
900
901         public function get_curl_code()
902         {
903                 return $this->curl_code;
904         }
905
906         public function set_curl_content_type($content_type)
907         {
908                 $this->curl_content_type = $content_type;
909         }
910
911         public function get_curl_content_type()
912         {
913                 return $this->curl_content_type;
914         }
915
916         public function set_curl_headers($headers)
917         {
918                 $this->curl_headers = $headers;
919         }
920
921         public function get_curl_headers()
922         {
923                 return $this->curl_headers;
924         }
925
926         /**
927          * @brief Removes the base url from an url. This avoids some mixed content problems.
928          *
929          * @param string $orig_url
930          *
931          * @return string The cleaned url
932          */
933         public function remove_baseurl($orig_url)
934         {
935                 // Remove the hostname from the url if it is an internal link
936                 $nurl = normalise_link($orig_url);
937                 $base = normalise_link($this->get_baseurl());
938                 $url = str_replace($base . '/', '', $nurl);
939
940                 // if it is an external link return the orignal value
941                 if ($url == normalise_link($orig_url)) {
942                         return $orig_url;
943                 } else {
944                         return $url;
945                 }
946         }
947
948         /**
949          * @brief Register template engine class
950          *
951          * @param string $class
952          */
953         private function register_template_engine($class)
954         {
955                 $v = get_class_vars($class);
956                 if (x($v, 'name')) {
957                         $name = $v['name'];
958                         $this->template_engines[$name] = $class;
959                 } else {
960                         echo "template engine <tt>$class</tt> cannot be registered without a name.\n";
961                         die();
962                 }
963         }
964
965         /**
966          * @brief Return template engine instance.
967          *
968          * If $name is not defined, return engine defined by theme,
969          * or default
970          *
971          * @return object Template Engine instance
972          */
973         public function template_engine()
974         {
975                 $template_engine = 'smarty3';
976                 if (x($this->theme, 'template_engine')) {
977                         $template_engine = $this->theme['template_engine'];
978                 }
979
980                 if (isset($this->template_engines[$template_engine])) {
981                         if (isset($this->template_engine_instance[$template_engine])) {
982                                 return $this->template_engine_instance[$template_engine];
983                         } else {
984                                 $class = $this->template_engines[$template_engine];
985                                 $obj = new $class;
986                                 $this->template_engine_instance[$template_engine] = $obj;
987                                 return $obj;
988                         }
989                 }
990
991                 echo "template engine <tt>$template_engine</tt> is not registered!\n";
992                 killme();
993         }
994
995         /**
996          * @brief Returns the active template engine.
997          *
998          * @return string
999          */
1000         public function get_template_engine()
1001         {
1002                 return $this->theme['template_engine'];
1003         }
1004
1005         public function set_template_engine($engine = 'smarty3')
1006         {
1007                 $this->theme['template_engine'] = $engine;
1008         }
1009
1010         public function get_template_ldelim($engine = 'smarty3')
1011         {
1012                 return $this->ldelim[$engine];
1013         }
1014
1015         public function get_template_rdelim($engine = 'smarty3')
1016         {
1017                 return $this->rdelim[$engine];
1018         }
1019
1020         public function save_timestamp($stamp, $value)
1021         {
1022                 if (!isset($this->config['system']['profiler']) || !$this->config['system']['profiler']) {
1023                         return;
1024                 }
1025
1026                 $duration = (float) (microtime(true) - $stamp);
1027
1028                 if (!isset($this->performance[$value])) {
1029                         // Prevent ugly E_NOTICE
1030                         $this->performance[$value] = 0;
1031                 }
1032
1033                 $this->performance[$value] += (float) $duration;
1034                 $this->performance['marktime'] += (float) $duration;
1035
1036                 $callstack = System::callstack();
1037
1038                 if (!isset($this->callstack[$value][$callstack])) {
1039                         // Prevent ugly E_NOTICE
1040                         $this->callstack[$value][$callstack] = 0;
1041                 }
1042
1043                 $this->callstack[$value][$callstack] += (float) $duration;
1044         }
1045
1046         public function get_useragent()
1047         {
1048                 return
1049                         FRIENDICA_PLATFORM . " '" .
1050                         FRIENDICA_CODENAME . "' " .
1051                         FRIENDICA_VERSION . '-' .
1052                         DB_UPDATE_VERSION . '; ' .
1053                         $this->get_baseurl();
1054         }
1055
1056         public function is_friendica_app()
1057         {
1058                 return $this->is_friendica_app;
1059         }
1060
1061         /**
1062          * @brief Checks if the site is called via a backend process
1063          *
1064          * This isn't a perfect solution. But we need this check very early.
1065          * So we cannot wait until the modules are loaded.
1066          *
1067          * @return bool Is it a known backend?
1068          */
1069         public function is_backend()
1070         {
1071                 static $backends = [
1072                         '_well_known',
1073                         'api',
1074                         'dfrn_notify',
1075                         'fetch',
1076                         'hcard',
1077                         'hostxrd',
1078                         'nodeinfo',
1079                         'noscrape',
1080                         'p',
1081                         'poco',
1082                         'post',
1083                         'proxy',
1084                         'pubsub',
1085                         'pubsubhubbub',
1086                         'receive',
1087                         'rsd_xml',
1088                         'salmon',
1089                         'statistics_json',
1090                         'xrd',
1091                 ];
1092
1093                 // Check if current module is in backend or backend flag is set
1094                 return (in_array($this->module, $backends) || $this->backend);
1095         }
1096
1097         /**
1098          * @brief Checks if the maximum number of database processes is reached
1099          *
1100          * @return bool Is the limit reached?
1101          */
1102         public function isMaxProcessesReached()
1103         {
1104                 // Deactivated, needs more investigating if this check really makes sense
1105                 return false;
1106
1107                 /*
1108                  * Commented out to suppress static analyzer issues
1109                  *
1110                 if ($this->is_backend()) {
1111                         $process = 'backend';
1112                         $max_processes = Config::get('system', 'max_processes_backend');
1113                         if (intval($max_processes) == 0) {
1114                                 $max_processes = 5;
1115                         }
1116                 } else {
1117                         $process = 'frontend';
1118                         $max_processes = Config::get('system', 'max_processes_frontend');
1119                         if (intval($max_processes) == 0) {
1120                                 $max_processes = 20;
1121                         }
1122                 }
1123
1124                 $processlist = DBA::processlist();
1125                 if ($processlist['list'] != '') {
1126                         logger('Processcheck: Processes: ' . $processlist['amount'] . ' - Processlist: ' . $processlist['list'], LOGGER_DEBUG);
1127
1128                         if ($processlist['amount'] > $max_processes) {
1129                                 logger('Processcheck: Maximum number of processes for ' . $process . ' tasks (' . $max_processes . ') reached.', LOGGER_DEBUG);
1130                                 return true;
1131                         }
1132                 }
1133                 return false;
1134                  */
1135         }
1136
1137         /**
1138          * @brief Checks if the minimal memory is reached
1139          *
1140          * @return bool Is the memory limit reached?
1141          */
1142         public function min_memory_reached()
1143         {
1144                 $min_memory = Config::get('system', 'min_memory', 0);
1145                 if ($min_memory == 0) {
1146                         return false;
1147                 }
1148
1149                 if (!is_readable('/proc/meminfo')) {
1150                         return false;
1151                 }
1152
1153                 $memdata = explode("\n", file_get_contents('/proc/meminfo'));
1154
1155                 $meminfo = [];
1156                 foreach ($memdata as $line) {
1157                         $data = explode(':', $line);
1158                         if (count($data) != 2) {
1159                                 continue;
1160                         }
1161                         list($key, $val) = $data;
1162                         $meminfo[$key] = (int) trim(str_replace('kB', '', $val));
1163                         $meminfo[$key] = (int) ($meminfo[$key] / 1024);
1164                 }
1165
1166                 if (!isset($meminfo['MemAvailable']) || !isset($meminfo['MemFree'])) {
1167                         return false;
1168                 }
1169
1170                 $free = $meminfo['MemAvailable'] + $meminfo['MemFree'];
1171
1172                 $reached = ($free < $min_memory);
1173
1174                 if ($reached) {
1175                         logger('Minimal memory reached: ' . $free . '/' . $meminfo['MemTotal'] . ' - limit ' . $min_memory, LOGGER_DEBUG);
1176                 }
1177
1178                 return $reached;
1179         }
1180
1181         /**
1182          * @brief Checks if the maximum load is reached
1183          *
1184          * @return bool Is the load reached?
1185          */
1186         public function isMaxLoadReached()
1187         {
1188                 if ($this->is_backend()) {
1189                         $process = 'backend';
1190                         $maxsysload = intval(Config::get('system', 'maxloadavg'));
1191                         if ($maxsysload < 1) {
1192                                 $maxsysload = 50;
1193                         }
1194                 } else {
1195                         $process = 'frontend';
1196                         $maxsysload = intval(Config::get('system', 'maxloadavg_frontend'));
1197                         if ($maxsysload < 1) {
1198                                 $maxsysload = 50;
1199                         }
1200                 }
1201
1202                 $load = current_load();
1203                 if ($load) {
1204                         if (intval($load) > $maxsysload) {
1205                                 logger('system: load ' . $load . ' for ' . $process . ' tasks (' . $maxsysload . ') too high.');
1206                                 return true;
1207                         }
1208                 }
1209                 return false;
1210         }
1211
1212         /**
1213          * Executes a child process with 'proc_open'
1214          *
1215          * @param string $command The command to execute
1216          * @param array  $args    Arguments to pass to the command ( [ 'key' => value, 'key2' => value2, ... ]
1217          */
1218         public function proc_run($command, $args)
1219         {
1220                 if (!function_exists('proc_open')) {
1221                         return;
1222                 }
1223
1224                 $cmdline = $this->getConfigValue('config', 'php_path', 'php') . ' ' . escapeshellarg($command);
1225
1226                 foreach ($args as $key => $value) {
1227                         if (!is_null($value) && is_bool($value) && !$value) {
1228                                 continue;
1229                         }
1230
1231                         $cmdline .= ' --' . $key;
1232                         if (!is_null($value) && !is_bool($value)) {
1233                                 $cmdline .= ' ' . $value;
1234                         }
1235                 }
1236
1237                 if ($this->min_memory_reached()) {
1238                         return;
1239                 }
1240
1241                 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
1242                         $resource = proc_open('cmd /c start /b ' . $cmdline, [], $foo, $this->get_basepath());
1243                 } else {
1244                         $resource = proc_open($cmdline . ' &', [], $foo, $this->get_basepath());
1245                 }
1246                 if (!is_resource($resource)) {
1247                         logger('We got no resource for command ' . $cmdline, LOGGER_DEBUG);
1248                         return;
1249                 }
1250                 proc_close($resource);
1251         }
1252
1253         /**
1254          * @brief Returns the system user that is executing the script
1255          *
1256          * This mostly returns something like "www-data".
1257          *
1258          * @return string system username
1259          */
1260         private static function systemuser()
1261         {
1262                 if (!function_exists('posix_getpwuid') || !function_exists('posix_geteuid')) {
1263                         return '';
1264                 }
1265
1266                 $processUser = posix_getpwuid(posix_geteuid());
1267                 return $processUser['name'];
1268         }
1269
1270         /**
1271          * @brief Checks if a given directory is usable for the system
1272          *
1273          * @return boolean the directory is usable
1274          */
1275         public static function directory_usable($directory, $check_writable = true)
1276         {
1277                 if ($directory == '') {
1278                         logger('Directory is empty. This shouldn\'t happen.', LOGGER_DEBUG);
1279                         return false;
1280                 }
1281
1282                 if (!file_exists($directory)) {
1283                         logger('Path "' . $directory . '" does not exist for user ' . self::systemuser(), LOGGER_DEBUG);
1284                         return false;
1285                 }
1286
1287                 if (is_file($directory)) {
1288                         logger('Path "' . $directory . '" is a file for user ' . self::systemuser(), LOGGER_DEBUG);
1289                         return false;
1290                 }
1291
1292                 if (!is_dir($directory)) {
1293                         logger('Path "' . $directory . '" is not a directory for user ' . self::systemuser(), LOGGER_DEBUG);
1294                         return false;
1295                 }
1296
1297                 if ($check_writable && !is_writable($directory)) {
1298                         logger('Path "' . $directory . '" is not writable for user ' . self::systemuser(), LOGGER_DEBUG);
1299                         return false;
1300                 }
1301
1302                 return true;
1303         }
1304
1305         /**
1306          * @param string $cat     Config category
1307          * @param string $k       Config key
1308          * @param mixed  $default Default value if it isn't set
1309          */
1310         public function getConfigValue($cat, $k, $default = null)
1311         {
1312                 $return = $default;
1313
1314                 if ($cat === 'config') {
1315                         if (isset($this->config[$k])) {
1316                                 $return = $this->config[$k];
1317                         }
1318                 } else {
1319                         if (isset($this->config[$cat][$k])) {
1320                                 $return = $this->config[$cat][$k];
1321                         }
1322                 }
1323
1324                 return $return;
1325         }
1326
1327         /**
1328          * Sets a default value in the config cache. Ignores already existing keys.
1329          *
1330          * @param string $cat Config category
1331          * @param string $k   Config key
1332          * @param mixed  $v   Default value to set
1333          */
1334         private function setDefaultConfigValue($cat, $k, $v)
1335         {
1336                 if (!isset($this->config[$cat][$k])) {
1337                         $this->setConfigValue($cat, $k, $v);
1338                 }
1339         }
1340
1341         /**
1342          * Sets a value in the config cache. Accepts raw output from the config table
1343          *
1344          * @param string $cat Config category
1345          * @param string $k   Config key
1346          * @param mixed  $v   Value to set
1347          */
1348         public function setConfigValue($cat, $k, $v)
1349         {
1350                 // Only arrays are serialized in database, so we have to unserialize sparingly
1351                 $value = is_string($v) && preg_match("|^a:[0-9]+:{.*}$|s", $v) ? unserialize($v) : $v;
1352
1353                 if ($cat === 'config') {
1354                         $this->config[$k] = $value;
1355                 } else {
1356                         if (!isset($this->config[$cat])) {
1357                                 $this->config[$cat] = [];
1358                         }
1359
1360                         $this->config[$cat][$k] = $value;
1361                 }
1362         }
1363
1364         /**
1365          * Deletes a value from the config cache
1366          *
1367          * @param string $cat Config category
1368          * @param string $k   Config key
1369          */
1370         public function deleteConfigValue($cat, $k)
1371         {
1372                 if ($cat === 'config') {
1373                         if (isset($this->config[$k])) {
1374                                 unset($this->config[$k]);
1375                         }
1376                 } else {
1377                         if (isset($this->config[$cat][$k])) {
1378                                 unset($this->config[$cat][$k]);
1379                         }
1380                 }
1381         }
1382
1383
1384         /**
1385          * Retrieves a value from the user config cache
1386          *
1387          * @param int    $uid     User Id
1388          * @param string $cat     Config category
1389          * @param string $k       Config key
1390          * @param mixed  $default Default value if key isn't set
1391          */
1392         public function getPConfigValue($uid, $cat, $k, $default = null)
1393         {
1394                 $return = $default;
1395
1396                 if (isset($this->config[$uid][$cat][$k])) {
1397                         $return = $this->config[$uid][$cat][$k];
1398                 }
1399
1400                 return $return;
1401         }
1402
1403         /**
1404          * Sets a value in the user config cache
1405          *
1406          * Accepts raw output from the pconfig table
1407          *
1408          * @param int    $uid User Id
1409          * @param string $cat Config category
1410          * @param string $k   Config key
1411          * @param mixed  $v   Value to set
1412          */
1413         public function setPConfigValue($uid, $cat, $k, $v)
1414         {
1415                 // Only arrays are serialized in database, so we have to unserialize sparingly
1416                 $value = is_string($v) && preg_match("|^a:[0-9]+:{.*}$|s", $v) ? unserialize($v) : $v;
1417
1418                 if (!isset($this->config[$uid]) || !is_array($this->config[$uid])) {
1419                         $this->config[$uid] = [];
1420                 }
1421
1422                 if (!isset($this->config[$uid][$cat]) || !is_array($this->config[$uid][$cat])) {
1423                         $this->config[$uid][$cat] = [];
1424                 }
1425
1426                 $this->config[$uid][$cat][$k] = $value;
1427         }
1428
1429         /**
1430          * Deletes a value from the user config cache
1431          *
1432          * @param int    $uid User Id
1433          * @param string $cat Config category
1434          * @param string $k   Config key
1435          */
1436         public function deletePConfigValue($uid, $cat, $k)
1437         {
1438                 if (isset($this->config[$uid][$cat][$k])) {
1439                         unset($this->config[$uid][$cat][$k]);
1440                 }
1441         }
1442
1443         /**
1444          * Generates the site's default sender email address
1445          *
1446          * @return string
1447          */
1448         public function getSenderEmailAddress()
1449         {
1450                 $sender_email = Config::get('config', 'sender_email');
1451                 if (empty($sender_email)) {
1452                         $hostname = $this->get_hostname();
1453                         if (strpos($hostname, ':')) {
1454                                 $hostname = substr($hostname, 0, strpos($hostname, ':'));
1455                         }
1456
1457                         $sender_email = 'noreply@' . $hostname;
1458                 }
1459
1460                 return $sender_email;
1461         }
1462
1463         /**
1464          * Returns the current theme name.
1465          *
1466          * @return string
1467          */
1468         public function getCurrentTheme()
1469         {
1470                 if ($this->isInstallMode()) {
1471                         return '';
1472                 }
1473
1474                 //// @TODO Compute the current theme only once (this behavior has
1475                 /// already been implemented, but it didn't work well -
1476                 /// https://github.com/friendica/friendica/issues/5092)
1477                 $this->computeCurrentTheme();
1478
1479                 return $this->current_theme;
1480         }
1481
1482         /**
1483          * Computes the current theme name based on the node settings, the user settings and the device type
1484          *
1485          * @throws Exception
1486          */
1487         private function computeCurrentTheme()
1488         {
1489                 $system_theme = Config::get('system', 'theme');
1490                 if (!$system_theme) {
1491                         throw new Exception(L10n::t('No system theme config value set.'));
1492                 }
1493
1494                 // Sane default
1495                 $this->current_theme = $system_theme;
1496
1497                 $allowed_themes = explode(',', Config::get('system', 'allowed_themes', $system_theme));
1498
1499                 $page_theme = null;
1500                 // Find the theme that belongs to the user whose stuff we are looking at
1501                 if ($this->profile_uid && ($this->profile_uid != local_user())) {
1502                         // Allow folks to override user themes and always use their own on their own site.
1503                         // This works only if the user is on the same server
1504                         $user = DBA::selectFirst('user', ['theme'], ['uid' => $this->profile_uid]);
1505                         if (DBA::isResult($user) && !PConfig::get(local_user(), 'system', 'always_my_theme')) {
1506                                 $page_theme = $user['theme'];
1507                         }
1508                 }
1509
1510                 $user_theme = Core\Session::get('theme', $system_theme);
1511
1512                 // Specific mobile theme override
1513                 if (($this->is_mobile || $this->is_tablet) && Core\Session::get('show-mobile', true)) {
1514                         $system_mobile_theme = Config::get('system', 'mobile-theme');
1515                         $user_mobile_theme = Core\Session::get('mobile-theme', $system_mobile_theme);
1516
1517                         // --- means same mobile theme as desktop
1518                         if (!empty($user_mobile_theme) && $user_mobile_theme !== '---') {
1519                                 $user_theme = $user_mobile_theme;
1520                         }
1521                 }
1522
1523                 if ($page_theme) {
1524                         $theme_name = $page_theme;
1525                 } else {
1526                         $theme_name = $user_theme;
1527                 }
1528
1529                 if ($theme_name
1530                         && in_array($theme_name, $allowed_themes)
1531                         && (file_exists('view/theme/' . $theme_name . '/style.css')
1532                         || file_exists('view/theme/' . $theme_name . '/style.php'))
1533                 ) {
1534                         $this->current_theme = $theme_name;
1535                 }
1536         }
1537
1538         /**
1539          * @brief Return full URL to theme which is currently in effect.
1540          *
1541          * Provide a sane default if nothing is chosen or the specified theme does not exist.
1542          *
1543          * @return string
1544          */
1545         public function getCurrentThemeStylesheetPath()
1546         {
1547                 return Core\Theme::getStylesheetPath($this->getCurrentTheme());
1548         }
1549 }