use "," as delimiter in config files/data */ const ARRAY_KEYS = ['CCP_ESI_SCOPES', 'CCP_ESI_SCOPES_ADMIN']; /** * custom HTTP status codes */ const HTTP_422='Unprocessable Entity'; /** * all environment data * @var array */ private $serverConfigData = []; /** * Config constructor. * @param \Base $f3 */ public function __construct(\Base $f3){ // set server data // -> CGI params (Nginx) // -> .htaccess (Apache) $this->setServerData(); // set environment data $this->setAllEnvironmentData($f3); // set hive configuration variables // -> overwrites default configuration $this->setHiveVariables($f3); // set global getter for \DateTimeZone $f3->set('getTimeZone', function() use ($f3) : \DateTimeZone { return new \DateTimeZone( $f3->get('TZ') ); }); // set global getter for new \DateTime $f3->set('getDateTime', function(string $time = 'now', ?\DateTimeZone $timeZone = null) use ($f3) : \DateTime { $timeZone = $timeZone ? : $f3->get('getTimeZone')(); return new \DateTime($time, $timeZone); }); // database connection pool ----------------------------------------------------------------------------------- $f3->set(Pool::POOL_NAME, Pool::instance( function(string $alias) use ($f3) : array { // get DB config by alias for new connections return self::getDatabaseConfig($f3, $alias); }, function(string $schema) use ($f3) : array { // get DB requirement vars from requirements.ini return self::getRequiredDbVars($f3, $schema); } )); // lazy init Web Api clients ---------------------------------------------------------------------------------- $f3->set(SsoClient::CLIENT_NAME, SsoClient::instance()); $f3->set(CcpClient::CLIENT_NAME, CcpClient::instance()); $f3->set(GitHubClient::CLIENT_NAME, GitHubClient::instance()); $f3->set(EveScoutClient::CLIENT_NAME, EveScoutClient::instance()); // Socket connectors ------------------------------------------------------------------------------------------ $f3->set(TcpSocket::SOCKET_NAME, function(array $options = ['timeout' => 1]) : SocketInterface { return AbstractSocket::factory(TcpSocket::class, self::getSocketUri(), $options); }); } /** * get environment configuration data * @param \Base $f3 * @return array|null */ protected function getAllEnvironmentData(\Base $f3){ if(!$f3->exists(self::HIVE_KEY_ENVIRONMENT, $environmentData)){ $environmentData = $this->setAllEnvironmentData($f3); } return $environmentData; } /** * set/overwrite some global framework variables original set in config.ini * -> can be overwritten in environments.ini OR ENV-Vars * -> see: https://github.com/exodus4d/pathfinder/issues/175 * that depend on environment settings * @param \Base $f3 */ protected function setHiveVariables(\Base $f3){ // hive keys that can be overwritten $hiveKeys = ['BASE', 'URL', 'DEBUG', 'CACHE']; foreach($hiveKeys as $key){ if( !is_null( $var = self::getEnvironmentData($key)) ){ $f3->set($key,$var); } } } /** * set all environment configuration data * @param \Base $f3 * @return array|mixed|null */ protected function setAllEnvironmentData(\Base $f3){ $environmentData = null; if( !empty($this->serverConfigData['ENV']) ){ // get environment config from $_SERVER data $environmentData = (array)$this->serverConfigData['ENV']; // some environment variables should be parsed as array array_walk($environmentData, function(&$item, $key){ $item = (in_array($key, self::ARRAY_KEYS)) ? explode(',', $item) : $item; }); $environmentData['ENVIRONMENT_CONFIG'] = 'PHP: environment variables'; }else{ // get environment data from *.ini file config $customConfDir = $f3->get('CONF'); // check "custom" ini dir, of not found check default ini dir foreach($customConfDir as $type => $path){ $envConfFile = $path . 'environment.ini'; $f3->config($envConfFile, true); if( $f3->exists(self::HIVE_KEY_ENVIRONMENT) && ($environment = $f3->get(self::HIVE_KEY_ENVIRONMENT . '.SERVER')) && ($environmentData = $f3->get(self::HIVE_KEY_ENVIRONMENT . '.' . $environment)) ){ $environmentData['ENVIRONMENT_CONFIG'] = 'Config: ' . $envConfFile; break; } } } if( !is_null($environmentData) ){ ksort($environmentData); $f3->set(self::HIVE_KEY_ENVIRONMENT, $environmentData); } return $environmentData; } /** * get/extract all server data passed to PHP * this can be done by either: * OS Environment variables: * -> add to /etc/environment * OR: * Nginx (server config): * -> FastCGI syntax * fastcgi_param PF-ENV-DEBUG 3; */ protected function setServerData(){ $data = []; foreach($_SERVER as $key => $value){ if(strpos($key, self::PREFIX_KEY . self::ARRAY_DELIMITER) === 0){ $path = explode( self::ARRAY_DELIMITER, $key); // remove prefix array_shift($path); $tmp = &$data; foreach($path as $segment){ $tmp[$segment] = (array)$tmp[$segment]; $tmp = &$tmp[$segment]; } // type cast values // (e.g. '1.2' => (float); '4' => (int),...) $tmp = is_numeric($value) ? $value + 0 : $value; } } $this->serverConfigData = $data; } /** * get a environment variable by hive key * @param $key * @return string|null */ static function getEnvironmentData($key){ $hiveKey = self::HIVE_KEY_ENVIRONMENT . '.' . $key; \Base::instance()->exists($hiveKey, $data); return $data; } /** * get database config values * @param \Base $f3 * @param string $alias * @return array */ static function getDatabaseConfig(\Base $f3, string $alias) : array { $alias = strtoupper($alias); $config = [ 'ALIAS' => $alias, 'SCHEME' => 'mysql', 'HOST' => 'localhost', 'PORT' => 3306, 'SOCKET' => null, 'NAME' => self::getEnvironmentData('DB_' . $alias . '_NAME'), 'USER' => self::getEnvironmentData('DB_' . $alias . '_USER'), 'PASS' => self::getEnvironmentData('DB_' . $alias . '_PASS') ]; $pdoReg = '/^(?[[:alpha:]]+):((host=(?[a-zA-Z0-9-_\.]*))|(unix_socket=(?[a-zA-Z0-9\/]*\.sock)))((;dbname=(?\w*))|(;port=(?\d*))){0,2}/'; if(preg_match($pdoReg, self::getEnvironmentData('DB_' . $alias . '_DNS'), $matches)){ // remove unnamed matches $matches = array_intersect_key($matches, $config); // remove empty matches $matches = array_filter($matches); // merge matches with default config $config = array_merge($config, $matches); } // connect options -------------------------------------------------------------------------------------------- $options = [ \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, \PDO::ATTR_TIMEOUT => $f3->get('REQUIREMENTS.MYSQL.PDO_TIMEOUT') ]; if($config['SCHEME'] == 'mysql'){ $options[\PDO::MYSQL_ATTR_COMPRESS] = true; $options[\PDO::MYSQL_ATTR_INIT_COMMAND] = implode(',', [ "SET NAMES " . self::getRequiredDbVars($f3, $config['SCHEME'])['CHARACTER_SET_CONNECTION'] . " COLLATE " . self::getRequiredDbVars($f3, $config['SCHEME'])['COLLATION_CONNECTION'], "@@session.time_zone = '+00:00'", "@@session.default_storage_engine = " . self::getRequiredDbVars($f3, $config['SCHEME'])['DEFAULT_STORAGE_ENGINE'] ]); } if(self::getPathfinderData('experiments.persistent_db_connections')){ $options[\PDO::ATTR_PERSISTENT] = true; } $config['OPTIONS'] = $options; return $config; } /** * get required MySQL variables from requirements.ini * @param \Base $f3 * @param string $schema * @return array */ static function getRequiredDbVars(\Base $f3, string $schema) : array { return $f3->exists('REQUIREMENTS[' . strtoupper($schema) . '][VARS]', $vars) ? $vars : []; } /** * get SMTP config values * @return \stdClass */ static function getSMTPConfig() : \stdClass{ $config = new \stdClass(); $config->host = self::getEnvironmentData('SMTP_HOST'); $config->port = self::getEnvironmentData('SMTP_PORT'); $config->scheme = self::getEnvironmentData('SMTP_SCHEME'); $config->username = self::getEnvironmentData('SMTP_USER'); $config->password = self::getEnvironmentData('SMTP_PASS'); $config->from = [ self::getEnvironmentData('SMTP_FROM') => self::getPathfinderData('name') ]; return $config; } /** * validates an SMTP config * @param \stdClass $config * @return bool */ static function isValidSMTPConfig(\stdClass $config) : bool { // validate email from either an configured array or plain string $validateMailConfig = function($mailConf = null) : bool { $email = null; if(is_array($mailConf)){ reset($mailConf); $email = key($mailConf); }elseif(is_string($mailConf)){ $email = $mailConf; } return \Audit::instance()->email($email); }; return ( !empty($config->host) && !empty($config->username) && $validateMailConfig($config->from) && $validateMailConfig($config->to) ); } /** * get email for notifications by hive key * @param $key * @return mixed */ static function getNotificationMail($key){ return self::getPathfinderData('notification' . ($key ? '.' . $key : '')); } /** * get map default config values for map types (private/corp/ally) * -> read from pathfinder.ini * @param string $mapType * @return mixed */ static function getMapsDefaultConfig($mapType = ''){ if( $mapConfig = self::getPathfinderData('map' . ($mapType ? '.' . $mapType : '')) ){ $mapConfig = Util::arrayChangeKeyCaseRecursive($mapConfig); } return $mapConfig; } /** * get Plugin config from `plugin.ini` * @param string|null $key * @param bool $checkEnabled * @return array|null */ static function getPluginConfig(?string $key, bool $checkEnabled = true) : ?array { $isEnabled = $checkEnabled ? filter_var(\Base::instance()->get( self::HIVE_KEY_PLUGIN . '.' . strtoupper($key) . '_ENABLED'), FILTER_VALIDATE_BOOLEAN ) : true; $data = null; if($isEnabled){ $hiveKey = self::HIVE_KEY_PLUGIN . '.' . strtoupper($key); $data = (array)\Base::instance()->get($hiveKey); } return $data; } /** * get custom $message for a a HTTP $status * -> use this in addition to the very general Base::HTTP_XXX labels * @param int $status * @return string */ static function getMessageFromHTTPStatus(int $status) : string { switch($status){ case 403: $message = 'Access denied: User not found'; break; default: $message = ''; } return $message; } /** * use this function to "validate" the socket connection. * The result will be CACHED for a few seconds! * This function is intended to pre-check a Socket connection if it MIGHT exists. * No data will be send to the Socket, this function just validates if a socket is available * -> see pingDomain() * @param string $uri * @return bool */ static function validSocketConnect(string $uri) : bool{ $valid = false; $f3 = \Base::instance(); if( !$f3->exists(self::CACHE_KEY_SOCKET_VALID, $valid) ){ if( $socketUrl = self::getSocketUri() ){ // get socket URI parts -> not elegant... $domain = parse_url( $socketUrl, PHP_URL_SCHEME) . '://' . parse_url( $socketUrl, PHP_URL_HOST); $port = parse_url( $socketUrl, PHP_URL_PORT); // check connection -> get ms $status = self::pingDomain($domain, $port); if($status >= 0){ // connection OK $valid = true; }else{ // connection error/timeout $valid = false; } }else{ // requirements check failed or URL not valid $valid = false; } $f3->set(self::CACHE_KEY_SOCKET_VALID, $valid, self::CACHE_TTL_SOCKET_VALID); } return $valid; } /** * get response time for a host in ms or -1 on error/timeout * @param string $domain * @param int $port * @param int $timeout * @return int */ static function pingDomain(string $domain, int $port, $timeout = 1) : int { $startTime = microtime(true); $file = @fsockopen ($domain, $port, $errno, $errstr, $timeout); $stopTime = microtime(true); if (!$file){ // Site is down $status = -1; }else { fclose($file); $status = ($stopTime - $startTime) * 1000; $status = floor($status); } return $status; } /** * get URI for TCP socket * @return bool|string */ static function getSocketUri(){ $uri = false; if( ( $ip = self::getEnvironmentData('SOCKET_HOST') ) && ( $port = self::getEnvironmentData('SOCKET_PORT') ) ){ $uri = 'tcp://' . $ip . ':' . $port; } return $uri; } /** * @param string $key * @return null|mixed */ static function getPathfinderData($key = ''){ $hiveKey = self::HIVE_KEY_PATHFINDER . ($key ? '.' . strtoupper($key) : ''); if( !\Base::instance()->exists($hiveKey, $data) ){ $data = null; } return $data; } /** * get HTTP status message by HTTP return code * -> either from F3 or from self::Config constants * @param int $code * @return string */ static function getHttpStatusByCode(int $code) : string { if(empty($status = @constant('Base::HTTP_' . $code))){ $status = @constant('self::HTTP_' . $code); } return $status; } /** * parse [D]ata [S]ource [N]ame string from *.ini into $conf parts * -> $dsn = redis=localhost:6379:2 * $conf = ['type' => 'redis', 'host' => 'localhost', 'port' => 6379, 'db' => 2] * -> some $conf values might be NULL if not found in $dsn! * -> some missing values become defaults * @param string $dsn * @param array|null $conf * @return bool */ static function parseDSN(string $dsn, ?array &$conf = []) : bool { // reset reference if($matches = (bool)preg_match('/^(\w+)\h*=\h*(.+)/', strtolower(trim($dsn)), $parts)){ $conf['type'] = $parts[1]; if($conf['type'] == 'redis'){ [$conf['host'], $conf['port'], $conf['db'], $conf['auth']] = explode(':', $parts[2]) + [1 => 6379, 2 => null, 3 => null]; }elseif($conf['type'] == 'folder'){ $conf['folder'] = $parts[2]; } // int cast numeric values $conf = array_map(function($val){ return is_numeric($val) ? intval($val) : $val; }, $conf); } return $matches; } /** * check if a given DateTime() is within downTime range: downtime + 10m * -> can be used for prevent logging errors during downTime * @param \DateTime|null $dateCheck * @return bool */ static function inDownTimeRange(\DateTime $dateCheck = null) : bool { $inRange = false; // default daily downtime 00:00am $downTimeParts = [0, 0]; if( !empty($downTime = (string)self::getEnvironmentData('CCP_SSO_DOWNTIME')) ){ $parts = array_map('intval', explode(':', $downTime)); if(count($parts) === 2){ // well formatted DOWNTIME found in config files $downTimeParts = $parts; } } try{ // downTime Range is 10m $downtimeLength = self::DOWNTIME_LENGTH + (2 * self::DOWNTIME_BUFFER); $timezone = \Base::instance()->get('getTimeZone')(); // if not set -> use current time $dateCheck = is_null($dateCheck) ? new \DateTime('now', $timezone) : $dateCheck; $dateDowntimeStart = new \DateTime('now', $timezone); $dateDowntimeStart->setTime($downTimeParts[0],$downTimeParts[1]); $dateDowntimeStart->sub(new \DateInterval('PT' . self::DOWNTIME_BUFFER . 'M')); $dateDowntimeEnd = clone $dateDowntimeStart; $dateDowntimeEnd->add(new \DateInterval('PT' . $downtimeLength . 'M')); $dateRange = new DateRange($dateDowntimeStart, $dateDowntimeEnd); $inRange = $dateRange->inRange($dateCheck); }catch(\Exception $e){ $f3 = \Base::instance(); $f3->error(500, $e->getMessage(), $e->getTrace()); } return $inRange; } /** * format timeInterval in seconds into human readable string * @param int $seconds * @return string * @throws \Exception */ static function formatTimeInterval(int $seconds = 0) : string { $dtF = new \DateTime('@0'); $dtT = new \DateTime("@" . $seconds); $diff = $dtF->diff($dtT); $format = ($d = $diff->format('%d')) ? $d . 'd ' : ''; $format .= ($h = $diff->format('%h')) ? $h . 'h ' : ''; $format .= ($i = $diff->format('%i')) ? $i . 'm ' : ''; $format .= ($s = $diff->format('%s')) ? $s . 's' : ''; return $format; } /** * @param $fromExists * @param int $ttlMax * @return int */ static function ttlLeft($fromExists, int $ttlMax) : int { $ttlMax = max($ttlMax, 0); if($fromExists){ // == true || array if(is_array($fromExists)){ return max(min((int)ceil(round(array_sum($fromExists) - microtime(true), 4)), $ttlMax), 0); }else{ return 0; } }else{ // == false return $ttlMax; } } /** * @param string|null $class * @return string */ static function withNamespace(?string $class) : string { $path = [\Base::instance()->get('NAMESPACE')]; if($class){ $path[] = $class; } return implode('\\', $path); } }