Friendica Communications Platform (please note that this is a clone of the repository at github, issues are handled there) https://friendi.ca
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1509 lines
44 KiB

<?php
/**
* @file src/App.php
*/
namespace Friendica;
use Detection\MobileDetect;
use DOMDocument;
use DOMXPath;
use Exception;
use Friendica\Core\Config\Cache\ConfigCacheLoader;
use Friendica\Core\Config\Cache\IConfigCache;
use Friendica\Core\Config\Configuration;
use Friendica\Database\DBA;
use Friendica\Network\HTTPException\InternalServerErrorException;
use Friendica\Util\Profiler;
4 years ago
use Psr\Log\LoggerInterface;
/**
*
* class: App
*
* @brief Our main application structure for the life of this page.
*
* Primarily deals with the URL that got us here
* and tries to make some sense of it, and
* stores our page contents and config storage
* and anything else that might need to be passed around
* before we spit the page out.
*
*/
class App
{
public $module_loaded = false;
public $module_class = null;
public $query_string = '';
public $page = [];
public $profile;
public $profile_uid;
public $user;
public $cid;
public $contact;
public $contacts;
public $page_contact;
public $content;
public $data = [];
public $error = false;
public $cmd = '';
public $argv;
public $argc;
public $module;
public $timezone;
public $interactive = true;
public $identities;
public $is_mobile = false;
public $is_tablet = false;
public $theme_info = [];
public $category;
// Allow themes to control internal parameters
// by changing App values in theme.php
public $sourcename = '';
public $videowidth = 425;
public $videoheight = 350;
public $force_max_items = 0;
public $theme_events_in_profile = true;
public $stylesheets = [];
public $footerScripts = [];
/**
* @var App\Mode The Mode of the Application
*/
private $mode;
/**
* @var string The App base path
*/
private $basePath;
/**
* @var string The App URL path
*/
private $urlPath;
/**
* @var bool true, if the call is from the Friendica APP, otherwise false
*/
private $isFriendicaApp;
/**
* @var bool true, if the call is from an backend node (f.e. worker)
*/
private $isBackend;
/**
* @var string The name of the current theme
*/
private $currentTheme;
/**
* @var bool check if request was an AJAX (xmlhttprequest) request
*/
private $isAjax;
/**
* @var MobileDetect
*/
public $mobileDetect;
4 years ago
/**
* @var Configuration The config
*/
private $config;
/**
* @var LoggerInterface The logger
4 years ago
*/
private $logger;
/**
* @var Profiler The profiler of this app
*/
private $profiler;
/**
* Returns the current config cache of this node
*
* @return IConfigCache
*/
public function getConfigCache()
{
return $this->config->getCache();
}
/**
* The basepath of this app
*
* @return string
*/
public function getBasePath()
{
return $this->basePath;
}
/**
* The Logger of this app
*
* @return LoggerInterface
*/
public function getLogger()
{
return $this->logger;
}
/**
* The profiler of this app
*
* @return Profiler
*/
public function getProfiler()
{
return $this->profiler;
}
/**
* Register a stylesheet file path to be included in the <head> tag of every page.
* Inclusion is done in App->initHead().
* The path can be absolute or relative to the Friendica installation base folder.
*
* @see initHead()
*
* @param string $path
* @throws InternalServerErrorException
*/
public function registerStylesheet($path)
{
$url = str_replace($this->basePath . DIRECTORY_SEPARATOR, '', $path);
$this->stylesheets[] = trim($url, '/');
}
/**
* Register a javascript file path to be included in the <footer> tag of every page.
* Inclusion is done in App->initFooter().
* The path can be absolute or relative to the Friendica installation base folder.
*
* @see initFooter()
*
* @param string $path
* @throws InternalServerErrorException
*/
public function registerFooterScript($path)
{
$url = str_replace($this->basePath . DIRECTORY_SEPARATOR, '', $path);
$this->footerScripts[] = trim($url, '/');
}
public $process_id;
public $queue;
private $scheme;
private $hostname;
/**
* @brief App constructor.
*
* @param string $basePath The basedir of the app
* @param Configuration $config The Configuration
* @param LoggerInterface $logger The current app logger
* @param Profiler $profiler The profiler of this application
4 years ago
* @param bool $isBackend Whether it is used for backend or frontend (Default true=backend)
*
* @throws Exception if the Basepath is not usable
*/
public function __construct($basePath, Configuration $config, LoggerInterface $logger, Profiler $profiler, $isBackend = true)
{
BaseObject::setApp($this);
$this->logger = $logger;
$this->config = $config;
$this->profiler = $profiler;
$cfgBasePath = $this->config->get('system', 'basepath');
$this->basePath = (isset($cfgBasePath) && $cfgBasePath !== '') ? $cfgBasePath : $basePath;
4 years ago
if (!Core\System::isDirectoryUsable($this->basePath, false)) {
throw new Exception('Basepath \'' . $this->basePath . '\' isn\'t usable.');
}
$this->basePath = rtrim($this->basePath, DIRECTORY_SEPARATOR);
$this->checkBackend($isBackend);
$this->checkFriendicaApp();
$this->profiler->reset();
$this->mode = new App\Mode($this->basePath);
$this->reload();
set_time_limit(0);
// This has to be quite large to deal with embedded private photos
ini_set('pcre.backtrack_limit', 500000);
$this->scheme = 'http';
if (!empty($_SERVER['HTTPS']) ||
!empty($_SERVER['HTTP_FORWARDED']) && preg_match('/proto=https/', $_SERVER['HTTP_FORWARDED']) ||
!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https' ||
!empty($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] == 'on' ||
!empty($_SERVER['FRONT_END_HTTPS']) && $_SERVER['FRONT_END_HTTPS'] == 'on' ||
!empty($_SERVER['SERVER_PORT']) && (intval($_SERVER['SERVER_PORT']) == 443) // XXX: reasonable assumption, but isn't this hardcoding too much?
) {
$this->scheme = 'https';
}
if (!empty($_SERVER['SERVER_NAME'])) {
$this->hostname = $_SERVER['SERVER_NAME'];
if (!empty($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] != 80 && $_SERVER['SERVER_PORT'] != 443) {
$this->hostname .= ':' . $_SERVER['SERVER_PORT'];
}
}
set_include_path(
get_include_path() . PATH_SEPARATOR
. $this->basePath . DIRECTORY_SEPARATOR . 'include' . PATH_SEPARATOR
. $this->basePath . DIRECTORY_SEPARATOR . 'library' . PATH_SEPARATOR
. $this->basePath);
if (!empty($_SERVER['QUERY_STRING']) && strpos($_SERVER['QUERY_STRING'], 'pagename=') === 0) {
$this->query_string = substr($_SERVER['QUERY_STRING'], 9);
} elseif (!empty($_SERVER['QUERY_STRING']) && strpos($_SERVER['QUERY_STRING'], 'q=') === 0) {
$this->query_string = substr($_SERVER['QUERY_STRING'], 2);
}
// removing trailing / - maybe a nginx problem
$this->query_string = ltrim($this->query_string, '/');
if (!empty($_GET['pagename'])) {
$this->cmd = trim($_GET['pagename'], '/\\');
} elseif (!empty($_GET['q'])) {
$this->cmd = trim($_GET['q'], '/\\');
}
// fix query_string
$this->query_string = str_replace($this->cmd . '&', $this->cmd . '?', $this->query_string);
// unix style "homedir"
if (substr($this->cmd, 0, 1) === '~') {
$this->cmd = 'profile/' . substr($this->cmd, 1);
}
// Diaspora style profile url
if (substr($this->cmd, 0, 2) === 'u/') {
$this->cmd = 'profile/' . substr($this->cmd, 2);
}
/*
* Break the URL path into C style argc/argv style arguments for our
* modules. Given "http://example.com/module/arg1/arg2", $this->argc
* will be 3 (integer) and $this->argv will contain:
* [0] => 'module'
* [1] => 'arg1'
* [2] => 'arg2'
*
*
* There will always be one argument. If provided a naked domain
* URL, $this->argv[0] is set to "home".
*/
$this->argv = explode('/', $this->cmd);
$this->argc = count($this->argv);
if ((array_key_exists('0', $this->argv)) && strlen($this->argv[0])) {
$this->module = str_replace('.', '_', $this->argv[0]);
$this->module = str_replace('-', '_', $this->module);
} else {
$this->argc = 1;
$this->argv = ['home'];
$this->module = 'home';
}
// Detect mobile devices
$mobile_detect = new MobileDetect();
$this->mobileDetect = $mobile_detect;
$this->is_mobile = $mobile_detect->isMobile();
$this->is_tablet = $mobile_detect->isTablet();
4 years ago
$this->isAjax = strtolower(defaults($_SERVER, 'HTTP_X_REQUESTED_WITH', '')) == 'xmlhttprequest';
// Register template engines
Core\Renderer::registerTemplateEngine('Friendica\Render\FriendicaSmartyEngine');
}
/**
* Returns the Mode of the Application
*
* @return App\Mode The Application Mode
*
* @throws InternalServerErrorException when the mode isn't created
*/
public function getMode()
{
if (empty($this->mode)) {
throw new InternalServerErrorException('Mode of the Application is not defined');
}
return $this->mode;
}
/**
* Reloads the whole app instance
*/
public function reload()
{
$this->determineURLPath();
$this->getMode()->determine($this->basePath);
if ($this->getMode()->has(App\Mode::DBAVAILABLE)) {
Core\Hook::loadHooks();
$loader = new ConfigCacheLoader($this->basePath);
Core\Hook::callAll('load_config', $loader);
$this->config->getCache()->load($loader->loadCoreConfig('addon'), true);
}
$this->loadDefaultTimezone();
Core\L10n::init();
$this->process_id = Core\System::processID('log');
}
/**
* Loads the default timezone
*
* Include support for legacy $default_timezone
*
* @global string $default_timezone
*/
private function loadDefaultTimezone()
{
if ($this->config->get('system', 'default_timezone')) {
$this->timezone = $this->config->get('system', 'default_timezone');
} else {
global $default_timezone;
$this->timezone = !empty($default_timezone) ? $default_timezone : 'UTC';
}
if ($this->timezone) {
date_default_timezone_set($this->timezone);
}
}
/**
* Figure out if we are running at the top of a domain or in a sub-directory and adjust accordingly
*/
private function determineURLPath()
{
/*
* The automatic path detection in this function is currently deactivated,
* see issue https://github.com/friendica/friendica/issues/6679
*
* The problem is that the function seems to be confused with some url.
* These then confuses the detection which changes the url path.
*/
/* Relative script path to the web server root
* Not all of those $_SERVER properties can be present, so we do by inverse priority order
*/
/*
$relative_script_path = '';
$relative_script_path = defaults($_SERVER, 'REDIRECT_URL' , $relative_script_path);
$relative_script_path = defaults($_SERVER, 'REDIRECT_URI' , $relative_script_path);
$relative_script_path = defaults($_SERVER, 'REDIRECT_SCRIPT_URL', $relative_script_path);
$relative_script_path = defaults($_SERVER, 'SCRIPT_URL' , $relative_script_path);
$relative_script_path = defaults($_SERVER, 'REQUEST_URI' , $relative_script_path);
*/
$this->urlPath = $this->config->get('system', 'urlpath');
/* $relative_script_path gives /relative/path/to/friendica/module/parameter
* QUERY_STRING gives pagename=module/parameter
*
* To get /relative/path/to/friendica we perform dirname() for as many levels as there are slashes in the QUERY_STRING
*/
/*
if (!empty($relative_script_path)) {
// Module
if (!empty($_SERVER['QUERY_STRING'])) {
$path = trim(rdirname($relative_script_path, substr_count(trim($_SERVER['QUERY_STRING'], '/'), '/') + 1), '/');
} else {
// Root page
$path = trim($relative_script_path, '/');
}
if ($path && $path != $this->urlPath) {
$this->urlPath = $path;
}
}
*/
}
public function getScheme()
{
return $this->scheme;
}
/**
* @brief Retrieves the Friendica instance base URL
*
* This function assembles the base URL from multiple parts:
* - Protocol is determined either by the request or a combination of
* system.ssl_policy and the $ssl parameter.
* - Host name is determined either by system.hostname or inferred from request
* - Path is inferred from SCRIPT_NAME
*
* Note: $ssl parameter value doesn't directly correlate with the resulting protocol
*
* @param bool $ssl Whether to append http or https under SSL_POLICY_SELFSIGN
* @return string Friendica server base URL
* @throws InternalServerErrorException
*/
public function getBaseURL($ssl = false)
{
$scheme = $this->scheme;
if (Core\Config::get('system', 'ssl_policy') == SSL_POLICY_FULL) {
$scheme = 'https';
}
// Basically, we have $ssl = true on any links which can only be seen by a logged in user
// (and also the login link). Anything seen by an outsider will have it turned off.
if (Core\Config::get('system', 'ssl_policy') == SSL_POLICY_SELFSIGN) {
if ($ssl) {
$scheme = 'https';
} else {
$scheme = 'http';
}
}
if (Core\Config::get('config', 'hostname') != '') {
$this->hostname = Core\Config::get('config', 'hostname');
}
return $scheme . '://' . $this->hostname . (!empty($this->getURLPath()) ? '/' . $this->getURLPath() : '' );
}
/**
* @brief Initializes the baseurl components
*
* Clears the baseurl cache to prevent inconsistencies
*
* @param string $url
* @throws InternalServerErrorException
*/
public function setBaseURL($url)
{
$parsed = @parse_url($url);
$hostname = '';
if (!empty($parsed)) {
if (!empty($parsed['scheme'])) {
$this->scheme = $parsed['scheme'];
}
if (!empty($parsed['host'])) {
$hostname = $parsed['host'];
}
if (!empty($parsed['port'])) {
$hostname .= ':' . $parsed['port'];
}
if (!empty($parsed['path'])) {
$this->urlPath = trim($parsed['path'], '\\/');
}
if (file_exists($this->basePath . '/.htpreconfig.php')) {
include $this->basePath . '/.htpreconfig.php';
}
if (Core\Config::get('config', 'hostname') != '') {
$this->hostname = Core\Config::get('config', 'hostname');
}
if (!isset($this->hostname) || ($this->hostname == '')) {
$this->hostname = $hostname;
}
}
}
public function getHostName()
{
if (Core\Config::get('config', 'hostname') != '') {
$this->hostname = Core\Config::get('config', 'hostname');
}
return $this->hostname;
}
public function getURLPath()
{
return $this->urlPath;
}
/**
* Initializes App->page['htmlhead'].
*
* Includes:
* - Page title
* - Favicons
* - Registered stylesheets (through App->registerStylesheet())
* - Infinite scroll data
* - head.tpl template
*/
public function initHead()
{
$interval = ((local_user()) ? Core\PConfig::get(local_user(), 'system', 'update_interval') : 40000);
// If the update is 'deactivated' set it to the highest integer number (~24 days)
if ($interval < 0) {
$interval = 2147483647;
}
if ($interval < 10000) {
$interval = 40000;
}
// compose the page title from the sitename and the
// current module called
if (!$this->module == '') {
$this->page['title'] = $this->config->get('config', 'sitename') . ' (' . $this->module . ')';
} else {
$this->page['title'] = $this->config->get('config', 'sitename');
}
if (!empty(Core\Renderer::$theme['stylesheet'])) {
$stylesheet = Core\Renderer::$theme['stylesheet'];
} else {
$stylesheet = $this->getCurrentThemeStylesheetPath();
}
$this->registerStylesheet($stylesheet);
$shortcut_icon = Core\Config::get('system', 'shortcut_icon');
if ($shortcut_icon == '') {
$shortcut_icon = 'images/friendica-32.png';
}
$touch_icon = Core\Config::get('system', 'touch_icon');
if ($touch_icon == '') {
$touch_icon = 'images/friendica-128.png';
}
Core\Hook::callAll('head', $this->page['htmlhead']);
$tpl = Core\Renderer::getMarkupTemplate('head.tpl');
/* put the head template at the beginning of page['htmlhead']
* since the code added by the modules frequently depends on it
* being first
*/
$this->page['htmlhead'] = Core\Renderer::replaceMacros($tpl, [
'$baseurl' => $this->getBaseURL(),
'$local_user' => local_user(),
'$generator' => 'Friendica' . ' ' . FRIENDICA_VERSION,
'$delitem' => Core\L10n::t('Delete this item?'),
'$update_interval' => $interval,
'$shortcut_icon' => $shortcut_icon,
'$touch_icon' => $touch_icon,
'$block_public' => intval(Core\Config::get('system', 'block_public')),
'$stylesheets' => $this->stylesheets,
]) . $this->page['htmlhead'];
}
/**
* Initializes App->page['footer'].
*
* Includes:
* - Javascript homebase
* - Mobile toggle link
* - Registered footer scripts (through App->registerFooterScript())
* - footer.tpl template
*/
public function initFooter()
{
// If you're just visiting, let javascript take you home
if (!empty($_SESSION['visitor_home'])) {
$homebase = $_SESSION['visitor_home'];
} elseif (local_user()) {
$homebase = 'profile/' . $this->user['nickname'];
}
if (isset($homebase)) {
$this->page['footer'] .= '<script>var homebase="' . $homebase . '";</script>' . "\n";
}
/*
* Add a "toggle mobile" link if we're using a mobile device
*/
if ($this->is_mobile || $this->is_tablet) {
if (isset($_SESSION['show-mobile']) && !$_SESSION['show-mobile']) {
$link = 'toggle_mobile?address=' . urlencode(curPageURL());
} else {
$link = 'toggle_mobile?off=1&address=' . urlencode(curPageURL());
}
$this->page['footer'] .= Core\Renderer::replaceMacros(Core\Renderer::getMarkupTemplate("toggle_mobile_footer.tpl"), [
'$toggle_link' => $link,
'$toggle_text' => Core\L10n::t('toggle mobile')
]);
}
Core\Hook::callAll('footer', $this->page['footer']);
$tpl = Core\Renderer::getMarkupTemplate('footer.tpl');
$this->page['footer'] = Core\Renderer::replaceMacros($tpl, [
'$baseurl' => $this->getBaseURL(),
'$footerScripts' => $this->footerScripts,
]) . $this->page['footer'];
}
/**
* @brief Removes the base url from an url. This avoids some mixed content problems.
*
* @param string $origURL
*
* @return string The cleaned url
* @throws InternalServerErrorException
*/
public function removeBaseURL($origURL)
{
// Remove the hostname from the url if it is an internal link
$nurl = Util\Strings::normaliseLink($origURL);
$base = Util\Strings::normaliseLink($this->getBaseURL());
$url = str_replace($base . '/', '', $nurl);
// if it is an external link return the orignal value
if ($url == Util\Strings::normaliseLink($origURL)) {
return $origURL;
} else {
return $url;
}
}
/**
* Returns the current UserAgent as a String
*
* @return string the UserAgent as a String
* @throws InternalServerErrorException
*/
public function getUserAgent()
{
return
FRIENDICA_PLATFORM . " '" .
FRIENDICA_CODENAME . "' " .
FRIENDICA_VERSION . '-' .
DB_UPDATE_VERSION . '; ' .
$this->getBaseURL();
}
/**
* Checks, if the call is from the Friendica App
*
* Reason:
* The friendica client has problems with the GUID in the notify. this is some workaround
*/
private function checkFriendicaApp()
{
// Friendica-Client
$this->isFriendicaApp = isset($_SERVER['HTTP_USER_AGENT']) && $_SERVER['HTTP_USER_AGENT'] == 'Apache-HttpClient/UNAVAILABLE (java 1.4)';
}
/**
* Is the call via the Friendica app? (not a "normale" call)
*
* @return bool true if it's from the Friendica app
*/
public function isFriendicaApp()
{
return $this->isFriendicaApp;
}