Friendica Communications Platform (please note that this is a clone of the repository at github, issues are handled there) https://friendi.ca
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.

1944 wiersze
56KB

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