[], 'BASE' => ['missingOk' => true], 'URL' => [], 'DEBUG' => [], 'DB_PF_DNS' => [], 'DB_PF_NAME' => [], 'DB_PF_USER' => [], 'DB_PF_PASS' => [], 'DB_UNIVERSE_DNS' => [], 'DB_UNIVERSE_NAME' => [], 'DB_UNIVERSE_USER' => [], 'DB_UNIVERSE_PASS' => [], 'CCP_SSO_URL' => [], 'CCP_SSO_CLIENT_ID' => [], 'CCP_SSO_SECRET_KEY' => [], 'CCP_SSO_DOWNTIME' => [], 'CCP_ESI_URL' => [], 'CCP_ESI_DATASOURCE' => [], 'SMTP_HOST' => [], 'SMTP_PORT' => [], 'SMTP_SCHEME' => [], 'SMTP_USER' => [], 'SMTP_PASS' => [], 'SMTP_FROM' => [], 'SMTP_ERROR' => [] ]; /** * required database setup * @var array */ protected $databases = [ 'PF' => [ 'info' => [], 'models' => [ 'Model\Pathfinder\CronModel', 'Model\Pathfinder\UserModel', 'Model\Pathfinder\AllianceModel', 'Model\Pathfinder\CorporationModel', 'Model\Pathfinder\MapModel', 'Model\Pathfinder\MapScopeModel', 'Model\Pathfinder\MapTypeModel', 'Model\Pathfinder\SystemTypeModel', 'Model\Pathfinder\SystemStatusModel', 'Model\Pathfinder\RightModel', 'Model\Pathfinder\RoleModel', 'Model\Pathfinder\StructureModel', 'Model\Pathfinder\CharacterStatusModel', 'Model\Pathfinder\ConnectionScopeModel', 'Model\Pathfinder\StructureStatusModel', 'Model\Pathfinder\CharacterMapModel', 'Model\Pathfinder\AllianceMapModel', 'Model\Pathfinder\CorporationMapModel', 'Model\Pathfinder\CorporationRightModel', 'Model\Pathfinder\CorporationStructureModel', 'Model\Pathfinder\UserCharacterModel', 'Model\Pathfinder\CharacterModel', 'Model\Pathfinder\CharacterAuthenticationModel', 'Model\Pathfinder\CharacterLogModel', 'Model\Pathfinder\SystemModel', 'Model\Pathfinder\ConnectionModel', 'Model\Pathfinder\ConnectionLogModel', 'Model\Pathfinder\SystemSignatureModel', 'Model\Pathfinder\ActivityLogModel', 'Model\Pathfinder\SystemShipKillModel', 'Model\Pathfinder\SystemPodKillModel', 'Model\Pathfinder\SystemFactionKillModel', 'Model\Pathfinder\SystemJumpModel' ] ], 'UNIVERSE' => [ 'info' => [], 'models' => [ 'Model\Universe\DogmaAttributeModel', 'Model\Universe\TypeAttributeModel', 'Model\Universe\TypeModel', 'Model\Universe\GroupModel', 'Model\Universe\CategoryModel', 'Model\Universe\FactionModel', 'Model\Universe\AllianceModel', 'Model\Universe\CorporationModel', 'Model\Universe\RaceModel', 'Model\Universe\StationModel', 'Model\Universe\StructureModel', 'Model\Universe\StargateModel', 'Model\Universe\StarModel', 'Model\Universe\PlanetModel', 'Model\Universe\SystemModel', 'Model\Universe\ConstellationModel', 'Model\Universe\RegionModel', 'Model\Universe\SystemNeighbourModel', 'Model\Universe\SystemStaticModel', 'Model\Universe\SovereigntyMapModel', 'Model\Universe\FactionWarSystemModel' ] ] ]; /** * database error * @var bool */ protected $databaseHasError = false; /** * event handler for all "views" * some global template variables are set in here * @param \Base $f3 * @param array $params * @return bool */ function beforeroute(\Base $f3, $params): bool { $f3->set('tplResource', $this->initResource($f3)); // page title $f3->set('tplPageTitle', 'Setup | ' . Config::getPathfinderData('name')); // main page content $f3->set('tplPageContent', Config::getPathfinderData('view.setup')); // body element class $f3->set('tplBodyClass', 'pf-landing'); // top navigation configuration $f3->set('tplNavigation', $this->getNavigationConfig()); return true; } /** * @param \Base $f3 */ public function afterroute(\Base $f3) { // js view (file) $f3->set('tplJsView', 'setup'); $f3->set('tplCounter', $this->counter()); $f3->set('tplConvertBytes', function(){ return call_user_func_array([Number::instance(), 'bytesToString'], func_get_args()); }); // render view echo \Template::instance()->render( Config::getPathfinderData('view.index') ); } /** * main setup route handler * works as dispatcher for setup functions * -> for security reasons all /setup "routes" are dispatched by GET params * @param \Base $f3 * @throws \Exception */ public function init(\Base $f3){ $params = $f3->get('GET'); // enables automatic column fix $fixColumns = false; switch($params['action']){ case 'createDB': $this->createDB($f3, $params['db']); break; case 'bootstrapDB': $this->bootstrapDB($f3, $params['db']); break; case 'fixCols': $fixColumns = true; break; case 'importTable': $this->importTable($params['model']); break; case 'exportTable': $this->exportTable($params['model']); break; case 'clearFiles': $this->clearFiles((string)$params['path']); break; case 'flushRedisDb': $this->flushRedisDb((string)$params['host'], (int)$params['port'], (int)$params['db']); break; case 'invalidateCookies': $this->invalidateCookies($f3); break; } // ============================================================================================================ // Template data // ============================================================================================================ // Server ----------------------------------------------------------------------------------------------------- // Server information $f3->set('serverInformation', $this->getServerInformation($f3)); // Pathfinder directory config $f3->set('directoryConfig', $this->getDirectoryConfig($f3)); // Server environment variables $f3->set('checkSystemConfig', $this->checkSystemConfig($f3)); // Environment ------------------------------------------------------------------------------------------------ // Server requirement $f3->set('checkRequirements', $this->checkRequirements($f3)); // PHP config $f3->set('checkPHPConfig', $this->checkPHPConfig($f3)); // Settings --------------------------------------------------------------------------------------------------- // Pathfinder environment config $f3->set('environmentInformation', $this->getEnvironmentInformation($f3)); // Pathfinder map default config $f3->set('mapsDefaultConfig', $this->getMapsDefaultConfig($f3)); // Database --------------------------------------------------------------------------------------------------- // Database config $f3->set('checkDatabase', $this->checkDatabase($f3, $fixColumns)); // Redis ------------------------------------------------------------------------------------------------------ // Redis information $f3->set('checkRedisInformation', $this->checkRedisInformation($f3)); // Socket ----------------------------------------------------------------------------------------------------- // WebSocket information $f3->set('socketInformation', $this->getSocketInformation($f3)); // Cronjob ---------------------------------------------------------------------------------------------------- $f3->set('cronConfig', $this->getCronConfig($f3)); // Administration --------------------------------------------------------------------------------------------- // Index information $f3->set('indexInformation', $this->getIndexData($f3)); // Filesystem (cache) size $f3->set('checkDirSize', $this->checkDirSize($f3)); } /** * get top navigation configuration * @return array */ protected function getNavigationConfig() : array { return [ 'server' => [ 'icon' => 'fa-home' ], 'environment' => [ 'icon' => 'fa-server' ], 'settings' => [ 'icon' => 'fa-sliders-h' ], 'database' => [ 'icon' => 'fa-database' ], 'cache' => [ 'icon' => 'fa-hdd' ], 'socket' => [ 'icon' => 'fa-exchange-alt' ], 'cronjob' => [ 'icon' => 'fa-user-clock' ], 'administration' => [ 'icon' => 'fa-wrench' ] ]; } /** * set environment information * @param \Base $f3 * @return array */ protected function getEnvironmentInformation(\Base $f3) : array { $environmentData = []; // exclude some sensitive data (e.g. database, passwords) $excludeVars = [ 'DB_PF_DNS', 'DB_PF_NAME', 'DB_PF_USER', 'DB_PF_PASS', 'DB_UNIVERSE_DNS', 'DB_UNIVERSE_NAME', 'DB_UNIVERSE_USER', 'DB_UNIVERSE_PASS' ]; // obscure some values $obscureVars = ['CCP_SSO_CLIENT_ID', 'CCP_SSO_SECRET_KEY', 'SMTP_PASS']; foreach($this->environmentVars as $var => $options){ if( !in_array($var, $excludeVars) ){ $value = Config::getEnvironmentData($var); $check = true; if(is_null($value) && !array_key_exists('missingOk', $options)){ // variable missing $check = false; $value = '[missing]'; }elseif( in_array($var, $obscureVars)){ $value = Util::obscureString($value); } $environmentData[$var] = [ 'label' => $var, 'value' => ((empty($value) && !is_int($value)) ? ' ' : $value), 'check' => $check ]; } } return $environmentData; } /** * get server information * @param \Base $f3 * @return array */ protected function getServerInformation(\Base $f3) : array { return [ 'time' => [ 'label' => 'Time', 'value' => date('Y/m/d H:i:s') . ' - (' . $f3->get('TZ') . ')' ], 'os' => [ 'label' => 'OS', 'value' => function_exists('php_uname') ? php_uname('s') : $_SERVER['OS'] ], 'name' => [ 'label' => 'Host name', 'value' => function_exists('php_uname') ? php_uname('n') : $_SERVER['SERVER_NAME'] ], 'release' => [ 'label' => 'Release name', 'value' => function_exists('php_uname') ? php_uname('r') : 'unknown' ], 'version' => [ 'label' => 'Version info', 'value' => function_exists('php_uname') ? php_uname('v') : 'unknown' ], 'machine' => [ 'label' => 'Machine type', 'value' => function_exists('php_uname') ? php_uname('m') : $_SERVER['PROCESSOR_ARCHITECTURE'] ], 'root' => [ 'label' => 'Document root', 'value' => $f3->get('ROOT') ], 'port' => [ 'label' => 'Port', 'value' => $f3->get('PORT') ], 'protocol' => [ 'label' => 'Protocol - scheme', 'value' => $f3->get('SERVER.SERVER_PROTOCOL') . ' - ' . $f3->get('SCHEME') ] ]; } /** * get information for used directories * @param \Base $f3 * @return array */ protected function getDirectoryConfig(\Base $f3) : array { return [ 'TEMP' => [ 'label' => 'TEMP', 'value' => $f3->get('TEMP'), 'check' => true, 'tooltip' => 'Temporary folder for pre compiled templates.', 'chmod' => Util::filesystemInfo($f3->get('TEMP'))['chmod'] ], 'CACHE' => [ 'label' => 'CACHE', 'value' => $f3->get('CACHE'), 'check' => true, 'tooltip' => 'Cache backend. Support for Redis, Memcache, APC, WinCache, XCache and a filesystem-based (default) cache.', 'chmod' => ((Config::parseDSN($f3->get('CACHE'), $confCache)) && $confCache['type'] == 'folder') ? Util::filesystemInfo((string)$confCache['folder'])['chmod'] : '' ], 'API_CACHE' => [ 'label' => 'API_CACHE', 'value' => $f3->get('API_CACHE'), 'check' => true, 'tooltip' => 'Cache backend for API related cache data. Support for Redis and a filesystem-based (default) cache.', 'chmod' => ((Config::parseDSN($f3->get('API_CACHE'), $confCacheApi)) && $confCacheApi['type'] == 'folder') ? Util::filesystemInfo((string)$confCacheApi['folder'])['chmod'] : '' ], 'LOGS' => [ 'label' => 'LOGS', 'value' => $f3->get('LOGS'), 'check' => true, 'tooltip' => 'Folder for pathfinder logs (e.g. cronjob-, error-logs, ...).', 'chmod' => Util::filesystemInfo($f3->get('LOGS'))['chmod'] ], 'UI' => [ 'label' => 'UI', 'value' => $f3->get('UI'), 'check' => true, 'tooltip' => 'Folder for public accessible resources (templates, js, css, images,..).', 'chmod' => Util::filesystemInfo($f3->get('UI'))['chmod'] ], 'AUTOLOAD' => [ 'label' => 'AUTOLOAD', 'value' => $f3->get('AUTOLOAD'), 'check' => true, 'tooltip' => 'Autoload folder for PHP files.', 'chmod' => Util::filesystemInfo($f3->get('AUTOLOAD'))['chmod'] ], 'FAVICON' => [ 'label' => 'FAVICON', 'value' => $f3->get('FAVICON'), 'check' => true, 'tooltip' => 'Folder for Favicons.', 'chmod' => Util::filesystemInfo($f3->get('FAVICON'))['chmod'] ], 'HISTORY' => [ 'label' => 'HISTORY [optional]', 'value' => Config::getPathfinderData('history.log'), 'check' => true, 'tooltip' => 'Folder for log history files. (e.g. change logs for maps).', 'chmod' => Util::filesystemInfo(Config::getPathfinderData('history.log'))['chmod'] ], 'CONFIG' => [ 'label' => 'CONFIG PATH [optional]', 'value' => implode(' ', (array)$f3->get('CONF')), 'check' => true, 'tooltip' => 'Folder for custom *.ini files. (e.g. when overwriting of default values in app/*.ini)' ] ]; } /** * check all required backend requirements * (Fat Free Framework) * @param \Base $f3 * @return array */ protected function checkRequirements(\Base $f3) : array { $serverData = self::getServerData(0); $checkRequirements = [ 'serverType' => [ 'label' => 'Server type', 'version' => $serverData->type, 'check' => true ], 'serverVersion' => [ 'label' => 'Server version', 'required' => $serverData->requiredVersion, 'version' => $serverData->version, 'check' => version_compare( $serverData->version, $serverData->requiredVersion, '>='), 'tooltip' => 'If not specified, please check your \'ServerTokens\' server config. (not critical)' ], 'phpInterface' => [ 'label' => 'PHP interface type', 'version' => $serverData->phpInterfaceType, 'check' => empty($serverData->phpInterfaceType) ? false : true ], 'php' => [ 'label' => 'PHP', 'required' => number_format((float)$f3->get('REQUIREMENTS.PHP.VERSION'), 1, '.', ''), 'version' => phpversion(), 'check' => version_compare( phpversion(), $f3->get('REQUIREMENTS.PHP.VERSION'), '>=') ], 'php_bit' => [ 'label' => 'php_int_size', 'required' => ($f3->get('REQUIREMENTS.PHP.PHP_INT_SIZE') * 8 ) . '-bit', 'version' => (PHP_INT_SIZE * 8) . '-bit', 'check' => $f3->get('REQUIREMENTS.PHP.PHP_INT_SIZE') == PHP_INT_SIZE ], [ 'label' => 'PHP extensions' ], 'pcre' => [ 'label' => 'PCRE', 'required' => $f3->get('REQUIREMENTS.PHP.PCRE_VERSION'), 'version' => strstr(PCRE_VERSION, ' ', true), 'check' => version_compare( strstr(PCRE_VERSION, ' ', true), $f3->get('REQUIREMENTS.PHP.PCRE_VERSION'), '>=') ], 'ext_pdo' => [ 'label' => 'PDO', 'required' => 'installed', 'version' => extension_loaded('pdo') ? 'installed' : 'missing', 'check' => extension_loaded('pdo') ], 'ext_pdoMysql' => [ 'label' => 'PDO_MYSQL', 'required' => 'installed', 'version' => extension_loaded('pdo_mysql') ? 'installed' : 'missing', 'check' => extension_loaded('pdo_mysql') ], 'ext_openssl' => [ 'label' => 'OpenSSL', 'required' => 'installed', 'version' => extension_loaded('openssl') ? 'installed' : 'missing', 'check' => extension_loaded('openssl') ], 'ext_xml' => [ 'label' => 'XML', 'required' => 'installed', 'version' => extension_loaded('xml') ? 'installed' : 'missing', 'check' => extension_loaded('xml') ], 'ext_gd' => [ 'label' => 'GD Library (for Image plugin)', 'required' => 'installed', 'version' => (extension_loaded('gd') && function_exists('gd_info')) ? 'installed' : 'missing', 'check' => (extension_loaded('gd') && function_exists('gd_info')) ], 'ext_curl' => [ 'label' => 'cURL (for Web plugin)', 'required' => 'installed', 'version' => (extension_loaded('curl') && function_exists('curl_version')) ? 'installed' : 'missing', 'check' => (extension_loaded('curl') && function_exists('curl_version')) ], 'ext_redis' => [ 'label' => 'Redis [optional]', 'required' => $f3->get('REQUIREMENTS.PHP.REDIS'), 'version' => extension_loaded('redis') ? phpversion('redis') : 'missing', 'check' => version_compare( phpversion('redis'), $f3->get('REQUIREMENTS.PHP.REDIS'), '>='), 'tooltip' => 'Redis can replace the default file-caching mechanic. It is much faster!' ], [ 'label' => 'LibEvent library [optional]' ], 'ext_event' => [ 'label' => 'Event extension', 'required' => $f3->get('REQUIREMENTS.PHP.EVENT'), 'version' => extension_loaded('event') ? phpversion('event') : 'missing', 'check' => version_compare( phpversion('event'), $f3->get('REQUIREMENTS.PHP.EVENT'), '>='), 'tooltip' => 'LibEvent PHP extension. Optional performance boost for WebSocket configuration.' ] ]; if($serverData->type != 'nginx'){ // default msg if module status not available $modNotFoundMsg = 'Module status can not be identified. ' . 'This can happen if PHP runs as \'FastCGI\'. Please check manual! '; // mod_rewrite check -------------------------------------------------------------------------------------- $modRewriteCheck = false; $modRewriteVersion = 'disabled'; $modRewriteTooltip = false; if(function_exists('apache_get_modules')){ if(in_array('mod_rewrite',apache_get_modules())){ $modRewriteCheck = true; $modRewriteVersion = 'enabled'; } }else{ // e.g. Nginx server $modRewriteVersion = 'unknown'; $modRewriteTooltip = $modNotFoundMsg; } $checkRequirements['mod_rewrite'] = [ 'label' => 'mod_rewrite', 'required' => 'enabled', 'version' => $modRewriteVersion, 'check' => $modRewriteCheck, 'tooltip' => $modRewriteTooltip ]; // mod_headers check -------------------------------------------------------------------------------------- $modHeadersCheck = false; $modHeadersVersion = 'disabled'; $modHeadersTooltip = false; if(function_exists('apache_get_modules')){ if(in_array('mod_headers',apache_get_modules())){ $modHeadersCheck = true; $modHeadersVersion = 'enabled'; } }else{ // e.g. Nginx server $modHeadersVersion = 'unknown'; $modHeadersTooltip = $modNotFoundMsg; } $checkRequirements['mod_headers'] = [ 'label' => 'mod_headers', 'required' => 'enabled', 'version' => $modHeadersVersion, 'check' => $modHeadersCheck, 'tooltip' => $modHeadersTooltip ]; } return $checkRequirements; } /** * check PHP config (php.ini) * @param \Base $f3 * @return array */ protected function checkPHPConfig(\Base $f3): array { $memoryLimit = (int)ini_get('memory_limit'); $maxInputVars = (int)ini_get('max_input_vars'); $maxExecutionTime = (int)ini_get('max_execution_time'); // 0 == infinite $htmlErrors = (int)ini_get('html_errors'); return [ 'exec' => [ 'label' => 'exec()', 'required' => $f3->get('REQUIREMENTS.PHP.EXEC'), 'version' => function_exists('exec'), 'check' => function_exists('exec') == $f3->get('REQUIREMENTS.PHP.EXEC'), 'tooltip' => 'exec() funktion. Check "disable_functions" in php.ini' ], 'memoryLimit' => [ 'label' => 'memory_limit', 'required' => $f3->get('REQUIREMENTS.PHP.MEMORY_LIMIT'), 'version' => $memoryLimit, 'check' => $memoryLimit >= $f3->get('REQUIREMENTS.PHP.MEMORY_LIMIT'), 'tooltip' => 'PHP default = 64MB.' ], 'maxInputVars' => [ 'label' => 'max_input_vars', 'required' => $f3->get('REQUIREMENTS.PHP.MAX_INPUT_VARS'), 'version' => $maxInputVars, 'check' => $maxInputVars >= $f3->get('REQUIREMENTS.PHP.MAX_INPUT_VARS'), 'tooltip' => 'PHP default = 1000. Increase it in order to import larger maps.' ], 'maxExecutionTime' => [ 'label' => 'max_execution_time', 'required' => $f3->get('REQUIREMENTS.PHP.MAX_EXECUTION_TIME'), 'version' => $maxExecutionTime, 'check' => !$maxExecutionTime || $maxExecutionTime >= $f3->get('REQUIREMENTS.PHP.MAX_EXECUTION_TIME'), 'tooltip' => 'PHP default = 30. Max execution time for PHP scripts.' ], 'htmlErrors' => [ 'label' => 'html_errors', 'required' => $f3->get('REQUIREMENTS.PHP.HTML_ERRORS'), 'version' => $htmlErrors, 'check' => (bool)$htmlErrors == (bool)$f3->get('REQUIREMENTS.PHP.HTML_ERRORS'), 'tooltip' => 'Formatted HTML StackTrace on error.' ], [ 'label' => 'Session' ], 'sessionSaveHandler' => [ 'label' => 'save_handler', 'version' => ini_get('session.save_handler'), 'check' => true, 'tooltip' => 'PHP Session save handler (Redis is preferred).' ], 'sessionSavePath' => [ 'label' => 'session.save_path', 'version' => ini_get('session.save_path'), 'check' => true, 'tooltip' => 'PHP Session save path (Redis is preferred).' ], 'sessionName' => [ 'label' => 'session.name', 'version' => ini_get('session.name'), 'check' => true, 'tooltip' => 'PHP Session name.' ] ]; } /** * check Redis (cache) config * -> only visible if Redis is used as Cache backend * @param \Base $f3 * @return array */ protected function checkRedisInformation(\Base $f3): array { $redisConfig = []; if( extension_loaded('redis') && class_exists('\Redis') ){ // collection of DSN specific $conf array (host, port, db,..) $dsnData = []; /** * @param int $dbNum * @param string $tag * @return string */ $getDbLabel = function(int $dbNum, string $tag) : string { return ' db(' . $dbNum . ') : ' . $tag; }; /** * get client information for a Redis client * @param \Redis $client * @param array $conf * @return array */ $getClientInfo = function(\Redis $client, array $conf) : array { return [ 'dsn' => [ 'label' => 'DSN', 'value' => $conf['host'] . ':' . $conf['port'] ], 'connected' => [ 'label' => 'status', 'value' => $client->isConnected() ] ]; }; /** * get status information for a Redis client * @param \Redis $client * @return array */ $getClientStats = function(\Redis $client) use ($f3) : array { $redisStats = []; if($client->isConnected() && !$client->getLastError()){ $redisServerInfo = (array)$client->info('SERVER'); $redisClientsInfo = (array)$client->info('CLIENTS'); $redisMemoryInfo = (array)$client->info('MEMORY'); $redisStatsInfo = (array)$client->info('STATS'); $redisStats = [ 'redisVersion' => [ 'label' => 'redis_version', 'required' => number_format((float)$f3->get('REQUIREMENTS.REDIS.VERSION'), 1, '.', ''), 'version' => $redisServerInfo['redis_version'], 'check' => version_compare( $redisServerInfo['redis_version'], $f3->get('REQUIREMENTS.REDIS.VERSION'), '>='), 'tooltip' => 'Redis server version' ], 'maxMemory' => [ 'label' => 'maxmemory', 'required' => Number::instance()->bytesToString($f3->get('REQUIREMENTS.REDIS.MAX_MEMORY')), 'version' => Number::instance()->bytesToString($redisMemoryInfo['maxmemory']), 'check' => $redisMemoryInfo['maxmemory'] >= $f3->get('REQUIREMENTS.REDIS.MAX_MEMORY'), 'tooltip' => 'Max memory limit for Redis' ], 'usedMemory' => [ 'label' => 'used_memory', 'version' => Number::instance()->bytesToString($redisMemoryInfo['used_memory']), 'check' => $redisMemoryInfo['used_memory'] < $redisMemoryInfo['maxmemory'], 'tooltip' => 'Current memory used by Redis' ], 'usedMemoryPeak' => [ 'label' => 'used_memory_peak', 'version' => Number::instance()->bytesToString($redisMemoryInfo['used_memory_peak']), 'check' => $redisMemoryInfo['used_memory_peak'] <= $redisMemoryInfo['maxmemory'], 'tooltip' => 'Peak memory used by Redis' ], 'maxmemoryPolicy' => [ 'label' => 'maxmemory_policy', 'required' => $f3->get('REQUIREMENTS.REDIS.MAXMEMORY_POLICY'), 'version' => $redisMemoryInfo['maxmemory_policy'], 'check' => $redisMemoryInfo['maxmemory_policy'] == $f3->get('REQUIREMENTS.REDIS.MAXMEMORY_POLICY'), 'tooltip' => 'How Redis behaves if \'maxmemory\' limit reached' ], 'connectedClients' => [ 'label' => 'connected_clients', 'version' => $redisClientsInfo['connected_clients'], 'check' => (bool)$redisClientsInfo['connected_clients'], 'tooltip' => 'Number of client connections (excluding connections from replicas)' ], 'blockedClients' => [ 'label' => 'blocked_clients', 'version' => $redisClientsInfo['blocked_clients'], 'check' => !(bool)$redisClientsInfo['blocked_clients'], 'tooltip' => 'Number of clients pending on a blocking call (BLPOP, BRPOP, BRPOPLPUSH)' ], 'evictedKeys' => [ 'label' => 'evicted_keys', 'version' => $redisStatsInfo['evicted_keys'], 'check' => !(bool)$redisStatsInfo['evicted_keys'], 'tooltip' => 'Number of evicted keys due to maxmemory limit' ], [ 'label' => 'Databases' ] ]; } return $redisStats; }; /** * get database status for current selected db * @param \Redis $client * @param string $tag * @return array */ $getDatabaseStatus = function(\Redis $client, string $tag) use ($getDbLabel) : array { $redisDatabases = []; if($client->isConnected() && !$client->getLastError()){ $dbNum = $client->getDbNum(); $dbSize = $client->dbSize(); $redisDatabases = [ 'db_' . $dbNum => [ 'label' => $getDbLabel($dbNum, $tag), 'version' => $dbSize . ' keys', 'check' => $dbSize > 0, 'tooltip' => 'Keys in db(' . $dbNum . ')', 'task' => [ [ 'action' => http_build_query([ 'action' => 'flushRedisDb', 'host' => $client->getHost(), 'port' => $client->getPort(), 'db' => $dbNum ]) . '#pf-setup-cache', 'label' => 'Flush', 'icon' => 'fa-trash', 'btn' => 'btn-danger' . (($dbSize > 0) ? '' : ' disabled') ] ] ] ]; } return $redisDatabases; }; /** * build (modify) $redisConfig with DNS $conf data * @param array $conf */ $buildRedisConfig = function(array $conf) use (&$redisConfig, $getDbLabel, $getClientInfo, $getClientStats, $getDatabaseStatus){ if($conf['type'] == 'redis'){ // is Redis -> group all DNS by host:port $uid = $conf['host'] . ':' . $conf['port']; $client = new \Redis(); try{ $client->pconnect($conf['host'], $conf['port'], 0.3); if(!empty($conf['auth'])){ $client->auth($conf['auth']); } if(isset($conf['db'])) { $client->select($conf['db']); } $conf['db'] = $client->getDbNum(); }catch(\RedisException $e){ // connection failed, getLastError() is called further down } if(!array_key_exists($uid, $redisConfig)){ $redisConfig[$uid] = $getClientInfo($client, $conf); $redisConfig[$uid]['status'] = $getClientStats($client) + $getDatabaseStatus($client, $conf['tag']); }elseif(!array_key_exists($uidDb = 'db_' . $conf['db'], $redisConfig[$uid]['status'])){ $redisConfig[$uid]['status'] += $getDatabaseStatus($client, $conf['tag']); }else{ $redisConfig[$uid]['status'][$uidDb]['label'] .= '; ' . $conf['tag']; } if($error = $client->getLastError()){ $redisConfig[$uid]['errors'][] = [ 'label' => $getDbLabel((int)$conf['db'], $conf['tag']), 'error' => $error ]; } $client->close(); } }; // potential Redis caches --------------------------------------------------------------------------------- $redisCaches = [ 'CACHE' => $f3->get('CACHE'), 'API_CACHE' => $f3->get('API_CACHE') ]; foreach($redisCaches as $tag => $dsn){ if(Config::parseDSN($dsn, $conf)){ $conf['tag'] = $tag; $dsnData[] = $conf; } } // if Session handler is also Redis -> add this as well --------------------------------------------------- // -> the DSN format is not the same, convert URL format into DSN if( strtolower(session_module_name()) == 'redis' && ($parts = parse_url(strtolower(session_save_path()))) ){ // parse URL parameters parse_str((string)$parts['query'], $params); $conf = [ 'type' => 'redis', 'host' => $parts['host'], 'port' => $parts['port'], 'db' => !empty($params['database']) ? (int)$params['database'] : 0, 'auth' => !empty($params['auth']) ? $params['auth'] : null, 'tag' => 'SESSION' ]; $dsnData[] = $conf; } // sort all $dsnData by 'db' number ----------------------------------------------------------------------- usort($dsnData, function($a, $b){ return $a['db'] <=> $b['db']; }); foreach($dsnData as $conf){ $buildRedisConfig($conf); } } return $redisConfig; } /** * check system environment vars * -> mostly relevant for development/build/deployment * @param \Base $f3 * @return array */ protected function checkSystemConfig(\Base $f3): array { $systemConf = []; if(function_exists('exec')){ $gitOut = $composerOut = $nodeOut = $npmOut = []; $gitStatus = $composerStatus = $nodeStatus = $npmStatus = 1; exec('git --version', $gitOut, $gitStatus); exec('composer -V', $composerOut, $composerStatus); exec('node -v', $nodeOut, $nodeStatus); exec('npm -v', $npmOut, $npmStatus); $normalizeVersion = function($version): string { return preg_replace("/[^0-9\.\s]/", '', (string)$version); }; $systemConf = [ 'git' => [ 'label' => 'Git', 'version' => $gitOut[0] ? 'installed' : 'missing', 'check' => $gitStatus == 0, 'tooltip' => 'Git # git --version : ' . $gitOut[0] ], 'composer' => [ 'label' => 'Composer', 'version' => $composerOut[0] ? 'installed' : 'missing', 'check' => $composerStatus == 0, 'tooltip' => 'Composer # composer -V : ' . $composerOut[0] ], 'node' => [ 'label' => 'NodeJs', 'required' => number_format((float)$f3->get('REQUIREMENTS.PATH.NODE'), 1, '.', ''), 'version' => $normalizeVersion($nodeOut[0]) ?: 'missing', 'check' => version_compare( $normalizeVersion($nodeOut[0]), number_format((float)$f3->get('REQUIREMENTS.PATH.NODE'), 1, '.', ''), '>='), 'tooltip' => 'NodeJs # node -v' ], 'npm' => [ 'label' => 'npm', 'required' => $f3->get('REQUIREMENTS.PATH.NPM'), 'version' => $normalizeVersion($npmOut[0]) ?: 'missing', 'check' => version_compare( $normalizeVersion($npmOut[0]), $f3->get('REQUIREMENTS.PATH.NPM'), '>='), 'tooltip' => 'npm # npm -v' ] ]; } return $systemConf; } /** * get default map config * @param \Base $f3 * @return array */ protected function getMapsDefaultConfig(\Base $f3): array { $matrix = \Matrix::instance(); $mapsDefaultConfig = (array)Config::getMapsDefaultConfig(); $matrix->transpose($mapsDefaultConfig); $mapConfig = ['mapTypes' => array_keys(reset($mapsDefaultConfig))]; foreach($mapsDefaultConfig as $option => $defaultConfig){ $tooltip = ''; switch($option){ case 'lifetime': $label = 'Map lifetime (days)'; $tooltip = 'Unchanged/inactive maps get auto deleted afterwards (cronjob).'; break; case 'max_count': $label = 'Max. maps count/user'; break; case 'max_shared': $label = 'Map share limit/map'; $tooltip = 'E.g. A Corp map can be shared with X other corps.'; break; case 'max_systems': $label = 'Max. systems count/map'; break; case 'log_activity_enabled': $label = ' Activity statistics'; $tooltip = 'If "enabled", map admins can enable user statistics for a map.'; break; case 'log_history_enabled': $label = ' History log files'; $tooltip = 'If "enabled", map admins can pipe map logs to file. (one file per map)'; break; case 'send_history_slack_enabled': $label = ' History log Slack'; $tooltip = 'If "enabled", map admins can set a Slack channel were map logs get piped to.'; break; case 'send_rally_slack_enabled': $label = ' Rally point poke Slack'; $tooltip = 'If "enabled", map admins can set a Slack channel for rally point pokes.'; break; case 'send_history_discord_enabled': $label = ' History log Discord'; $tooltip = 'If "enabled", map admins can set a Discord channel were map logs get piped to.'; break; case 'send_rally_discord_enabled': $label = ' Rally point poke Discord'; $tooltip = 'If "enabled", map admins can set a Discord channel for rally point pokes.'; break; case 'send_rally_mail_enabled': $label = ' Rally point poke Email'; $tooltip = 'If "enabled", rally point pokes can be send by Email (SMTP config + recipient address required).'; break; default: $label = 'unknown'; } $mapsDefaultConfig[$option] = [ 'label' => $label, 'tooltip' => $tooltip, 'data' => $defaultConfig ]; } $mapConfig['mapConfig'] = $mapsDefaultConfig; return $mapConfig; } /** * get database connection information * @param \Base $f3 * @param bool|false $exec * @return array */ protected function checkDatabase(\Base $f3, $exec = false){ foreach($this->databases as $dbAlias => $dbData){ $dbLabel = ''; $dbConfig = []; // DB connection status $dbConnected = false; // DB initialized as persistent connection $dbPersistent = false; // DB type (e.g. MySql,..) $dbDriver = 'unknown'; // enable database ::create() function on UI $dbCreate = false; // enable database ::setup() function on UI $dbSetupEnable = false; // check if everything is OK (connection, tables, columns, indexes,..) $dbStatusCheckCount = 0; // db queries for column fixes (types, indexes, unique) $dbColumnQueries = []; // tables that should exist in this DB $requiredTables = []; // get DB config $dbConfigValues = Config::getDatabaseConfig($f3, $dbAlias); // collection for errors $dbErrors = []; /** * @var $db Sql */ $db = $f3->DB->getDB($dbAlias); // check config that does NOT require a valid DB connection switch($dbAlias){ case 'PF': $dbLabel = 'Pathfinder'; break; case 'UNIVERSE': $dbLabel = 'EVE-Online universe'; break; } $dbName = $dbConfigValues['NAME']; $dbUser = $dbConfigValues['USER']; $dbAlias = $dbConfigValues['ALIAS']; if($db){ switch($dbAlias){ case 'PF': case 'UNIVERSE': // enable (table) setup for this DB $dbSetupEnable = true; // get table data from model foreach($dbData['models'] as $model){ $tableConfig = call_user_func(Config::withNamespace($model) . '::resolveConfiguration'); $requiredTables[$tableConfig['table']] = [ 'model' => $model, 'name' => $tableConfig['table'], 'fieldConf' => $tableConfig['fieldConf'], 'exists' => false, 'empty' => true, 'requiredCharset' => $tableConfig['charset'], 'requiredCollation' => $tableConfig['charset'] . '_unicode_ci', 'foreignKeys' => [] ]; } break; } // db connect was successful $dbConnected = true; $dbPersistent = $db->pdo()->getAttribute(\PDO::ATTR_PERSISTENT); $dbDriver = $db->driver(); $dbConfig = $this->checkDBConfig($f3, $db); // get tables $schema = new Schema($db); $currentTables = $schema->getTables(); // check each table for changes foreach($requiredTables as $requiredTableName => $data){ $tableCharset = null; $tableCollation = null; $tableExists = false; $tableRows = 0; // Check if table status is OK (no errors/warnings,..) $tableStatusCheckCount = 0; $currentColumns = []; if(in_array($requiredTableName, $currentTables)){ // Table exists $tableExists = true; // get existing table columns and column related constraints (if exists) $tableModifierTemp = new Mysql\TableModifier($requiredTableName, $schema); $currentColumns = $tableModifierTemp->getCols(true); // get row count $tableRows = $db->getRowCount($requiredTableName); $tableStatus = $db->getTableStatus($requiredTableName); if( !empty($tableStatus['Collation']) && ($statusVal = strstr($tableStatus['Collation'], '_', true)) !== false ){ $tableCharset = $statusVal; $tableCollation = $tableStatus['Collation']; } // find deprecated columns that are no longer needed ------------------------------------------ $deprecatedColumnNames = array_diff(array_keys($currentColumns), array_keys($data['fieldConf']), ['id']); foreach($deprecatedColumnNames as $deprecatedColumnName){ $requiredTables[$requiredTableName]['fieldConf'][$deprecatedColumnName]['deprecated'] = true; $requiredTables[$requiredTableName]['fieldConf'][$deprecatedColumnName]['currentType'] = 'deprecated'; //$requiredTables[$requiredTableName]['fieldConf'][$deprecatedColumnName]['statusCheck'] = false; //$tableStatusCheckCount++; //$tableModifierTemp->dropColumn($deprecatedColumnName); } //$buildStatus = $tableModifierTemp->build(false); //$dbColumnQueries = array_merge($dbColumnQueries, (array)$buildStatus); }else{ // table missing $dbStatusCheckCount++; $tableStatusCheckCount++; } foreach((array)$data['fieldConf'] as $columnName => $fieldConf){ // if 'nullable' key not set in $fieldConf, Column was created with 'nullable' = true (Cortex default) $fieldConf['nullable'] = isset($fieldConf['nullable']) ? (bool)$fieldConf['nullable'] : true; $columnStatusCheck = true; $foreignKeyStatusCheck = true; $requiredTables[$requiredTableName]['fieldConf'][$columnName]['requiredType'] = $fieldConf['type']; $requiredTables[$requiredTableName]['fieldConf'][$columnName]['requiredNullable'] = ($fieldConf['nullable']) ? '1' : '0'; $requiredTables[$requiredTableName]['fieldConf'][$columnName]['requiredIndex'] = ($fieldConf['index']) ? '1' : '0'; $requiredTables[$requiredTableName]['fieldConf'][$columnName]['requiredUnique'] = ($fieldConf['unique']) ? '1' : '0'; if(array_key_exists($columnName, $currentColumns)){ // column exists // get tableModifier -> possible column update $tableModifier = new Mysql\TableModifier($requiredTableName, $schema); // get new column and copy Schema from existing column $col = new Mysql\Column($columnName, $tableModifier); $col->copyfrom($currentColumns[$columnName]); $currentColType = $currentColumns[$columnName]['type']; $currentNullable = $currentColumns[$columnName]['nullable']; $hasNullable = $currentNullable ? '1' : '0'; $currentColIndexData = call_user_func(Config::withNamespace($data['model']) . '::indexExists', [$columnName]); $currentColIndex = is_array($currentColIndexData); $hasIndex = ($currentColIndex) ? '1' : '0'; $hasUnique = ($currentColIndexData['unique']) ? '1' : '0'; $changedType = false; $changedNullable = false; $changedUnique = false; $changedIndex = false; $addConstraints = []; // set (new) column information ----------------------------------------------------------- $requiredTables[$requiredTableName]['fieldConf'][$columnName]['exists'] = true; $requiredTables[$requiredTableName]['fieldConf'][$columnName]['currentType'] = $currentColType; $requiredTables[$requiredTableName]['fieldConf'][$columnName]['currentNullable'] = $hasNullable; $requiredTables[$requiredTableName]['fieldConf'][$columnName]['currentIndex'] = $hasIndex; $requiredTables[$requiredTableName]['fieldConf'][$columnName]['currentUnique'] = $hasUnique; // check constraint ----------------------------------------------------------------------- if(isset($fieldConf['constraint'])){ // add or update constraints foreach((array)$fieldConf['constraint'] as $constraintData){ $constraint = $col->newConstraint($constraintData); $foreignKeyExists = $col->constraintExists($constraint); // constraint information -> show in template $requiredTables[$requiredTableName]['foreignKeys'][] = [ 'exists' => $foreignKeyExists, 'keyName' => $constraint->getConstraintName() ]; if($foreignKeyExists){ // drop constraint and re-add again at the and, in case something has changed $col->dropConstraint($constraint); }else{ $tableStatusCheckCount++; $foreignKeyStatusCheck = false; } $addConstraints[] = $constraint; } } // check type changed --------------------------------------------------------------------- if( $fieldConf['type'] !== 'JSON' && !$schema->isCompatible($fieldConf['type'], $currentColType) ){ // column type has changed $changedType = true; $columnStatusCheck = false; $tableStatusCheckCount++; } // check if column nullable changed ------------------------------------------------------- if( $currentNullable != $fieldConf['nullable']){ $changedNullable = true; $columnStatusCheck = false; $tableStatusCheckCount++; } // check if column index changed ---------------------------------------------------------- $indexUpdate = false; $indexKey = (bool)$hasIndex; $indexUnique = (bool)$hasUnique; if($currentColIndex != $fieldConf['index']){ $changedIndex = true; $columnStatusCheck = false; $tableStatusCheckCount++; $indexUpdate = true; $indexKey = (bool)$fieldConf['index']; } // check if column unique changed --------------------------------------------------------- if($currentColIndexData['unique'] != $fieldConf['unique']){ $changedUnique = true; $columnStatusCheck = false; $tableStatusCheckCount++; $indexUpdate = true; $indexUnique = (bool)$fieldConf['unique']; } // build table with changed columns ------------------------------------------------------- if(!$columnStatusCheck || !$foreignKeyStatusCheck){ if(!$columnStatusCheck ){ // IMPORTANT: setType is always required! Even if type has not changed $col->type($fieldConf['type']); // update "nullable" if($changedNullable){ $col->nullable($fieldConf['nullable']); } // update/change/delete index/unique keys if($indexUpdate){ if($hasIndex){ $tableModifier->dropIndex($columnName); } if($indexKey){ $tableModifier->addIndex($columnName, $indexUnique); } } $tableModifier->updateColumn($columnName, $col); } // (re-)add constraints !after! index update is done // otherwise index update will fail if there are existing constraints foreach($addConstraints as $constraint){ $col->addConstraint($constraint); } $buildStatus = $tableModifier->build($exec); if( is_array($buildStatus) || is_string($buildStatus) ){ // query strings for change available $dbColumnQueries = array_merge($dbColumnQueries, (array)$buildStatus); } } // set (new) column information ----------------------------------------------------------- $requiredTables[$requiredTableName]['fieldConf'][$columnName]['changedType'] = $changedType; $requiredTables[$requiredTableName]['fieldConf'][$columnName]['changedNullable'] = $changedNullable; $requiredTables[$requiredTableName]['fieldConf'][$columnName]['changedUnique'] = $changedUnique; $requiredTables[$requiredTableName]['fieldConf'][$columnName]['changedIndex'] = $changedIndex; }elseif( !isset($fieldConf['has-manny']) && isset($fieldConf['type']) ){ // column not exists but it is required! // columns that do not match this criteria ("mas-manny") are "virtual" fields // and can be ignored $requiredTables[$requiredTableName]['fieldConf'][$columnName]['currentType'] = ''; $columnStatusCheck = false; $tableStatusCheckCount++; } $requiredTables[$requiredTableName]['fieldConf'][$columnName]['statusCheck'] = $columnStatusCheck; } $dbStatusCheckCount += $tableStatusCheckCount; $requiredTables[$requiredTableName]['currentCharset'] = $tableCharset; $requiredTables[$requiredTableName]['currentCollation'] = $tableCollation; $requiredTables[$requiredTableName]['rows'] = $tableRows; $requiredTables[$requiredTableName]['exists'] = $tableExists; $requiredTables[$requiredTableName]['statusCheckCount'] = $tableStatusCheckCount; } }else{ // DB connection failed $dbStatusCheckCount++; foreach($f3->DB->getErrors($dbAlias, 10) as $dbException){ $dbErrors[] = $dbException->getMessage(); } // try to connect without! DB (-> offer option to create them) // do not log errors (silent) $f3->DB->setSilent(true); $dbServer = $f3->DB->connectToServer($dbAlias); $f3->DB->setSilent(false); if(!is_null($dbServer)){ // connection succeeded $dbCreate = true; $dbDriver = $dbServer->driver(); } } if($dbStatusCheckCount !== 0){ $this->databaseHasError = true; } // sort tables for better readability ksort($requiredTables); $this->databases[$dbAlias]['info'] = [ 'label' => $dbLabel, 'host' => $dbConfigValues['SOCKET'] ? : $dbConfigValues['HOST'], 'port' => $dbConfigValues['PORT'] && !$dbConfigValues['SOCKET'] ? $dbConfigValues['PORT'] : '', 'driver' => $dbDriver, 'name' => $dbName, 'user' => $dbUser, 'pass' => Util::obscureString((string)$dbConfigValues['PASS'], 8), 'dbConfig' => $dbConfig, 'dbCreate' => $dbCreate, 'setupEnable' => $dbSetupEnable, 'connected' => $dbConnected, 'persistent' => $dbPersistent, 'statusCheckCount' => $dbStatusCheckCount, 'columnQueries' => $dbColumnQueries, 'tableData' => $requiredTables, 'errors' => $dbErrors ]; } if($exec){ $f3->reroute('@setup'); } return $this->databases; } /** * check MySQL params * @param \Base $f3 * @param Sql $db * @return array */ protected function checkDBConfig(\Base $f3, Sql $db) : array { $checkAll = true; // some db like "Maria DB" have some strange version strings.... $dbVersionString = $db->version(); $dbVersionParts = explode('-', $dbVersionString); $dbVersion = 'unknown'; foreach($dbVersionParts as $dbVersionPart){ // check if this is a valid version number // hint: MariaDB´s version is NOT always the last valid version number if( version_compare( $dbVersionPart, '1', '>' ) > 0 ){ $dbVersion = $dbVersionPart; } } $dbConfig = [ 'data' => [ 'version' => [ 'label' => 'DB version', 'required' => $f3->get('REQUIREMENTS.MYSQL.VERSION'), 'version' => $dbVersion, 'check' => version_compare($dbVersion, $f3->get('REQUIREMENTS.MYSQL.VERSION'), '>=' ) ? : $checkAll = false ] ] ]; $mySQLConfig = array_change_key_case((array)$f3->get('REQUIREMENTS.MYSQL.VARS')); $mySQLConfigKeys = array_keys($mySQLConfig); $results = $db->exec("SHOW VARIABLES WHERE Variable_Name IN ('" . implode("','", $mySQLConfigKeys) . "')"); $getValue = function(string $param) use ($results) : string { $match = array_filter($results, function($k) use ($param) : bool { return strtolower($k['Variable_name']) == $param; }); return !empty($match) ? end(reset($match)) : 'unknown'; }; $checkValue = function($requiredValue, $value) : bool { $check = true; if(!empty($requiredValue)){ if(is_int($requiredValue)){ $check = $requiredValue <= $value; }else{ $check = $requiredValue == $value; } } return $check; }; foreach($mySQLConfig as $param => $requiredValue){ $value = $getValue($param); $dbConfig['data'][] = [ 'label' => $param, 'required' => $requiredValue, 'version' => $value, 'check' => $checkValue($requiredValue, $value) ? : $checkAll = false ]; } $dbConfig['meta'] = [ 'check' => $checkAll ]; return $dbConfig; } /** * try to create a fresh database * @param \Base $f3 * @param string $dbAlias */ protected function createDB(\Base $f3, string $dbAlias){ // check for valid key if(!empty($this->databases[$dbAlias])){ // disable logging (we expect the DB connect to fail -> no db created) $f3->DB->setSilent(true); // try to connect $db = $f3->DB->getDB($dbAlias); // enable logging $f3->DB->setSilent(false, true); if(is_null($db)){ // try create new db $db = $f3->DB->createDB($dbAlias); if(is_null($db)){ foreach($f3->DB->getErrors($dbAlias, 5) as $error){ // ... no further error handling here -> check log files //$error->getMessage() } } } } } /** * init the complete database * - create tables * - create indexes * - set default static values * @param \Base $f3 * @param string $dbAlias * @return array */ protected function bootstrapDB(\Base $f3, string $dbAlias) : array { $checkTables = []; if($db = $f3->DB->getDB($dbAlias)){ // set some default config for this database $requiredVars = Config::getRequiredDbVars($f3, $db->driver()); $db->prepareDatabase($requiredVars['CHARACTER_SET_DATABASE'], $requiredVars['COLLATION_DATABASE']); // setup tables foreach($this->databases[$dbAlias]['models'] as $modelClass){ $checkTables[] = call_user_func(Config::withNamespace($modelClass) . '::setup', $db); } } return $checkTables; } /** * get Socket information (TCP (internal)), (WebSocket (clients)) * @param \Base $f3 * @return array * @throws \Exception */ protected function getSocketInformation(\Base $f3) : array { $ttl = 0.6; $task = 'healthCheck'; $healthCheckToken = microtime(true); $statusTcp = [ 'type' => 'danger', 'label' => 'INIT CONNECTION…', 'class' => 'txt-color-danger' ]; $statusWeb = [ 'type' => 'danger', 'label' => 'INIT CONNECTION…', 'class' => 'txt-color-danger' ]; $statsTcp = false; $statsWeb = false; $setStats = function(array $stats) use (&$statsTcp, &$statsWeb) { if(!empty($stats['tcpSocket'])){ $statsTcp = $stats['tcpSocket']; } if(!empty($stats['webSocket'])){ $statsWeb = $stats['webSocket']; } }; // ping TCP Socket with "healthCheck" task $f3->webSocket(['timeout' => $ttl]) ->write($task, $healthCheckToken) ->then( function($payload) use ($task, $healthCheckToken, &$statusTcp, $setStats) { if( $payload['task'] == $task && $payload['load'] == $healthCheckToken ){ $statusTcp['type'] = 'success'; $statusTcp['label'] = 'PING OK'; $statusTcp['class'] = 'txt-color-success'; }else{ $statusTcp['type'] = 'warning'; $statusTcp['label'] = is_string($payload['load']) ? $payload['load'] : 'INVALID RESPONSE'; $statusTcp['class'] = 'txt-color-warning'; } // statistics (e.g. current connection count) $setStats((array)$payload['stats']); }, function($payload) use (&$statusTcp, $setStats) { $statusTcp['label'] = $payload['load']; // statistics (e.g. current connection count) $setStats((array)$payload['stats']); }); return [ 'tcpSocket' => [ 'label' => 'TCP-Socket (intern)', 'icon' => 'fa-exchange-alt', 'status' => $statusTcp, 'stats' => $statsTcp, 'data' => [ [ 'label' => 'HOST', 'value' => Config::getEnvironmentData('SOCKET_HOST') ? : '[missing]', 'check' => !empty( Config::getEnvironmentData('SOCKET_HOST') ) ],[ 'label' => 'PORT', 'value' => Config::getEnvironmentData('SOCKET_PORT') ? : '[missing]', 'check' => !empty( Config::getEnvironmentData('SOCKET_PORT') ) ],[ 'label' => 'URI', 'value' => Config::getSocketUri() ? : '[missing]', 'check' => !empty( Config::getSocketUri() ) ],[ 'label' => 'timeout (seconds)', 'value' => $ttl, 'check' => !empty( $ttl ) ],[ 'label' => 'uptime', 'value' => Config::formatTimeInterval($statsTcp['startup'] ? : 0), 'check' => $statsTcp['startup'] > 0 ] ], 'token' => $healthCheckToken ], 'webSocket' => [ 'label' => 'Web-Socket', 'icon' => 'fa-random', 'status' => $statusWeb, 'stats' => $statsWeb, 'data' => [ [ 'label' => 'URI', 'value' => '', 'check' => null // undefined ] ] ] ]; } /** * get cronjob config * @param \Base $f3 * @return array */ protected function getCronConfig(\Base $f3) : array { $cron = Cron::instance(); $cronConf = [ 'log' => [ 'label' => 'LOG', 'required' => $f3->get('REQUIREMENTS.CRON.LOG'), 'version' => $f3->get('CRON.log'), 'check' => $f3->get('CRON.log') == $f3->get('REQUIREMENTS.CRON.LOG'), 'tooltip' => 'Write default cron.log' ], 'cli' => [ 'label' => 'CLI', 'required' => $f3->get('REQUIREMENTS.CRON.CLI'), 'version' => $f3->get('CRON.cli'), 'check' => $f3->get('CRON.cli') == $f3->get('REQUIREMENTS.CRON.CLI'), 'tooltip' => 'Jobs can be triggered by CLI. Must be set on Unix where "crontab -e" config is used' ], 'web' => [ 'label' => 'WEB', 'version' => (int)$f3->get('CRON.web'), 'check' => true, 'tooltip' => 'Jobs can be triggered by URL. Could be useful if jobs should be triggered by e.g. 3rd party app. Secure "/cron" url if active!' ], 'silent' => [ 'label' => 'SILENT', 'version' => (int)$f3->get('CRON.silent'), 'check' => true, 'tooltip' => 'Write job execution status to STDOUT if job completes' ] ]; return [ 'checkCronConfig' => $cronConf, 'settings' => $f3->constants($cron, 'DEFAULT_'), 'jobs' => $cron->getJobsConfig() ]; } /** * get indexed (cache) data information * @param \Base $f3 * @return array * @throws \Exception */ protected function getIndexData(\Base $f3) : array { // active DB and tables are required for obtain index data if(!$this->databaseHasError){ /** * @var $categoryUniverseModel Universe\CategoryModel */ $categoryUniverseModel = Universe\AbstractUniverseModel::getNew('CategoryModel'); $categoryUniverseModel->getById(Config::ESI_CATEGORY_STRUCTURE_ID, 0); $groupsCountStructure = $categoryUniverseModel->getGroupsCount(false); $typesCountStructure = $categoryUniverseModel->getTypesCount(false); $categoryUniverseModel->getById(Config::ESI_CATEGORY_SHIP_ID, 0); $groupsCountShip = $categoryUniverseModel->getGroupsCount(false); $typesCountShip = $categoryUniverseModel->getTypesCount(false); /** * @var $groupUniverseModel Universe\GroupModel */ $groupUniverseModel = Universe\AbstractUniverseModel::getNew('GroupModel'); $groupUniverseModel->getById(Config::ESI_GROUP_WORMHOLE_ID, 0); $wormholeCount = $groupUniverseModel->getTypesCount(false); /** * @var $systemNeighbourModel Universe\SystemNeighbourModel */ $systemNeighbourModel = Universe\AbstractUniverseModel::getNew('SystemNeighbourModel'); /** * @var $systemStaticModel Universe\SystemStaticModel */ $systemStaticModel = Universe\AbstractUniverseModel::getNew('SystemStaticModel'); if(empty($systemCountAll = count(($universeController = new UniverseController())->getSystemIds(true)))){ // no systems found in 'universe' DB. Clear potential existing system cache $universeController->clearSystemsIndex(); } $sum = function(int $carry, int $value) : int { return $carry + $value; }; $indexInfo = [ 'Wormholes' => [ 'task' => [ [ 'action' => 'buildIndex', 'label' => 'Import', 'icon' => 'fa-sync', 'btn' => 'btn-primary' ] ], 'label' => 'Wormholes data', 'countBuild' => $wormholeCount, 'countAll' => count(Universe\GroupModel::getUniverseGroupTypes(Config::ESI_GROUP_WORMHOLE_ID)), 'tooltip' => 'import all wormhole types (e.g. L031) from ESI. Runtime: ~25s' ], 'Structures' => [ 'task' => [ [ 'action' => 'buildIndex', 'label' => 'Import', 'icon' => 'fa-sync', 'btn' => 'btn-primary' ] ], 'label' => 'Structures data', 'countBuild' => $groupsCountStructure, 'countAll' => count(Universe\CategoryModel::getUniverseCategoryGroups(Config::ESI_CATEGORY_STRUCTURE_ID)), 'tooltip' => 'import all structure types (e.g. Citadels) from ESI. Runtime: ~15s', 'subCount' => [ 'countBuild' => $typesCountStructure, 'countAll' => array_reduce(array_map('count', Universe\CategoryModel::getUniverseCategoryTypes(Config::ESI_CATEGORY_STRUCTURE_ID)), $sum, 0), ] ], 'Ships' => [ 'task' => [ [ 'action' => 'buildIndex', 'label' => 'Import', 'icon' => 'fa-sync', 'btn' => 'btn-primary' ] ], 'label' => 'Ships data', 'countBuild' => $groupsCountShip, 'countAll' => count(Universe\CategoryModel::getUniverseCategoryGroups(Config::ESI_CATEGORY_SHIP_ID)), 'tooltip' => 'import all ships from ESI. Runtime: ~2min', 'subCount' => [ 'countBuild' => $typesCountShip, 'countAll' => array_reduce(array_map('count', Universe\CategoryModel::getUniverseCategoryTypes(Config::ESI_CATEGORY_SHIP_ID)), $sum, 0), ] ], 'SystemStatic' => [ 'task' => [ [ 'action' => 'buildIndex', 'label' => 'Import', 'icon' => 'fa-sync', 'btn' => 'btn-primary' ] ], 'label' => 'Wormhole statics data', 'countBuild' => $systemStaticModel->getRowCount(), 'countAll' => 3772, 'tooltip' => 'import all static wormholes for systems. Runtime: ~25s' ], [ 'label' => 'Build search index', 'icon' => 'fa-search', 'tooltip' => 'Search indexes are build from static EVE universe data (e.g. systems, stargate connections,…). Re-build if underlying data was updated.' ], 'Systems' => [ 'task' => [ [ 'action' => 'clearIndex', 'label' => 'Clear', 'icon' => 'fa-trash', 'btn' => 'btn-danger' ],[ 'action' => 'buildIndex', 'label' => 'Build', 'icon' => 'fa-sync', 'btn' => 'btn-primary' ] ], 'label' => 'Systems data index', 'countBuild' => count($universeController->getSystemsIndex()), 'countAll' => $systemCountAll, 'tooltip' => 'Build up a static search index over all systems, found on DB. Runtime: ~5min' ], 'SystemNeighbour' => [ 'task' => [ [ 'action' => 'clearIndex', 'label' => 'Clear', 'icon' => 'fa-trash', 'btn' => 'btn-danger' ],[ 'action' => 'buildIndex', 'label' => 'Build', 'icon' => 'fa-sync', 'btn' => 'btn-primary' ] ], 'label' => 'Systems neighbour index', 'countBuild' => $systemNeighbourModel->getRowCount(), 'countAll' => (int)$f3->get('REQUIREMENTS.DATA.NEIGHBOURS'), 'tooltip' => 'Build up a static search index for route search. This is used as fallback in case ESI is down. Runtime: ~10s' ] ]; }else{ $indexInfo = [ [ 'label' => 'Fix database errors first!', 'class' => 'txt-color-danger text-center' ] ]; } return $indexInfo; } /** * import table data from existing dump file (e.g *.csv) * @param string $modelClass * @return bool * @throws \Exception */ protected function importTable($modelClass){ $this->getDB('PF'); return Pathfinder\AbstractPathfinderModel::getNew($modelClass)->importData(); } /** * export table data * @param string $modelClass * @throws \Exception */ protected function exportTable($modelClass){ $this->getDB('PF'); Pathfinder\AbstractPathfinderModel::getNew($modelClass)->exportData(); } /** * get cache folder size * @param \Base $f3 * @return array */ protected function checkDirSize(\Base $f3) : array { // limit shown cache size. Reduce page load on big cache. In Bytes $maxBytes = 10 * 1024 * 1024; // 10MB $dirTemp = (string)$f3->get('TEMP'); $cacheDsn = (string)$f3->get('CACHE'); Config::parseDSN($cacheDsn, $conf); // if 'CACHE' is e.g. redis=... -> show default dir for cache $dirCache = $conf['type'] == 'folder' ? $conf['folder'] : $dirTemp . 'cache/'; $dirAll = [ 'TEMP' => [ 'label' => 'Temp dir', 'path' => $dirTemp ], 'CACHE' => [ 'label' => 'Cache dir', 'path' => $dirCache ] ]; $maxHitAll = false; $bytesAll = 0; foreach($dirAll as $key => $dirData){ $maxHit = false; $bytes = 0; $files = Search::getFilesByMTime($dirData['path']); foreach($files as $filename => $file) { $bytes += $file->getSize(); if($bytes > $maxBytes){ $maxHit = $maxHitAll = true; break; } } $bytesAll += $bytes; $dirAll[$key]['size'] = ($maxHit ? '>' : '') . Number::instance()->bytesToString($bytes); $dirAll[$key]['task'] = [ [ 'action' => http_build_query([ 'action' => 'clearFiles', 'path' => $dirData['path'] ]), 'label' => 'Delete files', 'icon' => 'fa-trash', 'btn' => 'btn-danger' . (($bytes > 0) ? '' : ' disabled') ] ]; } return [ 'sizeAll' => ($maxHitAll ? '>' : '') . Number::instance()->bytesToString($bytesAll), 'dirAll' => $dirAll ]; } /** * clear directory * @param string $path */ protected function clearFiles(string $path){ $files = Search::getFilesByMTime($path); foreach($files as $filename => $file){ /** * @var $file \SplFileInfo */ if($file->isFile()){ if($file->isWritable()){ unlink($file->getRealPath()); } } } } /** * clear all key in a specific Redis database * @param string $host * @param int $port * @param int $db */ protected function flushRedisDb(string $host, int $port, int $db = 0){ $client = new \Redis(); $client->pconnect($host, $port, 0.3); $client->select($db); $client->flushDB(); $client->close(); } /** * clear all character authentication (Cookie) data * @param \Base $f3 * @throws \Exception */ protected function invalidateCookies(\Base $f3){ $this->getDB('PF'); $authenticationModel = Pathfinder\AbstractPathfinderModel::getNew('CharacterAuthenticationModel'); $results = $authenticationModel->find(); if($results){ foreach($results as $result){ $result->erase(); } } } }