Files
pathfinder/app/Lib/Config.php
Mark Friedrich 9a3f45fdc7 - New map right "share map" added, closed #927
- Upgraded "[_Select2_](https://select2.org/)" js lib `v4.0.6-rc.1` → `v4.0.13`
- Fixed some issues where changed map settings (e.g. "share") do not get updated/stored, closed #889, closed #925
- Moved ajax endpoints for map create/update/delete into `/Api/Rest/` dir
- Minor UI improvements for "manual dialog" (fixed pixelated text)
2020-03-15 19:38:44 +01:00

686 lines
23 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Created by PhpStorm.
* User: Exodus
* Date: 28.05.2016
* Time: 16:05
*/
namespace Exodus4D\Pathfinder\Lib;
use Exodus4D\Pathfinder\Lib\Db\Pool;
use Exodus4D\Pathfinder\Lib\Api\CcpClient;
use Exodus4D\Pathfinder\Lib\Api\GitHubClient;
use Exodus4D\Pathfinder\Lib\Api\SsoClient;
use Exodus4D\Pathfinder\Lib\Api\EveScoutClient;
use Exodus4D\Pathfinder\Lib\Socket\AbstractSocket;
use Exodus4D\Pathfinder\Lib\Socket\SocketInterface;
use Exodus4D\Pathfinder\Lib\Socket\TcpSocket;
class Config extends \Prefab {
/**
* prefix for custom Pathfinder env vars
*/
const PREFIX_KEY = 'PF';
/**
* delimiter for custom Pathfinder env vars
*/
const ARRAY_DELIMITER = '-';
/**
* Hive key for all Pathfinder config vars (*.ini files)
*/
const HIVE_KEY_PATHFINDER = 'PATHFINDER';
/**
* Hive key for all environment config vars (*.ini files)
*/
const HIVE_KEY_ENVIRONMENT = 'ENVIRONMENT';
/**
* Hive key for custom plugins (js map modules)
*/
const HIVE_KEY_PLUGIN = 'PLUGIN';
/**
* Hive key for Socket validation check
*/
const CACHE_KEY_SOCKET_VALID = 'CACHED_SOCKET_VALID';
/**
* Cache time for Socket validation check
*/
const CACHE_TTL_SOCKET_VALID = 60;
// ================================================================================================================
// Redis
// ================================================================================================================
/**
* Redis connect timeout (seconds)
*/
const REDIS_OPT_TIMEOUT = 2;
/**
* Redis read timeout (seconds)
*/
const REDIS_OPT_READ_TIMEOUT = 10;
/**
* redis retry interval (milliseconds)
*/
const REDIS_OPT_RETRY_INTERVAL = 200;
// ================================================================================================================
// EVE downtime
// ================================================================================================================
/**
* SSO downtime length (estimation), minutes
*/
const DOWNTIME_LENGTH = 8;
/**
* SSO downtime buffer length extends downtime length, minutes
*/
const DOWNTIME_BUFFER = 1;
// ================================================================================================================
// ESI API id´s
// ================================================================================================================
/**
* ESI categoryId or 'Structure's
*/
const ESI_CATEGORY_STRUCTURE_ID = 65;
/**
* ESI categoryId or 'Ship's
*/
const ESI_CATEGORY_SHIP_ID = 6;
/**
* ESI groupId or 'Wormhole's
*/
const ESI_GROUP_WORMHOLE_ID = 988;
/**
* ESI dogmaAttributeId for 'scanWormholeStrength's
*/
const ESI_DOGMA_ATTRIBUTE_SCANWHSTRENGTH_ID = 1908;
/**
* error message for missing Composer dependency class
*/
const ERROR_CLASS_NOT_EXISTS_COMPOSER = 'Class "%s" not found. → Check installed Composer packages';
/**
* error message for missing Composer dependency method
*/
const ERROR_METHOD_NOT_EXISTS_COMPOSER = 'Method "%s()" not found in class "%s". → Check installed Composer packages';
/**
* environment config keys that should be parsed as array
* -> 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 = '/^(?<SCHEME>[[:alpha:]]+):((host=(?<HOST>[a-zA-Z0-9-_\.]*))|(unix_socket=(?<SOCKET>[a-zA-Z0-9\/]*\.sock)))((;dbname=(?<NAME>\w*))|(;port=(?<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;
}
/**
* 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 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);
}
}