]> git.mxchange.org Git - friendica.git/blob - src/App.php
c028f9d9d8c80e161dd4d1c9077c2ec302f3cc28
[friendica.git] / src / App.php
1 <?php
2 /**
3  * @file src/App.php
4  */
5 namespace Friendica;
6
7 use Exception;
8 use Friendica\App\Arguments;
9 use Friendica\App\BaseURL;
10 use Friendica\App\Page;
11 use Friendica\App\Authentication;
12 use Friendica\Core\Config\Cache\ConfigCache;
13 use Friendica\Core\Config\Configuration;
14 use Friendica\Core\Config\PConfiguration;
15 use Friendica\Core\L10n\L10n;
16 use Friendica\Core\System;
17 use Friendica\Core\Theme;
18 use Friendica\Database\Database;
19 use Friendica\Model\Profile;
20 use Friendica\Module\Special\HTTPException as ModuleHTTPException;
21 use Friendica\Network\HTTPException;
22 use Friendica\Util\ConfigFileLoader;
23 use Friendica\Util\HTTPSignature;
24 use Friendica\Util\Profiler;
25 use Friendica\Util\Strings;
26 use Psr\Log\LoggerInterface;
27
28 /**
29  *
30  * class: App
31  *
32  * @brief Our main application structure for the life of this page.
33  *
34  * Primarily deals with the URL that got us here
35  * and tries to make some sense of it, and
36  * stores our page contents and config storage
37  * and anything else that might need to be passed around
38  * before we spit the page out.
39  *
40  */
41 class App
42 {
43         /**
44          * @var Page The current page environment
45          */
46         public $page;
47         public $profile;
48         public $profile_uid;
49         public $user;
50         public $cid;
51         public $contact;
52         public $contacts;
53         public $page_contact;
54         public $content;
55         public $data = [];
56         /** @deprecated 2019.09 - use App\Arguments->getCommand() */
57         public $cmd = '';
58         /** @deprecated 2019.09 - use App\Arguments->getArgv() or Arguments->get() */
59         public $argv;
60         /** @deprecated 2019.09 - use App\Arguments->getArgc() */
61         public $argc;
62         /** @deprecated 2019.09 - Use App\Module->getName() instead */
63         public $module;
64         public $timezone;
65         public $interactive = true;
66         public $identities;
67         public $theme_info = [];
68         public $category;
69         // Allow themes to control internal parameters
70         // by changing App values in theme.php
71
72         public $sourcename              = '';
73         public $videowidth              = 425;
74         public $videoheight             = 350;
75         public $force_max_items         = 0;
76         public $theme_events_in_profile = true;
77         public $queue;
78
79         /**
80          * @var App\Mode The Mode of the Application
81          */
82         private $mode;
83
84         /**
85          * @var BaseURL
86          */
87         private $baseURL;
88
89         /** @var string The name of the current theme */
90         private $currentTheme;
91         /** @var string The name of the current mobile theme */
92         private $currentMobileTheme;
93
94         /**
95          * @var Configuration The config
96          */
97         private $config;
98
99         /**
100          * @var LoggerInterface The logger
101          */
102         private $logger;
103
104         /**
105          * @var Profiler The profiler of this app
106          */
107         private $profiler;
108
109         /**
110          * @var Database The Friendica database connection
111          */
112         private $database;
113
114         /**
115          * @var L10n The translator
116          */
117         private $l10n;
118
119         /**
120          * @var App\Arguments
121          */
122         private $args;
123
124         /**
125          * @var Core\Process The process methods
126          */
127         private $process;
128
129         /**
130          * Returns the current config cache of this node
131          *
132          * @return ConfigCache
133          */
134         public function getConfigCache()
135         {
136                 return $this->config->getCache();
137         }
138
139         /**
140          * The basepath of this app
141          *
142          * @return string
143          */
144         public function getBasePath()
145         {
146                 // Don't use the basepath of the config table for basepath (it should always be the config-file one)
147                 return $this->config->getCache()->get('system', 'basepath');
148         }
149
150         /**
151          * @param Database        $database The Friendica Database
152          * @param Configuration   $config   The Configuration
153          * @param App\Mode        $mode     The mode of this Friendica app
154          * @param BaseURL         $baseURL  The full base URL of this Friendica app
155          * @param LoggerInterface $logger   The current app logger
156          * @param Profiler        $profiler The profiler of this application
157          * @param L10n            $l10n     The translator instance
158          * @param App\Arguments   $args     The Friendica Arguments of the call
159          * @param Core\Process    $process  The process methods
160          */
161         public function __construct(Database $database, Configuration $config, App\Mode $mode, BaseURL $baseURL, LoggerInterface $logger, Profiler $profiler, L10n $l10n, Arguments $args, App\Module $module, App\Page $page, Core\Process $process)
162         {
163                 $this->database = $database;
164                 $this->config   = $config;
165                 $this->mode     = $mode;
166                 $this->baseURL  = $baseURL;
167                 $this->profiler = $profiler;
168                 $this->logger   = $logger;
169                 $this->l10n     = $l10n;
170                 $this->args     = $args;
171                 $this->process  = $process;
172
173                 $this->cmd          = $args->getCommand();
174                 $this->argv         = $args->getArgv();
175                 $this->argc         = $args->getArgc();
176                 $this->module       = $module->getName();
177                 $this->page         = $page;
178
179                 $this->load();
180         }
181
182         /**
183          * Load the whole app instance
184          */
185         public function load()
186         {
187                 set_time_limit(0);
188
189                 // This has to be quite large to deal with embedded private photos
190                 ini_set('pcre.backtrack_limit', 500000);
191
192                 set_include_path(
193                         get_include_path() . PATH_SEPARATOR
194                         . $this->getBasePath() . DIRECTORY_SEPARATOR . 'include' . PATH_SEPARATOR
195                         . $this->getBasePath() . DIRECTORY_SEPARATOR . 'library' . PATH_SEPARATOR
196                         . $this->getBasePath());
197
198                 $this->profiler->reset();
199
200                 if ($this->mode->has(App\Mode::DBAVAILABLE)) {
201                         $this->profiler->update($this->config);
202
203                         Core\Hook::loadHooks();
204                         $loader = new ConfigFileLoader($this->getBasePath());
205                         Core\Hook::callAll('load_config', $loader);
206                 }
207
208                 $this->loadDefaultTimezone();
209                 // Register template engines
210                 Core\Renderer::registerTemplateEngine('Friendica\Render\FriendicaSmartyEngine');
211         }
212
213         /**
214          * Loads the default timezone
215          *
216          * Include support for legacy $default_timezone
217          *
218          * @global string $default_timezone
219          */
220         private function loadDefaultTimezone()
221         {
222                 if ($this->config->get('system', 'default_timezone')) {
223                         $this->timezone = $this->config->get('system', 'default_timezone');
224                 } else {
225                         global $default_timezone;
226                         $this->timezone = !empty($default_timezone) ? $default_timezone : 'UTC';
227                 }
228
229                 if ($this->timezone) {
230                         date_default_timezone_set($this->timezone);
231                 }
232         }
233
234         /**
235          * Returns the current UserAgent as a String
236          *
237          * @return string the UserAgent as a String
238          * @throws HTTPException\InternalServerErrorException
239          */
240         public function getUserAgent()
241         {
242                 return
243                         FRIENDICA_PLATFORM . " '" .
244                         FRIENDICA_CODENAME . "' " .
245                         FRIENDICA_VERSION . '-' .
246                         DB_UPDATE_VERSION . '; ' .
247                         $this->baseURL->get();
248         }
249
250         /**
251          * Generates the site's default sender email address
252          *
253          * @return string
254          * @throws HTTPException\InternalServerErrorException
255          */
256         public function getSenderEmailAddress()
257         {
258                 $sender_email = $this->config->get('config', 'sender_email');
259                 if (empty($sender_email)) {
260                         $hostname = $this->baseURL->getHostname();
261                         if (strpos($hostname, ':')) {
262                                 $hostname = substr($hostname, 0, strpos($hostname, ':'));
263                         }
264
265                         $sender_email = 'noreply@' . $hostname;
266                 }
267
268                 return $sender_email;
269         }
270
271         /**
272          * Returns the current theme name. May be overriden by the mobile theme name.
273          *
274          * @return string
275          * @throws Exception
276          */
277         public function getCurrentTheme()
278         {
279                 if ($this->mode->isInstall()) {
280                         return '';
281                 }
282
283                 // Specific mobile theme override
284                 if (($this->mode->isMobile() || $this->mode->isTablet()) && Core\Session::get('show-mobile', true)) {
285                         $user_mobile_theme = $this->getCurrentMobileTheme();
286
287                         // --- means same mobile theme as desktop
288                         if (!empty($user_mobile_theme) && $user_mobile_theme !== '---') {
289                                 return $user_mobile_theme;
290                         }
291                 }
292
293                 if (!$this->currentTheme) {
294                         $this->computeCurrentTheme();
295                 }
296
297                 return $this->currentTheme;
298         }
299
300         /**
301          * Returns the current mobile theme name.
302          *
303          * @return string
304          * @throws Exception
305          */
306         public function getCurrentMobileTheme()
307         {
308                 if ($this->mode->isInstall()) {
309                         return '';
310                 }
311
312                 if (is_null($this->currentMobileTheme)) {
313                         $this->computeCurrentMobileTheme();
314                 }
315
316                 return $this->currentMobileTheme;
317         }
318
319         public function setCurrentTheme($theme)
320         {
321                 $this->currentTheme = $theme;
322         }
323
324         public function setCurrentMobileTheme($theme)
325         {
326                 $this->currentMobileTheme = $theme;
327         }
328
329         /**
330          * Computes the current theme name based on the node settings, the page owner settings and the user settings
331          *
332          * @throws Exception
333          */
334         private function computeCurrentTheme()
335         {
336                 $system_theme = $this->config->get('system', 'theme');
337                 if (!$system_theme) {
338                         throw new Exception($this->l10n->t('No system theme config value set.'));
339                 }
340
341                 // Sane default
342                 $this->setCurrentTheme($system_theme);
343
344                 $page_theme = null;
345                 // Find the theme that belongs to the user whose stuff we are looking at
346                 if ($this->profile_uid && ($this->profile_uid != local_user())) {
347                         // Allow folks to override user themes and always use their own on their own site.
348                         // This works only if the user is on the same server
349                         $user = $this->database->selectFirst('user', ['theme'], ['uid' => $this->profile_uid]);
350                         if ($this->database->isResult($user) && !Core\PConfig::get(local_user(), 'system', 'always_my_theme')) {
351                                 $page_theme = $user['theme'];
352                         }
353                 }
354
355                 $theme_name = $page_theme ?: Core\Session::get('theme', $system_theme);
356
357                 $theme_name = Strings::sanitizeFilePathItem($theme_name);
358                 if ($theme_name
359                     && in_array($theme_name, Theme::getAllowedList())
360                     && (file_exists('view/theme/' . $theme_name . '/style.css')
361                         || file_exists('view/theme/' . $theme_name . '/style.php'))
362                 ) {
363                         $this->setCurrentTheme($theme_name);
364                 }
365         }
366
367         /**
368          * Computes the current mobile theme name based on the node settings, the page owner settings and the user settings
369          */
370         private function computeCurrentMobileTheme()
371         {
372                 $system_mobile_theme = $this->config->get('system', 'mobile-theme', '');
373
374                 // Sane default
375                 $this->setCurrentMobileTheme($system_mobile_theme);
376
377                 $page_mobile_theme = null;
378                 // Find the theme that belongs to the user whose stuff we are looking at
379                 if ($this->profile_uid && ($this->profile_uid != local_user())) {
380                         // Allow folks to override user themes and always use their own on their own site.
381                         // This works only if the user is on the same server
382                         if (!Core\PConfig::get(local_user(), 'system', 'always_my_theme')) {
383                                 $page_mobile_theme = Core\PConfig::get($this->profile_uid, 'system', 'mobile-theme');
384                         }
385                 }
386
387                 $mobile_theme_name = $page_mobile_theme ?: Core\Session::get('mobile-theme', $system_mobile_theme);
388
389                 $mobile_theme_name = Strings::sanitizeFilePathItem($mobile_theme_name);
390                 if ($mobile_theme_name == '---'
391                         ||
392                         in_array($mobile_theme_name, Theme::getAllowedList())
393                         && (file_exists('view/theme/' . $mobile_theme_name . '/style.css')
394                                 || file_exists('view/theme/' . $mobile_theme_name . '/style.php'))
395                 ) {
396                         $this->setCurrentMobileTheme($mobile_theme_name);
397                 }
398         }
399
400         /**
401          * @brief Return full URL to theme which is currently in effect.
402          *
403          * Provide a sane default if nothing is chosen or the specified theme does not exist.
404          *
405          * @return string
406          * @throws Exception
407          */
408         public function getCurrentThemeStylesheetPath()
409         {
410                 return Core\Theme::getStylesheetPath($this->getCurrentTheme());
411         }
412
413         /**
414          * Sets the base url for use in cmdline programs which don't have
415          * $_SERVER variables
416          */
417         public function checkURL()
418         {
419                 $url = $this->config->get('system', 'url');
420
421                 // if the url isn't set or the stored url is radically different
422                 // than the currently visited url, store the current value accordingly.
423                 // "Radically different" ignores common variations such as http vs https
424                 // and www.example.com vs example.com.
425                 // We will only change the url to an ip address if there is no existing setting
426
427                 if (empty($url) || (!Util\Strings::compareLink($url, $this->baseURL->get())) && (!preg_match("/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/", $this->baseURL->getHostname()))) {
428                         $this->config->set('system', 'url', $this->baseURL->get());
429                 }
430         }
431
432         /**
433          * Frontend App script
434          *
435          * The App object behaves like a container and a dispatcher at the same time, including a representation of the
436          * request and a representation of the response.
437          *
438          * This probably should change to limit the size of this monster method.
439          *
440          * @param App\Module     $module The determined module
441          * @param App\Router     $router
442          * @param PConfiguration $pconfig
443          * @param Authentication $auth The Authentication backend of the node
444          * @throws HTTPException\InternalServerErrorException
445          * @throws \ImagickException
446          */
447         public function runFrontend(App\Module $module, App\Router $router, PConfiguration $pconfig, Authentication $auth)
448         {
449                 $moduleName = $module->getName();
450
451                 try {
452                         // Missing DB connection: ERROR
453                         if ($this->mode->has(App\Mode::LOCALCONFIGPRESENT) && !$this->mode->has(App\Mode::DBAVAILABLE)) {
454                                 throw new HTTPException\InternalServerErrorException('Apologies but the website is unavailable at the moment.');
455                         }
456
457                         // Max Load Average reached: ERROR
458                         if ($this->process->isMaxProcessesReached() || $this->process->isMaxLoadReached()) {
459                                 header('Retry-After: 120');
460                                 header('Refresh: 120; url=' . $this->baseURL->get() . "/" . $this->args->getQueryString());
461
462                                 throw new HTTPException\ServiceUnavailableException('The node is currently overloaded. Please try again later.');
463                         }
464
465                         if (!$this->mode->isInstall()) {
466                                 // Force SSL redirection
467                                 if ($this->baseURL->checkRedirectHttps()) {
468                                         System::externalRedirect($this->baseURL->get() . '/' . $this->args->getQueryString());
469                                 }
470
471                                 Core\Hook::callAll('init_1');
472                         }
473
474                         // Exclude the backend processes from the session management
475                         if ($this->mode->isBackend()) {
476                                 Core\Worker::executeIfIdle();
477                         }
478
479                         if ($this->mode->isNormal()) {
480                                 $requester = HTTPSignature::getSigner('', $_SERVER);
481                                 if (!empty($requester)) {
482                                         Profile::addVisitorCookieForHandle($requester);
483                                 }
484                         }
485
486                         // ZRL
487                         if (!empty($_GET['zrl']) && $this->mode->isNormal()) {
488                                 if (!local_user()) {
489                                         // Only continue when the given profile link seems valid
490                                         // Valid profile links contain a path with "/profile/" and no query parameters
491                                         if ((parse_url($_GET['zrl'], PHP_URL_QUERY) == "") &&
492                                             strstr(parse_url($_GET['zrl'], PHP_URL_PATH), "/profile/")) {
493                                                 if (Core\Session::get('visitor_home') != $_GET["zrl"]) {
494                                                         Core\Session::set('my_url', $_GET['zrl']);
495                                                         Core\Session::set('authenticated', 0);
496                                                 }
497
498                                                 Model\Profile::zrlInit($this);
499                                         } else {
500                                                 // Someone came with an invalid parameter, maybe as a DDoS attempt
501                                                 // We simply stop processing here
502                                                 $this->logger->debug('Invalid ZRL parameter.', ['zrl' => $_GET['zrl']]);
503                                                 throw new HTTPException\ForbiddenException();
504                                         }
505                                 }
506                         }
507
508                         if (!empty($_GET['owt']) && $this->mode->isNormal()) {
509                                 $token = $_GET['owt'];
510                                 Model\Profile::openWebAuthInit($token);
511                         }
512
513                         $auth->withSession($this);
514
515                         if (empty($_SESSION['authenticated'])) {
516                                 header('X-Account-Management-Status: none');
517                         }
518
519                         $_SESSION['sysmsg']       = Core\Session::get('sysmsg', []);
520                         $_SESSION['sysmsg_info']  = Core\Session::get('sysmsg_info', []);
521                         $_SESSION['last_updated'] = Core\Session::get('last_updated', []);
522
523                         /*
524                          * check_config() is responsible for running update scripts. These automatically
525                          * update the DB schema whenever we push a new one out. It also checks to see if
526                          * any addons have been added or removed and reacts accordingly.
527                          */
528
529                         // in install mode, any url loads install module
530                         // but we need "view" module for stylesheet
531                         if ($this->mode->isInstall() && $moduleName !== 'install') {
532                                 $this->baseURL->redirect('install');
533                         } elseif (!$this->mode->isInstall() && !$this->mode->has(App\Mode::MAINTENANCEDISABLED) && $moduleName !== 'maintenance') {
534                                 $this->baseURL->redirect('maintenance');
535                         } else {
536                                 $this->checkURL();
537                                 Core\Update::check($this->getBasePath(), false, $this->mode);
538                                 Core\Addon::loadAddons();
539                                 Core\Hook::loadHooks();
540                         }
541
542                         // Compatibility with the Android Diaspora client
543                         if ($moduleName == 'stream') {
544                                 $this->baseURL->redirect('network?order=post');
545                         }
546
547                         if ($moduleName == 'conversations') {
548                                 $this->baseURL->redirect('message');
549                         }
550
551                         if ($moduleName == 'commented') {
552                                 $this->baseURL->redirect('network?order=comment');
553                         }
554
555                         if ($moduleName == 'liked') {
556                                 $this->baseURL->redirect('network?order=comment');
557                         }
558
559                         if ($moduleName == 'activity') {
560                                 $this->baseURL->redirect('network?conv=1');
561                         }
562
563                         if (($moduleName == 'status_messages') && ($this->args->getCommand() == 'status_messages/new')) {
564                                 $this->baseURL->redirect('bookmarklet');
565                         }
566
567                         if (($moduleName == 'user') && ($this->args->getCommand() == 'user/edit')) {
568                                 $this->baseURL->redirect('settings');
569                         }
570
571                         if (($moduleName == 'tag_followings') && ($this->args->getCommand() == 'tag_followings/manage')) {
572                                 $this->baseURL->redirect('search');
573                         }
574
575                         // Initialize module that can set the current theme in the init() method, either directly or via App->profile_uid
576                         $this->page['page_title'] = $moduleName;
577
578                         // determine the module class and save it to the module instance
579                         // @todo there's an implicit dependency due SESSION::start(), so it has to be called here (yet)
580                         $module = $module->determineClass($this->args, $router, $this->config);
581
582                         // Let the module run it's internal process (init, get, post, ...)
583                         $module->run($this->l10n, $this->baseURL, $this->logger, $_SERVER, $_POST);
584                 } catch (HTTPException $e) {
585                         ModuleHTTPException::rawContent($e);
586                 }
587
588                 $this->page->run($this, $this->baseURL, $this->mode, $module, $this->l10n, $this->config, $pconfig);
589         }
590
591         /**
592          * Automatically redirects to relative or absolute URL
593          * Should only be used if it isn't clear if the URL is either internal or external
594          *
595          * @param string $toUrl The target URL
596          *
597          * @throws HTTPException\InternalServerErrorException
598          */
599         public function redirect($toUrl)
600         {
601                 if (!empty(parse_url($toUrl, PHP_URL_SCHEME))) {
602                         Core\System::externalRedirect($toUrl);
603                 } else {
604                         $this->baseURL->redirect($toUrl);
605                 }
606         }
607 }