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.

1869 lines
54 KiB

<?php
/**
* @file src/App.php
*/
namespace Friendica;
use Detection\MobileDetect;
use DOMDocument;
use DOMXPath;
use Exception;
use Friendica\Database\DBA;
use Friendica\Network\HTTPException\InternalServerErrorException;
require_once 'boot.php';
require_once 'include/text.php';
/**
*
* 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 $config = [];
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 $performance = [];
public $callstack = [];
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;
/**
* 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
*/
public function registerStylesheet($path)
{
$url = str_replace($this->getBasePath() . 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
*/
public function registerFooterScript($path)
{
$url = str_replace($this->getBasePath() . DIRECTORY_SEPARATOR, '', $path);
$this->footerScripts[] = trim($url, '/');
}
public $process_id;
public $queue;
private $scheme;
private $hostname;
/**
* @brief App constructor.
*
* @param string $basePath Path to the app base folder
* @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, $isBackend = true)
{
if (!static::isDirectoryUsable($basePath, false)) {
throw new Exception('Basepath ' . $basePath . ' isn\'t usable.');
}
BaseObject::setApp($this);
$this->basePath = rtrim($basePath, DIRECTORY_SEPARATOR);
$this->checkBackend($isBackend);
$this->checkFriendicaApp();
$this->performance['start'] = microtime(true);
$this->performance['database'] = 0;
$this->performance['database_write'] = 0;
$this->performance['cache'] = 0;
$this->performance['cache_write'] = 0;
$this->performance['network'] = 0;
$this->performance['file'] = 0;
$this->performance['rendering'] = 0;
$this->performance['parser'] = 0;
$this->performance['marktime'] = 0;
$this->performance['markstart'] = microtime(true);
$this->callstack['database'] = [];
$this->callstack['database_write'] = [];
$this->callstack['cache'] = [];
$this->callstack['cache_write'] = [];
$this->callstack['network'] = [];
$this->callstack['file'] = [];
$this->callstack['rendering'] = [];
$this->callstack['parser'] = [];
$this->mode = new App\Mode($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->getBasePath() . DIRECTORY_SEPARATOR . 'include' . PATH_SEPARATOR
. $this->getBasePath(). DIRECTORY_SEPARATOR . 'library' . PATH_SEPARATOR
. $this->getBasePath());
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->is_mobile = $mobile_detect->isMobile();
$this->is_tablet = $mobile_detect->isTablet();
3 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()
{
// The order of the following calls is important to ensure proper initialization
$this->loadConfigFiles();
$this->loadDatabase();
$this->getMode()->determine($this->getBasePath());
$this->determineURLPath();
Core\Config::load();
if ($this->getMode()->has(App\Mode::DBAVAILABLE)) {
Core\Hook::loadHooks();
$this->loadAddonConfig();
}
$this->loadDefaultTimezone();
Core\L10n::init();
$this->process_id = Core\System::processID('log');
}
/**
* Load the configuration files
*
* First loads the default value for all the configuration keys, then the legacy configuration files, then the
* expected local.ini.php
*/
private function loadConfigFiles()
{
$this->loadConfigFile($this->getBasePath() . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'config.ini.php');
$this->loadConfigFile($this->getBasePath() . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'settings.ini.php');
// Legacy .htconfig.php support
if (file_exists($this->getBasePath() . DIRECTORY_SEPARATOR . '.htpreconfig.php')) {
$a = $this;
include $this->getBasePath() . DIRECTORY_SEPARATOR . '.htpreconfig.php';
}
// Legacy .htconfig.php support
if (file_exists($this->getBasePath() . DIRECTORY_SEPARATOR . '.htconfig.php')) {
$a = $this;
include $this->getBasePath() . DIRECTORY_SEPARATOR . '.htconfig.php';
$this->setConfigValue('database', 'hostname', $db_host);
$this->setConfigValue('database', 'username', $db_user);
$this->setConfigValue('database', 'password', $db_pass);
$this->setConfigValue('database', 'database', $db_data);
if (isset($a->config['system']['db_charset'])) {
$this->setConfigValue('database', 'charset', $a->config['system']['db_charset']);
}
unset($db_host, $db_user, $db_pass, $db_data);
if (isset($default_timezone)) {
$this->setConfigValue('system', 'default_timezone', $default_timezone);
unset($default_timezone);
}
if (isset($pidfile)) {
$this->setConfigValue('system', 'pidfile', $pidfile);
unset($pidfile);
}
if (isset($lang)) {
$this->setConfigValue('system', 'language', $lang);
unset($lang);
}
}
if (file_exists($this->getBasePath() . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'local.ini.php')) {
$this->loadConfigFile($this->getBasePath() . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'local.ini.php', true);
}
}
/**
* Tries to load the specified configuration file into the App->config array.
* Doesn't overwrite previously set values by default to prevent default config files to supersede DB Config.
*
* The config format is INI and the template for configuration files is the following:
*
* <?php return <<<INI
*
* [section]
* key = value
*
* INI;
* // Keep this line
*
* @param string $filepath
* @param bool $overwrite Force value overwrite if the config key already exists
* @throws Exception
*/
public function loadConfigFile($filepath, $overwrite = false)
{
if (!file_exists($filepath)) {
throw new Exception('Error parsing non-existent config file ' . $filepath);
}
$contents = include($filepath);
$config = parse_ini_string($contents, true, INI_SCANNER_TYPED);
if ($config === false) {
throw new Exception('Error parsing config file ' . $filepath);
}
foreach ($config as $category => $values) {
foreach ($values as $key => $value) {
if ($overwrite) {
$this->setConfigValue($category, $key, $value);
} else {
$this->setDefaultConfigValue($category, $key, $value);
}
}
}
}
/**
* Loads addons configuration files
*
* First loads all activated addons default configuration throught the load_config hook, then load the local.ini.php
* again to overwrite potential local addon configuration.
*/
private function loadAddonConfig()
{
// Loads addons default config
Core\Addon::callHooks('load_config');
// Load the local addon config file to overwritten default addon config values
if (file_exists($this->getBasePath() . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'addon.ini.php')) {
$this->loadConfigFile($this->getBasePath() . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'addon.ini.php', true);
}
}
/**
* Loads the default timezone
*
* Include support for legacy $default_timezone
*
* @global string $default_timezone
*/
private function loadDefaultTimezone()
{
if ($this->getConfigValue('system', 'default_timezone')) {
$this->timezone = $this->getConfigValue('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()
{
/* 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->getConfigValue('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(dirname($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 loadDatabase()
{
if (DBA::connected()) {
return;
}
$db_host = $this->getConfigValue('database', 'hostname');
$db_user = $this->getConfigValue('database', 'username');
$db_pass = $this->getConfigValue('database', 'password');
$db_data = $this->getConfigValue('database', 'database');
$charset = $this->getConfigValue('database', 'charset');
// Use environment variables for mysql if they are set beforehand
if (!empty(getenv('MYSQL_HOST'))
&& (!empty(getenv('MYSQL_USERNAME')) || !empty(getenv('MYSQL_USER')))
&& getenv('MYSQL_PASSWORD') !== false
&& !empty(getenv('MYSQL_DATABASE')))
{
$db_host = getenv('MYSQL_HOST');
if (!empty(getenv('MYSQL_PORT'))) {
$db_host .= ':' . getenv('MYSQL_PORT');
}
if (!empty(getenv('MYSQL_USERNAME'))) {
$db_user = getenv('MYSQL_USERNAME');
} else {
$db_user = getenv('MYSQL_USER');
}
$db_pass = (string) getenv('MYSQL_PASSWORD');
$db_data = getenv('MYSQL_DATABASE');
}
$stamp1 = microtime(true);
if (DBA::connect($db_host, $db_user, $db_pass, $db_data, $charset)) {
// Loads DB_UPDATE_VERSION constant
Database\DBStructure::definition(false);
}
unset($db_host, $db_user, $db_pass, $db_data, $charset);
$this->saveTimestamp($stamp1, 'network');
}
/**
* @brief Returns the base filesystem path of the App
*
* It first checks for the internal variable, then for DOCUMENT_ROOT and
* finally for PWD
*
* @return string
*/
public function getBasePath()
{
$basepath = $this->basePath;
if (!$basepath) {
$basepath = Core\Config::get('system', 'basepath');
}
if (!$basepath && !empty($_SERVER['DOCUMENT_ROOT'])) {
$basepath = $_SERVER['DOCUMENT_ROOT'];
}
if (!$basepath && !empty($_SERVER['PWD'])) {
$basepath = $_SERVER['PWD'];
}
return self::getRealPath($basepath);
}
/**
* @brief Returns a normalized file path
*
* This is a wrapper for the "realpath" function.
* That function cannot detect the real path when some folders aren't readable.
* Since this could happen with some hosters we need to handle this.
*
* @param string $path The path that is about to be normalized
* @return string normalized path - when possible
*/
public static function getRealPath($path)
{
$normalized = realpath($path);
if (!is_bool($normalized)) {
return $normalized;
} else {
return $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
*/
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
*/
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->getBasePath() . DIRECTORY_SEPARATOR . '.htpreconfig.php')) {
include $this->getBasePath() . DIRECTORY_SEPARATOR . '.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['sitename'] . ' (' . $this->module . ')';
} else {
$this->page['title'] = $this->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\Addon::callHooks('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?'),
'$showmore' => Core\L10n::t('show more'),
'$showfewer' => Core\L10n::t('show fewer'),
'$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'];