7 use Detection\MobileDetect;
11 use Friendica\App\Arguments;
12 use Friendica\App\Module;
13 use Friendica\Core\Config\Cache\ConfigCache;
14 use Friendica\Core\Config\Configuration;
15 use Friendica\Core\Config\PConfiguration;
16 use Friendica\Core\L10n\L10n;
17 use Friendica\Core\System;
18 use Friendica\Core\Theme;
19 use Friendica\Database\Database;
20 use Friendica\Database\DBA;
21 use Friendica\Model\Profile;
22 use Friendica\Module\Login;
23 use Friendica\Module\Special\HTTPException as ModuleHTTPException;
24 use Friendica\Network\HTTPException;
25 use Friendica\Util\BaseURL;
26 use Friendica\Util\ConfigFileLoader;
27 use Friendica\Util\HTTPSignature;
28 use Friendica\Util\Profiler;
29 use Friendica\Util\Strings;
30 use Psr\Log\LoggerInterface;
36 * @brief Our main application structure for the life of this page.
38 * Primarily deals with the URL that got us here
39 * and tries to make some sense of it, and
40 * stores our page contents and config storage
41 * and anything else that might need to be passed around
42 * before we spit the page out.
47 /** @deprecated 2019.09 - use App\Arguments->getQueryString() */
48 public $query_string = '';
59 /** @deprecated 2019.09 - use App\Arguments->getCommand() */
61 /** @deprecated 2019.09 - use App\Arguments->getArgv() or Arguments->get() */
63 /** @deprecated 2019.09 - use App\Arguments->getArgc() */
65 /** @deprecated 2019.09 - Use App\Module->getName() instead */
68 public $interactive = true;
70 public $is_mobile = false;
71 public $is_tablet = false;
72 public $theme_info = [];
74 // Allow themes to control internal parameters
75 // by changing App values in theme.php
77 public $sourcename = '';
78 public $videowidth = 425;
79 public $videoheight = 350;
80 public $force_max_items = 0;
81 public $theme_events_in_profile = true;
83 public $stylesheets = [];
84 public $footerScripts = [];
87 * @var App\Mode The Mode of the Application
102 * @var bool true, if the call is from an backend node (f.e. worker)
107 * @var string The name of the current theme
109 private $currentTheme;
112 * @var bool check if request was an AJAX (xmlhttprequest) request
119 public $mobileDetect;
122 * @var Configuration The config
127 * @var LoggerInterface The logger
132 * @var Profiler The profiler of this app
137 * @var Database The Friendica database connection
142 * @var L10n The translator
154 private $moduleClass;
157 * Returns the current config cache of this node
159 * @return ConfigCache
161 public function getConfigCache()
163 return $this->config->getCache();
167 * Returns the current config of this node
169 * @return Configuration
171 public function getConfig()
173 return $this->config;
177 * The basepath of this app
181 public function getBasePath()
183 // Don't use the basepath of the config table for basepath (it should always be the config-file one)
184 return $this->config->getCache()->get('system', 'basepath');
188 * The Logger of this app
190 * @return LoggerInterface
192 public function getLogger()
194 return $this->logger;
198 * The profiler of this app
202 public function getProfiler()
204 return $this->profiler;
208 * Returns the Mode of the Application
210 * @return App\Mode The Application Mode
212 public function getMode()
218 * Returns the Database of the Application
222 public function getDBA()
224 return $this->database;
228 * Register a stylesheet file path to be included in the <head> tag of every page.
229 * Inclusion is done in App->initHead().
230 * The path can be absolute or relative to the Friendica installation base folder.
234 * @param string $path
236 public function registerStylesheet($path)
238 if (mb_strpos($path, $this->getBasePath() . DIRECTORY_SEPARATOR) === 0) {
239 $path = mb_substr($path, mb_strlen($this->getBasePath() . DIRECTORY_SEPARATOR));
242 $this->stylesheets[] = trim($path, '/');
246 * Register a javascript file path to be included in the <footer> tag of every page.
247 * Inclusion is done in App->initFooter().
248 * The path can be absolute or relative to the Friendica installation base folder.
252 * @param string $path
254 public function registerFooterScript($path)
256 $url = str_replace($this->getBasePath() . DIRECTORY_SEPARATOR, '', $path);
258 $this->footerScripts[] = trim($url, '/');
264 * @brief App constructor.
266 * @param Database $database The Friendica Database
267 * @param Configuration $config The Configuration
268 * @param App\Mode $mode The mode of this Friendica app
269 * @param App\Router $router The router of this Friendica app
270 * @param BaseURL $baseURL The full base URL of this Friendica app
271 * @param LoggerInterface $logger The current app logger
272 * @param Profiler $profiler The profiler of this application
273 * @param L10n $l10n The translator instance
275 * @throws Exception if the Basepath is not usable
277 public function __construct(Database $database, Configuration $config, App\Mode $mode, App\Router $router, BaseURL $baseURL, LoggerInterface $logger, Profiler $profiler, L10n $l10n, Arguments $args, Module $module)
279 $this->database = $database;
280 $this->config = $config;
282 $this->router = $router;
283 $this->baseURL = $baseURL;
284 $this->profiler = $profiler;
285 $this->logger = $logger;
288 $this->isBackend = $this->checkBackend($module);
290 $this->profiler->reset();
296 // This has to be quite large to deal with embedded private photos
297 ini_set('pcre.backtrack_limit', 500000);
300 get_include_path() . PATH_SEPARATOR
301 . $this->getBasePath() . DIRECTORY_SEPARATOR . 'include' . PATH_SEPARATOR
302 . $this->getBasePath() . DIRECTORY_SEPARATOR . 'library' . PATH_SEPARATOR
303 . $this->getBasePath());
305 $this->cmd = $args->getCommand();
306 $this->argv = $args->getArgv();
307 $this->argc = $args->getArgc();
308 $this->query_string = $args->getQueryString();
310 // Detect mobile devices
311 $mobile_detect = new MobileDetect();
313 $this->mobileDetect = $mobile_detect;
315 $this->is_mobile = $mobile_detect->isMobile();
316 $this->is_tablet = $mobile_detect->isTablet();
318 $this->isAjax = strtolower(defaults($_SERVER, 'HTTP_X_REQUESTED_WITH', '')) == 'xmlhttprequest';
320 // Register template engines
321 Core\Renderer::registerTemplateEngine('Friendica\Render\FriendicaSmartyEngine');
325 * Reloads the whole app instance
327 public function reload()
329 if ($this->getMode()->has(App\Mode::DBAVAILABLE)) {
330 $this->profiler->update($this->config);
332 Core\Hook::loadHooks();
333 $loader = new ConfigFileLoader($this->getBasePath());
334 Core\Hook::callAll('load_config', $loader);
337 $this->loadDefaultTimezone();
341 * Loads the default timezone
343 * Include support for legacy $default_timezone
345 * @global string $default_timezone
347 private function loadDefaultTimezone()
349 if ($this->config->get('system', 'default_timezone')) {
350 $this->timezone = $this->config->get('system', 'default_timezone');
352 global $default_timezone;
353 $this->timezone = !empty($default_timezone) ? $default_timezone : 'UTC';
356 if ($this->timezone) {
357 date_default_timezone_set($this->timezone);
362 * Returns the scheme of the current call
365 * @deprecated 2019.06 - use BaseURL->getScheme() instead
367 public function getScheme()
369 return $this->baseURL->getScheme();
373 * Retrieves the Friendica instance base URL
375 * @param bool $ssl Whether to append http or https under BaseURL::SSL_POLICY_SELFSIGN
377 * @return string Friendica server base URL
379 * @deprecated 2019.09 - use BaseUrl->get($ssl) instead
381 public function getBaseURL($ssl = false)
383 return $this->baseURL->get($ssl);
387 * @brief Initializes the baseurl components
389 * Clears the baseurl cache to prevent inconsistencies
393 * @deprecated 2019.06 - use BaseURL->saveByURL($url) instead
395 public function setBaseURL($url)
397 $this->baseURL->saveByURL($url);
401 * Returns the current hostname
405 * @deprecated 2019.06 - use BaseURL->getHostname() instead
407 public function getHostName()
409 return $this->baseURL->getHostname();
413 * Returns the sub-path of the full URL
417 * @deprecated 2019.06 - use BaseURL->getUrlPath() instead
419 public function getURLPath()
421 return $this->baseURL->getUrlPath();
425 * Initializes App->page['htmlhead'].
430 * - Registered stylesheets (through App->registerStylesheet())
431 * - Infinite scroll data
432 * - head.tpl template
434 private function initHead(App\Module $module, PConfiguration $pconfig)
436 $interval = ((local_user()) ? $pconfig->get(local_user(), 'system', 'update_interval') : 40000);
438 // If the update is 'deactivated' set it to the highest integer number (~24 days)
440 $interval = 2147483647;
443 if ($interval < 10000) {
447 // Default title: current module called
448 if (empty($this->page['title']) && $module->getName()) {
449 $this->page['title'] = ucfirst($module->getName());
452 // Prepend the sitename to the page title
453 $this->page['title'] = $this->config->get('config', 'sitename', '') . (!empty($this->page['title']) ? ' | ' . $this->page['title'] : '');
455 if (!empty(Core\Renderer::$theme['stylesheet'])) {
456 $stylesheet = Core\Renderer::$theme['stylesheet'];
458 $stylesheet = $this->getCurrentThemeStylesheetPath();
461 $this->registerStylesheet($stylesheet);
463 $shortcut_icon = $this->config->get('system', 'shortcut_icon');
464 if ($shortcut_icon == '') {
465 $shortcut_icon = 'images/friendica-32.png';
468 $touch_icon = $this->config->get('system', 'touch_icon');
469 if ($touch_icon == '') {
470 $touch_icon = 'images/friendica-128.png';
473 Core\Hook::callAll('head', $this->page['htmlhead']);
475 $tpl = Core\Renderer::getMarkupTemplate('head.tpl');
476 /* put the head template at the beginning of page['htmlhead']
477 * since the code added by the modules frequently depends on it
480 $this->page['htmlhead'] = Core\Renderer::replaceMacros($tpl, [
481 '$local_user' => local_user(),
482 '$generator' => 'Friendica' . ' ' . FRIENDICA_VERSION,
483 '$delitem' => $this->l10n->t('Delete this item?'),
484 '$update_interval' => $interval,
485 '$shortcut_icon' => $shortcut_icon,
486 '$touch_icon' => $touch_icon,
487 '$block_public' => intval($this->config->get('system', 'block_public')),
488 '$stylesheets' => $this->stylesheets,
489 ]) . $this->page['htmlhead'];
493 * Initializes App->page['footer'].
496 * - Javascript homebase
497 * - Mobile toggle link
498 * - Registered footer scripts (through App->registerFooterScript())
499 * - footer.tpl template
501 private function initFooter()
503 // If you're just visiting, let javascript take you home
504 if (!empty($_SESSION['visitor_home'])) {
505 $homebase = $_SESSION['visitor_home'];
506 } elseif (local_user()) {
507 $homebase = 'profile/' . $this->user['nickname'];
510 if (isset($homebase)) {
511 $this->page['footer'] .= '<script>var homebase="' . $homebase . '";</script>' . "\n";
515 * Add a "toggle mobile" link if we're using a mobile device
517 if ($this->is_mobile || $this->is_tablet) {
518 if (isset($_SESSION['show-mobile']) && !$_SESSION['show-mobile']) {
519 $link = 'toggle_mobile?address=' . urlencode(curPageURL());
521 $link = 'toggle_mobile?off=1&address=' . urlencode(curPageURL());
523 $this->page['footer'] .= Core\Renderer::replaceMacros(Core\Renderer::getMarkupTemplate("toggle_mobile_footer.tpl"), [
524 '$toggle_link' => $link,
525 '$toggle_text' => $this->l10n->t('toggle mobile')
529 Core\Hook::callAll('footer', $this->page['footer']);
531 $tpl = Core\Renderer::getMarkupTemplate('footer.tpl');
532 $this->page['footer'] = Core\Renderer::replaceMacros($tpl, [
533 '$footerScripts' => $this->footerScripts,
534 ]) . $this->page['footer'];
538 * @brief Removes the base url from an url. This avoids some mixed content problems.
540 * @param string $origURL
542 * @return string The cleaned url
543 * @throws HTTPException\InternalServerErrorException
545 public function removeBaseURL($origURL)
547 // Remove the hostname from the url if it is an internal link
548 $nurl = Util\Strings::normaliseLink($origURL);
549 $base = Util\Strings::normaliseLink($this->getBaseURL());
550 $url = str_replace($base . '/', '', $nurl);
552 // if it is an external link return the orignal value
553 if ($url == Util\Strings::normaliseLink($origURL)) {
561 * Returns the current UserAgent as a String
563 * @return string the UserAgent as a String
564 * @throws HTTPException\InternalServerErrorException
566 public function getUserAgent()
569 FRIENDICA_PLATFORM . " '" .
570 FRIENDICA_CODENAME . "' " .
571 FRIENDICA_VERSION . '-' .
572 DB_UPDATE_VERSION . '; ' .
577 * Checks if the site is called via a backend process
579 * @param Module $module The pre-loaded module (just name, not class!)
581 * @return bool True, if the call is a backend call
583 private function checkBackend(Module $module)
585 return basename(($_SERVER['PHP_SELF'] ?? ''), '.php') !== 'index' ||
586 $module->isBackend();
590 * Returns true, if the call is from a backend node (f.e. from a worker)
592 * @return bool Is it a known backend?
594 public function isBackend()
596 return $this->isBackend;
600 * @brief Checks if the maximum number of database processes is reached
602 * @return bool Is the limit reached?
604 public function isMaxProcessesReached()
606 // Deactivated, needs more investigating if this check really makes sense
610 * Commented out to suppress static analyzer issues
612 if ($this->is_backend()) {
613 $process = 'backend';
614 $max_processes = $this->config->get('system', 'max_processes_backend');
615 if (intval($max_processes) == 0) {
619 $process = 'frontend';
620 $max_processes = $this->config->get('system', 'max_processes_frontend');
621 if (intval($max_processes) == 0) {
626 $processlist = DBA::processlist();
627 if ($processlist['list'] != '') {
628 Core\Logger::log('Processcheck: Processes: ' . $processlist['amount'] . ' - Processlist: ' . $processlist['list'], Core\Logger::DEBUG);
630 if ($processlist['amount'] > $max_processes) {
631 Core\Logger::log('Processcheck: Maximum number of processes for ' . $process . ' tasks (' . $max_processes . ') reached.', Core\Logger::DEBUG);
640 * @brief Checks if the minimal memory is reached
642 * @return bool Is the memory limit reached?
643 * @throws HTTPException\InternalServerErrorException
645 public function isMinMemoryReached()
647 $min_memory = $this->config->get('system', 'min_memory', 0);
648 if ($min_memory == 0) {
652 if (!is_readable('/proc/meminfo')) {
656 $memdata = explode("\n", file_get_contents('/proc/meminfo'));
659 foreach ($memdata as $line) {
660 $data = explode(':', $line);
661 if (count($data) != 2) {
664 list($key, $val) = $data;
665 $meminfo[$key] = (int) trim(str_replace('kB', '', $val));
666 $meminfo[$key] = (int) ($meminfo[$key] / 1024);
669 if (!isset($meminfo['MemFree'])) {
673 $free = $meminfo['MemFree'];
675 $reached = ($free < $min_memory);
678 Core\Logger::log('Minimal memory reached: ' . $free . '/' . $meminfo['MemTotal'] . ' - limit ' . $min_memory, Core\Logger::DEBUG);
685 * @brief Checks if the maximum load is reached
687 * @return bool Is the load reached?
688 * @throws HTTPException\InternalServerErrorException
690 public function isMaxLoadReached()
692 if ($this->isBackend()) {
693 $process = 'backend';
694 $maxsysload = intval($this->config->get('system', 'maxloadavg'));
695 if ($maxsysload < 1) {
699 $process = 'frontend';
700 $maxsysload = intval($this->config->get('system', 'maxloadavg_frontend'));
701 if ($maxsysload < 1) {
706 $load = Core\System::currentLoad();
708 if (intval($load) > $maxsysload) {
709 Core\Logger::log('system: load ' . $load . ' for ' . $process . ' tasks (' . $maxsysload . ') too high.');
717 * Executes a child process with 'proc_open'
719 * @param string $command The command to execute
720 * @param array $args Arguments to pass to the command ( [ 'key' => value, 'key2' => value2, ... ]
721 * @throws HTTPException\InternalServerErrorException
723 public function proc_run($command, $args)
725 if (!function_exists('proc_open')) {
729 $cmdline = $this->config->get('config', 'php_path', 'php') . ' ' . escapeshellarg($command);
731 foreach ($args as $key => $value) {
732 if (!is_null($value) && is_bool($value) && !$value) {
736 $cmdline .= ' --' . $key;
737 if (!is_null($value) && !is_bool($value)) {
738 $cmdline .= ' ' . $value;
742 if ($this->isMinMemoryReached()) {
746 if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
747 $resource = proc_open('cmd /c start /b ' . $cmdline, [], $foo, $this->getBasePath());
749 $resource = proc_open($cmdline . ' &', [], $foo, $this->getBasePath());
751 if (!is_resource($resource)) {
752 Core\Logger::log('We got no resource for command ' . $cmdline, Core\Logger::DEBUG);
755 proc_close($resource);
759 * Generates the site's default sender email address
762 * @throws HTTPException\InternalServerErrorException
764 public function getSenderEmailAddress()
766 $sender_email = $this->config->get('config', 'sender_email');
767 if (empty($sender_email)) {
768 $hostname = $this->baseURL->getHostname();
769 if (strpos($hostname, ':')) {
770 $hostname = substr($hostname, 0, strpos($hostname, ':'));
773 $sender_email = 'noreply@' . $hostname;
776 return $sender_email;
780 * Returns the current theme name.
782 * @return string the name of the current theme
783 * @throws HTTPException\InternalServerErrorException
785 public function getCurrentTheme()
787 if ($this->getMode()->isInstall()) {
791 if (!$this->currentTheme) {
792 $this->computeCurrentTheme();
795 return $this->currentTheme;
798 public function setCurrentTheme($theme)
800 $this->currentTheme = $theme;
804 * Computes the current theme name based on the node settings, the user settings and the device type
808 private function computeCurrentTheme()
810 $system_theme = $this->config->get('system', 'theme');
811 if (!$system_theme) {
812 throw new Exception($this->l10n->t('No system theme config value set.'));
816 $this->currentTheme = $system_theme;
819 // Find the theme that belongs to the user whose stuff we are looking at
820 if ($this->profile_uid && ($this->profile_uid != local_user())) {
821 // Allow folks to override user themes and always use their own on their own site.
822 // This works only if the user is on the same server
823 $user = DBA::selectFirst('user', ['theme'], ['uid' => $this->profile_uid]);
824 if (DBA::isResult($user) && !Core\PConfig::get(local_user(), 'system', 'always_my_theme')) {
825 $page_theme = $user['theme'];
829 $user_theme = Core\Session::get('theme', $system_theme);
831 // Specific mobile theme override
832 if (($this->is_mobile || $this->is_tablet) && Core\Session::get('show-mobile', true)) {
833 $system_mobile_theme = $this->config->get('system', 'mobile-theme');
834 $user_mobile_theme = Core\Session::get('mobile-theme', $system_mobile_theme);
836 // --- means same mobile theme as desktop
837 if (!empty($user_mobile_theme) && $user_mobile_theme !== '---') {
838 $user_theme = $user_mobile_theme;
843 $theme_name = $page_theme;
845 $theme_name = $user_theme;
848 $theme_name = Strings::sanitizeFilePathItem($theme_name);
850 && in_array($theme_name, Theme::getAllowedList())
851 && (file_exists('view/theme/' . $theme_name . '/style.css')
852 || file_exists('view/theme/' . $theme_name . '/style.php'))
854 $this->currentTheme = $theme_name;
859 * @brief Return full URL to theme which is currently in effect.
861 * Provide a sane default if nothing is chosen or the specified theme does not exist.
864 * @throws HTTPException\InternalServerErrorException
866 public function getCurrentThemeStylesheetPath()
868 return Core\Theme::getStylesheetPath($this->getCurrentTheme());
872 * Check if request was an AJAX (xmlhttprequest) request.
874 * @return boolean true if it was an AJAX request
876 public function isAjax()
878 return $this->isAjax;
882 * @deprecated use Arguments->get() instead
886 public function getArgumentValue($position, $default = '')
888 return $this->args->get($position, $default);
892 * Sets the base url for use in cmdline programs which don't have
895 public function checkURL()
897 $url = $this->config->get('system', 'url');
899 // if the url isn't set or the stored url is radically different
900 // than the currently visited url, store the current value accordingly.
901 // "Radically different" ignores common variations such as http vs https
902 // and www.example.com vs example.com.
903 // We will only change the url to an ip address if there is no existing setting
905 if (empty($url) || (!Util\Strings::compareLink($url, $this->getBaseURL())) && (!preg_match("/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/", $this->baseURL->getHostname()))) {
906 $this->config->set('system', 'url', $this->getBaseURL());
911 * Frontend App script
913 * The App object behaves like a container and a dispatcher at the same time, including a representation of the
914 * request and a representation of the response.
916 * This probably should change to limit the size of this monster method.
918 * @param App\Module $module The determined module
920 public function runFrontend(App\Module $module, App\Router $router, PConfiguration $pconfig)
922 $moduleName = $module->getName();
925 // Missing DB connection: ERROR
926 if ($this->getMode()->has(App\Mode::LOCALCONFIGPRESENT) && !$this->getMode()->has(App\Mode::DBAVAILABLE)) {
927 throw new HTTPException\InternalServerErrorException('Apologies but the website is unavailable at the moment.');
930 // Max Load Average reached: ERROR
931 if ($this->isMaxProcessesReached() || $this->isMaxLoadReached()) {
932 header('Retry-After: 120');
933 header('Refresh: 120; url=' . $this->baseURL->get() . "/" . $this->args->getQueryString());
935 throw new HTTPException\ServiceUnavailableException('The node is currently overloaded. Please try again later.');
938 if (!$this->getMode()->isInstall()) {
939 // Force SSL redirection
940 if ($this->baseURL->checkRedirectHttps()) {
941 System::externalRedirect($this->baseURL->get() . '/' . $this->args->getQueryString());
944 Core\Session::init();
945 Core\Hook::callAll('init_1');
948 // Exclude the backend processes from the session management
949 if (!$this->isBackend()) {
950 $stamp1 = microtime(true);
952 $this->profiler->saveTimestamp($stamp1, 'parser', Core\System::callstack());
953 $this->l10n->setSessionVariable();
954 $this->l10n->setLangFromSession();
957 Core\Worker::executeIfIdle();
960 if ($this->getMode()->isNormal()) {
961 $requester = HTTPSignature::getSigner('', $_SERVER);
962 if (!empty($requester)) {
963 Profile::addVisitorCookieForHandle($requester);
968 if (!empty($_GET['zrl']) && $this->getMode()->isNormal()) {
970 // Only continue when the given profile link seems valid
971 // Valid profile links contain a path with "/profile/" and no query parameters
972 if ((parse_url($_GET['zrl'], PHP_URL_QUERY) == "") &&
973 strstr(parse_url($_GET['zrl'], PHP_URL_PATH), "/profile/")) {
974 if (Core\Session::get('visitor_home') != $_GET["zrl"]) {
975 Core\Session::set('my_url', $_GET['zrl']);
976 Core\Session::set('authenticated', 0);
979 Model\Profile::zrlInit($this);
981 // Someone came with an invalid parameter, maybe as a DDoS attempt
982 // We simply stop processing here
983 Core\Logger::log("Invalid ZRL parameter " . $_GET['zrl'], Core\Logger::DEBUG);
984 throw new HTTPException\ForbiddenException();
989 if (!empty($_GET['owt']) && $this->getMode()->isNormal()) {
990 $token = $_GET['owt'];
991 Model\Profile::openWebAuthInit($token);
994 Login::sessionAuth();
996 if (empty($_SESSION['authenticated'])) {
997 header('X-Account-Management-Status: none');
1000 $_SESSION['sysmsg'] = Core\Session::get('sysmsg', []);
1001 $_SESSION['sysmsg_info'] = Core\Session::get('sysmsg_info', []);
1002 $_SESSION['last_updated'] = Core\Session::get('last_updated', []);
1005 * check_config() is responsible for running update scripts. These automatically
1006 * update the DB schema whenever we push a new one out. It also checks to see if
1007 * any addons have been added or removed and reacts accordingly.
1010 // in install mode, any url loads install module
1011 // but we need "view" module for stylesheet
1012 if ($this->getMode()->isInstall() && $moduleName !== 'install') {
1013 $this->internalRedirect('install');
1014 } elseif (!$this->getMode()->isInstall() && !$this->getMode()->has(App\Mode::MAINTENANCEDISABLED) && $moduleName !== 'maintenance') {
1015 $this->internalRedirect('maintenance');
1018 Core\Update::check($this->getBasePath(), false, $this->getMode());
1019 Core\Addon::loadAddons();
1020 Core\Hook::loadHooks();
1031 'right_aside' => '',
1036 // Compatibility with the Android Diaspora client
1037 if ($moduleName == 'stream') {
1038 $this->internalRedirect('network?order=post');
1041 if ($moduleName == 'conversations') {
1042 $this->internalRedirect('message');
1045 if ($moduleName == 'commented') {
1046 $this->internalRedirect('network?order=comment');
1049 if ($moduleName == 'liked') {
1050 $this->internalRedirect('network?order=comment');
1053 if ($moduleName == 'activity') {
1054 $this->internalRedirect('network?conv=1');
1057 if (($moduleName == 'status_messages') && ($this->args->getCommand() == 'status_messages/new')) {
1058 $this->internalRedirect('bookmarklet');
1061 if (($moduleName == 'user') && ($this->args->getCommand() == 'user/edit')) {
1062 $this->internalRedirect('settings');
1065 if (($moduleName == 'tag_followings') && ($this->args->getCommand() == 'tag_followings/manage')) {
1066 $this->internalRedirect('search');
1069 // Initialize module that can set the current theme in the init() method, either directly or via App->profile_uid
1070 $this->page['page_title'] = $moduleName;
1072 // determine the module class and save it to the module instance
1073 // @todo there's an implicit dependency due SESSION::start(), so it has to be called here (yet)
1074 $module = $module->determineClass($this->args, $router, $this->config);
1076 // Let the module run it's internal process (init, get, post, ...)
1077 $module->run($this->l10n, $this, $this->logger, $this->getCurrentTheme(), $_SERVER, $_POST);
1079 } catch(HTTPException $e) {
1080 ModuleHTTPException::rawContent($e);
1086 $moduleClass = $module->getClassName();
1088 $arr = ['content' => $content];
1089 Core\Hook::callAll($moduleClass . '_mod_content', $arr);
1090 $content = $arr['content'];
1091 $arr = ['content' => call_user_func([$moduleClass, 'content'])];
1092 Core\Hook::callAll($moduleClass . '_mod_aftercontent', $arr);
1093 $content .= $arr['content'];
1094 } catch(HTTPException $e) {
1095 $content = ModuleHTTPException::content($e);
1098 // initialise content region
1099 if ($this->getMode()->isNormal()) {
1100 Core\Hook::callAll('page_content_top', $this->page['content']);
1103 $this->page['content'] .= $content;
1105 /* Create the page head after setting the language
1106 * and getting any auth credentials.
1108 * Moved initHead() and initFooter() to after
1109 * all the module functions have executed so that all
1110 * theme choices made by the modules can take effect.
1112 $this->initHead($module, $pconfig);
1114 /* Build the page ending -- this is stuff that goes right before
1115 * the closing </body> tag
1117 $this->initFooter();
1119 if (!$this->isAjax()) {
1120 Core\Hook::callAll('page_end', $this->page['content']);
1123 // Add the navigation (menu) template
1124 if ($moduleName != 'install' && $moduleName != 'maintenance') {
1125 $this->page['htmlhead'] .= Core\Renderer::replaceMacros(Core\Renderer::getMarkupTemplate('nav_head.tpl'), []);
1126 $this->page['nav'] = Content\Nav::build($this);
1129 // Build the page - now that we have all the components
1130 if (isset($_GET["mode"]) && (($_GET["mode"] == "raw") || ($_GET["mode"] == "minimal"))) {
1131 $doc = new DOMDocument();
1133 $target = new DOMDocument();
1134 $target->loadXML("<root></root>");
1136 $content = mb_convert_encoding($this->page["content"], 'HTML-ENTITIES', "UTF-8");
1138 /// @TODO one day, kill those error-surpressing @ stuff, or PHP should ban it
1139 @$doc->loadHTML($content);
1141 $xpath = new DOMXPath($doc);
1143 $list = $xpath->query("//*[contains(@id,'tread-wrapper-')]"); /* */
1145 foreach ($list as $item) {
1146 $item = $target->importNode($item, true);
1148 // And then append it to the target
1149 $target->documentElement->appendChild($item);
1152 if ($_GET["mode"] == "raw") {
1153 header("Content-type: text/html; charset=utf-8");
1155 echo substr($target->saveHTML(), 6, -8);
1161 $page = $this->page;
1162 $profile = $this->profile;
1164 header("X-Friendica-Version: " . FRIENDICA_VERSION);
1165 header("Content-type: text/html; charset=utf-8");
1167 if ($this->config->get('system', 'hsts') && ($this->baseURL->getSSLPolicy() == BaseUrl::SSL_POLICY_FULL)) {
1168 header("Strict-Transport-Security: max-age=31536000");
1171 // Some security stuff
1172 header('X-Content-Type-Options: nosniff');
1173 header('X-XSS-Protection: 1; mode=block');
1174 header('X-Permitted-Cross-Domain-Policies: none');
1175 header('X-Frame-Options: sameorigin');
1177 // Things like embedded OSM maps don't work, when this is enabled
1178 // 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'");
1180 /* We use $_GET["mode"] for special page templates. So we will check if we have
1181 * to load another page template than the default one.
1182 * The page templates are located in /view/php/ or in the theme directory.
1184 if (isset($_GET["mode"])) {
1185 $template = Core\Theme::getPathForFile($_GET["mode"] . '.php');
1188 // If there is no page template use the default page template
1189 if (empty($template)) {
1190 $template = Core\Theme::getPathForFile("default.php");
1193 // Theme templates expect $a as an App instance
1196 // Used as is in view/php/default.php
1197 $lang = $this->l10n->getCurrentLang();
1199 /// @TODO Looks unsafe (remote-inclusion), is maybe not but Core\Theme::getPathForFile() uses file_exists() but does not escape anything
1200 require_once $template;
1204 * Redirects to another module relative to the current Friendica base.
1205 * If you want to redirect to a external URL, use System::externalRedirectTo()
1207 * @param string $toUrl The destination URL (Default is empty, which is the default page of the Friendica node)
1208 * @param bool $ssl if true, base URL will try to get called with https:// (works just for relative paths)
1210 * @throws HTTPException\InternalServerErrorException In Case the given URL is not relative to the Friendica node
1212 public function internalRedirect($toUrl = '', $ssl = false)
1214 if (!empty(parse_url($toUrl, PHP_URL_SCHEME))) {
1215 throw new HTTPException\InternalServerErrorException("'$toUrl is not a relative path, please use System::externalRedirectTo");
1218 $redirectTo = $this->getBaseURL($ssl) . '/' . ltrim($toUrl, '/');
1219 Core\System::externalRedirect($redirectTo);
1223 * Automatically redirects to relative or absolute URL
1224 * Should only be used if it isn't clear if the URL is either internal or external
1226 * @param string $toUrl The target URL
1227 * @throws HTTPException\InternalServerErrorException
1229 public function redirect($toUrl)
1231 if (!empty(parse_url($toUrl, PHP_URL_SCHEME))) {
1232 Core\System::externalRedirect($toUrl);
1234 $this->internalRedirect($toUrl);