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.

1868 lines
54 KiB

3 years ago
3 years ago
3 years ago
  1. <?php
  2. /**
  3. * @file src/App.php
  4. */
  5. namespace Friendica;
  6. use Detection\MobileDetect;
  7. use DOMDocument;
  8. use DOMXPath;
  9. use Exception;
  10. use Friendica\Database\DBA;
  11. use Friendica\Network\HTTPException\InternalServerErrorException;
  12. require_once 'boot.php';
  13. require_once 'include/text.php';
  14. /**
  15. *
  16. * class: App
  17. *
  18. * @brief Our main application structure for the life of this page.
  19. *
  20. * Primarily deals with the URL that got us here
  21. * and tries to make some sense of it, and
  22. * stores our page contents and config storage
  23. * and anything else that might need to be passed around
  24. * before we spit the page out.
  25. *
  26. */
  27. class App
  28. {
  29. public $module_loaded = false;
  30. public $module_class = null;
  31. public $query_string = '';
  32. public $config = [];
  33. public $page = [];
  34. public $profile;
  35. public $profile_uid;
  36. public $user;
  37. public $cid;
  38. public $contact;
  39. public $contacts;
  40. public $page_contact;
  41. public $content;
  42. public $data = [];
  43. public $error = false;
  44. public $cmd = '';
  45. public $argv;
  46. public $argc;
  47. public $module;
  48. public $timezone;
  49. public $interactive = true;
  50. public $identities;
  51. public $is_mobile = false;
  52. public $is_tablet = false;
  53. public $performance = [];
  54. public $callstack = [];
  55. public $theme_info = [];
  56. public $category;
  57. // Allow themes to control internal parameters
  58. // by changing App values in theme.php
  59. public $sourcename = '';
  60. public $videowidth = 425;
  61. public $videoheight = 350;
  62. public $force_max_items = 0;
  63. public $theme_events_in_profile = true;
  64. public $stylesheets = [];
  65. public $footerScripts = [];
  66. /**
  67. * @var App\Mode The Mode of the Application
  68. */
  69. private $mode;
  70. /**
  71. * @var string The App base path
  72. */
  73. private $basePath;
  74. /**
  75. * @var string The App URL path
  76. */
  77. private $urlPath;
  78. /**
  79. * @var bool true, if the call is from the Friendica APP, otherwise false
  80. */
  81. private $isFriendicaApp;
  82. /**
  83. * @var bool true, if the call is from an backend node (f.e. worker)
  84. */
  85. private $isBackend;
  86. /**
  87. * @var string The name of the current theme
  88. */
  89. private $currentTheme;
  90. /**
  91. * @var bool check if request was an AJAX (xmlhttprequest) request
  92. */
  93. private $isAjax;
  94. /**
  95. * Register a stylesheet file path to be included in the <head> tag of every page.
  96. * Inclusion is done in App->initHead().
  97. * The path can be absolute or relative to the Friendica installation base folder.
  98. *
  99. * @see initHead()
  100. *
  101. * @param string $path
  102. */
  103. public function registerStylesheet($path)
  104. {
  105. $url = str_replace($this->getBasePath() . DIRECTORY_SEPARATOR, '', $path);
  106. $this->stylesheets[] = trim($url, '/');
  107. }
  108. /**
  109. * Register a javascript file path to be included in the <footer> tag of every page.
  110. * Inclusion is done in App->initFooter().
  111. * The path can be absolute or relative to the Friendica installation base folder.
  112. *
  113. * @see initFooter()
  114. *
  115. * @param string $path
  116. */
  117. public function registerFooterScript($path)
  118. {
  119. $url = str_replace($this->getBasePath() . DIRECTORY_SEPARATOR, '', $path);
  120. $this->footerScripts[] = trim($url, '/');
  121. }
  122. public $process_id;
  123. public $queue;
  124. private $scheme;
  125. private $hostname;
  126. /**
  127. * @brief App constructor.
  128. *
  129. * @param string $basePath Path to the app base folder
  130. * @param bool $isBackend Whether it is used for backend or frontend (Default true=backend)
  131. *
  132. * @throws Exception if the Basepath is not usable
  133. */
  134. public function __construct($basePath, $isBackend = true)
  135. {
  136. if (!static::isDirectoryUsable($basePath, false)) {
  137. throw new Exception('Basepath ' . $basePath . ' isn\'t usable.');
  138. }
  139. BaseObject::setApp($this);
  140. $this->basePath = rtrim($basePath, DIRECTORY_SEPARATOR);
  141. $this->checkBackend($isBackend);
  142. $this->checkFriendicaApp();
  143. $this->performance['start'] = microtime(true);
  144. $this->performance['database'] = 0;
  145. $this->performance['database_write'] = 0;
  146. $this->performance['cache'] = 0;
  147. $this->performance['cache_write'] = 0;
  148. $this->performance['network'] = 0;
  149. $this->performance['file'] = 0;
  150. $this->performance['rendering'] = 0;
  151. $this->performance['parser'] = 0;
  152. $this->performance['marktime'] = 0;
  153. $this->performance['markstart'] = microtime(true);
  154. $this->callstack['database'] = [];
  155. $this->callstack['database_write'] = [];
  156. $this->callstack['cache'] = [];
  157. $this->callstack['cache_write'] = [];
  158. $this->callstack['network'] = [];
  159. $this->callstack['file'] = [];
  160. $this->callstack['rendering'] = [];
  161. $this->callstack['parser'] = [];
  162. $this->mode = new App\Mode($basePath);
  163. $this->reload();
  164. set_time_limit(0);
  165. // This has to be quite large to deal with embedded private photos
  166. ini_set('pcre.backtrack_limit', 500000);
  167. $this->scheme = 'http';
  168. if (!empty($_SERVER['HTTPS']) ||
  169. !empty($_SERVER['HTTP_FORWARDED']) && preg_match('/proto=https/', $_SERVER['HTTP_FORWARDED']) ||
  170. !empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https' ||
  171. !empty($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] == 'on' ||
  172. !empty($_SERVER['FRONT_END_HTTPS']) && $_SERVER['FRONT_END_HTTPS'] == 'on' ||
  173. !empty($_SERVER['SERVER_PORT']) && (intval($_SERVER['SERVER_PORT']) == 443) // XXX: reasonable assumption, but isn't this hardcoding too much?
  174. ) {
  175. $this->scheme = 'https';
  176. }
  177. if (!empty($_SERVER['SERVER_NAME'])) {
  178. $this->hostname = $_SERVER['SERVER_NAME'];
  179. if (!empty($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] != 80 && $_SERVER['SERVER_PORT'] != 443) {
  180. $this->hostname .= ':' . $_SERVER['SERVER_PORT'];
  181. }
  182. }
  183. set_include_path(
  184. get_include_path() . PATH_SEPARATOR
  185. . $this->getBasePath() . DIRECTORY_SEPARATOR . 'include' . PATH_SEPARATOR
  186. . $this->getBasePath(). DIRECTORY_SEPARATOR . 'library' . PATH_SEPARATOR
  187. . $this->getBasePath());
  188. if (!empty($_SERVER['QUERY_STRING']) && strpos($_SERVER['QUERY_STRING'], 'pagename=') === 0) {
  189. $this->query_string = substr($_SERVER['QUERY_STRING'], 9);
  190. } elseif (!empty($_SERVER['QUERY_STRING']) && strpos($_SERVER['QUERY_STRING'], 'q=') === 0) {
  191. $this->query_string = substr($_SERVER['QUERY_STRING'], 2);
  192. }
  193. // removing trailing / - maybe a nginx problem
  194. $this->query_string = ltrim($this->query_string, '/');
  195. if (!empty($_GET['pagename'])) {
  196. $this->cmd = trim($_GET['pagename'], '/\\');
  197. } elseif (!empty($_GET['q'])) {
  198. $this->cmd = trim($_GET['q'], '/\\');
  199. }
  200. // fix query_string
  201. $this->query_string = str_replace($this->cmd . '&', $this->cmd . '?', $this->query_string);
  202. // unix style "homedir"
  203. if (substr($this->cmd, 0, 1) === '~') {
  204. $this->cmd = 'profile/' . substr($this->cmd, 1);
  205. }
  206. // Diaspora style profile url
  207. if (substr($this->cmd, 0, 2) === 'u/') {
  208. $this->cmd = 'profile/' . substr($this->cmd, 2);
  209. }
  210. /*
  211. * Break the URL path into C style argc/argv style arguments for our
  212. * modules. Given "http://example.com/module/arg1/arg2", $this->argc
  213. * will be 3 (integer) and $this->argv will contain:
  214. * [0] => 'module'
  215. * [1] => 'arg1'
  216. * [2] => 'arg2'
  217. *
  218. *
  219. * There will always be one argument. If provided a naked domain
  220. * URL, $this->argv[0] is set to "home".
  221. */
  222. $this->argv = explode('/', $this->cmd);
  223. $this->argc = count($this->argv);
  224. if ((array_key_exists('0', $this->argv)) && strlen($this->argv[0])) {
  225. $this->module = str_replace('.', '_', $this->argv[0]);
  226. $this->module = str_replace('-', '_', $this->module);
  227. } else {
  228. $this->argc = 1;
  229. $this->argv = ['home'];
  230. $this->module = 'home';
  231. }
  232. // Detect mobile devices
  233. $mobile_detect = new MobileDetect();
  234. $this->is_mobile = $mobile_detect->isMobile();
  235. $this->is_tablet = $mobile_detect->isTablet();
  236. $this->isAjax = strtolower(defaults($_SERVER, 'HTTP_X_REQUESTED_WITH', '')) == 'xmlhttprequest';
  237. // Register template engines
  238. Core\Renderer::registerTemplateEngine('Friendica\Render\FriendicaSmartyEngine');
  239. }
  240. /**
  241. * Returns the Mode of the Application
  242. *
  243. * @return App\Mode The Application Mode
  244. *
  245. * @throws InternalServerErrorException when the mode isn't created
  246. */
  247. public function getMode()
  248. {
  249. if (empty($this->mode)) {
  250. throw new InternalServerErrorException('Mode of the Application is not defined');
  251. }
  252. return $this->mode;
  253. }
  254. /**
  255. * Reloads the whole app instance
  256. */
  257. public function reload()
  258. {
  259. // The order of the following calls is important to ensure proper initialization
  260. $this->loadConfigFiles();
  261. $this->loadDatabase();
  262. $this->getMode()->determine($this->getBasePath());
  263. $this->determineURLPath();
  264. Core\Config::load();
  265. if ($this->getMode()->has(App\Mode::DBAVAILABLE)) {
  266. Core\Hook::loadHooks();
  267. $this->loadAddonConfig();
  268. }
  269. $this->loadDefaultTimezone();
  270. Core\L10n::init();
  271. $this->process_id = Core\System::processID('log');
  272. }
  273. /**
  274. * Load the configuration files
  275. *
  276. * First loads the default value for all the configuration keys, then the legacy configuration files, then the
  277. * expected local.ini.php
  278. */
  279. private function loadConfigFiles()
  280. {
  281. $this->loadConfigFile($this->getBasePath() . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'config.ini.php');
  282. $this->loadConfigFile($this->getBasePath() . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'settings.ini.php');
  283. // Legacy .htconfig.php support
  284. if (file_exists($this->getBasePath() . DIRECTORY_SEPARATOR . '.htpreconfig.php')) {
  285. $a = $this;
  286. include $this->getBasePath() . DIRECTORY_SEPARATOR . '.htpreconfig.php';
  287. }
  288. // Legacy .htconfig.php support
  289. if (file_exists($this->getBasePath() . DIRECTORY_SEPARATOR . '.htconfig.php')) {
  290. $a = $this;
  291. include $this->getBasePath() . DIRECTORY_SEPARATOR . '.htconfig.php';
  292. $this->setConfigValue('database', 'hostname', $db_host);
  293. $this->setConfigValue('database', 'username', $db_user);
  294. $this->setConfigValue('database', 'password', $db_pass);
  295. $this->setConfigValue('database', 'database', $db_data);
  296. if (isset($a->config['system']['db_charset'])) {
  297. $this->setConfigValue('database', 'charset', $a->config['system']['db_charset']);
  298. }
  299. unset($db_host, $db_user, $db_pass, $db_data);
  300. if (isset($default_timezone)) {
  301. $this->setConfigValue('system', 'default_timezone', $default_timezone);
  302. unset($default_timezone);
  303. }
  304. if (isset($pidfile)) {
  305. $this->setConfigValue('system', 'pidfile', $pidfile);
  306. unset($pidfile);
  307. }
  308. if (isset($lang)) {
  309. $this->setConfigValue('system', 'language', $lang);
  310. unset($lang);
  311. }
  312. }
  313. if (file_exists($this->getBasePath() . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'local.ini.php')) {
  314. $this->loadConfigFile($this->getBasePath() . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'local.ini.php', true);
  315. }
  316. }
  317. /**
  318. * Tries to load the specified configuration file into the App->config array.
  319. * Doesn't overwrite previously set values by default to prevent default config files to supersede DB Config.
  320. *
  321. * The config format is INI and the template for configuration files is the following:
  322. *
  323. * <?php return <<<INI
  324. *
  325. * [section]
  326. * key = value
  327. *
  328. * INI;
  329. * // Keep this line
  330. *
  331. * @param string $filepath
  332. * @param bool $overwrite Force value overwrite if the config key already exists
  333. * @throws Exception
  334. */
  335. public function loadConfigFile($filepath, $overwrite = false)
  336. {
  337. if (!file_exists($filepath)) {
  338. throw new Exception('Error parsing non-existent config file ' . $filepath);
  339. }
  340. $contents = include($filepath);
  341. $config = parse_ini_string($contents, true, INI_SCANNER_TYPED);
  342. if ($config === false) {
  343. throw new Exception('Error parsing config file ' . $filepath);
  344. }
  345. foreach ($config as $category => $values) {
  346. foreach ($values as $key => $value) {
  347. if ($overwrite) {
  348. $this->setConfigValue($category, $key, $value);
  349. } else {
  350. $this->setDefaultConfigValue($category, $key, $value);
  351. }
  352. }
  353. }
  354. }
  355. /**
  356. * Loads addons configuration files
  357. *
  358. * First loads all activated addons default configuration throught the load_config hook, then load the local.ini.php
  359. * again to overwrite potential local addon configuration.
  360. */
  361. private function loadAddonConfig()
  362. {
  363. // Loads addons default config
  364. Core\Addon::callHooks('load_config');
  365. // Load the local addon config file to overwritten default addon config values
  366. if (file_exists($this->getBasePath() . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'addon.ini.php')) {
  367. $this->loadConfigFile($this->getBasePath() . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'addon.ini.php', true);
  368. }
  369. }
  370. /**
  371. * Loads the default timezone
  372. *
  373. * Include support for legacy $default_timezone
  374. *
  375. * @global string $default_timezone
  376. */
  377. private function loadDefaultTimezone()
  378. {
  379. if ($this->getConfigValue('system', 'default_timezone')) {
  380. $this->timezone = $this->getConfigValue('system', 'default_timezone');
  381. } else {
  382. global $default_timezone;
  383. $this->timezone = !empty($default_timezone) ? $default_timezone : 'UTC';
  384. }
  385. if ($this->timezone) {
  386. date_default_timezone_set($this->timezone);
  387. }
  388. }
  389. /**
  390. * Figure out if we are running at the top of a domain or in a sub-directory and adjust accordingly
  391. */
  392. private function determineURLPath()
  393. {
  394. /* Relative script path to the web server root
  395. * Not all of those $_SERVER properties can be present, so we do by inverse priority order
  396. */
  397. $relative_script_path = '';
  398. $relative_script_path = defaults($_SERVER, 'REDIRECT_URL' , $relative_script_path);
  399. $relative_script_path = defaults($_SERVER, 'REDIRECT_URI' , $relative_script_path);
  400. $relative_script_path = defaults($_SERVER, 'REDIRECT_SCRIPT_URL', $relative_script_path);
  401. $relative_script_path = defaults($_SERVER, 'SCRIPT_URL' , $relative_script_path);
  402. $relative_script_path = defaults($_SERVER, 'REQUEST_URI' , $relative_script_path);
  403. $this->urlPath = $this->getConfigValue('system', 'urlpath');
  404. /* $relative_script_path gives /relative/path/to/friendica/module/parameter
  405. * QUERY_STRING gives pagename=module/parameter
  406. *
  407. * To get /relative/path/to/friendica we perform dirname() for as many levels as there are slashes in the QUERY_STRING
  408. */
  409. if (!empty($relative_script_path)) {
  410. // Module
  411. if (!empty($_SERVER['QUERY_STRING'])) {
  412. $path = trim(dirname($relative_script_path, substr_count(trim($_SERVER['QUERY_STRING'], '/'), '/') + 1), '/');
  413. } else {
  414. // Root page
  415. $path = trim($relative_script_path, '/');
  416. }
  417. if ($path && $path != $this->urlPath) {
  418. $this->urlPath = $path;
  419. }
  420. }
  421. }
  422. public function loadDatabase()
  423. {
  424. if (DBA::connected()) {
  425. return;
  426. }
  427. $db_host = $this->getConfigValue('database', 'hostname');
  428. $db_user = $this->getConfigValue('database', 'username');
  429. $db_pass = $this->getConfigValue('database', 'password');
  430. $db_data = $this->getConfigValue('database', 'database');
  431. $charset = $this->getConfigValue('database', 'charset');
  432. // Use environment variables for mysql if they are set beforehand
  433. if (!empty(getenv('MYSQL_HOST'))
  434. && (!empty(getenv('MYSQL_USERNAME')) || !empty(getenv('MYSQL_USER')))
  435. && getenv('MYSQL_PASSWORD') !== false
  436. && !empty(getenv('MYSQL_DATABASE')))
  437. {
  438. $db_host = getenv('MYSQL_HOST');
  439. if (!empty(getenv('MYSQL_PORT'))) {
  440. $db_host .= ':' . getenv('MYSQL_PORT');
  441. }
  442. if (!empty(getenv('MYSQL_USERNAME'))) {
  443. $db_user = getenv('MYSQL_USERNAME');
  444. } else {
  445. $db_user = getenv('MYSQL_USER');
  446. }
  447. $db_pass = (string) getenv('MYSQL_PASSWORD');
  448. $db_data = getenv('MYSQL_DATABASE');
  449. }
  450. $stamp1 = microtime(true);
  451. if (DBA::connect($db_host, $db_user, $db_pass, $db_data, $charset)) {
  452. // Loads DB_UPDATE_VERSION constant
  453. Database\DBStructure::definition(false);
  454. }
  455. unset($db_host, $db_user, $db_pass, $db_data, $charset);
  456. $this->saveTimestamp($stamp1, 'network');
  457. }
  458. /**
  459. * @brief Returns the base filesystem path of the App
  460. *
  461. * It first checks for the internal variable, then for DOCUMENT_ROOT and
  462. * finally for PWD
  463. *
  464. * @return string
  465. */
  466. public function getBasePath()
  467. {
  468. $basepath = $this->basePath;
  469. if (!$basepath) {
  470. $basepath = Core\Config::get('system', 'basepath');
  471. }
  472. if (!$basepath && !empty($_SERVER['DOCUMENT_ROOT'])) {
  473. $basepath = $_SERVER['DOCUMENT_ROOT'];
  474. }
  475. if (!$basepath && !empty($_SERVER['PWD'])) {
  476. $basepath = $_SERVER['PWD'];
  477. }
  478. return self::getRealPath($basepath);
  479. }
  480. /**
  481. * @brief Returns a normalized file path
  482. *
  483. * This is a wrapper for the "realpath" function.
  484. * That function cannot detect the real path when some folders aren't readable.
  485. * Since this could happen with some hosters we need to handle this.
  486. *
  487. * @param string $path The path that is about to be normalized
  488. * @return string normalized path - when possible
  489. */
  490. public static function getRealPath($path)
  491. {
  492. $normalized = realpath($path);
  493. if (!is_bool($normalized)) {
  494. return $normalized;
  495. } else {
  496. return $path;
  497. }
  498. }
  499. public function getScheme()
  500. {
  501. return $this->scheme;
  502. }
  503. /**
  504. * @brief Retrieves the Friendica instance base URL
  505. *
  506. * This function assembles the base URL from multiple parts:
  507. * - Protocol is determined either by the request or a combination of
  508. * system.ssl_policy and the $ssl parameter.
  509. * - Host name is determined either by system.hostname or inferred from request
  510. * - Path is inferred from SCRIPT_NAME
  511. *
  512. * Note: $ssl parameter value doesn't directly correlate with the resulting protocol
  513. *
  514. * @param bool $ssl Whether to append http or https under SSL_POLICY_SELFSIGN
  515. * @return string Friendica server base URL
  516. */
  517. public function getBaseURL($ssl = false)
  518. {
  519. $scheme = $this->scheme;
  520. if (Core\Config::get('system', 'ssl_policy') == SSL_POLICY_FULL) {
  521. $scheme = 'https';
  522. }
  523. // Basically, we have $ssl = true on any links which can only be seen by a logged in user
  524. // (and also the login link). Anything seen by an outsider will have it turned off.
  525. if (Core\Config::get('system', 'ssl_policy') == SSL_POLICY_SELFSIGN) {
  526. if ($ssl) {
  527. $scheme = 'https';
  528. } else {
  529. $scheme = 'http';
  530. }
  531. }
  532. if (Core\Config::get('config', 'hostname') != '') {
  533. $this->hostname = Core\Config::get('config', 'hostname');
  534. }
  535. return $scheme . '://' . $this->hostname . (!empty($this->getURLPath()) ? '/' . $this->getURLPath() : '' );
  536. }
  537. /**
  538. * @brief Initializes the baseurl components
  539. *
  540. * Clears the baseurl cache to prevent inconsistencies
  541. *
  542. * @param string $url
  543. */
  544. public function setBaseURL($url)
  545. {
  546. $parsed = @parse_url($url);
  547. $hostname = '';
  548. if (!empty($parsed)) {
  549. if (!empty($parsed['scheme'])) {
  550. $this->scheme = $parsed['scheme'];
  551. }
  552. if (!empty($parsed['host'])) {
  553. $hostname = $parsed['host'];
  554. }
  555. if (!empty($parsed['port'])) {
  556. $hostname .= ':' . $parsed['port'];
  557. }
  558. if (!empty($parsed['path'])) {
  559. $this->urlPath = trim($parsed['path'], '\\/');
  560. }
  561. if (file_exists($this->getBasePath() . DIRECTORY_SEPARATOR . '.htpreconfig.php')) {
  562. include $this->getBasePath() . DIRECTORY_SEPARATOR . '.htpreconfig.php';
  563. }
  564. if (Core\Config::get('config', 'hostname') != '') {
  565. $this->hostname = Core\Config::get('config', 'hostname');
  566. }
  567. if (!isset($this->hostname) || ($this->hostname == '')) {
  568. $this->hostname = $hostname;
  569. }
  570. }
  571. }
  572. public function getHostName()
  573. {
  574. if (Core\Config::get('config', 'hostname') != '') {
  575. $this->hostname = Core\Config::get('config', 'hostname');
  576. }
  577. return $this->hostname;
  578. }
  579. public function getURLPath()
  580. {
  581. return $this->urlPath;
  582. }
  583. /**
  584. * Initializes App->page['htmlhead'].
  585. *
  586. * Includes:
  587. * - Page title
  588. * - Favicons
  589. * - Registered stylesheets (through App->registerStylesheet())
  590. * - Infinite scroll data
  591. * - head.tpl template
  592. */
  593. public function initHead()
  594. {
  595. $interval = ((local_user()) ? Core\PConfig::get(local_user(), 'system', 'update_interval') : 40000);
  596. // If the update is 'deactivated' set it to the highest integer number (~24 days)
  597. if ($interval < 0) {
  598. $interval = 2147483647;
  599. }
  600. if ($interval < 10000) {
  601. $interval = 40000;
  602. }
  603. // compose the page title from the sitename and the
  604. // current module called
  605. if (!$this->module == '') {
  606. $this->page['title'] = $this->config['sitename'] . ' (' . $this->module . ')';
  607. } else {
  608. $this->page['title'] = $this->config['sitename'];
  609. }
  610. if (!empty(Core\Renderer::$theme['stylesheet'])) {
  611. $stylesheet = Core\Renderer::$theme['stylesheet'];
  612. } else {
  613. $stylesheet = $this->getCurrentThemeStylesheetPath();
  614. }
  615. $this->registerStylesheet($stylesheet);
  616. $shortcut_icon = Core\Config::get('system', 'shortcut_icon');
  617. if ($shortcut_icon == '') {
  618. $shortcut_icon = 'images/friendica-32.png';
  619. }
  620. $touch_icon = Core\Config::get('system', 'touch_icon');
  621. if ($touch_icon == '') {
  622. $touch_icon = 'images/friendica-128.png';
  623. }
  624. Core\Addon::callHooks('head', $this->page['htmlhead']);
  625. $tpl = Core\Renderer::getMarkupTemplate('head.tpl');
  626. /* put the head template at the beginning of page['htmlhead']
  627. * since the code added by the modules frequently depends on it
  628. * being first
  629. */
  630. $this->page['htmlhead'] = Core\Renderer::replaceMacros($tpl, [
  631. '$baseurl' => $this->getBaseURL(),
  632. '$local_user' => local_user(),
  633. '$generator' => 'Friendica' . ' ' . FRIENDICA_VERSION,
  634. '$delitem' => Core\L10n::t('Delete this item?'),
  635. '$showmore' => Core\L10n::t('show more'),
  636. '$showfewer' => Core\L10n::t('show fewer'),
  637. '$update_interval' => $interval,
  638. '$shortcut_icon' => $shortcut_icon,
  639. '$touch_icon' => $touch_icon,
  640. '$block_public' => intval(Core\Config::get('system', 'block_public')),
  641. '$stylesheets' => $this->stylesheets,
  642. ]) . $this->page['htmlhead'];
  643. }
  644. /**
  645. * Initializes App->page['footer'].
  646. *
  647. * Includes:
  648. * - Javascript homebase
  649. * - Mobile toggle link
  650. * - Registered footer scripts (through App->registerFooterScript())
  651. * - footer.tpl template
  652. */
  653. public function initFooter()
  654. {
  655. // If you're just visiting, let javascript take you home
  656. if (!empty($_SESSION['visitor_home'])) {
  657. $homebase = $_SESSION['visitor_home'];
  658. } elseif (local_user()) {
  659. $homebase = 'profile/' . $this->user['nickname'];
  660. }
  661. if (isset($homebase)) {
  662. $this->page['footer'] .= '<script>var homebase="' . $homebase . '";</script>' . "\n";
  663. }
  664. /*
  665. * Add a "toggle mobile" link if we're using a mobile device
  666. */
  667. if ($this->is_mobile || $this->is_tablet) {
  668. if (isset($_SESSION['show-mobile']) && !$_SESSION['show-mobile']) {
  669. $link = 'toggle_mobile?address=' . curPageURL();
  670. } else {
  671. $link = 'toggle_mobile?off=1&address=' . curPageURL();
  672. }
  673. $this->page['footer'] .= Core\Renderer::replaceMacros(Core\Renderer::getMarkupTemplate("toggle_mobile_footer.tpl"), [
  674. '$toggle_link' => $link,
  675. '$toggle_text' => Core\L10n::t('toggle mobile')
  676. ]);
  677. }
  678. Core\Addon::callHooks('footer', $this->page['footer']);
  679. $tpl = Core\Renderer::getMarkupTemplate('footer.tpl');
  680. $this->page['footer'] = Core\Renderer::replaceMacros($tpl, [
  681. '$baseurl' => $this->getBaseURL(),
  682. '$footerScripts' => $this->footerScripts,
  683. ]) . $this->page['footer'];
  684. }
  685. /**
  686. * @brief Removes the base url from an url. This avoids some mixed content problems.
  687. *
  688. * @param string $origURL
  689. *
  690. * @return string The cleaned url
  691. */
  692. public function removeBaseURL($origURL)
  693. {
  694. // Remove the hostname from the url if it is an internal link
  695. $nurl = Util\Strings::normaliseLink($origURL);
  696. $base = Util\Strings::normaliseLink($this->getBaseURL());
  697. $url = str_replace($base . '/', '', $nurl);
  698. // if it is an external link return the orignal value
  699. if ($url == Util\Strings::normaliseLink($origURL)) {
  700. return $origURL;
  701. } else {
  702. return $url;
  703. }
  704. }
  705. /**
  706. * Saves a timestamp for a value - f.e. a call
  707. * Necessary for profiling Friendica
  708. *
  709. * @param int $timestamp the Timestamp
  710. * @param string $value A value to profile
  711. */
  712. public function saveTimestamp($timestamp, $value)
  713. {
  714. if (!isset($this->config['system']['profiler']) || !$this->config['system']['profiler']) {
  715. return;
  716. }
  717. $duration = (float) (microtime(true) - $timestamp);
  718. if (!isset($this->performance[$value])) {
  719. // Prevent ugly E_NOTICE
  720. $this->performance[$value] = 0;
  721. }
  722. $this->performance[$value] += (float) $duration;
  723. $this->performance['marktime'] += (float) $duration;
  724. $callstack = Core\System::callstack();
  725. if (!isset($this->callstack[$value][$callstack])) {
  726. // Prevent ugly E_NOTICE
  727. $this->callstack[$value][$callstack] = 0;
  728. }
  729. $this->callstack[$value][$callstack] += (float) $duration;
  730. }
  731. /**
  732. * Returns the current UserAgent as a String
  733. *
  734. * @return string the UserAgent as a String
  735. */
  736. public function getUserAgent()
  737. {
  738. return
  739. FRIENDICA_PLATFORM . " '" .
  740. FRIENDICA_CODENAME . "' " .
  741. FRIENDICA_VERSION . '-' .
  742. DB_UPDATE_VERSION . '; ' .
  743. $this->getBaseURL();
  744. }
  745. /**
  746. * Checks, if the call is from the Friendica App
  747. *
  748. * Reason:
  749. * The friendica client has problems with the GUID in the notify. this is some workaround
  750. */
  751. private function checkFriendicaApp()
  752. {
  753. // Friendica-Client
  754. $this->isFriendicaApp = isset($_SERVER['HTTP_USER_AGENT']) && $_SERVER['HTTP_USER_AGENT'] == 'Apache-HttpClient/UNAVAILABLE (java 1.4)';
  755. }
  756. /**
  757. * Is the call via the Friendica app? (not a "normale" call)
  758. *
  759. * @return bool true if it's from the Friendica app
  760. */
  761. public function isFriendicaApp()
  762. {
  763. return $this->isFriendicaApp;
  764. }
  765. /**
  766. * @brief Checks if the site is called via a backend process
  767. *
  768. * This isn't a perfect solution. But we need this check very early.
  769. * So we cannot wait until the modules are loaded.
  770. *
  771. * @param string $backend true, if the backend flag was set during App initialization
  772. *
  773. */
  774. private function checkBackend($backend) {
  775. static $backends = [
  776. '_well_known',
  777. 'api',
  778. 'dfrn_notify',
  779. 'fetch',
  780. 'hcard',
  781. 'hostxrd',
  782. 'nodeinfo',
  783. 'noscrape',
  784. 'p',
  785. 'poco',
  786. 'post',
  787. 'proxy',
  788. 'pubsub',
  789. 'pubsubhubbub',
  790. 'receive',
  791. 'rsd_xml',
  792. 'salmon',
  793. 'statistics_json',
  794. 'xrd',
  795. ];
  796. // Check if current module is in backend or backend flag is set
  797. $this->isBackend = (in_array($this->module, $backends) || $backend || $this->isBackend);
  798. }
  799. /**
  800. * Returns true, if the call is from a backend node (f.e. from a worker)
  801. *
  802. * @return bool Is it a known backend?
  803. */
  804. public function isBackend()
  805. {
  806. return $this->isBackend;
  807. }
  808. /**
  809. * @brief Checks if the maximum number of database processes is reached
  810. *
  811. * @return bool Is the limit reached?
  812. */
  813. public function isMaxProcessesReached()
  814. {
  815. // Deactivated, needs more investigating if this check really makes sense
  816. return false;
  817. /*
  818. * Commented out to suppress static analyzer issues
  819. *
  820. if ($this->is_backend()) {
  821. $process = 'backend';
  822. $max_processes = Core\Config::get('system', 'max_processes_backend');
  823. if (intval($max_processes) == 0) {
  824. $max_processes = 5;
  825. }
  826. } else {
  827. $process = 'frontend';
  828. $max_processes = Core\Config::get('system', 'max_processes_frontend');
  829. if (intval($max_processes) == 0) {
  830. $max_processes = 20;
  831. }
  832. }
  833. $processlist = DBA::processlist();
  834. if ($processlist['list'] != '') {
  835. Core\Logger::log('Processcheck: Processes: ' . $processlist['amount'] . ' - Processlist: ' . $processlist['list'], Core\Logger::DEBUG);
  836. if ($processlist['amount'] > $max_processes) {
  837. Core\Logger::log('Processcheck: Maximum number of processes for ' . $process . ' tasks (' . $max_processes . ') reached.', Core\Logger::DEBUG);
  838. return true;
  839. }
  840. }
  841. return false;
  842. */
  843. }
  844. /**
  845. * @brief Checks if the minimal memory is reached
  846. *
  847. * @return bool Is the memory limit reached?
  848. */
  849. public function isMinMemoryReached()
  850. {
  851. $min_memory = Core\Config::get('system', 'min_memory', 0);
  852. if ($min_memory == 0) {
  853. return false;
  854. }
  855. if (!is_readable('/proc/meminfo')) {
  856. return false;
  857. }
  858. $memdata = explode("\n", file_get_contents('/proc/meminfo'));
  859. $meminfo = [];
  860. foreach ($memdata as $line) {
  861. $data = explode(':', $line);
  862. if (count($data) != 2) {
  863. continue;
  864. }
  865. list($key, $val) = $data;
  866. $meminfo[$key] = (int) trim(str_replace('kB', '', $val));
  867. $meminfo[$key] = (int) ($meminfo[$key] / 1024);
  868. }
  869. if (!isset($meminfo['MemAvailable']) || !isset($meminfo['MemFree'])) {
  870. return false;
  871. }
  872. $free = $meminfo['MemAvailable'] + $meminfo['MemFree'];
  873. $reached = ($free < $min_memory);
  874. if ($reached) {
  875. Core\Logger::log('Minimal memory reached: ' . $free . '/' . $meminfo['MemTotal'] . ' - limit ' . $min_memory, Core\Logger::DEBUG);
  876. }
  877. return $reached;
  878. }
  879. /**
  880. * @brief Checks if the maximum load is reached
  881. *
  882. * @return bool Is the load reached?
  883. */
  884. public function isMaxLoadReached()
  885. {
  886. if ($this->isBackend()) {
  887. $process = 'backend';
  888. $maxsysload = intval(Core\Config::get('system', 'maxloadavg'));
  889. if ($maxsysload < 1) {
  890. $maxsysload = 50;
  891. }
  892. } else {
  893. $process = 'frontend';
  894. $maxsysload = intval(Core\Config::get('system', 'maxloadavg_frontend'));
  895. if ($maxsysload < 1) {
  896. $maxsysload = 50;
  897. }
  898. }
  899. $load = Core\System::currentLoad();
  900. if ($load) {
  901. if (intval($load) > $maxsysload) {
  902. Core\Logger::log('system: load ' . $load . ' for ' . $process . ' tasks (' . $maxsysload . ') too high.');
  903. return true;
  904. }
  905. }
  906. return false;
  907. }
  908. /**
  909. * Executes a child process with 'proc_open'
  910. *
  911. * @param string $command The command to execute
  912. * @param array $args Arguments to pass to the command ( [ 'key' => value, 'key2' => value2, ... ]
  913. */
  914. public function proc_run($command, $args)
  915. {
  916. if (!function_exists('proc_open')) {
  917. return;
  918. }
  919. $cmdline = $this->getConfigValue('config', 'php_path', 'php') . ' ' . escapeshellarg($command);
  920. foreach ($args as $key => $value) {
  921. if (!is_null($value) && is_bool($value) && !$value) {
  922. continue;
  923. }
  924. $cmdline .= ' --' . $key;
  925. if (!is_null($value) && !is_bool($value)) {
  926. $cmdline .= ' ' . $value;
  927. }
  928. }
  929. if ($this->isMinMemoryReached()) {
  930. return;
  931. }
  932. if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
  933. $resource = proc_open('cmd /c start /b ' . $cmdline, [], $foo, $this->getBasePath());
  934. } else {
  935. $resource = proc_open($cmdline . ' &', [], $foo, $this->getBasePath());
  936. }
  937. if (!is_resource($resource)) {
  938. Core\Logger::log('We got no resource for command ' . $cmdline, Core\Logger::DEBUG);
  939. return;
  940. }
  941. proc_close($resource);
  942. }
  943. /**
  944. * @brief Returns the system user that is executing the script
  945. *
  946. * This mostly returns something like "www-data".
  947. *
  948. * @return string system username
  949. */
  950. private static function getSystemUser()
  951. {
  952. if (!function_exists('posix_getpwuid') || !function_exists('posix_geteuid')) {
  953. return '';
  954. }
  955. $processUser = posix_getpwuid(posix_geteuid());
  956. return $processUser['name'];
  957. }
  958. /**
  959. * @brief Checks if a given directory is usable for the system
  960. *
  961. * @return boolean the directory is usable
  962. */
  963. public static function isDirectoryUsable($directory, $check_writable = true)
  964. {
  965. if ($directory == '') {
  966. Core\Logger::log('Directory is empty. This shouldn\'t happen.', Core\Logger::DEBUG);
  967. return false;
  968. }
  969. if (!file_exists($directory)) {
  970. Core\Logger::log('Path "' . $directory . '" does not exist for user ' . self::getSystemUser(), Core\Logger::DEBUG);
  971. return false;
  972. }
  973. if (is_file($directory)) {
  974. Core\Logger::log('Path "' . $directory . '" is a file for user ' . self::getSystemUser(), Core\Logger::DEBUG);
  975. return false;
  976. }
  977. if (!is_dir($directory)) {
  978. Core\Logger::log('Path "' . $directory . '" is not a directory for user ' . self::getSystemUser(), Core\Logger::DEBUG);
  979. return false;
  980. }
  981. if ($check_writable && !is_writable($directory)) {
  982. Core\Logger::log('Path "' . $directory . '" is not writable for user ' . self::getSystemUser(), Core\Logger::DEBUG);
  983. return false;
  984. }
  985. return true;
  986. }
  987. /**
  988. * @param string $cat Config category
  989. * @param string $k Config key
  990. * @param mixed $default Default value if it isn't set
  991. *
  992. * @return string Returns the value of the Config entry
  993. */
  994. public function getConfigValue($cat, $k, $default = null)
  995. {
  996. $return = $default;
  997. if ($cat === 'config') {
  998. if (isset($this->config[$k])) {
  999. $return = $this->config[$k];
  1000. }
  1001. } else {
  1002. if (isset($this->config[$cat][$k])) {
  1003. $return = $this->config[$cat][$k];
  1004. }
  1005. }
  1006. return $return;
  1007. }
  1008. /**
  1009. * Sets a default value in the config cache. Ignores already existing keys.
  1010. *
  1011. * @param string $cat Config category
  1012. * @param string $k Config key
  1013. * @param mixed $v Default value to set
  1014. */
  1015. private function setDefaultConfigValue($cat, $k, $v)
  1016. {
  1017. if (!isset($this->config[$cat][$k])) {
  1018. $this->setConfigValue($cat, $k, $v);
  1019. }
  1020. }
  1021. /**
  1022. * Sets a value in the config cache. Accepts raw output from the config table
  1023. *
  1024. * @param string $cat Config category
  1025. * @param string $k Config key
  1026. * @param mixed $v Value to set
  1027. */
  1028. public function setConfigValue($cat, $k, $v)
  1029. {
  1030. // Only arrays are serialized in database, so we have to unserialize sparingly
  1031. $value = is_string($v) && preg_match("|^a:[0-9]+:{.*}$|s", $v) ? unserialize($v) : $v;
  1032. if ($cat === 'config') {
  1033. $this->config[$k] = $value;
  1034. } else {
  1035. if (!isset($this->config[$cat])) {
  1036. $this->config[$cat] = [];
  1037. }
  1038. $this->config[$cat][$k] = $value;
  1039. }
  1040. }
  1041. /**
  1042. * Deletes a value from the config cache
  1043. *
  1044. * @param string $cat Config category
  1045. * @param string $k Config key
  1046. */
  1047. public function deleteConfigValue($cat, $k)
  1048. {
  1049. if ($cat === 'config') {
  1050. if (isset($this->config[$k])) {
  1051. unset($this->config[$k]);
  1052. }
  1053. } else {
  1054. if (isset($this->config[$cat][$k])) {
  1055. unset($this->config[$cat][$k]);
  1056. }
  1057. }
  1058. }
  1059. /**
  1060. * Retrieves a value from the user config cache
  1061. *
  1062. * @param int $uid User Id
  1063. * @param string $cat Config category
  1064. * @param string $k Config key
  1065. * @param mixed $default Default value if key isn't set
  1066. *
  1067. * @return string The value of the config entry
  1068. */
  1069. public function getPConfigValue($uid, $cat, $k, $default = null)
  1070. {
  1071. $return = $default;
  1072. if (isset($this->config[$uid][$cat][$k])) {
  1073. $return = $this->config[$uid][$cat][$k];
  1074. }
  1075. return $return;
  1076. }
  1077. /**
  1078. * Sets a value in the user config cache
  1079. *
  1080. * Accepts raw output from the pconfig table
  1081. *
  1082. * @param int $uid User Id
  1083. * @param string $cat Config category
  1084. * @param string $k Config key
  1085. * @param mixed $v Value to set
  1086. */
  1087. public function setPConfigValue($uid, $cat, $k, $v)
  1088. {
  1089. // Only arrays are serialized in database, so we have to unserialize sparingly
  1090. $value = is_string($v) && preg_match("|^a:[0-9]+:{.*}$|s", $v) ? unserialize($v) : $v;
  1091. if (!isset($this->config[$uid]) || !is_array($this->config[$uid])) {
  1092. $this->config[$uid] = [];
  1093. }
  1094. if (!isset($this->config[$uid][$cat]) || !is_array($this->config[$uid][$cat])) {
  1095. $this->config[$uid][$cat] = [];
  1096. }
  1097. $this->config[$uid][$cat][$k] = $value;
  1098. }
  1099. /**
  1100. * Deletes a value from the user config cache
  1101. *
  1102. * @param int $uid User Id
  1103. * @param string $cat Config category
  1104. * @param string $k Config key
  1105. */
  1106. public function deletePConfigValue($uid, $cat, $k)
  1107. {
  1108. if (isset($this->config[$uid][$cat][$k])) {
  1109. unset($this->config[$uid][$cat][$k]);
  1110. }
  1111. }
  1112. /**
  1113. * Generates the site's default sender email address
  1114. *
  1115. * @return string
  1116. */
  1117. public function getSenderEmailAddress()
  1118. {
  1119. $sender_email = Core\Config::get('config', 'sender_email');
  1120. if (empty($sender_email)) {
  1121. $hostname = $this->getHostName();
  1122. if (strpos($hostname, ':')) {
  1123. $hostname = substr($hostname, 0, strpos($hostname, ':'));
  1124. }
  1125. $sender_email = 'noreply@' . $hostname;
  1126. }
  1127. return $sender_email;
  1128. }
  1129. /**
  1130. * Returns the current theme name.
  1131. *
  1132. * @return string the name of the current theme
  1133. */
  1134. public function getCurrentTheme()
  1135. {
  1136. if ($this->getMode()->isInstall()) {
  1137. return '';
  1138. }
  1139. //// @TODO Compute the current theme only once (this behavior has
  1140. /// already been implemented, but it didn't work well -
  1141. /// https://github.com/friendica/friendica/issues/5092)
  1142. $this->computeCurrentTheme();
  1143. return $this->currentTheme;
  1144. }
  1145. /**
  1146. * Computes the current theme name based on the node settings, the user settings and the device type
  1147. *
  1148. * @throws Exception
  1149. */
  1150. private function computeCurrentTheme()
  1151. {
  1152. $system_theme = Core\Config::get('system', 'theme');
  1153. if (!$system_theme) {
  1154. throw new Exception(Core\L10n::t('No system theme config value set.'));
  1155. }
  1156. // Sane default
  1157. $this->currentTheme = $system_theme;
  1158. $allowed_themes = explode(',', Core\Config::get('system', 'allowed_themes', $system_theme));
  1159. $page_theme = null;
  1160. // Find the theme that belongs to the user whose stuff we are looking at
  1161. if ($this->profile_uid && ($this->profile_uid != local_user())) {
  1162. // Allow folks to override user themes and always use their own on their own site.
  1163. // This works only if the user is on the same server
  1164. $user = DBA::selectFirst('user', ['theme'], ['uid' => $this->profile_uid]);
  1165. if (DBA::isResult($user) && !Core\PConfig::get(local_user(), 'system', 'always_my_theme')) {
  1166. $page_theme = $user['theme'];
  1167. }
  1168. }
  1169. $user_theme = Core\Session::get('theme', $system_theme);
  1170. // Specific mobile theme override
  1171. if (($this->is_mobile || $this->is_tablet) && Core\Session::get('show-mobile', true)) {
  1172. $system_mobile_theme = Core\Config::get('system', 'mobile-theme');
  1173. $user_mobile_theme = Core\Session::get('mobile-theme', $system_mobile_theme);
  1174. // --- means same mobile theme as desktop
  1175. if (!empty($user_mobile_theme) && $user_mobile_theme !== '---') {
  1176. $user_theme = $user_mobile_theme;
  1177. }
  1178. }
  1179. if ($page_theme) {
  1180. $theme_name = $page_theme;
  1181. } else {
  1182. $theme_name = $user_theme;
  1183. }
  1184. if ($theme_name
  1185. && in_array($theme_name, $allowed_themes)
  1186. && (file_exists('view/theme/' . $theme_name . '/style.css')
  1187. || file_exists('view/theme/' . $theme_name . '/style.php'))
  1188. ) {
  1189. $this->currentTheme = $theme_name;
  1190. }
  1191. }
  1192. /**
  1193. * @brief Return full URL to theme which is currently in effect.
  1194. *
  1195. * Provide a sane default if nothing is chosen or the specified theme does not exist.
  1196. *
  1197. * @return string
  1198. */
  1199. public function getCurrentThemeStylesheetPath()
  1200. {
  1201. return Core\Theme::getStylesheetPath($this->getCurrentTheme());
  1202. }
  1203. /**
  1204. * Check if request was an AJAX (xmlhttprequest) request.
  1205. *
  1206. * @return boolean true if it was an AJAX request
  1207. */
  1208. public function isAjax()
  1209. {
  1210. return $this->isAjax;
  1211. }
  1212. /**
  1213. * Returns the value of a argv key
  1214. * TODO there are a lot of $a->argv usages in combination with defaults() which can be replaced with this method
  1215. *
  1216. * @param int $position the position of the argument
  1217. * @param mixed $default the default value if not found
  1218. *
  1219. * @return mixed returns the value of the argument
  1220. */
  1221. public function getArgumentValue($position, $default = '')
  1222. {
  1223. if (array_key_exists($position, $this->argv)) {
  1224. return $this->argv[$position];
  1225. }
  1226. return $default;
  1227. }
  1228. /**
  1229. * Sets the base url for use in cmdline programs which don't have
  1230. * $_SERVER variables
  1231. */
  1232. public function checkURL()
  1233. {
  1234. $url = Core\Config::get('system', 'url');
  1235. // if the url isn't set or the stored url is radically different
  1236. // than the currently visited url, store the current value accordingly.
  1237. // "Radically different" ignores common variations such as http vs https
  1238. // and www.example.com vs example.com.
  1239. // We will only change the url to an ip address if there is no existing setting
  1240. if (empty($url) || (!Util\Strings::compareLink($url, $this->getBaseURL())) && (!preg_match("/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/", $this->getHostName()))) {
  1241. Core\Config::set('system', 'url', $this->getBaseURL());
  1242. }
  1243. }
  1244. /**
  1245. * Frontend App script
  1246. *
  1247. * The App object behaves like a container and a dispatcher at the same time, including a representation of the
  1248. * request and a representation of the response.
  1249. *
  1250. * This probably should change to limit the size of this monster method.
  1251. */
  1252. public function runFrontend()
  1253. {
  1254. // Missing DB connection: ERROR
  1255. if ($this->getMode()->has(App\Mode::LOCALCONFIGPRESENT) && !$this->getMode()->has(App\Mode::DBAVAILABLE)) {
  1256. Core\System::httpExit(500, ['title' => 'Error 500 - Internal Server Error', 'description' => 'Apologies but the website is unavailable at the moment.']);
  1257. }
  1258. // Max Load Average reached: ERROR
  1259. if ($this->isMaxProcessesReached() || $this->isMaxLoadReached()) {
  1260. header('Retry-After: 120');
  1261. header('Refresh: 120; url=' . $this->getBaseURL() . "/" . $this->query_string);
  1262. Core\System::httpExit(503, ['title' => 'Error 503 - Service Temporarily Unavailable', 'description' => 'Core\System is currently overloaded. Please try again later.']);
  1263. }
  1264. if (strstr($this->query_string, '.well-known/host-meta') && ($this->query_string != '.well-known/host-meta')) {
  1265. Core\System::httpExit(404);
  1266. }
  1267. if (!$this->getMode()->isInstall()) {
  1268. // Force SSL redirection
  1269. if (Core\Config::get('system', 'force_ssl') && ($this->getScheme() == "http")
  1270. && intval(Core\Config::get('system', 'ssl_policy')) == SSL_POLICY_FULL
  1271. && strpos($this->getBaseURL(), 'https://') === 0
  1272. && $_SERVER['REQUEST_METHOD'] == 'GET') {
  1273. header('HTTP/1.1 302 Moved Temporarily');
  1274. header('Location: ' . $this->getBaseURL() . '/' . $this->query_string);
  1275. exit();
  1276. }
  1277. Core\Session::init();
  1278. Core\Addon::callHooks('init_1');
  1279. }
  1280. // Exclude the backend processes from the session management
  1281. if (!$this->isBackend()) {
  1282. $stamp1 = microtime(true);
  1283. session_start();
  1284. $this->saveTimestamp($stamp1, 'parser');
  1285. Core\L10n::setSessionVariable();
  1286. Core\L10n::setLangFromSession();
  1287. } else {
  1288. $_SESSION = [];
  1289. Core\Worker::executeIfIdle();
  1290. }
  1291. // ZRL
  1292. if (!empty($_GET['zrl']) && $this->getMode()->isNormal()) {
  1293. $this->query_string = Model\Profile::stripZrls($this->query_string);
  1294. if (!local_user()) {
  1295. // Only continue when the given profile link seems valid
  1296. // Valid profile links contain a path with "/profile/" and no query parameters
  1297. if ((parse_url($_GET['zrl'], PHP_URL_QUERY) == "") &&
  1298. strstr(parse_url($_GET['zrl'], PHP_URL_PATH), "/profile/")) {
  1299. if (defaults($_SESSION, "visitor_home", "") != $_GET["zrl"]) {
  1300. $_SESSION['my_url'] = $_GET['zrl'];
  1301. $_SESSION['authenticated'] = 0;
  1302. }
  1303. Model\Profile::zrlInit($this);
  1304. } else {
  1305. // Someone came with an invalid parameter, maybe as a DDoS attempt
  1306. // We simply stop processing here
  1307. Core\Logger::log("Invalid ZRL parameter " . $_GET['zrl'], Core\Logger::DEBUG);
  1308. Core\System::httpExit(403, ['title' => '403 Forbidden']);
  1309. }
  1310. }
  1311. }
  1312. if (!empty($_GET['owt']) && $this->getMode()->isNormal()) {
  1313. $token = $_GET['owt'];
  1314. $this->query_string = Model\Profile::stripQueryParam($this->query_string, 'owt');
  1315. Model\Profile::openWebAuthInit($token);
  1316. }
  1317. Module\Login::sessionAuth();
  1318. if (empty($_SESSION['authenticated'])) {
  1319. header('X-Account-Management-Status: none');
  1320. }
  1321. $_SESSION['sysmsg'] = defaults($_SESSION, 'sysmsg' , []);
  1322. $_SESSION['sysmsg_info'] = defaults($_SESSION, 'sysmsg_info' , []);
  1323. $_SESSION['last_updated'] = defaults($_SESSION, 'last_updated', []);
  1324. /*
  1325. * check_config() is responsible for running update scripts. These automatically
  1326. * update the DB schema whenever we push a new one out. It also checks to see if
  1327. * any addons have been added or removed and reacts accordingly.
  1328. */
  1329. // in install mode, any url loads install module
  1330. // but we need "view" module for stylesheet
  1331. if ($this->getMode()->isInstall() && $this->module != 'view') {
  1332. $this->module = 'install';
  1333. } elseif (!$this->getMode()->has(App\Mode::MAINTENANCEDISABLED) && $this->module != 'view') {
  1334. $this->module = 'maintenance';
  1335. } else {
  1336. $this->checkURL();
  1337. Core\Update::check(false);
  1338. Core\Addon::loadAddons();
  1339. Core\Hook::loadHooks();
  1340. }
  1341. $this->page = [
  1342. 'aside' => '',
  1343. 'bottom' => '',
  1344. 'content' => '',
  1345. 'footer' => '',
  1346. 'htmlhead' => '',
  1347. 'nav' => '',
  1348. 'page_title' => '',
  1349. 'right_aside' => '',
  1350. 'template' => '',
  1351. 'title' => ''
  1352. ];
  1353. if (strlen($this->module)) {
  1354. // Compatibility with the Android Diaspora client
  1355. if ($this->module == 'stream') {
  1356. $this->internalRedirect('network?f=&order=post');
  1357. }
  1358. if ($this->module == 'conversations') {
  1359. $this->internalRedirect('message');
  1360. }
  1361. if ($this->module == 'commented') {
  1362. $this->internalRedirect('network?f=&order=comment');
  1363. }
  1364. if ($this->module == 'liked') {
  1365. $this->internalRedirect('network?f=&order=comment');
  1366. }
  1367. if ($this->module == 'activity') {
  1368. $this->internalRedirect('network/?f=&conv=1');
  1369. }
  1370. if (($this->module == 'status_messages') && ($this->cmd == 'status_messages/new')) {
  1371. $this->internalRedirect('bookmarklet');
  1372. }
  1373. if (($this->module == 'user') && ($this->cmd == 'user/edit')) {
  1374. $this->internalRedirect('settings');
  1375. }
  1376. if (($this->module == 'tag_followings') && ($this->cmd == 'tag_followings/manage')) {
  1377. $this->internalRedirect('search');
  1378. }
  1379. // Compatibility with the Firefox App
  1380. if (($this->module == "users") && ($this->cmd == "users/sign_in")) {
  1381. $this->module = "login";
  1382. }
  1383. $privateapps = Core\Config::get('config', 'private_addons', false);
  1384. if (Core\Addon::isEnabled($this->module) && file_exists("addon/{$this->module}/{$this->module}.php")) {
  1385. //Check if module is an app and if public access to apps is allowed or not
  1386. if ((!local_user()) && Core\Hook::isAddonApp($this->module) && $privateapps) {
  1387. info(Core\L10n::t("You must be logged in to use addons. "));
  1388. } else {
  1389. include_once "addon/{$this->module}/{$this->module}.php";
  1390. if (function_exists($this->module . '_module')) {
  1391. LegacyModule::setModuleFile("addon/{$this->module}/{$this->module}.php");
  1392. $this->module_class = 'Friendica\\LegacyModule';
  1393. $this->module_loaded = true;
  1394. }
  1395. }
  1396. }
  1397. // Controller class routing
  1398. if (! $this->module_loaded && class_exists('Friendica\\Module\\' . ucfirst($this->module))) {
  1399. $this->module_class = 'Friendica\\Module\\' . ucfirst($this->module);
  1400. $this->module_loaded = true;
  1401. }
  1402. /* If not, next look for a 'standard' program module in the 'mod' directory
  1403. * We emulate a Module class through the LegacyModule class
  1404. */
  1405. if (! $this->module_loaded && file_exists("mod/{$this->module}.php")) {
  1406. LegacyModule::setModuleFile("mod/{$this->module}.php");
  1407. $this->module_class = 'Friendica\\LegacyModule';
  1408. $this->module_loaded = true;
  1409. }
  1410. /* The URL provided does not resolve to a valid module.
  1411. *
  1412. * On Dreamhost sites, quite often things go wrong for no apparent reason and they send us to '/internal_error.html'.
  1413. * We don't like doing this, but as it occasionally accounts for 10-20% or more of all site traffic -
  1414. * we are going to trap this and redirect back to the requested page. As long as you don't have a critical error on your page
  1415. * this will often succeed and eventually do the right thing.
  1416. *
  1417. * Otherwise we are going to emit a 404 not found.
  1418. */
  1419. if (! $this->module_loaded) {
  1420. // Stupid browser tried to pre-fetch our Javascript img template. Don't log the event or return anything - just quietly exit.
  1421. if (!empty($_SERVER['QUERY_STRING']) && preg_match('/{[0-9]}/', $_SERVER['QUERY_STRING']) !== 0) {
  1422. exit();
  1423. }
  1424. if (!empty($_SERVER['QUERY_STRING']) && ($_SERVER['QUERY_STRING'] === 'q=internal_error.html') && isset($dreamhost_error_hack)) {
  1425. Core\Logger::log('index.php: dreamhost_error_hack invoked. Original URI =' . $_SERVER['REQUEST_URI']);
  1426. $this->internalRedirect($_SERVER['REQUEST_URI']);
  1427. }
  1428. Core\Logger::log('index.php: page not found: ' . $_SERVER['REQUEST_URI'] . ' ADDRESS: ' . $_SERVER['REMOTE_ADDR'] . ' QUERY: ' . $_SERVER['QUERY_STRING'], Core\Logger::DEBUG);
  1429. header($_SERVER["SERVER_PROTOCOL"] . ' 404 ' . Core\L10n::t('Not Found'));
  1430. $tpl = Core\Renderer::getMarkupTemplate("404.tpl");
  1431. $this->page['content'] = Core\Renderer::replaceMacros($tpl, [
  1432. '$message' => Core\L10n::t('Page not found.')
  1433. ]);
  1434. }
  1435. }
  1436. // Load current theme info
  1437. $theme_info_file = 'view/theme/' . $this->getCurrentTheme() . '/theme.php';
  1438. if (file_exists($theme_info_file)) {
  1439. require_once $theme_info_file;
  1440. }
  1441. // initialise content region
  1442. if ($this->getMode()->isNormal()) {
  1443. Core\Addon::callHooks('page_content_top', $this->page['content']);
  1444. }
  1445. // Call module functions
  1446. if ($this->module_loaded) {
  1447. $this->page['page_title'] = $this->module;
  1448. $placeholder = '';
  1449. Core\Addon::callHooks($this->module . '_mod_init', $placeholder);
  1450. call_user_func([$this->module_class, 'init']);
  1451. // "rawContent" is especially meant for technical endpoints.
  1452. // This endpoint doesn't need any theme initialization or other comparable stuff.
  1453. if (!$this->error) {
  1454. call_user_func([$this->module_class, 'rawContent']);
  1455. }
  1456. if (function_exists(str_replace('-', '_', $this->getCurrentTheme()) . '_init')) {
  1457. $func = str_replace('-', '_', $this->getCurrentTheme()) . '_init';
  1458. $func($this);
  1459. }
  1460. if (! $this->error && $_SERVER['REQUEST_METHOD'] === 'POST') {
  1461. Core\Addon::callHooks($this->module . '_mod_post', $_POST);
  1462. call_user_func([$this->module_class, 'post']);
  1463. }
  1464. if (! $this->error) {
  1465. Core\Addon::callHooks($this->module . '_mod_afterpost', $placeholder);
  1466. call_user_func([$this->module_class, 'afterpost']);
  1467. }
  1468. if (! $this->error) {
  1469. $arr = ['content' => $this->page['content']];
  1470. Core\Addon::callHooks($this->module . '_mod_content', $arr);
  1471. $this->page['content'] = $arr['content'];
  1472. $arr = ['content' => call_user_func([$this->module_class, 'content'])];
  1473. Core\Addon::callHooks($this->module . '_mod_aftercontent', $arr);
  1474. $this->page['content'] .= $arr['content'];
  1475. }
  1476. if (function_exists(str_replace('-', '_', $this->getCurrentTheme()) . '_content_loaded')) {
  1477. $func = str_replace('-', '_', $this->getCurrentTheme()) . '_content_loaded';
  1478. $func($this);
  1479. }
  1480. }
  1481. /* Create the page head after setting the language
  1482. * and getting any auth credentials.
  1483. *
  1484. * Moved initHead() and initFooter() to after
  1485. * all the module functions have executed so that all
  1486. * theme choices made by the modules can take effect.
  1487. */
  1488. $this->initHead();
  1489. /* Build the page ending -- this is stuff that goes right before
  1490. * the closing </body> tag
  1491. */
  1492. $this->initFooter();
  1493. /* now that we've been through the module content, see if the page reported
  1494. * a permission problem and if so, a 403 response would seem to be in order.
  1495. */
  1496. if (stristr(implode("", $_SESSION['sysmsg']), Core\L10n::t('Permission denied'))) {
  1497. header($_SERVER["SERVER_PROTOCOL"] . ' 403 ' . Core\L10n::t('Permission denied.'));
  1498. }
  1499. // Report anything which needs to be communicated in the notification area (before the main body)
  1500. Core\Addon::callHooks('page_end', $this->page['content']);
  1501. // Add the navigation (menu) template
  1502. if ($this->module != 'install' && $this->module != 'maintenance') {
  1503. $this->page['htmlhead'] .= Core\Renderer::replaceMacros(Core\Renderer::getMarkupTemplate('nav_head.tpl'), []);
  1504. $this->page['nav'] = Content\Nav::build($this);
  1505. }
  1506. // Build the page - now that we have all the components
  1507. if (isset($_GET["mode"]) && (($_GET["mode"] == "raw") || ($_GET["mode"] == "minimal"))) {
  1508. $doc = new DOMDocument();
  1509. $target = new DOMDocument();
  1510. $target->loadXML("<root></root>");
  1511. $content = mb_convert_encoding($this->page["content"], 'HTML-ENTITIES', "UTF-8");
  1512. /// @TODO one day, kill those error-surpressing @ stuff, or PHP should ban it
  1513. @$doc->loadHTML($content);
  1514. $xpath = new DOMXPath($doc);
  1515. $list = $xpath->query("//*[contains(@id,'tread-wrapper-')]"); /* */
  1516. foreach ($list as $item) {
  1517. $item = $target->importNode($item, true);
  1518. // And then append it to the target
  1519. $target->documentElement->appendChild($item);
  1520. }
  1521. }
  1522. if (isset($_GET["mode"]) && ($_GET["mode"] == "raw")) {
  1523. header("Content-type: text/html; charset=utf-8");
  1524. echo substr($target->saveHTML(), 6, -8);
  1525. exit();
  1526. }
  1527. $page = $this->page;
  1528. $profile = $this->profile;
  1529. header("X-Friendica-Version: " . FRIENDICA_VERSION);
  1530. header("Content-type: text/html; charset=utf-8");
  1531. if (Core\Config::get('system', 'hsts') && (Core\Config::get('system', 'ssl_policy') == SSL_POLICY_FULL)) {
  1532. header("Strict-Transport-Security: max-age=31536000");
  1533. }
  1534. // Some security stuff
  1535. header('X-Content-Type-Options: nosniff');
  1536. header('X-XSS-Protection: 1; mode=block');
  1537. header('X-Permitted-Cross-Domain-Policies: none');
  1538. header('X-Frame-Options: sameorigin');
  1539. // Things like embedded OSM maps don't work, when this is enabled
  1540. // 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'");
  1541. /* We use $_GET["mode"] for special page templates. So we will check if we have
  1542. * to load another page template than the default one.
  1543. * The page templates are located in /view/php/ or in the theme directory.
  1544. */
  1545. if (isset($_GET["mode"])) {
  1546. $template = Core\Theme::getPathForFile($_GET["mode"] . '.php');
  1547. }
  1548. // If there is no page template use the default page template
  1549. if (empty($template)) {
  1550. $template = Core\Theme::getPathForFile("default.php");
  1551. }
  1552. // Theme templates expect $a as an App instance
  1553. $a = $this;
  1554. // Used as is in view/php/default.php
  1555. $lang = Core\L10n::getCurrentLang();
  1556. /// @TODO Looks unsafe (remote-inclusion), is maybe not but Core\Theme::getPathForFile() uses file_exists() but does not escape anything
  1557. require_once $template;
  1558. }
  1559. /**
  1560. * Redirects to another module relative to the current Friendica base.
  1561. * If you want to redirect to a external URL, use System::externalRedirectTo()
  1562. *
  1563. * @param string $toUrl The destination URL (Default is empty, which is the default page of the Friendica node)
  1564. * @param bool $ssl if true, base URL will try to get called with https:// (works just for relative paths)
  1565. *
  1566. * @throws InternalServerErrorException In Case the given URL is not relative to the Friendica node
  1567. */
  1568. public function internalRedirect($toUrl = '', $ssl = false)
  1569. {
  1570. if (filter_var($toUrl, FILTER_VALIDATE_URL)) {
  1571. throw new InternalServerErrorException("'$toUrl is not a relative path, please use System::externalRedirectTo");
  1572. }
  1573. $redirectTo = $this->getBaseURL($ssl) . '/' . ltrim($toUrl, '/');
  1574. Core\System::externalRedirect($redirectTo);
  1575. }
  1576. /**
  1577. * Automatically redirects to relative or absolute URL
  1578. * Should only be used if it isn't clear if the URL is either internal or external
  1579. *
  1580. * @param string $toUrl The target URL
  1581. *
  1582. */
  1583. public function redirect($toUrl)
  1584. {
  1585. if (filter_var($toUrl, FILTER_VALIDATE_URL)) {
  1586. Core\System::externalRedirect($toUrl);
  1587. } else {
  1588. $this->internalRedirect($toUrl);
  1589. }
  1590. }
  1591. }