[ "pgsql", Config::T_STRING ], Config::DB_HOST => [ "db", Config::T_STRING ], Config::DB_USER => [ "", Config::T_STRING ], Config::DB_NAME => [ "", Config::T_STRING ], Config::DB_PASS => [ "", Config::T_STRING ], Config::DB_PORT => [ "5432", Config::T_STRING ], Config::MYSQL_CHARSET => [ "UTF8", Config::T_STRING ], Config::SELF_URL_PATH => [ "https://example.com/tt-rss", Config::T_STRING ], Config::SINGLE_USER_MODE => [ "", Config::T_BOOL ], Config::SIMPLE_UPDATE_MODE => [ "", Config::T_BOOL ], Config::PHP_EXECUTABLE => [ "/usr/bin/php", Config::T_STRING ], Config::LOCK_DIRECTORY => [ "lock", Config::T_STRING ], Config::CACHE_DIR => [ "cache", Config::T_STRING ], Config::ICONS_DIR => [ "feed-icons", Config::T_STRING ], Config::ICONS_URL => [ "feed-icons", Config::T_STRING ], Config::AUTH_AUTO_CREATE => [ "true", Config::T_BOOL ], Config::AUTH_AUTO_LOGIN => [ "true", Config::T_BOOL ], Config::FORCE_ARTICLE_PURGE => [ 0, Config::T_INT ], Config::SESSION_COOKIE_LIFETIME => [ 86400, Config::T_INT ], Config::SMTP_FROM_NAME => [ "Tiny Tiny RSS", Config::T_STRING ], Config::SMTP_FROM_ADDRESS => [ "noreply@localhost", Config::T_STRING ], Config::DIGEST_SUBJECT => [ "[tt-rss] New headlines for last 24 hours", Config::T_STRING ], Config::CHECK_FOR_UPDATES => [ "true", Config::T_BOOL ], Config::PLUGINS => [ "auth_internal", Config::T_STRING ], Config::LOG_DESTINATION => [ Logger::LOG_DEST_SQL, Config::T_STRING ], Config::LOCAL_OVERRIDE_STYLESHEET => [ "local-overrides.css", Config::T_STRING ], Config::LOCAL_OVERRIDE_JS => [ "local-overrides.js", Config::T_STRING ], Config::DAEMON_MAX_CHILD_RUNTIME => [ 1800, Config::T_INT ], Config::DAEMON_MAX_JOBS => [ 2, Config::T_INT ], Config::FEED_FETCH_TIMEOUT => [ 45, Config::T_INT ], Config::FEED_FETCH_NO_CACHE_TIMEOUT => [ 15, Config::T_INT ], Config::FILE_FETCH_TIMEOUT => [ 45, Config::T_INT ], Config::FILE_FETCH_CONNECT_TIMEOUT => [ 15, Config::T_INT ], Config::DAEMON_UPDATE_LOGIN_LIMIT => [ 30, Config::T_INT ], Config::DAEMON_FEED_LIMIT => [ 500, Config::T_INT ], Config::DAEMON_SLEEP_INTERVAL => [ 120, Config::T_INT ], Config::MAX_CACHE_FILE_SIZE => [ 64*1024*1024, Config::T_INT ], Config::MAX_DOWNLOAD_FILE_SIZE => [ 16*1024*1024, Config::T_INT ], Config::MAX_FAVICON_FILE_SIZE => [ 1*1024*1024, Config::T_INT ], Config::CACHE_MAX_DAYS => [ 7, Config::T_INT ], Config::MAX_CONDITIONAL_INTERVAL => [ 3600*12, Config::T_INT ], Config::DAEMON_UNSUCCESSFUL_DAYS_LIMIT => [ 30, Config::T_INT ], Config::LOG_SENT_MAIL => [ "", Config::T_BOOL ], Config::HTTP_PROXY => [ "", Config::T_STRING ], Config::FORBID_PASSWORD_CHANGES => [ "", Config::T_BOOL ], Config::SESSION_NAME => [ "ttrss_sid", Config::T_STRING ], Config::CHECK_FOR_PLUGIN_UPDATES => [ "true", Config::T_BOOL ], Config::ENABLE_PLUGIN_INSTALLER => [ "true", Config::T_BOOL ], Config::AUTH_MIN_INTERVAL => [ 5, Config::T_INT ], Config::HTTP_USER_AGENT => [ 'Tiny Tiny RSS/%s (https://tt-rss.org/)', Config::T_STRING ], Config::HTTP_429_THROTTLE_INTERVAL => [ 3600, Config::T_INT ], Config::OPENTELEMETRY_ENDPOINT => [ "", Config::T_STRING ], Config::OPENTELEMETRY_SERVICE => [ "tt-rss", Config::T_STRING ], ]; /** @var Config|null */ private static $instance; /** @var array> */ private $params = []; /** @var array */ private $version = []; /** @var Db_Migrations|null $migrations */ private $migrations; private static $self_url_path_strip_dirs = 0; public static function get_instance() : Config { if (self::$instance == null) self::$instance = new self(); return self::$instance; } private function __clone() { // } function __construct() { $ref = new ReflectionClass(get_class($this)); foreach ($ref->getConstants() as $const => $cvalue) { if (isset(self::_DEFAULTS[$const])) { $override = getenv(self::_ENVVAR_PREFIX . $const); list ($defval, $deftype) = self::_DEFAULTS[$const]; $this->params[$cvalue] = [ self::cast_to($override !== false ? $override : $defval, $deftype), $deftype ]; } } } /** determine tt-rss version (using git) * * package maintainers who don't use git: if version_static.txt exists in tt-rss root * directory, its contents are displayed instead of git commit-based version, this could be generated * based on source git tree commit used when creating the package * @return array|string */ static function get_version(bool $as_string = true) { return self::get_instance()->_get_version($as_string); } // returns version showing (if possible) full timestamp of commit id static function get_version_html() : string { $version = self::get_version(false); return sprintf("%s", date("Y-m-d H:i:s", ($version['timestamp'] ?? 0)), $version['commit'] ?? '', $version['branch'] ?? '', $version['version']); } /** * @return array|string */ private function _get_version(bool $as_string = true) { $root_dir = dirname(__DIR__); if (empty($this->version)) { $this->version["status"] = -1; if (getenv("CI_COMMIT_SHORT_SHA") && getenv("CI_COMMIT_TIMESTAMP")) { $this->version["branch"] = getenv("CI_COMMIT_BRANCH"); $this->version["timestamp"] = strtotime(getenv("CI_COMMIT_TIMESTAMP")); $this->version["version"] = sprintf("%s-%s", date("y.m", $this->version["timestamp"]), getenv("CI_COMMIT_SHORT_SHA")); $this->version["commit"] = getenv("CI_COMMIT_SHORT_SHA"); $this->version["status"] = 0; } else if (PHP_OS === "Darwin") { $this->version["version"] = "UNKNOWN (Unsupported, Darwin)"; } else if (file_exists("$root_dir/version_static.txt")) { $this->version["version"] = trim(file_get_contents("$root_dir/version_static.txt")) . " (Unsupported)"; } else if (ini_get("open_basedir")) { $this->version["version"] = "UNKNOWN (Unsupported, open_basedir)"; } else if (is_dir("$root_dir/.git")) { $this->version = self::get_version_from_git($root_dir); if ($this->version["status"] != 0) { user_error("Unable to determine version: " . $this->version["version"], E_USER_WARNING); $this->version["version"] = "UNKNOWN (Unsupported, Git error)"; } else if (!getenv("SCRIPT_ROOT") || !file_exists("/.dockerenv")) { $this->version["version"] .= " (Unsupported)"; } } else { $this->version["version"] = "UNKNOWN (Unsupported)"; } } return $as_string ? $this->version["version"] : $this->version; } /** * @return array */ static function get_version_from_git(string $dir): array { $descriptorspec = [ 1 => ["pipe", "w"], // STDOUT 2 => ["pipe", "w"], // STDERR ]; $rv = [ "status" => -1, "version" => "", "branch" => "", "commit" => "", "timestamp" => 0, ]; $proc = proc_open("git --no-pager log --pretty=\"version-%ct-%h\" -n1 HEAD", $descriptorspec, $pipes, $dir); if (is_resource($proc)) { $stdout = trim(stream_get_contents($pipes[1])); $stderr = trim(stream_get_contents($pipes[2])); $status = proc_close($proc); $rv["status"] = $status; list($check, $timestamp, $commit) = explode("-", $stdout); if ($check == "version") { $rv["version"] = sprintf("%s-%s", date("y.m", (int)$timestamp), $commit); $rv["commit"] = $commit; $rv["timestamp"] = $timestamp; // proc_close() may return -1 even if command completed successfully // so if it looks like we got valid data, we ignore it if ($rv["status"] == -1) $rv["status"] = 0; } else { $rv["version"] = T_sprintf("Git error [RC=%d]: %s", $status, $stderr); } } return $rv; } static function get_migrations() : Db_Migrations { return self::get_instance()->_get_migrations(); } private function _get_migrations() : Db_Migrations { if (empty($this->migrations)) { $this->migrations = new Db_Migrations(); $this->migrations->initialize(dirname(__DIR__) . "/sql", "ttrss_version", true, self::SCHEMA_VERSION); } return $this->migrations; } static function is_migration_needed() : bool { return self::get_migrations()->is_migration_needed(); } static function get_schema_version() : int { return self::get_migrations()->get_version(); } /** * @return bool|int|string */ static function cast_to(string $value, int $type_hint) { switch ($type_hint) { case self::T_BOOL: return sql_bool_to_bool($value); case self::T_INT: return (int) $value; default: return $value; } } /** * @return bool|int|string */ private function _get(string $param) { list ($value, $type_hint) = $this->params[$param]; return $this->cast_to($value, $type_hint); } private function _add(string $param, string $default, int $type_hint): void { $override = getenv(self::_ENVVAR_PREFIX . $param); $this->params[$param] = [ self::cast_to($override !== false ? $override : $default, $type_hint), $type_hint ]; } static function add(string $param, string $default, int $type_hint = Config::T_STRING): void { $instance = self::get_instance(); $instance->_add($param, $default, $type_hint); } /** * @return bool|int|string */ static function get(string $param) { $instance = self::get_instance(); return $instance->_get($param); } static function is_server_https() : bool { return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) || (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https'); } static function strip_self_url_path_dirs($amount) { self::$self_url_path_strip_dirs = $amount; } /** returns fully-qualified external URL to tt-rss (no trailing slash) * SELF_URL_PATH configuration variable is used as a fallback for the CLI SAPI * */ static function get_self_url() : string { if (php_sapi_name() == "cli") { return self::get(Config::SELF_URL_PATH); } else { $proto = self::is_server_https() ? 'https' : 'http'; $self_url_path = $proto . '://' . $_SERVER["HTTP_HOST"] . parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH); $self_url_path = preg_replace("/\w+\.php(\?.*$)?$/", "", $self_url_path); for ($i = 0; $i < self::$self_url_path_strip_dirs; $i++) $self_url_path = dirname($self_url_path); if (substr($self_url_path, -1) === "/") { return substr($self_url_path, 0, -1); } else { return $self_url_path; } } } /* sanity check stuff */ /** checks for mysql tables not using InnoDB (tt-rss is incompatible with MyISAM) * @return array> A list of entries identifying tt-rss tables with bad config */ private static function check_mysql_tables() { $pdo = Db::pdo(); $sth = $pdo->prepare("SELECT engine, table_name FROM information_schema.tables WHERE table_schema = ? AND table_name LIKE 'ttrss_%' AND engine != 'InnoDB'"); $sth->execute([self::get(Config::DB_NAME)]); $bad_tables = []; while ($line = $sth->fetch()) { array_push($bad_tables, $line); } return $bad_tables; } static function sanity_check(): void { /* we don't actually need the DB object right now but some checks below might use ORM which won't be initialized because it is set up in the Db constructor, which is why it's a good idea to invoke it as early as possible it is a bit of a hack, maybe ORM should be initialized somewhere else (functions.php?) */ $pdo = Db::pdo(); $errors = []; if (strpos(self::get(Config::PLUGINS), "auth_") === false) { array_push($errors, "Please enable at least one authentication module via PLUGINS"); } /* we assume our dependencies are sane under docker, so some sanity checks are skipped. this also allows tt-rss process to run under root if requested (I'm using this for development under podman because of uidmapping issues with rootless containers, don't use in production -fox) */ if (!getenv("container")) { if (function_exists('posix_getuid') && posix_getuid() == 0) { array_push($errors, "Please don't run this script as root."); } if (version_compare(PHP_VERSION, '7.4.0', '<')) { array_push($errors, "PHP version 7.4.0 or newer required. You're using " . PHP_VERSION . "."); } if (!class_exists("UConverter")) { array_push($errors, "PHP UConverter class is missing, it's provided by the Internationalization (intl) module."); } if (!function_exists("curl_init") && !ini_get("allow_url_fopen")) { array_push($errors, "PHP configuration option allow_url_fopen is disabled, and CURL functions are not present. Either enable allow_url_fopen or install PHP extension for CURL."); } if (!function_exists("json_encode")) { array_push($errors, "PHP support for JSON is required, but was not found."); } if (!function_exists("flock")) { array_push($errors, "PHP support for flock() function is required."); } if (!class_exists("PDO")) { array_push($errors, "PHP support for PDO is required but was not found."); } if (!function_exists("mb_strlen")) { array_push($errors, "PHP support for mbstring functions is required but was not found."); } if (!function_exists("hash")) { array_push($errors, "PHP support for hash() function is required but was not found."); } if (ini_get("safe_mode")) { array_push($errors, "PHP safe mode setting is obsolete and not supported by tt-rss."); } if (!function_exists("mime_content_type")) { array_push($errors, "PHP function mime_content_type() is missing, try enabling fileinfo module."); } if (!class_exists("DOMDocument")) { array_push($errors, "PHP support for DOMDocument is required, but was not found."); } } if (!is_writable(self::get(Config::CACHE_DIR) . "/images")) { array_push($errors, "Image cache is not writable (chmod -R 777 ".self::get(Config::CACHE_DIR)."/images)"); } if (!is_writable(self::get(Config::CACHE_DIR) . "/upload")) { array_push($errors, "Upload cache is not writable (chmod -R 777 ".self::get(Config::CACHE_DIR)."/upload)"); } if (!is_writable(self::get(Config::CACHE_DIR) . "/export")) { array_push($errors, "Data export cache is not writable (chmod -R 777 ".self::get(Config::CACHE_DIR)."/export)"); } if (!is_writable(self::get(Config::ICONS_DIR))) { array_push($errors, "ICONS_DIR defined in config.php is not writable (chmod -R 777 ".self::get(Config::ICONS_DIR).").\n"); } if (!is_writable(self::get(Config::LOCK_DIRECTORY))) { array_push($errors, "LOCK_DIRECTORY is not writable (chmod -R 777 ".self::get(Config::LOCK_DIRECTORY).").\n"); } // ttrss_users won't be there on initial startup (before migrations are done) if (!Config::is_migration_needed() && self::get(Config::SINGLE_USER_MODE)) { if (UserHelper::get_login_by_id(1) != "admin") { array_push($errors, "SINGLE_USER_MODE is enabled but default admin account (ID: 1) is not found."); } } // skip check for CLI scripts so that we could install database schema if it is missing. if (php_sapi_name() != "cli") { if (self::get_schema_version() < 0) { array_push($errors, "Base database schema is missing. Either load it manually or perform a migration (update.php --update-schema)"); } } if (self::get(Config::DB_TYPE) == "mysql") { $bad_tables = self::check_mysql_tables(); if (count($bad_tables) > 0) { $bad_tables_fmt = []; foreach ($bad_tables as $bt) { array_push($bad_tables_fmt, sprintf("%s (%s)", $bt['table_name'], $bt['engine'])); } $msg = "

The following tables use an unsupported MySQL engine: " . implode(", ", $bad_tables_fmt) . ".

"; $msg .= "

The only supported engine on MySQL is InnoDB. MyISAM lacks functionality to run tt-rss. Please backup your data (via OPML) and re-import the schema before continuing.

WARNING: importing the schema would mean LOSS OF ALL YOUR DATA.

"; array_push($errors, $msg); } } if (count($errors) > 0 && php_sapi_name() != "cli") { ?> Startup failed

Startup failed

Please fix errors indicated by the following messages:

You might want to check tt-rss wiki or the forums for more information. Please search the forums before creating new topic for your question.

0) { echo "Please fix errors indicated by the following messages:\n\n"; foreach ($errors as $error) { echo " * " . strip_tags($error)."\n"; } echo "\nYou might want to check tt-rss wiki or the forums for more information.\n"; echo "Please search the forums before creating new topic for your question.\n"; exit(1); } } private static function format_error(string $msg): string { return "
$msg
"; } static function get_override_links(): string { $rv = ""; $local_css = get_theme_path(self::get(self::LOCAL_OVERRIDE_STYLESHEET)); if ($local_css) $rv .= stylesheet_tag($local_css); $local_js = get_theme_path(self::get(self::LOCAL_OVERRIDE_JS)); if ($local_js) $rv .= javascript_tag($local_js); return $rv; } static function get_user_agent(): string { return sprintf(self::get(self::HTTP_USER_AGENT), self::get_version()); } }